Merge branch 'main' into placeholder_for_link

This commit is contained in:
Aman Raj Singh Mourya 2024-05-27 14:39:37 +05:30
commit acf7b3a865
503 changed files with 22562 additions and 6275 deletions

View file

@ -4,11 +4,12 @@ labels: ["triage"]
body:
- type: markdown
attributes:
value: >
Before opening a new issue, please ensure you are on the latest
version (it might've already been fixed), and that you've searched
for existing issues (please add you observations as a comment
there instead of creating a duplicate).
value: |
Before opening a new bug report, please ensure
1. you are on the latest version (it might've already been fixed),
2. you've searched for existing issues (please add your observations as a comment there instead of creating a duplicate).
If you are self hosting, please create a community [Q&A](https://github.com/ente-io/ente/discussions/categories/q-a) instead.
- type: textarea
attributes:
label: Description
@ -16,7 +17,8 @@ body:
Please describe the bug. If possible, also include the steps to
reproduce the behaviour, and the expected behaviour (sometimes
bugs are just expectation mismatches, in which case this would be
a good fit for Discussions).
a good fit for [feature
requests](https://github.com/ente-io/ente/discussions/categories/feature-requests)).
validations:
required: true
- type: input

View file

@ -4,7 +4,7 @@ on:
workflow_dispatch: # Allow manually running the action
env:
FLUTTER_VERSION: "3.19.3"
FLUTTER_VERSION: "3.22.0"
jobs:
build:

View file

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

6
.gitignore vendored
View file

@ -1,8 +1,6 @@
# Let folks use their custom .vscode settings
# Let folks use their custom editor settings
.vscode
.idea
# macOS
.DS_Store
.idea
.ente.authenticator.db
.ente.offline_authenticator.db

View file

@ -1,7 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application android:name="${applicationName}"
android:label="auth"
android:label="Auth"
android:icon="@mipmap/launcher_icon"
android:usesCleartextTraffic="true"
android:requestLegacyExternalStorage="true"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 801 KiB

View file

@ -20,7 +20,7 @@
<string>es</string>
</array>
<key>CFBundleName</key>
<string>auth</string>
<string>Auth</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>

View file

@ -189,7 +189,7 @@ class _AppState extends State<App> with WindowListener, TrayListener {
windowManager.show();
break;
case 'exit_app':
windowManager.close();
windowManager.destroy();
break;
}
}

View file

@ -20,6 +20,8 @@
"codeIssuerHint": "発行者",
"codeSecretKeyHint": "秘密鍵",
"codeAccountHint": "アカウント (you@domain.com)",
"codeTagHint": "タグ",
"accountKeyType": "鍵の種類",
"sessionExpired": "セッションが失効しました",
"@sessionExpired": {
"description": "Title of the dialog when the users current session is invalid/expired"
@ -77,6 +79,7 @@
"data": "データ",
"importCodes": "コードをインポート",
"importTypePlainText": "プレーンテキスト",
"importTypeEnteEncrypted": "Ente 暗号化されたエクスポート",
"passwordForDecryptingExport": "復号化用パスワード",
"passwordEmptyError": "パスワードは空欄にできません",
"importFromApp": "{appName} からコードをインポート",
@ -121,6 +124,7 @@
"suggestFeatures": "機能を提案",
"faq": "FAQ",
"faq_q_1": "Authはどのくらい安全ですか",
"faq_a_1": "Ente Authでバックアップされたコードはすべてエンドツーエンドで暗号化されて保存されます。つまり、コードにアクセスできるのはあなただけです。当社のアプリはオープンソースであり、暗号化技術は外部監査を受けています。",
"faq_q_2": "パソコンから私のコードにアクセスできますか?",
"faq_a_2": "auth.ente.io で Web からコードにアクセス可能です。",
"faq_q_3": "コードを削除するにはどうすればいいですか?",
@ -154,6 +158,7 @@
}
}
},
"invalidQRCode": "QRコードが無効です",
"noRecoveryKeyTitle": "回復キーがありませんか?",
"enterEmailHint": "メールアドレスを入力してください",
"invalidEmailTitle": "メールアドレスが無効です",
@ -347,6 +352,7 @@
"deleteCodeAuthMessage": "コードを削除するためには認証が必要です",
"showQRAuthMessage": "QR コードを表示するためには認証が必要です",
"confirmAccountDeleteTitle": "アカウントの削除に同意",
"confirmAccountDeleteMessage": "このアカウントは他のEnteアプリも使用している場合はそれらにも紐づけされています。\nすべてのEnteアプリでアップロードされたデータは削除され、アカウントは完全に削除されます。",
"androidBiometricHint": "本人を確認する",
"@androidBiometricHint": {
"description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters."
@ -417,5 +423,18 @@
"invalidEndpoint": "無効なエンドポイントです",
"invalidEndpointMessage": "入力されたエンドポイントは無効です。有効なエンドポイントを入力して再試行してください。",
"endpointUpdatedMessage": "エンドポイントの更新に成功しました",
"customEndpoint": "{endpoint} に接続しました"
"customEndpoint": "{endpoint} に接続しました",
"pinText": "固定",
"unpinText": "固定を解除",
"pinnedCodeMessage": "{code} を固定しました",
"unpinnedCodeMessage": "{code} の固定が解除されました",
"tags": "タグ",
"createNewTag": "新しいタグの作成",
"tag": "タグ",
"create": "作成",
"editTag": "タグの編集",
"deleteTagTitle": "タグを削除しますか?",
"deleteTagMessage": "このタグを削除してもよろしいですか?この操作は取り消しできません。",
"somethingWentWrongParsingCode": "{x} のコードを解析できませんでした。",
"updateNotAvailable": "アップデートは利用できません"
}

View file

@ -20,6 +20,8 @@
"codeIssuerHint": "Emissor",
"codeSecretKeyHint": "Chave secreta",
"codeAccountHint": "Conta (voce@dominio.com)",
"codeTagHint": "Etiqueta",
"accountKeyType": "Tipo de chave",
"sessionExpired": "Sessão expirada",
"@sessionExpired": {
"description": "Title of the dialog when the users current session is invalid/expired"
@ -156,6 +158,7 @@
}
}
},
"invalidQRCode": "QR Code inválido",
"noRecoveryKeyTitle": "Sem chave de recuperação?",
"enterEmailHint": "Insira o seu endereço de e-mail",
"invalidEmailTitle": "Endereço de e-mail inválido",
@ -420,5 +423,16 @@
"invalidEndpoint": "Endpoint inválido",
"invalidEndpointMessage": "Desculpe, o endpoint que você inseriu é inválido. Por favor, insira um endpoint válido e tente novamente.",
"endpointUpdatedMessage": "Endpoint atualizado com sucesso",
"customEndpoint": "Conectado a {endpoint}"
"customEndpoint": "Conectado a {endpoint}",
"pinText": "Fixar",
"pinnedCodeMessage": "{code} foi fixado",
"tags": "Etiquetas",
"createNewTag": "Criar etiqueta",
"tag": "Etiqueta",
"create": "Criar",
"editTag": "Editar etiqueta",
"deleteTagTitle": "Excluir etiqueta?",
"deleteTagMessage": "Tem certeza de que deseja excluir esta etiqueta? Essa ação é irreversível.",
"somethingWentWrongParsingCode": "Não foi possível analisar os códigos {x}.",
"updateNotAvailable": "Atualização não está disponível"
}

View file

@ -20,6 +20,8 @@
"codeIssuerHint": "发行人",
"codeSecretKeyHint": "私钥",
"codeAccountHint": "账户 (you@domain.com)",
"codeTagHint": "标签",
"accountKeyType": "密钥类型",
"sessionExpired": "会话已过期",
"@sessionExpired": {
"description": "Title of the dialog when the users current session is invalid/expired"
@ -156,6 +158,7 @@
}
}
},
"invalidQRCode": "二维码无效",
"noRecoveryKeyTitle": "没有恢复密钥吗?",
"enterEmailHint": "请输入您的电子邮件地址",
"invalidEmailTitle": "无效的电子邮件地址",
@ -420,5 +423,18 @@
"invalidEndpoint": "端点无效",
"invalidEndpointMessage": "抱歉,您输入的端点无效。请输入有效的端点,然后重试。",
"endpointUpdatedMessage": "端点更新成功",
"customEndpoint": "已连接至 {endpoint}"
"customEndpoint": "已连接至 {endpoint}",
"pinText": "置顶",
"unpinText": "取消置顶",
"pinnedCodeMessage": "{code} 已被置顶",
"unpinnedCodeMessage": "{code} 已被取消置顶",
"tags": "标签",
"createNewTag": "创建新标签",
"tag": "标签",
"create": "创建",
"editTag": "编辑标签",
"deleteTagTitle": "要删除标签吗?",
"deleteTagMessage": "您确定要删除此标签吗?此操作是不可逆的。",
"somethingWentWrongParsingCode": "我们无法解析 {x} 代码。",
"updateNotAvailable": "更新不可用"
}

View file

