diff --git a/src/renderer/App.vue b/src/renderer/App.vue index b6487e11..9bfb6b2a 100644 --- a/src/renderer/App.vue +++ b/src/renderer/App.vue @@ -8,6 +8,7 @@ import { useSonner, useTheme, useVaultDoctor, + VAULT_DOCTOR_NOTICE_ID, } from '@/composables' import { i18n, ipc, store } from '@/electron' import { router, RouterName } from '@/router' @@ -114,10 +115,12 @@ function checkVaultHealth() { } sonner({ + id: VAULT_DOCTOR_NOTICE_ID, message: i18n.t('messages:warning.vaultDoctorConflicts', { count: data.summary.conflicts, }), type: 'warning', + closeButton: true, action: { label: i18n.t('messages:warning.vaultDoctorReview'), onClick: () => { diff --git a/src/renderer/components/preferences/Storage.vue b/src/renderer/components/preferences/Storage.vue index b377f91a..367d810d 100644 --- a/src/renderer/components/preferences/Storage.vue +++ b/src/renderer/components/preferences/Storage.vue @@ -21,6 +21,7 @@ import { useSnippets, useSonner, useVaultDoctor, + VAULT_DOCTOR_NOTICE_ID, } from '@/composables' import { i18n, ipc, store } from '@/electron' import { AlertTriangle, Check, LoaderCircle } from 'lucide-vue-next' @@ -36,7 +37,7 @@ interface MoveVaultResponse { vaultPath: string } -const { sonner } = useSonner() +const { sonner, dismiss } = useSonner() const { confirm } = useDialog() const { getFolders } = useFolders() const { getHttpFolders } = useHttpFolders() @@ -111,6 +112,7 @@ const { isDecisionSelected: isVaultDoctorDecisionSelected, scan: scanVault, apply: applyVault, + reset: resetVaultDoctor, } = useVaultDoctor() function showLoadingCounts() { @@ -226,6 +228,8 @@ async function openVaultStorage() { message: i18n.t('messages:success.vaultLoaded'), type: 'success', }) + + await refreshVaultDoctorAfterVaultChange() } async function moveVaultStorage() { @@ -273,6 +277,10 @@ async function moveVaultStorage() { vaultPath.value = result.vaultPath await resetAndReloadVaultData() + // Контент тот же, но путь сменился — прежний отчёт по старым путям неактуален. + dismiss(VAULT_DOCTOR_NOTICE_ID) + resetVaultDoctor() + sonner({ message: i18n.t('messages:success.vaultMoved'), type: 'success', @@ -409,6 +417,39 @@ async function applyVaultDoctorSafeFixes() { } } +function scrollToVaultDoctorSection() { + nextTick(() => { + vaultDoctorSection.value?.$el?.scrollIntoView({ + behavior: 'smooth', + block: 'start', + }) + }) +} + +// Смена vault делает прежний отчёт неактуальным (он про другой путь). Сбрасываем +// и пере-сканируем новый vault. Сканируем тихо (без sonner): мы уже в Storage, +// секция сама покажет результат, а при конфликтах подсвечиваем её скроллом. +async function refreshVaultDoctorAfterVaultChange() { + // Убираем уведомление о конфликтах прежнего vault — оно больше не актуально. + dismiss(VAULT_DOCTOR_NOTICE_ID) + resetVaultDoctor() + showVaultDoctorScanLoader() + + try { + await scanVault() + + if (vaultDoctorConflictCount.value > 0) { + scrollToVaultDoctorSection() + } + } + catch { + // Vault уже загружен; провал health-чека не критичен и не требует sonner. + } + finally { + hideVaultDoctorScanLoader() + } +} + // Переход из startup-уведомления о конфликтах: переиспользуем отчёт // startup-проверки (если он есть), иначе сканируем, и скроллим к секции, // чтобы пользователь не искал её вручную. Query чистим, чтобы повторный @@ -424,12 +465,7 @@ onMounted(() => { scanVaultDoctor() } - nextTick(() => { - vaultDoctorSection.value?.$el?.scrollIntoView({ - behavior: 'smooth', - block: 'start', - }) - }) + scrollToVaultDoctorSection() }) diff --git a/src/renderer/components/ui/sonner/types.ts b/src/renderer/components/ui/sonner/types.ts index 41aed7e1..016bb1e5 100644 --- a/src/renderer/components/ui/sonner/types.ts +++ b/src/renderer/components/ui/sonner/types.ts @@ -1,4 +1,5 @@ export interface Props { + id?: string | number message?: string component?: Component type?: 'default' | 'success' | 'error' | 'warning' diff --git a/src/renderer/composables/useSonner.ts b/src/renderer/composables/useSonner.ts index abcf324e..0e3040ca 100644 --- a/src/renderer/composables/useSonner.ts +++ b/src/renderer/composables/useSonner.ts @@ -4,14 +4,20 @@ import { toast } from 'vue-sonner' export function useSonner() { const sonner = (config: Props) => { - toast.custom(markRaw(Sonner), { + return toast.custom(markRaw(Sonner), { + id: config.id, componentProps: config, duration: config.action ? Infinity : config.duration || 5000, onDismiss: config.onClose, }) } + const dismiss = (id?: string | number) => { + toast.dismiss(id) + } + return { sonner, + dismiss, } } diff --git a/src/renderer/composables/useVaultDoctor.ts b/src/renderer/composables/useVaultDoctor.ts index 4a76a0df..e4f8077f 100644 --- a/src/renderer/composables/useVaultDoctor.ts +++ b/src/renderer/composables/useVaultDoctor.ts @@ -3,6 +3,10 @@ import { api } from '~/renderer/services/api' type ConflictGroup = VaultDoctorResponse['conflictGroups'][number] +// Стабильный id sonner-уведомления о конфликтах: позволяет убрать висящее +// уведомление о прежнем vault при смене на другой. +export const VAULT_DOCTOR_NOTICE_ID = 'vault-doctor-conflicts' + // Эвристика конфликтных копий cloud-sync: Dropbox "(conflicted copy)", // Яндекс.Диск / macOS "— копия", generic "copy". Используется только для // визуальной подсказки и предвыбора canonical, не влияет на backend. @@ -133,6 +137,13 @@ export function useVaultDoctor() { } } + // Сбрасывает отчёт и решения. Нужно при смене vault: старый отчёт относится + // к прежнему пути и к новому vault неприменим. + function reset() { + report.value = null + resetDecisions() + } + async function apply(): Promise { isApplying.value = true @@ -174,5 +185,6 @@ export function useVaultDoctor() { isDecisionSelected, scan, apply, + reset, } }