Compare commits
323 commits
auth-v3.0.
...
main
Author | SHA1 | Date | |
---|---|---|---|
![]() |
ae61fc9c6f | ||
![]() |
c291fa70d3 | ||
![]() |
232acfa211 | ||
![]() |
f25f119ca1 | ||
![]() |
89a61b3bf7 | ||
![]() |
380d37267b | ||
![]() |
9cf5691e42 | ||
![]() |
8f474a4500 | ||
![]() |
c7be2270ff | ||
![]() |
ced1f0bd79 | ||
![]() |
9f58f1eeb3 | ||
![]() |
04be2b6a2c | ||
![]() |
9f361237b1 | ||
![]() |
8cb7cae7b7 | ||
![]() |
a2a209a849 | ||
![]() |
d413c4f4c1 | ||
![]() |
ee8976e92b | ||
![]() |
baa90c42ad | ||
![]() |
30ade541df | ||
![]() |
86fb8ebfaf | ||
![]() |
b2e8c3c0eb | ||
![]() |
e203a8378e | ||
![]() |
b100f1d4bf | ||
![]() |
7b4559f3ca | ||
![]() |
eac142025d | ||
![]() |
c5aa536c3b | ||
![]() |
05406333e4 | ||
![]() |
8ebd50606a | ||
![]() |
cbcfc243fc | ||
![]() |
7d497b5ae1 | ||
![]() |
b28f6c3d8c | ||
![]() |
71a8049a35 | ||
![]() |
e95cba0ace | ||
![]() |
e836ada0d6 | ||
![]() |
19a104374d | ||
![]() |
693ef45e2c | ||
![]() |
55bdb070ce | ||
![]() |
27127ff3d4 | ||
![]() |
345c706814 | ||
![]() |
49133b7b86 | ||
![]() |
3a5311cdcc | ||
![]() |
7182795732 | ||
![]() |
ca00b3b558 | ||
![]() |
4bcb765810 | ||
![]() |
17b49595a0 | ||
![]() |
b99c573d3a | ||
![]() |
d3d3e4dbed | ||
![]() |
ba1af5eaf0 | ||
![]() |
14cf59c1e5 | ||
![]() |
452872156a | ||
![]() |
4f31bd625d | ||
![]() |
6bf6f78147 | ||
![]() |
5576f99548 | ||
![]() |
5bbe768acb | ||
![]() |
babe378301 | ||
![]() |
b2fda16561 | ||
![]() |
6d289d73db | ||
![]() |
17acf4b3ee | ||
![]() |
4d666d4b01 | ||
![]() |
619f8319ed | ||
![]() |
3261da3515 | ||
![]() |
d0d491f7f5 | ||
![]() |
db3764d448 | ||
![]() |
5fe5451f5c | ||
![]() |
6d3d5d03f8 | ||
![]() |
582eb9e1ea | ||
![]() |
51770a11ef | ||
![]() |
1ea7a8f3a7 | ||
![]() |
b4536a7aee | ||
![]() |
9d2be29fad | ||
![]() |
f92a18efca | ||
![]() |
af382d483d | ||
![]() |
99f1ba799d | ||
![]() |
1548bcd378 | ||
![]() |
c2fc0a3d57 | ||
![]() |
39a706ea20 | ||
![]() |
38d6464f55 | ||
![]() |
c5b6297cea | ||
![]() |
390b4b1f81 | ||
![]() |
b19b34b3dc | ||
![]() |
5690d613bb | ||
![]() |
bb713cfc76 | ||
![]() |
4a0c93373d | ||
![]() |
b42759d473 | ||
![]() |
2e93281368 | ||
![]() |
c18be32c09 | ||
![]() |
650163c341 | ||
![]() |
d101208baa | ||
![]() |
76f7215269 | ||
![]() |
621c482529 | ||
![]() |
314c8f69f2 | ||
![]() |
1f45cf00c7 | ||
![]() |
e0e80ee91f | ||
![]() |
225278adb7 | ||
![]() |
8d30bfbefa | ||
![]() |
ad96f679c9 | ||
![]() |
4b896d3aab | ||
![]() |
533e6d06e7 | ||
![]() |
e88b5c99ba | ||
![]() |
1ec7e02695 | ||
![]() |
19e08cf803 | ||
![]() |
08073b927c | ||
![]() |
711a44412d | ||
![]() |
c9f94f062b | ||
![]() |
c8205b8475 | ||
![]() |
b0d3fcfe79 | ||
![]() |
11a354c560 | ||
![]() |
823f739c32 | ||
![]() |
f8876c8154 | ||
![]() |
90db45d845 | ||
![]() |
6a1f5945b9 | ||
![]() |
f7ca838428 | ||
![]() |
2b065dd68d | ||
![]() |
f168ea9e1e | ||
![]() |
58702103f3 | ||
![]() |
dfb3a6f65c | ||
![]() |
491f38b120 | ||
![]() |
79c0880c9c | ||
![]() |
834b8f78b7 | ||
![]() |
cbf0336cd0 | ||
![]() |
431d629641 | ||
![]() |
94c1cc011b | ||
![]() |
b26b0759d6 | ||
![]() |
d51fb99fd3 | ||
![]() |
0379216e05 | ||
![]() |
ccd486f659 | ||
![]() |
ce3ab55069 | ||
![]() |
34effef810 | ||
![]() |
56aceb589d | ||
![]() |
92a2506f8a | ||
![]() |
e23bc2602f | ||
![]() |
69beecb7bb | ||
![]() |
880b13f436 | ||
![]() |
9061caac99 | ||
![]() |
11cc8e46b7 | ||
![]() |
54820689c2 | ||
![]() |
acebb86fec | ||
![]() |
367e09599d | ||
![]() |
b9fe509567 | ||
![]() |
82bffd81de | ||
![]() |
7340443b86 | ||
![]() |
2cd1dfd720 | ||
![]() |
3c8d29bcdc | ||
![]() |
06a698ddbb | ||
![]() |
3b8c48e92d | ||
![]() |
3c0cb20a9b | ||
![]() |
74bb169f0d | ||
![]() |
302890baef | ||
![]() |
54e33d3f42 | ||
![]() |
0adb94f405 | ||
![]() |
7d634aa703 | ||
![]() |
b1e0c83733 | ||
![]() |
d4af7792d4 | ||
![]() |
f301ab57f2 | ||
![]() |
7b0f5909b5 | ||
![]() |
e9064f6904 | ||
![]() |
0d21fc77b5 | ||
![]() |
b26c6e9c0d | ||
![]() |
057d11f39b | ||
![]() |
c9de6d7a82 | ||
![]() |
698ac9f29e | ||
![]() |
a0d26c860c | ||
![]() |
bd2444d353 | ||
![]() |
ca24a86179 | ||
![]() |
fffe96a4c7 | ||
![]() |
0ec75c2435 | ||
![]() |
cb78c848d6 | ||
![]() |
6594db9393 | ||
![]() |
f6c40ee67d | ||
![]() |
36aa33ed5a | ||
![]() |
776dba4fb0 | ||
![]() |
7f49f530c5 | ||
![]() |
ef6fe80944 | ||
![]() |
370b28f9e4 | ||
![]() |
05e737cb11 | ||
![]() |
0fdb58eda1 | ||
![]() |
1ce90839fe | ||
![]() |
697946f415 | ||
![]() |
cc91cb8012 | ||
![]() |
754de7065f | ||
![]() |
5587373b42 | ||
![]() |
f1d1a4a9e1 | ||
![]() |
dc38a8bc9f | ||
![]() |
edf9f743f4 | ||
![]() |
fec040e528 | ||
![]() |
86f96a5713 | ||
![]() |
c3fb472287 | ||
![]() |
eaf8b9cebc | ||
![]() |
2ce9212457 | ||
![]() |
4fa59ce258 | ||
![]() |
59ed89cba1 | ||
![]() |
623b71715d | ||
![]() |
a74943698f | ||
![]() |
bfe8fd83ac | ||
![]() |
0a01cac57b | ||
![]() |
b7f248fa93 | ||
![]() |
d814b6cdf0 | ||
![]() |
1712bf60cb | ||
![]() |
369a5a5233 | ||
![]() |
9bae31d748 | ||
![]() |
11453b327f | ||
![]() |
7780c1c7b7 | ||
![]() |
0f1c98d0d0 | ||
![]() |
48fcbdc98c | ||
![]() |
90d0196d47 | ||
![]() |
30a8691c7f | ||
![]() |
69cea6786d | ||
![]() |
ccac5e73a3 | ||
![]() |
3e79c8cf28 | ||
![]() |
31dee1249d | ||
![]() |
e5a293a6ab | ||
![]() |
ffcb68b32f | ||
![]() |
a8af90dfee | ||
![]() |
6ee38cb291 | ||
![]() |
3810df1b20 | ||
![]() |
cc8e345a17 | ||
![]() |
63653411b8 | ||
![]() |
c4a6011621 | ||
![]() |
1ee52c780f | ||
![]() |
b402662c09 | ||
![]() |
51756d45d9 | ||
![]() |
a3bb7ad85a | ||
![]() |
17058299c1 | ||
![]() |
65de02d8d9 | ||
![]() |
1f9e222d6e | ||
![]() |
3d96be6c27 | ||
![]() |
1bbe495306 | ||
![]() |
a76f3ca1b3 | ||
![]() |
7800b7db32 | ||
![]() |
ea2a355bcc | ||
![]() |
d585b75514 | ||
![]() |
5caa32b1e0 | ||
![]() |
11402d7819 | ||
![]() |
a41f705dad | ||
![]() |
69b808e62c | ||
![]() |
1e1e629891 | ||
![]() |
a7e96d055c | ||
![]() |
5e2261f793 | ||
![]() |
206be5c16f | ||
![]() |
41c87efc5a | ||
![]() |
171af35d85 | ||
![]() |
99f47dc1ae | ||
![]() |
cc7a516eba | ||
![]() |
26436f116f | ||
![]() |
9eab415906 | ||
![]() |
14655e5633 | ||
![]() |
51dc8d1de6 | ||
![]() |
51568e6c56 | ||
![]() |
d2743f4121 | ||
![]() |
05c50e78bc | ||
![]() |
9ac7b29e96 | ||
![]() |
42106a72b3 | ||
![]() |
2504046e26 | ||
![]() |
a104f36561 | ||
![]() |
b26afdcf2e | ||
![]() |
bf707ae02d | ||
![]() |
68648d2f6c | ||
![]() |
371b8bf9cc | ||
![]() |
3b89471b87 | ||
![]() |
8a2117f9d4 | ||
![]() |
132ddd3648 | ||
![]() |
048aaee40d | ||
![]() |
04475110ce | ||
![]() |
02366eb27f | ||
![]() |
6c3953e855 | ||
![]() |
201286f59a | ||
![]() |
b00bffd785 | ||
![]() |
d477b55071 | ||
![]() |
227b7ddba0 | ||
![]() |
d12f570178 | ||
![]() |
70dc660f5a | ||
![]() |
e4c379963f | ||
![]() |
e44be63586 | ||
![]() |
6d5436c885 | ||
![]() |
d75abcf6a7 | ||
![]() |
b3229785a0 | ||
![]() |
bd8757bbb8 | ||
![]() |
92bafa7c38 | ||
![]() |
df756076e8 | ||
![]() |
ffc9eecbd1 | ||
![]() |
678efd1e8b | ||
![]() |
9ab82621b9 | ||
![]() |
59c2c7e343 | ||
![]() |
8c3c0b2128 | ||
![]() |
954581093d | ||
![]() |
78afae4013 | ||
![]() |
7811c58214 | ||
![]() |
85a8f6b7cf | ||
![]() |
f60e750848 | ||
![]() |
a086f36433 | ||
![]() |
4cb49c0b4a | ||
![]() |
334587474f | ||
![]() |
0d52737c49 | ||
![]() |
d4dc080231 | ||
![]() |
f8d35c3dcf | ||
![]() |
c20b9fa5fa | ||
![]() |
6a8fa727a9 | ||
![]() |
7712a8bd10 | ||
![]() |
4feb8fd1f1 | ||
![]() |
994876911a | ||
![]() |
d6398bd8fc | ||
![]() |
43064b617a | ||
![]() |
789783a370 | ||
![]() |
9db1197c19 | ||
![]() |
56a71c2cd8 | ||
![]() |
608c97603b | ||
![]() |
a9721e7744 | ||
![]() |
44e5af0434 | ||
![]() |
dfbdc94e61 | ||
![]() |
71d3427879 | ||
![]() |
d235ff1035 | ||
![]() |
ee5be7f339 | ||
![]() |
9b0e8b265d | ||
![]() |
c0f243cee0 | ||
![]() |
1bd2033a63 | ||
![]() |
982f0d8f77 | ||
![]() |
9e26b81adf | ||
![]() |
94cc26aead | ||
![]() |
2d5894c5d6 | ||
![]() |
0d43c0d326 | ||
![]() |
1b46e159da | ||
![]() |
d23638c30d | ||
![]() |
5724fad813 |
341 changed files with 4742 additions and 4377 deletions
14
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
14
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
2
.github/workflows/mobile-lint.yml
vendored
2
.github/workflows/mobile-lint.yml
vendored
|
@ -9,7 +9,7 @@ on:
|
|||
- ".github/workflows/mobile-lint.yml"
|
||||
|
||||
env:
|
||||
FLUTTER_VERSION: "3.19.5"
|
||||
FLUTTER_VERSION: "3.22.0"
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
|
|
|
@ -189,7 +189,7 @@ class _AppState extends State<App> with WindowListener, TrayListener {
|
|||
windowManager.show();
|
||||
break;
|
||||
case 'exit_app':
|
||||
windowManager.close();
|
||||
windowManager.destroy();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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": "アップデートは利用できません"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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": "更新不可用"
|
||||
}
|
|
@ -128,7 +128,7 @@ class Code {
|
|||
final code = Code(
|
||||
_getAccount(uri),
|
||||
issuer,
|
||||
_getDigits(uri, issuer),
|
||||
_getDigits(uri),
|
||||
_getPeriod(uri),
|
||||
getSanitizedSecret(uri.queryParameters['secret']!),
|
||||
_getAlgorithm(uri),
|
||||
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.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 _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)}";
|
||||
|
|
|
@ -4,7 +4,7 @@ import 'package:otp/otp.dart' as otp;
|
|||
import 'package:steam_totp/steam_totp.dart';
|
||||
|
||||
String getOTP(Code code) {
|
||||
if (code.issuer.toLowerCase() == 'steam') {
|
||||
if (code.type == Type.steam) {
|
||||
return _getSteamCode(code);
|
||||
}
|
||||
if (code.type == Type.hotp) {
|
||||
|
@ -39,7 +39,7 @@ String _getSteamCode(Code code, [bool isNext = false]) {
|
|||
}
|
||||
|
||||
String getNextTotp(Code code) {
|
||||
if (code.issuer.toLowerCase() == 'steam') {
|
||||
if (code.type == Type.steam) {
|
||||
return _getSteamCode(code, true);
|
||||
}
|
||||
return otp.OTP.generateTOTPCodeString(
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
"compare-versions": "^6.1",
|
||||
"electron-log": "^5.1",
|
||||
"electron-store": "^8.2",
|
||||
"electron-updater": "^6.1",
|
||||
"electron-updater": "^6.2",
|
||||
"ffmpeg-static": "^5.2",
|
||||
"html-entities": "^2.5",
|
||||
"jpeg-js": "^0.4",
|
||||
|
|
|
@ -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}`,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
@ -743,10 +743,10 @@ buffer@^5.1.0, buffer@^5.5.0:
|
|||
base64-js "^1.3.1"
|
||||
ieee754 "^1.1.13"
|
||||
|
||||
builder-util-runtime@9.2.3:
|
||||
version "9.2.3"
|
||||
resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz#0a82c7aca8eadef46d67b353c638f052c206b83c"
|
||||
integrity sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw==
|
||||
builder-util-runtime@9.2.4:
|
||||
version "9.2.4"
|
||||
resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz#13cd1763da621e53458739a1e63f7fcba673c42a"
|
||||
integrity sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==
|
||||
dependencies:
|
||||
debug "^4.3.4"
|
||||
sax "^1.2.4"
|
||||
|
@ -1251,12 +1251,12 @@ electron-store@^8.2:
|
|||
conf "^10.2.0"
|
||||
type-fest "^2.17.0"
|
||||
|
||||
electron-updater@^6.1:
|
||||
version "6.1.8"
|
||||
resolved "https://registry.yarnpkg.com/electron-updater/-/electron-updater-6.1.8.tgz#17637bca165322f4e526b13c99165f43e6f697d8"
|
||||
integrity sha512-hhOTfaFAd6wRHAfUaBhnAOYc+ymSGCWJLtFkw4xJqOvtpHmIdNHnXDV9m1MHC+A6q08Abx4Ykgyz/R5DGKNAMQ==
|
||||
electron-updater@^6.2:
|
||||
version "6.2.1"
|
||||
resolved "https://registry.yarnpkg.com/electron-updater/-/electron-updater-6.2.1.tgz#1c9adb9ba2a21a5dc50a8c434c45360d5e9fe6c9"
|
||||
integrity sha512-83eKIPW14qwZqUUM6wdsIRwVKZyjmHxQ4/8G+1C6iS5PdDt7b1umYQyj1/qPpH510GmHEQe4q0kCPe3qmb3a0Q==
|
||||
dependencies:
|
||||
builder-util-runtime "9.2.3"
|
||||
builder-util-runtime "9.2.4"
|
||||
fs-extra "^10.1.0"
|
||||
js-yaml "^4.1.0"
|
||||
lazy-val "^1.0.5"
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
|
|
79
docs/docs/auth/migration-guides/steam/index.md
Normal file
79
docs/docs/auth/migration-guides/steam/index.md
Normal 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
|
|
@ -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).
|
||||
|
|
|
@ -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`
|
||||
|
||||
|
|
|
@ -427,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
|
||||
|
|
|
@ -35,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();
|
||||
|
@ -585,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 {
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -13,6 +13,8 @@ import "package:photos/face/model/face.dart";
|
|||
import "package:photos/models/file/file.dart";
|
||||
import "package:photos/services/machine_learning/face_ml/face_clustering/face_info_for_clustering.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";
|
||||
import "package:photos/utils/ml_util.dart";
|
||||
import 'package:sqlite_async/sqlite_async.dart';
|
||||
|
||||
/// Stores all data for the FacesML-related features. The database can be accessed by `FaceMLDataDB.instance.database`.
|
||||
|
@ -27,12 +29,21 @@ class FaceMLDataDB {
|
|||
static final Logger _logger = Logger("FaceMLDataDB");
|
||||
|
||||
static const _databaseName = "ente.face_ml_db.db";
|
||||
static const _databaseVersion = 1;
|
||||
// static const _databaseVersion = 1;
|
||||
|
||||
FaceMLDataDB._privateConstructor();
|
||||
|
||||
static final FaceMLDataDB instance = FaceMLDataDB._privateConstructor();
|
||||
|
||||
static final _migrationScripts = [
|
||||
createFacesTable,
|
||||
createFaceClustersTable,
|
||||
createClusterPersonTable,
|
||||
createClusterSummaryTable,
|
||||
createNotPersonFeedbackTable,
|
||||
fcClusterIDIndex,
|
||||
];
|
||||
|
||||
// only have a single app-wide reference to the database
|
||||
static Future<SqliteDatabase>? _sqliteAsyncDBFuture;
|
||||
|
||||
|
@ -48,23 +59,42 @@ class FaceMLDataDB {
|
|||
_logger.info("Opening sqlite_async access: DB path " + databaseDirectory);
|
||||
final asyncDBConnection =
|
||||
SqliteDatabase(path: databaseDirectory, maxReaders: 2);
|
||||
await _onCreate(asyncDBConnection);
|
||||
final stopwatch = Stopwatch()..start();
|
||||
_logger.info("FaceMLDataDB: Starting migration");
|
||||
await _migrate(asyncDBConnection);
|
||||
_logger.info(
|
||||
"FaceMLDataDB Migration took ${stopwatch.elapsedMilliseconds} ms",
|
||||
);
|
||||
stopwatch.stop();
|
||||
|
||||
return asyncDBConnection;
|
||||
}
|
||||
|
||||
Future<void> _onCreate(SqliteDatabase asyncDBConnection) async {
|
||||
final migrations = SqliteMigrations()
|
||||
..add(
|
||||
SqliteMigration(_databaseVersion, (tx) async {
|
||||
await tx.execute(createFacesTable);
|
||||
await tx.execute(createFaceClustersTable);
|
||||
await tx.execute(createClusterPersonTable);
|
||||
await tx.execute(createClusterSummaryTable);
|
||||
await tx.execute(createNotPersonFeedbackTable);
|
||||
await tx.execute(fcClusterIDIndex);
|
||||
}),
|
||||
Future<void> _migrate(
|
||||
SqliteDatabase database,
|
||||
) async {
|
||||
final result = await database.execute('PRAGMA user_version');
|
||||
final currentVersion = result[0]['user_version'] as int;
|
||||
final toVersion = _migrationScripts.length;
|
||||
|
||||
if (currentVersion < toVersion) {
|
||||
_logger.info("Migrating database from $currentVersion to $toVersion");
|
||||
await database.writeTransaction((tx) async {
|
||||
for (int i = currentVersion + 1; i <= toVersion; i++) {
|
||||
try {
|
||||
await tx.execute(_migrationScripts[i - 1]);
|
||||
} catch (e) {
|
||||
_logger.severe("Error running migration script index ${i - 1}", e);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
await tx.execute('PRAGMA user_version = $toVersion');
|
||||
});
|
||||
} else if (currentVersion > toVersion) {
|
||||
throw AssertionError(
|
||||
"currentVersion($currentVersion) cannot be greater than toVersion($toVersion)",
|
||||
);
|
||||
await migrations.migrate(asyncDBConnection);
|
||||
}
|
||||
}
|
||||
|
||||
// bulkInsertFaces inserts the faces in the database in batches of 1000.
|
||||
|
@ -199,10 +229,10 @@ class FaceMLDataDB {
|
|||
final db = await instance.asyncDB;
|
||||
|
||||
await db.execute(deleteFacesTable);
|
||||
await db.execute(dropClusterPersonTable);
|
||||
await db.execute(dropClusterSummaryTable);
|
||||
await db.execute(deletePersonTable);
|
||||
await db.execute(dropNotPersonFeedbackTable);
|
||||
await db.execute(deleteFaceClustersTable);
|
||||
await db.execute(deleteClusterPersonTable);
|
||||
await db.execute(deleteClusterSummaryTable);
|
||||
await db.execute(deleteNotPersonFeedbackTable);
|
||||
}
|
||||
|
||||
Future<Iterable<Uint8List>> getFaceEmbeddingsForCluster(
|
||||
|
@ -255,7 +285,7 @@ class FaceMLDataDB {
|
|||
final List<int> fileId = [recentFileID];
|
||||
int? avatarFileId;
|
||||
if (avatarFaceId != null) {
|
||||
avatarFileId = int.tryParse(avatarFaceId.split('_')[0]);
|
||||
avatarFileId = tryGetFileIdFromFaceId(avatarFaceId);
|
||||
if (avatarFileId != null) {
|
||||
fileId.add(avatarFileId);
|
||||
}
|
||||
|
@ -407,8 +437,10 @@ class FaceMLDataDB {
|
|||
final personID = map[personIdColumn] as String;
|
||||
final clusterID = map[fcClusterID] as int;
|
||||
final faceID = map[fcFaceId] as String;
|
||||
result.putIfAbsent(personID, () => {}).putIfAbsent(clusterID, () => {})
|
||||
..add(faceID);
|
||||
result
|
||||
.putIfAbsent(personID, () => {})
|
||||
.putIfAbsent(clusterID, () => {})
|
||||
.add(faceID);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
@ -482,8 +514,7 @@ class FaceMLDataDB {
|
|||
for (final map in maps) {
|
||||
final clusterID = map[fcClusterID] as int;
|
||||
final faceID = map[fcFaceId] as String;
|
||||
final x = faceID.split('_').first;
|
||||
final fileID = int.parse(x);
|
||||
final fileID = getFileIdFromFaceId(faceID);
|
||||
result[fileID] = (result[fileID] ?? {})..add(clusterID);
|
||||
}
|
||||
return result;
|
||||
|
@ -671,19 +702,55 @@ class FaceMLDataDB {
|
|||
return maps.first['count'] as int;
|
||||
}
|
||||
|
||||
Future<int> getClusteredFaceCount() async {
|
||||
Future<int> getClusteredOrFacelessFileCount() async {
|
||||
final db = await instance.asyncDB;
|
||||
final List<Map<String, dynamic>> maps = await db.getAll(
|
||||
'SELECT COUNT(DISTINCT $fcFaceId) as count FROM $faceClustersTable',
|
||||
final List<Map<String, dynamic>> clustered = await db.getAll(
|
||||
'SELECT $fcFaceId FROM $faceClustersTable',
|
||||
);
|
||||
return maps.first['count'] as int;
|
||||
final Set<int> clusteredFileIDs = {};
|
||||
for (final map in clustered) {
|
||||
final int fileID = getFileIdFromFaceId(map[fcFaceId] as String);
|
||||
clusteredFileIDs.add(fileID);
|
||||
}
|
||||
|
||||
final List<Map<String, dynamic>> badFacesFiles = await db.getAll(
|
||||
'SELECT DISTINCT $fileIDColumn FROM $facesTable WHERE $faceScore <= $kMinimumQualityFaceScore OR $faceBlur <= $kLaplacianHardThreshold',
|
||||
);
|
||||
final Set<int> badFileIDs = {};
|
||||
for (final map in badFacesFiles) {
|
||||
badFileIDs.add(map[fileIDColumn] as int);
|
||||
}
|
||||
|
||||
final List<Map<String, dynamic>> goodFacesFiles = await db.getAll(
|
||||
'SELECT DISTINCT $fileIDColumn FROM $facesTable WHERE $faceScore > $kMinimumQualityFaceScore AND $faceBlur > $kLaplacianHardThreshold',
|
||||
);
|
||||
final Set<int> goodFileIDs = {};
|
||||
for (final map in goodFacesFiles) {
|
||||
goodFileIDs.add(map[fileIDColumn] as int);
|
||||
}
|
||||
final trulyFacelessFiles = badFileIDs.difference(goodFileIDs);
|
||||
return clusteredFileIDs.length + trulyFacelessFiles.length;
|
||||
}
|
||||
|
||||
Future<double> getClusteredToTotalFacesRatio() async {
|
||||
final int totalFaces = await getTotalFaceCount();
|
||||
final int clusteredFaces = await getClusteredFaceCount();
|
||||
Future<double> getClusteredToIndexableFilesRatio() async {
|
||||
final int indexableFiles = (await getIndexableFileIDs()).length;
|
||||
final int clusteredFiles = await getClusteredOrFacelessFileCount();
|
||||
|
||||
return clusteredFaces / totalFaces;
|
||||
return clusteredFiles / indexableFiles;
|
||||
}
|
||||
|
||||
Future<int> getUnclusteredFaceCount() async {
|
||||
final db = await instance.asyncDB;
|
||||
const String query = '''
|
||||
SELECT f.$faceIDColumn
|
||||
FROM $facesTable f
|
||||
LEFT JOIN $faceClustersTable fc ON f.$faceIDColumn = fc.$fcFaceId
|
||||
WHERE f.$faceScore > $kMinimumQualityFaceScore
|
||||
AND f.$faceBlur > $kLaplacianHardThreshold
|
||||
AND fc.$fcFaceId IS NULL
|
||||
''';
|
||||
final List<Map<String, dynamic>> maps = await db.getAll(query);
|
||||
return maps.length;
|
||||
}
|
||||
|
||||
Future<int> getBlurryFaceCount([
|
||||
|
@ -701,7 +768,7 @@ class FaceMLDataDB {
|
|||
try {
|
||||
final db = await instance.asyncDB;
|
||||
|
||||
await db.execute(dropFaceClustersTable);
|
||||
await db.execute(deleteFaceClustersTable);
|
||||
await db.execute(createFaceClustersTable);
|
||||
await db.execute(fcClusterIDIndex);
|
||||
} catch (e, s) {
|
||||
|
@ -801,7 +868,7 @@ class FaceMLDataDB {
|
|||
for (final map in maps) {
|
||||
final clusterID = map[clusterIDColumn] as int;
|
||||
final String faceID = map[fcFaceId] as String;
|
||||
final fileID = int.parse(faceID.split('_').first);
|
||||
final fileID = getFileIdFromFaceId(faceID);
|
||||
result[fileID] = (result[fileID] ?? {})..add(clusterID);
|
||||
}
|
||||
return result;
|
||||
|
@ -820,8 +887,8 @@ class FaceMLDataDB {
|
|||
final Map<int, Set<int>> result = {};
|
||||
for (final map in maps) {
|
||||
final clusterID = map[fcClusterID] as int;
|
||||
final faceId = map[fcFaceId] as String;
|
||||
final fileID = int.parse(faceId.split("_").first);
|
||||
final faceID = map[fcFaceId] as String;
|
||||
final fileID = getFileIdFromFaceId(faceID);
|
||||
result[fileID] = (result[fileID] ?? {})..add(clusterID);
|
||||
}
|
||||
return result;
|
||||
|
@ -912,17 +979,16 @@ class FaceMLDataDB {
|
|||
if (faces) {
|
||||
await db.execute(deleteFacesTable);
|
||||
await db.execute(createFacesTable);
|
||||
await db.execute(dropFaceClustersTable);
|
||||
await db.execute(deleteFaceClustersTable);
|
||||
await db.execute(createFaceClustersTable);
|
||||
await db.execute(fcClusterIDIndex);
|
||||
}
|
||||
|
||||
await db.execute(deletePersonTable);
|
||||
await db.execute(dropClusterPersonTable);
|
||||
await db.execute(dropNotPersonFeedbackTable);
|
||||
await db.execute(dropClusterSummaryTable);
|
||||
await db.execute(dropFaceClustersTable);
|
||||
|
||||
await db.execute(deleteClusterPersonTable);
|
||||
await db.execute(deleteNotPersonFeedbackTable);
|
||||
await db.execute(deleteClusterSummaryTable);
|
||||
await db.execute(deleteFaceClustersTable);
|
||||
|
||||
await db.execute(createClusterPersonTable);
|
||||
await db.execute(createNotPersonFeedbackTable);
|
||||
await db.execute(createClusterSummaryTable);
|
||||
|
@ -939,9 +1005,8 @@ class FaceMLDataDB {
|
|||
final db = await instance.asyncDB;
|
||||
|
||||
// Drop the tables
|
||||
await db.execute(deletePersonTable);
|
||||
await db.execute(dropClusterPersonTable);
|
||||
await db.execute(dropNotPersonFeedbackTable);
|
||||
await db.execute(deleteClusterPersonTable);
|
||||
await db.execute(deleteNotPersonFeedbackTable);
|
||||
|
||||
// Recreate the tables
|
||||
await db.execute(createClusterPersonTable);
|
||||
|
@ -970,7 +1035,7 @@ class FaceMLDataDB {
|
|||
final Map<String, int> faceIDToClusterID = {};
|
||||
for (final row in faceIdsResult) {
|
||||
final faceID = row[fcFaceId] as String;
|
||||
if (fileIds.contains(faceID.split('_').first)) {
|
||||
if (fileIds.contains(getFileIdFromFaceId(faceID))) {
|
||||
maxClusterID += 1;
|
||||
faceIDToClusterID[faceID] = maxClusterID;
|
||||
}
|
||||
|
@ -996,7 +1061,7 @@ class FaceMLDataDB {
|
|||
final Map<String, int> faceIDToClusterID = {};
|
||||
for (final row in faceIdsResult) {
|
||||
final faceID = row[fcFaceId] as String;
|
||||
if (fileIds.contains(faceID.split('_').first)) {
|
||||
if (fileIds.contains(getFileIdFromFaceId(faceID))) {
|
||||
maxClusterID += 1;
|
||||
faceIDToClusterID[faceID] = maxClusterID;
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ const createFacesTable = '''CREATE TABLE IF NOT EXISTS $facesTable (
|
|||
);
|
||||
''';
|
||||
|
||||
const deleteFacesTable = 'DROP TABLE IF EXISTS $facesTable';
|
||||
const deleteFacesTable = 'DELETE FROM $facesTable';
|
||||
// End of Faces Table Fields & Schema Queries
|
||||
|
||||
//##region Face Clusters Table Fields & Schema Queries
|
||||
|
@ -48,15 +48,9 @@ CREATE TABLE IF NOT EXISTS $faceClustersTable (
|
|||
// -- 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';
|
||||
const deleteFaceClustersTable = 'DELETE FROM $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';
|
||||
|
@ -69,7 +63,7 @@ CREATE TABLE IF NOT EXISTS $clusterPersonTable (
|
|||
PRIMARY KEY($personIdColumn, $clusterIDColumn)
|
||||
);
|
||||
''';
|
||||
const dropClusterPersonTable = 'DROP TABLE IF EXISTS $clusterPersonTable';
|
||||
const deleteClusterPersonTable = 'DELETE FROM $clusterPersonTable';
|
||||
// End Clusters Table Fields & Schema Queries
|
||||
|
||||
/// Cluster Summary Table Fields & Schema Queries
|
||||
|
@ -85,7 +79,7 @@ CREATE TABLE IF NOT EXISTS $clusterSummaryTable (
|
|||
);
|
||||
''';
|
||||
|
||||
const dropClusterSummaryTable = 'DROP TABLE IF EXISTS $clusterSummaryTable';
|
||||
const deleteClusterSummaryTable = 'DELETE FROM $clusterSummaryTable';
|
||||
|
||||
/// End Cluster Summary Table Fields & Schema Queries
|
||||
|
||||
|
@ -99,5 +93,5 @@ CREATE TABLE IF NOT EXISTS $notPersonFeedback (
|
|||
PRIMARY KEY($personIdColumn, $clusterIDColumn)
|
||||
);
|
||||
''';
|
||||
const dropNotPersonFeedbackTable = 'DROP TABLE IF EXISTS $notPersonFeedback';
|
||||
const deleteNotPersonFeedbackTable = 'DELETE FROM $notPersonFeedback';
|
||||
// End Clusters Table Fields & Schema Queries
|
||||
|
|
|
@ -1,42 +1,34 @@
|
|||
/// Bounding box of a face.
|
||||
///
|
||||
/// [xMin] and [yMin] are the coordinates of the top left corner of the box, and
|
||||
/// [ 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 xMin;
|
||||
final double yMin;
|
||||
final double x;
|
||||
final double y;
|
||||
final double width;
|
||||
final double height;
|
||||
|
||||
FaceBox({
|
||||
required this.xMin,
|
||||
required this.yMin,
|
||||
required this.x,
|
||||
required this.y,
|
||||
required this.width,
|
||||
required this.height,
|
||||
});
|
||||
|
||||
factory FaceBox.fromJson(Map<String, dynamic> json) {
|
||||
return FaceBox(
|
||||
xMin: (json['xMin'] is int
|
||||
? (json['xMin'] as int).toDouble()
|
||||
: json['xMin'] as double),
|
||||
yMin: (json['yMin'] is int
|
||||
? (json['yMin'] as int).toDouble()
|
||||
: json['yMin'] as double),
|
||||
width: (json['width'] is int
|
||||
? (json['width'] as int).toDouble()
|
||||
: json['width'] as double),
|
||||
height: (json['height'] is int
|
||||
? (json['height'] as int).toDouble()
|
||||
: json['height'] as double),
|
||||
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() => {
|
||||
'xMin': xMin,
|
||||
'yMin': yMin,
|
||||
'x': x,
|
||||
'y': y,
|
||||
'width': width,
|
||||
'height': height,
|
||||
};
|
||||
|
|
|
@ -6,7 +6,7 @@ import "package:photos/services/machine_learning/face_ml/face_detection/detectio
|
|||
|
||||
/// Stores the face detection data, notably the bounding box and landmarks.
|
||||
///
|
||||
/// - Bounding box: [FaceBox] with xMin, yMin (so top left corner), width, height
|
||||
/// - 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]!
|
||||
|
@ -24,8 +24,8 @@ class Detection {
|
|||
// empty box
|
||||
Detection.empty()
|
||||
: box = FaceBox(
|
||||
xMin: 0,
|
||||
yMin: 0,
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
),
|
||||
|
|
2
mobile/lib/generated/intl/messages_cs.dart
generated
2
mobile/lib/generated/intl/messages_cs.dart
generated
|
@ -54,6 +54,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"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":
|
||||
|
|
2
mobile/lib/generated/intl/messages_de.dart
generated
2
mobile/lib/generated/intl/messages_de.dart
generated
|
@ -819,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":
|
||||
|
|
2
mobile/lib/generated/intl/messages_en.dart
generated
2
mobile/lib/generated/intl/messages_en.dart
generated
|
@ -813,6 +813,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"incorrectRecoveryKeyTitle":
|
||||
MessageLookupByLibrary.simpleMessage("Incorrect recovery key"),
|
||||
"indexedItems": MessageLookupByLibrary.simpleMessage("Indexed items"),
|
||||
"indexingIsPaused": MessageLookupByLibrary.simpleMessage(
|
||||
"Indexing is paused. It will automatically resume when device is ready."),
|
||||
"insecureDevice":
|
||||
MessageLookupByLibrary.simpleMessage("Insecure device"),
|
||||
"installManually":
|
||||
|
|
2
mobile/lib/generated/intl/messages_es.dart
generated
2
mobile/lib/generated/intl/messages_es.dart
generated
|
@ -699,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":
|
||||
|
|
2
mobile/lib/generated/intl/messages_fr.dart
generated
2
mobile/lib/generated/intl/messages_fr.dart
generated
|
@ -804,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":
|
||||
|
|
2
mobile/lib/generated/intl/messages_it.dart
generated
2
mobile/lib/generated/intl/messages_it.dart
generated
|
@ -773,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":
|
||||
|
|
2
mobile/lib/generated/intl/messages_ko.dart
generated
2
mobile/lib/generated/intl/messages_ko.dart
generated
|
@ -54,6 +54,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"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":
|
||||
|
|
2
mobile/lib/generated/intl/messages_nl.dart
generated
2
mobile/lib/generated/intl/messages_nl.dart
generated
|
@ -840,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":
|
||||
|
|
2
mobile/lib/generated/intl/messages_no.dart
generated
2
mobile/lib/generated/intl/messages_no.dart
generated
|
@ -72,6 +72,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"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"),
|
||||
|
|
2
mobile/lib/generated/intl/messages_pl.dart
generated
2
mobile/lib/generated/intl/messages_pl.dart
generated
|
@ -131,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"),
|
||||
|
|
23
mobile/lib/generated/intl/messages_pt.dart
generated
23
mobile/lib/generated/intl/messages_pt.dart
generated
|
@ -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":
|
||||
|
@ -446,7 +447,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"clubByFileName": MessageLookupByLibrary.simpleMessage(
|
||||
"Agrupar pelo nome de arquivo"),
|
||||
"clusteringProgress":
|
||||
MessageLookupByLibrary.simpleMessage("Clustering progress"),
|
||||
MessageLookupByLibrary.simpleMessage("Progresso de agrupamento"),
|
||||
"codeAppliedPageTitle":
|
||||
MessageLookupByLibrary.simpleMessage("Código aplicado"),
|
||||
"codeCopiedToClipboard": MessageLookupByLibrary.simpleMessage(
|
||||
|
@ -692,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":
|
||||
|
@ -717,9 +720,9 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"exportYourData":
|
||||
MessageLookupByLibrary.simpleMessage("Exportar seus dados"),
|
||||
"faceRecognition":
|
||||
MessageLookupByLibrary.simpleMessage("Face recognition"),
|
||||
MessageLookupByLibrary.simpleMessage("Reconhecimento facial"),
|
||||
"faceRecognitionIndexingDescription": MessageLookupByLibrary.simpleMessage(
|
||||
"Please note that this will result in a higher bandwidth and battery usage until all items are indexed."),
|
||||
"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"),
|
||||
|
@ -761,12 +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("Found faces"),
|
||||
"foundFaces":
|
||||
MessageLookupByLibrary.simpleMessage("Rostos encontrados"),
|
||||
"freeStorageClaimed": MessageLookupByLibrary.simpleMessage(
|
||||
"Armazenamento gratuito reivindicado"),
|
||||
"freeStorageOnReferralSuccess": m24,
|
||||
|
@ -830,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":
|
||||
|
@ -1064,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(
|
||||
|
@ -1197,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(
|
||||
|
@ -1260,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(
|
||||
|
|
2
mobile/lib/generated/intl/messages_zh.dart
generated
2
mobile/lib/generated/intl/messages_zh.dart
generated
|
@ -686,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":
|
||||
|
|
10
mobile/lib/generated/l10n.dart
generated
10
mobile/lib/generated/l10n.dart
generated
|
@ -8793,6 +8793,16 @@ class S {
|
|||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Indexing is paused. It will automatically resume when device is ready.`
|
||||
String get indexingIsPaused {
|
||||
return Intl.message(
|
||||
'Indexing is paused. It will automatically resume when device is ready.',
|
||||
name: 'indexingIsPaused',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AppLocalizationDelegate extends LocalizationsDelegate<S> {
|
||||
|
|
|
@ -24,5 +24,6 @@
|
|||
"faceRecognition": "Face recognition",
|
||||
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
|
||||
"foundFaces": "Found faces",
|
||||
"clusteringProgress": "Clustering progress"
|
||||
"clusteringProgress": "Clustering progress",
|
||||
"indexingIsPaused": "Indexing is paused, will automatically resume when device is ready"
|
||||
}
|
|
@ -1212,5 +1212,6 @@
|
|||
"faceRecognition": "Face recognition",
|
||||
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
|
||||
"foundFaces": "Found faces",
|
||||
"clusteringProgress": "Clustering progress"
|
||||
"clusteringProgress": "Clustering progress",
|
||||
"indexingIsPaused": "Indexing is paused, will automatically resume when device is ready"
|
||||
}
|
|
@ -1235,5 +1235,6 @@
|
|||
"faceRecognition": "Face recognition",
|
||||
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
|
||||
"foundFaces": "Found faces",
|
||||
"clusteringProgress": "Clustering progress"
|
||||
}
|
||||
"clusteringProgress": "Clustering progress",
|
||||
"indexingIsPaused": "Indexing is paused. It will automatically resume when device is ready."
|
||||
}
|
||||
|
|
|
@ -986,5 +986,6 @@
|
|||
"faceRecognition": "Face recognition",
|
||||
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
|
||||
"foundFaces": "Found faces",
|
||||
"clusteringProgress": "Clustering progress"
|
||||
"clusteringProgress": "Clustering progress",
|
||||
"indexingIsPaused": "Indexing is paused, will automatically resume when device is ready"
|
||||
}
|
|
@ -1167,5 +1167,6 @@
|
|||
"faceRecognition": "Face recognition",
|
||||
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
|
||||
"foundFaces": "Found faces",
|
||||
"clusteringProgress": "Clustering progress"
|
||||
"clusteringProgress": "Clustering progress",
|
||||
"indexingIsPaused": "Indexing is paused, will automatically resume when device is ready"
|
||||
}
|
|
@ -1129,5 +1129,6 @@
|
|||
"faceRecognition": "Face recognition",
|
||||
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
|
||||
"foundFaces": "Found faces",
|
||||
"clusteringProgress": "Clustering progress"
|
||||
"clusteringProgress": "Clustering progress",
|
||||
"indexingIsPaused": "Indexing is paused, will automatically resume when device is ready"
|
||||
}
|
|
@ -24,5 +24,6 @@
|
|||
"faceRecognition": "Face recognition",
|
||||
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
|
||||
"foundFaces": "Found faces",
|
||||
"clusteringProgress": "Clustering progress"
|
||||
"clusteringProgress": "Clustering progress",
|
||||
"indexingIsPaused": "Indexing is paused, will automatically resume when device is ready"
|
||||
}
|
|
@ -1230,5 +1230,6 @@
|
|||
"faceRecognition": "Face recognition",
|
||||
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
|
||||
"foundFaces": "Found faces",
|
||||
"clusteringProgress": "Clustering progress"
|
||||
"clusteringProgress": "Clustering progress",
|
||||
"indexingIsPaused": "Indexing is paused, will automatically resume when device is ready"
|
||||
}
|
|
@ -38,5 +38,6 @@
|
|||
"faceRecognition": "Face recognition",
|
||||
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
|
||||
"foundFaces": "Found faces",
|
||||
"clusteringProgress": "Clustering progress"
|
||||
"clusteringProgress": "Clustering progress",
|
||||
"indexingIsPaused": "Indexing is paused, will automatically resume when device is ready"
|
||||
}
|
|
@ -125,5 +125,6 @@
|
|||
"faceRecognition": "Face recognition",
|
||||
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
|
||||
"foundFaces": "Found faces",
|
||||
"clusteringProgress": "Clustering progress"
|
||||
"clusteringProgress": "Clustering progress",
|
||||
"indexingIsPaused": "Indexing is paused, will automatically resume when device is ready"
|
||||
}
|
|
@ -987,7 +987,7 @@
|
|||
"fileTypesAndNames": "Tipos de arquivo e nomes",
|
||||
"location": "Local",
|
||||
"moments": "Momentos",
|
||||
"searchFaceEmptySection": "Encontre todas as fotos de uma pessoa",
|
||||
"searchFaceEmptySection": "Pessoas serão exibidas aqui uma vez que a indexação é feita",
|
||||
"searchDatesEmptySection": "Pesquisar por data, mês ou ano",
|
||||
"searchLocationEmptySection": "Fotos de grupo que estão sendo tiradas em algum raio da foto",
|
||||
"searchPeopleEmptySection": "Convide pessoas e você verá todas as fotos compartilhadas por elas aqui",
|
||||
|
@ -1042,7 +1042,7 @@
|
|||
"@storageUsageInfo": {
|
||||
"description": "Example: 1.2 GB of 2 GB used or 100 GB or 2TB used"
|
||||
},
|
||||
"freeStorageSpace": "{freeAmount} {storageUnit} grátis",
|
||||
"freeStorageSpace": "{freeAmount} {storageUnit} livre",
|
||||
"appVersion": "Versão: {versionValue}",
|
||||
"verifyIDLabel": "Verificar",
|
||||
"fileInfoAddDescHint": "Adicionar descrição...",
|
||||
|
@ -1171,6 +1171,7 @@
|
|||
}
|
||||
},
|
||||
"faces": "Rostos",
|
||||
"people": "Pessoas",
|
||||
"contents": "Conteúdos",
|
||||
"addNew": "Adicionar novo",
|
||||
"@addNew": {
|
||||
|
@ -1196,14 +1197,14 @@
|
|||
"verifyPasskey": "Verificar chave de acesso",
|
||||
"playOnTv": "Reproduzir álbum na TV",
|
||||
"pair": "Parear",
|
||||
"autoPair": "Pareamento automático",
|
||||
"pairWithPin": "Parear com PIN",
|
||||
"deviceNotFound": "Dispositivo não encontrado",
|
||||
"castInstruction": "Visite cast.ente.io no dispositivo que você deseja parear.\n\ndigite o código abaixo para reproduzir o álbum em sua TV.",
|
||||
"deviceCodeHint": "Insira o código",
|
||||
"joinDiscord": "Junte-se ao Discord",
|
||||
"locations": "Locais",
|
||||
"descriptions": "Descrições",
|
||||
"addAName": "Adicione um nome",
|
||||
"findPeopleByName": "Encontre pessoas rapidamente por nome",
|
||||
"addViewers": "{count, plural, zero {Adicionar visualizador} one {Adicionar visualizador} other {Adicionar Visualizadores}}",
|
||||
"addCollaborators": "{count, plural, zero {Adicionar colaborador} one {Adicionar coloborador} other {Adicionar colaboradores}}",
|
||||
"longPressAnEmailToVerifyEndToEndEncryption": "Pressione e segure um e-mail para verificar a criptografia de ponta a ponta.",
|
||||
|
@ -1216,6 +1217,8 @@
|
|||
"customEndpoint": "Conectado a {endpoint}",
|
||||
"createCollaborativeLink": "Criar link colaborativo",
|
||||
"search": "Pesquisar",
|
||||
"enterPersonName": "Inserir nome da pessoa",
|
||||
"removePersonLabel": "Remover etiqueta da pessoa",
|
||||
"autoPairDesc": "O pareamento automático funciona apenas com dispositivos que suportam o Chromecast.",
|
||||
"manualPairDesc": "Parear com o PIN funciona com qualquer tela que você deseja ver o seu álbum ativado.",
|
||||
"connectToDevice": "Conectar ao dispositivo",
|
||||
|
@ -1227,8 +1230,11 @@
|
|||
"castIPMismatchTitle": "Falha ao transmitir álbum",
|
||||
"castIPMismatchBody": "Certifique-se de estar na mesma rede que a TV.",
|
||||
"pairingComplete": "Pareamento concluído",
|
||||
"faceRecognition": "Face recognition",
|
||||
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
|
||||
"foundFaces": "Found faces",
|
||||
"clusteringProgress": "Clustering progress"
|
||||
"autoPair": "Pareamento automático",
|
||||
"pairWithPin": "Parear com PIN",
|
||||
"faceRecognition": "Reconhecimento facial",
|
||||
"faceRecognitionIndexingDescription": "Por favor, note que isso resultará em uma largura de banda maior e uso de bateria até que todos os itens sejam indexados.",
|
||||
"foundFaces": "Rostos encontrados",
|
||||
"clusteringProgress": "Progresso de agrupamento",
|
||||
"indexingIsPaused": "Indexing is paused, will automatically resume when device is ready"
|
||||
}
|
|
@ -1230,5 +1230,6 @@
|
|||
"faceRecognition": "Face recognition",
|
||||
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
|
||||
"foundFaces": "Found faces",
|
||||
"clusteringProgress": "Clustering progress"
|
||||
"clusteringProgress": "Clustering progress",
|
||||
"indexingIsPaused": "Indexing is paused, will automatically resume when device is ready"
|
||||
}
|
|
@ -51,6 +51,7 @@ import 'package:photos/services/user_service.dart';
|
|||
import 'package:photos/ui/tools/app_lock.dart';
|
||||
import 'package:photos/ui/tools/lock_screen.dart';
|
||||
import 'package:photos/utils/crypto_util.dart';
|
||||
import "package:photos/utils/email_util.dart";
|
||||
import 'package:photos/utils/file_uploader.dart';
|
||||
import 'package:photos/utils/local_settings.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
@ -180,6 +181,16 @@ void _headlessTaskHandler(HeadlessTask task) {
|
|||
}
|
||||
|
||||
Future<void> _init(bool isBackground, {String via = ''}) async {
|
||||
bool initComplete = false;
|
||||
Future.delayed(const Duration(seconds: 15), () {
|
||||
if (!initComplete && !isBackground) {
|
||||
sendLogsForInit(
|
||||
"support@ente.io",
|
||||
"Stuck on splash screen for >= 15 seconds",
|
||||
null,
|
||||
);
|
||||
}
|
||||
});
|
||||
_isProcessRunning = true;
|
||||
_logger.info("Initializing... inBG =$isBackground via: $via");
|
||||
final SharedPreferences preferences = await SharedPreferences.getInstance();
|
||||
|
@ -235,19 +246,11 @@ Future<void> _init(bool isBackground, {String via = ''}) async {
|
|||
|
||||
unawaited(SemanticSearchService.instance.init());
|
||||
MachineLearningController.instance.init();
|
||||
// Can not including existing tf/ml binaries as they are not being built
|
||||
// from source.
|
||||
// See https://gitlab.com/fdroid/fdroiddata/-/merge_requests/12671#note_1294346819
|
||||
if (!UpdateService.instance.isFdroidFlavor()) {
|
||||
// unawaited(ObjectDetectionService.instance.init());
|
||||
if (flagService.faceSearchEnabled) {
|
||||
unawaited(FaceMlService.instance.init());
|
||||
FaceMlService.instance.listenIndexOnDiffSync();
|
||||
FaceMlService.instance.listenOnPeopleChangedSync();
|
||||
} else {
|
||||
if (LocalSettings.instance.isFaceIndexingEnabled) {
|
||||
unawaited(LocalSettings.instance.toggleFaceIndexing());
|
||||
}
|
||||
if (flagService.faceSearchEnabled) {
|
||||
unawaited(FaceMlService.instance.init());
|
||||
} else {
|
||||
if (LocalSettings.instance.isFaceIndexingEnabled) {
|
||||
unawaited(LocalSettings.instance.toggleFaceIndexing());
|
||||
}
|
||||
}
|
||||
PersonService.init(
|
||||
|
@ -256,6 +259,7 @@ Future<void> _init(bool isBackground, {String via = ''}) async {
|
|||
preferences,
|
||||
);
|
||||
|
||||
initComplete = true;
|
||||
_logger.info("Initialization done");
|
||||
}
|
||||
|
||||
|
|
|
@ -498,19 +498,8 @@ class FaceClusteringService {
|
|||
}
|
||||
}
|
||||
|
||||
// Sort the faceInfos based on fileCreationTime, in ascending order, so oldest faces are first
|
||||
if (fileIDToCreationTime != null) {
|
||||
faceInfos.sort((a, b) {
|
||||
if (a.fileCreationTime == null && b.fileCreationTime == null) {
|
||||
return 0;
|
||||
} else if (a.fileCreationTime == null) {
|
||||
return 1;
|
||||
} else if (b.fileCreationTime == null) {
|
||||
return -1;
|
||||
} else {
|
||||
return a.fileCreationTime!.compareTo(b.fileCreationTime!);
|
||||
}
|
||||
});
|
||||
_sortFaceInfosOnCreationTime(faceInfos);
|
||||
}
|
||||
|
||||
// Sort the faceInfos such that the ones with null clusterId are at the end
|
||||
|
@ -796,19 +785,8 @@ class FaceClusteringService {
|
|||
);
|
||||
}
|
||||
|
||||
// Sort the faceInfos based on fileCreationTime, in ascending order, so oldest faces are first
|
||||
if (fileIDToCreationTime != null) {
|
||||
faceInfos.sort((a, b) {
|
||||
if (a.fileCreationTime == null && b.fileCreationTime == null) {
|
||||
return 0;
|
||||
} else if (a.fileCreationTime == null) {
|
||||
return 1;
|
||||
} else if (b.fileCreationTime == null) {
|
||||
return -1;
|
||||
} else {
|
||||
return a.fileCreationTime!.compareTo(b.fileCreationTime!);
|
||||
}
|
||||
});
|
||||
_sortFaceInfosOnCreationTime(faceInfos);
|
||||
}
|
||||
|
||||
if (faceInfos.isEmpty) {
|
||||
|
@ -996,19 +974,8 @@ class FaceClusteringService {
|
|||
);
|
||||
}
|
||||
|
||||
// Sort the faceInfos based on fileCreationTime, in ascending order, so oldest faces are first
|
||||
if (fileIDToCreationTime != null) {
|
||||
faceInfos.sort((a, b) {
|
||||
if (a.fileCreationTime == null && b.fileCreationTime == null) {
|
||||
return 0;
|
||||
} else if (a.fileCreationTime == null) {
|
||||
return 1;
|
||||
} else if (b.fileCreationTime == null) {
|
||||
return -1;
|
||||
} else {
|
||||
return a.fileCreationTime!.compareTo(b.fileCreationTime!);
|
||||
}
|
||||
});
|
||||
_sortFaceInfosOnCreationTime(faceInfos);
|
||||
}
|
||||
|
||||
// Get the embeddings
|
||||
|
@ -1027,3 +994,20 @@ class FaceClusteringService {
|
|||
return clusteredFaceIDs;
|
||||
}
|
||||
}
|
||||
|
||||
/// Sort the faceInfos based on fileCreationTime, in descending order, so newest faces are first
|
||||
void _sortFaceInfosOnCreationTime(
|
||||
List<FaceInfo> faceInfos,
|
||||
) {
|
||||
faceInfos.sort((b, a) {
|
||||
if (a.fileCreationTime == null && b.fileCreationTime == null) {
|
||||
return 0;
|
||||
} else if (a.fileCreationTime == null) {
|
||||
return 1;
|
||||
} else if (b.fileCreationTime == null) {
|
||||
return -1;
|
||||
} else {
|
||||
return a.fileCreationTime!.compareTo(b.fileCreationTime!);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -8,6 +8,18 @@ class GeneralFaceMlException implements Exception {
|
|||
String toString() => 'GeneralFaceMlException: $message';
|
||||
}
|
||||
|
||||
class ThumbnailRetrievalException implements Exception {
|
||||
final String message;
|
||||
final StackTrace stackTrace;
|
||||
|
||||
ThumbnailRetrievalException(this.message, this.stackTrace);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ThumbnailRetrievalException: $message\n$stackTrace';
|
||||
}
|
||||
}
|
||||
|
||||
class CouldNotRetrieveAnyFileData implements Exception {}
|
||||
|
||||
class CouldNotInitializeFaceDetector implements Exception {}
|
||||
|
|
|
@ -310,5 +310,9 @@ class FaceResultBuilder {
|
|||
}
|
||||
|
||||
int getFileIdFromFaceId(String faceId) {
|
||||
return int.parse(faceId.split("_")[0]);
|
||||
return int.parse(faceId.split("_").first);
|
||||
}
|
||||
|
||||
int? tryGetFileIdFromFaceId(String faceId) {
|
||||
return int.tryParse(faceId.split("_").first);
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,7 @@
|
|||
import "dart:async";
|
||||
import "dart:convert";
|
||||
|
||||
import "package:computer/computer.dart";
|
||||
import "package:logging/logging.dart";
|
||||
import "package:photos/core/network/network.dart";
|
||||
import "package:photos/db/files_db.dart";
|
||||
|
@ -16,6 +17,8 @@ import "package:shared_preferences/shared_preferences.dart";
|
|||
class RemoteFileMLService {
|
||||
RemoteFileMLService._privateConstructor();
|
||||
|
||||
static final Computer _computer = Computer.shared();
|
||||
|
||||
static final RemoteFileMLService instance =
|
||||
RemoteFileMLService._privateConstructor();
|
||||
|
||||
|
@ -52,13 +55,13 @@ class RemoteFileMLService {
|
|||
}
|
||||
|
||||
Future<FilesMLDataResponse> getFilessEmbedding(
|
||||
List<int> fileIds,
|
||||
Set<int> fileIds,
|
||||
) async {
|
||||
try {
|
||||
final res = await _dio.post(
|
||||
"/embeddings/files",
|
||||
data: {
|
||||
"fileIDs": fileIds,
|
||||
"fileIDs": fileIds.toList(),
|
||||
"model": 'file-ml-clip-face',
|
||||
},
|
||||
);
|
||||
|
@ -107,15 +110,17 @@ class RemoteFileMLService {
|
|||
final input = EmbeddingsDecoderInput(embedding, fileKey);
|
||||
inputs.add(input);
|
||||
}
|
||||
// todo: use compute or isolate
|
||||
return decryptFileMLComputer(
|
||||
{
|
||||
return _computer.compute<Map<String, dynamic>, Map<int, FileMl>>(
|
||||
_decryptFileMLComputer,
|
||||
param: {
|
||||
"inputs": inputs,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<Map<int, FileMl>> decryptFileMLComputer(
|
||||
}
|
||||
|
||||
Future<Map<int, FileMl>> _decryptFileMLComputer(
|
||||
Map<String, dynamic> args,
|
||||
) async {
|
||||
final result = <int, FileMl>{};
|
||||
|
@ -134,5 +139,4 @@ class RemoteFileMLService {
|
|||
result[input.embedding.fileID] = decodedEmbedding;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@ import "dart:io";
|
|||
|
||||
import "package:battery_info/battery_info_plugin.dart";
|
||||
import "package:battery_info/model/android_battery_info.dart";
|
||||
import "package:battery_info/model/iso_battery_info.dart";
|
||||
import "package:logging/logging.dart";
|
||||
import "package:photos/core/event_bus.dart";
|
||||
import "package:photos/events/machine_learning_control_event.dart";
|
||||
|
@ -17,7 +18,7 @@ class MachineLearningController {
|
|||
|
||||
static const kMaximumTemperature = 42; // 42 degree celsius
|
||||
static const kMinimumBatteryLevel = 20; // 20%
|
||||
static const kDefaultInteractionTimeout = Duration(seconds: 15);
|
||||
static const kDefaultInteractionTimeout = Duration(seconds: 10);
|
||||
static const kUnhealthyStates = ["over_heat", "over_voltage", "dead"];
|
||||
|
||||
bool _isDeviceHealthy = true;
|
||||
|
@ -25,19 +26,27 @@ class MachineLearningController {
|
|||
bool _canRunML = false;
|
||||
late Timer _userInteractionTimer;
|
||||
|
||||
bool get isDeviceHealthy => _isDeviceHealthy;
|
||||
|
||||
void init() {
|
||||
_logger.info('init called');
|
||||
if (Platform.isAndroid) {
|
||||
_startInteractionTimer();
|
||||
BatteryInfoPlugin()
|
||||
.androidBatteryInfoStream
|
||||
.listen((AndroidBatteryInfo? batteryInfo) {
|
||||
_onBatteryStateUpdate(batteryInfo);
|
||||
_onAndroidBatteryStateUpdate(batteryInfo);
|
||||
});
|
||||
} else {
|
||||
// Always run Machine Learning on iOS
|
||||
_canRunML = true;
|
||||
Bus.instance.fire(MachineLearningControlEvent(true));
|
||||
}
|
||||
if (Platform.isIOS) {
|
||||
BatteryInfoPlugin()
|
||||
.iosBatteryInfoStream
|
||||
.listen((IosBatteryInfo? batteryInfo) {
|
||||
_oniOSBatteryStateUpdate(batteryInfo);
|
||||
});
|
||||
}
|
||||
_fireControlEvent();
|
||||
_logger.info('init done');
|
||||
}
|
||||
|
||||
void onUserInteraction() {
|
||||
|
@ -53,7 +62,8 @@ class MachineLearningController {
|
|||
}
|
||||
|
||||
void _fireControlEvent() {
|
||||
final shouldRunML = _isDeviceHealthy && !_isUserInteracting;
|
||||
final shouldRunML =
|
||||
_isDeviceHealthy && (Platform.isAndroid ? !_isUserInteracting : true);
|
||||
if (shouldRunML != _canRunML) {
|
||||
_canRunML = shouldRunML;
|
||||
_logger.info(
|
||||
|
@ -76,18 +86,28 @@ class MachineLearningController {
|
|||
_startInteractionTimer();
|
||||
}
|
||||
|
||||
void _onBatteryStateUpdate(AndroidBatteryInfo? batteryInfo) {
|
||||
void _onAndroidBatteryStateUpdate(AndroidBatteryInfo? batteryInfo) {
|
||||
_logger.info("Battery info: ${batteryInfo!.toJson()}");
|
||||
_isDeviceHealthy = _computeIsDeviceHealthy(batteryInfo);
|
||||
_isDeviceHealthy = _computeIsAndroidDeviceHealthy(batteryInfo);
|
||||
_fireControlEvent();
|
||||
}
|
||||
|
||||
bool _computeIsDeviceHealthy(AndroidBatteryInfo info) {
|
||||
void _oniOSBatteryStateUpdate(IosBatteryInfo? batteryInfo) {
|
||||
_logger.info("Battery info: ${batteryInfo!.toJson()}");
|
||||
_isDeviceHealthy = _computeIsiOSDeviceHealthy(batteryInfo);
|
||||
_fireControlEvent();
|
||||
}
|
||||
|
||||
bool _computeIsAndroidDeviceHealthy(AndroidBatteryInfo info) {
|
||||
return _hasSufficientBattery(info.batteryLevel ?? kMinimumBatteryLevel) &&
|
||||
_isAcceptableTemperature(info.temperature ?? kMaximumTemperature) &&
|
||||
_isBatteryHealthy(info.health ?? "");
|
||||
}
|
||||
|
||||
bool _computeIsiOSDeviceHealthy(IosBatteryInfo info) {
|
||||
return _hasSufficientBattery(info.batteryLevel ?? kMinimumBatteryLevel);
|
||||
}
|
||||
|
||||
bool _hasSufficientBattery(int batteryLevel) {
|
||||
return batteryLevel >= kMinimumBatteryLevel;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import "dart:async";
|
||||
import "dart:collection";
|
||||
import "dart:io";
|
||||
import "dart:math" show min;
|
||||
|
||||
import "package:computer/computer.dart";
|
||||
|
@ -24,6 +23,7 @@ import 'package:photos/services/machine_learning/semantic_search/frameworks/onnx
|
|||
import "package:photos/utils/debouncer.dart";
|
||||
import "package:photos/utils/device_info.dart";
|
||||
import "package:photos/utils/local_settings.dart";
|
||||
import "package:photos/utils/ml_util.dart";
|
||||
import "package:photos/utils/thumbnail_util.dart";
|
||||
|
||||
class SemanticSearchService {
|
||||
|
@ -103,17 +103,13 @@ class SemanticSearchService {
|
|||
if (shouldSyncImmediately) {
|
||||
unawaited(sync());
|
||||
}
|
||||
if (Platform.isAndroid) {
|
||||
Bus.instance.on<MachineLearningControlEvent>().listen((event) {
|
||||
if (event.shouldRun) {
|
||||
_startIndexing();
|
||||
} else {
|
||||
_pauseIndexing();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
_startIndexing();
|
||||
}
|
||||
Bus.instance.on<MachineLearningControlEvent>().listen((event) {
|
||||
if (event.shouldRun) {
|
||||
_startIndexing();
|
||||
} else {
|
||||
_pauseIndexing();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> release() async {
|
||||
|
@ -165,8 +161,7 @@ class SemanticSearchService {
|
|||
}
|
||||
|
||||
Future<IndexStatus> getIndexStatus() async {
|
||||
final indexableFileIDs = await FilesDB.instance
|
||||
.getOwnedFileIDs(Configuration.instance.getUserID()!);
|
||||
final indexableFileIDs = await getIndexableFileIDs();
|
||||
return IndexStatus(
|
||||
min(_cachedEmbeddings.length, indexableFileIDs.length),
|
||||
(await _getFileIDsToBeIndexed()).length,
|
||||
|
@ -227,8 +222,7 @@ class SemanticSearchService {
|
|||
}
|
||||
|
||||
Future<List<int>> _getFileIDsToBeIndexed() async {
|
||||
final uploadedFileIDs = await FilesDB.instance
|
||||
.getOwnedFileIDs(Configuration.instance.getUserID()!);
|
||||
final uploadedFileIDs = await getIndexableFileIDs();
|
||||
final embeddedFileIDs =
|
||||
await EmbeddingsDB.instance.getFileIDs(_currentModel);
|
||||
|
||||
|
|
|
@ -754,15 +754,6 @@ class SearchService {
|
|||
|
||||
Future<List<GenericSearchResult>> getAllFace(int? limit) async {
|
||||
try {
|
||||
// Don't return anything if clustering is not nearly complete yet
|
||||
final foundFaces = await FaceMLDataDB.instance.getTotalFaceCount();
|
||||
final clusteredFaces =
|
||||
await FaceMLDataDB.instance.getClusteredFaceCount();
|
||||
final clusteringDoneRatio = clusteredFaces / foundFaces;
|
||||
if (clusteringDoneRatio < 0.9) {
|
||||
return [];
|
||||
}
|
||||
|
||||
debugPrint("getting faces");
|
||||
final Map<int, Set<int>> fileIdToClusterID =
|
||||
await FaceMLDataDB.instance.getFileIdToClusterIds();
|
||||
|
@ -848,8 +839,9 @@ class SearchService {
|
|||
final String clusterName = "$clusterId";
|
||||
|
||||
if (clusterIDToPersonID[clusterId] != null) {
|
||||
throw Exception(
|
||||
"Cluster $clusterId should not have person id ${clusterIDToPersonID[clusterId]}",
|
||||
// This should not happen, means a faceID is assigned to multiple persons.
|
||||
_logger.severe(
|
||||
"`getAllFace`: Cluster $clusterId should not have person id ${clusterIDToPersonID[clusterId]}",
|
||||
);
|
||||
}
|
||||
if (files.length < kMinimumClusterSizeSearchResult &&
|
||||
|
|
|
@ -79,7 +79,7 @@ class _FaceDebugSectionWidgetState extends State<FaceDebugSectionWidget> {
|
|||
final isEnabled =
|
||||
await LocalSettings.instance.toggleFaceIndexing();
|
||||
if (!isEnabled) {
|
||||
FaceMlService.instance.pauseIndexing();
|
||||
FaceMlService.instance.pauseIndexingAndClustering();
|
||||
}
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
|
@ -107,7 +107,7 @@ class _FaceDebugSectionWidgetState extends State<FaceDebugSectionWidget> {
|
|||
setState(() {});
|
||||
}
|
||||
} catch (e, s) {
|
||||
_logger.warning('indexing failed ', e, s);
|
||||
_logger.warning('Remote fetch toggle failed ', e, s);
|
||||
await showGenericErrorDialog(context: context, error: e);
|
||||
}
|
||||
},
|
||||
|
@ -115,22 +115,25 @@ class _FaceDebugSectionWidgetState extends State<FaceDebugSectionWidget> {
|
|||
sectionOptionSpacing,
|
||||
MenuItemWidget(
|
||||
captionedTextWidget: CaptionedTextWidget(
|
||||
title: FaceMlService.instance.canRunMLController
|
||||
? "canRunML enabled"
|
||||
: "canRunML disabled",
|
||||
title: FaceMlService.instance.debugIndexingDisabled
|
||||
? "Debug enable indexing again"
|
||||
: "Debug disable indexing",
|
||||
),
|
||||
pressedColor: getEnteColorScheme(context).fillFaint,
|
||||
trailingIcon: Icons.chevron_right_outlined,
|
||||
trailingIconIsMuted: true,
|
||||
onTap: () async {
|
||||
try {
|
||||
FaceMlService.instance.canRunMLController =
|
||||
!FaceMlService.instance.canRunMLController;
|
||||
FaceMlService.instance.debugIndexingDisabled =
|
||||
!FaceMlService.instance.debugIndexingDisabled;
|
||||
if (FaceMlService.instance.debugIndexingDisabled) {
|
||||
FaceMlService.instance.pauseIndexingAndClustering();
|
||||
}
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
} catch (e, s) {
|
||||
_logger.warning('canRunML toggle failed ', e, s);
|
||||
_logger.warning('debugIndexingDisabled toggle failed ', e, s);
|
||||
await showGenericErrorDialog(context: context, error: e);
|
||||
}
|
||||
},
|
||||
|
@ -145,6 +148,7 @@ class _FaceDebugSectionWidgetState extends State<FaceDebugSectionWidget> {
|
|||
trailingIconIsMuted: true,
|
||||
onTap: () async {
|
||||
try {
|
||||
FaceMlService.instance.debugIndexingDisabled = false;
|
||||
unawaited(FaceMlService.instance.indexAndClusterAll());
|
||||
} catch (e, s) {
|
||||
_logger.warning('indexAndClusterAll failed ', e, s);
|
||||
|
@ -162,6 +166,7 @@ class _FaceDebugSectionWidgetState extends State<FaceDebugSectionWidget> {
|
|||
trailingIconIsMuted: true,
|
||||
onTap: () async {
|
||||
try {
|
||||
FaceMlService.instance.debugIndexingDisabled = false;
|
||||
unawaited(FaceMlService.instance.indexAllImages());
|
||||
} catch (e, s) {
|
||||
_logger.warning('indexing failed ', e, s);
|
||||
|
@ -172,7 +177,7 @@ class _FaceDebugSectionWidgetState extends State<FaceDebugSectionWidget> {
|
|||
sectionOptionSpacing,
|
||||
MenuItemWidget(
|
||||
captionedTextWidget: FutureBuilder<double>(
|
||||
future: FaceMLDataDB.instance.getClusteredToTotalFacesRatio(),
|
||||
future: FaceMLDataDB.instance.getClusteredToIndexableFilesRatio(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return CaptionedTextWidget(
|
||||
|
@ -189,6 +194,7 @@ class _FaceDebugSectionWidgetState extends State<FaceDebugSectionWidget> {
|
|||
onTap: () async {
|
||||
try {
|
||||
await PersonService.instance.storeRemoteFeedback();
|
||||
FaceMlService.instance.debugIndexingDisabled = false;
|
||||
await FaceMlService.instance
|
||||
.clusterAllImages(clusterInBuckets: true);
|
||||
Bus.instance.fire(PeopleChangedEvent());
|
||||
|
|
|
@ -3,6 +3,7 @@ import "dart:math" show max, min;
|
|||
|
||||
import "package:flutter/material.dart";
|
||||
import "package:intl/intl.dart";
|
||||
import "package:logging/logging.dart";
|
||||
import "package:photos/core/event_bus.dart";
|
||||
import 'package:photos/events/embedding_updated_event.dart';
|
||||
import "package:photos/face/db.dart";
|
||||
|
@ -10,6 +11,7 @@ import "package:photos/generated/l10n.dart";
|
|||
import "package:photos/models/ml/ml_versions.dart";
|
||||
import "package:photos/service_locator.dart";
|
||||
import "package:photos/services/machine_learning/face_ml/face_ml_service.dart";
|
||||
import "package:photos/services/machine_learning/machine_learning_controller.dart";
|
||||
import 'package:photos/services/machine_learning/semantic_search/frameworks/ml_framework.dart';
|
||||
import 'package:photos/services/machine_learning/semantic_search/semantic_search_service.dart';
|
||||
import "package:photos/services/remote_assets_service.dart";
|
||||
|
@ -25,6 +27,10 @@ import "package:photos/ui/components/title_bar_widget.dart";
|
|||
import "package:photos/ui/components/toggle_switch_widget.dart";
|
||||
import "package:photos/utils/data_util.dart";
|
||||
import "package:photos/utils/local_settings.dart";
|
||||
import "package:photos/utils/ml_util.dart";
|
||||
import "package:photos/utils/wakelock_util.dart";
|
||||
|
||||
final _logger = Logger("MachineLearningSettingsPage");
|
||||
|
||||
class MachineLearningSettingsPage extends StatefulWidget {
|
||||
const MachineLearningSettingsPage({super.key});
|
||||
|
@ -37,6 +43,7 @@ class MachineLearningSettingsPage extends StatefulWidget {
|
|||
class _MachineLearningSettingsPageState
|
||||
extends State<MachineLearningSettingsPage> {
|
||||
late InitializationState _state;
|
||||
final EnteWakeLock _wakeLock = EnteWakeLock();
|
||||
|
||||
late StreamSubscription<MLFrameworkInitializationUpdateEvent>
|
||||
_eventSubscription;
|
||||
|
@ -50,6 +57,7 @@ class _MachineLearningSettingsPageState
|
|||
setState(() {});
|
||||
});
|
||||
_fetchState();
|
||||
_wakeLock.enable();
|
||||
}
|
||||
|
||||
void _fetchState() {
|
||||
|
@ -60,11 +68,13 @@ class _MachineLearningSettingsPageState
|
|||
void dispose() {
|
||||
super.dispose();
|
||||
_eventSubscription.cancel();
|
||||
_wakeLock.disable();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bool facesFlag = flagService.faceSearchEnabled;
|
||||
_logger.info("On page open, facesFlag: $facesFlag");
|
||||
return Scaffold(
|
||||
body: CustomScrollView(
|
||||
primary: false,
|
||||
|
@ -79,8 +89,8 @@ class _MachineLearningSettingsPageState
|
|||
iconButtonType: IconButtonType.secondary,
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(context);
|
||||
if (Navigator.canPop(context)) Navigator.pop(context);
|
||||
if (Navigator.canPop(context)) Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
|
@ -204,7 +214,7 @@ class _MachineLearningSettingsPageState
|
|||
if (isEnabled) {
|
||||
unawaited(FaceMlService.instance.ensureInitialized());
|
||||
} else {
|
||||
FaceMlService.instance.pauseIndexing();
|
||||
FaceMlService.instance.pauseIndexingAndClustering();
|
||||
}
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
|
@ -434,17 +444,28 @@ class FaceRecognitionStatusWidgetState
|
|||
});
|
||||
}
|
||||
|
||||
Future<(int, int, int, double)> getIndexStatus() async {
|
||||
final indexedFiles = await FaceMLDataDB.instance
|
||||
.getIndexedFileCount(minimumMlVersion: faceMlVersion);
|
||||
final indexableFiles = (await FaceMlService.getIndexableFileIDs()).length;
|
||||
final showIndexedFiles = min(indexedFiles, indexableFiles);
|
||||
final pendingFiles = max(indexableFiles - indexedFiles, 0);
|
||||
final foundFaces = await FaceMLDataDB.instance.getTotalFaceCount();
|
||||
final clusteredFaces = await FaceMLDataDB.instance.getClusteredFaceCount();
|
||||
final clusteringDoneRatio = clusteredFaces / foundFaces;
|
||||
Future<(int, int, double, bool)> getIndexStatus() async {
|
||||
try {
|
||||
final indexedFiles = await FaceMLDataDB.instance
|
||||
.getIndexedFileCount(minimumMlVersion: faceMlVersion);
|
||||
final indexableFiles = (await getIndexableFileIDs()).length;
|
||||
final showIndexedFiles = min(indexedFiles, indexableFiles);
|
||||
final pendingFiles = max(indexableFiles - indexedFiles, 0);
|
||||
final clusteringDoneRatio =
|
||||
await FaceMLDataDB.instance.getClusteredToIndexableFilesRatio();
|
||||
final bool deviceIsHealthy =
|
||||
MachineLearningController.instance.isDeviceHealthy;
|
||||
|
||||
return (showIndexedFiles, pendingFiles, foundFaces, clusteringDoneRatio);
|
||||
return (
|
||||
showIndexedFiles,
|
||||
pendingFiles,
|
||||
clusteringDoneRatio,
|
||||
deviceIsHealthy
|
||||
);
|
||||
} catch (e, s) {
|
||||
_logger.severe('Error getting face recognition status', e, s);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -469,10 +490,17 @@ class FaceRecognitionStatusWidgetState
|
|||
if (snapshot.hasData) {
|
||||
final int indexedFiles = snapshot.data!.$1;
|
||||
final int pendingFiles = snapshot.data!.$2;
|
||||
final int foundFaces = snapshot.data!.$3;
|
||||
final double clusteringDoneRatio = snapshot.data!.$4;
|
||||
final double clusteringDoneRatio = snapshot.data!.$3;
|
||||
final double clusteringPercentage =
|
||||
(clusteringDoneRatio * 100).clamp(0, 100);
|
||||
final bool isDeviceHealthy = snapshot.data!.$4;
|
||||
|
||||
if (!isDeviceHealthy &&
|
||||
(pendingFiles > 0 || clusteringPercentage < 99)) {
|
||||
return MenuSectionDescriptionWidget(
|
||||
content: S.of(context).indexingIsPaused,
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
|
@ -502,19 +530,6 @@ class FaceRecognitionStatusWidgetState
|
|||
isGestureDetectorDisabled: true,
|
||||
key: ValueKey("pending_items_" + pendingFiles.toString()),
|
||||
),
|
||||
MenuItemWidget(
|
||||
captionedTextWidget: CaptionedTextWidget(
|
||||
title: S.of(context).foundFaces,
|
||||
),
|
||||
trailingWidget: Text(
|
||||
NumberFormat().format(foundFaces),
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
singleBorderRadius: 8,
|
||||
alignCaptionedTextToLeft: true,
|
||||
isGestureDetectorDisabled: true,
|
||||
key: ValueKey("found_faces_" + foundFaces.toString()),
|
||||
),
|
||||
MenuItemWidget(
|
||||
captionedTextWidget: CaptionedTextWidget(
|
||||
title: S.of(context).clusteringProgress,
|
||||
|
|
|
@ -17,9 +17,9 @@ import 'package:photos/ui/viewer/file/video_controls.dart';
|
|||
import "package:photos/utils/dialog_util.dart";
|
||||
import 'package:photos/utils/file_util.dart';
|
||||
import 'package:photos/utils/toast_util.dart';
|
||||
import "package:photos/utils/wakelock_util.dart";
|
||||
import 'package:video_player/video_player.dart';
|
||||
import 'package:visibility_detector/visibility_detector.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
|
||||
class VideoWidget extends StatefulWidget {
|
||||
final EnteFile file;
|
||||
|
@ -45,7 +45,7 @@ class _VideoWidgetState extends State<VideoWidget> {
|
|||
ChewieController? _chewieController;
|
||||
final _progressNotifier = ValueNotifier<double?>(null);
|
||||
bool _isPlaying = false;
|
||||
bool _wakeLockEnabledHere = false;
|
||||
final EnteWakeLock _wakeLock = EnteWakeLock();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
@ -126,13 +126,7 @@ class _VideoWidgetState extends State<VideoWidget> {
|
|||
_chewieController?.dispose();
|
||||
_progressNotifier.dispose();
|
||||
|
||||
if (_wakeLockEnabledHere) {
|
||||
unawaited(
|
||||
WakelockPlus.enabled.then((isEnabled) {
|
||||
isEnabled ? WakelockPlus.disable() : null;
|
||||
}),
|
||||
);
|
||||
}
|
||||
_wakeLock.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
@ -257,17 +251,10 @@ class _VideoWidgetState extends State<VideoWidget> {
|
|||
|
||||
Future<void> _keepScreenAliveOnPlaying(bool isPlaying) async {
|
||||
if (isPlaying) {
|
||||
return WakelockPlus.enabled.then((value) {
|
||||
if (value == false) {
|
||||
WakelockPlus.enable();
|
||||
//wakeLockEnabledHere will not be set to true if wakeLock is already enabled from settings on iOS.
|
||||
//We shouldn't disable when video is not playing if it was enabled manually by the user from ente settings by user.
|
||||
_wakeLockEnabledHere = true;
|
||||
}
|
||||
});
|
||||
_wakeLock.enable();
|
||||
}
|
||||
if (_wakeLockEnabledHere && !isPlaying) {
|
||||
return WakelockPlus.disable();
|
||||
if (!isPlaying) {
|
||||
_wakeLock.disable();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import "package:flutter/material.dart";
|
|||
import "package:flutter_animate/flutter_animate.dart";
|
||||
import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
|
||||
import "package:photos/core/configuration.dart";
|
||||
import "package:photos/core/constants.dart";
|
||||
import "package:photos/db/files_db.dart";
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import "package:photos/l10n/l10n.dart";
|
||||
|
@ -167,7 +168,14 @@ class AddPhotosPhotoWidget extends StatelessWidget {
|
|||
|
||||
Future<void> _onPickFromDeviceClicked(BuildContext context) async {
|
||||
try {
|
||||
final List<AssetEntity>? result = await AssetPicker.pickAssets(context);
|
||||
final assetPickerTextDelegate = await _getAssetPickerTextDelegate();
|
||||
final List<AssetEntity>? result = await AssetPicker.pickAssets(
|
||||
context,
|
||||
pickerConfig: AssetPickerConfig(
|
||||
maxAssets: maxPickAssetLimit,
|
||||
textDelegate: assetPickerTextDelegate,
|
||||
),
|
||||
);
|
||||
if (result != null && result.isNotEmpty) {
|
||||
final ca = CollectionActions(
|
||||
CollectionsService.instance,
|
||||
|
@ -204,6 +212,39 @@ class AddPhotosPhotoWidget extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// _getAssetPickerTextDelegate returns the text delegate for the asset picker
|
||||
// This custom method is required to enforce English as the default fallback
|
||||
// instead of Chinese.
|
||||
Future<AssetPickerTextDelegate> _getAssetPickerTextDelegate() async {
|
||||
final Locale locale = await getLocale();
|
||||
switch (locale.languageCode.toLowerCase()) {
|
||||
case "en":
|
||||
return const EnglishAssetPickerTextDelegate();
|
||||
case "he":
|
||||
return const HebrewAssetPickerTextDelegate();
|
||||
case "de":
|
||||
return const GermanAssetPickerTextDelegate();
|
||||
case "ru":
|
||||
return const RussianAssetPickerTextDelegate();
|
||||
case "ja":
|
||||
return const JapaneseAssetPickerTextDelegate();
|
||||
case "ar":
|
||||
return const ArabicAssetPickerTextDelegate();
|
||||
case "fr":
|
||||
return const FrenchAssetPickerTextDelegate();
|
||||
case "vi":
|
||||
return const VietnameseAssetPickerTextDelegate();
|
||||
case "tr":
|
||||
return const TurkishAssetPickerTextDelegate();
|
||||
case "ko":
|
||||
return const KoreanAssetPickerTextDelegate();
|
||||
case "zh":
|
||||
return const AssetPickerTextDelegate();
|
||||
default:
|
||||
return const EnglishAssetPickerTextDelegate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DelayedGallery extends StatefulWidget {
|
||||
|
|
|
@ -97,7 +97,7 @@ class _AppBarWidgetState extends State<ClusterAppBar> {
|
|||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
actions: kDebugMode ? _getDefaultActions(context) : null,
|
||||
actions: _getDefaultActions(context),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -161,43 +161,45 @@ class _ClusterPageState extends State<ClusterPage> {
|
|||
),
|
||||
),
|
||||
showNamingBanner
|
||||
? Dismissible(
|
||||
key: const Key("namingBanner"),
|
||||
direction: DismissDirection.horizontal,
|
||||
onDismissed: (direction) {
|
||||
setState(() {
|
||||
userDismissedNamingBanner = true;
|
||||
});
|
||||
},
|
||||
child: PeopleBanner(
|
||||
type: PeopleBannerType.addName,
|
||||
faceWidget: PersonFaceWidget(
|
||||
files.first,
|
||||
clusterID: widget.clusterID,
|
||||
),
|
||||
actionIcon: Icons.add_outlined,
|
||||
text: S.of(context).addAName,
|
||||
subText: S.of(context).findPeopleByName,
|
||||
onTap: () async {
|
||||
if (widget.personID == null) {
|
||||
final result = await showAssignPersonAction(
|
||||
context,
|
||||
clusterID: widget.clusterID,
|
||||
);
|
||||
if (result != null &&
|
||||
result is (PersonEntity, EnteFile)) {
|
||||
Navigator.pop(context);
|
||||
// ignore: unawaited_futures
|
||||
routeToPage(context, PeoplePage(person: result.$1));
|
||||
} else if (result != null && result is PersonEntity) {
|
||||
Navigator.pop(context);
|
||||
// ignore: unawaited_futures
|
||||
routeToPage(context, PeoplePage(person: result));
|
||||
}
|
||||
} else {
|
||||
showShortToast(context, "No personID or clusterID");
|
||||
}
|
||||
? SafeArea(
|
||||
child: Dismissible(
|
||||
key: const Key("namingBanner"),
|
||||
direction: DismissDirection.horizontal,
|
||||
onDismissed: (direction) {
|
||||
setState(() {
|
||||
userDismissedNamingBanner = true;
|
||||
});
|
||||
},
|
||||
child: PeopleBanner(
|
||||
type: PeopleBannerType.addName,
|
||||
faceWidget: PersonFaceWidget(
|
||||
files.first,
|
||||
clusterID: widget.clusterID,
|
||||
),
|
||||
actionIcon: Icons.add_outlined,
|
||||
text: S.of(context).addAName,
|
||||
subText: S.of(context).findPeopleByName,
|
||||
onTap: () async {
|
||||
if (widget.personID == null) {
|
||||
final result = await showAssignPersonAction(
|
||||
context,
|
||||
clusterID: widget.clusterID,
|
||||
);
|
||||
if (result != null &&
|
||||
result is (PersonEntity, EnteFile)) {
|
||||
Navigator.pop(context);
|
||||
// ignore: unawaited_futures
|
||||
routeToPage(context, PeoplePage(person: result.$1));
|
||||
} else if (result != null && result is PersonEntity) {
|
||||
Navigator.pop(context);
|
||||
// ignore: unawaited_futures
|
||||
routeToPage(context, PeoplePage(person: result));
|
||||
}
|
||||
} else {
|
||||
showShortToast(context, "No personID or clusterID");
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
|
|
|
@ -49,10 +49,8 @@ class CroppedFaceImageView extends StatelessWidget {
|
|||
|
||||
final faceBox = face.detection.box;
|
||||
|
||||
final double relativeFaceCenterX =
|
||||
faceBox.xMin + faceBox.width / 2;
|
||||
final double relativeFaceCenterY =
|
||||
faceBox.yMin + faceBox.height / 2;
|
||||
final double relativeFaceCenterX = faceBox.x + faceBox.width / 2;
|
||||
final double relativeFaceCenterY = faceBox.y + faceBox.height / 2;
|
||||
|
||||
const double desiredFaceHeightRelativeToWidget = 8 / 10;
|
||||
final double scale =
|
||||
|
|
|
@ -38,12 +38,17 @@ class _PersonClustersPageState extends State<PersonClustersPage> {
|
|||
.getClusterFilesForPersonID(widget.person.remoteID),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
final List<int> keys = snapshot.data!.keys.toList();
|
||||
final clusters = snapshot.data!;
|
||||
final List<int> keys = clusters.keys.toList();
|
||||
// Sort the clusters by the number of files in each cluster, largest first
|
||||
keys.sort(
|
||||
(b, a) => clusters[a]!.length.compareTo(clusters[b]!.length),
|
||||
);
|
||||
return ListView.builder(
|
||||
itemCount: keys.length,
|
||||
itemBuilder: (context, index) {
|
||||
final int clusterID = keys[index];
|
||||
final List<EnteFile> files = snapshot.data![keys[index]]!;
|
||||
final List<EnteFile> files = clusters[clusterID]!;
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
|
@ -93,34 +98,37 @@ class _PersonClustersPageState extends State<PersonClustersPage> {
|
|||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
"${snapshot.data![keys[index]]!.length} photos",
|
||||
"${files.length} photos",
|
||||
style: getEnteTextTheme(context).body,
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
try {
|
||||
await PersonService.instance
|
||||
.removeClusterToPerson(
|
||||
personID: widget.person.remoteID,
|
||||
clusterID: clusterID,
|
||||
);
|
||||
_logger.info(
|
||||
"Removed cluster $clusterID from person ${widget.person.remoteID}",
|
||||
);
|
||||
Bus.instance.fire(PeopleChangedEvent());
|
||||
setState(() {});
|
||||
} catch (e) {
|
||||
_logger.severe(
|
||||
"removing cluster from person,",
|
||||
e,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const Icon(
|
||||
CupertinoIcons.minus_circled,
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
(index != 0)
|
||||
? GestureDetector(
|
||||
onTap: () async {
|
||||
try {
|
||||
await PersonService.instance
|
||||
.removeClusterToPerson(
|
||||
personID: widget.person.remoteID,
|
||||
clusterID: clusterID,
|
||||
);
|
||||
_logger.info(
|
||||
"Removed cluster $clusterID from person ${widget.person.remoteID}",
|
||||
);
|
||||
Bus.instance
|
||||
.fire(PeopleChangedEvent());
|
||||
setState(() {});
|
||||
} catch (e) {
|
||||
_logger.severe(
|
||||
"removing cluster from person,",
|
||||
e,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const Icon(
|
||||
CupertinoIcons.minus_circled,
|
||||
color: Colors.red,
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
@ -203,7 +203,7 @@ class SearchWidgetState extends State<SearchWidget> {
|
|||
String query,
|
||||
) {
|
||||
int resultCount = 0;
|
||||
final maxResultCount = _isYearValid(query) ? 13 : 12;
|
||||
final maxResultCount = _isYearValid(query) ? 12 : 11;
|
||||
final streamController = StreamController<List<SearchResult>>();
|
||||
|
||||
if (query.isEmpty) {
|
||||
|
@ -260,10 +260,11 @@ class SearchWidgetState extends State<SearchWidget> {
|
|||
onResultsReceived(locationResult);
|
||||
},
|
||||
);
|
||||
|
||||
_searchService.getAllFace(null).then(
|
||||
(locationResult) {
|
||||
(faceResult) {
|
||||
final List<GenericSearchResult> filteredResults = [];
|
||||
for (final result in locationResult) {
|
||||
for (final result in faceResult) {
|
||||
if (result.name().toLowerCase().contains(query.toLowerCase())) {
|
||||
filteredResults.add(result);
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ import 'package:path_provider/path_provider.dart';
|
|||
import 'package:photos/core/configuration.dart';
|
||||
import 'package:photos/core/error-reporting/super_logging.dart';
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import "package:photos/ui/common/progress_dialog.dart";
|
||||
import 'package:photos/ui/components/buttons/button_widget.dart';
|
||||
import 'package:photos/ui/components/dialog_widget.dart';
|
||||
import 'package:photos/ui/components/models/button_type.dart';
|
||||
|
@ -122,9 +123,28 @@ Future<void> _sendLogs(
|
|||
}
|
||||
}
|
||||
|
||||
Future<String> getZippedLogsFile(BuildContext context) async {
|
||||
final dialog = createProgressDialog(context, S.of(context).preparingLogs);
|
||||
await dialog.show();
|
||||
Future<void> sendLogsForInit(
|
||||
String toEmail,
|
||||
String? subject,
|
||||
String? body,
|
||||
) async {
|
||||
final String zipFilePath = await getZippedLogsFile(null);
|
||||
final Email email = Email(
|
||||
recipients: [toEmail],
|
||||
subject: subject ?? '',
|
||||
body: body ?? '',
|
||||
attachmentPaths: [zipFilePath],
|
||||
isHTML: false,
|
||||
);
|
||||
await FlutterEmailSender.send(email);
|
||||
}
|
||||
|
||||
Future<String> getZippedLogsFile(BuildContext? context) async {
|
||||
late final ProgressDialog dialog;
|
||||
if (context != null) {
|
||||
dialog = createProgressDialog(context, S.of(context).preparingLogs);
|
||||
await dialog.show();
|
||||
}
|
||||
final logsPath = (await getApplicationSupportDirectory()).path;
|
||||
final logsDirectory = Directory(logsPath + "/logs");
|
||||
final tempPath = (await getTemporaryDirectory()).path;
|
||||
|
@ -134,7 +154,9 @@ Future<String> getZippedLogsFile(BuildContext context) async {
|
|||
encoder.create(zipFilePath);
|
||||
await encoder.addDirectory(logsDirectory);
|
||||
encoder.close();
|
||||
await dialog.hide();
|
||||
if (context != null) {
|
||||
await dialog.hide();
|
||||
}
|
||||
return zipFilePath;
|
||||
}
|
||||
|
||||
|
|
|
@ -110,8 +110,8 @@ FaceBoxImage _getSquareFaceBoxImage(img.Image image, FaceBox faceBox) {
|
|||
final width = (image.width * faceBox.width).round();
|
||||
final height = (image.height * faceBox.height).round();
|
||||
final side = max(width, height);
|
||||
final xImage = (image.width * faceBox.xMin).round();
|
||||
final yImage = (image.height * faceBox.yMin).round();
|
||||
final xImage = (image.width * faceBox.x).round();
|
||||
final yImage = (image.height * faceBox.y).round();
|
||||
|
||||
if (height >= width) {
|
||||
final xImageAdj = (xImage - (height - width) / 2).round();
|
||||
|
|
|
@ -37,25 +37,30 @@ Future<File?> getFile(
|
|||
bool isOrigin = false,
|
||||
} // only relevant for live photos
|
||||
) async {
|
||||
if (file.isRemoteFile) {
|
||||
return getFileFromServer(file, liveVideo: liveVideo);
|
||||
} else {
|
||||
final String key = file.tag + liveVideo.toString() + isOrigin.toString();
|
||||
final cachedFile = FileLruCache.get(key);
|
||||
if (cachedFile == null) {
|
||||
final diskFile = await _getLocalDiskFile(
|
||||
file,
|
||||
liveVideo: liveVideo,
|
||||
isOrigin: isOrigin,
|
||||
);
|
||||
// do not cache origin file for IOS as they are immediately deleted
|
||||
// after usage
|
||||
if (!(isOrigin && Platform.isIOS) && diskFile != null) {
|
||||
FileLruCache.put(key, diskFile);
|
||||
try {
|
||||
if (file.isRemoteFile) {
|
||||
return getFileFromServer(file, liveVideo: liveVideo);
|
||||
} else {
|
||||
final String key = file.tag + liveVideo.toString() + isOrigin.toString();
|
||||
final cachedFile = FileLruCache.get(key);
|
||||
if (cachedFile == null) {
|
||||
final diskFile = await _getLocalDiskFile(
|
||||
file,
|
||||
liveVideo: liveVideo,
|
||||
isOrigin: isOrigin,
|
||||
);
|
||||
// do not cache origin file for IOS as they are immediately deleted
|
||||
// after usage
|
||||
if (!(isOrigin && Platform.isIOS) && diskFile != null) {
|
||||
FileLruCache.put(key, diskFile);
|
||||
}
|
||||
return diskFile;
|
||||
}
|
||||
return diskFile;
|
||||
return cachedFile;
|
||||
}
|
||||
return cachedFile;
|
||||
} catch (e, s) {
|
||||
_logger.warning("Failed to get file", e, s);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -559,6 +559,7 @@ Future<Image> cropImageWithCanvasSimple(
|
|||
}
|
||||
|
||||
@Deprecated('Old image processing method, use `cropImage` instead!')
|
||||
|
||||
/// Crops an [image] based on the specified [x], [y], [width] and [height].
|
||||
/// Optionally, the cropped image can be resized to comply with a [maxSize] and/or [minSize].
|
||||
/// Optionally, the cropped image can be rotated from the center by [rotation] radians.
|
||||
|
@ -1276,8 +1277,8 @@ Future<List<Uint8List>> generateFaceThumbnails(
|
|||
|
||||
for (final faceBox in faceBoxes) {
|
||||
// Note that the faceBox values are relative to the image size, so we need to convert them to absolute values first
|
||||
final double xMinAbs = faceBox.xMin * img.width;
|
||||
final double yMinAbs = faceBox.yMin * img.height;
|
||||
final double xMinAbs = faceBox.x * img.width;
|
||||
final double yMinAbs = faceBox.y * img.height;
|
||||
final double widthAbs = faceBox.width * img.width;
|
||||
final double heightAbs = faceBox.height * img.height;
|
||||
|
||||
|
@ -1323,8 +1324,8 @@ Future<List<Uint8List>> generateFaceThumbnailsUsingCanvas(
|
|||
final futureFaceThumbnails = <Future<Uint8List>>[];
|
||||
for (final faceBox in faceBoxes) {
|
||||
// Note that the faceBox values are relative to the image size, so we need to convert them to absolute values first
|
||||
final double xMinAbs = faceBox.xMin * img.width;
|
||||
final double yMinAbs = faceBox.yMin * img.height;
|
||||
final double xMinAbs = faceBox.x * img.width;
|
||||
final double yMinAbs = faceBox.y * img.height;
|
||||
final double widthAbs = faceBox.width * img.width;
|
||||
final double heightAbs = faceBox.height * img.height;
|
||||
|
||||
|
|
7
mobile/lib/utils/ml_util.dart
Normal file
7
mobile/lib/utils/ml_util.dart
Normal file
|
@ -0,0 +1,7 @@
|
|||
import "package:photos/core/configuration.dart";
|
||||
import "package:photos/db/files_db.dart";
|
||||
|
||||
Future<List<int>> getIndexableFileIDs() async {
|
||||
return FilesDB.instance
|
||||
.getOwnedFileIDs(Configuration.instance.getUserID()!);
|
||||
}
|
38
mobile/lib/utils/wakelock_util.dart
Normal file
38
mobile/lib/utils/wakelock_util.dart
Normal file
|
@ -0,0 +1,38 @@
|
|||
import "dart:async" show unawaited;
|
||||
|
||||
import "package:wakelock_plus/wakelock_plus.dart";
|
||||
|
||||
class EnteWakeLock {
|
||||
bool _wakeLockEnabledHere = false;
|
||||
|
||||
void enable() {
|
||||
WakelockPlus.enabled.then((value) {
|
||||
if (value == false) {
|
||||
WakelockPlus.enable();
|
||||
//wakeLockEnabledHere will not be set to true if wakeLock is already enabled from settings on iOS.
|
||||
//We shouldn't disable when video is not playing if it was enabled manually by the user from ente settings by user.
|
||||
_wakeLockEnabledHere = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void disable() {
|
||||
if (_wakeLockEnabledHere) {
|
||||
WakelockPlus.disable();
|
||||
}
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
if (_wakeLockEnabledHere) {
|
||||
unawaited(
|
||||
WakelockPlus.enabled.then((isEnabled) {
|
||||
isEnabled ? WakelockPlus.disable() : null;
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> toggle({required bool enable}) async {
|
||||
await WakelockPlus.toggle(enable: enable);
|
||||
}
|
||||
}
|
|
@ -67,7 +67,7 @@ class FlagService {
|
|||
|
||||
bool get mapEnabled => flags.mapEnabled;
|
||||
|
||||
bool get faceSearchEnabled => internalUser || flags.faceSearchEnabled;
|
||||
bool get faceSearchEnabled => internalUser || flags.betaUser;
|
||||
|
||||
bool get passKeyEnabled => flags.passKeyEnabled || internalOrBetaUser;
|
||||
|
||||
|
|
|
@ -45,10 +45,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: animated_list_plus
|
||||
sha256: fe66f9c300d715254727fbdf050487844d17b013fec344fa28081d29bddbdf1a
|
||||
sha256: fb3d7f1fbaf5af84907f3c739236bacda8bf32cbe1f118dd51510752883ff50c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.4.5"
|
||||
version: "0.5.2"
|
||||
animated_stack_widget:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -971,10 +971,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: home_widget
|
||||
sha256: "29565bfee4b32eaf9e7e8b998d504618b779a74b2b1ac62dd4dac7468e66f1a3"
|
||||
sha256: "2a0fdd6267ff975bd07bedf74686bd5577200f504f5de36527ac1b56bdbe68e3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.0"
|
||||
version: "0.6.0"
|
||||
html:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1152,26 +1152,26 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa"
|
||||
sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.0.0"
|
||||
version: "10.0.4"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0
|
||||
sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
version: "3.0.3"
|
||||
leak_tracker_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_testing
|
||||
sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47
|
||||
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
version: "3.0.1"
|
||||
like_button:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -1368,10 +1368,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04
|
||||
sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.11.0"
|
||||
version: "1.12.0"
|
||||
mgrs_dart:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -2144,10 +2144,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: styled_text
|
||||
sha256: f72928d1ebe8cb149e3b34a689cb1ddca696b808187cf40ac3a0bd183dff379c
|
||||
sha256: fd624172cf629751b4f171dd0ecf9acf02a06df3f8a81bb56c0caa4f1df706c3
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.0"
|
||||
version: "8.1.0"
|
||||
sync_http:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -2160,18 +2160,18 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: syncfusion_flutter_core
|
||||
sha256: "9be1bb9bbdb42823439a18da71484f1964c14dbe1c255ab1b931932b12fa96e8"
|
||||
sha256: "63108a33f9b0d89f7b6b56cce908b8e519fe433dbbe0efcf41ad3e8bb2081bd9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "19.4.56"
|
||||
version: "25.2.5"
|
||||
syncfusion_flutter_sliders:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: syncfusion_flutter_sliders
|
||||
sha256: "1f6a63ccab4180b544074b9264a20f01ee80b553de154192fe1d7b434089d3c2"
|
||||
sha256: f27310bedc0e96e84054f0a70ac593d1a3c38397c158c5226ba86027ad77b2c1
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "19.4.56"
|
||||
version: "25.2.5"
|
||||
synchronized:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -2192,26 +2192,26 @@ packages:
|
|||
dependency: "direct dev"
|
||||
description:
|
||||
name: test
|
||||
sha256: a1f7595805820fcc05e5c52e3a231aedd0b72972cb333e8c738a8b1239448b6f
|
||||
sha256: "7ee446762c2c50b3bd4ea96fe13ffac69919352bd3b4b17bac3f3465edc58073"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.24.9"
|
||||
version: "1.25.2"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b"
|
||||
sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.1"
|
||||
version: "0.7.0"
|
||||
test_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_core
|
||||
sha256: a757b14fc47507060a162cc2530d9a4a2f92f5100a952c7443b5cad5ef5b106a
|
||||
sha256: "2bc4b4ecddd75309300d8096f781c0e3280ca1ef85beda558d33fcbedc2eead4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.9"
|
||||
version: "0.6.0"
|
||||
timezone:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -2441,10 +2441,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957
|
||||
sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "13.0.0"
|
||||
version: "14.2.1"
|
||||
volume_controller:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -2591,4 +2591,4 @@ packages:
|
|||
version: "3.1.2"
|
||||
sdks:
|
||||
dart: ">=3.3.0 <4.0.0"
|
||||
flutter: ">=3.19.0"
|
||||
flutter: ">=3.20.0-1.2.pre"
|
||||
|
|
|
@ -12,7 +12,7 @@ description: ente photos application
|
|||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
|
||||
version: 0.8.97+617
|
||||
version: 0.8.112+636
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
|
@ -21,7 +21,7 @@ environment:
|
|||
dependencies:
|
||||
adaptive_theme: ^3.1.0
|
||||
animate_do: ^2.0.0
|
||||
animated_list_plus: ^0.4.5
|
||||
animated_list_plus: ^0.5.2
|
||||
archive: ^3.1.2
|
||||
background_fetch: ^1.2.1
|
||||
battery_info: ^1.1.1
|
||||
|
@ -93,13 +93,13 @@ dependencies:
|
|||
fluttertoast: ^8.0.6
|
||||
freezed_annotation: ^2.4.1
|
||||
google_nav_bar: ^5.0.5
|
||||
home_widget: ^0.5.0
|
||||
home_widget: ^0.6.0
|
||||
html_unescape: ^2.0.0
|
||||
http: ^1.1.0
|
||||
image: ^4.0.17
|
||||
image_editor: ^1.3.0
|
||||
in_app_purchase: ^3.0.7
|
||||
intl: ^0.18.0
|
||||
intl: ^0.19.0
|
||||
json_annotation: ^4.8.0
|
||||
latlong2: ^0.9.0
|
||||
like_button: ^2.0.5
|
||||
|
@ -152,9 +152,9 @@ dependencies:
|
|||
sqlite3_flutter_libs: ^0.5.20
|
||||
sqlite_async: ^0.6.1
|
||||
step_progress_indicator: ^1.0.2
|
||||
styled_text: ^7.0.0
|
||||
syncfusion_flutter_core: ^19.2.49
|
||||
syncfusion_flutter_sliders: ^19.2.49
|
||||
styled_text: ^8.1.0
|
||||
syncfusion_flutter_core: ^25.2.5
|
||||
syncfusion_flutter_sliders: ^25.2.5
|
||||
synchronized: ^3.1.0
|
||||
tuple: ^2.0.0
|
||||
uni_links: ^0.5.1
|
||||
|
@ -177,6 +177,7 @@ dependency_overrides:
|
|||
# Remove this after removing dependency from flutter_sodium.
|
||||
# Newer flutter packages depends on ffi > 2.0.0 while flutter_sodium depends on ffi < 2.0.0
|
||||
ffi: 2.1.0
|
||||
intl: 0.18.1
|
||||
video_player:
|
||||
git:
|
||||
url: https://github.com/ente-io/packages.git
|
||||
|
|
|
@ -95,6 +95,15 @@ db:
|
|||
# Map of data centers
|
||||
#
|
||||
# Each data center also specifies which bucket in that provider should be used.
|
||||
#
|
||||
# If you're not using replication (it is off by default), you only need to
|
||||
# provide valid credentials for the first entry (the default hot storage,
|
||||
# "b2-eu-cen").
|
||||
#
|
||||
# Note that you need to use the same key names (e.g. "b2-eu-cen") as below. The
|
||||
# values and the S3 provider itself can any arbitrary S3 storage, it is not tied
|
||||
# to the region (eu-cen) or provider (b2, wasabi), but for historical reasons
|
||||
# the key names have to be one of those in the list below.
|
||||
s3:
|
||||
# Override the primary and secondary hot storage. The commented out values
|
||||
# are the defaults.
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,17 +1,16 @@
|
|||
import { CustomHead } from "@/next/components/Head";
|
||||
import { setupI18n } from "@/next/i18n";
|
||||
import { logUnhandledErrorsAndRejections } from "@/next/log-web";
|
||||
import type { AppName, BaseAppContextT } from "@/next/types/app";
|
||||
import { ensure } from "@/utils/ensure";
|
||||
import { PAGES } from "@ente/accounts/constants/pages";
|
||||
import { accountLogout } from "@ente/accounts/services/logout";
|
||||
import { APPS, APP_TITLES } from "@ente/shared/apps/constants";
|
||||
import { Overlay } from "@ente/shared/components/Container";
|
||||
import DialogBoxV2 from "@ente/shared/components/DialogBoxV2";
|
||||
import {
|
||||
DialogBoxAttributesV2,
|
||||
SetDialogBoxAttributesV2,
|
||||
} from "@ente/shared/components/DialogBoxV2/types";
|
||||
import type { DialogBoxAttributesV2 } from "@ente/shared/components/DialogBoxV2/types";
|
||||
import EnteSpinner from "@ente/shared/components/EnteSpinner";
|
||||
import AppNavbar from "@ente/shared/components/Navbar/app";
|
||||
import { AppNavbar } from "@ente/shared/components/Navbar/app";
|
||||
import { useLocalState } from "@ente/shared/hooks/useLocalState";
|
||||
import HTTPService from "@ente/shared/network/HTTPService";
|
||||
import { LS_KEYS, getData } from "@ente/shared/storage/localStorage";
|
||||
|
@ -22,25 +21,28 @@ import { ThemeProvider } from "@mui/material/styles";
|
|||
import { t } from "i18next";
|
||||
import { AppProps } from "next/app";
|
||||
import { useRouter } from "next/router";
|
||||
import { createContext, useEffect, useState } from "react";
|
||||
import { createContext, useContext, useEffect, useState } from "react";
|
||||
import "styles/global.css";
|
||||
|
||||
interface AppContextProps {
|
||||
isMobile: boolean;
|
||||
showNavBar: (show: boolean) => void;
|
||||
setDialogBoxAttributesV2: SetDialogBoxAttributesV2;
|
||||
logout: () => void;
|
||||
}
|
||||
/** The accounts app has no extra properties on top of the base context. */
|
||||
type AppContextT = BaseAppContextT;
|
||||
|
||||
export const AppContext = createContext<AppContextProps>({} as AppContextProps);
|
||||
/** The React {@link Context} available to all pages. */
|
||||
export const AppContext = createContext<AppContextT | undefined>(undefined);
|
||||
|
||||
/** Utility hook to reduce amount of boilerplate in account related pages. */
|
||||
export const useAppContext = () => ensure(useContext(AppContext));
|
||||
|
||||
export default function App({ Component, pageProps }: AppProps) {
|
||||
const appName: AppName = "account";
|
||||
|
||||
const [isI18nReady, setIsI18nReady] = useState<boolean>(false);
|
||||
|
||||
const [showNavbar, setShowNavBar] = useState(false);
|
||||
|
||||
const [dialogBoxAttributeV2, setDialogBoxAttributesV2] =
|
||||
useState<DialogBoxAttributesV2>();
|
||||
const [dialogBoxAttributeV2, setDialogBoxAttributesV2] = useState<
|
||||
DialogBoxAttributesV2 | undefined
|
||||
>();
|
||||
|
||||
const [dialogBoxV2View, setDialogBoxV2View] = useState(false);
|
||||
|
||||
|
@ -85,8 +87,17 @@ export default function App({ Component, pageProps }: AppProps) {
|
|||
void accountLogout().then(() => router.push(PAGES.ROOT));
|
||||
};
|
||||
|
||||
const appContext = {
|
||||
appName,
|
||||
logout,
|
||||
showNavBar,
|
||||
isMobile,
|
||||
setDialogBoxAttributesV2,
|
||||
};
|
||||
|
||||
// TODO: This string doesn't actually exist
|
||||
const title = isI18nReady
|
||||
? t("TITLE", { context: APPS.ACCOUNTS })
|
||||
? t("title", { context: "accounts" })
|
||||
: APP_TITLES.get(APPS.ACCOUNTS);
|
||||
|
||||
return (
|
||||
|
@ -102,15 +113,7 @@ export default function App({ Component, pageProps }: AppProps) {
|
|||
attributes={dialogBoxAttributeV2 as any}
|
||||
/>
|
||||
|
||||
<AppContext.Provider
|
||||
value={{
|
||||
isMobile,
|
||||
showNavBar,
|
||||
setDialogBoxAttributesV2:
|
||||
setDialogBoxAttributesV2 as any,
|
||||
logout,
|
||||
}}
|
||||
>
|
||||
<AppContext.Provider value={appContext}>
|
||||
{!isI18nReady && (
|
||||
<Overlay
|
||||
sx={(theme) => ({
|
||||
|
|
6
web/apps/accounts/src/pages/credentials.tsx
Normal file
6
web/apps/accounts/src/pages/credentials.tsx
Normal file
|
@ -0,0 +1,6 @@
|
|||
import Page_ from "@ente/accounts/pages/credentials";
|
||||
import { useAppContext } from "./_app";
|
||||
|
||||
const Page = () => <Page_ appContext={useAppContext()} />;
|
||||
|
||||
export default Page;
|
|
@ -1,9 +0,0 @@
|
|||
import CredentialPage from "@ente/accounts/pages/credentials";
|
||||
import { APPS } from "@ente/shared/apps/constants";
|
||||
import { useContext } from "react";
|
||||
import { AppContext } from "../_app";
|
||||
|
||||
export default function Credential() {
|
||||
const appContext = useContext(AppContext);
|
||||
return <CredentialPage appContext={appContext} appName={APPS.ACCOUNTS} />;
|
||||
}
|
6
web/apps/accounts/src/pages/generate.tsx
Normal file
6
web/apps/accounts/src/pages/generate.tsx
Normal file
|
@ -0,0 +1,6 @@
|
|||
import Page_ from "@ente/accounts/pages/generate";
|
||||
import { useAppContext } from "./_app";
|
||||
|
||||
const Page = () => <Page_ appContext={useAppContext()} />;
|
||||
|
||||
export default Page;
|
|
@ -1,9 +0,0 @@
|
|||
import GeneratePage from "@ente/accounts/pages/generate";
|
||||
import { APPS } from "@ente/shared/apps/constants";
|
||||
import { AppContext } from "pages/_app";
|
||||
import { useContext } from "react";
|
||||
|
||||
export default function Generate() {
|
||||
const appContext = useContext(AppContext);
|
||||
return <GeneratePage appContext={appContext} appName={APPS.ACCOUNTS} />;
|
||||
}
|
6
web/apps/accounts/src/pages/login.tsx
Normal file
6
web/apps/accounts/src/pages/login.tsx
Normal file
|
@ -0,0 +1,6 @@
|
|||
import Page_ from "@ente/accounts/pages/login";
|
||||
import { useAppContext } from "./_app";
|
||||
|
||||
const Page = () => <Page_ appContext={useAppContext()} />;
|
||||
|
||||
export default Page;
|
|
@ -1,9 +0,0 @@
|
|||
import LoginPage from "@ente/accounts/pages/login";
|
||||
import { APPS } from "@ente/shared/apps/constants";
|
||||
import { useContext } from "react";
|
||||
import { AppContext } from "../_app";
|
||||
|
||||
export default function Login() {
|
||||
const appContext = useContext(AppContext);
|
||||
return <LoginPage appContext={appContext} appName={APPS.ACCOUNTS} />;
|
||||
}
|
|
@ -1,16 +1,12 @@
|
|||
import { TwoFactorType } from "@ente/accounts/constants/twofactor";
|
||||
import RecoverPage from "@ente/accounts/pages/recover";
|
||||
import { APPS } from "@ente/shared/apps/constants";
|
||||
import { AppContext } from "pages/_app";
|
||||
import { useContext } from "react";
|
||||
import RecoverPage from "@ente/accounts/pages/two-factor/recover";
|
||||
import { useAppContext } from "../../_app";
|
||||
|
||||
export default function Recover() {
|
||||
const appContext = useContext(AppContext);
|
||||
return (
|
||||
<RecoverPage
|
||||
appContext={appContext}
|
||||
appName={APPS.PHOTOS}
|
||||
twoFactorType={TwoFactorType.PASSKEY}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const Page = () => (
|
||||
<RecoverPage
|
||||
appContext={useAppContext()}
|
||||
twoFactorType={TwoFactorType.PASSKEY}
|
||||
/>
|
||||
);
|
||||
|
||||
export default Page;
|
||||
|
|
|
@ -9,14 +9,8 @@ import { t } from "i18next";
|
|||
import _sodium from "libsodium-wrappers";
|
||||
import { useRouter } from "next/router";
|
||||
import { AppContext } from "pages/_app";
|
||||
import {
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
import { createContext, useContext, useEffect, useState } from "react";
|
||||
import { Passkey } from "types/passkey";
|
||||
import {
|
||||
finishPasskeyRegistration,
|
||||
|
|
6
web/apps/accounts/src/pages/recover.tsx
Normal file
6
web/apps/accounts/src/pages/recover.tsx
Normal file
|
@ -0,0 +1,6 @@
|
|||
import Page_ from "@ente/accounts/pages/recover";
|
||||
import { useAppContext } from "./_app";
|
||||
|
||||
const Page = () => <Page_ appContext={useAppContext()} />;
|
||||
|
||||
export default Page;
|
|
@ -1,9 +0,0 @@
|
|||
import RecoverPage from "@ente/accounts/pages/recover";
|
||||
import { APPS } from "@ente/shared/apps/constants";
|
||||
import { AppContext } from "pages/_app";
|
||||
import { useContext } from "react";
|
||||
|
||||
export default function Recover() {
|
||||
const appContext = useContext(AppContext);
|
||||
return <RecoverPage appContext={appContext} appName={APPS.ACCOUNTS} />;
|
||||
}
|
6
web/apps/accounts/src/pages/signup.tsx
Normal file
6
web/apps/accounts/src/pages/signup.tsx
Normal file
|
@ -0,0 +1,6 @@
|
|||
import Page_ from "@ente/accounts/pages/signup";
|
||||
import { useAppContext } from "./_app";
|
||||
|
||||
const Page = () => <Page_ appContext={useAppContext()} />;
|
||||
|
||||
export default Page;
|
|
@ -1,9 +0,0 @@
|
|||
import SignupPage from "@ente/accounts/pages/signup";
|
||||
import { APPS } from "@ente/shared/apps/constants";
|
||||
import { AppContext } from "pages/_app";
|
||||
import { useContext } from "react";
|
||||
|
||||
export default function Sigup() {
|
||||
const appContext = useContext(AppContext);
|
||||
return <SignupPage appContext={appContext} appName={APPS.ACCOUNTS} />;
|
||||
}
|
6
web/apps/accounts/src/pages/two-factor/recover.tsx
Normal file
6
web/apps/accounts/src/pages/two-factor/recover.tsx
Normal file
|
@ -0,0 +1,6 @@
|
|||
import Page_ from "@ente/accounts/pages/two-factor/recover";
|
||||
import { useAppContext } from "../_app";
|
||||
|
||||
const Page = () => <Page_ appContext={useAppContext()} />;
|
||||
|
||||
export default Page;
|
|
@ -1,11 +0,0 @@
|
|||
import TwoFactorRecoverPage from "@ente/accounts/pages/two-factor/recover";
|
||||
import { APPS } from "@ente/shared/apps/constants";
|
||||
import { AppContext } from "pages/_app";
|
||||
import { useContext } from "react";
|
||||
|
||||
export default function TwoFactorRecover() {
|
||||
const appContext = useContext(AppContext);
|
||||
return (
|
||||
<TwoFactorRecoverPage appContext={appContext} appName={APPS.ACCOUNTS} />
|
||||
);
|
||||
}
|
6
web/apps/accounts/src/pages/two-factor/setup.tsx
Normal file
6
web/apps/accounts/src/pages/two-factor/setup.tsx
Normal file
|
@ -0,0 +1,6 @@
|
|||
import Page_ from "@ente/accounts/pages/two-factor/setup";
|
||||
import { useAppContext } from "../_app";
|
||||
|
||||
const Page = () => <Page_ appContext={useAppContext()} />;
|
||||
|
||||
export default Page;
|
|
@ -1,11 +0,0 @@
|
|||
import TwoFactorSetupPage from "@ente/accounts/pages/two-factor/setup";
|
||||
import { APPS } from "@ente/shared/apps/constants";
|
||||
import { AppContext } from "pages/_app";
|
||||
import { useContext } from "react";
|
||||
|
||||
export default function TwoFactorSetup() {
|
||||
const appContext = useContext(AppContext);
|
||||
return (
|
||||
<TwoFactorSetupPage appContext={appContext} appName={APPS.ACCOUNTS} />
|
||||
);
|
||||
}
|
6
web/apps/accounts/src/pages/two-factor/verify.tsx
Normal file
6
web/apps/accounts/src/pages/two-factor/verify.tsx
Normal file
|
@ -0,0 +1,6 @@
|
|||
import Page_ from "@ente/accounts/pages/two-factor/verify";
|
||||
import { useAppContext } from "../_app";
|
||||
|
||||
const Page = () => <Page_ appContext={useAppContext()} />;
|
||||
|
||||
export default Page;
|
|
@ -1,11 +0,0 @@
|
|||
import TwoFactorVerifyPage from "@ente/accounts/pages/two-factor/verify";
|
||||
import { APPS } from "@ente/shared/apps/constants";
|
||||
import { AppContext } from "pages/_app";
|
||||
import { useContext } from "react";
|
||||
|
||||
export default function TwoFactorVerify() {
|
||||
const appContext = useContext(AppContext);
|
||||
return (
|
||||
<TwoFactorVerifyPage appContext={appContext} appName={APPS.ACCOUNTS} />
|
||||
);
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue