Merge branch 'main' into redesign_search_tab
This commit is contained in:
commit
faa40119b4
58 changed files with 2771 additions and 353 deletions
10
.github/workflows/build.yml
vendored
10
.github/workflows/build.yml
vendored
|
@ -10,8 +10,8 @@ jobs:
|
|||
steps:
|
||||
|
||||
# Setup Java environment in order to build the Android app.
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-java@v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'adopt'
|
||||
java-version: '11'
|
||||
|
@ -47,18 +47,18 @@ jobs:
|
|||
run: sha256sum build/app/outputs/flutter-apk/ente.apk > build/app/outputs/flutter-apk/sha256sum
|
||||
|
||||
# Upload generated apk to the artifacts.
|
||||
- uses: actions/upload-artifact@v2
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release-apk
|
||||
path: build/app/outputs/flutter-apk/ente.apk
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release-checksum
|
||||
path: build/app/outputs/flutter-apk/sha256sum
|
||||
|
||||
# Create a pre-release
|
||||
- uses: ncipollo/release-action@v1
|
||||
- uses: ncipollo/release-action@v1.14.0
|
||||
with:
|
||||
artifacts: "build/app/outputs/flutter-apk/ente.apk,build/app/outputs/flutter-apk/sha256sum"
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
|
3
android/.gitignore
vendored
3
android/.gitignore
vendored
|
@ -5,6 +5,3 @@ gradle-wrapper.jar
|
|||
/gradlew.bat
|
||||
/local.properties
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
|
@ -27,7 +27,7 @@ FEATURES
|
|||
- und noch VIELES mehr!
|
||||
|
||||
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
|
||||
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:
|
||||
- background_fetch (1.2.1):
|
||||
- Flutter
|
||||
- battery_info (0.0.1):
|
||||
- Flutter
|
||||
- connectivity_plus (0.0.1):
|
||||
- Flutter
|
||||
- ReachabilitySwift
|
||||
|
@ -213,6 +215,7 @@ PODS:
|
|||
|
||||
DEPENDENCIES:
|
||||
- background_fetch (from `.symlinks/plugins/background_fetch/ios`)
|
||||
- battery_info (from `.symlinks/plugins/battery_info/ios`)
|
||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||
- file_saver (from `.symlinks/plugins/file_saver/ios`)
|
||||
|
@ -286,6 +289,8 @@ SPEC REPOS:
|
|||
EXTERNAL SOURCES:
|
||||
background_fetch:
|
||||
:path: ".symlinks/plugins/background_fetch/ios"
|
||||
battery_info:
|
||||
:path: ".symlinks/plugins/battery_info/ios"
|
||||
connectivity_plus:
|
||||
:path: ".symlinks/plugins/connectivity_plus/ios"
|
||||
device_info_plus:
|
||||
|
@ -377,6 +382,7 @@ EXTERNAL SOURCES:
|
|||
|
||||
SPEC CHECKSUMS:
|
||||
background_fetch: 896944864b038d2837fc750d470e9841e1e6a363
|
||||
battery_info: 09f5c9ee65394f2291c8c6227bedff345b8a730c
|
||||
connectivity_plus: 53efb943fc2882c8512d84c45707bcabc4c36076
|
||||
device_info_plus: 7545d84d8d1b896cb16a4ff98c19f07ec4b298ea
|
||||
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
|
||||
|
|
|
@ -276,6 +276,7 @@
|
|||
"${BUILT_PRODUCTS_DIR}/SentryPrivate/SentryPrivate.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/Toast/Toast.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}/device_info_plus/device_info_plus.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}/Toast.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}/device_info_plus.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/l10n/l10n.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/ui/tabs/home_widget.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 {
|
||||
static const initialInteractionTimeout = Duration(seconds: 10);
|
||||
static const defaultInteractionTimeout = Duration(seconds: 5);
|
||||
|
||||
final _logger = Logger("EnteAppState");
|
||||
late Locale locale;
|
||||
late Timer _userInteractionTimer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
@ -57,7 +53,6 @@ class _EnteAppState extends State<EnteApp> with WidgetsBindingObserver {
|
|||
locale = widget.locale;
|
||||
setupIntentAction();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
_setupInteractionTimer(timeout: initialInteractionTimeout);
|
||||
}
|
||||
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
if (Platform.isAndroid || kDebugMode) {
|
||||
return Listener(
|
||||
onPointerDown: (event) {
|
||||
SemanticSearchService.instance.pauseIndexing();
|
||||
debugPrint("user is interacting with the app");
|
||||
_resetTimer();
|
||||
MachineLearningController.instance.onUserInteraction();
|
||||
},
|
||||
child: AdaptiveTheme(
|
||||
light: lightThemeData,
|
||||
|
@ -149,7 +126,6 @@ class _EnteAppState extends State<EnteApp> with WidgetsBindingObserver {
|
|||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
_userInteractionTimer.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
|
@ -25,9 +25,9 @@ import 'package:photos/services/billing_service.dart';
|
|||
import 'package:photos/services/collections_service.dart';
|
||||
import 'package:photos/services/favorites_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/search_service.dart';
|
||||
import "package:photos/services/semantic_search/semantic_search_service.dart";
|
||||
import 'package:photos/services/sync_service.dart';
|
||||
import 'package:photos/utils/crypto_util.dart';
|
||||
import 'package:photos/utils/file_uploader.dart';
|
||||
|
|
|
@ -67,4 +67,30 @@ const galleryGridSpacing = 2.0;
|
|||
|
||||
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();
|
||||
}
|
||||
|
||||
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 {
|
||||
await _isar.writeTxn(() async {
|
||||
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_sync_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/push_service.dart';
|
||||
import 'package:photos/services/remote_sync_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/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(SemanticSearchService.instance.init());
|
||||
MachineLearningController.instance.init();
|
||||
// Can not including existing tf/ml binaries as they are not being built
|
||||
// from source.
|
||||
// See https://gitlab.com/fdroid/fdroiddata/-/merge_requests/12671#note_1294346819
|
||||
|
|
|
@ -11,6 +11,7 @@ const heightKey = 'h';
|
|||
const latKey = "lat";
|
||||
const longKey = "long";
|
||||
const motionVideoIndexKey = "mvi";
|
||||
const noThumbKey = "noThumb";
|
||||
|
||||
class MagicMetadata {
|
||||
// 0 -> visible
|
||||
|
@ -47,6 +48,13 @@ class PubMagicMetadata {
|
|||
// photo
|
||||
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({
|
||||
this.editedTime,
|
||||
this.editedName,
|
||||
|
@ -57,6 +65,7 @@ class PubMagicMetadata {
|
|||
this.lat,
|
||||
this.long,
|
||||
this.mvi,
|
||||
this.noThumb,
|
||||
});
|
||||
|
||||
factory PubMagicMetadata.fromEncodedJson(String encodedJson) =>
|
||||
|
@ -77,6 +86,7 @@ class PubMagicMetadata {
|
|||
lat: map[latKey],
|
||||
long: map[longKey],
|
||||
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/models/collection/collection.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/typedefs.dart";
|
||||
import "package:photos/services/collections_service.dart";
|
||||
|
@ -24,6 +25,7 @@ enum ResultType {
|
|||
collection,
|
||||
file,
|
||||
location,
|
||||
locationSuggestion,
|
||||
month,
|
||||
year,
|
||||
fileType,
|
||||
|
@ -243,10 +245,10 @@ extension SectionTypeExtensions on SectionType {
|
|||
}) {
|
||||
switch (this) {
|
||||
case SectionType.face:
|
||||
return SearchService.instance.getAllLocationTags(limit);
|
||||
return Future.value(List<GenericSearchResult>.empty());
|
||||
|
||||
case SectionType.content:
|
||||
return SearchService.instance.getAllLocationTags(limit);
|
||||
return Future.value(List<GenericSearchResult>.empty());
|
||||
|
||||
case SectionType.moment:
|
||||
return SearchService.instance.getRandomMomentsSearchResults(context);
|
||||
|
|
|
@ -1372,10 +1372,10 @@ class CollectionsService {
|
|||
}
|
||||
|
||||
Future<void> move(
|
||||
int toCollectionID,
|
||||
int fromCollectionID,
|
||||
List<EnteFile> files,
|
||||
) async {
|
||||
List<EnteFile> files, {
|
||||
required int toCollectionID,
|
||||
required int fromCollectionID,
|
||||
}) async {
|
||||
_validateMoveRequest(toCollectionID, fromCollectionID, files);
|
||||
files.removeWhere((element) => element.uploadedFileID == null);
|
||||
if (files.isEmpty) {
|
||||
|
@ -1443,9 +1443,19 @@ class CollectionsService {
|
|||
int fromCollectionID,
|
||||
List<EnteFile> files,
|
||||
) {
|
||||
final int userID = Configuration.instance.getUserID()!;
|
||||
if (toCollectionID == fromCollectionID) {
|
||||
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) {
|
||||
if (file.uploadedFileID == null) {
|
||||
throw AssertionError("Can only move uploaded memories");
|
||||
|
|
|
@ -71,10 +71,9 @@ class FeatureFlagService {
|
|||
bool isInternalUserOrDebugBuild() {
|
||||
final String? email = Configuration.instance.getEmail();
|
||||
final userID = Configuration.instance.getUserID();
|
||||
isInternalUser = (email != null && email.endsWith("@ente.io")) ||
|
||||
return (email != null && email.endsWith("@ente.io")) ||
|
||||
_internalUserIDs.contains(userID) ||
|
||||
kDebugMode;
|
||||
return isInternalUser;
|
||||
}
|
||||
|
||||
Future<void> fetchFeatureFlags() async {
|
||||
|
|
|
@ -57,21 +57,25 @@ extension HiddenService on CollectionsService {
|
|||
Future<Collection> clubAllDefaultHiddenToOne(
|
||||
List<Collection> allDefaultHidden,
|
||||
) async {
|
||||
final Collection result = allDefaultHidden.first;
|
||||
|
||||
for (Collection defaultHidden in allDefaultHidden) {
|
||||
// select first collection as default hidden where all files will be clubbed
|
||||
final Collection defaultHidden = allDefaultHidden.first;
|
||||
for (Collection hidden in allDefaultHidden) {
|
||||
try {
|
||||
if (defaultHidden.id == result.id) {
|
||||
if (hidden.id == defaultHidden.id) {
|
||||
continue;
|
||||
}
|
||||
final filesInCollection = (await FilesDB.instance.getFilesInCollection(
|
||||
defaultHidden.id,
|
||||
hidden.id,
|
||||
galleryLoadStartTime,
|
||||
galleryLoadEndTime,
|
||||
))
|
||||
.files;
|
||||
await move(result.id, defaultHidden.id, filesInCollection);
|
||||
await CollectionsService.instance.trashEmptyCollection(defaultHidden);
|
||||
await move(
|
||||
filesInCollection,
|
||||
toCollectionID: defaultHidden.id,
|
||||
fromCollectionID: hidden.id,
|
||||
);
|
||||
await CollectionsService.instance.trashEmptyCollection(hidden);
|
||||
} catch (e, s) {
|
||||
_logger.severe(
|
||||
"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
|
||||
|
@ -137,7 +141,18 @@ extension HiddenService on CollectionsService {
|
|||
_logger.finest('file already part of hidden collection');
|
||||
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(
|
||||
LocalPhotosUpdatedEvent(
|
||||
|
|
|
@ -7,6 +7,7 @@ import "package:logging/logging.dart";
|
|||
import "package:photos/core/constants.dart";
|
||||
import "package:photos/core/event_bus.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/file/file.dart";
|
||||
import "package:photos/models/local_entity_data.dart";
|
||||
|
@ -45,6 +46,8 @@ class LocationService {
|
|||
List<EnteFile> allFiles,
|
||||
String query,
|
||||
) async {
|
||||
final EnteWatch w = EnteWatch("cities_search")..start();
|
||||
w.log('start for files ${allFiles.length} and query $query');
|
||||
final result = await _computer.compute(
|
||||
getCityResults,
|
||||
param: {
|
||||
|
@ -53,6 +56,10 @@ class LocationService {
|
|||
"files": allFiles,
|
||||
},
|
||||
);
|
||||
w.log(
|
||||
'end for query: $query on ${allFiles.length} files, found '
|
||||
'${result.length} cities',
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@ -235,31 +242,29 @@ Future<List<City>> parseCities(Map args) async {
|
|||
|
||||
Map<City, List<EnteFile>> getCityResults(Map args) {
|
||||
final query = (args["query"] as String).toLowerCase();
|
||||
final cities = args["cities"] as List<City>;
|
||||
final files = args["files"] as List<EnteFile>;
|
||||
final List<City> cities = args["cities"] as List<City>;
|
||||
final List<EnteFile> files = args["files"] as List<EnteFile>;
|
||||
|
||||
final matchingCities = cities.where(
|
||||
(city) => city.city.toLowerCase().contains(query),
|
||||
);
|
||||
final matchingCities = cities
|
||||
.where(
|
||||
(city) => city.city.toLowerCase().contains(query),
|
||||
)
|
||||
.toList();
|
||||
|
||||
final Map<City, List<EnteFile>> results = {};
|
||||
for (final city in matchingCities) {
|
||||
final List<EnteFile> matchingFiles = [];
|
||||
final cityLocation = Location(latitude: city.lat, longitude: city.lng);
|
||||
for (final file in files) {
|
||||
if (file.hasLocation) {
|
||||
if (isFileInsideLocationTag(
|
||||
cityLocation,
|
||||
file.location!,
|
||||
defaultCityRadius,
|
||||
)) {
|
||||
matchingFiles.add(file);
|
||||
}
|
||||
for (final file in files) {
|
||||
if (!file.hasLocation) continue; // Skip files without location
|
||||
for (final city in matchingCities) {
|
||||
final cityLocation = Location(latitude: city.lat, longitude: city.lng);
|
||||
if (isFileInsideLocationTag(
|
||||
cityLocation,
|
||||
file.location!,
|
||||
defaultCityRadius,
|
||||
)) {
|
||||
results.putIfAbsent(city, () => []).add(file);
|
||||
break; // Stop searching once a file is matched with a city
|
||||
}
|
||||
}
|
||||
if (matchingFiles.isNotEmpty) {
|
||||
results[city] = matchingFiles;
|
||||
}
|
||||
}
|
||||
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/models/embedding.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/file_download_util.dart";
|
||||
import "package:shared_preferences/shared_preferences.dart";
|
||||
|
@ -53,13 +53,22 @@ class EmbeddingStore {
|
|||
final fileMap = await FilesDB.instance
|
||||
.getFilesFromIDs(pendingItems.map((e) => e.fileID).toList());
|
||||
_logger.info("Pushing ${pendingItems.length} embeddings");
|
||||
final deletedEntries = <int>[];
|
||||
for (final item in pendingItems) {
|
||||
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) {
|
||||
_logger.severe(e, s);
|
||||
}
|
||||
}
|
||||
if (deletedEntries.isNotEmpty) {
|
||||
await EmbeddingsDB.instance.deleteEmbeddings(deletedEntries);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> storeEmbedding(EnteFile file, Embedding embedding) async {
|
|
@ -1,7 +1,7 @@
|
|||
import "package:clip_ggml/clip_ggml.dart";
|
||||
import "package:computer/computer.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 {
|
||||
static const kModelBucketEndpoint = "https://models.ente.io/";
|
|
@ -1,9 +1,9 @@
|
|||
import "package:computer/computer.dart";
|
||||
import "package:logging/logging.dart";
|
||||
import "package:onnxruntime/onnxruntime.dart";
|
||||
import "package:photos/services/semantic_search/frameworks/ml_framework.dart";
|
||||
import "package:photos/services/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/ml_framework.dart';
|
||||
import 'package:photos/services/machine_learning/semantic_search/frameworks/onnx/onnx_image_encoder.dart';
|
||||
import 'package:photos/services/machine_learning/semantic_search/frameworks/onnx/onnx_text_encoder.dart';
|
||||
|
||||
class ONNX extends MLFramework {
|
||||
static const kModelBucketEndpoint = "https://models.ente.io/";
|
|
@ -5,7 +5,7 @@ import "dart:typed_data";
|
|||
import "package:flutter/services.dart";
|
||||
import "package:logging/logging.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 {
|
||||
static const kVocabFilePath = "assets/models/clip/bpe_simple_vocab_16e6.txt";
|
|
@ -1,5 +1,6 @@
|
|||
import "dart:async";
|
||||
import "dart:collection";
|
||||
import "dart:io";
|
||||
|
||||
import "package:computer/computer.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/embedding_updated_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/file/file.dart";
|
||||
import "package:photos/services/collections_service.dart";
|
||||
import "package:photos/services/semantic_search/embedding_store.dart";
|
||||
import "package:photos/services/semantic_search/frameworks/ggml.dart";
|
||||
import "package:photos/services/semantic_search/frameworks/ml_framework.dart";
|
||||
import 'package:photos/services/semantic_search/frameworks/onnx/onnx.dart';
|
||||
import 'package:photos/services/machine_learning/semantic_search/embedding_store.dart';
|
||||
import 'package:photos/services/machine_learning/semantic_search/frameworks/ggml.dart';
|
||||
import 'package:photos/services/machine_learning/semantic_search/frameworks/ml_framework.dart';
|
||||
import 'package:photos/services/machine_learning/semantic_search/frameworks/onnx/onnx.dart';
|
||||
import "package:photos/utils/debouncer.dart";
|
||||
import "package:photos/utils/device_info.dart";
|
||||
import "package:photos/utils/local_settings.dart";
|
||||
|
@ -50,22 +52,10 @@ class SemanticSearchService {
|
|||
Future<List<EnteFile>>? _ongoingRequest;
|
||||
List<Embedding> _cachedEmbeddings = <Embedding>[];
|
||||
PendingQuery? _nextQuery;
|
||||
Completer<void> _userInteraction = Completer<void>();
|
||||
Completer<void> _mlController = Completer<void>();
|
||||
|
||||
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 {
|
||||
if (!LocalSettings.instance.hasEnabledMagicSearch()) {
|
||||
return;
|
||||
|
@ -111,6 +101,17 @@ class SemanticSearchService {
|
|||
if (shouldSyncImmediately) {
|
||||
unawaited(sync());
|
||||
}
|
||||
if (Platform.isAndroid) {
|
||||
Bus.instance.on<MachineLearningControlEvent>().listen((event) {
|
||||
if (event.shouldRun) {
|
||||
_startIndexing();
|
||||
} else {
|
||||
_pauseIndexing();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
_startIndexing();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> release() async {
|
||||
|
@ -242,15 +243,23 @@ class SemanticSearchService {
|
|||
|
||||
final ignoredCollections =
|
||||
CollectionsService.instance.getHiddenCollectionIds();
|
||||
final deletedEntries = <int>[];
|
||||
for (final result in queryResults) {
|
||||
final file = filesMap[result.id];
|
||||
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");
|
||||
|
||||
if (deletedEntries.isNotEmpty) {
|
||||
unawaited(EmbeddingsDB.instance.deleteEmbeddings(deletedEntries));
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
|
@ -294,9 +303,9 @@ class SemanticSearchService {
|
|||
if (!_frameworkInitialization.isCompleted) {
|
||||
return;
|
||||
}
|
||||
if (!_userInteraction.isCompleted) {
|
||||
_logger.info("Waiting for user interactions to stop...");
|
||||
await _userInteraction.future;
|
||||
if (!_mlController.isCompleted) {
|
||||
_logger.info("Waiting for a green signal from controller...");
|
||||
await _mlController.future;
|
||||
}
|
||||
try {
|
||||
final thumbnail = await getThumbnailForUploadedFile(file);
|
||||
|
@ -369,6 +378,20 @@ class SemanticSearchService {
|
|||
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) {
|
|
@ -3,6 +3,7 @@ import "dart:math";
|
|||
import "package:flutter/cupertino.dart";
|
||||
import "package:intl/intl.dart";
|
||||
import 'package:logging/logging.dart';
|
||||
import "package:photos/core/constants.dart";
|
||||
import 'package:photos/core/event_bus.dart';
|
||||
import 'package:photos/data/holidays.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_type.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/search/album_search_result.dart';
|
||||
import 'package:photos/models/search/generic_search_result.dart';
|
||||
import "package:photos/models/search/search_types.dart";
|
||||
import 'package:photos/services/collections_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/ui/viewer/location/add_location_sheet.dart";
|
||||
import "package:photos/ui/viewer/location/location_screen.dart";
|
||||
import 'package:photos/utils/date_time_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 =
|
||||
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 (!locationTagNames.contains(entry.key.city)) {
|
||||
if (!locationTagNames.contains(city.city)) {
|
||||
searchResults.add(
|
||||
GenericSearchResult(
|
||||
ResultType.location,
|
||||
entry.key.city,
|
||||
entry.value,
|
||||
city.city,
|
||||
results[city]!,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -701,6 +711,7 @@ class SearchService {
|
|||
final locationTagEntities =
|
||||
(await LocationService.instance.getLocationTags());
|
||||
final allFiles = await getAllFiles();
|
||||
final List<EnteFile> filesWithNoLocTag = [];
|
||||
|
||||
for (int i = 0; i < locationTagEntities.length; i++) {
|
||||
if (limit != null && i >= limit) break;
|
||||
|
@ -709,15 +720,22 @@ class SearchService {
|
|||
|
||||
for (EnteFile file in allFiles) {
|
||||
if (file.hasLocation) {
|
||||
bool hasLocationTag = false;
|
||||
for (LocalEntity<LocationTag> tag in tagToItemsMap.keys) {
|
||||
if (isFileInsideLocationTag(
|
||||
tag.item.centerPoint,
|
||||
file.location!,
|
||||
tag.item.radius,
|
||||
)) {
|
||||
hasLocationTag = true;
|
||||
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;
|
||||
} catch (e) {
|
||||
_logger.severe("Error in getAllLocationTags", e);
|
||||
|
|
|
@ -154,9 +154,12 @@ class UpdateService {
|
|||
);
|
||||
}
|
||||
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(
|
||||
"app store",
|
||||
"App Store",
|
||||
"https://apps.apple.com/in/app/ente-photos/id1542026904",
|
||||
);
|
||||
}
|
||||
|
|
|
@ -14,11 +14,14 @@ import "package:photos/utils/debouncer.dart";
|
|||
class LocationTagStateProvider extends StatefulWidget {
|
||||
final LocalEntity<LocationTag>? locationTagEntity;
|
||||
final Location? centerPoint;
|
||||
final double? radius;
|
||||
final Widget child;
|
||||
const LocationTagStateProvider(
|
||||
this.child, {
|
||||
this.centerPoint,
|
||||
this.locationTagEntity,
|
||||
// if the locationTagEntity is null, we use the centerPoint and radius
|
||||
this.radius,
|
||||
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
|
||||
///value to the list of default radius values only for this location tag and
|
||||
///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 =
|
||||
Bus.instance.on<LocationTagUpdatedEvent>().listen((event) {
|
||||
|
|
|
@ -42,7 +42,7 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
|
|||
_passwordController.text = _volatilePassword!;
|
||||
Future.delayed(
|
||||
Duration.zero,
|
||||
() => verifyPassword(_volatilePassword!),
|
||||
() => verifyPassword(_volatilePassword!),
|
||||
);
|
||||
}
|
||||
_passwordFocusNode.addListener(() {
|
||||
|
@ -100,69 +100,68 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
|
|||
}
|
||||
|
||||
Future<void> verifyPassword(String password) async {
|
||||
FocusScope.of(context).unfocus();
|
||||
final dialog =
|
||||
createProgressDialog(context, S.of(context).pleaseWait);
|
||||
await dialog.show();
|
||||
try {
|
||||
final kek = await Configuration.instance.decryptSecretsAndGetKeyEncKey(
|
||||
password,
|
||||
Configuration.instance.getKeyAttributes()!,
|
||||
);
|
||||
_registerSRPForExistingUsers(kek).ignore();
|
||||
} on KeyDerivationError catch (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;
|
||||
}
|
||||
FocusScope.of(context).unfocus();
|
||||
final dialog = createProgressDialog(context, S.of(context).pleaseWait);
|
||||
await dialog.show();
|
||||
try {
|
||||
final kek = await Configuration.instance.decryptSecretsAndGetKeyEncKey(
|
||||
password,
|
||||
Configuration.instance.getKeyAttributes()!,
|
||||
);
|
||||
_registerSRPForExistingUsers(kek).ignore();
|
||||
} on KeyDerivationError catch (e, s) {
|
||||
_logger.severe("Password verification failed", e, s);
|
||||
await dialog.hide();
|
||||
Configuration.instance.setVolatilePassword(null);
|
||||
Bus.instance.fire(SubscriptionPurchasedEvent());
|
||||
unawaited(
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
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 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 {
|
||||
|
@ -266,8 +265,8 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
|
|||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.spaceBetween,
|
||||
children: [
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
|
@ -280,17 +279,13 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
|
|||
),
|
||||
);
|
||||
},
|
||||
child: Center(
|
||||
child: Text(
|
||||
S.of(context).forgotPassword,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium!
|
||||
.copyWith(
|
||||
fontSize: 14,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
S.of(context).forgotPassword,
|
||||
style:
|
||||
Theme.of(context).textTheme.titleMedium!.copyWith(
|
||||
fontSize: 14,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
|
@ -306,17 +301,13 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
|
|||
Navigator.of(context)
|
||||
.popUntil((route) => route.isFirst);
|
||||
},
|
||||
child: Center(
|
||||
child: Text(
|
||||
S.of(context).changeEmail,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium!
|
||||
.copyWith(
|
||||
fontSize: 14,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
S.of(context).changeEmail,
|
||||
style:
|
||||
Theme.of(context).textTheme.titleMedium!.copyWith(
|
||||
fontSize: 14,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
@ -558,9 +558,9 @@ class CollectionActions {
|
|||
);
|
||||
} else {
|
||||
await collectionsService.move(
|
||||
entry.key,
|
||||
collection.id,
|
||||
entry.value,
|
||||
toCollectionID: entry.key,
|
||||
fromCollectionID: collection.id,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -398,9 +398,9 @@ class AlbumVerticalListWidget extends StatelessWidget {
|
|||
try {
|
||||
final int fromCollectionID = selectedFiles!.files.first.collectionID!;
|
||||
await CollectionsService.instance.move(
|
||||
toCollectionID,
|
||||
fromCollectionID,
|
||||
selectedFiles!.files.toList(),
|
||||
toCollectionID: toCollectionID,
|
||||
fromCollectionID: fromCollectionID,
|
||||
);
|
||||
await dialog.hide();
|
||||
unawaited(RemoteSyncService.instance.sync(silently: true));
|
||||
|
|
|
@ -60,7 +60,6 @@ class DynamicFAB extends StatelessWidget {
|
|||
} else {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
height: 56,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: OutlinedButton(
|
||||
onPressed:
|
||||
|
|
|
@ -153,6 +153,7 @@ class _FullScreenMemoryState extends State<FullScreenMemory> {
|
|||
final inheritedData = FullScreenMemoryData.of(context)!;
|
||||
final showStepProgressIndicator = inheritedData.memories.length < 60;
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
extendBodyBehindAppBar: true,
|
||||
appBar: AppBar(
|
||||
toolbarHeight: 84,
|
||||
|
|
|
@ -221,6 +221,7 @@ class _MemoryCoverWidgetState extends State<MemoryCoverWidget> {
|
|||
child: ThumbnailWidget(
|
||||
memory.file,
|
||||
shouldShowArchiveStatus: false,
|
||||
shouldShowSyncStatus: false,
|
||||
key: Key("memories" + memory.file.tag),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -49,7 +49,11 @@ class _PaymentWebPageState extends State<PaymentWebPage> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_dialog = createProgressDialog(context, S.of(context).pleaseWait);
|
||||
_dialog = createProgressDialog(
|
||||
context,
|
||||
S.of(context).pleaseWait,
|
||||
isDismissible: true,
|
||||
);
|
||||
if (initPaymentUrl == null) {
|
||||
return const EnteLoadingWidget();
|
||||
}
|
||||
|
|
|
@ -36,13 +36,9 @@ class _SubscriptionHeaderWidgetState extends State<SubscriptionHeaderWidget> {
|
|||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
S.of(context).selectYourPlan,
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
],
|
||||
Text(
|
||||
S.of(context).selectYourPlan,
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
|
|
|
@ -6,8 +6,8 @@ import "package:photos/core/event_bus.dart";
|
|||
import 'package:photos/events/embedding_updated_event.dart';
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import "package:photos/services/feature_flag_service.dart";
|
||||
import "package:photos/services/semantic_search/frameworks/ml_framework.dart";
|
||||
import "package:photos/services/semantic_search/semantic_search_service.dart";
|
||||
import 'package:photos/services/machine_learning/semantic_search/frameworks/ml_framework.dart';
|
||||
import 'package:photos/services/machine_learning/semantic_search/semantic_search_service.dart';
|
||||
import "package:photos/theme/ente_theme.dart";
|
||||
import "package:photos/ui/common/loading_widget.dart";
|
||||
import "package:photos/ui/components/buttons/icon_button_widget.dart";
|
||||
|
|
|
@ -42,14 +42,16 @@ class _LogFileViewerState extends State<LogFileViewer> {
|
|||
}
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(left: 12, top: 8, right: 12),
|
||||
child: SingleChildScrollView(
|
||||
child: Text(
|
||||
_logs!,
|
||||
style: const TextStyle(
|
||||
fontFeatures: [
|
||||
FontFeature.tabularFigures(),
|
||||
],
|
||||
height: 1.2,
|
||||
child: Scrollbar(
|
||||
child: SingleChildScrollView(
|
||||
child: Text(
|
||||
_logs!,
|
||||
style: const TextStyle(
|
||||
fontFeatures: [
|
||||
FontFeature.tabularFigures(),
|
||||
],
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -73,6 +73,7 @@ class DetailPage extends StatefulWidget {
|
|||
class _DetailPageState extends State<DetailPage> {
|
||||
static const kLoadLimit = 100;
|
||||
final _logger = Logger("DetailPageState");
|
||||
bool _shouldDisableScroll = false;
|
||||
List<EnteFile>? _files;
|
||||
late PageController _pageController;
|
||||
final _selectedIndexNotifier = ValueNotifier(0);
|
||||
|
@ -171,6 +172,14 @@ class _DetailPageState extends State<DetailPage> {
|
|||
file,
|
||||
autoPlay: shouldAutoPlay(),
|
||||
tagPrefix: widget.config.tagPrefix,
|
||||
shouldDisableScroll: (value) {
|
||||
if (_shouldDisableScroll != value) {
|
||||
setState(() {
|
||||
_logger.fine('setState $_shouldDisableScroll to $value');
|
||||
_shouldDisableScroll = value;
|
||||
});
|
||||
}
|
||||
},
|
||||
playbackCallback: (isPlaying) {
|
||||
Future.delayed(Duration.zero, () {
|
||||
_toggleFullScreen(shouldEnable: isPlaying);
|
||||
|
@ -199,7 +208,9 @@ class _DetailPageState extends State<DetailPage> {
|
|||
}
|
||||
_preloadEntries();
|
||||
},
|
||||
physics: const FastScrollPhysics(speedFactor: 4.0),
|
||||
physics: _shouldDisableScroll
|
||||
? const NeverScrollableScrollPhysics()
|
||||
: const FastScrollPhysics(speedFactor: 4.0),
|
||||
controller: _pageController,
|
||||
itemCount: _files!.length,
|
||||
);
|
||||
|
|
|
@ -8,6 +8,7 @@ import "package:photos/ui/viewer/file/zoomable_live_image_new.dart";
|
|||
class FileWidget extends StatelessWidget {
|
||||
final EnteFile file;
|
||||
final String? tagPrefix;
|
||||
final Function(bool)? shouldDisableScroll;
|
||||
final Function(bool)? playbackCallback;
|
||||
final BoxDecoration? backgroundDecoration;
|
||||
final bool? autoPlay;
|
||||
|
@ -15,6 +16,7 @@ class FileWidget extends StatelessWidget {
|
|||
const FileWidget(
|
||||
this.file, {
|
||||
this.autoPlay,
|
||||
this.shouldDisableScroll,
|
||||
this.playbackCallback,
|
||||
this.tagPrefix,
|
||||
this.backgroundDecoration,
|
||||
|
@ -30,6 +32,7 @@ class FileWidget extends StatelessWidget {
|
|||
file.fileType == FileType.image) {
|
||||
return ZoomableLiveImageNew(
|
||||
file,
|
||||
shouldDisableScroll: shouldDisableScroll,
|
||||
tagPrefix: tagPrefix,
|
||||
backgroundDecoration: backgroundDecoration,
|
||||
key: key ?? ValueKey(fileKey),
|
||||
|
|
|
@ -6,7 +6,6 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter/widgets.dart';
|
||||
import 'package:logging/logging.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/constants.dart';
|
||||
import 'package:photos/core/event_bus.dart';
|
||||
|
@ -25,6 +24,7 @@ import 'package:photos/utils/thumbnail_util.dart';
|
|||
|
||||
class ZoomableImage extends StatefulWidget {
|
||||
final EnteFile photo;
|
||||
final Function(bool)? shouldDisableScroll;
|
||||
final String? tagPrefix;
|
||||
final Decoration? backgroundDecoration;
|
||||
final bool shouldCover;
|
||||
|
@ -32,6 +32,7 @@ class ZoomableImage extends StatefulWidget {
|
|||
const ZoomableImage(
|
||||
this.photo, {
|
||||
Key? key,
|
||||
this.shouldDisableScroll,
|
||||
required this.tagPrefix,
|
||||
this.backgroundDecoration,
|
||||
this.shouldCover = false,
|
||||
|
@ -51,9 +52,9 @@ class _ZoomableImageState extends State<ZoomableImage>
|
|||
bool _loadedLargeThumbnail = false;
|
||||
bool _loadingFinalImage = false;
|
||||
bool _loadedFinalImage = false;
|
||||
PhotoViewController _photoViewController = PhotoViewController();
|
||||
bool _isZooming = false;
|
||||
ValueChanged<PhotoViewScaleState>? _scaleStateChangedCallback;
|
||||
bool _isZooming = false;
|
||||
PhotoViewController _photoViewController = PhotoViewController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
@ -61,8 +62,12 @@ class _ZoomableImageState extends State<ZoomableImage>
|
|||
_logger = Logger("ZoomableImage");
|
||||
_logger.info('initState for ${_photo.generatedID} with tag ${_photo.tag}');
|
||||
_scaleStateChangedCallback = (value) {
|
||||
if (widget.shouldDisableScroll != null) {
|
||||
widget.shouldDisableScroll!(value != PhotoViewScaleState.initial);
|
||||
}
|
||||
_isZooming = value != PhotoViewScaleState.initial;
|
||||
debugPrint("isZooming = $_isZooming, currentState $value");
|
||||
// _logger.info('is reakky zooming $_isZooming with state $value');
|
||||
};
|
||||
super.initState();
|
||||
}
|
||||
|
@ -83,41 +88,41 @@ class _ZoomableImageState extends State<ZoomableImage>
|
|||
Widget content;
|
||||
|
||||
if (_imageProvider != null) {
|
||||
content = PhotoViewGallery.builder(
|
||||
gaplessPlayback: true,
|
||||
scaleStateChangedCallback: _scaleStateChangedCallback,
|
||||
backgroundDecoration: widget.backgroundDecoration as BoxDecoration?,
|
||||
builder: (context, index) {
|
||||
return PhotoViewGalleryPageOptions(
|
||||
imageProvider: _imageProvider!,
|
||||
minScale: widget.shouldCover
|
||||
? PhotoViewComputedScale.covered
|
||||
: PhotoViewComputedScale.contained,
|
||||
heroAttributes: PhotoViewHeroAttributes(
|
||||
tag: widget.tagPrefix! + _photo.tag,
|
||||
),
|
||||
controller: _photoViewController,
|
||||
);
|
||||
},
|
||||
itemCount: 1,
|
||||
content = PhotoViewGestureDetectorScope(
|
||||
axis: Axis.vertical,
|
||||
child: PhotoView(
|
||||
imageProvider: _imageProvider,
|
||||
controller: _photoViewController,
|
||||
scaleStateChangedCallback: _scaleStateChangedCallback,
|
||||
minScale: widget.shouldCover
|
||||
? PhotoViewComputedScale.covered
|
||||
: PhotoViewComputedScale.contained,
|
||||
gaplessPlayback: true,
|
||||
heroAttributes: PhotoViewHeroAttributes(
|
||||
tag: widget.tagPrefix! + _photo.tag,
|
||||
),
|
||||
backgroundDecoration: widget.backgroundDecoration as BoxDecoration?,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
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(
|
||||
onVerticalDragUpdate: verticalDragCallback,
|
||||
child: content,
|
||||
|
@ -258,7 +263,9 @@ class _ZoomableImageState extends State<ZoomableImage>
|
|||
required ImageProvider? previewImageProvider,
|
||||
required ImageProvider finalImageProvider,
|
||||
}) async {
|
||||
final bool shouldFixPosition = previewImageProvider != null && _isZooming;
|
||||
final bool shouldFixPosition = previewImageProvider != null &&
|
||||
_isZooming &&
|
||||
_photoViewController.scale != null;
|
||||
ImageInfo? finalImageInfo;
|
||||
if (shouldFixPosition) {
|
||||
final prevImageInfo = await getImageInfo(previewImageProvider);
|
||||
|
|
|
@ -16,12 +16,14 @@ import 'package:video_player/video_player.dart';
|
|||
|
||||
class ZoomableLiveImage extends StatefulWidget {
|
||||
final EnteFile enteFile;
|
||||
final Function(bool)? shouldDisableScroll;
|
||||
final String? tagPrefix;
|
||||
final Decoration? backgroundDecoration;
|
||||
|
||||
const ZoomableLiveImage(
|
||||
this.enteFile, {
|
||||
Key? key,
|
||||
this.shouldDisableScroll,
|
||||
required this.tagPrefix,
|
||||
this.backgroundDecoration,
|
||||
}) : super(key: key);
|
||||
|
@ -43,9 +45,8 @@ class _ZoomableLiveImageState extends State<ZoomableLiveImage>
|
|||
@override
|
||||
void initState() {
|
||||
_enteFile = widget.enteFile;
|
||||
_logger.info(
|
||||
'initState for ${_enteFile.generatedID} with tag ${_enteFile.tag} and name ${_enteFile.displayName}',
|
||||
);
|
||||
_logger.info('initState for ${_enteFile.generatedID} with tag ${_enteFile
|
||||
.tag} and name ${_enteFile.displayName}');
|
||||
super.initState();
|
||||
}
|
||||
|
||||
|
@ -75,6 +76,7 @@ class _ZoomableLiveImageState extends State<ZoomableLiveImage>
|
|||
content = ZoomableImage(
|
||||
_enteFile,
|
||||
tagPrefix: widget.tagPrefix,
|
||||
shouldDisableScroll: widget.shouldDisableScroll,
|
||||
backgroundDecoration: widget.backgroundDecoration,
|
||||
);
|
||||
}
|
||||
|
@ -136,8 +138,7 @@ class _ZoomableLiveImageState extends State<ZoomableLiveImage>
|
|||
}
|
||||
|
||||
Future<File?> _getLivePhotoVideo() async {
|
||||
if (_enteFile.isRemoteFile &&
|
||||
!(await isFileCached(_enteFile, liveVideo: true))) {
|
||||
if (_enteFile.isRemoteFile && !(await isFileCached(_enteFile, liveVideo: true))) {
|
||||
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 {
|
||||
final EnteFile enteFile;
|
||||
final Function(bool)? shouldDisableScroll;
|
||||
final String? tagPrefix;
|
||||
final Decoration? backgroundDecoration;
|
||||
|
||||
const ZoomableLiveImageNew(
|
||||
this.enteFile, {
|
||||
Key? key,
|
||||
this.shouldDisableScroll,
|
||||
required this.tagPrefix,
|
||||
this.backgroundDecoration,
|
||||
}) : super(key: key);
|
||||
|
@ -79,6 +81,7 @@ class _ZoomableLiveImageNewState extends State<ZoomableLiveImageNew>
|
|||
content = ZoomableImage(
|
||||
_enteFile,
|
||||
tagPrefix: widget.tagPrefix,
|
||||
shouldDisableScroll: widget.shouldDisableScroll,
|
||||
backgroundDecoration: widget.backgroundDecoration,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@ import "package:flutter/cupertino.dart";
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:photos/core/configuration.dart';
|
||||
import "package:photos/core/constants.dart";
|
||||
import 'package:photos/core/event_bus.dart';
|
||||
import "package:photos/core/network/network.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/selected_files.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/update_service.dart';
|
||||
import 'package:photos/ui/actions/collection/collection_sharing_actions.dart';
|
||||
|
@ -88,6 +88,7 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
|
|||
late CollectionActions collectionActions;
|
||||
final GlobalKey shareButtonKey = GlobalKey();
|
||||
bool isQuickLink = false;
|
||||
late bool isInternalUser;
|
||||
late GalleryType galleryType;
|
||||
|
||||
@override
|
||||
|
@ -96,6 +97,7 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
|
|||
_selectedFilesListener = () {
|
||||
setState(() {});
|
||||
};
|
||||
isInternalUser = FeatureFlagService.instance.isInternalUserOrDebugBuild();
|
||||
collectionActions = CollectionActions(CollectionsService.instance);
|
||||
widget.selectedFiles.addListener(_selectedFilesListener);
|
||||
_userAuthEventSubscription =
|
||||
|
|
|
@ -22,14 +22,20 @@ import "package:photos/ui/viewer/location/radius_picker_widget.dart";
|
|||
|
||||
showAddLocationSheet(
|
||||
BuildContext context,
|
||||
Location coordinates,
|
||||
) {
|
||||
Location coordinates, {
|
||||
String name = '',
|
||||
double radius = defaultRadiusValue,
|
||||
}) {
|
||||
showBarModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return LocationTagStateProvider(
|
||||
centerPoint: coordinates,
|
||||
const AddLocationSheet(),
|
||||
AddLocationSheet(
|
||||
radius: radius,
|
||||
name: name,
|
||||
),
|
||||
radius: radius,
|
||||
);
|
||||
},
|
||||
shape: const RoundedRectangleBorder(
|
||||
|
@ -45,7 +51,13 @@ showAddLocationSheet(
|
|||
}
|
||||
|
||||
class AddLocationSheet extends StatefulWidget {
|
||||
const AddLocationSheet({super.key});
|
||||
final double radius;
|
||||
final String name;
|
||||
const AddLocationSheet({
|
||||
super.key,
|
||||
this.radius = defaultRadiusValue,
|
||||
this.name = '',
|
||||
});
|
||||
|
||||
@override
|
||||
State<AddLocationSheet> createState() => _AddLocationSheetState();
|
||||
|
@ -61,17 +73,20 @@ class _AddLocationSheetState extends State<AddLocationSheet> {
|
|||
final ValueNotifier<bool> _submitNotifer = ValueNotifier(false);
|
||||
|
||||
final ValueNotifier<bool> _cancelNotifier = ValueNotifier(false);
|
||||
final ValueNotifier<double> _selectedRadiusNotifier =
|
||||
ValueNotifier(defaultRadiusValue);
|
||||
late ValueNotifier<double> _selectedRadiusNotifier;
|
||||
final _focusNode = FocusNode();
|
||||
final _textEditingController = TextEditingController();
|
||||
final _isEmptyNotifier = ValueNotifier(true);
|
||||
late final ValueNotifier<bool> _isEmptyNotifier;
|
||||
Widget? _keyboardTopButtons;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_textEditingController.text = widget.name;
|
||||
_isEmptyNotifier = ValueNotifier(widget.name.isEmpty);
|
||||
_focusNode.addListener(_focusNodeListener);
|
||||
_selectedRadiusNotifier = ValueNotifier(widget.radius);
|
||||
_selectedRadiusNotifier.addListener(_selectedRadiusListener);
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
|
@ -155,11 +170,12 @@ class _AddLocationSheetState extends State<AddLocationSheet> {
|
|||
RadiusPickerWidget(
|
||||
_selectedRadiusNotifier,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
S.of(context).locationTagFeatureDescription,
|
||||
style: textTheme.smallMuted,
|
||||
),
|
||||
if (widget.name.isEmpty) const SizedBox(height: 16),
|
||||
if (widget.name.isEmpty)
|
||||
Text(
|
||||
S.of(context).locationTagFeatureDescription,
|
||||
style: textTheme.smallMuted,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
@ -131,6 +131,8 @@ class SearchResultWidget extends StatelessWidget {
|
|||
return "Day";
|
||||
case ResultType.location:
|
||||
return "Location";
|
||||
case ResultType.locationSuggestion:
|
||||
return "Add Location";
|
||||
case ResultType.fileType:
|
||||
return "Type";
|
||||
case ResultType.fileExtension:
|
||||
|
|
|
@ -3,6 +3,7 @@ import "dart:async";
|
|||
import "package:flutter/material.dart";
|
||||
import "package:flutter_animate/flutter_animate.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/generic_search_result.dart";
|
||||
import "package:photos/models/search/recent_searches.dart";
|
||||
|
@ -83,8 +84,6 @@ class _SearchSectionAllPageState extends State<SearchSectionAllPage> {
|
|||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
final sectionResults = snapshot.data!;
|
||||
sectionResults
|
||||
.sort((a, b) => a.name().compareTo(b.name()));
|
||||
return Text(sectionResults.length.toString())
|
||||
.animate()
|
||||
.fadeIn(
|
||||
|
@ -109,7 +108,15 @@ class _SearchSectionAllPageState extends State<SearchSectionAllPage> {
|
|||
future: sectionData,
|
||||
builder: (context, snapshot) {
|
||||
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(
|
||||
itemBuilder: (context, index) {
|
||||
if (sectionResults.length == index) {
|
||||
|
|
|
@ -77,7 +77,10 @@ class SearchableItemWidget extends StatelessWidget {
|
|||
children: [
|
||||
Text(
|
||||
searchResult.name(),
|
||||
style: textTheme.body,
|
||||
style: searchResult.type() ==
|
||||
ResultType.locationSuggestion
|
||||
? textTheme.bodyFaint
|
||||
: textTheme.body,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(
|
||||
|
|
|
@ -129,7 +129,6 @@ class SearchWidgetState extends State<SearchWidget> {
|
|||
child: Container(
|
||||
color: colorScheme.backgroundBase,
|
||||
child: Container(
|
||||
height: 44,
|
||||
color: colorScheme.fillFaint,
|
||||
child: TextFormField(
|
||||
controller: textController,
|
||||
|
|
|
@ -2,10 +2,10 @@ import "package:dio/dio.dart";
|
|||
import "package:flutter/foundation.dart";
|
||||
import 'package:flutter/material.dart';
|
||||
import "package:flutter/services.dart";
|
||||
import "package:photos/core/constants.dart";
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import 'package:photos/models/button_result.dart';
|
||||
import 'package:photos/models/typedefs.dart';
|
||||
import "package:photos/services/feature_flag_service.dart";
|
||||
import 'package:photos/theme/colors.dart';
|
||||
import 'package:photos/ui/common/loading_widget.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
|
||||
if (!(isInternalUser && kDebugMode)) {
|
||||
if (!(FeatureFlagService.instance.isInternalUserOrDebugBuild() &&
|
||||
kDebugMode)) {
|
||||
return genericError;
|
||||
}
|
||||
String errorInfo = "";
|
||||
|
|
|
@ -10,6 +10,7 @@ import 'package:dio/dio.dart';
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:photos/core/configuration.dart';
|
||||
import "package:photos/core/constants.dart";
|
||||
import 'package:photos/core/errors.dart';
|
||||
import 'package:photos/core/event_bus.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/file_download_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:tuple/tuple.dart';
|
||||
import "package:uuid/uuid.dart";
|
||||
|
@ -69,6 +71,7 @@ class FileUploader {
|
|||
late ProcessType _processType;
|
||||
late bool _isBackground;
|
||||
late SharedPreferences _prefs;
|
||||
|
||||
// _hasInitiatedForceUpload is used to track if user attempted force upload
|
||||
// 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
|
||||
|
@ -307,12 +310,36 @@ class FileUploader {
|
|||
return file.path.contains(kUploadTempPrefix) &&
|
||||
file.path.contains(".encrypted");
|
||||
});
|
||||
if (filesToDelete.isEmpty) {
|
||||
return;
|
||||
if (filesToDelete.isNotEmpty) {
|
||||
_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) {
|
||||
await file.delete();
|
||||
|
||||
if (Platform.isAndroid) {
|
||||
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) {
|
||||
_logger.severe("Failed to remove stale files", e, s);
|
||||
|
@ -431,7 +458,13 @@ class FileUploader {
|
|||
encryptedFilePath,
|
||||
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 =
|
||||
await CryptoUtil.encryptChaCha(
|
||||
|
@ -493,17 +526,21 @@ class FileUploader {
|
|||
CryptoUtil.bin2base64(encryptedFileKeyData.encryptedData!);
|
||||
final keyDecryptionNonce =
|
||||
CryptoUtil.bin2base64(encryptedFileKeyData.nonce!);
|
||||
final Map<String, dynamic> pubMetadata = {};
|
||||
MetadataRequest? pubMetadataRequest;
|
||||
if ((mediaUploadData.height ?? 0) != 0 &&
|
||||
(mediaUploadData.width ?? 0) != 0) {
|
||||
final pubMetadata = {
|
||||
heightKey: mediaUploadData.height,
|
||||
widthKey: mediaUploadData.width,
|
||||
};
|
||||
if (mediaUploadData.motionPhotoStartIndex != null) {
|
||||
pubMetadata[motionVideoIndexKey] =
|
||||
mediaUploadData.motionPhotoStartIndex;
|
||||
}
|
||||
pubMetadata[heightKey] = mediaUploadData.height;
|
||||
pubMetadata[widthKey] = mediaUploadData.width;
|
||||
}
|
||||
if (mediaUploadData.motionPhotoStartIndex != null) {
|
||||
pubMetadata[motionVideoIndexKey] =
|
||||
mediaUploadData.motionPhotoStartIndex;
|
||||
}
|
||||
if (mediaUploadData.thumbnail == null) {
|
||||
pubMetadata[noThumbKey] = true;
|
||||
}
|
||||
if (pubMetadata.isNotEmpty) {
|
||||
pubMetadataRequest = await getPubMetadataRequest(
|
||||
file,
|
||||
pubMetadata,
|
||||
|
|
|
@ -208,6 +208,10 @@ Future<Uint8List?> _getThumbnailForUpload(
|
|||
quality: thumbnailQuality,
|
||||
);
|
||||
if (thumbnailData == null) {
|
||||
// allow videos to be uploaded without thumbnails
|
||||
if (asset.type == AssetType.video) {
|
||||
return null;
|
||||
}
|
||||
throw InvalidFileError(
|
||||
"no thumbnail : ${file.fileType} ${file.tag}",
|
||||
InvalidReason.thumbnailMissing,
|
||||
|
@ -227,6 +231,10 @@ Future<Uint8List?> _getThumbnailForUpload(
|
|||
final String errMessage =
|
||||
"thumbErr for ${file.fileType}, ${extension(file.displayName)} ${file.tag}";
|
||||
_logger.warning(errMessage, e);
|
||||
// allow videos to be uploaded without thumbnails
|
||||
if (asset.type == AssetType.video) {
|
||||
return null;
|
||||
}
|
||||
throw InvalidFileError(errMessage, InvalidReason.thumbnailMissing);
|
||||
}
|
||||
}
|
||||
|
|
12
pubspec.lock
12
pubspec.lock
|
@ -89,6 +89,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -1389,8 +1397,8 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: HEAD
|
||||
resolved-ref: "1318dce97f3aae5ec9bdf7491d5eff0ad6beb378"
|
||||
ref: "5f26aef45ed9f5e563c26f90c1e21b3339ed906d"
|
||||
resolved-ref: "5f26aef45ed9f5e563c26f90c1e21b3339ed906d"
|
||||
url: "https://github.com/ente-io/onnxruntime.git"
|
||||
source: git
|
||||
version: "1.1.0"
|
||||
|
|
|
@ -12,8 +12,7 @@ description: ente photos application
|
|||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
|
||||
version: 0.8.58+578
|
||||
|
||||
version: 0.8.64+584
|
||||
environment:
|
||||
sdk: ">=3.0.0 <4.0.0"
|
||||
|
||||
|
@ -23,6 +22,7 @@ dependencies:
|
|||
animated_list_plus: ^0.4.5
|
||||
archive: ^3.1.2
|
||||
background_fetch: ^1.2.1
|
||||
battery_info: ^1.1.1
|
||||
bip39: ^1.0.6
|
||||
cached_network_image: ^3.0.0
|
||||
chewie:
|
||||
|
@ -119,7 +119,9 @@ dependencies:
|
|||
|
||||
# open_file: ^3.2.1
|
||||
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
|
||||
package_info_plus: ^4.1.0
|
||||
page_transition: ^2.0.2
|
||||
|
|
Loading…
Add table
Reference in a new issue