commit
232acfa211
12 changed files with 127 additions and 87 deletions
|
@ -35,6 +35,15 @@ 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;
|
||||||
|
|
||||||
|
@ -50,17 +59,42 @@ 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);
|
||||||
await _onCreate(asyncDBConnection);
|
final stopwatch = Stopwatch()..start();
|
||||||
|
_logger.info("FaceMLDataDB: Starting migration");
|
||||||
|
await _migrate(asyncDBConnection);
|
||||||
|
_logger.info(
|
||||||
|
"FaceMLDataDB Migration took ${stopwatch.elapsedMilliseconds} ms",
|
||||||
|
);
|
||||||
|
stopwatch.stop();
|
||||||
|
|
||||||
return asyncDBConnection;
|
return asyncDBConnection;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onCreate(SqliteDatabase asyncDBConnection) async {
|
Future<void> _migrate(
|
||||||
await asyncDBConnection.execute(createFacesTable);
|
SqliteDatabase database,
|
||||||
await asyncDBConnection.execute(createFaceClustersTable);
|
) async {
|
||||||
await asyncDBConnection.execute(createClusterPersonTable);
|
final result = await database.execute('PRAGMA user_version');
|
||||||
await asyncDBConnection.execute(createClusterSummaryTable);
|
final currentVersion = result[0]['user_version'] as int;
|
||||||
await asyncDBConnection.execute(createNotPersonFeedbackTable);
|
final toVersion = _migrationScripts.length;
|
||||||
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.
|
||||||
|
@ -195,10 +229,10 @@ class FaceMLDataDB {
|
||||||
final db = await instance.asyncDB;
|
final db = await instance.asyncDB;
|
||||||
|
|
||||||
await db.execute(deleteFacesTable);
|
await db.execute(deleteFacesTable);
|
||||||
await db.execute(dropClusterPersonTable);
|
await db.execute(deleteFaceClustersTable);
|
||||||
await db.execute(dropClusterSummaryTable);
|
await db.execute(deleteClusterPersonTable);
|
||||||
await db.execute(deletePersonTable);
|
await db.execute(deleteClusterSummaryTable);
|
||||||
await db.execute(dropNotPersonFeedbackTable);
|
await db.execute(deleteNotPersonFeedbackTable);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Iterable<Uint8List>> getFaceEmbeddingsForCluster(
|
Future<Iterable<Uint8List>> getFaceEmbeddingsForCluster(
|
||||||
|
@ -734,7 +768,7 @@ class FaceMLDataDB {
|
||||||
try {
|
try {
|
||||||
final db = await instance.asyncDB;
|
final db = await instance.asyncDB;
|
||||||
|
|
||||||
await db.execute(dropFaceClustersTable);
|
await db.execute(deleteFaceClustersTable);
|
||||||
await db.execute(createFaceClustersTable);
|
await db.execute(createFaceClustersTable);
|
||||||
await db.execute(fcClusterIDIndex);
|
await db.execute(fcClusterIDIndex);
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
|
@ -945,16 +979,15 @@ 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(dropFaceClustersTable);
|
await db.execute(deleteFaceClustersTable);
|
||||||
await db.execute(createFaceClustersTable);
|
await db.execute(createFaceClustersTable);
|
||||||
await db.execute(fcClusterIDIndex);
|
await db.execute(fcClusterIDIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.execute(deletePersonTable);
|
await db.execute(deleteClusterPersonTable);
|
||||||
await db.execute(dropClusterPersonTable);
|
await db.execute(deleteNotPersonFeedbackTable);
|
||||||
await db.execute(dropNotPersonFeedbackTable);
|
await db.execute(deleteClusterSummaryTable);
|
||||||
await db.execute(dropClusterSummaryTable);
|
await db.execute(deleteFaceClustersTable);
|
||||||
await db.execute(dropFaceClustersTable);
|
|
||||||
|
|
||||||
await db.execute(createClusterPersonTable);
|
await db.execute(createClusterPersonTable);
|
||||||
await db.execute(createNotPersonFeedbackTable);
|
await db.execute(createNotPersonFeedbackTable);
|
||||||
|
@ -972,9 +1005,8 @@ class FaceMLDataDB {
|
||||||
final db = await instance.asyncDB;
|
final db = await instance.asyncDB;
|
||||||
|
|
||||||
// Drop the tables
|
// Drop the tables
|
||||||
await db.execute(deletePersonTable);
|
await db.execute(deleteClusterPersonTable);
|
||||||
await db.execute(dropClusterPersonTable);
|
await db.execute(deleteNotPersonFeedbackTable);
|
||||||
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 = 'DROP TABLE IF EXISTS $facesTable';
|
const deleteFacesTable = 'DELETE FROM $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,15 +48,9 @@ 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 dropFaceClustersTable = 'DROP TABLE IF EXISTS $faceClustersTable';
|
const deleteFaceClustersTable = 'DELETE FROM $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';
|
||||||
|
@ -69,7 +63,7 @@ CREATE TABLE IF NOT EXISTS $clusterPersonTable (
|
||||||
PRIMARY KEY($personIdColumn, $clusterIDColumn)
|
PRIMARY KEY($personIdColumn, $clusterIDColumn)
|
||||||
);
|
);
|
||||||
''';
|
''';
|
||||||
const dropClusterPersonTable = 'DROP TABLE IF EXISTS $clusterPersonTable';
|
const deleteClusterPersonTable = 'DELETE FROM $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
|
||||||
|
@ -85,7 +79,7 @@ CREATE TABLE IF NOT EXISTS $clusterSummaryTable (
|
||||||
);
|
);
|
||||||
''';
|
''';
|
||||||
|
|
||||||
const dropClusterSummaryTable = 'DROP TABLE IF EXISTS $clusterSummaryTable';
|
const deleteClusterSummaryTable = 'DELETE FROM $clusterSummaryTable';
|
||||||
|
|
||||||
/// End Cluster Summary Table Fields & Schema Queries
|
/// End Cluster Summary Table Fields & Schema Queries
|
||||||
|
|
||||||
|
@ -99,5 +93,5 @@ CREATE TABLE IF NOT EXISTS $notPersonFeedback (
|
||||||
PRIMARY KEY($personIdColumn, $clusterIDColumn)
|
PRIMARY KEY($personIdColumn, $clusterIDColumn)
|
||||||
);
|
);
|
||||||
''';
|
''';
|
||||||
const dropNotPersonFeedbackTable = 'DROP TABLE IF EXISTS $notPersonFeedback';
|
const deleteNotPersonFeedbackTable = 'DELETE FROM $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, will automatically resume when device is ready"),
|
"Indexing is paused. It 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, will automatically resume when device is ready`
|
/// `Indexing is paused. It will automatically resume when device is ready.`
|
||||||
String get indexingIsPaused {
|
String get indexingIsPaused {
|
||||||
return Intl.message(
|
return Intl.message(
|
||||||
'Indexing is paused, will automatically resume when device is ready',
|
'Indexing is paused. It 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, will automatically resume when device is ready"
|
"indexingIsPaused": "Indexing is paused. It will automatically resume when device is ready."
|
||||||
}
|
}
|
||||||
|
|
|
@ -246,17 +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
|
if (flagService.faceSearchEnabled) {
|
||||||
// from source.
|
unawaited(FaceMlService.instance.init());
|
||||||
// See https://gitlab.com/fdroid/fdroiddata/-/merge_requests/12671#note_1294346819
|
} else {
|
||||||
if (!UpdateService.instance.isFdroidFlavor()) {
|
if (LocalSettings.instance.isFaceIndexingEnabled) {
|
||||||
// unawaited(ObjectDetectionService.instance.init());
|
unawaited(LocalSettings.instance.toggleFaceIndexing());
|
||||||
if (flagService.faceSearchEnabled) {
|
|
||||||
unawaited(FaceMlService.instance.init());
|
|
||||||
} else {
|
|
||||||
if (LocalSettings.instance.isFaceIndexingEnabled) {
|
|
||||||
unawaited(LocalSettings.instance.toggleFaceIndexing());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PersonService.init(
|
PersonService.init(
|
||||||
|
|
|
@ -43,6 +43,7 @@ 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';
|
||||||
|
@ -99,7 +100,7 @@ class FaceMlService {
|
||||||
|
|
||||||
final int _fileDownloadLimit = 5;
|
final int _fileDownloadLimit = 5;
|
||||||
final int _embeddingFetchLimit = 200;
|
final int _embeddingFetchLimit = 200;
|
||||||
final int _kForceClusteringFaceCount = 4000;
|
final int _kForceClusteringFaceCount = 8000;
|
||||||
|
|
||||||
Future<void> init({bool initializeImageMlIsolate = false}) async {
|
Future<void> init({bool initializeImageMlIsolate = false}) async {
|
||||||
if (LocalSettings.instance.isFaceIndexingEnabled == false) {
|
if (LocalSettings.instance.isFaceIndexingEnabled == false) {
|
||||||
|
@ -163,9 +164,16 @@ 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');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1016,9 +1024,13 @@ class FaceMlService {
|
||||||
File? file;
|
File? file;
|
||||||
if (enteFile.fileType == FileType.video) {
|
if (enteFile.fileType == FileType.video) {
|
||||||
try {
|
try {
|
||||||
file = await getThumbnailForUploadedFile(enteFile);
|
file = await getThumbnailForUploadedFile(enteFile);
|
||||||
} on PlatformException catch (e, s) {
|
} on PlatformException catch (e, s) {
|
||||||
_logger.severe("Could not get thumbnail for $enteFile due to PlatformException", e, s);
|
_logger.severe(
|
||||||
|
"Could not get thumbnail for $enteFile due to PlatformException",
|
||||||
|
e,
|
||||||
|
s,
|
||||||
|
);
|
||||||
throw ThumbnailRetrievalException(e.toString(), s);
|
throw ThumbnailRetrievalException(e.toString(), s);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -4,7 +4,6 @@ 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";
|
||||||
|
@ -19,8 +18,7 @@ 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 =
|
static const kDefaultInteractionTimeout = Duration(seconds: 10);
|
||||||
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;
|
||||||
|
@ -31,6 +29,7 @@ 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()
|
||||||
|
@ -47,6 +46,7 @@ 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);
|
||||||
Navigator.pop(context);
|
if (Navigator.canPop(context)) Navigator.pop(context);
|
||||||
Navigator.pop(context);
|
if (Navigator.canPop(context)) Navigator.pop(context);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
@ -97,7 +97,7 @@ class _AppBarWidgetState extends State<ClusterAppBar> {
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
actions: kDebugMode ? _getDefaultActions(context) : null,
|
actions: _getDefaultActions(context),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -38,12 +38,17 @@ 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 List<int> keys = snapshot.data!.keys.toList();
|
final clusters = snapshot.data!;
|
||||||
|
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 = snapshot.data![keys[index]]!;
|
final List<EnteFile> files = clusters[clusterID]!;
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.of(context).push(
|
Navigator.of(context).push(
|
||||||
|
@ -93,34 +98,37 @@ class _PersonClustersPageState extends State<PersonClustersPage> {
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text(
|
Text(
|
||||||
"${snapshot.data![keys[index]]!.length} photos",
|
"${files.length} photos",
|
||||||
style: getEnteTextTheme(context).body,
|
style: getEnteTextTheme(context).body,
|
||||||
),
|
),
|
||||||
GestureDetector(
|
(index != 0)
|
||||||
onTap: () async {
|
? GestureDetector(
|
||||||
try {
|
onTap: () async {
|
||||||
await PersonService.instance
|
try {
|
||||||
.removeClusterToPerson(
|
await PersonService.instance
|
||||||
personID: widget.person.remoteID,
|
.removeClusterToPerson(
|
||||||
clusterID: clusterID,
|
personID: widget.person.remoteID,
|
||||||
);
|
clusterID: clusterID,
|
||||||
_logger.info(
|
);
|
||||||
"Removed cluster $clusterID from person ${widget.person.remoteID}",
|
_logger.info(
|
||||||
);
|
"Removed cluster $clusterID from person ${widget.person.remoteID}",
|
||||||
Bus.instance.fire(PeopleChangedEvent());
|
);
|
||||||
setState(() {});
|
Bus.instance
|
||||||
} catch (e) {
|
.fire(PeopleChangedEvent());
|
||||||
_logger.severe(
|
setState(() {});
|
||||||
"removing cluster from person,",
|
} catch (e) {
|
||||||
e,
|
_logger.severe(
|
||||||
);
|
"removing cluster from person,",
|
||||||
}
|
e,
|
||||||
},
|
);
|
||||||
child: const Icon(
|
}
|
||||||
CupertinoIcons.minus_circled,
|
},
|
||||||
color: Colors.red,
|
child: const Icon(
|
||||||
),
|
CupertinoIcons.minus_circled,
|
||||||
),
|
color: Colors.red,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -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.110+634
|
version: 0.8.112+636
|
||||||
publish_to: none
|
publish_to: none
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
|
|
Loading…
Reference in a new issue