Compare commits
118 Commits
auth-v2.0.
...
photos-v0.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1800ad0a1f | ||
|
|
3230b9275e | ||
|
|
ce5627f04c | ||
|
|
8dd7c100af | ||
|
|
2e7dcc6bc2 | ||
|
|
0e1bdfe07e | ||
|
|
1e106d707f | ||
|
|
0053e814c8 | ||
|
|
53184da7fb | ||
|
|
165bcb5c6e | ||
|
|
d6316a1724 | ||
|
|
bc0a453cbc | ||
|
|
166e9ad1bf | ||
|
|
841921a732 | ||
|
|
769da989c4 | ||
|
|
3e917bd855 | ||
|
|
7f1730b56c | ||
|
|
25e762ba57 | ||
|
|
d5f294980e | ||
|
|
1e410a82f2 | ||
|
|
d013519655 | ||
|
|
3b3d314f9c | ||
|
|
855d362cca | ||
|
|
eced463f6f | ||
|
|
f8febe12df | ||
|
|
0c44d1b789 | ||
|
|
f74af4199d | ||
|
|
9b27cac465 | ||
|
|
7b94c32bbf | ||
|
|
881c94be05 | ||
|
|
7248a226bc | ||
|
|
8ae7ae2de9 | ||
|
|
28cf7d76d5 | ||
|
|
c2957238da | ||
|
|
f9a2ec774a | ||
|
|
548721e415 | ||
|
|
0568cd03c9 | ||
|
|
226f891e99 | ||
|
|
9643dd645f | ||
|
|
7e897815a1 | ||
|
|
a9b92b9bfa | ||
|
|
489de9f8c2 | ||
|
|
39bc68390f | ||
|
|
83dabfbdee | ||
|
|
35f2a6944e | ||
|
|
fbe2996dcc | ||
|
|
7f23b31bbc | ||
|
|
f43e260434 | ||
|
|
18698d35bb | ||
|
|
9e41b906a7 | ||
|
|
0f2181c09b | ||
|
|
707e14702e | ||
|
|
f3a0240f1d | ||
|
|
e84b989484 | ||
|
|
86e4cffb8e | ||
|
|
1d02fe4f32 | ||
|
|
e5edeae370 | ||
|
|
40a1da1ba7 | ||
|
|
5dfafa28c7 | ||
|
|
d3df6b31ae | ||
|
|
145850a66e | ||
|
|
6b56c28870 | ||
|
|
9ec68ecd3d | ||
|
|
8c127a6cec | ||
|
|
3890373d4a | ||
|
|
ee1eb75bdf | ||
|
|
14e99ea26a | ||
|
|
7183a8b493 | ||
|
|
e1ac5e7394 | ||
|
|
1391cff1f1 | ||
|
|
204d4d048e | ||
|
|
8dfd60df19 | ||
|
|
5810d2b762 | ||
|
|
f6abcafc83 | ||
|
|
7950f1ec26 | ||
|
|
6974672f8c | ||
|
|
25cedd5e2f | ||
|
|
a51a965fc8 | ||
|
|
fc5d8aeca6 | ||
|
|
03fc59a8fb | ||
|
|
80a27f7e6f | ||
|
|
cc2c8e3e26 | ||
|
|
d52f873c92 | ||
|
|
911cdd9448 | ||
|
|
b4699ecfcb | ||
|
|
ded151241f | ||
|
|
3dfc3a6dba | ||
|
|
08b7fe6a49 | ||
|
|
7ef59bb4cc | ||
|
|
049a240916 | ||
|
|
3fdf5f1e46 | ||
|
|
d92fd25a78 | ||
|
|
640b546d78 | ||
|
|
9e3fbce6c7 | ||
|
|
e89eb48214 | ||
|
|
9440b967c8 | ||
|
|
d6769fb1d5 | ||
|
|
df377ebcf3 | ||
|
|
0114775e22 | ||
|
|
31ffb124b6 | ||
|
|
f01583d168 | ||
|
|
f6dca2dfc9 | ||
|
|
45f9b47f24 | ||
|
|
cb7a2445e1 | ||
|
|
4830a69aad | ||
|
|
f8fbedfe10 | ||
|
|
d22cf34a0e | ||
|
|
ef250acad9 | ||
|
|
459c4515a0 | ||
|
|
056e29a5f5 | ||
|
|
71e87ef7e9 | ||
|
|
0d3662d9fe | ||
|
|
70e5e9b13c | ||
|
|
949780d1e8 | ||
|
|
a06a93e73d | ||
|
|
bc45db51a9 | ||
|
|
953824ca25 | ||
|
|
7c05069dbd |
BIN
.github/assets/github-badge.png
vendored
|
Before Width: | Height: | Size: 12 KiB |
2
.github/workflows/auth-release.yml
vendored
@@ -85,7 +85,7 @@ jobs:
|
||||
- name: Install dependencies for desktop build
|
||||
run: |
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y libsecret-1-dev libsodium-dev libwebkit2gtk-4.0-dev libfuse2 ninja-build libgtk-3-dev dpkg-dev pkg-config rpm libsqlite3-dev locate appindicator3-0.1 libappindicator3-dev
|
||||
sudo apt-get install -y libsecret-1-dev libsodium-dev libwebkit2gtk-4.0-dev libfuse2 ninja-build libgtk-3-dev dpkg-dev pkg-config rpm libsqlite3-dev locate appindicator3-0.1 libappindicator3-dev libffi7
|
||||
|
||||
- name: Install appimagetool
|
||||
run: |
|
||||
|
||||
@@ -70,7 +70,7 @@ existing users will be grandfathered in.
|
||||
[<img height="42" src=".github/assets/app-store-badge.svg">](https://apps.apple.com/app/id6444121398)
|
||||
[<img height="42" src=".github/assets/play-store-badge.png">](https://play.google.com/store/apps/details?id=io.ente.auth)
|
||||
[<img height="42" src=".github/assets/f-droid-badge.png">](https://f-droid.org/packages/io.ente.auth/)
|
||||
[<img height="42" src=".github/assets/github-badge.png">](https://github.com/ente-io/ente/releases?q=tag%3Aauth-v2)
|
||||
[<img height="42" src=".github/assets/desktop-badge.png">](https://github.com/ente-io/ente/releases?q=tag%3Aauth-v2)
|
||||
[<img height="42" src=".github/assets/web-badge.svg">](https://auth.ente.io)
|
||||
|
||||
</div>
|
||||
|
||||
@@ -31,14 +31,16 @@ You can alternatively install the build from PlayStore or F-Droid.
|
||||
<img height="59" src="../.github/assets/app-store-badge.svg">
|
||||
</a>
|
||||
|
||||
### Desktop
|
||||
|
||||
You can [**download**](https://github.com/ente-io/ente/releases?q=tag%3Aauth-v2)
|
||||
a native desktop app from this repository's GitHub releases. The desktop app
|
||||
works on Windows, Linux and macOS.
|
||||
|
||||
### Web
|
||||
|
||||
You can view your 2FA codes at [auth.ente.io](https://auth.ente.io). For adding
|
||||
or managing your secrets, please use our mobile app.
|
||||
|
||||
### Desktop
|
||||
|
||||
A native desktop app is coming soon!
|
||||
or managing your secrets, please use our mobile or desktop app.
|
||||
|
||||
## 🧑💻 Build from source
|
||||
|
||||
|
||||
@@ -36,7 +36,9 @@
|
||||
},
|
||||
{
|
||||
"title": "BorgBase",
|
||||
"altNames": ["borg"],
|
||||
"altNames": [
|
||||
"borg"
|
||||
],
|
||||
"slug": "BorgBase"
|
||||
},
|
||||
{
|
||||
@@ -54,6 +56,9 @@
|
||||
"slug": "cih",
|
||||
"hex": "D14633"
|
||||
},
|
||||
{
|
||||
"title": "ConfigCat"
|
||||
},
|
||||
{
|
||||
"title": "Cloudflare"
|
||||
},
|
||||
@@ -67,7 +72,9 @@
|
||||
},
|
||||
{
|
||||
"title": "DCS",
|
||||
"altNames": ["Digital Combat Simulator"],
|
||||
"altNames": [
|
||||
"Digital Combat Simulator"
|
||||
],
|
||||
"slug": "dcs"
|
||||
},
|
||||
{
|
||||
@@ -115,9 +122,14 @@
|
||||
},
|
||||
{
|
||||
"title": "Gosuslugi",
|
||||
"altNames": ["Госуслуги"],
|
||||
"altNames": [
|
||||
"Госуслуги"
|
||||
],
|
||||
"slug": "Gosuslugi"
|
||||
},
|
||||
{
|
||||
"title": "Habbo"
|
||||
},
|
||||
{
|
||||
"title": "Healthchecks.io",
|
||||
"slug": "healthchecks"
|
||||
@@ -180,13 +192,24 @@
|
||||
},
|
||||
{
|
||||
"title": "Mastodon",
|
||||
"altNames": ["mstdn", "fediscience", "mathstodon", "fosstodon"],
|
||||
"altNames": [
|
||||
"mstdn",
|
||||
"fediscience",
|
||||
"mathstodon",
|
||||
"fosstodon"
|
||||
],
|
||||
"slug": "mastodon",
|
||||
"hex": "6364FF"
|
||||
},
|
||||
{
|
||||
"title": "Mercado Livre",
|
||||
"slug": "mercado_livre"
|
||||
},
|
||||
{
|
||||
"title": "Murena",
|
||||
"altNames": ["eCloud"],
|
||||
"altNames": [
|
||||
"eCloud"
|
||||
],
|
||||
"slug": "ecloud"
|
||||
},
|
||||
{
|
||||
@@ -284,6 +307,9 @@
|
||||
"slug": "rust_language_forum",
|
||||
"hex": "000000"
|
||||
},
|
||||
{
|
||||
"title": "Sendgrid"
|
||||
},
|
||||
{
|
||||
"title": "service-bw"
|
||||
},
|
||||
@@ -374,13 +400,18 @@
|
||||
},
|
||||
{
|
||||
"title": "X",
|
||||
"altNames": ["twitter"],
|
||||
"altNames": [
|
||||
"twitter"
|
||||
],
|
||||
"slug": "x"
|
||||
},
|
||||
{
|
||||
"title": "Yandex",
|
||||
"altNames": ["Ya", "Яндекс"],
|
||||
"altNames": [
|
||||
"Ya",
|
||||
"Яндекс"
|
||||
],
|
||||
"slug": "Yandex"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
9
auth/assets/custom-icons/icons/configcat.svg
Normal file
|
After Width: | Height: | Size: 68 KiB |
9
auth/assets/custom-icons/icons/habbo.svg
Normal file
|
After Width: | Height: | Size: 26 KiB |
9
auth/assets/custom-icons/icons/mercado_livre.svg
Normal file
|
After Width: | Height: | Size: 57 KiB |
9
auth/assets/custom-icons/icons/sendgrid.svg
Normal file
|
After Width: | Height: | Size: 59 KiB |
@@ -46,7 +46,6 @@ class _AppState extends State<App> with WindowListener, TrayListener {
|
||||
|
||||
Future<void> initWindowManager() async {
|
||||
windowManager.addListener(this);
|
||||
await windowManager.setPreventClose(true);
|
||||
}
|
||||
|
||||
Future<void> initTrayManager() async {
|
||||
@@ -154,11 +153,6 @@ class _AppState extends State<App> with WindowListener, TrayListener {
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowClose() async {
|
||||
await windowManager.hide();
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowResize() {
|
||||
WindowListenerService.instance.onWindowResize().ignore();
|
||||
@@ -187,11 +181,16 @@ class _AppState extends State<App> with WindowListener, TrayListener {
|
||||
|
||||
@override
|
||||
void onTrayMenuItemClick(MenuItem menuItem) {
|
||||
if (menuItem.key == 'show_window') {
|
||||
windowManager.show();
|
||||
} else if (menuItem.key == 'exit_app') {
|
||||
windowManager.setPreventClose(false);
|
||||
windowManager.close();
|
||||
switch (menuItem.key) {
|
||||
case 'hide_window':
|
||||
windowManager.hide();
|
||||
break;
|
||||
case 'show_window':
|
||||
windowManager.show();
|
||||
break;
|
||||
case 'exit_app':
|
||||
windowManager.close();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,10 @@ Future<void> initSystemTray() async {
|
||||
await trayManager.setIcon(path);
|
||||
Menu menu = Menu(
|
||||
items: [
|
||||
MenuItem(
|
||||
key: 'hide_window',
|
||||
label: 'Hide Window',
|
||||
),
|
||||
MenuItem(
|
||||
key: 'show_window',
|
||||
label: 'Show Window',
|
||||
|
||||
@@ -17,8 +17,8 @@ import 'package:ente_auth/utils/dialog_util.dart';
|
||||
import 'package:ente_auth/utils/platform_util.dart';
|
||||
import 'package:ente_auth/utils/toast_util.dart';
|
||||
import 'package:ente_auth/utils/totp_util.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_context_menu/flutter_context_menu.dart';
|
||||
import 'package:flutter_slidable/flutter_slidable.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:move_to_background/move_to_background.dart';
|
||||
@@ -86,108 +86,122 @@ class _CodeWidgetState extends State<CodeWidget> {
|
||||
final l10n = context.l10n;
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(left: 16, right: 16, bottom: 8, top: 8),
|
||||
child: Slidable(
|
||||
key: ValueKey(widget.code.hashCode),
|
||||
endActionPane: ActionPane(
|
||||
extentRatio: 0.60,
|
||||
motion: const ScrollMotion(),
|
||||
children: [
|
||||
const SizedBox(
|
||||
width: 4,
|
||||
),
|
||||
SlidableAction(
|
||||
onPressed: _onShowQrPressed,
|
||||
backgroundColor: Colors.grey.withOpacity(0.1),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.inverseBackgroundColor,
|
||||
icon: Icons.qr_code_2_outlined,
|
||||
label: "QR",
|
||||
padding: const EdgeInsets.only(left: 4, right: 0),
|
||||
spacing: 8,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 4,
|
||||
),
|
||||
SlidableAction(
|
||||
onPressed: _onEditPressed,
|
||||
backgroundColor: Colors.grey.withOpacity(0.1),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.inverseBackgroundColor,
|
||||
icon: Icons.edit_outlined,
|
||||
label: l10n.edit,
|
||||
padding: const EdgeInsets.only(left: 4, right: 0),
|
||||
spacing: 8,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 4,
|
||||
),
|
||||
SlidableAction(
|
||||
onPressed: _onDeletePressed,
|
||||
backgroundColor: Colors.grey.withOpacity(0.1),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
|
||||
foregroundColor: const Color(0xFFFE4A49),
|
||||
icon: Icons.delete,
|
||||
label: l10n.delete,
|
||||
padding: const EdgeInsets.only(left: 0, right: 0),
|
||||
spacing: 8,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
return RawGestureDetector(
|
||||
gestures: {
|
||||
PanGestureRecognizer:
|
||||
GestureRecognizerFactoryWithHandlers<PanGestureRecognizer>(
|
||||
() => PanGestureRecognizer(
|
||||
debugOwner: this,
|
||||
// This recognizer accepts any button press made with a secondary button.
|
||||
allowedButtonsFilter: (int buttons) =>
|
||||
buttons & kSecondaryButton != 0,
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
if (PlatformUtil.isDesktop()) {
|
||||
return ContextMenuRegion(
|
||||
contextMenu: ContextMenu(
|
||||
entries: <ContextMenuEntry>[
|
||||
MenuItem(
|
||||
label: 'QR',
|
||||
icon: Icons.qr_code_2_outlined,
|
||||
onSelected: () => _onShowQrPressed(null),
|
||||
),
|
||||
(PanGestureRecognizer instance) {
|
||||
instance
|
||||
..dragStartBehavior = DragStartBehavior.down
|
||||
..onEnd = (DragEndDetails details) {
|
||||
Slidable.of(context)?.openEndActionPane();
|
||||
};
|
||||
},
|
||||
),
|
||||
},
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Container(
|
||||
color: Theme.of(context).colorScheme.codeCardBackgroundColor,
|
||||
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),
|
||||
),
|
||||
MenuItem(
|
||||
label: l10n.edit,
|
||||
icon: Icons.edit,
|
||||
onSelected: () => _onEditPressed(null),
|
||||
),
|
||||
),
|
||||
const MenuDivider(),
|
||||
MenuItem(
|
||||
label: l10n.delete,
|
||||
value: "Delete",
|
||||
icon: Icons.delete,
|
||||
onSelected: () => _onDeletePressed(null),
|
||||
),
|
||||
],
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
),
|
||||
child: _clippedCard(l10n),
|
||||
);
|
||||
},
|
||||
}
|
||||
|
||||
return Slidable(
|
||||
key: ValueKey(widget.code.hashCode),
|
||||
endActionPane: ActionPane(
|
||||
extentRatio: 0.60,
|
||||
motion: const ScrollMotion(),
|
||||
children: [
|
||||
const SizedBox(
|
||||
width: 4,
|
||||
),
|
||||
SlidableAction(
|
||||
onPressed: _onShowQrPressed,
|
||||
backgroundColor: Colors.grey.withOpacity(0.1),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.inverseBackgroundColor,
|
||||
icon: Icons.qr_code_2_outlined,
|
||||
label: "QR",
|
||||
padding: const EdgeInsets.only(left: 4, right: 0),
|
||||
spacing: 8,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 4,
|
||||
),
|
||||
SlidableAction(
|
||||
onPressed: _onEditPressed,
|
||||
backgroundColor: Colors.grey.withOpacity(0.1),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.inverseBackgroundColor,
|
||||
icon: Icons.edit_outlined,
|
||||
label: l10n.edit,
|
||||
padding: const EdgeInsets.only(left: 4, right: 0),
|
||||
spacing: 8,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 4,
|
||||
),
|
||||
SlidableAction(
|
||||
onPressed: _onDeletePressed,
|
||||
backgroundColor: Colors.grey.withOpacity(0.1),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
|
||||
foregroundColor: const Color(0xFFFE4A49),
|
||||
icon: Icons.delete,
|
||||
label: l10n.delete,
|
||||
padding: const EdgeInsets.only(left: 0, right: 0),
|
||||
spacing: 8,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Builder(
|
||||
builder: (context) => _clippedCard(l10n),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _clippedCard(AppLocalizations l10n) {
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Container(
|
||||
color: Theme.of(context).colorScheme.codeCardBackgroundColor,
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -23,4 +23,5 @@ startup_notify: false
|
||||
#
|
||||
# include:
|
||||
# - libcurl.so.4
|
||||
include: []
|
||||
include:
|
||||
- libffi.so.7
|
||||
|
||||
@@ -63,7 +63,7 @@ PODS:
|
||||
- sqlite3/fts5
|
||||
- sqlite3/perf-threadsafe
|
||||
- sqlite3/rtree
|
||||
- system_tray (0.0.1):
|
||||
- tray_manager (0.0.1):
|
||||
- FlutterMacOS
|
||||
- url_launcher_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
@@ -91,7 +91,7 @@ DEPENDENCIES:
|
||||
- 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`)
|
||||
- system_tray (from `Flutter/ephemeral/.symlinks/plugins/system_tray/macos`)
|
||||
- tray_manager (from `Flutter/ephemeral/.symlinks/plugins/tray_manager/macos`)
|
||||
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
||||
- window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`)
|
||||
|
||||
@@ -144,8 +144,8 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/sqflite/darwin
|
||||
sqlite3_flutter_libs:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos
|
||||
system_tray:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/system_tray/macos
|
||||
tray_manager:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/tray_manager/macos
|
||||
url_launcher_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
|
||||
window_manager:
|
||||
@@ -177,7 +177,7 @@ SPEC CHECKSUMS:
|
||||
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
||||
sqlite3: 73b7fc691fdc43277614250e04d183740cb15078
|
||||
sqlite3_flutter_libs: 06a05802529659a272beac4ee1350bfec294f386
|
||||
system_tray: e53c972838c69589ff2e77d6d3abfd71332f9e5d
|
||||
tray_manager: 9064e219c56d75c476e46b9a21182087930baf90
|
||||
url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95
|
||||
window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8
|
||||
|
||||
|
||||
@@ -347,6 +347,14 @@ packages:
|
||||
url: "https://github.com/ente-io/ente_crypto_dart.git"
|
||||
source: git
|
||||
version: "1.0.0"
|
||||
equatable:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: equatable
|
||||
sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.5"
|
||||
event_bus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -440,6 +448,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.1.5"
|
||||
flutter_context_menu:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_context_menu
|
||||
sha256: "9f220a8fa0290c68e38000d6d62a0dc4555d490c15a5bd856a6e6d255d81b8dc"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.3"
|
||||
flutter_displaymode:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name: ente_auth
|
||||
description: ente two-factor authenticator
|
||||
version: 2.0.48+248
|
||||
version: 2.0.50+250
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
@@ -41,6 +41,7 @@ dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_bloc: ^8.0.1
|
||||
flutter_context_menu: ^0.1.3
|
||||
flutter_displaymode: ^0.6.0
|
||||
flutter_email_sender: ^6.0.2
|
||||
flutter_inappwebview: ^6.0.0
|
||||
|
||||
@@ -18,6 +18,10 @@ type File struct {
|
||||
Info *FileInfo `json:"info,omitempty"`
|
||||
}
|
||||
|
||||
func (f File) IsRemovedFromAlbum() bool {
|
||||
return f.IsDeleted || f.File.EncryptedData == "-"
|
||||
}
|
||||
|
||||
// FileInfo has information about storage used by the file & it's metadata(future)
|
||||
type FileInfo struct {
|
||||
FileSize int64 `json:"fileSize,omitempty"`
|
||||
|
||||
@@ -98,7 +98,8 @@ func DecryptChaChaBase64(data string, key []byte, nonce string) (string, []byte,
|
||||
// Decode data from base64
|
||||
dataBytes, err := base64.StdEncoding.DecodeString(data)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("invalid data: %v", err)
|
||||
// safe to log the encrypted data
|
||||
return "", nil, fmt.Errorf("invalid base64 data %s: %v", data, err)
|
||||
}
|
||||
// Decode nonce from base64
|
||||
nonceBytes, err := base64.StdEncoding.DecodeString(nonce)
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
var AppVersion = "0.1.12"
|
||||
var AppVersion = "0.1.13"
|
||||
|
||||
func main() {
|
||||
cliDBPath, err := GetCLIConfigPath()
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/ente-io/cli/internal/api"
|
||||
eCrypto "github.com/ente-io/cli/internal/crypto"
|
||||
"github.com/ente-io/cli/pkg/model"
|
||||
@@ -41,7 +42,7 @@ func MapCollectionToAlbum(ctx context.Context, collection api.Collection, holder
|
||||
if collection.MagicMetadata != nil {
|
||||
_, encodedJsonBytes, err := eCrypto.DecryptChaChaBase64(collection.MagicMetadata.Data, collectionKey, collection.MagicMetadata.Header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("failed to decrypt magic metadata for collection %d: %w", collection.ID, err)
|
||||
}
|
||||
err = json.Unmarshal(encodedJsonBytes, &album.PrivateMeta)
|
||||
if err != nil {
|
||||
@@ -51,28 +52,28 @@ func MapCollectionToAlbum(ctx context.Context, collection api.Collection, holder
|
||||
if collection.PublicMagicMetadata != nil {
|
||||
_, encodedJsonBytes, err := eCrypto.DecryptChaChaBase64(collection.PublicMagicMetadata.Data, collectionKey, collection.PublicMagicMetadata.Header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("failed to decrypt public magic metadata for collection %d: %w", collection.ID, err)
|
||||
}
|
||||
err = json.Unmarshal(encodedJsonBytes, &album.PublicMeta)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("failed to unmarshal public magic metadata for collection %d: %w", collection.ID, err)
|
||||
}
|
||||
}
|
||||
if album.IsShared && collection.SharedMagicMetadata != nil {
|
||||
_, encodedJsonBytes, err := eCrypto.DecryptChaChaBase64(collection.SharedMagicMetadata.Data, collectionKey, collection.SharedMagicMetadata.Header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("failed to decrypt shared magic metadata for collection %d: %w", collection.ID, err)
|
||||
}
|
||||
err = json.Unmarshal(encodedJsonBytes, &album.SharedMeta)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("failed to unmarshal shared magic metadata for collection %d: %w", collection.ID, err)
|
||||
}
|
||||
}
|
||||
return &album, nil
|
||||
}
|
||||
|
||||
func MapApiFileToPhotoFile(ctx context.Context, album model.RemoteAlbum, file api.File, holder *secrets.KeyHolder) (*model.RemoteFile, error) {
|
||||
if file.IsDeleted {
|
||||
if file.IsRemovedFromAlbum() {
|
||||
return nil, errors.New("file is deleted")
|
||||
}
|
||||
albumKey := album.AlbumKey.MustDecrypt(holder.DeviceKey)
|
||||
@@ -99,7 +100,7 @@ func MapApiFileToPhotoFile(ctx context.Context, album model.RemoteAlbum, file ap
|
||||
if file.Metadata.DecryptionHeader != "" {
|
||||
_, encodedJsonBytes, err := eCrypto.DecryptChaChaBase64(file.Metadata.EncryptedData, fileKey, file.Metadata.DecryptionHeader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("failed to decrypt metadata for file %d: %w", file.ID, err)
|
||||
}
|
||||
err = json.Unmarshal(encodedJsonBytes, &photoFile.Metadata)
|
||||
if err != nil {
|
||||
@@ -109,7 +110,7 @@ func MapApiFileToPhotoFile(ctx context.Context, album model.RemoteAlbum, file ap
|
||||
if file.MagicMetadata != nil {
|
||||
_, encodedJsonBytes, err := eCrypto.DecryptChaChaBase64(file.MagicMetadata.Data, fileKey, file.MagicMetadata.Header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("failed to decrypt magic metadata for file %d: %w", file.ID, err)
|
||||
}
|
||||
err = json.Unmarshal(encodedJsonBytes, &photoFile.PrivateMetadata)
|
||||
if err != nil {
|
||||
@@ -119,7 +120,7 @@ func MapApiFileToPhotoFile(ctx context.Context, album model.RemoteAlbum, file ap
|
||||
if file.PubicMagicMetadata != nil {
|
||||
_, encodedJsonBytes, err := eCrypto.DecryptChaChaBase64(file.PubicMagicMetadata.Data, fileKey, file.PubicMagicMetadata.Header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("failed to decrypt public magic metadata for file %d: %w", file.ID, err)
|
||||
}
|
||||
err = json.Unmarshal(encodedJsonBytes, &photoFile.PublicMetadata)
|
||||
if err != nil {
|
||||
|
||||
@@ -87,16 +87,16 @@ func (c *ClICtrl) fetchRemoteFiles(ctx context.Context) error {
|
||||
if file.UpdationTime > maxUpdated {
|
||||
maxUpdated = file.UpdationTime
|
||||
}
|
||||
if isFirstSync && file.IsDeleted {
|
||||
if isFirstSync && file.IsRemovedFromAlbum() {
|
||||
// on first sync, no need to sync delete markers
|
||||
continue
|
||||
}
|
||||
albumEntry := model.AlbumFileEntry{AlbumID: album.ID, FileID: file.ID, IsDeleted: file.IsDeleted, SyncedLocally: false}
|
||||
albumEntry := model.AlbumFileEntry{AlbumID: album.ID, FileID: file.ID, IsDeleted: file.IsRemovedFromAlbum(), SyncedLocally: false}
|
||||
putErr := c.UpsertAlbumEntry(ctx, &albumEntry)
|
||||
if putErr != nil {
|
||||
return putErr
|
||||
}
|
||||
if file.IsDeleted {
|
||||
if file.IsRemovedFromAlbum() {
|
||||
continue
|
||||
}
|
||||
photoFile, err := mapper.MapApiFileToPhotoFile(ctx, album, file, c.KeyHolder)
|
||||
|
||||
@@ -31,7 +31,7 @@ are built against `electron`'s packaged `node` version. We use
|
||||
to rebuild those modules automatically after each `yarn install` by invoking it
|
||||
in as the `postinstall` step in our package.json.
|
||||
|
||||
### lint and lint-fix
|
||||
### lint, lint-fix
|
||||
|
||||
Use `yarn lint` to check that your code formatting is as expected, and that
|
||||
there are no linter errors. Use `yarn lint-fix` to try and automatically fix the
|
||||
|
||||
@@ -21,3 +21,9 @@ photos and videos are always available in normal directories and files.
|
||||
"continuous" exports, where it will automatically export new items in the
|
||||
background without you needing to run any other cron jobs. See
|
||||
[migration/export](/photos/migration/export/) for more details.
|
||||
|
||||
## Does the exported data from Ente photos preserve the same folder and album structure as in the app?
|
||||
|
||||
When you export your data for local backup, it will maintain the exact structure
|
||||
how you have set up within Ente. The exported data will reflect the same photos
|
||||
and album structure intact.
|
||||
|
||||
@@ -74,3 +74,29 @@ If you would like to fund the development of this project, please consider
|
||||
## How do I pronounce ente?
|
||||
|
||||
It's like cafe 😊. kaf-_ay_. en-_tay_.
|
||||
|
||||
## Does Ente apply compression to uploaded photos?
|
||||
|
||||
Ente does not apply compression to uploaded photos. The file size of your photos in Ente will be similar to the original file sizes you have.
|
||||
|
||||
## Can I add photos from a shared album to albums that I created in Ente?
|
||||
|
||||
Currently, Ente does not support adding photos from a shared album to your personal albums. If you want to include photos from a shared album in your own albums, you will need to ask the owner of the photos to add them to your album.
|
||||
|
||||
## How do I ensure that the Ente desktop app stays up to date on my system?
|
||||
|
||||
Ente desktop includes an auto-update feature, ensuring that whenever updates are deployed, the app will automatically download and install them. You don't need to manually update the software.
|
||||
|
||||
## Can I sync a folder containing multiple subfolders, each representing an album?
|
||||
|
||||
Yes, when you drag and drop the folder onto the desktop app, the app will detect the multiple folders and prompt you to choose whether you want to create a single album or separate albums for each folder.
|
||||
|
||||
## What is the difference between **Magic** and **Content** search results on the desktop?
|
||||
|
||||
**Magic** is where you can search for long queries. Like, "baby in red dress", or "dog playing at the beach".
|
||||
|
||||
**Content** is where you can search for single-words. Like, "car" or "pizza".
|
||||
|
||||
## How do I identify which files experienced upload issues within the desktop app?
|
||||
|
||||
Check the sections within the upload progress bar for "Failed Uploads," "Ignored Uploads," and "Unsuccessful Uploads."
|
||||
@@ -156,3 +156,7 @@ Sorry, since we're building a business that does not involve monetization of
|
||||
user data, we have to charge to remain sustainable.
|
||||
|
||||
We do offer a generous free trial for you to experience the product.
|
||||
|
||||
## Will I need to pay for Ente Auth after my Ente Photos free plan expires?
|
||||
|
||||
No, you will not need to pay for Ente Auth after your Ente Photos free plan expires. Ente Auth is completely free to use, and the expiration of your Ente Photos free plan will not impact your ability to access or use Ente Auth.
|
||||
|
||||
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 21 KiB |
BIN
docs/docs/photos/migration/export/export-5.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
@@ -11,26 +11,44 @@ videos you have uploaded to Ente.
|
||||
1. Sign in to [our desktop app](https://ente.io/download/desktop), if you
|
||||
haven't done so already.
|
||||
|
||||

|
||||
|
||||
2. Open the side bar, and select the option to **export data**.
|
||||
|
||||

|
||||
|
||||
3. Select the destination folder and click on **start**.
|
||||
3. Choose the destination folder by clicking on three dots icon.
|
||||
|
||||

|
||||
<div align="center">
|
||||
|
||||
4. Wait for the export to get completed.
|
||||
{width=400px}
|
||||
|
||||

|
||||
</div>
|
||||
|
||||
5. Later on if you wish to sync newer files that were uploaded since the last
|
||||
time you exported, simply select **export data** again and click on
|
||||
**resync**.
|
||||
4. Select the folder and then click on **Start**
|
||||
|
||||

|
||||
<div align="center">
|
||||
|
||||
{width=400px}
|
||||
|
||||
</div>
|
||||
|
||||
5. Wait for the export to get completed.
|
||||
|
||||
<div align="center">
|
||||
|
||||
{width=400px}
|
||||
|
||||
</div>
|
||||
|
||||
6. In case your download gets interrupted, Ente will resume from where it left
|
||||
off. Simply select **export data** again and click on **resync**.
|
||||
off. Simply select **export data** again and click on **Resync**.
|
||||
|
||||
<div align="center">
|
||||
|
||||
{width=400px}
|
||||
|
||||
</div>
|
||||
|
||||
7. **Sync continuously** : You can utilize Continuous Sync to eliminate manual
|
||||
exports each time new photos are added to Ente. This feature automatically
|
||||
@@ -39,6 +57,8 @@ videos you have uploaded to Ente.
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
If you run into any issues during your data export, please reach out to
|
||||
[support@ente.io](mailto:support@ente.io) and we will be happy to help you!
|
||||
|
||||
|
||||
BIN
docs/docs/photos/migration/export/sign-in.png
Normal file
|
After Width: | Height: | Size: 161 KiB |
@@ -1,6 +1,6 @@
|
||||
# CHANGELOG
|
||||
|
||||
## v0.8.72
|
||||
## v0.8.73
|
||||
### Added
|
||||
* #### Share an Album to Multiple Contacts at Once
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
* #### Bug Fixes and Performance Improvements
|
||||
|
||||
Many a bugs were squashed in this release. If you run into any, please write to team@ente.io, or let us know on Discord! 🙏
|
||||
Many a bugs were squashed in this release and have improved performance on app start. If you run into any bugs, please write to team@ente.io, or let us know on Discord! 🙏
|
||||
|
||||
|
||||
## v0.8.67
|
||||
|
||||
@@ -79,3 +79,5 @@ class LoginKeyDerivationError extends Error {}
|
||||
class SrpSetupNotCompleteError extends Error {}
|
||||
|
||||
class SharingNotPermittedForFreeAccountsError extends Error {}
|
||||
|
||||
class NoMediaLocationAccessError extends Error {}
|
||||
|
||||
@@ -15,6 +15,7 @@ class FileUpdationDB {
|
||||
static const columnLocalID = 'local_id';
|
||||
static const columnReason = 'reason';
|
||||
static const livePhotoCheck = 'livePhotoCheck';
|
||||
static const androidMissingGPS = 'androidMissingGPS';
|
||||
|
||||
static const modificationTimeUpdated = 'modificationTimeUpdated';
|
||||
|
||||
|
||||
@@ -1533,6 +1533,24 @@ class FilesDB {
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<List<String>> getLocalFilesBackedUpWithoutLocation(int userId) async {
|
||||
final db = await instance.database;
|
||||
final rows = await db.query(
|
||||
filesTable,
|
||||
columns: [columnLocalID],
|
||||
distinct: true,
|
||||
where:
|
||||
'$columnOwnerID = ? AND $columnLocalID IS NOT NULL AND ($columnUploadedFileID IS NOT NULL AND $columnUploadedFileID IS NOT -1) '
|
||||
'AND ($columnLatitude IS NULL OR $columnLongitude IS NULL OR $columnLongitude = 0.0 or $columnLongitude = 0.0)',
|
||||
whereArgs: [userId],
|
||||
);
|
||||
final result = <String>[];
|
||||
for (final row in rows) {
|
||||
result.add(row[columnLocalID] as String);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// updateSizeForUploadIDs takes a map of upploadedFileID and fileSize and
|
||||
// update the fileSize for the given uploadedFileID
|
||||
Future<void> updateSizeForUploadIDs(
|
||||
|
||||
3
mobile/lib/generated/intl/messages_pt.dart
generated
@@ -766,6 +766,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"Não rastreamos instalações do aplicativo. Seria útil se você nos contasse onde nos encontrou!"),
|
||||
"hearUsWhereTitle": MessageLookupByLibrary.simpleMessage(
|
||||
"Como você ouviu sobre o Ente? (opcional)"),
|
||||
"help": MessageLookupByLibrary.simpleMessage("Ajuda"),
|
||||
"hidden": MessageLookupByLibrary.simpleMessage("Oculto"),
|
||||
"hide": MessageLookupByLibrary.simpleMessage("Ocultar"),
|
||||
"hiding": MessageLookupByLibrary.simpleMessage("Ocultando..."),
|
||||
@@ -1011,6 +1012,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
MessageLookupByLibrary.simpleMessage("Detalhes de pagamento"),
|
||||
"paymentFailed":
|
||||
MessageLookupByLibrary.simpleMessage("Falha no pagamento"),
|
||||
"paymentFailedMessage": MessageLookupByLibrary.simpleMessage(
|
||||
"Infelizmente o seu pagamento falhou. Entre em contato com o suporte e nós ajudaremos você!"),
|
||||
"paymentFailedTalkToProvider": m37,
|
||||
"pendingItems": MessageLookupByLibrary.simpleMessage("Itens pendentes"),
|
||||
"pendingSync":
|
||||
|
||||
3
mobile/lib/generated/intl/messages_zh.dart
generated
@@ -630,6 +630,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"我们不跟踪应用程序安装情况。如果您告诉我们您是在哪里找到我们的,将会有所帮助!"),
|
||||
"hearUsWhereTitle":
|
||||
MessageLookupByLibrary.simpleMessage("您是如何知道Ente的? (可选的)"),
|
||||
"help": MessageLookupByLibrary.simpleMessage("帮助"),
|
||||
"hidden": MessageLookupByLibrary.simpleMessage("已隐藏"),
|
||||
"hide": MessageLookupByLibrary.simpleMessage("隐藏"),
|
||||
"hiding": MessageLookupByLibrary.simpleMessage("正在隐藏..."),
|
||||
@@ -834,6 +835,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"我们不储存这个密码,所以如果忘记, <underline>我们将无法解密您的数据</underline>"),
|
||||
"paymentDetails": MessageLookupByLibrary.simpleMessage("付款明细"),
|
||||
"paymentFailed": MessageLookupByLibrary.simpleMessage("支付失败"),
|
||||
"paymentFailedMessage": MessageLookupByLibrary.simpleMessage(
|
||||
"不幸的是,您的付款失败。请联系支持人员,我们将为您提供帮助!"),
|
||||
"paymentFailedTalkToProvider": m37,
|
||||
"pendingItems": MessageLookupByLibrary.simpleMessage("待处理项目"),
|
||||
"pendingSync": MessageLookupByLibrary.simpleMessage("正在等待同步"),
|
||||
|
||||
@@ -304,6 +304,7 @@
|
||||
}
|
||||
},
|
||||
"faq": "Perguntas frequentes",
|
||||
"help": "Ajuda",
|
||||
"oopsSomethingWentWrong": "Ops! Algo deu errado",
|
||||
"peopleUsingYourCode": "Pessoas que usam seu código",
|
||||
"eligible": "elegível",
|
||||
@@ -640,7 +641,7 @@
|
||||
"thankYou": "Obrigado",
|
||||
"failedToVerifyPaymentStatus": "Falha ao verificar status do pagamento",
|
||||
"pleaseWaitForSometimeBeforeRetrying": "Por favor, aguarde algum tempo antes de tentar novamente",
|
||||
"paymentFailedWithReason": "Infelizmente o seu pagamento falhou devido a {reason}",
|
||||
"paymentFailedMessage": "Infelizmente o seu pagamento falhou. Entre em contato com o suporte e nós ajudaremos você!",
|
||||
"youAreOnAFamilyPlan": "Você está em um plano familiar!",
|
||||
"contactFamilyAdmin": "Entre em contato com <green>{familyAdminEmail}</green> para gerenciar sua assinatura",
|
||||
"leaveFamily": "Sair da família",
|
||||
|
||||
@@ -304,6 +304,7 @@
|
||||
}
|
||||
},
|
||||
"faq": "常见问题",
|
||||
"help": "帮助",
|
||||
"oopsSomethingWentWrong": "哎呀,似乎出了点问题",
|
||||
"peopleUsingYourCode": "使用您的代码的人",
|
||||
"eligible": "符合资格",
|
||||
@@ -640,7 +641,7 @@
|
||||
"thankYou": "非常感谢您",
|
||||
"failedToVerifyPaymentStatus": "验证支付状态失败",
|
||||
"pleaseWaitForSometimeBeforeRetrying": "请稍等片刻后再重试",
|
||||
"paymentFailedWithReason": "很抱歉,您的支付因 {reason} 而失败",
|
||||
"paymentFailedMessage": "不幸的是,您的付款失败。请联系支持人员,我们将为您提供帮助!",
|
||||
"youAreOnAFamilyPlan": "你在一个家庭计划中!",
|
||||
"contactFamilyAdmin": "请联系 <green>{familyAdminEmail}</green> 来管理您的订阅",
|
||||
"leaveFamily": "离开家庭计划",
|
||||
|
||||
@@ -145,13 +145,7 @@ class HomeWidgetService {
|
||||
}
|
||||
|
||||
Future<int> countHomeWidgets() async {
|
||||
return await hw.HomeWidget.getWidgetCount(
|
||||
name: 'SlideshowWidgetProvider',
|
||||
androidName: 'SlideshowWidgetProvider',
|
||||
qualifiedAndroidName: 'io.ente.photos.SlideshowWidgetProvider',
|
||||
iOSName: 'SlideshowWidget',
|
||||
) ??
|
||||
0;
|
||||
return (await hw.HomeWidget.getInstalledWidgets()).length;
|
||||
}
|
||||
|
||||
Future<void> clearHomeWidget() async {
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:logging/logging.dart';
|
||||
import "package:photos/core/errors.dart";
|
||||
import 'package:photos/db/ignored_files_db.dart';
|
||||
import 'package:photos/models/file/file.dart';
|
||||
import 'package:photos/models/ignored_file.dart';
|
||||
@@ -47,7 +48,10 @@ class IgnoredFilesService {
|
||||
return false;
|
||||
}
|
||||
|
||||
String? getUploadSkipReason(Map<String, String> idToReasonMap, EnteFile file) {
|
||||
String? getUploadSkipReason(
|
||||
Map<String, String> idToReasonMap,
|
||||
EnteFile file,
|
||||
) {
|
||||
final id = _getIgnoreID(file.localID, file.deviceFolder, file.title);
|
||||
if (id != null && id.isNotEmpty) {
|
||||
return idToReasonMap[id];
|
||||
@@ -100,6 +104,12 @@ class IgnoredFilesService {
|
||||
for (IgnoredFile iFile in dbResult) {
|
||||
final id = _idForIgnoredFile(iFile);
|
||||
if (id != null) {
|
||||
if (Platform.isIOS &&
|
||||
iFile.reason == InvalidReason.sourceFileMissing.name) {
|
||||
// ignoreSourceFileMissing error on iOS as the file fetch from iCloud might have failed,
|
||||
// but the file might be available later
|
||||
continue;
|
||||
}
|
||||
result[id] = iFile.reason;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:core';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:logging/logging.dart';
|
||||
import "package:photo_manager/photo_manager.dart";
|
||||
import "package:photos/core/configuration.dart";
|
||||
import 'package:photos/core/errors.dart';
|
||||
import 'package:photos/db/file_updation_db.dart';
|
||||
@@ -25,6 +26,10 @@ class LocalFileUpdateService {
|
||||
late Logger _logger;
|
||||
final String _iosLivePhotoSizeMigrationDone = 'fm_ios_live_photo_check';
|
||||
final String _doneLivePhotoImport = 'fm_import_ios_live_photo_check';
|
||||
final String _androidMissingGPSImportDone =
|
||||
'fm_android_missing_gps_import_done';
|
||||
final String _androidMissingGPSCheckDone =
|
||||
'fm_android_missing_gps_check_done';
|
||||
static int twoHundredKb = 200 * 1024;
|
||||
final List<String> _oldMigrationKeys = [
|
||||
'fm_badCreationTime',
|
||||
@@ -63,6 +68,9 @@ class LocalFileUpdateService {
|
||||
if (!Platform.isAndroid) {
|
||||
await _handleLivePhotosSizedCheck();
|
||||
}
|
||||
if (Platform.isAndroid) {
|
||||
await _androidMissingGPSCheck();
|
||||
}
|
||||
} catch (e, s) {
|
||||
_logger.severe('failed to perform migration', e, s);
|
||||
} finally {
|
||||
@@ -385,6 +393,131 @@ class LocalFileUpdateService {
|
||||
await _prefs.setBool(_doneLivePhotoImport, true);
|
||||
}
|
||||
|
||||
//#region Android Missing GPS specific methods ###
|
||||
|
||||
Future<void> _androidMissingGPSCheck() async {
|
||||
if (_prefs.containsKey(_androidMissingGPSCheckDone)) {
|
||||
return;
|
||||
}
|
||||
await _importAndroidBadGPSCandidate();
|
||||
// singleRunLimit indicates number of files to check during single
|
||||
// invocation of this method. The limit act as a crude way to limit the
|
||||
// resource consumed by the method
|
||||
const int singleRunLimit = 500;
|
||||
final localIDsToProcess =
|
||||
await _fileUpdationDB.getLocalIDsForPotentialReUpload(
|
||||
singleRunLimit,
|
||||
FileUpdationDB.androidMissingGPS,
|
||||
);
|
||||
if (localIDsToProcess.isNotEmpty) {
|
||||
final chunksOf50 = localIDsToProcess.chunks(50);
|
||||
for (final chunk in chunksOf50) {
|
||||
final sTime = DateTime.now().microsecondsSinceEpoch;
|
||||
final List<Future> futures = [];
|
||||
final chunkOf10 = chunk.chunks(10);
|
||||
for (final smallChunk in chunkOf10) {
|
||||
futures.add(_checkForMissingGPS(smallChunk));
|
||||
}
|
||||
await Future.wait(futures);
|
||||
final eTime = DateTime.now().microsecondsSinceEpoch;
|
||||
final d = Duration(microseconds: eTime - sTime);
|
||||
_logger.info(
|
||||
'Performed missing GPS Location check for ${chunk.length} files '
|
||||
'completed in ${d.inSeconds.toString()} secs',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
_logger.info('Completed android missing GPS check');
|
||||
await _prefs.setBool(_androidMissingGPSCheckDone, true);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _checkForMissingGPS(List<String> localIDs) async {
|
||||
try {
|
||||
final List<EnteFile> localFiles =
|
||||
await FilesDB.instance.getLocalFiles(localIDs);
|
||||
final ownerID = Configuration.instance.getUserID()!;
|
||||
final Set<String> localIDsWithFile = {};
|
||||
final Set<String> reuploadCandidate = {};
|
||||
final Set<String> processedIDs = {};
|
||||
for (EnteFile file in localFiles) {
|
||||
if (file.localID == null) continue;
|
||||
// ignore files that are not uploaded or have different owner
|
||||
if (!file.isUploaded || file.ownerID! != ownerID) {
|
||||
processedIDs.add(file.localID!);
|
||||
}
|
||||
if (file.hasLocation) {
|
||||
processedIDs.add(file.localID!);
|
||||
}
|
||||
}
|
||||
for (EnteFile enteFile in localFiles) {
|
||||
try {
|
||||
if (enteFile.localID == null ||
|
||||
processedIDs.contains(enteFile.localID!)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final localID = enteFile.localID!;
|
||||
localIDsWithFile.add(localID);
|
||||
final AssetEntity? entity = await AssetEntity.fromId(localID);
|
||||
if (entity == null) {
|
||||
processedIDs.add(localID);
|
||||
} else {
|
||||
final latLng = await entity.latlngAsync();
|
||||
if ((latLng.longitude ?? 0) == 0 || (latLng.latitude ?? 0) == 0) {
|
||||
processedIDs.add(localID);
|
||||
} else {
|
||||
reuploadCandidate.add(localID);
|
||||
processedIDs.add(localID);
|
||||
}
|
||||
}
|
||||
} catch (e, s) {
|
||||
processedIDs.add(enteFile.localID!);
|
||||
_logger.severe('lat/long check file ${enteFile.toString()}', e, s);
|
||||
}
|
||||
}
|
||||
for (String id in localIDs) {
|
||||
// if the file with given localID doesn't exist, consider it as done.
|
||||
if (!localIDsWithFile.contains(id)) {
|
||||
processedIDs.add(id);
|
||||
}
|
||||
}
|
||||
await FileUpdationDB.instance.insertMultiple(
|
||||
reuploadCandidate.toList(),
|
||||
FileUpdationDB.modificationTimeUpdated,
|
||||
);
|
||||
await FileUpdationDB.instance.deleteByLocalIDs(
|
||||
processedIDs.toList(),
|
||||
FileUpdationDB.androidMissingGPS,
|
||||
);
|
||||
} catch (e, s) {
|
||||
_logger.severe('error while checking missing GPS', e, s);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _importAndroidBadGPSCandidate() async {
|
||||
if (_prefs.containsKey(_androidMissingGPSImportDone)) {
|
||||
return;
|
||||
}
|
||||
final sTime = DateTime.now().microsecondsSinceEpoch;
|
||||
_logger.info('importing files without missing GPS');
|
||||
final int ownerID = Configuration.instance.getUserID()!;
|
||||
final fileLocalIDs =
|
||||
await FilesDB.instance.getLocalFilesBackedUpWithoutLocation(ownerID);
|
||||
await _fileUpdationDB.insertMultiple(
|
||||
fileLocalIDs,
|
||||
FileUpdationDB.androidMissingGPS,
|
||||
);
|
||||
final eTime = DateTime.now().microsecondsSinceEpoch;
|
||||
final d = Duration(microseconds: eTime - sTime);
|
||||
_logger.info(
|
||||
'importing completed, total files count ${fileLocalIDs.length} and took ${d.inSeconds.toString()} seconds',
|
||||
);
|
||||
await _prefs.setBool(_androidMissingGPSImportDone, true);
|
||||
}
|
||||
|
||||
//#endregion Android Missing GPS specific methods ###
|
||||
|
||||
Future<MediaUploadData> getUploadData(EnteFile file) async {
|
||||
final mediaUploadData = await getUploadDataFromEnteFile(file);
|
||||
// delete the file from app's internal cache if it was copied to app
|
||||
|
||||
@@ -20,6 +20,7 @@ import 'package:photos/services/app_lifecycle_service.dart';
|
||||
import "package:photos/services/ignored_files_service.dart";
|
||||
import 'package:photos/services/local/local_sync_util.dart';
|
||||
import "package:photos/utils/debouncer.dart";
|
||||
import "package:photos/utils/photo_manager_util.dart";
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
@@ -61,7 +62,7 @@ class LocalSyncService {
|
||||
return;
|
||||
}
|
||||
if (Platform.isAndroid && AppLifecycleService.instance.isForeground) {
|
||||
final permissionState = await PhotoManager.requestPermissionExtend();
|
||||
final permissionState = await requestPhotoMangerPermissions();
|
||||
if (permissionState != PermissionState.authorized) {
|
||||
_logger.severe(
|
||||
"sync requested with invalid permission",
|
||||
@@ -213,6 +214,11 @@ class LocalSyncService {
|
||||
_logger.warning('Invalid file received for ignoring: $file');
|
||||
return;
|
||||
}
|
||||
if (Platform.isIOS && error.reason == InvalidReason.sourceFileMissing) {
|
||||
// ignoreSourceFileMissing error on iOS as the file fetch from iCloud might have failed,
|
||||
// but the file might be available later
|
||||
return;
|
||||
}
|
||||
final ignored = IgnoredFile(
|
||||
file.localID,
|
||||
file.title,
|
||||
|
||||
@@ -170,7 +170,8 @@ class RemoteSyncService {
|
||||
e is NoActiveSubscriptionError ||
|
||||
e is WiFiUnavailableError ||
|
||||
e is StorageLimitExceededError ||
|
||||
e is SyncStopRequestedError) {
|
||||
e is SyncStopRequestedError ||
|
||||
e is NoMediaLocationAccessError) {
|
||||
_logger.warning("Error executing remote sync", e, s);
|
||||
rethrow;
|
||||
} else {
|
||||
@@ -555,6 +556,7 @@ class RemoteSyncService {
|
||||
final int toBeUploaded = filesToBeUploaded.length + updatedFileIDs.length;
|
||||
if (toBeUploaded > 0) {
|
||||
Bus.instance.fire(SyncStatusUpdate(SyncStatus.preparingForUpload));
|
||||
await _uploader.verifyMediaLocationAccess();
|
||||
await _uploader.checkNetworkForUpload();
|
||||
// verify if files upload is allowed based on their subscription plan and
|
||||
// storage limit. To avoid creating new endpoint, we are using
|
||||
|
||||
@@ -120,6 +120,14 @@ class SyncService {
|
||||
} on UnauthorizedError {
|
||||
_logger.info("Logging user out");
|
||||
Bus.instance.fire(TriggerLogoutEvent());
|
||||
} on NoMediaLocationAccessError {
|
||||
_logger.severe("Not uploading due to no media location access");
|
||||
Bus.instance.fire(
|
||||
SyncStatusUpdate(
|
||||
SyncStatus.error,
|
||||
error: NoMediaLocationAccessError(),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
if (e is DioError) {
|
||||
if (e.type == DioErrorType.connectTimeout ||
|
||||
|
||||
@@ -16,7 +16,7 @@ class UpdateService {
|
||||
static final UpdateService instance = UpdateService._privateConstructor();
|
||||
static const kUpdateAvailableShownTimeKey = "update_available_shown_time_key";
|
||||
static const changeLogVersionKey = "update_change_log_key";
|
||||
static const currentChangeLogVersion = 16;
|
||||
static const currentChangeLogVersion = 17;
|
||||
|
||||
LatestVersionInfo? _latestVersion;
|
||||
final _logger = Logger("UpdateService");
|
||||
|
||||
@@ -10,6 +10,7 @@ import 'package:photos/ui/components/buttons/icon_button_widget.dart';
|
||||
import "package:photos/ui/settings/backup/backup_folder_selection_page.dart";
|
||||
import "package:photos/utils/dialog_util.dart";
|
||||
import "package:photos/utils/navigation_util.dart";
|
||||
import "package:photos/utils/photo_manager_util.dart";
|
||||
|
||||
class HomeHeaderWidget extends StatefulWidget {
|
||||
final Widget centerWidget;
|
||||
@@ -48,7 +49,7 @@ class _HomeHeaderWidgetState extends State<HomeHeaderWidget> {
|
||||
onTap: () async {
|
||||
try {
|
||||
final PermissionState state =
|
||||
await PhotoManager.requestPermissionExtend();
|
||||
await requestPhotoMangerPermissions();
|
||||
await LocalSyncService.instance.onUpdatePermission(state);
|
||||
} on Exception catch (e) {
|
||||
Logger("HomeHeaderWidget").severe(
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import 'package:photos/services/sync_service.dart';
|
||||
import "package:photos/utils/photo_manager_util.dart";
|
||||
import "package:styled_text/styled_text.dart";
|
||||
|
||||
class GrantPermissionsWidget extends StatelessWidget {
|
||||
@@ -91,7 +92,7 @@ class GrantPermissionsWidget extends StatelessWidget {
|
||||
key: const ValueKey("grantPermissionButton"),
|
||||
child: Text(S.of(context).grantPermission),
|
||||
onPressed: () async {
|
||||
final state = await PhotoManager.requestPermissionExtend();
|
||||
final state = await requestPhotoMangerPermissions();
|
||||
if (state == PermissionState.authorized ||
|
||||
state == PermissionState.limited) {
|
||||
await SyncService.instance.onPermissionGranted(state);
|
||||
|
||||
@@ -128,8 +128,8 @@ class _ChangeLogPageState extends State<ChangeLogPage> {
|
||||
),
|
||||
ChangeLogEntry(
|
||||
"Bug Fixes and Performance Improvements",
|
||||
'Many a bugs were squashed in this release.\n'
|
||||
'\nIf you run into any, please write to team@ente.io, or let us know on Discord! 🙏',
|
||||
'Many a bugs were squashed in this release and have improved performance on app start.\n'
|
||||
'\nIf you run into any bugs, please write to team@ente.io, or let us know on Discord! 🙏',
|
||||
),
|
||||
]);
|
||||
|
||||
|
||||
@@ -140,6 +140,7 @@ class _DetailPageState extends State<DetailPage> {
|
||||
),
|
||||
extendBodyBehindAppBar: true,
|
||||
resizeToAvoidBottomInset: false,
|
||||
backgroundColor: Colors.black,
|
||||
body: Center(
|
||||
child: Stack(
|
||||
children: [
|
||||
@@ -165,6 +166,7 @@ class _DetailPageState extends State<DetailPage> {
|
||||
|
||||
Widget _buildPageView(BuildContext context) {
|
||||
return PageView.builder(
|
||||
clipBehavior: Clip.none,
|
||||
itemBuilder: (context, index) {
|
||||
final file = _files![index];
|
||||
_preloadFiles(index);
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import "package:flutter_image_compress/flutter_image_compress.dart";
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:photo_view/photo_view.dart';
|
||||
import 'package:photos/core/cache/thumbnail_in_memory_cache.dart';
|
||||
@@ -50,6 +51,7 @@ class _ZoomableImageState extends State<ZoomableImage> {
|
||||
bool _loadedLargeThumbnail = false;
|
||||
bool _loadingFinalImage = false;
|
||||
bool _loadedFinalImage = false;
|
||||
bool _convertToSupportedFormat = false;
|
||||
ValueChanged<PhotoViewScaleState>? _scaleStateChangedCallback;
|
||||
bool _isZooming = false;
|
||||
PhotoViewController _photoViewController = PhotoViewController();
|
||||
@@ -57,6 +59,7 @@ class _ZoomableImageState extends State<ZoomableImage> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_photo = widget.photo;
|
||||
_logger = Logger("ZoomableImage");
|
||||
_logger.info('initState for ${_photo.generatedID} with tag ${_photo.tag}');
|
||||
@@ -68,7 +71,6 @@ class _ZoomableImageState extends State<ZoomableImage> {
|
||||
debugPrint("isZooming = $_isZooming, currentState $value");
|
||||
// _logger.info('is reakky zooming $_isZooming with state $value');
|
||||
};
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -133,7 +135,9 @@ class _ZoomableImageState extends State<ZoomableImage> {
|
||||
height: screenRelativeImageHeight,
|
||||
child: Hero(
|
||||
tag: widget.tagPrefix! + _photo.tag,
|
||||
child: const EnteLoadingWidget(),
|
||||
child: const EnteLoadingWidget(
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -141,7 +145,9 @@ class _ZoomableImageState extends State<ZoomableImage> {
|
||||
),
|
||||
);
|
||||
} else {
|
||||
content = const EnteLoadingWidget();
|
||||
content = const EnteLoadingWidget(
|
||||
color: Colors.white,
|
||||
);
|
||||
}
|
||||
|
||||
final GestureDragUpdateCallback? verticalDragCallback = _isZooming
|
||||
@@ -194,11 +200,8 @@ class _ZoomableImageState extends State<ZoomableImage> {
|
||||
_loadingFinalImage = true;
|
||||
getFileFromServer(_photo).then((file) {
|
||||
if (file != null) {
|
||||
_onFinalImageLoaded(
|
||||
Image.file(
|
||||
file,
|
||||
gaplessPlayback: true,
|
||||
).image,
|
||||
_onFileLoaded(
|
||||
file,
|
||||
);
|
||||
} else {
|
||||
_loadingFinalImage = false;
|
||||
@@ -239,7 +242,9 @@ class _ZoomableImageState extends State<ZoomableImage> {
|
||||
_isGIF(), // since on iOS GIFs playback only when origin-files are loaded
|
||||
).then((file) {
|
||||
if (file != null && file.existsSync()) {
|
||||
_onFinalImageLoaded(Image.file(file).image);
|
||||
_onFileLoaded(
|
||||
file,
|
||||
);
|
||||
} else {
|
||||
_logger.info("File was deleted " + _photo.toString());
|
||||
if (_photo.uploadedFileID != null) {
|
||||
@@ -277,24 +282,45 @@ class _ZoomableImageState extends State<ZoomableImage> {
|
||||
}
|
||||
}
|
||||
|
||||
void _onFinalImageLoaded(ImageProvider imageProvider) {
|
||||
void _onFileLoaded(File file) {
|
||||
final imageProvider = Image.file(
|
||||
file,
|
||||
gaplessPlayback: true,
|
||||
).image;
|
||||
|
||||
if (mounted) {
|
||||
precacheImage(imageProvider, context).then((value) async {
|
||||
if (mounted) {
|
||||
await _updatePhotoViewController(
|
||||
previewImageProvider: _imageProvider,
|
||||
finalImageProvider: imageProvider,
|
||||
);
|
||||
setState(() {
|
||||
_imageProvider = imageProvider;
|
||||
_loadedFinalImage = true;
|
||||
_logger.info("Final image loaded");
|
||||
});
|
||||
precacheImage(
|
||||
imageProvider,
|
||||
context,
|
||||
onError: (exception, _) async {
|
||||
_logger
|
||||
.info(exception.toString() + ". Filename: ${_photo.displayName}");
|
||||
if (exception.toString().contains(
|
||||
"Codec failed to produce an image, possibly due to invalid image data",
|
||||
)) {
|
||||
unawaited(_loadInSupportedFormat(file));
|
||||
}
|
||||
},
|
||||
).then((value) {
|
||||
if (mounted && !_loadedFinalImage && !_convertToSupportedFormat) {
|
||||
_updateViewWithFinalImage(imageProvider);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _updateViewWithFinalImage(ImageProvider imageProvider) async {
|
||||
await _updatePhotoViewController(
|
||||
previewImageProvider: _imageProvider,
|
||||
finalImageProvider: imageProvider,
|
||||
);
|
||||
setState(() {
|
||||
_imageProvider = imageProvider;
|
||||
_loadedFinalImage = true;
|
||||
_logger.info("Final image loaded");
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _updatePhotoViewController({
|
||||
required ImageProvider? previewImageProvider,
|
||||
required ImageProvider finalImageProvider,
|
||||
@@ -348,4 +374,28 @@ class _ZoomableImageState extends State<ZoomableImage> {
|
||||
}
|
||||
|
||||
bool _isGIF() => _photo.displayName.toLowerCase().endsWith(".gif");
|
||||
|
||||
Future<void> _loadInSupportedFormat(File file) async {
|
||||
_logger.info("Compressing ${_photo.displayName} to viewable format");
|
||||
_convertToSupportedFormat = true;
|
||||
|
||||
final compressedFile =
|
||||
await FlutterImageCompress.compressWithFile(file.path);
|
||||
|
||||
if (compressedFile != null) {
|
||||
final imageProvider = MemoryImage(compressedFile);
|
||||
|
||||
unawaited(
|
||||
precacheImage(imageProvider, context).then((value) {
|
||||
if (mounted) {
|
||||
_updateViewWithFinalImage(imageProvider);
|
||||
}
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
_logger.severe(
|
||||
"Failed to compress image ${_photo.displayName} to viewable format",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import "package:photos/ui/components/models/button_type.dart";
|
||||
import "package:photos/ui/components/title_bar_title_widget.dart";
|
||||
import "package:photos/ui/viewer/gallery/gallery.dart";
|
||||
import "package:photos/utils/dialog_util.dart";
|
||||
import "package:photos/utils/photo_manager_util.dart";
|
||||
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
|
||||
|
||||
Future<dynamic> showAddPhotosSheet(
|
||||
@@ -203,7 +204,7 @@ class AddPhotosPhotoWidget extends StatelessWidget {
|
||||
}
|
||||
} catch (e) {
|
||||
if (e is StateError) {
|
||||
final PermissionState ps = await PhotoManager.requestPermissionExtend();
|
||||
final PermissionState ps = await requestPhotoMangerPermissions();
|
||||
if (ps != PermissionState.authorized && ps != PermissionState.limited) {
|
||||
await showChoiceDialog(
|
||||
context,
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import "package:permission_handler/permission_handler.dart";
|
||||
import 'package:photos/core/configuration.dart';
|
||||
import "package:photos/core/constants.dart";
|
||||
import 'package:photos/core/errors.dart';
|
||||
@@ -363,6 +364,15 @@ class FileUploader {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> verifyMediaLocationAccess() async {
|
||||
if (Platform.isAndroid) {
|
||||
final bool hasPermission = await Permission.accessMediaLocation.isGranted;
|
||||
if (!hasPermission) {
|
||||
throw NoMediaLocationAccessError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<EnteFile> forceUpload(EnteFile file, int collectionID) async {
|
||||
_hasInitiatedForceUpload = true;
|
||||
return _tryToUpload(file, collectionID, true);
|
||||
|
||||
12
mobile/lib/utils/photo_manager_util.dart
Normal file
@@ -0,0 +1,12 @@
|
||||
import "package:photo_manager/photo_manager.dart";
|
||||
|
||||
Future<PermissionState> requestPhotoMangerPermissions() {
|
||||
return PhotoManager.requestPermissionExtend(
|
||||
requestOption: const PermissionRequestOption(
|
||||
androidPermission: AndroidPermission(
|
||||
type: RequestType.common,
|
||||
mediaLocation: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -933,12 +933,11 @@ packages:
|
||||
home_widget:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: main
|
||||
resolved-ref: "49158ce4a517e87817dc84c6b96c00639281229a"
|
||||
url: "https://github.com/ente-io/FlutterHomeWidget"
|
||||
source: git
|
||||
version: "0.4.1"
|
||||
name: home_widget
|
||||
sha256: "29565bfee4b32eaf9e7e8b998d504618b779a74b2b1ac62dd4dac7468e66f1a3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.0"
|
||||
html:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1539,6 +1538,46 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.11.1"
|
||||
permission_handler:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: permission_handler
|
||||
sha256: "284a66179cabdf942f838543e10413246f06424d960c92ba95c84439154fcac8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "11.0.1"
|
||||
permission_handler_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_android
|
||||
sha256: f9fddd3b46109bd69ff3f9efa5006d2d309b7aec0f3c1c5637a60a2d5659e76e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "11.1.0"
|
||||
permission_handler_apple:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_apple
|
||||
sha256: "99e220bce3f8877c78e4ace901082fb29fa1b4ebde529ad0932d8d664b34f3f5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.1.4"
|
||||
permission_handler_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_platform_interface
|
||||
sha256: "6760eb5ef34589224771010805bea6054ad28453906936f843a8cc4d3a55c4a4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.12.0"
|
||||
permission_handler_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_windows
|
||||
sha256: cc074aace208760f1eee6aa4fae766b45d947df85bc831cde77009cdb4720098
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.3"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -12,7 +12,7 @@ description: ente photos application
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
|
||||
version: 0.8.72+592
|
||||
version: 0.8.74+594
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
@@ -91,10 +91,7 @@ dependencies:
|
||||
fluttertoast: ^8.0.6
|
||||
freezed_annotation: ^2.2.0
|
||||
google_nav_bar: ^5.0.5
|
||||
home_widget:
|
||||
git:
|
||||
url: https://github.com/ente-io/FlutterHomeWidget
|
||||
ref: main
|
||||
home_widget: ^0.5.0
|
||||
html_unescape: ^2.0.0
|
||||
http: ^1.1.0
|
||||
image: ^4.0.17
|
||||
@@ -133,6 +130,7 @@ dependencies:
|
||||
path: #dart
|
||||
path_provider: ^2.1.1
|
||||
pedantic: ^1.9.2
|
||||
permission_handler: ^11.0.1
|
||||
photo_manager: ^2.8.1
|
||||
photo_view: ^0.14.0
|
||||
pinput: ^1.2.2
|
||||
|
||||
@@ -37,7 +37,6 @@ import (
|
||||
embeddingCtrl "github.com/ente-io/museum/pkg/controller/embedding"
|
||||
"github.com/ente-io/museum/pkg/controller/family"
|
||||
kexCtrl "github.com/ente-io/museum/pkg/controller/kex"
|
||||
"github.com/ente-io/museum/pkg/controller/locationtag"
|
||||
"github.com/ente-io/museum/pkg/controller/lock"
|
||||
remoteStoreCtrl "github.com/ente-io/museum/pkg/controller/remotestore"
|
||||
"github.com/ente-io/museum/pkg/controller/storagebonus"
|
||||
@@ -50,7 +49,6 @@ import (
|
||||
"github.com/ente-io/museum/pkg/repo/datacleanup"
|
||||
"github.com/ente-io/museum/pkg/repo/embedding"
|
||||
"github.com/ente-io/museum/pkg/repo/kex"
|
||||
locationtagRepo "github.com/ente-io/museum/pkg/repo/locationtag"
|
||||
"github.com/ente-io/museum/pkg/repo/passkey"
|
||||
"github.com/ente-io/museum/pkg/repo/remotestore"
|
||||
storageBonusRepo "github.com/ente-io/museum/pkg/repo/storagebonus"
|
||||
@@ -150,7 +148,6 @@ func main() {
|
||||
twoFactorRecoveryRepo := &two_factor_recovery.Repository{Db: db, SecretEncryptionKey: secretEncryptionKeyBytes}
|
||||
billingRepo := &repo.BillingRepository{DB: db}
|
||||
userEntityRepo := &userEntityRepo.Repository{DB: db}
|
||||
locationTagRepository := &locationtagRepo.Repository{DB: db}
|
||||
authRepo := &authenticatorRepo.Repository{DB: db}
|
||||
remoteStoreRepository := &remotestore.Repository{DB: db}
|
||||
dataCleanupRepository := &datacleanup.Repository{DB: db}
|
||||
@@ -641,13 +638,6 @@ func main() {
|
||||
privateAPI.DELETE("/user-entity/entity", userEntityHandler.DeleteEntity)
|
||||
privateAPI.GET("/user-entity/entity/diff", userEntityHandler.GetDiff)
|
||||
|
||||
locationTagController := &locationtag.Controller{Repo: locationTagRepository}
|
||||
locationTagHandler := &api.LocationTagHandler{Controller: locationTagController}
|
||||
privateAPI.POST("/locationtag/create", locationTagHandler.Create)
|
||||
privateAPI.POST("/locationtag/update", locationTagHandler.Update)
|
||||
privateAPI.DELETE("/locationtag/delete", locationTagHandler.Delete)
|
||||
privateAPI.GET("/locationtag/diff", locationTagHandler.GetDiff)
|
||||
|
||||
authenticatorController := &authenticatorCtrl.Controller{Repo: authRepo}
|
||||
authenticatorHandler := &api.AuthenticatorHandler{Controller: authenticatorController}
|
||||
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
package ente
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"github.com/ente-io/stacktrace"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// LocationTag represents a location tag in the system. The location information
|
||||
// is stored in an encrypted as Attributes
|
||||
type LocationTag struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
OwnerID int64 `json:"ownerId,omitempty"`
|
||||
EncryptedKey string `json:"encryptedKey" binding:"required"`
|
||||
KeyDecryptionNonce string `json:"keyDecryptionNonce" binding:"required"`
|
||||
Attributes LocationTagAttribute `json:"attributes" binding:"required"`
|
||||
IsDeleted bool `json:"isDeleted"`
|
||||
Provider string `json:"provider,omitempty"`
|
||||
CreatedAt int64 `json:"createdAt,omitempty"` // utc epoch microseconds
|
||||
UpdatedAt int64 `json:"updatedAt,omitempty"` // utc epoch microseconds
|
||||
}
|
||||
|
||||
// LocationTagAttribute holds encrypted data about user's location tag.
|
||||
type LocationTagAttribute struct {
|
||||
Version int `json:"version,omitempty" binding:"required"`
|
||||
EncryptedData string `json:"encryptedData,omitempty" binding:"required"`
|
||||
DecryptionNonce string `json:"decryptionNonce,omitempty" binding:"required"`
|
||||
}
|
||||
|
||||
// Value implements the driver.Valuer interface. This method
|
||||
// simply returns the JSON-encoded representation of the struct.
|
||||
func (la LocationTagAttribute) Value() (driver.Value, error) {
|
||||
return json.Marshal(la)
|
||||
}
|
||||
|
||||
// Scan implements the sql.Scanner interface. This method
|
||||
// simply decodes a JSON-encoded value into the struct fields.
|
||||
func (la *LocationTagAttribute) Scan(value interface{}) error {
|
||||
b, ok := value.([]byte)
|
||||
if !ok {
|
||||
return stacktrace.NewError("type assertion to []byte failed")
|
||||
}
|
||||
return json.Unmarshal(b, &la)
|
||||
}
|
||||
|
||||
// DeleteLocationTagRequest is request structure for deleting a location tag
|
||||
type DeleteLocationTagRequest struct {
|
||||
ID uuid.UUID `json:"id" binding:"required"`
|
||||
OwnerID int64 // should be populated from req headers
|
||||
}
|
||||
|
||||
// GetLocationTagDiffRequest is request struct for fetching locationTag changes
|
||||
type GetLocationTagDiffRequest struct {
|
||||
// SinceTime *int64. Pointer allows us to pass 0 value otherwise binding fails for zero Value.
|
||||
SinceTime *int64 `form:"sinceTime" binding:"required"`
|
||||
Limit int16 `form:"limit" binding:"required"`
|
||||
OwnerID int64 // should be populated from req headers
|
||||
}
|
||||
@@ -8,6 +8,7 @@ type EntityType string
|
||||
|
||||
const (
|
||||
Location EntityType = "location"
|
||||
Person EntityType = "person"
|
||||
)
|
||||
|
||||
type EntityKey struct {
|
||||
|
||||
1
server/migrations/82_drop_location_tag_table.down.sql
Normal file
@@ -0,0 +1 @@
|
||||
-- no-op
|
||||
2
server/migrations/82_drop_location_tag_table.up.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
DROP TRIGGER IF EXISTS update_location_tag_updated_at ON location_tag;
|
||||
DROP TABLE location_tag;
|
||||
@@ -1,88 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/ente-io/museum/ente"
|
||||
"github.com/ente-io/museum/pkg/controller/locationtag"
|
||||
"github.com/ente-io/museum/pkg/utils/auth"
|
||||
"github.com/ente-io/museum/pkg/utils/handler"
|
||||
"github.com/ente-io/stacktrace"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// LocationTagHandler expose request handlers to all location tag requests
|
||||
type LocationTagHandler struct {
|
||||
Controller *locationtag.Controller
|
||||
}
|
||||
|
||||
// Create handler for creating a new location tag
|
||||
func (h *LocationTagHandler) Create(c *gin.Context) {
|
||||
var request ente.LocationTag
|
||||
if err := c.ShouldBindJSON(&request); err != nil {
|
||||
handler.Error(c,
|
||||
stacktrace.Propagate(ente.ErrBadRequest, fmt.Sprintf("Request binding failed %s", err)))
|
||||
return
|
||||
}
|
||||
request.OwnerID = auth.GetUserID(c.Request.Header)
|
||||
resp, err := h.Controller.Create(c, request)
|
||||
if err != nil {
|
||||
handler.Error(c, stacktrace.Propagate(err, "Failed to create locationTag"))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// Update handler for updating location tag
|
||||
func (h *LocationTagHandler) Update(c *gin.Context) {
|
||||
var request ente.LocationTag
|
||||
if err := c.ShouldBindJSON(&request); err != nil {
|
||||
handler.Error(c,
|
||||
stacktrace.Propagate(ente.ErrBadRequest, fmt.Sprintf("Request binding failed %s", err)))
|
||||
return
|
||||
}
|
||||
request.OwnerID = auth.GetUserID(c.Request.Header)
|
||||
resp, err := h.Controller.Update(c, request)
|
||||
if err != nil {
|
||||
handler.Error(c, stacktrace.Propagate(err, "Failed to update locationTag"))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"locationTag": resp})
|
||||
}
|
||||
|
||||
// Delete handler for deleting location tag
|
||||
func (h *LocationTagHandler) Delete(c *gin.Context) {
|
||||
var request ente.DeleteLocationTagRequest
|
||||
if err := c.ShouldBindJSON(&request); err != nil {
|
||||
handler.Error(c,
|
||||
stacktrace.Propagate(ente.ErrBadRequest, fmt.Sprintf("Request binding failed %s", err)))
|
||||
return
|
||||
}
|
||||
request.OwnerID = auth.GetUserID(c.Request.Header)
|
||||
_, err := h.Controller.Delete(c, request)
|
||||
if err != nil {
|
||||
handler.Error(c, stacktrace.Propagate(err, "Failed to delete locationTag"))
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
// GetDiff handler for fetching diff of location tag changes
|
||||
func (h *LocationTagHandler) GetDiff(c *gin.Context) {
|
||||
var request ente.GetLocationTagDiffRequest
|
||||
if err := c.ShouldBindQuery(&request); err != nil {
|
||||
handler.Error(c,
|
||||
stacktrace.Propagate(ente.ErrBadRequest, fmt.Sprintf("Request binding failed %s", err)))
|
||||
return
|
||||
}
|
||||
request.OwnerID = auth.GetUserID(c.Request.Header)
|
||||
locationTags, err := h.Controller.GetDiff(c, request)
|
||||
if err != nil {
|
||||
handler.Error(c, stacktrace.Propagate(err, "Failed to fetch locationTag diff"))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"diff": locationTags,
|
||||
})
|
||||
}
|
||||
@@ -488,6 +488,15 @@ func (c *CollectionController) GetDiffV2(ctx *gin.Context, cID int64, userID int
|
||||
if diff[idx].OwnerID != userID {
|
||||
diff[idx].MagicMetadata = nil
|
||||
}
|
||||
if diff[idx].Metadata.EncryptedData == "-" && !diff[idx].IsDeleted {
|
||||
// This indicates that the file is deleted, but we still have a stale entry in the collection
|
||||
log.WithFields(log.Fields{
|
||||
"file_id": diff[idx].ID,
|
||||
"collection_id": cID,
|
||||
"updated_at": diff[idx].UpdationTime,
|
||||
}).Warning("stale collection_file found")
|
||||
diff[idx].IsDeleted = true
|
||||
}
|
||||
}
|
||||
return diff, hasMore, nil
|
||||
}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
package locationtag
|
||||
|
||||
import (
|
||||
"github.com/ente-io/museum/ente"
|
||||
"github.com/ente-io/museum/pkg/repo/locationtag"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Controller is interface for exposing business logic related to location tags
|
||||
type Controller struct {
|
||||
Repo *locationtag.Repository
|
||||
}
|
||||
|
||||
// Create a new location tag in the system
|
||||
func (c *Controller) Create(ctx *gin.Context, req ente.LocationTag) (ente.LocationTag, error) {
|
||||
return c.Repo.Create(ctx, req)
|
||||
}
|
||||
func (c *Controller) Update(ctx *gin.Context, req ente.LocationTag) (ente.LocationTag, error) {
|
||||
// todo: verify ownership before updating
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
// Delete the location tag for the given id and ownerId
|
||||
func (c *Controller) Delete(ctx *gin.Context, req ente.DeleteLocationTagRequest) (bool, error) {
|
||||
return c.Repo.Delete(ctx, req.ID.String(), req.OwnerID)
|
||||
}
|
||||
|
||||
// GetDiff fetches the locationTags which have changed after the specified time
|
||||
func (c *Controller) GetDiff(ctx *gin.Context, req ente.GetLocationTagDiffRequest) ([]ente.LocationTag, error) {
|
||||
return c.Repo.GetDiff(ctx, req.OwnerID, *req.SinceTime, req.Limit)
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
package locationtag
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"github.com/ente-io/museum/ente"
|
||||
"github.com/ente-io/stacktrace"
|
||||
"github.com/google/uuid"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Repository defines the methods for inserting, updating and retrieving
|
||||
// locationTag related entities from the underlying repository
|
||||
type Repository struct {
|
||||
DB *sql.DB
|
||||
}
|
||||
|
||||
// Create inserts a new &{ente.LocationTag} entry
|
||||
func (r *Repository) Create(ctx context.Context, locationTag ente.LocationTag) (ente.LocationTag, error) {
|
||||
err := r.DB.QueryRow(`INSERT into location_tag(
|
||||
id,
|
||||
user_id,
|
||||
encrypted_key,
|
||||
key_decryption_nonce,
|
||||
attributes) VALUES ($1,$2,$3,$4,$5) RETURNING id,created_at,updated_at`,
|
||||
uuid.New(), //$1 id
|
||||
locationTag.OwnerID, // $2 user_id
|
||||
locationTag.EncryptedKey, // $3 encrypted_key
|
||||
locationTag.KeyDecryptionNonce, // $4 key_decryption_nonce
|
||||
locationTag.Attributes). // %5 attributes
|
||||
Scan(&locationTag.ID, &locationTag.CreatedAt, &locationTag.UpdatedAt)
|
||||
if err != nil {
|
||||
return ente.LocationTag{}, stacktrace.Propagate(err, "Failed to create locationTag")
|
||||
}
|
||||
return locationTag, nil
|
||||
}
|
||||
|
||||
// GetDiff returns the &{[]ente.LocationTag} which have been added or
|
||||
// modified after the given sinceTime
|
||||
func (r *Repository) GetDiff(ctx context.Context, ownerID int64, sinceTime int64, limit int16) ([]ente.LocationTag, error) {
|
||||
rows, err := r.DB.Query(`SELECT
|
||||
id, user_id, provider, encrypted_key, key_decryption_nonce,
|
||||
attributes, is_deleted, created_at, updated_at
|
||||
FROM location_tag
|
||||
WHERE user_id = $1
|
||||
and updated_at > $2
|
||||
ORDER BY updated_at
|
||||
LIMIT $3`,
|
||||
ownerID, // $1
|
||||
sinceTime, // %2
|
||||
limit, // $3
|
||||
)
|
||||
if err != nil {
|
||||
return nil, stacktrace.Propagate(err, "GetDiff query failed")
|
||||
}
|
||||
return convertRowsToLocationTags(rows)
|
||||
}
|
||||
|
||||
func (r *Repository) Delete(ctx context.Context, id string, ownerID int64) (bool, error) {
|
||||
_, err := r.DB.ExecContext(ctx,
|
||||
`UPDATE location_tag SET is_deleted=$1, attributes=$2 where id=$3 and user_id = $4`,
|
||||
true, `{}`, // $1 is_deleted, $2 attr
|
||||
id, ownerID) // $3 tagId, $4 ownerID
|
||||
if err != nil {
|
||||
return false, stacktrace.Propagate(err, fmt.Sprintf("faield to delele tag with id=%s", id))
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func convertRowsToLocationTags(rows *sql.Rows) ([]ente.LocationTag, error) {
|
||||
defer func() {
|
||||
if err := rows.Close(); err != nil {
|
||||
logrus.Error(err)
|
||||
}
|
||||
}()
|
||||
locationTags := make([]ente.LocationTag, 0)
|
||||
for rows.Next() {
|
||||
tag := ente.LocationTag{}
|
||||
err := rows.Scan(
|
||||
&tag.ID, &tag.OwnerID, &tag.Provider, &tag.EncryptedKey, &tag.KeyDecryptionNonce,
|
||||
&tag.Attributes, &tag.IsDeleted, &tag.CreatedAt, &tag.UpdatedAt)
|
||||
if err != nil {
|
||||
return nil, stacktrace.Propagate(err, "Failed to convert rowToLocationTag")
|
||||
}
|
||||
locationTags = append(locationTags, tag)
|
||||
}
|
||||
return locationTags, nil
|
||||
}
|
||||
@@ -32,7 +32,7 @@ yarn dev
|
||||
|
||||
That's it. The web app will automatically hot reload when you make changes.
|
||||
|
||||
If you're new to web development and unsure about how to get started, or are
|
||||
If you're new to web development and unsure about how to get started, or are
|
||||
facing some problems when running the above steps, see [docs/new](docs/new.md).
|
||||
|
||||
## Other apps
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { setupI18n } from "@/ui/i18n";
|
||||
import { CacheProvider } from "@emotion/react";
|
||||
import { APPS, APP_TITLES } from "@ente/shared/apps/constants";
|
||||
import { EnteAppProps } from "@ente/shared/apps/types";
|
||||
import { Overlay } from "@ente/shared/components/Container";
|
||||
import DialogBoxV2 from "@ente/shared/components/DialogBoxV2";
|
||||
import {
|
||||
@@ -15,9 +13,9 @@ import HTTPService from "@ente/shared/network/HTTPService";
|
||||
import { LS_KEYS, getData } from "@ente/shared/storage/localStorage";
|
||||
import { getTheme } from "@ente/shared/themes";
|
||||
import { THEME_COLOR } from "@ente/shared/themes/constants";
|
||||
import createEmotionCache from "@ente/shared/themes/createEmotionCache";
|
||||
import { CssBaseline, useMediaQuery } from "@mui/material";
|
||||
import { ThemeProvider } from "@mui/material/styles";
|
||||
import { AppProps } from "next/app";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
import { createContext, useEffect, useState } from "react";
|
||||
@@ -31,10 +29,7 @@ interface AppContextProps {
|
||||
|
||||
export const AppContext = createContext<AppContextProps>({} as AppContextProps);
|
||||
|
||||
// Client-side cache, shared for the whole session of the user in the browser.
|
||||
const clientSideEmotionCache = createEmotionCache();
|
||||
|
||||
export default function App(props: EnteAppProps) {
|
||||
export default function App(props: AppProps) {
|
||||
const [isI18nReady, setIsI18nReady] = useState<boolean>(false);
|
||||
|
||||
const [showNavbar, setShowNavBar] = useState(false);
|
||||
@@ -54,11 +49,7 @@ export default function App(props: EnteAppProps) {
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const {
|
||||
Component,
|
||||
emotionCache = clientSideEmotionCache,
|
||||
pageProps,
|
||||
} = props;
|
||||
const { Component, pageProps } = props;
|
||||
|
||||
const [themeColor] = useLocalState(LS_KEYS.THEME, THEME_COLOR.DARK);
|
||||
|
||||
@@ -87,7 +78,7 @@ export default function App(props: EnteAppProps) {
|
||||
|
||||
// TODO: Localise APP_TITLES
|
||||
return (
|
||||
<CacheProvider value={emotionCache}>
|
||||
<>
|
||||
<Head>
|
||||
<title>{APP_TITLES.get(APPS.ACCOUNTS)}</title>
|
||||
<meta
|
||||
@@ -128,9 +119,9 @@ export default function App(props: EnteAppProps) {
|
||||
</Overlay>
|
||||
)}
|
||||
{showNavbar && <AppNavbar isMobile={isMobile} />}
|
||||
<Component {...pageProps} />
|
||||
{isI18nReady && <Component {...pageProps} />}
|
||||
</AppContext.Provider>
|
||||
</ThemeProvider>
|
||||
</CacheProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
import DocumentPage, {
|
||||
EnteDocumentProps,
|
||||
} from "@ente/shared/next/pages/_document";
|
||||
import DocumentPage from "@ente/shared/next/pages/_document";
|
||||
|
||||
export default function Document(props: EnteDocumentProps) {
|
||||
return <DocumentPage {...props} />;
|
||||
}
|
||||
export default DocumentPage;
|
||||
|
||||
@@ -150,21 +150,6 @@ body {
|
||||
background-color: #51cd7c;
|
||||
}
|
||||
|
||||
.carousel-inner {
|
||||
padding-bottom: 50px !important;
|
||||
}
|
||||
|
||||
.carousel-indicators li {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.carousel-indicators .active {
|
||||
background-color: #51cd7c;
|
||||
}
|
||||
|
||||
div.otp-input input {
|
||||
width: 36px !important;
|
||||
height: 36px;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import AppNavbar from "@ente/shared/components/Navbar/app";
|
||||
import { t } from "i18next";
|
||||
import { createContext, useEffect, useRef, useState } from "react";
|
||||
|
||||
import { setupI18n } from "@/ui/i18n";
|
||||
import {
|
||||
APPS,
|
||||
APP_TITLES,
|
||||
CLIENT_PACKAGE_NAMES,
|
||||
} from "@ente/shared/apps/constants";
|
||||
import { Overlay } from "@ente/shared/components/Container";
|
||||
import DialogBoxV2 from "@ente/shared/components/DialogBoxV2";
|
||||
import {
|
||||
@@ -10,32 +12,26 @@ import {
|
||||
} from "@ente/shared/components/DialogBoxV2/types";
|
||||
import EnteSpinner from "@ente/shared/components/EnteSpinner";
|
||||
import { MessageContainer } from "@ente/shared/components/MessageContainer";
|
||||
import AppNavbar from "@ente/shared/components/Navbar/app";
|
||||
import { PHOTOS_PAGES as PAGES } from "@ente/shared/constants/pages";
|
||||
import { useLocalState } from "@ente/shared/hooks/useLocalState";
|
||||
import {
|
||||
clearLogsIfLocalStorageLimitExceeded,
|
||||
logStartupMessage,
|
||||
} from "@ente/shared/logging/web";
|
||||
import HTTPService from "@ente/shared/network/HTTPService";
|
||||
import { LS_KEYS } from "@ente/shared/storage/localStorage";
|
||||
import { CssBaseline, useMediaQuery } from "@mui/material";
|
||||
import { ThemeProvider } from "@mui/material/styles";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
import LoadingBar from "react-top-loading-bar";
|
||||
|
||||
import { setupI18n } from "@/ui/i18n";
|
||||
import { CacheProvider } from "@emotion/react";
|
||||
import {
|
||||
APP_TITLES,
|
||||
APPS,
|
||||
CLIENT_PACKAGE_NAMES,
|
||||
} from "@ente/shared/apps/constants";
|
||||
import { EnteAppProps } from "@ente/shared/apps/types";
|
||||
import { PHOTOS_PAGES as PAGES } from "@ente/shared/constants/pages";
|
||||
import { useLocalState } from "@ente/shared/hooks/useLocalState";
|
||||
import { getTheme } from "@ente/shared/themes";
|
||||
import { THEME_COLOR } from "@ente/shared/themes/constants";
|
||||
import createEmotionCache from "@ente/shared/themes/createEmotionCache";
|
||||
import { SetTheme } from "@ente/shared/themes/types";
|
||||
import { CssBaseline, useMediaQuery } from "@mui/material";
|
||||
import { ThemeProvider } from "@mui/material/styles";
|
||||
import { t } from "i18next";
|
||||
import { AppProps } from "next/app";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
import { createContext, useEffect, useRef, useState } from "react";
|
||||
import LoadingBar from "react-top-loading-bar";
|
||||
import "../../public/css/global.css";
|
||||
|
||||
type AppContextType = {
|
||||
@@ -51,15 +47,8 @@ type AppContextType = {
|
||||
|
||||
export const AppContext = createContext<AppContextType>(null);
|
||||
|
||||
// Client-side cache, shared for the whole session of the user in the browser.
|
||||
const clientSideEmotionCache = createEmotionCache();
|
||||
|
||||
export default function App(props: EnteAppProps) {
|
||||
const {
|
||||
Component,
|
||||
emotionCache = clientSideEmotionCache,
|
||||
pageProps,
|
||||
} = props;
|
||||
export default function App(props: AppProps) {
|
||||
const { Component, pageProps } = props;
|
||||
const router = useRouter();
|
||||
const [isI18nReady, setIsI18nReady] = useState<boolean>(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -141,7 +130,7 @@ export default function App(props: EnteAppProps) {
|
||||
});
|
||||
|
||||
return (
|
||||
<CacheProvider value={emotionCache}>
|
||||
<>
|
||||
<Head>
|
||||
<title>
|
||||
{isI18nReady
|
||||
@@ -195,9 +184,11 @@ export default function App(props: EnteAppProps) {
|
||||
<EnteSpinner />
|
||||
</Overlay>
|
||||
)}
|
||||
<Component setLoading={setLoading} {...pageProps} />
|
||||
{isI18nReady && (
|
||||
<Component setLoading={setLoading} {...pageProps} />
|
||||
)}
|
||||
</AppContext.Provider>
|
||||
</ThemeProvider>
|
||||
</CacheProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
import DocumentPage, {
|
||||
EnteDocumentProps,
|
||||
} from "@ente/shared/next/pages/_document";
|
||||
import DocumentPage from "@ente/shared/next/pages/_document";
|
||||
|
||||
export default function Document(props: EnteDocumentProps) {
|
||||
return <DocumentPage {...props} />;
|
||||
}
|
||||
export default DocumentPage;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { styled } from "@mui/material";
|
||||
|
||||
const colourPool = [
|
||||
"#87CEFA", // Light Blue
|
||||
"#90EE90", // Light Green
|
||||
@@ -23,44 +25,41 @@ const colourPool = [
|
||||
|
||||
export default function LargeType({ chars }: { chars: string[] }) {
|
||||
return (
|
||||
<table
|
||||
style={{
|
||||
fontSize: "4rem",
|
||||
fontWeight: "bold",
|
||||
fontFamily: "monospace",
|
||||
display: "flex",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<Container style={{}}>
|
||||
{chars.map((char, i) => (
|
||||
<tr
|
||||
<span
|
||||
key={i}
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
padding: "0.5rem",
|
||||
// alternating background
|
||||
backgroundColor: i % 2 === 0 ? "#2e2e2e" : "#5e5e5e",
|
||||
// varying colors
|
||||
color: colourPool[i % colourPool.length],
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
color: colourPool[i % colourPool.length],
|
||||
lineHeight: 1.2,
|
||||
}}
|
||||
>
|
||||
{char}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
fontSize: "1rem",
|
||||
}}
|
||||
>
|
||||
{i + 1}
|
||||
</span>
|
||||
</tr>
|
||||
{char}
|
||||
</span>
|
||||
))}
|
||||
</table>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
const Container = styled("div")`
|
||||
font-size: 4rem;
|
||||
font-weight: bold;
|
||||
font-family: monospace;
|
||||
|
||||
line-height: 1.2;
|
||||
|
||||
/*
|
||||
* - We want them to be spans so that when the text is copy pasted, there
|
||||
* is no extra whitespace inserted.
|
||||
*
|
||||
* - But we also want them to have a block level padding.
|
||||
*
|
||||
* To achieve both these goals, make them inline-blocks
|
||||
*/
|
||||
span {
|
||||
display: inline-block;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
`;
|
||||
|
||||
95
web/apps/cast/src/components/PhotoAuditorium.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import { SlideshowContext } from "pages/slideshow";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
|
||||
export default function PhotoAuditorium({
|
||||
url,
|
||||
nextSlideUrl,
|
||||
}: {
|
||||
url: string;
|
||||
nextSlideUrl: string;
|
||||
}) {
|
||||
const { showNextSlide } = useContext(SlideshowContext);
|
||||
|
||||
const [showPreloadedNextSlide, setShowPreloadedNextSlide] = useState(false);
|
||||
const [nextSlidePrerendered, setNextSlidePrerendered] = useState(false);
|
||||
const [prerenderTime, setPrerenderTime] = useState<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let timeout: NodeJS.Timeout;
|
||||
let timeout2: NodeJS.Timeout;
|
||||
|
||||
if (nextSlidePrerendered) {
|
||||
const elapsedTime = prerenderTime ? Date.now() - prerenderTime : 0;
|
||||
const delayTime = Math.max(10000 - elapsedTime, 0);
|
||||
|
||||
if (elapsedTime >= 10000) {
|
||||
setShowPreloadedNextSlide(true);
|
||||
} else {
|
||||
timeout = setTimeout(() => {
|
||||
setShowPreloadedNextSlide(true);
|
||||
}, delayTime);
|
||||
}
|
||||
|
||||
if (showNextSlide) {
|
||||
timeout2 = setTimeout(() => {
|
||||
showNextSlide();
|
||||
setNextSlidePrerendered(false);
|
||||
setPrerenderTime(null);
|
||||
setShowPreloadedNextSlide(false);
|
||||
}, delayTime);
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (timeout) clearTimeout(timeout);
|
||||
if (timeout2) clearTimeout(timeout2);
|
||||
};
|
||||
}, [nextSlidePrerendered, showNextSlide, prerenderTime]);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: "100vw",
|
||||
height: "100vh",
|
||||
backgroundImage: `url(${url})`,
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
backgroundRepeat: "no-repeat",
|
||||
backgroundBlendMode: "multiply",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
backdropFilter: "blur(10px)",
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={url}
|
||||
style={{
|
||||
maxWidth: "100%",
|
||||
maxHeight: "100%",
|
||||
display: showPreloadedNextSlide ? "none" : "block",
|
||||
}}
|
||||
/>
|
||||
<img
|
||||
src={nextSlideUrl}
|
||||
style={{
|
||||
maxWidth: "100%",
|
||||
maxHeight: "100%",
|
||||
display: showPreloadedNextSlide ? "block" : "none",
|
||||
}}
|
||||
onLoad={() => {
|
||||
setNextSlidePrerendered(true);
|
||||
setPrerenderTime(Date.now());
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -20,9 +20,9 @@ export default function PhotoAuditorium({
|
||||
|
||||
if (nextSlidePrerendered) {
|
||||
const elapsedTime = prerenderTime ? Date.now() - prerenderTime : 0;
|
||||
const delayTime = Math.max(5000 - elapsedTime, 0);
|
||||
const delayTime = Math.max(10000 - elapsedTime, 0);
|
||||
|
||||
if (elapsedTime >= 5000) {
|
||||
if (elapsedTime >= 10000) {
|
||||
setShowPreloadedNextSlide(true);
|
||||
} else {
|
||||
timeout = setTimeout(() => {
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function TimerBar({ percentage }: { percentage: number }) {
|
||||
const okColor = "#75C157";
|
||||
const warningColor = "#FFC000";
|
||||
const lateColor = "#FF0000";
|
||||
|
||||
const [backgroundColor, setBackgroundColor] = useState(okColor);
|
||||
|
||||
useEffect(() => {
|
||||
if (percentage >= 40) {
|
||||
setBackgroundColor(okColor);
|
||||
} else if (percentage >= 20) {
|
||||
setBackgroundColor(warningColor);
|
||||
} else {
|
||||
setBackgroundColor(lateColor);
|
||||
}
|
||||
}, [percentage]);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: `${percentage}%`, // Set the width based on the time left
|
||||
height: "10px", // Same as the border thickness
|
||||
backgroundColor, // The color of the moving border
|
||||
transition: "width 1s linear", // Smooth transition for the width change
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
import { getAlbumsURL } from "@ente/shared/network/api";
|
||||
import { runningInBrowser } from "@ente/shared/platform";
|
||||
import { PAGES } from "constants/pages";
|
||||
|
||||
export enum APPS {
|
||||
PHOTOS = "PHOTOS",
|
||||
AUTH = "AUTH",
|
||||
ALBUMS = "ALBUMS",
|
||||
}
|
||||
|
||||
export const ALLOWED_APP_PAGES = new Map([
|
||||
[APPS.ALBUMS, [PAGES.SHARED_ALBUMS, PAGES.ROOT]],
|
||||
[
|
||||
APPS.AUTH,
|
||||
[
|
||||
PAGES.ROOT,
|
||||
PAGES.LOGIN,
|
||||
PAGES.SIGNUP,
|
||||
PAGES.VERIFY,
|
||||
PAGES.CREDENTIALS,
|
||||
PAGES.RECOVER,
|
||||
PAGES.CHANGE_PASSWORD,
|
||||
PAGES.GENERATE,
|
||||
PAGES.AUTH,
|
||||
PAGES.TWO_FACTOR_VERIFY,
|
||||
PAGES.TWO_FACTOR_RECOVER,
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
export const CLIENT_PACKAGE_NAMES = new Map([
|
||||
[APPS.ALBUMS, "io.ente.albums.web"],
|
||||
[APPS.PHOTOS, "io.ente.photos.web"],
|
||||
[APPS.AUTH, "io.ente.auth.web"],
|
||||
]);
|
||||
|
||||
export const getAppNameAndTitle = () => {
|
||||
if (!runningInBrowser()) {
|
||||
return {};
|
||||
}
|
||||
const currentURL = new URL(window.location.href);
|
||||
const albumsURL = new URL(getAlbumsURL());
|
||||
if (currentURL.origin === albumsURL.origin) {
|
||||
return { name: APPS.ALBUMS, title: "ente Photos" };
|
||||
} else {
|
||||
return { name: APPS.PHOTOS, title: "ente Photos" };
|
||||
}
|
||||
};
|
||||
|
||||
export const getAppTitle = () => {
|
||||
return getAppNameAndTitle().title;
|
||||
};
|
||||
|
||||
export const getAppName = () => {
|
||||
return getAppNameAndTitle().name;
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
export enum CACHES {
|
||||
THUMBS = "thumbs",
|
||||
FACE_CROPS = "face-crops",
|
||||
FILES = "files",
|
||||
}
|
||||
@@ -1,14 +1,3 @@
|
||||
export const MIN_EDITED_CREATION_TIME = new Date(1800, 0, 1);
|
||||
export const MAX_EDITED_CREATION_TIME = new Date();
|
||||
|
||||
export const MAX_EDITED_FILE_NAME_LENGTH = 100;
|
||||
export const MAX_CAPTION_SIZE = 5000;
|
||||
|
||||
export const TYPE_HEIC = "heic";
|
||||
export const TYPE_HEIF = "heif";
|
||||
export const TYPE_JPEG = "jpeg";
|
||||
export const TYPE_JPG = "jpg";
|
||||
|
||||
export enum FILE_TYPE {
|
||||
IMAGE,
|
||||
VIDEO,
|
||||
@@ -29,15 +18,3 @@ export const RAW_FORMATS = [
|
||||
"dng",
|
||||
"tif",
|
||||
];
|
||||
export const SUPPORTED_RAW_FORMATS = [
|
||||
"heic",
|
||||
"rw2",
|
||||
"tiff",
|
||||
"arw",
|
||||
"cr3",
|
||||
"cr2",
|
||||
"nef",
|
||||
"psd",
|
||||
"dng",
|
||||
"tif",
|
||||
];
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
export const GAP_BTW_TILES = 4;
|
||||
export const DATE_CONTAINER_HEIGHT = 48;
|
||||
export const SIZE_AND_COUNT_CONTAINER_HEIGHT = 72;
|
||||
export const IMAGE_CONTAINER_MAX_HEIGHT = 180;
|
||||
export const IMAGE_CONTAINER_MAX_WIDTH = 180;
|
||||
export const MIN_COLUMNS = 4;
|
||||
export const SPACE_BTW_DATES = 44;
|
||||
export const SPACE_BTW_DATES_TO_IMAGE_CONTAINER_WIDTH_RATIO = 0.244;
|
||||
|
||||
export enum PLAN_PERIOD {
|
||||
MONTH = "month",
|
||||
YEAR = "year",
|
||||
}
|
||||
|
||||
export const SYNC_INTERVAL_IN_MICROSECONDS = 1000 * 60 * 5; // 5 minutes
|
||||
@@ -1,20 +0,0 @@
|
||||
export enum PAGES {
|
||||
CHANGE_EMAIL = "/change-email",
|
||||
CHANGE_PASSWORD = "/change-password",
|
||||
CREDENTIALS = "/credentials",
|
||||
GALLERY = "/gallery",
|
||||
GENERATE = "/generate",
|
||||
LOGIN = "/login",
|
||||
RECOVER = "/recover",
|
||||
SIGNUP = "/signup",
|
||||
TWO_FACTOR_SETUP = "/two-factor/setup",
|
||||
TWO_FACTOR_VERIFY = "/two-factor/verify",
|
||||
TWO_FACTOR_RECOVER = "/two-factor/recover",
|
||||
VERIFY = "/verify",
|
||||
ROOT = "/",
|
||||
SHARED_ALBUMS = "/shared-albums",
|
||||
// ML_DEBUG = '/ml-debug',
|
||||
DEDUPLICATE = "/deduplicate",
|
||||
// AUTH page is used to show (auth)enticator codes
|
||||
AUTH = "/auth",
|
||||
}
|
||||
@@ -1,11 +1,5 @@
|
||||
import { ENCRYPTION_CHUNK_SIZE } from "@ente/shared/crypto/constants";
|
||||
import { FILE_TYPE } from "constants/file";
|
||||
import {
|
||||
FileTypeInfo,
|
||||
ImportSuggestion,
|
||||
Location,
|
||||
ParsedExtractedMetadata,
|
||||
} from "types/upload";
|
||||
import { FileTypeInfo } from "types/upload";
|
||||
|
||||
// list of format that were missed by type-detection for some files.
|
||||
export const WHITELISTED_FILE_FORMATS: FileTypeInfo[] = [
|
||||
@@ -45,98 +39,3 @@ export const WHITELISTED_FILE_FORMATS: FileTypeInfo[] = [
|
||||
];
|
||||
|
||||
export const KNOWN_NON_MEDIA_FORMATS = ["xmp", "html", "txt"];
|
||||
|
||||
export const EXIFLESS_FORMATS = ["gif", "bmp"];
|
||||
|
||||
// this is the chunk size of the un-encrypted file which is read and encrypted before uploading it as a single part.
|
||||
export const MULTIPART_PART_SIZE = 20 * 1024 * 1024;
|
||||
|
||||
export const FILE_READER_CHUNK_SIZE = ENCRYPTION_CHUNK_SIZE;
|
||||
|
||||
export const FILE_CHUNKS_COMBINED_FOR_A_UPLOAD_PART = Math.floor(
|
||||
MULTIPART_PART_SIZE / FILE_READER_CHUNK_SIZE,
|
||||
);
|
||||
|
||||
export const RANDOM_PERCENTAGE_PROGRESS_FOR_PUT = () => 90 + 10 * Math.random();
|
||||
|
||||
export const NULL_LOCATION: Location = { latitude: null, longitude: null };
|
||||
|
||||
export enum UPLOAD_STAGES {
|
||||
START,
|
||||
READING_GOOGLE_METADATA_FILES,
|
||||
EXTRACTING_METADATA,
|
||||
UPLOADING,
|
||||
CANCELLING,
|
||||
FINISH,
|
||||
}
|
||||
|
||||
export enum UPLOAD_STRATEGY {
|
||||
SINGLE_COLLECTION,
|
||||
COLLECTION_PER_FOLDER,
|
||||
}
|
||||
|
||||
export enum UPLOAD_RESULT {
|
||||
FAILED,
|
||||
ALREADY_UPLOADED,
|
||||
UNSUPPORTED,
|
||||
BLOCKED,
|
||||
TOO_LARGE,
|
||||
LARGER_THAN_AVAILABLE_STORAGE,
|
||||
UPLOADED,
|
||||
UPLOADED_WITH_STATIC_THUMBNAIL,
|
||||
ADDED_SYMLINK,
|
||||
}
|
||||
|
||||
export enum PICKED_UPLOAD_TYPE {
|
||||
FILES = "files",
|
||||
FOLDERS = "folders",
|
||||
ZIPS = "zips",
|
||||
}
|
||||
|
||||
export const MAX_FILE_SIZE_SUPPORTED = 4 * 1024 * 1024 * 1024; // 4 GB
|
||||
|
||||
export const LIVE_PHOTO_ASSET_SIZE_LIMIT = 20 * 1024 * 1024; // 20MB
|
||||
|
||||
export const NULL_EXTRACTED_METADATA: ParsedExtractedMetadata = {
|
||||
location: NULL_LOCATION,
|
||||
creationTime: null,
|
||||
width: null,
|
||||
height: null,
|
||||
};
|
||||
|
||||
export const A_SEC_IN_MICROSECONDS = 1e6;
|
||||
|
||||
export const DEFAULT_IMPORT_SUGGESTION: ImportSuggestion = {
|
||||
rootFolderName: "",
|
||||
hasNestedFolders: false,
|
||||
hasRootLevelFileWithFolder: false,
|
||||
};
|
||||
|
||||
export const BLACK_THUMBNAIL_BASE64 =
|
||||
"/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEB" +
|
||||
"AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQ" +
|
||||
"EBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARC" +
|
||||
"ACWASwDAREAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUF" +
|
||||
"BAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk" +
|
||||
"6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztL" +
|
||||
"W2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAA" +
|
||||
"AAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVY" +
|
||||
"nLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImK" +
|
||||
"kpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oAD" +
|
||||
"AMBAAIRAxEAPwD/AD/6ACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKA" +
|
||||
"CgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACg" +
|
||||
"AoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKAC" +
|
||||
"gAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAo" +
|
||||
"AKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACg" +
|
||||
"AoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACg" +
|
||||
"AoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKA" +
|
||||
"CgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKA" +
|
||||
"CgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoA" +
|
||||
"KACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACg" +
|
||||
"AoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAo" +
|
||||
"AKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKA" +
|
||||
"CgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAK" +
|
||||
"ACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoA" +
|
||||
"KACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAo" +
|
||||
"AKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAo" +
|
||||
"AKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgD/9k=";
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
export const ENTE_WEBSITE_LINK = "https://ente.io";
|
||||
|
||||
export const ML_BLOG_LINK = "https://ente.io/blog/desktop-ml-beta";
|
||||
|
||||
export const FACE_SEARCH_PRIVACY_POLICY_LINK =
|
||||
"https://ente.io/privacy#8-biometric-information-privacy-policy";
|
||||
|
||||
export const SUPPORT_EMAIL = "support@ente.io";
|
||||
|
||||
export const APP_DOWNLOAD_URL = "https://ente.io/download/desktop";
|
||||
|
||||
export const FEEDBACK_EMAIL = "feedback@ente.io";
|
||||
|
||||
export const DELETE_ACCOUNT_EMAIL = "account-deletion@ente.io";
|
||||
|
||||
export const WEB_ROADMAP_URL = "https://github.com/ente-io/ente/discussions";
|
||||
|
||||
export const DESKTOP_ROADMAP_URL =
|
||||
"https://github.com/ente-io/ente/discussions";
|
||||
@@ -59,21 +59,12 @@ export default function Slideshow() {
|
||||
}
|
||||
};
|
||||
|
||||
const init = async () => {
|
||||
try {
|
||||
const castToken = window.localStorage.getItem("castToken");
|
||||
setCastToken(castToken);
|
||||
} catch (e) {
|
||||
logError(e, "error during sync");
|
||||
router.push("/");
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (castToken) {
|
||||
const intervalId = setInterval(() => {
|
||||
syncCastFiles(castToken);
|
||||
}, 5000);
|
||||
}, 10000);
|
||||
syncCastFiles(castToken);
|
||||
|
||||
return () => clearInterval(intervalId);
|
||||
}
|
||||
@@ -105,7 +96,20 @@ export default function Slideshow() {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
init();
|
||||
try {
|
||||
const castToken = window.localStorage.getItem("castToken");
|
||||
// Wait 2 seconds to ensure the green tick and the confirmation
|
||||
// message remains visible for at least 2 seconds before we start
|
||||
// the slideshow.
|
||||
const timeoutId = setTimeout(() => {
|
||||
setCastToken(castToken);
|
||||
}, 2000);
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
} catch (e) {
|
||||
logError(e, "error during sync");
|
||||
router.push("/");
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
export enum MS_KEYS {
|
||||
SRP_CONFIGURE_IN_PROGRESS = "srpConfigureInProgress",
|
||||
REDIRECT_URL = "redirectUrl",
|
||||
}
|
||||
|
||||
type StoreType = Map<Partial<MS_KEYS>, any>;
|
||||
|
||||
class InMemoryStore {
|
||||
private store: StoreType = new Map();
|
||||
|
||||
get(key: MS_KEYS) {
|
||||
return this.store.get(key);
|
||||
}
|
||||
|
||||
set(key: MS_KEYS, value: any) {
|
||||
this.store.set(key, value);
|
||||
}
|
||||
|
||||
delete(key: MS_KEYS) {
|
||||
this.store.delete(key);
|
||||
}
|
||||
|
||||
has(key: MS_KEYS) {
|
||||
return this.store.has(key);
|
||||
}
|
||||
clear() {
|
||||
this.store.clear();
|
||||
}
|
||||
}
|
||||
|
||||
export default new InMemoryStore();
|
||||
@@ -1,25 +0,0 @@
|
||||
import { LimitedCacheStorage } from "types/cache/index";
|
||||
|
||||
class cacheStorageFactory {
|
||||
getCacheStorage(): LimitedCacheStorage {
|
||||
return transformBrowserCacheStorageToLimitedCacheStorage(caches);
|
||||
}
|
||||
}
|
||||
|
||||
export const CacheStorageFactory = new cacheStorageFactory();
|
||||
|
||||
function transformBrowserCacheStorageToLimitedCacheStorage(
|
||||
caches: CacheStorage,
|
||||
): LimitedCacheStorage {
|
||||
return {
|
||||
async open(cacheName) {
|
||||
const cache = await caches.open(cacheName);
|
||||
return {
|
||||
match: cache.match.bind(cache),
|
||||
put: cache.put.bind(cache),
|
||||
delete: cache.delete.bind(cache),
|
||||
};
|
||||
},
|
||||
delete: caches.delete.bind(caches),
|
||||
};
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import { logError } from "@ente/shared/sentry";
|
||||
import { CacheStorageFactory } from "./cacheStorageFactory";
|
||||
|
||||
const SecurityError = "SecurityError";
|
||||
const INSECURE_OPERATION = "The operation is insecure.";
|
||||
async function openCache(cacheName: string) {
|
||||
try {
|
||||
return await CacheStorageFactory.getCacheStorage().open(cacheName);
|
||||
} catch (e) {
|
||||
// ignoring insecure operation error, as it is thrown in incognito mode in firefox
|
||||
if (e.name === SecurityError && e.message === INSECURE_OPERATION) {
|
||||
// no-op
|
||||
} else {
|
||||
// log and ignore, we don't want to break the caller flow, when cache is not available
|
||||
logError(e, "openCache failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
async function deleteCache(cacheName: string) {
|
||||
try {
|
||||
return await CacheStorageFactory.getCacheStorage().delete(cacheName);
|
||||
} catch (e) {
|
||||
// ignoring insecure operation error, as it is thrown in incognito mode in firefox
|
||||
if (e.name === SecurityError && e.message === INSECURE_OPERATION) {
|
||||
// no-op
|
||||
} else {
|
||||
// log and ignore, we don't want to break the caller flow, when cache is not available
|
||||
logError(e, "deleteCache failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const CacheStorageService = { open: openCache, delete: deleteCache };
|
||||
@@ -1,162 +1,14 @@
|
||||
import { EnteFile } from "types/file";
|
||||
import {
|
||||
createTypedObjectURL,
|
||||
generateStreamFromArrayBuffer,
|
||||
getRenderableFileURL,
|
||||
} from "utils/file";
|
||||
|
||||
import { CustomError } from "@ente/shared/error";
|
||||
import HTTPService from "@ente/shared/network/HTTPService";
|
||||
import { getCastFileURL, getCastThumbnailURL } from "@ente/shared/network/api";
|
||||
import { logError } from "@ente/shared/sentry";
|
||||
import { CACHES } from "constants/cache";
|
||||
import { getCastFileURL } from "@ente/shared/network/api";
|
||||
import { FILE_TYPE } from "constants/file";
|
||||
import { LimitedCache } from "types/cache";
|
||||
import { EnteFile } from "types/file";
|
||||
import ComlinkCryptoWorker from "utils/comlink/ComlinkCryptoWorker";
|
||||
import { CacheStorageService } from "./cache/cacheStorageService";
|
||||
import { generateStreamFromArrayBuffer } from "utils/file";
|
||||
|
||||
class CastDownloadManager {
|
||||
private fileObjectURLPromise = new Map<
|
||||
string,
|
||||
Promise<{ original: string[]; converted: string[] }>
|
||||
>();
|
||||
private thumbnailObjectURLPromise = new Map<number, Promise<string>>();
|
||||
|
||||
private fileDownloadProgress = new Map<number, number>();
|
||||
|
||||
private progressUpdater: (value: Map<number, number>) => void;
|
||||
|
||||
setProgressUpdater(progressUpdater: (value: Map<number, number>) => void) {
|
||||
this.progressUpdater = progressUpdater;
|
||||
}
|
||||
|
||||
private async getThumbnailCache() {
|
||||
try {
|
||||
const thumbnailCache = await CacheStorageService.open(
|
||||
CACHES.THUMBS,
|
||||
);
|
||||
return thumbnailCache;
|
||||
} catch (e) {
|
||||
return null;
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
public async getCachedThumbnail(
|
||||
file: EnteFile,
|
||||
thumbnailCache?: LimitedCache,
|
||||
) {
|
||||
try {
|
||||
if (!thumbnailCache) {
|
||||
thumbnailCache = await this.getThumbnailCache();
|
||||
}
|
||||
const cacheResp: Response = await thumbnailCache?.match(
|
||||
file.id.toString(),
|
||||
);
|
||||
|
||||
if (cacheResp) {
|
||||
return URL.createObjectURL(await cacheResp.blob());
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
logError(e, "failed to get cached thumbnail");
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public async getThumbnail(file: EnteFile, castToken: string) {
|
||||
try {
|
||||
if (!this.thumbnailObjectURLPromise.has(file.id)) {
|
||||
const downloadPromise = async () => {
|
||||
const thumbnailCache = await this.getThumbnailCache();
|
||||
const cachedThumb = await this.getCachedThumbnail(
|
||||
file,
|
||||
thumbnailCache,
|
||||
);
|
||||
if (cachedThumb) {
|
||||
return cachedThumb;
|
||||
}
|
||||
|
||||
const thumb = await this.downloadThumb(castToken, file);
|
||||
const thumbBlob = new Blob([thumb]);
|
||||
try {
|
||||
await thumbnailCache?.put(
|
||||
file.id.toString(),
|
||||
new Response(thumbBlob),
|
||||
);
|
||||
} catch (e) {
|
||||
// TODO: handle storage full exception.
|
||||
}
|
||||
return URL.createObjectURL(thumbBlob);
|
||||
};
|
||||
this.thumbnailObjectURLPromise.set(file.id, downloadPromise());
|
||||
}
|
||||
|
||||
return await this.thumbnailObjectURLPromise.get(file.id);
|
||||
} catch (e) {
|
||||
this.thumbnailObjectURLPromise.delete(file.id);
|
||||
logError(e, "get castDownloadManager preview Failed");
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private downloadThumb = async (castToken: string, file: EnteFile) => {
|
||||
const resp = await HTTPService.get(
|
||||
getCastThumbnailURL(file.id),
|
||||
null,
|
||||
{
|
||||
"X-Cast-Access-Token": castToken,
|
||||
},
|
||||
{ responseType: "arraybuffer" },
|
||||
);
|
||||
if (typeof resp.data === "undefined") {
|
||||
throw Error(CustomError.REQUEST_FAILED);
|
||||
}
|
||||
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
||||
const decrypted = await cryptoWorker.decryptThumbnail(
|
||||
new Uint8Array(resp.data),
|
||||
await cryptoWorker.fromB64(file.thumbnail.decryptionHeader),
|
||||
file.key,
|
||||
);
|
||||
return decrypted;
|
||||
};
|
||||
|
||||
getFile = async (file: EnteFile, castToken: string, forPreview = false) => {
|
||||
const fileKey = forPreview ? `${file.id}_preview` : `${file.id}`;
|
||||
try {
|
||||
const getFilePromise = async () => {
|
||||
const fileStream = await this.downloadFile(castToken, file);
|
||||
const fileBlob = await new Response(fileStream).blob();
|
||||
if (forPreview) {
|
||||
return await getRenderableFileURL(file, fileBlob);
|
||||
} else {
|
||||
const fileURL = await createTypedObjectURL(
|
||||
fileBlob,
|
||||
file.metadata.title,
|
||||
);
|
||||
return { converted: [fileURL], original: [fileURL] };
|
||||
}
|
||||
};
|
||||
|
||||
if (!this.fileObjectURLPromise.get(fileKey)) {
|
||||
this.fileObjectURLPromise.set(fileKey, getFilePromise());
|
||||
}
|
||||
const fileURLs = await this.fileObjectURLPromise.get(fileKey);
|
||||
return fileURLs;
|
||||
} catch (e) {
|
||||
this.fileObjectURLPromise.delete(fileKey);
|
||||
logError(e, "castDownloadManager failed to get file");
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
public async getCachedOriginalFile(file: EnteFile) {
|
||||
return await this.fileObjectURLPromise.get(file.id.toString());
|
||||
}
|
||||
|
||||
async downloadFile(castToken: string, file: EnteFile) {
|
||||
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
||||
const onDownloadProgress = this.trackDownloadProgress(file.id);
|
||||
|
||||
if (
|
||||
file.metadata.fileType === FILE_TYPE.IMAGE ||
|
||||
@@ -187,9 +39,6 @@ class CastDownloadManager {
|
||||
});
|
||||
const reader = resp.body.getReader();
|
||||
|
||||
const contentLength = +resp.headers.get("Content-Length");
|
||||
let downloadedBytes = 0;
|
||||
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
const decryptionHeader = await cryptoWorker.fromB64(
|
||||
@@ -208,11 +57,6 @@ class CastDownloadManager {
|
||||
reader.read().then(async ({ done, value }) => {
|
||||
// Is there more data to read?
|
||||
if (!done) {
|
||||
downloadedBytes += value.byteLength;
|
||||
onDownloadProgress({
|
||||
loaded: downloadedBytes,
|
||||
total: contentLength,
|
||||
});
|
||||
const buffer = new Uint8Array(
|
||||
data.byteLength + value.byteLength,
|
||||
);
|
||||
@@ -254,20 +98,6 @@ class CastDownloadManager {
|
||||
});
|
||||
return stream;
|
||||
}
|
||||
|
||||
trackDownloadProgress = (fileID: number) => {
|
||||
return (event: { loaded: number; total: number }) => {
|
||||
if (event.loaded === event.total) {
|
||||
this.fileDownloadProgress.delete(fileID);
|
||||
} else {
|
||||
this.fileDownloadProgress.set(
|
||||
fileID,
|
||||
Math.round((event.loaded * 100) / event.total),
|
||||
);
|
||||
}
|
||||
this.progressUpdater(new Map(this.fileDownloadProgress));
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export default new CastDownloadManager();
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { EventEmitter } from "eventemitter3";
|
||||
|
||||
// When registering event handlers,
|
||||
// handle errors to avoid unhandled rejection or propagation to emit call
|
||||
|
||||
export enum Events {
|
||||
LOGOUT = "logout",
|
||||
FILE_UPLOADED = "fileUploaded",
|
||||
LOCAL_FILES_UPDATED = "localFilesUpdated",
|
||||
}
|
||||
|
||||
export const eventBus = new EventEmitter<Events>();
|
||||
@@ -1,26 +1,19 @@
|
||||
// import isElectron from 'is-electron';
|
||||
// import { ElectronFFmpeg } from 'services/electron/ffmpeg';
|
||||
import { ElectronFile } from "types/upload";
|
||||
import ComlinkFFmpegWorker from "utils/comlink/ComlinkFFmpegWorker";
|
||||
|
||||
export interface IFFmpeg {
|
||||
run: (
|
||||
cmd: string[],
|
||||
inputFile: File | ElectronFile,
|
||||
inputFile: File,
|
||||
outputFilename: string,
|
||||
dontTimeout?: boolean,
|
||||
) => Promise<File | ElectronFile>;
|
||||
) => Promise<File>;
|
||||
}
|
||||
|
||||
class FFmpegFactory {
|
||||
private client: IFFmpeg;
|
||||
async getFFmpegClient() {
|
||||
if (!this.client) {
|
||||
// if (isElectron()) {
|
||||
// this.client = new ElectronFFmpeg();
|
||||
// } else {
|
||||
this.client = await ComlinkFFmpegWorker.getInstance();
|
||||
// }
|
||||
}
|
||||
return this.client;
|
||||
}
|
||||
|
||||
@@ -4,10 +4,9 @@ import {
|
||||
INPUT_PATH_PLACEHOLDER,
|
||||
OUTPUT_PATH_PLACEHOLDER,
|
||||
} from "constants/ffmpeg";
|
||||
import { ElectronFile } from "types/upload";
|
||||
import ffmpegFactory from "./ffmpegFactory";
|
||||
|
||||
export async function convertToMP4(file: File | ElectronFile) {
|
||||
export async function convertToMP4(file: File) {
|
||||
try {
|
||||
const ffmpegClient = await ffmpegFactory.getFFmpegClient();
|
||||
return await ffmpegClient.run(
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { logError } from "@ente/shared/sentry";
|
||||
import WasmHEICConverterService from "./wasmHeicConverter/wasmHEICConverterService";
|
||||
|
||||
class HeicConversionService {
|
||||
async convert(heicFileData: Blob): Promise<Blob> {
|
||||
try {
|
||||
return await WasmHEICConverterService.convert(heicFileData);
|
||||
} catch (e) {
|
||||
logError(e, "failed to convert heic file");
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
export default new HeicConversionService();
|
||||
@@ -30,16 +30,3 @@ export const decodeLivePhoto = async (file: EnteFile, zipBlob: Blob) => {
|
||||
}
|
||||
return livePhoto;
|
||||
};
|
||||
|
||||
export const encodeLivePhoto = async (livePhoto: LivePhoto) => {
|
||||
const zip = new JSZip();
|
||||
zip.file(
|
||||
"image" + getFileExtensionWithDot(livePhoto.imageNameTitle),
|
||||
livePhoto.image,
|
||||
);
|
||||
zip.file(
|
||||
"video" + getFileExtensionWithDot(livePhoto.videoNameTitle),
|
||||
livePhoto.video,
|
||||
);
|
||||
return await zip.generateAsync({ type: "uint8array" });
|
||||
};
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { logError } from "@ente/shared/sentry";
|
||||
import { convertBytesToHumanReadable } from "@ente/shared/utils/size";
|
||||
import { ElectronFile } from "types/upload";
|
||||
|
||||
export async function getUint8ArrayView(
|
||||
file: Blob | ElectronFile,
|
||||
): Promise<Uint8Array> {
|
||||
export async function getUint8ArrayView(file: Blob): Promise<Uint8Array> {
|
||||
try {
|
||||
return new Uint8Array(await file.arrayBuffer());
|
||||
} catch (e) {
|
||||
@@ -14,45 +11,3 @@ export async function getUint8ArrayView(
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
export function getFileStream(file: File, chunkSize: number) {
|
||||
const fileChunkReader = fileChunkReaderMaker(file, chunkSize);
|
||||
|
||||
const stream = new ReadableStream<Uint8Array>({
|
||||
async pull(controller: ReadableStreamDefaultController) {
|
||||
const chunk = await fileChunkReader.next();
|
||||
if (chunk.done) {
|
||||
controller.close();
|
||||
} else {
|
||||
controller.enqueue(chunk.value);
|
||||
}
|
||||
},
|
||||
});
|
||||
const chunkCount = Math.ceil(file.size / chunkSize);
|
||||
return {
|
||||
stream,
|
||||
chunkCount,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getElectronFileStream(
|
||||
file: ElectronFile,
|
||||
chunkSize: number,
|
||||
) {
|
||||
const chunkCount = Math.ceil(file.size / chunkSize);
|
||||
return {
|
||||
stream: await file.stream(),
|
||||
chunkCount,
|
||||
};
|
||||
}
|
||||
|
||||
async function* fileChunkReaderMaker(file: File, chunkSize: number) {
|
||||
let offset = 0;
|
||||
while (offset < file.size) {
|
||||
const blob = file.slice(offset, chunkSize + offset);
|
||||
const fileChunk = await getUint8ArrayView(blob);
|
||||
yield fileChunk;
|
||||
offset += chunkSize;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -6,37 +6,25 @@ import {
|
||||
KNOWN_NON_MEDIA_FORMATS,
|
||||
WHITELISTED_FILE_FORMATS,
|
||||
} from "constants/upload";
|
||||
import FileType, { FileTypeResult } from "file-type";
|
||||
import { ElectronFile, FileTypeInfo } from "types/upload";
|
||||
import FileType from "file-type";
|
||||
import { FileTypeInfo } from "types/upload";
|
||||
import { getFileExtension } from "utils/file";
|
||||
import { getUint8ArrayView } from "./readerService";
|
||||
|
||||
function getFileSize(file: File | ElectronFile) {
|
||||
return file.size;
|
||||
}
|
||||
|
||||
const TYPE_VIDEO = "video";
|
||||
const TYPE_IMAGE = "image";
|
||||
const CHUNK_SIZE_FOR_TYPE_DETECTION = 4100;
|
||||
|
||||
export async function getFileType(
|
||||
receivedFile: File | ElectronFile,
|
||||
): Promise<FileTypeInfo> {
|
||||
export async function getFileType(receivedFile: File): Promise<FileTypeInfo> {
|
||||
try {
|
||||
let fileType: FILE_TYPE;
|
||||
let typeResult: FileTypeResult;
|
||||
|
||||
if (receivedFile instanceof File) {
|
||||
typeResult = await extractFileType(receivedFile);
|
||||
} else {
|
||||
typeResult = await extractElectronFileType(receivedFile);
|
||||
}
|
||||
|
||||
const typeResult = await extractFileType(receivedFile);
|
||||
const mimTypeParts: string[] = typeResult.mime?.split("/");
|
||||
|
||||
if (mimTypeParts?.length !== 2) {
|
||||
throw Error(CustomError.INVALID_MIME_TYPE(typeResult.mime));
|
||||
}
|
||||
|
||||
switch (mimTypeParts[0]) {
|
||||
case TYPE_IMAGE:
|
||||
fileType = FILE_TYPE.IMAGE;
|
||||
@@ -54,7 +42,7 @@ export async function getFileType(
|
||||
};
|
||||
} catch (e) {
|
||||
const fileFormat = getFileExtension(receivedFile.name);
|
||||
const fileSize = convertBytesToHumanReadable(getFileSize(receivedFile));
|
||||
const fileSize = convertBytesToHumanReadable(receivedFile.size);
|
||||
const whiteListedFormat = WHITELISTED_FILE_FORMATS.find(
|
||||
(a) => a.exactType === fileFormat,
|
||||
);
|
||||
@@ -85,14 +73,6 @@ async function extractFileType(file: File) {
|
||||
return getFileTypeFromBuffer(fileDataChunk);
|
||||
}
|
||||
|
||||
async function extractElectronFileType(file: ElectronFile) {
|
||||
const stream = await file.stream();
|
||||
const reader = stream.getReader();
|
||||
const { value: fileDataChunk } = await reader.read();
|
||||
await reader.cancel();
|
||||
return getFileTypeFromBuffer(fileDataChunk);
|
||||
}
|
||||
|
||||
async function getFileTypeFromBuffer(buffer: Uint8Array) {
|
||||
const result = await FileType.fromBuffer(buffer);
|
||||
if (!result?.mime) {
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import * as HeicConvert from "heic-convert";
|
||||
import { getUint8ArrayView } from "services/readerService";
|
||||
|
||||
export async function convertHEIC(
|
||||
fileBlob: Blob,
|
||||
format: string,
|
||||
): Promise<Blob> {
|
||||
const filedata = await getUint8ArrayView(fileBlob);
|
||||
const result = await HeicConvert({ buffer: filedata, format });
|
||||
const convertedFileData = new Uint8Array(result);
|
||||
const convertedFileBlob = new Blob([convertedFileData]);
|
||||
return convertedFileBlob;
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
import { CustomError } from "@ente/shared/error";
|
||||
import { addLogLine } from "@ente/shared/logging";
|
||||
import { retryAsyncFunction } from "@ente/shared/promise";
|
||||
import { logError } from "@ente/shared/sentry";
|
||||
import QueueProcessor from "@ente/shared/utils/queueProcessor";
|
||||
import { convertBytesToHumanReadable } from "@ente/shared/utils/size";
|
||||
import { getDedicatedConvertWorker } from "utils/comlink/ComlinkConvertWorker";
|
||||
import { ComlinkWorker } from "utils/comlink/comlinkWorker";
|
||||
import { DedicatedConvertWorker } from "worker/convert.worker";
|
||||
|
||||
const WORKER_POOL_SIZE = 2;
|
||||
const MAX_CONVERSION_IN_PARALLEL = 1;
|
||||
const WAIT_TIME_BEFORE_NEXT_ATTEMPT_IN_MICROSECONDS = [100, 100];
|
||||
const WAIT_TIME_IN_MICROSECONDS = 30 * 1000;
|
||||
const BREATH_TIME_IN_MICROSECONDS = 1000;
|
||||
const CONVERT_FORMAT = "JPEG";
|
||||
|
||||
class HEICConverter {
|
||||
private convertProcessor = new QueueProcessor<Blob>(
|
||||
MAX_CONVERSION_IN_PARALLEL,
|
||||
);
|
||||
private workerPool: ComlinkWorker<typeof DedicatedConvertWorker>[] = [];
|
||||
private ready: Promise<void>;
|
||||
|
||||
constructor() {
|
||||
this.ready = this.init();
|
||||
}
|
||||
private async init() {
|
||||
this.workerPool = [];
|
||||
for (let i = 0; i < WORKER_POOL_SIZE; i++) {
|
||||
this.workerPool.push(getDedicatedConvertWorker());
|
||||
}
|
||||
}
|
||||
async convert(fileBlob: Blob): Promise<Blob> {
|
||||
await this.ready;
|
||||
const response = this.convertProcessor.queueUpRequest(() =>
|
||||
retryAsyncFunction<Blob>(async () => {
|
||||
const convertWorker = this.workerPool.shift();
|
||||
const worker = await convertWorker.remote;
|
||||
try {
|
||||
const convertedHEIC = await new Promise<Blob>(
|
||||
(resolve, reject) => {
|
||||
const main = async () => {
|
||||
try {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(Error("wait time exceeded"));
|
||||
}, WAIT_TIME_IN_MICROSECONDS);
|
||||
const startTime = Date.now();
|
||||
const convertedHEIC =
|
||||
await worker.convertHEIC(
|
||||
fileBlob,
|
||||
CONVERT_FORMAT,
|
||||
);
|
||||
addLogLine(
|
||||
`originalFileSize:${convertBytesToHumanReadable(
|
||||
fileBlob?.size,
|
||||
)},convertedFileSize:${convertBytesToHumanReadable(
|
||||
convertedHEIC?.size,
|
||||
)}, heic conversion time: ${
|
||||
Date.now() - startTime
|
||||
}ms `,
|
||||
);
|
||||
clearTimeout(timeout);
|
||||
resolve(convertedHEIC);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
};
|
||||
main();
|
||||
},
|
||||
);
|
||||
if (!convertedHEIC || convertedHEIC?.size === 0) {
|
||||
logError(
|
||||
Error(`converted heic fileSize is Zero`),
|
||||
"converted heic fileSize is Zero",
|
||||
{
|
||||
originalFileSize: convertBytesToHumanReadable(
|
||||
fileBlob?.size ?? 0,
|
||||
),
|
||||
convertedFileSize: convertBytesToHumanReadable(
|
||||
convertedHEIC?.size ?? 0,
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(
|
||||
() => resolve(null),
|
||||
BREATH_TIME_IN_MICROSECONDS,
|
||||
);
|
||||
});
|
||||
this.workerPool.push(convertWorker);
|
||||
return convertedHEIC;
|
||||
} catch (e) {
|
||||
logError(e, "heic conversion failed");
|
||||
convertWorker.terminate();
|
||||
this.workerPool.push(getDedicatedConvertWorker());
|
||||
throw e;
|
||||
}
|
||||
}, WAIT_TIME_BEFORE_NEXT_ATTEMPT_IN_MICROSECONDS),
|
||||
);
|
||||
try {
|
||||
return await response.promise;
|
||||
} catch (e) {
|
||||
if (e.message === CustomError.REQUEST_CANCELLED) {
|
||||
// ignore
|
||||
return null;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new HEICConverter();
|
||||
10
web/apps/cast/src/types/cache/index.ts
vendored
@@ -1,10 +0,0 @@
|
||||
export interface LimitedCacheStorage {
|
||||
open: (cacheName: string) => Promise<LimitedCache>;
|
||||
delete: (cacheName: string) => Promise<boolean>;
|
||||
}
|
||||
|
||||
export interface LimitedCache {
|
||||
match: (key: string) => Promise<Response>;
|
||||
put: (key: string, data: Response) => Promise<void>;
|
||||
delete: (key: string) => Promise<boolean>;
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
export interface CastPayload {
|
||||
collectionID: number;
|
||||
collectionKey: string;
|
||||
castToken: string;
|
||||
}
|
||||