Compare commits

..

13 commits

Author SHA1 Message Date
Neeraj Gupta bcf3084d97 Merge branch 'main' into generic_group_by 2024-05-27 11:54:37 +05:30
Neeraj Gupta 5e4d530b93 [mob] Fix range calculation while refresh lazy gallery group 2024-05-23 15:18:02 +05:30
Neeraj Gupta 22ff318249 [mob] Fix bug in daysInSameWeek check 2024-05-23 15:11:41 +05:30
Neeraj Gupta 643b77e81e Fix title for month 2024-05-23 14:18:37 +05:30
Neeraj Gupta 3ce8a09e39 Merge branch 'main' into generic_group_by 2024-05-23 14:13:35 +05:30
Neeraj Gupta ce6160a06a Merge branch 'main' into generic_group_by 2024-05-22 15:18:44 +05:30
Neeraj Gupta c21a0cfdb4 [mob] Lint fix 2024-05-21 17:46:23 +05:30
Neeraj Gupta 241c755446 Merge branch 'main' into generic_group_by 2024-05-21 17:45:27 +05:30
Neeraj Gupta cff695dd02 [mob] Fix title for month grouping 2024-05-04 12:35:33 +05:30
Neeraj Gupta 5f9b0d11f2 [mob] Gallery: Support grouping by day/week/month/year 2024-05-04 12:31:08 +05:30
Neeraj Gupta e75be714d9 [mob] Refactor groupHeader to use groupType 2024-05-04 12:07:58 +05:30
Neeraj Gupta da329c498c [mob] Add groupType in Gallery context state 2024-05-04 12:00:16 +05:30
Neeraj Gupta cc74e08155 [mob] Add groupType with common extn methods 2024-05-04 11:56:29 +05:30
23 changed files with 383 additions and 264 deletions

View file

@ -30,7 +30,7 @@
"compare-versions": "^6.1", "compare-versions": "^6.1",
"electron-log": "^5.1", "electron-log": "^5.1",
"electron-store": "^8.2", "electron-store": "^8.2",
"electron-updater": "^6.2", "electron-updater": "^6.1",
"ffmpeg-static": "^5.2", "ffmpeg-static": "^5.2",
"html-entities": "^2.5", "html-entities": "^2.5",
"jpeg-js": "^0.4", "jpeg-js": "^0.4",

View file

@ -743,10 +743,10 @@ buffer@^5.1.0, buffer@^5.5.0:
base64-js "^1.3.1" base64-js "^1.3.1"
ieee754 "^1.1.13" ieee754 "^1.1.13"
builder-util-runtime@9.2.4: builder-util-runtime@9.2.3:
version "9.2.4" version "9.2.3"
resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz#13cd1763da621e53458739a1e63f7fcba673c42a" resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz#0a82c7aca8eadef46d67b353c638f052c206b83c"
integrity sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA== integrity sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw==
dependencies: dependencies:
debug "^4.3.4" debug "^4.3.4"
sax "^1.2.4" sax "^1.2.4"
@ -1251,12 +1251,12 @@ electron-store@^8.2:
conf "^10.2.0" conf "^10.2.0"
type-fest "^2.17.0" type-fest "^2.17.0"
electron-updater@^6.2: electron-updater@^6.1:
version "6.2.1" version "6.1.8"
resolved "https://registry.yarnpkg.com/electron-updater/-/electron-updater-6.2.1.tgz#1c9adb9ba2a21a5dc50a8c434c45360d5e9fe6c9" resolved "https://registry.yarnpkg.com/electron-updater/-/electron-updater-6.1.8.tgz#17637bca165322f4e526b13c99165f43e6f697d8"
integrity sha512-83eKIPW14qwZqUUM6wdsIRwVKZyjmHxQ4/8G+1C6iS5PdDt7b1umYQyj1/qPpH510GmHEQe4q0kCPe3qmb3a0Q== integrity sha512-hhOTfaFAd6wRHAfUaBhnAOYc+ymSGCWJLtFkw4xJqOvtpHmIdNHnXDV9m1MHC+A6q08Abx4Ykgyz/R5DGKNAMQ==
dependencies: dependencies:
builder-util-runtime "9.2.4" builder-util-runtime "9.2.3"
fs-extra "^10.1.0" fs-extra "^10.1.0"
js-yaml "^4.1.0" js-yaml "^4.1.0"
lazy-val "^1.0.5" lazy-val "^1.0.5"

View file

