diff --git a/package-lock.json b/package-lock.json index 55f1bcd1..6be1ebbb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,6 +45,7 @@ "pug": "^3.0.3", "pug-plain-loader": "^1.1.0", "raw-loader": "^4.0.2", + "remixicon": "^4.6.0", "sass": "^1.90.0", "sass-loader": "^16.0.5", "vue": "^2.7.16", @@ -17224,6 +17225,13 @@ "node": ">= 0.10" } }, + "node_modules/remixicon": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/remixicon/-/remixicon-4.6.0.tgz", + "integrity": "sha512-bKM5odjqE1yzVxEZGJE7F79WHhNrJFIKHXR+GG+P1IWXn8AnJZhl8SbIRDJsNAvIqx4VPkNwjuHfc42tutMDpQ==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/remove-trailing-separator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", diff --git a/package.json b/package.json index dae72bdb..850c30aa 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "pug": "^3.0.3", "pug-plain-loader": "^1.1.0", "raw-loader": "^4.0.2", + "remixicon": "^4.6.0", "sass": "^1.90.0", "sass-loader": "^16.0.5", "vue": "^2.7.16", diff --git a/src/App.vue b/src/App.vue index 4f6c3d1c..db00e95c 100644 --- a/src/App.vue +++ b/src/App.vue @@ -19,6 +19,7 @@ import Notification from './views/Notifications/Notification.vue'; import FriendList from './views/FriendList/FriendList.vue'; import Charts from './views/Charts/Charts.vue'; + import Tools from './views/Tools/Tools.vue'; import Profile from './views/Profile/Profile.vue'; import Settings from './views/Settings/Settings.vue'; @@ -59,6 +60,7 @@ Notification, FriendList, Charts, + Tools, Profile, Settings, diff --git a/src/app.pug b/src/app.pug index 3d706ff2..29727f78 100644 --- a/src/app.pug +++ b/src/app.pug @@ -33,6 +33,8 @@ doctype html Charts + Tools + Profile Settings diff --git a/src/bootstrap.js b/src/bootstrap.js index 1638b264..994f1ae2 100644 --- a/src/bootstrap.js +++ b/src/bootstrap.js @@ -3,6 +3,8 @@ import '@fontsource/noto-sans-jp'; import '@fontsource/noto-sans-sc'; import '@fontsource/noto-sans-tc'; +import 'remixicon/fonts/remixicon.css'; + import Vue from 'vue'; import { PiniaVuePlugin } from 'pinia'; import { DataTables } from 'vue-data-tables'; diff --git a/src/components/NavMenu.vue b/src/components/NavMenu.vue index 1672ef94..b9d9c84b 100644 --- a/src/components/NavMenu.vue +++ b/src/components/NavMenu.vue @@ -42,18 +42,19 @@ import { useUiStore, useVRCXUpdaterStore } from '../stores'; const navItems = [ - { index: 'feed', icon: 'el-icon-news', tooltip: 'nav_tooltip.feed' }, - { index: 'gameLog', icon: 'el-icon-s-data', tooltip: 'nav_tooltip.game_log' }, - { index: 'playerList', icon: 'el-icon-tickets', tooltip: 'nav_tooltip.player_list' }, - { index: 'search', icon: 'el-icon-search', tooltip: 'nav_tooltip.search' }, - { index: 'favorite', icon: 'el-icon-star-off', tooltip: 'nav_tooltip.favorites' }, - { index: 'friendLog', icon: 'el-icon-notebook-2', tooltip: 'nav_tooltip.friend_log' }, - { index: 'moderation', icon: 'el-icon-finished', tooltip: 'nav_tooltip.moderation' }, - { index: 'notification', icon: 'el-icon-bell', tooltip: 'nav_tooltip.notification' }, - { index: 'friendList', icon: 'el-icon-s-management', tooltip: 'nav_tooltip.friend_list' }, - { index: 'charts', icon: 'el-icon-data-analysis', tooltip: 'nav_tooltip.charts' }, - { index: 'profile', icon: 'el-icon-user', tooltip: 'nav_tooltip.profile' }, - { index: 'settings', icon: 'el-icon-s-tools', tooltip: 'nav_tooltip.settings' } + { index: 'feed', icon: 'ri-rss-line', tooltip: 'nav_tooltip.feed' }, + { index: 'gameLog', icon: 'ri-history-line', tooltip: 'nav_tooltip.game_log' }, + { index: 'playerList', icon: 'ri-group-3-line', tooltip: 'nav_tooltip.player_list' }, + { index: 'search', icon: 'ri-search-line', tooltip: 'nav_tooltip.search' }, + { index: 'favorite', icon: 'ri-star-line', tooltip: 'nav_tooltip.favorites' }, + { index: 'friendLog', icon: 'ri-contacts-book-3-line', tooltip: 'nav_tooltip.friend_log' }, + { index: 'moderation', icon: 'ri-user-forbid-line', tooltip: 'nav_tooltip.moderation' }, + { index: 'notification', icon: 'ri-notification-2-line', tooltip: 'nav_tooltip.notification' }, + { index: 'friendList', icon: 'ri-contacts-book-2-line', tooltip: 'nav_tooltip.friend_list' }, + { index: 'charts', icon: 'ri-bar-chart-line', tooltip: 'nav_tooltip.charts' }, + { index: 'tools', icon: 'ri-tools-line', tooltip: 'nav_tooltip.tools' }, + { index: 'profile', icon: 'ri-user-line', tooltip: 'nav_tooltip.profile' }, + { index: 'settings', icon: 'ri-settings-3-line', tooltip: 'nav_tooltip.settings' } ]; const VRCXUpdaterStore = useVRCXUpdaterStore(); @@ -63,3 +64,16 @@ const { menuActiveIndex, notifiedMenus } = storeToRefs(uiStore); const { selectMenu } = uiStore; + + diff --git a/src/components/dialogs/UserDialog/UserDialog.vue b/src/components/dialogs/UserDialog/UserDialog.vue index 6f2e6bd0..8ce0acb1 100644 --- a/src/components/dialogs/UserDialog/UserDialog.vue +++ b/src/components/dialogs/UserDialog/UserDialog.vue @@ -1890,13 +1890,12 @@ const { friendLogTable } = storeToRefs(useFriendStore()); const { getFriendRequest, handleFriendDelete } = useFriendStore(); const { previousImagesDialogVisible, previousImagesTable } = storeToRefs(useGalleryStore()); - const { clearInviteImageUpload, showGalleryDialog, checkPreviousImageAvailable, showFullscreenImageDialog } = - useGalleryStore(); + const { clearInviteImageUpload, checkPreviousImageAvailable, showFullscreenImageDialog } = useGalleryStore(); const { isGameRunning } = storeToRefs(useGameStore()); const { logout } = useAuthStore(); const { cachedConfig } = storeToRefs(useAuthStore()); const { applyPlayerModeration, handlePlayerModerationDelete } = useModerationStore(); - const { shiftHeld } = storeToRefs(useUiStore()); + const { shiftHeld, menuActiveIndex } = storeToRefs(useUiStore()); watch( () => userDialog.value.loading, @@ -2296,7 +2295,9 @@ } else if (command === 'Previous Instances') { showPreviousInstancesUserDialog(D.ref); } else if (command === 'Manage Gallery') { - showGalleryDialog(); + // redirect to tools tab + userDialog.value.visible = false; + menuActiveIndex.value = 'tools'; } else if (command === 'Invite To Group') { showInviteGroupDialog('', D.id); // } else if (command === 'Send Boop') { diff --git a/src/localization/en/en.json b/src/localization/en/en.json index d8cc10df..83dc066a 100644 --- a/src/localization/en/en.json +++ b/src/localization/en/en.json @@ -12,6 +12,7 @@ "notification": "Notification", "friend_list": "Friend List", "charts": "Charts", + "tools": "Tools", "profile": "Profile", "settings": "Settings" }, diff --git a/src/views/Profile/Profile.vue b/src/views/Profile/Profile.vue index 92233515..1fcad22d 100644 --- a/src/views/Profile/Profile.vue +++ b/src/views/Profile/Profile.vue @@ -529,7 +529,6 @@ inviteRequestMessageTable, inviteRequestResponseMessageTable } = storeToRefs(useInviteStore()); - const { showGalleryDialog } = useGalleryStore(); const { menuActiveIndex } = storeToRefs(useUiStore()); const { directAccessWorld } = useSearchStore(); const { logout } = useAuthStore(); @@ -550,6 +549,11 @@ const visits = ref(0); + // redirect to tools tab + function showGalleryDialog() { + menuActiveIndex.value = 'tools'; + } + function getVisits() { miscRequest.getVisits().then((args) => { visits.value = args.json; diff --git a/src/views/Settings/Settings.vue b/src/views/Settings/Settings.vue index 3e7cc496..7ab203ba 100644 --- a/src/views/Settings/Settings.vue +++ b/src/views/Settings/Settings.vue @@ -628,6 +628,7 @@ :label="t('view.settings.appearance.user_dialog.vrcx_memos')" :value="!hideUserMemos" @change="setHideUserMemos" /> +
{{ t('view.settings.appearance.user_dialog.export_vrcx_memos_into_vrchat_notes') @@ -641,6 +642,7 @@ >{{ t('view.settings.appearance.user_dialog.export_notes') }}
+
@@ -1264,6 +1266,7 @@ +
{{ t('view.settings.category.pictures') }}
@@ -1274,6 +1277,7 @@
+
{{ t('view.settings.pictures.pictures.open_folder') }} @@ -1845,9 +1849,7 @@ - - @@ -1886,9 +1888,7 @@ } from '../../stores'; import { photonEventTableTypeFilterList } from '../../shared/constants'; import OpenSourceSoftwareNoticeDialog from './dialogs/OpenSourceSoftwareNoticeDialog.vue'; - import NoteExportDialog from './dialogs/NoteExportDialog.vue'; import NotificationPositionDialog from './dialogs/NotificationPositionDialog.vue'; - import ScreenshotMetadataDialog from './dialogs/ScreenshotMetadataDialog.vue'; import RegistryBackupDialog from './dialogs/RegistryBackupDialog.vue'; import YouTubeApiDialog from './dialogs/YouTubeApiDialog.vue'; import ChangelogDialog from './dialogs/ChangelogDialog.vue'; @@ -2179,20 +2179,10 @@ ]); const ossDialog = ref(false); - const isNoteExportDialogVisible = ref(false); const feedFiltersDialogMode = ref(''); const isNotificationPositionDialogVisible = ref(false); const isYouTubeApiDialogVisible = ref(false); - const screenshotMetadataDialog = ref({ - visible: false, - loading: false, - search: '', - searchType: 'Player Name', - searchTypes: ['Player Name', 'Player ID', 'World Name', 'World ID'], - metadata: {}, - isUploading: false - }); const zoomLevel = ref(100); @@ -2224,16 +2214,18 @@ feedFiltersDialogMode.value = 'wrist'; } + // redirect to tools tab function showNoteExportDialog() { - isNoteExportDialogVisible.value = true; + menuActiveIndex.value = 'tools'; } function showNotificationPositionDialog() { isNotificationPositionDialogVisible.value = true; } + // redirect to tools tab function showScreenshotMetadataDialog() { - screenshotMetadataDialog.value.visible = true; + menuActiveIndex.value = 'tools'; } function openVrcxAppDataFolder() { diff --git a/src/views/Sidebar/Sidebar.vue b/src/views/Sidebar/Sidebar.vue index 4369806d..642c8f05 100644 --- a/src/views/Sidebar/Sidebar.vue +++ b/src/views/Sidebar/Sidebar.vue @@ -78,11 +78,9 @@ ({{ groupInstances.length }}) - -
@@ -100,7 +98,6 @@ } from '../../stores'; import FriendsSidebar from './components/FriendsSidebar.vue'; import GroupsSidebar from './components/GroupsSidebar.vue'; - import GroupCalendarDialog from '../../components/dialogs/GroupDialog/GroupCalendarDialog.vue'; const { friends, isRefreshFriendsLoading, onlineFriendCount } = storeToRefs(useFriendStore()); const { refreshFriendsList, confirmDeleteFriend } = useFriendStore(); @@ -110,15 +107,9 @@ const { quickSearchItems } = storeToRefs(useSearchStore()); const { inGameGroupOrder, groupInstances } = storeToRefs(useGroupStore()); - const isGroupCalendarDialogVisible = ref(false); - const isSideBarTabShow = computed(() => { return !(menuActiveIndex.value === 'friendList' || menuActiveIndex.value === 'charts'); }); - - function openGroupCalendarDialog() { - isGroupCalendarDialogVisible.value = true; - } diff --git a/src/components/dialogs/GroupDialog/GroupCalendarEventCard.vue b/src/views/Tools/components/GroupCalendarEventCard.vue similarity index 99% rename from src/components/dialogs/GroupDialog/GroupCalendarEventCard.vue rename to src/views/Tools/components/GroupCalendarEventCard.vue index c908d998..51177377 100644 --- a/src/components/dialogs/GroupDialog/GroupCalendarEventCard.vue +++ b/src/views/Tools/components/GroupCalendarEventCard.vue @@ -65,6 +65,7 @@ mode: { type: String, required: true, + // @ts-ignore validator: (value) => ['timeline', 'grid'].includes(value) }, isFollowing: { diff --git a/src/components/dialogs/GroupDialog/GroupCalendarDialog.vue b/src/views/Tools/dialogs/GroupCalendarDialog.vue similarity index 86% rename from src/components/dialogs/GroupDialog/GroupCalendarDialog.vue rename to src/views/Tools/dialogs/GroupCalendarDialog.vue index e2459d17..38eb265d 100644 --- a/src/components/dialogs/GroupDialog/GroupCalendarDialog.vue +++ b/src/views/Tools/dialogs/GroupCalendarDialog.vue @@ -49,7 +49,7 @@ :class="{ 'has-events': filteredCalendar[formatDateKey(date)]?.length }"> - {{ dayjs(date).format('D') }} + {{ dayjs(date).local().format('D') }}
-
+
+ {{ group.groupName }}
-
+
props.visible, async (newVisible) => { if (newVisible) { + selectedDay.value = new Date(); isLoading.value = true; await Promise.all([getCalendarData(), getFollowingCalendarData()]) .catch((error) => { @@ -172,6 +176,74 @@ } ); + const groupedByGroupEvents = computed(() => { + const currentMonth = dayjs(selectedDay.value).month(); + const currentYear = dayjs(selectedDay.value).year(); + + const currentMonthEvents = calendar.value.filter((event) => { + const eventDate = dayjs(event.startsAt); + return eventDate.month() === currentMonth && eventDate.year() === currentYear; + }); + + const groupMap = new Map(); + currentMonthEvents.forEach((event) => { + const groupId = event.ownerId; + if (!groupMap.has(groupId)) { + groupMap.set(groupId, []); + } + groupMap.get(groupId).push(event); + }); + + Array.from(groupMap.values()).forEach((events) => { + events.sort((a, b) => (dayjs(a.startsAt).isBefore(dayjs(b.startsAt)) ? -1 : 1)); + }); + + return Array.from(groupMap.entries()).map(([groupId, events]) => ({ + groupId, + groupName: getGroupName(events[0]), + events: events + })); + }); + + const filteredGroupEvents = computed(() => { + const hasSearch = searchQuery.value.trim(); + return !hasSearch + ? groupedByGroupEvents.value + : groupedByGroupEvents.value.filter((group) => { + if (group.groupName.toLowerCase().includes(searchQuery.value.toLowerCase())) return true; + + return group.events.some( + (event) => + event.title?.toLowerCase().includes(searchQuery.value.toLowerCase()) || + event.description?.toLowerCase().includes(searchQuery.value.toLowerCase()) + ); + }); + }); + + watch( + [filteredGroupEvents, searchQuery], + ([groups, search]) => { + const newCollapsed = { ...groupCollapsed.value }; + let hasChanged = false; + const hasSearch = search.trim(); + + groups.forEach((group) => { + if (!(group.groupId in newCollapsed)) { + newCollapsed[group.groupId] = false; + hasChanged = true; + } else if (hasSearch) { + newCollapsed[group.groupId] = false; + hasChanged = true; + } + }); + + if (hasChanged) { + groupCollapsed.value = newCollapsed; + } + }, + { immediate: true } + ); + const filteredCalendar = computed(() => { const result = {}; calendar.value.forEach((item) => { @@ -231,49 +303,6 @@ .sort((a, b) => dayjs(a.startsAt).diff(dayjs(b.startsAt))); }); - const groupedByGroupEvents = computed(() => { - const currentMonth = dayjs(selectedDay.value).month(); - const currentYear = dayjs(selectedDay.value).year(); - - const currentMonthEvents = calendar.value.filter((event) => { - const eventDate = dayjs(event.startsAt); - return eventDate.month() === currentMonth && eventDate.year() === currentYear; - }); - - const groupMap = new Map(); - currentMonthEvents.forEach((event) => { - const groupId = event.ownerId; - if (!groupMap.has(groupId)) { - groupMap.set(groupId, []); - } - groupMap.get(groupId).push(event); - }); - - Array.from(groupMap.values()).forEach((events) => { - events.sort((a, b) => (dayjs(a.startsAt).isBefore(dayjs(b.startsAt)) ? -1 : 1)); - }); - - return Array.from(groupMap.entries()).map(([groupId, events]) => ({ - groupId, - groupName: getGroupName(events[0]), - events: events - })); - }); - - const filteredGroupEvents = computed(() => { - if (!searchQuery.value.trim()) return groupedByGroupEvents.value; - - const query = searchQuery.value.toLowerCase(); - return groupedByGroupEvents.value.filter((group) => { - if (group.groupName.toLowerCase().includes(query)) return true; - - return group.events.some( - (event) => - event.title?.toLowerCase().includes(query) || event.description?.toLowerCase().includes(query) - ); - }); - }); - const formatDateKey = (date) => dayjs(date).format('YYYY-MM-DD'); function getGroupName(event) { @@ -315,6 +344,13 @@ viewMode.value = viewMode.value === 'timeline' ? 'grid' : 'timeline'; } + function toggleGroup(groupId) { + groupCollapsed.value = { + ...groupCollapsed.value, + [groupId]: !groupCollapsed.value[groupId] + }; + } + function closeDialog() { emit('close'); } @@ -447,7 +483,7 @@ display: flex; flex-direction: column; .search-container { - padding: 16px 20px; + padding: 2px 20px 12px 20px; border-bottom: 1px solid #ebeef5; display: flex; justify-content: flex-end; @@ -463,22 +499,24 @@ .groups-container { overflow: visible; .group-row { - margin-bottom: 24px; + margin-bottom: 18px; overflow: visible; .group-header { font-size: 16px; font-weight: bold; color: var(--el-text-color-primary); - padding: 8px 12px 12px 12px; - margin-bottom: 16px; + padding: 4px 12px 10px 12px; cursor: pointer; - transition: all 0.3s ease; border-radius: 4px; - margin: 0 -12px 16px -12px; - &:hover { + margin: 0 -12px 10px -12px; + display: flex; + align-items: center; + + .el-icon-arrow-right { + font-size: 14px; + margin-right: 8px; + transition: transform 0.3s; color: var(--el-color-primary); - background-color: var(--el-color-primary-light-9); - transform: translateX(4px); } } .events-row { @@ -499,4 +537,8 @@ } } } + + .rotate { + transform: rotate(90deg); + } diff --git a/src/views/Settings/dialogs/NoteExportDialog.vue b/src/views/Tools/dialogs/NoteExportDialog.vue similarity index 98% rename from src/views/Settings/dialogs/NoteExportDialog.vue rename to src/views/Tools/dialogs/NoteExportDialog.vue index 1eae6b63..2244587e 100644 --- a/src/views/Settings/dialogs/NoteExportDialog.vue +++ b/src/views/Tools/dialogs/NoteExportDialog.vue @@ -139,7 +139,7 @@ errors.value = ''; } - const emit = defineEmits(['update:isNoteExportDialogVisible']); + const emit = defineEmits(['close']); function updateNoteExportDialog() { const data = []; @@ -196,6 +196,6 @@ } function closeDialog() { - emit('update:isNoteExportDialogVisible', false); + emit('close'); } diff --git a/src/views/Settings/dialogs/ScreenshotMetadataDialog.vue b/src/views/Tools/dialogs/ScreenshotMetadataDialog.vue similarity index 94% rename from src/views/Settings/dialogs/ScreenshotMetadataDialog.vue rename to src/views/Tools/dialogs/ScreenshotMetadataDialog.vue index f58c75ef..35b6e396 100644 --- a/src/views/Settings/dialogs/ScreenshotMetadataDialog.vue +++ b/src/views/Tools/dialogs/ScreenshotMetadataDialog.vue @@ -1,12 +1,12 @@