import { computed, ref, shallowRef, watch } from 'vue' import { defineStore } from 'pinia' import { useCollectionsStore } from '@MailManager/stores/collectionsStore' import { useMailStore } from '@/stores/mailStore' import type { ServiceIdentifier, EntityIdentifier } from '@MailManager/types/common' import type { EntityObject, ServiceObject } from '@MailManager/models' import type { CollectionObject } from '@MailManager/models/collection' export const useMailUiStore = defineStore('mailUiStore', () => { const collectionsStore = useCollectionsStore() const mailStore = useMailStore() const sidebarVisible = ref(true) const settingsDialogVisible = ref(false) const composeMode = ref<'new' | 'reply' | 'forward'>('new') const composeSource = shallowRef(null) const composeVisible = ref(false) const selectionMode = ref(false) const selectionList = ref([]) const moveMessagesDialogVisible = ref(false) const moveMessagesDialogService = shallowRef(null) const moveMessagesDialogCandidates = ref(null) const createFolderDialogVisible = ref(false) const createFolderDialogService = shallowRef(null) const createFolderDialogParent = shallowRef(null) const createFolderDialogLoading = ref(false) const createFolderDialogError = ref('') const renameFolderDialogVisible = ref(false) const renameFolderDialogService = shallowRef(null) const renameFolderDialogFolder = shallowRef(null) const renameFolderDialogLoading = ref(false) const renameFolderDialogError = ref('') const moveFolderDialogVisible = ref(false) const moveFolderDialogService = shallowRef(null) const moveFolderDialogSource = shallowRef(null) const deleteFolderDialogVisible = ref(false) const deleteFolderDialogService = shallowRef(null) const deleteFolderDialogFolder = shallowRef(null) const deleteFolderDialogLoading = ref(false) const deleteFolderDialogError = ref('') const createFolderDialogParentLabel = computed(() => { return createFolderDialogParent.value?.properties.label || 'Root' }) const renameFolderDialogParentLabel = computed(() => { const folder = renameFolderDialogFolder.value if (!folder || folder.collection === null || folder.collection === undefined) { return 'Root' } const parent = collectionsStore.collectionsInCollection(folder.provider, folder.service, null) .flatMap(rootFolder => [rootFolder, ...collectionsStore.collectionsForService(folder.provider, folder.service)]) .find(candidate => String(candidate.identifier) === String(folder.collection)) return parent?.properties.label || 'Root' }) const moveFolderDialogInvalidFolderKeys = computed(() => { const sourceFolder = moveFolderDialogSource.value if (!sourceFolder) { return [] } const invalidKeys = new Set() const queue = [sourceFolder] while (queue.length > 0) { const currentFolder = queue.shift() if (!currentFolder) { continue } invalidKeys.add(String(currentFolder.identifier)) collectionsStore .collectionsInCollection(currentFolder.provider, currentFolder.service, currentFolder.identifier) .forEach(childFolder => { queue.push(childFolder) }) } return Array.from(invalidKeys) }) watch( () => mailStore.selectedFolder, () => { closeCompose() deactivateSelectionMode() }, ) watch( () => mailStore.selectedMessage, selectedMessage => { if (selectedMessage) { closeCompose() } }, ) watch( () => mailStore.currentMessages, () => { reconcileSelection() }, ) function validateFolderNameBase(service: ServiceObject, name: string): string[] { const errors: string[] = [] if (!name || name.trim().length === 0) { errors.push('Folder name is required') return errors } if (name.length > 255) { errors.push('Folder name too long (max 255 characters)') } if (/[<>:"|?*\x00-\x1F]/.test(name)) { errors.push('Folder name contains invalid characters') } if (service.provider === 'imap' && /[\/\\]/.test(name)) { errors.push('IMAP folder names cannot contain / or \\') } if (name !== name.trim()) { errors.push('Folder name cannot have leading or trailing spaces') } return errors } function validateCreateFolderName(name: string): string[] { const service = createFolderDialogService.value if (!service || service.identifier === null) { return ['Folder service is unavailable'] } const errors = validateFolderNameBase(service, name) if (errors.length > 0) { return errors } const parentIdentifier = createFolderDialogParent.value?.identifier ?? null const duplicate = collectionsStore .collectionsInCollection(service.provider, service.identifier, parentIdentifier) .some(folder => folder.properties.label === name) if (duplicate) { errors.push('A folder with this name already exists in this location') } return errors } function validateRenameFolderName(name: string): string[] { const service = renameFolderDialogService.value const folder = renameFolderDialogFolder.value if (!service || !folder || service.identifier === null) { return ['Folder service is unavailable'] } const errors = validateFolderNameBase(service, name) if (errors.length > 0) { return errors } const parentIdentifier = folder.collection ?? null const duplicate = collectionsStore .collectionsInCollection(service.provider, service.identifier, parentIdentifier) .some(candidate => String(candidate.identifier) !== String(folder.identifier) && candidate.properties.label === name, ) if (duplicate) { errors.push('A folder with this name already exists in this location') } return errors } function setSelectionList(nextIds: EntityIdentifier[]) { selectionList.value = Array.from(new Set(nextIds)) } function openCompose(source?: EntityObject, mode: 'reply' | 'forward' = 'reply') { mailStore.clearSelectedMessage() composeSource.value = source ?? null composeMode.value = mode composeVisible.value = true } function closeCompose() { composeMode.value = 'new' composeSource.value = null composeVisible.value = false } async function afterSent() { closeCompose() await mailStore.reloadSelectedFolder() } function clearSelection() { setSelectionList([]) } function activateSelectionMode(message?: EntityObject) { selectionMode.value = true if (!message) { return } const identifier = message.identifier if (!selectionList.value.includes(identifier)) { setSelectionList([...selectionList.value, identifier]) } } function deactivateSelectionMode() { selectionMode.value = false clearSelection() } function toggleMessageSelection(message: EntityObject) { const identifier = message.identifier selectionMode.value = true if (selectionList.value.includes(identifier)) { setSelectionList(selectionList.value.filter(selectedId => selectedId !== identifier)) return } setSelectionList([...selectionList.value, identifier]) } function selectAllCurrentMessages() { selectionMode.value = true setSelectionList(mailStore.currentMessages.map(message => message.identifier)) } function reconcileSelection() { if (!mailStore.selectedFolder) { clearSelection() return } const currentMessageIdentifiers = new Set(mailStore.currentMessages.map(message => message.identifier)) const nextSelectedIds = selectionList.value.filter(identifier => currentMessageIdentifiers.has(identifier)) if (nextSelectedIds.length !== selectionList.value.length) { setSelectionList(nextSelectedIds) } if (nextSelectedIds.length === 0) { selectionMode.value = false } } function toggleSidebar() { sidebarVisible.value = !sidebarVisible.value } function closeSidebar() { sidebarVisible.value = false } function openSettings() { settingsDialogVisible.value = true } function closeSettings() { settingsDialogVisible.value = false } async function openMoveMessagesDialog(entities?: EntityObject | EntityObject[]) { let moveMessagesServiceIdentifier = null as ServiceIdentifier | null moveMessagesDialogCandidates.value = [] if (entities) { if (Array.isArray(entities)) { moveMessagesDialogCandidates.value = entities.map(entity => entity.identifier) moveMessagesServiceIdentifier = entities[0]?.service as ServiceIdentifier || null } else { moveMessagesDialogCandidates.value = [entities.identifier] moveMessagesServiceIdentifier = entities.service as ServiceIdentifier || null } } else { moveMessagesDialogCandidates.value = [...selectionList.value] moveMessagesServiceIdentifier = mailStore.selectedFolder?.service as ServiceIdentifier || null } moveMessagesDialogService.value = await mailStore.retrieveService(moveMessagesServiceIdentifier); moveMessagesDialogVisible.value = true } function closeMoveMessagesDialog() { moveMessagesDialogVisible.value = false moveMessagesDialogService.value = null moveMessagesDialogCandidates.value = null } async function confirmMoveMessages(targetIdentifier: Parameters[0]) { await mailStore.moveMessages(targetIdentifier, moveMessagesDialogCandidates.value ?? []) deactivateSelectionMode() closeMoveMessagesDialog() } async function deleteSelectedMessages() { await mailStore.deleteMessages([...selectionList.value]) deactivateSelectionMode() } function openCreateFolderDialog(service: ServiceObject, parentFolder: CollectionObject | null = null) { createFolderDialogService.value = service createFolderDialogParent.value = parentFolder createFolderDialogError.value = '' createFolderDialogLoading.value = false createFolderDialogVisible.value = true } function closeCreateFolderDialog() { createFolderDialogVisible.value = false createFolderDialogService.value = null createFolderDialogParent.value = null createFolderDialogError.value = '' createFolderDialogLoading.value = false } async function confirmCreateFolder(label: string) { const service = createFolderDialogService.value if (!service) { return null } createFolderDialogLoading.value = true createFolderDialogError.value = '' try { const folder = await mailStore.createFolder(service, label, createFolderDialogParent.value) closeCreateFolderDialog() return folder } catch (error) { createFolderDialogError.value = error instanceof Error ? error.message : 'Failed to create folder. Please try again.' throw error } finally { createFolderDialogLoading.value = false } } async function openRenameFolderDialog(target: CollectionObject) { const service = await mailStore.retrieveService(target.service) renameFolderDialogService.value = service renameFolderDialogFolder.value = target renameFolderDialogError.value = '' renameFolderDialogLoading.value = false renameFolderDialogVisible.value = true } function closeRenameFolderDialog() { renameFolderDialogVisible.value = false renameFolderDialogService.value = null renameFolderDialogFolder.value = null renameFolderDialogError.value = '' renameFolderDialogLoading.value = false } async function confirmRenameFolder(label: string) { const folder = renameFolderDialogFolder.value if (!folder) { return null } renameFolderDialogLoading.value = true renameFolderDialogError.value = '' try { const updatedFolder = await mailStore.renameFolder(folder, label) closeRenameFolderDialog() return updatedFolder } catch (error) { renameFolderDialogError.value = error instanceof Error ? error.message : 'Failed to rename folder. Please try again.' throw error } finally { renameFolderDialogLoading.value = false } } async function openMoveFolderDialog(source: CollectionObject) { const service = await mailStore.retrieveService(source.service) moveFolderDialogService.value = service moveFolderDialogSource.value = source moveFolderDialogVisible.value = true } function closeMoveFolderDialog() { moveFolderDialogVisible.value = false moveFolderDialogService.value = null moveFolderDialogSource.value = null } async function confirmMoveFolder(target: CollectionObject) { const source = moveFolderDialogSource.value if (!source) { return null } const movedFolder = await mailStore.moveFolder(source, target) closeMoveFolderDialog() return movedFolder } async function openDeleteFolderDialog(target: CollectionObject) { const service = await mailStore.retrieveService(target.service) deleteFolderDialogService.value = service deleteFolderDialogFolder.value = target deleteFolderDialogError.value = '' deleteFolderDialogLoading.value = false deleteFolderDialogVisible.value = true } function closeDeleteFolderDialog() { deleteFolderDialogVisible.value = false deleteFolderDialogService.value = null deleteFolderDialogFolder.value = null deleteFolderDialogError.value = '' deleteFolderDialogLoading.value = false } async function confirmDeleteFolder() { const folder = deleteFolderDialogFolder.value if (!folder) { return null } deleteFolderDialogLoading.value = true deleteFolderDialogError.value = '' try { const deleted = await mailStore.deleteFolder(folder) closeDeleteFolderDialog() return deleted } catch (error) { deleteFolderDialogError.value = error instanceof Error ? error.message : 'Failed to delete folder. Please try again.' throw error } finally { deleteFolderDialogLoading.value = false } } return { sidebarVisible, settingsDialogVisible, composeMode, composeSource, composeVisible, selectionMode, selectionList, moveMessagesDialogVisible, moveMessagesDialogService, moveMessagesDialogCandidates, createFolderDialogParentLabel, createFolderDialogVisible, createFolderDialogService, createFolderDialogParent, createFolderDialogLoading, createFolderDialogError, renameFolderDialogVisible, renameFolderDialogService, renameFolderDialogFolder, renameFolderDialogParentLabel, renameFolderDialogLoading, renameFolderDialogError, moveFolderDialogVisible, moveFolderDialogService, moveFolderDialogSource, moveFolderDialogInvalidFolderKeys, deleteFolderDialogVisible, deleteFolderDialogService, deleteFolderDialogFolder, deleteFolderDialogLoading, deleteFolderDialogError, toggleSidebar, closeSidebar, openSettings, closeSettings, openCompose, closeCompose, afterSent, activateSelectionMode, deactivateSelectionMode, toggleMessageSelection, selectAllCurrentMessages, clearSelection, validateCreateFolderName, validateRenameFolderName, openMoveMessagesDialog, closeMoveMessagesDialog, confirmMoveMessages, deleteSelectedMessages, openCreateFolderDialog, closeCreateFolderDialog, confirmCreateFolder, openRenameFolderDialog, closeRenameFolderDialog, confirmRenameFolder, openMoveFolderDialog, closeMoveFolderDialog, confirmMoveFolder, openDeleteFolderDialog, closeDeleteFolderDialog, confirmDeleteFolder, } })