@ -35,15 +35,6 @@ class FaceMLDataDB {
static final FaceMLDataDB instance = FaceMLDataDB._privateConstructor(); static final FaceMLDataDB instance = FaceMLDataDB._privateConstructor();
static final _migrationScripts = [
createFacesTable,
createFaceClustersTable,
createClusterPersonTable,
createClusterSummaryTable,
createNotPersonFeedbackTable,
fcClusterIDIndex,
];
// only have a single app-wide reference to the database // only have a single app-wide reference to the database
static Future<SqliteDatabase>? _sqliteAsyncDBFuture; static Future<SqliteDatabase>? _sqliteAsyncDBFuture;
@ -59,42 +50,17 @@ class FaceMLDataDB {
_logger.info("Opening sqlite_async access: DB path " + databaseDirectory); _logger.info("Opening sqlite_async access: DB path " + databaseDirectory);
final asyncDBConnection = final asyncDBConnection =
SqliteDatabase(path: databaseDirectory, maxReaders: 2); SqliteDatabase(path: databaseDirectory, maxReaders: 2);
final stopwatch = Stopwatch()..start(); await _onCreate(asyncDBConnection);
_logger.info("FaceMLDataDB: Starting migration");
await _migrate(asyncDBConnection);
_logger.info(
"FaceMLDataDB Migration took ${stopwatch.elapsedMilliseconds} ms",
);
stopwatch.stop();
return asyncDBConnection; return asyncDBConnection;
} }
Future<void> _migrate( Future<void> _onCreate(SqliteDatabase asyncDBConnection) async {
SqliteDatabase database, await asyncDBConnection.execute(createFacesTable);
) async { await asyncDBConnection.execute(createFaceClustersTable);
final result = await database.execute('PRAGMA user_version'); await asyncDBConnection.execute(createClusterPersonTable);
final currentVersion = result[0]['user_version'] as int; await asyncDBConnection.execute(createClusterSummaryTable);
final toVersion = _migrationScripts.length; await asyncDBConnection.execute(createNotPersonFeedbackTable);
await asyncDBConnection.execute(fcClusterIDIndex);
if (currentVersion < toVersion) {
_logger.info("Migrating database from $currentVersion to $toVersion");
await database.writeTransaction((tx) async {
for (int i = currentVersion + 1; i <= toVersion; i++) {
try {
await tx.execute(_migrationScripts[i - 1]);
} catch (e) {
_logger.severe("Error running migration script index ${i - 1}", e);
rethrow;
}
}
await tx.execute('PRAGMA user_version = $toVersion');
});
} else if (currentVersion > toVersion) {
throw AssertionError(
"currentVersion($currentVersion) cannot be greater than toVersion($toVersion)",
);
}
} }
// bulkInsertFaces inserts the faces in the database in batches of 1000. // bulkInsertFaces inserts the faces in the database in batches of 1000.
@ -229,10 +195,10 @@ class FaceMLDataDB {
final db = await instance.asyncDB; final db = await instance.asyncDB;
await db.execute(deleteFacesTable); await db.execute(deleteFacesTable);
await db.execute(deleteFaceClustersTable); await db.execute(dropClusterPersonTable);
await db.execute(deleteClusterPersonTable); await db.execute(dropClusterSummaryTable);
await db.execute(deleteClusterSummaryTable); await db.execute(deletePersonTable);
await db.execute(deleteNotPersonFeedbackTable); await db.execute(dropNotPersonFeedbackTable);
} }
Future<Iterable<Uint8List>> getFaceEmbeddingsForCluster( Future<Iterable<Uint8List>> getFaceEmbeddingsForCluster(
@ -768,7 +734,7 @@ class FaceMLDataDB {
try { try {
final db = await instance.asyncDB; final db = await instance.asyncDB;
await db.execute(deleteFaceClustersTable); await db.execute(dropFaceClustersTable);
await db.execute(createFaceClustersTable); await db.execute(createFaceClustersTable);
await db.execute(fcClusterIDIndex); await db.execute(fcClusterIDIndex);
} catch (e, s) { } catch (e, s) {
@ -979,15 +945,16 @@ class FaceMLDataDB {
if (faces) { if (faces) {
await db.execute(deleteFacesTable); await db.execute(deleteFacesTable);
await db.execute(createFacesTable); await db.execute(createFacesTable);
await db.execute(deleteFaceClustersTable); await db.execute(dropFaceClustersTable);
await db.execute(createFaceClustersTable); await db.execute(createFaceClustersTable);
await db.execute(fcClusterIDIndex); await db.execute(fcClusterIDIndex);
} }
await db.execute(deleteClusterPersonTable); await db.execute(deletePersonTable);
await db.execute(deleteNotPersonFeedbackTable); await db.execute(dropClusterPersonTable);
await db.execute(deleteClusterSummaryTable); await db.execute(dropNotPersonFeedbackTable);
await db.execute(deleteFaceClustersTable); await db.execute(dropClusterSummaryTable);
await db.execute(dropFaceClustersTable);
await db.execute(createClusterPersonTable); await db.execute(createClusterPersonTable);
await db.execute(createNotPersonFeedbackTable); await db.execute(createNotPersonFeedbackTable);
@ -1005,8 +972,9 @@ class FaceMLDataDB {
final db = await instance.asyncDB; final db = await instance.asyncDB;
// Drop the tables // Drop the tables
await db.execute(deleteClusterPersonTable); await db.execute(deletePersonTable);
await db.execute(deleteNotPersonFeedbackTable); await db.execute(dropClusterPersonTable);
await db.execute(dropNotPersonFeedbackTable);
// Recreate the tables // Recreate the tables
await db.execute(createClusterPersonTable); await db.execute(createClusterPersonTable);

View file

@ -29,7 +29,7 @@ const createFacesTable = '''CREATE TABLE IF NOT EXISTS $facesTable (
); );
'''; ''';
const deleteFacesTable = 'DELETE FROM $facesTable'; const deleteFacesTable = 'DROP TABLE IF EXISTS $facesTable';
// End of Faces Table Fields & Schema Queries // End of Faces Table Fields & Schema Queries
//##region Face Clusters Table Fields & Schema Queries //##region Face Clusters Table Fields & Schema Queries
@ -48,9 +48,15 @@ CREATE TABLE IF NOT EXISTS $faceClustersTable (
// -- Creating a non-unique index on clusterID for query optimization // -- Creating a non-unique index on clusterID for query optimization
const fcClusterIDIndex = const fcClusterIDIndex =
'''CREATE INDEX IF NOT EXISTS idx_fcClusterID ON $faceClustersTable($fcClusterID);'''; '''CREATE INDEX IF NOT EXISTS idx_fcClusterID ON $faceClustersTable($fcClusterID);''';
const deleteFaceClustersTable = 'DELETE FROM $faceClustersTable'; const dropFaceClustersTable = 'DROP TABLE IF EXISTS $faceClustersTable';
//##endregion //##endregion
// People Table Fields & Schema Queries
const personTable = 'person';
const deletePersonTable = 'DROP TABLE IF EXISTS $personTable';
//End People Table Fields & Schema Queries
// Clusters Table Fields & Schema Queries // Clusters Table Fields & Schema Queries
const clusterPersonTable = 'cluster_person'; const clusterPersonTable = 'cluster_person';
const personIdColumn = 'person_id'; const personIdColumn = 'person_id';
@ -63,7 +69,7 @@ CREATE TABLE IF NOT EXISTS $clusterPersonTable (
PRIMARY KEY($personIdColumn, $clusterIDColumn) PRIMARY KEY($personIdColumn, $clusterIDColumn)
); );
'''; ''';
const deleteClusterPersonTable = 'DELETE FROM $clusterPersonTable'; const dropClusterPersonTable = 'DROP TABLE IF EXISTS $clusterPersonTable';
// End Clusters Table Fields & Schema Queries // End Clusters Table Fields & Schema Queries
/// Cluster Summary Table Fields & Schema Queries /// Cluster Summary Table Fields & Schema Queries
@ -79,7 +85,7 @@ CREATE TABLE IF NOT EXISTS $clusterSummaryTable (
); );
'''; ''';
const deleteClusterSummaryTable = 'DELETE FROM $clusterSummaryTable'; const dropClusterSummaryTable = 'DROP TABLE IF EXISTS $clusterSummaryTable';
/// End Cluster Summary Table Fields & Schema Queries /// End Cluster Summary Table Fields & Schema Queries
@ -93,5 +99,5 @@ CREATE TABLE IF NOT EXISTS $notPersonFeedback (
PRIMARY KEY($personIdColumn, $clusterIDColumn) PRIMARY KEY($personIdColumn, $clusterIDColumn)
); );
'''; ''';
const deleteNotPersonFeedbackTable = 'DELETE FROM $notPersonFeedback'; const dropNotPersonFeedbackTable = 'DROP TABLE IF EXISTS $notPersonFeedback';
// End Clusters Table Fields & Schema Queries // End Clusters Table Fields & Schema Queries

View file

@ -814,7 +814,7 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Incorrect recovery key"), MessageLookupByLibrary.simpleMessage("Incorrect recovery key"),
"indexedItems": MessageLookupByLibrary.simpleMessage("Indexed items"), "indexedItems": MessageLookupByLibrary.simpleMessage("Indexed items"),
"indexingIsPaused": MessageLookupByLibrary.simpleMessage( "indexingIsPaused": MessageLookupByLibrary.simpleMessage(
"Indexing is paused. It will automatically resume when device is ready."), "Indexing is paused, will automatically resume when device is ready"),
"insecureDevice": "insecureDevice":
MessageLookupByLibrary.simpleMessage("Insecure device"), MessageLookupByLibrary.simpleMessage("Insecure device"),
"installManually": "installManually":

View file

@ -8794,10 +8794,10 @@ class S {
); );
} }
/// `Indexing is paused. It will automatically resume when device is ready.` /// `Indexing is paused, will automatically resume when device is ready`
String get indexingIsPaused { String get indexingIsPaused {
return Intl.message( return Intl.message(
'Indexing is paused. It will automatically resume when device is ready.', 'Indexing is paused, will automatically resume when device is ready',
name: 'indexingIsPaused', name: 'indexingIsPaused',
desc: '', desc: '',
args: [], args: [],

View file

@ -1236,5 +1236,5 @@
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.", "faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
"foundFaces": "Found faces", "foundFaces": "Found faces",
"clusteringProgress": "Clustering progress", "clusteringProgress": "Clustering progress",
"indexingIsPaused": "Indexing is paused. It will automatically resume when device is ready." "indexingIsPaused": "Indexing is paused, will automatically resume when device is ready"
} }

View file

@ -246,6 +246,11 @@ Future<void> _init(bool isBackground, {String via = ''}) async {
unawaited(SemanticSearchService.instance.init()); unawaited(SemanticSearchService.instance.init());
MachineLearningController.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
if (!UpdateService.instance.isFdroidFlavor()) {
// unawaited(ObjectDetectionService.instance.init());
if (flagService.faceSearchEnabled) { if (flagService.faceSearchEnabled) {
unawaited(FaceMlService.instance.init()); unawaited(FaceMlService.instance.init());
} else { } else {
@ -253,6 +258,7 @@ Future<void> _init(bool isBackground, {String via = ''}) async {
unawaited(LocalSettings.instance.toggleFaceIndexing()); unawaited(LocalSettings.instance.toggleFaceIndexing());
} }
} }
}
PersonService.init( PersonService.init(
EntityService.instance, EntityService.instance,
FaceMLDataDB.instance, FaceMLDataDB.instance,

View file

@ -43,7 +43,6 @@ import 'package:photos/services/machine_learning/face_ml/face_ml_result.dart';
import "package:photos/services/machine_learning/face_ml/person/person_service.dart"; import "package:photos/services/machine_learning/face_ml/person/person_service.dart";
import 'package:photos/services/machine_learning/file_ml/file_ml.dart'; import 'package:photos/services/machine_learning/file_ml/file_ml.dart';
import 'package:photos/services/machine_learning/file_ml/remote_fileml_service.dart'; import 'package:photos/services/machine_learning/file_ml/remote_fileml_service.dart';
import "package:photos/services/machine_learning/machine_learning_controller.dart";
import "package:photos/services/search_service.dart"; import "package:photos/services/search_service.dart";
import "package:photos/utils/file_util.dart"; import "package:photos/utils/file_util.dart";
import 'package:photos/utils/image_ml_isolate.dart'; import 'package:photos/utils/image_ml_isolate.dart';
@ -100,7 +99,7 @@ class FaceMlService {
final int _fileDownloadLimit = 5; final int _fileDownloadLimit = 5;
final int _embeddingFetchLimit = 200; final int _embeddingFetchLimit = 200;
final int _kForceClusteringFaceCount = 8000; final int _kForceClusteringFaceCount = 4000;
Future<void> init({bool initializeImageMlIsolate = false}) async { Future<void> init({bool initializeImageMlIsolate = false}) async {
if (LocalSettings.instance.isFaceIndexingEnabled == false) { if (LocalSettings.instance.isFaceIndexingEnabled == false) {
@ -164,16 +163,9 @@ class FaceMlService {
pauseIndexingAndClustering(); pauseIndexingAndClustering();
} }
}); });
if (Platform.isIOS &&
MachineLearningController.instance.isDeviceHealthy) {
_logger.info("Starting face indexing and clustering on iOS from init");
unawaited(indexAndClusterAll());
}
_listenIndexOnDiffSync(); _listenIndexOnDiffSync();
_listenOnPeopleChangedSync(); _listenOnPeopleChangedSync();
_logger.info('init done');
}); });
} }
@ -1026,11 +1018,7 @@ class FaceMlService {
try { try {
file = await getThumbnailForUploadedFile(enteFile); file = await getThumbnailForUploadedFile(enteFile);
} on PlatformException catch (e, s) { } on PlatformException catch (e, s) {
_logger.severe( _logger.severe("Could not get thumbnail for $enteFile due to PlatformException", e, s);
"Could not get thumbnail for $enteFile due to PlatformException",
e,
s,
);
throw ThumbnailRetrievalException(e.toString(), s); throw ThumbnailRetrievalException(e.toString(), s);
} }
} else { } else {

View file

@ -4,6 +4,7 @@ import "dart:io";
import "package:battery_info/battery_info_plugin.dart"; import "package:battery_info/battery_info_plugin.dart";
import "package:battery_info/model/android_battery_info.dart"; import "package:battery_info/model/android_battery_info.dart";
import "package:battery_info/model/iso_battery_info.dart"; import "package:battery_info/model/iso_battery_info.dart";
import "package:flutter/foundation.dart" show kDebugMode;
import "package:logging/logging.dart"; import "package:logging/logging.dart";
import "package:photos/core/event_bus.dart"; import "package:photos/core/event_bus.dart";
import "package:photos/events/machine_learning_control_event.dart"; import "package:photos/events/machine_learning_control_event.dart";
@ -18,7 +19,8 @@ class MachineLearningController {
static const kMaximumTemperature = 42; // 42 degree celsius static const kMaximumTemperature = 42; // 42 degree celsius
static const kMinimumBatteryLevel = 20; // 20% static const kMinimumBatteryLevel = 20; // 20%
static const kDefaultInteractionTimeout = Duration(seconds: 10); static const kDefaultInteractionTimeout =
kDebugMode ? Duration(seconds: 3) : Duration(seconds: 5);
static const kUnhealthyStates = ["over_heat", "over_voltage", "dead"]; static const kUnhealthyStates = ["over_heat", "over_voltage", "dead"];
bool _isDeviceHealthy = true; bool _isDeviceHealthy = true;
@ -29,7 +31,6 @@ class MachineLearningController {
bool get isDeviceHealthy => _isDeviceHealthy; bool get isDeviceHealthy => _isDeviceHealthy;
void init() { void init() {
_logger.info('init called');
if (Platform.isAndroid) { if (Platform.isAndroid) {
_startInteractionTimer(); _startInteractionTimer();
BatteryInfoPlugin() BatteryInfoPlugin()
@ -46,7 +47,6 @@ class MachineLearningController {
}); });
} }
_fireControlEvent(); _fireControlEvent();
_logger.info('init done');
} }
void onUserInteraction() { void onUserInteraction() {

View file

@ -89,8 +89,8 @@ class _MachineLearningSettingsPageState
iconButtonType: IconButtonType.secondary, iconButtonType: IconButtonType.secondary,
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(context);
if (Navigator.canPop(context)) Navigator.pop(context); Navigator.pop(context);
if (Navigator.canPop(context)) Navigator.pop(context); Navigator.pop(context);
}, },
), ),
], ],

View file

@ -1,16 +1,15 @@
import "package:flutter/cupertino.dart"; import "package:flutter/cupertino.dart";
import "package:intl/intl.dart";
import 'package:photos/core/constants.dart'; import 'package:photos/core/constants.dart';
import "package:photos/generated/l10n.dart"; import "package:photos/generated/l10n.dart";
import "package:photos/theme/ente_theme.dart"; import "package:photos/theme/ente_theme.dart";
class GroupHeaderWidget extends StatelessWidget { class GroupHeaderWidget extends StatelessWidget {
final int timestamp; final String title;
final int gridSize; final int gridSize;
const GroupHeaderWidget({ const GroupHeaderWidget({
super.key, super.key,
required this.timestamp, required this.title,
required this.gridSize, required this.gridSize,
}); });
@ -22,7 +21,7 @@ class GroupHeaderWidget extends StatelessWidget {
gridSize < photoGridSizeMax ? textTheme.body : textTheme.small; gridSize < photoGridSizeMax ? textTheme.body : textTheme.small;
final double horizontalPadding = gridSize < photoGridSizeMax ? 12.0 : 8.0; final double horizontalPadding = gridSize < photoGridSizeMax ? 12.0 : 8.0;
final double verticalPadding = gridSize < photoGridSizeMax ? 12.0 : 14.0; final double verticalPadding = gridSize < photoGridSizeMax ? 12.0 : 14.0;
final String dayTitle = _getDayTitle(context, timestamp);
return Padding( return Padding(
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(
horizontal: horizontalPadding, horizontal: horizontalPadding,
@ -31,33 +30,12 @@ class GroupHeaderWidget extends StatelessWidget {
child: Container( child: Container(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Text( child: Text(
dayTitle, title,
style: (dayTitle == S.of(context).dayToday) style: (title == S.of(context).dayToday)
? textStyle ? textStyle
: textStyle.copyWith(color: colorScheme.textMuted), : textStyle.copyWith(color: colorScheme.textMuted),
), ),
), ),
); );
} }
String _getDayTitle(BuildContext context, int timestamp) {
final date = DateTime.fromMicrosecondsSinceEpoch(timestamp);
final now = DateTime.now();
if (date.year == now.year && date.month == now.month) {
if (date.day == now.day) {
return S.of(context).dayToday;
} else if (date.day == now.day - 1) {
return S.of(context).dayYesterday;
}
}
if (date.year != DateTime.now().year) {
return DateFormat.yMMMEd(Localizations.localeOf(context).languageCode)
.format(date);
} else {
return DateFormat.MMMEd(Localizations.localeOf(context).languageCode)
.format(date);
}
}
} }

View file

@ -11,6 +11,7 @@ import 'package:photos/theme/ente_theme.dart';
import "package:photos/ui/viewer/gallery/component/grid/place_holder_grid_view_widget.dart"; import "package:photos/ui/viewer/gallery/component/grid/place_holder_grid_view_widget.dart";
import "package:photos/ui/viewer/gallery/component/group/group_gallery.dart"; import "package:photos/ui/viewer/gallery/component/group/group_gallery.dart";
import "package:photos/ui/viewer/gallery/component/group/group_header_widget.dart"; import "package:photos/ui/viewer/gallery/component/group/group_header_widget.dart";
import "package:photos/ui/viewer/gallery/component/group/type.dart";
import 'package:photos/ui/viewer/gallery/gallery.dart'; import 'package:photos/ui/viewer/gallery/gallery.dart';
import "package:photos/ui/viewer/gallery/state/gallery_context_state.dart"; import "package:photos/ui/viewer/gallery/state/gallery_context_state.dart";
@ -104,32 +105,30 @@ class _LazyGroupGalleryState extends State<LazyGroupGallery> {
if (_filesInGroup.isEmpty) { if (_filesInGroup.isEmpty) {
return; return;
} }
final DateTime groupDate = final galleryState = context.findAncestorStateOfType<GalleryState>();
DateTime.fromMicrosecondsSinceEpoch(_filesInGroup[0].creationTime!); final groupType = GalleryContextState.of(context)!.type;
// iterate over files and check if any of the belongs to this group // iterate over files and check if any of the belongs to this group
final anyCandidateForGroup = event.updatedFiles.any((file) { final anyCandidateForGroup = groupType.areModifiedFilesPartOfGroup(
final fileDate = DateTime.fromMicrosecondsSinceEpoch(file.creationTime!); event.updatedFiles,
return fileDate.year == groupDate.year && _filesInGroup[0],
fileDate.month == groupDate.month && lastFile: _filesInGroup.last,
fileDate.day == groupDate.day; );
});
if (anyCandidateForGroup) { if (anyCandidateForGroup) {
late int startRange, endRange;
(startRange, endRange) = groupType.getGroupRange(_filesInGroup[0]);
if (kDebugMode) { if (kDebugMode) {
_logger.info( _logger.info(
" files were updated due to ${event.reason} on " + " files were updated due to ${event.reason} on type ${groupType.name} from ${DateTime.fromMicrosecondsSinceEpoch(startRange).toIso8601String()}"
DateTime.fromMicrosecondsSinceEpoch( " to ${DateTime.fromMicrosecondsSinceEpoch(endRange).toIso8601String()}",
groupDate.microsecondsSinceEpoch,
).toIso8601String(),
); );
} }
if (event.type == EventType.addedOrUpdated || if (event.type == EventType.addedOrUpdated ||
widget.removalEventTypes.contains(event.type)) { widget.removalEventTypes.contains(event.type)) {
// We are reloading the whole group // We are reloading the whole group
final dayStartTime =
DateTime(groupDate.year, groupDate.month, groupDate.day);
final result = await widget.asyncLoader( final result = await widget.asyncLoader(
dayStartTime.microsecondsSinceEpoch, startRange,
dayStartTime.microsecondsSinceEpoch + microSecondsInDay - 1, endRange,
asc: GalleryContextState.of(context)!.sortOrderAsc, asc: GalleryContextState.of(context)!.sortOrderAsc,
); );
@ -144,7 +143,7 @@ class _LazyGroupGalleryState extends State<LazyGroupGallery> {
//[galleryState] will never be null except when LazyLoadingGallery is //[galleryState] will never be null except when LazyLoadingGallery is
//used without Gallery as an ancestor. //used without Gallery as an ancestor.
final galleryState = context.findAncestorStateOfType<GalleryState>();
if (galleryState?.mounted ?? false) { if (galleryState?.mounted ?? false) {
galleryState!.setState(() {}); galleryState!.setState(() {});
_filesInGroup = result.files; _filesInGroup = result.files;
@ -178,6 +177,7 @@ class _LazyGroupGalleryState extends State<LazyGroupGallery> {
if (_filesInGroup.isEmpty) { if (_filesInGroup.isEmpty) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
final groupType = GalleryContextState.of(context)!.type;
return Column( return Column(
children: [ children: [
Row( Row(
@ -185,7 +185,11 @@ class _LazyGroupGalleryState extends State<LazyGroupGallery> {
children: [ children: [
if (widget.enableFileGrouping) if (widget.enableFileGrouping)
GroupHeaderWidget( GroupHeaderWidget(
timestamp: _filesInGroup[0].creationTime!, title: groupType.getTitle(
context,
_filesInGroup[0],
lastFile: _filesInGroup.last,
),
gridSize: widget.photoGridSize, gridSize: widget.photoGridSize,
), ),
Expanded(child: Container()), Expanded(child: Container()),

View file

@ -0,0 +1,175 @@
import "package:flutter/widgets.dart";
import "package:intl/intl.dart";
import "package:photos/core/constants.dart";
import "package:photos/generated/l10n.dart";
import "package:photos/models/file/file.dart";
import "package:photos/utils/date_time_util.dart";
enum GroupType { day, week, month, size, year }
extension GroupTypeExtension on GroupType {
String get name {
switch (this) {
case GroupType.day:
return "day";
case GroupType.week:
return "week";
case GroupType.month:
return "month";
case GroupType.size:
return "size";
case GroupType.year:
return "year";
}
}
String getTitle(BuildContext context, EnteFile file, {EnteFile? lastFile}) {
if (this == GroupType.day) {
return _getDayTitle(context, file.creationTime!);
} else if (this == GroupType.week) {
// return weeks starting date to end date based on file
final date = DateTime.fromMicrosecondsSinceEpoch(file.creationTime!);
final startOfWeek = date.subtract(Duration(days: date.weekday - 1));
final endOfWeek = startOfWeek.add(const Duration(days: 6));
return "${DateFormat.MMMd(Localizations.localeOf(context).languageCode).format(startOfWeek)} - ${DateFormat.MMMd(Localizations.localeOf(context).languageCode).format(endOfWeek)}, ${endOfWeek.year}";
} else if (this == GroupType.year) {
final date = DateTime.fromMicrosecondsSinceEpoch(file.creationTime!);
return DateFormat.y(Localizations.localeOf(context).languageCode)
.format(date);
} else if (this == GroupType.month) {
final date = DateTime.fromMicrosecondsSinceEpoch(file.creationTime!);
return DateFormat.yMMM(Localizations.localeOf(context).languageCode)
.format(date);
} else {
throw UnimplementedError("not implemented for $this");
}
}
// returns true if the group should be refreshed.
// If groupType is day, it should return true if the list of modified files contains a file that was created on the same day as the first file.
// If groupType is week, it should return true if the list of modified files contains a file that was created in the same week as the first file.
// If groupType is month, it should return true if the list of modified files contains a file that was created in the same month as the first file.
// If groupType is year, it should return true if the list of modified files contains a file that was created in the same year as the first file.
bool areModifiedFilesPartOfGroup(
List<EnteFile> modifiedFiles,
EnteFile fistFile, {
EnteFile? lastFile,
}) {
switch (this) {
case GroupType.day:
return modifiedFiles.any(
(file) => areFromSameDay(fistFile.creationTime!, file.creationTime!),
);
case GroupType.week:
return modifiedFiles.any((file) {
final firstDate =
DateTime.fromMicrosecondsSinceEpoch(fistFile.creationTime!);
final fileDate =
DateTime.fromMicrosecondsSinceEpoch(file.creationTime!);
return areDatesInSameWeek(firstDate, fileDate);
});
case GroupType.month:
return modifiedFiles.any((file) {
final firstDate =
DateTime.fromMicrosecondsSinceEpoch(fistFile.creationTime!);
final fileDate =
DateTime.fromMicrosecondsSinceEpoch(file.creationTime!);
return firstDate.year == fileDate.year &&
firstDate.month == fileDate.month;
});
case GroupType.year:
return modifiedFiles.any((file) {
final firstDate =
DateTime.fromMicrosecondsSinceEpoch(fistFile.creationTime!);
final fileDate =
DateTime.fromMicrosecondsSinceEpoch(file.creationTime!);
return firstDate.year == fileDate.year;
});
default:
throw UnimplementedError("not implemented for $this");
}
}
// for day, year, month, year type, return the microsecond range of the group
(int, int) getGroupRange(EnteFile file) {
switch (this) {
case GroupType.day:
final date = DateTime.fromMicrosecondsSinceEpoch(file.creationTime!);
final startOfDay = DateTime(date.year, date.month, date.day);
return (
startOfDay.microsecondsSinceEpoch,
(startOfDay.microsecondsSinceEpoch + microSecondsInDay - 1),
);
case GroupType.week:
final date = DateTime.fromMicrosecondsSinceEpoch(file.creationTime!);
final startOfWeek = DateTime(date.year, date.month, date.day)
.subtract(Duration(days: date.weekday - 1));
final endOfWeek = startOfWeek.add(const Duration(days: 7));
return (
startOfWeek.microsecondsSinceEpoch,
endOfWeek.microsecondsSinceEpoch - 1
);
case GroupType.month:
final date = DateTime.fromMicrosecondsSinceEpoch(file.creationTime!);
final startOfMonth = DateTime(date.year, date.month);
final endOfMonth = DateTime(date.year, date.month + 1);
return (
startOfMonth.microsecondsSinceEpoch,
endOfMonth.microsecondsSinceEpoch - 1
);
case GroupType.year:
final date = DateTime.fromMicrosecondsSinceEpoch(file.creationTime!);
final startOfYear = DateTime(date.year);
final endOfYear = DateTime(date.year + 1);
return (
startOfYear.microsecondsSinceEpoch,
endOfYear.microsecondsSinceEpoch - 1
);
default:
throw UnimplementedError("not implemented for $this");
}
}
bool areFromSameGroup(EnteFile first, EnteFile second) {
switch (this) {
case GroupType.day:
return areFromSameDay(first.creationTime!, second.creationTime!);
case GroupType.month:
return DateTime.fromMicrosecondsSinceEpoch(first.creationTime!).year ==
DateTime.fromMicrosecondsSinceEpoch(second.creationTime!)
.year &&
DateTime.fromMicrosecondsSinceEpoch(first.creationTime!).month ==
DateTime.fromMicrosecondsSinceEpoch(second.creationTime!).month;
case GroupType.year:
return DateTime.fromMicrosecondsSinceEpoch(first.creationTime!).year ==
DateTime.fromMicrosecondsSinceEpoch(second.creationTime!).year;
case GroupType.week:
final firstDate =
DateTime.fromMicrosecondsSinceEpoch(first.creationTime!);
final secondDate =
DateTime.fromMicrosecondsSinceEpoch(second.creationTime!);
return areDatesInSameWeek(firstDate, secondDate);
default:
throw UnimplementedError("not implemented for $this");
}
}
String _getDayTitle(BuildContext context, int timestamp) {
final date = DateTime.fromMicrosecondsSinceEpoch(timestamp);
final now = DateTime.now();
if (date.year == now.year && date.month == now.month) {
if (date.day == now.day) {
return S.of(context).dayToday;
} else if (date.day == now.day - 1) {
return S.of(context).dayYesterday;
}
}
if (date.year != DateTime.now().year) {
return DateFormat.yMMMEd(Localizations.localeOf(context).languageCode)
.format(date);
} else {
return DateFormat.MMMEd(Localizations.localeOf(context).languageCode)
.format(date);
}
}
}

View file

@ -12,10 +12,10 @@ import 'package:photos/models/file/file.dart';
import 'package:photos/models/file_load_result.dart'; import 'package:photos/models/file_load_result.dart';
import 'package:photos/models/selected_files.dart'; import 'package:photos/models/selected_files.dart';
import 'package:photos/ui/common/loading_widget.dart'; import 'package:photos/ui/common/loading_widget.dart';
import "package:photos/ui/viewer/gallery/component/group/type.dart";
import "package:photos/ui/viewer/gallery/component/multiple_groups_gallery_view.dart"; import "package:photos/ui/viewer/gallery/component/multiple_groups_gallery_view.dart";
import 'package:photos/ui/viewer/gallery/empty_state.dart'; import 'package:photos/ui/viewer/gallery/empty_state.dart';
import "package:photos/ui/viewer/gallery/state/gallery_context_state.dart"; import "package:photos/ui/viewer/gallery/state/gallery_context_state.dart";
import 'package:photos/utils/date_time_util.dart';
import "package:photos/utils/debouncer.dart"; import "package:photos/utils/debouncer.dart";
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
@ -59,6 +59,7 @@ class Gallery extends StatefulWidget {
// add a Function variable to get sort value in bool // add a Function variable to get sort value in bool
final SortAscFn? sortAsyncFn; final SortAscFn? sortAsyncFn;
final GroupType groupType;
const Gallery({ const Gallery({
required this.asyncLoader, required this.asyncLoader,
@ -73,6 +74,7 @@ class Gallery extends StatefulWidget {
this.emptyState = const EmptyState(), this.emptyState = const EmptyState(),
this.scrollBottomSafeArea = 120.0, this.scrollBottomSafeArea = 120.0,
this.albumName = '', this.albumName = '',
this.groupType = GroupType.day,
this.enableFileGrouping = true, this.enableFileGrouping = true,
this.loadingWidget = const EnteLoadingWidget(), this.loadingWidget = const EnteLoadingWidget(),
this.disableScroll = false, this.disableScroll = false,
@ -248,6 +250,7 @@ class GalleryState extends State<Gallery> {
return GalleryContextState( return GalleryContextState(
sortOrderAsc: _sortOrderAsc, sortOrderAsc: _sortOrderAsc,
inSelectionMode: widget.inSelectionMode, inSelectionMode: widget.inSelectionMode,
type: widget.groupType,
child: MultipleGroupsGalleryView( child: MultipleGroupsGalleryView(
itemScroller: _itemScroller, itemScroller: _itemScroller,
groupedFiles: currentGroupedFiles, groupedFiles: currentGroupedFiles,
@ -273,13 +276,11 @@ class GalleryState extends State<Gallery> {
List<List<EnteFile>> _groupFiles(List<EnteFile> files) { List<List<EnteFile>> _groupFiles(List<EnteFile> files) {
List<EnteFile> dailyFiles = []; List<EnteFile> dailyFiles = [];
final List<List<EnteFile>> resultGroupedFiles = []; final List<List<EnteFile>> resultGroupedFiles = [];
for (int index = 0; index < files.length; index++) { for (int index = 0; index < files.length; index++) {
if (index > 0 && if (index > 0 &&
!areFromSameDay( !widget.groupType.areFromSameGroup(files[index - 1], files[index])) {
files[index - 1].creationTime!,
files[index].creationTime!,
)) {
resultGroupedFiles.add(dailyFiles); resultGroupedFiles.add(dailyFiles);
dailyFiles = []; dailyFiles = [];
} }

View file

@ -1,12 +1,15 @@
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:photos/ui/viewer/gallery/component/group/type.dart";
class GalleryContextState extends InheritedWidget { class GalleryContextState extends InheritedWidget {
///Sorting by creation time ///Sorting by creation time
final bool sortOrderAsc; final bool sortOrderAsc;
final bool inSelectionMode; final bool inSelectionMode;
final GroupType type;
const GalleryContextState({ const GalleryContextState({
this.inSelectionMode = false, this.inSelectionMode = false,
this.type = GroupType.day,
required this.sortOrderAsc, required this.sortOrderAsc,
required Widget child, required Widget child,
Key? key, Key? key,
@ -19,6 +22,7 @@ class GalleryContextState extends InheritedWidget {
@override @override
bool updateShouldNotify(GalleryContextState oldWidget) { bool updateShouldNotify(GalleryContextState oldWidget) {
return sortOrderAsc != oldWidget.sortOrderAsc || return sortOrderAsc != oldWidget.sortOrderAsc ||
inSelectionMode != oldWidget.inSelectionMode; inSelectionMode != oldWidget.inSelectionMode ||
type != oldWidget.type;
} }
} }

View file

@ -97,7 +97,7 @@ class _AppBarWidgetState extends State<ClusterAppBar> {
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
actions: _getDefaultActions(context), actions: kDebugMode ? _getDefaultActions(context) : null,
); );
} }

View file

@ -161,8 +161,7 @@ class _ClusterPageState extends State<ClusterPage> {
), ),
), ),
showNamingBanner showNamingBanner
? SafeArea( ? Dismissible(
child: Dismissible(
key: const Key("namingBanner"), key: const Key("namingBanner"),
direction: DismissDirection.horizontal, direction: DismissDirection.horizontal,
onDismissed: (direction) { onDismissed: (direction) {
@ -200,7 +199,6 @@ class _ClusterPageState extends State<ClusterPage> {
} }
}, },
), ),
),
) )
: const SizedBox.shrink(), : const SizedBox.shrink(),
], ],

View file

@ -38,17 +38,12 @@ class _PersonClustersPageState extends State<PersonClustersPage> {
.getClusterFilesForPersonID(widget.person.remoteID), .getClusterFilesForPersonID(widget.person.remoteID),
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.hasData) { if (snapshot.hasData) {
final clusters = snapshot.data!; final List<int> keys = snapshot.data!.keys.toList();
final List<int> keys = clusters.keys.toList();
// Sort the clusters by the number of files in each cluster, largest first
keys.sort(
(b, a) => clusters[a]!.length.compareTo(clusters[b]!.length),
);
return ListView.builder( return ListView.builder(
itemCount: keys.length, itemCount: keys.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final int clusterID = keys[index]; final int clusterID = keys[index];
final List<EnteFile> files = clusters[clusterID]!; final List<EnteFile> files = snapshot.data![keys[index]]!;
return InkWell( return InkWell(
onTap: () { onTap: () {
Navigator.of(context).push( Navigator.of(context).push(
@ -98,11 +93,10 @@ class _PersonClustersPageState extends State<PersonClustersPage> {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[ children: <Widget>[
Text( Text(
"${files.length} photos", "${snapshot.data![keys[index]]!.length} photos",
style: getEnteTextTheme(context).body, style: getEnteTextTheme(context).body,
), ),
(index != 0) GestureDetector(
? GestureDetector(
onTap: () async { onTap: () async {
try { try {
await PersonService.instance await PersonService.instance
@ -113,8 +107,7 @@ class _PersonClustersPageState extends State<PersonClustersPage> {
_logger.info( _logger.info(
"Removed cluster $clusterID from person ${widget.person.remoteID}", "Removed cluster $clusterID from person ${widget.person.remoteID}",
); );
Bus.instance Bus.instance.fire(PeopleChangedEvent());
.fire(PeopleChangedEvent());
setState(() {}); setState(() {});
} catch (e) { } catch (e) {
_logger.severe( _logger.severe(
@ -127,8 +120,7 @@ class _PersonClustersPageState extends State<PersonClustersPage> {
CupertinoIcons.minus_circled, CupertinoIcons.minus_circled,
color: Colors.red, color: Colors.red,
), ),
) ),
: const SizedBox.shrink(),
], ],
), ),
), ),

View file

@ -28,6 +28,27 @@ bool areFromSameDay(int firstCreationTime, int secondCreationTime) {
firstDate.day == secondDate.day; firstDate.day == secondDate.day;
} }
bool areDatesInSameWeek(DateTime date1, DateTime date2) {
if (date1.year == date2.year &&
date1.month == date2.month &&
date1.day == date2.day) {
return true;
}
final int dayOfWeek1 = date1.weekday;
final int dayOfWeek2 = date2.weekday;
// Calculate the start and end dates of the week for both dates
final DateTime startOfWeek1 = date1.subtract(Duration(days: dayOfWeek1 - 1));
final DateTime endOfWeek1 = startOfWeek1.add(const Duration(days: 6));
final DateTime startOfWeek2 = date2.subtract(Duration(days: dayOfWeek2 - 1));
final DateTime endOfWeek2 = startOfWeek2.add(const Duration(days: 6));
// Check if the two dates fall within the same week range
if ((date1.isAfter(startOfWeek2) && date1.isBefore(endOfWeek2)) ||
(date2.isAfter(startOfWeek1) && date2.isBefore(endOfWeek1))) {
return true;
}
return false;
}
// Create link default names: // Create link default names:
// Same day: "Dec 19, 2022" // Same day: "Dec 19, 2022"
// Same month: "Dec 19 - 22, 2022" // Same month: "Dec 19 - 22, 2022"

View file

@ -12,7 +12,7 @@ description: ente photos application
# Read more about iOS versioning at # Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 0.8.112+636 version: 0.8.110+634
publish_to: none publish_to: none
environment: environment:

View file

@ -84,6 +84,20 @@ export const codeFromURIString = (id: string, uriString: string): Code => {
const _codeFromURIString = (id: string, uriString: string): Code => { const _codeFromURIString = (id: string, uriString: string): Code => {
const url = new URL(uriString); const url = new URL(uriString);
// A URL like
//
// new URL("otpauth://hotp/Test?secret=AAABBBCCCDDDEEEFFF&issuer=Test&counter=0")
//
// is parsed differently by the browser and Node depending on the scheme.
// When the scheme is http(s), then both of them consider "hotp" as the
// `host`. However, when the scheme is "otpauth", as is our case, the
// browser considers the entire thing as part of the pathname. so we get.
//
// host: ""
// pathname: "//hotp/Test"
//
// Since this code run on browsers only, we parse as per that behaviour.
const [type, path] = parsePathname(url); const [type, path] = parsePathname(url);
return { return {
@ -101,46 +115,10 @@ const _codeFromURIString = (id: string, uriString: string): Code => {
}; };
const parsePathname = (url: URL): [type: Code["type"], path: string] => { const parsePathname = (url: URL): [type: Code["type"], path: string] => {
// A URL like
//
// new
// URL("otpauth://hotp/Test?secret=AAABBBCCCDDDEEEFFF&issuer=Test&counter=0")
//
// is parsed differently by different browsers, and there are differences
// even depending on the scheme.
//
// When the scheme is http(s), then all of them consider "hotp" as the
// `host`. However, when the scheme is "otpauth", as is our case, Safari
// splits it into
//
// host: "hotp"
// pathname: "/Test"
//
// while Chrome and Firefox consider the entire thing as part of the
// pathname
//
// host: ""
// pathname: "//hotp/Test"
//
// So we try to handle both scenarios by first checking for the host match,
// and if not fall back to deducing the "host" from the pathname.
switch (url.host.toLowerCase()) {
case "totp":
return ["totp", url.pathname.toLowerCase()];
case "hotp":
return ["hotp", url.pathname.toLowerCase()];
case "steam":
return ["steam", url.pathname.toLowerCase()];
default:
break;
}
const p = url.pathname.toLowerCase(); const p = url.pathname.toLowerCase();
if (p.startsWith("//totp")) return ["totp", url.pathname.slice(6)]; if (p.startsWith("//totp")) return ["totp", url.pathname.slice(6)];
if (p.startsWith("//hotp")) return ["hotp", url.pathname.slice(6)]; if (p.startsWith("//hotp")) return ["hotp", url.pathname.slice(6)];
if (p.startsWith("//steam")) return ["steam", url.pathname.slice(7)]; if (p.startsWith("//steam")) return ["steam", url.pathname.slice(7)];
throw new Error(`Unsupported code or unparseable path "${url.pathname}"`); throw new Error(`Unsupported code or unparseable path "${url.pathname}"`);
}; };

View file

@ -153,12 +153,12 @@ const Title_ = styled("div")`
`; `;
interface WatchList { interface WatchList {
watches: FolderWatch[] | undefined; watches: FolderWatch[];
removeWatch: (watch: FolderWatch) => void; removeWatch: (watch: FolderWatch) => void;
} }
const WatchList: React.FC<WatchList> = ({ watches, removeWatch }) => { const WatchList: React.FC<WatchList> = ({ watches, removeWatch }) => {
return (watches ?? []).length === 0 ? ( return watches.length === 0 ? (
<NoWatches /> <NoWatches />
) : ( ) : (
<WatchesContainer> <WatchesContainer>