[mob][photos] Resolve conflicts and merge main
This commit is contained in:
commit
99cf23d286
232 changed files with 1677 additions and 1441 deletions
14
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
14
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
@ -4,11 +4,12 @@ labels: ["triage"]
|
|||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: >
|
||||
Before opening a new issue, please ensure you are on the latest
|
||||
version (it might've already been fixed), and that you've searched
|
||||
for existing issues (please add you observations as a comment
|
||||
there instead of creating a duplicate).
|
||||
value: |
|
||||
Before opening a new bug report, please ensure
|
||||
1. you are on the latest version (it might've already been fixed),
|
||||
2. you've searched for existing issues (please add your observations as a comment there instead of creating a duplicate).
|
||||
|
||||
If you are self hosting, please create a community [Q&A](https://github.com/ente-io/ente/discussions/categories/q-a) instead.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Description
|
||||
|
@ -16,7 +17,8 @@ body:
|
|||
Please describe the bug. If possible, also include the steps to
|
||||
reproduce the behaviour, and the expected behaviour (sometimes
|
||||
bugs are just expectation mismatches, in which case this would be
|
||||
a good fit for Discussions).
|
||||
a good fit for [feature
|
||||
requests](https://github.com/ente-io/ente/discussions/categories/feature-requests)).
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
|
|
|
@ -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": "アップデートは利用できません"
|
||||
}
|
|
@ -30,7 +30,7 @@
|
|||
"compare-versions": "^6.1",
|
||||
"electron-log": "^5.1",
|
||||
"electron-store": "^8.2",
|
||||
"electron-updater": "^6.1",
|
||||
"electron-updater": "^6.2",
|
||||
"ffmpeg-static": "^5.2",
|
||||
"html-entities": "^2.5",
|
||||
"jpeg-js": "^0.4",
|
||||
|
|
|
@ -743,10 +743,10 @@ buffer@^5.1.0, buffer@^5.5.0:
|
|||
base64-js "^1.3.1"
|
||||
ieee754 "^1.1.13"
|
||||
|
||||
builder-util-runtime@9.2.3:
|
||||
version "9.2.3"
|
||||
resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz#0a82c7aca8eadef46d67b353c638f052c206b83c"
|
||||
integrity sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw==
|
||||
builder-util-runtime@9.2.4:
|
||||
version "9.2.4"
|
||||
resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz#13cd1763da621e53458739a1e63f7fcba673c42a"
|
||||
integrity sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==
|
||||
dependencies:
|
||||
debug "^4.3.4"
|
||||
sax "^1.2.4"
|
||||
|
@ -1251,12 +1251,12 @@ electron-store@^8.2:
|
|||
conf "^10.2.0"
|
||||
type-fest "^2.17.0"
|
||||
|
||||
electron-updater@^6.1:
|
||||
version "6.1.8"
|
||||
resolved "https://registry.yarnpkg.com/electron-updater/-/electron-updater-6.1.8.tgz#17637bca165322f4e526b13c99165f43e6f697d8"
|
||||
integrity sha512-hhOTfaFAd6wRHAfUaBhnAOYc+ymSGCWJLtFkw4xJqOvtpHmIdNHnXDV9m1MHC+A6q08Abx4Ykgyz/R5DGKNAMQ==
|
||||
electron-updater@^6.2:
|
||||
version "6.2.1"
|
||||
resolved "https://registry.yarnpkg.com/electron-updater/-/electron-updater-6.2.1.tgz#1c9adb9ba2a21a5dc50a8c434c45360d5e9fe6c9"
|
||||
integrity sha512-83eKIPW14qwZqUUM6wdsIRwVKZyjmHxQ4/8G+1C6iS5PdDt7b1umYQyj1/qPpH510GmHEQe4q0kCPe3qmb3a0Q==
|
||||
dependencies:
|
||||
builder-util-runtime "9.2.3"
|
||||
builder-util-runtime "9.2.4"
|
||||
fs-extra "^10.1.0"
|
||||
js-yaml "^4.1.0"
|
||||
lazy-val "^1.0.5"
|
||||
|
|
|
@ -163,6 +163,10 @@ export const sidebar = [
|
|||
text: "From Authy",
|
||||
link: "/auth/migration-guides/authy/",
|
||||
},
|
||||
{
|
||||
text: "From Steam",
|
||||
link: "/auth/migration-guides/steam/",
|
||||
},
|
||||
{
|
||||
text: "Exporting your data",
|
||||
link: "/auth/migration-guides/export",
|
||||
|
|
|
@ -7,4 +7,5 @@ description:
|
|||
# Migrating to/from Ente Auth
|
||||
|
||||
- [Migrating from Authy](authy/)
|
||||
- [Importing codes from Steam](steam/)
|
||||
- [Exporting your data out of Ente Auth](export)
|
||||
|
|
|
@ -9,21 +9,27 @@ 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
|
||||
> 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.
|
||||
to simplify the process and skip directly to generating a qr code to Ente
|
||||
Authenticator.
|
||||
|
||||
## Download/Install steamguard-cli
|
||||
|
||||
### Windows
|
||||
|
||||
1. Download `steamguard.exe` from the [releases page][releases].
|
||||
2. Place `steamguard.exe` in a folder of your choice. For this example, we will use `%USERPROFILE%\Desktop`.
|
||||
3. Open Powershell or Command Prompt. The prompt should be at `%USERPROFILE%` (eg. `C:\Users\<username>`).
|
||||
4. Use `cd` to change directory into the folder where you placed `steamguard.exe`. For this example, it would be `cd Desktop`.
|
||||
5. You should now be able to run `steamguard.exe` by typing `.\steamguard.exe --help` and pressing enter.
|
||||
2. Place `steamguard.exe` in a folder of your choice. For this example, we will
|
||||
use `%USERPROFILE%\Desktop`.
|
||||
3. Open Powershell or Command Prompt. The prompt should be at `%USERPROFILE%`
|
||||
(eg. `C:\Users\<username>`).
|
||||
4. Use `cd` to change directory into the folder where you placed
|
||||
`steamguard.exe`. For this example, it would be `cd Desktop`.
|
||||
5. You should now be able to run `steamguard.exe` by typing
|
||||
`.\steamguard.exe --help` and pressing enter.
|
||||
|
||||
### Linux
|
||||
|
||||
|
@ -31,6 +37,7 @@ to simplify the process and skip directly to generating a qr code to Ente Authen
|
|||
|
||||
1. Download the `.deb` from the [releases page][releases].
|
||||
2. Open a terminal and run this to install it:
|
||||
|
||||
```bash
|
||||
sudo dpkg -i ./steamguard-cli_<version>_amd64.deb
|
||||
```
|
||||
|
@ -38,12 +45,16 @@ sudo dpkg -i ./steamguard-cli_<version>_amd64.deb
|
|||
#### Other Linux
|
||||
|
||||
1. Download `steamguard` from the [releases page][releases]
|
||||
2. Make it executable, and move `steamguard` to `/usr/local/bin` or any other directory in your `$PATH`.
|
||||
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.
|
||||
|
||||
3. You should now be able to run `steamguard` by typing `steamguard --help` and
|
||||
pressing enter.
|
||||
|
||||
## Login to Steam account
|
||||
|
||||
|
@ -62,6 +73,7 @@ steamguard qr # print QR code for the first account in your maFiles
|
|||
steamguard -u <account name> qr # print QR code for a specific account
|
||||
```
|
||||
|
||||
Open Ente Auth, press the '+' button, select `Scan a QR code`, and scan the qr code.
|
||||
Open Ente Auth, press the '+' button, select `Scan a QR code`, and scan the qr
|
||||
code.
|
||||
|
||||
You should now have your steam code inside Ente Auth
|
||||
|
|
|
@ -78,3 +78,23 @@ To summarize:
|
|||
Set the S3 bucket `endpoint` in `credentials.yaml` to a `yourserverip:3200` or
|
||||
some such IP/hostname that accessible from both where you are running the Ente
|
||||
clients (e.g. the mobile app) and also from within the Docker compose cluster.
|
||||
|
||||
### 403 Forbidden
|
||||
|
||||
If museum (`2`) is able to make a network connection to your S3 bucket (`3`) but
|
||||
uploads are still failing, it could be a credentials or permissions issue. A
|
||||
telltale sign of this is that in the museum logs you can see `403 Forbidden`
|
||||
errors about it not able to find the size of a file even though the
|
||||
corresponding object exists in the S3 bucket.
|
||||
|
||||
To fix these, you should ensure the following:
|
||||
|
||||
1. The bucket CORS rules do not allow museum to access these objects.
|
||||
|
||||
> For uploading files from the browser, you will need to currently set
|
||||
> allowedOrigins to "\*", and allow the "X-Auth-Token", "X-Client-Package"
|
||||
> headers configuration too.
|
||||
> [Here is an example of a working configuration](https://github.com/ente-io/ente/discussions/1764#discussioncomment-9478204).
|
||||
|
||||
2. The credentials are not being picked up (you might be setting the correct
|
||||
creds, but not in the place where museum picks them from).
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -35,6 +35,15 @@ class FaceMLDataDB {
|
|||
|
||||
static final FaceMLDataDB instance = FaceMLDataDB._privateConstructor();
|
||||
|
||||
static final _migrationScripts = [
|
||||
createFacesTable,
|
||||
createFaceClustersTable,
|
||||
createClusterPersonTable,
|
||||
createClusterSummaryTable,
|
||||
createNotPersonFeedbackTable,
|
||||
fcClusterIDIndex,
|
||||
];
|
||||
|
||||
// only have a single app-wide reference to the database
|
||||
static Future<SqliteDatabase>? _sqliteAsyncDBFuture;
|
||||
|
||||
|
@ -50,17 +59,42 @@ class FaceMLDataDB {
|
|||
_logger.info("Opening sqlite_async access: DB path " + databaseDirectory);
|
||||
final asyncDBConnection =
|
||||
SqliteDatabase(path: databaseDirectory, maxReaders: 2);
|
||||
await _onCreate(asyncDBConnection);
|
||||
final stopwatch = Stopwatch()..start();
|
||||
_logger.info("FaceMLDataDB: Starting migration");
|
||||
await _migrate(asyncDBConnection);
|
||||
_logger.info(
|
||||
"FaceMLDataDB Migration took ${stopwatch.elapsedMilliseconds} ms",
|
||||
);
|
||||
stopwatch.stop();
|
||||
|
||||
return asyncDBConnection;
|
||||
}
|
||||
|
||||
Future<void> _onCreate(SqliteDatabase asyncDBConnection) async {
|
||||
await asyncDBConnection.execute(createFacesTable);
|
||||
await asyncDBConnection.execute(createFaceClustersTable);
|
||||
await asyncDBConnection.execute(createClusterPersonTable);
|
||||
await asyncDBConnection.execute(createClusterSummaryTable);
|
||||
await asyncDBConnection.execute(createNotPersonFeedbackTable);
|
||||
await asyncDBConnection.execute(fcClusterIDIndex);
|
||||
Future<void> _migrate(
|
||||
SqliteDatabase database,
|
||||
) async {
|
||||
final result = await database.execute('PRAGMA user_version');
|
||||
final currentVersion = result[0]['user_version'] as int;
|
||||
final toVersion = _migrationScripts.length;
|
||||
|
||||
if (currentVersion < toVersion) {
|
||||
_logger.info("Migrating database from $currentVersion to $toVersion");
|
||||
await database.writeTransaction((tx) async {
|
||||
for (int i = currentVersion + 1; i <= toVersion; i++) {
|
||||
try {
|
||||
await tx.execute(_migrationScripts[i - 1]);
|
||||
} catch (e) {
|
||||
_logger.severe("Error running migration script index ${i - 1}", e);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
await tx.execute('PRAGMA user_version = $toVersion');
|
||||
});
|
||||
} else if (currentVersion > toVersion) {
|
||||
throw AssertionError(
|
||||
"currentVersion($currentVersion) cannot be greater than toVersion($toVersion)",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// bulkInsertFaces inserts the faces in the database in batches of 1000.
|
||||
|
@ -195,10 +229,10 @@ class FaceMLDataDB {
|
|||
final db = await instance.asyncDB;
|
||||
|
||||
await db.execute(deleteFacesTable);
|
||||
await db.execute(dropClusterPersonTable);
|
||||
await db.execute(dropClusterSummaryTable);
|
||||
await db.execute(deletePersonTable);
|
||||
await db.execute(dropNotPersonFeedbackTable);
|
||||
await db.execute(deleteFaceClustersTable);
|
||||
await db.execute(deleteClusterPersonTable);
|
||||
await db.execute(deleteClusterSummaryTable);
|
||||
await db.execute(deleteNotPersonFeedbackTable);
|
||||
}
|
||||
|
||||
Future<Iterable<Uint8List>> getFaceEmbeddingsForCluster(
|
||||
|
@ -734,7 +768,7 @@ class FaceMLDataDB {
|
|||
try {
|
||||
final db = await instance.asyncDB;
|
||||
|
||||
await db.execute(dropFaceClustersTable);
|
||||
await db.execute(deleteFaceClustersTable);
|
||||
await db.execute(createFaceClustersTable);
|
||||
await db.execute(fcClusterIDIndex);
|
||||
} catch (e, s) {
|
||||
|
@ -945,16 +979,15 @@ class FaceMLDataDB {
|
|||
if (faces) {
|
||||
await db.execute(deleteFacesTable);
|
||||
await db.execute(createFacesTable);
|
||||
await db.execute(dropFaceClustersTable);
|
||||
await db.execute(deleteFaceClustersTable);
|
||||
await db.execute(createFaceClustersTable);
|
||||
await db.execute(fcClusterIDIndex);
|
||||
}
|
||||
|
||||
await db.execute(deletePersonTable);
|
||||
await db.execute(dropClusterPersonTable);
|
||||
await db.execute(dropNotPersonFeedbackTable);
|
||||
await db.execute(dropClusterSummaryTable);
|
||||
await db.execute(dropFaceClustersTable);
|
||||
await db.execute(deleteClusterPersonTable);
|
||||
await db.execute(deleteNotPersonFeedbackTable);
|
||||
await db.execute(deleteClusterSummaryTable);
|
||||
await db.execute(deleteFaceClustersTable);
|
||||
|
||||
await db.execute(createClusterPersonTable);
|
||||
await db.execute(createNotPersonFeedbackTable);
|
||||
|
@ -972,9 +1005,8 @@ class FaceMLDataDB {
|
|||
final db = await instance.asyncDB;
|
||||
|
||||
// Drop the tables
|
||||
await db.execute(deletePersonTable);
|
||||
await db.execute(dropClusterPersonTable);
|
||||
await db.execute(dropNotPersonFeedbackTable);
|
||||
await db.execute(deleteClusterPersonTable);
|
||||
await db.execute(deleteNotPersonFeedbackTable);
|
||||
|
||||
// Recreate the tables
|
||||
await db.execute(createClusterPersonTable);
|
||||
|
|
|
@ -29,7 +29,7 @@ const createFacesTable = '''CREATE TABLE IF NOT EXISTS $facesTable (
|
|||
);
|
||||
''';
|
||||
|
||||
const deleteFacesTable = 'DROP TABLE IF EXISTS $facesTable';
|
||||
const deleteFacesTable = 'DELETE FROM $facesTable';
|
||||
// End of Faces Table Fields & Schema Queries
|
||||
|
||||
//##region Face Clusters Table Fields & Schema Queries
|
||||
|
@ -48,15 +48,9 @@ CREATE TABLE IF NOT EXISTS $faceClustersTable (
|
|||
// -- Creating a non-unique index on clusterID for query optimization
|
||||
const fcClusterIDIndex =
|
||||
'''CREATE INDEX IF NOT EXISTS idx_fcClusterID ON $faceClustersTable($fcClusterID);''';
|
||||
const dropFaceClustersTable = 'DROP TABLE IF EXISTS $faceClustersTable';
|
||||
const deleteFaceClustersTable = 'DELETE FROM $faceClustersTable';
|
||||
//##endregion
|
||||
|
||||
// People Table Fields & Schema Queries
|
||||
const personTable = 'person';
|
||||
|
||||
const deletePersonTable = 'DROP TABLE IF EXISTS $personTable';
|
||||
//End People Table Fields & Schema Queries
|
||||
|
||||
// Clusters Table Fields & Schema Queries
|
||||
const clusterPersonTable = 'cluster_person';
|
||||
const personIdColumn = 'person_id';
|
||||
|
@ -69,7 +63,7 @@ CREATE TABLE IF NOT EXISTS $clusterPersonTable (
|
|||
PRIMARY KEY($personIdColumn, $clusterIDColumn)
|
||||
);
|
||||
''';
|
||||
const dropClusterPersonTable = 'DROP TABLE IF EXISTS $clusterPersonTable';
|
||||
const deleteClusterPersonTable = 'DELETE FROM $clusterPersonTable';
|
||||
// End Clusters Table Fields & Schema Queries
|
||||
|
||||
/// Cluster Summary Table Fields & Schema Queries
|
||||
|
@ -85,7 +79,7 @@ CREATE TABLE IF NOT EXISTS $clusterSummaryTable (
|
|||
);
|
||||
''';
|
||||
|
||||
const dropClusterSummaryTable = 'DROP TABLE IF EXISTS $clusterSummaryTable';
|
||||
const deleteClusterSummaryTable = 'DELETE FROM $clusterSummaryTable';
|
||||
|
||||
/// End Cluster Summary Table Fields & Schema Queries
|
||||
|
||||
|
@ -99,5 +93,5 @@ CREATE TABLE IF NOT EXISTS $notPersonFeedback (
|
|||
PRIMARY KEY($personIdColumn, $clusterIDColumn)
|
||||
);
|
||||
''';
|
||||
const dropNotPersonFeedbackTable = 'DROP TABLE IF EXISTS $notPersonFeedback';
|
||||
const deleteNotPersonFeedbackTable = 'DELETE FROM $notPersonFeedback';
|
||||
// End Clusters Table Fields & Schema Queries
|
||||
|
|
2
mobile/lib/generated/intl/messages_en.dart
generated
2
mobile/lib/generated/intl/messages_en.dart
generated
|
@ -814,7 +814,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
MessageLookupByLibrary.simpleMessage("Incorrect recovery key"),
|
||||
"indexedItems": MessageLookupByLibrary.simpleMessage("Indexed items"),
|
||||
"indexingIsPaused": MessageLookupByLibrary.simpleMessage(
|
||||
"Indexing is paused, will automatically resume when device is ready"),
|
||||
"Indexing is paused. It will automatically resume when device is ready."),
|
||||
"insecureDevice":
|
||||
MessageLookupByLibrary.simpleMessage("Insecure device"),
|
||||
"installManually":
|
||||
|
|
4
mobile/lib/generated/l10n.dart
generated
4
mobile/lib/generated/l10n.dart
generated
|
@ -8794,10 +8794,10 @@ class S {
|
|||
);
|
||||
}
|
||||
|
||||
/// `Indexing is paused, will automatically resume when device is ready`
|
||||
/// `Indexing is paused. It will automatically resume when device is ready.`
|
||||
String get indexingIsPaused {
|
||||
return Intl.message(
|
||||
'Indexing is paused, will automatically resume when device is ready',
|
||||
'Indexing is paused. It will automatically resume when device is ready.',
|
||||
name: 'indexingIsPaused',
|
||||
desc: '',
|
||||
args: [],
|
||||
|
|
|
@ -1236,5 +1236,5 @@
|
|||
"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",
|
||||
"indexingIsPaused": "Indexing is paused, will automatically resume when device is ready"
|
||||
}
|
||||
"indexingIsPaused": "Indexing is paused. It will automatically resume when device is ready."
|
||||
}
|
||||
|
|
|
@ -51,6 +51,7 @@ import 'package:photos/services/user_service.dart';
|
|||
import 'package:photos/ui/tools/app_lock.dart';
|
||||
import 'package:photos/ui/tools/lock_screen.dart';
|
||||
import 'package:photos/utils/crypto_util.dart';
|
||||
import "package:photos/utils/email_util.dart";
|
||||
import 'package:photos/utils/file_uploader.dart';
|
||||
import 'package:photos/utils/local_settings.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
@ -180,6 +181,16 @@ void _headlessTaskHandler(HeadlessTask task) {
|
|||
}
|
||||
|
||||
Future<void> _init(bool isBackground, {String via = ''}) async {
|
||||
bool initComplete = false;
|
||||
Future.delayed(const Duration(seconds: 15), () {
|
||||
if (!initComplete && !isBackground) {
|
||||
sendLogsForInit(
|
||||
"support@ente.io",
|
||||
"Stuck on splash screen for >= 15 seconds",
|
||||
null,
|
||||
);
|
||||
}
|
||||
});
|
||||
_isProcessRunning = true;
|
||||
_logger.info("Initializing... inBG =$isBackground via: $via");
|
||||
final SharedPreferences preferences = await SharedPreferences.getInstance();
|
||||
|
@ -235,17 +246,11 @@ Future<void> _init(bool isBackground, {String via = ''}) async {
|
|||
|
||||
unawaited(SemanticSearchService.instance.init());
|
||||
MachineLearningController.instance.init();
|
||||
// Can not including existing tf/ml binaries as they are not being built
|
||||
// from source.
|
||||
// See https://gitlab.com/fdroid/fdroiddata/-/merge_requests/12671#note_1294346819
|
||||
if (!UpdateService.instance.isFdroidFlavor()) {
|
||||
// unawaited(ObjectDetectionService.instance.init());
|
||||
if (flagService.faceSearchEnabled) {
|
||||
unawaited(FaceMlService.instance.init());
|
||||
} else {
|
||||
if (LocalSettings.instance.isFaceIndexingEnabled) {
|
||||
unawaited(LocalSettings.instance.toggleFaceIndexing());
|
||||
}
|
||||
if (flagService.faceSearchEnabled) {
|
||||
unawaited(FaceMlService.instance.init());
|
||||
} else {
|
||||
if (LocalSettings.instance.isFaceIndexingEnabled) {
|
||||
unawaited(LocalSettings.instance.toggleFaceIndexing());
|
||||
}
|
||||
}
|
||||
PersonService.init(
|
||||
|
@ -254,6 +259,7 @@ Future<void> _init(bool isBackground, {String via = ''}) async {
|
|||
preferences,
|
||||
);
|
||||
|
||||
initComplete = true;
|
||||
_logger.info("Initialization done");
|
||||
}
|
||||
|
||||
|
|
|
@ -43,6 +43,7 @@ import 'package:photos/services/machine_learning/face_ml/face_ml_result.dart';
|
|||
import "package:photos/services/machine_learning/face_ml/person/person_service.dart";
|
||||
import 'package:photos/services/machine_learning/file_ml/file_ml.dart';
|
||||
import 'package:photos/services/machine_learning/file_ml/remote_fileml_service.dart';
|
||||
import "package:photos/services/machine_learning/machine_learning_controller.dart";
|
||||
import "package:photos/services/search_service.dart";
|
||||
import "package:photos/utils/file_util.dart";
|
||||
import 'package:photos/utils/image_ml_isolate.dart';
|
||||
|
@ -99,7 +100,7 @@ class FaceMlService {
|
|||
|
||||
final int _fileDownloadLimit = 5;
|
||||
final int _embeddingFetchLimit = 200;
|
||||
final int _kForceClusteringFaceCount = 4000;
|
||||
final int _kForceClusteringFaceCount = 8000;
|
||||
|
||||
Future<void> init({bool initializeImageMlIsolate = false}) async {
|
||||
if (LocalSettings.instance.isFaceIndexingEnabled == false) {
|
||||
|
@ -163,9 +164,16 @@ class FaceMlService {
|
|||
pauseIndexingAndClustering();
|
||||
}
|
||||
});
|
||||
if (Platform.isIOS &&
|
||||
MachineLearningController.instance.isDeviceHealthy) {
|
||||
_logger.info("Starting face indexing and clustering on iOS from init");
|
||||
unawaited(indexAndClusterAll());
|
||||
}
|
||||
|
||||
_listenIndexOnDiffSync();
|
||||
_listenOnPeopleChangedSync();
|
||||
|
||||
_logger.info('init done');
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1016,9 +1024,13 @@ class FaceMlService {
|
|||
File? file;
|
||||
if (enteFile.fileType == FileType.video) {
|
||||
try {
|
||||
file = await getThumbnailForUploadedFile(enteFile);
|
||||
file = await getThumbnailForUploadedFile(enteFile);
|
||||
} on PlatformException catch (e, s) {
|
||||
_logger.severe("Could not get thumbnail for $enteFile due to PlatformException", e, s);
|
||||
_logger.severe(
|
||||
"Could not get thumbnail for $enteFile due to PlatformException",
|
||||
e,
|
||||
s,
|
||||
);
|
||||
throw ThumbnailRetrievalException(e.toString(), s);
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -4,7 +4,6 @@ import "dart:io";
|
|||
import "package:battery_info/battery_info_plugin.dart";
|
||||
import "package:battery_info/model/android_battery_info.dart";
|
||||
import "package:battery_info/model/iso_battery_info.dart";
|
||||
import "package:flutter/foundation.dart" show kDebugMode;
|
||||
import "package:logging/logging.dart";
|
||||
import "package:photos/core/event_bus.dart";
|
||||
import "package:photos/events/machine_learning_control_event.dart";
|
||||
|
@ -19,8 +18,7 @@ class MachineLearningController {
|
|||
|
||||
static const kMaximumTemperature = 42; // 42 degree celsius
|
||||
static const kMinimumBatteryLevel = 20; // 20%
|
||||
static const kDefaultInteractionTimeout =
|
||||
kDebugMode ? Duration(seconds: 3) : Duration(seconds: 5);
|
||||
static const kDefaultInteractionTimeout = Duration(seconds: 10);
|
||||
static const kUnhealthyStates = ["over_heat", "over_voltage", "dead"];
|
||||
|
||||
bool _isDeviceHealthy = true;
|
||||
|
@ -31,6 +29,7 @@ class MachineLearningController {
|
|||
bool get isDeviceHealthy => _isDeviceHealthy;
|
||||
|
||||
void init() {
|
||||
_logger.info('init called');
|
||||
if (Platform.isAndroid) {
|
||||
_startInteractionTimer();
|
||||
BatteryInfoPlugin()
|
||||
|
@ -47,6 +46,7 @@ class MachineLearningController {
|
|||
});
|
||||
}
|
||||
_fireControlEvent();
|
||||
_logger.info('init done');
|
||||
}
|
||||
|
||||
void onUserInteraction() {
|
||||
|
|
|
@ -89,8 +89,8 @@ class _MachineLearningSettingsPageState
|
|||
iconButtonType: IconButtonType.secondary,
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(context);
|
||||
if (Navigator.canPop(context)) Navigator.pop(context);
|
||||
if (Navigator.canPop(context)) Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
|
|
|
@ -5,6 +5,7 @@ import "package:flutter/material.dart";
|
|||
import "package:flutter_animate/flutter_animate.dart";
|
||||
import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
|
||||
import "package:photos/core/configuration.dart";
|
||||
import "package:photos/core/constants.dart";
|
||||
import "package:photos/db/files_db.dart";
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import "package:photos/l10n/l10n.dart";
|
||||
|
@ -167,7 +168,14 @@ class AddPhotosPhotoWidget extends StatelessWidget {
|
|||
|
||||
Future<void> _onPickFromDeviceClicked(BuildContext context) async {
|
||||
try {
|
||||
final List<AssetEntity>? result = await AssetPicker.pickAssets(context);
|
||||
final assetPickerTextDelegate = await _getAssetPickerTextDelegate();
|
||||
final List<AssetEntity>? result = await AssetPicker.pickAssets(
|
||||
context,
|
||||
pickerConfig: AssetPickerConfig(
|
||||
maxAssets: maxPickAssetLimit,
|
||||
textDelegate: assetPickerTextDelegate,
|
||||
),
|
||||
);
|
||||
if (result != null && result.isNotEmpty) {
|
||||
final ca = CollectionActions(
|
||||
CollectionsService.instance,
|
||||
|
@ -204,6 +212,39 @@ class AddPhotosPhotoWidget extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// _getAssetPickerTextDelegate returns the text delegate for the asset picker
|
||||
// This custom method is required to enforce English as the default fallback
|
||||
// instead of Chinese.
|
||||
Future<AssetPickerTextDelegate> _getAssetPickerTextDelegate() async {
|
||||
final Locale locale = await getLocale();
|
||||
switch (locale.languageCode.toLowerCase()) {
|
||||
case "en":
|
||||
return const EnglishAssetPickerTextDelegate();
|
||||
case "he":
|
||||
return const HebrewAssetPickerTextDelegate();
|
||||
case "de":
|
||||
return const GermanAssetPickerTextDelegate();
|
||||
case "ru":
|
||||
return const RussianAssetPickerTextDelegate();
|
||||
case "ja":
|
||||
return const JapaneseAssetPickerTextDelegate();
|
||||
case "ar":
|
||||
return const ArabicAssetPickerTextDelegate();
|
||||
case "fr":
|
||||
return const FrenchAssetPickerTextDelegate();
|
||||
case "vi":
|
||||
return const VietnameseAssetPickerTextDelegate();
|
||||
case "tr":
|
||||
return const TurkishAssetPickerTextDelegate();
|
||||
case "ko":
|
||||
return const KoreanAssetPickerTextDelegate();
|
||||
case "zh":
|
||||
return const AssetPickerTextDelegate();
|
||||
default:
|
||||
return const EnglishAssetPickerTextDelegate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DelayedGallery extends StatefulWidget {
|
||||
|
|
|
@ -97,7 +97,7 @@ class _AppBarWidgetState extends State<ClusterAppBar> {
|
|||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
actions: kDebugMode ? _getDefaultActions(context) : null,
|
||||
actions: _getDefaultActions(context),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -38,12 +38,17 @@ class _PersonClustersPageState extends State<PersonClustersPage> {
|
|||
.getClusterFilesForPersonID(widget.person.remoteID),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
final List<int> keys = snapshot.data!.keys.toList();
|
||||
final clusters = snapshot.data!;
|
||||
final List<int> keys = clusters.keys.toList();
|
||||
// Sort the clusters by the number of files in each cluster, largest first
|
||||
keys.sort(
|
||||
(b, a) => clusters[a]!.length.compareTo(clusters[b]!.length),
|
||||
);
|
||||
return ListView.builder(
|
||||
itemCount: keys.length,
|
||||
itemBuilder: (context, index) {
|
||||
final int clusterID = keys[index];
|
||||
final List<EnteFile> files = snapshot.data![keys[index]]!;
|
||||
final List<EnteFile> files = clusters[clusterID]!;
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
|
@ -93,34 +98,37 @@ class _PersonClustersPageState extends State<PersonClustersPage> {
|
|||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
"${snapshot.data![keys[index]]!.length} photos",
|
||||
"${files.length} photos",
|
||||
style: getEnteTextTheme(context).body,
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
try {
|
||||
await PersonService.instance
|
||||
.removeClusterToPerson(
|
||||
personID: widget.person.remoteID,
|
||||
clusterID: clusterID,
|
||||
);
|
||||
_logger.info(
|
||||
"Removed cluster $clusterID from person ${widget.person.remoteID}",
|
||||
);
|
||||
Bus.instance.fire(PeopleChangedEvent());
|
||||
setState(() {});
|
||||
} catch (e) {
|
||||
_logger.severe(
|
||||
"removing cluster from person,",
|
||||
e,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const Icon(
|
||||
CupertinoIcons.minus_circled,
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
(index != 0)
|
||||
? GestureDetector(
|
||||
onTap: () async {
|
||||
try {
|
||||
await PersonService.instance
|
||||
.removeClusterToPerson(
|
||||
personID: widget.person.remoteID,
|
||||
clusterID: clusterID,
|
||||
);
|
||||
_logger.info(
|
||||
"Removed cluster $clusterID from person ${widget.person.remoteID}",
|
||||
);
|
||||
Bus.instance
|
||||
.fire(PeopleChangedEvent());
|
||||
setState(() {});
|
||||
} catch (e) {
|
||||
_logger.severe(
|
||||
"removing cluster from person,",
|
||||
e,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const Icon(
|
||||
CupertinoIcons.minus_circled,
|
||||
color: Colors.red,
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
@ -16,6 +16,7 @@ import 'package:path_provider/path_provider.dart';
|
|||
import 'package:photos/core/configuration.dart';
|
||||
import 'package:photos/core/error-reporting/super_logging.dart';
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import "package:photos/ui/common/progress_dialog.dart";
|
||||
import 'package:photos/ui/components/buttons/button_widget.dart';
|
||||
import 'package:photos/ui/components/dialog_widget.dart';
|
||||
import 'package:photos/ui/components/models/button_type.dart';
|
||||
|
@ -122,9 +123,28 @@ Future<void> _sendLogs(
|
|||
}
|
||||
}
|
||||
|
||||
Future<String> getZippedLogsFile(BuildContext context) async {
|
||||
final dialog = createProgressDialog(context, S.of(context).preparingLogs);
|
||||
await dialog.show();
|
||||
Future<void> sendLogsForInit(
|
||||
String toEmail,
|
||||
String? subject,
|
||||
String? body,
|
||||
) async {
|
||||
final String zipFilePath = await getZippedLogsFile(null);
|
||||
final Email email = Email(
|
||||
recipients: [toEmail],
|
||||
subject: subject ?? '',
|
||||
body: body ?? '',
|
||||
attachmentPaths: [zipFilePath],
|
||||
isHTML: false,
|
||||
);
|
||||
await FlutterEmailSender.send(email);
|
||||
}
|
||||
|
||||
Future<String> getZippedLogsFile(BuildContext? context) async {
|
||||
late final ProgressDialog dialog;
|
||||
if (context != null) {
|
||||
dialog = createProgressDialog(context, S.of(context).preparingLogs);
|
||||
await dialog.show();
|
||||
}
|
||||
final logsPath = (await getApplicationSupportDirectory()).path;
|
||||
final logsDirectory = Directory(logsPath + "/logs");
|
||||
final tempPath = (await getTemporaryDirectory()).path;
|
||||
|
@ -134,7 +154,9 @@ Future<String> getZippedLogsFile(BuildContext context) async {
|
|||
encoder.create(zipFilePath);
|
||||
await encoder.addDirectory(logsDirectory);
|
||||
encoder.close();
|
||||
await dialog.hide();
|
||||
if (context != null) {
|
||||
await dialog.hide();
|
||||
}
|
||||
return zipFilePath;
|
||||
}
|
||||
|
||||
|
|
|
@ -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.111+635
|
||||
version: 0.8.112+636
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
|
|
|
@ -95,6 +95,15 @@ db:
|
|||
# Map of data centers
|
||||
#
|
||||
# Each data center also specifies which bucket in that provider should be used.
|
||||
#
|
||||
# If you're not using replication (it is off by default), you only need to
|
||||
# provide valid credentials for the first entry (the default hot storage,
|
||||
# "b2-eu-cen").
|
||||
#
|
||||
# Note that you need to use the same key names (e.g. "b2-eu-cen") as below. The
|
||||
# values and the S3 provider itself can any arbitrary S3 storage, it is not tied
|
||||
# to the region (eu-cen) or provider (b2, wasabi), but for historical reasons
|
||||
# the key names have to be one of those in the list below.
|
||||
s3:
|
||||
# Override the primary and secondary hot storage. The commented out values
|
||||
# are the defaults.
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,17 +1,16 @@
|
|||
import { CustomHead } from "@/next/components/Head";
|
||||
import { setupI18n } from "@/next/i18n";
|
||||
import { logUnhandledErrorsAndRejections } from "@/next/log-web";
|
||||
import type { AppName, BaseAppContextT } from "@/next/types/app";
|
||||
import { ensure } from "@/utils/ensure";
|
||||
import { PAGES } from "@ente/accounts/constants/pages";
|
||||
import { accountLogout } from "@ente/accounts/services/logout";
|
||||
import { APPS, APP_TITLES } from "@ente/shared/apps/constants";
|
||||
import { Overlay } from "@ente/shared/components/Container";
|
||||
import DialogBoxV2 from "@ente/shared/components/DialogBoxV2";
|
||||
import {
|
||||
DialogBoxAttributesV2,
|
||||
SetDialogBoxAttributesV2,
|
||||
} from "@ente/shared/components/DialogBoxV2/types";
|
||||
import type { DialogBoxAttributesV2 } from "@ente/shared/components/DialogBoxV2/types";
|
||||
import EnteSpinner from "@ente/shared/components/EnteSpinner";
|
||||
import AppNavbar from "@ente/shared/components/Navbar/app";
|
||||
import { AppNavbar } from "@ente/shared/components/Navbar/app";
|
||||
import { useLocalState } from "@ente/shared/hooks/useLocalState";
|
||||
import HTTPService from "@ente/shared/network/HTTPService";
|
||||
import { LS_KEYS, getData } from "@ente/shared/storage/localStorage";
|
||||
|
@ -22,25 +21,28 @@ import { ThemeProvider } from "@mui/material/styles";
|
|||
import { t } from "i18next";
|
||||
import { AppProps } from "next/app";
|
||||
import { useRouter } from "next/router";
|
||||
import { createContext, useEffect, useState } from "react";
|
||||
import { createContext, useContext, useEffect, useState } from "react";
|
||||
import "styles/global.css";
|
||||
|
||||
interface AppContextProps {
|
||||
isMobile: boolean;
|
||||
showNavBar: (show: boolean) => void;
|
||||
setDialogBoxAttributesV2: SetDialogBoxAttributesV2;
|
||||
logout: () => void;
|
||||
}
|
||||
/** The accounts app has no extra properties on top of the base context. */
|
||||
type AppContextT = BaseAppContextT;
|
||||
|
||||
export const AppContext = createContext<AppContextProps>({} as AppContextProps);
|
||||
/** The React {@link Context} available to all pages. */
|
||||
export const AppContext = createContext<AppContextT | undefined>(undefined);
|
||||
|
||||
/** Utility hook to reduce amount of boilerplate in account related pages. */
|
||||
export const useAppContext = () => ensure(useContext(AppContext));
|
||||
|
||||
export default function App({ Component, pageProps }: AppProps) {
|
||||
const appName: AppName = "account";
|
||||
|
||||
const [isI18nReady, setIsI18nReady] = useState<boolean>(false);
|
||||
|
||||
const [showNavbar, setShowNavBar] = useState(false);
|
||||
|
||||
const [dialogBoxAttributeV2, setDialogBoxAttributesV2] =
|
||||
useState<DialogBoxAttributesV2>();
|
||||
const [dialogBoxAttributeV2, setDialogBoxAttributesV2] = useState<
|
||||
DialogBoxAttributesV2 | undefined
|
||||
>();
|
||||
|
||||
const [dialogBoxV2View, setDialogBoxV2View] = useState(false);
|
||||
|
||||
|
@ -85,8 +87,17 @@ export default function App({ Component, pageProps }: AppProps) {
|
|||
void accountLogout().then(() => router.push(PAGES.ROOT));
|
||||
};
|
||||
|
||||
const appContext = {
|
||||
appName,
|
||||
logout,
|
||||
showNavBar,
|
||||
isMobile,
|
||||
setDialogBoxAttributesV2,
|
||||
};
|
||||
|
||||
// TODO: This string doesn't actually exist
|
||||
const title = isI18nReady
|
||||
? t("TITLE", { context: APPS.ACCOUNTS })
|
||||
? t("title", { context: "accounts" })
|
||||
: APP_TITLES.get(APPS.ACCOUNTS);
|
||||
|
||||
return (
|
||||
|
@ -102,15 +113,7 @@ export default function App({ Component, pageProps }: AppProps) {
|
|||
attributes={dialogBoxAttributeV2 as any}
|
||||
/>
|
||||
|
||||
<AppContext.Provider
|
||||
value={{
|
||||
isMobile,
|
||||
showNavBar,
|
||||
setDialogBoxAttributesV2:
|
||||
setDialogBoxAttributesV2 as any,
|
||||
logout,
|
||||
}}
|
||||
>
|
||||
<AppContext.Provider value={appContext}>
|
||||
{!isI18nReady && (
|
||||
<Overlay
|
||||
sx={(theme) => ({
|
||||
|
|
6
web/apps/accounts/src/pages/credentials.tsx
Normal file
6
web/apps/accounts/src/pages/credentials.tsx
Normal file
|
@ -0,0 +1,6 @@
|
|||
import Page_ from "@ente/accounts/pages/credentials";
|
||||
import { useAppContext } from "./_app";
|
||||
|
||||
const Page = () => <Page_ appContext={useAppContext()} />;
|
||||
|
||||
export default Page;
|
|
@ -1,9 +0,0 @@
|
|||
import CredentialPage from "@ente/accounts/pages/credentials";
|
||||
import { APPS } from "@ente/shared/apps/constants";
|
||||
import { useContext } from "react";
|
||||
import { AppContext } from "../_app";
|
||||
|
||||
export default function Credential() {
|
||||
const appContext = useContext(AppContext);
|
||||
return <CredentialPage appContext={appContext} appName={APPS.ACCOUNTS} />;
|
||||
}
|
6
web/apps/accounts/src/pages/generate.tsx
Normal file
6
web/apps/accounts/src/pages/generate.tsx
Normal file
|
@ -0,0 +1,6 @@
|
|||
import Page_ from "@ente/accounts/pages/generate";
|
||||
import { useAppContext } from "./_app";
|
||||
|
||||
const Page = () => <Page_ appContext={useAppContext()} />;
|
||||
|
||||
export default Page;
|
|
@ -1,9 +0,0 @@
|
|||
import GeneratePage from "@ente/accounts/pages/generate";
|
||||
import { APPS } from "@ente/shared/apps/constants";
|
||||
import { AppContext } from "pages/_app";
|
||||
import { useContext } from "react";
|
||||
|
||||
export default function Generate() {
|
||||
const appContext = useContext(AppContext);
|
||||
return <GeneratePage appContext={appContext} appName={APPS.ACCOUNTS} />;
|
||||
}
|
6
web/apps/accounts/src/pages/login.tsx
Normal file
6
web/apps/accounts/src/pages/login.tsx
Normal file
|
@ -0,0 +1,6 @@
|
|||
import Page_ from "@ente/accounts/pages/login";
|
||||
import { useAppContext } from "./_app";
|
||||
|
||||
const Page = () => <Page_ appContext={useAppContext()} />;
|
||||
|
||||
export default Page;
|
|
@ -1,9 +0,0 @@
|
|||
import LoginPage from "@ente/accounts/pages/login";
|
||||
import { APPS } from "@ente/shared/apps/constants";
|
||||
import { useContext } from "react";
|
||||
import { AppContext } from "../_app";
|
||||
|
||||
export default function Login() {
|
||||
const appContext = useContext(AppContext);
|
||||
return <LoginPage appContext={appContext} appName={APPS.ACCOUNTS} />;
|
||||
}
|
|
@ -1,16 +1,12 @@
|
|||
import { TwoFactorType } from "@ente/accounts/constants/twofactor";
|
||||
import RecoverPage from "@ente/accounts/pages/recover";
|
||||
import { APPS } from "@ente/shared/apps/constants";
|
||||
import { AppContext } from "pages/_app";
|
||||
import { useContext } from "react";
|
||||
import RecoverPage from "@ente/accounts/pages/two-factor/recover";
|
||||
import { useAppContext } from "../../_app";
|
||||
|
||||
export default function Recover() {
|
||||
const appContext = useContext(AppContext);
|
||||
return (
|
||||
<RecoverPage
|
||||
appContext={appContext}
|
||||
appName={APPS.PHOTOS}
|
||||
twoFactorType={TwoFactorType.PASSKEY}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const Page = () => (
|
||||
<RecoverPage
|
||||
appContext={useAppContext()}
|
||||
twoFactorType={TwoFactorType.PASSKEY}
|
||||
/>
|
||||
);
|
||||
|
||||
export default Page;
|
||||
|
|
|
@ -9,14 +9,8 @@ import { t } from "i18next";
|
|||
import _sodium from "libsodium-wrappers";
|
||||
import { useRouter } from "next/router";
|
||||
import { AppContext } from "pages/_app";
|
||||
import {
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
import { createContext, useContext, useEffect, useState } from "react";
|
||||
import { Passkey } from "types/passkey";
|
||||
import {
|
||||
finishPasskeyRegistration,
|
||||
|
|
6
web/apps/accounts/src/pages/recover.tsx
Normal file
6
web/apps/accounts/src/pages/recover.tsx
Normal file
|
@ -0,0 +1,6 @@
|
|||
import Page_ from "@ente/accounts/pages/recover";
|
||||
import { useAppContext } from "./_app";
|
||||
|
||||
const Page = () => <Page_ appContext={useAppContext()} />;
|
||||
|
||||
export default Page;
|
|
@ -1,9 +0,0 @@
|
|||
import RecoverPage from "@ente/accounts/pages/recover";
|
||||
import { APPS } from "@ente/shared/apps/constants";
|
||||
import { AppContext } from "pages/_app";
|
||||
import { useContext } from "react";
|
||||
|
||||
export default function Recover() {
|
||||
const appContext = useContext(AppContext);
|
||||
return <RecoverPage appContext={appContext} appName={APPS.ACCOUNTS} />;
|
||||
}
|
6
web/apps/accounts/src/pages/signup.tsx
Normal file
6
web/apps/accounts/src/pages/signup.tsx
Normal file
|
@ -0,0 +1,6 @@
|
|||
import Page_ from "@ente/accounts/pages/signup";
|
||||
import { useAppContext } from "./_app";
|
||||
|
||||
const Page = () => <Page_ appContext={useAppContext()} />;
|
||||
|
||||
export default Page;
|
|
@ -1,9 +0,0 @@
|
|||
import SignupPage from "@ente/accounts/pages/signup";
|
||||
import { APPS } from "@ente/shared/apps/constants";
|
||||
import { AppContext } from "pages/_app";
|
||||
import { useContext } from "react";
|
||||
|
||||
export default function Sigup() {
|
||||
const appContext = useContext(AppContext);
|
||||
return <SignupPage appContext={appContext} appName={APPS.ACCOUNTS} />;
|
||||
}
|
6
web/apps/accounts/src/pages/two-factor/recover.tsx
Normal file
6
web/apps/accounts/src/pages/two-factor/recover.tsx
Normal file
|
@ -0,0 +1,6 @@
|
|||
import Page_ from "@ente/accounts/pages/two-factor/recover";
|
||||
import { useAppContext } from "../_app";
|
||||
|
||||
const Page = () => <Page_ appContext={useAppContext()} />;
|
||||
|
||||
export default Page;
|
|
@ -1,11 +0,0 @@
|
|||
import TwoFactorRecoverPage from "@ente/accounts/pages/two-factor/recover";
|
||||
import { APPS } from "@ente/shared/apps/constants";
|
||||
import { AppContext } from "pages/_app";
|
||||
import { useContext } from "react";
|
||||
|
||||
export default function TwoFactorRecover() {
|
||||
const appContext = useContext(AppContext);
|
||||
return (
|
||||
<TwoFactorRecoverPage appContext={appContext} appName={APPS.ACCOUNTS} />
|
||||
);
|
||||
}
|
6
web/apps/accounts/src/pages/two-factor/setup.tsx
Normal file
6
web/apps/accounts/src/pages/two-factor/setup.tsx
Normal file
|
@ -0,0 +1,6 @@
|
|||
import Page_ from "@ente/accounts/pages/two-factor/setup";
|
||||
import { useAppContext } from "../_app";
|
||||
|
||||
const Page = () => <Page_ appContext={useAppContext()} />;
|
||||
|
||||
export default Page;
|
|
@ -1,11 +0,0 @@
|
|||
import TwoFactorSetupPage from "@ente/accounts/pages/two-factor/setup";
|
||||
import { APPS } from "@ente/shared/apps/constants";
|
||||
import { AppContext } from "pages/_app";
|
||||
import { useContext } from "react";
|
||||
|
||||
export default function TwoFactorSetup() {
|
||||
const appContext = useContext(AppContext);
|
||||
return (
|
||||
<TwoFactorSetupPage appContext={appContext} appName={APPS.ACCOUNTS} />
|
||||
);
|
||||
}
|
6
web/apps/accounts/src/pages/two-factor/verify.tsx
Normal file
6
web/apps/accounts/src/pages/two-factor/verify.tsx
Normal file
|
@ -0,0 +1,6 @@
|
|||
import Page_ from "@ente/accounts/pages/two-factor/verify";
|
||||
import { useAppContext } from "../_app";
|
||||
|
||||
const Page = () => <Page_ appContext={useAppContext()} />;
|
||||
|
||||
export default Page;
|
|
@ -1,11 +0,0 @@
|
|||
import TwoFactorVerifyPage from "@ente/accounts/pages/two-factor/verify";
|
||||
import { APPS } from "@ente/shared/apps/constants";
|
||||
import { AppContext } from "pages/_app";
|
||||
import { useContext } from "react";
|
||||
|
||||
export default function TwoFactorVerify() {
|
||||
const appContext = useContext(AppContext);
|
||||
return (
|
||||
<TwoFactorVerifyPage appContext={appContext} appName={APPS.ACCOUNTS} />
|
||||
);
|
||||
}
|
6
web/apps/accounts/src/pages/verify.tsx
Normal file
6
web/apps/accounts/src/pages/verify.tsx
Normal file
|
@ -0,0 +1,6 @@
|
|||
import Page_ from "@ente/accounts/pages/verify";
|
||||
import { useAppContext } from "./_app";
|
||||
|
||||
const Page = () => <Page_ appContext={useAppContext()} />;
|
||||
|
||||
export default Page;
|
|
@ -1,9 +0,0 @@
|
|||
import VerifyPage from "@ente/accounts/pages/verify";
|
||||
import { APPS } from "@ente/shared/apps/constants";
|
||||
import { AppContext } from "pages/_app";
|
||||
import { useContext } from "react";
|
||||
|
||||
export default function Verify() {
|
||||
const appContext = useContext(AppContext);
|
||||
return <VerifyPage appContext={appContext} appName={APPS.ACCOUNTS} />;
|
||||
}
|
|
@ -1 +1,4 @@
|
|||
NEXT_TELEMETRY_DISABLED = 1
|
||||
|
||||
# For details on how to populate a .env.local to run auth and get it to connect
|
||||
# to an arbitrary Ente instance, see `apps/photos/.env`.
|
||||
|
|
|
@ -9,5 +9,5 @@ module.exports = {
|
|||
tsconfigRootDir: __dirname,
|
||||
project: "./tsconfig.json",
|
||||
},
|
||||
ignorePatterns: [".eslintrc.js", "out"],
|
||||
ignorePatterns: [".eslintrc.js", "next.config.js", "out"],
|
||||
};
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@/build-config": "*",
|
||||
"@/next": "*",
|
||||
"@ente/accounts": "*",
|
||||
"@ente/eslint-config": "*",
|
||||
|
|
|
@ -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<AppContextType>(null);
|
||||
/** The React {@link Context} available to all pages. */
|
||||
export const AppContext = createContext<AppContextT | undefined>(undefined);
|
||||
|
||||
/** Utility hook to reduce amount of boilerplate in account related pages. */
|
||||
export const useAppContext = () => ensure(useContext(AppContext));
|
||||
|
||||
export default function App({ Component, pageProps }: AppProps) {
|
||||
const appName: AppName = "auth";
|
||||
|
||||
const router = useRouter();
|
||||
const [isI18nReady, setIsI18nReady] = useState<boolean>(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<DialogBoxAttributesV2>();
|
||||
const isLoadingBarRunning = useRef<boolean>(false);
|
||||
const loadingBar = useRef<LoadingBarRef>(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}
|
||||
/>
|
||||
|
||||
<AppContext.Provider
|
||||
value={{
|
||||
showNavBar,
|
||||
startLoading,
|
||||
finishLoading,
|
||||
isMobile,
|
||||
themeColor,
|
||||
setThemeColor,
|
||||
somethingWentWrong,
|
||||
setDialogBoxAttributesV2,
|
||||
logout,
|
||||
}}
|
||||
>
|
||||
<AppContext.Provider value={appContext}>
|
||||
{(loading || !isI18nReady) && (
|
||||
<Overlay
|
||||
sx={(theme) => ({
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { ensure } from "@/utils/ensure";
|
||||
import {
|
||||
HorizontalFlex,
|
||||
VerticallyCentered,
|
||||
|
@ -12,7 +13,7 @@ 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 } from "@mui/material";
|
||||
import { Button, ButtonBase, Snackbar, TextField, styled } from "@mui/material";
|
||||
import { t } from "i18next";
|
||||
import { useRouter } from "next/router";
|
||||
import { AppContext } from "pages/_app";
|
||||
|
@ -20,20 +21,22 @@ import React, { useContext, useEffect, useState } from "react";
|
|||
import { generateOTPs, type Code } from "services/code";
|
||||
import { getAuthCodes } from "services/remote";
|
||||
|
||||
const AuthenticatorCodesPage = () => {
|
||||
const appContext = useContext(AppContext);
|
||||
const Page: React.FC = () => {
|
||||
const appContext = ensure(useContext(AppContext));
|
||||
const router = useRouter();
|
||||
const [codes, setCodes] = useState([]);
|
||||
const [codes, setCodes] = useState<Code[]>([]);
|
||||
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) {
|
||||
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 {
|
||||
|
@ -55,11 +58,9 @@ const AuthenticatorCodesPage = () => {
|
|||
|
||||
if (!hasFetched) {
|
||||
return (
|
||||
<>
|
||||
<VerticallyCentered>
|
||||
<EnteSpinner></EnteSpinner>
|
||||
</VerticallyCentered>
|
||||
</>
|
||||
<VerticallyCentered>
|
||||
<EnteSpinner />
|
||||
</VerticallyCentered>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -77,7 +78,7 @@ const AuthenticatorCodesPage = () => {
|
|||
}}
|
||||
>
|
||||
<div style={{ marginBottom: "1rem" }} />
|
||||
{filteredCodes.length === 0 && searchTerm.length === 0 ? (
|
||||
{filteredCodes.length == 0 && searchTerm.length == 0 ? (
|
||||
<></>
|
||||
) : (
|
||||
<TextField
|
||||
|
@ -101,7 +102,7 @@ const AuthenticatorCodesPage = () => {
|
|||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
{filteredCodes.length === 0 ? (
|
||||
{filteredCodes.length == 0 ? (
|
||||
<div
|
||||
style={{
|
||||
alignItems: "center",
|
||||
|
@ -110,10 +111,10 @@ const AuthenticatorCodesPage = () => {
|
|||
marginTop: "32px",
|
||||
}}
|
||||
>
|
||||
{searchTerm.length !== 0 ? (
|
||||
{searchTerm.length > 0 ? (
|
||||
<p>{t("NO_RESULTS")}</p>
|
||||
) : (
|
||||
<div />
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
|
@ -122,18 +123,16 @@ const AuthenticatorCodesPage = () => {
|
|||
))
|
||||
)}
|
||||
</div>
|
||||
<div style={{ marginBottom: "2rem" }} />
|
||||
<Footer />
|
||||
<div style={{ marginBottom: "4rem" }} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuthenticatorCodesPage;
|
||||
export default Page;
|
||||
|
||||
const AuthNavbar: React.FC = () => {
|
||||
const { isMobile, logout } = useContext(AppContext);
|
||||
const { isMobile, logout } = ensure(useContext(AppContext));
|
||||
|
||||
return (
|
||||
<NavbarBase isMobile={isMobile}>
|
||||
|
@ -158,11 +157,11 @@ const AuthNavbar: React.FC = () => {
|
|||
);
|
||||
};
|
||||
|
||||
interface CodeDisplay {
|
||||
interface CodeDisplayProps {
|
||||
code: Code;
|
||||
}
|
||||
|
||||
const CodeDisplay: React.FC<CodeDisplay> = ({ code }) => {
|
||||
const CodeDisplay: React.FC<CodeDisplayProps> = ({ code }) => {
|
||||
const [otp, setOTP] = useState("");
|
||||
const [nextOTP, setNextOTP] = useState("");
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
|
@ -393,14 +392,7 @@ const UnparseableCode: React.FC<UnparseableCodeProps> = ({
|
|||
|
||||
const Footer: React.FC = () => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Footer_>
|
||||
<p>{t("AUTH_DOWNLOAD_MOBILE_APP")}</p>
|
||||
<a
|
||||
href="https://github.com/ente-io/ente/tree/main/auth#-download"
|
||||
|
@ -408,6 +400,15 @@ const Footer: React.FC = () => {
|
|||
>
|
||||
<Button color="accent">{t("DOWNLOAD")}</Button>
|
||||
</a>
|
||||
</div>
|
||||
</Footer_>
|
||||
);
|
||||
};
|
||||
|
||||
const Footer_ = styled("div")`
|
||||
margin-block-start: 2rem;
|
||||
margin-block-end: 4rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import ChangeEmailPage from "@ente/accounts/pages/change-email";
|
||||
import { APPS } from "@ente/shared/apps/constants";
|
||||
import { AppContext } from "pages/_app";
|
||||
import { useContext } from "react";
|
||||
import Page_ from "@ente/accounts/pages/change-email";
|
||||
import { useAppContext } from "./_app";
|
||||
|
||||
export default function ChangeEmail() {
|
||||
const appContext = useContext(AppContext);
|
||||
return <ChangeEmailPage appContext={appContext} appName={APPS.AUTH} />;
|
||||
}
|
||||
const Page = () => <Page_ appContext={useAppContext()} />;
|
||||
|
||||
export default Page;
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import ChangePasswordPage from "@ente/accounts/pages/change-password";
|
||||
import { APPS } from "@ente/shared/apps/constants";
|
||||
import { AppContext } from "pages/_app";
|
||||
import { useContext } from "react";
|
||||
import Page_ from "@ente/accounts/pages/change-password";
|
||||
import { useAppContext } from "./_app";
|
||||
|
||||
export default function ChangePassword() {
|
||||
const appContext = useContext(AppContext);
|
||||
return <ChangePasswordPage appContext={appContext} appName={APPS.AUTH} />;
|
||||
}
|
||||
const Page = () => <Page_ appContext={useAppContext()} />;
|
||||
|
||||
export default Page;
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import CredentialPage from "@ente/accounts/pages/credentials";
|
||||
import { APPS } from "@ente/shared/apps/constants";
|
||||
import { AppContext } from "pages/_app";
|
||||
import { useContext } from "react";
|
||||
import Page_ from "@ente/accounts/pages/credentials";
|
||||
import { useAppContext } from "./_app";
|
||||
|
||||
export default function Credential() {
|
||||
const appContext = useContext(AppContext);
|
||||
return <CredentialPage appContext={appContext} appName={APPS.AUTH} />;
|
||||
}
|
||||
const Page = () => <Page_ appContext={useAppContext()} />;
|
||||
|
||||
export default Page;
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import GeneratePage from "@ente/accounts/pages/generate";
|
||||
import { APPS } from "@ente/shared/apps/constants";
|
||||
import { AppContext } from "pages/_app";
|
||||
import { useContext } from "react";
|
||||
import Page_ from "@ente/accounts/pages/generate";
|
||||
import { useAppContext } from "./_app";
|
||||
|
||||
export default function Generate() {
|
||||
const appContext = useContext(AppContext);
|
||||
return <GeneratePage appContext={appContext} appName={APPS.AUTH} />;
|
||||
}
|
||||
const Page = () => <Page_ appContext={useAppContext()} />;
|
||||
|
||||
export default Page;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import LoginPage from "@ente/accounts/pages/login";
|
||||
import { APPS } from "@ente/shared/apps/constants";
|
||||
import { AppContext } from "pages/_app";
|
||||
import { useContext } from "react";
|
||||
import Page_ from "@ente/accounts/pages/login";
|
||||
import { useAppContext } from "./_app";
|
||||
|
||||
export default function Login() {
|
||||
const appContext = useContext(AppContext);
|
||||
return <LoginPage appContext={appContext} appName={APPS.AUTH} />;
|
||||
}
|
||||
const Page = () => <Page_ appContext={useAppContext()} />;
|
||||
|
||||
export default Page;
|
||||
|
|
|
@ -1,11 +1,3 @@
|
|||
import PasskeysFinishPage from "@ente/accounts/pages/passkeys/finish";
|
||||
import Page from "@ente/accounts/pages/passkeys/finish";
|
||||
|
||||
const PasskeysFinish = () => {
|
||||
return (
|
||||
<>
|
||||
<PasskeysFinishPage />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PasskeysFinish;
|
||||
export default Page;
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import RecoverPage from "@ente/accounts/pages/recover";
|
||||
import { APPS } from "@ente/shared/apps/constants";
|
||||
import { AppContext } from "pages/_app";
|
||||
import { useContext } from "react";
|
||||
import Page_ from "@ente/accounts/pages/recover";
|
||||
import { useAppContext } from "./_app";
|
||||
|
||||
export default function Recover() {
|
||||
const appContext = useContext(AppContext);
|
||||
return <RecoverPage appContext={appContext} appName={APPS.AUTH} />;
|
||||
}
|
||||
const Page = () => <Page_ appContext={useAppContext()} />;
|
||||
|
||||
export default Page;
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import SignupPage from "@ente/accounts/pages/signup";
|
||||
import { APPS } from "@ente/shared/apps/constants";
|
||||
import { AppContext } from "pages/_app";
|
||||
import { useContext } from "react";
|
||||
import Page_ from "@ente/accounts/pages/signup";
|
||||
import { useAppContext } from "./_app";
|
||||
|
||||
export default function Sigup() {
|
||||
const appContext = useContext(AppContext);
|
||||
return <SignupPage appContext={appContext} appName={APPS.AUTH} />;
|
||||
}
|
||||
const Page = () => <Page_ appContext={useAppContext()} />;
|
||||
|
||||
export default Page;
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
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";
|
||||
import Page_ from "@ente/accounts/pages/two-factor/recover";
|
||||
import { useAppContext } from "../_app";
|
||||
|
||||
export default function TwoFactorRecover() {
|
||||
const appContext = useContext(AppContext);
|
||||
return <TwoFactorRecoverPage appContext={appContext} appName={APPS.AUTH} />;
|
||||
}
|
||||
const Page = () => <Page_ appContext={useAppContext()} />;
|
||||
|
||||
export default Page;
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
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";
|
||||
import Page_ from "@ente/accounts/pages/two-factor/setup";
|
||||
import { useAppContext } from "../_app";
|
||||
|
||||
export default function TwoFactorSetup() {
|
||||
const appContext = useContext(AppContext);
|
||||
return <TwoFactorSetupPage appContext={appContext} appName={APPS.AUTH} />;
|
||||
}
|
||||
const Page = () => <Page_ appContext={useAppContext()} />;
|
||||
|
||||
export default Page;
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import TwoFactorVerifyPage from "@ente/accounts/pages/two-factor/verify";
|
||||
import { APPS } from "@ente/shared/apps/constants";
|
||||
import { useContext } from "react";
|
||||
import { AppContext } from "../_app";
|
||||
import Page_ from "@ente/accounts/pages/two-factor/verify";
|
||||
import { useAppContext } from "../_app";
|
||||
|
||||
export default function TwoFactorVerify() {
|
||||
const appContext = useContext(AppContext);
|
||||
return <TwoFactorVerifyPage appContext={appContext} appName={APPS.AUTH} />;
|
||||
}
|
||||
const Page = () => <Page_ appContext={useAppContext()} />;
|
||||
|
||||
export default Page;
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import VerifyPage from "@ente/accounts/pages/verify";
|
||||
import { APPS } from "@ente/shared/apps/constants";
|
||||
import { AppContext } from "pages/_app";
|
||||
import { useContext } from "react";
|
||||
import Page_ from "@ente/accounts/pages/verify";
|
||||
import { useAppContext } from "./_app";
|
||||
|
||||
export default function Verify() {
|
||||
const appContext = useContext(AppContext);
|
||||
return <VerifyPage appContext={appContext} appName={APPS.AUTH} />;
|
||||
}
|
||||
const Page = () => <Page_ appContext={useAppContext()} />;
|
||||
|
||||
export default Page;
|
||||
|
|
|
@ -9,7 +9,7 @@ import { Steam } from "./steam";
|
|||
*/
|
||||
export interface Code {
|
||||
/** A unique id for the corresponding "auth entity" in our system. */
|
||||
id?: String;
|
||||
id: string;
|
||||
/** The type of the code. */
|
||||
type: "totp" | "hotp" | "steam";
|
||||
/** The user's account or email for which this code is used. */
|
||||
|
@ -84,20 +84,6 @@ export const codeFromURIString = (id: string, uriString: string): Code => {
|
|||
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 {
|
||||
|
@ -115,10 +101,46 @@ const _codeFromURIString = (id: string, uriString: string): Code => {
|
|||
};
|
||||
|
||||
const parsePathname = (url: URL): [type: Code["type"], path: string] => {
|
||||
// A URL like
|
||||
//
|
||||
// new
|
||||
// URL("otpauth://hotp/Test?secret=AAABBBCCCDDDEEEFFF&issuer=Test&counter=0")
|
||||
//
|
||||
// is parsed differently by different browsers, and there are differences
|
||||
// even depending on the scheme.
|
||||
//
|
||||
// When the scheme is http(s), then all of them consider "hotp" as the
|
||||
// `host`. However, when the scheme is "otpauth", as is our case, Safari
|
||||
// splits it into
|
||||
//
|
||||
// host: "hotp"
|
||||
// pathname: "/Test"
|
||||
//
|
||||
// while Chrome and Firefox consider the entire thing as part of the
|
||||
// pathname
|
||||
//
|
||||
// host: ""
|
||||
// pathname: "//hotp/Test"
|
||||
//
|
||||
// So we try to handle both scenarios by first checking for the host match,
|
||||
// and if not fall back to deducing the "host" from the pathname.
|
||||
|
||||
switch (url.host.toLowerCase()) {
|
||||
case "totp":
|
||||
return ["totp", url.pathname.toLowerCase()];
|
||||
case "hotp":
|
||||
return ["hotp", url.pathname.toLowerCase()];
|
||||
case "steam":
|
||||
return ["steam", url.pathname.toLowerCase()];
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
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}"`);
|
||||
};
|
||||
|
||||
|
@ -146,8 +168,8 @@ const parseIssuer = (url: URL, path: string): string => {
|
|||
let p = decodeURIComponent(path);
|
||||
if (p.startsWith("/")) p = p.slice(1);
|
||||
|
||||
if (p.includes(":")) p = p.split(":")[0];
|
||||
else if (p.includes("-")) p = p.split("-")[0];
|
||||
if (p.includes(":")) p = ensure(p.split(":")[0]);
|
||||
else if (p.includes("-")) p = ensure(p.split("-")[0]);
|
||||
|
||||
return p;
|
||||
};
|
||||
|
|
|
@ -26,6 +26,9 @@ export const getAuthCodes = async (): Promise<Code[]> => {
|
|||
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(
|
||||
|
@ -36,14 +39,12 @@ export const getAuthCodes = async (): Promise<Code[]> => {
|
|||
return codeFromURIString(entity.id, decryptedCode);
|
||||
} catch (e) {
|
||||
log.error(`Failed to parse codeID ${entity.id}`, e);
|
||||
return null;
|
||||
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,7 +59,7 @@ export const getAuthCodes = async (): Promise<Code[]> => {
|
|||
});
|
||||
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;
|
||||
|
@ -92,7 +93,7 @@ export const getAuthKey = async (): Promise<AuthKey> => {
|
|||
} catch (e) {
|
||||
if (
|
||||
e instanceof ApiError &&
|
||||
e.httpStatusCode === HttpStatusCode.NotFound
|
||||
e.httpStatusCode == HttpStatusCode.NotFound
|
||||
) {
|
||||
throw Error(CustomError.AUTH_KEY_NOT_FOUND);
|
||||
} else {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -3,7 +3,7 @@ import {
|
|||
FormControlLabel,
|
||||
FormGroup,
|
||||
Typography,
|
||||
TypographyProps,
|
||||
type TypographyProps,
|
||||
} from "@mui/material";
|
||||
|
||||
interface Iprops {
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -6,7 +6,7 @@ import {
|
|||
SelectChangeEvent,
|
||||
Stack,
|
||||
Typography,
|
||||
TypographyProps,
|
||||
type TypographyProps,
|
||||
} from "@mui/material";
|
||||
|
||||
export interface DropdownOption<T> {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import CircularProgress, {
|
||||
CircularProgressProps,
|
||||
type CircularProgressProps,
|
||||
} from "@mui/material/CircularProgress";
|
||||
|
||||
export default function EnteSpinner(props: CircularProgressProps) {
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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) => (
|
||||
<Button color="secondary" {...props} />
|
||||
|
|
|
@ -3,7 +3,7 @@ import { EnteMenuItem } from "components/Menu/EnteMenuItem";
|
|||
import { MenuItemGroup } from "components/Menu/MenuItemGroup";
|
||||
import MenuSectionTitle from "components/Menu/MenuSectionTitle";
|
||||
import { t } from "i18next";
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
|
||||
interface IProps {
|
||||
brightness: number;
|
||||
|
|
|
@ -31,16 +31,8 @@ import MenuSectionTitle from "components/Menu/MenuSectionTitle";
|
|||
import { CORNER_THRESHOLD, FILTER_DEFAULT_VALUES } from "constants/photoEditor";
|
||||
import { t } from "i18next";
|
||||
import { AppContext } from "pages/_app";
|
||||
import {
|
||||
Dispatch,
|
||||
MutableRefObject,
|
||||
SetStateAction,
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import type { Dispatch, MutableRefObject, SetStateAction } from "react";
|
||||
import { createContext, useContext, useEffect, useRef, useState } from "react";
|
||||
import { getLocalCollections } from "services/collectionService";
|
||||
import downloadManager from "services/download";
|
||||
import uploadManager from "services/upload/uploadManager";
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { Overlay } from "@ente/shared/components/Container";
|
||||
import {
|
||||
CircularProgress,
|
||||
CircularProgressProps,
|
||||
Typography,
|
||||
type CircularProgressProps,
|
||||
} from "@mui/material";
|
||||
|
||||
function CircularProgressWithLabel(
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
ACCOUNTS_PAGES,
|
||||
PHOTOS_PAGES as PAGES,
|
||||
} from "@ente/shared/constants/pages";
|
||||
import ComlinkCryptoWorker from "@ente/shared/crypto";
|
||||
import { getRecoveryKey } from "@ente/shared/crypto/helpers";
|
||||
import {
|
||||
encryptToB64,
|
||||
|
@ -157,9 +158,9 @@ const UserDetailsSection: React.FC<UserDetailsSectionProps> = ({
|
|||
}) => {
|
||||
const galleryContext = useContext(GalleryContext);
|
||||
|
||||
const [userDetails, setUserDetails] = useLocalState<UserDetails>(
|
||||
LS_KEYS.USER_DETAILS,
|
||||
);
|
||||
const [userDetails, setUserDetails] = useLocalState<
|
||||
UserDetails | undefined
|
||||
>(LS_KEYS.USER_DETAILS, undefined);
|
||||
const [memberSubscriptionManageView, setMemberSubscriptionManageView] =
|
||||
useState(false);
|
||||
|
||||
|
@ -198,6 +199,7 @@ const UserDetailsSection: React.FC<UserDetailsSectionProps> = ({
|
|||
openMemberSubscriptionManage();
|
||||
} else {
|
||||
if (
|
||||
userDetails &&
|
||||
hasStripeSubscription(userDetails.subscription) &&
|
||||
isSubscriptionPastDue(userDetails.subscription)
|
||||
) {
|
||||
|
@ -493,9 +495,10 @@ const UtilitySection: React.FC<UtilitySectionProps> = ({ closeSidebar }) => {
|
|||
|
||||
const resetSecret = await generateEncryptionKey();
|
||||
|
||||
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
||||
const encryptionResult = await encryptToB64(
|
||||
resetSecret,
|
||||
recoveryKey,
|
||||
await cryptoWorker.fromHex(recoveryKey),
|
||||
);
|
||||
|
||||
await configurePasskeyRecovery(
|
||||
|
@ -529,7 +532,7 @@ const UtilitySection: React.FC<UtilitySectionProps> = ({ closeSidebar }) => {
|
|||
});
|
||||
|
||||
const toggleTheme = () => {
|
||||
setThemeColor((themeColor) =>
|
||||
setThemeColor(
|
||||
themeColor === THEME_COLOR.DARK
|
||||
? THEME_COLOR.LIGHT
|
||||
: THEME_COLOR.DARK,
|
||||
|
@ -601,7 +604,7 @@ const UtilitySection: React.FC<UtilitySectionProps> = ({ closeSidebar }) => {
|
|||
label={t("PREFERENCES")}
|
||||
/>
|
||||
<RecoveryKey
|
||||
appContext={appContext}
|
||||
isMobile={appContext.isMobile}
|
||||
show={recoverModalView}
|
||||
onHide={closeRecoveryKeyModal}
|
||||
somethingWentWrong={somethingWentWrong}
|
||||
|
|
|
@ -22,7 +22,7 @@ export const CollectionMappingChoiceModal: React.FC<
|
|||
|
||||
return (
|
||||
<Dialog open={open} onClose={handleClose}>
|
||||
<DialogTitleWithCloseButton onClose={handleClose}>
|
||||
<DialogTitleWithCloseButton onClose={onClose}>
|
||||
{t("MULTI_FOLDER_UPLOAD")}
|
||||
</DialogTitleWithCloseButton>
|
||||
<DialogContent>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Typography, TypographyProps, styled } from "@mui/material";
|
||||
import { Typography, styled, type TypographyProps } from "@mui/material";
|
||||
import MuiAccordion, { AccordionProps } from "@mui/material/Accordion";
|
||||
import MuiAccordionDetails from "@mui/material/AccordionDetails";
|
||||
import MuiAccordionSummary from "@mui/material/AccordionSummary";
|
||||
|
|
|
@ -119,12 +119,11 @@ export const WatchFolder: React.FC<WatchFolderProps> = ({ open, onClose }) => {
|
|||
onClose={onClose}
|
||||
PaperProps={{ sx: { height: "448px", maxWidth: "414px" } }}
|
||||
>
|
||||
<DialogTitleWithCloseButton
|
||||
onClose={onClose}
|
||||
sx={{ "&&&": { padding: "32px 16px 16px 24px" } }}
|
||||
>
|
||||
{t("WATCHED_FOLDERS")}
|
||||
</DialogTitleWithCloseButton>
|
||||
<Title_>
|
||||
<DialogTitleWithCloseButton onClose={onClose}>
|
||||
{t("WATCHED_FOLDERS")}
|
||||
</DialogTitleWithCloseButton>
|
||||
</Title_>
|
||||
<DialogContent sx={{ flex: 1 }}>
|
||||
<Stack spacing={1} p={1.5} height={"100%"}>
|
||||
<WatchList {...{ watches, removeWatch }} />
|
||||
|
@ -149,13 +148,17 @@ export const WatchFolder: React.FC<WatchFolderProps> = ({ open, onClose }) => {
|
|||
);
|
||||
};
|
||||
|
||||
const Title_ = styled("div")`
|
||||
padding: 32px 16px 16px 24px;
|
||||
`;
|
||||
|
||||
interface WatchList {
|
||||
watches: FolderWatch[];
|
||||
watches: FolderWatch[] | undefined;
|
||||
removeWatch: (watch: FolderWatch) => void;
|
||||
}
|
||||
|
||||
const WatchList: React.FC<WatchList> = ({ watches, removeWatch }) => {
|
||||
return watches.length === 0 ? (
|
||||
return (watches ?? []).length === 0 ? (
|
||||
<NoWatches />
|
||||
) : (
|
||||
<WatchesContainer>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { ButtonProps, Link, LinkProps } from "@mui/material";
|
||||
import React, { FC } from "react";
|
||||
import { Link, type ButtonProps, type LinkProps } from "@mui/material";
|
||||
import React from "react";
|
||||
|
||||
export type LinkButtonProps = React.PropsWithChildren<{
|
||||
onClick: () => void;
|
||||
|
@ -7,12 +7,9 @@ export type LinkButtonProps = React.PropsWithChildren<{
|
|||
style?: React.CSSProperties;
|
||||
}>;
|
||||
|
||||
const LinkButton: FC<LinkProps<"button", { color?: ButtonProps["color"] }>> = ({
|
||||
children,
|
||||
sx,
|
||||
color,
|
||||
...props
|
||||
}) => {
|
||||
const LinkButton: React.FC<
|
||||
LinkProps<"button", { color?: ButtonProps["color"] }>
|
||||
> = ({ children, sx, color, ...props }) => {
|
||||
return (
|
||||
<Link
|
||||
component="button"
|
||||
|
|
|
@ -5,7 +5,9 @@ import {
|
|||
logStartupBanner,
|
||||
logUnhandledErrorsAndRejections,
|
||||
} from "@/next/log-web";
|
||||
import type { AppName, BaseAppContextT } from "@/next/types/app";
|
||||
import { AppUpdate } from "@/next/types/ipc";
|
||||
import { ensure } from "@/utils/ensure";
|
||||
import {
|
||||
APPS,
|
||||
APP_TITLES,
|
||||
|
@ -18,13 +20,10 @@ import {
|
|||
SetDialogBoxAttributes,
|
||||
} from "@ente/shared/components/DialogBox/types";
|
||||
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";
|
||||
|
@ -36,7 +35,6 @@ import {
|
|||
} from "@ente/shared/storage/localStorage/helpers";
|
||||
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 ArrowForward from "@mui/icons-material/ArrowForward";
|
||||
import { CssBaseline, useMediaQuery } from "@mui/material";
|
||||
|
@ -45,10 +43,10 @@ import Notification from "components/Notification";
|
|||
import { REDIRECTS } from "constants/redirects";
|
||||
import { t } from "i18next";
|
||||
import isElectron from "is-electron";
|
||||
import { AppProps } from "next/app";
|
||||
import type { AppProps } from "next/app";
|
||||
import { useRouter } from "next/router";
|
||||
import "photoswipe/dist/photoswipe.css";
|
||||
import { createContext, useEffect, useRef, useState } from "react";
|
||||
import { createContext, useContext, useEffect, useRef, useState } from "react";
|
||||
import LoadingBar from "react-top-loading-bar";
|
||||
import DownloadManager from "services/download";
|
||||
import { resumeExportsIfNeeded } from "services/export";
|
||||
|
@ -78,8 +76,11 @@ const redirectMap = new Map([
|
|||
[REDIRECTS.FAMILIES, getFamilyPortalRedirectURL],
|
||||
]);
|
||||
|
||||
type AppContextType = {
|
||||
showNavBar: (show: boolean) => void;
|
||||
/**
|
||||
* Properties available via the {@link AppContext} to the Photos app's React
|
||||
* tree.
|
||||
*/
|
||||
type AppContextT = BaseAppContextT & {
|
||||
mlSearchEnabled: boolean;
|
||||
mapEnabled: boolean;
|
||||
updateMlSearchEnabled: (enabled: boolean) => Promise<void>;
|
||||
|
@ -93,19 +94,22 @@ type AppContextType = {
|
|||
setWatchFolderView: (isOpen: boolean) => void;
|
||||
watchFolderFiles: FileList;
|
||||
setWatchFolderFiles: (files: FileList) => void;
|
||||
isMobile: boolean;
|
||||
themeColor: THEME_COLOR;
|
||||
setThemeColor: SetTheme;
|
||||
setThemeColor: (themeColor: THEME_COLOR) => void;
|
||||
somethingWentWrong: () => void;
|
||||
setDialogBoxAttributesV2: SetDialogBoxAttributesV2;
|
||||
isCFProxyDisabled: boolean;
|
||||
setIsCFProxyDisabled: (disabled: boolean) => void;
|
||||
logout: () => void;
|
||||
};
|
||||
|
||||
export const AppContext = createContext<AppContextType>(null);
|
||||
/** The React {@link Context} available to all pages. */
|
||||
export const AppContext = createContext<AppContextT | undefined>(undefined);
|
||||
|
||||
/** Utility hook to reduce amount of boilerplate in account related pages. */
|
||||
export const useAppContext = () => ensure(useContext(AppContext));
|
||||
|
||||
export default function App({ Component, pageProps }: AppProps) {
|
||||
const appName: AppName = "photos";
|
||||
|
||||
const router = useRouter();
|
||||
const [isI18nReady, setIsI18nReady] = useState<boolean>(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
@ -119,8 +123,9 @@ export default function App({ Component, pageProps }: AppProps) {
|
|||
const isLoadingBarRunning = useRef(false);
|
||||
const loadingBar = useRef(null);
|
||||
const [dialogMessage, setDialogMessage] = useState<DialogBoxAttributes>();
|
||||
const [dialogBoxAttributeV2, setDialogBoxAttributesV2] =
|
||||
useState<DialogBoxAttributesV2>();
|
||||
const [dialogBoxAttributeV2, setDialogBoxAttributesV2] = useState<
|
||||
DialogBoxAttributesV2 | undefined
|
||||
>();
|
||||
useState<DialogBoxAttributes>(null);
|
||||
const [messageDialogView, setMessageDialogView] = useState(false);
|
||||
const [dialogBoxV2View, setDialogBoxV2View] = useState(false);
|
||||
|
@ -327,8 +332,34 @@ export default function App({ Component, pageProps }: AppProps) {
|
|||
void photosLogout().then(() => router.push(PAGES.ROOT));
|
||||
};
|
||||
|
||||
const appContext = {
|
||||
appName,
|
||||
showNavBar,
|
||||
mlSearchEnabled,
|
||||
updateMlSearchEnabled,
|
||||
startLoading,
|
||||
finishLoading,
|
||||
closeMessageDialog,
|
||||
setDialogMessage,
|
||||
watchFolderView,
|
||||
setWatchFolderView,
|
||||
watchFolderFiles,
|
||||
setWatchFolderFiles,
|
||||
isMobile,
|
||||
setNotificationAttributes,
|
||||
themeColor,
|
||||
setThemeColor,
|
||||
somethingWentWrong,
|
||||
setDialogBoxAttributesV2,
|
||||
mapEnabled,
|
||||
updateMapEnabled,
|
||||
isCFProxyDisabled,
|
||||
setIsCFProxyDisabled,
|
||||
logout,
|
||||
};
|
||||
|
||||
const title = isI18nReady
|
||||
? t("TITLE", { context: APPS.PHOTOS })
|
||||
? t("title", { context: "photos" })
|
||||
: APP_TITLES.get(APPS.PHOTOS);
|
||||
|
||||
return (
|
||||
|
@ -362,32 +393,7 @@ export default function App({ Component, pageProps }: AppProps) {
|
|||
attributes={notificationAttributes}
|
||||
/>
|
||||
|
||||
<AppContext.Provider
|
||||
value={{
|
||||
showNavBar,
|
||||
mlSearchEnabled,
|
||||
updateMlSearchEnabled,
|
||||
startLoading,
|
||||
finishLoading,
|
||||
closeMessageDialog,
|
||||
setDialogMessage,
|
||||
watchFolderView,
|
||||
setWatchFolderView,
|
||||
watchFolderFiles,
|
||||
setWatchFolderFiles,
|
||||
isMobile,
|
||||
setNotificationAttributes,
|
||||
themeColor,
|
||||
setThemeColor,
|
||||
somethingWentWrong,
|
||||
setDialogBoxAttributesV2,
|
||||
mapEnabled,
|
||||
updateMapEnabled,
|
||||
isCFProxyDisabled,
|
||||
setIsCFProxyDisabled,
|
||||
logout,
|
||||
}}
|
||||
>
|
||||
<AppContext.Provider value={appContext}>
|
||||
{(loading || !isI18nReady) && (
|
||||
<Overlay
|
||||
sx={(theme) => ({
|
||||
|
|
6
web/apps/photos/src/pages/change-email.tsx
Normal file
6
web/apps/photos/src/pages/change-email.tsx
Normal file
|
@ -0,0 +1,6 @@
|
|||
import Page_ from "@ente/accounts/pages/change-email";
|
||||
import { useAppContext } from "./_app";
|
||||
|
||||
const Page = () => <Page_ appContext={useAppContext()} />;
|
||||
|
||||
export default Page;
|
|
@ -1,9 +0,0 @@
|
|||
import ChangeEmailPage from "@ente/accounts/pages/change-email";
|
||||
import { APPS } from "@ente/shared/apps/constants";
|
||||
import { AppContext } from "pages/_app";
|
||||
import { useContext } from "react";
|
||||
|
||||
export default function ChangeEmail() {
|
||||
const appContext = useContext(AppContext);
|
||||
return <ChangeEmailPage appContext={appContext} appName={APPS.PHOTOS} />;
|
||||
}
|
6
web/apps/photos/src/pages/change-password.tsx
Normal file
6
web/apps/photos/src/pages/change-password.tsx
Normal file
|
@ -0,0 +1,6 @@
|
|||
import Page_ from "@ente/accounts/pages/change-password";
|
||||
import { useAppContext } from "./_app";
|
||||
|
||||
const Page = () => <Page_ appContext={useAppContext()} />;
|
||||
|
||||
export default Page;
|
|
@ -1,9 +0,0 @@
|
|||
import ChangePasswordPage from "@ente/accounts/pages/change-password";
|
||||
import { APPS } from "@ente/shared/apps/constants";
|
||||
import { AppContext } from "pages/_app";
|
||||
import { useContext } from "react";
|
||||
|
||||
export default function ChangePassword() {
|
||||
const appContext = useContext(AppContext);
|
||||
return <ChangePasswordPage appContext={appContext} appName={APPS.PHOTOS} />;
|
||||
}
|
6
web/apps/photos/src/pages/credentials.tsx
Normal file
6
web/apps/photos/src/pages/credentials.tsx
Normal file
|
@ -0,0 +1,6 @@
|
|||
import Page_ from "@ente/accounts/pages/credentials";
|
||||
import { useAppContext } from "./_app";
|
||||
|
||||
const Page = () => <Page_ appContext={useAppContext()} />;
|
||||
|
||||
export default Page;
|
|
@ -1,9 +0,0 @@
|
|||
import CredentialPage from "@ente/accounts/pages/credentials";
|
||||
import { APPS } from "@ente/shared/apps/constants";
|
||||
import { AppContext } from "pages/_app";
|
||||
import { useContext } from "react";
|
||||
|
||||
export default function Credential() {
|
||||
const appContext = useContext(AppContext);
|
||||
return <CredentialPage appContext={appContext} appName={APPS.PHOTOS} />;
|
||||
}
|
|
@ -21,7 +21,7 @@ import {
|
|||
clearKeys,
|
||||
getKey,
|
||||
} from "@ente/shared/storage/sessionStorage";
|
||||
import { User } from "@ente/shared/user/types";
|
||||
import type { User } from "@ente/shared/user/types";
|
||||
import { isPromise } from "@ente/shared/utils";
|
||||
import { Typography, styled } from "@mui/material";
|
||||
import AuthenticateUserModal from "components/AuthenticateUserModal";
|
||||
|
|
6
web/apps/photos/src/pages/generate.tsx
Normal file
6
web/apps/photos/src/pages/generate.tsx
Normal file
|
@ -0,0 +1,6 @@
|
|||
import Page_ from "@ente/accounts/pages/generate";
|
||||
import { useAppContext } from "./_app";
|
||||
|
||||
const Page = () => <Page_ appContext={useAppContext()} />;
|
||||
|
||||
export default Page;
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue