Merge branch 'main' into generic_group_by

This commit is contained in:
Neeraj Gupta 2024-05-27 11:54:37 +05:30
commit bcf3084d97
293 changed files with 2704 additions and 2235 deletions

View file

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

View file

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

View file

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

View file

@ -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": "アップデートは利用できません"
} }

View file

@ -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}`,
}, },
}); });
}; };

View file

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

View file

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

View file

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

View file

@ -78,3 +78,23 @@ To summarize:
Set the S3 bucket `endpoint` in `credentials.yaml` to a `yourserverip:3200` or 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).

View file

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

View file

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

View file

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

View file

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

View file

@ -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;
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -98,7 +98,7 @@ class MessageLookup extends MessageLookupByLibrary {
"${storageAmountInGB} GB cada vez que alguém se inscrever para um plano pago e aplica o seu código"; "${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(

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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");
} }

View file

@ -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!);
}
});
}

View file

@ -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 {}

View file

@ -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);
}

View file

@ -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) {

View file

@ -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;
} }
}

View file

@ -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();

View file

@ -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);

View file

@ -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();

View file

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

View file

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

View file

@ -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();
} }
} }

View file

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

View file

@ -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);
} }

View file

@ -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;
} }

View 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()!);
}

View 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);
}
}

View file

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

View file

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

View file

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

View file

@ -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) => ({

View file

@ -0,0 +1,6 @@
import Page_ from "@ente/accounts/pages/credentials";
import { useAppContext } from "./_app";
const Page = () => <Page_ appContext={useAppContext()} />;
export default Page;

View file

@ -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} />;
}

View file

@ -0,0 +1,6 @@
import Page_ from "@ente/accounts/pages/generate";
import { useAppContext } from "./_app";
const Page = () => <Page_ appContext={useAppContext()} />;
export default Page;

View file

@ -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} />;
}

View file

@ -0,0 +1,6 @@
import Page_ from "@ente/accounts/pages/login";
import { useAppContext } from "./_app";
const Page = () => <Page_ appContext={useAppContext()} />;
export default Page;

View file

@ -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} />;
}

View file

@ -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;
);
}

View file

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

View file

@ -0,0 +1,6 @@
import Page_ from "@ente/accounts/pages/recover";
import { useAppContext } from "./_app";
const Page = () => <Page_ appContext={useAppContext()} />;
export default Page;

View file

@ -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} />;
}

View file

@ -0,0 +1,6 @@
import Page_ from "@ente/accounts/pages/signup";
import { useAppContext } from "./_app";
const Page = () => <Page_ appContext={useAppContext()} />;
export default Page;

View file

@ -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} />;
}

View 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;

View file

@ -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} />
);
}

View 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;

View file

@ -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} />
);
}

View 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;

View file

@ -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} />
);
}

View file

@ -0,0 +1,6 @@
import Page_ from "@ente/accounts/pages/verify";
import { useAppContext } from "./_app";
const Page = () => <Page_ appContext={useAppContext()} />;
export default Page;

View file

@ -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} />;
}

View file

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

View file

@ -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"],
}; };

View file

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

View file

@ -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>
);
};

View file

@ -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>
);
}

View file

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

View file

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

View file

@ -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} />;
}

View file

@ -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) => ({

View 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;
`;

View file

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

View 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;

View file

@ -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} />;
}

View 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;

View file

@ -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} />;
}

View file

@ -0,0 +1,6 @@
import Page_ from "@ente/accounts/pages/credentials";
import { useAppContext } from "./_app";
const Page = () => <Page_ appContext={useAppContext()} />;
export default Page;

View file

@ -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} />;
}

View file

@ -0,0 +1,6 @@
import Page_ from "@ente/accounts/pages/generate";
import { useAppContext } from "./_app";
const Page = () => <Page_ appContext={useAppContext()} />;
export default Page;

View file

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