201 lines
4.7 KiB
TypeScript
201 lines
4.7 KiB
TypeScript
import { defineStore } from 'pinia'
|
|
import { ref, computed } from 'vue'
|
|
|
|
export interface OpenedTab {
|
|
id: string
|
|
title: string
|
|
path: string
|
|
icon: string
|
|
timestamp: number
|
|
}
|
|
|
|
export const useLayoutStore = defineStore('layout', () => {
|
|
// Sidebar state
|
|
const sidebarCollapsed = ref(
|
|
localStorage.getItem('sidebar-collapsed') === 'true'
|
|
)
|
|
const mobileMenuOpen = ref(false)
|
|
|
|
// Tabs state
|
|
const openedTabs = ref<OpenedTab[]>([])
|
|
const activeTabId = ref<string | null>(null)
|
|
const maxTabs = ref(8) // Maximum number of open tabs
|
|
|
|
// Notifications
|
|
const notifications = ref<Array<{
|
|
id: string
|
|
type: 'success' | 'error' | 'warning' | 'info'
|
|
message: string
|
|
timestamp: number
|
|
autoClose?: boolean
|
|
duration?: number
|
|
}>>([])
|
|
|
|
// Theme
|
|
const isDarkMode = ref(
|
|
localStorage.getItem('theme') === 'dark' ||
|
|
(typeof window !== 'undefined' && typeof window.matchMedia === 'function' && window.matchMedia('(prefers-color-scheme: dark)').matches)
|
|
)
|
|
|
|
// Save sidebar state
|
|
const toggleSidebar = () => {
|
|
sidebarCollapsed.value = !sidebarCollapsed.value
|
|
localStorage.setItem('sidebar-collapsed', String(sidebarCollapsed.value))
|
|
}
|
|
|
|
// Tab management
|
|
const addTab = (tab: Omit<OpenedTab, 'timestamp'>) => {
|
|
// Standardize tab ID based on path to prevent duplicates
|
|
const tabId = tab.id || `tab-${tab.path}`
|
|
const existing = openedTabs.value.find(t => t.path === tab.path)
|
|
if (existing) {
|
|
activeTabId.value = existing.id
|
|
return
|
|
}
|
|
|
|
// Respect max tabs limit (FIFO eviction)
|
|
if (openedTabs.value.length >= maxTabs.value) {
|
|
// Find oldest tab that is NOT currently active if possible
|
|
const oldestIndex = openedTabs.value.findIndex(t => t.id !== activeTabId.value)
|
|
if (oldestIndex !== -1) {
|
|
openedTabs.value.splice(oldestIndex, 1)
|
|
} else {
|
|
openedTabs.value.shift()
|
|
}
|
|
}
|
|
|
|
const newTab: OpenedTab = {
|
|
...tab,
|
|
id: tabId,
|
|
timestamp: Date.now(),
|
|
}
|
|
openedTabs.value.push(newTab)
|
|
activeTabId.value = newTab.id
|
|
saveTabs()
|
|
}
|
|
|
|
const closeTab = (tabId: string) => {
|
|
const index = openedTabs.value.findIndex(t => t.id === tabId)
|
|
if (index === -1) return
|
|
|
|
openedTabs.value.splice(index, 1)
|
|
|
|
// Switch to next tab if closed tab was active
|
|
if (activeTabId.value === tabId) {
|
|
activeTabId.value = openedTabs.value[Math.max(0, index - 1)]?.id || null
|
|
}
|
|
|
|
saveTabs()
|
|
}
|
|
|
|
const closeAllTabs = () => {
|
|
openedTabs.value = []
|
|
activeTabId.value = null
|
|
localStorage.removeItem('opened-tabs')
|
|
}
|
|
|
|
const closeOtherTabs = (tabId: string) => {
|
|
openedTabs.value = openedTabs.value.filter(t => t.id === tabId)
|
|
activeTabId.value = tabId
|
|
saveTabs()
|
|
}
|
|
|
|
const saveTabs = () => {
|
|
localStorage.setItem('opened-tabs', JSON.stringify(openedTabs.value))
|
|
}
|
|
|
|
const loadTabs = () => {
|
|
const saved = localStorage.getItem('opened-tabs')
|
|
if (saved) {
|
|
try {
|
|
openedTabs.value = JSON.parse(saved)
|
|
if (openedTabs.value.length > 0) {
|
|
activeTabId.value = openedTabs.value[0].id
|
|
}
|
|
} catch {
|
|
openedTabs.value = []
|
|
}
|
|
}
|
|
}
|
|
|
|
// Notification management
|
|
const addNotification = (
|
|
message: string,
|
|
type: 'success' | 'error' | 'warning' | 'info' = 'info',
|
|
duration = 3000
|
|
) => {
|
|
const id = `notif-${Date.now()}-${Math.random()}`
|
|
const notif = {
|
|
id,
|
|
type,
|
|
message,
|
|
timestamp: Date.now(),
|
|
autoClose: true,
|
|
duration,
|
|
}
|
|
notifications.value.push(notif)
|
|
|
|
if (duration > 0) {
|
|
setTimeout(() => {
|
|
removeNotification(id)
|
|
}, duration)
|
|
}
|
|
}
|
|
|
|
const removeNotification = (id: string) => {
|
|
const index = notifications.value.findIndex(n => n.id === id)
|
|
if (index !== -1) {
|
|
notifications.value.splice(index, 1)
|
|
}
|
|
}
|
|
|
|
// Theme management
|
|
const setDarkMode = (dark: boolean) => {
|
|
isDarkMode.value = dark
|
|
localStorage.setItem('theme', dark ? 'dark' : 'light')
|
|
const root = document.documentElement
|
|
root.setAttribute('data-theme', dark ? 'dark' : 'light')
|
|
}
|
|
|
|
const toggleTheme = () => {
|
|
setDarkMode(!isDarkMode.value)
|
|
}
|
|
|
|
// Computed
|
|
const hasOpenedTabs = computed(() => openedTabs.value.length > 0)
|
|
|
|
// Load persisted state
|
|
loadTabs()
|
|
if (isDarkMode.value) {
|
|
const root = document.documentElement
|
|
root.setAttribute('data-theme', 'dark')
|
|
}
|
|
|
|
return {
|
|
// Sidebar
|
|
sidebarCollapsed,
|
|
mobileMenuOpen,
|
|
toggleSidebar,
|
|
|
|
// Tabs
|
|
openedTabs,
|
|
activeTabId,
|
|
hasOpenedTabs,
|
|
addTab,
|
|
closeTab,
|
|
closeAllTabs,
|
|
closeOtherTabs,
|
|
saveTabs,
|
|
|
|
// Notifications
|
|
notifications,
|
|
addNotification,
|
|
removeNotification,
|
|
|
|
// Theme
|
|
isDarkMode,
|
|
setDarkMode,
|
|
toggleTheme,
|
|
}
|
|
})
|