feat: enhance sharing features with security and expiration settings

- Introduced ShareSecurityModal and ShareExpirationModal components to manage share security and expiration settings.
- Updated SharesModals and SharesTable components to integrate new modals for managing security and expiration.
- Enhanced ShareManager to handle updates for security and expiration settings.
- Improved localization files to include new strings related to share security and expiration across multiple languages.
- Refactored existing components to support the new functionality, improving user experience in managing shared items.
This commit is contained in:
Daniel Luiz Alves
2025-06-02 16:52:46 -03:00
parent 5f4f8acbca
commit f067e160ba
26 changed files with 2319 additions and 242 deletions

View File

@@ -10,7 +10,10 @@
"yes": "نعم",
"no": "لا",
"dashboard": "لوحة القيادة",
"back": "رجوع"
"back": "رجوع",
"updating": "جاري التحديث...",
"saving": "جاري الحفظ...",
"update": "تحديث"
},
"createShare": {
"title": "إنشاء مشاركة",
@@ -18,7 +21,7 @@
"descriptionLabel": "الوصف",
"descriptionPlaceholder": "أدخل وصفًا (اختياري)",
"expirationLabel": "تاريخ الانتهاء",
"expirationPlaceholder": "MM/DD/YYYY HH:MM",
"expirationPlaceholder": "شهر/يوم/سنة ساعة:دقيقة",
"maxViewsLabel": "الحد الأقصى للمشاهدات",
"maxViewsPlaceholder": "اتركه فارغًا للحصول على عدد غير محدود",
"passwordProtection": "محمي بكلمة مرور",
@@ -275,7 +278,8 @@
"title": "الرفع الأخير",
"viewAll": "عرض الكل",
"uploadFile": "رفع ملف",
"noFiles": "لم يتم رفع أي ملفات بعد"
"noFiles": "لم يتم رفع أي ملفات بعد",
"upload": "رفع"
},
"recentShares": {
"title": "المشاركات الأخيرة",
@@ -296,7 +300,25 @@
"removeError": "فشل في إزالة المستلم",
"sendingNotifications": "جاري إرسال الإشعارات...",
"notifySuccess": "تم إعلام المستلمين بنجاح",
"notifyError": "فشل في إعلام المستلمين"
"notifyError": "فشل في إعلام المستلمين",
"bulkNotifySuccess": "تم إرسال إشعارات إلى {count} مستلم",
"selectAll": "تحديد الكل",
"singleNotifySuccess": "تم إرسال إشعار إلى {email}",
"removeSingle": "إزالة هذا المستلم",
"selectRecipient": "تحديد {email}",
"bulkRemoveSuccess": "تم إزالة {count} مستلم بنجاح",
"notifySingle": "إشعار هذا المستلم",
"notifySelected": "إشعار المحددين",
"invalidEmail": "يرجى إدخال عنوان بريد إلكتروني صالح",
"noRecipientsDescription": "إضافة مستلمين لمشاركة هذا المحتوى عبر البريد الإلكتروني",
"singleNotifyError": "فشل في إشعار المستلم",
"bulkRemoveError": "فشل في إزالة المستلمين المحددين",
"modalDescription": "إضافة وإدارة المستلمين لهذه المشاركة. يمكنك إشعار جميع أو مستلمين محددين عند تكوين SMTP.",
"duplicateEmail": "تم إضافة هذا المستلم بالفعل",
"removeSelected": "إزالة المحددين",
"selectedCount": "{count} محدد",
"addRecipient": "إضافة مستلم",
"bulkNotifyError": "فشل في إشعار المستلمين المحددين"
},
"register": {
"validation": {
@@ -536,7 +558,9 @@
"recipients": "المستقبلون",
"notAvailable": "غير متاح",
"invalidDate": "تاريخ غير صحيح",
"loadError": "فشل في تحميل تفاصيل المشاركة"
"loadError": "فشل في تحميل تفاصيل المشاركة",
"editSecurity": "تحرير الأمان",
"editExpiration": "تحرير انتهاء الصلاحية"
},
"shareManager": {
"deleteSuccess": "تم حذف المشاركة بنجاح",
@@ -554,7 +578,11 @@
"notifyError": "فشل في إعلام المستلمين",
"bulkDeleteError": "فشل في حذف المشاركات",
"bulkDeleteLoading": "جارٍ حذف {count, plural, =1 {مشاركة واحدة} other {# مشاركات}}...",
"bulkDeleteSuccess": "{count, plural, =1 {تم حذف مشاركة واحدة بنجاح} other {تم حذف # مشاركات بنجاح}}"
"bulkDeleteSuccess": "{count, plural, =1 {تم حذف مشاركة واحدة بنجاح} other {تم حذف # مشاركات بنجاح}}",
"securityUpdateError": "فشل في تحديث إعدادات الأمان",
"expirationUpdateError": "فشل في تحديث إعدادات انتهاء الصلاحية",
"securityUpdateSuccess": "تم تحديث إعدادات الأمان بنجاح",
"expirationUpdateSuccess": "تم تحديث إعدادات انتهاء الصلاحية بنجاح"
},
"shares": {
"errors": {
@@ -790,5 +818,63 @@
"totalSize": "الحجم الإجمالي",
"creating": "جاري الإنشاء...",
"create": "إنشاء مشاركة"
},
"shareSecurity": {
"subtitle": "تكوين حماية كلمة المرور وخيارات الأمان لهذه المشاركة",
"info": {
"title": "كيف يعمل:",
"withoutPassword": "يمكن لأي شخص لديه الرابط الوصول إلى هذه المشاركة بدون كلمة مرور.",
"withPassword": "سيحتاج المستخدمون إلى إدخال كلمة المرور للوصول إلى هذه المشاركة."
},
"existingPasswordMessage": "هذه المشاركة لديها بالفعل كلمة مرور. إذا كنت تريد تحديثها، أدخل كلمة المرور الجديدة في الحقل أدناه واحفظ.",
"passwordProtection": "حماية كلمة المرور",
"error": {
"updateFailed": "فشل في تحديث إعدادات الأمان"
},
"passwordRequirements": {
"title": "متطلبات كلمة المرور:",
"minLength": "على الأقل حرفان"
},
"newPassword": "كلمة مرور جديدة",
"success": {
"passwordUpdated": "تم تحديث كلمة المرور بنجاح",
"passwordRemoved": "تم إزالة حماية كلمة المرور بنجاح",
"passwordSet": "تم تمكين حماية كلمة المرور بنجاح"
},
"password": "كلمة المرور",
"validation": {
"passwordRequired": "كلمة المرور مطلوبة",
"passwordTooShort": "يجب أن تكون كلمة المرور حرفين على الأقل"
},
"currentStatus": "الحالة الحالية",
"passwordPlaceholder": "أدخل كلمة مرور آمنة",
"title": "إعدادات أمان المشاركة"
},
"shareExpiration": {
"neverExpires": "لا تنتهي صلاحيته أبداً",
"success": {
"expirationUpdated": "تم تحديث تاريخ انتهاء الصلاحية بنجاح",
"expirationRemoved": "تم إزالة انتهاء الصلاحية بنجاح - المشاركة الآن دائمة",
"expirationSet": "تم تعيين تاريخ انتهاء الصلاحية بنجاح"
},
"info": {
"canBeChanged": "يمكنك تغيير أو إزالة تاريخ انتهاء الصلاحية في أي وقت",
"willBeInaccessible": "ستصبح المشاركة غير قابلة للوصول بعد هذا التاريخ",
"noExpiration": "هذه المشاركة لن تنتهي صلاحيتها أبداً وستبقى قابلة للوصول إلى أجل غير مسمى.",
"title": "حول انتهاء الصلاحية:"
},
"enableExpiration": "تمكين انتهاء الصلاحية",
"title": "إعدادات انتهاء صلاحية المشاركة",
"subtitle": "تكوين متى ستنتهي صلاحية هذه المشاركة",
"validation": {
"dateMustBeFuture": "يجب أن يكون تاريخ انتهاء الصلاحية في المستقبل",
"dateRequired": "يرجى تحديد تاريخ انتهاء صلاحية"
},
"currentStatus": "الحالة الحالية",
"error": {
"updateFailed": "فشل في تحديث إعدادات انتهاء الصلاحية"
},
"expires": "تنتهي صلاحيته:",
"expirationDate": "تاريخ انتهاء الصلاحية"
}
}

View File

@@ -10,7 +10,10 @@
"yes": "Ja",
"no": "Nein",
"dashboard": "Dashboard",
"back": "Zurück"
"back": "Zurück",
"updating": "Aktualisierung läuft...",
"saving": "Speichere...",
"update": "Aktualisieren"
},
"createShare": {
"title": "Freigabe Erstellen",
@@ -140,7 +143,7 @@
}
},
"footer": {
"poweredBy": "Powered by",
"poweredBy": "Angetrieben von",
"kyanHomepage": "Kyantech Homepage"
},
"forgotPassword": {
@@ -275,7 +278,8 @@
"title": "Kürzlich hochgeladen",
"viewAll": "Alle anzeigen",
"uploadFile": "Datei hochladen",
"noFiles": "Noch keine Dateien hochgeladen"
"noFiles": "Noch keine Dateien hochgeladen",
"upload": "Hochladen"
},
"recentShares": {
"title": "Kürzlich geteilte",
@@ -296,7 +300,25 @@
"removeError": "Fehler beim Entfernen des Empfängers",
"sendingNotifications": "Benachrichtigungen werden gesendet...",
"notifySuccess": "Empfänger erfolgreich benachrichtigt",
"notifyError": "Fehler beim Benachrichtigen der Empfänger"
"notifyError": "Fehler beim Benachrichtigen der Empfänger",
"bulkNotifySuccess": "Benachrichtigungen an {count} Empfänger gesendet",
"selectAll": "Alle auswählen",
"singleNotifySuccess": "Benachrichtigung an {email} gesendet",
"removeSingle": "Diesen Empfänger entfernen",
"selectRecipient": "{email} auswählen",
"bulkRemoveSuccess": "{count} Empfänger erfolgreich entfernt",
"notifySingle": "Diesen Empfänger benachrichtigen",
"notifySelected": "Ausgewählte benachrichtigen",
"invalidEmail": "Bitte geben Sie eine gültige E-Mail-Adresse ein",
"noRecipientsDescription": "Empfänger hinzufügen, um diesen Inhalt per E-Mail zu teilen",
"singleNotifyError": "Empfänger konnte nicht benachrichtigt werden",
"bulkRemoveError": "Ausgewählte Empfänger konnten nicht entfernt werden",
"modalDescription": "Empfänger für diese Freigabe hinzufügen und verwalten. Sie können alle oder bestimmte Empfänger benachrichtigen, wenn SMTP konfiguriert ist.",
"duplicateEmail": "Dieser Empfänger wurde bereits hinzugefügt",
"removeSelected": "Ausgewählte entfernen",
"selectedCount": "{count} ausgewählt",
"addRecipient": "Empfänger hinzufügen",
"bulkNotifyError": "Ausgewählte Empfänger konnten nicht benachrichtigt werden"
},
"register": {
"validation": {
@@ -536,7 +558,9 @@
"recipients": "Empfänger",
"notAvailable": "N/V",
"invalidDate": "Ungültiges Datum",
"loadError": "Fehler beim Laden der Freigabe-Details"
"loadError": "Fehler beim Laden der Freigabe-Details",
"editSecurity": "Sicherheit bearbeiten",
"editExpiration": "Ablauf bearbeiten"
},
"shareManager": {
"deleteSuccess": "Freigabe erfolgreich gelöscht",
@@ -554,7 +578,11 @@
"notifyError": "Fehler beim Benachrichtigen der Empfänger",
"bulkDeleteError": "Fehler beim Löschen der Freigaben",
"bulkDeleteLoading": "Lösche {count, plural, =1 {1 Freigabe} other {# Freigaben}}...",
"bulkDeleteSuccess": "{count, plural, =1 {1 Freigabe erfolgreich gelöscht} other {# Freigaben erfolgreich gelöscht}}"
"bulkDeleteSuccess": "{count, plural, =1 {1 Freigabe erfolgreich gelöscht} other {# Freigaben erfolgreich gelöscht}}",
"securityUpdateError": "Sicherheitseinstellungen konnten nicht aktualisiert werden",
"expirationUpdateError": "Ablaufeinstellungen konnten nicht aktualisiert werden",
"securityUpdateSuccess": "Sicherheitseinstellungen erfolgreich aktualisiert",
"expirationUpdateSuccess": "Ablaufeinstellungen erfolgreich aktualisiert"
},
"shares": {
"errors": {
@@ -790,5 +818,63 @@
"totalSize": "Gesamtgröße",
"creating": "Erstellen...",
"create": "Freigabe Erstellen"
},
"shareSecurity": {
"subtitle": "Passwortschutz und Sicherheitsoptionen für diese Freigabe konfigurieren",
"info": {
"title": "So funktioniert es:",
"withoutPassword": "Jeder mit dem Link kann auf diese Freigabe ohne Passwort zugreifen.",
"withPassword": "Benutzer müssen das Passwort eingeben, um auf diese Freigabe zuzugreifen."
},
"existingPasswordMessage": "Diese Freigabe hat bereits ein Passwort. Wenn Sie es aktualisieren möchten, geben Sie das neue Passwort in das Feld unten ein und speichern Sie.",
"passwordProtection": "Passwortschutz",
"error": {
"updateFailed": "Sicherheitseinstellungen konnten nicht aktualisiert werden"
},
"passwordRequirements": {
"title": "Passwort-Anforderungen:",
"minLength": "Mindestens 2 Zeichen"
},
"newPassword": "Neues Passwort",
"success": {
"passwordUpdated": "Passwort erfolgreich aktualisiert",
"passwordRemoved": "Passwortschutz erfolgreich entfernt",
"passwordSet": "Passwortschutz erfolgreich aktiviert"
},
"password": "Passwort",
"validation": {
"passwordRequired": "Passwort ist erforderlich",
"passwordTooShort": "Passwort muss mindestens 2 Zeichen haben"
},
"currentStatus": "Aktueller Status",
"passwordPlaceholder": "Sicheres Passwort eingeben",
"title": "Freigabe-Sicherheitseinstellungen"
},
"shareExpiration": {
"neverExpires": "Läuft nie ab",
"success": {
"expirationUpdated": "Ablaufdatum erfolgreich aktualisiert",
"expirationRemoved": "Ablauf erfolgreich entfernt - Freigabe ist jetzt dauerhaft",
"expirationSet": "Ablaufdatum erfolgreich festgelegt"
},
"info": {
"canBeChanged": "Sie können das Ablaufdatum jederzeit ändern oder entfernen",
"willBeInaccessible": "Die Freigabe wird nach diesem Datum unzugänglich",
"noExpiration": "Diese Freigabe läuft nie ab und bleibt unbegrenzt zugänglich.",
"title": "Über Ablauf:"
},
"enableExpiration": "Ablauf aktivieren",
"title": "Freigabe-Ablaufeinstellungen",
"subtitle": "Konfigurieren, wann diese Freigabe abläuft",
"validation": {
"dateMustBeFuture": "Ablaufdatum muss in der Zukunft liegen",
"dateRequired": "Bitte wählen Sie ein Ablaufdatum"
},
"currentStatus": "Aktueller Status",
"error": {
"updateFailed": "Ablaufeinstellungen konnten nicht aktualisiert werden"
},
"expires": "Läuft ab:",
"expirationDate": "Ablaufdatum"
}
}

View File

@@ -3,6 +3,9 @@
"loading": "Loading, please wait...",
"cancel": "Cancel",
"save": "Save",
"saving": "Saving...",
"update": "Update",
"updating": "Updating...",
"delete": "Delete",
"close": "Close",
"download": "Download",
@@ -274,6 +277,7 @@
"recentFiles": {
"title": "Recent Uploads",
"viewAll": "View All",
"upload": "Upload",
"uploadFile": "Upload File",
"noFiles": "No files uploaded yet"
},
@@ -296,7 +300,25 @@
"removeError": "Failed to remove recipient",
"sendingNotifications": "Sending notifications...",
"notifySuccess": "Recipients notified successfully",
"notifyError": "Failed to notify recipients"
"notifyError": "Failed to notify recipients",
"selectAll": "Select all",
"selectedCount": "{count} selected",
"selectRecipient": "Select {email}",
"notifySelected": "Notify Selected",
"removeSelected": "Remove Selected",
"notifySingle": "Notify this recipient",
"removeSingle": "Remove this recipient",
"bulkRemoveSuccess": "{count} recipients removed successfully",
"bulkRemoveError": "Failed to remove selected recipients",
"bulkNotifySuccess": "Notifications sent to {count} recipients",
"bulkNotifyError": "Failed to notify selected recipients",
"singleNotifySuccess": "Notification sent to {email}",
"singleNotifyError": "Failed to notify recipient",
"modalDescription": "Add and manage recipients for this share. You can notify all or specific recipients when SMTP is configured.",
"addRecipient": "Add Recipient",
"invalidEmail": "Please enter a valid email address",
"duplicateEmail": "This recipient has already been added",
"noRecipientsDescription": "Add recipients to share this content via email"
},
"register": {
"validation": {
@@ -529,6 +551,8 @@
"expires": "Expires",
"never": "Never",
"security": "Security",
"editSecurity": "Edit Security",
"editExpiration": "Edit Expiration",
"passwordProtected": "Password Protected",
"publicAccess": "Public Access",
"maxViews": "Max Views:",
@@ -538,11 +562,73 @@
"invalidDate": "Invalid date",
"loadError": "Failed to load share details"
},
"shareSecurity": {
"title": "Share Security Settings",
"subtitle": "Configure password protection and security options for this share",
"currentStatus": "Current Status",
"passwordProtection": "Password Protection",
"password": "Password",
"newPassword": "New Password",
"passwordPlaceholder": "Enter a secure password",
"existingPasswordMessage": "This share already has a password. If you want to update it, enter the new password in the field below and save.",
"passwordRequirements": {
"title": "Password requirements:",
"minLength": "At least 2 characters"
},
"info": {
"title": "How it works:",
"withPassword": "Users will need to enter the password to access this share.",
"withoutPassword": "Anyone with the link can access this share without a password."
},
"validation": {
"passwordRequired": "Password is required",
"passwordTooShort": "Password must be at least 2 characters"
},
"success": {
"passwordSet": "Password protection enabled successfully",
"passwordUpdated": "Password updated successfully",
"passwordRemoved": "Password protection removed successfully"
},
"error": {
"updateFailed": "Failed to update security settings"
}
},
"shareExpiration": {
"title": "Share Expiration Settings",
"subtitle": "Configure when this share will expire",
"currentStatus": "Current Status",
"expires": "Expires:",
"neverExpires": "Never Expires",
"enableExpiration": "Enable Expiration",
"expirationDate": "Expiration Date",
"validation": {
"dateRequired": "Please select an expiration date",
"dateMustBeFuture": "Expiration date must be in the future"
},
"success": {
"expirationSet": "Expiration date set successfully",
"expirationUpdated": "Expiration date updated successfully",
"expirationRemoved": "Expiration removed successfully - share is now permanent"
},
"error": {
"updateFailed": "Failed to update expiration settings"
},
"info": {
"title": "About expiration:",
"willBeInaccessible": "The share will become inaccessible after this date",
"canBeChanged": "You can change or remove the expiration date anytime",
"noExpiration": "This share will never expire and will remain accessible indefinitely."
}
},
"shareManager": {
"deleteSuccess": "Share deleted successfully",
"deleteError": "Failed to delete share",
"updateSuccess": "Share updated successfully",
"updateError": "Failed to update share",
"securityUpdateSuccess": "Security settings updated successfully",
"securityUpdateError": "Failed to update security settings",
"expirationUpdateSuccess": "Expiration settings updated successfully",
"expirationUpdateError": "Failed to update expiration settings",
"filesUpdateSuccess": "Files updated successfully",
"filesUpdateError": "Failed to update files",
"recipientsUpdateSuccess": "Recipients updated successfully",

View File

@@ -10,7 +10,10 @@
"yes": "Sí",
"no": "No",
"dashboard": "Panel",
"back": "Volver"
"back": "Volver",
"updating": "Actualizando...",
"saving": "Guardando...",
"update": "Actualizar"
},
"createShare": {
"title": "Crear Compartir",
@@ -272,9 +275,10 @@
}
},
"recentFiles": {
"title": "Subidas recientes",
"viewAll": "Ver todo",
"uploadFile": "Subir archivo",
"title": "Cargas Recientes",
"viewAll": "Ver Todo",
"upload": "Subir",
"uploadFile": "Subir Archivo",
"noFiles": "Aún no se han subido archivos"
},
"recentShares": {
@@ -296,7 +300,25 @@
"removeError": "Error al eliminar el destinatario",
"sendingNotifications": "Enviando notificaciones...",
"notifySuccess": "Destinatarios notificados exitosamente",
"notifyError": "Error al notificar a los destinatarios"
"notifyError": "Error al notificar a los destinatarios",
"bulkNotifySuccess": "Notificaciones enviadas a {count} destinatarios",
"selectAll": "Seleccionar todo",
"singleNotifySuccess": "Notificación enviada a {email}",
"removeSingle": "Eliminar este destinatario",
"selectRecipient": "Seleccionar {email}",
"bulkRemoveSuccess": "{count} destinatarios eliminados exitosamente",
"notifySingle": "Notificar este destinatario",
"notifySelected": "Notificar Seleccionados",
"invalidEmail": "Por favor ingresa una dirección de correo válida",
"noRecipientsDescription": "Agregar destinatarios para compartir este contenido por correo",
"singleNotifyError": "Error al notificar destinatario",
"bulkRemoveError": "Error al eliminar destinatarios seleccionados",
"modalDescription": "Agregar y gestionar destinatarios para este compartir. Puedes notificar a todos o destinatarios específicos cuando SMTP esté configurado.",
"duplicateEmail": "Este destinatario ya ha sido agregado",
"removeSelected": "Eliminar Seleccionados",
"selectedCount": "{count} seleccionados",
"addRecipient": "Agregar Destinatario",
"bulkNotifyError": "Error al notificar destinatarios seleccionados"
},
"register": {
"validation": {
@@ -534,9 +556,11 @@
"maxViews": "Vistas Máx.:",
"files": "Archivos",
"recipients": "Destinatarios",
"notAvailable": "N/A",
"notAvailable": "N/D",
"invalidDate": "Fecha inválida",
"loadError": "Error al cargar detalles del compartir"
"loadError": "Error al cargar detalles del compartir",
"editSecurity": "Editar Seguridad",
"editExpiration": "Editar Expiración"
},
"shareManager": {
"deleteSuccess": "Compartición eliminada exitosamente",
@@ -554,7 +578,11 @@
"notifyError": "Error al notificar a los destinatarios",
"bulkDeleteError": "Error al eliminar compartidos",
"bulkDeleteLoading": "Eliminando {count, plural, =1 {1 compartido} other {# compartidos}}...",
"bulkDeleteSuccess": "{count, plural, =1 {1 compartido eliminado con éxito} other {# compartidos eliminados con éxito}}"
"bulkDeleteSuccess": "{count, plural, =1 {1 compartido eliminado con éxito} other {# compartidos eliminados con éxito}}",
"securityUpdateError": "Error al actualizar configuración de seguridad",
"expirationUpdateError": "Error al actualizar configuración de expiración",
"securityUpdateSuccess": "Configuración de seguridad actualizada exitosamente",
"expirationUpdateSuccess": "Configuración de expiración actualizada exitosamente"
},
"shares": {
"errors": {
@@ -790,5 +818,63 @@
"totalSize": "Tamaño total",
"creating": "Creando...",
"create": "Crear Compartir"
},
"shareSecurity": {
"subtitle": "Configurar protección por contraseña y opciones de seguridad para este compartir",
"info": {
"title": "Cómo funciona:",
"withoutPassword": "Cualquiera con el enlace puede acceder a este compartir sin contraseña.",
"withPassword": "Los usuarios necesitarán ingresar la contraseña para acceder a este compartir."
},
"existingPasswordMessage": "Este compartir ya tiene una contraseña. Si quieres actualizarla, ingresa la nueva contraseña en el campo de abajo y guarda.",
"passwordProtection": "Protección por Contraseña",
"error": {
"updateFailed": "Error al actualizar configuración de seguridad"
},
"passwordRequirements": {
"title": "Requisitos de contraseña:",
"minLength": "Al menos 2 caracteres"
},
"newPassword": "Nueva Contraseña",
"success": {
"passwordUpdated": "Contraseña actualizada exitosamente",
"passwordRemoved": "Protección por contraseña eliminada exitosamente",
"passwordSet": "Protección por contraseña habilitada exitosamente"
},
"password": "Contraseña",
"validation": {
"passwordRequired": "La contraseña es requerida",
"passwordTooShort": "La contraseña debe tener al menos 2 caracteres"
},
"currentStatus": "Estado Actual",
"passwordPlaceholder": "Ingresa una contraseña segura",
"title": "Configuración de Seguridad del Compartir"
},
"shareExpiration": {
"neverExpires": "Nunca Expira",
"success": {
"expirationUpdated": "Fecha de expiración actualizada exitosamente",
"expirationRemoved": "Expiración eliminada exitosamente - el compartir es ahora permanente",
"expirationSet": "Fecha de expiración establecida exitosamente"
},
"info": {
"canBeChanged": "Puedes cambiar o eliminar la fecha de expiración en cualquier momento",
"willBeInaccessible": "El compartir será inaccesible después de esta fecha",
"noExpiration": "Este compartir nunca expirará y permanecerá accesible indefinidamente.",
"title": "Acerca de la expiración:"
},
"enableExpiration": "Habilitar Expiración",
"title": "Configuración de Expiración del Compartir",
"subtitle": "Configurar cuándo expirará este compartir",
"validation": {
"dateMustBeFuture": "La fecha de expiración debe estar en el futuro",
"dateRequired": "Por favor selecciona una fecha de expiración"
},
"currentStatus": "Estado Actual",
"error": {
"updateFailed": "Error al actualizar configuración de expiración"
},
"expires": "Expira:",
"expirationDate": "Fecha de Expiración"
}
}

View File

@@ -10,7 +10,10 @@
"yes": "Oui",
"no": "Non",
"dashboard": "Tableau de bord",
"back": "Retour"
"back": "Retour",
"updating": "Mise à jour...",
"saving": "Enregistrement...",
"update": "Mettre à jour"
},
"createShare": {
"title": "Créer un Partage",
@@ -231,7 +234,7 @@
"firstName": "Prénom",
"lastName": "Nom",
"username": "Nom d'Utilisateur",
"email": "Email",
"email": "E-mail",
"updateButton": "Mettre à Jour le Profil"
},
"header": {
@@ -275,7 +278,8 @@
"title": "Téléchargements Récents",
"viewAll": "Voir Tout",
"uploadFile": "Envoyer un Fichier",
"noFiles": "Aucun fichier téléchargé pour le moment"
"noFiles": "Aucun fichier téléchargé pour le moment",
"upload": "Télécharger"
},
"recentShares": {
"title": "Partages Récents",
@@ -296,7 +300,25 @@
"removeError": "Échec de la suppression du destinataire",
"sendingNotifications": "Envoi des notifications...",
"notifySuccess": "Destinataires notifiés avec succès",
"notifyError": "Échec de la notification des destinataires"
"notifyError": "Échec de la notification des destinataires",
"bulkNotifySuccess": "Notifications envoyées à {count} destinataires",
"selectAll": "Tout sélectionner",
"singleNotifySuccess": "Notification envoyée à {email}",
"removeSingle": "Supprimer ce destinataire",
"selectRecipient": "Sélectionner {email}",
"bulkRemoveSuccess": "{count} destinataires supprimés avec succès",
"notifySingle": "Notifier ce destinataire",
"notifySelected": "Notifier les Sélectionnés",
"invalidEmail": "Veuillez entrer une adresse email valide",
"noRecipientsDescription": "Ajouter des destinataires pour partager ce contenu par email",
"singleNotifyError": "Échec de notification du destinataire",
"bulkRemoveError": "Échec de suppression des destinataires sélectionnés",
"modalDescription": "Ajouter et gérer les destinataires pour ce partage. Vous pouvez notifier tous ou des destinataires spécifiques lorsque SMTP est configuré.",
"duplicateEmail": "Ce destinataire a déjà été ajouté",
"removeSelected": "Supprimer les Sélectionnés",
"selectedCount": "{count} sélectionnés",
"addRecipient": "Ajouter Destinataire",
"bulkNotifyError": "Échec de notification des destinataires sélectionnés"
},
"register": {
"validation": {
@@ -312,7 +334,7 @@
"firstName": "Prénom",
"lastName": "Nom",
"username": "Nom d'utilisateur",
"email": "Email",
"email": "E-mail",
"password": "Mot de passe"
},
"buttons": {
@@ -358,7 +380,7 @@
"description": "Paramètres de base de l'application"
},
"email": {
"title": "Email",
"title": "E-mail",
"description": "Configuration du serveur de messagerie"
},
"security": {
@@ -534,9 +556,11 @@
"maxViews": "Vues Max.:",
"files": "Fichiers",
"recipients": "Destinataires",
"notAvailable": "N/A",
"notAvailable": "N/D",
"invalidDate": "Date invalide",
"loadError": "Échec du chargement des détails du partage"
"loadError": "Échec du chargement des détails du partage",
"editSecurity": "Modifier la Sécurité",
"editExpiration": "Modifier l'Expiration"
},
"shareManager": {
"deleteSuccess": "Partage supprimé avec succès",
@@ -554,7 +578,11 @@
"notifyError": "Échec de la notification des destinataires",
"bulkDeleteError": "Échec de la suppression des partages",
"bulkDeleteLoading": "Suppression de {count, plural, =1 {1 partage} other {# partages}}...",
"bulkDeleteSuccess": "{count, plural, =1 {1 partage supprimé avec succès} other {# partages supprimés avec succès}}"
"bulkDeleteSuccess": "{count, plural, =1 {1 partage supprimé avec succès} other {# partages supprimés avec succès}}",
"securityUpdateError": "Échec de mise à jour des paramètres de sécurité",
"expirationUpdateError": "Échec de mise à jour des paramètres d'expiration",
"securityUpdateSuccess": "Paramètres de sécurité mis à jour avec succès",
"expirationUpdateSuccess": "Paramètres d'expiration mis à jour avec succès"
},
"shares": {
"errors": {
@@ -700,7 +728,7 @@
"firstName": "Prénom",
"lastName": "Nom",
"username": "Nom d'Utilisateur",
"email": "Email",
"email": "E-mail",
"password": "Mot de Passe",
"newPassword": "Nouveau Mot de Passe (optionnel)",
"passwordPlaceholder": "Laisser vide pour garder le mot de passe actuel",
@@ -730,7 +758,7 @@
"actions": "ACTIONS",
"active": "Actif",
"inactive": "Inactif",
"admin": "Admin",
"admin": "Administrateur",
"userr": "Utilisateur"
}
},
@@ -790,5 +818,63 @@
"totalSize": "Taille totale",
"creating": "Création...",
"create": "Créer un Partage"
},
"shareSecurity": {
"subtitle": "Configurer la protection par mot de passe et les options de sécurité pour ce partage",
"info": {
"title": "Comment ça marche:",
"withoutPassword": "Toute personne ayant le lien peut accéder à ce partage sans mot de passe.",
"withPassword": "Les utilisateurs devront entrer le mot de passe pour accéder à ce partage."
},
"existingPasswordMessage": "Ce partage a déjà un mot de passe. Si vous voulez le mettre à jour, entrez le nouveau mot de passe dans le champ ci-dessous et enregistrez.",
"passwordProtection": "Protection par Mot de Passe",
"error": {
"updateFailed": "Échec de mise à jour des paramètres de sécurité"
},
"passwordRequirements": {
"title": "Exigences du mot de passe:",
"minLength": "Au moins 2 caractères"
},
"newPassword": "Nouveau Mot de Passe",
"success": {
"passwordUpdated": "Mot de passe mis à jour avec succès",
"passwordRemoved": "Protection par mot de passe supprimée avec succès",
"passwordSet": "Protection par mot de passe activée avec succès"
},
"password": "Mot de Passe",
"validation": {
"passwordRequired": "Le mot de passe est requis",
"passwordTooShort": "Le mot de passe doit contenir au moins 2 caractères"
},
"currentStatus": "Statut Actuel",
"passwordPlaceholder": "Entrez un mot de passe sécurisé",
"title": "Paramètres de Sécurité du Partage"
},
"shareExpiration": {
"neverExpires": "N'expire Jamais",
"success": {
"expirationUpdated": "Date d'expiration mise à jour avec succès",
"expirationRemoved": "Expiration supprimée avec succès - le partage est maintenant permanent",
"expirationSet": "Date d'expiration définie avec succès"
},
"info": {
"canBeChanged": "Vous pouvez changer ou supprimer la date d'expiration à tout moment",
"willBeInaccessible": "Le partage deviendra inaccessible après cette date",
"noExpiration": "Ce partage n'expirera jamais et restera accessible indéfiniment.",
"title": "À propos de l'expiration:"
},
"enableExpiration": "Activer l'Expiration",
"title": "Paramètres d'Expiration du Partage",
"subtitle": "Configurer quand ce partage expirera",
"validation": {
"dateMustBeFuture": "La date d'expiration doit être dans le futur",
"dateRequired": "Veuillez sélectionner une date d'expiration"
},
"currentStatus": "Statut Actuel",
"error": {
"updateFailed": "Échec de mise à jour des paramètres d'expiration"
},
"expires": "Expire:",
"expirationDate": "Date d'Expiration"
}
}

