Merge branch 'main' into generic_group_by
This commit is contained in:
commit
bcf3084d97
14
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
14
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
@ -4,11 +4,12 @@ labels: ["triage"]
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: >
|
value: |
|
||||||
Before opening a new issue, please ensure you are on the latest
|
Before opening a new bug report, please ensure
|
||||||
version (it might've already been fixed), and that you've searched
|
1. you are on the latest version (it might've already been fixed),
|
||||||
for existing issues (please add you observations as a comment
|
2. you've searched for existing issues (please add your observations as a comment there instead of creating a duplicate).
|
||||||
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
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Description
|
label: Description
|
||||||
|
@ -16,7 +17,8 @@ body:
|
||||||
Please describe the bug. If possible, also include the steps to
|
Please describe the bug. If possible, also include the steps to
|
||||||
reproduce the behaviour, and the expected behaviour (sometimes
|
reproduce the behaviour, and the expected behaviour (sometimes
|
||||||
bugs are just expectation mismatches, in which case this would be
|
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:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: input
|
- type: input
|
||||||
|
|
|
@ -4,7 +4,7 @@ on:
|
||||||
workflow_dispatch: # Allow manually running the action
|
workflow_dispatch: # Allow manually running the action
|
||||||
|
|
||||||
env:
|
env:
|
||||||
FLUTTER_VERSION: "3.19.3"
|
FLUTTER_VERSION: "3.22.0"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
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"
|
- ".github/workflows/mobile-lint.yml"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
FLUTTER_VERSION: "3.19.5"
|
FLUTTER_VERSION: "3.22.0"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
|
|
|
@ -20,6 +20,8 @@
|
||||||
"codeIssuerHint": "発行者",
|
"codeIssuerHint": "発行者",
|
||||||
"codeSecretKeyHint": "秘密鍵",
|
"codeSecretKeyHint": "秘密鍵",
|
||||||
"codeAccountHint": "アカウント (you@domain.com)",
|
"codeAccountHint": "アカウント (you@domain.com)",
|
||||||
|
"codeTagHint": "タグ",
|
||||||
|
"accountKeyType": "鍵の種類",
|
||||||
"sessionExpired": "セッションが失効しました",
|
"sessionExpired": "セッションが失効しました",
|
||||||
"@sessionExpired": {
|
"@sessionExpired": {
|
||||||
"description": "Title of the dialog when the users current session is invalid/expired"
|
"description": "Title of the dialog when the users current session is invalid/expired"
|
||||||
|
@ -77,6 +79,7 @@
|
||||||
"data": "データ",
|
"data": "データ",
|
||||||
"importCodes": "コードをインポート",
|
"importCodes": "コードをインポート",
|
||||||
"importTypePlainText": "プレーンテキスト",
|
"importTypePlainText": "プレーンテキスト",
|
||||||
|
"importTypeEnteEncrypted": "Ente 暗号化されたエクスポート",
|
||||||
"passwordForDecryptingExport": "復号化用パスワード",
|
"passwordForDecryptingExport": "復号化用パスワード",
|
||||||
"passwordEmptyError": "パスワードは空欄にできません",
|
"passwordEmptyError": "パスワードは空欄にできません",
|
||||||
"importFromApp": "{appName} からコードをインポート",
|
"importFromApp": "{appName} からコードをインポート",
|
||||||
|
@ -121,6 +124,7 @@
|
||||||
"suggestFeatures": "機能を提案",
|
"suggestFeatures": "機能を提案",
|
||||||
"faq": "FAQ",
|
"faq": "FAQ",
|
||||||
"faq_q_1": "Authはどのくらい安全ですか?",
|
"faq_q_1": "Authはどのくらい安全ですか?",
|
||||||
|
"faq_a_1": "Ente Authでバックアップされたコードはすべてエンドツーエンドで暗号化されて保存されます。つまり、コードにアクセスできるのはあなただけです。当社のアプリはオープンソースであり、暗号化技術は外部監査を受けています。",
|
||||||
"faq_q_2": "パソコンから私のコードにアクセスできますか?",
|
"faq_q_2": "パソコンから私のコードにアクセスできますか?",
|
||||||
"faq_a_2": "auth.ente.io で Web からコードにアクセス可能です。",
|
"faq_a_2": "auth.ente.io で Web からコードにアクセス可能です。",
|
||||||
"faq_q_3": "コードを削除するにはどうすればいいですか?",
|
"faq_q_3": "コードを削除するにはどうすればいいですか?",
|
||||||
|
@ -154,6 +158,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"invalidQRCode": "QRコードが無効です",
|
||||||
"noRecoveryKeyTitle": "回復キーがありませんか?",
|
"noRecoveryKeyTitle": "回復キーがありませんか?",
|
||||||
"enterEmailHint": "メールアドレスを入力してください",
|
"enterEmailHint": "メールアドレスを入力してください",
|
||||||
"invalidEmailTitle": "メールアドレスが無効です",
|
"invalidEmailTitle": "メールアドレスが無効です",
|
||||||
|
@ -347,6 +352,7 @@
|
||||||
"deleteCodeAuthMessage": "コードを削除するためには認証が必要です",
|
"deleteCodeAuthMessage": "コードを削除するためには認証が必要です",
|
||||||
"showQRAuthMessage": "QR コードを表示するためには認証が必要です",
|
"showQRAuthMessage": "QR コードを表示するためには認証が必要です",
|
||||||
"confirmAccountDeleteTitle": "アカウントの削除に同意",
|
"confirmAccountDeleteTitle": "アカウントの削除に同意",
|
||||||
|
"confirmAccountDeleteMessage": "このアカウントは他のEnteアプリも使用している場合はそれらにも紐づけされています。\nすべてのEnteアプリでアップロードされたデータは削除され、アカウントは完全に削除されます。",
|
||||||
"androidBiometricHint": "本人を確認する",
|
"androidBiometricHint": "本人を確認する",
|
||||||
"@androidBiometricHint": {
|
"@androidBiometricHint": {
|
||||||
"description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters."
|
"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": "無効なエンドポイントです",
|
"invalidEndpoint": "無効なエンドポイントです",
|
||||||
"invalidEndpointMessage": "入力されたエンドポイントは無効です。有効なエンドポイントを入力して再試行してください。",
|
"invalidEndpointMessage": "入力されたエンドポイントは無効です。有効なエンドポイントを入力して再試行してください。",
|
||||||
"endpointUpdatedMessage": "エンドポイントの更新に成功しました",
|
"endpointUpdatedMessage": "エンドポイントの更新に成功しました",
|
||||||
"customEndpoint": "{endpoint} に接続しました"
|
"customEndpoint": "{endpoint} に接続しました",
|
||||||
|
"pinText": "固定",
|
||||||
|
"unpinText": "固定を解除",
|
||||||
|
"pinnedCodeMessage": "{code} を固定しました",
|
||||||
|
"unpinnedCodeMessage": "{code} の固定が解除されました",
|
||||||
|
"tags": "タグ",
|
||||||
|
"createNewTag": "新しいタグの作成",
|
||||||
|
"tag": "タグ",
|
||||||
|
"create": "作成",
|
||||||
|
"editTag": "タグの編集",
|
||||||
|
"deleteTagTitle": "タグを削除しますか?",
|
||||||
|
"deleteTagMessage": "このタグを削除してもよろしいですか?この操作は取り消しできません。",
|
||||||
|
"somethingWentWrongParsingCode": "{x} のコードを解析できませんでした。",
|
||||||
|
"updateNotAvailable": "アップデートは利用できません"
|
||||||
}
|
}
|
|
@ -106,7 +106,7 @@ const handleRead = async (path: string) => {
|
||||||
res.headers.set("Content-Length", `${fileSize}`);
|
res.headers.set("Content-Length", `${fileSize}`);
|
||||||
|
|
||||||
// Add the file's last modified time (as epoch milliseconds).
|
// 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}`);
|
res.headers.set("X-Last-Modified-Ms", `${mtimeMs}`);
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
|
@ -132,6 +132,13 @@ const handleReadZip = async (zipPath: string, entryName: string) => {
|
||||||
// Close the zip handle when the underlying stream closes.
|
// Close the zip handle when the underlying stream closes.
|
||||||
stream.on("end", () => void zip.close());
|
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, {
|
return new Response(webReadableStream, {
|
||||||
headers: {
|
headers: {
|
||||||
// We don't know the exact type, but it doesn't really matter, just
|
// 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.
|
// doesn't tinker with it thinking of it as text.
|
||||||
"Content-Type": "application/octet-stream",
|
"Content-Type": "application/octet-stream",
|
||||||
"Content-Length": `${entry.size}`,
|
"Content-Length": `${entry.size}`,
|
||||||
// While it is documented that entry.time is the modification time,
|
"X-Last-Modified-Ms": `${modifiedMs}`,
|
||||||
// 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}`,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -163,6 +163,10 @@ export const sidebar = [
|
||||||
text: "From Authy",
|
text: "From Authy",
|
||||||
link: "/auth/migration-guides/authy/",
|
link: "/auth/migration-guides/authy/",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: "From Steam",
|
||||||
|
link: "/auth/migration-guides/steam/",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: "Exporting your data",
|
text: "Exporting your data",
|
||||||
link: "/auth/migration-guides/export",
|
link: "/auth/migration-guides/export",
|
||||||
|
|
|
@ -7,4 +7,5 @@ description:
|
||||||
# Migrating to/from Ente Auth
|
# Migrating to/from Ente Auth
|
||||||
|
|
||||||
- [Migrating from Authy](authy/)
|
- [Migrating from Authy](authy/)
|
||||||
|
- [Importing codes from Steam](steam/)
|
||||||
- [Exporting your data out of Ente Auth](export)
|
- [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
|
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
|
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.
|
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
|
## 🧑💻 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`
|
2. Pull in all submodules with `git submodule update --init --recursive`
|
||||||
|
|
||||||
|
|
|
@ -427,7 +427,7 @@ SPEC CHECKSUMS:
|
||||||
home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57
|
home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57
|
||||||
image_editor_common: d6f6644ae4a6de80481e89fe6d0a8c49e30b4b43
|
image_editor_common: d6f6644ae4a6de80481e89fe6d0a8c49e30b4b43
|
||||||
in_app_purchase_storekit: 0e4b3c2e43ba1e1281f4f46dd71b0593ce529892
|
in_app_purchase_storekit: 0e4b3c2e43ba1e1281f4f46dd71b0593ce529892
|
||||||
integration_test: 13825b8a9334a850581300559b8839134b124670
|
integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4
|
||||||
libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009
|
libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009
|
||||||
local_auth_darwin: c7e464000a6a89e952235699e32b329457608d98
|
local_auth_darwin: c7e464000a6a89e952235699e32b329457608d98
|
||||||
local_auth_ios: 5046a18c018dd973247a0564496c8898dbb5adf9
|
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/crypto_util.dart';
|
||||||
import 'package:photos/utils/file_uploader.dart';
|
import 'package:photos/utils/file_uploader.dart';
|
||||||
import 'package:photos/utils/validator_util.dart';
|
import 'package:photos/utils/validator_util.dart';
|
||||||
|
import "package:photos/utils/wakelock_util.dart";
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import "package:tuple/tuple.dart";
|
import "package:tuple/tuple.dart";
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
|
||||||
|
|
||||||
class Configuration {
|
class Configuration {
|
||||||
Configuration._privateConstructor();
|
Configuration._privateConstructor();
|
||||||
|
@ -585,7 +585,7 @@ class Configuration {
|
||||||
|
|
||||||
Future<void> setShouldKeepDeviceAwake(bool value) async {
|
Future<void> setShouldKeepDeviceAwake(bool value) async {
|
||||||
await _preferences.setBool(keyShouldKeepDeviceAwake, value);
|
await _preferences.setBool(keyShouldKeepDeviceAwake, value);
|
||||||
await WakelockPlus.toggle(enable: value);
|
await EnteWakeLock.toggle(enable: value);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> setShouldBackupVideos(bool value) async {
|
Future<void> setShouldBackupVideos(bool value) async {
|
||||||
|
|
|
@ -69,6 +69,8 @@ const galleryGridSpacing = 2.0;
|
||||||
|
|
||||||
const kSearchSectionLimit = 9;
|
const kSearchSectionLimit = 9;
|
||||||
|
|
||||||
|
const maxPickAssetLimit = 50;
|
||||||
|
|
||||||
const iOSGroupID = "group.io.ente.frame.SlideshowWidget";
|
const iOSGroupID = "group.io.ente.frame.SlideshowWidget";
|
||||||
|
|
||||||
const blackThumbnailBase64 = '/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEB'
|
const blackThumbnailBase64 = '/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEB'
|
||||||
|
|
|
@ -13,6 +13,8 @@ import "package:photos/face/model/face.dart";
|
||||||
import "package:photos/models/file/file.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_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_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';
|
import 'package:sqlite_async/sqlite_async.dart';
|
||||||
|
|
||||||
/// Stores all data for the FacesML-related features. The database can be accessed by `FaceMLDataDB.instance.database`.
|
/// Stores all data for the FacesML-related features. The database can be accessed by `FaceMLDataDB.instance.database`.
|
||||||
|
@ -249,7 +251,7 @@ class FaceMLDataDB {
|
||||||
final List<int> fileId = [recentFileID];
|
final List<int> fileId = [recentFileID];
|
||||||
int? avatarFileId;
|
int? avatarFileId;
|
||||||
if (avatarFaceId != null) {
|
if (avatarFaceId != null) {
|
||||||
avatarFileId = int.tryParse(avatarFaceId.split('_')[0]);
|
avatarFileId = tryGetFileIdFromFaceId(avatarFaceId);
|
||||||
if (avatarFileId != null) {
|
if (avatarFileId != null) {
|
||||||
fileId.add(avatarFileId);
|
fileId.add(avatarFileId);
|
||||||
}
|
}
|
||||||
|
@ -401,8 +403,10 @@ class FaceMLDataDB {
|
||||||
final personID = map[personIdColumn] as String;
|
final personID = map[personIdColumn] as String;
|
||||||
final clusterID = map[fcClusterID] as int;
|
final clusterID = map[fcClusterID] as int;
|
||||||
final faceID = map[fcFaceId] as String;
|
final faceID = map[fcFaceId] as String;
|
||||||
result.putIfAbsent(personID, () => {}).putIfAbsent(clusterID, () => {})
|
result
|
||||||
.add(faceID);
|
.putIfAbsent(personID, () => {})
|
||||||
|
.putIfAbsent(clusterID, () => {})
|
||||||
|
.add(faceID);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
@ -476,8 +480,7 @@ class FaceMLDataDB {
|
||||||
for (final map in maps) {
|
for (final map in maps) {
|
||||||
final clusterID = map[fcClusterID] as int;
|
final clusterID = map[fcClusterID] as int;
|
||||||
final faceID = map[fcFaceId] as String;
|
final faceID = map[fcFaceId] as String;
|
||||||
final x = faceID.split('_').first;
|
final fileID = getFileIdFromFaceId(faceID);
|
||||||
final fileID = int.parse(x);
|
|
||||||
result[fileID] = (result[fileID] ?? {})..add(clusterID);
|
result[fileID] = (result[fileID] ?? {})..add(clusterID);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
@ -665,19 +668,55 @@ class FaceMLDataDB {
|
||||||
return maps.first['count'] as int;
|
return maps.first['count'] as int;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int> getClusteredFaceCount() async {
|
Future<int> getClusteredOrFacelessFileCount() async {
|
||||||
final db = await instance.asyncDB;
|
final db = await instance.asyncDB;
|
||||||
final List<Map<String, dynamic>> maps = await db.getAll(
|
final List<Map<String, dynamic>> clustered = await db.getAll(
|
||||||
'SELECT COUNT(DISTINCT $fcFaceId) as count FROM $faceClustersTable',
|
'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 {
|
Future<double> getClusteredToIndexableFilesRatio() async {
|
||||||
final int totalFaces = await getTotalFaceCount();
|
final int indexableFiles = (await getIndexableFileIDs()).length;
|
||||||
final int clusteredFaces = await getClusteredFaceCount();
|
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([
|
Future<int> getBlurryFaceCount([
|
||||||
|
@ -795,7 +834,7 @@ class FaceMLDataDB {
|
||||||
for (final map in maps) {
|
for (final map in maps) {
|
||||||
final clusterID = map[clusterIDColumn] as int;
|
final clusterID = map[clusterIDColumn] as int;
|
||||||
final String faceID = map[fcFaceId] as String;
|
final String faceID = map[fcFaceId] as String;
|
||||||
final fileID = int.parse(faceID.split('_').first);
|
final fileID = getFileIdFromFaceId(faceID);
|
||||||
result[fileID] = (result[fileID] ?? {})..add(clusterID);
|
result[fileID] = (result[fileID] ?? {})..add(clusterID);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
@ -814,8 +853,8 @@ class FaceMLDataDB {
|
||||||
final Map<int, Set<int>> result = {};
|
final Map<int, Set<int>> result = {};
|
||||||
for (final map in maps) {
|
for (final map in maps) {
|
||||||
final clusterID = map[fcClusterID] as int;
|
final clusterID = map[fcClusterID] as int;
|
||||||
final faceId = map[fcFaceId] as String;
|
final faceID = map[fcFaceId] as String;
|
||||||
final fileID = int.parse(faceId.split("_").first);
|
final fileID = getFileIdFromFaceId(faceID);
|
||||||
result[fileID] = (result[fileID] ?? {})..add(clusterID);
|
result[fileID] = (result[fileID] ?? {})..add(clusterID);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
@ -964,7 +1003,7 @@ class FaceMLDataDB {
|
||||||
final Map<String, int> faceIDToClusterID = {};
|
final Map<String, int> faceIDToClusterID = {};
|
||||||
for (final row in faceIdsResult) {
|
for (final row in faceIdsResult) {
|
||||||
final faceID = row[fcFaceId] as String;
|
final faceID = row[fcFaceId] as String;
|
||||||
if (fileIds.contains(faceID.split('_').first)) {
|
if (fileIds.contains(getFileIdFromFaceId(faceID))) {
|
||||||
maxClusterID += 1;
|
maxClusterID += 1;
|
||||||
faceIDToClusterID[faceID] = maxClusterID;
|
faceIDToClusterID[faceID] = maxClusterID;
|
||||||
}
|
}
|
||||||
|
@ -990,7 +1029,7 @@ class FaceMLDataDB {
|
||||||
final Map<String, int> faceIDToClusterID = {};
|
final Map<String, int> faceIDToClusterID = {};
|
||||||
for (final row in faceIdsResult) {
|
for (final row in faceIdsResult) {
|
||||||
final faceID = row[fcFaceId] as String;
|
final faceID = row[fcFaceId] as String;
|
||||||
if (fileIds.contains(faceID.split('_').first)) {
|
if (fileIds.contains(getFileIdFromFaceId(faceID))) {
|
||||||
maxClusterID += 1;
|
maxClusterID += 1;
|
||||||
faceIDToClusterID[faceID] = maxClusterID;
|
faceIDToClusterID[faceID] = maxClusterID;
|
||||||
}
|
}
|
||||||
|
|
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."),
|
"Please note that this will result in a higher bandwidth and battery usage until all items are indexed."),
|
||||||
"fileTypes": MessageLookupByLibrary.simpleMessage("File types"),
|
"fileTypes": MessageLookupByLibrary.simpleMessage("File types"),
|
||||||
"foundFaces": MessageLookupByLibrary.simpleMessage("Found faces"),
|
"foundFaces": MessageLookupByLibrary.simpleMessage("Found faces"),
|
||||||
|
"indexingIsPaused": MessageLookupByLibrary.simpleMessage(
|
||||||
|
"Indexing is paused, will automatically resume when device is ready"),
|
||||||
"joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"),
|
"joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"),
|
||||||
"locations": MessageLookupByLibrary.simpleMessage("Locations"),
|
"locations": MessageLookupByLibrary.simpleMessage("Locations"),
|
||||||
"longPressAnEmailToVerifyEndToEndEncryption":
|
"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"),
|
"Falscher Wiederherstellungs-Schlüssel"),
|
||||||
"indexedItems":
|
"indexedItems":
|
||||||
MessageLookupByLibrary.simpleMessage("Indizierte Elemente"),
|
MessageLookupByLibrary.simpleMessage("Indizierte Elemente"),
|
||||||
|
"indexingIsPaused": MessageLookupByLibrary.simpleMessage(
|
||||||
|
"Indexing is paused, will automatically resume when device is ready"),
|
||||||
"insecureDevice":
|
"insecureDevice":
|
||||||
MessageLookupByLibrary.simpleMessage("Unsicheres Gerät"),
|
MessageLookupByLibrary.simpleMessage("Unsicheres Gerät"),
|
||||||
"installManually":
|
"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":
|
"incorrectRecoveryKeyTitle":
|
||||||
MessageLookupByLibrary.simpleMessage("Incorrect recovery key"),
|
MessageLookupByLibrary.simpleMessage("Incorrect recovery key"),
|
||||||
"indexedItems": MessageLookupByLibrary.simpleMessage("Indexed items"),
|
"indexedItems": MessageLookupByLibrary.simpleMessage("Indexed items"),
|
||||||
|
"indexingIsPaused": MessageLookupByLibrary.simpleMessage(
|
||||||
|
"Indexing is paused, will automatically resume when device is ready"),
|
||||||
"insecureDevice":
|
"insecureDevice":
|
||||||
MessageLookupByLibrary.simpleMessage("Insecure device"),
|
MessageLookupByLibrary.simpleMessage("Insecure device"),
|
||||||
"installManually":
|
"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"),
|
"La clave de recuperación introducida es incorrecta"),
|
||||||
"incorrectRecoveryKeyTitle": MessageLookupByLibrary.simpleMessage(
|
"incorrectRecoveryKeyTitle": MessageLookupByLibrary.simpleMessage(
|
||||||
"Clave de recuperación incorrecta"),
|
"Clave de recuperación incorrecta"),
|
||||||
|
"indexingIsPaused": MessageLookupByLibrary.simpleMessage(
|
||||||
|
"Indexing is paused, will automatically resume when device is ready"),
|
||||||
"insecureDevice":
|
"insecureDevice":
|
||||||
MessageLookupByLibrary.simpleMessage("Dispositivo inseguro"),
|
MessageLookupByLibrary.simpleMessage("Dispositivo inseguro"),
|
||||||
"installManually":
|
"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"),
|
"La clé de secours que vous avez entrée est incorrecte"),
|
||||||
"incorrectRecoveryKeyTitle":
|
"incorrectRecoveryKeyTitle":
|
||||||
MessageLookupByLibrary.simpleMessage("Clé de secours non valide"),
|
MessageLookupByLibrary.simpleMessage("Clé de secours non valide"),
|
||||||
|
"indexingIsPaused": MessageLookupByLibrary.simpleMessage(
|
||||||
|
"Indexing is paused, will automatically resume when device is ready"),
|
||||||
"insecureDevice":
|
"insecureDevice":
|
||||||
MessageLookupByLibrary.simpleMessage("Appareil non sécurisé"),
|
MessageLookupByLibrary.simpleMessage("Appareil non sécurisé"),
|
||||||
"installManually":
|
"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"),
|
"Il codice che hai inserito non è corretto"),
|
||||||
"incorrectRecoveryKeyTitle":
|
"incorrectRecoveryKeyTitle":
|
||||||
MessageLookupByLibrary.simpleMessage("Chiave di recupero errata"),
|
MessageLookupByLibrary.simpleMessage("Chiave di recupero errata"),
|
||||||
|
"indexingIsPaused": MessageLookupByLibrary.simpleMessage(
|
||||||
|
"Indexing is paused, will automatically resume when device is ready"),
|
||||||
"insecureDevice":
|
"insecureDevice":
|
||||||
MessageLookupByLibrary.simpleMessage("Dispositivo non sicuro"),
|
MessageLookupByLibrary.simpleMessage("Dispositivo non sicuro"),
|
||||||
"installManually":
|
"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."),
|
"Please note that this will result in a higher bandwidth and battery usage until all items are indexed."),
|
||||||
"fileTypes": MessageLookupByLibrary.simpleMessage("File types"),
|
"fileTypes": MessageLookupByLibrary.simpleMessage("File types"),
|
||||||
"foundFaces": MessageLookupByLibrary.simpleMessage("Found faces"),
|
"foundFaces": MessageLookupByLibrary.simpleMessage("Found faces"),
|
||||||
|
"indexingIsPaused": MessageLookupByLibrary.simpleMessage(
|
||||||
|
"Indexing is paused, will automatically resume when device is ready"),
|
||||||
"joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"),
|
"joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"),
|
||||||
"locations": MessageLookupByLibrary.simpleMessage("Locations"),
|
"locations": MessageLookupByLibrary.simpleMessage("Locations"),
|
||||||
"longPressAnEmailToVerifyEndToEndEncryption":
|
"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"),
|
MessageLookupByLibrary.simpleMessage("Onjuiste herstelsleutel"),
|
||||||
"indexedItems":
|
"indexedItems":
|
||||||
MessageLookupByLibrary.simpleMessage("Geïndexeerde bestanden"),
|
MessageLookupByLibrary.simpleMessage("Geïndexeerde bestanden"),
|
||||||
|
"indexingIsPaused": MessageLookupByLibrary.simpleMessage(
|
||||||
|
"Indexing is paused, will automatically resume when device is ready"),
|
||||||
"insecureDevice":
|
"insecureDevice":
|
||||||
MessageLookupByLibrary.simpleMessage("Onveilig apparaat"),
|
MessageLookupByLibrary.simpleMessage("Onveilig apparaat"),
|
||||||
"installManually":
|
"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"),
|
"feedback": MessageLookupByLibrary.simpleMessage("Tilbakemelding"),
|
||||||
"fileTypes": MessageLookupByLibrary.simpleMessage("File types"),
|
"fileTypes": MessageLookupByLibrary.simpleMessage("File types"),
|
||||||
"foundFaces": MessageLookupByLibrary.simpleMessage("Found faces"),
|
"foundFaces": MessageLookupByLibrary.simpleMessage("Found faces"),
|
||||||
|
"indexingIsPaused": MessageLookupByLibrary.simpleMessage(
|
||||||
|
"Indexing is paused, will automatically resume when device is ready"),
|
||||||
"invalidEmailAddress":
|
"invalidEmailAddress":
|
||||||
MessageLookupByLibrary.simpleMessage("Ugyldig e-postadresse"),
|
MessageLookupByLibrary.simpleMessage("Ugyldig e-postadresse"),
|
||||||
"joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"),
|
"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"),
|
MessageLookupByLibrary.simpleMessage("Kod jest nieprawidłowy"),
|
||||||
"incorrectRecoveryKeyTitle": MessageLookupByLibrary.simpleMessage(
|
"incorrectRecoveryKeyTitle": MessageLookupByLibrary.simpleMessage(
|
||||||
"Nieprawidłowy klucz odzyskiwania"),
|
"Nieprawidłowy klucz odzyskiwania"),
|
||||||
|
"indexingIsPaused": MessageLookupByLibrary.simpleMessage(
|
||||||
|
"Indexing is paused, will automatically resume when device is ready"),
|
||||||
"invalidEmailAddress":
|
"invalidEmailAddress":
|
||||||
MessageLookupByLibrary.simpleMessage("Nieprawidłowy adres e-mail"),
|
MessageLookupByLibrary.simpleMessage("Nieprawidłowy adres e-mail"),
|
||||||
"joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"),
|
"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";
|
"${storageAmountInGB} GB cada vez que alguém se inscrever para um plano pago e aplica o seu código";
|
||||||
|
|
||||||
static String m25(freeAmount, storageUnit) =>
|
static String m25(freeAmount, storageUnit) =>
|
||||||
"${freeAmount} ${storageUnit} grátis";
|
"${freeAmount} ${storageUnit} livre";
|
||||||
|
|
||||||
static String m26(endDate) => "Teste gratuito acaba em ${endDate}";
|
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>."),
|
"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":
|
"activeSessions":
|
||||||
MessageLookupByLibrary.simpleMessage("Sessões ativas"),
|
MessageLookupByLibrary.simpleMessage("Sessões ativas"),
|
||||||
|
"addAName": MessageLookupByLibrary.simpleMessage("Adicione um nome"),
|
||||||
"addANewEmail":
|
"addANewEmail":
|
||||||
MessageLookupByLibrary.simpleMessage("Adicionar um novo email"),
|
MessageLookupByLibrary.simpleMessage("Adicionar um novo email"),
|
||||||
"addCollaborator":
|
"addCollaborator":
|
||||||
|
@ -446,7 +447,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||||
"clubByFileName": MessageLookupByLibrary.simpleMessage(
|
"clubByFileName": MessageLookupByLibrary.simpleMessage(
|
||||||
"Agrupar pelo nome de arquivo"),
|
"Agrupar pelo nome de arquivo"),
|
||||||
"clusteringProgress":
|
"clusteringProgress":
|
||||||
MessageLookupByLibrary.simpleMessage("Clustering progress"),
|
MessageLookupByLibrary.simpleMessage("Progresso de agrupamento"),
|
||||||
"codeAppliedPageTitle":
|
"codeAppliedPageTitle":
|
||||||
MessageLookupByLibrary.simpleMessage("Código aplicado"),
|
MessageLookupByLibrary.simpleMessage("Código aplicado"),
|
||||||
"codeCopiedToClipboard": MessageLookupByLibrary.simpleMessage(
|
"codeCopiedToClipboard": MessageLookupByLibrary.simpleMessage(
|
||||||
|
@ -692,6 +693,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||||
"enterPassword": MessageLookupByLibrary.simpleMessage("Digite a senha"),
|
"enterPassword": MessageLookupByLibrary.simpleMessage("Digite a senha"),
|
||||||
"enterPasswordToEncrypt": MessageLookupByLibrary.simpleMessage(
|
"enterPasswordToEncrypt": MessageLookupByLibrary.simpleMessage(
|
||||||
"Insira a senha para criptografar seus dados"),
|
"Insira a senha para criptografar seus dados"),
|
||||||
|
"enterPersonName":
|
||||||
|
MessageLookupByLibrary.simpleMessage("Inserir nome da pessoa"),
|
||||||
"enterReferralCode": MessageLookupByLibrary.simpleMessage(
|
"enterReferralCode": MessageLookupByLibrary.simpleMessage(
|
||||||
"Insira o código de referência"),
|
"Insira o código de referência"),
|
||||||
"enterThe6digitCodeFromnyourAuthenticatorApp":
|
"enterThe6digitCodeFromnyourAuthenticatorApp":
|
||||||
|
@ -717,9 +720,9 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||||
"exportYourData":
|
"exportYourData":
|
||||||
MessageLookupByLibrary.simpleMessage("Exportar seus dados"),
|
MessageLookupByLibrary.simpleMessage("Exportar seus dados"),
|
||||||
"faceRecognition":
|
"faceRecognition":
|
||||||
MessageLookupByLibrary.simpleMessage("Face recognition"),
|
MessageLookupByLibrary.simpleMessage("Reconhecimento facial"),
|
||||||
"faceRecognitionIndexingDescription": MessageLookupByLibrary.simpleMessage(
|
"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"),
|
"faces": MessageLookupByLibrary.simpleMessage("Rostos"),
|
||||||
"failedToApplyCode":
|
"failedToApplyCode":
|
||||||
MessageLookupByLibrary.simpleMessage("Falha ao aplicar o código"),
|
MessageLookupByLibrary.simpleMessage("Falha ao aplicar o código"),
|
||||||
|
@ -761,12 +764,15 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||||
MessageLookupByLibrary.simpleMessage("Arquivos excluídos"),
|
MessageLookupByLibrary.simpleMessage("Arquivos excluídos"),
|
||||||
"filesSavedToGallery":
|
"filesSavedToGallery":
|
||||||
MessageLookupByLibrary.simpleMessage("Arquivos salvos na galeria"),
|
MessageLookupByLibrary.simpleMessage("Arquivos salvos na galeria"),
|
||||||
|
"findPeopleByName": MessageLookupByLibrary.simpleMessage(
|
||||||
|
"Encontre pessoas rapidamente por nome"),
|
||||||
"flip": MessageLookupByLibrary.simpleMessage("Inverter"),
|
"flip": MessageLookupByLibrary.simpleMessage("Inverter"),
|
||||||
"forYourMemories":
|
"forYourMemories":
|
||||||
MessageLookupByLibrary.simpleMessage("para suas memórias"),
|
MessageLookupByLibrary.simpleMessage("para suas memórias"),
|
||||||
"forgotPassword":
|
"forgotPassword":
|
||||||
MessageLookupByLibrary.simpleMessage("Esqueceu sua senha"),
|
MessageLookupByLibrary.simpleMessage("Esqueceu sua senha"),
|
||||||
"foundFaces": MessageLookupByLibrary.simpleMessage("Found faces"),
|
"foundFaces":
|
||||||
|
MessageLookupByLibrary.simpleMessage("Rostos encontrados"),
|
||||||
"freeStorageClaimed": MessageLookupByLibrary.simpleMessage(
|
"freeStorageClaimed": MessageLookupByLibrary.simpleMessage(
|
||||||
"Armazenamento gratuito reivindicado"),
|
"Armazenamento gratuito reivindicado"),
|
||||||
"freeStorageOnReferralSuccess": m24,
|
"freeStorageOnReferralSuccess": m24,
|
||||||
|
@ -830,6 +836,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||||
"incorrectRecoveryKeyTitle": MessageLookupByLibrary.simpleMessage(
|
"incorrectRecoveryKeyTitle": MessageLookupByLibrary.simpleMessage(
|
||||||
"Chave de recuperação incorreta"),
|
"Chave de recuperação incorreta"),
|
||||||
"indexedItems": MessageLookupByLibrary.simpleMessage("Itens indexados"),
|
"indexedItems": MessageLookupByLibrary.simpleMessage("Itens indexados"),
|
||||||
|
"indexingIsPaused": MessageLookupByLibrary.simpleMessage(
|
||||||
|
"Indexing is paused, will automatically resume when device is ready"),
|
||||||
"insecureDevice":
|
"insecureDevice":
|
||||||
MessageLookupByLibrary.simpleMessage("Dispositivo não seguro"),
|
MessageLookupByLibrary.simpleMessage("Dispositivo não seguro"),
|
||||||
"installManually":
|
"installManually":
|
||||||
|
@ -1064,6 +1072,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||||
"pendingItems": MessageLookupByLibrary.simpleMessage("Itens pendentes"),
|
"pendingItems": MessageLookupByLibrary.simpleMessage("Itens pendentes"),
|
||||||
"pendingSync":
|
"pendingSync":
|
||||||
MessageLookupByLibrary.simpleMessage("Sincronização pendente"),
|
MessageLookupByLibrary.simpleMessage("Sincronização pendente"),
|
||||||
|
"people": MessageLookupByLibrary.simpleMessage("Pessoas"),
|
||||||
"peopleUsingYourCode":
|
"peopleUsingYourCode":
|
||||||
MessageLookupByLibrary.simpleMessage("Pessoas que usam seu código"),
|
MessageLookupByLibrary.simpleMessage("Pessoas que usam seu código"),
|
||||||
"permDeleteWarning": MessageLookupByLibrary.simpleMessage(
|
"permDeleteWarning": MessageLookupByLibrary.simpleMessage(
|
||||||
|
@ -1197,6 +1206,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||||
"removeParticipant":
|
"removeParticipant":
|
||||||
MessageLookupByLibrary.simpleMessage("Remover participante"),
|
MessageLookupByLibrary.simpleMessage("Remover participante"),
|
||||||
"removeParticipantBody": m43,
|
"removeParticipantBody": m43,
|
||||||
|
"removePersonLabel":
|
||||||
|
MessageLookupByLibrary.simpleMessage("Remover etiqueta da pessoa"),
|
||||||
"removePublicLink":
|
"removePublicLink":
|
||||||
MessageLookupByLibrary.simpleMessage("Remover link público"),
|
MessageLookupByLibrary.simpleMessage("Remover link público"),
|
||||||
"removeShareItemsWarning": MessageLookupByLibrary.simpleMessage(
|
"removeShareItemsWarning": MessageLookupByLibrary.simpleMessage(
|
||||||
|
@ -1260,7 +1271,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||||
"searchDatesEmptySection": MessageLookupByLibrary.simpleMessage(
|
"searchDatesEmptySection": MessageLookupByLibrary.simpleMessage(
|
||||||
"Pesquisar por data, mês ou ano"),
|
"Pesquisar por data, mês ou ano"),
|
||||||
"searchFaceEmptySection": MessageLookupByLibrary.simpleMessage(
|
"searchFaceEmptySection": MessageLookupByLibrary.simpleMessage(
|
||||||
"Encontre todas as fotos de uma pessoa"),
|
"Pessoas serão exibidas aqui uma vez que a indexação é feita"),
|
||||||
"searchFileTypesAndNamesEmptySection":
|
"searchFileTypesAndNamesEmptySection":
|
||||||
MessageLookupByLibrary.simpleMessage("Tipos de arquivo e nomes"),
|
MessageLookupByLibrary.simpleMessage("Tipos de arquivo e nomes"),
|
||||||
"searchHint1": MessageLookupByLibrary.simpleMessage(
|
"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":
|
"incorrectRecoveryKeyTitle":
|
||||||
MessageLookupByLibrary.simpleMessage("不正确的恢复密钥"),
|
MessageLookupByLibrary.simpleMessage("不正确的恢复密钥"),
|
||||||
"indexedItems": MessageLookupByLibrary.simpleMessage("已索引项目"),
|
"indexedItems": MessageLookupByLibrary.simpleMessage("已索引项目"),
|
||||||
|
"indexingIsPaused": MessageLookupByLibrary.simpleMessage(
|
||||||
|
"Indexing is paused, will automatically resume when device is ready"),
|
||||||
"insecureDevice": MessageLookupByLibrary.simpleMessage("设备不安全"),
|
"insecureDevice": MessageLookupByLibrary.simpleMessage("设备不安全"),
|
||||||
"installManually": MessageLookupByLibrary.simpleMessage("手动安装"),
|
"installManually": MessageLookupByLibrary.simpleMessage("手动安装"),
|
||||||
"invalidEmailAddress":
|
"invalidEmailAddress":
|
||||||
|
|
10
mobile/lib/generated/l10n.dart
generated
10
mobile/lib/generated/l10n.dart
generated
|
@ -8793,6 +8793,16 @@ class S {
|
||||||
args: [],
|
args: [],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `Indexing is paused, will automatically resume when device is ready`
|
||||||
|
String get indexingIsPaused {
|
||||||
|
return Intl.message(
|
||||||
|
'Indexing is paused, will automatically resume when device is ready',
|
||||||
|
name: 'indexingIsPaused',
|
||||||
|
desc: '',
|
||||||
|
args: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AppLocalizationDelegate extends LocalizationsDelegate<S> {
|
class AppLocalizationDelegate extends LocalizationsDelegate<S> {
|
||||||
|
|
|
@ -24,5 +24,6 @@
|
||||||
"faceRecognition": "Face recognition",
|
"faceRecognition": "Face recognition",
|
||||||
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
|
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
|
||||||
"foundFaces": "Found faces",
|
"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",
|
"faceRecognition": "Face recognition",
|
||||||
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
|
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
|
||||||
"foundFaces": "Found faces",
|
"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",
|
"faceRecognition": "Face recognition",
|
||||||
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
|
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
|
||||||
"foundFaces": "Found faces",
|
"foundFaces": "Found faces",
|
||||||
"clusteringProgress": "Clustering progress"
|
"clusteringProgress": "Clustering progress",
|
||||||
|
"indexingIsPaused": "Indexing is paused, will automatically resume when device is ready"
|
||||||
}
|
}
|
|
@ -986,5 +986,6 @@
|
||||||
"faceRecognition": "Face recognition",
|
"faceRecognition": "Face recognition",
|
||||||
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
|
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
|
||||||
"foundFaces": "Found faces",
|
"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",
|
"faceRecognition": "Face recognition",
|
||||||
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
|
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
|
||||||
"foundFaces": "Found faces",
|
"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",
|
"faceRecognition": "Face recognition",
|
||||||
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
|
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
|
||||||
"foundFaces": "Found faces",
|
"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",
|
"faceRecognition": "Face recognition",
|
||||||
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
|
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
|
||||||
"foundFaces": "Found faces",
|
"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",
|
"faceRecognition": "Face recognition",
|
||||||
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
|
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
|
||||||
"foundFaces": "Found faces",
|
"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",
|
"faceRecognition": "Face recognition",
|
||||||
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
|
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
|
||||||
"foundFaces": "Found faces",
|
"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",
|
"faceRecognition": "Face recognition",
|
||||||
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
|
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
|
||||||
"foundFaces": "Found faces",
|
"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",
|
"fileTypesAndNames": "Tipos de arquivo e nomes",
|
||||||
"location": "Local",
|
"location": "Local",
|
||||||
"moments": "Momentos",
|
"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",
|
"searchDatesEmptySection": "Pesquisar por data, mês ou ano",
|
||||||
"searchLocationEmptySection": "Fotos de grupo que estão sendo tiradas em algum raio da foto",
|
"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",
|
"searchPeopleEmptySection": "Convide pessoas e você verá todas as fotos compartilhadas por elas aqui",
|
||||||
|
@ -1042,7 +1042,7 @@
|
||||||
"@storageUsageInfo": {
|
"@storageUsageInfo": {
|
||||||
"description": "Example: 1.2 GB of 2 GB used or 100 GB or 2TB used"
|
"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}",
|
"appVersion": "Versão: {versionValue}",
|
||||||
"verifyIDLabel": "Verificar",
|
"verifyIDLabel": "Verificar",
|
||||||
"fileInfoAddDescHint": "Adicionar descrição...",
|
"fileInfoAddDescHint": "Adicionar descrição...",
|
||||||
|
@ -1171,6 +1171,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"faces": "Rostos",
|
"faces": "Rostos",
|
||||||
|
"people": "Pessoas",
|
||||||
"contents": "Conteúdos",
|
"contents": "Conteúdos",
|
||||||
"addNew": "Adicionar novo",
|
"addNew": "Adicionar novo",
|
||||||
"@addNew": {
|
"@addNew": {
|
||||||
|
@ -1196,14 +1197,14 @@
|
||||||
"verifyPasskey": "Verificar chave de acesso",
|
"verifyPasskey": "Verificar chave de acesso",
|
||||||
"playOnTv": "Reproduzir álbum na TV",
|
"playOnTv": "Reproduzir álbum na TV",
|
||||||
"pair": "Parear",
|
"pair": "Parear",
|
||||||
"autoPair": "Pareamento automático",
|
|
||||||
"pairWithPin": "Parear com PIN",
|
|
||||||
"deviceNotFound": "Dispositivo não encontrado",
|
"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.",
|
"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",
|
"deviceCodeHint": "Insira o código",
|
||||||
"joinDiscord": "Junte-se ao Discord",
|
"joinDiscord": "Junte-se ao Discord",
|
||||||
"locations": "Locais",
|
"locations": "Locais",
|
||||||
"descriptions": "Descrições",
|
"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}}",
|
"addViewers": "{count, plural, zero {Adicionar visualizador} one {Adicionar visualizador} other {Adicionar Visualizadores}}",
|
||||||
"addCollaborators": "{count, plural, zero {Adicionar colaborador} one {Adicionar coloborador} other {Adicionar colaboradores}}",
|
"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.",
|
"longPressAnEmailToVerifyEndToEndEncryption": "Pressione e segure um e-mail para verificar a criptografia de ponta a ponta.",
|
||||||
|
@ -1216,6 +1217,8 @@
|
||||||
"customEndpoint": "Conectado a {endpoint}",
|
"customEndpoint": "Conectado a {endpoint}",
|
||||||
"createCollaborativeLink": "Criar link colaborativo",
|
"createCollaborativeLink": "Criar link colaborativo",
|
||||||
"search": "Pesquisar",
|
"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.",
|
"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.",
|
"manualPairDesc": "Parear com o PIN funciona com qualquer tela que você deseja ver o seu álbum ativado.",
|
||||||
"connectToDevice": "Conectar ao dispositivo",
|
"connectToDevice": "Conectar ao dispositivo",
|
||||||
|
@ -1227,8 +1230,11 @@
|
||||||
"castIPMismatchTitle": "Falha ao transmitir álbum",
|
"castIPMismatchTitle": "Falha ao transmitir álbum",
|
||||||
"castIPMismatchBody": "Certifique-se de estar na mesma rede que a TV.",
|
"castIPMismatchBody": "Certifique-se de estar na mesma rede que a TV.",
|
||||||
"pairingComplete": "Pareamento concluído",
|
"pairingComplete": "Pareamento concluído",
|
||||||
"faceRecognition": "Face recognition",
|
"autoPair": "Pareamento automático",
|
||||||
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
|
"pairWithPin": "Parear com PIN",
|
||||||
"foundFaces": "Found faces",
|
"faceRecognition": "Reconhecimento facial",
|
||||||
"clusteringProgress": "Clustering progress"
|
"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",
|
"faceRecognition": "Face recognition",
|
||||||
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
|
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
|
||||||
"foundFaces": "Found faces",
|
"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/app_lock.dart';
|
||||||
import 'package:photos/ui/tools/lock_screen.dart';
|
import 'package:photos/ui/tools/lock_screen.dart';
|
||||||
import 'package:photos/utils/crypto_util.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/file_uploader.dart';
|
||||||
import 'package:photos/utils/local_settings.dart';
|
import 'package:photos/utils/local_settings.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
@ -180,6 +181,16 @@ void _headlessTaskHandler(HeadlessTask task) {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _init(bool isBackground, {String via = ''}) async {
|
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;
|
_isProcessRunning = true;
|
||||||
_logger.info("Initializing... inBG =$isBackground via: $via");
|
_logger.info("Initializing... inBG =$isBackground via: $via");
|
||||||
final SharedPreferences preferences = await SharedPreferences.getInstance();
|
final SharedPreferences preferences = await SharedPreferences.getInstance();
|
||||||
|
@ -254,6 +265,7 @@ Future<void> _init(bool isBackground, {String via = ''}) async {
|
||||||
preferences,
|
preferences,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
initComplete = true;
|
||||||
_logger.info("Initialization done");
|
_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) {
|
if (fileIDToCreationTime != null) {
|
||||||
faceInfos.sort((a, b) {
|
_sortFaceInfosOnCreationTime(faceInfos);
|
||||||
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!);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort the faceInfos such that the ones with null clusterId are at the end
|
// 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) {
|
if (fileIDToCreationTime != null) {
|
||||||
faceInfos.sort((a, b) {
|
_sortFaceInfosOnCreationTime(faceInfos);
|
||||||
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!);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (faceInfos.isEmpty) {
|
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) {
|
if (fileIDToCreationTime != null) {
|
||||||
faceInfos.sort((a, b) {
|
_sortFaceInfosOnCreationTime(faceInfos);
|
||||||
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!);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the embeddings
|
// Get the embeddings
|
||||||
|
@ -1027,3 +994,20 @@ class FaceClusteringService {
|
||||||
return clusteredFaceIDs;
|
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';
|
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 CouldNotRetrieveAnyFileData implements Exception {}
|
||||||
|
|
||||||
class CouldNotInitializeFaceDetector implements Exception {}
|
class CouldNotInitializeFaceDetector implements Exception {}
|
||||||
|
|
|
@ -310,5 +310,9 @@ class FaceResultBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
int getFileIdFromFaceId(String faceId) {
|
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);
|
||||||
|
}
|
|
@ -9,10 +9,10 @@ import "dart:ui" show Image;
|
||||||
import "package:computer/computer.dart";
|
import "package:computer/computer.dart";
|
||||||
import "package:dart_ui_isolate/dart_ui_isolate.dart";
|
import "package:dart_ui_isolate/dart_ui_isolate.dart";
|
||||||
import "package:flutter/foundation.dart" show debugPrint, kDebugMode;
|
import "package:flutter/foundation.dart" show debugPrint, kDebugMode;
|
||||||
|
import "package:flutter/services.dart";
|
||||||
import "package:logging/logging.dart";
|
import "package:logging/logging.dart";
|
||||||
import "package:onnxruntime/onnxruntime.dart";
|
import "package:onnxruntime/onnxruntime.dart";
|
||||||
import "package:package_info_plus/package_info_plus.dart";
|
import "package:package_info_plus/package_info_plus.dart";
|
||||||
import "package:photos/core/configuration.dart";
|
|
||||||
import "package:photos/core/event_bus.dart";
|
import "package:photos/core/event_bus.dart";
|
||||||
import "package:photos/db/files_db.dart";
|
import "package:photos/db/files_db.dart";
|
||||||
import "package:photos/events/diff_sync_complete_event.dart";
|
import "package:photos/events/diff_sync_complete_event.dart";
|
||||||
|
@ -97,8 +97,9 @@ class FaceMlService {
|
||||||
bool _shouldSyncPeople = false;
|
bool _shouldSyncPeople = false;
|
||||||
bool _isSyncing = false;
|
bool _isSyncing = false;
|
||||||
|
|
||||||
final int _fileDownloadLimit = 10;
|
final int _fileDownloadLimit = 5;
|
||||||
final int _embeddingFetchLimit = 200;
|
final int _embeddingFetchLimit = 200;
|
||||||
|
final int _kForceClusteringFaceCount = 4000;
|
||||||
|
|
||||||
Future<void> init({bool initializeImageMlIsolate = false}) async {
|
Future<void> init({bool initializeImageMlIsolate = false}) async {
|
||||||
if (LocalSettings.instance.isFaceIndexingEnabled == false) {
|
if (LocalSettings.instance.isFaceIndexingEnabled == false) {
|
||||||
|
@ -109,6 +110,7 @@ class FaceMlService {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_logger.info("init called");
|
_logger.info("init called");
|
||||||
|
_logStatus();
|
||||||
await _computer.compute(initOrtEnv);
|
await _computer.compute(initOrtEnv);
|
||||||
try {
|
try {
|
||||||
await FaceDetectionService.instance.init();
|
await FaceDetectionService.instance.init();
|
||||||
|
@ -152,8 +154,8 @@ class FaceMlService {
|
||||||
_logger.info(
|
_logger.info(
|
||||||
"MLController allowed running ML, faces indexing starting",
|
"MLController allowed running ML, faces indexing starting",
|
||||||
);
|
);
|
||||||
unawaited(indexAndClusterAll());
|
|
||||||
}
|
}
|
||||||
|
unawaited(indexAndClusterAll());
|
||||||
} else {
|
} else {
|
||||||
_logger.info(
|
_logger.info(
|
||||||
"MLController stopped running ML, faces indexing will be paused (unless it's fetching embeddings)",
|
"MLController stopped running ML, faces indexing will be paused (unless it's fetching embeddings)",
|
||||||
|
@ -245,6 +247,7 @@ class FaceMlService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The main execution function of the isolate.
|
/// The main execution function of the isolate.
|
||||||
|
@pragma('vm:entry-point')
|
||||||
static void _isolateMain(SendPort mainSendPort) async {
|
static void _isolateMain(SendPort mainSendPort) async {
|
||||||
final receivePort = ReceivePort();
|
final receivePort = ReceivePort();
|
||||||
mainSendPort.send(receivePort.sendPort);
|
mainSendPort.send(receivePort.sendPort);
|
||||||
|
@ -287,10 +290,6 @@ class FaceMlService {
|
||||||
return _functionLock.synchronized(() async {
|
return _functionLock.synchronized(() async {
|
||||||
_resetInactivityTimer();
|
_resetInactivityTimer();
|
||||||
|
|
||||||
if (_shouldPauseIndexingAndClustering == false) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final completer = Completer<dynamic>();
|
final completer = Completer<dynamic>();
|
||||||
final answerPort = ReceivePort();
|
final answerPort = ReceivePort();
|
||||||
|
|
||||||
|
@ -360,16 +359,17 @@ class FaceMlService {
|
||||||
if (_cannotRunMLFunction()) return;
|
if (_cannotRunMLFunction()) return;
|
||||||
|
|
||||||
await sync(forceSync: _shouldSyncPeople);
|
await sync(forceSync: _shouldSyncPeople);
|
||||||
await indexAllImages();
|
|
||||||
final indexingCompleteRatio = await _getIndexedDoneRatio();
|
final int unclusteredFacesCount =
|
||||||
if (indexingCompleteRatio < 0.95) {
|
await FaceMLDataDB.instance.getUnclusteredFaceCount();
|
||||||
|
if (unclusteredFacesCount > _kForceClusteringFaceCount) {
|
||||||
_logger.info(
|
_logger.info(
|
||||||
"Indexing is not far enough to start clustering, skipping clustering. Indexing is at $indexingCompleteRatio",
|
"There are $unclusteredFacesCount unclustered faces, doing clustering first",
|
||||||
);
|
);
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
await clusterAllImages();
|
await clusterAllImages();
|
||||||
}
|
}
|
||||||
|
await indexAllImages();
|
||||||
|
await clusterAllImages();
|
||||||
}
|
}
|
||||||
|
|
||||||
void pauseIndexingAndClustering() {
|
void pauseIndexingAndClustering() {
|
||||||
|
@ -447,7 +447,8 @@ class FaceMlService {
|
||||||
|
|
||||||
if (LocalSettings.instance.remoteFetchEnabled) {
|
if (LocalSettings.instance.remoteFetchEnabled) {
|
||||||
try {
|
try {
|
||||||
final List<int> fileIds = [];
|
final Set<int> fileIds =
|
||||||
|
{}; // if there are duplicates here server returns 400
|
||||||
// Try to find embeddings on the remote server
|
// Try to find embeddings on the remote server
|
||||||
for (final f in chunk) {
|
for (final f in chunk) {
|
||||||
fileIds.add(f.uploadedFileID!);
|
fileIds.add(f.uploadedFileID!);
|
||||||
|
@ -512,12 +513,19 @@ class FaceMlService {
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
if (!await canUseHighBandwidth()) {
|
_logger.warning(
|
||||||
continue;
|
'Not fetching embeddings because user manually disabled it in debug options',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
final smallerChunks = chunk.chunks(_fileDownloadLimit);
|
final smallerChunks = chunk.chunks(_fileDownloadLimit);
|
||||||
for (final smallestChunk in smallerChunks) {
|
for (final smallestChunk in smallerChunks) {
|
||||||
|
if (!await canUseHighBandwidth()) {
|
||||||
|
_logger.info(
|
||||||
|
'stopping indexing because user is not connected to wifi',
|
||||||
|
);
|
||||||
|
break outerLoop;
|
||||||
|
}
|
||||||
for (final enteFile in smallestChunk) {
|
for (final enteFile in smallestChunk) {
|
||||||
if (_shouldPauseIndexingAndClustering) {
|
if (_shouldPauseIndexingAndClustering) {
|
||||||
_logger.info("indexAllImages() was paused, stopping");
|
_logger.info("indexAllImages() was paused, stopping");
|
||||||
|
@ -543,8 +551,9 @@ class FaceMlService {
|
||||||
|
|
||||||
stopwatch.stop();
|
stopwatch.stop();
|
||||||
_logger.info(
|
_logger.info(
|
||||||
"`indexAllImages()` finished. Fetched $fetchedCount and analyzed $fileAnalyzedCount images, in ${stopwatch.elapsed.inSeconds} seconds (avg of ${stopwatch.elapsed.inSeconds / fileAnalyzedCount} seconds per image, skipped $fileSkippedCount images. MLController status: $_mlControllerStatus)",
|
"`indexAllImages()` finished. Fetched $fetchedCount and analyzed $fileAnalyzedCount images, in ${stopwatch.elapsed.inSeconds} seconds (avg of ${stopwatch.elapsed.inSeconds / fileAnalyzedCount} seconds per image, skipped $fileSkippedCount images)",
|
||||||
);
|
);
|
||||||
|
_logStatus();
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
_logger.severe("indexAllImages failed", e, s);
|
_logger.severe("indexAllImages failed", e, s);
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -584,8 +593,8 @@ class FaceMlService {
|
||||||
allFaceInfoForClustering.add(faceInfo);
|
allFaceInfoForClustering.add(faceInfo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// sort the embeddings based on file creation time, oldest first
|
// sort the embeddings based on file creation time, newest first
|
||||||
allFaceInfoForClustering.sort((a, b) {
|
allFaceInfoForClustering.sort((b, a) {
|
||||||
return fileIDToCreationTime[a.fileID]!
|
return fileIDToCreationTime[a.fileID]!
|
||||||
.compareTo(fileIDToCreationTime[b.fileID]!);
|
.compareTo(fileIDToCreationTime[b.fileID]!);
|
||||||
});
|
});
|
||||||
|
@ -758,6 +767,9 @@ class FaceMlService {
|
||||||
// disposeImageIsolateAfterUse: false,
|
// disposeImageIsolateAfterUse: false,
|
||||||
);
|
);
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
|
_logger.severe(
|
||||||
|
"Failed to analyze image with uploadedFileID: ${enteFile.uploadedFileID}",
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
final List<Face> faces = [];
|
final List<Face> faces = [];
|
||||||
|
@ -834,13 +846,22 @@ class FaceMlService {
|
||||||
}
|
}
|
||||||
await FaceMLDataDB.instance.bulkInsertFaces(faces);
|
await FaceMLDataDB.instance.bulkInsertFaces(faces);
|
||||||
return true;
|
return true;
|
||||||
|
} on ThumbnailRetrievalException catch (e, s) {
|
||||||
|
_logger.severe(
|
||||||
|
'ThumbnailRetrievalException while processing image with ID ${enteFile.uploadedFileID}, storing empty face so indexing does not get stuck',
|
||||||
|
e,
|
||||||
|
s,
|
||||||
|
);
|
||||||
|
await FaceMLDataDB.instance
|
||||||
|
.bulkInsertFaces([Face.empty(enteFile.uploadedFileID!, error: true)]);
|
||||||
|
return true;
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
_logger.severe(
|
_logger.severe(
|
||||||
"Failed to analyze using FaceML for image with ID: ${enteFile.uploadedFileID}",
|
"Failed to analyze using FaceML for image with ID: ${enteFile.uploadedFileID}",
|
||||||
e,
|
e,
|
||||||
s,
|
s,
|
||||||
);
|
);
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -877,6 +898,7 @@ class FaceMlService {
|
||||||
),
|
),
|
||||||
) as String?;
|
) as String?;
|
||||||
if (resultJsonString == null) {
|
if (resultJsonString == null) {
|
||||||
|
_logger.severe('Analyzing image in isolate is giving back null');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
result = FaceMlResult.fromJsonString(resultJsonString);
|
result = FaceMlResult.fromJsonString(resultJsonString);
|
||||||
|
@ -993,7 +1015,12 @@ class FaceMlService {
|
||||||
final stopwatch = Stopwatch()..start();
|
final stopwatch = Stopwatch()..start();
|
||||||
File? file;
|
File? file;
|
||||||
if (enteFile.fileType == FileType.video) {
|
if (enteFile.fileType == FileType.video) {
|
||||||
|
try {
|
||||||
file = await getThumbnailForUploadedFile(enteFile);
|
file = await getThumbnailForUploadedFile(enteFile);
|
||||||
|
} on PlatformException catch (e, s) {
|
||||||
|
_logger.severe("Could not get thumbnail for $enteFile due to PlatformException", e, s);
|
||||||
|
throw ThumbnailRetrievalException(e.toString(), s);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
file = await getFile(enteFile, isOrigin: true);
|
file = await getFile(enteFile, isOrigin: true);
|
||||||
// TODO: This is returning null for Pragadees for all files, so something is wrong here!
|
// TODO: This is returning null for Pragadees for all files, so something is wrong here!
|
||||||
|
@ -1161,24 +1188,6 @@ class FaceMlService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<double> _getIndexedDoneRatio() async {
|
|
||||||
final w = (kDebugMode ? EnteWatch('_getIndexedDoneRatio') : null)?..start();
|
|
||||||
|
|
||||||
final int alreadyIndexedCount = await FaceMLDataDB.instance
|
|
||||||
.getIndexedFileCount(minimumMlVersion: faceMlVersion);
|
|
||||||
final int totalIndexableCount = (await getIndexableFileIDs()).length;
|
|
||||||
final ratio = alreadyIndexedCount / totalIndexableCount;
|
|
||||||
|
|
||||||
w?.log('getIndexedDoneRatio');
|
|
||||||
|
|
||||||
return ratio;
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<List<int>> getIndexableFileIDs() async {
|
|
||||||
return FilesDB.instance
|
|
||||||
.getOwnedFileIDs(Configuration.instance.getUserID()!);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _skipAnalysisEnteFile(EnteFile enteFile, Map<int, int> indexedFileIds) {
|
bool _skipAnalysisEnteFile(EnteFile enteFile, Map<int, int> indexedFileIds) {
|
||||||
if (_isIndexingOrClusteringRunning == false ||
|
if (_isIndexingOrClusteringRunning == false ||
|
||||||
_mlControllerStatus == false) {
|
_mlControllerStatus == false) {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import "dart:async";
|
import "dart:async";
|
||||||
import "dart:convert";
|
import "dart:convert";
|
||||||
|
|
||||||
|
import "package:computer/computer.dart";
|
||||||
import "package:logging/logging.dart";
|
import "package:logging/logging.dart";
|
||||||
import "package:photos/core/network/network.dart";
|
import "package:photos/core/network/network.dart";
|
||||||
import "package:photos/db/files_db.dart";
|
import "package:photos/db/files_db.dart";
|
||||||
|
@ -16,6 +17,8 @@ import "package:shared_preferences/shared_preferences.dart";
|
||||||
class RemoteFileMLService {
|
class RemoteFileMLService {
|
||||||
RemoteFileMLService._privateConstructor();
|
RemoteFileMLService._privateConstructor();
|
||||||
|
|
||||||
|
static final Computer _computer = Computer.shared();
|
||||||
|
|
||||||
static final RemoteFileMLService instance =
|
static final RemoteFileMLService instance =
|
||||||
RemoteFileMLService._privateConstructor();
|
RemoteFileMLService._privateConstructor();
|
||||||
|
|
||||||
|
@ -52,13 +55,13 @@ class RemoteFileMLService {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<FilesMLDataResponse> getFilessEmbedding(
|
Future<FilesMLDataResponse> getFilessEmbedding(
|
||||||
List<int> fileIds,
|
Set<int> fileIds,
|
||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
final res = await _dio.post(
|
final res = await _dio.post(
|
||||||
"/embeddings/files",
|
"/embeddings/files",
|
||||||
data: {
|
data: {
|
||||||
"fileIDs": fileIds,
|
"fileIDs": fileIds.toList(),
|
||||||
"model": 'file-ml-clip-face',
|
"model": 'file-ml-clip-face',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -107,15 +110,17 @@ class RemoteFileMLService {
|
||||||
final input = EmbeddingsDecoderInput(embedding, fileKey);
|
final input = EmbeddingsDecoderInput(embedding, fileKey);
|
||||||
inputs.add(input);
|
inputs.add(input);
|
||||||
}
|
}
|
||||||
// todo: use compute or isolate
|
return _computer.compute<Map<String, dynamic>, Map<int, FileMl>>(
|
||||||
return decryptFileMLComputer(
|
_decryptFileMLComputer,
|
||||||
{
|
param: {
|
||||||
"inputs": inputs,
|
"inputs": inputs,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<int, FileMl>> decryptFileMLComputer(
|
}
|
||||||
|
|
||||||
|
Future<Map<int, FileMl>> _decryptFileMLComputer(
|
||||||
Map<String, dynamic> args,
|
Map<String, dynamic> args,
|
||||||
) async {
|
) async {
|
||||||
final result = <int, FileMl>{};
|
final result = <int, FileMl>{};
|
||||||
|
@ -134,5 +139,4 @@ class RemoteFileMLService {
|
||||||
result[input.embedding.fileID] = decodedEmbedding;
|
result[input.embedding.fileID] = decodedEmbedding;
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
|
|
@ -28,6 +28,8 @@ class MachineLearningController {
|
||||||
bool _canRunML = false;
|
bool _canRunML = false;
|
||||||
late Timer _userInteractionTimer;
|
late Timer _userInteractionTimer;
|
||||||
|
|
||||||
|
bool get isDeviceHealthy => _isDeviceHealthy;
|
||||||
|
|
||||||
void init() {
|
void init() {
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
_startInteractionTimer();
|
_startInteractionTimer();
|
||||||
|
|
|
@ -23,6 +23,7 @@ import 'package:photos/services/machine_learning/semantic_search/frameworks/onnx
|
||||||
import "package:photos/utils/debouncer.dart";
|
import "package:photos/utils/debouncer.dart";
|
||||||
import "package:photos/utils/device_info.dart";
|
import "package:photos/utils/device_info.dart";
|
||||||
import "package:photos/utils/local_settings.dart";
|
import "package:photos/utils/local_settings.dart";
|
||||||
|
import "package:photos/utils/ml_util.dart";
|
||||||
import "package:photos/utils/thumbnail_util.dart";
|
import "package:photos/utils/thumbnail_util.dart";
|
||||||
|
|
||||||
class SemanticSearchService {
|
class SemanticSearchService {
|
||||||
|
@ -160,8 +161,7 @@ class SemanticSearchService {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<IndexStatus> getIndexStatus() async {
|
Future<IndexStatus> getIndexStatus() async {
|
||||||
final indexableFileIDs = await FilesDB.instance
|
final indexableFileIDs = await getIndexableFileIDs();
|
||||||
.getOwnedFileIDs(Configuration.instance.getUserID()!);
|
|
||||||
return IndexStatus(
|
return IndexStatus(
|
||||||
min(_cachedEmbeddings.length, indexableFileIDs.length),
|
min(_cachedEmbeddings.length, indexableFileIDs.length),
|
||||||
(await _getFileIDsToBeIndexed()).length,
|
(await _getFileIDsToBeIndexed()).length,
|
||||||
|
@ -222,8 +222,7 @@ class SemanticSearchService {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<int>> _getFileIDsToBeIndexed() async {
|
Future<List<int>> _getFileIDsToBeIndexed() async {
|
||||||
final uploadedFileIDs = await FilesDB.instance
|
final uploadedFileIDs = await getIndexableFileIDs();
|
||||||
.getOwnedFileIDs(Configuration.instance.getUserID()!);
|
|
||||||
final embeddedFileIDs =
|
final embeddedFileIDs =
|
||||||
await EmbeddingsDB.instance.getFileIDs(_currentModel);
|
await EmbeddingsDB.instance.getFileIDs(_currentModel);
|
||||||
|
|
||||||
|
|
|
@ -754,15 +754,6 @@ class SearchService {
|
||||||
|
|
||||||
Future<List<GenericSearchResult>> getAllFace(int? limit) async {
|
Future<List<GenericSearchResult>> getAllFace(int? limit) async {
|
||||||
try {
|
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");
|
debugPrint("getting faces");
|
||||||
final Map<int, Set<int>> fileIdToClusterID =
|
final Map<int, Set<int>> fileIdToClusterID =
|
||||||
await FaceMLDataDB.instance.getFileIdToClusterIds();
|
await FaceMLDataDB.instance.getFileIdToClusterIds();
|
||||||
|
|
|
@ -177,7 +177,7 @@ class _FaceDebugSectionWidgetState extends State<FaceDebugSectionWidget> {
|
||||||
sectionOptionSpacing,
|
sectionOptionSpacing,
|
||||||
MenuItemWidget(
|
MenuItemWidget(
|
||||||
captionedTextWidget: FutureBuilder<double>(
|
captionedTextWidget: FutureBuilder<double>(
|
||||||
future: FaceMLDataDB.instance.getClusteredToTotalFacesRatio(),
|
future: FaceMLDataDB.instance.getClusteredToIndexableFilesRatio(),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.hasData) {
|
if (snapshot.hasData) {
|
||||||
return CaptionedTextWidget(
|
return CaptionedTextWidget(
|
||||||
|
|
|
@ -11,6 +11,7 @@ import "package:photos/generated/l10n.dart";
|
||||||
import "package:photos/models/ml/ml_versions.dart";
|
import "package:photos/models/ml/ml_versions.dart";
|
||||||
import "package:photos/service_locator.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/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/frameworks/ml_framework.dart';
|
||||||
import 'package:photos/services/machine_learning/semantic_search/semantic_search_service.dart';
|
import 'package:photos/services/machine_learning/semantic_search/semantic_search_service.dart';
|
||||||
import "package:photos/services/remote_assets_service.dart";
|
import "package:photos/services/remote_assets_service.dart";
|
||||||
|
@ -26,6 +27,8 @@ import "package:photos/ui/components/title_bar_widget.dart";
|
||||||
import "package:photos/ui/components/toggle_switch_widget.dart";
|
import "package:photos/ui/components/toggle_switch_widget.dart";
|
||||||
import "package:photos/utils/data_util.dart";
|
import "package:photos/utils/data_util.dart";
|
||||||
import "package:photos/utils/local_settings.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");
|
final _logger = Logger("MachineLearningSettingsPage");
|
||||||
|
|
||||||
|
@ -40,6 +43,7 @@ class MachineLearningSettingsPage extends StatefulWidget {
|
||||||
class _MachineLearningSettingsPageState
|
class _MachineLearningSettingsPageState
|
||||||
extends State<MachineLearningSettingsPage> {
|
extends State<MachineLearningSettingsPage> {
|
||||||
late InitializationState _state;
|
late InitializationState _state;
|
||||||
|
final EnteWakeLock _wakeLock = EnteWakeLock();
|
||||||
|
|
||||||
late StreamSubscription<MLFrameworkInitializationUpdateEvent>
|
late StreamSubscription<MLFrameworkInitializationUpdateEvent>
|
||||||
_eventSubscription;
|
_eventSubscription;
|
||||||
|
@ -53,6 +57,7 @@ class _MachineLearningSettingsPageState
|
||||||
setState(() {});
|
setState(() {});
|
||||||
});
|
});
|
||||||
_fetchState();
|
_fetchState();
|
||||||
|
_wakeLock.enable();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _fetchState() {
|
void _fetchState() {
|
||||||
|
@ -63,6 +68,7 @@ class _MachineLearningSettingsPageState
|
||||||
void dispose() {
|
void dispose() {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
_eventSubscription.cancel();
|
_eventSubscription.cancel();
|
||||||
|
_wakeLock.disable();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -438,19 +444,24 @@ class FaceRecognitionStatusWidgetState
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<(int, int, int, double)> getIndexStatus() async {
|
Future<(int, int, double, bool)> getIndexStatus() async {
|
||||||
try {
|
try {
|
||||||
final indexedFiles = await FaceMLDataDB.instance
|
final indexedFiles = await FaceMLDataDB.instance
|
||||||
.getIndexedFileCount(minimumMlVersion: faceMlVersion);
|
.getIndexedFileCount(minimumMlVersion: faceMlVersion);
|
||||||
final indexableFiles = (await FaceMlService.getIndexableFileIDs()).length;
|
final indexableFiles = (await getIndexableFileIDs()).length;
|
||||||
final showIndexedFiles = min(indexedFiles, indexableFiles);
|
final showIndexedFiles = min(indexedFiles, indexableFiles);
|
||||||
final pendingFiles = max(indexableFiles - indexedFiles, 0);
|
final pendingFiles = max(indexableFiles - indexedFiles, 0);
|
||||||
final foundFaces = await FaceMLDataDB.instance.getTotalFaceCount();
|
final clusteringDoneRatio =
|
||||||
final clusteredFaces =
|
await FaceMLDataDB.instance.getClusteredToIndexableFilesRatio();
|
||||||
await FaceMLDataDB.instance.getClusteredFaceCount();
|
final bool deviceIsHealthy =
|
||||||
final clusteringDoneRatio = clusteredFaces / foundFaces;
|
MachineLearningController.instance.isDeviceHealthy;
|
||||||
|
|
||||||
return (showIndexedFiles, pendingFiles, foundFaces, clusteringDoneRatio);
|
return (
|
||||||
|
showIndexedFiles,
|
||||||
|
pendingFiles,
|
||||||
|
clusteringDoneRatio,
|
||||||
|
deviceIsHealthy
|
||||||
|
);
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
_logger.severe('Error getting face recognition status', e, s);
|
_logger.severe('Error getting face recognition status', e, s);
|
||||||
rethrow;
|
rethrow;
|
||||||
|
@ -479,10 +490,17 @@ class FaceRecognitionStatusWidgetState
|
||||||
if (snapshot.hasData) {
|
if (snapshot.hasData) {
|
||||||
final int indexedFiles = snapshot.data!.$1;
|
final int indexedFiles = snapshot.data!.$1;
|
||||||
final int pendingFiles = snapshot.data!.$2;
|
final int pendingFiles = snapshot.data!.$2;
|
||||||
final int foundFaces = snapshot.data!.$3;
|
final double clusteringDoneRatio = snapshot.data!.$3;
|
||||||
final double clusteringDoneRatio = snapshot.data!.$4;
|
|
||||||
final double clusteringPercentage =
|
final double clusteringPercentage =
|
||||||
(clusteringDoneRatio * 100).clamp(0, 100);
|
(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(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
|
@ -512,19 +530,6 @@ class FaceRecognitionStatusWidgetState
|
||||||
isGestureDetectorDisabled: true,
|
isGestureDetectorDisabled: true,
|
||||||
key: ValueKey("pending_items_" + pendingFiles.toString()),
|
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(
|
MenuItemWidget(
|
||||||
captionedTextWidget: CaptionedTextWidget(
|
captionedTextWidget: CaptionedTextWidget(
|
||||||
title: S.of(context).clusteringProgress,
|
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/dialog_util.dart";
|
||||||
import 'package:photos/utils/file_util.dart';
|
import 'package:photos/utils/file_util.dart';
|
||||||
import 'package:photos/utils/toast_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:video_player/video_player.dart';
|
||||||
import 'package:visibility_detector/visibility_detector.dart';
|
import 'package:visibility_detector/visibility_detector.dart';
|
||||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
|
||||||
|
|
||||||
class VideoWidget extends StatefulWidget {
|
class VideoWidget extends StatefulWidget {
|
||||||
final EnteFile file;
|
final EnteFile file;
|
||||||
|
@ -45,7 +45,7 @@ class _VideoWidgetState extends State<VideoWidget> {
|
||||||
ChewieController? _chewieController;
|
ChewieController? _chewieController;
|
||||||
final _progressNotifier = ValueNotifier<double?>(null);
|
final _progressNotifier = ValueNotifier<double?>(null);
|
||||||
bool _isPlaying = false;
|
bool _isPlaying = false;
|
||||||
bool _wakeLockEnabledHere = false;
|
final EnteWakeLock _wakeLock = EnteWakeLock();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
@ -126,13 +126,7 @@ class _VideoWidgetState extends State<VideoWidget> {
|
||||||
_chewieController?.dispose();
|
_chewieController?.dispose();
|
||||||
_progressNotifier.dispose();
|
_progressNotifier.dispose();
|
||||||
|
|
||||||
if (_wakeLockEnabledHere) {
|
_wakeLock.dispose();
|
||||||
unawaited(
|
|
||||||
WakelockPlus.enabled.then((isEnabled) {
|
|
||||||
isEnabled ? WakelockPlus.disable() : null;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -257,17 +251,10 @@ class _VideoWidgetState extends State<VideoWidget> {
|
||||||
|
|
||||||
Future<void> _keepScreenAliveOnPlaying(bool isPlaying) async {
|
Future<void> _keepScreenAliveOnPlaying(bool isPlaying) async {
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
return WakelockPlus.enabled.then((value) {
|
_wakeLock.enable();
|
||||||
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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if (_wakeLockEnabledHere && !isPlaying) {
|
if (!isPlaying) {
|
||||||
return WakelockPlus.disable();
|
_wakeLock.disable();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import "package:flutter/material.dart";
|
||||||
import "package:flutter_animate/flutter_animate.dart";
|
import "package:flutter_animate/flutter_animate.dart";
|
||||||
import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
|
import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
|
||||||
import "package:photos/core/configuration.dart";
|
import "package:photos/core/configuration.dart";
|
||||||
|
import "package:photos/core/constants.dart";
|
||||||
import "package:photos/db/files_db.dart";
|
import "package:photos/db/files_db.dart";
|
||||||
import "package:photos/generated/l10n.dart";
|
import "package:photos/generated/l10n.dart";
|
||||||
import "package:photos/l10n/l10n.dart";
|
import "package:photos/l10n/l10n.dart";
|
||||||
|
@ -167,7 +168,14 @@ class AddPhotosPhotoWidget extends StatelessWidget {
|
||||||
|
|
||||||
Future<void> _onPickFromDeviceClicked(BuildContext context) async {
|
Future<void> _onPickFromDeviceClicked(BuildContext context) async {
|
||||||
try {
|
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) {
|
if (result != null && result.isNotEmpty) {
|
||||||
final ca = CollectionActions(
|
final ca = CollectionActions(
|
||||||
CollectionsService.instance,
|
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 {
|
class DelayedGallery extends StatefulWidget {
|
||||||
|
|
|
@ -203,7 +203,7 @@ class SearchWidgetState extends State<SearchWidget> {
|
||||||
String query,
|
String query,
|
||||||
) {
|
) {
|
||||||
int resultCount = 0;
|
int resultCount = 0;
|
||||||
final maxResultCount = _isYearValid(query) ? 13 : 12;
|
final maxResultCount = _isYearValid(query) ? 12 : 11;
|
||||||
final streamController = StreamController<List<SearchResult>>();
|
final streamController = StreamController<List<SearchResult>>();
|
||||||
|
|
||||||
if (query.isEmpty) {
|
if (query.isEmpty) {
|
||||||
|
@ -260,10 +260,11 @@ class SearchWidgetState extends State<SearchWidget> {
|
||||||
onResultsReceived(locationResult);
|
onResultsReceived(locationResult);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
_searchService.getAllFace(null).then(
|
_searchService.getAllFace(null).then(
|
||||||
(locationResult) {
|
(faceResult) {
|
||||||
final List<GenericSearchResult> filteredResults = [];
|
final List<GenericSearchResult> filteredResults = [];
|
||||||
for (final result in locationResult) {
|
for (final result in faceResult) {
|
||||||
if (result.name().toLowerCase().contains(query.toLowerCase())) {
|
if (result.name().toLowerCase().contains(query.toLowerCase())) {
|
||||||
filteredResults.add(result);
|
filteredResults.add(result);
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:photos/core/configuration.dart';
|
import 'package:photos/core/configuration.dart';
|
||||||
import 'package:photos/core/error-reporting/super_logging.dart';
|
import 'package:photos/core/error-reporting/super_logging.dart';
|
||||||
import "package:photos/generated/l10n.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/buttons/button_widget.dart';
|
||||||
import 'package:photos/ui/components/dialog_widget.dart';
|
import 'package:photos/ui/components/dialog_widget.dart';
|
||||||
import 'package:photos/ui/components/models/button_type.dart';
|
import 'package:photos/ui/components/models/button_type.dart';
|
||||||
|
@ -122,9 +123,28 @@ Future<void> _sendLogs(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> getZippedLogsFile(BuildContext context) async {
|
Future<void> sendLogsForInit(
|
||||||
final dialog = createProgressDialog(context, S.of(context).preparingLogs);
|
String toEmail,
|
||||||
await dialog.show();
|
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 logsPath = (await getApplicationSupportDirectory()).path;
|
||||||
final logsDirectory = Directory(logsPath + "/logs");
|
final logsDirectory = Directory(logsPath + "/logs");
|
||||||
final tempPath = (await getTemporaryDirectory()).path;
|
final tempPath = (await getTemporaryDirectory()).path;
|
||||||
|
@ -134,7 +154,9 @@ Future<String> getZippedLogsFile(BuildContext context) async {
|
||||||
encoder.create(zipFilePath);
|
encoder.create(zipFilePath);
|
||||||
await encoder.addDirectory(logsDirectory);
|
await encoder.addDirectory(logsDirectory);
|
||||||
encoder.close();
|
encoder.close();
|
||||||
await dialog.hide();
|
if (context != null) {
|
||||||
|
await dialog.hide();
|
||||||
|
}
|
||||||
return zipFilePath;
|
return zipFilePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -45,10 +45,10 @@ packages:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: animated_list_plus
|
name: animated_list_plus
|
||||||
sha256: fe66f9c300d715254727fbdf050487844d17b013fec344fa28081d29bddbdf1a
|
sha256: fb3d7f1fbaf5af84907f3c739236bacda8bf32cbe1f118dd51510752883ff50c
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.4.5"
|
version: "0.5.2"
|
||||||
animated_stack_widget:
|
animated_stack_widget:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -971,10 +971,10 @@ packages:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: home_widget
|
name: home_widget
|
||||||
sha256: "29565bfee4b32eaf9e7e8b998d504618b779a74b2b1ac62dd4dac7468e66f1a3"
|
sha256: "2a0fdd6267ff975bd07bedf74686bd5577200f504f5de36527ac1b56bdbe68e3"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.5.0"
|
version: "0.6.0"
|
||||||
html:
|
html:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -1152,26 +1152,26 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker
|
name: leak_tracker
|
||||||
sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa"
|
sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "10.0.0"
|
version: "10.0.4"
|
||||||
leak_tracker_flutter_testing:
|
leak_tracker_flutter_testing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker_flutter_testing
|
name: leak_tracker_flutter_testing
|
||||||
sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0
|
sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.1"
|
version: "3.0.3"
|
||||||
leak_tracker_testing:
|
leak_tracker_testing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker_testing
|
name: leak_tracker_testing
|
||||||
sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47
|
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.1"
|
version: "3.0.1"
|
||||||
like_button:
|
like_button:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -1368,10 +1368,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04
|
sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.11.0"
|
version: "1.12.0"
|
||||||
mgrs_dart:
|
mgrs_dart:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -2144,10 +2144,10 @@ packages:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: styled_text
|
name: styled_text
|
||||||
sha256: f72928d1ebe8cb149e3b34a689cb1ddca696b808187cf40ac3a0bd183dff379c
|
sha256: fd624172cf629751b4f171dd0ecf9acf02a06df3f8a81bb56c0caa4f1df706c3
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.0.0"
|
version: "8.1.0"
|
||||||
sync_http:
|
sync_http:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -2160,18 +2160,18 @@ packages:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: syncfusion_flutter_core
|
name: syncfusion_flutter_core
|
||||||
sha256: "9be1bb9bbdb42823439a18da71484f1964c14dbe1c255ab1b931932b12fa96e8"
|
sha256: "63108a33f9b0d89f7b6b56cce908b8e519fe433dbbe0efcf41ad3e8bb2081bd9"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "19.4.56"
|
version: "25.2.5"
|
||||||
syncfusion_flutter_sliders:
|
syncfusion_flutter_sliders:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: syncfusion_flutter_sliders
|
name: syncfusion_flutter_sliders
|
||||||
sha256: "1f6a63ccab4180b544074b9264a20f01ee80b553de154192fe1d7b434089d3c2"
|
sha256: f27310bedc0e96e84054f0a70ac593d1a3c38397c158c5226ba86027ad77b2c1
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "19.4.56"
|
version: "25.2.5"
|
||||||
synchronized:
|
synchronized:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -2192,26 +2192,26 @@ packages:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: test
|
name: test
|
||||||
sha256: a1f7595805820fcc05e5c52e3a231aedd0b72972cb333e8c738a8b1239448b6f
|
sha256: "7ee446762c2c50b3bd4ea96fe13ffac69919352bd3b4b17bac3f3465edc58073"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.24.9"
|
version: "1.25.2"
|
||||||
test_api:
|
test_api:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b"
|
sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.1"
|
version: "0.7.0"
|
||||||
test_core:
|
test_core:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_core
|
name: test_core
|
||||||
sha256: a757b14fc47507060a162cc2530d9a4a2f92f5100a952c7443b5cad5ef5b106a
|
sha256: "2bc4b4ecddd75309300d8096f781c0e3280ca1ef85beda558d33fcbedc2eead4"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.5.9"
|
version: "0.6.0"
|
||||||
timezone:
|
timezone:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -2441,10 +2441,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: vm_service
|
name: vm_service
|
||||||
sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957
|
sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "13.0.0"
|
version: "14.2.1"
|
||||||
volume_controller:
|
volume_controller:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -2591,4 +2591,4 @@ packages:
|
||||||
version: "3.1.2"
|
version: "3.1.2"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.3.0 <4.0.0"
|
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
|
# Read more about iOS versioning at
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
|
|
||||||
version: 0.8.106+630
|
version: 0.8.110+634
|
||||||
publish_to: none
|
publish_to: none
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
|
@ -21,7 +21,7 @@ environment:
|
||||||
dependencies:
|
dependencies:
|
||||||
adaptive_theme: ^3.1.0
|
adaptive_theme: ^3.1.0
|
||||||
animate_do: ^2.0.0
|
animate_do: ^2.0.0
|
||||||
animated_list_plus: ^0.4.5
|
animated_list_plus: ^0.5.2
|
||||||
archive: ^3.1.2
|
archive: ^3.1.2
|
||||||
background_fetch: ^1.2.1
|
background_fetch: ^1.2.1
|
||||||
battery_info: ^1.1.1
|
battery_info: ^1.1.1
|
||||||
|
@ -93,13 +93,13 @@ dependencies:
|
||||||
fluttertoast: ^8.0.6
|
fluttertoast: ^8.0.6
|
||||||
freezed_annotation: ^2.4.1
|
freezed_annotation: ^2.4.1
|
||||||
google_nav_bar: ^5.0.5
|
google_nav_bar: ^5.0.5
|
||||||
home_widget: ^0.5.0
|
home_widget: ^0.6.0
|
||||||
html_unescape: ^2.0.0
|
html_unescape: ^2.0.0
|
||||||
http: ^1.1.0
|
http: ^1.1.0
|
||||||
image: ^4.0.17
|
image: ^4.0.17
|
||||||
image_editor: ^1.3.0
|
image_editor: ^1.3.0
|
||||||
in_app_purchase: ^3.0.7
|
in_app_purchase: ^3.0.7
|
||||||
intl: ^0.18.0
|
intl: ^0.19.0
|
||||||
json_annotation: ^4.8.0
|
json_annotation: ^4.8.0
|
||||||
latlong2: ^0.9.0
|
latlong2: ^0.9.0
|
||||||
like_button: ^2.0.5
|
like_button: ^2.0.5
|
||||||
|
@ -152,9 +152,9 @@ dependencies:
|
||||||
sqlite3_flutter_libs: ^0.5.20
|
sqlite3_flutter_libs: ^0.5.20
|
||||||
sqlite_async: ^0.6.1
|
sqlite_async: ^0.6.1
|
||||||
step_progress_indicator: ^1.0.2
|
step_progress_indicator: ^1.0.2
|
||||||
styled_text: ^7.0.0
|
styled_text: ^8.1.0
|
||||||
syncfusion_flutter_core: ^19.2.49
|
syncfusion_flutter_core: ^25.2.5
|
||||||
syncfusion_flutter_sliders: ^19.2.49
|
syncfusion_flutter_sliders: ^25.2.5
|
||||||
synchronized: ^3.1.0
|
synchronized: ^3.1.0
|
||||||
tuple: ^2.0.0
|
tuple: ^2.0.0
|
||||||
uni_links: ^0.5.1
|
uni_links: ^0.5.1
|
||||||
|
@ -177,6 +177,7 @@ dependency_overrides:
|
||||||
# Remove this after removing dependency from flutter_sodium.
|
# 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
|
# Newer flutter packages depends on ffi > 2.0.0 while flutter_sodium depends on ffi < 2.0.0
|
||||||
ffi: 2.1.0
|
ffi: 2.1.0
|
||||||
|
intl: 0.18.1
|
||||||
video_player:
|
video_player:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/ente-io/packages.git
|
url: https://github.com/ente-io/packages.git
|
||||||
|
|
|
@ -95,6 +95,15 @@ db:
|
||||||
# Map of data centers
|
# Map of data centers
|
||||||
#
|
#
|
||||||
# Each data center also specifies which bucket in that provider should be used.
|
# 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:
|
s3:
|
||||||
# Override the primary and secondary hot storage. The commented out values
|
# Override the primary and secondary hot storage. The commented out values
|
||||||
# are the defaults.
|
# 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 { CustomHead } from "@/next/components/Head";
|
||||||
import { setupI18n } from "@/next/i18n";
|
import { setupI18n } from "@/next/i18n";
|
||||||
import { logUnhandledErrorsAndRejections } from "@/next/log-web";
|
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 { PAGES } from "@ente/accounts/constants/pages";
|
||||||
import { accountLogout } from "@ente/accounts/services/logout";
|
import { accountLogout } from "@ente/accounts/services/logout";
|
||||||
import { APPS, APP_TITLES } from "@ente/shared/apps/constants";
|
import { APPS, APP_TITLES } from "@ente/shared/apps/constants";
|
||||||
import { Overlay } from "@ente/shared/components/Container";
|
import { Overlay } from "@ente/shared/components/Container";
|
||||||
import DialogBoxV2 from "@ente/shared/components/DialogBoxV2";
|
import DialogBoxV2 from "@ente/shared/components/DialogBoxV2";
|
||||||
import {
|
import type { DialogBoxAttributesV2 } from "@ente/shared/components/DialogBoxV2/types";
|
||||||
DialogBoxAttributesV2,
|
|
||||||
SetDialogBoxAttributesV2,
|
|
||||||
} from "@ente/shared/components/DialogBoxV2/types";
|
|
||||||
import EnteSpinner from "@ente/shared/components/EnteSpinner";
|
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 { useLocalState } from "@ente/shared/hooks/useLocalState";
|
||||||
import HTTPService from "@ente/shared/network/HTTPService";
|
import HTTPService from "@ente/shared/network/HTTPService";
|
||||||
import { LS_KEYS, getData } from "@ente/shared/storage/localStorage";
|
import { LS_KEYS, getData } from "@ente/shared/storage/localStorage";
|
||||||
|
@ -22,25 +21,28 @@ import { ThemeProvider } from "@mui/material/styles";
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
import { AppProps } from "next/app";
|
import { AppProps } from "next/app";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { createContext, useEffect, useState } from "react";
|
import { createContext, useContext, useEffect, useState } from "react";
|
||||||
import "styles/global.css";
|
import "styles/global.css";
|
||||||
|
|
||||||
interface AppContextProps {
|
/** The accounts app has no extra properties on top of the base context. */
|
||||||
isMobile: boolean;
|
type AppContextT = BaseAppContextT;
|
||||||
showNavBar: (show: boolean) => void;
|
|
||||||
setDialogBoxAttributesV2: SetDialogBoxAttributesV2;
|
|
||||||
logout: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
export default function App({ Component, pageProps }: AppProps) {
|
||||||
|
const appName: AppName = "account";
|
||||||
|
|
||||||
const [isI18nReady, setIsI18nReady] = useState<boolean>(false);
|
const [isI18nReady, setIsI18nReady] = useState<boolean>(false);
|
||||||
|
|
||||||
const [showNavbar, setShowNavBar] = useState(false);
|
const [showNavbar, setShowNavBar] = useState(false);
|
||||||
|
|
||||||
const [dialogBoxAttributeV2, setDialogBoxAttributesV2] =
|
const [dialogBoxAttributeV2, setDialogBoxAttributesV2] = useState<
|
||||||
useState<DialogBoxAttributesV2>();
|
DialogBoxAttributesV2 | undefined
|
||||||
|
>();
|
||||||
|
|
||||||
const [dialogBoxV2View, setDialogBoxV2View] = useState(false);
|
const [dialogBoxV2View, setDialogBoxV2View] = useState(false);
|
||||||
|
|
||||||
|
@ -85,8 +87,17 @@ export default function App({ Component, pageProps }: AppProps) {
|
||||||
void accountLogout().then(() => router.push(PAGES.ROOT));
|
void accountLogout().then(() => router.push(PAGES.ROOT));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const appContext = {
|
||||||
|
appName,
|
||||||
|
logout,
|
||||||
|
showNavBar,
|
||||||
|
isMobile,
|
||||||
|
setDialogBoxAttributesV2,
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: This string doesn't actually exist
|
||||||
const title = isI18nReady
|
const title = isI18nReady
|
||||||
? t("TITLE", { context: APPS.ACCOUNTS })
|
? t("title", { context: "accounts" })
|
||||||
: APP_TITLES.get(APPS.ACCOUNTS);
|
: APP_TITLES.get(APPS.ACCOUNTS);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -102,15 +113,7 @@ export default function App({ Component, pageProps }: AppProps) {
|
||||||
attributes={dialogBoxAttributeV2 as any}
|
attributes={dialogBoxAttributeV2 as any}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AppContext.Provider
|
<AppContext.Provider value={appContext}>
|
||||||
value={{
|
|
||||||
isMobile,
|
|
||||||
showNavBar,
|
|
||||||
setDialogBoxAttributesV2:
|
|
||||||
setDialogBoxAttributesV2 as any,
|
|
||||||
logout,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{!isI18nReady && (
|
{!isI18nReady && (
|
||||||
<Overlay
|
<Overlay
|
||||||
sx={(theme) => ({
|
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 { TwoFactorType } from "@ente/accounts/constants/twofactor";
|
||||||
import RecoverPage from "@ente/accounts/pages/recover";
|
import RecoverPage from "@ente/accounts/pages/two-factor/recover";
|
||||||
import { APPS } from "@ente/shared/apps/constants";
|
import { useAppContext } from "../../_app";
|
||||||
import { AppContext } from "pages/_app";
|
|
||||||
import { useContext } from "react";
|
|
||||||
|
|
||||||
export default function Recover() {
|
const Page = () => (
|
||||||
const appContext = useContext(AppContext);
|
<RecoverPage
|
||||||
return (
|
appContext={useAppContext()}
|
||||||
<RecoverPage
|
twoFactorType={TwoFactorType.PASSKEY}
|
||||||
appContext={appContext}
|
/>
|
||||||
appName={APPS.PHOTOS}
|
);
|
||||||
twoFactorType={TwoFactorType.PASSKEY}
|
|
||||||
/>
|
export default Page;
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
@ -9,14 +9,8 @@ import { t } from "i18next";
|
||||||
import _sodium from "libsodium-wrappers";
|
import _sodium from "libsodium-wrappers";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { AppContext } from "pages/_app";
|
import { AppContext } from "pages/_app";
|
||||||
import {
|
import type { Dispatch, SetStateAction } from "react";
|
||||||
Dispatch,
|
import { createContext, useContext, useEffect, useState } from "react";
|
||||||
SetStateAction,
|
|
||||||
createContext,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import { Passkey } from "types/passkey";
|
import { Passkey } from "types/passkey";
|
||||||
import {
|
import {
|
||||||
finishPasskeyRegistration,
|
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} />
|
|
||||||
);
|
|
||||||
}
|
|
6
web/apps/accounts/src/pages/verify.tsx
Normal file
6
web/apps/accounts/src/pages/verify.tsx
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import Page_ from "@ente/accounts/pages/verify";
|
||||||
|
import { useAppContext } from "./_app";
|
||||||
|
|
||||||
|
const Page = () => <Page_ appContext={useAppContext()} />;
|
||||||
|
|
||||||
|
export default Page;
|
|
@ -1,9 +0,0 @@
|
||||||
import VerifyPage from "@ente/accounts/pages/verify";
|
|
||||||
import { APPS } from "@ente/shared/apps/constants";
|
|
||||||
import { AppContext } from "pages/_app";
|
|
||||||
import { useContext } from "react";
|
|
||||||
|
|
||||||
export default function Verify() {
|
|
||||||
const appContext = useContext(AppContext);
|
|
||||||
return <VerifyPage appContext={appContext} appName={APPS.ACCOUNTS} />;
|
|
||||||
}
|
|
|
@ -1 +1,4 @@
|
||||||
NEXT_TELEMETRY_DISABLED = 1
|
NEXT_TELEMETRY_DISABLED = 1
|
||||||
|
|
||||||
|
# For details on how to populate a .env.local to run auth and get it to connect
|
||||||
|
# to an arbitrary Ente instance, see `apps/photos/.env`.
|
||||||
|
|
|
@ -9,5 +9,5 @@ module.exports = {
|
||||||
tsconfigRootDir: __dirname,
|
tsconfigRootDir: __dirname,
|
||||||
project: "./tsconfig.json",
|
project: "./tsconfig.json",
|
||||||
},
|
},
|
||||||
ignorePatterns: [".eslintrc.js", "out"],
|
ignorePatterns: [".eslintrc.js", "next.config.js", "out"],
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,9 +3,12 @@
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@/build-config": "*",
|
||||||
"@/next": "*",
|
"@/next": "*",
|
||||||
"@ente/accounts": "*",
|
"@ente/accounts": "*",
|
||||||
"@ente/eslint-config": "*",
|
"@ente/eslint-config": "*",
|
||||||
"@ente/shared": "*"
|
"@ente/shared": "*",
|
||||||
|
"jssha": "~3.3.1",
|
||||||
|
"otpauth": "^9"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
import { Button } from "@mui/material";
|
|
||||||
import { t } from "i18next";
|
|
||||||
|
|
||||||
export const AuthFooter = () => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<p>{t("AUTH_DOWNLOAD_MOBILE_APP")}</p>
|
|
||||||
<a
|
|
||||||
href="https://github.com/ente-io/ente/tree/main/auth#-download"
|
|
||||||
download
|
|
||||||
>
|
|
||||||
<Button color="accent">{t("DOWNLOAD")}</Button>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,35 +0,0 @@
|
||||||
import { HorizontalFlex } from "@ente/shared/components/Container";
|
|
||||||
import { EnteLogo } from "@ente/shared/components/EnteLogo";
|
|
||||||
import NavbarBase from "@ente/shared/components/Navbar/base";
|
|
||||||
import OverflowMenu from "@ente/shared/components/OverflowMenu/menu";
|
|
||||||
import { OverflowMenuOption } from "@ente/shared/components/OverflowMenu/option";
|
|
||||||
import LogoutOutlined from "@mui/icons-material/LogoutOutlined";
|
|
||||||
import MoreHoriz from "@mui/icons-material/MoreHoriz";
|
|
||||||
import { t } from "i18next";
|
|
||||||
import { AppContext } from "pages/_app";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
export default function AuthNavbar() {
|
|
||||||
const { isMobile, logout } = React.useContext(AppContext);
|
|
||||||
return (
|
|
||||||
<NavbarBase isMobile={isMobile}>
|
|
||||||
<HorizontalFlex flex={1} justifyContent={"center"}>
|
|
||||||
<EnteLogo />
|
|
||||||
</HorizontalFlex>
|
|
||||||
<HorizontalFlex position={"absolute"} right="24px">
|
|
||||||
<OverflowMenu
|
|
||||||
ariaControls={"auth-options"}
|
|
||||||
triggerButtonIcon={<MoreHoriz />}
|
|
||||||
>
|
|
||||||
<OverflowMenuOption
|
|
||||||
color="critical"
|
|
||||||
startIcon={<LogoutOutlined />}
|
|
||||||
onClick={logout}
|
|
||||||
>
|
|
||||||
{t("LOGOUT")}
|
|
||||||
</OverflowMenuOption>
|
|
||||||
</OverflowMenu>
|
|
||||||
</HorizontalFlex>
|
|
||||||
</NavbarBase>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,237 +0,0 @@
|
||||||
import { ButtonBase, Snackbar } from "@mui/material";
|
|
||||||
import { t } from "i18next";
|
|
||||||
import { HOTP, TOTP } from "otpauth";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { Code } from "types/code";
|
|
||||||
import TimerProgress from "./TimerProgress";
|
|
||||||
|
|
||||||
const TOTPDisplay = ({ issuer, account, code, nextCode, period }) => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
backgroundColor: "rgba(40, 40, 40, 0.6)",
|
|
||||||
borderRadius: "4px",
|
|
||||||
overflow: "hidden",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TimerProgress period={period ?? Code.defaultPeriod} />
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
padding: "12px 20px 0px 20px",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "flex-start",
|
|
||||||
minWidth: "320px",
|
|
||||||
minHeight: "120px",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
alignItems: "flex-start",
|
|
||||||
minWidth: "200px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<p
|
|
||||||
style={{
|
|
||||||
fontWeight: "bold",
|
|
||||||
margin: "0px",
|
|
||||||
fontSize: "14px",
|
|
||||||
textAlign: "left",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{issuer}
|
|
||||||
</p>
|
|
||||||
<p
|
|
||||||
style={{
|
|
||||||
marginTop: "0px",
|
|
||||||
marginBottom: "8px",
|
|
||||||
textAlign: "left",
|
|
||||||
fontSize: "12px",
|
|
||||||
maxWidth: "200px",
|
|
||||||
minHeight: "16px",
|
|
||||||
color: "grey",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{account}
|
|
||||||
</p>
|
|
||||||
<p
|
|
||||||
style={{
|
|
||||||
margin: "0px",
|
|
||||||
marginBottom: "1rem",
|
|
||||||
fontSize: "24px",
|
|
||||||
fontWeight: "bold",
|
|
||||||
textAlign: "left",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{code}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div style={{ flex: 1 }} />
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
alignItems: "flex-end",
|
|
||||||
minWidth: "120px",
|
|
||||||
textAlign: "right",
|
|
||||||
marginTop: "auto",
|
|
||||||
marginBottom: "1rem",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<p
|
|
||||||
style={{
|
|
||||||
fontWeight: "bold",
|
|
||||||
marginBottom: "0px",
|
|
||||||
fontSize: "10px",
|
|
||||||
marginTop: "auto",
|
|
||||||
textAlign: "right",
|
|
||||||
color: "grey",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("AUTH_NEXT")}
|
|
||||||
</p>
|
|
||||||
<p
|
|
||||||
style={{
|
|
||||||
fontSize: "14px",
|
|
||||||
fontWeight: "bold",
|
|
||||||
marginBottom: "0px",
|
|
||||||
marginTop: "auto",
|
|
||||||
textAlign: "right",
|
|
||||||
color: "grey",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{nextCode}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
function BadCodeInfo({ codeInfo, codeErr }) {
|
|
||||||
const [showRawData, setShowRawData] = useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="code-info">
|
|
||||||
<div>{codeInfo.title}</div>
|
|
||||||
<div>{codeErr}</div>
|
|
||||||
<div>
|
|
||||||
{showRawData ? (
|
|
||||||
<div onClick={() => setShowRawData(false)}>
|
|
||||||
{codeInfo.rawData ?? "no raw data"}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div onClick={() => setShowRawData(true)}>Show rawData</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface OTPDisplayProps {
|
|
||||||
codeInfo: Code;
|
|
||||||
}
|
|
||||||
|
|
||||||
const OTPDisplay = (props: OTPDisplayProps) => {
|
|
||||||
const { codeInfo } = props;
|
|
||||||
const [code, setCode] = useState("");
|
|
||||||
const [nextCode, setNextCode] = useState("");
|
|
||||||
const [codeErr, setCodeErr] = useState("");
|
|
||||||
const [hasCopied, setHasCopied] = useState(false);
|
|
||||||
|
|
||||||
const generateCodes = () => {
|
|
||||||
try {
|
|
||||||
const currentTime = new Date().getTime();
|
|
||||||
if (codeInfo.type.toLowerCase() === "totp") {
|
|
||||||
const totp = new TOTP({
|
|
||||||
secret: codeInfo.secret,
|
|
||||||
algorithm: codeInfo.algorithm ?? Code.defaultAlgo,
|
|
||||||
period: codeInfo.period ?? Code.defaultPeriod,
|
|
||||||
digits: codeInfo.digits ?? Code.defaultDigits,
|
|
||||||
});
|
|
||||||
setCode(totp.generate());
|
|
||||||
setNextCode(
|
|
||||||
totp.generate({
|
|
||||||
timestamp: currentTime + codeInfo.period * 1000,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} else if (codeInfo.type.toLowerCase() === "hotp") {
|
|
||||||
const hotp = new HOTP({
|
|
||||||
secret: codeInfo.secret,
|
|
||||||
counter: 0,
|
|
||||||
algorithm: codeInfo.algorithm,
|
|
||||||
});
|
|
||||||
setCode(hotp.generate());
|
|
||||||
setNextCode(hotp.generate({ counter: 1 }));
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
setCodeErr(err.message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const copyCode = () => {
|
|
||||||
navigator.clipboard.writeText(code);
|
|
||||||
setHasCopied(true);
|
|
||||||
setTimeout(() => {
|
|
||||||
setHasCopied(false);
|
|
||||||
}, 2000);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// this is to set the initial code and nextCode on component mount
|
|
||||||
generateCodes();
|
|
||||||
const codeType = codeInfo.type;
|
|
||||||
const codePeriodInMs = codeInfo.period * 1000;
|
|
||||||
const timeToNextCode =
|
|
||||||
codePeriodInMs - (new Date().getTime() % codePeriodInMs);
|
|
||||||
const intervalId = null;
|
|
||||||
// wait until we are at the start of the next code period,
|
|
||||||
// and then start the interval loop
|
|
||||||
setTimeout(() => {
|
|
||||||
// we need to call generateCodes() once before the interval loop
|
|
||||||
// to set the initial code and nextCode
|
|
||||||
generateCodes();
|
|
||||||
codeType.toLowerCase() === "totp" ||
|
|
||||||
codeType.toLowerCase() === "hotp"
|
|
||||||
? setInterval(() => {
|
|
||||||
generateCodes();
|
|
||||||
}, codePeriodInMs)
|
|
||||||
: null;
|
|
||||||
}, timeToNextCode);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (intervalId) clearInterval(intervalId);
|
|
||||||
};
|
|
||||||
}, [codeInfo]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ padding: "8px" }}>
|
|
||||||
{codeErr === "" ? (
|
|
||||||
<ButtonBase
|
|
||||||
component="div"
|
|
||||||
onClick={() => {
|
|
||||||
copyCode();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TOTPDisplay
|
|
||||||
period={codeInfo.period}
|
|
||||||
issuer={codeInfo.issuer}
|
|
||||||
account={codeInfo.account}
|
|
||||||
code={code}
|
|
||||||
nextCode={nextCode}
|
|
||||||
/>
|
|
||||||
<Snackbar
|
|
||||||
open={hasCopied}
|
|
||||||
message="Code copied to clipboard"
|
|
||||||
/>
|
|
||||||
</ButtonBase>
|
|
||||||
) : (
|
|
||||||
<BadCodeInfo codeInfo={codeInfo} codeErr={codeErr} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default OTPDisplay;
|
|
|
@ -1,41 +0,0 @@
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
const TimerProgress = ({ period }) => {
|
|
||||||
const [progress, setProgress] = useState(0);
|
|
||||||
const [ticker, setTicker] = useState(null);
|
|
||||||
const microSecondsInPeriod = period * 1000000;
|
|
||||||
|
|
||||||
const startTicker = () => {
|
|
||||||
const ticker = setInterval(() => {
|
|
||||||
updateTimeRemaining();
|
|
||||||
}, 10);
|
|
||||||
setTicker(ticker);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateTimeRemaining = () => {
|
|
||||||
const timeRemaining =
|
|
||||||
microSecondsInPeriod -
|
|
||||||
((new Date().getTime() * 1000) % microSecondsInPeriod);
|
|
||||||
setProgress(timeRemaining / microSecondsInPeriod);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
startTicker();
|
|
||||||
return () => clearInterval(ticker);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const color = progress > 0.4 ? "green" : "orange";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
borderTopLeftRadius: "3px",
|
|
||||||
width: `${progress * 100}%`,
|
|
||||||
height: "3px",
|
|
||||||
backgroundColor: color,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TimerProgress;
|
|
|
@ -1,9 +1,3 @@
|
||||||
import { APPS } from "@ente/shared/apps/constants";
|
import Page from "@ente/shared/next/pages/404";
|
||||||
import NotFoundPage from "@ente/shared/next/pages/404";
|
|
||||||
import { AppContext } from "pages/_app";
|
|
||||||
import { useContext } from "react";
|
|
||||||
|
|
||||||
export default function NotFound() {
|
export default Page;
|
||||||
const appContext = useContext(AppContext);
|
|
||||||
return <NotFoundPage appContext={appContext} appName={APPS.AUTH} />;
|
|
||||||
}
|
|
||||||
|
|
|
@ -4,6 +4,8 @@ import {
|
||||||
logStartupBanner,
|
logStartupBanner,
|
||||||
logUnhandledErrorsAndRejections,
|
logUnhandledErrorsAndRejections,
|
||||||
} from "@/next/log-web";
|
} from "@/next/log-web";
|
||||||
|
import type { AppName, BaseAppContextT } from "@/next/types/app";
|
||||||
|
import { ensure } from "@/utils/ensure";
|
||||||
import { accountLogout } from "@ente/accounts/services/logout";
|
import { accountLogout } from "@ente/accounts/services/logout";
|
||||||
import {
|
import {
|
||||||
APPS,
|
APPS,
|
||||||
|
@ -12,45 +14,46 @@ import {
|
||||||
} from "@ente/shared/apps/constants";
|
} from "@ente/shared/apps/constants";
|
||||||
import { Overlay } from "@ente/shared/components/Container";
|
import { Overlay } from "@ente/shared/components/Container";
|
||||||
import DialogBoxV2 from "@ente/shared/components/DialogBoxV2";
|
import DialogBoxV2 from "@ente/shared/components/DialogBoxV2";
|
||||||
import {
|
import type { DialogBoxAttributesV2 } from "@ente/shared/components/DialogBoxV2/types";
|
||||||
DialogBoxAttributesV2,
|
|
||||||
SetDialogBoxAttributesV2,
|
|
||||||
} from "@ente/shared/components/DialogBoxV2/types";
|
|
||||||
import EnteSpinner from "@ente/shared/components/EnteSpinner";
|
import EnteSpinner from "@ente/shared/components/EnteSpinner";
|
||||||
import { MessageContainer } from "@ente/shared/components/MessageContainer";
|
import { MessageContainer } from "@ente/shared/components/MessageContainer";
|
||||||
import AppNavbar from "@ente/shared/components/Navbar/app";
|
import { AppNavbar } from "@ente/shared/components/Navbar/app";
|
||||||
import { PHOTOS_PAGES as PAGES } from "@ente/shared/constants/pages";
|
import { PHOTOS_PAGES as PAGES } from "@ente/shared/constants/pages";
|
||||||
import { useLocalState } from "@ente/shared/hooks/useLocalState";
|
import { useLocalState } from "@ente/shared/hooks/useLocalState";
|
||||||
import HTTPService from "@ente/shared/network/HTTPService";
|
import HTTPService from "@ente/shared/network/HTTPService";
|
||||||
import { LS_KEYS, getData } from "@ente/shared/storage/localStorage";
|
import { LS_KEYS, getData } from "@ente/shared/storage/localStorage";
|
||||||
import { getTheme } from "@ente/shared/themes";
|
import { getTheme } from "@ente/shared/themes";
|
||||||
import { THEME_COLOR } from "@ente/shared/themes/constants";
|
import { THEME_COLOR } from "@ente/shared/themes/constants";
|
||||||
import { SetTheme } from "@ente/shared/themes/types";
|
|
||||||
import type { User } from "@ente/shared/user/types";
|
import type { User } from "@ente/shared/user/types";
|
||||||
import { CssBaseline, useMediaQuery } from "@mui/material";
|
import { CssBaseline, useMediaQuery } from "@mui/material";
|
||||||
import { ThemeProvider } from "@mui/material/styles";
|
import { ThemeProvider } from "@mui/material/styles";
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
import { AppProps } from "next/app";
|
import type { AppProps } from "next/app";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { createContext, useEffect, useRef, useState } from "react";
|
import { createContext, useContext, useEffect, useRef, useState } from "react";
|
||||||
import LoadingBar from "react-top-loading-bar";
|
import LoadingBar, { type LoadingBarRef } from "react-top-loading-bar";
|
||||||
import "../../public/css/global.css";
|
import "../../public/css/global.css";
|
||||||
|
|
||||||
type AppContextType = {
|
/**
|
||||||
showNavBar: (show: boolean) => void;
|
* Properties available via the {@link AppContext} to the Auth app's React tree.
|
||||||
|
*/
|
||||||
|
type AppContextT = BaseAppContextT & {
|
||||||
startLoading: () => void;
|
startLoading: () => void;
|
||||||
finishLoading: () => void;
|
finishLoading: () => void;
|
||||||
isMobile: boolean;
|
|
||||||
themeColor: THEME_COLOR;
|
themeColor: THEME_COLOR;
|
||||||
setThemeColor: SetTheme;
|
setThemeColor: (themeColor: THEME_COLOR) => void;
|
||||||
somethingWentWrong: () => void;
|
somethingWentWrong: () => void;
|
||||||
setDialogBoxAttributesV2: SetDialogBoxAttributesV2;
|
|
||||||
logout: () => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AppContext = createContext<AppContextType>(null);
|
/** 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) {
|
export default function App({ Component, pageProps }: AppProps) {
|
||||||
|
const appName: AppName = "auth";
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [isI18nReady, setIsI18nReady] = useState<boolean>(false);
|
const [isI18nReady, setIsI18nReady] = useState<boolean>(false);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
@ -58,10 +61,11 @@ export default function App({ Component, pageProps }: AppProps) {
|
||||||
typeof window !== "undefined" && !window.navigator.onLine,
|
typeof window !== "undefined" && !window.navigator.onLine,
|
||||||
);
|
);
|
||||||
const [showNavbar, setShowNavBar] = useState(false);
|
const [showNavbar, setShowNavBar] = useState(false);
|
||||||
const isLoadingBarRunning = useRef(false);
|
const isLoadingBarRunning = useRef<boolean>(false);
|
||||||
const loadingBar = useRef(null);
|
const loadingBar = useRef<LoadingBarRef>(null);
|
||||||
const [dialogBoxAttributeV2, setDialogBoxAttributesV2] =
|
const [dialogBoxAttributeV2, setDialogBoxAttributesV2] = useState<
|
||||||
useState<DialogBoxAttributesV2>();
|
DialogBoxAttributesV2 | undefined
|
||||||
|
>();
|
||||||
const [dialogBoxV2View, setDialogBoxV2View] = useState(false);
|
const [dialogBoxV2View, setDialogBoxV2View] = useState(false);
|
||||||
const isMobile = useMediaQuery("(max-width:428px)");
|
const isMobile = useMediaQuery("(max-width:428px)");
|
||||||
const [themeColor, setThemeColor] = useLocalState(
|
const [themeColor, setThemeColor] = useLocalState(
|
||||||
|
@ -134,9 +138,23 @@ export default function App({ Component, pageProps }: AppProps) {
|
||||||
void accountLogout().then(() => router.push(PAGES.ROOT));
|
void accountLogout().then(() => router.push(PAGES.ROOT));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const appContext = {
|
||||||
|
appName,
|
||||||
|
logout,
|
||||||
|
showNavBar,
|
||||||
|
isMobile,
|
||||||
|
setDialogBoxAttributesV2,
|
||||||
|
startLoading,
|
||||||
|
finishLoading,
|
||||||
|
themeColor,
|
||||||
|
setThemeColor,
|
||||||
|
somethingWentWrong,
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: Refactor this to have a fallback
|
||||||
const title = isI18nReady
|
const title = isI18nReady
|
||||||
? t("TITLE", { context: APPS.AUTH })
|
? t("title", { context: "auth" })
|
||||||
: APP_TITLES.get(APPS.AUTH);
|
: APP_TITLES.get(APPS.AUTH) ?? "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -158,19 +176,7 @@ export default function App({ Component, pageProps }: AppProps) {
|
||||||
attributes={dialogBoxAttributeV2}
|
attributes={dialogBoxAttributeV2}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AppContext.Provider
|
<AppContext.Provider value={appContext}>
|
||||||
value={{
|
|
||||||
showNavBar,
|
|
||||||
startLoading,
|
|
||||||
finishLoading,
|
|
||||||
isMobile,
|
|
||||||
themeColor,
|
|
||||||
setThemeColor,
|
|
||||||
somethingWentWrong,
|
|
||||||
setDialogBoxAttributesV2,
|
|
||||||
logout,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{(loading || !isI18nReady) && (
|
{(loading || !isI18nReady) && (
|
||||||
<Overlay
|
<Overlay
|
||||||
sx={(theme) => ({
|
sx={(theme) => ({
|
||||||
|
|
414
web/apps/auth/src/pages/auth.tsx
Normal file
414
web/apps/auth/src/pages/auth.tsx
Normal file
|
@ -0,0 +1,414 @@
|
||||||
|
import { ensure } from "@/utils/ensure";
|
||||||
|
import {
|
||||||
|
HorizontalFlex,
|
||||||
|
VerticallyCentered,
|
||||||
|
} from "@ente/shared/components/Container";
|
||||||
|
import { EnteLogo } from "@ente/shared/components/EnteLogo";
|
||||||
|
import EnteSpinner from "@ente/shared/components/EnteSpinner";
|
||||||
|
import NavbarBase from "@ente/shared/components/Navbar/base";
|
||||||
|
import OverflowMenu from "@ente/shared/components/OverflowMenu/menu";
|
||||||
|
import { OverflowMenuOption } from "@ente/shared/components/OverflowMenu/option";
|
||||||
|
import { AUTH_PAGES as PAGES } from "@ente/shared/constants/pages";
|
||||||
|
import { CustomError } from "@ente/shared/error";
|
||||||
|
import InMemoryStore, { MS_KEYS } from "@ente/shared/storage/InMemoryStore";
|
||||||
|
import LogoutOutlined from "@mui/icons-material/LogoutOutlined";
|
||||||
|
import MoreHoriz from "@mui/icons-material/MoreHoriz";
|
||||||
|
import { Button, ButtonBase, Snackbar, TextField, styled } from "@mui/material";
|
||||||
|
import { t } from "i18next";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { AppContext } from "pages/_app";
|
||||||
|
import React, { useContext, useEffect, useState } from "react";
|
||||||
|
import { generateOTPs, type Code } from "services/code";
|
||||||
|
import { getAuthCodes } from "services/remote";
|
||||||
|
|
||||||
|
const Page: React.FC = () => {
|
||||||
|
const appContext = ensure(useContext(AppContext));
|
||||||
|
const router = useRouter();
|
||||||
|
const [codes, setCodes] = useState<Code[]>([]);
|
||||||
|
const [hasFetched, setHasFetched] = useState(false);
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchCodes = async () => {
|
||||||
|
try {
|
||||||
|
setCodes(await getAuthCodes());
|
||||||
|
} catch (e) {
|
||||||
|
if (
|
||||||
|
e instanceof Error &&
|
||||||
|
e.message == CustomError.KEY_MISSING
|
||||||
|
) {
|
||||||
|
InMemoryStore.set(MS_KEYS.REDIRECT_URL, PAGES.AUTH);
|
||||||
|
router.push(PAGES.ROOT);
|
||||||
|
} else {
|
||||||
|
// do not log errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setHasFetched(true);
|
||||||
|
};
|
||||||
|
void fetchCodes();
|
||||||
|
appContext.showNavBar(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const lcSearch = searchTerm.toLowerCase();
|
||||||
|
const filteredCodes = codes.filter(
|
||||||
|
(code) =>
|
||||||
|
code.issuer?.toLowerCase().includes(lcSearch) ||
|
||||||
|
code.account?.toLowerCase().includes(lcSearch),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasFetched) {
|
||||||
|
return (
|
||||||
|
<VerticallyCentered>
|
||||||
|
<EnteSpinner />
|
||||||
|
</VerticallyCentered>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AuthNavbar />
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
maxWidth: "800px",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
margin: "0 auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ marginBottom: "1rem" }} />
|
||||||
|
{filteredCodes.length == 0 && searchTerm.length == 0 ? (
|
||||||
|
<></>
|
||||||
|
) : (
|
||||||
|
<TextField
|
||||||
|
id="search"
|
||||||
|
name="search"
|
||||||
|
label={t("SEARCH")}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
variant="filled"
|
||||||
|
style={{ width: "350px" }}
|
||||||
|
value={searchTerm}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div style={{ marginBottom: "1rem" }} />
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{filteredCodes.length == 0 ? (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
alignItems: "center",
|
||||||
|
display: "flex",
|
||||||
|
textAlign: "center",
|
||||||
|
marginTop: "32px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{searchTerm.length > 0 ? (
|
||||||
|
<p>{t("NO_RESULTS")}</p>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
filteredCodes.map((code) => (
|
||||||
|
<CodeDisplay key={code.id} code={code} />
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Page;
|
||||||
|
|
||||||
|
const AuthNavbar: React.FC = () => {
|
||||||
|
const { isMobile, logout } = ensure(useContext(AppContext));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NavbarBase isMobile={isMobile}>
|
||||||
|
<HorizontalFlex flex={1} justifyContent={"center"}>
|
||||||
|
<EnteLogo />
|
||||||
|
</HorizontalFlex>
|
||||||
|
<HorizontalFlex position={"absolute"} right="24px">
|
||||||
|
<OverflowMenu
|
||||||
|
ariaControls={"auth-options"}
|
||||||
|
triggerButtonIcon={<MoreHoriz />}
|
||||||
|
>
|
||||||
|
<OverflowMenuOption
|
||||||
|
color="critical"
|
||||||
|
startIcon={<LogoutOutlined />}
|
||||||
|
onClick={logout}
|
||||||
|
>
|
||||||
|
{t("LOGOUT")}
|
||||||
|
</OverflowMenuOption>
|
||||||
|
</OverflowMenu>
|
||||||
|
</HorizontalFlex>
|
||||||
|
</NavbarBase>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface CodeDisplayProps {
|
||||||
|
code: Code;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CodeDisplay: React.FC<CodeDisplayProps> = ({ code }) => {
|
||||||
|
const [otp, setOTP] = useState("");
|
||||||
|
const [nextOTP, setNextOTP] = useState("");
|
||||||
|
const [errorMessage, setErrorMessage] = useState("");
|
||||||
|
const [hasCopied, setHasCopied] = useState(false);
|
||||||
|
|
||||||
|
const regen = () => {
|
||||||
|
try {
|
||||||
|
const [m, n] = generateOTPs(code);
|
||||||
|
setOTP(m);
|
||||||
|
setNextOTP(n);
|
||||||
|
} catch (e) {
|
||||||
|
setErrorMessage(e instanceof Error ? e.message : String(e));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyCode = () => {
|
||||||
|
navigator.clipboard.writeText(otp);
|
||||||
|
setHasCopied(true);
|
||||||
|
setTimeout(() => setHasCopied(false), 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Generate to set the initial otp and nextOTP on component mount.
|
||||||
|
regen();
|
||||||
|
|
||||||
|
const periodMs = code.period * 1000;
|
||||||
|
const timeToNextCode = periodMs - (Date.now() % periodMs);
|
||||||
|
|
||||||
|
let interval: ReturnType<typeof setInterval> | undefined;
|
||||||
|
// Wait until we are at the start of the next code period, and then
|
||||||
|
// start the interval loop.
|
||||||
|
setTimeout(() => {
|
||||||
|
// We need to call regen() once before the interval loop to set the
|
||||||
|
// initial otp and nextOTP.
|
||||||
|
regen();
|
||||||
|
interval = setInterval(regen, periodMs);
|
||||||
|
}, timeToNextCode);
|
||||||
|
|
||||||
|
return () => interval && clearInterval(interval);
|
||||||
|
}, [code]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: "8px" }}>
|
||||||
|
{errorMessage ? (
|
||||||
|
<UnparseableCode {...{ code, errorMessage }} />
|
||||||
|
) : (
|
||||||
|
<ButtonBase component="div" onClick={copyCode}>
|
||||||
|
<OTPDisplay {...{ code, otp, nextOTP }} />
|
||||||
|
<Snackbar open={hasCopied} message={t("COPIED")} />
|
||||||
|
</ButtonBase>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface OTPDisplayProps {
|
||||||
|
code: Code;
|
||||||
|
otp: string;
|
||||||
|
nextOTP: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const OTPDisplay: React.FC<OTPDisplayProps> = ({ code, otp, nextOTP }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
backgroundColor: "rgba(40, 40, 40, 0.6)",
|
||||||
|
borderRadius: "4px",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CodeValidityBar code={code} />
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: "12px 20px 0px 20px",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "flex-start",
|
||||||
|
minWidth: "320px",
|
||||||
|
minHeight: "120px",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "flex-start",
|
||||||
|
minWidth: "200px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
fontWeight: "bold",
|
||||||
|
margin: "0px",
|
||||||
|
fontSize: "14px",
|
||||||
|
textAlign: "left",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{code.issuer ?? ""}
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
marginTop: "0px",
|
||||||
|
marginBottom: "8px",
|
||||||
|
textAlign: "left",
|
||||||
|
fontSize: "12px",
|
||||||
|
maxWidth: "200px",
|
||||||
|
minHeight: "16px",
|
||||||
|
color: "grey",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{code.account ?? ""}
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
margin: "0px",
|
||||||
|
marginBottom: "1rem",
|
||||||
|
fontSize: "24px",
|
||||||
|
fontWeight: "bold",
|
||||||
|
textAlign: "left",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{otp}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div style={{ flex: 1 }} />
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "flex-end",
|
||||||
|
minWidth: "120px",
|
||||||
|
textAlign: "right",
|
||||||
|
marginTop: "auto",
|
||||||
|
marginBottom: "1rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
fontWeight: "bold",
|
||||||
|
marginBottom: "0px",
|
||||||
|
fontSize: "10px",
|
||||||
|
marginTop: "auto",
|
||||||
|
textAlign: "right",
|
||||||
|
color: "grey",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("AUTH_NEXT")}
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
fontSize: "14px",
|
||||||
|
fontWeight: "bold",
|
||||||
|
marginBottom: "0px",
|
||||||
|
marginTop: "auto",
|
||||||
|
textAlign: "right",
|
||||||
|
color: "grey",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{nextOTP}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface CodeValidityBarProps {
|
||||||
|
code: Code;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CodeValidityBar: React.FC<CodeValidityBarProps> = ({ code }) => {
|
||||||
|
const [progress, setProgress] = useState(code.type == "hotp" ? 1 : 0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const advance = () => {
|
||||||
|
const us = code.period * 1e6;
|
||||||
|
const timeRemaining = us - ((Date.now() * 1000) % us);
|
||||||
|
setProgress(timeRemaining / us);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ticker =
|
||||||
|
code.type == "hotp" ? undefined : setInterval(advance, 10);
|
||||||
|
|
||||||
|
return () => ticker && clearInterval(ticker);
|
||||||
|
}, [code]);
|
||||||
|
|
||||||
|
const color = progress > 0.4 ? "green" : "orange";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
borderTopLeftRadius: "3px",
|
||||||
|
width: `${progress * 100}%`,
|
||||||
|
height: "3px",
|
||||||
|
backgroundColor: color,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface UnparseableCodeProps {
|
||||||
|
code: Code;
|
||||||
|
errorMessage: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UnparseableCode: React.FC<UnparseableCodeProps> = ({
|
||||||
|
code,
|
||||||
|
errorMessage,
|
||||||
|
}) => {
|
||||||
|
const [showRawData, setShowRawData] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="code-info">
|
||||||
|
<div>{code.issuer}</div>
|
||||||
|
<div>{errorMessage}</div>
|
||||||
|
<div>
|
||||||
|
{showRawData ? (
|
||||||
|
<div onClick={() => setShowRawData(false)}>
|
||||||
|
{code.uriString}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div onClick={() => setShowRawData(true)}>Show rawData</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Footer: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<Footer_>
|
||||||
|
<p>{t("AUTH_DOWNLOAD_MOBILE_APP")}</p>
|
||||||
|
<a
|
||||||
|
href="https://github.com/ente-io/ente/tree/main/auth#-download"
|
||||||
|
download
|
||||||
|
>
|
||||||
|
<Button color="accent">{t("DOWNLOAD")}</Button>
|
||||||
|
</a>
|
||||||
|
</Footer_>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Footer_ = styled("div")`
|
||||||
|
margin-block-start: 2rem;
|
||||||
|
margin-block-end: 4rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
`;
|
|
@ -1,129 +0,0 @@
|
||||||
import { VerticallyCentered } from "@ente/shared/components/Container";
|
|
||||||
import EnteSpinner from "@ente/shared/components/EnteSpinner";
|
|
||||||
import { AUTH_PAGES as PAGES } from "@ente/shared/constants/pages";
|
|
||||||
import { CustomError } from "@ente/shared/error";
|
|
||||||
import InMemoryStore, { MS_KEYS } from "@ente/shared/storage/InMemoryStore";
|
|
||||||
import { TextField } from "@mui/material";
|
|
||||||
import { AuthFooter } from "components/AuthFooter";
|
|
||||||
import AuthNavbar from "components/Navbar";
|
|
||||||
import OTPDisplay from "components/OTPDisplay";
|
|
||||||
import { t } from "i18next";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { AppContext } from "pages/_app";
|
|
||||||
import { useContext, useEffect, useState } from "react";
|
|
||||||
import { getAuthCodes } from "services";
|
|
||||||
|
|
||||||
const AuthenticatorCodesPage = () => {
|
|
||||||
const appContext = useContext(AppContext);
|
|
||||||
const router = useRouter();
|
|
||||||
const [codes, setCodes] = useState([]);
|
|
||||||
const [hasFetched, setHasFetched] = useState(false);
|
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchCodes = async () => {
|
|
||||||
try {
|
|
||||||
const res = await getAuthCodes();
|
|
||||||
setCodes(res);
|
|
||||||
} catch (err) {
|
|
||||||
if (err.message === CustomError.KEY_MISSING) {
|
|
||||||
InMemoryStore.set(MS_KEYS.REDIRECT_URL, PAGES.AUTH);
|
|
||||||
router.push(PAGES.ROOT);
|
|
||||||
} else {
|
|
||||||
// do not log errors
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setHasFetched(true);
|
|
||||||
};
|
|
||||||
fetchCodes();
|
|
||||||
appContext.showNavBar(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const filteredCodes = codes.filter(
|
|
||||||
(secret) =>
|
|
||||||
(secret.issuer ?? "")
|
|
||||||
.toLowerCase()
|
|
||||||
.includes(searchTerm.toLowerCase()) ||
|
|
||||||
(secret.account ?? "")
|
|
||||||
.toLowerCase()
|
|
||||||
.includes(searchTerm.toLowerCase()),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!hasFetched) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<VerticallyCentered>
|
|
||||||
<EnteSpinner></EnteSpinner>
|
|
||||||
</VerticallyCentered>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<AuthNavbar />
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
maxWidth: "800px",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
margin: "0 auto",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{ marginBottom: "1rem" }} />
|
|
||||||
{filteredCodes.length === 0 && searchTerm.length === 0 ? (
|
|
||||||
<></>
|
|
||||||
) : (
|
|
||||||
<TextField
|
|
||||||
id="search"
|
|
||||||
name="search"
|
|
||||||
label={t("SEARCH")}
|
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
|
||||||
variant="filled"
|
|
||||||
style={{ width: "350px" }}
|
|
||||||
value={searchTerm}
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div style={{ marginBottom: "1rem" }} />
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "row",
|
|
||||||
flexWrap: "wrap",
|
|
||||||
justifyContent: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{filteredCodes.length === 0 ? (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
alignItems: "center",
|
|
||||||
display: "flex",
|
|
||||||
textAlign: "center",
|
|
||||||
marginTop: "32px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{searchTerm.length !== 0 ? (
|
|
||||||
<p>{t("NO_RESULTS")}</p>
|
|
||||||
) : (
|
|
||||||
<div />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
filteredCodes.map((code) => (
|
|
||||||
<OTPDisplay codeInfo={code} key={code.id} />
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div style={{ marginBottom: "2rem" }} />
|
|
||||||
<AuthFooter />
|
|
||||||
<div style={{ marginBottom: "4rem" }} />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AuthenticatorCodesPage;
|
|
6
web/apps/auth/src/pages/change-email.tsx
Normal file
6
web/apps/auth/src/pages/change-email.tsx
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import Page_ from "@ente/accounts/pages/change-email";
|
||||||
|
import { useAppContext } from "./_app";
|
||||||
|
|
||||||
|
const Page = () => <Page_ appContext={useAppContext()} />;
|
||||||
|
|
||||||
|
export default Page;
|
|
@ -1,9 +0,0 @@
|
||||||
import ChangeEmailPage from "@ente/accounts/pages/change-email";
|
|
||||||
import { APPS } from "@ente/shared/apps/constants";
|
|
||||||
import { AppContext } from "pages/_app";
|
|
||||||
import { useContext } from "react";
|
|
||||||
|
|
||||||
export default function ChangeEmail() {
|
|
||||||
const appContext = useContext(AppContext);
|
|
||||||
return <ChangeEmailPage appContext={appContext} appName={APPS.AUTH} />;
|
|
||||||
}
|
|
6
web/apps/auth/src/pages/change-password.tsx
Normal file
6
web/apps/auth/src/pages/change-password.tsx
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import Page_ from "@ente/accounts/pages/change-password";
|
||||||
|
import { useAppContext } from "./_app";
|
||||||
|
|
||||||
|
const Page = () => <Page_ appContext={useAppContext()} />;
|
||||||
|
|
||||||
|
export default Page;
|
|
@ -1,9 +0,0 @@
|
||||||
import ChangePasswordPage from "@ente/accounts/pages/change-password";
|
|
||||||
import { APPS } from "@ente/shared/apps/constants";
|
|
||||||
import { AppContext } from "pages/_app";
|
|
||||||
import { useContext } from "react";
|
|
||||||
|
|
||||||
export default function ChangePassword() {
|
|
||||||
const appContext = useContext(AppContext);
|
|
||||||
return <ChangePasswordPage appContext={appContext} appName={APPS.AUTH} />;
|
|
||||||
}
|
|
6
web/apps/auth/src/pages/credentials.tsx
Normal file
6
web/apps/auth/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 { AppContext } from "pages/_app";
|
|
||||||
import { useContext } from "react";
|
|
||||||
|
|
||||||
export default function Credential() {
|
|
||||||
const appContext = useContext(AppContext);
|
|
||||||
return <CredentialPage appContext={appContext} appName={APPS.AUTH} />;
|
|
||||||
}
|
|
6
web/apps/auth/src/pages/generate.tsx
Normal file
6
web/apps/auth/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.AUTH} />;
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue