diff --git a/.github/workflows/mobile-lint.yml b/.github/workflows/mobile-lint.yml index 48e38dc6c..57b2ca4db 100644 --- a/.github/workflows/mobile-lint.yml +++ b/.github/workflows/mobile-lint.yml @@ -9,7 +9,7 @@ on: - ".github/workflows/mobile-lint.yml" env: - FLUTTER_VERSION: "3.13.4" + FLUTTER_VERSION: "3.19.5" jobs: lint: diff --git a/.github/workflows/mobile-release.yml b/.github/workflows/mobile-release.yml index 3ce4db8d2..0f45df751 100644 --- a/.github/workflows/mobile-release.yml +++ b/.github/workflows/mobile-release.yml @@ -9,7 +9,7 @@ on: - "photos-v*" env: - FLUTTER_VERSION: "3.13.4" + FLUTTER_VERSION: "3.19.5" jobs: build: diff --git a/auth/lib/l10n/arb/app_ar.arb b/auth/lib/l10n/arb/app_ar.arb index ea93af7a4..68bd38900 100644 --- a/auth/lib/l10n/arb/app_ar.arb +++ b/auth/lib/l10n/arb/app_ar.arb @@ -78,14 +78,12 @@ "data": "البيانات", "importCodes": "رمزالاستيراد", "importTypePlainText": "نص عادي", - "importTypeEnteEncrypted": "تصدير مشفر ente", "passwordForDecryptingExport": "كلمة المرور لفك تشفير التصدير", "passwordEmptyError": "لا يمكن أن تكون كلمة المرور فارغة", "importFromApp": "استيراد الرموز من {appName}", "importGoogleAuthGuide": "قم بتصدير حساباتك من Google Authenticator إلى رمز QR code باستخدام خيار \"Transfer Accounts\" ثم استخدم جهازًا آخر لمسح رمز الاستجابة السريعة ضوئيًا.\n\nنصيحة: يمكنك استخدام كاميرا الويب الخاصة بالكمبيوتر المحمول لالتقاط صورة لرمز الاستجابة السريعة.", "importSelectJsonFile": "حدد ملف JSON", "importSelectAppExport": "حدد ملف التصدير {appName}", - "importEnteEncGuide": "حدد ملف JSON المشفر الذي تم تصديره من ente", "importRaivoGuide": "استخدم خيار تصدير OTP إلى أرشيف Zip في إعدادات Raivo.\n\nاستخرج ملف zip واسترد ملف JSON.", "importBitwardenGuide": "استخدم خيار \"تصدير خزانة\" داخل أدوات Bitwarden واستيراد ملف JSON غير مشفر.", "importAegisGuide": "استخدم خيار \"Export the vault\" في إعدادات Aegis.\n\nإذا كان المخزن الخاص بك مشفرًا، فستحتاج إلى إدخال كلمة مرور المخزن لفك تشفير المخزن.", @@ -115,22 +113,18 @@ "copied": "تم النسخ", "pleaseTryAgain": "حاول مرة اخرى", "existingUser": "المستخدم موجود", - "newUser": "جديد إلى Ente", "delete": "حذف", "enterYourPasswordHint": "أدخل كلمة المرور الخاصة بك", "forgotPassword": "هل نسيت كلمة المرور", "oops": "عذرًا", "suggestFeatures": "اقتراح ميزة", "faq": "الأسئلة الأكثر شيوعاً", - "faq_q_1": "ما مدى أمان المصادقة؟", - "faq_a_1": "يتم تشفير جميع الرموز التي تقوم بنسخها احتياطا عبر Ente. وهذا يعني أنه يمكنك فقط الوصول إلى الرموز الخاصة بك. تطبيقاتنا مفتوحة المصدر وقد تم مراجعة التشفير خارجيا.", "faq_q_2": "هل يمكنني الوصول إلى رموزي على سطح المكتب؟", "faq_a_2": "يمكنك الوصول إلى رموزك على الويب @ auth.ente.io.", "faq_q_3": "كيف يمكنني حذف الرموز؟", "faq_a_3": "يمكنك حذف الرمز عن طريق السحب لليسار على هذا العنصر.", "faq_q_4": "كيف يمكنني دعم هذا المشروع؟", "faq_a_4": "يمكنك دعم تطوير هذا المشروع عن طريق الاشتراك في تطبيق الصور @ ente.io.", - "faq_q_5": "كيف يمكنني تمكين قفل FaceID في المصادقة Ente", "faq_a_5": "يمكنك تمكين قفل FaceID تحت الإعدادات => الحماية => قفل الشاشة.", "somethingWentWrongMessage": "حدث خطأ ما، يرجى المحاولة مرة أخرى", "leaveFamily": "مغادرة خطة العائلة", @@ -144,6 +138,8 @@ "enterCodeHint": "أدخل الرمز المكون من 6 أرقام من\nتطبيق المصادقة", "lostDeviceTitle": "جهاز مفقود ؟", "twoFactorAuthTitle": "المصادقة الثنائية", + "passkeyAuthTitle": "التحقق من مفتاح المرور", + "verifyPasskey": "تحقق من مفتاح المرور", "recoverAccount": "إسترجاع الحساب", "enterRecoveryKeyHint": "أدخل رمز الاسترداد", "recover": "استرداد", @@ -197,6 +193,8 @@ "recoveryKeySaveDescription": "نحن لا نخزن هذا المفتاح، يرجى حفظ مفتاح الـ 24 كلمة هذا في مكان آمن.", "doThisLater": "قم بهذا لاحقاً", "saveKey": "حفظ المفتاح", + "save": "حفظ", + "send": "إرسال", "back": "الرجوع", "createAccount": "إنشاء حساب", "passwordStrength": "قوة كلمة المرور: {passwordStrengthValue}", @@ -344,7 +342,6 @@ "deleteCodeAuthMessage": "المصادقة لحذف الرمز", "showQRAuthMessage": "المصادقة لإظهار رمز QR", "confirmAccountDeleteTitle": "تأكيد حذف الحساب", - "confirmAccountDeleteMessage": "هذا الحساب مرتبط بتطبيقات Ente أخرى، إذا كنت تستخدم أي منها.\n\nبياناتك التي تم تحميلها، عبر جميع تطبيقات Ente سيتم جدولتها للحذف، وسيتم حذف حسابك بشكل دائم.", "androidBiometricHint": "التحقق من الهوية", "@androidBiometricHint": { "description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters." @@ -404,5 +401,7 @@ "signOutOtherDevices": "تسجيل الخروج من الأجهزة الأخرى", "doNotSignOut": "لا تقم بتسجيل الخروج", "hearUsWhereTitle": "كيف سمعت عن Ente؟ (اختياري)", - "hearUsExplanation": "نحن لا نتتبع تثبيت التطبيق. سيكون من المفيد إذا أخبرتنا أين وجدتنا!" + "hearUsExplanation": "نحن لا نتتبع تثبيت التطبيق. سيكون من المفيد إذا أخبرتنا أين وجدتنا!", + "passkey": "مفتاح المرور", + "developerSettings": "اعدادات المطور" } \ No newline at end of file diff --git a/auth/lib/l10n/arb/app_de.arb b/auth/lib/l10n/arb/app_de.arb index 7b21fcab1..a7cd56043 100644 --- a/auth/lib/l10n/arb/app_de.arb +++ b/auth/lib/l10n/arb/app_de.arb @@ -78,14 +78,12 @@ "data": "Datei", "importCodes": "Codes importieren", "importTypePlainText": "Klartext", - "importTypeEnteEncrypted": "ente verschlüsselt exportieren", "passwordForDecryptingExport": "Passwort um den Export zu entschlüsseln", "passwordEmptyError": "Passwort kann nicht leer sein", "importFromApp": "Importiere Codes von {appName}", "importGoogleAuthGuide": "Exportiere deine Accounts von Google Authenticator zu einem QR-Code, durch die \"Konten übertragen\" Option. Scanne den QR-Code danach mit einem anderen Gerät.\n\nTipp: Du kannst die Kamera eines Laptops verwenden, um ein Foto den dem QR-Code zu erstellen.", "importSelectJsonFile": "Wähle eine JSON-Datei", "importSelectAppExport": "{appName} Exportdatei auswählen", - "importEnteEncGuide": "Wähle die von ente exportierte, verschlüsselte JSON-Datei", "importRaivoGuide": "Verwenden Sie die Option \"Export OTPs to Zip archive\" in den Raivo-Einstellungen.\n\nEntpacken Sie die Zip-Datei und importieren Sie die JSON-Datei.", "importBitwardenGuide": "Verwenden Sie die Option \"Tresor exportieren\" innerhalb der Bitwarden Tools und importieren Sie die unverschlüsselte JSON-Datei.", "importAegisGuide": "Verwenden Sie die Option \"Tresor exportieren\" in den Aegis-Einstellungen.\n\nFalls Ihr Tresor verschlüsselt ist, müssen Sie das Passwort für den Tresor eingeben, um ihn zu entschlüsseln.", @@ -115,22 +113,18 @@ "copied": "Kopiert", "pleaseTryAgain": "Bitte versuchen Sie es erneut", "existingUser": "Bestehender Benutzer", - "newUser": "Neu bei ente", "delete": "Löschen", "enterYourPasswordHint": "Geben Sie Ihr Passwort ein", "forgotPassword": "Passwort vergessen", "oops": "Hopla", "suggestFeatures": "Features vorschlagen", "faq": "FAQ", - "faq_q_1": "Wie sicher ist ente Auth?", - "faq_a_1": "Alle Codes, die Sie über ente sichern, werden Ende-zu-Ende-verschlüsselt gespeichert. Das bedeutet, dass nur Sie auf Ihre Codes zugreifen können. Unsere Apps sind Open Source und unsere Kryptografie wurde extern überprüft.", "faq_q_2": "Kann ich auf meine Codes auf dem Desktop zugreifen?", "faq_a_2": "Sie können auf Ihre Codes im Web via auth.ente.io zugreifen.", "faq_q_3": "Wie kann ich Codes löschen?", "faq_a_3": "Sie können einen Code löschen, indem Sie auf dem Code nach links wischen.", "faq_q_4": "Wie kann ich das Projekt unterstützen?", "faq_a_4": "Sie können die Entwicklung dieses Projekts unterstützen, indem Sie unsere Fotos-App auf ente.io abonnieren.", - "faq_q_5": "Wie kann ich FaceID Sperre in ente Auth aktivieren", "faq_a_5": "Sie können FaceID unter Einstellungen → Sicherheit → Sperrbildschirm aktivieren.", "somethingWentWrongMessage": "Ein Fehler ist aufgetreten, bitte versuchen Sie es erneut", "leaveFamily": "Familie verlassen", @@ -346,7 +340,6 @@ "deleteCodeAuthMessage": "Authentifizieren, um Code zu löschen", "showQRAuthMessage": "Authentifizieren, um QR-Code anzuzeigen", "confirmAccountDeleteTitle": "Kontolöschung bestätigen", - "confirmAccountDeleteMessage": "Dieses Konto ist mit anderen ente Apps verknüpft, sofern du diese benutzt.\n\nDeine hochgeladenen Daten werden zur permanenten Löschung freigegeben. Dies gilt für alle ente Apps.", "androidBiometricHint": "Identität bestätigen", "@androidBiometricHint": { "description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters." diff --git a/auth/lib/l10n/arb/app_es.arb b/auth/lib/l10n/arb/app_es.arb index 0740accb1..41113f0b9 100644 --- a/auth/lib/l10n/arb/app_es.arb +++ b/auth/lib/l10n/arb/app_es.arb @@ -78,14 +78,12 @@ "data": "Datos", "importCodes": "Importar códigos", "importTypePlainText": "Texto sin formato", - "importTypeEnteEncrypted": "Exportación cifrada ente", "passwordForDecryptingExport": "Contraseña para descifrar exportación", "passwordEmptyError": "La contraseña no puede estar vacía", "importFromApp": "Importar códigos de {appName}", "importGoogleAuthGuide": "Exportar tus cuentas desde Google Authenticator a un código QR usando la opción \"Transferir Cuentas\". A continuación, usando otro dispositivo, escanee el código QR.\n\nConsejo: Puede usar la webcam de su portátil para tomar una foto del código QR.", "importSelectJsonFile": "Seleccione el archivo JSON", "importSelectAppExport": "Seleccione el archivo de exportación de {appName}", - "importEnteEncGuide": "Seleccione el archivo JSON cifrado exportado desde ente", "importRaivoGuide": "Utilice la opción \"Exportar códigos a un archivo de Zip\" en la configuración de Raivo.\n\nExtraiga el archivo zip e importe el archivo JSON.", "importBitwardenGuide": "Use la opción \"Exportar caja fuerte\" dentro del menú Herramientas de Bitwarden e importe el fichero JSON no crifrado.", "importAegisGuide": "Utilice la opción \"Exportar la bóveda\" en ajustes de Aegis.\n\nSi tu bóveda es cifrada, necesitara entrar contraseña de bóveda para descifrar la bóveda.", @@ -115,22 +113,18 @@ "copied": "Copiado", "pleaseTryAgain": "Por favor, inténtalo nuevamente", "existingUser": "Usuario existente", - "newUser": "Nuevo en ente", "delete": "Borrar", "enterYourPasswordHint": "Ingrese su contraseña", "forgotPassword": "Olvidé mi contraseña", "oops": "Ups", "suggestFeatures": "Sugerir funcionalidades", "faq": "Preguntas Frecuentes", - "faq_q_1": "¿Cuán seguro es ente Auth?", - "faq_a_1": "Todos los códigos que copia de seguridad vía ente se almacenan cifrados de extremo a extremo. Esto significa que solo usted puede acceder a sus códigos. Nuestras aplicaciones son de código abierto y nuestra criptografía ha sido auditada externamente.", "faq_q_2": "¿Puedo acceder a mis códigos en el escritorio?", "faq_a_2": "Puede acceder a tus códigos en la web en auth.ente.io.", "faq_q_3": "¿Cómo puedo borrar códigos?", "faq_a_3": "Puede eliminar un código deslizando a la izquierda en ese elemento.", "faq_q_4": "¿Cómo puedo apoyar este proyecto?", "faq_a_4": "Puedes apoyar el desarrollo de este proyecto suscribiéndote a nuestra app de Fotos en ente.io.", - "faq_q_5": "¿Cómo puedo habilitar bloqueo FaceID en ente Auth", "faq_a_5": "Puede activar el bloqueo FaceID en Ajustes → Seguridad → Pantalla de bloqueo.", "somethingWentWrongMessage": "Algo ha ido mal, por favor, prueba otra vez", "leaveFamily": "Dejar plan familiar", @@ -344,7 +338,6 @@ "deleteCodeAuthMessage": "Autenticar para borrar código", "showQRAuthMessage": "Autenticar para mostrar código QR", "confirmAccountDeleteTitle": "Confirmar eliminación de la cuenta", - "confirmAccountDeleteMessage": "Esta cuenta está vinculada a otras aplicaciones de ente, si utiliza alguna.\n\nSe programará la eliminación de los datos que cargue en todas las aplicaciones de ente y su cuenta se eliminará permanentemente.", "androidBiometricHint": "Verificar identidad", "@androidBiometricHint": { "description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters." diff --git a/auth/lib/l10n/arb/app_fa.arb b/auth/lib/l10n/arb/app_fa.arb index 90d9e1f81..0cba193a9 100644 --- a/auth/lib/l10n/arb/app_fa.arb +++ b/auth/lib/l10n/arb/app_fa.arb @@ -85,7 +85,6 @@ "copied": "کپی شد", "pleaseTryAgain": "لطفا دوباره تلاش کنید", "existingUser": "کاربر موجود", - "newUser": "کاربر جدید ente", "delete": "حذف", "enterYourPasswordHint": "رمز عبور خود را وارد کنید", "forgotPassword": "فراموشی رمز عبور", diff --git a/auth/lib/l10n/arb/app_fi.arb b/auth/lib/l10n/arb/app_fi.arb index 8dfe22530..72309b331 100644 --- a/auth/lib/l10n/arb/app_fi.arb +++ b/auth/lib/l10n/arb/app_fi.arb @@ -74,7 +74,6 @@ "copied": "Jäljennetty", "pleaseTryAgain": "Yritä uudestaan", "existingUser": "Jo valmiiksi olemassaoleva käyttäjä", - "newUser": "Uusi Ente-käyttäjä", "delete": "Poista", "enterYourPasswordHint": "Syötä salasanasi", "forgotPassword": "Olen unohtanut salasanani", diff --git a/auth/lib/l10n/arb/app_fr.arb b/auth/lib/l10n/arb/app_fr.arb index 6fa997b84..04a7058c7 100644 --- a/auth/lib/l10n/arb/app_fr.arb +++ b/auth/lib/l10n/arb/app_fr.arb @@ -1,5 +1,6 @@ { "account": "Compte", + "unlock": "Déverrouiller", "recoveryKey": "Clé de récupération", "counterAppBarTitle": "Compteur", "@counterAppBarTitle": { @@ -77,15 +78,17 @@ "data": "Données", "importCodes": "Importer les codes", "importTypePlainText": "Texte brut", - "importTypeEnteEncrypted": "ente Exportation chiffrée", "passwordForDecryptingExport": "Mot de passe pour déchiffrer l'exportation", "passwordEmptyError": "Le mot de passe ne peut pas être vide", "importFromApp": "Importer des codes depuis {appName}", "importGoogleAuthGuide": "Exportez vos comptes depuis Google Authenticator vers un code QR en utilisant l'option \"Transférer des comptes\". Ensuite, en utilisant un autre appareil, scannez le code QR.\n\nAstuce : Vous pouvez utiliser la webcam de votre ordinateur portable pour prendre une photo du code QR.", "importSelectJsonFile": "Sélectionnez un fichier JSON", - "importEnteEncGuide": "Sélectionnez le fichier JSON chiffré exporté à partir de ente", + "importSelectAppExport": "Sélectionnez le fichier d'exportation {appName}", "importRaivoGuide": "Utilisez l'option \"Exporter les OTPs vers l'archive Zip\" dans les paramètres de Raivo.\n\nExtrayez le fichier zip et importez le fichier JSON.", + "importBitwardenGuide": "Utilisez l'option « Exporter le coffre » dans les outils Bitwarden et importez le fichier JSON non chiffré.", "importAegisGuide": "Utilisez l'option \"Exporter le coffre-fort\" dans les paramètres d'Aegis.\n\nSi votre coffre-fort est crypté, vous devrez saisir le mot de passe du coffre-fort pour déchiffrer le coffre-fort.", + "import2FasGuide": "Utilisez l'option \"Paramètres->Sauvegarde -Export\" dans 2FAS.\n\nSi votre sauvegarde est chiffrée, vous devrez entrer le mot de passe pour déchiffrer la sauvegarde", + "importLastpassGuide": "Utilisez l'option \"Transférer des comptes\" dans les paramètres de l'authentificateur Lastpass et appuyez sur \"Exporter des comptes vers un fichier\". Importez le JSON téléchargé.", "exportCodes": "Exporter les codes", "importLabel": "Importer", "importInstruction": "Veuillez sélectionner un fichier qui contient une liste de vos codes dans le format suivant", @@ -97,6 +100,8 @@ "authToViewYourRecoveryKey": "Veuillez vous authentifier pour afficher votre clé de récupération", "authToChangeYourEmail": "Veuillez vous authentifier pour modifier votre adresse e-mail", "authToChangeYourPassword": "Veuillez vous authentifier pour modifier votre mot de passe", + "authToViewSecrets": "Veuillez vous authentifier pour voir vos souvenirs", + "authToInitiateSignIn": "Veuillez vous authentifier pour ouvrir une session de sauvegarde.", "ok": "Ok", "cancel": "Annuler", "yes": "Oui", @@ -108,22 +113,18 @@ "copied": "Copié", "pleaseTryAgain": "Veuillez réessayer", "existingUser": "Utilisateur existant", - "newUser": "Nouveau sur ente", "delete": "Supprimer", "enterYourPasswordHint": "Saisir votre mot de passe", "forgotPassword": "Mot de passe oublié", "oops": "Oups", "suggestFeatures": "Suggérer des fonctionnalités", "faq": "FAQ", - "faq_q_1": "À quel point ente Auth est-il sécurisé ?", - "faq_a_1": "Tous les codes que vous sauvegardez via ente sont chiffrés de bout en bout. Cela signifie que vous seul pouvez accéder à vos codes. Nos applications sont open source et notre cryptographie a fait l'objet d'un audit externe.", "faq_q_2": "Puis-je accéder à mes codes sur mon ordinateur ?", "faq_a_2": "Vous pouvez accéder à vos codes sur le web via auth.ente.io.", "faq_q_3": "Comment puis-je supprimer des codes ?", "faq_a_3": "Vous pouvez supprimer un code en glissant vers la gauche.", "faq_q_4": "Comment puis-je soutenir le projet ?", "faq_a_4": "Vous pouvez soutenir le développement de ce projet en vous abonnant à notre application Photos, ente.io.", - "faq_q_5": "Comment puis-je activer le verrouillage FaceID sur ente Auth", "faq_a_5": "Vous pouvez activer le verrouillage FaceID dans Paramètres → Sécurité → Écran de verrouillage.", "somethingWentWrongMessage": "Quelque chose s'est mal passé, veuillez recommencer", "leaveFamily": "Quitter le plan familial", @@ -137,6 +138,8 @@ "enterCodeHint": "Saisir le code à 6 caractères de votre appli d'authentification", "lostDeviceTitle": "Appareil perdu ?", "twoFactorAuthTitle": "Authentification à deux facteurs", + "passkeyAuthTitle": "Vérification du code d'accès", + "verifyPasskey": "Vérifier le code d'accès", "recoverAccount": "Récupérer un compte", "enterRecoveryKeyHint": "Saisissez votre clé de récupération", "recover": "Restaurer", @@ -190,6 +193,10 @@ "recoveryKeySaveDescription": "Nous ne stockons pas cette clé, veuillez enregistrer cette clé de 24 mots dans un endroit sûr.", "doThisLater": "Plus tard", "saveKey": "Enregistrer la clé", + "save": "Sauvegarder", + "send": "Envoyer", + "saveOrSendDescription": "Voulez-vous enregistrer ceci sur votre stockage (dossier Téléchargements par défaut) ou l'envoyer à d'autres applications ?", + "saveOnlyDescription": "Voulez-vous enregistrer ceci sur votre stockage (dossier Téléchargements par défaut) ?", "back": "Retour", "createAccount": "Créer un compte", "passwordStrength": "Force du mot de passe : {passwordStrengthValue}", @@ -329,6 +336,7 @@ "offlineModeWarning": "Vous avez choisi de procéder sans sauvegarde. Veuillez prendre des sauvegardes manuelles pour vous assurer que vos codes sont sûrs.", "showLargeIcons": "Afficher les grandes icônes", "shouldHideCode": "Cacher les codes", + "doubleTapToViewHiddenCode": "Vous pouvez appuyer deux fois sur une entrée pour afficher le code", "focusOnSearchBar": "Cibler le champ de recherche au démarrage de l'application", "confirmUpdatingkey": "Êtes-vous sûr de vouloir mettre à jour la clé secrète ?", "minimizeAppOnCopy": "Réduire l'application après la copie", @@ -336,5 +344,75 @@ "deleteCodeAuthMessage": "Authentification requise pour supprimer le code", "showQRAuthMessage": "Authentification requise pour afficher le code QR", "confirmAccountDeleteTitle": "Confirmer la suppression du compte", - "confirmAccountDeleteMessage": "Ce compte peut être lié à d'autres applications ente.\n\nVos données seront bientôt effacées de toutes les applications et votre compte sera définitivement supprimé." + "androidBiometricHint": "Vérifier l’identité", + "@androidBiometricHint": { + "description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters." + }, + "androidBiometricNotRecognized": "Non reconnu. Veuillez réessayer.", + "@androidBiometricNotRecognized": { + "description": "Message to let the user know that authentication was failed. It is used on Android side. Maximum 60 characters." + }, + "androidBiometricSuccess": "Parfait", + "@androidBiometricSuccess": { + "description": "Message to let the user know that authentication was successful. It is used on Android side. Maximum 60 characters." + }, + "androidCancelButton": "Annuler", + "@androidCancelButton": { + "description": "Message showed on a button that the user can click to leave the current dialog. It is used on Android side. Maximum 30 characters." + }, + "androidSignInTitle": "Authentification requise", + "@androidSignInTitle": { + "description": "Message showed as a title in a dialog which indicates the user that they need to scan biometric to continue. It is used on Android side. Maximum 60 characters." + }, + "androidBiometricRequiredTitle": "Empreinte digitale requise", + "@androidBiometricRequiredTitle": { + "description": "Message showed as a title in a dialog which indicates the user has not set up biometric authentication on their device. It is used on Android side. Maximum 60 characters." + }, + "androidDeviceCredentialsRequiredTitle": "Identifiants de l'appareil requis", + "@androidDeviceCredentialsRequiredTitle": { + "description": "Message showed as a title in a dialog which indicates the user has not set up credentials authentication on their device. It is used on Android side. Maximum 60 characters." + }, + "androidDeviceCredentialsSetupDescription": "Identifiants de l'appareil requis", + "@androidDeviceCredentialsSetupDescription": { + "description": "Message advising the user to go to the settings and configure device credentials on their device. It shows in a dialog on Android side." + }, + "goToSettings": "Allez dans les paramètres", + "@goToSettings": { + "description": "Message showed on a button that the user can click to go to settings pages from the current dialog. It is used on both Android and iOS side. Maximum 30 characters." + }, + "androidGoToSettingsDescription": "L'authentification biométrique n'est pas configurée sur votre appareil. Allez dans 'Paramètres > Sécurité' pour l'ajouter.", + "@androidGoToSettingsDescription": { + "description": "Message advising the user to go to the settings and configure biometric on their device. It shows in a dialog on Android side." + }, + "iOSLockOut": "L'authentification biométrique est désactivée. Veuillez verrouiller et déverrouiller votre écran pour l'activer.", + "@iOSLockOut": { + "description": "Message advising the user to re-enable biometrics on their device. It shows in a dialog on iOS side." + }, + "iOSGoToSettingsDescription": "L'authentification biométrique n'est pas configurée sur votre appareil. Veuillez activer Touch ID ou Face ID sur votre téléphone.", + "@iOSGoToSettingsDescription": { + "description": "Message advising the user to go to the settings and configure Biometrics for their device. It shows in a dialog on iOS side." + }, + "iOSOkButton": "Ok", + "@iOSOkButton": { + "description": "Message showed on a button that the user can click to leave the current dialog. It is used on iOS side. Maximum 30 characters." + }, + "noInternetConnection": "Aucune connexion internet", + "pleaseCheckYourInternetConnectionAndTryAgain": "Veuillez vérifier votre connexion internet puis réessayer.", + "signOutFromOtherDevices": "Déconnexion des autres appareils", + "signOutOtherBody": "Si vous pensez que quelqu'un connaît peut-être votre mot de passe, vous pouvez forcer tous les autres appareils utilisant votre compte à se déconnecter.", + "signOutOtherDevices": "Déconnecter les autres appareils", + "doNotSignOut": "Ne pas se déconnecter", + "hearUsWhereTitle": "Comment avez-vous entendu parler de Ente? (facultatif)", + "hearUsExplanation": "Nous ne suivons pas les installations d'applications. Il serait utile que vous nous disiez comment vous nous avez trouvés !", + "recoveryKeySaved": "Clé de récupération enregistrée dans le dossier Téléchargements !", + "waitingForBrowserRequest": "En attente de la requête du navigateur...", + "waitingForVerification": "En attente de vérification...", + "passkey": "Code d'accès", + "developerSettingsWarning": "Êtes-vous sûr de vouloir modifier les paramètres du développeur ?", + "developerSettings": "Paramètres du développeur", + "serverEndpoint": "Point de terminaison serveur", + "invalidEndpoint": "Point de terminaison non valide", + "invalidEndpointMessage": "Désolé, le point de terminaison que vous avez entré n'est pas valide. Veuillez en entrer un valide puis réessayez.", + "endpointUpdatedMessage": "Point de terminaison mis à jour avec succès", + "customEndpoint": "Connecté à {endpoint}" } \ No newline at end of file diff --git a/auth/lib/l10n/arb/app_he.arb b/auth/lib/l10n/arb/app_he.arb index c3703d1d0..330585097 100644 --- a/auth/lib/l10n/arb/app_he.arb +++ b/auth/lib/l10n/arb/app_he.arb @@ -77,13 +77,11 @@ "data": "נתונים", "importCodes": "ייבא קוד", "importTypePlainText": "טקסט רגיל", - "importTypeEnteEncrypted": "ייצוא ente מוצפן", "passwordForDecryptingExport": "סיסמא כדי לפענח יצוא", "passwordEmptyError": "הסיסמה אינה יכולה להיות ריקה", "importFromApp": "יבא קודים מ-{appName}", "importGoogleAuthGuide": "יצא את החשבונות שלך מ-Google Authenticator לקוד QR תוך שימוש באפשרות \"Transfer Accounts\". אז, תוך שימוש במכשיר אחר, סרוק את הקוד QR.", "importSelectJsonFile": "בחר קובץ JSON", - "importEnteEncGuide": "בחר את קובץ ה-JSON המוצפן שייוצא מ-ente", "importRaivoGuide": "השתמש באפוציה של \"Export OTPs to Zip archive\" בהגדרות של Raivo.", "importAegisGuide": "אנא השתמש באפשרות של \"ייצוא של הכספת\" בתוך ההגדרות של Aegis.", "exportCodes": "ייצא קודים", @@ -108,22 +106,18 @@ "copied": "הועתק", "pleaseTryAgain": "אנא נסה שנית", "existingUser": "משתמש קיים", - "newUser": "חדש בente", "delete": "למחוק", "enterYourPasswordHint": "הכנס סיסמא", "forgotPassword": "שכחתי סיסמה", "oops": "אופס", "suggestFeatures": "הציעו מאפיינים", "faq": "שאלות נפוצות", - "faq_q_1": "כמה מאובטח ente Auth?", - "faq_a_1": "כל הקודים שאתה מגבה דרך ente מאוחסנים מקצה לקצה בהצפנה. הכוונה שרק אתה יכול לגשת לקודים שלך. האפליקציות שלנו הם מפותחות דרך קוד פתוח והקריפטוגרפיה שלנו מבוקרת חיצונית.", "faq_q_2": "האם ישנה אפשרות להשתמש בקודים שלי במחשב?", "faq_a_2": "אתה יכול לגשת לקודים שלך ברשת ב- auth.ente.io.", "faq_q_3": "איך אפשר למחוק קודים?", "faq_a_3": "אתה יכול למחוק את הקוד על-ידי החלקה שמאלה על הפריט הזה.", "faq_q_4": "איך אפשר לתמוך בפרויקט זה?", "faq_a_4": "אתה יכול לתמוך בפיתוח של הפרויקט הזה על ידי שתירשם לאפליקצית תמונות שלנו ב-ente.io.", - "faq_q_5": "איך אני יכול להפעיל מנעול FaceID ב-ente Auth", "faq_a_5": "אתה יכול להפעיל מנעול FaceID תחת הגדרות -> אבטחה -> מסך נעילה.", "somethingWentWrongMessage": "משהו השתבש, אנא נסה שנית", "leaveFamily": "עזוב משפחה", diff --git a/auth/lib/l10n/arb/app_it.arb b/auth/lib/l10n/arb/app_it.arb index 1cec02cd2..473b3a2b3 100644 --- a/auth/lib/l10n/arb/app_it.arb +++ b/auth/lib/l10n/arb/app_it.arb @@ -78,14 +78,12 @@ "data": "Dati", "importCodes": "Importa codici", "importTypePlainText": "Testo in chiaro", - "importTypeEnteEncrypted": "ente Esportazione criptata", "passwordForDecryptingExport": "Password per decriptare il file esportato", "passwordEmptyError": "La password è obbligatoria", "importFromApp": "Importa codici da {appName}", "importGoogleAuthGuide": "Esporta i tuoi account da Google Authenticator in un codice QR utilizzando l'opzione \"Trasferisci Account\". Quindi, usando un altro dispositivo, scansiona il codice QR.\n\nSuggerimento: Puoi usare la webcam del tuo computer portatile per scattare una foto del codice QR.", "importSelectJsonFile": "Seleziona file JSON", "importSelectAppExport": "Seleziona il file di esportazione {appName}", - "importEnteEncGuide": "Seleziona il file JSON criptato esportato da ente", "importRaivoGuide": "Utilizza l'opzione \"Esporta i codici OTP in archivio Zip\" nelle impostazioni di Raivo.\n\nEstrai il file zip e importa il file JSON.", "importBitwardenGuide": "Utilizzare l'opzione \"Esporta vault\" all'interno di Bitwarden Tools e importa il file JSON non crittografato.", "importAegisGuide": "Usa l'opzione \"Esporta la cassaforte\" nelle impostazioni di Aegis.\n\nSe la tua cassaforte è criptata, dovrai inserire la password della cassaforte per decriptarla.", @@ -114,22 +112,18 @@ "copied": "Copiato", "pleaseTryAgain": "Per favore riprova", "existingUser": "Accedi", - "newUser": "Nuovo utente", "delete": "Cancella", "enterYourPasswordHint": "Inserisci la tua password", "forgotPassword": "Password dimenticata", "oops": "Oops", "suggestFeatures": "Suggerisci funzionalità", "faq": "FAQ", - "faq_q_1": "Quanto è sicuro ente Auth?", - "faq_a_1": "Tutti i codici di cui fai il backup tramite ente sono memorizzati con crittografia end-to-end. Ciò significa che solo tu puoi accedere ai tuoi codici. Le nostre app sono open source e la nostra crittografia è stata verificata esternamente.", "faq_q_2": "Posso accedere ai miei codici sul desktop?", "faq_a_2": "Puoi accedere ai tuoi codici sul web @ auth.ente.io.", "faq_q_3": "Come posso cancellare i codici?", "faq_a_3": "Puoi eliminare un codice scorrendo il dito a sinistra sul codice in questione.", "faq_q_4": "Come posso supportare questo progetto?", "faq_a_4": "Puoi supportare lo sviluppo di questo progetto abbonandoti alla nostra app Photos @ ente.io.", - "faq_q_5": "Come posso abilitare il blocco FaceID in ente Auth", "faq_a_5": "Puoi abilitare il blocco FaceID in Impostazioni → Sicurezza → Schermata di blocco.", "somethingWentWrongMessage": "Qualcosa è andato storto, per favore riprova", "leaveFamily": "Abbandona il piano famiglia", @@ -343,7 +337,6 @@ "deleteCodeAuthMessage": "Autenticarsi per cancellare il codice", "showQRAuthMessage": "Autenticarsi per mostrare il codice QR", "confirmAccountDeleteTitle": "Conferma l'eliminazione dell'account", - "confirmAccountDeleteMessage": "Questo account è collegato ad altre app di ente, se ne utilizzi.\n\nI tuoi dati caricati, su tutte le app di ente, saranno pianificati per la cancellazione e il tuo account verrà eliminato definitivamente.", "androidBiometricHint": "Verifica l'identità", "@androidBiometricHint": { "description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters." diff --git a/auth/lib/l10n/arb/app_ja.arb b/auth/lib/l10n/arb/app_ja.arb index 5b281747f..60d0a5150 100644 --- a/auth/lib/l10n/arb/app_ja.arb +++ b/auth/lib/l10n/arb/app_ja.arb @@ -78,14 +78,13 @@ "data": "データ", "importCodes": "コードをインポート", "importTypePlainText": "プレーンテキスト", - "importTypeEnteEncrypted": "ente 暗号化エクスポート", "passwordForDecryptingExport": "復号化用パスワード", "passwordEmptyError": "パスワードは空欄にできません", "importFromApp": "{appName} からコードをインポート", "importGoogleAuthGuide": "Google Authenticator の \"アカウントを転送\" オプションを使用してあなたのアカウントを QR コードにエクスポートしてください。その後、他のデバイスで QR コードを読み取ってください。\n\nヒント: ノートパソコンのウェブカメラを使用して QR コードを撮影することができます。", "importSelectJsonFile": "JSON ファイルを選択", "importSelectAppExport": "{appName} のエクスポートファイルを選択", - "importEnteEncGuide": "ente からエクスポートされた暗号化 JSON ファイルを選択", + "importEnteEncGuide": "Enteからエクスポートした暗号化されたJSONファイルを選択してください", "importRaivoGuide": "Raivo の設定の \"OTP を zip アーカイブにエクスポート\" を使用してください。\n\nzip ファイルを解凍し JSON ファイルをインポートしてください。", "importBitwardenGuide": "Bitwarden Tools の \"保管庫のエクスポート\" オプションを使用後、平文の JSON ファイルをインポートしてください。", "importAegisGuide": "Aegis の設定の \"保管庫をエクスポート\" を使用してください。\n\n保管庫が暗号化されている場合、保管庫を復号するためにパスワードの入力が必要になります。", @@ -115,22 +114,21 @@ "copied": "コピーしました", "pleaseTryAgain": "再度お試しください", "existingUser": "既存のユーザー", - "newUser": "ente 新規ユーザー", + "newUser": "Enteを初めて使用", "delete": "削除", "enterYourPasswordHint": "パスワードを入力してください", "forgotPassword": "パスワードを忘れた場合", "oops": "おっと", "suggestFeatures": "機能を提案", "faq": "FAQ", - "faq_q_1": "ente Auth はどのくらい安全ですか?", - "faq_a_1": "ente でバックアップされたすべてのコードはエンドツーエンドで暗号化されて保管されます。これはあなただけがコードにアクセスできることを意味します。私たちのアプリはオープンソースであり、私たちの暗号は外部有識者によって検証済みです。", + "faq_q_1": "Authはどのくらい安全ですか?", "faq_q_2": "パソコンから私のコードにアクセスできますか?", "faq_a_2": "auth.ente.io で Web からコードにアクセス可能です。", "faq_q_3": "コードを削除するにはどうすればいいですか?", "faq_a_3": "その項目を左にスワイプすることでコードを削除できます。", "faq_q_4": "このプロジェクトを支援するにはどうすればいいですか?", "faq_a_4": "ente.io で私たちの写真アプリを購読することでこのプロジェクトの開発を支援できます。", - "faq_q_5": "ente Auth で FaceID ロックを有効にするにはどうすればいいですか?", + "faq_q_5": "AuthでFaceIDロックを有効にするにはどうすればいいですか?", "faq_a_5": "設定→セキュリティ→画面のロックから FaceID ロックを有効にできます。", "somethingWentWrongMessage": "問題が発生しました、再試行してください", "leaveFamily": "ファミリープランから退会", @@ -199,6 +197,10 @@ "recoveryKeySaveDescription": "私たちはこのキーを保存しません。この 24 単語のキーを安全な場所に保存してください。", "doThisLater": "後で行う", "saveKey": "キーを保存", + "save": "保存", + "send": "送信", + "saveOrSendDescription": "これをストレージ (デフォルトではダウンロードフォルダ) に保存しますか、もしくは他のアプリに送信しますか?", + "saveOnlyDescription": "これをストレージに保存しますか? (デフォルトではダウンロードフォルダに保存)", "back": "戻る", "createAccount": "アカウント作成", "passwordStrength": "パスワードの強度: {passwordStrengthValue}", @@ -346,7 +348,6 @@ "deleteCodeAuthMessage": "コードを削除するためには認証が必要です", "showQRAuthMessage": "QR コードを表示するためには認証が必要です", "confirmAccountDeleteTitle": "アカウントの削除に同意", - "confirmAccountDeleteMessage": "このアカウントは他の ente アプリも使用している場合はそれらに結びつけられています。\n\nすべての ente アプリであなたがアップロードしたデータは削除がスケジュールされ、あなたのアカウントは永久に削除されます。", "androidBiometricHint": "本人を確認する", "@androidBiometricHint": { "description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters." @@ -407,6 +408,7 @@ "doNotSignOut": "サインアウトしない", "hearUsWhereTitle": "Ente についてどのようにお聞きになりましたか?(任意)", "hearUsExplanation": "私たちはアプリのインストールを追跡していません。私たちをお知りになった場所を教えてください!", + "recoveryKeySaved": "リカバリキーをダウンロードフォルダに保存しました!", "waitingForBrowserRequest": "ブラウザのリクエストを待っています...", "waitingForVerification": "認証を待っています...", "passkey": "パスキー", diff --git a/auth/lib/l10n/arb/app_ka.arb b/auth/lib/l10n/arb/app_ka.arb index d7be292dd..cb7dc8281 100644 --- a/auth/lib/l10n/arb/app_ka.arb +++ b/auth/lib/l10n/arb/app_ka.arb @@ -74,7 +74,6 @@ "data": "მონაცემები", "importCodes": "კოდების იმპორტირება", "importTypePlainText": "სტანდარტული ტექსტი", - "importTypeEnteEncrypted": "ente დაშიფრული ექსპორტი", "passwordForDecryptingExport": "ექსპორტის გაშიფრვის პაროლი", "passwordEmptyError": "პაროლის ველი არ შეიძლება იყოს ცარიელი", "emailVerificationToggle": "ელექტრონული ფოსტის ვერიფიკაცია", diff --git a/auth/lib/l10n/arb/app_nl.arb b/auth/lib/l10n/arb/app_nl.arb index ad1728204..2e84ae11b 100644 --- a/auth/lib/l10n/arb/app_nl.arb +++ b/auth/lib/l10n/arb/app_nl.arb @@ -78,7 +78,7 @@ "data": "Gegevens", "importCodes": "Codes importeren", "importTypePlainText": "Kale tekst", - "importTypeEnteEncrypted": "ente versleutelde export", + "importTypeEnteEncrypted": "Ente versleutelde export", "passwordForDecryptingExport": "Wachtwoord voor het decoderen van export", "passwordEmptyError": "Wachtwoord kan niet leeg zijn", "importFromApp": "Importeer codes van {appName}", @@ -115,15 +115,15 @@ "copied": "Gekopieerd", "pleaseTryAgain": "Probeer het nog eens", "existingUser": "Bestaande gebruiker", - "newUser": "Nieuw bij ente", + "newUser": "Nieuw bij Ente", "delete": "Verwijderen", "enterYourPasswordHint": "Voer je wachtwoord in", "forgotPassword": "Wachtwoord vergeten", "oops": "Oeps", "suggestFeatures": "Features voorstellen", "faq": "Veelgestelde vragen", - "faq_q_1": "Hoe veilig is ente Auth?", - "faq_a_1": "Alle codes in ente zijn versleuteld opgeslagen met end-to-end encryptie. Dit betekent dat alleen jij toegang hebt tot je codes. Onze apps zijn open source en onze cryptografie is extern gecontroleerd.", + "faq_q_1": "Hoe veilig is Ente Auth?", + "faq_a_1": "Alle codes in Auth zijn versleuteld opgeslagen met end-to-end encryptie. Dit betekent dat alleen jij toegang hebt tot je codes. Onze apps zijn open source en onze cryptografie is extern gecontroleerd.", "faq_q_2": "Kan ik toegang krijgen tot mijn codes op desktop?", "faq_a_2": "U heeft toegang tot uw codes op het web @ auth.ente.io.", "faq_q_3": "Hoe kan ik codes verwijderen?", @@ -199,6 +199,10 @@ "recoveryKeySaveDescription": "We slaan deze code niet op, bewaar deze code met 24 woorden op een veilige plaats.", "doThisLater": "Doe dit later", "saveKey": "Sleutel opslaan", + "save": "Opslaan", + "send": "Verzenden", + "saveOrSendDescription": "Wil je dit opslaan naar je opslagruimte (Downloads map) of naar andere apps versturen?", + "saveOnlyDescription": "Wil je dit opslaan naar je opslagruimte (Downloads map)?", "back": "Terug", "createAccount": "Account aanmaken", "passwordStrength": "Wachtwoord sterkte: {passwordStrengthValue}", @@ -346,7 +350,7 @@ "deleteCodeAuthMessage": "Authenticeren om code te verwijderen", "showQRAuthMessage": "Authenticeren om QR-code te tonen", "confirmAccountDeleteTitle": "Account verwijderen bevestigen", - "confirmAccountDeleteMessage": "Dit account is gekoppeld aan andere ente apps, als je er gebruik van maakt.\n\nJe geüploade gegevens worden in alle ente apps gepland voor verwijdering, en je account wordt permanent verwijderd voor alle ente diensten.", + "confirmAccountDeleteMessage": "Dit account is gekoppeld aan andere Ente apps, als je er gebruik van maakt.\n\nJe geüploade gegevens worden in alle Ente apps gepland voor verwijdering, en je account wordt permanent verwijderd voor alle Ente diensten.", "androidBiometricHint": "Identiteit verifiëren", "@androidBiometricHint": { "description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters." @@ -407,6 +411,7 @@ "doNotSignOut": "Niet uitloggen", "hearUsWhereTitle": "Hoe hoorde je over Ente? (optioneel)", "hearUsExplanation": "Wij gebruiken geen tracking. Het zou helpen als je ons vertelt waar je ons gevonden hebt!", + "recoveryKeySaved": "Herstelsleutel opgeslagen in de Downloads map!", "waitingForBrowserRequest": "Wachten op browserverzoek...", "waitingForVerification": "Wachten op verificatie...", "passkey": "Passkey", diff --git a/auth/lib/l10n/arb/app_pl.arb b/auth/lib/l10n/arb/app_pl.arb index 3221799d0..8ebc935dc 100644 --- a/auth/lib/l10n/arb/app_pl.arb +++ b/auth/lib/l10n/arb/app_pl.arb @@ -78,13 +78,11 @@ "data": "Dane", "importCodes": "Importuj kody", "importTypePlainText": "Zwykły tekst", - "importTypeEnteEncrypted": "Zaszyfrowany eksport ente", "passwordForDecryptingExport": "Hasło do odszyfrowania eksportu", "passwordEmptyError": "Pole hasło nie może być puste", "importFromApp": "Importuj kody z {appName}", "importGoogleAuthGuide": "Wyeksportuj twoje konta z Google Authenticator do kodu QR używając opcji \"Przenieś konta\". Potem używając innego urządzenia, zeskanuj kod QR.", "importSelectJsonFile": "Wybierz plik JSON", - "importEnteEncGuide": "Wybierz zaszyfrowany plik JSON wyeksportowany z ente", "importRaivoGuide": "Użyj opcji \"Eksportuj OTP do archiwum ZIP\" w Ustawieniach Raivo.\n\nWyodrębnij plik zip i zaimportuj plik JSON.", "importAegisGuide": "Użyj opcji \"Eksportuj sejf\" w ustawieniach Aegis.\n\nJeśli twój sejf jest zaszyfrowany, musisz wprowadzić hasło sejfu, aby odszyfrować sejf.", "exportCodes": "Eksportuj kody", @@ -110,22 +108,18 @@ "copied": "Skopiowano", "pleaseTryAgain": "Proszę spróbować ponownie", "existingUser": "Istniejący użytkownik", - "newUser": "Nowy do ente", "delete": "Usuń", "enterYourPasswordHint": "Wprowadź swoje hasło", "forgotPassword": "Nie pamiętam hasła", "oops": "Ups", "suggestFeatures": "Zaproponuj funkcje", "faq": "Najczęściej zadawane pytania (FAQ)", - "faq_q_1": "Jak bezpieczny jest ente Auth?", - "faq_a_1": "Wszystkie kody, których tworzysz kopię zapasową za pomocą ente są przechowywane zaszyfrowane end-to-end. Oznacza to, że tylko Ty możesz uzyskać dostęp do swoich kodów. Nasze aplikacje są otwarto-źródłowe, a nasza kryptografia została poddana sprawdzeniu z zewnątrz.", "faq_q_2": "Czy mogę uzyskać dostęp do moich kodów na komputerze?", "faq_a_2": "Możesz uzyskać dostęp do swoich kodów na stronie auth.ente.io.", "faq_q_3": "Jak mogę usunąć kody?", "faq_a_3": "Możesz usunąć kod, przesuwając go w lewo.", "faq_q_4": "Jak mogę wesprzeć ten projekt?", "faq_a_4": "Możesz wspierać rozwój tego projektu, subskrybując do naszej aplikacji Zdjęcia na ente.io.", - "faq_q_5": "Jak mogę włączyć blokadę FaceID w ente Auth?", "faq_a_5": "Możesz włączyć blokadę FaceID w Ustawienia → Bezpieczeństwo→ Ekran blokady.", "somethingWentWrongMessage": "Coś poszło nie tak. Proszę, spróbuj ponownie", "leaveFamily": "Opuść rodzinę", @@ -338,7 +332,6 @@ "deleteCodeAuthMessage": "Uwierzytelnij, aby usunąć kod", "showQRAuthMessage": "Uwierzytelnij, aby pokazać kod QR", "confirmAccountDeleteTitle": "Potwierdź usunięcie konta", - "confirmAccountDeleteMessage": "To konto jest połączone z innymi aplikacjami ente, jeśli ich używasz.\n\nTwoje przesłane dane, we wszystkich aplikacjach ente, zostaną zaplanowane do usunięcia, a Twoje konto zostanie trwale usunięte.", "androidBiometricNotRecognized": "Nie rozpoznano. Spróbuj ponownie.", "@androidBiometricNotRecognized": { "description": "Message to let the user know that authentication was failed. It is used on Android side. Maximum 60 characters." diff --git a/auth/lib/l10n/arb/app_pt.arb b/auth/lib/l10n/arb/app_pt.arb index e6ca32893..06e2c0bb4 100644 --- a/auth/lib/l10n/arb/app_pt.arb +++ b/auth/lib/l10n/arb/app_pt.arb @@ -78,14 +78,12 @@ "data": "Dados", "importCodes": "Importar códigos", "importTypePlainText": "Texto simples", - "importTypeEnteEncrypted": "ente Exportação criptografada", "passwordForDecryptingExport": "Senha para descriptografar a exportação", "passwordEmptyError": "O campo senha não pode estar vazio", "importFromApp": "Importar códigos do {appName}", "importGoogleAuthGuide": "Exporte suas contas do Google Authenticator para um QR code usando a opção \"Transferir contas\". Então, usando outro dispositivo, escaneie o QR code.\n\nDica: Você pode usar a câmera do seu notebook para fotografar o QR code.", "importSelectJsonFile": "Selecione o arquivo JSON", "importSelectAppExport": "Selecione o arquivo de exportação do aplicativo {appName}", - "importEnteEncGuide": "Selecione o arquivo JSON criptografado exportado pelo ente", "importRaivoGuide": "Use a opção \"Exportar OTPs para arquivo Zip\" nas configurações do Raivo.\n\nExtraia o arquivo zip e importe o arquivo JSON.", "importBitwardenGuide": "Use a opção \"Exportar cofre\" nas configurações do Bitwarden e importe o arquivo JSON não criptografado.", "importAegisGuide": "Use a opção \"Exportar cofre\" nas Configurações do Aegis.\n\nSe o seu cofre estiver criptografado, você precisará inserir a senha do cofre para descriptografá-lo.", @@ -115,22 +113,18 @@ "copied": "Copiado", "pleaseTryAgain": "Por favor, tente novamente", "existingUser": "Usuário Existente", - "newUser": "Novo no ente", "delete": "Excluir", "enterYourPasswordHint": "Insira sua senha", "forgotPassword": "Esqueci a senha", "oops": "Oops", "suggestFeatures": "Sugerir funcionalidades", "faq": "Perguntas frequentes", - "faq_q_1": "Quão seguro é o ente Auth?", - "faq_a_1": "Todos os códigos que você faz cópia de segurança via ente são armazenados criptografados de ponta a ponta. Isso significa que somente você pode acessar seus códigos. Nossos aplicativos são de código aberto e nossa criptografia foi auditada externamente.", "faq_q_2": "Eu posso acessar meus códigos no computador?", "faq_a_2": "Você pode acessar seus códigos na web em auth.ente.io.", "faq_q_3": "Como faço para excluir códigos?", "faq_a_3": "Você pode excluir um código deslizando para a esquerda sobre esse item.", "faq_q_4": "Como posso apoiar este projeto?", "faq_a_4": "Você pode apoiar o desenvolvimento deste projeto assinando nosso aplicativo de Fotos em ente.io.", - "faq_q_5": "Como posso ativar o bloqueio facial no ente Auth", "faq_a_5": "Você pode ativar o bloqueio facial em Configurações → Segurança → Tela de bloqueio.", "somethingWentWrongMessage": "Algo deu errado. Por favor, tente outra vez", "leaveFamily": "Sair da família", @@ -199,6 +193,10 @@ "recoveryKeySaveDescription": "Não armazenamos essa chave, por favor, salve essa chave de 24 palavras em um lugar seguro.", "doThisLater": "Fazer isso mais tarde", "saveKey": "Salvar chave", + "save": "Salvar", + "send": "Enviar", + "saveOrSendDescription": "Você deseja salvar isso no seu armazenamento (pasta de downloads por padrão) ou enviá-lo para outros aplicativos?", + "saveOnlyDescription": "Você deseja salvar isto no seu armazenamento (pasta de downloads por padrão)?", "back": "Voltar", "createAccount": "Criar uma conta", "passwordStrength": "Força da senha: {passwordStrengthValue}", @@ -346,7 +344,6 @@ "deleteCodeAuthMessage": "Autenticar para excluir o código", "showQRAuthMessage": "Autenticar para mostrar o QR code", "confirmAccountDeleteTitle": "Confirmar exclusão de conta", - "confirmAccountDeleteMessage": "Esta conta está vinculada a outros aplicativos ente, se você usa algum.\n\nSeus dados enviados, em todos os aplicativos ente, serão agendados para exclusão, e sua conta será excluída permanentemente.", "androidBiometricHint": "Verificar identidade", "@androidBiometricHint": { "description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters." @@ -407,6 +404,7 @@ "doNotSignOut": "Não encerrar sessão", "hearUsWhereTitle": "Como você ouviu sobre o Ente? (opcional)", "hearUsExplanation": "Não rastreamos instalações do aplicativo. Seria útil se você nos contasse onde nos encontrou!", + "recoveryKeySaved": "Chave de recuperação salva na pasta Downloads!", "waitingForBrowserRequest": "Aguardando solicitação do navegador...", "waitingForVerification": "Esperando por verificação...", "passkey": "Chave de acesso", diff --git a/auth/lib/l10n/arb/app_ru.arb b/auth/lib/l10n/arb/app_ru.arb index afbfb297d..7ae37a87b 100644 --- a/auth/lib/l10n/arb/app_ru.arb +++ b/auth/lib/l10n/arb/app_ru.arb @@ -78,14 +78,12 @@ "data": "Данные", "importCodes": "Импортировать коды", "importTypePlainText": "Обычный текст", - "importTypeEnteEncrypted": "ente Зашифрованный экспорт", "passwordForDecryptingExport": "Пароль для расшифровки экспорта", "passwordEmptyError": "Пароль не может быть пустым", "importFromApp": "Импорт кодов из {appName}", "importGoogleAuthGuide": "Экспортируйте учетные записи из Google Authenticator в QR-код, используя опцию «Перенести учетные записи». Затем с помощью другого устройства отсканируйте QR-код.\n\nСовет: Чтобы сфотографировать QR-код, можно воспользоваться веб-камерой ноутбука.", "importSelectJsonFile": "Выбрать JSON-файл", "importSelectAppExport": "Выбрать файл экспорта {appName}", - "importEnteEncGuide": "Выберите зашифрованный JSON-файл, экспортированный из ente", "importRaivoGuide": "Используйте опцию «Export OTPs to Zip archive» в настройках Raivo.\n\nРаспакуйте zip-архив и импортируйте JSON-файл.", "importBitwardenGuide": "Используйте опцию \"Экспортировать хранилище\" в Bitwarden Tools и импортируйте незашифрованный JSON файл.", "importAegisGuide": "Используйте опцию «Экспортировать хранилище» в настройках Aegis.\n\nЕсли ваше хранилище зашифровано, то для его расшифровки потребуется ввести пароль хранилища.", @@ -113,22 +111,18 @@ "copied": "Скопировано", "pleaseTryAgain": "Пожалуйста, попробуйте ещё раз", "existingUser": "Существующий пользователь", - "newUser": "Новый аккаунт", "delete": "Удалить", "enterYourPasswordHint": "Введите пароль", "forgotPassword": "Забыл пароль", "oops": "Ой", "suggestFeatures": "Предложить идеи", "faq": "FAQ", - "faq_q_1": "Насколько безопасен ente Auth?", - "faq_a_1": "Все коды, которые вы резервируете с помощью ente, хранятся в зашифрованном виде. Это означает, что только вы можете получить доступ к своим кодам. Наши приложения имеют открытый исходный код, а наша криптография прошла внешний аудит.", "faq_q_2": "Могу ли я получить доступ к моим кодам на компьютере?", "faq_a_2": "Вы можете получить доступ к своим кодам на сайте @ auth.ente.io.", "faq_q_3": "Как я могу удалить коды?", "faq_a_3": "Вы можете удалить код, проведя пальцем влево по этому элементу.", "faq_q_4": "Как я могу поддержать этот проект?", "faq_a_4": "Вы можете поддержать развитие этого проекта, подписавшись на наше приложение Photos @ ente.io.", - "faq_q_5": "Как мне включить блокировку FaceID в ente Auth?", "faq_a_5": "Вы можете включить блокировку FaceID в Настройки → Безопасность → Экран блокировки.", "somethingWentWrongMessage": "Что-то пошло не так. Попробуйте еще раз", "leaveFamily": "Покинуть семью", @@ -342,7 +336,6 @@ "deleteCodeAuthMessage": "Аутентификация для удаления кода", "showQRAuthMessage": "Аутентификация для отображения QR-кода", "confirmAccountDeleteTitle": "Подтвердить удаление аккаунта", - "confirmAccountDeleteMessage": "Эта учетная запись связана с другими приложениями ente, если вы ими пользуетесь.\n\nЗагруженные вами данные во всех приложениях ente будут запланированы к удалению, а ваша учетная запись будет удалена без возможности восстановления.", "androidBiometricHint": "Подтвердите личность", "@androidBiometricHint": { "description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters." diff --git a/auth/lib/l10n/arb/app_sv.arb b/auth/lib/l10n/arb/app_sv.arb index d99ed5f5f..d1dc39e05 100644 --- a/auth/lib/l10n/arb/app_sv.arb +++ b/auth/lib/l10n/arb/app_sv.arb @@ -73,7 +73,6 @@ "oops": "Hoppsan", "suggestFeatures": "Föreslå funktionalitet", "faq": "FAQ", - "faq_q_1": "Hur säkert är ente Auth?", "scan": "Skanna", "twoFactorAuthTitle": "Tvåfaktorsautentisering", "enterRecoveryKeyHint": "Ange din återställningsnyckel", @@ -105,6 +104,7 @@ "recoveryKeyCopiedToClipboard": "Återställningsnyckel kopierad till urklipp", "recoveryKeyOnForgotPassword": "Om du glömmer ditt lösenord är det enda sättet du kan återställa dina data med denna nyckel.", "saveKey": "Spara nyckel", + "save": "Spara", "back": "Tillbaka", "createAccount": "Skapa konto", "password": "Lösenord", diff --git a/auth/lib/l10n/arb/app_ti.arb b/auth/lib/l10n/arb/app_ti.arb index f8d2b95d1..27147ebb6 100644 --- a/auth/lib/l10n/arb/app_ti.arb +++ b/auth/lib/l10n/arb/app_ti.arb @@ -78,14 +78,12 @@ "data": "ሓበሬታ", "importCodes": "ኮድ ኣእቱ", "importTypePlainText": "Plain text", - "importTypeEnteEncrypted": "ente Encrypted export", "passwordForDecryptingExport": "Password to decrypt export", "passwordEmptyError": "Password can not be empty", "importFromApp": "Import codes from {appName}", "importGoogleAuthGuide": "Export your accounts from Google Authenticator to a QR code using the \"Transfer Accounts\" option. Then using another device, scan the QR code.\n\nTip: You can use your laptop's webcam to take a picture of the QR code.", "importSelectJsonFile": "Select JSON file", "importSelectAppExport": "Select {appName} export file", - "importEnteEncGuide": "Select the encrypted JSON file exported from ente", "importRaivoGuide": "Use the \"Export OTPs to Zip archive\" option in Raivo's Settings.\n\nExtract the zip file and import the JSON file.", "importBitwardenGuide": "Use the \"Export vault\" option within Bitwarden Tools and import the unencrypted JSON file.", "importAegisGuide": "Use the \"Export the vault\" option in Aegis's Settings.\n\nIf your vault is encrypted, you will need to enter vault password to decrypt the vault.", @@ -114,22 +112,18 @@ "copied": "Copied", "pleaseTryAgain": "Please try again", "existingUser": "Existing User", - "newUser": "New to ente", "delete": "Delete", "enterYourPasswordHint": "Enter your password", "forgotPassword": "Forgot password", "oops": "ዉዉኡ", "suggestFeatures": "Suggest features", "faq": "FAQ", - "faq_q_1": "How secure is ente Auth?", - "faq_a_1": "All codes you backup via ente is stored end-to-end encrypted. This means only you can access your codes. Our apps are open source and our cryptography has been externally audited.", "faq_q_2": "Can I access my codes on desktop?", "faq_a_2": "You can access your codes on the web @ auth.ente.io.", "faq_q_3": "How can I delete codes?", "faq_a_3": "You can delete a code by swiping left on that item.", "faq_q_4": "How can I support this project?", "faq_a_4": "You can support the development of this project by subscribing to our Photos app @ ente.io.", - "faq_q_5": "How can I enable FaceID lock in ente Auth", "faq_a_5": "You can enable FaceID lock under Settings → Security → Lockscreen.", "somethingWentWrongMessage": "Something went wrong, please try again", "leaveFamily": "Leave family", @@ -343,7 +337,6 @@ "deleteCodeAuthMessage": "Authenticate to delete code", "showQRAuthMessage": "Authenticate to show QR code", "confirmAccountDeleteTitle": "Confirm account deletion", - "confirmAccountDeleteMessage": "This account is linked to other ente apps, if you use any.\n\nYour uploaded data, across all ente apps, will be scheduled for deletion, and your account will be permanently deleted.", "androidBiometricHint": "Verify identity", "@androidBiometricHint": { "description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters." diff --git a/auth/lib/l10n/arb/app_tr.arb b/auth/lib/l10n/arb/app_tr.arb index 4564abae1..9b847faf0 100644 --- a/auth/lib/l10n/arb/app_tr.arb +++ b/auth/lib/l10n/arb/app_tr.arb @@ -78,14 +78,12 @@ "data": "Veri", "importCodes": "Kodu içe aktar", "importTypePlainText": "Salt metin", - "importTypeEnteEncrypted": "ente Şifreli dışa aktarma", "passwordForDecryptingExport": "Dışa aktarımın şifresini çözmek için parola", "passwordEmptyError": "Şifre boş olamaz", "importFromApp": "Kodları {appName} uygulamasından içe aktarın", "importGoogleAuthGuide": "\"Hesapları Aktar\" seçeneğini kullanarak hesaplarınızı Google Authenticator'dan bir QR koduna aktarın. Ardından başka bir cihaz kullanarak QR kodunu tarayın.\n\nİpucu: QR kodunun fotoğrafını çekmek için dizüstü bilgisayarınızın kamerasını kullanabilirsiniz.", "importSelectJsonFile": "JSON dosyasını seçin", "importSelectAppExport": "{appName} dışarı aktarma dosyasını seçin", - "importEnteEncGuide": "Ente'den dışa aktarılan şifrelenmiş JSON dosyasını seçin", "importRaivoGuide": "Raivo'nun ayarlarında \"OTP'leri Zip arşivine aktar\" seçeneğini kullanın.\n\nZip dosyasını çıkarın ve JSON dosyasını içe aktarın.", "importBitwardenGuide": "Bitwarden Tools içindeki \"Kasayı dışa aktar\" seçeneğini kullanın ve şifrelenmemiş JSON dosyasını içe aktarın.", "importAegisGuide": "Aegis'in Ayarlarında \"Kasayı dışa aktar\" seçeneğini kullanın.\n\nKasanız şifrelenmişse, kasanın şifresini çözmek için kasa parolasını girmeniz gerekecektir.", @@ -115,22 +113,18 @@ "copied": "Kopyalandı", "pleaseTryAgain": "Lütfen tekrar deneyin", "existingUser": "Mevcut kullanıcı", - "newUser": "Yeni ente kullanıcısı", "delete": "Sil", "enterYourPasswordHint": "Parolanızı girin", "forgotPassword": "Şifremi unuttum", "oops": "Hay aksi", "suggestFeatures": "Özellik önerin", "faq": "SSS", - "faq_q_1": "Ente Auth ne kadar güvenli?", - "faq_a_1": "Ente aracılığıyla yedeklediğiniz tüm kodlar uçtan uca şifrelenmiş olarak saklanır. Bu, kodlarınıza yalnızca sizin erişebileceğiniz anlamına gelir. Uygulamalarımız açık kaynaklıdır ve kriptografimiz harici olarak denetlenmiştir.", "faq_q_2": "Kodlarıma masaüstünden erişebilir miyim?", "faq_a_2": "Kodlarınıza internet üzerinden @ auth.ente.io adresinden erişebilirsiniz.", "faq_q_3": "Kodları nasıl silebilirim?", "faq_a_3": "Bir kodu, o öğenin üzerinde sola kaydırarak silebilirsiniz.", "faq_q_4": "Bu projeye nasıl destek olabilirim?", "faq_a_4": "Fotoğraflar uygulamamıza @ ente.io abone olarak bu projenin geliştirilmesine destek olabilirsiniz.", - "faq_q_5": "FaceID kilidini ente Auth'ta nasıl etkinleştirebilirim", "faq_a_5": "FaceID kilidini Ayarlar → Güvenlik → Kilit Ekranı altında etkinleştirebilirsiniz.", "somethingWentWrongMessage": "Bir şeyler ters gitti, lütfen tekrar deneyin", "leaveFamily": "Aile planından ayrıl", @@ -344,7 +338,6 @@ "deleteCodeAuthMessage": "Kodu silmek için doğrulama yapın", "showQRAuthMessage": "QR kodunu göstermek için doğrulama yapın", "confirmAccountDeleteTitle": "Hesap silme işlemini onayla", - "confirmAccountDeleteMessage": "Bu hesap, eğer kullanıyorsanız, diğer ente uygulamalarıyla bağlantılıdır.\n\nTüm ente uygulamalarında yüklediğiniz veriler silinmek üzere programlanacak ve hesabınız kalıcı olarak silinecektir.", "androidBiometricHint": "Kimliği doğrula", "@androidBiometricHint": { "description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters." diff --git a/auth/lib/l10n/arb/app_vi.arb b/auth/lib/l10n/arb/app_vi.arb index 61747181d..e318f9b55 100644 --- a/auth/lib/l10n/arb/app_vi.arb +++ b/auth/lib/l10n/arb/app_vi.arb @@ -78,14 +78,12 @@ "data": "Dữ liệu", "importCodes": "Nhập mã", "importTypePlainText": "Văn bản thuần", - "importTypeEnteEncrypted": "xuất ente đã mã hóa", "passwordForDecryptingExport": "Mật khẩu để giải mã xuất", "passwordEmptyError": "Mật khẩu không thể để trống", "importFromApp": "Nhập mã từ {appName}", "importGoogleAuthGuide": "Xuất dữ liệu tài khoản của bạn từ Google Authenticator sang mã QR bằng tùy chọn \"Chuyển tài khoản\". Sau đó dùng thiết bị khác quét mã QR.", "importSelectJsonFile": "Chọn tệp JSON", "importSelectAppExport": "Chọn {appName} tệp dữ liệu xuất", - "importEnteEncGuide": "Chọn tệp JSON được mã hóa đã xuất từ ​​ente", "importRaivoGuide": "Sử dụng tùy chọn \"Xuất OTP sang lưu trữ Zip\" trong cài đặt của Raivo.", "importBitwardenGuide": "Sử dụng tùy chọn \"Xuất vault\" trong công cụ Bitwarden và nhập tệp JSON không được mã hóa.", "importAegisGuide": "Nếu vault của bạn được mã hóa, bạn sẽ cần nhập mật khẩu vault để giải mã vault.", @@ -114,22 +112,18 @@ "copied": "\u001dĐã sao chép", "pleaseTryAgain": "Vui lòng thử lại", "existingUser": "Người dùng hiện tại", - "newUser": "Mới tham gia ente", "delete": "Xóa", "enterYourPasswordHint": "Nhập mật khẩu của bạn", "forgotPassword": "Quên mật khẩu", "oops": "Rất tiếc", "suggestFeatures": "Tính năng đề nghị", "faq": "Câu hỏi thường gặp", - "faq_q_1": "Mức độ an toàn của ente Auth như thế nào?", - "faq_a_1": "Tất cả các mã bạn sao lưu qua ente đều được lưu trữ dưới dạng mã hóa đầu cuối. Điều này có nghĩa là chỉ bạn mới có thể truy cập mã của mình. Ứng dụng của chúng tôi là nguồn mở và mật mã của chúng tôi đã được kiểm toán độc lập.", "faq_q_2": "Tôi có thể truy cập mã của mình trên máy tính không?", "faq_a_2": "Bạn có thể truy cập mã của mình trên web @ auth.ente.io.", "faq_q_3": "Làm cách nào để xóa mã?", "faq_a_3": "Bạn có thể xóa mã bằng cách vuốt sang trái vào mục đó.", "faq_q_4": "Tôi có thể hỗ trợ dự án này như thế nào?", "faq_a_4": "Bạn có thể hỗ trợ sự phát triển của dự án này bằng cách đăng ký ứng dụng Ảnh @ ente.io của chúng tôi.", - "faq_q_5": "Tôi có thể bật khóa FaceID trong ente Auth như thế nào", "faq_a_5": "Bạn có thể bật khóa FaceID trong Cài đặt → Bảo mật → Màn hình khóa.", "somethingWentWrongMessage": "Phát hiện có lỗi, xin thử lại", "leaveFamily": "Rời khỏi gia đình", @@ -343,7 +337,6 @@ "deleteCodeAuthMessage": "Xác minh để xóa mã", "showQRAuthMessage": "Xác minh để hiển thị mã QR", "confirmAccountDeleteTitle": "Xác nhận xóa tài khoản", - "confirmAccountDeleteMessage": "Tài khoản này được liên kết với các ứng dụng ente khác, nếu bạn sử dụng bất kỳ ứng dụng nào.\n\nDữ liệu đã tải lên của bạn, trên tất cả các ứng dụng, sẽ bị lên lịch xóa và tài khoản của bạn sẽ bị xóa vĩnh viễn.", "androidBiometricHint": "Xác định danh tính", "@androidBiometricHint": { "description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters." diff --git a/auth/lib/l10n/arb/app_zh.arb b/auth/lib/l10n/arb/app_zh.arb index 5328a14b9..077ee26fd 100644 --- a/auth/lib/l10n/arb/app_zh.arb +++ b/auth/lib/l10n/arb/app_zh.arb @@ -78,14 +78,14 @@ "data": "数据", "importCodes": "导入代码", "importTypePlainText": "纯文本", - "importTypeEnteEncrypted": "ente 加密导出", + "importTypeEnteEncrypted": "Ente 加密导出", "passwordForDecryptingExport": "用来解密导出的密码", "passwordEmptyError": "密码不能为空", "importFromApp": "从 {appName} 导入代码", "importGoogleAuthGuide": "使用“转移帐户”选项将您的帐户从 Google 身份验证器导出到二维码。然后使用另一台设备扫描二维码。\n\n提示:您可以使用笔记本电脑的网络摄像头拍摄二维码的照片。", "importSelectJsonFile": "选择 JSON 文件", "importSelectAppExport": "选择 {appName} 的导出文件", - "importEnteEncGuide": "选择从ente导出的加密JSON文件", + "importEnteEncGuide": "选择从 Ente 导出的 JSON 加密文件", "importRaivoGuide": "使用 Raivo 设置中的“将 OTP 导出到 Zip 存档”选项。\n\n解压 zip 文件并导入 JSON 文件。", "importBitwardenGuide": "使用 Bitwarden 工具中的“导出保管库”选项并导入未加密的 JSON 文件。", "importAegisGuide": "在Aegis的设置中使用\"导出密码库\"选项。\n\n如果您的密码库已加密,您需要输入密码才能解密密码库。", @@ -115,22 +115,22 @@ "copied": "已复制", "pleaseTryAgain": "请重试", "existingUser": "已注册用户", - "newUser": "刚来到ente", + "newUser": "初来 Ente", "delete": "删除", "enterYourPasswordHint": "输入您的密码", "forgotPassword": "忘记密码", "oops": "哎呀", "suggestFeatures": "建议新功能", "faq": "常见问题", - "faq_q_1": "ente Auth的安全程度如何?", - "faq_a_1": "您通过 ente 备份的所有代码均以端到端加密方式存储。这意味着只有您可以访问您的代码。 我们的应用程序是开源的,我们的加密技术已经过外部审计。", + "faq_q_1": "Auth 的安全性如何?", + "faq_a_1": "您通过 Auth 备份的所有代码均以端到端加密方式存储。这意味着只有您可以访问您的代码。我们的应用程序是开源的并且我们的加密技术已经过外部审计。", "faq_q_2": "我可以在桌面上访问我的代码吗?", "faq_a_2": "您可以在 web @auth.ente.io 上访问您的代码。", "faq_q_3": "我如何删除代码?", "faq_a_3": "您可以通过向左滑动该项目来删除该代码。", "faq_q_4": "我该如何支持该项目?", "faq_a_4": "您可以通过订阅我们的照片应用程序@ente.io来支持该项目的开发。", - "faq_q_5": "如何在 ente Auth 中启用 FaceID 锁定", + "faq_q_5": "我如何启用 Auth 中的面容 ID 锁", "faq_a_5": "您可以在“设置”→“安全”→“锁屏”下启用 FaceID 锁定。", "somethingWentWrongMessage": "出了点问题,请重试", "leaveFamily": "离开家庭", @@ -143,7 +143,7 @@ "verifyEmail": "验证电子邮件", "enterCodeHint": "从你的身份验证器应用中\n输入6位数字代码", "lostDeviceTitle": "丢失了设备吗?", - "twoFactorAuthTitle": "双因素认证", + "twoFactorAuthTitle": "双重认证", "passkeyAuthTitle": "通行密钥认证", "verifyPasskey": "验证通行密钥", "recoverAccount": "恢复账户", @@ -199,6 +199,10 @@ "recoveryKeySaveDescription": "我们不会存储此密钥,请将此24个单词密钥保存在一个安全的地方。", "doThisLater": "稍后再做", "saveKey": "保存密钥", + "save": "保存", + "send": "发送", + "saveOrSendDescription": "您想将其保存到您的内置存储(默认情况下为“下载”文件夹)还是将其发送到其他应用程序?", + "saveOnlyDescription": "您想将其保存到您的内置存储中(默认情况下为“下载”文件夹)吗?", "back": "返回", "createAccount": "创建账户", "passwordStrength": "密码强度: {passwordStrengthValue}", @@ -321,7 +325,7 @@ "emailChangedTo": "电子邮件已更改为 {newEmail}", "authenticationFailedPleaseTryAgain": "认证失败,请重试", "authenticationSuccessful": "认证成功!", - "twofactorAuthenticationSuccessfullyReset": "双因素身份验证已成功重置", + "twofactorAuthenticationSuccessfullyReset": "双重认证已成功重置", "incorrectRecoveryKey": "恢复密钥不正确", "theRecoveryKeyYouEnteredIsIncorrect": "您输入的恢复密钥不正确", "enterPassword": "输入密码", @@ -346,7 +350,7 @@ "deleteCodeAuthMessage": "删除代码需要身份验证", "showQRAuthMessage": "显示QR码需要身份验证", "confirmAccountDeleteTitle": "确认删除账户", - "confirmAccountDeleteMessage": "该账户已链接到其他ente旗下的应用程序(如果您使用任何相关的应用程序)。\n\n您在所有ente旗下应用程序中上传的数据都将被安排删除,并且您的账户将被永久删除。", + "confirmAccountDeleteMessage": "如果您使用其他 Ente 应用程序,该账户将会与其他应用程序链接。\n\n在所有 Ente 应用程序中,您上传的数据将被安排用于删除,并且您的账户将被永久删除。", "androidBiometricHint": "验证身份", "@androidBiometricHint": { "description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters." @@ -407,6 +411,7 @@ "doNotSignOut": "不要退登", "hearUsWhereTitle": "您是如何知道Ente的? (可选的)", "hearUsExplanation": "我们不跟踪应用程序安装情况。如果您告诉我们您是在哪里找到我们的,将会有所帮助!", + "recoveryKeySaved": "恢复密钥已保存在下载文件夹中!", "waitingForBrowserRequest": "正在等待浏览器请求...", "waitingForVerification": "等待验证...", "passkey": "通行密钥", diff --git a/auth/pubspec.lock b/auth/pubspec.lock index 73b3075cb..2d61b77c3 100644 --- a/auth/pubspec.lock +++ b/auth/pubspec.lock @@ -53,10 +53,10 @@ packages: dependency: transitive description: name: args - sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.5.0" async: dependency: transitive description: @@ -157,10 +157,10 @@ packages: dependency: transitive description: name: built_value - sha256: fedde275e0a6b798c3296963c5cd224e3e1b55d0e478d5b7e65e6b540f363a0e + sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb url: "https://pub.dev" source: hosted - version: "8.9.1" + version: "8.9.2" characters: dependency: transitive description: @@ -318,10 +318,10 @@ packages: dependency: "direct main" description: name: dio - sha256: "0978e9a3e45305a80a7210dbeaf79d6ee8bee33f70c8e542dc654c952070217f" + sha256: "639179e1cc0957779e10dd5b786ce180c477c4c0aca5aaba5d1700fa2e834801" url: "https://pub.dev" source: hosted - version: "5.4.2+1" + version: "5.4.3" dotted_border: dependency: "direct main" description: @@ -586,10 +586,10 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: b068ffc46f82a55844acfa4fdbb61fad72fa2aef0905548419d97f0f95c456da + sha256: "8cf40eebf5dec866a6d1956ad7b4f7016e6c0cc69847ab946833b7d43743809f" url: "https://pub.dev" source: hosted - version: "2.0.17" + version: "2.0.19" flutter_secure_storage: dependency: "direct main" description: @@ -1053,18 +1053,18 @@ packages: dependency: "direct main" description: name: path_provider - sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b + sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" + sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.2.4" path_provider_foundation: dependency: transitive description: @@ -1133,10 +1133,10 @@ packages: dependency: "direct main" description: name: pointycastle - sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29" + sha256: "70fe966348fe08c34bf929582f1d8247d9d9408130723206472b4687227e4333" url: "https://pub.dev" source: hosted - version: "3.7.4" + version: "3.8.0" pool: dependency: transitive description: @@ -1221,18 +1221,18 @@ packages: dependency: "direct main" description: name: sentry - sha256: a460aa48568d47140dd0557410b624d344ffb8c05555107ac65035c1097cf1ad + sha256: fe99a06970b909a491b7f89d54c9b5119772e3a48a400308a6e129625b333f5b url: "https://pub.dev" source: hosted - version: "7.18.0" + version: "7.19.0" sentry_flutter: dependency: "direct main" description: name: sentry_flutter - sha256: "3d0d1d4e0e407d276ae8128d123263ccbc37e988bae906765efd6f37d544f4c6" + sha256: fc013d4a753447320f62989b1871fdc1f20c77befcc8be3e38774dd7402e7a62 url: "https://pub.dev" source: hosted - version: "7.18.0" + version: "7.19.0" share_plus: dependency: "direct main" description: @@ -1253,18 +1253,18 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02" + sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180 url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.2.3" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" + sha256: "1ee8bf911094a1b592de7ab29add6f826a7331fb854273d55918693d5364a1f2" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.2.2" shared_preferences_foundation: dependency: transitive description: @@ -1387,10 +1387,10 @@ packages: description: path: sqflite ref: HEAD - resolved-ref: "075b3e2f81e691a19a500e7ff6db2953de7f83a9" + resolved-ref: f281785e12e8b1abf2f9d41a587fc83d810724cf url: "https://github.com/tekartik/sqflite" source: git - version: "2.3.2" + version: "2.3.3" sqflite_common: dependency: transitive description: @@ -1411,10 +1411,10 @@ packages: dependency: "direct main" description: name: sqlite3 - sha256: "072128763f1547e3e9b4735ce846bfd226d68019ccda54db4cd427b12dfdedc9" + sha256: "1abbeb84bf2b1a10e5e1138c913123c8aa9d83cd64e5f9a0dd847b3c83063202" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.2" sqlite3_flutter_libs: dependency: "direct main" description: @@ -1515,10 +1515,10 @@ packages: dependency: "direct main" description: name: tray_manager - sha256: "4ab709d70a4374af172f8c39e018db33a4271265549c6fc9d269a65e5f4b0225" + sha256: e0ac9a88b2700f366b8629b97e8663b6ef450a2f169560a685dc167bfe9c9c29 url: "https://pub.dev" source: hosted - version: "0.2.1" + version: "0.2.2" tuple: dependency: "direct main" description: @@ -1547,18 +1547,18 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: "0ecc004c62fd3ed36a2ffcbe0dd9700aee63bd7532d0b642a488b1ec310f492e" + sha256: "6ce1e04375be4eed30548f10a315826fd933c1e493206eab82eed01f438c8d2e" url: "https://pub.dev" source: hosted - version: "6.2.5" + version: "6.2.6" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: d4ed0711849dd8e33eb2dd69c25db0d0d3fdc37e0a62e629fe32f57a22db2745 + sha256: "360a6ed2027f18b73c8d98e159dda67a61b7f2e0f6ec26e86c3ada33b0621775" url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.3.1" url_launcher_ios: dependency: transitive description: @@ -1611,10 +1611,10 @@ packages: dependency: "direct main" description: name: uuid - sha256: cd210a09f7c18cbe5a02511718e0334de6559871052c90a90c0cca46a4aa81c8 + sha256: "814e9e88f21a176ae1359149021870e87f7cddaf633ab678a5d2b0bff7fd1ba8" url: "https://pub.dev" source: hosted - version: "4.3.3" + version: "4.4.0" vector_graphics: dependency: transitive description: @@ -1675,18 +1675,18 @@ packages: dependency: transitive description: name: web_socket_channel - sha256: "1d8e795e2a8b3730c41b8a98a2dff2e0fb57ae6f0764a1c46ec5915387d257b2" + sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42" url: "https://pub.dev" source: hosted - version: "2.4.4" + version: "2.4.5" win32: dependency: "direct main" description: name: win32 - sha256: "8cb58b45c47dcb42ab3651533626161d6b67a2921917d8d429791f76972b3480" + sha256: "0a989dc7ca2bb51eac91e8fd00851297cfffd641aa7538b165c62637ca0eaa4a" url: "https://pub.dev" source: hosted - version: "5.3.0" + version: "5.4.0" win32_registry: dependency: transitive description: diff --git a/desktop/src/main/fs.ts b/desktop/src/main/fs.ts index 0da89fb00..11ab36049 100644 --- a/desktop/src/main/fs.ts +++ b/desktop/src/main/fs.ts @@ -3,11 +3,20 @@ */ import { createWriteStream, existsSync } from "node:fs"; import fs from "node:fs/promises"; -import path from "node:path"; import { Readable } from "node:stream"; export const fsExists = (path: string) => existsSync(path); +export const fsRename = (oldPath: string, newPath: string) => + fs.rename(oldPath, newPath); + +export const fsMkdirIfNeeded = (dirPath: string) => + fs.mkdir(dirPath, { recursive: true }); + +export const fsRmdir = (path: string) => fs.rmdir(path); + +export const fsRm = (path: string) => fs.rm(path); + /** * Write a (web) ReadableStream to a file at the given {@link filePath}. * @@ -73,9 +82,6 @@ const writeNodeStream = async ( /* TODO: Audit below this */ -export const checkExistsAndCreateDir = (dirPath: string) => - fs.mkdir(dirPath, { recursive: true }); - export const saveStreamToDisk = writeStream; export const saveFileToDisk = (path: string, contents: string) => @@ -84,50 +90,8 @@ export const saveFileToDisk = (path: string, contents: string) => export const readTextFile = async (filePath: string) => fs.readFile(filePath, "utf-8"); -export const moveFile = async (sourcePath: string, destinationPath: string) => { - if (!existsSync(sourcePath)) { - throw new Error("File does not exist"); - } - if (existsSync(destinationPath)) { - throw new Error("Destination file already exists"); - } - // check if destination folder exists - const destinationFolder = path.dirname(destinationPath); - await fs.mkdir(destinationFolder, { recursive: true }); - await fs.rename(sourcePath, destinationPath); -}; - export const isFolder = async (dirPath: string) => { if (!existsSync(dirPath)) return false; const stats = await fs.stat(dirPath); return stats.isDirectory(); }; - -export const deleteFolder = async (folderPath: string) => { - // Ensure it is folder - if (!isFolder(folderPath)) return; - - // Ensure folder is empty - const files = await fs.readdir(folderPath); - if (files.length > 0) throw new Error("Folder is not empty"); - - // rm -rf it - await fs.rmdir(folderPath); -}; - -export const rename = async (oldPath: string, newPath: string) => { - if (!existsSync(oldPath)) throw new Error("Path does not exist"); - await fs.rename(oldPath, newPath); -}; - -export const deleteFile = async (filePath: string) => { - // Ensure it exists - if (!existsSync(filePath)) return; - - // And is a file - const stat = await fs.stat(filePath); - if (!stat.isFile()) throw new Error("Path is not a file"); - - // rm it - return fs.rm(filePath); -}; diff --git a/desktop/src/main/ipc.ts b/desktop/src/main/ipc.ts index 180e68cdc..36e13ec60 100644 --- a/desktop/src/main/ipc.ts +++ b/desktop/src/main/ipc.ts @@ -18,14 +18,13 @@ import { showUploadZipDialog, } from "./dialogs"; import { - checkExistsAndCreateDir, - deleteFile, - deleteFolder, fsExists, + fsMkdirIfNeeded, + fsRename, + fsRm, + fsRmdir, isFolder, - moveFile, readTextFile, - rename, saveFileToDisk, saveStreamToDisk, } from "./fs"; @@ -169,12 +168,18 @@ export const attachIPCHandlers = () => { ipcMain.handle("fsExists", (_, path) => fsExists(path)); - // - FS Legacy - - ipcMain.handle("checkExistsAndCreateDir", (_, dirPath) => - checkExistsAndCreateDir(dirPath), + ipcMain.handle("fsRename", (_, oldPath: string, newPath: string) => + fsRename(oldPath, newPath), ); + ipcMain.handle("fsMkdirIfNeeded", (_, dirPath) => fsMkdirIfNeeded(dirPath)); + + ipcMain.handle("fsRmdir", (_, path: string) => fsRmdir(path)); + + ipcMain.handle("fsRm", (_, path: string) => fsRm(path)); + + // - FS Legacy + ipcMain.handle( "saveStreamToDisk", (_, path: string, fileStream: ReadableStream) => @@ -189,18 +194,6 @@ export const attachIPCHandlers = () => { ipcMain.handle("isFolder", (_, dirPath: string) => isFolder(dirPath)); - ipcMain.handle("moveFile", (_, oldPath: string, newPath: string) => - moveFile(oldPath, newPath), - ); - - ipcMain.handle("deleteFolder", (_, path: string) => deleteFolder(path)); - - ipcMain.handle("deleteFile", (_, path: string) => deleteFile(path)); - - ipcMain.handle("rename", (_, oldPath: string, newPath: string) => - rename(oldPath, newPath), - ); - // - Upload ipcMain.handle("getPendingUploads", () => getPendingUploads()); diff --git a/desktop/src/preload.ts b/desktop/src/preload.ts index 2db39e229..1a344e832 100644 --- a/desktop/src/preload.ts +++ b/desktop/src/preload.ts @@ -99,6 +99,17 @@ const skipAppUpdate = (version: string) => { const fsExists = (path: string): Promise => ipcRenderer.invoke("fsExists", path); +const fsMkdirIfNeeded = (dirPath: string): Promise => + ipcRenderer.invoke("fsMkdirIfNeeded", dirPath); + +const fsRename = (oldPath: string, newPath: string): Promise => + ipcRenderer.invoke("fsRename", oldPath, newPath); + +const fsRmdir = (path: string): Promise => + ipcRenderer.invoke("fsRmdir", path); + +const fsRm = (path: string): Promise => ipcRenderer.invoke("fsRm", path); + // - AUDIT below this // - Conversion @@ -218,9 +229,6 @@ const updateWatchMappingIgnoredFiles = ( // - FS Legacy -const checkExistsAndCreateDir = (dirPath: string): Promise => - ipcRenderer.invoke("checkExistsAndCreateDir", dirPath); - const saveStreamToDisk = ( path: string, fileStream: ReadableStream, @@ -235,18 +243,6 @@ const readTextFile = (path: string): Promise => const isFolder = (dirPath: string): Promise => ipcRenderer.invoke("isFolder", dirPath); -const moveFile = (oldPath: string, newPath: string): Promise => - ipcRenderer.invoke("moveFile", oldPath, newPath); - -const deleteFolder = (path: string): Promise => - ipcRenderer.invoke("deleteFolder", path); - -const deleteFile = (path: string): Promise => - ipcRenderer.invoke("deleteFile", path); - -const rename = (oldPath: string, newPath: string): Promise => - ipcRenderer.invoke("rename", oldPath, newPath); - // - Upload const getPendingUploads = (): Promise<{ @@ -348,19 +344,18 @@ contextBridge.exposeInMainWorld("electron", { // - FS fs: { exists: fsExists, + rename: fsRename, + mkdirIfNeeded: fsMkdirIfNeeded, + rmdir: fsRmdir, + rm: fsRm, }, // - FS legacy // TODO: Move these into fs + document + rename if needed - checkExistsAndCreateDir, saveStreamToDisk, saveFileToDisk, readTextFile, isFolder, - moveFile, - deleteFolder, - deleteFile, - rename, // - Upload diff --git a/docs/docs/auth/faq/enteception/index.md b/docs/docs/auth/faq/enteception/index.md index 4b3167f73..1155986b7 100644 --- a/docs/docs/auth/faq/enteception/index.md +++ b/docs/docs/auth/faq/enteception/index.md @@ -12,7 +12,7 @@ There are multiple answers, none of which are better or worse, they just depend on your situation and risk tolerance. If you are using the same account for both Ente Photos and Ente Auth and have -enabled 2FA from the ente Photos app, we recommend that you ensure you store +enabled 2FA from the Ente Photos app, we recommend that you ensure you store your recovery key in a safe place (writing it down on a paper is a good idea). This key can be used to bypass Ente 2FA in case you are locked out. diff --git a/docs/docs/self-hosting/faq/sharing.md b/docs/docs/self-hosting/faq/sharing.md index 0ad58e1c0..4e3652ff7 100644 --- a/docs/docs/self-hosting/faq/sharing.md +++ b/docs/docs/self-hosting/faq/sharing.md @@ -41,3 +41,19 @@ NEXT_PUBLIC_ENTE_ENDPOINT=http://localhost:8080 \ NEXT_PUBLIC_ENTE_ALBUMS_ENDPOINT=http://localhost:3002 \ yarn dev:albums ``` + +If you also want to change the prefix (the origin) in the generated public +links, to use your custom albums endpoint in the generated public link instead +of albums.ente.io, set `apps.public-albums` property in museum's configuration + +For example, when running using the starter docker compose file, you can do this +by creating a `museum.yaml` and defining the following configuration there: + +```yaml +apps: + public-albums: http://localhost:3002 +``` + +(For more details, see +[local.yaml](https://github.com/ente-io/ente/blob/main/server/configurations/local.yaml) +in the server's source code). diff --git a/mobile/README.md b/mobile/README.md index 005d303b6..662e71403 100644 --- a/mobile/README.md +++ b/mobile/README.md @@ -46,7 +46,7 @@ You can alternatively install the build from PlayStore or F-Droid. ## 🧑‍💻 Building from source -1. [Install Flutter v3.13.4](https://flutter.dev/docs/get-started/install). +1. [Install Flutter v3.19.5](https://flutter.dev/docs/get-started/install). 2. Pull in all submodules with `git submodule update --init --recursive` diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle index a241cbe7e..01ec11ff8 100644 --- a/mobile/android/app/build.gradle +++ b/mobile/android/app/build.gradle @@ -1,3 +1,8 @@ +plugins { + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" +} def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { @@ -6,11 +11,6 @@ if (localPropertiesFile.exists()) { } } -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { flutterVersionCode = '1' @@ -21,10 +21,6 @@ if (flutterVersionName == null) { flutterVersionName = '1.0' } -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - def keystoreProperties = new Properties() def keystorePropertiesFile = rootProject.file('key.properties') if (keystorePropertiesFile.exists()) { @@ -126,7 +122,7 @@ flutter { dependencies { implementation 'io.sentry:sentry-android:2.0.0' - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.21" implementation 'com.android.support:multidex:1.0.3' implementation 'com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava' testImplementation 'junit:junit:4.12' diff --git a/mobile/android/build.gradle b/mobile/android/build.gradle index ce75713c0..ad3554980 100644 --- a/mobile/android/build.gradle +++ b/mobile/android/build.gradle @@ -1,16 +1,5 @@ -buildscript { - ext.kotlin_version = '1.8.21' - repositories { - google() - jcenter() - } - - ext.appCompatVersion = '1.1.0' // for background_fetch - - dependencies { - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath 'com.android.tools.build:gradle:7.1.2' // for background_fetch - } +ext { + appCompatVersion = '1.1.0' // for background_fetch } allprojects { diff --git a/mobile/android/settings.gradle b/mobile/android/settings.gradle index 5a2f14fb1..0fff0ecaf 100644 --- a/mobile/android/settings.gradle +++ b/mobile/android/settings.gradle @@ -1,15 +1,25 @@ -include ':app' +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + }() -def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") -def plugins = new Properties() -def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') -if (pluginsFile.exists()) { - pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } + repositories { + google() + mavenCentral() + gradlePluginPortal() + } } -plugins.each { name, path -> - def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() - include ":$name" - project(":$name").projectDir = pluginDirectory +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "7.1.2" apply false + id "org.jetbrains.kotlin.android" version "1.8.21" apply false } + +include ":app" \ No newline at end of file diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock index a03f7843d..09f9ded3c 100644 --- a/mobile/ios/Podfile.lock +++ b/mobile/ios/Podfile.lock @@ -1,8 +1,11 @@ PODS: - - background_fetch (1.2.1): + - background_fetch (1.3.2): - Flutter - battery_info (0.0.1): - Flutter + - bonsoir_darwin (3.0.0): + - Flutter + - FlutterMacOS - connectivity_plus (0.0.1): - Flutter - ReachabilitySwift @@ -10,38 +13,38 @@ PODS: - Flutter - file_saver (0.0.1): - Flutter - - Firebase/CoreOnly (10.18.0): - - FirebaseCore (= 10.18.0) - - Firebase/Messaging (10.18.0): + - Firebase/CoreOnly (10.22.0): + - FirebaseCore (= 10.22.0) + - Firebase/Messaging (10.22.0): - Firebase/CoreOnly - - FirebaseMessaging (~> 10.18.0) - - firebase_core (2.24.2): - - Firebase/CoreOnly (= 10.18.0) + - FirebaseMessaging (~> 10.22.0) + - firebase_core (2.29.0): + - Firebase/CoreOnly (= 10.22.0) - Flutter - - firebase_messaging (14.7.10): - - Firebase/Messaging (= 10.18.0) + - firebase_messaging (14.7.19): + - Firebase/Messaging (= 10.22.0) - firebase_core - Flutter - - FirebaseCore (10.18.0): + - FirebaseCore (10.22.0): - FirebaseCoreInternal (~> 10.0) - GoogleUtilities/Environment (~> 7.12) - GoogleUtilities/Logger (~> 7.12) - - FirebaseCoreInternal (10.21.0): + - FirebaseCoreInternal (10.24.0): - "GoogleUtilities/NSData+zlib (~> 7.8)" - - FirebaseInstallations (10.21.0): + - FirebaseInstallations (10.24.0): - FirebaseCore (~> 10.0) - GoogleUtilities/Environment (~> 7.8) - GoogleUtilities/UserDefaults (~> 7.8) - PromisesObjC (~> 2.1) - - FirebaseMessaging (10.18.0): + - FirebaseMessaging (10.22.0): - FirebaseCore (~> 10.0) - FirebaseInstallations (~> 10.0) - - GoogleDataTransport (~> 9.2) + - GoogleDataTransport (~> 9.3) - GoogleUtilities/AppDelegateSwizzler (~> 7.8) - GoogleUtilities/Environment (~> 7.8) - GoogleUtilities/Reachability (~> 7.8) - GoogleUtilities/UserDefaults (~> 7.8) - - nanopb (< 2.30910.0, >= 2.30908.0) + - nanopb (< 2.30911.0, >= 2.30908.0) - fk_user_agent (2.0.0): - Flutter - Flutter (1.0.0) @@ -70,27 +73,35 @@ PODS: - fluttertoast (0.0.2): - Flutter - Toast - - GoogleDataTransport (9.3.0): + - GoogleDataTransport (9.4.1): - GoogleUtilities/Environment (~> 7.7) - - nanopb (< 2.30910.0, >= 2.30908.0) + - nanopb (< 2.30911.0, >= 2.30908.0) - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/AppDelegateSwizzler (7.12.0): + - GoogleUtilities/AppDelegateSwizzler (7.13.0): - GoogleUtilities/Environment - GoogleUtilities/Logger - GoogleUtilities/Network - - GoogleUtilities/Environment (7.12.0): + - GoogleUtilities/Privacy + - GoogleUtilities/Environment (7.13.0): + - GoogleUtilities/Privacy - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/Logger (7.12.0): + - GoogleUtilities/Logger (7.13.0): - GoogleUtilities/Environment - - GoogleUtilities/Network (7.12.0): + - GoogleUtilities/Privacy + - GoogleUtilities/Network (7.13.0): - GoogleUtilities/Logger - "GoogleUtilities/NSData+zlib" + - GoogleUtilities/Privacy - GoogleUtilities/Reachability - - "GoogleUtilities/NSData+zlib (7.12.0)" - - GoogleUtilities/Reachability (7.12.0): + - "GoogleUtilities/NSData+zlib (7.13.0)": + - GoogleUtilities/Privacy + - GoogleUtilities/Privacy (7.13.0) + - GoogleUtilities/Reachability (7.13.0): - GoogleUtilities/Logger - - GoogleUtilities/UserDefaults (7.12.0): + - GoogleUtilities/Privacy + - GoogleUtilities/UserDefaults (7.13.0): - GoogleUtilities/Logger + - GoogleUtilities/Privacy - home_widget (0.0.1): - Flutter - image_editor_common (1.0.0): @@ -114,6 +125,8 @@ PODS: - libwebp/sharpyuv (1.3.2) - libwebp/webp (1.3.2): - libwebp/sharpyuv + - local_auth_darwin (0.0.1): + - Flutter - local_auth_ios (0.0.1): - Flutter - Mantle (2.2.0): @@ -131,11 +144,11 @@ PODS: - Flutter - move_to_background (0.0.1): - Flutter - - nanopb (2.30909.1): - - nanopb/decode (= 2.30909.1) - - nanopb/encode (= 2.30909.1) - - nanopb/decode (2.30909.1) - - nanopb/encode (2.30909.1) + - nanopb (2.30910.0): + - nanopb/decode (= 2.30910.0) + - nanopb/encode (= 2.30910.0) + - nanopb/decode (2.30910.0) + - nanopb/encode (2.30910.0) - onnxruntime (0.0.1): - Flutter - onnxruntime-objc (= 1.15.1) @@ -152,30 +165,30 @@ PODS: - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS - - permission_handler_apple (9.1.1): + - permission_handler_apple (9.3.0): - Flutter - photo_manager (2.0.0): - Flutter - FlutterMacOS - - PromisesObjC (2.3.1) - - ReachabilitySwift (5.0.0) - - receive_sharing_intent (1.6.7): + - PromisesObjC (2.4.0) + - ReachabilitySwift (5.2.1) + - receive_sharing_intent (1.6.8): - Flutter - screen_brightness_ios (0.1.0): - Flutter - - SDWebImage (5.18.11): - - SDWebImage/Core (= 5.18.11) - - SDWebImage/Core (5.18.11) + - SDWebImage (5.19.1): + - SDWebImage/Core (= 5.19.1) + - SDWebImage/Core (5.19.1) - SDWebImageWebPCoder (0.14.5): - libwebp (~> 1.0) - SDWebImage/Core (~> 5.17) - - Sentry/HybridSDK (8.18.0): - - SentryPrivate (= 8.18.0) - - sentry_flutter (0.0.1): + - Sentry/HybridSDK (8.21.0): + - SentryPrivate (= 8.21.0) + - sentry_flutter (7.19.0): - Flutter - FlutterMacOS - - Sentry/HybridSDK (= 8.18.0) - - SentryPrivate (8.18.0) + - Sentry/HybridSDK (= 8.21.0) + - SentryPrivate (8.21.0) - share_plus (0.0.1): - Flutter - shared_preferences_foundation (0.0.1): @@ -199,7 +212,7 @@ PODS: - sqlite3/fts5 - sqlite3/perf-threadsafe - sqlite3/rtree - - Toast (4.1.0) + - Toast (4.1.1) - uni_links (0.0.1): - Flutter - url_launcher_ios (0.0.1): @@ -218,6 +231,7 @@ PODS: DEPENDENCIES: - background_fetch (from `.symlinks/plugins/background_fetch/ios`) - battery_info (from `.symlinks/plugins/battery_info/ios`) + - bonsoir_darwin (from `.symlinks/plugins/bonsoir_darwin/darwin`) - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - file_saver (from `.symlinks/plugins/file_saver/ios`) @@ -238,6 +252,7 @@ DEPENDENCIES: - in_app_purchase_storekit (from `.symlinks/plugins/in_app_purchase_storekit/darwin`) - integration_test (from `.symlinks/plugins/integration_test/ios`) - isar_flutter_libs (from `.symlinks/plugins/isar_flutter_libs/ios`) + - local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`) - local_auth_ios (from `.symlinks/plugins/local_auth_ios/ios`) - media_extension (from `.symlinks/plugins/media_extension/ios`) - media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`) @@ -294,6 +309,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/background_fetch/ios" battery_info: :path: ".symlinks/plugins/battery_info/ios" + bonsoir_darwin: + :path: ".symlinks/plugins/bonsoir_darwin/darwin" connectivity_plus: :path: ".symlinks/plugins/connectivity_plus/ios" device_info_plus: @@ -334,6 +351,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/integration_test/ios" isar_flutter_libs: :path: ".symlinks/plugins/isar_flutter_libs/ios" + local_auth_darwin: + :path: ".symlinks/plugins/local_auth_darwin/darwin" local_auth_ios: :path: ".symlinks/plugins/local_auth_ios/ios" media_extension: @@ -388,37 +407,39 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/wakelock_plus/ios" SPEC CHECKSUMS: - background_fetch: 896944864b038d2837fc750d470e9841e1e6a363 + background_fetch: 2319bf7e18237b4b269430b7f14d177c0df09c5a battery_info: 09f5c9ee65394f2291c8c6227bedff345b8a730c - connectivity_plus: 53efb943fc2882c8512d84c45707bcabc4c36076 + bonsoir_darwin: 127bdc632fdc154ae2f277a4d5c86a6212bc75be + connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808 - Firebase: 414ad272f8d02dfbf12662a9d43f4bba9bec2a06 - firebase_core: 0af4a2b24f62071f9bf283691c0ee41556dcb3f5 - firebase_messaging: 90e8a6db84b6e1e876cebce4f30f01dc495e7014 - FirebaseCore: 2322423314d92f946219c8791674d2f3345b598f - FirebaseCoreInternal: 43c1788eaeee9d1b97caaa751af567ce11010d00 - FirebaseInstallations: 390ea1d10a4d02b20c965cbfd527ee9b3b412acb - FirebaseMessaging: 9bc34a98d2e0237e1b121915120d4d48ddcf301e + Firebase: 797fd7297b7e1be954432743a0b3f90038e45a71 + firebase_core: aaadbddb3cb2ee3792b9804f9dbb63e5f6f7b55c + firebase_messaging: e65050bf9b187511d80ea3a4de7cf5573d2c7543 + FirebaseCore: 0326ec9b05fbed8f8716cddbf0e36894a13837f7 + FirebaseCoreInternal: bcb5acffd4ea05e12a783ecf835f2210ce3dc6af + FirebaseInstallations: 8f581fca6478a50705d2bd2abd66d306e0f5736e + FirebaseMessaging: 9f71037fd9db3376a4caa54e5a3949d1027b4b6e fk_user_agent: 1f47ec39291e8372b1d692b50084b0d54103c545 - Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 + Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_email_sender: 02d7443217d8c41483223627972bfdc09f74276b flutter_image_compress: 5a5e9aee05b6553048b8df1c3bc456d0afaac433 flutter_inappwebview: 3d32228f1304635e7c028b0d4252937730bbc6cf - flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743 + flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086 flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be flutter_sodium: c84426b4de738514b5b66cfdeb8a06634e72fe0b fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265 - GoogleDataTransport: 57c22343ab29bc686febbf7cbb13bad167c2d8fe - GoogleUtilities: 0759d1a57ebb953965c2dfe0ba4c82e95ccc2e34 + GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a + GoogleUtilities: d053d902a8edaa9904e1bd00c37535385b8ed152 home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57 image_editor_common: d6f6644ae4a6de80481e89fe6d0a8c49e30b4b43 - in_app_purchase_storekit: 9e9931234f0adcf71ae323f8c83785b96030edf1 + in_app_purchase_storekit: 0e4b3c2e43ba1e1281f4f46dd71b0593ce529892 integration_test: 13825b8a9334a850581300559b8839134b124670 isar_flutter_libs: b69f437aeab9c521821c3f376198c4371fa21073 libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009 - local_auth_ios: 1ba1475238daa33a6ffa2a29242558437be435ac + local_auth_darwin: c7e464000a6a89e952235699e32b329457608d98 + local_auth_ios: 5046a18c018dd973247a0564496c8898dbb5adf9 Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d media_extension: 6d30dc1431ebaa63f43c397c37917b1a0a597a4c media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1 @@ -426,7 +447,7 @@ SPEC CHECKSUMS: media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e motionphoto: d4a432b8c8f22fb3ad966258597c0103c9c5ff16 move_to_background: 39a5b79b26d577b0372cbe8a8c55e7aa9fcd3a2d - nanopb: d4d75c12cd1316f4a64e3c6963f879ecd4b5e0d5 + nanopb: 438bc412db1928dac798aa6fd75726007be04262 onnxruntime: e9346181d75b8dea8733bdae512a22c298962e00 onnxruntime-c: ebdcfd8650bcbd10121c125262f99dea681b92a3 onnxruntime-objc: ae7acec7a3d03eaf072d340afed7a35635c1c2a6 @@ -434,30 +455,30 @@ SPEC CHECKSUMS: OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85 path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c - permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 + permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604 - PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4 - ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 - receive_sharing_intent: 9ca20ae908f83c36ddaaaa8c9bd30ce4700495e8 + PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 + ReachabilitySwift: 5ae15e16814b5f9ef568963fb2c87aeb49158c66 + receive_sharing_intent: 6837b01768e567fe8562182397bf43d63d8c6437 screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625 - SDWebImage: a3ba0b8faac7228c3c8eadd1a55c9c9fe5e16457 + SDWebImage: 40b0b4053e36c660a764958bff99eed16610acbb SDWebImageWebPCoder: c94f09adbca681822edad9e532ac752db713eabf - Sentry: 8984a4ffb2b9bd2894d74fb36e6f5833865bc18e - sentry_flutter: c87a0556eeb6cbf7f9f924d30e878bdedf22d364 - SentryPrivate: 2f0c9ba4c3fc993f70eab6ca95673509561e0085 - share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68 + Sentry: ebc12276bd17613a114ab359074096b6b3725203 + sentry_flutter: 88ebea3f595b0bc16acc5bedacafe6d60c12dcd5 + SentryPrivate: d651efb234cf385ec9a1cdd3eff94b5e78a0e0fe + share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5 shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec sqlite3: 73b7fc691fdc43277614250e04d183740cb15078 sqlite3_flutter_libs: af0e8fe9bce48abddd1ffdbbf839db0302d72d80 - Toast: ec33c32b8688982cecc6348adeae667c1b9938da + Toast: 1f5ea13423a1e6674c4abdac5be53587ae481c4e uni_links: d97da20c7701486ba192624d99bffaaffcfc298a - url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812 - video_player_avfoundation: e9e6f9cae7d7a6d9b43519b0aab382bca60fcfd1 + url_launcher_ios: 6116280ddcfe98ab8820085d8d76ae7449447586 + video_player_avfoundation: 2b4384f3b157206b5e150a0083cdc0c905d260d3 video_thumbnail: c4e2a3c539e247d4de13cd545344fd2d26ffafd1 volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9 wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47 PODFILE CHECKSUM: c1a8f198a245ed1f10e40b617efdb129b021b225 -COCOAPODS: 1.14.3 +COCOAPODS: 1.15.2 diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index 2ddddfd9f..72c5ef5cf 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -174,6 +174,7 @@ 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, ABF2FD2FD606DC6DD54BD9AB /* [CP] Embed Pods Frameworks */, + F5BF2E85B889CF8483C26F35 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -191,7 +192,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1520; - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = "The Chromium Authors"; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -292,6 +293,7 @@ "${BUILT_PRODUCTS_DIR}/Toast/Toast.framework", "${BUILT_PRODUCTS_DIR}/background_fetch/background_fetch.framework", "${BUILT_PRODUCTS_DIR}/battery_info/battery_info.framework", + "${BUILT_PRODUCTS_DIR}/bonsoir_darwin/bonsoir_darwin.framework", "${BUILT_PRODUCTS_DIR}/connectivity_plus/connectivity_plus.framework", "${BUILT_PRODUCTS_DIR}/device_info_plus/device_info_plus.framework", "${BUILT_PRODUCTS_DIR}/file_saver/file_saver.framework", @@ -310,6 +312,7 @@ "${BUILT_PRODUCTS_DIR}/integration_test/integration_test.framework", "${BUILT_PRODUCTS_DIR}/isar_flutter_libs/isar_flutter_libs.framework", "${BUILT_PRODUCTS_DIR}/libwebp/libwebp.framework", + "${BUILT_PRODUCTS_DIR}/local_auth_darwin/local_auth_darwin.framework", "${BUILT_PRODUCTS_DIR}/local_auth_ios/local_auth_ios.framework", "${BUILT_PRODUCTS_DIR}/media_extension/media_extension.framework", "${BUILT_PRODUCTS_DIR}/media_kit_libs_ios_video/media_kit_libs_ios_video.framework", @@ -374,6 +377,7 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Toast.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/background_fetch.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/battery_info.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/bonsoir_darwin.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/connectivity_plus.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/device_info_plus.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_saver.framework", @@ -392,6 +396,7 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/integration_test.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/isar_flutter_libs.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libwebp.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/local_auth_darwin.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/local_auth_ios.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/media_extension.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/media_kit_libs_ios_video.framework", @@ -464,6 +469,24 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; + F5BF2E85B889CF8483C26F35 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh", + "${PODS_CONFIGURATION_BUILD_DIR}/permission_handler_apple/permission_handler_apple_privacy.bundle", + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/permission_handler_apple_privacy.bundle", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ diff --git a/mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index a6b826db2..5e31d3d34 100644 --- a/mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ existingPathIds = await getDevicePathIDs(); for (Tuple2 tup in devicePathInfo) { final AssetPathEntity pathEntity = tup.item1; + final assetCount = await pathEntity.assetCountAsync; final String localID = tup.item2; final bool shouldUpdate = existingPathIds.contains(pathEntity.id); if (shouldUpdate) { @@ -190,11 +191,11 @@ extension DeviceFiles on FilesDB { [ pathEntity.name, localID, - await pathEntity.assetCountAsync, + assetCount, pathEntity.id, pathEntity.name, localID, - await pathEntity.assetCountAsync, + assetCount, ], ); if (rowUpdated > 0) { @@ -208,7 +209,7 @@ extension DeviceFiles on FilesDB { { "id": pathEntity.id, "name": pathEntity.name, - "count": await pathEntity.assetCountAsync, + "count": assetCount, "cover_id": localID, "should_backup": shouldBackup ? _sqlBoolTrue : _sqlBoolFalse, }, diff --git a/mobile/lib/models/location/location.freezed.dart b/mobile/lib/models/location/location.freezed.dart index e3cc1a19d..135377e74 100644 --- a/mobile/lib/models/location/location.freezed.dart +++ b/mobile/lib/models/location/location.freezed.dart @@ -12,7 +12,7 @@ part of 'location.dart'; T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); Location _$LocationFromJson(Map json) { return _Location.fromJson(json); @@ -67,21 +67,22 @@ class _$LocationCopyWithImpl<$Res, $Val extends Location> } /// @nodoc -abstract class _$$_LocationCopyWith<$Res> implements $LocationCopyWith<$Res> { - factory _$$_LocationCopyWith( - _$_Location value, $Res Function(_$_Location) then) = - __$$_LocationCopyWithImpl<$Res>; +abstract class _$$LocationImplCopyWith<$Res> + implements $LocationCopyWith<$Res> { + factory _$$LocationImplCopyWith( + _$LocationImpl value, $Res Function(_$LocationImpl) then) = + __$$LocationImplCopyWithImpl<$Res>; @override @useResult $Res call({double? latitude, double? longitude}); } /// @nodoc -class __$$_LocationCopyWithImpl<$Res> - extends _$LocationCopyWithImpl<$Res, _$_Location> - implements _$$_LocationCopyWith<$Res> { - __$$_LocationCopyWithImpl( - _$_Location _value, $Res Function(_$_Location) _then) +class __$$LocationImplCopyWithImpl<$Res> + extends _$LocationCopyWithImpl<$Res, _$LocationImpl> + implements _$$LocationImplCopyWith<$Res> { + __$$LocationImplCopyWithImpl( + _$LocationImpl _value, $Res Function(_$LocationImpl) _then) : super(_value, _then); @pragma('vm:prefer-inline') @@ -90,7 +91,7 @@ class __$$_LocationCopyWithImpl<$Res> Object? latitude = freezed, Object? longitude = freezed, }) { - return _then(_$_Location( + return _then(_$LocationImpl( latitude: freezed == latitude ? _value.latitude : latitude // ignore: cast_nullable_to_non_nullable @@ -105,11 +106,11 @@ class __$$_LocationCopyWithImpl<$Res> /// @nodoc @JsonSerializable() -class _$_Location implements _Location { - const _$_Location({required this.latitude, required this.longitude}); +class _$LocationImpl implements _Location { + const _$LocationImpl({required this.latitude, required this.longitude}); - factory _$_Location.fromJson(Map json) => - _$$_LocationFromJson(json); + factory _$LocationImpl.fromJson(Map json) => + _$$LocationImplFromJson(json); @override final double? latitude; @@ -122,10 +123,10 @@ class _$_Location implements _Location { } @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is _$_Location && + other is _$LocationImpl && (identical(other.latitude, latitude) || other.latitude == latitude) && (identical(other.longitude, longitude) || @@ -139,12 +140,12 @@ class _$_Location implements _Location { @JsonKey(ignore: true) @override @pragma('vm:prefer-inline') - _$$_LocationCopyWith<_$_Location> get copyWith => - __$$_LocationCopyWithImpl<_$_Location>(this, _$identity); + _$$LocationImplCopyWith<_$LocationImpl> get copyWith => + __$$LocationImplCopyWithImpl<_$LocationImpl>(this, _$identity); @override Map toJson() { - return _$$_LocationToJson( + return _$$LocationImplToJson( this, ); } @@ -153,9 +154,10 @@ class _$_Location implements _Location { abstract class _Location implements Location { const factory _Location( {required final double? latitude, - required final double? longitude}) = _$_Location; + required final double? longitude}) = _$LocationImpl; - factory _Location.fromJson(Map json) = _$_Location.fromJson; + factory _Location.fromJson(Map json) = + _$LocationImpl.fromJson; @override double? get latitude; @@ -163,6 +165,6 @@ abstract class _Location implements Location { double? get longitude; @override @JsonKey(ignore: true) - _$$_LocationCopyWith<_$_Location> get copyWith => + _$$LocationImplCopyWith<_$LocationImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/mobile/lib/models/location/location.g.dart b/mobile/lib/models/location/location.g.dart index fe91798f9..39f546d68 100644 --- a/mobile/lib/models/location/location.g.dart +++ b/mobile/lib/models/location/location.g.dart @@ -6,12 +6,13 @@ part of 'location.dart'; // JsonSerializableGenerator // ************************************************************************** -_$_Location _$$_LocationFromJson(Map json) => _$_Location( +_$LocationImpl _$$LocationImplFromJson(Map json) => + _$LocationImpl( latitude: (json['latitude'] as num?)?.toDouble(), longitude: (json['longitude'] as num?)?.toDouble(), ); -Map _$$_LocationToJson(_$_Location instance) => +Map _$$LocationImplToJson(_$LocationImpl instance) => { 'latitude': instance.latitude, 'longitude': instance.longitude, diff --git a/mobile/lib/models/location_tag/location_tag.freezed.dart b/mobile/lib/models/location_tag/location_tag.freezed.dart index 2c0f795b6..fcec42492 100644 --- a/mobile/lib/models/location_tag/location_tag.freezed.dart +++ b/mobile/lib/models/location_tag/location_tag.freezed.dart @@ -12,7 +12,7 @@ part of 'location_tag.dart'; T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); LocationTag _$LocationTagFromJson(Map json) { return _LocationTag.fromJson(json); @@ -101,11 +101,11 @@ class _$LocationTagCopyWithImpl<$Res, $Val extends LocationTag> } /// @nodoc -abstract class _$$_LocationTagCopyWith<$Res> +abstract class _$$LocationTagImplCopyWith<$Res> implements $LocationTagCopyWith<$Res> { - factory _$$_LocationTagCopyWith( - _$_LocationTag value, $Res Function(_$_LocationTag) then) = - __$$_LocationTagCopyWithImpl<$Res>; + factory _$$LocationTagImplCopyWith( + _$LocationTagImpl value, $Res Function(_$LocationTagImpl) then) = + __$$LocationTagImplCopyWithImpl<$Res>; @override @useResult $Res call( @@ -120,11 +120,11 @@ abstract class _$$_LocationTagCopyWith<$Res> } /// @nodoc -class __$$_LocationTagCopyWithImpl<$Res> - extends _$LocationTagCopyWithImpl<$Res, _$_LocationTag> - implements _$$_LocationTagCopyWith<$Res> { - __$$_LocationTagCopyWithImpl( - _$_LocationTag _value, $Res Function(_$_LocationTag) _then) +class __$$LocationTagImplCopyWithImpl<$Res> + extends _$LocationTagCopyWithImpl<$Res, _$LocationTagImpl> + implements _$$LocationTagImplCopyWith<$Res> { + __$$LocationTagImplCopyWithImpl( + _$LocationTagImpl _value, $Res Function(_$LocationTagImpl) _then) : super(_value, _then); @pragma('vm:prefer-inline') @@ -136,7 +136,7 @@ class __$$_LocationTagCopyWithImpl<$Res> Object? bSquare = null, Object? centerPoint = null, }) { - return _then(_$_LocationTag( + return _then(_$LocationTagImpl( name: null == name ? _value.name : name // ignore: cast_nullable_to_non_nullable @@ -163,8 +163,8 @@ class __$$_LocationTagCopyWithImpl<$Res> /// @nodoc @JsonSerializable() -class _$_LocationTag extends _LocationTag { - const _$_LocationTag( +class _$LocationTagImpl extends _LocationTag { + const _$LocationTagImpl( {required this.name, required this.radius, required this.aSquare, @@ -172,8 +172,8 @@ class _$_LocationTag extends _LocationTag { required this.centerPoint}) : super._(); - factory _$_LocationTag.fromJson(Map json) => - _$$_LocationTagFromJson(json); + factory _$LocationTagImpl.fromJson(Map json) => + _$$LocationTagImplFromJson(json); @override final String name; @@ -192,10 +192,10 @@ class _$_LocationTag extends _LocationTag { } @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is _$_LocationTag && + other is _$LocationTagImpl && (identical(other.name, name) || other.name == name) && (identical(other.radius, radius) || other.radius == radius) && (identical(other.aSquare, aSquare) || other.aSquare == aSquare) && @@ -212,12 +212,12 @@ class _$_LocationTag extends _LocationTag { @JsonKey(ignore: true) @override @pragma('vm:prefer-inline') - _$$_LocationTagCopyWith<_$_LocationTag> get copyWith => - __$$_LocationTagCopyWithImpl<_$_LocationTag>(this, _$identity); + _$$LocationTagImplCopyWith<_$LocationTagImpl> get copyWith => + __$$LocationTagImplCopyWithImpl<_$LocationTagImpl>(this, _$identity); @override Map toJson() { - return _$$_LocationTagToJson( + return _$$LocationTagImplToJson( this, ); } @@ -229,11 +229,11 @@ abstract class _LocationTag extends LocationTag { required final double radius, required final double aSquare, required final double bSquare, - required final Location centerPoint}) = _$_LocationTag; + required final Location centerPoint}) = _$LocationTagImpl; const _LocationTag._() : super._(); factory _LocationTag.fromJson(Map json) = - _$_LocationTag.fromJson; + _$LocationTagImpl.fromJson; @override String get name; @@ -247,6 +247,6 @@ abstract class _LocationTag extends LocationTag { Location get centerPoint; @override @JsonKey(ignore: true) - _$$_LocationTagCopyWith<_$_LocationTag> get copyWith => + _$$LocationTagImplCopyWith<_$LocationTagImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/mobile/lib/models/location_tag/location_tag.g.dart b/mobile/lib/models/location_tag/location_tag.g.dart index 2f159bf99..06ccad402 100644 --- a/mobile/lib/models/location_tag/location_tag.g.dart +++ b/mobile/lib/models/location_tag/location_tag.g.dart @@ -6,8 +6,8 @@ part of 'location_tag.dart'; // JsonSerializableGenerator // ************************************************************************** -_$_LocationTag _$$_LocationTagFromJson(Map json) => - _$_LocationTag( +_$LocationTagImpl _$$LocationTagImplFromJson(Map json) => + _$LocationTagImpl( name: json['name'] as String, radius: (json['radius'] as num).toDouble(), aSquare: (json['aSquare'] as num).toDouble(), @@ -16,7 +16,7 @@ _$_LocationTag _$$_LocationTagFromJson(Map json) => Location.fromJson(json['centerPoint'] as Map), ); -Map _$$_LocationTagToJson(_$_LocationTag instance) => +Map _$$LocationTagImplToJson(_$LocationTagImpl instance) => { 'name': instance.name, 'radius': instance.radius, diff --git a/mobile/lib/models/typedefs.dart b/mobile/lib/models/typedefs.dart index d358180da..284d3a244 100644 --- a/mobile/lib/models/typedefs.dart +++ b/mobile/lib/models/typedefs.dart @@ -18,3 +18,4 @@ typedef VoidCallbackParamSearchResutlsStream = void Function( typedef FutureVoidCallback = Future Function(); typedef FutureOrVoidCallback = FutureOr Function(); typedef FutureVoidCallbackParamStr = Future Function(String); +typedef FutureVoidCallbackParamBool = Future Function(bool); diff --git a/mobile/lib/services/update_service.dart b/mobile/lib/services/update_service.dart index 21a2c59bc..28c5732c8 100644 --- a/mobile/lib/services/update_service.dart +++ b/mobile/lib/services/update_service.dart @@ -16,7 +16,7 @@ class UpdateService { static final UpdateService instance = UpdateService._privateConstructor(); static const kUpdateAvailableShownTimeKey = "update_available_shown_time_key"; static const changeLogVersionKey = "update_change_log_key"; - static const currentChangeLogVersion = 17; + static const currentChangeLogVersion = 18; LatestVersionInfo? _latestVersion; final _logger = Logger("UpdateService"); diff --git a/mobile/lib/ui/account/email_entry_page.dart b/mobile/lib/ui/account/email_entry_page.dart index e46e8fa7e..143f593ef 100644 --- a/mobile/lib/ui/account/email_entry_page.dart +++ b/mobile/lib/ui/account/email_entry_page.dart @@ -148,7 +148,9 @@ class _EmailEntryPageState extends State { style: Theme.of(context).textTheme.titleMedium, autofillHints: const [AutofillHints.email], decoration: InputDecoration( - fillColor: _emailIsValid ? _validFieldValueColor : null, + fillColor: _emailIsValid + ? _validFieldValueColor + : getEnteColorScheme(context).fillFaint, filled: true, hintText: S.of(context).email, contentPadding: const EdgeInsets.symmetric( @@ -195,8 +197,9 @@ class _EmailEntryPageState extends State { enableSuggestions: true, autofillHints: const [AutofillHints.newPassword], decoration: InputDecoration( - fillColor: - _passwordIsValid ? _validFieldValueColor : null, + fillColor: _passwordIsValid + ? _validFieldValueColor + : getEnteColorScheme(context).fillFaint, filled: true, hintText: S.of(context).password, contentPadding: const EdgeInsets.symmetric( @@ -265,7 +268,7 @@ class _EmailEntryPageState extends State { decoration: InputDecoration( fillColor: _passwordsMatch && _passwordIsValid ? _validFieldValueColor - : null, + : getEnteColorScheme(context).fillFaint, filled: true, hintText: S.of(context).confirmPassword, contentPadding: const EdgeInsets.symmetric( @@ -343,7 +346,7 @@ class _EmailEntryPageState extends State { child: TextFormField( style: Theme.of(context).textTheme.titleMedium, decoration: InputDecoration( - fillColor: null, + fillColor: getEnteColorScheme(context).fillFaint, filled: true, contentPadding: const EdgeInsets.symmetric( horizontal: 16, @@ -375,7 +378,10 @@ class _EmailEntryPageState extends State { textInputAction: TextInputAction.next, ), ), - const Divider(thickness: 1), + Divider( + thickness: 1, + color: getEnteColorScheme(context).strokeFaint, + ), const SizedBox(height: 12), _getAgreement(), const SizedBox(height: 40), diff --git a/mobile/lib/ui/account/login_page.dart b/mobile/lib/ui/account/login_page.dart index 2d620ecb2..7d79dc856 100644 --- a/mobile/lib/ui/account/login_page.dart +++ b/mobile/lib/ui/account/login_page.dart @@ -8,6 +8,7 @@ import "package:photos/generated/l10n.dart"; import "package:photos/l10n/l10n.dart"; import "package:photos/models/api/user/srp.dart"; import 'package:photos/services/user_service.dart'; +import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/account/login_pwd_verification_page.dart"; import 'package:photos/ui/common/dynamic_fab.dart'; import 'package:photos/ui/common/web_page.dart'; @@ -28,18 +29,18 @@ class _LoginPageState extends State { final Logger _logger = Logger('_LoginPageState'); @override - void initState() { + void didChangeDependencies() { + super.didChangeDependencies(); if ((_config.getEmail() ?? '').isNotEmpty) { updateEmail(_config.getEmail()!); } else if (kDebugMode) { updateEmail(const String.fromEnvironment("email")); } - super.initState(); } @override Widget build(BuildContext context) { - final isKeypadOpen = MediaQuery.of(context).viewInsets.bottom > 100; + final isKeypadOpen = MediaQuery.viewInsetsOf(context).bottom > 100; FloatingActionButtonLocation? fabLocation() { if (isKeypadOpen) { @@ -159,10 +160,11 @@ class _LoginPageState extends State { autofocus: true, ), ), - const Padding( - padding: EdgeInsets.symmetric(vertical: 18), + Padding( + padding: const EdgeInsets.symmetric(vertical: 18), child: Divider( thickness: 1, + color: getEnteColorScheme(context).strokeFaint, ), ), Padding( @@ -235,7 +237,7 @@ class _LoginPageState extends State { if (_emailIsValid) { _emailInputFieldColor = const Color.fromRGBO(45, 194, 98, 0.2); } else { - _emailInputFieldColor = null; + _emailInputFieldColor = getEnteColorScheme(context).fillFaint; } } } diff --git a/mobile/lib/ui/account/login_pwd_verification_page.dart b/mobile/lib/ui/account/login_pwd_verification_page.dart index a253b1585..ac974c20d 100644 --- a/mobile/lib/ui/account/login_pwd_verification_page.dart +++ b/mobile/lib/ui/account/login_pwd_verification_page.dart @@ -251,6 +251,7 @@ class _LoginPasswordVerificationPageState borderSide: BorderSide.none, borderRadius: BorderRadius.circular(6), ), + fillColor: getEnteColorScheme(context).fillFaint, suffixIcon: _passwordInFocus ? IconButton( icon: Icon( @@ -282,10 +283,11 @@ class _LoginPasswordVerificationPageState }, ), ), - const Padding( - padding: EdgeInsets.symmetric(vertical: 18), + Padding( + padding: const EdgeInsets.symmetric(vertical: 18), child: Divider( thickness: 1, + color: getEnteColorScheme(context).strokeFaint, ), ), Padding( diff --git a/mobile/lib/ui/account/ott_verification_page.dart b/mobile/lib/ui/account/ott_verification_page.dart index 77b48d360..d03861055 100644 --- a/mobile/lib/ui/account/ott_verification_page.dart +++ b/mobile/lib/ui/account/ott_verification_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:photos/ente_theme_data.dart'; import "package:photos/generated/l10n.dart"; import 'package:photos/services/user_service.dart'; +import "package:photos/theme/ente_theme.dart"; import 'package:photos/ui/common/dynamic_fab.dart'; import 'package:step_progress_indicator/step_progress_indicator.dart'; import "package:styled_text/styled_text.dart"; @@ -16,7 +17,7 @@ class OTTVerificationPage extends StatefulWidget { this.email, { this.isChangeEmail = false, this.isCreateAccountScreen = false, - this.isResetPasswordScreen = false, + this.isResetPasswordScreen = false, Key? key, }) : super(key: key); @@ -78,9 +79,11 @@ class _OTTVerificationPageState extends State { _verificationCodeController.text, ); } else { - UserService.instance - .verifyEmail(context, _verificationCodeController.text, - isResettingPasswordScreen: widget.isResetPasswordScreen,); + UserService.instance.verifyEmail( + context, + _verificationCodeController.text, + isResettingPasswordScreen: widget.isResetPasswordScreen, + ); } FocusScope.of(context).unfocus(); }, @@ -130,21 +133,21 @@ class _OTTVerificationPageState extends State { }, ), ), - widget.isResetPasswordScreen ? - Text( - S.of(context).toResetVerifyEmail, - style: Theme.of(context) - .textTheme - .titleMedium! - .copyWith(fontSize: 14), - ): - Text( - S.of(context).checkInboxAndSpamFolder, - style: Theme.of(context) - .textTheme - .titleMedium! - .copyWith(fontSize: 14), - ), + widget.isResetPasswordScreen + ? Text( + S.of(context).toResetVerifyEmail, + style: Theme.of(context) + .textTheme + .titleMedium! + .copyWith(fontSize: 14), + ) + : Text( + S.of(context).checkInboxAndSpamFolder, + style: Theme.of(context) + .textTheme + .titleMedium! + .copyWith(fontSize: 14), + ), ], ), ), @@ -168,6 +171,7 @@ class _OTTVerificationPageState extends State { borderSide: BorderSide.none, borderRadius: BorderRadius.circular(6), ), + fillColor: getEnteColorScheme(context).fillFaint, ), controller: _verificationCodeController, autofocus: false, @@ -178,8 +182,9 @@ class _OTTVerificationPageState extends State { }, ), ), - const Divider( + Divider( thickness: 1, + color: getEnteColorScheme(context).strokeFaint, ), Padding( padding: const EdgeInsets.all(20), diff --git a/mobile/lib/ui/account/password_entry_page.dart b/mobile/lib/ui/account/password_entry_page.dart index a5099e333..54b7af850 100644 --- a/mobile/lib/ui/account/password_entry_page.dart +++ b/mobile/lib/ui/account/password_entry_page.dart @@ -12,6 +12,7 @@ import "package:photos/generated/l10n.dart"; import "package:photos/l10n/l10n.dart"; import "package:photos/models/key_gen_result.dart"; import 'package:photos/services/user_service.dart'; +import "package:photos/theme/ente_theme.dart"; import 'package:photos/ui/account/recovery_key_page.dart'; import 'package:photos/ui/common/dynamic_fab.dart'; import 'package:photos/ui/common/web_page.dart'; @@ -218,8 +219,9 @@ class _PasswordEntryPageState extends State { child: TextFormField( autofillHints: const [AutofillHints.newPassword], decoration: InputDecoration( - fillColor: - _isPasswordValid ? _validFieldValueColor : null, + fillColor: _isPasswordValid + ? _validFieldValueColor + : getEnteColorScheme(context).fillFaint, filled: true, hintText: S.of(context).password, contentPadding: const EdgeInsets.all(20), @@ -282,7 +284,9 @@ class _PasswordEntryPageState extends State { autofillHints: const [AutofillHints.newPassword], onEditingComplete: () => TextInput.finishAutofillContext(), decoration: InputDecoration( - fillColor: _passwordsMatch ? _validFieldValueColor : null, + fillColor: _passwordsMatch + ? _validFieldValueColor + : getEnteColorScheme(context).fillFaint, filled: true, hintText: S.of(context).confirmPassword, contentPadding: const EdgeInsets.symmetric( diff --git a/mobile/lib/ui/account/password_reentry_page.dart b/mobile/lib/ui/account/password_reentry_page.dart index 1b240134f..d3f6be564 100644 --- a/mobile/lib/ui/account/password_reentry_page.dart +++ b/mobile/lib/ui/account/password_reentry_page.dart @@ -9,6 +9,7 @@ import 'package:photos/core/event_bus.dart'; import 'package:photos/events/subscription_purchased_event.dart'; import "package:photos/generated/l10n.dart"; import "package:photos/services/user_service.dart"; +import "package:photos/theme/ente_theme.dart"; import 'package:photos/ui/account/recovery_page.dart'; import 'package:photos/ui/common/dynamic_fab.dart'; import 'package:photos/ui/components/buttons/button_widget.dart'; @@ -221,6 +222,7 @@ class _PasswordReentryPageState extends State { decoration: InputDecoration( hintText: S.of(context).enterYourPassword, filled: true, + fillColor: getEnteColorScheme(context).fillFaint, contentPadding: const EdgeInsets.all(20), border: UnderlineInputBorder( borderSide: BorderSide.none, @@ -257,10 +259,11 @@ class _PasswordReentryPageState extends State { }, ), ), - const Padding( - padding: EdgeInsets.symmetric(vertical: 18), + Padding( + padding: const EdgeInsets.symmetric(vertical: 18), child: Divider( thickness: 1, + color: getEnteColorScheme(context).strokeFaint, ), ), Padding( diff --git a/mobile/lib/ui/account/recovery_key_page.dart b/mobile/lib/ui/account/recovery_key_page.dart index 6b4a11624..9ea310709 100644 --- a/mobile/lib/ui/account/recovery_key_page.dart +++ b/mobile/lib/ui/account/recovery_key_page.dart @@ -11,7 +11,7 @@ import 'package:photos/ente_theme_data.dart'; import "package:photos/generated/l10n.dart"; import 'package:photos/ui/common/gradient_button.dart'; import 'package:photos/utils/toast_util.dart'; -import 'package:share_plus/share_plus.dart'; +import "package:share_plus/share_plus.dart"; import 'package:step_progress_indicator/step_progress_indicator.dart'; class RecoveryKeyPage extends StatefulWidget { @@ -248,7 +248,8 @@ class _RecoveryKeyPageState extends State { await _recoveryKeyFile.delete(); } _recoveryKeyFile.writeAsStringSync(recoveryKey); - await Share.shareFiles([_recoveryKeyFile.path]); + + await Share.shareXFiles([XFile(_recoveryKeyFile.path)]); Future.delayed(const Duration(milliseconds: 500), () { if (mounted) { setState(() { diff --git a/mobile/lib/ui/account/recovery_page.dart b/mobile/lib/ui/account/recovery_page.dart index 97d3bfc33..4b3d49995 100644 --- a/mobile/lib/ui/account/recovery_page.dart +++ b/mobile/lib/ui/account/recovery_page.dart @@ -3,6 +3,7 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:photos/core/configuration.dart'; import "package:photos/generated/l10n.dart"; +import "package:photos/theme/ente_theme.dart"; import 'package:photos/ui/account/password_entry_page.dart'; import 'package:photos/ui/common/dynamic_fab.dart'; import 'package:photos/utils/dialog_util.dart'; @@ -102,6 +103,7 @@ class _RecoveryPageState extends State { child: TextFormField( decoration: InputDecoration( filled: true, + fillColor: getEnteColorScheme(context).fillFaint, hintText: S.of(context).enterYourRecoveryKey, contentPadding: const EdgeInsets.all(20), border: UnderlineInputBorder( @@ -123,10 +125,11 @@ class _RecoveryPageState extends State { }, ), ), - const Padding( - padding: EdgeInsets.symmetric(vertical: 18), + Padding( + padding: const EdgeInsets.symmetric(vertical: 18), child: Divider( thickness: 1, + color: getEnteColorScheme(context).strokeFaint, ), ), Row( diff --git a/mobile/lib/ui/account/request_pwd_verification_page.dart b/mobile/lib/ui/account/request_pwd_verification_page.dart index 67908f734..e29d56886 100644 --- a/mobile/lib/ui/account/request_pwd_verification_page.dart +++ b/mobile/lib/ui/account/request_pwd_verification_page.dart @@ -174,6 +174,7 @@ class _RequestPasswordVerificationPageState decoration: InputDecoration( hintText: context.l10n.enterYourPassword, filled: true, + fillColor: getEnteColorScheme(context).fillFaint, contentPadding: const EdgeInsets.all(20), border: UnderlineInputBorder( borderSide: BorderSide.none, @@ -210,10 +211,11 @@ class _RequestPasswordVerificationPageState }, ), ), - const Padding( - padding: EdgeInsets.symmetric(vertical: 18), + Padding( + padding: const EdgeInsets.symmetric(vertical: 18), child: Divider( thickness: 1, + color: getEnteColorScheme(context).strokeFaint, ), ), ], diff --git a/mobile/lib/ui/account/sessions_page.dart b/mobile/lib/ui/account/sessions_page.dart index e4468c5e2..603d95135 100644 --- a/mobile/lib/ui/account/sessions_page.dart +++ b/mobile/lib/ui/account/sessions_page.dart @@ -5,6 +5,7 @@ import 'package:photos/ente_theme_data.dart'; import "package:photos/generated/l10n.dart"; import 'package:photos/models/sessions.dart'; import 'package:photos/services/user_service.dart'; +import "package:photos/theme/ente_theme.dart"; import 'package:photos/ui/common/loading_widget.dart'; import "package:photos/utils/date_time_util.dart"; import 'package:photos/utils/dialog_util.dart'; @@ -106,7 +107,9 @@ class _SessionsPageState extends State { ), ), ), - const Divider(), + Divider( + color: getEnteColorScheme(context).strokeFaint, + ), ], ); } diff --git a/mobile/lib/ui/account/verify_recovery_page.dart b/mobile/lib/ui/account/verify_recovery_page.dart index 063d5f4b1..f4acc4738 100644 --- a/mobile/lib/ui/account/verify_recovery_page.dart +++ b/mobile/lib/ui/account/verify_recovery_page.dart @@ -11,6 +11,7 @@ import "package:photos/generated/l10n.dart"; import 'package:photos/services/local_authentication_service.dart'; import 'package:photos/services/user_remote_flag_service.dart'; import 'package:photos/services/user_service.dart'; +import "package:photos/theme/ente_theme.dart"; import 'package:photos/ui/account/recovery_key_page.dart'; import 'package:photos/ui/common/gradient_button.dart'; import 'package:photos/ui/components/buttons/button_widget.dart'; @@ -162,6 +163,7 @@ class _VerifyRecoveryPageState extends State { TextFormField( decoration: InputDecoration( filled: true, + fillColor: getEnteColorScheme(context).fillFaint, hintText: S.of(context).enterYourRecoveryKey, contentPadding: const EdgeInsets.all(20), border: UnderlineInputBorder( diff --git a/mobile/lib/ui/components/toggle_switch_widget.dart b/mobile/lib/ui/components/toggle_switch_widget.dart index 33477ae9b..de6507c1f 100644 --- a/mobile/lib/ui/components/toggle_switch_widget.dart +++ b/mobile/lib/ui/components/toggle_switch_widget.dart @@ -1,3 +1,6 @@ +import "dart:io"; + +import "package:flutter/cupertino.dart"; import 'package:flutter/material.dart'; import 'package:photos/ente_theme_data.dart'; import 'package:photos/models/execution_states.dart'; @@ -25,8 +28,8 @@ class _ToggleSwitchWidgetState extends State { @override void initState() { - toggleValue = widget.value.call(); super.initState(); + toggleValue = widget.value.call(); } @override @@ -49,54 +52,23 @@ class _ToggleSwitchWidgetState extends State { height: 31, child: FittedBox( fit: BoxFit.contain, - child: Switch.adaptive( - activeColor: enteColorScheme.primary400, - inactiveTrackColor: enteColorScheme.fillMuted, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - value: toggleValue ?? false, - onChanged: (negationOfToggleValue) async { - setState(() { - toggleValue = negationOfToggleValue; - //start showing inProgress statu icons if toggle takes more than debounce time - _debouncer.run( - () => Future( - () { - setState(() { - executionState = ExecutionState.inProgress; - }); - }, + child: Platform.isAndroid + ? Switch( + inactiveTrackColor: Colors.transparent, + activeTrackColor: enteColorScheme.primary500, + activeColor: Colors.white, + inactiveThumbColor: enteColorScheme.primary500, + trackOutlineColor: MaterialStateColor.resolveWith( + (states) => enteColorScheme.primary500, ), - ); - }); - final Stopwatch stopwatch = Stopwatch()..start(); - await widget.onChanged.call().onError( - (error, stackTrace) => _debouncer.cancelDebounce(), - ); - //for toggle feedback on short unsuccessful onChanged - await _feedbackOnUnsuccessfulToggle(stopwatch); - //debouncer gets canceled if onChanged takes less than debounce time - _debouncer.cancelDebounce(); - - final newValue = widget.value.call(); - setState(() { - if (toggleValue == newValue) { - if (executionState == ExecutionState.inProgress) { - executionState = ExecutionState.successful; - Future.delayed(const Duration(seconds: 2), () { - if (mounted) { - setState(() { - executionState = ExecutionState.idle; - }); - } - }); - } - } else { - toggleValue = !toggleValue!; - executionState = ExecutionState.idle; - } - }); - }, - ), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + value: toggleValue ?? false, + onChanged: _onChanged, + ) + : CupertinoSwitch( + value: toggleValue ?? false, + onChanged: _onChanged, + ), ), ), ], @@ -132,4 +104,47 @@ class _ToggleSwitchWidgetState extends State { ); } } + + Future _onChanged(bool negationOfToggleValue) async { + setState(() { + toggleValue = negationOfToggleValue; + //start showing inProgress statu icons if toggle takes more than debounce time + _debouncer.run( + () => Future( + () { + setState(() { + executionState = ExecutionState.inProgress; + }); + }, + ), + ); + }); + final Stopwatch stopwatch = Stopwatch()..start(); + await widget.onChanged.call().onError( + (error, stackTrace) => _debouncer.cancelDebounce(), + ); + //for toggle feedback on short unsuccessful onChanged + await _feedbackOnUnsuccessfulToggle(stopwatch); + //debouncer gets canceled if onChanged takes less than debounce time + _debouncer.cancelDebounce(); + + final newValue = widget.value.call(); + setState(() { + if (toggleValue == newValue) { + if (executionState == ExecutionState.inProgress) { + executionState = ExecutionState.successful; + Future.delayed(const Duration(seconds: 2), () { + if (mounted) { + setState(() { + executionState = ExecutionState.idle; + }); + } + }); + } + } else { + toggleValue = !toggleValue!; + executionState = ExecutionState.idle; + } + }); + } } diff --git a/mobile/lib/ui/home/status_bar_widget.dart b/mobile/lib/ui/home/status_bar_widget.dart index e730fbd63..8df1a9024 100644 --- a/mobile/lib/ui/home/status_bar_widget.dart +++ b/mobile/lib/ui/home/status_bar_widget.dart @@ -10,6 +10,7 @@ import 'package:photos/events/sync_status_update_event.dart'; import "package:photos/generated/l10n.dart"; import 'package:photos/services/sync_service.dart'; import 'package:photos/services/user_remote_flag_service.dart'; +import "package:photos/theme/ente_theme.dart"; import 'package:photos/theme/text_style.dart'; import 'package:photos/ui/account/verify_recovery_page.dart'; import 'package:photos/ui/components/home_header_widget.dart'; @@ -93,8 +94,9 @@ class _StatusBarWidgetState extends State { : const Text("ente", style: brandStyleMedium), ), _showErrorBanner - ? const Divider( + ? Divider( height: 8, + color: getEnteColorScheme(context).strokeFaint, ) : const SizedBox.shrink(), _showErrorBanner diff --git a/mobile/lib/ui/notification/update/change_log_page.dart b/mobile/lib/ui/notification/update/change_log_page.dart index 8cf629cd6..289d84590 100644 --- a/mobile/lib/ui/notification/update/change_log_page.dart +++ b/mobile/lib/ui/notification/update/change_log_page.dart @@ -122,14 +122,18 @@ class _ChangeLogPageState extends State { final List items = []; items.addAll([ ChangeLogEntry( - "Share an Album to Multiple Contacts at Once", - 'Adding multiple viewers and collaborators just got easier!\n' - '\nYou can now select multiple contacts and add all of them at once.', + "Improved Performance for Large Galleries ✨", + 'We\'ve made significant improvements to how quickly galleries load and' + ' with less stutter, especially for those with a lot of photos and videos.', ), ChangeLogEntry( - "Bug Fixes and Performance Improvements", - 'Many a bugs were squashed in this release and have improved performance on app start.\n' - '\nIf you run into any bugs, please write to team@ente.io, or let us know on Discord! 🙏', + "Enhanced Functionality for Video Backups", + 'Even if video backups are disabled, you can now manually upload individual videos.', + ), + ChangeLogEntry( + "Bug Fixes", + 'Many a bugs were squashed in this release.\n' + '\nIf you run into any, please write to team@ente.io, or let us know on Discord! 🙏', ), ]); diff --git a/mobile/lib/ui/payment/store_subscription_page.dart b/mobile/lib/ui/payment/store_subscription_page.dart index 4bcdd4d5b..3925bf017 100644 --- a/mobile/lib/ui/payment/store_subscription_page.dart +++ b/mobile/lib/ui/payment/store_subscription_page.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:io'; +import "package:flutter/cupertino.dart"; import "package:flutter/foundation.dart"; import 'package:flutter/material.dart'; import 'package:in_app_purchase/in_app_purchase.dart'; @@ -385,43 +386,49 @@ class _StoreSubscriptionPageState extends State { } Widget _showSubscriptionToggle() { - Widget planText(String title, bool reduceOpacity) { - return Padding( - padding: const EdgeInsets.only(left: 4, right: 4), - child: Text( - title, - style: TextStyle( - color: Theme.of(context) - .colorScheme - .onSurface - .withOpacity(reduceOpacity ? 0.5 : 1.0), - ), - ), - ); - } - return Container( padding: const EdgeInsets.only(left: 8, right: 8, top: 2, bottom: 2), margin: const EdgeInsets.only(bottom: 6), child: Column( children: [ RepaintBoundary( - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - planText(S.of(context).monthly, showYearlyPlan), - Switch( - value: showYearlyPlan, - activeColor: Colors.white, - inactiveThumbColor: Colors.white, - activeTrackColor: getEnteColorScheme(context).strokeMuted, - onChanged: (value) async { - showYearlyPlan = value; - await _filterStorePlansForUi(); - }, - ), - planText(S.of(context).yearly, !showYearlyPlan), - ], + child: SizedBox( + width: 250, + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: SegmentedButton( + style: SegmentedButton.styleFrom( + selectedBackgroundColor: + getEnteColorScheme(context).fillMuted, + selectedForegroundColor: + getEnteColorScheme(context).textBase, + side: BorderSide( + color: getEnteColorScheme(context).strokeMuted, + width: 1, + ), + ), + segments: >[ + ButtonSegment( + label: Text(S.of(context).monthly), + value: false, + ), + ButtonSegment( + label: Text(S.of(context).yearly), + value: true, + ), + ], + selected: {showYearlyPlan}, + onSelectionChanged: (p0) { + showYearlyPlan = p0.first; + _filterStorePlansForUi(); + }, + ), + ), + ], + ), ), ), _isFreePlanUser() && !UpdateService.instance.isPlayStoreFlavor() diff --git a/mobile/lib/ui/payment/stripe_subscription_page.dart b/mobile/lib/ui/payment/stripe_subscription_page.dart index 4a04d49d1..31694f174 100644 --- a/mobile/lib/ui/payment/stripe_subscription_page.dart +++ b/mobile/lib/ui/payment/stripe_subscription_page.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import "package:flutter/cupertino.dart"; import "package:flutter/foundation.dart"; import 'package:flutter/material.dart'; import "package:logging/logging.dart"; @@ -537,43 +538,49 @@ class _StripeSubscriptionPageState extends State { } Widget _showSubscriptionToggle() { - Widget planText(String title, bool reduceOpacity) { - return Padding( - padding: const EdgeInsets.only(left: 4, right: 4), - child: Text( - title, - style: TextStyle( - color: Theme.of(context) - .colorScheme - .onSurface - .withOpacity(reduceOpacity ? 0.5 : 1.0), - ), - ), - ); - } - return Container( padding: const EdgeInsets.only(left: 8, right: 8, top: 2, bottom: 2), margin: const EdgeInsets.only(bottom: 6), child: Column( children: [ RepaintBoundary( - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - planText(S.of(context).monthly, _showYearlyPlan), - Switch( - value: _showYearlyPlan, - activeColor: Colors.white, - inactiveThumbColor: Colors.white, - activeTrackColor: getEnteColorScheme(context).strokeMuted, - onChanged: (value) async { - _showYearlyPlan = value; - await _filterStripeForUI(); - }, - ), - planText(S.of(context).yearly, !_showYearlyPlan), - ], + child: SizedBox( + width: 250, + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: SegmentedButton( + style: SegmentedButton.styleFrom( + selectedBackgroundColor: + getEnteColorScheme(context).fillMuted, + selectedForegroundColor: + getEnteColorScheme(context).textBase, + side: BorderSide( + color: getEnteColorScheme(context).strokeMuted, + width: 1, + ), + ), + segments: >[ + ButtonSegment( + label: Text(S.of(context).monthly), + value: false, + ), + ButtonSegment( + label: Text(S.of(context).yearly), + value: true, + ), + ], + selected: {_showYearlyPlan}, + onSelectionChanged: (p0) { + _showYearlyPlan = p0.first; + _filterStripeForUI(); + }, + ), + ), + ], + ), ), ), _isFreePlanUser() && !UpdateService.instance.isPlayStoreFlavor() diff --git a/mobile/lib/ui/sharing/manage_links_widget.dart b/mobile/lib/ui/sharing/manage_links_widget.dart index 02878735e..3478d1864 100644 --- a/mobile/lib/ui/sharing/manage_links_widget.dart +++ b/mobile/lib/ui/sharing/manage_links_widget.dart @@ -15,6 +15,7 @@ import 'package:photos/ui/components/captioned_text_widget.dart'; import 'package:photos/ui/components/divider_widget.dart'; import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart'; import 'package:photos/ui/components/menu_section_description_widget.dart'; +import "package:photos/ui/components/toggle_switch_widget.dart"; import 'package:photos/ui/sharing/pickers/device_limit_picker_page.dart'; import 'package:photos/ui/sharing/pickers/link_expiry_picker_page.dart'; import 'package:photos/utils/crypto_util.dart'; @@ -78,14 +79,12 @@ class _ManageSharedLinkWidgetState extends State { ), alignCaptionedTextToLeft: true, menuItemColor: getEnteColorScheme(context).fillFaint, - trailingWidget: Switch.adaptive( - value: widget.collection!.publicURLs?.firstOrNull - ?.enableCollect ?? - false, - onChanged: (value) async { + trailingWidget: ToggleSwitchWidget( + value: () => isCollectEnabled, + onChanged: () async { await _updateUrlSettings( context, - {'enableCollect': value}, + {'enableCollect': !isCollectEnabled}, ); }, ), @@ -168,14 +167,14 @@ class _ManageSharedLinkWidgetState extends State { isBottomBorderRadiusRemoved: true, isTopBorderRadiusRemoved: true, menuItemColor: getEnteColorScheme(context).fillFaint, - trailingWidget: Switch.adaptive( - value: isDownloadEnabled, - onChanged: (value) async { + trailingWidget: ToggleSwitchWidget( + value: () => isDownloadEnabled, + onChanged: () async { await _updateUrlSettings( context, - {'enableDownload': value}, + {'enableDownload': !isDownloadEnabled}, ); - if (!value) { + if (!isDownloadEnabled) { // ignore: unawaited_futures showErrorDialog( context, @@ -198,10 +197,10 @@ class _ManageSharedLinkWidgetState extends State { alignCaptionedTextToLeft: true, isTopBorderRadiusRemoved: true, menuItemColor: getEnteColorScheme(context).fillFaint, - trailingWidget: Switch.adaptive( - value: isPasswordEnabled, - onChanged: (enablePassword) async { - if (enablePassword) { + trailingWidget: ToggleSwitchWidget( + value: () => isPasswordEnabled, + onChanged: () async { + if (!isPasswordEnabled) { // ignore: unawaited_futures showTextInputDialog( context, diff --git a/mobile/lib/ui/tabs/home_widget.dart b/mobile/lib/ui/tabs/home_widget.dart index 561b1f4ba..802b40be8 100644 --- a/mobile/lib/ui/tabs/home_widget.dart +++ b/mobile/lib/ui/tabs/home_widget.dart @@ -284,7 +284,7 @@ class _HomeWidgetState extends State { void _initMediaShareSubscription() { // For sharing images coming from outside the app while the app is in the memory _intentDataStreamSubscription = - ReceiveSharingIntent.getMediaStream().listen( + ReceiveSharingIntent.instance.getMediaStream().listen( (List value) { setState(() { _shouldRenderCreateCollectionSheet = true; @@ -296,7 +296,9 @@ class _HomeWidgetState extends State { }, ); // For sharing images coming from outside the app while the app is closed - ReceiveSharingIntent.getInitialMedia().then((List value) { + ReceiveSharingIntent.instance + .getInitialMedia() + .then((List value) { if (mounted) { setState(() { _sharedFiles = value; @@ -380,7 +382,7 @@ class _HomeWidgetState extends State { //So to stop showing multiple CreateCollectionSheets, this flag //needs to be set to false the first time it is rendered. _shouldRenderCreateCollectionSheet = false; - ReceiveSharingIntent.reset(); + ReceiveSharingIntent.instance.reset(); Future.delayed(const Duration(milliseconds: 10), () { showCollectionActionSheet( context, diff --git a/mobile/lib/ui/tabs/user_collections_tab.dart b/mobile/lib/ui/tabs/user_collections_tab.dart index e5eab8317..93e5b1982 100644 --- a/mobile/lib/ui/tabs/user_collections_tab.dart +++ b/mobile/lib/ui/tabs/user_collections_tab.dart @@ -10,6 +10,7 @@ import 'package:photos/events/user_logged_out_event.dart'; import "package:photos/generated/l10n.dart"; import 'package:photos/models/collection/collection.dart'; import 'package:photos/services/collections_service.dart'; +import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/collections/button/archived_button.dart"; import "package:photos/ui/collections/button/hidden_button.dart"; import "package:photos/ui/collections/button/trash_button.dart"; @@ -184,7 +185,11 @@ class _UserCollectionsTabState extends State ), ) : const SliverToBoxAdapter(child: SizedBox.shrink()), - const SliverToBoxAdapter(child: Divider()), + SliverToBoxAdapter( + child: Divider( + color: getEnteColorScheme(context).strokeFaint, + ), + ), const SliverToBoxAdapter(child: SizedBox(height: 12)), SliverToBoxAdapter( child: Padding( diff --git a/mobile/lib/ui/viewer/search/search_widget.dart b/mobile/lib/ui/viewer/search/search_widget.dart index c624a78b3..2beaa1ec1 100644 --- a/mobile/lib/ui/viewer/search/search_widget.dart +++ b/mobile/lib/ui/viewer/search/search_widget.dart @@ -142,6 +142,7 @@ class SearchWidgetState extends State { //TODO: Extract string hintText: "Search", filled: true, + fillColor: getEnteColorScheme(context).fillFaint, border: const UnderlineInputBorder( borderSide: BorderSide.none, ), diff --git a/mobile/lib/utils/dialog_util.dart b/mobile/lib/utils/dialog_util.dart index ae4425620..f9bd733ae 100644 --- a/mobile/lib/utils/dialog_util.dart +++ b/mobile/lib/utils/dialog_util.dart @@ -291,7 +291,6 @@ ProgressDialog createProgressDialog( context, type: ProgressDialogType.normal, isDismissible: isDismissible, - barrierColor: Colors.black12, ); dialog.style( message: message, diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index c7aa74259..ccb0775d7 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -13,18 +13,18 @@ packages: dependency: transitive description: name: _flutterfire_internals - sha256: f5628cd9c92ed11083f425fd1f8f1bc60ecdda458c81d73b143aeda036c35fe7 + sha256: "0cb43f83f36ba8cb20502dee0c205e3f3aafb751732d724aeac3f2e044212cc2" url: "https://pub.dev" source: hosted - version: "1.3.16" + version: "1.3.29" adaptive_theme: dependency: "direct main" description: name: adaptive_theme - sha256: b0c4c35b22ef8226757881fe4dce38c40a06c551bca83236022ec7613e157c83 + sha256: f4ee609b464e5efc68131d9d15ba9aa1de4e3b5ede64be17781c6e19a52d637d url: "https://pub.dev" source: hosted - version: "3.5.0" + version: "3.6.0" analyzer: dependency: transitive description: @@ -77,10 +77,10 @@ packages: dependency: transitive description: name: args - sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.5.0" async: dependency: transitive description: @@ -93,10 +93,10 @@ packages: dependency: "direct main" description: name: background_fetch - sha256: f70b28a0f7a3156195e9742229696f004ea3bf10f74039b7bf4c78a74fbda8a4 + sha256: dbffec0317ccdef6e2014cb543e147f52441e29c4fcb53dfd23558c4d92ddece url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.3.2" battery_info: dependency: "direct main" description: @@ -113,6 +113,38 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.6" + bonsoir: + dependency: transitive + description: + name: bonsoir + sha256: "800d77c0581fff06cc43ef2b7723dfe5ee9b899ab0fdf80fb1c7b8829a5deb5c" + url: "https://pub.dev" + source: hosted + version: "3.0.0+1" + bonsoir_android: + dependency: transitive + description: + name: bonsoir_android + sha256: "7207c36fd7e0f3c7c2d8cf353f02bd640d96e2387d575837f8ac051c9cbf4aa7" + url: "https://pub.dev" + source: hosted + version: "3.0.0+1" + bonsoir_darwin: + dependency: transitive + description: + name: bonsoir_darwin + sha256: "7211042c85da2d6efa80c0976bbd9568f2b63624097779847548ed4530675ade" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + bonsoir_platform_interface: + dependency: transitive + description: + name: bonsoir_platform_interface + sha256: "64d57cd52bd477b4891e9b9d419e6408da171ed9e0efc8aa716e7e343d5d93ad" + url: "https://pub.dev" + source: hosted + version: "3.0.0" boolean_selector: dependency: transitive description: @@ -157,18 +189,18 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "581bacf68f89ec8792f5e5a0b2c4decd1c948e97ce659dc783688c8a88fbec21" + sha256: "3ac61a79bfb6f6cc11f693591063a7f19a7af628dc52f141743edac5c16e8c22" url: "https://pub.dev" source: hosted - version: "2.4.8" + version: "2.4.9" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: c9e32d21dd6626b5c163d48b037ce906bbe428bc23ab77bcd77bb21e593b6185 + sha256: "4ae8ffe5ac758da294ecf1802f2aff01558d8b1b00616aa7538ea9a8a5d50799" url: "https://pub.dev" source: hosted - version: "7.2.11" + version: "7.3.0" built_collection: dependency: transitive description: @@ -181,10 +213,10 @@ packages: dependency: transitive description: name: built_value - sha256: a3ec2e0f967bc47f69f95009bb93db936288d61d5343b9436e378b28a2f830c6 + sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb url: "https://pub.dev" source: hosted - version: "8.9.0" + version: "8.9.2" cached_network_image: dependency: "direct main" description: @@ -209,6 +241,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + cast: + dependency: "direct main" + description: + name: cast + sha256: b70f6be547a53481dffec93ad3cc4974fae5ed707f0b677d4a50c329d7299b98 + url: "https://pub.dev" + source: hosted + version: "2.0.0" characters: dependency: transitive description: @@ -285,11 +325,10 @@ packages: connectivity_plus: dependency: "direct main" description: - path: "packages/connectivity_plus/connectivity_plus" - ref: check_mobile_first - resolved-ref: "452aa4b6448adbd3a9e592b82da3e9d355af2125" - url: "https://github.com/ente-io/plus_plugins.git" - source: git + name: connectivity_plus + sha256: "77a180d6938f78ca7d2382d2240eb626c0f6a735d0bfdce227d8ffb80f95c48b" + url: "https://pub.dev" + source: hosted version: "4.0.2" connectivity_plus_platform_interface: dependency: transitive @@ -319,10 +358,10 @@ packages: dependency: "direct main" description: name: cross_file - sha256: "2f9d2cbccb76127ba28528cb3ae2c2326a122446a83de5a056aaa3880d3882c5" + sha256: "55d7b444feb71301ef6b8838dbc1ae02e63dd48c8773f3810ff53bb1e2945b32" url: "https://pub.dev" source: hosted - version: "0.3.3+7" + version: "0.3.4+1" crypto: dependency: "direct main" description: @@ -383,10 +422,10 @@ packages: dependency: "direct main" description: name: device_info_plus - sha256: "0042cb3b2a76413ea5f8a2b40cec2a33e01d0c937e91f0f7c211fde4f7739ba6" + sha256: "77f757b789ff68e4eaf9c56d1752309bd9f7ad557cb105b938a7f8eb89e59110" url: "https://pub.dev" source: hosted - version: "9.1.1" + version: "9.1.2" device_info_plus_platform_interface: dependency: transitive description: @@ -479,18 +518,18 @@ packages: dependency: "direct main" description: name: extended_image - sha256: b4d72a27851751cfadaf048936d42939db7cd66c08fdcfe651eeaa1179714ee6 + sha256: d7f091d068fcac7246c4b22a84b8dac59a62e04d29a5c172710c696e67a22f94 url: "https://pub.dev" source: hosted - version: "8.1.1" + version: "8.2.0" extended_image_library: dependency: transitive description: name: extended_image_library - sha256: "8bf87c0b14dcb59200c923a9a3952304e4732a0901e40811428834ef39018ee1" + sha256: c9caee8fe9b6547bd41c960c4f2d1ef8e34321804de6a1777f1d614a24247ad6 url: "https://pub.dev" source: hosted - version: "3.6.0" + version: "4.0.4" fade_indexed_stack: dependency: "direct main" description: @@ -552,10 +591,10 @@ packages: dependency: "direct main" description: name: firebase_core - sha256: "96607c0e829a581c2a483c658f04e8b159964c3bae2730f73297070bc85d40bb" + sha256: a864d1b6afd25497a3b57b016886d1763df52baaa69758a46723164de8d187fe url: "https://pub.dev" source: hosted - version: "2.24.2" + version: "2.29.0" firebase_core_platform_interface: dependency: transitive description: @@ -568,34 +607,34 @@ packages: dependency: transitive description: name: firebase_core_web - sha256: d585bdf3c656c3f7821ba1bd44da5f13365d22fcecaf5eb75c4295246aaa83c0 + sha256: c8b02226e548f35aace298e2bb2e6c24e34e8a203d614e742bb1146e5a4ad3c8 url: "https://pub.dev" source: hosted - version: "2.10.0" + version: "2.15.0" firebase_messaging: dependency: "direct main" description: name: firebase_messaging - sha256: "980259425fa5e2afc03e533f33723335731d21a56fd255611083bceebf4373a8" + sha256: e41586e0fd04fe9a40424f8b0053d0832e6d04f49e020cdaf9919209a28497e9 url: "https://pub.dev" source: hosted - version: "14.7.10" + version: "14.7.19" firebase_messaging_platform_interface: dependency: transitive description: name: firebase_messaging_platform_interface - sha256: "54e283a0e41d81d854636ad0dad73066adc53407a60a7c3189c9656e2f1b6107" + sha256: "80b4ccf20066b0579ebc88d4678230a5f53ab282fe040e31671af745db1588f9" url: "https://pub.dev" source: hosted - version: "4.5.18" + version: "4.5.31" firebase_messaging_web: dependency: transitive description: name: firebase_messaging_web - sha256: "90dc7ed885e90a24bb0e56d661d4d2b5f84429697fd2cbb9e5890a0ca370e6f4" + sha256: "9224aa4db1ce6f08d96a82978453d37e9980204a20e410a11d9b774b24c6841c" url: "https://pub.dev" source: hosted - version: "3.5.18" + version: "3.8.1" fixnum: dependency: transitive description: @@ -621,10 +660,10 @@ packages: dependency: "direct main" description: name: flutter_animate - sha256: cabe33af6201144be052352d53572a1b8a4f5782b46080be7520d95abe763715 + sha256: "7c8a6594a9252dad30cc2ef16e33270b6248c4dedc3b3d06c86c4f3f4dc05ae5" url: "https://pub.dev" source: hosted - version: "4.4.1" + version: "4.5.0" flutter_cache_manager: dependency: "direct main" description: @@ -759,10 +798,10 @@ packages: dependency: "direct main" description: name: flutter_native_splash - sha256: "17d9671396fb8ec45ad10f4a975eb8a0f70bedf0fdaf0720b31ea9de6da8c4da" + sha256: "558f10070f03ee71f850a78f7136ab239a67636a294a44a06b6b7345178edb1e" url: "https://pub.dev" source: hosted - version: "2.3.7" + version: "2.3.10" flutter_password_strength: dependency: "direct main" description: @@ -775,10 +814,10 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: b068ffc46f82a55844acfa4fdbb61fad72fa2aef0905548419d97f0f95c456da + sha256: "8cf40eebf5dec866a6d1956ad7b4f7016e6c0cc69847ab946833b7d43743809f" url: "https://pub.dev" source: hosted - version: "2.0.17" + version: "2.0.19" flutter_secure_storage: dependency: "direct main" description: @@ -827,6 +866,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + flutter_shaders: + dependency: transitive + description: + name: flutter_shaders + sha256: "02750b545c01ff4d8e9bbe8f27a7731aa3778402506c67daa1de7f5fc3f4befe" + url: "https://pub.dev" + source: hosted + version: "0.1.2" flutter_sodium: dependency: "direct main" description: @@ -839,10 +886,10 @@ packages: dependency: transitive description: name: flutter_spinkit - sha256: b39c753e909d4796906c5696a14daf33639a76e017136c8d82bf3e620ce5bb8e + sha256: d2696eed13732831414595b98863260e33e8882fc069ee80ec35d4ac9ddb0472 url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "5.2.1" flutter_staggered_grid_view: dependency: "direct main" description: @@ -873,10 +920,10 @@ packages: dependency: "direct dev" description: name: freezed - sha256: "6c5031daae12c7072b3a87eff98983076434b4889ef2a44384d0cae3f82372ba" + sha256: a434911f643466d78462625df76fd9eb13e57348ff43fe1f77bbe909522c67a1 url: "https://pub.dev" source: hosted - version: "2.4.6" + version: "2.5.2" freezed_annotation: dependency: "direct main" description: @@ -958,10 +1005,10 @@ packages: dependency: "direct main" description: name: http - sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" + sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.2.1" http_client_helper: dependency: transitive description: @@ -990,18 +1037,18 @@ packages: dependency: "direct main" description: name: image - sha256: "004a2e90ce080f8627b5a04aecb4cdfac87d2c3f3b520aa291260be5a32c033d" + sha256: "4c68bfd5ae83e700b5204c1e74451e7bf3cf750e6843c6e158289cf56bda018e" url: "https://pub.dev" source: hosted - version: "4.1.4" + version: "4.1.7" image_editor: dependency: "direct main" description: name: image_editor - sha256: "9877a057b0cd2fafcd9a3dce5279948bd850d53ce76231a83c9678a2c9f186e9" + sha256: "6401a431ef1e988e35a8b19ff02cb7d31bd881fd7db0d39261ac8236683ef1c1" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" image_editor_common: dependency: transitive description: @@ -1010,6 +1057,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + image_editor_ohos: + dependency: transitive + description: + name: image_editor_ohos + sha256: aee8fa1490fedbb98583dfaebb4162c295abeb0044e94f2eb2ad52ae419e6f6e + url: "https://pub.dev" + source: hosted + version: "0.0.7" image_editor_platform_interface: dependency: transitive description: @@ -1030,10 +1085,10 @@ packages: dependency: transitive description: name: in_app_purchase_android - sha256: "28164faac635a6cc357c96f47813e675eec7622a8f41e829b501573dbbce2cce" + sha256: b9d4ecf70c51ab46222502c050b1535f6249caf9d768c4abd856ea16a18a882d url: "https://pub.dev" source: hosted - version: "0.3.0+17" + version: "0.3.3+1" in_app_purchase_platform_interface: dependency: transitive description: @@ -1046,10 +1101,10 @@ packages: dependency: transitive description: name: in_app_purchase_storekit - sha256: c4b17a7f2ca8ddc7fd7996a8c32a3af6beddf91d651997c8675a5f23c103c9bc + sha256: e0f860e760488dbd666e0f27e239d128cba744607fc62434dc76c19d1c292439 url: "https://pub.dev" source: hosted - version: "0.3.8+1" + version: "0.3.13+1" integration_test: dependency: "direct dev" description: flutter @@ -1123,10 +1178,10 @@ packages: dependency: "direct main" description: name: latlong2 - sha256: "18712164760cee655bc790122b0fd8f3d5b3c36da2cb7bf94b68a197fbb0811b" + sha256: "98227922caf49e6056f91b6c56945ea1c7b166f28ffcd5fb8e72fc0b453cc8fe" url: "https://pub.dev" source: hosted - version: "0.9.0" + version: "0.9.1" leak_tracker: dependency: transitive description: @@ -1187,26 +1242,34 @@ packages: dependency: "direct main" description: name: local_auth - sha256: "27679ed8e0d7daab2357db6bb7076359e083a56b295c0c59723845301da6aed9" + sha256: "280421b416b32de31405b0a25c3bd42dfcef2538dfbb20c03019e02a5ed55ed0" url: "https://pub.dev" source: hosted - version: "2.1.8" + version: "2.2.0" local_auth_android: dependency: "direct main" description: name: local_auth_android - sha256: "54e9c35ce52c06333355ab0d0f41e4c06dbca354b23426765ba41dfb1de27598" + sha256: "3bcd732dda7c75fcb7ddaef12e131230f53dcc8c00790d0d6efb3aa0fbbeda57" url: "https://pub.dev" source: hosted - version: "1.0.36" + version: "1.0.37" + local_auth_darwin: + dependency: transitive + description: + name: local_auth_darwin + sha256: "33381a15b0de2279523eca694089393bb146baebdce72a404555d03174ebc1e9" + url: "https://pub.dev" + source: hosted + version: "1.2.2" local_auth_ios: dependency: "direct main" description: name: local_auth_ios - sha256: eb283b530029b334698918f1e282d4483737cbca972ff21b9193be3d6de8e2b8 + sha256: "6dde47dc852bc0c8343cb58e66a46efb16b62eddf389ce103d4dacb0c6c40c71" url: "https://pub.dev" source: hosted - version: "1.1.6" + version: "1.1.7" local_auth_platform_interface: dependency: transitive description: @@ -1355,10 +1418,10 @@ packages: dependency: transitive description: name: mime - sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.5" modal_bottom_sheet: dependency: "direct main" description: @@ -1510,18 +1573,18 @@ packages: dependency: "direct main" description: name: path_provider - sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b + sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" + sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.2.4" path_provider_foundation: dependency: transitive description: @@ -1566,50 +1629,58 @@ packages: dependency: "direct main" description: name: permission_handler - sha256: "284a66179cabdf942f838543e10413246f06424d960c92ba95c84439154fcac8" + sha256: "18bf33f7fefbd812f37e72091a15575e72d5318854877e0e4035a24ac1113ecb" url: "https://pub.dev" source: hosted - version: "11.0.1" + version: "11.3.1" permission_handler_android: dependency: transitive description: name: permission_handler_android - sha256: f9fddd3b46109bd69ff3f9efa5006d2d309b7aec0f3c1c5637a60a2d5659e76e + sha256: "1acac6bae58144b442f11e66621c062aead9c99841093c38f5bcdcc24c1c3474" url: "https://pub.dev" source: hosted - version: "11.1.0" + version: "12.0.5" permission_handler_apple: dependency: transitive description: name: permission_handler_apple - sha256: "99e220bce3f8877c78e4ace901082fb29fa1b4ebde529ad0932d8d664b34f3f5" + sha256: e9ad66020b89ff1b63908f247c2c6f931c6e62699b756ef8b3c4569350cd8662 url: "https://pub.dev" source: hosted - version: "9.1.4" + version: "9.4.4" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: "54bf176b90f6eddd4ece307e2c06cf977fb3973719c35a93b85cc7093eb6070d" + url: "https://pub.dev" + source: hosted + version: "0.1.1" permission_handler_platform_interface: dependency: transitive description: name: permission_handler_platform_interface - sha256: "6760eb5ef34589224771010805bea6054ad28453906936f843a8cc4d3a55c4a4" + sha256: "48d4fcf201a1dad93ee869ab0d4101d084f49136ec82a8a06ed9cfeacab9fd20" url: "https://pub.dev" source: hosted - version: "3.12.0" + version: "4.2.1" permission_handler_windows: dependency: transitive description: name: permission_handler_windows - sha256: cc074aace208760f1eee6aa4fae766b45d947df85bc831cde77009cdb4720098 + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" url: "https://pub.dev" source: hosted - version: "0.1.3" + version: "0.2.1" petitparser: dependency: transitive description: name: petitparser - sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 + sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 url: "https://pub.dev" source: hosted - version: "5.4.0" + version: "6.0.2" photo_manager: dependency: "direct main" description: @@ -1662,10 +1733,10 @@ packages: dependency: "direct main" description: name: pointycastle - sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29" + sha256: "70fe966348fe08c34bf929582f1d8247d9d9408130723206472b4687227e4333" url: "https://pub.dev" source: hosted - version: "3.7.4" + version: "3.8.0" polylabel: dependency: transitive description: @@ -1698,14 +1769,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + protobuf: + dependency: transitive + description: + name: protobuf + sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d" + url: "https://pub.dev" + source: hosted + version: "3.1.0" provider: dependency: "direct main" description: name: provider - sha256: "9a96a0a19b594dbc5bf0f1f27d2bc67d5f95957359b461cd9feb44ed6ae75096" + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c url: "https://pub.dev" source: hosted - version: "6.1.1" + version: "6.1.2" pub_semver: dependency: transitive description: @@ -1734,10 +1813,10 @@ packages: dependency: "direct main" description: name: receive_sharing_intent - sha256: "8fdf5927934041264becf65199ef8057b8b176e879d95ffa0420cd2a6219c0fd" + sha256: fe02f858ac9f8d44d62e1964dadded000bb48dea424085ed280d542a61c4e8ba url: "https://pub.dev" source: hosted - version: "1.6.7" + version: "1.7.0" rxdart: dependency: transitive description: @@ -1814,18 +1893,18 @@ packages: dependency: "direct main" description: name: sentry - sha256: "5686ed515bb620dc52b4ae99a6586fe720d443591183cf1f620ec5d1f0eec100" + sha256: fe99a06970b909a491b7f89d54c9b5119772e3a48a400308a6e129625b333f5b url: "https://pub.dev" source: hosted - version: "7.15.0" + version: "7.19.0" sentry_flutter: dependency: "direct main" description: name: sentry_flutter - sha256: "505dec3b6810562785d2c34ae871c73ff2cba6cf436c32c188f0464df226ba8f" + sha256: fc013d4a753447320f62989b1871fdc1f20c77befcc8be3e38774dd7402e7a62 url: "https://pub.dev" source: hosted - version: "7.15.0" + version: "7.19.0" share_plus: dependency: "direct main" description: @@ -1838,26 +1917,26 @@ packages: dependency: transitive description: name: share_plus_platform_interface - sha256: df08bc3a07d01f5ea47b45d03ffcba1fa9cd5370fb44b3f38c70e42cced0f956 + sha256: "251eb156a8b5fa9ce033747d73535bf53911071f8d3b6f4f0b578505ce0d4496" url: "https://pub.dev" source: hosted - version: "3.3.1" + version: "3.4.0" shared_preferences: dependency: "direct main" description: name: shared_preferences - sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02" + sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180 url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.2.3" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" + sha256: "1ee8bf911094a1b592de7ab29add6f826a7331fb854273d55918693d5364a1f2" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.2.2" shared_preferences_foundation: dependency: transitive description: @@ -1886,10 +1965,10 @@ packages: dependency: transitive description: name: shared_preferences_web - sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf + sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.0" shared_preferences_windows: dependency: transitive description: @@ -2011,10 +2090,10 @@ packages: dependency: "direct main" description: name: sqlite3 - sha256: db65233e6b99e99b2548932f55a987961bc06d82a31a0665451fa0b4fff4c3fb + sha256: "072128763f1547e3e9b4735ce846bfd226d68019ccda54db4cd427b12dfdedc9" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.4.0" sqlite3_flutter_libs: dependency: "direct main" description: @@ -2027,10 +2106,10 @@ packages: dependency: "direct main" description: name: sqlite_async - sha256: b252fd3a53766460b2f240e082517d3bc1cce7682a1550817f0f799d4a7a4087 + sha256: "139c8f1085132d0941b925efacb4fa0fed9ee40d624739cc26a051dbc36bf727" url: "https://pub.dev" source: hosted - version: "0.5.2" + version: "0.6.1" stack_trace: dependency: transitive description: @@ -2243,26 +2322,26 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: c512655380d241a337521703af62d2c122bf7b77a46ff7dd750092aa9433499c + sha256: "6ce1e04375be4eed30548f10a315826fd933c1e493206eab82eed01f438c8d2e" url: "https://pub.dev" source: hosted - version: "6.2.4" + version: "6.2.6" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "507dc655b1d9cb5ebc756032eb785f114e415f91557b73bf60b7e201dfedeb2f" + sha256: "360a6ed2027f18b73c8d98e159dda67a61b7f2e0f6ec26e86c3ada33b0621775" url: "https://pub.dev" source: hosted - version: "6.2.2" + version: "6.3.1" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "75bb6fe3f60070407704282a2d295630cab232991eb52542b18347a8a941df03" + sha256: "9149d493b075ed740901f3ee844a38a00b33116c7c5c10d7fb27df8987fb51d5" url: "https://pub.dev" source: hosted - version: "6.2.4" + version: "6.2.5" url_launcher_linux: dependency: transitive description: @@ -2283,18 +2362,18 @@ packages: dependency: transitive description: name: url_launcher_platform_interface - sha256: a932c3a8082e118f80a475ce692fde89dc20fddb24c57360b96bc56f7035de1f + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: "7fd2f55fe86cea2897b963e864dc01a7eb0719ecc65fcef4c1cc3d686d718bb2" + sha256: "3692a459204a33e04bc94f5fb91158faf4f2c8903281ddd82915adecdb1a901d" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.0" url_launcher_windows: dependency: transitive description: @@ -2332,18 +2411,18 @@ packages: dependency: transitive description: name: video_player_android - sha256: "7f8f25d7ad56819a82b2948357f3c3af071f6a678db33833b26ec36bbc221316" + sha256: "821cff3446bbde255e8d03c12fe1f9810c69fee2c26c394545b13d824ba63c2e" url: "https://pub.dev" source: hosted - version: "2.4.11" + version: "2.4.13" video_player_avfoundation: dependency: transitive description: name: video_player_avfoundation - sha256: bc923884640d6dc403050586eb40713cdb8d1d84e6886d8aca50ab04c59124c2 + sha256: "00c49b1d68071341397cf760b982c1e26ed9232464c8506ee08378a5cca5070d" url: "https://pub.dev" source: hosted - version: "2.5.2" + version: "2.5.7" video_player_platform_interface: dependency: transitive description: @@ -2424,6 +2503,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + web: + dependency: transitive + description: + name: web + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + url: "https://pub.dev" + source: hosted + version: "0.5.1" web_socket_channel: dependency: transitive description: @@ -2468,10 +2555,10 @@ packages: dependency: transitive description: name: win32 - sha256: b0f37db61ba2f2e9b7a78a1caece0052564d1bc70668156cf3a29d676fe4e574 + sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8" url: "https://pub.dev" source: hosted - version: "5.1.1" + version: "5.2.0" win32_registry: dependency: transitive description: @@ -2500,10 +2587,10 @@ packages: dependency: "direct main" description: name: xml - sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84" + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.5.0" xmlstream: dependency: transitive description: @@ -2529,5 +2616,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.2.0 <4.0.0" - flutter: ">=3.16.0" + dart: ">=3.3.0 <4.0.0" + flutter: ">=3.19.0" diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 0a87700dd..9f26ef1e1 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -12,7 +12,7 @@ description: ente photos application # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 0.8.78+598 +version: 0.8.80+600 publish_to: none environment: @@ -27,6 +27,7 @@ dependencies: battery_info: ^1.1.1 bip39: ^1.0.6 cached_network_image: ^3.0.0 + cast: ^2.0.0 chewie: git: url: https://github.com/ente-io/chewie.git @@ -59,7 +60,6 @@ dependencies: extended_image: ^8.1.1 fade_indexed_stack: ^0.2.2 fast_base58: ^0.2.1 - figma_squircle: ^0.5.3 file_saver: # Use forked version till this PR is merged: https://github.com/incrediblezayed/file_saver/pull/87 @@ -89,7 +89,7 @@ dependencies: flutter_sodium: ^0.2.0 flutter_staggered_grid_view: ^0.6.2 fluttertoast: ^8.0.6 - freezed_annotation: ^2.2.0 + freezed_annotation: ^2.4.1 google_nav_bar: ^5.0.5 home_widget: ^0.5.0 html_unescape: ^2.0.0 @@ -137,17 +137,17 @@ dependencies: pointycastle: ^3.7.3 provider: ^6.0.0 quiver: ^3.0.1 - receive_sharing_intent: ^1.6.7 + receive_sharing_intent: ^1.7.0 scrollable_positioned_list: ^0.3.5 sentry: ^7.9.0 sentry_flutter: ^7.9.0 - share_plus: ^7.2.2 + share_plus: 7.2.2 shared_preferences: ^2.0.5 sqflite: ^2.3.0 sqflite_migration: ^0.3.0 sqlite3: ^2.1.0 sqlite3_flutter_libs: ^0.5.20 - sqlite_async: ^0.5.2 + sqlite_async: ^0.6.1 step_progress_indicator: ^1.0.2 styled_text: ^7.0.0 syncfusion_flutter_core: ^19.2.49 @@ -175,10 +175,10 @@ dependencies: xml: ^6.3.0 dependency_overrides: - # current fork of tfite_flutter_helper depends on ffi: ^1.x.x - # but we need ffi: ^2.0.1 for newer packages. The original tfite_flutter_helper - # - ffi: ^2.1.0 + connectivity_plus: ^4.0.0 + # Remove this after removing dependency from flutter_sodium. + # Newer flutter packages depends on ffi > 2.0.0 while flutter_sodium depends on ffi < 2.0.0 + ffi: 2.1.0 video_player: git: url: https://github.com/ente-io/packages.git @@ -194,7 +194,7 @@ dev_dependencies: flutter_lints: ^2.0.1 flutter_test: sdk: flutter - freezed: ^2.3.2 + freezed: ^2.5.2 integration_test: sdk: flutter isar_generator: ^3.1.0+1 diff --git a/server/cmd/museum/main.go b/server/cmd/museum/main.go index 4cbc00612..c451b8b9c 100644 --- a/server/cmd/museum/main.go +++ b/server/cmd/museum/main.go @@ -163,7 +163,7 @@ func main() { ObjectCopiesRepo: objectCopiesRepo, UsageRepo: usageRepo} familyRepo := &repo.FamilyRepository{DB: db} trashRepo := &repo.TrashRepository{DB: db, ObjectRepo: objectRepo, FileRepo: fileRepo, QueueRepo: queueRepo} - publicCollectionRepo := &repo.PublicCollectionRepository{DB: db} + publicCollectionRepo := repo.NewPublicCollectionRepository(db, viper.GetString("apps.public-albums")) collectionRepo := &repo.CollectionRepository{DB: db, FileRepo: fileRepo, PublicCollectionRepo: publicCollectionRepo, TrashRepo: trashRepo, SecretEncryptionKey: secretEncryptionKeyBytes, QueueRepo: queueRepo, LatencyLogger: latencyLogger} pushRepo := &repo.PushTokenRepository{DB: db} diff --git a/server/configurations/local.yaml b/server/configurations/local.yaml index bbad3f278..0e456a53a 100644 --- a/server/configurations/local.yaml +++ b/server/configurations/local.yaml @@ -71,6 +71,11 @@ http: # By default, this is false, and museum will bind to 8080 without TLS. # use-tls: true +# Specify the base endpoints for various apps +apps: + public-albums: "https://albums.ente.io" + + # Database connection parameters db: host: localhost diff --git a/server/pkg/controller/data_cleanup/controller.go b/server/pkg/controller/data_cleanup/controller.go index 05727ff0c..c6f2a1f81 100644 --- a/server/pkg/controller/data_cleanup/controller.go +++ b/server/pkg/controller/data_cleanup/controller.go @@ -190,8 +190,17 @@ func (c *DeleteUserCleanupController) storageCheck(ctx context.Context, item *en } func (c *DeleteUserCleanupController) isDeleted(item *entity.DataCleanup) error { - _, err := c.UserRepo.Get(item.UserID) + u, err := c.UserRepo.Get(item.UserID) if err == nil { + // user is not deleted, double check by verifying email is not empty + if u.Email != "" { + // todo: remove this logic after next deployment. This is to only handle cases + // where we have not removed scheduled delete entry for account post recovery. + remErr := c.Repo.RemoveScheduledDelete(context.Background(), item.UserID) + if remErr != nil { + return stacktrace.Propagate(remErr, "failed to remove scheduled delete entry") + } + } return stacktrace.Propagate(ente.NewBadRequestWithMessage("User ID is linked to undeleted account"), "") } if !errors.Is(err, ente.ErrUserDeleted) { diff --git a/server/pkg/controller/public_collection.go b/server/pkg/controller/public_collection.go index 76a604937..694db8bb1 100644 --- a/server/pkg/controller/public_collection.go +++ b/server/pkg/controller/public_collection.go @@ -80,7 +80,7 @@ func (c *PublicCollectionController) CreateAccessToken(ctx context.Context, req } } response := ente.PublicURL{ - URL: fmt.Sprintf(repo.BaseShareURL, accessToken), + URL: c.PublicCollectionRepo.GetAlbumUrl(accessToken), ValidTill: req.ValidTill, DeviceLimit: req.DeviceLimit, EnableDownload: true, @@ -151,7 +151,7 @@ func (c *PublicCollectionController) UpdateSharedUrl(ctx context.Context, req en return ente.PublicURL{}, stacktrace.Propagate(err, "") } return ente.PublicURL{ - URL: fmt.Sprintf(repo.BaseShareURL, publicCollectionToken.Token), + URL: c.PublicCollectionRepo.GetAlbumUrl(publicCollectionToken.Token), DeviceLimit: publicCollectionToken.DeviceLimit, ValidTill: publicCollectionToken.ValidTill, EnableDownload: publicCollectionToken.EnableDownload, diff --git a/server/pkg/controller/user/user.go b/server/pkg/controller/user/user.go index afba09058..ddd6cf2de 100644 --- a/server/pkg/controller/user/user.go +++ b/server/pkg/controller/user/user.go @@ -368,7 +368,15 @@ func (c *UserController) HandleAccountRecovery(ctx *gin.Context, req ente.Recove return stacktrace.Propagate(err, "") } err = c.UserRepo.UpdateEmail(req.UserID, encryptedEmail, emailHash) - return stacktrace.Propagate(err, "failed to update email") + if err != nil { + return stacktrace.Propagate(err, "failed to update email") + } + err = c.DataCleanupRepo.RemoveScheduledDelete(ctx, req.UserID) + if err != nil { + logrus.WithError(err).Error("failed to remove scheduled delete") + return stacktrace.Propagate(err, "") + } + return stacktrace.Propagate(err, "") } func (c *UserController) attachFreeSubscription(userID int64) (ente.Subscription, error) { diff --git a/server/pkg/repo/collection.go b/server/pkg/repo/collection.go index 38700dec4..16ae85324 100644 --- a/server/pkg/repo/collection.go +++ b/server/pkg/repo/collection.go @@ -216,7 +216,7 @@ pct.access_token, pct.valid_till, pct.device_limit, pct.created_at, pct.updated_ if _, ok := addPublicUrlMap[pctToken.String]; !ok { addPublicUrlMap[pctToken.String] = true url := ente.PublicURL{ - URL: fmt.Sprintf(BaseShareURL, pctToken.String), + URL: repo.PublicCollectionRepo.GetAlbumUrl(pctToken.String), DeviceLimit: int(pctDeviceLimit.Int32), ValidTill: pctValidTill.Int64, EnableDownload: pctEnableDownload.Bool, diff --git a/server/pkg/repo/datacleanup/repository.go b/server/pkg/repo/datacleanup/repository.go index 4870cecf5..fc1a1c08f 100644 --- a/server/pkg/repo/datacleanup/repository.go +++ b/server/pkg/repo/datacleanup/repository.go @@ -3,7 +3,7 @@ package datacleanup import ( "context" "database/sql" - + "fmt" entity "github.com/ente-io/museum/ente/data_cleanup" "github.com/ente-io/museum/pkg/utils/time" "github.com/ente-io/stacktrace" @@ -19,6 +19,21 @@ func (r *Repository) Insert(ctx context.Context, userID int64) error { return stacktrace.Propagate(err, "failed to insert") } +func (r *Repository) RemoveScheduledDelete(ctx context.Context, userID int64) error { + res, execErr := r.DB.ExecContext(ctx, `DELETE from data_cleanup where user_id= $1 and stage = $2`, userID, entity.Scheduled) + if execErr != nil { + return execErr + } + affected, affErr := res.RowsAffected() + if affErr != nil { + return affErr + } + if affected != 1 { + return fmt.Errorf("only one row should have been affected, got %d", affected) + } + return nil +} + func (r *Repository) GetItemsPendingCompletion(ctx context.Context, limit int) ([]*entity.DataCleanup, error) { rows, err := r.DB.QueryContext(ctx, `SELECT user_id, stage, stage_schedule_time, stage_attempt_count, created_at, updated_at from data_cleanup where stage != $1 and stage_schedule_time < now_utc_micro_seconds() diff --git a/server/pkg/repo/public_collection.go b/server/pkg/repo/public_collection.go index 0b1d2514f..d4aa81bf2 100644 --- a/server/pkg/repo/public_collection.go +++ b/server/pkg/repo/public_collection.go @@ -16,7 +16,23 @@ const BaseShareURL = "https://albums.ente.io?t=%s" // PublicCollectionRepository defines the methods for inserting, updating and // retrieving entities related to public collections type PublicCollectionRepository struct { - DB *sql.DB + DB *sql.DB + albumHost string +} + +// NewPublicCollectionRepository .. +func NewPublicCollectionRepository(db *sql.DB, albumHost string) *PublicCollectionRepository { + if albumHost == "" { + panic("albumHost can not be empty") + } + return &PublicCollectionRepository{ + DB: db, + albumHost: albumHost, + } +} + +func (pcr *PublicCollectionRepository) GetAlbumUrl(token string) string { + return fmt.Sprintf("%s?t=%s", pcr.albumHost, token) } func (pcr *PublicCollectionRepository) Insert(ctx context.Context, @@ -59,7 +75,7 @@ func (pcr *PublicCollectionRepository) GetCollectionToActivePublicURLMap(ctx con if err = rows.Scan(&collectionID, &accessToken, &publicUrl.ValidTill, &publicUrl.DeviceLimit, &publicUrl.EnableDownload, &publicUrl.EnableCollect, &nonce, &memLimit, &opsLimit); err != nil { return nil, stacktrace.Propagate(err, "") } - publicUrl.URL = fmt.Sprintf(BaseShareURL, accessToken) + publicUrl.URL = pcr.GetAlbumUrl(accessToken) if nonce != nil { publicUrl.Nonce = nonce publicUrl.MemLimit = memLimit diff --git a/web/apps/cast/src/services/typeDetectionService.ts b/web/apps/cast/src/services/typeDetectionService.ts index c52e2d80c..5acd3844d 100644 --- a/web/apps/cast/src/services/typeDetectionService.ts +++ b/web/apps/cast/src/services/typeDetectionService.ts @@ -1,3 +1,4 @@ +import { nameAndExtension } from "@/next/file"; import log from "@/next/log"; import { CustomError } from "@ente/shared/error"; import { FILE_TYPE } from "constants/file"; @@ -7,7 +8,6 @@ import { } from "constants/upload"; import FileType from "file-type"; import { FileTypeInfo } from "types/upload"; -import { getFileExtension } from "utils/file"; import { getUint8ArrayView } from "./readerService"; const TYPE_VIDEO = "video"; @@ -40,7 +40,8 @@ export async function getFileType(receivedFile: File): Promise { mimeType: typeResult.mime, }; } catch (e) { - const fileFormat = getFileExtension(receivedFile.name); + const ne = nameAndExtension(receivedFile.name); + const fileFormat = ne[1].toLowerCase(); const whiteListedFormat = WHITELISTED_FILE_FORMATS.find( (a) => a.exactType === fileFormat, ); diff --git a/web/apps/cast/src/utils/file/index.ts b/web/apps/cast/src/utils/file/index.ts index caa15d743..4f6311cbd 100644 --- a/web/apps/cast/src/utils/file/index.ts +++ b/web/apps/cast/src/utils/file/index.ts @@ -97,20 +97,6 @@ export function getFileExtensionWithDot(filename: string) { else return filename.slice(lastDotPosition); } -export function splitFilenameAndExtension(filename: string): [string, string] { - const lastDotPosition = filename.lastIndexOf("."); - if (lastDotPosition === -1) return [filename, null]; - else - return [ - filename.slice(0, lastDotPosition), - filename.slice(lastDotPosition + 1), - ]; -} - -export function getFileExtension(filename: string) { - return splitFilenameAndExtension(filename)[1]?.toLocaleLowerCase(); -} - export function generateStreamFromArrayBuffer(data: Uint8Array) { return new ReadableStream({ async start(controller: ReadableStreamDefaultController) { diff --git a/web/apps/photos/.env b/web/apps/photos/.env index 2680ead9f..a039e9105 100644 --- a/web/apps/photos/.env +++ b/web/apps/photos/.env @@ -61,6 +61,9 @@ # on port 3002 (using `yarn dev:albums`), we can connect to it and emulate the # production behaviour. # +# Note: To use your custom albums endpoint in the generated public link, set the +# `apps.public-albums` property in museum's configuration. +# # Enhancement: Consider splitting this into a separate app/ in this repository. # That can also reduce bundle sizes and make it load faster. # diff --git a/web/apps/photos/src/components/ExportInProgress.tsx b/web/apps/photos/src/components/ExportInProgress.tsx index ce2da895c..280ae52d4 100644 --- a/web/apps/photos/src/components/ExportInProgress.tsx +++ b/web/apps/photos/src/components/ExportInProgress.tsx @@ -10,9 +10,9 @@ import { LinearProgress, styled, } from "@mui/material"; -import { ExportStage } from "constants/export"; import { t } from "i18next"; import { Trans } from "react-i18next"; +import { ExportStage } from "services/export"; import { ExportProgress } from "types/export"; export const ComfySpan = styled("span")` diff --git a/web/apps/photos/src/components/ExportModal.tsx b/web/apps/photos/src/components/ExportModal.tsx index 877dee90f..ae0de37a6 100644 --- a/web/apps/photos/src/components/ExportModal.tsx +++ b/web/apps/photos/src/components/ExportModal.tsx @@ -14,12 +14,14 @@ import { Switch, Typography, } from "@mui/material"; -import { ExportStage } from "constants/export"; import { t } from "i18next"; import isElectron from "is-electron"; import { AppContext } from "pages/_app"; import { useContext, useEffect, useState } from "react"; -import exportService from "services/export"; +import exportService, { + ExportStage, + selectAndPrepareExportDirectory, +} from "services/export"; import { ExportProgress, ExportSettings } from "types/export"; import { EnteFile } from "types/file"; import { getExportDirectoryDoesNotExistMessage } from "utils/ui"; @@ -78,21 +80,6 @@ export default function ExportModal(props: Props) { void syncExportRecord(exportFolder); }, [props.show]); - // ============= - // STATE UPDATERS - // ============== - const updateExportFolder = (newFolder: string) => { - exportService.updateExportSettings({ folder: newFolder }); - setExportFolder(newFolder); - }; - - const updateContinuousExport = (updatedContinuousExport: boolean) => { - exportService.updateExportSettings({ - continuousExport: updatedContinuousExport, - }); - setContinuousExport(updatedContinuousExport); - }; - // ====================== // HELPER FUNCTIONS // ======================= @@ -102,8 +89,9 @@ export default function ExportModal(props: Props) { appContext.setDialogMessage( getExportDirectoryDoesNotExistMessage(), ); - throw Error(CustomError.EXPORT_FOLDER_DOES_NOT_EXIST); + return false; } + return true; }; const syncExportRecord = async (exportFolder: string): Promise => { @@ -132,42 +120,34 @@ export default function ExportModal(props: Props) { // ============= const handleChangeExportDirectoryClick = async () => { - try { - const newFolder = await exportService.changeExportDirectory(); - log.info(`Export folder changed to ${newFolder}`); - updateExportFolder(newFolder); - void syncExportRecord(newFolder); - } catch (e) { - if (e.message !== CustomError.SELECT_FOLDER_ABORTED) { - log.error("handleChangeExportDirectoryClick failed", e); - } - } + const newFolder = await selectAndPrepareExportDirectory(); + if (!newFolder) return; + + log.info(`Export folder changed to ${newFolder}`); + exportService.updateExportSettings({ folder: newFolder }); + setExportFolder(newFolder); + await syncExportRecord(newFolder); }; const toggleContinuousExport = async () => { - try { - await verifyExportFolderExists(); - const newContinuousExport = !continuousExport; - if (newContinuousExport) { - exportService.enableContinuousExport(); - } else { - exportService.disableContinuousExport(); - } - updateContinuousExport(newContinuousExport); - } catch (e) { - log.error("onContinuousExportChange failed", e); + if (!(await verifyExportFolderExists())) return; + + const newContinuousExport = !continuousExport; + if (newContinuousExport) { + exportService.enableContinuousExport(); + } else { + exportService.disableContinuousExport(); } + exportService.updateExportSettings({ + continuousExport: newContinuousExport, + }); + setContinuousExport(newContinuousExport); }; const startExport = async () => { - try { - await verifyExportFolderExists(); - await exportService.scheduleExport(); - } catch (e) { - if (e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) { - log.error("scheduleExport failed", e); - } - } + if (!(await verifyExportFolderExists())) return; + + await exportService.scheduleExport(); }; const stopExport = () => { diff --git a/web/apps/photos/src/constants/export.ts b/web/apps/photos/src/constants/export.ts deleted file mode 100644 index cd6c0c0ee..000000000 --- a/web/apps/photos/src/constants/export.ts +++ /dev/null @@ -1,14 +0,0 @@ -export const ENTE_METADATA_FOLDER = "metadata"; - -export const ENTE_TRASH_FOLDER = "Trash"; - -export enum ExportStage { - INIT = 0, - MIGRATION = 1, - STARTING = 2, - EXPORTING_FILES = 3, - TRASHING_DELETED_FILES = 4, - RENAMING_COLLECTION_FOLDERS = 5, - TRASHING_DELETED_COLLECTIONS = 6, - FINISHED = 7, -} diff --git a/web/apps/photos/src/pages/_app.tsx b/web/apps/photos/src/pages/_app.tsx index 06961d6c9..c31256f13 100644 --- a/web/apps/photos/src/pages/_app.tsx +++ b/web/apps/photos/src/pages/_app.tsx @@ -52,7 +52,7 @@ import "photoswipe/dist/photoswipe.css"; import { createContext, useEffect, useRef, useState } from "react"; import LoadingBar from "react-top-loading-bar"; import DownloadManager from "services/download"; -import exportService from "services/export"; +import exportService, { resumeExportsIfNeeded } from "services/export"; import mlWorkManager from "services/machineLearning/mlWorkManager"; import { getFamilyPortalRedirectURL, @@ -64,7 +64,6 @@ import { NotificationAttributes, SetNotificationAttributes, } from "types/Notification"; -import { isExportInProgress } from "utils/export"; import { getMLSearchConfig, updateMLSearchConfig, @@ -214,37 +213,10 @@ export default function App({ Component, pageProps }: AppProps) { return; } const initExport = async () => { - try { - log.info("init export"); - const token = getToken(); - if (!token) { - log.info( - "User not logged in, not starting export continuous sync job", - ); - return; - } - await DownloadManager.init(APPS.PHOTOS, { token }); - const exportSettings = exportService.getExportSettings(); - if ( - !(await exportService.exportFolderExists( - exportSettings?.folder, - )) - ) { - return; - } - const exportRecord = await exportService.getExportRecord( - exportSettings.folder, - ); - if (exportSettings.continuousExport) { - exportService.enableContinuousExport(); - } - if (isExportInProgress(exportRecord.stage)) { - log.info("export was in progress, resuming"); - exportService.scheduleExport(); - } - } catch (e) { - log.error("init export failed", e); - } + const token = getToken(); + if (!token) return; + await DownloadManager.init(APPS.PHOTOS, { token }); + await resumeExportsIfNeeded(); }; initExport(); try { diff --git a/web/apps/photos/src/services/collectionService.ts b/web/apps/photos/src/services/collectionService.ts index 0812ca8f9..552512c4d 100644 --- a/web/apps/photos/src/services/collectionService.ts +++ b/web/apps/photos/src/services/collectionService.ts @@ -1385,7 +1385,6 @@ export async function moveToHiddenCollection(files: EnteFile[]) { hiddenCollection = await createHiddenCollection(); } const groupiedFiles = groupFilesBasedOnCollectionID(files); - // eslint-disable-next-line @typescript-eslint/no-unused-vars for (const [collectionID, files] of groupiedFiles.entries()) { if (collectionID === hiddenCollection.id) { continue; @@ -1404,7 +1403,6 @@ export async function unhideToCollection( ) { try { const groupiedFiles = groupFilesBasedOnCollectionID(files); - // eslint-disable-next-line @typescript-eslint/no-unused-vars for (const [collectionID, files] of groupiedFiles.entries()) { if (collectionID === collection.id) { continue; diff --git a/web/apps/photos/src/services/export/index.ts b/web/apps/photos/src/services/export/index.ts index f2e90139a..f7a0c3f3e 100644 --- a/web/apps/photos/src/services/export/index.ts +++ b/web/apps/photos/src/services/export/index.ts @@ -3,65 +3,74 @@ import log from "@/next/log"; import { CustomError } from "@ente/shared/error"; import { Events, eventBus } from "@ente/shared/events"; import { LS_KEYS, getData, setData } from "@ente/shared/storage/localStorage"; +import { formatDateTimeShort } from "@ente/shared/time/format"; import { User } from "@ente/shared/user/types"; import { sleep } from "@ente/shared/utils"; import QueueProcessor, { CancellationStatus, RequestCanceller, } from "@ente/shared/utils/queueProcessor"; -import { ExportStage } from "constants/export"; import { FILE_TYPE } from "constants/file"; import { Collection } from "types/collection"; import { + CollectionExportNames, ExportProgress, ExportRecord, ExportSettings, ExportUIUpdaters, + FileExportNames, } from "types/export"; import { EnteFile } from "types/file"; +import { Metadata } from "types/upload"; import { constructCollectionNameMap, getCollectionUserFacingName, getNonEmptyPersonalCollections, } from "utils/collection"; -import { - convertCollectionIDExportNameObjectToMap, - convertFileIDExportNameObjectToMap, - getCollectionExportPath, - getCollectionExportedFiles, - getCollectionIDFromFileUID, - getDeletedExportedCollections, - getDeletedExportedFiles, - getExportRecordFileUID, - getFileExportPath, - getFileMetadataExportPath, - getGoogleLikeMetadataFile, - getLivePhotoExportName, - getMetadataFileExportPath, - getMetadataFolderExportPath, - getRenamedExportedCollections, - getTrashedFileExportPath, - getUnExportedFiles, - getUniqueCollectionExportName, - getUniqueFileExportName, - isLivePhotoExportName, - parseLivePhotoExportName, -} from "utils/export"; import { generateStreamFromArrayBuffer, getPersonalFiles, getUpdatedEXIFFileForDownload, mergeMetadata, } from "utils/file"; +import { safeDirectoryName, safeFileName } from "utils/native-fs"; import { getAllLocalCollections } from "../collectionService"; import downloadManager from "../download"; import { getAllLocalFiles } from "../fileService"; import { decodeLivePhoto } from "../livePhotoService"; import { migrateExport } from "./migration"; -const EXPORT_RECORD_FILE_NAME = "export_status.json"; +/** Name of the JSON file in which we keep the state of the export. */ +const exportRecordFileName = "export_status.json"; -export const ENTE_EXPORT_DIRECTORY = "ente Photos"; +/** + * Name of the top level directory which we create underneath the selected + * directory when the user starts an export to the filesystem. + */ +const exportDirectoryName = "Ente Photos"; + +/** + * Name of the directory in which we put our metadata when exporting to the + * filesystem. + */ +export const exportMetadataDirectoryName = "metadata"; + +/** + * Name of the directory in which we keep trash items when deleting files that + * have been exported to the local disk previously. + */ +export const exportTrashDirectoryName = "Trash"; + +export enum ExportStage { + INIT = 0, + MIGRATION = 1, + STARTING = 2, + EXPORTING_FILES = 3, + TRASHING_DELETED_FILES = 4, + RENAMING_COLLECTION_FOLDERS = 5, + TRASHING_DELETED_COLLECTIONS = 6, + FINISHED = 7, +} export const NULL_EXPORT_RECORD: ExportRecord = { version: 3, @@ -155,23 +164,6 @@ class ExportService { this.uiUpdater.setLastExportTime(exportTime); } - async changeExportDirectory() { - try { - const newRootDir = await ensureElectron().selectDirectory(); - if (!newRootDir) { - throw Error(CustomError.SELECT_FOLDER_ABORTED); - } - const newExportDir = `${newRootDir}/${ENTE_EXPORT_DIRECTORY}`; - await ensureElectron().checkExistsAndCreateDir(newExportDir); - return newExportDir; - } catch (e) { - if (e.message !== CustomError.SELECT_FOLDER_ABORTED) { - log.error("changeExportDirectory failed", e); - } - throw e; - } - } - enableContinuousExport() { try { if (this.continuousExportEventHandler) { @@ -475,6 +467,7 @@ class ExportService { renamedCollections: Collection[], isCanceled: CancellationStatus, ) { + const fs = ensureElectron().fs; try { for (const collection of renamedCollections) { try { @@ -484,24 +477,16 @@ class ExportService { await this.verifyExportFolderExists(exportFolder); const oldCollectionExportName = collectionIDExportNameMap.get(collection.id); - const oldCollectionExportPath = getCollectionExportPath( + const oldCollectionExportPath = `${exportFolder}/${oldCollectionExportName}`; + const newCollectionExportName = await safeDirectoryName( exportFolder, - oldCollectionExportName, + getCollectionUserFacingName(collection), + fs.exists, ); - - const newCollectionExportName = - await getUniqueCollectionExportName( - exportFolder, - getCollectionUserFacingName(collection), - ); log.info( `renaming collection with id ${collection.id} from ${oldCollectionExportName} to ${newCollectionExportName}`, ); - const newCollectionExportPath = getCollectionExportPath( - exportFolder, - newCollectionExportName, - ); - + const newCollectionExportPath = `${exportFolder}/${newCollectionExportName}`; await this.addCollectionExportedRecord( exportFolder, collection.id, @@ -512,7 +497,7 @@ class ExportService { newCollectionExportName, ); try { - await ensureElectron().rename( + await fs.rename( oldCollectionExportPath, newCollectionExportPath, ); @@ -560,6 +545,7 @@ class ExportService { exportFolder: string, isCanceled: CancellationStatus, ) { + const fs = ensureElectron().fs; try { const exportRecord = await this.getExportRecord(exportFolder); const collectionIDPathMap = @@ -587,23 +573,18 @@ class ExportService { "collection is not empty, can't remove", ); } - const collectionExportPath = getCollectionExportPath( - exportFolder, - collectionExportName, - ); + const collectionExportPath = `${exportFolder}/${collectionExportName}`; await this.removeCollectionExportedRecord( exportFolder, collectionID, ); try { // delete the collection metadata folder - await ensureElectron().deleteFolder( + await fs.rmdir( getMetadataFolderExportPath(collectionExportPath), ); // delete the collection folder - await ensureElectron().deleteFolder( - collectionExportPath, - ); + await fs.rmdir(collectionExportPath); } catch (e) { await this.addCollectionExportedRecord( exportFolder, @@ -648,6 +629,7 @@ class ExportService { incrementFailed: () => void, isCanceled: CancellationStatus, ): Promise { + const fs = ensureElectron().fs; try { for (const file of files) { log.info( @@ -682,14 +664,9 @@ class ExportService { collectionExportName, ); } - const collectionExportPath = getCollectionExportPath( - exportDir, - collectionExportName, - ); - await ensureElectron().checkExistsAndCreateDir( - collectionExportPath, - ); - await ensureElectron().checkExistsAndCreateDir( + const collectionExportPath = `${exportDir}/${collectionExportName}`; + await fs.mkdirIfNeeded(collectionExportPath); + await fs.mkdirIfNeeded( getMetadataFolderExportPath(collectionExportPath), ); await this.downloadAndSave( @@ -750,108 +727,32 @@ class ExportService { try { const fileExportName = fileIDExportNameMap.get(fileUID); const collectionID = getCollectionIDFromFileUID(fileUID); - const collectionExportPath = getCollectionExportPath( - exportDir, - collectionIDExportNameMap.get(collectionID), - ); + const collectionExportName = + collectionIDExportNameMap.get(collectionID); + await this.removeFileExportedRecord(exportDir, fileUID); try { if (isLivePhotoExportName(fileExportName)) { - const { - image: imageExportName, - video: videoExportName, - } = parseLivePhotoExportName(fileExportName); - const imageExportPath = getFileExportPath( - collectionExportPath, - imageExportName, - ); - log.info( - `moving image file ${imageExportPath} to trash folder`, - ); - if (await this.exists(imageExportPath)) { - await ensureElectron().moveFile( - imageExportPath, - await getTrashedFileExportPath( - exportDir, - imageExportPath, - ), - ); - } + const { image, video } = + parseLivePhotoExportName(fileExportName); - const imageMetadataFileExportPath = - getMetadataFileExportPath(imageExportPath); - - if ( - await this.exists(imageMetadataFileExportPath) - ) { - await ensureElectron().moveFile( - imageMetadataFileExportPath, - await getTrashedFileExportPath( - exportDir, - imageMetadataFileExportPath, - ), - ); - } - - const videoExportPath = getFileExportPath( - collectionExportPath, - videoExportName, + await moveToTrash( + exportDir, + collectionExportName, + image, ); - log.info( - `moving video file ${videoExportPath} to trash folder`, + + await moveToTrash( + exportDir, + collectionExportName, + video, ); - if (await this.exists(videoExportPath)) { - await ensureElectron().moveFile( - videoExportPath, - await getTrashedFileExportPath( - exportDir, - videoExportPath, - ), - ); - } - const videoMetadataFileExportPath = - getMetadataFileExportPath(videoExportPath); - if ( - await this.exists(videoMetadataFileExportPath) - ) { - await ensureElectron().moveFile( - videoMetadataFileExportPath, - await getTrashedFileExportPath( - exportDir, - videoMetadataFileExportPath, - ), - ); - } } else { - const fileExportPath = getFileExportPath( - collectionExportPath, + await moveToTrash( + exportDir, + collectionExportName, fileExportName, ); - const trashedFilePath = - await getTrashedFileExportPath( - exportDir, - fileExportPath, - ); - log.info( - `moving file ${fileExportPath} to ${trashedFilePath} trash folder`, - ); - if (await this.exists(fileExportPath)) { - await ensureElectron().moveFile( - fileExportPath, - trashedFilePath, - ); - } - const metadataFileExportPath = - getMetadataFileExportPath(fileExportPath); - if (await this.exists(metadataFileExportPath)) { - await ensureElectron().moveFile( - metadataFileExportPath, - await getTrashedFileExportPath( - exportDir, - metadataFileExportPath, - ), - ); - } } } catch (e) { await this.addFileExportedRecord( @@ -861,7 +762,7 @@ class ExportService { ); throw e; } - log.info(`trashing file with id ${fileUID} successful`); + log.info(`Moved file id ${fileUID} to Trash`); } catch (e) { log.error("trashing failed for a file", e); if ( @@ -984,7 +885,7 @@ class ExportService { const exportRecord = await this.getExportRecord(folder); const newRecord: ExportRecord = { ...exportRecord, ...newData }; await ensureElectron().saveFileToDisk( - `${folder}/${EXPORT_RECORD_FILE_NAME}`, + `${folder}/${exportRecordFileName}`, JSON.stringify(newRecord, null, 2), ); return newRecord; @@ -998,14 +899,16 @@ class ExportService { } async getExportRecord(folder: string, retry = true): Promise { + const electron = ensureElectron(); + const fs = electron.fs; try { await this.verifyExportFolderExists(folder); - const exportRecordJSONPath = `${folder}/${EXPORT_RECORD_FILE_NAME}`; - if (!(await this.exists(exportRecordJSONPath))) { + const exportRecordJSONPath = `${folder}/${exportRecordFileName}`; + if (!(await fs.exists(exportRecordJSONPath))) { return this.createEmptyExportRecord(exportRecordJSONPath); } const recordFile = - await ensureElectron().readTextFile(exportRecordJSONPath); + await electron.readTextFile(exportRecordJSONPath); try { return JSON.parse(recordFile); } catch (e) { @@ -1031,18 +934,17 @@ class ExportService { collectionID: number, collectionIDNameMap: Map, ) { + const fs = ensureElectron().fs; await this.verifyExportFolderExists(exportFolder); const collectionName = collectionIDNameMap.get(collectionID); - const collectionExportName = await getUniqueCollectionExportName( + const collectionExportName = await safeDirectoryName( exportFolder, collectionName, + fs.exists, ); - const collectionExportPath = getCollectionExportPath( - exportFolder, - collectionExportName, - ); - await ensureElectron().checkExistsAndCreateDir(collectionExportPath); - await ensureElectron().checkExistsAndCreateDir( + const collectionExportPath = `${exportFolder}/${collectionExportName}`; + await fs.mkdirIfNeeded(collectionExportPath); + await fs.mkdirIfNeeded( getMetadataFolderExportPath(collectionExportPath), ); @@ -1054,6 +956,7 @@ class ExportService { collectionExportPath: string, file: EnteFile, ): Promise { + const electron = ensureElectron(); try { const fileUID = getExportRecordFileUID(file); const originalFileStream = await downloadManager.getFile(file); @@ -1074,9 +977,10 @@ class ExportService { file, ); } else { - const fileExportName = await getUniqueFileExportName( + const fileExportName = await safeFileName( collectionExportPath, file.metadata.title, + electron.fs.exists, ); await this.addFileExportedRecord( exportDir, @@ -1089,8 +993,8 @@ class ExportService { fileExportName, file, ); - await ensureElectron().saveStreamToDisk( - getFileExportPath(collectionExportPath, fileExportName), + await electron.saveStreamToDisk( + `${collectionExportPath}/${fileExportName}`, updatedFileStream, ); } catch (e) { @@ -1111,15 +1015,18 @@ class ExportService { fileStream: ReadableStream, file: EnteFile, ) { + const electron = ensureElectron(); const fileBlob = await new Response(fileStream).blob(); const livePhoto = await decodeLivePhoto(file, fileBlob); - const imageExportName = await getUniqueFileExportName( + const imageExportName = await safeFileName( collectionExportPath, livePhoto.imageNameTitle, + electron.fs.exists, ); - const videoExportName = await getUniqueFileExportName( + const videoExportName = await safeFileName( collectionExportPath, livePhoto.videoNameTitle, + electron.fs.exists, ); const livePhotoExportName = getLivePhotoExportName( imageExportName, @@ -1137,8 +1044,8 @@ class ExportService { imageExportName, file, ); - await ensureElectron().saveStreamToDisk( - getFileExportPath(collectionExportPath, imageExportName), + await electron.saveStreamToDisk( + `${collectionExportPath}/${imageExportName}`, imageStream, ); @@ -1149,13 +1056,13 @@ class ExportService { file, ); try { - await ensureElectron().saveStreamToDisk( - getFileExportPath(collectionExportPath, videoExportName), + await electron.saveStreamToDisk( + `${collectionExportPath}/${videoExportName}`, videoStream, ); } catch (e) { - await ensureElectron().deleteFile( - getFileExportPath(collectionExportPath, imageExportName), + await electron.fs.rm( + `${collectionExportPath}/${imageExportName}`, ); throw e; } @@ -1180,20 +1087,8 @@ class ExportService { return this.exportInProgress; }; - exists = (path: string) => { - return ensureElectron().fs.exists(path); - }; - - rename = (oldPath: string, newPath: string) => { - return ensureElectron().rename(oldPath, newPath); - }; - - checkExistsAndCreateDir = (path: string) => { - return ensureElectron().checkExistsAndCreateDir(path); - }; - exportFolderExists = async (exportFolder: string) => { - return exportFolder && (await this.exists(exportFolder)); + return exportFolder && (await ensureElectron().fs.exists(exportFolder)); }; private verifyExportFolderExists = async (exportFolder: string) => { @@ -1218,4 +1113,297 @@ class ExportService { return exportRecord; }; } -export default new ExportService(); + +const exportService = new ExportService(); + +export default exportService; + +/** + * If there are any in-progress exports, or if continuous exports are enabled, + * resume them. + */ +export const resumeExportsIfNeeded = async () => { + const exportSettings = exportService.getExportSettings(); + if (!(await exportService.exportFolderExists(exportSettings?.folder))) { + return; + } + const exportRecord = await exportService.getExportRecord( + exportSettings.folder, + ); + if (exportSettings.continuousExport) { + exportService.enableContinuousExport(); + } + if (isExportInProgress(exportRecord.stage)) { + log.debug(() => "Resuming in-progress export"); + exportService.scheduleExport(); + } +}; + +/** + * Prompt the user to select a directory and create an export directory in it. + * + * If the user cancels the selection, return undefined. + */ +export const selectAndPrepareExportDirectory = async (): Promise< + string | undefined +> => { + const electron = ensureElectron(); + + const rootDir = await electron.selectDirectory(); + if (!rootDir) return undefined; + + const exportDir = `${rootDir}/${exportDirectoryName}`; + await electron.fs.mkdirIfNeeded(exportDir); + return exportDir; +}; + +export const getExportRecordFileUID = (file: EnteFile) => + `${file.id}_${file.collectionID}_${file.updationTime}`; + +export const getCollectionIDFromFileUID = (fileUID: string) => + Number(fileUID.split("_")[1]); + +const convertCollectionIDExportNameObjectToMap = ( + collectionExportNames: CollectionExportNames, +): Map => { + return new Map( + Object.entries(collectionExportNames ?? {}).map((e) => { + return [Number(e[0]), String(e[1])]; + }), + ); +}; + +const convertFileIDExportNameObjectToMap = ( + fileExportNames: FileExportNames, +): Map => { + return new Map( + Object.entries(fileExportNames ?? {}).map((e) => { + return [String(e[0]), String(e[1])]; + }), + ); +}; + +const getRenamedExportedCollections = ( + collections: Collection[], + exportRecord: ExportRecord, +) => { + if (!exportRecord?.collectionExportNames) { + return []; + } + const collectionIDExportNameMap = convertCollectionIDExportNameObjectToMap( + exportRecord.collectionExportNames, + ); + const renamedCollections = collections.filter((collection) => { + if (collectionIDExportNameMap.has(collection.id)) { + const currentExportName = collectionIDExportNameMap.get( + collection.id, + ); + + const collectionExportName = + getCollectionUserFacingName(collection); + + if (currentExportName === collectionExportName) { + return false; + } + const hasNumberedSuffix = currentExportName.match(/\(\d+\)$/); + const currentExportNameWithoutNumberedSuffix = hasNumberedSuffix + ? currentExportName.replace(/\(\d+\)$/, "") + : currentExportName; + + return ( + collectionExportName !== currentExportNameWithoutNumberedSuffix + ); + } + return false; + }); + return renamedCollections; +}; + +const getDeletedExportedCollections = ( + collections: Collection[], + exportRecord: ExportRecord, +) => { + if (!exportRecord?.collectionExportNames) { + return []; + } + const presentCollections = new Set( + collections.map((collection) => collection.id), + ); + const deletedExportedCollections = Object.keys( + exportRecord?.collectionExportNames, + ) + .map(Number) + .filter((collectionID) => { + if (!presentCollections.has(collectionID)) { + return true; + } + return false; + }); + return deletedExportedCollections; +}; + +const getUnExportedFiles = ( + allFiles: EnteFile[], + exportRecord: ExportRecord, +) => { + if (!exportRecord?.fileExportNames) { + return allFiles; + } + const exportedFiles = new Set(Object.keys(exportRecord?.fileExportNames)); + const unExportedFiles = allFiles.filter((file) => { + if (!exportedFiles.has(getExportRecordFileUID(file))) { + return true; + } + return false; + }); + return unExportedFiles; +}; + +const getDeletedExportedFiles = ( + allFiles: EnteFile[], + exportRecord: ExportRecord, +): string[] => { + if (!exportRecord?.fileExportNames) { + return []; + } + const presentFileUIDs = new Set( + allFiles?.map((file) => getExportRecordFileUID(file)), + ); + const deletedExportedFiles = Object.keys( + exportRecord?.fileExportNames, + ).filter((fileUID) => { + if (!presentFileUIDs.has(fileUID)) { + return true; + } + return false; + }); + return deletedExportedFiles; +}; + +const getCollectionExportedFiles = ( + exportRecord: ExportRecord, + collectionID: number, +): string[] => { + if (!exportRecord?.fileExportNames) { + return []; + } + const collectionExportedFiles = Object.keys( + exportRecord?.fileExportNames, + ).filter((fileUID) => { + const fileCollectionID = Number(fileUID.split("_")[1]); + if (fileCollectionID === collectionID) { + return true; + } else { + return false; + } + }); + return collectionExportedFiles; +}; + +const getGoogleLikeMetadataFile = (fileExportName: string, file: EnteFile) => { + const metadata: Metadata = file.metadata; + const creationTime = Math.floor(metadata.creationTime / 1000000); + const modificationTime = Math.floor( + (metadata.modificationTime ?? metadata.creationTime) / 1000000, + ); + const captionValue: string = file?.pubMagicMetadata?.data?.caption; + return JSON.stringify( + { + title: fileExportName, + caption: captionValue, + creationTime: { + timestamp: creationTime, + formatted: formatDateTimeShort(creationTime * 1000), + }, + modificationTime: { + timestamp: modificationTime, + formatted: formatDateTimeShort(modificationTime * 1000), + }, + geoData: { + latitude: metadata.latitude, + longitude: metadata.longitude, + }, + }, + null, + 2, + ); +}; + +export const getMetadataFolderExportPath = (collectionExportPath: string) => + `${collectionExportPath}/${exportMetadataDirectoryName}`; + +// if filepath is /home/user/Ente/Export/Collection1/1.jpg +// then metadata path is /home/user/Ente/Export/Collection1/ENTE_METADATA_FOLDER/1.jpg.json +const getFileMetadataExportPath = ( + collectionExportPath: string, + fileExportName: string, +) => + `${collectionExportPath}/${exportMetadataDirectoryName}/${fileExportName}.json`; + +export const getLivePhotoExportName = ( + imageExportName: string, + videoExportName: string, +) => + JSON.stringify({ + image: imageExportName, + video: videoExportName, + }); + +export const isLivePhotoExportName = (exportName: string) => { + try { + JSON.parse(exportName); + return true; + } catch (e) { + return false; + } +}; + +const parseLivePhotoExportName = ( + livePhotoExportName: string, +): { image: string; video: string } => { + const { image, video } = JSON.parse(livePhotoExportName); + return { image, video }; +}; + +const isExportInProgress = (exportStage: ExportStage) => + exportStage > ExportStage.INIT && exportStage < ExportStage.FINISHED; + +/** + * Move {@link fileName} in {@link collectionName} to Trash. + * + * Also move its associated metadata JSON to Trash. + * + * @param exportDir The root directory on the user's filesystem where we are + * exporting to. + * */ +const moveToTrash = async ( + exportDir: string, + collectionName: string, + fileName: string, +) => { + const fs = ensureElectron().fs; + + const filePath = `${exportDir}/${collectionName}/${fileName}`; + const trashDir = `${exportDir}/${exportTrashDirectoryName}/${collectionName}`; + const metadataFileName = `${fileName}.json`; + const metadataFilePath = `${exportDir}/${collectionName}/${exportMetadataDirectoryName}/${metadataFileName}`; + const metadataTrashDir = `${exportDir}/${exportTrashDirectoryName}/${collectionName}/${exportMetadataDirectoryName}`; + + log.info(`Moving file ${filePath} and its metadata to trash folder`); + + if (await fs.exists(filePath)) { + await fs.mkdirIfNeeded(trashDir); + const trashFilePath = await safeFileName(trashDir, fileName, fs.exists); + await fs.rename(filePath, trashFilePath); + } + + if (await fs.exists(metadataFilePath)) { + await fs.mkdirIfNeeded(metadataTrashDir); + const metadataTrashFilePath = await safeFileName( + metadataTrashDir, + metadataFileName, + fs.exists, + ); + await fs.rename(filePath, metadataTrashFilePath); + } +}; diff --git a/web/apps/photos/src/services/export/migration.ts b/web/apps/photos/src/services/export/migration.ts index 6c79420ed..b90c12e1c 100644 --- a/web/apps/photos/src/services/export/migration.ts +++ b/web/apps/photos/src/services/export/migration.ts @@ -1,3 +1,4 @@ +import { ensureElectron } from "@/next/electron"; import log from "@/next/log"; import { LS_KEYS, getData } from "@ente/shared/storage/localStorage"; import { User } from "@ente/shared/user/types"; @@ -15,34 +16,29 @@ import { ExportRecordV0, ExportRecordV1, ExportRecordV2, + ExportedCollectionPaths, FileExportNames, } from "types/export"; import { EnteFile } from "types/file"; import { getNonEmptyPersonalCollections } from "utils/collection"; -import { - getCollectionExportPath, - getCollectionIDFromFileUID, - getExportRecordFileUID, - getLivePhotoExportName, - getMetadataFolderExportPath, -} from "utils/export"; -import { - convertCollectionIDFolderPathObjectToMap, - getExportedFiles, - getFileMetadataSavePath, - getFileSavePath, - getOldCollectionFolderPath, - getOldFileMetadataSavePath, - getOldFileSavePath, - getUniqueCollectionFolderPath, - getUniqueFileExportNameForMigration, - getUniqueFileSaveName, -} from "utils/export/migration"; +import { splitFilenameAndExtension } from "utils/ffmpeg"; import { getIDBasedSortedFiles, getPersonalFiles, mergeMetadata, } from "utils/file"; +import { + safeDirectoryName, + safeFileName, + sanitizeFilename, +} from "utils/native-fs"; +import { + exportMetadataDirectoryName, + getCollectionIDFromFileUID, + getExportRecordFileUID, + getLivePhotoExportName, + getMetadataFolderExportPath, +} from "."; import exportService from "./index"; export async function migrateExport( @@ -193,40 +189,29 @@ async function migrationV4ToV5(exportDir: string, exportRecord: ExportRecord) { await removeCollectionExportMissingMetadataFolder(exportDir, exportRecord); } -/* - This updates the folder name of already exported folders from the earlier format of - `collectionID_collectionName` to newer `collectionName(numbered)` format -*/ -async function migrateCollectionFolders( +/** + * Update the folder name of already exported folders from the earlier format of + * `collectionID_collectionName` to newer `collectionName(numbered)` format. + */ +const migrateCollectionFolders = async ( collections: Collection[], exportDir: string, collectionIDPathMap: Map, -) { +) => { + const fs = ensureElectron().fs; for (const collection of collections) { - const oldCollectionExportPath = getOldCollectionFolderPath( - exportDir, - collection.id, - collection.name, - ); - const newCollectionExportPath = await getUniqueCollectionFolderPath( + const oldPath = `${exportDir}/${collection.id}_${oldSanitizeName(collection.name)}`; + const newPath = await safeDirectoryName( exportDir, collection.name, + fs.exists, ); - collectionIDPathMap.set(collection.id, newCollectionExportPath); - if (!(await exportService.exists(oldCollectionExportPath))) { - continue; - } - await exportService.rename( - oldCollectionExportPath, - newCollectionExportPath, - ); - await addCollectionExportedRecordV1( - exportDir, - collection.id, - newCollectionExportPath, - ); + collectionIDPathMap.set(collection.id, newPath); + if (!(await fs.exists(oldPath))) continue; + await fs.rename(oldPath, newPath); + await addCollectionExportedRecordV1(exportDir, collection.id, newPath); } -} +}; /* This updates the file name of already exported files from the earlier format of @@ -236,37 +221,27 @@ async function migrateFiles( files: EnteFile[], collectionIDPathMap: Map, ) { + const fs = ensureElectron().fs; for (const file of files) { - const oldFileSavePath = getOldFileSavePath( - collectionIDPathMap.get(file.collectionID), - file, - ); - const oldFileMetadataSavePath = getOldFileMetadataSavePath( - collectionIDPathMap.get(file.collectionID), - file, - ); - const newFileSaveName = await getUniqueFileSaveName( - collectionIDPathMap.get(file.collectionID), + const collectionPath = collectionIDPathMap.get(file.collectionID); + const metadataPath = `${collectionPath}/${exportMetadataDirectoryName}`; + + const oldFileName = `${file.id}_${oldSanitizeName(file.metadata.title)}`; + const oldFilePath = `${collectionPath}/${oldFileName}`; + const oldFileMetadataPath = `${metadataPath}/${oldFileName}.json`; + + const newFileName = await safeFileName( + collectionPath, file.metadata.title, + fs.exists, ); + const newFilePath = `${collectionPath}/${newFileName}`; + const newFileMetadataPath = `${metadataPath}/${newFileName}.json`; - const newFileSavePath = getFileSavePath( - collectionIDPathMap.get(file.collectionID), - newFileSaveName, - ); + if (!(await fs.exists(oldFilePath))) continue; - const newFileMetadataSavePath = getFileMetadataSavePath( - collectionIDPathMap.get(file.collectionID), - newFileSaveName, - ); - if (!(await exportService.exists(oldFileSavePath))) { - continue; - } - await exportService.rename(oldFileSavePath, newFileSavePath); - await exportService.rename( - oldFileMetadataSavePath, - newFileMetadataSavePath, - ); + await fs.rename(oldFilePath, newFilePath); + await fs.rename(oldFileMetadataPath, newFileMetadataPath); } } @@ -426,6 +401,7 @@ async function removeCollectionExportMissingMetadataFolder( exportDir: string, exportRecord: ExportRecord, ) { + const fs = ensureElectron().fs; if (!exportRecord?.collectionExportNames) { return; } @@ -439,9 +415,9 @@ async function removeCollectionExportMissingMetadataFolder( collectionExportName, ] of properlyExportedCollectionsAll) { if ( - await exportService.exists( + await fs.exists( getMetadataFolderExportPath( - getCollectionExportPath(exportDir, collectionExportName), + `${exportDir}/${collectionExportName}`, ), ) ) { @@ -475,3 +451,68 @@ async function removeCollectionExportMissingMetadataFolder( }; await exportService.updateExportRecord(exportDir, updatedExportRecord); } + +const convertCollectionIDFolderPathObjectToMap = ( + exportedCollectionPaths: ExportedCollectionPaths, +): Map => { + return new Map( + Object.entries(exportedCollectionPaths ?? {}).map((e) => { + return [Number(e[0]), String(e[1])]; + }), + ); +}; + +const getExportedFiles = ( + allFiles: EnteFile[], + exportRecord: ExportRecordV0 | ExportRecordV1 | ExportRecordV2, +) => { + if (!exportRecord?.exportedFiles) { + return []; + } + const exportedFileIds = new Set(exportRecord?.exportedFiles); + const exportedFiles = allFiles.filter((file) => { + if (exportedFileIds.has(getExportRecordFileUID(file))) { + return true; + } else { + return false; + } + }); + return exportedFiles; +}; + +const oldSanitizeName = (name: string) => + name.replaceAll("/", "_").replaceAll(" ", "_"); + +const getFileSavePath = (collectionFolderPath: string, fileSaveName: string) => + `${collectionFolderPath}/${fileSaveName}`; + +const getUniqueFileExportNameForMigration = ( + collectionPath: string, + filename: string, + usedFilePaths: Map>, +) => { + let fileExportName = sanitizeFilename(filename); + let count = 1; + while ( + usedFilePaths + .get(collectionPath) + ?.has(getFileSavePath(collectionPath, fileExportName)) + ) { + const filenameParts = splitFilenameAndExtension( + sanitizeFilename(filename), + ); + if (filenameParts[1]) { + fileExportName = `${filenameParts[0]}(${count}).${filenameParts[1]}`; + } else { + fileExportName = `${filenameParts[0]}(${count})`; + } + count++; + } + if (!usedFilePaths.has(collectionPath)) { + usedFilePaths.set(collectionPath, new Set()); + } + usedFilePaths + .get(collectionPath) + .add(getFileSavePath(collectionPath, fileExportName)); + return fileExportName; +}; diff --git a/web/apps/photos/src/types/export/index.ts b/web/apps/photos/src/types/export/index.ts index ce85f32fd..64ef249ed 100644 --- a/web/apps/photos/src/types/export/index.ts +++ b/web/apps/photos/src/types/export/index.ts @@ -1,4 +1,4 @@ -import { ExportStage } from "constants/export"; +import type { ExportStage } from "services/export"; import { EnteFile } from "types/file"; export interface ExportProgress { diff --git a/web/apps/photos/src/utils/collection/index.ts b/web/apps/photos/src/utils/collection/index.ts index c18861515..b0116964f 100644 --- a/web/apps/photos/src/utils/collection/index.ts +++ b/web/apps/photos/src/utils/collection/index.ts @@ -1,6 +1,6 @@ +import { ensureElectron } from "@/next/electron"; import log from "@/next/log"; import { CustomError } from "@ente/shared/error"; -import { getAlbumsURL } from "@ente/shared/network/api"; import { LS_KEYS, getData } from "@ente/shared/storage/localStorage"; import { getUnixTimeInMicroSecondsWithDelta } from "@ente/shared/time"; import { User } from "@ente/shared/user/types"; @@ -30,7 +30,6 @@ import { updatePublicCollectionMagicMetadata, updateSharedCollectionMagicMetadata, } from "services/collectionService"; -import exportService from "services/export"; import { getAllLocalFiles, getLocalFiles } from "services/fileService"; import { COLLECTION_ROLE, @@ -42,12 +41,9 @@ import { import { EnteFile } from "types/file"; import { SetFilesDownloadProgressAttributes } from "types/gallery"; import { SUB_TYPE, VISIBILITY_STATE } from "types/magicMetadata"; -import { - getCollectionExportPath, - getUniqueCollectionExportName, -} from "utils/export"; import { downloadFilesWithProgress } from "utils/file"; import { isArchivedCollection, updateMagicMetadata } from "utils/magicMetadata"; +import { safeDirectoryName } from "utils/native-fs"; export enum COLLECTION_OPS_TYPE { ADD, @@ -172,15 +168,14 @@ async function createCollectionDownloadFolder( downloadDirPath: string, collectionName: string, ) { - const collectionDownloadName = await getUniqueCollectionExportName( + const fs = ensureElectron().fs; + const collectionDownloadName = await safeDirectoryName( downloadDirPath, collectionName, + fs.exists, ); - const collectionDownloadPath = getCollectionExportPath( - downloadDirPath, - collectionDownloadName, - ); - await exportService.checkExistsAndCreateDir(collectionDownloadPath); + const collectionDownloadPath = `${downloadDirPath}/${collectionDownloadName}`; + await fs.mkdirIfNeeded(collectionDownloadPath); return collectionDownloadPath; } @@ -193,11 +188,6 @@ export function appendCollectionKeyToShareURL( } const sharableURL = new URL(url); - const albumsURL = new URL(getAlbumsURL()); - - sharableURL.protocol = albumsURL.protocol; - sharableURL.host = albumsURL.host; - sharableURL.pathname = albumsURL.pathname; const bytes = Buffer.from(collectionKey, "base64"); sharableURL.hash = bs58.encode(bytes); diff --git a/web/apps/photos/src/utils/export/index.ts b/web/apps/photos/src/utils/export/index.ts deleted file mode 100644 index a98e431b2..000000000 --- a/web/apps/photos/src/utils/export/index.ts +++ /dev/null @@ -1,312 +0,0 @@ -import exportService from "services/export"; -import { Collection } from "types/collection"; -import { - CollectionExportNames, - ExportRecord, - FileExportNames, -} from "types/export"; - -import { EnteFile } from "types/file"; - -import { formatDateTimeShort } from "@ente/shared/time/format"; -import { - ENTE_METADATA_FOLDER, - ENTE_TRASH_FOLDER, - ExportStage, -} from "constants/export"; -import sanitize from "sanitize-filename"; -import { Metadata } from "types/upload"; -import { getCollectionUserFacingName } from "utils/collection"; -import { splitFilenameAndExtension } from "utils/file"; - -export const getExportRecordFileUID = (file: EnteFile) => - `${file.id}_${file.collectionID}_${file.updationTime}`; - -export const getCollectionIDFromFileUID = (fileUID: string) => - Number(fileUID.split("_")[1]); - -export const convertCollectionIDExportNameObjectToMap = ( - collectionExportNames: CollectionExportNames, -): Map => { - return new Map( - Object.entries(collectionExportNames ?? {}).map((e) => { - return [Number(e[0]), String(e[1])]; - }), - ); -}; - -export const convertFileIDExportNameObjectToMap = ( - fileExportNames: FileExportNames, -): Map => { - return new Map( - Object.entries(fileExportNames ?? {}).map((e) => { - return [String(e[0]), String(e[1])]; - }), - ); -}; - -export const getRenamedExportedCollections = ( - collections: Collection[], - exportRecord: ExportRecord, -) => { - if (!exportRecord?.collectionExportNames) { - return []; - } - const collectionIDExportNameMap = convertCollectionIDExportNameObjectToMap( - exportRecord.collectionExportNames, - ); - const renamedCollections = collections.filter((collection) => { - if (collectionIDExportNameMap.has(collection.id)) { - const currentExportName = collectionIDExportNameMap.get( - collection.id, - ); - - const collectionExportName = - getCollectionUserFacingName(collection); - - if (currentExportName === collectionExportName) { - return false; - } - const hasNumberedSuffix = currentExportName.match(/\(\d+\)$/); - const currentExportNameWithoutNumberedSuffix = hasNumberedSuffix - ? currentExportName.replace(/\(\d+\)$/, "") - : currentExportName; - - return ( - collectionExportName !== currentExportNameWithoutNumberedSuffix - ); - } - return false; - }); - return renamedCollections; -}; - -export const getDeletedExportedCollections = ( - collections: Collection[], - exportRecord: ExportRecord, -) => { - if (!exportRecord?.collectionExportNames) { - return []; - } - const presentCollections = new Set( - collections.map((collection) => collection.id), - ); - const deletedExportedCollections = Object.keys( - exportRecord?.collectionExportNames, - ) - .map(Number) - .filter((collectionID) => { - if (!presentCollections.has(collectionID)) { - return true; - } - return false; - }); - return deletedExportedCollections; -}; - -export const getUnExportedFiles = ( - allFiles: EnteFile[], - exportRecord: ExportRecord, -) => { - if (!exportRecord?.fileExportNames) { - return allFiles; - } - const exportedFiles = new Set(Object.keys(exportRecord?.fileExportNames)); - const unExportedFiles = allFiles.filter((file) => { - if (!exportedFiles.has(getExportRecordFileUID(file))) { - return true; - } - return false; - }); - return unExportedFiles; -}; - -export const getDeletedExportedFiles = ( - allFiles: EnteFile[], - exportRecord: ExportRecord, -): string[] => { - if (!exportRecord?.fileExportNames) { - return []; - } - const presentFileUIDs = new Set( - allFiles?.map((file) => getExportRecordFileUID(file)), - ); - const deletedExportedFiles = Object.keys( - exportRecord?.fileExportNames, - ).filter((fileUID) => { - if (!presentFileUIDs.has(fileUID)) { - return true; - } - return false; - }); - return deletedExportedFiles; -}; - -export const getCollectionExportedFiles = ( - exportRecord: ExportRecord, - collectionID: number, -): string[] => { - if (!exportRecord?.fileExportNames) { - return []; - } - const collectionExportedFiles = Object.keys( - exportRecord?.fileExportNames, - ).filter((fileUID) => { - const fileCollectionID = Number(fileUID.split("_")[1]); - if (fileCollectionID === collectionID) { - return true; - } else { - return false; - } - }); - return collectionExportedFiles; -}; - -export const getGoogleLikeMetadataFile = ( - fileExportName: string, - file: EnteFile, -) => { - const metadata: Metadata = file.metadata; - const creationTime = Math.floor(metadata.creationTime / 1000000); - const modificationTime = Math.floor( - (metadata.modificationTime ?? metadata.creationTime) / 1000000, - ); - const captionValue: string = file?.pubMagicMetadata?.data?.caption; - return JSON.stringify( - { - title: fileExportName, - caption: captionValue, - creationTime: { - timestamp: creationTime, - formatted: formatDateTimeShort(creationTime * 1000), - }, - modificationTime: { - timestamp: modificationTime, - formatted: formatDateTimeShort(modificationTime * 1000), - }, - geoData: { - latitude: metadata.latitude, - longitude: metadata.longitude, - }, - }, - null, - 2, - ); -}; - -export const sanitizeName = (name: string) => - sanitize(name, { replacement: "_" }); - -export const getUniqueCollectionExportName = async ( - dir: string, - collectionName: string, -): Promise => { - let collectionExportName = sanitizeName(collectionName); - let count = 1; - while ( - (await exportService.exists( - getCollectionExportPath(dir, collectionExportName), - )) || - collectionExportName === ENTE_TRASH_FOLDER - ) { - collectionExportName = `${sanitizeName(collectionName)}(${count})`; - count++; - } - return collectionExportName; -}; - -export const getMetadataFolderExportPath = (collectionExportPath: string) => - `${collectionExportPath}/${ENTE_METADATA_FOLDER}`; - -export const getUniqueFileExportName = async ( - collectionExportPath: string, - filename: string, -) => { - let fileExportName = sanitizeName(filename); - let count = 1; - while ( - await exportService.exists( - getFileExportPath(collectionExportPath, fileExportName), - ) - ) { - const filenameParts = splitFilenameAndExtension(sanitizeName(filename)); - if (filenameParts[1]) { - fileExportName = `${filenameParts[0]}(${count}).${filenameParts[1]}`; - } else { - fileExportName = `${filenameParts[0]}(${count})`; - } - count++; - } - return fileExportName; -}; - -export const getFileMetadataExportPath = ( - collectionExportPath: string, - fileExportName: string, -) => `${collectionExportPath}/${ENTE_METADATA_FOLDER}/${fileExportName}.json`; - -export const getCollectionExportPath = ( - exportFolder: string, - collectionExportName: string, -) => `${exportFolder}/${collectionExportName}`; - -export const getFileExportPath = ( - collectionExportPath: string, - fileExportName: string, -) => `${collectionExportPath}/${fileExportName}`; - -export const getTrashedFileExportPath = async ( - exportDir: string, - path: string, -) => { - const fileRelativePath = path.replace(`${exportDir}/`, ""); - let trashedFilePath = `${exportDir}/${ENTE_TRASH_FOLDER}/${fileRelativePath}`; - let count = 1; - while (await exportService.exists(trashedFilePath)) { - const trashedFilePathParts = splitFilenameAndExtension(trashedFilePath); - if (trashedFilePathParts[1]) { - trashedFilePath = `${trashedFilePathParts[0]}(${count}).${trashedFilePathParts[1]}`; - } else { - trashedFilePath = `${trashedFilePathParts[0]}(${count})`; - } - count++; - } - return trashedFilePath; -}; - -// if filepath is /home/user/Ente/Export/Collection1/1.jpg -// then metadata path is /home/user/Ente/Export/Collection1/ENTE_METADATA_FOLDER/1.jpg.json -export const getMetadataFileExportPath = (filePath: string) => { - // extract filename and collection folder path - const filename = filePath.split("/").pop(); - const collectionExportPath = filePath.replace(`/${filename}`, ""); - return `${collectionExportPath}/${ENTE_METADATA_FOLDER}/${filename}.json`; -}; - -export const getLivePhotoExportName = ( - imageExportName: string, - videoExportName: string, -) => - JSON.stringify({ - image: imageExportName, - video: videoExportName, - }); - -export const isLivePhotoExportName = (exportName: string) => { - try { - JSON.parse(exportName); - return true; - } catch (e) { - return false; - } -}; - -export const parseLivePhotoExportName = ( - livePhotoExportName: string, -): { image: string; video: string } => { - const { image, video } = JSON.parse(livePhotoExportName); - return { image, video }; -}; - -export const isExportInProgress = (exportStage: ExportStage) => - exportStage > ExportStage.INIT && exportStage < ExportStage.FINISHED; diff --git a/web/apps/photos/src/utils/export/migration.ts b/web/apps/photos/src/utils/export/migration.ts deleted file mode 100644 index c8988cac4..000000000 --- a/web/apps/photos/src/utils/export/migration.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { ENTE_METADATA_FOLDER } from "constants/export"; -import exportService from "services/export"; -import { - ExportedCollectionPaths, - ExportRecordV0, - ExportRecordV1, - ExportRecordV2, -} from "types/export"; -import { EnteFile } from "types/file"; -import { splitFilenameAndExtension } from "utils/ffmpeg"; -import { getExportRecordFileUID, sanitizeName } from "."; - -export const convertCollectionIDFolderPathObjectToMap = ( - exportedCollectionPaths: ExportedCollectionPaths, -): Map => { - return new Map( - Object.entries(exportedCollectionPaths ?? {}).map((e) => { - return [Number(e[0]), String(e[1])]; - }), - ); -}; - -export const getExportedFiles = ( - allFiles: EnteFile[], - exportRecord: ExportRecordV0 | ExportRecordV1 | ExportRecordV2, -) => { - if (!exportRecord?.exportedFiles) { - return []; - } - const exportedFileIds = new Set(exportRecord?.exportedFiles); - const exportedFiles = allFiles.filter((file) => { - if (exportedFileIds.has(getExportRecordFileUID(file))) { - return true; - } else { - return false; - } - }); - return exportedFiles; -}; - -export const oldSanitizeName = (name: string) => - name.replaceAll("/", "_").replaceAll(" ", "_"); - -export const getUniqueCollectionFolderPath = async ( - dir: string, - collectionName: string, -): Promise => { - let collectionFolderPath = `${dir}/${sanitizeName(collectionName)}`; - let count = 1; - while (await exportService.exists(collectionFolderPath)) { - collectionFolderPath = `${dir}/${sanitizeName( - collectionName, - )}(${count})`; - count++; - } - return collectionFolderPath; -}; - -export const getMetadataFolderPath = (collectionFolderPath: string) => - `${collectionFolderPath}/${ENTE_METADATA_FOLDER}`; - -export const getUniqueFileSaveName = async ( - collectionPath: string, - filename: string, -) => { - let fileSaveName = sanitizeName(filename); - let count = 1; - while ( - await exportService.exists( - getFileSavePath(collectionPath, fileSaveName), - ) - ) { - const filenameParts = splitFilenameAndExtension(sanitizeName(filename)); - if (filenameParts[1]) { - fileSaveName = `${filenameParts[0]}(${count}).${filenameParts[1]}`; - } else { - fileSaveName = `${filenameParts[0]}(${count})`; - } - count++; - } - return fileSaveName; -}; - -export const getOldFileSaveName = (filename: string, fileID: number) => - `${fileID}_${oldSanitizeName(filename)}`; - -export const getFileMetadataSavePath = ( - collectionFolderPath: string, - fileSaveName: string, -) => `${collectionFolderPath}/${ENTE_METADATA_FOLDER}/${fileSaveName}.json`; - -export const getFileSavePath = ( - collectionFolderPath: string, - fileSaveName: string, -) => `${collectionFolderPath}/${fileSaveName}`; - -export const getOldCollectionFolderPath = ( - dir: string, - collectionID: number, - collectionName: string, -) => `${dir}/${collectionID}_${oldSanitizeName(collectionName)}`; - -export const getOldFileSavePath = ( - collectionFolderPath: string, - file: EnteFile, -) => - `${collectionFolderPath}/${file.id}_${oldSanitizeName( - file.metadata.title, - )}`; - -export const getOldFileMetadataSavePath = ( - collectionFolderPath: string, - file: EnteFile, -) => - `${collectionFolderPath}/${ENTE_METADATA_FOLDER}/${ - file.id - }_${oldSanitizeName(file.metadata.title)}.json`; - -export const getUniqueFileExportNameForMigration = ( - collectionPath: string, - filename: string, - usedFilePaths: Map>, -) => { - let fileExportName = sanitizeName(filename); - let count = 1; - while ( - usedFilePaths - .get(collectionPath) - ?.has(getFileSavePath(collectionPath, fileExportName)) - ) { - const filenameParts = splitFilenameAndExtension(sanitizeName(filename)); - if (filenameParts[1]) { - fileExportName = `${filenameParts[0]}(${count}).${filenameParts[1]}`; - } else { - fileExportName = `${filenameParts[0]}(${count})`; - } - count++; - } - if (!usedFilePaths.has(collectionPath)) { - usedFilePaths.set(collectionPath, new Set()); - } - usedFilePaths - .get(collectionPath) - .add(getFileSavePath(collectionPath, fileExportName)); - return fileExportName; -}; diff --git a/web/apps/photos/src/utils/file/index.ts b/web/apps/photos/src/utils/file/index.ts index 089c5f40d..8f72cb450 100644 --- a/web/apps/photos/src/utils/file/index.ts +++ b/web/apps/photos/src/utils/file/index.ts @@ -51,8 +51,8 @@ import { } from "types/gallery"; import { VISIBILITY_STATE } from "types/magicMetadata"; import { FileTypeInfo } from "types/upload"; -import { getFileExportPath, getUniqueFileExportName } from "utils/export"; import { isArchivedFile, updateMagicMetadata } from "utils/magicMetadata"; +import { safeFileName } from "utils/native-fs"; const WAIT_TIME_IMAGE_CONVERSION = 30 * 1000; @@ -812,38 +812,39 @@ async function downloadFileDesktop( if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) { const fileBlob = await new Response(updatedFileStream).blob(); const livePhoto = await decodeLivePhoto(file, fileBlob); - const imageExportName = await getUniqueFileExportName( + const imageExportName = await safeFileName( downloadPath, livePhoto.imageNameTitle, + electron.fs.exists, ); const imageStream = generateStreamFromArrayBuffer(livePhoto.image); await electron.saveStreamToDisk( - getFileExportPath(downloadPath, imageExportName), + `${downloadPath}/${imageExportName}`, imageStream, ); try { - const videoExportName = await getUniqueFileExportName( + const videoExportName = await safeFileName( downloadPath, livePhoto.videoNameTitle, + electron.fs.exists, ); const videoStream = generateStreamFromArrayBuffer(livePhoto.video); await electron.saveStreamToDisk( - getFileExportPath(downloadPath, videoExportName), + `${downloadPath}/${videoExportName}`, videoStream, ); } catch (e) { - await electron.deleteFile( - getFileExportPath(downloadPath, imageExportName), - ); + await electron.fs.rm(`${downloadPath}/${imageExportName}`); throw e; } } else { - const fileExportName = await getUniqueFileExportName( + const fileExportName = await safeFileName( downloadPath, file.metadata.title, + electron.fs.exists, ); await electron.saveStreamToDisk( - getFileExportPath(downloadPath, fileExportName), + `${downloadPath}/${fileExportName}`, updatedFileStream, ); } diff --git a/web/apps/photos/src/utils/native-fs.ts b/web/apps/photos/src/utils/native-fs.ts new file mode 100644 index 000000000..2ef896302 --- /dev/null +++ b/web/apps/photos/src/utils/native-fs.ts @@ -0,0 +1,79 @@ +/** + * @file Utilities for native filesystem access. + * + * While they don't have any direct dependencies to our desktop app, they were + * written for use by the code that runs in our desktop app. + */ + +import { nameAndExtension } from "@/next/file"; +import sanitize from "sanitize-filename"; +import { + exportMetadataDirectoryName, + exportTrashDirectoryName, +} from "services/export"; + +/** + * Sanitize string for use as file or directory name. + * + * Return a string suitable for use as a file or directory name by replacing + * directory separators and invalid characters in the input string {@link s} + * with "_". + */ +export const sanitizeFilename = (s: string) => + sanitize(s, { replacement: "_" }); + +/** + * Return a new sanitized and unique directory name based on {@link name} that + * is not the same as any existing item in the given {@link directoryPath}. + * + * We also ensure we don't return names which might collide with our own special + * directories. + * + * @param exists A function to check if an item already exists at the given + * path. Usually, you'd pass `fs.exists` from {@link Electron}. + * + * See also: {@link safeFileame} + */ +export const safeDirectoryName = async ( + directoryPath: string, + name: string, + exists: (path: string) => Promise, +): Promise => { + const specialDirectoryNames = [ + exportTrashDirectoryName, + exportMetadataDirectoryName, + ]; + + let result = sanitizeFilename(name); + let count = 1; + while ( + (await exists(`${directoryPath}/${result}`)) || + specialDirectoryNames.includes(result) + ) { + result = `${sanitizeFilename(name)}(${count})`; + count++; + } + return result; +}; + +/** + * Return a new sanitized and unique file name based on {@link name} that is not + * the same as any existing item in the given {@link directoryPath}. + * + * This is a sibling of {@link safeDirectoryName} for use with file names. + */ +export const safeFileName = async ( + directoryPath: string, + name: string, + exists: (path: string) => Promise, +) => { + let result = sanitizeFilename(name); + let count = 1; + while (await exists(`${directoryPath}/${result}`)) { + const [fn, ext] = nameAndExtension(sanitizeFilename(name)); + if (ext) result = `${fn}(${count}).${ext}`; + else result = `${fn}(${count})`; + count++; + } + return result; +}; diff --git a/web/apps/photos/src/utils/upload/index.ts b/web/apps/photos/src/utils/upload/index.ts index 6cce03aa9..643c931fe 100644 --- a/web/apps/photos/src/utils/upload/index.ts +++ b/web/apps/photos/src/utils/upload/index.ts @@ -1,4 +1,3 @@ -import { ENTE_METADATA_FOLDER } from "constants/export"; import { FILE_TYPE } from "constants/file"; import { A_SEC_IN_MICROSECONDS, @@ -6,6 +5,7 @@ import { PICKED_UPLOAD_TYPE, } from "constants/upload"; import isElectron from "is-electron"; +import { exportMetadataDirectoryName } from "services/export"; import { EnteFile } from "types/file"; import { ElectronFile, @@ -175,7 +175,7 @@ export function groupFilesBasedOnParentFolder( // For Eg,For FileList -> [a/x.png, a/metadata/x.png.json] // they will both we grouped into the collection "a" // This is cluster the metadata json files in the same collection as the file it is for - if (folderPath.endsWith(ENTE_METADATA_FOLDER)) { + if (folderPath.endsWith(exportMetadataDirectoryName)) { folderPath = folderPath.substring(0, folderPath.lastIndexOf("/")); } const folderName = folderPath.substring( diff --git a/web/docs/dependencies.md b/web/docs/dependencies.md index 813f0e3c9..d0660bb3e 100644 --- a/web/docs/dependencies.md +++ b/web/docs/dependencies.md @@ -17,6 +17,8 @@ package: - "eslint-plugin-react-hooks", "eslint-plugin-react-namespace-import" - Some React specific ESLint rules and configurations that are used by the workspaces that have React code. +- "eslint-plugin-react-refresh" - A plugin to ensure that React components are + exported in a way that they can be HMR-ed. - "prettier-plugin-organize-imports" - A Prettier plugin to sort imports. - "prettier-plugin-packagejson" - A Prettier plugin to also prettify `package.json`. @@ -128,3 +130,10 @@ For some of our newer code, we have started to use [Vite](https://vitejs.dev). It is more lower level than Next, but the bells and whistles it doesn't have are the bells and whistles (and the accompanying complexity) that we don't need in some cases. + +## Photos + +### Misc + +- "sanitize-filename" is for converting arbitrary strings into strings that + are suitable for being used as filenames. diff --git a/web/package.json b/web/package.json index 3b8697bd8..2d5919eb1 100644 --- a/web/package.json +++ b/web/package.json @@ -27,7 +27,7 @@ "dev:payments": "yarn workspace payments dev", "dev:photos": "yarn workspace photos next dev", "dev:staff": "yarn workspace staff dev", - "lint": "yarn prettier --check . && yarn workspaces run eslint --report-unused-disable-directives", + "lint": "yarn prettier --check . && yarn workspaces run eslint --report-unused-disable-directives .", "lint-fix": "yarn prettier --write . && yarn workspaces run eslint --fix .", "preview": "yarn preview:photos", "preview:accounts": "yarn build:accounts && python3 -m http.server -d apps/accounts/out 3001", diff --git a/web/packages/build-config/eslintrc-react.js b/web/packages/build-config/eslintrc-react.js index 13df31cfd..7a0f04fe5 100644 --- a/web/packages/build-config/eslintrc-react.js +++ b/web/packages/build-config/eslintrc-react.js @@ -5,5 +5,12 @@ module.exports = { "plugin:react/recommended", "plugin:react-hooks/recommended", ], + plugins: ["react-refresh"], settings: { react: { version: "18.2" } }, + rules: { + "react-refresh/only-export-components": [ + "warn", + { allowConstantExport: true }, + ], + }, }; diff --git a/web/packages/build-config/package.json b/web/packages/build-config/package.json index 52a29fb6a..e46bb96b1 100644 --- a/web/packages/build-config/package.json +++ b/web/packages/build-config/package.json @@ -7,6 +7,7 @@ "@typescript-eslint/parser": "^7", "eslint-plugin-react": "^7.34", "eslint-plugin-react-hooks": "^4.6", + "eslint-plugin-react-refresh": "^0.4.6", "prettier-plugin-organize-imports": "^3.2", "prettier-plugin-packagejson": "^2.4" } diff --git a/web/packages/eslint-config/index.js b/web/packages/eslint-config/index.js index 930ebab4e..ee73deae3 100644 --- a/web/packages/eslint-config/index.js +++ b/web/packages/eslint-config/index.js @@ -24,6 +24,7 @@ module.exports = { "max-len": "off", "new-cap": "off", "no-invalid-this": "off", + "no-throw-literal": "error", // TODO(MR): We want this off anyway, for now forcing it here eqeqeq: "off", "object-curly-spacing": ["error", "always"], diff --git a/web/packages/next/blob-cache.ts b/web/packages/next/blob-cache.ts index 30c290f8f..8789a5078 100644 --- a/web/packages/next/blob-cache.ts +++ b/web/packages/next/blob-cache.ts @@ -148,7 +148,6 @@ const openOPFSCacheWeb = async (name: BlobCacheNamespace) => { const root = await navigator.storage.getDirectory(); const caches = await root.getDirectoryHandle("cache", { create: true }); - // eslint-disable-next-line @typescript-eslint/no-unused-vars const cache = await caches.getDirectoryHandle(name, { create: true }); return { diff --git a/web/packages/next/file.ts b/web/packages/next/file.ts index 0f07ba3ce..b69fece50 100644 --- a/web/packages/next/file.ts +++ b/web/packages/next/file.ts @@ -1,5 +1,19 @@ import type { ElectronFile } from "./types/file"; +/** + * Split a filename into its components - the name itself, and the extension (if + * any) - returning both. The dot is not included in either. + * + * For example, `foo-bar.png` will be split into ["foo-bar", "png"]. + */ +export const nameAndExtension = ( + fileName: string, +): [string, string | undefined] => { + const i = fileName.lastIndexOf("."); + if (i == -1) return [fileName, undefined]; + else return [fileName.slice(0, i), fileName.slice(i + 1)]; +}; + export function getFileNameSize(file: File | ElectronFile) { return `${file.name}_${convertBytesToHumanReadable(file.size)}`; } diff --git a/web/packages/next/types/ipc.ts b/web/packages/next/types/ipc.ts index 5b0979eaa..69b0c3593 100644 --- a/web/packages/next/types/ipc.ts +++ b/web/packages/next/types/ipc.ts @@ -80,7 +80,7 @@ export interface Electron { * * If no such key is found, return `undefined`. * - * @see {@link saveEncryptionKey}. + * See also: {@link saveEncryptionKey}. */ encryptionKey: () => Promise; @@ -155,15 +155,39 @@ export interface Electron { * or watching some folders for changes and syncing them automatically. * * Towards this end, this fs object provides some generic file system access - * functions that are needed for such features. In addition, there are other - * feature specific methods too in the top level electron object. + * functions that are needed for such features (in some cases, there are + * other feature specific methods too in the top level electron object). */ fs: { - /** - * Return true if there is a file or directory at the given - * {@link path}. - */ + /** Return true if there is an item at the given {@link path}. */ exists: (path: string) => Promise; + + /** + * Equivalent of `mkdir -p`. + * + * Create a directory at the given path if it does not already exist. + * Any parent directories in the path that don't already exist will also + * be created recursively, i.e. this command is analogous to an running + * `mkdir -p`. + */ + mkdirIfNeeded: (dirPath: string) => Promise; + + /** Rename {@link oldPath} to {@link newPath} */ + rename: (oldPath: string, newPath: string) => Promise; + + /** + * Equivalent of `rmdir`. + * + * Delete the directory at the {@link path} if it is empty. + */ + rmdir: (path: string) => Promise; + + /** + * Equivalent of `rm`. + * + * Delete the file at {@link path}. + */ + rm: (path: string) => Promise; }; /* @@ -276,7 +300,6 @@ export interface Electron { ) => Promise; // - FS legacy - checkExistsAndCreateDir: (dirPath: string) => Promise; saveStreamToDisk: ( path: string, fileStream: ReadableStream, @@ -284,10 +307,6 @@ export interface Electron { saveFileToDisk: (path: string, contents: string) => Promise; readTextFile: (path: string) => Promise; isFolder: (dirPath: string) => Promise; - moveFile: (oldPath: string, newPath: string) => Promise; - deleteFolder: (path: string) => Promise; - deleteFile: (path: string) => Promise; - rename: (oldPath: string, newPath: string) => Promise; // - Upload diff --git a/web/packages/shared/components/EnteLogo.tsx b/web/packages/shared/components/EnteLogo.tsx index a22be3f5e..50c1e8303 100644 --- a/web/packages/shared/components/EnteLogo.tsx +++ b/web/packages/shared/components/EnteLogo.tsx @@ -3,6 +3,7 @@ import { styled } from "@mui/material"; const LogoImage = styled("img")` margin: 3px 0; pointer-events: none; + vertical-align: middle; `; export function EnteLogo(props) { diff --git a/web/packages/shared/network/HTTPService.ts b/web/packages/shared/network/HTTPService.ts index 350f7f01d..eda0709f5 100644 --- a/web/packages/shared/network/HTTPService.ts +++ b/web/packages/shared/network/HTTPService.ts @@ -125,7 +125,6 @@ class HTTPService { /** * Returns axios interceptors. */ - // eslint-disable-next-line class-methods-use-this public getInterceptors() { return axios.interceptors; } @@ -137,7 +136,6 @@ class HTTPService { * over what was sent in config. */ public async request(config: AxiosRequestConfig, customConfig?: any) { - // eslint-disable-next-line no-param-reassign config.headers = { ...this.headers, ...config.headers, diff --git a/web/packages/shared/themes/components.ts b/web/packages/shared/themes/components.ts index 10e122fe2..6d8eb3880 100644 --- a/web/packages/shared/themes/components.ts +++ b/web/packages/shared/themes/components.ts @@ -2,7 +2,6 @@ import { Shadow, ThemeColorsOptions } from "@mui/material"; import { Components } from "@mui/material/styles/components"; import { TypographyOptions } from "@mui/material/styles/createTypography"; -// eslint-disable-next-line @typescript-eslint/no-unused-vars export const getComponents = ( colors: ThemeColorsOptions, typography: TypographyOptions, diff --git a/web/yarn.lock b/web/yarn.lock index 90300ce69..11cc8b8e1 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -2290,6 +2290,11 @@ eslint-plugin-jsx-a11y@^6.7.1: resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz#4c3e697ad95b77e93f8646aaa1630c1ba607edd3" integrity sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g== +eslint-plugin-react-refresh@^0.4.6: + version "0.4.6" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.6.tgz#e8e8accab681861baed00c5c12da70267db0936f" + integrity sha512-NjGXdm7zgcKRkKMua34qVO9doI7VOxZ6ancSvBELJSSoX97jyndXcSoa8XBh69JoB31dNz3EEzlMcizZl7LaMA== + eslint-plugin-react@^7.33.2: version "7.33.2" resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.33.2.tgz#69ee09443ffc583927eafe86ffebb470ee737608"