View File

@@ -10,7 +10,10 @@
"yes": "हाँ",
"no": "नहीं",
"dashboard": "डैशबोर्ड",
"back": "वापस"
"back": "वापस",
"updating": "अपडेट हो रहा है...",
"saving": "सहेज रहा है...",
"update": "अपडेट करें"
},
"createShare": {
"title": "साझाकरण बनाएं",
@@ -275,7 +278,8 @@
"title": "हाल की अपलोड्स",
"viewAll": "सभी देखें",
"uploadFile": "फाइल अपलोड करें",
"noFiles": "अभी तक कोई फाइल अपलोड नहीं हुई"
"noFiles": "अभी तक कोई फाइल अपलोड नहीं हुई",
"upload": "अपलोड"
},
"recentShares": {
"title": "हाल के साझाकरण",
@@ -296,7 +300,25 @@
"removeError": "प्राप्तकर्ता हटाने में विफल",
"sendingNotifications": "सूचनाएँ भेजी जा रही हैं...",
"notifySuccess": "प्राप्तकर्ताओं को सफलतापूर्वक सूचित किया गया",
"notifyError": "प्राप्तकर्ताओं को सूचित करने में विफल"
"notifyError": "प्राप्तकर्ताओं को सूचित करने में विफल",
"bulkNotifySuccess": "{count} प्राप्तकर्ताओं को सूचनाएं भेजी गईं",
"selectAll": "सभी चुनें",
"singleNotifySuccess": "{email} को सूचना भेजी गई",
"removeSingle": "इस प्राप्तकर्ता को हटाएं",
"selectRecipient": "{email} चुनें",
"bulkRemoveSuccess": "{count} प्राप्तकर्ता सफलतापूर्वक हटाए गए",
"notifySingle": "इस प्राप्तकर्ता को सूचित करें",
"notifySelected": "चयनित को सूचित करें",
"invalidEmail": "कृपया एक वैध ईमेल पता दर्ज करें",
"noRecipientsDescription": "ईमेल के माध्यम से इस सामग्री को साझा करने के लिए प्राप्तकर्ता जोड़ें",
"singleNotifyError": "प्राप्तकर्ता को सूचित करने में विफल",
"bulkRemoveError": "चयनित प्राप्तकर्ताओं को हटाने में विफल",
"modalDescription": "इस साझाकरण के लिए प्राप्तकर्ता जोड़ें और प्रबंधित करें। जब SMTP कॉन्फ़िगर किया गया हो तो आप सभी या विशिष्ट प्राप्तकर्ताओं को सूचित कर सकते हैं।",
"duplicateEmail": "यह प्राप्तकर्ता पहले से ही जोड़ा गया है",
"removeSelected": "चयनित हटाएं",
"selectedCount": "{count} चयनित",
"addRecipient": "प्राप्तकर्ता जोड़ें",
"bulkNotifyError": "चयनित प्राप्तकर्ताओं को सूचित करने में विफल"
},
"register": {
"validation": {
@@ -536,7 +558,9 @@
"recipients": "प्राप्तकर्ता",
"notAvailable": "उप/नहीं",
"invalidDate": "अमान्य तिथि",
"loadError": "साझाकरण विवरण लोड करने में विफल"
"loadError": "साझाकरण विवरण लोड करने में विफल",
"editSecurity": "सुरक्षा संपादित करें",
"editExpiration": "समाप्ति संपादित करें"
},
"shareManager": {
"deleteSuccess": "साझाकरण सफलतापूर्वक हटाया गया",
@@ -554,7 +578,11 @@
"notifyError": "प्राप्तकर्ताओं को सूचित करने में त्रुटि",
"bulkDeleteError": "साझाकरण हटाने में विफल",
"bulkDeleteLoading": "{count, plural, =1 {1 साझाकरण} other {# साझाकरण}} हटाया जा रहा है...",
"bulkDeleteSuccess": "{count, plural, =1 {1 साझाकरण सफलतापूर्वक हटाया गया} other {# साझाकरण सफलतापूर्वक हटाए गए}}"
"bulkDeleteSuccess": "{count, plural, =1 {1 साझाकरण सफलतापूर्वक हटाया गया} other {# साझाकरण सफलतापूर्वक हटाए गए}}",
"securityUpdateError": "सुरक्षा सेटिंग्स अपडेट करने में विफल",
"expirationUpdateError": "समाप्ति सेटिंग्स अपडेट करने में विफल",
"securityUpdateSuccess": "सुरक्षा सेटिंग्स सफलतापूर्वक अपडेट हुईं",
"expirationUpdateSuccess": "समाप्ति सेटिंग्स सफलतापूर्वक अपडेट हुईं"
},
"shares": {
"errors": {
@@ -790,5 +818,63 @@
"totalSize": "कुल आकार",
"creating": "बनाया जा रहा है...",
"create": "साझाकरण बनाएं"
},
"shareSecurity": {
"subtitle": "इस साझाकरण के लिए पासवर्ड सुरक्षा और सुरक्षा विकल्प कॉन्फ़िगर करें",
"info": {
"title": "यह कैसे काम करता है:",
"withoutPassword": "लिंक वाला कोई भी व्यक्ति बिना पासवर्ड के इस साझाकरण तक पहुंच सकता है।",
"withPassword": "उपयोगकर्ताओं को इस साझाकरण तक पहुंचने के लिए पासवर्ड दर्ज करना होगा।"
},
"existingPasswordMessage": "इस साझाकरण में पहले से ही एक पासवर्ड है। यदि आप इसे अपडेट करना चाहते हैं, तो नीचे दिए गए फील्ड में नया पासवर्ड दर्ज करें और सहेजें।",
"passwordProtection": "पासवर्ड सुरक्षा",
"error": {
"updateFailed": "सुरक्षा सेटिंग्स अपडेट करने में विफल"
},
"passwordRequirements": {
"title": "पासवर्ड आवश्यकताएं:",
"minLength": "कम से कम 2 अक्षर"
},
"newPassword": "नया पासवर्ड",
"success": {
"passwordUpdated": "पासवर्ड सफलतापूर्वक अपडेट हुआ",
"passwordRemoved": "पासवर्ड सुरक्षा सफलतापूर्वक हटाई गई",
"passwordSet": "पासवर्ड सुरक्षा सफलतापूर्वक सक्षम की गई"
},
"password": "पासवर्ड",
"validation": {
"passwordRequired": "पासवर्ड आवश्यक है",
"passwordTooShort": "पासवर्ड कम से कम 2 अक्षर का होना चाहिए"
},
"currentStatus": "वर्तमान स्थिति",
"passwordPlaceholder": "एक सुरक्षित पासवर्ड दर्ज करें",
"title": "साझाकरण सुरक्षा सेटिंग्स"
},
"shareExpiration": {
"neverExpires": "कभी समाप्त नहीं होता",
"success": {
"expirationUpdated": "समाप्ति तिथि सफलतापूर्वक अपडेट हुई",
"expirationRemoved": "समाप्ति सफलतापूर्वक हटाई गई - साझाकरण अब स्थायी है",
"expirationSet": "समाप्ति तिथि सफलतापूर्वक सेट की गई"
},
"info": {
"canBeChanged": "आप समाप्ति तिथि को कभी भी बदल या हटा सकते हैं",
"willBeInaccessible": "इस तिथि के बाद साझाकरण अगम्य हो जाएगा",
"noExpiration": "यह साझाकरण कभी समाप्त नहीं होगा और अनिश्चित काल तक पहुंच योग्य रहेगा।",
"title": "समाप्ति के बारे में:"
},
"enableExpiration": "समाप्ति सक्षम करें",
"title": "साझाकरण समाप्ति सेटिंग्स",
"subtitle": "कॉन्फ़िगर करें कि यह साझाकरण कब समाप्त होगा",
"validation": {
"dateMustBeFuture": "समाप्ति तिथि भविष्य में होनी चाहिए",
"dateRequired": "कृपया एक समाप्ति तिथि चुनें"
},
"currentStatus": "वर्तमान स्थिति",
"error": {
"updateFailed": "समाप्ति सेटिंग्स अपडेट करने में विफल"
},
"expires": "समाप्त होता है:",
"expirationDate": "समाप्ति तिथि"
}
}

View File

@@ -10,7 +10,10 @@
"yes": "Sì",
"no": "No",
"dashboard": "Dashboard",
"back": "Indietro"
"back": "Indietro",
"updating": "Aggiornamento...",
"saving": "Salvataggio...",
"update": "Aggiorna"
},
"createShare": {
"title": "Crea Condivisione",
@@ -275,7 +278,8 @@
"title": "Caricamenti Recenti",
"viewAll": "Visualizza Tutti",
"uploadFile": "Carica File",
"noFiles": "Nessun file caricato ancora"
"noFiles": "Nessun file caricato ancora",
"upload": "Carica"
},
"recentShares": {
"title": "Condivisioni Recenti",
@@ -296,7 +300,25 @@
"removeError": "Errore durante la rimozione del destinatario",
"sendingNotifications": "Invio notifiche in corso...",
"notifySuccess": "Destinatari notificati con successo",
"notifyError": "Errore durante la notifica dei destinatari"
"notifyError": "Errore durante la notifica dei destinatari",
"bulkNotifySuccess": "Notifiche inviate a {count} destinatari",
"selectAll": "Seleziona tutto",
"singleNotifySuccess": "Notifica inviata a {email}",
"removeSingle": "Rimuovi questo destinatario",
"selectRecipient": "Seleziona {email}",
"bulkRemoveSuccess": "{count} destinatari rimossi con successo",
"notifySingle": "Notifica questo destinatario",
"notifySelected": "Notifica Selezionati",
"invalidEmail": "Inserisci un indirizzo email valido",
"noRecipientsDescription": "Aggiungi destinatari per condividere questo contenuto via email",
"singleNotifyError": "Impossibile notificare il destinatario",
"bulkRemoveError": "Impossibile rimuovere i destinatari selezionati",
"modalDescription": "Aggiungi e gestisci destinatari per questa condivisione. Puoi notificare tutti o destinatari specifici quando SMTP è configurato.",
"duplicateEmail": "Questo destinatario è già stato aggiunto",
"removeSelected": "Rimuovi Selezionati",
"selectedCount": "{count} selezionati",
"addRecipient": "Aggiungi Destinatario",
"bulkNotifyError": "Impossibile notificare i destinatari selezionati"
},
"register": {
"validation": {
@@ -536,7 +558,9 @@
"recipients": "Destinatari",
"notAvailable": "N/D",
"invalidDate": "Data non valida",
"loadError": "Errore nel caricamento dei dettagli della condivisione"
"loadError": "Errore nel caricamento dei dettagli della condivisione",
"editSecurity": "Modifica Sicurezza",
"editExpiration": "Modifica Scadenza"
},
"shareManager": {
"deleteSuccess": "Condivisione eliminata con successo",
@@ -554,7 +578,11 @@
"notifyError": "Errore durante la notifica dei destinatari",
"bulkDeleteError": "Errore nell'eliminazione delle condivisioni",
"bulkDeleteLoading": "Eliminazione di {count, plural, =1 {1 condivisione} other {# condivisioni}}...",
"bulkDeleteSuccess": "{count, plural, =1 {1 condivisione eliminata con successo} other {# condivisioni eliminate con successo}}"
"bulkDeleteSuccess": "{count, plural, =1 {1 condivisione eliminata con successo} other {# condivisioni eliminate con successo}}",
"securityUpdateError": "Impossibile aggiornare le impostazioni di sicurezza",
"expirationUpdateError": "Impossibile aggiornare le impostazioni di scadenza",
"securityUpdateSuccess": "Impostazioni di sicurezza aggiornate con successo",
"expirationUpdateSuccess": "Impostazioni di scadenza aggiornate con successo"
},
"shares": {
"errors": {
@@ -790,5 +818,63 @@
"totalSize": "Dimensione totale",
"creating": "Creazione...",
"create": "Crea Condivisione"
},
"shareSecurity": {
"subtitle": "Configura protezione password e opzioni di sicurezza per questa condivisione",
"info": {
"title": "Come funziona:",
"withoutPassword": "Chiunque abbia il link può accedere a questa condivisione senza password.",
"withPassword": "Gli utenti dovranno inserire la password per accedere a questa condivisione."
},
"existingPasswordMessage": "Questa condivisione ha già una password. Se vuoi aggiornarla, inserisci la nuova password nel campo sottostante e salva.",
"passwordProtection": "Protezione Password",
"error": {
"updateFailed": "Impossibile aggiornare le impostazioni di sicurezza"
},
"passwordRequirements": {
"title": "Requisiti password:",
"minLength": "Almeno 2 caratteri"
},
"newPassword": "Nuova Password",
"success": {
"passwordUpdated": "Password aggiornata con successo",
"passwordRemoved": "Protezione password rimossa con successo",
"passwordSet": "Protezione password abilitata con successo"
},
"password": "Password",
"validation": {
"passwordRequired": "La password è obbligatoria",
"passwordTooShort": "La password deve essere di almeno 2 caratteri"
},
"currentStatus": "Stato Attuale",
"passwordPlaceholder": "Inserisci una password sicura",
"title": "Impostazioni Sicurezza Condivisione"
},
"shareExpiration": {
"neverExpires": "Non Scade Mai",
"success": {
"expirationUpdated": "Data di scadenza aggiornata con successo",
"expirationRemoved": "Scadenza rimossa con successo - la condivisione è ora permanente",
"expirationSet": "Data di scadenza impostata con successo"
},
"info": {
"canBeChanged": "Puoi cambiare o rimuovere la data di scadenza in qualsiasi momento",
"willBeInaccessible": "La condivisione diventerà inaccessibile dopo questa data",
"noExpiration": "Questa condivisione non scadrà mai e rimarrà accessibile indefinitamente.",
"title": "Informazioni sulla scadenza:"
},
"enableExpiration": "Abilita Scadenza",
"title": "Impostazioni Scadenza Condivisione",
"subtitle": "Configura quando questa condivisione scadrà",
"validation": {
"dateMustBeFuture": "La data di scadenza deve essere nel futuro",
"dateRequired": "Seleziona una data di scadenza"
},
"currentStatus": "Stato Attuale",
"error": {
"updateFailed": "Impossibile aggiornare le impostazioni di scadenza"
},
"expires": "Scade:",
"expirationDate": "Data di Scadenza"
}
}

View File

@@ -10,7 +10,10 @@
"yes": "はい",
"no": "いいえ",
"dashboard": "ダッシュボード",
"back": "戻る"
"back": "戻る",
"updating": "更新中...",
"saving": "保存中...",
"update": "更新"
},
"createShare": {
"title": "共有を作成",
@@ -275,7 +278,8 @@
"title": "最近のアップロード",
"viewAll": "すべて表示",
"uploadFile": "ファイルをアップロード",
"noFiles": "まだファイルがアップロードされていません"
"noFiles": "まだファイルがアップロードされていません",
"upload": "アップロード"
},
"recentShares": {
"title": "最近の共有",
@@ -296,7 +300,25 @@
"removeError": "受信者の削除に失敗しました",
"sendingNotifications": "通知を送信中...",
"notifySuccess": "受信者に正常に通知しました",
"notifyError": "受信者への通知に失敗しました"
"notifyError": "受信者への通知に失敗しました",
"bulkNotifySuccess": "{count}人の受信者に通知を送信しました",
"selectAll": "すべて選択",
"singleNotifySuccess": "{email}に通知を送信しました",
"removeSingle": "この受信者を削除",
"selectRecipient": "{email}を選択",
"bulkRemoveSuccess": "{count}人の受信者が正常に削除されました",
"notifySingle": "この受信者に通知",
"notifySelected": "選択済みに通知",
"invalidEmail": "有効なメールアドレスを入力してください",
"noRecipientsDescription": "メールでこのコンテンツを共有するために受信者を追加してください",
"singleNotifyError": "受信者への通知に失敗しました",
"bulkRemoveError": "選択された受信者の削除に失敗しました",
"modalDescription": "この共有の受信者を追加・管理します。SMTPが設定されている場合、すべての受信者または特定の受信者に通知できます。",
"duplicateEmail": "この受信者は既に追加されています",
"removeSelected": "選択済みを削除",
"selectedCount": "{count}件選択済み",
"addRecipient": "受信者を追加",
"bulkNotifyError": "選択された受信者への通知に失敗しました"
},
"register": {
"validation": {
@@ -534,9 +556,11 @@
"maxViews": "最大表示回数:",
"files": "ファイル",
"recipients": "受信者",
"notAvailable": "N/A",
"notAvailable": "該当なし",
"invalidDate": "無効な日付",
"loadError": "共有詳細の読み込みに失敗しました"
"loadError": "共有詳細の読み込みに失敗しました",
"editSecurity": "セキュリティを編集",
"editExpiration": "期限を編集"
},
"shareManager": {
"deleteSuccess": "共有が正常に削除されました",
@@ -554,7 +578,11 @@
"notifyError": "受信者への通知に失敗しました",
"bulkDeleteError": "共有の削除に失敗しました",
"bulkDeleteLoading": "{count, plural, =1 {1つの共有} other {#つの共有}}を削除中...",
"bulkDeleteSuccess": "{count, plural, =1 {1つの共有が正常に削除されました} other {#つの共有が正常に削除されました}}"
"bulkDeleteSuccess": "{count, plural, =1 {1つの共有が正常に削除されました} other {#つの共有が正常に削除されました}}",
"securityUpdateError": "セキュリティ設定の更新に失敗しました",
"expirationUpdateError": "有効期限設定の更新に失敗しました",
"securityUpdateSuccess": "セキュリティ設定が正常に更新されました",
"expirationUpdateSuccess": "有効期限設定が正常に更新されました"
},
"shares": {
"errors": {
@@ -650,17 +678,17 @@
"insufficientStorage": "ストレージ容量が不足しています。利用可能な容量は {availablespace}MB です。",
"unauthorized": "権限がありません: このリソースにアクセスするには有効なトークンが必要です。",
"selectMultipleFiles": "複数のファイルを選択するにはクリック",
"finish": "Concluir",
"finish": "完了",
"confirmCancel": {
"warning": "Se você fechar agora, os uploads serão cancelados e qualquer progresso será perdido.",
"continue": "Continuar Uploads",
"title": "Cancelar Uploads",
"messageMultiple": "{count} uploads em andamento.",
"messageSingle": "Há um upload em andamento.",
"cancel": "Cancelar Uploads"
"warning": "今閉じるとアップロードがキャンセルされ、進行状況が失われます。",
"continue": "アップロードを続行",
"title": "アップロードをキャンセル",
"messageMultiple": "{count}件のアップロードが進行中です。",
"messageSingle": "1件のアップロードが進行中です。",
"cancel": "アップロードをキャンセル"
},
"multipleTitle": "Enviar Múltiplos Arquivos",
"startUploads": "Iniciar Uploads",
"multipleTitle": "複数ファイルをアップロード",
"startUploads": "アップロードを開始",
"allSuccess": "{count, plural, =1 {ファイルがアップロードされました} other {#個のファイルがアップロードされました}}",
"partialSuccess": "{success}個のファイルがアップロードされ、{error}個が失敗しました",
"dragAndDrop": "またはここにファイルをドラッグ&ドロップ"
@@ -790,5 +818,63 @@
"totalSize": "合計サイズ",
"creating": "作成中...",
"create": "共有を作成"
},
"shareSecurity": {
"subtitle": "この共有のパスワード保護とセキュリティオプションを設定",
"info": {
"title": "仕組み:",
"withoutPassword": "リンクを持つ誰でもパスワードなしでこの共有にアクセスできます。",
"withPassword": "ユーザーはこの共有にアクセスするためにパスワードを入力する必要があります。"
},
"existingPasswordMessage": "この共有には既にパスワードが設定されています。更新したい場合は、下のフィールドに新しいパスワードを入力して保存してください。",
"passwordProtection": "パスワード保護",
"error": {
"updateFailed": "セキュリティ設定の更新に失敗しました"
},
"passwordRequirements": {
"title": "パスワード要件:",
"minLength": "2文字以上"
},
"newPassword": "新しいパスワード",
"success": {
"passwordUpdated": "パスワードが正常に更新されました",
"passwordRemoved": "パスワード保護が正常に削除されました",
"passwordSet": "パスワード保護が正常に有効化されました"
},
"password": "パスワード",
"validation": {
"passwordRequired": "パスワードが必要です",
"passwordTooShort": "パスワードは2文字以上である必要があります"
},
"currentStatus": "現在のステータス",
"passwordPlaceholder": "安全なパスワードを入力してください",
"title": "共有セキュリティ設定"
},
"shareExpiration": {
"neverExpires": "期限なし",
"success": {
"expirationUpdated": "有効期限が正常に更新されました",
"expirationRemoved": "有効期限が正常に削除されました - 共有は永続的になりました",
"expirationSet": "有効期限が正常に設定されました"
},
"info": {
"canBeChanged": "有効期限はいつでも変更または削除できます",
"willBeInaccessible": "この日時以降、共有にアクセスできなくなります",
"noExpiration": "この共有は期限がなく、無期限でアクセス可能です。",
"title": "有効期限について:"
},
"enableExpiration": "有効期限を有効にする",
"title": "共有有効期限設定",
"subtitle": "この共有の期限を設定",
"validation": {
"dateMustBeFuture": "有効期限は未来の日時である必要があります",
"dateRequired": "有効期限を選択してください"
},
"currentStatus": "現在のステータス",
"error": {
"updateFailed": "有効期限設定の更新に失敗しました"
},
"expires": "期限:",
"expirationDate": "有効期限"
}
}

View File

@@ -10,7 +10,10 @@
"yes": "예",
"no": "아니오",
"dashboard": "대시보드",
"back": "뒤로"
"back": "뒤로",
"updating": "업데이트 중...",
"saving": "저장 중...",
"update": "업데이트"
},
"createShare": {
"title": "공유 생성",
@@ -275,7 +278,8 @@
"title": "최근 업로드",
"viewAll": "전체 보기",
"uploadFile": "파일 업로드",
"noFiles": "아직 파일이 업로드되지 않았습니다"
"noFiles": "아직 파일이 업로드되지 않았습니다",
"upload": "업로드"
},
"recentShares": {
"title": "최근 공유",
@@ -296,7 +300,25 @@
"removeError": "수신자 제거에 실패했습니다",
"sendingNotifications": "알림 전송 중...",
"notifySuccess": "수신자에게 성공적으로 알렸습니다",
"notifyError": "수신자 알림 전송에 실패했습니다"
"notifyError": "수신자 알림 전송에 실패했습니다",
"bulkNotifySuccess": "{count}명의 수신자에게 알림을 전송했습니다",
"selectAll": "모두 선택",
"singleNotifySuccess": "{email}에게 알림을 전송했습니다",
"removeSingle": "이 수신자 제거",
"selectRecipient": "{email} 선택",
"bulkRemoveSuccess": "{count}명의 수신자가 성공적으로 제거되었습니다",
"notifySingle": "이 수신자에게 알림",
"notifySelected": "선택된 항목에 알림",
"invalidEmail": "유효한 이메일 주소를 입력해주세요",
"noRecipientsDescription": "이메일을 통해 이 콘텐츠를 공유하려면 수신자를 추가하세요",
"singleNotifyError": "수신자 알림 전송에 실패했습니다",
"bulkRemoveError": "선택된 수신자 제거에 실패했습니다",
"modalDescription": "이 공유의 수신자를 추가하고 관리합니다. SMTP가 설정되어 있을 때 모든 수신자 또는 특정 수신자에게 알림을 보낼 수 있습니다.",
"duplicateEmail": "이 수신자는 이미 추가되었습니다",
"removeSelected": "선택된 항목 제거",
"selectedCount": "{count}개 선택됨",
"addRecipient": "수신자 추가",
"bulkNotifyError": "선택된 수신자들에게 알림 전송에 실패했습니다"
},
"register": {
"validation": {
@@ -534,9 +556,11 @@
"maxViews": "최대 조회수:",
"files": "파일",
"recipients": "받는 사람",
"notAvailable": "N/A",
"notAvailable": "해당없음",
"invalidDate": "잘못된 날짜",
"loadError": "공유 세부 정보 로드에 실패했습니다"
"loadError": "공유 세부 정보 로드에 실패했습니다",
"editSecurity": "보안 편집",
"editExpiration": "만료 편집"
},
"shareManager": {
"deleteSuccess": "공유가 성공적으로 삭제되었습니다",
@@ -554,7 +578,11 @@
"notifyError": "수신자 알림 전송에 실패했습니다",
"bulkDeleteError": "공유 삭제 실패",
"bulkDeleteLoading": "{count, plural, =1 {1개의 공유} other {#개의 공유}} 삭제 중...",
"bulkDeleteSuccess": "{count, plural, =1 {1개의 공유가 성공적으로 삭제되었습니다} other {#개의 공유가 성공적으로 삭제되었습니다}}"
"bulkDeleteSuccess": "{count, plural, =1 {1개의 공유가 성공적으로 삭제되었습니다} other {#개의 공유가 성공적으로 삭제되었습니다}}",
"securityUpdateError": "보안 설정 업데이트에 실패했습니다",
"expirationUpdateError": "만료 설정 업데이트에 실패했습니다",
"securityUpdateSuccess": "보안 설정이 성공적으로 업데이트되었습니다",
"expirationUpdateSuccess": "만료 설정이 성공적으로 업데이트되었습니다"
},
"shares": {
"errors": {
@@ -790,5 +818,63 @@
"totalSize": "전체 크기",
"creating": "생성 중...",
"create": "공유 생성"
},
"shareSecurity": {
"subtitle": "이 공유의 비밀번호 보호 및 보안 옵션을 구성하세요",
"info": {
"title": "작동 방식:",
"withoutPassword": "링크를 가진 누구나 비밀번호 없이 이 공유에 액세스할 수 있습니다.",
"withPassword": "사용자는 이 공유에 액세스하려면 비밀번호를 입력해야 합니다."
},
"existingPasswordMessage": "이 공유에는 이미 비밀번호가 설정되어 있습니다. 업데이트하려면 아래 필드에 새 비밀번호를 입력하고 저장하세요.",
"passwordProtection": "비밀번호 보호",
"error": {
"updateFailed": "보안 설정 업데이트에 실패했습니다"
},
"passwordRequirements": {
"title": "비밀번호 요구사항:",
"minLength": "최소 2자 이상"
},
"newPassword": "새 비밀번호",
"success": {
"passwordUpdated": "비밀번호가 성공적으로 업데이트되었습니다",
"passwordRemoved": "비밀번호 보호가 성공적으로 제거되었습니다",
"passwordSet": "비밀번호 보호가 성공적으로 활성화되었습니다"
},
"password": "비밀번호",
"validation": {
"passwordRequired": "비밀번호가 필요합니다",
"passwordTooShort": "비밀번호는 최소 2자 이상이어야 합니다"
},
"currentStatus": "현재 상태",
"passwordPlaceholder": "안전한 비밀번호를 입력하세요",
"title": "공유 보안 설정"
},
"shareExpiration": {
"neverExpires": "만료되지 않음",
"success": {
"expirationUpdated": "만료 날짜가 성공적으로 업데이트되었습니다",
"expirationRemoved": "만료가 성공적으로 제거되었습니다 - 공유가 이제 영구적입니다",
"expirationSet": "만료 날짜가 성공적으로 설정되었습니다"
},
"info": {
"canBeChanged": "언제든지 만료 날짜를 변경하거나 제거할 수 있습니다",
"willBeInaccessible": "이 날짜 이후에는 공유에 액세스할 수 없게 됩니다",
"noExpiration": "이 공유는 만료되지 않으며 무기한 액세스 가능합니다.",
"title": "만료에 대해:"
},
"enableExpiration": "만료 활성화",
"title": "공유 만료 설정",
"subtitle": "이 공유가 언제 만료될지 구성하세요",
"validation": {
"dateMustBeFuture": "만료 날짜는 미래여야 합니다",
"dateRequired": "만료 날짜를 선택해주세요"
},
"currentStatus": "현재 상태",
"error": {
"updateFailed": "만료 설정 업데이트에 실패했습니다"
},
"expires": "만료:",
"expirationDate": "만료 날짜"
}
}

View File

@@ -10,7 +10,10 @@
"yes": "Ja",
"no": "Nee",
"dashboard": "Dashboard",
"back": "Terug"
"back": "Terug",
"updating": "Bijwerken...",
"saving": "Opslaan...",
"update": "Bijwerken"
},
"createShare": {
"title": "Delen Maken",
@@ -275,7 +278,8 @@
"title": "Recente Uploads",
"viewAll": "Alles Bekijken",
"uploadFile": "Bestand Uploaden",
"noFiles": "Nog geen bestanden geüpload"
"noFiles": "Nog geen bestanden geüpload",
"upload": "Uploaden"
},
"recentShares": {
"title": "Recente Delingen",
@@ -296,7 +300,25 @@
"removeError": "Fout bij het verwijderen van ontvanger",
"sendingNotifications": "Meldingen verzenden...",
"notifySuccess": "Ontvangers succesvol gewaarschuwd",
"notifyError": "Fout bij het waarschuwen van ontvangers"
"notifyError": "Fout bij het waarschuwen van ontvangers",
"bulkNotifySuccess": "Notificaties verzonden naar {count} ontvangers",
"selectAll": "Alles selecteren",
"singleNotifySuccess": "Notificatie verzonden naar {email}",
"removeSingle": "Deze ontvanger verwijderen",
"selectRecipient": "{email} selecteren",
"bulkRemoveSuccess": "{count} ontvangers succesvol verwijderd",
"notifySingle": "Deze ontvanger waarschuwen",
"notifySelected": "Geselecteerde waarschuwen",
"invalidEmail": "Voer een geldig e-mailadres in",
"noRecipientsDescription": "Voeg ontvangers toe om deze inhoud via e-mail te delen",
"singleNotifyError": "Fout bij het waarschuwen van ontvanger",
"bulkRemoveError": "Fout bij het verwijderen van geselecteerde ontvangers",
"modalDescription": "Voeg ontvangers toe en beheer ze voor dit delen. Je kunt alle of specifieke ontvangers waarschuwen wanneer SMTP is geconfigureerd.",
"duplicateEmail": "Deze ontvanger is al toegevoegd",
"removeSelected": "Geselecteerde verwijderen",
"selectedCount": "{count} geselecteerd",
"addRecipient": "Ontvanger Toevoegen",
"bulkNotifyError": "Fout bij het waarschuwen van geselecteerde ontvangers"
},
"register": {
"validation": {
@@ -536,7 +558,9 @@
"recipients": "Ontvangers",
"notAvailable": "N/B",
"invalidDate": "Ongeldige datum",
"loadError": "Fout bij laden van delen details"
"loadError": "Fout bij laden van delen details",
"editSecurity": "Beveiliging Bewerken",
"editExpiration": "Vervaldatum Bewerken"
},
"shareManager": {
"deleteSuccess": "Delen succesvol verwijderd",
@@ -554,7 +578,11 @@
"notifyError": "Fout bij het waarschuwen van ontvangers",
"bulkDeleteError": "Delen verwijderen mislukt",
"bulkDeleteLoading": "{count, plural, =1 {1 deel} other {# delen}} verwijderen...",
"bulkDeleteSuccess": "{count, plural, =1 {1 deel succesvol verwijderd} other {# delen succesvol verwijderd}}"
"bulkDeleteSuccess": "{count, plural, =1 {1 deel succesvol verwijderd} other {# delen succesvol verwijderd}}",
"securityUpdateError": "Fout bij het bijwerken van beveiligingsinstellingen",
"expirationUpdateError": "Fout bij het bijwerken van verloop instellingen",
"securityUpdateSuccess": "Beveiligingsinstellingen succesvol bijgewerkt",
"expirationUpdateSuccess": "Verloop instellingen succesvol bijgewerkt"
},
"shares": {
"errors": {
@@ -790,5 +818,63 @@
"totalSize": "Totale grootte",
"creating": "Aanmaken...",
"create": "Delen Maken"
},
"shareSecurity": {
"subtitle": "Configureer wachtwoordbeveiliging en beveiligingsopties voor dit delen",
"info": {
"title": "Hoe het werkt:",
"withoutPassword": "Iedereen met de link kan dit delen benaderen zonder wachtwoord.",
"withPassword": "Gebruikers moeten het wachtwoord invoeren om dit delen te benaderen."
},
"existingPasswordMessage": "Dit delen heeft al een wachtwoord. Als je het wilt bijwerken, voer dan het nieuwe wachtwoord in het onderstaande veld in en sla op.",
"passwordProtection": "Wachtwoordbeveiliging",
"error": {
"updateFailed": "Fout bij het bijwerken van beveiligingsinstellingen"
},
"passwordRequirements": {
"title": "Wachtwoordvereisten:",
"minLength": "Minimaal 2 tekens"
},
"newPassword": "Nieuw Wachtwoord",
"success": {
"passwordUpdated": "Wachtwoord succesvol bijgewerkt",
"passwordRemoved": "Wachtwoordbeveiliging succesvol verwijderd",
"passwordSet": "Wachtwoordbeveiliging succesvol ingeschakeld"
},
"password": "Wachtwoord",
"validation": {
"passwordRequired": "Wachtwoord is vereist",
"passwordTooShort": "Wachtwoord moet minimaal 2 tekens bevatten"
},
"currentStatus": "Huidige Status",
"passwordPlaceholder": "Voer een veilig wachtwoord in",
"title": "Delen Beveiligingsinstellingen"
},
"shareExpiration": {
"neverExpires": "Verloopt Nooit",
"success": {
"expirationUpdated": "Vervaldatum succesvol bijgewerkt",
"expirationRemoved": "Verloop succesvol verwijderd - delen is nu permanent",
"expirationSet": "Vervaldatum succesvol ingesteld"
},
"info": {
"canBeChanged": "Je kunt de vervaldatum op elk moment wijzigen of verwijderen",
"willBeInaccessible": "Het delen wordt na deze datum ontoegankelijk",
"noExpiration": "Dit delen verloopt nooit en blijft voor onbepaalde tijd toegankelijk.",
"title": "Over verloop:"
},
"enableExpiration": "Vervaldatum Inschakelen",
"title": "Delen Verloop Instellingen",
"subtitle": "Configureer wanneer dit delen verloopt",
"validation": {
"dateMustBeFuture": "Vervaldatum moet in de toekomst liggen",
"dateRequired": "Selecteer een vervaldatum"
},
"currentStatus": "Huidige Status",
"error": {
"updateFailed": "Fout bij het bijwerken van verloop instellingen"
},
"expires": "Verloopt:",
"expirationDate": "Vervaldatum"
}
}

View File

@@ -10,7 +10,10 @@
"yes": "Sim",
"no": "Não",
"dashboard": "Painel",
"back": "Voltar"
"back": "Voltar",
"updating": "Atualizando...",
"saving": "Salvando...",
"update": "Atualizar"
},
"createShare": {
"title": "Criar Compartilhamento",
@@ -253,7 +256,7 @@
"firstName": "Nome",
"lastName": "Sobrenome",
"username": "Nome de Usuário",
"email": "Email",
"email": "E-mail",
"updateButton": "Atualizar Perfil"
},
"header": {
@@ -297,7 +300,8 @@
"title": "Uploads Recentes",
"viewAll": "Ver Todos",
"uploadFile": "Enviar Arquivo",
"noFiles": "Nenhum arquivo enviado ainda"
"noFiles": "Nenhum arquivo enviado ainda",
"upload": "Carregar"
},
"recentShares": {
"title": "Compartilhamentos Recentes",
@@ -318,7 +322,25 @@
"removeError": "Falha ao remover destinatário",
"sendingNotifications": "Enviando notificações...",
"notifySuccess": "Destinatários notificados com sucesso",
"notifyError": "Falha ao notificar destinatários"
"notifyError": "Falha ao notificar destinatários",
"selectAll": "Selecionar todos",
"selectedCount": "{count} selecionados",
"selectRecipient": "Selecionar {email}",
"notifySelected": "Notificar Selecionados",
"removeSelected": "Remover Selecionados",
"notifySingle": "Notificar este destinatário",
"removeSingle": "Remover este destinatário",
"bulkRemoveSuccess": "{count} destinatários removidos com sucesso",
"bulkRemoveError": "Falha ao remover destinatários selecionados",
"bulkNotifySuccess": "Notificações enviadas para {count} destinatários",
"bulkNotifyError": "Falha ao notificar destinatários selecionados",
"singleNotifySuccess": "Notificação enviada para {email}",
"singleNotifyError": "Falha ao notificar destinatário",
"modalDescription": "Adicione e gerencie destinatários para este compartilhamento. Você pode notificar todos ou destinatários específicos quando o SMTP estiver configurado.",
"addRecipient": "Adicionar Destinatário",
"invalidEmail": "Por favor, digite um endereço de e-mail válido",
"duplicateEmail": "Este destinatário já foi adicionado",
"noRecipientsDescription": "Adicione destinatários para compartilhar este conteúdo via e-mail"
},
"register": {
"validation": {
@@ -334,7 +356,7 @@
"firstName": "Nome",
"lastName": "Sobrenome",
"username": "Nome de Usuário",
"email": "Email",
"email": "E-mail",
"password": "Senha"
},
"buttons": {
@@ -377,7 +399,7 @@
"description": "Configurações básicas da aplicação"
},
"email": {
"title": "Email",
"title": "E-mail",
"description": "Configuração do servidor de email"
},
"security": {
@@ -556,9 +578,11 @@
"maxViews": "Máx. Visualizações:",
"files": "Arquivos",
"recipients": "Destinatários",
"notAvailable": "N/A",
"notAvailable": "N/D",
"invalidDate": "Data inválida",
"loadError": "Falha ao carregar detalhes do compartilhamento"
"loadError": "Falha ao carregar detalhes do compartilhamento",
"editSecurity": "Editar Segurança",
"editExpiration": "Editar Expiração"
},
"shareManager": {
"deleteSuccess": "Compartilhamento excluído com sucesso",
@@ -576,7 +600,11 @@
"linkGenerateError": "Falha ao gerar link de compartilhamento",
"notifyLoading": "Enviando notificações...",
"notifySuccess": "Destinatários notificados com sucesso",
"notifyError": "Falha ao notificar destinatários"
"notifyError": "Falha ao notificar destinatários",
"securityUpdateError": "Falha ao atualizar configurações de segurança",
"expirationUpdateError": "Falha ao atualizar configurações de expiração",
"securityUpdateSuccess": "Configurações de segurança atualizadas com sucesso",
"expirationUpdateSuccess": "Configurações de expiração atualizadas com sucesso"
},
"shares": {
"errors": {
@@ -722,7 +750,7 @@
"firstName": "Nome",
"lastName": "Sobrenome",
"username": "Nome de Usuário",
"email": "Email",
"email": "E-mail",
"password": "Senha",
"newPassword": "Nova Senha (opcional)",
"passwordPlaceholder": "Deixe em branco para manter a senha atual",
@@ -752,7 +780,7 @@
"actions": "AÇÕES",
"active": "Ativo",
"inactive": "Inativo",
"admin": "Admin",
"admin": "Administrador",
"userr": "Usuário"
}
},
@@ -790,5 +818,63 @@
"totalSize": "Tamanho total",
"creating": "Criando...",
"create": "Criar Compartilhamento"
},
"shareSecurity": {
"subtitle": "Configurar proteção por senha e opções de segurança para este compartilhamento",
"info": {
"title": "Como funciona:",
"withoutPassword": "Qualquer pessoa com o link pode acessar este compartilhamento sem senha.",
"withPassword": "Os usuários precisarão digitar a senha para acessar este compartilhamento."
},
"existingPasswordMessage": "Este compartilhamento já tem uma senha. Se você quiser atualizá-la, digite a nova senha no campo abaixo e salve.",
"passwordProtection": "Proteção por Senha",
"newPassword": "Nova Senha",
"error": {
"updateFailed": "Falha ao atualizar configurações de segurança"
},
"passwordRequirements": {
"title": "Requisitos da senha:",
"minLength": "Pelo menos 2 caracteres"
},
"success": {
"passwordUpdated": "Senha atualizada com sucesso",
"passwordRemoved": "Proteção por senha removida com sucesso",
"passwordSet": "Proteção por senha habilitada com sucesso"
},
"password": "Senha",
"validation": {
"passwordRequired": "Senha é obrigatória",
"passwordTooShort": "A senha deve ter pelo menos 2 caracteres"
},
"currentStatus": "Status Atual",
"passwordPlaceholder": "Digite uma senha segura",
"title": "Configurações de Segurança do Compartilhamento"
},
"shareExpiration": {
"neverExpires": "Nunca Expira",
"success": {
"expirationUpdated": "Data de expiração atualizada com sucesso",
"expirationRemoved": "Expiração removida com sucesso - o compartilhamento agora é permanente",
"expirationSet": "Data de expiração definida com sucesso"
},
"info": {
"canBeChanged": "Você pode alterar ou remover a data de expiração a qualquer momento",
"willBeInaccessible": "O compartilhamento ficará inacessível após esta data",
"noExpiration": "Este compartilhamento nunca expirará e permanecerá acessível indefinidamente.",
"title": "Sobre expiração:"
},
"enableExpiration": "Habilitar Expiração",
"title": "Configurações de Expiração do Compartilhamento",
"subtitle": "Configurar quando este compartilhamento expirará",
"validation": {
"dateMustBeFuture": "A data de expiração deve estar no futuro",
"dateRequired": "Selecione uma data de expiração"
},
"currentStatus": "Status Atual",
"error": {
"updateFailed": "Falha ao atualizar configurações de expiração"
},
"expires": "Expira:",
"expirationDate": "Data de Expiração"
}
}

