Merge branch 'main' into redesign_search_tab
This commit is contained in:
commit
faa40119b4
10
.github/workflows/build.yml
vendored
10
.github/workflows/build.yml
vendored
|
@ -10,8 +10,8 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
# Setup Java environment in order to build the Android app.
|
# Setup Java environment in order to build the Android app.
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-java@v2
|
- uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
distribution: 'adopt'
|
distribution: 'adopt'
|
||||||
java-version: '11'
|
java-version: '11'
|
||||||
|
@ -47,18 +47,18 @@ jobs:
|
||||||
run: sha256sum build/app/outputs/flutter-apk/ente.apk > build/app/outputs/flutter-apk/sha256sum
|
run: sha256sum build/app/outputs/flutter-apk/ente.apk > build/app/outputs/flutter-apk/sha256sum
|
||||||
|
|
||||||
# Upload generated apk to the artifacts.
|
# Upload generated apk to the artifacts.
|
||||||
- uses: actions/upload-artifact@v2
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: release-apk
|
name: release-apk
|
||||||
path: build/app/outputs/flutter-apk/ente.apk
|
path: build/app/outputs/flutter-apk/ente.apk
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v2
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: release-checksum
|
name: release-checksum
|
||||||
path: build/app/outputs/flutter-apk/sha256sum
|
path: build/app/outputs/flutter-apk/sha256sum
|
||||||
|
|
||||||
# Create a pre-release
|
# Create a pre-release
|
||||||
- uses: ncipollo/release-action@v1
|
- uses: ncipollo/release-action@v1.14.0
|
||||||
with:
|
with:
|
||||||
artifacts: "build/app/outputs/flutter-apk/ente.apk,build/app/outputs/flutter-apk/sha256sum"
|
artifacts: "build/app/outputs/flutter-apk/ente.apk,build/app/outputs/flutter-apk/sha256sum"
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
3
android/.gitignore
vendored
3
android/.gitignore
vendored
|
@ -5,6 +5,3 @@ gradle-wrapper.jar
|
||||||
/gradlew.bat
|
/gradlew.bat
|
||||||
/local.properties
|
/local.properties
|
||||||
GeneratedPluginRegistrant.java
|
GeneratedPluginRegistrant.java
|
||||||
|
|
||||||
# Signing config files
|
|
||||||
*.jks
|
|
|
@ -2,7 +2,7 @@ ente ist eine einfache App, um Ihre Fotos und Videos automatisch zu sichern und
|
||||||
|
|
||||||
Wenn Sie auf der Suche nach einer datenschutzfreundlichen Alternative zu Google Fotos sind, sind Sie an der richtigen Stelle. Mit Ente werden Ihre Fotos Ende-zu-Ende-verschlüsselt gespeichert (e2ee). Dies bedeutet, dass nur Sie sie sehen können.
|
Wenn Sie auf der Suche nach einer datenschutzfreundlichen Alternative zu Google Fotos sind, sind Sie an der richtigen Stelle. Mit Ente werden Ihre Fotos Ende-zu-Ende-verschlüsselt gespeichert (e2ee). Dies bedeutet, dass nur Sie sie sehen können.
|
||||||
|
|
||||||
Wir haben Open-Source-Apps für Android, iOS, Web und Desktop. Ihre Fotos werden verschlüsselt (e2ee) zwischen allen Geräten synchronisiert.
|
Ihre Fotos werden verschlüsselt (e2ee) zwischen allen Geräten synchronisiert.
|
||||||
|
|
||||||
ente ermöglicht es, deine Alben simpel & schnell mit deinen Geliebten zu teilen. Sie können öffentlich einsehbare Links teilen, sodass andere sogar ohne einen Account oder eine App Ihr Album sehen und darin zusammenarbeiten können, indem sie Fotos hinzufügen.
|
ente ermöglicht es, deine Alben simpel & schnell mit deinen Geliebten zu teilen. Sie können öffentlich einsehbare Links teilen, sodass andere sogar ohne einen Account oder eine App Ihr Album sehen und darin zusammenarbeiten können, indem sie Fotos hinzufügen.
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ FEATURES
|
||||||
- und noch VIELES mehr!
|
- und noch VIELES mehr!
|
||||||
|
|
||||||
BERECHTIGUNGEN
|
BERECHTIGUNGEN
|
||||||
ente benötigt bestimmte Berechtigungen, um als Fotospeicher zu funktionieren. Diese können unter folgendem Link überprüft werden: https://github.com/ente-io/photos-app/blob/f-droid/android/permissions.md
|
Diese können unter folgendem Link überprüft werden: https://github.com/ente-io/photos-app/blob/f-droid/android/permissions.md
|
||||||
|
|
||||||
PREIS
|
PREIS
|
||||||
Wir bieten keine lebenslang kostenlosen Abonnements an, da es für uns wichtig ist, einen nachhaltigen Service anzubieten. Wir bieten jedoch bezahlbare Abonemments an, welche auch mit der Familie geteilt werden können. Mehr Informationen sind auf ente.io zu finden.
|
Wir bieten keine lebenslang kostenlosen Abonnements an, da es für uns wichtig ist, einen nachhaltigen Service anzubieten. Wir bieten jedoch bezahlbare Abonemments an, welche auch mit der Familie geteilt werden können. Mehr Informationen sind auf ente.io zu finden.
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
PODS:
|
PODS:
|
||||||
- background_fetch (1.2.1):
|
- background_fetch (1.2.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- battery_info (0.0.1):
|
||||||
|
- Flutter
|
||||||
- connectivity_plus (0.0.1):
|
- connectivity_plus (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- ReachabilitySwift
|
- ReachabilitySwift
|
||||||
|
@ -213,6 +215,7 @@ PODS:
|
||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- background_fetch (from `.symlinks/plugins/background_fetch/ios`)
|
- background_fetch (from `.symlinks/plugins/background_fetch/ios`)
|
||||||
|
- battery_info (from `.symlinks/plugins/battery_info/ios`)
|
||||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
||||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||||
- file_saver (from `.symlinks/plugins/file_saver/ios`)
|
- file_saver (from `.symlinks/plugins/file_saver/ios`)
|
||||||
|
@ -286,6 +289,8 @@ SPEC REPOS:
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
background_fetch:
|
background_fetch:
|
||||||
:path: ".symlinks/plugins/background_fetch/ios"
|
:path: ".symlinks/plugins/background_fetch/ios"
|
||||||
|
battery_info:
|
||||||
|
:path: ".symlinks/plugins/battery_info/ios"
|
||||||
connectivity_plus:
|
connectivity_plus:
|
||||||
:path: ".symlinks/plugins/connectivity_plus/ios"
|
:path: ".symlinks/plugins/connectivity_plus/ios"
|
||||||
device_info_plus:
|
device_info_plus:
|
||||||
|
@ -377,6 +382,7 @@ EXTERNAL SOURCES:
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
background_fetch: 896944864b038d2837fc750d470e9841e1e6a363
|
background_fetch: 896944864b038d2837fc750d470e9841e1e6a363
|
||||||
|
battery_info: 09f5c9ee65394f2291c8c6227bedff345b8a730c
|
||||||
connectivity_plus: 53efb943fc2882c8512d84c45707bcabc4c36076
|
connectivity_plus: 53efb943fc2882c8512d84c45707bcabc4c36076
|
||||||
device_info_plus: 7545d84d8d1b896cb16a4ff98c19f07ec4b298ea
|
device_info_plus: 7545d84d8d1b896cb16a4ff98c19f07ec4b298ea
|
||||||
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
|
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
|
||||||
|
|
|
@ -276,6 +276,7 @@
|
||||||
"${BUILT_PRODUCTS_DIR}/SentryPrivate/SentryPrivate.framework",
|
"${BUILT_PRODUCTS_DIR}/SentryPrivate/SentryPrivate.framework",
|
||||||
"${BUILT_PRODUCTS_DIR}/Toast/Toast.framework",
|
"${BUILT_PRODUCTS_DIR}/Toast/Toast.framework",
|
||||||
"${BUILT_PRODUCTS_DIR}/background_fetch/background_fetch.framework",
|
"${BUILT_PRODUCTS_DIR}/background_fetch/background_fetch.framework",
|
||||||
|
"${BUILT_PRODUCTS_DIR}/battery_info/battery_info.framework",
|
||||||
"${BUILT_PRODUCTS_DIR}/connectivity_plus/connectivity_plus.framework",
|
"${BUILT_PRODUCTS_DIR}/connectivity_plus/connectivity_plus.framework",
|
||||||
"${BUILT_PRODUCTS_DIR}/device_info_plus/device_info_plus.framework",
|
"${BUILT_PRODUCTS_DIR}/device_info_plus/device_info_plus.framework",
|
||||||
"${BUILT_PRODUCTS_DIR}/file_saver/file_saver.framework",
|
"${BUILT_PRODUCTS_DIR}/file_saver/file_saver.framework",
|
||||||
|
@ -357,6 +358,7 @@
|
||||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SentryPrivate.framework",
|
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SentryPrivate.framework",
|
||||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Toast.framework",
|
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Toast.framework",
|
||||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/background_fetch.framework",
|
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/background_fetch.framework",
|
||||||
|
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/battery_info.framework",
|
||||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/connectivity_plus.framework",
|
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/connectivity_plus.framework",
|
||||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/device_info_plus.framework",
|
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/device_info_plus.framework",
|
||||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_saver.framework",
|
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_saver.framework",
|
||||||
|
|
28
lib/app.dart
28
lib/app.dart
|
@ -13,7 +13,7 @@ import 'package:photos/ente_theme_data.dart';
|
||||||
import "package:photos/generated/l10n.dart";
|
import "package:photos/generated/l10n.dart";
|
||||||
import "package:photos/l10n/l10n.dart";
|
import "package:photos/l10n/l10n.dart";
|
||||||
import 'package:photos/services/app_lifecycle_service.dart';
|
import 'package:photos/services/app_lifecycle_service.dart';
|
||||||
import "package:photos/services/semantic_search/semantic_search_service.dart";
|
import "package:photos/services/machine_learning/machine_learning_controller.dart";
|
||||||
import 'package:photos/services/sync_service.dart';
|
import 'package:photos/services/sync_service.dart';
|
||||||
import 'package:photos/ui/tabs/home_widget.dart';
|
import 'package:photos/ui/tabs/home_widget.dart';
|
||||||
import "package:photos/ui/viewer/actions/file_viewer.dart";
|
import "package:photos/ui/viewer/actions/file_viewer.dart";
|
||||||
|
@ -43,12 +43,8 @@ class EnteApp extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _EnteAppState extends State<EnteApp> with WidgetsBindingObserver {
|
class _EnteAppState extends State<EnteApp> with WidgetsBindingObserver {
|
||||||
static const initialInteractionTimeout = Duration(seconds: 10);
|
|
||||||
static const defaultInteractionTimeout = Duration(seconds: 5);
|
|
||||||
|
|
||||||
final _logger = Logger("EnteAppState");
|
final _logger = Logger("EnteAppState");
|
||||||
late Locale locale;
|
late Locale locale;
|
||||||
late Timer _userInteractionTimer;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
@ -57,7 +53,6 @@ class _EnteAppState extends State<EnteApp> with WidgetsBindingObserver {
|
||||||
locale = widget.locale;
|
locale = widget.locale;
|
||||||
setupIntentAction();
|
setupIntentAction();
|
||||||
WidgetsBinding.instance.addObserver(this);
|
WidgetsBinding.instance.addObserver(this);
|
||||||
_setupInteractionTimer(timeout: initialInteractionTimeout);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setLocale(Locale newLocale) {
|
setLocale(Locale newLocale) {
|
||||||
|
@ -76,30 +71,12 @@ class _EnteAppState extends State<EnteApp> with WidgetsBindingObserver {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _resetTimer() {
|
|
||||||
_userInteractionTimer.cancel();
|
|
||||||
_setupInteractionTimer();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _setupInteractionTimer({Duration timeout = defaultInteractionTimeout}) {
|
|
||||||
if (Platform.isAndroid || kDebugMode) {
|
|
||||||
_userInteractionTimer = Timer(timeout, () {
|
|
||||||
debugPrint("user is not interacting with the app");
|
|
||||||
SemanticSearchService.instance.resumeIndexing();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
SemanticSearchService.instance.resumeIndexing();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (Platform.isAndroid || kDebugMode) {
|
if (Platform.isAndroid || kDebugMode) {
|
||||||
return Listener(
|
return Listener(
|
||||||
onPointerDown: (event) {
|
onPointerDown: (event) {
|
||||||
SemanticSearchService.instance.pauseIndexing();
|
MachineLearningController.instance.onUserInteraction();
|
||||||
debugPrint("user is interacting with the app");
|
|
||||||
_resetTimer();
|
|
||||||
},
|
},
|
||||||
child: AdaptiveTheme(
|
child: AdaptiveTheme(
|
||||||
light: lightThemeData,
|
light: lightThemeData,
|
||||||
|
@ -149,7 +126,6 @@ class _EnteAppState extends State<EnteApp> with WidgetsBindingObserver {
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
WidgetsBinding.instance.removeObserver(this);
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
_userInteractionTimer.cancel();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,9 +25,9 @@ import 'package:photos/services/billing_service.dart';
|
||||||
import 'package:photos/services/collections_service.dart';
|
import 'package:photos/services/collections_service.dart';
|
||||||
import 'package:photos/services/favorites_service.dart';
|
import 'package:photos/services/favorites_service.dart';
|
||||||
import 'package:photos/services/ignored_files_service.dart';
|
import 'package:photos/services/ignored_files_service.dart';
|
||||||
|
import 'package:photos/services/machine_learning/semantic_search/semantic_search_service.dart';
|
||||||
import 'package:photos/services/memories_service.dart';
|
import 'package:photos/services/memories_service.dart';
|
||||||
import 'package:photos/services/search_service.dart';
|
import 'package:photos/services/search_service.dart';
|
||||||
import "package:photos/services/semantic_search/semantic_search_service.dart";
|
|
||||||
import 'package:photos/services/sync_service.dart';
|
import 'package:photos/services/sync_service.dart';
|
||||||
import 'package:photos/utils/crypto_util.dart';
|
import 'package:photos/utils/crypto_util.dart';
|
||||||
import 'package:photos/utils/file_uploader.dart';
|
import 'package:photos/utils/file_uploader.dart';
|
||||||
|
|
|
@ -67,4 +67,30 @@ const galleryGridSpacing = 2.0;
|
||||||
|
|
||||||
const kSearchSectionLimit = 7;
|
const kSearchSectionLimit = 7;
|
||||||
|
|
||||||
bool isInternalUser = false;
|
const blackThumbnailBase64 = '/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=';
|
||||||
|
|
|
@ -48,6 +48,19 @@ class EmbeddingsDB {
|
||||||
return await _isar.embeddings.filter().updationTimeEqualTo(null).findAll();
|
return await _isar.embeddings.filter().updationTimeEqualTo(null).findAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> deleteEmbeddings(List<int> fileIDs) async {
|
||||||
|
await _isar.writeTxn(() async {
|
||||||
|
final embeddings = <Embedding>[];
|
||||||
|
for (final fileID in fileIDs) {
|
||||||
|
embeddings.addAll(
|
||||||
|
await _isar.embeddings.filter().fileIDEqualTo(fileID).findAll(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await _isar.embeddings.deleteAll(embeddings.map((e) => e.id).toList());
|
||||||
|
Bus.instance.fire(EmbeddingUpdatedEvent());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> deleteAllForModel(Model model) async {
|
Future<void> deleteAllForModel(Model model) async {
|
||||||
await _isar.writeTxn(() async {
|
await _isar.writeTxn(() async {
|
||||||
final embeddings =
|
final embeddings =
|
||||||
|
|
7
lib/events/machine_learning_control_event.dart
Normal file
7
lib/events/machine_learning_control_event.dart
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import "package:photos/events/event.dart";
|
||||||
|
|
||||||
|
class MachineLearningControlEvent extends Event {
|
||||||
|
final bool shouldRun;
|
||||||
|
|
||||||
|
MachineLearningControlEvent(this.shouldRun);
|
||||||
|
}
|
1199
lib/generated/intl/messages_pt.dart
generated
1199
lib/generated/intl/messages_pt.dart
generated
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -30,11 +30,12 @@ import 'package:photos/services/feature_flag_service.dart';
|
||||||
import 'package:photos/services/local_file_update_service.dart';
|
import 'package:photos/services/local_file_update_service.dart';
|
||||||
import 'package:photos/services/local_sync_service.dart';
|
import 'package:photos/services/local_sync_service.dart';
|
||||||
import "package:photos/services/location_service.dart";
|
import "package:photos/services/location_service.dart";
|
||||||
|
import "package:photos/services/machine_learning/machine_learning_controller.dart";
|
||||||
|
import 'package:photos/services/machine_learning/semantic_search/semantic_search_service.dart';
|
||||||
import 'package:photos/services/memories_service.dart';
|
import 'package:photos/services/memories_service.dart';
|
||||||
import 'package:photos/services/push_service.dart';
|
import 'package:photos/services/push_service.dart';
|
||||||
import 'package:photos/services/remote_sync_service.dart';
|
import 'package:photos/services/remote_sync_service.dart';
|
||||||
import 'package:photos/services/search_service.dart';
|
import 'package:photos/services/search_service.dart';
|
||||||
import 'package:photos/services/semantic_search/semantic_search_service.dart';
|
|
||||||
import "package:photos/services/storage_bonus_service.dart";
|
import "package:photos/services/storage_bonus_service.dart";
|
||||||
import 'package:photos/services/sync_service.dart';
|
import 'package:photos/services/sync_service.dart';
|
||||||
import 'package:photos/services/trash_sync_service.dart';
|
import 'package:photos/services/trash_sync_service.dart';
|
||||||
|
@ -194,6 +195,7 @@ Future<void> _init(bool isBackground, {String via = ''}) async {
|
||||||
}
|
}
|
||||||
unawaited(FeatureFlagService.instance.init());
|
unawaited(FeatureFlagService.instance.init());
|
||||||
unawaited(SemanticSearchService.instance.init());
|
unawaited(SemanticSearchService.instance.init());
|
||||||
|
MachineLearningController.instance.init();
|
||||||
// Can not including existing tf/ml binaries as they are not being built
|
// Can not including existing tf/ml binaries as they are not being built
|
||||||
// from source.
|
// from source.
|
||||||
// See https://gitlab.com/fdroid/fdroiddata/-/merge_requests/12671#note_1294346819
|
// See https://gitlab.com/fdroid/fdroiddata/-/merge_requests/12671#note_1294346819
|
||||||
|
|
|
@ -11,6 +11,7 @@ const heightKey = 'h';
|
||||||
const latKey = "lat";
|
const latKey = "lat";
|
||||||
const longKey = "long";
|
const longKey = "long";
|
||||||
const motionVideoIndexKey = "mvi";
|
const motionVideoIndexKey = "mvi";
|
||||||
|
const noThumbKey = "noThumb";
|
||||||
|
|
||||||
class MagicMetadata {
|
class MagicMetadata {
|
||||||
// 0 -> visible
|
// 0 -> visible
|
||||||
|
@ -47,6 +48,13 @@ class PubMagicMetadata {
|
||||||
// photo
|
// photo
|
||||||
int? mvi;
|
int? mvi;
|
||||||
|
|
||||||
|
// if true, then the thumbnail is not available
|
||||||
|
// Note: desktop/web sets hasStaticThumbnail in the file metadata.
|
||||||
|
// As we don't want to support updating the og file metadata (yet), adding
|
||||||
|
// this new field to the pub metadata. For static thumbnail, all thumbnails
|
||||||
|
// should have exact same hash with should match the constant `blackThumbnailBase64`
|
||||||
|
bool? noThumb;
|
||||||
|
|
||||||
PubMagicMetadata({
|
PubMagicMetadata({
|
||||||
this.editedTime,
|
this.editedTime,
|
||||||
this.editedName,
|
this.editedName,
|
||||||
|
@ -57,6 +65,7 @@ class PubMagicMetadata {
|
||||||
this.lat,
|
this.lat,
|
||||||
this.long,
|
this.long,
|
||||||
this.mvi,
|
this.mvi,
|
||||||
|
this.noThumb,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory PubMagicMetadata.fromEncodedJson(String encodedJson) =>
|
factory PubMagicMetadata.fromEncodedJson(String encodedJson) =>
|
||||||
|
@ -77,6 +86,7 @@ class PubMagicMetadata {
|
||||||
lat: map[latKey],
|
lat: map[latKey],
|
||||||
long: map[longKey],
|
long: map[longKey],
|
||||||
mvi: map[motionVideoIndexKey],
|
mvi: map[motionVideoIndexKey],
|
||||||
|
noThumb: map[noThumbKey],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import "package:photos/events/location_tag_updated_event.dart";
|
||||||
import "package:photos/generated/l10n.dart";
|
import "package:photos/generated/l10n.dart";
|
||||||
import "package:photos/models/collection/collection.dart";
|
import "package:photos/models/collection/collection.dart";
|
||||||
import "package:photos/models/collection/collection_items.dart";
|
import "package:photos/models/collection/collection_items.dart";
|
||||||
|
import "package:photos/models/search/generic_search_result.dart";
|
||||||
import "package:photos/models/search/search_result.dart";
|
import "package:photos/models/search/search_result.dart";
|
||||||
import "package:photos/models/typedefs.dart";
|
import "package:photos/models/typedefs.dart";
|
||||||
import "package:photos/services/collections_service.dart";
|
import "package:photos/services/collections_service.dart";
|
||||||
|
@ -24,6 +25,7 @@ enum ResultType {
|
||||||
collection,
|
collection,
|
||||||
file,
|
file,
|
||||||
location,
|
location,
|
||||||
|
locationSuggestion,
|
||||||
month,
|
month,
|
||||||
year,
|
year,
|
||||||
fileType,
|
fileType,
|
||||||
|
@ -243,10 +245,10 @@ extension SectionTypeExtensions on SectionType {
|
||||||
}) {
|
}) {
|
||||||
switch (this) {
|
switch (this) {
|
||||||
case SectionType.face:
|
case SectionType.face:
|
||||||
return SearchService.instance.getAllLocationTags(limit);
|
return Future.value(List<GenericSearchResult>.empty());
|
||||||
|
|
||||||
case SectionType.content:
|
case SectionType.content:
|
||||||
return SearchService.instance.getAllLocationTags(limit);
|
return Future.value(List<GenericSearchResult>.empty());
|
||||||
|
|
||||||
case SectionType.moment:
|
case SectionType.moment:
|
||||||
return SearchService.instance.getRandomMomentsSearchResults(context);
|
return SearchService.instance.getRandomMomentsSearchResults(context);
|
||||||
|
|
|
@ -1372,10 +1372,10 @@ class CollectionsService {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> move(
|
Future<void> move(
|
||||||
int toCollectionID,
|
List<EnteFile> files, {
|
||||||
int fromCollectionID,
|
required int toCollectionID,
|
||||||
List<EnteFile> files,
|
required int fromCollectionID,
|
||||||
) async {
|
}) async {
|
||||||
_validateMoveRequest(toCollectionID, fromCollectionID, files);
|
_validateMoveRequest(toCollectionID, fromCollectionID, files);
|
||||||
files.removeWhere((element) => element.uploadedFileID == null);
|
files.removeWhere((element) => element.uploadedFileID == null);
|
||||||
if (files.isEmpty) {
|
if (files.isEmpty) {
|
||||||
|
@ -1443,9 +1443,19 @@ class CollectionsService {
|
||||||
int fromCollectionID,
|
int fromCollectionID,
|
||||||
List<EnteFile> files,
|
List<EnteFile> files,
|
||||||
) {
|
) {
|
||||||
|
final int userID = Configuration.instance.getUserID()!;
|
||||||
if (toCollectionID == fromCollectionID) {
|
if (toCollectionID == fromCollectionID) {
|
||||||
throw AssertionError("Can't move to same album");
|
throw AssertionError("Can't move to same album");
|
||||||
}
|
}
|
||||||
|
final Collection? toCollection = _collectionIDToCollections[toCollectionID];
|
||||||
|
final Collection? fromCollection =
|
||||||
|
_collectionIDToCollections[fromCollectionID];
|
||||||
|
if (toCollection != null && !toCollection.isOwner(userID)) {
|
||||||
|
throw AssertionError("Can't move to a collection you don't own");
|
||||||
|
}
|
||||||
|
if (fromCollection != null && !fromCollection.isOwner(userID)) {
|
||||||
|
throw AssertionError("Can't move from a collection you don't own");
|
||||||
|
}
|
||||||
for (final file in files) {
|
for (final file in files) {
|
||||||
if (file.uploadedFileID == null) {
|
if (file.uploadedFileID == null) {
|
||||||
throw AssertionError("Can only move uploaded memories");
|
throw AssertionError("Can only move uploaded memories");
|
||||||
|
|
|
@ -71,10 +71,9 @@ class FeatureFlagService {
|
||||||
bool isInternalUserOrDebugBuild() {
|
bool isInternalUserOrDebugBuild() {
|
||||||
final String? email = Configuration.instance.getEmail();
|
final String? email = Configuration.instance.getEmail();
|
||||||
final userID = Configuration.instance.getUserID();
|
final userID = Configuration.instance.getUserID();
|
||||||
isInternalUser = (email != null && email.endsWith("@ente.io")) ||
|
return (email != null && email.endsWith("@ente.io")) ||
|
||||||
_internalUserIDs.contains(userID) ||
|
_internalUserIDs.contains(userID) ||
|
||||||
kDebugMode;
|
kDebugMode;
|
||||||
return isInternalUser;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> fetchFeatureFlags() async {
|
Future<void> fetchFeatureFlags() async {
|
||||||
|
|
|
@ -57,21 +57,25 @@ extension HiddenService on CollectionsService {
|
||||||
Future<Collection> clubAllDefaultHiddenToOne(
|
Future<Collection> clubAllDefaultHiddenToOne(
|
||||||
List<Collection> allDefaultHidden,
|
List<Collection> allDefaultHidden,
|
||||||
) async {
|
) async {
|
||||||
final Collection result = allDefaultHidden.first;
|
// select first collection as default hidden where all files will be clubbed
|
||||||
|
final Collection defaultHidden = allDefaultHidden.first;
|
||||||
for (Collection defaultHidden in allDefaultHidden) {
|
for (Collection hidden in allDefaultHidden) {
|
||||||
try {
|
try {
|
||||||
if (defaultHidden.id == result.id) {
|
if (hidden.id == defaultHidden.id) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
final filesInCollection = (await FilesDB.instance.getFilesInCollection(
|
final filesInCollection = (await FilesDB.instance.getFilesInCollection(
|
||||||
defaultHidden.id,
|
hidden.id,
|
||||||
galleryLoadStartTime,
|
galleryLoadStartTime,
|
||||||
galleryLoadEndTime,
|
galleryLoadEndTime,
|
||||||
))
|
))
|
||||||
.files;
|
.files;
|
||||||
await move(result.id, defaultHidden.id, filesInCollection);
|
await move(
|
||||||
await CollectionsService.instance.trashEmptyCollection(defaultHidden);
|
filesInCollection,
|
||||||
|
toCollectionID: defaultHidden.id,
|
||||||
|
fromCollectionID: hidden.id,
|
||||||
|
);
|
||||||
|
await CollectionsService.instance.trashEmptyCollection(hidden);
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
_logger.severe(
|
_logger.severe(
|
||||||
"One iteration of clubbing all default hidden failed",
|
"One iteration of clubbing all default hidden failed",
|
||||||
|
@ -82,7 +86,7 @@ extension HiddenService on CollectionsService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return defaultHidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
// getUncategorizedCollection will return the uncategorized collection
|
// getUncategorizedCollection will return the uncategorized collection
|
||||||
|
@ -137,7 +141,18 @@ extension HiddenService on CollectionsService {
|
||||||
_logger.finest('file already part of hidden collection');
|
_logger.finest('file already part of hidden collection');
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
await move(defaultHiddenCollection.id, entry.key, entry.value);
|
final Collection? c = getCollectionByID(entry.key);
|
||||||
|
// if the collection is not owned by the user, remove the file from the
|
||||||
|
// collection
|
||||||
|
if (c != null && !c.isOwner(userID)) {
|
||||||
|
await removeFromCollection(entry.key, entry.value);
|
||||||
|
} else {
|
||||||
|
await move(
|
||||||
|
entry.value,
|
||||||
|
toCollectionID: defaultHiddenCollection.id,
|
||||||
|
fromCollectionID: entry.key,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Bus.instance.fire(
|
Bus.instance.fire(
|
||||||
LocalPhotosUpdatedEvent(
|
LocalPhotosUpdatedEvent(
|
||||||
|
|
|
@ -7,6 +7,7 @@ import "package:logging/logging.dart";
|
||||||
import "package:photos/core/constants.dart";
|
import "package:photos/core/constants.dart";
|
||||||
import "package:photos/core/event_bus.dart";
|
import "package:photos/core/event_bus.dart";
|
||||||
import "package:photos/events/location_tag_updated_event.dart";
|
import "package:photos/events/location_tag_updated_event.dart";
|
||||||
|
import "package:photos/extensions/stop_watch.dart";
|
||||||
import "package:photos/models/api/entity/type.dart";
|
import "package:photos/models/api/entity/type.dart";
|
||||||
import "package:photos/models/file/file.dart";
|
import "package:photos/models/file/file.dart";
|
||||||
import "package:photos/models/local_entity_data.dart";
|
import "package:photos/models/local_entity_data.dart";
|
||||||
|
@ -45,6 +46,8 @@ class LocationService {
|
||||||
List<EnteFile> allFiles,
|
List<EnteFile> allFiles,
|
||||||
String query,
|
String query,
|
||||||
) async {
|
) async {
|
||||||
|
final EnteWatch w = EnteWatch("cities_search")..start();
|
||||||
|
w.log('start for files ${allFiles.length} and query $query');
|
||||||
final result = await _computer.compute(
|
final result = await _computer.compute(
|
||||||
getCityResults,
|
getCityResults,
|
||||||
param: {
|
param: {
|
||||||
|
@ -53,6 +56,10 @@ class LocationService {
|
||||||
"files": allFiles,
|
"files": allFiles,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
w.log(
|
||||||
|
'end for query: $query on ${allFiles.length} files, found '
|
||||||
|
'${result.length} cities',
|
||||||
|
);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -235,31 +242,29 @@ Future<List<City>> parseCities(Map args) async {
|
||||||
|
|
||||||
Map<City, List<EnteFile>> getCityResults(Map args) {
|
Map<City, List<EnteFile>> getCityResults(Map args) {
|
||||||
final query = (args["query"] as String).toLowerCase();
|
final query = (args["query"] as String).toLowerCase();
|
||||||
final cities = args["cities"] as List<City>;
|
final List<City> cities = args["cities"] as List<City>;
|
||||||
final files = args["files"] as List<EnteFile>;
|
final List<EnteFile> files = args["files"] as List<EnteFile>;
|
||||||
|
|
||||||
final matchingCities = cities.where(
|
final matchingCities = cities
|
||||||
(city) => city.city.toLowerCase().contains(query),
|
.where(
|
||||||
);
|
(city) => city.city.toLowerCase().contains(query),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
|
||||||
final Map<City, List<EnteFile>> results = {};
|
final Map<City, List<EnteFile>> results = {};
|
||||||
for (final city in matchingCities) {
|
for (final file in files) {
|
||||||
final List<EnteFile> matchingFiles = [];
|
if (!file.hasLocation) continue; // Skip files without location
|
||||||
final cityLocation = Location(latitude: city.lat, longitude: city.lng);
|
for (final city in matchingCities) {
|
||||||
for (final file in files) {
|
final cityLocation = Location(latitude: city.lat, longitude: city.lng);
|
||||||
if (file.hasLocation) {
|
if (isFileInsideLocationTag(
|
||||||
if (isFileInsideLocationTag(
|
cityLocation,
|
||||||
cityLocation,
|
file.location!,
|
||||||
file.location!,
|
defaultCityRadius,
|
||||||
defaultCityRadius,
|
)) {
|
||||||
)) {
|
results.putIfAbsent(city, () => []).add(file);
|
||||||
matchingFiles.add(file);
|
break; // Stop searching once a file is matched with a city
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (matchingFiles.isNotEmpty) {
|
|
||||||
results[city] = matchingFiles;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
102
lib/services/machine_learning/machine_learning_controller.dart
Normal file
102
lib/services/machine_learning/machine_learning_controller.dart
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
import "dart:async";
|
||||||
|
import "dart:io";
|
||||||
|
|
||||||
|
import "package:battery_info/battery_info_plugin.dart";
|
||||||
|
import "package:battery_info/model/android_battery_info.dart";
|
||||||
|
import "package:logging/logging.dart";
|
||||||
|
import "package:photos/core/event_bus.dart";
|
||||||
|
import "package:photos/events/machine_learning_control_event.dart";
|
||||||
|
|
||||||
|
class MachineLearningController {
|
||||||
|
MachineLearningController._privateConstructor();
|
||||||
|
|
||||||
|
static final MachineLearningController instance =
|
||||||
|
MachineLearningController._privateConstructor();
|
||||||
|
|
||||||
|
final _logger = Logger("MachineLearningController");
|
||||||
|
|
||||||
|
static const kMaximumTemperature = 42; // 42 degree celsius
|
||||||
|
static const kMinimumBatteryLevel = 20; // 20%
|
||||||
|
static const kInitialInteractionTimeout = Duration(seconds: 10);
|
||||||
|
static const kDefaultInteractionTimeout = Duration(seconds: 5);
|
||||||
|
static const kUnhealthyStates = ["over_heat", "over_voltage", "dead"];
|
||||||
|
|
||||||
|
bool _isDeviceHealthy = true;
|
||||||
|
bool _isUserInteracting = true;
|
||||||
|
bool _isRunningML = false;
|
||||||
|
late Timer _userInteractionTimer;
|
||||||
|
|
||||||
|
void init() {
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
_startInteractionTimer(timeout: kInitialInteractionTimeout);
|
||||||
|
BatteryInfoPlugin()
|
||||||
|
.androidBatteryInfoStream
|
||||||
|
.listen((AndroidBatteryInfo? batteryInfo) {
|
||||||
|
_onBatteryStateUpdate(batteryInfo);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Always run Machine Learning on iOS
|
||||||
|
Bus.instance.fire(MachineLearningControlEvent(true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onUserInteraction() {
|
||||||
|
if (Platform.isIOS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!_isUserInteracting) {
|
||||||
|
_logger.info("User is interacting with the app");
|
||||||
|
_isUserInteracting = true;
|
||||||
|
_fireControlEvent();
|
||||||
|
}
|
||||||
|
_resetTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _fireControlEvent() {
|
||||||
|
final shouldRunML = _isDeviceHealthy && !_isUserInteracting;
|
||||||
|
if (shouldRunML != _isRunningML) {
|
||||||
|
_isRunningML = shouldRunML;
|
||||||
|
_logger.info(
|
||||||
|
"Firing event with device health: $_isDeviceHealthy and user interaction: $_isUserInteracting",
|
||||||
|
);
|
||||||
|
Bus.instance.fire(MachineLearningControlEvent(shouldRunML));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _startInteractionTimer({Duration timeout = kDefaultInteractionTimeout}) {
|
||||||
|
_userInteractionTimer = Timer(timeout, () {
|
||||||
|
_logger.info("User is not interacting with the app");
|
||||||
|
_isUserInteracting = false;
|
||||||
|
_fireControlEvent();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _resetTimer() {
|
||||||
|
_userInteractionTimer.cancel();
|
||||||
|
_startInteractionTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onBatteryStateUpdate(AndroidBatteryInfo? batteryInfo) {
|
||||||
|
_logger.info("Battery info: ${batteryInfo!.toJson()}");
|
||||||
|
_isDeviceHealthy = _computeIsDeviceHealthy(batteryInfo);
|
||||||
|
_fireControlEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _computeIsDeviceHealthy(AndroidBatteryInfo info) {
|
||||||
|
return _hasSufficientBattery(info.batteryLevel ?? kMinimumBatteryLevel) &&
|
||||||
|
_isAcceptableTemperature(info.temperature ?? kMaximumTemperature) &&
|
||||||
|
_isBatteryHealthy(info.health ?? "");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _hasSufficientBattery(int batteryLevel) {
|
||||||
|
return batteryLevel >= kMinimumBatteryLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isAcceptableTemperature(int temperature) {
|
||||||
|
return temperature <= kMaximumTemperature;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isBatteryHealthy(String health) {
|
||||||
|
return !kUnhealthyStates.contains(health);
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,7 +9,7 @@ import "package:photos/db/embeddings_db.dart";
|
||||||
import "package:photos/db/files_db.dart";
|
import "package:photos/db/files_db.dart";
|
||||||
import "package:photos/models/embedding.dart";
|
import "package:photos/models/embedding.dart";
|
||||||
import "package:photos/models/file/file.dart";
|
import "package:photos/models/file/file.dart";
|
||||||
import "package:photos/services/semantic_search/remote_embedding.dart";
|
import 'package:photos/services/machine_learning/semantic_search/remote_embedding.dart';
|
||||||
import "package:photos/utils/crypto_util.dart";
|
import "package:photos/utils/crypto_util.dart";
|
||||||
import "package:photos/utils/file_download_util.dart";
|
import "package:photos/utils/file_download_util.dart";
|
||||||
import "package:shared_preferences/shared_preferences.dart";
|
import "package:shared_preferences/shared_preferences.dart";
|
||||||
|
@ -53,13 +53,22 @@ class EmbeddingStore {
|
||||||
final fileMap = await FilesDB.instance
|
final fileMap = await FilesDB.instance
|
||||||
.getFilesFromIDs(pendingItems.map((e) => e.fileID).toList());
|
.getFilesFromIDs(pendingItems.map((e) => e.fileID).toList());
|
||||||
_logger.info("Pushing ${pendingItems.length} embeddings");
|
_logger.info("Pushing ${pendingItems.length} embeddings");
|
||||||
|
final deletedEntries = <int>[];
|
||||||
for (final item in pendingItems) {
|
for (final item in pendingItems) {
|
||||||
try {
|
try {
|
||||||
await _pushEmbedding(fileMap[item.fileID]!, item);
|
final file = fileMap[item.fileID];
|
||||||
|
if (file != null) {
|
||||||
|
await _pushEmbedding(file, item);
|
||||||
|
} else {
|
||||||
|
deletedEntries.add(item.fileID);
|
||||||
|
}
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
_logger.severe(e, s);
|
_logger.severe(e, s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (deletedEntries.isNotEmpty) {
|
||||||
|
await EmbeddingsDB.instance.deleteEmbeddings(deletedEntries);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> storeEmbedding(EnteFile file, Embedding embedding) async {
|
Future<void> storeEmbedding(EnteFile file, Embedding embedding) async {
|
|
@ -1,7 +1,7 @@
|
||||||
import "package:clip_ggml/clip_ggml.dart";
|
import "package:clip_ggml/clip_ggml.dart";
|
||||||
import "package:computer/computer.dart";
|
import "package:computer/computer.dart";
|
||||||
import "package:logging/logging.dart";
|
import "package:logging/logging.dart";
|
||||||
import 'package:photos/services/semantic_search/frameworks/ml_framework.dart';
|
import 'package:photos/services/machine_learning/semantic_search/frameworks/ml_framework.dart';
|
||||||
|
|
||||||
class GGML extends MLFramework {
|
class GGML extends MLFramework {
|
||||||
static const kModelBucketEndpoint = "https://models.ente.io/";
|
static const kModelBucketEndpoint = "https://models.ente.io/";
|
|
@ -1,9 +1,9 @@
|
||||||
import "package:computer/computer.dart";
|
import "package:computer/computer.dart";
|
||||||
import "package:logging/logging.dart";
|
import "package:logging/logging.dart";
|
||||||
import "package:onnxruntime/onnxruntime.dart";
|
import "package:onnxruntime/onnxruntime.dart";
|
||||||
import "package:photos/services/semantic_search/frameworks/ml_framework.dart";
|
import 'package:photos/services/machine_learning/semantic_search/frameworks/ml_framework.dart';
|
||||||
import "package:photos/services/semantic_search/frameworks/onnx/onnx_image_encoder.dart";
|
import 'package:photos/services/machine_learning/semantic_search/frameworks/onnx/onnx_image_encoder.dart';
|
||||||
import "package:photos/services/semantic_search/frameworks/onnx/onnx_text_encoder.dart";
|
import 'package:photos/services/machine_learning/semantic_search/frameworks/onnx/onnx_text_encoder.dart';
|
||||||
|
|
||||||
class ONNX extends MLFramework {
|
class ONNX extends MLFramework {
|
||||||
static const kModelBucketEndpoint = "https://models.ente.io/";
|
static const kModelBucketEndpoint = "https://models.ente.io/";
|
|
@ -5,7 +5,7 @@ import "dart:typed_data";
|
||||||
import "package:flutter/services.dart";
|
import "package:flutter/services.dart";
|
||||||
import "package:logging/logging.dart";
|
import "package:logging/logging.dart";
|
||||||
import "package:onnxruntime/onnxruntime.dart";
|
import "package:onnxruntime/onnxruntime.dart";
|
||||||
import "package:photos/services/semantic_search/frameworks/onnx/onnx_text_tokenizer.dart";
|
import 'package:photos/services/machine_learning/semantic_search/frameworks/onnx/onnx_text_tokenizer.dart';
|
||||||
|
|
||||||
class OnnxTextEncoder {
|
class OnnxTextEncoder {
|
||||||
static const kVocabFilePath = "assets/models/clip/bpe_simple_vocab_16e6.txt";
|
static const kVocabFilePath = "assets/models/clip/bpe_simple_vocab_16e6.txt";
|
|
@ -1,5 +1,6 @@
|
||||||
import "dart:async";
|
import "dart:async";
|
||||||
import "dart:collection";
|
import "dart:collection";
|
||||||
|
import "dart:io";
|
||||||
|
|
||||||
import "package:computer/computer.dart";
|
import "package:computer/computer.dart";
|
||||||
import "package:logging/logging.dart";
|
import "package:logging/logging.dart";
|
||||||
|
@ -11,13 +12,14 @@ import "package:photos/db/files_db.dart";
|
||||||
import "package:photos/events/diff_sync_complete_event.dart";
|
import "package:photos/events/diff_sync_complete_event.dart";
|
||||||
import 'package:photos/events/embedding_updated_event.dart';
|
import 'package:photos/events/embedding_updated_event.dart';
|
||||||
import "package:photos/events/file_uploaded_event.dart";
|
import "package:photos/events/file_uploaded_event.dart";
|
||||||
|
import "package:photos/events/machine_learning_control_event.dart";
|
||||||
import "package:photos/models/embedding.dart";
|
import "package:photos/models/embedding.dart";
|
||||||
import "package:photos/models/file/file.dart";
|
import "package:photos/models/file/file.dart";
|
||||||
import "package:photos/services/collections_service.dart";
|
import "package:photos/services/collections_service.dart";
|
||||||
import "package:photos/services/semantic_search/embedding_store.dart";
|
import 'package:photos/services/machine_learning/semantic_search/embedding_store.dart';
|
||||||
import "package:photos/services/semantic_search/frameworks/ggml.dart";
|
import 'package:photos/services/machine_learning/semantic_search/frameworks/ggml.dart';
|
||||||
import "package:photos/services/semantic_search/frameworks/ml_framework.dart";
|
import 'package:photos/services/machine_learning/semantic_search/frameworks/ml_framework.dart';
|
||||||
import 'package:photos/services/semantic_search/frameworks/onnx/onnx.dart';
|
import 'package:photos/services/machine_learning/semantic_search/frameworks/onnx/onnx.dart';
|
||||||
import "package:photos/utils/debouncer.dart";
|
import "package:photos/utils/debouncer.dart";
|
||||||
import "package:photos/utils/device_info.dart";
|
import "package:photos/utils/device_info.dart";
|
||||||
import "package:photos/utils/local_settings.dart";
|
import "package:photos/utils/local_settings.dart";
|
||||||
|
@ -50,22 +52,10 @@ class SemanticSearchService {
|
||||||
Future<List<EnteFile>>? _ongoingRequest;
|
Future<List<EnteFile>>? _ongoingRequest;
|
||||||
List<Embedding> _cachedEmbeddings = <Embedding>[];
|
List<Embedding> _cachedEmbeddings = <Embedding>[];
|
||||||
PendingQuery? _nextQuery;
|
PendingQuery? _nextQuery;
|
||||||
Completer<void> _userInteraction = Completer<void>();
|
Completer<void> _mlController = Completer<void>();
|
||||||
|
|
||||||
get hasInitialized => _hasInitialized;
|
get hasInitialized => _hasInitialized;
|
||||||
|
|
||||||
void resumeIndexing() {
|
|
||||||
_logger.info("Resuming indexing");
|
|
||||||
_userInteraction.complete();
|
|
||||||
}
|
|
||||||
|
|
||||||
void pauseIndexing() {
|
|
||||||
if (_userInteraction.isCompleted) {
|
|
||||||
_logger.info("Pausing indexing");
|
|
||||||
_userInteraction = Completer<void>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> init({bool shouldSyncImmediately = false}) async {
|
Future<void> init({bool shouldSyncImmediately = false}) async {
|
||||||
if (!LocalSettings.instance.hasEnabledMagicSearch()) {
|
if (!LocalSettings.instance.hasEnabledMagicSearch()) {
|
||||||
return;
|
return;
|
||||||
|
@ -111,6 +101,17 @@ class SemanticSearchService {
|
||||||
if (shouldSyncImmediately) {
|
if (shouldSyncImmediately) {
|
||||||
unawaited(sync());
|
unawaited(sync());
|
||||||
}
|
}
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
Bus.instance.on<MachineLearningControlEvent>().listen((event) {
|
||||||
|
if (event.shouldRun) {
|
||||||
|
_startIndexing();
|
||||||
|
} else {
|
||||||
|
_pauseIndexing();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
_startIndexing();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> release() async {
|
Future<void> release() async {
|
||||||
|
@ -242,15 +243,23 @@ class SemanticSearchService {
|
||||||
|
|
||||||
final ignoredCollections =
|
final ignoredCollections =
|
||||||
CollectionsService.instance.getHiddenCollectionIds();
|
CollectionsService.instance.getHiddenCollectionIds();
|
||||||
|
final deletedEntries = <int>[];
|
||||||
for (final result in queryResults) {
|
for (final result in queryResults) {
|
||||||
final file = filesMap[result.id];
|
final file = filesMap[result.id];
|
||||||
if (file != null && !ignoredCollections.contains(file.collectionID)) {
|
if (file != null && !ignoredCollections.contains(file.collectionID)) {
|
||||||
results.add(filesMap[result.id]!);
|
results.add(file);
|
||||||
|
}
|
||||||
|
if (file == null) {
|
||||||
|
deletedEntries.add(result.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.info(results.length.toString() + " results");
|
_logger.info(results.length.toString() + " results");
|
||||||
|
|
||||||
|
if (deletedEntries.isNotEmpty) {
|
||||||
|
unawaited(EmbeddingsDB.instance.deleteEmbeddings(deletedEntries));
|
||||||
|
}
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -294,9 +303,9 @@ class SemanticSearchService {
|
||||||
if (!_frameworkInitialization.isCompleted) {
|
if (!_frameworkInitialization.isCompleted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!_userInteraction.isCompleted) {
|
if (!_mlController.isCompleted) {
|
||||||
_logger.info("Waiting for user interactions to stop...");
|
_logger.info("Waiting for a green signal from controller...");
|
||||||
await _userInteraction.future;
|
await _mlController.future;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
final thumbnail = await getThumbnailForUploadedFile(file);
|
final thumbnail = await getThumbnailForUploadedFile(file);
|
||||||
|
@ -369,6 +378,20 @@ class SemanticSearchService {
|
||||||
return Model.onnxClip;
|
return Model.onnxClip;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _startIndexing() {
|
||||||
|
_logger.info("Start indexing");
|
||||||
|
if (!_mlController.isCompleted) {
|
||||||
|
_mlController.complete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _pauseIndexing() {
|
||||||
|
if (_mlController.isCompleted) {
|
||||||
|
_logger.info("Pausing indexing");
|
||||||
|
_mlController = Completer<void>();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<QueryResult> computeBulkScore(Map args) {
|
List<QueryResult> computeBulkScore(Map args) {
|
|
@ -3,6 +3,7 @@ import "dart:math";
|
||||||
import "package:flutter/cupertino.dart";
|
import "package:flutter/cupertino.dart";
|
||||||
import "package:intl/intl.dart";
|
import "package:intl/intl.dart";
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
import "package:photos/core/constants.dart";
|
||||||
import 'package:photos/core/event_bus.dart';
|
import 'package:photos/core/event_bus.dart';
|
||||||
import 'package:photos/data/holidays.dart';
|
import 'package:photos/data/holidays.dart';
|
||||||
import 'package:photos/data/months.dart';
|
import 'package:photos/data/months.dart';
|
||||||
|
@ -17,14 +18,16 @@ import "package:photos/models/file/extensions/file_props.dart";
|
||||||
import 'package:photos/models/file/file.dart';
|
import 'package:photos/models/file/file.dart';
|
||||||
import 'package:photos/models/file/file_type.dart';
|
import 'package:photos/models/file/file_type.dart';
|
||||||
import "package:photos/models/local_entity_data.dart";
|
import "package:photos/models/local_entity_data.dart";
|
||||||
|
import "package:photos/models/location/location.dart";
|
||||||
import "package:photos/models/location_tag/location_tag.dart";
|
import "package:photos/models/location_tag/location_tag.dart";
|
||||||
import 'package:photos/models/search/album_search_result.dart';
|
import 'package:photos/models/search/album_search_result.dart';
|
||||||
import 'package:photos/models/search/generic_search_result.dart';
|
import 'package:photos/models/search/generic_search_result.dart';
|
||||||
import "package:photos/models/search/search_types.dart";
|
import "package:photos/models/search/search_types.dart";
|
||||||
import 'package:photos/services/collections_service.dart';
|
import 'package:photos/services/collections_service.dart';
|
||||||
import "package:photos/services/location_service.dart";
|
import "package:photos/services/location_service.dart";
|
||||||
import 'package:photos/services/semantic_search/semantic_search_service.dart';
|
import 'package:photos/services/machine_learning/semantic_search/semantic_search_service.dart';
|
||||||
import "package:photos/states/location_screen_state.dart";
|
import "package:photos/states/location_screen_state.dart";
|
||||||
|
import "package:photos/ui/viewer/location/add_location_sheet.dart";
|
||||||
import "package:photos/ui/viewer/location/location_screen.dart";
|
import "package:photos/ui/viewer/location/location_screen.dart";
|
||||||
import 'package:photos/utils/date_time_util.dart';
|
import 'package:photos/utils/date_time_util.dart';
|
||||||
import "package:photos/utils/navigation_util.dart";
|
import "package:photos/utils/navigation_util.dart";
|
||||||
|
@ -676,17 +679,24 @@ class SearchService {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//todo: remove this later, this hack is for interval+external evaluation
|
||||||
|
// for suggestions
|
||||||
|
final allCitiesSearch = query == '__city';
|
||||||
|
if (allCitiesSearch) {
|
||||||
|
query = '';
|
||||||
|
}
|
||||||
final results =
|
final results =
|
||||||
await LocationService.instance.getFilesInCity(allFiles, query);
|
await LocationService.instance.getFilesInCity(allFiles, query);
|
||||||
for (final entry in results.entries) {
|
final List<City> sortedByResultCount = results.keys.toList()
|
||||||
|
..sort((a, b) => results[b]!.length.compareTo(results[a]!.length));
|
||||||
|
for (final city in sortedByResultCount) {
|
||||||
// If the location tag already exists for a city, don't add it again
|
// If the location tag already exists for a city, don't add it again
|
||||||
if (!locationTagNames.contains(entry.key.city)) {
|
if (!locationTagNames.contains(city.city)) {
|
||||||
searchResults.add(
|
searchResults.add(
|
||||||
GenericSearchResult(
|
GenericSearchResult(
|
||||||
ResultType.location,
|
ResultType.location,
|
||||||
entry.key.city,
|
city.city,
|
||||||
entry.value,
|
results[city]!,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -701,6 +711,7 @@ class SearchService {
|
||||||
final locationTagEntities =
|
final locationTagEntities =
|
||||||
(await LocationService.instance.getLocationTags());
|
(await LocationService.instance.getLocationTags());
|
||||||
final allFiles = await getAllFiles();
|
final allFiles = await getAllFiles();
|
||||||
|
final List<EnteFile> filesWithNoLocTag = [];
|
||||||
|
|
||||||
for (int i = 0; i < locationTagEntities.length; i++) {
|
for (int i = 0; i < locationTagEntities.length; i++) {
|
||||||
if (limit != null && i >= limit) break;
|
if (limit != null && i >= limit) break;
|
||||||
|
@ -709,15 +720,22 @@ class SearchService {
|
||||||
|
|
||||||
for (EnteFile file in allFiles) {
|
for (EnteFile file in allFiles) {
|
||||||
if (file.hasLocation) {
|
if (file.hasLocation) {
|
||||||
|
bool hasLocationTag = false;
|
||||||
for (LocalEntity<LocationTag> tag in tagToItemsMap.keys) {
|
for (LocalEntity<LocationTag> tag in tagToItemsMap.keys) {
|
||||||
if (isFileInsideLocationTag(
|
if (isFileInsideLocationTag(
|
||||||
tag.item.centerPoint,
|
tag.item.centerPoint,
|
||||||
file.location!,
|
file.location!,
|
||||||
tag.item.radius,
|
tag.item.radius,
|
||||||
)) {
|
)) {
|
||||||
|
hasLocationTag = true;
|
||||||
tagToItemsMap[tag]!.add(file);
|
tagToItemsMap[tag]!.add(file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// If the location tag already exists for a city, do not consider
|
||||||
|
// it for the city suggestions
|
||||||
|
if (!hasLocationTag) {
|
||||||
|
filesWithNoLocTag.add(file);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -746,6 +764,30 @@ class SearchService {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (limit == null || tagSearchResults.length < limit) {
|
||||||
|
final results = await LocationService.instance
|
||||||
|
.getFilesInCity(filesWithNoLocTag, '');
|
||||||
|
final List<City> sortedByResultCount = results.keys.toList()
|
||||||
|
..sort((a, b) => results[b]!.length.compareTo(results[a]!.length));
|
||||||
|
for (final city in sortedByResultCount) {
|
||||||
|
if (results[city]!.length <= 1) continue;
|
||||||
|
tagSearchResults.add(
|
||||||
|
GenericSearchResult(
|
||||||
|
ResultType.locationSuggestion,
|
||||||
|
city.city,
|
||||||
|
results[city]!,
|
||||||
|
onResultTap: (ctx) {
|
||||||
|
showAddLocationSheet(
|
||||||
|
ctx,
|
||||||
|
Location(latitude: city.lat, longitude: city.lng),
|
||||||
|
name: city.city,
|
||||||
|
radius: defaultCityRadius,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
return tagSearchResults;
|
return tagSearchResults;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_logger.severe("Error in getAllLocationTags", e);
|
_logger.severe("Error in getAllLocationTags", e);
|
||||||
|
|
|
@ -154,9 +154,12 @@ class UpdateService {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return Platform.isAndroid
|
return Platform.isAndroid
|
||||||
? const Tuple2("play store", "market://details?id=io.ente.photos")
|
? const Tuple2(
|
||||||
|
"Google Play",
|
||||||
|
"https://play.google.com/store/apps/details?id=io.ente.photos",
|
||||||
|
)
|
||||||
: const Tuple2(
|
: const Tuple2(
|
||||||
"app store",
|
"App Store",
|
||||||
"https://apps.apple.com/in/app/ente-photos/id1542026904",
|
"https://apps.apple.com/in/app/ente-photos/id1542026904",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,11 +14,14 @@ import "package:photos/utils/debouncer.dart";
|
||||||
class LocationTagStateProvider extends StatefulWidget {
|
class LocationTagStateProvider extends StatefulWidget {
|
||||||
final LocalEntity<LocationTag>? locationTagEntity;
|
final LocalEntity<LocationTag>? locationTagEntity;
|
||||||
final Location? centerPoint;
|
final Location? centerPoint;
|
||||||
|
final double? radius;
|
||||||
final Widget child;
|
final Widget child;
|
||||||
const LocationTagStateProvider(
|
const LocationTagStateProvider(
|
||||||
this.child, {
|
this.child, {
|
||||||
this.centerPoint,
|
this.centerPoint,
|
||||||
this.locationTagEntity,
|
this.locationTagEntity,
|
||||||
|
// if the locationTagEntity is null, we use the centerPoint and radius
|
||||||
|
this.radius,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -47,9 +50,12 @@ class _LocationTagStateProviderState extends State<LocationTagStateProvider> {
|
||||||
///If the location tag has a custom radius value, we add the custom radius
|
///If the location tag has a custom radius value, we add the custom radius
|
||||||
///value to the list of default radius values only for this location tag and
|
///value to the list of default radius values only for this location tag and
|
||||||
///keep it in the state of this widget.
|
///keep it in the state of this widget.
|
||||||
_radiusValues = _getRadiusValuesOfLocTag(_locationTagEntity?.item.radius);
|
_radiusValues = _getRadiusValuesOfLocTag(
|
||||||
|
_locationTagEntity?.item.radius ?? widget.radius,
|
||||||
|
);
|
||||||
|
|
||||||
_selectedRadius = _locationTagEntity?.item.radius ?? defaultRadiusValue;
|
_selectedRadius =
|
||||||
|
_locationTagEntity?.item.radius ?? widget.radius ?? defaultRadiusValue;
|
||||||
|
|
||||||
_locTagEntityListener =
|
_locTagEntityListener =
|
||||||
Bus.instance.on<LocationTagUpdatedEvent>().listen((event) {
|
Bus.instance.on<LocationTagUpdatedEvent>().listen((event) {
|
||||||
|
|
|
@ -42,7 +42,7 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
|
||||||
_passwordController.text = _volatilePassword!;
|
_passwordController.text = _volatilePassword!;
|
||||||
Future.delayed(
|
Future.delayed(
|
||||||
Duration.zero,
|
Duration.zero,
|
||||||
() => verifyPassword(_volatilePassword!),
|
() => verifyPassword(_volatilePassword!),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
_passwordFocusNode.addListener(() {
|
_passwordFocusNode.addListener(() {
|
||||||
|
@ -100,69 +100,68 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> verifyPassword(String password) async {
|
Future<void> verifyPassword(String password) async {
|
||||||
FocusScope.of(context).unfocus();
|
FocusScope.of(context).unfocus();
|
||||||
final dialog =
|
final dialog = createProgressDialog(context, S.of(context).pleaseWait);
|
||||||
createProgressDialog(context, S.of(context).pleaseWait);
|
await dialog.show();
|
||||||
await dialog.show();
|
try {
|
||||||
try {
|
final kek = await Configuration.instance.decryptSecretsAndGetKeyEncKey(
|
||||||
final kek = await Configuration.instance.decryptSecretsAndGetKeyEncKey(
|
password,
|
||||||
password,
|
Configuration.instance.getKeyAttributes()!,
|
||||||
Configuration.instance.getKeyAttributes()!,
|
);
|
||||||
);
|
_registerSRPForExistingUsers(kek).ignore();
|
||||||
_registerSRPForExistingUsers(kek).ignore();
|
} on KeyDerivationError catch (e, s) {
|
||||||
} on KeyDerivationError catch (e, s) {
|
_logger.severe("Password verification failed", e, s);
|
||||||
_logger.severe("Password verification failed", e, s);
|
|
||||||
await dialog.hide();
|
|
||||||
final dialogChoice = await showChoiceDialog(
|
|
||||||
context,
|
|
||||||
title: S.of(context).recreatePasswordTitle,
|
|
||||||
body: S.of(context).recreatePasswordBody,
|
|
||||||
firstButtonLabel: S.of(context).useRecoveryKey,
|
|
||||||
);
|
|
||||||
if (dialogChoice!.action == ButtonAction.first) {
|
|
||||||
// ignore: unawaited_futures
|
|
||||||
Navigator.of(context).push(
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (BuildContext context) {
|
|
||||||
return const RecoveryPage();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
} catch (e, s) {
|
|
||||||
_logger.severe("Password verification failed", e, s);
|
|
||||||
await dialog.hide();
|
|
||||||
final dialogChoice = await showChoiceDialog(
|
|
||||||
context,
|
|
||||||
title: S.of(context).incorrectPasswordTitle,
|
|
||||||
body: S.of(context).pleaseTryAgain,
|
|
||||||
firstButtonLabel: S.of(context).contactSupport,
|
|
||||||
secondButtonLabel: S.of(context).ok,
|
|
||||||
);
|
|
||||||
if (dialogChoice!.action == ButtonAction.first) {
|
|
||||||
await sendLogs(
|
|
||||||
context,
|
|
||||||
S.of(context).contactSupport,
|
|
||||||
"support@ente.io",
|
|
||||||
postShare: () {},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await dialog.hide();
|
await dialog.hide();
|
||||||
Configuration.instance.setVolatilePassword(null);
|
final dialogChoice = await showChoiceDialog(
|
||||||
Bus.instance.fire(SubscriptionPurchasedEvent());
|
context,
|
||||||
unawaited(
|
title: S.of(context).recreatePasswordTitle,
|
||||||
Navigator.of(context).pushAndRemoveUntil(
|
body: S.of(context).recreatePasswordBody,
|
||||||
|
firstButtonLabel: S.of(context).useRecoveryKey,
|
||||||
|
);
|
||||||
|
if (dialogChoice!.action == ButtonAction.first) {
|
||||||
|
// ignore: unawaited_futures
|
||||||
|
Navigator.of(context).push(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
return const HomeWidget();
|
return const RecoveryPage();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
(route) => false,
|
);
|
||||||
),
|
}
|
||||||
|
return;
|
||||||
|
} catch (e, s) {
|
||||||
|
_logger.severe("Password verification failed", e, s);
|
||||||
|
await dialog.hide();
|
||||||
|
final dialogChoice = await showChoiceDialog(
|
||||||
|
context,
|
||||||
|
title: S.of(context).incorrectPasswordTitle,
|
||||||
|
body: S.of(context).pleaseTryAgain,
|
||||||
|
firstButtonLabel: S.of(context).contactSupport,
|
||||||
|
secondButtonLabel: S.of(context).ok,
|
||||||
);
|
);
|
||||||
|
if (dialogChoice!.action == ButtonAction.first) {
|
||||||
|
await sendLogs(
|
||||||
|
context,
|
||||||
|
S.of(context).contactSupport,
|
||||||
|
"support@ente.io",
|
||||||
|
postShare: () {},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await dialog.hide();
|
||||||
|
Configuration.instance.setVolatilePassword(null);
|
||||||
|
Bus.instance.fire(SubscriptionPurchasedEvent());
|
||||||
|
unawaited(
|
||||||
|
Navigator.of(context).pushAndRemoveUntil(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return const HomeWidget();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(route) => false,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _registerSRPForExistingUsers(Uint8List key) async {
|
Future<void> _registerSRPForExistingUsers(Uint8List key) async {
|
||||||
|
@ -266,8 +265,8 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
child: Row(
|
child: Wrap(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
alignment: WrapAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
behavior: HitTestBehavior.opaque,
|
behavior: HitTestBehavior.opaque,
|
||||||
|
@ -280,17 +279,13 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: Center(
|
child: Text(
|
||||||
child: Text(
|
S.of(context).forgotPassword,
|
||||||
S.of(context).forgotPassword,
|
style:
|
||||||
style: Theme.of(context)
|
Theme.of(context).textTheme.titleMedium!.copyWith(
|
||||||
.textTheme
|
fontSize: 14,
|
||||||
.titleMedium!
|
decoration: TextDecoration.underline,
|
||||||
.copyWith(
|
),
|
||||||
fontSize: 14,
|
|
||||||
decoration: TextDecoration.underline,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
|
@ -306,17 +301,13 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
|
||||||
Navigator.of(context)
|
Navigator.of(context)
|
||||||
.popUntil((route) => route.isFirst);
|
.popUntil((route) => route.isFirst);
|
||||||
},
|
},
|
||||||
child: Center(
|
child: Text(
|
||||||
child: Text(
|
S.of(context).changeEmail,
|
||||||
S.of(context).changeEmail,
|
style:
|
||||||
style: Theme.of(context)
|
Theme.of(context).textTheme.titleMedium!.copyWith(
|
||||||
.textTheme
|
fontSize: 14,
|
||||||
.titleMedium!
|
decoration: TextDecoration.underline,
|
||||||
.copyWith(
|
),
|
||||||
fontSize: 14,
|
|
||||||
decoration: TextDecoration.underline,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
@ -558,9 +558,9 @@ class CollectionActions {
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await collectionsService.move(
|
await collectionsService.move(
|
||||||
entry.key,
|
|
||||||
collection.id,
|
|
||||||
entry.value,
|
entry.value,
|
||||||
|
toCollectionID: entry.key,
|
||||||
|
fromCollectionID: collection.id,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -398,9 +398,9 @@ class AlbumVerticalListWidget extends StatelessWidget {
|
||||||
try {
|
try {
|
||||||
final int fromCollectionID = selectedFiles!.files.first.collectionID!;
|
final int fromCollectionID = selectedFiles!.files.first.collectionID!;
|
||||||
await CollectionsService.instance.move(
|
await CollectionsService.instance.move(
|
||||||
toCollectionID,
|
|
||||||
fromCollectionID,
|
|
||||||
selectedFiles!.files.toList(),
|
selectedFiles!.files.toList(),
|
||||||
|
toCollectionID: toCollectionID,
|
||||||
|
fromCollectionID: fromCollectionID,
|
||||||
);
|
);
|
||||||
await dialog.hide();
|
await dialog.hide();
|
||||||
unawaited(RemoteSyncService.instance.sync(silently: true));
|
unawaited(RemoteSyncService.instance.sync(silently: true));
|
||||||
|
|
|
@ -60,7 +60,6 @@ class DynamicFAB extends StatelessWidget {
|
||||||
} else {
|
} else {
|
||||||
return Container(
|
return Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: 56,
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
child: OutlinedButton(
|
child: OutlinedButton(
|
||||||
onPressed:
|
onPressed:
|
||||||
|
|
|
@ -153,6 +153,7 @@ class _FullScreenMemoryState extends State<FullScreenMemory> {
|
||||||
final inheritedData = FullScreenMemoryData.of(context)!;
|
final inheritedData = FullScreenMemoryData.of(context)!;
|
||||||
final showStepProgressIndicator = inheritedData.memories.length < 60;
|
final showStepProgressIndicator = inheritedData.memories.length < 60;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
backgroundColor: Colors.black,
|
||||||
extendBodyBehindAppBar: true,
|
extendBodyBehindAppBar: true,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
toolbarHeight: 84,
|
toolbarHeight: 84,
|
||||||
|
|
|
@ -221,6 +221,7 @@ class _MemoryCoverWidgetState extends State<MemoryCoverWidget> {
|
||||||
child: ThumbnailWidget(
|
child: ThumbnailWidget(
|
||||||
memory.file,
|
memory.file,
|
||||||
shouldShowArchiveStatus: false,
|
shouldShowArchiveStatus: false,
|
||||||
|
shouldShowSyncStatus: false,
|
||||||
key: Key("memories" + memory.file.tag),
|
key: Key("memories" + memory.file.tag),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -49,7 +49,11 @@ class _PaymentWebPageState extends State<PaymentWebPage> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
_dialog = createProgressDialog(context, S.of(context).pleaseWait);
|
_dialog = createProgressDialog(
|
||||||
|
context,
|
||||||
|
S.of(context).pleaseWait,
|
||||||
|
isDismissible: true,
|
||||||
|
);
|
||||||
if (initPaymentUrl == null) {
|
if (initPaymentUrl == null) {
|
||||||
return const EnteLoadingWidget();
|
return const EnteLoadingWidget();
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,13 +36,9 @@ class _SubscriptionHeaderWidgetState extends State<SubscriptionHeaderWidget> {
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Text(
|
||||||
children: [
|
S.of(context).selectYourPlan,
|
||||||
Text(
|
style: Theme.of(context).textTheme.headlineMedium,
|
||||||
S.of(context).selectYourPlan,
|
|
||||||
style: Theme.of(context).textTheme.headlineMedium,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
Text(
|
Text(
|
||||||
|
|
|
@ -6,8 +6,8 @@ import "package:photos/core/event_bus.dart";
|
||||||
import 'package:photos/events/embedding_updated_event.dart';
|
import 'package:photos/events/embedding_updated_event.dart';
|
||||||
import "package:photos/generated/l10n.dart";
|
import "package:photos/generated/l10n.dart";
|
||||||
import "package:photos/services/feature_flag_service.dart";
|
import "package:photos/services/feature_flag_service.dart";
|
||||||
import "package:photos/services/semantic_search/frameworks/ml_framework.dart";
|
import 'package:photos/services/machine_learning/semantic_search/frameworks/ml_framework.dart';
|
||||||
import "package:photos/services/semantic_search/semantic_search_service.dart";
|
import 'package:photos/services/machine_learning/semantic_search/semantic_search_service.dart';
|
||||||
import "package:photos/theme/ente_theme.dart";
|
import "package:photos/theme/ente_theme.dart";
|
||||||
import "package:photos/ui/common/loading_widget.dart";
|
import "package:photos/ui/common/loading_widget.dart";
|
||||||
import "package:photos/ui/components/buttons/icon_button_widget.dart";
|
import "package:photos/ui/components/buttons/icon_button_widget.dart";
|
||||||
|
|
|
@ -42,14 +42,16 @@ class _LogFileViewerState extends State<LogFileViewer> {
|
||||||
}
|
}
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.only(left: 12, top: 8, right: 12),
|
padding: const EdgeInsets.only(left: 12, top: 8, right: 12),
|
||||||
child: SingleChildScrollView(
|
child: Scrollbar(
|
||||||
child: Text(
|
child: SingleChildScrollView(
|
||||||
_logs!,
|
child: Text(
|
||||||
style: const TextStyle(
|
_logs!,
|
||||||
fontFeatures: [
|
style: const TextStyle(
|
||||||
FontFeature.tabularFigures(),
|
fontFeatures: [
|
||||||
],
|
FontFeature.tabularFigures(),
|
||||||
height: 1.2,
|
],
|
||||||
|
height: 1.2,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -73,6 +73,7 @@ class DetailPage extends StatefulWidget {
|
||||||
class _DetailPageState extends State<DetailPage> {
|
class _DetailPageState extends State<DetailPage> {
|
||||||
static const kLoadLimit = 100;
|
static const kLoadLimit = 100;
|
||||||
final _logger = Logger("DetailPageState");
|
final _logger = Logger("DetailPageState");
|
||||||
|
bool _shouldDisableScroll = false;
|
||||||
List<EnteFile>? _files;
|
List<EnteFile>? _files;
|
||||||
late PageController _pageController;
|
late PageController _pageController;
|
||||||
final _selectedIndexNotifier = ValueNotifier(0);
|
final _selectedIndexNotifier = ValueNotifier(0);
|
||||||
|
@ -171,6 +172,14 @@ class _DetailPageState extends State<DetailPage> {
|
||||||
file,
|
file,
|
||||||
autoPlay: shouldAutoPlay(),
|
autoPlay: shouldAutoPlay(),
|
||||||
tagPrefix: widget.config.tagPrefix,
|
tagPrefix: widget.config.tagPrefix,
|
||||||
|
shouldDisableScroll: (value) {
|
||||||
|
if (_shouldDisableScroll != value) {
|
||||||
|
setState(() {
|
||||||
|
_logger.fine('setState $_shouldDisableScroll to $value');
|
||||||
|
_shouldDisableScroll = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
playbackCallback: (isPlaying) {
|
playbackCallback: (isPlaying) {
|
||||||
Future.delayed(Duration.zero, () {
|
Future.delayed(Duration.zero, () {
|
||||||
_toggleFullScreen(shouldEnable: isPlaying);
|
_toggleFullScreen(shouldEnable: isPlaying);
|
||||||
|
@ -199,7 +208,9 @@ class _DetailPageState extends State<DetailPage> {
|
||||||
}
|
}
|
||||||
_preloadEntries();
|
_preloadEntries();
|
||||||
},
|
},
|
||||||
physics: const FastScrollPhysics(speedFactor: 4.0),
|
physics: _shouldDisableScroll
|
||||||
|
? const NeverScrollableScrollPhysics()
|
||||||
|
: const FastScrollPhysics(speedFactor: 4.0),
|
||||||
controller: _pageController,
|
controller: _pageController,
|
||||||
itemCount: _files!.length,
|
itemCount: _files!.length,
|
||||||
);
|
);
|
||||||
|
|
|
@ -8,6 +8,7 @@ import "package:photos/ui/viewer/file/zoomable_live_image_new.dart";
|
||||||
class FileWidget extends StatelessWidget {
|
class FileWidget extends StatelessWidget {
|
||||||
final EnteFile file;
|
final EnteFile file;
|
||||||
final String? tagPrefix;
|
final String? tagPrefix;
|
||||||
|
final Function(bool)? shouldDisableScroll;
|
||||||
final Function(bool)? playbackCallback;
|
final Function(bool)? playbackCallback;
|
||||||
final BoxDecoration? backgroundDecoration;
|
final BoxDecoration? backgroundDecoration;
|
||||||
final bool? autoPlay;
|
final bool? autoPlay;
|
||||||
|
@ -15,6 +16,7 @@ class FileWidget extends StatelessWidget {
|
||||||
const FileWidget(
|
const FileWidget(
|
||||||
this.file, {
|
this.file, {
|
||||||
this.autoPlay,
|
this.autoPlay,
|
||||||
|
this.shouldDisableScroll,
|
||||||
this.playbackCallback,
|
this.playbackCallback,
|
||||||
this.tagPrefix,
|
this.tagPrefix,
|
||||||
this.backgroundDecoration,
|
this.backgroundDecoration,
|
||||||
|
@ -30,6 +32,7 @@ class FileWidget extends StatelessWidget {
|
||||||
file.fileType == FileType.image) {
|
file.fileType == FileType.image) {
|
||||||
return ZoomableLiveImageNew(
|
return ZoomableLiveImageNew(
|
||||||
file,
|
file,
|
||||||
|
shouldDisableScroll: shouldDisableScroll,
|
||||||
tagPrefix: tagPrefix,
|
tagPrefix: tagPrefix,
|
||||||
backgroundDecoration: backgroundDecoration,
|
backgroundDecoration: backgroundDecoration,
|
||||||
key: key ?? ValueKey(fileKey),
|
key: key ?? ValueKey(fileKey),
|
||||||
|
|
|
@ -6,7 +6,6 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:photo_view/photo_view.dart';
|
import 'package:photo_view/photo_view.dart';
|
||||||
import "package:photo_view/photo_view_gallery.dart";
|
|
||||||
import 'package:photos/core/cache/thumbnail_in_memory_cache.dart';
|
import 'package:photos/core/cache/thumbnail_in_memory_cache.dart';
|
||||||
import 'package:photos/core/constants.dart';
|
import 'package:photos/core/constants.dart';
|
||||||
import 'package:photos/core/event_bus.dart';
|
import 'package:photos/core/event_bus.dart';
|
||||||
|
@ -25,6 +24,7 @@ import 'package:photos/utils/thumbnail_util.dart';
|
||||||
|
|
||||||
class ZoomableImage extends StatefulWidget {
|
class ZoomableImage extends StatefulWidget {
|
||||||
final EnteFile photo;
|
final EnteFile photo;
|
||||||
|
final Function(bool)? shouldDisableScroll;
|
||||||
final String? tagPrefix;
|
final String? tagPrefix;
|
||||||
final Decoration? backgroundDecoration;
|
final Decoration? backgroundDecoration;
|
||||||
final bool shouldCover;
|
final bool shouldCover;
|
||||||
|
@ -32,6 +32,7 @@ class ZoomableImage extends StatefulWidget {
|
||||||
const ZoomableImage(
|
const ZoomableImage(
|
||||||
this.photo, {
|
this.photo, {
|
||||||
Key? key,
|
Key? key,
|
||||||
|
this.shouldDisableScroll,
|
||||||
required this.tagPrefix,
|
required this.tagPrefix,
|
||||||
this.backgroundDecoration,
|
this.backgroundDecoration,
|
||||||
this.shouldCover = false,
|
this.shouldCover = false,
|
||||||
|
@ -51,9 +52,9 @@ class _ZoomableImageState extends State<ZoomableImage>
|
||||||
bool _loadedLargeThumbnail = false;
|
bool _loadedLargeThumbnail = false;
|
||||||
bool _loadingFinalImage = false;
|
bool _loadingFinalImage = false;
|
||||||
bool _loadedFinalImage = false;
|
bool _loadedFinalImage = false;
|
||||||
PhotoViewController _photoViewController = PhotoViewController();
|
|
||||||
bool _isZooming = false;
|
|
||||||
ValueChanged<PhotoViewScaleState>? _scaleStateChangedCallback;
|
ValueChanged<PhotoViewScaleState>? _scaleStateChangedCallback;
|
||||||
|
bool _isZooming = false;
|
||||||
|
PhotoViewController _photoViewController = PhotoViewController();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
@ -61,8 +62,12 @@ class _ZoomableImageState extends State<ZoomableImage>
|
||||||
_logger = Logger("ZoomableImage");
|
_logger = Logger("ZoomableImage");
|
||||||
_logger.info('initState for ${_photo.generatedID} with tag ${_photo.tag}');
|
_logger.info('initState for ${_photo.generatedID} with tag ${_photo.tag}');
|
||||||
_scaleStateChangedCallback = (value) {
|
_scaleStateChangedCallback = (value) {
|
||||||
|
if (widget.shouldDisableScroll != null) {
|
||||||
|
widget.shouldDisableScroll!(value != PhotoViewScaleState.initial);
|
||||||
|
}
|
||||||
_isZooming = value != PhotoViewScaleState.initial;
|
_isZooming = value != PhotoViewScaleState.initial;
|
||||||
debugPrint("isZooming = $_isZooming, currentState $value");
|
debugPrint("isZooming = $_isZooming, currentState $value");
|
||||||
|
// _logger.info('is reakky zooming $_isZooming with state $value');
|
||||||
};
|
};
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
@ -83,41 +88,41 @@ class _ZoomableImageState extends State<ZoomableImage>
|
||||||
Widget content;
|
Widget content;
|
||||||
|
|
||||||
if (_imageProvider != null) {
|
if (_imageProvider != null) {
|
||||||
content = PhotoViewGallery.builder(
|
content = PhotoViewGestureDetectorScope(
|
||||||
gaplessPlayback: true,
|
axis: Axis.vertical,
|
||||||
scaleStateChangedCallback: _scaleStateChangedCallback,
|
child: PhotoView(
|
||||||
backgroundDecoration: widget.backgroundDecoration as BoxDecoration?,
|
imageProvider: _imageProvider,
|
||||||
builder: (context, index) {
|
controller: _photoViewController,
|
||||||
return PhotoViewGalleryPageOptions(
|
scaleStateChangedCallback: _scaleStateChangedCallback,
|
||||||
imageProvider: _imageProvider!,
|
minScale: widget.shouldCover
|
||||||
minScale: widget.shouldCover
|
? PhotoViewComputedScale.covered
|
||||||
? PhotoViewComputedScale.covered
|
: PhotoViewComputedScale.contained,
|
||||||
: PhotoViewComputedScale.contained,
|
gaplessPlayback: true,
|
||||||
heroAttributes: PhotoViewHeroAttributes(
|
heroAttributes: PhotoViewHeroAttributes(
|
||||||
tag: widget.tagPrefix! + _photo.tag,
|
tag: widget.tagPrefix! + _photo.tag,
|
||||||
),
|
),
|
||||||
controller: _photoViewController,
|
backgroundDecoration: widget.backgroundDecoration as BoxDecoration?,
|
||||||
);
|
),
|
||||||
},
|
|
||||||
itemCount: 1,
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
content = const EnteLoadingWidget();
|
content = const EnteLoadingWidget();
|
||||||
}
|
}
|
||||||
verticalDragCallback(d) => {
|
|
||||||
if (!_isZooming)
|
|
||||||
{
|
|
||||||
if (d.delta.dy > dragSensitivity)
|
|
||||||
{
|
|
||||||
{Navigator.of(context).pop()},
|
|
||||||
}
|
|
||||||
else if (d.delta.dy < (dragSensitivity * -1))
|
|
||||||
{
|
|
||||||
showDetailsSheet(context, widget.photo),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
|
final GestureDragUpdateCallback? verticalDragCallback = _isZooming
|
||||||
|
? null
|
||||||
|
: (d) => {
|
||||||
|
if (!_isZooming)
|
||||||
|
{
|
||||||
|
if (d.delta.dy > dragSensitivity)
|
||||||
|
{
|
||||||
|
{Navigator.of(context).pop()},
|
||||||
|
}
|
||||||
|
else if (d.delta.dy < (dragSensitivity * -1))
|
||||||
|
{
|
||||||
|
showDetailsSheet(context, widget.photo),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onVerticalDragUpdate: verticalDragCallback,
|
onVerticalDragUpdate: verticalDragCallback,
|
||||||
child: content,
|
child: content,
|
||||||
|
@ -258,7 +263,9 @@ class _ZoomableImageState extends State<ZoomableImage>
|
||||||
required ImageProvider? previewImageProvider,
|
required ImageProvider? previewImageProvider,
|
||||||
required ImageProvider finalImageProvider,
|
required ImageProvider finalImageProvider,
|
||||||
}) async {
|
}) async {
|
||||||
final bool shouldFixPosition = previewImageProvider != null && _isZooming;
|
final bool shouldFixPosition = previewImageProvider != null &&
|
||||||
|
_isZooming &&
|
||||||
|
_photoViewController.scale != null;
|
||||||
ImageInfo? finalImageInfo;
|
ImageInfo? finalImageInfo;
|
||||||
if (shouldFixPosition) {
|
if (shouldFixPosition) {
|
||||||
final prevImageInfo = await getImageInfo(previewImageProvider);
|
final prevImageInfo = await getImageInfo(previewImageProvider);
|
||||||
|
|
|
@ -16,12 +16,14 @@ import 'package:video_player/video_player.dart';
|
||||||
|
|
||||||
class ZoomableLiveImage extends StatefulWidget {
|
class ZoomableLiveImage extends StatefulWidget {
|
||||||
final EnteFile enteFile;
|
final EnteFile enteFile;
|
||||||
|
final Function(bool)? shouldDisableScroll;
|
||||||
final String? tagPrefix;
|
final String? tagPrefix;
|
||||||
final Decoration? backgroundDecoration;
|
final Decoration? backgroundDecoration;
|
||||||
|
|
||||||
const ZoomableLiveImage(
|
const ZoomableLiveImage(
|
||||||
this.enteFile, {
|
this.enteFile, {
|
||||||
Key? key,
|
Key? key,
|
||||||
|
this.shouldDisableScroll,
|
||||||
required this.tagPrefix,
|
required this.tagPrefix,
|
||||||
this.backgroundDecoration,
|
this.backgroundDecoration,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
@ -43,9 +45,8 @@ class _ZoomableLiveImageState extends State<ZoomableLiveImage>
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
_enteFile = widget.enteFile;
|
_enteFile = widget.enteFile;
|
||||||
_logger.info(
|
_logger.info('initState for ${_enteFile.generatedID} with tag ${_enteFile
|
||||||
'initState for ${_enteFile.generatedID} with tag ${_enteFile.tag} and name ${_enteFile.displayName}',
|
.tag} and name ${_enteFile.displayName}');
|
||||||
);
|
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,6 +76,7 @@ class _ZoomableLiveImageState extends State<ZoomableLiveImage>
|
||||||
content = ZoomableImage(
|
content = ZoomableImage(
|
||||||
_enteFile,
|
_enteFile,
|
||||||
tagPrefix: widget.tagPrefix,
|
tagPrefix: widget.tagPrefix,
|
||||||
|
shouldDisableScroll: widget.shouldDisableScroll,
|
||||||
backgroundDecoration: widget.backgroundDecoration,
|
backgroundDecoration: widget.backgroundDecoration,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -136,8 +138,7 @@ class _ZoomableLiveImageState extends State<ZoomableLiveImage>
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<File?> _getLivePhotoVideo() async {
|
Future<File?> _getLivePhotoVideo() async {
|
||||||
if (_enteFile.isRemoteFile &&
|
if (_enteFile.isRemoteFile && !(await isFileCached(_enteFile, liveVideo: true))) {
|
||||||
!(await isFileCached(_enteFile, liveVideo: true))) {
|
|
||||||
showShortToast(context, S.of(context).downloading);
|
showShortToast(context, S.of(context).downloading);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -205,4 +206,5 @@ class _ZoomableLiveImageState extends State<ZoomableLiveImage>
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,12 +17,14 @@ import 'package:photos/utils/toast_util.dart';
|
||||||
|
|
||||||
class ZoomableLiveImageNew extends StatefulWidget {
|
class ZoomableLiveImageNew extends StatefulWidget {
|
||||||
final EnteFile enteFile;
|
final EnteFile enteFile;
|
||||||
|
final Function(bool)? shouldDisableScroll;
|
||||||
final String? tagPrefix;
|
final String? tagPrefix;
|
||||||
final Decoration? backgroundDecoration;
|
final Decoration? backgroundDecoration;
|
||||||
|
|
||||||
const ZoomableLiveImageNew(
|
const ZoomableLiveImageNew(
|
||||||
this.enteFile, {
|
this.enteFile, {
|
||||||
Key? key,
|
Key? key,
|
||||||
|
this.shouldDisableScroll,
|
||||||
required this.tagPrefix,
|
required this.tagPrefix,
|
||||||
this.backgroundDecoration,
|
this.backgroundDecoration,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
@ -79,6 +81,7 @@ class _ZoomableLiveImageNewState extends State<ZoomableLiveImageNew>
|
||||||
content = ZoomableImage(
|
content = ZoomableImage(
|
||||||
_enteFile,
|
_enteFile,
|
||||||
tagPrefix: widget.tagPrefix,
|
tagPrefix: widget.tagPrefix,
|
||||||
|
shouldDisableScroll: widget.shouldDisableScroll,
|
||||||
backgroundDecoration: widget.backgroundDecoration,
|
backgroundDecoration: widget.backgroundDecoration,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,6 @@ import "package:flutter/cupertino.dart";
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:photos/core/configuration.dart';
|
import 'package:photos/core/configuration.dart';
|
||||||
import "package:photos/core/constants.dart";
|
|
||||||
import 'package:photos/core/event_bus.dart';
|
import 'package:photos/core/event_bus.dart';
|
||||||
import "package:photos/core/network/network.dart";
|
import "package:photos/core/network/network.dart";
|
||||||
import "package:photos/db/files_db.dart";
|
import "package:photos/db/files_db.dart";
|
||||||
|
@ -21,6 +20,7 @@ import 'package:photos/models/gallery_type.dart';
|
||||||
import "package:photos/models/metadata/common_keys.dart";
|
import "package:photos/models/metadata/common_keys.dart";
|
||||||
import 'package:photos/models/selected_files.dart';
|
import 'package:photos/models/selected_files.dart';
|
||||||
import 'package:photos/services/collections_service.dart';
|
import 'package:photos/services/collections_service.dart';
|
||||||
|
import "package:photos/services/feature_flag_service.dart";
|
||||||
import 'package:photos/services/sync_service.dart';
|
import 'package:photos/services/sync_service.dart';
|
||||||
import 'package:photos/services/update_service.dart';
|
import 'package:photos/services/update_service.dart';
|
||||||
import 'package:photos/ui/actions/collection/collection_sharing_actions.dart';
|
import 'package:photos/ui/actions/collection/collection_sharing_actions.dart';
|
||||||
|
@ -88,6 +88,7 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
|
||||||
late CollectionActions collectionActions;
|
late CollectionActions collectionActions;
|
||||||
final GlobalKey shareButtonKey = GlobalKey();
|
final GlobalKey shareButtonKey = GlobalKey();
|
||||||
bool isQuickLink = false;
|
bool isQuickLink = false;
|
||||||
|
late bool isInternalUser;
|
||||||
late GalleryType galleryType;
|
late GalleryType galleryType;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -96,6 +97,7 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
|
||||||
_selectedFilesListener = () {
|
_selectedFilesListener = () {
|
||||||
setState(() {});
|
setState(() {});
|
||||||
};
|
};
|
||||||
|
isInternalUser = FeatureFlagService.instance.isInternalUserOrDebugBuild();
|
||||||
collectionActions = CollectionActions(CollectionsService.instance);
|
collectionActions = CollectionActions(CollectionsService.instance);
|
||||||
widget.selectedFiles.addListener(_selectedFilesListener);
|
widget.selectedFiles.addListener(_selectedFilesListener);
|
||||||
_userAuthEventSubscription =
|
_userAuthEventSubscription =
|
||||||
|
|
|
@ -22,14 +22,20 @@ import "package:photos/ui/viewer/location/radius_picker_widget.dart";
|
||||||
|
|
||||||
showAddLocationSheet(
|
showAddLocationSheet(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
Location coordinates,
|
Location coordinates, {
|
||||||
) {
|
String name = '',
|
||||||
|
double radius = defaultRadiusValue,
|
||||||
|
}) {
|
||||||
showBarModalBottomSheet(
|
showBarModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return LocationTagStateProvider(
|
return LocationTagStateProvider(
|
||||||
centerPoint: coordinates,
|
centerPoint: coordinates,
|
||||||
const AddLocationSheet(),
|
AddLocationSheet(
|
||||||
|
radius: radius,
|
||||||
|
name: name,
|
||||||
|
),
|
||||||
|
radius: radius,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
shape: const RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
|
@ -45,7 +51,13 @@ showAddLocationSheet(
|
||||||
}
|
}
|
||||||
|
|
||||||
class AddLocationSheet extends StatefulWidget {
|
class AddLocationSheet extends StatefulWidget {
|
||||||
const AddLocationSheet({super.key});
|
final double radius;
|
||||||
|
final String name;
|
||||||
|
const AddLocationSheet({
|
||||||
|
super.key,
|
||||||
|
this.radius = defaultRadiusValue,
|
||||||
|
this.name = '',
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<AddLocationSheet> createState() => _AddLocationSheetState();
|
State<AddLocationSheet> createState() => _AddLocationSheetState();
|
||||||
|
@ -61,17 +73,20 @@ class _AddLocationSheetState extends State<AddLocationSheet> {
|
||||||
final ValueNotifier<bool> _submitNotifer = ValueNotifier(false);
|
final ValueNotifier<bool> _submitNotifer = ValueNotifier(false);
|
||||||
|
|
||||||
final ValueNotifier<bool> _cancelNotifier = ValueNotifier(false);
|
final ValueNotifier<bool> _cancelNotifier = ValueNotifier(false);
|
||||||
final ValueNotifier<double> _selectedRadiusNotifier =
|
late ValueNotifier<double> _selectedRadiusNotifier;
|
||||||
ValueNotifier(defaultRadiusValue);
|
|
||||||
final _focusNode = FocusNode();
|
final _focusNode = FocusNode();
|
||||||
final _textEditingController = TextEditingController();
|
final _textEditingController = TextEditingController();
|
||||||
final _isEmptyNotifier = ValueNotifier(true);
|
late final ValueNotifier<bool> _isEmptyNotifier;
|
||||||
Widget? _keyboardTopButtons;
|
Widget? _keyboardTopButtons;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
_textEditingController.text = widget.name;
|
||||||
|
_isEmptyNotifier = ValueNotifier(widget.name.isEmpty);
|
||||||
_focusNode.addListener(_focusNodeListener);
|
_focusNode.addListener(_focusNodeListener);
|
||||||
|
_selectedRadiusNotifier = ValueNotifier(widget.radius);
|
||||||
_selectedRadiusNotifier.addListener(_selectedRadiusListener);
|
_selectedRadiusNotifier.addListener(_selectedRadiusListener);
|
||||||
|
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -155,11 +170,12 @@ class _AddLocationSheetState extends State<AddLocationSheet> {
|
||||||
RadiusPickerWidget(
|
RadiusPickerWidget(
|
||||||
_selectedRadiusNotifier,
|
_selectedRadiusNotifier,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
if (widget.name.isEmpty) const SizedBox(height: 16),
|
||||||
Text(
|
if (widget.name.isEmpty)
|
||||||
S.of(context).locationTagFeatureDescription,
|
Text(
|
||||||
style: textTheme.smallMuted,
|
S.of(context).locationTagFeatureDescription,
|
||||||
),
|
style: textTheme.smallMuted,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -131,6 +131,8 @@ class SearchResultWidget extends StatelessWidget {
|
||||||
return "Day";
|
return "Day";
|
||||||
case ResultType.location:
|
case ResultType.location:
|
||||||
return "Location";
|
return "Location";
|
||||||
|
case ResultType.locationSuggestion:
|
||||||
|
return "Add Location";
|
||||||
case ResultType.fileType:
|
case ResultType.fileType:
|
||||||
return "Type";
|
return "Type";
|
||||||
case ResultType.fileExtension:
|
case ResultType.fileExtension:
|
||||||
|
|
|
@ -3,6 +3,7 @@ import "dart:async";
|
||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
import "package:flutter_animate/flutter_animate.dart";
|
import "package:flutter_animate/flutter_animate.dart";
|
||||||
import "package:photos/events/event.dart";
|
import "package:photos/events/event.dart";
|
||||||
|
import "package:photos/extensions/list.dart";
|
||||||
import "package:photos/models/search/album_search_result.dart";
|
import "package:photos/models/search/album_search_result.dart";
|
||||||
import "package:photos/models/search/generic_search_result.dart";
|
import "package:photos/models/search/generic_search_result.dart";
|
||||||
import "package:photos/models/search/recent_searches.dart";
|
import "package:photos/models/search/recent_searches.dart";
|
||||||
|
@ -83,8 +84,6 @@ class _SearchSectionAllPageState extends State<SearchSectionAllPage> {
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.hasData) {
|
if (snapshot.hasData) {
|
||||||
final sectionResults = snapshot.data!;
|
final sectionResults = snapshot.data!;
|
||||||
sectionResults
|
|
||||||
.sort((a, b) => a.name().compareTo(b.name()));
|
|
||||||
return Text(sectionResults.length.toString())
|
return Text(sectionResults.length.toString())
|
||||||
.animate()
|
.animate()
|
||||||
.fadeIn(
|
.fadeIn(
|
||||||
|
@ -109,7 +108,15 @@ class _SearchSectionAllPageState extends State<SearchSectionAllPage> {
|
||||||
future: sectionData,
|
future: sectionData,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.hasData) {
|
if (snapshot.hasData) {
|
||||||
final sectionResults = snapshot.data!;
|
List<SearchResult> sectionResults = snapshot.data!;
|
||||||
|
sectionResults.sort((a, b) => a.name().compareTo(b.name()));
|
||||||
|
if (widget.sectionType == SectionType.location) {
|
||||||
|
final result = sectionResults.splitMatch(
|
||||||
|
(e) => e.type() == ResultType.location,
|
||||||
|
);
|
||||||
|
sectionResults = result.matched;
|
||||||
|
sectionResults.addAll(result.unmatched);
|
||||||
|
}
|
||||||
return ListView.separated(
|
return ListView.separated(
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
if (sectionResults.length == index) {
|
if (sectionResults.length == index) {
|
||||||
|
|
|
@ -77,7 +77,10 @@ class SearchableItemWidget extends StatelessWidget {
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
searchResult.name(),
|
searchResult.name(),
|
||||||
style: textTheme.body,
|
style: searchResult.type() ==
|
||||||
|
ResultType.locationSuggestion
|
||||||
|
? textTheme.bodyFaint
|
||||||
|
: textTheme.body,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
|
|
|
@ -129,7 +129,6 @@ class SearchWidgetState extends State<SearchWidget> {
|
||||||
child: Container(
|
child: Container(
|
||||||
color: colorScheme.backgroundBase,
|
color: colorScheme.backgroundBase,
|
||||||
child: Container(
|
child: Container(
|
||||||
height: 44,
|
|
||||||
color: colorScheme.fillFaint,
|
color: colorScheme.fillFaint,
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
controller: textController,
|
controller: textController,
|
||||||
|
|
|
@ -2,10 +2,10 @@ import "package:dio/dio.dart";
|
||||||
import "package:flutter/foundation.dart";
|
import "package:flutter/foundation.dart";
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import "package:flutter/services.dart";
|
import "package:flutter/services.dart";
|
||||||
import "package:photos/core/constants.dart";
|
|
||||||
import "package:photos/generated/l10n.dart";
|
import "package:photos/generated/l10n.dart";
|
||||||
import 'package:photos/models/button_result.dart';
|
import 'package:photos/models/button_result.dart';
|
||||||
import 'package:photos/models/typedefs.dart';
|
import 'package:photos/models/typedefs.dart';
|
||||||
|
import "package:photos/services/feature_flag_service.dart";
|
||||||
import 'package:photos/theme/colors.dart';
|
import 'package:photos/theme/colors.dart';
|
||||||
import 'package:photos/ui/common/loading_widget.dart';
|
import 'package:photos/ui/common/loading_widget.dart';
|
||||||
import 'package:photos/ui/common/progress_dialog.dart';
|
import 'package:photos/ui/common/progress_dialog.dart';
|
||||||
|
@ -91,7 +91,8 @@ String parseErrorForUI(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// return generic error if the user is not internal and the error is not in debug mode
|
// return generic error if the user is not internal and the error is not in debug mode
|
||||||
if (!(isInternalUser && kDebugMode)) {
|
if (!(FeatureFlagService.instance.isInternalUserOrDebugBuild() &&
|
||||||
|
kDebugMode)) {
|
||||||
return genericError;
|
return genericError;
|
||||||
}
|
}
|
||||||
String errorInfo = "";
|
String errorInfo = "";
|
||||||
|
|
|
@ -10,6 +10,7 @@ import 'package:dio/dio.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:photos/core/configuration.dart';
|
import 'package:photos/core/configuration.dart';
|
||||||
|
import "package:photos/core/constants.dart";
|
||||||
import 'package:photos/core/errors.dart';
|
import 'package:photos/core/errors.dart';
|
||||||
import 'package:photos/core/event_bus.dart';
|
import 'package:photos/core/event_bus.dart';
|
||||||
import 'package:photos/core/network/network.dart';
|
import 'package:photos/core/network/network.dart';
|
||||||
|
@ -34,6 +35,7 @@ import "package:photos/services/user_service.dart";
|
||||||
import 'package:photos/utils/crypto_util.dart';
|
import 'package:photos/utils/crypto_util.dart';
|
||||||
import 'package:photos/utils/file_download_util.dart';
|
import 'package:photos/utils/file_download_util.dart';
|
||||||
import 'package:photos/utils/file_uploader_util.dart';
|
import 'package:photos/utils/file_uploader_util.dart';
|
||||||
|
import "package:photos/utils/file_util.dart";
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:tuple/tuple.dart';
|
import 'package:tuple/tuple.dart';
|
||||||
import "package:uuid/uuid.dart";
|
import "package:uuid/uuid.dart";
|
||||||
|
@ -69,6 +71,7 @@ class FileUploader {
|
||||||
late ProcessType _processType;
|
late ProcessType _processType;
|
||||||
late bool _isBackground;
|
late bool _isBackground;
|
||||||
late SharedPreferences _prefs;
|
late SharedPreferences _prefs;
|
||||||
|
|
||||||
// _hasInitiatedForceUpload is used to track if user attempted force upload
|
// _hasInitiatedForceUpload is used to track if user attempted force upload
|
||||||
// where files are uploaded directly (without adding them to DB). In such
|
// where files are uploaded directly (without adding them to DB). In such
|
||||||
// cases, we don't want to clear the stale upload files. See #removeStaleFiles
|
// cases, we don't want to clear the stale upload files. See #removeStaleFiles
|
||||||
|
@ -307,12 +310,36 @@ class FileUploader {
|
||||||
return file.path.contains(kUploadTempPrefix) &&
|
return file.path.contains(kUploadTempPrefix) &&
|
||||||
file.path.contains(".encrypted");
|
file.path.contains(".encrypted");
|
||||||
});
|
});
|
||||||
if (filesToDelete.isEmpty) {
|
if (filesToDelete.isNotEmpty) {
|
||||||
return;
|
_logger.info('cleaning up state files ${filesToDelete.length}');
|
||||||
|
for (final file in filesToDelete) {
|
||||||
|
await file.delete();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_logger.info('cleaning up state files ${filesToDelete.length}');
|
|
||||||
for (final file in filesToDelete) {
|
if (Platform.isAndroid) {
|
||||||
await file.delete();
|
final sharedMediaDir =
|
||||||
|
Configuration.instance.getSharedMediaDirectory() + "/";
|
||||||
|
final sharedFiles = await Directory(sharedMediaDir).list().toList();
|
||||||
|
if (sharedFiles.isNotEmpty) {
|
||||||
|
_logger.info('Shared media directory cleanup ${sharedFiles.length}');
|
||||||
|
final int ownerID = Configuration.instance.getUserID()!;
|
||||||
|
final existingLocalFileIDs =
|
||||||
|
await FilesDB.instance.getExistingLocalFileIDs(ownerID);
|
||||||
|
final Set<String> trackedSharedFilePaths = {};
|
||||||
|
for (String localID in existingLocalFileIDs) {
|
||||||
|
if (localID.contains(sharedMediaIdentifier)) {
|
||||||
|
trackedSharedFilePaths
|
||||||
|
.add(getSharedMediaPathFromLocalID(localID));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (final file in sharedFiles) {
|
||||||
|
if (!trackedSharedFilePaths.contains(file.path)) {
|
||||||
|
_logger.info('Deleting stale shared media file ${file.path}');
|
||||||
|
await file.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
_logger.severe("Failed to remove stale files", e, s);
|
_logger.severe("Failed to remove stale files", e, s);
|
||||||
|
@ -431,7 +458,13 @@ class FileUploader {
|
||||||
encryptedFilePath,
|
encryptedFilePath,
|
||||||
key: key,
|
key: key,
|
||||||
);
|
);
|
||||||
final thumbnailData = mediaUploadData.thumbnail;
|
late final Uint8List? thumbnailData;
|
||||||
|
if (mediaUploadData.thumbnail == null &&
|
||||||
|
file.fileType == FileType.video) {
|
||||||
|
thumbnailData = base64Decode(blackThumbnailBase64);
|
||||||
|
} else {
|
||||||
|
thumbnailData = mediaUploadData.thumbnail;
|
||||||
|
}
|
||||||
|
|
||||||
final EncryptionResult encryptedThumbnailData =
|
final EncryptionResult encryptedThumbnailData =
|
||||||
await CryptoUtil.encryptChaCha(
|
await CryptoUtil.encryptChaCha(
|
||||||
|
@ -493,17 +526,21 @@ class FileUploader {
|
||||||
CryptoUtil.bin2base64(encryptedFileKeyData.encryptedData!);
|
CryptoUtil.bin2base64(encryptedFileKeyData.encryptedData!);
|
||||||
final keyDecryptionNonce =
|
final keyDecryptionNonce =
|
||||||
CryptoUtil.bin2base64(encryptedFileKeyData.nonce!);
|
CryptoUtil.bin2base64(encryptedFileKeyData.nonce!);
|
||||||
|
final Map<String, dynamic> pubMetadata = {};
|
||||||
MetadataRequest? pubMetadataRequest;
|
MetadataRequest? pubMetadataRequest;
|
||||||
if ((mediaUploadData.height ?? 0) != 0 &&
|
if ((mediaUploadData.height ?? 0) != 0 &&
|
||||||
(mediaUploadData.width ?? 0) != 0) {
|
(mediaUploadData.width ?? 0) != 0) {
|
||||||
final pubMetadata = {
|
pubMetadata[heightKey] = mediaUploadData.height;
|
||||||
heightKey: mediaUploadData.height,
|
pubMetadata[widthKey] = mediaUploadData.width;
|
||||||
widthKey: mediaUploadData.width,
|
}
|
||||||
};
|
if (mediaUploadData.motionPhotoStartIndex != null) {
|
||||||
if (mediaUploadData.motionPhotoStartIndex != null) {
|
pubMetadata[motionVideoIndexKey] =
|
||||||
pubMetadata[motionVideoIndexKey] =
|
mediaUploadData.motionPhotoStartIndex;
|
||||||
mediaUploadData.motionPhotoStartIndex;
|
}
|
||||||
}
|
if (mediaUploadData.thumbnail == null) {
|
||||||
|
pubMetadata[noThumbKey] = true;
|
||||||
|
}
|
||||||
|
if (pubMetadata.isNotEmpty) {
|
||||||
pubMetadataRequest = await getPubMetadataRequest(
|
pubMetadataRequest = await getPubMetadataRequest(
|
||||||
file,
|
file,
|
||||||
pubMetadata,
|
pubMetadata,
|
||||||
|
|
|
@ -208,6 +208,10 @@ Future<Uint8List?> _getThumbnailForUpload(
|
||||||
quality: thumbnailQuality,
|
quality: thumbnailQuality,
|
||||||
);
|
);
|
||||||
if (thumbnailData == null) {
|
if (thumbnailData == null) {
|
||||||
|
// allow videos to be uploaded without thumbnails
|
||||||
|
if (asset.type == AssetType.video) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
throw InvalidFileError(
|
throw InvalidFileError(
|
||||||
"no thumbnail : ${file.fileType} ${file.tag}",
|
"no thumbnail : ${file.fileType} ${file.tag}",
|
||||||
InvalidReason.thumbnailMissing,
|
InvalidReason.thumbnailMissing,
|
||||||
|
@ -227,6 +231,10 @@ Future<Uint8List?> _getThumbnailForUpload(
|
||||||
final String errMessage =
|
final String errMessage =
|
||||||
"thumbErr for ${file.fileType}, ${extension(file.displayName)} ${file.tag}";
|
"thumbErr for ${file.fileType}, ${extension(file.displayName)} ${file.tag}";
|
||||||
_logger.warning(errMessage, e);
|
_logger.warning(errMessage, e);
|
||||||
|
// allow videos to be uploaded without thumbnails
|
||||||
|
if (asset.type == AssetType.video) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
throw InvalidFileError(errMessage, InvalidReason.thumbnailMissing);
|
throw InvalidFileError(errMessage, InvalidReason.thumbnailMissing);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
12
pubspec.lock
12
pubspec.lock
|
@ -89,6 +89,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.1"
|
version: "1.2.1"
|
||||||
|
battery_info:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: battery_info
|
||||||
|
sha256: "5d5249c87a600a0a20d6b2f5ffdf90d711bccb1bfd3a58e5a6228f270031c680"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.1"
|
||||||
bip39:
|
bip39:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -1389,8 +1397,8 @@ packages:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: HEAD
|
ref: "5f26aef45ed9f5e563c26f90c1e21b3339ed906d"
|
||||||
resolved-ref: "1318dce97f3aae5ec9bdf7491d5eff0ad6beb378"
|
resolved-ref: "5f26aef45ed9f5e563c26f90c1e21b3339ed906d"
|
||||||
url: "https://github.com/ente-io/onnxruntime.git"
|
url: "https://github.com/ente-io/onnxruntime.git"
|
||||||
source: git
|
source: git
|
||||||
version: "1.1.0"
|
version: "1.1.0"
|
||||||
|
|
|
@ -12,8 +12,7 @@ description: ente photos application
|
||||||
# Read more about iOS versioning at
|
# Read more about iOS versioning at
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
|
|
||||||
version: 0.8.58+578
|
version: 0.8.64+584
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=3.0.0 <4.0.0"
|
sdk: ">=3.0.0 <4.0.0"
|
||||||
|
|
||||||
|
@ -23,6 +22,7 @@ dependencies:
|
||||||
animated_list_plus: ^0.4.5
|
animated_list_plus: ^0.4.5
|
||||||
archive: ^3.1.2
|
archive: ^3.1.2
|
||||||
background_fetch: ^1.2.1
|
background_fetch: ^1.2.1
|
||||||
|
battery_info: ^1.1.1
|
||||||
bip39: ^1.0.6
|
bip39: ^1.0.6
|
||||||
cached_network_image: ^3.0.0
|
cached_network_image: ^3.0.0
|
||||||
chewie:
|
chewie:
|
||||||
|
@ -119,7 +119,9 @@ dependencies:
|
||||||
|
|
||||||
# open_file: ^3.2.1
|
# open_file: ^3.2.1
|
||||||
onnxruntime:
|
onnxruntime:
|
||||||
git: "https://github.com/ente-io/onnxruntime.git"
|
git:
|
||||||
|
url: https://github.com/ente-io/onnxruntime.git
|
||||||
|
ref: 5f26aef45ed9f5e563c26f90c1e21b3339ed906d
|
||||||
open_mail_app: ^0.4.5
|
open_mail_app: ^0.4.5
|
||||||
package_info_plus: ^4.1.0
|
package_info_plus: ^4.1.0
|
||||||
page_transition: ^2.0.2
|
page_transition: ^2.0.2
|
||||||
|
|
Loading…
Reference in a new issue