@ -125,10 +125,10 @@ class Code {
final issuer = _getIssuer(uri);
try {
return Code(
final code = Code(
_getAccount(uri),
issuer,
_getDigits(uri, issuer),
_getDigits(uri),
_getPeriod(uri),
getSanitizedSecret(uri.queryParameters['secret']!),
_getAlgorithm(uri),
@ -137,6 +137,7 @@ class Code {
rawData,
display: CodeDisplay.fromUri(uri) ?? CodeDisplay(),
);
return code;
} catch (e) {
// if account name contains # without encoding,
// rest of the url are treated as url fragment
@ -174,12 +175,11 @@ class Code {
}
String toOTPAuthUrlFormat() {
final uri = Uri.parse(rawData);
final uri = Uri.parse(rawData.replaceAll("#", '%23'));
final query = {...uri.queryParameters};
query["codeDisplay"] = jsonEncode(display.toJson());
final newUri = uri.replace(queryParameters: query);
return jsonEncode(newUri.toString());
}
@ -201,11 +201,11 @@ class Code {
}
}
static int _getDigits(Uri uri, String issuer) {
static int _getDigits(Uri uri) {
try {
return int.parse(uri.queryParameters['digits']!);
} catch (e) {
if (issuer.toLowerCase() == "steam") {
if (uri.host == "steam") {
return steamDigits;
}
return defaultDigits;

View file

@ -1,6 +1,7 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:logging/logging.dart';
/// Used to store the display settings of a code.
class CodeDisplay {
@ -54,13 +55,34 @@ class CodeDisplay {
);
}
static CodeDisplay? fromUri(Uri uri) {
/// Converts the [CodeDisplay] to a json object.
/// When [safeParsing] is true, the json will be parsed safely.
/// If we fail to parse the json, we will return an empty [CodeDisplay].
static CodeDisplay? fromUri(Uri uri, {bool safeParsing = false}) {
if (!uri.queryParameters.containsKey("codeDisplay")) return null;
final String codeDisplay =
uri.queryParameters['codeDisplay']!.replaceAll('%2C', ',');
final decodedDisplay = jsonDecode(codeDisplay);
return _parseCodeDisplayJson(codeDisplay, safeParsing);
}
return CodeDisplay.fromJson(decodedDisplay);
static CodeDisplay _parseCodeDisplayJson(String json, bool safeParsing) {
try {
final decodedDisplay = jsonDecode(json);
return CodeDisplay.fromJson(decodedDisplay);
} catch (e, s) {
Logger("CodeDisplay")
.severe("Could not parse code display from json", e, s);
// (ng/prateek) Handle the case where we have fragment in the rawDataUrl
if (!json.endsWith("}") && json.contains("}#")) {
Logger("CodeDisplay").warning("ignoring code display as it's invalid");
return CodeDisplay();
}
if (safeParsing) {
return CodeDisplay();
} else {
rethrow;
}
}
}
Map<String, dynamic> toJson() {

View file

@ -240,7 +240,7 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
final account = _accountController.text.trim();
final issuer = _issuerController.text.trim();
final secret = _secretController.text.trim().replaceAll(' ', '');
final isStreamCode = issuer.toLowerCase() == "steam";
final isStreamCode = issuer.toLowerCase() == "steam" || issuer.toLowerCase().contains('steampowered.com');
if (widget.code != null && widget.code!.secret != secret) {
ButtonResult? result = await showChoiceActionSheet(
context,

View file

@ -41,9 +41,9 @@ class CodeStore {
} else {
code = Code.fromExportJson(decodeJson);
}
} catch (e) {
} catch (e, s) {
code = Code.withError(e, entity.rawData);
_logger.severe("Could not parse code", code.err);
_logger.severe("Could not parse code", e, s);
}
code.generatedID = entity.generatedID;
code.hasSynced = entity.hasSynced;

View file

@ -48,7 +48,6 @@ class _CodeWidgetState extends State<CodeWidget> {
late bool _shouldShowLargeIcon;
late bool _hideCode;
bool isMaskingEnabled = false;
late final colorScheme = getEnteColorScheme(context);
@override
void initState() {
@ -78,6 +77,7 @@ class _CodeWidgetState extends State<CodeWidget> {
@override
Widget build(BuildContext context) {
final colorScheme = getEnteColorScheme(context);
if (isMaskingEnabled != PreferenceService.instance.shouldHideCodes()) {
isMaskingEnabled = PreferenceService.instance.shouldHideCodes();
_hideCode = isMaskingEnabled;
@ -91,6 +91,100 @@ class _CodeWidgetState extends State<CodeWidget> {
_isInitialized = true;
}
final l10n = context.l10n;
Widget getCardContents(AppLocalizations l10n) {
return Stack(
children: [
if (widget.code.isPinned)
Align(
alignment: Alignment.topRight,
child: CustomPaint(
painter: PinBgPainter(
color: colorScheme.pinnedBgColor,
),
size: const Size(39, 39),
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (widget.code.type.isTOTPCompatible)
CodeTimerProgress(
period: widget.code.period,
),
const SizedBox(height: 16),
Row(
children: [
_shouldShowLargeIcon ? _getIcon() : const SizedBox.shrink(),
Expanded(
child: Column(
children: [
_getTopRow(),
const SizedBox(height: 4),
_getBottomRow(l10n),
],
),
),
],
),
const SizedBox(
height: 20,
),
],
),
if (widget.code.isPinned) ...[
Align(
alignment: Alignment.topRight,
child: Padding(
padding: const EdgeInsets.only(right: 6, top: 6),
child: SvgPicture.asset("assets/svg/pin-card.svg"),
),
),
],
],
);
}
Widget clippedCard(AppLocalizations l10n) {
return Container(
height: 132,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: Theme.of(context).colorScheme.codeCardBackgroundColor,
boxShadow:
widget.code.isPinned ? colorScheme.pinnedCardBoxShadow : [],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Material(
color: Colors.transparent,
child: InkWell(
customBorder: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
onTap: () {
_copyCurrentOTPToClipboard();
},
onDoubleTap: isMaskingEnabled
? () {
setState(
() {
_hideCode = !_hideCode;
},
);
}
: null,
onLongPress: () {
_copyCurrentOTPToClipboard();
},
child: getCardContents(l10n),
),
),
),
);
}
return Container(
margin: const EdgeInsets.only(left: 16, right: 16, bottom: 8, top: 8),
child: Builder(
@ -126,7 +220,7 @@ class _CodeWidgetState extends State<CodeWidget> {
],
padding: const EdgeInsets.all(8.0),
),
child: _clippedCard(l10n),
child: clippedCard(l10n),
);
}
@ -216,7 +310,7 @@ class _CodeWidgetState extends State<CodeWidget> {
],
),
child: Builder(
builder: (context) => _clippedCard(l10n),
builder: (context) => clippedCard(l10n),
),
);
},
@ -224,98 +318,6 @@ class _CodeWidgetState extends State<CodeWidget> {
);
}
Widget _clippedCard(AppLocalizations l10n) {
return Container(
height: 132,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: Theme.of(context).colorScheme.codeCardBackgroundColor,
boxShadow: widget.code.isPinned ? colorScheme.pinnedCardBoxShadow : [],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Material(
color: Colors.transparent,
child: InkWell(
customBorder: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
onTap: () {
_copyCurrentOTPToClipboard();
},
onDoubleTap: isMaskingEnabled
? () {
setState(
() {
_hideCode = !_hideCode;
},
);
}
: null,
onLongPress: () {
_copyCurrentOTPToClipboard();
},
child: _getCardContents(l10n),
),
),
),
);
}
Widget _getCardContents(AppLocalizations l10n) {
return Stack(
children: [
if (widget.code.isPinned)
Align(
alignment: Alignment.topRight,
child: CustomPaint(
painter: PinBgPainter(
color: colorScheme.pinnedBgColor,
),
size: const Size(39, 39),
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (widget.code.type == Type.totp)
CodeTimerProgress(
period: widget.code.period,
),
const SizedBox(height: 16),
Row(
children: [
_shouldShowLargeIcon ? _getIcon() : const SizedBox.shrink(),
Expanded(
child: Column(
children: [
_getTopRow(),
const SizedBox(height: 4),
_getBottomRow(l10n),
],
),
),
],
),
const SizedBox(
height: 20,
),
],
),
if (widget.code.isPinned) ...[
Align(
alignment: Alignment.topRight,
child: Padding(
padding: const EdgeInsets.only(right: 6, top: 6),
child: SvgPicture.asset("assets/svg/pin-card.svg"),
),
),
],
],
);
}
Widget _getBottomRow(AppLocalizations l10n) {
return Container(
padding: const EdgeInsets.only(left: 16, right: 16),
@ -585,7 +587,7 @@ class _CodeWidgetState extends State<CodeWidget> {
String _getFormattedCode(String code) {
if (_hideCode) {
// replace all digits with
code = code.replaceAll(RegExp(r'\d'), '');
code = code.replaceAll(RegExp(r'\S'), '');
}
if (code.length == 6) {
return "${code.substring(0, 3)} ${code.substring(3, 6)}";

View file

@ -1,8 +1,12 @@
import 'package:ente_auth/models/code.dart';
import 'package:flutter/foundation.dart';
import 'package:otp/otp.dart' as otp;
import 'package:steam_totp/steam_totp.dart';
String getOTP(Code code) {
if (code.type == Type.steam) {
return _getSteamCode(code);
}
if (code.type == Type.hotp) {
return _getHOTPCode(code);
}
@ -26,7 +30,18 @@ String _getHOTPCode(Code code) {
);
}
String _getSteamCode(Code code, [bool isNext = false]) {
final SteamTOTP steamtotp = SteamTOTP(secret: code.secret);
return steamtotp.generate(
DateTime.now().millisecondsSinceEpoch ~/ 1000 + (isNext ? code.period : 0),
);
}
String getNextTotp(Code code) {
if (code.type == Type.steam) {
return _getSteamCode(code, true);
}
return otp.OTP.generateTOTPCodeString(
getSanitizedSecret(code.secret),
DateTime.now().millisecondsSinceEpoch + code.period * 1000,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 240 B

After

Width:  |  Height:  |  Size: 324 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 628 B

After

Width:  |  Height:  |  Size: 780 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -745,6 +745,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.0"
hashlib:
dependency: transitive
description:
name: hashlib
sha256: "67e640e19cc33070113acab3125cd48ebe480a0300e15554dec089b8878a729f"
url: "https://pub.dev"
source: hosted
version: "1.16.0"
hashlib_codecs:
dependency: transitive
description:
name: hashlib_codecs
sha256: "49e2a471f74b15f1854263e58c2ac11f2b631b5b12c836f9708a35397d36d626"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
hex:
dependency: transitive
description:
@ -1439,6 +1455,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.11.1"
steam_totp:
dependency: "direct main"
description:
name: steam_totp
sha256: "3c09143c983f6bb05bb53e9232f9d40bbcc01c596ba0273c3e6bb246729abfa1"
url: "https://pub.dev"
source: hosted
version: "0.0.1"
step_progress_indicator:
dependency: "direct main"
description:

View file

@ -1,6 +1,6 @@
name: ente_auth
description: ente two-factor authenticator
version: 3.0.1+301
version: 3.0.4+304
publish_to: none
environment:
@ -94,6 +94,7 @@ dependencies:
sqflite_common_ffi: ^2.3.0+4
sqlite3: ^2.1.0
sqlite3_flutter_libs: ^0.5.19+1
steam_totp: ^0.0.1
step_progress_indicator: ^1.0.2
styled_text: ^8.1.0
tray_manager: ^0.2.1

View file

@ -14,7 +14,7 @@
"build:ci": "yarn build-renderer && tsc",
"build:quick": "yarn build-renderer && yarn build-main:quick",
"dev": "concurrently --kill-others --success first --names 'main,rndr' \"yarn dev-main\" \"yarn dev-renderer\"",
"dev-main": "tsc && electron app/main.js",
"dev-main": "tsc && electron .",
"dev-renderer": "cd ../web && yarn install && yarn dev:photos",
"postinstall": "electron-builder install-app-deps",
"lint": "yarn prettier --check --log-level warn . && eslint --ext .ts src && yarn tsc",

View file

@ -315,32 +315,18 @@ const setupTrayItem = (mainWindow: BrowserWindow) => {
/**
* Older versions of our app used to maintain a cache dir using the main
* process. This has been removed in favor of cache on the web layer.
* process. This has been removed in favor of cache on the web layer. Delete the
* old cache dir if it exists.
*
* Delete the old cache dir if it exists.
*
* This will happen in two phases. The cache had three subdirectories:
*
* - Two of them, "thumbs" and "files", will be removed now (v1.7.0, May 2024).
*
* - The third one, "face-crops" will be removed once we finish the face search
* changes. See: [Note: Legacy face crops].
*
* This migration code can be removed after some time once most people have
* upgraded to newer versions.
* Added May 2024, v1.7.0. This migration code can be removed after some time
* once most people have upgraded to newer versions.
*/
const deleteLegacyDiskCacheDirIfExists = async () => {
const removeIfExists = async (dirPath: string) => {
if (existsSync(dirPath)) {
log.info(`Removing legacy disk cache from ${dirPath}`);
await fs.rm(dirPath, { recursive: true });
}
};
// [Note: Getting the cache path]
//
// The existing code was passing "cache" as a parameter to getPath.
//
// However, "cache" is not a valid parameter to getPath. It works! (for
// However, "cache" is not a valid parameter to getPath. It works (for
// example, on macOS I get `~/Library/Caches`), but it is intentionally not
// documented as part of the public API:
//
@ -353,8 +339,8 @@ const deleteLegacyDiskCacheDirIfExists = async () => {
// @ts-expect-error "cache" works but is not part of the public API.
const cacheDir = path.join(app.getPath("cache"), "ente");
if (existsSync(cacheDir)) {
await removeIfExists(path.join(cacheDir, "thumbs"));
await removeIfExists(path.join(cacheDir, "files"));
log.info(`Removing legacy disk cache from ${cacheDir}`);
await fs.rm(cacheDir, { recursive: true });
}
};

View file

@ -24,7 +24,6 @@ import {
updateOnNextRestart,
} from "./services/app-update";
import {
legacyFaceCrop,
openDirectory,
openLogDirectory,
selectDirectory,
@ -43,10 +42,10 @@ import {
import { convertToJPEG, generateImageThumbnail } from "./services/image";
import { logout } from "./services/logout";
import {
clipImageEmbedding,
clipTextEmbeddingIfAvailable,
computeCLIPImageEmbedding,
computeCLIPTextEmbeddingIfAvailable,
} from "./services/ml-clip";
import { detectFaces, faceEmbeddings } from "./services/ml-face";
import { computeFaceEmbeddings, detectFaces } from "./services/ml-face";
import { encryptionKey, saveEncryptionKey } from "./services/store";
import {
clearPendingUploads,
@ -170,24 +169,22 @@ export const attachIPCHandlers = () => {
// - ML
ipcMain.handle("clipImageEmbedding", (_, jpegImageData: Uint8Array) =>
clipImageEmbedding(jpegImageData),
ipcMain.handle(
"computeCLIPImageEmbedding",
(_, jpegImageData: Uint8Array) =>
computeCLIPImageEmbedding(jpegImageData),
);
ipcMain.handle("clipTextEmbeddingIfAvailable", (_, text: string) =>
clipTextEmbeddingIfAvailable(text),
ipcMain.handle("computeCLIPTextEmbeddingIfAvailable", (_, text: string) =>
computeCLIPTextEmbeddingIfAvailable(text),
);
ipcMain.handle("detectFaces", (_, input: Float32Array) =>
detectFaces(input),
);
ipcMain.handle("faceEmbeddings", (_, input: Float32Array) =>
faceEmbeddings(input),
);
ipcMain.handle("legacyFaceCrop", (_, faceID: string) =>
legacyFaceCrop(faceID),
ipcMain.handle("computeFaceEmbeddings", (_, input: Float32Array) =>
computeFaceEmbeddings(input),
);
// - Upload

View file

@ -163,7 +163,7 @@ const checkForUpdatesAndNotify = async (mainWindow: BrowserWindow) => {
};
/**
* Return the version of the desktop app
* Return the version of the desktop app.
*
* The return value is of the form `v1.2.3`.
*/

View file

@ -1,7 +1,5 @@
import { shell } from "electron/common";
import { app, dialog } from "electron/main";
import { existsSync } from "fs";
import fs from "node:fs/promises";
import path from "node:path";
import { posixPath } from "../utils/electron";
@ -53,14 +51,6 @@ export const openLogDirectory = () => openDirectory(logDirectoryPath());
* "userData" directory. This is the **primary** place applications are meant to
* store user's data, e.g. various configuration files and saved state.
*
* During development, our app name is "Electron", so this'd be, for example,
* `~/Library/Application Support/Electron` if we run using `yarn dev`. For the
* packaged production app, our app name is "ente", so this would be:
*
* - Windows: `%APPDATA%\ente`, e.g. `C:\Users\<username>\AppData\Local\ente`
* - Linux: `~/.config/ente`
* - macOS: `~/Library/Application Support/ente`
*
* Note that Chromium also stores the browser state, e.g. localStorage or disk
* caches, in userData.
*
@ -73,21 +63,7 @@ export const openLogDirectory = () => openDirectory(logDirectoryPath());
* "ente.log", it can be found at:
*
* - macOS: ~/Library/Logs/ente/ente.log (production)
* - macOS: ~/Library/Logs/Electron/ente.log (dev)
* - Linux: ~/.config/ente/logs/ente.log
* - Windows: %USERPROFILE%\AppData\Roaming\ente\logs\ente.log
*/
const logDirectoryPath = () => app.getPath("logs");
/**
* See: [Note: Legacy face crops]
*/
export const legacyFaceCrop = async (
faceID: string,
): Promise<Uint8Array | undefined> => {
// See: [Note: Getting the cache path]
// @ts-expect-error "cache" works but is not part of the public API.
const cacheDir = path.join(app.getPath("cache"), "ente");
const filePath = path.join(cacheDir, "face-crops", faceID);
return existsSync(filePath) ? await fs.readFile(filePath) : undefined;
};

View file

@ -11,7 +11,7 @@ import * as ort from "onnxruntime-node";
import Tokenizer from "../../thirdparty/clip-bpe-ts/mod";
import log from "../log";
import { writeStream } from "../stream";
import { ensure } from "../utils/common";
import { ensure, wait } from "../utils/common";
import { deleteTempFile, makeTempFilePath } from "../utils/temp";
import { makeCachedInferenceSession } from "./ml";
@ -20,7 +20,7 @@ const cachedCLIPImageSession = makeCachedInferenceSession(
351468764 /* 335.2 MB */,
);
export const clipImageEmbedding = async (jpegImageData: Uint8Array) => {
export const computeCLIPImageEmbedding = async (jpegImageData: Uint8Array) => {
const tempFilePath = await makeTempFilePath();
const imageStream = new Response(jpegImageData.buffer).body;
await writeStream(tempFilePath, ensure(imageStream));
@ -42,7 +42,7 @@ const clipImageEmbedding_ = async (jpegFilePath: string) => {
const results = await session.run(feeds);
log.debug(
() =>
`onnx/clip image embedding took ${Date.now() - t1} ms (prep: ${t2 - t1} ms, inference: ${Date.now() - t2} ms)`,
`ONNX/CLIP image embedding took ${Date.now() - t1} ms (prep: ${t2 - t1} ms, inference: ${Date.now() - t2} ms)`,
);
/* Need these model specific casts to type the result */
const imageEmbedding = ensure(results.output).data as Float32Array;
@ -140,21 +140,23 @@ const getTokenizer = () => {
return _tokenizer;
};
export const clipTextEmbeddingIfAvailable = async (text: string) => {
const sessionOrStatus = await Promise.race([
export const computeCLIPTextEmbeddingIfAvailable = async (text: string) => {
const sessionOrSkip = await Promise.race([
cachedCLIPTextSession(),
"downloading-model",
// Wait for a tick to get the session promise to resolved the first time
// this code runs on each app start (and the model has been downloaded).
wait(0).then(() => 1),
]);
// Don't wait for the download to complete
if (typeof sessionOrStatus == "string") {
// Don't wait for the download to complete.
if (typeof sessionOrSkip == "number") {
log.info(
"Ignoring CLIP text embedding request because model download is pending",
);
return undefined;
}
const session = sessionOrStatus;
const session = sessionOrSkip;
const t1 = Date.now();
const tokenizer = getTokenizer();
const tokenizedText = Int32Array.from(tokenizer.encodeForCLIP(text));
@ -165,7 +167,7 @@ export const clipTextEmbeddingIfAvailable = async (text: string) => {
const results = await session.run(feeds);
log.debug(
() =>
`onnx/clip text embedding took ${Date.now() - t1} ms (prep: ${t2 - t1} ms, inference: ${Date.now() - t2} ms)`,
`ONNX/CLIP text embedding took ${Date.now() - t1} ms (prep: ${t2 - t1} ms, inference: ${Date.now() - t2} ms)`,
);
const textEmbedding = ensure(results.output).data as Float32Array;
return normalizeEmbedding(textEmbedding);

View file

@ -23,7 +23,7 @@ export const detectFaces = async (input: Float32Array) => {
input: new ort.Tensor("float32", input, [1, 3, 640, 640]),
};
const results = await session.run(feeds);
log.debug(() => `onnx/yolo face detection took ${Date.now() - t} ms`);
log.debug(() => `ONNX/YOLO face detection took ${Date.now() - t} ms`);
return ensure(results.output).data;
};
@ -32,7 +32,7 @@ const cachedFaceEmbeddingSession = makeCachedInferenceSession(
5286998 /* 5 MB */,
);
export const faceEmbeddings = async (input: Float32Array) => {
export const computeFaceEmbeddings = async (input: Float32Array) => {
// Dimension of each face (alias)
const mobileFaceNetFaceSize = 112;
// Smaller alias
@ -45,7 +45,7 @@ export const faceEmbeddings = async (input: Float32Array) => {
const t = Date.now();
const feeds = { img_inputs: inputTensor };
const results = await session.run(feeds);
log.debug(() => `onnx/yolo face embedding took ${Date.now() - t} ms`);
log.debug(() => `ONNX/MFNT face embedding took ${Date.now() - t} ms`);
/* Need these model specific casts to extract and type the result */
return (results.embeddings as unknown as Record<string, unknown>)
.cpuData as Float32Array;

View file

@ -18,10 +18,7 @@ export const clearStores = () => {
* [Note: Safe storage keys]
*
* On macOS, `safeStorage` stores our data under a Keychain entry named
* "<app-name> Safe Storage". Which resolves to:
*
* - Electron Safe Storage (dev)
* - ente Safe Storage (prod)
* "<app-name> Safe Storage". In our case, "ente Safe Storage".
*/
export const saveEncryptionKey = (encryptionKey: string) => {
const encryptedKey = safeStorage.encryptString(encryptionKey);

View file

@ -106,7 +106,7 @@ const handleRead = async (path: string) => {
res.headers.set("Content-Length", `${fileSize}`);
// Add the file's last modified time (as epoch milliseconds).
const mtimeMs = stat.mtimeMs;
const mtimeMs = stat.mtime.getTime();
res.headers.set("X-Last-Modified-Ms", `${mtimeMs}`);
}
return res;
@ -132,6 +132,13 @@ const handleReadZip = async (zipPath: string, entryName: string) => {
// Close the zip handle when the underlying stream closes.
stream.on("end", () => void zip.close());
// While it is documented that entry.time is the modification time,
// the units are not mentioned. By seeing the source code, we can
// verify that it is indeed epoch milliseconds. See `parseZipTime`
// in the node-stream-zip source,
// https://github.com/antelle/node-stream-zip/blob/master/node_stream_zip.js
const modifiedMs = entry.time;
return new Response(webReadableStream, {
headers: {
// We don't know the exact type, but it doesn't really matter, just
@ -139,12 +146,7 @@ const handleReadZip = async (zipPath: string, entryName: string) => {
// doesn't tinker with it thinking of it as text.
"Content-Type": "application/octet-stream",
"Content-Length": `${entry.size}`,
// While it is documented that entry.time is the modification time,
// the units are not mentioned. By seeing the source code, we can
// verify that it is indeed epoch milliseconds. See `parseZipTime`
// in the node-stream-zip source,
// https://github.com/antelle/node-stream-zip/blob/master/node_stream_zip.js
"X-Last-Modified-Ms": `${entry.time}`,
"X-Last-Modified-Ms": `${modifiedMs}`,
},
});
};

View file

@ -13,3 +13,12 @@ export const ensure = <T>(v: T | null | undefined): T => {
if (v === undefined) throw new Error("Required value was not found");
return v;
};
/**
* Wait for {@link ms} milliseconds
*
* This function is a promisified `setTimeout`. It returns a promise that
* resolves after {@link ms} milliseconds.
*/
export const wait = (ms: number) =>
new Promise((resolve) => setTimeout(resolve, ms));

View file

@ -55,9 +55,7 @@ export const execAsync = async (command: string | string[]) => {
: command;
const startTime = Date.now();
const result = await execAsync_(escapedCommand);
log.debug(
() => `${escapedCommand} (${Math.round(Date.now() - startTime)} ms)`,
);
log.debug(() => `${escapedCommand} (${Date.now() - startTime} ms)`);
return result;
};

View file

@ -65,7 +65,7 @@ const selectDirectory = () => ipcRenderer.invoke("selectDirectory");
const logout = () => {
watchRemoveListeners();
ipcRenderer.send("logout");
return ipcRenderer.invoke("logout");
};
const encryptionKey = () => ipcRenderer.invoke("encryptionKey");
@ -153,20 +153,17 @@ const ffmpegExec = (
// - ML
const clipImageEmbedding = (jpegImageData: Uint8Array) =>
ipcRenderer.invoke("clipImageEmbedding", jpegImageData);
const computeCLIPImageEmbedding = (jpegImageData: Uint8Array) =>
ipcRenderer.invoke("computeCLIPImageEmbedding", jpegImageData);
const clipTextEmbeddingIfAvailable = (text: string) =>
ipcRenderer.invoke("clipTextEmbeddingIfAvailable", text);
const computeCLIPTextEmbeddingIfAvailable = (text: string) =>
ipcRenderer.invoke("computeCLIPTextEmbeddingIfAvailable", text);
const detectFaces = (input: Float32Array) =>
ipcRenderer.invoke("detectFaces", input);
const faceEmbeddings = (input: Float32Array) =>
ipcRenderer.invoke("faceEmbeddings", input);
const legacyFaceCrop = (faceID: string) =>
ipcRenderer.invoke("legacyFaceCrop", faceID);
const computeFaceEmbeddings = (input: Float32Array) =>
ipcRenderer.invoke("computeFaceEmbeddings", input);
// - Watch
@ -340,11 +337,10 @@ contextBridge.exposeInMainWorld("electron", {
// - ML
clipImageEmbedding,
clipTextEmbeddingIfAvailable,
computeCLIPImageEmbedding,
computeCLIPTextEmbeddingIfAvailable,
detectFaces,
faceEmbeddings,
legacyFaceCrop,
computeFaceEmbeddings,
// - Watch

View file

@ -163,6 +163,10 @@ export const sidebar = [
text: "From Authy",
link: "/auth/migration-guides/authy/",
},
{
text: "From Steam",
link: "/auth/migration-guides/steam/",
},
{
text: "Exporting your data",
link: "/auth/migration-guides/export",

View file

@ -7,4 +7,5 @@ description:
# Migrating to/from Ente Auth
- [Migrating from Authy](authy/)
- [Importing codes from Steam](steam/)
- [Exporting your data out of Ente Auth](export)

View file

@ -0,0 +1,79 @@
---
title: Migrating from Steam Authenticator
description: Guide for importing from Steam Authenticator to Ente Auth
---
# Migrating from Steam Authenticator
A guide written by an ente.io lover
> [!WARNING]
>
> Steam Authenticator code is only supported after auth-v3.0.3, check the app's
> version number before migration.
One way to migrate is to
[use this tool by dyc3](https://github.com/dyc3/steamguard-cli/releases/latest)
to simplify the process and skip directly to generating a qr code to Ente
Authenticator.
## Download/Install steamguard-cli
### Windows
1. Download `steamguard.exe` from the [releases page][releases].
2. Place `steamguard.exe` in a folder of your choice. For this example, we will
use `%USERPROFILE%\Desktop`.
3. Open Powershell or Command Prompt. The prompt should be at `%USERPROFILE%`
(eg. `C:\Users\<username>`).
4. Use `cd` to change directory into the folder where you placed
`steamguard.exe`. For this example, it would be `cd Desktop`.
5. You should now be able to run `steamguard.exe` by typing
`.\steamguard.exe --help` and pressing enter.
### Linux
#### Ubuntu/Debian
1. Download the `.deb` from the [releases page][releases].
2. Open a terminal and run this to install it:
```bash
sudo dpkg -i ./steamguard-cli_<version>_amd64.deb
```
#### Other Linux
1. Download `steamguard` from the [releases page][releases]
2. Make it executable, and move `steamguard` to `/usr/local/bin` or any other
directory in your `$PATH`.
```bash
chmod +x ./steamguard
sudo mv ./steamguard /usr/local/bin
```
3. You should now be able to run `steamguard` by typing `steamguard --help` and
pressing enter.
## Login to Steam account
Set up a new account with steamguard-cli
```bash
steamguard setup # set up a new account with steamguard-cli
```
## Generate & importing QR codes
steamguard-cli can then generate a QR code for your 2FA secret.
```bash
steamguard qr # print QR code for the first account in your maFiles
steamguard -u <account name> qr # print QR code for a specific account
```
Open Ente Auth, press the '+' button, select `Scan a QR code`, and scan the qr
code.
You should now have your steam code inside Ente Auth

View file

@ -78,3 +78,23 @@ To summarize:
Set the S3 bucket `endpoint` in `credentials.yaml` to a `yourserverip:3200` or
some such IP/hostname that accessible from both where you are running the Ente
clients (e.g. the mobile app) and also from within the Docker compose cluster.
### 403 Forbidden
If museum (`2`) is able to make a network connection to your S3 bucket (`3`) but
uploads are still failing, it could be a credentials or permissions issue. A
telltale sign of this is that in the museum logs you can see `403 Forbidden`
errors about it not able to find the size of a file even though the
corresponding object exists in the S3 bucket.
To fix these, you should ensure the following:
1. The bucket CORS rules do not allow museum to access these objects.
> For uploading files from the browser, you will need to currently set
> allowedOrigins to "\*", and allow the "X-Auth-Token", "X-Client-Package"
> headers configuration too.
> [Here is an example of a working configuration](https://github.com/ente-io/ente/discussions/1764#discussioncomment-9478204).
2. The credentials are not being picked up (you might be setting the correct
creds, but not in the place where museum picks them from).

View file

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

View file

@ -43,7 +43,7 @@ android {
defaultConfig {
applicationId "io.ente.photos"
minSdkVersion 21
minSdkVersion 26
targetSdkVersion 33
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
@ -70,6 +70,10 @@ android {
dimension "default"
applicationIdSuffix ".dev"
}
face {
dimension "default"
applicationIdSuffix ".face"
}
playstore {
dimension "default"
}

View file

@ -0,0 +1,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="io.ente.photos">
<!-- Flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
</manifest>

View file

@ -0,0 +1,4 @@
<resources>
<string name="app_name">ente face</string>
<string name="backup">backup face</string>
</resources>

View file

@ -1,91 +0,0 @@
unknown
person
bicycle
car
motorcycle
airplane
bus
train
truck
boat
traffic light
fire hydrant
unknown
stop sign
parking meter
bench
bird
cat
dog
horse
sheep
cow
elephant
bear
zebra
giraffe
unknown
backpack
umbrella
unknown
unknown
handbag
tie
suitcase
frisbee
skis
snowboard
sports ball
kite
baseball bat
baseball glove
skateboard
surfboard
tennis racket
bottle
unknown
wine glass
cup
fork
knife
spoon
bowl
banana
apple
sandwich
orange
broccoli
carrot
hot dog
pizza
donut
cake
chair
couch
potted plant
bed
unknown
dining table
unknown
unknown
toilet
unknown
tv
laptop
mouse
remote
keyboard
cell phone
microwave
oven
toaster
sink
refrigerator
unknown
book
clock
vase
scissors
teddy bear
hair drier
toothbrush

View file

@ -1,30 +0,0 @@
waterfall
snow
landscape
underwater
architecture
sunset / sunrise
blue sky
cloudy sky
greenery
autumn leaves
portrait
flower
night shot
stage concert
fireworks
candle light
neon lights
indoor
backlight
text documents
qr images
group portrait
computer screens
kids
dog
cat
macro
food
beach
mountain

View file

@ -6,6 +6,8 @@ PODS:
- connectivity_plus (0.0.1):
- Flutter
- FlutterMacOS
- dart_ui_isolate (0.0.1):
- Flutter
- device_info_plus (0.0.1):
- Flutter
- file_saver (0.0.1):
@ -226,6 +228,7 @@ DEPENDENCIES:
- background_fetch (from `.symlinks/plugins/background_fetch/ios`)
- battery_info (from `.symlinks/plugins/battery_info/ios`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`)
- dart_ui_isolate (from `.symlinks/plugins/dart_ui_isolate/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- file_saver (from `.symlinks/plugins/file_saver/ios`)
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
@ -302,6 +305,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/battery_info/ios"
connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/darwin"
dart_ui_isolate:
:path: ".symlinks/plugins/dart_ui_isolate/ios"
device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios"
file_saver:
@ -397,6 +402,7 @@ SPEC CHECKSUMS:
background_fetch: 2319bf7e18237b4b269430b7f14d177c0df09c5a
battery_info: 09f5c9ee65394f2291c8c6227bedff345b8a730c
connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db
dart_ui_isolate: d5bcda83ca4b04f129d70eb90110b7a567aece14
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
Firebase: 91fefd38712feb9186ea8996af6cbdef41473442
@ -421,7 +427,7 @@ SPEC CHECKSUMS:
home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57
image_editor_common: d6f6644ae4a6de80481e89fe6d0a8c49e30b4b43
in_app_purchase_storekit: 0e4b3c2e43ba1e1281f4f46dd71b0593ce529892
integration_test: 13825b8a9334a850581300559b8839134b124670
integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4
libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009
local_auth_darwin: c7e464000a6a89e952235699e32b329457608d98
local_auth_ios: 5046a18c018dd973247a0564496c8898dbb5adf9

View file

@ -293,6 +293,7 @@
"${BUILT_PRODUCTS_DIR}/background_fetch/background_fetch.framework",
"${BUILT_PRODUCTS_DIR}/battery_info/battery_info.framework",
"${BUILT_PRODUCTS_DIR}/connectivity_plus/connectivity_plus.framework",
"${BUILT_PRODUCTS_DIR}/dart_ui_isolate/dart_ui_isolate.framework",
"${BUILT_PRODUCTS_DIR}/device_info_plus/device_info_plus.framework",
"${BUILT_PRODUCTS_DIR}/file_saver/file_saver.framework",
"${BUILT_PRODUCTS_DIR}/fk_user_agent/fk_user_agent.framework",
@ -374,6 +375,7 @@
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/background_fetch.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/battery_info.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/connectivity_plus.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/dart_ui_isolate.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/device_info_plus.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_saver.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/fk_user_agent.framework",

View file

@ -65,9 +65,9 @@
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>FLTEnableImpeller</key>
<false />
<true />
<key>FLTEnableWideGamut</key>
<false/>
<true/>
<key>NSFaceIDUsageDescription</key>
<string>Please allow ente to lock itself with FaceID or TouchID</string>
<key>NSCameraUsageDescription</key>

View file

@ -19,6 +19,7 @@ import 'package:photos/db/upload_locks_db.dart';
import "package:photos/events/endpoint_updated_event.dart";
import 'package:photos/events/signed_in_event.dart';
import 'package:photos/events/user_logged_out_event.dart';
import "package:photos/face/db.dart";
import 'package:photos/models/key_attributes.dart';
import 'package:photos/models/key_gen_result.dart';
import 'package:photos/models/private_key_attributes.dart';
@ -34,10 +35,10 @@ 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/validator_util.dart';
import "package:photos/utils/wakelock_util.dart";
import 'package:shared_preferences/shared_preferences.dart';
import "package:tuple/tuple.dart";
import 'package:uuid/uuid.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
class Configuration {
Configuration._privateConstructor();
@ -187,6 +188,7 @@ class Configuration {
: null;
await CollectionsDB.instance.clearTable();
await MemoriesDB.instance.clearTable();
await FaceMLDataDB.instance.clearTable();
await UploadLocksDB.instance.clearTable();
await IgnoredFilesService.instance.reset();
@ -583,7 +585,7 @@ class Configuration {
Future<void> setShouldKeepDeviceAwake(bool value) async {
await _preferences.setBool(keyShouldKeepDeviceAwake, value);
await WakelockPlus.toggle(enable: value);
await EnteWakeLock.toggle(enable: value);
}
Future<void> setShouldBackupVideos(bool value) async {

View file

@ -69,6 +69,8 @@ const galleryGridSpacing = 2.0;
const kSearchSectionLimit = 9;
const maxPickAssetLimit = 50;
const iOSGroupID = "group.io.ente.frame.SlideshowWidget";
const blackThumbnailBase64 = '/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEB'
@ -99,6 +101,9 @@ const blackThumbnailBase64 = '/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEB'
'AKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAo' +
'AKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgD/9k=';
const localFileServer =
String.fromEnvironment("localFileServer", defaultValue: "");
const uploadTempFilePrefix = "upload_file_";
final tempDirCleanUpInterval = kDebugMode
? const Duration(seconds: 30).inMicroseconds

View file

@ -54,7 +54,7 @@ class EmbeddingsDB {
Future<void> clearTable() async {
final db = await _database;
await db.execute('DELETE * FROM $tableName');
await db.execute('DELETE FROM $tableName');
}
Future<List<Embedding>> getAll(Model model) async {

View file

@ -9,7 +9,7 @@ extension EntitiesDB on FilesDB {
List<LocalEntityData> data, {
ConflictAlgorithm conflictAlgorithm = ConflictAlgorithm.replace,
}) async {
debugPrint("Inserting missing PathIDToLocalIDMapping");
debugPrint("entitiesDB: upsertEntities ${data.length} entities");
final db = await database;
var batch = db.batch();
int batchCounter = 0;
@ -62,4 +62,17 @@ extension EntitiesDB on FilesDB {
return LocalEntityData.fromJson(maps[i]);
});
}
Future<LocalEntityData?> getEntity(EntityType type, String id) async {
final db = await database;
final List<Map<String, dynamic>> maps = await db.query(
"entities",
where: "type = ? AND id = ?",
whereArgs: [type.typeToString(), id],
);
if (maps.isEmpty) {
return null;
}
return LocalEntityData.fromJson(maps.first);
}
}

View file

@ -491,6 +491,18 @@ class FilesDB {
return convertToFiles(results)[0];
}
Future<EnteFile?> getAnyUploadedFile(int uploadedID) async {
final db = await instance.sqliteAsyncDB;
final results = await db.getAll(
'SELECT * FROM $filesTable WHERE $columnUploadedFileID = ?',
[uploadedID],
);
if (results.isEmpty) {
return null;
}
return convertToFiles(results)[0];
}
Future<Set<int>> getUploadedFileIDs(int collectionID) async {
final db = await instance.sqliteAsyncDB;
final results = await db.getAll(
@ -683,6 +695,17 @@ class FilesDB {
return files;
}
Future<List<EnteFile>> getAllFilesFromCollections(
Iterable<int> collectionID,
) async {
final db = await instance.sqliteAsyncDB;
final String sql =
'SELECT * FROM $filesTable WHERE $columnCollectionID IN (${collectionID.join(',')})';
final results = await db.getAll(sql);
final files = convertToFiles(results);
return files;
}
Future<List<EnteFile>> getNewFilesInCollection(
int collectionID,
int addedTime,
@ -1304,6 +1327,23 @@ class FilesDB {
return result;
}
Future<Map<int, int>> getFileIDToCreationTime() async {
final db = await instance.sqliteAsyncDB;
final rows = await db.getAll(
'''
SELECT $columnUploadedFileID, $columnCreationTime
FROM $filesTable
WHERE
($columnUploadedFileID IS NOT NULL AND $columnUploadedFileID IS NOT -1);
''',
);
final result = <int, int>{};
for (final row in rows) {
result[row[columnUploadedFileID] as int] = row[columnCreationTime] as int;
}
return result;
}
// getCollectionFileFirstOrLast returns the first or last uploaded file in
// the collection based on the given collectionID and the order.
Future<EnteFile?> getCollectionFileFirstOrLast(
@ -1643,13 +1683,14 @@ class FilesDB {
}
Future<List<int>> getOwnedFileIDs(int ownerID) async {
final db = await instance.database;
final results = await db.query(
filesTable,
columns: [columnUploadedFileID],
where:
'($columnOwnerID = $ownerID AND $columnUploadedFileID IS NOT NULL AND $columnUploadedFileID IS NOT -1)',
distinct: true,
final db = await instance.sqliteAsyncDB;
final results = await db.getAll(
'''
SELECT DISTINCT $columnUploadedFileID FROM $filesTable
WHERE ($columnOwnerID = ? AND $columnUploadedFileID IS NOT NULL AND
$columnUploadedFileID IS NOT -1)
''',
[ownerID],
);
final ids = <int>[];
for (final result in results) {
@ -1659,16 +1700,17 @@ class FilesDB {
}
Future<List<EnteFile>> getUploadedFiles(List<int> uploadedIDs) async {
final db = await instance.database;
final db = await instance.sqliteAsyncDB;
String inParam = "";
for (final id in uploadedIDs) {
inParam += "'" + id.toString() + "',";
}
inParam = inParam.substring(0, inParam.length - 1);
final results = await db.query(
filesTable,
where: '$columnUploadedFileID IN ($inParam)',
groupBy: columnUploadedFileID,
final results = await db.getAll(
'''
SELECT * FROM $filesTable WHERE $columnUploadedFileID IN ($inParam)
GROUP BY $columnUploadedFileID
''',
);
if (results.isEmpty) {
return <EnteFile>[];

View file

@ -26,4 +26,6 @@ enum EventType {
hide,
unhide,
coverChanged,
peopleChanged,
peopleClusterChanged,
}

View file

@ -0,0 +1,22 @@
import "package:photos/events/event.dart";
import "package:photos/models/file/file.dart";
class PeopleChangedEvent extends Event {
final List<EnteFile>? relevantFiles;
final PeopleEventType type;
final String source;
PeopleChangedEvent({
this.relevantFiles,
this.type = PeopleEventType.defaultType,
this.source = "",
});
@override
String get reason => '$runtimeType{type: ${type.name}, "via": $source}';
}
enum PeopleEventType {
defaultType,
removedFilesFromCluster,
}

View file

@ -0,0 +1,193 @@
import 'dart:math' as math show sin, cos, atan2, sqrt, pow;
import 'package:ml_linalg/linalg.dart';
extension SetVectorValues on Vector {
Vector setValues(int start, int end, Iterable<double> values) {
if (values.length > length) {
throw Exception('Values cannot be larger than vector');
} else if (end - start != values.length) {
throw Exception('Values must be same length as range');
} else if (start < 0 || end > length) {
throw Exception('Range must be within vector');
}
final tempList = toList();
tempList.replaceRange(start, end, values);
final newVector = Vector.fromList(tempList);
return newVector;
}
}
extension SetMatrixValues on Matrix {
Matrix setSubMatrix(
int startRow,
int endRow,
int startColumn,
int endColumn,
Iterable<Iterable<double>> values,
) {
if (values.length > rowCount) {
throw Exception('New values cannot have more rows than original matrix');
} else if (values.elementAt(0).length > columnCount) {
throw Exception(
'New values cannot have more columns than original matrix',
);
} else if (endRow - startRow != values.length) {
throw Exception('Values (number of rows) must be same length as range');
} else if (endColumn - startColumn != values.elementAt(0).length) {
throw Exception(
'Values (number of columns) must be same length as range',
);
} else if (startRow < 0 ||
endRow > rowCount ||
startColumn < 0 ||
endColumn > columnCount) {
throw Exception('Range must be within matrix');
}
final tempList = asFlattenedList
.toList(); // You need `.toList()` here to make sure the list is growable, otherwise `replaceRange` will throw an error
for (var i = startRow; i < endRow; i++) {
tempList.replaceRange(
i * columnCount + startColumn,
i * columnCount + endColumn,
values.elementAt(i).toList(),
);
}
final newMatrix = Matrix.fromFlattenedList(tempList, rowCount, columnCount);
return newMatrix;
}
Matrix setValues(
int startRow,
int endRow,
int startColumn,
int endColumn,
Iterable<double> values,
) {
if ((startRow - endRow) * (startColumn - endColumn) != values.length) {
throw Exception('Values must be same length as range');
} else if (startRow < 0 ||
endRow > rowCount ||
startColumn < 0 ||
endColumn > columnCount) {
throw Exception('Range must be within matrix');
}
final tempList = asFlattenedList
.toList(); // You need `.toList()` here to make sure the list is growable, otherwise `replaceRange` will throw an error
var index = 0;
for (var i = startRow; i < endRow; i++) {
for (var j = startColumn; j < endColumn; j++) {
tempList[i * columnCount + j] = values.elementAt(index);
index++;
}
}
final newMatrix = Matrix.fromFlattenedList(tempList, rowCount, columnCount);
return newMatrix;
}
Matrix setValue(int row, int column, double value) {
if (row < 0 || row > rowCount || column < 0 || column > columnCount) {
throw Exception('Index must be within range of matrix');
}
final tempList = asFlattenedList;
tempList[row * columnCount + column] = value;
final newMatrix = Matrix.fromFlattenedList(tempList, rowCount, columnCount);
return newMatrix;
}
Matrix appendRow(List<double> row) {
final oldNumberOfRows = rowCount;
final oldNumberOfColumns = columnCount;
if (row.length != oldNumberOfColumns) {
throw Exception('Row must have same number of columns as matrix');
}
final flatListMatrix = asFlattenedList;
flatListMatrix.addAll(row);
return Matrix.fromFlattenedList(
flatListMatrix,
oldNumberOfRows + 1,
oldNumberOfColumns,
);
}
}
extension MatrixCalculations on Matrix {
double determinant() {
final int length = rowCount;
if (length != columnCount) {
throw Exception('Matrix must be square');
}
if (length == 1) {
return this[0][0];
} else if (length == 2) {
return this[0][0] * this[1][1] - this[0][1] * this[1][0];
} else {
throw Exception('Determinant for Matrix larger than 2x2 not implemented');
}
}
/// Computes the singular value decomposition of a matrix, using https://lucidar.me/en/mathematics/singular-value-decomposition-of-a-2x2-matrix/ as reference, but with slightly different signs for the second columns of U and V
Map<String, dynamic> svd() {
if (rowCount != 2 || columnCount != 2) {
throw Exception('Matrix must be 2x2');
}
final a = this[0][0];
final b = this[0][1];
final c = this[1][0];
final d = this[1][1];
// Computation of U matrix
final tempCalc = a * a + b * b - c * c - d * d;
final theta = 0.5 * math.atan2(2 * a * c + 2 * b * d, tempCalc);
final U = Matrix.fromList([
[math.cos(theta), math.sin(theta)],
[math.sin(theta), -math.cos(theta)],
]);
// Computation of S matrix
// ignore: non_constant_identifier_names
final S1 = a * a + b * b + c * c + d * d;
// ignore: non_constant_identifier_names
final S2 =
math.sqrt(math.pow(tempCalc, 2) + 4 * math.pow(a * c + b * d, 2));
final sigma1 = math.sqrt((S1 + S2) / 2);
final sigma2 = math.sqrt((S1 - S2) / 2);
final S = Vector.fromList([sigma1, sigma2]);
// Computation of V matrix
final tempCalc2 = a * a - b * b + c * c - d * d;
final phi = 0.5 * math.atan2(2 * a * b + 2 * c * d, tempCalc2);
final s11 = (a * math.cos(theta) + c * math.sin(theta)) * math.cos(phi) +
(b * math.cos(theta) + d * math.sin(theta)) * math.sin(phi);
final s22 = (a * math.sin(theta) - c * math.cos(theta)) * math.sin(phi) +
(-b * math.sin(theta) + d * math.cos(theta)) * math.cos(phi);
final V = Matrix.fromList([
[s11.sign * math.cos(phi), s22.sign * math.sin(phi)],
[s11.sign * math.sin(phi), -s22.sign * math.cos(phi)],
]);
return {
'U': U,
'S': S,
'V': V,
};
}
int matrixRank() {
final svdResult = svd();
final Vector S = svdResult['S']!;
final rank = S.toList().where((element) => element > 1e-10).length;
return rank;
}
}
extension TransformMatrix on Matrix {
List<List<double>> to2DList() {
final List<List<double>> outerList = [];
for (var i = 0; i < rowCount; i++) {
final innerList = this[i].toList();
outerList.add(innerList);
}
return outerList;
}
}

View file

@ -23,4 +23,9 @@ class EnteWatch extends Stopwatch {
reset();
previousElapsed = 0;
}
void stopWithLog(String msg) {
log(msg);
stop();
}
}

1051
mobile/lib/face/db.dart Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,103 @@
// Faces Table Fields & Schema Queries
import 'package:photos/services/machine_learning/face_ml/face_filtering/face_filtering_constants.dart';
const facesTable = 'faces';
const fileIDColumn = 'file_id';
const faceIDColumn = 'face_id';
const faceDetectionColumn = 'detection';
const faceEmbeddingBlob = 'eBlob';
const faceScore = 'score';
const faceBlur = 'blur';
const isSideways = 'is_sideways';
const imageWidth = 'width';
const imageHeight = 'height';
const faceClusterId = 'cluster_id';
const mlVersionColumn = 'ml_version';
const createFacesTable = '''CREATE TABLE IF NOT EXISTS $facesTable (
$fileIDColumn INTEGER NOT NULL,
$faceIDColumn TEXT NOT NULL UNIQUE,
$faceDetectionColumn TEXT NOT NULL,
$faceEmbeddingBlob BLOB NOT NULL,
$faceScore REAL NOT NULL,
$faceBlur REAL NOT NULL DEFAULT $kLapacianDefault,
$isSideways INTEGER NOT NULL DEFAULT 0,
$imageHeight INTEGER NOT NULL DEFAULT 0,
$imageWidth INTEGER NOT NULL DEFAULT 0,
$mlVersionColumn INTEGER NOT NULL DEFAULT -1,
PRIMARY KEY($fileIDColumn, $faceIDColumn)
);
''';
const deleteFacesTable = 'DROP TABLE IF EXISTS $facesTable';
// End of Faces Table Fields & Schema Queries
//##region Face Clusters Table Fields & Schema Queries
const faceClustersTable = 'face_clusters';
const fcClusterID = 'cluster_id';
const fcFaceId = 'face_id';
// fcClusterId & fcFaceId are the primary keys and fcClusterId is a foreign key to faces table
const createFaceClustersTable = '''
CREATE TABLE IF NOT EXISTS $faceClustersTable (
$fcFaceId TEXT NOT NULL,
$fcClusterID INTEGER NOT NULL,
PRIMARY KEY($fcFaceId)
);
''';
// -- Creating a non-unique index on clusterID for query optimization
const fcClusterIDIndex =
'''CREATE INDEX IF NOT EXISTS idx_fcClusterID ON $faceClustersTable($fcClusterID);''';
const dropFaceClustersTable = 'DROP TABLE IF EXISTS $faceClustersTable';
//##endregion
// People Table Fields & Schema Queries
const personTable = 'person';
const deletePersonTable = 'DROP TABLE IF EXISTS $personTable';
//End People Table Fields & Schema Queries
// Clusters Table Fields & Schema Queries
const clusterPersonTable = 'cluster_person';
const personIdColumn = 'person_id';
const clusterIDColumn = 'cluster_id';
const createClusterPersonTable = '''
CREATE TABLE IF NOT EXISTS $clusterPersonTable (
$personIdColumn TEXT NOT NULL,
$clusterIDColumn INTEGER NOT NULL,
PRIMARY KEY($personIdColumn, $clusterIDColumn)
);
''';
const dropClusterPersonTable = 'DROP TABLE IF EXISTS $clusterPersonTable';
// End Clusters Table Fields & Schema Queries
/// Cluster Summary Table Fields & Schema Queries
const clusterSummaryTable = 'cluster_summary';
const avgColumn = 'avg';
const countColumn = 'count';
const createClusterSummaryTable = '''
CREATE TABLE IF NOT EXISTS $clusterSummaryTable (
$clusterIDColumn INTEGER NOT NULL,
$avgColumn BLOB NOT NULL,
$countColumn INTEGER NOT NULL,
PRIMARY KEY($clusterIDColumn)
);
''';
const dropClusterSummaryTable = 'DROP TABLE IF EXISTS $clusterSummaryTable';
/// End Cluster Summary Table Fields & Schema Queries
/// notPersonFeedback Table Fields & Schema Queries
const notPersonFeedback = 'not_person_feedback';
const createNotPersonFeedbackTable = '''
CREATE TABLE IF NOT EXISTS $notPersonFeedback (
$personIdColumn TEXT NOT NULL,
$clusterIDColumn INTEGER NOT NULL,
PRIMARY KEY($personIdColumn, $clusterIDColumn)
);
''';
const dropNotPersonFeedbackTable = 'DROP TABLE IF EXISTS $notPersonFeedback';
// End Clusters Table Fields & Schema Queries

View file

@ -0,0 +1,57 @@
import "dart:convert";
import 'package:photos/face/db_fields.dart';
import "package:photos/face/model/detection.dart";
import "package:photos/face/model/face.dart";
import "package:photos/generated/protos/ente/common/vector.pb.dart";
import "package:photos/models/ml/ml_versions.dart";
int boolToSQLInt(bool? value, {bool defaultValue = false}) {
final bool v = value ?? defaultValue;
if (v == false) {
return 0;
} else {
return 1;
}
}
bool sqlIntToBool(int? value, {bool defaultValue = false}) {
final int v = value ?? (defaultValue ? 1 : 0);
if (v == 0) {
return false;
} else {
return true;
}
}
Map<String, dynamic> mapRemoteToFaceDB(Face face) {
return {
faceIDColumn: face.faceID,
fileIDColumn: face.fileID,
faceDetectionColumn: json.encode(face.detection.toJson()),
faceEmbeddingBlob: EVector(
values: face.embedding,
).writeToBuffer(),
faceScore: face.score,
faceBlur: face.blur,
isSideways: face.detection.faceIsSideways() ? 1 : 0,
mlVersionColumn: faceMlVersion,
imageWidth: face.fileInfo?.imageWidth ?? 0,
imageHeight: face.fileInfo?.imageHeight ?? 0,
};
}
Face mapRowToFace(Map<String, dynamic> row) {
return Face(
row[faceIDColumn] as String,
row[fileIDColumn] as int,
EVector.fromBuffer(row[faceEmbeddingBlob] as List<int>).values,
row[faceScore] as double,
Detection.fromJson(json.decode(row[faceDetectionColumn] as String)),
row[faceBlur] as double,
fileInfo: FileInfo(
imageWidth: row[imageWidth] as int,
imageHeight: row[imageHeight] as int,
),
);
}

View file

@ -0,0 +1,35 @@
/// Bounding box of a face.
///
/// [ x] and [y] are the minimum coordinates, so the top left corner of the box.
/// [width] and [height] are the width and height of the box.
///
/// WARNING: All values are relative to the original image size, so in the range [0, 1].
class FaceBox {
final double x;
final double y;
final double width;
final double height;
FaceBox({
required this.x,
required this.y,
required this.width,
required this.height,
});
factory FaceBox.fromJson(Map<String, dynamic> json) {
return FaceBox(
x: (json['x'] as double?) ?? (json['xMin'] as double),
y: (json['y'] as double?) ?? (json['yMin'] as double),
width: json['width'] as double,
height: json['height'] as double,
);
}
Map<String, dynamic> toJson() => {
'x': x,
'y': y,
'width': width,
'height': height,
};
}

View file

@ -0,0 +1,120 @@
import "dart:math" show min, max;
import "package:photos/face/model/box.dart";
import "package:photos/face/model/landmark.dart";
import "package:photos/services/machine_learning/face_ml/face_detection/detection.dart";
/// Stores the face detection data, notably the bounding box and landmarks.
///
/// - Bounding box: [FaceBox] with x, y (minimum, so top left corner), width, height
/// - Landmarks: list of [Landmark]s, namely leftEye, rightEye, nose, leftMouth, rightMouth
///
/// WARNING: All coordinates are relative to the image size, so in the range [0, 1]!
class Detection {
FaceBox box;
List<Landmark> landmarks;
Detection({
required this.box,
required this.landmarks,
});
bool get isEmpty => box.width == 0 && box.height == 0 && landmarks.isEmpty;
// empty box
Detection.empty()
: box = FaceBox(
x: 0,
y: 0,
width: 0,
height: 0,
),
landmarks = [];
Map<String, dynamic> toJson() => {
'box': box.toJson(),
'landmarks': landmarks.map((x) => x.toJson()).toList(),
};
factory Detection.fromJson(Map<String, dynamic> json) {
return Detection(
box: FaceBox.fromJson(json['box'] as Map<String, dynamic>),
landmarks: List<Landmark>.from(
json['landmarks']
.map((x) => Landmark.fromJson(x as Map<String, dynamic>)),
),
);
}
int getFaceArea(int imageWidth, int imageHeight) {
return (box.width * imageWidth * box.height * imageHeight).toInt();
}
FaceDirection getFaceDirection() {
if (isEmpty) {
return FaceDirection.straight;
}
final leftEye = [landmarks[0].x, landmarks[0].y];
final rightEye = [landmarks[1].x, landmarks[1].y];
final nose = [landmarks[2].x, landmarks[2].y];
final leftMouth = [landmarks[3].x, landmarks[3].y];
final rightMouth = [landmarks[4].x, landmarks[4].y];
final double eyeDistanceX = (rightEye[0] - leftEye[0]).abs();
final double eyeDistanceY = (rightEye[1] - leftEye[1]).abs();
final double mouthDistanceY = (rightMouth[1] - leftMouth[1]).abs();
final bool faceIsUpright =
(max(leftEye[1], rightEye[1]) + 0.5 * eyeDistanceY < nose[1]) &&
(nose[1] + 0.5 * mouthDistanceY < min(leftMouth[1], rightMouth[1]));
final bool noseStickingOutLeft = (nose[0] < min(leftEye[0], rightEye[0])) &&
(nose[0] < min(leftMouth[0], rightMouth[0]));
final bool noseStickingOutRight =
(nose[0] > max(leftEye[0], rightEye[0])) &&
(nose[0] > max(leftMouth[0], rightMouth[0]));
final bool noseCloseToLeftEye =
(nose[0] - leftEye[0]).abs() < 0.2 * eyeDistanceX;
final bool noseCloseToRightEye =
(nose[0] - rightEye[0]).abs() < 0.2 * eyeDistanceX;
// if (faceIsUpright && (noseStickingOutLeft || noseCloseToLeftEye)) {
if (noseStickingOutLeft || (faceIsUpright && noseCloseToLeftEye)) {
return FaceDirection.left;
// } else if (faceIsUpright && (noseStickingOutRight || noseCloseToRightEye)) {
} else if (noseStickingOutRight || (faceIsUpright && noseCloseToRightEye)) {
return FaceDirection.right;
}
return FaceDirection.straight;
}
bool faceIsSideways() {
if (isEmpty) {
return false;
}
final leftEye = [landmarks[0].x, landmarks[0].y];
final rightEye = [landmarks[1].x, landmarks[1].y];
final nose = [landmarks[2].x, landmarks[2].y];
final leftMouth = [landmarks[3].x, landmarks[3].y];
final rightMouth = [landmarks[4].x, landmarks[4].y];
final double eyeDistanceX = (rightEye[0] - leftEye[0]).abs();
final double eyeDistanceY = (rightEye[1] - leftEye[1]).abs();
final double mouthDistanceY = (rightMouth[1] - leftMouth[1]).abs();
final bool faceIsUpright =
(max(leftEye[1], rightEye[1]) + 0.5 * eyeDistanceY < nose[1]) &&
(nose[1] + 0.5 * mouthDistanceY < min(leftMouth[1], rightMouth[1]));
final bool noseStickingOutLeft =
(nose[0] < min(leftEye[0], rightEye[0]) - 0.5 * eyeDistanceX) &&
(nose[0] < min(leftMouth[0], rightMouth[0]));
final bool noseStickingOutRight =
(nose[0] > max(leftEye[0], rightEye[0]) + 0.5 * eyeDistanceX) &&
(nose[0] > max(leftMouth[0], rightMouth[0]));
return faceIsUpright && (noseStickingOutLeft || noseStickingOutRight);
}
}

View file

@ -0,0 +1,25 @@
class Dimensions {
final int width;
final int height;
const Dimensions({required this.width, required this.height});
@override
String toString() {
return 'Dimensions(width: $width, height: $height})';
}
Map<String, int> toJson() {
return {
'width': width,
'height': height,
};
}
factory Dimensions.fromJson(Map<String, dynamic> json) {
return Dimensions(
width: json['width'] as int,
height: json['height'] as int,
);
}
}

View file

@ -0,0 +1,85 @@
import "package:photos/face/model/detection.dart";
import 'package:photos/services/machine_learning/face_ml/face_filtering/face_filtering_constants.dart';
import "package:photos/services/machine_learning/face_ml/face_ml_result.dart";
// FileInfo contains the image width and height of the image the face was detected in.
class FileInfo {
int? imageWidth;
int? imageHeight;
FileInfo({
this.imageWidth,
this.imageHeight,
});
}
class Face {
final String faceID;
final List<double> embedding;
Detection detection;
final double score;
final double blur;
///#region Local DB fields
// This is not stored on the server, using it for local DB row
FileInfo? fileInfo;
final int fileID;
///#endregion
bool get isBlurry => blur < kLaplacianHardThreshold;
bool get hasHighScore => score > kMinimumQualityFaceScore;
bool get isHighQuality => (!isBlurry) && hasHighScore;
int area({int? w, int? h}) {
return detection.getFaceArea(
fileInfo?.imageWidth ?? w ?? 0,
fileInfo?.imageHeight ?? h ?? 0,
);
}
Face(
this.faceID,
this.fileID,
this.embedding,
this.score,
this.detection,
this.blur, {
this.fileInfo,
});
factory Face.empty(int fileID, {bool error = false}) {
return Face(
"$fileID-0",
fileID,
<double>[],
error ? -1.0 : 0.0,
Detection.empty(),
0.0,
);
}
factory Face.fromJson(Map<String, dynamic> json) {
final String faceID = json['faceID'] as String;
final int fileID = getFileIdFromFaceId(faceID);
return Face(
faceID,
fileID,
List<double>.from((json['embedding'] ?? json['embeddings']) as List),
json['score'] as double,
Detection.fromJson(json['detection'] as Map<String, dynamic>),
// high value means t
(json['blur'] ?? kLapacianDefault) as double,
);
}
// Note: Keep the information in toJson minimum. Keep in sync with desktop.
// Derive fields like fileID from other values whenever possible
Map<String, dynamic> toJson() => {
'faceID': faceID,
'embedding': embedding,
'detection': detection.toJson(),
'score': score,
'blur': blur,
};
}

View file

@ -0,0 +1,33 @@
/// Landmark coordinate data.
///
/// WARNING: All coordinates are relative to the image size, so in the range [0, 1]!
class Landmark {
double x;
double y;
Landmark({
required this.x,
required this.y,
});
Map<String, dynamic> toJson() => {
'x': x,
'y': y,
};
factory Landmark.fromJson(Map<String, dynamic> json) {
return Landmark(
x: (json['x'] is int
? (json['x'] as int).toDouble()
: json['x'] as double),
y: (json['y'] is int
? (json['y'] as int).toDouble()
: json['y'] as double),
);
}
@override
toString() {
return '(x: ${x.toStringAsFixed(4)}, y: ${y.toStringAsFixed(4)})';
}
}

View file

@ -0,0 +1,139 @@
// PersonEntity represents information about a Person in the context of FaceClustering that is stored.
// On the remote server, the PersonEntity is stored as {Entity} with type person.
// On the device, this information is stored as [LocalEntityData] with type person.
import "package:flutter/foundation.dart";
class PersonEntity {
final String remoteID;
final PersonData data;
PersonEntity(
this.remoteID,
this.data,
);
// copyWith
PersonEntity copyWith({
String? remoteID,
PersonData? data,
}) {
return PersonEntity(
remoteID ?? this.remoteID,
data ?? this.data,
);
}
}
class ClusterInfo {
final int id;
final Set<String> faces;
ClusterInfo({
required this.id,
required this.faces,
});
// toJson
Map<String, dynamic> toJson() => {
'id': id,
'faces': faces.toList(),
};
// from Json
factory ClusterInfo.fromJson(Map<String, dynamic> json) {
return ClusterInfo(
id: json['id'] as int,
faces: (json['faces'] as List<dynamic>).map((e) => e as String).toSet(),
);
}
}
class PersonData {
final String name;
final bool isHidden;
String? avatarFaceId;
List<ClusterInfo>? assigned = List<ClusterInfo>.empty();
List<ClusterInfo>? rejected = List<ClusterInfo>.empty();
final String? birthDate;
bool hasAvatar() => avatarFaceId != null;
bool get isIgnored =>
(name.isEmpty || name == '(hidden)' || name == '(ignored)');
PersonData({
required this.name,
this.assigned,
this.rejected,
this.avatarFaceId,
this.isHidden = false,
this.birthDate,
});
// copyWith
PersonData copyWith({
String? name,
List<ClusterInfo>? assigned,
String? avatarFaceId,
bool? isHidden,
int? version,
String? birthDate,
}) {
return PersonData(
name: name ?? this.name,
assigned: assigned ?? this.assigned,
avatarFaceId: avatarFaceId ?? this.avatarFaceId,
isHidden: isHidden ?? this.isHidden,
birthDate: birthDate ?? this.birthDate,
);
}
void logStats() {
if (kDebugMode == false) return;
// log number of assigned and rejected clusters and total number of faces in each cluster
final StringBuffer sb = StringBuffer();
sb.writeln('Person: $name');
int assignedCount = 0;
for (final a in (assigned ?? <ClusterInfo>[])) {
assignedCount += a.faces.length;
}
sb.writeln('Assigned: ${assigned?.length} withFaces $assignedCount');
sb.writeln('Rejected: ${rejected?.length}');
if (assigned != null) {
for (var cluster in assigned!) {
sb.writeln('Cluster: ${cluster.id} - ${cluster.faces.length}');
}
}
debugPrint(sb.toString());
}
// toJson
Map<String, dynamic> toJson() => {
'name': name,
'assigned': assigned?.map((e) => e.toJson()).toList(),
'rejected': rejected?.map((e) => e.toJson()).toList(),
'avatarFaceId': avatarFaceId,
'isHidden': isHidden,
'birthDate': birthDate,
};
// fromJson
factory PersonData.fromJson(Map<String, dynamic> json) {
final assigned = (json['assigned'] == null || json['assigned'].length == 0)
? <ClusterInfo>[]
: List<ClusterInfo>.from(
json['assigned'].map((x) => ClusterInfo.fromJson(x)),
);
final rejected = (json['rejected'] == null || json['rejected'].length == 0)
? <ClusterInfo>[]
: List<ClusterInfo>.from(
json['rejected'].map((x) => ClusterInfo.fromJson(x)),
);
return PersonData(
name: json['name'] as String,
assigned: assigned,
rejected: rejected,
avatarFaceId: json['avatarFaceId'] as String?,
isHidden: json['isHidden'] as bool? ?? false,
birthDate: json['birthDate'] as String?,
);
}
}

View file

@ -34,6 +34,8 @@ class MessageLookup extends MessageLookupByLibrary {
"addViewers": m1,
"changeLocationOfSelectedItems": MessageLookupByLibrary.simpleMessage(
"Change location of selected items?"),
"clusteringProgress":
MessageLookupByLibrary.simpleMessage("Clustering progress"),
"contacts": MessageLookupByLibrary.simpleMessage("Contacts"),
"createCollaborativeLink":
MessageLookupByLibrary.simpleMessage("Create collaborative link"),
@ -44,7 +46,16 @@ class MessageLookup extends MessageLookupByLibrary {
"editsToLocationWillOnlyBeSeenWithinEnte":
MessageLookupByLibrary.simpleMessage(
"Edits to location will only be seen within Ente"),
"enterPersonName":
MessageLookupByLibrary.simpleMessage("Enter person name"),
"faceRecognition":
MessageLookupByLibrary.simpleMessage("Face recognition"),
"faceRecognitionIndexingDescription": MessageLookupByLibrary.simpleMessage(
"Please note that this will result in a higher bandwidth and battery usage until all items are indexed."),
"fileTypes": MessageLookupByLibrary.simpleMessage("File types"),
"foundFaces": MessageLookupByLibrary.simpleMessage("Found faces"),
"indexingIsPaused": MessageLookupByLibrary.simpleMessage(
"Indexing is paused, will automatically resume when device is ready"),
"joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"),
"locations": MessageLookupByLibrary.simpleMessage("Locations"),
"longPressAnEmailToVerifyEndToEndEncryption":
@ -55,6 +66,8 @@ class MessageLookup extends MessageLookupByLibrary {
"Modify your query, or try searching for"),
"moveToHiddenAlbum":
MessageLookupByLibrary.simpleMessage("Move to hidden album"),
"removePersonLabel":
MessageLookupByLibrary.simpleMessage("Remove person label"),
"search": MessageLookupByLibrary.simpleMessage("Search"),
"selectALocation":
MessageLookupByLibrary.simpleMessage("Select a location"),

View file

@ -227,6 +227,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Ich verstehe, dass ich meine Daten verlieren kann, wenn ich mein Passwort vergesse, da meine Daten <underline>Ende-zu-Ende-verschlüsselt</underline> sind."),
"activeSessions":
MessageLookupByLibrary.simpleMessage("Aktive Sitzungen"),
"addAName": MessageLookupByLibrary.simpleMessage("Add a name"),
"addANewEmail": MessageLookupByLibrary.simpleMessage(
"Neue E-Mail-Adresse hinzufügen"),
"addCollaborator":
@ -435,6 +436,8 @@ class MessageLookup extends MessageLookupByLibrary {
"Nach Aufnahmezeit gruppieren"),
"clubByFileName":
MessageLookupByLibrary.simpleMessage("Nach Dateiname gruppieren"),
"clusteringProgress":
MessageLookupByLibrary.simpleMessage("Clustering progress"),
"codeAppliedPageTitle":
MessageLookupByLibrary.simpleMessage("Code eingelöst"),
"codeCopiedToClipboard": MessageLookupByLibrary.simpleMessage(
@ -675,6 +678,8 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Passwort eingeben"),
"enterPasswordToEncrypt": MessageLookupByLibrary.simpleMessage(
"Gib ein Passwort ein, mit dem wir deine Daten verschlüsseln können"),
"enterPersonName":
MessageLookupByLibrary.simpleMessage("Enter person name"),
"enterReferralCode": MessageLookupByLibrary.simpleMessage(
"Gib den Weiterempfehlungs-Code ein"),
"enterThe6digitCodeFromnyourAuthenticatorApp":
@ -699,6 +704,10 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Protokolle exportieren"),
"exportYourData":
MessageLookupByLibrary.simpleMessage("Daten exportieren"),
"faceRecognition":
MessageLookupByLibrary.simpleMessage("Face recognition"),
"faceRecognitionIndexingDescription": MessageLookupByLibrary.simpleMessage(
"Please note that this will result in a higher bandwidth and battery usage until all items are indexed."),
"faces": MessageLookupByLibrary.simpleMessage("Gesichter"),
"failedToApplyCode": MessageLookupByLibrary.simpleMessage(
"Der Code konnte nicht aktiviert werden"),
@ -738,11 +747,14 @@ class MessageLookup extends MessageLookupByLibrary {
"filesBackedUpInAlbum": m23,
"filesDeleted":
MessageLookupByLibrary.simpleMessage("Dateien gelöscht"),
"findPeopleByName": MessageLookupByLibrary.simpleMessage(
"Find people quickly by searching by name"),
"flip": MessageLookupByLibrary.simpleMessage("Spiegeln"),
"forYourMemories":
MessageLookupByLibrary.simpleMessage("Als Erinnerung"),
"forgotPassword":
MessageLookupByLibrary.simpleMessage("Passwort vergessen"),
"foundFaces": MessageLookupByLibrary.simpleMessage("Found faces"),
"freeStorageClaimed": MessageLookupByLibrary.simpleMessage(
"Kostenlos hinzugefügter Speicherplatz"),
"freeStorageOnReferralSuccess": m24,
@ -807,6 +819,8 @@ class MessageLookup extends MessageLookupByLibrary {
"Falscher Wiederherstellungs-Schlüssel"),
"indexedItems":
MessageLookupByLibrary.simpleMessage("Indizierte Elemente"),
"indexingIsPaused": MessageLookupByLibrary.simpleMessage(
"Indexing is paused, will automatically resume when device is ready"),
"insecureDevice":
MessageLookupByLibrary.simpleMessage("Unsicheres Gerät"),
"installManually":
@ -1164,6 +1178,8 @@ class MessageLookup extends MessageLookupByLibrary {
"removeParticipant":
MessageLookupByLibrary.simpleMessage("Teilnehmer entfernen"),
"removeParticipantBody": m43,
"removePersonLabel":
MessageLookupByLibrary.simpleMessage("Remove person label"),
"removePublicLink":
MessageLookupByLibrary.simpleMessage("Öffentlichen Link entfernen"),
"removeShareItemsWarning": MessageLookupByLibrary.simpleMessage(

View file

@ -132,7 +132,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Please talk to ${providerName} support if you were charged";
static String m38(endDate) =>
"Free trial valid till ${endDate}.\nYou can purchase a paid plan afterwards.";
"Free trial valid till ${endDate}.\nYou can choose a paid plan afterwards.";
static String m39(toEmail) => "Please email us at ${toEmail}";
@ -225,6 +225,7 @@ class MessageLookup extends MessageLookupByLibrary {
"I understand that if I lose my password, I may lose my data since my data is <underline>end-to-end encrypted</underline>."),
"activeSessions":
MessageLookupByLibrary.simpleMessage("Active sessions"),
"addAName": MessageLookupByLibrary.simpleMessage("Add a name"),
"addANewEmail": MessageLookupByLibrary.simpleMessage("Add a new email"),
"addCollaborator":
MessageLookupByLibrary.simpleMessage("Add collaborator"),
@ -434,6 +435,8 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Club by capture time"),
"clubByFileName":
MessageLookupByLibrary.simpleMessage("Club by file name"),
"clusteringProgress":
MessageLookupByLibrary.simpleMessage("Clustering progress"),
"codeAppliedPageTitle":
MessageLookupByLibrary.simpleMessage("Code applied"),
"codeCopiedToClipboard":
@ -675,6 +678,8 @@ class MessageLookup extends MessageLookupByLibrary {
"enterPassword": MessageLookupByLibrary.simpleMessage("Enter password"),
"enterPasswordToEncrypt": MessageLookupByLibrary.simpleMessage(
"Enter a password we can use to encrypt your data"),
"enterPersonName":
MessageLookupByLibrary.simpleMessage("Enter person name"),
"enterReferralCode":
MessageLookupByLibrary.simpleMessage("Enter referral code"),
"enterThe6digitCodeFromnyourAuthenticatorApp":
@ -697,6 +702,10 @@ class MessageLookup extends MessageLookupByLibrary {
"exportLogs": MessageLookupByLibrary.simpleMessage("Export logs"),
"exportYourData":
MessageLookupByLibrary.simpleMessage("Export your data"),
"faceRecognition":
MessageLookupByLibrary.simpleMessage("Face recognition"),
"faceRecognitionIndexingDescription": MessageLookupByLibrary.simpleMessage(
"Please note that this will result in a higher bandwidth and battery usage until all items are indexed."),
"faces": MessageLookupByLibrary.simpleMessage("Faces"),
"failedToApplyCode":
MessageLookupByLibrary.simpleMessage("Failed to apply code"),
@ -736,11 +745,14 @@ class MessageLookup extends MessageLookupByLibrary {
"filesDeleted": MessageLookupByLibrary.simpleMessage("Files deleted"),
"filesSavedToGallery":
MessageLookupByLibrary.simpleMessage("Files saved to gallery"),
"findPeopleByName":
MessageLookupByLibrary.simpleMessage("Find people quickly by name"),
"flip": MessageLookupByLibrary.simpleMessage("Flip"),
"forYourMemories":
MessageLookupByLibrary.simpleMessage("for your memories"),
"forgotPassword":
MessageLookupByLibrary.simpleMessage("Forgot password"),
"foundFaces": MessageLookupByLibrary.simpleMessage("Found faces"),
"freeStorageClaimed":
MessageLookupByLibrary.simpleMessage("Free storage claimed"),
"freeStorageOnReferralSuccess": m24,
@ -801,6 +813,8 @@ class MessageLookup extends MessageLookupByLibrary {
"incorrectRecoveryKeyTitle":
MessageLookupByLibrary.simpleMessage("Incorrect recovery key"),
"indexedItems": MessageLookupByLibrary.simpleMessage("Indexed items"),
"indexingIsPaused": MessageLookupByLibrary.simpleMessage(
"Indexing is paused, will automatically resume when device is ready"),
"insecureDevice":
MessageLookupByLibrary.simpleMessage("Insecure device"),
"installManually":
@ -1022,6 +1036,7 @@ class MessageLookup extends MessageLookupByLibrary {
"paymentFailedTalkToProvider": m37,
"pendingItems": MessageLookupByLibrary.simpleMessage("Pending items"),
"pendingSync": MessageLookupByLibrary.simpleMessage("Pending sync"),
"people": MessageLookupByLibrary.simpleMessage("People"),
"peopleUsingYourCode":
MessageLookupByLibrary.simpleMessage("People using your code"),
"permDeleteWarning": MessageLookupByLibrary.simpleMessage(
@ -1151,6 +1166,8 @@ class MessageLookup extends MessageLookupByLibrary {
"removeParticipant":
MessageLookupByLibrary.simpleMessage("Remove participant"),
"removeParticipantBody": m43,
"removePersonLabel":
MessageLookupByLibrary.simpleMessage("Remove person label"),
"removePublicLink":
MessageLookupByLibrary.simpleMessage("Remove public link"),
"removeShareItemsWarning": MessageLookupByLibrary.simpleMessage(
@ -1208,8 +1225,8 @@ class MessageLookup extends MessageLookupByLibrary {
"Add descriptions like \"#trip\" in photo info to quickly find them here"),
"searchDatesEmptySection": MessageLookupByLibrary.simpleMessage(
"Search by a date, month or year"),
"searchFaceEmptySection":
MessageLookupByLibrary.simpleMessage("Find all photos of a person"),
"searchFaceEmptySection": MessageLookupByLibrary.simpleMessage(
"Persons will be shown here once indexing is done"),
"searchFileTypesAndNamesEmptySection":
MessageLookupByLibrary.simpleMessage("File types and names"),
"searchHint1":

View file

@ -367,6 +367,8 @@ class MessageLookup extends MessageLookupByLibrary {
"close": MessageLookupByLibrary.simpleMessage("Cerrar"),
"clubByCaptureTime": MessageLookupByLibrary.simpleMessage(
"Agrupar por tiempo de captura"),
"clusteringProgress":
MessageLookupByLibrary.simpleMessage("Clustering progress"),
"codeAppliedPageTitle":
MessageLookupByLibrary.simpleMessage("Código aplicado"),
"codeCopiedToClipboard": MessageLookupByLibrary.simpleMessage(
@ -585,6 +587,8 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Introduzca contraseña"),
"enterPasswordToEncrypt": MessageLookupByLibrary.simpleMessage(
"Introduzca una contraseña que podamos usar para cifrar sus datos"),
"enterPersonName":
MessageLookupByLibrary.simpleMessage("Enter person name"),
"enterReferralCode": MessageLookupByLibrary.simpleMessage(
"Ingresar código de referencia"),
"enterThe6digitCodeFromnyourAuthenticatorApp":
@ -609,6 +613,10 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Exportar registros"),
"exportYourData":
MessageLookupByLibrary.simpleMessage("Exportar tus datos"),
"faceRecognition":
MessageLookupByLibrary.simpleMessage("Face recognition"),
"faceRecognitionIndexingDescription": MessageLookupByLibrary.simpleMessage(
"Please note that this will result in a higher bandwidth and battery usage until all items are indexed."),
"failedToApplyCode":
MessageLookupByLibrary.simpleMessage("Error al aplicar el código"),
"failedToCancel":
@ -647,6 +655,7 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("para tus recuerdos"),
"forgotPassword":
MessageLookupByLibrary.simpleMessage("Olvidé mi contraseña"),
"foundFaces": MessageLookupByLibrary.simpleMessage("Found faces"),
"freeStorageClaimed": MessageLookupByLibrary.simpleMessage(
"Almacenamiento gratuito reclamado"),
"freeStorageOnReferralSuccess": m24,
@ -690,6 +699,8 @@ class MessageLookup extends MessageLookupByLibrary {
"La clave de recuperación introducida es incorrecta"),
"incorrectRecoveryKeyTitle": MessageLookupByLibrary.simpleMessage(
"Clave de recuperación incorrecta"),
"indexingIsPaused": MessageLookupByLibrary.simpleMessage(
"Indexing is paused, will automatically resume when device is ready"),
"insecureDevice":
MessageLookupByLibrary.simpleMessage("Dispositivo inseguro"),
"installManually":
@ -997,6 +1008,8 @@ class MessageLookup extends MessageLookupByLibrary {
"removeParticipant":
MessageLookupByLibrary.simpleMessage("Quitar participante"),
"removeParticipantBody": m43,
"removePersonLabel":
MessageLookupByLibrary.simpleMessage("Remove person label"),
"removePublicLink":
MessageLookupByLibrary.simpleMessage("Quitar enlace público"),
"removeShareItemsWarning": MessageLookupByLibrary.simpleMessage(

View file

@ -425,6 +425,8 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Grouper par durée"),
"clubByFileName":
MessageLookupByLibrary.simpleMessage("Grouper par nom de fichier"),
"clusteringProgress":
MessageLookupByLibrary.simpleMessage("Clustering progress"),
"codeAppliedPageTitle":
MessageLookupByLibrary.simpleMessage("Code appliqué"),
"codeCopiedToClipboard": MessageLookupByLibrary.simpleMessage(
@ -665,6 +667,8 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Saisissez le mot de passe"),
"enterPasswordToEncrypt": MessageLookupByLibrary.simpleMessage(
"Entrez un mot de passe que nous pouvons utiliser pour chiffrer vos données"),
"enterPersonName":
MessageLookupByLibrary.simpleMessage("Enter person name"),
"enterReferralCode": MessageLookupByLibrary.simpleMessage(
"Entrez le code de parrainage"),
"enterThe6digitCodeFromnyourAuthenticatorApp":
@ -688,6 +692,10 @@ class MessageLookup extends MessageLookupByLibrary {
"exportLogs": MessageLookupByLibrary.simpleMessage("Exporter les logs"),
"exportYourData":
MessageLookupByLibrary.simpleMessage("Exportez vos données"),
"faceRecognition":
MessageLookupByLibrary.simpleMessage("Face recognition"),
"faceRecognitionIndexingDescription": MessageLookupByLibrary.simpleMessage(
"Please note that this will result in a higher bandwidth and battery usage until all items are indexed."),
"faces": MessageLookupByLibrary.simpleMessage("Visages"),
"failedToApplyCode": MessageLookupByLibrary.simpleMessage(
"Impossible d\'appliquer le code"),
@ -732,6 +740,7 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("pour vos souvenirs"),
"forgotPassword":
MessageLookupByLibrary.simpleMessage("Mot de passe oublié"),
"foundFaces": MessageLookupByLibrary.simpleMessage("Found faces"),
"freeStorageClaimed":
MessageLookupByLibrary.simpleMessage("Stockage gratuit réclamé"),
"freeStorageOnReferralSuccess": m24,
@ -795,6 +804,8 @@ class MessageLookup extends MessageLookupByLibrary {
"La clé de secours que vous avez entrée est incorrecte"),
"incorrectRecoveryKeyTitle":
MessageLookupByLibrary.simpleMessage("Clé de secours non valide"),
"indexingIsPaused": MessageLookupByLibrary.simpleMessage(
"Indexing is paused, will automatically resume when device is ready"),
"insecureDevice":
MessageLookupByLibrary.simpleMessage("Appareil non sécurisé"),
"installManually":
@ -1129,6 +1140,8 @@ class MessageLookup extends MessageLookupByLibrary {
"removeParticipant":
MessageLookupByLibrary.simpleMessage("Supprimer le participant"),
"removeParticipantBody": m43,
"removePersonLabel":
MessageLookupByLibrary.simpleMessage("Remove person label"),
"removePublicLink":
MessageLookupByLibrary.simpleMessage("Supprimer le lien public"),
"removeShareItemsWarning": MessageLookupByLibrary.simpleMessage(

View file

@ -411,6 +411,8 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Club per tempo di cattura"),
"clubByFileName":
MessageLookupByLibrary.simpleMessage("Unisci per nome file"),
"clusteringProgress":
MessageLookupByLibrary.simpleMessage("Clustering progress"),
"codeAppliedPageTitle":
MessageLookupByLibrary.simpleMessage("Codice applicato"),
"codeCopiedToClipboard": MessageLookupByLibrary.simpleMessage(
@ -644,6 +646,8 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Inserisci password"),
"enterPasswordToEncrypt": MessageLookupByLibrary.simpleMessage(
"Inserisci una password per criptare i tuoi dati"),
"enterPersonName":
MessageLookupByLibrary.simpleMessage("Enter person name"),
"enterReferralCode": MessageLookupByLibrary.simpleMessage(
"Inserisci il codice di invito"),
"enterThe6digitCodeFromnyourAuthenticatorApp":
@ -665,6 +669,10 @@ class MessageLookup extends MessageLookupByLibrary {
"Questo link è scaduto. Si prega di selezionare un nuovo orario di scadenza o disabilitare la scadenza del link."),
"exportLogs": MessageLookupByLibrary.simpleMessage("Esporta log"),
"exportYourData": MessageLookupByLibrary.simpleMessage("Esporta dati"),
"faceRecognition":
MessageLookupByLibrary.simpleMessage("Face recognition"),
"faceRecognitionIndexingDescription": MessageLookupByLibrary.simpleMessage(
"Please note that this will result in a higher bandwidth and battery usage until all items are indexed."),
"failedToApplyCode": MessageLookupByLibrary.simpleMessage(
"Impossibile applicare il codice"),
"failedToCancel":
@ -704,6 +712,7 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("per i tuoi ricordi"),
"forgotPassword":
MessageLookupByLibrary.simpleMessage("Password dimenticata"),
"foundFaces": MessageLookupByLibrary.simpleMessage("Found faces"),
"freeStorageClaimed":
MessageLookupByLibrary.simpleMessage("Spazio gratuito richiesto"),
"freeStorageOnReferralSuccess": m24,
@ -764,6 +773,8 @@ class MessageLookup extends MessageLookupByLibrary {
"Il codice che hai inserito non è corretto"),
"incorrectRecoveryKeyTitle":
MessageLookupByLibrary.simpleMessage("Chiave di recupero errata"),
"indexingIsPaused": MessageLookupByLibrary.simpleMessage(
"Indexing is paused, will automatically resume when device is ready"),
"insecureDevice":
MessageLookupByLibrary.simpleMessage("Dispositivo non sicuro"),
"installManually":
@ -1090,6 +1101,8 @@ class MessageLookup extends MessageLookupByLibrary {
"removeParticipant":
MessageLookupByLibrary.simpleMessage("Rimuovi partecipante"),
"removeParticipantBody": m43,
"removePersonLabel":
MessageLookupByLibrary.simpleMessage("Remove person label"),
"removePublicLink":
MessageLookupByLibrary.simpleMessage("Rimuovi link pubblico"),
"removeShareItemsWarning": MessageLookupByLibrary.simpleMessage(

View file

@ -34,6 +34,8 @@ class MessageLookup extends MessageLookupByLibrary {
"addViewers": m1,
"changeLocationOfSelectedItems": MessageLookupByLibrary.simpleMessage(
"Change location of selected items?"),
"clusteringProgress":
MessageLookupByLibrary.simpleMessage("Clustering progress"),
"contacts": MessageLookupByLibrary.simpleMessage("Contacts"),
"createCollaborativeLink":
MessageLookupByLibrary.simpleMessage("Create collaborative link"),
@ -44,7 +46,16 @@ class MessageLookup extends MessageLookupByLibrary {
"editsToLocationWillOnlyBeSeenWithinEnte":
MessageLookupByLibrary.simpleMessage(
"Edits to location will only be seen within Ente"),
"enterPersonName":
MessageLookupByLibrary.simpleMessage("Enter person name"),
"faceRecognition":
MessageLookupByLibrary.simpleMessage("Face recognition"),
"faceRecognitionIndexingDescription": MessageLookupByLibrary.simpleMessage(
"Please note that this will result in a higher bandwidth and battery usage until all items are indexed."),
"fileTypes": MessageLookupByLibrary.simpleMessage("File types"),
"foundFaces": MessageLookupByLibrary.simpleMessage("Found faces"),
"indexingIsPaused": MessageLookupByLibrary.simpleMessage(
"Indexing is paused, will automatically resume when device is ready"),
"joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"),
"locations": MessageLookupByLibrary.simpleMessage("Locations"),
"longPressAnEmailToVerifyEndToEndEncryption":
@ -55,6 +66,8 @@ class MessageLookup extends MessageLookupByLibrary {
"Modify your query, or try searching for"),
"moveToHiddenAlbum":
MessageLookupByLibrary.simpleMessage("Move to hidden album"),
"removePersonLabel":
MessageLookupByLibrary.simpleMessage("Remove person label"),
"search": MessageLookupByLibrary.simpleMessage("Search"),
"selectALocation":
MessageLookupByLibrary.simpleMessage("Select a location"),

View file

@ -447,6 +447,8 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Samenvoegen op tijd"),
"clubByFileName":
MessageLookupByLibrary.simpleMessage("Samenvoegen op bestandsnaam"),
"clusteringProgress":
MessageLookupByLibrary.simpleMessage("Clustering progress"),
"codeAppliedPageTitle":
MessageLookupByLibrary.simpleMessage("Code toegepast"),
"codeCopiedToClipboard": MessageLookupByLibrary.simpleMessage(
@ -723,6 +725,10 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Logboek exporteren"),
"exportYourData":
MessageLookupByLibrary.simpleMessage("Exporteer je gegevens"),
"faceRecognition":
MessageLookupByLibrary.simpleMessage("Face recognition"),
"faceRecognitionIndexingDescription": MessageLookupByLibrary.simpleMessage(
"Please note that this will result in a higher bandwidth and battery usage until all items are indexed."),
"faces": MessageLookupByLibrary.simpleMessage("Gezichten"),
"failedToApplyCode":
MessageLookupByLibrary.simpleMessage("Code toepassen mislukt"),
@ -771,6 +777,7 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("voor uw herinneringen"),
"forgotPassword":
MessageLookupByLibrary.simpleMessage("Wachtwoord vergeten"),
"foundFaces": MessageLookupByLibrary.simpleMessage("Found faces"),
"freeStorageClaimed":
MessageLookupByLibrary.simpleMessage("Gratis opslag geclaimd"),
"freeStorageOnReferralSuccess": m24,
@ -833,6 +840,8 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Onjuiste herstelsleutel"),
"indexedItems":
MessageLookupByLibrary.simpleMessage("Geïndexeerde bestanden"),
"indexingIsPaused": MessageLookupByLibrary.simpleMessage(
"Indexing is paused, will automatically resume when device is ready"),
"insecureDevice":
MessageLookupByLibrary.simpleMessage("Onveilig apparaat"),
"installManually":

View file

@ -39,6 +39,8 @@ class MessageLookup extends MessageLookupByLibrary {
"cancel": MessageLookupByLibrary.simpleMessage("Avbryt"),
"changeLocationOfSelectedItems": MessageLookupByLibrary.simpleMessage(
"Change location of selected items?"),
"clusteringProgress":
MessageLookupByLibrary.simpleMessage("Clustering progress"),
"confirmAccountDeletion":
MessageLookupByLibrary.simpleMessage("Bekreft sletting av konto"),
"confirmDeletePrompt": MessageLookupByLibrary.simpleMessage(
@ -57,12 +59,21 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage(
"Edits to location will only be seen within Ente"),
"email": MessageLookupByLibrary.simpleMessage("E-post"),
"enterPersonName":
MessageLookupByLibrary.simpleMessage("Enter person name"),
"enterValidEmail": MessageLookupByLibrary.simpleMessage(
"Vennligst skriv inn en gyldig e-postadresse."),
"enterYourEmailAddress": MessageLookupByLibrary.simpleMessage(
"Skriv inn e-postadressen din"),
"faceRecognition":
MessageLookupByLibrary.simpleMessage("Face recognition"),
"faceRecognitionIndexingDescription": MessageLookupByLibrary.simpleMessage(
"Please note that this will result in a higher bandwidth and battery usage until all items are indexed."),
"feedback": MessageLookupByLibrary.simpleMessage("Tilbakemelding"),
"fileTypes": MessageLookupByLibrary.simpleMessage("File types"),
"foundFaces": MessageLookupByLibrary.simpleMessage("Found faces"),
"indexingIsPaused": MessageLookupByLibrary.simpleMessage(
"Indexing is paused, will automatically resume when device is ready"),
"invalidEmailAddress":
MessageLookupByLibrary.simpleMessage("Ugyldig e-postadresse"),
"joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"),
@ -77,6 +88,8 @@ class MessageLookup extends MessageLookupByLibrary {
"Modify your query, or try searching for"),
"moveToHiddenAlbum":
MessageLookupByLibrary.simpleMessage("Move to hidden album"),
"removePersonLabel":
MessageLookupByLibrary.simpleMessage("Remove person label"),
"search": MessageLookupByLibrary.simpleMessage("Search"),
"selectALocation":
MessageLookupByLibrary.simpleMessage("Select a location"),

View file

@ -49,6 +49,8 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Zmień hasło"),
"checkInboxAndSpamFolder": MessageLookupByLibrary.simpleMessage(
"Sprawdź swoją skrzynkę odbiorczą (i spam), aby zakończyć weryfikację"),
"clusteringProgress":
MessageLookupByLibrary.simpleMessage("Clustering progress"),
"codeCopiedToClipboard": MessageLookupByLibrary.simpleMessage(
"Kod został skopiowany do schowka"),
"confirm": MessageLookupByLibrary.simpleMessage("Potwierdź"),
@ -101,6 +103,8 @@ class MessageLookup extends MessageLookupByLibrary {
"Wprowadź nowe hasło, którego możemy użyć do zaszyfrowania Twoich danych"),
"enterPasswordToEncrypt": MessageLookupByLibrary.simpleMessage(
"Wprowadź hasło, którego możemy użyć do zaszyfrowania Twoich danych"),
"enterPersonName":
MessageLookupByLibrary.simpleMessage("Enter person name"),
"enterValidEmail": MessageLookupByLibrary.simpleMessage(
"Podaj poprawny adres e-mail."),
"enterYourEmailAddress":
@ -109,10 +113,15 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Wprowadź hasło"),
"enterYourRecoveryKey": MessageLookupByLibrary.simpleMessage(
"Wprowadź swój klucz odzyskiwania"),
"faceRecognition":
MessageLookupByLibrary.simpleMessage("Face recognition"),
"faceRecognitionIndexingDescription": MessageLookupByLibrary.simpleMessage(
"Please note that this will result in a higher bandwidth and battery usage until all items are indexed."),
"feedback": MessageLookupByLibrary.simpleMessage("Informacja zwrotna"),
"fileTypes": MessageLookupByLibrary.simpleMessage("File types"),
"forgotPassword":
MessageLookupByLibrary.simpleMessage("Nie pamiętam hasła"),
"foundFaces": MessageLookupByLibrary.simpleMessage("Found faces"),
"generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage(
"Generowanie kluczy szyfrujących..."),
"howItWorks": MessageLookupByLibrary.simpleMessage("Jak to działa"),
@ -122,6 +131,8 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Kod jest nieprawidłowy"),
"incorrectRecoveryKeyTitle": MessageLookupByLibrary.simpleMessage(
"Nieprawidłowy klucz odzyskiwania"),
"indexingIsPaused": MessageLookupByLibrary.simpleMessage(
"Indexing is paused, will automatically resume when device is ready"),
"invalidEmailAddress":
MessageLookupByLibrary.simpleMessage("Nieprawidłowy adres e-mail"),
"joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"),
@ -166,6 +177,8 @@ class MessageLookup extends MessageLookupByLibrary {
"Jeśli zapomnisz hasła, jedynym sposobem odzyskania danych jest ten klucz."),
"recoverySuccessful":
MessageLookupByLibrary.simpleMessage("Odzyskano pomyślnie!"),
"removePersonLabel":
MessageLookupByLibrary.simpleMessage("Remove person label"),
"resendEmail":
MessageLookupByLibrary.simpleMessage("Wyślij e-mail ponownie"),
"resetPasswordTitle":

View file

@ -98,7 +98,7 @@ class MessageLookup extends MessageLookupByLibrary {
"${storageAmountInGB} GB cada vez que alguém se inscrever para um plano pago e aplica o seu código";
static String m25(freeAmount, storageUnit) =>
"${freeAmount} ${storageUnit} grátis";
"${freeAmount} ${storageUnit} livre";
static String m26(endDate) => "Teste gratuito acaba em ${endDate}";
@ -225,6 +225,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Eu entendo que se eu perder minha senha, posso perder meus dados, já que meus dados são <underline>criptografados de ponta a ponta</underline>."),
"activeSessions":
MessageLookupByLibrary.simpleMessage("Sessões ativas"),
"addAName": MessageLookupByLibrary.simpleMessage("Adicione um nome"),
"addANewEmail":
MessageLookupByLibrary.simpleMessage("Adicionar um novo email"),
"addCollaborator":
@ -445,6 +446,8 @@ class MessageLookup extends MessageLookupByLibrary {
"Agrupar por tempo de captura"),
"clubByFileName": MessageLookupByLibrary.simpleMessage(
"Agrupar pelo nome de arquivo"),
"clusteringProgress":
MessageLookupByLibrary.simpleMessage("Progresso de agrupamento"),
"codeAppliedPageTitle":
MessageLookupByLibrary.simpleMessage("Código aplicado"),
"codeCopiedToClipboard": MessageLookupByLibrary.simpleMessage(
@ -587,7 +590,7 @@ class MessageLookup extends MessageLookupByLibrary {
"descriptions": MessageLookupByLibrary.simpleMessage("Descrições"),
"deselectAll": MessageLookupByLibrary.simpleMessage("Desmarcar todos"),
"designedToOutlive":
MessageLookupByLibrary.simpleMessage("Feito para ter logenvidade"),
MessageLookupByLibrary.simpleMessage("Feito para ter longevidade"),
"details": MessageLookupByLibrary.simpleMessage("Detalhes"),
"devAccountChanged": MessageLookupByLibrary.simpleMessage(
"A conta de desenvolvedor que usamos para publicar o Ente na App Store foi alterada. Por esse motivo, você precisará fazer entrar novamente.\n\nPedimos desculpas pelo inconveniente, mas isso era inevitável."),
@ -690,6 +693,8 @@ class MessageLookup extends MessageLookupByLibrary {
"enterPassword": MessageLookupByLibrary.simpleMessage("Digite a senha"),
"enterPasswordToEncrypt": MessageLookupByLibrary.simpleMessage(
"Insira a senha para criptografar seus dados"),
"enterPersonName":
MessageLookupByLibrary.simpleMessage("Inserir nome da pessoa"),
"enterReferralCode": MessageLookupByLibrary.simpleMessage(
"Insira o código de referência"),
"enterThe6digitCodeFromnyourAuthenticatorApp":
@ -714,6 +719,10 @@ class MessageLookup extends MessageLookupByLibrary {
"exportLogs": MessageLookupByLibrary.simpleMessage("Exportar logs"),
"exportYourData":
MessageLookupByLibrary.simpleMessage("Exportar seus dados"),
"faceRecognition":
MessageLookupByLibrary.simpleMessage("Reconhecimento facial"),
"faceRecognitionIndexingDescription": MessageLookupByLibrary.simpleMessage(
"Por favor, note que isso resultará em uma largura de banda maior e uso de bateria até que todos os itens sejam indexados."),
"faces": MessageLookupByLibrary.simpleMessage("Rostos"),
"failedToApplyCode":
MessageLookupByLibrary.simpleMessage("Falha ao aplicar o código"),
@ -755,11 +764,15 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Arquivos excluídos"),
"filesSavedToGallery":
MessageLookupByLibrary.simpleMessage("Arquivos salvos na galeria"),
"findPeopleByName": MessageLookupByLibrary.simpleMessage(
"Encontre pessoas rapidamente por nome"),
"flip": MessageLookupByLibrary.simpleMessage("Inverter"),
"forYourMemories":
MessageLookupByLibrary.simpleMessage("para suas memórias"),
"forgotPassword":
MessageLookupByLibrary.simpleMessage("Esqueceu sua senha"),
"foundFaces":
MessageLookupByLibrary.simpleMessage("Rostos encontrados"),
"freeStorageClaimed": MessageLookupByLibrary.simpleMessage(
"Armazenamento gratuito reivindicado"),
"freeStorageOnReferralSuccess": m24,
@ -823,6 +836,8 @@ class MessageLookup extends MessageLookupByLibrary {
"incorrectRecoveryKeyTitle": MessageLookupByLibrary.simpleMessage(
"Chave de recuperação incorreta"),
"indexedItems": MessageLookupByLibrary.simpleMessage("Itens indexados"),
"indexingIsPaused": MessageLookupByLibrary.simpleMessage(
"Indexing is paused, will automatically resume when device is ready"),
"insecureDevice":
MessageLookupByLibrary.simpleMessage("Dispositivo não seguro"),
"installManually":
@ -1057,6 +1072,7 @@ class MessageLookup extends MessageLookupByLibrary {
"pendingItems": MessageLookupByLibrary.simpleMessage("Itens pendentes"),
"pendingSync":
MessageLookupByLibrary.simpleMessage("Sincronização pendente"),
"people": MessageLookupByLibrary.simpleMessage("Pessoas"),
"peopleUsingYourCode":
MessageLookupByLibrary.simpleMessage("Pessoas que usam seu código"),
"permDeleteWarning": MessageLookupByLibrary.simpleMessage(
@ -1190,6 +1206,8 @@ class MessageLookup extends MessageLookupByLibrary {
"removeParticipant":
MessageLookupByLibrary.simpleMessage("Remover participante"),
"removeParticipantBody": m43,
"removePersonLabel":
MessageLookupByLibrary.simpleMessage("Remover etiqueta da pessoa"),
"removePublicLink":
MessageLookupByLibrary.simpleMessage("Remover link público"),
"removeShareItemsWarning": MessageLookupByLibrary.simpleMessage(
@ -1253,7 +1271,7 @@ class MessageLookup extends MessageLookupByLibrary {
"searchDatesEmptySection": MessageLookupByLibrary.simpleMessage(
"Pesquisar por data, mês ou ano"),
"searchFaceEmptySection": MessageLookupByLibrary.simpleMessage(
"Encontre todas as fotos de uma pessoa"),
"Pessoas serão exibidas aqui uma vez que a indexação é feita"),
"searchFileTypesAndNamesEmptySection":
MessageLookupByLibrary.simpleMessage("Tipos de arquivo e nomes"),
"searchHint1": MessageLookupByLibrary.simpleMessage(

View file

@ -382,6 +382,8 @@ class MessageLookup extends MessageLookupByLibrary {
"close": MessageLookupByLibrary.simpleMessage("关闭"),
"clubByCaptureTime": MessageLookupByLibrary.simpleMessage("按拍摄时间分组"),
"clubByFileName": MessageLookupByLibrary.simpleMessage("按文件名排序"),
"clusteringProgress":
MessageLookupByLibrary.simpleMessage("Clustering progress"),
"codeAppliedPageTitle": MessageLookupByLibrary.simpleMessage("代码已应用"),
"codeCopiedToClipboard":
MessageLookupByLibrary.simpleMessage("代码已复制到剪贴板"),
@ -543,7 +545,7 @@ class MessageLookup extends MessageLookupByLibrary {
"emailVerificationToggle":
MessageLookupByLibrary.simpleMessage("电子邮件验证"),
"emailYourLogs": MessageLookupByLibrary.simpleMessage("通过电子邮件发送您的日志"),
"empty": MessageLookupByLibrary.simpleMessage(""),
"empty": MessageLookupByLibrary.simpleMessage(""),
"emptyTrash": MessageLookupByLibrary.simpleMessage("要清空回收站吗?"),
"enableMaps": MessageLookupByLibrary.simpleMessage("启用地图"),
"enableMapsDesc": MessageLookupByLibrary.simpleMessage(
@ -592,6 +594,10 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("此链接已过期。请选择新的过期时间或禁用链接有效期。"),
"exportLogs": MessageLookupByLibrary.simpleMessage("导出日志"),
"exportYourData": MessageLookupByLibrary.simpleMessage("导出您的数据"),
"faceRecognition":
MessageLookupByLibrary.simpleMessage("Face recognition"),
"faceRecognitionIndexingDescription": MessageLookupByLibrary.simpleMessage(
"Please note that this will result in a higher bandwidth and battery usage until all items are indexed."),
"faces": MessageLookupByLibrary.simpleMessage("人脸"),
"failedToApplyCode": MessageLookupByLibrary.simpleMessage("无法使用此代码"),
"failedToCancel": MessageLookupByLibrary.simpleMessage("取消失败"),
@ -626,6 +632,7 @@ class MessageLookup extends MessageLookupByLibrary {
"flip": MessageLookupByLibrary.simpleMessage("上下翻转"),
"forYourMemories": MessageLookupByLibrary.simpleMessage("为您的回忆"),
"forgotPassword": MessageLookupByLibrary.simpleMessage("忘记密码"),
"foundFaces": MessageLookupByLibrary.simpleMessage("Found faces"),
"freeStorageClaimed": MessageLookupByLibrary.simpleMessage("已领取的免费存储"),
"freeStorageOnReferralSuccess": m24,
"freeStorageSpace": m25,
@ -679,6 +686,8 @@ class MessageLookup extends MessageLookupByLibrary {
"incorrectRecoveryKeyTitle":
MessageLookupByLibrary.simpleMessage("不正确的恢复密钥"),
"indexedItems": MessageLookupByLibrary.simpleMessage("已索引项目"),
"indexingIsPaused": MessageLookupByLibrary.simpleMessage(
"Indexing is paused, will automatically resume when device is ready"),
"insecureDevice": MessageLookupByLibrary.simpleMessage("设备不安全"),
"installManually": MessageLookupByLibrary.simpleMessage("手动安装"),
"invalidEmailAddress":

View file

@ -4034,10 +4034,10 @@ class S {
);
}
/// `Free trial valid till {endDate}.\nYou can purchase a paid plan afterwards.`
/// `Free trial valid till {endDate}.\nYou can choose a paid plan afterwards.`
String playStoreFreeTrialValidTill(Object endDate) {
return Intl.message(
'Free trial valid till $endDate.\nYou can purchase a paid plan afterwards.',
'Free trial valid till $endDate.\nYou can choose a paid plan afterwards.',
name: 'playStoreFreeTrialValidTill',
desc: '',
args: [endDate],
@ -6969,10 +6969,10 @@ class S {
);
}
/// `Find all photos of a person`
/// `Persons will be shown here once indexing is done`
String get searchFaceEmptySection {
return Intl.message(
'Find all photos of a person',
'Persons will be shown here once indexing is done',
name: 'searchFaceEmptySection',
desc: '',
args: [],
@ -8168,6 +8168,16 @@ class S {
);
}
/// `People`
String get people {
return Intl.message(
'People',
name: 'people',
desc: '',
args: [],
);
}
/// `Contents`
String get contents {
return Intl.message(
@ -8388,26 +8398,6 @@ class S {
);
}
/// `Auto pair`
String get autoPair {
return Intl.message(
'Auto pair',
name: 'autoPair',
desc: '',
args: [],
);
}
/// `Pair with PIN`
String get pairWithPin {
return Intl.message(
'Pair with PIN',
name: 'pairWithPin',
desc: '',
args: [],
);
}
/// `Device not found`
String get deviceNotFound {
return Intl.message(
@ -8468,6 +8458,26 @@ class S {
);
}
/// `Add a name`
String get addAName {
return Intl.message(
'Add a name',
name: 'addAName',
desc: '',
args: [],
);
}
/// `Find people quickly by name`
String get findPeopleByName {
return Intl.message(
'Find people quickly by name',
name: 'findPeopleByName',
desc: '',
args: [],
);
}
/// `{count, plural, zero {Add viewer} one {Add viewer} other {Add viewers}}`
String addViewers(num count) {
return Intl.plural(
@ -8594,6 +8604,26 @@ class S {
);
}
/// `Enter person name`
String get enterPersonName {
return Intl.message(
'Enter person name',
name: 'enterPersonName',
desc: '',
args: [],
);
}
/// `Remove person label`
String get removePersonLabel {
return Intl.message(
'Remove person label',
name: 'removePersonLabel',
desc: '',
args: [],
);
}
/// `Auto pair works only with devices that support Chromecast.`
String get autoPairDesc {
return Intl.message(
@ -8703,6 +8733,76 @@ class S {
args: [],
);
}
/// `Auto pair`
String get autoPair {
return Intl.message(
'Auto pair',
name: 'autoPair',
desc: '',
args: [],
);
}
/// `Pair with PIN`
String get pairWithPin {
return Intl.message(
'Pair with PIN',
name: 'pairWithPin',
desc: '',
args: [],
);
}
/// `Face recognition`
String get faceRecognition {
return Intl.message(
'Face recognition',
name: 'faceRecognition',
desc: '',
args: [],
);
}
/// `Please note that this will result in a higher bandwidth and battery usage until all items are indexed.`
String get faceRecognitionIndexingDescription {
return Intl.message(
'Please note that this will result in a higher bandwidth and battery usage until all items are indexed.',
name: 'faceRecognitionIndexingDescription',
desc: '',
args: [],
);
}
/// `Found faces`
String get foundFaces {
return Intl.message(
'Found faces',
name: 'foundFaces',
desc: '',
args: [],
);
}
/// `Clustering progress`
String get clusteringProgress {
return Intl.message(
'Clustering progress',
name: 'clusteringProgress',
desc: '',
args: [],
);
}
/// `Indexing is paused, will automatically resume when device is ready`
String get indexingIsPaused {
return Intl.message(
'Indexing is paused, will automatically resume when device is ready',
name: 'indexingIsPaused',
desc: '',
args: [],
);
}
}
class AppLocalizationDelegate extends LocalizationsDelegate<S> {

View file

@ -0,0 +1,111 @@
//
// Generated code. Do not modify.
// source: ente/common/box.proto
//
// @dart = 2.12
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_final_fields
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
import 'dart:core' as $core;
import 'package:protobuf/protobuf.dart' as $pb;
/// CenterBox is a box where x,y is the center of the box
class CenterBox extends $pb.GeneratedMessage {
factory CenterBox({
$core.double? x,
$core.double? y,
$core.double? height,
$core.double? width,
}) {
final $result = create();
if (x != null) {
$result.x = x;
}
if (y != null) {
$result.y = y;
}
if (height != null) {
$result.height = height;
}
if (width != null) {
$result.width = width;
}
return $result;
}
CenterBox._() : super();
factory CenterBox.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
factory CenterBox.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'CenterBox', package: const $pb.PackageName(_omitMessageNames ? '' : 'ente.common'), createEmptyInstance: create)
..a<$core.double>(1, _omitFieldNames ? '' : 'x', $pb.PbFieldType.OF)
..a<$core.double>(2, _omitFieldNames ? '' : 'y', $pb.PbFieldType.OF)
..a<$core.double>(3, _omitFieldNames ? '' : 'height', $pb.PbFieldType.OF)
..a<$core.double>(4, _omitFieldNames ? '' : 'width', $pb.PbFieldType.OF)
..hasRequiredFields = false
;
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
'Will be removed in next major version')
CenterBox clone() => CenterBox()..mergeFromMessage(this);
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
'Will be removed in next major version')
CenterBox copyWith(void Function(CenterBox) updates) => super.copyWith((message) => updates(message as CenterBox)) as CenterBox;
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static CenterBox create() => CenterBox._();
CenterBox createEmptyInstance() => create();
static $pb.PbList<CenterBox> createRepeated() => $pb.PbList<CenterBox>();
@$core.pragma('dart2js:noInline')
static CenterBox getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<CenterBox>(create);
static CenterBox? _defaultInstance;
@$pb.TagNumber(1)
$core.double get x => $_getN(0);
@$pb.TagNumber(1)
set x($core.double v) { $_setFloat(0, v); }
@$pb.TagNumber(1)
$core.bool hasX() => $_has(0);
@$pb.TagNumber(1)
void clearX() => clearField(1);
@$pb.TagNumber(2)
$core.double get y => $_getN(1);
@$pb.TagNumber(2)
set y($core.double v) { $_setFloat(1, v); }
@$pb.TagNumber(2)
$core.bool hasY() => $_has(1);
@$pb.TagNumber(2)
void clearY() => clearField(2);
@$pb.TagNumber(3)
$core.double get height => $_getN(2);
@$pb.TagNumber(3)
set height($core.double v) { $_setFloat(2, v); }
@$pb.TagNumber(3)
$core.bool hasHeight() => $_has(2);
@$pb.TagNumber(3)
void clearHeight() => clearField(3);
@$pb.TagNumber(4)
$core.double get width => $_getN(3);
@$pb.TagNumber(4)
set width($core.double v) { $_setFloat(3, v); }
@$pb.TagNumber(4)
$core.bool hasWidth() => $_has(3);
@$pb.TagNumber(4)
void clearWidth() => clearField(4);
}
const _omitFieldNames = $core.bool.fromEnvironment('protobuf.omit_field_names');
const _omitMessageNames = $core.bool.fromEnvironment('protobuf.omit_message_names');

View file

@ -0,0 +1,11 @@
//
// Generated code. Do not modify.
// source: ente/common/box.proto
//
// @dart = 2.12
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_final_fields
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import

View file

@ -0,0 +1,38 @@
//
// Generated code. Do not modify.
// source: ente/common/box.proto
//
// @dart = 2.12
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_final_fields
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
import 'dart:convert' as $convert;
import 'dart:core' as $core;
import 'dart:typed_data' as $typed_data;
@$core.Deprecated('Use centerBoxDescriptor instead')
const CenterBox$json = {
'1': 'CenterBox',
'2': [
{'1': 'x', '3': 1, '4': 1, '5': 2, '9': 0, '10': 'x', '17': true},
{'1': 'y', '3': 2, '4': 1, '5': 2, '9': 1, '10': 'y', '17': true},
{'1': 'height', '3': 3, '4': 1, '5': 2, '9': 2, '10': 'height', '17': true},
{'1': 'width', '3': 4, '4': 1, '5': 2, '9': 3, '10': 'width', '17': true},
],
'8': [
{'1': '_x'},
{'1': '_y'},
{'1': '_height'},
{'1': '_width'},
],
};
/// Descriptor for `CenterBox`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List centerBoxDescriptor = $convert.base64Decode(
'CglDZW50ZXJCb3gSEQoBeBgBIAEoAkgAUgF4iAEBEhEKAXkYAiABKAJIAVIBeYgBARIbCgZoZW'
'lnaHQYAyABKAJIAlIGaGVpZ2h0iAEBEhkKBXdpZHRoGAQgASgCSANSBXdpZHRoiAEBQgQKAl94'
'QgQKAl95QgkKB19oZWlnaHRCCAoGX3dpZHRo');

View file

@ -0,0 +1,14 @@
//
// Generated code. Do not modify.
// source: ente/common/box.proto
//
// @dart = 2.12
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names
// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_final_fields
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
export 'box.pb.dart';

View file

@ -0,0 +1,83 @@
//
// Generated code. Do not modify.
// source: ente/common/point.proto
//
// @dart = 2.12
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_final_fields
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
import 'dart:core' as $core;
import 'package:protobuf/protobuf.dart' as $pb;
/// EPoint is a point in 2D space
class EPoint extends $pb.GeneratedMessage {
factory EPoint({
$core.double? x,
$core.double? y,
}) {
final $result = create();
if (x != null) {
$result.x = x;
}
if (y != null) {
$result.y = y;
}
return $result;
}
EPoint._() : super();
factory EPoint.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
factory EPoint.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EPoint', package: const $pb.PackageName(_omitMessageNames ? '' : 'ente.common'), createEmptyInstance: create)
..a<$core.double>(1, _omitFieldNames ? '' : 'x', $pb.PbFieldType.OF)
..a<$core.double>(2, _omitFieldNames ? '' : 'y', $pb.PbFieldType.OF)
..hasRequiredFields = false
;
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
'Will be removed in next major version')
EPoint clone() => EPoint()..mergeFromMessage(this);
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
'Will be removed in next major version')
EPoint copyWith(void Function(EPoint) updates) => super.copyWith((message) => updates(message as EPoint)) as EPoint;
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static EPoint create() => EPoint._();
EPoint createEmptyInstance() => create();
static $pb.PbList<EPoint> createRepeated() => $pb.PbList<EPoint>();
@$core.pragma('dart2js:noInline')
static EPoint getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<EPoint>(create);
static EPoint? _defaultInstance;
@$pb.TagNumber(1)
$core.double get x => $_getN(0);
@$pb.TagNumber(1)
set x($core.double v) { $_setFloat(0, v); }
@$pb.TagNumber(1)
$core.bool hasX() => $_has(0);
@$pb.TagNumber(1)
void clearX() => clearField(1);
@$pb.TagNumber(2)
$core.double get y => $_getN(1);
@$pb.TagNumber(2)
set y($core.double v) { $_setFloat(1, v); }
@$pb.TagNumber(2)
$core.bool hasY() => $_has(1);
@$pb.TagNumber(2)
void clearY() => clearField(2);
}
const _omitFieldNames = $core.bool.fromEnvironment('protobuf.omit_field_names');
const _omitMessageNames = $core.bool.fromEnvironment('protobuf.omit_message_names');

View file

@ -0,0 +1,11 @@
//
// Generated code. Do not modify.
// source: ente/common/point.proto
//
// @dart = 2.12
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_final_fields
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import

View file

@ -0,0 +1,33 @@
//
// Generated code. Do not modify.
// source: ente/common/point.proto
//
// @dart = 2.12
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_final_fields
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
import 'dart:convert' as $convert;
import 'dart:core' as $core;
import 'dart:typed_data' as $typed_data;
@$core.Deprecated('Use ePointDescriptor instead')
const EPoint$json = {
'1': 'EPoint',
'2': [
{'1': 'x', '3': 1, '4': 1, '5': 2, '9': 0, '10': 'x', '17': true},
{'1': 'y', '3': 2, '4': 1, '5': 2, '9': 1, '10': 'y', '17': true},
],
'8': [
{'1': '_x'},
{'1': '_y'},
],
};
/// Descriptor for `EPoint`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List ePointDescriptor = $convert.base64Decode(
'CgZFUG9pbnQSEQoBeBgBIAEoAkgAUgF4iAEBEhEKAXkYAiABKAJIAVIBeYgBAUIECgJfeEIECg'
'JfeQ==');

View file

@ -0,0 +1,14 @@
//
// Generated code. Do not modify.
// source: ente/common/point.proto
//
// @dart = 2.12
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names
// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_final_fields
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
export 'point.pb.dart';

View file

@ -0,0 +1,64 @@
//
// Generated code. Do not modify.
// source: ente/common/vector.proto
//
// @dart = 2.12
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_final_fields
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
import 'dart:core' as $core;
import 'package:protobuf/protobuf.dart' as $pb;
/// Vector is generic message for dealing with lists of doubles
/// It should ideally be used independently and not as a submessage
class EVector extends $pb.GeneratedMessage {
factory EVector({
$core.Iterable<$core.double>? values,
}) {
final $result = create();
if (values != null) {
$result.values.addAll(values);
}
return $result;
}
EVector._() : super();
factory EVector.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
factory EVector.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EVector', package: const $pb.PackageName(_omitMessageNames ? '' : 'ente.common'), createEmptyInstance: create)
..p<$core.double>(1, _omitFieldNames ? '' : 'values', $pb.PbFieldType.KD)
..hasRequiredFields = false
;
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
'Will be removed in next major version')
EVector clone() => EVector()..mergeFromMessage(this);
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
'Will be removed in next major version')
EVector copyWith(void Function(EVector) updates) => super.copyWith((message) => updates(message as EVector)) as EVector;
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static EVector create() => EVector._();
EVector createEmptyInstance() => create();
static $pb.PbList<EVector> createRepeated() => $pb.PbList<EVector>();
@$core.pragma('dart2js:noInline')
static EVector getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<EVector>(create);
static EVector? _defaultInstance;
@$pb.TagNumber(1)
$core.List<$core.double> get values => $_getList(0);
}
const _omitFieldNames = $core.bool.fromEnvironment('protobuf.omit_field_names');
const _omitMessageNames = $core.bool.fromEnvironment('protobuf.omit_message_names');

View file

@ -0,0 +1,11 @@
//
// Generated code. Do not modify.
// source: ente/common/vector.proto
//
// @dart = 2.12
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_final_fields
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import

View file

@ -0,0 +1,27 @@
//
// Generated code. Do not modify.
// source: ente/common/vector.proto
//
// @dart = 2.12
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_final_fields
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
import 'dart:convert' as $convert;
import 'dart:core' as $core;
import 'dart:typed_data' as $typed_data;
@$core.Deprecated('Use eVectorDescriptor instead')
const EVector$json = {
'1': 'EVector',
'2': [
{'1': 'values', '3': 1, '4': 3, '5': 1, '10': 'values'},
],
};
/// Descriptor for `EVector`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List eVectorDescriptor = $convert.base64Decode(
'CgdFVmVjdG9yEhYKBnZhbHVlcxgBIAMoAVIGdmFsdWVz');

View file

@ -0,0 +1,14 @@
//
// Generated code. Do not modify.
// source: ente/common/vector.proto
//
// @dart = 2.12
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names
// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_final_fields
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
export 'vector.pb.dart';

View file

@ -0,0 +1,169 @@
//
// Generated code. Do not modify.
// source: ente/ml/face.proto
//
// @dart = 2.12
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_final_fields
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
import 'dart:core' as $core;
import 'package:protobuf/protobuf.dart' as $pb;
import '../common/box.pb.dart' as $0;
import '../common/point.pb.dart' as $1;
class Detection extends $pb.GeneratedMessage {
factory Detection({
$0.CenterBox? box,
$1.EPoint? landmarks,
}) {
final $result = create();
if (box != null) {
$result.box = box;
}
if (landmarks != null) {
$result.landmarks = landmarks;
}
return $result;
}
Detection._() : super();
factory Detection.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
factory Detection.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Detection', package: const $pb.PackageName(_omitMessageNames ? '' : 'ente.ml'), createEmptyInstance: create)
..aOM<$0.CenterBox>(1, _omitFieldNames ? '' : 'box', subBuilder: $0.CenterBox.create)
..aOM<$1.EPoint>(2, _omitFieldNames ? '' : 'landmarks', subBuilder: $1.EPoint.create)
..hasRequiredFields = false
;
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
'Will be removed in next major version')
Detection clone() => Detection()..mergeFromMessage(this);
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
'Will be removed in next major version')
Detection copyWith(void Function(Detection) updates) => super.copyWith((message) => updates(message as Detection)) as Detection;
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static Detection create() => Detection._();
Detection createEmptyInstance() => create();
static $pb.PbList<Detection> createRepeated() => $pb.PbList<Detection>();
@$core.pragma('dart2js:noInline')
static Detection getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Detection>(create);
static Detection? _defaultInstance;
@$pb.TagNumber(1)
$0.CenterBox get box => $_getN(0);
@$pb.TagNumber(1)
set box($0.CenterBox v) { setField(1, v); }
@$pb.TagNumber(1)
$core.bool hasBox() => $_has(0);
@$pb.TagNumber(1)
void clearBox() => clearField(1);
@$pb.TagNumber(1)
$0.CenterBox ensureBox() => $_ensure(0);
@$pb.TagNumber(2)
$1.EPoint get landmarks => $_getN(1);
@$pb.TagNumber(2)
set landmarks($1.EPoint v) { setField(2, v); }
@$pb.TagNumber(2)
$core.bool hasLandmarks() => $_has(1);
@$pb.TagNumber(2)
void clearLandmarks() => clearField(2);
@$pb.TagNumber(2)
$1.EPoint ensureLandmarks() => $_ensure(1);
}
class Face extends $pb.GeneratedMessage {
factory Face({
$core.String? id,
Detection? detection,
$core.double? confidence,
}) {
final $result = create();
if (id != null) {
$result.id = id;
}
if (detection != null) {
$result.detection = detection;
}
if (confidence != null) {
$result.confidence = confidence;
}
return $result;
}
Face._() : super();
factory Face.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
factory Face.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Face', package: const $pb.PackageName(_omitMessageNames ? '' : 'ente.ml'), createEmptyInstance: create)
..aOS(1, _omitFieldNames ? '' : 'id')
..aOM<Detection>(2, _omitFieldNames ? '' : 'detection', subBuilder: Detection.create)
..a<$core.double>(3, _omitFieldNames ? '' : 'confidence', $pb.PbFieldType.OF)
..hasRequiredFields = false
;
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
'Will be removed in next major version')
Face clone() => Face()..mergeFromMessage(this);
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
'Will be removed in next major version')
Face copyWith(void Function(Face) updates) => super.copyWith((message) => updates(message as Face)) as Face;
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static Face create() => Face._();
Face createEmptyInstance() => create();
static $pb.PbList<Face> createRepeated() => $pb.PbList<Face>();
@$core.pragma('dart2js:noInline')
static Face getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Face>(create);
static Face? _defaultInstance;
@$pb.TagNumber(1)
$core.String get id => $_getSZ(0);
@$pb.TagNumber(1)
set id($core.String v) { $_setString(0, v); }
@$pb.TagNumber(1)
$core.bool hasId() => $_has(0);
@$pb.TagNumber(1)
void clearId() => clearField(1);
@$pb.TagNumber(2)
Detection get detection => $_getN(1);
@$pb.TagNumber(2)
set detection(Detection v) { setField(2, v); }
@$pb.TagNumber(2)
$core.bool hasDetection() => $_has(1);
@$pb.TagNumber(2)
void clearDetection() => clearField(2);
@$pb.TagNumber(2)
Detection ensureDetection() => $_ensure(1);
@$pb.TagNumber(3)
$core.double get confidence => $_getN(2);
@$pb.TagNumber(3)
set confidence($core.double v) { $_setFloat(2, v); }
@$pb.TagNumber(3)
$core.bool hasConfidence() => $_has(2);
@$pb.TagNumber(3)
void clearConfidence() => clearField(3);
}
const _omitFieldNames = $core.bool.fromEnvironment('protobuf.omit_field_names');
const _omitMessageNames = $core.bool.fromEnvironment('protobuf.omit_message_names');

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