feat(location_tag): add location tags (#908)
This commit is contained in:
commit
06c9ead3e2
55 changed files with 3823 additions and 313 deletions
|
@ -57,3 +57,11 @@ const double restrictedMaxWidth = 430;
|
|||
const double mobileSmallThreshold = 336;
|
||||
|
||||
const publicLinkDeviceLimits = [50, 25, 10, 5, 2, 1];
|
||||
|
||||
const kilometersPerDegree = 111.16;
|
||||
|
||||
const radiusValues = <int>[2, 10, 20, 40, 80, 200, 400, 1200];
|
||||
|
||||
const defaultRadiusValueIndex = 4;
|
||||
|
||||
const galleryGridSpacing = 2.0;
|
||||
|
|
65
lib/db/entities_db.dart
Normal file
65
lib/db/entities_db.dart
Normal file
|
@ -0,0 +1,65 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:photos/db/files_db.dart';
|
||||
import "package:photos/models/api/entity/type.dart";
|
||||
import "package:photos/models/local_entity_data.dart";
|
||||
import 'package:sqflite/sqlite_api.dart';
|
||||
|
||||
extension EntitiesDB on FilesDB {
|
||||
Future<void> upsertEntities(
|
||||
List<LocalEntityData> data, {
|
||||
ConflictAlgorithm conflictAlgorithm = ConflictAlgorithm.replace,
|
||||
}) async {
|
||||
debugPrint("Inserting missing PathIDToLocalIDMapping");
|
||||
final db = await database;
|
||||
var batch = db.batch();
|
||||
int batchCounter = 0;
|
||||
for (LocalEntityData e in data) {
|
||||
if (batchCounter == 400) {
|
||||
await batch.commit(noResult: true);
|
||||
batch = db.batch();
|
||||
batchCounter = 0;
|
||||
}
|
||||
batch.insert(
|
||||
"entities",
|
||||
e.toJson(),
|
||||
conflictAlgorithm: conflictAlgorithm,
|
||||
);
|
||||
batchCounter++;
|
||||
}
|
||||
await batch.commit(noResult: true);
|
||||
}
|
||||
|
||||
Future<void> deleteEntities(
|
||||
List<String> ids,
|
||||
) async {
|
||||
final db = await database;
|
||||
var batch = db.batch();
|
||||
int batchCounter = 0;
|
||||
for (String id in ids) {
|
||||
if (batchCounter == 400) {
|
||||
await batch.commit(noResult: true);
|
||||
batch = db.batch();
|
||||
batchCounter = 0;
|
||||
}
|
||||
batch.delete(
|
||||
"entities",
|
||||
where: "id = ?",
|
||||
whereArgs: [id],
|
||||
);
|
||||
batchCounter++;
|
||||
}
|
||||
await batch.commit(noResult: true);
|
||||
}
|
||||
|
||||
Future<List<LocalEntityData>> getEntities(EntityType type) async {
|
||||
final db = await database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
"entities",
|
||||
where: "type = ?",
|
||||
whereArgs: [type.typeToString()],
|
||||
);
|
||||
return List.generate(maps.length, (i) {
|
||||
return LocalEntityData.fromJson(maps[i]);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
import 'dart:developer' as dev;
|
||||
import 'dart:io' as io;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
@ -8,7 +9,7 @@ import 'package:photos/models/backup_status.dart';
|
|||
import 'package:photos/models/file.dart';
|
||||
import 'package:photos/models/file_load_result.dart';
|
||||
import 'package:photos/models/file_type.dart';
|
||||
import 'package:photos/models/location.dart';
|
||||
import 'package:photos/models/location/location.dart';
|
||||
import 'package:photos/models/magic_metadata.dart';
|
||||
import 'package:photos/utils/file_uploader_util.dart';
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
|
@ -79,6 +80,7 @@ class FilesDB {
|
|||
...createOnDeviceFilesAndPathCollection(),
|
||||
...addFileSizeColumn(),
|
||||
...updateIndexes(),
|
||||
...createEntityDataTable(),
|
||||
];
|
||||
|
||||
final dbConfig = MigrationConfig(
|
||||
|
@ -331,6 +333,20 @@ class FilesDB {
|
|||
];
|
||||
}
|
||||
|
||||
static List<String> createEntityDataTable() {
|
||||
return [
|
||||
'''
|
||||
CREATE TABLE IF NOT EXISTS entities (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
ownerID INTEGER NOT NULL,
|
||||
data TEXT NOT NULL DEFAULT '{}',
|
||||
updatedAt INTEGER NOT NULL
|
||||
);
|
||||
'''
|
||||
];
|
||||
}
|
||||
|
||||
static List<String> addFileSizeColumn() {
|
||||
return [
|
||||
'''
|
||||
|
@ -485,14 +501,20 @@ class FilesDB {
|
|||
bool? asc,
|
||||
int visibility = visibilityVisible,
|
||||
Set<int>? ignoredCollectionIDs,
|
||||
bool onlyFilesWithLocation = false,
|
||||
}) async {
|
||||
final stopWatch = Stopwatch()..start();
|
||||
|
||||
final db = await instance.database;
|
||||
final order = (asc ?? false ? 'ASC' : 'DESC');
|
||||
final results = await db.query(
|
||||
filesTable,
|
||||
where:
|
||||
'$columnCreationTime >= ? AND $columnCreationTime <= ? AND ($columnOwnerID IS NULL OR $columnOwnerID = ?) AND ($columnCollectionID IS NOT NULL AND $columnCollectionID IS NOT -1)'
|
||||
' AND $columnMMdVisibility = ?',
|
||||
where: onlyFilesWithLocation
|
||||
? '$columnLatitude IS NOT NULL AND $columnLongitude IS NOT NULL AND ($columnLatitude IS NOT 0 OR $columnLongitude IS NOT 0)'
|
||||
'AND $columnCreationTime >= ? AND $columnCreationTime <= ? AND ($columnOwnerID IS NULL OR $columnOwnerID = ?) AND ($columnCollectionID IS NOT NULL AND $columnCollectionID IS NOT -1)'
|
||||
'AND $columnMMdVisibility = ?'
|
||||
: '$columnCreationTime >= ? AND $columnCreationTime <= ? AND ($columnOwnerID IS NULL OR $columnOwnerID = ?) AND ($columnCollectionID IS NOT NULL AND $columnCollectionID IS NOT -1)'
|
||||
' AND $columnMMdVisibility = ?',
|
||||
whereArgs: [startTime, endTime, ownerID, visibility],
|
||||
orderBy:
|
||||
'$columnCreationTime ' + order + ', $columnModificationTime ' + order,
|
||||
|
@ -501,6 +523,9 @@ class FilesDB {
|
|||
final files = convertToFiles(results);
|
||||
final List<File> deduplicatedFiles =
|
||||
_deduplicatedAndFilterIgnoredFiles(files, ignoredCollectionIDs);
|
||||
dev.log(
|
||||
"getAllPendingOrUploadedFiles time taken: ${stopWatch.elapsedMilliseconds} ms");
|
||||
stopWatch.stop();
|
||||
return FileLoadResult(deduplicatedFiles, files.length == limit);
|
||||
}
|
||||
|
||||
|
@ -511,14 +536,19 @@ class FilesDB {
|
|||
int? limit,
|
||||
bool? asc,
|
||||
Set<int>? ignoredCollectionIDs,
|
||||
bool onlyFilesWithLocation = false,
|
||||
}) async {
|
||||
final db = await instance.database;
|
||||
final order = (asc ?? false ? 'ASC' : 'DESC');
|
||||
final results = await db.query(
|
||||
filesTable,
|
||||
where:
|
||||
'$columnCreationTime >= ? AND $columnCreationTime <= ? AND ($columnOwnerID IS NULL OR $columnOwnerID = ?) AND ($columnMMdVisibility IS NULL OR $columnMMdVisibility = ?)'
|
||||
' AND ($columnLocalID IS NOT NULL OR ($columnCollectionID IS NOT NULL AND $columnCollectionID IS NOT -1))',
|
||||
where: onlyFilesWithLocation
|
||||
? '$columnLatitude IS NOT NULL AND $columnLongitude IS NOT NULL AND ($columnLatitude IS NOT 0 OR $columnLongitude IS NOT 0)'
|
||||
' AND $columnCreationTime >= ? AND $columnCreationTime <= ? AND '
|
||||
'($columnOwnerID IS NULL OR $columnOwnerID = ?) AND ($columnMMdVisibility IS NULL OR $columnMMdVisibility = ?)'
|
||||
' AND ($columnLocalID IS NOT NULL OR ($columnCollectionID IS NOT NULL AND $columnCollectionID IS NOT -1))'
|
||||
: '$columnCreationTime >= ? AND $columnCreationTime <= ? AND ($columnOwnerID IS NULL OR $columnOwnerID = ?) AND ($columnMMdVisibility IS NULL OR $columnMMdVisibility = ?)'
|
||||
' AND ($columnLocalID IS NOT NULL OR ($columnCollectionID IS NOT NULL AND $columnCollectionID IS NOT -1))',
|
||||
whereArgs: [startTime, endTime, ownerID, visibilityVisible],
|
||||
orderBy:
|
||||
'$columnCreationTime ' + order + ', $columnModificationTime ' + order,
|
||||
|
@ -1376,6 +1406,33 @@ class FilesDB {
|
|||
return filesCount;
|
||||
}
|
||||
|
||||
Future<FileLoadResult> getAllUploadedAndSharedFiles(
|
||||
int startTime,
|
||||
int endTime, {
|
||||
int? limit,
|
||||
bool? asc,
|
||||
Set<int>? ignoredCollectionIDs,
|
||||
}) async {
|
||||
final db = await instance.database;
|
||||
final order = (asc ?? false ? 'ASC' : 'DESC');
|
||||
final results = await db.query(
|
||||
filesTable,
|
||||
where:
|
||||
'$columnLatitude IS NOT NULL AND $columnLongitude IS NOT NULL AND ($columnLatitude IS NOT 0 OR $columnLongitude IS NOT 0)'
|
||||
' AND $columnCreationTime >= ? AND $columnCreationTime <= ? AND '
|
||||
'($columnMMdVisibility IS NULL OR $columnMMdVisibility = ?)'
|
||||
' AND ($columnLocalID IS NOT NULL OR ($columnCollectionID IS NOT NULL AND $columnCollectionID IS NOT -1))',
|
||||
whereArgs: [startTime, endTime, visibilityVisible],
|
||||
orderBy:
|
||||
'$columnCreationTime ' + order + ', $columnModificationTime ' + order,
|
||||
limit: limit,
|
||||
);
|
||||
final files = convertToFiles(results);
|
||||
final List<File> deduplicatedFiles =
|
||||
_deduplicatedAndFilterIgnoredFiles(files, ignoredCollectionIDs);
|
||||
return FileLoadResult(deduplicatedFiles, files.length == limit);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _getRowForFile(File file) {
|
||||
final row = <String, dynamic>{};
|
||||
if (file.generatedID != null) {
|
||||
|
@ -1387,6 +1444,10 @@ class FilesDB {
|
|||
row[columnCollectionID] = file.collectionID ?? -1;
|
||||
row[columnTitle] = file.title;
|
||||
row[columnDeviceFolder] = file.deviceFolder;
|
||||
// if (file.location == null ||
|
||||
// (file.location!.latitude == null && file.location!.longitude == null)) {
|
||||
// file.location = Location.randomLocation();
|
||||
// }
|
||||
if (file.location != null) {
|
||||
row[columnLatitude] = file.location!.latitude;
|
||||
row[columnLongitude] = file.location!.longitude;
|
||||
|
@ -1471,7 +1532,10 @@ class FilesDB {
|
|||
file.title = row[columnTitle];
|
||||
file.deviceFolder = row[columnDeviceFolder];
|
||||
if (row[columnLatitude] != null && row[columnLongitude] != null) {
|
||||
file.location = Location(row[columnLatitude], row[columnLongitude]);
|
||||
file.location = Location(
|
||||
latitude: row[columnLatitude],
|
||||
longitude: row[columnLongitude],
|
||||
);
|
||||
}
|
||||
file.fileType = getFileType(row[columnFileType]);
|
||||
file.creationTime = row[columnCreationTime];
|
||||
|
|
16
lib/events/location_tag_updated_event.dart
Normal file
16
lib/events/location_tag_updated_event.dart
Normal file
|
@ -0,0 +1,16 @@
|
|||
import 'package:photos/events/event.dart';
|
||||
import "package:photos/models/local_entity_data.dart";
|
||||
import "package:photos/models/location_tag/location_tag.dart";
|
||||
|
||||
class LocationTagUpdatedEvent extends Event {
|
||||
final List<LocalEntity<LocationTag>>? updatedLocTagEntities;
|
||||
final LocTagEventType type;
|
||||
|
||||
LocationTagUpdatedEvent(this.type, {this.updatedLocTagEntities});
|
||||
}
|
||||
|
||||
enum LocTagEventType {
|
||||
add,
|
||||
update,
|
||||
delete,
|
||||
}
|
114
lib/gateways/entity_gw.dart
Normal file
114
lib/gateways/entity_gw.dart
Normal file
|
@ -0,0 +1,114 @@
|
|||
import "package:dio/dio.dart";
|
||||
import "package:photos/models/api/entity/data.dart";
|
||||
import "package:photos/models/api/entity/key.dart";
|
||||
import "package:photos/models/api/entity/type.dart";
|
||||
|
||||
class EntityGateway {
|
||||
final Dio _enteDio;
|
||||
|
||||
EntityGateway(this._enteDio);
|
||||
|
||||
Future<void> createKey(
|
||||
EntityType entityType,
|
||||
String encKey,
|
||||
String header,
|
||||
) async {
|
||||
await _enteDio.post(
|
||||
"/user-entity/key",
|
||||
data: {
|
||||
"type": entityType.typeToString(),
|
||||
"encryptedKey": encKey,
|
||||
"header": header,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<EntityKey> getKey(EntityType type) async {
|
||||
try {
|
||||
final response = await _enteDio.get(
|
||||
"/user-entity/key",
|
||||
queryParameters: {
|
||||
"type": type.typeToString(),
|
||||
},
|
||||
);
|
||||
return EntityKey.fromMap(response.data);
|
||||
} on DioError catch (e) {
|
||||
if (e.response != null && (e.response!.statusCode ?? 0) == 404) {
|
||||
throw EntityKeyNotFound();
|
||||
} else {
|
||||
rethrow;
|
||||
}
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<EntityData> createEntity(
|
||||
EntityType type,
|
||||
String encryptedData,
|
||||
String header,
|
||||
) async {
|
||||
final response = await _enteDio.post(
|
||||
"/user-entity/entity",
|
||||
data: {
|
||||
"encryptedData": encryptedData,
|
||||
"header": header,
|
||||
"type": type.typeToString(),
|
||||
},
|
||||
);
|
||||
return EntityData.fromMap(response.data);
|
||||
}
|
||||
|
||||
Future<EntityData> updateEntity(
|
||||
EntityType type,
|
||||
String id,
|
||||
String encryptedData,
|
||||
String header,
|
||||
) async {
|
||||
final response = await _enteDio.put(
|
||||
"/user-entity/entity",
|
||||
data: {
|
||||
"id": id,
|
||||
"encryptedData": encryptedData,
|
||||
"header": header,
|
||||
"type": type.typeToString(),
|
||||
},
|
||||
);
|
||||
return EntityData.fromMap(response.data);
|
||||
}
|
||||
|
||||
Future<void> deleteEntity(
|
||||
String id,
|
||||
) async {
|
||||
await _enteDio.delete(
|
||||
"/user-entity/entity",
|
||||
queryParameters: {
|
||||
"id": id,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<EntityData>> getDiff(
|
||||
EntityType type,
|
||||
int sinceTime, {
|
||||
int limit = 500,
|
||||
}) async {
|
||||
final response = await _enteDio.get(
|
||||
"/user-entity/entity/diff",
|
||||
queryParameters: {
|
||||
"sinceTime": sinceTime,
|
||||
"limit": limit,
|
||||
"type": type.typeToString(),
|
||||
},
|
||||
);
|
||||
final List<EntityData> authEntities = <EntityData>[];
|
||||
final diff = response.data["diff"] as List;
|
||||
for (var entry in diff) {
|
||||
final EntityData entity = EntityData.fromMap(entry);
|
||||
authEntities.add(entity);
|
||||
}
|
||||
return authEntities;
|
||||
}
|
||||
}
|
||||
|
||||
class EntityKeyNotFound extends Error {}
|
|
@ -21,10 +21,12 @@ import "package:photos/l10n/l10n.dart";
|
|||
import 'package:photos/services/app_lifecycle_service.dart';
|
||||
import 'package:photos/services/billing_service.dart';
|
||||
import 'package:photos/services/collections_service.dart';
|
||||
import "package:photos/services/entity_service.dart";
|
||||
import 'package:photos/services/favorites_service.dart';
|
||||
import 'package:photos/services/feature_flag_service.dart';
|
||||
import 'package:photos/services/local_file_update_service.dart';
|
||||
import 'package:photos/services/local_sync_service.dart';
|
||||
import "package:photos/services/location_service.dart";
|
||||
import 'package:photos/services/memories_service.dart';
|
||||
import 'package:photos/services/notification_service.dart';
|
||||
import "package:photos/services/object_detection/object_detection_service.dart";
|
||||
|
@ -156,6 +158,9 @@ Future<void> _init(bool isBackground, {String via = ''}) async {
|
|||
await NetworkClient.instance.init();
|
||||
await Configuration.instance.init();
|
||||
await UserService.instance.init();
|
||||
await EntityService.instance.init();
|
||||
LocationService.instance.init(preferences);
|
||||
|
||||
await UserRemoteFlagService.instance.init();
|
||||
await UpdateService.instance.init();
|
||||
BillingService.instance.init();
|
||||
|
|
55
lib/models/api/entity/data.dart
Normal file
55
lib/models/api/entity/data.dart
Normal file
|
@ -0,0 +1,55 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@immutable
|
||||
class EntityData {
|
||||
final String id;
|
||||
|
||||
// encryptedData will be null for diff items when item is deleted
|
||||
final String? encryptedData;
|
||||
final String? header;
|
||||
final bool isDeleted;
|
||||
final int createdAt;
|
||||
final int updatedAt;
|
||||
final int userID;
|
||||
|
||||
const EntityData(
|
||||
this.id,
|
||||
this.userID,
|
||||
this.encryptedData,
|
||||
this.header,
|
||||
this.isDeleted,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
);
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'id': id,
|
||||
'userID': userID,
|
||||
'encryptedData': encryptedData,
|
||||
'header': header,
|
||||
'isDeleted': isDeleted,
|
||||
'createdAt': createdAt,
|
||||
'updatedAt': updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
factory EntityData.fromMap(Map<String, dynamic> map) {
|
||||
return EntityData(
|
||||
map['id'],
|
||||
map['userID'],
|
||||
map['encryptedData'],
|
||||
map['header'],
|
||||
map['isDeleted']!,
|
||||
map['createdAt']!,
|
||||
map['updatedAt']!,
|
||||
);
|
||||
}
|
||||
|
||||
String toJson() => json.encode(toMap());
|
||||
|
||||
factory EntityData.fromJson(String source) =>
|
||||
EntityData.fromMap(json.decode(source));
|
||||
}
|
46
lib/models/api/entity/key.dart
Normal file
46
lib/models/api/entity/key.dart
Normal file
|
@ -0,0 +1,46 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import "package:photos/models/api/entity/type.dart";
|
||||
|
||||
@immutable
|
||||
class EntityKey {
|
||||
final int userID;
|
||||
final String encryptedKey;
|
||||
final EntityType type;
|
||||
final String header;
|
||||
final int createdAt;
|
||||
|
||||
const EntityKey(
|
||||
this.userID,
|
||||
this.encryptedKey,
|
||||
this.header,
|
||||
this.createdAt,
|
||||
this.type,
|
||||
);
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'userID': userID,
|
||||
'type': type.typeToString(),
|
||||
'encryptedKey': encryptedKey,
|
||||
'header': header,
|
||||
'createdAt': createdAt,
|
||||
};
|
||||
}
|
||||
|
||||
factory EntityKey.fromMap(Map<String, dynamic> map) {
|
||||
return EntityKey(
|
||||
map['userID']?.toInt() ?? 0,
|
||||
map['encryptedKey']!,
|
||||
map['header']!,
|
||||
map['createdAt']?.toInt() ?? 0,
|
||||
typeFromString(map['type']!),
|
||||
);
|
||||
}
|
||||
|
||||
String toJson() => json.encode(toMap());
|
||||
|
||||
factory EntityKey.fromJson(String source) =>
|
||||
EntityKey.fromMap(json.decode(source));
|
||||
}
|
26
lib/models/api/entity/type.dart
Normal file
26
lib/models/api/entity/type.dart
Normal file
|
@ -0,0 +1,26 @@
|
|||
import "package:flutter/foundation.dart";
|
||||
|
||||
enum EntityType {
|
||||
location,
|
||||
unknown,
|
||||
}
|
||||
|
||||
EntityType typeFromString(String type) {
|
||||
switch (type) {
|
||||
case "location":
|
||||
return EntityType.location;
|
||||
}
|
||||
debugPrint("unexpected collection type $type");
|
||||
return EntityType.unknown;
|
||||
}
|
||||
|
||||
extension EntityTypeExtn on EntityType {
|
||||
String typeToString() {
|
||||
switch (this) {
|
||||
case EntityType.location:
|
||||
return "location";
|
||||
case EntityType.unknown:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,7 +8,7 @@ import 'package:photos/core/configuration.dart';
|
|||
import 'package:photos/core/constants.dart';
|
||||
import 'package:photos/models/ente_file.dart';
|
||||
import 'package:photos/models/file_type.dart';
|
||||
import 'package:photos/models/location.dart';
|
||||
import 'package:photos/models/location/location.dart';
|
||||
import 'package:photos/models/magic_metadata.dart';
|
||||
import 'package:photos/services/feature_flag_service.dart';
|
||||
import 'package:photos/utils/date_time_util.dart';
|
||||
|
@ -72,7 +72,8 @@ class File extends EnteFile {
|
|||
file.localID = asset.id;
|
||||
file.title = asset.title;
|
||||
file.deviceFolder = pathName;
|
||||
file.location = Location(asset.latitude, asset.longitude);
|
||||
file.location =
|
||||
Location(latitude: asset.latitude, longitude: asset.longitude);
|
||||
file.fileType = _fileTypeFromAsset(asset);
|
||||
file.creationTime = parseFileCreationTime(file.title, asset);
|
||||
file.modificationTime = asset.modifiedDateTime.microsecondsSinceEpoch;
|
||||
|
@ -147,7 +148,7 @@ class File extends EnteFile {
|
|||
if (latitude == null || longitude == null) {
|
||||
location = null;
|
||||
} else {
|
||||
location = Location(latitude, longitude);
|
||||
location = Location(latitude: latitude, longitude: longitude);
|
||||
}
|
||||
fileType = getFileType(metadata["fileType"] ?? -1);
|
||||
fileSubType = metadata["subType"] ?? -1;
|
||||
|
|
|
@ -9,7 +9,8 @@ enum GalleryType {
|
|||
// indicator for gallery view of collections shared with the user
|
||||
sharedCollection,
|
||||
ownedCollection,
|
||||
searchResults
|
||||
searchResults,
|
||||
locationTag,
|
||||
}
|
||||
|
||||
extension GalleyTypeExtension on GalleryType {
|
||||
|
@ -21,6 +22,7 @@ extension GalleyTypeExtension on GalleryType {
|
|||
case GalleryType.ownedCollection:
|
||||
case GalleryType.searchResults:
|
||||
case GalleryType.favorite:
|
||||
case GalleryType.locationTag:
|
||||
return true;
|
||||
|
||||
case GalleryType.hidden:
|
||||
|
@ -45,6 +47,7 @@ extension GalleyTypeExtension on GalleryType {
|
|||
case GalleryType.homepage:
|
||||
case GalleryType.trash:
|
||||
case GalleryType.sharedCollection:
|
||||
case GalleryType.locationTag:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -59,6 +62,7 @@ extension GalleyTypeExtension on GalleryType {
|
|||
case GalleryType.favorite:
|
||||
case GalleryType.localFolder:
|
||||
case GalleryType.uncategorized:
|
||||
case GalleryType.locationTag:
|
||||
return true;
|
||||
case GalleryType.trash:
|
||||
case GalleryType.archive:
|
||||
|
@ -78,6 +82,7 @@ extension GalleyTypeExtension on GalleryType {
|
|||
case GalleryType.archive:
|
||||
case GalleryType.hidden:
|
||||
case GalleryType.localFolder:
|
||||
case GalleryType.locationTag:
|
||||
return true;
|
||||
case GalleryType.trash:
|
||||
case GalleryType.sharedCollection:
|
||||
|
@ -93,6 +98,7 @@ extension GalleyTypeExtension on GalleryType {
|
|||
case GalleryType.favorite:
|
||||
case GalleryType.archive:
|
||||
case GalleryType.uncategorized:
|
||||
case GalleryType.locationTag:
|
||||
return true;
|
||||
case GalleryType.hidden:
|
||||
case GalleryType.localFolder:
|
||||
|
@ -115,6 +121,7 @@ extension GalleyTypeExtension on GalleryType {
|
|||
case GalleryType.archive:
|
||||
case GalleryType.localFolder:
|
||||
case GalleryType.trash:
|
||||
case GalleryType.locationTag:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -133,6 +140,7 @@ extension GalleyTypeExtension on GalleryType {
|
|||
case GalleryType.localFolder:
|
||||
case GalleryType.trash:
|
||||
case GalleryType.sharedCollection:
|
||||
case GalleryType.locationTag:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -148,6 +156,7 @@ extension GalleyTypeExtension on GalleryType {
|
|||
case GalleryType.searchResults:
|
||||
case GalleryType.archive:
|
||||
case GalleryType.uncategorized:
|
||||
case GalleryType.locationTag:
|
||||
return true;
|
||||
|
||||
case GalleryType.hidden:
|
||||
|
@ -169,6 +178,7 @@ extension GalleyTypeExtension on GalleryType {
|
|||
case GalleryType.homepage:
|
||||
case GalleryType.searchResults:
|
||||
case GalleryType.uncategorized:
|
||||
case GalleryType.locationTag:
|
||||
return true;
|
||||
|
||||
case GalleryType.hidden:
|
||||
|
|
48
lib/models/local_entity_data.dart
Normal file
48
lib/models/local_entity_data.dart
Normal file
|
@ -0,0 +1,48 @@
|
|||
import "package:equatable/equatable.dart";
|
||||
import "package:photos/models/api/entity/type.dart";
|
||||
|
||||
class LocalEntityData {
|
||||
final String id;
|
||||
final EntityType type;
|
||||
final String data;
|
||||
final int ownerID;
|
||||
final int updatedAt;
|
||||
|
||||
LocalEntityData({
|
||||
required this.id,
|
||||
required this.type,
|
||||
required this.data,
|
||||
required this.ownerID,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
"id": id,
|
||||
"type": type.typeToString(),
|
||||
"data": data,
|
||||
"ownerID": ownerID,
|
||||
"updatedAt": updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
factory LocalEntityData.fromJson(Map<String, dynamic> json) {
|
||||
return LocalEntityData(
|
||||
id: json["id"],
|
||||
type: typeFromString(json["type"]),
|
||||
data: json["data"],
|
||||
ownerID: json["ownerID"] as int,
|
||||
updatedAt: json["updatedAt"] as int,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LocalEntity<T> extends Equatable {
|
||||
final T item;
|
||||
final String id;
|
||||
|
||||
const LocalEntity(this.item, this.id);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [item, id];
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
class Location {
|
||||
final double? latitude;
|
||||
final double? longitude;
|
||||
|
||||
Location(this.latitude, this.longitude);
|
||||
|
||||
@override
|
||||
String toString() => 'Location(latitude: $latitude, longitude: $longitude)';
|
||||
}
|
16
lib/models/location/location.dart
Normal file
16
lib/models/location/location.dart
Normal file
|
@ -0,0 +1,16 @@
|
|||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'location.freezed.dart';
|
||||
|
||||
part 'location.g.dart';
|
||||
|
||||
@freezed
|
||||
class Location with _$Location {
|
||||
const factory Location({
|
||||
required double? latitude,
|
||||
required double? longitude,
|
||||
}) = _Location;
|
||||
|
||||
factory Location.fromJson(Map<String, Object?> json) =>
|
||||
_$LocationFromJson(json);
|
||||
}
|
168
lib/models/location/location.freezed.dart
Normal file
168
lib/models/location/location.freezed.dart
Normal file
|
@ -0,0 +1,168 @@
|
|||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'location.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
|
||||
|
||||
Location _$LocationFromJson(Map<String, dynamic> json) {
|
||||
return _Location.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$Location {
|
||||
double? get latitude => throw _privateConstructorUsedError;
|
||||
double? get longitude => throw _privateConstructorUsedError;
|
||||
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@JsonKey(ignore: true)
|
||||
$LocationCopyWith<Location> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $LocationCopyWith<$Res> {
|
||||
factory $LocationCopyWith(Location value, $Res Function(Location) then) =
|
||||
_$LocationCopyWithImpl<$Res, Location>;
|
||||
@useResult
|
||||
$Res call({double? latitude, double? longitude});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$LocationCopyWithImpl<$Res, $Val extends Location>
|
||||
implements $LocationCopyWith<$Res> {
|
||||
_$LocationCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? latitude = freezed,
|
||||
Object? longitude = freezed,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
latitude: freezed == latitude
|
||||
? _value.latitude
|
||||
: latitude // ignore: cast_nullable_to_non_nullable
|
||||
as double?,
|
||||
longitude: freezed == longitude
|
||||
? _value.longitude
|
||||
: longitude // ignore: cast_nullable_to_non_nullable
|
||||
as double?,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$_LocationCopyWith<$Res> implements $LocationCopyWith<$Res> {
|
||||
factory _$$_LocationCopyWith(
|
||||
_$_Location value, $Res Function(_$_Location) then) =
|
||||
__$$_LocationCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({double? latitude, double? longitude});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$_LocationCopyWithImpl<$Res>
|
||||
extends _$LocationCopyWithImpl<$Res, _$_Location>
|
||||
implements _$$_LocationCopyWith<$Res> {
|
||||
__$$_LocationCopyWithImpl(
|
||||
_$_Location _value, $Res Function(_$_Location) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? latitude = freezed,
|
||||
Object? longitude = freezed,
|
||||
}) {
|
||||
return _then(_$_Location(
|
||||
latitude: freezed == latitude
|
||||
? _value.latitude
|
||||
: latitude // ignore: cast_nullable_to_non_nullable
|
||||
as double?,
|
||||
longitude: freezed == longitude
|
||||
? _value.longitude
|
||||
: longitude // ignore: cast_nullable_to_non_nullable
|
||||
as double?,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$_Location implements _Location {
|
||||
const _$_Location({required this.latitude, required this.longitude});
|
||||
|
||||
factory _$_Location.fromJson(Map<String, dynamic> json) =>
|
||||
_$$_LocationFromJson(json);
|
||||
|
||||
@override
|
||||
final double? latitude;
|
||||
@override
|
||||
final double? longitude;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Location(latitude: $latitude, longitude: $longitude)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$_Location &&
|
||||
(identical(other.latitude, latitude) ||
|
||||
other.latitude == latitude) &&
|
||||
(identical(other.longitude, longitude) ||
|
||||
other.longitude == longitude));
|
||||
}
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, latitude, longitude);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$_LocationCopyWith<_$_Location> get copyWith =>
|
||||
__$$_LocationCopyWithImpl<_$_Location>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$_LocationToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _Location implements Location {
|
||||
const factory _Location(
|
||||
{required final double? latitude,
|
||||
required final double? longitude}) = _$_Location;
|
||||
|
||||
factory _Location.fromJson(Map<String, dynamic> json) = _$_Location.fromJson;
|
||||
|
||||
@override
|
||||
double? get latitude;
|
||||
@override
|
||||
double? get longitude;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$_LocationCopyWith<_$_Location> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
18
lib/models/location/location.g.dart
Normal file
18
lib/models/location/location.g.dart
Normal file
|
@ -0,0 +1,18 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'location.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$_Location _$$_LocationFromJson(Map<String, dynamic> json) => _$_Location(
|
||||
latitude: (json['latitude'] as num?)?.toDouble(),
|
||||
longitude: (json['longitude'] as num?)?.toDouble(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$_LocationToJson(_$_Location instance) =>
|
||||
<String, dynamic>{
|
||||
'latitude': instance.latitude,
|
||||
'longitude': instance.longitude,
|
||||
};
|
25
lib/models/location_tag/location_tag.dart
Normal file
25
lib/models/location_tag/location_tag.dart
Normal file
|
@ -0,0 +1,25 @@
|
|||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import "package:photos/core/constants.dart";
|
||||
import 'package:photos/models/location/location.dart';
|
||||
|
||||
part 'location_tag.freezed.dart';
|
||||
part 'location_tag.g.dart';
|
||||
|
||||
@freezed
|
||||
class LocationTag with _$LocationTag {
|
||||
const LocationTag._();
|
||||
const factory LocationTag({
|
||||
required String name,
|
||||
required int radius,
|
||||
required double aSquare,
|
||||
required double bSquare,
|
||||
required Location centerPoint,
|
||||
}) = _LocationTag;
|
||||
|
||||
factory LocationTag.fromJson(Map<String, Object?> json) =>
|
||||
_$LocationTagFromJson(json);
|
||||
|
||||
int get radiusIndex {
|
||||
return radiusValues.indexOf(radius);
|
||||
}
|
||||
}
|
252
lib/models/location_tag/location_tag.freezed.dart
Normal file
252
lib/models/location_tag/location_tag.freezed.dart
Normal file
|
@ -0,0 +1,252 @@
|
|||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'location_tag.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
|
||||
|
||||
LocationTag _$LocationTagFromJson(Map<String, dynamic> json) {
|
||||
return _LocationTag.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$LocationTag {
|
||||
String get name => throw _privateConstructorUsedError;
|
||||
int get radius => throw _privateConstructorUsedError;
|
||||
double get aSquare => throw _privateConstructorUsedError;
|
||||
double get bSquare => throw _privateConstructorUsedError;
|
||||
Location get centerPoint => throw _privateConstructorUsedError;
|
||||
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@JsonKey(ignore: true)
|
||||
$LocationTagCopyWith<LocationTag> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $LocationTagCopyWith<$Res> {
|
||||
factory $LocationTagCopyWith(
|
||||
LocationTag value, $Res Function(LocationTag) then) =
|
||||
_$LocationTagCopyWithImpl<$Res, LocationTag>;
|
||||
@useResult
|
||||
$Res call(
|
||||
{String name,
|
||||
int radius,
|
||||
double aSquare,
|
||||
double bSquare,
|
||||
Location centerPoint});
|
||||
|
||||
$LocationCopyWith<$Res> get centerPoint;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$LocationTagCopyWithImpl<$Res, $Val extends LocationTag>
|
||||
implements $LocationTagCopyWith<$Res> {
|
||||
_$LocationTagCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? name = null,
|
||||
Object? radius = null,
|
||||
Object? aSquare = null,
|
||||
Object? bSquare = null,
|
||||
Object? centerPoint = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
name: null == name
|
||||
? _value.name
|
||||
: name // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
radius: null == radius
|
||||
? _value.radius
|
||||
: radius // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
aSquare: null == aSquare
|
||||
? _value.aSquare
|
||||
: aSquare // ignore: cast_nullable_to_non_nullable
|
||||
as double,
|
||||
bSquare: null == bSquare
|
||||
? _value.bSquare
|
||||
: bSquare // ignore: cast_nullable_to_non_nullable
|
||||
as double,
|
||||
centerPoint: null == centerPoint
|
||||
? _value.centerPoint
|
||||
: centerPoint // ignore: cast_nullable_to_non_nullable
|
||||
as Location,
|
||||
) as $Val);
|
||||
}
|
||||
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$LocationCopyWith<$Res> get centerPoint {
|
||||
return $LocationCopyWith<$Res>(_value.centerPoint, (value) {
|
||||
return _then(_value.copyWith(centerPoint: value) as $Val);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$_LocationTagCopyWith<$Res>
|
||||
implements $LocationTagCopyWith<$Res> {
|
||||
factory _$$_LocationTagCopyWith(
|
||||
_$_LocationTag value, $Res Function(_$_LocationTag) then) =
|
||||
__$$_LocationTagCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{String name,
|
||||
int radius,
|
||||
double aSquare,
|
||||
double bSquare,
|
||||
Location centerPoint});
|
||||
|
||||
@override
|
||||
$LocationCopyWith<$Res> get centerPoint;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$_LocationTagCopyWithImpl<$Res>
|
||||
extends _$LocationTagCopyWithImpl<$Res, _$_LocationTag>
|
||||
implements _$$_LocationTagCopyWith<$Res> {
|
||||
__$$_LocationTagCopyWithImpl(
|
||||
_$_LocationTag _value, $Res Function(_$_LocationTag) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? name = null,
|
||||
Object? radius = null,
|
||||
Object? aSquare = null,
|
||||
Object? bSquare = null,
|
||||
Object? centerPoint = null,
|
||||
}) {
|
||||
return _then(_$_LocationTag(
|
||||
name: null == name
|
||||
? _value.name
|
||||
: name // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
radius: null == radius
|
||||
? _value.radius
|
||||
: radius // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
aSquare: null == aSquare
|
||||
? _value.aSquare
|
||||
: aSquare // ignore: cast_nullable_to_non_nullable
|
||||
as double,
|
||||
bSquare: null == bSquare
|
||||
? _value.bSquare
|
||||
: bSquare // ignore: cast_nullable_to_non_nullable
|
||||
as double,
|
||||
centerPoint: null == centerPoint
|
||||
? _value.centerPoint
|
||||
: centerPoint // ignore: cast_nullable_to_non_nullable
|
||||
as Location,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$_LocationTag extends _LocationTag {
|
||||
const _$_LocationTag(
|
||||
{required this.name,
|
||||
required this.radius,
|
||||
required this.aSquare,
|
||||
required this.bSquare,
|
||||
required this.centerPoint})
|
||||
: super._();
|
||||
|
||||
factory _$_LocationTag.fromJson(Map<String, dynamic> json) =>
|
||||
_$$_LocationTagFromJson(json);
|
||||
|
||||
@override
|
||||
final String name;
|
||||
@override
|
||||
final int radius;
|
||||
@override
|
||||
final double aSquare;
|
||||
@override
|
||||
final double bSquare;
|
||||
@override
|
||||
final Location centerPoint;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'LocationTag(name: $name, radius: $radius, aSquare: $aSquare, bSquare: $bSquare, centerPoint: $centerPoint)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$_LocationTag &&
|
||||
(identical(other.name, name) || other.name == name) &&
|
||||
(identical(other.radius, radius) || other.radius == radius) &&
|
||||
(identical(other.aSquare, aSquare) || other.aSquare == aSquare) &&
|
||||
(identical(other.bSquare, bSquare) || other.bSquare == bSquare) &&
|
||||
(identical(other.centerPoint, centerPoint) ||
|
||||
other.centerPoint == centerPoint));
|
||||
}
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
int get hashCode =>
|
||||
Object.hash(runtimeType, name, radius, aSquare, bSquare, centerPoint);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$_LocationTagCopyWith<_$_LocationTag> get copyWith =>
|
||||
__$$_LocationTagCopyWithImpl<_$_LocationTag>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$_LocationTagToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _LocationTag extends LocationTag {
|
||||
const factory _LocationTag(
|
||||
{required final String name,
|
||||
required final int radius,
|
||||
required final double aSquare,
|
||||
required final double bSquare,
|
||||
required final Location centerPoint}) = _$_LocationTag;
|
||||
const _LocationTag._() : super._();
|
||||
|
||||
factory _LocationTag.fromJson(Map<String, dynamic> json) =
|
||||
_$_LocationTag.fromJson;
|
||||
|
||||
@override
|
||||
String get name;
|
||||
@override
|
||||
int get radius;
|
||||
@override
|
||||
double get aSquare;
|
||||
@override
|
||||
double get bSquare;
|
||||
@override
|
||||
Location get centerPoint;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$_LocationTagCopyWith<_$_LocationTag> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
26
lib/models/location_tag/location_tag.g.dart
Normal file
26
lib/models/location_tag/location_tag.g.dart
Normal file
|
@ -0,0 +1,26 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'location_tag.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$_LocationTag _$$_LocationTagFromJson(Map<String, dynamic> json) =>
|
||||
_$_LocationTag(
|
||||
name: json['name'] as String,
|
||||
radius: json['radius'] as int,
|
||||
aSquare: (json['aSquare'] as num).toDouble(),
|
||||
bSquare: (json['bSquare'] as num).toDouble(),
|
||||
centerPoint:
|
||||
Location.fromJson(json['centerPoint'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$_LocationTagToJson(_$_LocationTag instance) =>
|
||||
<String, dynamic>{
|
||||
'name': instance.name,
|
||||
'radius': instance.radius,
|
||||
'aSquare': instance.aSquare,
|
||||
'bSquare': instance.bSquare,
|
||||
'centerPoint': instance.centerPoint,
|
||||
};
|
|
@ -23,5 +23,5 @@ enum ResultType {
|
|||
fileType,
|
||||
fileExtension,
|
||||
fileCaption,
|
||||
event
|
||||
event,
|
||||
}
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
import 'dart:async';
|
||||
|
||||
import "package:photos/models/location/location.dart";
|
||||
|
||||
typedef FutureVoidCallback = Future<void> Function();
|
||||
typedef BoolCallBack = bool Function();
|
||||
typedef FutureVoidCallbackParamStr = Future<void> Function(String);
|
||||
typedef VoidCallbackParamStr = void Function(String);
|
||||
typedef FutureOrVoidCallback = FutureOr<void> Function();
|
||||
typedef VoidCallbackParamInt = void Function(int);
|
||||
typedef VoidCallbackParamLocation = void Function(Location);
|
||||
|
|
192
lib/services/entity_service.dart
Normal file
192
lib/services/entity_service.dart
Normal file
|
@ -0,0 +1,192 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_sodium/flutter_sodium.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import "package:photos/core/configuration.dart";
|
||||
import "package:photos/core/network/network.dart";
|
||||
import "package:photos/db/entities_db.dart";
|
||||
import "package:photos/db/files_db.dart";
|
||||
import "package:photos/gateways/entity_gw.dart";
|
||||
import "package:photos/models/api/entity/data.dart";
|
||||
import "package:photos/models/api/entity/key.dart";
|
||||
import "package:photos/models/api/entity/type.dart";
|
||||
import "package:photos/models/local_entity_data.dart";
|
||||
import "package:photos/utils/crypto_util.dart";
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class EntityService {
|
||||
static const int fetchLimit = 500;
|
||||
final _logger = Logger((EntityService).toString());
|
||||
final _config = Configuration.instance;
|
||||
late SharedPreferences _prefs;
|
||||
late EntityGateway _gateway;
|
||||
late FilesDB _db;
|
||||
|
||||
EntityService._privateConstructor();
|
||||
|
||||
static final EntityService instance = EntityService._privateConstructor();
|
||||
|
||||
Future<void> init() async {
|
||||
_prefs = await SharedPreferences.getInstance();
|
||||
_db = FilesDB.instance;
|
||||
_gateway = EntityGateway(NetworkClient.instance.enteDio);
|
||||
}
|
||||
|
||||
String _getEntityKeyPrefix(EntityType type) {
|
||||
return "entity_key_" + type.typeToString();
|
||||
}
|
||||
|
||||
String _getEntityHeaderPrefix(EntityType type) {
|
||||
return "entity_key_header_" + type.typeToString();
|
||||
}
|
||||
|
||||
String _getEntityLastSyncTimePrefix(EntityType type) {
|
||||
return "entity_last_sync_time_" + type.typeToString();
|
||||
}
|
||||
|
||||
Future<List<LocalEntityData>> getEntities(EntityType type) async {
|
||||
return await _db.getEntities(type);
|
||||
}
|
||||
|
||||
Future<LocalEntityData> addOrUpdate(
|
||||
EntityType type,
|
||||
String plainText, {
|
||||
String? id,
|
||||
}) async {
|
||||
final key = await getOrCreateEntityKey(type);
|
||||
final encryptedKeyData = await CryptoUtil.encryptChaCha(
|
||||
utf8.encode(plainText) as Uint8List,
|
||||
key,
|
||||
);
|
||||
final String encryptedData =
|
||||
Sodium.bin2base64(encryptedKeyData.encryptedData!);
|
||||
final String header = Sodium.bin2base64(encryptedKeyData.header!);
|
||||
debugPrint("Adding entity of type: " + type.typeToString());
|
||||
final EntityData data = id == null
|
||||
? await _gateway.createEntity(type, encryptedData, header)
|
||||
: await _gateway.updateEntity(type, id, encryptedData, header);
|
||||
final LocalEntityData localData = LocalEntityData(
|
||||
id: data.id,
|
||||
type: type,
|
||||
data: plainText,
|
||||
ownerID: data.userID,
|
||||
updatedAt: data.updatedAt,
|
||||
);
|
||||
await _db.upsertEntities([localData]);
|
||||
syncEntities().ignore();
|
||||
return localData;
|
||||
}
|
||||
|
||||
Future<void> deleteEntry(String id) async {
|
||||
await _gateway.deleteEntity(id);
|
||||
await _db.deleteEntities([id]);
|
||||
}
|
||||
|
||||
Future<void> syncEntities() async {
|
||||
try {
|
||||
await _remoteToLocalSync(EntityType.location);
|
||||
} catch (e) {
|
||||
_logger.severe("Failed to sync entities", e);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _remoteToLocalSync(EntityType type) async {
|
||||
final int lastSyncTime =
|
||||
_prefs.getInt(_getEntityLastSyncTimePrefix(type)) ?? 0;
|
||||
final List<EntityData> result = await _gateway.getDiff(
|
||||
type,
|
||||
lastSyncTime,
|
||||
limit: fetchLimit,
|
||||
);
|
||||
if (result.isEmpty) {
|
||||
debugPrint("No $type entries to sync");
|
||||
return;
|
||||
}
|
||||
final bool hasMoreItems = result.length == fetchLimit;
|
||||
_logger.info("${result.length} entries of type $type fetched");
|
||||
final maxSyncTime = result.map((e) => e.updatedAt).reduce(max);
|
||||
final List<String> deletedIDs =
|
||||
result.where((element) => element.isDeleted).map((e) => e.id).toList();
|
||||
if (deletedIDs.isNotEmpty) {
|
||||
_logger.info("${deletedIDs.length} entries of type $type deleted");
|
||||
await _db.deleteEntities(deletedIDs);
|
||||
}
|
||||
result.removeWhere((element) => element.isDeleted);
|
||||
if (result.isNotEmpty) {
|
||||
final entityKey = await getOrCreateEntityKey(type);
|
||||
final List<LocalEntityData> entities = [];
|
||||
for (EntityData e in result) {
|
||||
try {
|
||||
final decryptedValue = await CryptoUtil.decryptChaCha(
|
||||
Sodium.base642bin(e.encryptedData!),
|
||||
entityKey,
|
||||
Sodium.base642bin(e.header!),
|
||||
);
|
||||
final String plainText = utf8.decode(decryptedValue);
|
||||
entities.add(
|
||||
LocalEntityData(
|
||||
id: e.id,
|
||||
type: type,
|
||||
data: plainText,
|
||||
ownerID: e.userID,
|
||||
updatedAt: e.updatedAt,
|
||||
),
|
||||
);
|
||||
} catch (e, s) {
|
||||
_logger.severe("Failed to decrypted data for key $type", e, s);
|
||||
}
|
||||
}
|
||||
if (entities.isNotEmpty) {
|
||||
await _db.upsertEntities(entities);
|
||||
}
|
||||
}
|
||||
_prefs.setInt(_getEntityLastSyncTimePrefix(type), maxSyncTime);
|
||||
if (hasMoreItems) {
|
||||
_logger.info("Diff limit reached, pulling again");
|
||||
await _remoteToLocalSync(type);
|
||||
}
|
||||
}
|
||||
|
||||
Future<Uint8List> getOrCreateEntityKey(EntityType type) async {
|
||||
late String encryptedKey;
|
||||
late String header;
|
||||
try {
|
||||
if (_prefs.containsKey(_getEntityKeyPrefix(type)) &&
|
||||
_prefs.containsKey(_getEntityHeaderPrefix(type))) {
|
||||
encryptedKey = _prefs.getString(_getEntityKeyPrefix(type))!;
|
||||
header = _prefs.getString(_getEntityHeaderPrefix(type))!;
|
||||
} else {
|
||||
final EntityKey response = await _gateway.getKey(type);
|
||||
encryptedKey = response.encryptedKey;
|
||||
header = response.header;
|
||||
_prefs.setString(_getEntityKeyPrefix(type), encryptedKey);
|
||||
_prefs.setString(_getEntityHeaderPrefix(type), header);
|
||||
}
|
||||
final entityKey = CryptoUtil.decryptSync(
|
||||
Sodium.base642bin(encryptedKey),
|
||||
_config.getKey()!,
|
||||
Sodium.base642bin(header),
|
||||
);
|
||||
return entityKey;
|
||||
} on EntityKeyNotFound catch (e) {
|
||||
_logger.info(
|
||||
"EntityKeyNotFound generating key for type $type ${e.stackTrace}");
|
||||
final key = CryptoUtil.generateKey();
|
||||
final encryptedKeyData = CryptoUtil.encryptSync(key, _config.getKey()!);
|
||||
await _gateway.createKey(
|
||||
type,
|
||||
Sodium.bin2base64(encryptedKeyData.encryptedData!),
|
||||
Sodium.bin2base64(encryptedKeyData.nonce!),
|
||||
);
|
||||
_prefs.setString(_getEntityKeyPrefix(type), encryptedKey);
|
||||
_prefs.setString(_getEntityHeaderPrefix(type), header);
|
||||
return key;
|
||||
} catch (e, s) {
|
||||
_logger.severe("Failed to getOrCreateKey for type $type", e, s);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,8 +6,10 @@ import 'package:photos/core/network/network.dart';
|
|||
import 'package:photos/db/files_db.dart';
|
||||
import 'package:photos/extensions/list.dart';
|
||||
import 'package:photos/models/file.dart';
|
||||
import "package:photos/models/file_load_result.dart";
|
||||
import 'package:photos/models/magic_metadata.dart';
|
||||
import 'package:photos/services/file_magic_service.dart';
|
||||
import "package:photos/services/ignored_files_service.dart";
|
||||
import 'package:photos/utils/date_time_util.dart';
|
||||
|
||||
class FilesService {
|
||||
|
@ -94,6 +96,15 @@ class FilesService {
|
|||
);
|
||||
return timeResult?.microsecondsSinceEpoch;
|
||||
}
|
||||
|
||||
Future<void> removeIgnoredFiles(Future<FileLoadResult> result) async {
|
||||
final ignoredIDs = await IgnoredFilesService.instance.ignoredIDs;
|
||||
(await result).files.removeWhere(
|
||||
(f) =>
|
||||
f.uploadedFileID == null &&
|
||||
IgnoredFilesService.instance.shouldSkipUpload(ignoredIDs, f),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum EditTimeSource {
|
||||
|
|
212
lib/services/location_service.dart
Normal file
212
lib/services/location_service.dart
Normal file
|
@ -0,0 +1,212 @@
|
|||
import "dart:convert";
|
||||
import "dart:math";
|
||||
|
||||
import "package:logging/logging.dart";
|
||||
import "package:photos/core/constants.dart";
|
||||
import "package:photos/core/event_bus.dart";
|
||||
import "package:photos/events/location_tag_updated_event.dart";
|
||||
import "package:photos/models/api/entity/type.dart";
|
||||
import "package:photos/models/local_entity_data.dart";
|
||||
import "package:photos/models/location/location.dart";
|
||||
import 'package:photos/models/location_tag/location_tag.dart';
|
||||
import "package:photos/services/entity_service.dart";
|
||||
import "package:shared_preferences/shared_preferences.dart";
|
||||
|
||||
class LocationService {
|
||||
late SharedPreferences prefs;
|
||||
final Logger _logger = Logger((LocationService).toString());
|
||||
|
||||
LocationService._privateConstructor();
|
||||
|
||||
static final LocationService instance = LocationService._privateConstructor();
|
||||
|
||||
void init(SharedPreferences preferences) {
|
||||
prefs = preferences;
|
||||
}
|
||||
|
||||
Future<Iterable<LocalEntity<LocationTag>>> _getStoredLocationTags() async {
|
||||
final data = await EntityService.instance.getEntities(EntityType.location);
|
||||
return data.map(
|
||||
(e) => LocalEntity(LocationTag.fromJson(json.decode(e.data)), e.id),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Iterable<LocalEntity<LocationTag>>> getLocationTags() {
|
||||
return _getStoredLocationTags();
|
||||
}
|
||||
|
||||
Future<void> addLocation(
|
||||
String location,
|
||||
Location centerPoint,
|
||||
int radius,
|
||||
) async {
|
||||
//The area enclosed by the location tag will be a circle on a 3D spherical
|
||||
//globe and an ellipse on a 2D Mercator projection (2D map)
|
||||
//a & b are the semi-major and semi-minor axes of the ellipse
|
||||
//Converting the unit from kilometers to degrees for a and b as that is
|
||||
//the unit on the caritesian plane
|
||||
|
||||
final a =
|
||||
(radius * _scaleFactor(centerPoint.latitude!)) / kilometersPerDegree;
|
||||
final b = radius / kilometersPerDegree;
|
||||
final locationTag = LocationTag(
|
||||
name: location,
|
||||
radius: radius,
|
||||
aSquare: a * a,
|
||||
bSquare: b * b,
|
||||
centerPoint: centerPoint,
|
||||
);
|
||||
await EntityService.instance
|
||||
.addOrUpdate(EntityType.location, json.encode(locationTag.toJson()));
|
||||
Bus.instance.fire(LocationTagUpdatedEvent(LocTagEventType.add));
|
||||
}
|
||||
|
||||
///The area bounded by the location tag becomes more elliptical with increase
|
||||
///in the magnitude of the latitude on the caritesian plane. When latitude is
|
||||
///0 degrees, the ellipse is a circle with a = b = r. When latitude incrases,
|
||||
///the major axis (a) has to be scaled by the secant of the latitude.
|
||||
double _scaleFactor(double lat) {
|
||||
return 1 / cos(lat * (pi / 180));
|
||||
}
|
||||
|
||||
Future<List<LocalEntity<LocationTag>>> enclosingLocationTags(
|
||||
Location fileCoordinates,
|
||||
) async {
|
||||
try {
|
||||
final result = List<LocalEntity<LocationTag>>.of([]);
|
||||
final locationTagEntities = await getLocationTags();
|
||||
for (LocalEntity<LocationTag> locationTagEntity in locationTagEntities) {
|
||||
final locationTag = locationTagEntity.item;
|
||||
final x = fileCoordinates.latitude! - locationTag.centerPoint.latitude!;
|
||||
final y =
|
||||
fileCoordinates.longitude! - locationTag.centerPoint.longitude!;
|
||||
if ((x * x) / (locationTag.aSquare) + (y * y) / (locationTag.bSquare) <=
|
||||
1) {
|
||||
result.add(
|
||||
locationTagEntity,
|
||||
);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
} catch (e, s) {
|
||||
_logger.severe("Failed to get enclosing location tags", e, s);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
bool isFileInsideLocationTag(
|
||||
Location centerPoint,
|
||||
Location fileCoordinates,
|
||||
int radius,
|
||||
) {
|
||||
final a =
|
||||
(radius * _scaleFactor(centerPoint.latitude!)) / kilometersPerDegree;
|
||||
final b = radius / kilometersPerDegree;
|
||||
final x = centerPoint.latitude! - fileCoordinates.latitude!;
|
||||
final y = centerPoint.longitude! - fileCoordinates.longitude!;
|
||||
if ((x * x) / (a * a) + (y * y) / (b * b) <= 1) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
String convertLocationToDMS(Location centerPoint) {
|
||||
final lat = centerPoint.latitude!;
|
||||
final long = centerPoint.longitude!;
|
||||
final latRef = lat >= 0 ? "N" : "S";
|
||||
final longRef = long >= 0 ? "E" : "W";
|
||||
final latDMS = convertCoordinateToDMS(lat.abs());
|
||||
final longDMS = convertCoordinateToDMS(long.abs());
|
||||
return "${latDMS[0]}°${latDMS[1]}'${latDMS[2]}\"$latRef, ${longDMS[0]}°${longDMS[1]}'${longDMS[2]}\"$longRef";
|
||||
}
|
||||
|
||||
List<int> convertCoordinateToDMS(double coordinate) {
|
||||
final degrees = coordinate.floor();
|
||||
final minutes = ((coordinate - degrees) * 60).floor();
|
||||
final seconds = ((coordinate - degrees - minutes / 60) * 3600).floor();
|
||||
return [degrees, minutes, seconds];
|
||||
}
|
||||
|
||||
///Will only update if there is a change in the locationTag's properties
|
||||
Future<void> updateLocationTag({
|
||||
required LocalEntity<LocationTag> locationTagEntity,
|
||||
int? newRadius,
|
||||
Location? newCenterPoint,
|
||||
String? newName,
|
||||
}) async {
|
||||
try {
|
||||
final radius = newRadius ?? locationTagEntity.item.radius;
|
||||
final centerPoint = newCenterPoint ?? locationTagEntity.item.centerPoint;
|
||||
final name = newName ?? locationTagEntity.item.name;
|
||||
|
||||
final locationTag = locationTagEntity.item;
|
||||
//Exit if there is no change in locationTag's properties
|
||||
if (radius == locationTag.radius &&
|
||||
centerPoint == locationTag.centerPoint &&
|
||||
name == locationTag.name) {
|
||||
return;
|
||||
}
|
||||
final a =
|
||||
(radius * _scaleFactor(centerPoint.latitude!)) / kilometersPerDegree;
|
||||
final b = radius / kilometersPerDegree;
|
||||
final updatedLoationTag = locationTagEntity.item.copyWith(
|
||||
centerPoint: centerPoint,
|
||||
aSquare: a * a,
|
||||
bSquare: b * b,
|
||||
radius: radius,
|
||||
name: name,
|
||||
);
|
||||
|
||||
await EntityService.instance.addOrUpdate(
|
||||
EntityType.location,
|
||||
json.encode(updatedLoationTag.toJson()),
|
||||
id: locationTagEntity.id,
|
||||
);
|
||||
Bus.instance.fire(
|
||||
LocationTagUpdatedEvent(
|
||||
LocTagEventType.update,
|
||||
updatedLocTagEntities: [
|
||||
LocalEntity(updatedLoationTag, locationTagEntity.id)
|
||||
],
|
||||
),
|
||||
);
|
||||
} catch (e, s) {
|
||||
_logger.severe("Failed to update location tag", e, s);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteLocationTag(String locTagEntityId) async {
|
||||
try {
|
||||
await EntityService.instance.deleteEntry(
|
||||
locTagEntityId,
|
||||
);
|
||||
Bus.instance.fire(
|
||||
LocationTagUpdatedEvent(
|
||||
LocTagEventType.delete,
|
||||
),
|
||||
);
|
||||
} catch (e, s) {
|
||||
_logger.severe("Failed to delete location tag", e, s);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class GPSData {
|
||||
final String latRef;
|
||||
final List<double> lat;
|
||||
final String longRef;
|
||||
final List<double> long;
|
||||
|
||||
GPSData(this.latRef, this.lat, this.longRef, this.long);
|
||||
|
||||
Location toLocationObj() {
|
||||
final latSign = latRef == "N" ? 1 : -1;
|
||||
final longSign = longRef == "E" ? 1 : -1;
|
||||
return Location(
|
||||
latitude: latSign * lat[0] + lat[1] / 60 + lat[2] / 3600,
|
||||
longitude: longSign * long[0] + long[1] / 60 + long[2] / 3600,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -9,12 +9,12 @@ import 'package:photos/models/collection.dart';
|
|||
import 'package:photos/models/collection_items.dart';
|
||||
import 'package:photos/models/file.dart';
|
||||
import 'package:photos/models/file_type.dart';
|
||||
import 'package:photos/models/location.dart';
|
||||
import "package:photos/models/location_tag/location_tag.dart";
|
||||
import 'package:photos/models/search/album_search_result.dart';
|
||||
import 'package:photos/models/search/generic_search_result.dart';
|
||||
import 'package:photos/models/search/location_api_response.dart';
|
||||
import 'package:photos/models/search/search_result.dart';
|
||||
import 'package:photos/services/collections_service.dart';
|
||||
import "package:photos/services/location_service.dart";
|
||||
import 'package:photos/utils/date_time_util.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
|
@ -53,46 +53,6 @@ class SearchService {
|
|||
_cachedFilesFuture = null;
|
||||
}
|
||||
|
||||
Future<List<GenericSearchResult>> getLocationSearchResults(
|
||||
String query,
|
||||
) async {
|
||||
final List<GenericSearchResult> searchResults = [];
|
||||
try {
|
||||
final List<File> allFiles = await _getAllFiles();
|
||||
// This code used an deprecated API earlier. We've retained the
|
||||
// scaffolding for when we implement a client side location search, and
|
||||
// meanwhile have replaced the API response.data with an empty map here.
|
||||
final matchedLocationSearchResults = LocationApiResponse.fromMap({});
|
||||
|
||||
for (var locationData in matchedLocationSearchResults.results) {
|
||||
final List<File> filesInLocation = [];
|
||||
|
||||
for (var file in allFiles) {
|
||||
if (_isValidLocation(file.location) &&
|
||||
_isLocationWithinBounds(file.location!, locationData)) {
|
||||
filesInLocation.add(file);
|
||||
}
|
||||
}
|
||||
filesInLocation.sort(
|
||||
(first, second) =>
|
||||
second.creationTime!.compareTo(first.creationTime!),
|
||||
);
|
||||
if (filesInLocation.isNotEmpty) {
|
||||
searchResults.add(
|
||||
GenericSearchResult(
|
||||
ResultType.location,
|
||||
locationData.place,
|
||||
filesInLocation,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
_logger.severe(e);
|
||||
}
|
||||
return searchResults;
|
||||
}
|
||||
|
||||
// getFilteredCollectionsWithThumbnail removes deleted or archived or
|
||||
// collections which don't have a file from search result
|
||||
Future<List<AlbumSearchResult>> getCollectionSearchResults(
|
||||
|
@ -263,6 +223,51 @@ class SearchService {
|
|||
return searchResults;
|
||||
}
|
||||
|
||||
Future<List<GenericSearchResult>> getLocationResults(
|
||||
String query,
|
||||
) async {
|
||||
final locations =
|
||||
(await LocationService.instance.getLocationTags()).map((e) => e.item);
|
||||
final Map<LocationTag, List<File>> result = {};
|
||||
|
||||
final List<GenericSearchResult> searchResults = [];
|
||||
|
||||
for (LocationTag tag in locations) {
|
||||
if (tag.name.toLowerCase().contains(query.toLowerCase())) {
|
||||
result[tag] = [];
|
||||
}
|
||||
}
|
||||
if (result.isEmpty) {
|
||||
return searchResults;
|
||||
}
|
||||
final allFiles = await _getAllFiles();
|
||||
for (File file in allFiles) {
|
||||
if (file.hasLocation) {
|
||||
for (LocationTag tag in result.keys) {
|
||||
if (LocationService.instance.isFileInsideLocationTag(
|
||||
tag.centerPoint,
|
||||
file.location!,
|
||||
tag.radius,
|
||||
)) {
|
||||
result[tag]!.add(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (MapEntry<LocationTag, List<File>> entry in result.entries) {
|
||||
if (entry.value.isNotEmpty) {
|
||||
searchResults.add(
|
||||
GenericSearchResult(
|
||||
ResultType.location,
|
||||
entry.key.name,
|
||||
entry.value,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return searchResults;
|
||||
}
|
||||
|
||||
Future<List<GenericSearchResult>> getMonthSearchResults(String query) async {
|
||||
final List<GenericSearchResult> searchResults = [];
|
||||
for (var month in _getMatchingMonths(query)) {
|
||||
|
@ -363,25 +368,6 @@ class SearchService {
|
|||
return durationsOfMonthInEveryYear;
|
||||
}
|
||||
|
||||
bool _isValidLocation(Location? location) {
|
||||
return location != null &&
|
||||
location.latitude != null &&
|
||||
location.latitude != 0 &&
|
||||
location.longitude != null &&
|
||||
location.longitude != 0;
|
||||
}
|
||||
|
||||
bool _isLocationWithinBounds(
|
||||
Location location,
|
||||
LocationDataFromResponse locationData,
|
||||
) {
|
||||
//format returned by the api is [lng,lat,lng,lat] where indexes 0 & 1 are southwest and 2 & 3 northeast
|
||||
return location.longitude! > locationData.bbox[0] &&
|
||||
location.latitude! > locationData.bbox[1] &&
|
||||
location.longitude! < locationData.bbox[2] &&
|
||||
location.latitude! < locationData.bbox[3];
|
||||
}
|
||||
|
||||
List<Tuple3<int, MonthData, int?>> _getPossibleEventDate(String query) {
|
||||
final List<Tuple3<int, MonthData, int?>> possibleEvents = [];
|
||||
if (query.trim().isEmpty) {
|
||||
|
|
77
lib/states/location_screen_state.dart
Normal file
77
lib/states/location_screen_state.dart
Normal file
|
@ -0,0 +1,77 @@
|
|||
import "dart:async";
|
||||
|
||||
import "package:flutter/material.dart";
|
||||
import "package:photos/core/event_bus.dart";
|
||||
import "package:photos/events/location_tag_updated_event.dart";
|
||||
import "package:photos/models/local_entity_data.dart";
|
||||
import 'package:photos/models/location_tag/location_tag.dart';
|
||||
|
||||
class LocationScreenStateProvider extends StatefulWidget {
|
||||
final LocalEntity<LocationTag> locationTagEntity;
|
||||
final Widget child;
|
||||
const LocationScreenStateProvider(
|
||||
this.locationTagEntity,
|
||||
this.child, {
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<LocationScreenStateProvider> createState() =>
|
||||
_LocationScreenStateProviderState();
|
||||
}
|
||||
|
||||
class _LocationScreenStateProviderState
|
||||
extends State<LocationScreenStateProvider> {
|
||||
late LocalEntity<LocationTag> _locationTagEntity;
|
||||
late final StreamSubscription _locTagUpdateListener;
|
||||
@override
|
||||
void initState() {
|
||||
_locationTagEntity = widget.locationTagEntity;
|
||||
_locTagUpdateListener =
|
||||
Bus.instance.on<LocationTagUpdatedEvent>().listen((event) {
|
||||
if (event.type == LocTagEventType.update) {
|
||||
setState(() {
|
||||
_locationTagEntity = event.updatedLocTagEntities!.first;
|
||||
});
|
||||
}
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
dispose() {
|
||||
_locTagUpdateListener.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InheritedLocationScreenState(
|
||||
_locationTagEntity,
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class InheritedLocationScreenState extends InheritedWidget {
|
||||
final LocalEntity<LocationTag> locationTagEntity;
|
||||
const InheritedLocationScreenState(
|
||||
this.locationTagEntity, {
|
||||
super.key,
|
||||
required super.child,
|
||||
});
|
||||
|
||||
//This is used to show loading state when memory count is beign computed and to
|
||||
//show count after computation.
|
||||
static final memoryCountNotifier = ValueNotifier<int?>(null);
|
||||
|
||||
static InheritedLocationScreenState of(BuildContext context) {
|
||||
return context
|
||||
.dependOnInheritedWidgetOfExactType<InheritedLocationScreenState>()!;
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(covariant InheritedLocationScreenState oldWidget) {
|
||||
return oldWidget.locationTagEntity != locationTagEntity;
|
||||
}
|
||||
}
|
132
lib/states/location_state.dart
Normal file
132
lib/states/location_state.dart
Normal file
|
@ -0,0 +1,132 @@
|
|||
import "dart:async";
|
||||
|
||||
import "package:flutter/material.dart";
|
||||
import "package:photos/core/constants.dart";
|
||||
import "package:photos/core/event_bus.dart";
|
||||
import "package:photos/events/location_tag_updated_event.dart";
|
||||
import "package:photos/models/local_entity_data.dart";
|
||||
import "package:photos/models/location/location.dart";
|
||||
import "package:photos/models/location_tag/location_tag.dart";
|
||||
import "package:photos/models/typedefs.dart";
|
||||
import "package:photos/utils/debouncer.dart";
|
||||
|
||||
class LocationTagStateProvider extends StatefulWidget {
|
||||
final LocalEntity<LocationTag>? locationTagEntity;
|
||||
final Location? centerPoint;
|
||||
final Widget child;
|
||||
const LocationTagStateProvider(
|
||||
this.child, {
|
||||
this.centerPoint,
|
||||
this.locationTagEntity,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<LocationTagStateProvider> createState() =>
|
||||
_LocationTagStateProviderState();
|
||||
}
|
||||
|
||||
class _LocationTagStateProviderState extends State<LocationTagStateProvider> {
|
||||
int _selectedRaduisIndex = defaultRadiusValueIndex;
|
||||
late Location? _centerPoint;
|
||||
late LocalEntity<LocationTag>? _locationTagEntity;
|
||||
final Debouncer _selectedRadiusDebouncer =
|
||||
Debouncer(const Duration(milliseconds: 300));
|
||||
late final StreamSubscription _locTagEntityListener;
|
||||
@override
|
||||
void initState() {
|
||||
_locationTagEntity = widget.locationTagEntity;
|
||||
_centerPoint = widget.centerPoint;
|
||||
assert(_centerPoint != null || _locationTagEntity != null);
|
||||
_centerPoint = _locationTagEntity?.item.centerPoint ?? _centerPoint!;
|
||||
_selectedRaduisIndex =
|
||||
_locationTagEntity?.item.radiusIndex ?? defaultRadiusValueIndex;
|
||||
_locTagEntityListener =
|
||||
Bus.instance.on<LocationTagUpdatedEvent>().listen((event) {
|
||||
_locationTagUpdateListener(event);
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_locTagEntityListener.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _locationTagUpdateListener(LocationTagUpdatedEvent event) {
|
||||
if (event.type == LocTagEventType.update) {
|
||||
if (event.updatedLocTagEntities!.first.id == _locationTagEntity!.id) {
|
||||
//Update state when locationTag is updated.
|
||||
setState(() {
|
||||
final updatedLocTagEntity = event.updatedLocTagEntities!.first;
|
||||
_selectedRaduisIndex = updatedLocTagEntity.item.radiusIndex;
|
||||
_centerPoint = updatedLocTagEntity.item.centerPoint;
|
||||
_locationTagEntity = updatedLocTagEntity;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _updateSelectedIndex(int index) {
|
||||
_selectedRadiusDebouncer.cancelDebounce();
|
||||
_selectedRadiusDebouncer.run(() async {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_selectedRaduisIndex = index;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _updateCenterPoint(Location centerPoint) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_centerPoint = centerPoint;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InheritedLocationTagData(
|
||||
_selectedRaduisIndex,
|
||||
_centerPoint!,
|
||||
_updateSelectedIndex,
|
||||
_locationTagEntity,
|
||||
_updateCenterPoint,
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
///This InheritedWidget's state is used in add & edit location sheets
|
||||
class InheritedLocationTagData extends InheritedWidget {
|
||||
final int selectedRadiusIndex;
|
||||
final Location centerPoint;
|
||||
//locationTag is null when we are creating a new location tag in add location sheet
|
||||
final LocalEntity<LocationTag>? locationTagEntity;
|
||||
final VoidCallbackParamInt updateSelectedIndex;
|
||||
final VoidCallbackParamLocation updateCenterPoint;
|
||||
const InheritedLocationTagData(
|
||||
this.selectedRadiusIndex,
|
||||
this.centerPoint,
|
||||
this.updateSelectedIndex,
|
||||
this.locationTagEntity,
|
||||
this.updateCenterPoint, {
|
||||
required super.child,
|
||||
super.key,
|
||||
});
|
||||
|
||||
static InheritedLocationTagData of(BuildContext context) {
|
||||
return context
|
||||
.dependOnInheritedWidgetOfExactType<InheritedLocationTagData>()!;
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(InheritedLocationTagData oldWidget) {
|
||||
return oldWidget.selectedRadiusIndex != selectedRadiusIndex ||
|
||||
oldWidget.centerPoint != centerPoint ||
|
||||
oldWidget.locationTagEntity != locationTagEntity;
|
||||
}
|
||||
}
|
|
@ -216,6 +216,7 @@ const Color tabIconDark = Color.fromRGBO(255, 255, 255, 0.80);
|
|||
// Fixed Colors
|
||||
|
||||
const Color fixedStrokeMutedWhite = Color.fromRGBO(255, 255, 255, 0.50);
|
||||
const Color strokeSolidMutedLight = Color.fromRGBO(147, 147, 147, 1);
|
||||
|
||||
const Color _primary700 = Color.fromRGBO(0, 179, 60, 1);
|
||||
const Color _primary500 = Color.fromRGBO(29, 185, 84, 1);
|
||||
|
|
|
@ -131,7 +131,7 @@ Future<void> showSingleFileDeleteSheet(
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> showInfoSheet(BuildContext context, File file) async {
|
||||
Future<void> showDetailsSheet(BuildContext context, File file) async {
|
||||
final colorScheme = getEnteColorScheme(context);
|
||||
return showBarModalBottomSheet(
|
||||
topControl: const SizedBox.shrink(),
|
||||
|
|
|
@ -159,8 +159,9 @@ class _CollectionActionSheetState extends State<CollectionActionSheet> {
|
|||
_searchQuery = value;
|
||||
});
|
||||
},
|
||||
cancellable: true,
|
||||
shouldUnfocusOnCancelOrSubmit: true,
|
||||
isClearable: true,
|
||||
shouldUnfocusOnClearOrSubmit: true,
|
||||
borderRadius: 2,
|
||||
),
|
||||
),
|
||||
_getCollectionItems(filesCount),
|
||||
|
|
|
@ -37,14 +37,16 @@ class ChipButtonWidget extends StatelessWidget {
|
|||
size: 17,
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
const SizedBox(width: 4),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: Text(
|
||||
label ?? "",
|
||||
style: getEnteTextTheme(context).smallBold,
|
||||
),
|
||||
)
|
||||
if (label != null && leadingIcon != null)
|
||||
const SizedBox(width: 4),
|
||||
if (label != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: Text(
|
||||
label!,
|
||||
style: getEnteTextTheme(context).smallBold,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
@ -28,17 +28,23 @@ class DividerWidget extends StatelessWidget {
|
|||
: getEnteColorScheme(context).strokeFaint;
|
||||
|
||||
if (dividerType == DividerType.solid) {
|
||||
return Container(
|
||||
color: getEnteColorScheme(context).strokeFaint,
|
||||
width: double.infinity,
|
||||
height: 1,
|
||||
return Padding(
|
||||
padding: padding ?? EdgeInsets.zero,
|
||||
child: Container(
|
||||
color: getEnteColorScheme(context).strokeFaint,
|
||||
width: double.infinity,
|
||||
height: 1,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (dividerType == DividerType.bottomBar) {
|
||||
return Container(
|
||||
color: dividerColor,
|
||||
width: double.infinity,
|
||||
height: 1,
|
||||
return Padding(
|
||||
padding: padding ?? EdgeInsets.zero,
|
||||
child: Container(
|
||||
color: dividerColor,
|
||||
width: double.infinity,
|
||||
height: 1,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -16,10 +16,15 @@ class TextInputWidget extends StatefulWidget {
|
|||
final Alignment? alignMessage;
|
||||
final bool? autoFocus;
|
||||
final int? maxLength;
|
||||
final double borderRadius;
|
||||
|
||||
///TextInputWidget will listen to this notifier and executes onSubmit when
|
||||
///notified.
|
||||
final ValueNotifier? submitNotifier;
|
||||
|
||||
///TextInputWidget will listen to this notifier and clears and unfocuses the
|
||||
///textFiled when notified.
|
||||
final ValueNotifier? cancelNotifier;
|
||||
final bool alwaysShowSuccessState;
|
||||
final bool showOnlyLoadingState;
|
||||
final FutureVoidCallbackParamStr? onSubmit;
|
||||
|
@ -28,8 +33,14 @@ class TextInputWidget extends StatefulWidget {
|
|||
final bool shouldSurfaceExecutionStates;
|
||||
final TextCapitalization? textCapitalization;
|
||||
final bool isPasswordInput;
|
||||
final bool cancellable;
|
||||
final bool shouldUnfocusOnCancelOrSubmit;
|
||||
|
||||
///Clear comes in the form of a suffix icon. It is unrelated to onCancel.
|
||||
final bool isClearable;
|
||||
final bool shouldUnfocusOnClearOrSubmit;
|
||||
final FocusNode? focusNode;
|
||||
final VoidCallback? onCancel;
|
||||
final TextEditingController? textEditingController;
|
||||
final ValueNotifier? isEmptyNotifier;
|
||||
const TextInputWidget({
|
||||
this.onSubmit,
|
||||
this.onChange,
|
||||
|
@ -42,14 +53,20 @@ class TextInputWidget extends StatefulWidget {
|
|||
this.autoFocus,
|
||||
this.maxLength,
|
||||
this.submitNotifier,
|
||||
this.cancelNotifier,
|
||||
this.alwaysShowSuccessState = false,
|
||||
this.showOnlyLoadingState = false,
|
||||
this.popNavAfterSubmission = false,
|
||||
this.shouldSurfaceExecutionStates = true,
|
||||
this.textCapitalization = TextCapitalization.none,
|
||||
this.isPasswordInput = false,
|
||||
this.cancellable = false,
|
||||
this.shouldUnfocusOnCancelOrSubmit = false,
|
||||
this.isClearable = false,
|
||||
this.shouldUnfocusOnClearOrSubmit = false,
|
||||
this.borderRadius = 8,
|
||||
this.focusNode,
|
||||
this.onCancel,
|
||||
this.textEditingController,
|
||||
this.isEmptyNotifier,
|
||||
super.key,
|
||||
});
|
||||
|
||||
|
@ -59,7 +76,7 @@ class TextInputWidget extends StatefulWidget {
|
|||
|
||||
class _TextInputWidgetState extends State<TextInputWidget> {
|
||||
ExecutionState executionState = ExecutionState.idle;
|
||||
final _textController = TextEditingController();
|
||||
late final TextEditingController _textController;
|
||||
final _debouncer = Debouncer(const Duration(milliseconds: 300));
|
||||
late final ValueNotifier<bool> _obscureTextNotifier;
|
||||
|
||||
|
@ -70,6 +87,8 @@ class _TextInputWidgetState extends State<TextInputWidget> {
|
|||
@override
|
||||
void initState() {
|
||||
widget.submitNotifier?.addListener(_onSubmit);
|
||||
widget.cancelNotifier?.addListener(_onCancel);
|
||||
_textController = widget.textEditingController ?? TextEditingController();
|
||||
|
||||
if (widget.initialValue != null) {
|
||||
_textController.value = TextEditingValue(
|
||||
|
@ -84,14 +103,22 @@ class _TextInputWidgetState extends State<TextInputWidget> {
|
|||
}
|
||||
_obscureTextNotifier = ValueNotifier(widget.isPasswordInput);
|
||||
_obscureTextNotifier.addListener(_safeRefresh);
|
||||
|
||||
if (widget.isEmptyNotifier != null) {
|
||||
_textController.addListener(() {
|
||||
widget.isEmptyNotifier!.value = _textController.text.isEmpty;
|
||||
});
|
||||
}
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.submitNotifier?.removeListener(_onSubmit);
|
||||
widget.cancelNotifier?.removeListener(_onCancel);
|
||||
_obscureTextNotifier.dispose();
|
||||
_textController.dispose();
|
||||
widget.isEmptyNotifier?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
@ -113,12 +140,13 @@ class _TextInputWidgetState extends State<TextInputWidget> {
|
|||
}
|
||||
textInputChildren.add(
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
borderRadius: BorderRadius.all(Radius.circular(widget.borderRadius)),
|
||||
child: Material(
|
||||
child: TextFormField(
|
||||
textCapitalization: widget.textCapitalization!,
|
||||
autofocus: widget.autoFocus ?? false,
|
||||
controller: _textController,
|
||||
focusNode: widget.focusNode,
|
||||
inputFormatters: widget.maxLength != null
|
||||
? [LengthLimitingTextInputFormatter(50)]
|
||||
: null,
|
||||
|
@ -155,9 +183,9 @@ class _TextInputWidgetState extends State<TextInputWidget> {
|
|||
obscureTextNotifier: _obscureTextNotifier,
|
||||
isPasswordInput: widget.isPasswordInput,
|
||||
textController: _textController,
|
||||
isCancellable: widget.cancellable,
|
||||
shouldUnfocusOnCancelOrSubmit:
|
||||
widget.shouldUnfocusOnCancelOrSubmit,
|
||||
isClearable: widget.isClearable,
|
||||
shouldUnfocusOnClearOrSubmit:
|
||||
widget.shouldUnfocusOnClearOrSubmit,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -224,7 +252,7 @@ class _TextInputWidgetState extends State<TextInputWidget> {
|
|||
});
|
||||
}),
|
||||
);
|
||||
if (widget.shouldUnfocusOnCancelOrSubmit) {
|
||||
if (widget.shouldUnfocusOnClearOrSubmit) {
|
||||
FocusScope.of(context).unfocus();
|
||||
}
|
||||
try {
|
||||
|
@ -303,6 +331,15 @@ class _TextInputWidgetState extends State<TextInputWidget> {
|
|||
}
|
||||
}
|
||||
|
||||
void _onCancel() {
|
||||
if (widget.onCancel != null) {
|
||||
widget.onCancel!();
|
||||
} else {
|
||||
_textController.clear();
|
||||
FocusScope.of(context).unfocus();
|
||||
}
|
||||
}
|
||||
|
||||
void _popNavigatorStack(BuildContext context, {Exception? e}) {
|
||||
Navigator.of(context).canPop() ? Navigator.of(context).pop(e) : null;
|
||||
}
|
||||
|
@ -315,8 +352,8 @@ class SuffixIconWidget extends StatelessWidget {
|
|||
final TextEditingController textController;
|
||||
final ValueNotifier? obscureTextNotifier;
|
||||
final bool isPasswordInput;
|
||||
final bool isCancellable;
|
||||
final bool shouldUnfocusOnCancelOrSubmit;
|
||||
final bool isClearable;
|
||||
final bool shouldUnfocusOnClearOrSubmit;
|
||||
|
||||
const SuffixIconWidget({
|
||||
required this.executionState,
|
||||
|
@ -324,8 +361,8 @@ class SuffixIconWidget extends StatelessWidget {
|
|||
required this.textController,
|
||||
this.obscureTextNotifier,
|
||||
this.isPasswordInput = false,
|
||||
this.isCancellable = false,
|
||||
this.shouldUnfocusOnCancelOrSubmit = false,
|
||||
this.isClearable = false,
|
||||
this.shouldUnfocusOnClearOrSubmit = false,
|
||||
super.key,
|
||||
});
|
||||
|
||||
|
@ -335,11 +372,11 @@ class SuffixIconWidget extends StatelessWidget {
|
|||
final colorScheme = getEnteColorScheme(context);
|
||||
if (executionState == ExecutionState.idle ||
|
||||
!shouldSurfaceExecutionStates) {
|
||||
if (isCancellable) {
|
||||
if (isClearable) {
|
||||
trailingWidget = GestureDetector(
|
||||
onTap: () {
|
||||
textController.clear();
|
||||
if (shouldUnfocusOnCancelOrSubmit) {
|
||||
if (shouldUnfocusOnClearOrSubmit) {
|
||||
FocusScope.of(context).unfocus();
|
||||
}
|
||||
},
|
||||
|
|
|
@ -13,6 +13,7 @@ class TitleBarWidget extends StatelessWidget {
|
|||
final bool isFlexibleSpaceDisabled;
|
||||
final bool isOnTopOfScreen;
|
||||
final Color? backgroundColor;
|
||||
final bool isSliver;
|
||||
const TitleBarWidget({
|
||||
this.leading,
|
||||
this.title,
|
||||
|
@ -24,103 +25,96 @@ class TitleBarWidget extends StatelessWidget {
|
|||
this.isFlexibleSpaceDisabled = false,
|
||||
this.isOnTopOfScreen = true,
|
||||
this.backgroundColor,
|
||||
this.isSliver = true,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const toolbarHeight = 48.0;
|
||||
final textTheme = getEnteTextTheme(context);
|
||||
final colorTheme = getEnteColorScheme(context);
|
||||
return SliverAppBar(
|
||||
backgroundColor: backgroundColor,
|
||||
primary: isOnTopOfScreen ? true : false,
|
||||
toolbarHeight: toolbarHeight,
|
||||
leadingWidth: 48,
|
||||
automaticallyImplyLeading: false,
|
||||
pinned: true,
|
||||
expandedHeight: isFlexibleSpaceDisabled ? toolbarHeight : 102,
|
||||
centerTitle: false,
|
||||
titleSpacing: 4,
|
||||
title: Padding(
|
||||
padding: EdgeInsets.only(left: isTitleH2WithoutLeading ? 16 : 0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
title == null
|
||||
? const SizedBox.shrink()
|
||||
: Text(
|
||||
title!,
|
||||
style: isTitleH2WithoutLeading
|
||||
? textTheme.h2Bold
|
||||
: textTheme.largeBold,
|
||||
),
|
||||
caption == null || isTitleH2WithoutLeading
|
||||
? const SizedBox.shrink()
|
||||
: Text(
|
||||
caption!,
|
||||
style: textTheme.mini.copyWith(color: colorTheme.textMuted),
|
||||
)
|
||||
],
|
||||
if (isSliver) {
|
||||
return SliverAppBar(
|
||||
backgroundColor: backgroundColor,
|
||||
primary: isOnTopOfScreen ? true : false,
|
||||
toolbarHeight: toolbarHeight,
|
||||
leadingWidth: 48,
|
||||
automaticallyImplyLeading: false,
|
||||
pinned: true,
|
||||
expandedHeight: isFlexibleSpaceDisabled ? toolbarHeight : 102,
|
||||
centerTitle: false,
|
||||
titleSpacing: 4,
|
||||
title: TitleWidget(
|
||||
title: title,
|
||||
caption: caption,
|
||||
isTitleH2WithoutLeading: isTitleH2WithoutLeading,
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: Row(
|
||||
children: _actionsWithPaddingInBetween(),
|
||||
),
|
||||
),
|
||||
],
|
||||
leading: isTitleH2WithoutLeading
|
||||
? null
|
||||
: leading ??
|
||||
IconButtonWidget(
|
||||
icon: Icons.arrow_back_outlined,
|
||||
iconButtonType: IconButtonType.primary,
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
flexibleSpace: isFlexibleSpaceDisabled
|
||||
? null
|
||||
: FlexibleSpaceBar(
|
||||
background: SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
const SizedBox(height: toolbarHeight),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 4,
|
||||
horizontal: 16,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
flexibleSpaceTitle == null
|
||||
? const SizedBox.shrink()
|
||||
: flexibleSpaceTitle!,
|
||||
flexibleSpaceCaption == null
|
||||
? const SizedBox.shrink()
|
||||
: Text(
|
||||
flexibleSpaceCaption!,
|
||||
style: textTheme.small.copyWith(
|
||||
color: colorTheme.textMuted,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: Row(
|
||||
children: _actionsWithPaddingInBetween(),
|
||||
),
|
||||
);
|
||||
),
|
||||
],
|
||||
leading: isTitleH2WithoutLeading
|
||||
? null
|
||||
: leading ??
|
||||
IconButtonWidget(
|
||||
icon: Icons.arrow_back_outlined,
|
||||
iconButtonType: IconButtonType.primary,
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
flexibleSpace: isFlexibleSpaceDisabled
|
||||
? null
|
||||
: FlexibleSpaceBarWidget(
|
||||
flexibleSpaceTitle,
|
||||
flexibleSpaceCaption,
|
||||
toolbarHeight,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return AppBar(
|
||||
backgroundColor: backgroundColor,
|
||||
primary: isOnTopOfScreen ? true : false,
|
||||
toolbarHeight: toolbarHeight,
|
||||
leadingWidth: 48,
|
||||
automaticallyImplyLeading: false,
|
||||
centerTitle: false,
|
||||
titleSpacing: 4,
|
||||
title: TitleWidget(
|
||||
title: title,
|
||||
caption: caption,
|
||||
isTitleH2WithoutLeading: isTitleH2WithoutLeading,
|
||||
),
|
||||
actions: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: Row(
|
||||
children: _actionsWithPaddingInBetween(),
|
||||
),
|
||||
),
|
||||
],
|
||||
leading: isTitleH2WithoutLeading
|
||||
? null
|
||||
: leading ??
|
||||
IconButtonWidget(
|
||||
icon: Icons.arrow_back_outlined,
|
||||
iconButtonType: IconButtonType.primary,
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
flexibleSpace: isFlexibleSpaceDisabled
|
||||
? null
|
||||
: FlexibleSpaceBarWidget(
|
||||
flexibleSpaceTitle,
|
||||
flexibleSpaceCaption,
|
||||
toolbarHeight,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_actionsWithPaddingInBetween() {
|
||||
|
@ -150,3 +144,89 @@ class TitleBarWidget extends StatelessWidget {
|
|||
return actions;
|
||||
}
|
||||
}
|
||||
|
||||
class TitleWidget extends StatelessWidget {
|
||||
final String? title;
|
||||
final String? caption;
|
||||
final bool isTitleH2WithoutLeading;
|
||||
const TitleWidget(
|
||||
{this.title,
|
||||
this.caption,
|
||||
required this.isTitleH2WithoutLeading,
|
||||
super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textTheme = getEnteTextTheme(context);
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(left: isTitleH2WithoutLeading ? 16 : 0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
title == null
|
||||
? const SizedBox.shrink()
|
||||
: Text(
|
||||
title!,
|
||||
style: isTitleH2WithoutLeading
|
||||
? textTheme.h2Bold
|
||||
: textTheme.largeBold,
|
||||
),
|
||||
caption == null || isTitleH2WithoutLeading
|
||||
? const SizedBox.shrink()
|
||||
: Text(
|
||||
caption!,
|
||||
style: textTheme.miniMuted,
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FlexibleSpaceBarWidget extends StatelessWidget {
|
||||
final Widget? flexibleSpaceTitle;
|
||||
final String? flexibleSpaceCaption;
|
||||
final double toolbarHeight;
|
||||
const FlexibleSpaceBarWidget(
|
||||
this.flexibleSpaceTitle, this.flexibleSpaceCaption, this.toolbarHeight,
|
||||
{super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textTheme = getEnteTextTheme(context);
|
||||
return FlexibleSpaceBar(
|
||||
background: SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
SizedBox(height: toolbarHeight),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 4,
|
||||
horizontal: 16,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
flexibleSpaceTitle == null
|
||||
? const SizedBox.shrink()
|
||||
: flexibleSpaceTitle!,
|
||||
flexibleSpaceCaption == null
|
||||
? const SizedBox.shrink()
|
||||
: Text(
|
||||
flexibleSpaceCaption!,
|
||||
style: textTheme.smallMuted,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -369,7 +369,7 @@ class _FullScreenMemoryState extends State<FullScreenMemory> {
|
|||
color: Colors.white, //same for both themes
|
||||
),
|
||||
onPressed: () {
|
||||
showInfoSheet(context, file);
|
||||
showDetailsSheet(context, file);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
|
|
|
@ -60,6 +60,8 @@ class HugeListView<T> extends StatefulWidget {
|
|||
|
||||
final EdgeInsetsGeometry? thumbPadding;
|
||||
|
||||
final bool disableScroll;
|
||||
|
||||
const HugeListView({
|
||||
Key? key,
|
||||
this.controller,
|
||||
|
@ -77,6 +79,7 @@ class HugeListView<T> extends StatefulWidget {
|
|||
this.bottomSafeArea = 120.0,
|
||||
this.isDraggableScrollbarEnabled = true,
|
||||
this.thumbPadding,
|
||||
this.disableScroll = false,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
|
@ -160,6 +163,9 @@ class HugeListViewState<T> extends State<HugeListView<T>> {
|
|||
isEnabled: widget.isDraggableScrollbarEnabled,
|
||||
padding: widget.thumbPadding,
|
||||
child: ScrollablePositionedList.builder(
|
||||
physics: widget.disableScroll
|
||||
? const NeverScrollableScrollPhysics()
|
||||
: null,
|
||||
itemScrollController: widget.controller,
|
||||
itemPositionsListener: listener,
|
||||
initialScrollIndex: widget.startIndex,
|
||||
|
|
|
@ -32,11 +32,13 @@ class LazyLoadingGallery extends StatefulWidget {
|
|||
final Stream<FilesUpdatedEvent>? reloadEvent;
|
||||
final Set<EventType> removalEventTypes;
|
||||
final GalleryLoader asyncLoader;
|
||||
final SelectedFiles selectedFiles;
|
||||
final SelectedFiles? selectedFiles;
|
||||
final String tag;
|
||||
final String? logTag;
|
||||
final Stream<int> currentIndexStream;
|
||||
final int photoGirdSize;
|
||||
final bool areFilesCollatedByDay;
|
||||
final bool limitSelectionToOne;
|
||||
LazyLoadingGallery(
|
||||
this.files,
|
||||
this.index,
|
||||
|
@ -45,9 +47,11 @@ class LazyLoadingGallery extends StatefulWidget {
|
|||
this.asyncLoader,
|
||||
this.selectedFiles,
|
||||
this.tag,
|
||||
this.currentIndexStream, {
|
||||
this.currentIndexStream,
|
||||
this.areFilesCollatedByDay, {
|
||||
this.logTag = "",
|
||||
this.photoGirdSize = photoGridSizeDefault,
|
||||
this.limitSelectionToOne = false,
|
||||
Key? key,
|
||||
}) : super(key: key ?? UniqueKey());
|
||||
|
||||
|
@ -62,7 +66,7 @@ class _LazyLoadingGalleryState extends State<LazyLoadingGallery> {
|
|||
late Logger _logger;
|
||||
|
||||
late List<File> _files;
|
||||
late StreamSubscription<FilesUpdatedEvent> _reloadEventSubscription;
|
||||
late StreamSubscription<FilesUpdatedEvent>? _reloadEventSubscription;
|
||||
late StreamSubscription<int> _currentIndexSubscription;
|
||||
bool? _shouldRender;
|
||||
final ValueNotifier<bool> _toggleSelectAllFromDay = ValueNotifier(false);
|
||||
|
@ -72,7 +76,7 @@ class _LazyLoadingGalleryState extends State<LazyLoadingGallery> {
|
|||
@override
|
||||
void initState() {
|
||||
//this is for removing the 'select all from day' icon on unselecting all files with 'cancel'
|
||||
widget.selectedFiles.addListener(_selectedFilesListener);
|
||||
widget.selectedFiles?.addListener(_selectedFilesListener);
|
||||
super.initState();
|
||||
_init();
|
||||
}
|
||||
|
@ -81,7 +85,7 @@ class _LazyLoadingGalleryState extends State<LazyLoadingGallery> {
|
|||
_logger = Logger("LazyLoading_${widget.logTag}");
|
||||
_shouldRender = true;
|
||||
_files = widget.files;
|
||||
_reloadEventSubscription = widget.reloadEvent!.listen((e) => _onReload(e));
|
||||
_reloadEventSubscription = widget.reloadEvent?.listen((e) => _onReload(e));
|
||||
|
||||
_currentIndexSubscription =
|
||||
widget.currentIndexStream.listen((currentIndex) {
|
||||
|
@ -162,9 +166,9 @@ class _LazyLoadingGalleryState extends State<LazyLoadingGallery> {
|
|||
|
||||
@override
|
||||
void dispose() {
|
||||
_reloadEventSubscription.cancel();
|
||||
_reloadEventSubscription?.cancel();
|
||||
_currentIndexSubscription.cancel();
|
||||
widget.selectedFiles.removeListener(_selectedFilesListener);
|
||||
widget.selectedFiles?.removeListener(_selectedFilesListener);
|
||||
_toggleSelectAllFromDay.dispose();
|
||||
_showSelectAllButton.dispose();
|
||||
_areAllFromDaySelected.dispose();
|
||||
|
@ -175,7 +179,7 @@ class _LazyLoadingGalleryState extends State<LazyLoadingGallery> {
|
|||
void didUpdateWidget(LazyLoadingGallery oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (!listEquals(_files, widget.files)) {
|
||||
_reloadEventSubscription.cancel();
|
||||
_reloadEventSubscription?.cancel();
|
||||
_init();
|
||||
}
|
||||
}
|
||||
|
@ -190,47 +194,50 @@ class _LazyLoadingGalleryState extends State<LazyLoadingGallery> {
|
|||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
getDayWidget(
|
||||
context,
|
||||
_files[0].creationTime!,
|
||||
widget.photoGirdSize,
|
||||
),
|
||||
ValueListenableBuilder(
|
||||
valueListenable: _showSelectAllButton,
|
||||
builder: (context, dynamic value, _) {
|
||||
return !value
|
||||
? const SizedBox.shrink()
|
||||
: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
child: SizedBox(
|
||||
width: 48,
|
||||
height: 44,
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: _areAllFromDaySelected,
|
||||
builder: (context, dynamic value, _) {
|
||||
return value
|
||||
? const Icon(
|
||||
Icons.check_circle,
|
||||
size: 18,
|
||||
)
|
||||
: Icon(
|
||||
Icons.check_circle_outlined,
|
||||
color: getEnteColorScheme(context)
|
||||
.strokeMuted,
|
||||
size: 18,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
//this value has no significance
|
||||
//changing only to notify the listeners
|
||||
_toggleSelectAllFromDay.value =
|
||||
!_toggleSelectAllFromDay.value;
|
||||
},
|
||||
);
|
||||
},
|
||||
)
|
||||
if (widget.areFilesCollatedByDay)
|
||||
getDayWidget(
|
||||
context,
|
||||
_files[0].creationTime!,
|
||||
widget.photoGirdSize,
|
||||
),
|
||||
widget.limitSelectionToOne
|
||||
? const SizedBox.shrink()
|
||||
: ValueListenableBuilder(
|
||||
valueListenable: _showSelectAllButton,
|
||||
builder: (context, dynamic value, _) {
|
||||
return !value
|
||||
? const SizedBox.shrink()
|
||||
: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
child: SizedBox(
|
||||
width: 48,
|
||||
height: 44,
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: _areAllFromDaySelected,
|
||||
builder: (context, dynamic value, _) {
|
||||
return value
|
||||
? const Icon(
|
||||
Icons.check_circle,
|
||||
size: 18,
|
||||
)
|
||||
: Icon(
|
||||
Icons.check_circle_outlined,
|
||||
color: getEnteColorScheme(context)
|
||||
.strokeMuted,
|
||||
size: 18,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
//this value has no significance
|
||||
//changing only to notify the listeners
|
||||
_toggleSelectAllFromDay.value =
|
||||
!_toggleSelectAllFromDay.value;
|
||||
},
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
_shouldRender!
|
||||
|
@ -261,6 +268,7 @@ class _LazyLoadingGalleryState extends State<LazyLoadingGallery> {
|
|||
_toggleSelectAllFromDay,
|
||||
_areAllFromDaySelected,
|
||||
widget.photoGirdSize,
|
||||
limitSelectionToOne: widget.limitSelectionToOne,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -271,7 +279,7 @@ class _LazyLoadingGalleryState extends State<LazyLoadingGallery> {
|
|||
}
|
||||
|
||||
void _selectedFilesListener() {
|
||||
if (widget.selectedFiles.files.isEmpty) {
|
||||
if (widget.selectedFiles!.files.isEmpty) {
|
||||
_showSelectAllButton.value = false;
|
||||
} else {
|
||||
_showSelectAllButton.value = true;
|
||||
|
@ -283,12 +291,13 @@ class LazyLoadingGridView extends StatefulWidget {
|
|||
final String tag;
|
||||
final List<File> filesInDay;
|
||||
final GalleryLoader asyncLoader;
|
||||
final SelectedFiles selectedFiles;
|
||||
final SelectedFiles? selectedFiles;
|
||||
final bool shouldRender;
|
||||
final bool shouldRecycle;
|
||||
final ValueNotifier toggleSelectAllFromDay;
|
||||
final ValueNotifier areAllFilesSelected;
|
||||
final int? photoGridSize;
|
||||
final bool limitSelectionToOne;
|
||||
|
||||
LazyLoadingGridView(
|
||||
this.tag,
|
||||
|
@ -300,6 +309,7 @@ class LazyLoadingGridView extends StatefulWidget {
|
|||
this.toggleSelectAllFromDay,
|
||||
this.areAllFilesSelected,
|
||||
this.photoGridSize, {
|
||||
this.limitSelectionToOne = false,
|
||||
Key? key,
|
||||
}) : super(key: key ?? UniqueKey());
|
||||
|
||||
|
@ -316,7 +326,7 @@ class _LazyLoadingGridViewState extends State<LazyLoadingGridView> {
|
|||
void initState() {
|
||||
_shouldRender = widget.shouldRender;
|
||||
_currentUserID = Configuration.instance.getUserID();
|
||||
widget.selectedFiles.addListener(_selectedFilesListener);
|
||||
widget.selectedFiles?.addListener(_selectedFilesListener);
|
||||
_clearSelectionsEvent =
|
||||
Bus.instance.on<ClearSelectionsEvent>().listen((event) {
|
||||
if (mounted) {
|
||||
|
@ -329,7 +339,7 @@ class _LazyLoadingGridViewState extends State<LazyLoadingGridView> {
|
|||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.selectedFiles.removeListener(_selectedFilesListener);
|
||||
widget.selectedFiles?.removeListener(_selectedFilesListener);
|
||||
_clearSelectionsEvent.cancel();
|
||||
widget.toggleSelectAllFromDay
|
||||
.removeListener(_toggleSelectAllFromDayListener);
|
||||
|
@ -403,12 +413,12 @@ class _LazyLoadingGridViewState extends State<LazyLoadingGridView> {
|
|||
mainAxisSpacing: 2,
|
||||
crossAxisCount: widget.photoGridSize!,
|
||||
),
|
||||
padding: const EdgeInsets.all(0),
|
||||
padding: const EdgeInsets.symmetric(vertical: (galleryGridSpacing / 2)),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFile(BuildContext context, File file) {
|
||||
final isFileSelected = widget.selectedFiles.isFileSelected(file);
|
||||
final isFileSelected = widget.selectedFiles?.isFileSelected(file) ?? false;
|
||||
Color selectionColor = Colors.white;
|
||||
if (isFileSelected &&
|
||||
file.isUploaded &&
|
||||
|
@ -421,25 +431,15 @@ class _LazyLoadingGridViewState extends State<LazyLoadingGridView> {
|
|||
selectionColor = avatarColors[(randomID).remainder(avatarColors.length)];
|
||||
}
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
if (widget.selectedFiles.files.isNotEmpty) {
|
||||
_selectFile(file);
|
||||
} else {
|
||||
if (AppLifecycleService.instance.mediaExtensionAction.action ==
|
||||
IntentAction.pick) {
|
||||
final ioFile = await getFile(file);
|
||||
MediaExtension().setResult("file://${ioFile!.path}");
|
||||
} else {
|
||||
_routeToDetailPage(file, context);
|
||||
}
|
||||
}
|
||||
onTap: () {
|
||||
widget.limitSelectionToOne
|
||||
? _onTapWithSelectionLimit(file)
|
||||
: _onTapNoSelectionLimit(file);
|
||||
},
|
||||
onLongPress: () {
|
||||
if (AppLifecycleService.instance.mediaExtensionAction.action ==
|
||||
IntentAction.main) {
|
||||
HapticFeedback.lightImpact();
|
||||
_selectFile(file);
|
||||
}
|
||||
widget.limitSelectionToOne
|
||||
? _onLongPressWithSelectionLimit(file)
|
||||
: _onLongPressNoSelectionLimit(file);
|
||||
},
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(1),
|
||||
|
@ -485,8 +485,50 @@ class _LazyLoadingGridViewState extends State<LazyLoadingGridView> {
|
|||
);
|
||||
}
|
||||
|
||||
void _selectFile(File file) {
|
||||
widget.selectedFiles.toggleSelection(file);
|
||||
void _toggleFileSelection(File file) {
|
||||
widget.selectedFiles!.toggleSelection(file);
|
||||
}
|
||||
|
||||
void _onTapNoSelectionLimit(File file) async {
|
||||
if (widget.selectedFiles?.files.isNotEmpty ?? false) {
|
||||
_toggleFileSelection(file);
|
||||
} else {
|
||||
if (AppLifecycleService.instance.mediaExtensionAction.action ==
|
||||
IntentAction.pick) {
|
||||
final ioFile = await getFile(file);
|
||||
MediaExtension().setResult("file://${ioFile!.path}");
|
||||
} else {
|
||||
_routeToDetailPage(file, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _onTapWithSelectionLimit(File file) {
|
||||
if (widget.selectedFiles!.files.isNotEmpty &&
|
||||
widget.selectedFiles!.files.first != file) {
|
||||
widget.selectedFiles!.clearAll();
|
||||
}
|
||||
_toggleFileSelection(file);
|
||||
}
|
||||
|
||||
void _onLongPressNoSelectionLimit(File file) {
|
||||
if (widget.selectedFiles!.files.isNotEmpty) {
|
||||
_routeToDetailPage(file, context);
|
||||
} else if (AppLifecycleService.instance.mediaExtensionAction.action ==
|
||||
IntentAction.main) {
|
||||
HapticFeedback.lightImpact();
|
||||
_toggleFileSelection(file);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onLongPressWithSelectionLimit(File file) async {
|
||||
if (AppLifecycleService.instance.mediaExtensionAction.action ==
|
||||
IntentAction.pick) {
|
||||
final ioFile = await getFile(file);
|
||||
MediaExtension().setResult("file://${ioFile!.path}");
|
||||
} else {
|
||||
_routeToDetailPage(file, context);
|
||||
}
|
||||
}
|
||||
|
||||
void _routeToDetailPage(File file, BuildContext context) {
|
||||
|
@ -502,14 +544,14 @@ class _LazyLoadingGridViewState extends State<LazyLoadingGridView> {
|
|||
}
|
||||
|
||||
void _selectedFilesListener() {
|
||||
if (widget.selectedFiles.files.containsAll(widget.filesInDay.toSet())) {
|
||||
if (widget.selectedFiles!.files.containsAll(widget.filesInDay.toSet())) {
|
||||
widget.areAllFilesSelected.value = true;
|
||||
} else {
|
||||
widget.areAllFilesSelected.value = false;
|
||||
}
|
||||
bool shouldRefresh = false;
|
||||
for (final file in widget.filesInDay) {
|
||||
if (widget.selectedFiles.isPartOfLastSelected(file)) {
|
||||
if (widget.selectedFiles!.isPartOfLastSelected(file)) {
|
||||
shouldRefresh = true;
|
||||
}
|
||||
}
|
||||
|
@ -519,12 +561,12 @@ class _LazyLoadingGridViewState extends State<LazyLoadingGridView> {
|
|||
}
|
||||
|
||||
void _toggleSelectAllFromDayListener() {
|
||||
if (widget.selectedFiles.files.containsAll(widget.filesInDay.toSet())) {
|
||||
if (widget.selectedFiles!.files.containsAll(widget.filesInDay.toSet())) {
|
||||
setState(() {
|
||||
widget.selectedFiles.unSelectAll(widget.filesInDay.toSet());
|
||||
widget.selectedFiles!.unSelectAll(widget.filesInDay.toSet());
|
||||
});
|
||||
} else {
|
||||
widget.selectedFiles.selectAll(widget.filesInDay.toSet());
|
||||
widget.selectedFiles!.selectAll(widget.filesInDay.toSet());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ import 'package:photos/core/event_bus.dart';
|
|||
import 'package:photos/db/files_db.dart';
|
||||
import 'package:photos/events/local_photos_updated_event.dart';
|
||||
import 'package:photos/models/file.dart' as ente;
|
||||
import 'package:photos/models/location.dart';
|
||||
import 'package:photos/models/location/location.dart';
|
||||
import 'package:photos/services/sync_service.dart';
|
||||
import 'package:photos/ui/common/loading_widget.dart';
|
||||
import 'package:photos/ui/components/action_sheet_widget.dart';
|
||||
|
@ -362,7 +362,10 @@ class _ImageEditorPageState extends State<ImageEditorPage> {
|
|||
final assetEntity = await widget.originalFile.getAsset;
|
||||
if (assetEntity != null) {
|
||||
final latLong = await assetEntity.latlngAsync();
|
||||
newFile.location = Location(latLong.latitude, latLong.longitude);
|
||||
newFile.location = Location(
|
||||
latitude: latLong.latitude,
|
||||
longitude: latLong.longitude,
|
||||
);
|
||||
}
|
||||
}
|
||||
newFile.generatedID = await FilesDB.instance.insert(newFile);
|
||||
|
|
|
@ -76,7 +76,7 @@ class FadingBottomBarState extends State<FadingBottomBar> {
|
|||
color: Colors.white,
|
||||
),
|
||||
onPressed: () async {
|
||||
await _displayInfo(widget.file);
|
||||
await _displayDetails(widget.file);
|
||||
safeRefresh(); //to instantly show the new caption if keypad is closed after pressing 'done' - here the caption will be updated before the bottom sheet is closed
|
||||
await Future.delayed(
|
||||
const Duration(milliseconds: 500),
|
||||
|
@ -268,7 +268,7 @@ class FadingBottomBarState extends State<FadingBottomBar> {
|
|||
);
|
||||
}
|
||||
|
||||
Future<void> _displayInfo(File file) async {
|
||||
await showInfoSheet(context, file);
|
||||
Future<void> _displayDetails(File file) async {
|
||||
await showDetailsSheet(context, file);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -66,7 +66,7 @@ class _FileCaptionWidgetState extends State<FileCaptionWidget> {
|
|||
final _focusNode = FocusNode();
|
||||
String? editedCaption;
|
||||
String hintText = fileCaptionDefaultHint;
|
||||
Widget? keyboardTopButtoms;
|
||||
Widget? keyboardTopButtons;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
@ -172,12 +172,12 @@ class _FileCaptionWidgetState extends State<FileCaptionWidget> {
|
|||
editedCaption = caption;
|
||||
}
|
||||
final bool hasFocus = _focusNode.hasFocus;
|
||||
keyboardTopButtoms ??= KeyboardTopButton(
|
||||
keyboardTopButtons ??= KeyboardTopButton(
|
||||
onDoneTap: onDoneTap,
|
||||
onCancelTap: onCancelTap,
|
||||
);
|
||||
if (hasFocus) {
|
||||
KeyboardOverlay.showOverlay(context, keyboardTopButtoms!);
|
||||
KeyboardOverlay.showOverlay(context, keyboardTopButtons!);
|
||||
} else {
|
||||
KeyboardOverlay.removeOverlay();
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ import 'package:photos/ui/viewer/file_details/backed_up_time_item_widget.dart';
|
|||
import "package:photos/ui/viewer/file_details/creation_time_item_widget.dart";
|
||||
import 'package:photos/ui/viewer/file_details/exif_item_widgets.dart';
|
||||
import "package:photos/ui/viewer/file_details/file_properties_item_widget.dart";
|
||||
import "package:photos/ui/viewer/file_details/location_tags_widget.dart";
|
||||
import "package:photos/ui/viewer/file_details/objects_item_widget.dart";
|
||||
import "package:photos/utils/exif_util.dart";
|
||||
|
||||
|
@ -39,12 +40,17 @@ class _FileDetailsWidgetState extends State<FileDetailsWidget> {
|
|||
"takenOnDevice": null,
|
||||
"exposureTime": null,
|
||||
"ISO": null,
|
||||
"megaPixels": null
|
||||
"megaPixels": null,
|
||||
"lat": null,
|
||||
"long": null,
|
||||
"latRef": null,
|
||||
"longRef": null,
|
||||
};
|
||||
|
||||
bool _isImage = false;
|
||||
late int _currentUserID;
|
||||
bool showExifListTile = false;
|
||||
bool hasGPSData = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
@ -52,6 +58,12 @@ class _FileDetailsWidgetState extends State<FileDetailsWidget> {
|
|||
_currentUserID = Configuration.instance.getUserID()!;
|
||||
_isImage = widget.file.fileType == FileType.image ||
|
||||
widget.file.fileType == FileType.livePhoto;
|
||||
_exifNotifier.addListener(() {
|
||||
if (_exifNotifier.value != null) {
|
||||
_generateExifForLocation(_exifNotifier.value!);
|
||||
hasGPSData = _haGPSData();
|
||||
}
|
||||
});
|
||||
if (_isImage) {
|
||||
_exifNotifier.addListener(() {
|
||||
if (_exifNotifier.value != null) {
|
||||
|
@ -63,10 +75,10 @@ class _FileDetailsWidgetState extends State<FileDetailsWidget> {
|
|||
_exifData["exposureTime"] != null ||
|
||||
_exifData["ISO"] != null;
|
||||
});
|
||||
getExif(widget.file).then((exif) {
|
||||
_exifNotifier.value = exif;
|
||||
});
|
||||
}
|
||||
getExif(widget.file).then((exif) {
|
||||
_exifNotifier.value = exif;
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
|
@ -125,6 +137,25 @@ class _FileDetailsWidgetState extends State<FileDetailsWidget> {
|
|||
},
|
||||
),
|
||||
);
|
||||
if (FeatureFlagService.instance.isInternalUserOrDebugBuild()) {
|
||||
fileDetailsTiles.addAll([
|
||||
ValueListenableBuilder(
|
||||
valueListenable: _exifNotifier,
|
||||
builder: (context, _, __) {
|
||||
return hasGPSData
|
||||
? Column(
|
||||
children: [
|
||||
LocationTagsWidget(
|
||||
widget.file.location!,
|
||||
),
|
||||
const FileDetailsDivider(),
|
||||
],
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
},
|
||||
)
|
||||
]);
|
||||
}
|
||||
if (_isImage) {
|
||||
fileDetailsTiles.addAll([
|
||||
ValueListenableBuilder(
|
||||
|
@ -200,6 +231,38 @@ class _FileDetailsWidgetState extends State<FileDetailsWidget> {
|
|||
);
|
||||
}
|
||||
|
||||
bool _haGPSData() {
|
||||
final fileLocation = widget.file.location;
|
||||
final hasLocation = (fileLocation != null &&
|
||||
fileLocation.latitude != null &&
|
||||
fileLocation.longitude != null) &&
|
||||
(fileLocation.latitude != 0 || fileLocation.longitude != 0);
|
||||
return hasLocation;
|
||||
}
|
||||
|
||||
void _generateExifForLocation(Map<String, IfdTag> exif) {
|
||||
if (exif["GPS GPSLatitude"] != null) {
|
||||
_exifData["lat"] = exif["GPS GPSLatitude"]!
|
||||
.values
|
||||
.toList()
|
||||
.map((e) => ((e as Ratio).numerator / e.denominator))
|
||||
.toList();
|
||||
}
|
||||
if (exif["GPS GPSLongitude"] != null) {
|
||||
_exifData["long"] = exif["GPS GPSLongitude"]!
|
||||
.values
|
||||
.toList()
|
||||
.map((e) => ((e as Ratio).numerator / e.denominator))
|
||||
.toList();
|
||||
}
|
||||
if (exif["GPS GPSLatitudeRef"] != null) {
|
||||
_exifData["latRef"] = exif["GPS GPSLatitudeRef"].toString();
|
||||
}
|
||||
if (exif["GPS GPSLongitudeRef"] != null) {
|
||||
_exifData["longRef"] = exif["GPS GPSLongitudeRef"].toString();
|
||||
}
|
||||
}
|
||||
|
||||
_generateExifForDetails(Map<String, IfdTag> exif) {
|
||||
if (exif["EXIF FocalLength"] != null) {
|
||||
_exifData["focalLength"] =
|
||||
|
|
118
lib/ui/viewer/file_details/location_tags_widget.dart
Normal file
118
lib/ui/viewer/file_details/location_tags_widget.dart
Normal file
|
@ -0,0 +1,118 @@
|
|||
import "dart:async";
|
||||
|
||||
import "package:flutter/material.dart";
|
||||
import "package:photos/core/event_bus.dart";
|
||||
import "package:photos/events/location_tag_updated_event.dart";
|
||||
import "package:photos/models/location/location.dart";
|
||||
import "package:photos/services/location_service.dart";
|
||||
import "package:photos/states/location_screen_state.dart";
|
||||
import "package:photos/ui/components/buttons/chip_button_widget.dart";
|
||||
import "package:photos/ui/components/buttons/inline_button_widget.dart";
|
||||
import "package:photos/ui/components/info_item_widget.dart";
|
||||
import 'package:photos/ui/viewer/location/add_location_sheet.dart';
|
||||
import "package:photos/ui/viewer/location/location_screen.dart";
|
||||
import "package:photos/utils/navigation_util.dart";
|
||||
|
||||
class LocationTagsWidget extends StatefulWidget {
|
||||
final Location centerPoint;
|
||||
const LocationTagsWidget(this.centerPoint, {super.key});
|
||||
|
||||
@override
|
||||
State<LocationTagsWidget> createState() => _LocationTagsWidgetState();
|
||||
}
|
||||
|
||||
class _LocationTagsWidgetState extends State<LocationTagsWidget> {
|
||||
String? title;
|
||||
IconData? leadingIcon;
|
||||
bool? hasChipButtons;
|
||||
late Future<List<Widget>> locationTagChips;
|
||||
late StreamSubscription<LocationTagUpdatedEvent> _locTagUpdateListener;
|
||||
@override
|
||||
void initState() {
|
||||
locationTagChips = _getLocationTags();
|
||||
_locTagUpdateListener =
|
||||
Bus.instance.on<LocationTagUpdatedEvent>().listen((event) {
|
||||
locationTagChips = _getLocationTags();
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_locTagUpdateListener.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
switchInCurve: Curves.easeInOutExpo,
|
||||
switchOutCurve: Curves.easeInOutExpo,
|
||||
child: InfoItemWidget(
|
||||
key: ValueKey(title),
|
||||
leadingIcon: leadingIcon ?? Icons.pin_drop_outlined,
|
||||
title: title,
|
||||
subtitleSection: locationTagChips,
|
||||
hasChipButtons: hasChipButtons ?? true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<Widget>> _getLocationTags() async {
|
||||
final locationTags = await LocationService.instance
|
||||
.enclosingLocationTags(widget.centerPoint);
|
||||
if (locationTags.isEmpty) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
title = "Add location";
|
||||
leadingIcon = Icons.add_location_alt_outlined;
|
||||
hasChipButtons = false;
|
||||
});
|
||||
}
|
||||
|
||||
return [
|
||||
InlineButtonWidget(
|
||||
"Group nearby photos",
|
||||
() => showAddLocationSheet(
|
||||
context,
|
||||
widget.centerPoint,
|
||||
),
|
||||
),
|
||||
];
|
||||
} else {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
title = "Location";
|
||||
leadingIcon = Icons.pin_drop_outlined;
|
||||
hasChipButtons = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
final result = locationTags
|
||||
.map(
|
||||
(locationTagEntity) => ChipButtonWidget(
|
||||
locationTagEntity.item.name,
|
||||
onTap: () {
|
||||
routeToPage(
|
||||
context,
|
||||
LocationScreenStateProvider(
|
||||
locationTagEntity,
|
||||
const LocationScreen(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
result.add(
|
||||
ChipButtonWidget(
|
||||
null,
|
||||
leadingIcon: Icons.add_outlined,
|
||||
onTap: () => showAddLocationSheet(context, widget.centerPoint),
|
||||
),
|
||||
);
|
||||
return result;
|
||||
}
|
||||
}
|
|
@ -33,18 +33,22 @@ class Gallery extends StatefulWidget {
|
|||
final Stream<FilesUpdatedEvent>? reloadEvent;
|
||||
final List<Stream<Event>>? forceReloadEvents;
|
||||
final Set<EventType> removalEventTypes;
|
||||
final SelectedFiles selectedFiles;
|
||||
final SelectedFiles? selectedFiles;
|
||||
final String tagPrefix;
|
||||
final Widget? header;
|
||||
final Widget? footer;
|
||||
final Widget emptyState;
|
||||
final String? albumName;
|
||||
final double scrollBottomSafeArea;
|
||||
final bool shouldCollateFilesByDay;
|
||||
final Widget loadingWidget;
|
||||
final bool disableScroll;
|
||||
final bool limitSelectionToOne;
|
||||
|
||||
const Gallery({
|
||||
required this.asyncLoader,
|
||||
required this.selectedFiles,
|
||||
required this.tagPrefix,
|
||||
this.selectedFiles,
|
||||
this.initialFiles,
|
||||
this.reloadEvent,
|
||||
this.forceReloadEvents,
|
||||
|
@ -54,6 +58,10 @@ class Gallery extends StatefulWidget {
|
|||
this.emptyState = const EmptyState(),
|
||||
this.scrollBottomSafeArea = 120.0,
|
||||
this.albumName = '',
|
||||
this.shouldCollateFilesByDay = true,
|
||||
this.loadingWidget = const EnteLoadingWidget(),
|
||||
this.disableScroll = false,
|
||||
this.limitSelectionToOne = false,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
|
@ -168,7 +176,8 @@ class _GalleryState extends State<Gallery> {
|
|||
|
||||
// Collates files and returns `true` if it resulted in a gallery reload
|
||||
bool _onFilesLoaded(List<File> files) {
|
||||
final updatedCollatedFiles = _collateFiles(files);
|
||||
final updatedCollatedFiles =
|
||||
widget.shouldCollateFilesByDay ? _collateFiles(files) : [files];
|
||||
if (_collatedFiles.length != updatedCollatedFiles.length ||
|
||||
_collatedFiles.isEmpty) {
|
||||
if (mounted) {
|
||||
|
@ -198,7 +207,7 @@ class _GalleryState extends State<Gallery> {
|
|||
Widget build(BuildContext context) {
|
||||
_logger.finest("Building Gallery ${widget.tagPrefix}");
|
||||
if (!_hasLoadedFiles) {
|
||||
return const EnteLoadingWidget();
|
||||
return widget.loadingWidget;
|
||||
}
|
||||
_photoGridSize = LocalSettings.instance.getPhotoGridSize();
|
||||
return _getListView();
|
||||
|
@ -211,6 +220,7 @@ class _GalleryState extends State<Gallery> {
|
|||
startIndex: 0,
|
||||
totalCount: _collatedFiles.length,
|
||||
isDraggableScrollbarEnabled: _collatedFiles.length > 10,
|
||||
disableScroll: widget.disableScroll,
|
||||
waitBuilder: (_) {
|
||||
return const EnteLoadingWidget();
|
||||
},
|
||||
|
@ -246,8 +256,10 @@ class _GalleryState extends State<Gallery> {
|
|||
.on<GalleryIndexUpdatedEvent>()
|
||||
.where((event) => event.tag == widget.tagPrefix)
|
||||
.map((event) => event.index),
|
||||
widget.shouldCollateFilesByDay,
|
||||
logTag: _logTag,
|
||||
photoGirdSize: _photoGridSize,
|
||||
limitSelectionToOne: widget.limitSelectionToOne,
|
||||
);
|
||||
if (widget.header != null && index == 0) {
|
||||
gallery = Column(children: [widget.header!, gallery]);
|
||||
|
|
265
lib/ui/viewer/location/add_location_sheet.dart
Normal file
265
lib/ui/viewer/location/add_location_sheet.dart
Normal file
|
@ -0,0 +1,265 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
|
||||
import "package:photos/core/constants.dart";
|
||||
import "package:photos/models/location/location.dart";
|
||||
import "package:photos/services/location_service.dart";
|
||||
import 'package:photos/states/location_state.dart';
|
||||
import "package:photos/theme/colors.dart";
|
||||
import "package:photos/theme/ente_theme.dart";
|
||||
import "package:photos/ui/common/loading_widget.dart";
|
||||
import "package:photos/ui/components/bottom_of_title_bar_widget.dart";
|
||||
import "package:photos/ui/components/buttons/button_widget.dart";
|
||||
import "package:photos/ui/components/divider_widget.dart";
|
||||
import "package:photos/ui/components/keyboard/keybiard_oveylay.dart";
|
||||
import "package:photos/ui/components/keyboard/keyboard_top_button.dart";
|
||||
import "package:photos/ui/components/models/button_type.dart";
|
||||
import "package:photos/ui/components/text_input_widget.dart";
|
||||
import "package:photos/ui/components/title_bar_title_widget.dart";
|
||||
import 'package:photos/ui/viewer/location/dynamic_location_gallery_widget.dart';
|
||||
import "package:photos/ui/viewer/location/radius_picker_widget.dart";
|
||||
|
||||
showAddLocationSheet(
|
||||
BuildContext context,
|
||||
Location coordinates,
|
||||
) {
|
||||
showBarModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return LocationTagStateProvider(
|
||||
centerPoint: coordinates,
|
||||
const AddLocationSheet(),
|
||||
);
|
||||
},
|
||||
shape: const RoundedRectangleBorder(
|
||||
side: BorderSide(width: 0),
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(5),
|
||||
),
|
||||
),
|
||||
topControl: const SizedBox.shrink(),
|
||||
backgroundColor: getEnteColorScheme(context).backgroundElevated,
|
||||
barrierColor: backdropFaintDark,
|
||||
);
|
||||
}
|
||||
|
||||
class AddLocationSheet extends StatefulWidget {
|
||||
const AddLocationSheet({super.key});
|
||||
|
||||
@override
|
||||
State<AddLocationSheet> createState() => _AddLocationSheetState();
|
||||
}
|
||||
|
||||
class _AddLocationSheetState extends State<AddLocationSheet> {
|
||||
//The value of these notifiers has no significance.
|
||||
//When memoriesCountNotifier is null, we show the loading widget in the
|
||||
//memories count section which also means the gallery is loading.
|
||||
final ValueNotifier<int?> _memoriesCountNotifier = ValueNotifier(null);
|
||||
final ValueNotifier<bool> _submitNotifer = ValueNotifier(false);
|
||||
final ValueNotifier<bool> _cancelNotifier = ValueNotifier(false);
|
||||
final ValueNotifier<int> _selectedRadiusIndexNotifier =
|
||||
ValueNotifier(defaultRadiusValueIndex);
|
||||
final _focusNode = FocusNode();
|
||||
final _textEditingController = TextEditingController();
|
||||
final _isEmptyNotifier = ValueNotifier(true);
|
||||
Widget? _keyboardTopButtons;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_focusNode.addListener(_focusNodeListener);
|
||||
_selectedRadiusIndexNotifier.addListener(_selectedRadiusIndexListener);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_focusNode.removeListener(_focusNodeListener);
|
||||
_submitNotifer.dispose();
|
||||
_cancelNotifier.dispose();
|
||||
_selectedRadiusIndexNotifier.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textTheme = getEnteTextTheme(context);
|
||||
final colorScheme = getEnteColorScheme(context);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0, 32, 0, 8),
|
||||
child: Column(
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(bottom: 16),
|
||||
child: BottomOfTitleBarWidget(
|
||||
title: TitleBarTitleWidget(title: "Add location"),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(
|
||||
decelerationRate: ScrollDecelerationRate.fast,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextInputWidget(
|
||||
hintText: "Location name",
|
||||
borderRadius: 2,
|
||||
focusNode: _focusNode,
|
||||
submitNotifier: _submitNotifer,
|
||||
cancelNotifier: _cancelNotifier,
|
||||
popNavAfterSubmission: false,
|
||||
shouldUnfocusOnClearOrSubmit: true,
|
||||
alwaysShowSuccessState: true,
|
||||
textEditingController: _textEditingController,
|
||||
isEmptyNotifier: _isEmptyNotifier,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ValueListenableBuilder(
|
||||
valueListenable: _isEmptyNotifier,
|
||||
builder: (context, bool value, _) {
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
switchInCurve: Curves.easeInOut,
|
||||
switchOutCurve: Curves.easeInOut,
|
||||
child: ButtonWidget(
|
||||
key: ValueKey(value),
|
||||
buttonType: ButtonType.secondary,
|
||||
buttonSize: ButtonSize.small,
|
||||
labelText: "Add",
|
||||
isDisabled: value,
|
||||
onTap: () async {
|
||||
_focusNode.unfocus();
|
||||
await _addLocationTag();
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
RadiusPickerWidget(
|
||||
_selectedRadiusIndexNotifier,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
"A location tag groups all photos that were taken within some radius of a photo",
|
||||
style: textTheme.smallMuted,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const DividerWidget(
|
||||
dividerType: DividerType.solid,
|
||||
padding: EdgeInsets.only(top: 24, bottom: 20),
|
||||
),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: _memoriesCountNotifier,
|
||||
builder: (context, value, _) {
|
||||
Widget widget;
|
||||
if (value == null) {
|
||||
widget = RepaintBoundary(
|
||||
child: EnteLoadingWidget(
|
||||
size: 14,
|
||||
color: colorScheme.strokeMuted,
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: 3,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
widget = Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
value == 1 ? "1 memory" : "$value memories",
|
||||
style: textTheme.body,
|
||||
),
|
||||
if (value as int > 1000)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 2),
|
||||
child: Text(
|
||||
"Up to 1000 memories shown in gallery",
|
||||
style: textTheme.miniMuted,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
return Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
switchInCurve: Curves.easeInOutExpo,
|
||||
switchOutCurve: Curves.easeInOutExpo,
|
||||
child: widget,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
DynamicLocationGalleryWidget(
|
||||
_memoriesCountNotifier,
|
||||
"Add_location",
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _addLocationTag() async {
|
||||
final locationData = InheritedLocationTagData.of(context);
|
||||
final coordinates = locationData.centerPoint;
|
||||
final radius = radiusValues[locationData.selectedRadiusIndex];
|
||||
await LocationService.instance.addLocation(
|
||||
_textEditingController.text.trim(),
|
||||
coordinates,
|
||||
radius,
|
||||
);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
void _focusNodeListener() {
|
||||
final bool hasFocus = _focusNode.hasFocus;
|
||||
_keyboardTopButtons ??= KeyboardTopButton(
|
||||
onDoneTap: () {
|
||||
_submitNotifer.value = !_submitNotifer.value;
|
||||
},
|
||||
onCancelTap: () {
|
||||
_cancelNotifier.value = !_cancelNotifier.value;
|
||||
},
|
||||
);
|
||||
if (hasFocus) {
|
||||
KeyboardOverlay.showOverlay(context, _keyboardTopButtons!);
|
||||
} else {
|
||||
KeyboardOverlay.removeOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
void _selectedRadiusIndexListener() {
|
||||
InheritedLocationTagData.of(
|
||||
context,
|
||||
).updateSelectedIndex(
|
||||
_selectedRadiusIndexNotifier.value,
|
||||
);
|
||||
_memoriesCountNotifier.value = null;
|
||||
}
|
||||
}
|
143
lib/ui/viewer/location/dynamic_location_gallery_widget.dart
Normal file
143
lib/ui/viewer/location/dynamic_location_gallery_widget.dart
Normal file
|
@ -0,0 +1,143 @@
|
|||
import "dart:developer" as dev;
|
||||
import "dart:math";
|
||||
|
||||
import "package:flutter/material.dart";
|
||||
import "package:photos/core/constants.dart";
|
||||
import "package:photos/db/files_db.dart";
|
||||
import "package:photos/models/file.dart";
|
||||
import "package:photos/models/file_load_result.dart";
|
||||
import "package:photos/services/collections_service.dart";
|
||||
import "package:photos/services/files_service.dart";
|
||||
import "package:photos/services/location_service.dart";
|
||||
import 'package:photos/states/location_state.dart';
|
||||
import "package:photos/ui/viewer/gallery/gallery.dart";
|
||||
import "package:photos/utils/local_settings.dart";
|
||||
|
||||
///This gallery will get rebuilt with the updated radius when
|
||||
///InheritedLocationTagData notifies a change in radius.
|
||||
class DynamicLocationGalleryWidget extends StatefulWidget {
|
||||
final ValueNotifier<int?> memoriesCountNotifier;
|
||||
final String tagPrefix;
|
||||
const DynamicLocationGalleryWidget(
|
||||
this.memoriesCountNotifier,
|
||||
this.tagPrefix, {
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<DynamicLocationGalleryWidget> createState() =>
|
||||
_DynamicLocationGalleryWidgetState();
|
||||
}
|
||||
|
||||
class _DynamicLocationGalleryWidgetState
|
||||
extends State<DynamicLocationGalleryWidget> {
|
||||
late final Future<FileLoadResult> fileLoadResult;
|
||||
late Future<void> removeIgnoredFiles;
|
||||
double heightOfGallery = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
final collectionsToHide =
|
||||
CollectionsService.instance.collectionsHiddenFromTimeline();
|
||||
fileLoadResult = FilesDB.instance.getAllUploadedAndSharedFiles(
|
||||
galleryLoadStartTime,
|
||||
galleryLoadEndTime,
|
||||
limit: null,
|
||||
asc: false,
|
||||
ignoredCollectionIDs: collectionsToHide,
|
||||
);
|
||||
removeIgnoredFiles =
|
||||
FilesService.instance.removeIgnoredFiles(fileLoadResult);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const galleryFilesLimit = 1000;
|
||||
final selectedRadius = _selectedRadius();
|
||||
Future<FileLoadResult> filterFiles() async {
|
||||
final FileLoadResult result = await fileLoadResult;
|
||||
//wait for ignored files to be removed after init
|
||||
await removeIgnoredFiles;
|
||||
final stopWatch = Stopwatch()..start();
|
||||
final copyOfFiles = List<File>.from(result.files);
|
||||
copyOfFiles.removeWhere((f) {
|
||||
return !LocationService.instance.isFileInsideLocationTag(
|
||||
InheritedLocationTagData.of(context).centerPoint,
|
||||
f.location!,
|
||||
selectedRadius,
|
||||
);
|
||||
});
|
||||
dev.log(
|
||||
"Time taken to get all files in a location tag: ${stopWatch.elapsedMilliseconds} ms",
|
||||
);
|
||||
stopWatch.stop();
|
||||
widget.memoriesCountNotifier.value = copyOfFiles.length;
|
||||
final limitedResults = copyOfFiles.take(galleryFilesLimit).toList();
|
||||
|
||||
return Future.value(
|
||||
FileLoadResult(
|
||||
limitedResults,
|
||||
result.hasMore,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return FutureBuilder(
|
||||
//Only rebuild Gallery if the center point or radius changes
|
||||
key: ValueKey(
|
||||
"${InheritedLocationTagData.of(context).centerPoint}$selectedRadius",
|
||||
),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return SizedBox(
|
||||
height: _galleryHeight(
|
||||
min(
|
||||
(widget.memoriesCountNotifier.value ?? 0),
|
||||
galleryFilesLimit,
|
||||
),
|
||||
),
|
||||
child: Gallery(
|
||||
loadingWidget: const SizedBox.shrink(),
|
||||
disableScroll: true,
|
||||
asyncLoader: (
|
||||
creationStartTime,
|
||||
creationEndTime, {
|
||||
limit,
|
||||
asc,
|
||||
}) async {
|
||||
return snapshot.data as FileLoadResult;
|
||||
},
|
||||
tagPrefix: widget.tagPrefix,
|
||||
shouldCollateFilesByDay: false,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
},
|
||||
future: filterFiles(),
|
||||
);
|
||||
}
|
||||
|
||||
int _selectedRadius() {
|
||||
return radiusValues[
|
||||
InheritedLocationTagData.of(context).selectedRadiusIndex];
|
||||
}
|
||||
|
||||
double _galleryHeight(int fileCount) {
|
||||
final photoGridSize = LocalSettings.instance.getPhotoGridSize();
|
||||
final totalWhiteSpaceBetweenPhotos =
|
||||
galleryGridSpacing * (photoGridSize - 1);
|
||||
|
||||
final thumbnailHeight =
|
||||
((MediaQuery.of(context).size.width - totalWhiteSpaceBetweenPhotos) /
|
||||
photoGridSize);
|
||||
|
||||
final numberOfRows = (fileCount / photoGridSize).ceil();
|
||||
|
||||
final galleryHeight = (thumbnailHeight * numberOfRows) +
|
||||
(galleryGridSpacing * (numberOfRows - 1));
|
||||
return galleryHeight + 120;
|
||||
}
|
||||
}
|
69
lib/ui/viewer/location/edit_center_point_tile_widget.dart
Normal file
69
lib/ui/viewer/location/edit_center_point_tile_widget.dart
Normal file
|
@ -0,0 +1,69 @@
|
|||
import "package:flutter/material.dart";
|
||||
import "package:photos/models/file.dart";
|
||||
import "package:photos/services/location_service.dart";
|
||||
import "package:photos/states/location_state.dart";
|
||||
import "package:photos/theme/ente_theme.dart";
|
||||
import "package:photos/ui/viewer/location/pick_center_point_widget.dart";
|
||||
|
||||
class EditCenterPointTileWidget extends StatelessWidget {
|
||||
const EditCenterPointTileWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textTheme = getEnteTextTheme(context);
|
||||
final colorScheme = getEnteColorScheme(context);
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
color: colorScheme.fillFaint,
|
||||
child: Icon(
|
||||
Icons.location_on_outlined,
|
||||
color: colorScheme.strokeFaint,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 4.5, 16, 4.5),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Center point",
|
||||
style: textTheme.body,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
LocationService.instance.convertLocationToDMS(
|
||||
InheritedLocationTagData.of(context)
|
||||
.locationTagEntity!
|
||||
.item
|
||||
.centerPoint,
|
||||
),
|
||||
style: textTheme.miniMuted,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
final File? centerPointFile = await showPickCenterPointSheet(
|
||||
context,
|
||||
InheritedLocationTagData.of(context).locationTagEntity!,
|
||||
);
|
||||
if (centerPointFile != null) {
|
||||
InheritedLocationTagData.of(context)
|
||||
.updateCenterPoint(centerPointFile.location!);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.edit),
|
||||
color: getEnteColorScheme(context).strokeMuted,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
274
lib/ui/viewer/location/edit_location_sheet.dart
Normal file
274
lib/ui/viewer/location/edit_location_sheet.dart
Normal file
|
@ -0,0 +1,274 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
|
||||
import "package:photos/core/constants.dart";
|
||||
import "package:photos/models/local_entity_data.dart";
|
||||
import "package:photos/models/location_tag/location_tag.dart";
|
||||
import "package:photos/services/location_service.dart";
|
||||
import "package:photos/states/location_state.dart";
|
||||
import "package:photos/theme/colors.dart";
|
||||
import "package:photos/theme/ente_theme.dart";
|
||||
import "package:photos/ui/common/loading_widget.dart";
|
||||
import "package:photos/ui/components/bottom_of_title_bar_widget.dart";
|
||||
import "package:photos/ui/components/buttons/button_widget.dart";
|
||||
import "package:photos/ui/components/divider_widget.dart";
|
||||
import "package:photos/ui/components/keyboard/keybiard_oveylay.dart";
|
||||
import "package:photos/ui/components/keyboard/keyboard_top_button.dart";
|
||||
import "package:photos/ui/components/models/button_type.dart";
|
||||
import "package:photos/ui/components/text_input_widget.dart";
|
||||
import "package:photos/ui/components/title_bar_title_widget.dart";
|
||||
import 'package:photos/ui/viewer/location/dynamic_location_gallery_widget.dart';
|
||||
import "package:photos/ui/viewer/location/edit_center_point_tile_widget.dart";
|
||||
import "package:photos/ui/viewer/location/radius_picker_widget.dart";
|
||||
|
||||
showEditLocationSheet(
|
||||
BuildContext context,
|
||||
LocalEntity<LocationTag> locationTagEntity,
|
||||
) {
|
||||
showBarModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return LocationTagStateProvider(
|
||||
locationTagEntity: locationTagEntity,
|
||||
const EditLocationSheet(),
|
||||
);
|
||||
},
|
||||
shape: const RoundedRectangleBorder(
|
||||
side: BorderSide(width: 0),
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(5),
|
||||
),
|
||||
),
|
||||
topControl: const SizedBox.shrink(),
|
||||
backgroundColor: getEnteColorScheme(context).backgroundElevated,
|
||||
barrierColor: backdropFaintDark,
|
||||
);
|
||||
}
|
||||
|
||||
class EditLocationSheet extends StatefulWidget {
|
||||
const EditLocationSheet({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<EditLocationSheet> createState() => _EditLocationSheetState();
|
||||
}
|
||||
|
||||
class _EditLocationSheetState extends State<EditLocationSheet> {
|
||||
//The value of these notifiers has no significance.
|
||||
//When memoriesCountNotifier is null, we show the loading widget in the
|
||||
//memories count section which also means the gallery is loading.
|
||||
final ValueNotifier<int?> _memoriesCountNotifier = ValueNotifier(null);
|
||||
final ValueNotifier<bool> _submitNotifer = ValueNotifier(false);
|
||||
final ValueNotifier<bool> _cancelNotifier = ValueNotifier(false);
|
||||
final ValueNotifier<int> _selectedRadiusIndexNotifier =
|
||||
ValueNotifier(defaultRadiusValueIndex);
|
||||
final _focusNode = FocusNode();
|
||||
final _textEditingController = TextEditingController();
|
||||
final _isEmptyNotifier = ValueNotifier(false);
|
||||
Widget? _keyboardTopButtons;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_focusNode.addListener(_focusNodeListener);
|
||||
_selectedRadiusIndexNotifier.addListener(_selectedRadiusIndexListener);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_focusNode.removeListener(_focusNodeListener);
|
||||
_submitNotifer.dispose();
|
||||
_cancelNotifier.dispose();
|
||||
_selectedRadiusIndexNotifier.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textTheme = getEnteTextTheme(context);
|
||||
final colorScheme = getEnteColorScheme(context);
|
||||
final locationName =
|
||||
InheritedLocationTagData.of(context).locationTagEntity!.item.name;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0, 32, 0, 8),
|
||||
child: Column(
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(bottom: 16),
|
||||
child: BottomOfTitleBarWidget(
|
||||
title: TitleBarTitleWidget(title: "Edit location"),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(
|
||||
decelerationRate: ScrollDecelerationRate.fast,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextInputWidget(
|
||||
hintText: "Location name",
|
||||
borderRadius: 2,
|
||||
focusNode: _focusNode,
|
||||
submitNotifier: _submitNotifer,
|
||||
cancelNotifier: _cancelNotifier,
|
||||
popNavAfterSubmission: false,
|
||||
shouldUnfocusOnClearOrSubmit: true,
|
||||
alwaysShowSuccessState: true,
|
||||
initialValue: locationName,
|
||||
onCancel: () {
|
||||
_focusNode.unfocus();
|
||||
_textEditingController.value =
|
||||
TextEditingValue(text: locationName);
|
||||
},
|
||||
textEditingController: _textEditingController,
|
||||
isEmptyNotifier: _isEmptyNotifier,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ValueListenableBuilder(
|
||||
valueListenable: _isEmptyNotifier,
|
||||
builder: (context, bool value, _) {
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
switchInCurve: Curves.easeInOut,
|
||||
switchOutCurve: Curves.easeInOut,
|
||||
child: ButtonWidget(
|
||||
key: ValueKey(value),
|
||||
buttonType: ButtonType.secondary,
|
||||
buttonSize: ButtonSize.small,
|
||||
labelText: "Save",
|
||||
isDisabled: value,
|
||||
onTap: () async {
|
||||
_focusNode.unfocus();
|
||||
await _editLocation();
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const EditCenterPointTileWidget(),
|
||||
const SizedBox(height: 20),
|
||||
RadiusPickerWidget(
|
||||
_selectedRadiusIndexNotifier,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
),
|
||||
),
|
||||
const DividerWidget(
|
||||
dividerType: DividerType.solid,
|
||||
padding: EdgeInsets.only(top: 24, bottom: 20),
|
||||
),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: _memoriesCountNotifier,
|
||||
builder: (context, value, _) {
|
||||
Widget widget;
|
||||
if (value == null) {
|
||||
widget = RepaintBoundary(
|
||||
child: EnteLoadingWidget(
|
||||
size: 14,
|
||||
color: colorScheme.strokeMuted,
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: 3,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
widget = Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
value == 1 ? "1 memory" : "$value memories",
|
||||
style: textTheme.body,
|
||||
),
|
||||
if (value as int > 1000)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 2),
|
||||
child: Text(
|
||||
"Up to 1000 memories shown in gallery",
|
||||
style: textTheme.miniMuted,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
return Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
switchInCurve: Curves.easeInOutExpo,
|
||||
switchOutCurve: Curves.easeInOutExpo,
|
||||
child: widget,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
DynamicLocationGalleryWidget(
|
||||
_memoriesCountNotifier,
|
||||
"Edit_location",
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _editLocation() async {
|
||||
final locationTagState = InheritedLocationTagData.of(context);
|
||||
await LocationService.instance.updateLocationTag(
|
||||
locationTagEntity: locationTagState.locationTagEntity!,
|
||||
newRadius: radiusValues[locationTagState.selectedRadiusIndex],
|
||||
newName: _textEditingController.text.trim(),
|
||||
newCenterPoint: InheritedLocationTagData.of(context).centerPoint,
|
||||
);
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
void _focusNodeListener() {
|
||||
final bool hasFocus = _focusNode.hasFocus;
|
||||
_keyboardTopButtons ??= KeyboardTopButton(
|
||||
onDoneTap: () {
|
||||
_submitNotifer.value = !_submitNotifer.value;
|
||||
},
|
||||
onCancelTap: () {
|
||||
_cancelNotifier.value = !_cancelNotifier.value;
|
||||
},
|
||||
);
|
||||
if (hasFocus) {
|
||||
KeyboardOverlay.showOverlay(context, _keyboardTopButtons!);
|
||||
} else {
|
||||
KeyboardOverlay.removeOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
void _selectedRadiusIndexListener() {
|
||||
InheritedLocationTagData.of(
|
||||
context,
|
||||
).updateSelectedIndex(
|
||||
_selectedRadiusIndexNotifier.value,
|
||||
);
|
||||
_memoriesCountNotifier.value = null;
|
||||
}
|
||||
}
|
300
lib/ui/viewer/location/location_screen.dart
Normal file
300
lib/ui/viewer/location/location_screen.dart
Normal file
|
@ -0,0 +1,300 @@
|
|||
import 'dart:developer' as dev;
|
||||
import "package:flutter/material.dart";
|
||||
import "package:photos/core/constants.dart";
|
||||
import "package:photos/core/event_bus.dart";
|
||||
import "package:photos/db/files_db.dart";
|
||||
import "package:photos/events/files_updated_event.dart";
|
||||
import "package:photos/events/local_photos_updated_event.dart";
|
||||
import "package:photos/models/file.dart";
|
||||
import "package:photos/models/file_load_result.dart";
|
||||
import "package:photos/models/gallery_type.dart";
|
||||
import "package:photos/models/selected_files.dart";
|
||||
import "package:photos/services/collections_service.dart";
|
||||
import "package:photos/services/files_service.dart";
|
||||
import "package:photos/services/location_service.dart";
|
||||
import "package:photos/states/location_screen_state.dart";
|
||||
import "package:photos/theme/colors.dart";
|
||||
import "package:photos/theme/ente_theme.dart";
|
||||
import "package:photos/ui/common/loading_widget.dart";
|
||||
import "package:photos/ui/components/buttons/icon_button_widget.dart";
|
||||
import "package:photos/ui/components/title_bar_title_widget.dart";
|
||||
import "package:photos/ui/components/title_bar_widget.dart";
|
||||
import "package:photos/ui/viewer/actions/file_selection_overlay_bar.dart";
|
||||
import "package:photos/ui/viewer/gallery/gallery.dart";
|
||||
import "package:photos/ui/viewer/location/edit_location_sheet.dart";
|
||||
import "package:photos/utils/dialog_util.dart";
|
||||
|
||||
class LocationScreen extends StatelessWidget {
|
||||
const LocationScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: const PreferredSize(
|
||||
preferredSize: Size(double.infinity, 48),
|
||||
child: TitleBarWidget(
|
||||
isSliver: false,
|
||||
isFlexibleSpaceDisabled: true,
|
||||
actionIcons: [LocationScreenPopUpMenu()],
|
||||
),
|
||||
),
|
||||
body: Column(
|
||||
children: <Widget>[
|
||||
SizedBox(
|
||||
height: MediaQuery.of(context).size.height - 102,
|
||||
width: double.infinity,
|
||||
child: const LocationGalleryWidget(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LocationScreenPopUpMenu extends StatelessWidget {
|
||||
const LocationScreenPopUpMenu({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textTheme = getEnteTextTheme(context);
|
||||
final colorScheme = getEnteColorScheme(context);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 4),
|
||||
child: Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
highlightColor: Colors.transparent,
|
||||
splashColor: Colors.transparent,
|
||||
),
|
||||
child: PopupMenuButton(
|
||||
elevation: 2,
|
||||
offset: const Offset(10, 50),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
color: colorScheme.backgroundElevated2,
|
||||
child: const IconButtonWidget(
|
||||
icon: Icons.more_horiz,
|
||||
iconButtonType: IconButtonType.primary,
|
||||
disableGestureDetector: true,
|
||||
),
|
||||
itemBuilder: (context) {
|
||||
return [
|
||||
PopupMenuItem(
|
||||
value: "edit",
|
||||
child: Text(
|
||||
"Edit",
|
||||
style: textTheme.bodyBold,
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
onTap: () {},
|
||||
value: "delete",
|
||||
child: Text(
|
||||
"Delete Location",
|
||||
style: textTheme.bodyBold.copyWith(color: warning500),
|
||||
),
|
||||
),
|
||||
];
|
||||
},
|
||||
onSelected: (value) async {
|
||||
if (value == "edit") {
|
||||
showEditLocationSheet(
|
||||
context,
|
||||
InheritedLocationScreenState.of(context).locationTagEntity,
|
||||
);
|
||||
} else if (value == "delete") {
|
||||
try {
|
||||
await LocationService.instance.deleteLocationTag(
|
||||
InheritedLocationScreenState.of(context).locationTagEntity.id,
|
||||
);
|
||||
Navigator.of(context).pop();
|
||||
} catch (e) {
|
||||
showGenericErrorDialog(context: context);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LocationGalleryWidget extends StatefulWidget {
|
||||
const LocationGalleryWidget({super.key});
|
||||
|
||||
@override
|
||||
State<LocationGalleryWidget> createState() => _LocationGalleryWidgetState();
|
||||
}
|
||||
|
||||
class _LocationGalleryWidgetState extends State<LocationGalleryWidget> {
|
||||
late final Future<FileLoadResult> fileLoadResult;
|
||||
late Future<void> removeIgnoredFiles;
|
||||
late Widget galleryHeaderWidget;
|
||||
final _selectedFiles = SelectedFiles();
|
||||
@override
|
||||
void initState() {
|
||||
final collectionsToHide =
|
||||
CollectionsService.instance.collectionsHiddenFromTimeline();
|
||||
fileLoadResult = FilesDB.instance.getAllUploadedAndSharedFiles(
|
||||
galleryLoadStartTime,
|
||||
galleryLoadEndTime,
|
||||
limit: null,
|
||||
asc: false,
|
||||
ignoredCollectionIDs: collectionsToHide,
|
||||
);
|
||||
removeIgnoredFiles =
|
||||
FilesService.instance.removeIgnoredFiles(fileLoadResult);
|
||||
galleryHeaderWidget = const GalleryHeaderWidget();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
InheritedLocationScreenState.memoryCountNotifier.value = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final selectedRadius =
|
||||
InheritedLocationScreenState.of(context).locationTagEntity.item.radius;
|
||||
final centerPoint = InheritedLocationScreenState.of(context)
|
||||
.locationTagEntity
|
||||
.item
|
||||
.centerPoint;
|
||||
Future<FileLoadResult> filterFiles() async {
|
||||
final FileLoadResult result = await fileLoadResult;
|
||||
//wait for ignored files to be removed after init
|
||||
await removeIgnoredFiles;
|
||||
final stopWatch = Stopwatch()..start();
|
||||
final copyOfFiles = List<File>.from(result.files);
|
||||
copyOfFiles.removeWhere((f) {
|
||||
return !LocationService.instance.isFileInsideLocationTag(
|
||||
centerPoint,
|
||||
f.location!,
|
||||
selectedRadius,
|
||||
);
|
||||
});
|
||||
dev.log(
|
||||
"Time taken to get all files in a location tag: ${stopWatch.elapsedMilliseconds} ms",
|
||||
);
|
||||
stopWatch.stop();
|
||||
InheritedLocationScreenState.memoryCountNotifier.value =
|
||||
copyOfFiles.length;
|
||||
|
||||
return Future.value(
|
||||
FileLoadResult(
|
||||
copyOfFiles,
|
||||
result.hasMore,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return FutureBuilder(
|
||||
//rebuild gallery only when there is change in radius or center point
|
||||
key: ValueKey("$centerPoint$selectedRadius"),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return Stack(
|
||||
children: [
|
||||
Gallery(
|
||||
loadingWidget: Column(
|
||||
children: [
|
||||
galleryHeaderWidget,
|
||||
EnteLoadingWidget(
|
||||
color: getEnteColorScheme(context).strokeMuted,
|
||||
),
|
||||
],
|
||||
),
|
||||
header: galleryHeaderWidget,
|
||||
asyncLoader: (
|
||||
creationStartTime,
|
||||
creationEndTime, {
|
||||
limit,
|
||||
asc,
|
||||
}) async {
|
||||
return snapshot.data as FileLoadResult;
|
||||
},
|
||||
reloadEvent: Bus.instance.on<LocalPhotosUpdatedEvent>(),
|
||||
removalEventTypes: const {
|
||||
EventType.deletedFromRemote,
|
||||
EventType.deletedFromEverywhere,
|
||||
},
|
||||
selectedFiles: _selectedFiles,
|
||||
tagPrefix: "location_gallery",
|
||||
),
|
||||
FileSelectionOverlayBar(
|
||||
GalleryType.locationTag,
|
||||
_selectedFiles,
|
||||
)
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return Column(
|
||||
children: [
|
||||
galleryHeaderWidget,
|
||||
const Expanded(
|
||||
child: EnteLoadingWidget(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
},
|
||||
future: filterFiles(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GalleryHeaderWidget extends StatefulWidget {
|
||||
const GalleryHeaderWidget({super.key});
|
||||
|
||||
@override
|
||||
State<GalleryHeaderWidget> createState() => _GalleryHeaderWidgetState();
|
||||
}
|
||||
|
||||
class _GalleryHeaderWidgetState extends State<GalleryHeaderWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final locationName =
|
||||
InheritedLocationScreenState.of(context).locationTagEntity.item.name;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 20),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
key: ValueKey(locationName),
|
||||
width: double.infinity,
|
||||
child: TitleBarTitleWidget(
|
||||
title: locationName,
|
||||
),
|
||||
),
|
||||
ValueListenableBuilder(
|
||||
valueListenable: InheritedLocationScreenState.memoryCountNotifier,
|
||||
builder: (context, value, _) {
|
||||
if (value == null) {
|
||||
return RepaintBoundary(
|
||||
child: EnteLoadingWidget(
|
||||
size: 12,
|
||||
color: getEnteColorScheme(context).strokeMuted,
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: 2.5,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return Text(
|
||||
value == 1 ? "1 memory" : "$value memories",
|
||||
style: getEnteTextTheme(context).smallMuted,
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
196
lib/ui/viewer/location/pick_center_point_widget.dart
Normal file
196
lib/ui/viewer/location/pick_center_point_widget.dart
Normal file
|
@ -0,0 +1,196 @@
|
|||
import "dart:math";
|
||||
|
||||
import "package:flutter/material.dart";
|
||||
import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
|
||||
import "package:photos/core/configuration.dart";
|
||||
import "package:photos/db/files_db.dart";
|
||||
import "package:photos/models/file.dart";
|
||||
import "package:photos/models/file_load_result.dart";
|
||||
import "package:photos/models/local_entity_data.dart";
|
||||
import "package:photos/models/location_tag/location_tag.dart";
|
||||
import "package:photos/models/selected_files.dart";
|
||||
import "package:photos/services/collections_service.dart";
|
||||
import "package:photos/services/ignored_files_service.dart";
|
||||
import "package:photos/theme/colors.dart";
|
||||
import "package:photos/theme/ente_theme.dart";
|
||||
import "package:photos/ui/components/bottom_of_title_bar_widget.dart";
|
||||
import "package:photos/ui/components/buttons/button_widget.dart";
|
||||
import "package:photos/ui/components/models/button_type.dart";
|
||||
import "package:photos/ui/components/title_bar_title_widget.dart";
|
||||
import "package:photos/ui/viewer/gallery/gallery.dart";
|
||||
|
||||
Future<File?> showPickCenterPointSheet(
|
||||
BuildContext context,
|
||||
LocalEntity<LocationTag> locationTagEntity,
|
||||
) async {
|
||||
return await showBarModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return PickCenterPointWidget(locationTagEntity);
|
||||
},
|
||||
shape: const RoundedRectangleBorder(
|
||||
side: BorderSide(width: 0),
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(5),
|
||||
),
|
||||
),
|
||||
topControl: const SizedBox.shrink(),
|
||||
backgroundColor: getEnteColorScheme(context).backgroundElevated,
|
||||
barrierColor: backdropFaintDark,
|
||||
enableDrag: false,
|
||||
);
|
||||
}
|
||||
|
||||
class PickCenterPointWidget extends StatelessWidget {
|
||||
final LocalEntity<LocationTag> locationTagEntity;
|
||||
|
||||
const PickCenterPointWidget(
|
||||
this.locationTagEntity, {
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ValueNotifier<bool> isFileSelected = ValueNotifier(false);
|
||||
final selectedFiles = SelectedFiles();
|
||||
selectedFiles.addListener(() {
|
||||
isFileSelected.value = selectedFiles.files.isNotEmpty;
|
||||
});
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: min(428, MediaQuery.of(context).size.width),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0, 32, 0, 8),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
BottomOfTitleBarWidget(
|
||||
title: const TitleBarTitleWidget(
|
||||
title: "Pick center point",
|
||||
),
|
||||
caption: locationTagEntity.item.name,
|
||||
),
|
||||
Expanded(
|
||||
child: Gallery(
|
||||
asyncLoader: (
|
||||
creationStartTime,
|
||||
creationEndTime, {
|
||||
limit,
|
||||
asc,
|
||||
}) async {
|
||||
final ownerID =
|
||||
Configuration.instance.getUserID();
|
||||
final hasSelectedAllForBackup = Configuration
|
||||
.instance
|
||||
.hasSelectedAllFoldersForBackup();
|
||||
final collectionsToHide = CollectionsService
|
||||
.instance
|
||||
.collectionsHiddenFromTimeline();
|
||||
FileLoadResult result;
|
||||
if (hasSelectedAllForBackup) {
|
||||
result = await FilesDB.instance
|
||||
.getAllLocalAndUploadedFiles(
|
||||
creationStartTime,
|
||||
creationEndTime,
|
||||
ownerID!,
|
||||
limit: limit,
|
||||
asc: asc,
|
||||
ignoredCollectionIDs: collectionsToHide,
|
||||
);
|
||||
} else {
|
||||
result = await FilesDB.instance
|
||||
.getAllPendingOrUploadedFiles(
|
||||
creationStartTime,
|
||||
creationEndTime,
|
||||
ownerID!,
|
||||
limit: limit,
|
||||
asc: asc,
|
||||
ignoredCollectionIDs: collectionsToHide,
|
||||
);
|
||||
}
|
||||
|
||||
// hide ignored files from home page UI
|
||||
final ignoredIDs =
|
||||
await IgnoredFilesService.instance.ignoredIDs;
|
||||
result.files.removeWhere(
|
||||
(f) =>
|
||||
f.uploadedFileID == null &&
|
||||
IgnoredFilesService.instance
|
||||
.shouldSkipUpload(ignoredIDs, f),
|
||||
);
|
||||
return result;
|
||||
},
|
||||
tagPrefix: "pick_center_point_gallery",
|
||||
selectedFiles: selectedFiles,
|
||||
limitSelectionToOne: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SafeArea(
|
||||
child: Container(
|
||||
//inner stroke of 1pt + 15 pts of top padding = 16 pts
|
||||
padding: const EdgeInsets.fromLTRB(16, 15, 16, 8),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
color: getEnteColorScheme(context).strokeFaint,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
ValueListenableBuilder(
|
||||
valueListenable: isFileSelected,
|
||||
builder: (context, bool value, _) {
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
switchInCurve: Curves.easeInOutExpo,
|
||||
switchOutCurve: Curves.easeInOutExpo,
|
||||
child: ButtonWidget(
|
||||
key: ValueKey(value),
|
||||
isDisabled: !value,
|
||||
buttonType: ButtonType.neutral,
|
||||
labelText: "Use selected photo",
|
||||
onTap: () async {
|
||||
final selectedFile =
|
||||
selectedFiles.files.first;
|
||||
Navigator.pop(context, selectedFile);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ButtonWidget(
|
||||
buttonType: ButtonType.secondary,
|
||||
buttonAction: ButtonAction.cancel,
|
||||
labelText: "Cancel",
|
||||
onTap: () async {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
142
lib/ui/viewer/location/radius_picker_widget.dart
Normal file
142
lib/ui/viewer/location/radius_picker_widget.dart
Normal file
|
@ -0,0 +1,142 @@
|
|||
import "package:flutter/material.dart";
|
||||
import "package:photos/core/constants.dart";
|
||||
import "package:photos/states/location_state.dart";
|
||||
import "package:photos/theme/colors.dart";
|
||||
import "package:photos/theme/ente_theme.dart";
|
||||
|
||||
class CustomTrackShape extends RoundedRectSliderTrackShape {
|
||||
@override
|
||||
Rect getPreferredRect({
|
||||
required RenderBox parentBox,
|
||||
Offset offset = Offset.zero,
|
||||
required SliderThemeData sliderTheme,
|
||||
bool isEnabled = false,
|
||||
bool isDiscrete = false,
|
||||
}) {
|
||||
const trackHeight = 2.0;
|
||||
final trackWidth = parentBox.size.width;
|
||||
return Rect.fromLTWH(0, 0, trackWidth, trackHeight);
|
||||
}
|
||||
}
|
||||
|
||||
class RadiusPickerWidget extends StatefulWidget {
|
||||
///This notifier can be listened to get the selected radius index from
|
||||
///a parent widget.
|
||||
final ValueNotifier<int> selectedRadiusIndexNotifier;
|
||||
const RadiusPickerWidget(
|
||||
this.selectedRadiusIndexNotifier, {
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<RadiusPickerWidget> createState() => _RadiusPickerWidgetState();
|
||||
}
|
||||
|
||||
class _RadiusPickerWidgetState extends State<RadiusPickerWidget> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
widget.selectedRadiusIndexNotifier.value =
|
||||
InheritedLocationTagData.of(context).selectedRadiusIndex;
|
||||
super.didChangeDependencies();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final selectedRadiusIndex = widget.selectedRadiusIndexNotifier.value;
|
||||
final radiusValue = radiusValues[selectedRadiusIndex];
|
||||
final textTheme = getEnteTextTheme(context);
|
||||
final colorScheme = getEnteColorScheme(context);
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
height: 48,
|
||||
width: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.fillFaint,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(2)),
|
||||
),
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 6,
|
||||
child: Text(
|
||||
radiusValue.toString(),
|
||||
style: radiusValue != 1200
|
||||
? textTheme.largeBold
|
||||
: textTheme.bodyBold,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 5,
|
||||
child: Text(
|
||||
"km",
|
||||
style: textTheme.miniMuted,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text("Radius", style: textTheme.body),
|
||||
const SizedBox(height: 10),
|
||||
SizedBox(
|
||||
height: 12,
|
||||
child: SliderTheme(
|
||||
data: SliderThemeData(
|
||||
overlayColor: Colors.transparent,
|
||||
thumbColor: strokeSolidMutedLight,
|
||||
activeTrackColor: strokeSolidMutedLight,
|
||||
inactiveTrackColor: colorScheme.strokeFaint,
|
||||
activeTickMarkColor: colorScheme.strokeMuted,
|
||||
inactiveTickMarkColor: strokeSolidMutedLight,
|
||||
trackShape: CustomTrackShape(),
|
||||
thumbShape: const RoundSliderThumbShape(
|
||||
enabledThumbRadius: 6,
|
||||
pressedElevation: 0,
|
||||
elevation: 0,
|
||||
),
|
||||
tickMarkShape: const RoundSliderTickMarkShape(
|
||||
tickMarkRadius: 1,
|
||||
),
|
||||
),
|
||||
child: RepaintBoundary(
|
||||
child: Slider(
|
||||
value: selectedRadiusIndex.toDouble(),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
widget.selectedRadiusIndexNotifier.value =
|
||||
value.toInt();
|
||||
});
|
||||
},
|
||||
min: 0,
|
||||
max: radiusValues.length - 1,
|
||||
divisions: radiusValues.length - 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -5,7 +5,6 @@ import 'package:logging/logging.dart';
|
|||
import 'package:photos/ente_theme_data.dart';
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import 'package:photos/models/search/search_result.dart';
|
||||
import 'package:photos/services/feature_flag_service.dart';
|
||||
import 'package:photos/services/search_service.dart';
|
||||
import 'package:photos/ui/components/buttons/icon_button_widget.dart';
|
||||
import 'package:photos/ui/viewer/search/result/no_result_widget.dart';
|
||||
|
@ -215,16 +214,19 @@ class _SearchWidgetState extends State<SearchWidget> {
|
|||
await _searchService.getFileExtensionResults(query);
|
||||
allResults.addAll(fileExtnResult);
|
||||
|
||||
final locationResult = await _searchService.getLocationResults(query);
|
||||
allResults.addAll(locationResult);
|
||||
|
||||
final collectionResults =
|
||||
await _searchService.getCollectionSearchResults(query);
|
||||
allResults.addAll(collectionResults);
|
||||
|
||||
if (FeatureFlagService.instance.isInternalUserOrDebugBuild() &&
|
||||
query.startsWith("l:")) {
|
||||
final locationResults = await _searchService
|
||||
.getLocationSearchResults(query.replaceAll("l:", ""));
|
||||
allResults.addAll(locationResults);
|
||||
}
|
||||
// if (FeatureFlagService.instance.isInternalUserOrDebugBuild() &&
|
||||
// query.startsWith("l:")) {
|
||||
// final locationResults = await _searchService
|
||||
// .getLocationSearchResults(query.replaceAll("l:", ""));
|
||||
// allResults.addAll(locationResults);
|
||||
// }
|
||||
|
||||
final monthResults = await _searchService.getMonthSearchResults(query);
|
||||
allResults.addAll(monthResults);
|
||||
|
|
|
@ -13,7 +13,7 @@ import 'package:photos/core/constants.dart';
|
|||
import 'package:photos/core/errors.dart';
|
||||
import 'package:photos/models/file.dart' as ente;
|
||||
import 'package:photos/models/file_type.dart';
|
||||
import 'package:photos/models/location.dart';
|
||||
import 'package:photos/models/location/location.dart';
|
||||
import 'package:photos/utils/crypto_util.dart';
|
||||
import 'package:photos/utils/file_util.dart';
|
||||
import 'package:video_thumbnail/video_thumbnail.dart';
|
||||
|
@ -155,7 +155,8 @@ Future<void> _decorateEnteFileData(ente.File file, AssetEntity asset) async {
|
|||
if (file.location == null ||
|
||||
(file.location!.latitude == 0 && file.location!.longitude == 0)) {
|
||||
final latLong = await asset.latlngAsync();
|
||||
file.location = Location(latLong.latitude, latLong.longitude);
|
||||
file.location =
|
||||
Location(latitude: latLong.latitude, longitude: latLong.longitude);
|
||||
}
|
||||
|
||||
if (file.title == null || file.title!.isEmpty) {
|
||||
|
|
14
lib/utils/lat_lon_util.dart
Normal file
14
lib/utils/lat_lon_util.dart
Normal file
|
@ -0,0 +1,14 @@
|
|||
String convertLatLng(double decimal, bool isLat) {
|
||||
final degree = "${decimal.toString().split(".")[0]}°";
|
||||
final minutesBeforeConversion =
|
||||
double.parse("0.${decimal.toString().split(".")[1]}");
|
||||
final minutes = "${(minutesBeforeConversion * 60).toString().split('.')[0]}'";
|
||||
final secondsBeforeConversion = double.parse(
|
||||
"0.${(minutesBeforeConversion * 60).toString().split('.')[1]}",
|
||||
);
|
||||
final seconds =
|
||||
'${double.parse((secondsBeforeConversion * 60).toString()).toStringAsFixed(0)}" ';
|
||||
final dmsOutput =
|
||||
"$degree$minutes$seconds${isLat ? decimal > 0 ? 'N' : 'S' : decimal > 0 ? 'E' : 'W'}";
|
||||
return dmsOutput;
|
||||
}
|
162
pubspec.lock
162
pubspec.lock
|
@ -97,6 +97,70 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
build:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build
|
||||
sha256: "3fbda25365741f8251b39f3917fb3c8e286a96fd068a5a242e11c2012d495777"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.1"
|
||||
build_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_config
|
||||
sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
build_daemon:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_daemon
|
||||
sha256: "757153e5d9cd88253cb13f28c2fb55a537dc31fefd98137549895b5beb7c6169"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
build_resolvers:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_resolvers
|
||||
sha256: db49b8609ef8c81cca2b310618c3017c00f03a92af44c04d310b907b2d692d95
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
build_runner:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: build_runner
|
||||
sha256: b0a8a7b8a76c493e85f1b84bffa0588859a06197863dba8c9036b15581fd9727
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.3"
|
||||
build_runner_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_runner_core
|
||||
sha256: "14febe0f5bac5ae474117a36099b4de6f1dbc52df6c5e55534b3da9591bf4292"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.2.7"
|
||||
built_collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: built_collection
|
||||
sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.1.1"
|
||||
built_value:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: built_value
|
||||
sha256: "31b7c748fd4b9adf8d25d72a4c4a59ef119f12876cf414f94f8af5131d5fa2b0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.4.4"
|
||||
cached_network_image:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -169,6 +233,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
checked_yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: checked_yaml
|
||||
sha256: "3d1505d91afa809d177efd4eed5bb0eb65805097a1463abdd2add076effae311"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
chewie:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -184,6 +256,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
code_builder:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: code_builder
|
||||
sha256: "0d43dd1288fd145de1ecc9a3948ad4a6d5a82f0a14c4fdd0892260787d975cbe"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.4.0"
|
||||
collection:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -272,6 +352,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
dart_style:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dart_style
|
||||
sha256: "5be16bf1707658e4c03078d4a9b90208ded217fb02c163e207d334082412f2fb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.5"
|
||||
dbus:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -472,6 +560,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.12"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fixnum
|
||||
sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
fk_user_agent:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -756,6 +852,22 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.1.3"
|
||||
freezed:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: freezed
|
||||
sha256: e819441678f1679b719008ff2ff0ef045d66eed9f9ec81166ca0d9b02a187454
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
freezed_annotation:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: freezed_annotation
|
||||
sha256: aeac15850ef1b38ee368d4c53ba9a847e900bb2c53a4db3f6881cbb3cb684338
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
frontend_server_client:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -780,6 +892,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.6"
|
||||
graphs:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: graphs
|
||||
sha256: f9e130f3259f52d26f0cfc0e964513796dafed572fa52e45d2f8d6ca14db39b2
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
hex:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -917,13 +1037,21 @@ packages:
|
|||
source: hosted
|
||||
version: "0.6.5"
|
||||
json_annotation:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: json_annotation
|
||||
sha256: c33da08e136c3df0190bd5bbe51ae1df4a7d96e7954d1d7249fea2968a72d317
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.8.0"
|
||||
json_serializable:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: json_serializable
|
||||
sha256: dadc08bd61f72559f938dd08ec20dbfec6c709bba83515085ea943d2078d187a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.6.1"
|
||||
like_button:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -1349,6 +1477,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
pubspec_parse:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pubspec_parse
|
||||
sha256: ec85d7d55339d85f44ec2b682a82fea340071e8978257e5a43e69f79e98ef50c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.2"
|
||||
quiver:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -1538,6 +1674,22 @@ packages:
|
|||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.99"
|
||||
source_gen:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_gen
|
||||
sha256: c2bea18c95cfa0276a366270afaa2850b09b4a76db95d546f3d003dcc7011298
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.7"
|
||||
source_helper:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_helper
|
||||
sha256: "3b67aade1d52416149c633ba1bb36df44d97c6b51830c2198e934e3fca87ca1f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.3"
|
||||
source_map_stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1723,6 +1875,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.0"
|
||||
timing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: timing
|
||||
sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
tuple:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
|
@ -68,12 +68,14 @@ dependencies:
|
|||
flutter_sodium: ^0.2.0
|
||||
flutter_typeahead: ^4.0.0
|
||||
fluttertoast: ^8.0.6
|
||||
freezed_annotation: ^2.2.0
|
||||
google_nav_bar: ^5.0.5
|
||||
http: ^0.13.4
|
||||
image: ^3.0.2
|
||||
image_editor: ^1.3.0
|
||||
in_app_purchase: ^3.0.7
|
||||
intl: ^0.17.0
|
||||
json_annotation: ^4.8.0
|
||||
like_button: ^2.0.2
|
||||
loading_animations: ^2.1.0
|
||||
local_auth: ^2.1.5
|
||||
|
@ -134,9 +136,12 @@ dependency_overrides:
|
|||
wakelock: ^0.6.1+2
|
||||
|
||||
dev_dependencies:
|
||||
build_runner: ^2.3.3
|
||||
flutter_lints: ^2.0.1
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
freezed: ^2.3.2
|
||||
json_serializable: ^6.6.1
|
||||
test:
|
||||
|
||||
flutter_icons:
|
||||
|
|
Loading…
Add table
Reference in a new issue