mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-01-22 16:56:19 +01:00
Merge branch 'map-browser'
This commit is contained in:
commit
5903b45e11
34 changed files with 1771 additions and 25 deletions
|
@ -16,6 +16,8 @@ import 'package:nc_photos/entity/file.dart';
|
||||||
import 'package:nc_photos/entity/file/data_source.dart';
|
import 'package:nc_photos/entity/file/data_source.dart';
|
||||||
import 'package:nc_photos/entity/file/data_source2.dart';
|
import 'package:nc_photos/entity/file/data_source2.dart';
|
||||||
import 'package:nc_photos/entity/file/repo.dart';
|
import 'package:nc_photos/entity/file/repo.dart';
|
||||||
|
import 'package:nc_photos/entity/image_location/data_source.dart';
|
||||||
|
import 'package:nc_photos/entity/image_location/repo.dart';
|
||||||
import 'package:nc_photos/entity/local_file.dart';
|
import 'package:nc_photos/entity/local_file.dart';
|
||||||
import 'package:nc_photos/entity/local_file/data_source.dart';
|
import 'package:nc_photos/entity/local_file/data_source.dart';
|
||||||
import 'package:nc_photos/entity/nc_album/data_source.dart';
|
import 'package:nc_photos/entity/nc_album/data_source.dart';
|
||||||
|
@ -187,6 +189,8 @@ Future<void> _initDiContainer(InitIsolateType isolateType) async {
|
||||||
const BasicRecognizeFaceRepo(RecognizeFaceRemoteDataSource());
|
const BasicRecognizeFaceRepo(RecognizeFaceRemoteDataSource());
|
||||||
c.recognizeFaceRepoLocal =
|
c.recognizeFaceRepoLocal =
|
||||||
BasicRecognizeFaceRepo(RecognizeFaceSqliteDbDataSource(c.npDb));
|
BasicRecognizeFaceRepo(RecognizeFaceSqliteDbDataSource(c.npDb));
|
||||||
|
c.imageLocationRepo =
|
||||||
|
BasicImageLocationRepo(ImageLocationNpDbDataSource(c.npDb));
|
||||||
|
|
||||||
c.touchManager = TouchManager(c);
|
c.touchManager = TouchManager(c);
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
// ignore_for_file: deprecated_member_use_from_same_package
|
// ignore_for_file: deprecated_member_use_from_same_package
|
||||||
|
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:nc_photos/account.dart';
|
import 'package:nc_photos/account.dart';
|
||||||
import 'package:nc_photos/di_container.dart';
|
import 'package:nc_photos/di_container.dart';
|
||||||
import 'package:nc_photos/entity/collection/util.dart';
|
import 'package:nc_photos/entity/collection/util.dart';
|
||||||
import 'package:nc_photos/entity/pref.dart';
|
import 'package:nc_photos/entity/pref.dart';
|
||||||
|
import 'package:nc_photos/json_util.dart';
|
||||||
import 'package:nc_photos/language_util.dart';
|
import 'package:nc_photos/language_util.dart';
|
||||||
import 'package:nc_photos/object_extension.dart';
|
import 'package:nc_photos/object_extension.dart';
|
||||||
import 'package:nc_photos/protected_page_handler.dart';
|
import 'package:nc_photos/protected_page_handler.dart';
|
||||||
|
@ -158,6 +161,13 @@ class PrefController {
|
||||||
value: value,
|
value: value,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Future<bool> setMapBrowserPrevPosition(MapCoord value) => _set<MapCoord?>(
|
||||||
|
controller: _mapBrowserPrevPositionController,
|
||||||
|
setter: (pref, value) => pref.setMapBrowserPrevPosition(
|
||||||
|
jsonEncode([value!.latitude, value.longitude])),
|
||||||
|
value: value,
|
||||||
|
);
|
||||||
|
|
||||||
Future<bool> _set<T>({
|
Future<bool> _set<T>({
|
||||||
required BehaviorSubject<T> controller,
|
required BehaviorSubject<T> controller,
|
||||||
required Future<bool> Function(Pref pref, T value) setter,
|
required Future<bool> Function(Pref pref, T value) setter,
|
||||||
|
@ -258,6 +268,11 @@ class PrefController {
|
||||||
@npSubjectAccessor
|
@npSubjectAccessor
|
||||||
late final _isDontShowVideoPreviewHintController =
|
late final _isDontShowVideoPreviewHintController =
|
||||||
BehaviorSubject.seeded(_c.pref.isDontShowVideoPreviewHintOr(false));
|
BehaviorSubject.seeded(_c.pref.isDontShowVideoPreviewHintOr(false));
|
||||||
|
@npSubjectAccessor
|
||||||
|
late final _mapBrowserPrevPositionController = BehaviorSubject.seeded(_c.pref
|
||||||
|
.getMapBrowserPrevPosition()
|
||||||
|
?.let(tryJsonDecode)
|
||||||
|
?.let(_tryMapCoordFromJson));
|
||||||
}
|
}
|
||||||
|
|
||||||
@npSubjectAccessor
|
@npSubjectAccessor
|
||||||
|
|
|
@ -157,6 +157,15 @@ extension $PrefControllerNpSubjectAccessor on PrefController {
|
||||||
isDontShowVideoPreviewHint.distinct().skip(1);
|
isDontShowVideoPreviewHint.distinct().skip(1);
|
||||||
bool get isDontShowVideoPreviewHintValue =>
|
bool get isDontShowVideoPreviewHintValue =>
|
||||||
_isDontShowVideoPreviewHintController.value;
|
_isDontShowVideoPreviewHintController.value;
|
||||||
|
// _mapBrowserPrevPositionController
|
||||||
|
ValueStream<MapCoord?> get mapBrowserPrevPosition =>
|
||||||
|
_mapBrowserPrevPositionController.stream;
|
||||||
|
Stream<MapCoord?> get mapBrowserPrevPositionNew =>
|
||||||
|
mapBrowserPrevPosition.skip(1);
|
||||||
|
Stream<MapCoord?> get mapBrowserPrevPositionChange =>
|
||||||
|
mapBrowserPrevPosition.distinct().skip(1);
|
||||||
|
MapCoord? get mapBrowserPrevPositionValue =>
|
||||||
|
_mapBrowserPrevPositionController.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
extension $SecurePrefControllerNpSubjectAccessor on SecurePrefController {
|
extension $SecurePrefControllerNpSubjectAccessor on SecurePrefController {
|
||||||
|
|
|
@ -88,4 +88,20 @@ extension on Pref {
|
||||||
isDontShowVideoPreviewHint() ?? def;
|
isDontShowVideoPreviewHint() ?? def;
|
||||||
Future<bool> setDontShowVideoPreviewHint(bool value) =>
|
Future<bool> setDontShowVideoPreviewHint(bool value) =>
|
||||||
provider.setBool(PrefKey.dontShowVideoPreviewHint, value);
|
provider.setBool(PrefKey.dontShowVideoPreviewHint, value);
|
||||||
|
|
||||||
|
String? getMapBrowserPrevPosition() =>
|
||||||
|
provider.getString(PrefKey.mapBrowserPrevPosition);
|
||||||
|
Future<bool> setMapBrowserPrevPosition(String value) =>
|
||||||
|
provider.setString(PrefKey.mapBrowserPrevPosition, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
MapCoord? _tryMapCoordFromJson(dynamic json) {
|
||||||
|
try {
|
||||||
|
final j = (json as List).cast<double>();
|
||||||
|
return MapCoord(j[0], j[1]);
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
_$__NpLog.log
|
||||||
|
.severe("[_tryMapCoordFromJson] Failed to parse json", e, stackTrace);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import 'package:nc_photos/entity/exif.dart';
|
||||||
import 'package:nc_photos/entity/face_recognition_person.dart';
|
import 'package:nc_photos/entity/face_recognition_person.dart';
|
||||||
import 'package:nc_photos/entity/file.dart';
|
import 'package:nc_photos/entity/file.dart';
|
||||||
import 'package:nc_photos/entity/file_descriptor.dart';
|
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||||
|
import 'package:nc_photos/entity/image_location/repo.dart';
|
||||||
import 'package:nc_photos/entity/nc_album.dart';
|
import 'package:nc_photos/entity/nc_album.dart';
|
||||||
import 'package:nc_photos/entity/nc_album_item.dart';
|
import 'package:nc_photos/entity/nc_album_item.dart';
|
||||||
import 'package:nc_photos/entity/recognize_face.dart';
|
import 'package:nc_photos/entity/recognize_face.dart';
|
||||||
|
@ -275,6 +276,16 @@ abstract class DbLocationGroupConverter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
abstract class DbImageLatLngConverter {
|
||||||
|
static ImageLatLng fromDb(DbImageLatLng src) {
|
||||||
|
return ImageLatLng(
|
||||||
|
latitude: src.lat,
|
||||||
|
longitude: src.lng,
|
||||||
|
fileId: src.fileId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension FileExtension on File {
|
extension FileExtension on File {
|
||||||
DbFileKey toDbKey() {
|
DbFileKey toDbKey() {
|
||||||
if (fileId != null) {
|
if (fileId != null) {
|
||||||
|
|
|
@ -4,6 +4,7 @@ import 'package:nc_photos/entity/face_recognition_person/repo.dart';
|
||||||
import 'package:nc_photos/entity/favorite.dart';
|
import 'package:nc_photos/entity/favorite.dart';
|
||||||
import 'package:nc_photos/entity/file.dart';
|
import 'package:nc_photos/entity/file.dart';
|
||||||
import 'package:nc_photos/entity/file/repo.dart';
|
import 'package:nc_photos/entity/file/repo.dart';
|
||||||
|
import 'package:nc_photos/entity/image_location/repo.dart';
|
||||||
import 'package:nc_photos/entity/local_file.dart';
|
import 'package:nc_photos/entity/local_file.dart';
|
||||||
import 'package:nc_photos/entity/nc_album/repo.dart';
|
import 'package:nc_photos/entity/nc_album/repo.dart';
|
||||||
import 'package:nc_photos/entity/pref.dart';
|
import 'package:nc_photos/entity/pref.dart';
|
||||||
|
@ -48,6 +49,7 @@ enum DiType {
|
||||||
recognizeFaceRepo,
|
recognizeFaceRepo,
|
||||||
recognizeFaceRepoRemote,
|
recognizeFaceRepoRemote,
|
||||||
recognizeFaceRepoLocal,
|
recognizeFaceRepoLocal,
|
||||||
|
imageLocationRepo,
|
||||||
pref,
|
pref,
|
||||||
touchManager,
|
touchManager,
|
||||||
npDb,
|
npDb,
|
||||||
|
@ -86,6 +88,7 @@ class DiContainer {
|
||||||
RecognizeFaceRepo? recognizeFaceRepo,
|
RecognizeFaceRepo? recognizeFaceRepo,
|
||||||
RecognizeFaceRepo? recognizeFaceRepoRemote,
|
RecognizeFaceRepo? recognizeFaceRepoRemote,
|
||||||
RecognizeFaceRepo? recognizeFaceRepoLocal,
|
RecognizeFaceRepo? recognizeFaceRepoLocal,
|
||||||
|
ImageLocationRepo? imageLocationRepo,
|
||||||
Pref? pref,
|
Pref? pref,
|
||||||
TouchManager? touchManager,
|
TouchManager? touchManager,
|
||||||
NpDb? npDb,
|
NpDb? npDb,
|
||||||
|
@ -120,6 +123,7 @@ class DiContainer {
|
||||||
_recognizeFaceRepo = recognizeFaceRepo,
|
_recognizeFaceRepo = recognizeFaceRepo,
|
||||||
_recognizeFaceRepoRemote = recognizeFaceRepoRemote,
|
_recognizeFaceRepoRemote = recognizeFaceRepoRemote,
|
||||||
_recognizeFaceRepoLocal = recognizeFaceRepoLocal,
|
_recognizeFaceRepoLocal = recognizeFaceRepoLocal,
|
||||||
|
_imageLocationRepo = imageLocationRepo,
|
||||||
_pref = pref,
|
_pref = pref,
|
||||||
_touchManager = touchManager,
|
_touchManager = touchManager,
|
||||||
_npDb = npDb,
|
_npDb = npDb,
|
||||||
|
@ -189,6 +193,8 @@ class DiContainer {
|
||||||
return contianer._recognizeFaceRepoRemote != null;
|
return contianer._recognizeFaceRepoRemote != null;
|
||||||
case DiType.recognizeFaceRepoLocal:
|
case DiType.recognizeFaceRepoLocal:
|
||||||
return contianer._recognizeFaceRepoLocal != null;
|
return contianer._recognizeFaceRepoLocal != null;
|
||||||
|
case DiType.imageLocationRepo:
|
||||||
|
return contianer._imageLocationRepo != null;
|
||||||
case DiType.pref:
|
case DiType.pref:
|
||||||
return contianer._pref != null;
|
return contianer._pref != null;
|
||||||
case DiType.touchManager:
|
case DiType.touchManager:
|
||||||
|
@ -215,6 +221,7 @@ class DiContainer {
|
||||||
OrNull<NcAlbumRepo>? ncAlbumRepo,
|
OrNull<NcAlbumRepo>? ncAlbumRepo,
|
||||||
OrNull<FaceRecognitionPersonRepo>? faceRecognitionPersonRepo,
|
OrNull<FaceRecognitionPersonRepo>? faceRecognitionPersonRepo,
|
||||||
OrNull<RecognizeFaceRepo>? recognizeFaceRepo,
|
OrNull<RecognizeFaceRepo>? recognizeFaceRepo,
|
||||||
|
OrNull<ImageLocationRepo>? imageLocationRepo,
|
||||||
OrNull<Pref>? pref,
|
OrNull<Pref>? pref,
|
||||||
OrNull<TouchManager>? touchManager,
|
OrNull<TouchManager>? touchManager,
|
||||||
OrNull<NpDb>? npDb,
|
OrNull<NpDb>? npDb,
|
||||||
|
@ -240,6 +247,9 @@ class DiContainer {
|
||||||
recognizeFaceRepo: recognizeFaceRepo == null
|
recognizeFaceRepo: recognizeFaceRepo == null
|
||||||
? _recognizeFaceRepo
|
? _recognizeFaceRepo
|
||||||
: recognizeFaceRepo.obj,
|
: recognizeFaceRepo.obj,
|
||||||
|
imageLocationRepo: imageLocationRepo == null
|
||||||
|
? _imageLocationRepo
|
||||||
|
: imageLocationRepo.obj,
|
||||||
pref: pref == null ? _pref : pref.obj,
|
pref: pref == null ? _pref : pref.obj,
|
||||||
touchManager: touchManager == null ? _touchManager : touchManager.obj,
|
touchManager: touchManager == null ? _touchManager : touchManager.obj,
|
||||||
npDb: npDb == null ? _npDb : npDb.obj,
|
npDb: npDb == null ? _npDb : npDb.obj,
|
||||||
|
@ -280,6 +290,7 @@ class DiContainer {
|
||||||
RecognizeFaceRepo get recognizeFaceRepo => _recognizeFaceRepo!;
|
RecognizeFaceRepo get recognizeFaceRepo => _recognizeFaceRepo!;
|
||||||
RecognizeFaceRepo get recognizeFaceRepoRemote => _recognizeFaceRepoRemote!;
|
RecognizeFaceRepo get recognizeFaceRepoRemote => _recognizeFaceRepoRemote!;
|
||||||
RecognizeFaceRepo get recognizeFaceRepoLocal => _recognizeFaceRepoLocal!;
|
RecognizeFaceRepo get recognizeFaceRepoLocal => _recognizeFaceRepoLocal!;
|
||||||
|
ImageLocationRepo get imageLocationRepo => _imageLocationRepo!;
|
||||||
|
|
||||||
Pref get pref => _pref!;
|
Pref get pref => _pref!;
|
||||||
TouchManager get touchManager => _touchManager!;
|
TouchManager get touchManager => _touchManager!;
|
||||||
|
@ -436,6 +447,11 @@ class DiContainer {
|
||||||
_recognizeFaceRepoLocal = v;
|
_recognizeFaceRepoLocal = v;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
set imageLocationRepo(ImageLocationRepo v) {
|
||||||
|
assert(_imageLocationRepo == null);
|
||||||
|
_imageLocationRepo = v;
|
||||||
|
}
|
||||||
|
|
||||||
set pref(Pref v) {
|
set pref(Pref v) {
|
||||||
assert(_pref == null);
|
assert(_pref == null);
|
||||||
_pref = v;
|
_pref = v;
|
||||||
|
@ -489,6 +505,7 @@ class DiContainer {
|
||||||
RecognizeFaceRepo? _recognizeFaceRepo;
|
RecognizeFaceRepo? _recognizeFaceRepo;
|
||||||
RecognizeFaceRepo? _recognizeFaceRepoRemote;
|
RecognizeFaceRepo? _recognizeFaceRepoRemote;
|
||||||
RecognizeFaceRepo? _recognizeFaceRepoLocal;
|
RecognizeFaceRepo? _recognizeFaceRepoLocal;
|
||||||
|
ImageLocationRepo? _imageLocationRepo;
|
||||||
|
|
||||||
Pref? _pref;
|
Pref? _pref;
|
||||||
TouchManager? _touchManager;
|
TouchManager? _touchManager;
|
||||||
|
|
|
@ -2,12 +2,14 @@ import 'package:flutter/foundation.dart';
|
||||||
import 'package:nc_photos/account.dart';
|
import 'package:nc_photos/account.dart';
|
||||||
import 'package:nc_photos/di_container.dart';
|
import 'package:nc_photos/di_container.dart';
|
||||||
import 'package:nc_photos/entity/collection.dart';
|
import 'package:nc_photos/entity/collection.dart';
|
||||||
|
import 'package:nc_photos/entity/collection/adapter/ad_hoc.dart';
|
||||||
import 'package:nc_photos/entity/collection/adapter/album.dart';
|
import 'package:nc_photos/entity/collection/adapter/album.dart';
|
||||||
import 'package:nc_photos/entity/collection/adapter/location_group.dart';
|
import 'package:nc_photos/entity/collection/adapter/location_group.dart';
|
||||||
import 'package:nc_photos/entity/collection/adapter/memory.dart';
|
import 'package:nc_photos/entity/collection/adapter/memory.dart';
|
||||||
import 'package:nc_photos/entity/collection/adapter/nc_album.dart';
|
import 'package:nc_photos/entity/collection/adapter/nc_album.dart';
|
||||||
import 'package:nc_photos/entity/collection/adapter/person.dart';
|
import 'package:nc_photos/entity/collection/adapter/person.dart';
|
||||||
import 'package:nc_photos/entity/collection/adapter/tag.dart';
|
import 'package:nc_photos/entity/collection/adapter/tag.dart';
|
||||||
|
import 'package:nc_photos/entity/collection/content_provider/ad_hoc.dart';
|
||||||
import 'package:nc_photos/entity/collection/content_provider/album.dart';
|
import 'package:nc_photos/entity/collection/content_provider/album.dart';
|
||||||
import 'package:nc_photos/entity/collection/content_provider/location_group.dart';
|
import 'package:nc_photos/entity/collection/content_provider/location_group.dart';
|
||||||
import 'package:nc_photos/entity/collection/content_provider/memory.dart';
|
import 'package:nc_photos/entity/collection/content_provider/memory.dart';
|
||||||
|
@ -30,6 +32,8 @@ abstract class CollectionAdapter {
|
||||||
static CollectionAdapter of(
|
static CollectionAdapter of(
|
||||||
DiContainer c, Account account, Collection collection) {
|
DiContainer c, Account account, Collection collection) {
|
||||||
switch (collection.contentProvider.runtimeType) {
|
switch (collection.contentProvider.runtimeType) {
|
||||||
|
case const (CollectionAdHocProvider):
|
||||||
|
return CollectionAdHocAdapter(c, account, collection);
|
||||||
case const (CollectionAlbumProvider):
|
case const (CollectionAlbumProvider):
|
||||||
return CollectionAlbumAdapter(c, account, collection);
|
return CollectionAlbumAdapter(c, account, collection);
|
||||||
case const (CollectionLocationGroupProvider):
|
case const (CollectionLocationGroupProvider):
|
||||||
|
|
57
app/lib/entity/collection/adapter/ad_hoc.dart
Normal file
57
app/lib/entity/collection/adapter/ad_hoc.dart
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import 'package:nc_photos/account.dart';
|
||||||
|
import 'package:nc_photos/di_container.dart';
|
||||||
|
import 'package:nc_photos/entity/collection.dart';
|
||||||
|
import 'package:nc_photos/entity/collection/adapter.dart';
|
||||||
|
import 'package:nc_photos/entity/collection/adapter/adapter_mixin.dart';
|
||||||
|
import 'package:nc_photos/entity/collection/content_provider/ad_hoc.dart';
|
||||||
|
import 'package:nc_photos/entity/collection_item.dart';
|
||||||
|
import 'package:nc_photos/entity/collection_item/basic_item.dart';
|
||||||
|
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
||||||
|
import 'package:nc_photos/use_case/find_file_descriptor.dart';
|
||||||
|
|
||||||
|
class CollectionAdHocAdapter
|
||||||
|
with
|
||||||
|
CollectionAdapterReadOnlyTag,
|
||||||
|
CollectionAdapterUnremovableTag,
|
||||||
|
CollectionAdapterUnshareableTag
|
||||||
|
implements CollectionAdapter {
|
||||||
|
CollectionAdHocAdapter(this._c, this.account, this.collection)
|
||||||
|
: _provider = collection.contentProvider as CollectionAdHocProvider;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<List<CollectionItem>> listItem() async* {
|
||||||
|
final files = await FindFileDescriptor(_c)(
|
||||||
|
account,
|
||||||
|
_provider.fileIds,
|
||||||
|
onFileNotFound: (_) {
|
||||||
|
// ignore not found
|
||||||
|
},
|
||||||
|
);
|
||||||
|
yield files
|
||||||
|
.where((f) => file_util.isSupportedFormat(f))
|
||||||
|
.map((f) => BasicCollectionFileItem(f))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<CollectionItem> adaptToNewItem(CollectionItem original) async {
|
||||||
|
if (original is CollectionFileItem) {
|
||||||
|
return BasicCollectionFileItem(original.file);
|
||||||
|
} else {
|
||||||
|
throw UnsupportedError("Unsupported type: ${original.runtimeType}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool isItemDeletable(CollectionItem item) => true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool isPermitted(CollectionCapability capability) =>
|
||||||
|
_provider.capabilities.contains(capability);
|
||||||
|
|
||||||
|
final DiContainer _c;
|
||||||
|
final Account account;
|
||||||
|
final Collection collection;
|
||||||
|
|
||||||
|
final CollectionAdHocProvider _provider;
|
||||||
|
}
|
83
app/lib/entity/collection/content_provider/ad_hoc.dart
Normal file
83
app/lib/entity/collection/content_provider/ad_hoc.dart
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
import 'package:clock/clock.dart';
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:nc_photos/account.dart';
|
||||||
|
import 'package:nc_photos/api/api_util.dart' as api_util;
|
||||||
|
import 'package:nc_photos/entity/collection.dart';
|
||||||
|
import 'package:nc_photos/entity/collection/util.dart';
|
||||||
|
import 'package:nc_photos/entity/collection_item/util.dart';
|
||||||
|
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||||
|
import 'package:np_common/object_util.dart';
|
||||||
|
import 'package:to_string/to_string.dart';
|
||||||
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
|
part 'ad_hoc.g.dart';
|
||||||
|
|
||||||
|
@toString
|
||||||
|
class CollectionAdHocProvider
|
||||||
|
with EquatableMixin
|
||||||
|
implements CollectionContentProvider {
|
||||||
|
CollectionAdHocProvider({
|
||||||
|
required this.account,
|
||||||
|
required this.fileIds,
|
||||||
|
this.cover,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get fourCc => "ADHC";
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get id => _id;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int? get count => fileIds.length;
|
||||||
|
|
||||||
|
@override
|
||||||
|
DateTime get lastModified => clock.now();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<CollectionCapability> get capabilities => [
|
||||||
|
CollectionCapability.deleteItem,
|
||||||
|
];
|
||||||
|
|
||||||
|
@override
|
||||||
|
CollectionItemSort get itemSort => CollectionItemSort.dateDescending;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<CollectionShare> get shares => [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? getCoverUrl(
|
||||||
|
int width,
|
||||||
|
int height, {
|
||||||
|
bool? isKeepAspectRatio,
|
||||||
|
}) {
|
||||||
|
return cover?.let((cover) => api_util.getFilePreviewUrl(
|
||||||
|
account,
|
||||||
|
cover,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
isKeepAspectRatio: isKeepAspectRatio ?? false,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get isDynamicCollection => true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get isPendingSharedAlbum => false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get isOwned => true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => _$toString();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [account, fileIds, cover];
|
||||||
|
|
||||||
|
final Account account;
|
||||||
|
final List<int> fileIds;
|
||||||
|
final FileDescriptor? cover;
|
||||||
|
|
||||||
|
late final _id = const Uuid().v4();
|
||||||
|
}
|
14
app/lib/entity/collection/content_provider/ad_hoc.g.dart
Normal file
14
app/lib/entity/collection/content_provider/ad_hoc.g.dart
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'ad_hoc.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// ToStringGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
extension _$CollectionAdHocProviderToString on CollectionAdHocProvider {
|
||||||
|
String _$toString() {
|
||||||
|
// ignore: unnecessary_string_interpolations
|
||||||
|
return "CollectionAdHocProvider {account: $account, fileIds: [length: ${fileIds.length}], cover: ${cover == null ? null : "${cover!.fdPath}"}, _id: $_id}";
|
||||||
|
}
|
||||||
|
}
|
38
app/lib/entity/image_location/data_source.dart
Normal file
38
app/lib/entity/image_location/data_source.dart
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:nc_photos/account.dart';
|
||||||
|
import 'package:nc_photos/db/entity_converter.dart';
|
||||||
|
import 'package:nc_photos/entity/file.dart';
|
||||||
|
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||||
|
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
||||||
|
import 'package:nc_photos/entity/image_location/repo.dart';
|
||||||
|
import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util;
|
||||||
|
import 'package:np_async/np_async.dart';
|
||||||
|
import 'package:np_codegen/np_codegen.dart';
|
||||||
|
import 'package:np_datetime/np_datetime.dart';
|
||||||
|
import 'package:np_db/np_db.dart';
|
||||||
|
|
||||||
|
part 'data_source.g.dart';
|
||||||
|
|
||||||
|
@npLog
|
||||||
|
class ImageLocationNpDbDataSource implements ImageLocationDataSource {
|
||||||
|
const ImageLocationNpDbDataSource(this.db);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<ImageLatLng>> getLocations(
|
||||||
|
Account account, TimeRange timeRange) async {
|
||||||
|
_log.info("[getLocations] timeRange: $timeRange");
|
||||||
|
final results = await db.getImageLatLngWithFileIds(
|
||||||
|
account: account.toDb(),
|
||||||
|
timeRange: timeRange,
|
||||||
|
includeRelativeRoots: account.roots
|
||||||
|
.map((e) => File(path: file_util.unstripPath(account, e))
|
||||||
|
.strippedPathWithEmpty)
|
||||||
|
.toList(),
|
||||||
|
excludeRelativeRoots: [remote_storage_util.remoteStorageDirRelativePath],
|
||||||
|
mimes: file_util.supportedFormatMimes,
|
||||||
|
);
|
||||||
|
return results.computeAll(DbImageLatLngConverter.fromDb);
|
||||||
|
}
|
||||||
|
|
||||||
|
final NpDb db;
|
||||||
|
}
|
15
app/lib/entity/image_location/data_source.g.dart
Normal file
15
app/lib/entity/image_location/data_source.g.dart
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'data_source.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// NpLogGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
extension _$ImageLocationNpDbDataSourceNpLog on ImageLocationNpDbDataSource {
|
||||||
|
// ignore: unused_element
|
||||||
|
Logger get _log => log;
|
||||||
|
|
||||||
|
static final log =
|
||||||
|
Logger("entity.image_location.data_source.ImageLocationNpDbDataSource");
|
||||||
|
}
|
52
app/lib/entity/image_location/repo.dart
Normal file
52
app/lib/entity/image_location/repo.dart
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:nc_photos/account.dart';
|
||||||
|
import 'package:np_codegen/np_codegen.dart';
|
||||||
|
import 'package:np_datetime/np_datetime.dart';
|
||||||
|
|
||||||
|
part 'repo.g.dart';
|
||||||
|
|
||||||
|
class ImageLatLng with EquatableMixin {
|
||||||
|
const ImageLatLng({
|
||||||
|
required this.latitude,
|
||||||
|
required this.longitude,
|
||||||
|
required this.fileId,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [
|
||||||
|
latitude,
|
||||||
|
longitude,
|
||||||
|
fileId,
|
||||||
|
];
|
||||||
|
|
||||||
|
final double latitude;
|
||||||
|
final double longitude;
|
||||||
|
final int fileId;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class ImageLocationRepo {
|
||||||
|
/// Query all locations with the corresponding file ids
|
||||||
|
///
|
||||||
|
/// Returned data are sorted by the file date time in descending order
|
||||||
|
Future<List<ImageLatLng>> getLocations(Account account, TimeRange timeRange);
|
||||||
|
}
|
||||||
|
|
||||||
|
@npLog
|
||||||
|
class BasicImageLocationRepo implements ImageLocationRepo {
|
||||||
|
const BasicImageLocationRepo(this.dataSrc);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<ImageLatLng>> getLocations(
|
||||||
|
Account account, TimeRange timeRange) =>
|
||||||
|
dataSrc.getLocations(account, timeRange);
|
||||||
|
|
||||||
|
final ImageLocationDataSource dataSrc;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class ImageLocationDataSource {
|
||||||
|
/// Query all locations with the corresponding file ids
|
||||||
|
///
|
||||||
|
/// Returned data are sorted by the file date time in descending order
|
||||||
|
Future<List<ImageLatLng>> getLocations(Account account, TimeRange timeRange);
|
||||||
|
}
|
15
app/lib/entity/image_location/repo.g.dart
Normal file
15
app/lib/entity/image_location/repo.g.dart
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'repo.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// NpLogGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
extension _$BasicImageLocationRepoNpLog on BasicImageLocationRepo {
|
||||||
|
// ignore: unused_element
|
||||||
|
Logger get _log => log;
|
||||||
|
|
||||||
|
static final log =
|
||||||
|
Logger("entity.image_location.repo.BasicImageLocationRepo");
|
||||||
|
}
|
|
@ -113,6 +113,7 @@ enum PrefKey implements PrefKeyInterface {
|
||||||
protectedPageAuthPin,
|
protectedPageAuthPin,
|
||||||
protectedPageAuthPassword,
|
protectedPageAuthPassword,
|
||||||
dontShowVideoPreviewHint,
|
dontShowVideoPreviewHint,
|
||||||
|
mapBrowserPrevPosition,
|
||||||
;
|
;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -199,6 +200,8 @@ enum PrefKey implements PrefKeyInterface {
|
||||||
return "protectedPageAuthPassword";
|
return "protectedPageAuthPassword";
|
||||||
case PrefKey.dontShowVideoPreviewHint:
|
case PrefKey.dontShowVideoPreviewHint:
|
||||||
return "dontShowVideoPreviewHint";
|
return "dontShowVideoPreviewHint";
|
||||||
|
case PrefKey.mapBrowserPrevPosition:
|
||||||
|
return "mapBrowserPrevPosition";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:nc_photos/object_extension.dart';
|
import 'package:nc_photos/object_extension.dart';
|
||||||
|
|
||||||
/// Convert a boolean to an indexable type in json for DB
|
/// Convert a boolean to an indexable type in json for DB
|
||||||
|
@ -8,3 +10,14 @@ Object? boolToJson(bool? value) => value?.run((v) => v ? 1 : 0);
|
||||||
|
|
||||||
/// Convert a boolean from an indexable type in json for DB
|
/// Convert a boolean from an indexable type in json for DB
|
||||||
bool? boolFromJson(Object? value) => value?.run((v) => v != 0);
|
bool? boolFromJson(Object? value) => value?.run((v) => v != 0);
|
||||||
|
|
||||||
|
Object? tryJsonDecode(String source) {
|
||||||
|
try {
|
||||||
|
return jsonDecode(source);
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Object? jsonDecodeOr(String source, dynamic def) =>
|
||||||
|
tryJsonDecode(source) ?? def;
|
||||||
|
|
|
@ -1487,6 +1487,15 @@
|
||||||
"trustedCertManagerFailedToRemoveCertError": "Failed to remove certificate",
|
"trustedCertManagerFailedToRemoveCertError": "Failed to remove certificate",
|
||||||
"missingVideoThumbnailHelpDialogTitle": "Having trouble with video thumbnails?",
|
"missingVideoThumbnailHelpDialogTitle": "Having trouble with video thumbnails?",
|
||||||
"dontShowAgain": "Don't show again",
|
"dontShowAgain": "Don't show again",
|
||||||
|
"mapBrowserDateRangeLabel": "Date range",
|
||||||
|
"@mapBrowserDateRangeLabel": {
|
||||||
|
"description": "Filter photos by date range"
|
||||||
|
},
|
||||||
|
"mapBrowserDateRangeThisMonth": "This month",
|
||||||
|
"mapBrowserDateRangePrevMonth": "Previous month",
|
||||||
|
"mapBrowserDateRangeThisYear": "This year",
|
||||||
|
"mapBrowserDateRangeCustom": "Custom",
|
||||||
|
"homeTabMapBrowser": "Map",
|
||||||
|
|
||||||
"errorUnauthenticated": "Unauthenticated access. Please sign-in again if the problem continues",
|
"errorUnauthenticated": "Unauthenticated access. Please sign-in again if the problem continues",
|
||||||
"@errorUnauthenticated": {
|
"@errorUnauthenticated": {
|
||||||
|
|
|
@ -249,6 +249,12 @@
|
||||||
"trustedCertManagerFailedToRemoveCertError",
|
"trustedCertManagerFailedToRemoveCertError",
|
||||||
"missingVideoThumbnailHelpDialogTitle",
|
"missingVideoThumbnailHelpDialogTitle",
|
||||||
"dontShowAgain",
|
"dontShowAgain",
|
||||||
|
"mapBrowserDateRangeLabel",
|
||||||
|
"mapBrowserDateRangeThisMonth",
|
||||||
|
"mapBrowserDateRangePrevMonth",
|
||||||
|
"mapBrowserDateRangeThisYear",
|
||||||
|
"mapBrowserDateRangeCustom",
|
||||||
|
"homeTabMapBrowser",
|
||||||
"errorUnauthenticated",
|
"errorUnauthenticated",
|
||||||
"errorDisconnected",
|
"errorDisconnected",
|
||||||
"errorLocked",
|
"errorLocked",
|
||||||
|
@ -284,7 +290,13 @@
|
||||||
"trustedCertManagerNoHttpsServerError",
|
"trustedCertManagerNoHttpsServerError",
|
||||||
"trustedCertManagerFailedToRemoveCertError",
|
"trustedCertManagerFailedToRemoveCertError",
|
||||||
"missingVideoThumbnailHelpDialogTitle",
|
"missingVideoThumbnailHelpDialogTitle",
|
||||||
"dontShowAgain"
|
"dontShowAgain",
|
||||||
|
"mapBrowserDateRangeLabel",
|
||||||
|
"mapBrowserDateRangeThisMonth",
|
||||||
|
"mapBrowserDateRangePrevMonth",
|
||||||
|
"mapBrowserDateRangeThisYear",
|
||||||
|
"mapBrowserDateRangeCustom",
|
||||||
|
"homeTabMapBrowser"
|
||||||
],
|
],
|
||||||
|
|
||||||
"de": [
|
"de": [
|
||||||
|
@ -318,7 +330,13 @@
|
||||||
"trustedCertManagerNoHttpsServerError",
|
"trustedCertManagerNoHttpsServerError",
|
||||||
"trustedCertManagerFailedToRemoveCertError",
|
"trustedCertManagerFailedToRemoveCertError",
|
||||||
"missingVideoThumbnailHelpDialogTitle",
|
"missingVideoThumbnailHelpDialogTitle",
|
||||||
"dontShowAgain"
|
"dontShowAgain",
|
||||||
|
"mapBrowserDateRangeLabel",
|
||||||
|
"mapBrowserDateRangeThisMonth",
|
||||||
|
"mapBrowserDateRangePrevMonth",
|
||||||
|
"mapBrowserDateRangeThisYear",
|
||||||
|
"mapBrowserDateRangeCustom",
|
||||||
|
"homeTabMapBrowser"
|
||||||
],
|
],
|
||||||
|
|
||||||
"el": [
|
"el": [
|
||||||
|
@ -455,7 +473,13 @@
|
||||||
"trustedCertManagerNoHttpsServerError",
|
"trustedCertManagerNoHttpsServerError",
|
||||||
"trustedCertManagerFailedToRemoveCertError",
|
"trustedCertManagerFailedToRemoveCertError",
|
||||||
"missingVideoThumbnailHelpDialogTitle",
|
"missingVideoThumbnailHelpDialogTitle",
|
||||||
"dontShowAgain"
|
"dontShowAgain",
|
||||||
|
"mapBrowserDateRangeLabel",
|
||||||
|
"mapBrowserDateRangeThisMonth",
|
||||||
|
"mapBrowserDateRangePrevMonth",
|
||||||
|
"mapBrowserDateRangeThisYear",
|
||||||
|
"mapBrowserDateRangeCustom",
|
||||||
|
"homeTabMapBrowser"
|
||||||
],
|
],
|
||||||
|
|
||||||
"es": [
|
"es": [
|
||||||
|
@ -483,7 +507,13 @@
|
||||||
"trustedCertManagerNoHttpsServerError",
|
"trustedCertManagerNoHttpsServerError",
|
||||||
"trustedCertManagerFailedToRemoveCertError",
|
"trustedCertManagerFailedToRemoveCertError",
|
||||||
"missingVideoThumbnailHelpDialogTitle",
|
"missingVideoThumbnailHelpDialogTitle",
|
||||||
"dontShowAgain"
|
"dontShowAgain",
|
||||||
|
"mapBrowserDateRangeLabel",
|
||||||
|
"mapBrowserDateRangeThisMonth",
|
||||||
|
"mapBrowserDateRangePrevMonth",
|
||||||
|
"mapBrowserDateRangeThisYear",
|
||||||
|
"mapBrowserDateRangeCustom",
|
||||||
|
"homeTabMapBrowser"
|
||||||
],
|
],
|
||||||
|
|
||||||
"fi": [
|
"fi": [
|
||||||
|
@ -511,7 +541,13 @@
|
||||||
"trustedCertManagerNoHttpsServerError",
|
"trustedCertManagerNoHttpsServerError",
|
||||||
"trustedCertManagerFailedToRemoveCertError",
|
"trustedCertManagerFailedToRemoveCertError",
|
||||||
"missingVideoThumbnailHelpDialogTitle",
|
"missingVideoThumbnailHelpDialogTitle",
|
||||||
"dontShowAgain"
|
"dontShowAgain",
|
||||||
|
"mapBrowserDateRangeLabel",
|
||||||
|
"mapBrowserDateRangeThisMonth",
|
||||||
|
"mapBrowserDateRangePrevMonth",
|
||||||
|
"mapBrowserDateRangeThisYear",
|
||||||
|
"mapBrowserDateRangeCustom",
|
||||||
|
"homeTabMapBrowser"
|
||||||
],
|
],
|
||||||
|
|
||||||
"fr": [
|
"fr": [
|
||||||
|
@ -539,7 +575,13 @@
|
||||||
"trustedCertManagerNoHttpsServerError",
|
"trustedCertManagerNoHttpsServerError",
|
||||||
"trustedCertManagerFailedToRemoveCertError",
|
"trustedCertManagerFailedToRemoveCertError",
|
||||||
"missingVideoThumbnailHelpDialogTitle",
|
"missingVideoThumbnailHelpDialogTitle",
|
||||||
"dontShowAgain"
|
"dontShowAgain",
|
||||||
|
"mapBrowserDateRangeLabel",
|
||||||
|
"mapBrowserDateRangeThisMonth",
|
||||||
|
"mapBrowserDateRangePrevMonth",
|
||||||
|
"mapBrowserDateRangeThisYear",
|
||||||
|
"mapBrowserDateRangeCustom",
|
||||||
|
"homeTabMapBrowser"
|
||||||
],
|
],
|
||||||
|
|
||||||
"it": [
|
"it": [
|
||||||
|
@ -572,7 +614,13 @@
|
||||||
"trustedCertManagerNoHttpsServerError",
|
"trustedCertManagerNoHttpsServerError",
|
||||||
"trustedCertManagerFailedToRemoveCertError",
|
"trustedCertManagerFailedToRemoveCertError",
|
||||||
"missingVideoThumbnailHelpDialogTitle",
|
"missingVideoThumbnailHelpDialogTitle",
|
||||||
"dontShowAgain"
|
"dontShowAgain",
|
||||||
|
"mapBrowserDateRangeLabel",
|
||||||
|
"mapBrowserDateRangeThisMonth",
|
||||||
|
"mapBrowserDateRangePrevMonth",
|
||||||
|
"mapBrowserDateRangeThisYear",
|
||||||
|
"mapBrowserDateRangeCustom",
|
||||||
|
"homeTabMapBrowser"
|
||||||
],
|
],
|
||||||
|
|
||||||
"nl": [
|
"nl": [
|
||||||
|
@ -942,6 +990,12 @@
|
||||||
"trustedCertManagerFailedToRemoveCertError",
|
"trustedCertManagerFailedToRemoveCertError",
|
||||||
"missingVideoThumbnailHelpDialogTitle",
|
"missingVideoThumbnailHelpDialogTitle",
|
||||||
"dontShowAgain",
|
"dontShowAgain",
|
||||||
|
"mapBrowserDateRangeLabel",
|
||||||
|
"mapBrowserDateRangeThisMonth",
|
||||||
|
"mapBrowserDateRangePrevMonth",
|
||||||
|
"mapBrowserDateRangeThisYear",
|
||||||
|
"mapBrowserDateRangeCustom",
|
||||||
|
"homeTabMapBrowser",
|
||||||
"errorUnauthenticated",
|
"errorUnauthenticated",
|
||||||
"errorDisconnected",
|
"errorDisconnected",
|
||||||
"errorLocked",
|
"errorLocked",
|
||||||
|
@ -981,7 +1035,13 @@
|
||||||
"trustedCertManagerNoHttpsServerError",
|
"trustedCertManagerNoHttpsServerError",
|
||||||
"trustedCertManagerFailedToRemoveCertError",
|
"trustedCertManagerFailedToRemoveCertError",
|
||||||
"missingVideoThumbnailHelpDialogTitle",
|
"missingVideoThumbnailHelpDialogTitle",
|
||||||
"dontShowAgain"
|
"dontShowAgain",
|
||||||
|
"mapBrowserDateRangeLabel",
|
||||||
|
"mapBrowserDateRangeThisMonth",
|
||||||
|
"mapBrowserDateRangePrevMonth",
|
||||||
|
"mapBrowserDateRangeThisYear",
|
||||||
|
"mapBrowserDateRangeCustom",
|
||||||
|
"homeTabMapBrowser"
|
||||||
],
|
],
|
||||||
|
|
||||||
"pt": [
|
"pt": [
|
||||||
|
@ -1029,7 +1089,13 @@
|
||||||
"trustedCertManagerNoHttpsServerError",
|
"trustedCertManagerNoHttpsServerError",
|
||||||
"trustedCertManagerFailedToRemoveCertError",
|
"trustedCertManagerFailedToRemoveCertError",
|
||||||
"missingVideoThumbnailHelpDialogTitle",
|
"missingVideoThumbnailHelpDialogTitle",
|
||||||
"dontShowAgain"
|
"dontShowAgain",
|
||||||
|
"mapBrowserDateRangeLabel",
|
||||||
|
"mapBrowserDateRangeThisMonth",
|
||||||
|
"mapBrowserDateRangePrevMonth",
|
||||||
|
"mapBrowserDateRangeThisYear",
|
||||||
|
"mapBrowserDateRangeCustom",
|
||||||
|
"homeTabMapBrowser"
|
||||||
],
|
],
|
||||||
|
|
||||||
"ru": [
|
"ru": [
|
||||||
|
@ -1057,7 +1123,22 @@
|
||||||
"trustedCertManagerNoHttpsServerError",
|
"trustedCertManagerNoHttpsServerError",
|
||||||
"trustedCertManagerFailedToRemoveCertError",
|
"trustedCertManagerFailedToRemoveCertError",
|
||||||
"missingVideoThumbnailHelpDialogTitle",
|
"missingVideoThumbnailHelpDialogTitle",
|
||||||
"dontShowAgain"
|
"dontShowAgain",
|
||||||
|
"mapBrowserDateRangeLabel",
|
||||||
|
"mapBrowserDateRangeThisMonth",
|
||||||
|
"mapBrowserDateRangePrevMonth",
|
||||||
|
"mapBrowserDateRangeThisYear",
|
||||||
|
"mapBrowserDateRangeCustom",
|
||||||
|
"homeTabMapBrowser"
|
||||||
|
],
|
||||||
|
|
||||||
|
"tr": [
|
||||||
|
"mapBrowserDateRangeLabel",
|
||||||
|
"mapBrowserDateRangeThisMonth",
|
||||||
|
"mapBrowserDateRangePrevMonth",
|
||||||
|
"mapBrowserDateRangeThisYear",
|
||||||
|
"mapBrowserDateRangeCustom",
|
||||||
|
"homeTabMapBrowser"
|
||||||
],
|
],
|
||||||
|
|
||||||
"zh": [
|
"zh": [
|
||||||
|
@ -1116,7 +1197,13 @@
|
||||||
"trustedCertManagerNoHttpsServerError",
|
"trustedCertManagerNoHttpsServerError",
|
||||||
"trustedCertManagerFailedToRemoveCertError",
|
"trustedCertManagerFailedToRemoveCertError",
|
||||||
"missingVideoThumbnailHelpDialogTitle",
|
"missingVideoThumbnailHelpDialogTitle",
|
||||||
"dontShowAgain"
|
"dontShowAgain",
|
||||||
|
"mapBrowserDateRangeLabel",
|
||||||
|
"mapBrowserDateRangeThisMonth",
|
||||||
|
"mapBrowserDateRangePrevMonth",
|
||||||
|
"mapBrowserDateRangeThisYear",
|
||||||
|
"mapBrowserDateRangeCustom",
|
||||||
|
"homeTabMapBrowser"
|
||||||
],
|
],
|
||||||
|
|
||||||
"zh_Hant": [
|
"zh_Hant": [
|
||||||
|
@ -1269,6 +1356,12 @@
|
||||||
"trustedCertManagerNoHttpsServerError",
|
"trustedCertManagerNoHttpsServerError",
|
||||||
"trustedCertManagerFailedToRemoveCertError",
|
"trustedCertManagerFailedToRemoveCertError",
|
||||||
"missingVideoThumbnailHelpDialogTitle",
|
"missingVideoThumbnailHelpDialogTitle",
|
||||||
"dontShowAgain"
|
"dontShowAgain",
|
||||||
|
"mapBrowserDateRangeLabel",
|
||||||
|
"mapBrowserDateRangeThisMonth",
|
||||||
|
"mapBrowserDateRangePrevMonth",
|
||||||
|
"mapBrowserDateRangeThisYear",
|
||||||
|
"mapBrowserDateRangeCustom",
|
||||||
|
"homeTabMapBrowser"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ import 'package:nc_photos/use_case/import_potential_shared_album.dart';
|
||||||
import 'package:nc_photos/widget/home_collections.dart';
|
import 'package:nc_photos/widget/home_collections.dart';
|
||||||
import 'package:nc_photos/widget/home_photos2.dart';
|
import 'package:nc_photos/widget/home_photos2.dart';
|
||||||
import 'package:nc_photos/widget/home_search.dart';
|
import 'package:nc_photos/widget/home_search.dart';
|
||||||
|
import 'package:nc_photos/widget/map_browser.dart';
|
||||||
import 'package:np_codegen/np_codegen.dart';
|
import 'package:np_codegen/np_codegen.dart';
|
||||||
import 'package:np_common/or_null.dart';
|
import 'package:np_common/or_null.dart';
|
||||||
|
|
||||||
|
@ -87,7 +88,7 @@ class _HomeState extends State<Home> with TickerProviderStateMixin {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
bottomNavigationBar: _buildBottomNavigationBar(context),
|
bottomNavigationBar: _buildBottomNavigationBar(context),
|
||||||
body: Builder(builder: (context) => _buildContent(context)),
|
body: Builder(builder: (context) => _buildContent(context)),
|
||||||
|
@ -114,6 +115,11 @@ class _HomeState extends State<Home> with TickerProviderStateMixin {
|
||||||
selectedIcon: const Icon(Icons.grid_view_sharp),
|
selectedIcon: const Icon(Icons.grid_view_sharp),
|
||||||
label: L10n.global().collectionsTooltip,
|
label: L10n.global().collectionsTooltip,
|
||||||
),
|
),
|
||||||
|
NavigationDestination(
|
||||||
|
icon: const Icon(Icons.map_outlined),
|
||||||
|
selectedIcon: const Icon(Icons.map),
|
||||||
|
label: L10n.global().homeTabMapBrowser,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
selectedIndex: _nextPage,
|
selectedIndex: _nextPage,
|
||||||
onDestinationSelected: _onTapNavItem,
|
onDestinationSelected: _onTapNavItem,
|
||||||
|
@ -125,7 +131,7 @@ class _HomeState extends State<Home> with TickerProviderStateMixin {
|
||||||
return PageView.builder(
|
return PageView.builder(
|
||||||
controller: _pageController,
|
controller: _pageController,
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
itemCount: 3,
|
itemCount: 4,
|
||||||
itemBuilder: (context, index) => SlideTransition(
|
itemBuilder: (context, index) => SlideTransition(
|
||||||
position: Tween(
|
position: Tween(
|
||||||
begin: const Offset(0, .05),
|
begin: const Offset(0, .05),
|
||||||
|
@ -152,6 +158,9 @@ class _HomeState extends State<Home> with TickerProviderStateMixin {
|
||||||
case 2:
|
case 2:
|
||||||
return const HomeCollections();
|
return const HomeCollections();
|
||||||
|
|
||||||
|
case 3:
|
||||||
|
return const MapBrowser();
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw ArgumentError("Invalid page index: $index");
|
throw ArgumentError("Invalid page index: $index");
|
||||||
}
|
}
|
||||||
|
|
131
app/lib/widget/map_browser.dart
Normal file
131
app/lib/widget/map_browser.dart
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:clock/clock.dart';
|
||||||
|
import 'package:copy_with/copy_with.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:google_maps_cluster_manager/google_maps_cluster_manager.dart';
|
||||||
|
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||||
|
import 'package:intl/intl.dart' as intl;
|
||||||
|
import 'package:kiwi/kiwi.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:nc_photos/account.dart';
|
||||||
|
import 'package:nc_photos/app_localizations.dart';
|
||||||
|
import 'package:nc_photos/bloc_util.dart';
|
||||||
|
import 'package:nc_photos/controller/account_controller.dart';
|
||||||
|
import 'package:nc_photos/controller/pref_controller.dart';
|
||||||
|
import 'package:nc_photos/di_container.dart';
|
||||||
|
import 'package:nc_photos/entity/collection.dart';
|
||||||
|
import 'package:nc_photos/entity/collection/content_provider/ad_hoc.dart';
|
||||||
|
import 'package:nc_photos/entity/image_location/repo.dart';
|
||||||
|
import 'package:nc_photos/exception_event.dart';
|
||||||
|
import 'package:nc_photos/k.dart' as k;
|
||||||
|
import 'package:nc_photos/snack_bar_manager.dart';
|
||||||
|
import 'package:nc_photos/stream_extension.dart';
|
||||||
|
import 'package:nc_photos/theme.dart';
|
||||||
|
import 'package:nc_photos/theme/dimension.dart';
|
||||||
|
import 'package:nc_photos/widget/collection_browser.dart';
|
||||||
|
import 'package:nc_photos/widget/measure.dart';
|
||||||
|
import 'package:nc_photos/widget/navigation_bar_blur_filter.dart';
|
||||||
|
import 'package:np_codegen/np_codegen.dart';
|
||||||
|
import 'package:np_common/object_util.dart';
|
||||||
|
import 'package:np_datetime/np_datetime.dart';
|
||||||
|
import 'package:np_gps_map/np_gps_map.dart';
|
||||||
|
import 'package:to_string/to_string.dart';
|
||||||
|
|
||||||
|
part 'map_browser.g.dart';
|
||||||
|
part 'map_browser/bloc.dart';
|
||||||
|
part 'map_browser/state_event.dart';
|
||||||
|
part 'map_browser/type.dart';
|
||||||
|
part 'map_browser/view.dart';
|
||||||
|
|
||||||
|
class MapBrowser extends StatelessWidget {
|
||||||
|
const MapBrowser({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocProvider(
|
||||||
|
create: (_) => _Bloc(
|
||||||
|
KiwiContainer().resolve(),
|
||||||
|
account: context.read<AccountController>().account,
|
||||||
|
prefController: context.read(),
|
||||||
|
)..add(const _LoadData()),
|
||||||
|
child: const _WrappedMapBrowser(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _WrappedMapBrowser extends StatelessWidget {
|
||||||
|
const _WrappedMapBrowser();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MultiBlocListener(
|
||||||
|
listeners: [
|
||||||
|
_BlocListenerT<ExceptionEvent?>(
|
||||||
|
selector: (state) => state.error,
|
||||||
|
listener: (context, error) {
|
||||||
|
if (error != null) {
|
||||||
|
SnackBarManager().showSnackBarForException(error.error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
const _MapView(),
|
||||||
|
Positioned.directional(
|
||||||
|
textDirection: Directionality.of(context),
|
||||||
|
top: MediaQuery.of(context).padding.top + 8,
|
||||||
|
end: 8,
|
||||||
|
child: const _DateRangeToggle(),
|
||||||
|
),
|
||||||
|
_BlocSelector<bool>(
|
||||||
|
selector: (state) => state.isShowDataRangeControlPanel,
|
||||||
|
builder: (context, isShowAnyPanel) => Positioned.fill(
|
||||||
|
child: isShowAnyPanel
|
||||||
|
? GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
context.addEvent(const _CloseControlPanel());
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
left: 8,
|
||||||
|
right: 8,
|
||||||
|
top: MediaQuery.of(context).padding.top + 8,
|
||||||
|
child: _BlocSelector<bool>(
|
||||||
|
selector: (state) => state.isShowDataRangeControlPanel,
|
||||||
|
builder: (context, isShowDataRangeControlPanel) =>
|
||||||
|
_PanelContainer(
|
||||||
|
isShow: isShowDataRangeControlPanel,
|
||||||
|
child: const _DateRangeControlPanel(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: NavigationBarBlurFilter(
|
||||||
|
height: AppDimension.of(context).homeBottomAppBarHeight,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef _BlocBuilder = BlocBuilder<_Bloc, _State>;
|
||||||
|
// typedef _BlocListener = BlocListener<_Bloc, _State>;
|
||||||
|
typedef _BlocListenerT<T> = BlocListenerT<_Bloc, _State, T>;
|
||||||
|
typedef _BlocSelector<T> = BlocSelector<_Bloc, _State, T>;
|
||||||
|
|
||||||
|
extension on BuildContext {
|
||||||
|
_Bloc get bloc => read<_Bloc>();
|
||||||
|
// _State get state => bloc.state;
|
||||||
|
void addEvent(_Event event) => bloc.add(event);
|
||||||
|
}
|
128
app/lib/widget/map_browser.g.dart
Normal file
128
app/lib/widget/map_browser.g.dart
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'map_browser.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// CopyWithLintRuleGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// ignore_for_file: library_private_types_in_public_api, duplicate_ignore
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// CopyWithGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
abstract class $_StateCopyWithWorker {
|
||||||
|
_State call(
|
||||||
|
{List<_DataPoint>? data,
|
||||||
|
MapCoord? initialPoint,
|
||||||
|
Set<Marker>? markers,
|
||||||
|
bool? isShowDataRangeControlPanel,
|
||||||
|
_DateRangeType? dateRangeType,
|
||||||
|
DateRange? localDateRange,
|
||||||
|
ExceptionEvent? error});
|
||||||
|
}
|
||||||
|
|
||||||
|
class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker {
|
||||||
|
_$_StateCopyWithWorkerImpl(this.that);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_State call(
|
||||||
|
{dynamic data,
|
||||||
|
dynamic initialPoint = copyWithNull,
|
||||||
|
dynamic markers,
|
||||||
|
dynamic isShowDataRangeControlPanel,
|
||||||
|
dynamic dateRangeType,
|
||||||
|
dynamic localDateRange,
|
||||||
|
dynamic error = copyWithNull}) {
|
||||||
|
return _State(
|
||||||
|
data: data as List<_DataPoint>? ?? that.data,
|
||||||
|
initialPoint: initialPoint == copyWithNull
|
||||||
|
? that.initialPoint
|
||||||
|
: initialPoint as MapCoord?,
|
||||||
|
markers: markers as Set<Marker>? ?? that.markers,
|
||||||
|
isShowDataRangeControlPanel: isShowDataRangeControlPanel as bool? ??
|
||||||
|
that.isShowDataRangeControlPanel,
|
||||||
|
dateRangeType: dateRangeType as _DateRangeType? ?? that.dateRangeType,
|
||||||
|
localDateRange: localDateRange as DateRange? ?? that.localDateRange,
|
||||||
|
error: error == copyWithNull ? that.error : error as ExceptionEvent?);
|
||||||
|
}
|
||||||
|
|
||||||
|
final _State that;
|
||||||
|
}
|
||||||
|
|
||||||
|
extension $_StateCopyWith on _State {
|
||||||
|
$_StateCopyWithWorker get copyWith => _$copyWith;
|
||||||
|
$_StateCopyWithWorker get _$copyWith => _$_StateCopyWithWorkerImpl(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// NpLogGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
extension _$_BlocNpLog on _Bloc {
|
||||||
|
// ignore: unused_element
|
||||||
|
Logger get _log => log;
|
||||||
|
|
||||||
|
static final log = Logger("widget.map_browser._Bloc");
|
||||||
|
}
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// ToStringGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
extension _$_StateToString on _State {
|
||||||
|
String _$toString() {
|
||||||
|
// ignore: unnecessary_string_interpolations
|
||||||
|
return "_State {data: [length: ${data.length}], initialPoint: $initialPoint, markers: {length: ${markers.length}}, isShowDataRangeControlPanel: $isShowDataRangeControlPanel, dateRangeType: ${dateRangeType.name}, localDateRange: $localDateRange, error: $error}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension _$_LoadDataToString on _LoadData {
|
||||||
|
String _$toString() {
|
||||||
|
// ignore: unnecessary_string_interpolations
|
||||||
|
return "_LoadData {}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension _$_SetMarkersToString on _SetMarkers {
|
||||||
|
String _$toString() {
|
||||||
|
// ignore: unnecessary_string_interpolations
|
||||||
|
return "_SetMarkers {markers: {length: ${markers.length}}}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension _$_OpenDataRangeControlPanelToString on _OpenDataRangeControlPanel {
|
||||||
|
String _$toString() {
|
||||||
|
// ignore: unnecessary_string_interpolations
|
||||||
|
return "_OpenDataRangeControlPanel {}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension _$_CloseControlPanelToString on _CloseControlPanel {
|
||||||
|
String _$toString() {
|
||||||
|
// ignore: unnecessary_string_interpolations
|
||||||
|
return "_CloseControlPanel {}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension _$_SetDateRangeTypeToString on _SetDateRangeType {
|
||||||
|
String _$toString() {
|
||||||
|
// ignore: unnecessary_string_interpolations
|
||||||
|
return "_SetDateRangeType {value: ${value.name}}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension _$_SetLocalDateRangeToString on _SetLocalDateRange {
|
||||||
|
String _$toString() {
|
||||||
|
// ignore: unnecessary_string_interpolations
|
||||||
|
return "_SetLocalDateRange {value: $value}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension _$_SetErrorToString on _SetError {
|
||||||
|
String _$toString() {
|
||||||
|
// ignore: unnecessary_string_interpolations
|
||||||
|
return "_SetError {error: $error, stackTrace: $stackTrace}";
|
||||||
|
}
|
||||||
|
}
|
168
app/lib/widget/map_browser/bloc.dart
Normal file
168
app/lib/widget/map_browser/bloc.dart
Normal file
|
@ -0,0 +1,168 @@
|
||||||
|
part of '../map_browser.dart';
|
||||||
|
|
||||||
|
@npLog
|
||||||
|
class _Bloc extends Bloc<_Event, _State>
|
||||||
|
with BlocLogger, BlocForEachMixin<_Event, _State> {
|
||||||
|
_Bloc(
|
||||||
|
this._c, {
|
||||||
|
required this.account,
|
||||||
|
required this.prefController,
|
||||||
|
}) : super(_State.init(
|
||||||
|
dateRangeType: _DateRangeType.thisMonth,
|
||||||
|
localDateRange:
|
||||||
|
_calcDateRange(clock.now().toDate(), _DateRangeType.thisMonth),
|
||||||
|
)) {
|
||||||
|
on<_LoadData>(_onLoadData);
|
||||||
|
on<_SetMarkers>(_onSetMarkers);
|
||||||
|
on<_OpenDataRangeControlPanel>(_onOpenDataRangeControlPanel);
|
||||||
|
on<_CloseControlPanel>(_onCloseControlPanel);
|
||||||
|
on<_SetDateRangeType>(_onSetDateRangeType);
|
||||||
|
on<_SetLocalDateRange>(_onSetDateRange);
|
||||||
|
on<_SetError>(_onSetError);
|
||||||
|
|
||||||
|
_subscriptions
|
||||||
|
.add(stream.distinctBy((state) => state.localDateRange).listen((state) {
|
||||||
|
add(const _LoadData());
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> close() {
|
||||||
|
for (final s in _subscriptions) {
|
||||||
|
s.cancel();
|
||||||
|
}
|
||||||
|
return super.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tag => _log.fullName;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onError(Object error, StackTrace stackTrace) {
|
||||||
|
// we need this to prevent onError being triggered recursively
|
||||||
|
if (!isClosed && !_isHandlingError) {
|
||||||
|
_isHandlingError = true;
|
||||||
|
try {
|
||||||
|
add(_SetError(error, stackTrace));
|
||||||
|
} catch (_) {}
|
||||||
|
_isHandlingError = false;
|
||||||
|
}
|
||||||
|
super.onError(error, stackTrace);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onLoadData(_LoadData ev, Emitter<_State> emit) async {
|
||||||
|
_log.info(ev);
|
||||||
|
// convert local DateRange to TimeRange in UTC
|
||||||
|
final localTimeRange = state.localDateRange.toLocalTimeRange();
|
||||||
|
final utcTimeRange = localTimeRange.copyWith(
|
||||||
|
from: localTimeRange.from?.toUtc(),
|
||||||
|
to: localTimeRange.to?.toUtc(),
|
||||||
|
);
|
||||||
|
final raw = await _c.imageLocationRepo.getLocations(account, utcTimeRange);
|
||||||
|
_log.info("[_onLoadData] Loaded ${raw.length} markers");
|
||||||
|
if (state.initialPoint == null) {
|
||||||
|
final initialPoint =
|
||||||
|
raw.firstOrNull?.let((obj) => MapCoord(obj.latitude, obj.longitude));
|
||||||
|
if (initialPoint != null) {
|
||||||
|
unawaited(prefController.setMapBrowserPrevPosition(initialPoint));
|
||||||
|
}
|
||||||
|
emit(state.copyWith(
|
||||||
|
data: raw.map(_DataPoint.fromImageLatLng).toList(),
|
||||||
|
initialPoint: initialPoint,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
emit(state.copyWith(
|
||||||
|
data: raw.map(_DataPoint.fromImageLatLng).toList(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onSetMarkers(_SetMarkers ev, Emitter<_State> emit) {
|
||||||
|
_log.info(ev);
|
||||||
|
emit(state.copyWith(markers: ev.markers));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onOpenDataRangeControlPanel(
|
||||||
|
_OpenDataRangeControlPanel ev, Emitter<_State> emit) {
|
||||||
|
_log.info(ev);
|
||||||
|
emit(state.copyWith(
|
||||||
|
isShowDataRangeControlPanel: true,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onCloseControlPanel(_CloseControlPanel ev, Emitter<_State> emit) {
|
||||||
|
_log.info(ev);
|
||||||
|
emit(state.copyWith(
|
||||||
|
isShowDataRangeControlPanel: false,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onSetDateRangeType(_SetDateRangeType ev, Emitter<_State> emit) {
|
||||||
|
_log.info(ev);
|
||||||
|
emit(state.copyWith(
|
||||||
|
dateRangeType: ev.value,
|
||||||
|
localDateRange: ev.value == _DateRangeType.custom
|
||||||
|
? null
|
||||||
|
: _calcDateRange(clock.now().toDate(), ev.value),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onSetDateRange(_SetLocalDateRange ev, Emitter<_State> emit) {
|
||||||
|
_log.info(ev);
|
||||||
|
emit(state.copyWith(
|
||||||
|
dateRangeType: _DateRangeType.custom,
|
||||||
|
localDateRange: ev.value,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onSetError(_SetError ev, Emitter<_State> emit) {
|
||||||
|
_log.info(ev);
|
||||||
|
emit(state.copyWith(error: ExceptionEvent(ev.error, ev.stackTrace)));
|
||||||
|
}
|
||||||
|
|
||||||
|
static DateRange _calcDateRange(Date today, _DateRangeType type) {
|
||||||
|
assert(type != _DateRangeType.custom);
|
||||||
|
switch (type) {
|
||||||
|
case _DateRangeType.thisMonth:
|
||||||
|
return DateRange(
|
||||||
|
from: today.copyWith(day: 1),
|
||||||
|
to: today,
|
||||||
|
toBound: TimeRangeBound.inclusive,
|
||||||
|
);
|
||||||
|
case _DateRangeType.prevMonth:
|
||||||
|
if (today.month == 1) {
|
||||||
|
return DateRange(
|
||||||
|
from: Date(today.year - 1, 12, 1),
|
||||||
|
to: Date(today.year - 1, 12, 31),
|
||||||
|
toBound: TimeRangeBound.inclusive,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return DateRange(
|
||||||
|
from: Date(today.year, today.month - 1, 1),
|
||||||
|
to: Date(today.year, today.month, 1).add(day: -1),
|
||||||
|
toBound: TimeRangeBound.inclusive,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case _DateRangeType.thisYear:
|
||||||
|
return DateRange(
|
||||||
|
from: today.copyWith(month: 1, day: 1),
|
||||||
|
to: today,
|
||||||
|
toBound: TimeRangeBound.inclusive,
|
||||||
|
);
|
||||||
|
case _DateRangeType.custom:
|
||||||
|
return DateRange(
|
||||||
|
from: today,
|
||||||
|
to: today,
|
||||||
|
toBound: TimeRangeBound.inclusive,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final DiContainer _c;
|
||||||
|
final Account account;
|
||||||
|
final PrefController prefController;
|
||||||
|
|
||||||
|
final _subscriptions = <StreamSubscription>[];
|
||||||
|
|
||||||
|
var _isHandlingError = false;
|
||||||
|
}
|
110
app/lib/widget/map_browser/state_event.dart
Normal file
110
app/lib/widget/map_browser/state_event.dart
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
part of '../map_browser.dart';
|
||||||
|
|
||||||
|
@genCopyWith
|
||||||
|
@toString
|
||||||
|
class _State {
|
||||||
|
const _State({
|
||||||
|
required this.data,
|
||||||
|
this.initialPoint,
|
||||||
|
required this.markers,
|
||||||
|
required this.isShowDataRangeControlPanel,
|
||||||
|
required this.dateRangeType,
|
||||||
|
required this.localDateRange,
|
||||||
|
this.error,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory _State.init({
|
||||||
|
required _DateRangeType dateRangeType,
|
||||||
|
required DateRange localDateRange,
|
||||||
|
}) {
|
||||||
|
return _State(
|
||||||
|
data: const [],
|
||||||
|
markers: const {},
|
||||||
|
isShowDataRangeControlPanel: false,
|
||||||
|
dateRangeType: dateRangeType,
|
||||||
|
localDateRange: localDateRange,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => _$toString();
|
||||||
|
|
||||||
|
final List<_DataPoint> data;
|
||||||
|
final MapCoord? initialPoint;
|
||||||
|
final Set<Marker> markers;
|
||||||
|
|
||||||
|
final bool isShowDataRangeControlPanel;
|
||||||
|
final _DateRangeType dateRangeType;
|
||||||
|
final DateRange localDateRange;
|
||||||
|
|
||||||
|
final ExceptionEvent? error;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _Event {
|
||||||
|
const _Event();
|
||||||
|
}
|
||||||
|
|
||||||
|
@toString
|
||||||
|
class _LoadData implements _Event {
|
||||||
|
const _LoadData();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => _$toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@toString
|
||||||
|
class _SetMarkers implements _Event {
|
||||||
|
const _SetMarkers(this.markers);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => _$toString();
|
||||||
|
|
||||||
|
final Set<Marker> markers;
|
||||||
|
}
|
||||||
|
|
||||||
|
@toString
|
||||||
|
class _OpenDataRangeControlPanel implements _Event {
|
||||||
|
const _OpenDataRangeControlPanel();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => _$toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@toString
|
||||||
|
class _CloseControlPanel implements _Event {
|
||||||
|
const _CloseControlPanel();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => _$toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@toString
|
||||||
|
class _SetDateRangeType implements _Event {
|
||||||
|
const _SetDateRangeType(this.value);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => _$toString();
|
||||||
|
|
||||||
|
final _DateRangeType value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@toString
|
||||||
|
class _SetLocalDateRange implements _Event {
|
||||||
|
const _SetLocalDateRange(this.value);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => _$toString();
|
||||||
|
|
||||||
|
final DateRange value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@toString
|
||||||
|
class _SetError implements _Event {
|
||||||
|
const _SetError(this.error, [this.stackTrace]);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => _$toString();
|
||||||
|
|
||||||
|
final Object error;
|
||||||
|
final StackTrace? stackTrace;
|
||||||
|
}
|
46
app/lib/widget/map_browser/type.dart
Normal file
46
app/lib/widget/map_browser/type.dart
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
part of '../map_browser.dart';
|
||||||
|
|
||||||
|
class _DataPoint implements ClusterItem {
|
||||||
|
const _DataPoint({
|
||||||
|
required this.location,
|
||||||
|
required this.fileId,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory _DataPoint.fromImageLatLng(ImageLatLng src) => _DataPoint(
|
||||||
|
location: LatLng(src.latitude, src.longitude),
|
||||||
|
fileId: src.fileId,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get geohash =>
|
||||||
|
Geohash.encode(location, codeLength: ClusterManager.precision);
|
||||||
|
|
||||||
|
@override
|
||||||
|
final LatLng location;
|
||||||
|
final int fileId;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum _DateRangeType {
|
||||||
|
thisMonth,
|
||||||
|
prevMonth,
|
||||||
|
thisYear,
|
||||||
|
custom,
|
||||||
|
;
|
||||||
|
|
||||||
|
String toDisplayString() {
|
||||||
|
switch (this) {
|
||||||
|
case thisMonth:
|
||||||
|
return L10n.global().mapBrowserDateRangeThisMonth;
|
||||||
|
case prevMonth:
|
||||||
|
return L10n.global().mapBrowserDateRangePrevMonth;
|
||||||
|
case thisYear:
|
||||||
|
return L10n.global().mapBrowserDateRangeThisYear;
|
||||||
|
case custom:
|
||||||
|
return L10n.global().mapBrowserDateRangeCustom;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension on MapCoord {
|
||||||
|
LatLng toLatLng() => LatLng(latitude, longitude);
|
||||||
|
}
|
464
app/lib/widget/map_browser/view.dart
Normal file
464
app/lib/widget/map_browser/view.dart
Normal file
|
@ -0,0 +1,464 @@
|
||||||
|
part of '../map_browser.dart';
|
||||||
|
|
||||||
|
class _MapView extends StatefulWidget {
|
||||||
|
const _MapView();
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => _MapViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MapViewState extends State<_MapView> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MultiBlocListener(
|
||||||
|
listeners: [
|
||||||
|
_BlocListenerT<List<_DataPoint>>(
|
||||||
|
selector: (state) => state.data,
|
||||||
|
listener: (context, data) {
|
||||||
|
_clusterManager.setItems(data);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_BlocListenerT<MapCoord?>(
|
||||||
|
selector: (state) => state.initialPoint,
|
||||||
|
listener: (context, initialPoint) {
|
||||||
|
if (initialPoint != null) {
|
||||||
|
_mapController?.animateCamera(
|
||||||
|
CameraUpdate.newLatLngZoom(initialPoint.toLatLng(), 10));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: _BlocBuilder(
|
||||||
|
buildWhen: (previous, current) => previous.markers != current.markers,
|
||||||
|
builder: (context, state) => GoogleMap(
|
||||||
|
mapType: MapType.normal,
|
||||||
|
initialCameraPosition: context
|
||||||
|
.read<PrefController>()
|
||||||
|
.mapBrowserPrevPositionValue
|
||||||
|
?.let(
|
||||||
|
(p) => CameraPosition(target: p.toLatLng(), zoom: 10)) ??
|
||||||
|
const CameraPosition(target: LatLng(0, 0)),
|
||||||
|
markers: state.markers,
|
||||||
|
onMapCreated: (controller) {
|
||||||
|
_clusterManager.setMapId(controller.mapId);
|
||||||
|
_mapController = controller;
|
||||||
|
if (Theme.of(context).brightness == Brightness.dark) {
|
||||||
|
controller.setMapStyle(_mapStyleNight);
|
||||||
|
}
|
||||||
|
if (state.initialPoint != null) {
|
||||||
|
controller.animateCamera(CameraUpdate.newLatLngZoom(
|
||||||
|
state.initialPoint!.toLatLng(), 10));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onCameraMove: _clusterManager.onCameraMove,
|
||||||
|
onCameraIdle: _clusterManager.updateMap,
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
top: MediaQuery.of(context).padding.top,
|
||||||
|
bottom: MediaQuery.of(context).padding.bottom,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<BitmapDescriptor> _getClusterBitmap(
|
||||||
|
int size, {
|
||||||
|
String? text,
|
||||||
|
required Color color,
|
||||||
|
}) async {
|
||||||
|
final PictureRecorder pictureRecorder = PictureRecorder();
|
||||||
|
final Canvas canvas = Canvas(pictureRecorder);
|
||||||
|
final fillPaint = Paint()..color = color;
|
||||||
|
final outlinePaint = Paint()
|
||||||
|
..color = Theme.of(context).brightness == Brightness.light
|
||||||
|
? Colors.black.withOpacity(.28)
|
||||||
|
: Colors.white.withOpacity(.6)
|
||||||
|
..strokeWidth = size / 28
|
||||||
|
..style = PaintingStyle.stroke;
|
||||||
|
|
||||||
|
const shadowPadding = 6.0;
|
||||||
|
const shadowPaddingHalf = shadowPadding / 2;
|
||||||
|
final shadowPath = Path()
|
||||||
|
..addOval(
|
||||||
|
Rect.fromLTWH(0, 0, size - shadowPadding, size - shadowPadding));
|
||||||
|
canvas.drawShadow(shadowPath, Colors.black, 1, false);
|
||||||
|
canvas.drawCircle(
|
||||||
|
Offset(size / 2 - shadowPaddingHalf, size / 2 - shadowPaddingHalf),
|
||||||
|
size / 2 - shadowPaddingHalf,
|
||||||
|
fillPaint,
|
||||||
|
);
|
||||||
|
canvas.drawCircle(
|
||||||
|
Offset(size / 2 - shadowPaddingHalf, size / 2 - shadowPaddingHalf),
|
||||||
|
size / 2 - shadowPaddingHalf - (size / 28 / 2),
|
||||||
|
outlinePaint,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (text != null) {
|
||||||
|
TextPainter painter = TextPainter(textDirection: TextDirection.ltr);
|
||||||
|
painter.text = TextSpan(
|
||||||
|
text: text,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: size / 3 - ((text.length / 6) * (size * 0.1)),
|
||||||
|
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||||
|
fontWeight: FontWeight.normal,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
painter.layout();
|
||||||
|
painter.paint(
|
||||||
|
canvas,
|
||||||
|
Offset(
|
||||||
|
size / 2 - painter.width / 2 - shadowPaddingHalf,
|
||||||
|
size / 2 - painter.height / 2 - shadowPaddingHalf,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final img = await pictureRecorder.endRecording().toImage(size, size);
|
||||||
|
final data = await img.toByteData(format: ImageByteFormat.png) as ByteData;
|
||||||
|
|
||||||
|
return BitmapDescriptor.fromBytes(data.buffer.asUint8List());
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getMarkerCountString(int count) {
|
||||||
|
switch (count) {
|
||||||
|
case >= 10000:
|
||||||
|
return "10000+";
|
||||||
|
case >= 1000:
|
||||||
|
return "${count ~/ 1000 * 1000}+";
|
||||||
|
case >= 100:
|
||||||
|
return "${count ~/ 100 * 100}+";
|
||||||
|
case >= 10:
|
||||||
|
return "${count ~/ 10 * 10}+";
|
||||||
|
default:
|
||||||
|
return count.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Color _getMarkerColor(int count) {
|
||||||
|
const step = 1 / 4;
|
||||||
|
final double r;
|
||||||
|
switch (count) {
|
||||||
|
case >= 10000:
|
||||||
|
r = 1;
|
||||||
|
case >= 1000:
|
||||||
|
r = (count ~/ 1000) / 10 * step + step * 3;
|
||||||
|
case >= 100:
|
||||||
|
r = (count ~/ 100) / 10 * step + step * 2;
|
||||||
|
case >= 10:
|
||||||
|
r = (count ~/ 10) / 10 * step + step;
|
||||||
|
default:
|
||||||
|
r = (count / 10) * step;
|
||||||
|
}
|
||||||
|
if (Theme.of(context).brightness == Brightness.light) {
|
||||||
|
return HSLColor.fromAHSL(
|
||||||
|
1,
|
||||||
|
_colorHsl.hue,
|
||||||
|
r * .7 + .3,
|
||||||
|
(_colorHsl.lightness - (.1 - r * .1)).clamp(0, 1),
|
||||||
|
).toColor();
|
||||||
|
} else {
|
||||||
|
return HSLColor.fromAHSL(
|
||||||
|
1,
|
||||||
|
_colorHsl.hue,
|
||||||
|
r * .6 + .4,
|
||||||
|
(_colorHsl.lightness - (.1 - r * .1)).clamp(0, 1),
|
||||||
|
).toColor();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int _getMarkerSize(int count) {
|
||||||
|
const step = 1 / 4;
|
||||||
|
final double r;
|
||||||
|
switch (count) {
|
||||||
|
case >= 10000:
|
||||||
|
r = 1;
|
||||||
|
case >= 1000:
|
||||||
|
r = (count ~/ 1000) / 10 * step + step * 3;
|
||||||
|
case >= 100:
|
||||||
|
r = (count ~/ 100) / 10 * step + step * 2;
|
||||||
|
case >= 10:
|
||||||
|
r = (count ~/ 10) / 10 * step + step;
|
||||||
|
default:
|
||||||
|
r = (count / 10) * step;
|
||||||
|
}
|
||||||
|
return (r * 85).toInt() + 85;
|
||||||
|
}
|
||||||
|
|
||||||
|
late final _clusterManager = ClusterManager<_DataPoint>(
|
||||||
|
const [],
|
||||||
|
(markers) {
|
||||||
|
if (mounted) {
|
||||||
|
context.addEvent(_SetMarkers(markers));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
markerBuilder: (cluster) async => Marker(
|
||||||
|
markerId: MarkerId(cluster.getId()),
|
||||||
|
position: cluster.location,
|
||||||
|
onTap: () {
|
||||||
|
final c = Collection(
|
||||||
|
name: "",
|
||||||
|
contentProvider: CollectionAdHocProvider(
|
||||||
|
account: context.bloc.account,
|
||||||
|
fileIds: cluster.items.map((e) => e.fileId).toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
Navigator.of(context).pushNamed(
|
||||||
|
CollectionBrowser.routeName,
|
||||||
|
arguments: CollectionBrowserArguments(c),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
icon: await _getClusterBitmap(
|
||||||
|
_getMarkerSize(cluster.count * 1),
|
||||||
|
text: _getMarkerCountString(cluster.count * 1),
|
||||||
|
color: _getMarkerColor(cluster.count * 1),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
GoogleMapController? _mapController;
|
||||||
|
|
||||||
|
late final _colorHsl =
|
||||||
|
HSLColor.fromColor(Theme.of(context).colorScheme.primaryContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PanelContainer extends StatefulWidget {
|
||||||
|
const _PanelContainer({
|
||||||
|
required this.isShow,
|
||||||
|
required this.child,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => _PanelContainerState();
|
||||||
|
|
||||||
|
final bool isShow;
|
||||||
|
final Widget child;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PanelContainerState extends State<_PanelContainer>
|
||||||
|
with TickerProviderStateMixin {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_animationController = AnimationController(
|
||||||
|
duration: k.animationDurationNormal,
|
||||||
|
vsync: this,
|
||||||
|
value: 0,
|
||||||
|
);
|
||||||
|
_animation = CurvedAnimation(
|
||||||
|
parent: _animationController,
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_animationController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant _PanelContainer oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (oldWidget.isShow != widget.isShow) {
|
||||||
|
if (widget.isShow) {
|
||||||
|
_animationController.animateTo(1);
|
||||||
|
} else {
|
||||||
|
_animationController.animateBack(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MatrixTransition(
|
||||||
|
animation: _animation,
|
||||||
|
onTransform: (animationValue) => Matrix4.identity()
|
||||||
|
..translate(0.0, -(_size.height / 2) * (1 - animationValue), 0.0)
|
||||||
|
..scale(1.0, animationValue, 1.0),
|
||||||
|
child: MeasureSize(
|
||||||
|
onChange: (size) => setState(() {
|
||||||
|
_size = size;
|
||||||
|
}),
|
||||||
|
child: widget.child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
late AnimationController _animationController;
|
||||||
|
late Animation<double> _animation;
|
||||||
|
var _size = Size.zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DateRangeToggle extends StatelessWidget {
|
||||||
|
const _DateRangeToggle();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return FloatingActionButton.small(
|
||||||
|
onPressed: () {
|
||||||
|
context.addEvent(const _OpenDataRangeControlPanel());
|
||||||
|
},
|
||||||
|
child: const Icon(Icons.date_range_outlined),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DateRangeControlPanel extends StatelessWidget {
|
||||||
|
const _DateRangeControlPanel();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.elevate(theme.colorScheme.surface, 2),
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
boxShadow: const [
|
||||||
|
BoxShadow(
|
||||||
|
blurRadius: 4,
|
||||||
|
offset: Offset(0, 2),
|
||||||
|
color: Colors.black26,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
L10n.global().mapBrowserDateRangeLabel,
|
||||||
|
style: Theme.of(context).listTileTheme.titleTextStyle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: _BlocSelector<_DateRangeType>(
|
||||||
|
selector: (state) => state.dateRangeType,
|
||||||
|
builder: (context, dateRangeType) =>
|
||||||
|
DropdownButtonFormField<_DateRangeType>(
|
||||||
|
items: _DateRangeType.values
|
||||||
|
.map((e) => DropdownMenuItem(
|
||||||
|
value: e,
|
||||||
|
child: Text(e.toDisplayString()),
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
value: dateRangeType,
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value != null) {
|
||||||
|
context.addEvent(_SetDateRangeType(value));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: _BlocSelector<DateRange>(
|
||||||
|
selector: (state) => state.localDateRange,
|
||||||
|
builder: (context, localDateRange) => _DateField(
|
||||||
|
localDateRange.from!,
|
||||||
|
onChanged: (value) {
|
||||||
|
context.addEvent(_SetLocalDateRange(
|
||||||
|
localDateRange.copyWith(from: value.toDate())));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Text(" - "),
|
||||||
|
Expanded(
|
||||||
|
child: _BlocSelector<DateRange>(
|
||||||
|
selector: (state) => state.localDateRange,
|
||||||
|
builder: (context, localDateRange) => _DateField(
|
||||||
|
localDateRange.to!,
|
||||||
|
onChanged: (value) {
|
||||||
|
context.addEvent(_SetLocalDateRange(
|
||||||
|
localDateRange.copyWith(to: value.toDate())));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DateField extends StatefulWidget {
|
||||||
|
const _DateField(
|
||||||
|
this.date, {
|
||||||
|
this.onChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => _DateFieldState();
|
||||||
|
|
||||||
|
final Date date;
|
||||||
|
final ValueChanged<DateTime>? onChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DateFieldState extends State<_DateField> {
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
|
_controller.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant _DateField oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (widget.date != oldWidget.date) {
|
||||||
|
_controller.text = _stringify(widget.date);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Material(
|
||||||
|
type: MaterialType.transparency,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () async {
|
||||||
|
final result = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
firstDate: DateTime(1970),
|
||||||
|
lastDate: clock.now(),
|
||||||
|
currentDate: widget.date.toLocalDateTime(),
|
||||||
|
);
|
||||||
|
if (result == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
widget.onChanged?.call(result);
|
||||||
|
},
|
||||||
|
child: IgnorePointer(
|
||||||
|
child: ExcludeFocus(
|
||||||
|
child: TextFormField(
|
||||||
|
controller: _controller,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _stringify(Date date) {
|
||||||
|
return intl.DateFormat(intl.DateFormat.YEAR_ABBR_MONTH_DAY,
|
||||||
|
Localizations.localeOf(context).languageCode)
|
||||||
|
.format(date.toLocalDateTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
late final _controller = TextEditingController(text: _stringify(widget.date));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generated in https://mapstyle.withgoogle.com/
|
||||||
|
const _mapStyleNight =
|
||||||
|
'[{"elementType":"geometry","stylers":[{"color":"#242f3e"}]},{"elementType":"labels.text.fill","stylers":[{"color":"#746855"}]},{"elementType":"labels.text.stroke","stylers":[{"color":"#242f3e"}]},{"featureType":"administrative.locality","elementType":"labels.text.fill","stylers":[{"color":"#d59563"}]},{"featureType":"poi","elementType":"labels.text.fill","stylers":[{"color":"#d59563"}]},{"featureType":"poi.park","elementType":"geometry","stylers":[{"color":"#263c3f"}]},{"featureType":"poi.park","elementType":"labels.text.fill","stylers":[{"color":"#6b9a76"}]},{"featureType":"road","elementType":"geometry","stylers":[{"color":"#38414e"}]},{"featureType":"road","elementType":"geometry.stroke","stylers":[{"color":"#212a37"}]},{"featureType":"road","elementType":"labels.text.fill","stylers":[{"color":"#9ca5b3"}]},{"featureType":"road.highway","elementType":"geometry","stylers":[{"color":"#746855"}]},{"featureType":"road.highway","elementType":"geometry.stroke","stylers":[{"color":"#1f2835"}]},{"featureType":"road.highway","elementType":"labels.text.fill","stylers":[{"color":"#f3d19c"}]},{"featureType":"transit","elementType":"geometry","stylers":[{"color":"#2f3948"}]},{"featureType":"transit.station","elementType":"labels.text.fill","stylers":[{"color":"#d59563"}]},{"featureType":"water","elementType":"geometry","stylers":[{"color":"#17263c"}]},{"featureType":"water","elementType":"labels.text.fill","stylers":[{"color":"#515c6d"}]},{"featureType":"water","elementType":"labels.text.stroke","stylers":[{"color":"#17263c"}]}]';
|
|
@ -652,38 +652,62 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.2"
|
version: "2.1.2"
|
||||||
google_maps_flutter:
|
google_maps:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: google_maps
|
||||||
|
sha256: "555d5d736339b0478e821167ac521c810d7b51c3b2734e6802a9f046b64ea37a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.3.0"
|
||||||
|
google_maps_cluster_manager:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: google_maps_cluster_manager
|
||||||
|
sha256: "36e9a4b2d831c470fc85d692a6c9cec70e0f385d578b9697de5f4de347561b83"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.0"
|
||||||
|
google_maps_flutter:
|
||||||
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: google_maps_flutter
|
name: google_maps_flutter
|
||||||
sha256: abefcb1e5e5c96bdd8084939dda555257af272c7972902ca46d5631092c1df68
|
sha256: ae66fef3e71261d7df2eff29b2a119e190b2884325ecaa55321b1e17b5504066
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.8"
|
version: "2.5.3"
|
||||||
google_maps_flutter_android:
|
google_maps_flutter_android:
|
||||||
dependency: transitive
|
dependency: "direct overridden"
|
||||||
description:
|
description:
|
||||||
name: google_maps_flutter_android
|
name: google_maps_flutter_android
|
||||||
sha256: "9512c862df77c1f0fa5f445513dd3c57f5996f0a809dccb74e54b690ee4e3a0f"
|
sha256: "256b3c974e415bd17555ceff76a5d0badd2cbfd29febfc23070993358f639550"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.15"
|
version: "2.7.0"
|
||||||
google_maps_flutter_ios:
|
google_maps_flutter_ios:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: google_maps_flutter_ios
|
name: google_maps_flutter_ios
|
||||||
sha256: a9462a433bf3ebe60aadcf4906d2d6341a270d69d3e0fcaa8eb2b64699fcfb4f
|
sha256: "244b3abc7cb611c4a5a2c5ce5a5f36a0d11bf5ccc3f05535d9baf61115ba9a5a"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.3"
|
version: "2.6.1"
|
||||||
google_maps_flutter_platform_interface:
|
google_maps_flutter_platform_interface:
|
||||||
dependency: transitive
|
dependency: "direct overridden"
|
||||||
description:
|
description:
|
||||||
name: google_maps_flutter_platform_interface
|
name: google_maps_flutter_platform_interface
|
||||||
sha256: "308f0af138fa78e8224d598d46ca182673874d0ef4d754b7157c073b5b4b8e0d"
|
sha256: c14381cfbe65b27cc129a68a79c045083b0e19afac0c23b3ffb0e44d7c7e0944
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.7"
|
version: "2.5.0"
|
||||||
|
google_maps_flutter_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: google_maps_flutter_web
|
||||||
|
sha256: "6245721c160d6f531c1ef568cf9bef8d660cd585a982aa75121269030163785a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.5.4+3"
|
||||||
graphs:
|
graphs:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -765,6 +789,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.5"
|
version: "0.6.5"
|
||||||
|
js_wrapping:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: js_wrapping
|
||||||
|
sha256: e385980f7c76a8c1c9a560dfb623b890975841542471eade630b2871d243851c
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.7.4"
|
||||||
json_annotation:
|
json_annotation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -1341,6 +1373,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.27.7"
|
version: "0.27.7"
|
||||||
|
sanitize_html:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sanitize_html
|
||||||
|
sha256: "12669c4a913688a26555323fb9cec373d8f9fbe091f2d01c40c723b33caa8989"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.0"
|
||||||
screen_brightness:
|
screen_brightness:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
|
@ -158,6 +158,9 @@ dependencies:
|
||||||
wakelock_plus: ^1.1.1
|
wakelock_plus: ^1.1.1
|
||||||
woozy_search: ^2.0.3
|
woozy_search: ^2.0.3
|
||||||
|
|
||||||
|
google_maps_flutter: 2.5.3
|
||||||
|
google_maps_cluster_manager: 3.1.0
|
||||||
|
|
||||||
dependency_overrides:
|
dependency_overrides:
|
||||||
video_player:
|
video_player:
|
||||||
git:
|
git:
|
||||||
|
@ -174,6 +177,8 @@ dependency_overrides:
|
||||||
url: https://gitlab.com/nc-photos/flutter-plugins
|
url: https://gitlab.com/nc-photos/flutter-plugins
|
||||||
ref: video_player-v2.8.6-nc-photos-2
|
ref: video_player-v2.8.6-nc-photos-2
|
||||||
path: packages/video_player/video_player_platform_interface
|
path: packages/video_player/video_player_platform_interface
|
||||||
|
google_maps_flutter_android: 2.7.0
|
||||||
|
google_maps_flutter_platform_interface: 2.5.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
test: ^1.22.1
|
test: ^1.22.1
|
||||||
|
|
|
@ -9,6 +9,22 @@ class DateRange {
|
||||||
this.toBound = TimeRangeBound.exclusive,
|
this.toBound = TimeRangeBound.exclusive,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Return a copy of the current instance with some changed fields. Setting
|
||||||
|
/// null is not supported
|
||||||
|
DateRange copyWith({
|
||||||
|
Date? from,
|
||||||
|
TimeRangeBound? fromBound,
|
||||||
|
Date? to,
|
||||||
|
TimeRangeBound? toBound,
|
||||||
|
}) {
|
||||||
|
return DateRange(
|
||||||
|
from: from ?? this.from,
|
||||||
|
fromBound: fromBound ?? this.fromBound,
|
||||||
|
to: to ?? this.to,
|
||||||
|
toBound: toBound ?? this.toBound,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return "${fromBound == TimeRangeBound.inclusive ? "[" : "("}"
|
return "${fromBound == TimeRangeBound.inclusive ? "[" : "("}"
|
||||||
|
|
|
@ -11,6 +11,22 @@ class TimeRange {
|
||||||
this.toBound = TimeRangeBound.exclusive,
|
this.toBound = TimeRangeBound.exclusive,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Return a copy of the current instance with some changed fields. Setting
|
||||||
|
/// null is not supported
|
||||||
|
TimeRange copyWith({
|
||||||
|
DateTime? from,
|
||||||
|
TimeRangeBound? fromBound,
|
||||||
|
DateTime? to,
|
||||||
|
TimeRangeBound? toBound,
|
||||||
|
}) {
|
||||||
|
return TimeRange(
|
||||||
|
from: from ?? this.from,
|
||||||
|
fromBound: fromBound ?? this.fromBound,
|
||||||
|
to: to ?? this.to,
|
||||||
|
toBound: toBound ?? this.toBound,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return "${fromBound == TimeRangeBound.inclusive ? "[" : "("}"
|
return "${fromBound == TimeRangeBound.inclusive ? "[" : "("}"
|
||||||
|
|
|
@ -131,6 +131,29 @@ class DbLocationGroupResult {
|
||||||
final List<DbLocationGroup> countryCode;
|
final List<DbLocationGroup> countryCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@toString
|
||||||
|
class DbImageLatLng with EquatableMixin {
|
||||||
|
const DbImageLatLng({
|
||||||
|
required this.lat,
|
||||||
|
required this.lng,
|
||||||
|
required this.fileId,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => _$toString();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [
|
||||||
|
lat,
|
||||||
|
lng,
|
||||||
|
fileId,
|
||||||
|
];
|
||||||
|
|
||||||
|
final double lat;
|
||||||
|
final double lng;
|
||||||
|
final int fileId;
|
||||||
|
}
|
||||||
|
|
||||||
@genCopyWith
|
@genCopyWith
|
||||||
@toString
|
@toString
|
||||||
class DbFilesSummaryItem with EquatableMixin {
|
class DbFilesSummaryItem with EquatableMixin {
|
||||||
|
@ -409,6 +432,15 @@ abstract class NpDb {
|
||||||
List<String>? excludeRelativeRoots,
|
List<String>? excludeRelativeRoots,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Return the latitude, longitude and the file id of all files
|
||||||
|
Future<List<DbImageLatLng>> getImageLatLngWithFileIds({
|
||||||
|
required DbAccount account,
|
||||||
|
TimeRange? timeRange,
|
||||||
|
List<String>? includeRelativeRoots,
|
||||||
|
List<String>? excludeRelativeRoots,
|
||||||
|
List<String>? mimes,
|
||||||
|
});
|
||||||
|
|
||||||
Future<List<DbNcAlbum>> getNcAlbums({
|
Future<List<DbNcAlbum>> getNcAlbums({
|
||||||
required DbAccount account,
|
required DbAccount account,
|
||||||
});
|
});
|
||||||
|
|
|
@ -124,6 +124,13 @@ extension _$DbLocationGroupResultToString on DbLocationGroupResult {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension _$DbImageLatLngToString on DbImageLatLng {
|
||||||
|
String _$toString() {
|
||||||
|
// ignore: unnecessary_string_interpolations
|
||||||
|
return "DbImageLatLng {lat: ${lat.toStringAsFixed(3)}, lng: ${lng.toStringAsFixed(3)}, fileId: $fileId}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension _$DbFilesSummaryItemToString on DbFilesSummaryItem {
|
extension _$DbFilesSummaryItemToString on DbFilesSummaryItem {
|
||||||
String _$toString() {
|
String _$toString() {
|
||||||
// ignore: unnecessary_string_interpolations
|
// ignore: unnecessary_string_interpolations
|
||||||
|
|
|
@ -16,7 +16,82 @@ class ImageLocationGroup {
|
||||||
final DateTime latestDateTime;
|
final DateTime latestDateTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ImageLatLng {
|
||||||
|
const ImageLatLng({
|
||||||
|
required this.lat,
|
||||||
|
required this.lng,
|
||||||
|
required this.fileId,
|
||||||
|
});
|
||||||
|
|
||||||
|
final double lat;
|
||||||
|
final double lng;
|
||||||
|
final int fileId;
|
||||||
|
}
|
||||||
|
|
||||||
extension SqliteDbImageLocationExtension on SqliteDb {
|
extension SqliteDbImageLocationExtension on SqliteDb {
|
||||||
|
Future<List<ImageLatLng>> queryImageLatLngWithFileIds({
|
||||||
|
required ByAccount account,
|
||||||
|
TimeRange? timeRange,
|
||||||
|
List<String>? includeRelativeRoots,
|
||||||
|
List<String>? includeRelativeDirs,
|
||||||
|
List<String>? excludeRelativeRoots,
|
||||||
|
List<String>? mimes,
|
||||||
|
}) async {
|
||||||
|
_log.info("[queryImageLatLngWithFileIds] timeRange: $timeRange");
|
||||||
|
final query = _queryFiles().let((q) {
|
||||||
|
q
|
||||||
|
..setQueryMode(
|
||||||
|
FilesQueryMode.expression,
|
||||||
|
expressions: [files.fileId],
|
||||||
|
)
|
||||||
|
..setExtraJoins([
|
||||||
|
innerJoin(
|
||||||
|
imageLocations,
|
||||||
|
imageLocations.accountFile.equalsExp(accountFiles.rowId),
|
||||||
|
),
|
||||||
|
])
|
||||||
|
..setAccount(account);
|
||||||
|
if (includeRelativeRoots != null) {
|
||||||
|
if (includeRelativeRoots.none((p) => p.isEmpty)) {
|
||||||
|
for (final r in includeRelativeRoots) {
|
||||||
|
q.byOrRelativePathPattern("$r/%");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return q.build();
|
||||||
|
});
|
||||||
|
query.addColumns([
|
||||||
|
imageLocations.latitude,
|
||||||
|
imageLocations.longitude,
|
||||||
|
]);
|
||||||
|
if (excludeRelativeRoots != null) {
|
||||||
|
for (final r in excludeRelativeRoots) {
|
||||||
|
query.where(accountFiles.relativePath.like("$r/%").not());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mimes != null) {
|
||||||
|
query.where(files.contentType.isIn(mimes));
|
||||||
|
} else {
|
||||||
|
query.where(files.isCollection.isNotValue(true));
|
||||||
|
}
|
||||||
|
if (timeRange != null) {
|
||||||
|
accountFiles.bestDateTime
|
||||||
|
.isBetweenTimeRange(timeRange)
|
||||||
|
?.let((e) => query.where(e));
|
||||||
|
}
|
||||||
|
query
|
||||||
|
..where(imageLocations.latitude.isNotNull() &
|
||||||
|
imageLocations.longitude.isNotNull())
|
||||||
|
..orderBy([OrderingTerm.desc(accountFiles.bestDateTime)]);
|
||||||
|
return query
|
||||||
|
.map((r) => ImageLatLng(
|
||||||
|
lat: r.read(imageLocations.latitude)!,
|
||||||
|
lng: r.read(imageLocations.longitude)!,
|
||||||
|
fileId: r.read(files.fileId)!,
|
||||||
|
))
|
||||||
|
.get();
|
||||||
|
}
|
||||||
|
|
||||||
Future<List<ImageLocationGroup>> groupImageLocationsByName({
|
Future<List<ImageLocationGroup>> groupImageLocationsByName({
|
||||||
required ByAccount account,
|
required ByAccount account,
|
||||||
List<String>? includeRelativeRoots,
|
List<String>? includeRelativeRoots,
|
||||||
|
|
|
@ -36,6 +36,10 @@ class FilesQueryBuilder {
|
||||||
_selectExpressions = expressions;
|
_selectExpressions = expressions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setExtraJoins(List<Join> joins) {
|
||||||
|
_extraJoins = joins.toList();
|
||||||
|
}
|
||||||
|
|
||||||
void setAccount(ByAccount account) {
|
void setAccount(ByAccount account) {
|
||||||
if (account.sqlAccount != null) {
|
if (account.sqlAccount != null) {
|
||||||
assert(_dbAccount == null);
|
assert(_dbAccount == null);
|
||||||
|
@ -131,6 +135,7 @@ class FilesQueryBuilder {
|
||||||
if (_queryMode == FilesQueryMode.completeFile || _byLocation != null)
|
if (_queryMode == FilesQueryMode.completeFile || _byLocation != null)
|
||||||
leftOuterJoin(db.imageLocations,
|
leftOuterJoin(db.imageLocations,
|
||||||
db.imageLocations.accountFile.equalsExp(db.accountFiles.rowId)),
|
db.imageLocations.accountFile.equalsExp(db.accountFiles.rowId)),
|
||||||
|
if (_extraJoins != null) ..._extraJoins!,
|
||||||
]) as JoinedSelectStatement;
|
]) as JoinedSelectStatement;
|
||||||
if (_queryMode == FilesQueryMode.expression) {
|
if (_queryMode == FilesQueryMode.expression) {
|
||||||
query.addColumns(_selectExpressions!);
|
query.addColumns(_selectExpressions!);
|
||||||
|
@ -232,6 +237,7 @@ class FilesQueryBuilder {
|
||||||
|
|
||||||
FilesQueryMode _queryMode = FilesQueryMode.file;
|
FilesQueryMode _queryMode = FilesQueryMode.file;
|
||||||
Iterable<Expression>? _selectExpressions;
|
Iterable<Expression>? _selectExpressions;
|
||||||
|
List<Join>? _extraJoins;
|
||||||
|
|
||||||
Account? _sqlAccount;
|
Account? _sqlAccount;
|
||||||
DbAccount? _dbAccount;
|
DbAccount? _dbAccount;
|
||||||
|
|
|
@ -3,6 +3,7 @@ import 'dart:io' as io;
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:np_async/np_async.dart';
|
||||||
import 'package:np_codegen/np_codegen.dart';
|
import 'package:np_codegen/np_codegen.dart';
|
||||||
import 'package:np_collection/np_collection.dart';
|
import 'package:np_collection/np_collection.dart';
|
||||||
import 'package:np_common/object_util.dart';
|
import 'package:np_common/object_util.dart';
|
||||||
|
@ -574,6 +575,30 @@ class NpDbSqlite implements NpDb {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<DbImageLatLng>> getImageLatLngWithFileIds({
|
||||||
|
required DbAccount account,
|
||||||
|
TimeRange? timeRange,
|
||||||
|
List<String>? includeRelativeRoots,
|
||||||
|
List<String>? excludeRelativeRoots,
|
||||||
|
List<String>? mimes,
|
||||||
|
}) async {
|
||||||
|
final sqlObjs = await _db.use((db) async {
|
||||||
|
return await db.queryImageLatLngWithFileIds(
|
||||||
|
account: ByAccount.db(account),
|
||||||
|
timeRange: timeRange,
|
||||||
|
includeRelativeRoots: includeRelativeRoots,
|
||||||
|
excludeRelativeRoots: excludeRelativeRoots,
|
||||||
|
mimes: mimes,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return sqlObjs.computeAll((e) => DbImageLatLng(
|
||||||
|
lat: e.lat,
|
||||||
|
lng: e.lng,
|
||||||
|
fileId: e.fileId,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<DbNcAlbum>> getNcAlbums({
|
Future<List<DbNcAlbum>> getNcAlbums({
|
||||||
required DbAccount account,
|
required DbAccount account,
|
||||||
|
|
Loading…
Reference in a new issue