Compare commits
13 commits
main
...
generic_gr
Author | SHA1 | Date | |
---|---|---|---|
bcf3084d97 | |||
5e4d530b93 | |||
22ff318249 | |||
643b77e81e | |||
3ce8a09e39 | |||
ce6160a06a | |||
c21a0cfdb4 | |||
241c755446 | |||
cff695dd02 | |||
5f9b0d11f2 | |||
e75be714d9 | |||
da329c498c | |||
cc74e08155 |
|
@ -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",
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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
|
||||||
|
|
2
mobile/lib/generated/intl/messages_en.dart
generated
2
mobile/lib/generated/intl/messages_en.dart
generated
|
@ -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":
|
||||||
|
|
4
mobile/lib/generated/l10n.dart
generated
4
mobile/lib/generated/l10n.dart
generated
|
@ -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: [],
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
|
@ -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,
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()),
|
||||||
|
|
175
mobile/lib/ui/viewer/gallery/component/group/type.dart
Normal file
175
mobile/lib/ui/viewer/gallery/component/group/type.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 = [];
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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(),
|
||||||
],
|
],
|
||||||
|
|
|
@ -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(),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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}"`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue