Compare commits

..

3 commits

Author SHA1 Message Date
Prateek Sunal
fae54faffc fix(icons): update dev flavor icon 2024-05-26 17:29:04 +05:30
Prateek Sunal
d4f781bf35 fix(auth): update video editor 2024-05-20 20:57:44 +05:30
Prateek Sunal
9c4e72aa0f feat(mobile): init video editor 2024-05-17 01:32:13 +05:30
556 changed files with 8447 additions and 23839 deletions

View file

@ -4,12 +4,11 @@ labels: ["triage"]
body:
- type: markdown
attributes:
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.
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).
- type: textarea
attributes:
label: Description
@ -17,8 +16,7 @@ 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 [feature
requests](https://github.com/ente-io/ente/discussions/categories/feature-requests)).
a good fit for Discussions).
validations:
required: true
- type: input

View file

@ -4,7 +4,7 @@ on:
workflow_dispatch: # Allow manually running the action
env:
FLUTTER_VERSION: "3.22.0"
FLUTTER_VERSION: "3.19.3"
jobs:
build:

View file

@ -9,7 +9,7 @@ on:
- ".github/workflows/mobile-lint.yml"
env:
FLUTTER_VERSION: "3.22.0"
FLUTTER_VERSION: "3.19.5"
jobs:
lint:

6
.gitignore vendored
View file

@ -1,6 +1,8 @@
# Let folks use their custom editor settings
# Let folks use their custom .vscode settings
.vscode
.idea
# macOS
.DS_Store
.idea
.ente.authenticator.db
.ente.offline_authenticator.db

View file

@ -1,7 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application android:name="${applicationName}"
android:label="Auth"
android:label="auth"
android:icon="@mipmap/launcher_icon"
android:usesCleartextTraffic="true"
android:requestLegacyExternalStorage="true"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 801 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View file