View File

@@ -10,7 +10,10 @@
"yes": "Да",
"no": "Нет",
"dashboard": "Панель управления",
"back": "Назад"
"back": "Назад",
"updating": "Обновление...",
"saving": "Сохранение...",
"update": "Обновить"
},
"createShare": {
"title": "Создать общий доступ",
@@ -275,7 +278,8 @@
"title": "Последние загрузки",
"viewAll": "Показать все",
"uploadFile": "Загрузить файл",
"noFiles": "Файлы еще не загружены"
"noFiles": "Файлы еще не загружены",
"upload": "Загрузить"
},
"recentShares": {
"title": "Последние общие доступы",
@@ -296,7 +300,25 @@
"removeError": "Не удалось удалить получателя",
"sendingNotifications": "Отправка уведомлений...",
"notifySuccess": "Получатели успешно уведомлены",
"notifyError": "Не удалось уведомить получателей"
"notifyError": "Не удалось уведомить получателей",
"bulkNotifySuccess": "Уведомления отправлены {count} получателям",
"selectAll": "Выбрать все",
"singleNotifySuccess": "Уведомление отправлено {email}",
"removeSingle": "Удалить этого получателя",
"selectRecipient": "Выбрать {email}",
"bulkRemoveSuccess": "{count} получателей успешно удалены",
"notifySingle": "Уведомить этого получателя",
"notifySelected": "Уведомить выбранных",
"invalidEmail": "Пожалуйста, введите действительный адрес электронной почты",
"noRecipientsDescription": "Добавьте получателей для обмена этим контентом по электронной почте",
"singleNotifyError": "Не удалось уведомить получателя",
"bulkRemoveError": "Не удалось удалить выбранных получателей",
"modalDescription": "Добавляйте и управляйте получателями для этого общего доступа. Вы можете уведомлять всех или конкретных получателей при настроенном SMTP.",
"duplicateEmail": "Этот получатель уже добавлен",
"removeSelected": "Удалить выбранных",
"selectedCount": "{count} выбрано",
"addRecipient": "Добавить получателя",
"bulkNotifyError": "Не удалось уведомить выбранных получателей"
},
"register": {
"validation": {
@@ -312,7 +334,7 @@
"firstName": "Имя",
"lastName": "Фамилия",
"username": "Имя пользователя",
"email": "Email",
"email": "Эл. почта",
"password": "Пароль"
},
"buttons": {
@@ -536,7 +558,9 @@
"recipients": "Получатели",
"notAvailable": "Н/Д",
"invalidDate": "Неверная дата",
"loadError": "Ошибка загрузки деталей общего доступа"
"loadError": "Ошибка загрузки деталей общего доступа",
"editSecurity": "Изменить безопасность",
"editExpiration": "Изменить срок действия"
},
"shareManager": {
"deleteSuccess": "Общий доступ успешно удален",
@@ -554,7 +578,11 @@
"notifyError": "Ошибка при уведомлении получателей",
"bulkDeleteError": "Не удалось удалить общие папки",
"bulkDeleteLoading": "Удаление {count, plural, =1 {1 общей папки} other {# общих папок}}...",
"bulkDeleteSuccess": "{count, plural, =1 {1 общая папка успешно удалена} other {# общих папок успешно удалены}}"
"bulkDeleteSuccess": "{count, plural, =1 {1 общая папка успешно удалена} other {# общих папок успешно удалены}}",
"securityUpdateError": "Не удалось обновить настройки безопасности",
"expirationUpdateError": "Не удалось обновить настройки истечения",
"securityUpdateSuccess": "Настройки безопасности успешно обновлены",
"expirationUpdateSuccess": "Настройки истечения успешно обновлены"
},
"shares": {
"errors": {
@@ -790,5 +818,63 @@
"totalSize": "Общий размер",
"creating": "Создание...",
"create": "Создать Общий Доступ"
},
"shareSecurity": {
"subtitle": "Настройте защиту паролем и параметры безопасности для этого общего доступа",
"info": {
"title": "Как это работает:",
"withoutPassword": "Любой, у кого есть ссылка, может получить доступ к этому общему доступу без пароля.",
"withPassword": "Пользователям нужно будет ввести пароль для доступа к этому общему доступу."
},
"existingPasswordMessage": "У этого общего доступа уже есть пароль. Если вы хотите его обновить, введите новый пароль в поле ниже и сохраните.",
"passwordProtection": "Защита паролем",
"error": {
"updateFailed": "Не удалось обновить настройки безопасности"
},
"passwordRequirements": {
"title": "Требования к паролю:",
"minLength": "Минимум 2 символа"
},
"newPassword": "Новый пароль",
"success": {
"passwordUpdated": "Пароль успешно обновлен",
"passwordRemoved": "Защита паролем успешно удалена",
"passwordSet": "Защита паролем успешно включена"
},
"password": "Пароль",
"validation": {
"passwordRequired": "Требуется пароль",
"passwordTooShort": "Пароль должен содержать не менее 2 символов"
},
"currentStatus": "Текущий статус",
"passwordPlaceholder": "Введите надежный пароль",
"title": "Настройки безопасности общего доступа"
},
"shareExpiration": {
"neverExpires": "Никогда не истекает",
"success": {
"expirationUpdated": "Дата истечения успешно обновлена",
"expirationRemoved": "Истечение успешно удалено - общий доступ теперь постоянный",
"expirationSet": "Дата истечения успешно установлена"
},
"info": {
"canBeChanged": "Вы можете изменить или удалить дату истечения в любое время",
"willBeInaccessible": "Общий доступ станет недоступным после этой даты",
"noExpiration": "Этот общий доступ никогда не истечет и останется доступным бессрочно.",
"title": "Об истечении:"
},
"enableExpiration": "Включить срок действия",
"title": "Настройки истечения общего доступа",
"subtitle": "Настройте, когда истечет этот общий доступ",
"validation": {
"dateMustBeFuture": "Дата истечения должна быть в будущем",
"dateRequired": "Пожалуйста, выберите дату истечения"
},
"currentStatus": "Текущий статус",
"error": {
"updateFailed": "Не удалось обновить настройки истечения"
},
"expires": "Истекает:",
"expirationDate": "Дата истечения"
}
}

View File

@@ -10,7 +10,10 @@
"yes": "Evet",
"no": "Hayır",
"dashboard": "Kontrol Paneli",
"back": "Geri"
"back": "Geri",
"updating": "Güncelleniyor...",
"saving": "Kaydediliyor...",
"update": "Güncelle"
},
"createShare": {
"title": "Paylaşım Oluştur",
@@ -275,7 +278,8 @@
"title": "Son Yüklemeler",
"viewAll": "Tümünü Görüntüle",
"uploadFile": "Dosya Yükle",
"noFiles": "Henüz dosya yüklenmedi"
"noFiles": "Henüz dosya yüklenmedi",
"upload": "Yükle"
},
"recentShares": {
"title": "Son Paylaşımlar",
@@ -296,7 +300,25 @@
"removeError": "Alıcı kaldırılamadı",
"sendingNotifications": "Bildirimler gönderiliyor...",
"notifySuccess": "Alıcılar başarıyla bildirildi",
"notifyError": "Alıcılara bildirim gönderilemedi"
"notifyError": "Alıcılara bildirim gönderilemedi",
"bulkNotifySuccess": "Bildirimleri {count} alıcıya gönderildi",
"selectAll": "Tümünü seç",
"singleNotifySuccess": "Bildirim {email} adresine gönderildi",
"removeSingle": "Bu alıcıyı kaldır",
"selectRecipient": "{email} seç",
"bulkRemoveSuccess": "{count} alıcı başarıyla kaldırıldı",
"notifySingle": "Bu alıcıyı bilgilendir",
"notifySelected": "Seçilenleri Bilgilendir",
"invalidEmail": "Lütfen geçerli bir e-posta adresi girin",
"noRecipientsDescription": "Bu içeriği e-posta ile paylaşmak için alıcılar ekleyin",
"singleNotifyError": "Alıcıya bildirim gönderme başarısız",
"bulkRemoveError": "Seçilen alıcıları kaldırma başarısız",
"modalDescription": "Bu paylaşım için alıcıları ekleyin ve yönetin. SMTP yapılandırıldığında tüm veya belirli alıcılara bildirim gönderebilirsiniz.",
"duplicateEmail": "Bu alıcı zaten eklenmiş",
"removeSelected": "Seçilenleri Kaldır",
"selectedCount": "{count} seçildi",
"addRecipient": "Alıcı Ekle",
"bulkNotifyError": "Seçilen alıcılara bildirim gönderme başarısız"
},
"register": {
"validation": {
@@ -536,7 +558,9 @@
"recipients": "Alıcılar",
"notAvailable": "M/D",
"invalidDate": "Geçersiz tarih",
"loadError": "Paylaşım detaylarını yükleme başarısız"
"loadError": "Paylaşım detaylarını yükleme başarısız",
"editSecurity": "Güvenlik Düzenle",
"editExpiration": "Son Kullanma Düzenle"
},
"shareManager": {
"deleteSuccess": "Paylaşım başarıyla silindi",
@@ -554,7 +578,11 @@
"notifyError": "Alıcılara bildirim gönderilemedi",
"bulkDeleteError": "Paylaşımları silme başarısız",
"bulkDeleteLoading": "{count, plural, =1 {1 paylaşım} other {# paylaşım}} siliniyor...",
"bulkDeleteSuccess": "{count, plural, =1 {1 paylaşım başarıyla silindi} other {# paylaşım başarıyla silindi}}"
"bulkDeleteSuccess": "{count, plural, =1 {1 paylaşım başarıyla silindi} other {# paylaşım başarıyla silindi}}",
"securityUpdateError": "Güvenlik ayarlarını güncelleme başarısız",
"expirationUpdateError": "Son kullanma ayarlarını güncelleme başarısız",
"securityUpdateSuccess": "Güvenlik ayarları başarıyla güncellendi",
"expirationUpdateSuccess": "Son kullanma ayarları başarıyla güncellendi"
},
"shares": {
"errors": {
@@ -790,5 +818,63 @@
"totalSize": "Toplam boyut",
"creating": "Oluşturuluyor...",
"create": "Paylaşım Oluştur"
},
"shareSecurity": {
"subtitle": "Bu paylaşım için şifre koruması ve güvenlik seçeneklerini yapılandırın",
"info": {
"title": "Nasıl çalışır:",
"withoutPassword": "Bağlantıya sahip herkes bu paylaşıma şifre olmadan erişebilir.",
"withPassword": "Kullanıcıların bu paylaşıma erişmek için şifre girmeleri gerekecek."
},
"existingPasswordMessage": "Bu paylaşımın zaten bir şifresi var. Güncellemek istiyorsanız, aşağıdaki alana yeni şifreyi girin ve kaydedin.",
"passwordProtection": "Şifre Koruması",
"error": {
"updateFailed": "Güvenlik ayarlarını güncelleme başarısız"
},
"passwordRequirements": {
"title": "Şifre gereksinimleri:",
"minLength": "En az 2 karakter"
},
"newPassword": "Yeni Şifre",
"success": {
"passwordUpdated": "Şifre başarıyla güncellendi",
"passwordRemoved": "Şifre koruması başarıyla kaldırıldı",
"passwordSet": "Şifre koruması başarıyla etkinleştirildi"
},
"password": "Şifre",
"validation": {
"passwordRequired": "Şifre gereklidir",
"passwordTooShort": "Şifre en az 2 karakter olmalıdır"
},
"currentStatus": "Mevcut Durum",
"passwordPlaceholder": "Güvenli bir şifre girin",
"title": "Paylaşım Güvenlik Ayarları"
},
"shareExpiration": {
"neverExpires": "Asla Sona Ermez",
"success": {
"expirationUpdated": "Son kullanma tarihi başarıyla güncellendi",
"expirationRemoved": "Son kullanma başarıyla kaldırıldı - paylaşım artık kalıcı",
"expirationSet": "Son kullanma tarihi başarıyla ayarlandı"
},
"info": {
"canBeChanged": "Son kullanma tarihini istediğiniz zaman değiştirebilir veya kaldırabilirsiniz",
"willBeInaccessible": "Paylaşım bu tarihten sonra erişilemez hale gelecek",
"noExpiration": "Bu paylaşım asla sona ermeyecek ve süresiz olarak erişilebilir kalacak.",
"title": "Son kullanma hakkında:"
},
"enableExpiration": "Son Kullanmayı Etkinleştir",
"title": "Paylaşım Son Kullanma Ayarları",
"subtitle": "Bu paylaşımın ne zaman sona ereceğini yapılandırın",
"validation": {
"dateMustBeFuture": "Son kullanma tarihi gelecekte olmalıdır",
"dateRequired": "Lütfen bir son kullanma tarihi seçin"
},
"currentStatus": "Mevcut Durum",
"error": {
"updateFailed": "Son kullanma ayarlarını güncelleme başarısız"
},
"expires": "Sona erer:",
"expirationDate": "Son Kullanma Tarihi"
}
}

View File

@@ -10,7 +10,10 @@
"yes": "是",
"no": "否",
"dashboard": "仪表板",
"back": "返回"
"back": "返回",
"updating": "更新中...",
"saving": "保存中...",
"update": "更新"
},
"createShare": {
"title": "创建分享",
@@ -55,7 +58,7 @@
"descriptionLabel": "描述",
"descriptionPlaceholder": "请输入文件描述",
"deleteFile": "删除文件",
"deleteConfirmation": "您确定要删除{fileName}吗?",
"deleteConfirmation": "您确定要删除{fileName}吗?",
"deleteWarning": "此操作不可撤销。"
},
"fileManager": {
@@ -275,7 +278,8 @@
"title": "最近上传",
"viewAll": "查看全部",
"uploadFile": "上传文件",
"noFiles": "尚未上传文件"
"noFiles": "尚未上传文件",
"upload": "上传"
},
"recentShares": {
"title": "最近共享",
@@ -296,7 +300,25 @@
"removeError": "删除收件人失败",
"sendingNotifications": "正在发送通知...",
"notifySuccess": "收件人通知成功",
"notifyError": "通知收件人失败"
"notifyError": "通知收件人失败",
"bulkNotifySuccess": "已向{count}位收件人发送通知",
"selectAll": "全选",
"singleNotifySuccess": "已向{email}发送通知",
"removeSingle": "移除此收件人",
"selectRecipient": "选择{email}",
"bulkRemoveSuccess": "成功移除{count}位收件人",
"notifySingle": "通知此收件人",
"notifySelected": "通知选中项",
"invalidEmail": "请输入有效的电子邮件地址",
"noRecipientsDescription": "添加收件人以通过电子邮件分享此内容",
"singleNotifyError": "通知收件人失败",
"bulkRemoveError": "移除选中的收件人失败",
"modalDescription": "为此分享添加和管理收件人。当配置了SMTP时您可以通知所有或特定收件人。",
"duplicateEmail": "此收件人已经添加过了",
"removeSelected": "移除选中项",
"selectedCount": "已选择{count}个",
"addRecipient": "添加收件人",
"bulkNotifyError": "通知选中收件人失败"
},
"register": {
"validation": {
@@ -528,15 +550,17 @@
"notAvailable": "不适用",
"invalidDate": "无效的日期",
"loadError": "加载共享详情失败",
"editLink": "Editar Link",
"openLink": "Abrir em nova guia",
"shareLink": "Link de Compartilhamento",
"noDescription": "Nenhuma descrição fornecida",
"copyLink": "Copiar link",
"generateLink": "Gerar Link",
"noLink": "Nenhum link gerado ainda",
"editLink": "编辑链接",
"openLink": "在新标签页中打开",
"shareLink": "分享链接",
"noDescription": "未提供描述",
"copyLink": "复制链接",
"generateLink": "生成链接",
"noLink": "尚未生成链接",
"description": "描述",
"linkCopied": "Link copiado para a área de transferência"
"linkCopied": "链接已复制到剪贴板",
"editSecurity": "编辑安全",
"editExpiration": "编辑过期"
},
"shareManager": {
"deleteSuccess": "共享删除成功",
@@ -554,7 +578,11 @@
"notifyError": "通知收件人失败",
"bulkDeleteError": "删除共享失败",
"bulkDeleteLoading": "正在删除{count, plural, =1 {1个共享} other {#个共享}}...",
"bulkDeleteSuccess": "{count, plural, =1 {1个共享删除成功} other {#个共享删除成功}}"
"bulkDeleteSuccess": "{count, plural, =1 {1个共享删除成功} other {#个共享删除成功}}",
"securityUpdateError": "更新安全设置失败",
"expirationUpdateError": "更新过期设置失败",
"securityUpdateSuccess": "安全设置更新成功",
"expirationUpdateSuccess": "过期设置更新成功"
},
"shares": {
"errors": {
@@ -790,5 +818,63 @@
"totalSize": "总大小",
"creating": "创建中...",
"create": "创建分享"
},
"shareSecurity": {
"subtitle": "为此分享配置密码保护和安全选项",
"info": {
"title": "工作原理:",
"withoutPassword": "任何拥有链接的人都可以在没有密码的情况下访问此分享。",
"withPassword": "用户需要输入密码才能访问此分享。"
},
"existingPasswordMessage": "此分享已有密码。如果您想更新它,请在下面的字段中输入新密码并保存。",
"passwordProtection": "密码保护",
"error": {
"updateFailed": "更新安全设置失败"
},
"passwordRequirements": {
"title": "密码要求:",
"minLength": "至少2个字符"
},
"newPassword": "新密码",
"success": {
"passwordUpdated": "密码更新成功",
"passwordRemoved": "密码保护移除成功",
"passwordSet": "密码保护启用成功"
},
"password": "密码",
"validation": {
"passwordRequired": "密码是必需的",
"passwordTooShort": "密码必须至少包含2个字符"
},
"currentStatus": "当前状态",
"passwordPlaceholder": "输入安全密码",
"title": "分享安全设置"
},
"shareExpiration": {
"neverExpires": "永不过期",
"success": {
"expirationUpdated": "过期日期更新成功",
"expirationRemoved": "过期已移除成功 - 分享现在是永久的",
"expirationSet": "过期日期设置成功"
},
"info": {
"canBeChanged": "您可以随时更改或移除过期日期",
"willBeInaccessible": "分享在此日期后将无法访问",
"noExpiration": "此分享永不过期,将始终可访问。",
"title": "关于过期:"
},
"enableExpiration": "启用过期",
"title": "分享过期设置",
"subtitle": "配置此分享何时过期",
"validation": {
"dateMustBeFuture": "过期日期必须在将来",
"dateRequired": "请选择过期日期"
},
"currentStatus": "当前状态",
"error": {
"updateFailed": "更新过期设置失败"
},
"expires": "过期:",
"expirationDate": "过期日期"
}
}

View File

@@ -6,6 +6,11 @@ import { DeleteConfirmationModal } from "@/components/modals/delete-confirmation
import { GenerateShareLinkModal } from "@/components/modals/generate-share-link-modal";
import { ShareActionsModals } from "@/components/modals/share-actions-modals";
import { ShareDetailsModal } from "@/components/modals/share-details-modal";
import { ShareExpirationModal } from "@/components/modals/share-expiration-modal";
import { ShareFileModal } from "@/components/modals/share-file-modal";
import { ShareMultipleFilesModal } from "@/components/modals/share-multiple-files-modal";
import { ShareSecurityModal } from "@/components/modals/share-security-modal";
import { UploadFileModal } from "@/components/modals/upload-file-modal";
import { SharesModalsProps } from "../types";
export function SharesModals({
@@ -63,6 +68,8 @@ export function SharesModals({
onClose={onCloseViewDetails}
onUpdateName={shareManager.handleUpdateName}
onUpdateDescription={shareManager.handleUpdateDescription}
onUpdateSecurity={shareManager.handleUpdateSecurity}
onUpdateExpiration={shareManager.handleUpdateExpiration}
onGenerateLink={shareManager.handleGenerateLink}
onManageFiles={shareManager.setShareToManageFiles}
refreshTrigger={shareDetailsRefresh}
@@ -76,6 +83,36 @@ export function SharesModals({
onGenerate={shareManager.handleGenerateLink}
onSuccess={handleShareSuccess}
/>
<ShareSecurityModal
shareId={shareManager.shareToManageSecurity?.id || null}
share={shareManager.shareToManageSecurity || null}
onClose={() => shareManager.setShareToManageSecurity(null)}
onSuccess={handleShareSuccess}
/>
<ShareExpirationModal
shareId={shareManager.shareToManageExpiration?.id || null}
share={shareManager.shareToManageExpiration || null}
onClose={() => shareManager.setShareToManageExpiration(null)}
onSuccess={handleShareSuccess}
/>
<ShareMultipleFilesModal
files={fileManager.filesToShare}
isOpen={!!fileManager.filesToShare}
onClose={() => fileManager.setFilesToShare(null)}
onSuccess={() => {
fileManager.handleShareBulkSuccess();
onSuccess();
}}
/>
<UploadFileModal
isOpen={!!shareManager.shareToEdit}
onClose={() => shareManager.setShareToEdit(null)}
onSuccess={onSuccess}
/>
</>
);
}

View File

@@ -12,6 +12,8 @@ export function SharesTableContainer({ shares, onCopyLink, onCreateShare, shareM
onEdit={shareManager.setShareToEdit}
onUpdateName={shareManager.handleUpdateName}
onUpdateDescription={shareManager.handleUpdateDescription}
onUpdateSecurity={shareManager.setShareToManageSecurity}
onUpdateExpiration={shareManager.setShareToManageExpiration}
onGenerateLink={shareManager.setShareToGenerateLink}
onManageFiles={shareManager.setShareToManageFiles}
onManageRecipients={shareManager.setShareToManageRecipients}

View File

@@ -20,7 +20,8 @@ export function RecentFiles({ files, fileManager, onOpenUploadModal }: RecentFil
<IconCloudUpload className="text-xl text-gray-500" />
{t("recentFiles.title")}
</CardTitle>
{files.length >= 5 ? (
<div className="flex items-center gap-2">
<Button
className="font-semibold text-sm cursor-pointer"
variant="outline"
@@ -30,7 +31,7 @@ export function RecentFiles({ files, fileManager, onOpenUploadModal }: RecentFil
<IconFolderOpen className="h-4 w-4" />
{t("recentFiles.viewAll")}
</Button>
) : (
<Button
className="font-semibold text-sm cursor-pointer"
variant="outline"
@@ -40,7 +41,7 @@ export function RecentFiles({ files, fileManager, onOpenUploadModal }: RecentFil
<IconCloudUpload className="h-4 w-4" />
{t("recentFiles.upload")}
</Button>
)}
</div>
</div>
</CardHeader>
<CardContent>

View File

@@ -21,7 +21,8 @@ export function RecentShares({ shares, shareManager, onOpenCreateModal, onCopyLi
<IconShare className="text-xl text-gray-500" />
{t("recentShares.title")}
</h2>
{shares.length >= 5 ? (
<div className="flex items-center gap-2">
<Button
className="font-semibold text-sm cursor-pointer"
variant="outline"
@@ -31,7 +32,7 @@ export function RecentShares({ shares, shareManager, onOpenCreateModal, onCopyLi
<IconShare className="h-4 w-4" />
{t("recentShares.viewAll")}
</Button>
) : shares.length === 0 ? null : (
<Button
className="font-semibold text-sm cursor-pointer"
variant="outline"
@@ -41,7 +42,7 @@ export function RecentShares({ shares, shareManager, onOpenCreateModal, onCopyLi
<IconPlus className="h-4 w-4" />
{t("recentShares.createShare")}
</Button>
)}
</div>
</div>
{shares.length > 0 ? (
@@ -53,6 +54,8 @@ export function RecentShares({ shares, shareManager, onOpenCreateModal, onCopyLi
onEdit={shareManager.setShareToEdit}
onUpdateName={shareManager.handleUpdateName}
onUpdateDescription={shareManager.handleUpdateDescription}
onUpdateSecurity={shareManager.setShareToManageSecurity}
onUpdateExpiration={shareManager.setShareToManageExpiration}
onGenerateLink={shareManager.setShareToGenerateLink}
onManageFiles={shareManager.setShareToManageFiles}
onManageRecipients={shareManager.setShareToManageRecipients}

View File

@@ -9,8 +9,10 @@ import { FilePreviewModal } from "@/components/modals/file-preview-modal";
import { GenerateShareLinkModal } from "@/components/modals/generate-share-link-modal";
import { ShareActionsModals } from "@/components/modals/share-actions-modals";
import { ShareDetailsModal } from "@/components/modals/share-details-modal";
import { ShareExpirationModal } from "@/components/modals/share-expiration-modal";
import { ShareFileModal } from "@/components/modals/share-file-modal";
import { ShareMultipleFilesModal } from "@/components/modals/share-multiple-files-modal";
import { ShareSecurityModal } from "@/components/modals/share-security-modal";
import { UploadFileModal } from "@/components/modals/upload-file-modal";
import { DashboardModalsProps } from "../types";
@@ -113,12 +115,28 @@ export function DashboardModals({ modals, fileManager, shareManager, onSuccess }
onClose={() => shareManager.setShareToViewDetails(null)}
onUpdateName={shareManager.handleUpdateName}
onUpdateDescription={shareManager.handleUpdateDescription}
onUpdateSecurity={shareManager.handleUpdateSecurity}
onUpdateExpiration={shareManager.handleUpdateExpiration}
onGenerateLink={shareManager.handleGenerateLink}
onManageFiles={shareManager.setShareToManageFiles}
refreshTrigger={shareDetailsRefresh}
onSuccess={onSuccess}
/>
<ShareSecurityModal
shareId={shareManager.shareToManageSecurity?.id || null}
share={shareManager.shareToManageSecurity || null}
onClose={() => shareManager.setShareToManageSecurity(null)}
onSuccess={onSuccess}
/>
<ShareExpirationModal
shareId={shareManager.shareToManageExpiration?.id || null}
share={shareManager.shareToManageExpiration || null}
onClose={() => shareManager.setShareToManageExpiration(null)}
onSuccess={onSuccess}
/>
<GenerateShareLinkModal
share={shareManager.shareToGenerateLink || null}
shareId={shareManager.shareToGenerateLink?.id || null}

View File

@@ -1,12 +1,14 @@
"use client";
import { useEffect, useState } from "react";
import { IconBell, IconMail, IconPlus, IconTrash } from "@tabler/icons-react";
import { IconBell, IconCheck, IconMail, IconPlus, IconTrash, IconUsers, IconX } from "@tabler/icons-react";
import { useTranslations } from "next-intl";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Separator } from "@/components/ui/separator";
import { useShareContext } from "@/contexts/share-context";
import { addRecipients, notifyRecipients, removeRecipients } from "@/http/endpoints";
@@ -29,39 +31,95 @@ export function RecipientSelector({ shareId, selectedRecipients, shareAlias, onS
const { smtpEnabled } = useShareContext();
const [recipients, setRecipients] = useState<string[]>(selectedRecipients?.map((recipient) => recipient.email) || []);
const [newRecipient, setNewRecipient] = useState("");
const [selectedForAction, setSelectedForAction] = useState<Set<string>>(new Set());
const [isAddingRecipient, setIsAddingRecipient] = useState(false);
useEffect(() => {
setRecipients(selectedRecipients?.map((recipient) => recipient.email) || []);
setSelectedForAction(new Set());
}, [selectedRecipients]);
const handleAddRecipient = () => {
if (newRecipient && !recipients.includes(newRecipient)) {
addRecipients(shareId, { emails: [newRecipient] })
.then(() => {
setRecipients([...recipients, newRecipient]);
setNewRecipient("");
toast.success(t("recipientSelector.addSuccess"));
onSuccess();
})
.catch(() => {
toast.error(t("recipientSelector.addError"));
});
const isValidEmail = (email: string) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
};
const handleAddRecipient = async () => {
if (!newRecipient.trim()) return;
if (!isValidEmail(newRecipient)) {
toast.error(t("recipientSelector.invalidEmail"));
return;
}
if (recipients.includes(newRecipient)) {
toast.error(t("recipientSelector.duplicateEmail"));
return;
}
setIsAddingRecipient(true);
try {
await addRecipients(shareId, { emails: [newRecipient] });
setRecipients([...recipients, newRecipient]);
setNewRecipient("");
toast.success(t("recipientSelector.addSuccess"));
onSuccess();
} catch (error) {
toast.error(t("recipientSelector.addError"));
} finally {
setIsAddingRecipient(false);
}
};
const handleRemoveRecipient = (email: string) => {
removeRecipients(shareId, { emails: [email] })
.then(() => {
setRecipients(recipients.filter((r) => r !== email));
toast.success(t("recipientSelector.removeSuccess"));
onSuccess();
})
.catch(() => {
toast.error(t("recipientSelector.removeError"));
const handleRemoveRecipient = async (email: string) => {
try {
await removeRecipients(shareId, { emails: [email] });
setRecipients(recipients.filter((r) => r !== email));
setSelectedForAction((prev) => {
const newSet = new Set(prev);
newSet.delete(email);
return newSet;
});
toast.success(t("recipientSelector.removeSuccess"));
onSuccess();
} catch (error) {
toast.error(t("recipientSelector.removeError"));
}
};
const handleNotifyRecipients = async () => {
const handleRemoveSelected = async () => {
const emailsToRemove = Array.from(selectedForAction);
try {
await removeRecipients(shareId, { emails: emailsToRemove });
setRecipients(recipients.filter((r) => !selectedForAction.has(r)));
setSelectedForAction(new Set());
toast.success(t("recipientSelector.bulkRemoveSuccess", { count: emailsToRemove.length }));
onSuccess();
} catch (error) {
toast.error(t("recipientSelector.bulkRemoveError"));
}
};
const handleNotifySelected = async () => {
if (!shareAlias) return;
const emailsToNotify = Array.from(selectedForAction);
const link = `${window.location.origin}/s/${shareAlias}`;
const loadingToast = toast.loading(t("recipientSelector.sendingNotifications"));
try {
await notifyRecipients(shareId, { shareLink: link });
toast.dismiss(loadingToast);
toast.success(t("recipientSelector.bulkNotifySuccess", { count: emailsToNotify.length }));
setSelectedForAction(new Set());
} catch (error) {
console.error(error);
toast.dismiss(loadingToast);
toast.error(t("recipientSelector.bulkNotifyError"));
}
};
const handleNotifyAll = async () => {
if (!shareAlias) return;
const link = `${window.location.origin}/s/${shareAlias}`;
@@ -78,61 +136,200 @@ export function RecipientSelector({ shareId, selectedRecipients, shareAlias, onS
}
};
const handleSelectAll = (checked: boolean) => {
if (checked) {
setSelectedForAction(new Set(recipients));
} else {
setSelectedForAction(new Set());
}
};
const handleSelectRecipient = (email: string, checked: boolean) => {
const newSelected = new Set(selectedForAction);
if (checked) {
newSelected.add(email);
} else {
newSelected.delete(email);
}
setSelectedForAction(newSelected);
};
const isAllSelected = recipients.length > 0 && selectedForAction.size === recipients.length;
const hasSelection = selectedForAction.size > 0;
return (
<div className="flex flex-col gap-4 mb-4">
<div className="flex gap-2">
<div className="relative flex-1">
<IconMail className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500 h-4 w-4" />
<Input
className="pl-9"
placeholder={t("recipientSelector.emailPlaceholder")}
value={newRecipient}
onChange={(e) => setNewRecipient(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleAddRecipient()}
/>
<div className="space-y-6">
<div className="space-y-4">
<div className="flex items-center gap-2">
<IconPlus className="h-5 w-5 text-primary" />
<h3 className="text-lg font-medium">{t("recipientSelector.addRecipient")}</h3>
</div>
<div className="flex flex-col sm:flex-row gap-3">
<div className="relative flex-1">
<IconMail className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground h-4 w-4" />
<Input
className="pl-9 h-10"
placeholder={t("recipientSelector.emailPlaceholder")}
value={newRecipient}
onChange={(e) => setNewRecipient(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && !isAddingRecipient && handleAddRecipient()}
disabled={isAddingRecipient}
/>
</div>
<Button
onClick={handleAddRecipient}
disabled={!newRecipient.trim() || isAddingRecipient}
className="h-10 px-6 sm:w-auto w-full"
>
{isAddingRecipient ? (
<div className="flex items-center gap-2">
<div className="h-4 w-4 animate-spin rounded-full border-2 border-background border-t-transparent" />
{t("common.loading")}
</div>
) : (
<>
<IconPlus className="h-4 w-4" />
{t("recipientSelector.add")}
</>
)}
</Button>
</div>
<Button onClick={handleAddRecipient}>
<IconPlus className="h-4 w-4" />
{t("recipientSelector.add")}
</Button>
</div>
<div className="border rounded-lg p-4">
<div className="flex items-center justify-between mb-4">
<h3 className="font-medium">{t("recipientSelector.recipients", { count: recipients.length })}</h3>
<Separator />
<div className="space-y-4">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<div className="flex items-center gap-2">
<IconUsers className="h-5 w-5 text-primary" />
<h3 className="text-lg font-medium">{t("recipientSelector.recipients", { count: recipients.length })}</h3>
</div>
{recipients.length > 0 && shareAlias && smtpEnabled === "true" && (
<Button variant="outline" size="sm" onClick={handleNotifyRecipients}>
<Button variant="outline" size="sm" onClick={handleNotifyAll} className="sm:w-auto w-full">
<IconBell className="h-4 w-4" />
{t("recipientSelector.notifyAll")}
</Button>
)}
</div>
<div className="max-h-[400px] overflow-y-auto">
<div className="flex flex-col gap-2">
{recipients.length === 0 ? (
<div className="text-center py-8 text-gray-500">{t("recipientSelector.noRecipients")}</div>
) : (
recipients.map((email, index) => (
<div
key={index}
className="flex items-center justify-between p-3 bg-secondary rounded-lg hover:bg-secondary/80"
>
<div className="flex items-center gap-2">
<IconMail className="text-gray-500 h-4 w-4" />
<span>{email}</span>
</div>
<Button
variant="ghost"
size="sm"
className="text-destructive hover:text-destructive hover:bg-destructive/10"
onClick={() => handleRemoveRecipient(email)}
>
<IconTrash className="h-4 w-4" />
</Button>
</div>
))
)}
{hasSelection && (
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 p-3 bg-blue-50 dark:bg-blue-950/30 border border-blue-200 dark:border-blue-800 rounded-lg">
<div className="flex items-center gap-2">
<IconCheck className="h-4 w-4 text-blue-600" />
<span className="text-sm font-medium text-blue-900 dark:text-blue-100">
{t("recipientSelector.selectedCount", { count: selectedForAction.size })}
</span>
</div>
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2">
{smtpEnabled === "true" && shareAlias && (
<Button variant="outline" size="sm" onClick={handleNotifySelected} className="sm:w-auto w-full">
<IconBell className="h-4 w-4" />
{t("recipientSelector.notifySelected")}
</Button>
)}
<Button variant="destructive" size="sm" onClick={handleRemoveSelected} className="sm:w-auto w-full">
<IconTrash className="h-4 w-4" />
{t("recipientSelector.removeSelected")}
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => setSelectedForAction(new Set())}
className="h-8 w-8 p-0 self-center"
title={t("common.cancel")}
>
<IconX className="h-4 w-4" />
</Button>
</div>
</div>
)}
<div className="border rounded-lg overflow-hidden">
{recipients.length === 0 ? (
<div className="text-center py-12 px-6">
<div className="mx-auto w-16 h-16 bg-muted rounded-full flex items-center justify-center mb-4">
<IconUsers className="h-8 w-8 text-muted-foreground" />
</div>
<h4 className="text-lg font-medium mb-2">{t("recipientSelector.noRecipients")}</h4>
<p className="text-sm text-muted-foreground mb-4">{t("recipientSelector.noRecipientsDescription")}</p>
</div>
) : (
<>
<div className="flex items-center gap-3 p-4 border-b bg-muted/30">
<Checkbox
checked={isAllSelected}
onCheckedChange={handleSelectAll}
aria-label={t("recipientSelector.selectAll")}
/>
<span className="text-sm font-medium text-muted-foreground">{t("recipientSelector.selectAll")}</span>
</div>
<div className="divide-y max-h-80 overflow-y-auto">
{recipients.map((email, index) => {
const isSelected = selectedForAction.has(email);
return (
<div
key={index}
className={`flex items-center gap-3 p-4 hover:bg-muted/50 transition-colors ${
isSelected ? "bg-blue-50 dark:bg-blue-950/30" : ""
}`}
>
<Checkbox
checked={isSelected}
onCheckedChange={(checked) => handleSelectRecipient(email, checked as boolean)}
aria-label={t("recipientSelector.selectRecipient", { email })}
/>
<div className="flex items-center gap-3 flex-1 min-w-0">
<div className="w-8 h-8 bg-primary/10 rounded-full flex items-center justify-center flex-shrink-0">
<IconMail className="h-4 w-4 text-primary" />
</div>
<span className="truncate font-medium">{email}</span>
</div>
<div className="flex items-center gap-1">
{smtpEnabled === "true" && shareAlias && (
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0 text-blue-600 hover:text-blue-700 hover:bg-blue-100 dark:hover:bg-blue-900/30"
onClick={async () => {
const link = `${window.location.origin}/s/${shareAlias}`;
const loadingToast = toast.loading(t("recipientSelector.sendingNotifications"));
try {
await notifyRecipients(shareId, { shareLink: link });
toast.dismiss(loadingToast);
toast.success(t("recipientSelector.singleNotifySuccess", { email }));
} catch (error) {
console.error(error);
toast.dismiss(loadingToast);
toast.error(t("recipientSelector.singleNotifyError"));
}
}}
title={t("recipientSelector.notifySingle")}
>
<IconBell className="h-4 w-4" />
</Button>
)}
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0 text-destructive hover:text-destructive hover:bg-destructive/10"
onClick={() => handleRemoveRecipient(email)}
title={t("recipientSelector.removeSingle")}
>
<IconTrash className="h-4 w-4" />
</Button>
</div>
</div>
);
})}
</div>
</>
)}
</div>
</div>
</div>

View File

@@ -234,16 +234,21 @@ export function ShareActionsModals({
</Dialog>
<Dialog open={!!shareToManageRecipients} onOpenChange={() => onCloseManageRecipients()}>
<DialogContent className="sm:max-w-[425px] md:max-w-[700px]">
<DialogHeader>
<DialogTitle>{t("shareActions.manageRecipientsTitle")}</DialogTitle>
<DialogContent className="sm:max-w-[500px] md:max-w-[650px] max-h-[85vh] overflow-hidden">
<DialogHeader className="space-y-3">
<DialogTitle className="text-xl font-semibold">{t("shareActions.manageRecipientsTitle")}</DialogTitle>
<DialogDescription className="text-muted-foreground">
{t("recipientSelector.modalDescription")}
</DialogDescription>
</DialogHeader>
<RecipientSelector
selectedRecipients={shareToManageRecipients?.recipients || []}
shareAlias={shareToManageRecipients?.alias?.alias}
shareId={shareToManageRecipients?.id}
onSuccess={onSuccess}
/>
<div className="overflow-y-auto max-h-[calc(85vh-140px)] py-2">
<RecipientSelector
selectedRecipients={shareToManageRecipients?.recipients || []}
shareAlias={shareToManageRecipients?.alias?.alias}
shareId={shareToManageRecipients?.id}
onSuccess={onSuccess}
/>
</div>
</DialogContent>
</Dialog>
</>

View File

@@ -30,6 +30,8 @@ import { Loader } from "@/components/ui/loader";
import { getShare } from "@/http/endpoints";
import { getFileIcon } from "@/utils/file-icons";
import { GenerateShareLinkModal } from "./generate-share-link-modal";
import { ShareExpirationModal } from "./share-expiration-modal";
import { ShareSecurityModal } from "./share-security-modal";
interface ShareDetailsModalProps {
shareId: string | null;
@@ -38,6 +40,8 @@ interface ShareDetailsModalProps {
onUpdateDescription?: (shareId: string, newDescription: string) => Promise<void>;
onGenerateLink?: (shareId: string, alias: string) => Promise<void>;
onManageFiles?: (share: any) => void;
onUpdateSecurity?: (shareId: string) => Promise<void>;
onUpdateExpiration?: (shareId: string) => Promise<void>;
refreshTrigger?: number;
onSuccess?: () => void;
}
@@ -68,6 +72,8 @@ export function ShareDetailsModal({
onUpdateDescription,
onGenerateLink,
onManageFiles,
onUpdateSecurity,
onUpdateExpiration,
refreshTrigger,
onSuccess,
}: ShareDetailsModalProps) {
@@ -78,6 +84,8 @@ export function ShareDetailsModal({
const [editValue, setEditValue] = useState("");
const [pendingChanges, setPendingChanges] = useState<{ name?: string; description?: string }>({});
const [showLinkModal, setShowLinkModal] = useState(false);
const [showSecurityModal, setShowSecurityModal] = useState(false);
const [showExpirationModal, setShowExpirationModal] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
@@ -93,12 +101,10 @@ export function ShareDetailsModal({
}
}, [editingField]);
// Clear pending changes when share is updated
useEffect(() => {
setPendingChanges({});
}, [share]);
// Refresh data when external update happens
useEffect(() => {
if (refreshTrigger) {
loadShareDetails();
@@ -140,7 +146,6 @@ export function ShareDetailsModal({
const { field } = editingField;
// Update local state optimistically
setPendingChanges((prev) => ({
...prev,
[field]: editValue,
@@ -153,14 +158,13 @@ export function ShareDetailsModal({
await onUpdateDescription(shareId, editValue);
}
// Reload share details to get updated data
await loadShareDetails();
if (onSuccess) {
onSuccess();
}
} catch (error) {
console.error("Failed to update:", error);
// Revert optimistic update on error
setPendingChanges((prev) => {
const newState = { ...prev };
delete newState[field];
@@ -216,6 +220,22 @@ export function ShareDetailsModal({
}
};
const handleSecurityUpdated = async () => {
setShowSecurityModal(false);
await loadShareDetails();
if (onSuccess) {
onSuccess();
}
};
const handleExpirationUpdated = async () => {
setShowExpirationModal(false);
await loadShareDetails();
if (onSuccess) {
onSuccess();
}
};
if (!share) return null;
const shareLink = share?.alias?.alias ? `${window.location.origin}/s/${share.alias.alias}` : null;
@@ -239,7 +259,6 @@ export function ShareDetailsModal({
</div>
) : (
<div className="space-y-4">
{/* Key metrics */}
<div className="grid grid-cols-3 gap-3">
<div className="text-center p-2 bg-muted/30 rounded-lg">
<p className="text-lg font-semibold text-green-600">{share.viewCount || 0}</p>
@@ -255,13 +274,11 @@ export function ShareDetailsModal({
</div>
</div>
{/* Basic Information */}
<div className="space-y-3">
<div className="flex items-center gap-2 border-b pb-2">
<h3 className="text-base font-medium text-foreground">{t("shareDetails.basicInfo")}</h3>
</div>
{/* Name */}
<div>
<div className="flex items-center gap-2 mb-1">
<label className="text-sm font-medium text-muted-foreground">{t("shareDetails.name")}</label>
@@ -308,7 +325,6 @@ export function ShareDetailsModal({
)}
</div>
{/* Description */}
<div>
<div className="flex items-center gap-2 mb-1">
<label className="text-sm font-medium text-muted-foreground">
@@ -359,10 +375,20 @@ export function ShareDetailsModal({
</div>
</div>
{/* Share Link Section */}
<div className="space-y-3">
<div className="flex items-center gap-2 border-b pb-2">
<h3 className="text-base font-medium text-foreground">{t("shareDetails.shareLink")}</h3>
{onGenerateLink && (
<Button
size="icon"
variant="ghost"
className="h-5 w-5 text-muted-foreground hover:text-foreground"
onClick={() => setShowLinkModal(true)}
title={shareLink ? t("shareDetails.editLink") : t("shareDetails.generateLink")}
>
<IconEdit className="h-3 w-3" />
</Button>
)}
</div>
{shareLink ? (
<div className="flex gap-2">
@@ -385,37 +411,30 @@ export function ShareDetailsModal({
>
<IconExternalLink className="h-3.5 w-3.5" />
</Button>
<Button
variant="outline"
size="icon"
className="h-8 w-8"
onClick={() => setShowLinkModal(true)}
title={t("shareDetails.editLink")}
>
<IconEdit className="h-3.5 w-3.5" />
</Button>
</div>
) : (
<div className="flex items-center justify-between p-2 bg-muted/20 rounded-lg">
<p className="text-sm text-muted-foreground">{t("shareDetails.noLink")}</p>
<Button
variant="outline"
size="sm"
onClick={() => setShowLinkModal(true)}
className="gap-1 h-7 text-xs"
>
<IconEdit className="h-3 w-3" />
{t("shareDetails.generateLink")}
</Button>
</div>
)}
</div>
{/* Dates and Security in Grid */}
<div className="grid grid-cols-2 gap-4">
{/* Dates */}
<div className="space-y-3">
<h3 className="text-base font-medium text-foreground border-b pb-2">{t("shareDetails.dates")}</h3>
<div className="flex items-center gap-2 border-b pb-2">
<h3 className="text-base font-medium text-foreground">{t("shareDetails.dates")}</h3>
{onUpdateExpiration && (
<Button
size="icon"
variant="ghost"
className="h-5 w-5 text-muted-foreground hover:text-foreground"
onClick={() => setShowExpirationModal(true)}
title={t("shareDetails.editExpiration")}
>
<IconEdit className="h-3 w-3" />
</Button>
)}
</div>
<div className="space-y-2">
<div>
<div className="text-xs font-medium text-muted-foreground">{t("shareDetails.created")}</div>
@@ -430,11 +449,21 @@ export function ShareDetailsModal({
</div>
</div>
{/* Security */}
<div className="space-y-3">
<h3 className="text-base font-medium text-foreground border-b pb-2">
{t("shareDetails.security")}
</h3>
<div className="flex items-center gap-2 border-b pb-2">
<h3 className="text-base font-medium text-foreground">{t("shareDetails.security")}</h3>
{onUpdateSecurity && (
<Button
size="icon"
variant="ghost"
className="h-5 w-5 text-muted-foreground hover:text-foreground"
onClick={() => setShowSecurityModal(true)}
title={t("shareDetails.editSecurity")}
>
<IconEdit className="h-3 w-3" />
</Button>
)}
</div>
<div className="flex flex-col gap-2">
{share.security?.hasPassword ? (
<Badge variant="secondary" className="bg-yellow-500/20 text-yellow-700 border-yellow-200 w-fit">
@@ -456,7 +485,6 @@ export function ShareDetailsModal({
</div>
</div>
{/* Files */}
{share.files && share.files.length > 0 && (
<div className="space-y-3">
<div className="flex items-center gap-2 border-b pb-2">
@@ -504,7 +532,6 @@ export function ShareDetailsModal({
</div>
)}
{/* Recipients */}
{share.recipients && share.recipients.length > 0 && (
<div className="space-y-3">
<h3 className="text-base font-medium text-foreground border-b pb-2">
@@ -541,6 +568,22 @@ export function ShareDetailsModal({
onSuccess={handleLinkGenerated}
/>
)}
{showSecurityModal && (
<ShareSecurityModal
shareId={shareId}
share={share}
onClose={() => setShowSecurityModal(false)}
onSuccess={handleSecurityUpdated}
/>
)}
{showExpirationModal && (
<ShareExpirationModal
shareId={shareId}
share={share}
onClose={() => setShowExpirationModal(false)}
onSuccess={handleExpirationUpdated}
/>
)}
</>
);
}

View File

@@ -0,0 +1,203 @@
"use client";
import { useEffect, useState } from "react";
import { IconCalendar, IconClock, IconClockOff } from "@tabler/icons-react";
import { useTranslations } from "next-intl";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Loader } from "@/components/ui/loader";
import { Switch } from "@/components/ui/switch";
import { updateShare } from "@/http/endpoints";
interface ShareExpirationModalProps {
shareId: string | null;
share: any;
onClose: () => void;
onSuccess?: () => void;
}
export function ShareExpirationModal({ shareId, share, onClose, onSuccess }: ShareExpirationModalProps) {
const t = useTranslations();
const [isLoading, setIsLoading] = useState(false);
const [hasExpiration, setHasExpiration] = useState(false);
const [expirationDate, setExpirationDate] = useState("");
useEffect(() => {
if (share) {
const hasCurrentExpiration = !!share.expiration;
setHasExpiration(hasCurrentExpiration);
if (hasCurrentExpiration) {
// Converter para formato datetime-local
const date = new Date(share.expiration);
setExpirationDate(date.toISOString().slice(0, 16));
} else {
setExpirationDate("");
}
}
}, [share]);
const handleSave = async () => {
if (!shareId) return;
// Validação
if (hasExpiration) {
if (!expirationDate.trim()) {
toast.error(t("shareExpiration.validation.dateRequired"));
return;
}
const selectedDate = new Date(expirationDate);
const now = new Date();
if (selectedDate <= now) {
toast.error(t("shareExpiration.validation.dateMustBeFuture"));
return;
}
}
setIsLoading(true);
try {
await updateShare({
id: shareId,
expiration: hasExpiration ? new Date(expirationDate).toISOString() : undefined,
});
const successMessage = hasExpiration
? share?.expiration
? t("shareExpiration.success.expirationUpdated")
: t("shareExpiration.success.expirationSet")
: t("shareExpiration.success.expirationRemoved");
toast.success(successMessage);
if (onSuccess) {
onSuccess();
}
onClose();
} catch (error) {
console.error("Failed to update share expiration:", error);
toast.error(t("shareExpiration.error.updateFailed"));
} finally {
setIsLoading(false);
}
};
const handleExpirationToggle = (checked: boolean) => {
setHasExpiration(checked);
if (!checked) {
setExpirationDate("");
} else if (!expirationDate) {
// Definir data padrão para 7 dias no futuro
const defaultDate = new Date();
defaultDate.setDate(defaultDate.getDate() + 7);
setExpirationDate(defaultDate.toISOString().slice(0, 16));
}
};
// Determina o texto do botão
const getButtonText = () => {
if (isLoading) {
return (
<div className="flex items-center gap-2">
<Loader size="sm" />
{share?.expiration ? t("common.updating") : t("common.saving")}
</div>
);
}
return share?.expiration ? t("common.update") : t("common.save");
};
return (
<Dialog open={!!shareId} onOpenChange={() => onClose()}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle>{t("shareExpiration.title")}</DialogTitle>
<DialogDescription>{t("shareExpiration.subtitle")}</DialogDescription>
</DialogHeader>
<div className="py-4 space-y-6">
{/* Status Atual */}
<div className="space-y-3">
<h3 className="text-sm font-medium text-foreground">{t("shareExpiration.currentStatus")}</h3>
<div className="flex gap-2">
{share?.expiration ? (
<div className="bg-yellow-500/20 text-yellow-800 border border-yellow-300 dark:bg-yellow-500/10 dark:text-yellow-400 dark:border-yellow-500/20 rounded-md px-2 py-1 text-xs font-medium flex items-center gap-1">
<IconClock className="h-3 w-3" />
{t("shareExpiration.expires")} {new Date(share.expiration).toLocaleString()}
</div>
) : (
<div className="bg-green-500/20 text-green-800 border border-green-300 dark:bg-green-500/10 dark:text-green-400 dark:border-green-500/20 rounded-md px-2 py-1 text-xs font-medium flex items-center gap-1">
<IconClockOff className="h-3 w-3" />
{t("shareExpiration.neverExpires")}
</div>
)}
</div>
</div>
{/* Configuração de Expiração */}
<div className="space-y-4">
<div className="flex items-center space-x-2">
<Switch id="expiration-enabled" checked={hasExpiration} onCheckedChange={handleExpirationToggle} />
<Label htmlFor="expiration-enabled" className="flex items-center gap-2">
<IconCalendar size={16} />
{t("shareExpiration.enableExpiration")}
</Label>
</div>
{hasExpiration && (
<div className="space-y-4 pl-6 border-l-2 border-muted">
<div className="space-y-2">
<Label htmlFor="expiration-date">{t("shareExpiration.expirationDate")}</Label>
<Input
id="expiration-date"
type="datetime-local"
value={expirationDate}
onChange={(e) => setExpirationDate(e.target.value)}
min={new Date().toISOString().slice(0, 16)}
/>
</div>
<div className="text-xs text-muted-foreground space-y-1">
<p>{t("shareExpiration.info.title")}</p>
<ul className="list-disc list-inside space-y-1 ml-2">
<li>{t("shareExpiration.info.willBeInaccessible")}</li>
<li>{t("shareExpiration.info.canBeChanged")}</li>
</ul>
</div>
</div>
)}
{!hasExpiration && (
<div className="pl-6 border-l-2 border-muted">
<div className="bg-muted/50 border border-border rounded-lg p-3">
<p className="text-sm text-muted-foreground">{t("shareExpiration.info.noExpiration")}</p>
</div>
</div>
)}
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={onClose} disabled={isLoading}>
{t("common.cancel")}
</Button>
<Button onClick={handleSave} disabled={isLoading}>
{getButtonText()}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,226 @@
"use client";
import { useEffect, useState } from "react";
import { IconCheck, IconEye, IconEyeOff, IconLock, IconLockOpen, IconX } from "@tabler/icons-react";
import { useTranslations } from "next-intl";
import { toast } from "sonner";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Loader } from "@/components/ui/loader";
import { Switch } from "@/components/ui/switch";
import { updateSharePassword } from "@/http/endpoints";
interface ShareSecurityModalProps {
shareId: string | null;
share: any;
onClose: () => void;
onSuccess?: () => void;
}
export function ShareSecurityModal({ shareId, share, onClose, onSuccess }: ShareSecurityModalProps) {
const t = useTranslations();
const [isLoading, setIsLoading] = useState(false);
const [hasPassword, setHasPassword] = useState(false);
const [password, setPassword] = useState("");
const [showPassword, setShowPassword] = useState(false);
useEffect(() => {
if (share?.security) {
const hasCurrentPassword = share.security.hasPassword || false;
setHasPassword(hasCurrentPassword);
// Campo sempre começa vazio
setPassword("");
}
}, [share]);
const handleSave = async () => {
if (!shareId) return;
// Validação de senha
if (hasPassword) {
if (!password.trim()) {
toast.error(t("shareSecurity.validation.passwordRequired"));
return;
}
if (password.length < 2) {
toast.error(t("shareSecurity.validation.passwordTooShort"));
return;
}
}
setIsLoading(true);
try {
await updateSharePassword(shareId, {
password: hasPassword ? password : null,
});
const successMessage = hasPassword
? share?.security?.hasPassword
? t("shareSecurity.success.passwordUpdated")
: t("shareSecurity.success.passwordSet")
: t("shareSecurity.success.passwordRemoved");
toast.success(successMessage);
if (onSuccess) {
onSuccess();
}
onClose();
} catch (error) {
console.error("Failed to update share security:", error);
toast.error(t("shareSecurity.error.updateFailed"));
} finally {
setIsLoading(false);
}
};
const handlePasswordToggle = (checked: boolean) => {
setHasPassword(checked);
if (!checked) {
setPassword("");
setShowPassword(false);
}
};
const togglePasswordVisibility = () => {
setShowPassword(!showPassword);
};
// Determina o texto do botão
const getButtonText = () => {
if (isLoading) {
return (
<div className="flex items-center gap-2">
<Loader size="sm" />
{share?.security?.hasPassword ? t("common.updating") : t("common.saving")}
</div>
);
}
return share?.security?.hasPassword ? t("common.update") : t("common.save");
};
return (
<Dialog open={!!shareId} onOpenChange={() => onClose()}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle>{t("shareSecurity.title")}</DialogTitle>
<DialogDescription>{t("shareSecurity.subtitle")}</DialogDescription>
</DialogHeader>
<div className="py-4 space-y-6">
{/* Status Atual */}
<div className="space-y-3">
<h3 className="text-sm font-medium text-foreground">{t("shareSecurity.currentStatus")}</h3>
<div className="flex gap-2">
{share?.security?.hasPassword ? (
<Badge
variant="secondary"
className="bg-yellow-500/20 text-yellow-800 border-yellow-300 dark:bg-yellow-500/10 dark:text-yellow-400 dark:border-yellow-500/20"
>
<IconLock className="h-3 w-3 mr-1" />
{t("shareDetails.passwordProtected")}
</Badge>
) : (
<Badge
variant="secondary"
className="bg-green-500/20 text-green-800 border-green-300 dark:bg-green-500/10 dark:text-green-400 dark:border-green-500/20"
>
<IconLockOpen className="h-3 w-3 mr-1" />
{t("shareDetails.publicAccess")}
</Badge>
)}
</div>
</div>
{/* Configuração de Senha */}
<div className="space-y-4">
<div className="flex items-center space-x-2">
<Switch id="password-protection" checked={hasPassword} onCheckedChange={handlePasswordToggle} />
<Label htmlFor="password-protection" className="flex items-center gap-2">
<IconLock size={16} />
{t("shareSecurity.passwordProtection")}
</Label>
</div>
{hasPassword && (
<div className="space-y-4 pl-6 border-l-2 border-muted">
{/* Mensagem explicativa quando já tem senha */}
{share?.security?.hasPassword && (
<div className="bg-muted/50 border border-border rounded-lg p-3">
<p className="text-sm text-muted-foreground">{t("shareSecurity.existingPasswordMessage")}</p>
</div>
)}
<div className="space-y-2">
<Label htmlFor="password">
{share?.security?.hasPassword ? t("shareSecurity.newPassword") : t("shareSecurity.password")}
</Label>
<div className="relative">
<Input
id="password"
type={showPassword ? "text" : "password"}
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder={t("shareSecurity.passwordPlaceholder")}
className="pr-10"
/>
<Button
type="button"
variant="ghost"
size="icon"
className="absolute right-0 top-0 h-full w-10 hover:bg-transparent"
onClick={togglePasswordVisibility}
>
{showPassword ? (
<IconEyeOff className="h-4 w-4 text-muted-foreground hover:text-foreground" />
) : (
<IconEye className="h-4 w-4 text-muted-foreground hover:text-foreground" />
)}
</Button>
</div>
</div>
<div className="text-xs text-muted-foreground space-y-1">
<p>{t("shareSecurity.passwordRequirements.title")}</p>
<ul className="list-disc list-inside space-y-1 ml-2">
<li>{t("shareSecurity.passwordRequirements.minLength")}</li>
</ul>
</div>
</div>
)}
</div>
{/* Informações Adicionais */}
<div className="bg-muted/30 p-3 rounded-lg">
<div className="text-sm space-y-1">
<p className="font-medium text-muted-foreground">{t("shareSecurity.info.title")}</p>
<p className="text-muted-foreground">
{hasPassword ? t("shareSecurity.info.withPassword") : t("shareSecurity.info.withoutPassword")}
</p>
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={onClose} disabled={isLoading}>
{t("common.cancel")}
</Button>
<Button onClick={handleSave} disabled={isLoading}>
{getButtonText()}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -37,6 +37,8 @@ export interface SharesTableProps {
onEdit: (share: any) => void;
onUpdateName: (shareId: string, newName: string) => void;
onUpdateDescription: (shareId: string, newDescription: string) => void;
onUpdateSecurity?: (share: any) => void;
onUpdateExpiration?: (share: any) => void;
onManageFiles: (share: any) => void;
onManageRecipients: (share: any) => void;
onViewDetails: (share: any) => void;
@@ -53,6 +55,8 @@ export function SharesTable({
onEdit,
onUpdateName,
onUpdateDescription,
onUpdateSecurity,
onUpdateExpiration,
onManageFiles,
onManageRecipients,
onViewDetails,
@@ -66,7 +70,10 @@ export function SharesTable({
const { smtpEnabled } = useShareContext();
const [editingField, setEditingField] = useState<{ shareId: string; field: "name" | "description" } | null>(null);
const [editValue, setEditValue] = useState("");
const [hoveredField, setHoveredField] = useState<{ shareId: string; field: "name" | "description" } | null>(null);
const [hoveredField, setHoveredField] = useState<{
shareId: string;
field: "name" | "description" | "security" | "expiration" | "files" | "recipients";
} | null>(null);
const [pendingChanges, setPendingChanges] = useState<{ [shareId: string]: { name?: string; description?: string } }>(
{}
);
@@ -246,6 +253,10 @@ export function SharesTable({
const isEditingDescription = editingField?.shareId === share.id && editingField?.field === "description";
const isHoveringName = hoveredField?.shareId === share.id && hoveredField?.field === "name";
const isHoveringDescription = hoveredField?.shareId === share.id && hoveredField?.field === "description";
const isHoveringSecurity = hoveredField?.shareId === share.id && hoveredField?.field === "security";
const isHoveringExpiration = hoveredField?.shareId === share.id && hoveredField?.field === "expiration";
const isHoveringFiles = hoveredField?.shareId === share.id && hoveredField?.field === "files";
const isHoveringRecipients = hoveredField?.shareId === share.id && hoveredField?.field === "recipients";
const isSelected = selectedShares.has(share.id);
const displayName = getDisplayValue(share, "name");
const displayDescription = getDisplayValue(share, "description");
@@ -391,7 +402,32 @@ export function SharesTable({
</TableCell>
<TableCell className="h-12 px-4">{format(new Date(share.createdAt), "MM/dd/yyyy HH:mm")}</TableCell>
<TableCell className="h-12 px-4">
{share.expiration ? format(new Date(share.expiration), "MM/dd/yyyy HH:mm") : t("sharesTable.never")}
<div
className="flex items-center gap-1 min-w-0"
onMouseEnter={() => setHoveredField({ shareId: share.id, field: "expiration" })}
onMouseLeave={() => setHoveredField(null)}
>
<span className="text-sm">
{share.expiration
? format(new Date(share.expiration), "MM/dd/yyyy HH:mm")
: t("sharesTable.never")}
</span>
<div className="w-6 flex justify-center flex-shrink-0">
{isHoveringExpiration && onUpdateExpiration && (
<Button
size="icon"
variant="ghost"
className="h-6 w-6 text-muted-foreground hover:text-foreground hidden sm:block"
onClick={(e) => {
e.stopPropagation();
onUpdateExpiration(share);
}}
>
<IconEdit className="h-3 w-3" />
</Button>
)}
</div>
</div>
</TableCell>
<TableCell className="h-12 px-4">
<Badge
@@ -410,29 +446,96 @@ export function SharesTable({
</Badge>
</TableCell>
<TableCell className="h-12 px-4">
<Badge
variant="secondary"
className={`flex items-center gap-1 ${
share.security.hasPassword
? "bg-yellow-500/20 hover:bg-yellow-500/30 text-yellow-500"
: "bg-green-500/20 hover:bg-green-500/30 text-green-500"
}`}
<div
className="flex items-center gap-1 min-w-0"
onMouseEnter={() => setHoveredField({ shareId: share.id, field: "security" })}
onMouseLeave={() => setHoveredField(null)}
>
{share.security.hasPassword ? (
<IconLock className="h-4 w-4" />
) : (
<IconLockOpen className="h-4 w-4" />
)}
{share.security.hasPassword
? t("sharesTable.security.protected")
: t("sharesTable.security.public")}
</Badge>
<Badge
variant="secondary"
className={`flex items-center gap-1 ${
share.security.hasPassword
? "bg-yellow-500/20 hover:bg-yellow-500/30 text-yellow-500"
: "bg-green-500/20 hover:bg-green-500/30 text-green-500"
}`}
>
{share.security.hasPassword ? (
<IconLock className="h-4 w-4" />
) : (
<IconLockOpen className="h-4 w-4" />
)}
{share.security.hasPassword
? t("sharesTable.security.protected")
: t("sharesTable.security.public")}
</Badge>
<div className="w-6 flex justify-center flex-shrink-0">
{isHoveringSecurity && onUpdateSecurity && (
<Button
size="icon"
variant="ghost"
className="h-6 w-6 text-muted-foreground hover:text-foreground hidden sm:block"
onClick={(e) => {
e.stopPropagation();
onUpdateSecurity(share);
}}
>
<IconEdit className="h-3 w-3" />
</Button>
)}
</div>
</div>
</TableCell>
<TableCell className="h-12 px-4">
{share.files?.length || 0} {t("sharesTable.filesCount")}
<div
className="flex items-center gap-1 min-w-0"
onMouseEnter={() => setHoveredField({ shareId: share.id, field: "files" })}
onMouseLeave={() => setHoveredField(null)}
>
<span className="text-sm">
{share.files?.length || 0} {t("sharesTable.filesCount")}
</span>
<div className="w-6 flex justify-center flex-shrink-0">
{isHoveringFiles && onManageFiles && (
<Button
size="icon"
variant="ghost"
className="h-6 w-6 text-muted-foreground hover:text-foreground hidden sm:block"
onClick={(e) => {
e.stopPropagation();
onManageFiles(share);
}}
>
<IconEdit className="h-3 w-3" />
</Button>
)}
</div>
</div>
</TableCell>
<TableCell className="h-12 px-4">
{share.recipients?.length || 0} {t("sharesTable.recipientsCount")}
<div
className="flex items-center gap-1 min-w-0"
onMouseEnter={() => setHoveredField({ shareId: share.id, field: "recipients" })}
onMouseLeave={() => setHoveredField(null)}
>
<span className="text-sm">
{share.recipients?.length || 0} {t("sharesTable.recipientsCount")}
</span>
<div className="w-6 flex justify-center flex-shrink-0">
{isHoveringRecipients && onManageRecipients && (
<Button
size="icon"
variant="ghost"
className="h-6 w-6 text-muted-foreground hover:text-foreground hidden sm:block"
onClick={(e) => {
e.stopPropagation();
onManageRecipients(share);
}}
>
<IconEdit className="h-3 w-3" />
</Button>
)}
</div>
</div>
</TableCell>
<TableCell className="h-12 px-4 text-right">
<DropdownMenu>

View File

@@ -11,6 +11,7 @@ import {
deleteShare,
notifyRecipients,
updateShare,
updateSharePassword,
} from "@/http/endpoints";
import { ListUserShares200SharesItem } from "@/http/models/listUserShares200SharesItem";
@@ -19,6 +20,8 @@ export interface ShareManagerHook {
shareToEdit: ListUserShares200SharesItem | null;
shareToManageFiles: ListUserShares200SharesItem | null;
shareToManageRecipients: ListUserShares200SharesItem | null;
shareToManageSecurity: ListUserShares200SharesItem | null;
shareToManageExpiration: ListUserShares200SharesItem | null;
shareToViewDetails: ListUserShares200SharesItem | null;
shareToGenerateLink: ListUserShares200SharesItem | null;
sharesToDelete: ListUserShares200SharesItem[] | null;
@@ -26,6 +29,8 @@ export interface ShareManagerHook {
setShareToEdit: (share: ListUserShares200SharesItem | null) => void;
setShareToManageFiles: (share: ListUserShares200SharesItem | null) => void;
setShareToManageRecipients: (share: ListUserShares200SharesItem | null) => void;
setShareToManageSecurity: (share: ListUserShares200SharesItem | null) => void;
setShareToManageExpiration: (share: ListUserShares200SharesItem | null) => void;
setShareToViewDetails: (share: ListUserShares200SharesItem | null) => void;
setShareToGenerateLink: (share: ListUserShares200SharesItem | null) => void;
setSharesToDelete: (shares: ListUserShares200SharesItem[] | null) => void;
@@ -35,6 +40,8 @@ export interface ShareManagerHook {
handleEdit: (shareId: string, data: any) => Promise<void>;
handleUpdateName: (shareId: string, newName: string) => Promise<void>;
handleUpdateDescription: (shareId: string, newDescription: string) => Promise<void>;
handleUpdateSecurity: (shareId: string) => Promise<void>;
handleUpdateExpiration: (shareId: string) => Promise<void>;
handleManageFiles: (shareId: string, files: any[]) => Promise<void>;
handleManageRecipients: (shareId: string, recipients: any[]) => Promise<void>;
handleGenerateLink: (shareId: string, alias: string) => Promise<void>;
@@ -48,6 +55,8 @@ export function useShareManager(onSuccess: () => void) {
const [shareToEdit, setShareToEdit] = useState<ListUserShares200SharesItem | null>(null);
const [shareToManageFiles, setShareToManageFiles] = useState<ListUserShares200SharesItem | null>(null);
const [shareToManageRecipients, setShareToManageRecipients] = useState<ListUserShares200SharesItem | null>(null);
const [shareToManageSecurity, setShareToManageSecurity] = useState<ListUserShares200SharesItem | null>(null);
const [shareToManageExpiration, setShareToManageExpiration] = useState<ListUserShares200SharesItem | null>(null);
const [shareToViewDetails, setShareToViewDetails] = useState<ListUserShares200SharesItem | null>(null);
const [shareToGenerateLink, setShareToGenerateLink] = useState<ListUserShares200SharesItem | null>(null);
const [sharesToDelete, setSharesToDelete] = useState<ListUserShares200SharesItem[] | null>(null);
@@ -130,6 +139,26 @@ export function useShareManager(onSuccess: () => void) {
}
};
const handleUpdateSecurity = async (shareId: string) => {
try {
await onSuccess();
toast.success(t("shareManager.securityUpdateSuccess"));
} catch (error) {
toast.error(t("shareManager.securityUpdateError"));
console.error(error);
}
};
const handleUpdateExpiration = async (shareId: string) => {
try {
await onSuccess();
toast.success(t("shareManager.expirationUpdateSuccess"));
} catch (error) {
toast.error(t("shareManager.expirationUpdateError"));
console.error(error);
}
};
const handleManageFiles = async (shareId: string, files: string[]) => {
try {
await addFiles(shareId, { files });
@@ -186,6 +215,8 @@ export function useShareManager(onSuccess: () => void) {
shareToEdit,
shareToManageFiles,
shareToManageRecipients,
shareToManageSecurity,
shareToManageExpiration,
shareToViewDetails,
shareToGenerateLink,
sharesToDelete,
@@ -193,6 +224,8 @@ export function useShareManager(onSuccess: () => void) {
setShareToEdit,
setShareToManageFiles,
setShareToManageRecipients,
setShareToManageSecurity,
setShareToManageExpiration,
setShareToViewDetails,
setShareToGenerateLink,
setSharesToDelete,
@@ -202,6 +235,8 @@ export function useShareManager(onSuccess: () => void) {
handleEdit,
handleUpdateName,
handleUpdateDescription,
handleUpdateSecurity,
handleUpdateExpiration,
handleManageFiles,
handleManageRecipients,
handleGenerateLink,