mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-02-24 18:38:48 +01:00
Merge branch 'reverse-geocoding' into dev
This commit is contained in:
commit
51e3431cdc
41 changed files with 2962 additions and 71 deletions
BIN
app/assets/cities.sqlite
Normal file
BIN
app/assets/cities.sqlite
Normal file
Binary file not shown.
|
@ -56,6 +56,26 @@ String getFilePreviewUrlRelative(
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return the preview image URL for [fileId]. See [getFilePreviewUrlRelative]
|
||||||
|
String getFilePreviewUrlByFileId(
|
||||||
|
Account account,
|
||||||
|
int fileId, {
|
||||||
|
required int width,
|
||||||
|
required int height,
|
||||||
|
String? mode,
|
||||||
|
bool? a,
|
||||||
|
}) {
|
||||||
|
String url = "${account.url}/index.php/core/preview?fileId=$fileId";
|
||||||
|
url = "$url&x=$width&y=$height";
|
||||||
|
if (mode != null) {
|
||||||
|
url = "$url&mode=$mode";
|
||||||
|
}
|
||||||
|
if (a != null) {
|
||||||
|
url = "$url&a=${a ? 1 : 0}";
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
String getFileUrl(Account account, File file) {
|
String getFileUrl(Account account, File file) {
|
||||||
return "${account.url}/${getFileUrlRelative(file)}";
|
return "${account.url}/${getFileUrlRelative(file)}";
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import 'package:nc_photos/entity/person.dart';
|
||||||
import 'package:nc_photos/entity/tag.dart';
|
import 'package:nc_photos/entity/tag.dart';
|
||||||
import 'package:nc_photos/iterable_extension.dart';
|
import 'package:nc_photos/iterable_extension.dart';
|
||||||
import 'package:nc_photos/use_case/list_album.dart';
|
import 'package:nc_photos/use_case/list_album.dart';
|
||||||
|
import 'package:nc_photos/use_case/list_location_group.dart';
|
||||||
import 'package:nc_photos/use_case/list_person.dart';
|
import 'package:nc_photos/use_case/list_person.dart';
|
||||||
import 'package:nc_photos/use_case/list_tag.dart';
|
import 'package:nc_photos/use_case/list_tag.dart';
|
||||||
import 'package:tuple/tuple.dart';
|
import 'package:tuple/tuple.dart';
|
||||||
|
@ -51,6 +52,17 @@ class HomeSearchPersonResult implements HomeSearchResult {
|
||||||
final Person person;
|
final Person person;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class HomeSearchLocationResult implements HomeSearchResult {
|
||||||
|
const HomeSearchLocationResult(this.location);
|
||||||
|
|
||||||
|
@override
|
||||||
|
toString() => "$runtimeType {"
|
||||||
|
"location: $location, "
|
||||||
|
"}";
|
||||||
|
|
||||||
|
final LocationGroup location;
|
||||||
|
}
|
||||||
|
|
||||||
abstract class HomeSearchSuggestionBlocEvent {
|
abstract class HomeSearchSuggestionBlocEvent {
|
||||||
const HomeSearchSuggestionBlocEvent();
|
const HomeSearchSuggestionBlocEvent();
|
||||||
}
|
}
|
||||||
|
@ -200,6 +212,19 @@ class HomeSearchSuggestionBloc
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_log.warning("[_onEventPreloadData] Failed while ListPerson", e);
|
_log.warning("[_onEventPreloadData] Failed while ListPerson", e);
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
final locations = await ListLocationGroup(_c)(account);
|
||||||
|
// replace duplicated entries in name by the one in countryCode
|
||||||
|
final map = <String, LocationGroup>{};
|
||||||
|
for (final l in locations.name + locations.countryCode) {
|
||||||
|
map[l.place] = l;
|
||||||
|
}
|
||||||
|
product.addAll(map.values.map((e) => _LocationSearcheable(e)));
|
||||||
|
_log.info(
|
||||||
|
"[_onEventPreloadData] Loaded ${locations.name.length + locations.countryCode.length} locations");
|
||||||
|
} catch (e) {
|
||||||
|
_log.warning("[_onEventPreloadData] Failed while ListLocationGroup", e);
|
||||||
|
}
|
||||||
|
|
||||||
_setSearchItems(product);
|
_setSearchItems(product);
|
||||||
}
|
}
|
||||||
|
@ -262,3 +287,15 @@ class _PersonSearcheable implements _Searcheable {
|
||||||
|
|
||||||
final Person person;
|
final Person person;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _LocationSearcheable implements _Searcheable {
|
||||||
|
const _LocationSearcheable(this.location);
|
||||||
|
|
||||||
|
@override
|
||||||
|
toKeywords() => [location.place.toCi()];
|
||||||
|
|
||||||
|
@override
|
||||||
|
toResult() => HomeSearchLocationResult(location);
|
||||||
|
|
||||||
|
final LocationGroup location;
|
||||||
|
}
|
||||||
|
|
176
app/lib/bloc/list_location.dart
Normal file
176
app/lib/bloc/list_location.dart
Normal file
|
@ -0,0 +1,176 @@
|
||||||
|
import 'package:bloc/bloc.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:nc_photos/account.dart';
|
||||||
|
import 'package:nc_photos/di_container.dart';
|
||||||
|
import 'package:nc_photos/entity/file.dart';
|
||||||
|
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
||||||
|
import 'package:nc_photos/event/event.dart';
|
||||||
|
import 'package:nc_photos/throttler.dart';
|
||||||
|
import 'package:nc_photos/use_case/list_location_file.dart';
|
||||||
|
import 'package:nc_photos/use_case/list_location_group.dart';
|
||||||
|
|
||||||
|
abstract class ListLocationBlocEvent {
|
||||||
|
const ListLocationBlocEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
class ListLocationBlocQuery extends ListLocationBlocEvent {
|
||||||
|
const ListLocationBlocQuery(this.account);
|
||||||
|
|
||||||
|
@override
|
||||||
|
toString() => "$runtimeType {"
|
||||||
|
"account: $account, "
|
||||||
|
"}";
|
||||||
|
|
||||||
|
final Account account;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An external event has happened and may affect the state of this bloc
|
||||||
|
class _ListLocationBlocExternalEvent extends ListLocationBlocEvent {
|
||||||
|
const _ListLocationBlocExternalEvent();
|
||||||
|
|
||||||
|
@override
|
||||||
|
toString() => "$runtimeType {"
|
||||||
|
"}";
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class ListLocationBlocState {
|
||||||
|
const ListLocationBlocState(this.account, this.result);
|
||||||
|
|
||||||
|
@override
|
||||||
|
toString() => "$runtimeType {"
|
||||||
|
"account: $account, "
|
||||||
|
"result: $result, "
|
||||||
|
"}";
|
||||||
|
|
||||||
|
final Account? account;
|
||||||
|
final LocationGroupResult result;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ListLocationBlocInit extends ListLocationBlocState {
|
||||||
|
ListLocationBlocInit()
|
||||||
|
: super(null, const LocationGroupResult([], [], [], []));
|
||||||
|
}
|
||||||
|
|
||||||
|
class ListLocationBlocLoading extends ListLocationBlocState {
|
||||||
|
const ListLocationBlocLoading(Account? account, LocationGroupResult result)
|
||||||
|
: super(account, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ListLocationBlocSuccess extends ListLocationBlocState {
|
||||||
|
const ListLocationBlocSuccess(Account? account, LocationGroupResult result)
|
||||||
|
: super(account, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ListLocationBlocFailure extends ListLocationBlocState {
|
||||||
|
const ListLocationBlocFailure(
|
||||||
|
Account? account, LocationGroupResult result, this.exception)
|
||||||
|
: super(account, result);
|
||||||
|
|
||||||
|
@override
|
||||||
|
toString() => "$runtimeType {"
|
||||||
|
"super: ${super.toString()}, "
|
||||||
|
"exception: $exception, "
|
||||||
|
"}";
|
||||||
|
|
||||||
|
final Object exception;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The state of this bloc is inconsistent. This typically means that the data
|
||||||
|
/// may have been changed externally
|
||||||
|
class ListLocationBlocInconsistent extends ListLocationBlocState {
|
||||||
|
const ListLocationBlocInconsistent(
|
||||||
|
Account? account, LocationGroupResult result)
|
||||||
|
: super(account, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List all files associated with a specific tag
|
||||||
|
class ListLocationBloc
|
||||||
|
extends Bloc<ListLocationBlocEvent, ListLocationBlocState> {
|
||||||
|
ListLocationBloc(this._c)
|
||||||
|
: assert(require(_c)),
|
||||||
|
assert(ListLocationFile.require(_c)),
|
||||||
|
super(ListLocationBlocInit()) {
|
||||||
|
_fileRemovedEventListener.begin();
|
||||||
|
|
||||||
|
on<ListLocationBlocEvent>(_onEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool require(DiContainer c) =>
|
||||||
|
DiContainer.has(c, DiType.taggedFileRepo);
|
||||||
|
|
||||||
|
@override
|
||||||
|
close() {
|
||||||
|
_fileRemovedEventListener.end();
|
||||||
|
return super.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onEvent(
|
||||||
|
ListLocationBlocEvent event, Emitter<ListLocationBlocState> emit) async {
|
||||||
|
_log.info("[_onEvent] $event");
|
||||||
|
if (event is ListLocationBlocQuery) {
|
||||||
|
await _onEventQuery(event, emit);
|
||||||
|
} else if (event is _ListLocationBlocExternalEvent) {
|
||||||
|
await _onExternalEvent(event, emit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onEventQuery(
|
||||||
|
ListLocationBlocQuery ev, Emitter<ListLocationBlocState> emit) async {
|
||||||
|
try {
|
||||||
|
emit(ListLocationBlocLoading(ev.account, state.result));
|
||||||
|
emit(ListLocationBlocSuccess(ev.account, await _query(ev)));
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
_log.severe("[_onEventQuery] Exception while request", e, stackTrace);
|
||||||
|
emit(ListLocationBlocFailure(ev.account, state.result, e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onExternalEvent(_ListLocationBlocExternalEvent ev,
|
||||||
|
Emitter<ListLocationBlocState> emit) async {
|
||||||
|
emit(ListLocationBlocInconsistent(state.account, state.result));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onFileRemovedEvent(FileRemovedEvent ev) {
|
||||||
|
if (state is ListLocationBlocInit) {
|
||||||
|
// no data in this bloc, ignore
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_isFileOfInterest(ev.file)) {
|
||||||
|
_refreshThrottler.trigger(
|
||||||
|
maxResponceTime: const Duration(seconds: 3),
|
||||||
|
maxPendingCount: 10,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<LocationGroupResult> _query(ListLocationBlocQuery ev) =>
|
||||||
|
ListLocationGroup(_c.withLocalRepo())(ev.account);
|
||||||
|
|
||||||
|
bool _isFileOfInterest(File file) {
|
||||||
|
if (!file_util.isSupportedFormat(file)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final r in state.account?.roots ?? []) {
|
||||||
|
final dir = File(path: file_util.unstripPath(state.account!, r));
|
||||||
|
if (file_util.isUnderDir(file, dir)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final DiContainer _c;
|
||||||
|
|
||||||
|
late final _fileRemovedEventListener =
|
||||||
|
AppEventListener<FileRemovedEvent>(_onFileRemovedEvent);
|
||||||
|
|
||||||
|
late final _refreshThrottler = Throttler(
|
||||||
|
onTriggered: (_) {
|
||||||
|
add(const _ListLocationBlocExternalEvent());
|
||||||
|
},
|
||||||
|
logTag: "ListLocationBloc.refresh",
|
||||||
|
);
|
||||||
|
|
||||||
|
static final _log = Logger("bloc.list_location.ListLocationBloc");
|
||||||
|
}
|
184
app/lib/bloc/list_location_file.dart
Normal file
184
app/lib/bloc/list_location_file.dart
Normal file
|
@ -0,0 +1,184 @@
|
||||||
|
import 'package:bloc/bloc.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:nc_photos/account.dart';
|
||||||
|
import 'package:nc_photos/di_container.dart';
|
||||||
|
import 'package:nc_photos/entity/file.dart';
|
||||||
|
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
||||||
|
import 'package:nc_photos/event/event.dart';
|
||||||
|
import 'package:nc_photos/throttler.dart';
|
||||||
|
import 'package:nc_photos/use_case/list_location_file.dart';
|
||||||
|
|
||||||
|
abstract class ListLocationFileBlocEvent {
|
||||||
|
const ListLocationFileBlocEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
class ListLocationFileBlocQuery extends ListLocationFileBlocEvent {
|
||||||
|
const ListLocationFileBlocQuery(this.account, this.place, this.countryCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
toString() => "$runtimeType {"
|
||||||
|
"account: $account, "
|
||||||
|
"place: $place, "
|
||||||
|
"countryCode: $countryCode, "
|
||||||
|
"}";
|
||||||
|
|
||||||
|
final Account account;
|
||||||
|
final String? place;
|
||||||
|
final String countryCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An external event has happened and may affect the state of this bloc
|
||||||
|
class _ListLocationFileBlocExternalEvent extends ListLocationFileBlocEvent {
|
||||||
|
const _ListLocationFileBlocExternalEvent();
|
||||||
|
|
||||||
|
@override
|
||||||
|
toString() => "$runtimeType {"
|
||||||
|
"}";
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class ListLocationFileBlocState {
|
||||||
|
const ListLocationFileBlocState(this.account, this.items);
|
||||||
|
|
||||||
|
@override
|
||||||
|
toString() => "$runtimeType {"
|
||||||
|
"account: $account, "
|
||||||
|
"items: List {length: ${items.length}}, "
|
||||||
|
"}";
|
||||||
|
|
||||||
|
final Account? account;
|
||||||
|
final List<File> items;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ListLocationFileBlocInit extends ListLocationFileBlocState {
|
||||||
|
ListLocationFileBlocInit() : super(null, const []);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ListLocationFileBlocLoading extends ListLocationFileBlocState {
|
||||||
|
const ListLocationFileBlocLoading(Account? account, List<File> items)
|
||||||
|
: super(account, items);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ListLocationFileBlocSuccess extends ListLocationFileBlocState {
|
||||||
|
const ListLocationFileBlocSuccess(Account? account, List<File> items)
|
||||||
|
: super(account, items);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ListLocationFileBlocFailure extends ListLocationFileBlocState {
|
||||||
|
const ListLocationFileBlocFailure(
|
||||||
|
Account? account, List<File> items, this.exception)
|
||||||
|
: super(account, items);
|
||||||
|
|
||||||
|
@override
|
||||||
|
toString() => "$runtimeType {"
|
||||||
|
"super: ${super.toString()}, "
|
||||||
|
"exception: $exception, "
|
||||||
|
"}";
|
||||||
|
|
||||||
|
final Object exception;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The state of this bloc is inconsistent. This typically means that the data
|
||||||
|
/// may have been changed externally
|
||||||
|
class ListLocationFileBlocInconsistent extends ListLocationFileBlocState {
|
||||||
|
const ListLocationFileBlocInconsistent(Account? account, List<File> items)
|
||||||
|
: super(account, items);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List all files associated with a specific tag
|
||||||
|
class ListLocationFileBloc
|
||||||
|
extends Bloc<ListLocationFileBlocEvent, ListLocationFileBlocState> {
|
||||||
|
ListLocationFileBloc(this._c)
|
||||||
|
: assert(require(_c)),
|
||||||
|
assert(ListLocationFile.require(_c)),
|
||||||
|
super(ListLocationFileBlocInit()) {
|
||||||
|
_fileRemovedEventListener.begin();
|
||||||
|
|
||||||
|
on<ListLocationFileBlocEvent>(_onEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool require(DiContainer c) =>
|
||||||
|
DiContainer.has(c, DiType.taggedFileRepo);
|
||||||
|
|
||||||
|
@override
|
||||||
|
close() {
|
||||||
|
_fileRemovedEventListener.end();
|
||||||
|
return super.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onEvent(ListLocationFileBlocEvent event,
|
||||||
|
Emitter<ListLocationFileBlocState> emit) async {
|
||||||
|
_log.info("[_onEvent] $event");
|
||||||
|
if (event is ListLocationFileBlocQuery) {
|
||||||
|
await _onEventQuery(event, emit);
|
||||||
|
} else if (event is _ListLocationFileBlocExternalEvent) {
|
||||||
|
await _onExternalEvent(event, emit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onEventQuery(ListLocationFileBlocQuery ev,
|
||||||
|
Emitter<ListLocationFileBlocState> emit) async {
|
||||||
|
try {
|
||||||
|
emit(ListLocationFileBlocLoading(ev.account, state.items));
|
||||||
|
emit(ListLocationFileBlocSuccess(ev.account, await _query(ev)));
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
_log.severe("[_onEventQuery] Exception while request", e, stackTrace);
|
||||||
|
emit(ListLocationFileBlocFailure(ev.account, state.items, e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onExternalEvent(_ListLocationFileBlocExternalEvent ev,
|
||||||
|
Emitter<ListLocationFileBlocState> emit) async {
|
||||||
|
emit(ListLocationFileBlocInconsistent(state.account, state.items));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onFileRemovedEvent(FileRemovedEvent ev) {
|
||||||
|
if (state is ListLocationFileBlocInit) {
|
||||||
|
// no data in this bloc, ignore
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_isFileOfInterest(ev.file)) {
|
||||||
|
_refreshThrottler.trigger(
|
||||||
|
maxResponceTime: const Duration(seconds: 3),
|
||||||
|
maxPendingCount: 10,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<File>> _query(ListLocationFileBlocQuery ev) async {
|
||||||
|
final files = <File>[];
|
||||||
|
for (final r in ev.account.roots) {
|
||||||
|
final dir = File(path: file_util.unstripPath(ev.account, r));
|
||||||
|
files.addAll(await ListLocationFile(_c)(
|
||||||
|
ev.account, dir, ev.place, ev.countryCode));
|
||||||
|
}
|
||||||
|
return files.where((f) => file_util.isSupportedFormat(f)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isFileOfInterest(File file) {
|
||||||
|
if (!file_util.isSupportedFormat(file)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final r in state.account?.roots ?? []) {
|
||||||
|
final dir = File(path: file_util.unstripPath(state.account!, r));
|
||||||
|
if (file_util.isUnderDir(file, dir)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final DiContainer _c;
|
||||||
|
|
||||||
|
late final _fileRemovedEventListener =
|
||||||
|
AppEventListener<FileRemovedEvent>(_onFileRemovedEvent);
|
||||||
|
|
||||||
|
late final _refreshThrottler = Throttler(
|
||||||
|
onTriggered: (_) {
|
||||||
|
add(const _ListLocationFileBlocExternalEvent());
|
||||||
|
},
|
||||||
|
logTag: "ListLocationFileBloc.refresh",
|
||||||
|
);
|
||||||
|
|
||||||
|
static final _log = Logger("bloc.list_location_file.ListLocationFileBloc");
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ 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/person.dart';
|
import 'package:nc_photos/entity/person.dart';
|
||||||
|
import 'package:nc_photos/use_case/list_location_group.dart';
|
||||||
import 'package:nc_photos/use_case/list_person.dart';
|
import 'package:nc_photos/use_case/list_person.dart';
|
||||||
|
|
||||||
abstract class SearchLandingBlocEvent {
|
abstract class SearchLandingBlocEvent {
|
||||||
|
@ -21,36 +22,41 @@ class SearchLandingBlocQuery extends SearchLandingBlocEvent {
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class SearchLandingBlocState {
|
abstract class SearchLandingBlocState {
|
||||||
const SearchLandingBlocState(this.account, this.persons);
|
const SearchLandingBlocState(this.account, this.persons, this.locations);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
toString() => "$runtimeType {"
|
toString() => "$runtimeType {"
|
||||||
"account: $account, "
|
"account: $account, "
|
||||||
"persons: List {length: ${persons.length}}, "
|
"persons: List {length: ${persons.length}}, "
|
||||||
|
"locations: $locations, "
|
||||||
"}";
|
"}";
|
||||||
|
|
||||||
final Account? account;
|
final Account? account;
|
||||||
final List<Person> persons;
|
final List<Person> persons;
|
||||||
|
final LocationGroupResult locations;
|
||||||
}
|
}
|
||||||
|
|
||||||
class SearchLandingBlocInit extends SearchLandingBlocState {
|
class SearchLandingBlocInit extends SearchLandingBlocState {
|
||||||
SearchLandingBlocInit() : super(null, const []);
|
SearchLandingBlocInit()
|
||||||
|
: super(null, const [], const LocationGroupResult([], [], [], []));
|
||||||
}
|
}
|
||||||
|
|
||||||
class SearchLandingBlocLoading extends SearchLandingBlocState {
|
class SearchLandingBlocLoading extends SearchLandingBlocState {
|
||||||
const SearchLandingBlocLoading(Account? account, List<Person> persons)
|
const SearchLandingBlocLoading(
|
||||||
: super(account, persons);
|
Account? account, List<Person> persons, LocationGroupResult locations)
|
||||||
|
: super(account, persons, locations);
|
||||||
}
|
}
|
||||||
|
|
||||||
class SearchLandingBlocSuccess extends SearchLandingBlocState {
|
class SearchLandingBlocSuccess extends SearchLandingBlocState {
|
||||||
const SearchLandingBlocSuccess(Account? account, List<Person> persons)
|
const SearchLandingBlocSuccess(
|
||||||
: super(account, persons);
|
Account? account, List<Person> persons, LocationGroupResult locations)
|
||||||
|
: super(account, persons, locations);
|
||||||
}
|
}
|
||||||
|
|
||||||
class SearchLandingBlocFailure extends SearchLandingBlocState {
|
class SearchLandingBlocFailure extends SearchLandingBlocState {
|
||||||
const SearchLandingBlocFailure(
|
const SearchLandingBlocFailure(Account? account, List<Person> persons,
|
||||||
Account? account, List<Person> persons, this.exception)
|
LocationGroupResult locations, this.exception)
|
||||||
: super(account, persons);
|
: super(account, persons, locations);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
toString() => "$runtimeType {"
|
toString() => "$runtimeType {"
|
||||||
|
@ -83,17 +89,39 @@ class SearchLandingBloc
|
||||||
Future<void> _onEventQuery(
|
Future<void> _onEventQuery(
|
||||||
SearchLandingBlocQuery ev, Emitter<SearchLandingBlocState> emit) async {
|
SearchLandingBlocQuery ev, Emitter<SearchLandingBlocState> emit) async {
|
||||||
try {
|
try {
|
||||||
emit(SearchLandingBlocLoading(ev.account, state.persons));
|
emit(
|
||||||
emit(SearchLandingBlocSuccess(ev.account, await _query(ev)));
|
SearchLandingBlocLoading(ev.account, state.persons, state.locations));
|
||||||
|
|
||||||
|
List<Person>? persons;
|
||||||
|
try {
|
||||||
|
persons = await _queryPeople(ev);
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
_log.shout("[_onEventQuery] Failed while _queryPeople", e, stackTrace);
|
||||||
|
}
|
||||||
|
|
||||||
|
LocationGroupResult? locations;
|
||||||
|
try {
|
||||||
|
locations = await _queryLocations(ev);
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
_log.shout(
|
||||||
|
"[_onEventQuery] Failed while _queryLocations", e, stackTrace);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(SearchLandingBlocSuccess(ev.account, persons ?? [],
|
||||||
|
locations ?? const LocationGroupResult([], [], [], [])));
|
||||||
} catch (e, stackTrace) {
|
} catch (e, stackTrace) {
|
||||||
_log.severe("[_onEventQuery] Exception while request", e, stackTrace);
|
_log.severe("[_onEventQuery] Exception while request", e, stackTrace);
|
||||||
emit(SearchLandingBlocFailure(ev.account, state.persons, e));
|
emit(SearchLandingBlocFailure(
|
||||||
|
ev.account, state.persons, state.locations, e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Person>> _query(SearchLandingBlocQuery ev) =>
|
Future<List<Person>> _queryPeople(SearchLandingBlocQuery ev) =>
|
||||||
ListPerson(_c.withLocalRepo())(ev.account);
|
ListPerson(_c.withLocalRepo())(ev.account);
|
||||||
|
|
||||||
|
Future<LocationGroupResult> _queryLocations(SearchLandingBlocQuery ev) =>
|
||||||
|
ListLocationGroup(_c.withLocalRepo())(ev.account);
|
||||||
|
|
||||||
final DiContainer _c;
|
final DiContainer _c;
|
||||||
|
|
||||||
static final _log = Logger("bloc.search_landing.SearchLandingBloc");
|
static final _log = Logger("bloc.search_landing.SearchLandingBloc");
|
||||||
|
|
|
@ -21,6 +21,81 @@ int compareFileDateTimeDescending(File x, File y) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ImageLocation with EquatableMixin {
|
||||||
|
const ImageLocation({
|
||||||
|
this.version = appVersion,
|
||||||
|
required this.name,
|
||||||
|
required this.latitude,
|
||||||
|
required this.longitude,
|
||||||
|
required this.countryCode,
|
||||||
|
this.admin1,
|
||||||
|
this.admin2,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory ImageLocation.empty() => const ImageLocation(
|
||||||
|
name: null, latitude: null, longitude: null, countryCode: null);
|
||||||
|
|
||||||
|
static ImageLocation fromJson(JsonObj json) {
|
||||||
|
return ImageLocation(
|
||||||
|
version: json["v"],
|
||||||
|
name: json["name"],
|
||||||
|
latitude: json["lat"] == null ? null : json["lat"] / 10000,
|
||||||
|
longitude: json["lng"] == null ? null : json["lng"] / 10000,
|
||||||
|
countryCode: json["cc"],
|
||||||
|
admin1: json["admin1"],
|
||||||
|
admin2: json["admin2"],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonObj toJson() => {
|
||||||
|
"v": version,
|
||||||
|
if (name != null) "name": name,
|
||||||
|
if (latitude != null) "lat": (latitude! * 10000).round(),
|
||||||
|
if (longitude != null) "lng": (longitude! * 10000).round(),
|
||||||
|
if (countryCode != null) "cc": countryCode,
|
||||||
|
if (admin1 != null) "admin1": admin1,
|
||||||
|
if (admin2 != null) "admin2": admin2,
|
||||||
|
};
|
||||||
|
|
||||||
|
bool isEmpty() => name == null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
toString() {
|
||||||
|
var product = "$runtimeType {"
|
||||||
|
"version: $version, ";
|
||||||
|
if (name != null) {
|
||||||
|
product += "name: $name, "
|
||||||
|
"latitude: $latitude, "
|
||||||
|
"longitude: $longitude, "
|
||||||
|
"countryCode: $countryCode, "
|
||||||
|
"admin1: $admin1, "
|
||||||
|
"admin2: $admin2, ";
|
||||||
|
}
|
||||||
|
return product + "}";
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
get props => [
|
||||||
|
version,
|
||||||
|
name,
|
||||||
|
latitude,
|
||||||
|
longitude,
|
||||||
|
countryCode,
|
||||||
|
admin1,
|
||||||
|
admin2,
|
||||||
|
];
|
||||||
|
|
||||||
|
final int version;
|
||||||
|
final String? name;
|
||||||
|
final double? latitude;
|
||||||
|
final double? longitude;
|
||||||
|
final String? countryCode;
|
||||||
|
final String? admin1;
|
||||||
|
final String? admin2;
|
||||||
|
|
||||||
|
static const appVersion = 1;
|
||||||
|
}
|
||||||
|
|
||||||
/// Immutable object that hold metadata of a [File]
|
/// Immutable object that hold metadata of a [File]
|
||||||
class Metadata with EquatableMixin {
|
class Metadata with EquatableMixin {
|
||||||
Metadata({
|
Metadata({
|
||||||
|
@ -238,6 +313,7 @@ class File with EquatableMixin {
|
||||||
this.trashbinFilename,
|
this.trashbinFilename,
|
||||||
this.trashbinOriginalLocation,
|
this.trashbinOriginalLocation,
|
||||||
this.trashbinDeletionTime,
|
this.trashbinDeletionTime,
|
||||||
|
this.location,
|
||||||
}) : path = path.trimAny("/");
|
}) : path = path.trimAny("/");
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -292,6 +368,9 @@ class File with EquatableMixin {
|
||||||
overrideDateTime: json["overrideDateTime"] == null
|
overrideDateTime: json["overrideDateTime"] == null
|
||||||
? null
|
? null
|
||||||
: DateTime.parse(json["overrideDateTime"]),
|
: DateTime.parse(json["overrideDateTime"]),
|
||||||
|
location: json["location"] == null
|
||||||
|
? null
|
||||||
|
: ImageLocation.fromJson(json["location"]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -350,6 +429,9 @@ class File with EquatableMixin {
|
||||||
if (overrideDateTime != null) {
|
if (overrideDateTime != null) {
|
||||||
product += "overrideDateTime: $overrideDateTime, ";
|
product += "overrideDateTime: $overrideDateTime, ";
|
||||||
}
|
}
|
||||||
|
if (location != null) {
|
||||||
|
product += "location: $location, ";
|
||||||
|
}
|
||||||
return product + "}";
|
return product + "}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -377,6 +459,7 @@ class File with EquatableMixin {
|
||||||
if (isArchived != null) "isArchived": isArchived,
|
if (isArchived != null) "isArchived": isArchived,
|
||||||
if (overrideDateTime != null)
|
if (overrideDateTime != null)
|
||||||
"overrideDateTime": overrideDateTime!.toUtc().toIso8601String(),
|
"overrideDateTime": overrideDateTime!.toUtc().toIso8601String(),
|
||||||
|
if (location != null) "location": location!.toJson(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -399,6 +482,7 @@ class File with EquatableMixin {
|
||||||
OrNull<Metadata>? metadata,
|
OrNull<Metadata>? metadata,
|
||||||
OrNull<bool>? isArchived,
|
OrNull<bool>? isArchived,
|
||||||
OrNull<DateTime>? overrideDateTime,
|
OrNull<DateTime>? overrideDateTime,
|
||||||
|
OrNull<ImageLocation>? location,
|
||||||
}) {
|
}) {
|
||||||
return File(
|
return File(
|
||||||
path: path ?? this.path,
|
path: path ?? this.path,
|
||||||
|
@ -422,6 +506,7 @@ class File with EquatableMixin {
|
||||||
overrideDateTime: overrideDateTime == null
|
overrideDateTime: overrideDateTime == null
|
||||||
? this.overrideDateTime
|
? this.overrideDateTime
|
||||||
: overrideDateTime.obj,
|
: overrideDateTime.obj,
|
||||||
|
location: location == null ? this.location : location.obj,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -445,6 +530,7 @@ class File with EquatableMixin {
|
||||||
// metadata is handled separately, see [equals]
|
// metadata is handled separately, see [equals]
|
||||||
isArchived,
|
isArchived,
|
||||||
overrideDateTime,
|
overrideDateTime,
|
||||||
|
location,
|
||||||
];
|
];
|
||||||
|
|
||||||
final String path;
|
final String path;
|
||||||
|
@ -466,6 +552,7 @@ class File with EquatableMixin {
|
||||||
final Metadata? metadata;
|
final Metadata? metadata;
|
||||||
final bool? isArchived;
|
final bool? isArchived;
|
||||||
final DateTime? overrideDateTime;
|
final DateTime? overrideDateTime;
|
||||||
|
final ImageLocation? location;
|
||||||
}
|
}
|
||||||
|
|
||||||
extension FileExtension on File {
|
extension FileExtension on File {
|
||||||
|
@ -581,6 +668,7 @@ class FileRepo {
|
||||||
OrNull<bool>? isArchived,
|
OrNull<bool>? isArchived,
|
||||||
OrNull<DateTime>? overrideDateTime,
|
OrNull<DateTime>? overrideDateTime,
|
||||||
bool? favorite,
|
bool? favorite,
|
||||||
|
OrNull<ImageLocation>? location,
|
||||||
}) =>
|
}) =>
|
||||||
dataSrc.updateProperty(
|
dataSrc.updateProperty(
|
||||||
account,
|
account,
|
||||||
|
@ -589,6 +677,7 @@ class FileRepo {
|
||||||
isArchived: isArchived,
|
isArchived: isArchived,
|
||||||
overrideDateTime: overrideDateTime,
|
overrideDateTime: overrideDateTime,
|
||||||
favorite: favorite,
|
favorite: favorite,
|
||||||
|
location: location,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// See [FileDataSource.copy]
|
/// See [FileDataSource.copy]
|
||||||
|
@ -660,6 +749,7 @@ abstract class FileDataSource {
|
||||||
OrNull<bool>? isArchived,
|
OrNull<bool>? isArchived,
|
||||||
OrNull<DateTime>? overrideDateTime,
|
OrNull<DateTime>? overrideDateTime,
|
||||||
bool? favorite,
|
bool? favorite,
|
||||||
|
OrNull<ImageLocation>? location,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Copy [f] to [destination]
|
/// Copy [f] to [destination]
|
||||||
|
|
|
@ -58,6 +58,7 @@ class FileWebdavDataSource implements FileDataSource {
|
||||||
"app:metadata",
|
"app:metadata",
|
||||||
"app:is-archived",
|
"app:is-archived",
|
||||||
"app:override-date-time",
|
"app:override-date-time",
|
||||||
|
"app:location",
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -135,6 +136,7 @@ class FileWebdavDataSource implements FileDataSource {
|
||||||
OrNull<bool>? isArchived,
|
OrNull<bool>? isArchived,
|
||||||
OrNull<DateTime>? overrideDateTime,
|
OrNull<DateTime>? overrideDateTime,
|
||||||
bool? favorite,
|
bool? favorite,
|
||||||
|
OrNull<ImageLocation>? location,
|
||||||
}) async {
|
}) async {
|
||||||
_log.info("[updateProperty] ${f.path}");
|
_log.info("[updateProperty] ${f.path}");
|
||||||
if (metadata?.obj != null && metadata!.obj!.fileEtag != f.etag) {
|
if (metadata?.obj != null && metadata!.obj!.fileEtag != f.etag) {
|
||||||
|
@ -149,11 +151,14 @@ class FileWebdavDataSource implements FileDataSource {
|
||||||
"app:override-date-time":
|
"app:override-date-time":
|
||||||
overrideDateTime!.obj!.toUtc().toIso8601String(),
|
overrideDateTime!.obj!.toUtc().toIso8601String(),
|
||||||
if (favorite != null) "oc:favorite": favorite ? 1 : 0,
|
if (favorite != null) "oc:favorite": favorite ? 1 : 0,
|
||||||
|
if (location?.obj != null)
|
||||||
|
"app:location": jsonEncode(location!.obj!.toJson()),
|
||||||
};
|
};
|
||||||
final removeProps = [
|
final removeProps = [
|
||||||
if (OrNull.isSetNull(metadata)) "app:metadata",
|
if (OrNull.isSetNull(metadata)) "app:metadata",
|
||||||
if (OrNull.isSetNull(isArchived)) "app:is-archived",
|
if (OrNull.isSetNull(isArchived)) "app:is-archived",
|
||||||
if (OrNull.isSetNull(overrideDateTime)) "app:override-date-time",
|
if (OrNull.isSetNull(overrideDateTime)) "app:override-date-time",
|
||||||
|
if (OrNull.isSetNull(location)) "app:location",
|
||||||
];
|
];
|
||||||
final response = await Api(account).files().proppatch(
|
final response = await Api(account).files().proppatch(
|
||||||
path: f.path,
|
path: f.path,
|
||||||
|
@ -417,6 +422,7 @@ class FileSqliteDbDataSource implements FileDataSource {
|
||||||
r.readTable(db.files),
|
r.readTable(db.files),
|
||||||
r.readTable(db.accountFiles),
|
r.readTable(db.accountFiles),
|
||||||
r.readTableOrNull(db.images),
|
r.readTableOrNull(db.images),
|
||||||
|
r.readTableOrNull(db.imageLocations),
|
||||||
r.readTableOrNull(db.trashes),
|
r.readTableOrNull(db.trashes),
|
||||||
))
|
))
|
||||||
.get();
|
.get();
|
||||||
|
@ -450,6 +456,7 @@ class FileSqliteDbDataSource implements FileDataSource {
|
||||||
OrNull<bool>? isArchived,
|
OrNull<bool>? isArchived,
|
||||||
OrNull<DateTime>? overrideDateTime,
|
OrNull<DateTime>? overrideDateTime,
|
||||||
bool? favorite,
|
bool? favorite,
|
||||||
|
OrNull<ImageLocation>? location,
|
||||||
}) async {
|
}) async {
|
||||||
_log.info("[updateProperty] ${f.path}");
|
_log.info("[updateProperty] ${f.path}");
|
||||||
await _c.sqliteDb.use((db) async {
|
await _c.sqliteDb.use((db) async {
|
||||||
|
@ -490,6 +497,26 @@ class FileSqliteDbDataSource implements FileDataSource {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (location != null) {
|
||||||
|
if (location.obj == null) {
|
||||||
|
await (db.delete(db.imageLocations)
|
||||||
|
..where((t) => t.accountFile.equals(rowIds.accountFileRowId)))
|
||||||
|
.go();
|
||||||
|
} else {
|
||||||
|
await db
|
||||||
|
.into(db.imageLocations)
|
||||||
|
.insertOnConflictUpdate(sql.ImageLocationsCompanion.insert(
|
||||||
|
accountFile: sql.Value(rowIds.accountFileRowId),
|
||||||
|
version: location.obj!.version,
|
||||||
|
name: sql.Value(location.obj!.name),
|
||||||
|
latitude: sql.Value(location.obj!.latitude),
|
||||||
|
longitude: sql.Value(location.obj!.longitude),
|
||||||
|
countryCode: sql.Value(location.obj!.countryCode),
|
||||||
|
admin1: sql.Value(location.obj!.admin1),
|
||||||
|
admin2: sql.Value(location.obj!.admin2),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -627,6 +654,7 @@ class FileCachedDataSource implements FileDataSource {
|
||||||
OrNull<bool>? isArchived,
|
OrNull<bool>? isArchived,
|
||||||
OrNull<DateTime>? overrideDateTime,
|
OrNull<DateTime>? overrideDateTime,
|
||||||
bool? favorite,
|
bool? favorite,
|
||||||
|
OrNull<ImageLocation>? location,
|
||||||
}) async {
|
}) async {
|
||||||
await _remoteSrc.updateProperty(
|
await _remoteSrc.updateProperty(
|
||||||
account,
|
account,
|
||||||
|
@ -635,6 +663,7 @@ class FileCachedDataSource implements FileDataSource {
|
||||||
isArchived: isArchived,
|
isArchived: isArchived,
|
||||||
overrideDateTime: overrideDateTime,
|
overrideDateTime: overrideDateTime,
|
||||||
favorite: favorite,
|
favorite: favorite,
|
||||||
|
location: location,
|
||||||
);
|
);
|
||||||
await _sqliteDbSrc.updateProperty(
|
await _sqliteDbSrc.updateProperty(
|
||||||
account,
|
account,
|
||||||
|
@ -643,6 +672,7 @@ class FileCachedDataSource implements FileDataSource {
|
||||||
isArchived: isArchived,
|
isArchived: isArchived,
|
||||||
overrideDateTime: overrideDateTime,
|
overrideDateTime: overrideDateTime,
|
||||||
favorite: favorite,
|
favorite: favorite,
|
||||||
|
location: location,
|
||||||
);
|
);
|
||||||
|
|
||||||
// generate a new random token
|
// generate a new random token
|
||||||
|
|
|
@ -226,6 +226,14 @@ class FileSqliteCacheUpdater {
|
||||||
t.accountFile.equals(thisRowIds.accountFileRowId),
|
t.accountFile.equals(thisRowIds.accountFileRowId),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (f.imageLocation != null) {
|
||||||
|
batch.update(
|
||||||
|
db.imageLocations,
|
||||||
|
f.imageLocation!,
|
||||||
|
where: (sql.$ImageLocationsTable t) =>
|
||||||
|
t.accountFile.equals(thisRowIds.accountFileRowId),
|
||||||
|
);
|
||||||
|
}
|
||||||
if (f.trash != null) {
|
if (f.trash != null) {
|
||||||
batch.update(
|
batch.update(
|
||||||
db.trashes,
|
db.trashes,
|
||||||
|
@ -289,6 +297,10 @@ class FileSqliteCacheUpdater {
|
||||||
await db.into(db.images).insert(
|
await db.into(db.images).insert(
|
||||||
f.image!.copyWith(accountFile: sql.Value(dbAccountFile.rowId)));
|
f.image!.copyWith(accountFile: sql.Value(dbAccountFile.rowId)));
|
||||||
}
|
}
|
||||||
|
if (f.imageLocation != null) {
|
||||||
|
await db.into(db.imageLocations).insert(f.imageLocation!
|
||||||
|
.copyWith(accountFile: sql.Value(dbAccountFile.rowId)));
|
||||||
|
}
|
||||||
if (f.trash != null) {
|
if (f.trash != null) {
|
||||||
await db
|
await db
|
||||||
.into(db.trashes)
|
.into(db.trashes)
|
||||||
|
|
|
@ -88,8 +88,11 @@ bool isNoMediaMarkerPath(String path) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return if there's missing metadata in [file]
|
/// Return if there's missing metadata in [file]
|
||||||
|
///
|
||||||
|
/// Current this function will check both [File.metadata] and [File.location]
|
||||||
bool isMissingMetadata(File file) =>
|
bool isMissingMetadata(File file) =>
|
||||||
isSupportedImageFormat(file) && file.metadata == null;
|
isSupportedImageFormat(file) &&
|
||||||
|
(file.metadata == null || file.location == null);
|
||||||
|
|
||||||
final _supportedFormatMimes = [
|
final _supportedFormatMimes = [
|
||||||
"image/jpeg",
|
"image/jpeg",
|
||||||
|
|
|
@ -39,6 +39,7 @@ class SearchSqliteDbDataSource implements SearchDataSource {
|
||||||
r.readTable(db.files),
|
r.readTable(db.files),
|
||||||
r.readTable(db.accountFiles),
|
r.readTable(db.accountFiles),
|
||||||
r.readTableOrNull(db.images),
|
r.readTableOrNull(db.images),
|
||||||
|
r.readTableOrNull(db.imageLocations),
|
||||||
r.readTableOrNull(db.trashes),
|
r.readTableOrNull(db.trashes),
|
||||||
))
|
))
|
||||||
.get();
|
.get();
|
||||||
|
|
|
@ -89,6 +89,22 @@ class Images extends Table {
|
||||||
get primaryKey => {accountFile};
|
get primaryKey => {accountFile};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Estimated locations for images
|
||||||
|
class ImageLocations extends Table {
|
||||||
|
IntColumn get accountFile =>
|
||||||
|
integer().references(AccountFiles, #rowId, onDelete: KeyAction.cascade)();
|
||||||
|
IntColumn get version => integer()();
|
||||||
|
TextColumn get name => text().nullable()();
|
||||||
|
RealColumn get latitude => real().nullable()();
|
||||||
|
RealColumn get longitude => real().nullable()();
|
||||||
|
TextColumn get countryCode => text().nullable()();
|
||||||
|
TextColumn get admin1 => text().nullable()();
|
||||||
|
TextColumn get admin2 => text().nullable()();
|
||||||
|
|
||||||
|
@override
|
||||||
|
get primaryKey => {accountFile};
|
||||||
|
}
|
||||||
|
|
||||||
/// A file inside trashbin
|
/// A file inside trashbin
|
||||||
@DataClassName("Trash")
|
@DataClassName("Trash")
|
||||||
class Trashes extends Table {
|
class Trashes extends Table {
|
||||||
|
@ -184,6 +200,7 @@ class Persons extends Table {
|
||||||
Accounts,
|
Accounts,
|
||||||
Files,
|
Files,
|
||||||
Images,
|
Images,
|
||||||
|
ImageLocations,
|
||||||
Trashes,
|
Trashes,
|
||||||
AccountFiles,
|
AccountFiles,
|
||||||
DirFiles,
|
DirFiles,
|
||||||
|
@ -201,7 +218,7 @@ class SqliteDb extends _$SqliteDb {
|
||||||
SqliteDb.connect(DatabaseConnection connection) : super.connect(connection);
|
SqliteDb.connect(DatabaseConnection connection) : super.connect(connection);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
get schemaVersion => 2;
|
get schemaVersion => 3;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
get migration => MigrationStrategy(
|
get migration => MigrationStrategy(
|
||||||
|
@ -242,6 +259,10 @@ class SqliteDb extends _$SqliteDb {
|
||||||
await m.createTable(persons);
|
await m.createTable(persons);
|
||||||
await _createIndexV2(m);
|
await _createIndexV2(m);
|
||||||
}
|
}
|
||||||
|
if (from < 3) {
|
||||||
|
await m.createTable(imageLocations);
|
||||||
|
await _createIndexV3(m);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} catch (e, stackTrace) {
|
} catch (e, stackTrace) {
|
||||||
_log.shout("[onUpgrade] Failed upgrading sqlite db", e, stackTrace);
|
_log.shout("[onUpgrade] Failed upgrading sqlite db", e, stackTrace);
|
||||||
|
@ -265,6 +286,17 @@ class SqliteDb extends _$SqliteDb {
|
||||||
"CREATE INDEX persons_account_index ON persons(account);"));
|
"CREATE INDEX persons_account_index ON persons(account);"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _createIndexV3(Migrator m) async {
|
||||||
|
await m.createIndex(Index("image_locations_name_index",
|
||||||
|
"CREATE INDEX image_locations_name_index ON image_locations(name);"));
|
||||||
|
await m.createIndex(Index("image_locations_country_code_index",
|
||||||
|
"CREATE INDEX image_locations_country_code_index ON image_locations(country_code);"));
|
||||||
|
await m.createIndex(Index("image_locations_admin1_index",
|
||||||
|
"CREATE INDEX image_locations_admin1_index ON image_locations(admin1);"));
|
||||||
|
await m.createIndex(Index("image_locations_admin2_index",
|
||||||
|
"CREATE INDEX image_locations_admin2_index ON image_locations(admin2);"));
|
||||||
|
}
|
||||||
|
|
||||||
static final _log = Logger("entity.sqlite_table.SqliteDb");
|
static final _log = Logger("entity.sqlite_table.SqliteDb");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1820,6 +1820,416 @@ class $ImagesTable extends Images with TableInfo<$ImagesTable, Image> {
|
||||||
const _DateTimeConverter();
|
const _DateTimeConverter();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ImageLocation extends DataClass implements Insertable<ImageLocation> {
|
||||||
|
final int accountFile;
|
||||||
|
final int version;
|
||||||
|
final String? name;
|
||||||
|
final double? latitude;
|
||||||
|
final double? longitude;
|
||||||
|
final String? countryCode;
|
||||||
|
final String? admin1;
|
||||||
|
final String? admin2;
|
||||||
|
ImageLocation(
|
||||||
|
{required this.accountFile,
|
||||||
|
required this.version,
|
||||||
|
this.name,
|
||||||
|
this.latitude,
|
||||||
|
this.longitude,
|
||||||
|
this.countryCode,
|
||||||
|
this.admin1,
|
||||||
|
this.admin2});
|
||||||
|
factory ImageLocation.fromData(Map<String, dynamic> data, {String? prefix}) {
|
||||||
|
final effectivePrefix = prefix ?? '';
|
||||||
|
return ImageLocation(
|
||||||
|
accountFile: const IntType()
|
||||||
|
.mapFromDatabaseResponse(data['${effectivePrefix}account_file'])!,
|
||||||
|
version: const IntType()
|
||||||
|
.mapFromDatabaseResponse(data['${effectivePrefix}version'])!,
|
||||||
|
name: const StringType()
|
||||||
|
.mapFromDatabaseResponse(data['${effectivePrefix}name']),
|
||||||
|
latitude: const RealType()
|
||||||
|
.mapFromDatabaseResponse(data['${effectivePrefix}latitude']),
|
||||||
|
longitude: const RealType()
|
||||||
|
.mapFromDatabaseResponse(data['${effectivePrefix}longitude']),
|
||||||
|
countryCode: const StringType()
|
||||||
|
.mapFromDatabaseResponse(data['${effectivePrefix}country_code']),
|
||||||
|
admin1: const StringType()
|
||||||
|
.mapFromDatabaseResponse(data['${effectivePrefix}admin1']),
|
||||||
|
admin2: const StringType()
|
||||||
|
.mapFromDatabaseResponse(data['${effectivePrefix}admin2']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@override
|
||||||
|
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||||
|
final map = <String, Expression>{};
|
||||||
|
map['account_file'] = Variable<int>(accountFile);
|
||||||
|
map['version'] = Variable<int>(version);
|
||||||
|
if (!nullToAbsent || name != null) {
|
||||||
|
map['name'] = Variable<String?>(name);
|
||||||
|
}
|
||||||
|
if (!nullToAbsent || latitude != null) {
|
||||||
|
map['latitude'] = Variable<double?>(latitude);
|
||||||
|
}
|
||||||
|
if (!nullToAbsent || longitude != null) {
|
||||||
|
map['longitude'] = Variable<double?>(longitude);
|
||||||
|
}
|
||||||
|
if (!nullToAbsent || countryCode != null) {
|
||||||
|
map['country_code'] = Variable<String?>(countryCode);
|
||||||
|
}
|
||||||
|
if (!nullToAbsent || admin1 != null) {
|
||||||
|
map['admin1'] = Variable<String?>(admin1);
|
||||||
|
}
|
||||||
|
if (!nullToAbsent || admin2 != null) {
|
||||||
|
map['admin2'] = Variable<String?>(admin2);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImageLocationsCompanion toCompanion(bool nullToAbsent) {
|
||||||
|
return ImageLocationsCompanion(
|
||||||
|
accountFile: Value(accountFile),
|
||||||
|
version: Value(version),
|
||||||
|
name: name == null && nullToAbsent ? const Value.absent() : Value(name),
|
||||||
|
latitude: latitude == null && nullToAbsent
|
||||||
|
? const Value.absent()
|
||||||
|
: Value(latitude),
|
||||||
|
longitude: longitude == null && nullToAbsent
|
||||||
|
? const Value.absent()
|
||||||
|
: Value(longitude),
|
||||||
|
countryCode: countryCode == null && nullToAbsent
|
||||||
|
? const Value.absent()
|
||||||
|
: Value(countryCode),
|
||||||
|
admin1:
|
||||||
|
admin1 == null && nullToAbsent ? const Value.absent() : Value(admin1),
|
||||||
|
admin2:
|
||||||
|
admin2 == null && nullToAbsent ? const Value.absent() : Value(admin2),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory ImageLocation.fromJson(Map<String, dynamic> json,
|
||||||
|
{ValueSerializer? serializer}) {
|
||||||
|
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||||
|
return ImageLocation(
|
||||||
|
accountFile: serializer.fromJson<int>(json['accountFile']),
|
||||||
|
version: serializer.fromJson<int>(json['version']),
|
||||||
|
name: serializer.fromJson<String?>(json['name']),
|
||||||
|
latitude: serializer.fromJson<double?>(json['latitude']),
|
||||||
|
longitude: serializer.fromJson<double?>(json['longitude']),
|
||||||
|
countryCode: serializer.fromJson<String?>(json['countryCode']),
|
||||||
|
admin1: serializer.fromJson<String?>(json['admin1']),
|
||||||
|
admin2: serializer.fromJson<String?>(json['admin2']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||||
|
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||||
|
return <String, dynamic>{
|
||||||
|
'accountFile': serializer.toJson<int>(accountFile),
|
||||||
|
'version': serializer.toJson<int>(version),
|
||||||
|
'name': serializer.toJson<String?>(name),
|
||||||
|
'latitude': serializer.toJson<double?>(latitude),
|
||||||
|
'longitude': serializer.toJson<double?>(longitude),
|
||||||
|
'countryCode': serializer.toJson<String?>(countryCode),
|
||||||
|
'admin1': serializer.toJson<String?>(admin1),
|
||||||
|
'admin2': serializer.toJson<String?>(admin2),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
ImageLocation copyWith(
|
||||||
|
{int? accountFile,
|
||||||
|
int? version,
|
||||||
|
Value<String?> name = const Value.absent(),
|
||||||
|
Value<double?> latitude = const Value.absent(),
|
||||||
|
Value<double?> longitude = const Value.absent(),
|
||||||
|
Value<String?> countryCode = const Value.absent(),
|
||||||
|
Value<String?> admin1 = const Value.absent(),
|
||||||
|
Value<String?> admin2 = const Value.absent()}) =>
|
||||||
|
ImageLocation(
|
||||||
|
accountFile: accountFile ?? this.accountFile,
|
||||||
|
version: version ?? this.version,
|
||||||
|
name: name.present ? name.value : this.name,
|
||||||
|
latitude: latitude.present ? latitude.value : this.latitude,
|
||||||
|
longitude: longitude.present ? longitude.value : this.longitude,
|
||||||
|
countryCode: countryCode.present ? countryCode.value : this.countryCode,
|
||||||
|
admin1: admin1.present ? admin1.value : this.admin1,
|
||||||
|
admin2: admin2.present ? admin2.value : this.admin2,
|
||||||
|
);
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return (StringBuffer('ImageLocation(')
|
||||||
|
..write('accountFile: $accountFile, ')
|
||||||
|
..write('version: $version, ')
|
||||||
|
..write('name: $name, ')
|
||||||
|
..write('latitude: $latitude, ')
|
||||||
|
..write('longitude: $longitude, ')
|
||||||
|
..write('countryCode: $countryCode, ')
|
||||||
|
..write('admin1: $admin1, ')
|
||||||
|
..write('admin2: $admin2')
|
||||||
|
..write(')'))
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(accountFile, version, name, latitude,
|
||||||
|
longitude, countryCode, admin1, admin2);
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
(other is ImageLocation &&
|
||||||
|
other.accountFile == this.accountFile &&
|
||||||
|
other.version == this.version &&
|
||||||
|
other.name == this.name &&
|
||||||
|
other.latitude == this.latitude &&
|
||||||
|
other.longitude == this.longitude &&
|
||||||
|
other.countryCode == this.countryCode &&
|
||||||
|
other.admin1 == this.admin1 &&
|
||||||
|
other.admin2 == this.admin2);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ImageLocationsCompanion extends UpdateCompanion<ImageLocation> {
|
||||||
|
final Value<int> accountFile;
|
||||||
|
final Value<int> version;
|
||||||
|
final Value<String?> name;
|
||||||
|
final Value<double?> latitude;
|
||||||
|
final Value<double?> longitude;
|
||||||
|
final Value<String?> countryCode;
|
||||||
|
final Value<String?> admin1;
|
||||||
|
final Value<String?> admin2;
|
||||||
|
const ImageLocationsCompanion({
|
||||||
|
this.accountFile = const Value.absent(),
|
||||||
|
this.version = const Value.absent(),
|
||||||
|
this.name = const Value.absent(),
|
||||||
|
this.latitude = const Value.absent(),
|
||||||
|
this.longitude = const Value.absent(),
|
||||||
|
this.countryCode = const Value.absent(),
|
||||||
|
this.admin1 = const Value.absent(),
|
||||||
|
this.admin2 = const Value.absent(),
|
||||||
|
});
|
||||||
|
ImageLocationsCompanion.insert({
|
||||||
|
this.accountFile = const Value.absent(),
|
||||||
|
required int version,
|
||||||
|
this.name = const Value.absent(),
|
||||||
|
this.latitude = const Value.absent(),
|
||||||
|
this.longitude = const Value.absent(),
|
||||||
|
this.countryCode = const Value.absent(),
|
||||||
|
this.admin1 = const Value.absent(),
|
||||||
|
this.admin2 = const Value.absent(),
|
||||||
|
}) : version = Value(version);
|
||||||
|
static Insertable<ImageLocation> custom({
|
||||||
|
Expression<int>? accountFile,
|
||||||
|
Expression<int>? version,
|
||||||
|
Expression<String?>? name,
|
||||||
|
Expression<double?>? latitude,
|
||||||
|
Expression<double?>? longitude,
|
||||||
|
Expression<String?>? countryCode,
|
||||||
|
Expression<String?>? admin1,
|
||||||
|
Expression<String?>? admin2,
|
||||||
|
}) {
|
||||||
|
return RawValuesInsertable({
|
||||||
|
if (accountFile != null) 'account_file': accountFile,
|
||||||
|
if (version != null) 'version': version,
|
||||||
|
if (name != null) 'name': name,
|
||||||
|
if (latitude != null) 'latitude': latitude,
|
||||||
|
if (longitude != null) 'longitude': longitude,
|
||||||
|
if (countryCode != null) 'country_code': countryCode,
|
||||||
|
if (admin1 != null) 'admin1': admin1,
|
||||||
|
if (admin2 != null) 'admin2': admin2,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ImageLocationsCompanion copyWith(
|
||||||
|
{Value<int>? accountFile,
|
||||||
|
Value<int>? version,
|
||||||
|
Value<String?>? name,
|
||||||
|
Value<double?>? latitude,
|
||||||
|
Value<double?>? longitude,
|
||||||
|
Value<String?>? countryCode,
|
||||||
|
Value<String?>? admin1,
|
||||||
|
Value<String?>? admin2}) {
|
||||||
|
return ImageLocationsCompanion(
|
||||||
|
accountFile: accountFile ?? this.accountFile,
|
||||||
|
version: version ?? this.version,
|
||||||
|
name: name ?? this.name,
|
||||||
|
latitude: latitude ?? this.latitude,
|
||||||
|
longitude: longitude ?? this.longitude,
|
||||||
|
countryCode: countryCode ?? this.countryCode,
|
||||||
|
admin1: admin1 ?? this.admin1,
|
||||||
|
admin2: admin2 ?? this.admin2,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||||
|
final map = <String, Expression>{};
|
||||||
|
if (accountFile.present) {
|
||||||
|
map['account_file'] = Variable<int>(accountFile.value);
|
||||||
|
}
|
||||||
|
if (version.present) {
|
||||||
|
map['version'] = Variable<int>(version.value);
|
||||||
|
}
|
||||||
|
if (name.present) {
|
||||||
|
map['name'] = Variable<String?>(name.value);
|
||||||
|
}
|
||||||
|
if (latitude.present) {
|
||||||
|
map['latitude'] = Variable<double?>(latitude.value);
|
||||||
|
}
|
||||||
|
if (longitude.present) {
|
||||||
|
map['longitude'] = Variable<double?>(longitude.value);
|
||||||
|
}
|
||||||
|
if (countryCode.present) {
|
||||||
|
map['country_code'] = Variable<String?>(countryCode.value);
|
||||||
|
}
|
||||||
|
if (admin1.present) {
|
||||||
|
map['admin1'] = Variable<String?>(admin1.value);
|
||||||
|
}
|
||||||
|
if (admin2.present) {
|
||||||
|
map['admin2'] = Variable<String?>(admin2.value);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return (StringBuffer('ImageLocationsCompanion(')
|
||||||
|
..write('accountFile: $accountFile, ')
|
||||||
|
..write('version: $version, ')
|
||||||
|
..write('name: $name, ')
|
||||||
|
..write('latitude: $latitude, ')
|
||||||
|
..write('longitude: $longitude, ')
|
||||||
|
..write('countryCode: $countryCode, ')
|
||||||
|
..write('admin1: $admin1, ')
|
||||||
|
..write('admin2: $admin2')
|
||||||
|
..write(')'))
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class $ImageLocationsTable extends ImageLocations
|
||||||
|
with TableInfo<$ImageLocationsTable, ImageLocation> {
|
||||||
|
@override
|
||||||
|
final GeneratedDatabase attachedDatabase;
|
||||||
|
final String? _alias;
|
||||||
|
$ImageLocationsTable(this.attachedDatabase, [this._alias]);
|
||||||
|
final VerificationMeta _accountFileMeta =
|
||||||
|
const VerificationMeta('accountFile');
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<int?> accountFile = GeneratedColumn<int?>(
|
||||||
|
'account_file', aliasedName, false,
|
||||||
|
type: const IntType(),
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
defaultConstraints:
|
||||||
|
'REFERENCES account_files (row_id) ON DELETE CASCADE');
|
||||||
|
final VerificationMeta _versionMeta = const VerificationMeta('version');
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<int?> version = GeneratedColumn<int?>(
|
||||||
|
'version', aliasedName, false,
|
||||||
|
type: const IntType(), requiredDuringInsert: true);
|
||||||
|
final VerificationMeta _nameMeta = const VerificationMeta('name');
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<String?> name = GeneratedColumn<String?>(
|
||||||
|
'name', aliasedName, true,
|
||||||
|
type: const StringType(), requiredDuringInsert: false);
|
||||||
|
final VerificationMeta _latitudeMeta = const VerificationMeta('latitude');
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<double?> latitude = GeneratedColumn<double?>(
|
||||||
|
'latitude', aliasedName, true,
|
||||||
|
type: const RealType(), requiredDuringInsert: false);
|
||||||
|
final VerificationMeta _longitudeMeta = const VerificationMeta('longitude');
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<double?> longitude = GeneratedColumn<double?>(
|
||||||
|
'longitude', aliasedName, true,
|
||||||
|
type: const RealType(), requiredDuringInsert: false);
|
||||||
|
final VerificationMeta _countryCodeMeta =
|
||||||
|
const VerificationMeta('countryCode');
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<String?> countryCode = GeneratedColumn<String?>(
|
||||||
|
'country_code', aliasedName, true,
|
||||||
|
type: const StringType(), requiredDuringInsert: false);
|
||||||
|
final VerificationMeta _admin1Meta = const VerificationMeta('admin1');
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<String?> admin1 = GeneratedColumn<String?>(
|
||||||
|
'admin1', aliasedName, true,
|
||||||
|
type: const StringType(), requiredDuringInsert: false);
|
||||||
|
final VerificationMeta _admin2Meta = const VerificationMeta('admin2');
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<String?> admin2 = GeneratedColumn<String?>(
|
||||||
|
'admin2', aliasedName, true,
|
||||||
|
type: const StringType(), requiredDuringInsert: false);
|
||||||
|
@override
|
||||||
|
List<GeneratedColumn> get $columns => [
|
||||||
|
accountFile,
|
||||||
|
version,
|
||||||
|
name,
|
||||||
|
latitude,
|
||||||
|
longitude,
|
||||||
|
countryCode,
|
||||||
|
admin1,
|
||||||
|
admin2
|
||||||
|
];
|
||||||
|
@override
|
||||||
|
String get aliasedName => _alias ?? 'image_locations';
|
||||||
|
@override
|
||||||
|
String get actualTableName => 'image_locations';
|
||||||
|
@override
|
||||||
|
VerificationContext validateIntegrity(Insertable<ImageLocation> instance,
|
||||||
|
{bool isInserting = false}) {
|
||||||
|
final context = VerificationContext();
|
||||||
|
final data = instance.toColumns(true);
|
||||||
|
if (data.containsKey('account_file')) {
|
||||||
|
context.handle(
|
||||||
|
_accountFileMeta,
|
||||||
|
accountFile.isAcceptableOrUnknown(
|
||||||
|
data['account_file']!, _accountFileMeta));
|
||||||
|
}
|
||||||
|
if (data.containsKey('version')) {
|
||||||
|
context.handle(_versionMeta,
|
||||||
|
version.isAcceptableOrUnknown(data['version']!, _versionMeta));
|
||||||
|
} else if (isInserting) {
|
||||||
|
context.missing(_versionMeta);
|
||||||
|
}
|
||||||
|
if (data.containsKey('name')) {
|
||||||
|
context.handle(
|
||||||
|
_nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta));
|
||||||
|
}
|
||||||
|
if (data.containsKey('latitude')) {
|
||||||
|
context.handle(_latitudeMeta,
|
||||||
|
latitude.isAcceptableOrUnknown(data['latitude']!, _latitudeMeta));
|
||||||
|
}
|
||||||
|
if (data.containsKey('longitude')) {
|
||||||
|
context.handle(_longitudeMeta,
|
||||||
|
longitude.isAcceptableOrUnknown(data['longitude']!, _longitudeMeta));
|
||||||
|
}
|
||||||
|
if (data.containsKey('country_code')) {
|
||||||
|
context.handle(
|
||||||
|
_countryCodeMeta,
|
||||||
|
countryCode.isAcceptableOrUnknown(
|
||||||
|
data['country_code']!, _countryCodeMeta));
|
||||||
|
}
|
||||||
|
if (data.containsKey('admin1')) {
|
||||||
|
context.handle(_admin1Meta,
|
||||||
|
admin1.isAcceptableOrUnknown(data['admin1']!, _admin1Meta));
|
||||||
|
}
|
||||||
|
if (data.containsKey('admin2')) {
|
||||||
|
context.handle(_admin2Meta,
|
||||||
|
admin2.isAcceptableOrUnknown(data['admin2']!, _admin2Meta));
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<GeneratedColumn> get $primaryKey => {accountFile};
|
||||||
|
@override
|
||||||
|
ImageLocation map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||||
|
return ImageLocation.fromData(data,
|
||||||
|
prefix: tablePrefix != null ? '$tablePrefix.' : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
$ImageLocationsTable createAlias(String alias) {
|
||||||
|
return $ImageLocationsTable(attachedDatabase, alias);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class Trash extends DataClass implements Insertable<Trash> {
|
class Trash extends DataClass implements Insertable<Trash> {
|
||||||
final int file;
|
final int file;
|
||||||
final String filename;
|
final String filename;
|
||||||
|
@ -3717,6 +4127,7 @@ abstract class _$SqliteDb extends GeneratedDatabase {
|
||||||
late final $FilesTable files = $FilesTable(this);
|
late final $FilesTable files = $FilesTable(this);
|
||||||
late final $AccountFilesTable accountFiles = $AccountFilesTable(this);
|
late final $AccountFilesTable accountFiles = $AccountFilesTable(this);
|
||||||
late final $ImagesTable images = $ImagesTable(this);
|
late final $ImagesTable images = $ImagesTable(this);
|
||||||
|
late final $ImageLocationsTable imageLocations = $ImageLocationsTable(this);
|
||||||
late final $TrashesTable trashes = $TrashesTable(this);
|
late final $TrashesTable trashes = $TrashesTable(this);
|
||||||
late final $DirFilesTable dirFiles = $DirFilesTable(this);
|
late final $DirFilesTable dirFiles = $DirFilesTable(this);
|
||||||
late final $AlbumsTable albums = $AlbumsTable(this);
|
late final $AlbumsTable albums = $AlbumsTable(this);
|
||||||
|
@ -3732,6 +4143,7 @@ abstract class _$SqliteDb extends GeneratedDatabase {
|
||||||
files,
|
files,
|
||||||
accountFiles,
|
accountFiles,
|
||||||
images,
|
images,
|
||||||
|
imageLocations,
|
||||||
trashes,
|
trashes,
|
||||||
dirFiles,
|
dirFiles,
|
||||||
albums,
|
albums,
|
||||||
|
|
|
@ -115,6 +115,15 @@ class SqliteFileConverter {
|
||||||
imageHeight: obj.height,
|
imageHeight: obj.height,
|
||||||
exif: obj.exifRaw?.run((e) => Exif.fromJson(jsonDecode(e))),
|
exif: obj.exifRaw?.run((e) => Exif.fromJson(jsonDecode(e))),
|
||||||
));
|
));
|
||||||
|
final location = f.imageLocation?.run((obj) => ImageLocation(
|
||||||
|
version: obj.version,
|
||||||
|
name: obj.name,
|
||||||
|
latitude: obj.latitude,
|
||||||
|
longitude: obj.longitude,
|
||||||
|
countryCode: obj.countryCode,
|
||||||
|
admin1: obj.admin1,
|
||||||
|
admin2: obj.admin2,
|
||||||
|
));
|
||||||
return File(
|
return File(
|
||||||
path: "remote.php/dav/files/$userId/${f.accountFile.relativePath}",
|
path: "remote.php/dav/files/$userId/${f.accountFile.relativePath}",
|
||||||
contentLength: f.file.contentLength,
|
contentLength: f.file.contentLength,
|
||||||
|
@ -134,6 +143,7 @@ class SqliteFileConverter {
|
||||||
metadata: metadata,
|
metadata: metadata,
|
||||||
isArchived: f.accountFile.isArchived,
|
isArchived: f.accountFile.isArchived,
|
||||||
overrideDateTime: f.accountFile.overrideDateTime,
|
overrideDateTime: f.accountFile.overrideDateTime,
|
||||||
|
location: location,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -167,6 +177,16 @@ class SqliteFileConverter {
|
||||||
exifRaw: Value(m.exif?.toJson().run((j) => jsonEncode(j))),
|
exifRaw: Value(m.exif?.toJson().run((j) => jsonEncode(j))),
|
||||||
dateTimeOriginal: Value(m.exif?.dateTimeOriginal),
|
dateTimeOriginal: Value(m.exif?.dateTimeOriginal),
|
||||||
));
|
));
|
||||||
|
final dbImageLocation =
|
||||||
|
file.location?.run((l) => sql.ImageLocationsCompanion.insert(
|
||||||
|
version: l.version,
|
||||||
|
name: Value(l.name),
|
||||||
|
latitude: Value(l.latitude),
|
||||||
|
longitude: Value(l.longitude),
|
||||||
|
countryCode: Value(l.countryCode),
|
||||||
|
admin1: Value(l.admin1),
|
||||||
|
admin2: Value(l.admin2),
|
||||||
|
));
|
||||||
final dbTrash = file.trashbinDeletionTime == null
|
final dbTrash = file.trashbinDeletionTime == null
|
||||||
? null
|
? null
|
||||||
: sql.TrashesCompanion.insert(
|
: sql.TrashesCompanion.insert(
|
||||||
|
@ -174,7 +194,8 @@ class SqliteFileConverter {
|
||||||
originalLocation: file.trashbinOriginalLocation!,
|
originalLocation: file.trashbinOriginalLocation!,
|
||||||
deletionTime: file.trashbinDeletionTime!,
|
deletionTime: file.trashbinDeletionTime!,
|
||||||
);
|
);
|
||||||
return sql.CompleteFileCompanion(dbFile, dbAccountFile, dbImage, dbTrash);
|
return sql.CompleteFileCompanion(
|
||||||
|
dbFile, dbAccountFile, dbImage, dbImageLocation, dbTrash);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,21 +16,24 @@ import 'package:nc_photos/platform/k.dart' as platform_k;
|
||||||
const maxByFileIdsSize = 30000;
|
const maxByFileIdsSize = 30000;
|
||||||
|
|
||||||
class CompleteFile {
|
class CompleteFile {
|
||||||
const CompleteFile(this.file, this.accountFile, this.image, this.trash);
|
const CompleteFile(
|
||||||
|
this.file, this.accountFile, this.image, this.imageLocation, this.trash);
|
||||||
|
|
||||||
final File file;
|
final File file;
|
||||||
final AccountFile accountFile;
|
final AccountFile accountFile;
|
||||||
final Image? image;
|
final Image? image;
|
||||||
|
final ImageLocation? imageLocation;
|
||||||
final Trash? trash;
|
final Trash? trash;
|
||||||
}
|
}
|
||||||
|
|
||||||
class CompleteFileCompanion {
|
class CompleteFileCompanion {
|
||||||
const CompleteFileCompanion(
|
const CompleteFileCompanion(
|
||||||
this.file, this.accountFile, this.image, this.trash);
|
this.file, this.accountFile, this.image, this.imageLocation, this.trash);
|
||||||
|
|
||||||
final FilesCompanion file;
|
final FilesCompanion file;
|
||||||
final AccountFilesCompanion accountFile;
|
final AccountFilesCompanion accountFile;
|
||||||
final ImagesCompanion? image;
|
final ImagesCompanion? image;
|
||||||
|
final ImageLocationsCompanion? imageLocation;
|
||||||
final TrashesCompanion? trash;
|
final TrashesCompanion? trash;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -347,6 +350,7 @@ extension SqliteDbExtension on SqliteDb {
|
||||||
r.readTable(files),
|
r.readTable(files),
|
||||||
r.readTable(accountFiles),
|
r.readTable(accountFiles),
|
||||||
r.readTableOrNull(images),
|
r.readTableOrNull(images),
|
||||||
|
r.readTableOrNull(imageLocations),
|
||||||
r.readTableOrNull(trashes),
|
r.readTableOrNull(trashes),
|
||||||
))
|
))
|
||||||
.get();
|
.get();
|
||||||
|
@ -374,6 +378,7 @@ extension SqliteDbExtension on SqliteDb {
|
||||||
r.readTable(files),
|
r.readTable(files),
|
||||||
r.readTable(accountFiles),
|
r.readTable(accountFiles),
|
||||||
r.readTableOrNull(images),
|
r.readTableOrNull(images),
|
||||||
|
r.readTableOrNull(imageLocations),
|
||||||
r.readTableOrNull(trashes),
|
r.readTableOrNull(trashes),
|
||||||
))
|
))
|
||||||
.get();
|
.get();
|
||||||
|
@ -400,6 +405,7 @@ extension SqliteDbExtension on SqliteDb {
|
||||||
r.readTable(files),
|
r.readTable(files),
|
||||||
r.readTable(accountFiles),
|
r.readTable(accountFiles),
|
||||||
r.readTableOrNull(images),
|
r.readTableOrNull(images),
|
||||||
|
r.readTableOrNull(imageLocations),
|
||||||
r.readTableOrNull(trashes),
|
r.readTableOrNull(trashes),
|
||||||
))
|
))
|
||||||
.get();
|
.get();
|
||||||
|
@ -454,6 +460,7 @@ extension SqliteDbExtension on SqliteDb {
|
||||||
await delete(accounts).go();
|
await delete(accounts).go();
|
||||||
await delete(files).go();
|
await delete(files).go();
|
||||||
await delete(images).go();
|
await delete(images).go();
|
||||||
|
await delete(imageLocations).go();
|
||||||
await delete(trashes).go();
|
await delete(trashes).go();
|
||||||
await delete(accountFiles).go();
|
await delete(accountFiles).go();
|
||||||
await delete(dirFiles).go();
|
await delete(dirFiles).go();
|
||||||
|
@ -571,6 +578,8 @@ class FilesQueryBuilder {
|
||||||
if (_queryMode == FilesQueryMode.completeFile) ...[
|
if (_queryMode == FilesQueryMode.completeFile) ...[
|
||||||
leftOuterJoin(
|
leftOuterJoin(
|
||||||
db.images, db.images.accountFile.equalsExp(db.accountFiles.rowId)),
|
db.images, db.images.accountFile.equalsExp(db.accountFiles.rowId)),
|
||||||
|
leftOuterJoin(db.imageLocations,
|
||||||
|
db.imageLocations.accountFile.equalsExp(db.accountFiles.rowId)),
|
||||||
leftOuterJoin(db.trashes, db.trashes.file.equalsExp(db.files.rowId)),
|
leftOuterJoin(db.trashes, db.trashes.file.equalsExp(db.files.rowId)),
|
||||||
],
|
],
|
||||||
]) as JoinedSelectStatement;
|
]) as JoinedSelectStatement;
|
||||||
|
|
|
@ -101,6 +101,7 @@ class WebdavResponseParser {
|
||||||
String? trashbinFilename;
|
String? trashbinFilename;
|
||||||
String? trashbinOriginalLocation;
|
String? trashbinOriginalLocation;
|
||||||
DateTime? trashbinDeletionTime;
|
DateTime? trashbinDeletionTime;
|
||||||
|
ImageLocation? location;
|
||||||
|
|
||||||
for (final child in element.children.whereType<XmlElement>()) {
|
for (final child in element.children.whereType<XmlElement>()) {
|
||||||
if (child.matchQualifiedName("href",
|
if (child.matchQualifiedName("href",
|
||||||
|
@ -139,6 +140,7 @@ class WebdavResponseParser {
|
||||||
trashbinFilename = propParser.trashbinFilename;
|
trashbinFilename = propParser.trashbinFilename;
|
||||||
trashbinOriginalLocation = propParser.trashbinOriginalLocation;
|
trashbinOriginalLocation = propParser.trashbinOriginalLocation;
|
||||||
trashbinDeletionTime = propParser.trashbinDeletionTime;
|
trashbinDeletionTime = propParser.trashbinDeletionTime;
|
||||||
|
location = propParser.location;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -161,6 +163,7 @@ class WebdavResponseParser {
|
||||||
trashbinFilename: trashbinFilename,
|
trashbinFilename: trashbinFilename,
|
||||||
trashbinOriginalLocation: trashbinOriginalLocation,
|
trashbinOriginalLocation: trashbinOriginalLocation,
|
||||||
trashbinDeletionTime: trashbinDeletionTime,
|
trashbinDeletionTime: trashbinDeletionTime,
|
||||||
|
location: location,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -357,6 +360,9 @@ class _FilePropParser {
|
||||||
} else if (child.matchQualifiedName("override-date-time",
|
} else if (child.matchQualifiedName("override-date-time",
|
||||||
prefix: "com.nkming.nc_photos", namespaces: namespaces)) {
|
prefix: "com.nkming.nc_photos", namespaces: namespaces)) {
|
||||||
_overrideDateTime = DateTime.parse(child.innerText);
|
_overrideDateTime = DateTime.parse(child.innerText);
|
||||||
|
} else if (child.matchQualifiedName("location",
|
||||||
|
prefix: "com.nkming.nc_photos", namespaces: namespaces)) {
|
||||||
|
_location = ImageLocation.fromJson(jsonDecode(child.innerText));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 2nd pass that depends on data in 1st pass
|
// 2nd pass that depends on data in 1st pass
|
||||||
|
@ -395,6 +401,7 @@ class _FilePropParser {
|
||||||
String? get trashbinFilename => _trashbinFilename;
|
String? get trashbinFilename => _trashbinFilename;
|
||||||
String? get trashbinOriginalLocation => _trashbinOriginalLocation;
|
String? get trashbinOriginalLocation => _trashbinOriginalLocation;
|
||||||
DateTime? get trashbinDeletionTime => _trashbinDeletionTime;
|
DateTime? get trashbinDeletionTime => _trashbinDeletionTime;
|
||||||
|
ImageLocation? get location => _location;
|
||||||
|
|
||||||
final Map<String, String> namespaces;
|
final Map<String, String> namespaces;
|
||||||
|
|
||||||
|
@ -418,6 +425,7 @@ class _FilePropParser {
|
||||||
String? _trashbinFilename;
|
String? _trashbinFilename;
|
||||||
String? _trashbinOriginalLocation;
|
String? _trashbinOriginalLocation;
|
||||||
DateTime? _trashbinDeletionTime;
|
DateTime? _trashbinDeletionTime;
|
||||||
|
ImageLocation? _location;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _FileIdPropParser {
|
class _FileIdPropParser {
|
||||||
|
|
|
@ -71,6 +71,7 @@ class FilePropertyUpdatedEvent {
|
||||||
static const propIsArchived = 0x02;
|
static const propIsArchived = 0x02;
|
||||||
static const propOverrideDateTime = 0x04;
|
static const propOverrideDateTime = 0x04;
|
||||||
static const propFavorite = 0x08;
|
static const propFavorite = 0x08;
|
||||||
|
static const propImageLocation = 0x10;
|
||||||
}
|
}
|
||||||
|
|
||||||
class FileRemovedEvent {
|
class FileRemovedEvent {
|
||||||
|
|
|
@ -1374,6 +1374,25 @@
|
||||||
"@showAllButtonLabel": {
|
"@showAllButtonLabel": {
|
||||||
"description": "A button to show all items of a certain item group (e.g., show all recognized faces)"
|
"description": "A button to show all items of a certain item group (e.g., show all recognized faces)"
|
||||||
},
|
},
|
||||||
|
"gpsPlaceText": "Near {place}",
|
||||||
|
"@gpsPlaceText": {
|
||||||
|
"description": "The estimated place where a photo was taken at. The place could be a town, a city, an administrative region, or a country.",
|
||||||
|
"placeholders": {
|
||||||
|
"place": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gpsPlaceAboutDialogTitle": "About place",
|
||||||
|
"@gpsPlaceAboutDialogTitle": {
|
||||||
|
"description": "Warn about the inaccurate nature of our offline reverse geocoding feature (i.e., converting coordinates into addresses)"
|
||||||
|
},
|
||||||
|
"gpsPlaceAboutDialogContent": "The place shown here is only a rough estimation and not guaranteed to be accurate. It does not represent our views on any disputed areas.",
|
||||||
|
"@gpsPlaceAboutDialogContent": {
|
||||||
|
"description": "Warn about the inaccurate nature of our offline reverse geocoding feature (i.e., converting coordinates into addresses)"
|
||||||
|
},
|
||||||
|
"collectionPlacesLabel": "Places",
|
||||||
|
"@collectionPlacesLabel": {
|
||||||
|
"description": "Browse photos grouped by place"
|
||||||
|
},
|
||||||
|
|
||||||
"errorUnauthenticated": "Unauthenticated access. Please sign-in again if the problem continues",
|
"errorUnauthenticated": "Unauthenticated access. Please sign-in again if the problem continues",
|
||||||
"@errorUnauthenticated": {
|
"@errorUnauthenticated": {
|
||||||
|
|
|
@ -143,6 +143,10 @@
|
||||||
"searchFilterBubbleFavoriteTrueText",
|
"searchFilterBubbleFavoriteTrueText",
|
||||||
"searchFilterBubbleFavoriteFalseText",
|
"searchFilterBubbleFavoriteFalseText",
|
||||||
"showAllButtonLabel",
|
"showAllButtonLabel",
|
||||||
|
"gpsPlaceText",
|
||||||
|
"gpsPlaceAboutDialogTitle",
|
||||||
|
"gpsPlaceAboutDialogContent",
|
||||||
|
"collectionPlacesLabel",
|
||||||
"errorAlbumDowngrade"
|
"errorAlbumDowngrade"
|
||||||
],
|
],
|
||||||
|
|
||||||
|
@ -304,6 +308,10 @@
|
||||||
"searchFilterBubbleFavoriteTrueText",
|
"searchFilterBubbleFavoriteTrueText",
|
||||||
"searchFilterBubbleFavoriteFalseText",
|
"searchFilterBubbleFavoriteFalseText",
|
||||||
"showAllButtonLabel",
|
"showAllButtonLabel",
|
||||||
|
"gpsPlaceText",
|
||||||
|
"gpsPlaceAboutDialogTitle",
|
||||||
|
"gpsPlaceAboutDialogContent",
|
||||||
|
"collectionPlacesLabel",
|
||||||
"errorAlbumDowngrade"
|
"errorAlbumDowngrade"
|
||||||
],
|
],
|
||||||
|
|
||||||
|
@ -345,13 +353,28 @@
|
||||||
"searchFilterFavoriteLabel",
|
"searchFilterFavoriteLabel",
|
||||||
"searchFilterBubbleFavoriteTrueText",
|
"searchFilterBubbleFavoriteTrueText",
|
||||||
"searchFilterBubbleFavoriteFalseText",
|
"searchFilterBubbleFavoriteFalseText",
|
||||||
"showAllButtonLabel"
|
"showAllButtonLabel",
|
||||||
|
"gpsPlaceText",
|
||||||
|
"gpsPlaceAboutDialogTitle",
|
||||||
|
"gpsPlaceAboutDialogContent",
|
||||||
|
"collectionPlacesLabel"
|
||||||
],
|
],
|
||||||
|
|
||||||
"es": [
|
"es": [
|
||||||
"settingsLanguageOptionSystemDefaultLabel",
|
"settingsLanguageOptionSystemDefaultLabel",
|
||||||
"rootPickerSkipConfirmationDialogContent2",
|
"rootPickerSkipConfirmationDialogContent2",
|
||||||
"showAllButtonLabel"
|
"showAllButtonLabel",
|
||||||
|
"gpsPlaceText",
|
||||||
|
"gpsPlaceAboutDialogTitle",
|
||||||
|
"gpsPlaceAboutDialogContent",
|
||||||
|
"collectionPlacesLabel"
|
||||||
|
],
|
||||||
|
|
||||||
|
"fi": [
|
||||||
|
"gpsPlaceText",
|
||||||
|
"gpsPlaceAboutDialogTitle",
|
||||||
|
"gpsPlaceAboutDialogContent",
|
||||||
|
"collectionPlacesLabel"
|
||||||
],
|
],
|
||||||
|
|
||||||
"fr": [
|
"fr": [
|
||||||
|
@ -417,7 +440,11 @@
|
||||||
"searchFilterFavoriteLabel",
|
"searchFilterFavoriteLabel",
|
||||||
"searchFilterBubbleFavoriteTrueText",
|
"searchFilterBubbleFavoriteTrueText",
|
||||||
"searchFilterBubbleFavoriteFalseText",
|
"searchFilterBubbleFavoriteFalseText",
|
||||||
"showAllButtonLabel"
|
"showAllButtonLabel",
|
||||||
|
"gpsPlaceText",
|
||||||
|
"gpsPlaceAboutDialogTitle",
|
||||||
|
"gpsPlaceAboutDialogContent",
|
||||||
|
"collectionPlacesLabel"
|
||||||
],
|
],
|
||||||
|
|
||||||
"pl": [
|
"pl": [
|
||||||
|
@ -500,7 +527,11 @@
|
||||||
"searchFilterFavoriteLabel",
|
"searchFilterFavoriteLabel",
|
||||||
"searchFilterBubbleFavoriteTrueText",
|
"searchFilterBubbleFavoriteTrueText",
|
||||||
"searchFilterBubbleFavoriteFalseText",
|
"searchFilterBubbleFavoriteFalseText",
|
||||||
"showAllButtonLabel"
|
"showAllButtonLabel",
|
||||||
|
"gpsPlaceText",
|
||||||
|
"gpsPlaceAboutDialogTitle",
|
||||||
|
"gpsPlaceAboutDialogContent",
|
||||||
|
"collectionPlacesLabel"
|
||||||
],
|
],
|
||||||
|
|
||||||
"pt": [
|
"pt": [
|
||||||
|
@ -562,7 +593,11 @@
|
||||||
"searchFilterFavoriteLabel",
|
"searchFilterFavoriteLabel",
|
||||||
"searchFilterBubbleFavoriteTrueText",
|
"searchFilterBubbleFavoriteTrueText",
|
||||||
"searchFilterBubbleFavoriteFalseText",
|
"searchFilterBubbleFavoriteFalseText",
|
||||||
"showAllButtonLabel"
|
"showAllButtonLabel",
|
||||||
|
"gpsPlaceText",
|
||||||
|
"gpsPlaceAboutDialogTitle",
|
||||||
|
"gpsPlaceAboutDialogContent",
|
||||||
|
"collectionPlacesLabel"
|
||||||
],
|
],
|
||||||
|
|
||||||
"ru": [
|
"ru": [
|
||||||
|
@ -624,7 +659,11 @@
|
||||||
"searchFilterFavoriteLabel",
|
"searchFilterFavoriteLabel",
|
||||||
"searchFilterBubbleFavoriteTrueText",
|
"searchFilterBubbleFavoriteTrueText",
|
||||||
"searchFilterBubbleFavoriteFalseText",
|
"searchFilterBubbleFavoriteFalseText",
|
||||||
"showAllButtonLabel"
|
"showAllButtonLabel",
|
||||||
|
"gpsPlaceText",
|
||||||
|
"gpsPlaceAboutDialogTitle",
|
||||||
|
"gpsPlaceAboutDialogContent",
|
||||||
|
"collectionPlacesLabel"
|
||||||
],
|
],
|
||||||
|
|
||||||
"zh": [
|
"zh": [
|
||||||
|
@ -686,7 +725,11 @@
|
||||||
"searchFilterFavoriteLabel",
|
"searchFilterFavoriteLabel",
|
||||||
"searchFilterBubbleFavoriteTrueText",
|
"searchFilterBubbleFavoriteTrueText",
|
||||||
"searchFilterBubbleFavoriteFalseText",
|
"searchFilterBubbleFavoriteFalseText",
|
||||||
"showAllButtonLabel"
|
"showAllButtonLabel",
|
||||||
|
"gpsPlaceText",
|
||||||
|
"gpsPlaceAboutDialogTitle",
|
||||||
|
"gpsPlaceAboutDialogContent",
|
||||||
|
"collectionPlacesLabel"
|
||||||
],
|
],
|
||||||
|
|
||||||
"zh_Hant": [
|
"zh_Hant": [
|
||||||
|
@ -748,6 +791,10 @@
|
||||||
"searchFilterFavoriteLabel",
|
"searchFilterFavoriteLabel",
|
||||||
"searchFilterBubbleFavoriteTrueText",
|
"searchFilterBubbleFavoriteTrueText",
|
||||||
"searchFilterBubbleFavoriteFalseText",
|
"searchFilterBubbleFavoriteFalseText",
|
||||||
"showAllButtonLabel"
|
"showAllButtonLabel",
|
||||||
|
"gpsPlaceText",
|
||||||
|
"gpsPlaceAboutDialogTitle",
|
||||||
|
"gpsPlaceAboutDialogContent",
|
||||||
|
"collectionPlacesLabel"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
254
app/lib/location_util.dart
Normal file
254
app/lib/location_util.dart
Normal file
|
@ -0,0 +1,254 @@
|
||||||
|
/// Convert a ISO 3166-1 alpha-2 code into country name
|
||||||
|
String? alpha2CodeToName(String cc) => _ccMap[cc];
|
||||||
|
|
||||||
|
const _ccMap = {
|
||||||
|
"AD": "Andorra",
|
||||||
|
"AE": "United Arab Emirates",
|
||||||
|
"AF": "Afghanistan",
|
||||||
|
"AG": "Antigua and Barbuda",
|
||||||
|
"AI": "Anguilla",
|
||||||
|
"AL": "Albania",
|
||||||
|
"AM": "Armenia",
|
||||||
|
"AO": "Angola",
|
||||||
|
"AQ": "Antarctica",
|
||||||
|
"AR": "Argentina",
|
||||||
|
"AS": "American Samoa",
|
||||||
|
"AT": "Austria",
|
||||||
|
"AU": "Australia",
|
||||||
|
"AW": "Aruba",
|
||||||
|
"AX": "Åland Islands",
|
||||||
|
"AZ": "Azerbaijan",
|
||||||
|
"BA": "Bosnia and Herzegovina",
|
||||||
|
"BB": "Barbados",
|
||||||
|
"BD": "Bangladesh",
|
||||||
|
"BE": "Belgium",
|
||||||
|
"BF": "Burkina Faso",
|
||||||
|
"BG": "Bulgaria",
|
||||||
|
"BH": "Bahrain",
|
||||||
|
"BI": "Burundi",
|
||||||
|
"BJ": "Benin",
|
||||||
|
"BL": "Saint Barthélemy",
|
||||||
|
"BM": "Bermuda",
|
||||||
|
"BN": "Brunei Darussalam",
|
||||||
|
"BO": "Bolivia",
|
||||||
|
"BQ": "Bonaire, Sint Eustatius and Saba",
|
||||||
|
"BR": "Brazil",
|
||||||
|
"BS": "Bahamas",
|
||||||
|
"BT": "Bhutan",
|
||||||
|
"BV": "Bouvet Island",
|
||||||
|
"BW": "Botswana",
|
||||||
|
"BY": "Belarus",
|
||||||
|
"BZ": "Belize",
|
||||||
|
"CA": "Canada",
|
||||||
|
"CC": "Cocos (Keeling) Islands",
|
||||||
|
"CD": "DR Congo",
|
||||||
|
"CF": "Central African Republic",
|
||||||
|
"CG": "Congo",
|
||||||
|
"CH": "Switzerland",
|
||||||
|
"CI": "Ivory Coast",
|
||||||
|
"CK": "Cook Islands",
|
||||||
|
"CL": "Chile",
|
||||||
|
"CM": "Cameroon",
|
||||||
|
"CN": "China",
|
||||||
|
"CO": "Colombia",
|
||||||
|
"CR": "Costa Rica",
|
||||||
|
"CU": "Cuba",
|
||||||
|
"CV": "Cabo Verde",
|
||||||
|
"CW": "Curaçao",
|
||||||
|
"CX": "Christmas Island",
|
||||||
|
"CY": "Cyprus",
|
||||||
|
"CZ": "Czechia",
|
||||||
|
"DE": "Germany",
|
||||||
|
"DJ": "Djibouti",
|
||||||
|
"DK": "Denmark",
|
||||||
|
"DM": "Dominica",
|
||||||
|
"DO": "Dominican Republic",
|
||||||
|
"DZ": "Algeria",
|
||||||
|
"EC": "Ecuador",
|
||||||
|
"EE": "Estonia",
|
||||||
|
"EG": "Egypt",
|
||||||
|
"EH": "Western Sahara",
|
||||||
|
"ER": "Eritrea",
|
||||||
|
"ES": "Spain",
|
||||||
|
"ET": "Ethiopia",
|
||||||
|
"FI": "Finland",
|
||||||
|
"FJ": "Fiji",
|
||||||
|
"FK": "Falkland Islands (Malvinas)",
|
||||||
|
"FM": "Micronesia",
|
||||||
|
"FO": "Faroe Islands",
|
||||||
|
"FR": "France",
|
||||||
|
"GA": "Gabon",
|
||||||
|
"GB": "United Kingdom",
|
||||||
|
"GD": "Grenada",
|
||||||
|
"GE": "Georgia",
|
||||||
|
"GF": "French Guiana",
|
||||||
|
"GG": "Guernsey",
|
||||||
|
"GH": "Ghana",
|
||||||
|
"GI": "Gibraltar",
|
||||||
|
"GL": "Greenland",
|
||||||
|
"GM": "Gambia",
|
||||||
|
"GN": "Guinea",
|
||||||
|
"GP": "Guadeloupe",
|
||||||
|
"GQ": "Equatorial Guinea",
|
||||||
|
"GR": "Greece",
|
||||||
|
"GS": "South Georgia and the South Sandwich Islands",
|
||||||
|
"GT": "Guatemala",
|
||||||
|
"GU": "Guam",
|
||||||
|
"GW": "Guinea-Bissau",
|
||||||
|
"GY": "Guyana",
|
||||||
|
"HK": "Hong Kong",
|
||||||
|
"HM": "Heard Island and McDonald Islands",
|
||||||
|
"HN": "Honduras",
|
||||||
|
"HR": "Croatia",
|
||||||
|
"HT": "Haiti",
|
||||||
|
"HU": "Hungary",
|
||||||
|
"ID": "Indonesia",
|
||||||
|
"IE": "Ireland",
|
||||||
|
"IL": "Israel",
|
||||||
|
"IM": "Isle of Man",
|
||||||
|
"IN": "India",
|
||||||
|
"IO": "British Indian Ocean Territory",
|
||||||
|
"IQ": "Iraq",
|
||||||
|
"IR": "Iran",
|
||||||
|
"IS": "Iceland",
|
||||||
|
"IT": "Italy",
|
||||||
|
"JE": "Jersey",
|
||||||
|
"JM": "Jamaica",
|
||||||
|
"JO": "Jordan",
|
||||||
|
"JP": "Japan",
|
||||||
|
"KE": "Kenya",
|
||||||
|
"KG": "Kyrgyzstan",
|
||||||
|
"KH": "Cambodia",
|
||||||
|
"KI": "Kiribati",
|
||||||
|
"KM": "Comoros",
|
||||||
|
"KN": "Saint Kitts and Nevis",
|
||||||
|
"KP": "North Korea",
|
||||||
|
"KR": "South Korea",
|
||||||
|
"KW": "Kuwait",
|
||||||
|
"KY": "Cayman Islands",
|
||||||
|
"KZ": "Kazakhstan",
|
||||||
|
"LA": "Laos",
|
||||||
|
"LB": "Lebanon",
|
||||||
|
"LC": "Saint Lucia",
|
||||||
|
"LI": "Liechtenstein",
|
||||||
|
"LK": "Sri Lanka",
|
||||||
|
"LR": "Liberia",
|
||||||
|
"LS": "Lesotho",
|
||||||
|
"LT": "Lithuania",
|
||||||
|
"LU": "Luxembourg",
|
||||||
|
"LV": "Latvia",
|
||||||
|
"LY": "Libya",
|
||||||
|
"MA": "Morocco",
|
||||||
|
"MC": "Monaco",
|
||||||
|
"MD": "Moldova",
|
||||||
|
"ME": "Montenegro",
|
||||||
|
"MF": "Saint Martin",
|
||||||
|
"MG": "Madagascar",
|
||||||
|
"MH": "Marshall Islands",
|
||||||
|
"MK": "North Macedonia",
|
||||||
|
"ML": "Mali",
|
||||||
|
"MM": "Myanmar",
|
||||||
|
"MN": "Mongolia",
|
||||||
|
"MO": "Macau",
|
||||||
|
"MP": "Northern Mariana Islands",
|
||||||
|
"MQ": "Martinique",
|
||||||
|
"MR": "Mauritania",
|
||||||
|
"MS": "Montserrat",
|
||||||
|
"MT": "Malta",
|
||||||
|
"MU": "Mauritius",
|
||||||
|
"MV": "Maldives",
|
||||||
|
"MW": "Malawi",
|
||||||
|
"MX": "Mexico",
|
||||||
|
"MY": "Malaysia",
|
||||||
|
"MZ": "Mozambique",
|
||||||
|
"NA": "Namibia",
|
||||||
|
"NC": "New Caledonia",
|
||||||
|
"NE": "Niger",
|
||||||
|
"NF": "Norfolk Island",
|
||||||
|
"NG": "Nigeria",
|
||||||
|
"NI": "Nicaragua",
|
||||||
|
"NL": "Netherlands",
|
||||||
|
"NO": "Norway",
|
||||||
|
"NP": "Nepal",
|
||||||
|
"NR": "Nauru",
|
||||||
|
"NU": "Niue",
|
||||||
|
"NZ": "New Zealand",
|
||||||
|
"OM": "Oman",
|
||||||
|
"PA": "Panama",
|
||||||
|
"PE": "Peru",
|
||||||
|
"PF": "French Polynesia",
|
||||||
|
"PG": "Papua New Guinea",
|
||||||
|
"PH": "Philippines",
|
||||||
|
"PK": "Pakistan",
|
||||||
|
"PL": "Poland",
|
||||||
|
"PM": "Saint Pierre and Miquelon",
|
||||||
|
"PN": "Pitcairn",
|
||||||
|
"PR": "Puerto Rico",
|
||||||
|
"PS": "Palestine",
|
||||||
|
"PT": "Portugal",
|
||||||
|
"PW": "Palau",
|
||||||
|
"PY": "Paraguay",
|
||||||
|
"QA": "Qatar",
|
||||||
|
"RE": "Réunion",
|
||||||
|
"RO": "Romania",
|
||||||
|
"RS": "Serbia",
|
||||||
|
"RU": "Russia",
|
||||||
|
"RW": "Rwanda",
|
||||||
|
"SA": "Saudi Arabia",
|
||||||
|
"SB": "Solomon Islands",
|
||||||
|
"SC": "Seychelles",
|
||||||
|
"SD": "Sudan",
|
||||||
|
"SE": "Sweden",
|
||||||
|
"SG": "Singapore",
|
||||||
|
"SH": "Saint Helena, Ascension and Tristan da Cunha",
|
||||||
|
"SI": "Slovenia",
|
||||||
|
"SJ": "Svalbard and Jan Mayen",
|
||||||
|
"SK": "Slovakia",
|
||||||
|
"SL": "Sierra Leone",
|
||||||
|
"SM": "San Marino",
|
||||||
|
"SN": "Senegal",
|
||||||
|
"SO": "Somalia",
|
||||||
|
"SR": "Suriname",
|
||||||
|
"SS": "South Sudan",
|
||||||
|
"ST": "Sao Tome and Principe",
|
||||||
|
"SV": "El Salvador",
|
||||||
|
"SX": "Sint Maarten",
|
||||||
|
"SY": "Syria",
|
||||||
|
"SZ": "Eswatini",
|
||||||
|
"TC": "Turks and Caicos Islands",
|
||||||
|
"TD": "Chad",
|
||||||
|
"TF": "French Southern Territories",
|
||||||
|
"TG": "Togo",
|
||||||
|
"TH": "Thailand",
|
||||||
|
"TJ": "Tajikistan",
|
||||||
|
"TK": "Tokelau",
|
||||||
|
"TL": "Timor-Leste",
|
||||||
|
"TM": "Turkmenistan",
|
||||||
|
"TN": "Tunisia",
|
||||||
|
"TO": "Tonga",
|
||||||
|
"TR": "Türkiye",
|
||||||
|
"TT": "Trinidad and Tobago",
|
||||||
|
"TV": "Tuvalu",
|
||||||
|
"TW": "Taiwan",
|
||||||
|
"TZ": "Tanzania",
|
||||||
|
"UA": "Ukraine",
|
||||||
|
"UG": "Uganda",
|
||||||
|
"UM": "United States Minor Outlying Islands",
|
||||||
|
"US": "United States",
|
||||||
|
"UY": "Uruguay",
|
||||||
|
"UZ": "Uzbekistan",
|
||||||
|
"VA": "Holy See",
|
||||||
|
"VC": "Saint Vincent and the Grenadines",
|
||||||
|
"VE": "Venezuela",
|
||||||
|
"VG": "Virgin Islands (British)",
|
||||||
|
"VI": "Virgin Islands (U.S.)",
|
||||||
|
"VN": "Viet Nam",
|
||||||
|
"VU": "Vanuatu",
|
||||||
|
"WF": "Wallis and Futuna",
|
||||||
|
"WS": "Samoa",
|
||||||
|
"YE": "Yemen",
|
||||||
|
"YT": "Mayotte",
|
||||||
|
"ZA": "South Africa",
|
||||||
|
"ZM": "Zambia",
|
||||||
|
"ZW": "Zimbabwe",
|
||||||
|
};
|
|
@ -9,6 +9,7 @@ import 'package:nc_photos/entity/file.dart';
|
||||||
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
||||||
import 'package:nc_photos/event/event.dart';
|
import 'package:nc_photos/event/event.dart';
|
||||||
import 'package:nc_photos/pref.dart';
|
import 'package:nc_photos/pref.dart';
|
||||||
|
import 'package:nc_photos/reverse_geocoder.dart';
|
||||||
import 'package:nc_photos/use_case/update_missing_metadata.dart';
|
import 'package:nc_photos/use_case/update_missing_metadata.dart';
|
||||||
|
|
||||||
/// Task to update metadata for missing files
|
/// Task to update metadata for missing files
|
||||||
|
@ -29,11 +30,13 @@ class MetadataTask {
|
||||||
final shareFolder =
|
final shareFolder =
|
||||||
File(path: file_util.unstripPath(account, pref.getShareFolderOr()));
|
File(path: file_util.unstripPath(account, pref.getShareFolderOr()));
|
||||||
bool hasScanShareFolder = false;
|
bool hasScanShareFolder = false;
|
||||||
|
final geocoder = ReverseGeocoder();
|
||||||
|
await geocoder.init();
|
||||||
for (final r in account.roots) {
|
for (final r in account.roots) {
|
||||||
final dir = File(path: file_util.unstripPath(account, r));
|
final dir = File(path: file_util.unstripPath(account, r));
|
||||||
hasScanShareFolder |= file_util.isOrUnderDir(shareFolder, dir);
|
hasScanShareFolder |= file_util.isOrUnderDir(shareFolder, dir);
|
||||||
final op = UpdateMissingMetadata(
|
final op = UpdateMissingMetadata(_c.fileRepo,
|
||||||
_c.fileRepo, const _UpdateMissingMetadataConfigProvider());
|
const _UpdateMissingMetadataConfigProvider(), geocoder);
|
||||||
await for (final _ in op(account, dir)) {
|
await for (final _ in op(account, dir)) {
|
||||||
if (!Pref().isEnableExifOr()) {
|
if (!Pref().isEnableExifOr()) {
|
||||||
_log.info("[call] EXIF disabled, task ending immaturely");
|
_log.info("[call] EXIF disabled, task ending immaturely");
|
||||||
|
@ -43,8 +46,8 @@ class MetadataTask {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!hasScanShareFolder) {
|
if (!hasScanShareFolder) {
|
||||||
final op = UpdateMissingMetadata(
|
final op = UpdateMissingMetadata(_c.fileRepo,
|
||||||
_c.fileRepo, const _UpdateMissingMetadataConfigProvider());
|
const _UpdateMissingMetadataConfigProvider(), geocoder);
|
||||||
await for (final _ in op(
|
await for (final _ in op(
|
||||||
account,
|
account,
|
||||||
shareFolder,
|
shareFolder,
|
||||||
|
|
|
@ -2,9 +2,11 @@ import 'dart:io' as dart;
|
||||||
|
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:drift/native.dart';
|
import 'package:drift/native.dart';
|
||||||
|
import 'package:flutter/services.dart' show rootBundle;
|
||||||
import 'package:path/path.dart' as path_lib;
|
import 'package:path/path.dart' as path_lib;
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart' as sqlite3;
|
import 'package:sqlite3/sqlite3.dart';
|
||||||
|
import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart' as sql;
|
||||||
|
|
||||||
Future<Map<String, dynamic>> getSqliteConnectionArgs() async {
|
Future<Map<String, dynamic>> getSqliteConnectionArgs() async {
|
||||||
// put the database file, called db.sqlite here, into the documents folder
|
// put the database file, called db.sqlite here, into the documents folder
|
||||||
|
@ -32,5 +34,27 @@ QueryExecutor openSqliteConnection() {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> applyWorkaroundToOpenSqlite3OnOldAndroidVersions() {
|
Future<void> applyWorkaroundToOpenSqlite3OnOldAndroidVersions() {
|
||||||
return sqlite3.applyWorkaroundToOpenSqlite3OnOldAndroidVersions();
|
return sql.applyWorkaroundToOpenSqlite3OnOldAndroidVersions();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Database> openRawSqliteDbFromAsset(
|
||||||
|
String assetRelativePath,
|
||||||
|
String outputFilename, {
|
||||||
|
bool isReadOnly = false,
|
||||||
|
}) async {
|
||||||
|
final dbFolder = await getApplicationDocumentsDirectory();
|
||||||
|
final file = dart.File(path_lib.join(dbFolder.path, outputFilename));
|
||||||
|
if (!await file.exists()) {
|
||||||
|
// copy file from assets
|
||||||
|
final blob = await rootBundle.load("assets/$assetRelativePath");
|
||||||
|
final buffer = blob.buffer;
|
||||||
|
await file.writeAsBytes(
|
||||||
|
buffer.asUint8List(blob.offsetInBytes, blob.lengthInBytes),
|
||||||
|
flush: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return sqlite3.open(
|
||||||
|
file.path,
|
||||||
|
mode: isReadOnly ? OpenMode.readOnly : OpenMode.readWriteCreate,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
172
app/lib/reverse_geocoder.dart
Normal file
172
app/lib/reverse_geocoder.dart
Normal file
|
@ -0,0 +1,172 @@
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:kdtree/kdtree.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:nc_photos/entity/file.dart';
|
||||||
|
import 'package:nc_photos/math_util.dart' as math_util;
|
||||||
|
import 'package:nc_photos/mobile/platform.dart'
|
||||||
|
if (dart.library.html) 'package:nc_photos/web/platform.dart' as platform;
|
||||||
|
import 'package:sqlite3/common.dart';
|
||||||
|
|
||||||
|
class ReverseGeocoderLocation {
|
||||||
|
const ReverseGeocoderLocation(this.name, this.latitude, this.longitude,
|
||||||
|
this.countryCode, this.admin1, this.admin2);
|
||||||
|
|
||||||
|
@override
|
||||||
|
toString() => "$runtimeType {"
|
||||||
|
"name: $name, "
|
||||||
|
"latitude: $latitude, "
|
||||||
|
"longitude: $longitude, "
|
||||||
|
"countryCode: $countryCode, "
|
||||||
|
"admin1: $admin1, "
|
||||||
|
"admin2: $admin2, "
|
||||||
|
"}";
|
||||||
|
|
||||||
|
final String name;
|
||||||
|
final double latitude;
|
||||||
|
final double longitude;
|
||||||
|
final String countryCode;
|
||||||
|
final String? admin1;
|
||||||
|
final String? admin2;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ReverseGeocoder {
|
||||||
|
Future<void> init() async {
|
||||||
|
final s = Stopwatch()..start();
|
||||||
|
_db = await _openDatabase();
|
||||||
|
_searchTree = _buildSearchTree(_db);
|
||||||
|
_log.info("[init] Elapsed time: ${s.elapsedMilliseconds}ms");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert a geographic coordinate (in degree) into a location
|
||||||
|
Future<ReverseGeocoderLocation?> call(
|
||||||
|
double latitude, double longitude) async {
|
||||||
|
_log.info(
|
||||||
|
"[call] latitude: ${latitude.toStringAsFixed(3)}, longitude: ${longitude.toStringAsFixed(3)}");
|
||||||
|
final latitudeInt = (latitude * 10000).round();
|
||||||
|
final longitudeInt = (longitude * 10000).round();
|
||||||
|
final nearest = _searchTree
|
||||||
|
.nearest({"t": latitudeInt, "g": longitudeInt}, 1).firstOrNull;
|
||||||
|
if (nearest == null) {
|
||||||
|
_log.info("[call] Nearest point not found");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final nearestLat = nearest[0]["t"];
|
||||||
|
final nearestLatF = nearestLat / 10000;
|
||||||
|
final nearestLng = nearest[0]["g"];
|
||||||
|
final nearestLngF = nearestLng / 10000;
|
||||||
|
_log.info("[call] Nearest point, (lat: $nearestLatF, lng: $nearestLngF)");
|
||||||
|
try {
|
||||||
|
final distance = _distanceInKm(
|
||||||
|
math_util.degToRad(latitude),
|
||||||
|
math_util.degToRad(longitude),
|
||||||
|
math_util.degToRad(nearestLatF),
|
||||||
|
math_util.degToRad(nearestLngF),
|
||||||
|
);
|
||||||
|
_log.info(
|
||||||
|
"[call] (lat: ${latitude.toStringAsFixed(3)}, lng: ${longitude.toStringAsFixed(3)}) <-> (lat: $nearestLatF, lng: $nearestLngF) = ${distance.toStringAsFixed(3)}km");
|
||||||
|
// a completely arbitrary threshold :)
|
||||||
|
if (distance > 10) {
|
||||||
|
_log.info("[call] Nearest point is too far away");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
_log.severe("[call] Uncaught exception", e, stackTrace);
|
||||||
|
}
|
||||||
|
|
||||||
|
final data = _queryPoint(nearestLat, nearestLng);
|
||||||
|
if (data == null) {
|
||||||
|
_log.severe(
|
||||||
|
"[call] Row not found for point: latitude: $nearestLat, longitude: $nearestLng");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final result = ReverseGeocoderLocation(data.name, data.latitude / 10000,
|
||||||
|
data.longitude / 10000, data.countryCode, data.admin1, data.admin2);
|
||||||
|
_log.info("[call] Found: $result");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
_DatabaseRow? _queryPoint(int latitudeInt, int longitudeInt) {
|
||||||
|
final result = _db.select(
|
||||||
|
"SELECT * FROM cities WHERE latitude = ? AND longitude = ? LIMIT 1;",
|
||||||
|
[latitudeInt, longitudeInt],
|
||||||
|
);
|
||||||
|
if (result.isEmpty) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return _DatabaseRow(
|
||||||
|
result.first.columnAt(1),
|
||||||
|
result.first.columnAt(2),
|
||||||
|
result.first.columnAt(3),
|
||||||
|
result.first.columnAt(4),
|
||||||
|
result.first.columnAt(5),
|
||||||
|
result.first.columnAt(6),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
late final CommonDatabase _db;
|
||||||
|
late final KDTree _searchTree;
|
||||||
|
|
||||||
|
static final _log = Logger("reverse_geocoder.ReverseGeocoder");
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ReverseGeocoderExtension on ReverseGeocoderLocation {
|
||||||
|
ImageLocation toImageLocation() {
|
||||||
|
return ImageLocation(
|
||||||
|
name: name,
|
||||||
|
latitude: latitude,
|
||||||
|
longitude: longitude,
|
||||||
|
countryCode: countryCode,
|
||||||
|
admin1: admin1,
|
||||||
|
admin2: admin2,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DatabaseRow {
|
||||||
|
const _DatabaseRow(this.name, this.latitude, this.longitude, this.countryCode,
|
||||||
|
this.admin1, this.admin2);
|
||||||
|
|
||||||
|
final String name;
|
||||||
|
final int latitude;
|
||||||
|
final int longitude;
|
||||||
|
final String countryCode;
|
||||||
|
final String? admin1;
|
||||||
|
final String? admin2;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<CommonDatabase> _openDatabase() async {
|
||||||
|
return platform.openRawSqliteDbFromAsset("cities.sqlite", "cities.sqlite");
|
||||||
|
}
|
||||||
|
|
||||||
|
KDTree _buildSearchTree(CommonDatabase db) {
|
||||||
|
final results = db.select("SELECT latitude, longitude FROM cities;");
|
||||||
|
return KDTree(
|
||||||
|
results.map((e) => {"t": e.columnAt(0), "g": e.columnAt(1)}).toList(),
|
||||||
|
_kdTreeDistance,
|
||||||
|
["t", "g"],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
int _kdTreeDistance(Map a, Map b) {
|
||||||
|
return (math.pow((a["t"] as int) - (b["t"] as int), 2) +
|
||||||
|
math.pow((a["g"] as int) - (b["g"] as int), 2)) as int;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculate the distance in KM between two point
|
||||||
|
///
|
||||||
|
/// Both latitude and longitude are expected to be in radian
|
||||||
|
double _distanceInKm(
|
||||||
|
double latitude1, double longitude1, double latitude2, double longitude2) {
|
||||||
|
final dLat = latitude2 - latitude1;
|
||||||
|
final dLon = longitude2 - longitude1;
|
||||||
|
final a = math.pow(math.sin(dLat / 2), 2) +
|
||||||
|
math.cos(latitude1) *
|
||||||
|
math.cos(latitude2) *
|
||||||
|
math.pow(math.sin(dLon / 2), 2);
|
||||||
|
final c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a));
|
||||||
|
// 6371 = earth radius
|
||||||
|
return 6371 * c;
|
||||||
|
}
|
|
@ -19,6 +19,7 @@ import 'package:nc_photos/event/native_event.dart';
|
||||||
import 'package:nc_photos/future_extension.dart';
|
import 'package:nc_photos/future_extension.dart';
|
||||||
import 'package:nc_photos/language_util.dart' as language_util;
|
import 'package:nc_photos/language_util.dart' as language_util;
|
||||||
import 'package:nc_photos/pref.dart';
|
import 'package:nc_photos/pref.dart';
|
||||||
|
import 'package:nc_photos/reverse_geocoder.dart';
|
||||||
import 'package:nc_photos/use_case/update_missing_metadata.dart';
|
import 'package:nc_photos/use_case/update_missing_metadata.dart';
|
||||||
import 'package:nc_photos_plugin/nc_photos_plugin.dart';
|
import 'package:nc_photos_plugin/nc_photos_plugin.dart';
|
||||||
|
|
||||||
|
@ -248,11 +249,13 @@ class _MetadataTask {
|
||||||
path: file_util.unstripPath(account, accountPref.getShareFolderOr()));
|
path: file_util.unstripPath(account, accountPref.getShareFolderOr()));
|
||||||
bool hasScanShareFolder = false;
|
bool hasScanShareFolder = false;
|
||||||
final c = KiwiContainer().resolve<DiContainer>();
|
final c = KiwiContainer().resolve<DiContainer>();
|
||||||
|
final geocoder = ReverseGeocoder();
|
||||||
|
await geocoder.init();
|
||||||
for (final r in account.roots) {
|
for (final r in account.roots) {
|
||||||
final dir = File(path: file_util.unstripPath(account, r));
|
final dir = File(path: file_util.unstripPath(account, r));
|
||||||
hasScanShareFolder |= file_util.isOrUnderDir(shareFolder, dir);
|
hasScanShareFolder |= file_util.isOrUnderDir(shareFolder, dir);
|
||||||
final updater = UpdateMissingMetadata(
|
final updater = UpdateMissingMetadata(
|
||||||
c.fileRepo, const _UpdateMissingMetadataConfigProvider());
|
c.fileRepo, const _UpdateMissingMetadataConfigProvider(), geocoder);
|
||||||
void onServiceStop() {
|
void onServiceStop() {
|
||||||
_log.info("[_updateMetadata] Stopping task: user canceled");
|
_log.info("[_updateMetadata] Stopping task: user canceled");
|
||||||
updater.stop();
|
updater.stop();
|
||||||
|
@ -275,7 +278,7 @@ class _MetadataTask {
|
||||||
}
|
}
|
||||||
if (!hasScanShareFolder) {
|
if (!hasScanShareFolder) {
|
||||||
final shareUpdater = UpdateMissingMetadata(
|
final shareUpdater = UpdateMissingMetadata(
|
||||||
c.fileRepo, const _UpdateMissingMetadataConfigProvider());
|
c.fileRepo, const _UpdateMissingMetadataConfigProvider(), geocoder);
|
||||||
void onServiceStop() {
|
void onServiceStop() {
|
||||||
_log.info("[_updateMetadata] Stopping task: user canceled");
|
_log.info("[_updateMetadata] Stopping task: user canceled");
|
||||||
shareUpdater.stop();
|
shareUpdater.stop();
|
||||||
|
|
58
app/lib/use_case/list_location_file.dart
Normal file
58
app/lib/use_case/list_location_file.dart
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
import 'package:drift/drift.dart' as sql;
|
||||||
|
import 'package:nc_photos/account.dart';
|
||||||
|
import 'package:nc_photos/di_container.dart';
|
||||||
|
import 'package:nc_photos/entity/file.dart';
|
||||||
|
import 'package:nc_photos/entity/sqlite_table_converter.dart';
|
||||||
|
import 'package:nc_photos/entity/sqlite_table_extension.dart' as sql;
|
||||||
|
import 'package:nc_photos/location_util.dart' as location_util;
|
||||||
|
import 'package:nc_photos/object_extension.dart';
|
||||||
|
|
||||||
|
class ListLocationFile {
|
||||||
|
ListLocationFile(this._c) : assert(require(_c));
|
||||||
|
|
||||||
|
static bool require(DiContainer c) => DiContainer.has(c, DiType.sqliteDb);
|
||||||
|
|
||||||
|
/// List all files located in [place], [countryCode]
|
||||||
|
Future<List<File>> call(
|
||||||
|
Account account, File dir, String? place, String countryCode) async {
|
||||||
|
final dbFiles = await _c.sqliteDb.use((db) async {
|
||||||
|
final query = db.queryFiles().run((q) {
|
||||||
|
q
|
||||||
|
..setQueryMode(sql.FilesQueryMode.completeFile)
|
||||||
|
..setAppAccount(account);
|
||||||
|
dir.strippedPathWithEmpty.run((p) {
|
||||||
|
if (p.isNotEmpty) {
|
||||||
|
q.byOrRelativePathPattern("$p/%");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return q.build();
|
||||||
|
});
|
||||||
|
if (place == null ||
|
||||||
|
location_util.alpha2CodeToName(countryCode) == place) {
|
||||||
|
// some places in the DB have the same name as the country, in such
|
||||||
|
// cases, we return all photos from the country
|
||||||
|
query.where(db.imageLocations.countryCode.equals(countryCode));
|
||||||
|
} else {
|
||||||
|
query
|
||||||
|
..where(db.imageLocations.name.equals(place) |
|
||||||
|
db.imageLocations.admin1.equals(place) |
|
||||||
|
db.imageLocations.admin2.equals(place))
|
||||||
|
..where(db.imageLocations.countryCode.equals(countryCode));
|
||||||
|
}
|
||||||
|
return await query
|
||||||
|
.map((r) => sql.CompleteFile(
|
||||||
|
r.readTable(db.files),
|
||||||
|
r.readTable(db.accountFiles),
|
||||||
|
r.readTableOrNull(db.images),
|
||||||
|
r.readTableOrNull(db.imageLocations),
|
||||||
|
r.readTableOrNull(db.trashes),
|
||||||
|
))
|
||||||
|
.get();
|
||||||
|
});
|
||||||
|
return dbFiles
|
||||||
|
.map((f) => SqliteFileConverter.fromSql(account.userId.toString(), f))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
final DiContainer _c;
|
||||||
|
}
|
173
app/lib/use_case/list_location_group.dart
Normal file
173
app/lib/use_case/list_location_group.dart
Normal file
|
@ -0,0 +1,173 @@
|
||||||
|
import 'package:drift/drift.dart' as sql;
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:nc_photos/account.dart';
|
||||||
|
import 'package:nc_photos/di_container.dart';
|
||||||
|
import 'package:nc_photos/entity/sqlite_table.dart' as sql;
|
||||||
|
import 'package:nc_photos/entity/sqlite_table_extension.dart' as sql;
|
||||||
|
import 'package:nc_photos/location_util.dart' as location_util;
|
||||||
|
|
||||||
|
class LocationGroup {
|
||||||
|
const LocationGroup(
|
||||||
|
this.place, this.countryCode, this.count, this.latestFileId);
|
||||||
|
|
||||||
|
@override
|
||||||
|
toString() => "$runtimeType {"
|
||||||
|
"place: $place, "
|
||||||
|
"countryCode: $countryCode, "
|
||||||
|
"count: $count, "
|
||||||
|
"latestFileId: $latestFileId, "
|
||||||
|
"}";
|
||||||
|
|
||||||
|
final String place;
|
||||||
|
final String countryCode;
|
||||||
|
final int count;
|
||||||
|
final int latestFileId;
|
||||||
|
}
|
||||||
|
|
||||||
|
class LocationGroupResult {
|
||||||
|
const LocationGroupResult(
|
||||||
|
this.name, this.admin1, this.admin2, this.countryCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
toString() => "$runtimeType {"
|
||||||
|
"name: List {length: ${name.length}}, "
|
||||||
|
"admin1: List {length: ${admin1.length}}, "
|
||||||
|
"admin2: List {length: ${admin2.length}}, "
|
||||||
|
"countryCode: List {length: ${countryCode.length}}, "
|
||||||
|
"}";
|
||||||
|
|
||||||
|
final List<LocationGroup> name;
|
||||||
|
final List<LocationGroup> admin1;
|
||||||
|
final List<LocationGroup> admin2;
|
||||||
|
final List<LocationGroup> countryCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ListLocationGroup {
|
||||||
|
ListLocationGroup(this._c) : assert(require(_c));
|
||||||
|
|
||||||
|
static bool require(DiContainer c) => DiContainer.has(c, DiType.sqliteDb);
|
||||||
|
|
||||||
|
/// List location groups based on the name of the places
|
||||||
|
Future<LocationGroupResult> call(Account account) async {
|
||||||
|
final s = Stopwatch()..start();
|
||||||
|
try {
|
||||||
|
return await _c.sqliteDb.use((db) async {
|
||||||
|
final dbAccount = await db.accountOf(account);
|
||||||
|
|
||||||
|
final nameResult = <LocationGroup>[];
|
||||||
|
final admin1Result = <LocationGroup>[];
|
||||||
|
final admin2Result = <LocationGroup>[];
|
||||||
|
final countryCodeResult = <LocationGroup>[];
|
||||||
|
for (final r in account.roots) {
|
||||||
|
final latest = db.accountFiles.bestDateTime.max();
|
||||||
|
final count = db.imageLocations.rowId.count();
|
||||||
|
final nameQ = _buildQuery(
|
||||||
|
db, dbAccount, r, latest, count, db.imageLocations.name);
|
||||||
|
try {
|
||||||
|
nameResult.addAll(await nameQ
|
||||||
|
.map((r) => LocationGroup(
|
||||||
|
r.read(db.imageLocations.name)!,
|
||||||
|
r.read(db.imageLocations.countryCode)!,
|
||||||
|
r.read(count),
|
||||||
|
r.read(db.files.fileId)!,
|
||||||
|
))
|
||||||
|
.get());
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
_log.shout("[call] Failed while query name group", e, stackTrace);
|
||||||
|
}
|
||||||
|
|
||||||
|
final admin1Q = _buildQuery(
|
||||||
|
db, dbAccount, r, latest, count, db.imageLocations.admin1);
|
||||||
|
try {
|
||||||
|
admin1Result.addAll(await admin1Q
|
||||||
|
.map((r) => LocationGroup(
|
||||||
|
r.read(db.imageLocations.admin1)!,
|
||||||
|
r.read(db.imageLocations.countryCode)!,
|
||||||
|
r.read(count),
|
||||||
|
r.read(db.files.fileId)!,
|
||||||
|
))
|
||||||
|
.get());
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
_log.shout("[call] Failed while query admin1 group", e, stackTrace);
|
||||||
|
}
|
||||||
|
|
||||||
|
final admin2Q = _buildQuery(
|
||||||
|
db, dbAccount, r, latest, count, db.imageLocations.admin2);
|
||||||
|
try {
|
||||||
|
admin2Result.addAll(await admin2Q
|
||||||
|
.map((r) => LocationGroup(
|
||||||
|
r.read(db.imageLocations.admin2)!,
|
||||||
|
r.read(db.imageLocations.countryCode)!,
|
||||||
|
r.read(count),
|
||||||
|
r.read(db.files.fileId)!,
|
||||||
|
))
|
||||||
|
.get());
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
_log.shout("[call] Failed while query admin2 group", e, stackTrace);
|
||||||
|
}
|
||||||
|
|
||||||
|
final countryCodeQ = _buildQuery(
|
||||||
|
db, dbAccount, r, latest, count, db.imageLocations.countryCode);
|
||||||
|
try {
|
||||||
|
countryCodeResult.addAll(await countryCodeQ.map((r) {
|
||||||
|
final cc = r.read(db.imageLocations.countryCode)!;
|
||||||
|
return LocationGroup(location_util.alpha2CodeToName(cc) ?? cc, cc,
|
||||||
|
r.read(count), r.read(db.files.fileId)!);
|
||||||
|
}).get());
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
_log.shout(
|
||||||
|
"[call] Failed while query countryCode group", e, stackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return LocationGroupResult(
|
||||||
|
nameResult, admin1Result, admin2Result, countryCodeResult);
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
_log.info("[call] Elapsed time: ${s.elapsedMilliseconds}ms");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sql.JoinedSelectStatement _buildQuery(
|
||||||
|
sql.SqliteDb db,
|
||||||
|
sql.Account dbAccount,
|
||||||
|
String dir,
|
||||||
|
sql.Expression<DateTime> latest,
|
||||||
|
sql.Expression<int> count,
|
||||||
|
sql.GeneratedColumn<String?> groupColumn,
|
||||||
|
) {
|
||||||
|
final query = db.selectOnly(db.imageLocations).join([
|
||||||
|
sql.innerJoin(db.accountFiles,
|
||||||
|
db.accountFiles.rowId.equalsExp(db.imageLocations.accountFile),
|
||||||
|
useColumns: false),
|
||||||
|
sql.innerJoin(db.files, db.files.rowId.equalsExp(db.accountFiles.file),
|
||||||
|
useColumns: false),
|
||||||
|
]);
|
||||||
|
if (identical(groupColumn, db.imageLocations.countryCode)) {
|
||||||
|
query
|
||||||
|
..addColumns([db.imageLocations.countryCode, count, db.files.fileId])
|
||||||
|
..groupBy([db.imageLocations.countryCode],
|
||||||
|
having: db.accountFiles.bestDateTime.equalsExp(latest));
|
||||||
|
} else {
|
||||||
|
query
|
||||||
|
..addColumns([
|
||||||
|
groupColumn,
|
||||||
|
db.imageLocations.countryCode,
|
||||||
|
count,
|
||||||
|
db.files.fileId
|
||||||
|
])
|
||||||
|
..groupBy([groupColumn, db.imageLocations.countryCode],
|
||||||
|
having: db.accountFiles.bestDateTime.equalsExp(latest));
|
||||||
|
}
|
||||||
|
query
|
||||||
|
..where(db.accountFiles.account.equals(dbAccount.rowId))
|
||||||
|
..where(groupColumn.isNotNull());
|
||||||
|
if (dir.isNotEmpty) {
|
||||||
|
query.where(db.accountFiles.relativePath.like("$dir/%"));
|
||||||
|
}
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
|
final DiContainer _c;
|
||||||
|
|
||||||
|
static final _log = Logger("use_case.list_location_group.ListLocationGroup");
|
||||||
|
}
|
|
@ -46,6 +46,7 @@ class ScanDirOffline {
|
||||||
r.readTable(db.files),
|
r.readTable(db.files),
|
||||||
r.readTable(db.accountFiles),
|
r.readTable(db.accountFiles),
|
||||||
r.readTableOrNull(db.images),
|
r.readTableOrNull(db.images),
|
||||||
|
r.readTableOrNull(db.imageLocations),
|
||||||
r.readTableOrNull(db.trashes),
|
r.readTableOrNull(db.trashes),
|
||||||
))
|
))
|
||||||
.get();
|
.get();
|
||||||
|
@ -97,6 +98,7 @@ class ScanDirOfflineMini {
|
||||||
r.readTable(db.files),
|
r.readTable(db.files),
|
||||||
r.readTable(db.accountFiles),
|
r.readTable(db.accountFiles),
|
||||||
r.readTableOrNull(db.images),
|
r.readTableOrNull(db.images),
|
||||||
|
r.readTableOrNull(db.imageLocations),
|
||||||
r.readTableOrNull(db.trashes),
|
r.readTableOrNull(db.trashes),
|
||||||
))
|
))
|
||||||
.get();
|
.get();
|
||||||
|
|
|
@ -4,11 +4,13 @@ import 'package:kiwi/kiwi.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/connectivity_util.dart' as connectivity_util;
|
import 'package:nc_photos/connectivity_util.dart' as connectivity_util;
|
||||||
|
import 'package:nc_photos/entity/exif_extension.dart';
|
||||||
import 'package:nc_photos/entity/file.dart';
|
import 'package:nc_photos/entity/file.dart';
|
||||||
import 'package:nc_photos/event/event.dart';
|
import 'package:nc_photos/event/event.dart';
|
||||||
import 'package:nc_photos/exception.dart';
|
import 'package:nc_photos/exception.dart';
|
||||||
import 'package:nc_photos/exception_event.dart';
|
import 'package:nc_photos/exception_event.dart';
|
||||||
import 'package:nc_photos/or_null.dart';
|
import 'package:nc_photos/or_null.dart';
|
||||||
|
import 'package:nc_photos/reverse_geocoder.dart';
|
||||||
import 'package:nc_photos/use_case/get_file_binary.dart';
|
import 'package:nc_photos/use_case/get_file_binary.dart';
|
||||||
import 'package:nc_photos/use_case/load_metadata.dart';
|
import 'package:nc_photos/use_case/load_metadata.dart';
|
||||||
import 'package:nc_photos/use_case/scan_missing_metadata.dart';
|
import 'package:nc_photos/use_case/scan_missing_metadata.dart';
|
||||||
|
@ -19,7 +21,7 @@ abstract class UpdateMissingMetadataConfigProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
class UpdateMissingMetadata {
|
class UpdateMissingMetadata {
|
||||||
UpdateMissingMetadata(this.fileRepo, this.configProvider);
|
UpdateMissingMetadata(this.fileRepo, this.configProvider, this.geocoder);
|
||||||
|
|
||||||
/// Update metadata for all files that support one under a dir
|
/// Update metadata for all files that support one under a dir
|
||||||
///
|
///
|
||||||
|
@ -43,6 +45,9 @@ class UpdateMissingMetadata {
|
||||||
isRecursive: isRecursive,
|
isRecursive: isRecursive,
|
||||||
);
|
);
|
||||||
await for (final d in dataStream) {
|
await for (final d in dataStream) {
|
||||||
|
if (!_shouldRun) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (d is ExceptionEvent) {
|
if (d is ExceptionEvent) {
|
||||||
yield d;
|
yield d;
|
||||||
continue;
|
continue;
|
||||||
|
@ -54,12 +59,16 @@ class UpdateMissingMetadata {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
OrNull<Metadata>? metadataUpdate;
|
||||||
|
OrNull<ImageLocation>? locationUpdate;
|
||||||
|
if (file.metadata == null) {
|
||||||
// since we need to download multiple images in their original size,
|
// since we need to download multiple images in their original size,
|
||||||
// we only do it with WiFi
|
// we only do it with WiFi
|
||||||
await _ensureWifi();
|
await _ensureWifi();
|
||||||
await _ensureBattery();
|
await _ensureBattery();
|
||||||
KiwiContainer().resolve<EventBus>().fire(
|
KiwiContainer().resolve<EventBus>().fire(
|
||||||
const MetadataTaskStateChangedEvent(MetadataTaskState.prcoessing));
|
const MetadataTaskStateChangedEvent(
|
||||||
|
MetadataTaskState.prcoessing));
|
||||||
if (!_shouldRun) {
|
if (!_shouldRun) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -69,13 +78,39 @@ class UpdateMissingMetadata {
|
||||||
(await LoadMetadata().loadRemote(account, file, binary)).copyWith(
|
(await LoadMetadata().loadRemote(account, file, binary)).copyWith(
|
||||||
fileEtag: file.etag,
|
fileEtag: file.etag,
|
||||||
);
|
);
|
||||||
|
metadataUpdate = OrNull(metadata);
|
||||||
|
} else {
|
||||||
|
_log.finer("[call] Skip updating metadata for ${file.path}");
|
||||||
|
}
|
||||||
|
|
||||||
|
final lat =
|
||||||
|
(metadataUpdate?.obj ?? file.metadata)?.exif?.gpsLatitudeDeg;
|
||||||
|
final lng =
|
||||||
|
(metadataUpdate?.obj ?? file.metadata)?.exif?.gpsLongitudeDeg;
|
||||||
|
try {
|
||||||
|
ImageLocation? location;
|
||||||
|
if (lat != null && lng != null) {
|
||||||
|
_log.fine("[call] Reverse geocoding for ${file.path}");
|
||||||
|
final l = await geocoder(lat, lng);
|
||||||
|
if (l != null) {
|
||||||
|
location = l.toImageLocation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
locationUpdate = OrNull(location ?? ImageLocation.empty());
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
_log.severe("[call] Failed while reverse geocoding: ${file.path}", e,
|
||||||
|
stackTrace);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metadataUpdate != null || locationUpdate != null) {
|
||||||
await UpdateProperty(fileRepo)(
|
await UpdateProperty(fileRepo)(
|
||||||
account,
|
account,
|
||||||
file,
|
file,
|
||||||
metadata: OrNull(metadata),
|
metadata: metadataUpdate,
|
||||||
|
location: locationUpdate,
|
||||||
);
|
);
|
||||||
yield file;
|
yield file;
|
||||||
|
}
|
||||||
|
|
||||||
// slow down a bit to give some space for the main isolate
|
// slow down a bit to give some space for the main isolate
|
||||||
await Future.delayed(const Duration(milliseconds: 10));
|
await Future.delayed(const Duration(milliseconds: 10));
|
||||||
|
@ -123,6 +158,7 @@ class UpdateMissingMetadata {
|
||||||
|
|
||||||
final FileRepo fileRepo;
|
final FileRepo fileRepo;
|
||||||
final UpdateMissingMetadataConfigProvider configProvider;
|
final UpdateMissingMetadataConfigProvider configProvider;
|
||||||
|
final ReverseGeocoder geocoder;
|
||||||
|
|
||||||
bool _shouldRun = true;
|
bool _shouldRun = true;
|
||||||
|
|
||||||
|
|
|
@ -16,11 +16,13 @@ class UpdateProperty {
|
||||||
OrNull<bool>? isArchived,
|
OrNull<bool>? isArchived,
|
||||||
OrNull<DateTime>? overrideDateTime,
|
OrNull<DateTime>? overrideDateTime,
|
||||||
bool? favorite,
|
bool? favorite,
|
||||||
|
OrNull<ImageLocation>? location,
|
||||||
}) async {
|
}) async {
|
||||||
if (metadata == null &&
|
if (metadata == null &&
|
||||||
isArchived == null &&
|
isArchived == null &&
|
||||||
overrideDateTime == null &&
|
overrideDateTime == null &&
|
||||||
favorite == null) {
|
favorite == null &&
|
||||||
|
location == null) {
|
||||||
// ?
|
// ?
|
||||||
_log.warning("[call] Nothing to update");
|
_log.warning("[call] Nothing to update");
|
||||||
return;
|
return;
|
||||||
|
@ -37,6 +39,7 @@ class UpdateProperty {
|
||||||
isArchived: isArchived,
|
isArchived: isArchived,
|
||||||
overrideDateTime: overrideDateTime,
|
overrideDateTime: overrideDateTime,
|
||||||
favorite: favorite,
|
favorite: favorite,
|
||||||
|
location: location,
|
||||||
);
|
);
|
||||||
|
|
||||||
int properties = 0;
|
int properties = 0;
|
||||||
|
@ -52,6 +55,9 @@ class UpdateProperty {
|
||||||
if (favorite != null) {
|
if (favorite != null) {
|
||||||
properties |= FilePropertyUpdatedEvent.propFavorite;
|
properties |= FilePropertyUpdatedEvent.propFavorite;
|
||||||
}
|
}
|
||||||
|
if (location != null) {
|
||||||
|
properties |= FilePropertyUpdatedEvent.propImageLocation;
|
||||||
|
}
|
||||||
assert(properties != 0);
|
assert(properties != 0);
|
||||||
KiwiContainer()
|
KiwiContainer()
|
||||||
.resolve<EventBus>()
|
.resolve<EventBus>()
|
||||||
|
|
30
app/lib/widget/about_geocoding_dialog.dart
Normal file
30
app/lib/widget/about_geocoding_dialog.dart
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:nc_photos/app_localizations.dart';
|
||||||
|
import 'package:nc_photos/theme.dart';
|
||||||
|
|
||||||
|
class AboutGeocodingDialog extends StatelessWidget {
|
||||||
|
const AboutGeocodingDialog({
|
||||||
|
Key? key,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
build(BuildContext context) {
|
||||||
|
return AppTheme(
|
||||||
|
child: AlertDialog(
|
||||||
|
title: Text(L10n.global().gpsPlaceAboutDialogTitle),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(L10n.global().gpsPlaceAboutDialogContent),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
const Divider(height: 16),
|
||||||
|
const Text(
|
||||||
|
"Based on GeoNames Gazetteer data by GeoNames, licensed under CC BY 4.0",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,9 +11,11 @@ import 'package:nc_photos/exception_util.dart' as exception_util;
|
||||||
import 'package:nc_photos/k.dart' as k;
|
import 'package:nc_photos/k.dart' as k;
|
||||||
import 'package:nc_photos/snack_bar_manager.dart';
|
import 'package:nc_photos/snack_bar_manager.dart';
|
||||||
import 'package:nc_photos/theme.dart';
|
import 'package:nc_photos/theme.dart';
|
||||||
|
import 'package:nc_photos/use_case/list_location_group.dart';
|
||||||
import 'package:nc_photos/widget/album_browser_util.dart' as album_browser_util;
|
import 'package:nc_photos/widget/album_browser_util.dart' as album_browser_util;
|
||||||
import 'package:nc_photos/widget/page_visibility_mixin.dart';
|
import 'package:nc_photos/widget/page_visibility_mixin.dart';
|
||||||
import 'package:nc_photos/widget/person_browser.dart';
|
import 'package:nc_photos/widget/person_browser.dart';
|
||||||
|
import 'package:nc_photos/widget/place_browser.dart';
|
||||||
import 'package:nc_photos/widget/tag_browser.dart';
|
import 'package:nc_photos/widget/tag_browser.dart';
|
||||||
|
|
||||||
class HomeSearchSuggestionController {
|
class HomeSearchSuggestionController {
|
||||||
|
@ -133,6 +135,14 @@ class _HomeSearchSuggestionState extends State<HomeSearchSuggestion>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onLocationPressed(_LocationListItem item) {
|
||||||
|
if (mounted) {
|
||||||
|
Navigator.of(context).pushNamed(PlaceBrowser.routeName,
|
||||||
|
arguments: PlaceBrowserArguments(
|
||||||
|
widget.account, item.location.place, item.location.countryCode));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _transformItems(List<HomeSearchResult> results) {
|
void _transformItems(List<HomeSearchResult> results) {
|
||||||
final items = () sync* {
|
final items = () sync* {
|
||||||
for (final r in results) {
|
for (final r in results) {
|
||||||
|
@ -142,6 +152,8 @@ class _HomeSearchSuggestionState extends State<HomeSearchSuggestion>
|
||||||
yield _TagListItem(r.tag, onTap: _onTagPressed);
|
yield _TagListItem(r.tag, onTap: _onTagPressed);
|
||||||
} else if (r is HomeSearchPersonResult) {
|
} else if (r is HomeSearchPersonResult) {
|
||||||
yield _PersonListItem(r.person, onTap: _onPersonPressed);
|
yield _PersonListItem(r.person, onTap: _onPersonPressed);
|
||||||
|
} else if (r is HomeSearchLocationResult) {
|
||||||
|
yield _LocationListItem(r.location, onTap: _onLocationPressed);
|
||||||
} else {
|
} else {
|
||||||
_log.warning("[_transformItems] Unknown type: ${r.runtimeType}");
|
_log.warning("[_transformItems] Unknown type: ${r.runtimeType}");
|
||||||
}
|
}
|
||||||
|
@ -213,3 +225,20 @@ class _PersonListItem implements _ListItem {
|
||||||
final Person person;
|
final Person person;
|
||||||
final void Function(_PersonListItem)? onTap;
|
final void Function(_PersonListItem)? onTap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _LocationListItem implements _ListItem {
|
||||||
|
const _LocationListItem(
|
||||||
|
this.location, {
|
||||||
|
this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
buildWidget(BuildContext context) => ListTile(
|
||||||
|
leading: const Icon(Icons.location_on_outlined),
|
||||||
|
title: Text(location.place),
|
||||||
|
onTap: onTap == null ? null : () => onTap!(this),
|
||||||
|
);
|
||||||
|
|
||||||
|
final LocationGroup location;
|
||||||
|
final void Function(_LocationListItem)? onTap;
|
||||||
|
}
|
||||||
|
|
|
@ -23,6 +23,8 @@ import 'package:nc_photos/widget/image_editor.dart';
|
||||||
import 'package:nc_photos/widget/local_file_viewer.dart';
|
import 'package:nc_photos/widget/local_file_viewer.dart';
|
||||||
import 'package:nc_photos/widget/people_browser.dart';
|
import 'package:nc_photos/widget/people_browser.dart';
|
||||||
import 'package:nc_photos/widget/person_browser.dart';
|
import 'package:nc_photos/widget/person_browser.dart';
|
||||||
|
import 'package:nc_photos/widget/place_browser.dart';
|
||||||
|
import 'package:nc_photos/widget/places_browser.dart';
|
||||||
import 'package:nc_photos/widget/root_picker.dart';
|
import 'package:nc_photos/widget/root_picker.dart';
|
||||||
import 'package:nc_photos/widget/settings.dart';
|
import 'package:nc_photos/widget/settings.dart';
|
||||||
import 'package:nc_photos/widget/setup.dart';
|
import 'package:nc_photos/widget/setup.dart';
|
||||||
|
@ -167,6 +169,8 @@ class _MyAppState extends State<MyApp>
|
||||||
route ??= _handleChangelogRoute(settings);
|
route ??= _handleChangelogRoute(settings);
|
||||||
route ??= _handleTagBrowserRoute(settings);
|
route ??= _handleTagBrowserRoute(settings);
|
||||||
route ??= _handlePeopleBrowserRoute(settings);
|
route ??= _handlePeopleBrowserRoute(settings);
|
||||||
|
route ??= _handlePlaceBrowserRoute(settings);
|
||||||
|
route ??= _handlePlacesBrowserRoute(settings);
|
||||||
return route;
|
return route;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -570,6 +574,32 @@ class _MyAppState extends State<MyApp>
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Route<dynamic>? _handlePlaceBrowserRoute(RouteSettings settings) {
|
||||||
|
try {
|
||||||
|
if (settings.name == PlaceBrowser.routeName &&
|
||||||
|
settings.arguments != null) {
|
||||||
|
final args = settings.arguments as PlaceBrowserArguments;
|
||||||
|
return PlaceBrowser.buildRoute(args);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
_log.severe("[_handlePlaceBrowserRoute] Failed while handling route", e);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Route<dynamic>? _handlePlacesBrowserRoute(RouteSettings settings) {
|
||||||
|
try {
|
||||||
|
if (settings.name == PlacesBrowser.routeName &&
|
||||||
|
settings.arguments != null) {
|
||||||
|
final args = settings.arguments as PlacesBrowserArguments;
|
||||||
|
return PlacesBrowser.buildRoute(args);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
_log.severe("[_handlePlacesBrowserRoute] Failed while handling route", e);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
final _scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
|
final _scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
|
||||||
final _navigatorKey = GlobalKey<NavigatorState>();
|
final _navigatorKey = GlobalKey<NavigatorState>();
|
||||||
|
|
||||||
|
|
426
app/lib/widget/place_browser.dart
Normal file
426
app/lib/widget/place_browser.dart
Normal file
|
@ -0,0 +1,426 @@
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
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/list_location_file.dart';
|
||||||
|
import 'package:nc_photos/compute_queue.dart';
|
||||||
|
import 'package:nc_photos/di_container.dart';
|
||||||
|
import 'package:nc_photos/download_handler.dart';
|
||||||
|
import 'package:nc_photos/entity/file.dart';
|
||||||
|
import 'package:nc_photos/exception_util.dart' as exception_util;
|
||||||
|
import 'package:nc_photos/k.dart' as k;
|
||||||
|
import 'package:nc_photos/language_util.dart' as language_util;
|
||||||
|
import 'package:nc_photos/location_util.dart' as location_util;
|
||||||
|
import 'package:nc_photos/object_extension.dart';
|
||||||
|
import 'package:nc_photos/pref.dart';
|
||||||
|
import 'package:nc_photos/share_handler.dart';
|
||||||
|
import 'package:nc_photos/snack_bar_manager.dart';
|
||||||
|
import 'package:nc_photos/theme.dart';
|
||||||
|
import 'package:nc_photos/widget/about_geocoding_dialog.dart';
|
||||||
|
import 'package:nc_photos/widget/builder/photo_list_item_builder.dart';
|
||||||
|
import 'package:nc_photos/widget/handler/add_selection_to_album_handler.dart';
|
||||||
|
import 'package:nc_photos/widget/handler/archive_selection_handler.dart';
|
||||||
|
import 'package:nc_photos/widget/handler/remove_selection_handler.dart';
|
||||||
|
import 'package:nc_photos/widget/photo_list_item.dart';
|
||||||
|
import 'package:nc_photos/widget/photo_list_util.dart' as photo_list_util;
|
||||||
|
import 'package:nc_photos/widget/selectable_item_stream_list_mixin.dart';
|
||||||
|
import 'package:nc_photos/widget/selection_app_bar.dart';
|
||||||
|
import 'package:nc_photos/widget/viewer.dart';
|
||||||
|
import 'package:nc_photos/widget/zoom_menu_button.dart';
|
||||||
|
|
||||||
|
class PlaceBrowserArguments {
|
||||||
|
const PlaceBrowserArguments(this.account, this.place, this.countryCode);
|
||||||
|
|
||||||
|
final Account account;
|
||||||
|
final String? place;
|
||||||
|
final String countryCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
class PlaceBrowser extends StatefulWidget {
|
||||||
|
static const routeName = "/place-browser";
|
||||||
|
|
||||||
|
static Route buildRoute(PlaceBrowserArguments args) => MaterialPageRoute(
|
||||||
|
builder: (context) => PlaceBrowser.fromArgs(args),
|
||||||
|
);
|
||||||
|
|
||||||
|
const PlaceBrowser({
|
||||||
|
Key? key,
|
||||||
|
required this.account,
|
||||||
|
required this.place,
|
||||||
|
required this.countryCode,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
PlaceBrowser.fromArgs(PlaceBrowserArguments args, {Key? key})
|
||||||
|
: this(
|
||||||
|
key: key,
|
||||||
|
account: args.account,
|
||||||
|
place: args.place,
|
||||||
|
countryCode: args.countryCode,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
createState() => _PlaceBrowserState();
|
||||||
|
|
||||||
|
final Account account;
|
||||||
|
final String? place;
|
||||||
|
final String countryCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PlaceBrowserState extends State<PlaceBrowser>
|
||||||
|
with SelectableItemStreamListMixin<PlaceBrowser> {
|
||||||
|
_PlaceBrowserState() {
|
||||||
|
final c = KiwiContainer().resolve<DiContainer>();
|
||||||
|
assert(require(c));
|
||||||
|
assert(ListLocationFileBloc.require(c));
|
||||||
|
_c = c;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool require(DiContainer c) => true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
initState() {
|
||||||
|
super.initState();
|
||||||
|
_initBloc();
|
||||||
|
_thumbZoomLevel = Pref().getAlbumBrowserZoomLevelOr(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
build(BuildContext context) {
|
||||||
|
return AppTheme(
|
||||||
|
child: Scaffold(
|
||||||
|
body: BlocListener<ListLocationFileBloc, ListLocationFileBlocState>(
|
||||||
|
bloc: _bloc,
|
||||||
|
listener: (context, state) => _onStateChange(context, state),
|
||||||
|
child: BlocBuilder<ListLocationFileBloc, ListLocationFileBlocState>(
|
||||||
|
bloc: _bloc,
|
||||||
|
builder: (context, state) => _buildContent(context, state),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
onItemTap(SelectableItem item, int index) {
|
||||||
|
item.as<PhotoListFileItem>()?.run((fileItem) {
|
||||||
|
Navigator.pushNamed(
|
||||||
|
context,
|
||||||
|
Viewer.routeName,
|
||||||
|
arguments:
|
||||||
|
ViewerArguments(widget.account, _backingFiles, fileItem.fileIndex),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _initBloc() {
|
||||||
|
_log.info("[_initBloc] Initialize bloc");
|
||||||
|
_reqQuery();
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildContent(BuildContext context, ListLocationFileBlocState state) {
|
||||||
|
return buildItemStreamListOuter(
|
||||||
|
context,
|
||||||
|
child: Theme(
|
||||||
|
data: Theme.of(context).copyWith(
|
||||||
|
colorScheme: Theme.of(context).colorScheme.copyWith(
|
||||||
|
secondary: AppTheme.getOverscrollIndicatorColor(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
_buildAppBar(context, state),
|
||||||
|
if (state is ListLocationFileBlocLoading ||
|
||||||
|
_buildItemQueue.isProcessing)
|
||||||
|
const SliverToBoxAdapter(
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: LinearProgressIndicator(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
buildItemStreamList(
|
||||||
|
maxCrossAxisExtent: _thumbSize.toDouble(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildAppBar(BuildContext context, ListLocationFileBlocState state) {
|
||||||
|
if (isSelectionMode) {
|
||||||
|
return _buildSelectionAppBar(context);
|
||||||
|
} else {
|
||||||
|
return _buildNormalAppBar(context, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildNormalAppBar(
|
||||||
|
BuildContext context, ListLocationFileBlocState state) {
|
||||||
|
return SliverAppBar(
|
||||||
|
floating: true,
|
||||||
|
titleSpacing: 0,
|
||||||
|
title: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
widget.place ?? location_util.alpha2CodeToName(widget.countryCode)!,
|
||||||
|
style: TextStyle(
|
||||||
|
color: AppTheme.getPrimaryTextColor(context),
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
softWrap: false,
|
||||||
|
overflow: TextOverflow.clip,
|
||||||
|
),
|
||||||
|
if (state is! ListLocationFileBlocLoading &&
|
||||||
|
!_buildItemQueue.isProcessing)
|
||||||
|
Text(
|
||||||
|
L10n.global().personPhotoCountText(_backingFiles.length),
|
||||||
|
style: TextStyle(
|
||||||
|
color: AppTheme.getSecondaryTextColor(context),
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
ZoomMenuButton(
|
||||||
|
initialZoom: _thumbZoomLevel,
|
||||||
|
minZoom: 0,
|
||||||
|
maxZoom: 2,
|
||||||
|
onZoomChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_thumbZoomLevel = value.round();
|
||||||
|
});
|
||||||
|
Pref().setAlbumBrowserZoomLevel(_thumbZoomLevel);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => const AboutGeocodingDialog(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.info_outline),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSelectionAppBar(BuildContext context) {
|
||||||
|
return SelectionAppBar(
|
||||||
|
count: selectedListItems.length,
|
||||||
|
onClosePressed: () {
|
||||||
|
setState(() {
|
||||||
|
clearSelectedItems();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.share),
|
||||||
|
tooltip: L10n.global().shareTooltip,
|
||||||
|
onPressed: () {
|
||||||
|
_onSelectionSharePressed(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
tooltip: L10n.global().addToAlbumTooltip,
|
||||||
|
onPressed: () {
|
||||||
|
_onSelectionAddToAlbumPressed(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
PopupMenuButton<_SelectionMenuOption>(
|
||||||
|
tooltip: MaterialLocalizations.of(context).moreButtonTooltip,
|
||||||
|
itemBuilder: (context) => [
|
||||||
|
PopupMenuItem(
|
||||||
|
value: _SelectionMenuOption.download,
|
||||||
|
child: Text(L10n.global().downloadTooltip),
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
value: _SelectionMenuOption.archive,
|
||||||
|
child: Text(L10n.global().archiveTooltip),
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
value: _SelectionMenuOption.delete,
|
||||||
|
child: Text(L10n.global().deleteTooltip),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onSelected: (option) {
|
||||||
|
_onSelectionMenuSelected(context, option);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onStateChange(BuildContext context, ListLocationFileBlocState state) {
|
||||||
|
if (state is ListLocationFileBlocInit) {
|
||||||
|
itemStreamListItems = [];
|
||||||
|
} else if (state is ListLocationFileBlocSuccess ||
|
||||||
|
state is ListLocationFileBlocLoading) {
|
||||||
|
_transformItems(state.items);
|
||||||
|
} else if (state is ListLocationFileBlocFailure) {
|
||||||
|
_transformItems(state.items);
|
||||||
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
|
content: Text(exception_util.toUserString(state.exception)),
|
||||||
|
duration: k.snackBarDurationNormal,
|
||||||
|
));
|
||||||
|
} else if (state is ListLocationFileBlocInconsistent) {
|
||||||
|
_reqQuery();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onSelectionMenuSelected(
|
||||||
|
BuildContext context, _SelectionMenuOption option) {
|
||||||
|
switch (option) {
|
||||||
|
case _SelectionMenuOption.archive:
|
||||||
|
_onSelectionArchivePressed(context);
|
||||||
|
break;
|
||||||
|
case _SelectionMenuOption.delete:
|
||||||
|
_onSelectionDeletePressed(context);
|
||||||
|
break;
|
||||||
|
case _SelectionMenuOption.download:
|
||||||
|
_onSelectionDownloadPressed();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
_log.shout("[_onSelectionMenuSelected] Unknown option: $option");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onSelectionSharePressed(BuildContext context) {
|
||||||
|
final selected = selectedListItems
|
||||||
|
.whereType<PhotoListFileItem>()
|
||||||
|
.map((e) => e.file)
|
||||||
|
.toList();
|
||||||
|
ShareHandler(
|
||||||
|
context: context,
|
||||||
|
clearSelection: () {
|
||||||
|
setState(() {
|
||||||
|
clearSelectedItems();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
).shareFiles(widget.account, selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onSelectionAddToAlbumPressed(BuildContext context) {
|
||||||
|
return AddSelectionToAlbumHandler()(
|
||||||
|
context: context,
|
||||||
|
account: widget.account,
|
||||||
|
selectedFiles: selectedListItems
|
||||||
|
.whereType<PhotoListFileItem>()
|
||||||
|
.map((e) => e.file)
|
||||||
|
.toList(),
|
||||||
|
clearSelection: () {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
clearSelectedItems();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onSelectionDownloadPressed() {
|
||||||
|
final selected = selectedListItems
|
||||||
|
.whereType<PhotoListFileItem>()
|
||||||
|
.map((e) => e.file)
|
||||||
|
.toList();
|
||||||
|
DownloadHandler().downloadFiles(widget.account, selected);
|
||||||
|
setState(() {
|
||||||
|
clearSelectedItems();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onSelectionArchivePressed(BuildContext context) async {
|
||||||
|
final selectedFiles = selectedListItems
|
||||||
|
.whereType<PhotoListFileItem>()
|
||||||
|
.map((e) => e.file)
|
||||||
|
.toList();
|
||||||
|
setState(() {
|
||||||
|
clearSelectedItems();
|
||||||
|
});
|
||||||
|
await ArchiveSelectionHandler(KiwiContainer().resolve<DiContainer>())(
|
||||||
|
account: widget.account,
|
||||||
|
selectedFiles: selectedFiles,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onSelectionDeletePressed(BuildContext context) async {
|
||||||
|
final selectedFiles = selectedListItems
|
||||||
|
.whereType<PhotoListFileItem>()
|
||||||
|
.map((e) => e.file)
|
||||||
|
.toList();
|
||||||
|
setState(() {
|
||||||
|
clearSelectedItems();
|
||||||
|
});
|
||||||
|
await RemoveSelectionHandler()(
|
||||||
|
account: widget.account,
|
||||||
|
selectedFiles: selectedFiles,
|
||||||
|
isMoveToTrash: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _transformItems(List<File> files) async {
|
||||||
|
final PhotoListItemSorter? sorter;
|
||||||
|
final PhotoListItemGrouper? grouper;
|
||||||
|
if (Pref().isPhotosTabSortByNameOr()) {
|
||||||
|
sorter = photoListFilenameSorter;
|
||||||
|
grouper = null;
|
||||||
|
} else {
|
||||||
|
sorter = photoListFileDateTimeSorter;
|
||||||
|
grouper = PhotoListFileDateGrouper(isMonthOnly: _thumbZoomLevel < 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
_buildItemQueue.addJob(
|
||||||
|
PhotoListItemBuilderArguments(
|
||||||
|
widget.account,
|
||||||
|
files,
|
||||||
|
sorter: sorter,
|
||||||
|
grouper: grouper,
|
||||||
|
shouldShowFavoriteBadge: true,
|
||||||
|
locale: language_util.getSelectedLocale() ??
|
||||||
|
PlatformDispatcher.instance.locale,
|
||||||
|
),
|
||||||
|
buildPhotoListItem,
|
||||||
|
(result) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_backingFiles = result.backingFiles;
|
||||||
|
itemStreamListItems = result.listItems;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _reqQuery() {
|
||||||
|
_bloc.add(ListLocationFileBlocQuery(
|
||||||
|
widget.account, widget.place, widget.countryCode));
|
||||||
|
}
|
||||||
|
|
||||||
|
late final DiContainer _c;
|
||||||
|
|
||||||
|
late final ListLocationFileBloc _bloc = ListLocationFileBloc(_c);
|
||||||
|
var _backingFiles = <File>[];
|
||||||
|
|
||||||
|
final _buildItemQueue =
|
||||||
|
ComputeQueue<PhotoListItemBuilderArguments, PhotoListItemBuilderResult>();
|
||||||
|
|
||||||
|
var _thumbZoomLevel = 0;
|
||||||
|
int get _thumbSize => photo_list_util.getThumbSize(_thumbZoomLevel);
|
||||||
|
|
||||||
|
static final _log = Logger("widget.place_browser._PlaceBrowserState");
|
||||||
|
}
|
||||||
|
|
||||||
|
enum _SelectionMenuOption {
|
||||||
|
archive,
|
||||||
|
delete,
|
||||||
|
download,
|
||||||
|
}
|
349
app/lib/widget/places_browser.dart
Normal file
349
app/lib/widget/places_browser.dart
Normal file
|
@ -0,0 +1,349 @@
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:cached_network_image_platform_interface/cached_network_image_platform_interface.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||||
|
import 'package:kiwi/kiwi.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:nc_photos/account.dart';
|
||||||
|
import 'package:nc_photos/api/api.dart';
|
||||||
|
import 'package:nc_photos/api/api_util.dart' as api_util;
|
||||||
|
import 'package:nc_photos/app_localizations.dart';
|
||||||
|
import 'package:nc_photos/bloc/list_location.dart';
|
||||||
|
import 'package:nc_photos/cache_manager_util.dart';
|
||||||
|
import 'package:nc_photos/di_container.dart';
|
||||||
|
import 'package:nc_photos/exception.dart';
|
||||||
|
import 'package:nc_photos/exception_util.dart' as exception_util;
|
||||||
|
import 'package:nc_photos/k.dart' as k;
|
||||||
|
import 'package:nc_photos/snack_bar_manager.dart';
|
||||||
|
import 'package:nc_photos/theme.dart';
|
||||||
|
import 'package:nc_photos/use_case/list_location_group.dart';
|
||||||
|
import 'package:nc_photos/widget/about_geocoding_dialog.dart';
|
||||||
|
import 'package:nc_photos/widget/collection_list_item.dart';
|
||||||
|
import 'package:nc_photos/widget/place_browser.dart';
|
||||||
|
|
||||||
|
class PlacesBrowserArguments {
|
||||||
|
const PlacesBrowserArguments(this.account);
|
||||||
|
|
||||||
|
final Account account;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show a list of all people associated with this account
|
||||||
|
class PlacesBrowser extends StatefulWidget {
|
||||||
|
static const routeName = "/places-browser";
|
||||||
|
|
||||||
|
static Route buildRoute(PlacesBrowserArguments args) => MaterialPageRoute(
|
||||||
|
builder: (context) => PlacesBrowser.fromArgs(args),
|
||||||
|
);
|
||||||
|
|
||||||
|
const PlacesBrowser({
|
||||||
|
Key? key,
|
||||||
|
required this.account,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
PlacesBrowser.fromArgs(PlacesBrowserArguments args, {Key? key})
|
||||||
|
: this(
|
||||||
|
key: key,
|
||||||
|
account: args.account,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
createState() => _PlacesBrowserState();
|
||||||
|
|
||||||
|
final Account account;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PlacesBrowserState extends State<PlacesBrowser> {
|
||||||
|
@override
|
||||||
|
initState() {
|
||||||
|
super.initState();
|
||||||
|
_initBloc();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
build(BuildContext context) {
|
||||||
|
return AppTheme(
|
||||||
|
child: Scaffold(
|
||||||
|
body: BlocListener<ListLocationBloc, ListLocationBlocState>(
|
||||||
|
bloc: _bloc,
|
||||||
|
listener: (context, state) => _onStateChange(context, state),
|
||||||
|
child: BlocBuilder<ListLocationBloc, ListLocationBlocState>(
|
||||||
|
bloc: _bloc,
|
||||||
|
builder: (context, state) => _buildContent(context, state),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _initBloc() {
|
||||||
|
if (_bloc.state is ListLocationBlocInit) {
|
||||||
|
_log.info("[_initBloc] Initialize bloc");
|
||||||
|
} else {
|
||||||
|
// process the current state
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
setState(() {
|
||||||
|
_onStateChange(context, _bloc.state);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_reqQuery();
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildContent(BuildContext context, ListLocationBlocState state) {
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
Theme(
|
||||||
|
data: Theme.of(context).copyWith(
|
||||||
|
colorScheme: Theme.of(context).colorScheme.copyWith(
|
||||||
|
secondary: AppTheme.getOverscrollIndicatorColor(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
_buildAppBar(context),
|
||||||
|
if (state is ListLocationBlocLoading)
|
||||||
|
const SliverToBoxAdapter(
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: LinearProgressIndicator(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: SizedBox(
|
||||||
|
height: 48,
|
||||||
|
child: ListView.separated(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
itemCount: _countryItems.length,
|
||||||
|
itemBuilder: (context, i) =>
|
||||||
|
_countryItems[i].buildWidget(context),
|
||||||
|
separatorBuilder: (_, __) => const SizedBox(width: 8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SliverToBoxAdapter(
|
||||||
|
child: SizedBox(height: 8),
|
||||||
|
),
|
||||||
|
SliverStaggeredGrid.extentBuilder(
|
||||||
|
maxCrossAxisExtent: 160,
|
||||||
|
mainAxisSpacing: 2,
|
||||||
|
crossAxisSpacing: 2,
|
||||||
|
itemCount: _placeItems.length,
|
||||||
|
itemBuilder: (context, i) =>
|
||||||
|
_placeItems[i].buildWidget(context),
|
||||||
|
staggeredTileBuilder: (_) => const StaggeredTile.count(1, 1),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildAppBar(BuildContext context) {
|
||||||
|
return SliverAppBar(
|
||||||
|
title: Text(L10n.global().collectionPlacesLabel),
|
||||||
|
floating: true,
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => const AboutGeocodingDialog(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.info_outline),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onStateChange(BuildContext context, ListLocationBlocState state) {
|
||||||
|
if (state is ListLocationBlocInit) {
|
||||||
|
_placeItems = [];
|
||||||
|
_countryItems = [];
|
||||||
|
} else if (state is ListLocationBlocSuccess ||
|
||||||
|
state is ListLocationBlocLoading) {
|
||||||
|
_transformItems(state.result);
|
||||||
|
} else if (state is ListLocationBlocFailure) {
|
||||||
|
_transformItems(state.result);
|
||||||
|
try {
|
||||||
|
final e = state.exception as ApiException;
|
||||||
|
if (e.response.statusCode == 404) {
|
||||||
|
// face recognition app probably not installed, ignore
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
|
content: Text(exception_util.toUserString(state.exception)),
|
||||||
|
duration: k.snackBarDurationNormal,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onPlaceTap(LocationGroup location) {
|
||||||
|
Navigator.pushNamed(context, PlaceBrowser.routeName,
|
||||||
|
arguments: PlaceBrowserArguments(
|
||||||
|
widget.account, location.place, location.countryCode));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onCountryTap(LocationGroup location) {
|
||||||
|
Navigator.pushNamed(context, PlaceBrowser.routeName,
|
||||||
|
arguments:
|
||||||
|
PlaceBrowserArguments(widget.account, null, location.countryCode));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _transformItems(LocationGroupResult? result) {
|
||||||
|
if (result == null) {
|
||||||
|
_placeItems = [];
|
||||||
|
_countryItems = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sorter(LocationGroup a, LocationGroup b) {
|
||||||
|
final compare = b.count.compareTo(a.count);
|
||||||
|
if (compare == 0) {
|
||||||
|
return a.place.compareTo(b.place);
|
||||||
|
} else {
|
||||||
|
return compare;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_placeItems = result.name
|
||||||
|
.sorted(sorter)
|
||||||
|
.map((e) => _PlaceItem(
|
||||||
|
account: widget.account,
|
||||||
|
place: e.place,
|
||||||
|
thumbUrl: api_util.getFilePreviewUrlByFileId(
|
||||||
|
widget.account,
|
||||||
|
e.latestFileId,
|
||||||
|
width: k.photoThumbSize,
|
||||||
|
height: k.photoThumbSize,
|
||||||
|
),
|
||||||
|
onTap: () => _onPlaceTap(e),
|
||||||
|
))
|
||||||
|
.toList();
|
||||||
|
_countryItems = result.countryCode
|
||||||
|
.sorted(sorter)
|
||||||
|
.map((e) => _CountryItem(
|
||||||
|
account: widget.account,
|
||||||
|
country: e.place,
|
||||||
|
thumbUrl: api_util.getFilePreviewUrlByFileId(
|
||||||
|
widget.account,
|
||||||
|
e.latestFileId,
|
||||||
|
width: k.photoThumbSize,
|
||||||
|
height: k.photoThumbSize,
|
||||||
|
),
|
||||||
|
onTap: () => _onCountryTap(e),
|
||||||
|
))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _reqQuery() {
|
||||||
|
_bloc.add(ListLocationBlocQuery(widget.account));
|
||||||
|
}
|
||||||
|
|
||||||
|
late final _bloc = ListLocationBloc(KiwiContainer().resolve<DiContainer>());
|
||||||
|
|
||||||
|
var _placeItems = <_PlaceItem>[];
|
||||||
|
var _countryItems = <_CountryItem>[];
|
||||||
|
|
||||||
|
static final _log = Logger("widget.places_browser._PlacesBrowserState");
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PlaceItem {
|
||||||
|
const _PlaceItem({
|
||||||
|
required this.account,
|
||||||
|
required this.place,
|
||||||
|
required this.thumbUrl,
|
||||||
|
this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
Widget buildWidget(BuildContext context) => CollectionListSmall(
|
||||||
|
account: account,
|
||||||
|
label: place,
|
||||||
|
coverUrl: thumbUrl,
|
||||||
|
fallbackBuilder: (_) => Icon(
|
||||||
|
Icons.location_on,
|
||||||
|
color: Colors.white.withOpacity(.8),
|
||||||
|
),
|
||||||
|
onTap: onTap,
|
||||||
|
);
|
||||||
|
|
||||||
|
final Account account;
|
||||||
|
final String place;
|
||||||
|
final String thumbUrl;
|
||||||
|
final VoidCallback? onTap;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CountryItem {
|
||||||
|
const _CountryItem({
|
||||||
|
required this.account,
|
||||||
|
required this.country,
|
||||||
|
required this.thumbUrl,
|
||||||
|
this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
Widget buildWidget(BuildContext context) {
|
||||||
|
return ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
CachedNetworkImage(
|
||||||
|
cacheManager: ThumbnailCacheManager.inst,
|
||||||
|
imageUrl: thumbUrl,
|
||||||
|
httpHeaders: {
|
||||||
|
"Authorization": Api.getAuthorizationHeaderValue(account),
|
||||||
|
},
|
||||||
|
fadeInDuration: const Duration(),
|
||||||
|
filterQuality: FilterQuality.high,
|
||||||
|
errorWidget: (_, __, ___) => Padding(
|
||||||
|
padding: const EdgeInsetsDirectional.fromSTEB(8, 8, 0, 8),
|
||||||
|
child: Icon(
|
||||||
|
Icons.location_on,
|
||||||
|
color: AppTheme.getUnfocusedIconColor(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(country),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Positioned.fill(
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color: AppTheme.getListItemBackgroundColor(context),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (onTap != null)
|
||||||
|
Positioned.fill(
|
||||||
|
child: Material(
|
||||||
|
type: MaterialType.transparency,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: onTap,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Account account;
|
||||||
|
final String country;
|
||||||
|
final String thumbUrl;
|
||||||
|
final VoidCallback? onTap;
|
||||||
|
}
|
|
@ -21,8 +21,11 @@ import 'package:nc_photos/pref.dart';
|
||||||
import 'package:nc_photos/snack_bar_manager.dart';
|
import 'package:nc_photos/snack_bar_manager.dart';
|
||||||
import 'package:nc_photos/theme.dart';
|
import 'package:nc_photos/theme.dart';
|
||||||
import 'package:nc_photos/url_launcher_util.dart';
|
import 'package:nc_photos/url_launcher_util.dart';
|
||||||
|
import 'package:nc_photos/use_case/list_location_group.dart';
|
||||||
import 'package:nc_photos/widget/people_browser.dart';
|
import 'package:nc_photos/widget/people_browser.dart';
|
||||||
import 'package:nc_photos/widget/person_browser.dart';
|
import 'package:nc_photos/widget/person_browser.dart';
|
||||||
|
import 'package:nc_photos/widget/place_browser.dart';
|
||||||
|
import 'package:nc_photos/widget/places_browser.dart';
|
||||||
|
|
||||||
class SearchLanding extends StatefulWidget {
|
class SearchLanding extends StatefulWidget {
|
||||||
const SearchLanding({
|
const SearchLanding({
|
||||||
|
@ -85,6 +88,7 @@ class _SearchLandingState extends State<SearchLanding> {
|
||||||
children: [
|
children: [
|
||||||
if (AccountPref.of(widget.account).isEnableFaceRecognitionAppOr())
|
if (AccountPref.of(widget.account).isEnableFaceRecognitionAppOr())
|
||||||
..._buildPeopleSection(context, state),
|
..._buildPeopleSection(context, state),
|
||||||
|
..._buildLocationSection(context, state),
|
||||||
ListTile(
|
ListTile(
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
title: Text(L10n.global().categoriesLabel),
|
title: Text(L10n.global().categoriesLabel),
|
||||||
|
@ -113,7 +117,7 @@ class _SearchLandingState extends State<SearchLanding> {
|
||||||
BuildContext context, SearchLandingBlocState state) {
|
BuildContext context, SearchLandingBlocState state) {
|
||||||
final isNoResult = (state is SearchLandingBlocSuccess ||
|
final isNoResult = (state is SearchLandingBlocSuccess ||
|
||||||
state is SearchLandingBlocFailure) &&
|
state is SearchLandingBlocFailure) &&
|
||||||
state.persons.isEmpty;
|
_personItems.isEmpty;
|
||||||
return [
|
return [
|
||||||
ListTile(
|
ListTile(
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
@ -147,26 +151,61 @@ class _SearchLandingState extends State<SearchLanding> {
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
itemCount: _items.length,
|
itemCount: _personItems.length,
|
||||||
itemBuilder: (context, i) => _buildItem(context, i),
|
itemBuilder: (context, i) => _personItems[i].buildWidget(context),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildItem(BuildContext context, int index) {
|
List<Widget> _buildLocationSection(
|
||||||
final item = _items[index];
|
BuildContext context, SearchLandingBlocState state) {
|
||||||
return item.buildWidget(context);
|
final isNoResult = (state is SearchLandingBlocSuccess ||
|
||||||
|
state is SearchLandingBlocFailure) &&
|
||||||
|
_locationItems.isEmpty;
|
||||||
|
return [
|
||||||
|
ListTile(
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
title: Text(L10n.global().collectionPlacesLabel),
|
||||||
|
trailing: isNoResult
|
||||||
|
? null
|
||||||
|
: TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pushNamed(PlacesBrowser.routeName,
|
||||||
|
arguments: PlacesBrowserArguments(widget.account));
|
||||||
|
},
|
||||||
|
child: Text(L10n.global().showAllButtonLabel),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (isNoResult)
|
||||||
|
SizedBox(
|
||||||
|
height: 48,
|
||||||
|
child: Center(
|
||||||
|
child: Text(L10n.global().listNoResultsText),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
SizedBox(
|
||||||
|
height: 128,
|
||||||
|
child: ListView.builder(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
itemCount: _locationItems.length,
|
||||||
|
itemBuilder: (context, i) => _locationItems[i].buildWidget(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onStateChange(BuildContext context, SearchLandingBlocState state) {
|
void _onStateChange(BuildContext context, SearchLandingBlocState state) {
|
||||||
if (state is SearchLandingBlocInit) {
|
if (state is SearchLandingBlocInit) {
|
||||||
_items = [];
|
_personItems = [];
|
||||||
|
_locationItems = [];
|
||||||
} else if (state is SearchLandingBlocSuccess ||
|
} else if (state is SearchLandingBlocSuccess ||
|
||||||
state is SearchLandingBlocLoading) {
|
state is SearchLandingBlocLoading) {
|
||||||
_transformItems(state.persons);
|
_transformItems(state.persons, state.locations);
|
||||||
} else if (state is SearchLandingBlocFailure) {
|
} else if (state is SearchLandingBlocFailure) {
|
||||||
_transformItems(state.persons);
|
_transformItems(state.persons, state.locations);
|
||||||
try {
|
try {
|
||||||
final e = state.exception as ApiException;
|
final e = state.exception as ApiException;
|
||||||
if (e.response.statusCode == 404) {
|
if (e.response.statusCode == 404) {
|
||||||
|
@ -189,13 +228,26 @@ class _SearchLandingState extends State<SearchLanding> {
|
||||||
widget.onVideoPressed?.call();
|
widget.onVideoPressed?.call();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onItemTap(Person person) {
|
void _onPersonItemTap(Person person) {
|
||||||
Navigator.pushNamed(context, PersonBrowser.routeName,
|
Navigator.pushNamed(context, PersonBrowser.routeName,
|
||||||
arguments: PersonBrowserArguments(widget.account, person));
|
arguments: PersonBrowserArguments(widget.account, person));
|
||||||
}
|
}
|
||||||
|
|
||||||
void _transformItems(List<Person> items) {
|
void _onLocationItemTap(LocationGroup location) {
|
||||||
_items = items
|
Navigator.of(context).pushNamed(
|
||||||
|
PlaceBrowser.routeName,
|
||||||
|
arguments: PlaceBrowserArguments(
|
||||||
|
widget.account, location.place, location.countryCode),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _transformItems(List<Person> persons, LocationGroupResult locations) {
|
||||||
|
_transformPersons(persons);
|
||||||
|
_transformLocations(locations);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _transformPersons(List<Person> persons) {
|
||||||
|
_personItems = persons
|
||||||
.sorted((a, b) {
|
.sorted((a, b) {
|
||||||
final countCompare = b.count.compareTo(a.count);
|
final countCompare = b.count.compareTo(a.count);
|
||||||
if (countCompare == 0) {
|
if (countCompare == 0) {
|
||||||
|
@ -209,7 +261,31 @@ class _SearchLandingState extends State<SearchLanding> {
|
||||||
name: e.name,
|
name: e.name,
|
||||||
faceUrl: api_util.getFacePreviewUrl(widget.account, e.thumbFaceId,
|
faceUrl: api_util.getFacePreviewUrl(widget.account, e.thumbFaceId,
|
||||||
size: k.faceThumbSize),
|
size: k.faceThumbSize),
|
||||||
onTap: () => _onItemTap(e),
|
onTap: () => _onPersonItemTap(e),
|
||||||
|
))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _transformLocations(LocationGroupResult locations) {
|
||||||
|
_locationItems = locations.name
|
||||||
|
.sorted((a, b) {
|
||||||
|
final compare = b.count.compareTo(a.count);
|
||||||
|
if (compare == 0) {
|
||||||
|
return a.place.compareTo(b.place);
|
||||||
|
} else {
|
||||||
|
return compare;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map((e) => _LandingLocationItem(
|
||||||
|
account: widget.account,
|
||||||
|
name: e.place,
|
||||||
|
thumbUrl: api_util.getFilePreviewUrlByFileId(
|
||||||
|
widget.account,
|
||||||
|
e.latestFileId,
|
||||||
|
width: k.photoThumbSize,
|
||||||
|
height: k.photoThumbSize,
|
||||||
|
),
|
||||||
|
onTap: () => _onLocationItemTap(e),
|
||||||
))
|
))
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
@ -220,7 +296,8 @@ class _SearchLandingState extends State<SearchLanding> {
|
||||||
|
|
||||||
late final _bloc = SearchLandingBloc(KiwiContainer().resolve<DiContainer>());
|
late final _bloc = SearchLandingBloc(KiwiContainer().resolve<DiContainer>());
|
||||||
|
|
||||||
var _items = <_LandingPersonItem>[];
|
var _personItems = <_LandingPersonItem>[];
|
||||||
|
var _locationItems = <_LandingLocationItem>[];
|
||||||
|
|
||||||
static final _log = Logger("widget.search_landing._SearchLandingState");
|
static final _log = Logger("widget.search_landing._SearchLandingState");
|
||||||
}
|
}
|
||||||
|
@ -250,6 +327,31 @@ class _LandingPersonItem {
|
||||||
final VoidCallback? onTap;
|
final VoidCallback? onTap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _LandingLocationItem {
|
||||||
|
_LandingLocationItem({
|
||||||
|
required this.account,
|
||||||
|
required this.name,
|
||||||
|
required this.thumbUrl,
|
||||||
|
this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
Widget buildWidget(BuildContext context) => _LandingItemWidget(
|
||||||
|
account: account,
|
||||||
|
label: name,
|
||||||
|
coverUrl: thumbUrl,
|
||||||
|
onTap: onTap,
|
||||||
|
fallbackBuilder: (_) => Icon(
|
||||||
|
Icons.location_on,
|
||||||
|
color: Colors.white.withOpacity(.8),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Account account;
|
||||||
|
final String name;
|
||||||
|
final String thumbUrl;
|
||||||
|
final VoidCallback? onTap;
|
||||||
|
}
|
||||||
|
|
||||||
class _LandingItemWidget extends StatelessWidget {
|
class _LandingItemWidget extends StatelessWidget {
|
||||||
const _LandingItemWidget({
|
const _LandingItemWidget({
|
||||||
Key? key,
|
Key? key,
|
||||||
|
|
|
@ -17,7 +17,9 @@ import 'package:nc_photos/entity/album/provider.dart';
|
||||||
import 'package:nc_photos/entity/exif_extension.dart';
|
import 'package:nc_photos/entity/exif_extension.dart';
|
||||||
import 'package:nc_photos/entity/file.dart';
|
import 'package:nc_photos/entity/file.dart';
|
||||||
import 'package:nc_photos/k.dart' as k;
|
import 'package:nc_photos/k.dart' as k;
|
||||||
|
import 'package:nc_photos/location_util.dart' as location_util;
|
||||||
import 'package:nc_photos/notified_action.dart';
|
import 'package:nc_photos/notified_action.dart';
|
||||||
|
import 'package:nc_photos/object_extension.dart';
|
||||||
import 'package:nc_photos/platform/features.dart' as features;
|
import 'package:nc_photos/platform/features.dart' as features;
|
||||||
import 'package:nc_photos/platform/k.dart' as platform_k;
|
import 'package:nc_photos/platform/k.dart' as platform_k;
|
||||||
import 'package:nc_photos/snack_bar_manager.dart';
|
import 'package:nc_photos/snack_bar_manager.dart';
|
||||||
|
@ -26,6 +28,7 @@ import 'package:nc_photos/use_case/list_file_tag.dart';
|
||||||
import 'package:nc_photos/use_case/remove_from_album.dart';
|
import 'package:nc_photos/use_case/remove_from_album.dart';
|
||||||
import 'package:nc_photos/use_case/update_album.dart';
|
import 'package:nc_photos/use_case/update_album.dart';
|
||||||
import 'package:nc_photos/use_case/update_property.dart';
|
import 'package:nc_photos/use_case/update_property.dart';
|
||||||
|
import 'package:nc_photos/widget/about_geocoding_dialog.dart';
|
||||||
import 'package:nc_photos/widget/animated_visibility.dart';
|
import 'package:nc_photos/widget/animated_visibility.dart';
|
||||||
import 'package:nc_photos/widget/gps_map.dart';
|
import 'package:nc_photos/widget/gps_map.dart';
|
||||||
import 'package:nc_photos/widget/handler/add_selection_to_album_handler.dart';
|
import 'package:nc_photos/widget/handler/add_selection_to_album_handler.dart';
|
||||||
|
@ -291,6 +294,27 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
||||||
title: Text(_model!),
|
title: Text(_model!),
|
||||||
subtitle: cameraSubStr.isNotEmpty ? Text(cameraSubStr) : null,
|
subtitle: cameraSubStr.isNotEmpty ? Text(cameraSubStr) : null,
|
||||||
),
|
),
|
||||||
|
if (_location?.name != null)
|
||||||
|
ListTile(
|
||||||
|
leading: ListTileCenterLeading(
|
||||||
|
child: Icon(
|
||||||
|
Icons.location_on_outlined,
|
||||||
|
color: AppTheme.getSecondaryTextColor(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: Text(L10n.global().gpsPlaceText(_location!.name!)),
|
||||||
|
subtitle: _location!.toSubtitle()?.run((obj) => Text(obj)),
|
||||||
|
trailing: Icon(
|
||||||
|
Icons.info_outline,
|
||||||
|
color: AppTheme.getSecondaryTextColor(context),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => const AboutGeocodingDialog(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
if (features.isSupportMapView && _gps != null)
|
if (features.isSupportMapView && _gps != null)
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
opacity: _shouldBlockGpsMap ? 0 : 1,
|
opacity: _shouldBlockGpsMap ? 0 : 1,
|
||||||
|
@ -339,6 +363,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
||||||
if (lat != null && lng != null) {
|
if (lat != null && lng != null) {
|
||||||
_log.fine("GPS: ($lat, $lng)");
|
_log.fine("GPS: ($lat, $lng)");
|
||||||
_gps = Tuple2(lat, lng);
|
_gps = Tuple2(lat, lng);
|
||||||
|
_location = widget.file.location;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -520,6 +545,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
||||||
double? _focalLength;
|
double? _focalLength;
|
||||||
int? _isoSpeedRatings;
|
int? _isoSpeedRatings;
|
||||||
Tuple2<double, double>? _gps;
|
Tuple2<double, double>? _gps;
|
||||||
|
ImageLocation? _location;
|
||||||
|
|
||||||
final _tags = <String>[];
|
final _tags = <String>[];
|
||||||
|
|
||||||
|
@ -589,3 +615,17 @@ String _byteSizeToString(int byteSize) {
|
||||||
}
|
}
|
||||||
return "${remain.toStringAsFixed(2)}${units[i]}";
|
return "${remain.toStringAsFixed(2)}${units[i]}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension on ImageLocation {
|
||||||
|
String? toSubtitle() {
|
||||||
|
if (countryCode == null) {
|
||||||
|
return null;
|
||||||
|
} else if (admin1 == null) {
|
||||||
|
return location_util.alpha2CodeToName(countryCode!);
|
||||||
|
} else if (admin2 == null) {
|
||||||
|
return "$admin1, ${location_util.alpha2CodeToName(countryCode!)}";
|
||||||
|
} else {
|
||||||
|
return "$admin2, $admin1, ${location_util.alpha2CodeToName(countryCode!)}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -679,6 +679,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.6.0"
|
version: "4.6.0"
|
||||||
|
kdtree:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: kdtree
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.0"
|
||||||
kiwi:
|
kiwi:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -1161,7 +1168,7 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.1+1"
|
version: "2.2.1+1"
|
||||||
sqlite3:
|
sqlite3:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: sqlite3
|
name: sqlite3
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
|
|
|
@ -76,6 +76,7 @@ dependencies:
|
||||||
ref: 1.0.0-nc-photos-2
|
ref: 1.0.0-nc-photos-2
|
||||||
path: library
|
path: library
|
||||||
intl: ^0.17.0
|
intl: ^0.17.0
|
||||||
|
kdtree: ^0.2.0
|
||||||
kiwi: ^4.0.1
|
kiwi: ^4.0.1
|
||||||
logging: ^1.0.1
|
logging: ^1.0.1
|
||||||
memory_info: ^0.0.2
|
memory_info: ^0.0.2
|
||||||
|
@ -90,6 +91,7 @@ dependencies:
|
||||||
quiver: ^3.1.0
|
quiver: ^3.1.0
|
||||||
screen_brightness: ^0.2.1
|
screen_brightness: ^0.2.1
|
||||||
shared_preferences: ^2.0.8
|
shared_preferences: ^2.0.8
|
||||||
|
sqlite3: any
|
||||||
sqlite3_flutter_libs: ^0.5.8
|
sqlite3_flutter_libs: ^0.5.8
|
||||||
synchronized: ^3.0.0
|
synchronized: ^3.0.0
|
||||||
tuple: ^2.0.0
|
tuple: ^2.0.0
|
||||||
|
|
|
@ -313,6 +313,7 @@ Future<void> _truncate() async {
|
||||||
"accounts",
|
"accounts",
|
||||||
"files",
|
"files",
|
||||||
"images",
|
"images",
|
||||||
|
"image_locations",
|
||||||
"trashes",
|
"trashes",
|
||||||
"account_files",
|
"account_files",
|
||||||
"dir_files",
|
"dir_files",
|
||||||
|
|
|
@ -181,6 +181,7 @@ class MockFileDataSource implements FileDataSource {
|
||||||
OrNull<bool>? isArchived,
|
OrNull<bool>? isArchived,
|
||||||
OrNull<DateTime>? overrideDateTime,
|
OrNull<DateTime>? overrideDateTime,
|
||||||
bool? favorite,
|
bool? favorite,
|
||||||
|
OrNull<ImageLocation>? location,
|
||||||
}) {
|
}) {
|
||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
}
|
}
|
||||||
|
@ -272,6 +273,7 @@ class MockFileWebdavDataSource implements FileWebdavDataSource {
|
||||||
OrNull<bool>? isArchived,
|
OrNull<bool>? isArchived,
|
||||||
OrNull<DateTime>? overrideDateTime,
|
OrNull<DateTime>? overrideDateTime,
|
||||||
bool? favorite,
|
bool? favorite,
|
||||||
|
OrNull<ImageLocation>? location,
|
||||||
}) =>
|
}) =>
|
||||||
src.updateProperty(
|
src.updateProperty(
|
||||||
account,
|
account,
|
||||||
|
@ -280,6 +282,7 @@ class MockFileWebdavDataSource implements FileWebdavDataSource {
|
||||||
isArchived: isArchived,
|
isArchived: isArchived,
|
||||||
overrideDateTime: overrideDateTime,
|
overrideDateTime: overrideDateTime,
|
||||||
favorite: favorite,
|
favorite: favorite,
|
||||||
|
location: location,
|
||||||
);
|
);
|
||||||
|
|
||||||
final MockFileMemoryDataSource src;
|
final MockFileMemoryDataSource src;
|
||||||
|
|
|
@ -447,6 +447,10 @@ Future<void> insertFiles(
|
||||||
await db.into(db.images).insert(
|
await db.into(db.images).insert(
|
||||||
insert.image!.copyWith(accountFile: sql.Value(dbAccountFile.rowId)));
|
insert.image!.copyWith(accountFile: sql.Value(dbAccountFile.rowId)));
|
||||||
}
|
}
|
||||||
|
if (insert.imageLocation != null) {
|
||||||
|
await db.into(db.imageLocations).insert(insert.imageLocation!
|
||||||
|
.copyWith(accountFile: sql.Value(dbAccountFile.rowId)));
|
||||||
|
}
|
||||||
if (insert.trash != null) {
|
if (insert.trash != null) {
|
||||||
await db
|
await db
|
||||||
.into(db.trashes)
|
.into(db.trashes)
|
||||||
|
@ -499,6 +503,8 @@ Future<Set<File>> listSqliteDbFiles(sql.SqliteDb db) async {
|
||||||
db.accounts, db.accounts.rowId.equalsExp(db.accountFiles.account)),
|
db.accounts, db.accounts.rowId.equalsExp(db.accountFiles.account)),
|
||||||
sql.leftOuterJoin(
|
sql.leftOuterJoin(
|
||||||
db.images, db.images.accountFile.equalsExp(db.accountFiles.rowId)),
|
db.images, db.images.accountFile.equalsExp(db.accountFiles.rowId)),
|
||||||
|
sql.leftOuterJoin(db.imageLocations,
|
||||||
|
db.imageLocations.accountFile.equalsExp(db.accountFiles.rowId)),
|
||||||
sql.leftOuterJoin(db.trashes, db.trashes.file.equalsExp(db.files.rowId)),
|
sql.leftOuterJoin(db.trashes, db.trashes.file.equalsExp(db.files.rowId)),
|
||||||
]);
|
]);
|
||||||
return (await query
|
return (await query
|
||||||
|
@ -508,6 +514,7 @@ Future<Set<File>> listSqliteDbFiles(sql.SqliteDb db) async {
|
||||||
r.readTable(db.files),
|
r.readTable(db.files),
|
||||||
r.readTable(db.accountFiles),
|
r.readTable(db.accountFiles),
|
||||||
r.readTableOrNull(db.images),
|
r.readTableOrNull(db.images),
|
||||||
|
r.readTableOrNull(db.imageLocations),
|
||||||
r.readTableOrNull(db.trashes),
|
r.readTableOrNull(db.trashes),
|
||||||
),
|
),
|
||||||
))
|
))
|
||||||
|
@ -523,6 +530,8 @@ Future<Map<File, Set<File>>> listSqliteDbDirs(sql.SqliteDb db) async {
|
||||||
db.accounts, db.accounts.rowId.equalsExp(db.accountFiles.account)),
|
db.accounts, db.accounts.rowId.equalsExp(db.accountFiles.account)),
|
||||||
sql.leftOuterJoin(
|
sql.leftOuterJoin(
|
||||||
db.images, db.images.accountFile.equalsExp(db.accountFiles.rowId)),
|
db.images, db.images.accountFile.equalsExp(db.accountFiles.rowId)),
|
||||||
|
sql.leftOuterJoin(db.imageLocations,
|
||||||
|
db.imageLocations.accountFile.equalsExp(db.accountFiles.rowId)),
|
||||||
sql.leftOuterJoin(db.trashes, db.trashes.file.equalsExp(db.files.rowId)),
|
sql.leftOuterJoin(db.trashes, db.trashes.file.equalsExp(db.files.rowId)),
|
||||||
]);
|
]);
|
||||||
final fileMap = Map.fromEntries(await query.map((r) {
|
final fileMap = Map.fromEntries(await query.map((r) {
|
||||||
|
@ -530,6 +539,7 @@ Future<Map<File, Set<File>>> listSqliteDbDirs(sql.SqliteDb db) async {
|
||||||
r.readTable(db.files),
|
r.readTable(db.files),
|
||||||
r.readTable(db.accountFiles),
|
r.readTable(db.accountFiles),
|
||||||
r.readTableOrNull(db.images),
|
r.readTableOrNull(db.images),
|
||||||
|
r.readTableOrNull(db.imageLocations),
|
||||||
r.readTableOrNull(db.trashes),
|
r.readTableOrNull(db.trashes),
|
||||||
);
|
);
|
||||||
return MapEntry(
|
return MapEntry(
|
||||||
|
@ -563,6 +573,7 @@ Future<Set<Album>> listSqliteDbAlbums(sql.SqliteDb db) async {
|
||||||
r.readTable(db.accountFiles),
|
r.readTable(db.accountFiles),
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
|
null,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return Tuple2(
|
return Tuple2(
|
||||||
|
|
Loading…
Reference in a new issue