mirror of
https://github.com/immich-app/immich.git
synced 2025-03-11 02:23:09 -05:00
feat(web): Global map showing all assets with geo information (#2355)
* First crude implementation of the global asset map in web * Use single DOM element for all markers * Minor layout changes * Refactor * Add asset viewer * Add API endpoint that returns only assets with location information (Thanks @EPP100) * Remove sidebar icon flip * Add dark theme support * Center map to most recent asset * Allow cluster viewing * Fix linter errors * Add newlines * Fix ts errors * Fix eslint error * Run prettier * Server code style * Fix openapi mobile code generation issues * Map markers test * fix: Support video thumbnails * Update API * Review suggestions * Review suggestions * Linter error * Chage mapMarker endpoint to map-marker * Clean up leaflet imports
This commit is contained in:
parent
15a498fd60
commit
65daf342df
28 changed files with 902 additions and 5 deletions
3
mobile/openapi/.openapi-generator/FILES
generated
3
mobile/openapi/.openapi-generator/FILES
generated
|
@ -55,6 +55,7 @@ doc/JobStatusDto.md
|
||||||
doc/LoginCredentialDto.md
|
doc/LoginCredentialDto.md
|
||||||
doc/LoginResponseDto.md
|
doc/LoginResponseDto.md
|
||||||
doc/LogoutResponseDto.md
|
doc/LogoutResponseDto.md
|
||||||
|
doc/MapMarkerResponseDto.md
|
||||||
doc/OAuthApi.md
|
doc/OAuthApi.md
|
||||||
doc/OAuthCallbackDto.md
|
doc/OAuthCallbackDto.md
|
||||||
doc/OAuthConfigDto.md
|
doc/OAuthConfigDto.md
|
||||||
|
@ -171,6 +172,7 @@ lib/model/job_status_dto.dart
|
||||||
lib/model/login_credential_dto.dart
|
lib/model/login_credential_dto.dart
|
||||||
lib/model/login_response_dto.dart
|
lib/model/login_response_dto.dart
|
||||||
lib/model/logout_response_dto.dart
|
lib/model/logout_response_dto.dart
|
||||||
|
lib/model/map_marker_response_dto.dart
|
||||||
lib/model/o_auth_callback_dto.dart
|
lib/model/o_auth_callback_dto.dart
|
||||||
lib/model/o_auth_config_dto.dart
|
lib/model/o_auth_config_dto.dart
|
||||||
lib/model/o_auth_config_response_dto.dart
|
lib/model/o_auth_config_response_dto.dart
|
||||||
|
@ -264,6 +266,7 @@ test/job_status_dto_test.dart
|
||||||
test/login_credential_dto_test.dart
|
test/login_credential_dto_test.dart
|
||||||
test/login_response_dto_test.dart
|
test/login_response_dto_test.dart
|
||||||
test/logout_response_dto_test.dart
|
test/logout_response_dto_test.dart
|
||||||
|
test/map_marker_response_dto_test.dart
|
||||||
test/o_auth_api_test.dart
|
test/o_auth_api_test.dart
|
||||||
test/o_auth_callback_dto_test.dart
|
test/o_auth_callback_dto_test.dart
|
||||||
test/o_auth_config_dto_test.dart
|
test/o_auth_config_dto_test.dart
|
||||||
|
|
2
mobile/openapi/README.md
generated
2
mobile/openapi/README.md
generated
|
@ -107,6 +107,7 @@ Class | Method | HTTP request | Description
|
||||||
*AssetApi* | [**getAssetThumbnail**](doc//AssetApi.md#getassetthumbnail) | **GET** /asset/thumbnail/{assetId} |
|
*AssetApi* | [**getAssetThumbnail**](doc//AssetApi.md#getassetthumbnail) | **GET** /asset/thumbnail/{assetId} |
|
||||||
*AssetApi* | [**getCuratedLocations**](doc//AssetApi.md#getcuratedlocations) | **GET** /asset/curated-locations |
|
*AssetApi* | [**getCuratedLocations**](doc//AssetApi.md#getcuratedlocations) | **GET** /asset/curated-locations |
|
||||||
*AssetApi* | [**getCuratedObjects**](doc//AssetApi.md#getcuratedobjects) | **GET** /asset/curated-objects |
|
*AssetApi* | [**getCuratedObjects**](doc//AssetApi.md#getcuratedobjects) | **GET** /asset/curated-objects |
|
||||||
|
*AssetApi* | [**getMapMarkers**](doc//AssetApi.md#getmapmarkers) | **GET** /asset/map-marker |
|
||||||
*AssetApi* | [**getUserAssetsByDeviceId**](doc//AssetApi.md#getuserassetsbydeviceid) | **GET** /asset/{deviceId} |
|
*AssetApi* | [**getUserAssetsByDeviceId**](doc//AssetApi.md#getuserassetsbydeviceid) | **GET** /asset/{deviceId} |
|
||||||
*AssetApi* | [**removeAssetsFromSharedLink**](doc//AssetApi.md#removeassetsfromsharedlink) | **PATCH** /asset/shared-link/remove |
|
*AssetApi* | [**removeAssetsFromSharedLink**](doc//AssetApi.md#removeassetsfromsharedlink) | **PATCH** /asset/shared-link/remove |
|
||||||
*AssetApi* | [**searchAsset**](doc//AssetApi.md#searchasset) | **POST** /asset/search |
|
*AssetApi* | [**searchAsset**](doc//AssetApi.md#searchasset) | **POST** /asset/search |
|
||||||
|
@ -209,6 +210,7 @@ Class | Method | HTTP request | Description
|
||||||
- [LoginCredentialDto](doc//LoginCredentialDto.md)
|
- [LoginCredentialDto](doc//LoginCredentialDto.md)
|
||||||
- [LoginResponseDto](doc//LoginResponseDto.md)
|
- [LoginResponseDto](doc//LoginResponseDto.md)
|
||||||
- [LogoutResponseDto](doc//LogoutResponseDto.md)
|
- [LogoutResponseDto](doc//LogoutResponseDto.md)
|
||||||
|
- [MapMarkerResponseDto](doc//MapMarkerResponseDto.md)
|
||||||
- [OAuthCallbackDto](doc//OAuthCallbackDto.md)
|
- [OAuthCallbackDto](doc//OAuthCallbackDto.md)
|
||||||
- [OAuthConfigDto](doc//OAuthConfigDto.md)
|
- [OAuthConfigDto](doc//OAuthConfigDto.md)
|
||||||
- [OAuthConfigResponseDto](doc//OAuthConfigResponseDto.md)
|
- [OAuthConfigResponseDto](doc//OAuthConfigResponseDto.md)
|
||||||
|
|
58
mobile/openapi/doc/AssetApi.md
generated
58
mobile/openapi/doc/AssetApi.md
generated
|
@ -27,6 +27,7 @@ Method | HTTP request | Description
|
||||||
[**getAssetThumbnail**](AssetApi.md#getassetthumbnail) | **GET** /asset/thumbnail/{assetId} |
|
[**getAssetThumbnail**](AssetApi.md#getassetthumbnail) | **GET** /asset/thumbnail/{assetId} |
|
||||||
[**getCuratedLocations**](AssetApi.md#getcuratedlocations) | **GET** /asset/curated-locations |
|
[**getCuratedLocations**](AssetApi.md#getcuratedlocations) | **GET** /asset/curated-locations |
|
||||||
[**getCuratedObjects**](AssetApi.md#getcuratedobjects) | **GET** /asset/curated-objects |
|
[**getCuratedObjects**](AssetApi.md#getcuratedobjects) | **GET** /asset/curated-objects |
|
||||||
|
[**getMapMarkers**](AssetApi.md#getmapmarkers) | **GET** /asset/map-marker |
|
||||||
[**getUserAssetsByDeviceId**](AssetApi.md#getuserassetsbydeviceid) | **GET** /asset/{deviceId} |
|
[**getUserAssetsByDeviceId**](AssetApi.md#getuserassetsbydeviceid) | **GET** /asset/{deviceId} |
|
||||||
[**removeAssetsFromSharedLink**](AssetApi.md#removeassetsfromsharedlink) | **PATCH** /asset/shared-link/remove |
|
[**removeAssetsFromSharedLink**](AssetApi.md#removeassetsfromsharedlink) | **PATCH** /asset/shared-link/remove |
|
||||||
[**searchAsset**](AssetApi.md#searchasset) | **POST** /asset/search |
|
[**searchAsset**](AssetApi.md#searchasset) | **POST** /asset/search |
|
||||||
|
@ -1039,6 +1040,63 @@ This endpoint does not need any parameter.
|
||||||
|
|
||||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||||
|
|
||||||
|
# **getMapMarkers**
|
||||||
|
> List<MapMarkerResponseDto> getMapMarkers(isFavorite, isArchived, skip)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Get all assets that have GPS information embedded
|
||||||
|
|
||||||
|
### Example
|
||||||
|
```dart
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
// TODO Configure API key authorization: cookie
|
||||||
|
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
|
||||||
|
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||||
|
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
|
||||||
|
// TODO Configure HTTP Bearer authorization: bearer
|
||||||
|
// Case 1. Use String Token
|
||||||
|
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
|
||||||
|
// Case 2. Use Function which generate token.
|
||||||
|
// String yourTokenGeneratorFunction() { ... }
|
||||||
|
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
|
||||||
|
|
||||||
|
final api_instance = AssetApi();
|
||||||
|
final isFavorite = true; // bool |
|
||||||
|
final isArchived = true; // bool |
|
||||||
|
final skip = 8.14; // num |
|
||||||
|
|
||||||
|
try {
|
||||||
|
final result = api_instance.getMapMarkers(isFavorite, isArchived, skip);
|
||||||
|
print(result);
|
||||||
|
} catch (e) {
|
||||||
|
print('Exception when calling AssetApi->getMapMarkers: $e\n');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
Name | Type | Description | Notes
|
||||||
|
------------- | ------------- | ------------- | -------------
|
||||||
|
**isFavorite** | **bool**| | [optional]
|
||||||
|
**isArchived** | **bool**| | [optional]
|
||||||
|
**skip** | **num**| | [optional]
|
||||||
|
|
||||||
|
### Return type
|
||||||
|
|
||||||
|
[**List<MapMarkerResponseDto>**](MapMarkerResponseDto.md)
|
||||||
|
|
||||||
|
### Authorization
|
||||||
|
|
||||||
|
[cookie](../README.md#cookie), [bearer](../README.md#bearer)
|
||||||
|
|
||||||
|
### HTTP request headers
|
||||||
|
|
||||||
|
- **Content-Type**: Not defined
|
||||||
|
- **Accept**: application/json
|
||||||
|
|
||||||
|
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||||
|
|
||||||
# **getUserAssetsByDeviceId**
|
# **getUserAssetsByDeviceId**
|
||||||
> List<String> getUserAssetsByDeviceId(deviceId)
|
> List<String> getUserAssetsByDeviceId(deviceId)
|
||||||
|
|
||||||
|
|
18
mobile/openapi/doc/MapMarkerResponseDto.md
generated
Normal file
18
mobile/openapi/doc/MapMarkerResponseDto.md
generated
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# openapi.model.MapMarkerResponseDto
|
||||||
|
|
||||||
|
## Load the model package
|
||||||
|
```dart
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
```
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
Name | Type | Description | Notes
|
||||||
|
------------ | ------------- | ------------- | -------------
|
||||||
|
**type** | [**AssetTypeEnum**](AssetTypeEnum.md) | |
|
||||||
|
**lat** | **double** | |
|
||||||
|
**lon** | **double** | |
|
||||||
|
**id** | **String** | |
|
||||||
|
|
||||||
|
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||||
|
|
||||||
|
|
1
mobile/openapi/lib/api.dart
generated
1
mobile/openapi/lib/api.dart
generated
|
@ -88,6 +88,7 @@ part 'model/job_status_dto.dart';
|
||||||
part 'model/login_credential_dto.dart';
|
part 'model/login_credential_dto.dart';
|
||||||
part 'model/login_response_dto.dart';
|
part 'model/login_response_dto.dart';
|
||||||
part 'model/logout_response_dto.dart';
|
part 'model/logout_response_dto.dart';
|
||||||
|
part 'model/map_marker_response_dto.dart';
|
||||||
part 'model/o_auth_callback_dto.dart';
|
part 'model/o_auth_callback_dto.dart';
|
||||||
part 'model/o_auth_config_dto.dart';
|
part 'model/o_auth_config_dto.dart';
|
||||||
part 'model/o_auth_config_response_dto.dart';
|
part 'model/o_auth_config_response_dto.dart';
|
||||||
|
|
73
mobile/openapi/lib/api/asset_api.dart
generated
73
mobile/openapi/lib/api/asset_api.dart
generated
|
@ -979,6 +979,79 @@ class AssetApi {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get all assets that have GPS information embedded
|
||||||
|
///
|
||||||
|
/// Note: This method returns the HTTP [Response].
|
||||||
|
///
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [bool] isFavorite:
|
||||||
|
///
|
||||||
|
/// * [bool] isArchived:
|
||||||
|
///
|
||||||
|
/// * [num] skip:
|
||||||
|
Future<Response> getMapMarkersWithHttpInfo({ bool? isFavorite, bool? isArchived, num? skip, }) async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final path = r'/asset/map-marker';
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
if (isFavorite != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'isFavorite', isFavorite));
|
||||||
|
}
|
||||||
|
if (isArchived != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'isArchived', isArchived));
|
||||||
|
}
|
||||||
|
if (skip != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'skip', skip));
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentTypes = <String>[];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
path,
|
||||||
|
'GET',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all assets that have GPS information embedded
|
||||||
|
///
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [bool] isFavorite:
|
||||||
|
///
|
||||||
|
/// * [bool] isArchived:
|
||||||
|
///
|
||||||
|
/// * [num] skip:
|
||||||
|
Future<List<MapMarkerResponseDto>?> getMapMarkers({ bool? isFavorite, bool? isArchived, num? skip, }) async {
|
||||||
|
final response = await getMapMarkersWithHttpInfo( isFavorite: isFavorite, isArchived: isArchived, skip: skip, );
|
||||||
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
}
|
||||||
|
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||||
|
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||||
|
// FormatException when trying to decode an empty string.
|
||||||
|
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||||
|
final responseBody = await _decodeBodyBytes(response);
|
||||||
|
return (await apiClient.deserializeAsync(responseBody, 'List<MapMarkerResponseDto>') as List)
|
||||||
|
.cast<MapMarkerResponseDto>()
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/// Get all asset of a device that are in the database, ID only.
|
/// Get all asset of a device that are in the database, ID only.
|
||||||
///
|
///
|
||||||
/// Note: This method returns the HTTP [Response].
|
/// Note: This method returns the HTTP [Response].
|
||||||
|
|
2
mobile/openapi/lib/api_client.dart
generated
2
mobile/openapi/lib/api_client.dart
generated
|
@ -275,6 +275,8 @@ class ApiClient {
|
||||||
return LoginResponseDto.fromJson(value);
|
return LoginResponseDto.fromJson(value);
|
||||||
case 'LogoutResponseDto':
|
case 'LogoutResponseDto':
|
||||||
return LogoutResponseDto.fromJson(value);
|
return LogoutResponseDto.fromJson(value);
|
||||||
|
case 'MapMarkerResponseDto':
|
||||||
|
return MapMarkerResponseDto.fromJson(value);
|
||||||
case 'OAuthCallbackDto':
|
case 'OAuthCallbackDto':
|
||||||
return OAuthCallbackDto.fromJson(value);
|
return OAuthCallbackDto.fromJson(value);
|
||||||
case 'OAuthConfigDto':
|
case 'OAuthConfigDto':
|
||||||
|
|
133
mobile/openapi/lib/model/map_marker_response_dto.dart
generated
Normal file
133
mobile/openapi/lib/model/map_marker_response_dto.dart
generated
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.12
|
||||||
|
|
||||||
|
// ignore_for_file: unused_element, unused_import
|
||||||
|
// ignore_for_file: always_put_required_named_parameters_first
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
part of openapi.api;
|
||||||
|
|
||||||
|
class MapMarkerResponseDto {
|
||||||
|
/// Returns a new [MapMarkerResponseDto] instance.
|
||||||
|
MapMarkerResponseDto({
|
||||||
|
required this.type,
|
||||||
|
required this.lat,
|
||||||
|
required this.lon,
|
||||||
|
required this.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
AssetTypeEnum type;
|
||||||
|
|
||||||
|
double lat;
|
||||||
|
|
||||||
|
double lon;
|
||||||
|
|
||||||
|
String id;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is MapMarkerResponseDto &&
|
||||||
|
other.type == type &&
|
||||||
|
other.lat == lat &&
|
||||||
|
other.lon == lon &&
|
||||||
|
other.id == id;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(type.hashCode) +
|
||||||
|
(lat.hashCode) +
|
||||||
|
(lon.hashCode) +
|
||||||
|
(id.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'MapMarkerResponseDto[type=$type, lat=$lat, lon=$lon, id=$id]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
json[r'type'] = this.type;
|
||||||
|
json[r'lat'] = this.lat;
|
||||||
|
json[r'lon'] = this.lon;
|
||||||
|
json[r'id'] = this.id;
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [MapMarkerResponseDto] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static MapMarkerResponseDto? fromJson(dynamic value) {
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
// Ensure that the map contains the required keys.
|
||||||
|
// Note 1: the values aren't checked for validity beyond being non-null.
|
||||||
|
// Note 2: this code is stripped in release mode!
|
||||||
|
assert(() {
|
||||||
|
requiredKeys.forEach((key) {
|
||||||
|
assert(json.containsKey(key), 'Required key "MapMarkerResponseDto[$key]" is missing from JSON.');
|
||||||
|
assert(json[key] != null, 'Required key "MapMarkerResponseDto[$key]" has a null value in JSON.');
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}());
|
||||||
|
|
||||||
|
return MapMarkerResponseDto(
|
||||||
|
type: AssetTypeEnum.fromJson(json[r'type'])!,
|
||||||
|
lat: mapValueOfType<double>(json, r'lat')!,
|
||||||
|
lon: mapValueOfType<double>(json, r'lon')!,
|
||||||
|
id: mapValueOfType<String>(json, r'id')!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<MapMarkerResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <MapMarkerResponseDto>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = MapMarkerResponseDto.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, MapMarkerResponseDto> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, MapMarkerResponseDto>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = MapMarkerResponseDto.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of MapMarkerResponseDto-objects as value to a dart map
|
||||||
|
static Map<String, List<MapMarkerResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<MapMarkerResponseDto>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = MapMarkerResponseDto.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'type',
|
||||||
|
'lat',
|
||||||
|
'lon',
|
||||||
|
'id',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
7
mobile/openapi/test/asset_api_test.dart
generated
7
mobile/openapi/test/asset_api_test.dart
generated
|
@ -117,6 +117,13 @@ void main() {
|
||||||
// TODO
|
// TODO
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Get all assets that have GPS information embedded
|
||||||
|
//
|
||||||
|
//Future<List<MapMarkerResponseDto>> getMapMarkers({ bool isFavorite, bool isArchived, num skip }) async
|
||||||
|
test('test getMapMarkers', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
// Get all asset of a device that are in the database, ID only.
|
// Get all asset of a device that are in the database, ID only.
|
||||||
//
|
//
|
||||||
//Future<List<String>> getUserAssetsByDeviceId(String deviceId) async
|
//Future<List<String>> getUserAssetsByDeviceId(String deviceId) async
|
||||||
|
|
42
mobile/openapi/test/map_marker_response_dto_test.dart
generated
Normal file
42
mobile/openapi/test/map_marker_response_dto_test.dart
generated
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.12
|
||||||
|
|
||||||
|
// ignore_for_file: unused_element, unused_import
|
||||||
|
// ignore_for_file: always_put_required_named_parameters_first
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
// tests for MapMarkerResponseDto
|
||||||
|
void main() {
|
||||||
|
// final instance = MapMarkerResponseDto();
|
||||||
|
|
||||||
|
group('test MapMarkerResponseDto', () {
|
||||||
|
// AssetTypeEnum type
|
||||||
|
test('to test the property `type`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
// double lat
|
||||||
|
test('to test the property `lat`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
// double lon
|
||||||
|
test('to test the property `lon`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
// String id
|
||||||
|
test('to test the property `id`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
|
@ -31,7 +31,7 @@ import { CheckDuplicateAssetDto } from './dto/check-duplicate-asset.dto';
|
||||||
import { ApiBody, ApiConsumes, ApiHeader, ApiOkResponse, ApiTags } from '@nestjs/swagger';
|
import { ApiBody, ApiConsumes, ApiHeader, ApiOkResponse, ApiTags } from '@nestjs/swagger';
|
||||||
import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto';
|
import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto';
|
||||||
import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto';
|
import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto';
|
||||||
import { AssetResponseDto, ImmichReadStream } from '@app/domain';
|
import { AssetResponseDto, ImmichReadStream, MapMarkerResponseDto } from '@app/domain';
|
||||||
import { CheckDuplicateAssetResponseDto } from './response-dto/check-duplicate-asset-response.dto';
|
import { CheckDuplicateAssetResponseDto } from './response-dto/check-duplicate-asset-response.dto';
|
||||||
import { CreateAssetDto, mapToUploadFile } from './dto/create-asset.dto';
|
import { CreateAssetDto, mapToUploadFile } from './dto/create-asset.dto';
|
||||||
import { AssetFileUploadResponseDto } from './response-dto/asset-file-upload-response.dto';
|
import { AssetFileUploadResponseDto } from './response-dto/asset-file-upload-response.dto';
|
||||||
|
@ -260,6 +260,18 @@ export class AssetController {
|
||||||
return await this.assetService.getAssetByTimeBucket(authUser, getAssetByTimeBucketDto);
|
return await this.assetService.getAssetByTimeBucket(authUser, getAssetByTimeBucketDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all assets that have GPS information embedded
|
||||||
|
*/
|
||||||
|
@Authenticated()
|
||||||
|
@Get('/map-marker')
|
||||||
|
getMapMarkers(
|
||||||
|
@GetAuthUser() authUser: AuthUserDto,
|
||||||
|
@Query(new ValidationPipe({ transform: true })) dto: AssetSearchDto,
|
||||||
|
): Promise<MapMarkerResponseDto[]> {
|
||||||
|
return this.assetService.getMapMarkers(authUser, dto);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all asset of a device that are in the database, ID only.
|
* Get all asset of a device that are in the database, ID only.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { IAssetRepository } from './asset-repository';
|
import { IAssetRepository } from './asset-repository';
|
||||||
import { AssetService } from './asset.service';
|
import { AssetService } from './asset.service';
|
||||||
import { QueryFailedError, Repository } from 'typeorm';
|
import { QueryFailedError, Repository } from 'typeorm';
|
||||||
import { AssetEntity, AssetType } from '@app/infra/entities';
|
import { AssetEntity, AssetType, ExifEntity } from '@app/infra/entities';
|
||||||
import { CreateAssetDto } from './dto/create-asset.dto';
|
import { CreateAssetDto } from './dto/create-asset.dto';
|
||||||
import { AssetCountByTimeBucket } from './response-dto/asset-count-by-time-group-response.dto';
|
import { AssetCountByTimeBucket } from './response-dto/asset-count-by-time-group-response.dto';
|
||||||
import { TimeGroupEnum } from './dto/get-asset-count-by-time-bucket.dto';
|
import { TimeGroupEnum } from './dto/get-asset-count-by-time-bucket.dto';
|
||||||
|
@ -57,6 +57,9 @@ const _getAsset_1 = () => {
|
||||||
asset_1.webpPath = '';
|
asset_1.webpPath = '';
|
||||||
asset_1.encodedVideoPath = '';
|
asset_1.encodedVideoPath = '';
|
||||||
asset_1.duration = '0:00:00.000000';
|
asset_1.duration = '0:00:00.000000';
|
||||||
|
asset_1.exifInfo = new ExifEntity();
|
||||||
|
asset_1.exifInfo.latitude = 49.533547;
|
||||||
|
asset_1.exifInfo.longitude = 10.703075;
|
||||||
return asset_1;
|
return asset_1;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -492,4 +495,17 @@ describe('AssetService', () => {
|
||||||
expect(storageMock.createReadStream).toHaveBeenCalledWith('fake_path/asset_1.jpeg', 'image/jpeg');
|
expect(storageMock.createReadStream).toHaveBeenCalledWith('fake_path/asset_1.jpeg', 'image/jpeg');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('get map markers', () => {
|
||||||
|
it('should get geo information of assets', async () => {
|
||||||
|
assetRepositoryMock.getAllByUserId.mockResolvedValue(_getAssets());
|
||||||
|
|
||||||
|
const markers = await sut.getMapMarkers(authStub.admin, {});
|
||||||
|
|
||||||
|
expect(markers).toHaveLength(1);
|
||||||
|
expect(markers[0].lat).toBe(_getAsset_1().exifInfo?.latitude);
|
||||||
|
expect(markers[0].lon).toBe(_getAsset_1().exifInfo?.longitude);
|
||||||
|
expect(markers[0].id).toBe(_getAsset_1().id);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -30,6 +30,8 @@ import {
|
||||||
JobName,
|
JobName,
|
||||||
mapAsset,
|
mapAsset,
|
||||||
mapAssetWithoutExif,
|
mapAssetWithoutExif,
|
||||||
|
MapMarkerResponseDto,
|
||||||
|
mapAssetMapMarker,
|
||||||
} from '@app/domain';
|
} from '@app/domain';
|
||||||
import { CreateAssetDto, UploadFile } from './dto/create-asset.dto';
|
import { CreateAssetDto, UploadFile } from './dto/create-asset.dto';
|
||||||
import { DeleteAssetResponseDto, DeleteAssetStatusEnum } from './response-dto/delete-asset-response.dto';
|
import { DeleteAssetResponseDto, DeleteAssetStatusEnum } from './response-dto/delete-asset-response.dto';
|
||||||
|
@ -142,6 +144,12 @@ export class AssetService {
|
||||||
return assets.map((asset) => mapAsset(asset));
|
return assets.map((asset) => mapAsset(asset));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getMapMarkers(authUser: AuthUserDto, dto: AssetSearchDto): Promise<MapMarkerResponseDto[]> {
|
||||||
|
const assets = await this._assetRepository.getAllByUserId(authUser.id, dto);
|
||||||
|
|
||||||
|
return assets.map((asset) => mapAssetMapMarker(asset)).filter((marker) => marker != null) as MapMarkerResponseDto[];
|
||||||
|
}
|
||||||
|
|
||||||
public async getAssetByTimeBucket(
|
public async getAssetByTimeBucket(
|
||||||
authUser: AuthUserDto,
|
authUser: AuthUserDto,
|
||||||
getAssetByTimeBucketDto: GetAssetByTimeBucketDto,
|
getAssetByTimeBucketDto: GetAssetByTimeBucketDto,
|
||||||
|
|
|
@ -2601,6 +2601,64 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/asset/map-marker": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "getMapMarkers",
|
||||||
|
"description": "Get all assets that have GPS information embedded",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "isFavorite",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "isArchived",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "skip",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/MapMarkerResponseDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"Asset"
|
||||||
|
],
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/asset/{deviceId}": {
|
"/asset/{deviceId}": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "getUserAssetsByDeviceId",
|
"operationId": "getUserAssetsByDeviceId",
|
||||||
|
@ -5426,6 +5484,31 @@
|
||||||
"timeBucket"
|
"timeBucket"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"MapMarkerResponseDto": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"$ref": "#/components/schemas/AssetTypeEnum"
|
||||||
|
},
|
||||||
|
"lat": {
|
||||||
|
"type": "number",
|
||||||
|
"format": "double"
|
||||||
|
},
|
||||||
|
"lon": {
|
||||||
|
"type": "number",
|
||||||
|
"format": "double"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"type",
|
||||||
|
"lat",
|
||||||
|
"lon",
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
"UpdateAssetDto": {
|
"UpdateAssetDto": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
export * from './asset-response.dto';
|
export * from './asset-response.dto';
|
||||||
export * from './exif-response.dto';
|
export * from './exif-response.dto';
|
||||||
export * from './smart-info-response.dto';
|
export * from './smart-info-response.dto';
|
||||||
|
export * from './map-marker-response.dto';
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { AssetEntity, AssetType } from '@app/infra/entities';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class MapMarkerResponseDto {
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ enumName: 'AssetTypeEnum', enum: AssetType })
|
||||||
|
type!: AssetType;
|
||||||
|
|
||||||
|
@ApiProperty({ type: 'number', format: 'double' })
|
||||||
|
lat!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ type: 'number', format: 'double' })
|
||||||
|
lon!: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapAssetMapMarker(entity: AssetEntity): MapMarkerResponseDto | null {
|
||||||
|
if (!entity.exifInfo) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lat = entity.exifInfo.latitude;
|
||||||
|
const lon = entity.exifInfo.longitude;
|
||||||
|
|
||||||
|
if (!lat || !lon) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: entity.id,
|
||||||
|
type: entity.type,
|
||||||
|
lon,
|
||||||
|
lat,
|
||||||
|
};
|
||||||
|
}
|
34
web/package-lock.json
generated
34
web/package-lock.json
generated
|
@ -13,6 +13,7 @@
|
||||||
"handlebars": "^4.7.7",
|
"handlebars": "^4.7.7",
|
||||||
"justified-layout": "^4.1.0",
|
"justified-layout": "^4.1.0",
|
||||||
"leaflet": "^1.9.3",
|
"leaflet": "^1.9.3",
|
||||||
|
"leaflet.markercluster": "^1.5.3",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"luxon": "^3.2.1",
|
"luxon": "^3.2.1",
|
||||||
"rxjs": "^7.8.0",
|
"rxjs": "^7.8.0",
|
||||||
|
@ -31,6 +32,7 @@
|
||||||
"@types/cookie": "^0.5.1",
|
"@types/cookie": "^0.5.1",
|
||||||
"@types/justified-layout": "^4.1.0",
|
"@types/justified-layout": "^4.1.0",
|
||||||
"@types/leaflet": "^1.9.1",
|
"@types/leaflet": "^1.9.1",
|
||||||
|
"@types/leaflet.markercluster": "^1.5.1",
|
||||||
"@types/lodash-es": "^4.17.6",
|
"@types/lodash-es": "^4.17.6",
|
||||||
"@types/luxon": "^3.2.0",
|
"@types/luxon": "^3.2.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.53.0",
|
"@typescript-eslint/eslint-plugin": "^5.53.0",
|
||||||
|
@ -3622,6 +3624,15 @@
|
||||||
"@types/geojson": "*"
|
"@types/geojson": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/leaflet.markercluster": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/leaflet.markercluster/-/leaflet.markercluster-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-gzJzP10qO6Zkts5QNVmSAEDLYicQHTEBLT9HZpFrJiSww9eDAs5OWHvIskldf41MvDv1gbMukuEBQEawHn+wtA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/leaflet": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/lodash": {
|
"node_modules/@types/lodash": {
|
||||||
"version": "4.14.191",
|
"version": "4.14.191",
|
||||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz",
|
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz",
|
||||||
|
@ -9044,6 +9055,14 @@
|
||||||
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.3.tgz",
|
||||||
"integrity": "sha512-iB2cR9vAkDOu5l3HAay2obcUHZ7xwUBBjph8+PGtmW/2lYhbLizWtG7nTeYht36WfOslixQF9D/uSIzhZgGMfQ=="
|
"integrity": "sha512-iB2cR9vAkDOu5l3HAay2obcUHZ7xwUBBjph8+PGtmW/2lYhbLizWtG7nTeYht36WfOslixQF9D/uSIzhZgGMfQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/leaflet.markercluster": {
|
||||||
|
"version": "1.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/leaflet.markercluster/-/leaflet.markercluster-1.5.3.tgz",
|
||||||
|
"integrity": "sha512-vPTw/Bndq7eQHjLBVlWpnGeLa3t+3zGiuM7fJwCkiMFq+nmRuG3RI3f7f4N4TDX7T4NpbAXpR2+NTRSEGfCSeA==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"leaflet": "^1.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/leven": {
|
"node_modules/leven": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
|
||||||
|
@ -14055,6 +14074,15 @@
|
||||||
"@types/geojson": "*"
|
"@types/geojson": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/leaflet.markercluster": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/leaflet.markercluster/-/leaflet.markercluster-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-gzJzP10qO6Zkts5QNVmSAEDLYicQHTEBLT9HZpFrJiSww9eDAs5OWHvIskldf41MvDv1gbMukuEBQEawHn+wtA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/leaflet": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/lodash": {
|
"@types/lodash": {
|
||||||
"version": "4.14.191",
|
"version": "4.14.191",
|
||||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz",
|
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz",
|
||||||
|
@ -18045,6 +18073,12 @@
|
||||||
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.3.tgz",
|
||||||
"integrity": "sha512-iB2cR9vAkDOu5l3HAay2obcUHZ7xwUBBjph8+PGtmW/2lYhbLizWtG7nTeYht36WfOslixQF9D/uSIzhZgGMfQ=="
|
"integrity": "sha512-iB2cR9vAkDOu5l3HAay2obcUHZ7xwUBBjph8+PGtmW/2lYhbLizWtG7nTeYht36WfOslixQF9D/uSIzhZgGMfQ=="
|
||||||
},
|
},
|
||||||
|
"leaflet.markercluster": {
|
||||||
|
"version": "1.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/leaflet.markercluster/-/leaflet.markercluster-1.5.3.tgz",
|
||||||
|
"integrity": "sha512-vPTw/Bndq7eQHjLBVlWpnGeLa3t+3zGiuM7fJwCkiMFq+nmRuG3RI3f7f4N4TDX7T4NpbAXpR2+NTRSEGfCSeA==",
|
||||||
|
"requires": {}
|
||||||
|
},
|
||||||
"leven": {
|
"leven": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
|
||||||
|
|
|
@ -29,6 +29,7 @@
|
||||||
"@types/cookie": "^0.5.1",
|
"@types/cookie": "^0.5.1",
|
||||||
"@types/justified-layout": "^4.1.0",
|
"@types/justified-layout": "^4.1.0",
|
||||||
"@types/leaflet": "^1.9.1",
|
"@types/leaflet": "^1.9.1",
|
||||||
|
"@types/leaflet.markercluster": "^1.5.1",
|
||||||
"@types/lodash-es": "^4.17.6",
|
"@types/lodash-es": "^4.17.6",
|
||||||
"@types/luxon": "^3.2.0",
|
"@types/luxon": "^3.2.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.53.0",
|
"@typescript-eslint/eslint-plugin": "^5.53.0",
|
||||||
|
@ -61,6 +62,7 @@
|
||||||
"handlebars": "^4.7.7",
|
"handlebars": "^4.7.7",
|
||||||
"justified-layout": "^4.1.0",
|
"justified-layout": "^4.1.0",
|
||||||
"leaflet": "^1.9.3",
|
"leaflet": "^1.9.3",
|
||||||
|
"leaflet.markercluster": "^1.5.3",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"luxon": "^3.2.1",
|
"luxon": "^3.2.1",
|
||||||
"rxjs": "^7.8.0",
|
"rxjs": "^7.8.0",
|
||||||
|
|
119
web/src/api/open-api/api.ts
generated
119
web/src/api/open-api/api.ts
generated
|
@ -1438,6 +1438,39 @@ export interface LogoutResponseDto {
|
||||||
*/
|
*/
|
||||||
'redirectUri': string;
|
'redirectUri': string;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @interface MapMarkerResponseDto
|
||||||
|
*/
|
||||||
|
export interface MapMarkerResponseDto {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {AssetTypeEnum}
|
||||||
|
* @memberof MapMarkerResponseDto
|
||||||
|
*/
|
||||||
|
'type': AssetTypeEnum;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
* @memberof MapMarkerResponseDto
|
||||||
|
*/
|
||||||
|
'lat': number;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
* @memberof MapMarkerResponseDto
|
||||||
|
*/
|
||||||
|
'lon': number;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof MapMarkerResponseDto
|
||||||
|
*/
|
||||||
|
'id': string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @export
|
* @export
|
||||||
|
@ -4752,6 +4785,56 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||||
|
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||||
|
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: toPathString(localVarUrlObj),
|
||||||
|
options: localVarRequestOptions,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Get all assets that have GPS information embedded
|
||||||
|
* @param {boolean} [isFavorite]
|
||||||
|
* @param {boolean} [isArchived]
|
||||||
|
* @param {number} [skip]
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
getMapMarkers: async (isFavorite?: boolean, isArchived?: boolean, skip?: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||||
|
const localVarPath = `/asset/map-marker`;
|
||||||
|
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||||
|
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||||
|
let baseOptions;
|
||||||
|
if (configuration) {
|
||||||
|
baseOptions = configuration.baseOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
|
||||||
|
const localVarHeaderParameter = {} as any;
|
||||||
|
const localVarQueryParameter = {} as any;
|
||||||
|
|
||||||
|
// authentication cookie required
|
||||||
|
|
||||||
|
// authentication bearer required
|
||||||
|
// http bearer authentication required
|
||||||
|
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||||
|
|
||||||
|
if (isFavorite !== undefined) {
|
||||||
|
localVarQueryParameter['isFavorite'] = isFavorite;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isArchived !== undefined) {
|
||||||
|
localVarQueryParameter['isArchived'] = isArchived;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (skip !== undefined) {
|
||||||
|
localVarQueryParameter['skip'] = skip;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||||
|
@ -5321,6 +5404,18 @@ export const AssetApiFp = function(configuration?: Configuration) {
|
||||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getCuratedObjects(options);
|
const localVarAxiosArgs = await localVarAxiosParamCreator.getCuratedObjects(options);
|
||||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Get all assets that have GPS information embedded
|
||||||
|
* @param {boolean} [isFavorite]
|
||||||
|
* @param {boolean} [isArchived]
|
||||||
|
* @param {number} [skip]
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
async getMapMarkers(isFavorite?: boolean, isArchived?: boolean, skip?: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<MapMarkerResponseDto>>> {
|
||||||
|
const localVarAxiosArgs = await localVarAxiosParamCreator.getMapMarkers(isFavorite, isArchived, skip, options);
|
||||||
|
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* Get all asset of a device that are in the database, ID only.
|
* Get all asset of a device that are in the database, ID only.
|
||||||
* @param {string} deviceId
|
* @param {string} deviceId
|
||||||
|
@ -5577,6 +5672,17 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
|
||||||
getCuratedObjects(options?: any): AxiosPromise<Array<CuratedObjectsResponseDto>> {
|
getCuratedObjects(options?: any): AxiosPromise<Array<CuratedObjectsResponseDto>> {
|
||||||
return localVarFp.getCuratedObjects(options).then((request) => request(axios, basePath));
|
return localVarFp.getCuratedObjects(options).then((request) => request(axios, basePath));
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Get all assets that have GPS information embedded
|
||||||
|
* @param {boolean} [isFavorite]
|
||||||
|
* @param {boolean} [isArchived]
|
||||||
|
* @param {number} [skip]
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
getMapMarkers(isFavorite?: boolean, isArchived?: boolean, skip?: number, options?: any): AxiosPromise<Array<MapMarkerResponseDto>> {
|
||||||
|
return localVarFp.getMapMarkers(isFavorite, isArchived, skip, options).then((request) => request(axios, basePath));
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* Get all asset of a device that are in the database, ID only.
|
* Get all asset of a device that are in the database, ID only.
|
||||||
* @param {string} deviceId
|
* @param {string} deviceId
|
||||||
|
@ -5863,6 +5969,19 @@ export class AssetApi extends BaseAPI {
|
||||||
return AssetApiFp(this.configuration).getCuratedObjects(options).then((request) => request(this.axios, this.basePath));
|
return AssetApiFp(this.configuration).getCuratedObjects(options).then((request) => request(this.axios, this.basePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all assets that have GPS information embedded
|
||||||
|
* @param {boolean} [isFavorite]
|
||||||
|
* @param {boolean} [isArchived]
|
||||||
|
* @param {number} [skip]
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
* @memberof AssetApi
|
||||||
|
*/
|
||||||
|
public getMapMarkers(isFavorite?: boolean, isArchived?: boolean, skip?: number, options?: AxiosRequestConfig) {
|
||||||
|
return AssetApiFp(this.configuration).getMapMarkers(isFavorite, isArchived, skip, options).then((request) => request(this.axios, this.basePath));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all asset of a device that are in the database, ID only.
|
* Get all asset of a device that are in the database, ID only.
|
||||||
* @param {string} deviceId
|
* @param {string} deviceId
|
||||||
|
|
|
@ -296,7 +296,7 @@
|
||||||
|
|
||||||
<section
|
<section
|
||||||
id="immich-asset-viewer"
|
id="immich-asset-viewer"
|
||||||
class="fixed h-screen w-screen left-0 top-0 overflow-y-hidden bg-black z-[999] grid grid-rows-[64px_1fr] grid-cols-4"
|
class="fixed h-screen w-screen left-0 top-0 overflow-y-hidden bg-black z-[1001] grid grid-rows-[64px_1fr] grid-cols-4"
|
||||||
>
|
>
|
||||||
<div class="col-start-1 col-span-4 row-start-1 row-span-1 z-[1000] transition-transform">
|
<div class="col-start-1 col-span-4 row-start-1 row-span-1 z-[1000] transition-transform">
|
||||||
<AssetViewerNavBar
|
<AssetViewerNavBar
|
||||||
|
|
|
@ -0,0 +1,119 @@
|
||||||
|
<script lang="ts" context="module">
|
||||||
|
import { createContext } from '$lib/utils/context';
|
||||||
|
import { MarkerClusterGroup, Marker, Icon, LeafletEvent } from 'leaflet';
|
||||||
|
|
||||||
|
const { get: getContext, set: setClusterContext } = createContext<() => MarkerClusterGroup>();
|
||||||
|
|
||||||
|
export const getClusterContext = () => {
|
||||||
|
return getContext()();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import 'leaflet.markercluster';
|
||||||
|
import { onDestroy, onMount } from 'svelte';
|
||||||
|
import { getMapContext } from './map.svelte';
|
||||||
|
import { MapMarkerResponseDto, api } from '@api';
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
|
class AssetMarker extends Marker {
|
||||||
|
marker: MapMarkerResponseDto;
|
||||||
|
|
||||||
|
constructor(marker: MapMarkerResponseDto) {
|
||||||
|
super([marker.lat, marker.lon], {
|
||||||
|
icon: new Icon({
|
||||||
|
iconUrl: api.getAssetThumbnailUrl(marker.id),
|
||||||
|
iconRetinaUrl: api.getAssetThumbnailUrl(marker.id),
|
||||||
|
iconSize: [60, 60],
|
||||||
|
iconAnchor: [12, 41],
|
||||||
|
popupAnchor: [1, -34],
|
||||||
|
tooltipAnchor: [16, -28],
|
||||||
|
shadowSize: [41, 41]
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
this.marker = marker;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAssetId(): string {
|
||||||
|
return this.marker.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher<{ view: { assets: string[] } }>();
|
||||||
|
|
||||||
|
export let markers: MapMarkerResponseDto[];
|
||||||
|
|
||||||
|
const map = getMapContext();
|
||||||
|
|
||||||
|
let cluster: MarkerClusterGroup;
|
||||||
|
|
||||||
|
setClusterContext(() => cluster);
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
cluster = new MarkerClusterGroup({
|
||||||
|
showCoverageOnHover: false,
|
||||||
|
zoomToBoundsOnClick: false,
|
||||||
|
spiderfyOnMaxZoom: false,
|
||||||
|
maxClusterRadius: 30,
|
||||||
|
spiderLegPolylineOptions: { opacity: 0 },
|
||||||
|
spiderfyDistanceMultiplier: 3
|
||||||
|
});
|
||||||
|
|
||||||
|
cluster.on('clusterclick', (event: LeafletEvent) => {
|
||||||
|
const ids = event.sourceTarget
|
||||||
|
.getAllChildMarkers()
|
||||||
|
.map((marker: AssetMarker) => marker.getAssetId());
|
||||||
|
dispatch('view', { assets: ids });
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let marker of markers) {
|
||||||
|
const leafletMarker = new AssetMarker(marker);
|
||||||
|
|
||||||
|
leafletMarker.on('click', () => {
|
||||||
|
dispatch('view', { assets: [marker.id] });
|
||||||
|
});
|
||||||
|
|
||||||
|
cluster.addLayer(leafletMarker);
|
||||||
|
}
|
||||||
|
|
||||||
|
map.addLayer(cluster);
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
if (cluster) cluster.remove();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if cluster}
|
||||||
|
<slot />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:global(.leaflet-marker-icon) {
|
||||||
|
border-radius: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.marker-cluster) {
|
||||||
|
background-clip: padding-box;
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.marker-cluster div) {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
margin-left: 5px;
|
||||||
|
margin-top: 5px;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
background-color: rgb(236, 237, 246);
|
||||||
|
color: rgb(69, 80, 169);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.marker-cluster span) {
|
||||||
|
line-height: 40px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,3 +1,4 @@
|
||||||
export { default as Map } from './map.svelte';
|
export { default as Map } from './map.svelte';
|
||||||
export { default as Marker } from './marker.svelte';
|
export { default as Marker } from './marker.svelte';
|
||||||
export { default as TileLayer } from './tile-layer.svelte';
|
export { default as TileLayer } from './tile-layer.svelte';
|
||||||
|
export { default as AssetMarkerCluster } from './asset-marker-cluster.svelte';
|
||||||
|
|
|
@ -5,15 +5,30 @@
|
||||||
|
|
||||||
export let urlTemplate: string;
|
export let urlTemplate: string;
|
||||||
export let options: TileLayerOptions | undefined = undefined;
|
export let options: TileLayerOptions | undefined = undefined;
|
||||||
|
export let allowDarkMode = false;
|
||||||
|
|
||||||
let tileLayer: TileLayer;
|
let tileLayer: TileLayer;
|
||||||
|
|
||||||
const map = getMapContext();
|
const map = getMapContext();
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
tileLayer = new TileLayer(urlTemplate, options).addTo(map);
|
tileLayer = new TileLayer(urlTemplate, {
|
||||||
|
className: allowDarkMode ? 'leaflet-layer-dynamic' : 'leaflet-layer',
|
||||||
|
...options
|
||||||
|
}).addTo(map);
|
||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
if (tileLayer) tileLayer.remove();
|
if (tileLayer) tileLayer.remove();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:global(.leaflet-layer-dynamic) {
|
||||||
|
filter: brightness(100%) contrast(100%) saturate(80%);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark .leaflet-layer-dynamic) {
|
||||||
|
filter: invert(100%) brightness(130%) saturate(0%);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
import ImageOutline from 'svelte-material-icons/ImageOutline.svelte';
|
import ImageOutline from 'svelte-material-icons/ImageOutline.svelte';
|
||||||
import ArchiveArrowDownOutline from 'svelte-material-icons/ArchiveArrowDownOutline.svelte';
|
import ArchiveArrowDownOutline from 'svelte-material-icons/ArchiveArrowDownOutline.svelte';
|
||||||
import Magnify from 'svelte-material-icons/Magnify.svelte';
|
import Magnify from 'svelte-material-icons/Magnify.svelte';
|
||||||
|
import Map from 'svelte-material-icons/Map.svelte';
|
||||||
import StarOutline from 'svelte-material-icons/StarOutline.svelte';
|
import StarOutline from 'svelte-material-icons/StarOutline.svelte';
|
||||||
import { AppRoute } from '../../../constants';
|
import { AppRoute } from '../../../constants';
|
||||||
import LoadingSpinner from '../loading-spinner.svelte';
|
import LoadingSpinner from '../loading-spinner.svelte';
|
||||||
|
@ -108,6 +109,9 @@
|
||||||
isSelected={$page.route.id === '/(user)/explore'}
|
isSelected={$page.route.id === '/(user)/explore'}
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
|
<a data-sveltekit-preload-data="hover" href={AppRoute.MAP} draggable="false">
|
||||||
|
<SideBarButton title="Map" logo={Map} isSelected={$page.route.id === '/(user)/map'} />
|
||||||
|
</a>
|
||||||
<a data-sveltekit-preload-data="hover" href={AppRoute.SHARING} draggable="false">
|
<a data-sveltekit-preload-data="hover" href={AppRoute.SHARING} draggable="false">
|
||||||
<SideBarButton
|
<SideBarButton
|
||||||
title="Sharing"
|
title="Sharing"
|
||||||
|
|
|
@ -14,6 +14,7 @@ export enum AppRoute {
|
||||||
EXPLORE = '/explore',
|
EXPLORE = '/explore',
|
||||||
SHARING = '/sharing',
|
SHARING = '/sharing',
|
||||||
SEARCH = '/search',
|
SEARCH = '/search',
|
||||||
|
MAP = '/map',
|
||||||
|
|
||||||
AUTH_LOGIN = '/auth/login',
|
AUTH_LOGIN = '/auth/login',
|
||||||
AUTH_LOGOUT = '/auth/logout',
|
AUTH_LOGOUT = '/auth/logout',
|
||||||
|
|
|
@ -53,7 +53,11 @@ function createAssetInteractionStore() {
|
||||||
* Asset Viewer
|
* Asset Viewer
|
||||||
*/
|
*/
|
||||||
const setViewingAsset = async (asset: AssetResponseDto) => {
|
const setViewingAsset = async (asset: AssetResponseDto) => {
|
||||||
const { data } = await api.assetApi.getAssetById(asset.id);
|
setViewingAssetId(asset.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setViewingAssetId = async (id: string) => {
|
||||||
|
const { data } = await api.assetApi.getAssetById(id);
|
||||||
viewingAssetStoreState.set(data);
|
viewingAssetStoreState.set(data);
|
||||||
isViewingAssetStoreState.set(true);
|
isViewingAssetStoreState.set(true);
|
||||||
};
|
};
|
||||||
|
@ -140,6 +144,7 @@ function createAssetInteractionStore() {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
setViewingAsset,
|
setViewingAsset,
|
||||||
|
setViewingAssetId,
|
||||||
setIsViewingAsset,
|
setIsViewingAsset,
|
||||||
navigateAsset,
|
navigateAsset,
|
||||||
addAssetToMultiselectGroup,
|
addAssetToMultiselectGroup,
|
||||||
|
|
23
web/src/routes/(user)/map/+page.server.ts
Normal file
23
web/src/routes/(user)/map/+page.server.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import { AppRoute } from '$lib/constants';
|
||||||
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
|
export const load = (async ({ locals: { api, user } }) => {
|
||||||
|
if (!user) {
|
||||||
|
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data: mapMarkers } = await api.assetApi.getMapMarkers();
|
||||||
|
|
||||||
|
return {
|
||||||
|
user,
|
||||||
|
mapMarkers,
|
||||||
|
meta: {
|
||||||
|
title: 'Map'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||||
|
}
|
||||||
|
}) satisfies PageServerLoad;
|
80
web/src/routes/(user)/map/+page.svelte
Normal file
80
web/src/routes/(user)/map/+page.svelte
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { PageData } from '../map/$types';
|
||||||
|
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||||
|
import Portal from '$lib/components/shared-components/portal/portal.svelte';
|
||||||
|
import AssetViewer from '$lib/components/asset-viewer/asset-viewer.svelte';
|
||||||
|
import {
|
||||||
|
assetInteractionStore,
|
||||||
|
isViewingAssetStoreState,
|
||||||
|
viewingAssetStoreState
|
||||||
|
} from '$lib/stores/asset-interaction.store';
|
||||||
|
|
||||||
|
export let data: PageData;
|
||||||
|
|
||||||
|
let initialMapCenter: [number, number] = [48, 11];
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if (data.mapMarkers.length) {
|
||||||
|
let firstMarker = data.mapMarkers[0];
|
||||||
|
initialMapCenter = [firstMarker.lat, firstMarker.lon];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let viewingAssets: string[] = [];
|
||||||
|
let viewingAssetCursor = 0;
|
||||||
|
|
||||||
|
function onViewAssets(assets: string[]) {
|
||||||
|
assetInteractionStore.setViewingAssetId(assets[0]);
|
||||||
|
viewingAssets = assets;
|
||||||
|
viewingAssetCursor = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigateNext() {
|
||||||
|
if (viewingAssetCursor < viewingAssets.length - 1) {
|
||||||
|
assetInteractionStore.setViewingAssetId(viewingAssets[++viewingAssetCursor]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigatePrevious() {
|
||||||
|
if (viewingAssetCursor > 0) {
|
||||||
|
assetInteractionStore.setViewingAssetId(viewingAssets[--viewingAssetCursor]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<UserPageLayout user={data.user} title={data.meta.title}>
|
||||||
|
<div slot="buttons" />
|
||||||
|
|
||||||
|
<div class="h-[90%] w-full">
|
||||||
|
{#await import('$lib/components/shared-components/leaflet') then { Map, TileLayer, AssetMarkerCluster }}
|
||||||
|
<Map latlng={initialMapCenter} zoom={7}>
|
||||||
|
<TileLayer
|
||||||
|
allowDarkMode={true}
|
||||||
|
urlTemplate={'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'}
|
||||||
|
options={{
|
||||||
|
attribution:
|
||||||
|
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<AssetMarkerCluster
|
||||||
|
markers={data.mapMarkers}
|
||||||
|
on:view={(event) => onViewAssets(event.detail.assets)}
|
||||||
|
/>
|
||||||
|
</Map>
|
||||||
|
{/await}
|
||||||
|
</div>
|
||||||
|
</UserPageLayout>
|
||||||
|
|
||||||
|
<Portal target="body">
|
||||||
|
{#if $isViewingAssetStoreState}
|
||||||
|
<AssetViewer
|
||||||
|
asset={$viewingAssetStoreState}
|
||||||
|
showNavigation={viewingAssets.length > 1}
|
||||||
|
on:navigate-next={navigateNext}
|
||||||
|
on:navigate-previous={navigatePrevious}
|
||||||
|
on:close={() => {
|
||||||
|
assetInteractionStore.setIsViewingAsset(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</Portal>
|
Loading…
Add table
Reference in a new issue