Merge branch 'main' into redesign_search_tab

This commit is contained in:
ashilkn 2024-02-29 22:46:01 +05:30
commit faa40119b4
58 changed files with 2771 additions and 353 deletions

View file

@ -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
View file

@ -5,6 +5,3 @@ gradle-wrapper.jar
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
# Signing config files
*.jks

View file

@ -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.

View file

@ -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

View file

@ -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",

View file

@ -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();
}

View file

@ -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';

View file

@ -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=';

View file

@ -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 =

View file

@ -0,0 +1,7 @@
import "package:photos/events/event.dart";
class MachineLearningControlEvent extends Event {
final bool shouldRun;
MachineLearningControlEvent(this.shouldRun);
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -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

View file

@ -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],
);
}
}

View file

@ -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);

View file

@ -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");

View file

@ -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 {

View file

@ -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(

View file

@ -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,32 +242,30 @@ 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(
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 (!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,
)) {
matchingFiles.add(file);
results.putIfAbsent(city, () => []).add(file);
break; // Stop searching once a file is matched with a city
}
}
}
if (matchingFiles.isNotEmpty) {
results[city] = matchingFiles;
}
}
return results;
}

View 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);
}
}

View file

@ -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 {

View file

@ -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/";

View file

@ -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/";

View file

@ -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";

View file

@ -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) {

View file

@ -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);

View file

@ -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",
);
}

View file

@ -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) {

View file

@ -101,8 +101,7 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
Future<void> verifyPassword(String password) async {
FocusScope.of(context).unfocus();
final dialog =
createProgressDialog(context, S.of(context).pleaseWait);
final dialog = createProgressDialog(context, S.of(context).pleaseWait);
await dialog.show();
try {
final kek = await Configuration.instance.decryptSecretsAndGetKeyEncKey(
@ -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,19 +279,15 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
),
);
},
child: Center(
child: Text(
S.of(context).forgotPassword,
style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(
style:
Theme.of(context).textTheme.titleMedium!.copyWith(
fontSize: 14,
decoration: TextDecoration.underline,
),
),
),
),
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () async {
@ -306,19 +301,15 @@ 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(
style:
Theme.of(context).textTheme.titleMedium!.copyWith(
fontSize: 14,
decoration: TextDecoration.underline,
),
),
),
),
],
),
),

View file

@ -558,9 +558,9 @@ class CollectionActions {
);
} else {
await collectionsService.move(
entry.key,
collection.id,
entry.value,
toCollectionID: entry.key,
fromCollectionID: collection.id,
);
}
}

View file

@ -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));

View file

@ -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:

View file

@ -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,

View file

@ -221,6 +221,7 @@ class _MemoryCoverWidgetState extends State<MemoryCoverWidget> {
child: ThumbnailWidget(
memory.file,
shouldShowArchiveStatus: false,
shouldShowSyncStatus: false,
key: Key("memories" + memory.file.tag),
),
),

View file

@ -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();
}

View file

@ -35,15 +35,11 @@ class _SubscriptionHeaderWidgetState extends State<SubscriptionHeaderWidget> {
padding: const EdgeInsets.fromLTRB(20, 20, 20, 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
S.of(context).selectYourPlan,
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
const SizedBox(height: 10),
Text(
S.of(context).enteSubscriptionPitch,

View file

@ -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";

View file

@ -42,6 +42,7 @@ class _LogFileViewerState extends State<LogFileViewer> {
}
return Container(
padding: const EdgeInsets.only(left: 12, top: 8, right: 12),
child: Scrollbar(
child: SingleChildScrollView(
child: Text(
_logs!,
@ -53,6 +54,7 @@ class _LogFileViewerState extends State<LogFileViewer> {
),
),
),
),
);
}
}

View file

@ -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,
);

View file

@ -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),

View file

@ -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,28 +88,29 @@ class _ZoomableImageState extends State<ZoomableImage>
Widget content;
if (_imageProvider != null) {
content = PhotoViewGallery.builder(
gaplessPlayback: true,
content = PhotoViewGestureDetectorScope(
axis: Axis.vertical,
child: PhotoView(
imageProvider: _imageProvider,
controller: _photoViewController,
scaleStateChangedCallback: _scaleStateChangedCallback,
backgroundDecoration: widget.backgroundDecoration as BoxDecoration?,
builder: (context, index) {
return PhotoViewGalleryPageOptions(
imageProvider: _imageProvider!,
minScale: widget.shouldCover
? PhotoViewComputedScale.covered
: PhotoViewComputedScale.contained,
gaplessPlayback: true,
heroAttributes: PhotoViewHeroAttributes(
tag: widget.tagPrefix! + _photo.tag,
),
controller: _photoViewController,
);
},
itemCount: 1,
backgroundDecoration: widget.backgroundDecoration as BoxDecoration?,
),
);
} else {
content = const EnteLoadingWidget();
}
verticalDragCallback(d) => {
final GestureDragUpdateCallback? verticalDragCallback = _isZooming
? null
: (d) => {
if (!_isZooming)
{
if (d.delta.dy > dragSensitivity)
@ -117,7 +123,6 @@ class _ZoomableImageState extends State<ZoomableImage>
},
},
};
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);

View file

@ -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>
}
});
}
}

View file

@ -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,
);
}

View file

@ -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 =

View file

@ -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,7 +170,8 @@ class _AddLocationSheetState extends State<AddLocationSheet> {
RadiusPickerWidget(
_selectedRadiusNotifier,
),
const SizedBox(height: 16),
if (widget.name.isEmpty) const SizedBox(height: 16),
if (widget.name.isEmpty)
Text(
S.of(context).locationTagFeatureDescription,
style: textTheme.smallMuted,

View file

@ -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:

View file

@ -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) {

View file

@ -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(

View file

@ -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,

View file

@ -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 = "";

View file

@ -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,13 +310,37 @@ 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();
}
}
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,
};
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,

View file

@ -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);
}
}

View file

@ -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"

View file

@ -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