diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index cd158ee9d..2f53c55d3 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -4,11 +4,12 @@ labels: ["triage"] body: - type: markdown attributes: - value: > - Before opening a new issue, please ensure you are on the latest - version (it might've already been fixed), and that you've searched - for existing issues (please add you observations as a comment - there instead of creating a duplicate). + value: | + Before opening a new bug report, please ensure + 1. you are on the latest version (it might've already been fixed), + 2. you've searched for existing issues (please add your observations as a comment there instead of creating a duplicate). + + If you are self hosting, please create a community [Q&A](https://github.com/ente-io/ente/discussions/categories/q-a) instead. - type: textarea attributes: label: Description @@ -16,7 +17,8 @@ body: Please describe the bug. If possible, also include the steps to reproduce the behaviour, and the expected behaviour (sometimes bugs are just expectation mismatches, in which case this would be - a good fit for Discussions). + a good fit for [feature + requests](https://github.com/ente-io/ente/discussions/categories/feature-requests)). validations: required: true - type: input diff --git a/.github/workflows/mobile-internal-release.yml b/.github/workflows/mobile-internal-release.yml index c2e43d34f..fac4eb1d2 100644 --- a/.github/workflows/mobile-internal-release.yml +++ b/.github/workflows/mobile-internal-release.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: # Allow manually running the action env: - FLUTTER_VERSION: "3.19.3" + FLUTTER_VERSION: "3.22.0" jobs: build: diff --git a/.github/workflows/mobile-lint.yml b/.github/workflows/mobile-lint.yml index 493185b6b..3a43924a3 100644 --- a/.github/workflows/mobile-lint.yml +++ b/.github/workflows/mobile-lint.yml @@ -9,7 +9,7 @@ on: - ".github/workflows/mobile-lint.yml" env: - FLUTTER_VERSION: "3.19.5" + FLUTTER_VERSION: "3.22.0" jobs: lint: diff --git a/auth/lib/l10n/arb/app_ja.arb b/auth/lib/l10n/arb/app_ja.arb index 8fea34c5e..46ba7b9dd 100644 --- a/auth/lib/l10n/arb/app_ja.arb +++ b/auth/lib/l10n/arb/app_ja.arb @@ -20,6 +20,8 @@ "codeIssuerHint": "発行者", "codeSecretKeyHint": "秘密鍵", "codeAccountHint": "アカウント (you@domain.com)", + "codeTagHint": "タグ", + "accountKeyType": "鍵の種類", "sessionExpired": "セッションが失効しました", "@sessionExpired": { "description": "Title of the dialog when the users current session is invalid/expired" @@ -77,6 +79,7 @@ "data": "データ", "importCodes": "コードをインポート", "importTypePlainText": "プレーンテキスト", + "importTypeEnteEncrypted": "Ente 暗号化されたエクスポート", "passwordForDecryptingExport": "復号化用パスワード", "passwordEmptyError": "パスワードは空欄にできません", "importFromApp": "{appName} からコードをインポート", @@ -121,6 +124,7 @@ "suggestFeatures": "機能を提案", "faq": "FAQ", "faq_q_1": "Authはどのくらい安全ですか?", + "faq_a_1": "Ente Authでバックアップされたコードはすべてエンドツーエンドで暗号化されて保存されます。つまり、コードにアクセスできるのはあなただけです。当社のアプリはオープンソースであり、暗号化技術は外部監査を受けています。", "faq_q_2": "パソコンから私のコードにアクセスできますか?", "faq_a_2": "auth.ente.io で Web からコードにアクセス可能です。", "faq_q_3": "コードを削除するにはどうすればいいですか?", @@ -154,6 +158,7 @@ } } }, + "invalidQRCode": "QRコードが無効です", "noRecoveryKeyTitle": "回復キーがありませんか?", "enterEmailHint": "メールアドレスを入力してください", "invalidEmailTitle": "メールアドレスが無効です", @@ -347,6 +352,7 @@ "deleteCodeAuthMessage": "コードを削除するためには認証が必要です", "showQRAuthMessage": "QR コードを表示するためには認証が必要です", "confirmAccountDeleteTitle": "アカウントの削除に同意", + "confirmAccountDeleteMessage": "このアカウントは他のEnteアプリも使用している場合はそれらにも紐づけされています。\nすべてのEnteアプリでアップロードされたデータは削除され、アカウントは完全に削除されます。", "androidBiometricHint": "本人を確認する", "@androidBiometricHint": { "description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters." @@ -417,5 +423,18 @@ "invalidEndpoint": "無効なエンドポイントです", "invalidEndpointMessage": "入力されたエンドポイントは無効です。有効なエンドポイントを入力して再試行してください。", "endpointUpdatedMessage": "エンドポイントの更新に成功しました", - "customEndpoint": "{endpoint} に接続しました" + "customEndpoint": "{endpoint} に接続しました", + "pinText": "固定", + "unpinText": "固定を解除", + "pinnedCodeMessage": "{code} を固定しました", + "unpinnedCodeMessage": "{code} の固定が解除されました", + "tags": "タグ", + "createNewTag": "新しいタグの作成", + "tag": "タグ", + "create": "作成", + "editTag": "タグの編集", + "deleteTagTitle": "タグを削除しますか?", + "deleteTagMessage": "このタグを削除してもよろしいですか?この操作は取り消しできません。", + "somethingWentWrongParsingCode": "{x} のコードを解析できませんでした。", + "updateNotAvailable": "アップデートは利用できません" } \ No newline at end of file diff --git a/desktop/src/main/stream.ts b/desktop/src/main/stream.ts index 1c8223c87..c11fb1121 100644 --- a/desktop/src/main/stream.ts +++ b/desktop/src/main/stream.ts @@ -106,7 +106,7 @@ const handleRead = async (path: string) => { res.headers.set("Content-Length", `${fileSize}`); // Add the file's last modified time (as epoch milliseconds). - const mtimeMs = stat.mtimeMs; + const mtimeMs = stat.mtime.getTime(); res.headers.set("X-Last-Modified-Ms", `${mtimeMs}`); } return res; @@ -132,6 +132,13 @@ const handleReadZip = async (zipPath: string, entryName: string) => { // Close the zip handle when the underlying stream closes. stream.on("end", () => void zip.close()); + // While it is documented that entry.time is the modification time, + // the units are not mentioned. By seeing the source code, we can + // verify that it is indeed epoch milliseconds. See `parseZipTime` + // in the node-stream-zip source, + // https://github.com/antelle/node-stream-zip/blob/master/node_stream_zip.js + const modifiedMs = entry.time; + return new Response(webReadableStream, { headers: { // We don't know the exact type, but it doesn't really matter, just @@ -139,12 +146,7 @@ const handleReadZip = async (zipPath: string, entryName: string) => { // doesn't tinker with it thinking of it as text. "Content-Type": "application/octet-stream", "Content-Length": `${entry.size}`, - // While it is documented that entry.time is the modification time, - // the units are not mentioned. By seeing the source code, we can - // verify that it is indeed epoch milliseconds. See `parseZipTime` - // in the node-stream-zip source, - // https://github.com/antelle/node-stream-zip/blob/master/node_stream_zip.js - "X-Last-Modified-Ms": `${entry.time}`, + "X-Last-Modified-Ms": `${modifiedMs}`, }, }); }; diff --git a/docs/docs/.vitepress/sidebar.ts b/docs/docs/.vitepress/sidebar.ts index 84ae5e0fa..ef7ee47c4 100644 --- a/docs/docs/.vitepress/sidebar.ts +++ b/docs/docs/.vitepress/sidebar.ts @@ -163,6 +163,10 @@ export const sidebar = [ text: "From Authy", link: "/auth/migration-guides/authy/", }, + { + text: "From Steam", + link: "/auth/migration-guides/steam/", + }, { text: "Exporting your data", link: "/auth/migration-guides/export", diff --git a/docs/docs/auth/migration-guides/index.md b/docs/docs/auth/migration-guides/index.md index f10d9db41..3fb638ca1 100644 --- a/docs/docs/auth/migration-guides/index.md +++ b/docs/docs/auth/migration-guides/index.md @@ -7,4 +7,5 @@ description: # Migrating to/from Ente Auth - [Migrating from Authy](authy/) +- [Importing codes from Steam](steam/) - [Exporting your data out of Ente Auth](export) diff --git a/docs/docs/auth/migration-guides/steam/index.md b/docs/docs/auth/migration-guides/steam/index.md new file mode 100644 index 000000000..acb1f77aa --- /dev/null +++ b/docs/docs/auth/migration-guides/steam/index.md @@ -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\`). +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__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 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 diff --git a/docs/docs/self-hosting/guides/configuring-s3.md b/docs/docs/self-hosting/guides/configuring-s3.md index 8e823ed2a..56e922f02 100644 --- a/docs/docs/self-hosting/guides/configuring-s3.md +++ b/docs/docs/self-hosting/guides/configuring-s3.md @@ -78,3 +78,23 @@ To summarize: Set the S3 bucket `endpoint` in `credentials.yaml` to a `yourserverip:3200` or some such IP/hostname that accessible from both where you are running the Ente clients (e.g. the mobile app) and also from within the Docker compose cluster. + +### 403 Forbidden + +If museum (`2`) is able to make a network connection to your S3 bucket (`3`) but +uploads are still failing, it could be a credentials or permissions issue. A +telltale sign of this is that in the museum logs you can see `403 Forbidden` +errors about it not able to find the size of a file even though the +corresponding object exists in the S3 bucket. + +To fix these, you should ensure the following: + +1. The bucket CORS rules do not allow museum to access these objects. + + > For uploading files from the browser, you will need to currently set + > allowedOrigins to "\*", and allow the "X-Auth-Token", "X-Client-Package" + > headers configuration too. + > [Here is an example of a working configuration](https://github.com/ente-io/ente/discussions/1764#discussioncomment-9478204). + +2. The credentials are not being picked up (you might be setting the correct + creds, but not in the place where museum picks them from). diff --git a/mobile/README.md b/mobile/README.md index fc17f6b26..6d86ad534 100644 --- a/mobile/README.md +++ b/mobile/README.md @@ -46,7 +46,7 @@ You can alternatively install the build from PlayStore or F-Droid. ## 🧑‍💻 Building from source -1. [Install Flutter v3.19.3](https://flutter.dev/docs/get-started/install). +1. [Install Flutter v3.22.0](https://flutter.dev/docs/get-started/install). 2. Pull in all submodules with `git submodule update --init --recursive` diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock index 558a27910..9f74d552a 100644 --- a/mobile/ios/Podfile.lock +++ b/mobile/ios/Podfile.lock @@ -427,7 +427,7 @@ SPEC CHECKSUMS: home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57 image_editor_common: d6f6644ae4a6de80481e89fe6d0a8c49e30b4b43 in_app_purchase_storekit: 0e4b3c2e43ba1e1281f4f46dd71b0593ce529892 - integration_test: 13825b8a9334a850581300559b8839134b124670 + integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4 libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009 local_auth_darwin: c7e464000a6a89e952235699e32b329457608d98 local_auth_ios: 5046a18c018dd973247a0564496c8898dbb5adf9 diff --git a/mobile/lib/core/configuration.dart b/mobile/lib/core/configuration.dart index 4809ba863..8019e2a73 100644 --- a/mobile/lib/core/configuration.dart +++ b/mobile/lib/core/configuration.dart @@ -35,10 +35,10 @@ import 'package:photos/services/sync_service.dart'; import 'package:photos/utils/crypto_util.dart'; import 'package:photos/utils/file_uploader.dart'; import 'package:photos/utils/validator_util.dart'; +import "package:photos/utils/wakelock_util.dart"; import 'package:shared_preferences/shared_preferences.dart'; import "package:tuple/tuple.dart"; import 'package:uuid/uuid.dart'; -import 'package:wakelock_plus/wakelock_plus.dart'; class Configuration { Configuration._privateConstructor(); @@ -585,7 +585,7 @@ class Configuration { Future setShouldKeepDeviceAwake(bool value) async { await _preferences.setBool(keyShouldKeepDeviceAwake, value); - await WakelockPlus.toggle(enable: value); + await EnteWakeLock.toggle(enable: value); } Future setShouldBackupVideos(bool value) async { diff --git a/mobile/lib/core/constants.dart b/mobile/lib/core/constants.dart index 02923b6c4..3918541b5 100644 --- a/mobile/lib/core/constants.dart +++ b/mobile/lib/core/constants.dart @@ -69,6 +69,8 @@ const galleryGridSpacing = 2.0; const kSearchSectionLimit = 9; +const maxPickAssetLimit = 50; + const iOSGroupID = "group.io.ente.frame.SlideshowWidget"; const blackThumbnailBase64 = '/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEB' diff --git a/mobile/lib/face/db.dart b/mobile/lib/face/db.dart index c72b197b4..3ad90915d 100644 --- a/mobile/lib/face/db.dart +++ b/mobile/lib/face/db.dart @@ -13,6 +13,8 @@ import "package:photos/face/model/face.dart"; import "package:photos/models/file/file.dart"; import "package:photos/services/machine_learning/face_ml/face_clustering/face_info_for_clustering.dart"; import 'package:photos/services/machine_learning/face_ml/face_filtering/face_filtering_constants.dart'; +import "package:photos/services/machine_learning/face_ml/face_ml_result.dart"; +import "package:photos/utils/ml_util.dart"; import 'package:sqlite_async/sqlite_async.dart'; /// Stores all data for the FacesML-related features. The database can be accessed by `FaceMLDataDB.instance.database`. @@ -249,7 +251,7 @@ class FaceMLDataDB { final List fileId = [recentFileID]; int? avatarFileId; if (avatarFaceId != null) { - avatarFileId = int.tryParse(avatarFaceId.split('_')[0]); + avatarFileId = tryGetFileIdFromFaceId(avatarFaceId); if (avatarFileId != null) { fileId.add(avatarFileId); } @@ -401,8 +403,10 @@ class FaceMLDataDB { final personID = map[personIdColumn] as String; final clusterID = map[fcClusterID] as int; final faceID = map[fcFaceId] as String; - result.putIfAbsent(personID, () => {}).putIfAbsent(clusterID, () => {}) - .add(faceID); + result + .putIfAbsent(personID, () => {}) + .putIfAbsent(clusterID, () => {}) + .add(faceID); } return result; } @@ -476,8 +480,7 @@ class FaceMLDataDB { for (final map in maps) { final clusterID = map[fcClusterID] as int; final faceID = map[fcFaceId] as String; - final x = faceID.split('_').first; - final fileID = int.parse(x); + final fileID = getFileIdFromFaceId(faceID); result[fileID] = (result[fileID] ?? {})..add(clusterID); } return result; @@ -665,19 +668,55 @@ class FaceMLDataDB { return maps.first['count'] as int; } - Future getClusteredFaceCount() async { + Future getClusteredOrFacelessFileCount() async { final db = await instance.asyncDB; - final List> maps = await db.getAll( - 'SELECT COUNT(DISTINCT $fcFaceId) as count FROM $faceClustersTable', + final List> clustered = await db.getAll( + 'SELECT $fcFaceId FROM $faceClustersTable', ); - return maps.first['count'] as int; + final Set clusteredFileIDs = {}; + for (final map in clustered) { + final int fileID = getFileIdFromFaceId(map[fcFaceId] as String); + clusteredFileIDs.add(fileID); + } + + final List> badFacesFiles = await db.getAll( + 'SELECT DISTINCT $fileIDColumn FROM $facesTable WHERE $faceScore <= $kMinimumQualityFaceScore OR $faceBlur <= $kLaplacianHardThreshold', + ); + final Set badFileIDs = {}; + for (final map in badFacesFiles) { + badFileIDs.add(map[fileIDColumn] as int); + } + + final List> goodFacesFiles = await db.getAll( + 'SELECT DISTINCT $fileIDColumn FROM $facesTable WHERE $faceScore > $kMinimumQualityFaceScore AND $faceBlur > $kLaplacianHardThreshold', + ); + final Set goodFileIDs = {}; + for (final map in goodFacesFiles) { + goodFileIDs.add(map[fileIDColumn] as int); + } + final trulyFacelessFiles = badFileIDs.difference(goodFileIDs); + return clusteredFileIDs.length + trulyFacelessFiles.length; } - Future getClusteredToTotalFacesRatio() async { - final int totalFaces = await getTotalFaceCount(); - final int clusteredFaces = await getClusteredFaceCount(); + Future getClusteredToIndexableFilesRatio() async { + final int indexableFiles = (await getIndexableFileIDs()).length; + final int clusteredFiles = await getClusteredOrFacelessFileCount(); - return clusteredFaces / totalFaces; + return clusteredFiles / indexableFiles; + } + + Future 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> maps = await db.getAll(query); + return maps.length; } Future getBlurryFaceCount([ @@ -795,7 +834,7 @@ class FaceMLDataDB { for (final map in maps) { final clusterID = map[clusterIDColumn] as int; final String faceID = map[fcFaceId] as String; - final fileID = int.parse(faceID.split('_').first); + final fileID = getFileIdFromFaceId(faceID); result[fileID] = (result[fileID] ?? {})..add(clusterID); } return result; @@ -814,8 +853,8 @@ class FaceMLDataDB { final Map> result = {}; for (final map in maps) { final clusterID = map[fcClusterID] as int; - final faceId = map[fcFaceId] as String; - final fileID = int.parse(faceId.split("_").first); + final faceID = map[fcFaceId] as String; + final fileID = getFileIdFromFaceId(faceID); result[fileID] = (result[fileID] ?? {})..add(clusterID); } return result; @@ -964,7 +1003,7 @@ class FaceMLDataDB { final Map faceIDToClusterID = {}; for (final row in faceIdsResult) { final faceID = row[fcFaceId] as String; - if (fileIds.contains(faceID.split('_').first)) { + if (fileIds.contains(getFileIdFromFaceId(faceID))) { maxClusterID += 1; faceIDToClusterID[faceID] = maxClusterID; } @@ -990,7 +1029,7 @@ class FaceMLDataDB { final Map faceIDToClusterID = {}; for (final row in faceIdsResult) { final faceID = row[fcFaceId] as String; - if (fileIds.contains(faceID.split('_').first)) { + if (fileIds.contains(getFileIdFromFaceId(faceID))) { maxClusterID += 1; faceIDToClusterID[faceID] = maxClusterID; } diff --git a/mobile/lib/generated/intl/messages_cs.dart b/mobile/lib/generated/intl/messages_cs.dart index 4506011b1..6301af561 100644 --- a/mobile/lib/generated/intl/messages_cs.dart +++ b/mobile/lib/generated/intl/messages_cs.dart @@ -54,6 +54,8 @@ class MessageLookup extends MessageLookupByLibrary { "Please note that this will result in a higher bandwidth and battery usage until all items are indexed."), "fileTypes": MessageLookupByLibrary.simpleMessage("File types"), "foundFaces": MessageLookupByLibrary.simpleMessage("Found faces"), + "indexingIsPaused": MessageLookupByLibrary.simpleMessage( + "Indexing is paused, will automatically resume when device is ready"), "joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"), "locations": MessageLookupByLibrary.simpleMessage("Locations"), "longPressAnEmailToVerifyEndToEndEncryption": diff --git a/mobile/lib/generated/intl/messages_de.dart b/mobile/lib/generated/intl/messages_de.dart index 0ff50cfa4..49c7ac93c 100644 --- a/mobile/lib/generated/intl/messages_de.dart +++ b/mobile/lib/generated/intl/messages_de.dart @@ -819,6 +819,8 @@ class MessageLookup extends MessageLookupByLibrary { "Falscher Wiederherstellungs-Schlüssel"), "indexedItems": MessageLookupByLibrary.simpleMessage("Indizierte Elemente"), + "indexingIsPaused": MessageLookupByLibrary.simpleMessage( + "Indexing is paused, will automatically resume when device is ready"), "insecureDevice": MessageLookupByLibrary.simpleMessage("Unsicheres Gerät"), "installManually": diff --git a/mobile/lib/generated/intl/messages_en.dart b/mobile/lib/generated/intl/messages_en.dart index ee799aeb9..320df2c1d 100644 --- a/mobile/lib/generated/intl/messages_en.dart +++ b/mobile/lib/generated/intl/messages_en.dart @@ -813,6 +813,8 @@ class MessageLookup extends MessageLookupByLibrary { "incorrectRecoveryKeyTitle": MessageLookupByLibrary.simpleMessage("Incorrect recovery key"), "indexedItems": MessageLookupByLibrary.simpleMessage("Indexed items"), + "indexingIsPaused": MessageLookupByLibrary.simpleMessage( + "Indexing is paused, will automatically resume when device is ready"), "insecureDevice": MessageLookupByLibrary.simpleMessage("Insecure device"), "installManually": diff --git a/mobile/lib/generated/intl/messages_es.dart b/mobile/lib/generated/intl/messages_es.dart index 879f0f8c1..f0b4c87f5 100644 --- a/mobile/lib/generated/intl/messages_es.dart +++ b/mobile/lib/generated/intl/messages_es.dart @@ -699,6 +699,8 @@ class MessageLookup extends MessageLookupByLibrary { "La clave de recuperación introducida es incorrecta"), "incorrectRecoveryKeyTitle": MessageLookupByLibrary.simpleMessage( "Clave de recuperación incorrecta"), + "indexingIsPaused": MessageLookupByLibrary.simpleMessage( + "Indexing is paused, will automatically resume when device is ready"), "insecureDevice": MessageLookupByLibrary.simpleMessage("Dispositivo inseguro"), "installManually": diff --git a/mobile/lib/generated/intl/messages_fr.dart b/mobile/lib/generated/intl/messages_fr.dart index 47817371e..5c4a2b4e4 100644 --- a/mobile/lib/generated/intl/messages_fr.dart +++ b/mobile/lib/generated/intl/messages_fr.dart @@ -804,6 +804,8 @@ class MessageLookup extends MessageLookupByLibrary { "La clé de secours que vous avez entrée est incorrecte"), "incorrectRecoveryKeyTitle": MessageLookupByLibrary.simpleMessage("Clé de secours non valide"), + "indexingIsPaused": MessageLookupByLibrary.simpleMessage( + "Indexing is paused, will automatically resume when device is ready"), "insecureDevice": MessageLookupByLibrary.simpleMessage("Appareil non sécurisé"), "installManually": diff --git a/mobile/lib/generated/intl/messages_it.dart b/mobile/lib/generated/intl/messages_it.dart index 6dbae342c..d7a902db8 100644 --- a/mobile/lib/generated/intl/messages_it.dart +++ b/mobile/lib/generated/intl/messages_it.dart @@ -773,6 +773,8 @@ class MessageLookup extends MessageLookupByLibrary { "Il codice che hai inserito non è corretto"), "incorrectRecoveryKeyTitle": MessageLookupByLibrary.simpleMessage("Chiave di recupero errata"), + "indexingIsPaused": MessageLookupByLibrary.simpleMessage( + "Indexing is paused, will automatically resume when device is ready"), "insecureDevice": MessageLookupByLibrary.simpleMessage("Dispositivo non sicuro"), "installManually": diff --git a/mobile/lib/generated/intl/messages_ko.dart b/mobile/lib/generated/intl/messages_ko.dart index 65e26e631..614d860dc 100644 --- a/mobile/lib/generated/intl/messages_ko.dart +++ b/mobile/lib/generated/intl/messages_ko.dart @@ -54,6 +54,8 @@ class MessageLookup extends MessageLookupByLibrary { "Please note that this will result in a higher bandwidth and battery usage until all items are indexed."), "fileTypes": MessageLookupByLibrary.simpleMessage("File types"), "foundFaces": MessageLookupByLibrary.simpleMessage("Found faces"), + "indexingIsPaused": MessageLookupByLibrary.simpleMessage( + "Indexing is paused, will automatically resume when device is ready"), "joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"), "locations": MessageLookupByLibrary.simpleMessage("Locations"), "longPressAnEmailToVerifyEndToEndEncryption": diff --git a/mobile/lib/generated/intl/messages_nl.dart b/mobile/lib/generated/intl/messages_nl.dart index b0f7b601f..1981b338c 100644 --- a/mobile/lib/generated/intl/messages_nl.dart +++ b/mobile/lib/generated/intl/messages_nl.dart @@ -840,6 +840,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Onjuiste herstelsleutel"), "indexedItems": MessageLookupByLibrary.simpleMessage("Geïndexeerde bestanden"), + "indexingIsPaused": MessageLookupByLibrary.simpleMessage( + "Indexing is paused, will automatically resume when device is ready"), "insecureDevice": MessageLookupByLibrary.simpleMessage("Onveilig apparaat"), "installManually": diff --git a/mobile/lib/generated/intl/messages_no.dart b/mobile/lib/generated/intl/messages_no.dart index 88d2b1632..cce44555a 100644 --- a/mobile/lib/generated/intl/messages_no.dart +++ b/mobile/lib/generated/intl/messages_no.dart @@ -72,6 +72,8 @@ class MessageLookup extends MessageLookupByLibrary { "feedback": MessageLookupByLibrary.simpleMessage("Tilbakemelding"), "fileTypes": MessageLookupByLibrary.simpleMessage("File types"), "foundFaces": MessageLookupByLibrary.simpleMessage("Found faces"), + "indexingIsPaused": MessageLookupByLibrary.simpleMessage( + "Indexing is paused, will automatically resume when device is ready"), "invalidEmailAddress": MessageLookupByLibrary.simpleMessage("Ugyldig e-postadresse"), "joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"), diff --git a/mobile/lib/generated/intl/messages_pl.dart b/mobile/lib/generated/intl/messages_pl.dart index 096a0eb65..01cb3cb61 100644 --- a/mobile/lib/generated/intl/messages_pl.dart +++ b/mobile/lib/generated/intl/messages_pl.dart @@ -131,6 +131,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Kod jest nieprawidłowy"), "incorrectRecoveryKeyTitle": MessageLookupByLibrary.simpleMessage( "Nieprawidłowy klucz odzyskiwania"), + "indexingIsPaused": MessageLookupByLibrary.simpleMessage( + "Indexing is paused, will automatically resume when device is ready"), "invalidEmailAddress": MessageLookupByLibrary.simpleMessage("Nieprawidłowy adres e-mail"), "joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"), diff --git a/mobile/lib/generated/intl/messages_pt.dart b/mobile/lib/generated/intl/messages_pt.dart index aa17fc422..e32fd3637 100644 --- a/mobile/lib/generated/intl/messages_pt.dart +++ b/mobile/lib/generated/intl/messages_pt.dart @@ -98,7 +98,7 @@ class MessageLookup extends MessageLookupByLibrary { "${storageAmountInGB} GB cada vez que alguém se inscrever para um plano pago e aplica o seu código"; static String m25(freeAmount, storageUnit) => - "${freeAmount} ${storageUnit} grátis"; + "${freeAmount} ${storageUnit} livre"; static String m26(endDate) => "Teste gratuito acaba em ${endDate}"; @@ -225,6 +225,7 @@ class MessageLookup extends MessageLookupByLibrary { "Eu entendo que se eu perder minha senha, posso perder meus dados, já que meus dados são criptografados de ponta a ponta."), "activeSessions": MessageLookupByLibrary.simpleMessage("Sessões ativas"), + "addAName": MessageLookupByLibrary.simpleMessage("Adicione um nome"), "addANewEmail": MessageLookupByLibrary.simpleMessage("Adicionar um novo email"), "addCollaborator": @@ -446,7 +447,7 @@ class MessageLookup extends MessageLookupByLibrary { "clubByFileName": MessageLookupByLibrary.simpleMessage( "Agrupar pelo nome de arquivo"), "clusteringProgress": - MessageLookupByLibrary.simpleMessage("Clustering progress"), + MessageLookupByLibrary.simpleMessage("Progresso de agrupamento"), "codeAppliedPageTitle": MessageLookupByLibrary.simpleMessage("Código aplicado"), "codeCopiedToClipboard": MessageLookupByLibrary.simpleMessage( @@ -692,6 +693,8 @@ class MessageLookup extends MessageLookupByLibrary { "enterPassword": MessageLookupByLibrary.simpleMessage("Digite a senha"), "enterPasswordToEncrypt": MessageLookupByLibrary.simpleMessage( "Insira a senha para criptografar seus dados"), + "enterPersonName": + MessageLookupByLibrary.simpleMessage("Inserir nome da pessoa"), "enterReferralCode": MessageLookupByLibrary.simpleMessage( "Insira o código de referência"), "enterThe6digitCodeFromnyourAuthenticatorApp": @@ -717,9 +720,9 @@ class MessageLookup extends MessageLookupByLibrary { "exportYourData": MessageLookupByLibrary.simpleMessage("Exportar seus dados"), "faceRecognition": - MessageLookupByLibrary.simpleMessage("Face recognition"), + MessageLookupByLibrary.simpleMessage("Reconhecimento facial"), "faceRecognitionIndexingDescription": MessageLookupByLibrary.simpleMessage( - "Please note that this will result in a higher bandwidth and battery usage until all items are indexed."), + "Por favor, note que isso resultará em uma largura de banda maior e uso de bateria até que todos os itens sejam indexados."), "faces": MessageLookupByLibrary.simpleMessage("Rostos"), "failedToApplyCode": MessageLookupByLibrary.simpleMessage("Falha ao aplicar o código"), @@ -761,12 +764,15 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Arquivos excluídos"), "filesSavedToGallery": MessageLookupByLibrary.simpleMessage("Arquivos salvos na galeria"), + "findPeopleByName": MessageLookupByLibrary.simpleMessage( + "Encontre pessoas rapidamente por nome"), "flip": MessageLookupByLibrary.simpleMessage("Inverter"), "forYourMemories": MessageLookupByLibrary.simpleMessage("para suas memórias"), "forgotPassword": MessageLookupByLibrary.simpleMessage("Esqueceu sua senha"), - "foundFaces": MessageLookupByLibrary.simpleMessage("Found faces"), + "foundFaces": + MessageLookupByLibrary.simpleMessage("Rostos encontrados"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage( "Armazenamento gratuito reivindicado"), "freeStorageOnReferralSuccess": m24, @@ -830,6 +836,8 @@ class MessageLookup extends MessageLookupByLibrary { "incorrectRecoveryKeyTitle": MessageLookupByLibrary.simpleMessage( "Chave de recuperação incorreta"), "indexedItems": MessageLookupByLibrary.simpleMessage("Itens indexados"), + "indexingIsPaused": MessageLookupByLibrary.simpleMessage( + "Indexing is paused, will automatically resume when device is ready"), "insecureDevice": MessageLookupByLibrary.simpleMessage("Dispositivo não seguro"), "installManually": @@ -1064,6 +1072,7 @@ class MessageLookup extends MessageLookupByLibrary { "pendingItems": MessageLookupByLibrary.simpleMessage("Itens pendentes"), "pendingSync": MessageLookupByLibrary.simpleMessage("Sincronização pendente"), + "people": MessageLookupByLibrary.simpleMessage("Pessoas"), "peopleUsingYourCode": MessageLookupByLibrary.simpleMessage("Pessoas que usam seu código"), "permDeleteWarning": MessageLookupByLibrary.simpleMessage( @@ -1197,6 +1206,8 @@ class MessageLookup extends MessageLookupByLibrary { "removeParticipant": MessageLookupByLibrary.simpleMessage("Remover participante"), "removeParticipantBody": m43, + "removePersonLabel": + MessageLookupByLibrary.simpleMessage("Remover etiqueta da pessoa"), "removePublicLink": MessageLookupByLibrary.simpleMessage("Remover link público"), "removeShareItemsWarning": MessageLookupByLibrary.simpleMessage( @@ -1260,7 +1271,7 @@ class MessageLookup extends MessageLookupByLibrary { "searchDatesEmptySection": MessageLookupByLibrary.simpleMessage( "Pesquisar por data, mês ou ano"), "searchFaceEmptySection": MessageLookupByLibrary.simpleMessage( - "Encontre todas as fotos de uma pessoa"), + "Pessoas serão exibidas aqui uma vez que a indexação é feita"), "searchFileTypesAndNamesEmptySection": MessageLookupByLibrary.simpleMessage("Tipos de arquivo e nomes"), "searchHint1": MessageLookupByLibrary.simpleMessage( diff --git a/mobile/lib/generated/intl/messages_zh.dart b/mobile/lib/generated/intl/messages_zh.dart index 63b8668b5..ecca5d7b8 100644 --- a/mobile/lib/generated/intl/messages_zh.dart +++ b/mobile/lib/generated/intl/messages_zh.dart @@ -686,6 +686,8 @@ class MessageLookup extends MessageLookupByLibrary { "incorrectRecoveryKeyTitle": MessageLookupByLibrary.simpleMessage("不正确的恢复密钥"), "indexedItems": MessageLookupByLibrary.simpleMessage("已索引项目"), + "indexingIsPaused": MessageLookupByLibrary.simpleMessage( + "Indexing is paused, will automatically resume when device is ready"), "insecureDevice": MessageLookupByLibrary.simpleMessage("设备不安全"), "installManually": MessageLookupByLibrary.simpleMessage("手动安装"), "invalidEmailAddress": diff --git a/mobile/lib/generated/l10n.dart b/mobile/lib/generated/l10n.dart index 4e2c53e29..de8922161 100644 --- a/mobile/lib/generated/l10n.dart +++ b/mobile/lib/generated/l10n.dart @@ -8793,6 +8793,16 @@ class S { 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 { diff --git a/mobile/lib/l10n/intl_cs.arb b/mobile/lib/l10n/intl_cs.arb index 449bdb760..024197d9e 100644 --- a/mobile/lib/l10n/intl_cs.arb +++ b/mobile/lib/l10n/intl_cs.arb @@ -24,5 +24,6 @@ "faceRecognition": "Face recognition", "faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.", "foundFaces": "Found faces", - "clusteringProgress": "Clustering progress" + "clusteringProgress": "Clustering progress", + "indexingIsPaused": "Indexing is paused, will automatically resume when device is ready" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_de.arb b/mobile/lib/l10n/intl_de.arb index acee623ab..913af46b6 100644 --- a/mobile/lib/l10n/intl_de.arb +++ b/mobile/lib/l10n/intl_de.arb @@ -1212,5 +1212,6 @@ "faceRecognition": "Face recognition", "faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.", "foundFaces": "Found faces", - "clusteringProgress": "Clustering progress" + "clusteringProgress": "Clustering progress", + "indexingIsPaused": "Indexing is paused, will automatically resume when device is ready" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_en.arb b/mobile/lib/l10n/intl_en.arb index 0fe06c95a..08e794074 100644 --- a/mobile/lib/l10n/intl_en.arb +++ b/mobile/lib/l10n/intl_en.arb @@ -1235,5 +1235,6 @@ "faceRecognition": "Face recognition", "faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.", "foundFaces": "Found faces", - "clusteringProgress": "Clustering progress" + "clusteringProgress": "Clustering progress", + "indexingIsPaused": "Indexing is paused, will automatically resume when device is ready" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_es.arb b/mobile/lib/l10n/intl_es.arb index a472aaf8e..22acb2b33 100644 --- a/mobile/lib/l10n/intl_es.arb +++ b/mobile/lib/l10n/intl_es.arb @@ -986,5 +986,6 @@ "faceRecognition": "Face recognition", "faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.", "foundFaces": "Found faces", - "clusteringProgress": "Clustering progress" + "clusteringProgress": "Clustering progress", + "indexingIsPaused": "Indexing is paused, will automatically resume when device is ready" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_fr.arb b/mobile/lib/l10n/intl_fr.arb index a5b2f2fd0..90c0ad80e 100644 --- a/mobile/lib/l10n/intl_fr.arb +++ b/mobile/lib/l10n/intl_fr.arb @@ -1167,5 +1167,6 @@ "faceRecognition": "Face recognition", "faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.", "foundFaces": "Found faces", - "clusteringProgress": "Clustering progress" + "clusteringProgress": "Clustering progress", + "indexingIsPaused": "Indexing is paused, will automatically resume when device is ready" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_it.arb b/mobile/lib/l10n/intl_it.arb index e81ac6377..071933ae5 100644 --- a/mobile/lib/l10n/intl_it.arb +++ b/mobile/lib/l10n/intl_it.arb @@ -1129,5 +1129,6 @@ "faceRecognition": "Face recognition", "faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.", "foundFaces": "Found faces", - "clusteringProgress": "Clustering progress" + "clusteringProgress": "Clustering progress", + "indexingIsPaused": "Indexing is paused, will automatically resume when device is ready" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_ko.arb b/mobile/lib/l10n/intl_ko.arb index 449bdb760..024197d9e 100644 --- a/mobile/lib/l10n/intl_ko.arb +++ b/mobile/lib/l10n/intl_ko.arb @@ -24,5 +24,6 @@ "faceRecognition": "Face recognition", "faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.", "foundFaces": "Found faces", - "clusteringProgress": "Clustering progress" + "clusteringProgress": "Clustering progress", + "indexingIsPaused": "Indexing is paused, will automatically resume when device is ready" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_nl.arb b/mobile/lib/l10n/intl_nl.arb index 682aee259..f54f6b604 100644 --- a/mobile/lib/l10n/intl_nl.arb +++ b/mobile/lib/l10n/intl_nl.arb @@ -1230,5 +1230,6 @@ "faceRecognition": "Face recognition", "faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.", "foundFaces": "Found faces", - "clusteringProgress": "Clustering progress" + "clusteringProgress": "Clustering progress", + "indexingIsPaused": "Indexing is paused, will automatically resume when device is ready" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_no.arb b/mobile/lib/l10n/intl_no.arb index 697a9f3c4..40085833b 100644 --- a/mobile/lib/l10n/intl_no.arb +++ b/mobile/lib/l10n/intl_no.arb @@ -38,5 +38,6 @@ "faceRecognition": "Face recognition", "faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.", "foundFaces": "Found faces", - "clusteringProgress": "Clustering progress" + "clusteringProgress": "Clustering progress", + "indexingIsPaused": "Indexing is paused, will automatically resume when device is ready" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_pl.arb b/mobile/lib/l10n/intl_pl.arb index f9b66901e..b3eb77879 100644 --- a/mobile/lib/l10n/intl_pl.arb +++ b/mobile/lib/l10n/intl_pl.arb @@ -125,5 +125,6 @@ "faceRecognition": "Face recognition", "faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.", "foundFaces": "Found faces", - "clusteringProgress": "Clustering progress" + "clusteringProgress": "Clustering progress", + "indexingIsPaused": "Indexing is paused, will automatically resume when device is ready" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_pt.arb b/mobile/lib/l10n/intl_pt.arb index f47dd89e9..428dbf5fc 100644 --- a/mobile/lib/l10n/intl_pt.arb +++ b/mobile/lib/l10n/intl_pt.arb @@ -987,7 +987,7 @@ "fileTypesAndNames": "Tipos de arquivo e nomes", "location": "Local", "moments": "Momentos", - "searchFaceEmptySection": "Encontre todas as fotos de uma pessoa", + "searchFaceEmptySection": "Pessoas serão exibidas aqui uma vez que a indexação é feita", "searchDatesEmptySection": "Pesquisar por data, mês ou ano", "searchLocationEmptySection": "Fotos de grupo que estão sendo tiradas em algum raio da foto", "searchPeopleEmptySection": "Convide pessoas e você verá todas as fotos compartilhadas por elas aqui", @@ -1042,7 +1042,7 @@ "@storageUsageInfo": { "description": "Example: 1.2 GB of 2 GB used or 100 GB or 2TB used" }, - "freeStorageSpace": "{freeAmount} {storageUnit} grátis", + "freeStorageSpace": "{freeAmount} {storageUnit} livre", "appVersion": "Versão: {versionValue}", "verifyIDLabel": "Verificar", "fileInfoAddDescHint": "Adicionar descrição...", @@ -1171,6 +1171,7 @@ } }, "faces": "Rostos", + "people": "Pessoas", "contents": "Conteúdos", "addNew": "Adicionar novo", "@addNew": { @@ -1196,14 +1197,14 @@ "verifyPasskey": "Verificar chave de acesso", "playOnTv": "Reproduzir álbum na TV", "pair": "Parear", - "autoPair": "Pareamento automático", - "pairWithPin": "Parear com PIN", "deviceNotFound": "Dispositivo não encontrado", "castInstruction": "Visite cast.ente.io no dispositivo que você deseja parear.\n\ndigite o código abaixo para reproduzir o álbum em sua TV.", "deviceCodeHint": "Insira o código", "joinDiscord": "Junte-se ao Discord", "locations": "Locais", "descriptions": "Descrições", + "addAName": "Adicione um nome", + "findPeopleByName": "Encontre pessoas rapidamente por nome", "addViewers": "{count, plural, zero {Adicionar visualizador} one {Adicionar visualizador} other {Adicionar Visualizadores}}", "addCollaborators": "{count, plural, zero {Adicionar colaborador} one {Adicionar coloborador} other {Adicionar colaboradores}}", "longPressAnEmailToVerifyEndToEndEncryption": "Pressione e segure um e-mail para verificar a criptografia de ponta a ponta.", @@ -1216,6 +1217,8 @@ "customEndpoint": "Conectado a {endpoint}", "createCollaborativeLink": "Criar link colaborativo", "search": "Pesquisar", + "enterPersonName": "Inserir nome da pessoa", + "removePersonLabel": "Remover etiqueta da pessoa", "autoPairDesc": "O pareamento automático funciona apenas com dispositivos que suportam o Chromecast.", "manualPairDesc": "Parear com o PIN funciona com qualquer tela que você deseja ver o seu álbum ativado.", "connectToDevice": "Conectar ao dispositivo", @@ -1227,8 +1230,11 @@ "castIPMismatchTitle": "Falha ao transmitir álbum", "castIPMismatchBody": "Certifique-se de estar na mesma rede que a TV.", "pairingComplete": "Pareamento concluído", - "faceRecognition": "Face recognition", - "faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.", - "foundFaces": "Found faces", - "clusteringProgress": "Clustering progress" + "autoPair": "Pareamento automático", + "pairWithPin": "Parear com PIN", + "faceRecognition": "Reconhecimento facial", + "faceRecognitionIndexingDescription": "Por favor, note que isso resultará em uma largura de banda maior e uso de bateria até que todos os itens sejam indexados.", + "foundFaces": "Rostos encontrados", + "clusteringProgress": "Progresso de agrupamento", + "indexingIsPaused": "Indexing is paused, will automatically resume when device is ready" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_zh.arb b/mobile/lib/l10n/intl_zh.arb index 933eea126..81fd22914 100644 --- a/mobile/lib/l10n/intl_zh.arb +++ b/mobile/lib/l10n/intl_zh.arb @@ -1230,5 +1230,6 @@ "faceRecognition": "Face recognition", "faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.", "foundFaces": "Found faces", - "clusteringProgress": "Clustering progress" + "clusteringProgress": "Clustering progress", + "indexingIsPaused": "Indexing is paused, will automatically resume when device is ready" } \ No newline at end of file diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 50de0b9a1..247ab9553 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -51,6 +51,7 @@ import 'package:photos/services/user_service.dart'; import 'package:photos/ui/tools/app_lock.dart'; import 'package:photos/ui/tools/lock_screen.dart'; import 'package:photos/utils/crypto_util.dart'; +import "package:photos/utils/email_util.dart"; import 'package:photos/utils/file_uploader.dart'; import 'package:photos/utils/local_settings.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -180,6 +181,16 @@ void _headlessTaskHandler(HeadlessTask task) { } Future _init(bool isBackground, {String via = ''}) async { + bool initComplete = false; + Future.delayed(const Duration(seconds: 15), () { + if (!initComplete && !isBackground) { + sendLogsForInit( + "support@ente.io", + "Stuck on splash screen for >= 15 seconds", + null, + ); + } + }); _isProcessRunning = true; _logger.info("Initializing... inBG =$isBackground via: $via"); final SharedPreferences preferences = await SharedPreferences.getInstance(); @@ -254,6 +265,7 @@ Future _init(bool isBackground, {String via = ''}) async { preferences, ); + initComplete = true; _logger.info("Initialization done"); } diff --git a/mobile/lib/services/machine_learning/face_ml/face_clustering/face_clustering_service.dart b/mobile/lib/services/machine_learning/face_ml/face_clustering/face_clustering_service.dart index 1b8d9c3bd..1a635b0f0 100644 --- a/mobile/lib/services/machine_learning/face_ml/face_clustering/face_clustering_service.dart +++ b/mobile/lib/services/machine_learning/face_ml/face_clustering/face_clustering_service.dart @@ -498,19 +498,8 @@ class FaceClusteringService { } } - // Sort the faceInfos based on fileCreationTime, in ascending order, so oldest faces are first if (fileIDToCreationTime != null) { - faceInfos.sort((a, b) { - if (a.fileCreationTime == null && b.fileCreationTime == null) { - return 0; - } else if (a.fileCreationTime == null) { - return 1; - } else if (b.fileCreationTime == null) { - return -1; - } else { - return a.fileCreationTime!.compareTo(b.fileCreationTime!); - } - }); + _sortFaceInfosOnCreationTime(faceInfos); } // Sort the faceInfos such that the ones with null clusterId are at the end @@ -796,19 +785,8 @@ class FaceClusteringService { ); } - // Sort the faceInfos based on fileCreationTime, in ascending order, so oldest faces are first if (fileIDToCreationTime != null) { - faceInfos.sort((a, b) { - if (a.fileCreationTime == null && b.fileCreationTime == null) { - return 0; - } else if (a.fileCreationTime == null) { - return 1; - } else if (b.fileCreationTime == null) { - return -1; - } else { - return a.fileCreationTime!.compareTo(b.fileCreationTime!); - } - }); + _sortFaceInfosOnCreationTime(faceInfos); } if (faceInfos.isEmpty) { @@ -996,19 +974,8 @@ class FaceClusteringService { ); } - // Sort the faceInfos based on fileCreationTime, in ascending order, so oldest faces are first if (fileIDToCreationTime != null) { - faceInfos.sort((a, b) { - if (a.fileCreationTime == null && b.fileCreationTime == null) { - return 0; - } else if (a.fileCreationTime == null) { - return 1; - } else if (b.fileCreationTime == null) { - return -1; - } else { - return a.fileCreationTime!.compareTo(b.fileCreationTime!); - } - }); + _sortFaceInfosOnCreationTime(faceInfos); } // Get the embeddings @@ -1027,3 +994,20 @@ class FaceClusteringService { return clusteredFaceIDs; } } + +/// Sort the faceInfos based on fileCreationTime, in descending order, so newest faces are first +void _sortFaceInfosOnCreationTime( + List 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!); + } + }); +} diff --git a/mobile/lib/services/machine_learning/face_ml/face_ml_exceptions.dart b/mobile/lib/services/machine_learning/face_ml/face_ml_exceptions.dart index 78a4bcb1f..2c43e6e56 100644 --- a/mobile/lib/services/machine_learning/face_ml/face_ml_exceptions.dart +++ b/mobile/lib/services/machine_learning/face_ml/face_ml_exceptions.dart @@ -8,6 +8,18 @@ class GeneralFaceMlException implements Exception { String toString() => 'GeneralFaceMlException: $message'; } +class ThumbnailRetrievalException implements Exception { + final String message; + final StackTrace stackTrace; + + ThumbnailRetrievalException(this.message, this.stackTrace); + + @override + String toString() { + return 'ThumbnailRetrievalException: $message\n$stackTrace'; + } +} + class CouldNotRetrieveAnyFileData implements Exception {} class CouldNotInitializeFaceDetector implements Exception {} diff --git a/mobile/lib/services/machine_learning/face_ml/face_ml_result.dart b/mobile/lib/services/machine_learning/face_ml/face_ml_result.dart index 19f954013..9f87b8722 100644 --- a/mobile/lib/services/machine_learning/face_ml/face_ml_result.dart +++ b/mobile/lib/services/machine_learning/face_ml/face_ml_result.dart @@ -310,5 +310,9 @@ class FaceResultBuilder { } int getFileIdFromFaceId(String faceId) { - return int.parse(faceId.split("_")[0]); + return int.parse(faceId.split("_").first); } + +int? tryGetFileIdFromFaceId(String faceId) { + return int.tryParse(faceId.split("_").first); +} \ No newline at end of file diff --git a/mobile/lib/services/machine_learning/face_ml/face_ml_service.dart b/mobile/lib/services/machine_learning/face_ml/face_ml_service.dart index 165a695ed..bbe719dbe 100644 --- a/mobile/lib/services/machine_learning/face_ml/face_ml_service.dart +++ b/mobile/lib/services/machine_learning/face_ml/face_ml_service.dart @@ -9,10 +9,10 @@ import "dart:ui" show Image; import "package:computer/computer.dart"; import "package:dart_ui_isolate/dart_ui_isolate.dart"; import "package:flutter/foundation.dart" show debugPrint, kDebugMode; +import "package:flutter/services.dart"; import "package:logging/logging.dart"; import "package:onnxruntime/onnxruntime.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/db/files_db.dart"; import "package:photos/events/diff_sync_complete_event.dart"; @@ -97,8 +97,9 @@ class FaceMlService { bool _shouldSyncPeople = false; bool _isSyncing = false; - final int _fileDownloadLimit = 10; + final int _fileDownloadLimit = 5; final int _embeddingFetchLimit = 200; + final int _kForceClusteringFaceCount = 4000; Future init({bool initializeImageMlIsolate = false}) async { if (LocalSettings.instance.isFaceIndexingEnabled == false) { @@ -109,6 +110,7 @@ class FaceMlService { return; } _logger.info("init called"); + _logStatus(); await _computer.compute(initOrtEnv); try { await FaceDetectionService.instance.init(); @@ -152,8 +154,8 @@ class FaceMlService { _logger.info( "MLController allowed running ML, faces indexing starting", ); - unawaited(indexAndClusterAll()); } + unawaited(indexAndClusterAll()); } else { _logger.info( "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. + @pragma('vm:entry-point') static void _isolateMain(SendPort mainSendPort) async { final receivePort = ReceivePort(); mainSendPort.send(receivePort.sendPort); @@ -287,10 +290,6 @@ class FaceMlService { return _functionLock.synchronized(() async { _resetInactivityTimer(); - if (_shouldPauseIndexingAndClustering == false) { - return null; - } - final completer = Completer(); final answerPort = ReceivePort(); @@ -360,16 +359,17 @@ class FaceMlService { if (_cannotRunMLFunction()) return; await sync(forceSync: _shouldSyncPeople); - await indexAllImages(); - final indexingCompleteRatio = await _getIndexedDoneRatio(); - if (indexingCompleteRatio < 0.95) { + + final int unclusteredFacesCount = + await FaceMLDataDB.instance.getUnclusteredFaceCount(); + if (unclusteredFacesCount > _kForceClusteringFaceCount) { _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 indexAllImages(); + await clusterAllImages(); } void pauseIndexingAndClustering() { @@ -447,7 +447,8 @@ class FaceMlService { if (LocalSettings.instance.remoteFetchEnabled) { try { - final List fileIds = []; + final Set fileIds = + {}; // if there are duplicates here server returns 400 // Try to find embeddings on the remote server for (final f in chunk) { fileIds.add(f.uploadedFileID!); @@ -512,12 +513,19 @@ class FaceMlService { rethrow; } } - } - if (!await canUseHighBandwidth()) { - continue; + } else { + _logger.warning( + 'Not fetching embeddings because user manually disabled it in debug options', + ); } final smallerChunks = chunk.chunks(_fileDownloadLimit); 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) { if (_shouldPauseIndexingAndClustering) { _logger.info("indexAllImages() was paused, stopping"); @@ -543,8 +551,9 @@ class FaceMlService { stopwatch.stop(); _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) { _logger.severe("indexAllImages failed", e, s); } finally { @@ -584,8 +593,8 @@ class FaceMlService { allFaceInfoForClustering.add(faceInfo); } } - // sort the embeddings based on file creation time, oldest first - allFaceInfoForClustering.sort((a, b) { + // sort the embeddings based on file creation time, newest first + allFaceInfoForClustering.sort((b, a) { return fileIDToCreationTime[a.fileID]! .compareTo(fileIDToCreationTime[b.fileID]!); }); @@ -758,6 +767,9 @@ class FaceMlService { // disposeImageIsolateAfterUse: false, ); if (result == null) { + _logger.severe( + "Failed to analyze image with uploadedFileID: ${enteFile.uploadedFileID}", + ); return false; } final List faces = []; @@ -834,13 +846,22 @@ class FaceMlService { } await FaceMLDataDB.instance.bulkInsertFaces(faces); 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) { _logger.severe( "Failed to analyze using FaceML for image with ID: ${enteFile.uploadedFileID}", e, s, ); - return true; + return false; } } @@ -877,6 +898,7 @@ class FaceMlService { ), ) as String?; if (resultJsonString == null) { + _logger.severe('Analyzing image in isolate is giving back null'); return null; } result = FaceMlResult.fromJsonString(resultJsonString); @@ -993,7 +1015,12 @@ class FaceMlService { final stopwatch = Stopwatch()..start(); File? file; if (enteFile.fileType == FileType.video) { + try { 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 { file = await getFile(enteFile, isOrigin: true); // TODO: This is returning null for Pragadees for all files, so something is wrong here! @@ -1161,24 +1188,6 @@ class FaceMlService { } } - Future _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> getIndexableFileIDs() async { - return FilesDB.instance - .getOwnedFileIDs(Configuration.instance.getUserID()!); - } - bool _skipAnalysisEnteFile(EnteFile enteFile, Map indexedFileIds) { if (_isIndexingOrClusteringRunning == false || _mlControllerStatus == false) { diff --git a/mobile/lib/services/machine_learning/file_ml/remote_fileml_service.dart b/mobile/lib/services/machine_learning/file_ml/remote_fileml_service.dart index eafbc6323..4712916d0 100644 --- a/mobile/lib/services/machine_learning/file_ml/remote_fileml_service.dart +++ b/mobile/lib/services/machine_learning/file_ml/remote_fileml_service.dart @@ -1,6 +1,7 @@ import "dart:async"; import "dart:convert"; +import "package:computer/computer.dart"; import "package:logging/logging.dart"; import "package:photos/core/network/network.dart"; import "package:photos/db/files_db.dart"; @@ -16,6 +17,8 @@ import "package:shared_preferences/shared_preferences.dart"; class RemoteFileMLService { RemoteFileMLService._privateConstructor(); + static final Computer _computer = Computer.shared(); + static final RemoteFileMLService instance = RemoteFileMLService._privateConstructor(); @@ -52,13 +55,13 @@ class RemoteFileMLService { } Future getFilessEmbedding( - List fileIds, + Set fileIds, ) async { try { final res = await _dio.post( "/embeddings/files", data: { - "fileIDs": fileIds, + "fileIDs": fileIds.toList(), "model": 'file-ml-clip-face', }, ); @@ -107,15 +110,17 @@ class RemoteFileMLService { final input = EmbeddingsDecoderInput(embedding, fileKey); inputs.add(input); } - // todo: use compute or isolate - return decryptFileMLComputer( - { + return _computer.compute, Map>( + _decryptFileMLComputer, + param: { "inputs": inputs, }, ); } - Future> decryptFileMLComputer( +} + +Future> _decryptFileMLComputer( Map args, ) async { final result = {}; @@ -134,5 +139,4 @@ class RemoteFileMLService { result[input.embedding.fileID] = decodedEmbedding; } return result; - } -} + } \ No newline at end of file diff --git a/mobile/lib/services/machine_learning/machine_learning_controller.dart b/mobile/lib/services/machine_learning/machine_learning_controller.dart index 852ebcd5b..1b70ea48d 100644 --- a/mobile/lib/services/machine_learning/machine_learning_controller.dart +++ b/mobile/lib/services/machine_learning/machine_learning_controller.dart @@ -28,6 +28,8 @@ class MachineLearningController { bool _canRunML = false; late Timer _userInteractionTimer; + bool get isDeviceHealthy => _isDeviceHealthy; + void init() { if (Platform.isAndroid) { _startInteractionTimer(); diff --git a/mobile/lib/services/machine_learning/semantic_search/semantic_search_service.dart b/mobile/lib/services/machine_learning/semantic_search/semantic_search_service.dart index db1713c2c..138475081 100644 --- a/mobile/lib/services/machine_learning/semantic_search/semantic_search_service.dart +++ b/mobile/lib/services/machine_learning/semantic_search/semantic_search_service.dart @@ -23,6 +23,7 @@ import 'package:photos/services/machine_learning/semantic_search/frameworks/onnx import "package:photos/utils/debouncer.dart"; import "package:photos/utils/device_info.dart"; import "package:photos/utils/local_settings.dart"; +import "package:photos/utils/ml_util.dart"; import "package:photos/utils/thumbnail_util.dart"; class SemanticSearchService { @@ -160,8 +161,7 @@ class SemanticSearchService { } Future getIndexStatus() async { - final indexableFileIDs = await FilesDB.instance - .getOwnedFileIDs(Configuration.instance.getUserID()!); + final indexableFileIDs = await getIndexableFileIDs(); return IndexStatus( min(_cachedEmbeddings.length, indexableFileIDs.length), (await _getFileIDsToBeIndexed()).length, @@ -222,8 +222,7 @@ class SemanticSearchService { } Future> _getFileIDsToBeIndexed() async { - final uploadedFileIDs = await FilesDB.instance - .getOwnedFileIDs(Configuration.instance.getUserID()!); + final uploadedFileIDs = await getIndexableFileIDs(); final embeddedFileIDs = await EmbeddingsDB.instance.getFileIDs(_currentModel); diff --git a/mobile/lib/services/search_service.dart b/mobile/lib/services/search_service.dart index 1ff73dbc8..d15eddb71 100644 --- a/mobile/lib/services/search_service.dart +++ b/mobile/lib/services/search_service.dart @@ -754,15 +754,6 @@ class SearchService { Future> getAllFace(int? limit) async { try { - // Don't return anything if clustering is not nearly complete yet - final foundFaces = await FaceMLDataDB.instance.getTotalFaceCount(); - final clusteredFaces = - await FaceMLDataDB.instance.getClusteredFaceCount(); - final clusteringDoneRatio = clusteredFaces / foundFaces; - if (clusteringDoneRatio < 0.9) { - return []; - } - debugPrint("getting faces"); final Map> fileIdToClusterID = await FaceMLDataDB.instance.getFileIdToClusterIds(); diff --git a/mobile/lib/ui/settings/debug/face_debug_section_widget.dart b/mobile/lib/ui/settings/debug/face_debug_section_widget.dart index 726a9f2ce..376793769 100644 --- a/mobile/lib/ui/settings/debug/face_debug_section_widget.dart +++ b/mobile/lib/ui/settings/debug/face_debug_section_widget.dart @@ -177,7 +177,7 @@ class _FaceDebugSectionWidgetState extends State { sectionOptionSpacing, MenuItemWidget( captionedTextWidget: FutureBuilder( - future: FaceMLDataDB.instance.getClusteredToTotalFacesRatio(), + future: FaceMLDataDB.instance.getClusteredToIndexableFilesRatio(), builder: (context, snapshot) { if (snapshot.hasData) { return CaptionedTextWidget( diff --git a/mobile/lib/ui/settings/machine_learning_settings_page.dart b/mobile/lib/ui/settings/machine_learning_settings_page.dart index 47e216628..0ea1588a0 100644 --- a/mobile/lib/ui/settings/machine_learning_settings_page.dart +++ b/mobile/lib/ui/settings/machine_learning_settings_page.dart @@ -11,6 +11,7 @@ import "package:photos/generated/l10n.dart"; import "package:photos/models/ml/ml_versions.dart"; import "package:photos/service_locator.dart"; import "package:photos/services/machine_learning/face_ml/face_ml_service.dart"; +import "package:photos/services/machine_learning/machine_learning_controller.dart"; import 'package:photos/services/machine_learning/semantic_search/frameworks/ml_framework.dart'; import 'package:photos/services/machine_learning/semantic_search/semantic_search_service.dart'; import "package:photos/services/remote_assets_service.dart"; @@ -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/utils/data_util.dart"; import "package:photos/utils/local_settings.dart"; +import "package:photos/utils/ml_util.dart"; +import "package:photos/utils/wakelock_util.dart"; final _logger = Logger("MachineLearningSettingsPage"); @@ -40,6 +43,7 @@ class MachineLearningSettingsPage extends StatefulWidget { class _MachineLearningSettingsPageState extends State { late InitializationState _state; + final EnteWakeLock _wakeLock = EnteWakeLock(); late StreamSubscription _eventSubscription; @@ -53,6 +57,7 @@ class _MachineLearningSettingsPageState setState(() {}); }); _fetchState(); + _wakeLock.enable(); } void _fetchState() { @@ -63,6 +68,7 @@ class _MachineLearningSettingsPageState void dispose() { super.dispose(); _eventSubscription.cancel(); + _wakeLock.disable(); } @override @@ -438,19 +444,24 @@ class FaceRecognitionStatusWidgetState }); } - Future<(int, int, int, double)> getIndexStatus() async { + Future<(int, int, double, bool)> getIndexStatus() async { try { final indexedFiles = await FaceMLDataDB.instance .getIndexedFileCount(minimumMlVersion: faceMlVersion); - final indexableFiles = (await FaceMlService.getIndexableFileIDs()).length; + final indexableFiles = (await getIndexableFileIDs()).length; final showIndexedFiles = min(indexedFiles, indexableFiles); final pendingFiles = max(indexableFiles - indexedFiles, 0); - final foundFaces = await FaceMLDataDB.instance.getTotalFaceCount(); - final clusteredFaces = - await FaceMLDataDB.instance.getClusteredFaceCount(); - final clusteringDoneRatio = clusteredFaces / foundFaces; + final clusteringDoneRatio = + await FaceMLDataDB.instance.getClusteredToIndexableFilesRatio(); + final bool deviceIsHealthy = + MachineLearningController.instance.isDeviceHealthy; - return (showIndexedFiles, pendingFiles, foundFaces, clusteringDoneRatio); + return ( + showIndexedFiles, + pendingFiles, + clusteringDoneRatio, + deviceIsHealthy + ); } catch (e, s) { _logger.severe('Error getting face recognition status', e, s); rethrow; @@ -479,10 +490,17 @@ class FaceRecognitionStatusWidgetState if (snapshot.hasData) { final int indexedFiles = snapshot.data!.$1; final int pendingFiles = snapshot.data!.$2; - final int foundFaces = snapshot.data!.$3; - final double clusteringDoneRatio = snapshot.data!.$4; + final double clusteringDoneRatio = snapshot.data!.$3; final double clusteringPercentage = (clusteringDoneRatio * 100).clamp(0, 100); + final bool isDeviceHealthy = snapshot.data!.$4; + + if (!isDeviceHealthy && + (pendingFiles > 0 || clusteringPercentage < 99)) { + return MenuSectionDescriptionWidget( + content: S.of(context).indexingIsPaused, + ); + } return Column( children: [ @@ -512,19 +530,6 @@ class FaceRecognitionStatusWidgetState isGestureDetectorDisabled: true, key: ValueKey("pending_items_" + pendingFiles.toString()), ), - MenuItemWidget( - captionedTextWidget: CaptionedTextWidget( - title: S.of(context).foundFaces, - ), - trailingWidget: Text( - NumberFormat().format(foundFaces), - style: Theme.of(context).textTheme.bodySmall, - ), - singleBorderRadius: 8, - alignCaptionedTextToLeft: true, - isGestureDetectorDisabled: true, - key: ValueKey("found_faces_" + foundFaces.toString()), - ), MenuItemWidget( captionedTextWidget: CaptionedTextWidget( title: S.of(context).clusteringProgress, diff --git a/mobile/lib/ui/viewer/file/video_widget.dart b/mobile/lib/ui/viewer/file/video_widget.dart index 7f9218e9a..ed772df4f 100644 --- a/mobile/lib/ui/viewer/file/video_widget.dart +++ b/mobile/lib/ui/viewer/file/video_widget.dart @@ -17,9 +17,9 @@ import 'package:photos/ui/viewer/file/video_controls.dart'; import "package:photos/utils/dialog_util.dart"; import 'package:photos/utils/file_util.dart'; import 'package:photos/utils/toast_util.dart'; +import "package:photos/utils/wakelock_util.dart"; import 'package:video_player/video_player.dart'; import 'package:visibility_detector/visibility_detector.dart'; -import 'package:wakelock_plus/wakelock_plus.dart'; class VideoWidget extends StatefulWidget { final EnteFile file; @@ -45,7 +45,7 @@ class _VideoWidgetState extends State { ChewieController? _chewieController; final _progressNotifier = ValueNotifier(null); bool _isPlaying = false; - bool _wakeLockEnabledHere = false; + final EnteWakeLock _wakeLock = EnteWakeLock(); @override void initState() { @@ -126,13 +126,7 @@ class _VideoWidgetState extends State { _chewieController?.dispose(); _progressNotifier.dispose(); - if (_wakeLockEnabledHere) { - unawaited( - WakelockPlus.enabled.then((isEnabled) { - isEnabled ? WakelockPlus.disable() : null; - }), - ); - } + _wakeLock.dispose(); super.dispose(); } @@ -257,17 +251,10 @@ class _VideoWidgetState extends State { Future _keepScreenAliveOnPlaying(bool isPlaying) async { if (isPlaying) { - return WakelockPlus.enabled.then((value) { - if (value == false) { - WakelockPlus.enable(); - //wakeLockEnabledHere will not be set to true if wakeLock is already enabled from settings on iOS. - //We shouldn't disable when video is not playing if it was enabled manually by the user from ente settings by user. - _wakeLockEnabledHere = true; - } - }); + _wakeLock.enable(); } - if (_wakeLockEnabledHere && !isPlaying) { - return WakelockPlus.disable(); + if (!isPlaying) { + _wakeLock.disable(); } } diff --git a/mobile/lib/ui/viewer/gallery/hooks/add_photos_sheet.dart b/mobile/lib/ui/viewer/gallery/hooks/add_photos_sheet.dart index 0ccdc93e1..573893b17 100644 --- a/mobile/lib/ui/viewer/gallery/hooks/add_photos_sheet.dart +++ b/mobile/lib/ui/viewer/gallery/hooks/add_photos_sheet.dart @@ -5,6 +5,7 @@ import "package:flutter/material.dart"; import "package:flutter_animate/flutter_animate.dart"; import "package:modal_bottom_sheet/modal_bottom_sheet.dart"; import "package:photos/core/configuration.dart"; +import "package:photos/core/constants.dart"; import "package:photos/db/files_db.dart"; import "package:photos/generated/l10n.dart"; import "package:photos/l10n/l10n.dart"; @@ -167,7 +168,14 @@ class AddPhotosPhotoWidget extends StatelessWidget { Future _onPickFromDeviceClicked(BuildContext context) async { try { - final List? result = await AssetPicker.pickAssets(context); + final assetPickerTextDelegate = await _getAssetPickerTextDelegate(); + final List? result = await AssetPicker.pickAssets( + context, + pickerConfig: AssetPickerConfig( + maxAssets: maxPickAssetLimit, + textDelegate: assetPickerTextDelegate, + ), + ); if (result != null && result.isNotEmpty) { final ca = CollectionActions( CollectionsService.instance, @@ -204,6 +212,39 @@ class AddPhotosPhotoWidget extends StatelessWidget { } } } + + // _getAssetPickerTextDelegate returns the text delegate for the asset picker + // This custom method is required to enforce English as the default fallback + // instead of Chinese. + Future _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 { diff --git a/mobile/lib/ui/viewer/search/search_widget.dart b/mobile/lib/ui/viewer/search/search_widget.dart index c917d60e9..89002b1e1 100644 --- a/mobile/lib/ui/viewer/search/search_widget.dart +++ b/mobile/lib/ui/viewer/search/search_widget.dart @@ -203,7 +203,7 @@ class SearchWidgetState extends State { String query, ) { int resultCount = 0; - final maxResultCount = _isYearValid(query) ? 13 : 12; + final maxResultCount = _isYearValid(query) ? 12 : 11; final streamController = StreamController>(); if (query.isEmpty) { @@ -260,10 +260,11 @@ class SearchWidgetState extends State { onResultsReceived(locationResult); }, ); + _searchService.getAllFace(null).then( - (locationResult) { + (faceResult) { final List filteredResults = []; - for (final result in locationResult) { + for (final result in faceResult) { if (result.name().toLowerCase().contains(query.toLowerCase())) { filteredResults.add(result); } diff --git a/mobile/lib/utils/email_util.dart b/mobile/lib/utils/email_util.dart index f07d37b1c..7d085cbf6 100644 --- a/mobile/lib/utils/email_util.dart +++ b/mobile/lib/utils/email_util.dart @@ -16,6 +16,7 @@ import 'package:path_provider/path_provider.dart'; import 'package:photos/core/configuration.dart'; import 'package:photos/core/error-reporting/super_logging.dart'; import "package:photos/generated/l10n.dart"; +import "package:photos/ui/common/progress_dialog.dart"; import 'package:photos/ui/components/buttons/button_widget.dart'; import 'package:photos/ui/components/dialog_widget.dart'; import 'package:photos/ui/components/models/button_type.dart'; @@ -122,9 +123,28 @@ Future _sendLogs( } } -Future getZippedLogsFile(BuildContext context) async { - final dialog = createProgressDialog(context, S.of(context).preparingLogs); - await dialog.show(); +Future sendLogsForInit( + String toEmail, + String? subject, + String? body, +) async { + final String zipFilePath = await getZippedLogsFile(null); + final Email email = Email( + recipients: [toEmail], + subject: subject ?? '', + body: body ?? '', + attachmentPaths: [zipFilePath], + isHTML: false, + ); + await FlutterEmailSender.send(email); +} + +Future getZippedLogsFile(BuildContext? context) async { + late final ProgressDialog dialog; + if (context != null) { + dialog = createProgressDialog(context, S.of(context).preparingLogs); + await dialog.show(); + } final logsPath = (await getApplicationSupportDirectory()).path; final logsDirectory = Directory(logsPath + "/logs"); final tempPath = (await getTemporaryDirectory()).path; @@ -134,7 +154,9 @@ Future getZippedLogsFile(BuildContext context) async { encoder.create(zipFilePath); await encoder.addDirectory(logsDirectory); encoder.close(); - await dialog.hide(); + if (context != null) { + await dialog.hide(); + } return zipFilePath; } diff --git a/mobile/lib/utils/ml_util.dart b/mobile/lib/utils/ml_util.dart new file mode 100644 index 000000000..4033e2934 --- /dev/null +++ b/mobile/lib/utils/ml_util.dart @@ -0,0 +1,7 @@ +import "package:photos/core/configuration.dart"; +import "package:photos/db/files_db.dart"; + +Future> getIndexableFileIDs() async { + return FilesDB.instance + .getOwnedFileIDs(Configuration.instance.getUserID()!); + } \ No newline at end of file diff --git a/mobile/lib/utils/wakelock_util.dart b/mobile/lib/utils/wakelock_util.dart new file mode 100644 index 000000000..7e810dc0b --- /dev/null +++ b/mobile/lib/utils/wakelock_util.dart @@ -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 toggle({required bool enable}) async { + await WakelockPlus.toggle(enable: enable); + } +} diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 1d1082bfd..8b71025e9 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -45,10 +45,10 @@ packages: dependency: "direct main" description: name: animated_list_plus - sha256: fe66f9c300d715254727fbdf050487844d17b013fec344fa28081d29bddbdf1a + sha256: fb3d7f1fbaf5af84907f3c739236bacda8bf32cbe1f118dd51510752883ff50c url: "https://pub.dev" source: hosted - version: "0.4.5" + version: "0.5.2" animated_stack_widget: dependency: transitive description: @@ -971,10 +971,10 @@ packages: dependency: "direct main" description: name: home_widget - sha256: "29565bfee4b32eaf9e7e8b998d504618b779a74b2b1ac62dd4dac7468e66f1a3" + sha256: "2a0fdd6267ff975bd07bedf74686bd5577200f504f5de36527ac1b56bdbe68e3" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.6.0" html: dependency: transitive description: @@ -1152,26 +1152,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" url: "https://pub.dev" source: hosted - version: "10.0.0" + version: "10.0.4" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.3" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.1" like_button: dependency: "direct main" description: @@ -1368,10 +1368,10 @@ packages: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.12.0" mgrs_dart: dependency: transitive description: @@ -2144,10 +2144,10 @@ packages: dependency: "direct main" description: name: styled_text - sha256: f72928d1ebe8cb149e3b34a689cb1ddca696b808187cf40ac3a0bd183dff379c + sha256: fd624172cf629751b4f171dd0ecf9acf02a06df3f8a81bb56c0caa4f1df706c3 url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "8.1.0" sync_http: dependency: transitive description: @@ -2160,18 +2160,18 @@ packages: dependency: "direct main" description: name: syncfusion_flutter_core - sha256: "9be1bb9bbdb42823439a18da71484f1964c14dbe1c255ab1b931932b12fa96e8" + sha256: "63108a33f9b0d89f7b6b56cce908b8e519fe433dbbe0efcf41ad3e8bb2081bd9" url: "https://pub.dev" source: hosted - version: "19.4.56" + version: "25.2.5" syncfusion_flutter_sliders: dependency: "direct main" description: name: syncfusion_flutter_sliders - sha256: "1f6a63ccab4180b544074b9264a20f01ee80b553de154192fe1d7b434089d3c2" + sha256: f27310bedc0e96e84054f0a70ac593d1a3c38397c158c5226ba86027ad77b2c1 url: "https://pub.dev" source: hosted - version: "19.4.56" + version: "25.2.5" synchronized: dependency: "direct main" description: @@ -2192,26 +2192,26 @@ packages: dependency: "direct dev" description: name: test - sha256: a1f7595805820fcc05e5c52e3a231aedd0b72972cb333e8c738a8b1239448b6f + sha256: "7ee446762c2c50b3bd4ea96fe13ffac69919352bd3b4b17bac3f3465edc58073" url: "https://pub.dev" source: hosted - version: "1.24.9" + version: "1.25.2" test_api: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.7.0" test_core: dependency: transitive description: name: test_core - sha256: a757b14fc47507060a162cc2530d9a4a2f92f5100a952c7443b5cad5ef5b106a + sha256: "2bc4b4ecddd75309300d8096f781c0e3280ca1ef85beda558d33fcbedc2eead4" url: "https://pub.dev" source: hosted - version: "0.5.9" + version: "0.6.0" timezone: dependency: transitive description: @@ -2441,10 +2441,10 @@ packages: dependency: transitive description: name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "14.2.1" volume_controller: dependency: transitive description: @@ -2591,4 +2591,4 @@ packages: version: "3.1.2" sdks: dart: ">=3.3.0 <4.0.0" - flutter: ">=3.19.0" + flutter: ">=3.20.0-1.2.pre" diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 31c010671..1417d17f3 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -12,7 +12,7 @@ description: ente photos application # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 0.8.106+630 +version: 0.8.110+634 publish_to: none environment: @@ -21,7 +21,7 @@ environment: dependencies: adaptive_theme: ^3.1.0 animate_do: ^2.0.0 - animated_list_plus: ^0.4.5 + animated_list_plus: ^0.5.2 archive: ^3.1.2 background_fetch: ^1.2.1 battery_info: ^1.1.1 @@ -93,13 +93,13 @@ dependencies: fluttertoast: ^8.0.6 freezed_annotation: ^2.4.1 google_nav_bar: ^5.0.5 - home_widget: ^0.5.0 + home_widget: ^0.6.0 html_unescape: ^2.0.0 http: ^1.1.0 image: ^4.0.17 image_editor: ^1.3.0 in_app_purchase: ^3.0.7 - intl: ^0.18.0 + intl: ^0.19.0 json_annotation: ^4.8.0 latlong2: ^0.9.0 like_button: ^2.0.5 @@ -152,9 +152,9 @@ dependencies: sqlite3_flutter_libs: ^0.5.20 sqlite_async: ^0.6.1 step_progress_indicator: ^1.0.2 - styled_text: ^7.0.0 - syncfusion_flutter_core: ^19.2.49 - syncfusion_flutter_sliders: ^19.2.49 + styled_text: ^8.1.0 + syncfusion_flutter_core: ^25.2.5 + syncfusion_flutter_sliders: ^25.2.5 synchronized: ^3.1.0 tuple: ^2.0.0 uni_links: ^0.5.1 @@ -177,6 +177,7 @@ dependency_overrides: # Remove this after removing dependency from flutter_sodium. # Newer flutter packages depends on ffi > 2.0.0 while flutter_sodium depends on ffi < 2.0.0 ffi: 2.1.0 + intl: 0.18.1 video_player: git: url: https://github.com/ente-io/packages.git diff --git a/server/configurations/local.yaml b/server/configurations/local.yaml index 87502c271..fe29b9248 100644 --- a/server/configurations/local.yaml +++ b/server/configurations/local.yaml @@ -95,6 +95,15 @@ db: # Map of data centers # # Each data center also specifies which bucket in that provider should be used. +# +# If you're not using replication (it is off by default), you only need to +# provide valid credentials for the first entry (the default hot storage, +# "b2-eu-cen"). +# +# Note that you need to use the same key names (e.g. "b2-eu-cen") as below. The +# values and the S3 provider itself can any arbitrary S3 storage, it is not tied +# to the region (eu-cen) or provider (b2, wasabi), but for historical reasons +# the key names have to be one of those in the list below. s3: # Override the primary and secondary hot storage. The commented out values # are the defaults. diff --git a/server/pkg/controller/user/userauth.go b/server/pkg/controller/user/userauth.go index 087e00194..5d9664e99 100644 --- a/server/pkg/controller/user/userauth.go +++ b/server/pkg/controller/user/userauth.go @@ -302,7 +302,7 @@ func emailOTT(c *gin.Context, to string, ott string, client string, purpose stri inlineImage["content"] = "iVBORw0KGgoAAAANSUhEUgAAALAAAACwCAYAAACvt+ReAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAABHlSURBVHgB7Z1tjFxV/ce/U7ZGxa3bEsrflr+d5o+x5W9tN/ZJAu60PFi1ursm9SnSbuWFwZIAMTFAom6jKdEX0iYg+AK7SzRqfNHWmABa2qmokW61iwq7QA23L7oiaLvsQhG3MJ7vnXOXu7t3Zu7MfTrn3PNpTnd3Zpru7nznN9/fwzm3AEtoKpVKUXzwr2VidchVlA/zvg5ivMY6LZbjrUKh4MASigIscxBCpQDXyLVafiyitjCTYBhVQR+Tnw8LYY/DMgMrYEwLtgdVoXbjrWiqGsNyHUJV0A5yTm4FLERbQlW0XagKV0coZkbog0LMZeSQXAnYJ9odSNcOpIEjVlmswTyJ2XgBGy7aWjioinmfEPMwDMZIAUtP24eqny0h31DAFPIADMQoAUvh3irWbchPtA2Lg2pU3m1S8meEgGV9th9Vm2BpzAAMEbLWApbC3Q9rE1plAJoLeR40hMIVa0B8+jyseKPQJ9bz4ne5XwYD7dAqAluPmzj9qCZ82nT8tBGwEC9LYfdA3S6ZKTio2ooBaIDyArY+NzMGoIE/VtoDC/HSLpyEFW8W9Il1UjwH34TCKBmBbdRVjrJYO1WMxspFYBt1laSEajTug2IoI2BWGMRikrYXtsKgInxOWG67R1aDlEAJCyEtw1HYCoMuOGJtUsFSZB6BZXmMlqEIiy4UUbUUPciYTAUsM9wDsJZBR/icHci6SpGZhZB+9zZYTGCvsBO3IwNSF7BMABh1S7CYxEFUS22ptqFTFbBM1viDrobBjIyM4Pjx4zhz5oz79dKlS7F+/XqsXLkShsPh+d40k7vUBJyHSgNFe++997ofg6CQb7nlFvT29sJgHKRYoUhFwKaLd2JiAnfddRcOHz4c6vEUMIVMQRuKg5REnLiATRfv6Ogodu3aNW0XwkLxPvTQQ1bEEUm0jGa6eGkVbrzxxqbFS/hvtm/f7vplQymKdTTpQfnEBCyrDUzYijCQwcFBV4CTk5NoFYqYduLAgQMwlCKqteLE6vyJWQjxTfNZybxTkwRM1LjihJ6Yy1DKwkpsQgIkEoFlk8JI8e7Zsyd28ZIkXhQKUZKaiJ3YI7BsLfbDMFhpuPvuuxN/u6ctYUXDUPpFJN6NGIlVwHK4wzhDR/Hu2LEjtYTruuuucyP9ggULYCBsdBxETMQmYJltcqrMqMEcr1rQSqUhCuza0VIYWGZjq7kzrvJaLAKWWaZxI5FZidfD4Fqxg6qII89NxJXE0fcWYRBsUPT09GQmXpL1CyhBiqhqJjKRBSz3SRk1FvnYY4+5DYooNd648GrFBjY8bhPaiaybSBbCRN/LKsOdd94JFWEVxLBBoMh+OKqA2SYuwRB0qMUa2PCI1ORo2ULIem8JhqBLI8HAhkcpipVoKQKbZh1Yc2W2rxOGReKWrUSrAh6AIYdJ0+/qOkxDP0xfbAgtWYmmBSyrDvuhOWl315KCDQ9OxhnStWu6S9eUgE1pWLA0xbdfU0pTBjU8HDTZ4Gg2ieO5ZUVojImD5AY1PIposqcQOgLLxO15aIzBnS0XQyJxUwldMxG4HxrD1rDJ4iVe167WrmhNoE0N3WYOFYF1j758QrnxUoXWcFoY0LVbHiYKh43A/dAUlsii7l3TEZYHNW94hKp0NYzAOkdflpcMqpO2hOYNj4ZROEwE7oeGMPrkXbxE89ZzQy9cNwLrGn0N3yDZEhp37RbWqws3isD90Iykdg3rDnMBipgdSM2oWxduFIEZfYvQBJ3nGtJCw1oxo+/yWlG4ZgSWMw9FaACjiuEn3MSGhs0c1oX7at1Zz0LcCg3gE2HCUE6aaCji7lp3BFoIEX3XoDq0ozSmt4aThhNsLDVqcvA2T7osz76xVgRWfpOmFW90NLNegUeV1YrASidvVrzxo0HDY1xE4IWzb5wTgYV4S1BYvCqc12AiGtTOO6Q2ZxBkIfqgKBSvKuc1mIgGIp5jI+ZYCFXtA30amxRWvMmjcNdujo2YIWAZoo9CMZI8bKS9vR3bd2x3nzTdBsH5jsTy4X333he7peJlwRiNFdxrN6MaMVvAe6FY/TfJtzUKdvChQe33krmD7D29sb87KXpC5j4h4Okq2WwP3AWFSNqT7bpllxEnP/JnYDMnbhjdFaz2zNDotIDl5NkaKEIaCcWKFStgCj29yVzRQcGS5Rr/RWP8EThX4iUmXfo1yXcSBXdyT79a5wXdmCV2HFJNvJkTHj2rANPB1i/gTC/APTVxwRWubmeU5Qm2nu+44w48dWQELw2ddZ+zjJge7nGrENJTnENGnD/zGo7d9IT7Kn/lQ2fxsyd+gnOVs0iakVGzJthWrkjWEn1kQxd6Orbh/NGp6duWbL4Ma762Eu9c8g6kjLtTwxNwCRnWfw9v+y3Gn5lZAjr5xnEcefPRRIVsBRwO1spv+sCX8Z5ni5ianJpz/6VrF6HrhxuQMm49uE1+kVkC9+R3R+aIl3RetN5daQjZUpvuDb24+vy1eP2PU+CfIF46IezE5AXMb29DilCz0wIuIQPGn5nAcz9y6j6GIl4+7woceeNR/OlNrU+c0YrFCy7Dje/9EhaeXIzXawjXz0tD/3LtRIq4OZsn4GXIgKfvPxXqcR2FRfh02+exufJRPHjhPhuNE+ZjV27Fxn904aJnw0fU/0ymntC5riFTC/HyaHM7ZCnkr87/urUVCfH/l6/CDee34pJTi6EBRf7VJjtwWmFtRby8vfAObOnYig+/1oWpVxrbBUXgfHCREbgIDbG2Ih6uvrwL17+2FRdNttVM0hRGXwF7WFvxFix3hZ1IY5LGAHD5i5mkP3Ghv4A9rK2o7jJuJGCK/Kq2LmyetyWwpqsZ5giYeLai613XYuDcD6ytmAXtwsfbevDmGHS0C0Eso4CNuUysxyWvLHZtxdDFv8eRC48ERiVDruoTis6Vndh80RYsfHYx3oRRLKSA342M+M8rydYO1716Fd5XWYEj8+baCr6VmgZHKv1zu/wZt733C3j/C6tMsAtBvDvTCDw1kfwv1bMVH7y4E4fGf54LW0Hhdm/sxeoX1uP1Z6ZMsQtBFI20EEFc8eoKfGPZHjwy/ks8KpaJMAJvve6Twi581J0Ye91c4U6TGwGTV8dewzW4Fqvmd+JfG8ZgEgXx59NXbsM/fzzh/pw5oSNXAvagrej45SKM/O8pXPHFYtpTVLHDeeoTX/+LOxWWMzr0fuYiwmGi04fOYOXNV2BZt367kxl1n77/OTz3o9OmJmkNybWACd9uveh1pRByBjsLWuKfQ2fd7ztHdiGQ3AvYg5GYolA9Gl+YuICnHzjVcI46L1gB+/CiMcW89turlIvGp4RVoO3Jq10Iotmr1ecC2omHtxzDSMiB+6RhkvabLx13t19Z8c6EAh6HJRBGu0eEkJsdvI8Lblvni+jhjx3LY4UhDOO0EBRw7kppYaGtOPyZ37u+OM0kzyZpoRi3HjgkaSV5jLonvvFnjB15EZaGuAJ2YNBIZZIkXXKzSVrTuAJ+GZamYDTmoogZkaPCczH+/J0R63Ob52WbxEXAS/LOt+hTvSTtsW2/s+JtjXOehbC0CG0FS27NJnk2SYuF01bAMRE2ybNJWqw4VsAx4iV5Y0f/gdUBJzbaJC12rICTgNGVy0vybJKWGE5boVBwKpWKbWYkgDeuaX1uMlC73iyEA0siWPEmxjD/8gT8JCwWvTjNvzwBD8Ni0Ysy/7ICtujKDAthBZwA4ziHn1YGxS/3BCyx42rWnUbj1V5EJcKBHeqJjXLl1/gDfoN/iz+jlb+6Iu4pfFaUehbCEplhapaf+HdkHIIlMg7+hgcq9wiD9itXvP7b91b2uMK2RGbaMbQF3WhpHoq1XPmViLqP130chT1cGcKWwqewAh+ApSWmg61fwAfF2g9L09AePFI5NCPi1sPzxmuwFqXCDdZWNM/cCCx9MO9Q5qLfqkMhHqz8zLUHrUDhO5W/uSKmmC2hoP91vC9mbyk6BivghjDS/qHy+HSSFgXvRUBr0Ve42UbjxhzzfzF7W/1BWOpSTdK+NydJiwqF7CV5/4ZtP9dhhkZnRGBee9YO9gRDsdLnJl3T9ZI8aysCcahR/w1Bu5IHxboVlmloF+KOuPXwe2ub5M2gPPuGIAEzRFsBC17AmIi6v2g5SYuKl+RtLHwEG3E1LG5wncEcAVsb8VaSxqibNYzGtC5MGHOe5M2xD6TW2WiDyCn+JE0lvCSP7wj8PIeUg26sdTJP7myEF+lG8RRUhp0+zlbkMMnbF3RjoICljSiLT0vIAWknaVHJYZLH5kXgqEO9s9HYby7BYCgA1l2zStKiwiRvuHJCPEk3CCFfD4PZV+uOeucDD8DQU3uqNd1fYKDygLbi9cN3D/rjF/B3GAiTt4Fad9YUsJy3rKl8XaHHZZLWaGpMN2gr+HPRWhiW5JXr3dnoeNW9Yn0TBhB18EYXDBwQ2l3vzrqXGJBRWPuSGn0uo5Pp4vXwXqwcrNc8Gg/4J8+CCHONjH5oyvQuCI0qDHHCTqLmu0B2N3pAwxPa5ck9ZSRQkZi/YH4iF/wOuzsiL2g6INQw+pKwlxjYKdbziJm3vastdgHrVtNNCw1rxw2jLwklYBmF6YV3QFGyHrzRBSZ5o5WnsBHXxFo7vjjeyy2Eir6kmYu89IvVDcWGfFQavNEFDsx7tuJzhZ34H7wHihEq+pLQFzqUrwil6sKqDt7oQpy1444VCxATu8NGX9LsZbZYF6aNKCJD8lLTTYuoteP57fPFiuWKbQ6qGgtNU/+r3Ll8u/j0ADLCf+KNJT78m0t5glAR/xf633a8vx0xsds7cScsTV8rWfwHHLUsIwaWbL4s9GNrnXhjiRcKmTMizdiKS9ddghgYqDfzUIsCWkBE4aL4cBIRE7qXhs7i2E1P1H2MrelmS5jDV7oe3CBEvAgRYNTtbMb7erRkXGRZjZniPYgAf+hL1y4KvHZEnGcvWFrHG9mkkD++tBtvG3v7jPtZPosoXrKvFfGSliKwhxDxUUTs0M2Owm+0X8DjE0etcBXlqqXX4BNLujF1ouJ+vaz7cqz71ipEoCzEuwktElXARcRgJR7eUsaLS/6Onw/91FYWNOH6dTe4g/RX3bw+SgRu2Tp4NJ3E+ZH/ceiicy0++OD73OTMilcfLl7yTqz99qqo9uH2KOIlkSKwh4jErN1F3gR6ZuwM7v/+/Tg+dBxjY2OwqMe6devwlZu/4n6MCH3vbYhIXAKmhaCVKCImhoaGMPrMKCYnJ2HJhvb2dnd5bN68GQvaY+m4Oahah8hb1mIRMInLD1uMJ7Lv9RPJA/uR39BOWCz12RmXeElsAiaySxc5qbMYy26pkdiIzUL4iSupsxhFLEnbbBIRMImjyWExhoNCvL1IgCQFzGSOIraXLMg3vA53KY6KQxCxemA/8hvmq86BJa84YvUkJV6SWAT2kOU1RuIiLHnCEWtTnBWHIBIXMLEizh0OUhAvSUXAxIo4NzhISbwkMQ88G/kDcWzOXtLWXJiwpSZekpqAiU/E9np05lFGtdrgIEVSFTBhRiprgsYd3Zpj2KTYlGS1oRapC9hDdmVs21l/difRYQtLaklcLURy1yM+7IedYtMNRtudcc82NEvmAia2QqEdDlJO1mqRmYXwI38RnbC+WAf4HHWqIF6iRAT2I6JxH6rb9a2lUAtaBvrdpo5+ShrlBEykpaAvLsGiAmXEPIgeF0pYiNnwFyXPCmCVwshLfWkCf/e3yxKZAwVRMgL7kdG4Hwofrm0oZSgadf0oGYH9yGjch+p+OweWpHHE6lU56vpRPgL7kUPyLJobce06xfAubLk3i45aq2glYA9rK2JnAE2ejK4KWgrYwwo5MmVo4HProbwHrofPHy+HAVcUTZEBsZbr4nProXUEno0vInfBtqVno6XHbYRRAvYjO3o8myLvu6LLYh1C9Qh/42rqxgrYQwiZAmblIk9R2btIO89jKMNgjBewHyHmkvjQBzPFnBvR+smVgP1IMXMWmWLW1WZwf+Ex5Ey0fnIrYD8y+aOIKejVUFfQDqp+lsI9aKKnbRYr4ABkx2+NXJ7dSFPUFKaDqlCflB+HrWDnYgXcBDJS+9cyVOeWg1YQ43hrus7xfX1afu0u3WuzafJf05durhLhbZAAAAAASUVORK5CYII=" } inlineImages = append(inlineImages, inlineImage) - err := emailUtil.SendTemplatedEmail([]string{to}, "ente", "verification@ente.io", + err := emailUtil.SendTemplatedEmail([]string{to}, "ente", "verify@ente.io", ente.OTTEmailSubject, templateName, map[string]interface{}{ "VerificationCode": ott, }, inlineImages) diff --git a/web/apps/accounts/src/pages/_app.tsx b/web/apps/accounts/src/pages/_app.tsx index a1927f52b..6fc0936ea 100644 --- a/web/apps/accounts/src/pages/_app.tsx +++ b/web/apps/accounts/src/pages/_app.tsx @@ -1,17 +1,16 @@ import { CustomHead } from "@/next/components/Head"; import { setupI18n } from "@/next/i18n"; import { logUnhandledErrorsAndRejections } from "@/next/log-web"; +import type { AppName, BaseAppContextT } from "@/next/types/app"; +import { ensure } from "@/utils/ensure"; import { PAGES } from "@ente/accounts/constants/pages"; import { accountLogout } from "@ente/accounts/services/logout"; import { APPS, APP_TITLES } from "@ente/shared/apps/constants"; import { Overlay } from "@ente/shared/components/Container"; import DialogBoxV2 from "@ente/shared/components/DialogBoxV2"; -import { - DialogBoxAttributesV2, - SetDialogBoxAttributesV2, -} from "@ente/shared/components/DialogBoxV2/types"; +import type { DialogBoxAttributesV2 } from "@ente/shared/components/DialogBoxV2/types"; import EnteSpinner from "@ente/shared/components/EnteSpinner"; -import AppNavbar from "@ente/shared/components/Navbar/app"; +import { AppNavbar } from "@ente/shared/components/Navbar/app"; import { useLocalState } from "@ente/shared/hooks/useLocalState"; import HTTPService from "@ente/shared/network/HTTPService"; import { LS_KEYS, getData } from "@ente/shared/storage/localStorage"; @@ -22,25 +21,28 @@ import { ThemeProvider } from "@mui/material/styles"; import { t } from "i18next"; import { AppProps } from "next/app"; import { useRouter } from "next/router"; -import { createContext, useEffect, useState } from "react"; +import { createContext, useContext, useEffect, useState } from "react"; import "styles/global.css"; -interface AppContextProps { - isMobile: boolean; - showNavBar: (show: boolean) => void; - setDialogBoxAttributesV2: SetDialogBoxAttributesV2; - logout: () => void; -} +/** The accounts app has no extra properties on top of the base context. */ +type AppContextT = BaseAppContextT; -export const AppContext = createContext({} as AppContextProps); +/** The React {@link Context} available to all pages. */ +export const AppContext = createContext(undefined); + +/** Utility hook to reduce amount of boilerplate in account related pages. */ +export const useAppContext = () => ensure(useContext(AppContext)); export default function App({ Component, pageProps }: AppProps) { + const appName: AppName = "account"; + const [isI18nReady, setIsI18nReady] = useState(false); const [showNavbar, setShowNavBar] = useState(false); - const [dialogBoxAttributeV2, setDialogBoxAttributesV2] = - useState(); + const [dialogBoxAttributeV2, setDialogBoxAttributesV2] = useState< + DialogBoxAttributesV2 | undefined + >(); const [dialogBoxV2View, setDialogBoxV2View] = useState(false); @@ -85,8 +87,17 @@ export default function App({ Component, pageProps }: AppProps) { void accountLogout().then(() => router.push(PAGES.ROOT)); }; + const appContext = { + appName, + logout, + showNavBar, + isMobile, + setDialogBoxAttributesV2, + }; + + // TODO: This string doesn't actually exist const title = isI18nReady - ? t("TITLE", { context: APPS.ACCOUNTS }) + ? t("title", { context: "accounts" }) : APP_TITLES.get(APPS.ACCOUNTS); return ( @@ -102,15 +113,7 @@ export default function App({ Component, pageProps }: AppProps) { attributes={dialogBoxAttributeV2 as any} /> - + {!isI18nReady && ( ({ diff --git a/web/apps/accounts/src/pages/credentials.tsx b/web/apps/accounts/src/pages/credentials.tsx new file mode 100644 index 000000000..070aace4a --- /dev/null +++ b/web/apps/accounts/src/pages/credentials.tsx @@ -0,0 +1,6 @@ +import Page_ from "@ente/accounts/pages/credentials"; +import { useAppContext } from "./_app"; + +const Page = () => ; + +export default Page; diff --git a/web/apps/accounts/src/pages/credentials/index.tsx b/web/apps/accounts/src/pages/credentials/index.tsx deleted file mode 100644 index 306efc7b8..000000000 --- a/web/apps/accounts/src/pages/credentials/index.tsx +++ /dev/null @@ -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 ; -} diff --git a/web/apps/accounts/src/pages/generate.tsx b/web/apps/accounts/src/pages/generate.tsx new file mode 100644 index 000000000..c6804255a --- /dev/null +++ b/web/apps/accounts/src/pages/generate.tsx @@ -0,0 +1,6 @@ +import Page_ from "@ente/accounts/pages/generate"; +import { useAppContext } from "./_app"; + +const Page = () => ; + +export default Page; diff --git a/web/apps/accounts/src/pages/generate/index.tsx b/web/apps/accounts/src/pages/generate/index.tsx deleted file mode 100644 index ff1b6aa1f..000000000 --- a/web/apps/accounts/src/pages/generate/index.tsx +++ /dev/null @@ -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 ; -} diff --git a/web/apps/accounts/src/pages/login.tsx b/web/apps/accounts/src/pages/login.tsx new file mode 100644 index 000000000..1a7de0497 --- /dev/null +++ b/web/apps/accounts/src/pages/login.tsx @@ -0,0 +1,6 @@ +import Page_ from "@ente/accounts/pages/login"; +import { useAppContext } from "./_app"; + +const Page = () => ; + +export default Page; diff --git a/web/apps/accounts/src/pages/login/index.tsx b/web/apps/accounts/src/pages/login/index.tsx deleted file mode 100644 index 0631a7bd1..000000000 --- a/web/apps/accounts/src/pages/login/index.tsx +++ /dev/null @@ -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 ; -} diff --git a/web/apps/accounts/src/pages/passkeys/flow/Recover.tsx b/web/apps/accounts/src/pages/passkeys/flow/Recover.tsx index f992e3961..30e1f63af 100644 --- a/web/apps/accounts/src/pages/passkeys/flow/Recover.tsx +++ b/web/apps/accounts/src/pages/passkeys/flow/Recover.tsx @@ -1,16 +1,12 @@ import { TwoFactorType } from "@ente/accounts/constants/twofactor"; -import RecoverPage from "@ente/accounts/pages/recover"; -import { APPS } from "@ente/shared/apps/constants"; -import { AppContext } from "pages/_app"; -import { useContext } from "react"; +import RecoverPage from "@ente/accounts/pages/two-factor/recover"; +import { useAppContext } from "../../_app"; -export default function Recover() { - const appContext = useContext(AppContext); - return ( - - ); -} +const Page = () => ( + +); + +export default Page; diff --git a/web/apps/accounts/src/pages/passkeys/index.tsx b/web/apps/accounts/src/pages/passkeys/index.tsx index 977595af6..dec8916cc 100644 --- a/web/apps/accounts/src/pages/passkeys/index.tsx +++ b/web/apps/accounts/src/pages/passkeys/index.tsx @@ -9,14 +9,8 @@ import { t } from "i18next"; import _sodium from "libsodium-wrappers"; import { useRouter } from "next/router"; import { AppContext } from "pages/_app"; -import { - Dispatch, - SetStateAction, - createContext, - useContext, - useEffect, - useState, -} from "react"; +import type { Dispatch, SetStateAction } from "react"; +import { createContext, useContext, useEffect, useState } from "react"; import { Passkey } from "types/passkey"; import { finishPasskeyRegistration, diff --git a/web/apps/accounts/src/pages/recover.tsx b/web/apps/accounts/src/pages/recover.tsx new file mode 100644 index 000000000..d825729e5 --- /dev/null +++ b/web/apps/accounts/src/pages/recover.tsx @@ -0,0 +1,6 @@ +import Page_ from "@ente/accounts/pages/recover"; +import { useAppContext } from "./_app"; + +const Page = () => ; + +export default Page; diff --git a/web/apps/accounts/src/pages/recover/index.tsx b/web/apps/accounts/src/pages/recover/index.tsx deleted file mode 100644 index 2692225b2..000000000 --- a/web/apps/accounts/src/pages/recover/index.tsx +++ /dev/null @@ -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 ; -} diff --git a/web/apps/accounts/src/pages/signup.tsx b/web/apps/accounts/src/pages/signup.tsx new file mode 100644 index 000000000..403d3e735 --- /dev/null +++ b/web/apps/accounts/src/pages/signup.tsx @@ -0,0 +1,6 @@ +import Page_ from "@ente/accounts/pages/signup"; +import { useAppContext } from "./_app"; + +const Page = () => ; + +export default Page; diff --git a/web/apps/accounts/src/pages/signup/index.tsx b/web/apps/accounts/src/pages/signup/index.tsx deleted file mode 100644 index 40d073cf5..000000000 --- a/web/apps/accounts/src/pages/signup/index.tsx +++ /dev/null @@ -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 ; -} diff --git a/web/apps/accounts/src/pages/two-factor/recover.tsx b/web/apps/accounts/src/pages/two-factor/recover.tsx new file mode 100644 index 000000000..d3f40be49 --- /dev/null +++ b/web/apps/accounts/src/pages/two-factor/recover.tsx @@ -0,0 +1,6 @@ +import Page_ from "@ente/accounts/pages/two-factor/recover"; +import { useAppContext } from "../_app"; + +const Page = () => ; + +export default Page; diff --git a/web/apps/accounts/src/pages/two-factor/recover/index.tsx b/web/apps/accounts/src/pages/two-factor/recover/index.tsx deleted file mode 100644 index af5765323..000000000 --- a/web/apps/accounts/src/pages/two-factor/recover/index.tsx +++ /dev/null @@ -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 ( - - ); -} diff --git a/web/apps/accounts/src/pages/two-factor/setup.tsx b/web/apps/accounts/src/pages/two-factor/setup.tsx new file mode 100644 index 000000000..12716e2df --- /dev/null +++ b/web/apps/accounts/src/pages/two-factor/setup.tsx @@ -0,0 +1,6 @@ +import Page_ from "@ente/accounts/pages/two-factor/setup"; +import { useAppContext } from "../_app"; + +const Page = () => ; + +export default Page; diff --git a/web/apps/accounts/src/pages/two-factor/setup/index.tsx b/web/apps/accounts/src/pages/two-factor/setup/index.tsx deleted file mode 100644 index f1283e870..000000000 --- a/web/apps/accounts/src/pages/two-factor/setup/index.tsx +++ /dev/null @@ -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 ( - - ); -} diff --git a/web/apps/accounts/src/pages/two-factor/verify.tsx b/web/apps/accounts/src/pages/two-factor/verify.tsx new file mode 100644 index 000000000..7c682b1b9 --- /dev/null +++ b/web/apps/accounts/src/pages/two-factor/verify.tsx @@ -0,0 +1,6 @@ +import Page_ from "@ente/accounts/pages/two-factor/verify"; +import { useAppContext } from "../_app"; + +const Page = () => ; + +export default Page; diff --git a/web/apps/accounts/src/pages/two-factor/verify/index.tsx b/web/apps/accounts/src/pages/two-factor/verify/index.tsx deleted file mode 100644 index fd4c2ce09..000000000 --- a/web/apps/accounts/src/pages/two-factor/verify/index.tsx +++ /dev/null @@ -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 ( - - ); -} diff --git a/web/apps/accounts/src/pages/verify.tsx b/web/apps/accounts/src/pages/verify.tsx new file mode 100644 index 000000000..bb2dc8778 --- /dev/null +++ b/web/apps/accounts/src/pages/verify.tsx @@ -0,0 +1,6 @@ +import Page_ from "@ente/accounts/pages/verify"; +import { useAppContext } from "./_app"; + +const Page = () => ; + +export default Page; diff --git a/web/apps/accounts/src/pages/verify/index.tsx b/web/apps/accounts/src/pages/verify/index.tsx deleted file mode 100644 index b09480858..000000000 --- a/web/apps/accounts/src/pages/verify/index.tsx +++ /dev/null @@ -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 ; -} diff --git a/web/apps/auth/.env b/web/apps/auth/.env index 3f3b1cc9a..49fd5c743 100644 --- a/web/apps/auth/.env +++ b/web/apps/auth/.env @@ -1 +1,4 @@ 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`. diff --git a/web/apps/auth/.eslintrc.js b/web/apps/auth/.eslintrc.js index b1c4c2e16..a407643d8 100644 --- a/web/apps/auth/.eslintrc.js +++ b/web/apps/auth/.eslintrc.js @@ -9,5 +9,5 @@ module.exports = { tsconfigRootDir: __dirname, project: "./tsconfig.json", }, - ignorePatterns: [".eslintrc.js", "out"], + ignorePatterns: [".eslintrc.js", "next.config.js", "out"], }; diff --git a/web/apps/auth/package.json b/web/apps/auth/package.json index d21e1dc05..d16b973ab 100644 --- a/web/apps/auth/package.json +++ b/web/apps/auth/package.json @@ -3,9 +3,12 @@ "version": "0.0.0", "private": true, "dependencies": { + "@/build-config": "*", "@/next": "*", "@ente/accounts": "*", "@ente/eslint-config": "*", - "@ente/shared": "*" + "@ente/shared": "*", + "jssha": "~3.3.1", + "otpauth": "^9" } } diff --git a/web/apps/auth/src/components/AuthFooter.tsx b/web/apps/auth/src/components/AuthFooter.tsx deleted file mode 100644 index 029103125..000000000 --- a/web/apps/auth/src/components/AuthFooter.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { Button } from "@mui/material"; -import { t } from "i18next"; - -export const AuthFooter = () => { - return ( -
-

{t("AUTH_DOWNLOAD_MOBILE_APP")}

- - - -
- ); -}; diff --git a/web/apps/auth/src/components/Navbar.tsx b/web/apps/auth/src/components/Navbar.tsx deleted file mode 100644 index 87614d643..000000000 --- a/web/apps/auth/src/components/Navbar.tsx +++ /dev/null @@ -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 ( - - - - - - } - > - } - onClick={logout} - > - {t("LOGOUT")} - - - - - ); -} diff --git a/web/apps/auth/src/components/OTPDisplay.tsx b/web/apps/auth/src/components/OTPDisplay.tsx deleted file mode 100644 index 38de665aa..000000000 --- a/web/apps/auth/src/components/OTPDisplay.tsx +++ /dev/null @@ -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 ( -
- -
-
-

- {issuer} -

-

- {account} -

-

- {code} -

-
-
-
-

- {t("AUTH_NEXT")} -

-

- {nextCode} -

-
-
-
- ); -}; - -function BadCodeInfo({ codeInfo, codeErr }) { - const [showRawData, setShowRawData] = useState(false); - - return ( -
-
{codeInfo.title}
-
{codeErr}
-
- {showRawData ? ( -
setShowRawData(false)}> - {codeInfo.rawData ?? "no raw data"} -
- ) : ( -
setShowRawData(true)}>Show rawData
- )} -
-
- ); -} - -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 ( -
- {codeErr === "" ? ( - { - copyCode(); - }} - > - - - - ) : ( - - )} -
- ); -}; - -export default OTPDisplay; diff --git a/web/apps/auth/src/components/TimerProgress.tsx b/web/apps/auth/src/components/TimerProgress.tsx deleted file mode 100644 index d1f3726f6..000000000 --- a/web/apps/auth/src/components/TimerProgress.tsx +++ /dev/null @@ -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 ( -
- ); -}; - -export default TimerProgress; diff --git a/web/apps/auth/src/pages/404.tsx b/web/apps/auth/src/pages/404.tsx index 6cca72b77..dcd621c70 100644 --- a/web/apps/auth/src/pages/404.tsx +++ b/web/apps/auth/src/pages/404.tsx @@ -1,9 +1,3 @@ -import { APPS } from "@ente/shared/apps/constants"; -import NotFoundPage from "@ente/shared/next/pages/404"; -import { AppContext } from "pages/_app"; -import { useContext } from "react"; +import Page from "@ente/shared/next/pages/404"; -export default function NotFound() { - const appContext = useContext(AppContext); - return ; -} +export default Page; diff --git a/web/apps/auth/src/pages/_app.tsx b/web/apps/auth/src/pages/_app.tsx index a0a579a80..0ada75c3f 100644 --- a/web/apps/auth/src/pages/_app.tsx +++ b/web/apps/auth/src/pages/_app.tsx @@ -4,6 +4,8 @@ import { logStartupBanner, logUnhandledErrorsAndRejections, } 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 { APPS, @@ -12,45 +14,46 @@ import { } from "@ente/shared/apps/constants"; import { Overlay } from "@ente/shared/components/Container"; import DialogBoxV2 from "@ente/shared/components/DialogBoxV2"; -import { - DialogBoxAttributesV2, - SetDialogBoxAttributesV2, -} from "@ente/shared/components/DialogBoxV2/types"; +import type { DialogBoxAttributesV2 } from "@ente/shared/components/DialogBoxV2/types"; import EnteSpinner from "@ente/shared/components/EnteSpinner"; import { 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 { useLocalState } from "@ente/shared/hooks/useLocalState"; import HTTPService from "@ente/shared/network/HTTPService"; import { LS_KEYS, getData } from "@ente/shared/storage/localStorage"; import { getTheme } from "@ente/shared/themes"; import { THEME_COLOR } from "@ente/shared/themes/constants"; -import { SetTheme } from "@ente/shared/themes/types"; import type { User } from "@ente/shared/user/types"; import { CssBaseline, useMediaQuery } from "@mui/material"; import { ThemeProvider } from "@mui/material/styles"; import { t } from "i18next"; -import { AppProps } from "next/app"; +import type { AppProps } from "next/app"; import { useRouter } from "next/router"; -import { createContext, useEffect, useRef, useState } from "react"; -import LoadingBar from "react-top-loading-bar"; +import { createContext, useContext, useEffect, useRef, useState } from "react"; +import LoadingBar, { type LoadingBarRef } from "react-top-loading-bar"; 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; finishLoading: () => void; - isMobile: boolean; themeColor: THEME_COLOR; - setThemeColor: SetTheme; + setThemeColor: (themeColor: THEME_COLOR) => void; somethingWentWrong: () => void; - setDialogBoxAttributesV2: SetDialogBoxAttributesV2; - logout: () => void; }; -export const AppContext = createContext(null); +/** The React {@link Context} available to all pages. */ +export const AppContext = createContext(undefined); + +/** Utility hook to reduce amount of boilerplate in account related pages. */ +export const useAppContext = () => ensure(useContext(AppContext)); export default function App({ Component, pageProps }: AppProps) { + const appName: AppName = "auth"; + const router = useRouter(); const [isI18nReady, setIsI18nReady] = useState(false); const [loading, setLoading] = useState(false); @@ -58,10 +61,11 @@ export default function App({ Component, pageProps }: AppProps) { typeof window !== "undefined" && !window.navigator.onLine, ); const [showNavbar, setShowNavBar] = useState(false); - const isLoadingBarRunning = useRef(false); - const loadingBar = useRef(null); - const [dialogBoxAttributeV2, setDialogBoxAttributesV2] = - useState(); + const isLoadingBarRunning = useRef(false); + const loadingBar = useRef(null); + const [dialogBoxAttributeV2, setDialogBoxAttributesV2] = useState< + DialogBoxAttributesV2 | undefined + >(); const [dialogBoxV2View, setDialogBoxV2View] = useState(false); const isMobile = useMediaQuery("(max-width:428px)"); const [themeColor, setThemeColor] = useLocalState( @@ -134,9 +138,23 @@ export default function App({ Component, pageProps }: AppProps) { 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 - ? t("TITLE", { context: APPS.AUTH }) - : APP_TITLES.get(APPS.AUTH); + ? t("title", { context: "auth" }) + : APP_TITLES.get(APPS.AUTH) ?? ""; return ( <> @@ -158,19 +176,7 @@ export default function App({ Component, pageProps }: AppProps) { attributes={dialogBoxAttributeV2} /> - + {(loading || !isI18nReady) && ( ({ diff --git a/web/apps/auth/src/pages/auth.tsx b/web/apps/auth/src/pages/auth.tsx new file mode 100644 index 000000000..80a342f38 --- /dev/null +++ b/web/apps/auth/src/pages/auth.tsx @@ -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([]); + 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 ( + + + + ); + } + + return ( + <> + +
+
+ {filteredCodes.length == 0 && searchTerm.length == 0 ? ( + <> + ) : ( + setSearchTerm(e.target.value)} + variant="filled" + style={{ width: "350px" }} + value={searchTerm} + autoFocus + /> + )} + +
+
+ {filteredCodes.length == 0 ? ( +
+ {searchTerm.length > 0 ? ( +

{t("NO_RESULTS")}

+ ) : ( + <> + )} +
+ ) : ( + filteredCodes.map((code) => ( + + )) + )} +
+
+
+ + ); +}; + +export default Page; + +const AuthNavbar: React.FC = () => { + const { isMobile, logout } = ensure(useContext(AppContext)); + + return ( + + + + + + } + > + } + onClick={logout} + > + {t("LOGOUT")} + + + + + ); +}; + +interface CodeDisplayProps { + code: Code; +} + +const CodeDisplay: React.FC = ({ 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 | 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 ( +
+ {errorMessage ? ( + + ) : ( + + + + + )} +
+ ); +}; + +interface OTPDisplayProps { + code: Code; + otp: string; + nextOTP: string; +} + +const OTPDisplay: React.FC = ({ code, otp, nextOTP }) => { + return ( +
+ +
+
+

+ {code.issuer ?? ""} +

+

+ {code.account ?? ""} +

+

+ {otp} +

+
+
+
+

+ {t("AUTH_NEXT")} +

+

+ {nextOTP} +

+
+
+
+ ); +}; + +interface CodeValidityBarProps { + code: Code; +} + +const CodeValidityBar: React.FC = ({ 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 ( +
+ ); +}; + +interface UnparseableCodeProps { + code: Code; + errorMessage: string; +} + +const UnparseableCode: React.FC = ({ + code, + errorMessage, +}) => { + const [showRawData, setShowRawData] = useState(false); + + return ( +
+
{code.issuer}
+
{errorMessage}
+
+ {showRawData ? ( +
setShowRawData(false)}> + {code.uriString} +
+ ) : ( +
setShowRawData(true)}>Show rawData
+ )} +
+
+ ); +}; + +const Footer: React.FC = () => { + return ( + +

{t("AUTH_DOWNLOAD_MOBILE_APP")}

+ + + +
+ ); +}; + +const Footer_ = styled("div")` + margin-block-start: 2rem; + margin-block-end: 4rem; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +`; diff --git a/web/apps/auth/src/pages/auth/index.tsx b/web/apps/auth/src/pages/auth/index.tsx deleted file mode 100644 index 55dc33ce6..000000000 --- a/web/apps/auth/src/pages/auth/index.tsx +++ /dev/null @@ -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 ( - <> - - - - - ); - } - - return ( - <> - -
-
- {filteredCodes.length === 0 && searchTerm.length === 0 ? ( - <> - ) : ( - setSearchTerm(e.target.value)} - variant="filled" - style={{ width: "350px" }} - value={searchTerm} - autoFocus - /> - )} - -
-
- {filteredCodes.length === 0 ? ( -
- {searchTerm.length !== 0 ? ( -

{t("NO_RESULTS")}

- ) : ( -
- )} -
- ) : ( - filteredCodes.map((code) => ( - - )) - )} -
-
- -
-
- - ); -}; - -export default AuthenticatorCodesPage; diff --git a/web/apps/auth/src/pages/change-email.tsx b/web/apps/auth/src/pages/change-email.tsx new file mode 100644 index 000000000..89a765fbf --- /dev/null +++ b/web/apps/auth/src/pages/change-email.tsx @@ -0,0 +1,6 @@ +import Page_ from "@ente/accounts/pages/change-email"; +import { useAppContext } from "./_app"; + +const Page = () => ; + +export default Page; diff --git a/web/apps/auth/src/pages/change-email/index.tsx b/web/apps/auth/src/pages/change-email/index.tsx deleted file mode 100644 index 8be39d9e8..000000000 --- a/web/apps/auth/src/pages/change-email/index.tsx +++ /dev/null @@ -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 ; -} diff --git a/web/apps/auth/src/pages/change-password.tsx b/web/apps/auth/src/pages/change-password.tsx new file mode 100644 index 000000000..ed82edd92 --- /dev/null +++ b/web/apps/auth/src/pages/change-password.tsx @@ -0,0 +1,6 @@ +import Page_ from "@ente/accounts/pages/change-password"; +import { useAppContext } from "./_app"; + +const Page = () => ; + +export default Page; diff --git a/web/apps/auth/src/pages/change-password/index.tsx b/web/apps/auth/src/pages/change-password/index.tsx deleted file mode 100644 index 612288049..000000000 --- a/web/apps/auth/src/pages/change-password/index.tsx +++ /dev/null @@ -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 ; -} diff --git a/web/apps/auth/src/pages/credentials.tsx b/web/apps/auth/src/pages/credentials.tsx new file mode 100644 index 000000000..070aace4a --- /dev/null +++ b/web/apps/auth/src/pages/credentials.tsx @@ -0,0 +1,6 @@ +import Page_ from "@ente/accounts/pages/credentials"; +import { useAppContext } from "./_app"; + +const Page = () => ; + +export default Page; diff --git a/web/apps/auth/src/pages/credentials/index.tsx b/web/apps/auth/src/pages/credentials/index.tsx deleted file mode 100644 index 9b3c0c9c5..000000000 --- a/web/apps/auth/src/pages/credentials/index.tsx +++ /dev/null @@ -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 ; -} diff --git a/web/apps/auth/src/pages/generate.tsx b/web/apps/auth/src/pages/generate.tsx new file mode 100644 index 000000000..c6804255a --- /dev/null +++ b/web/apps/auth/src/pages/generate.tsx @@ -0,0 +1,6 @@ +import Page_ from "@ente/accounts/pages/generate"; +import { useAppContext } from "./_app"; + +const Page = () => ; + +export default Page; diff --git a/web/apps/auth/src/pages/generate/index.tsx b/web/apps/auth/src/pages/generate/index.tsx deleted file mode 100644 index df3851357..000000000 --- a/web/apps/auth/src/pages/generate/index.tsx +++ /dev/null @@ -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 ; -} diff --git a/web/apps/auth/src/pages/index.tsx b/web/apps/auth/src/pages/index.tsx index 09b22fe49..e5540896e 100644 --- a/web/apps/auth/src/pages/index.tsx +++ b/web/apps/auth/src/pages/index.tsx @@ -1,8 +1,8 @@ import { PHOTOS_PAGES as PAGES } from "@ente/shared/constants/pages"; import { useRouter } from "next/router"; -import { useEffect } from "react"; +import React, { useEffect } from "react"; -const IndexPage = () => { +const Page: React.FC = () => { const router = useRouter(); useEffect(() => { router.push(PAGES.LOGIN); @@ -11,4 +11,4 @@ const IndexPage = () => { return <>; }; -export default IndexPage; +export default Page; diff --git a/web/apps/auth/src/pages/login.tsx b/web/apps/auth/src/pages/login.tsx new file mode 100644 index 000000000..1a7de0497 --- /dev/null +++ b/web/apps/auth/src/pages/login.tsx @@ -0,0 +1,6 @@ +import Page_ from "@ente/accounts/pages/login"; +import { useAppContext } from "./_app"; + +const Page = () => ; + +export default Page; diff --git a/web/apps/auth/src/pages/login/index.tsx b/web/apps/auth/src/pages/login/index.tsx deleted file mode 100644 index ee2407b54..000000000 --- a/web/apps/auth/src/pages/login/index.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import LoginPage from "@ente/accounts/pages/login"; -import { APPS } from "@ente/shared/apps/constants"; -import { AppContext } from "pages/_app"; -import { useContext } from "react"; - -export default function Login() { - const appContext = useContext(AppContext); - return ; -} diff --git a/web/apps/auth/src/pages/passkeys/finish.tsx b/web/apps/auth/src/pages/passkeys/finish.tsx new file mode 100644 index 000000000..866dcf9e3 --- /dev/null +++ b/web/apps/auth/src/pages/passkeys/finish.tsx @@ -0,0 +1,3 @@ +import Page from "@ente/accounts/pages/passkeys/finish"; + +export default Page; diff --git a/web/apps/auth/src/pages/passkeys/finish/index.tsx b/web/apps/auth/src/pages/passkeys/finish/index.tsx deleted file mode 100644 index 289f351de..000000000 --- a/web/apps/auth/src/pages/passkeys/finish/index.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import PasskeysFinishPage from "@ente/accounts/pages/passkeys/finish"; - -const PasskeysFinish = () => { - return ( - <> - - - ); -}; - -export default PasskeysFinish; diff --git a/web/apps/auth/src/pages/recover.tsx b/web/apps/auth/src/pages/recover.tsx new file mode 100644 index 000000000..d825729e5 --- /dev/null +++ b/web/apps/auth/src/pages/recover.tsx @@ -0,0 +1,6 @@ +import Page_ from "@ente/accounts/pages/recover"; +import { useAppContext } from "./_app"; + +const Page = () => ; + +export default Page; diff --git a/web/apps/auth/src/pages/recover/index.tsx b/web/apps/auth/src/pages/recover/index.tsx deleted file mode 100644 index 64404ca35..000000000 --- a/web/apps/auth/src/pages/recover/index.tsx +++ /dev/null @@ -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 ; -} diff --git a/web/apps/auth/src/pages/signup.tsx b/web/apps/auth/src/pages/signup.tsx new file mode 100644 index 000000000..403d3e735 --- /dev/null +++ b/web/apps/auth/src/pages/signup.tsx @@ -0,0 +1,6 @@ +import Page_ from "@ente/accounts/pages/signup"; +import { useAppContext } from "./_app"; + +const Page = () => ; + +export default Page; diff --git a/web/apps/auth/src/pages/signup/index.tsx b/web/apps/auth/src/pages/signup/index.tsx deleted file mode 100644 index d272b5c51..000000000 --- a/web/apps/auth/src/pages/signup/index.tsx +++ /dev/null @@ -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 ; -} diff --git a/web/apps/auth/src/pages/two-factor/recover.tsx b/web/apps/auth/src/pages/two-factor/recover.tsx new file mode 100644 index 000000000..d3f40be49 --- /dev/null +++ b/web/apps/auth/src/pages/two-factor/recover.tsx @@ -0,0 +1,6 @@ +import Page_ from "@ente/accounts/pages/two-factor/recover"; +import { useAppContext } from "../_app"; + +const Page = () => ; + +export default Page; diff --git a/web/apps/auth/src/pages/two-factor/recover/index.tsx b/web/apps/auth/src/pages/two-factor/recover/index.tsx deleted file mode 100644 index a67c7b816..000000000 --- a/web/apps/auth/src/pages/two-factor/recover/index.tsx +++ /dev/null @@ -1,9 +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 ; -} diff --git a/web/apps/auth/src/pages/two-factor/setup.tsx b/web/apps/auth/src/pages/two-factor/setup.tsx new file mode 100644 index 000000000..12716e2df --- /dev/null +++ b/web/apps/auth/src/pages/two-factor/setup.tsx @@ -0,0 +1,6 @@ +import Page_ from "@ente/accounts/pages/two-factor/setup"; +import { useAppContext } from "../_app"; + +const Page = () => ; + +export default Page; diff --git a/web/apps/auth/src/pages/two-factor/setup/index.tsx b/web/apps/auth/src/pages/two-factor/setup/index.tsx deleted file mode 100644 index b007ab01b..000000000 --- a/web/apps/auth/src/pages/two-factor/setup/index.tsx +++ /dev/null @@ -1,9 +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 ; -} diff --git a/web/apps/auth/src/pages/two-factor/verify.tsx b/web/apps/auth/src/pages/two-factor/verify.tsx new file mode 100644 index 000000000..7c682b1b9 --- /dev/null +++ b/web/apps/auth/src/pages/two-factor/verify.tsx @@ -0,0 +1,6 @@ +import Page_ from "@ente/accounts/pages/two-factor/verify"; +import { useAppContext } from "../_app"; + +const Page = () => ; + +export default Page; diff --git a/web/apps/auth/src/pages/two-factor/verify/index.tsx b/web/apps/auth/src/pages/two-factor/verify/index.tsx deleted file mode 100644 index 2243a4354..000000000 --- a/web/apps/auth/src/pages/two-factor/verify/index.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import TwoFactorVerifyPage from "@ente/accounts/pages/two-factor/verify"; -import { APPS } from "@ente/shared/apps/constants"; -import { useContext } from "react"; -import { AppContext } from "../../_app"; - -export default function TwoFactorVerify() { - const appContext = useContext(AppContext); - return ; -} diff --git a/web/apps/auth/src/pages/verify.tsx b/web/apps/auth/src/pages/verify.tsx new file mode 100644 index 000000000..bb2dc8778 --- /dev/null +++ b/web/apps/auth/src/pages/verify.tsx @@ -0,0 +1,6 @@ +import Page_ from "@ente/accounts/pages/verify"; +import { useAppContext } from "./_app"; + +const Page = () => ; + +export default Page; diff --git a/web/apps/auth/src/pages/verify/index.tsx b/web/apps/auth/src/pages/verify/index.tsx deleted file mode 100644 index 5171462e7..000000000 --- a/web/apps/auth/src/pages/verify/index.tsx +++ /dev/null @@ -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 ; -} diff --git a/web/apps/auth/src/services/code.ts b/web/apps/auth/src/services/code.ts new file mode 100644 index 000000000..1df085b4e --- /dev/null +++ b/web/apps/auth/src/services/code.ts @@ -0,0 +1,239 @@ +import { ensure } from "@/utils/ensure"; +import { HOTP, TOTP } from "otpauth"; +import { Steam } from "./steam"; + +/** + * A parsed representation of an *OTP code URI. + * + * This is all the data we need to drive a OTP generator. + */ +export interface Code { + /** A unique id for the corresponding "auth entity" in our system. */ + id: string; + /** The type of the code. */ + type: "totp" | "hotp" | "steam"; + /** The user's account or email for which this code is used. */ + account?: string; + /** The name of the entity that issued this code. */ + issuer: string; + /** + * Length of the generated OTP. + * + * This is vernacularly called "digits", which is an accurate description + * for the OG TOTP/HOTP codes. However, steam codes are not just digits, so + * we name this as a content-neutral "length". + */ + length: number; + /** + * The time period (in seconds) for which a single OTP generated from this + * code remains valid. + */ + period: number; + /** The (HMAC) algorithm used by the OTP generator. */ + algorithm: "sha1" | "sha256" | "sha512"; + /** + * HOTP counter. + * + * Only valid for HOTP codes. It might be even missing for HOTP codes, in + * which case we should start from 0. + */ + counter?: number; + /** + * The secret that is used to drive the OTP generator. + * + * This is an arbitrary key encoded in Base32 that drives the HMAC (in a + * {@link type}-specific manner). + */ + secret: string; + /** The original string from which this code was generated. */ + uriString: string; +} + +/** + * Convert a OTP code URI into its parse representation, a {@link Code}. + * + * @param id A unique ID of this code within the auth app. + * + * @param uriString A string specifying how to generate a TOTP/HOTP/Steam OTP + * code. These strings are of the form: + * + * - (TOTP) + * otpauth://totp/ACME:user@example.org?algorithm=SHA1&digits=6&issuer=acme&period=30&secret=ALPHANUM + * + * - (HOTP) + * otpauth://hotp/Test?secret=AAABBBCCCDDDEEEFFF&issuer=Test&counter=0 + * + * - (Steam) + * otpauth://steam/Steam:SteamAccount?algorithm=SHA1&digits=5&issuer=Steam&period=30&secret=AAABBBCCCDDDEEEFFF + * + * See also `auth/test/models/code_test.dart`. + */ +export const codeFromURIString = (id: string, uriString: string): Code => { + try { + return _codeFromURIString(id, uriString); + } catch (e) { + // We might have legacy encodings of account names that contain a "#", + // which causes the rest of the URL to be treated as a fragment, and + // ignored. See if this was potentially such a case, otherwise rethrow. + if (uriString.includes("#")) + return _codeFromURIString(id, uriString.replaceAll("#", "%23")); + throw e; + } +}; + +const _codeFromURIString = (id: string, uriString: string): Code => { + const url = new URL(uriString); + + // A URL like + // + // new URL("otpauth://hotp/Test?secret=AAABBBCCCDDDEEEFFF&issuer=Test&counter=0") + // + // is parsed differently by the browser and Node depending on the scheme. + // When the scheme is http(s), then both of them consider "hotp" as the + // `host`. However, when the scheme is "otpauth", as is our case, the + // browser considers the entire thing as part of the pathname. so we get. + // + // host: "" + // pathname: "//hotp/Test" + // + // Since this code run on browsers only, we parse as per that behaviour. + + const [type, path] = parsePathname(url); + + return { + id, + type, + account: parseAccount(path), + issuer: parseIssuer(url, path), + length: parseLength(url, type), + period: parsePeriod(url), + algorithm: parseAlgorithm(url), + counter: parseCounter(url), + secret: parseSecret(url), + uriString, + }; +}; + +const parsePathname = (url: URL): [type: Code["type"], path: string] => { + const p = url.pathname.toLowerCase(); + if (p.startsWith("//totp")) return ["totp", url.pathname.slice(6)]; + if (p.startsWith("//hotp")) return ["hotp", url.pathname.slice(6)]; + if (p.startsWith("//steam")) return ["steam", url.pathname.slice(7)]; + throw new Error(`Unsupported code or unparseable path "${url.pathname}"`); +}; + +const parseAccount = (path: string): string | undefined => { + // "/ACME:user@example.org" => "user@example.org" + let p = decodeURIComponent(path); + if (p.startsWith("/")) p = p.slice(1); + if (p.includes(":")) p = p.split(":").slice(1).join(":"); + return p; +}; + +const parseIssuer = (url: URL, path: string): string => { + // If there is a "issuer" search param, use that. + let issuer = url.searchParams.get("issuer"); + if (issuer) { + // This is to handle bug in old versions of Ente Auth app. + if (issuer.endsWith("period")) { + issuer = issuer.substring(0, issuer.length - 6); + } + return issuer; + } + + // Otherwise use the `prefix:` from the account as the issuer. + // "/ACME:user@example.org" => "ACME" + let p = decodeURIComponent(path); + if (p.startsWith("/")) p = p.slice(1); + + if (p.includes(":")) p = ensure(p.split(":")[0]); + else if (p.includes("-")) p = ensure(p.split("-")[0]); + + return p; +}; + +/** + * Parse the length of the generated code. + * + * The URI query param is called digits since originally TOTP/HOTP codes used + * this for generating numeric codes. Now we also support steam, which instead + * shows non-numeric codes, and also with a different default length of 5. + */ +const parseLength = (url: URL, type: Code["type"]): number => { + const defaultLength = type == "steam" ? 5 : 6; + return parseInt(url.searchParams.get("digits") ?? "", 10) || defaultLength; +}; + +const parsePeriod = (url: URL): number => + parseInt(url.searchParams.get("period") ?? "", 10) || 30; + +const parseAlgorithm = (url: URL): Code["algorithm"] => { + switch (url.searchParams.get("algorithm")?.toLowerCase()) { + case "sha256": + return "sha256"; + case "sha512": + return "sha512"; + default: + return "sha1"; + } +}; + +const parseCounter = (url: URL): number | undefined => { + const c = url.searchParams.get("counter"); + return c ? parseInt(c, 10) : undefined; +}; + +const parseSecret = (url: URL): string => + ensure(url.searchParams.get("secret")).replaceAll(" ", "").toUpperCase(); + +/** + * Generate a pair of OTPs (one time passwords) from the given {@link code}. + * + * @param code The parsed code data, including the secret and code type. + * + * @returns a pair of OTPs, the current one and the next one, using the given + * {@link code}. + */ +export const generateOTPs = (code: Code): [otp: string, nextOTP: string] => { + let otp: string; + let nextOTP: string; + switch (code.type) { + case "totp": { + const totp = new TOTP({ + secret: code.secret, + algorithm: code.algorithm, + period: code.period, + digits: code.length, + }); + otp = totp.generate(); + nextOTP = totp.generate({ + timestamp: Date.now() + code.period * 1000, + }); + break; + } + + case "hotp": { + const counter = code.counter || 0; + const hotp = new HOTP({ + secret: code.secret, + counter: counter, + algorithm: code.algorithm, + }); + otp = hotp.generate({ counter }); + nextOTP = hotp.generate({ counter: counter + 1 }); + break; + } + + case "steam": { + const steam = new Steam({ + secret: code.secret, + }); + otp = steam.generate(); + nextOTP = steam.generate({ + timestamp: Date.now() + code.period * 1000, + }); + break; + } + } + return [otp, nextOTP]; +}; diff --git a/web/apps/auth/src/services/index.ts b/web/apps/auth/src/services/remote.ts similarity index 76% rename from web/apps/auth/src/services/index.ts rename to web/apps/auth/src/services/remote.ts index 5fd032215..9885e0b75 100644 --- a/web/apps/auth/src/services/index.ts +++ b/web/apps/auth/src/services/remote.ts @@ -6,10 +6,10 @@ import { getEndpoint } from "@ente/shared/network/api"; import { getToken } from "@ente/shared/storage/localStorage/helpers"; import { getActualKey } from "@ente/shared/user"; import { HttpStatusCode } from "axios"; -import { AuthEntity, AuthKey } from "types/api"; -import { Code } from "types/code"; +import { codeFromURIString, type Code } from "services/code"; const ENDPOINT = getEndpoint(); + export const getAuthCodes = async (): Promise => { const masterKey = await getActualKey(); try { @@ -26,6 +26,9 @@ export const getAuthCodes = async (): Promise => { authEntity .filter((f) => !f.isDeleted) .map(async (entity) => { + if (!entity.id) return undefined; + if (!entity.encryptedData) return undefined; + if (!entity.header) return undefined; try { const decryptedCode = await cryptoWorker.decryptMetadata( @@ -33,17 +36,15 @@ export const getAuthCodes = async (): Promise => { entity.header, authenticatorKey, ); - return Code.fromRawData(entity.id, decryptedCode); + return codeFromURIString(entity.id, decryptedCode); } catch (e) { - log.error(`failed to parse codeId = ${entity.id}`); - return null; + log.error(`Failed to parse codeID ${entity.id}`, e); + return undefined; } }), ); - // Remove null and undefined values - const filteredAuthCodes = authCodes.filter( - (f) => f !== null && f !== undefined, - ); + // Remove undefined values + const filteredAuthCodes = authCodes.filter((f): f is Code => !!f); filteredAuthCodes.sort((a, b) => { if (a.issuer && b.issuer) { return a.issuer.localeCompare(b.issuer); @@ -58,13 +59,27 @@ export const getAuthCodes = async (): Promise => { }); return filteredAuthCodes; } catch (e) { - if (e.message !== CustomError.AUTH_KEY_NOT_FOUND) { + if (e instanceof Error && e.message != CustomError.AUTH_KEY_NOT_FOUND) { log.error("get authenticator entities failed", e); } throw e; } }; +interface AuthEntity { + id: string; + encryptedData: string | null; + header: string | null; + isDeleted: boolean; + createdAt: number; + updatedAt: number; +} + +interface AuthKey { + encryptedKey: string; + header: string; +} + export const getAuthKey = async (): Promise => { try { const resp = await HTTPService.get( @@ -78,7 +93,7 @@ export const getAuthKey = async (): Promise => { } catch (e) { if ( e instanceof ApiError && - e.httpStatusCode === HttpStatusCode.NotFound + e.httpStatusCode == HttpStatusCode.NotFound ) { throw Error(CustomError.AUTH_KEY_NOT_FOUND); } else { diff --git a/web/apps/auth/src/services/steam.ts b/web/apps/auth/src/services/steam.ts new file mode 100644 index 000000000..f214640c2 --- /dev/null +++ b/web/apps/auth/src/services/steam.ts @@ -0,0 +1,74 @@ +import jsSHA from "jssha"; +import { Secret } from "otpauth"; + +/** + * Steam OTPs. + * + * Steam's algorithm is a custom variant of TOTP that uses a 26-character + * alphabet instead of digits. + * + * A Dart implementation of the algorithm can be found in + * https://github.com/elliotwutingfeng/steam_totp/blob/main/lib/src/steam_totp_base.dart + * (MIT license), and we use that as a reference. Our implementation is written + * in the style of the other TOTP/HOTP classes that are provided by the otpauth + * JS library that we use for the normal TOTP/HOTP generation + * https://github.com/hectorm/otpauth/blob/master/src/hotp.js (MIT license). + */ +export class Steam { + secret: Secret; + period: number; + + constructor({ secret }: { secret: string }) { + this.secret = Secret.fromBase32(secret); + this.period = 30; + } + + generate({ timestamp }: { timestamp: number } = { timestamp: Date.now() }) { + // Same as regular TOTP. + const counter = Math.floor(timestamp / 1000 / this.period); + + // Same as regular HOTP, but algorithm is fixed to SHA-1. + const digest = sha1HMACDigest(this.secret.buffer, uintToArray(counter)); + + // Same calculation as regular HOTP. + const offset = digest[digest.length - 1] & 15; + let otp = + ((digest[offset] & 127) << 24) | + ((digest[offset + 1] & 255) << 16) | + ((digest[offset + 2] & 255) << 8) | + (digest[offset + 3] & 255); + + // However, instead of using this as the OTP, use it to index into + // the steam OTP alphabet. + const alphabet = "23456789BCDFGHJKMNPQRTVWXY"; + const N = alphabet.length; + const steamOTP = []; + for (let i = 0; i < 5; i++) { + steamOTP.push(alphabet[otp % N]); + otp = Math.trunc(otp / N); + } + return steamOTP.join(""); + } +} + +// Equivalent to +// https://github.com/hectorm/otpauth/blob/master/src/utils/encoding/uint.js +const uintToArray = (n: number): Uint8Array => { + const result = new Uint8Array(8); + for (let i = 7; i >= 0; i--) { + result[i] = n & 255; + n >>= 8; + } + return result; +}; + +// We don't necessarily need a dependency on `jssha`, we could use SubtleCrypto +// here too. However, SubtleCrypto has an async interface, and we already have a +// transitive dependency on `jssha` via `otpauth`, so just using it here doesn't +// increase our bundle size any further. +const sha1HMACDigest = (key: ArrayBuffer, message: Uint8Array) => { + const hmac = new jsSHA("SHA-1", "UINT8ARRAY"); + hmac.setHMACKey(key, "ARRAYBUFFER"); + hmac.update(message); + return hmac.getHMAC("UINT8ARRAY"); +}; diff --git a/web/apps/auth/src/types/api.ts b/web/apps/auth/src/types/api.ts deleted file mode 100644 index 569df8185..000000000 --- a/web/apps/auth/src/types/api.ts +++ /dev/null @@ -1,13 +0,0 @@ -export interface AuthEntity { - id: string; - encryptedData: string | null; - header: string | null; - isDeleted: boolean; - createdAt: number; - updatedAt: number; -} - -export interface AuthKey { - encryptedKey: string; - header: string; -} diff --git a/web/apps/auth/src/types/code.ts b/web/apps/auth/src/types/code.ts deleted file mode 100644 index d61a2dcd6..000000000 --- a/web/apps/auth/src/types/code.ts +++ /dev/null @@ -1,182 +0,0 @@ -import { URI } from "vscode-uri"; - -type Type = "totp" | "TOTP" | "hotp" | "HOTP"; - -type AlgorithmType = - | "sha1" - | "SHA1" - | "sha256" - | "SHA256" - | "sha512" - | "SHA512"; - -export class Code { - static readonly defaultDigits = 6; - static readonly defaultAlgo = "sha1"; - static readonly defaultPeriod = 30; - - // id for the corresponding auth entity - id?: String; - account: string; - issuer: string; - digits?: number; - period: number; - secret: string; - algorithm: AlgorithmType; - type: Type; - rawData?: string; - - constructor( - account: string, - issuer: string, - digits: number | undefined, - period: number, - secret: string, - algorithm: AlgorithmType, - type: Type, - rawData?: string, - id?: string, - ) { - this.account = account; - this.issuer = issuer; - this.digits = digits; - this.period = period; - this.secret = secret; - this.algorithm = algorithm; - this.type = type; - this.rawData = rawData; - this.id = id; - } - - static fromRawData(id: string, rawData: string): Code { - let santizedRawData = rawData - .replace(/\+/g, "%2B") - .replace(/:/g, "%3A") - .replaceAll("\r", ""); - if (santizedRawData.startsWith('"')) { - santizedRawData = santizedRawData.substring(1); - } - if (santizedRawData.endsWith('"')) { - santizedRawData = santizedRawData.substring( - 0, - santizedRawData.length - 1, - ); - } - - const uriParams = {}; - const searchParamsString = - decodeURIComponent(santizedRawData).split("?")[1]; - searchParamsString.split("&").forEach((pair) => { - const [key, value] = pair.split("="); - uriParams[key] = value; - }); - - const uri = URI.parse(santizedRawData); - let uriPath = decodeURIComponent(uri.path); - if ( - uriPath.startsWith("/otpauth://") || - uriPath.startsWith("otpauth://") - ) { - uriPath = uriPath.split("otpauth://")[1]; - } else if (uriPath.startsWith("otpauth%3A//")) { - uriPath = uriPath.split("otpauth%3A//")[1]; - } - - return new Code( - Code._getAccount(uriPath), - Code._getIssuer(uriPath, uriParams), - Code._getDigits(uriParams), - Code._getPeriod(uriParams), - Code.getSanitizedSecret(uriParams), - Code._getAlgorithm(uriParams), - Code._getType(uriPath), - rawData, - id, - ); - } - - private static _getAccount(uriPath: string): string { - try { - const path = decodeURIComponent(uriPath); - if (path.includes(":")) { - return path.split(":")[1]; - } else if (path.includes("/")) { - return path.split("/")[1]; - } - } catch (e) { - return ""; - } - } - - private static _getIssuer( - uriPath: string, - uriParams: { get?: any }, - ): string { - try { - if (uriParams["issuer"] !== undefined) { - let issuer = uriParams["issuer"]; - // This is to handle bug in the ente auth app - if (issuer.endsWith("period")) { - issuer = issuer.substring(0, issuer.length - 6); - } - return issuer; - } - let path = decodeURIComponent(uriPath); - if (path.startsWith("totp/") || path.startsWith("hotp/")) { - path = path.substring(5); - } - if (path.includes(":")) { - return path.split(":")[0]; - } else if (path.includes("-")) { - return path.split("-")[0]; - } - return path; - } catch (e) { - return ""; - } - } - - private static _getDigits(uriParams): number { - try { - return parseInt(uriParams["digits"], 10) || Code.defaultDigits; - } catch (e) { - return Code.defaultDigits; - } - } - - private static _getPeriod(uriParams): number { - try { - return parseInt(uriParams["period"], 10) || Code.defaultPeriod; - } catch (e) { - return Code.defaultPeriod; - } - } - - private static _getAlgorithm(uriParams): AlgorithmType { - try { - const algorithm = uriParams["algorithm"].toLowerCase(); - if (algorithm === "sha256") { - return algorithm; - } else if (algorithm === "sha512") { - return algorithm; - } - } catch (e) { - // nothing - } - return "sha1"; - } - - private static _getType(uriPath: string): Type { - const oauthType = uriPath.split("/")[0].substring(0); - if (oauthType.toLowerCase() === "totp") { - return "totp"; - } else if (oauthType.toLowerCase() === "hotp") { - return "hotp"; - } - throw new Error(`Unsupported format with host ${oauthType}`); - } - - static getSanitizedSecret(uriParams): string { - return uriParams["secret"].replace(/ /g, "").toUpperCase(); - } -} diff --git a/web/apps/auth/tsconfig.json b/web/apps/auth/tsconfig.json index d9092609d..507ae19bd 100644 --- a/web/apps/auth/tsconfig.json +++ b/web/apps/auth/tsconfig.json @@ -1,24 +1,20 @@ { - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "baseUrl": "./src", - "downlevelIteration": true, - "jsx": "preserve", - "jsxImportSource": "@emotion/react", - "lib": ["dom", "dom.iterable", "esnext", "webworker"], - "noImplicitAny": false, - "noUnusedLocals": false, - "noUnusedParameters": false, - "strictNullChecks": false, - "target": "es5", - "useUnknownInCatchVariables": false - }, + "extends": "@/build-config/tsconfig-next.json", "include": [ + "src", "next-env.d.ts", - "**/*.ts", - "**/*.tsx", - "**/*.js", + "../../packages/next/global-electron.d.ts", "../../packages/shared/themes/mui-theme.d.ts" ], - "exclude": ["node_modules", "out", ".next", "thirdparty"] + "compilerOptions": { + /* Set the base directory from which to resolve bare module names */ + "baseUrl": "./src", + + /* This is hard to enforce in certain cases where we do a lot of array + indexing, e.g. image/ML ops, and TS doesn't currently have a way to + disable this for blocks of code. */ + "noUncheckedIndexedAccess": false, + /* MUI doesn't play great with exactOptionalPropertyTypes currently. */ + "exactOptionalPropertyTypes": false + } } diff --git a/web/apps/photos/package.json b/web/apps/photos/package.json index 8d515c07c..0ec924b29 100644 --- a/web/apps/photos/package.json +++ b/web/apps/photos/package.json @@ -28,7 +28,6 @@ "localforage": "^1.9.0", "memoize-one": "^6.0.0", "ml-matrix": "^6.11", - "otpauth": "^9.0.2", "p-debounce": "^4.0.0", "p-queue": "^7.1.0", "photoswipe": "file:./thirdparty/photoswipe", @@ -44,7 +43,6 @@ "similarity-transformation": "^0.0.1", "transformation-matrix": "^2.16", "uuid": "^9.0.1", - "vscode-uri": "^3.0.7", "xml-js": "^1.6.11", "zxcvbn": "^4.4.2" }, diff --git a/web/apps/photos/src/components/AuthenticateUserModal.tsx b/web/apps/photos/src/components/AuthenticateUserModal.tsx index 97f47e7fb..52be5b7de 100644 --- a/web/apps/photos/src/components/AuthenticateUserModal.tsx +++ b/web/apps/photos/src/components/AuthenticateUserModal.tsx @@ -1,10 +1,10 @@ import log from "@/next/log"; import DialogBoxV2 from "@ente/shared/components/DialogBoxV2"; import VerifyMasterPasswordForm, { - VerifyMasterPasswordFormProps, + type VerifyMasterPasswordFormProps, } from "@ente/shared/components/VerifyMasterPasswordForm"; import { LS_KEYS, getData } from "@ente/shared/storage/localStorage"; -import { KeyAttributes, User } from "@ente/shared/user/types"; +import type { KeyAttributes, User } from "@ente/shared/user/types"; import { t } from "i18next"; import { AppContext } from "pages/_app"; import { useContext, useEffect, useState } from "react"; diff --git a/web/apps/photos/src/components/CaptionedText.tsx b/web/apps/photos/src/components/CaptionedText.tsx index 64d6c344d..5cc07a88e 100644 --- a/web/apps/photos/src/components/CaptionedText.tsx +++ b/web/apps/photos/src/components/CaptionedText.tsx @@ -1,5 +1,5 @@ import { VerticallyCenteredFlex } from "@ente/shared/components/Container"; -import { ButtonProps, Typography } from "@mui/material"; +import { Typography, type ButtonProps } from "@mui/material"; interface Iprops { mainText: string; diff --git a/web/apps/photos/src/components/CheckboxInput.tsx b/web/apps/photos/src/components/CheckboxInput.tsx index 7cdc95e3a..df139fd2e 100644 --- a/web/apps/photos/src/components/CheckboxInput.tsx +++ b/web/apps/photos/src/components/CheckboxInput.tsx @@ -3,7 +3,7 @@ import { FormControlLabel, FormGroup, Typography, - TypographyProps, + type TypographyProps, } from "@mui/material"; interface Iprops { diff --git a/web/apps/photos/src/components/Collections/CollectionInfoWithOptions.tsx b/web/apps/photos/src/components/Collections/CollectionInfoWithOptions.tsx index 405e22c28..925956d60 100644 --- a/web/apps/photos/src/components/Collections/CollectionInfoWithOptions.tsx +++ b/web/apps/photos/src/components/Collections/CollectionInfoWithOptions.tsx @@ -6,7 +6,7 @@ import PeopleIcon from "@mui/icons-material/People"; import { SetCollectionNamerAttributes } from "components/Collections/CollectionNamer"; import CollectionOptions from "components/Collections/CollectionOptions"; import { CollectionSummaryType } from "constants/collection"; -import { Dispatch, SetStateAction } from "react"; +import type { Dispatch, SetStateAction } from "react"; import { Collection, CollectionSummary } from "types/collection"; import { SetFilesDownloadProgressAttributesCreator } from "types/gallery"; import { shouldShowOptions } from "utils/collection"; diff --git a/web/apps/photos/src/components/Collections/CollectionNamer.tsx b/web/apps/photos/src/components/Collections/CollectionNamer.tsx index 0f7abf99f..ebe18127e 100644 --- a/web/apps/photos/src/components/Collections/CollectionNamer.tsx +++ b/web/apps/photos/src/components/Collections/CollectionNamer.tsx @@ -1,6 +1,6 @@ import DialogBoxV2 from "@ente/shared/components/DialogBoxV2"; import SingleInputForm, { - SingleInputFormProps, + type SingleInputFormProps, } from "@ente/shared/components/SingleInputForm"; import { t } from "i18next"; import React from "react"; diff --git a/web/apps/photos/src/components/Collections/CollectionOptions/AlbumCastDialog.tsx b/web/apps/photos/src/components/Collections/CollectionOptions/AlbumCastDialog.tsx index 8b92f1cbb..c56917dbf 100644 --- a/web/apps/photos/src/components/Collections/CollectionOptions/AlbumCastDialog.tsx +++ b/web/apps/photos/src/components/Collections/CollectionOptions/AlbumCastDialog.tsx @@ -4,7 +4,7 @@ import DialogBoxV2 from "@ente/shared/components/DialogBoxV2"; import EnteButton from "@ente/shared/components/EnteButton"; import EnteSpinner from "@ente/shared/components/EnteSpinner"; import SingleInputForm, { - SingleInputFormProps, + type SingleInputFormProps, } from "@ente/shared/components/SingleInputForm"; import { boxSeal } from "@ente/shared/crypto/internal/libsodium"; import castGateway from "@ente/shared/network/cast"; diff --git a/web/apps/photos/src/components/Collections/CollectionOptions/index.tsx b/web/apps/photos/src/components/Collections/CollectionOptions/index.tsx index ffdc6ad1f..61dcbc9e7 100644 --- a/web/apps/photos/src/components/Collections/CollectionOptions/index.tsx +++ b/web/apps/photos/src/components/Collections/CollectionOptions/index.tsx @@ -11,7 +11,8 @@ import { import { t } from "i18next"; import { AppContext } from "pages/_app"; import { GalleryContext } from "pages/gallery"; -import { Dispatch, SetStateAction, useContext, useRef, useState } from "react"; +import type { Dispatch, SetStateAction } from "react"; +import { useContext, useRef, useState } from "react"; import { Trans } from "react-i18next"; import * as CollectionAPI from "services/collectionService"; import * as TrashService from "services/trashService"; diff --git a/web/apps/photos/src/components/Collections/CollectionShare/emailShare/AddParticipantForm.tsx b/web/apps/photos/src/components/Collections/CollectionShare/emailShare/AddParticipantForm.tsx index 55daa1edc..84089743f 100644 --- a/web/apps/photos/src/components/Collections/CollectionShare/emailShare/AddParticipantForm.tsx +++ b/web/apps/photos/src/components/Collections/CollectionShare/emailShare/AddParticipantForm.tsx @@ -8,7 +8,7 @@ import MenuItemDivider from "components/Menu/MenuItemDivider"; import { MenuItemGroup } from "components/Menu/MenuItemGroup"; import MenuSectionTitle from "components/Menu/MenuSectionTitle"; import Avatar from "components/pages/gallery/Avatar"; -import { Formik, FormikHelpers } from "formik"; +import { Formik, type FormikHelpers } from "formik"; import { t } from "i18next"; import { useMemo, useState } from "react"; import * as Yup from "yup"; diff --git a/web/apps/photos/src/components/Collections/CollectionShare/publicShare/manage/linkPassword/setPassword.tsx b/web/apps/photos/src/components/Collections/CollectionShare/publicShare/manage/linkPassword/setPassword.tsx index 349e41d7b..c98926f91 100644 --- a/web/apps/photos/src/components/Collections/CollectionShare/publicShare/manage/linkPassword/setPassword.tsx +++ b/web/apps/photos/src/components/Collections/CollectionShare/publicShare/manage/linkPassword/setPassword.tsx @@ -1,5 +1,5 @@ import SingleInputForm, { - SingleInputFormProps, + type SingleInputFormProps, } from "@ente/shared/components/SingleInputForm"; import ComlinkCryptoWorker from "@ente/shared/crypto"; import { Dialog, Stack, Typography } from "@mui/material"; diff --git a/web/apps/photos/src/components/DeleteAccountModal.tsx b/web/apps/photos/src/components/DeleteAccountModal.tsx index d6eb3a037..3fce47f4d 100644 --- a/web/apps/photos/src/components/DeleteAccountModal.tsx +++ b/web/apps/photos/src/components/DeleteAccountModal.tsx @@ -3,7 +3,7 @@ import DialogBoxV2 from "@ente/shared/components/DialogBoxV2"; import EnteButton from "@ente/shared/components/EnteButton"; import { DELETE_ACCOUNT_EMAIL } from "@ente/shared/constants/urls"; import { Button, Link, Stack } from "@mui/material"; -import { Formik, FormikHelpers } from "formik"; +import { Formik, type FormikHelpers } from "formik"; import { t } from "i18next"; import { AppContext } from "pages/_app"; import { GalleryContext } from "pages/gallery"; diff --git a/web/apps/photos/src/components/DropdownInput.tsx b/web/apps/photos/src/components/DropdownInput.tsx index 76f9e7423..11591612a 100644 --- a/web/apps/photos/src/components/DropdownInput.tsx +++ b/web/apps/photos/src/components/DropdownInput.tsx @@ -6,7 +6,7 @@ import { SelectChangeEvent, Stack, Typography, - TypographyProps, + type TypographyProps, } from "@mui/material"; export interface DropdownOption { diff --git a/web/apps/photos/src/components/EnteSpinner.tsx b/web/apps/photos/src/components/EnteSpinner.tsx index 8a5d0a289..5e7b5e803 100644 --- a/web/apps/photos/src/components/EnteSpinner.tsx +++ b/web/apps/photos/src/components/EnteSpinner.tsx @@ -1,5 +1,5 @@ import CircularProgress, { - CircularProgressProps, + type CircularProgressProps, } from "@mui/material/CircularProgress"; export default function EnteSpinner(props: CircularProgressProps) { diff --git a/web/apps/photos/src/components/Menu/EnteMenuItem.tsx b/web/apps/photos/src/components/Menu/EnteMenuItem.tsx index 21a9889af..eb473ebd6 100644 --- a/web/apps/photos/src/components/Menu/EnteMenuItem.tsx +++ b/web/apps/photos/src/components/Menu/EnteMenuItem.tsx @@ -4,10 +4,10 @@ import { } from "@ente/shared/components/Container"; import { Box, - ButtonProps, MenuItem, Typography, - TypographyProps, + type ButtonProps, + type TypographyProps, } from "@mui/material"; import { CaptionedText } from "components/CaptionedText"; import PublicShareSwitch from "components/Collections/CollectionShare/publicShare/switch"; diff --git a/web/apps/photos/src/components/Notification.tsx b/web/apps/photos/src/components/Notification.tsx index f000d229e..f1e996b3c 100644 --- a/web/apps/photos/src/components/Notification.tsx +++ b/web/apps/photos/src/components/Notification.tsx @@ -2,12 +2,12 @@ import CloseIcon from "@mui/icons-material/Close"; import { Box, Button, - ButtonProps, Snackbar, Stack, SxProps, Theme, Typography, + type ButtonProps, } from "@mui/material"; import { NotificationAttributes } from "types/Notification"; diff --git a/web/apps/photos/src/components/PhotoViewer/FileInfo/FileNameEditDialog.tsx b/web/apps/photos/src/components/PhotoViewer/FileInfo/FileNameEditDialog.tsx index 214b120f1..c7f731367 100644 --- a/web/apps/photos/src/components/PhotoViewer/FileInfo/FileNameEditDialog.tsx +++ b/web/apps/photos/src/components/PhotoViewer/FileInfo/FileNameEditDialog.tsx @@ -1,6 +1,6 @@ import DialogBoxV2 from "@ente/shared/components/DialogBoxV2"; import SingleInputForm, { - SingleInputFormProps, + type SingleInputFormProps, } from "@ente/shared/components/SingleInputForm"; import { t } from "i18next"; diff --git a/web/apps/photos/src/components/PhotoViewer/FileInfo/MapButton.tsx b/web/apps/photos/src/components/PhotoViewer/FileInfo/MapButton.tsx index 12b665199..ea5005da5 100644 --- a/web/apps/photos/src/components/PhotoViewer/FileInfo/MapButton.tsx +++ b/web/apps/photos/src/components/PhotoViewer/FileInfo/MapButton.tsx @@ -1,5 +1,5 @@ -import { Button, ButtonProps, styled } from "@mui/material"; -import { CSSProperties } from "@mui/material/styles/createTypography"; +import { Button, styled, type ButtonProps } from "@mui/material"; +import { type CSSProperties } from "@mui/material/styles/createTypography"; export const MapButton = styled((props: ButtonProps) => (