Merge branch 'main' into mobile-panorama

This commit is contained in:
Prateek Sunal 2024-07-04 21:08:40 +05:30
commit db5229170b
71 changed files with 3278 additions and 647 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -34,6 +34,7 @@ import 'package:photos/services/search_service.dart';
import 'package:photos/services/sync_service.dart';
import 'package:photos/utils/crypto_util.dart';
import 'package:photos/utils/file_uploader.dart';
import "package:photos/utils/lock_screen_settings.dart";
import 'package:photos/utils/validator_util.dart';
import "package:photos/utils/wakelock_util.dart";
import 'package:shared_preferences/shared_preferences.dart';
@ -59,7 +60,7 @@ class Configuration {
// keyShouldKeepDeviceAwake is used to determine whether the device screen
// should be kept on while the app is in foreground.
static const keyShouldKeepDeviceAwake = "should_keep_device_awake";
static const keyShouldShowLockScreen = "should_show_lock_screen";
static const keyShowSystemLockScreen = "should_show_lock_screen";
static const keyHasSelectedAnyBackupFolder =
"has_selected_any_folder_for_backup";
static const lastTempFolderClearTimeKey = "last_temp_folder_clear_time";
@ -72,7 +73,6 @@ class Configuration {
"has_selected_all_folders_for_backup";
static const anonymousUserIDKey = "anonymous_user_id";
static const endPointKey = "endpoint";
static final _logger = Logger("Configuration");
String? _cachedToken;
@ -83,7 +83,6 @@ class Configuration {
late FlutterSecureStorage _secureStorage;
late String _tempDocumentsDirPath;
late String _thumbnailCacheDirectory;
// 6th July 22: Remove this after 3 months. Hopefully, active users
// will migrate to newer version of the app, where shared media is stored
// on appSupport directory which OS won't clean up automatically
@ -621,16 +620,22 @@ class Configuration {
}
}
bool shouldShowLockScreen() {
if (_preferences.containsKey(keyShouldShowLockScreen)) {
return _preferences.getBool(keyShouldShowLockScreen)!;
Future<bool> shouldShowLockScreen() async {
final bool isPin = await LockScreenSettings.instance.isPinSet();
final bool isPass = await LockScreenSettings.instance.isPasswordSet();
return isPin || isPass || shouldShowSystemLockScreen();
}
bool shouldShowSystemLockScreen() {
if (_preferences.containsKey(keyShowSystemLockScreen)) {
return _preferences.getBool(keyShowSystemLockScreen)!;
} else {
return false;
}
}
Future<void> setShouldShowLockScreen(bool value) {
return _preferences.setBool(keyShouldShowLockScreen, value);
Future<void> setSystemLockScreen(bool value) {
return _preferences.setBool(keyShowSystemLockScreen, value);
}
void setVolatilePassword(String volatilePassword) {

View File

@ -32,6 +32,7 @@ class MessageLookup extends MessageLookupByLibrary {
"addToHiddenAlbum":
MessageLookupByLibrary.simpleMessage("Add to hidden album"),
"addViewers": m1,
"appLock": MessageLookupByLibrary.simpleMessage("App lock"),
"changeLocationOfSelectedItems": MessageLookupByLibrary.simpleMessage(
"Change location of selected items?"),
"clusteringProgress":
@ -42,12 +43,14 @@ class MessageLookup extends MessageLookupByLibrary {
"deleteConfirmDialogBody": MessageLookupByLibrary.simpleMessage(
"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."),
"descriptions": MessageLookupByLibrary.simpleMessage("Descriptions"),
"deviceLock": MessageLookupByLibrary.simpleMessage("Device lock"),
"editLocation": MessageLookupByLibrary.simpleMessage("Edit location"),
"editsToLocationWillOnlyBeSeenWithinEnte":
MessageLookupByLibrary.simpleMessage(
"Edits to location will only be seen within Ente"),
"enterPersonName":
MessageLookupByLibrary.simpleMessage("Enter person name"),
"enterPin": MessageLookupByLibrary.simpleMessage("Enter PIN"),
"faceRecognition":
MessageLookupByLibrary.simpleMessage("Face recognition"),
"fileTypes": MessageLookupByLibrary.simpleMessage("File types"),
@ -64,6 +67,14 @@ class MessageLookup extends MessageLookupByLibrary {
"Modify your query, or try searching for"),
"moveToHiddenAlbum":
MessageLookupByLibrary.simpleMessage("Move to hidden album"),
"next": MessageLookupByLibrary.simpleMessage("Next"),
"noSystemLockFound":
MessageLookupByLibrary.simpleMessage("No system lock found"),
"passwordLock": MessageLookupByLibrary.simpleMessage("Password lock"),
"pinLock": MessageLookupByLibrary.simpleMessage("PIN lock"),
"reenterPassword":
MessageLookupByLibrary.simpleMessage("Re-enter password"),
"reenterPin": MessageLookupByLibrary.simpleMessage("Re-enter PIN"),
"removePersonLabel":
MessageLookupByLibrary.simpleMessage("Remove person label"),
"search": MessageLookupByLibrary.simpleMessage("Search"),
@ -71,6 +82,15 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Select a location"),
"selectALocationFirst":
MessageLookupByLibrary.simpleMessage("Select a location first"),
"setNewPassword":
MessageLookupByLibrary.simpleMessage("Set new password"),
"setNewPin": MessageLookupByLibrary.simpleMessage("Set new PIN"),
"tapToUnlock": MessageLookupByLibrary.simpleMessage("Tap to unlock"),
"toEnableAppLockPleaseSetupDevicePasscodeOrScreen":
MessageLookupByLibrary.simpleMessage(
"To enable app lock, please setup device passcode or screen lock in your system settings."),
"tooManyIncorrectAttempts":
MessageLookupByLibrary.simpleMessage("Too many incorrect attempts"),
"yourMap": MessageLookupByLibrary.simpleMessage("Your map")
};
}

View File

@ -309,6 +309,7 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Android, iOS, Web, Desktop"),
"androidSignInTitle": MessageLookupByLibrary.simpleMessage(
"Authentifizierung erforderlich"),
"appLock": MessageLookupByLibrary.simpleMessage("App lock"),
"appVersion": m7,
"appleId": MessageLookupByLibrary.simpleMessage("Apple ID"),
"apply": MessageLookupByLibrary.simpleMessage("Anwenden"),
@ -612,6 +613,7 @@ class MessageLookup extends MessageLookupByLibrary {
"deviceCodeHint": MessageLookupByLibrary.simpleMessage("Code eingeben"),
"deviceFilesAutoUploading": MessageLookupByLibrary.simpleMessage(
"Dateien, die zu diesem Album hinzugefügt werden, werden automatisch zu Ente hochgeladen."),
"deviceLock": MessageLookupByLibrary.simpleMessage("Device lock"),
"deviceLockExplanation": MessageLookupByLibrary.simpleMessage(
"Verhindern, dass der Bildschirm gesperrt wird, während die App im Vordergrund ist und eine Sicherung läuft. Das ist normalerweise nicht notwendig, kann aber dabei helfen, große Uploads wie einen Erstimport schneller abzuschließen."),
"deviceNotFound":
@ -706,6 +708,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Gib ein Passwort ein, mit dem wir deine Daten verschlüsseln können"),
"enterPersonName":
MessageLookupByLibrary.simpleMessage("Namen der Person eingeben"),
"enterPin": MessageLookupByLibrary.simpleMessage("Enter PIN"),
"enterReferralCode": MessageLookupByLibrary.simpleMessage(
"Gib den Weiterempfehlungs-Code ein"),
"enterThe6digitCodeFromnyourAuthenticatorApp":
@ -1012,6 +1015,7 @@ class MessageLookup extends MessageLookupByLibrary {
"newAlbum": MessageLookupByLibrary.simpleMessage("Neues Album"),
"newToEnte": MessageLookupByLibrary.simpleMessage("Neu bei Ente"),
"newest": MessageLookupByLibrary.simpleMessage("Zuletzt"),
"next": MessageLookupByLibrary.simpleMessage("Next"),
"no": MessageLookupByLibrary.simpleMessage("Nein"),
"noAlbumsSharedByYouYet": MessageLookupByLibrary.simpleMessage(
"Noch keine Alben von dir geteilt"),
@ -1041,6 +1045,8 @@ class MessageLookup extends MessageLookupByLibrary {
"noResults": MessageLookupByLibrary.simpleMessage("Keine Ergebnisse"),
"noResultsFound":
MessageLookupByLibrary.simpleMessage("Keine Ergebnisse gefunden"),
"noSystemLockFound":
MessageLookupByLibrary.simpleMessage("No system lock found"),
"nothingSharedWithYouYet":
MessageLookupByLibrary.simpleMessage("Noch nichts mit Dir geteilt"),
"nothingToSeeHere": MessageLookupByLibrary.simpleMessage(
@ -1113,6 +1119,7 @@ class MessageLookup extends MessageLookupByLibrary {
"pickCenterPoint":
MessageLookupByLibrary.simpleMessage("Mittelpunkt auswählen"),
"pinAlbum": MessageLookupByLibrary.simpleMessage("Album anheften"),
"pinLock": MessageLookupByLibrary.simpleMessage("PIN lock"),
"playOnTv": MessageLookupByLibrary.simpleMessage(
"Album auf dem Fernseher wiedergeben"),
"playStoreFreeTrialValidTill": m39,
@ -1195,6 +1202,9 @@ class MessageLookup extends MessageLookupByLibrary {
"recreatePasswordTitle":
MessageLookupByLibrary.simpleMessage("Passwort wiederherstellen"),
"reddit": MessageLookupByLibrary.simpleMessage("Reddit"),
"reenterPassword":
MessageLookupByLibrary.simpleMessage("Re-enter password"),
"reenterPin": MessageLookupByLibrary.simpleMessage("Re-enter PIN"),
"referFriendsAnd2xYourPlan": MessageLookupByLibrary.simpleMessage(
"Begeistere Freunde für uns und verdopple deinen Speicher"),
"referralStep1": MessageLookupByLibrary.simpleMessage(
@ -1356,6 +1366,9 @@ class MessageLookup extends MessageLookupByLibrary {
"setAs": MessageLookupByLibrary.simpleMessage("Festlegen als"),
"setCover": MessageLookupByLibrary.simpleMessage("Titelbild festlegen"),
"setLabel": MessageLookupByLibrary.simpleMessage("Festlegen"),
"setNewPassword":
MessageLookupByLibrary.simpleMessage("Set new password"),
"setNewPin": MessageLookupByLibrary.simpleMessage("Set new PIN"),
"setPasswordTitle":
MessageLookupByLibrary.simpleMessage("Passwort festlegen"),
"setRadius": MessageLookupByLibrary.simpleMessage("Radius festlegen"),
@ -1485,6 +1498,7 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("zum Kopieren antippen"),
"tapToEnterCode": MessageLookupByLibrary.simpleMessage(
"Antippen, um den Code einzugeben"),
"tapToUnlock": MessageLookupByLibrary.simpleMessage("Tap to unlock"),
"tempErrorContactSupportIfPersists": MessageLookupByLibrary.simpleMessage(
"Etwas ist schiefgelaufen. Bitte versuche es später noch einmal. Sollte der Fehler weiter bestehen, kontaktiere unser Supportteam."),
"terminate": MessageLookupByLibrary.simpleMessage("Beenden"),
@ -1529,12 +1543,17 @@ class MessageLookup extends MessageLookupByLibrary {
"Dadurch wirst du von folgendem Gerät abgemeldet:"),
"thisWillLogYouOutOfThisDevice": MessageLookupByLibrary.simpleMessage(
"Dadurch wirst du von diesem Gerät abgemeldet!"),
"toEnableAppLockPleaseSetupDevicePasscodeOrScreen":
MessageLookupByLibrary.simpleMessage(
"To enable app lock, please setup device passcode or screen lock in your system settings."),
"toHideAPhotoOrVideo":
MessageLookupByLibrary.simpleMessage("Foto oder Video verstecken"),
"toResetVerifyEmail": MessageLookupByLibrary.simpleMessage(
"Um dein Passwort zurückzusetzen, verifiziere bitte zuerst deine E-Mail Adresse."),
"todaysLogs":
MessageLookupByLibrary.simpleMessage("Heutiges Protokoll"),
"tooManyIncorrectAttempts":
MessageLookupByLibrary.simpleMessage("Too many incorrect attempts"),
"total": MessageLookupByLibrary.simpleMessage("Gesamt"),
"totalSize": MessageLookupByLibrary.simpleMessage("Gesamtgröße"),
"trash": MessageLookupByLibrary.simpleMessage("Papierkorb"),

View File

@ -300,6 +300,7 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Android, iOS, Web, Desktop"),
"androidSignInTitle":
MessageLookupByLibrary.simpleMessage("Authentication required"),
"appLock": MessageLookupByLibrary.simpleMessage("App lock"),
"appVersion": m7,
"appleId": MessageLookupByLibrary.simpleMessage("Apple ID"),
"apply": MessageLookupByLibrary.simpleMessage("Apply"),
@ -596,6 +597,7 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Enter the code"),
"deviceFilesAutoUploading": MessageLookupByLibrary.simpleMessage(
"Files added to this device album will automatically get uploaded to Ente."),
"deviceLock": MessageLookupByLibrary.simpleMessage("Device lock"),
"deviceLockExplanation": MessageLookupByLibrary.simpleMessage(
"Disable the device screen lock when Ente is in the foreground and there is a backup in progress. This is normally not needed, but may help big uploads and initial imports of large libraries complete faster."),
"deviceNotFound":
@ -685,6 +687,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Enter a password we can use to encrypt your data"),
"enterPersonName":
MessageLookupByLibrary.simpleMessage("Enter person name"),
"enterPin": MessageLookupByLibrary.simpleMessage("Enter PIN"),
"enterReferralCode":
MessageLookupByLibrary.simpleMessage("Enter referral code"),
"enterThe6digitCodeFromnyourAuthenticatorApp":
@ -973,6 +976,7 @@ class MessageLookup extends MessageLookupByLibrary {
"newAlbum": MessageLookupByLibrary.simpleMessage("New album"),
"newToEnte": MessageLookupByLibrary.simpleMessage("New to Ente"),
"newest": MessageLookupByLibrary.simpleMessage("Newest"),
"next": MessageLookupByLibrary.simpleMessage("Next"),
"no": MessageLookupByLibrary.simpleMessage("No"),
"noAlbumsSharedByYouYet":
MessageLookupByLibrary.simpleMessage("No albums shared by you yet"),
@ -1001,6 +1005,8 @@ class MessageLookup extends MessageLookupByLibrary {
"noResults": MessageLookupByLibrary.simpleMessage("No results"),
"noResultsFound":
MessageLookupByLibrary.simpleMessage("No results found"),
"noSystemLockFound":
MessageLookupByLibrary.simpleMessage("No system lock found"),
"notPersonLabel": m70,
"nothingSharedWithYouYet":
MessageLookupByLibrary.simpleMessage("Nothing shared with you yet"),
@ -1070,6 +1076,7 @@ class MessageLookup extends MessageLookupByLibrary {
"pickCenterPoint":
MessageLookupByLibrary.simpleMessage("Pick center point"),
"pinAlbum": MessageLookupByLibrary.simpleMessage("Pin album"),
"pinLock": MessageLookupByLibrary.simpleMessage("PIN lock"),
"playOnTv": MessageLookupByLibrary.simpleMessage("Play album on TV"),
"playStoreFreeTrialValidTill": m39,
"playstoreSubscription":
@ -1148,6 +1155,9 @@ class MessageLookup extends MessageLookupByLibrary {
"recreatePasswordTitle":
MessageLookupByLibrary.simpleMessage("Recreate password"),
"reddit": MessageLookupByLibrary.simpleMessage("Reddit"),
"reenterPassword":
MessageLookupByLibrary.simpleMessage("Re-enter password"),
"reenterPin": MessageLookupByLibrary.simpleMessage("Re-enter PIN"),
"referFriendsAnd2xYourPlan": MessageLookupByLibrary.simpleMessage(
"Refer friends and 2x your plan"),
"referralStep1": MessageLookupByLibrary.simpleMessage(
@ -1303,6 +1313,9 @@ class MessageLookup extends MessageLookupByLibrary {
"setAs": MessageLookupByLibrary.simpleMessage("Set as"),
"setCover": MessageLookupByLibrary.simpleMessage("Set cover"),
"setLabel": MessageLookupByLibrary.simpleMessage("Set"),
"setNewPassword":
MessageLookupByLibrary.simpleMessage("Set new password"),
"setNewPin": MessageLookupByLibrary.simpleMessage("Set new PIN"),
"setPasswordTitle":
MessageLookupByLibrary.simpleMessage("Set password"),
"setRadius": MessageLookupByLibrary.simpleMessage("Set radius"),
@ -1424,6 +1437,7 @@ class MessageLookup extends MessageLookupByLibrary {
"tapToCopy": MessageLookupByLibrary.simpleMessage("tap to copy"),
"tapToEnterCode":
MessageLookupByLibrary.simpleMessage("Tap to enter code"),
"tapToUnlock": MessageLookupByLibrary.simpleMessage("Tap to unlock"),
"tempErrorContactSupportIfPersists": MessageLookupByLibrary.simpleMessage(
"It looks like something went wrong. Please retry after some time. If the error persists, please contact our support team."),
"terminate": MessageLookupByLibrary.simpleMessage("Terminate"),
@ -1467,11 +1481,16 @@ class MessageLookup extends MessageLookupByLibrary {
"This will log you out of the following device:"),
"thisWillLogYouOutOfThisDevice": MessageLookupByLibrary.simpleMessage(
"This will log you out of this device!"),
"toEnableAppLockPleaseSetupDevicePasscodeOrScreen":
MessageLookupByLibrary.simpleMessage(
"To enable app lock, please setup device passcode or screen lock in your system settings."),
"toHideAPhotoOrVideo":
MessageLookupByLibrary.simpleMessage("To hide a photo or video"),
"toResetVerifyEmail": MessageLookupByLibrary.simpleMessage(
"To reset your password, please verify your email first."),
"todaysLogs": MessageLookupByLibrary.simpleMessage("Today\'s logs"),
"tooManyIncorrectAttempts":
MessageLookupByLibrary.simpleMessage("Too many incorrect attempts"),
"total": MessageLookupByLibrary.simpleMessage("total"),
"totalSize": MessageLookupByLibrary.simpleMessage("Total size"),
"trash": MessageLookupByLibrary.simpleMessage("Trash"),

View File

@ -310,6 +310,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Android, iOS, Web, Computadora"),
"androidSignInTitle":
MessageLookupByLibrary.simpleMessage("Autentificación requerida"),
"appLock": MessageLookupByLibrary.simpleMessage("App lock"),
"appVersion": m7,
"appleId": MessageLookupByLibrary.simpleMessage("ID de Apple"),
"apply": MessageLookupByLibrary.simpleMessage("Aplicar"),
@ -616,6 +617,7 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Introduce el código"),
"deviceFilesAutoUploading": MessageLookupByLibrary.simpleMessage(
"Los archivos añadidos a este álbum de dispositivo se subirán automáticamente a Ente."),
"deviceLock": MessageLookupByLibrary.simpleMessage("Device lock"),
"deviceLockExplanation": MessageLookupByLibrary.simpleMessage(
"Deshabilita el bloqueo de pantalla del dispositivo cuando Ente está en primer plano y haya una copia de seguridad en curso. Normalmente esto no es necesario, pero puede ayudar a que las grandes cargas y las importaciones iniciales de grandes bibliotecas se completen más rápido."),
"deviceNotFound":
@ -713,6 +715,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Introduce una contraseña que podamos usar para cifrar tus datos"),
"enterPersonName": MessageLookupByLibrary.simpleMessage(
"Ingresar el nombre de una persona"),
"enterPin": MessageLookupByLibrary.simpleMessage("Enter PIN"),
"enterReferralCode": MessageLookupByLibrary.simpleMessage(
"Ingresar código de referencia"),
"enterThe6digitCodeFromnyourAuthenticatorApp":
@ -1021,6 +1024,7 @@ class MessageLookup extends MessageLookupByLibrary {
"newAlbum": MessageLookupByLibrary.simpleMessage("Nuevo álbum"),
"newToEnte": MessageLookupByLibrary.simpleMessage("Nuevo en Ente"),
"newest": MessageLookupByLibrary.simpleMessage("Más reciente"),
"next": MessageLookupByLibrary.simpleMessage("Next"),
"no": MessageLookupByLibrary.simpleMessage("No"),
"noAlbumsSharedByYouYet": MessageLookupByLibrary.simpleMessage(
"Aún no has compartido ningún álbum"),
@ -1050,6 +1054,8 @@ class MessageLookup extends MessageLookupByLibrary {
"noResults": MessageLookupByLibrary.simpleMessage("Sin resultados"),
"noResultsFound": MessageLookupByLibrary.simpleMessage(
"No se han encontrado resultados"),
"noSystemLockFound":
MessageLookupByLibrary.simpleMessage("No system lock found"),
"nothingSharedWithYouYet": MessageLookupByLibrary.simpleMessage(
"Aún no hay nada compartido contigo"),
"nothingToSeeHere": MessageLookupByLibrary.simpleMessage(
@ -1122,6 +1128,7 @@ class MessageLookup extends MessageLookupByLibrary {
"pickCenterPoint":
MessageLookupByLibrary.simpleMessage("Elegir punto central"),
"pinAlbum": MessageLookupByLibrary.simpleMessage("Fijar álbum"),
"pinLock": MessageLookupByLibrary.simpleMessage("PIN lock"),
"playOnTv":
MessageLookupByLibrary.simpleMessage("Reproducir álbum en TV"),
"playStoreFreeTrialValidTill": m39,
@ -1204,6 +1211,9 @@ class MessageLookup extends MessageLookupByLibrary {
"recreatePasswordTitle":
MessageLookupByLibrary.simpleMessage("Recrear contraseña"),
"reddit": MessageLookupByLibrary.simpleMessage("Reddit"),
"reenterPassword":
MessageLookupByLibrary.simpleMessage("Re-enter password"),
"reenterPin": MessageLookupByLibrary.simpleMessage("Re-enter PIN"),
"referFriendsAnd2xYourPlan": MessageLookupByLibrary.simpleMessage(
"Refiere a amigos y 2x su plan"),
"referralStep1": MessageLookupByLibrary.simpleMessage(
@ -1369,6 +1379,9 @@ class MessageLookup extends MessageLookupByLibrary {
"setAs": MessageLookupByLibrary.simpleMessage("Establecer como"),
"setCover": MessageLookupByLibrary.simpleMessage("Definir portada"),
"setLabel": MessageLookupByLibrary.simpleMessage("Establecer"),
"setNewPassword":
MessageLookupByLibrary.simpleMessage("Set new password"),
"setNewPin": MessageLookupByLibrary.simpleMessage("Set new PIN"),
"setPasswordTitle":
MessageLookupByLibrary.simpleMessage("Establecer contraseña"),
"setRadius": MessageLookupByLibrary.simpleMessage("Establecer radio"),
@ -1499,6 +1512,7 @@ class MessageLookup extends MessageLookupByLibrary {
"tapToCopy": MessageLookupByLibrary.simpleMessage("toca para copiar"),
"tapToEnterCode": MessageLookupByLibrary.simpleMessage(
"Toca para introducir el código"),
"tapToUnlock": MessageLookupByLibrary.simpleMessage("Tap to unlock"),
"tempErrorContactSupportIfPersists": MessageLookupByLibrary.simpleMessage(
"Parece que algo salió mal. Por favor, vuelve a intentarlo después de algún tiempo. Si el error persiste, ponte en contacto con nuestro equipo de soporte."),
"terminate": MessageLookupByLibrary.simpleMessage("Terminar"),
@ -1543,11 +1557,16 @@ class MessageLookup extends MessageLookupByLibrary {
"Esto cerrará la sesión del siguiente dispositivo:"),
"thisWillLogYouOutOfThisDevice": MessageLookupByLibrary.simpleMessage(
"¡Esto cerrará la sesión de este dispositivo!"),
"toEnableAppLockPleaseSetupDevicePasscodeOrScreen":
MessageLookupByLibrary.simpleMessage(
"To enable app lock, please setup device passcode or screen lock in your system settings."),
"toHideAPhotoOrVideo": MessageLookupByLibrary.simpleMessage(
"Para ocultar una foto o video"),
"toResetVerifyEmail": MessageLookupByLibrary.simpleMessage(
"Para restablecer tu contraseña, por favor verifica tu correo electrónico primero."),
"todaysLogs": MessageLookupByLibrary.simpleMessage("Registros de hoy"),
"tooManyIncorrectAttempts":
MessageLookupByLibrary.simpleMessage("Too many incorrect attempts"),
"total": MessageLookupByLibrary.simpleMessage("total"),
"totalSize": MessageLookupByLibrary.simpleMessage("Tamaño total"),
"trash": MessageLookupByLibrary.simpleMessage("Papelera"),

View File

@ -298,6 +298,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Android, iOS, Web, Ordinateur"),
"androidSignInTitle":
MessageLookupByLibrary.simpleMessage("Authentification requise"),
"appLock": MessageLookupByLibrary.simpleMessage("App lock"),
"appVersion": m7,
"appleId": MessageLookupByLibrary.simpleMessage("Apple ID"),
"apply": MessageLookupByLibrary.simpleMessage("Appliquer"),
@ -580,6 +581,7 @@ class MessageLookup extends MessageLookupByLibrary {
"details": MessageLookupByLibrary.simpleMessage("Détails"),
"deviceFilesAutoUploading": MessageLookupByLibrary.simpleMessage(
"Les fichiers ajoutés à cet album seront automatiquement téléchargés sur ente."),
"deviceLock": MessageLookupByLibrary.simpleMessage("Device lock"),
"deviceLockExplanation": MessageLookupByLibrary.simpleMessage(
"Désactiver le verrouillage de l\'écran de l\'appareil lorsque ente est au premier plan et il y a une sauvegarde en cours. Ce n\'est normalement pas nécessaire, mais peut aider les gros téléchargements et les premières importations de grandes bibliothèques plus rapidement."),
"didYouKnow": MessageLookupByLibrary.simpleMessage("Le savais-tu ?"),
@ -668,6 +670,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Entrez un mot de passe que nous pouvons utiliser pour chiffrer vos données"),
"enterPersonName":
MessageLookupByLibrary.simpleMessage("Enter person name"),
"enterPin": MessageLookupByLibrary.simpleMessage("Enter PIN"),
"enterReferralCode": MessageLookupByLibrary.simpleMessage(
"Entrez le code de parrainage"),
"enterThe6digitCodeFromnyourAuthenticatorApp":
@ -944,6 +947,7 @@ class MessageLookup extends MessageLookupByLibrary {
"newAlbum": MessageLookupByLibrary.simpleMessage("Nouvel album"),
"newToEnte": MessageLookupByLibrary.simpleMessage("Nouveau sur ente"),
"newest": MessageLookupByLibrary.simpleMessage("Le plus récent"),
"next": MessageLookupByLibrary.simpleMessage("Next"),
"no": MessageLookupByLibrary.simpleMessage("Non"),
"noAlbumsSharedByYouYet": MessageLookupByLibrary.simpleMessage(
"Aucun album que vous avez partagé"),
@ -969,6 +973,8 @@ class MessageLookup extends MessageLookupByLibrary {
"noResults": MessageLookupByLibrary.simpleMessage("Aucun résultat"),
"noResultsFound":
MessageLookupByLibrary.simpleMessage("Aucun résultat trouvé"),
"noSystemLockFound":
MessageLookupByLibrary.simpleMessage("No system lock found"),
"nothingSharedWithYouYet": MessageLookupByLibrary.simpleMessage(
"Rien n\'a encore été partagé avec vous"),
"nothingToSeeHere": MessageLookupByLibrary.simpleMessage(
@ -1028,6 +1034,7 @@ class MessageLookup extends MessageLookupByLibrary {
"pickCenterPoint": MessageLookupByLibrary.simpleMessage(
"Sélectionner le point central"),
"pinAlbum": MessageLookupByLibrary.simpleMessage("Épingler l\'album"),
"pinLock": MessageLookupByLibrary.simpleMessage("PIN lock"),
"playStoreFreeTrialValidTill": m39,
"playstoreSubscription":
MessageLookupByLibrary.simpleMessage("Abonnement au PlayStore"),
@ -1103,6 +1110,9 @@ class MessageLookup extends MessageLookupByLibrary {
"recreatePasswordTitle":
MessageLookupByLibrary.simpleMessage("Recréer le mot de passe"),
"reddit": MessageLookupByLibrary.simpleMessage("Reddit"),
"reenterPassword":
MessageLookupByLibrary.simpleMessage("Re-enter password"),
"reenterPin": MessageLookupByLibrary.simpleMessage("Re-enter PIN"),
"referFriendsAnd2xYourPlan": MessageLookupByLibrary.simpleMessage(
"Parrainez des amis et 2x votre abonnement"),
"referralStep1": MessageLookupByLibrary.simpleMessage(
@ -1263,6 +1273,9 @@ class MessageLookup extends MessageLookupByLibrary {
"setCover":
MessageLookupByLibrary.simpleMessage("Définir la couverture"),
"setLabel": MessageLookupByLibrary.simpleMessage("Définir"),
"setNewPassword":
MessageLookupByLibrary.simpleMessage("Set new password"),
"setNewPin": MessageLookupByLibrary.simpleMessage("Set new PIN"),
"setPasswordTitle":
MessageLookupByLibrary.simpleMessage("Définir le mot de passe"),
"setRadius": MessageLookupByLibrary.simpleMessage("Définir le rayon"),
@ -1382,6 +1395,7 @@ class MessageLookup extends MessageLookupByLibrary {
"tapToCopy": MessageLookupByLibrary.simpleMessage("taper pour copier"),
"tapToEnterCode":
MessageLookupByLibrary.simpleMessage("Appuyez pour entrer le code"),
"tapToUnlock": MessageLookupByLibrary.simpleMessage("Tap to unlock"),
"tempErrorContactSupportIfPersists": MessageLookupByLibrary.simpleMessage(
"Il semble qu\'une erreur s\'est produite. Veuillez réessayer après un certain temps. Si l\'erreur persiste, veuillez contacter notre équipe d\'assistance."),
"terminate": MessageLookupByLibrary.simpleMessage("Se déconnecter"),
@ -1426,11 +1440,16 @@ class MessageLookup extends MessageLookupByLibrary {
"Cela vous déconnectera de l\'appareil suivant :"),
"thisWillLogYouOutOfThisDevice": MessageLookupByLibrary.simpleMessage(
"Cela vous déconnectera de cet appareil !"),
"toEnableAppLockPleaseSetupDevicePasscodeOrScreen":
MessageLookupByLibrary.simpleMessage(
"To enable app lock, please setup device passcode or screen lock in your system settings."),
"toHideAPhotoOrVideo": MessageLookupByLibrary.simpleMessage(
"Cacher une photo ou une vidéo"),
"toResetVerifyEmail": MessageLookupByLibrary.simpleMessage(
"Pour réinitialiser votre mot de passe, veuillez d\'abord vérifier votre e-mail."),
"todaysLogs": MessageLookupByLibrary.simpleMessage("Journaux du jour"),
"tooManyIncorrectAttempts":
MessageLookupByLibrary.simpleMessage("Too many incorrect attempts"),
"total": MessageLookupByLibrary.simpleMessage("total"),
"totalSize": MessageLookupByLibrary.simpleMessage("Taille totale"),
"trash": MessageLookupByLibrary.simpleMessage("Corbeille"),

View File

@ -291,6 +291,7 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Android, iOS, Web, Desktop"),
"androidSignInTitle":
MessageLookupByLibrary.simpleMessage("Autenticazione necessaria"),
"appLock": MessageLookupByLibrary.simpleMessage("App lock"),
"appVersion": m7,
"appleId": MessageLookupByLibrary.simpleMessage("Apple ID"),
"apply": MessageLookupByLibrary.simpleMessage("Applica"),
@ -560,6 +561,7 @@ class MessageLookup extends MessageLookupByLibrary {
"details": MessageLookupByLibrary.simpleMessage("Dettagli"),
"deviceFilesAutoUploading": MessageLookupByLibrary.simpleMessage(
"I file aggiunti in questa cartella del dispositivo verranno automaticamente caricati su ente."),
"deviceLock": MessageLookupByLibrary.simpleMessage("Device lock"),
"deviceLockExplanation": MessageLookupByLibrary.simpleMessage(
"Disabilita il blocco schermo del dispositivo quando ente è in primo piano e c\'è un backup in corso. Questo normalmente non è necessario, ma può aiutare durante grossi caricamenti e le importazioni iniziali di grandi librerie si completano più velocemente."),
"didYouKnow": MessageLookupByLibrary.simpleMessage("Lo sapevi che?"),
@ -647,6 +649,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Inserisci una password per criptare i tuoi dati"),
"enterPersonName":
MessageLookupByLibrary.simpleMessage("Enter person name"),
"enterPin": MessageLookupByLibrary.simpleMessage("Enter PIN"),
"enterReferralCode": MessageLookupByLibrary.simpleMessage(
"Inserisci il codice di invito"),
"enterThe6digitCodeFromnyourAuthenticatorApp":
@ -911,6 +914,7 @@ class MessageLookup extends MessageLookupByLibrary {
"newAlbum": MessageLookupByLibrary.simpleMessage("Nuovo album"),
"newToEnte": MessageLookupByLibrary.simpleMessage("Nuovo utente"),
"newest": MessageLookupByLibrary.simpleMessage("Più recenti"),
"next": MessageLookupByLibrary.simpleMessage("Next"),
"no": MessageLookupByLibrary.simpleMessage("No"),
"noAlbumsSharedByYouYet": MessageLookupByLibrary.simpleMessage(
"Ancora nessun album condiviso da te"),
@ -936,6 +940,8 @@ class MessageLookup extends MessageLookupByLibrary {
"noResults": MessageLookupByLibrary.simpleMessage("Nessun risultato"),
"noResultsFound":
MessageLookupByLibrary.simpleMessage("Nessun risultato trovato"),
"noSystemLockFound":
MessageLookupByLibrary.simpleMessage("No system lock found"),
"nothingSharedWithYouYet": MessageLookupByLibrary.simpleMessage(
"Ancora nulla di condiviso con te"),
"nothingToSeeHere":
@ -992,6 +998,7 @@ class MessageLookup extends MessageLookupByLibrary {
"pickCenterPoint": MessageLookupByLibrary.simpleMessage(
"Selezionare il punto centrale"),
"pinAlbum": MessageLookupByLibrary.simpleMessage("Fissa l\'album"),
"pinLock": MessageLookupByLibrary.simpleMessage("PIN lock"),
"playStoreFreeTrialValidTill": m39,
"playstoreSubscription":
MessageLookupByLibrary.simpleMessage("Abbonamento su PlayStore"),
@ -1066,6 +1073,9 @@ class MessageLookup extends MessageLookupByLibrary {
"recreatePasswordTitle":
MessageLookupByLibrary.simpleMessage("Reimposta password"),
"reddit": MessageLookupByLibrary.simpleMessage("Reddit"),
"reenterPassword":
MessageLookupByLibrary.simpleMessage("Re-enter password"),
"reenterPin": MessageLookupByLibrary.simpleMessage("Re-enter PIN"),
"referFriendsAnd2xYourPlan": MessageLookupByLibrary.simpleMessage(
"Invita un amico e raddoppia il tuo spazio"),
"referralStep1": MessageLookupByLibrary.simpleMessage(
@ -1191,6 +1201,9 @@ class MessageLookup extends MessageLookupByLibrary {
"setAs": MessageLookupByLibrary.simpleMessage("Imposta come"),
"setCover": MessageLookupByLibrary.simpleMessage("Imposta copertina"),
"setLabel": MessageLookupByLibrary.simpleMessage("Imposta"),
"setNewPassword":
MessageLookupByLibrary.simpleMessage("Set new password"),
"setNewPin": MessageLookupByLibrary.simpleMessage("Set new PIN"),
"setPasswordTitle":
MessageLookupByLibrary.simpleMessage("Imposta password"),
"setRadius": MessageLookupByLibrary.simpleMessage("Imposta raggio"),
@ -1310,6 +1323,7 @@ class MessageLookup extends MessageLookupByLibrary {
"tapToCopy": MessageLookupByLibrary.simpleMessage("tocca per copiare"),
"tapToEnterCode": MessageLookupByLibrary.simpleMessage(
"Tocca per inserire il codice"),
"tapToUnlock": MessageLookupByLibrary.simpleMessage("Tap to unlock"),
"tempErrorContactSupportIfPersists": MessageLookupByLibrary.simpleMessage(
"Sembra che qualcosa sia andato storto. Riprova tra un po\'. Se l\'errore persiste, contatta il nostro team di supporto."),
"terminate": MessageLookupByLibrary.simpleMessage("Terminata"),
@ -1355,11 +1369,16 @@ class MessageLookup extends MessageLookupByLibrary {
"Verrai disconnesso dai seguenti dispositivi:"),
"thisWillLogYouOutOfThisDevice": MessageLookupByLibrary.simpleMessage(
"Verrai disconnesso dal tuo dispositivo!"),
"toEnableAppLockPleaseSetupDevicePasscodeOrScreen":
MessageLookupByLibrary.simpleMessage(
"To enable app lock, please setup device passcode or screen lock in your system settings."),
"toHideAPhotoOrVideo": MessageLookupByLibrary.simpleMessage(
"Per nascondere una foto o un video"),
"toResetVerifyEmail": MessageLookupByLibrary.simpleMessage(
"Per reimpostare la tua password, verifica prima la tua email."),
"todaysLogs": MessageLookupByLibrary.simpleMessage("Log di oggi"),
"tooManyIncorrectAttempts":
MessageLookupByLibrary.simpleMessage("Too many incorrect attempts"),
"total": MessageLookupByLibrary.simpleMessage("totale"),
"totalSize": MessageLookupByLibrary.simpleMessage("Dimensioni totali"),
"trash": MessageLookupByLibrary.simpleMessage("Cestino"),

View File

@ -32,6 +32,7 @@ class MessageLookup extends MessageLookupByLibrary {
"addToHiddenAlbum":
MessageLookupByLibrary.simpleMessage("Add to hidden album"),
"addViewers": m1,
"appLock": MessageLookupByLibrary.simpleMessage("App lock"),
"changeLocationOfSelectedItems": MessageLookupByLibrary.simpleMessage(
"Change location of selected items?"),
"clusteringProgress":
@ -42,12 +43,14 @@ class MessageLookup extends MessageLookupByLibrary {
"deleteConfirmDialogBody": MessageLookupByLibrary.simpleMessage(
"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."),
"descriptions": MessageLookupByLibrary.simpleMessage("Descriptions"),
"deviceLock": MessageLookupByLibrary.simpleMessage("Device lock"),
"editLocation": MessageLookupByLibrary.simpleMessage("Edit location"),
"editsToLocationWillOnlyBeSeenWithinEnte":
MessageLookupByLibrary.simpleMessage(
"Edits to location will only be seen within Ente"),
"enterPersonName":
MessageLookupByLibrary.simpleMessage("Enter person name"),
"enterPin": MessageLookupByLibrary.simpleMessage("Enter PIN"),
"faceRecognition":
MessageLookupByLibrary.simpleMessage("Face recognition"),
"fileTypes": MessageLookupByLibrary.simpleMessage("File types"),
@ -64,6 +67,14 @@ class MessageLookup extends MessageLookupByLibrary {
"Modify your query, or try searching for"),
"moveToHiddenAlbum":
MessageLookupByLibrary.simpleMessage("Move to hidden album"),
"next": MessageLookupByLibrary.simpleMessage("Next"),
"noSystemLockFound":
MessageLookupByLibrary.simpleMessage("No system lock found"),
"passwordLock": MessageLookupByLibrary.simpleMessage("Password lock"),
"pinLock": MessageLookupByLibrary.simpleMessage("PIN lock"),
"reenterPassword":
MessageLookupByLibrary.simpleMessage("Re-enter password"),
"reenterPin": MessageLookupByLibrary.simpleMessage("Re-enter PIN"),
"removePersonLabel":
MessageLookupByLibrary.simpleMessage("Remove person label"),
"search": MessageLookupByLibrary.simpleMessage("Search"),
@ -71,6 +82,15 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Select a location"),
"selectALocationFirst":
MessageLookupByLibrary.simpleMessage("Select a location first"),
"setNewPassword":
MessageLookupByLibrary.simpleMessage("Set new password"),
"setNewPin": MessageLookupByLibrary.simpleMessage("Set new PIN"),
"tapToUnlock": MessageLookupByLibrary.simpleMessage("Tap to unlock"),
"toEnableAppLockPleaseSetupDevicePasscodeOrScreen":
MessageLookupByLibrary.simpleMessage(
"To enable app lock, please setup device passcode or screen lock in your system settings."),
"tooManyIncorrectAttempts":
MessageLookupByLibrary.simpleMessage("Too many incorrect attempts"),
"yourMap": MessageLookupByLibrary.simpleMessage("Your map")
};
}

View File

@ -307,6 +307,7 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Android, iOS, Web, Desktop"),
"androidSignInTitle":
MessageLookupByLibrary.simpleMessage("Verificatie vereist"),
"appLock": MessageLookupByLibrary.simpleMessage("App lock"),
"appVersion": m7,
"appleId": MessageLookupByLibrary.simpleMessage("Apple ID"),
"apply": MessageLookupByLibrary.simpleMessage("Toepassen"),
@ -607,6 +608,7 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Voer de code in"),
"deviceFilesAutoUploading": MessageLookupByLibrary.simpleMessage(
"Bestanden toegevoegd aan dit album van dit apparaat zullen automatisch geüpload worden naar Ente."),
"deviceLock": MessageLookupByLibrary.simpleMessage("Device lock"),
"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."),
"deviceNotFound":
@ -700,6 +702,7 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Voer wachtwoord in"),
"enterPasswordToEncrypt": MessageLookupByLibrary.simpleMessage(
"Voer een wachtwoord in dat we kunnen gebruiken om je gegevens te versleutelen"),
"enterPin": MessageLookupByLibrary.simpleMessage("Enter PIN"),
"enterReferralCode":
MessageLookupByLibrary.simpleMessage("Voer verwijzingscode in"),
"enterThe6digitCodeFromnyourAuthenticatorApp":
@ -992,6 +995,7 @@ class MessageLookup extends MessageLookupByLibrary {
"newAlbum": MessageLookupByLibrary.simpleMessage("Nieuw album"),
"newToEnte": MessageLookupByLibrary.simpleMessage("Nieuw bij Ente"),
"newest": MessageLookupByLibrary.simpleMessage("Nieuwste"),
"next": MessageLookupByLibrary.simpleMessage("Next"),
"no": MessageLookupByLibrary.simpleMessage("Nee"),
"noAlbumsSharedByYouYet": MessageLookupByLibrary.simpleMessage(
"Nog geen albums gedeeld door jou"),
@ -1022,6 +1026,8 @@ class MessageLookup extends MessageLookupByLibrary {
"noResults": MessageLookupByLibrary.simpleMessage("Geen resultaten"),
"noResultsFound":
MessageLookupByLibrary.simpleMessage("Geen resultaten gevonden"),
"noSystemLockFound":
MessageLookupByLibrary.simpleMessage("No system lock found"),
"nothingSharedWithYouYet":
MessageLookupByLibrary.simpleMessage("Nog niets met je gedeeld"),
"nothingToSeeHere":
@ -1091,6 +1097,7 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Kies middelpunt"),
"pinAlbum":
MessageLookupByLibrary.simpleMessage("Album bovenaan vastzetten"),
"pinLock": MessageLookupByLibrary.simpleMessage("PIN lock"),
"playOnTv":
MessageLookupByLibrary.simpleMessage("Album afspelen op TV"),
"playStoreFreeTrialValidTill": m39,
@ -1170,6 +1177,9 @@ class MessageLookup extends MessageLookupByLibrary {
"recreatePasswordTitle": MessageLookupByLibrary.simpleMessage(
"Wachtwoord opnieuw instellen"),
"reddit": MessageLookupByLibrary.simpleMessage("Reddit"),
"reenterPassword":
MessageLookupByLibrary.simpleMessage("Re-enter password"),
"reenterPin": MessageLookupByLibrary.simpleMessage("Re-enter PIN"),
"referFriendsAnd2xYourPlan": MessageLookupByLibrary.simpleMessage(
"Verwijs vrienden en 2x uw abonnement"),
"referralStep1": MessageLookupByLibrary.simpleMessage(
@ -1324,6 +1334,9 @@ class MessageLookup extends MessageLookupByLibrary {
"setAs": MessageLookupByLibrary.simpleMessage("Instellen als"),
"setCover": MessageLookupByLibrary.simpleMessage("Omslag instellen"),
"setLabel": MessageLookupByLibrary.simpleMessage("Instellen"),
"setNewPassword":
MessageLookupByLibrary.simpleMessage("Set new password"),
"setNewPin": MessageLookupByLibrary.simpleMessage("Set new PIN"),
"setPasswordTitle":
MessageLookupByLibrary.simpleMessage("Wachtwoord instellen"),
"setRadius": MessageLookupByLibrary.simpleMessage("Radius instellen"),
@ -1447,6 +1460,7 @@ class MessageLookup extends MessageLookupByLibrary {
"tapToCopy": MessageLookupByLibrary.simpleMessage("tik om te kopiëren"),
"tapToEnterCode":
MessageLookupByLibrary.simpleMessage("Tik om code in te voeren"),
"tapToUnlock": MessageLookupByLibrary.simpleMessage("Tap to unlock"),
"tempErrorContactSupportIfPersists": 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."),
"terminate": MessageLookupByLibrary.simpleMessage("Beëindigen"),
@ -1491,12 +1505,17 @@ class MessageLookup extends MessageLookupByLibrary {
"Dit zal je uitloggen van het volgende apparaat:"),
"thisWillLogYouOutOfThisDevice": MessageLookupByLibrary.simpleMessage(
"Dit zal je uitloggen van dit apparaat!"),
"toEnableAppLockPleaseSetupDevicePasscodeOrScreen":
MessageLookupByLibrary.simpleMessage(
"To enable app lock, please setup device passcode or screen lock in your system settings."),
"toHideAPhotoOrVideo": MessageLookupByLibrary.simpleMessage(
"Om een foto of video te verbergen"),
"toResetVerifyEmail": MessageLookupByLibrary.simpleMessage(
"Verifieer eerst je e-mailadres om je wachtwoord opnieuw in te stellen."),
"todaysLogs":
MessageLookupByLibrary.simpleMessage("Logboeken van vandaag"),
"tooManyIncorrectAttempts":
MessageLookupByLibrary.simpleMessage("Too many incorrect attempts"),
"total": MessageLookupByLibrary.simpleMessage("totaal"),
"totalSize": MessageLookupByLibrary.simpleMessage("Totale grootte"),
"trash": MessageLookupByLibrary.simpleMessage("Prullenbak"),

View File

@ -34,6 +34,7 @@ class MessageLookup extends MessageLookupByLibrary {
"addToHiddenAlbum":
MessageLookupByLibrary.simpleMessage("Add to hidden album"),
"addViewers": m1,
"appLock": MessageLookupByLibrary.simpleMessage("App lock"),
"askDeleteReason": MessageLookupByLibrary.simpleMessage(
"Hva er hovedårsaken til at du sletter kontoen din?"),
"cancel": MessageLookupByLibrary.simpleMessage("Avbryt"),
@ -54,6 +55,7 @@ class MessageLookup extends MessageLookupByLibrary {
"deleteConfirmDialogBody": MessageLookupByLibrary.simpleMessage(
"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."),
"descriptions": MessageLookupByLibrary.simpleMessage("Descriptions"),
"deviceLock": MessageLookupByLibrary.simpleMessage("Device lock"),
"editLocation": MessageLookupByLibrary.simpleMessage("Edit location"),
"editsToLocationWillOnlyBeSeenWithinEnte":
MessageLookupByLibrary.simpleMessage(
@ -61,6 +63,7 @@ class MessageLookup extends MessageLookupByLibrary {
"email": MessageLookupByLibrary.simpleMessage("E-post"),
"enterPersonName":
MessageLookupByLibrary.simpleMessage("Enter person name"),
"enterPin": MessageLookupByLibrary.simpleMessage("Enter PIN"),
"enterValidEmail": MessageLookupByLibrary.simpleMessage(
"Vennligst skriv inn en gyldig e-postadresse."),
"enterYourEmailAddress": MessageLookupByLibrary.simpleMessage(
@ -86,6 +89,14 @@ class MessageLookup extends MessageLookupByLibrary {
"Modify your query, or try searching for"),
"moveToHiddenAlbum":
MessageLookupByLibrary.simpleMessage("Move to hidden album"),
"next": MessageLookupByLibrary.simpleMessage("Next"),
"noSystemLockFound":
MessageLookupByLibrary.simpleMessage("No system lock found"),
"passwordLock": MessageLookupByLibrary.simpleMessage("Password lock"),
"pinLock": MessageLookupByLibrary.simpleMessage("PIN lock"),
"reenterPassword":
MessageLookupByLibrary.simpleMessage("Re-enter password"),
"reenterPin": MessageLookupByLibrary.simpleMessage("Re-enter PIN"),
"removePersonLabel":
MessageLookupByLibrary.simpleMessage("Remove person label"),
"search": MessageLookupByLibrary.simpleMessage("Search"),
@ -93,6 +104,15 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Select a location"),
"selectALocationFirst":
MessageLookupByLibrary.simpleMessage("Select a location first"),
"setNewPassword":
MessageLookupByLibrary.simpleMessage("Set new password"),
"setNewPin": MessageLookupByLibrary.simpleMessage("Set new PIN"),
"tapToUnlock": MessageLookupByLibrary.simpleMessage("Tap to unlock"),
"toEnableAppLockPleaseSetupDevicePasscodeOrScreen":
MessageLookupByLibrary.simpleMessage(
"To enable app lock, please setup device passcode or screen lock in your system settings."),
"tooManyIncorrectAttempts":
MessageLookupByLibrary.simpleMessage("Too many incorrect attempts"),
"verify": MessageLookupByLibrary.simpleMessage("Bekreft"),
"yourMap": MessageLookupByLibrary.simpleMessage("Your map")
};

View File

@ -38,6 +38,7 @@ class MessageLookup extends MessageLookupByLibrary {
"addToHiddenAlbum":
MessageLookupByLibrary.simpleMessage("Add to hidden album"),
"addViewers": m1,
"appLock": MessageLookupByLibrary.simpleMessage("App lock"),
"askDeleteReason": MessageLookupByLibrary.simpleMessage(
"Jaka jest przyczyna usunięcia konta?"),
"cancel": MessageLookupByLibrary.simpleMessage("Anuluj"),
@ -91,6 +92,7 @@ class MessageLookup extends MessageLookupByLibrary {
"deleteRequestSLAText": MessageLookupByLibrary.simpleMessage(
"Twoje żądanie zostanie przetworzone w ciągu 72 godzin."),
"descriptions": MessageLookupByLibrary.simpleMessage("Descriptions"),
"deviceLock": MessageLookupByLibrary.simpleMessage("Device lock"),
"doThisLater": MessageLookupByLibrary.simpleMessage("Spróbuj później"),
"editLocation": MessageLookupByLibrary.simpleMessage("Edit location"),
"editsToLocationWillOnlyBeSeenWithinEnte":
@ -105,6 +107,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Wprowadź hasło, którego możemy użyć do zaszyfrowania Twoich danych"),
"enterPersonName":
MessageLookupByLibrary.simpleMessage("Enter person name"),
"enterPin": MessageLookupByLibrary.simpleMessage("Enter PIN"),
"enterValidEmail": MessageLookupByLibrary.simpleMessage(
"Podaj poprawny adres e-mail."),
"enterYourEmailAddress":
@ -147,18 +150,23 @@ class MessageLookup extends MessageLookupByLibrary {
"Modify your query, or try searching for"),
"moveToHiddenAlbum":
MessageLookupByLibrary.simpleMessage("Move to hidden album"),
"next": MessageLookupByLibrary.simpleMessage("Next"),
"noRecoveryKey":
MessageLookupByLibrary.simpleMessage("Brak klucza odzyskiwania?"),
"noRecoveryKeyNoDecryption": MessageLookupByLibrary.simpleMessage(
"Ze względu na charakter naszego protokołu szyfrowania end-to-end, dane nie mogą być odszyfrowane bez hasła lub klucza odzyskiwania"),
"noSystemLockFound":
MessageLookupByLibrary.simpleMessage("No system lock found"),
"ok": MessageLookupByLibrary.simpleMessage("Ok"),
"oops": MessageLookupByLibrary.simpleMessage("Ups"),
"password": MessageLookupByLibrary.simpleMessage("Hasło"),
"passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage(
"Hasło zostało pomyślnie zmienione"),
"passwordLock": MessageLookupByLibrary.simpleMessage("Password lock"),
"passwordStrength": m37,
"passwordWarning": MessageLookupByLibrary.simpleMessage(
"Nie przechowujemy tego hasła, więc jeśli go zapomnisz, <underline>nie będziemy w stanie odszyfrować Twoich danych</underline>"),
"pinLock": MessageLookupByLibrary.simpleMessage("PIN lock"),
"pleaseTryAgain":
MessageLookupByLibrary.simpleMessage("Spróbuj ponownie"),
"pleaseWait": MessageLookupByLibrary.simpleMessage("Proszę czekać..."),
@ -175,6 +183,9 @@ class MessageLookup extends MessageLookupByLibrary {
"Jeśli zapomnisz hasła, jedynym sposobem odzyskania danych jest ten klucz."),
"recoverySuccessful":
MessageLookupByLibrary.simpleMessage("Odzyskano pomyślnie!"),
"reenterPassword":
MessageLookupByLibrary.simpleMessage("Re-enter password"),
"reenterPin": MessageLookupByLibrary.simpleMessage("Re-enter PIN"),
"removePersonLabel":
MessageLookupByLibrary.simpleMessage("Remove person label"),
"resendEmail":
@ -189,6 +200,9 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Select a location first"),
"selectReason": MessageLookupByLibrary.simpleMessage("Wybierz powód"),
"sendEmail": MessageLookupByLibrary.simpleMessage("Wyślij e-mail"),
"setNewPassword":
MessageLookupByLibrary.simpleMessage("Set new password"),
"setNewPin": MessageLookupByLibrary.simpleMessage("Set new PIN"),
"setPasswordTitle": MessageLookupByLibrary.simpleMessage("Ustaw hasło"),
"signUpTerms": MessageLookupByLibrary.simpleMessage(
"Akceptuję <u-terms>warunki korzystania z usługi</u-terms> i <u-policy>politykę prywatności</u-policy>"),
@ -197,6 +211,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Coś poszło nie tak, spróbuj ponownie"),
"sorry": MessageLookupByLibrary.simpleMessage("Przepraszamy"),
"strongStrength": MessageLookupByLibrary.simpleMessage("Silne"),
"tapToUnlock": MessageLookupByLibrary.simpleMessage("Tap to unlock"),
"terminate": MessageLookupByLibrary.simpleMessage("Zakończ"),
"terminateSession":
MessageLookupByLibrary.simpleMessage("Zakończyć sesję?"),
@ -208,6 +223,11 @@ class MessageLookup extends MessageLookupByLibrary {
"To wyloguje Cię z tego urządzenia:"),
"thisWillLogYouOutOfThisDevice": MessageLookupByLibrary.simpleMessage(
"To wyloguje Cię z tego urządzenia!"),
"toEnableAppLockPleaseSetupDevicePasscodeOrScreen":
MessageLookupByLibrary.simpleMessage(
"To enable app lock, please setup device passcode or screen lock in your system settings."),
"tooManyIncorrectAttempts":
MessageLookupByLibrary.simpleMessage("Too many incorrect attempts"),
"tryAgain": MessageLookupByLibrary.simpleMessage("Spróbuj ponownie"),
"twofactorAuthenticationPageTitle":
MessageLookupByLibrary.simpleMessage(

View File

@ -306,6 +306,7 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Android, iOS, Web, Desktop"),
"androidSignInTitle":
MessageLookupByLibrary.simpleMessage("Autenticação necessária"),
"appLock": MessageLookupByLibrary.simpleMessage("App lock"),
"appVersion": m7,
"appleId": MessageLookupByLibrary.simpleMessage("ID da Apple"),
"apply": MessageLookupByLibrary.simpleMessage("Aplicar"),
@ -606,6 +607,7 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Insira o código"),
"deviceFilesAutoUploading": MessageLookupByLibrary.simpleMessage(
"Arquivos adicionados a este álbum do dispositivo serão automaticamente enviados para o Ente."),
"deviceLock": MessageLookupByLibrary.simpleMessage("Device lock"),
"deviceLockExplanation": MessageLookupByLibrary.simpleMessage(
"Desative o bloqueio de tela do dispositivo quando o Ente estiver em primeiro plano e houver um backup em andamento. Isso normalmente não é necessário, mas pode ajudar nos envios grandes e importações iniciais de grandes bibliotecas a serem concluídos mais rapidamente."),
"deviceNotFound":
@ -699,6 +701,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Insira a senha para criptografar seus dados"),
"enterPersonName":
MessageLookupByLibrary.simpleMessage("Inserir nome da pessoa"),
"enterPin": MessageLookupByLibrary.simpleMessage("Enter PIN"),
"enterReferralCode": MessageLookupByLibrary.simpleMessage(
"Insira o código de referência"),
"enterThe6digitCodeFromnyourAuthenticatorApp":
@ -1003,6 +1006,7 @@ class MessageLookup extends MessageLookupByLibrary {
"newAlbum": MessageLookupByLibrary.simpleMessage("Novo álbum"),
"newToEnte": MessageLookupByLibrary.simpleMessage("Novo no Ente"),
"newest": MessageLookupByLibrary.simpleMessage("Mais recente"),
"next": MessageLookupByLibrary.simpleMessage("Next"),
"no": MessageLookupByLibrary.simpleMessage("Não"),
"noAlbumsSharedByYouYet": MessageLookupByLibrary.simpleMessage(
"Nenhum álbum compartilhado por você ainda"),
@ -1032,6 +1036,8 @@ class MessageLookup extends MessageLookupByLibrary {
"noResults": MessageLookupByLibrary.simpleMessage("Nenhum resultado"),
"noResultsFound":
MessageLookupByLibrary.simpleMessage("Nenhum resultado encontrado"),
"noSystemLockFound":
MessageLookupByLibrary.simpleMessage("No system lock found"),
"nothingSharedWithYouYet": MessageLookupByLibrary.simpleMessage(
"Nada compartilhado com você ainda"),
"nothingToSeeHere":
@ -1103,6 +1109,7 @@ class MessageLookup extends MessageLookupByLibrary {
"pickCenterPoint":
MessageLookupByLibrary.simpleMessage("Escolha o ponto central"),
"pinAlbum": MessageLookupByLibrary.simpleMessage("Fixar álbum"),
"pinLock": MessageLookupByLibrary.simpleMessage("PIN lock"),
"playOnTv":
MessageLookupByLibrary.simpleMessage("Reproduzir álbum na TV"),
"playStoreFreeTrialValidTill": m39,
@ -1185,6 +1192,9 @@ class MessageLookup extends MessageLookupByLibrary {
"recreatePasswordTitle":
MessageLookupByLibrary.simpleMessage("Redefinir senha"),
"reddit": MessageLookupByLibrary.simpleMessage("Reddit"),
"reenterPassword":
MessageLookupByLibrary.simpleMessage("Re-enter password"),
"reenterPin": MessageLookupByLibrary.simpleMessage("Re-enter PIN"),
"referFriendsAnd2xYourPlan": MessageLookupByLibrary.simpleMessage(
"Indique amigos e 2x seu plano"),
"referralStep1": MessageLookupByLibrary.simpleMessage(
@ -1348,6 +1358,9 @@ class MessageLookup extends MessageLookupByLibrary {
"setAs": MessageLookupByLibrary.simpleMessage("Definir como"),
"setCover": MessageLookupByLibrary.simpleMessage("Definir capa"),
"setLabel": MessageLookupByLibrary.simpleMessage("Aplicar"),
"setNewPassword":
MessageLookupByLibrary.simpleMessage("Set new password"),
"setNewPin": MessageLookupByLibrary.simpleMessage("Set new PIN"),
"setPasswordTitle":
MessageLookupByLibrary.simpleMessage("Definir senha"),
"setRadius": MessageLookupByLibrary.simpleMessage("Definir raio"),
@ -1478,6 +1491,7 @@ class MessageLookup extends MessageLookupByLibrary {
"tapToCopy": MessageLookupByLibrary.simpleMessage("toque para copiar"),
"tapToEnterCode":
MessageLookupByLibrary.simpleMessage("Toque para inserir código"),
"tapToUnlock": MessageLookupByLibrary.simpleMessage("Tap to unlock"),
"tempErrorContactSupportIfPersists": MessageLookupByLibrary.simpleMessage(
"Parece que algo deu errado. Por favor, tente novamente mais tarde. Se o erro persistir, entre em contato com nossa equipe de suporte."),
"terminate": MessageLookupByLibrary.simpleMessage("Encerrar"),
@ -1521,11 +1535,16 @@ class MessageLookup extends MessageLookupByLibrary {
"Isso fará com que você saia do seguinte dispositivo:"),
"thisWillLogYouOutOfThisDevice": MessageLookupByLibrary.simpleMessage(
"Isso fará com que você saia deste dispositivo!"),
"toEnableAppLockPleaseSetupDevicePasscodeOrScreen":
MessageLookupByLibrary.simpleMessage(
"To enable app lock, please setup device passcode or screen lock in your system settings."),
"toHideAPhotoOrVideo": MessageLookupByLibrary.simpleMessage(
"Para ocultar uma foto ou vídeo"),
"toResetVerifyEmail": MessageLookupByLibrary.simpleMessage(
"Para redefinir a sua senha, por favor verifique o seu email primeiro."),
"todaysLogs": MessageLookupByLibrary.simpleMessage("Logs de hoje"),
"tooManyIncorrectAttempts":
MessageLookupByLibrary.simpleMessage("Too many incorrect attempts"),
"total": MessageLookupByLibrary.simpleMessage("total"),
"totalSize": MessageLookupByLibrary.simpleMessage("Tamanho total"),
"trash": MessageLookupByLibrary.simpleMessage("Lixeira"),

View File

@ -304,6 +304,7 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Android, iOS, Web, ПК"),
"androidSignInTitle":
MessageLookupByLibrary.simpleMessage("Требуется аутентификация"),
"appLock": MessageLookupByLibrary.simpleMessage("App lock"),
"appVersion": m7,
"appleId": MessageLookupByLibrary.simpleMessage("Apple ID"),
"apply": MessageLookupByLibrary.simpleMessage("Применить"),
@ -609,6 +610,7 @@ class MessageLookup extends MessageLookupByLibrary {
"deviceCodeHint": MessageLookupByLibrary.simpleMessage("Введите код"),
"deviceFilesAutoUploading": MessageLookupByLibrary.simpleMessage(
"Файлы, добавленные в этот альбом на устройстве, будут автоматически загружены в Ente."),
"deviceLock": MessageLookupByLibrary.simpleMessage("Device lock"),
"deviceLockExplanation": MessageLookupByLibrary.simpleMessage(
"Отключить блокировку экрана, когда Ente находится на переднем плане и выполняется резервное копирование. Обычно это не нужно, но это может ускорить загрузку и первоначальный импорт больших библиотек."),
"deviceNotFound":
@ -700,6 +702,7 @@ class MessageLookup extends MessageLookupByLibrary {
"enterPasswordToEncrypt": MessageLookupByLibrary.simpleMessage(
"Введите пароль, который мы можем использовать для шифрования ваших данных"),
"enterPersonName": MessageLookupByLibrary.simpleMessage("Введите имя"),
"enterPin": MessageLookupByLibrary.simpleMessage("Enter PIN"),
"enterReferralCode":
MessageLookupByLibrary.simpleMessage("Введите реферальный код"),
"enterThe6digitCodeFromnyourAuthenticatorApp":
@ -1005,6 +1008,7 @@ class MessageLookup extends MessageLookupByLibrary {
"newAlbum": MessageLookupByLibrary.simpleMessage("Новый альбом"),
"newToEnte": MessageLookupByLibrary.simpleMessage("Впервые в Ente"),
"newest": MessageLookupByLibrary.simpleMessage("Самые новые"),
"next": MessageLookupByLibrary.simpleMessage("Next"),
"no": MessageLookupByLibrary.simpleMessage("Нет"),
"noAlbumsSharedByYouYet":
MessageLookupByLibrary.simpleMessage("У вас пока нет альбомов"),
@ -1035,6 +1039,8 @@ class MessageLookup extends MessageLookupByLibrary {
"noResults": MessageLookupByLibrary.simpleMessage("Ничего не найденo"),
"noResultsFound":
MessageLookupByLibrary.simpleMessage("Ничего не найдено"),
"noSystemLockFound":
MessageLookupByLibrary.simpleMessage("No system lock found"),
"nothingSharedWithYouYet": MessageLookupByLibrary.simpleMessage(
"Пока никто не поделился с вами"),
"nothingToSeeHere":
@ -1107,6 +1113,7 @@ class MessageLookup extends MessageLookupByLibrary {
"pickCenterPoint":
MessageLookupByLibrary.simpleMessage("Указать центральную точку"),
"pinAlbum": MessageLookupByLibrary.simpleMessage("Закрепить альбом"),
"pinLock": MessageLookupByLibrary.simpleMessage("PIN lock"),
"playOnTv":
MessageLookupByLibrary.simpleMessage("Воспроизвести альбом на ТВ"),
"playStoreFreeTrialValidTill": m39,
@ -1189,6 +1196,9 @@ class MessageLookup extends MessageLookupByLibrary {
"recreatePasswordTitle":
MessageLookupByLibrary.simpleMessage("Сбросить пароль"),
"reddit": MessageLookupByLibrary.simpleMessage("Reddit"),
"reenterPassword":
MessageLookupByLibrary.simpleMessage("Re-enter password"),
"reenterPin": MessageLookupByLibrary.simpleMessage("Re-enter PIN"),
"referFriendsAnd2xYourPlan": MessageLookupByLibrary.simpleMessage(
"Пригласите друзей и удвойте свой план"),
"referralStep1": MessageLookupByLibrary.simpleMessage(
@ -1351,6 +1361,9 @@ class MessageLookup extends MessageLookupByLibrary {
"setAs": MessageLookupByLibrary.simpleMessage("Установить как"),
"setCover": MessageLookupByLibrary.simpleMessage("Установить обложку"),
"setLabel": MessageLookupByLibrary.simpleMessage("Установить"),
"setNewPassword":
MessageLookupByLibrary.simpleMessage("Set new password"),
"setNewPin": MessageLookupByLibrary.simpleMessage("Set new PIN"),
"setPasswordTitle":
MessageLookupByLibrary.simpleMessage("Установить пароль"),
"setRadius": MessageLookupByLibrary.simpleMessage("Установить радиус"),
@ -1480,6 +1493,7 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("нажмите, чтобы скопировать"),
"tapToEnterCode":
MessageLookupByLibrary.simpleMessage("Нажмите, чтобы ввести код"),
"tapToUnlock": MessageLookupByLibrary.simpleMessage("Tap to unlock"),
"tempErrorContactSupportIfPersists": MessageLookupByLibrary.simpleMessage(
"Похоже, что-то пошло не так. Пожалуйста, повторите попытку через некоторое время. Если ошибка повторится, обратитесь в нашу службу поддержки."),
"terminate": MessageLookupByLibrary.simpleMessage("Завершить"),
@ -1524,11 +1538,16 @@ class MessageLookup extends MessageLookupByLibrary {
"Вы выйдете из списка следующих устройств:"),
"thisWillLogYouOutOfThisDevice": MessageLookupByLibrary.simpleMessage(
"Совершив это действие, Вы выйдете из своей учетной записи!"),
"toEnableAppLockPleaseSetupDevicePasscodeOrScreen":
MessageLookupByLibrary.simpleMessage(
"To enable app lock, please setup device passcode or screen lock in your system settings."),
"toHideAPhotoOrVideo":
MessageLookupByLibrary.simpleMessage("Скрыть фото или видео"),
"toResetVerifyEmail": MessageLookupByLibrary.simpleMessage(
"Чтобы сбросить пароль, сначала подтвердите свой адрес электронной почты."),
"todaysLogs": MessageLookupByLibrary.simpleMessage("Сегодняшние логи"),
"tooManyIncorrectAttempts":
MessageLookupByLibrary.simpleMessage("Too many incorrect attempts"),
"total": MessageLookupByLibrary.simpleMessage("всего"),
"totalSize": MessageLookupByLibrary.simpleMessage("Общий размер"),
"trash": MessageLookupByLibrary.simpleMessage("Корзина"),

View File

@ -268,6 +268,7 @@ class MessageLookup extends MessageLookupByLibrary {
"androidIosWebDesktop":
MessageLookupByLibrary.simpleMessage("安卓, iOS, 网页端, 桌面端"),
"androidSignInTitle": MessageLookupByLibrary.simpleMessage("需要身份验证"),
"appLock": MessageLookupByLibrary.simpleMessage("App lock"),
"appVersion": m7,
"appleId": MessageLookupByLibrary.simpleMessage("Apple ID"),
"apply": MessageLookupByLibrary.simpleMessage("应用"),
@ -506,6 +507,7 @@ class MessageLookup extends MessageLookupByLibrary {
"deviceCodeHint": MessageLookupByLibrary.simpleMessage("输入代码"),
"deviceFilesAutoUploading":
MessageLookupByLibrary.simpleMessage("添加到此设备相册的文件将自动上传到 Ente。"),
"deviceLock": MessageLookupByLibrary.simpleMessage("Device lock"),
"deviceLockExplanation": MessageLookupByLibrary.simpleMessage(
"当 Ente 置于前台且正在进行备份时将禁用设备屏幕锁定。这通常是不需要的,但可能有助于更快地完成大型上传和大型库的初始导入。"),
"deviceNotFound": MessageLookupByLibrary.simpleMessage("未发现设备"),
@ -580,6 +582,7 @@ class MessageLookup extends MessageLookupByLibrary {
"enterPasswordToEncrypt":
MessageLookupByLibrary.simpleMessage("输入我们可以用来加密您的数据的密码"),
"enterPersonName": MessageLookupByLibrary.simpleMessage("输入人物名称"),
"enterPin": MessageLookupByLibrary.simpleMessage("Enter PIN"),
"enterReferralCode": MessageLookupByLibrary.simpleMessage("输入推荐代码"),
"enterThe6digitCodeFromnyourAuthenticatorApp":
MessageLookupByLibrary.simpleMessage("从你的身份验证器应用中\n输入6位数字代码"),
@ -825,6 +828,7 @@ class MessageLookup extends MessageLookupByLibrary {
"newAlbum": MessageLookupByLibrary.simpleMessage("新建相册"),
"newToEnte": MessageLookupByLibrary.simpleMessage("初来 Ente"),
"newest": MessageLookupByLibrary.simpleMessage("最新"),
"next": MessageLookupByLibrary.simpleMessage("Next"),
"no": MessageLookupByLibrary.simpleMessage(""),
"noAlbumsSharedByYouYet":
MessageLookupByLibrary.simpleMessage("您尚未共享任何相册"),
@ -847,6 +851,8 @@ class MessageLookup extends MessageLookupByLibrary {
"由于我们端到端加密协议的性质,如果没有您的密码或恢复密钥,您的数据将无法解密"),
"noResults": MessageLookupByLibrary.simpleMessage("无结果"),
"noResultsFound": MessageLookupByLibrary.simpleMessage("未找到任何结果"),
"noSystemLockFound":
MessageLookupByLibrary.simpleMessage("No system lock found"),
"nothingSharedWithYouYet":
MessageLookupByLibrary.simpleMessage("尚未与您共享任何内容"),
"nothingToSeeHere": MessageLookupByLibrary.simpleMessage("这里空空如也! 👀"),
@ -904,6 +910,7 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("您添加的照片将从相册中移除"),
"pickCenterPoint": MessageLookupByLibrary.simpleMessage("选择中心点"),
"pinAlbum": MessageLookupByLibrary.simpleMessage("置顶相册"),
"pinLock": MessageLookupByLibrary.simpleMessage("PIN lock"),
"playOnTv": MessageLookupByLibrary.simpleMessage("在电视上播放相册"),
"playStoreFreeTrialValidTill": m39,
"playstoreSubscription":
@ -965,6 +972,9 @@ class MessageLookup extends MessageLookupByLibrary {
"当前设备的功能不足以验证您的密码,但我们可以以适用于所有设备的方式重新生成。\n\n请使用您的恢复密钥登录并重新生成您的密码(如果您希望,可以再次使用相同的密码)。"),
"recreatePasswordTitle": MessageLookupByLibrary.simpleMessage("重新创建密码"),
"reddit": MessageLookupByLibrary.simpleMessage("Reddit"),
"reenterPassword":
MessageLookupByLibrary.simpleMessage("Re-enter password"),
"reenterPin": MessageLookupByLibrary.simpleMessage("Re-enter PIN"),
"referFriendsAnd2xYourPlan":
MessageLookupByLibrary.simpleMessage("把我们推荐给你的朋友然后获得延长一倍的订阅计划"),
"referralStep1": MessageLookupByLibrary.simpleMessage("1. 将此代码提供给您的朋友"),
@ -1086,6 +1096,9 @@ class MessageLookup extends MessageLookupByLibrary {
"setAs": MessageLookupByLibrary.simpleMessage("设置为"),
"setCover": MessageLookupByLibrary.simpleMessage("设置封面"),
"setLabel": MessageLookupByLibrary.simpleMessage("设置"),
"setNewPassword":
MessageLookupByLibrary.simpleMessage("Set new password"),
"setNewPin": MessageLookupByLibrary.simpleMessage("Set new PIN"),
"setPasswordTitle": MessageLookupByLibrary.simpleMessage("设置密码"),
"setRadius": MessageLookupByLibrary.simpleMessage("设定半径"),
"setupComplete": MessageLookupByLibrary.simpleMessage("设置完成"),
@ -1188,6 +1201,7 @@ class MessageLookup extends MessageLookupByLibrary {
"systemTheme": MessageLookupByLibrary.simpleMessage("适应系统"),
"tapToCopy": MessageLookupByLibrary.simpleMessage("点击以复制"),
"tapToEnterCode": MessageLookupByLibrary.simpleMessage("点击以输入代码"),
"tapToUnlock": MessageLookupByLibrary.simpleMessage("Tap to unlock"),
"tempErrorContactSupportIfPersists":
MessageLookupByLibrary.simpleMessage(
"看起来出了点问题。 请稍后重试。 如果错误仍然存在,请联系我们的支持团队。"),
@ -1226,10 +1240,15 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("这将使您在以下设备中退出登录:"),
"thisWillLogYouOutOfThisDevice":
MessageLookupByLibrary.simpleMessage("这将使您在此设备上退出登录!"),
"toEnableAppLockPleaseSetupDevicePasscodeOrScreen":
MessageLookupByLibrary.simpleMessage(
"To enable app lock, please setup device passcode or screen lock in your system settings."),
"toHideAPhotoOrVideo": MessageLookupByLibrary.simpleMessage("隐藏照片或视频"),
"toResetVerifyEmail":
MessageLookupByLibrary.simpleMessage("要重置您的密码,请先验证您的电子邮件。"),
"todaysLogs": MessageLookupByLibrary.simpleMessage("当天日志"),
"tooManyIncorrectAttempts":
MessageLookupByLibrary.simpleMessage("Too many incorrect attempts"),
"total": MessageLookupByLibrary.simpleMessage("总计"),
"totalSize": MessageLookupByLibrary.simpleMessage("总大小"),
"trash": MessageLookupByLibrary.simpleMessage("回收站"),

View File

@ -8984,6 +8984,136 @@ class S {
args: [],
);
}
/// `Re-enter password`
String get reenterPassword {
return Intl.message(
'Re-enter password',
name: 'reenterPassword',
desc: '',
args: [],
);
}
/// `Re-enter PIN`
String get reenterPin {
return Intl.message(
'Re-enter PIN',
name: 'reenterPin',
desc: '',
args: [],
);
}
/// `Device lock`
String get deviceLock {
return Intl.message(
'Device lock',
name: 'deviceLock',
desc: '',
args: [],
);
}
/// `PIN lock`
String get pinLock {
return Intl.message(
'PIN lock',
name: 'pinLock',
desc: '',
args: [],
);
}
/// `Next`
String get next {
return Intl.message(
'Next',
name: 'next',
desc: '',
args: [],
);
}
/// `Set new password`
String get setNewPassword {
return Intl.message(
'Set new password',
name: 'setNewPassword',
desc: '',
args: [],
);
}
/// `Enter PIN`
String get enterPin {
return Intl.message(
'Enter PIN',
name: 'enterPin',
desc: '',
args: [],
);
}
/// `Set new PIN`
String get setNewPin {
return Intl.message(
'Set new PIN',
name: 'setNewPin',
desc: '',
args: [],
);
}
/// `App lock`
String get appLock {
return Intl.message(
'App lock',
name: 'appLock',
desc: '',
args: [],
);
}
/// `No system lock found`
String get noSystemLockFound {
return Intl.message(
'No system lock found',
name: 'noSystemLockFound',
desc: '',
args: [],
);
}
/// `To enable app lock, please setup device passcode or screen lock in your system settings.`
String get toEnableAppLockPleaseSetupDevicePasscodeOrScreen {
return Intl.message(
'To enable app lock, please setup device passcode or screen lock in your system settings.',
name: 'toEnableAppLockPleaseSetupDevicePasscodeOrScreen',
desc: '',
args: [],
);
}
/// `Tap to unlock`
String get tapToUnlock {
return Intl.message(
'Tap to unlock',
name: 'tapToUnlock',
desc: '',
args: [],
);
}
/// `Too many incorrect attempts`
String get tooManyIncorrectAttempts {
return Intl.message(
'Too many incorrect attempts',
name: 'tooManyIncorrectAttempts',
desc: '',
args: [],
);
}
}
class AppLocalizationDelegate extends LocalizationsDelegate<S> {

View File

@ -25,5 +25,19 @@
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
"foundFaces": "Found faces",
"clusteringProgress": "Clustering progress",
"indexingIsPaused": "Indexing is paused, will automatically resume when device is ready"
"indexingIsPaused": "Indexing is paused, will automatically resume when device is ready",
"reenterPassword": "Re-enter password",
"reenterPin": "Re-enter PIN",
"deviceLock": "Device lock",
"pinLock": "PIN lock",
"passwordLock": "Password lock",
"next": "Next",
"setNewPassword": "Set new password",
"enterPin": "Enter PIN",
"setNewPin": "Set new PIN",
"appLock": "App lock",
"noSystemLockFound": "No system lock found",
"toEnableAppLockPleaseSetupDevicePasscodeOrScreen": "To enable app lock, please setup device passcode or screen lock in your system settings.",
"tapToUnlock": "Tap to unlock",
"tooManyIncorrectAttempts": "Too many incorrect attempts"
}

View File

@ -1251,5 +1251,18 @@
"left": "Links",
"right": "Rechts",
"whatsNew": "Neue Funktionen",
"reviewSuggestions": "Vorschläge überprüfen"
"reviewSuggestions": "Vorschläge überprüfen",
"reenterPassword": "Re-enter password",
"reenterPin": "Re-enter PIN",
"deviceLock": "Device lock",
"pinLock": "PIN lock",
"next": "Next",
"setNewPassword": "Set new password",
"enterPin": "Enter PIN",
"setNewPin": "Set new PIN",
"appLock": "App lock",
"noSystemLockFound": "No system lock found",
"toEnableAppLockPleaseSetupDevicePasscodeOrScreen": "To enable app lock, please setup device passcode or screen lock in your system settings.",
"tapToUnlock": "Tap to unlock",
"tooManyIncorrectAttempts": "Too many incorrect attempts"
}

View File

@ -1200,7 +1200,7 @@
"passkey": "Passkey",
"passkeyAuthTitle": "Passkey verification",
"passKeyPendingVerification": "Verification is still pending",
"loginSessionExpired" : "Session expired",
"loginSessionExpired": "Session expired",
"loginSessionExpiredDetails": "Your session has expired. Please login again.",
"verifyPasskey": "Verify passkey",
"playOnTv": "Play album on TV",
@ -1263,6 +1263,18 @@
}
}
},
"panorama": "Panorama"
"panorama": "Panorama",
"reenterPassword": "Re-enter password",
"reenterPin": "Re-enter PIN",
"deviceLock": "Device lock",
"pinLock": "PIN lock",
"next": "Next",
"setNewPassword": "Set new password",
"enterPin": "Enter PIN",
"setNewPin": "Set new PIN",
"appLock": "App lock",
"noSystemLockFound": "No system lock found",
"toEnableAppLockPleaseSetupDevicePasscodeOrScreen": "To enable app lock, please setup device passcode or screen lock in your system settings.",
"tapToUnlock": "Tap to unlock",
"tooManyIncorrectAttempts": "Too many incorrect attempts"
}

View File

@ -1251,5 +1251,18 @@
"left": "Izquierda",
"right": "Derecha",
"whatsNew": "Qué hay de nuevo",
"reviewSuggestions": "Revisar sugerencias"
"reviewSuggestions": "Revisar sugerencias",
"reenterPassword": "Re-enter password",
"reenterPin": "Re-enter PIN",
"deviceLock": "Device lock",
"pinLock": "PIN lock",
"next": "Next",
"setNewPassword": "Set new password",
"enterPin": "Enter PIN",
"setNewPin": "Set new PIN",
"appLock": "App lock",
"noSystemLockFound": "No system lock found",
"toEnableAppLockPleaseSetupDevicePasscodeOrScreen": "To enable app lock, please setup device passcode or screen lock in your system settings.",
"tapToUnlock": "Tap to unlock",
"tooManyIncorrectAttempts": "Too many incorrect attempts"
}

View File

@ -1168,5 +1168,18 @@
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
"foundFaces": "Found faces",
"clusteringProgress": "Clustering progress",
"indexingIsPaused": "Indexing is paused, will automatically resume when device is ready"
"indexingIsPaused": "Indexing is paused, will automatically resume when device is ready",
"reenterPassword": "Re-enter password",
"reenterPin": "Re-enter PIN",
"deviceLock": "Device lock",
"pinLock": "PIN lock",
"next": "Next",
"setNewPassword": "Set new password",
"enterPin": "Enter PIN",
"setNewPin": "Set new PIN",
"appLock": "App lock",
"noSystemLockFound": "No system lock found",
"toEnableAppLockPleaseSetupDevicePasscodeOrScreen": "To enable app lock, please setup device passcode or screen lock in your system settings.",
"tapToUnlock": "Tap to unlock",
"tooManyIncorrectAttempts": "Too many incorrect attempts"
}

View File

@ -1130,5 +1130,18 @@
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
"foundFaces": "Found faces",
"clusteringProgress": "Clustering progress",
"indexingIsPaused": "Indexing is paused, will automatically resume when device is ready"
"indexingIsPaused": "Indexing is paused, will automatically resume when device is ready",
"reenterPassword": "Re-enter password",
"reenterPin": "Re-enter PIN",
"deviceLock": "Device lock",
"pinLock": "PIN lock",
"next": "Next",
"setNewPassword": "Set new password",
"enterPin": "Enter PIN",
"setNewPin": "Set new PIN",
"appLock": "App lock",
"noSystemLockFound": "No system lock found",
"toEnableAppLockPleaseSetupDevicePasscodeOrScreen": "To enable app lock, please setup device passcode or screen lock in your system settings.",
"tapToUnlock": "Tap to unlock",
"tooManyIncorrectAttempts": "Too many incorrect attempts"
}

View File

@ -25,5 +25,19 @@
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
"foundFaces": "Found faces",
"clusteringProgress": "Clustering progress",
"indexingIsPaused": "Indexing is paused, will automatically resume when device is ready"
"indexingIsPaused": "Indexing is paused, will automatically resume when device is ready",
"reenterPassword": "Re-enter password",
"reenterPin": "Re-enter PIN",
"deviceLock": "Device lock",
"pinLock": "PIN lock",
"passwordLock": "Password lock",
"next": "Next",
"setNewPassword": "Set new password",
"enterPin": "Enter PIN",
"setNewPin": "Set new PIN",
"appLock": "App lock",
"noSystemLockFound": "No system lock found",
"toEnableAppLockPleaseSetupDevicePasscodeOrScreen": "To enable app lock, please setup device passcode or screen lock in your system settings.",
"tapToUnlock": "Tap to unlock",
"tooManyIncorrectAttempts": "Too many incorrect attempts"
}

View File

@ -1231,5 +1231,18 @@
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
"foundFaces": "Found faces",
"clusteringProgress": "Clustering progress",
"indexingIsPaused": "Indexing is paused, will automatically resume when device is ready"
"indexingIsPaused": "Indexing is paused, will automatically resume when device is ready",
"reenterPassword": "Re-enter password",
"reenterPin": "Re-enter PIN",
"deviceLock": "Device lock",
"pinLock": "PIN lock",
"next": "Next",
"setNewPassword": "Set new password",
"enterPin": "Enter PIN",
"setNewPin": "Set new PIN",
"appLock": "App lock",
"noSystemLockFound": "No system lock found",
"toEnableAppLockPleaseSetupDevicePasscodeOrScreen": "To enable app lock, please setup device passcode or screen lock in your system settings.",
"tapToUnlock": "Tap to unlock",
"tooManyIncorrectAttempts": "Too many incorrect attempts"
}

View File

@ -39,5 +39,19 @@
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
"foundFaces": "Found faces",
"clusteringProgress": "Clustering progress",
"indexingIsPaused": "Indexing is paused, will automatically resume when device is ready"
"indexingIsPaused": "Indexing is paused, will automatically resume when device is ready",
"reenterPassword": "Re-enter password",
"reenterPin": "Re-enter PIN",
"deviceLock": "Device lock",
"pinLock": "PIN lock",
"passwordLock": "Password lock",
"next": "Next",
"setNewPassword": "Set new password",
"enterPin": "Enter PIN",
"setNewPin": "Set new PIN",
"appLock": "App lock",
"noSystemLockFound": "No system lock found",
"toEnableAppLockPleaseSetupDevicePasscodeOrScreen": "To enable app lock, please setup device passcode or screen lock in your system settings.",
"tapToUnlock": "Tap to unlock",
"tooManyIncorrectAttempts": "Too many incorrect attempts"
}

View File

@ -126,5 +126,19 @@
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
"foundFaces": "Found faces",
"clusteringProgress": "Clustering progress",
"indexingIsPaused": "Indexing is paused, will automatically resume when device is ready"
"indexingIsPaused": "Indexing is paused, will automatically resume when device is ready",
"reenterPassword": "Re-enter password",
"reenterPin": "Re-enter PIN",
"deviceLock": "Device lock",
"pinLock": "PIN lock",
"passwordLock": "Password lock",
"next": "Next",
"setNewPassword": "Set new password",
"enterPin": "Enter PIN",
"setNewPin": "Set new PIN",
"appLock": "App lock",
"noSystemLockFound": "No system lock found",
"toEnableAppLockPleaseSetupDevicePasscodeOrScreen": "To enable app lock, please setup device passcode or screen lock in your system settings.",
"tapToUnlock": "Tap to unlock",
"tooManyIncorrectAttempts": "Too many incorrect attempts"
}

View File

@ -1251,5 +1251,18 @@
"left": "Esquerda",
"right": "Direita",
"whatsNew": "O que há de novo",
"reviewSuggestions": "Revisar sugestões"
"reviewSuggestions": "Revisar sugestões",
"reenterPassword": "Re-enter password",
"reenterPin": "Re-enter PIN",
"deviceLock": "Device lock",
"pinLock": "PIN lock",
"next": "Next",
"setNewPassword": "Set new password",
"enterPin": "Enter PIN",
"setNewPin": "Set new PIN",
"appLock": "App lock",
"noSystemLockFound": "No system lock found",
"toEnableAppLockPleaseSetupDevicePasscodeOrScreen": "To enable app lock, please setup device passcode or screen lock in your system settings.",
"tapToUnlock": "Tap to unlock",
"tooManyIncorrectAttempts": "Too many incorrect attempts"
}

View File

@ -1250,5 +1250,18 @@
"rotate": "Повернуть",
"left": "Влево",
"right": "Вправо",
"whatsNew": "Что нового"
"whatsNew": "Что нового",
"reenterPassword": "Re-enter password",
"reenterPin": "Re-enter PIN",
"deviceLock": "Device lock",
"pinLock": "PIN lock",
"next": "Next",
"setNewPassword": "Set new password",
"enterPin": "Enter PIN",
"setNewPin": "Set new PIN",
"appLock": "App lock",
"noSystemLockFound": "No system lock found",
"toEnableAppLockPleaseSetupDevicePasscodeOrScreen": "To enable app lock, please setup device passcode or screen lock in your system settings.",
"tapToUnlock": "Tap to unlock",
"tooManyIncorrectAttempts": "Too many incorrect attempts"
}

View File

@ -1251,5 +1251,18 @@
"left": "向左",
"right": "向右",
"whatsNew": "更新日志",
"reviewSuggestions": "查看建议"
"reviewSuggestions": "查看建议",
"reenterPassword": "Re-enter password",
"reenterPin": "Re-enter PIN",
"deviceLock": "Device lock",
"pinLock": "PIN lock",
"next": "Next",
"setNewPassword": "Set new password",
"enterPin": "Enter PIN",
"setNewPin": "Set new PIN",
"appLock": "App lock",
"noSystemLockFound": "No system lock found",
"toEnableAppLockPleaseSetupDevicePasscodeOrScreen": "To enable app lock, please setup device passcode or screen lock in your system settings.",
"tapToUnlock": "Tap to unlock",
"tooManyIncorrectAttempts": "Too many incorrect attempts"
}

View File

@ -54,6 +54,7 @@ import 'package:photos/utils/crypto_util.dart';
import "package:photos/utils/email_util.dart";
import 'package:photos/utils/file_uploader.dart';
import 'package:photos/utils/local_settings.dart';
import "package:photos/utils/lock_screen_settings.dart";
import 'package:shared_preferences/shared_preferences.dart';
final _logger = Logger("main");
@ -95,7 +96,7 @@ Future<void> _runInForeground(AdaptiveThemeMode? savedThemeMode) async {
builder: (args) =>
EnteApp(_runBackgroundTask, _killBGTask, locale, savedThemeMode),
lockScreen: const LockScreen(),
enabled: Configuration.instance.shouldShowLockScreen(),
enabled: await Configuration.instance.shouldShowLockScreen(),
locale: locale,
lightTheme: lightThemeData,
darkTheme: darkThemeData,
@ -196,7 +197,6 @@ Future<void> _init(bool isBackground, {String via = ''}) async {
_isProcessRunning = true;
_logger.info("Initializing... inBG =$isBackground via: $via");
final SharedPreferences preferences = await SharedPreferences.getInstance();
await _logFGHeartBeatInfo();
_logger.info("_logFGHeartBeatInfo done");
unawaited(_scheduleHeartBeat(preferences, isBackground));
@ -210,6 +210,9 @@ Future<void> _init(bool isBackground, {String via = ''}) async {
Computer.shared().turnOn(workersCount: 4).ignore();
CryptoUtil.init();
_logger.info("Lockscreen init");
LockScreenSettings.instance.init(preferences);
_logger.info("Configuration init");
await Configuration.instance.init();
_logger.info("Configuration done");

View File

@ -3,6 +3,8 @@ import "dart:async";
import 'package:flutter/material.dart';
import 'package:local_auth/local_auth.dart';
import 'package:photos/core/configuration.dart';
import "package:photos/ui/settings/lock_screen/lock_screen_password.dart";
import "package:photos/ui/settings/lock_screen/lock_screen_pin.dart";
import 'package:photos/ui/tools/app_lock.dart';
import 'package:photos/utils/auth_util.dart';
import 'package:photos/utils/dialog_util.dart';
@ -21,7 +23,7 @@ class LocalAuthenticationService {
AppLock.of(context)!.setEnabled(false);
final result = await requestAuthentication(context, infoMessage);
AppLock.of(context)!.setEnabled(
Configuration.instance.shouldShowLockScreen(),
await Configuration.instance.shouldShowLockScreen(),
);
if (!result) {
showToast(context, infoMessage);
@ -33,6 +35,47 @@ class LocalAuthenticationService {
return true;
}
Future<bool> requestEnteAuthForLockScreen(
BuildContext context,
String? savedPin,
String? savedPassword, {
bool isOnOpeningApp = false,
}) async {
if (savedPassword != null) {
final result = await Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return LockScreenPassword(
isAuthenticating: true,
isOnOpeningApp: isOnOpeningApp,
authPass: savedPassword,
);
},
),
);
if (result) {
return true;
}
}
if (savedPin != null) {
final result = await Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return LockScreenPin(
isAuthenticating: true,
isOnOpeningApp: isOnOpeningApp,
authPin: savedPin,
);
},
),
);
if (result) {
return true;
}
}
return false;
}
Future<bool> requestLocalAuthForLockScreen(
BuildContext context,
bool shouldEnableLockScreen,
@ -48,12 +91,13 @@ class LocalAuthenticationService {
);
if (result) {
AppLock.of(context)!.setEnabled(shouldEnableLockScreen);
await Configuration.instance
.setShouldShowLockScreen(shouldEnableLockScreen);
.setSystemLockScreen(shouldEnableLockScreen);
return true;
} else {
AppLock.of(context)!
.setEnabled(Configuration.instance.shouldShowLockScreen());
.setEnabled(await Configuration.instance.shouldShowLockScreen());
}
} else {
unawaited(

View File

@ -4,7 +4,7 @@ import "package:photos/generated/l10n.dart";
import "package:photos/models/account/two_factor.dart";
import 'package:photos/services/user_service.dart';
import 'package:photos/ui/lifecycle_event_handler.dart';
import 'package:pinput/pin_put/pin_put.dart';
import "package:pinput/pinput.dart";
class TwoFactorAuthenticationPage extends StatefulWidget {
final String sessionID;
@ -20,9 +20,14 @@ class TwoFactorAuthenticationPage extends StatefulWidget {
class _TwoFactorAuthenticationPageState
extends State<TwoFactorAuthenticationPage> {
final _pinController = TextEditingController();
final _pinPutDecoration = BoxDecoration(
border: Border.all(color: const Color.fromRGBO(45, 194, 98, 1.0)),
borderRadius: BorderRadius.circular(15.0),
final _pinPutDecoration = PinTheme(
height: 45,
width: 45,
decoration: BoxDecoration(
border: Border.all(color: const Color.fromRGBO(45, 194, 98, 1.0)),
borderRadius: BorderRadius.circular(15.0),
),
);
String _code = "";
late LifecycleEventHandler _lifecycleEventHandler;
@ -78,9 +83,9 @@ class _TwoFactorAuthenticationPageState
const Padding(padding: EdgeInsets.all(32)),
Padding(
padding: const EdgeInsets.fromLTRB(40, 0, 40, 0),
child: PinPut(
fieldsCount: 6,
onSubmit: (String code) {
child: Pinput(
length: 6,
onCompleted: (String code) {
_verifyTwoFactorCode(code);
},
onChanged: (String pin) {
@ -88,23 +93,33 @@ class _TwoFactorAuthenticationPageState
_code = pin;
});
},
autofocus: true,
controller: _pinController,
submittedFieldDecoration: _pinPutDecoration.copyWith(
borderRadius: BorderRadius.circular(20.0),
),
selectedFieldDecoration: _pinPutDecoration,
followingFieldDecoration: _pinPutDecoration.copyWith(
borderRadius: BorderRadius.circular(5.0),
border: Border.all(
color: const Color.fromRGBO(45, 194, 98, 0.5),
defaultPinTheme: _pinPutDecoration,
submittedPinTheme: _pinPutDecoration.copyWith(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5.0),
border: Border.all(
color: const Color.fromRGBO(45, 194, 98, 0.5),
),
),
),
inputDecoration: const InputDecoration(
focusedBorder: InputBorder.none,
border: InputBorder.none,
counterText: '',
followingPinTheme: _pinPutDecoration.copyWith(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10.0),
border: Border.all(
color: const Color.fromRGBO(45, 194, 98, 0.5),
),
),
),
focusedPinTheme: _pinPutDecoration.copyWith(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15.0),
border: Border.all(
color: const Color.fromRGBO(45, 194, 98, 0.5),
),
),
),
autofocus: true,
),
),
const Padding(padding: EdgeInsets.all(24)),

View File

@ -1,7 +1,6 @@
import 'dart:async';
import 'dart:ui';
import 'package:flutter/material.dart';
import "package:flutter/material.dart";
import 'package:flutter/services.dart';
import 'package:photos/core/configuration.dart';
import 'package:photos/ente_theme_data.dart';
@ -12,7 +11,7 @@ import 'package:photos/ui/lifecycle_event_handler.dart';
import 'package:photos/utils/crypto_util.dart';
import 'package:photos/utils/navigation_util.dart';
import 'package:photos/utils/toast_util.dart';
import 'package:pinput/pin_put/pin_put.dart';
import "package:pinput/pinput.dart";
class TwoFactorSetupPage extends StatefulWidget {
final String secretCode;
@ -34,9 +33,13 @@ class _TwoFactorSetupPageState extends State<TwoFactorSetupPage>
with SingleTickerProviderStateMixin {
late TabController _tabController;
final _pinController = TextEditingController();
final _pinPutDecoration = BoxDecoration(
border: Border.all(color: const Color.fromRGBO(45, 194, 98, 1.0)),
borderRadius: BorderRadius.circular(15.0),
final _pinPutDecoration = PinTheme(
height: 45,
width: 45,
decoration: BoxDecoration(
border: Border.all(color: const Color.fromRGBO(45, 194, 98, 1.0)),
borderRadius: BorderRadius.circular(15.0),
),
);
String _code = "";
late ImageProvider _imageProvider;
@ -219,9 +222,9 @@ class _TwoFactorSetupPageState extends State<TwoFactorSetupPage>
const Padding(padding: EdgeInsets.all(16)),
Padding(
padding: const EdgeInsets.fromLTRB(40, 0, 40, 0),
child: PinPut(
fieldsCount: 6,
onSubmit: (String code) {
child: Pinput(
length: 6,
onCompleted: (String code) {
_enableTwoFactor(code);
},
onChanged: (String pin) {
@ -230,20 +233,22 @@ class _TwoFactorSetupPageState extends State<TwoFactorSetupPage>
});
},
controller: _pinController,
submittedFieldDecoration: _pinPutDecoration.copyWith(
borderRadius: BorderRadius.circular(20.0),
),
selectedFieldDecoration: _pinPutDecoration,
followingFieldDecoration: _pinPutDecoration.copyWith(
borderRadius: BorderRadius.circular(5.0),
border: Border.all(
color: const Color.fromRGBO(45, 194, 98, 0.5),
submittedPinTheme: _pinPutDecoration.copyWith(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10.0),
border: Border.all(
color: const Color.fromRGBO(45, 194, 98, 0.5),
),
),
),
inputDecoration: const InputDecoration(
focusedBorder: InputBorder.none,
border: InputBorder.none,
counterText: '',
defaultPinTheme: _pinPutDecoration,
followingPinTheme: _pinPutDecoration.copyWith(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10.0),
border: Border.all(
color: const Color.fromRGBO(45, 194, 98, 0.5),
),
),
),
),
),

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import "package:logging/logging.dart";
import 'package:photos/models/execution_states.dart';
import 'package:photos/models/typedefs.dart';
import 'package:photos/theme/ente_theme.dart';
@ -7,6 +8,8 @@ import 'package:photos/ui/common/loading_widget.dart';
import 'package:photos/utils/debouncer.dart';
import 'package:photos/utils/separators_util.dart';
///To show wrong password state, throw an exception with the message
///"Incorrect password" in onSubmit.
class TextInputWidget extends StatefulWidget {
final String? label;
final String? message;
@ -19,7 +22,7 @@ class TextInputWidget extends StatefulWidget {
final double borderRadius;
///TextInputWidget will listen to this notifier and executes onSubmit when
///notified.
///notified. Value of this notifier is irrelevant.
final ValueNotifier? submitNotifier;
///TextInputWidget will listen to this notifier and clears and unfocuses the
@ -32,6 +35,9 @@ class TextInputWidget extends StatefulWidget {
final bool popNavAfterSubmission;
final bool shouldSurfaceExecutionStates;
final TextCapitalization? textCapitalization;
@Deprecated(
"Do not use this widget for password input. Create a separate PasswordInputWidget. This widget is becoming bloated and hard to maintain, so will create a PasswordInputWidget and remove this field from this widget in future",
)
final bool isPasswordInput;
///Clear comes in the form of a suffix icon. It is unrelated to onCancel.
@ -43,6 +49,7 @@ class TextInputWidget extends StatefulWidget {
final ValueNotifier? isEmptyNotifier;
final List<TextInputFormatter>? textInputFormatter;
final TextInputType? textInputType;
final bool enableFillColor;
const TextInputWidget({
this.onSubmit,
this.onChange,
@ -71,6 +78,7 @@ class TextInputWidget extends StatefulWidget {
this.isEmptyNotifier,
this.textInputFormatter,
this.textInputType,
this.enableFillColor = true,
super.key,
});
@ -79,6 +87,7 @@ class TextInputWidget extends StatefulWidget {
}
class _TextInputWidgetState extends State<TextInputWidget> {
final _logger = Logger("TextInputWidget");
ExecutionState executionState = ExecutionState.idle;
late final TextEditingController _textController;
final _debouncer = Debouncer(const Duration(milliseconds: 300));
@ -87,6 +96,7 @@ class _TextInputWidgetState extends State<TextInputWidget> {
///This is to pass if the TextInputWidget is in a dialog and an error is
///thrown in executing onSubmit by passing it as arg in Navigator.pop()
Exception? _exception;
bool _incorrectPassword = false;
@override
void initState() {
@ -156,7 +166,7 @@ class _TextInputWidgetState extends State<TextInputWidget> {
decoration: InputDecoration(
hintText: widget.hintText,
hintStyle: textTheme.body.copyWith(color: colorScheme.textMuted),
filled: true,
filled: widget.enableFillColor,
fillColor: colorScheme.fillFaint,
contentPadding: const EdgeInsets.fromLTRB(
12,
@ -168,7 +178,11 @@ class _TextInputWidgetState extends State<TextInputWidget> {
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: colorScheme.strokeFaint),
borderSide: BorderSide(
color: _incorrectPassword
? const Color.fromRGBO(245, 42, 42, 1)
: colorScheme.strokeFaint,
),
borderRadius: BorderRadius.circular(8),
),
suffixIcon: Padding(
@ -263,6 +277,10 @@ class _TextInputWidgetState extends State<TextInputWidget> {
executionState = ExecutionState.error;
_debouncer.cancelDebounce();
_exception = e as Exception;
if (e.toString().contains("Incorrect password")) {
_logger.warning("Incorrect password");
_surfaceWrongPasswordState();
}
if (!widget.popNavAfterSubmission) {
rethrow;
}
@ -381,6 +399,20 @@ class _TextInputWidgetState extends State<TextInputWidget> {
return formattedValue;
}
void _surfaceWrongPasswordState() {
setState(() {
_incorrectPassword = true;
HapticFeedback.vibrate();
Future.delayed(const Duration(seconds: 1), () {
if (mounted) {
setState(() {
_incorrectPassword = false;
});
}
});
});
}
}
//todo: Add clear and custom icon for suffic icon

View File

@ -0,0 +1,196 @@
import "package:flutter/material.dart";
import "package:photos/theme/ente_theme.dart";
class CustomPinKeypad extends StatelessWidget {
final TextEditingController controller;
const CustomPinKeypad({required this.controller, super.key});
@override
Widget build(BuildContext context) {
return SafeArea(
child: Container(
padding: const EdgeInsets.all(2),
color: getEnteColorScheme(context).strokeFainter,
child: Column(
children: [
Row(
children: [
_Button(
text: '',
number: '1',
onTap: () {
_onKeyTap('1');
},
),
_Button(
text: "ABC",
number: '2',
onTap: () {
_onKeyTap('2');
},
),
_Button(
text: "DEF",
number: '3',
onTap: () {
_onKeyTap('3');
},
),
],
),
Row(
children: [
_Button(
number: '4',
text: "GHI",
onTap: () {
_onKeyTap('4');
},
),
_Button(
number: '5',
text: 'JKL',
onTap: () {
_onKeyTap('5');
},
),
_Button(
number: '6',
text: 'MNO',
onTap: () {
_onKeyTap('6');
},
),
],
),
Row(
children: [
_Button(
number: '7',
text: 'PQRS',
onTap: () {
_onKeyTap('7');
},
),
_Button(
number: '8',
text: 'TUV',
onTap: () {
_onKeyTap('8');
},
),
_Button(
number: '9',
text: 'WXYZ',
onTap: () {
_onKeyTap('9');
},
),
],
),
Row(
children: [
const _Button(
number: '',
text: '',
muteButton: true,
onTap: null,
),
_Button(
number: '0',
text: '',
onTap: () {
_onKeyTap('0');
},
),
_Button(
number: '',
text: '',
icon: const Icon(Icons.backspace_outlined),
onTap: () {
_onBackspace();
},
),
],
),
],
),
),
);
}
void _onKeyTap(String number) {
controller.text += number;
return;
}
void _onBackspace() {
if (controller.text.isNotEmpty) {
controller.text =
controller.text.substring(0, controller.text.length - 1);
}
return;
}
}
class _Button extends StatelessWidget {
final String number;
final String text;
final VoidCallback? onTap;
final bool muteButton;
final Widget? icon;
const _Button({
required this.number,
required this.text,
this.muteButton = false,
required this.onTap,
this.icon,
});
@override
Widget build(BuildContext context) {
final colorScheme = getEnteColorScheme(context);
final textTheme = getEnteTextTheme(context);
return Expanded(
child: GestureDetector(
onTap: onTap,
child: Container(
margin: const EdgeInsets.all(4),
decoration: BoxDecoration(
shape: BoxShape.rectangle,
borderRadius: BorderRadius.circular(6),
color: muteButton
? colorScheme.fillFaintPressed
: icon == null
? colorScheme.backgroundElevated2
: null,
),
child: Center(
child: muteButton
? const SizedBox.shrink()
: icon != null
? Container(
child: icon,
)
: Container(
padding: const EdgeInsets.all(4),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
number,
style: textTheme.h3,
),
Text(
text,
style: textTheme.tinyBold,
),
],
),
),
),
),
),
);
}
}

View File

@ -0,0 +1,189 @@
import "package:flutter/material.dart";
import "package:flutter/services.dart";
import "package:photos/generated/l10n.dart";
import "package:photos/theme/ente_theme.dart";
import "package:photos/ui/common/dynamic_fab.dart";
import "package:photos/ui/components/buttons/icon_button_widget.dart";
import "package:photos/ui/components/text_input_widget.dart";
import "package:photos/utils/lock_screen_settings.dart";
class LockScreenConfirmPassword extends StatefulWidget {
const LockScreenConfirmPassword({
super.key,
required this.password,
});
final String password;
@override
State<LockScreenConfirmPassword> createState() =>
_LockScreenConfirmPasswordState();
}
class _LockScreenConfirmPasswordState extends State<LockScreenConfirmPassword> {
/// _confirmPasswordController is disposed by the [TextInputWidget]
final _confirmPasswordController = TextEditingController(text: null);
final LockScreenSettings _lockscreenSetting = LockScreenSettings.instance;
final _focusNode = FocusNode();
final _isFormValid = ValueNotifier<bool>(false);
final _submitNotifier = ValueNotifier(false);
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
_focusNode.requestFocus();
});
}
@override
void dispose() {
_submitNotifier.dispose();
_focusNode.dispose();
_isFormValid.dispose();
super.dispose();
}
Future<void> _confirmPasswordMatch() async {
if (widget.password == _confirmPasswordController.text) {
await _lockscreenSetting.setPassword(_confirmPasswordController.text);
Navigator.of(context).pop(true);
Navigator.of(context).pop(true);
return;
}
await HapticFeedback.vibrate();
throw Exception("Incorrect password");
}
@override
Widget build(BuildContext context) {
final colorTheme = getEnteColorScheme(context);
final textTheme = getEnteTextTheme(context);
final isKeypadOpen = MediaQuery.viewInsetsOf(context).bottom > 100;
FloatingActionButtonLocation? fabLocation() {
if (isKeypadOpen) {
return null;
} else {
return FloatingActionButtonLocation.centerFloat;
}
}
return Scaffold(
resizeToAvoidBottomInset: isKeypadOpen,
appBar: AppBar(
elevation: 0,
leading: IconButton(
onPressed: () {
FocusScope.of(context).unfocus();
Navigator.of(context).pop();
},
icon: Icon(
Icons.arrow_back,
color: colorTheme.tabIcon,
),
),
),
floatingActionButton: ValueListenableBuilder<bool>(
valueListenable: _isFormValid,
builder: (context, isFormValid, child) {
return DynamicFAB(
isKeypadOpen: isKeypadOpen,
buttonText: S.of(context).confirm,
isFormValid: isFormValid,
onPressedFunction: () async {
_submitNotifier.value = !_submitNotifier.value;
},
);
},
),
floatingActionButtonLocation: fabLocation(),
floatingActionButtonAnimator: NoScalingAnimation(),
body: SingleChildScrollView(
child: Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
height: 120,
width: 120,
child: Stack(
alignment: Alignment.center,
children: [
Container(
width: 82,
height: 82,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
colors: [
Colors.grey.shade500.withOpacity(0.2),
Colors.grey.shade50.withOpacity(0.1),
Colors.grey.shade400.withOpacity(0.2),
Colors.grey.shade300.withOpacity(0.4),
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: Padding(
padding: const EdgeInsets.all(1.0),
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: colorTheme.backgroundBase,
),
),
),
),
SizedBox(
height: 75,
width: 75,
child: CircularProgressIndicator(
backgroundColor: colorTheme.fillFaintPressed,
value: 1,
strokeWidth: 1.5,
),
),
IconButtonWidget(
size: 30,
icon: Icons.lock,
iconButtonType: IconButtonType.primary,
iconColor: colorTheme.tabIcon,
),
],
),
),
Text(
S.of(context).reenterPassword,
style: textTheme.bodyBold,
),
const Padding(padding: EdgeInsets.all(12)),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: TextInputWidget(
hintText: S.of(context).confirmPassword,
focusNode: _focusNode,
enableFillColor: false,
textCapitalization: TextCapitalization.none,
textEditingController: _confirmPasswordController,
isPasswordInput: true,
shouldSurfaceExecutionStates: false,
onChange: (p0) {
_isFormValid.value =
_confirmPasswordController.text.isNotEmpty;
},
onSubmit: (p0) {
return _confirmPasswordMatch();
},
submitNotifier: _submitNotifier,
),
),
const Padding(padding: EdgeInsets.all(12)),
],
),
),
),
);
}
}

View File

@ -0,0 +1,209 @@
import "package:flutter/material.dart";
import "package:flutter/services.dart";
import "package:photos/generated/l10n.dart";
import "package:photos/theme/ente_theme.dart";
import "package:photos/ui/components/buttons/icon_button_widget.dart";
import "package:photos/ui/settings/lock_screen/custom_pin_keypad.dart";
import "package:photos/utils/lock_screen_settings.dart";
import "package:pinput/pinput.dart";
class LockScreenConfirmPin extends StatefulWidget {
const LockScreenConfirmPin({super.key, required this.pin});
final String pin;
@override
State<LockScreenConfirmPin> createState() => _LockScreenConfirmPinState();
}
class _LockScreenConfirmPinState extends State<LockScreenConfirmPin> {
final _confirmPinController = TextEditingController(text: null);
bool isConfirmPinValid = false;
final LockScreenSettings _lockscreenSetting = LockScreenSettings.instance;
final _pinPutDecoration = PinTheme(
height: 48,
width: 48,
padding: const EdgeInsets.only(top: 6.0),
decoration: BoxDecoration(
border: Border.all(color: const Color.fromRGBO(45, 194, 98, 1.0)),
borderRadius: BorderRadius.circular(15.0),
),
);
@override
void dispose() {
super.dispose();
_confirmPinController.dispose();
}
Future<void> _confirmPinMatch() async {
if (widget.pin == _confirmPinController.text) {
await _lockscreenSetting.setPin(_confirmPinController.text);
Navigator.of(context).pop(true);
Navigator.of(context).pop(true);
return;
}
setState(() {
isConfirmPinValid = true;
});
await HapticFeedback.vibrate();
await Future.delayed(const Duration(milliseconds: 75));
_confirmPinController.clear();
setState(() {
isConfirmPinValid = false;
});
}
@override
Widget build(BuildContext context) {
final colorTheme = getEnteColorScheme(context);
final textTheme = getEnteTextTheme(context);
return Scaffold(
appBar: AppBar(
elevation: 0,
leading: IconButton(
onPressed: () {
Navigator.of(context).pop(false);
},
icon: Icon(
Icons.arrow_back,
color: colorTheme.tabIcon,
),
),
),
body: OrientationBuilder(
builder: (context, orientation) {
return orientation == Orientation.portrait
? _getBody(colorTheme, textTheme, isPortrait: true)
: SingleChildScrollView(
child: _getBody(colorTheme, textTheme, isPortrait: false),
);
},
),
);
}
Widget _getBody(colorTheme, textTheme, {required bool isPortrait}) {
return Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
height: 120,
width: 120,
child: Stack(
alignment: Alignment.center,
children: [
Container(
width: 82,
height: 82,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
colors: [
Colors.grey.shade500.withOpacity(0.2),
Colors.grey.shade50.withOpacity(0.1),
Colors.grey.shade400.withOpacity(0.2),
Colors.grey.shade300.withOpacity(0.4),
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: Padding(
padding: const EdgeInsets.all(1.0),
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: colorTheme.backgroundBase,
),
),
),
),
SizedBox(
height: 75,
width: 75,
child: ValueListenableBuilder(
valueListenable: _confirmPinController,
builder: (context, value, child) {
return TweenAnimationBuilder<double>(
tween: Tween<double>(
begin: 0,
end: _confirmPinController.text.length / 4,
),
curve: Curves.ease,
duration: const Duration(milliseconds: 250),
builder: (context, value, _) =>
CircularProgressIndicator(
backgroundColor: colorTheme.fillFaintPressed,
value: value,
color: colorTheme.primary400,
strokeWidth: 1.5,
),
);
},
),
),
IconButtonWidget(
size: 30,
icon: Icons.lock,
iconButtonType: IconButtonType.primary,
iconColor: colorTheme.tabIcon,
),
],
),
),
Text(
S.of(context).reenterPin,
style: textTheme.bodyBold,
),
const Padding(padding: EdgeInsets.all(12)),
Pinput(
length: 4,
showCursor: false,
useNativeKeyboard: false,
controller: _confirmPinController,
defaultPinTheme: _pinPutDecoration,
submittedPinTheme: _pinPutDecoration.copyWith(
textStyle: textTheme.h3Bold,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10.0),
border: Border.all(
color: colorTheme.fillBase,
),
),
),
followingPinTheme: _pinPutDecoration.copyWith(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10.0),
border: Border.all(
color: colorTheme.fillMuted,
),
),
),
focusedPinTheme: _pinPutDecoration,
errorPinTheme: _pinPutDecoration.copyWith(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10.0),
border: Border.all(
color: colorTheme.warning400,
),
),
),
errorText: '',
obscureText: true,
obscuringCharacter: '*',
forceErrorState: isConfirmPinValid,
onCompleted: (value) async {
await _confirmPinMatch();
},
),
isPortrait
? const Spacer()
: const Padding(padding: EdgeInsets.all(12)),
CustomPinKeypad(controller: _confirmPinController),
],
),
);
}
}

View File

@ -0,0 +1,222 @@
import "package:flutter/material.dart";
import "package:photos/core/configuration.dart";
import "package:photos/generated/l10n.dart";
import "package:photos/theme/ente_theme.dart";
import "package:photos/ui/components/captioned_text_widget.dart";
import "package:photos/ui/components/divider_widget.dart";
import "package:photos/ui/components/menu_item_widget/menu_item_widget.dart";
import "package:photos/ui/components/title_bar_title_widget.dart";
import "package:photos/ui/components/title_bar_widget.dart";
import "package:photos/ui/components/toggle_switch_widget.dart";
import "package:photos/ui/settings/lock_screen/lock_screen_password.dart";
import "package:photos/ui/settings/lock_screen/lock_screen_pin.dart";
import "package:photos/ui/tools/app_lock.dart";
import "package:photos/utils/lock_screen_settings.dart";
class LockScreenOptions extends StatefulWidget {
const LockScreenOptions({super.key});
@override
State<LockScreenOptions> createState() => _LockScreenOptionsState();
}
class _LockScreenOptionsState extends State<LockScreenOptions> {
final Configuration _configuration = Configuration.instance;
final LockScreenSettings _lockscreenSetting = LockScreenSettings.instance;
late bool appLock;
bool isPinEnabled = false;
bool isPasswordEnabled = false;
@override
void initState() {
super.initState();
_initializeSettings();
appLock = isPinEnabled ||
isPasswordEnabled ||
_configuration.shouldShowSystemLockScreen();
}
Future<void> _initializeSettings() async {
final bool passwordEnabled = await _lockscreenSetting.isPasswordSet();
final bool pinEnabled = await _lockscreenSetting.isPinSet();
setState(() {
isPasswordEnabled = passwordEnabled;
isPinEnabled = pinEnabled;
});
}
Future<void> _deviceLock() async {
await _lockscreenSetting.removePinAndPassword();
await _initializeSettings();
}
Future<void> _pinLock() async {
final bool result = await Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return const LockScreenPin();
},
),
);
setState(() {
_initializeSettings();
if (result) {
appLock = isPinEnabled ||
isPasswordEnabled ||
_configuration.shouldShowSystemLockScreen();
}
});
}
Future<void> _passwordLock() async {
final bool result = await Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return const LockScreenPassword();
},
),
);
setState(() {
_initializeSettings();
if (result) {
appLock = isPinEnabled ||
isPasswordEnabled ||
_configuration.shouldShowSystemLockScreen();
}
});
}
Future<void> _onToggleSwitch() async {
AppLock.of(context)!.setEnabled(!appLock);
await _configuration.setSystemLockScreen(!appLock);
await _lockscreenSetting.removePinAndPassword();
setState(() {
_initializeSettings();
appLock = !appLock;
});
}
@override
Widget build(BuildContext context) {
final colorTheme = getEnteColorScheme(context);
final textTheme = getEnteTextTheme(context);
return Scaffold(
body: CustomScrollView(
primary: false,
slivers: <Widget>[
const TitleBarWidget(
flexibleSpaceTitle: TitleBarTitleWidget(
title: 'App lock',
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Column(
children: [
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: 'App lock',
),
alignCaptionedTextToLeft: true,
singleBorderRadius: 8,
menuItemColor: colorTheme.fillFaint,
trailingWidget: ToggleSwitchWidget(
value: () => appLock,
onChanged: () => _onToggleSwitch(),
),
),
!appLock
? Padding(
padding: const EdgeInsets.only(
top: 14,
left: 14,
right: 12,
),
child: Text(
'Choose between your device\'s default lock screen and a custom lock screen with a PIN or password.',
style: textTheme.miniFaint,
textAlign: TextAlign.left,
),
)
: const SizedBox(),
const Padding(
padding: EdgeInsets.only(top: 24),
),
],
),
appLock
? Column(
children: [
MenuItemWidget(
captionedTextWidget: CaptionedTextWidget(
title: S.of(context).deviceLock,
),
alignCaptionedTextToLeft: true,
isTopBorderRadiusRemoved: false,
isBottomBorderRadiusRemoved: true,
menuItemColor: colorTheme.fillFaint,
trailingIcon:
!(isPasswordEnabled || isPinEnabled)
? Icons.check
: null,
trailingIconColor: colorTheme.tabIcon,
onTap: () => _deviceLock(),
),
DividerWidget(
dividerType: DividerType.menuNoIcon,
bgColor: colorTheme.fillFaint,
),
MenuItemWidget(
captionedTextWidget: CaptionedTextWidget(
title: S.of(context).pinLock,
),
alignCaptionedTextToLeft: true,
isTopBorderRadiusRemoved: true,
isBottomBorderRadiusRemoved: true,
menuItemColor: colorTheme.fillFaint,
trailingIcon:
isPinEnabled ? Icons.check : null,
trailingIconColor: colorTheme.tabIcon,
onTap: () => _pinLock(),
),
DividerWidget(
dividerType: DividerType.menuNoIcon,
bgColor: colorTheme.fillFaint,
),
MenuItemWidget(
captionedTextWidget: CaptionedTextWidget(
title: S.of(context).passwordLock,
),
alignCaptionedTextToLeft: true,
isTopBorderRadiusRemoved: true,
isBottomBorderRadiusRemoved: false,
menuItemColor: colorTheme.fillFaint,
trailingIcon:
isPasswordEnabled ? Icons.check : null,
trailingIconColor: colorTheme.tabIcon,
onTap: () => _passwordLock(),
),
],
)
: Container(),
],
),
),
);
},
childCount: 1,
),
),
],
),
);
}
}

View File

@ -0,0 +1,241 @@
import "dart:convert";
import "package:flutter/material.dart";
import "package:flutter/services.dart";
import "package:flutter_sodium/flutter_sodium.dart";
import "package:photos/generated/l10n.dart";
import "package:photos/theme/ente_theme.dart";
import "package:photos/ui/common/dynamic_fab.dart";
import "package:photos/ui/components/buttons/icon_button_widget.dart";
import "package:photos/ui/components/text_input_widget.dart";
import "package:photos/ui/settings/lock_screen/lock_screen_confirm_password.dart";
import "package:photos/ui/settings/lock_screen/lock_screen_options.dart";
import "package:photos/utils/crypto_util.dart";
import "package:photos/utils/lock_screen_settings.dart";
class LockScreenPassword extends StatefulWidget {
const LockScreenPassword({
super.key,
this.isAuthenticating = false,
this.isOnOpeningApp = false,
this.authPass,
});
//Is false when setting a new password
final bool isAuthenticating;
final bool isOnOpeningApp;
final String? authPass;
@override
State<LockScreenPassword> createState() => _LockScreenPasswordState();
}
class _LockScreenPasswordState extends State<LockScreenPassword> {
/// _passwordController is disposed by the [TextInputWidget]
final _passwordController = TextEditingController(text: null);
final _focusNode = FocusNode();
final _isFormValid = ValueNotifier<bool>(false);
final _submitNotifier = ValueNotifier(false);
int invalidAttemptsCount = 0;
final _lockscreenSetting = LockScreenSettings.instance;
@override
void initState() {
super.initState();
invalidAttemptsCount = _lockscreenSetting.getInvalidAttemptCount();
WidgetsBinding.instance.addPostFrameCallback((_) async {
_focusNode.requestFocus();
});
}
@override
void dispose() {
super.dispose();
_submitNotifier.dispose();
_focusNode.dispose();
_isFormValid.dispose();
}
@override
Widget build(BuildContext context) {
final colorTheme = getEnteColorScheme(context);
final textTheme = getEnteTextTheme(context);
final isKeypadOpen = MediaQuery.viewInsetsOf(context).bottom > 100;
FloatingActionButtonLocation? fabLocation() {
if (isKeypadOpen) {
return null;
} else {
return FloatingActionButtonLocation.centerFloat;
}
}
return Scaffold(
resizeToAvoidBottomInset: isKeypadOpen,
appBar: AppBar(
elevation: 0,
leading: IconButton(
onPressed: () {
FocusScope.of(context).unfocus();
Navigator.of(context).pop(false);
},
icon: Icon(
Icons.arrow_back,
color: colorTheme.tabIcon,
),
),
),
floatingActionButton: ValueListenableBuilder<bool>(
valueListenable: _isFormValid,
builder: (context, isFormValid, child) {
return DynamicFAB(
isKeypadOpen: isKeypadOpen,
buttonText: S.of(context).next,
isFormValid: isFormValid,
onPressedFunction: () async {
_submitNotifier.value = !_submitNotifier.value;
},
);
},
),
floatingActionButtonLocation: fabLocation(),
floatingActionButtonAnimator: NoScalingAnimation(),
body: SingleChildScrollView(
child: Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
height: 120,
width: 120,
child: Stack(
alignment: Alignment.center,
children: [
Container(
width: 82,
height: 82,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
colors: [
Colors.grey.shade500.withOpacity(0.2),
Colors.grey.shade50.withOpacity(0.1),
Colors.grey.shade400.withOpacity(0.2),
Colors.grey.shade300.withOpacity(0.4),
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: Padding(
padding: const EdgeInsets.all(1.0),
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: colorTheme.backgroundBase,
),
),
),
),
SizedBox(
height: 75,
width: 75,
child: CircularProgressIndicator(
backgroundColor: colorTheme.fillFaintPressed,
value: 1,
strokeWidth: 1.5,
),
),
IconButtonWidget(
size: 30,
icon: Icons.lock,
iconButtonType: IconButtonType.primary,
iconColor: colorTheme.tabIcon,
),
],
),
),
Text(
widget.isAuthenticating
? S.of(context).enterPassword
: S.of(context).setNewPassword,
textAlign: TextAlign.center,
style: textTheme.bodyBold,
),
const Padding(padding: EdgeInsets.all(12)),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: TextInputWidget(
hintText: S.of(context).password,
focusNode: _focusNode,
enableFillColor: false,
textCapitalization: TextCapitalization.none,
textEditingController: _passwordController,
isPasswordInput: true,
shouldSurfaceExecutionStates: false,
onChange: (p0) {
_isFormValid.value = _passwordController.text.isNotEmpty;
},
onSubmit: (p0) {
return _confirmPassword();
},
submitNotifier: _submitNotifier,
),
),
const Padding(padding: EdgeInsets.all(12)),
],
),
),
),
);
}
Future<bool> _confirmPasswordAuth(String inputtedPassword) async {
final Uint8List? salt = await _lockscreenSetting.getSalt();
final hash = cryptoPwHash({
"password": utf8.encode(inputtedPassword),
"salt": salt,
"opsLimit": Sodium.cryptoPwhashOpslimitInteractive,
"memLimit": Sodium.cryptoPwhashMemlimitInteractive,
});
if (widget.authPass == base64Encode(hash)) {
await _lockscreenSetting.setInvalidAttemptCount(0);
widget.isOnOpeningApp
? Navigator.of(context).pop(true)
: Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) => const LockScreenOptions(),
),
);
return true;
} else {
if (widget.isOnOpeningApp) {
invalidAttemptsCount++;
if (invalidAttemptsCount > 4) {
await _lockscreenSetting.setInvalidAttemptCount(invalidAttemptsCount);
Navigator.of(context).pop(false);
}
}
await HapticFeedback.vibrate();
throw Exception("Incorrect password");
}
}
Future<void> _confirmPassword() async {
if (widget.isAuthenticating) {
await _confirmPasswordAuth(_passwordController.text);
return;
} else {
await Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) => LockScreenConfirmPassword(
password: _passwordController.text,
),
),
);
_passwordController.clear();
}
}
}

View File

@ -0,0 +1,277 @@
import "dart:convert";
import "package:flutter/material.dart";
import "package:flutter/services.dart";
import "package:flutter_sodium/flutter_sodium.dart";
import "package:photos/generated/l10n.dart";
import "package:photos/theme/colors.dart";
import "package:photos/theme/ente_theme.dart";
import "package:photos/theme/text_style.dart";
import "package:photos/ui/components/buttons/icon_button_widget.dart";
import "package:photos/ui/settings/lock_screen/custom_pin_keypad.dart";
import "package:photos/ui/settings/lock_screen/lock_screen_confirm_pin.dart";
import "package:photos/ui/settings/lock_screen/lock_screen_options.dart";
import "package:photos/utils/crypto_util.dart";
import "package:photos/utils/lock_screen_settings.dart";
import 'package:pinput/pinput.dart';
class LockScreenPin extends StatefulWidget {
const LockScreenPin({
super.key,
this.isAuthenticating = false,
this.isOnOpeningApp = false,
this.authPin,
});
//Is false when setting a new password
final bool isAuthenticating;
final bool isOnOpeningApp;
final String? authPin;
@override
State<LockScreenPin> createState() => _LockScreenPinState();
}
class _LockScreenPinState extends State<LockScreenPin> {
final _pinController = TextEditingController(text: null);
final LockScreenSettings _lockscreenSetting = LockScreenSettings.instance;
bool isPinValid = false;
int invalidAttemptsCount = 0;
@override
void initState() {
super.initState();
invalidAttemptsCount = _lockscreenSetting.getInvalidAttemptCount();
}
@override
void dispose() {
super.dispose();
_pinController.dispose();
}
Future<bool> confirmPinAuth(String code) async {
final Uint8List? salt = await _lockscreenSetting.getSalt();
final hash = cryptoPwHash({
"password": utf8.encode(code),
"salt": salt,
"opsLimit": Sodium.cryptoPwhashOpslimitInteractive,
"memLimit": Sodium.cryptoPwhashMemlimitInteractive,
});
if (widget.authPin == base64Encode(hash)) {
invalidAttemptsCount = 0;
await _lockscreenSetting.setInvalidAttemptCount(0);
widget.isOnOpeningApp
? Navigator.of(context).pop(true)
: Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) => const LockScreenOptions(),
),
);
return true;
} else {
setState(() {
isPinValid = true;
});
await HapticFeedback.vibrate();
await Future.delayed(const Duration(milliseconds: 75));
_pinController.clear();
setState(() {
isPinValid = false;
});
if (widget.isOnOpeningApp) {
invalidAttemptsCount++;
if (invalidAttemptsCount > 4) {
await _lockscreenSetting.setInvalidAttemptCount(invalidAttemptsCount);
Navigator.of(context).pop(false);
}
}
return false;
}
}
Future<void> _confirmPin(String code) async {
if (widget.isAuthenticating) {
await confirmPinAuth(code);
return;
} else {
await Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) => LockScreenConfirmPin(pin: code),
),
);
_pinController.clear();
}
}
final _pinPutDecoration = PinTheme(
height: 48,
width: 48,
padding: const EdgeInsets.only(top: 6.0),
decoration: BoxDecoration(
border: Border.all(color: const Color.fromRGBO(45, 194, 98, 1.0)),
borderRadius: BorderRadius.circular(15.0),
),
);
@override
Widget build(BuildContext context) {
final colorTheme = getEnteColorScheme(context);
final textTheme = getEnteTextTheme(context);
return Scaffold(
appBar: AppBar(
elevation: 0,
leading: IconButton(
onPressed: () {
Navigator.of(context).pop(false);
},
icon: Icon(
Icons.arrow_back,
color: colorTheme.tabIcon,
),
),
),
body: OrientationBuilder(
builder: (context, orientation) {
return orientation == Orientation.portrait
? _getBody(colorTheme, textTheme, isPortrait: true)
: SingleChildScrollView(
child: _getBody(colorTheme, textTheme, isPortrait: false),
);
},
),
);
}
Widget _getBody(
EnteColorScheme colorTheme,
EnteTextTheme textTheme, {
required bool isPortrait,
}) {
return Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
height: 120,
width: 120,
child: Stack(
alignment: Alignment.center,
children: [
Container(
width: 82,
height: 82,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
colors: [
Colors.grey.shade500.withOpacity(0.2),
Colors.grey.shade50.withOpacity(0.1),
Colors.grey.shade400.withOpacity(0.2),
Colors.grey.shade300.withOpacity(0.4),
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: Padding(
padding: const EdgeInsets.all(1.0),
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: colorTheme.backgroundBase,
),
),
),
),
SizedBox(
height: 75,
width: 75,
child: ValueListenableBuilder(
valueListenable: _pinController,
builder: (context, value, child) {
return TweenAnimationBuilder<double>(
tween: Tween<double>(
begin: 0,
end: _pinController.text.length / 4,
),
curve: Curves.ease,
duration: const Duration(milliseconds: 250),
builder: (context, value, _) =>
CircularProgressIndicator(
backgroundColor: colorTheme.fillFaintPressed,
value: value,
color: colorTheme.primary400,
strokeWidth: 1.5,
),
);
},
),
),
IconButtonWidget(
size: 30,
icon: Icons.lock,
iconButtonType: IconButtonType.primary,
iconColor: colorTheme.tabIcon,
),
],
),
),
Text(
widget.isAuthenticating
? S.of(context).enterPin
: S.of(context).setNewPin,
style: textTheme.bodyBold,
),
const Padding(padding: EdgeInsets.all(12)),
Pinput(
length: 4,
showCursor: false,
useNativeKeyboard: false,
controller: _pinController,
defaultPinTheme: _pinPutDecoration,
submittedPinTheme: _pinPutDecoration.copyWith(
textStyle: textTheme.h3Bold,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10.0),
border: Border.all(
color: colorTheme.fillBase,
),
),
),
followingPinTheme: _pinPutDecoration.copyWith(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10.0),
border: Border.all(
color: colorTheme.fillMuted,
),
),
),
focusedPinTheme: _pinPutDecoration,
errorPinTheme: _pinPutDecoration.copyWith(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10.0),
border: Border.all(
color: colorTheme.warning400,
),
),
),
forceErrorState: isPinValid,
obscureText: true,
obscuringCharacter: '*',
errorText: '',
onCompleted: (value) async {
await _confirmPin(_pinController.text);
},
),
isPortrait
? const Spacer()
: const Padding(padding: EdgeInsets.all(12)),
CustomPinKeypad(controller: _pinController),
],
),
);
}
}

View File

@ -2,6 +2,7 @@ import 'dart:async';
import "dart:typed_data";
import 'package:flutter/material.dart';
import "package:local_auth/local_auth.dart";
import "package:logging/logging.dart";
import 'package:photos/core/configuration.dart';
import 'package:photos/core/event_bus.dart';
@ -21,6 +22,8 @@ import 'package:photos/ui/components/expandable_menu_item_widget.dart';
import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart';
import 'package:photos/ui/components/toggle_switch_widget.dart';
import 'package:photos/ui/settings/common_settings.dart';
import "package:photos/ui/settings/lock_screen/lock_screen_options.dart";
import "package:photos/utils/auth_util.dart";
import "package:photos/utils/crypto_util.dart";
import "package:photos/utils/dialog_util.dart";
import "package:photos/utils/navigation_util.dart";
@ -139,20 +142,33 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
children.addAll([
MenuItemWidget(
captionedTextWidget: CaptionedTextWidget(
title: S.of(context).lockscreen,
title: S.of(context).appLock,
),
trailingWidget: ToggleSwitchWidget(
value: () => _config.shouldShowLockScreen(),
onChanged: () async {
await LocalAuthenticationService.instance
.requestLocalAuthForLockScreen(
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () async {
if (await LocalAuthentication().isDeviceSupported()) {
final bool result = await requestAuthentication(
context,
!_config.shouldShowLockScreen(),
S.of(context).authToChangeLockscreenSetting,
S.of(context).lockScreenEnablePreSteps,
);
},
),
if (result) {
await Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return const LockScreenOptions();
},
),
);
}
} else {
await showErrorDialog(
context,
S.of(context).noSystemLockFound,
S.of(context).toEnableAppLockPleaseSetupDevicePasscodeOrScreen,
);
}
},
),
sectionOptionSpacing,
MenuItemWidget(

View File

@ -1,11 +1,21 @@
import "dart:async";
import "dart:io";
import "dart:math";
import 'package:flutter/material.dart';
import "package:flutter/scheduler.dart";
import "package:flutter_animate/flutter_animate.dart";
import 'package:logging/logging.dart';
import "package:photos/core/configuration.dart";
import "package:photos/ente_theme_data.dart";
import "package:photos/generated/l10n.dart";
import "package:photos/l10n/l10n.dart";
import 'package:photos/ui/common/gradient_button.dart';
import "package:photos/theme/ente_theme.dart";
import "package:photos/ui/components/buttons/icon_button_widget.dart";
import 'package:photos/ui/tools/app_lock.dart';
import 'package:photos/utils/auth_util.dart';
import "package:photos/utils/dialog_util.dart";
import "package:photos/utils/lock_screen_settings.dart";
class LockScreen extends StatefulWidget {
const LockScreen({Key? key}) : super(key: key);
@ -14,17 +24,34 @@ class LockScreen extends StatefulWidget {
State<LockScreen> createState() => _LockScreenState();
}
class _LockScreenState extends State<LockScreen> with WidgetsBindingObserver {
class _LockScreenState extends State<LockScreen>
with WidgetsBindingObserver, TickerProviderStateMixin {
final _logger = Logger("LockScreen");
bool _isShowingLockScreen = false;
bool _hasPlacedAppInBackground = false;
bool _hasAuthenticationFailed = false;
int? lastAuthenticatingTime;
bool isTimerRunning = false;
int lockedTimeInSeconds = 0;
int invalidAttemptCount = 0;
int remainingTimeInSeconds = 0;
bool showErrorMessage = true;
final _lockscreenSetting = LockScreenSettings.instance;
late final AnimationController _controller = AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this,
);
late final animation = CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
);
late Brightness _platformBrightness;
@override
void initState() {
_logger.info("initiatingState");
super.initState();
invalidAttemptCount = _lockscreenSetting.getInvalidAttemptCount();
WidgetsBinding.instance.addObserver(this);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
if (isNonMobileIOSDevice()) {
@ -33,37 +60,135 @@ class _LockScreenState extends State<LockScreen> with WidgetsBindingObserver {
}
_showLockScreen(source: "postFrameInit");
});
_platformBrightness =
SchedulerBinding.instance.platformDispatcher.platformBrightness;
}
@override
Widget build(BuildContext context) {
final colorTheme = getEnteColorScheme(context);
final textTheme = getEnteTextTheme(context);
return Scaffold(
body: Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Stack(
alignment: Alignment.center,
body: GestureDetector(
onTap: () {
isTimerRunning ? null : _showLockScreen(source: "tap");
},
child: Container(
decoration: BoxDecoration(
image: DecorationImage(
opacity: _platformBrightness == Brightness.light ? 0.08 : 0.12,
image: const ExactAssetImage(
'assets/lock_screen_background.png',
),
fit: BoxFit.cover,
),
),
child: Center(
child: Column(
children: [
Opacity(
opacity: 0.2,
child: Image.asset('assets/loading_photos_background.png'),
),
const Spacer(),
SizedBox(
width: 180,
child: GradientButton(
text: context.l10n.unlock,
iconData: Icons.lock_open_outlined,
onTap: () async {
// ignore: unawaited_futures
_showLockScreen(source: "tapUnlock");
},
height: 120,
width: 120,
child: Stack(
alignment: Alignment.center,
children: [
Container(
width: 82,
height: 82,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
colors: [
Colors.grey.shade500.withOpacity(0.2),
Colors.grey.shade50.withOpacity(0.1),
Colors.grey.shade400.withOpacity(0.2),
Colors.grey.shade300.withOpacity(0.4),
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: Padding(
padding: const EdgeInsets.all(1.0),
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: colorTheme.backgroundBase,
),
),
),
),
SizedBox(
height: 75,
width: 75,
child: TweenAnimationBuilder<double>(
tween: Tween<double>(
begin: 0,
end: _getFractionOfTimeElapsed(),
),
duration: const Duration(seconds: 1),
builder: (context, value, _) =>
CircularProgressIndicator(
backgroundColor: colorTheme.fillFaintPressed,
value: value,
color: colorTheme.primary400,
strokeWidth: 1.5,
),
),
),
IconButtonWidget(
size: 30,
icon: Icons.lock,
iconButtonType: IconButtonType.primary,
iconColor: colorTheme.tabIcon,
),
],
),
),
const Spacer(),
isTimerRunning
? Stack(
alignment: Alignment.center,
children: [
Text(
S.of(context).tooManyIncorrectAttempts,
style: textTheme.small,
)
.animate(
delay: const Duration(milliseconds: 2000),
)
.fadeOut(
duration: 400.ms,
curve: Curves.easeInOutCirc,
),
Text(
_formatTime(remainingTimeInSeconds),
style: textTheme.small,
)
.animate(
delay: const Duration(milliseconds: 2250),
)
.fadeIn(
duration: 400.ms,
curve: Curves.easeInOutCirc,
),
],
)
: GestureDetector(
onTap: () => _showLockScreen(source: "tap"),
child: Text(
S.of(context).tapToUnlock,
style: textTheme.small,
),
),
const Padding(
padding: EdgeInsets.only(bottom: 24),
),
],
),
],
),
),
),
);
@ -73,37 +198,76 @@ class _LockScreenState extends State<LockScreen> with WidgetsBindingObserver {
if (Platform.isAndroid) {
return false;
}
final shortestSide = MediaQuery.of(context).size.shortestSide;
final shortestSide = MediaQuery.sizeOf(context).shortestSide;
return shortestSide > 600 ? true : false;
}
Future<void> _autoLogoutOnMaxInvalidAttempts() async {
final AlertDialog alert = AlertDialog(
title: Text(S.of(context).tooManyIncorrectAttempts),
content: Text(S.of(context).pleaseLoginAgain),
actions: [
TextButton(
child: Text(
S.of(context).ok,
style: TextStyle(
color: Theme.of(context).colorScheme.greenAlternative,
),
),
onPressed: () async {
Navigator.of(context, rootNavigator: true).pop('dialog');
Navigator.of(context).popUntil((route) => route.isFirst);
final dialog =
createProgressDialog(context, S.of(context).loggingOut);
await dialog.show();
await Configuration.instance.logout();
await dialog.hide();
},
),
],
);
await showDialog(
context: context,
builder: (BuildContext context) {
return alert;
},
);
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
Future<void> didChangeAppLifecycleState(AppLifecycleState state) async {
_logger.info(state.toString());
if (state == AppLifecycleState.resumed && !_isShowingLockScreen) {
// This is triggered either when the lock screen is dismissed or when
// the app is brought to foreground
_hasPlacedAppInBackground = false;
final bool didAuthInLast5Seconds = lastAuthenticatingTime != null &&
DateTime.now().millisecondsSinceEpoch - lastAuthenticatingTime! <
5000;
if (!_hasAuthenticationFailed && !didAuthInLast5Seconds) {
// Show the lock screen again only if the app is resuming from the
// background, and not when the lock screen was explicitly dismissed
Future.delayed(
Duration.zero,
() => _showLockScreen(source: "lifeCycle"),
);
if (_lockscreenSetting.getlastInvalidAttemptTime() >
DateTime.now().millisecondsSinceEpoch &&
!_isShowingLockScreen) {
final int time = (_lockscreenSetting.getlastInvalidAttemptTime() -
DateTime.now().millisecondsSinceEpoch) ~/
1000;
Future.delayed(
Duration.zero,
() {
startLockTimer(time);
_showLockScreen(source: "lifeCycle");
},
);
}
} else {
_hasAuthenticationFailed = false; // Reset failure state
_hasAuthenticationFailed = false;
}
} else if (state == AppLifecycleState.paused ||
state == AppLifecycleState.inactive) {
// This is triggered either when the lock screen pops up or when
// the app is pushed to background
if (!_isShowingLockScreen) {
_hasPlacedAppInBackground = true;
_hasAuthenticationFailed = false; // reset failure state
_hasAuthenticationFailed = false;
}
}
}
@ -115,24 +279,100 @@ class _LockScreenState extends State<LockScreen> with WidgetsBindingObserver {
super.dispose();
}
Future<void> startLockTimer(int timeInSeconds) async {
if (isTimerRunning) {
return;
}
setState(() {
isTimerRunning = true;
remainingTimeInSeconds = timeInSeconds;
});
while (remainingTimeInSeconds > 0) {
await Future.delayed(const Duration(seconds: 1));
setState(() {
remainingTimeInSeconds--;
});
}
setState(() {
isTimerRunning = false;
});
}
double _getFractionOfTimeElapsed() {
final int totalLockedTime =
lockedTimeInSeconds = pow(2, invalidAttemptCount - 5).toInt() * 30;
if (remainingTimeInSeconds == 0) return 1;
return 1 - remainingTimeInSeconds / totalLockedTime;
}
String _formatTime(int seconds) {
final int hours = seconds ~/ 3600;
final int minutes = (seconds % 3600) ~/ 60;
final int remainingSeconds = seconds % 60;
if (hours > 0) {
return "${hours}h ${minutes}m";
} else if (minutes > 0) {
return "${minutes}m ${remainingSeconds}s";
} else {
return "${remainingSeconds}s";
}
}
Future<void> _showLockScreen({String source = ''}) async {
final int id = DateTime.now().millisecondsSinceEpoch;
_logger.info("Showing lock screen $source $id");
final int currentTimestamp = DateTime.now().millisecondsSinceEpoch;
_logger.info("Showing lock screen $source $currentTimestamp");
try {
if (currentTimestamp < _lockscreenSetting.getlastInvalidAttemptTime() &&
!_isShowingLockScreen) {
final int remainingTime =
(_lockscreenSetting.getlastInvalidAttemptTime() -
currentTimestamp) ~/
1000;
await startLockTimer(remainingTime);
}
_isShowingLockScreen = true;
final result = await requestAuthentication(
context,
context.l10n.authToViewYourMemories,
);
_logger.finest("LockScreen Result $result $id");
final result = isTimerRunning
? false
: await requestAuthentication(
context,
context.l10n.authToViewYourMemories,
isOpeningApp: true,
);
_logger.finest("LockScreen Result $result");
_isShowingLockScreen = false;
if (result) {
lastAuthenticatingTime = DateTime.now().millisecondsSinceEpoch;
AppLock.of(context)!.didUnlock();
await _lockscreenSetting.setInvalidAttemptCount(0);
setState(() {
lockedTimeInSeconds = 15;
isTimerRunning = false;
});
} else {
if (!_hasPlacedAppInBackground) {
// Treat this as a failure only if user did not explicitly
// put the app in background
if (_lockscreenSetting.getInvalidAttemptCount() > 4 &&
invalidAttemptCount !=
_lockscreenSetting.getInvalidAttemptCount()) {
invalidAttemptCount = _lockscreenSetting.getInvalidAttemptCount();
if (invalidAttemptCount > 9) {
await _autoLogoutOnMaxInvalidAttempts();
return;
}
lockedTimeInSeconds = pow(2, invalidAttemptCount - 5).toInt() * 30;
await _lockscreenSetting.setLastInvalidAttemptTime(
DateTime.now().millisecondsSinceEpoch +
lockedTimeInSeconds * 1000,
);
await startLockTimer(lockedTimeInSeconds);
}
_hasAuthenticationFailed = true;
_logger.info("Authentication failed");
}

View File

@ -4,35 +4,53 @@ import 'package:local_auth_android/local_auth_android.dart';
import 'package:local_auth_ios/local_auth_ios.dart';
import 'package:logging/logging.dart';
import "package:photos/generated/l10n.dart";
import "package:photos/services/local_authentication_service.dart";
import "package:photos/utils/lock_screen_settings.dart";
Future<bool> requestAuthentication(BuildContext context, String reason) async {
Future<bool> requestAuthentication(
BuildContext context,
String reason, {
bool isOpeningApp = false,
}) async {
Logger("AuthUtil").info("Requesting authentication");
await LocalAuthentication().stopAuthentication();
return await LocalAuthentication().authenticate(
localizedReason: reason,
authMessages: [
AndroidAuthMessages(
biometricHint: S.of(context).androidBiometricHint,
biometricNotRecognized: S.of(context).androidBiometricNotRecognized,
biometricRequiredTitle: S.of(context).androidBiometricRequiredTitle,
biometricSuccess: S.of(context).androidBiometricSuccess,
cancelButton: S.of(context).androidCancelButton,
deviceCredentialsRequiredTitle:
S.of(context).androidDeviceCredentialsRequiredTitle,
deviceCredentialsSetupDescription:
S.of(context).androidDeviceCredentialsSetupDescription,
goToSettingsButton: S.of(context).goToSettings,
goToSettingsDescription: S.of(context).androidGoToSettingsDescription,
signInTitle: S.of(context).androidSignInTitle,
),
IOSAuthMessages(
goToSettingsButton: S.of(context).goToSettings,
goToSettingsDescription: S.of(context).goToSettings,
lockOut: S.of(context).iOSLockOut,
// cancelButton default value is "Ok"
cancelButton: S.of(context).iOSOkButton,
),
],
);
final String? savedPin = await LockScreenSettings.instance.getPin();
final String? savedPassword = await LockScreenSettings.instance.getPassword();
if (savedPassword != null || savedPin != null) {
return await LocalAuthenticationService.instance
.requestEnteAuthForLockScreen(
context,
savedPin,
savedPassword,
isOnOpeningApp: isOpeningApp,
);
} else {
return await LocalAuthentication().authenticate(
localizedReason: reason,
authMessages: [
AndroidAuthMessages(
biometricHint: S.of(context).androidBiometricHint,
biometricNotRecognized: S.of(context).androidBiometricNotRecognized,
biometricRequiredTitle: S.of(context).androidBiometricRequiredTitle,
biometricSuccess: S.of(context).androidBiometricSuccess,
cancelButton: S.of(context).androidCancelButton,
deviceCredentialsRequiredTitle:
S.of(context).androidDeviceCredentialsRequiredTitle,
deviceCredentialsSetupDescription:
S.of(context).androidDeviceCredentialsSetupDescription,
goToSettingsButton: S.of(context).goToSettings,
goToSettingsDescription: S.of(context).androidGoToSettingsDescription,
signInTitle: S.of(context).androidSignInTitle,
),
IOSAuthMessages(
goToSettingsButton: S.of(context).goToSettings,
goToSettingsDescription: S.of(context).goToSettings,
lockOut: S.of(context).iOSLockOut,
// cancelButton default value is "Ok"
cancelButton: S.of(context).iOSOkButton,
),
],
);
}
}

View File

@ -0,0 +1,116 @@
import "dart:convert";
import "package:flutter/foundation.dart";
import "package:flutter_secure_storage/flutter_secure_storage.dart";
import "package:flutter_sodium/flutter_sodium.dart";
import "package:photos/utils/crypto_util.dart";
import "package:shared_preferences/shared_preferences.dart";
class LockScreenSettings {
LockScreenSettings._privateConstructor();
static final LockScreenSettings instance =
LockScreenSettings._privateConstructor();
static const password = "ls_password";
static const pin = "ls_pin";
static const saltKey = "ls_salt";
static const keyInvalidAttempts = "ls_invalid_attempts";
static const lastInvalidAttemptTime = "ls_last_invalid_attempt_time";
late FlutterSecureStorage _secureStorage;
late SharedPreferences _preferences;
void init(SharedPreferences prefs) async {
_secureStorage = const FlutterSecureStorage();
_preferences = prefs;
}
Future<void> setLastInvalidAttemptTime(int time) async {
await _preferences.setInt(lastInvalidAttemptTime, time);
}
int getlastInvalidAttemptTime() {
return _preferences.getInt(lastInvalidAttemptTime) ?? 0;
}
int getInvalidAttemptCount() {
return _preferences.getInt(keyInvalidAttempts) ?? 0;
}
Future<void> setInvalidAttemptCount(int count) async {
await _preferences.setInt(keyInvalidAttempts, count);
}
static Uint8List _generateSalt() {
return Sodium.randombytesBuf(Sodium.cryptoPwhashSaltbytes);
}
Future<void> setPin(String userPin) async {
await _secureStorage.delete(key: saltKey);
final salt = _generateSalt();
final hash = cryptoPwHash({
"password": utf8.encode(userPin),
"salt": salt,
"opsLimit": Sodium.cryptoPwhashOpslimitInteractive,
"memLimit": Sodium.cryptoPwhashMemlimitInteractive,
});
final String saltPin = base64Encode(salt);
final String hashedPin = base64Encode(hash);
await _secureStorage.write(key: saltKey, value: saltPin);
await _secureStorage.write(key: pin, value: hashedPin);
await _secureStorage.delete(key: password);
return;
}
Future<Uint8List?> getSalt() async {
final String? salt = await _secureStorage.read(key: saltKey);
if (salt == null) return null;
return base64Decode(salt);
}
Future<String?> getPin() async {
return _secureStorage.read(key: pin);
}
Future<void> setPassword(String pass) async {
await _secureStorage.delete(key: saltKey);
final salt = _generateSalt();
final hash = cryptoPwHash({
"password": utf8.encode(pass),
"salt": salt,
"opsLimit": Sodium.cryptoPwhashOpslimitInteractive,
"memLimit": Sodium.cryptoPwhashMemlimitInteractive,
});
final String saltPassword = base64Encode(salt);
final String hashPassword = base64Encode(hash);
await _secureStorage.write(key: saltKey, value: saltPassword);
await _secureStorage.write(key: password, value: hashPassword);
await _secureStorage.delete(key: pin);
return;
}
Future<String?> getPassword() async {
return _secureStorage.read(key: password);
}
Future<void> removePinAndPassword() async {
await _secureStorage.delete(key: saltKey);
await _secureStorage.delete(key: pin);
await _secureStorage.delete(key: password);
}
Future<bool> isPinSet() async {
return await _secureStorage.containsKey(key: pin);
}
Future<bool> isPasswordSet() async {
return await _secureStorage.containsKey(key: password);
}
}

View File

@ -1851,10 +1851,10 @@ packages:
dependency: "direct main"
description:
name: pinput
sha256: "27eb69042f75755bdb6544f6e79a50a6ed09d6e97e2d75c8421744df1e392949"
sha256: "543da5bfdefd9e06914a12100f8c9156f84cef3efc14bca507c49e966c5b813b"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
version: "2.3.0"
platform:
dependency: transitive
description:
@ -2172,6 +2172,14 @@ packages:
description: flutter
source: sdk
version: "0.0.99"
smart_auth:
dependency: transitive
description:
name: smart_auth
sha256: a25229b38c02f733d0a4e98d941b42bed91a976cb589e934895e60ccfa674cf6
url: "https://pub.dev"
source: hosted
version: "1.1.1"
source_gen:
dependency: transitive
description:
@ -2464,10 +2472,10 @@ packages:
dependency: transitive
description:
name: universal_platform
sha256: d315be0f6641898b280ffa34e2ddb14f3d12b1a37882557869646e0cc363d0cc
sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec"
url: "https://pub.dev"
source: hosted
version: "1.0.0+1"
version: "1.1.0"
uri_parser:
dependency: transitive
description:

View File

@ -12,7 +12,7 @@ description: ente photos application
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 0.9.3+903
version: 0.9.4+904
publish_to: none
environment:
@ -139,7 +139,7 @@ dependencies:
permission_handler: ^11.0.1
photo_manager: ^3.2.0
photo_view: ^0.14.0
pinput: ^1.2.2
pinput: ^2.0.2
pointycastle: ^3.7.3
pool: ^1.5.1
protobuf: ^3.1.0

View File

@ -3,6 +3,7 @@ import { formatDateTimeFull } from "@ente/shared/time/format";
import { Box, Stack, styled, Typography } from "@mui/material";
import Titlebar from "components/Titlebar";
import { t } from "i18next";
import React from "react";
import { FileInfoSidebar } from ".";
const ExifItem = styled(Box)`
@ -84,7 +85,7 @@ export function ExifData(props: {
</Typography>
</ExifItem>
) : (
<></>
<React.Fragment key={key}></React.Fragment>
),
)}
</Stack>

View File

@ -42,9 +42,19 @@ export default function InfoItem({
<Typography sx={{ wordBreak: "break-all" }}>
{title}
</Typography>
<Typography variant="small" color="text.muted">
{caption}
</Typography>
{!caption || typeof caption == "string" ? (
<Typography variant="small" color="text.muted">
{caption}
</Typography>
) : (
<Typography
variant="small"
component="div"
color="text.muted"
>
{caption}
</Typography>
)}
</>
)}
</Box>

View File

@ -332,7 +332,7 @@ export function FileInfo({
{isMLEnabled() && (
<>
{/* <PhotoPeopleList file={file} /> */}
<UnidentifiedFaces file={file} />
<UnidentifiedFaces enteFile={file} />
</>
)}
</Stack>

View File

@ -149,7 +149,7 @@ export const WatchFolder: React.FC<WatchFolderProps> = ({ open, onClose }) => {
};
const Title_ = styled("div")`
padding: 32px 16px 16px 24px;
padding: 16px 12px 16px 16px;
`;
interface WatchList {

View File

@ -1,43 +1,18 @@
import { unidentifiedFaceIDs } from "@/new/photos/services/ml";
import {
regenerateFaceCropsIfNeeded,
unidentifiedFaceIDs,
} from "@/new/photos/services/ml";
import type { Person } from "@/new/photos/services/ml/people";
import { EnteFile } from "@/new/photos/types/file";
import { blobCache } from "@/next/blob-cache";
import { Skeleton, styled } from "@mui/material";
import { Legend } from "components/PhotoViewer/styledComponents/Legend";
import { Skeleton, Typography, styled } from "@mui/material";
import { t } from "i18next";
import React, { useEffect, useState } from "react";
const FaceChipContainer = styled("div")`
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
margin-top: 5px;
margin-bottom: 5px;
overflow: auto;
`;
const FaceChip = styled("div")<{ clickable?: boolean }>`
width: 112px;
height: 112px;
margin: 5px;
border-radius: 50%;
overflow: hidden;
position: relative;
cursor: ${({ clickable }) => (clickable ? "pointer" : "normal")};
& > img {
width: 100%;
height: 100%;
}
`;
interface PeopleListPropsBase {
onSelect?: (person: Person, index: number) => void;
}
export interface PeopleListProps extends PeopleListPropsBase {
export interface PeopleListProps {
people: Array<Person>;
maxRows?: number;
onSelect?: (person: Person, index: number) => void;
}
export const PeopleList = React.memo((props: PeopleListProps) => {
@ -64,38 +39,81 @@ export const PeopleList = React.memo((props: PeopleListProps) => {
);
});
export interface PhotoPeopleListProps extends PeopleListPropsBase {
const FaceChipContainer = styled("div")`
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
margin-top: 5px;
margin-bottom: 5px;
overflow: auto;
`;
const FaceChip = styled("div")<{ clickable?: boolean }>`
width: 112px;
height: 112px;
margin: 5px;
border-radius: 50%;
overflow: hidden;
position: relative;
cursor: ${({ clickable }) => (clickable ? "pointer" : "normal")};
& > img {
width: 100%;
height: 100%;
}
`;
export interface PhotoPeopleListProps {
file: EnteFile;
onSelect?: (person: Person, index: number) => void;
}
export function PhotoPeopleList() {
return <></>;
}
export function UnidentifiedFaces({ file }: { file: EnteFile }) {
interface UnidentifiedFacesProps {
enteFile: EnteFile;
}
/**
* Show the list of faces in the given file that are not linked to a specific
* person ("face cluster").
*/
export const UnidentifiedFaces: React.FC<UnidentifiedFacesProps> = ({
enteFile,
}) => {
const [faceIDs, setFaceIDs] = useState<string[]>([]);
const [didRegen, setDidRegen] = useState(false);
useEffect(() => {
let didCancel = false;
(async () => {
const faceIDs = await unidentifiedFaceIDs(file);
const faceIDs = await unidentifiedFaceIDs(enteFile);
!didCancel && setFaceIDs(faceIDs);
// Don't block for the regeneration to happen. If anything got
// regenerated, the result will be true, in response to which we'll
// change the key of the face list and cause it to be rerendered
// (fetching the regenerated crops).
void regenerateFaceCropsIfNeeded(enteFile).then((r) =>
setDidRegen(r),
);
})();
return () => {
didCancel = true;
};
}, [file]);
}, [enteFile]);
if (faceIDs.length == 0) return <></>;
return (
<>
<div>
<Legend>{t("UNIDENTIFIED_FACES")}</Legend>
</div>
<FaceChipContainer>
<Typography variant="large" p={1}>
{t("UNIDENTIFIED_FACES")}
</Typography>
<FaceChipContainer key={didRegen ? 1 : 0}>
{faceIDs.map((faceID) => (
<FaceChip key={faceID}>
<FaceCropImageView {...{ faceID }} />
@ -104,12 +122,18 @@ export function UnidentifiedFaces({ file }: { file: EnteFile }) {
</FaceChipContainer>
</>
);
}
};
interface FaceCropImageViewProps {
faceID: string;
}
/**
* An image view showing the face crop for the given {@link faceID}.
*
* The image is read from the "face-crops" {@link BlobCache}. While the image is
* being fetched, or if it doesn't exist, a placeholder is shown.
*/
const FaceCropImageView: React.FC<FaceCropImageViewProps> = ({ faceID }) => {
const [objectURL, setObjectURL] = useState<string | undefined>();
@ -119,11 +143,6 @@ const FaceCropImageView: React.FC<FaceCropImageViewProps> = ({ faceID }) => {
blobCache("face-crops")
.then((cache) => cache.get(faceID))
.then((data) => {
/*
TODO(MR): regen if needed and get this to work on web too.
cachedOrNew("face-crops", cacheKey, async () => {
return regenerateFaceCrop(faceId);
})*/
if (data) {
const blob = new Blob([data]);
if (!didCancel) setObjectURL(URL.createObjectURL(blob));
@ -138,7 +157,7 @@ const FaceCropImageView: React.FC<FaceCropImageViewProps> = ({ faceID }) => {
}, [faceID]);
return objectURL ? (
<img src={objectURL} />
<img style={{ objectFit: "cover" }} src={objectURL} />
) : (
<Skeleton variant="circular" height={120} width={120} />
);

View File

@ -3,7 +3,7 @@ import {
getLocalTrashedFiles,
} from "@/new/photos/services/files";
import type { EmbeddingModel } from "@/new/photos/services/ml/embedding";
import type { FaceIndex } from "@/new/photos/services/ml/types";
import type { FaceIndex } from "@/new/photos/services/ml/face";
import { EnteFile } from "@/new/photos/types/file";
import { inWorker } from "@/next/env";
import log from "@/next/log";

View File

@ -1,7 +1,7 @@
import { FILE_TYPE } from "@/media/file-type";
import { potentialFileTypeFromExtension } from "@/media/live-photo";
import { getLocalFiles } from "@/new/photos/services/files";
import { onUpload as onUploadML } from "@/new/photos/services/ml";
import { indexNewUpload } from "@/new/photos/services/ml";
import type { UploadItem } from "@/new/photos/services/upload/types";
import {
RANDOM_PERCENTAGE_PROGRESS_FOR_PUT,
@ -614,11 +614,11 @@ class UploadManager {
UPLOAD_RESULT.UPLOADED_WITH_STATIC_THUMBNAIL,
].includes(uploadResult)
) {
const uploadItem =
uploadableItem.uploadItem ??
uploadableItem.livePhotoAssets.image;
try {
let file: File | undefined;
const uploadItem =
uploadableItem.uploadItem ??
uploadableItem.livePhotoAssets.image;
if (uploadItem) {
if (uploadItem instanceof File) {
file = uploadItem;
@ -635,10 +635,17 @@ class UploadManager {
enteFile: decryptedFile,
localFile: file,
});
onUploadML(decryptedFile, file);
} catch (e) {
log.warn("Ignoring error in fileUploaded handlers", e);
}
if (
uploadItem &&
(uploadResult == UPLOAD_RESULT.UPLOADED ||
uploadResult ==
UPLOAD_RESULT.UPLOADED_WITH_STATIC_THUMBNAIL)
) {
indexNewUpload(decryptedFile, uploadItem);
}
this.updateExistingFiles(decryptedFile);
}
await this.watchFolderCallback(

View File

@ -9,7 +9,7 @@ import type {
LivePhotoSourceURL,
SourceURLs,
} from "@/new/photos/types/file";
import { getRenderableImage } from "@/new/photos/utils/file";
import { renderableImageBlob } from "@/new/photos/utils/file";
import { isDesktop } from "@/next/app";
import { blobCache, type BlobCache } from "@/next/blob-cache";
import log from "@/next/log";
@ -458,7 +458,7 @@ async function getRenderableFileURL(
switch (file.metadata.fileType) {
case FILE_TYPE.IMAGE: {
const convertedBlob = await getRenderableImage(
const convertedBlob = await renderableImageBlob(
file.metadata.title,
fileBlob,
);
@ -511,7 +511,7 @@ async function getRenderableLivePhotoURL(
const getRenderableLivePhotoImageURL = async () => {
try {
const imageBlob = new Blob([livePhoto.imageData]);
const convertedImageBlob = await getRenderableImage(
const convertedImageBlob = await renderableImageBlob(
livePhoto.imageFileName,
imageBlob,
);

View File

@ -0,0 +1,107 @@
import { FILE_TYPE } from "@/media/file-type";
import { decodeLivePhoto } from "@/media/live-photo";
import { basename } from "@/next/file";
import { ensure } from "@/utils/ensure";
import type { EnteFile } from "../../types/file";
import { renderableImageBlob } from "../../utils/file";
import { readStream } from "../../utils/native-stream";
import DownloadManager from "../download";
import type { UploadItem } from "../upload/types";
import type { MLWorkerElectron } from "./worker-electron";
/**
* Return a {@link ImageBitmap} that downloads the source image corresponding to
* {@link enteFile} from remote.
*
* - For images the original is used.
* - For live photos the original image component is used.
* - For videos the thumbnail is used.
*/
export const renderableImageBitmap = async (enteFile: EnteFile) => {
const fileType = enteFile.metadata.fileType;
let blob: Blob | undefined;
if (fileType == FILE_TYPE.VIDEO) {
const thumbnailData = await DownloadManager.getThumbnail(enteFile);
blob = new Blob([ensure(thumbnailData)]);
} else {
blob = await fetchRenderableBlob(enteFile);
}
return createImageBitmap(ensure(blob));
};
/**
* Variant of {@link renderableImageBitmap} that uses the given
* {@link uploadItem} to construct the image bitmap instead of downloading the
* original from remote.
*
* For videos the thumbnail is still downloaded from remote.
*/
export const renderableUploadItemImageBitmap = async (
enteFile: EnteFile,
uploadItem: UploadItem,
electron: MLWorkerElectron,
) => {
const fileType = enteFile.metadata.fileType;
let blob: Blob | undefined;
if (fileType == FILE_TYPE.VIDEO) {
const thumbnailData = await DownloadManager.getThumbnail(enteFile);
blob = new Blob([ensure(thumbnailData)]);
} else {
const file = await readNonVideoUploadItem(uploadItem, electron);
blob = await renderableImageBlob(enteFile.metadata.title, file);
}
return createImageBitmap(ensure(blob));
};
/**
* Read the given {@link uploadItem} into an in-memory representation.
*
* See: [Note: Reading a UploadItem]
*
* @param uploadItem An {@link UploadItem} which we are trying to index. The
* code calling us guarantees that this function will not be called for videos.
*
* @returns a web {@link File} that can be used to access the upload item's
* contents.
*/
const readNonVideoUploadItem = async (
uploadItem: UploadItem,
electron: MLWorkerElectron,
): Promise<File> => {
if (typeof uploadItem == "string" || Array.isArray(uploadItem)) {
const { response, lastModifiedMs } = await readStream(
electron,
uploadItem,
);
const path = typeof uploadItem == "string" ? uploadItem : uploadItem[1];
// This function will not be called for videos, and for images
// it is reasonable to read the entire stream into memory here.
return new File([await response.arrayBuffer()], basename(path), {
lastModified: lastModifiedMs,
});
} else {
if (uploadItem instanceof File) {
return uploadItem;
} else {
return uploadItem.file;
}
}
};
const fetchRenderableBlob = async (enteFile: EnteFile) => {
const fileStream = await DownloadManager.getFile(enteFile);
const fileBlob = await new Response(fileStream).blob();
const fileType = enteFile.metadata.fileType;
if (fileType == FILE_TYPE.IMAGE) {
return renderableImageBlob(enteFile.metadata.title, fileBlob);
} else if (fileType == FILE_TYPE.LIVE_PHOTO) {
const { imageFileName, imageData } = await decodeLivePhoto(
enteFile.metadata.title,
fileBlob,
);
return renderableImageBlob(imageFileName, new Blob([imageData]));
} else {
// A layer above us should've already filtered these out.
throw new Error(`Cannot index unsupported file type ${fileType}`);
}
};

View File

@ -1,100 +1,114 @@
import type { Box } from "@/new/photos/services/ml/types";
import { blobCache } from "@/next/blob-cache";
import { ensure } from "@/utils/ensure";
import type { FaceAlignment } from "./index-face";
import type { EnteFile } from "../../types/file";
import { renderableImageBitmap } from "./bitmap";
import { type Box, type FaceIndex } from "./face";
import { clamp } from "./image";
export const saveFaceCrop = async (
imageBitmap: ImageBitmap,
faceID: string,
alignment: FaceAlignment,
/**
* Regenerate and locally save face crops for faces in the given file.
*
* Face crops (the rectangular regions of the original image where a particular
* face was detected) are not stored on remote and are generated on demand. On
* the client where the indexing occurred, they get generated during the face
* indexing pipeline itself. But we need to regenerate them locally if the user
* views that item on any other client.
*
* @param enteFile The {@link EnteFile} whose face crops we want to generate.
*
* @param faceIndex The {@link FaceIndex} containing information about the faces
* detected in the given image.
*
* The generated face crops are saved in a local cache and can subsequently be
* retrieved from the {@link BlobCache} named "face-crops".
*/
export const regenerateFaceCrops = async (
enteFile: EnteFile,
faceIndex: FaceIndex,
) => {
const faceCrop = extractFaceCrop(imageBitmap, alignment);
const blob = await imageBitmapToBlob(faceCrop);
faceCrop.close();
const imageBitmap = await renderableImageBitmap(enteFile);
const cache = await blobCache("face-crops");
await cache.put(faceID, blob);
return blob;
try {
await saveFaceCrops(imageBitmap, faceIndex);
} finally {
imageBitmap.close();
}
};
const imageBitmapToBlob = (imageBitmap: ImageBitmap) => {
const canvas = new OffscreenCanvas(imageBitmap.width, imageBitmap.height);
ensure(canvas.getContext("2d")).drawImage(imageBitmap, 0, 0);
/**
* Extract and locally save the face crops (the rectangle of the original image
* that contain the detected face) for each of the faces detected in an image.
*
* @param imageBitmap The original image.
*
* @param faceIndex The {@link FaceIndex} containing information about the faces
* detected in the given image.
*
* The face crops are saved in a local cache and can subsequently be retrieved
* from the {@link BlobCache} named "face-crops".
*/
export const saveFaceCrops = async (
imageBitmap: ImageBitmap,
faceIndex: FaceIndex,
) => {
const cache = await blobCache("face-crops");
return Promise.all(
faceIndex.faceEmbedding.faces.map(({ faceID, detection }) =>
extractFaceCrop(imageBitmap, detection.box).then((b) =>
cache.put(faceID, b),
),
),
);
};
/**
* Return the face crops corresponding to each of the given face detections.
*
* @param imageBitmap The original image.
*
* @param faceBox A box (a rectangle relative to the image size) marking the
* bounds of face in the given image.
*
* @returns a JPEG blob.
*/
export const extractFaceCrop = (imageBitmap: ImageBitmap, faceBox: Box) => {
const { width: imageWidth, height: imageHeight } = imageBitmap;
// The faceBox coordinates are normalized 0-1 relative to the image size,
// and we need to convert them back to absolute values first.
const faceX = faceBox.x * imageWidth;
const faceY = faceBox.y * imageHeight;
const faceWidth = faceBox.width * imageWidth;
const faceHeight = faceBox.height * imageHeight;
// Calculate the crop values by adding some padding around the face and
// making sure it's centered.
const regularPadding = 0.4;
const minimumPadding = 0.1;
const xCrop = faceX - faceWidth * regularPadding;
const xOvershoot = Math.abs(Math.min(0, xCrop)) / faceWidth;
const widthCrop =
faceWidth * (1 + 2 * regularPadding) -
2 * Math.min(xOvershoot, regularPadding - minimumPadding) * faceWidth;
const yCrop = faceY - faceHeight * regularPadding;
const yOvershoot = Math.abs(Math.min(0, yCrop)) / faceHeight;
const heightCrop =
faceHeight * (1 + 2 * regularPadding) -
2 * Math.min(yOvershoot, regularPadding - minimumPadding) * faceHeight;
// Prevent the crop from going out of image bounds.
const x = clamp(xCrop, 0, imageWidth);
const y = clamp(yCrop, 0, imageHeight);
const width = clamp(widthCrop, 0, imageWidth - x);
const height = clamp(heightCrop, 0, imageHeight - y);
const canvas = new OffscreenCanvas(width, height);
const ctx = ensure(canvas.getContext("2d"));
ctx.imageSmoothingQuality = "high";
ctx.drawImage(imageBitmap, x, y, width, height, 0, 0, width, height);
return canvas.convertToBlob({ type: "image/jpeg", quality: 0.8 });
};
const extractFaceCrop = (
imageBitmap: ImageBitmap,
alignment: FaceAlignment,
): ImageBitmap => {
// TODO-ML: This algorithm is different from what is used by the mobile app.
// Also, it needs to be something that can work fully using the embedding we
// receive from remote - the `alignment.boundingBox` will not be available
// to us in such cases.
const paddedBox = roundBox(enlargeBox(alignment.boundingBox, 1.5));
const outputSize = { width: paddedBox.width, height: paddedBox.height };
const maxDimension = 256;
const scale = Math.min(
maxDimension / paddedBox.width,
maxDimension / paddedBox.height,
);
if (scale < 1) {
outputSize.width = Math.round(scale * paddedBox.width);
outputSize.height = Math.round(scale * paddedBox.height);
}
const offscreen = new OffscreenCanvas(outputSize.width, outputSize.height);
const offscreenCtx = ensure(offscreen.getContext("2d"));
offscreenCtx.imageSmoothingQuality = "high";
offscreenCtx.translate(outputSize.width / 2, outputSize.height / 2);
const outputBox = {
x: -outputSize.width / 2,
y: -outputSize.height / 2,
width: outputSize.width,
height: outputSize.height,
};
const enlargedBox = enlargeBox(paddedBox, 1.5);
const enlargedOutputBox = enlargeBox(outputBox, 1.5);
offscreenCtx.drawImage(
imageBitmap,
enlargedBox.x,
enlargedBox.y,
enlargedBox.width,
enlargedBox.height,
enlargedOutputBox.x,
enlargedOutputBox.y,
enlargedOutputBox.width,
enlargedOutputBox.height,
);
return offscreen.transferToImageBitmap();
};
/** Round all the components of the box. */
const roundBox = (box: Box): Box => ({
x: Math.round(box.x),
y: Math.round(box.y),
width: Math.round(box.width),
height: Math.round(box.height),
});
/** Increase the size of the given {@link box} by {@link factor}. */
const enlargeBox = (box: Box, factor: number): Box => {
const center = { x: box.x + box.width / 2, y: box.y + box.height / 2 };
const newWidth = factor * box.width;
const newHeight = factor * box.height;
return {
x: center.x - newWidth / 2,
y: center.y - newHeight / 2,
width: newWidth,
height: newHeight,
};
};

View File

@ -1,6 +1,6 @@
import log from "@/next/log";
import { deleteDB, openDB, type DBSchema } from "idb";
import type { FaceIndex } from "./types";
import type { FaceIndex } from "./face";
/**
* Face DB schema.
@ -271,7 +271,7 @@ export const updateAssumingLocalFiles = async (
* These counts are mutually exclusive. The total number of files that fall
* within the purview of the indexer is thus indexable + indexed.
*/
export const indexedAndIndexableCounts = async () => {
export const indexableAndIndexedCounts = async () => {
const db = await faceDB();
const tx = db.transaction("file-status", "readwrite");
const indexableCount = await tx.store

View File

@ -13,8 +13,7 @@ import log from "@/next/log";
import { apiURL } from "@/next/origins";
import { z } from "zod";
import { saveFaceIndex } from "./db";
import { faceIndexingVersion } from "./index-face";
import { type FaceIndex } from "./types";
import { type FaceIndex, faceIndexingVersion } from "./face";
/**
* The embeddings that we (the current client) knows how to handle.

View File

@ -7,19 +7,8 @@
//
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { FILE_TYPE } from "@/media/file-type";
import { decodeLivePhoto } from "@/media/live-photo";
import DownloadManager from "@/new/photos/services/download";
import type {
Box,
Dimensions,
Face,
Point,
} from "@/new/photos/services/ml/types";
import type { EnteFile } from "@/new/photos/types/file";
import { getRenderableImage } from "@/new/photos/utils/file";
import log from "@/next/log";
import { workerBridge } from "@/next/worker/worker-bridge";
import { ensure } from "@/utils/ensure";
import { Matrix } from "ml-matrix";
import { getSimilarityTransformation } from "similarity-transformation";
@ -30,19 +19,185 @@ import {
translate,
type Matrix as TransformationMatrix,
} from "transformation-matrix";
import { saveFaceCrop } from "./crop";
import type { UploadItem } from "../upload/types";
import {
renderableImageBitmap,
renderableUploadItemImageBitmap,
} from "./bitmap";
import { saveFaceCrops } from "./crop";
import {
clamp,
grayscaleIntMatrixFromNormalized2List,
pixelRGBBilinear,
warpAffineFloat32List,
} from "./image";
import type { MLWorkerElectron } from "./worker-electron";
/**
* The version of the face indexing pipeline implemented by the current client.
*/
export const faceIndexingVersion = 1;
/**
* The faces in a file (and an embedding for each of them).
*
* This interface describes the format of both local and remote face data.
*
* - Local face detections and embeddings (collectively called as the face
* index) are generated by the current client when uploading a file (or when
* noticing a file which doesn't yet have a face index), stored in the local
* IndexedDB ("ml/db") and also uploaded (E2EE) to remote.
*
* - Remote embeddings are fetched by subsequent clients to avoid them having to
* reindex (indexing faces is a costly operation, esp for mobile clients).
*
* In both these scenarios (whether generated locally or fetched from remote),
* we end up with an face index described by this {@link FaceIndex} interface.
*
* It has a top level envelope with information about the file (in particular
* the primary key {@link fileID}), an inner envelope {@link faceEmbedding} with
* metadata about the indexing, and an array of {@link faces} each containing
* the result of a face detection and an embedding for that detected face.
*
* The word embedding is used to refer two things: The last one (faceEmbedding >
* faces > embedding) is the "actual" embedding, but sometimes we colloquially
* refer to the inner envelope (the "faceEmbedding") also an embedding since a
* file can have other types of embedding (envelopes), e.g. a "clipEmbedding".
*/
export interface FaceIndex {
/**
* The ID of the {@link EnteFile} whose index this is.
*
* This is used as the primary key when storing the index locally (An
* {@link EnteFile} is guaranteed to have its fileID be unique in the
* namespace of the user. Even if someone shares a file with the user the
* user will get a file entry with a fileID unique to them).
*/
fileID: number;
/**
* The width (in px) of the image (file).
*/
width: number;
/**
* The height (in px) of the image (file).
*/
height: number;
/**
* The "face embedding" for the file.
*
* This is an envelope that contains a list of indexed faces and metadata
* about the indexing.
*/
faceEmbedding: {
/**
* An integral version number of the indexing algorithm / pipeline.
*
* Clients agree out of band what a particular version means. The
* guarantee is that an embedding with a particular version will be the
* same (to negligible floating point epsilons) irrespective of the
* client that indexed the file.
*/
version: number;
/** The UA for the client which generated this embedding. */
client: string;
/** The list of faces (and their embeddings) detected in the file. */
faces: Face[];
};
}
/**
* A face detected in a file, and an embedding for this detected face.
*
* During face indexing, we first detect all the faces in a particular file.
* Then for each such detected region, we compute an embedding of that part of
* the file. Together, this detection region and the emedding travel together in
* this {@link Face} interface.
*/
export interface Face {
/**
* A unique identifier for the face.
*
* This ID is guaranteed to be unique for all the faces detected in all the
* files for the user. In particular, each file can have multiple faces but
* they all will get their own unique {@link faceID}.
*/
faceID: string;
/**
* The face detection. Describes the region within the image that was
* detected to be a face, and a set of landmarks (e.g. "eyes") of the
* detection.
*
* All coordinates are relative to and normalized by the image's dimension,
* i.e. they have been normalized to lie between 0 and 1, with 0 being the
* left (or top) and 1 being the width (or height) of the image.
*/
detection: {
/**
* The region within the image that contains the face.
*
* All coordinates and sizes are between 0 and 1, normalized by the
* dimensions of the image.
* */
box: Box;
/**
* Face "landmarks", e.g. eyes.
*
* The exact landmarks and their order depends on the face detection
* algorithm being used.
*
* The coordinatesare between 0 and 1, normalized by the dimensions of
* the image.
*/
landmarks: Point[];
};
/**
* An correctness probability (0 to 1) that the face detection algorithm
* gave to the detection. Higher values are better.
*/
score: number;
/**
* The computed blur for the detected face.
*
* The exact semantics and range for these (floating point) values depend on
* the face indexing algorithm / pipeline version being used.
* */
blur: number;
/**
* An embedding for the face.
*
* This is an opaque numeric (signed floating point) vector whose semantics
* and length depend on the version of the face indexing algorithm /
* pipeline that we are using. However, within a set of embeddings with the
* same version, the property is that two such embedding vectors will be
* "cosine similar" to each other if they are both faces of the same person.
*/
embedding: number[];
}
/** The x and y coordinates of a point. */
export interface Point {
x: number;
y: number;
}
/** The dimensions of something, say an image. */
export interface Dimensions {
width: number;
height: number;
}
/** A rectangle given by its top left coordinates and dimensions. */
export interface Box {
/** The x coordinate of the the top left (xMin). */
x: number;
/** The y coodinate of the top left (yMin). */
y: number;
/** The width of the box. */
width: number;
/** The height of the box. */
height: number;
}
/**
* Index faces in the given file.
*
@ -61,88 +216,62 @@ export const faceIndexingVersion = 1;
*
* @param enteFile The {@link EnteFile} to index.
*
* @param file The contents of {@link enteFile} as a web {@link File}, if
* available. These are used when they are provided, otherwise the file is
* downloaded and decrypted from remote.
* @param uploadItem If we're called during the upload process, then this will
* be set to the {@link UploadItem} that was uploaded. This way, we can directly
* use the on-disk file instead of needing to download the original from remote.
*
* @param electron The {@link MLWorkerElectron} instance that allows us to call
* our Node.js layer for various functionality.
*
* @param userAgent The UA of the client that is doing the indexing (us).
*/
export const indexFaces = async (
enteFile: EnteFile,
file: File | undefined,
uploadItem: UploadItem | undefined,
electron: MLWorkerElectron,
userAgent: string,
) => {
const imageBitmap = await renderableImageBlob(enteFile, file).then(
createImageBitmap,
);
): Promise<FaceIndex> => {
const imageBitmap = uploadItem
? await renderableUploadItemImageBitmap(enteFile, uploadItem, electron)
: await renderableImageBitmap(enteFile);
const { width, height } = imageBitmap;
const fileID = enteFile.id;
try {
return {
const faceIndex = {
fileID,
width,
height,
faceEmbedding: {
version: faceIndexingVersion,
client: userAgent,
faces: await indexFacesInBitmap(fileID, imageBitmap),
faces: await indexFacesInBitmap(fileID, imageBitmap, electron),
},
};
// This step, saving face crops, is not part of the indexing pipeline;
// we just do it here since we have already have the ImageBitmap at
// hand. Ignore errors that happen during this since it does not impact
// the generated face index.
try {
await saveFaceCrops(imageBitmap, faceIndex);
} catch (e) {
log.error(`Failed to save face crops for file ${fileID}`, e);
}
return faceIndex;
} finally {
imageBitmap.close();
}
};
/**
* Return a "renderable" image blob, using {@link file} if present otherwise
* downloading the source image corresponding to {@link enteFile} from remote.
*
* For videos their thumbnail is used.
*/
const renderableImageBlob = async (
enteFile: EnteFile,
file: File | undefined,
) => {
const fileType = enteFile.metadata.fileType;
if (fileType == FILE_TYPE.VIDEO) {
const thumbnailData = await DownloadManager.getThumbnail(enteFile);
return new Blob([ensure(thumbnailData)]);
} else {
return ensure(
file
? await getRenderableImage(enteFile.metadata.title, file)
: await fetchRenderableBlob(enteFile),
);
}
};
const fetchRenderableBlob = async (enteFile: EnteFile) => {
const fileStream = await DownloadManager.getFile(enteFile);
const fileBlob = await new Response(fileStream).blob();
const fileType = enteFile.metadata.fileType;
if (fileType == FILE_TYPE.IMAGE) {
return getRenderableImage(enteFile.metadata.title, fileBlob);
} else if (fileType == FILE_TYPE.LIVE_PHOTO) {
const { imageFileName, imageData } = await decodeLivePhoto(
enteFile.metadata.title,
fileBlob,
);
return getRenderableImage(imageFileName, new Blob([imageData]));
} else {
// A layer above us should've already filtered these out.
throw new Error(`Cannot index unsupported file type ${fileType}`);
}
};
const indexFacesInBitmap = async (
fileID: number,
imageBitmap: ImageBitmap,
electron: MLWorkerElectron,
): Promise<Face[]> => {
const { width, height } = imageBitmap;
const imageDimensions = { width, height };
const yoloFaceDetections = await detectFaces(imageBitmap);
const yoloFaceDetections = await detectFaces(imageBitmap, electron);
const partialResult = yoloFaceDetections.map(
({ box, landmarks, score }) => {
const faceID = makeFaceID(fileID, box, imageDimensions);
@ -151,28 +280,16 @@ const indexFacesInBitmap = async (
},
);
const alignments: FaceAlignment[] = [];
for (const { faceID, detection } of partialResult) {
const alignment = computeFaceAlignment(detection);
alignments.push(alignment);
// This step is not part of the indexing pipeline, we just do it here
// since we have already computed the face alignment. Ignore errors that
// happen during this since it does not impact the generated face index.
try {
await saveFaceCrop(imageBitmap, faceID, alignment);
} catch (e) {
log.error(`Failed to save face crop for faceID ${faceID}`, e);
}
}
const alignments = partialResult.map(({ detection }) =>
computeFaceAlignment(detection),
);
const alignedFacesData = convertToMobileFaceNetInput(
imageBitmap,
alignments,
);
const embeddings = await computeEmbeddings(alignedFacesData);
const embeddings = await computeEmbeddings(alignedFacesData, electron);
const blurs = detectBlur(
alignedFacesData,
partialResult.map((f) => f.detection),
@ -180,7 +297,7 @@ const indexFacesInBitmap = async (
return partialResult.map(({ faceID, detection, score }, i) => ({
faceID,
detection: normalizeToImageDimensions(detection, imageDimensions),
detection: normalizeByImageDimensions(detection, imageDimensions),
score,
blur: blurs[i]!,
embedding: Array.from(embeddings[i]!),
@ -194,6 +311,7 @@ const indexFacesInBitmap = async (
*/
const detectFaces = async (
imageBitmap: ImageBitmap,
electron: MLWorkerElectron,
): Promise<YOLOFaceDetection[]> => {
const rect = ({ width, height }: Dimensions) => ({
x: 0,
@ -204,7 +322,7 @@ const detectFaces = async (
const { yoloInput, yoloSize } =
convertToYOLOInputFloat32ChannelsFirst(imageBitmap);
const yoloOutput = await workerBridge.detectFaces(yoloInput);
const yoloOutput = await electron.detectFaces(yoloInput);
const faces = filterExtractDetectionsFromYOLOOutput(yoloOutput);
const faceDetections = transformYOLOFaceDetections(
faces,
@ -267,7 +385,7 @@ const convertToYOLOInputFloat32ChannelsFirst = (imageBitmap: ImageBitmap) => {
return { yoloInput, yoloSize };
};
export interface YOLOFaceDetection {
interface YOLOFaceDetection {
box: Box;
landmarks: Point[];
score: number;
@ -451,7 +569,7 @@ const makeFaceID = (fileID: number, box: Box, image: Dimensions) => {
return [`${fileID}`, xMin, yMin, xMax, yMax].join("_");
};
export interface FaceAlignment {
interface FaceAlignment {
/**
* An affine transformation matrix (rotation, translation, scaling) to align
* the face extracted from the image.
@ -762,8 +880,9 @@ const mobileFaceNetEmbeddingSize = 192;
*/
const computeEmbeddings = async (
faceData: Float32Array,
electron: MLWorkerElectron,
): Promise<Float32Array[]> => {
const outputData = await workerBridge.computeFaceEmbeddings(faceData);
const outputData = await electron.computeFaceEmbeddings(faceData);
const embeddingSize = mobileFaceNetEmbeddingSize;
const embeddings = new Array<Float32Array>(
@ -780,7 +899,7 @@ const computeEmbeddings = async (
/**
* Convert the coordinates to between 0-1, normalized by the image's dimensions.
*/
const normalizeToImageDimensions = (
const normalizeByImageDimensions = (
faceDetection: FaceDetection,
{ width, height }: Dimensions,
): FaceDetection => {

View File

@ -7,16 +7,16 @@ import {
isBetaUser,
isInternalUser,
} from "@/new/photos/services/feature-flags";
import {
clearFaceDB,
faceIndex,
indexedAndIndexableCounts,
} from "@/new/photos/services/ml/db";
import type { EnteFile } from "@/new/photos/types/file";
import { clientPackageName, isDesktop } from "@/next/app";
import { isDesktop } from "@/next/app";
import { blobCache } from "@/next/blob-cache";
import { ensureElectron } from "@/next/electron";
import log from "@/next/log";
import { ComlinkWorker } from "@/next/worker/comlink-worker";
import { proxy } from "comlink";
import type { UploadItem } from "../upload/types";
import { regenerateFaceCrops } from "./crop";
import { clearFaceDB, faceIndex, indexableAndIndexedCounts } from "./db";
import { MLWorker } from "./worker";
/**
@ -42,18 +42,21 @@ const worker = async () => {
};
const createComlinkWorker = async () => {
const electron = ensureElectron();
const mlWorkerElectron = {
appVersion: electron.appVersion,
detectFaces: electron.detectFaces,
computeFaceEmbeddings: electron.computeFaceEmbeddings,
};
const cw = new ComlinkWorker<typeof MLWorker>(
"ml",
"ML",
new Worker(new URL("worker.ts", import.meta.url)),
);
const ua = await getUserAgent();
await cw.remote.then((w) => w.init(ua));
await cw.remote.then((w) => w.init(proxy(mlWorkerElectron)));
return cw;
};
const getUserAgent = async () =>
`${clientPackageName}/${await ensureElectron().appVersion()}`;
/**
* Terminate {@link worker} (if any).
*
@ -163,20 +166,25 @@ export const triggerMLSync = () => {
};
/**
* Called by the uploader when it uploads a new file from this client.
* Run indexing on a file which was uploaded from this client.
*
* This function is called by the uploader when it uploads a new file from this
* client, giving us the opportunity to index it live. This is only an
* optimization - if we don't index it now it'll anyways get indexed later as
* part of the batch jobs, but that might require downloading the file's
* contents again.
*
* @param enteFile The {@link EnteFile} that got uploaded.
*
* @param file When available, the web {@link File} object representing the
* contents of the file that got uploaded.
* @param uploadItem The item that was uploaded. This can be used to get at the
* contents of the file that got uploaded. In case of live photos, this is the
* image part of the live photo that was uploaded.
*/
export const onUpload = (enteFile: EnteFile, file: File | undefined) => {
export const indexNewUpload = (enteFile: EnteFile, uploadItem: UploadItem) => {
if (!_isMLEnabled) return;
if (enteFile.metadata.fileType !== FILE_TYPE.IMAGE) return;
log.debug(() => ({ t: "ml-liveq", enteFile, file }));
// TODO-ML: 1. Use this file!
// TODO-ML: 2. Handle cases when File is something else (e.g. on desktop).
void worker().then((w) => w.onUpload(enteFile));
log.debug(() => ({ t: "ml/liveq", enteFile, uploadItem }));
void worker().then((w) => w.onUpload(enteFile, uploadItem));
};
export interface FaceIndexingStatus {
@ -210,7 +218,7 @@ export const faceIndexingStatus = async (): Promise<FaceIndexingStatus> => {
if (!isMLEnabled())
throw new Error("Cannot get indexing status when ML is not enabled");
const { indexedCount, indexableCount } = await indexedAndIndexableCounts();
const { indexedCount, indexableCount } = await indexableAndIndexedCounts();
const isIndexing = await (await worker()).isIndexing();
let phase: FaceIndexingStatus["phase"];
@ -237,3 +245,26 @@ export const unidentifiedFaceIDs = async (
const index = await faceIndex(enteFile.id);
return index?.faceEmbedding.faces.map((f) => f.faceID) ?? [];
};
/**
* Check to see if any of the faces in the given file do not have a face crop
* present locally. If so, then regenerate the face crops for all the faces in
* the file (updating the "face-crops" {@link BlobCache}).
*
* @returns true if one or more face crops were regenerated; false otherwise.
*/
export const regenerateFaceCropsIfNeeded = async (enteFile: EnteFile) => {
const index = await faceIndex(enteFile.id);
if (!index) return false;
const faceIDs = index.faceEmbedding.faces.map((f) => f.faceID);
const cache = await blobCache("face-crops");
for (const id of faceIDs) {
if (!(await cache.has(id))) {
await regenerateFaceCrops(enteFile, index);
return true;
}
}
return false;
};

View File

@ -1,159 +0,0 @@
/**
* The faces in a file (and an embedding for each of them).
*
* This interface describes the format of both local and remote face data.
*
* - Local face detections and embeddings (collectively called as the face
* index) are generated by the current client when uploading a file (or when
* noticing a file which doesn't yet have a face index), stored in the local
* IndexedDB ("ml/db") and also uploaded (E2EE) to remote.
*
* - Remote embeddings are fetched by subsequent clients to avoid them having to
* reindex (indexing faces is a costly operation, esp for mobile clients).
*
* In both these scenarios (whether generated locally or fetched from remote),
* we end up with an face index described by this {@link FaceIndex} interface.
*
* It has a top level envelope with information about the file (in particular
* the primary key {@link fileID}), an inner envelope {@link faceEmbedding} with
* metadata about the indexing, and an array of {@link faces} each containing
* the result of a face detection and an embedding for that detected face.
*
* The word embedding is used to refer two things: The last one (faceEmbedding >
* faces > embedding) is the "actual" embedding, but sometimes we colloquially
* refer to the inner envelope (the "faceEmbedding") also an embedding since a
* file can have other types of embedding (envelopes), e.g. a "clipEmbedding".
*/
export interface FaceIndex {
/**
* The ID of the {@link EnteFile} whose index this is.
*
* This is used as the primary key when storing the index locally (An
* {@link EnteFile} is guaranteed to have its fileID be unique in the
* namespace of the user. Even if someone shares a file with the user the
* user will get a file entry with a fileID unique to them).
*/
fileID: number;
/**
* The width (in px) of the image (file).
*/
width: number;
/**
* The height (in px) of the image (file).
*/
height: number;
/**
* The "face embedding" for the file.
*
* This is an envelope that contains a list of indexed faces and metadata
* about the indexing.
*/
faceEmbedding: {
/**
* An integral version number of the indexing algorithm / pipeline.
*
* Clients agree out of band what a particular version means. The
* guarantee is that an embedding with a particular version will be the
* same (to negligible floating point epsilons) irrespective of the
* client that indexed the file.
*/
version: number;
/** The UA for the client which generated this embedding. */
client: string;
/** The list of faces (and their embeddings) detected in the file. */
faces: Face[];
};
}
/**
* A face detected in a file, and an embedding for this detected face.
*
* During face indexing, we first detect all the faces in a particular file.
* Then for each such detected region, we compute an embedding of that part of
* the file. Together, this detection region and the emedding travel together in
* this {@link Face} interface.
*/
export interface Face {
/**
* A unique identifier for the face.
*
* This ID is guaranteed to be unique for all the faces detected in all the
* files for the user. In particular, each file can have multiple faces but
* they all will get their own unique {@link faceID}.
*/
faceID: string;
/**
* The face detection. Describes the region within the image that was
* detected to be a face, and a set of landmarks (e.g. "eyes") of the
* detection.
*
* All coordinates are relative to and normalized by the image's dimension,
* i.e. they have been normalized to lie between 0 and 1, with 0 being the
* left (or top) and 1 being the width (or height) of the image.
*/
detection: {
/**
* The region within the image that contains the face.
*
* All coordinates and sizes are between 0 and 1, normalized by the
* dimensions of the image.
* */
box: Box;
/**
* Face "landmarks", e.g. eyes.
*
* The exact landmarks and their order depends on the face detection
* algorithm being used.
*
* The coordinatesare between 0 and 1, normalized by the dimensions of
* the image.
*/
landmarks: Point[];
};
/**
* An correctness probability (0 to 1) that the face detection algorithm
* gave to the detection. Higher values are better.
*/
score: number;
/**
* The computed blur for the detected face.
*
* The exact semantics and range for these (floating point) values depend on
* the face indexing algorithm / pipeline version being used.
* */
blur: number;
/**
* An embedding for the face.
*
* This is an opaque numeric (signed floating point) vector whose semantics
* and length depend on the version of the face indexing algorithm /
* pipeline that we are using. However, within a set of embeddings with the
* same version, the property is that two such embedding vectors will be
* "cosine similar" to each other if they are both faces of the same person.
*/
embedding: number[];
}
/** The x and y coordinates of a point. */
export interface Point {
x: number;
y: number;
}
/** The dimensions of something, say an image. */
export interface Dimensions {
width: number;
height: number;
}
/** A rectangle given by its top left coordinates and dimensions. */
export interface Box {
/** The x coordinate of the the top left (xMin). */
x: number;
/** The y coodinate of the top left (yMin). */
y: number;
/** The width of the box. */
width: number;
/** The height of the box. */
height: number;
}

View File

@ -0,0 +1,13 @@
/**
* A subset of {@link Electron} provided to the {@link MLWorker}.
*
* `globalThis.electron` does not exist in the execution context of web workers.
* So instead, we manually provide a proxy object of type
* {@link MLWorkerElectron} that exposes a subset of the functions from
* {@link Electron} that are needed by the code running in the ML web worker.
*/
export interface MLWorkerElectron {
appVersion: () => Promise<string>;
detectFaces: (input: Float32Array) => Promise<Float32Array>;
computeFaceEmbeddings: (input: Float32Array) => Promise<Float32Array>;
}

View File

@ -1,26 +1,34 @@
import downloadManager from "@/new/photos/services/download";
import {
indexableFileIDs,
markIndexingFailed,
saveFaceIndex,
updateAssumingLocalFiles,
} from "@/new/photos/services/ml/db";
import type { FaceIndex } from "@/new/photos/services/ml/types";
import type { EnteFile } from "@/new/photos/types/file";
import { fileLogID } from "@/new/photos/utils/file";
import { clientPackageName } from "@/next/app";
import { getKVN } from "@/next/kv";
import { ensureAuthToken } from "@/next/local-user";
import log from "@/next/log";
import { ensure } from "@/utils/ensure";
import { wait } from "@/utils/promise";
import { expose } from "comlink";
import { fileLogID } from "../../utils/file";
import downloadManager from "../download";
import { getAllLocalFiles, getLocalTrashedFiles } from "../files";
import type { UploadItem } from "../upload/types";
import {
indexableFileIDs,
markIndexingFailed,
saveFaceIndex,
updateAssumingLocalFiles,
} from "./db";
import { pullFaceEmbeddings, putFaceIndex } from "./embedding";
import { indexFaces } from "./index-face";
import { type FaceIndex, indexFaces } from "./face";
import type { MLWorkerElectron } from "./worker-electron";
const idleDurationStart = 5; /* 5 seconds */
const idleDurationMax = 16 * 60; /* 16 minutes */
/** An entry in the liveQ maintained by the worker */
interface LiveQItem {
enteFile: EnteFile;
uploadItem: UploadItem;
}
/**
* Run operations related to machine learning (e.g. indexing) in a Web Worker.
*
@ -44,9 +52,10 @@ const idleDurationMax = 16 * 60; /* 16 minutes */
* - "idle": in between state transitions
*/
export class MLWorker {
private electron: MLWorkerElectron | undefined;
private userAgent: string | undefined;
private shouldSync = false;
private liveQ: EnteFile[] = [];
private liveQ: LiveQItem[] = [];
private state: "idle" | "pull" | "indexing" = "idle";
private idleTimeout: ReturnType<typeof setTimeout> | undefined;
private idleDuration = idleDurationStart; /* unit: seconds */
@ -57,11 +66,14 @@ export class MLWorker {
* This is conceptually the constructor, however it is easier to have this
* as a separate function to avoid confounding the comlink types too much.
*
* @param userAgent The user agent string to use as the client field in the
* embeddings generated during indexing by this client.
* @param electron The {@link MLWorkerElectron} that allows the worker to
* use the functionality provided by our Node.js layer when running in the
* context of our desktop app
*/
async init(userAgent: string) {
this.userAgent = userAgent;
async init(electron: MLWorkerElectron) {
this.electron = electron;
// Set the user agent that'll be set in the generated embeddings.
this.userAgent = `${clientPackageName}/${await electron.appVersion()}`;
// Initialize the downloadManager running in the web worker with the
// user's token. It'll be used to download files to index if needed.
await downloadManager.init(await ensureAuthToken());
@ -102,7 +114,7 @@ export class MLWorker {
* representation of the file's contents with us and won't need to download
* the file from remote.
*/
onUpload(file: EnteFile) {
onUpload(enteFile: EnteFile, uploadItem: UploadItem) {
// Add the recently uploaded file to the live indexing queue.
//
// Limit the queue to some maximum so that we don't keep growing
@ -113,11 +125,11 @@ export class MLWorker {
// long as we're not systematically ignoring it). This is because the
// live queue is just an optimization: if a file doesn't get indexed via
// the live queue, it'll later get indexed anyway when we backfill.
if (this.liveQ.length < 50) {
this.liveQ.push(file);
if (this.liveQ.length < 200) {
this.liveQ.push({ enteFile, uploadItem });
this.wakeUp();
} else {
log.debug(() => "Ignoring live item since liveQ is full");
log.debug(() => "Ignoring upload item since liveQ is full");
}
}
@ -130,7 +142,7 @@ export class MLWorker {
private async tick() {
log.debug(() => ({
t: "ml-tick",
t: "ml/tick",
state: this.state,
shouldSync: this.shouldSync,
liveQ: this.liveQ,
@ -156,7 +168,11 @@ export class MLWorker {
const liveQ = this.liveQ;
this.liveQ = [];
this.state = "indexing";
const allSuccess = await indexNextBatch(ensure(this.userAgent), liveQ);
const allSuccess = await indexNextBatch(
liveQ,
ensure(this.electron),
ensure(this.userAgent),
);
if (allSuccess) {
// Everything is running smoothly. Reset the idle duration.
this.idleDuration = idleDurationStart;
@ -197,7 +213,11 @@ const pull = pullFaceEmbeddings;
* Which means that when it returns true, all is well and there are more
* things pending to process, so we should chug along at full speed.
*/
const indexNextBatch = async (userAgent: string, liveQ: EnteFile[]) => {
const indexNextBatch = async (
liveQ: LiveQItem[],
electron: MLWorkerElectron,
userAgent: string,
) => {
if (!self.navigator.onLine) {
log.info("Skipping ML indexing since we are not online");
return false;
@ -205,16 +225,23 @@ const indexNextBatch = async (userAgent: string, liveQ: EnteFile[]) => {
const userID = ensure(await getKVN("userID"));
const files =
// Use the liveQ if present, otherwise get the next batch to backfill.
const items =
liveQ.length > 0
? liveQ
: await syncWithLocalFilesAndGetFilesToIndex(userID, 200);
if (files.length == 0) return false;
: await syncWithLocalFilesAndGetFilesToIndex(userID, 200).then(
(fs) =>
fs.map((f) => ({ enteFile: f, uploadItem: undefined })),
);
// Nothing to do.
if (items.length == 0) return false;
// Index, keeping track if any of the items failed.
let allSuccess = true;
for (const file of files) {
for (const { enteFile, uploadItem } of items) {
try {
await index(file, undefined, userAgent);
await index(enteFile, uploadItem, electron, userAgent);
// Possibly unnecessary, but let us drain the microtask queue.
await wait(0);
} catch {
@ -222,6 +249,7 @@ const indexNextBatch = async (userAgent: string, liveQ: EnteFile[]) => {
}
}
// Return true if nothing failed.
return allSuccess;
};
@ -264,7 +292,7 @@ const syncWithLocalFilesAndGetFilesToIndex = async (
*
* @param enteFile The {@link EnteFile} to index.
*
* @param file If the file is one which is being uploaded from the current
* @param uploadItem If the file is one which is being uploaded from the current
* client, then we will also have access to the file's content. In such
* cases, pass a web {@link File} object to use that its data directly for
* face indexing. If this is not provided, then the file's contents will be
@ -274,7 +302,8 @@ const syncWithLocalFilesAndGetFilesToIndex = async (
*/
export const index = async (
enteFile: EnteFile,
file: File | undefined,
uploadItem: UploadItem | undefined,
electron: MLWorkerElectron,
userAgent: string,
) => {
const f = fileLogID(enteFile);
@ -282,7 +311,7 @@ export const index = async (
let faceIndex: FaceIndex;
try {
faceIndex = await indexFaces(enteFile, file, userAgent);
faceIndex = await indexFaces(enteFile, uploadItem, electron, userAgent);
} catch (e) {
// Mark indexing as having failed only if the indexing itself
// failed, not if there were subsequent failures (like when trying

View File

@ -63,10 +63,38 @@ export function mergeMetadata(files: EnteFile[]): EnteFile[] {
}
/**
* The returned blob.type is filled in, whenever possible, with the MIME type of
* Return a new {@link Blob} containing data in a format that the browser
* (likely) knows how to render (in an img tag, or on the canvas).
*
* The type of the returned blob is set, whenever possible, to the MIME type of
* the data that we're dealing with.
*
* @param fileName The name of the file whose data is {@link imageBlob}.
*
* @param imageBlob A {@link Blob} containing the contents of an image file.
*
* The logic used by this function is:
*
* 1. Try to detect the MIME type of the file from its contents and/or name.
*
* 2. If this detected type is one of the types that we know that the browser
* doesn't know how to render, continue. Otherwise return the imageBlob that
* was passed in (after setting its MIME type).
*
* 3. If we're running in our desktop app and this MIME type is something our
* desktop app can natively convert to a JPEG (using ffmpeg), do that and
* return the resultant JPEG blob.
*
* 4. If this is an HEIC file, use our (WASM) HEIC converter and return the
* resultant JPEG blob.
*
* 5. Otherwise (or if any error occurs in the aforementioned steps), return
* `undefined`.
*/
export const getRenderableImage = async (fileName: string, imageBlob: Blob) => {
export const renderableImageBlob = async (
fileName: string,
imageBlob: Blob,
) => {
try {
const tempFile = new File([imageBlob], fileName);
const fileTypeInfo = await detectFileTypeInfo(tempFile);

View File

@ -7,6 +7,7 @@
*/
import type { Electron, ZipItem } from "@/next/types/ipc";
import type { MLWorkerElectron } from "../services/ml/worker-electron";
/**
* Stream the given file or zip entry from the user's local file system.
@ -16,7 +17,8 @@ import type { Electron, ZipItem } from "@/next/types/ipc";
* See: [Note: IPC streams].
*
* To avoid accidentally invoking it in a non-desktop app context, it requires
* the {@link Electron} object as a parameter (even though it doesn't use it).
* the {@link Electron} (or a functionally similar) object as a parameter (even
* though it doesn't use it).
*
* @param pathOrZipItem Either the path on the file on the user's local file
* system whose contents we want to stream. Or a tuple containing the path to a
@ -34,7 +36,7 @@ import type { Electron, ZipItem } from "@/next/types/ipc";
* reading, expressed as epoch milliseconds.
*/
export const readStream = async (
_: Electron,
_: Electron | MLWorkerElectron,
pathOrZipItem: string | ZipItem,
): Promise<{ response: Response; size: number; lastModifiedMs: number }> => {
let url: URL;

View File

@ -56,6 +56,10 @@ export interface BlobCache {
* Get the data corresponding to {@link key} (if found) from the cache.
*/
get: (key: string) => Promise<Blob | undefined>;
/**
* Check if there is an item corresponding to {@link key} in the cache.
*/
has: (key: string) => Promise<boolean>;
/**
* Add the given {@link key}-value ({@link blob}) pair to the cache.
*/
@ -153,6 +157,7 @@ const openWebCache = async (name: BlobCacheNamespace) => {
const res = await cache.match(key);
return await res?.blob();
},
has: async (key: string) => cache.match(key).then((v) => !!v),
put: (key: string, blob: Blob) => cache.put(key, new Response(blob)),
delete: (key: string) => cache.delete(key),
};
@ -184,6 +189,16 @@ const openOPFSCacheWeb = async (name: BlobCacheNamespace) => {
throw e;
}
},
has: async (key: string) => {
try {
await cache.getFileHandle(key);
return true;
} catch (e) {
if (e instanceof DOMException && e.name == "NotFoundError")
return false;
throw e;
}
},
put: async (key: string, blob: Blob) => {
const fileHandle = await cache.getFileHandle(key, {
create: true,
@ -205,25 +220,6 @@ const openOPFSCacheWeb = async (name: BlobCacheNamespace) => {
};
};
/**
* Return a cached blob for {@link key} in {@link cacheName}. If the blob is not
* found in the cache, recreate/fetch it using {@link get}, cache it, and then
* return it.
*/
export const cachedOrNew = async (
cacheName: BlobCacheNamespace,
key: string,
get: () => Promise<Blob>,
): Promise<Blob> => {
const cache = await openBlobCache(cacheName);
const cachedBlob = await cache.get(key);
if (cachedBlob) return cachedBlob;
const blob = await get();
await cache.put(key, blob);
return blob;
};
/**
* Delete all cached data, including cached caches.
*

View File

@ -59,8 +59,10 @@ export const fileNameFromComponents = (components: FileNameComponents) =>
*/
export const basename = (path: string) => {
const pathComponents = path.split("/");
for (let i = pathComponents.length - 1; i >= 0; i--)
if (pathComponents[i] !== "") return pathComponents[i];
for (let i = pathComponents.length - 1; i >= 0; i--) {
const component = pathComponents[i];
if (component && component.length > 0) return component;
}
return path;
};

View File

@ -30,6 +30,8 @@ export class HTTPError extends Error {
constructor(url: string, res: Response) {
super(`Failed to fetch ${url}: HTTP ${res.status}`);
// Cargo culted from
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#custom_error_types
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (Error.captureStackTrace) Error.captureStackTrace(this, HTTPError);

View File

@ -3,9 +3,34 @@ import log, { logToDisk } from "@/next/log";
import { expose, wrap, type Remote } from "comlink";
import { ensureLocalUser } from "../local-user";
/**
* A minimal wrapper for a web {@link Worker}, proxying a class of type T.
*
* `comlink` is a library that simplies working with web workers by
* transparently proxying objects across the boundary instead of us needing to
* work directly with the raw postMessage interface.
*
* This class is a thin wrapper over a common usage pattern of comlink. It takes
* a web worker ({@link Worker}) that is expected to have {@link expose}-ed a
* class of type T. It then makes available the main thread handle to this class
* as the {@link remote} property.
*
* It also exposes an object of type {@link WorkerBridge} _to_ the code running
* inside the web worker.
*
* It all gets a bit confusing sometimes, so here is a legend of the parties
* involved:
*
* - ComlinkWorker (wraps the web worker)
* - Web `Worker` (exposes class T)
* - ComlinkWorker.remote (the exposed class T running inside the web worker)
*/
export class ComlinkWorker<T extends new () => InstanceType<T>> {
/** The class (T) exposed by the web worker */
public remote: Promise<Remote<InstanceType<T>>>;
/** The web worker */
private worker: Worker;
/** An arbitrary name associated with this ComlinkWorker for debugging. */
private name: string;
constructor(name: string, worker: Worker) {
@ -17,7 +42,7 @@ export class ComlinkWorker<T extends new () => InstanceType<T>> {
`Got error event from worker: ${JSON.stringify({ event, name })}`,
);
};
log.debug(() => `Initiated web worker ${name}`);
log.debug(() => `Created ${name} web worker`);
const comlink = wrap<T>(worker);
this.remote = new comlink() as Promise<Remote<InstanceType<T>>>;
expose(workerBridge, worker);
@ -25,30 +50,27 @@ export class ComlinkWorker<T extends new () => InstanceType<T>> {
public terminate() {
this.worker.terminate();
log.debug(() => `Terminated web worker ${this.name}`);
log.debug(() => `Terminated ${this.name} web worker`);
}
}
/**
* A set of utility functions that we expose to all workers that we create.
* A set of utility functions that we expose to all web workers that we create.
*
* Inside the worker's code, this can be accessed by using the sibling
* `workerBridge` object after importing it from `worker-bridge.ts`.
*
* Not all workers need access to all these functions, and this can indeed be
* done in a more fine-grained, per-worker, manner if needed. For now, since it
* is a motley bunch, we just inject them all.
* is a motley bunch, we just inject them all to all workers.
*/
const workerBridge = {
// Needed: generally (presumably)
// Needed by all workers (likely, not necessarily).
logToDisk,
// Needed by ML worker
// Needed by MLWorker.
getAuthToken: () => ensureLocalUser().token,
convertToJPEG: (imageData: Uint8Array) =>
ensureElectron().convertToJPEG(imageData),
detectFaces: (input: Float32Array) => ensureElectron().detectFaces(input),
computeFaceEmbeddings: (input: Float32Array) =>
ensureElectron().computeFaceEmbeddings(input),
};
export type WorkerBridge = typeof workerBridge;