@ -85,16 +85,20 @@ PODS:
- SDWebImage (5.19.2):
- SDWebImage/Core (= 5.19.2)
- SDWebImage/Core (5.19.2)
- Sentry/HybridSDK (8.25.0)
- sentry_flutter (7.20.1):
- Sentry/HybridSDK (8.21.0):
- SentryPrivate (= 8.21.0)
- sentry_flutter (0.0.1):
- Flutter
- FlutterMacOS
- Sentry/HybridSDK (= 8.25.0)
- Sentry/HybridSDK (= 8.21.0)
- SentryPrivate (8.21.0)
- share_plus (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- smart_auth (0.0.1):
- Flutter
- sodium_libs (2.2.1):
- Flutter
- sqflite (0.0.3):
@ -111,7 +115,7 @@ PODS:
- sqlite3/common
- sqlite3_flutter_libs (0.0.1):
- Flutter
- "sqlite3 (~> 3.45.3+1)"
- sqlite3 (~> 3.45.1)
- sqlite3/fts5
- sqlite3/perf-threadsafe
- sqlite3/rtree
@ -144,6 +148,7 @@ DEPENDENCIES:
- sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- smart_auth (from `.symlinks/plugins/smart_auth/ios`)
- sodium_libs (from `.symlinks/plugins/sodium_libs/ios`)
- sqflite (from `.symlinks/plugins/sqflite/darwin`)
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/ios`)
@ -158,6 +163,7 @@ SPEC REPOS:
- ReachabilitySwift
- SDWebImage
- Sentry
- SentryPrivate
- sqlite3
- SwiftyGif
- Toast
@ -209,6 +215,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
smart_auth:
:path: ".symlinks/plugins/smart_auth/ios"
sodium_libs:
:path: ".symlinks/plugins/sodium_libs/ios"
sqflite:
@ -228,13 +236,13 @@ SPEC CHECKSUMS:
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
fk_user_agent: 1f47ec39291e8372b1d692b50084b0d54103c545
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_email_sender: 10a22605f92809a11ef52b2f412db806c6082d40
flutter_email_sender: 02d7443217d8c41483223627972bfdc09f74276b
flutter_inappwebview_ios: 97215cf7d4677db55df76782dbd2930c5e1c1ea0
flutter_local_authentication: 1172a4dd88f6306dadce067454e2c4caf07977bb
flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086
flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be
fluttertoast: 9f2f8e81bb5ce18facb9748d7855bf5a756fe3db
fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265
local_auth_darwin: c7e464000a6a89e952235699e32b329457608d98
move_to_background: 39a5b79b26d577b0372cbe8a8c55e7aa9fcd3a2d
MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
@ -245,14 +253,16 @@ SPEC CHECKSUMS:
qr_code_scanner: bb67d64904c3b9658ada8c402e8b4d406d5d796e
ReachabilitySwift: 2128f3a8c9107e1ad33574c6e58e8285d460b149
SDWebImage: dfe95b2466a9823cf9f0c6d01217c06550d7b29a
Sentry: cd86fc55628f5b7c572cabe66cc8f95a9d2f165a
sentry_flutter: 4cb24c1055c556d7b27262ab2e179d1e5a0b9b0c
Sentry: ebc12276bd17613a114ab359074096b6b3725203
sentry_flutter: dff1df05dc39c83d04f9330b36360fc374574c5e
SentryPrivate: d651efb234cf385ec9a1cdd3eff94b5e78a0e0fe
share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5
shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695
smart_auth: 4bedbc118723912d0e45a07e8ab34039c19e04f2
sodium_libs: 1faae17af662384acbd13e41867a0008cd2e2318
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
sqlite3: 02d1f07eaaa01f80a1c16b4b31dfcbb3345ee01a
sqlite3_flutter_libs: 9bfe005308998aeca155330bbc2ea6dddf834a3b
sqlite3_flutter_libs: af0e8fe9bce48abddd1ffdbbf839db0302d72d80
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
Toast: 1f5ea13423a1e6674c4abdac5be53587ae481c4e
url_launcher_ios: 6116280ddcfe98ab8820085d8d76ae7449447586

View file

@ -20,7 +20,7 @@
<string>es</string>
</array>
<key>CFBundleName</key>
<string>Auth</string>
<string>auth</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>

View file

@ -189,7 +189,7 @@ class _AppState extends State<App> with WindowListener, TrayListener {
windowManager.show();
break;
case 'exit_app':
windowManager.destroy();
windowManager.close();
break;
}
}

View file

@ -20,8 +20,6 @@
"codeIssuerHint": "発行者",
"codeSecretKeyHint": "秘密鍵",
"codeAccountHint": "アカウント (you@domain.com)",
"codeTagHint": "タグ",
"accountKeyType": "鍵の種類",
"sessionExpired": "セッションが失効しました",
"@sessionExpired": {
"description": "Title of the dialog when the users current session is invalid/expired"
@ -79,7 +77,6 @@
"data": "データ",
"importCodes": "コードをインポート",
"importTypePlainText": "プレーンテキスト",
"importTypeEnteEncrypted": "Ente 暗号化されたエクスポート",
"passwordForDecryptingExport": "復号化用パスワード",
"passwordEmptyError": "パスワードは空欄にできません",
"importFromApp": "{appName} からコードをインポート",
@ -124,7 +121,6 @@
"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": "コードを削除するにはどうすればいいですか?",
@ -158,7 +154,6 @@
}
}
},
"invalidQRCode": "QRコードが無効です",
"noRecoveryKeyTitle": "回復キーがありませんか?",
"enterEmailHint": "メールアドレスを入力してください",
"invalidEmailTitle": "メールアドレスが無効です",
@ -352,7 +347,6 @@
"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."
@ -423,18 +417,5 @@
"invalidEndpoint": "無効なエンドポイントです",
"invalidEndpointMessage": "入力されたエンドポイントは無効です。有効なエンドポイントを入力して再試行してください。",
"endpointUpdatedMessage": "エンドポイントの更新に成功しました",
"customEndpoint": "{endpoint} に接続しました",
"pinText": "固定",
"unpinText": "固定を解除",
"pinnedCodeMessage": "{code} を固定しました",
"unpinnedCodeMessage": "{code} の固定が解除されました",
"tags": "タグ",
"createNewTag": "新しいタグの作成",
"tag": "タグ",
"create": "作成",
"editTag": "タグの編集",
"deleteTagTitle": "タグを削除しますか?",
"deleteTagMessage": "このタグを削除してもよろしいですか?この操作は取り消しできません。",
"somethingWentWrongParsingCode": "{x} のコードを解析できませんでした。",
"updateNotAvailable": "アップデートは利用できません"
"customEndpoint": "{endpoint} に接続しました"
}

View file

@ -20,8 +20,6 @@
"codeIssuerHint": "Emissor",
"codeSecretKeyHint": "Chave secreta",
"codeAccountHint": "Conta (voce@dominio.com)",
"codeTagHint": "Etiqueta",
"accountKeyType": "Tipo de chave",
"sessionExpired": "Sessão expirada",
"@sessionExpired": {
"description": "Title of the dialog when the users current session is invalid/expired"
@ -158,7 +156,6 @@
}
}
},
"invalidQRCode": "QR Code inválido",
"noRecoveryKeyTitle": "Sem chave de recuperação?",
"enterEmailHint": "Insira o seu endereço de e-mail",
"invalidEmailTitle": "Endereço de e-mail inválido",
@ -423,16 +420,5 @@
"invalidEndpoint": "Endpoint inválido",
"invalidEndpointMessage": "Desculpe, o endpoint que você inseriu é inválido. Por favor, insira um endpoint válido e tente novamente.",
"endpointUpdatedMessage": "Endpoint atualizado com sucesso",
"customEndpoint": "Conectado a {endpoint}",
"pinText": "Fixar",
"pinnedCodeMessage": "{code} foi fixado",
"tags": "Etiquetas",
"createNewTag": "Criar etiqueta",
"tag": "Etiqueta",
"create": "Criar",
"editTag": "Editar etiqueta",
"deleteTagTitle": "Excluir etiqueta?",
"deleteTagMessage": "Tem certeza de que deseja excluir esta etiqueta? Essa ação é irreversível.",
"somethingWentWrongParsingCode": "Não foi possível analisar os códigos {x}.",
"updateNotAvailable": "Atualização não está disponível"
"customEndpoint": "Conectado a {endpoint}"
}

View file

@ -188,8 +188,6 @@
"recoveryKeySaveDescription": "Мы не храним этот ключ, пожалуйста, сохраните этот ключ в безопасном месте.",
"doThisLater": "Сделать позже",
"saveKey": "Сохранить ключ",
"save": "Сохранить",
"send": "Отправить",
"back": "Вернуться",
"createAccount": "Создать аккаунт",
"passwordStrength": "Мощность пароля: {passwordStrengthValue}",
@ -396,13 +394,5 @@
"signOutOtherDevices": "Выйти из других устройств",
"doNotSignOut": "Не выходить",
"hearUsWhereTitle": "Как вы узнали о Ente? (необязательно)",
"hearUsExplanation": "Будет полезно, если вы укажете, где нашли нас, так как мы не отслеживаем установки приложения",
"waitingForVerification": "Ожидание подтверждения...",
"developerSettingsWarning": "Вы уверены, что хотите изменить настройки разработчика?",
"developerSettings": "Настройки разработчика",
"serverEndpoint": "Конечная точка сервера",
"invalidEndpoint": "Неверная конечная точка",
"invalidEndpointMessage": "Извините, введенная вами конечная точка неверна. Пожалуйста, введите корректную конечную точку и повторите попытку.",
"endpointUpdatedMessage": "Конечная точка успешно обновлена",
"customEndpoint": "Подключено к {endpoint}"
"hearUsExplanation": "Будет полезно, если вы укажете, где нашли нас, так как мы не отслеживаем установки приложения"
}

View file

@ -20,8 +20,6 @@
"codeIssuerHint": "发行人",
"codeSecretKeyHint": "私钥",
"codeAccountHint": "账户 (you@domain.com)",
"codeTagHint": "标签",
"accountKeyType": "密钥类型",
"sessionExpired": "会话已过期",
"@sessionExpired": {
"description": "Title of the dialog when the users current session is invalid/expired"
@ -158,7 +156,6 @@
}
}
},
"invalidQRCode": "二维码无效",
"noRecoveryKeyTitle": "没有恢复密钥吗?",
"enterEmailHint": "请输入您的电子邮件地址",
"invalidEmailTitle": "无效的电子邮件地址",
@ -423,18 +420,5 @@
"invalidEndpoint": "端点无效",
"invalidEndpointMessage": "抱歉,您输入的端点无效。请输入有效的端点,然后重试。",
"endpointUpdatedMessage": "端点更新成功",
"customEndpoint": "已连接至 {endpoint}",
"pinText": "置顶",
"unpinText": "取消置顶",
"pinnedCodeMessage": "{code} 已被置顶",
"unpinnedCodeMessage": "{code} 已被取消置顶",
"tags": "标签",
"createNewTag": "创建新标签",
"tag": "标签",
"create": "创建",
"editTag": "编辑标签",
"deleteTagTitle": "要删除标签吗?",
"deleteTagMessage": "您确定要删除此标签吗?此操作是不可逆的。",
"somethingWentWrongParsingCode": "我们无法解析 {x} 代码。",
"updateNotAvailable": "更新不可用"
"customEndpoint": "已连接至 {endpoint}"
}

View file

@ -125,10 +125,10 @@ class Code {
final issuer = _getIssuer(uri);
try {
final code = Code(
return Code(
_getAccount(uri),
issuer,
_getDigits(uri),
_getDigits(uri, issuer),
_getPeriod(uri),
getSanitizedSecret(uri.queryParameters['secret']!),
_getAlgorithm(uri),
@ -137,7 +137,6 @@ class Code {
rawData,
display: CodeDisplay.fromUri(uri) ?? CodeDisplay(),
);
return code;
} catch (e) {
// if account name contains # without encoding,
// rest of the url are treated as url fragment
@ -175,11 +174,12 @@ class Code {
}
String toOTPAuthUrlFormat() {
final uri = Uri.parse(rawData.replaceAll("#", '%23'));
final uri = Uri.parse(rawData);
final query = {...uri.queryParameters};
query["codeDisplay"] = jsonEncode(display.toJson());
final newUri = uri.replace(queryParameters: query);
return jsonEncode(newUri.toString());
}
@ -201,11 +201,11 @@ class Code {
}
}
static int _getDigits(Uri uri) {
static int _getDigits(Uri uri, String issuer) {
try {
return int.parse(uri.queryParameters['digits']!);
} catch (e) {
if (uri.host == "steam") {
if (issuer.toLowerCase() == "steam") {
return steamDigits;
}
return defaultDigits;

View file

@ -1,7 +1,6 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:logging/logging.dart';
/// Used to store the display settings of a code.
class CodeDisplay {
@ -55,34 +54,13 @@ class CodeDisplay {
);
}
/// Converts the [CodeDisplay] to a json object.
/// When [safeParsing] is true, the json will be parsed safely.
/// If we fail to parse the json, we will return an empty [CodeDisplay].
static CodeDisplay? fromUri(Uri uri, {bool safeParsing = false}) {
static CodeDisplay? fromUri(Uri uri) {
if (!uri.queryParameters.containsKey("codeDisplay")) return null;
final String codeDisplay =
uri.queryParameters['codeDisplay']!.replaceAll('%2C', ',');
return _parseCodeDisplayJson(codeDisplay, safeParsing);
}
final decodedDisplay = jsonDecode(codeDisplay);
static CodeDisplay _parseCodeDisplayJson(String json, bool safeParsing) {
try {
final decodedDisplay = jsonDecode(json);
return CodeDisplay.fromJson(decodedDisplay);
} catch (e, s) {
Logger("CodeDisplay")
.severe("Could not parse code display from json", e, s);
// (ng/prateek) Handle the case where we have fragment in the rawDataUrl
if (!json.endsWith("}") && json.contains("}#")) {
Logger("CodeDisplay").warning("ignoring code display as it's invalid");
return CodeDisplay();
}
if (safeParsing) {
return CodeDisplay();
} else {
rethrow;
}
}
return CodeDisplay.fromJson(decodedDisplay);
}
Map<String, dynamic> toJson() {

View file

@ -240,7 +240,7 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
final account = _accountController.text.trim();
final issuer = _issuerController.text.trim();
final secret = _secretController.text.trim().replaceAll(' ', '');
final isStreamCode = issuer.toLowerCase() == "steam" || issuer.toLowerCase().contains('steampowered.com');
final isStreamCode = issuer.toLowerCase() == "steam";
if (widget.code != null && widget.code!.secret != secret) {
ButtonResult? result = await showChoiceActionSheet(
context,

View file

@ -41,9 +41,9 @@ class CodeStore {
} else {
code = Code.fromExportJson(decodeJson);
}
} catch (e, s) {
} catch (e) {
code = Code.withError(e, entity.rawData);
_logger.severe("Could not parse code", e, s);
_logger.severe("Could not parse code", code.err);
}
code.generatedID = entity.generatedID;
code.hasSynced = entity.hasSynced;

View file

@ -48,6 +48,7 @@ class _CodeWidgetState extends State<CodeWidget> {
late bool _shouldShowLargeIcon;
late bool _hideCode;
bool isMaskingEnabled = false;
late final colorScheme = getEnteColorScheme(context);
@override
void initState() {
@ -77,7 +78,6 @@ class _CodeWidgetState extends State<CodeWidget> {
@override
Widget build(BuildContext context) {
final colorScheme = getEnteColorScheme(context);
if (isMaskingEnabled != PreferenceService.instance.shouldHideCodes()) {
isMaskingEnabled = PreferenceService.instance.shouldHideCodes();
_hideCode = isMaskingEnabled;
@ -91,100 +91,6 @@ class _CodeWidgetState extends State<CodeWidget> {
_isInitialized = true;
}
final l10n = context.l10n;
Widget getCardContents(AppLocalizations l10n) {
return Stack(
children: [
if (widget.code.isPinned)
Align(
alignment: Alignment.topRight,
child: CustomPaint(
painter: PinBgPainter(
color: colorScheme.pinnedBgColor,
),
size: const Size(39, 39),
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (widget.code.type.isTOTPCompatible)
CodeTimerProgress(
period: widget.code.period,
),
const SizedBox(height: 16),
Row(
children: [
_shouldShowLargeIcon ? _getIcon() : const SizedBox.shrink(),
Expanded(
child: Column(
children: [
_getTopRow(),
const SizedBox(height: 4),
_getBottomRow(l10n),
],
),
),
],
),
const SizedBox(
height: 20,
),
],
),
if (widget.code.isPinned) ...[
Align(
alignment: Alignment.topRight,
child: Padding(
padding: const EdgeInsets.only(right: 6, top: 6),
child: SvgPicture.asset("assets/svg/pin-card.svg"),
),
),
],
],
);
}
Widget clippedCard(AppLocalizations l10n) {
return Container(
height: 132,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: Theme.of(context).colorScheme.codeCardBackgroundColor,
boxShadow:
widget.code.isPinned ? colorScheme.pinnedCardBoxShadow : [],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Material(
color: Colors.transparent,
child: InkWell(
customBorder: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
onTap: () {
_copyCurrentOTPToClipboard();
},
onDoubleTap: isMaskingEnabled
? () {
setState(
() {
_hideCode = !_hideCode;
},
);
}
: null,
onLongPress: () {
_copyCurrentOTPToClipboard();
},
child: getCardContents(l10n),
),
),
),
);
}
return Container(
margin: const EdgeInsets.only(left: 16, right: 16, bottom: 8, top: 8),
child: Builder(
@ -220,7 +126,7 @@ class _CodeWidgetState extends State<CodeWidget> {
],
padding: const EdgeInsets.all(8.0),
),
child: clippedCard(l10n),
child: _clippedCard(l10n),
);
}
@ -310,7 +216,7 @@ class _CodeWidgetState extends State<CodeWidget> {
],
),
child: Builder(
builder: (context) => clippedCard(l10n),
builder: (context) => _clippedCard(l10n),
),
);
},
@ -318,6 +224,98 @@ class _CodeWidgetState extends State<CodeWidget> {
);
}
Widget _clippedCard(AppLocalizations l10n) {
return Container(
height: 132,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: Theme.of(context).colorScheme.codeCardBackgroundColor,
boxShadow: widget.code.isPinned ? colorScheme.pinnedCardBoxShadow : [],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Material(
color: Colors.transparent,
child: InkWell(
customBorder: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
onTap: () {
_copyCurrentOTPToClipboard();
},
onDoubleTap: isMaskingEnabled
? () {
setState(
() {
_hideCode = !_hideCode;
},
);
}
: null,
onLongPress: () {
_copyCurrentOTPToClipboard();
},
child: _getCardContents(l10n),
),
),
),
);
}
Widget _getCardContents(AppLocalizations l10n) {
return Stack(
children: [
if (widget.code.isPinned)
Align(
alignment: Alignment.topRight,
child: CustomPaint(
painter: PinBgPainter(
color: colorScheme.pinnedBgColor,
),
size: const Size(39, 39),
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (widget.code.type == Type.totp)
CodeTimerProgress(
period: widget.code.period,
),
const SizedBox(height: 16),
Row(
children: [
_shouldShowLargeIcon ? _getIcon() : const SizedBox.shrink(),
Expanded(
child: Column(
children: [
_getTopRow(),
const SizedBox(height: 4),
_getBottomRow(l10n),
],
),
),
],
),
const SizedBox(
height: 20,
),
],
),
if (widget.code.isPinned) ...[
Align(
alignment: Alignment.topRight,
child: Padding(
padding: const EdgeInsets.only(right: 6, top: 6),
child: SvgPicture.asset("assets/svg/pin-card.svg"),
),
),
],
],
);
}
Widget _getBottomRow(AppLocalizations l10n) {
return Container(
padding: const EdgeInsets.only(left: 16, right: 16),
@ -587,7 +585,7 @@ class _CodeWidgetState extends State<CodeWidget> {
String _getFormattedCode(String code) {
if (_hideCode) {
// replace all digits with
code = code.replaceAll(RegExp(r'\S'), '');
code = code.replaceAll(RegExp(r'\d'), '');
}
if (code.length == 6) {
return "${code.substring(0, 3)} ${code.substring(3, 6)}";

View file

@ -1,12 +1,8 @@
import 'package:ente_auth/models/code.dart';
import 'package:flutter/foundation.dart';
import 'package:otp/otp.dart' as otp;
import 'package:steam_totp/steam_totp.dart';
String getOTP(Code code) {
if (code.type == Type.steam) {
return _getSteamCode(code);
}
if (code.type == Type.hotp) {
return _getHOTPCode(code);
}
@ -30,18 +26,7 @@ String _getHOTPCode(Code code) {
);
}
String _getSteamCode(Code code, [bool isNext = false]) {
final SteamTOTP steamtotp = SteamTOTP(secret: code.secret);
return steamtotp.generate(
DateTime.now().millisecondsSinceEpoch ~/ 1000 + (isNext ? code.period : 0),
);
}
String getNextTotp(Code code) {
if (code.type == Type.steam) {
return _getSteamCode(code, true);
}
return otp.OTP.generateTOTPCodeString(
getSanitizedSecret(code.secret),
DateTime.now().millisecondsSinceEpoch + code.period * 1000,

View file

@ -29,16 +29,20 @@ PODS:
- ReachabilitySwift (5.2.2)
- screen_retriever (0.0.1):
- FlutterMacOS
- Sentry/HybridSDK (8.25.0)
- sentry_flutter (7.20.1):
- Sentry/HybridSDK (8.21.0):
- SentryPrivate (= 8.21.0)
- sentry_flutter (0.0.1):
- Flutter
- FlutterMacOS
- Sentry/HybridSDK (= 8.25.0)
- Sentry/HybridSDK (= 8.21.0)
- SentryPrivate (8.21.0)
- share_plus (0.0.1):
- FlutterMacOS
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- smart_auth (0.0.1):
- FlutterMacOS
- sodium_libs (2.2.1):
- FlutterMacOS
- sqflite (0.0.3):
@ -55,7 +59,7 @@ PODS:
- sqlite3/common
- sqlite3_flutter_libs (0.0.1):
- FlutterMacOS
- "sqlite3 (~> 3.45.3+1)"
- sqlite3 (~> 3.45.1)
- sqlite3/fts5
- sqlite3/perf-threadsafe
- sqlite3/rtree
@ -83,6 +87,7 @@ DEPENDENCIES:
- sentry_flutter (from `Flutter/ephemeral/.symlinks/plugins/sentry_flutter/macos`)
- share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`)
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
- smart_auth (from `Flutter/ephemeral/.symlinks/plugins/smart_auth/macos`)
- sodium_libs (from `Flutter/ephemeral/.symlinks/plugins/sodium_libs/macos`)
- sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/darwin`)
- sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos`)
@ -95,6 +100,7 @@ SPEC REPOS:
- OrderedSet
- ReachabilitySwift
- Sentry
- SentryPrivate
- sqlite3
EXTERNAL SOURCES:
@ -130,6 +136,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos
shared_preferences_foundation:
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
smart_auth:
:path: Flutter/ephemeral/.symlinks/plugins/smart_auth/macos
sodium_libs:
:path: Flutter/ephemeral/.symlinks/plugins/sodium_libs/macos
sqflite:
@ -159,14 +167,16 @@ SPEC CHECKSUMS:
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
ReachabilitySwift: 2128f3a8c9107e1ad33574c6e58e8285d460b149
screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38
Sentry: cd86fc55628f5b7c572cabe66cc8f95a9d2f165a
sentry_flutter: 4cb24c1055c556d7b27262ab2e179d1e5a0b9b0c
Sentry: ebc12276bd17613a114ab359074096b6b3725203
sentry_flutter: dff1df05dc39c83d04f9330b36360fc374574c5e
SentryPrivate: d651efb234cf385ec9a1cdd3eff94b5e78a0e0fe
share_plus: 76dd39142738f7a68dd57b05093b5e8193f220f7
shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695
smart_auth: b38e3ab4bfe089eacb1e233aca1a2340f96c28e9
sodium_libs: d39bd76697736cb11ce4a0be73b9b4bc64466d6f
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
sqlite3: 02d1f07eaaa01f80a1c16b4b31dfcbb3345ee01a
sqlite3_flutter_libs: 8d204ef443cf0d5c1c8b058044eab53f3943a9c5
sqlite3_flutter_libs: 06a05802529659a272beac4ee1350bfec294f386
tray_manager: 9064e219c56d75c476e46b9a21182087930baf90
url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95
window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 324 B

