1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2024-12-28 22:51:59 +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:
Zack Pollard 2024-09-23 21:30:23 +01:00 committed by GitHub
parent e41785b1a1
commit bcd416477b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 659 additions and 798 deletions

6
e2e/package-lock.json generated
View file

@ -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": {

View file

@ -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');

View file

@ -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',
}); });
}); });
}); });

View file

@ -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',
}); });
}); });
}); });

View file

@ -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
}, },

View file

@ -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) {

View file

@ -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(

View file

@ -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,

View file

@ -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",

View file

@ -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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -3167,55 +3167,6 @@
] ]
} }
}, },
"/map/style.json": {
"get": {
"operationId": "getMapStyle",
"parameters": [
{
"name": "key",
"required": false,
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "theme",
"required": true,
"in": "query",
"schema": {
"$ref": "#/components/schemas/MapTheme"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"Map"
]
}
},
"/memories": { "/memories": {
"get": { "get": {
"operationId": "searchMemories", "operationId": "searchMemories",
@ -5356,8 +5307,8 @@
"name": "password", "name": "password",
"required": false, "required": false,
"in": "query", "in": "query",
"example": "password",
"schema": { "schema": {
"example": "password",
"type": "string" "type": "string"
} }
}, },
@ -9695,13 +9646,6 @@
], ],
"type": "object" "type": "object"
}, },
"MapTheme": {
"enum": [
"light",
"dark"
],
"type": "string"
},
"MemoriesResponse": { "MemoriesResponse": {
"properties": { "properties": {
"enabled": { "enabled": {
@ -10917,6 +10861,12 @@
"loginPageMessage": { "loginPageMessage": {
"type": "string" "type": "string"
}, },
"mapDarkStyleUrl": {
"type": "string"
},
"mapLightStyleUrl": {
"type": "string"
},
"oauthButtonText": { "oauthButtonText": {
"type": "string" "type": "string"
}, },
@ -10932,6 +10882,8 @@
"isInitialized", "isInitialized",
"isOnboarded", "isOnboarded",
"loginPageMessage", "loginPageMessage",
"mapDarkStyleUrl",
"mapLightStyleUrl",
"oauthButtonText", "oauthButtonText",
"trashDays", "trashDays",
"userDeleteDelay" "userDeleteDelay"

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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,

View file

@ -7,7 +7,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';
@ -22,12 +21,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)

View file

@ -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 {

View file

@ -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;
} }

View file

@ -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

View file

@ -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();
}); });

View file

@ -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,
}; };
} }

View file

@ -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,

View file

@ -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) {

View file

@ -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 () => {