Initial vue frontend
This commit is contained in:
178
frontend-vue/src/stores/app.ts
Normal file
178
frontend-vue/src/stores/app.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { get, set } from 'idb-keyval'
|
||||
import type { Channel, ExtendedMessage, UnsentMessage, AppSettings } from '@/types'
|
||||
|
||||
export const useAppStore = defineStore('app', () => {
|
||||
// State
|
||||
const channels = ref<Channel[]>([])
|
||||
const currentChannelId = ref<number | null>(null)
|
||||
const messages = ref<Record<number, ExtendedMessage[]>>({})
|
||||
const unsentMessages = ref<UnsentMessage[]>([])
|
||||
const settings = ref<AppSettings>({
|
||||
soundEnabled: true,
|
||||
speechEnabled: true,
|
||||
ttsEnabled: true,
|
||||
ttsRate: 1,
|
||||
ttsPitch: 1,
|
||||
ttsVolume: 1,
|
||||
selectedVoiceURI: null,
|
||||
defaultChannelId: null,
|
||||
theme: 'auto'
|
||||
})
|
||||
|
||||
// Computed
|
||||
const currentChannel = computed(() =>
|
||||
channels.value.find(c => c.id === currentChannelId.value) || null
|
||||
)
|
||||
|
||||
const currentMessages = computed(() => {
|
||||
const channelId = currentChannelId.value
|
||||
const channelMessages = channelId ? messages.value[channelId] || [] : []
|
||||
return channelMessages
|
||||
})
|
||||
|
||||
const unsentMessagesForChannel = computed(() =>
|
||||
currentChannelId.value
|
||||
? unsentMessages.value.filter(msg => msg.channelId === currentChannelId.value)
|
||||
: []
|
||||
)
|
||||
|
||||
// Actions
|
||||
const setChannels = (newChannels: Channel[]) => {
|
||||
channels.value = newChannels
|
||||
}
|
||||
|
||||
const addChannel = (channel: Channel) => {
|
||||
channels.value.push(channel)
|
||||
messages.value[channel.id] = []
|
||||
}
|
||||
|
||||
const removeChannel = (channelId: number) => {
|
||||
channels.value = channels.value.filter(c => c.id !== channelId)
|
||||
delete messages.value[channelId]
|
||||
if (currentChannelId.value === channelId) {
|
||||
currentChannelId.value = channels.value[0]?.id || null
|
||||
}
|
||||
}
|
||||
|
||||
const setCurrentChannel = async (channelId: number | null) => {
|
||||
currentChannelId.value = channelId
|
||||
await set('current_channel_id', channelId)
|
||||
}
|
||||
|
||||
const setMessages = (channelId: number, channelMessages: ExtendedMessage[]) => {
|
||||
console.log('Store: Setting messages for channel', channelId, ':', channelMessages.length, 'messages')
|
||||
messages.value[channelId] = channelMessages
|
||||
}
|
||||
|
||||
const addMessage = (message: ExtendedMessage) => {
|
||||
console.log('Store: Adding message to channel', message.channel_id, ':', message)
|
||||
if (!messages.value[message.channel_id]) {
|
||||
messages.value[message.channel_id] = []
|
||||
}
|
||||
messages.value[message.channel_id].push(message)
|
||||
console.log('Store: Messages for channel', message.channel_id, 'now has', messages.value[message.channel_id].length, 'messages')
|
||||
|
||||
// Note: Auto-save is now handled by the sync service to avoid excessive I/O
|
||||
}
|
||||
|
||||
const updateMessage = (messageId: number, updates: Partial<ExtendedMessage>) => {
|
||||
for (const channelId in messages.value) {
|
||||
const channelMessages = messages.value[parseInt(channelId)]
|
||||
const messageIndex = channelMessages.findIndex(m => m.id === messageId)
|
||||
if (messageIndex !== -1) {
|
||||
channelMessages[messageIndex] = { ...channelMessages[messageIndex], ...updates }
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const removeMessage = (messageId: number) => {
|
||||
for (const channelId in messages.value) {
|
||||
const channelMessages = messages.value[parseInt(channelId)]
|
||||
const messageIndex = channelMessages.findIndex(m => m.id === messageId)
|
||||
if (messageIndex !== -1) {
|
||||
channelMessages.splice(messageIndex, 1)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const addUnsentMessage = (message: UnsentMessage) => {
|
||||
unsentMessages.value.push(message)
|
||||
}
|
||||
|
||||
const removeUnsentMessage = (messageId: string) => {
|
||||
const index = unsentMessages.value.findIndex(m => m.id === messageId)
|
||||
if (index !== -1) {
|
||||
unsentMessages.value.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
const updateSettings = async (newSettings: Partial<AppSettings>) => {
|
||||
settings.value = { ...settings.value, ...newSettings }
|
||||
await set('app_settings', settings.value)
|
||||
}
|
||||
|
||||
const loadState = async () => {
|
||||
try {
|
||||
const [storedChannelId, storedMessages, storedUnsentMessages, storedSettings] = await Promise.all([
|
||||
get('current_channel_id'),
|
||||
get('messages'),
|
||||
get('unsent_messages'),
|
||||
get('app_settings')
|
||||
])
|
||||
|
||||
if (storedChannelId) currentChannelId.value = storedChannelId
|
||||
if (storedMessages) messages.value = storedMessages
|
||||
if (storedUnsentMessages) unsentMessages.value = storedUnsentMessages
|
||||
if (storedSettings) settings.value = { ...settings.value, ...storedSettings }
|
||||
} catch (error) {
|
||||
console.error('Failed to load state from storage:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const saveState = async () => {
|
||||
try {
|
||||
// Convert reactive objects to plain objects for IndexedDB
|
||||
await Promise.all([
|
||||
set('current_channel_id', currentChannelId.value),
|
||||
set('messages', JSON.parse(JSON.stringify(messages.value))),
|
||||
set('unsent_messages', JSON.parse(JSON.stringify(unsentMessages.value))),
|
||||
set('app_settings', JSON.parse(JSON.stringify(settings.value)))
|
||||
])
|
||||
} catch (error) {
|
||||
console.error('Failed to save state to storage:', error)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
channels,
|
||||
currentChannelId,
|
||||
messages,
|
||||
unsentMessages,
|
||||
settings,
|
||||
|
||||
// Computed
|
||||
currentChannel,
|
||||
currentMessages,
|
||||
unsentMessagesForChannel,
|
||||
|
||||
// Actions
|
||||
setChannels,
|
||||
addChannel,
|
||||
removeChannel,
|
||||
setCurrentChannel,
|
||||
setMessages,
|
||||
addMessage,
|
||||
updateMessage,
|
||||
removeMessage,
|
||||
addUnsentMessage,
|
||||
removeUnsentMessage,
|
||||
updateSettings,
|
||||
loadState,
|
||||
saveState
|
||||
}
|
||||
})
|
74
frontend-vue/src/stores/auth.ts
Normal file
74
frontend-vue/src/stores/auth.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, readonly } from 'vue'
|
||||
import { get, set } from 'idb-keyval'
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const token = ref<string | null>(null)
|
||||
const isAuthenticated = ref(false)
|
||||
|
||||
const setToken = async (newToken: string) => {
|
||||
token.value = newToken
|
||||
isAuthenticated.value = true
|
||||
await set('auth_token', newToken)
|
||||
}
|
||||
|
||||
const clearAuth = async () => {
|
||||
token.value = null
|
||||
isAuthenticated.value = false
|
||||
await set('auth_token', null)
|
||||
}
|
||||
|
||||
const checkAuth = async () => {
|
||||
try {
|
||||
const storedToken = await get('auth_token')
|
||||
if (storedToken) {
|
||||
// Verify token with backend
|
||||
const baseUrl = import.meta.env.DEV ? 'http://localhost:3000' : ''
|
||||
const response = await fetch(`${baseUrl}/check-token`, {
|
||||
headers: { Authorization: storedToken }
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
token.value = storedToken
|
||||
isAuthenticated.value = true
|
||||
} else {
|
||||
console.warn('Stored token is invalid, clearing auth')
|
||||
await clearAuth()
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Auth check failed:', error)
|
||||
await clearAuth()
|
||||
}
|
||||
}
|
||||
|
||||
const authenticate = async (authToken: string): Promise<boolean> => {
|
||||
try {
|
||||
const baseUrl = import.meta.env.DEV ? 'http://localhost:3000' : ''
|
||||
const response = await fetch(`${baseUrl}/check-token`, {
|
||||
headers: { Authorization: authToken }
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
await setToken(authToken)
|
||||
return true
|
||||
} else {
|
||||
await clearAuth()
|
||||
return false
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Authentication failed:', error)
|
||||
await clearAuth()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
token,
|
||||
isAuthenticated,
|
||||
setToken,
|
||||
clearAuth,
|
||||
checkAuth,
|
||||
authenticate
|
||||
}
|
||||
})
|
52
frontend-vue/src/stores/toast.ts
Normal file
52
frontend-vue/src/stores/toast.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, readonly } from 'vue'
|
||||
import type { ToastMessage } from '@/types'
|
||||
|
||||
export const useToastStore = defineStore('toast', () => {
|
||||
const toasts = ref<ToastMessage[]>([])
|
||||
|
||||
const addToast = (message: string, type: ToastMessage['type'] = 'info', duration = 3000) => {
|
||||
const id = Date.now().toString()
|
||||
const toast: ToastMessage = {
|
||||
id,
|
||||
message,
|
||||
type,
|
||||
duration
|
||||
}
|
||||
|
||||
toasts.value.push(toast)
|
||||
|
||||
if (duration > 0) {
|
||||
setTimeout(() => {
|
||||
removeToast(id)
|
||||
}, duration)
|
||||
}
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
const removeToast = (id: string) => {
|
||||
const index = toasts.value.findIndex(toast => toast.id === id)
|
||||
if (index > -1) {
|
||||
toasts.value.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
const clearToasts = () => {
|
||||
toasts.value = []
|
||||
}
|
||||
|
||||
const success = (message: string, duration?: number) => addToast(message, 'success', duration)
|
||||
const error = (message: string, duration?: number) => addToast(message, 'error', duration)
|
||||
const info = (message: string, duration?: number) => addToast(message, 'info', duration)
|
||||
|
||||
return {
|
||||
toasts,
|
||||
addToast,
|
||||
removeToast,
|
||||
clearToasts,
|
||||
success,
|
||||
error,
|
||||
info
|
||||
}
|
||||
})
|
Reference in New Issue
Block a user