After

Width:  |  Height:  |  Size: 240 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 780 B

After

Width:  |  Height:  |  Size: 628 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -745,22 +745,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.0"
hashlib:
dependency: transitive
description:
name: hashlib
sha256: "67e640e19cc33070113acab3125cd48ebe480a0300e15554dec089b8878a729f"
url: "https://pub.dev"
source: hosted
version: "1.16.0"
hashlib_codecs:
dependency: transitive
description:
name: hashlib_codecs
sha256: "49e2a471f74b15f1854263e58c2ac11f2b631b5b12c836f9708a35397d36d626"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
hex:
dependency: transitive
description:
@ -1455,14 +1439,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.11.1"
steam_totp:
dependency: "direct main"
description:
name: steam_totp
sha256: "3c09143c983f6bb05bb53e9232f9d40bbcc01c596ba0273c3e6bb246729abfa1"
url: "https://pub.dev"
source: hosted
version: "0.0.1"
step_progress_indicator:
dependency: "direct main"
description:

View file

@ -1,6 +1,6 @@
name: ente_auth
description: ente two-factor authenticator
version: 3.0.4+304
version: 2.0.58+258
publish_to: none
environment:
@ -94,7 +94,6 @@ dependencies:
sqflite_common_ffi: ^2.3.0+4
sqlite3: ^2.1.0
sqlite3_flutter_libs: ^0.5.19+1
steam_totp: ^0.0.1
step_progress_indicator: ^1.0.2
styled_text: ^8.1.0
tray_manager: ^0.2.1

