Merge branch 'main' into mobile_face

This commit is contained in:
Neeraj Gupta 2024-04-20 15:59:36 +05:30
commit 864f8444d5
285 changed files with 7705 additions and 6901 deletions

View file

@ -0,0 +1,56 @@
name: "Internal Release - Photos"
on:
workflow_dispatch: # Allow manually running the action
env:
FLUTTER_VERSION: "3.19.3"
jobs:
build:
runs-on: ubuntu-latest
defaults:
run:
working-directory: mobile
steps:
- name: Checkout code and submodules
uses: actions/checkout@v4
with:
submodules: recursive
- name: Setup JDK 17
uses: actions/setup-java@v1
with:
java-version: 17
- name: Install Flutter ${{ env.FLUTTER_VERSION }}
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
- name: Setup keys
uses: timheuer/base64-to-file@v1
with:
fileName: "keystore/ente_photos_key.jks"
encodedString: ${{ secrets.SIGNING_KEY_PHOTOS }}
- name: Build PlayStore AAB
run: |
flutter build appbundle --release --flavor playstore
env:
SIGNING_KEY_PATH: "/home/runner/work/_temp/keystore/ente_photos_key.jks"
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS_PHOTOS }}
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD_PHOTOS }}
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD_PHOTOS }}
- name: Upload AAB to PlayStore
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
packageName: io.ente.photos
releaseFiles: mobile/build/app/outputs/bundle/playstoreRelease/app-playstore-release.aab
track: internal

View file

@ -9,7 +9,7 @@ on:
- ".github/workflows/mobile-lint.yml"
env:
FLUTTER_VERSION: "3.13.4"
FLUTTER_VERSION: "3.19.5"
jobs:
lint:

View file

@ -9,7 +9,7 @@ on:
- "photos-v*"
env:
FLUTTER_VERSION: "3.13.4"
FLUTTER_VERSION: "3.19.3"
jobs:
build:
@ -25,6 +25,11 @@ jobs:
with:
submodules: recursive
- name: Setup JDK 17
uses: actions/setup-java@v1
with:
java-version: 17
- name: Install Flutter ${{ env.FLUTTER_VERSION }}
uses: subosito/flutter-action@v2
with:

View file

@ -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": "اعدادات المطور"
}

View file

@ -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,20 @@
"copied": "Kopiert",
"pleaseTryAgain": "Bitte versuchen Sie es erneut",
"existingUser": "Bestehender Benutzer",
"newUser": "Neu bei ente",
"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_1": "Wie sicher ist Auth?",
"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",
@ -199,6 +195,7 @@
"recoveryKeySaveDescription": "Wir speichern diesen Schlüssel nicht. Sichern sie dieses diesen Schlüssel bestehend aus 24 Wörtern an einem sicheren Platz.",
"doThisLater": "Auf später verschieben",
"saveKey": "Schlüssel speichern",
"save": "Speichern",
"back": "Zurück",
"createAccount": "Account erstellen",
"passwordStrength": "Passwortstärke: {passwordStrengthValue}",
@ -346,7 +343,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."
@ -407,6 +403,7 @@
"doNotSignOut": "Nicht abmelden",
"hearUsWhereTitle": "Wie hast du von Ente erfahren? (optional)",
"hearUsExplanation": "Wir tracken keine App-Installationen. Es würde uns jedoch helfen, wenn du uns mitteilst, wie du von uns erfahren hast!",
"recoveryKeySaved": "Wiederherstellungsschlüssel im Downloads-Ordner gespeichert!",
"waitingForBrowserRequest": "Warten auf Browseranfrage...",
"waitingForVerification": "Warte auf Bestätigung...",
"passkey": "Passkey",

View file

@ -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."

View file

@ -85,7 +85,6 @@
"copied": "کپی شد",
"pleaseTryAgain": "لطفا دوباره تلاش کنید",
"existingUser": "کاربر موجود",
"newUser": "کاربر جدید ente",
"delete": "حذف",
"enterYourPasswordHint": "رمز عبور خود را وارد کنید",
"forgotPassword": "فراموشی رمز عبور",

View file

@ -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",

View file

@ -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 lidentité",
"@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}"
}

View file

@ -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": "עזוב משפחה",

View file

@ -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",
@ -196,6 +190,7 @@
"recoveryKeySaveDescription": "Non memorizziamo questa chiave, per favore salva questa chiave di 24 parole in un posto sicuro.",
"doThisLater": "Fallo più tardi",
"saveKey": "Salva chiave",
"save": "Salva",
"back": "Indietro",
"createAccount": "Crea account",
"passwordStrength": "Forza password: {passwordStrengthValue}",
@ -343,7 +338,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."
@ -403,5 +397,6 @@
"signOutOtherDevices": "Esci dagli altri dispositivi",
"doNotSignOut": "Non uscire",
"hearUsWhereTitle": "Dove hai sentito parlare di Ente? (opzionale)",
"hearUsExplanation": "Non teniamo traccia delle installazioni dell'app. Sarebbe utile se ci dicessi dove ci hai trovato!"
"hearUsExplanation": "Non teniamo traccia delle installazioni dell'app. Sarebbe utile se ci dicessi dove ci hai trovato!",
"passkey": "Passkey"
}

View file

@ -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": "パスキー",

View file

@ -74,7 +74,6 @@
"data": "მონაცემები",
"importCodes": "კოდების იმპორტირება",
"importTypePlainText": "სტანდარტული ტექსტი",
"importTypeEnteEncrypted": "ente დაშიფრული ექსპორტი",
"passwordForDecryptingExport": "ექსპორტის გაშიფრვის პაროლი",
"passwordEmptyError": "პაროლის ველი არ შეიძლება იყოს ცარიელი",
"emailVerificationToggle": "ელექტრონული ფოსტის ვერიფიკაცია",

View file

@ -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",

View file

@ -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."

View file

@ -78,14 +78,14 @@
"data": "Dados",
"importCodes": "Importar códigos",
"importTypePlainText": "Texto simples",
"importTypeEnteEncrypted": "ente Exportação criptografada",
"importTypeEnteEncrypted": "Exportação Ente 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",
"importEnteEncGuide": "Selecione o arquivo JSON criptografado exportado do 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 +115,22 @@
"copied": "Copiado",
"pleaseTryAgain": "Por favor, tente novamente",
"existingUser": "Usuário Existente",
"newUser": "Novo no ente",
"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_1": "Quão seguro é o Auth?",
"faq_a_1": "Todos os códigos que você faz backup via Auth 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_q_5": "Como posso ativar o bloqueio facial no 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 +199,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 +350,7 @@
"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.",
"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 +411,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",

View file

@ -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."

View file

@ -61,6 +61,7 @@
"incorrectPasswordTitle": "Felaktigt lösenord",
"welcomeBack": "Välkommen tillbaka!",
"changePassword": "Ändra lösenord",
"importCodes": "Importera koder",
"cancel": "Avbryt",
"yes": "Ja",
"no": "Nej",
@ -73,7 +74,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 +105,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",

View file

@ -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."

View file

@ -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."

View file

@ -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."

View file

@ -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": "通行密钥",

View file

