mirror of
https://github.com/immich-app/immich.git
synced 2024-12-29 15:11:58 +00:00
feat: serve map tile styles from tiles.immich.cloud (#12858)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
This commit is contained in:
parent
5ea3b5a5c1
commit
b0947bf9b9
29 changed files with 650 additions and 741 deletions
6
e2e/package-lock.json
generated
6
e2e/package-lock.json
generated
|
@ -5016,9 +5016,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/path-to-regexp": {
|
"node_modules/path-to-regexp": {
|
||||||
"version": "6.2.2",
|
"version": "6.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz",
|
||||||
"integrity": "sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==",
|
"integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/pathe": {
|
"node_modules/pathe": {
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import { AssetMediaResponseDto, LoginResponseDto, SharedLinkType } from '@immich/sdk';
|
import { LoginResponseDto } from '@immich/sdk';
|
||||||
import { readFile } from 'node:fs/promises';
|
import { readFile } from 'node:fs/promises';
|
||||||
import { basename, join } from 'node:path';
|
import { basename, join } from 'node:path';
|
||||||
import { Socket } from 'socket.io-client';
|
import { Socket } from 'socket.io-client';
|
||||||
import { createUserDto } from 'src/fixtures';
|
|
||||||
import { errorDto } from 'src/responses';
|
import { errorDto } from 'src/responses';
|
||||||
import { app, testAssetDir, utils } from 'src/utils';
|
import { app, testAssetDir, utils } from 'src/utils';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
|
@ -11,18 +10,13 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||||
describe('/map', () => {
|
describe('/map', () => {
|
||||||
let websocket: Socket;
|
let websocket: Socket;
|
||||||
let admin: LoginResponseDto;
|
let admin: LoginResponseDto;
|
||||||
let nonAdmin: LoginResponseDto;
|
|
||||||
let asset: AssetMediaResponseDto;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await utils.resetDatabase();
|
await utils.resetDatabase();
|
||||||
admin = await utils.adminSetup({ onboarding: false });
|
admin = await utils.adminSetup({ onboarding: false });
|
||||||
nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user1);
|
|
||||||
|
|
||||||
websocket = await utils.connectWebsocket(admin.accessToken);
|
websocket = await utils.connectWebsocket(admin.accessToken);
|
||||||
|
|
||||||
asset = await utils.createAsset(admin.accessToken);
|
|
||||||
|
|
||||||
const files = ['formats/heic/IMG_2682.heic', 'metadata/gps-position/thompson-springs.jpg'];
|
const files = ['formats/heic/IMG_2682.heic', 'metadata/gps-position/thompson-springs.jpg'];
|
||||||
utils.resetEvents();
|
utils.resetEvents();
|
||||||
const uploadFile = async (input: string) => {
|
const uploadFile = async (input: string) => {
|
||||||
|
@ -103,63 +97,6 @@ describe('/map', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /map/style.json', () => {
|
|
||||||
it('should require authentication', async () => {
|
|
||||||
const { status, body } = await request(app).get('/map/style.json');
|
|
||||||
expect(status).toBe(401);
|
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should allow shared link access', async () => {
|
|
||||||
const sharedLink = await utils.createSharedLink(admin.accessToken, {
|
|
||||||
type: SharedLinkType.Individual,
|
|
||||||
assetIds: [asset.id],
|
|
||||||
});
|
|
||||||
const { status, body } = await request(app).get(`/map/style.json?key=${sharedLink.key}`).query({ theme: 'dark' });
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toEqual(expect.objectContaining({ id: 'immich-map-dark' }));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw an error if a theme is not light or dark', async () => {
|
|
||||||
for (const theme of ['dark1', true, 123, '', null, undefined]) {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get('/map/style.json')
|
|
||||||
.query({ theme })
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
expect(status).toBe(400);
|
|
||||||
expect(body).toEqual(errorDto.badRequest(['theme must be one of the following values: light, dark']));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return the light style.json', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get('/map/style.json')
|
|
||||||
.query({ theme: 'light' })
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toEqual(expect.objectContaining({ id: 'immich-map-light' }));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return the dark style.json', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get('/map/style.json')
|
|
||||||
.query({ theme: 'dark' })
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toEqual(expect.objectContaining({ id: 'immich-map-dark' }));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not require admin authentication', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get('/map/style.json')
|
|
||||||
.query({ theme: 'dark' })
|
|
||||||
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toEqual(expect.objectContaining({ id: 'immich-map-dark' }));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('GET /map/reverse-geocode', () => {
|
describe('GET /map/reverse-geocode', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).get('/map/reverse-geocode');
|
const { status, body } = await request(app).get('/map/reverse-geocode');
|
||||||
|
|
|
@ -128,6 +128,8 @@ describe('/server-info', () => {
|
||||||
isInitialized: true,
|
isInitialized: true,
|
||||||
externalDomain: '',
|
externalDomain: '',
|
||||||
isOnboarded: false,
|
isOnboarded: false,
|
||||||
|
mapDarkStyleUrl: 'https://tiles.immich.cloud/v1/style/dark.json',
|
||||||
|
mapLightStyleUrl: 'https://tiles.immich.cloud/v1/style/light.json',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -134,6 +134,8 @@ describe('/server', () => {
|
||||||
isInitialized: true,
|
isInitialized: true,
|
||||||
externalDomain: '',
|
externalDomain: '',
|
||||||
isOnboarded: false,
|
isOnboarded: false,
|
||||||
|
mapDarkStyleUrl: 'https://tiles.immich.cloud/v1/style/dark.json',
|
||||||
|
mapLightStyleUrl: 'https://tiles.immich.cloud/v1/style/light.json',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
2
mobile/.vscode/settings.json
vendored
2
mobile/.vscode/settings.json
vendored
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"dart.flutterSdkPath": ".fvm/versions/3.24.0",
|
"dart.flutterSdkPath": ".fvm/versions/3.24.3",
|
||||||
"search.exclude": {
|
"search.exclude": {
|
||||||
"**/.fvm": true
|
"**/.fvm": true
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,11 +4,15 @@ class ServerConfig {
|
||||||
final int trashDays;
|
final int trashDays;
|
||||||
final String oauthButtonText;
|
final String oauthButtonText;
|
||||||
final String externalDomain;
|
final String externalDomain;
|
||||||
|
final String mapDarkStyleUrl;
|
||||||
|
final String mapLightStyleUrl;
|
||||||
|
|
||||||
const ServerConfig({
|
const ServerConfig({
|
||||||
required this.trashDays,
|
required this.trashDays,
|
||||||
required this.oauthButtonText,
|
required this.oauthButtonText,
|
||||||
required this.externalDomain,
|
required this.externalDomain,
|
||||||
|
required this.mapDarkStyleUrl,
|
||||||
|
required this.mapLightStyleUrl,
|
||||||
});
|
});
|
||||||
|
|
||||||
ServerConfig copyWith({
|
ServerConfig copyWith({
|
||||||
|
@ -20,6 +24,8 @@ class ServerConfig {
|
||||||
trashDays: trashDays ?? this.trashDays,
|
trashDays: trashDays ?? this.trashDays,
|
||||||
oauthButtonText: oauthButtonText ?? this.oauthButtonText,
|
oauthButtonText: oauthButtonText ?? this.oauthButtonText,
|
||||||
externalDomain: externalDomain ?? this.externalDomain,
|
externalDomain: externalDomain ?? this.externalDomain,
|
||||||
|
mapDarkStyleUrl: mapDarkStyleUrl,
|
||||||
|
mapLightStyleUrl: mapLightStyleUrl,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,7 +36,9 @@ class ServerConfig {
|
||||||
ServerConfig.fromDto(ServerConfigDto dto)
|
ServerConfig.fromDto(ServerConfigDto dto)
|
||||||
: trashDays = dto.trashDays,
|
: trashDays = dto.trashDays,
|
||||||
oauthButtonText = dto.oauthButtonText,
|
oauthButtonText = dto.oauthButtonText,
|
||||||
externalDomain = dto.externalDomain;
|
externalDomain = dto.externalDomain,
|
||||||
|
mapDarkStyleUrl = dto.mapDarkStyleUrl,
|
||||||
|
mapLightStyleUrl = dto.mapLightStyleUrl;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(covariant ServerConfig other) {
|
bool operator ==(covariant ServerConfig other) {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
@ -7,27 +8,27 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:geolocator/geolocator.dart';
|
import 'package:geolocator/geolocator.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/latlngbounds_extension.dart';
|
import 'package:immich_mobile/extensions/latlngbounds_extension.dart';
|
||||||
import 'package:immich_mobile/extensions/maplibrecontroller_extensions.dart';
|
import 'package:immich_mobile/extensions/maplibrecontroller_extensions.dart';
|
||||||
import 'package:immich_mobile/models/map/map_event.model.dart';
|
import 'package:immich_mobile/models/map/map_event.model.dart';
|
||||||
import 'package:immich_mobile/models/map/map_marker.model.dart';
|
import 'package:immich_mobile/models/map/map_marker.model.dart';
|
||||||
|
import 'package:immich_mobile/providers/db.provider.dart';
|
||||||
import 'package:immich_mobile/providers/map/map_marker.provider.dart';
|
import 'package:immich_mobile/providers/map/map_marker.provider.dart';
|
||||||
import 'package:immich_mobile/providers/map/map_state.provider.dart';
|
import 'package:immich_mobile/providers/map/map_state.provider.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/utils/debounce.dart';
|
||||||
|
import 'package:immich_mobile/utils/immich_loading_overlay.dart';
|
||||||
import 'package:immich_mobile/utils/map_utils.dart';
|
import 'package:immich_mobile/utils/map_utils.dart';
|
||||||
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
|
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
import 'package:immich_mobile/widgets/map/map_app_bar.dart';
|
import 'package:immich_mobile/widgets/map/map_app_bar.dart';
|
||||||
import 'package:immich_mobile/widgets/map/map_asset_grid.dart';
|
import 'package:immich_mobile/widgets/map/map_asset_grid.dart';
|
||||||
import 'package:immich_mobile/widgets/map/map_bottom_sheet.dart';
|
import 'package:immich_mobile/widgets/map/map_bottom_sheet.dart';
|
||||||
import 'package:immich_mobile/widgets/map/map_theme_override.dart';
|
import 'package:immich_mobile/widgets/map/map_theme_override.dart';
|
||||||
import 'package:immich_mobile/widgets/map/positioned_asset_marker_icon.dart';
|
import 'package:immich_mobile/widgets/map/positioned_asset_marker_icon.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
|
||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
|
||||||
import 'package:immich_mobile/providers/db.provider.dart';
|
|
||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
|
||||||
import 'package:immich_mobile/utils/immich_loading_overlay.dart';
|
|
||||||
import 'package:immich_mobile/utils/debounce.dart';
|
|
||||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
|
@ -304,7 +305,7 @@ class MapPage extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
right: 0,
|
right: 0,
|
||||||
bottom: MediaQuery.of(context).padding.bottom + 16,
|
bottom: MediaQuery.paddingOf(context).bottom + 16,
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: onZoomToLocation,
|
onPressed: onZoomToLocation,
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
|
|
|
@ -1,28 +1,23 @@
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:immich_mobile/extensions/response_extensions.dart';
|
|
||||||
import 'package:immich_mobile/models/map/map_state.model.dart';
|
import 'package:immich_mobile/models/map/map_state.model.dart';
|
||||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/providers/api.provider.dart';
|
|
||||||
import 'package:logging/logging.dart';
|
|
||||||
import 'package:openapi/api.dart';
|
|
||||||
import 'package:path_provider/path_provider.dart';
|
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
part 'map_state.provider.g.dart';
|
part 'map_state.provider.g.dart';
|
||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
class MapStateNotifier extends _$MapStateNotifier {
|
class MapStateNotifier extends _$MapStateNotifier {
|
||||||
final _log = Logger("MapStateNotifier");
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MapState build() {
|
MapState build() {
|
||||||
final appSettingsProvider = ref.read(appSettingsServiceProvider);
|
final appSettingsProvider = ref.read(appSettingsServiceProvider);
|
||||||
|
|
||||||
// Fetch and save the Style JSONs
|
final lightStyleUrl =
|
||||||
loadStyles();
|
ref.read(serverInfoProvider).serverConfig.mapLightStyleUrl;
|
||||||
|
final darkStyleUrl =
|
||||||
|
ref.read(serverInfoProvider).serverConfig.mapDarkStyleUrl;
|
||||||
|
|
||||||
return MapState(
|
return MapState(
|
||||||
themeMode: ThemeMode.values[
|
themeMode: ThemeMode.values[
|
||||||
appSettingsProvider.getSetting<int>(AppSettingsEnum.mapThemeMode)],
|
appSettingsProvider.getSetting<int>(AppSettingsEnum.mapThemeMode)],
|
||||||
|
@ -34,65 +29,11 @@ class MapStateNotifier extends _$MapStateNotifier {
|
||||||
appSettingsProvider.getSetting<bool>(AppSettingsEnum.mapwithPartners),
|
appSettingsProvider.getSetting<bool>(AppSettingsEnum.mapwithPartners),
|
||||||
relativeTime:
|
relativeTime:
|
||||||
appSettingsProvider.getSetting<int>(AppSettingsEnum.mapRelativeDate),
|
appSettingsProvider.getSetting<int>(AppSettingsEnum.mapRelativeDate),
|
||||||
|
lightStyleFetched: AsyncData(lightStyleUrl),
|
||||||
|
darkStyleFetched: AsyncData(darkStyleUrl),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void loadStyles() async {
|
|
||||||
final documents = (await getApplicationDocumentsDirectory()).path;
|
|
||||||
|
|
||||||
// Set to loading
|
|
||||||
state = state.copyWith(lightStyleFetched: const AsyncLoading());
|
|
||||||
|
|
||||||
// Fetch and save light theme
|
|
||||||
final lightResponse = await ref
|
|
||||||
.read(apiServiceProvider)
|
|
||||||
.mapApi
|
|
||||||
.getMapStyleWithHttpInfo(MapTheme.light);
|
|
||||||
|
|
||||||
if (lightResponse.statusCode >= HttpStatus.badRequest) {
|
|
||||||
state = state.copyWith(
|
|
||||||
lightStyleFetched: AsyncError(lightResponse.body, StackTrace.current),
|
|
||||||
);
|
|
||||||
_log.severe(
|
|
||||||
"Cannot fetch map light style",
|
|
||||||
lightResponse.toLoggerString(),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final lightJSON = lightResponse.body;
|
|
||||||
final lightFile = await File("$documents/map-style-light.json")
|
|
||||||
.writeAsString(lightJSON, flush: true);
|
|
||||||
|
|
||||||
// Update state with path
|
|
||||||
state =
|
|
||||||
state.copyWith(lightStyleFetched: AsyncData(lightFile.absolute.path));
|
|
||||||
|
|
||||||
// Set to loading
|
|
||||||
state = state.copyWith(darkStyleFetched: const AsyncLoading());
|
|
||||||
|
|
||||||
// Fetch and save dark theme
|
|
||||||
final darkResponse = await ref
|
|
||||||
.read(apiServiceProvider)
|
|
||||||
.mapApi
|
|
||||||
.getMapStyleWithHttpInfo(MapTheme.dark);
|
|
||||||
|
|
||||||
if (darkResponse.statusCode >= HttpStatus.badRequest) {
|
|
||||||
state = state.copyWith(
|
|
||||||
darkStyleFetched: AsyncError(darkResponse.body, StackTrace.current),
|
|
||||||
);
|
|
||||||
_log.severe("Cannot fetch map dark style", darkResponse.toLoggerString());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final darkJSON = darkResponse.body;
|
|
||||||
final darkFile = await File("$documents/map-style-dark.json")
|
|
||||||
.writeAsString(darkJSON, flush: true);
|
|
||||||
|
|
||||||
// Update state with path
|
|
||||||
state = state.copyWith(darkStyleFetched: AsyncData(darkFile.absolute.path));
|
|
||||||
}
|
|
||||||
|
|
||||||
void switchTheme(ThemeMode mode) {
|
void switchTheme(ThemeMode mode) {
|
||||||
ref.read(appSettingsServiceProvider).setSetting(
|
ref.read(appSettingsServiceProvider).setSetting(
|
||||||
AppSettingsEnum.mapThemeMode,
|
AppSettingsEnum.mapThemeMode,
|
||||||
|
|
|
@ -34,6 +34,9 @@ class ServerInfoNotifier extends StateNotifier<ServerInfo> {
|
||||||
trashDays: 30,
|
trashDays: 30,
|
||||||
oauthButtonText: '',
|
oauthButtonText: '',
|
||||||
externalDomain: '',
|
externalDomain: '',
|
||||||
|
mapLightStyleUrl:
|
||||||
|
'https://tiles.immich.cloud/v1/style/light.json',
|
||||||
|
mapDarkStyleUrl: 'https://tiles.immich.cloud/v1/style/dark.json',
|
||||||
),
|
),
|
||||||
serverDiskInfo: const ServerDiskInfo(
|
serverDiskInfo: const ServerDiskInfo(
|
||||||
diskAvailable: "0",
|
diskAvailable: "0",
|
||||||
|
|
|
@ -12,6 +12,19 @@ dynamic upgradeDto(dynamic value, String targetType) {
|
||||||
addDefault(value, 'tags', TagsResponse().toJson());
|
addDefault(value, 'tags', TagsResponse().toJson());
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'ServerConfigDto':
|
||||||
|
if (value is Map) {
|
||||||
|
addDefault(
|
||||||
|
value,
|
||||||
|
'mapLightStyleUrl',
|
||||||
|
'https://tiles.immich.cloud/v1/style/light.json',
|
||||||
|
);
|
||||||
|
addDefault(
|
||||||
|
value,
|
||||||
|
'mapDarkStyleUrl',
|
||||||
|
'https://tiles.immich.cloud/v1/style/dark.json',
|
||||||
|
);
|
||||||
|
}
|
||||||
case 'UserResponseDto':
|
case 'UserResponseDto':
|
||||||
if (value is Map) {
|
if (value is Map) {
|
||||||
addDefault(value, 'profileChangedAt', DateTime.now().toIso8601String());
|
addDefault(value, 'profileChangedAt', DateTime.now().toIso8601String());
|
||||||
|
|
BIN
mobile/openapi/README.md
generated
BIN
mobile/openapi/README.md
generated
Binary file not shown.
BIN
mobile/openapi/lib/api.dart
generated
BIN
mobile/openapi/lib/api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/map_api.dart
generated
BIN
mobile/openapi/lib/api/map_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api_client.dart
generated
BIN
mobile/openapi/lib/api_client.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api_helper.dart
generated
BIN
mobile/openapi/lib/api_helper.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/map_theme.dart
generated
BIN
mobile/openapi/lib/model/map_theme.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/server_config_dto.dart
generated
BIN
mobile/openapi/lib/model/server_config_dto.dart
generated
Binary file not shown.
|
@ -928,6 +928,8 @@ export type ServerConfigDto = {
|
||||||
isInitialized: boolean;
|
isInitialized: boolean;
|
||||||
isOnboarded: boolean;
|
isOnboarded: boolean;
|
||||||
loginPageMessage: string;
|
loginPageMessage: string;
|
||||||
|
mapDarkStyleUrl: string;
|
||||||
|
mapLightStyleUrl: string;
|
||||||
oauthButtonText: string;
|
oauthButtonText: string;
|
||||||
trashDays: number;
|
trashDays: number;
|
||||||
userDeleteDelay: number;
|
userDeleteDelay: number;
|
||||||
|
@ -2138,20 +2140,6 @@ export function reverseGeocode({ lat, lon }: {
|
||||||
...opts
|
...opts
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
export function getMapStyle({ key, theme }: {
|
|
||||||
key?: string;
|
|
||||||
theme: MapTheme;
|
|
||||||
}, opts?: Oazapfts.RequestOpts) {
|
|
||||||
return oazapfts.ok(oazapfts.fetchJson<{
|
|
||||||
status: 200;
|
|
||||||
data: object;
|
|
||||||
}>(`/map/style.json${QS.query(QS.explode({
|
|
||||||
key,
|
|
||||||
theme
|
|
||||||
}))}`, {
|
|
||||||
...opts
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
export function searchMemories(opts?: Oazapfts.RequestOpts) {
|
export function searchMemories(opts?: Oazapfts.RequestOpts) {
|
||||||
return oazapfts.ok(oazapfts.fetchJson<{
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
status: 200;
|
status: 200;
|
||||||
|
@ -3469,10 +3457,6 @@ export enum JobCommand {
|
||||||
Empty = "empty",
|
Empty = "empty",
|
||||||
ClearFailed = "clear-failed"
|
ClearFailed = "clear-failed"
|
||||||
}
|
}
|
||||||
export enum MapTheme {
|
|
||||||
Light = "light",
|
|
||||||
Dark = "dark"
|
|
||||||
}
|
|
||||||
export enum MemoryType {
|
export enum MemoryType {
|
||||||
OnThisDay = "on_this_day"
|
OnThisDay = "on_this_day"
|
||||||
}
|
}
|
||||||
|
|
1122
server/package-lock.json
generated
1122
server/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -285,8 +285,8 @@ export const defaults = Object.freeze<SystemConfig>({
|
||||||
},
|
},
|
||||||
map: {
|
map: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
lightStyle: '',
|
lightStyle: 'https://tiles.immich.cloud/v1/style/light.json',
|
||||||
darkStyle: '',
|
darkStyle: 'https://tiles.immich.cloud/v1/style/dark.json',
|
||||||
},
|
},
|
||||||
reverseGeocoding: {
|
reverseGeocoding: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|
|
@ -12,7 +12,6 @@ import {
|
||||||
MapReverseGeocodeDto,
|
MapReverseGeocodeDto,
|
||||||
MapReverseGeocodeResponseDto,
|
MapReverseGeocodeResponseDto,
|
||||||
} from 'src/dtos/map.dto';
|
} from 'src/dtos/map.dto';
|
||||||
import { MapThemeDto } from 'src/dtos/system-config.dto';
|
|
||||||
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||||
import { MapService } from 'src/services/map.service';
|
import { MapService } from 'src/services/map.service';
|
||||||
|
|
||||||
|
@ -30,12 +29,6 @@ export class MapController {
|
||||||
return this.service.getMapMarkers(auth, options);
|
return this.service.getMapMarkers(auth, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Authenticated({ sharedLink: true })
|
|
||||||
@Get('style.json')
|
|
||||||
getMapStyle(@Query() dto: MapThemeDto) {
|
|
||||||
return this.service.getMapStyle(dto.theme);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Authenticated()
|
@Authenticated()
|
||||||
@Get('reverse-geocode')
|
@Get('reverse-geocode')
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
|
|
|
@ -121,6 +121,8 @@ export class ServerConfigDto {
|
||||||
isInitialized!: boolean;
|
isInitialized!: boolean;
|
||||||
isOnboarded!: boolean;
|
isOnboarded!: boolean;
|
||||||
externalDomain!: string;
|
externalDomain!: string;
|
||||||
|
mapDarkStyleUrl!: string;
|
||||||
|
mapLightStyleUrl!: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ServerFeaturesDto {
|
export class ServerFeaturesDto {
|
||||||
|
|
|
@ -296,10 +296,12 @@ class SystemConfigMapDto {
|
||||||
@ValidateBoolean()
|
@ValidateBoolean()
|
||||||
enabled!: boolean;
|
enabled!: boolean;
|
||||||
|
|
||||||
@IsString()
|
@IsNotEmpty()
|
||||||
|
@IsUrl()
|
||||||
lightStyle!: string;
|
lightStyle!: string;
|
||||||
|
|
||||||
@IsString()
|
@IsNotEmpty()
|
||||||
|
@IsUrl()
|
||||||
darkStyle!: string;
|
darkStyle!: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -43,17 +43,6 @@ export class MapService {
|
||||||
return this.mapRepository.getMapMarkers(userIds, albumIds, options);
|
return this.mapRepository.getMapMarkers(userIds, albumIds, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMapStyle(theme: 'light' | 'dark') {
|
|
||||||
const { map } = await this.configCore.getConfig({ withCache: false });
|
|
||||||
const styleUrl = theme === 'dark' ? map.darkStyle : map.lightStyle;
|
|
||||||
|
|
||||||
if (styleUrl) {
|
|
||||||
return this.mapRepository.fetchStyle(styleUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
return JSON.parse(await this.systemMetadataRepository.readFile(`./resources/style-${theme}.json`));
|
|
||||||
}
|
|
||||||
|
|
||||||
async reverseGeocode(dto: MapReverseGeocodeDto) {
|
async reverseGeocode(dto: MapReverseGeocodeDto) {
|
||||||
const { lat: latitude, lon: longitude } = dto;
|
const { lat: latitude, lon: longitude } = dto;
|
||||||
// eventually this should probably return an array of results
|
// eventually this should probably return an array of results
|
||||||
|
|
|
@ -186,6 +186,8 @@ describe(ServerService.name, () => {
|
||||||
isInitialized: undefined,
|
isInitialized: undefined,
|
||||||
isOnboarded: false,
|
isOnboarded: false,
|
||||||
externalDomain: '',
|
externalDomain: '',
|
||||||
|
mapDarkStyleUrl: 'https://tiles.immich.cloud/v1/style/dark.json',
|
||||||
|
mapLightStyleUrl: 'https://tiles.immich.cloud/v1/style/light.json',
|
||||||
});
|
});
|
||||||
expect(systemMock.get).toHaveBeenCalled();
|
expect(systemMock.get).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
|
@ -129,6 +129,8 @@ export class ServerService {
|
||||||
isInitialized,
|
isInitialized,
|
||||||
isOnboarded: onboarding?.isOnboarded || false,
|
isOnboarded: onboarding?.isOnboarded || false,
|
||||||
externalDomain: config.server.externalDomain,
|
externalDomain: config.server.externalDomain,
|
||||||
|
mapDarkStyleUrl: config.map.darkStyle,
|
||||||
|
mapLightStyleUrl: config.map.lightStyle,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -100,8 +100,8 @@ const updatedConfig = Object.freeze<SystemConfig>({
|
||||||
},
|
},
|
||||||
map: {
|
map: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
lightStyle: '',
|
lightStyle: 'https://tiles.immich.cloud/v1/style/light.json',
|
||||||
darkStyle: '',
|
darkStyle: 'https://tiles.immich.cloud/v1/style/dark.json',
|
||||||
},
|
},
|
||||||
reverseGeocoding: {
|
reverseGeocoding: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|
|
@ -6,8 +6,8 @@
|
||||||
import Icon from '$lib/components/elements/icon.svelte';
|
import Icon from '$lib/components/elements/icon.svelte';
|
||||||
import { Theme } from '$lib/constants';
|
import { Theme } from '$lib/constants';
|
||||||
import { colorTheme, mapSettings } from '$lib/stores/preferences.store';
|
import { colorTheme, mapSettings } from '$lib/stores/preferences.store';
|
||||||
import { getAssetThumbnailUrl, getKey, handlePromiseError } from '$lib/utils';
|
import { getAssetThumbnailUrl, handlePromiseError } from '$lib/utils';
|
||||||
import { getMapStyle, MapTheme, type MapMarkerResponseDto } from '@immich/sdk';
|
import { getServerConfig, type MapMarkerResponseDto } from '@immich/sdk';
|
||||||
import mapboxRtlUrl from '@mapbox/mapbox-gl-rtl-text?url';
|
import mapboxRtlUrl from '@mapbox/mapbox-gl-rtl-text?url';
|
||||||
import { mdiCog, mdiMap, mdiMapMarker } from '@mdi/js';
|
import { mdiCog, mdiMap, mdiMapMarker } from '@mdi/js';
|
||||||
import type { Feature, GeoJsonProperties, Geometry, Point } from 'geojson';
|
import type { Feature, GeoJsonProperties, Geometry, Point } from 'geojson';
|
||||||
|
@ -57,11 +57,13 @@
|
||||||
let map: maplibregl.Map;
|
let map: maplibregl.Map;
|
||||||
let marker: maplibregl.Marker | null = null;
|
let marker: maplibregl.Marker | null = null;
|
||||||
|
|
||||||
$: style = (() =>
|
$: style = (async () => {
|
||||||
getMapStyle({
|
const config = await getServerConfig();
|
||||||
theme: ($mapSettings.allowDarkMode ? $colorTheme.value : Theme.LIGHT) as unknown as MapTheme,
|
const theme = $mapSettings.allowDarkMode ? $colorTheme.value : Theme.LIGHT;
|
||||||
key: getKey(),
|
const styleUrl = theme === Theme.DARK ? config.mapDarkStyleUrl : config.mapLightStyleUrl;
|
||||||
}) as Promise<StyleSpecification>)();
|
const style = await fetch(styleUrl).then((response) => response.json());
|
||||||
|
return style as StyleSpecification;
|
||||||
|
})();
|
||||||
|
|
||||||
function handleAssetClick(assetId: string, map: Map | null) {
|
function handleAssetClick(assetId: string, map: Map | null) {
|
||||||
if (!map) {
|
if (!map) {
|
||||||
|
|
|
@ -32,6 +32,8 @@ export const serverConfig = writable<ServerConfig>({
|
||||||
isInitialized: false,
|
isInitialized: false,
|
||||||
isOnboarded: false,
|
isOnboarded: false,
|
||||||
externalDomain: '',
|
externalDomain: '',
|
||||||
|
mapDarkStyleUrl: '',
|
||||||
|
mapLightStyleUrl: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
export const retrieveServerConfig = async () => {
|
export const retrieveServerConfig = async () => {
|
||||||
|
|
Loading…
Reference in a new issue