View file

@ -14,7 +14,7 @@
"build:ci": "yarn build-renderer && tsc",
"build:quick": "yarn build-renderer && yarn build-main:quick",
"dev": "concurrently --kill-others --success first --names 'main,rndr' \"yarn dev-main\" \"yarn dev-renderer\"",
"dev-main": "tsc && electron .",
"dev-main": "tsc && electron app/main.js",
"dev-renderer": "cd ../web && yarn install && yarn dev:photos",
"postinstall": "electron-builder install-app-deps",
"lint": "yarn prettier --check --log-level warn . && eslint --ext .ts src && yarn tsc",
@ -30,7 +30,7 @@
"compare-versions": "^6.1",
"electron-log": "^5.1",
"electron-store": "^8.2",
"electron-updater": "^6.2",
"electron-updater": "^6.1",
"ffmpeg-static": "^5.2",
"html-entities": "^2.5",
"jpeg-js": "^0.4",

View file

@ -241,7 +241,7 @@ const uniqueSavePath = (dirPath: string, fileName: string) => {
*
* @param webContents The renderer to configure.
*/
export const allowExternalLinks = (webContents: WebContents) =>
export const allowExternalLinks = (webContents: WebContents) => {
// By default, if the user were open a link, say
// https://github.com/ente-io/ente/discussions, then it would open a _new_
// BrowserWindow within our app.
@ -253,37 +253,13 @@ export const allowExternalLinks = (webContents: WebContents) =>
// Returning `action` "deny" accomplishes this.
webContents.setWindowOpenHandler(({ url }) => {
if (!url.startsWith(rendererURL)) {
// This does not work in Ubuntu currently: mailto links seem to just
// get ignored, and HTTP links open in the text editor instead of in
// the browser.
// https://github.com/electron/electron/issues/31485
void shell.openExternal(url);
return { action: "deny" };
} else {
return { action: "allow" };
}
});
/**
* Allow uploading to arbitrary S3 buckets.
*
* The files in the desktop app are served over the ente:// protocol. During
* testing or self-hosting, we might be using a S3 bucket that does not allow
* whitelisting a custom URI scheme. To avoid requiring the bucket to set an
* "Access-Control-Allow-Origin: *" or do a echo-back of `Origin`, we add a
* workaround here instead, intercepting the ACAO header and allowing `*`.
*/
export const allowAllCORSOrigins = (webContents: WebContents) =>
webContents.session.webRequest.onHeadersReceived(
({ responseHeaders }, callback) => {
const headers: NonNullable<typeof responseHeaders> = {};
for (const [key, value] of Object.entries(responseHeaders ?? {}))
if (key.toLowerCase() != "access-control-allow-origin")
headers[key] = value;
headers["Access-Control-Allow-Origin"] = ["*"];
callback({ responseHeaders: headers });
},
);
};
/**
* Add an icon for our app in the system tray.
@ -315,18 +291,32 @@ const setupTrayItem = (mainWindow: BrowserWindow) => {
/**
* Older versions of our app used to maintain a cache dir using the main
* process. This has been removed in favor of cache on the web layer. Delete the
* old cache dir if it exists.
* process. This has been removed in favor of cache on the web layer.
*
* Added May 2024, v1.7.0. This migration code can be removed after some time
* once most people have upgraded to newer versions.
* Delete the old cache dir if it exists.
*
* This will happen in two phases. The cache had three subdirectories:
*
* - Two of them, "thumbs" and "files", will be removed now (v1.7.0, May 2024).
*
* - The third one, "face-crops" will be removed once we finish the face search
* changes. See: [Note: Legacy face crops].
*
* This migration code can be removed after some time once most people have
* upgraded to newer versions.
*/
const deleteLegacyDiskCacheDirIfExists = async () => {
const removeIfExists = async (dirPath: string) => {
if (existsSync(dirPath)) {
log.info(`Removing legacy disk cache from ${dirPath}`);
await fs.rm(dirPath, { recursive: true });
}
};
// [Note: Getting the cache path]
//
// The existing code was passing "cache" as a parameter to getPath.
//
// However, "cache" is not a valid parameter to getPath. It works (for
// However, "cache" is not a valid parameter to getPath. It works! (for
// example, on macOS I get `~/Library/Caches`), but it is intentionally not
// documented as part of the public API:
//
@ -339,8 +329,8 @@ const deleteLegacyDiskCacheDirIfExists = async () => {
// @ts-expect-error "cache" works but is not part of the public API.
const cacheDir = path.join(app.getPath("cache"), "ente");
if (existsSync(cacheDir)) {
log.info(`Removing legacy disk cache from ${cacheDir}`);
await fs.rm(cacheDir, { recursive: true });
await removeIfExists(path.join(cacheDir, "thumbs"));
await removeIfExists(path.join(cacheDir, "files"));
}
};
@ -400,10 +390,8 @@ const main = () => {
registerStreamProtocol();
// Configure the renderer's environment.
const webContents = mainWindow.webContents;
setDownloadPath(webContents);
allowExternalLinks(webContents);
allowAllCORSOrigins(webContents);
setDownloadPath(mainWindow.webContents);
allowExternalLinks(mainWindow.webContents);
// Start loading the renderer.
void mainWindow.loadURL(rendererURL);

View file

@ -24,6 +24,7 @@ import {
updateOnNextRestart,
} from "./services/app-update";
import {
legacyFaceCrop,
openDirectory,
openLogDirectory,
selectDirectory,
@ -42,10 +43,10 @@ import {
import { convertToJPEG, generateImageThumbnail } from "./services/image";
import { logout } from "./services/logout";
import {
computeCLIPImageEmbedding,
computeCLIPTextEmbeddingIfAvailable,
clipImageEmbedding,
clipTextEmbeddingIfAvailable,
} from "./services/ml-clip";
import { computeFaceEmbeddings, detectFaces } from "./services/ml-face";
import { detectFaces, faceEmbeddings } from "./services/ml-face";
import { encryptionKey, saveEncryptionKey } from "./services/store";
import {
clearPendingUploads,
@ -169,22 +170,24 @@ export const attachIPCHandlers = () => {
// - ML
ipcMain.handle(
"computeCLIPImageEmbedding",
(_, jpegImageData: Uint8Array) =>
computeCLIPImageEmbedding(jpegImageData),
ipcMain.handle("clipImageEmbedding", (_, jpegImageData: Uint8Array) =>
clipImageEmbedding(jpegImageData),
);
ipcMain.handle("computeCLIPTextEmbeddingIfAvailable", (_, text: string) =>
computeCLIPTextEmbeddingIfAvailable(text),
ipcMain.handle("clipTextEmbeddingIfAvailable", (_, text: string) =>
clipTextEmbeddingIfAvailable(text),
);
ipcMain.handle("detectFaces", (_, input: Float32Array) =>
detectFaces(input),
);
ipcMain.handle("computeFaceEmbeddings", (_, input: Float32Array) =>
computeFaceEmbeddings(input),
ipcMain.handle("faceEmbeddings", (_, input: Float32Array) =>
faceEmbeddings(input),
);
ipcMain.handle("legacyFaceCrop", (_, faceID: string) =>
legacyFaceCrop(faceID),
);
// - Upload

View file

@ -70,9 +70,8 @@ const logInfo = (...params: unknown[]) => {
const message = params
.map((p) => (typeof p == "string" ? p : util.inspect(p)))
.join(" ");
const m = `[info] ${message}`;
if (isDev) console.log(m);
log.info(`[main] ${m}`);
log.info(`[main] ${message}`);
if (isDev) console.log(`[info] ${message}`);
};
const logDebug = (param: () => unknown) => {

View file

@ -163,7 +163,7 @@ const checkForUpdatesAndNotify = async (mainWindow: BrowserWindow) => {
};
/**
* Return the version of the desktop app.
* Return the version of the desktop app
*
* The return value is of the form `v1.2.3`.
*/

View file

@ -1,5 +1,7 @@
import { shell } from "electron/common";
import { app, dialog } from "electron/main";
import { existsSync } from "fs";
import fs from "node:fs/promises";
import path from "node:path";
import { posixPath } from "../utils/electron";
@ -51,6 +53,14 @@ export const openLogDirectory = () => openDirectory(logDirectoryPath());
* "userData" directory. This is the **primary** place applications are meant to
* store user's data, e.g. various configuration files and saved state.
*
* During development, our app name is "Electron", so this'd be, for example,
* `~/Library/Application Support/Electron` if we run using `yarn dev`. For the
* packaged production app, our app name is "ente", so this would be:
*
* - Windows: `%APPDATA%\ente`, e.g. `C:\Users\<username>\AppData\Local\ente`
* - Linux: `~/.config/ente`
* - macOS: `~/Library/Application Support/ente`
*
* Note that Chromium also stores the browser state, e.g. localStorage or disk
* caches, in userData.
*
@ -63,7 +73,21 @@ export const openLogDirectory = () => openDirectory(logDirectoryPath());
* "ente.log", it can be found at:
*
* - macOS: ~/Library/Logs/ente/ente.log (production)
* - macOS: ~/Library/Logs/Electron/ente.log (dev)
* - Linux: ~/.config/ente/logs/ente.log
* - Windows: %USERPROFILE%\AppData\Roaming\ente\logs\ente.log
*/
const logDirectoryPath = () => app.getPath("logs");
/**
* See: [Note: Legacy face crops]
*/
export const legacyFaceCrop = async (
faceID: string,
): Promise<Uint8Array | undefined> => {
// See: [Note: Getting the cache path]
// @ts-expect-error "cache" works but is not part of the public API.
const cacheDir = path.join(app.getPath("cache"), "ente");
const filePath = path.join(cacheDir, "face-crops", faceID);
return existsSync(filePath) ? await fs.readFile(filePath) : undefined;
};

View file

@ -3,6 +3,7 @@
import fs from "node:fs/promises";
import path from "node:path";
import { CustomErrorMessage, type ZipItem } from "../../types/ipc";
import log from "../log";
import { execAsync, isDev } from "../utils/electron";
import {
deleteTempFileIgnoringErrors,
@ -92,6 +93,9 @@ export const generateImageThumbnail = async (
let thumbnail: Uint8Array;
do {
await execAsync(command);
// TODO(MR): release 1.7
// TODO(MR): imagemagick debugging. Remove me after verifying logs.
log.info(`Generated thumbnail using ${command.join(" ")}`);
thumbnail = new Uint8Array(await fs.readFile(outputFilePath));
quality -= 10;
command = generateImageThumbnailCommand(

View file

@ -11,7 +11,7 @@ import * as ort from "onnxruntime-node";
import Tokenizer from "../../thirdparty/clip-bpe-ts/mod";
import log from "../log";
import { writeStream } from "../stream";
import { ensure, wait } from "../utils/common";
import { ensure } from "../utils/common";
import { deleteTempFile, makeTempFilePath } from "../utils/temp";
import { makeCachedInferenceSession } from "./ml";
@ -20,7 +20,7 @@ const cachedCLIPImageSession = makeCachedInferenceSession(
351468764 /* 335.2 MB */,
);
export const computeCLIPImageEmbedding = async (jpegImageData: Uint8Array) => {
export const clipImageEmbedding = async (jpegImageData: Uint8Array) => {
const tempFilePath = await makeTempFilePath();
const imageStream = new Response(jpegImageData.buffer).body;
await writeStream(tempFilePath, ensure(imageStream));
@ -42,7 +42,7 @@ const clipImageEmbedding_ = async (jpegFilePath: string) => {
const results = await session.run(feeds);
log.debug(
() =>
`ONNX/CLIP image embedding took ${Date.now() - t1} ms (prep: ${t2 - t1} ms, inference: ${Date.now() - t2} ms)`,
`onnx/clip image embedding took ${Date.now() - t1} ms (prep: ${t2 - t1} ms, inference: ${Date.now() - t2} ms)`,
);
/* Need these model specific casts to type the result */
const imageEmbedding = ensure(results.output).data as Float32Array;
@ -140,23 +140,21 @@ const getTokenizer = () => {
return _tokenizer;
};
export const computeCLIPTextEmbeddingIfAvailable = async (text: string) => {
const sessionOrSkip = await Promise.race([
export const clipTextEmbeddingIfAvailable = async (text: string) => {
const sessionOrStatus = await Promise.race([
cachedCLIPTextSession(),
// Wait for a tick to get the session promise to resolved the first time
// this code runs on each app start (and the model has been downloaded).
wait(0).then(() => 1),
"downloading-model",
]);
// Don't wait for the download to complete.
if (typeof sessionOrSkip == "number") {
// Don't wait for the download to complete
if (typeof sessionOrStatus == "string") {
log.info(
"Ignoring CLIP text embedding request because model download is pending",
);
return undefined;
}
const session = sessionOrSkip;
const session = sessionOrStatus;
const t1 = Date.now();
const tokenizer = getTokenizer();
const tokenizedText = Int32Array.from(tokenizer.encodeForCLIP(text));
@ -167,7 +165,7 @@ export const computeCLIPTextEmbeddingIfAvailable = async (text: string) => {
const results = await session.run(feeds);
log.debug(
() =>
`ONNX/CLIP text embedding took ${Date.now() - t1} ms (prep: ${t2 - t1} ms, inference: ${Date.now() - t2} ms)`,
`onnx/clip text embedding took ${Date.now() - t1} ms (prep: ${t2 - t1} ms, inference: ${Date.now() - t2} ms)`,
);
const textEmbedding = ensure(results.output).data as Float32Array;
return normalizeEmbedding(textEmbedding);

View file

@ -23,7 +23,7 @@ export const detectFaces = async (input: Float32Array) => {
input: new ort.Tensor("float32", input, [1, 3, 640, 640]),
};
const results = await session.run(feeds);
log.debug(() => `ONNX/YOLO face detection took ${Date.now() - t} ms`);
log.debug(() => `onnx/yolo face detection took ${Date.now() - t} ms`);
return ensure(results.output).data;
};
@ -32,7 +32,7 @@ const cachedFaceEmbeddingSession = makeCachedInferenceSession(
5286998 /* 5 MB */,
);
export const computeFaceEmbeddings = async (input: Float32Array) => {
export const faceEmbeddings = async (input: Float32Array) => {
// Dimension of each face (alias)
const mobileFaceNetFaceSize = 112;
// Smaller alias
@ -45,7 +45,7 @@ export const computeFaceEmbeddings = async (input: Float32Array) => {
const t = Date.now();
const feeds = { img_inputs: inputTensor };
const results = await session.run(feeds);
log.debug(() => `ONNX/MFNT face embedding took ${Date.now() - t} ms`);
log.debug(() => `onnx/yolo face embedding took ${Date.now() - t} ms`);
/* Need these model specific casts to extract and type the result */
return (results.embeddings as unknown as Record<string, unknown>)
.cpuData as Float32Array;

View file

@ -18,7 +18,10 @@ export const clearStores = () => {
* [Note: Safe storage keys]
*
* On macOS, `safeStorage` stores our data under a Keychain entry named
* "<app-name> Safe Storage". In our case, "ente Safe Storage".
* "<app-name> Safe Storage". Which resolves to:
*
* - Electron Safe Storage (dev)
* - ente Safe Storage (prod)
*/
export const saveEncryptionKey = (encryptionKey: string) => {
const encryptedKey = safeStorage.encryptString(encryptionKey);

View file

@ -106,7 +106,7 @@ const handleRead = async (path: string) => {
res.headers.set("Content-Length", `${fileSize}`);
// Add the file's last modified time (as epoch milliseconds).
const mtimeMs = stat.mtime.getTime();
const mtimeMs = stat.mtimeMs;
res.headers.set("X-Last-Modified-Ms", `${mtimeMs}`);
}
return res;
@ -132,13 +132,6 @@ const handleReadZip = async (zipPath: string, entryName: string) => {
// Close the zip handle when the underlying stream closes.
stream.on("end", () => void zip.close());
// While it is documented that entry.time is the modification time,
// the units are not mentioned. By seeing the source code, we can
// verify that it is indeed epoch milliseconds. See `parseZipTime`
// in the node-stream-zip source,
// https://github.com/antelle/node-stream-zip/blob/master/node_stream_zip.js
const modifiedMs = entry.time;
return new Response(webReadableStream, {
headers: {
// We don't know the exact type, but it doesn't really matter, just
@ -146,7 +139,12 @@ const handleReadZip = async (zipPath: string, entryName: string) => {
// doesn't tinker with it thinking of it as text.
"Content-Type": "application/octet-stream",
"Content-Length": `${entry.size}`,
"X-Last-Modified-Ms": `${modifiedMs}`,
// While it is documented that entry.time is the modification time,
// the units are not mentioned. By seeing the source code, we can
// verify that it is indeed epoch milliseconds. See `parseZipTime`
// in the node-stream-zip source,
// https://github.com/antelle/node-stream-zip/blob/master/node_stream_zip.js
"X-Last-Modified-Ms": `${entry.time}`,
},
});
};

View file

@ -13,12 +13,3 @@ export const ensure = <T>(v: T | null | undefined): T => {
if (v === undefined) throw new Error("Required value was not found");
return v;
};
/**
* Wait for {@link ms} milliseconds
*
* This function is a promisified `setTimeout`. It returns a promise that
* resolves after {@link ms} milliseconds.
*/
export const wait = (ms: number) =>
new Promise((resolve) => setTimeout(resolve, ms));

View file

@ -55,7 +55,9 @@ export const execAsync = async (command: string | string[]) => {
: command;
const startTime = Date.now();
const result = await execAsync_(escapedCommand);
log.debug(() => `${escapedCommand} (${Date.now() - startTime} ms)`);
log.debug(
() => `${escapedCommand} (${Math.round(Date.now() - startTime)} ms)`,
);
return result;
};

View file

@ -65,7 +65,7 @@ const selectDirectory = () => ipcRenderer.invoke("selectDirectory");
const logout = () => {
watchRemoveListeners();
return ipcRenderer.invoke("logout");
ipcRenderer.send("logout");
};
const encryptionKey = () => ipcRenderer.invoke("encryptionKey");
@ -153,17 +153,20 @@ const ffmpegExec = (
// - ML
const computeCLIPImageEmbedding = (jpegImageData: Uint8Array) =>
ipcRenderer.invoke("computeCLIPImageEmbedding", jpegImageData);
const clipImageEmbedding = (jpegImageData: Uint8Array) =>
ipcRenderer.invoke("clipImageEmbedding", jpegImageData);
const computeCLIPTextEmbeddingIfAvailable = (text: string) =>
ipcRenderer.invoke("computeCLIPTextEmbeddingIfAvailable", text);
const clipTextEmbeddingIfAvailable = (text: string) =>
ipcRenderer.invoke("clipTextEmbeddingIfAvailable", text);
const detectFaces = (input: Float32Array) =>
ipcRenderer.invoke("detectFaces", input);
const computeFaceEmbeddings = (input: Float32Array) =>
ipcRenderer.invoke("computeFaceEmbeddings", input);
const faceEmbeddings = (input: Float32Array) =>
ipcRenderer.invoke("faceEmbeddings", input);
const legacyFaceCrop = (faceID: string) =>
ipcRenderer.invoke("legacyFaceCrop", faceID);
// - Watch
@ -337,10 +340,11 @@ contextBridge.exposeInMainWorld("electron", {
// - ML
computeCLIPImageEmbedding,
computeCLIPTextEmbeddingIfAvailable,
clipImageEmbedding,
clipTextEmbeddingIfAvailable,
detectFaces,
computeFaceEmbeddings,
faceEmbeddings,
legacyFaceCrop,
// - Watch

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.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==
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==
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.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==
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==
dependencies:
builder-util-runtime "9.2.4"
builder-util-runtime "9.2.3"
fs-extra "^10.1.0"
js-yaml "^4.1.0"
lazy-val "^1.0.5"

View file

@ -163,10 +163,6 @@ 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,5 +7,4 @@ 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

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

View file

@ -78,23 +78,3 @@ 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

@ -46,7 +46,7 @@ You can alternatively install the build from PlayStore or F-Droid.
## 🧑‍💻 Building from source
1. [Install Flutter v3.22.0](https://flutter.dev/docs/get-started/install).
1. [Install Flutter v3.19.3](https://flutter.dev/docs/get-started/install).
2. Pull in all submodules with `git submodule update --init --recursive`

View file

@ -43,7 +43,7 @@ android {
defaultConfig {
applicationId "io.ente.photos"
minSdkVersion 26
minSdkVersion 24
targetSdkVersion 33
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
@ -70,10 +70,6 @@ android {
dimension "default"
applicationIdSuffix ".dev"
}
face {
dimension "default"
applicationIdSuffix ".face"
}
playstore {
dimension "default"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View file

@ -1,10 +0,0 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="io.ente.photos">
<!-- Flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
</manifest>

View file

@ -1,4 +0,0 @@
<resources>
<string name="app_name">ente face</string>
<string name="backup">backup face</string>
</resources>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 MiB

View file

@ -0,0 +1,91 @@
unknown
person
bicycle
car
motorcycle
airplane
bus
train
truck
boat
traffic light
fire hydrant
unknown
stop sign
parking meter
bench
bird
cat
dog
horse
sheep
cow
elephant
bear
zebra
giraffe
unknown
backpack
umbrella
unknown
unknown
handbag
tie
suitcase
frisbee
skis
snowboard
sports ball
kite
baseball bat
baseball glove
skateboard
surfboard
tennis racket
bottle
unknown
wine glass
cup
fork
knife
spoon
bowl
banana
apple
sandwich
orange
broccoli
carrot
hot dog
pizza
donut
cake
chair
couch
potted plant
bed
unknown
dining table
unknown
unknown
toilet
unknown
tv
laptop
mouse
remote
keyboard
cell phone
microwave
oven
toaster
sink
refrigerator
unknown
book
clock
vase
scissors
teddy bear
hair drier
toothbrush

Binary file not shown.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,30 @@
waterfall
snow
landscape
underwater
architecture
sunset / sunrise
blue sky
cloudy sky
greenery
autumn leaves
portrait
flower
night shot
stage concert
fireworks
candle light
neon lights
indoor
backlight
text documents
qr images
group portrait
computer screens
kids
dog
cat
macro
food
beach
mountain

Binary file not shown.

View file

@ -0,0 +1,4 @@
flutter_launcher_icons:
android: "launcher_icon"
ios: true
image_path: "assets/launcher_icon/ente-icon-dev.png"

View file

@ -6,8 +6,6 @@ PODS:
- connectivity_plus (0.0.1):
- Flutter
- FlutterMacOS
- dart_ui_isolate (0.0.1):
- Flutter
- device_info_plus (0.0.1):
- Flutter
- file_saver (0.0.1):
@ -228,7 +226,6 @@ DEPENDENCIES:
- background_fetch (from `.symlinks/plugins/background_fetch/ios`)
- battery_info (from `.symlinks/plugins/battery_info/ios`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`)
- dart_ui_isolate (from `.symlinks/plugins/dart_ui_isolate/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- file_saver (from `.symlinks/plugins/file_saver/ios`)
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
@ -305,8 +302,6 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/battery_info/ios"
connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/darwin"
dart_ui_isolate:
:path: ".symlinks/plugins/dart_ui_isolate/ios"
device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios"
file_saver:
@ -402,7 +397,6 @@ SPEC CHECKSUMS:
background_fetch: 2319bf7e18237b4b269430b7f14d177c0df09c5a
battery_info: 09f5c9ee65394f2291c8c6227bedff345b8a730c
connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db
dart_ui_isolate: d5bcda83ca4b04f129d70eb90110b7a567aece14
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
Firebase: 91fefd38712feb9186ea8996af6cbdef41473442
@ -427,7 +421,7 @@ SPEC CHECKSUMS:
home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57
image_editor_common: d6f6644ae4a6de80481e89fe6d0a8c49e30b4b43
in_app_purchase_storekit: 0e4b3c2e43ba1e1281f4f46dd71b0593ce529892
integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4
integration_test: 13825b8a9334a850581300559b8839134b124670
libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009
local_auth_darwin: c7e464000a6a89e952235699e32b329457608d98
local_auth_ios: 5046a18c018dd973247a0564496c8898dbb5adf9

View file

@ -293,7 +293,6 @@
"${BUILT_PRODUCTS_DIR}/background_fetch/background_fetch.framework",
"${BUILT_PRODUCTS_DIR}/battery_info/battery_info.framework",
"${BUILT_PRODUCTS_DIR}/connectivity_plus/connectivity_plus.framework",
"${BUILT_PRODUCTS_DIR}/dart_ui_isolate/dart_ui_isolate.framework",
"${BUILT_PRODUCTS_DIR}/device_info_plus/device_info_plus.framework",
"${BUILT_PRODUCTS_DIR}/file_saver/file_saver.framework",
"${BUILT_PRODUCTS_DIR}/fk_user_agent/fk_user_agent.framework",
@ -375,7 +374,6 @@
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/background_fetch.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/battery_info.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/connectivity_plus.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/dart_ui_isolate.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/device_info_plus.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_saver.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/fk_user_agent.framework",

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View file

@ -0,0 +1 @@
{"images":[{"size":"20x20","idiom":"iphone","filename":"AppIcon-dev-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"AppIcon-dev-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"AppIcon-dev-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"AppIcon-dev-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"AppIcon-dev-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"AppIcon-dev-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"AppIcon-dev-40x40@3x.png","scale":"3x"},{"size":"50x50","idiom":"ipad","filename":"AppIcon-dev-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"AppIcon-dev-50x50@2x.png","scale":"2x"},{"size":"57x57","idiom":"iphone","filename":"AppIcon-dev-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"AppIcon-dev-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"AppIcon-dev-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"AppIcon-dev-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"AppIcon-dev-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"AppIcon-dev-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"AppIcon-dev-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"AppIcon-dev-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"AppIcon-dev-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"AppIcon-dev-40x40@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"AppIcon-dev-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"AppIcon-dev-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"AppIcon-dev-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"AppIcon-dev-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"AppIcon-dev-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"AppIcon-dev-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}}

View file

@ -65,9 +65,9 @@
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>FLTEnableImpeller</key>
<true />
<false />
<key>FLTEnableWideGamut</key>
<true/>
<false/>
<key>NSFaceIDUsageDescription</key>
<string>Please allow ente to lock itself with FaceID or TouchID</string>
<key>NSCameraUsageDescription</key>

View file

@ -19,7 +19,6 @@ import 'package:photos/db/upload_locks_db.dart';
import "package:photos/events/endpoint_updated_event.dart";
import 'package:photos/events/signed_in_event.dart';
import 'package:photos/events/user_logged_out_event.dart';
import "package:photos/face/db.dart";
import 'package:photos/models/key_attributes.dart';
import 'package:photos/models/key_gen_result.dart';
import 'package:photos/models/private_key_attributes.dart';
@ -35,10 +34,10 @@ import 'package:photos/services/sync_service.dart';
import 'package:photos/utils/crypto_util.dart';
import 'package:photos/utils/file_uploader.dart';
import 'package:photos/utils/validator_util.dart';
import "package:photos/utils/wakelock_util.dart";
import 'package:shared_preferences/shared_preferences.dart';
import "package:tuple/tuple.dart";
import 'package:uuid/uuid.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
class Configuration {
Configuration._privateConstructor();
@ -188,7 +187,6 @@ class Configuration {
: null;
await CollectionsDB.instance.clearTable();
await MemoriesDB.instance.clearTable();
await FaceMLDataDB.instance.clearTable();
await UploadLocksDB.instance.clearTable();
await IgnoredFilesService.instance.reset();
@ -585,7 +583,7 @@ class Configuration {
Future<void> setShouldKeepDeviceAwake(bool value) async {
await _preferences.setBool(keyShouldKeepDeviceAwake, value);
await EnteWakeLock.toggle(enable: value);
await WakelockPlus.toggle(enable: value);
}
Future<void> setShouldBackupVideos(bool value) async {

View file

@ -69,8 +69,6 @@ const galleryGridSpacing = 2.0;
const kSearchSectionLimit = 9;
const maxPickAssetLimit = 50;
const iOSGroupID = "group.io.ente.frame.SlideshowWidget";
const blackThumbnailBase64 = '/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEB'
@ -101,9 +99,6 @@ const blackThumbnailBase64 = '/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEB'
'AKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAo' +
'AKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgD/9k=';
const localFileServer =
String.fromEnvironment("localFileServer", defaultValue: "");
const uploadTempFilePrefix = "upload_file_";
final tempDirCleanUpInterval = kDebugMode
? const Duration(seconds: 30).inMicroseconds

View file

@ -54,7 +54,7 @@ class EmbeddingsDB {
Future<void> clearTable() async {
final db = await _database;
await db.execute('DELETE FROM $tableName');
await db.execute('DELETE * FROM $tableName');
}
Future<List<Embedding>> getAll(Model model) async {

View file

@ -9,7 +9,7 @@ extension EntitiesDB on FilesDB {
List<LocalEntityData> data, {
ConflictAlgorithm conflictAlgorithm = ConflictAlgorithm.replace,
}) async {
debugPrint("entitiesDB: upsertEntities ${data.length} entities");
debugPrint("Inserting missing PathIDToLocalIDMapping");
final db = await database;
var batch = db.batch();
int batchCounter = 0;
@ -62,17 +62,4 @@ extension EntitiesDB on FilesDB {
return LocalEntityData.fromJson(maps[i]);
});
}
Future<LocalEntityData?> getEntity(EntityType type, String id) async {
final db = await database;
final List<Map<String, dynamic>> maps = await db.query(
"entities",
where: "type = ? AND id = ?",
whereArgs: [type.typeToString(), id],
);
if (maps.isEmpty) {
return null;
}
return LocalEntityData.fromJson(maps.first);
}
}

View file

@ -491,18 +491,6 @@ class FilesDB {
return convertToFiles(results)[0];
}
Future<EnteFile?> getAnyUploadedFile(int uploadedID) async {
final db = await instance.sqliteAsyncDB;
final results = await db.getAll(
'SELECT * FROM $filesTable WHERE $columnUploadedFileID = ?',
[uploadedID],
);
if (results.isEmpty) {
return null;
}
return convertToFiles(results)[0];
}
Future<Set<int>> getUploadedFileIDs(int collectionID) async {
final db = await instance.sqliteAsyncDB;
final results = await db.getAll(
@ -695,17 +683,6 @@ class FilesDB {
return files;
}
Future<List<EnteFile>> getAllFilesFromCollections(
Iterable<int> collectionID,
) async {
final db = await instance.sqliteAsyncDB;
final String sql =
'SELECT * FROM $filesTable WHERE $columnCollectionID IN (${collectionID.join(',')})';
final results = await db.getAll(sql);
final files = convertToFiles(results);
return files;
}
Future<List<EnteFile>> getNewFilesInCollection(
int collectionID,
int addedTime,
@ -1327,23 +1304,6 @@ class FilesDB {
return result;
}
Future<Map<int, int>> getFileIDToCreationTime() async {
final db = await instance.sqliteAsyncDB;
final rows = await db.getAll(
'''
SELECT $columnUploadedFileID, $columnCreationTime
FROM $filesTable
WHERE
($columnUploadedFileID IS NOT NULL AND $columnUploadedFileID IS NOT -1);
''',
);
final result = <int, int>{};
for (final row in rows) {
result[row[columnUploadedFileID] as int] = row[columnCreationTime] as int;
}
return result;
}
// getCollectionFileFirstOrLast returns the first or last uploaded file in
// the collection based on the given collectionID and the order.
Future<EnteFile?> getCollectionFileFirstOrLast(
@ -1683,14 +1643,13 @@ class FilesDB {
}
Future<List<int>> getOwnedFileIDs(int ownerID) async {
final db = await instance.sqliteAsyncDB;
final results = await db.getAll(
'''
SELECT DISTINCT $columnUploadedFileID FROM $filesTable
WHERE ($columnOwnerID = ? AND $columnUploadedFileID IS NOT NULL AND
$columnUploadedFileID IS NOT -1)
''',
[ownerID],
final db = await instance.database;
final results = await db.query(
filesTable,
columns: [columnUploadedFileID],
where:
'($columnOwnerID = $ownerID AND $columnUploadedFileID IS NOT NULL AND $columnUploadedFileID IS NOT -1)',
distinct: true,
);
final ids = <int>[];
for (final result in results) {
@ -1700,17 +1659,16 @@ class FilesDB {
}
Future<List<EnteFile>> getUploadedFiles(List<int> uploadedIDs) async {
final db = await instance.sqliteAsyncDB;
final db = await instance.database;
String inParam = "";
for (final id in uploadedIDs) {
inParam += "'" + id.toString() + "',";
}
inParam = inParam.substring(0, inParam.length - 1);
final results = await db.getAll(
'''
SELECT * FROM $filesTable WHERE $columnUploadedFileID IN ($inParam)
GROUP BY $columnUploadedFileID
''',
final results = await db.query(
filesTable,
where: '$columnUploadedFileID IN ($inParam)',
groupBy: columnUploadedFileID,
);
if (results.isEmpty) {
return <EnteFile>[];

View file

@ -26,6 +26,4 @@ enum EventType {
hide,
unhide,
coverChanged,
peopleChanged,
peopleClusterChanged,
}

View file

@ -1,22 +0,0 @@
import "package:photos/events/event.dart";
import "package:photos/models/file/file.dart";
class PeopleChangedEvent extends Event {
final List<EnteFile>? relevantFiles;
final PeopleEventType type;
final String source;
PeopleChangedEvent({
this.relevantFiles,
this.type = PeopleEventType.defaultType,
this.source = "",
});
@override
String get reason => '$runtimeType{type: ${type.name}, "via": $source}';
}
enum PeopleEventType {
defaultType,
removedFilesFromCluster,
}

View file

@ -1,193 +0,0 @@
import 'dart:math' as math show sin, cos, atan2, sqrt, pow;
import 'package:ml_linalg/linalg.dart';
extension SetVectorValues on Vector {
Vector setValues(int start, int end, Iterable<double> values) {
if (values.length > length) {
throw Exception('Values cannot be larger than vector');
} else if (end - start != values.length) {
throw Exception('Values must be same length as range');
} else if (start < 0 || end > length) {
throw Exception('Range must be within vector');
}
final tempList = toList();
tempList.replaceRange(start, end, values);
final newVector = Vector.fromList(tempList);
return newVector;
}
}
extension SetMatrixValues on Matrix {
Matrix setSubMatrix(
int startRow,
int endRow,
int startColumn,
int endColumn,
Iterable<Iterable<double>> values,
) {
if (values.length > rowCount) {
throw Exception('New values cannot have more rows than original matrix');
} else if (values.elementAt(0).length > columnCount) {
throw Exception(
'New values cannot have more columns than original matrix',
);
} else if (endRow - startRow != values.length) {
throw Exception('Values (number of rows) must be same length as range');
} else if (endColumn - startColumn != values.elementAt(0).length) {
throw Exception(
'Values (number of columns) must be same length as range',
);
} else if (startRow < 0 ||
endRow > rowCount ||
startColumn < 0 ||
endColumn > columnCount) {
throw Exception('Range must be within matrix');
}
final tempList = asFlattenedList
.toList(); // You need `.toList()` here to make sure the list is growable, otherwise `replaceRange` will throw an error
for (var i = startRow; i < endRow; i++) {
tempList.replaceRange(
i * columnCount + startColumn,
i * columnCount + endColumn,
values.elementAt(i).toList(),
);
}
final newMatrix = Matrix.fromFlattenedList(tempList, rowCount, columnCount);
return newMatrix;
}
Matrix setValues(
int startRow,
int endRow,
int startColumn,
int endColumn,
Iterable<double> values,
) {
if ((startRow - endRow) * (startColumn - endColumn) != values.length) {
throw Exception('Values must be same length as range');
} else if (startRow < 0 ||
endRow > rowCount ||
startColumn < 0 ||
endColumn > columnCount) {
throw Exception('Range must be within matrix');
}
final tempList = asFlattenedList
.toList(); // You need `.toList()` here to make sure the list is growable, otherwise `replaceRange` will throw an error
var index = 0;
for (var i = startRow; i < endRow; i++) {
for (var j = startColumn; j < endColumn; j++) {
tempList[i * columnCount + j] = values.elementAt(index);
index++;
}
}
final newMatrix = Matrix.fromFlattenedList(tempList, rowCount, columnCount);
return newMatrix;
}
Matrix setValue(int row, int column, double value) {
if (row < 0 || row > rowCount || column < 0 || column > columnCount) {
throw Exception('Index must be within range of matrix');
}
final tempList = asFlattenedList;
tempList[row * columnCount + column] = value;
final newMatrix = Matrix.fromFlattenedList(tempList, rowCount, columnCount);
return newMatrix;
}
Matrix appendRow(List<double> row) {
final oldNumberOfRows = rowCount;
final oldNumberOfColumns = columnCount;
if (row.length != oldNumberOfColumns) {
throw Exception('Row must have same number of columns as matrix');
}
final flatListMatrix = asFlattenedList;
flatListMatrix.addAll(row);
return Matrix.fromFlattenedList(
flatListMatrix,
oldNumberOfRows + 1,
oldNumberOfColumns,
);
}
}
extension MatrixCalculations on Matrix {
double determinant() {
final int length = rowCount;
if (length != columnCount) {
throw Exception('Matrix must be square');
}
if (length == 1) {
return this[0][0];
} else if (length == 2) {
return this[0][0] * this[1][1] - this[0][1] * this[1][0];
} else {
throw Exception('Determinant for Matrix larger than 2x2 not implemented');
}
}
/// Computes the singular value decomposition of a matrix, using https://lucidar.me/en/mathematics/singular-value-decomposition-of-a-2x2-matrix/ as reference, but with slightly different signs for the second columns of U and V
Map<String, dynamic> svd() {
if (rowCount != 2 || columnCount != 2) {
throw Exception('Matrix must be 2x2');
}
final a = this[0][0];
final b = this[0][1];
final c = this[1][0];
final d = this[1][1];
// Computation of U matrix
final tempCalc = a * a + b * b - c * c - d * d;
final theta = 0.5 * math.atan2(2 * a * c + 2 * b * d, tempCalc);
final U = Matrix.fromList([
[math.cos(theta), math.sin(theta)],
[math.sin(theta), -math.cos(theta)],
]);
// Computation of S matrix
// ignore: non_constant_identifier_names
final S1 = a * a + b * b + c * c + d * d;
// ignore: non_constant_identifier_names
final S2 =
math.sqrt(math.pow(tempCalc, 2) + 4 * math.pow(a * c + b * d, 2));
final sigma1 = math.sqrt((S1 + S2) / 2);
final sigma2 = math.sqrt((S1 - S2) / 2);
final S = Vector.fromList([sigma1, sigma2]);
// Computation of V matrix
final tempCalc2 = a * a - b * b + c * c - d * d;
final phi = 0.5 * math.atan2(2 * a * b + 2 * c * d, tempCalc2);
final s11 = (a * math.cos(theta) + c * math.sin(theta)) * math.cos(phi) +
(b * math.cos(theta) + d * math.sin(theta)) * math.sin(phi);
final s22 = (a * math.sin(theta) - c * math.cos(theta)) * math.sin(phi) +
(-b * math.sin(theta) + d * math.cos(theta)) * math.cos(phi);
final V = Matrix.fromList([
[s11.sign * math.cos(phi), s22.sign * math.sin(phi)],
[s11.sign * math.sin(phi), -s22.sign * math.cos(phi)],
]);
return {
'U': U,
'S': S,
'V': V,
};
}
int matrixRank() {
final svdResult = svd();
final Vector S = svdResult['S']!;
final rank = S.toList().where((element) => element > 1e-10).length;
return rank;
}
}
extension TransformMatrix on Matrix {
List<List<double>> to2DList() {
final List<List<double>> outerList = [];
for (var i = 0; i < rowCount; i++) {
final innerList = this[i].toList();
outerList.add(innerList);
}
return outerList;
}
}

View file

@ -23,9 +23,4 @@ class EnteWatch extends Stopwatch {
reset();
previousElapsed = 0;
}
void stopWithLog(String msg) {
log(msg);
stop();
}
}

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