@ -51,6 +51,7 @@ class _HomePageState extends State<HomePage> {
final scaffoldKey = GlobalKey<ScaffoldState>();
final TextEditingController _textController = TextEditingController();
final FocusNode searchInputFocusNode = FocusNode();
bool _showSearchBox = false;
String _searchText = "";
List<Code> _codes = [];
@ -80,6 +81,17 @@ class _HomePageState extends State<HomePage> {
setState(() {});
});
_showSearchBox = PreferenceService.instance.shouldAutoFocusOnSearchBar();
if (_showSearchBox) {
WidgetsBinding.instance.addPostFrameCallback(
(_) {
// https://github.com/flutter/flutter/issues/20706#issuecomment-646328652
FocusScope.of(context).unfocus();
Timer(const Duration(milliseconds: 1), () {
FocusScope.of(context).requestFocus(searchInputFocusNode);
});
},
);
}
}
void _loadCodes() {
@ -192,6 +204,7 @@ class _HomePageState extends State<HomePage> {
title: !_showSearchBox
? const Text('Ente Auth')
: TextField(
focusNode: searchInputFocusNode,
autofocus: _searchText.isEmpty,
controller: _textController,
onChanged: (val) {

View file

@ -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:

View file

@ -11,7 +11,7 @@
"build-main:quick": "tsc && electron-builder --dir --config.compression=store --config.mac.identity=null",
"build-renderer": "cd ../web && yarn install && yarn build:photos && cd ../desktop && shx rm -f out && shx ln -sf ../web/apps/photos/out out",
"build:quick": "yarn build-renderer && yarn build-main:quick",
"dev": "concurrently --names 'main,rndr' \"yarn dev-main\" \"yarn dev-renderer\"",
"dev": "concurrently --kill-others --success first --names 'main,rndr' \"yarn dev-main\" \"yarn dev-renderer\"",
"dev-main": "tsc && electron app/main.js",
"dev-renderer": "cd ../web && yarn install && yarn dev:photos",
"postinstall": "electron-builder install-app-deps",
@ -44,8 +44,8 @@
"electron-builder-notarize": "^1.5",
"eslint": "^8",
"prettier": "^3",
"prettier-plugin-organize-imports": "^3.2",
"prettier-plugin-packagejson": "^2.4",
"prettier-plugin-organize-imports": "^3",
"prettier-plugin-packagejson": "^2",
"shx": "^0.3",
"typescript": "^5"
},

View file

@ -8,7 +8,8 @@
*
* https://www.electronjs.org/docs/latest/tutorial/process-model#the-main-process
*/
import { app, BrowserWindow, Menu } from "electron/main";
import { nativeImage } from "electron";
import { app, BrowserWindow, Menu, protocol, Tray } from "electron/main";
import serveNextAt from "next-electron-server";
import { existsSync } from "node:fs";
import fs from "node:fs/promises";
@ -16,60 +17,47 @@ import os from "node:os";
import path from "node:path";
import {
addAllowOriginHeader,
createWindow,
handleDockIconHideOnAutoLaunch,
handleDownloads,
handleExternalLinks,
setupMacWindowOnDockIconClick,
setupTrayItem,
} from "./main/init";
import { attachFSWatchIPCHandlers, attachIPCHandlers } from "./main/ipc";
import log, { initLogging } from "./main/log";
import { createApplicationMenu } from "./main/menu";
import { createApplicationMenu, createTrayContextMenu } from "./main/menu";
import { setupAutoUpdater } from "./main/services/app-update";
import { initWatcher } from "./main/services/chokidar";
import autoLauncher from "./main/services/auto-launcher";
import { createWatcher } from "./main/services/watch";
import { userPreferences } from "./main/stores/user-preferences";
import { migrateLegacyWatchStoreIfNeeded } from "./main/stores/watch";
import { registerStreamProtocol } from "./main/stream";
import { isDev } from "./main/util";
let appIsQuitting = false;
let updateIsAvailable = false;
export const isAppQuitting = (): boolean => {
return appIsQuitting;
};
export const setIsAppQuitting = (value: boolean): void => {
appIsQuitting = value;
};
export const isUpdateAvailable = (): boolean => {
return updateIsAvailable;
};
export const setIsUpdateAvailable = (value: boolean): void => {
updateIsAvailable = value;
};
/**
* The URL where the renderer HTML is being served from.
*/
export const rendererURL = "next://app";
export const rendererURL = "ente://app";
/**
* next-electron-server allows up to directly use the output of `next build` in
* production mode and `next dev` in development mode, whilst keeping the rest
* of our code the same.
* We want to hide our window instead of closing it when the user presses the
* cross button on the window.
*
* It uses protocol handlers to serve files from the "next://app" protocol
* > This is because there is 1. a perceptible initial window creation time for
* > our app, and 2. because the long running processes like export and watch
* > folders are tied to the lifetime of the window and otherwise won't run in
* > the background.
*
* - In development this is proxied to http://localhost:3000
* - In production it serves files from the `/out` directory
* Intercepting the window close event and using that to instead hide it is
* easy, however that prevents the actual app quit to stop working (since the
* window never gets closed).
*
* For more details, see this comparison:
* https://github.com/HaNdTriX/next-electron-server/issues/5
* So to achieve our original goal (hide window instead of closing) without
* disabling expected app quits, we keep a flag, and we turn it on when we're
* part of the quit sequence. When this flag is on, we bypass the code that
* prevents the window from being closed.
*/
const setupRendererServer = () => {
serveNextAt(rendererURL);
let shouldAllowWindowClose = false;
export const allowWindowClose = (): void => {
shouldAllowWindowClose = true;
};
/**
@ -87,29 +75,195 @@ const logStartupBanner = () => {
log.info("Running on", { platform, osRelease, systemVersion });
};
function enableSharedArrayBufferSupport() {
app.commandLine.appendSwitch("enable-features", "SharedArrayBuffer");
}
/**
* next-electron-server allows up to directly use the output of `next build` in
* production mode and `next dev` in development mode, whilst keeping the rest
* of our code the same.
*
* It uses protocol handlers to serve files from the "ente://" protocol.
*
* - In development this is proxied to http://localhost:3000
* - In production it serves files from the `/out` directory
*
* For more details, see this comparison:
* https://github.com/HaNdTriX/next-electron-server/issues/5
*/
const setupRendererServer = () => serveNextAt(rendererURL);
/**
* Register privileged schemes.
*
* We have two privileged schemes:
*
* 1. "ente", used for serving our web app (@see {@link setupRendererServer}).
*
* 2. "stream", used for streaming IPC (@see {@link registerStreamProtocol}).
*
* Both of these need some privileges, however, the documentation for Electron's
* [registerSchemesAsPrivileged](https://www.electronjs.org/docs/latest/api/protocol)
* says:
*
* > This method ... can be called only once.
*
* The library we use for the "ente" scheme, next-electron-server, already calls
* it once when we invoke {@link setupRendererServer}.
*
* In practice calling it multiple times just causes the values to be
* overwritten, and the last call wins. So we don't need to modify
* next-electron-server to prevent it from calling registerSchemesAsPrivileged.
* Instead, we (a) repeat what next-electron-server had done here, and (b)
* ensure that we're called after {@link setupRendererServer}.
*/
const registerPrivilegedSchemes = () => {
protocol.registerSchemesAsPrivileged([
{
// Taken verbatim from next-electron-server's code (index.js)
scheme: "ente",
privileges: {
standard: true,
secure: true,
allowServiceWorkers: true,
supportFetchAPI: true,
corsEnabled: true,
},
},
{
scheme: "stream",
privileges: {
// TODO(MR): Remove the commented bits if we don't end up
// needing them by the time the IPC refactoring is done.
// Prevent the insecure origin issues when fetching this
// secure: true,
// Allow the web fetch API in the renderer to use this scheme.
supportFetchAPI: true,
// Allow it to be used with video tags.
// stream: true,
},
},
]);
};
/**
* [Note: Increased disk cache for the desktop app]
*
* Set the "disk-cache-size" command line flag to ask the Chromium process to
* use a larger size for the caches that it keeps on disk. This allows us to use
* the same web-native caching mechanism on both the web and the desktop app,
* just ask the embedded Chromium to be a bit more generous in disk usage when
* the web based caching mechanisms on both the web and the desktop app, just
* ask the embedded Chromium to be a bit more generous in disk usage when
* running as the desktop app.
*
* The size we provide is in bytes. We set it to a large value, 5 GB (5 * 1024 *
* 1024 * 1024 = 5368709120)
* The size we provide is in bytes.
* https://www.electronjs.org/docs/latest/api/command-line-switches#--disk-cache-sizesize
*
* Note that increasing the disk cache size does not guarantee that Chromium
* will respect in verbatim, it uses its own heuristics atop this hint.
* https://superuser.com/questions/378991/what-is-chrome-default-cache-size-limit/1577693#1577693
*
* See also: [Note: Caching files].
*/
const increaseDiskCache = () => {
app.commandLine.appendSwitch("disk-cache-size", "5368709120");
const increaseDiskCache = () =>
app.commandLine.appendSwitch(
"disk-cache-size",
`${5 * 1024 * 1024 * 1024}`, // 5 GB
);
/**
* Create an return the {@link BrowserWindow} that will form our app's UI.
*
* This window will show the HTML served from {@link rendererURL}.
*/
const createMainWindow = async () => {
// Create the main window. This'll show our web content.
const window = new BrowserWindow({
webPreferences: {
preload: path.join(app.getAppPath(), "preload.js"),
sandbox: true,
},
// The color to show in the window until the web content gets loaded.
// See: https://www.electronjs.org/docs/latest/api/browser-window#setting-the-backgroundcolor-property
backgroundColor: "black",
// We'll show it conditionally depending on `wasAutoLaunched` later.
show: false,
});
const wasAutoLaunched = await autoLauncher.wasAutoLaunched();
if (wasAutoLaunched) {
// Don't automatically show the app's window if we were auto-launched.
// On macOS, also hide the dock icon on macOS.
if (process.platform == "darwin") app.dock.hide();
} else {
// Show our window (maximizing it) otherwise.
window.maximize();
}
// Open the DevTools automatically when running in dev mode
if (isDev) window.webContents.openDevTools();
window.webContents.on("render-process-gone", (_, details) => {
log.error(`render-process-gone: ${details}`);
window.webContents.reload();
});
window.webContents.on("unresponsive", () => {
log.error(
"Main window's webContents are unresponsive, will restart the renderer process",
);
window.webContents.forcefullyCrashRenderer();
});
window.on("close", (event) => {
if (!shouldAllowWindowClose) {
event.preventDefault();
window.hide();
}
return false;
});
window.on("hide", () => {
// On macOS, when hiding the window also hide the app's icon in the dock
// if the user has selected the Settings > Hide dock icon checkbox.
if (process.platform == "darwin" && userPreferences.get("hideDockIcon"))
app.dock.hide();
});
window.on("show", () => {
if (process.platform == "darwin") app.dock.show();
});
// Let ipcRenderer know when mainWindow is in the foreground so that it can
// in turn inform the renderer process.
window.on("focus", () => window.webContents.send("mainWindowFocus"));
return window;
};
/**
* Add an icon for our app in the system tray.
*
* For example, these are the small icons that appear on the top right of the
* screen in the main menu bar on macOS.
*/
const setupTrayItem = (mainWindow: BrowserWindow) => {
// There are a total of 6 files corresponding to this tray icon.
//
// On macOS, use template images (filename needs to end with "Template.ext")
// https://www.electronjs.org/docs/latest/api/native-image#template-image-macos
//
// And for each (template or otherwise), there are 3 "retina" variants
// https://www.electronjs.org/docs/latest/api/native-image#high-resolution-image
const iconName =
process.platform == "darwin"
? "taskbar-icon-Template.png"
: "taskbar-icon.png";
const trayImgPath = path.join(
isDev ? "build" : process.resourcesPath,
iconName,
);
const trayIcon = nativeImage.createFromPath(trayImgPath);
const tray = new Tray(trayIcon);
tray.setToolTip("Ente Photos");
tray.setContextMenu(createTrayContextMenu(mainWindow));
};
/**
@ -141,12 +295,19 @@ const deleteLegacyDiskCacheDirIfExists = async () => {
}
};
const attachEventHandlers = (mainWindow: BrowserWindow) => {
// Let ipcRenderer know when mainWindow is in the foreground so that it can
// in turn inform the renderer process.
mainWindow.on("focus", () =>
mainWindow.webContents.send("mainWindowFocus"),
);
/**
* Older versions of our app used to keep a keys.json. It is not needed anymore,
* remove it if it exists.
*
* This code was added March 2024, and can be removed after some time once most
* people have upgraded to newer versions.
*/
const deleteLegacyKeysStoreIfExists = async () => {
const keysStore = path.join(app.getPath("userData"), "keys.json");
if (existsSync(keysStore)) {
log.info(`Removing legacy keys store at ${keysStore}`);
await fs.rm(keysStore);
}
};
const main = () => {
@ -156,22 +317,21 @@ const main = () => {
return;
}
let mainWindow: BrowserWindow;
let mainWindow: BrowserWindow | undefined;
initLogging();
setupRendererServer();
logStartupBanner();
handleDockIconHideOnAutoLaunch();
// The order of the next two calls is important
setupRendererServer();
registerPrivilegedSchemes();
increaseDiskCache();
enableSharedArrayBufferSupport();
migrateLegacyWatchStoreIfNeeded();
app.on("second-instance", () => {
// Someone tried to run a second instance, we should focus our window.
if (mainWindow) {
mainWindow.show();
if (mainWindow.isMinimized()) {
mainWindow.restore();
}
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.focus();
}
});
@ -180,21 +340,26 @@ const main = () => {
//
// Note that some Electron APIs can only be used after this event occurs.
app.on("ready", async () => {
mainWindow = await createWindow();
const watcher = initWatcher(mainWindow);
setupTrayItem(mainWindow);
setupMacWindowOnDockIconClick();
Menu.setApplicationMenu(await createApplicationMenu(mainWindow));
// Create window and prepare for renderer
mainWindow = await createMainWindow();
attachIPCHandlers();
attachFSWatchIPCHandlers(watcher);
if (!isDev) setupAutoUpdater(mainWindow);
attachFSWatchIPCHandlers(createWatcher(mainWindow));
registerStreamProtocol();
handleDownloads(mainWindow);
handleExternalLinks(mainWindow);
addAllowOriginHeader(mainWindow);
attachEventHandlers(mainWindow);
// Start loading the renderer
mainWindow.loadURL(rendererURL);
// Continue on with the rest of the startup sequence
Menu.setApplicationMenu(await createApplicationMenu(mainWindow));
setupTrayItem(mainWindow);
if (!isDev) setupAutoUpdater(mainWindow);
try {
deleteLegacyDiskCacheDirIfExists();
deleteLegacyKeysStoreIfExists();
} catch (e) {
// Log but otherwise ignore errors during non-critical startup
// actions.
@ -202,7 +367,11 @@ const main = () => {
}
});
app.on("before-quit", () => setIsAppQuitting(true));
// This is a macOS only event. Show our window when the user activates the
// app, e.g. by clicking on its dock icon.
app.on("activate", () => mainWindow?.show());
app.on("before-quit", allowWindowClose);
};
main();

View file

@ -1,133 +1,29 @@
/**
* @file file system related functions exposed over the context bridge.
*/
import { createWriteStream, existsSync } from "node:fs";
import { 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);
/**
* Write a (web) ReadableStream to a file at the given {@link filePath}.
*
* The returned promise resolves when the write completes.
*
* @param filePath The local filesystem path where the file should be written.
* @param readableStream A [web
* ReadableStream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream)
*/
export const writeStream = (filePath: string, readableStream: ReadableStream) =>
writeNodeStream(filePath, convertWebReadableStreamToNode(readableStream));
export const fsRename = (oldPath: string, newPath: string) =>
fs.rename(oldPath, newPath);
/**
* Convert a Web ReadableStream into a Node.js ReadableStream
*
* This can be used to, for example, write a ReadableStream obtained via
* `net.fetch` into a file using the Node.js `fs` APIs
*/
const convertWebReadableStreamToNode = (readableStream: ReadableStream) => {
const reader = readableStream.getReader();
const rs = new Readable();
rs._read = async () => {
try {
const result = await reader.read();
if (!result.done) {
rs.push(Buffer.from(result.value));
} else {
rs.push(null);
return;
}
} catch (e) {
rs.emit("error", e);
}
};
return rs;
};
const writeNodeStream = async (
filePath: string,
fileStream: NodeJS.ReadableStream,
) => {
const writeable = createWriteStream(filePath);
fileStream.on("error", (error) => {
writeable.destroy(error); // Close the writable stream with an error
});
fileStream.pipe(writeable);
await new Promise((resolve, reject) => {
writeable.on("finish", resolve);
writeable.on("error", async (e: unknown) => {
if (existsSync(filePath)) {
await fs.unlink(filePath);
}
reject(e);
});
});
};
/* TODO: Audit below this */
export const checkExistsAndCreateDir = (dirPath: string) =>
export const fsMkdirIfNeeded = (dirPath: string) =>
fs.mkdir(dirPath, { recursive: true });
export const saveStreamToDisk = writeStream;
export const fsRmdir = (path: string) => fs.rmdir(path);
export const saveFileToDisk = (path: string, contents: string) =>
fs.writeFile(path, contents);
export const fsRm = (path: string) => fs.rm(path);
export const readTextFile = async (filePath: string) =>
export const fsReadTextFile = 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 fsWriteFile = (path: string, contents: string) =>
fs.writeFile(path, contents);
export const isFolder = async (dirPath: string) => {
export const fsIsDir = 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);
const stat = await fs.stat(dirPath);
return stat.isDirectory();
};

View file

@ -1,102 +1,7 @@
import { BrowserWindow, Tray, app, nativeImage, shell } from "electron";
import { BrowserWindow, app, shell } from "electron";
import { existsSync } from "node:fs";
import path from "node:path";
import { isAppQuitting, rendererURL } from "../main";
import log from "./log";
import { createTrayContextMenu } from "./menu";
import { isPlatform } from "./platform";
import autoLauncher from "./services/autoLauncher";
import { getHideDockIconPreference } from "./services/userPreference";
import { isDev } from "./util";
/**
* Create an return the {@link BrowserWindow} that will form our app's UI.
*
* This window will show the HTML served from {@link rendererURL}.
*/
export const createWindow = async () => {
// Create the main window. This'll show our web content.
const mainWindow = new BrowserWindow({
webPreferences: {
preload: path.join(app.getAppPath(), "preload.js"),
},
// The color to show in the window until the web content gets loaded.
// See: https://www.electronjs.org/docs/latest/api/browser-window#setting-the-backgroundcolor-property
backgroundColor: "black",
// We'll show it conditionally depending on `wasAutoLaunched` later.
show: false,
});
const wasAutoLaunched = await autoLauncher.wasAutoLaunched();
if (wasAutoLaunched) {
// Keep the macOS dock icon hidden if we were auto launched.
if (process.platform == "darwin") app.dock.hide();
} else {
// Show our window (maximizing it) if this is not an auto-launch on
// login.
mainWindow.maximize();
}
mainWindow.loadURL(rendererURL);
// Open the DevTools automatically when running in dev mode
if (isDev) mainWindow.webContents.openDevTools();
mainWindow.webContents.on("render-process-gone", (_, details) => {
log.error(`render-process-gone: ${details}`);
mainWindow.webContents.reload();
});
mainWindow.webContents.on("unresponsive", () => {
log.error("webContents unresponsive");
mainWindow.webContents.forcefullyCrashRenderer();
});
mainWindow.on("close", function (event) {
if (!isAppQuitting()) {
event.preventDefault();
mainWindow.hide();
}
return false;
});
mainWindow.on("hide", () => {
// On macOS, also hide the app's icon in the dock if the user has
// selected the Settings > Hide dock icon checkbox.
const shouldHideDockIcon = getHideDockIconPreference();
if (process.platform == "darwin" && shouldHideDockIcon) {
app.dock.hide();
}
});
mainWindow.on("show", () => {
if (process.platform == "darwin") app.dock.show();
});
return mainWindow;
};
export const setupTrayItem = (mainWindow: BrowserWindow) => {
// There are a total of 6 files corresponding to this tray icon.
//
// On macOS, use template images (filename needs to end with "Template.ext")
// https://www.electronjs.org/docs/latest/api/native-image#template-image-macos
//
// And for each (template or otherwise), there are 3 "retina" variants
// https://www.electronjs.org/docs/latest/api/native-image#high-resolution-image
const iconName =
process.platform == "darwin"
? "taskbar-icon-Template.png"
: "taskbar-icon.png";
const trayImgPath = path.join(
isDev ? "build" : process.resourcesPath,
iconName,
);
const trayIcon = nativeImage.createFromPath(trayImgPath);
const tray = new Tray(trayIcon);
tray.setToolTip("Ente Photos");
tray.setContextMenu(createTrayContextMenu(mainWindow));
};
import { rendererURL } from "../main";
export function handleDownloads(mainWindow: BrowserWindow) {
mainWindow.webContents.session.on("will-download", (_, item) => {
@ -137,23 +42,6 @@ export function getUniqueSavePath(filename: string, directory: string): string {
return uniqueFileSavePath;
}
export function setupMacWindowOnDockIconClick() {
app.on("activate", function () {
const windows = BrowserWindow.getAllWindows();
// we allow only one window
windows[0].show();
});
}
export async function handleDockIconHideOnAutoLaunch() {
const shouldHideDockIcon = getHideDockIconPreference();
const wasAutoLaunched = await autoLauncher.wasAutoLaunched();
if (isPlatform("mac") && shouldHideDockIcon && wasAutoLaunched) {
app.dock.hide();
}
}
function lowerCaseHeaders(responseHeaders: Record<string, string[]>) {
const headers: Record<string, string[]> = {};
for (const key of Object.keys(responseHeaders)) {

View file

@ -10,7 +10,12 @@
import type { FSWatcher } from "chokidar";
import { ipcMain } from "electron/main";
import type { ElectronFile, FILE_PATH_TYPE, WatchMapping } from "../types/ipc";
import type {
CollectionMapping,
ElectronFile,
FolderWatch,
PendingUploads,
} from "../types/ipc";
import {
selectDirectory,
showUploadDirsDialog,
@ -18,16 +23,14 @@ import {
showUploadZipDialog,
} from "./dialogs";
import {
checkExistsAndCreateDir,
deleteFile,
deleteFolder,
fsExists,
isFolder,
moveFile,
readTextFile,
rename,
saveFileToDisk,
saveStreamToDisk,
fsIsDir,
fsMkdirIfNeeded,
fsReadTextFile,
fsRename,
fsRm,
fsRmdir,
fsWriteFile,
} from "./fs";
import { logToDisk } from "./log";
import {
@ -51,16 +54,17 @@ import {
} from "./services/store";
import {
getElectronFilesFromGoogleZip,
getPendingUploads,
setToUploadCollection,
setToUploadFiles,
pendingUploads,
setPendingUploadCollection,
setPendingUploadFiles,
} from "./services/upload";
import {
addWatchMapping,
getWatchMappings,
removeWatchMapping,
updateWatchMappingIgnoredFiles,
updateWatchMappingSyncedFiles,
watchAdd,
watchFindFiles,
watchGet,
watchRemove,
watchUpdateIgnoredFiles,
watchUpdateSyncedFiles,
} from "./services/watch";
import { openDirectory, openLogDirectory } from "./util";
@ -114,6 +118,28 @@ export const attachIPCHandlers = () => {
ipcMain.on("skipAppUpdate", (_, version) => skipAppUpdate(version));
// - FS
ipcMain.handle("fsExists", (_, path) => fsExists(path));
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));
ipcMain.handle("fsReadTextFile", (_, path: string) => fsReadTextFile(path));
ipcMain.handle("fsWriteFile", (_, path: string, contents: string) =>
fsWriteFile(path, contents),
);
ipcMain.handle("fsIsDir", (_, dirPath: string) => fsIsDir(dirPath));
// - Conversion
ipcMain.handle("convertToJPEG", (_, fileData, filename) =>
@ -165,60 +191,26 @@ export const attachIPCHandlers = () => {
ipcMain.handle("showUploadZipDialog", () => showUploadZipDialog());
// - FS
ipcMain.handle("fsExists", (_, path) => fsExists(path));
// - FS Legacy
ipcMain.handle("checkExistsAndCreateDir", (_, dirPath) =>
checkExistsAndCreateDir(dirPath),
);
ipcMain.handle(
"saveStreamToDisk",
(_, path: string, fileStream: ReadableStream) =>
saveStreamToDisk(path, fileStream),
);
ipcMain.handle("saveFileToDisk", (_, path: string, contents: string) =>
saveFileToDisk(path, contents),
);
ipcMain.handle("readTextFile", (_, path: string) => readTextFile(path));
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());
ipcMain.handle("pendingUploads", () => pendingUploads());
ipcMain.handle("setPendingUploadCollection", (_, collectionName: string) =>
setPendingUploadCollection(collectionName),
);
ipcMain.handle(
"setToUploadFiles",
(_, type: FILE_PATH_TYPE, filePaths: string[]) =>
setToUploadFiles(type, filePaths),
"setPendingUploadFiles",
(_, type: PendingUploads["type"], filePaths: string[]) =>
setPendingUploadFiles(type, filePaths),
);
// -
ipcMain.handle("getElectronFilesFromGoogleZip", (_, filePath: string) =>
getElectronFilesFromGoogleZip(filePath),
);
ipcMain.handle("setToUploadCollection", (_, collectionName: string) =>
setToUploadCollection(collectionName),
);
ipcMain.handle("getDirFiles", (_, dirPath: string) => getDirFiles(dirPath));
};
@ -227,42 +219,36 @@ export const attachIPCHandlers = () => {
* watch folder functionality.
*
* It gets passed a {@link FSWatcher} instance which it can then forward to the
* actual handlers.
* actual handlers if they need access to it to do their thing.
*/
export const attachFSWatchIPCHandlers = (watcher: FSWatcher) => {
// - Watch
ipcMain.handle(
"addWatchMapping",
(
_,
collectionName: string,
folderPath: string,
uploadStrategy: number,
) =>
addWatchMapping(
watcher,
collectionName,
folderPath,
uploadStrategy,
),
);
ipcMain.handle("removeWatchMapping", (_, folderPath: string) =>
removeWatchMapping(watcher, folderPath),
);
ipcMain.handle("getWatchMappings", () => getWatchMappings());
ipcMain.handle("watchGet", () => watchGet(watcher));
ipcMain.handle(
"updateWatchMappingSyncedFiles",
(_, folderPath: string, files: WatchMapping["syncedFiles"]) =>
updateWatchMappingSyncedFiles(folderPath, files),
"watchAdd",
(_, folderPath: string, collectionMapping: CollectionMapping) =>
watchAdd(watcher, folderPath, collectionMapping),
);
ipcMain.handle("watchRemove", (_, folderPath: string) =>
watchRemove(watcher, folderPath),
);
ipcMain.handle(
"updateWatchMappingIgnoredFiles",
(_, folderPath: string, files: WatchMapping["ignoredFiles"]) =>
updateWatchMappingIgnoredFiles(folderPath, files),
"watchUpdateSyncedFiles",
(_, syncedFiles: FolderWatch["syncedFiles"], folderPath: string) =>
watchUpdateSyncedFiles(syncedFiles, folderPath),
);
ipcMain.handle(
"watchUpdateIgnoredFiles",
(_, ignoredFiles: FolderWatch["ignoredFiles"], folderPath: string) =>
watchUpdateIgnoredFiles(ignoredFiles, folderPath),
);
ipcMain.handle("watchFindFiles", (_, folderPath: string) =>
watchFindFiles(folderPath),
);
};

View file

@ -5,13 +5,10 @@ import {
MenuItemConstructorOptions,
shell,
} from "electron";
import { setIsAppQuitting } from "../main";
import { allowWindowClose } from "../main";
import { forceCheckForAppUpdates } from "./services/app-update";
import autoLauncher from "./services/autoLauncher";
import {
getHideDockIconPreference,
setHideDockIconPreference,
} from "./services/userPreference";
import autoLauncher from "./services/auto-launcher";
import { userPreferences } from "./stores/user-preferences";
import { openLogDirectory } from "./util";
/** Create and return the entries in the app's main menu bar */
@ -21,7 +18,7 @@ export const createApplicationMenu = async (mainWindow: BrowserWindow) => {
// Whenever the menu is redrawn the current value of these variables is used
// to set the checked state for the various settings checkboxes.
let isAutoLaunchEnabled = await autoLauncher.isEnabled();
let shouldHideDockIcon = getHideDockIconPreference();
let shouldHideDockIcon = userPreferences.get("hideDockIcon");
const macOSOnly = (options: MenuItemConstructorOptions[]) =>
process.platform == "darwin" ? options : [];
@ -39,7 +36,9 @@ export const createApplicationMenu = async (mainWindow: BrowserWindow) => {
};
const toggleHideDockIcon = () => {
setHideDockIconPreference(!shouldHideDockIcon);
// Persist
userPreferences.set("hideDockIcon", !shouldHideDockIcon);
// And update the in-memory state
shouldHideDockIcon = !shouldHideDockIcon;
};
@ -196,7 +195,7 @@ export const createTrayContextMenu = (mainWindow: BrowserWindow) => {
};
const handleClose = () => {
setIsAppQuitting(true);
allowWindowClose();
app.quit();
};

View file

@ -1,19 +0,0 @@
export function isPlatform(platform: "mac" | "windows" | "linux") {
return getPlatform() === platform;
}
export function getPlatform(): "mac" | "windows" | "linux" {
switch (process.platform) {
case "aix":
case "freebsd":
case "linux":
case "openbsd":
case "android":
return "linux";
case "darwin":
case "sunos":
return "mac";
case "win32":
return "windows";
}
}

View file

@ -1,11 +1,11 @@
import { compareVersions } from "compare-versions";
import { app, BrowserWindow } from "electron";
import { default as electronLog } from "electron-log";
import { autoUpdater } from "electron-updater";
import { setIsAppQuitting, setIsUpdateAvailable } from "../../main";
import { AppUpdateInfo } from "../../types/ipc";
import { app, BrowserWindow } from "electron/main";
import { allowWindowClose } from "../../main";
import { AppUpdate } from "../../types/ipc";
import log from "../log";
import { userPreferencesStore } from "../stores/user-preferences";
import { userPreferences } from "../stores/user-preferences";
export const setupAutoUpdater = (mainWindow: BrowserWindow) => {
autoUpdater.logger = electronLog;
@ -20,8 +20,8 @@ export const setupAutoUpdater = (mainWindow: BrowserWindow) => {
* Check for app update check ignoring any previously saved skips / mutes.
*/
export const forceCheckForAppUpdates = (mainWindow: BrowserWindow) => {
userPreferencesStore.delete("skipAppVersion");
userPreferencesStore.delete("muteUpdateNotificationVersion");
userPreferences.delete("skipAppVersion");
userPreferences.delete("muteUpdateNotificationVersion");
checkForUpdatesAndNotify(mainWindow);
};
@ -41,21 +41,19 @@ const checkForUpdatesAndNotify = async (mainWindow: BrowserWindow) => {
return;
}
if (version === userPreferencesStore.get("skipAppVersion")) {
if (version === userPreferences.get("skipAppVersion")) {
log.info(`User chose to skip version ${version}`);
return;
}
const mutedVersion = userPreferencesStore.get(
"muteUpdateNotificationVersion",
);
const mutedVersion = userPreferences.get("muteUpdateNotificationVersion");
if (version === mutedVersion) {
log.info(`User has muted update notifications for version ${version}`);
return;
}
const showUpdateDialog = (updateInfo: AppUpdateInfo) =>
mainWindow.webContents.send("appUpdateAvailable", updateInfo);
const showUpdateDialog = (update: AppUpdate) =>
mainWindow.webContents.send("appUpdateAvailable", update);
log.debug(() => "Attempting auto update");
autoUpdater.downloadUpdate();
@ -74,8 +72,6 @@ const checkForUpdatesAndNotify = async (mainWindow: BrowserWindow) => {
log.error("Auto update failed", error);
showUpdateDialog({ autoUpdatable: false, version });
});
setIsUpdateAvailable(true);
};
/**
@ -87,12 +83,12 @@ export const appVersion = () => `v${app.getVersion()}`;
export const updateAndRestart = () => {
log.info("Restarting the app to apply update");
setIsAppQuitting(true);
allowWindowClose();
autoUpdater.quitAndInstall();
};
export const updateOnNextRestart = (version: string) =>
userPreferencesStore.set("muteUpdateNotificationVersion", version);
userPreferences.set("muteUpdateNotificationVersion", version);
export const skipAppUpdate = (version: string) =>
userPreferencesStore.set("skipAppVersion", version);
userPreferences.set("skipAppVersion", version);

View file

@ -0,0 +1,51 @@
import AutoLaunch from "auto-launch";
import { app } from "electron/main";
class AutoLauncher {
/**
* This property will be set and used on Linux and Windows. On macOS,
* there's a separate API
*/
private autoLaunch?: AutoLaunch;
constructor() {
if (process.platform != "darwin") {
this.autoLaunch = new AutoLaunch({
name: "ente",
isHidden: true,
});
}
}
async isEnabled() {
const autoLaunch = this.autoLaunch;
if (autoLaunch) {
return await autoLaunch.isEnabled();
} else {
return app.getLoginItemSettings().openAtLogin;
}
}
async toggleAutoLaunch() {
const isEnabled = await this.isEnabled();
const autoLaunch = this.autoLaunch;
if (autoLaunch) {
if (isEnabled) await autoLaunch.disable();
else await autoLaunch.enable();
} else {
if (isEnabled) app.setLoginItemSettings({ openAtLogin: false });
else app.setLoginItemSettings({ openAtLogin: true });
}
}
async wasAutoLaunched() {
if (this.autoLaunch) {
return app.commandLine.hasSwitch("hidden");
} else {
// TODO(MR): This apparently doesn't work anymore.
return app.getLoginItemSettings().wasOpenedAtLogin;
}
}
}
export default new AutoLauncher();

View file

@ -1,41 +0,0 @@
import { AutoLauncherClient } from "../../types/main";
import { isPlatform } from "../platform";
import linuxAndWinAutoLauncher from "./autoLauncherClients/linuxAndWinAutoLauncher";
import macAutoLauncher from "./autoLauncherClients/macAutoLauncher";
class AutoLauncher {
private client: AutoLauncherClient;
async init() {
if (isPlatform("linux") || isPlatform("windows")) {
this.client = linuxAndWinAutoLauncher;
} else {
this.client = macAutoLauncher;
}
// migrate old auto launch settings for windows from mac auto launcher to linux and windows auto launcher
if (isPlatform("windows") && (await macAutoLauncher.isEnabled())) {
await macAutoLauncher.toggleAutoLaunch();
await linuxAndWinAutoLauncher.toggleAutoLaunch();
}
}
async isEnabled() {
if (!this.client) {
await this.init();
}
return await this.client.isEnabled();
}
async toggleAutoLaunch() {
if (!this.client) {
await this.init();
}
await this.client.toggleAutoLaunch();
}
async wasAutoLaunched() {
if (!this.client) {
await this.init();
}
return this.client.wasAutoLaunched();
}
}
export default new AutoLauncher();

View file

@ -1,39 +0,0 @@
import AutoLaunch from "auto-launch";
import { app } from "electron";
import { AutoLauncherClient } from "../../../types/main";
const LAUNCHED_AS_HIDDEN_FLAG = "hidden";
class LinuxAndWinAutoLauncher implements AutoLauncherClient {
private instance: AutoLaunch;
constructor() {
const autoLauncher = new AutoLaunch({
name: "ente",
isHidden: true,
});
this.instance = autoLauncher;
}
async isEnabled() {
return await this.instance.isEnabled();
}
async toggleAutoLaunch() {
if (await this.isEnabled()) {
await this.disableAutoLaunch();
} else {
await this.enableAutoLaunch();
}
}
async wasAutoLaunched() {
return app.commandLine.hasSwitch(LAUNCHED_AS_HIDDEN_FLAG);
}
private async disableAutoLaunch() {
await this.instance.disable();
}
private async enableAutoLaunch() {
await this.instance.enable();
}
}
export default new LinuxAndWinAutoLauncher();

View file

@ -1,28 +0,0 @@
import { app } from "electron";
import { AutoLauncherClient } from "../../../types/main";
class MacAutoLauncher implements AutoLauncherClient {
async isEnabled() {
return app.getLoginItemSettings().openAtLogin;
}
async toggleAutoLaunch() {
if (await this.isEnabled()) {
this.disableAutoLaunch();
} else {
this.enableAutoLaunch();
}
}
async wasAutoLaunched() {
return app.getLoginItemSettings().wasOpenedAtLogin;
}
private disableAutoLaunch() {
app.setLoginItemSettings({ openAtLogin: false });
}
private enableAutoLaunch() {
app.setLoginItemSettings({ openAtLogin: true });
}
}
export default new MacAutoLauncher();

View file

@ -1,45 +0,0 @@
import chokidar from "chokidar";
import { BrowserWindow } from "electron";
import path from "path";
import log from "../log";
import { getElectronFile } from "./fs";
import { getWatchMappings } from "./watch";
/**
* Convert a file system {@link filePath} that uses the local system specific
* path separators into a path that uses POSIX file separators.
*/
const normalizeToPOSIX = (filePath: string) =>
filePath.split(path.sep).join(path.posix.sep);
export function initWatcher(mainWindow: BrowserWindow) {
const mappings = getWatchMappings();
const folderPaths = mappings.map((mapping) => {
return mapping.folderPath;
});
const watcher = chokidar.watch(folderPaths, {
awaitWriteFinish: true,
});
watcher
.on("add", async (path) => {
mainWindow.webContents.send(
"watch-add",
await getElectronFile(normalizeToPOSIX(path)),
);
})
.on("unlink", (path) => {
mainWindow.webContents.send("watch-unlink", normalizeToPOSIX(path));
})
.on("unlinkDir", (path) => {
mainWindow.webContents.send(
"watch-unlink-dir",
normalizeToPOSIX(path),
);
})
.on("error", (error) => {
log.error("Error while watching files", error);
});
return watcher;
}

View file

@ -2,8 +2,8 @@ import pathToFfmpeg from "ffmpeg-static";
import { existsSync } from "node:fs";
import fs from "node:fs/promises";
import { ElectronFile } from "../../types/ipc";
import { writeStream } from "../fs";
import log from "../log";
import { writeStream } from "../stream";
import { generateTempFilePath, getTempDirPath } from "../temp";
import { execAsync } from "../util";

View file

@ -91,19 +91,6 @@ export async function getElectronFile(filePath: string): Promise<ElectronFile> {
};
}
export const getValidPaths = (paths: string[]) => {
if (!paths) {
return [] as string[];
}
return paths.filter(async (path) => {
try {
await fs.stat(path).then((stat) => stat.isFile());
} catch (e) {
return false;
}
});
};
export const getZipFileStream = async (
zip: StreamZip.StreamZipAsync,
filePath: string,

View file

@ -2,9 +2,8 @@ import { existsSync } from "fs";
import fs from "node:fs/promises";
import path from "path";
import { CustomErrors, ElectronFile } from "../../types/ipc";
import { writeStream } from "../fs";
import log from "../log";
import { isPlatform } from "../platform";
import { writeStream } from "../stream";
import { generateTempFilePath } from "../temp";
import { execAsync, isDev } from "../util";
import { deleteTempFile } from "./ffmpeg";
@ -67,19 +66,15 @@ const IMAGE_MAGICK_THUMBNAIL_GENERATE_COMMAND_TEMPLATE = [
OUTPUT_PATH_PLACEHOLDER,
];
function getImageMagickStaticPath() {
return isDev
? "resources/image-magick"
: path.join(process.resourcesPath, "image-magick");
}
const imageMagickStaticPath = () =>
path.join(isDev ? "build" : process.resourcesPath, "image-magick");
export async function convertToJPEG(
fileData: Uint8Array,
filename: string,
): Promise<Uint8Array> {
if (isPlatform("windows")) {
if (process.platform == "win32")
throw Error(CustomErrors.WINDOWS_NATIVE_IMAGE_PROCESSING_NOT_SUPPORTED);
}
const convertedFileData = await convertToJPEG_(fileData, filename);
return convertedFileData;
}
@ -126,7 +121,7 @@ function constructConvertCommand(
tempOutputFilePath: string,
) {
let convertCmd: string[];
if (isPlatform("mac")) {
if (process.platform == "darwin") {
convertCmd = SIPS_HEIC_CONVERT_COMMAND_TEMPLATE.map((cmdPart) => {
if (cmdPart === INPUT_PATH_PLACEHOLDER) {
return tempInputFilePath;
@ -136,11 +131,11 @@ function constructConvertCommand(
}
return cmdPart;
});
} else if (isPlatform("linux")) {
} else if (process.platform == "linux") {
convertCmd = IMAGEMAGICK_HEIC_CONVERT_COMMAND_TEMPLATE.map(
(cmdPart) => {
if (cmdPart === IMAGE_MAGICK_PLACEHOLDER) {
return getImageMagickStaticPath();
return imageMagickStaticPath();
}
if (cmdPart === INPUT_PATH_PLACEHOLDER) {
return tempInputFilePath;
@ -165,11 +160,10 @@ export async function generateImageThumbnail(
let inputFilePath = null;
let createdTempInputFile = null;
try {
if (isPlatform("windows")) {
if (process.platform == "win32")
throw Error(
CustomErrors.WINDOWS_NATIVE_IMAGE_PROCESSING_NOT_SUPPORTED,
);
}
if (!existsSync(inputFile.path)) {
const tempFilePath = await generateTempFilePath(inputFile.name);
await writeStream(tempFilePath, await inputFile.stream());
@ -240,7 +234,7 @@ function constructThumbnailGenerationCommand(
quality: number,
) {
let thumbnailGenerationCmd: string[];
if (isPlatform("mac")) {
if (process.platform == "darwin") {
thumbnailGenerationCmd = SIPS_THUMBNAIL_GENERATE_COMMAND_TEMPLATE.map(
(cmdPart) => {
if (cmdPart === INPUT_PATH_PLACEHOLDER) {
@ -258,11 +252,11 @@ function constructThumbnailGenerationCommand(
return cmdPart;
},
);
} else if (isPlatform("linux")) {
} else if (process.platform == "linux") {
thumbnailGenerationCmd =
IMAGE_MAGICK_THUMBNAIL_GENERATE_COMMAND_TEMPLATE.map((cmdPart) => {
if (cmdPart === IMAGE_MAGICK_PLACEHOLDER) {
return getImageMagickStaticPath();
return imageMagickStaticPath();
}
if (cmdPart === INPUT_PATH_PLACEHOLDER) {
return inputFilePath;

View file

@ -11,8 +11,8 @@ import fs from "node:fs/promises";
import * as ort from "onnxruntime-node";
import Tokenizer from "../../thirdparty/clip-bpe-ts/mod";
import { CustomErrors } from "../../types/ipc";
import { writeStream } from "../fs";
import log from "../log";
import { writeStream } from "../stream";
import { generateTempFilePath } from "../temp";
import { deleteTempFile } from "./ffmpeg";
import {

View file

@ -15,8 +15,8 @@ import { existsSync } from "fs";
import fs from "node:fs/promises";
import path from "node:path";
import * as ort from "onnxruntime-node";
import { writeStream } from "../fs";
import log from "../log";
import { writeStream } from "../stream";
/**
* Download the model named {@link modelName} if we don't already have it.

View file

@ -1,12 +1,15 @@
import { safeStorage } from "electron/main";
import { keysStore } from "../stores/keys.store";
import { safeStorageStore } from "../stores/safeStorage.store";
import { uploadStatusStore } from "../stores/upload.store";
import { watchStore } from "../stores/watch.store";
import { safeStorageStore } from "../stores/safe-storage";
import { uploadStatusStore } from "../stores/upload-status";
import { watchStore } from "../stores/watch";
/**
* Clear all stores except user preferences.
*
* This is useful to reset state when the user logs out.
*/
export const clearStores = () => {
uploadStatusStore.clear();
keysStore.clear();
safeStorageStore.clear();
watchStore.clear();
};

View file

@ -1,19 +1,23 @@
import StreamZip from "node-stream-zip";
import { existsSync } from "original-fs";
import path from "path";
import { ElectronFile, FILE_PATH_TYPE } from "../../types/ipc";
import { FILE_PATH_KEYS } from "../../types/main";
import { uploadStatusStore } from "../stores/upload.store";
import { getElectronFile, getValidPaths, getZipFileStream } from "./fs";
import { ElectronFile, type PendingUploads } from "../../types/ipc";
import {
uploadStatusStore,
type UploadStatusStore,
} from "../stores/upload-status";
import { getElectronFile, getZipFileStream } from "./fs";
export const getPendingUploads = async () => {
const filePaths = getSavedFilePaths(FILE_PATH_TYPE.FILES);
const zipPaths = getSavedFilePaths(FILE_PATH_TYPE.ZIPS);
export const pendingUploads = async () => {
const collectionName = uploadStatusStore.get("collectionName");
const filePaths = validSavedPaths("files");
const zipPaths = validSavedPaths("zips");
let files: ElectronFile[] = [];
let type: FILE_PATH_TYPE;
let type: PendingUploads["type"];
if (zipPaths.length) {
type = FILE_PATH_TYPE.ZIPS;
type = "zips";
for (const zipPath of zipPaths) {
files = [
...files,
@ -23,9 +27,10 @@ export const getPendingUploads = async () => {
const pendingFilePaths = new Set(filePaths);
files = files.filter((file) => pendingFilePaths.has(file.path));
} else if (filePaths.length) {
type = FILE_PATH_TYPE.FILES;
type = "files";
files = await Promise.all(filePaths.map(getElectronFile));
}
return {
files,
collectionName,
@ -33,16 +38,56 @@ export const getPendingUploads = async () => {
};
};
export const getSavedFilePaths = (type: FILE_PATH_TYPE) => {
const paths =
getValidPaths(
uploadStatusStore.get(FILE_PATH_KEYS[type]) as string[],
) ?? [];
setToUploadFiles(type, paths);
export const validSavedPaths = (type: PendingUploads["type"]) => {
const key = storeKey(type);
const savedPaths = (uploadStatusStore.get(key) as string[]) ?? [];
const paths = savedPaths.filter((p) => existsSync(p));
uploadStatusStore.set(key, paths);
return paths;
};
export const setPendingUploadCollection = (collectionName: string) => {
if (collectionName) uploadStatusStore.set("collectionName", collectionName);
else uploadStatusStore.delete("collectionName");
};
export const setPendingUploadFiles = (
type: PendingUploads["type"],
filePaths: string[],
) => {
const key = storeKey(type);
if (filePaths) uploadStatusStore.set(key, filePaths);
else uploadStatusStore.delete(key);
};
const storeKey = (type: PendingUploads["type"]): keyof UploadStatusStore => {
switch (type) {
case "zips":
return "zipPaths";
case "files":
return "filePaths";
}
};
export const getElectronFilesFromGoogleZip = async (filePath: string) => {
const zip = new StreamZip.async({
file: filePath,
});
const zipName = path.basename(filePath, ".zip");
const entries = await zip.entries();
const files: ElectronFile[] = [];
for (const entry of Object.values(entries)) {
const basename = path.basename(entry.name);
if (entry.isFile && basename.length > 0 && basename[0] !== ".") {
files.push(await getZipEntryAsElectronFile(zipName, zip, entry));
}
}
return files;
};
export async function getZipEntryAsElectronFile(
zipName: string,
zip: StreamZip.StreamZipAsync,
@ -69,39 +114,3 @@ export async function getZipEntryAsElectronFile(
},
};
}
export const setToUploadFiles = (type: FILE_PATH_TYPE, filePaths: string[]) => {
const key = FILE_PATH_KEYS[type];
if (filePaths) {
uploadStatusStore.set(key, filePaths);
} else {
uploadStatusStore.delete(key);
}
};
export const setToUploadCollection = (collectionName: string) => {
if (collectionName) {
uploadStatusStore.set("collectionName", collectionName);
} else {
uploadStatusStore.delete("collectionName");
}
};
export const getElectronFilesFromGoogleZip = async (filePath: string) => {
const zip = new StreamZip.async({
file: filePath,
});
const zipName = path.basename(filePath, ".zip");
const entries = await zip.entries();
const files: ElectronFile[] = [];
for (const entry of Object.values(entries)) {
const basename = path.basename(entry.name);
if (entry.isFile && basename.length > 0 && basename[0] !== ".") {
files.push(await getZipEntryAsElectronFile(zipName, zip, entry));
}
}
return files;
};

View file

@ -1,9 +0,0 @@
import { userPreferencesStore } from "../stores/user-preferences";
export function getHideDockIconPreference() {
return userPreferencesStore.get("hideDockIcon");
}
export function setHideDockIconPreference(shouldHideDockIcon: boolean) {
userPreferencesStore.set("hideDockIcon", shouldHideDockIcon);
}

View file

@ -1,101 +1,159 @@
import type { FSWatcher } from "chokidar";
import ElectronLog from "electron-log";
import { WatchMapping, WatchStoreType } from "../../types/ipc";
import { watchStore } from "../stores/watch.store";
import chokidar, { type FSWatcher } from "chokidar";
import { BrowserWindow } from "electron/main";
import fs from "node:fs/promises";
import path from "node:path";
import { FolderWatch, type CollectionMapping } from "../../types/ipc";
import { fsIsDir } from "../fs";
import log from "../log";
import { watchStore } from "../stores/watch";
export const addWatchMapping = async (
watcher: FSWatcher,
rootFolderName: string,
folderPath: string,
uploadStrategy: number,
) => {
ElectronLog.log(`Adding watch mapping: ${folderPath}`);
const watchMappings = getWatchMappings();
if (isMappingPresent(watchMappings, folderPath)) {
throw new Error(`Watch mapping already exists`);
/**
* Create and return a new file system watcher.
*
* Internally this uses the watcher from the chokidar package.
*
* @param mainWindow The window handle is used to notify the renderer process of
* pertinent file system events.
*/
export const createWatcher = (mainWindow: BrowserWindow) => {
const send = (eventName: string) => (path: string) =>
mainWindow.webContents.send(eventName, ...eventData(path));
const folderPaths = folderWatches().map((watch) => watch.folderPath);
const watcher = chokidar.watch(folderPaths, {
awaitWriteFinish: true,
});
watcher
.on("add", send("watchAddFile"))
.on("unlink", send("watchRemoveFile"))
.on("unlinkDir", send("watchRemoveDir"))
.on("error", (error) => log.error("Error while watching files", error));
return watcher;
};
const eventData = (path: string): [string, FolderWatch] => {
path = posixPath(path);
const watch = folderWatches().find((watch) =>
path.startsWith(watch.folderPath + "/"),
);
if (!watch) throw new Error(`No folder watch was found for path ${path}`);
return [path, watch];
};
/**
* Convert a file system {@link filePath} that uses the local system specific
* path separators into a path that uses POSIX file separators.
*/
const posixPath = (filePath: string) =>
filePath.split(path.sep).join(path.posix.sep);
export const watchGet = (watcher: FSWatcher) => {
const [valid, deleted] = folderWatches().reduce(
([valid, deleted], watch) => {
(fsIsDir(watch.folderPath) ? valid : deleted).push(watch);
return [valid, deleted];
},
[[], []],
);
if (deleted.length) {
for (const watch of deleted) watchRemove(watcher, watch.folderPath);
setFolderWatches(valid);
}
return valid;
};
watcher.add(folderPath);
const folderWatches = (): FolderWatch[] => watchStore.get("mappings") ?? [];
watchMappings.push({
rootFolderName,
uploadStrategy,
const setFolderWatches = (watches: FolderWatch[]) =>
watchStore.set("mappings", watches);
export const watchAdd = async (
watcher: FSWatcher,
folderPath: string,
collectionMapping: CollectionMapping,
) => {
const watches = folderWatches();
if (!fsIsDir(folderPath))
throw new Error(
`Attempting to add a folder watch for a folder path ${folderPath} that is not an existing directory`,
);
if (watches.find((watch) => watch.folderPath == folderPath))
throw new Error(
`A folder watch with the given folder path ${folderPath} already exists`,
);
watches.push({
folderPath,
collectionMapping,
syncedFiles: [],
ignoredFiles: [],
});
setWatchMappings(watchMappings);
setFolderWatches(watches);
watcher.add(folderPath);
return watches;
};
function isMappingPresent(watchMappings: WatchMapping[], folderPath: string) {
const watchMapping = watchMappings?.find(
(mapping) => mapping.folderPath === folderPath,
export const watchRemove = async (watcher: FSWatcher, folderPath: string) => {
const watches = folderWatches();
const filtered = watches.filter((watch) => watch.folderPath != folderPath);
if (watches.length == filtered.length)
throw new Error(
`Attempting to remove a non-existing folder watch for folder path ${folderPath}`,
);
return !!watchMapping;
}
setFolderWatches(filtered);
watcher.unwatch(folderPath);
return filtered;
};
export const removeWatchMapping = async (
watcher: FSWatcher,
export const watchUpdateSyncedFiles = (
syncedFiles: FolderWatch["syncedFiles"],
folderPath: string,
) => {
let watchMappings = getWatchMappings();
const watchMapping = watchMappings.find(
(mapping) => mapping.folderPath === folderPath,
);
if (!watchMapping) {
throw new Error(`Watch mapping does not exist`);
setFolderWatches(
folderWatches().map((watch) => {
if (watch.folderPath == folderPath) {
watch.syncedFiles = syncedFiles;
}
watcher.unwatch(watchMapping.folderPath);
watchMappings = watchMappings.filter(
(mapping) => mapping.folderPath !== watchMapping.folderPath,
return watch;
}),
);
setWatchMappings(watchMappings);
};
export function updateWatchMappingSyncedFiles(
export const watchUpdateIgnoredFiles = (
ignoredFiles: FolderWatch["ignoredFiles"],
folderPath: string,
files: WatchMapping["syncedFiles"],
): void {
const watchMappings = getWatchMappings();
const watchMapping = watchMappings.find(
(mapping) => mapping.folderPath === folderPath,
) => {
setFolderWatches(
folderWatches().map((watch) => {
if (watch.folderPath == folderPath) {
watch.ignoredFiles = ignoredFiles;
}
return watch;
}),
);
};
if (!watchMapping) {
throw Error(`Watch mapping not found`);
export const watchFindFiles = async (dirPath: string) => {
const items = await fs.readdir(dirPath, { withFileTypes: true });
let paths: string[] = [];
for (const item of items) {
const itemPath = path.posix.join(dirPath, item.name);
if (item.isFile()) {
paths.push(itemPath);
} else if (item.isDirectory()) {
paths = [...paths, ...(await watchFindFiles(itemPath))];
}
watchMapping.syncedFiles = files;
setWatchMappings(watchMappings);
}
export function updateWatchMappingIgnoredFiles(
folderPath: string,
files: WatchMapping["ignoredFiles"],
): void {
const watchMappings = getWatchMappings();
const watchMapping = watchMappings.find(
(mapping) => mapping.folderPath === folderPath,
);
if (!watchMapping) {
throw Error(`Watch mapping not found`);
}
watchMapping.ignoredFiles = files;
setWatchMappings(watchMappings);
}
export function getWatchMappings() {
const mappings = watchStore.get("mappings") ?? [];
return mappings;
}
function setWatchMappings(watchMappings: WatchStoreType["mappings"]) {
watchStore.set("mappings", watchMappings);
}
return paths;
};

View file

@ -1,18 +0,0 @@
import Store, { Schema } from "electron-store";
import type { KeysStoreType } from "../../types/main";
const keysStoreSchema: Schema<KeysStoreType> = {
AnonymizeUserID: {
type: "object",
properties: {
id: {
type: "string",
},
},
},
};
export const keysStore = new Store({
name: "keys",
schema: keysStoreSchema,
});

View file

@ -1,7 +1,10 @@
import Store, { Schema } from "electron-store";
import type { SafeStorageStoreType } from "../../types/main";
const safeStorageSchema: Schema<SafeStorageStoreType> = {
interface SafeStorageStore {
encryptionKey: string;
}
const safeStorageSchema: Schema<SafeStorageStore> = {
encryptionKey: {
type: "string",
},

View file

@ -1,7 +1,12 @@
import Store, { Schema } from "electron-store";
import type { UploadStoreType } from "../../types/main";
const uploadStoreSchema: Schema<UploadStoreType> = {
export interface UploadStatusStore {
filePaths: string[];
zipPaths: string[];
collectionName: string;
}
const uploadStatusSchema: Schema<UploadStatusStore> = {
filePaths: {
type: "array",
items: {
@ -21,5 +26,5 @@ const uploadStoreSchema: Schema<UploadStoreType> = {
export const uploadStatusStore = new Store({
name: "upload-status",
schema: uploadStoreSchema,
schema: uploadStatusSchema,
});

View file

@ -1,12 +1,12 @@
import Store, { Schema } from "electron-store";
interface UserPreferencesSchema {
interface UserPreferences {
hideDockIcon: boolean;
skipAppVersion?: string;
muteUpdateNotificationVersion?: string;
}
const userPreferencesSchema: Schema<UserPreferencesSchema> = {
const userPreferencesSchema: Schema<UserPreferences> = {
hideDockIcon: {
type: "boolean",
},
@ -18,7 +18,7 @@ const userPreferencesSchema: Schema<UserPreferencesSchema> = {
},
};
export const userPreferencesStore = new Store({
export const userPreferences = new Store({
name: "userPreferences",
schema: userPreferencesSchema,
});

View file

@ -1,47 +0,0 @@
import Store, { Schema } from "electron-store";
import { WatchStoreType } from "../../types/ipc";
const watchStoreSchema: Schema<WatchStoreType> = {
mappings: {
type: "array",
items: {
type: "object",
properties: {
rootFolderName: {
type: "string",
},
uploadStrategy: {
type: "number",
},
folderPath: {
type: "string",
},
syncedFiles: {
type: "array",
items: {
type: "object",
properties: {
path: {
type: "string",
},
id: {
type: "number",
},
},
},
},
ignoredFiles: {
type: "array",
items: {
type: "string",
},
},
},
},
},
};
export const watchStore = new Store({
name: "watch-status",
schema: watchStoreSchema,
});

View file

@ -0,0 +1,73 @@
import Store, { Schema } from "electron-store";
import { type FolderWatch } from "../../types/ipc";
import log from "../log";
interface WatchStore {
mappings: FolderWatchWithLegacyFields[];
}
type FolderWatchWithLegacyFields = FolderWatch & {
/** @deprecated Only retained for migration, do not use in other code */
rootFolderName?: string;
/** @deprecated Only retained for migration, do not use in other code */
uploadStrategy?: number;
};
const watchStoreSchema: Schema<WatchStore> = {
mappings: {
type: "array",
items: {
type: "object",
properties: {
rootFolderName: { type: "string" },
collectionMapping: { type: "string" },
uploadStrategy: { type: "number" },
folderPath: { type: "string" },
syncedFiles: {
type: "array",
items: {
type: "object",
properties: {
path: { type: "string" },
uploadedFileID: { type: "number" },
collectionID: { type: "number" },
},
},
},
ignoredFiles: {
type: "array",
items: { type: "string" },
},
},
},
},
};
export const watchStore = new Store({
name: "watch-status",
schema: watchStoreSchema,
});
/**
* Previous versions of the store used to store an integer to indicate the
* collection mapping, migrate these to the new schema if we encounter them.
*/
export const migrateLegacyWatchStoreIfNeeded = () => {
let needsUpdate = false;
const watches = watchStore.get("mappings")?.map((watch) => {
let collectionMapping = watch.collectionMapping;
if (!collectionMapping) {
collectionMapping = watch.uploadStrategy == 1 ? "parent" : "root";
needsUpdate = true;
}
if (watch.rootFolderName) {
delete watch.rootFolderName;
needsUpdate = true;
}
return { ...watch, collectionMapping };
});
if (needsUpdate) {
watchStore.set("mappings", watches);
log.info("Migrated legacy watch store data to new schema");
}
};

116
desktop/src/main/stream.ts Normal file
View file

@ -0,0 +1,116 @@
/**
* @file stream data to-from renderer using a custom protocol handler.
*/
import { protocol } from "electron/main";
import { createWriteStream, existsSync } from "node:fs";
import fs from "node:fs/promises";
import { Readable } from "node:stream";
import log from "./log";
/**
* Register a protocol handler that we use for streaming large files between the
* main process (node) and the renderer process (browser) layer.
*
* [Note: IPC streams]
*
* When running without node integration, there is no direct way to pass streams
* across IPC. And passing the entire contents of the file is not feasible for
* large video files because of the memory pressure the copying would entail.
*
* As an alternative, we register a custom protocol handler that can provided a
* bi-directional stream. The renderer can stream data to the node side by
* streaming the request. The node side can stream to the renderer side by
* streaming the response.
*
* See also: [Note: Transferring large amount of data over IPC]
*
* Depends on {@link registerPrivilegedSchemes}.
*/
export const registerStreamProtocol = () => {
protocol.handle("stream", async (request: Request) => {
const url = request.url;
const { host, pathname } = new URL(url);
// Convert e.g. "%20" to spaces.
const path = decodeURIComponent(pathname);
switch (host) {
/* stream://write/path/to/file */
/* host-pathname----- */
case "write":
try {
await writeStream(path, request.body);
return new Response("", { status: 200 });
} catch (e) {
log.error(`Failed to write stream for ${url}`, e);
return new Response(
`Failed to write stream: ${e.message}`,
{ status: 500 },
);
}
default:
return new Response("", { status: 404 });
}
});
};
/**
* Write a (web) ReadableStream to a file at the given {@link filePath}.
*
* The returned promise resolves when the write completes.
*
* @param filePath The local filesystem path where the file should be written.
* @param readableStream A [web
* ReadableStream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream)
*/
export const writeStream = (filePath: string, readableStream: ReadableStream) =>
writeNodeStream(filePath, convertWebReadableStreamToNode(readableStream));
/**
* Convert a Web ReadableStream into a Node.js ReadableStream
*
* This can be used to, for example, write a ReadableStream obtained via
* `net.fetch` into a file using the Node.js `fs` APIs
*/
const convertWebReadableStreamToNode = (readableStream: ReadableStream) => {
const reader = readableStream.getReader();
const rs = new Readable();
rs._read = async () => {
try {
const result = await reader.read();
if (!result.done) {
rs.push(Buffer.from(result.value));
} else {
rs.push(null);
return;
}
} catch (e) {
rs.emit("error", e);
}
};
return rs;
};
const writeNodeStream = async (
filePath: string,
fileStream: NodeJS.ReadableStream,
) => {
const writeable = createWriteStream(filePath);
fileStream.on("error", (error) => {
writeable.destroy(error); // Close the writable stream with an error
});
fileStream.pipe(writeable);
await new Promise((resolve, reject) => {
writeable.on("finish", resolve);
writeable.on("error", async (e: unknown) => {
if (existsSync(filePath)) {
await fs.unlink(filePath);
}
reject(e);
});
});
};

View file

@ -56,6 +56,13 @@ export const openDirectory = async (dirPath: string) => {
if (res) throw new Error(`Failed to open directory ${dirPath}: res`);
};
/**
* Open the app's log directory in the system's folder viewer.
*
* @see {@link openDirectory}
*/
export const openLogDirectory = () => openDirectory(logDirectoryPath());
/**
* Return the path where the logs for the app are saved.
*
@ -72,10 +79,3 @@ export const openDirectory = async (dirPath: string) => {
*
*/
const logDirectoryPath = () => app.getPath("logs");
/**
* Open the app's log directory in the system's folder viewer.
*
* @see {@link openDirectory}
*/
export const openLogDirectory = () => openDirectory(logDirectoryPath());

View file

@ -40,12 +40,13 @@
import { contextBridge, ipcRenderer } from "electron/renderer";
// While we can't import other code, we can import types since they're just
// needed when compiling and will not be needed / looked around for at runtime.
// needed when compiling and will not be needed or looked around for at runtime.
import type {
AppUpdateInfo,
AppUpdate,
CollectionMapping,
ElectronFile,
FILE_PATH_TYPE,
WatchMapping,
FolderWatch,
PendingUploads,
} from "./types/ipc";
// - General
@ -77,12 +78,12 @@ const onMainWindowFocus = (cb?: () => void) => {
// - App update
const onAppUpdateAvailable = (
cb?: ((updateInfo: AppUpdateInfo) => void) | undefined,
cb?: ((update: AppUpdate) => void) | undefined,
) => {
ipcRenderer.removeAllListeners("appUpdateAvailable");
if (cb) {
ipcRenderer.on("appUpdateAvailable", (_, updateInfo: AppUpdateInfo) =>
cb(updateInfo),
ipcRenderer.on("appUpdateAvailable", (_, update: AppUpdate) =>
cb(update),
);
}
};
@ -96,9 +97,31 @@ const skipAppUpdate = (version: string) => {
ipcRenderer.send("skipAppUpdate", version);
};
// - FS
const fsExists = (path: string): Promise<boolean> =>
ipcRenderer.invoke("fsExists", path);
const fsMkdirIfNeeded = (dirPath: string): Promise<void> =>
ipcRenderer.invoke("fsMkdirIfNeeded", dirPath);
const fsRename = (oldPath: string, newPath: string): Promise<void> =>
ipcRenderer.invoke("fsRename", oldPath, newPath);
const fsRmdir = (path: string): Promise<void> =>
ipcRenderer.invoke("fsRmdir", path);
const fsRm = (path: string): Promise<void> => ipcRenderer.invoke("fsRm", path);
const fsReadTextFile = (path: string): Promise<string> =>
ipcRenderer.invoke("fsReadTextFile", path);
const fsWriteFile = (path: string, contents: string): Promise<void> =>
ipcRenderer.invoke("fsWriteFile", path, contents);
const fsIsDir = (dirPath: string): Promise<boolean> =>
ipcRenderer.invoke("fsIsDir", dirPath);
// - AUDIT below this
// - Conversion
@ -169,108 +192,78 @@ const showUploadZipDialog = (): Promise<{
// - Watch
const registerWatcherFunctions = (
addFile: (file: ElectronFile) => Promise<void>,
removeFile: (path: string) => Promise<void>,
removeFolder: (folderPath: string) => Promise<void>,
) => {
ipcRenderer.removeAllListeners("watch-add");
ipcRenderer.removeAllListeners("watch-unlink");
ipcRenderer.removeAllListeners("watch-unlink-dir");
ipcRenderer.on("watch-add", (_, file: ElectronFile) => addFile(file));
ipcRenderer.on("watch-unlink", (_, filePath: string) =>
removeFile(filePath),
);
ipcRenderer.on("watch-unlink-dir", (_, folderPath: string) =>
removeFolder(folderPath),
const watchGet = (): Promise<FolderWatch[]> => ipcRenderer.invoke("watchGet");
const watchAdd = (
folderPath: string,
collectionMapping: CollectionMapping,
): Promise<FolderWatch[]> =>
ipcRenderer.invoke("watchAdd", folderPath, collectionMapping);
const watchRemove = (folderPath: string): Promise<FolderWatch[]> =>
ipcRenderer.invoke("watchRemove", folderPath);
const watchUpdateSyncedFiles = (
syncedFiles: FolderWatch["syncedFiles"],
folderPath: string,
): Promise<void> =>
ipcRenderer.invoke("watchUpdateSyncedFiles", syncedFiles, folderPath);
const watchUpdateIgnoredFiles = (
ignoredFiles: FolderWatch["ignoredFiles"],
folderPath: string,
): Promise<void> =>
ipcRenderer.invoke("watchUpdateIgnoredFiles", ignoredFiles, folderPath);
const watchOnAddFile = (f: (path: string, watch: FolderWatch) => void) => {
ipcRenderer.removeAllListeners("watchAddFile");
ipcRenderer.on("watchAddFile", (_, path: string, watch: FolderWatch) =>
f(path, watch),
);
};
const addWatchMapping = (
collectionName: string,
folderPath: string,
uploadStrategy: number,
): Promise<void> =>
ipcRenderer.invoke(
"addWatchMapping",
collectionName,
folderPath,
uploadStrategy,
const watchOnRemoveFile = (f: (path: string, watch: FolderWatch) => void) => {
ipcRenderer.removeAllListeners("watchRemoveFile");
ipcRenderer.on("watchRemoveFile", (_, path: string, watch: FolderWatch) =>
f(path, watch),
);
};
const removeWatchMapping = (folderPath: string): Promise<void> =>
ipcRenderer.invoke("removeWatchMapping", folderPath);
const watchOnRemoveDir = (f: (path: string, watch: FolderWatch) => void) => {
ipcRenderer.removeAllListeners("watchRemoveDir");
ipcRenderer.on("watchRemoveDir", (_, path: string, watch: FolderWatch) =>
f(path, watch),
);
};
const getWatchMappings = (): Promise<WatchMapping[]> =>
ipcRenderer.invoke("getWatchMappings");
const updateWatchMappingSyncedFiles = (
folderPath: string,
files: WatchMapping["syncedFiles"],
): Promise<void> =>
ipcRenderer.invoke("updateWatchMappingSyncedFiles", folderPath, files);
const updateWatchMappingIgnoredFiles = (
folderPath: string,
files: WatchMapping["ignoredFiles"],
): Promise<void> =>
ipcRenderer.invoke("updateWatchMappingIgnoredFiles", folderPath, files);
// - FS Legacy
const checkExistsAndCreateDir = (dirPath: string): Promise<void> =>
ipcRenderer.invoke("checkExistsAndCreateDir", dirPath);
const saveStreamToDisk = (
path: string,
fileStream: ReadableStream,
): Promise<void> => ipcRenderer.invoke("saveStreamToDisk", path, fileStream);
const saveFileToDisk = (path: string, contents: string): Promise<void> =>
ipcRenderer.invoke("saveFileToDisk", path, contents);
const readTextFile = (path: string): Promise<string> =>
ipcRenderer.invoke("readTextFile", path);
const isFolder = (dirPath: string): Promise<boolean> =>
ipcRenderer.invoke("isFolder", dirPath);
const moveFile = (oldPath: string, newPath: string): Promise<void> =>
ipcRenderer.invoke("moveFile", oldPath, newPath);
const deleteFolder = (path: string): Promise<void> =>
ipcRenderer.invoke("deleteFolder", path);
const deleteFile = (path: string): Promise<void> =>
ipcRenderer.invoke("deleteFile", path);
const rename = (oldPath: string, newPath: string): Promise<void> =>
ipcRenderer.invoke("rename", oldPath, newPath);
const watchFindFiles = (folderPath: string): Promise<string[]> =>
ipcRenderer.invoke("watchFindFiles", folderPath);
// - Upload
const getPendingUploads = (): Promise<{
files: ElectronFile[];
collectionName: string;
type: string;
}> => ipcRenderer.invoke("getPendingUploads");
const pendingUploads = (): Promise<PendingUploads | undefined> =>
ipcRenderer.invoke("pendingUploads");
const setToUploadFiles = (
type: FILE_PATH_TYPE,
const setPendingUploadCollection = (collectionName: string): Promise<void> =>
ipcRenderer.invoke("setPendingUploadCollection", collectionName);
const setPendingUploadFiles = (
type: PendingUploads["type"],
filePaths: string[],
): Promise<void> => ipcRenderer.invoke("setToUploadFiles", type, filePaths);
): Promise<void> =>
ipcRenderer.invoke("setPendingUploadFiles", type, filePaths);
// -
const getElectronFilesFromGoogleZip = (
filePath: string,
): Promise<ElectronFile[]> =>
ipcRenderer.invoke("getElectronFilesFromGoogleZip", filePath);
const setToUploadCollection = (collectionName: string): Promise<void> =>
ipcRenderer.invoke("setToUploadCollection", collectionName);
const getDirFiles = (dirPath: string): Promise<ElectronFile[]> =>
ipcRenderer.invoke("getDirFiles", dirPath);
//
// These objects exposed here will become available to the JS code in our
// renderer (the web/ code) as `window.ElectronAPIs.*`
//
@ -303,8 +296,12 @@ const getDirFiles = (dirPath: string): Promise<ElectronFile[]> =>
//
// The copy itself is relatively fast, but the problem with transfering large
// amounts of data is potentially running out of memory during the copy.
//
// For an alternative, see [Note: IPC streams].
//
contextBridge.exposeInMainWorld("electron", {
// - General
appVersion,
logToDisk,
openDirectory,
@ -315,58 +312,67 @@ contextBridge.exposeInMainWorld("electron", {
onMainWindowFocus,
// - App update
onAppUpdateAvailable,
updateAndRestart,
updateOnNextRestart,
skipAppUpdate,
// - FS
fs: {
exists: fsExists,
rename: fsRename,
mkdirIfNeeded: fsMkdirIfNeeded,
rmdir: fsRmdir,
rm: fsRm,
readTextFile: fsReadTextFile,
writeFile: fsWriteFile,
isDir: fsIsDir,
},
// - Conversion
convertToJPEG,
generateImageThumbnail,
runFFmpegCmd,
// - ML
clipImageEmbedding,
clipTextEmbedding,
detectFaces,
faceEmbedding,
// - File selection
selectDirectory,
showUploadFilesDialog,
showUploadDirsDialog,
showUploadZipDialog,
// - Watch
registerWatcherFunctions,
addWatchMapping,
removeWatchMapping,
getWatchMappings,
updateWatchMappingSyncedFiles,
updateWatchMappingIgnoredFiles,
// - FS
fs: {
exists: fsExists,
watch: {
get: watchGet,
add: watchAdd,
remove: watchRemove,
onAddFile: watchOnAddFile,
onRemoveFile: watchOnRemoveFile,
onRemoveDir: watchOnRemoveDir,
findFiles: watchFindFiles,
updateSyncedFiles: watchUpdateSyncedFiles,
updateIgnoredFiles: watchUpdateIgnoredFiles,
},
// - FS legacy
// TODO: Move these into fs + document + rename if needed
checkExistsAndCreateDir,
saveStreamToDisk,
saveFileToDisk,
readTextFile,
isFolder,
moveFile,
deleteFolder,
deleteFile,
rename,
// - Upload
getPendingUploads,
setToUploadFiles,
pendingUploads,
setPendingUploadCollection,
setPendingUploadFiles,
// -
getElectronFilesFromGoogleZip,
setToUploadCollection,
getDirFiles,
});

View file

@ -5,6 +5,32 @@
* See [Note: types.ts <-> preload.ts <-> ipc.ts]
*/
export interface AppUpdate {
autoUpdatable: boolean;
version: string;
}
export interface FolderWatch {
collectionMapping: CollectionMapping;
folderPath: string;
syncedFiles: FolderWatchSyncedFile[];
ignoredFiles: string[];
}
export type CollectionMapping = "root" | "parent";
export interface FolderWatchSyncedFile {
path: string;
uploadedFileID: number;
collectionID: number;
}
export interface PendingUploads {
collectionName: string;
type: "files" | "zips";
files: ElectronFile[];
}
/**
* Errors that have special semantics on the web side.
*
@ -51,32 +77,3 @@ export interface ElectronFile {
blob: () => Promise<Blob>;
arrayBuffer: () => Promise<Uint8Array>;
}
interface WatchMappingSyncedFile {
path: string;
uploadedFileID: number;
collectionID: number;
}
export interface WatchMapping {
rootFolderName: string;
uploadStrategy: number;
folderPath: string;
syncedFiles: WatchMappingSyncedFile[];
ignoredFiles: string[];
}
export interface WatchStoreType {
mappings: WatchMapping[];
}
export enum FILE_PATH_TYPE {
/* eslint-disable no-unused-vars */
FILES = "files",
ZIPS = "zips",
}
export interface AppUpdateInfo {
autoUpdatable: boolean;
version: string;
}

View file

@ -1,31 +0,0 @@
import { FILE_PATH_TYPE } from "./ipc";
export interface AutoLauncherClient {
isEnabled: () => Promise<boolean>;
toggleAutoLaunch: () => Promise<void>;
wasAutoLaunched: () => Promise<boolean>;
}
export interface UploadStoreType {
filePaths: string[];
zipPaths: string[];
collectionName: string;
}
export interface KeysStoreType {
AnonymizeUserID: {
id: string;
};
}
/* eslint-disable no-unused-vars */
export const FILE_PATH_KEYS: {
[k in FILE_PATH_TYPE]: keyof UploadStoreType;
} = {
[FILE_PATH_TYPE.ZIPS]: "zipPaths",
[FILE_PATH_TYPE.FILES]: "filePaths",
};
export interface SafeStorageStoreType {
encryptionKey: string;
}

View file

@ -125,7 +125,7 @@
dependencies:
eslint-visitor-keys "^3.3.0"
"@eslint-community/regexpp@^4.5.1", "@eslint-community/regexpp@^4.6.1":
"@eslint-community/regexpp@^4.10.0", "@eslint-community/regexpp@^4.6.1":
version "4.10.0"
resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.0.tgz#548f6de556857c8bb73bbee70c35dc82a2e74d63"
integrity sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==
@ -285,7 +285,7 @@
resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz#b979ebad3919799c979b17c72621c0bc0a31c6c4"
integrity sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==
"@types/json-schema@^7.0.12":
"@types/json-schema@^7.0.15":
version "7.0.15"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
@ -303,9 +303,9 @@
integrity sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==
"@types/node@*", "@types/node@^20.9.0":
version "20.11.30"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.30.tgz#9c33467fc23167a347e73834f788f4b9f399d66f"
integrity sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==
version "20.12.7"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.7.tgz#04080362fa3dd6c5822061aa3124f5c152cff384"
integrity sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==
dependencies:
undici-types "~5.26.4"
@ -334,7 +334,7 @@
dependencies:
"@types/node" "*"
"@types/semver@^7.5.0":
"@types/semver@^7.5.8":
version "7.5.8"
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e"
integrity sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==
@ -352,90 +352,90 @@
"@types/node" "*"
"@typescript-eslint/eslint-plugin@^7":
version "7.4.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.4.0.tgz#de61c3083842fc6ac889d2fc83c9a96b55ab8328"
integrity sha512-yHMQ/oFaM7HZdVrVm/M2WHaNPgyuJH4WelkSVEWSSsir34kxW2kDJCxlXRhhGWEsMN0WAW/vLpKfKVcm8k+MPw==
version "7.6.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.6.0.tgz#1f5df5cda490a0bcb6fbdd3382e19f1241024242"
integrity sha512-gKmTNwZnblUdnTIJu3e9kmeRRzV2j1a/LUO27KNNAnIC5zjy1aSvXSRp4rVNlmAoHlQ7HzX42NbKpcSr4jF80A==
dependencies:
"@eslint-community/regexpp" "^4.5.1"
"@typescript-eslint/scope-manager" "7.4.0"
"@typescript-eslint/type-utils" "7.4.0"
"@typescript-eslint/utils" "7.4.0"
"@typescript-eslint/visitor-keys" "7.4.0"
"@eslint-community/regexpp" "^4.10.0"
"@typescript-eslint/scope-manager" "7.6.0"
"@typescript-eslint/type-utils" "7.6.0"
"@typescript-eslint/utils" "7.6.0"
"@typescript-eslint/visitor-keys" "7.6.0"
debug "^4.3.4"
graphemer "^1.4.0"
ignore "^5.2.4"
ignore "^5.3.1"
natural-compare "^1.4.0"
semver "^7.5.4"
ts-api-utils "^1.0.1"
semver "^7.6.0"
ts-api-utils "^1.3.0"
"@typescript-eslint/parser@^7":
version "7.4.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-7.4.0.tgz#540f4321de1e52b886c0fa68628af1459954c1f1"
integrity sha512-ZvKHxHLusweEUVwrGRXXUVzFgnWhigo4JurEj0dGF1tbcGh6buL+ejDdjxOQxv6ytcY1uhun1p2sm8iWStlgLQ==
version "7.6.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-7.6.0.tgz#0aca5de3045d68b36e88903d15addaf13d040a95"
integrity sha512-usPMPHcwX3ZoPWnBnhhorc14NJw9J4HpSXQX4urF2TPKG0au0XhJoZyX62fmvdHONUkmyUe74Hzm1//XA+BoYg==
dependencies:
"@typescript-eslint/scope-manager" "7.4.0"
"@typescript-eslint/types" "7.4.0"
"@typescript-eslint/typescript-estree" "7.4.0"
"@typescript-eslint/visitor-keys" "7.4.0"
"@typescript-eslint/scope-manager" "7.6.0"
"@typescript-eslint/types" "7.6.0"
"@typescript-eslint/typescript-estree" "7.6.0"
"@typescript-eslint/visitor-keys" "7.6.0"
debug "^4.3.4"
"@typescript-eslint/scope-manager@7.4.0":
version "7.4.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-7.4.0.tgz#acfc69261f10ece7bf7ece1734f1713392c3655f"
integrity sha512-68VqENG5HK27ypafqLVs8qO+RkNc7TezCduYrx8YJpXq2QGZ30vmNZGJJJC48+MVn4G2dCV8m5ZTVnzRexTVtw==
"@typescript-eslint/scope-manager@7.6.0":
version "7.6.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-7.6.0.tgz#1e9972f654210bd7500b31feadb61a233f5b5e9d"
integrity sha512-ngttyfExA5PsHSx0rdFgnADMYQi+Zkeiv4/ZxGYUWd0nLs63Ha0ksmp8VMxAIC0wtCFxMos7Lt3PszJssG/E6w==
dependencies:
"@typescript-eslint/types" "7.4.0"
"@typescript-eslint/visitor-keys" "7.4.0"
"@typescript-eslint/types" "7.6.0"
"@typescript-eslint/visitor-keys" "7.6.0"
"@typescript-eslint/type-utils@7.4.0":
version "7.4.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-7.4.0.tgz#cfcaab21bcca441c57da5d3a1153555e39028cbd"
integrity sha512-247ETeHgr9WTRMqHbbQdzwzhuyaJ8dPTuyuUEMANqzMRB1rj/9qFIuIXK7l0FX9i9FXbHeBQl/4uz6mYuCE7Aw==
"@typescript-eslint/type-utils@7.6.0":
version "7.6.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-7.6.0.tgz#644f75075f379827d25fe0713e252ccd4e4a428c"
integrity sha512-NxAfqAPNLG6LTmy7uZgpK8KcuiS2NZD/HlThPXQRGwz6u7MDBWRVliEEl1Gj6U7++kVJTpehkhZzCJLMK66Scw==
dependencies:
"@typescript-eslint/typescript-estree" "7.4.0"
"@typescript-eslint/utils" "7.4.0"
"@typescript-eslint/typescript-estree" "7.6.0"
"@typescript-eslint/utils" "7.6.0"
debug "^4.3.4"
ts-api-utils "^1.0.1"
ts-api-utils "^1.3.0"
"@typescript-eslint/types@7.4.0":
version "7.4.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.4.0.tgz#ee9dafa75c99eaee49de6dcc9348b45d354419b6"
integrity sha512-mjQopsbffzJskos5B4HmbsadSJQWaRK0UxqQ7GuNA9Ga4bEKeiO6b2DnB6cM6bpc8lemaPseh0H9B/wyg+J7rw==
"@typescript-eslint/types@7.6.0":
version "7.6.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.6.0.tgz#53dba7c30c87e5f10a731054266dd905f1fbae38"
integrity sha512-h02rYQn8J+MureCvHVVzhl69/GAfQGPQZmOMjG1KfCl7o3HtMSlPaPUAPu6lLctXI5ySRGIYk94clD/AUMCUgQ==
"@typescript-eslint/typescript-estree@7.4.0":
version "7.4.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.4.0.tgz#12dbcb4624d952f72c10a9f4431284fca24624f4"
integrity sha512-A99j5AYoME/UBQ1ucEbbMEmGkN7SE0BvZFreSnTd1luq7yulcHdyGamZKizU7canpGDWGJ+Q6ZA9SyQobipePg==
"@typescript-eslint/typescript-estree@7.6.0":
version "7.6.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.6.0.tgz#112a3775563799fd3f011890ac8322f80830ac17"
integrity sha512-+7Y/GP9VuYibecrCQWSKgl3GvUM5cILRttpWtnAu8GNL9j11e4tbuGZmZjJ8ejnKYyBRb2ddGQ3rEFCq3QjMJw==
dependencies:
"@typescript-eslint/types" "7.4.0"
"@typescript-eslint/visitor-keys" "7.4.0"
"@typescript-eslint/types" "7.6.0"
"@typescript-eslint/visitor-keys" "7.6.0"
debug "^4.3.4"
globby "^11.1.0"
is-glob "^4.0.3"
minimatch "9.0.3"
semver "^7.5.4"
ts-api-utils "^1.0.1"
minimatch "^9.0.4"
semver "^7.6.0"
ts-api-utils "^1.3.0"
"@typescript-eslint/utils@7.4.0":
version "7.4.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-7.4.0.tgz#d889a0630cab88bddedaf7c845c64a00576257bd"
integrity sha512-NQt9QLM4Tt8qrlBVY9lkMYzfYtNz8/6qwZg8pI3cMGlPnj6mOpRxxAm7BMJN9K0AiY+1BwJ5lVC650YJqYOuNg==
"@typescript-eslint/utils@7.6.0":
version "7.6.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-7.6.0.tgz#e400d782280b6f724c8a1204269d984c79202282"
integrity sha512-x54gaSsRRI+Nwz59TXpCsr6harB98qjXYzsRxGqvA5Ue3kQH+FxS7FYU81g/omn22ML2pZJkisy6Q+ElK8pBCA==
dependencies:
"@eslint-community/eslint-utils" "^4.4.0"
"@types/json-schema" "^7.0.12"
"@types/semver" "^7.5.0"
"@typescript-eslint/scope-manager" "7.4.0"
"@typescript-eslint/types" "7.4.0"
"@typescript-eslint/typescript-estree" "7.4.0"
semver "^7.5.4"
"@types/json-schema" "^7.0.15"
"@types/semver" "^7.5.8"
"@typescript-eslint/scope-manager" "7.6.0"
"@typescript-eslint/types" "7.6.0"
"@typescript-eslint/typescript-estree" "7.6.0"
semver "^7.6.0"
"@typescript-eslint/visitor-keys@7.4.0":
version "7.4.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.4.0.tgz#0c8ff2c1f8a6fe8d7d1a57ebbd4a638e86a60a94"
integrity sha512-0zkC7YM0iX5Y41homUUeW1CHtZR01K3ybjM1l6QczoMuay0XKtrb93kv95AxUGwdjGr64nNqnOCwmEl616N8CA==
"@typescript-eslint/visitor-keys@7.6.0":
version "7.6.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.6.0.tgz#d1ce13145844379021e1f9bd102c1d78946f4e76"
integrity sha512-4eLB7t+LlNUmXzfOu1VAIAdkjbu5xNSerURS9X/S5TUKWFRpXRQZbmtPqgKmYx8bj3J0irtQXSiWAOY82v+cgw==
dependencies:
"@typescript-eslint/types" "7.4.0"
eslint-visitor-keys "^3.4.1"
"@typescript-eslint/types" "7.6.0"
eslint-visitor-keys "^3.4.3"
"@ungap/structured-clone@^1.2.0":
version "1.2.0"
@ -1140,9 +1140,9 @@ ejs@^3.1.8:
jake "^10.8.5"
electron-builder-notarize@^1.5:
version "1.5.1"
resolved "https://registry.yarnpkg.com/electron-builder-notarize/-/electron-builder-notarize-1.5.1.tgz#e00b868a67ef20a77f00017606626f24fdbdc445"
integrity sha512-xS7s9gE+1AcJIuJ4DU/LqCrmRypE1zOR/6b66egKzgP/UVh9YSa7rINos34gF/KcueNDQU39HcXcCEKiEI5wPQ==
version "1.5.2"
resolved "https://registry.yarnpkg.com/electron-builder-notarize/-/electron-builder-notarize-1.5.2.tgz#540185b57a336fc6eec01bfe092a3b4764459255"
integrity sha512-vo6RGgIFYxMk2yp59N4NsvmAYfB7ncYi6gV9Fcq2TVKxEn2tPXrSjIKB2e/pu+5iXIY6BHNZNXa75F3DHgOOLA==
dependencies:
dotenv "^8.2.0"
electron-notarize "^1.1.1"
@ -1215,9 +1215,9 @@ electron-updater@^6.1:
tiny-typed-emitter "^2.1.0"
electron@^29:
version "29.1.5"
resolved "https://registry.yarnpkg.com/electron/-/electron-29.1.5.tgz#b745b4d201c1ac9f84d6aa034126288dde34d5a1"
integrity sha512-1uWGRw/ffA62lcrklxGUgVxVtOHojsg/nwsYr+/F9cVjipZJn8iPv/ABGIIexhmUqWcho8BqfTJ4osCBa29gBg==
version "29.3.0"
resolved "https://registry.yarnpkg.com/electron/-/electron-29.3.0.tgz#8e65cb08e9c0952c66d3196e1b5c811c43b8c5b0"
integrity sha512-ZxFKm0/v48GSoBuO3DdnMlCYXefEUKUHLMsKxyXY4nZGgzbBKpF/X8haZa2paNj23CLfsCKBOtfc2vsEQiOOsA==
dependencies:
"@electron/get" "^2.0.0"
"@types/node" "^20.9.0"
@ -1835,7 +1835,7 @@ ieee754@^1.1.13:
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
ignore@^5.2.0, ignore@^5.2.4:
ignore@^5.2.0, ignore@^5.2.4, ignore@^5.3.1:
version "5.3.1"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef"
integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==
@ -2190,13 +2190,6 @@ mimic-response@^3.1.0:
resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9"
integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==
minimatch@9.0.3, minimatch@^9.0.1:
version "9.0.3"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825"
integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==
dependencies:
brace-expansion "^2.0.1"
minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
@ -2211,6 +2204,20 @@ minimatch@^5.0.1, minimatch@^5.1.1:
dependencies:
brace-expansion "^2.0.1"
minimatch@^9.0.1:
version "9.0.3"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825"
integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==
dependencies:
brace-expansion "^2.0.1"
minimatch@^9.0.4:
version "9.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.4.tgz#8e49c731d1749cbec05050ee5145147b32496a51"
integrity sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==
dependencies:
brace-expansion "^2.0.1"
minimist@^1.2.3, minimist@^1.2.6:
version "1.2.8"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
@ -2482,17 +2489,17 @@ prelude-ls@^1.2.1:
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
prettier-plugin-organize-imports@^3.2:
prettier-plugin-organize-imports@^3:
version "3.2.4"
resolved "https://registry.yarnpkg.com/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-3.2.4.tgz#77967f69d335e9c8e6e5d224074609309c62845e"
integrity sha512-6m8WBhIp0dfwu0SkgfOxJqh+HpdyfqSSLfKKRZSFbDuEQXDDndb8fTpRWkUrX/uBenkex3MgnVk0J3b3Y5byog==
prettier-plugin-packagejson@^2.4:
version "2.4.12"
resolved "https://registry.yarnpkg.com/prettier-plugin-packagejson/-/prettier-plugin-packagejson-2.4.12.tgz#eeb917dad83ae42d0caccc9f26d3728b5c4f2434"
integrity sha512-hifuuOgw5rHHTdouw9VrhT8+Nd7UwxtL1qco8dUfd4XUFQL6ia3xyjSxhPQTsGnSYFraTWy5Omb+MZm/OWDTpQ==
prettier-plugin-packagejson@^2:
version "2.5.0"
resolved "https://registry.yarnpkg.com/prettier-plugin-packagejson/-/prettier-plugin-packagejson-2.5.0.tgz#23d2cb8b1f7840702d35e3a5078e564ea0bc63e0"
integrity sha512-6XkH3rpin5QEQodBSVNg+rBo4r91g/1mCaRwS1YGdQJZ6jwqrg2UchBsIG9tpS1yK1kNBvOt84OILsX8uHzBGg==
dependencies:
sort-package-json "2.8.0"
sort-package-json "2.10.0"
synckit "0.9.0"
prettier@^3:
@ -2711,7 +2718,7 @@ semver@^6.2.0:
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
semver@^7.3.2, semver@^7.3.5, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4:
semver@^7.3.2, semver@^7.3.5, semver@^7.3.8, semver@^7.5.3, semver@^7.6.0:
version "7.6.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d"
integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==
@ -2800,10 +2807,10 @@ sort-object-keys@^1.1.3:
resolved "https://registry.yarnpkg.com/sort-object-keys/-/sort-object-keys-1.1.3.tgz#bff833fe85cab147b34742e45863453c1e190b45"
integrity sha512-855pvK+VkU7PaKYPc+Jjnmt4EzejQHyhhF33q31qG8x7maDzkeFhAAThdCYay11CISO+qAMwjOBP+fPZe0IPyg==
sort-package-json@2.8.0:
version "2.8.0"
resolved "https://registry.yarnpkg.com/sort-package-json/-/sort-package-json-2.8.0.tgz#6a46439ad0fef77f091e678e103f03ecbea575c8"
integrity sha512-PxeNg93bTJWmDGnu0HADDucoxfFiKkIr73Kv85EBThlI1YQPdc0XovBgg2llD0iABZbu2SlKo8ntGmOP9wOj/g==
sort-package-json@2.10.0:
version "2.10.0"
resolved "https://registry.yarnpkg.com/sort-package-json/-/sort-package-json-2.10.0.tgz#6be07424bf3b7db9fbb1bdd69e7945f301026d8a"
integrity sha512-MYecfvObMwJjjJskhxYfuOADkXp1ZMMnCFC8yhp+9HDsk7HhR336hd7eiBs96lTXfiqmUNI+WQCeCMRBhl251g==
dependencies:
detect-indent "^7.0.1"
detect-newline "^4.0.0"
@ -2811,6 +2818,7 @@ sort-package-json@2.8.0:
git-hooks-list "^3.0.0"
globby "^13.1.2"
is-plain-obj "^4.1.0"
semver "^7.6.0"
sort-object-keys "^1.1.3"
source-map-support@^0.5.19:
@ -3018,7 +3026,7 @@ truncate-utf8-bytes@^1.0.0:
dependencies:
utf8-byte-length "^1.0.1"
ts-api-utils@^1.0.1:
ts-api-utils@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz#4b490e27129f1e8e686b45cc4ab63714dc60eea1"
integrity sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==

View file

@ -180,6 +180,10 @@ export const sidebar = [
text: "Connect to custom server",
link: "/self-hosting/guides/custom-server/",
},
{
text: "Hosting the web app",
link: "/self-hosting/guides/web-app",
},
{
text: "Administering your server",
link: "/self-hosting/guides/admin",
@ -207,6 +211,10 @@ export const sidebar = [
text: "Verification code",
link: "/self-hosting/faq/otp",
},
{
text: "Shared albums",
link: "/self-hosting/faq/sharing",
},
],
},
{

View file

@ -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.

View file

@ -110,7 +110,7 @@ or "dog playing at the beach".
Check the sections within the upload progress bar for "Failed Uploads," "Ignored
Uploads," and "Unsuccessful Uploads."
## How do i keep NAS and Ente photos synced?
## How do I keep NAS and Ente photos synced?
Please try using our CLI to pull data into your NAS
https://github.com/ente-io/ente/tree/main/cli#readme.

View file

@ -0,0 +1,59 @@
---
title: Album sharing
description: Getting album sharing to work using an self-hosted Ente
---
# Is public sharing available for self-hosted instances?
Yes.
You'll need to run two instances of the web app, one is regular web app, but
another one is the same code but running on a different origin (i.e. on a
different hostname or different port).
Then, you need to tell the regular web app to use your second instance to
service public links. You can do this by setting the
`NEXT_PUBLIC_ENTE_ALBUMS_ENDPOINT` to point to your second instance when running
or building the regular web app.
For more details, see
[.env](https://github.com/ente-io/ente/blob/main/web/apps/photos/.env) and
[.env.development](https://github.com/ente-io/ente/blob/main/web/apps/photos/.env.development).
As a concrete example, assuming we have a Ente server running on
`localhost:8080`, we can start two instances of the web app, passing them
`NEXT_PUBLIC_ENTE_ALBUMS_ENDPOINT` that points to the origin
("scheme://host[:port]") of the second "albums" instance.
The first one, the normal web app
```sh
NEXT_PUBLIC_ENTE_ENDPOINT=http://localhost:8080 \
NEXT_PUBLIC_ENTE_ALBUMS_ENDPOINT=http://localhost:3002 \
yarn dev:photos
```
The second one, the same code but acting as the "albums" app (the only
difference is the port it is running on):
```sh
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).

View file

@ -0,0 +1,61 @@
---
title: Hosting the web app
description: Building and hosting Ente's web app, connecting it to your self-hosted server
---
# Web app
The getting started instructions mention using `yarn dev` (which is an alias of
`yarn dev:photos`) to serve your web app.
```sh
cd ente/web
git submodule update --init --recursive
yarn install
NEXT_PUBLIC_ENTE_ENDPOINT=http://localhost:8080 yarn dev:photos
```
This is fine for trying this out and verifying that your self-hosted server is
working correctly etc. But if you would like to use the web app for a longer
term, then it is recommended that you use a production build.
To create a production build, you can run the same process, but instead do a
`yarn build` (which is an alias for `yarn build:photos`). For example,
```sh
NEXT_PUBLIC_ENTE_ENDPOINT=http://localhost:8080 yarn build:photos
```
This creates a production build, which is a static site consisting of a folder
of HTML/CSS/JS files that can then be deployed on any standard web server.
Nginx is a common choice for a web server, and you can then put the generated
static site (from the `web/apps/photos/out` folder) to where nginx would serve
them. Note that there is nothing specific to nginx here - you can use any web
server - the basic gist is that yarn build will produce a web/apps/photos/out
folder that you can then serve with any web server of your choice.
If you're new to web development, you might find the [web app's README], and
some of the documentation it its source code -
[docs/new.md](https://github.com/ente-io/ente/blob/main/web/docs/new.md),
[docs/dev.md](https://github.com/ente-io/ente/blob/main/web/docs/dev.md) -
useful. We've also documented the process we use for our own production
deploypments in
[docs/deploy.md](https://github.com/ente-io/ente/blob/main/web/docs/deploy.md),
though be aware that that is probably overkill for simple cases.
## Using Docker
We currently don't offer pre-built Docker images for the web app, however it is
quite easy to build and deploy the web app in a Docker container without
installing anything extra on your machine. For example, you can use the
dockerfile from this
[discussion](https://github.com/ente-io/ente/discussions/1183), or use the
Dockerfile mentioned in the
[notes](https://help.ente.io/self-hosting/guides/external-s3) created by a
community member.
## Public sharing
If you'd also like to enable public sharing on the web app you're running,
please follow the [step here](https://help.ente.io/self-hosting/faq/sharing).

View file

@ -46,7 +46,7 @@ You can alternatively install the build from PlayStore or F-Droid.
## 🧑‍💻 Building from source
1. [Install Flutter v3.16.9](https://flutter.dev/docs/get-started/install).
1. [Install Flutter v3.19.3](https://flutter.dev/docs/get-started/install).
2. Pull in all submodules with `git submodule update --init --recursive`

View file

@ -22,6 +22,7 @@ linter:
- use_key_in_widget_constructors
- cancel_subscriptions
- avoid_empty_else
- exhaustive_cases
@ -59,6 +60,7 @@ analyzer:
prefer_final_locals: warning
unnecessary_const: error
cancel_subscriptions: error
unrelated_type_equality_checks: error
unawaited_futures: warning # convert to warning after fixing existing issues

View file

@ -11,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'
@ -26,11 +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()) {

View file

@ -3,12 +3,9 @@ PODS:
- Flutter
- battery_info (0.0.1):
- Flutter
- bonsoir_darwin (3.0.0):
- Flutter
- FlutterMacOS
- connectivity_plus (0.0.1):
- Flutter
- ReachabilitySwift
- FlutterMacOS
- dart_ui_isolate (0.0.1):
- Flutter
- device_info_plus (0.0.1):
@ -173,7 +170,6 @@ PODS:
- Flutter
- FlutterMacOS
- PromisesObjC (2.4.0)
- ReachabilitySwift (5.2.1)
- receive_sharing_intent (1.6.8):
- Flutter
- screen_brightness_ios (0.1.0):
@ -233,8 +229,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`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`)
- dart_ui_isolate (from `.symlinks/plugins/dart_ui_isolate/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- file_saver (from `.symlinks/plugins/file_saver/ios`)
@ -299,7 +294,6 @@ SPEC REPOS:
- onnxruntime-objc
- OrderedSet
- PromisesObjC
- ReachabilitySwift
- SDWebImage
- SDWebImageWebPCoder
- Sentry
@ -312,10 +306,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"
:path: ".symlinks/plugins/connectivity_plus/darwin"
dart_ui_isolate:
:path: ".symlinks/plugins/dart_ui_isolate/ios"
device_info_plus:
@ -414,8 +406,7 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
background_fetch: 2319bf7e18237b4b269430b7f14d177c0df09c5a
battery_info: 09f5c9ee65394f2291c8c6227bedff345b8a730c
bonsoir_darwin: 127bdc632fdc154ae2f277a4d5c86a6212bc75be
connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a
connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db
dart_ui_isolate: d5bcda83ca4b04f129d70eb90110b7a567aece14
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
@ -464,7 +455,6 @@ SPEC CHECKSUMS:
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
ReachabilitySwift: 5ae15e16814b5f9ef568963fb2c87aeb49158c66
receive_sharing_intent: 6837b01768e567fe8562182397bf43d63d8c6437
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
SDWebImage: 40b0b4053e36c660a764958bff99eed16610acbb
@ -480,11 +470,11 @@ SPEC CHECKSUMS:
Toast: 1f5ea13423a1e6674c4abdac5be53587ae481c4e
uni_links: d97da20c7701486ba192624d99bffaaffcfc298a
url_launcher_ios: 6116280ddcfe98ab8820085d8d76ae7449447586
video_player_avfoundation: 02011213dab73ae3687df27ce441fbbcc82b5579
video_player_avfoundation: 2b4384f3b157206b5e150a0083cdc0c905d260d3
video_thumbnail: c4e2a3c539e247d4de13cd545344fd2d26ffafd1
volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9
wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47
PODFILE CHECKSUM: c1a8f198a245ed1f10e40b617efdb129b021b225
COCOAPODS: 1.14.3
COCOAPODS: 1.15.2

View file

@ -285,7 +285,6 @@
"${BUILT_PRODUCTS_DIR}/Mantle/Mantle.framework",
"${BUILT_PRODUCTS_DIR}/OrderedSet/OrderedSet.framework",
"${BUILT_PRODUCTS_DIR}/PromisesObjC/FBLPromises.framework",
"${BUILT_PRODUCTS_DIR}/ReachabilitySwift/Reachability.framework",
"${BUILT_PRODUCTS_DIR}/SDWebImage/SDWebImage.framework",
"${BUILT_PRODUCTS_DIR}/SDWebImageWebPCoder/SDWebImageWebPCoder.framework",
"${BUILT_PRODUCTS_DIR}/Sentry/Sentry.framework",
@ -293,7 +292,6 @@
"${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}/dart_ui_isolate/dart_ui_isolate.framework",
"${BUILT_PRODUCTS_DIR}/device_info_plus/device_info_plus.framework",
@ -370,7 +368,6 @@
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Mantle.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OrderedSet.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FBLPromises.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Reachability.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDWebImage.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDWebImageWebPCoder.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Sentry.framework",
@ -378,7 +375,6 @@
"${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}/dart_ui_isolate.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/device_info_plus.framework",

View file

@ -13,18 +13,13 @@ import 'package:media_extension/media_extension_action_types.dart';
import 'package:photos/ente_theme_data.dart';
import "package:photos/generated/l10n.dart";
import "package:photos/l10n/l10n.dart";
import "package:photos/models/collection/collection_items.dart";
import 'package:photos/services/app_lifecycle_service.dart';
import "package:photos/services/collections_service.dart";
import "package:photos/services/favorites_service.dart";
import "package:photos/services/home_widget_service.dart";
import "package:photos/services/machine_learning/machine_learning_controller.dart";
import 'package:photos/services/sync_service.dart';
import 'package:photos/ui/tabs/home_widget.dart';
import "package:photos/ui/viewer/actions/file_viewer.dart";
import "package:photos/ui/viewer/gallery/collection_page.dart";
import "package:photos/utils/intent_util.dart";
import "package:photos/utils/navigation_util.dart";
class EnteApp extends StatefulWidget {
final Future<void> Function(String) runBackgroundTask;
@ -66,39 +61,14 @@ class _EnteAppState extends State<EnteApp> with WidgetsBindingObserver {
void didChangeDependencies() {
super.didChangeDependencies();
_checkForWidgetLaunch();
hw.HomeWidget.widgetClicked.listen(_launchedFromWidget);
}
void _checkForWidgetLaunch() {
hw.HomeWidget.initiallyLaunchedFromHomeWidget().then(_launchedFromWidget);
}
Future<void> _launchedFromWidget(Uri? uri) async {
if (uri == null) return;
final collectionID =
await FavoritesService.instance.getFavoriteCollectionID();
if (collectionID == null) {
return;
}
final collection = CollectionsService.instance.getCollectionByID(
collectionID,
hw.HomeWidget.initiallyLaunchedFromHomeWidget().then(
(uri) => HomeWidgetService.instance.onLaunchFromWidget(uri, context),
);
if (collection == null) {
return;
}
unawaited(HomeWidgetService.instance.initHomeWidget());
final thumbnail = await CollectionsService.instance.getCover(collection);
unawaited(
routeToPage(
context,
CollectionPage(
CollectionWithThumbnail(
collection,
thumbnail,
),
),
),
hw.HomeWidget.widgetClicked.listen(
(uri) => HomeWidgetService.instance.onLaunchFromWidget(uri, context),
);
}

View file

@ -16,6 +16,7 @@ const int jan011981Time = 347155200000000;
const int galleryLoadStartTime = -8000000000000000; // Wednesday, March 6, 1748
const int galleryLoadEndTime = 9223372036854775807; // 2^63 -1
const int batchSize = 1000;
const int batchSizeCopy = 100;
const photoGridSizeDefault = 4;
const photoGridSizeMin = 2;
const photoGridSizeMax = 6;
@ -45,6 +46,9 @@ class FFDefault {
static const bool enablePasskey = false;
}
// this is the chunk size of the un-encrypted file which is read and encrypted before uploading it as a single part.
const multipartPartSize = 20 * 1024 * 1024;
const kDefaultProductionEndpoint = 'https://api.ente.io';
const int intMaxValue = 9223372036854775807;
@ -71,10 +75,10 @@ const kSearchSectionLimit = 9;
const iOSGroupID = "group.io.ente.frame.SlideshowWidget";
const blackThumbnailBase64 = '/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEB' +
'AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQ' +
'EBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARC' +
'ACWASwDAREAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUF' +
const blackThumbnailBase64 = '/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEB'
'AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQ'
'EBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARC'
'ACWASwDAREAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUF'
'BAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk' +
'6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztL' +
'W2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAA' +

View file

@ -16,7 +16,6 @@ import "package:photos/services/filter/db_filters.dart";
import 'package:photos/utils/file_uploader_util.dart';
import 'package:sqflite/sqflite.dart';
import 'package:sqflite_migration/sqflite_migration.dart';
import 'package:sqlite3/sqlite3.dart' as sqlite3;
import 'package:sqlite_async/sqlite_async.dart' as sqlite_async;
class FilesDB {
@ -103,20 +102,15 @@ class FilesDB {
// only have a single app-wide reference to the database
static Future<Database>? _dbFuture;
static Future<sqlite3.Database>? _ffiDBFuture;
static Future<sqlite_async.SqliteDatabase>? _sqliteAsyncDBFuture;
@Deprecated("Use sqliteAsyncDB instead (sqlite_async)")
Future<Database> get database async {
// lazily instantiate the db the first time it is accessed
_dbFuture ??= _initDatabase();
return _dbFuture!;
}
Future<sqlite3.Database> get ffiDB async {
_ffiDBFuture ??= _initFFIDatabase();
return _ffiDBFuture!;
}
Future<sqlite_async.SqliteDatabase> get sqliteAsyncDB async {
_sqliteAsyncDBFuture ??= _initSqliteAsyncDatabase();
return _sqliteAsyncDBFuture!;
@ -131,14 +125,6 @@ class FilesDB {
return await openDatabaseWithMigration(path, dbConfig);
}
Future<sqlite3.Database> _initFFIDatabase() async {
final Directory documentsDirectory =
await getApplicationDocumentsDirectory();
final String path = join(documentsDirectory.path, _databaseName);
_logger.info("DB path " + path);
return sqlite3.sqlite3.open(path);
}
Future<sqlite_async.SqliteDatabase> _initSqliteAsyncDatabase() async {
final Directory documentsDirectory =
await getApplicationDocumentsDirectory();
@ -478,11 +464,10 @@ class FilesDB {
}
Future<EnteFile?> getFile(int generatedID) async {
final db = await instance.database;
final results = await db.query(
filesTable,
where: '$columnGeneratedID = ?',
whereArgs: [generatedID],
final db = await instance.sqliteAsyncDB;
final results = await db.getAll(
'SELECT * FROM $filesTable WHERE $columnGeneratedID = ?',
[generatedID],
);
if (results.isEmpty) {
return null;
@ -491,11 +476,10 @@ class FilesDB {
}
Future<EnteFile?> getUploadedFile(int uploadedID, int collectionID) async {
final db = await instance.database;
final results = await db.query(
filesTable,
where: '$columnUploadedFileID = ? AND $columnCollectionID = ?',
whereArgs: [
final db = await instance.sqliteAsyncDB;
final results = await db.getAll(
'SELECT * FROM $filesTable WHERE $columnUploadedFileID = ? AND $columnCollectionID = ?',
[
uploadedID,
collectionID,
],
@ -507,13 +491,10 @@ class FilesDB {
}
Future<EnteFile?> getAnyUploadedFile(int uploadedID) async {
final db = await instance.database;
final results = await db.query(
filesTable,
where: '$columnUploadedFileID = ?',
whereArgs: [
uploadedID,
],
final db = await instance.sqliteAsyncDB;
final results = await db.getAll(
'SELECT * FROM $filesTable WHERE $columnUploadedFileID = ?',
[uploadedID],
);
if (results.isEmpty) {
return null;
@ -522,13 +503,11 @@ class FilesDB {
}
Future<Set<int>> getUploadedFileIDs(int collectionID) async {
final db = await instance.database;
final results = await db.query(
filesTable,
columns: [columnUploadedFileID],
where:
'$columnCollectionID = ? AND ($columnUploadedFileID IS NOT NULL AND $columnUploadedFileID IS NOT -1)',
whereArgs: [
final db = await instance.sqliteAsyncDB;
final results = await db.getAll(
'SELECT $columnUploadedFileID FROM $filesTable'
' WHERE $columnCollectionID = ? AND ($columnUploadedFileID IS NOT NULL AND $columnUploadedFileID IS NOT -1)',
[
collectionID,
],
);
@ -539,13 +518,36 @@ class FilesDB {
return ids;
}
Future<BackedUpFileIDs> getBackedUpIDs() async {
Future<(Set<int>, Map<String, int>)> getUploadAndHash(
int collectionID,
) async {
final db = await instance.database;
final results = await db.query(
filesTable,
columns: [columnLocalID, columnUploadedFileID, columnFileSize],
columns: [columnUploadedFileID, columnHash],
where:
'$columnLocalID IS NOT NULL AND ($columnUploadedFileID IS NOT NULL AND $columnUploadedFileID IS NOT -1)',
'$columnCollectionID = ? AND ($columnUploadedFileID IS NOT NULL AND $columnUploadedFileID IS NOT -1)',
whereArgs: [
collectionID,
],
);
final ids = <int>{};
final hash = <String, int>{};
for (final result in results) {
ids.add(result[columnUploadedFileID] as int);
if (result[columnHash] != null) {
hash[result[columnHash] as String] =
result[columnUploadedFileID] as int;
}
}
return (ids, hash);
}
Future<BackedUpFileIDs> getBackedUpIDs() async {
final db = await instance.sqliteAsyncDB;
final results = await db.getAll(
'SELECT $columnLocalID, $columnUploadedFileID, $columnFileSize FROM $filesTable'
' WHERE $columnLocalID IS NOT NULL AND ($columnUploadedFileID IS NOT NULL AND $columnUploadedFileID IS NOT -1)',
);
final Set<String> localIDs = <String>{};
final Set<int> uploadedIDs = <int>{};
@ -681,13 +683,12 @@ class FilesDB {
}
Future<List<EnteFile>> getAllFilesCollection(int collectionID) async {
final db = await instance.database;
final db = await instance.sqliteAsyncDB;
const String whereClause = '$columnCollectionID = ?';
final List<Object> whereArgs = [collectionID];
final results = await db.query(
filesTable,
where: whereClause,
whereArgs: whereArgs,
final results = await db.getAll(
'SELECT * FROM $filesTable WHERE $whereClause',
whereArgs,
);
final files = convertToFiles(results);
return files;
@ -697,14 +698,13 @@ class FilesDB {
int collectionID,
int addedTime,
) async {
final db = await instance.database;
final db = await instance.sqliteAsyncDB;
const String whereClause =
'$columnCollectionID = ? AND $columnAddedTime > ?';
final List<Object> whereArgs = [collectionID, addedTime];
final results = await db.query(
filesTable,
where: whereClause,
whereArgs: whereArgs,
final results = await db.getAll(
'SELECT * FROM $filesTable WHERE $whereClause',
whereArgs,
);
final files = convertToFiles(results);
return files;
@ -726,20 +726,22 @@ class FilesDB {
inParam += "'" + id.toString() + "',";
}
inParam = inParam.substring(0, inParam.length - 1);
final db = await instance.database;
final db = await instance.sqliteAsyncDB;
final order = (asc ?? false ? 'ASC' : 'DESC');
final String whereClause =
'$columnCollectionID IN ($inParam) AND $columnCreationTime >= ? AND '
'$columnCreationTime <= ? AND $columnOwnerID = ?';
final List<Object> whereArgs = [startTime, endTime, userID];
final results = await db.query(
filesTable,
where: whereClause,
whereArgs: whereArgs,
orderBy:
'$columnCreationTime ' + order + ', $columnModificationTime ' + order,
limit: limit,
String query = 'SELECT * FROM $filesTable WHERE $whereClause ORDER BY '
'$columnCreationTime $order, $columnModificationTime $order';
if (limit != null) {
query += ' LIMIT ?';
whereArgs.add(limit);
}
final results = await db.getAll(
query,
whereArgs,
);
final files = convertToFiles(results);
final dedupeResult =
@ -757,7 +759,7 @@ class FilesDB {
if (durations.isEmpty) {
return <EnteFile>[];
}
final db = await instance.database;
final db = await instance.sqliteAsyncDB;
String whereClause = "( ";
for (int index = 0; index < durations.length; index++) {
whereClause += "($columnCreationTime >= " +
@ -772,44 +774,10 @@ class FilesDB {
}
}
whereClause += ")";
final results = await db.query(
filesTable,
where: whereClause,
orderBy: '$columnCreationTime ' + order,
);
final files = convertToFiles(results);
return applyDBFilters(
files,
DBFilterOptions(ignoredCollectionIDs: ignoredCollectionIDs),
);
}
Future<List<EnteFile>> getFilesCreatedWithinDurationsSync(
List<List<int>> durations,
Set<int> ignoredCollectionIDs, {
int? visibility,
String order = 'ASC',
}) async {
if (durations.isEmpty) {
return <EnteFile>[];
}
final db = await instance.ffiDB;
String whereClause = "( ";
for (int index = 0; index < durations.length; index++) {
whereClause += "($columnCreationTime >= " +
durations[index][0].toString() +
" AND $columnCreationTime < " +
durations[index][1].toString() +
")";
if (index != durations.length - 1) {
whereClause += " OR ";
} else if (visibility != null) {
whereClause += ' AND $columnMMdVisibility = $visibility';
}
}
whereClause += ")";
final results = db.select(
'select * from $filesTable where $whereClause order by $columnCreationTime $order',
final query =
'SELECT * FROM $filesTable WHERE $whereClause ORDER BY $columnCreationTime $order';
final results = await db.getAll(
query,
);
final files = convertToFiles(results);
return applyDBFilters(
@ -1041,6 +1009,29 @@ class FilesDB {
return convertToFiles(rows);
}
Future<Map<String, EnteFile>>
getUserOwnedFilesWithSameHashForGivenListOfFiles(
List<EnteFile> files,
int userID,
) async {
final db = await sqliteAsyncDB;
final List<String> hashes = [];
for (final file in files) {
if (file.hash != null && file.hash != '') {
hashes.add(file.hash!);
}
}
if (hashes.isEmpty) {
return {};
}
final inParam = hashes.map((e) => "'$e'").join(',');
final rows = await db.execute('''
SELECT * FROM $filesTable WHERE $columnHash IN ($inParam) AND $columnOwnerID = $userID;
''');
final matchedFiles = convertToFiles(rows);
return Map.fromIterable(matchedFiles, key: (e) => e.hash);
}
Future<List<EnteFile>> getUploadedFilesWithHashes(
FileHashData hashData,
FileType fileType,

View file

@ -55,6 +55,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Modify your query, or try searching for"),
"moveToHiddenAlbum":
MessageLookupByLibrary.simpleMessage("Move to hidden album"),
"search": MessageLookupByLibrary.simpleMessage("Search"),
"selectALocation":
MessageLookupByLibrary.simpleMessage("Select a location"),
"selectALocationFirst":

View file

@ -1216,6 +1216,7 @@ class MessageLookup extends MessageLookupByLibrary {
"scanThisBarcodeWithnyourAuthenticatorApp":
MessageLookupByLibrary.simpleMessage(
"Scanne diesen Code mit \ndeiner Authentifizierungs-App"),
"search": MessageLookupByLibrary.simpleMessage("Search"),
"searchAlbumsEmptySection":
MessageLookupByLibrary.simpleMessage("Alben"),
"searchByAlbumNameHint":

View file

@ -1179,6 +1179,7 @@ class MessageLookup extends MessageLookupByLibrary {
"scanThisBarcodeWithnyourAuthenticatorApp":
MessageLookupByLibrary.simpleMessage(
"Scan this barcode with\nyour authenticator app"),
"search": MessageLookupByLibrary.simpleMessage("Search"),
"searchAlbumsEmptySection":
MessageLookupByLibrary.simpleMessage("Albums"),
"searchByAlbumNameHint":

View file

@ -1044,6 +1044,7 @@ class MessageLookup extends MessageLookupByLibrary {
"scanThisBarcodeWithnyourAuthenticatorApp":
MessageLookupByLibrary.simpleMessage(
"Escanea este código QR con tu aplicación de autenticación"),
"search": MessageLookupByLibrary.simpleMessage("Search"),
"searchByAlbumNameHint":
MessageLookupByLibrary.simpleMessage("Nombre del álbum"),
"searchByExamples": MessageLookupByLibrary.simpleMessage(

View file

@ -1182,6 +1182,7 @@ class MessageLookup extends MessageLookupByLibrary {
"scanThisBarcodeWithnyourAuthenticatorApp":
MessageLookupByLibrary.simpleMessage(
"Scannez ce code-barres avec\nvotre application d\'authentification"),
"search": MessageLookupByLibrary.simpleMessage("Search"),
"searchAlbumsEmptySection":
MessageLookupByLibrary.simpleMessage("Albums"),
"searchByAlbumNameHint":

View file

@ -1137,6 +1137,7 @@ class MessageLookup extends MessageLookupByLibrary {
"scanThisBarcodeWithnyourAuthenticatorApp":
MessageLookupByLibrary.simpleMessage(
"Scansione questo codice QR\ncon la tua app di autenticazione"),
"search": MessageLookupByLibrary.simpleMessage("Search"),
"searchByAlbumNameHint":
MessageLookupByLibrary.simpleMessage("Nome album"),
"searchByExamples": MessageLookupByLibrary.simpleMessage(

View file

@ -55,6 +55,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Modify your query, or try searching for"),
"moveToHiddenAlbum":
MessageLookupByLibrary.simpleMessage("Move to hidden album"),
"search": MessageLookupByLibrary.simpleMessage("Search"),
"selectALocation":
MessageLookupByLibrary.simpleMessage("Select a location"),
"selectALocationFirst":

View file

@ -21,7 +21,7 @@ class MessageLookup extends MessageLookupByLibrary {
String get localeName => 'nl';
static String m0(count) =>
"${Intl.plural(count, zero: 'Add collaborator', one: 'Add collaborator', other: 'Add collaborators')}";
"${Intl.plural(count, zero: 'Voeg samenwerker toe', one: 'Voeg samenwerker toe', other: 'Voeg samenwerkers toe')}";
static String m2(count) =>
"${Intl.plural(count, one: 'Bestand toevoegen', other: 'Bestanden toevoegen')}";
@ -30,7 +30,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Jouw ${storageAmount} add-on is geldig tot ${endDate}";
static String m1(count) =>
"${Intl.plural(count, zero: 'Add viewer', one: 'Add viewer', other: 'Add viewers')}";
"${Intl.plural(count, one: 'Voeg kijker toe', other: 'Voeg kijkers toe')}";
static String m4(emailOrName) => "Toegevoegd door ${emailOrName}";
@ -64,6 +64,8 @@ class MessageLookup extends MessageLookupByLibrary {
static String m13(provider) =>
"Neem contact met ons op via support@ente.io om uw ${provider} abonnement te beheren.";
static String m69(endpoint) => "Verbonden met ${endpoint}";
static String m14(count) =>
"${Intl.plural(count, one: 'Verwijder ${count} bestand', other: 'Verwijder ${count} bestanden')}";
@ -85,7 +87,7 @@ class MessageLookup extends MessageLookupByLibrary {
static String m20(newEmail) => "E-mailadres gewijzigd naar ${newEmail}";
static String m21(email) =>
"${email} heeft geen ente account.\n\nStuur ze een uitnodiging om foto\'s te delen.";
"${email} heeft geen Ente account.\n\nStuur ze een uitnodiging om foto\'s te delen.";
static String m22(count, formattedNumber) =>
"${Intl.plural(count, one: '1 bestand', other: '${formattedNumber} bestanden')} in dit album zijn veilig geback-upt";
@ -102,7 +104,7 @@ class MessageLookup extends MessageLookupByLibrary {
static String m26(endDate) => "Gratis proefversie geldig tot ${endDate}";
static String m27(count) =>
"U heeft nog steeds toegang tot ${Intl.plural(count, one: 'het', other: 'ze')} op ente zolang u een actief abonnement heeft";
"Je hebt nog steeds toegang tot ${Intl.plural(count, one: 'het', other: 'ze')} op Ente zolang je een actief abonnement hebt";
static String m28(sizeInMBorGB) => "Maak ${sizeInMBorGB} vrij";
@ -164,7 +166,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Hey, kunt u bevestigen dat dit uw ente.io verificatie-ID is: ${verificationID}";
static String m50(referralCode, referralStorageInGB) =>
"ente verwijzingscode: ${referralCode} \n\nPas het toe bij Instellingen → Algemeen → Verwijzingen om ${referralStorageInGB} GB gratis te krijgen nadat je je hebt aangemeld voor een betaald abonnement\n\nhttps://ente.io";
"Ente verwijzingscode: ${referralCode} \n\nPas het toe bij Instellingen → Algemeen → Verwijzingen om ${referralStorageInGB} GB gratis te krijgen nadat je je hebt aangemeld voor een betaald abonnement\n\nhttps://ente.io";
static String m51(numberOfPeople) =>
"${Intl.plural(numberOfPeople, zero: 'Deel met specifieke mensen', one: 'Gedeeld met 1 persoon', other: 'Gedeeld met ${numberOfPeople} mensen')}";
@ -175,10 +177,10 @@ class MessageLookup extends MessageLookupByLibrary {
"Deze ${fileType} zal worden verwijderd van jouw apparaat.";
static String m54(fileType) =>
"Deze ${fileType} staat zowel in ente als op jouw apparaat.";
"Deze ${fileType} staat zowel in Ente als op jouw apparaat.";
static String m55(fileType) =>
"Deze ${fileType} zal worden verwijderd uit ente.";
"Deze ${fileType} zal worden verwijderd uit Ente.";
static String m56(storageAmountInGB) => "${storageAmountInGB} GB";
@ -187,7 +189,7 @@ class MessageLookup extends MessageLookupByLibrary {
"${usedAmount} ${usedStorageUnit} van ${totalAmount} ${totalStorageUnit} gebruikt";
static String m58(id) =>
"Uw ${id} is al aan een ander ente account gekoppeld.\nAls u uw ${id} wilt gebruiken met dit account, neem dan contact op met onze klantenservice";
"Jouw ${id} is al aan een ander Ente account gekoppeld.\nAls je jouw ${id} wilt gebruiken met dit account, neem dan contact op met onze klantenservice";
static String m59(endDate) => "Uw abonnement loopt af op ${endDate}";
@ -218,7 +220,7 @@ class MessageLookup extends MessageLookupByLibrary {
final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"aNewVersionOfEnteIsAvailable": MessageLookupByLibrary.simpleMessage(
"Er is een nieuwe versie van ente beschikbaar."),
"Er is een nieuwe versie van Ente beschikbaar."),
"about": MessageLookupByLibrary.simpleMessage("Over"),
"account": MessageLookupByLibrary.simpleMessage("Account"),
"accountWelcomeBack":
@ -249,7 +251,7 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Voeg geselecteerde toe"),
"addToAlbum":
MessageLookupByLibrary.simpleMessage("Toevoegen aan album"),
"addToEnte": MessageLookupByLibrary.simpleMessage("Toevoegen aan ente"),
"addToEnte": MessageLookupByLibrary.simpleMessage("Toevoegen aan Ente"),
"addToHiddenAlbum": MessageLookupByLibrary.simpleMessage(
"Toevoegen aan verborgen album"),
"addViewer": MessageLookupByLibrary.simpleMessage("Voeg kijker toe"),
@ -421,6 +423,8 @@ class MessageLookup extends MessageLookupByLibrary {
"claimedStorageSoFar": m10,
"cleanUncategorized":
MessageLookupByLibrary.simpleMessage("Ongecategoriseerd opschonen"),
"cleanUncategorizedDescription": MessageLookupByLibrary.simpleMessage(
"Verwijder alle bestanden van Ongecategoriseerd die aanwezig zijn in andere albums"),
"clearCaches": MessageLookupByLibrary.simpleMessage("Cache legen"),
"clearIndexes": MessageLookupByLibrary.simpleMessage("Index wissen"),
"click": MessageLookupByLibrary.simpleMessage("• Click"),
@ -438,7 +442,7 @@ class MessageLookup extends MessageLookupByLibrary {
"codeUsedByYou":
MessageLookupByLibrary.simpleMessage("Code gebruikt door jou"),
"collabLinkSectionDescription": MessageLookupByLibrary.simpleMessage(
"Maak een link waarmee mensen foto\'s in jouw gedeelde album kunnen toevoegen en bekijken zonder dat ze daarvoor een ente app of account nodig hebben. Handig voor het verzamelen van foto\'s van evenementen."),
"Maak een link waarmee mensen foto\'s in jouw gedeelde album kunnen toevoegen en bekijken zonder dat ze daarvoor een Ente app of account nodig hebben. Handig voor het verzamelen van foto\'s van evenementen."),
"collaborativeLink":
MessageLookupByLibrary.simpleMessage("Gezamenlijke link"),
"collaborativeLinkCreatedFor": m11,
@ -501,7 +505,7 @@ class MessageLookup extends MessageLookupByLibrary {
"createAlbumActionHint": MessageLookupByLibrary.simpleMessage(
"Lang indrukken om foto\'s te selecteren en klik + om een album te maken"),
"createCollaborativeLink":
MessageLookupByLibrary.simpleMessage("Create collaborative link"),
MessageLookupByLibrary.simpleMessage("Maak een gezamenlijke link"),
"createCollage": MessageLookupByLibrary.simpleMessage("Creëer collage"),
"createNewAccount":
MessageLookupByLibrary.simpleMessage("Nieuw account aanmaken"),
@ -516,6 +520,7 @@ class MessageLookup extends MessageLookupByLibrary {
"currentUsageIs":
MessageLookupByLibrary.simpleMessage("Huidig gebruik is "),
"custom": MessageLookupByLibrary.simpleMessage("Aangepast"),
"customEndpoint": m69,
"darkTheme": MessageLookupByLibrary.simpleMessage("Donker"),
"dayToday": MessageLookupByLibrary.simpleMessage("Vandaag"),
"dayYesterday": MessageLookupByLibrary.simpleMessage("Gisteren"),
@ -538,7 +543,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Hiermee worden alle lege albums verwijderd. Dit is handig wanneer je rommel in je albumlijst wilt verminderen."),
"deleteAll": MessageLookupByLibrary.simpleMessage("Alles Verwijderen"),
"deleteConfirmDialogBody": MessageLookupByLibrary.simpleMessage(
"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."),
"Dit account is gekoppeld aan andere Ente apps, als je er gebruik van maakt. Je geüploade gegevens worden in alle Ente apps gepland voor verwijdering, en je account wordt permanent verwijderd voor alle Ente diensten."),
"deleteEmailRequest": MessageLookupByLibrary.simpleMessage(
"Stuur een e-mail naar <warning>account-deletion@ente.io</warning> vanaf het door jou geregistreerde e-mailadres."),
"deleteEmptyAlbums":
@ -550,7 +555,7 @@ class MessageLookup extends MessageLookupByLibrary {
"deleteFromDevice":
MessageLookupByLibrary.simpleMessage("Verwijder van apparaat"),
"deleteFromEnte":
MessageLookupByLibrary.simpleMessage("Verwijder van ente"),
MessageLookupByLibrary.simpleMessage("Verwijder van Ente"),
"deleteItemCount": m14,
"deleteLocation":
MessageLookupByLibrary.simpleMessage("Verwijder locatie"),
@ -571,7 +576,7 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Gedeeld album verwijderen?"),
"deleteSharedAlbumDialogBody": MessageLookupByLibrary.simpleMessage(
"Het album wordt verwijderd voor iedereen\n\nJe verliest de toegang tot gedeelde foto\'s in dit album die eigendom zijn van anderen"),
"descriptions": MessageLookupByLibrary.simpleMessage("Descriptions"),
"descriptions": MessageLookupByLibrary.simpleMessage("Beschrijvingen"),
"deselectAll":
MessageLookupByLibrary.simpleMessage("Alles deselecteren"),
"designedToOutlive": MessageLookupByLibrary.simpleMessage(
@ -579,12 +584,16 @@ class MessageLookup extends MessageLookupByLibrary {
"details": MessageLookupByLibrary.simpleMessage("Details"),
"devAccountChanged": MessageLookupByLibrary.simpleMessage(
"Het ontwikkelaarsaccount dat we gebruiken om te publiceren in de App Store is veranderd. Daarom moet je opnieuw inloggen.\n\nOnze excuses voor het ongemak, helaas was dit onvermijdelijk."),
"developerSettings":
MessageLookupByLibrary.simpleMessage("Ontwikkelaarsinstellingen"),
"developerSettingsWarning": MessageLookupByLibrary.simpleMessage(
"Weet je zeker dat je de ontwikkelaarsinstellingen wilt wijzigen?"),
"deviceCodeHint":
MessageLookupByLibrary.simpleMessage("Voer de code in"),
"deviceFilesAutoUploading": MessageLookupByLibrary.simpleMessage(
"Bestanden toegevoegd aan dit album van dit apparaat zullen automatisch geüpload worden naar ente."),
"Bestanden toegevoegd aan dit album van dit apparaat zullen automatisch geüpload worden naar Ente."),
"deviceLockExplanation": MessageLookupByLibrary.simpleMessage(
"Schakel de schermvergrendeling van het apparaat uit wanneer ente op de voorgrond is en er een back-up aan de gang is. Dit is normaal gesproken niet nodig, maar kan grote uploads en initiële imports van grote mappen sneller laten verlopen."),
"Schakel de schermvergrendeling van het apparaat uit wanneer Ente op de voorgrond is en er een back-up aan de gang is. Dit is normaal gesproken niet nodig, maar kan grote uploads en initiële imports van grote mappen sneller laten verlopen."),
"deviceNotFound":
MessageLookupByLibrary.simpleMessage("Apparaat niet gevonden"),
"didYouKnow": MessageLookupByLibrary.simpleMessage("Wist u dat?"),
@ -648,15 +657,17 @@ class MessageLookup extends MessageLookupByLibrary {
"encryption": MessageLookupByLibrary.simpleMessage("Encryptie"),
"encryptionKeys":
MessageLookupByLibrary.simpleMessage("Encryptiesleutels"),
"endpointUpdatedMessage": MessageLookupByLibrary.simpleMessage(
"Eindpunt met succes bijgewerkt"),
"endtoendEncryptedByDefault": MessageLookupByLibrary.simpleMessage(
"Standaard end-to-end versleuteld"),
"enteCanEncryptAndPreserveFilesOnlyIfYouGrant":
MessageLookupByLibrary.simpleMessage(
"ente kan bestanden alleen versleutelen en bewaren als u toegang tot ze geeft"),
"Ente kan bestanden alleen versleutelen en bewaren als u toegang tot ze geeft"),
"entePhotosPerm": MessageLookupByLibrary.simpleMessage(
"ente <i>heeft toestemming nodig om</i> je foto\'s te bewaren"),
"Ente <i>heeft toestemming nodig om</i> je foto\'s te bewaren"),
"enteSubscriptionPitch": MessageLookupByLibrary.simpleMessage(
"ente bewaart uw herinneringen, zodat ze altijd beschikbaar voor u zijn, zelfs als u uw apparaat verliest."),
"Ente bewaart uw herinneringen, zodat ze altijd beschikbaar voor u zijn, zelfs als u uw apparaat verliest."),
"enteSubscriptionShareWithFamily": MessageLookupByLibrary.simpleMessage(
"Je familie kan ook aan je abonnement worden toegevoegd."),
"enterAlbumName":
@ -716,7 +727,7 @@ class MessageLookup extends MessageLookupByLibrary {
"failedToVerifyPaymentStatus": MessageLookupByLibrary.simpleMessage(
"Betalingsstatus verifiëren mislukt"),
"familyPlanOverview": MessageLookupByLibrary.simpleMessage(
"Voeg 5 gezinsleden toe aan uw bestaande abonnement zonder extra te betalen.\n\nElk lid krijgt zijn eigen privé ruimte en kan elkaars bestanden niet zien, tenzij ze zijn gedeeld.\n\nFamilieplannen zijn beschikbaar voor klanten die een betaald ente abonnement hebben.\n\nAbonneer u nu om aan de slag te gaan!"),
"Voeg 5 gezinsleden toe aan je bestaande abonnement zonder extra te betalen.\n\nElk lid krijgt zijn eigen privé ruimte en kan elkaars bestanden niet zien tenzij ze zijn gedeeld.\n\nFamilieplannen zijn beschikbaar voor klanten die een betaald Ente abonnement hebben.\n\nAbonneer nu om aan de slag te gaan!"),
"familyPlanPortalTitle":
MessageLookupByLibrary.simpleMessage("Familie"),
"familyPlans":
@ -777,6 +788,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Wij gebruiken geen tracking. Het zou helpen als je ons vertelt waar je ons gevonden hebt!"),
"hearUsWhereTitle": MessageLookupByLibrary.simpleMessage(
"Hoe hoorde je over Ente? (optioneel)"),
"help": MessageLookupByLibrary.simpleMessage("Hulp"),
"hidden": MessageLookupByLibrary.simpleMessage("Verborgen"),
"hide": MessageLookupByLibrary.simpleMessage("Verbergen"),
"hiding": MessageLookupByLibrary.simpleMessage("Verbergen..."),
@ -792,7 +804,7 @@ class MessageLookup extends MessageLookupByLibrary {
"iOSOkButton": MessageLookupByLibrary.simpleMessage("Oké"),
"ignoreUpdate": MessageLookupByLibrary.simpleMessage("Negeren"),
"ignoredFolderUploadReason": MessageLookupByLibrary.simpleMessage(
"Sommige bestanden in dit album worden genegeerd voor de upload omdat ze eerder van ente zijn verwijderd."),
"Sommige bestanden in dit album worden genegeerd voor uploaden omdat ze eerder van Ente zijn verwijderd."),
"importing": MessageLookupByLibrary.simpleMessage("Importeren...."),
"incorrectCode": MessageLookupByLibrary.simpleMessage("Onjuiste code"),
"incorrectPasswordTitle":
@ -811,16 +823,20 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Installeer handmatig"),
"invalidEmailAddress":
MessageLookupByLibrary.simpleMessage("Ongeldig e-mailadres"),
"invalidEndpoint":
MessageLookupByLibrary.simpleMessage("Ongeldig eindpunt"),
"invalidEndpointMessage": MessageLookupByLibrary.simpleMessage(
"Sorry, het eindpunt dat je hebt ingevoerd is ongeldig. Voer een geldig eindpunt in en probeer het opnieuw."),
"invalidKey": MessageLookupByLibrary.simpleMessage("Ongeldige sleutel"),
"invalidRecoveryKey": MessageLookupByLibrary.simpleMessage(
"De herstelsleutel die je hebt ingevoerd is niet geldig. Zorg ervoor dat deze 24 woorden bevat en controleer de spelling van elk van deze woorden.\n\nAls je een oudere herstelcode hebt ingevoerd, zorg ervoor dat deze 64 tekens lang is, en controleer ze allemaal."),
"invite": MessageLookupByLibrary.simpleMessage("Uitnodigen"),
"inviteToEnte":
MessageLookupByLibrary.simpleMessage("Uitnodigen voor ente"),
MessageLookupByLibrary.simpleMessage("Uitnodigen voor Ente"),
"inviteYourFriends":
MessageLookupByLibrary.simpleMessage("Vrienden uitnodigen"),
"inviteYourFriendsToEnte": MessageLookupByLibrary.simpleMessage(
"Vrienden uitnodigen voor ente"),
"Vrienden uitnodigen voor Ente"),
"itLooksLikeSomethingWentWrongPleaseRetryAfterSome":
MessageLookupByLibrary.simpleMessage(
"Het lijkt erop dat er iets fout is gegaan. Probeer het later opnieuw. Als de fout zich blijft voordoen, neem dan contact op met ons supportteam."),
@ -830,7 +846,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Bestanden tonen het aantal resterende dagen voordat ze permanent worden verwijderd"),
"itemsWillBeRemovedFromAlbum": MessageLookupByLibrary.simpleMessage(
"Geselecteerde items zullen worden verwijderd uit dit album"),
"joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"),
"joinDiscord": MessageLookupByLibrary.simpleMessage("Join de Discord"),
"keepPhotos": MessageLookupByLibrary.simpleMessage("Foto\'s behouden"),
"kiloMeterUnit": MessageLookupByLibrary.simpleMessage("km"),
"kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage(
@ -888,7 +904,7 @@ class MessageLookup extends MessageLookupByLibrary {
"locationName": MessageLookupByLibrary.simpleMessage("Locatie naam"),
"locationTagFeatureDescription": MessageLookupByLibrary.simpleMessage(
"Een locatie tag groept alle foto\'s die binnen een bepaalde straal van een foto zijn genomen"),
"locations": MessageLookupByLibrary.simpleMessage("Locations"),
"locations": MessageLookupByLibrary.simpleMessage("Locaties"),
"lockButtonLabel": MessageLookupByLibrary.simpleMessage("Vergrendel"),
"lockScreenEnablePreSteps": MessageLookupByLibrary.simpleMessage(
"Om vergrendelscherm in te schakelen, moet u een toegangscode of schermvergrendeling instellen in uw systeeminstellingen."),
@ -902,7 +918,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Dit zal logboeken verzenden om ons te helpen uw probleem op te lossen. Houd er rekening mee dat bestandsnamen zullen worden meegenomen om problemen met specifieke bestanden bij te houden."),
"longPressAnEmailToVerifyEndToEndEncryption":
MessageLookupByLibrary.simpleMessage(
"Long press an email to verify end to end encryption."),
"Druk lang op een e-mail om de versleuteling te verifiëren."),
"longpressOnAnItemToViewInFullscreen": MessageLookupByLibrary.simpleMessage(
"Houd een bestand lang ingedrukt om te bekijken op volledig scherm"),
"lostDevice":
@ -953,7 +969,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Kan geen verbinding maken met Ente, controleer uw netwerkinstellingen en neem contact op met ondersteuning als de fout zich blijft voordoen."),
"never": MessageLookupByLibrary.simpleMessage("Nooit"),
"newAlbum": MessageLookupByLibrary.simpleMessage("Nieuw album"),
"newToEnte": MessageLookupByLibrary.simpleMessage("Nieuw bij ente"),
"newToEnte": MessageLookupByLibrary.simpleMessage("Nieuw bij Ente"),
"newest": MessageLookupByLibrary.simpleMessage("Nieuwste"),
"no": MessageLookupByLibrary.simpleMessage("Nee"),
"noAlbumsSharedByYouYet": MessageLookupByLibrary.simpleMessage(
@ -1007,6 +1023,9 @@ class MessageLookup extends MessageLookupByLibrary {
"orPickAnExistingOne":
MessageLookupByLibrary.simpleMessage("Of kies een bestaande"),
"pair": MessageLookupByLibrary.simpleMessage("Koppelen"),
"passkey": MessageLookupByLibrary.simpleMessage("Passkey"),
"passkeyAuthTitle":
MessageLookupByLibrary.simpleMessage("Passkey verificatie"),
"password": MessageLookupByLibrary.simpleMessage("Wachtwoord"),
"passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage(
"Wachtwoord succesvol aangepast"),
@ -1018,6 +1037,8 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Betaalgegevens"),
"paymentFailed":
MessageLookupByLibrary.simpleMessage("Betaling mislukt"),
"paymentFailedMessage": MessageLookupByLibrary.simpleMessage(
"Helaas is je betaling mislukt. Neem contact op met support zodat we je kunnen helpen!"),
"paymentFailedTalkToProvider": m37,
"pendingItems":
MessageLookupByLibrary.simpleMessage("Bestanden in behandeling"),
@ -1206,6 +1227,7 @@ class MessageLookup extends MessageLookupByLibrary {
"scanThisBarcodeWithnyourAuthenticatorApp":
MessageLookupByLibrary.simpleMessage(
"Scan deze barcode met\nje authenticator app"),
"search": MessageLookupByLibrary.simpleMessage("Zoeken"),
"searchAlbumsEmptySection":
MessageLookupByLibrary.simpleMessage("Albums"),
"searchByAlbumNameHint":
@ -1253,7 +1275,7 @@ class MessageLookup extends MessageLookupByLibrary {
"selectYourPlan":
MessageLookupByLibrary.simpleMessage("Kies uw abonnement"),
"selectedFilesAreNotOnEnte": MessageLookupByLibrary.simpleMessage(
"Geselecteerde bestanden staan niet op ente"),
"Geselecteerde bestanden staan niet op Ente"),
"selectedFoldersWillBeEncryptedAndBackedUp":
MessageLookupByLibrary.simpleMessage(
"Geselecteerde mappen worden versleuteld en geback-upt"),
@ -1267,6 +1289,8 @@ class MessageLookup extends MessageLookupByLibrary {
"sendInvite":
MessageLookupByLibrary.simpleMessage("Stuur een uitnodiging"),
"sendLink": MessageLookupByLibrary.simpleMessage("Stuur link"),
"serverEndpoint":
MessageLookupByLibrary.simpleMessage("Server eindpunt"),
"sessionExpired":
MessageLookupByLibrary.simpleMessage("Sessie verlopen"),
"setAPassword":
@ -1290,15 +1314,15 @@ class MessageLookup extends MessageLookupByLibrary {
"Deel alleen met de mensen die u wilt"),
"shareTextConfirmOthersVerificationID": m49,
"shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage(
"Download ente zodat we gemakkelijk foto\'s en video\'s van originele kwaliteit kunnen delen\n\nhttps://ente.io"),
"Download Ente zodat we gemakkelijk foto\'s en video\'s in originele kwaliteit kunnen delen\n\nhttps://ente.io"),
"shareTextReferralCode": m50,
"shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage(
"Delen met niet-ente gebruikers"),
"Delen met niet-Ente gebruikers"),
"shareWithPeopleSectionTitle": m51,
"shareYourFirstAlbum":
MessageLookupByLibrary.simpleMessage("Deel jouw eerste album"),
"sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage(
"Maak gedeelde en collaboratieve albums met andere ente gebruikers, inclusief gebruikers met gratis abonnementen."),
"Maak gedeelde en collaboratieve albums met andere Ente gebruikers, inclusief gebruikers met gratis abonnementen."),
"sharedByMe": MessageLookupByLibrary.simpleMessage("Gedeeld door mij"),
"sharedByYou": MessageLookupByLibrary.simpleMessage("Gedeeld door jou"),
"sharedPhotoNotifications":
@ -1328,7 +1352,7 @@ class MessageLookup extends MessageLookupByLibrary {
"skip": MessageLookupByLibrary.simpleMessage("Overslaan"),
"social": MessageLookupByLibrary.simpleMessage("Sociale media"),
"someItemsAreInBothEnteAndYourDevice": MessageLookupByLibrary.simpleMessage(
"Sommige bestanden bevinden zich in zowel ente als op uw apparaat."),
"Sommige bestanden bevinden zich zowel in Ente als op jouw apparaat."),
"someOfTheFilesYouAreTryingToDeleteAre":
MessageLookupByLibrary.simpleMessage(
"Sommige bestanden die u probeert te verwijderen zijn alleen beschikbaar op uw apparaat en kunnen niet hersteld worden als deze verwijderd worden"),
@ -1494,9 +1518,8 @@ class MessageLookup extends MessageLookupByLibrary {
"Tot 50% korting, tot 4 december."),
"usableReferralStorageInfo": MessageLookupByLibrary.simpleMessage(
"Bruikbare opslag is beperkt door je huidige abonnement. Buitensporige geclaimde opslag zal automatisch bruikbaar worden wanneer je je abonnement upgrade."),
"usePublicLinksForPeopleNotOnEnte":
MessageLookupByLibrary.simpleMessage(
"Gebruik publieke links voor mensen die niet op ente zitten"),
"usePublicLinksForPeopleNotOnEnte": MessageLookupByLibrary.simpleMessage(
"Gebruik publieke links voor mensen die geen Ente account hebben"),
"useRecoveryKey":
MessageLookupByLibrary.simpleMessage("Herstelcode gebruiken"),
"useSelectedPhoto":
@ -1512,6 +1535,8 @@ class MessageLookup extends MessageLookupByLibrary {
"verifyEmail": MessageLookupByLibrary.simpleMessage("Bevestig e-mail"),
"verifyEmailID": m65,
"verifyIDLabel": MessageLookupByLibrary.simpleMessage("Verifiëren"),
"verifyPasskey":
MessageLookupByLibrary.simpleMessage("Bevestig passkey"),
"verifyPassword":
MessageLookupByLibrary.simpleMessage("Bevestig wachtwoord"),
"verifying": MessageLookupByLibrary.simpleMessage("Verifiëren..."),
@ -1532,6 +1557,8 @@ class MessageLookup extends MessageLookupByLibrary {
"viewer": MessageLookupByLibrary.simpleMessage("Kijker"),
"visitWebToManage": MessageLookupByLibrary.simpleMessage(
"Bezoek alstublieft web.ente.io om uw abonnement te beheren"),
"waitingForVerification":
MessageLookupByLibrary.simpleMessage("Wachten op verificatie..."),
"waitingForWifi":
MessageLookupByLibrary.simpleMessage("Wachten op WiFi..."),
"weAreOpenSource":

View file

@ -77,6 +77,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Modify your query, or try searching for"),
"moveToHiddenAlbum":
MessageLookupByLibrary.simpleMessage("Move to hidden album"),
"search": MessageLookupByLibrary.simpleMessage("Search"),
"selectALocation":
MessageLookupByLibrary.simpleMessage("Select a location"),
"selectALocationFirst":

View file

@ -171,6 +171,7 @@ class MessageLookup extends MessageLookupByLibrary {
"resetPasswordTitle":
MessageLookupByLibrary.simpleMessage("Zresetuj hasło"),
"saveKey": MessageLookupByLibrary.simpleMessage("Zapisz klucz"),
"search": MessageLookupByLibrary.simpleMessage("Search"),
"selectALocation":
MessageLookupByLibrary.simpleMessage("Select a location"),
"selectALocationFirst":

View file

@ -1219,6 +1219,7 @@ class MessageLookup extends MessageLookupByLibrary {
"scanThisBarcodeWithnyourAuthenticatorApp":
MessageLookupByLibrary.simpleMessage(
"Escaneie este código de barras com\nseu aplicativo autenticador"),
"search": MessageLookupByLibrary.simpleMessage("Pesquisar"),
"searchAlbumsEmptySection":
MessageLookupByLibrary.simpleMessage("Álbuns"),
"searchByAlbumNameHint":

View file

@ -988,6 +988,7 @@ class MessageLookup extends MessageLookupByLibrary {
"scanCode": MessageLookupByLibrary.simpleMessage("扫描二维码/条码"),
"scanThisBarcodeWithnyourAuthenticatorApp":
MessageLookupByLibrary.simpleMessage("用您的身份验证器应用\n扫描此条码"),
"search": MessageLookupByLibrary.simpleMessage("搜索"),
"searchAlbumsEmptySection": MessageLookupByLibrary.simpleMessage("相册"),
"searchByAlbumNameHint": MessageLookupByLibrary.simpleMessage("相册名称"),
"searchByExamples": MessageLookupByLibrary.simpleMessage(
@ -1026,7 +1027,7 @@ class MessageLookup extends MessageLookupByLibrary {
"selectedFilesAreNotOnEnte":
MessageLookupByLibrary.simpleMessage("所选文件不在 Ente 上"),
"selectedFoldersWillBeEncryptedAndBackedUp":
MessageLookupByLibrary.simpleMessage("所选文件夹将被加密备份"),
MessageLookupByLibrary.simpleMessage("所选文件夹将被加密备份"),
"selectedItemsWillBeDeletedFromAllAlbumsAndMoved":
MessageLookupByLibrary.simpleMessage("所选项目将从所有相册中删除并移动到回收站。"),
"selectedPhotos": m46,

View file

@ -8583,6 +8583,16 @@ class S {
args: [],
);
}
/// `Search`
String get search {
return Intl.message(
'Search',
name: 'search',
desc: '',
args: [],
);
}
}
class AppLocalizationDelegate extends LocalizationsDelegate<S> {

View file

@ -17,5 +17,6 @@
"addViewers": "{count, plural, zero {Add viewer} one {Add viewer} other {Add viewers}}",
"addCollaborators": "{count, plural, zero {Add collaborator} one {Add collaborator} other {Add collaborators}}",
"longPressAnEmailToVerifyEndToEndEncryption": "Long press an email to verify end to end encryption.",
"createCollaborativeLink": "Create collaborative link"
"createCollaborativeLink": "Create collaborative link",
"search": "Search"
}

View file

@ -1205,5 +1205,6 @@
"addViewers": "{count, plural, zero {Add viewer} one {Add viewer} other {Add viewers}}",
"addCollaborators": "{count, plural, zero {Add collaborator} one {Add collaborator} other {Add collaborators}}",
"longPressAnEmailToVerifyEndToEndEncryption": "Long press an email to verify end to end encryption.",
"createCollaborativeLink": "Create collaborative link"
"createCollaborativeLink": "Create collaborative link",
"search": "Search"
}

View file

@ -1214,5 +1214,6 @@
"invalidEndpointMessage": "Sorry, the endpoint you entered is invalid. Please enter a valid endpoint and try again.",
"endpointUpdatedMessage": "Endpoint updated successfully",
"customEndpoint": "Connected to {endpoint}",
"createCollaborativeLink": "Create collaborative link"
"createCollaborativeLink": "Create collaborative link",
"search": "Search"
}

View file

@ -979,5 +979,6 @@
"addViewers": "{count, plural, zero {Add viewer} one {Add viewer} other {Add viewers}}",
"addCollaborators": "{count, plural, zero {Add collaborator} one {Add collaborator} other {Add collaborators}}",
"longPressAnEmailToVerifyEndToEndEncryption": "Long press an email to verify end to end encryption.",
"createCollaborativeLink": "Create collaborative link"
"createCollaborativeLink": "Create collaborative link",
"search": "Search"
}

View file

@ -1160,5 +1160,6 @@
"addViewers": "{count, plural, zero {Add viewer} one {Add viewer} other {Add viewers}}",
"addCollaborators": "{count, plural, zero {Add collaborator} one {Add collaborator} other {Add collaborators}}",
"longPressAnEmailToVerifyEndToEndEncryption": "Long press an email to verify end to end encryption.",
"createCollaborativeLink": "Create collaborative link"
"createCollaborativeLink": "Create collaborative link",
"search": "Search"
}

View file

@ -1122,5 +1122,6 @@
"addViewers": "{count, plural, zero {Add viewer} one {Add viewer} other {Add viewers}}",
"addCollaborators": "{count, plural, zero {Add collaborator} one {Add collaborator} other {Add collaborators}}",
"longPressAnEmailToVerifyEndToEndEncryption": "Long press an email to verify end to end encryption.",
"createCollaborativeLink": "Create collaborative link"
"createCollaborativeLink": "Create collaborative link",
"search": "Search"
}

View file

@ -17,5 +17,6 @@
"addViewers": "{count, plural, zero {Add viewer} one {Add viewer} other {Add viewers}}",
"addCollaborators": "{count, plural, zero {Add collaborator} one {Add collaborator} other {Add collaborators}}",
"longPressAnEmailToVerifyEndToEndEncryption": "Long press an email to verify end to end encryption.",
"createCollaborativeLink": "Create collaborative link"
"createCollaborativeLink": "Create collaborative link",
"search": "Search"
}

View file

@ -23,7 +23,7 @@
"sendEmail": "E-mail versturen",
"deleteRequestSLAText": "Je verzoek wordt binnen 72 uur verwerkt.",
"deleteEmailRequest": "Stuur een e-mail naar <warning>account-deletion@ente.io</warning> vanaf het door jou geregistreerde e-mailadres.",
"entePhotosPerm": "ente <i>heeft toestemming nodig om</i> je foto's te bewaren",
"entePhotosPerm": "Ente <i>heeft toestemming nodig om</i> je foto's te bewaren",
"ok": "Oké",
"createAccount": "Account aanmaken",
"createNewAccount": "Nieuw account aanmaken",
@ -225,17 +225,17 @@
},
"description": "Number of participants in an album, including the album owner."
},
"collabLinkSectionDescription": "Maak een link waarmee mensen foto's in jouw gedeelde album kunnen toevoegen en bekijken zonder dat ze daarvoor een ente app of account nodig hebben. Handig voor het verzamelen van foto's van evenementen.",
"collabLinkSectionDescription": "Maak een link waarmee mensen foto's in jouw gedeelde album kunnen toevoegen en bekijken zonder dat ze daarvoor een Ente app of account nodig hebben. Handig voor het verzamelen van foto's van evenementen.",
"collectPhotos": "Foto's verzamelen",
"collaborativeLink": "Gezamenlijke link",
"shareWithNonenteUsers": "Delen met niet-ente gebruikers",
"shareWithNonenteUsers": "Delen met niet-Ente gebruikers",
"createPublicLink": "Maak publieke link",
"sendLink": "Stuur link",
"copyLink": "Kopieer link",
"linkHasExpired": "Link is vervallen",
"publicLinkEnabled": "Publieke link ingeschakeld",
"shareALink": "Deel een link",
"sharedAlbumSectionDescription": "Maak gedeelde en collaboratieve albums met andere ente gebruikers, inclusief gebruikers met gratis abonnementen.",
"sharedAlbumSectionDescription": "Maak gedeelde en collaboratieve albums met andere Ente gebruikers, inclusief gebruikers met gratis abonnementen.",
"shareWithPeopleSectionTitle": "{numberOfPeople, plural, =0 {Deel met specifieke mensen} =1 {Gedeeld met 1 persoon} other {Gedeeld met {numberOfPeople} mensen}}",
"@shareWithPeopleSectionTitle": {
"placeholders": {
@ -259,12 +259,12 @@
},
"verificationId": "Verificatie ID",
"verifyEmailID": "Verifieer {email}",
"emailNoEnteAccount": "{email} heeft geen ente account.\n\nStuur ze een uitnodiging om foto's te delen.",
"emailNoEnteAccount": "{email} heeft geen Ente account.\n\nStuur ze een uitnodiging om foto's te delen.",
"shareMyVerificationID": "Hier is mijn verificatie-ID: {verificationID} voor ente.io.",
"shareTextConfirmOthersVerificationID": "Hey, kunt u bevestigen dat dit uw ente.io verificatie-ID is: {verificationID}",
"somethingWentWrong": "Er ging iets mis",
"sendInvite": "Stuur een uitnodiging",
"shareTextRecommendUsingEnte": "Download ente zodat we gemakkelijk foto's en video's van originele kwaliteit kunnen delen\n\nhttps://ente.io",
"shareTextRecommendUsingEnte": "Download Ente zodat we gemakkelijk foto's en video's in originele kwaliteit kunnen delen\n\nhttps://ente.io",
"done": "Voltooid",
"applyCodeTitle": "Code toepassen",
"enterCodeDescription": "Voer de code van de vriend in om gratis opslag voor jullie beiden te claimen",
@ -281,7 +281,7 @@
"claimMore": "Claim meer!",
"theyAlsoGetXGb": "Zij krijgen ook {storageAmountInGB} GB",
"freeStorageOnReferralSuccess": "{storageAmountInGB} GB telkens als iemand zich aanmeldt voor een betaald abonnement en je code toepast",
"shareTextReferralCode": "ente verwijzingscode: {referralCode} \n\nPas het toe bij Instellingen → Algemeen → Verwijzingen om {referralStorageInGB} GB gratis te krijgen nadat je je hebt aangemeld voor een betaald abonnement\n\nhttps://ente.io",
"shareTextReferralCode": "Ente verwijzingscode: {referralCode} \n\nPas het toe bij Instellingen → Algemeen → Verwijzingen om {referralStorageInGB} GB gratis te krijgen nadat je je hebt aangemeld voor een betaald abonnement\n\nhttps://ente.io",
"claimFreeStorage": "Claim gratis opslag",
"inviteYourFriends": "Vrienden uitnodigen",
"failedToFetchReferralDetails": "Kan geen verwijzingsgegevens ophalen. Probeer het later nog eens.",
@ -304,6 +304,7 @@
}
},
"faq": "Veelgestelde vragen",
"help": "Hulp",
"oopsSomethingWentWrong": "Oeps, er is iets misgegaan",
"peopleUsingYourCode": "Mensen die jouw code gebruiken",
"eligible": "gerechtigd",
@ -333,7 +334,7 @@
"removeParticipantBody": "{userEmail} zal worden verwijderd uit dit gedeelde album\n\nAlle door hen toegevoegde foto's worden ook uit het album verwijderd",
"keepPhotos": "Foto's behouden",
"deletePhotos": "Foto's verwijderen",
"inviteToEnte": "Uitnodigen voor ente",
"inviteToEnte": "Uitnodigen voor Ente",
"removePublicLink": "Verwijder publieke link",
"disableLinkMessage": "Dit verwijdert de openbare link voor toegang tot \"{albumName}\".",
"sharing": "Delen...",
@ -349,10 +350,10 @@
"videoSmallCase": "video",
"photoSmallCase": "foto",
"singleFileDeleteHighlight": "Het wordt uit alle albums verwijderd.",
"singleFileInBothLocalAndRemote": "Deze {fileType} staat zowel in ente als op jouw apparaat.",
"singleFileInRemoteOnly": "Deze {fileType} zal worden verwijderd uit ente.",
"singleFileInBothLocalAndRemote": "Deze {fileType} staat zowel in Ente als op jouw apparaat.",
"singleFileInRemoteOnly": "Deze {fileType} zal worden verwijderd uit Ente.",
"singleFileDeleteFromDevice": "Deze {fileType} zal worden verwijderd van jouw apparaat.",
"deleteFromEnte": "Verwijder van ente",
"deleteFromEnte": "Verwijder van Ente",
"yesDelete": "Ja, verwijderen",
"movedToTrash": "Naar prullenbak verplaatst",
"deleteFromDevice": "Verwijder van apparaat",
@ -444,7 +445,7 @@
"backupOverMobileData": "Back-up maken via mobiele data",
"backupVideos": "Back-up video's",
"disableAutoLock": "Automatisch vergrendelen uitschakelen",
"deviceLockExplanation": "Schakel de schermvergrendeling van het apparaat uit wanneer ente op de voorgrond is en er een back-up aan de gang is. Dit is normaal gesproken niet nodig, maar kan grote uploads en initiële imports van grote mappen sneller laten verlopen.",
"deviceLockExplanation": "Schakel de schermvergrendeling van het apparaat uit wanneer Ente op de voorgrond is en er een back-up aan de gang is. Dit is normaal gesproken niet nodig, maar kan grote uploads en initiële imports van grote mappen sneller laten verlopen.",
"about": "Over",
"weAreOpenSource": "We zijn open source!",
"privacy": "Privacy",
@ -464,7 +465,7 @@
"authToInitiateAccountDeletion": "Gelieve te verifiëren om het verwijderen van je account te starten",
"areYouSureYouWantToLogout": "Weet je zeker dat je wilt uitloggen?",
"yesLogout": "Ja, log uit",
"aNewVersionOfEnteIsAvailable": "Er is een nieuwe versie van ente beschikbaar.",
"aNewVersionOfEnteIsAvailable": "Er is een nieuwe versie van Ente beschikbaar.",
"update": "Update",
"installManually": "Installeer handmatig",
"criticalUpdateAvailable": "Belangrijke update beschikbaar",
@ -553,11 +554,11 @@
"systemTheme": "Systeem",
"freeTrial": "Gratis proefversie",
"selectYourPlan": "Kies uw abonnement",
"enteSubscriptionPitch": "ente bewaart uw herinneringen, zodat ze altijd beschikbaar voor u zijn, zelfs als u uw apparaat verliest.",
"enteSubscriptionPitch": "Ente bewaart uw herinneringen, zodat ze altijd beschikbaar voor u zijn, zelfs als u uw apparaat verliest.",
"enteSubscriptionShareWithFamily": "Je familie kan ook aan je abonnement worden toegevoegd.",
"currentUsageIs": "Huidig gebruik is ",
"@currentUsageIs": {
"description": "This text is followed by storage usaged",
"description": "This text is followed by storage usage",
"examples": {
"0": "Current usage is 1.2 GB"
},
@ -619,7 +620,7 @@
"appleId": "Apple ID",
"playstoreSubscription": "PlayStore abonnement",
"appstoreSubscription": "PlayStore abonnement",
"subAlreadyLinkedErrMessage": "Uw {id} is al aan een ander ente account gekoppeld.\nAls u uw {id} wilt gebruiken met dit account, neem dan contact op met onze klantenservice",
"subAlreadyLinkedErrMessage": "Jouw {id} is al aan een ander Ente account gekoppeld.\nAls je jouw {id} wilt gebruiken met dit account, neem dan contact op met onze klantenservice",
"visitWebToManage": "Bezoek alstublieft web.ente.io om uw abonnement te beheren",
"couldNotUpdateSubscription": "Kon abonnement niet wijzigen",
"pleaseContactSupportAndWeWillBeHappyToHelp": "Neem alstublieft contact op met support@ente.io en we helpen u graag!",
@ -640,7 +641,7 @@
"thankYou": "Bedankt",
"failedToVerifyPaymentStatus": "Betalingsstatus verifiëren mislukt",
"pleaseWaitForSometimeBeforeRetrying": "Gelieve even te wachten voordat u opnieuw probeert",
"paymentFailedWithReason": "Helaas is uw betaling mislukt vanwege {reason}",
"paymentFailedMessage": "Helaas is je betaling mislukt. Neem contact op met support zodat we je kunnen helpen!",
"youAreOnAFamilyPlan": "U bent onderdeel van een familie abonnement!",
"contactFamilyAdmin": "Neem contact op met <green>{familyAdminEmail}</green> om uw abonnement te beheren",
"leaveFamily": "Familie abonnement verlaten",
@ -664,7 +665,7 @@
"everywhere": "overal",
"androidIosWebDesktop": "Android, iOS, Web, Desktop",
"mobileWebDesktop": "Mobiel, Web, Desktop",
"newToEnte": "Nieuw bij ente",
"newToEnte": "Nieuw bij Ente",
"pleaseLoginAgain": "Log opnieuw in",
"devAccountChanged": "Het ontwikkelaarsaccount dat we gebruiken om te publiceren in de App Store is veranderd. Daarom moet je opnieuw inloggen.\n\nOnze excuses voor het ongemak, helaas was dit onvermijdelijk.",
"yourSubscriptionHasExpired": "Uw abonnement is verlopen",
@ -677,12 +678,12 @@
},
"backupFailed": "Back-up mislukt",
"couldNotBackUpTryLater": "We konden uw gegevens niet back-uppen.\nWe zullen het later opnieuw proberen.",
"enteCanEncryptAndPreserveFilesOnlyIfYouGrant": "ente kan bestanden alleen versleutelen en bewaren als u toegang tot ze geeft",
"enteCanEncryptAndPreserveFilesOnlyIfYouGrant": "Ente kan bestanden alleen versleutelen en bewaren als u toegang tot ze geeft",
"pleaseGrantPermissions": "Geef alstublieft toestemming",
"grantPermission": "Toestemming verlenen",
"privateSharing": "Privé delen",
"shareOnlyWithThePeopleYouWant": "Deel alleen met de mensen die u wilt",
"usePublicLinksForPeopleNotOnEnte": "Gebruik publieke links voor mensen die niet op ente zitten",
"usePublicLinksForPeopleNotOnEnte": "Gebruik publieke links voor mensen die geen Ente account hebben",
"allowPeopleToAddPhotos": "Mensen toestaan foto's toe te voegen",
"shareAnAlbumNow": "Deel nu een album",
"collectEventPhotos": "Foto's van gebeurtenissen verzamelen",
@ -694,7 +695,7 @@
},
"onDevice": "Op het apparaat",
"@onEnte": {
"description": "The text displayed above albums backed up to ente",
"description": "The text displayed above albums backed up to Ente",
"type": "text"
},
"onEnte": "Op <branding>ente</branding>",
@ -740,7 +741,7 @@
"saveCollage": "Sla collage op",
"collageSaved": "Collage opgeslagen in gallerij",
"collageLayout": "Layout",
"addToEnte": "Toevoegen aan ente",
"addToEnte": "Toevoegen aan Ente",
"addToAlbum": "Toevoegen aan album",
"delete": "Verwijderen",
"hide": "Verbergen",
@ -805,9 +806,9 @@
"photosAddedByYouWillBeRemovedFromTheAlbum": "Foto's toegevoegd door u zullen worden verwijderd uit het album",
"youveNoFilesInThisAlbumThatCanBeDeleted": "Je hebt geen bestanden in dit album die verwijderd kunnen worden",
"youDontHaveAnyArchivedItems": "U heeft geen gearchiveerde bestanden.",
"ignoredFolderUploadReason": "Sommige bestanden in dit album worden genegeerd voor de upload omdat ze eerder van ente zijn verwijderd.",
"ignoredFolderUploadReason": "Sommige bestanden in dit album worden genegeerd voor uploaden omdat ze eerder van Ente zijn verwijderd.",
"resetIgnoredFiles": "Reset genegeerde bestanden",
"deviceFilesAutoUploading": "Bestanden toegevoegd aan dit album van dit apparaat zullen automatisch geüpload worden naar ente.",
"deviceFilesAutoUploading": "Bestanden toegevoegd aan dit album van dit apparaat zullen automatisch geüpload worden naar Ente.",
"turnOnBackupForAutoUpload": "Schakel back-up in om bestanden die toegevoegd zijn aan deze map op dit apparaat automatisch te uploaden.",
"noHiddenPhotosOrVideos": "Geen verborgen foto's of video's",
"toHideAPhotoOrVideo": "Om een foto of video te verbergen",
@ -885,7 +886,7 @@
"@freeUpSpaceSaving": {
"description": "Text to tell user how much space they can free up by deleting items from the device"
},
"freeUpAccessPostDelete": "U heeft nog steeds toegang tot {count, plural, one {het} other {ze}} op ente zolang u een actief abonnement heeft",
"freeUpAccessPostDelete": "Je hebt nog steeds toegang tot {count, plural, one {het} other {ze}} op Ente zolang je een actief abonnement hebt",
"@freeUpAccessPostDelete": {
"placeholders": {
"count": {
@ -936,7 +937,7 @@
"renameFile": "Bestandsnaam wijzigen",
"enterFileName": "Geef bestandsnaam op",
"filesDeleted": "Bestanden verwijderd",
"selectedFilesAreNotOnEnte": "Geselecteerde bestanden staan niet op ente",
"selectedFilesAreNotOnEnte": "Geselecteerde bestanden staan niet op Ente",
"thisActionCannotBeUndone": "Deze actie kan niet ongedaan gemaakt worden",
"emptyTrash": "Prullenbak leegmaken?",
"permDeleteWarning": "Alle bestanden in de prullenbak zullen permanent worden verwijderd\n\nDeze actie kan niet ongedaan worden gemaakt",
@ -945,7 +946,7 @@
"permanentlyDeleteFromDevice": "Permanent verwijderen van apparaat?",
"someOfTheFilesYouAreTryingToDeleteAre": "Sommige bestanden die u probeert te verwijderen zijn alleen beschikbaar op uw apparaat en kunnen niet hersteld worden als deze verwijderd worden",
"theyWillBeDeletedFromAllAlbums": "Ze zullen uit alle albums worden verwijderd.",
"someItemsAreInBothEnteAndYourDevice": "Sommige bestanden bevinden zich in zowel ente als op uw apparaat.",
"someItemsAreInBothEnteAndYourDevice": "Sommige bestanden bevinden zich zowel in Ente als op jouw apparaat.",
"selectedItemsWillBeDeletedFromAllAlbumsAndMoved": "Geselecteerde bestanden worden verwijderd uit alle albums en verplaatst naar de prullenbak.",
"theseItemsWillBeDeletedFromYourDevice": "Deze bestanden zullen worden verwijderd van uw apparaat.",
"itLooksLikeSomethingWentWrongPleaseRetryAfterSome": "Het lijkt erop dat er iets fout is gegaan. Probeer het later opnieuw. Als de fout zich blijft voordoen, neem dan contact op met ons supportteam.",
@ -1051,7 +1052,7 @@
},
"setRadius": "Radius instellen",
"familyPlanPortalTitle": "Familie",
"familyPlanOverview": "Voeg 5 gezinsleden toe aan uw bestaande abonnement zonder extra te betalen.\n\nElk lid krijgt zijn eigen privé ruimte en kan elkaars bestanden niet zien, tenzij ze zijn gedeeld.\n\nFamilieplannen zijn beschikbaar voor klanten die een betaald ente abonnement hebben.\n\nAbonneer u nu om aan de slag te gaan!",
"familyPlanOverview": "Voeg 5 gezinsleden toe aan je bestaande abonnement zonder extra te betalen.\n\nElk lid krijgt zijn eigen privé ruimte en kan elkaars bestanden niet zien tenzij ze zijn gedeeld.\n\nFamilieplannen zijn beschikbaar voor klanten die een betaald Ente abonnement hebben.\n\nAbonneer nu om aan de slag te gaan!",
"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."
@ -1129,7 +1130,7 @@
"noAlbumsSharedByYouYet": "Nog geen albums gedeeld door jou",
"sharedWithYou": "Gedeeld met jou",
"sharedByYou": "Gedeeld door jou",
"inviteYourFriendsToEnte": "Vrienden uitnodigen voor ente",
"inviteYourFriendsToEnte": "Vrienden uitnodigen voor Ente",
"failedToDownloadVideo": "Downloaden van video mislukt",
"hiding": "Verbergen...",
"unhiding": "Zichtbaar maken...",
@ -1139,7 +1140,7 @@
"addToHiddenAlbum": "Toevoegen aan verborgen album",
"moveToHiddenAlbum": "Verplaatsen naar verborgen album",
"fileTypes": "Bestandstype",
"deleteConfirmDialogBody": "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.",
"deleteConfirmDialogBody": "Dit account is gekoppeld aan andere Ente apps, als je er gebruik van maakt. Je geüploade gegevens worden in alle Ente apps gepland voor verwijdering, en je account wordt permanent verwijderd voor alle Ente diensten.",
"hearUsWhereTitle": "Hoe hoorde je over Ente? (optioneel)",
"hearUsExplanation": "Wij gebruiken geen tracking. Het zou helpen als je ons vertelt waar je ons gevonden hebt!",
"viewAddOnButton": "Add-ons bekijken",
@ -1187,16 +1188,29 @@
"changeLocationOfSelectedItems": "Locatie van geselecteerde items wijzigen?",
"editsToLocationWillOnlyBeSeenWithinEnte": "Bewerkte locatie wordt alleen gezien binnen Ente",
"cleanUncategorized": "Ongecategoriseerd opschonen",
"cleanUncategorizedDescription": "Verwijder alle bestanden van Ongecategoriseerd die aanwezig zijn in andere albums",
"waitingForVerification": "Wachten op verificatie...",
"passkey": "Passkey",
"passkeyAuthTitle": "Passkey verificatie",
"verifyPasskey": "Bevestig passkey",
"playOnTv": "Album afspelen op TV",
"pair": "Koppelen",
"deviceNotFound": "Apparaat niet gevonden",
"castInstruction": "Bezoek cast.ente.io op het apparaat dat u wilt koppelen.\n\nVoer de code hieronder in om het album op uw TV af te spelen.",
"deviceCodeHint": "Voer de code in",
"joinDiscord": "Join Discord",
"locations": "Locations",
"descriptions": "Descriptions",
"addViewers": "{count, plural, zero {Add viewer} one {Add viewer} other {Add viewers}}",
"addCollaborators": "{count, plural, zero {Add collaborator} one {Add collaborator} other {Add collaborators}}",
"longPressAnEmailToVerifyEndToEndEncryption": "Long press an email to verify end to end encryption.",
"createCollaborativeLink": "Create collaborative link"
"joinDiscord": "Join de Discord",
"locations": "Locaties",
"descriptions": "Beschrijvingen",
"addViewers": "{count, plural, one {Voeg kijker toe} other {Voeg kijkers toe}}",
"addCollaborators": "{count, plural, zero {Voeg samenwerker toe} one {Voeg samenwerker toe} other {Voeg samenwerkers toe}}",
"longPressAnEmailToVerifyEndToEndEncryption": "Druk lang op een e-mail om de versleuteling te verifiëren.",
"developerSettingsWarning": "Weet je zeker dat je de ontwikkelaarsinstellingen wilt wijzigen?",
"developerSettings": "Ontwikkelaarsinstellingen",
"serverEndpoint": "Server eindpunt",
"invalidEndpoint": "Ongeldig eindpunt",
"invalidEndpointMessage": "Sorry, het eindpunt dat je hebt ingevoerd is ongeldig. Voer een geldig eindpunt in en probeer het opnieuw.",
"endpointUpdatedMessage": "Eindpunt met succes bijgewerkt",
"customEndpoint": "Verbonden met {endpoint}",
"createCollaborativeLink": "Maak een gezamenlijke link",
"search": "Zoeken"
}

View file

@ -31,5 +31,6 @@
"addViewers": "{count, plural, zero {Add viewer} one {Add viewer} other {Add viewers}}",
"addCollaborators": "{count, plural, zero {Add collaborator} one {Add collaborator} other {Add collaborators}}",
"longPressAnEmailToVerifyEndToEndEncryption": "Long press an email to verify end to end encryption.",
"createCollaborativeLink": "Create collaborative link"
"createCollaborativeLink": "Create collaborative link",
"search": "Search"
}

View file

@ -118,5 +118,6 @@
"addViewers": "{count, plural, zero {Add viewer} one {Add viewer} other {Add viewers}}",
"addCollaborators": "{count, plural, zero {Add collaborator} one {Add collaborator} other {Add collaborators}}",
"longPressAnEmailToVerifyEndToEndEncryption": "Long press an email to verify end to end encryption.",
"createCollaborativeLink": "Create collaborative link"
"createCollaborativeLink": "Create collaborative link",
"search": "Search"
}

View file

@ -1212,5 +1212,6 @@
"invalidEndpointMessage": "Desculpe, o endpoint que você inseriu é inválido. Por favor, insira um endpoint válido e tente novamente.",
"endpointUpdatedMessage": "Endpoint atualizado com sucesso",
"customEndpoint": "Conectado a {endpoint}",
"createCollaborativeLink": "Criar link colaborativo"
"createCollaborativeLink": "Criar link colaborativo",
"search": "Pesquisar"
}

View file

@ -417,7 +417,7 @@
"pendingItems": "待处理项目",
"clearIndexes": "清空索引",
"selectFoldersForBackup": "选择要备份的文件夹",
"selectedFoldersWillBeEncryptedAndBackedUp": "所选文件夹将被加密备份",
"selectedFoldersWillBeEncryptedAndBackedUp": "所选文件夹将被加密备份",
"unselectAll": "取消全部选择",
"selectAll": "全选",
"skip": "跳过",
@ -1211,5 +1211,6 @@
"invalidEndpointMessage": "抱歉,您输入的端点无效。请输入有效的端点,然后重试。",
"endpointUpdatedMessage": "端点更新成功",
"customEndpoint": "已连接至 {endpoint}",
"createCollaborativeLink": "创建协作链接"
"createCollaborativeLink": "创建协作链接",
"search": "搜索"
}

View file

@ -85,13 +85,24 @@ class EnteFile {
static int parseFileCreationTime(String? fileTitle, AssetEntity asset) {
int creationTime = asset.createDateTime.microsecondsSinceEpoch;
final int modificationTime = asset.modifiedDateTime.microsecondsSinceEpoch;
if (creationTime >= jan011981Time) {
// assuming that fileSystem is returning correct creationTime.
// During upload, this might get overridden with exif Creation time
// When the assetModifiedTime is less than creationTime, than just use
// that as creationTime. This is to handle cases where file might be
// copied to the fileSystem from somewhere else See #https://superuser.com/a/1091147
if (modificationTime >= jan011981Time &&
modificationTime < creationTime) {
_logger.info(
'LocalID: ${asset.id} modification time is less than creation time. Using modification time as creation time',
);
creationTime = modificationTime;
}
return creationTime;
} else {
if (asset.modifiedDateTime.microsecondsSinceEpoch >= jan011981Time) {
creationTime = asset.modifiedDateTime.microsecondsSinceEpoch;
if (modificationTime >= jan011981Time) {
creationTime = modificationTime;
} else {
creationTime = DateTime.now().toUtc().microsecondsSinceEpoch;
}
@ -106,7 +117,6 @@ class EnteFile {
// ignore
}
}
return creationTime;
}

View file

@ -12,7 +12,7 @@ part of 'location.dart';
T _$identity<T>(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<String, dynamic> 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<String, dynamic> json) =>
_$$_LocationFromJson(json);
factory _$LocationImpl.fromJson(Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> json) = _$_Location.fromJson;
factory _Location.fromJson(Map<String, dynamic> 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;
}

View file

@ -6,12 +6,13 @@ part of 'location.dart';
// JsonSerializableGenerator
// **************************************************************************
_$_Location _$$_LocationFromJson(Map<String, dynamic> json) => _$_Location(
_$LocationImpl _$$LocationImplFromJson(Map<String, dynamic> json) =>
_$LocationImpl(
latitude: (json['latitude'] as num?)?.toDouble(),
longitude: (json['longitude'] as num?)?.toDouble(),
);
Map<String, dynamic> _$$_LocationToJson(_$_Location instance) =>
Map<String, dynamic> _$$LocationImplToJson(_$LocationImpl instance) =>
<String, dynamic>{
'latitude': instance.latitude,
'longitude': instance.longitude,

View file

@ -12,7 +12,7 @@ part of 'location_tag.dart';
T _$identity<T>(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<String, dynamic> 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<String, dynamic> json) =>
_$$_LocationTagFromJson(json);
factory _$LocationTagImpl.fromJson(Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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;
}

Some files were not shown because too many files have changed in this diff Show more