Location tag (#971)
This commit is contained in:
commit
59b43393cb
46 changed files with 3031 additions and 1062 deletions
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]);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -9,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';
|
||||
|
@ -80,6 +80,7 @@ class FilesDB {
|
|||
...createOnDeviceFilesAndPathCollection(),
|
||||
...addFileSizeColumn(),
|
||||
...updateIndexes(),
|
||||
...createEntityDataTable(),
|
||||
];
|
||||
|
||||
final dbConfig = MigrationConfig(
|
||||
|
@ -332,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 [
|
||||
'''
|
||||
|
@ -529,7 +544,8 @@ class FilesDB {
|
|||
filesTable,
|
||||
where: onlyFilesWithLocation
|
||||
? '$columnLatitude IS NOT NULL AND $columnLongitude IS NOT NULL AND ($columnLatitude IS NOT 0 OR $columnLongitude IS NOT 0)'
|
||||
'$columnCreationTime >= ? AND $columnCreationTime <= ? AND ($columnOwnerID IS NULL OR $columnOwnerID = ?) AND ($columnMMdVisibility IS NULL OR $columnMMdVisibility = ?)'
|
||||
' 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))',
|
||||
|
@ -1390,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) {
|
||||
|
@ -1489,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 {}
|
|
@ -20,6 +20,7 @@ import 'package:photos/ente_theme_data.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';
|
||||
|
@ -154,7 +155,9 @@ Future<void> _init(bool isBackground, {String via = ''}) async {
|
|||
await NetworkClient.instance.init();
|
||||
await Configuration.instance.init();
|
||||
await UserService.instance.init();
|
||||
await LocationService.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,
|
||||
};
|
|
@ -1,8 +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 {
|
||||
|
|
|
@ -1,51 +1,64 @@
|
|||
import "dart:collection";
|
||||
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 {
|
||||
SharedPreferences? prefs;
|
||||
late SharedPreferences prefs;
|
||||
final Logger _logger = Logger((LocationService).toString());
|
||||
|
||||
LocationService._privateConstructor();
|
||||
|
||||
static final LocationService instance = LocationService._privateConstructor();
|
||||
|
||||
Future<void> init() async {
|
||||
prefs ??= await SharedPreferences.getInstance();
|
||||
void init(SharedPreferences preferences) {
|
||||
prefs = preferences;
|
||||
}
|
||||
|
||||
List<String> getAllLocationTags() {
|
||||
var list = prefs!.getStringList('locations');
|
||||
list ??= [];
|
||||
return list;
|
||||
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,
|
||||
double lat,
|
||||
double long,
|
||||
Location centerPoint,
|
||||
int radius,
|
||||
) async {
|
||||
final list = getAllLocationTags();
|
||||
//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(lat)) / kilometersPerDegree;
|
||||
|
||||
final a =
|
||||
(radius * _scaleFactor(centerPoint.latitude!)) / kilometersPerDegree;
|
||||
final b = radius / kilometersPerDegree;
|
||||
final center = [lat, long];
|
||||
final data = {
|
||||
"name": location,
|
||||
"radius": radius,
|
||||
"aSquare": a * a,
|
||||
"bSquare": b * b,
|
||||
"center": center,
|
||||
};
|
||||
final encodedMap = json.encode(data);
|
||||
list.add(encodedMap);
|
||||
await prefs!.setStringList('locations', list);
|
||||
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
|
||||
|
@ -56,79 +69,127 @@ class LocationService {
|
|||
return 1 / cos(lat * (pi / 180));
|
||||
}
|
||||
|
||||
List<String> enclosingLocationTags(List<double> coordinates) {
|
||||
final result = List<String>.of([]);
|
||||
final allLocationTags = getAllLocationTags();
|
||||
for (var locationTag in allLocationTags) {
|
||||
final locationJson = json.decode(locationTag);
|
||||
final aSquare = locationJson["aSquare"];
|
||||
final bSquare = locationJson["bSquare"];
|
||||
final center = locationJson["center"];
|
||||
final x = coordinates[0] - center[0];
|
||||
final y = coordinates[1] - center[1];
|
||||
if ((x * x) / (aSquare) + (y * y) / (bSquare) <= 1) {
|
||||
result.add(locationJson["name"]);
|
||||
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;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool isFileInsideLocationTag(
|
||||
List<double> center,
|
||||
List<double> fileCoordinates,
|
||||
Location centerPoint,
|
||||
Location fileCoordinates,
|
||||
int radius,
|
||||
) {
|
||||
final a = (radius * _scaleFactor(center[0])) / kilometersPerDegree;
|
||||
final a =
|
||||
(radius * _scaleFactor(centerPoint.latitude!)) / kilometersPerDegree;
|
||||
final b = radius / kilometersPerDegree;
|
||||
final x = center[0] - fileCoordinates[0];
|
||||
final y = center[1] - fileCoordinates[1];
|
||||
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;
|
||||
}
|
||||
|
||||
Future<void> addFileToLocation(int locationId, int fileId) async {
|
||||
final list = getFilesByLocation(locationId.toString());
|
||||
list.add(fileId.toString());
|
||||
await prefs!.setStringList("location_$locationId", list);
|
||||
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<String> getFilesByLocation(String locationId) {
|
||||
var fileList = prefs!.getStringList("location_$locationId");
|
||||
fileList ??= [];
|
||||
return fileList;
|
||||
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];
|
||||
}
|
||||
|
||||
List<String> getLocationsByFileID(int fileId) {
|
||||
final locationList = getAllLocationTags();
|
||||
final locations = List<dynamic>.of([]);
|
||||
for (String locationString in locationList) {
|
||||
final locationJson = json.decode(locationString);
|
||||
locations.add(locationJson);
|
||||
}
|
||||
final res = List<String>.of([]);
|
||||
for (dynamic location in locations) {
|
||||
final list = getFilesByLocation(location["id"].toString());
|
||||
if (list.contains(fileId.toString())) {
|
||||
res.add(location["name"]);
|
||||
///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;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
Map<String, List<String>> clusterFilesByLocation() {
|
||||
final map = HashMap<String, List<String>>();
|
||||
var locations = prefs!.getStringList('locations');
|
||||
locations ??= [];
|
||||
for (String locationData in locations) {
|
||||
final locationJson = json.decode(locationData);
|
||||
map.putIfAbsent(
|
||||
locationData,
|
||||
() => getFilesByLocation(locationJson['id'].toString()),
|
||||
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;
|
||||
}
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -140,12 +201,12 @@ class GPSData {
|
|||
|
||||
GPSData(this.latRef, this.lat, this.longRef, this.long);
|
||||
|
||||
List<double> toSignedDecimalDegreeCoordinates() {
|
||||
Location toLocationObj() {
|
||||
final latSign = latRef == "N" ? 1 : -1;
|
||||
final longSign = longRef == "E" ? 1 : -1;
|
||||
return [
|
||||
latSign * lat[0] + lat[1] / 60 + lat[2] / 3600,
|
||||
longSign * long[0] + long[1] / 60 + long[2] / 3600
|
||||
];
|
||||
return Location(
|
||||
latitude: latSign * lat[0] + lat[1] / 60 + lat[2] / 3600,
|
||||
longitude: longSign * long[0] + long[1] / 60 + long[2] / 3600,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import "dart:convert";
|
||||
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:photos/core/event_bus.dart';
|
||||
import 'package:photos/data/holidays.dart';
|
||||
|
@ -11,10 +9,9 @@ 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";
|
||||
|
@ -56,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(
|
||||
|
@ -269,27 +226,41 @@ class SearchService {
|
|||
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 = [];
|
||||
final locations = LocationService.instance.getAllLocationTags();
|
||||
for (String location in locations) {
|
||||
final locationJson = json.decode(location);
|
||||
final locationName = locationJson["name"].toString();
|
||||
_logger.info(locationName);
|
||||
if (locationName.toLowerCase().contains(query.toLowerCase())) {
|
||||
_logger.info("TRUEEE");
|
||||
final fileIDs = LocationService.instance
|
||||
.getFilesByLocation(locationJson["id"].toString());
|
||||
final files = List<File>.empty(growable: true);
|
||||
for (String fileID in fileIDs) {
|
||||
final id = int.parse(fileID);
|
||||
final file = await FilesDB.instance.getFile(id);
|
||||
files.add(file!);
|
||||
|
||||
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,
|
||||
locationName,
|
||||
files,
|
||||
entry.key.name,
|
||||
entry.value,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -397,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) {
|
||||
|
|
|
@ -1,71 +0,0 @@
|
|||
import "package:flutter/material.dart";
|
||||
import "package:photos/core/constants.dart";
|
||||
import "package:photos/models/typedefs.dart";
|
||||
import "package:photos/utils/debouncer.dart";
|
||||
|
||||
class LocationTagDataStateProvider extends StatefulWidget {
|
||||
final List<double> coordinates;
|
||||
final Widget child;
|
||||
const LocationTagDataStateProvider(this.coordinates, this.child, {super.key});
|
||||
|
||||
@override
|
||||
State<LocationTagDataStateProvider> createState() =>
|
||||
_LocationTagDataStateProviderState();
|
||||
}
|
||||
|
||||
class _LocationTagDataStateProviderState
|
||||
extends State<LocationTagDataStateProvider> {
|
||||
int selectedRaduisIndex = defaultRadiusValueIndex;
|
||||
late List<double> coordinates;
|
||||
final Debouncer _selectedRadiusDebouncer =
|
||||
Debouncer(const Duration(milliseconds: 300));
|
||||
@override
|
||||
void initState() {
|
||||
coordinates = widget.coordinates;
|
||||
super.initState();
|
||||
}
|
||||
|
||||
void _updateSelectedIndex(int index) {
|
||||
_selectedRadiusDebouncer.cancelDebounce();
|
||||
_selectedRadiusDebouncer.run(() async {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
selectedRaduisIndex = index;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InheritedLocationTagData(
|
||||
selectedRaduisIndex,
|
||||
coordinates,
|
||||
_updateSelectedIndex,
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class InheritedLocationTagData extends InheritedWidget {
|
||||
final int selectedRadiusIndex;
|
||||
final List<double> coordinates;
|
||||
final VoidCallbackParamInt updateSelectedIndex;
|
||||
const InheritedLocationTagData(
|
||||
this.selectedRadiusIndex,
|
||||
this.coordinates,
|
||||
this.updateSelectedIndex, {
|
||||
required super.child,
|
||||
super.key,
|
||||
});
|
||||
|
||||
static InheritedLocationTagData of(BuildContext context) {
|
||||
return context
|
||||
.dependOnInheritedWidgetOfExactType<InheritedLocationTagData>()!;
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(InheritedLocationTagData oldWidget) {
|
||||
return oldWidget.selectedRadiusIndex != selectedRadiusIndex;
|
||||
}
|
||||
}
|
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;
|
||||
}
|
||||
}
|
|
@ -38,6 +38,9 @@ class TextInputWidget extends StatefulWidget {
|
|||
final bool isClearable;
|
||||
final bool shouldUnfocusOnClearOrSubmit;
|
||||
final FocusNode? focusNode;
|
||||
final VoidCallback? onCancel;
|
||||
final TextEditingController? textEditingController;
|
||||
final ValueNotifier? isEmptyNotifier;
|
||||
const TextInputWidget({
|
||||
this.onSubmit,
|
||||
this.onChange,
|
||||
|
@ -61,6 +64,9 @@ class TextInputWidget extends StatefulWidget {
|
|||
this.shouldUnfocusOnClearOrSubmit = false,
|
||||
this.borderRadius = 8,
|
||||
this.focusNode,
|
||||
this.onCancel,
|
||||
this.textEditingController,
|
||||
this.isEmptyNotifier,
|
||||
super.key,
|
||||
});
|
||||
|
||||
|
@ -70,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;
|
||||
|
||||
|
@ -82,6 +88,7 @@ class _TextInputWidgetState extends State<TextInputWidget> {
|
|||
void initState() {
|
||||
widget.submitNotifier?.addListener(_onSubmit);
|
||||
widget.cancelNotifier?.addListener(_onCancel);
|
||||
_textController = widget.textEditingController ?? TextEditingController();
|
||||
|
||||
if (widget.initialValue != null) {
|
||||
_textController.value = TextEditingValue(
|
||||
|
@ -96,6 +103,12 @@ 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();
|
||||
}
|
||||
|
||||
|
@ -105,6 +118,7 @@ class _TextInputWidgetState extends State<TextInputWidget> {
|
|||
widget.cancelNotifier?.removeListener(_onCancel);
|
||||
_obscureTextNotifier.dispose();
|
||||
_textController.dispose();
|
||||
widget.isEmptyNotifier?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
@ -318,8 +332,12 @@ class _TextInputWidgetState extends State<TextInputWidget> {
|
|||
}
|
||||
|
||||
void _onCancel() {
|
||||
_textController.clear();
|
||||
FocusScope.of(context).unfocus();
|
||||
if (widget.onCancel != null) {
|
||||
widget.onCancel!();
|
||||
} else {
|
||||
_textController.clear();
|
||||
FocusScope.of(context).unfocus();
|
||||
}
|
||||
}
|
||||
|
||||
void _popNavigatorStack(BuildContext context, {Exception? e}) {
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,6 +38,7 @@ class LazyLoadingGallery extends StatefulWidget {
|
|||
final Stream<int> currentIndexStream;
|
||||
final int photoGirdSize;
|
||||
final bool areFilesCollatedByDay;
|
||||
final bool limitSelectionToOne;
|
||||
LazyLoadingGallery(
|
||||
this.files,
|
||||
this.index,
|
||||
|
@ -50,6 +51,7 @@ class LazyLoadingGallery extends StatefulWidget {
|
|||
this.areFilesCollatedByDay, {
|
||||
this.logTag = "",
|
||||
this.photoGirdSize = photoGridSizeDefault,
|
||||
this.limitSelectionToOne = false,
|
||||
Key? key,
|
||||
}) : super(key: key ?? UniqueKey());
|
||||
|
||||
|
@ -198,42 +200,44 @@ class _LazyLoadingGalleryState extends State<LazyLoadingGallery> {
|
|||
_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;
|
||||
},
|
||||
);
|
||||
},
|
||||
)
|
||||
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!
|
||||
|
@ -264,6 +268,7 @@ class _LazyLoadingGalleryState extends State<LazyLoadingGallery> {
|
|||
_toggleSelectAllFromDay,
|
||||
_areAllFromDaySelected,
|
||||
widget.photoGirdSize,
|
||||
limitSelectionToOne: widget.limitSelectionToOne,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -292,6 +297,7 @@ class LazyLoadingGridView extends StatefulWidget {
|
|||
final ValueNotifier toggleSelectAllFromDay;
|
||||
final ValueNotifier areAllFilesSelected;
|
||||
final int? photoGridSize;
|
||||
final bool limitSelectionToOne;
|
||||
|
||||
LazyLoadingGridView(
|
||||
this.tag,
|
||||
|
@ -303,6 +309,7 @@ class LazyLoadingGridView extends StatefulWidget {
|
|||
this.toggleSelectAllFromDay,
|
||||
this.areAllFilesSelected,
|
||||
this.photoGridSize, {
|
||||
this.limitSelectionToOne = false,
|
||||
Key? key,
|
||||
}) : super(key: key ?? UniqueKey());
|
||||
|
||||
|
@ -424,28 +431,16 @@ class _LazyLoadingGridViewState extends State<LazyLoadingGridView> {
|
|||
selectionColor = avatarColors[(randomID).remainder(avatarColors.length)];
|
||||
}
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
if (widget.selectedFiles?.files.isNotEmpty ?? false) {
|
||||
_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: () {
|
||||
widget.limitSelectionToOne
|
||||
? _onLongPressWithSelectionLimit(file)
|
||||
: _onLongPressNoSelectionLimit(file);
|
||||
},
|
||||
onLongPress: widget.selectedFiles != null
|
||||
? () {
|
||||
if (AppLifecycleService.instance.mediaExtensionAction.action ==
|
||||
IntentAction.main) {
|
||||
HapticFeedback.lightImpact();
|
||||
_selectFile(file);
|
||||
}
|
||||
}
|
||||
: null,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(1),
|
||||
child: Stack(
|
||||
|
@ -490,10 +485,52 @@ class _LazyLoadingGridViewState extends State<LazyLoadingGridView> {
|
|||
);
|
||||
}
|
||||
|
||||
void _selectFile(File 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) {
|
||||
final page = DetailPage(
|
||||
DetailPageConfiguration(
|
||||
|
|
|
@ -12,7 +12,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';
|
||||
|
@ -359,7 +359,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);
|
||||
|
|
|
@ -5,7 +5,6 @@ import "package:photos/core/configuration.dart";
|
|||
import "package:photos/models/file.dart";
|
||||
import "package:photos/models/file_type.dart";
|
||||
import "package:photos/services/feature_flag_service.dart";
|
||||
import "package:photos/services/location_service.dart";
|
||||
import 'package:photos/theme/ente_theme.dart';
|
||||
import 'package:photos/ui/components/buttons/icon_button_widget.dart';
|
||||
import "package:photos/ui/components/divider_widget.dart";
|
||||
|
@ -147,12 +146,7 @@ class _FileDetailsWidgetState extends State<FileDetailsWidget> {
|
|||
? Column(
|
||||
children: [
|
||||
LocationTagsWidget(
|
||||
GPSData(
|
||||
_exifData["latRef"],
|
||||
_exifData["lat"],
|
||||
_exifData["longRef"],
|
||||
_exifData["long"],
|
||||
).toSignedDecimalDegreeCoordinates(),
|
||||
widget.file.location!,
|
||||
),
|
||||
const FileDetailsDivider(),
|
||||
],
|
||||
|
@ -238,10 +232,12 @@ class _FileDetailsWidgetState extends State<FileDetailsWidget> {
|
|||
}
|
||||
|
||||
bool _haGPSData() {
|
||||
return _exifData["lat"] != null &&
|
||||
_exifData["long"] != null &&
|
||||
_exifData["latRef"] != null &&
|
||||
_exifData["longRef"] != null;
|
||||
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) {
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
import "package:flutter/material.dart";
|
||||
import "package:photos/services/location_service.dart";
|
||||
|
||||
Widget locationChipList(int id, BuildContext context) {
|
||||
final locationService = LocationService.instance;
|
||||
final list = locationService.getLocationsByFileID(id);
|
||||
return Wrap(
|
||||
spacing: 6.0,
|
||||
runSpacing: 6.0,
|
||||
children: [
|
||||
...list.map((e) => _buildChip(e, context)).toList(),
|
||||
_addLocation(context)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildChip(String label, BuildContext context) {
|
||||
return Chip(
|
||||
labelPadding: const EdgeInsets.all(2.0),
|
||||
label: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
),
|
||||
backgroundColor: Theme.of(context).cardColor,
|
||||
elevation: 6.0,
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _addLocation(BuildContext context) {
|
||||
return IconButton(
|
||||
onPressed: () {},
|
||||
icon: const Icon(Icons.add),
|
||||
);
|
||||
}
|
|
@ -1,268 +0,0 @@
|
|||
import "package:flutter/material.dart";
|
||||
import "package:photos/ente_theme_data.dart";
|
||||
import "package:photos/services/location_service.dart";
|
||||
import "package:photos/ui/common/gradient_button.dart";
|
||||
import "package:photos/utils/lat_lon_util.dart";
|
||||
|
||||
class CreateLocation extends StatefulWidget {
|
||||
const CreateLocation({super.key});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return CreateLocationState();
|
||||
}
|
||||
}
|
||||
|
||||
class CreateLocationState extends State<CreateLocation> {
|
||||
TextEditingController locationController = TextEditingController();
|
||||
List<TextEditingController> centerPointController = List.from(
|
||||
[TextEditingController(text: "0.0"), TextEditingController(text: "0.0")],
|
||||
);
|
||||
List<double> centerPoint = List.of([0, 0]);
|
||||
final List<double> values = [2, 10, 20, 40, 80, 200, 400, 1200];
|
||||
int slider = 0;
|
||||
|
||||
Dialog selectCenterPoint(BuildContext context) => Dialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
), //this right here
|
||||
child: SizedBox(
|
||||
height: 300.0,
|
||||
width: 300.0,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(15),
|
||||
child: TextField(
|
||||
controller: centerPointController[0],
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
labelText: 'Longitude',
|
||||
hintText: 'Enter Longitude',
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(15),
|
||||
child: TextField(
|
||||
controller: centerPointController[1],
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
labelText: 'Latitude',
|
||||
hintText: 'Enter Latitude',
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.only(top: 50.0)),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
centerPoint = List.of([
|
||||
double.parse(centerPointController[0].text),
|
||||
double.parse(centerPointController[1].text)
|
||||
]);
|
||||
});
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(
|
||||
'Select',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.iconColor,
|
||||
fontSize: 18.0,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
margin: const EdgeInsets.fromLTRB(16, 12, 0, 0),
|
||||
child: const Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text.rich(
|
||||
TextSpan(text: "Add Location"),
|
||||
textAlign: TextAlign.start,
|
||||
style: TextStyle(fontSize: 24, fontWeight: FontWeight.w800),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(15),
|
||||
child: TextField(
|
||||
controller: locationController,
|
||||
decoration: InputDecoration(
|
||||
filled: true,
|
||||
fillColor: Theme.of(context).colorScheme.subTextColor,
|
||||
hintStyle: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
hintText: 'Enter Your Location',
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 15, vertical: 8),
|
||||
padding: const EdgeInsets.all(7),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
border: Border.all(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1,
|
||||
),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(10)),
|
||||
),
|
||||
child: ListTile(
|
||||
horizontalTitleGap: 2,
|
||||
leading: const Icon(Icons.location_on_rounded),
|
||||
title: const Text(
|
||||
"Center Point",
|
||||
),
|
||||
subtitle: Text(
|
||||
"${convertLatLng(centerPoint[0], true)}, ${convertLatLng(centerPoint[1], false)}",
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.defaultTextColor
|
||||
.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
trailing: IconButton(
|
||||
onPressed: () async {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) =>
|
||||
selectCenterPoint(context),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.edit),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
Container(
|
||||
height: 65,
|
||||
width: 70,
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).focusColor,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(5)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
values[slider].round().toString(),
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const Text(
|
||||
"Km",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w200,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 15),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(left: 20),
|
||||
child: Text(
|
||||
"Radius",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SliderTheme(
|
||||
data: SliderTheme.of(context).copyWith(
|
||||
trackHeight: 5,
|
||||
activeTrackColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.inverseBackgroundColor,
|
||||
inactiveTrackColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.subTextColor,
|
||||
thumbColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.inverseBackgroundColor,
|
||||
),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: Slider(
|
||||
value: slider.toDouble(),
|
||||
min: 0,
|
||||
max: values.length - 1,
|
||||
divisions: values.length - 1,
|
||||
label: values[slider].toString(),
|
||||
onChanged: (double value) {
|
||||
setState(() {
|
||||
slider = value.toInt();
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 200),
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 24, 20, 100),
|
||||
child: GradientButton(
|
||||
onTap: () async {
|
||||
await LocationService.instance.addLocation(
|
||||
locationController.text,
|
||||
centerPoint[0],
|
||||
centerPoint[1],
|
||||
values[slider].toInt(),
|
||||
);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
text: "Add Location",
|
||||
iconData: Icons.location_on,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,214 +0,0 @@
|
|||
import "dart:async";
|
||||
import "dart:convert";
|
||||
|
||||
import "package:flutter/material.dart";
|
||||
import "package:photos/db/files_db.dart";
|
||||
import "package:photos/ente_theme_data.dart";
|
||||
import "package:photos/services/location_service.dart";
|
||||
import "package:photos/ui/viewer/file/location_detail.dart";
|
||||
|
||||
//state = 0; normal mode
|
||||
//state = 1; selection mode
|
||||
class LocationsList extends StatelessWidget {
|
||||
final int state;
|
||||
final int? fileId;
|
||||
LocationsList({super.key, this.state = 0, this.fileId});
|
||||
|
||||
final clusteredLocationList =
|
||||
LocationService.instance.clusterFilesByLocation();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
margin: const EdgeInsets.fromLTRB(16, 12, 0, 0),
|
||||
child: const Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text.rich(
|
||||
TextSpan(text: "Locations"),
|
||||
textAlign: TextAlign.start,
|
||||
style: TextStyle(fontSize: 24, fontWeight: FontWeight.w800),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
margin: const EdgeInsets.fromLTRB(16, 12, 0, 0),
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text.rich(
|
||||
TextSpan(text: clusteredLocationList.length.toString()),
|
||||
textAlign: TextAlign.start,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w100,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
...clusteredLocationList.entries.map((entry) {
|
||||
final location = json.decode(entry.key);
|
||||
return InkWell(
|
||||
onTap: () async {
|
||||
if (state == 1) {
|
||||
await LocationService.instance
|
||||
.addFileToLocation(location["id"], fileId!);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(
|
||||
horizontal: 15,
|
||||
vertical: 10,
|
||||
),
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
border: Border.all(color: Theme.of(context).dividerColor),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(10)),
|
||||
),
|
||||
child: ListTile(
|
||||
horizontalTitleGap: 2,
|
||||
title: Text(
|
||||
location["name"],
|
||||
),
|
||||
subtitle: Text(
|
||||
"${entry.value.length} memories",
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.defaultTextColor
|
||||
.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
trailing: state == 0
|
||||
? IconButton(
|
||||
onPressed: () async {},
|
||||
icon: const Icon(Icons.arrow_forward_ios),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
unawaited(
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return const CreateLocation();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 15, vertical: 8),
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
border: Border.all(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1,
|
||||
),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(10)),
|
||||
),
|
||||
child: const ListTile(
|
||||
horizontalTitleGap: 2,
|
||||
leading: Icon(Icons.add_location_alt_rounded),
|
||||
title: Text(
|
||||
"Add New",
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LocationFilesList extends StatelessWidget {
|
||||
final List<String> fileIDs;
|
||||
const LocationFilesList({super.key, required this.fileIDs});
|
||||
|
||||
Future<void> generateFiles() async {
|
||||
final files = List.empty(growable: true);
|
||||
for (String fileID in fileIDs) {
|
||||
final file = await (FilesDB.instance.getFile(int.parse(fileID)));
|
||||
files.add(file!);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
...LocationService.instance
|
||||
.clusterFilesByLocation()
|
||||
.entries
|
||||
.map(
|
||||
(entry) => Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
border: Border.all(color: Theme.of(context).dividerColor),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(10)),
|
||||
),
|
||||
child: ListTile(
|
||||
horizontalTitleGap: 2,
|
||||
title: Text(
|
||||
entry.key,
|
||||
),
|
||||
subtitle: Text(
|
||||
"${entry.value.length} memories",
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.defaultTextColor
|
||||
.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
trailing: IconButton(
|
||||
onPressed: () async {},
|
||||
icon: const Icon(Icons.arrow_forward_ios),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
border: Border.all(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1,
|
||||
),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(10)),
|
||||
),
|
||||
child: const ListTile(
|
||||
horizontalTitleGap: 2,
|
||||
leading: Padding(
|
||||
padding: EdgeInsets.only(top: 8),
|
||||
child: Icon(Icons.add_location_alt_rounded),
|
||||
),
|
||||
title: Text(
|
||||
"Add Location",
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,31 +1,48 @@
|
|||
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 List<double> coordinates;
|
||||
const LocationTagsWidget(this.coordinates, {super.key});
|
||||
final Location centerPoint;
|
||||
const LocationTagsWidget(this.centerPoint, {super.key});
|
||||
|
||||
@override
|
||||
State<LocationTagsWidget> createState() => _LocationTagsWidgetState();
|
||||
}
|
||||
|
||||
class _LocationTagsWidgetState extends State<LocationTagsWidget> {
|
||||
String title = "Add location";
|
||||
IconData leadingIcon = Icons.add_location_alt_outlined;
|
||||
bool hasChipButtons = false;
|
||||
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(
|
||||
|
@ -34,52 +51,66 @@ class _LocationTagsWidgetState extends State<LocationTagsWidget> {
|
|||
switchOutCurve: Curves.easeInOutExpo,
|
||||
child: InfoItemWidget(
|
||||
key: ValueKey(title),
|
||||
leadingIcon: Icons.add_location_alt_outlined,
|
||||
leadingIcon: leadingIcon ?? Icons.pin_drop_outlined,
|
||||
title: title,
|
||||
subtitleSection: locationTagChips,
|
||||
hasChipButtons: hasChipButtons,
|
||||
hasChipButtons: hasChipButtons ?? true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<Widget>> _getLocationTags() async {
|
||||
final locationTags =
|
||||
LocationService.instance.enclosingLocationTags(widget.coordinates);
|
||||
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.coordinates,
|
||||
//This callback is for reloading the locationTagsWidget after adding a new location tag
|
||||
//so that it updates in file details.
|
||||
() {
|
||||
locationTagChips = _getLocationTags();
|
||||
},
|
||||
widget.centerPoint,
|
||||
),
|
||||
),
|
||||
];
|
||||
} else {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
title = "Location";
|
||||
leadingIcon = Icons.pin_drop_outlined;
|
||||
hasChipButtons = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
setState(() {
|
||||
title = "Location";
|
||||
leadingIcon = Icons.pin_drop_outlined;
|
||||
hasChipButtons = true;
|
||||
});
|
||||
final result = locationTags.map((e) => ChipButtonWidget(e)).toList();
|
||||
|
||||
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.coordinates,
|
||||
//This callback is for reloading the locationTagsWidget after adding a new location tag
|
||||
//so that it updates in file details.
|
||||
() {
|
||||
locationTagChips = _getLocationTags();
|
||||
},
|
||||
),
|
||||
onTap: () => showAddLocationSheet(context, widget.centerPoint),
|
||||
),
|
||||
);
|
||||
return result;
|
||||
|
|
|
@ -43,6 +43,7 @@ class Gallery extends StatefulWidget {
|
|||
final bool shouldCollateFilesByDay;
|
||||
final Widget loadingWidget;
|
||||
final bool disableScroll;
|
||||
final bool limitSelectionToOne;
|
||||
|
||||
const Gallery({
|
||||
required this.asyncLoader,
|
||||
|
@ -60,6 +61,7 @@ class Gallery extends StatefulWidget {
|
|||
this.shouldCollateFilesByDay = true,
|
||||
this.loadingWidget = const EnteLoadingWidget(),
|
||||
this.disableScroll = false,
|
||||
this.limitSelectionToOne = false,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
|
@ -257,6 +259,7 @@ class _GalleryState extends State<Gallery> {
|
|||
widget.shouldCollateFilesByDay,
|
||||
logTag: _logTag,
|
||||
photoGirdSize: _photoGridSize,
|
||||
limitSelectionToOne: widget.limitSelectionToOne,
|
||||
);
|
||||
if (widget.header != null && index == 0) {
|
||||
gallery = Column(children: [widget.header!, gallery]);
|
||||
|
|
|
@ -1,31 +1,33 @@
|
|||
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/add_location_state.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/add_location_gallery_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,
|
||||
List<double> coordinates,
|
||||
VoidCallback onLocationAdded,
|
||||
Location coordinates,
|
||||
) {
|
||||
showBarModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return LocationTagDataStateProvider(
|
||||
coordinates,
|
||||
AddLocationSheet(onLocationAdded),
|
||||
return LocationTagStateProvider(
|
||||
centerPoint: coordinates,
|
||||
const AddLocationSheet(),
|
||||
);
|
||||
},
|
||||
shape: const RoundedRectangleBorder(
|
||||
|
@ -37,13 +39,11 @@ showAddLocationSheet(
|
|||
topControl: const SizedBox.shrink(),
|
||||
backgroundColor: getEnteColorScheme(context).backgroundElevated,
|
||||
barrierColor: backdropFaintDark,
|
||||
enableDrag: false,
|
||||
);
|
||||
}
|
||||
|
||||
class AddLocationSheet extends StatefulWidget {
|
||||
final VoidCallback onLocationAdded;
|
||||
const AddLocationSheet(this.onLocationAdded, {super.key});
|
||||
const AddLocationSheet({super.key});
|
||||
|
||||
@override
|
||||
State<AddLocationSheet> createState() => _AddLocationSheetState();
|
||||
|
@ -56,12 +56,17 @@ class _AddLocationSheetState extends State<AddLocationSheet> {
|
|||
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();
|
||||
}
|
||||
|
||||
|
@ -70,6 +75,7 @@ class _AddLocationSheetState extends State<AddLocationSheet> {
|
|||
_focusNode.removeListener(_focusNodeListener);
|
||||
_submitNotifer.dispose();
|
||||
_cancelNotifier.dispose();
|
||||
_selectedRadiusIndexNotifier.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
@ -89,6 +95,9 @@ class _AddLocationSheetState extends State<AddLocationSheet> {
|
|||
),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(
|
||||
decelerationRate: ScrollDecelerationRate.fast,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
|
@ -96,21 +105,50 @@ class _AddLocationSheetState extends State<AddLocationSheet> {
|
|||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
children: [
|
||||
TextInputWidget(
|
||||
hintText: "Location name",
|
||||
borderRadius: 2,
|
||||
focusNode: _focusNode,
|
||||
submitNotifier: _submitNotifer,
|
||||
cancelNotifier: _cancelNotifier,
|
||||
popNavAfterSubmission: true,
|
||||
onSubmit: (locationName) async {
|
||||
await _addLocationTag(locationName);
|
||||
},
|
||||
shouldUnfocusOnClearOrSubmit: true,
|
||||
alwaysShowSuccessState: true,
|
||||
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(_memoriesCountNotifier),
|
||||
RadiusPickerWidget(
|
||||
_selectedRadiusIndexNotifier,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
"A location tag groups all photos that were taken within some radius of a photo",
|
||||
|
@ -174,7 +212,10 @@ class _AddLocationSheetState extends State<AddLocationSheet> {
|
|||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
AddLocationGalleryWidget(_memoriesCountNotifier),
|
||||
DynamicLocationGalleryWidget(
|
||||
_memoriesCountNotifier,
|
||||
"Add_location",
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -184,17 +225,16 @@ class _AddLocationSheetState extends State<AddLocationSheet> {
|
|||
);
|
||||
}
|
||||
|
||||
Future<void> _addLocationTag(String locationName) async {
|
||||
Future<void> _addLocationTag() async {
|
||||
final locationData = InheritedLocationTagData.of(context);
|
||||
final coordinates = locationData.coordinates;
|
||||
final coordinates = locationData.centerPoint;
|
||||
final radius = radiusValues[locationData.selectedRadiusIndex];
|
||||
await LocationService.instance.addLocation(
|
||||
locationName,
|
||||
coordinates.first,
|
||||
coordinates.last,
|
||||
_textEditingController.text.trim(),
|
||||
coordinates,
|
||||
radius,
|
||||
);
|
||||
widget.onLocationAdded.call();
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
void _focusNodeListener() {
|
||||
|
@ -213,4 +253,13 @@ class _AddLocationSheetState extends State<AddLocationSheet> {
|
|||
KeyboardOverlay.removeOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
void _selectedRadiusIndexListener() {
|
||||
InheritedLocationTagData.of(
|
||||
context,
|
||||
).updateSelectedIndex(
|
||||
_selectedRadiusIndexNotifier.value,
|
||||
);
|
||||
_memoriesCountNotifier.value = null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,36 +2,52 @@ import "dart:developer" as dev;
|
|||
import "dart:math";
|
||||
|
||||
import "package:flutter/material.dart";
|
||||
import "package:photos/core/configuration.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/ignored_files_service.dart";
|
||||
import "package:photos/services/files_service.dart";
|
||||
import "package:photos/services/location_service.dart";
|
||||
import "package:photos/states/add_location_state.dart";
|
||||
import 'package:photos/states/location_state.dart';
|
||||
import "package:photos/ui/viewer/gallery/gallery.dart";
|
||||
import "package:photos/utils/local_settings.dart";
|
||||
|
||||
class AddLocationGalleryWidget extends StatefulWidget {
|
||||
///This gallery will get rebuilt with the updated radius when
|
||||
///InheritedLocationTagData notifies a change in radius.
|
||||
class DynamicLocationGalleryWidget extends StatefulWidget {
|
||||
final ValueNotifier<int?> memoriesCountNotifier;
|
||||
const AddLocationGalleryWidget(this.memoriesCountNotifier, {super.key});
|
||||
final String tagPrefix;
|
||||
const DynamicLocationGalleryWidget(
|
||||
this.memoriesCountNotifier,
|
||||
this.tagPrefix, {
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AddLocationGalleryWidget> createState() =>
|
||||
_AddLocationGalleryWidgetState();
|
||||
State<DynamicLocationGalleryWidget> createState() =>
|
||||
_DynamicLocationGalleryWidgetState();
|
||||
}
|
||||
|
||||
class _AddLocationGalleryWidgetState extends State<AddLocationGalleryWidget> {
|
||||
class _DynamicLocationGalleryWidgetState
|
||||
extends State<DynamicLocationGalleryWidget> {
|
||||
late final Future<FileLoadResult> fileLoadResult;
|
||||
late Future<void> removeIgnoredFiles;
|
||||
double heightOfGallery = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
fileLoadResult = _fetchAllFilesWithLocationData();
|
||||
removeIgnoredFiles = _removeIgnoredFiles(fileLoadResult);
|
||||
final collectionsToHide =
|
||||
CollectionsService.instance.collectionsHiddenFromTimeline();
|
||||
fileLoadResult = FilesDB.instance.getAllUploadedAndSharedFiles(
|
||||
galleryLoadStartTime,
|
||||
galleryLoadEndTime,
|
||||
limit: null,
|
||||
asc: false,
|
||||
ignoredCollectionIDs: collectionsToHide,
|
||||
);
|
||||
removeIgnoredFiles =
|
||||
FilesService.instance.removeIgnoredFiles(fileLoadResult);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
|
@ -46,14 +62,9 @@ class _AddLocationGalleryWidgetState extends State<AddLocationGalleryWidget> {
|
|||
final stopWatch = Stopwatch()..start();
|
||||
final copyOfFiles = List<File>.from(result.files);
|
||||
copyOfFiles.removeWhere((f) {
|
||||
assert(
|
||||
f.location != null &&
|
||||
f.location!.latitude != null &&
|
||||
f.location!.longitude != null,
|
||||
);
|
||||
return !LocationService.instance.isFileInsideLocationTag(
|
||||
InheritedLocationTagData.of(context).coordinates,
|
||||
[f.location!.latitude!, f.location!.longitude!],
|
||||
InheritedLocationTagData.of(context).centerPoint,
|
||||
f.location!,
|
||||
selectedRadius,
|
||||
);
|
||||
});
|
||||
|
@ -73,7 +84,10 @@ class _AddLocationGalleryWidgetState extends State<AddLocationGalleryWidget> {
|
|||
}
|
||||
|
||||
return FutureBuilder(
|
||||
key: ValueKey(selectedRadius),
|
||||
//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(
|
||||
|
@ -84,7 +98,6 @@ class _AddLocationGalleryWidgetState extends State<AddLocationGalleryWidget> {
|
|||
),
|
||||
),
|
||||
child: Gallery(
|
||||
key: ValueKey(selectedRadius),
|
||||
loadingWidget: const SizedBox.shrink(),
|
||||
disableScroll: true,
|
||||
asyncLoader: (
|
||||
|
@ -95,7 +108,7 @@ class _AddLocationGalleryWidgetState extends State<AddLocationGalleryWidget> {
|
|||
}) async {
|
||||
return snapshot.data as FileLoadResult;
|
||||
},
|
||||
tagPrefix: "Add location",
|
||||
tagPrefix: widget.tagPrefix,
|
||||
shouldCollateFilesByDay: false,
|
||||
),
|
||||
);
|
||||
|
@ -112,15 +125,6 @@ class _AddLocationGalleryWidgetState extends State<AddLocationGalleryWidget> {
|
|||
InheritedLocationTagData.of(context).selectedRadiusIndex];
|
||||
}
|
||||
|
||||
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),
|
||||
);
|
||||
}
|
||||
|
||||
double _galleryHeight(int fileCount) {
|
||||
final photoGridSize = LocalSettings.instance.getPhotoGridSize();
|
||||
final totalWhiteSpaceBetweenPhotos =
|
||||
|
@ -136,33 +140,4 @@ class _AddLocationGalleryWidgetState extends State<AddLocationGalleryWidget> {
|
|||
(galleryGridSpacing * (numberOfRows - 1));
|
||||
return galleryHeight + 120;
|
||||
}
|
||||
|
||||
Future<FileLoadResult> _fetchAllFilesWithLocationData() {
|
||||
final ownerID = Configuration.instance.getUserID();
|
||||
final hasSelectedAllForBackup =
|
||||
Configuration.instance.hasSelectedAllFoldersForBackup();
|
||||
final collectionsToHide =
|
||||
CollectionsService.instance.collectionsHiddenFromTimeline();
|
||||
if (hasSelectedAllForBackup) {
|
||||
return FilesDB.instance.getAllLocalAndUploadedFiles(
|
||||
galleryLoadStartTime,
|
||||
galleryLoadEndTime,
|
||||
ownerID!,
|
||||
limit: null,
|
||||
asc: true,
|
||||
ignoredCollectionIDs: collectionsToHide,
|
||||
onlyFilesWithLocation: true,
|
||||
);
|
||||
} else {
|
||||
return FilesDB.instance.getAllPendingOrUploadedFiles(
|
||||
galleryLoadStartTime,
|
||||
galleryLoadEndTime,
|
||||
ownerID!,
|
||||
limit: null,
|
||||
asc: true,
|
||||
ignoredCollectionIDs: collectionsToHide,
|
||||
onlyFilesWithLocation: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
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();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import "package:flutter/material.dart";
|
||||
import "package:photos/core/constants.dart";
|
||||
import "package:photos/states/add_location_state.dart";
|
||||
import "package:photos/states/location_state.dart";
|
||||
import "package:photos/theme/colors.dart";
|
||||
import "package:photos/theme/ente_theme.dart";
|
||||
|
||||
|
@ -20,20 +20,34 @@ class CustomTrackShape extends RoundedRectSliderTrackShape {
|
|||
}
|
||||
|
||||
class RadiusPickerWidget extends StatefulWidget {
|
||||
final ValueNotifier<int?> memoriesCountNotifier;
|
||||
const RadiusPickerWidget(this.memoriesCountNotifier, {super.key});
|
||||
///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> {
|
||||
//Will maintain the state of the slider using this varialbe. Can't use
|
||||
//InheritedLocationData.selectedRadiusIndex as the state in the inheritedWidget
|
||||
//only changes after debounce time and the slider will not reflect the change immediately.
|
||||
int selectedRadiusIndex = defaultRadiusValueIndex;
|
||||
@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);
|
||||
|
@ -107,15 +121,9 @@ class _RadiusPickerWidgetState extends State<RadiusPickerWidget> {
|
|||
value: selectedRadiusIndex.toDouble(),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
selectedRadiusIndex = value.toInt();
|
||||
widget.selectedRadiusIndexNotifier.value =
|
||||
value.toInt();
|
||||
});
|
||||
|
||||
InheritedLocationTagData.of(
|
||||
context,
|
||||
).updateSelectedIndex(
|
||||
value.toInt(),
|
||||
);
|
||||
widget.memoriesCountNotifier.value = null;
|
||||
},
|
||||
min: 0,
|
||||
max: radiusValues.length - 1,
|
||||
|
|
|
@ -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) {
|
||||
|
|
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:
|
||||
|
@ -716,6 +812,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:
|
||||
|
@ -740,6 +852,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:
|
||||
|
@ -877,13 +997,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:
|
||||
|
@ -1309,6 +1437,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:
|
||||
|
@ -1498,6 +1634,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:
|
||||
|
@ -1675,6 +1827,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.0.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
|
||||
|
@ -130,9 +132,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