split charts into separate routes (#1605)

This commit is contained in:
pa
2026-02-03 00:07:09 +09:00
parent 1cbafbeaeb
commit bbbb79eaca
9 changed files with 290 additions and 265 deletions
+44 -4
View File
@@ -374,7 +374,14 @@
items: ['friend-log', 'friend-list', 'moderation'] items: ['friend-log', 'friend-list', 'moderation']
}, },
{ type: 'item', key: 'notification' }, { type: 'item', key: 'notification' },
{ type: 'item', key: 'charts' }, {
type: 'folder',
id: 'default-folder-charts',
nameKey: 'nav_tooltip.charts',
name: t('nav_tooltip.charts'),
icon: 'ri-pie-chart-line',
items: ['charts-instance', 'charts-mutual']
},
{ type: 'item', key: 'tools' }, { type: 'item', key: 'tools' },
{ type: 'item', key: 'direct-access' } { type: 'item', key: 'direct-access' }
]; ];
@@ -504,6 +511,7 @@
const sanitizeLayout = (layout) => { const sanitizeLayout = (layout) => {
const usedKeys = new Set(); const usedKeys = new Set();
const normalized = []; const normalized = [];
const chartsKeys = ['charts-instance', 'charts-mutual'];
const appendItemEntry = (key, target = normalized) => { const appendItemEntry = (key, target = normalized) => {
if (!key || usedKeys.has(key) || !navDefinitionMap.has(key)) { if (!key || usedKeys.has(key) || !navDefinitionMap.has(key)) {
@@ -513,9 +521,31 @@
usedKeys.add(key); usedKeys.add(key);
}; };
const appendChartsFolder = (target = normalized) => {
if (chartsKeys.some((key) => usedKeys.has(key))) {
return;
}
if (!chartsKeys.every((key) => navDefinitionMap.has(key))) {
return;
}
chartsKeys.forEach((key) => usedKeys.add(key));
target.push({
type: 'folder',
id: 'default-folder-charts',
nameKey: 'nav_tooltip.charts',
name: t('nav_tooltip.charts'),
icon: 'ri-pie-chart-line',
items: [...chartsKeys]
});
};
if (Array.isArray(layout)) { if (Array.isArray(layout)) {
layout.forEach((entry) => { layout.forEach((entry) => {
if (entry?.type === 'item') { if (entry?.type === 'item') {
if (entry.key === 'charts') {
appendChartsFolder();
return;
}
appendItemEntry(entry.key); appendItemEntry(entry.key);
return; return;
} }
@@ -550,11 +580,17 @@
navDefinitions.forEach((item) => { navDefinitions.forEach((item) => {
if (!usedKeys.has(item.key)) { if (!usedKeys.has(item.key)) {
normalized.push({ type: 'item', key: item.key }); if (chartsKeys.includes(item.key)) {
usedKeys.add(item.key); return;
}
appendItemEntry(item.key);
} }
}); });
if (!chartsKeys.some((key) => usedKeys.has(key))) {
appendChartsFolder();
}
return normalized; return normalized;
}; };
@@ -670,7 +706,11 @@
console.error('Failed to load custom nav', error); console.error('Failed to load custom nav', error);
} finally { } finally {
const fallbackLayout = layoutData?.length ? layoutData : createDefaultNavLayout(); const fallbackLayout = layoutData?.length ? layoutData : createDefaultNavLayout();
navLayout.value = sanitizeLayout(fallbackLayout); const sanitized = sanitizeLayout(fallbackLayout);
navLayout.value = sanitized;
if (layoutData?.length && JSON.stringify(sanitized) !== JSON.stringify(fallbackLayout)) {
await saveNavLayout(sanitized);
}
navLayoutReady.value = true; navLayoutReady.value = true;
navigateToFirstNavEntry(); navigateToFirstNavEntry();
} }
+16 -5
View File
@@ -2,8 +2,6 @@ import { createRouter, createWebHashHistory } from 'vue-router';
import { watchState } from './../service/watchState'; import { watchState } from './../service/watchState';
import MainLayout from '../views/Layout/MainLayout.vue';
import Charts from './../views/Charts/Charts.vue';
import FavoritesAvatar from './../views/Favorites/FavoritesAvatar.vue'; import FavoritesAvatar from './../views/Favorites/FavoritesAvatar.vue';
import FavoritesFriend from './../views/Favorites/FavoritesFriend.vue'; import FavoritesFriend from './../views/Favorites/FavoritesFriend.vue';
import FavoritesWorld from './../views/Favorites/FavoritesWorld.vue'; import FavoritesWorld from './../views/Favorites/FavoritesWorld.vue';
@@ -11,16 +9,17 @@ import Feed from './../views/Feed/Feed.vue';
import FriendList from './../views/FriendList/FriendList.vue'; import FriendList from './../views/FriendList/FriendList.vue';
import FriendLog from './../views/FriendLog/FriendLog.vue'; import FriendLog from './../views/FriendLog/FriendLog.vue';
import FriendsLocations from './../views/FriendsLocations/FriendsLocations.vue'; import FriendsLocations from './../views/FriendsLocations/FriendsLocations.vue';
import Gallery from './../views/Tools/Gallery.vue';
import GameLog from './../views/GameLog/GameLog.vue'; import GameLog from './../views/GameLog/GameLog.vue';
import Login from './../views/Login/Login.vue'; import Login from './../views/Login/Login.vue';
import MainLayout from '../views/Layout/MainLayout.vue';
import Moderation from './../views/Moderation/Moderation.vue'; import Moderation from './../views/Moderation/Moderation.vue';
import Notification from './../views/Notifications/Notification.vue'; import Notification from './../views/Notifications/Notification.vue';
import PlayerList from './../views/PlayerList/PlayerList.vue'; import PlayerList from './../views/PlayerList/PlayerList.vue';
import ScreenshotMetadata from './../views/Tools/ScreenshotMetadata.vue';
import Search from './../views/Search/Search.vue'; import Search from './../views/Search/Search.vue';
import Settings from './../views/Settings/Settings.vue'; import Settings from './../views/Settings/Settings.vue';
import Tools from './../views/Tools/Tools.vue'; import Tools from './../views/Tools/Tools.vue';
import Gallery from './../views/Tools/Gallery.vue';
import ScreenshotMetadata from './../views/Tools/ScreenshotMetadata.vue';
const routes = [ const routes = [
{ {
@@ -82,7 +81,19 @@ const routes = [
{ {
path: 'charts', path: 'charts',
name: 'charts', name: 'charts',
component: Charts redirect: { name: 'charts-instance' }
},
{
path: 'charts/instance',
name: 'charts-instance',
component: () =>
import('./../views/Charts/components/InstanceActivity.vue')
},
{
path: 'charts/mutual',
name: 'charts-mutual',
component: () =>
import('./../views/Charts/components/MutualFriends.vue')
}, },
{ path: 'tools', name: 'tools', component: Tools }, { path: 'tools', name: 'tools', component: Tools },
{ {
+12 -5
View File
@@ -84,11 +84,18 @@ const navDefinitions = [
routeName: 'notification' routeName: 'notification'
}, },
{ {
key: 'charts', key: 'charts-instance',
icon: 'ri-bar-chart-line', icon: 'ri-bar-chart-horizontal-line',
tooltip: 'nav_tooltip.charts', tooltip: 'view.charts.instance_activity.header',
labelKey: 'nav_tooltip.charts', labelKey: 'view.charts.instance_activity.header',
routeName: 'charts' routeName: 'charts-instance'
},
{
key: 'charts-mutual',
icon: 'ri-group-2-line',
tooltip: 'view.charts.mutual_friend.tab_label',
labelKey: 'view.charts.mutual_friend.tab_label',
routeName: 'charts-mutual'
}, },
{ {
key: 'tools', key: 'tools',
+1 -3
View File
@@ -1,4 +1,4 @@
import { computed, reactive, ref, watch } from 'vue'; import { computed, reactive, watch } from 'vue';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { toast } from 'vue-sonner'; import { toast } from 'vue-sonner';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
@@ -16,7 +16,6 @@ export const useChartsStore = defineStore('Charts', () => {
const { t } = useI18n(); const { t } = useI18n();
const activeTab = ref('instance');
const mutualGraphFetchState = reactive(createDefaultFetchState()); const mutualGraphFetchState = reactive(createDefaultFetchState());
const mutualGraphStatus = reactive({ const mutualGraphStatus = reactive({
isFetching: false, isFetching: false,
@@ -94,7 +93,6 @@ export const useChartsStore = defineStore('Charts', () => {
} }
return { return {
activeTab,
mutualGraphFetchState, mutualGraphFetchState,
mutualGraphStatus, mutualGraphStatus,
resetMutualGraphState resetMutualGraphState
+7 -5
View File
@@ -97,11 +97,13 @@ export const useAppearanceSettingsStore = defineStore(
const isNavCollapsed = ref(true); const isNavCollapsed = ref(true);
const isSideBarTabShow = computed(() => { const isSideBarTabShow = computed(() => {
const currentRouteName = router.currentRoute.value?.name; const currentRouteName = router.currentRoute.value?.name;
return !( return ![
currentRouteName === 'friends-locations' || 'friends-locations',
currentRouteName === 'friend-list' || 'friend-list',
currentRouteName === 'charts' 'charts',
); 'charts-instance',
'charts-mutual'
].includes(currentRouteName);
}); });
const isDataTableStriped = ref(false); const isDataTableStriped = ref(false);
-41
View File
@@ -1,41 +0,0 @@
<template>
<div id="chart" class="x-container">
<TabsUnderline v-model="activeTab" :items="chartTabs" :unmount-on-hide="false" class="charts-tabs">
<template #instance>
<InstanceActivity />
</template>
<template #mutual>
<MutualFriends />
</template>
</TabsUnderline>
<BackToTop target="#chart" :right="30" :bottom="30" />
</div>
</template>
<script setup>
import { computed, defineAsyncComponent } from 'vue';
import { TabsUnderline } from '@/components/ui/tabs';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import BackToTop from '@/components/BackToTop.vue';
import { useChartsStore } from '../../stores';
const InstanceActivity = defineAsyncComponent(() => import('./components/InstanceActivity.vue'));
const MutualFriends = defineAsyncComponent(() => import('./components/MutualFriends.vue'));
const { t } = useI18n();
const chartsStore = useChartsStore();
const { activeTab } = storeToRefs(chartsStore);
const chartTabs = computed(() => [
{ value: 'instance', label: t('view.charts.instance_activity.header') },
{ value: 'mutual', label: t('view.charts.mutual_friend.tab_label') }
]);
</script>
<style scoped>
:deep(.charts-tabs [data-slot='tabs-list']) {
margin: 0;
}
</style>
@@ -1,4 +1,5 @@
<template> <template>
<div id="chart" class="x-container">
<div ref="instanceActivityRef" class="pt-12"> <div ref="instanceActivityRef" class="pt-12">
<BackToTop :target="instanceActivityRef" :right="30" :bottom="30" :teleport="false" /> <BackToTop :target="instanceActivityRef" :right="30" :bottom="30" :teleport="false" />
<div class="options-container instance-activity" style="margin-top: 0"> <div class="options-container instance-activity" style="margin-top: 0">
@@ -33,7 +34,9 @@
<Popover> <Popover>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<div> <div>
<TooltipWrapper :content="t('view.charts.instance_activity.settings.header')" side="top"> <TooltipWrapper
:content="t('view.charts.instance_activity.settings.header')"
side="top">
<Button class="rounded-full" size="icon" variant="ghost" style="margin-right: 5px"> <Button class="rounded-full" size="icon" variant="ghost" style="margin-right: 5px">
<Settings /> <Settings />
</Button> </Button>
@@ -57,7 +60,8 @@
<Switch <Switch
v-model="isDetailVisible" v-model="isDetailVisible"
@update:modelValue=" @update:modelValue="
(value) => changeIsDetailInstanceVisible(value, () => handleSettingsChange()) (value) =>
changeIsDetailInstanceVisible(value, () => handleSettingsChange())
" /> " />
</div> </div>
<div v-if="isDetailVisible"> <div v-if="isDetailVisible">
@@ -69,11 +73,14 @@
" /> " />
</div> </div>
<div v-if="isDetailVisible"> <div v-if="isDetailVisible">
<span>{{ t('view.charts.instance_activity.settings.show_no_friend_instance') }}</span> <span>{{
t('view.charts.instance_activity.settings.show_no_friend_instance')
}}</span>
<Switch <Switch
v-model="isNoFriendInstanceVisible" v-model="isNoFriendInstanceVisible"
@update:modelValue=" @update:modelValue="
(value) => changeIsNoFriendInstanceVisible(value, () => handleSettingsChange()) (value) =>
changeIsNoFriendInstanceVisible(value, () => handleSettingsChange())
" /> " />
</div> </div>
</div> </div>
@@ -157,9 +164,12 @@
:bar-width="barWidth" /> :bar-width="barWidth" />
</template> </template>
</div> </div>
</div>
</template> </template>
<script setup> <script setup>
defineOptions({ name: 'ChartsInstance' });
import { computed, nextTick, onBeforeMount, onBeforeUnmount, onMounted, ref, watch } from 'vue'; import { computed, nextTick, onBeforeMount, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { ArrowLeft, ArrowRight, Calendar as CalendarIcon, Info, RefreshCcw, Settings } from 'lucide-vue-next'; import { ArrowLeft, ArrowRight, Calendar as CalendarIcon, Info, RefreshCcw, Settings } from 'lucide-vue-next';
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card'; import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card';
+12 -14
View File
@@ -1,5 +1,8 @@
<template> <template>
<div class="mt-0 flex min-h-[calc(100vh-140px)] flex-col items-center justify-betweenpt-12" ref="mutualGraphRef"> <div id="chart" class="x-container">
<div
class="mt-0 flex min-h-[calc(100vh-140px)] flex-col items-center justify-betweenpt-12"
ref="mutualGraphRef">
<div class="flex items-center w-full"> <div class="flex items-center w-full">
<div <div
class="options-container mt-2 mb-0 flex flex-wrap items-center gap-3 bg-transparent px-0 pb-2 shadow-none"> class="options-container mt-2 mb-0 flex flex-wrap items-center gap-3 bg-transparent px-0 pb-2 shadow-none">
@@ -46,9 +49,13 @@
</EmptyHeader> </EmptyHeader>
</Empty> </Empty>
</div> </div>
<BackToTop target="#chart" :right="30" :bottom="30" />
</div>
</template> </template>
<script setup> <script setup>
defineOptions({ name: 'ChartsMutual' });
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'; import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { Empty, EmptyDescription, EmptyHeader } from '@/components/ui/empty'; import { Empty, EmptyDescription, EmptyHeader } from '@/components/ui/empty';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
@@ -60,6 +67,7 @@
import { toast } from 'vue-sonner'; import { toast } from 'vue-sonner';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import BackToTop from '@/components/BackToTop.vue';
import Graph from 'graphology'; import Graph from 'graphology';
import Sigma from 'sigma'; import Sigma from 'sigma';
import forceAtlas2 from 'graphology-layout-forceatlas2'; import forceAtlas2 from 'graphology-layout-forceatlas2';
@@ -86,7 +94,6 @@
const appearanceStore = useAppearanceSettingsStore(); const appearanceStore = useAppearanceSettingsStore();
const { friends } = storeToRefs(friendStore); const { friends } = storeToRefs(friendStore);
const { currentUser } = storeToRefs(userStore); const { currentUser } = storeToRefs(userStore);
const { activeTab } = storeToRefs(chartsStore);
const { isDarkMode } = storeToRefs(appearanceStore); const { isDarkMode } = storeToRefs(appearanceStore);
const cachedUsers = userStore.cachedUsers; const cachedUsers = userStore.cachedUsers;
const showUserDialog = (userId) => userStore.showUserDialog(userId); const showUserDialog = (userId) => userStore.showUserDialog(userId);
@@ -201,21 +208,12 @@
if (mutualGraphResizeObserver) mutualGraphResizeObserver.disconnect(); if (mutualGraphResizeObserver) mutualGraphResizeObserver.disconnect();
}); });
watch(
activeTab,
(tab) => {
if (tab === 'mutual') loadGraphFromDatabase();
},
{ immediate: true }
);
watch( watch(
() => watchState.isFriendsLoaded, () => watchState.isFriendsLoaded,
(isFriendsLoaded) => { (isFriendsLoaded) => {
if (isFriendsLoaded && activeTab.value === 'mutual') { if (isFriendsLoaded) loadGraphFromDatabase();
loadGraphFromDatabase(); },
} { immediate: true }
}
); );
function showStatusMessage(message, type = 'info') { function showStatusMessage(message, type = 'info') {
+1 -1
View File
@@ -23,7 +23,7 @@
<template #default="{ layout }"> <template #default="{ layout }">
<ResizablePanel :default-size="mainDefaultSize" :order="1"> <ResizablePanel :default-size="mainDefaultSize" :order="1">
<RouterView v-slot="{ Component }"> <RouterView v-slot="{ Component }">
<KeepAlive exclude="Charts"> <KeepAlive exclude="ChartsInstance, ChartsMutual">
<component :is="Component" /> <component :is="Component" />
</KeepAlive> </KeepAlive>
</RouterView> </RouterView>