[mob][photos] Resolve conflicts and merge main

This commit is contained in:
ashilkn 2024-05-27 18:03:01 +05:30
commit 99cf23d286
232 changed files with 1677 additions and 1441 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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: [],

View file

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

View file

@ -51,6 +51,7 @@ import 'package:photos/services/user_service.dart';
import 'package:photos/ui/tools/app_lock.dart';
import 'package:photos/ui/tools/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");
}

View file

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

View file

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

View file

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

View file

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

View file

@ -97,7 +97,7 @@ class _AppBarWidgetState extends State<ClusterAppBar> {
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
actions: kDebugMode ? _getDefaultActions(context) : null,
actions: _getDefaultActions(context),
);
}

View file

@ -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(),
],
),
),

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,9 +0,0 @@
import CredentialPage from "@ente/accounts/pages/credentials";
import { APPS } from "@ente/shared/apps/constants";
import { useContext } from "react";
import { AppContext } from "../_app";
export default function Credential() {
const appContext = useContext(AppContext);
return <CredentialPage appContext={appContext} appName={APPS.ACCOUNTS} />;
}

View file

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

View file

@ -1,9 +0,0 @@
import GeneratePage from "@ente/accounts/pages/generate";
import { APPS } from "@ente/shared/apps/constants";
import { AppContext } from "pages/_app";
import { useContext } from "react";
export default function Generate() {
const appContext = useContext(AppContext);
return <GeneratePage appContext={appContext} appName={APPS.ACCOUNTS} />;
}

View file

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

View file

@ -1,9 +0,0 @@
import LoginPage from "@ente/accounts/pages/login";
import { APPS } from "@ente/shared/apps/constants";
import { useContext } from "react";
import { AppContext } from "../_app";
export default function Login() {
const appContext = useContext(AppContext);
return <LoginPage appContext={appContext} appName={APPS.ACCOUNTS} />;
}

View file

@ -1,16 +1,12 @@
import { TwoFactorType } from "@ente/accounts/constants/twofactor";
import 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;

View file

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

View file

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

View file

@ -1,9 +0,0 @@
import RecoverPage from "@ente/accounts/pages/recover";
import { APPS } from "@ente/shared/apps/constants";
import { AppContext } from "pages/_app";
import { useContext } from "react";
export default function Recover() {
const appContext = useContext(AppContext);
return <RecoverPage appContext={appContext} appName={APPS.ACCOUNTS} />;
}

View file

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

View file

@ -1,9 +0,0 @@
import SignupPage from "@ente/accounts/pages/signup";
import { APPS } from "@ente/shared/apps/constants";
import { AppContext } from "pages/_app";
import { useContext } from "react";
export default function Sigup() {
const appContext = useContext(AppContext);
return <SignupPage appContext={appContext} appName={APPS.ACCOUNTS} />;
}

View file

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

View file

@ -1,11 +0,0 @@
import TwoFactorRecoverPage from "@ente/accounts/pages/two-factor/recover";
import { APPS } from "@ente/shared/apps/constants";
import { AppContext } from "pages/_app";
import { useContext } from "react";
export default function TwoFactorRecover() {
const appContext = useContext(AppContext);
return (
<TwoFactorRecoverPage appContext={appContext} appName={APPS.ACCOUNTS} />
);
}

View file

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

View file

@ -1,11 +0,0 @@
import TwoFactorSetupPage from "@ente/accounts/pages/two-factor/setup";
import { APPS } from "@ente/shared/apps/constants";
import { AppContext } from "pages/_app";
import { useContext } from "react";
export default function TwoFactorSetup() {
const appContext = useContext(AppContext);
return (
<TwoFactorSetupPage appContext={appContext} appName={APPS.ACCOUNTS} />
);
}

View file

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

View file

@ -1,11 +0,0 @@
import TwoFactorVerifyPage from "@ente/accounts/pages/two-factor/verify";
import { APPS } from "@ente/shared/apps/constants";
import { AppContext } from "pages/_app";
import { useContext } from "react";
export default function TwoFactorVerify() {
const appContext = useContext(AppContext);
return (
<TwoFactorVerifyPage appContext={appContext} appName={APPS.ACCOUNTS} />
);
}

View file

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

View file

@ -1,9 +0,0 @@
import VerifyPage from "@ente/accounts/pages/verify";
import { APPS } from "@ente/shared/apps/constants";
import { AppContext } from "pages/_app";
import { useContext } from "react";
export default function Verify() {
const appContext = useContext(AppContext);
return <VerifyPage appContext={appContext} appName={APPS.ACCOUNTS} />;
}

View file

@ -1 +1,4 @@
NEXT_TELEMETRY_DISABLED = 1
# For details on how to populate a .env.local to run auth and get it to connect
# to an arbitrary Ente instance, see `apps/photos/.env`.

View file

@ -9,5 +9,5 @@ module.exports = {
tsconfigRootDir: __dirname,
project: "./tsconfig.json",
},
ignorePatterns: [".eslintrc.js", "out"],
ignorePatterns: [".eslintrc.js", "next.config.js", "out"],
};

View file

@ -3,6 +3,7 @@
"version": "0.0.0",
"private": true,
"dependencies": {
"@/build-config": "*",
"@/next": "*",
"@ente/accounts": "*",
"@ente/eslint-config": "*",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,7 +3,7 @@ import {
FormControlLabel,
FormGroup,
Typography,
TypographyProps,
type TypographyProps,
} from "@mui/material";
interface Iprops {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -6,7 +6,7 @@ import {
SelectChangeEvent,
Stack,
Typography,
TypographyProps,
type TypographyProps,
} from "@mui/material";
export interface DropdownOption<T> {

View file

@ -1,5 +1,5 @@
import CircularProgress, {
CircularProgressProps,
type CircularProgressProps,
} from "@mui/material/CircularProgress";
export default function EnteSpinner(props: CircularProgressProps) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,8 +1,8 @@
import { Overlay } from "@ente/shared/components/Container";
import {
CircularProgress,
CircularProgressProps,
Typography,
type CircularProgressProps,
} from "@mui/material";
function CircularProgressWithLabel(

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,9 +0,0 @@
import ChangeEmailPage from "@ente/accounts/pages/change-email";
import { APPS } from "@ente/shared/apps/constants";
import { AppContext } from "pages/_app";
import { useContext } from "react";
export default function ChangeEmail() {
const appContext = useContext(AppContext);
return <ChangeEmailPage appContext={appContext} appName={APPS.PHOTOS} />;
}

View file

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

View file

@ -1,9 +0,0 @@
import ChangePasswordPage from "@ente/accounts/pages/change-password";
import { APPS } from "@ente/shared/apps/constants";
import { AppContext } from "pages/_app";
import { useContext } from "react";
export default function ChangePassword() {
const appContext = useContext(AppContext);
return <ChangePasswordPage appContext={appContext} appName={APPS.PHOTOS} />;
}

View file

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

View file

@ -1,9 +0,0 @@
import CredentialPage from "@ente/accounts/pages/credentials";
import { APPS } from "@ente/shared/apps/constants";
import { AppContext } from "pages/_app";
import { useContext } from "react";
export default function Credential() {
const appContext = useContext(AppContext);
return <CredentialPage appContext={appContext} appName={APPS.PHOTOS} />;
}

View file

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

View file

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

Some files were not shown because too many files have changed in this diff Show more