Initial vue frontend

This commit is contained in:
2025-08-12 01:05:59 +02:00
parent 64e50027ca
commit 58e0c10b4e
70 changed files with 16958 additions and 0 deletions

View 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
}
})

View 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
}
})

View 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
}
})