Enhance backup and restore features with improved UI, new translations, and error handling for better user experience
This commit is contained in:
parent
1aa8e5d28c
commit
488f746683
1
.gitignore
vendored
1
.gitignore
vendored
@ -8,6 +8,7 @@ pnpm-debug.log*
|
|||||||
lerna-debug.log*
|
lerna-debug.log*
|
||||||
local.pastebar.*
|
local.pastebar.*
|
||||||
safelist.txt
|
safelist.txt
|
||||||
|
pastebar-data-backup*
|
||||||
|
|
||||||
clipboard-images/**/*
|
clipboard-images/**/*
|
||||||
clip-images/**/*
|
clip-images/**/*
|
||||||
|
@ -11,6 +11,7 @@ export type ToasterToast = {
|
|||||||
description?: React.ReactNode
|
description?: React.ReactNode
|
||||||
open?: boolean
|
open?: boolean
|
||||||
className?: string
|
className?: string
|
||||||
|
variant?: 'default' | 'success' | 'warning' | 'danger' | 'destructive' | 'info' | null
|
||||||
onOpenChange?: (open: boolean) => void
|
onOpenChange?: (open: boolean) => void
|
||||||
duration?: number
|
duration?: number
|
||||||
onDismiss?: () => void
|
onDismiss?: () => void
|
||||||
|
@ -591,6 +591,13 @@ export function NavBar() {
|
|||||||
<Shortcut keys="ALT+U" />
|
<Shortcut keys="ALT+U" />
|
||||||
</MenubarShortcut>
|
</MenubarShortcut>
|
||||||
</MenubarItem>
|
</MenubarItem>
|
||||||
|
<MenubarItem
|
||||||
|
onClick={() => {
|
||||||
|
navigate('/app-settings/backup-restore', { replace: true })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('Backup and Restore', { ns: 'backuprestore' })}
|
||||||
|
</MenubarItem>
|
||||||
<MenubarItem
|
<MenubarItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigate('/app-settings/security', { replace: true })
|
navigate('/app-settings/security', { replace: true })
|
||||||
@ -653,9 +660,7 @@ export function NavBar() {
|
|||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
className={`mr-2 ${
|
className={`mr-2 ${
|
||||||
isImageCaptureDisabled
|
isImageCaptureDisabled ? 'text-slate-900/50' : 'text-slate-800'
|
||||||
? 'text-slate-900/50'
|
|
||||||
: 'text-slate-800'
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{t('Enable Image Capture', { ns: 'history' })}
|
{t('Enable Image Capture', { ns: 'history' })}
|
||||||
|
@ -1,27 +1,46 @@
|
|||||||
Backup and Restore: Backup and Restore
|
Auto Backup on Restore: Auto Backup on Restore
|
||||||
Create Backup: Create Backup
|
|
||||||
Include images in backup: Include images in backup
|
|
||||||
Backup Now: Backup Now
|
|
||||||
Restore Data: Restore Data
|
|
||||||
Restore from File...: Restore from File...
|
|
||||||
Select backup file: Select backup file
|
|
||||||
Available Backups: Available Backups
|
Available Backups: Available Backups
|
||||||
Total backup space: "{{size}}": Total backup space: {{size}}
|
Backup Files: Backup Files
|
||||||
|
Backup Now: Backup Now
|
||||||
|
Backup and Restore: Backup and Restore
|
||||||
|
Backup created successfully: Backup created successfully
|
||||||
|
Backup deleted successfully: Backup deleted successfully
|
||||||
|
Backup on Restore: Backup on Restore
|
||||||
|
Browse: Browse
|
||||||
|
Create Backup: Create Backup
|
||||||
|
Create a backup of your data?: Create a backup of your data?
|
||||||
|
Created: Created
|
||||||
|
Creating backup...: Creating backup...
|
||||||
|
Delete: Delete
|
||||||
|
Delete this backup? This action cannot be undone.: Delete this backup? This action cannot be undone.
|
||||||
|
Failed to create backup: Failed to create backup
|
||||||
|
Failed to delete backup: Failed to delete backup
|
||||||
|
Failed to load backup list: Failed to load backup list
|
||||||
|
Failed to open backup folder: Failed to open backup folder
|
||||||
|
Failed to restore backup: Failed to restore backup
|
||||||
|
Failed to select backup file: Failed to select backup file
|
||||||
|
Include images in backup: Include images in backup
|
||||||
|
Invalid backup file: Invalid backup file
|
||||||
|
Loading backups...: Loading backups...
|
||||||
|
Move to another location?: Move to another location?
|
||||||
No backups found: No backups found
|
No backups found: No backups found
|
||||||
Restore: Restore
|
Restore: Restore
|
||||||
Delete: Delete
|
Restore Data: Restore Data
|
||||||
Create a backup of your data?: Create a backup of your data?
|
|
||||||
Backup created successfully: Backup created successfully
|
|
||||||
Move to another location?: Move to another location?
|
|
||||||
This will replace all current data. Are you sure?: This will replace all current data. Are you sure?
|
|
||||||
Restore from "{{filename}}"? This will replace all current data.: Restore from {{filename}}? This will replace all current data.
|
|
||||||
Delete this backup? This action cannot be undone.: Delete this backup? This action cannot be undone.
|
|
||||||
Restore completed. The application will restart.: Restore completed. The application will restart.
|
Restore completed. The application will restart.: Restore completed. The application will restart.
|
||||||
Creating backup...: Creating backup...
|
Restore from "{{filename}}"? This will replace all current data.: Restore from {{filename}}? This will replace all current data.
|
||||||
|
Restore from File...: Restore from File...
|
||||||
|
Restore from {{filename}}? This will replace all current data.: Restore from {{filename}}? This will replace all current data.
|
||||||
|
Restored from {{filename}}: Restored from {{filename}}
|
||||||
|
Restoring a backup will automatically restart the application.: Restoring a backup will automatically restart the application.
|
||||||
Restoring backup...: Restoring backup...
|
Restoring backup...: Restoring backup...
|
||||||
Backup deleted successfully: Backup deleted successfully
|
Restoring...: Restoring...
|
||||||
Failed to delete backup: Failed to delete backup
|
Select backup file: Select backup file
|
||||||
Invalid backup file: Invalid backup file
|
Size: Size
|
||||||
The selected file is not a valid PasteBar backup: The selected file is not a valid PasteBar backup
|
The selected file is not a valid PasteBar backup: The selected file is not a valid PasteBar backup
|
||||||
Created: Created
|
This action cannot be undone. All current data will be replaced with the backup data.: This action cannot be undone. All current data will be replaced with the backup data.
|
||||||
Size: Size
|
This will create a backup file containing your database: This will create a backup file containing your database
|
||||||
|
This will replace all current data. Are you sure?: This will replace all current data. Are you sure?
|
||||||
|
Total backup space {{size}}: 'Total backup space: {{size}}'
|
||||||
|
and images: and images
|
||||||
|
selected file: selected file
|
||||||
|
will be permanently deleted.: will be permanently deleted.
|
||||||
|
@ -101,6 +101,7 @@ Enter Passcode: Enter Passcode
|
|||||||
Enter Password: Enter Password
|
Enter Password: Enter Password
|
||||||
Enter Recovery Password: Enter Recovery Password
|
Enter Recovery Password: Enter Recovery Password
|
||||||
Enter passcode or password to unlock: Enter passcode or password to unlock
|
Enter passcode or password to unlock: Enter passcode or password to unlock
|
||||||
|
Error: Error
|
||||||
Errors:
|
Errors:
|
||||||
Error loading link: Error loading link
|
Error loading link: Error loading link
|
||||||
Cant save to file: Cant save to file
|
Cant save to file: Cant save to file
|
||||||
@ -237,6 +238,7 @@ Set: Set
|
|||||||
Set Password: Set Password
|
Set Password: Set Password
|
||||||
Show Large View: Show Large View
|
Show Large View: Show Large View
|
||||||
Show all: Show all
|
Show all: Show all
|
||||||
|
Size: Size
|
||||||
Source: Source
|
Source: Source
|
||||||
Split History Window: Split History Window
|
Split History Window: Split History Window
|
||||||
Star: Star
|
Star: Star
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
Backup and Restore: Copia de Seguridad y Restauración
|
Backup and Restore: Copia de Seguridad y Restauración
|
||||||
|
Auto Backup on Restore: Copia Automática al Restaurar
|
||||||
|
Browse: Explorar
|
||||||
Create Backup: Crear Copia de Seguridad
|
Create Backup: Crear Copia de Seguridad
|
||||||
Include images in backup: Incluir imágenes en la copia de seguridad
|
Include images in backup: Incluir imágenes en la copia de seguridad
|
||||||
Backup Now: Crear Copia Ahora
|
Backup Now: Crear Copia Ahora
|
||||||
@ -6,7 +8,7 @@ Restore Data: Restaurar Datos
|
|||||||
Restore from File...: Restaurar desde Archivo...
|
Restore from File...: Restaurar desde Archivo...
|
||||||
Select backup file: Seleccionar archivo de copia de seguridad
|
Select backup file: Seleccionar archivo de copia de seguridad
|
||||||
Available Backups: Copias de Seguridad Disponibles
|
Available Backups: Copias de Seguridad Disponibles
|
||||||
Total backup space: "{{size}}": Espacio total de copias: {{size}}
|
Total backup space {{size}}: 'Espacio total de copia s: {{size}}'
|
||||||
No backups found: No se encontraron copias de seguridad
|
No backups found: No se encontraron copias de seguridad
|
||||||
Restore: Restaurar
|
Restore: Restaurar
|
||||||
Delete: Eliminar
|
Delete: Eliminar
|
||||||
@ -24,4 +26,18 @@ Failed to delete backup: Error al eliminar la copia de seguridad
|
|||||||
Invalid backup file: Archivo de copia de seguridad inválido
|
Invalid backup file: Archivo de copia de seguridad inválido
|
||||||
The selected file is not a valid PasteBar backup: El archivo seleccionado no es una copia de seguridad válida de PasteBar
|
The selected file is not a valid PasteBar backup: El archivo seleccionado no es una copia de seguridad válida de PasteBar
|
||||||
Created: Creado
|
Created: Creado
|
||||||
Size: Tamaño
|
Size: Tamaño
|
||||||
|
Failed to load backup list: Error al cargar la lista de copias de seguridad
|
||||||
|
Failed to create backup: Error al crear la copia de seguridad
|
||||||
|
Restored from {{filename}}: Restaurado desde {{filename}}
|
||||||
|
Failed to restore backup: Error al restaurar la copia de seguridad
|
||||||
|
Backup Files: Archivos de Copia de Seguridad
|
||||||
|
selected file: archivo seleccionado
|
||||||
|
Failed to select backup file: Error al seleccionar el archivo de copia de seguridad
|
||||||
|
Failed to open backup folder: Error al abrir la carpeta de copia de seguridad
|
||||||
|
This will create a backup file containing your database: Esto creará un archivo de copia de seguridad que contiene tu base de datos
|
||||||
|
and images: e imágenes
|
||||||
|
Loading backups...: Cargando copias de seguridad...
|
||||||
|
will be permanently deleted.: será eliminado permanentemente.
|
||||||
|
This action cannot be undone. All current data will be replaced with the backup data.: Esta acción no se puede deshacer. Todos los datos actuales serán reemplazados con los datos de la copia de seguridad.
|
||||||
|
Restoring a backup will automatically restart the application.: Restaurar una copia de seguridad reiniciará automáticamente la aplicación.
|
@ -1,4 +1,6 @@
|
|||||||
Backup and Restore: Sauvegarde et Restauration
|
Backup and Restore: Sauvegarde et Restauration
|
||||||
|
Auto Backup on Restore: Sauvegarde Automatique à la Restauration
|
||||||
|
Browse: Parcourir
|
||||||
Create Backup: Créer une Sauvegarde
|
Create Backup: Créer une Sauvegarde
|
||||||
Include images in backup: Inclure les images dans la sauvegarde
|
Include images in backup: Inclure les images dans la sauvegarde
|
||||||
Backup Now: Sauvegarder Maintenant
|
Backup Now: Sauvegarder Maintenant
|
||||||
@ -6,7 +8,7 @@ Restore Data: Restaurer les Données
|
|||||||
Restore from File...: Restaurer depuis un Fichier...
|
Restore from File...: Restaurer depuis un Fichier...
|
||||||
Select backup file: Sélectionner le fichier de sauvegarde
|
Select backup file: Sélectionner le fichier de sauvegarde
|
||||||
Available Backups: Sauvegardes Disponibles
|
Available Backups: Sauvegardes Disponibles
|
||||||
Total backup space: "{{size}}": Espace total des sauvegardes: {{size}}
|
Total backup space {{size}}: 'Espace total des sauvegardes: {{size}}'
|
||||||
No backups found: Aucune sauvegarde trouvée
|
No backups found: Aucune sauvegarde trouvée
|
||||||
Restore: Restaurer
|
Restore: Restaurer
|
||||||
Delete: Supprimer
|
Delete: Supprimer
|
||||||
@ -24,4 +26,18 @@ Failed to delete backup: Échec de la suppression de la sauvegarde
|
|||||||
Invalid backup file: Fichier de sauvegarde invalide
|
Invalid backup file: Fichier de sauvegarde invalide
|
||||||
The selected file is not a valid PasteBar backup: Le fichier sélectionné n'est pas une sauvegarde PasteBar valide
|
The selected file is not a valid PasteBar backup: Le fichier sélectionné n'est pas une sauvegarde PasteBar valide
|
||||||
Created: Créé
|
Created: Créé
|
||||||
Size: Taille
|
Size: Taille
|
||||||
|
Failed to load backup list: Échec du chargement de la liste des sauvegardes
|
||||||
|
Failed to create backup: Échec de la création de la sauvegarde
|
||||||
|
Restored from {{filename}}: Restauré depuis {{filename}}
|
||||||
|
Failed to restore backup: Échec de la restauration de la sauvegarde
|
||||||
|
Backup Files: Fichiers de Sauvegarde
|
||||||
|
selected file: fichier sélectionné
|
||||||
|
Failed to select backup file: Échec de la sélection du fichier de sauvegarde
|
||||||
|
Failed to open backup folder: Échec de l'ouverture du dossier de sauvegarde
|
||||||
|
This will create a backup file containing your database: Ceci créera un fichier de sauvegarde contenant votre base de données
|
||||||
|
and images: et les images
|
||||||
|
Loading backups...: Chargement des sauvegardes...
|
||||||
|
will be permanently deleted.: sera définitivement supprimé.
|
||||||
|
This action cannot be undone. All current data will be replaced with the backup data.: Cette action ne peut pas être annulée. Toutes les données actuelles seront remplacées par les données de sauvegarde.
|
||||||
|
Restoring a backup will automatically restart the application.: La restauration d'une sauvegarde redémarrera automatiquement l'application.
|
@ -1,4 +1,6 @@
|
|||||||
|
Auto Backup on Restore: Backup Automatico al Ripristino
|
||||||
Backup and Restore: Backup e Ripristino
|
Backup and Restore: Backup e Ripristino
|
||||||
|
Browse: Sfoglia
|
||||||
Create Backup: Crea Backup
|
Create Backup: Crea Backup
|
||||||
Include images in backup: Includi immagini nel backup
|
Include images in backup: Includi immagini nel backup
|
||||||
Backup Now: Crea Backup Ora
|
Backup Now: Crea Backup Ora
|
||||||
@ -6,7 +8,7 @@ Restore Data: Ripristina Dati
|
|||||||
Restore from File...: Ripristina da File...
|
Restore from File...: Ripristina da File...
|
||||||
Select backup file: Seleziona file di backup
|
Select backup file: Seleziona file di backup
|
||||||
Available Backups: Backup Disponibili
|
Available Backups: Backup Disponibili
|
||||||
Total backup space: "{{size}}": Spazio totale backup: {{size}}
|
Total backup space {{size}}: 'Spazio totale di backup: {{size}}'
|
||||||
No backups found: Nessun backup trovato
|
No backups found: Nessun backup trovato
|
||||||
Restore: Ripristina
|
Restore: Ripristina
|
||||||
Delete: Elimina
|
Delete: Elimina
|
||||||
@ -24,4 +26,18 @@ Failed to delete backup: Impossibile eliminare il backup
|
|||||||
Invalid backup file: File di backup non valido
|
Invalid backup file: File di backup non valido
|
||||||
The selected file is not a valid PasteBar backup: Il file selezionato non è un backup PasteBar valido
|
The selected file is not a valid PasteBar backup: Il file selezionato non è un backup PasteBar valido
|
||||||
Created: Creato
|
Created: Creato
|
||||||
Size: Dimensione
|
Size: Dimensione
|
||||||
|
Failed to load backup list: Impossibile caricare l'elenco dei backup
|
||||||
|
Failed to create backup: Impossibile creare il backup
|
||||||
|
Restored from {{filename}}: Ripristinato da {{filename}}
|
||||||
|
Failed to restore backup: Impossibile ripristinare il backup
|
||||||
|
Backup Files: File di Backup
|
||||||
|
selected file: file selezionato
|
||||||
|
Failed to select backup file: Impossibile selezionare il file di backup
|
||||||
|
Failed to open backup folder: Impossibile aprire la cartella di backup
|
||||||
|
This will create a backup file containing your database: Questo creerà un file di backup contenente il tuo database
|
||||||
|
and images: e le immagini
|
||||||
|
Loading backups...: Caricamento backup...
|
||||||
|
will be permanently deleted.: sarà eliminato permanentemente.
|
||||||
|
This action cannot be undone. All current data will be replaced with the backup data.: Questa azione non può essere annullata. Tutti i dati attuali saranno sostituiti con i dati del backup.
|
||||||
|
Restoring a backup will automatically restart the application.: Il ripristino di un backup riavvierà automaticamente l'applicazione.
|
@ -1,4 +1,6 @@
|
|||||||
|
Auto Backup on Restore: Автобэкап при восстановлении
|
||||||
Backup and Restore: Резервное копирование и восстановление
|
Backup and Restore: Резервное копирование и восстановление
|
||||||
|
Browse: Обзор
|
||||||
Create Backup: Создать резервную копию
|
Create Backup: Создать резервную копию
|
||||||
Include images in backup: Включить изображения в резервную копию
|
Include images in backup: Включить изображения в резервную копию
|
||||||
Backup Now: Создать резервную копию сейчас
|
Backup Now: Создать резервную копию сейчас
|
||||||
@ -6,7 +8,7 @@ Restore Data: Восстановить данные
|
|||||||
Restore from File...: Восстановить из файла...
|
Restore from File...: Восстановить из файла...
|
||||||
Select backup file: Выбрать файл резервной копии
|
Select backup file: Выбрать файл резервной копии
|
||||||
Available Backups: Доступные резервные копии
|
Available Backups: Доступные резервные копии
|
||||||
Total backup space: "{{size}}": Общий размер резервных копий: {{size}}
|
Total backup space {{size}}: 'Общий размер резервных копий: {{size}}'
|
||||||
No backups found: Резервные копии не найдены
|
No backups found: Резервные копии не найдены
|
||||||
Restore: Восстановить
|
Restore: Восстановить
|
||||||
Delete: Удалить
|
Delete: Удалить
|
||||||
@ -24,4 +26,18 @@ Failed to delete backup: Не удалось удалить резервную
|
|||||||
Invalid backup file: Недействительный файл резервной копии
|
Invalid backup file: Недействительный файл резервной копии
|
||||||
The selected file is not a valid PasteBar backup: Выбранный файл не является действительной резервной копией PasteBar
|
The selected file is not a valid PasteBar backup: Выбранный файл не является действительной резервной копией PasteBar
|
||||||
Created: Создано
|
Created: Создано
|
||||||
Size: Размер
|
Size: Размер
|
||||||
|
Failed to load backup list: Не удалось загрузить список резервных копий
|
||||||
|
Failed to create backup: Не удалось создать резервную копию
|
||||||
|
Restored from {{filename}}: Восстановлено из {{filename}}
|
||||||
|
Failed to restore backup: Не удалось восстановить резервную копию
|
||||||
|
Backup Files: Файлы резервных копий
|
||||||
|
selected file: выбранный файл
|
||||||
|
Failed to select backup file: Не удалось выбрать файл резервной копии
|
||||||
|
Failed to open backup folder: Не удалось открыть папку резервных копий
|
||||||
|
This will create a backup file containing your database: Это создаст файл резервной копии, содержащий вашу базу данных
|
||||||
|
and images: и изображения
|
||||||
|
Loading backups...: Загрузка резервных копий...
|
||||||
|
will be permanently deleted.: будет удалена навсегда.
|
||||||
|
This action cannot be undone. All current data will be replaced with the backup data.: Это действие нельзя отменить. Все текущие данные будут заменены данными из резервной копии.
|
||||||
|
Restoring a backup will automatically restart the application.: Восстановление резервной копии автоматически перезапустит приложение.
|
@ -96,6 +96,7 @@ Enter Passcode: Введите код доступа
|
|||||||
Enter Password: Введите пароль
|
Enter Password: Введите пароль
|
||||||
Enter Recovery Password: Введите пароль восстановления
|
Enter Recovery Password: Введите пароль восстановления
|
||||||
Enter passcode or password to unlock: Введите код доступа или пароль для разблокировки
|
Enter passcode or password to unlock: Введите код доступа или пароль для разблокировки
|
||||||
|
Error: Error
|
||||||
Errors:
|
Errors:
|
||||||
Error loading link: Ошибка загрузки ссылки
|
Error loading link: Ошибка загрузки ссылки
|
||||||
Cant save file: Невозможно сохранить файл
|
Cant save file: Невозможно сохранить файл
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
|
Auto Backup on Restore: Geri Yüklemede Otomatik Yedek
|
||||||
Backup and Restore: Yedekleme ve Geri Yükleme
|
Backup and Restore: Yedekleme ve Geri Yükleme
|
||||||
|
Browse: Gözat
|
||||||
Create Backup: Yedek Oluştur
|
Create Backup: Yedek Oluştur
|
||||||
Include images in backup: Yedekte görselleri dahil et
|
Include images in backup: Yedekte görselleri dahil et
|
||||||
Backup Now: Şimdi Yedekle
|
Backup Now: Şimdi Yedekle
|
||||||
@ -6,7 +8,7 @@ Restore Data: Verileri Geri Yükle
|
|||||||
Restore from File...: Dosyadan Geri Yükle...
|
Restore from File...: Dosyadan Geri Yükle...
|
||||||
Select backup file: Yedek dosyası seç
|
Select backup file: Yedek dosyası seç
|
||||||
Available Backups: Mevcut Yedekler
|
Available Backups: Mevcut Yedekler
|
||||||
Total backup space: "{{size}}": Toplam yedek alanı: {{size}}
|
Total backup space {{size}}: 'Toplam yedek alanı: {{size}}'
|
||||||
No backups found: Yedek bulunamadı
|
No backups found: Yedek bulunamadı
|
||||||
Restore: Geri Yükle
|
Restore: Geri Yükle
|
||||||
Delete: Sil
|
Delete: Sil
|
||||||
@ -14,7 +16,7 @@ Create a backup of your data?: Verilerinizin bir yedeğini oluşturmak istiyor m
|
|||||||
Backup created successfully: Yedek başarıyla oluşturuldu
|
Backup created successfully: Yedek başarıyla oluşturuldu
|
||||||
Move to another location?: Başka bir konuma taşı?
|
Move to another location?: Başka bir konuma taşı?
|
||||||
This will replace all current data. Are you sure?: Bu, mevcut tüm verileri değiştirecek. Emin misiniz?
|
This will replace all current data. Are you sure?: Bu, mevcut tüm verileri değiştirecek. Emin misiniz?
|
||||||
Restore from "{{filename}}"? This will replace all current data.: {{filename}} dosyasından geri yüklensin mi? Bu, mevcut tüm verileri değiştirecek.
|
Restore from {{filename}}? This will replace all current data.: '{{filename}} dosyasından geri yüklensin mi? Bu, mevcut tüm verileri değiştirecek.'
|
||||||
Delete this backup? This action cannot be undone.: Bu yedeği sil? Bu eylem geri alınamaz.
|
Delete this backup? This action cannot be undone.: Bu yedeği sil? Bu eylem geri alınamaz.
|
||||||
Restore completed. The application will restart.: Geri yükleme tamamlandı. Uygulama yeniden başlatılacak.
|
Restore completed. The application will restart.: Geri yükleme tamamlandı. Uygulama yeniden başlatılacak.
|
||||||
Creating backup...: Yedek oluşturuluyor...
|
Creating backup...: Yedek oluşturuluyor...
|
||||||
@ -24,4 +26,18 @@ Failed to delete backup: Yedek silinemedi
|
|||||||
Invalid backup file: Geçersiz yedek dosyası
|
Invalid backup file: Geçersiz yedek dosyası
|
||||||
The selected file is not a valid PasteBar backup: Seçilen dosya geçerli bir PasteBar yedeği değil
|
The selected file is not a valid PasteBar backup: Seçilen dosya geçerli bir PasteBar yedeği değil
|
||||||
Created: Oluşturulma
|
Created: Oluşturulma
|
||||||
Size: Boyut
|
Size: Boyut
|
||||||
|
Failed to load backup list: Yedek listesi yüklenemedi
|
||||||
|
Failed to create backup: Yedek oluşturulamadı
|
||||||
|
'Restored from {{filename}}': '{{filename}} dosyasından geri yüklendi'
|
||||||
|
Failed to restore backup: Yedek geri yüklenemedi
|
||||||
|
Backup Files: Yedek Dosyaları
|
||||||
|
selected file: seçilen dosya
|
||||||
|
Failed to select backup file: Yedek dosyası seçilemedi
|
||||||
|
Failed to open backup folder: Yedek klasörü açılamadı
|
||||||
|
This will create a backup file containing your database: Bu, veritabanınızı içeren bir yedek dosyası oluşturacak
|
||||||
|
and images: ve görseller
|
||||||
|
Loading backups...: Yedekler yükleniyor...
|
||||||
|
will be permanently deleted.: kalıcı olarak silinecek.
|
||||||
|
This action cannot be undone. All current data will be replaced with the backup data.: Bu eylem geri alınamaz. Mevcut tüm veriler yedek verileriyle değiştirilecek.
|
||||||
|
Restoring a backup will automatically restart the application.: Bir yedeği geri yüklemek uygulamayı otomatik olarak yeniden başlatacak.
|
@ -1,4 +1,6 @@
|
|||||||
|
Auto Backup on Restore: Авто Бекап при Відновленні
|
||||||
Backup and Restore: Резервне копіювання та відновлення
|
Backup and Restore: Резервне копіювання та відновлення
|
||||||
|
Browse: Переглянути
|
||||||
Create Backup: Створити резервну копію
|
Create Backup: Створити резервну копію
|
||||||
Include images in backup: Включити зображення в резервну копію
|
Include images in backup: Включити зображення в резервну копію
|
||||||
Backup Now: Створити резервну копію зараз
|
Backup Now: Створити резервну копію зараз
|
||||||
@ -6,7 +8,7 @@ Restore Data: Відновити дані
|
|||||||
Restore from File...: Відновити з файлу...
|
Restore from File...: Відновити з файлу...
|
||||||
Select backup file: Вибрати файл резервної копії
|
Select backup file: Вибрати файл резервної копії
|
||||||
Available Backups: Доступні резервні копії
|
Available Backups: Доступні резервні копії
|
||||||
Total backup space: "{{size}}": Загальний розмір резервних копій: {{size}}
|
Total backup space {{size}}: 'Загальний розмір резервних копій: {{size}}'
|
||||||
No backups found: Резервні копії не знайдені
|
No backups found: Резервні копії не знайдені
|
||||||
Restore: Відновити
|
Restore: Відновити
|
||||||
Delete: Видалити
|
Delete: Видалити
|
||||||
@ -24,4 +26,18 @@ Failed to delete backup: Не вдалося видалити резервну
|
|||||||
Invalid backup file: Недійсний файл резервної копії
|
Invalid backup file: Недійсний файл резервної копії
|
||||||
The selected file is not a valid PasteBar backup: Вибраний файл не є дійсною резервною копією PasteBar
|
The selected file is not a valid PasteBar backup: Вибраний файл не є дійсною резервною копією PasteBar
|
||||||
Created: Створено
|
Created: Створено
|
||||||
Size: Розмір
|
Size: Розмір
|
||||||
|
Failed to load backup list: Не вдалося завантажити список резервних копій
|
||||||
|
Failed to create backup: Не вдалося створити резервну копію
|
||||||
|
Restored from {{filename}}: Відновлено з {{filename}}
|
||||||
|
Failed to restore backup: Не вдалося відновити резервну копію
|
||||||
|
Backup Files: Файли резервних копій
|
||||||
|
selected file: вибраний файл
|
||||||
|
Failed to select backup file: Не вдалося вибрати файл резервної копії
|
||||||
|
Failed to open backup folder: Не вдалося відкрити папку резервних копій
|
||||||
|
This will create a backup file containing your database: Це створить файл резервної копії, що містить вашу базу даних
|
||||||
|
and images: та зображення
|
||||||
|
Loading backups...: Завантаження резервних копій...
|
||||||
|
will be permanently deleted.: буде видалено назавжди.
|
||||||
|
This action cannot be undone. All current data will be replaced with the backup data.: Цю дію неможливо скасувати. Всі поточні дані будуть замінені даними з резервної копії.
|
||||||
|
Restoring a backup will automatically restart the application.: Відновлення резервної копії автоматично перезапустить додаток.
|
@ -1,4 +1,6 @@
|
|||||||
|
Auto Backup on Restore: 恢复时自动备份
|
||||||
Backup and Restore: 备份和恢复
|
Backup and Restore: 备份和恢复
|
||||||
|
Browse: 浏览
|
||||||
Create Backup: 创建备份
|
Create Backup: 创建备份
|
||||||
Include images in backup: 在备份中包含图片
|
Include images in backup: 在备份中包含图片
|
||||||
Backup Now: 立即备份
|
Backup Now: 立即备份
|
||||||
@ -6,7 +8,7 @@ Restore Data: 恢复数据
|
|||||||
Restore from File...: 从文件恢复...
|
Restore from File...: 从文件恢复...
|
||||||
Select backup file: 选择备份文件
|
Select backup file: 选择备份文件
|
||||||
Available Backups: 可用备份
|
Available Backups: 可用备份
|
||||||
Total backup space: "{{size}}": 备份总空间: {{size}}
|
Total backup space {{size}}: '备份总空间: {{size}}'
|
||||||
No backups found: 未找到备份
|
No backups found: 未找到备份
|
||||||
Restore: 恢复
|
Restore: 恢复
|
||||||
Delete: 删除
|
Delete: 删除
|
||||||
@ -24,4 +26,18 @@ Failed to delete backup: 删除备份失败
|
|||||||
Invalid backup file: 无效的备份文件
|
Invalid backup file: 无效的备份文件
|
||||||
The selected file is not a valid PasteBar backup: 选择的文件不是有效的PasteBar备份
|
The selected file is not a valid PasteBar backup: 选择的文件不是有效的PasteBar备份
|
||||||
Created: 创建时间
|
Created: 创建时间
|
||||||
Size: 大小
|
Size: 大小
|
||||||
|
Failed to load backup list: 加载备份列表失败
|
||||||
|
Failed to create backup: 创建备份失败
|
||||||
|
Restored from {{filename}}: 从{{filename}}恢复
|
||||||
|
Failed to restore backup: 恢复备份失败
|
||||||
|
Backup Files: 备份文件
|
||||||
|
selected file: 选择的文件
|
||||||
|
Failed to select backup file: 选择备份文件失败
|
||||||
|
Failed to open backup folder: 打开备份文件夹失败
|
||||||
|
This will create a backup file containing your database: 这将创建一个包含您数据库的备份文件
|
||||||
|
and images: 和图片
|
||||||
|
Loading backups...: 正在加载备份...
|
||||||
|
will be permanently deleted.: 将被永久删除。
|
||||||
|
This action cannot be undone. All current data will be replaced with the backup data.: 此操作无法撤销。所有当前数据将被备份数据替换。
|
||||||
|
Restoring a backup will automatically restart the application.: 恢复备份将自动重启应用程序。
|
@ -1,23 +1,23 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { invoke } from '@tauri-apps/api'
|
import { invoke } from '@tauri-apps/api'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { open } from '@tauri-apps/api/dialog'
|
||||||
import {
|
import { settingsStoreAtom, uiStoreAtom } from '~/store'
|
||||||
Archive,
|
import { useAtomValue } from 'jotai'
|
||||||
Download,
|
import {
|
||||||
FolderOpen,
|
Download,
|
||||||
HardDrive,
|
ExternalLink,
|
||||||
Loader2,
|
FolderOpen,
|
||||||
Package,
|
HardDrive,
|
||||||
RotateCcw,
|
Loader2,
|
||||||
Trash2,
|
Package,
|
||||||
Upload
|
RotateCcw,
|
||||||
|
Trash2,
|
||||||
|
Upload,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useToast } from '~/components/ui/use-toast'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
import AutoSize from 'react-virtualized-auto-sizer'
|
import AutoSize from 'react-virtualized-auto-sizer'
|
||||||
|
|
||||||
import Spacer from '~/components/atoms/spacer'
|
|
||||||
import { Icons } from '~/components/icons'
|
|
||||||
import SimpleBar from '~/components/libs/simplebar-react'
|
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
@ -29,6 +29,12 @@ import {
|
|||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
AlertDialogTrigger,
|
AlertDialogTrigger,
|
||||||
} from '~/components/ui/alert-dialog'
|
} from '~/components/ui/alert-dialog'
|
||||||
|
import { useToast } from '~/components/ui/use-toast'
|
||||||
|
import Spacer from '~/components/atoms/spacer'
|
||||||
|
import { TimeAgo } from '~/components/atoms/time-ago/TimeAgo'
|
||||||
|
import ToolTip from '~/components/atoms/tooltip'
|
||||||
|
import { Icons } from '~/components/icons'
|
||||||
|
import SimpleBar from '~/components/libs/simplebar-react'
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
Box,
|
Box,
|
||||||
@ -40,7 +46,6 @@ import {
|
|||||||
Checkbox,
|
Checkbox,
|
||||||
Flex,
|
Flex,
|
||||||
Text,
|
Text,
|
||||||
TextNormal,
|
|
||||||
} from '~/components/ui'
|
} from '~/components/ui'
|
||||||
|
|
||||||
interface BackupInfo {
|
interface BackupInfo {
|
||||||
@ -60,14 +65,47 @@ interface BackupListResponse {
|
|||||||
export default function BackupRestoreSettings() {
|
export default function BackupRestoreSettings() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { toast } = useToast()
|
const { toast } = useToast()
|
||||||
|
const { returnRoute } = useAtomValue(uiStoreAtom)
|
||||||
|
|
||||||
|
const { relaunchApp } = useAtomValue(settingsStoreAtom)
|
||||||
|
|
||||||
const [includeImages, setIncludeImages] = useState(true)
|
const [includeImages, setIncludeImages] = useState(true)
|
||||||
|
const [backupOnRestore, setBackupOnRestore] = useState(() => {
|
||||||
|
const saved = localStorage.getItem('backupOnRestore')
|
||||||
|
return saved !== null ? JSON.parse(saved) : true
|
||||||
|
})
|
||||||
|
|
||||||
|
// Save backup on restore preference to localStorage
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem('backupOnRestore', JSON.stringify(backupOnRestore))
|
||||||
|
}, [backupOnRestore])
|
||||||
const [isCreatingBackup, setIsCreatingBackup] = useState(false)
|
const [isCreatingBackup, setIsCreatingBackup] = useState(false)
|
||||||
const [isRestoring, setIsRestoring] = useState(false)
|
const [isRestoring, setIsRestoring] = useState(false)
|
||||||
|
const [restoringFromFile, setRestoringFromFile] = useState(false)
|
||||||
|
const [restoringBackupPath, setRestoringBackupPath] = useState<string | null>(null)
|
||||||
const [backups, setBackups] = useState<BackupInfo[]>([])
|
const [backups, setBackups] = useState<BackupInfo[]>([])
|
||||||
const [totalSize, setTotalSize] = useState('')
|
const [totalSize, setTotalSize] = useState('')
|
||||||
const [isLoadingBackups, setIsLoadingBackups] = useState(false)
|
const [isLoadingBackups, setIsLoadingBackups] = useState(false)
|
||||||
|
|
||||||
|
// Parse date from backup filename
|
||||||
|
const parseBackupDate = (filename: string): Date | null => {
|
||||||
|
// Extract date from filename format: pastebar-data-backup-YYYY-MM-DD-HH-MM.zip
|
||||||
|
const match = filename.match(
|
||||||
|
/pastebar-data-backup-(\d{4})-(\d{2})-(\d{2})-(\d{2})-(\d{2})\.zip/
|
||||||
|
)
|
||||||
|
if (match) {
|
||||||
|
const [, year, month, day, hour, minute] = match
|
||||||
|
return new Date(
|
||||||
|
parseInt(year),
|
||||||
|
parseInt(month) - 1,
|
||||||
|
parseInt(day),
|
||||||
|
parseInt(hour),
|
||||||
|
parseInt(minute)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
const loadBackups = async () => {
|
const loadBackups = async () => {
|
||||||
setIsLoadingBackups(true)
|
setIsLoadingBackups(true)
|
||||||
try {
|
try {
|
||||||
@ -77,8 +115,14 @@ export default function BackupRestoreSettings() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load backups:', error)
|
console.error('Failed to load backups:', error)
|
||||||
toast({
|
toast({
|
||||||
|
id: 'backup-list-error',
|
||||||
title: t('Error', { ns: 'common' }),
|
title: t('Error', { ns: 'common' }),
|
||||||
description: 'Failed to load backup list',
|
duration: 3000,
|
||||||
|
description: (
|
||||||
|
<Box className="word-break">
|
||||||
|
{t('Failed to load backup list', { ns: 'backuprestore' })}
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
})
|
})
|
||||||
} finally {
|
} finally {
|
||||||
@ -96,19 +140,28 @@ export default function BackupRestoreSettings() {
|
|||||||
const backupPath = await invoke<string>('create_backup', {
|
const backupPath = await invoke<string>('create_backup', {
|
||||||
includeImages,
|
includeImages,
|
||||||
})
|
})
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
|
id: 'backup-create-success',
|
||||||
title: t('Backup created successfully', { ns: 'backuprestore' }),
|
title: t('Backup created successfully', { ns: 'backuprestore' }),
|
||||||
description: backupPath,
|
duration: 3000,
|
||||||
|
description: <Box className="word-break">{backupPath}</Box>,
|
||||||
|
variant: 'success',
|
||||||
})
|
})
|
||||||
|
|
||||||
// Reload backup list
|
// Reload backup list
|
||||||
await loadBackups()
|
await loadBackups()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to create backup:', error)
|
console.error('Failed to create backup:', error)
|
||||||
toast({
|
toast({
|
||||||
|
id: 'backup-create-error',
|
||||||
title: t('Error', { ns: 'common' }),
|
title: t('Error', { ns: 'common' }),
|
||||||
description: `Failed to create backup: ${error}`,
|
duration: 3000,
|
||||||
|
description: (
|
||||||
|
<Box className="word-break">
|
||||||
|
{t('Failed to create backup', { ns: 'backuprestore' })}: {String(error)}
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
})
|
})
|
||||||
} finally {
|
} finally {
|
||||||
@ -118,284 +171,518 @@ export default function BackupRestoreSettings() {
|
|||||||
|
|
||||||
const handleRestoreBackup = async (backupPath: string, filename: string) => {
|
const handleRestoreBackup = async (backupPath: string, filename: string) => {
|
||||||
setIsRestoring(true)
|
setIsRestoring(true)
|
||||||
|
setRestoringBackupPath(backupPath)
|
||||||
try {
|
try {
|
||||||
await invoke('restore_backup', { backupPath })
|
await invoke('restore_backup', {
|
||||||
|
backupPath,
|
||||||
toast({
|
createPreRestoreBackup: backupOnRestore,
|
||||||
title: t('Restore completed. The application will restart.', { ns: 'backuprestore' }),
|
|
||||||
description: `Restored from ${filename}`,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
toast({
|
||||||
|
id: 'backup-restore-success',
|
||||||
|
title: t('Restore completed. The application will restart.', {
|
||||||
|
ns: 'backuprestore',
|
||||||
|
}),
|
||||||
|
duration: 3000,
|
||||||
|
description: (
|
||||||
|
<Box className="word-break">
|
||||||
|
{t('Restored from {{filename}}', { ns: 'backuprestore', filename })}
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
variant: 'success',
|
||||||
|
})
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
relaunchApp()
|
||||||
|
setIsRestoring(false)
|
||||||
|
}, 3000)
|
||||||
|
|
||||||
// Application should restart after restore
|
// Application should restart after restore
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to restore backup:', error)
|
console.error('Failed to restore backup:', error)
|
||||||
toast({
|
toast({
|
||||||
title: t('Error', { ns: 'common' }),
|
title: t('Error', { ns: 'common' }),
|
||||||
description: `Failed to restore backup: ${error}`,
|
id: 'backup-restore-error',
|
||||||
|
duration: 3000,
|
||||||
|
description: (
|
||||||
|
<Box className="word-break">
|
||||||
|
{t('Failed to restore backup', { ns: 'backuprestore' })}: {String(error)}
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
})
|
})
|
||||||
} finally {
|
} finally {
|
||||||
setIsRestoring(false)
|
setIsRestoring(false)
|
||||||
|
setRestoringBackupPath(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleRestoreFromFile = async () => {
|
const handleRestoreFromFile = async () => {
|
||||||
try {
|
try {
|
||||||
const selectedFile = await invoke<string | null>('select_backup_file')
|
setRestoringFromFile(true)
|
||||||
|
// Get current data directory path for defaultPath
|
||||||
if (selectedFile) {
|
const dataPaths = await invoke<{ data_dir: string }>('get_data_paths')
|
||||||
const filename = selectedFile.split(/[/\\]/).pop() || 'selected file'
|
|
||||||
await handleRestoreBackup(selectedFile, filename)
|
const selected = await open({
|
||||||
|
multiple: false,
|
||||||
|
defaultPath: dataPaths.data_dir,
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
name: t('Backup Files', { ns: 'backuprestore' }),
|
||||||
|
extensions: ['zip'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
if (selected && typeof selected === 'string') {
|
||||||
|
// Validate it's a valid backup file
|
||||||
|
const filename =
|
||||||
|
selected.split(/[/\\]/).pop() || t('selected file', { ns: 'backuprestore' })
|
||||||
|
if (!filename.includes('pastebar-data-backup-') || !filename.endsWith('.zip')) {
|
||||||
|
toast({
|
||||||
|
id: 'backup-invalid-file',
|
||||||
|
title: t('Invalid backup file', { ns: 'backuprestore' }),
|
||||||
|
duration: 3000,
|
||||||
|
description: (
|
||||||
|
<Box className="word-break">
|
||||||
|
{t('The selected file is not a valid PasteBar backup', {
|
||||||
|
ns: 'backuprestore',
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
variant: 'destructive',
|
||||||
|
})
|
||||||
|
setRestoringFromFile(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await handleRestoreBackup(selected, filename)
|
||||||
}
|
}
|
||||||
|
// If selected is null, user cancelled the dialog - no action needed
|
||||||
|
setRestoringFromFile(false)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to select backup file:', error)
|
console.error('Failed to select backup file:', error)
|
||||||
toast({
|
toast({
|
||||||
title: t('Error', { ns: 'common' }),
|
title: t('Error', { ns: 'common' }),
|
||||||
description: `Failed to select backup file: ${error}`,
|
id: 'backup-select-error',
|
||||||
|
duration: 3000,
|
||||||
|
description: (
|
||||||
|
<Box className="word-break">
|
||||||
|
{t('Failed to select backup file', { ns: 'backuprestore' })}: {String(error)}
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
})
|
})
|
||||||
|
setRestoringFromFile(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDeleteBackup = async (backupPath: string, filename: string) => {
|
const handleDeleteBackup = async (backupPath: string, filename: string) => {
|
||||||
try {
|
try {
|
||||||
await invoke('delete_backup', { backupPath })
|
await invoke('delete_backup', { backupPath })
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
|
id: 'backup-delete-success',
|
||||||
title: t('Backup deleted successfully', { ns: 'backuprestore' }),
|
title: t('Backup deleted successfully', { ns: 'backuprestore' }),
|
||||||
description: filename,
|
duration: 3000,
|
||||||
|
description: <Box className="word-break">{filename}</Box>,
|
||||||
|
variant: 'success',
|
||||||
})
|
})
|
||||||
|
|
||||||
// Reload backup list
|
// Reload backup list
|
||||||
await loadBackups()
|
await loadBackups()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to delete backup:', error)
|
console.error('Failed to delete backup:', error)
|
||||||
toast({
|
toast({
|
||||||
|
id: 'backup-delete-error',
|
||||||
title: t('Failed to delete backup', { ns: 'backuprestore' }),
|
title: t('Failed to delete backup', { ns: 'backuprestore' }),
|
||||||
description: `${error}`,
|
duration: 3000,
|
||||||
|
description: <Box className="word-break">{`${error}`}</Box>,
|
||||||
|
variant: 'destructive',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBrowseBackupFolder = async (backupPath: string) => {
|
||||||
|
try {
|
||||||
|
// Extract directory from the backup file path
|
||||||
|
const backupDir = backupPath.substring(
|
||||||
|
0,
|
||||||
|
backupPath.lastIndexOf('/') || backupPath.lastIndexOf('\\')
|
||||||
|
)
|
||||||
|
await invoke('open_path_or_app', { path: backupDir })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to open backup folder:', error)
|
||||||
|
toast({
|
||||||
|
id: 'backup-open-error',
|
||||||
|
title: t('Error', { ns: 'common' }),
|
||||||
|
duration: 3000,
|
||||||
|
description: (
|
||||||
|
<Box className="word-break">
|
||||||
|
{t('Failed to open backup folder', { ns: 'backuprestore' })}: {String(error)}
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className="w-full h-full">
|
<AutoSize disableWidth>
|
||||||
<AutoSize disableWidth>
|
{({ height }) => {
|
||||||
{({ height }) => (
|
return (
|
||||||
<SimpleBar style={{ height, width: '100%' }} autoHide>
|
height && (
|
||||||
<Box className="flex flex-col gap-6 p-6">
|
<Box className="p-4 py-6 select-none min-w-[320px]">
|
||||||
{/* Header */}
|
<Box className="text-xl my-2 mx-2 flex items-center justify-between">
|
||||||
<Box className="flex items-center gap-3">
|
<Text className="light">
|
||||||
<Archive className="w-6 h-6 text-blue-600 dark:text-blue-400" />
|
|
||||||
<Text className="text-2xl font-semibold">
|
|
||||||
{t('Backup and Restore', { ns: 'backuprestore' })}
|
{t('Backup and Restore', { ns: 'backuprestore' })}
|
||||||
</Text>
|
</Text>
|
||||||
|
<Link to={returnRoute} replace>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="text-sm bg-slate-200 dark:bg-slate-700 dark:text-slate-200"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{t('Back', { ns: 'common' })}
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
</Box>
|
</Box>
|
||||||
|
<Spacer h={3} />
|
||||||
|
<SimpleBar style={{ maxHeight: height - 85 }} autoHide={true}>
|
||||||
|
<Box className="animate-in fade-in max-w-xl mt-4">
|
||||||
|
{/* Create Backup Section */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center justify-between">
|
||||||
|
<Flex className="items-center gap-2">
|
||||||
|
<Upload className="w-5 h-5" />
|
||||||
|
{t('Create Backup', { ns: 'backuprestore' })}
|
||||||
|
</Flex>
|
||||||
|
|
||||||
{/* Create Backup Section */}
|
{/* Include Images option - right corner */}
|
||||||
<Card>
|
<Flex className="items-center text-sm">
|
||||||
<CardHeader>
|
<Checkbox
|
||||||
<CardTitle className="flex items-center gap-2">
|
id="include-images"
|
||||||
<Upload className="w-5 h-5" />
|
checked={includeImages}
|
||||||
{t('Create Backup', { ns: 'backuprestore' })}
|
onChange={checked => setIncludeImages(checked as boolean)}
|
||||||
</CardTitle>
|
>
|
||||||
</CardHeader>
|
{t('Include images in backup', { ns: 'backuprestore' })}
|
||||||
<CardContent className="space-y-4">
|
</Checkbox>
|
||||||
<Flex className="items-center space-x-2">
|
</Flex>
|
||||||
<Checkbox
|
</CardTitle>
|
||||||
id="include-images"
|
</CardHeader>
|
||||||
checked={includeImages}
|
<CardContent className="space-y-4">
|
||||||
onCheckedChange={(checked) => setIncludeImages(checked as boolean)}
|
<AlertDialog>
|
||||||
/>
|
<AlertDialogTrigger asChild>
|
||||||
<label
|
<Button
|
||||||
htmlFor="include-images"
|
disabled={
|
||||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
isCreatingBackup || isRestoring || restoringFromFile
|
||||||
>
|
}
|
||||||
{t('Include images in backup', { ns: 'backuprestore' })}
|
className="w-full"
|
||||||
</label>
|
>
|
||||||
</Flex>
|
{isCreatingBackup ? (
|
||||||
|
<>
|
||||||
<AlertDialog>
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||||
<AlertDialogTrigger asChild>
|
{t('Creating backup...', { ns: 'backuprestore' })}
|
||||||
<Button disabled={isCreatingBackup} className="w-full">
|
</>
|
||||||
{isCreatingBackup ? (
|
) : (
|
||||||
|
<>
|
||||||
|
<Package className="w-4 h-4 mr-2" />
|
||||||
|
{t('Backup Now', { ns: 'backuprestore' })}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>
|
||||||
|
{t('Create a backup of your data?', {
|
||||||
|
ns: 'backuprestore',
|
||||||
|
})}
|
||||||
|
</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
{t(
|
||||||
|
'This will create a backup file containing your database',
|
||||||
|
{ ns: 'backuprestore' }
|
||||||
|
)}
|
||||||
|
{includeImages
|
||||||
|
? t('and images', { ns: 'backuprestore' })
|
||||||
|
: ''}
|
||||||
|
.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>
|
||||||
|
{t('Cancel', { ns: 'common' })}
|
||||||
|
</AlertDialogCancel>
|
||||||
|
<AlertDialogAction onClick={handleCreateBackup}>
|
||||||
|
{t('Create Backup', { ns: 'backuprestore' })}
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box className="animate-in fade-in max-w-xl mt-4">
|
||||||
|
{/* Restore Section */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center justify-between">
|
||||||
|
<Flex className="items-center gap-2">
|
||||||
|
<Download className="w-5 h-5" />
|
||||||
|
{t('Restore Data', { ns: 'backuprestore' })}
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Flex className="items-center text-sm">
|
||||||
|
<Checkbox
|
||||||
|
id="backup-on-restore"
|
||||||
|
checked={backupOnRestore}
|
||||||
|
onChange={checked => setBackupOnRestore(checked as boolean)}
|
||||||
|
>
|
||||||
|
{t('Auto Backup on Restore', { ns: 'backuprestore' })}
|
||||||
|
</Checkbox>
|
||||||
|
</Flex>
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleRestoreFromFile}
|
||||||
|
disabled={isRestoring || restoringFromFile}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{restoringFromFile ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||||
{t('Creating backup...', { ns: 'backuprestore' })}
|
{t('Restoring...', { ns: 'backuprestore' })}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Package className="w-4 h-4 mr-2" />
|
<FolderOpen className="w-4 h-4 mr-2" />
|
||||||
{t('Backup Now', { ns: 'backuprestore' })}
|
{t('Restore from File...', { ns: 'backuprestore' })}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDialogTrigger>
|
|
||||||
<AlertDialogContent>
|
|
||||||
<AlertDialogHeader>
|
|
||||||
<AlertDialogTitle>
|
|
||||||
{t('Create a backup of your data?', { ns: 'backuprestore' })}
|
|
||||||
</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription>
|
|
||||||
This will create a backup file containing your database
|
|
||||||
{includeImages ? ' and images' : ''}.
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel>{t('Cancel', { ns: 'common' })}</AlertDialogCancel>
|
|
||||||
<AlertDialogAction onClick={handleCreateBackup}>
|
|
||||||
{t('Create Backup', { ns: 'backuprestore' })}
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Restore Section */}
|
<Spacer h={4} />
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="flex items-center gap-2">
|
|
||||||
<Download className="w-5 h-5" />
|
|
||||||
{t('Restore Data', { ns: 'backuprestore' })}
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={handleRestoreFromFile}
|
|
||||||
disabled={isRestoring}
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
<FolderOpen className="w-4 h-4 mr-2" />
|
|
||||||
{t('Restore from File...', { ns: 'backuprestore' })}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Spacer h={4} />
|
{/* Available Backups */}
|
||||||
|
<Box>
|
||||||
|
<Flex className="items-center justify-between mb-3">
|
||||||
|
<Text className="font-medium">
|
||||||
|
{t('Available Backups', { ns: 'backuprestore' })}
|
||||||
|
</Text>
|
||||||
|
{totalSize && (
|
||||||
|
<Badge variant="secondary">
|
||||||
|
{t('Total backup space {{size}}', {
|
||||||
|
ns: 'backuprestore',
|
||||||
|
size: totalSize,
|
||||||
|
})}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
|
||||||
{/* Available Backups */}
|
{isLoadingBackups ? (
|
||||||
<Box>
|
<Flex className="items-center justify-center py-8">
|
||||||
<Flex className="items-center justify-between mb-3">
|
<Loader2 className="w-6 h-6 animate-spin" />
|
||||||
<Text className="font-medium">
|
<Text className="ml-2">
|
||||||
{t('Available Backups', { ns: 'backuprestore' })}
|
{t('Loading backups...', { ns: 'backuprestore' })}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
) : backups.length === 0 ? (
|
||||||
|
<Box className="text-center py-8 text-muted-foreground">
|
||||||
|
<HardDrive className="w-12 h-12 mx-auto mb-2 opacity-50" />
|
||||||
|
<Text>{t('No backups found', { ns: 'backuprestore' })}</Text>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Box className="space-y-3">
|
||||||
|
{backups.map(backup => {
|
||||||
|
const backupDate = parseBackupDate(backup.filename)
|
||||||
|
return (
|
||||||
|
<Card key={backup.filename} className="p-4">
|
||||||
|
<Box className="space-y-3">
|
||||||
|
{/* File name - bold on top */}
|
||||||
|
<Text className="font-bold text-lg">
|
||||||
|
{backup.filename}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{/* Date and size line */}
|
||||||
|
<Flex className="items-center justify-between text-sm text-muted-foreground">
|
||||||
|
<Flex className="items-center gap-2">
|
||||||
|
{backupDate ? (
|
||||||
|
<TimeAgo date={backupDate.getTime()} />
|
||||||
|
) : (
|
||||||
|
<span>{backup.created_date}</span>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
<span>{backup.size_formatted}</span>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{/* Buttons row */}
|
||||||
|
<Flex className="items-center justify-between">
|
||||||
|
{/* Delete button on the left */}
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<ToolTip
|
||||||
|
side="bottom"
|
||||||
|
text={t('Delete', { ns: 'backuprestore' })}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
||||||
|
disabled={isRestoring || restoringFromFile}
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</ToolTip>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>
|
||||||
|
{t(
|
||||||
|
'Delete this backup? This action cannot be undone.',
|
||||||
|
{ ns: 'backuprestore' }
|
||||||
|
)}
|
||||||
|
</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
{backup.filename} ({backup.size_formatted})
|
||||||
|
{t('will be permanently deleted.', {
|
||||||
|
ns: 'backuprestore',
|
||||||
|
})}
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>
|
||||||
|
{t('Cancel', { ns: 'common' })}
|
||||||
|
</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={() =>
|
||||||
|
handleDeleteBackup(
|
||||||
|
backup.full_path,
|
||||||
|
backup.filename
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className="bg-red-600 hover:bg-red-700"
|
||||||
|
>
|
||||||
|
{t('Delete', { ns: 'backuprestore' })}
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
|
||||||
|
{/* Browse and Restore buttons on the right */}
|
||||||
|
<Flex className="gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
handleBrowseBackupFolder(backup.full_path)
|
||||||
|
}
|
||||||
|
disabled={isRestoring || restoringFromFile}
|
||||||
|
>
|
||||||
|
<ExternalLink className="w-4 h-4 mr-1" />
|
||||||
|
{t('Browse', { ns: 'backuprestore' })}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
disabled={isRestoring || restoringFromFile}
|
||||||
|
>
|
||||||
|
{restoringBackupPath ===
|
||||||
|
backup.full_path ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-4 h-4 mr-1 animate-spin" />
|
||||||
|
{t('Restoring...', {
|
||||||
|
ns: 'backuprestore',
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<RotateCcw className="w-4 h-4 mr-1" />
|
||||||
|
{t('Restore', { ns: 'backuprestore' })}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>
|
||||||
|
{t(
|
||||||
|
'Restore from {{filename}}? This will replace all current data.',
|
||||||
|
{
|
||||||
|
ns: 'backuprestore',
|
||||||
|
filename: backup.filename,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
{t(
|
||||||
|
'This action cannot be undone. All current data will be replaced with the backup data.',
|
||||||
|
{ ns: 'backuprestore' }
|
||||||
|
)}
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>
|
||||||
|
{t('Cancel', { ns: 'common' })}
|
||||||
|
</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={() =>
|
||||||
|
handleRestoreBackup(
|
||||||
|
backup.full_path,
|
||||||
|
backup.filename
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className="bg-red-600 hover:bg-red-700"
|
||||||
|
>
|
||||||
|
{t('Restore', { ns: 'backuprestore' })}
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Restore note */}
|
||||||
|
<Text className="text-xs text-muted-foreground mt-2">
|
||||||
|
{t(
|
||||||
|
'Restoring a backup will automatically restart the application.',
|
||||||
|
{ ns: 'backuprestore' }
|
||||||
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
{totalSize && (
|
</CardContent>
|
||||||
<Badge variant="secondary">
|
</Card>
|
||||||
{t('Total backup space: {{size}}', {
|
|
||||||
ns: 'settings',
|
|
||||||
size: totalSize
|
|
||||||
})}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
{isLoadingBackups ? (
|
<Spacer h={6} />
|
||||||
<Flex className="items-center justify-center py-8">
|
<Link to={returnRoute} replace>
|
||||||
<Loader2 className="w-6 h-6 animate-spin" />
|
<Button
|
||||||
<Text className="ml-2">Loading backups...</Text>
|
variant="ghost"
|
||||||
</Flex>
|
className="text-sm bg-slate-200 dark:bg-slate-700 dark:text-slate-200"
|
||||||
) : backups.length === 0 ? (
|
size="sm"
|
||||||
<Box className="text-center py-8 text-muted-foreground">
|
>
|
||||||
<HardDrive className="w-12 h-12 mx-auto mb-2 opacity-50" />
|
{t('Back', { ns: 'common' })}
|
||||||
<Text>{t('No backups found', { ns: 'backuprestore' })}</Text>
|
</Button>
|
||||||
</Box>
|
</Link>
|
||||||
) : (
|
<Spacer h={4} />
|
||||||
<Box className="space-y-3">
|
</Box>
|
||||||
{backups.map((backup) => (
|
</SimpleBar>
|
||||||
<Card key={backup.filename} className="p-4">
|
|
||||||
<Flex className="items-center justify-between">
|
|
||||||
<Box className="flex-1">
|
|
||||||
<Flex className="items-center gap-2 mb-1">
|
|
||||||
<Archive className="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
|
||||||
<Text className="font-medium">{backup.filename}</Text>
|
|
||||||
</Flex>
|
|
||||||
<TextNormal className="text-sm text-muted-foreground">
|
|
||||||
{t('Created', { ns: 'common' })}: {backup.created_date}
|
|
||||||
</TextNormal>
|
|
||||||
<TextNormal className="text-sm text-muted-foreground">
|
|
||||||
{t('Size', { ns: 'common' })}: {backup.size_formatted}
|
|
||||||
</TextNormal>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Flex className="gap-2">
|
|
||||||
<AlertDialog>
|
|
||||||
<AlertDialogTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
disabled={isRestoring}
|
|
||||||
>
|
|
||||||
<RotateCcw className="w-4 h-4 mr-1" />
|
|
||||||
{t('Restore', { ns: 'backuprestore' })}
|
|
||||||
</Button>
|
|
||||||
</AlertDialogTrigger>
|
|
||||||
<AlertDialogContent>
|
|
||||||
<AlertDialogHeader>
|
|
||||||
<AlertDialogTitle>
|
|
||||||
{t('Restore from {{filename}}? This will replace all current data.', {
|
|
||||||
ns: 'settings',
|
|
||||||
filename: backup.filename
|
|
||||||
})}
|
|
||||||
</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription>
|
|
||||||
This action cannot be undone. All current data will be replaced with the backup data.
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel>{t('Cancel', { ns: 'common' })}</AlertDialogCancel>
|
|
||||||
<AlertDialogAction
|
|
||||||
onClick={() => handleRestoreBackup(backup.full_path, backup.filename)}
|
|
||||||
className="bg-red-600 hover:bg-red-700"
|
|
||||||
>
|
|
||||||
{t('Restore', { ns: 'backuprestore' })}
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
|
|
||||||
<AlertDialog>
|
|
||||||
<AlertDialogTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
|
||||||
>
|
|
||||||
<Trash2 className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</AlertDialogTrigger>
|
|
||||||
<AlertDialogContent>
|
|
||||||
<AlertDialogHeader>
|
|
||||||
<AlertDialogTitle>
|
|
||||||
{t('Delete this backup? This action cannot be undone.', { ns: 'backuprestore' })}
|
|
||||||
</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription>
|
|
||||||
{backup.filename} ({backup.size_formatted}) will be permanently deleted.
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel>{t('Cancel', { ns: 'common' })}</AlertDialogCancel>
|
|
||||||
<AlertDialogAction
|
|
||||||
onClick={() => handleDeleteBackup(backup.full_path, backup.filename)}
|
|
||||||
className="bg-red-600 hover:bg-red-700"
|
|
||||||
>
|
|
||||||
{t('Delete', { ns: 'backuprestore' })}
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</Box>
|
</Box>
|
||||||
</SimpleBar>
|
)
|
||||||
)}
|
)
|
||||||
</AutoSize>
|
}}
|
||||||
</Box>
|
</AutoSize>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,6 @@ import {
|
|||||||
themeStoreAtom,
|
themeStoreAtom,
|
||||||
uiStoreAtom,
|
uiStoreAtom,
|
||||||
} from '~/store'
|
} from '~/store'
|
||||||
import CustomDatabaseLocationSettings from './CustomDatabaseLocationSettings'
|
|
||||||
import { useAtomValue } from 'jotai'
|
import { useAtomValue } from 'jotai'
|
||||||
import { ChevronDown, ChevronUp, MessageSquare, MessageSquareDashed } from 'lucide-react'
|
import { ChevronDown, ChevronUp, MessageSquare, MessageSquareDashed } from 'lucide-react'
|
||||||
import { useTheme } from 'next-themes'
|
import { useTheme } from 'next-themes'
|
||||||
@ -40,6 +39,8 @@ import {
|
|||||||
|
|
||||||
import md from '~/store/example.md?raw'
|
import md from '~/store/example.md?raw'
|
||||||
|
|
||||||
|
import CustomDatabaseLocationSettings from './CustomDatabaseLocationSettings'
|
||||||
|
|
||||||
export default function UserPreferences() {
|
export default function UserPreferences() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
@ -521,7 +521,7 @@ div.note-content.release-notes {
|
|||||||
--accent: 210 40% 96.1%;
|
--accent: 210 40% 96.1%;
|
||||||
--accent-foreground: 222.2 47.4% 11.2%;
|
--accent-foreground: 222.2 47.4% 11.2%;
|
||||||
|
|
||||||
--destructive: 0 100% 50%;
|
--destructive: 0 50% 50%;
|
||||||
--destructive-foreground: 210 40% 98%;
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
|
||||||
--ring: 215 20.2% 65.1%;
|
--ring: 215 20.2% 65.1%;
|
||||||
@ -904,3 +904,7 @@ pre.code-editor-pre .token-line {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: 13px;
|
border-radius: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.word-break {
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
@ -3,12 +3,11 @@ use std::path::{Path, PathBuf};
|
|||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use chrono::{DateTime, Local};
|
use chrono::{DateTime, Local};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tauri::api::dialog::FileDialogBuilder;
|
|
||||||
use zip::{ZipWriter, ZipArchive};
|
use zip::{ZipWriter, ZipArchive};
|
||||||
use zip::write::FileOptions;
|
use zip::write::FileOptions;
|
||||||
use std::io::{Read, Seek};
|
use std::io::{Read, Seek};
|
||||||
|
|
||||||
use crate::db::APP_CONSTANTS;
|
use crate::db::{get_data_dir, get_db_path, get_clip_images_dir, get_clipboard_images_dir};
|
||||||
use crate::services::utils::debug_output;
|
use crate::services::utils::debug_output;
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
@ -27,10 +26,6 @@ pub struct BackupListResponse {
|
|||||||
pub total_size_formatted: String,
|
pub total_size_formatted: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_data_dir() -> PathBuf {
|
|
||||||
// Get current data directory - could be custom or default
|
|
||||||
APP_CONSTANTS.get().unwrap().app_data_dir.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_backup_filename() -> String {
|
fn get_backup_filename() -> String {
|
||||||
let now = Local::now();
|
let now = Local::now();
|
||||||
@ -106,16 +101,22 @@ pub async fn create_backup(include_images: bool) -> Result<String, String> {
|
|||||||
let backup_filename = get_backup_filename();
|
let backup_filename = get_backup_filename();
|
||||||
let backup_path = data_dir.join(&backup_filename);
|
let backup_path = data_dir.join(&backup_filename);
|
||||||
|
|
||||||
// Database file path
|
debug_output(|| {
|
||||||
let db_filename = if cfg!(debug_assertions) {
|
println!("Data directory: {}", data_dir.display());
|
||||||
"local.pastebar-db.data"
|
println!("Backup will be created at: {}", backup_path.display());
|
||||||
} else {
|
});
|
||||||
"pastebar-db.data"
|
|
||||||
};
|
// Database file path - use the actual database path which handles debug/release naming
|
||||||
let db_path = data_dir.join(db_filename);
|
let db_path_str = get_db_path();
|
||||||
|
let db_path = PathBuf::from(&db_path_str);
|
||||||
|
|
||||||
|
debug_output(|| {
|
||||||
|
println!("Looking for database file at: {}", db_path_str);
|
||||||
|
println!("Database file exists: {}", db_path.exists());
|
||||||
|
});
|
||||||
|
|
||||||
if !db_path.exists() {
|
if !db_path.exists() {
|
||||||
return Err("Database file not found".to_string());
|
return Err(format!("Database file not found at: {}", db_path_str));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create zip file
|
// Create zip file
|
||||||
@ -134,6 +135,11 @@ pub async fn create_backup(include_images: bool) -> Result<String, String> {
|
|||||||
db_file.read_to_end(&mut db_buffer)
|
db_file.read_to_end(&mut db_buffer)
|
||||||
.map_err(|e| format!("Failed to read database file: {}", e))?;
|
.map_err(|e| format!("Failed to read database file: {}", e))?;
|
||||||
|
|
||||||
|
// Get just the filename for the zip entry
|
||||||
|
let db_filename = db_path.file_name()
|
||||||
|
.and_then(|name| name.to_str())
|
||||||
|
.unwrap_or("pastebar-db.data");
|
||||||
|
|
||||||
zip.start_file(db_filename, options)
|
zip.start_file(db_filename, options)
|
||||||
.map_err(|e| format!("Failed to start database file in zip: {}", e))?;
|
.map_err(|e| format!("Failed to start database file in zip: {}", e))?;
|
||||||
zip.write_all(&db_buffer)
|
zip.write_all(&db_buffer)
|
||||||
@ -141,8 +147,15 @@ pub async fn create_backup(include_images: bool) -> Result<String, String> {
|
|||||||
|
|
||||||
// Add image directories if requested
|
// Add image directories if requested
|
||||||
if include_images {
|
if include_images {
|
||||||
let clip_images_dir = data_dir.join("clip-images");
|
let clip_images_dir = get_clip_images_dir();
|
||||||
let history_images_dir = data_dir.join("history-images");
|
let history_images_dir = get_clipboard_images_dir();
|
||||||
|
|
||||||
|
debug_output(|| {
|
||||||
|
println!("Clip images directory: {}", clip_images_dir.display());
|
||||||
|
println!("Clip images exists: {}", clip_images_dir.exists());
|
||||||
|
println!("History images directory: {}", history_images_dir.display());
|
||||||
|
println!("History images exists: {}", history_images_dir.exists());
|
||||||
|
});
|
||||||
|
|
||||||
if clip_images_dir.exists() {
|
if clip_images_dir.exists() {
|
||||||
add_directory_to_zip(&mut zip, &clip_images_dir, &data_dir)
|
add_directory_to_zip(&mut zip, &clip_images_dir, &data_dir)
|
||||||
@ -151,7 +164,7 @@ pub async fn create_backup(include_images: bool) -> Result<String, String> {
|
|||||||
|
|
||||||
if history_images_dir.exists() {
|
if history_images_dir.exists() {
|
||||||
add_directory_to_zip(&mut zip, &history_images_dir, &data_dir)
|
add_directory_to_zip(&mut zip, &history_images_dir, &data_dir)
|
||||||
.map_err(|e| format!("Failed to add history-images directory: {}", e))?;
|
.map_err(|e| format!("Failed to add clipboard-images directory: {}", e))?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -224,87 +237,38 @@ pub async fn list_backups() -> Result<BackupListResponse, String> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn select_backup_file() -> Result<Option<String>, String> {
|
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
use std::sync::mpsc;
|
|
||||||
|
|
||||||
let (tx, rx) = mpsc::channel();
|
|
||||||
|
|
||||||
FileDialogBuilder::new()
|
|
||||||
.set_title("Select PasteBar Backup File")
|
|
||||||
.add_filter("Backup Files", &["zip"])
|
|
||||||
.pick_file(move |file_path| {
|
|
||||||
let _ = tx.send(file_path);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Wait for the dialog result
|
|
||||||
if let Ok(selected_path) = rx.recv() {
|
|
||||||
if let Some(path) = selected_path {
|
|
||||||
let path_str = path.to_string_lossy().to_string();
|
|
||||||
// Validate it's a valid backup file
|
|
||||||
if let Err(e) = validate_backup_file(&path_str) {
|
|
||||||
return Err(format!("Invalid backup file: {}", e));
|
|
||||||
}
|
|
||||||
Ok(Some(path_str))
|
|
||||||
} else {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Err("Failed to get file dialog result".to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate_backup_file(file_path: &str) -> Result<(), String> {
|
|
||||||
let path = Path::new(file_path);
|
|
||||||
|
|
||||||
if !path.exists() {
|
|
||||||
return Err("File does not exist".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
if !path.extension().map_or(false, |ext| ext == "zip") {
|
|
||||||
return Err("File is not a zip file".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open zip and check for required files
|
|
||||||
let file = fs::File::open(path)
|
|
||||||
.map_err(|e| format!("Cannot open file: {}", e))?;
|
|
||||||
|
|
||||||
let mut archive = ZipArchive::new(file)
|
|
||||||
.map_err(|e| format!("Invalid zip file: {}", e))?;
|
|
||||||
|
|
||||||
// Check for database file
|
|
||||||
let db_files = ["pastebar-db.data", "local.pastebar-db.data"];
|
|
||||||
let has_db = db_files.iter().any(|&db_file| {
|
|
||||||
archive.by_name(db_file).is_ok()
|
|
||||||
});
|
|
||||||
|
|
||||||
if !has_db {
|
|
||||||
return Err("Backup does not contain a valid PasteBar database".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn restore_backup(backup_path: String) -> Result<String, String> {
|
pub async fn restore_backup(backup_path: String, create_pre_restore_backup: bool) -> Result<String, String> {
|
||||||
debug_output(|| {
|
debug_output(|| {
|
||||||
println!("Restoring backup from: {}", backup_path);
|
println!("Restoring backup from: {}", backup_path);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Validate backup file
|
// Basic validation - check if file exists and is a zip
|
||||||
validate_backup_file(&backup_path)?;
|
let backup_file = Path::new(&backup_path);
|
||||||
|
if !backup_file.exists() {
|
||||||
|
return Err("Backup file does not exist".to_string());
|
||||||
|
}
|
||||||
|
if !backup_file.extension().map_or(false, |ext| ext == "zip") {
|
||||||
|
return Err("Backup file must be a zip file".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
let data_dir = get_data_dir();
|
let data_dir = get_data_dir();
|
||||||
|
|
||||||
// Create backup of current data before restore
|
// Optionally create backup of current data before restore
|
||||||
let _pre_restore_backup = format!("pre-restore-backup-{}.zip", Local::now().format("%Y-%m-%d-%H-%M-%S"));
|
if create_pre_restore_backup {
|
||||||
let _pre_restore_path = data_dir.join(&_pre_restore_backup);
|
if let Err(e) = create_backup(true).await {
|
||||||
|
debug_output(|| {
|
||||||
// Create backup of current state
|
println!("Warning: Could not create pre-restore backup: {}", e);
|
||||||
if let Err(e) = create_backup(true).await {
|
});
|
||||||
|
} else {
|
||||||
|
debug_output(|| {
|
||||||
|
println!("Created pre-restore backup");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
debug_output(|| {
|
debug_output(|| {
|
||||||
println!("Warning: Could not create pre-restore backup: {}", e);
|
println!("Skipping pre-restore backup as requested");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1105,7 +1105,6 @@ async fn main() {
|
|||||||
update_setting,
|
update_setting,
|
||||||
backup_restore_commands::create_backup,
|
backup_restore_commands::create_backup,
|
||||||
backup_restore_commands::list_backups,
|
backup_restore_commands::list_backups,
|
||||||
backup_restore_commands::select_backup_file,
|
|
||||||
backup_restore_commands::restore_backup,
|
backup_restore_commands::restore_backup,
|
||||||
backup_restore_commands::delete_backup,
|
backup_restore_commands::delete_backup,
|
||||||
backup_restore_commands::get_data_paths,
|
backup_restore_commands::get_data_paths,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user