Compare commits
3 commits
main
...
mobile-vid
Author | SHA1 | Date | |
---|---|---|---|
![]() |
fae54faffc | ||
![]() |
d4f781bf35 | ||
![]() |
9c4e72aa0f |
14
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
2
.github/workflows/mobile-lint.yml
vendored
|
@ -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
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 801 KiB After Width: | Height: | Size: 38 KiB |
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -189,7 +189,7 @@ class _AppState extends State<App> with WindowListener, TrayListener {
|
|||
windowManager.show();
|
||||
break;
|
||||
case 'exit_app':
|
||||
windowManager.destroy();
|
||||
windowManager.close();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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} に接続しました"
|
||||
}
|
|
@ -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}"
|
||||
}
|
|
@ -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": "Будет полезно, если вы укажете, где нашли нас, так как мы не отслеживаем установки приложения"
|
||||
}
|
|
@ -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}"
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)}";
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 324 B After Width: | Height: | Size: 240 B |
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 780 B After Width: | Height: | Size: 628 B |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 7.6 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.1 KiB |
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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`.
|
||||
*/
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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}`,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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).
|
||||
|
|
|
@ -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`
|
||||
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
BIN
mobile/android/app/src/dev/res/mipmap-hdpi/launcher_icon.png
Normal file
After Width: | Height: | Size: 8.2 KiB |
BIN
mobile/android/app/src/dev/res/mipmap-mdpi/launcher_icon.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
BIN
mobile/android/app/src/dev/res/mipmap-xhdpi/launcher_icon.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
mobile/android/app/src/dev/res/mipmap-xxhdpi/launcher_icon.png
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
mobile/android/app/src/dev/res/mipmap-xxxhdpi/launcher_icon.png
Normal file
After Width: | Height: | Size: 38 KiB |
|
@ -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>
|
|
@ -1,4 +0,0 @@
|
|||
<resources>
|
||||
<string name="app_name">ente face</string>
|
||||
<string name="backup">backup face</string>
|
||||
</resources>
|
BIN
mobile/assets/launcher_icon/ente-icon-dev.png
Normal file
After Width: | Height: | Size: 1 MiB |
91
mobile/assets/models/cocossd/labels.txt
Normal 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
|
BIN
mobile/assets/models/cocossd/model.tflite
Normal file
1001
mobile/assets/models/mobilenet/labels_mobilenet_quant_v1_224.txt
Normal file
BIN
mobile/assets/models/mobilenet/mobilenet_v1_1.0_224_quant.tflite
Normal file
30
mobile/assets/models/scenes/labels.txt
Normal 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
|
BIN
mobile/assets/models/scenes/model.tflite
Normal file
4
mobile/flutter_launcher_icons-dev.yaml
Normal file
|
@ -0,0 +1,4 @@
|
|||
flutter_launcher_icons:
|
||||
android: "launcher_icon"
|
||||
ios: true
|
||||
image_path: "assets/launcher_icon/ente-icon-dev.png"
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
After Width: | Height: | Size: 1.1 MiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 6 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 5.7 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 9.2 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 4.7 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 5.6 KiB |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 34 KiB |
After Width: | Height: | Size: 8.2 KiB |
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 8.9 KiB |
After Width: | Height: | Size: 26 KiB |
After Width: | Height: | Size: 30 KiB |
|
@ -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"}}
|
|
@ -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>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>[];
|
||||
|
|
|
@ -26,6 +26,4 @@ enum EventType {
|
|||
hide,
|
||||
unhide,
|
||||
coverChanged,
|
||||
peopleChanged,
|
||||
peopleClusterChanged,
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -23,9 +23,4 @@ class EnteWatch extends Stopwatch {
|
|||
reset();
|
||||
previousElapsed = 0;
|
||||
}
|
||||
|
||||
void stopWithLog(String msg) {
|
||||
log(msg);
|
||||
stop();
|
||||
}
|
||||
}
|
||||
|
|