Browse Source

feat(server,web): server config (#4006)

* feat: server config

* chore: open api

* fix: redirect /map to /photos when disabled
Jason Rasmussen 1 year ago
parent
commit
f1db257628
48 changed files with 1101 additions and 160 deletions
  1. 112 0
      cli/src/api/open-api/api.ts
  2. 6 0
      mobile/openapi/.openapi-generator/FILES
  3. 3 0
      mobile/openapi/README.md
  4. 17 0
      mobile/openapi/doc/ServerConfigDto.md
  5. 1 0
      mobile/openapi/doc/ServerFeaturesDto.md
  6. 38 0
      mobile/openapi/doc/ServerInfoApi.md
  7. 1 0
      mobile/openapi/doc/SystemConfigDto.md
  8. 16 0
      mobile/openapi/doc/SystemConfigMapDto.md
  9. 2 0
      mobile/openapi/lib/api.dart
  10. 41 0
      mobile/openapi/lib/api/server_info_api.dart
  11. 4 0
      mobile/openapi/lib/api_client.dart
  12. 114 0
      mobile/openapi/lib/model/server_config_dto.dart
  13. 9 1
      mobile/openapi/lib/model/server_features_dto.dart
  14. 9 1
      mobile/openapi/lib/model/system_config_dto.dart
  15. 106 0
      mobile/openapi/lib/model/system_config_map_dto.dart
  16. 37 0
      mobile/openapi/test/server_config_dto_test.dart
  17. 5 0
      mobile/openapi/test/server_features_dto_test.dart
  18. 5 0
      mobile/openapi/test/server_info_api_test.dart
  19. 5 0
      mobile/openapi/test/system_config_dto_test.dart
  20. 32 0
      mobile/openapi/test/system_config_map_dto_test.dart
  21. 68 5
      server/immich-openapi-specs.json
  22. 11 6
      server/src/domain/server-info/server-info.dto.ts
  23. 26 14
      server/src/domain/server-info/server-info.service.spec.ts
  24. 14 0
      server/src/domain/server-info/server-info.service.ts
  25. 9 0
      server/src/domain/system-config/dto/system-config-map.dto.ts
  26. 6 0
      server/src/domain/system-config/dto/system-config.dto.ts
  27. 6 1
      server/src/domain/system-config/system-config.core.ts
  28. 4 0
      server/src/domain/system-config/system-config.service.spec.ts
  29. 7 0
      server/src/immich/controllers/server-info.controller.ts
  30. 7 0
      server/src/infra/entities/system-config.entity.ts
  31. 13 0
      server/test/e2e/server-info.e2e-spec.ts
  32. 112 0
      web/src/api/open-api/api.ts
  33. 1 1
      web/src/lib/components/admin-page/jobs/jobs-panel.svelte
  34. 98 0
      web/src/lib/components/admin-page/settings/map-settings/map-settings.svelte
  35. 6 5
      web/src/lib/components/asset-viewer/detail-panel.svelte
  36. 2 2
      web/src/lib/components/forms/login-form.svelte
  37. 2 2
      web/src/lib/components/shared-components/leaflet/tile-layer.svelte
  38. 1 1
      web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte
  39. 6 4
      web/src/lib/components/shared-components/side-bar/side-bar.svelte
  40. 1 1
      web/src/lib/components/user-settings-page/oauth-settings.svelte
  41. 1 1
      web/src/lib/components/user-settings-page/user-settings-list.svelte
  42. 0 3
      web/src/lib/constants.ts
  43. 0 22
      web/src/lib/stores/feature-flags.store.ts
  44. 37 0
      web/src/lib/stores/server-config.store.ts
  45. 71 66
      web/src/routes/(user)/map/+page.svelte
  46. 3 3
      web/src/routes/+layout.svelte
  47. 23 17
      web/src/routes/admin/system-settings/+page.svelte
  48. 3 4
      web/src/routes/auth/login/+page.svelte

+ 112 - 0
cli/src/api/open-api/api.ts

@@ -2343,6 +2343,31 @@ export interface SearchResponseDto {
      */
      */
     'assets': SearchAssetResponseDto;
     'assets': SearchAssetResponseDto;
 }
 }
+/**
+ * 
+ * @export
+ * @interface ServerConfigDto
+ */
+export interface ServerConfigDto {
+    /**
+     * 
+     * @type {string}
+     * @memberof ServerConfigDto
+     */
+    'loginPageMessage': string;
+    /**
+     * 
+     * @type {string}
+     * @memberof ServerConfigDto
+     */
+    'mapTileUrl': string;
+    /**
+     * 
+     * @type {string}
+     * @memberof ServerConfigDto
+     */
+    'oauthButtonText': string;
+}
 /**
 /**
  * 
  * 
  * @export
  * @export
@@ -2367,6 +2392,12 @@ export interface ServerFeaturesDto {
      * @memberof ServerFeaturesDto
      * @memberof ServerFeaturesDto
      */
      */
     'facialRecognition': boolean;
     'facialRecognition': boolean;
+    /**
+     * 
+     * @type {boolean}
+     * @memberof ServerFeaturesDto
+     */
+    'map': boolean;
     /**
     /**
      * 
      * 
      * @type {boolean}
      * @type {boolean}
@@ -2810,6 +2841,12 @@ export interface SystemConfigDto {
      * @memberof SystemConfigDto
      * @memberof SystemConfigDto
      */
      */
     'machineLearning': SystemConfigMachineLearningDto;
     'machineLearning': SystemConfigMachineLearningDto;
+    /**
+     * 
+     * @type {SystemConfigMapDto}
+     * @memberof SystemConfigDto
+     */
+    'map': SystemConfigMapDto;
     /**
     /**
      * 
      * 
      * @type {SystemConfigOAuthDto}
      * @type {SystemConfigOAuthDto}
@@ -3050,6 +3087,25 @@ export interface SystemConfigMachineLearningDto {
      */
      */
     'url': string;
     'url': string;
 }
 }
+/**
+ * 
+ * @export
+ * @interface SystemConfigMapDto
+ */
+export interface SystemConfigMapDto {
+    /**
+     * 
+     * @type {boolean}
+     * @memberof SystemConfigMapDto
+     */
+    'enabled': boolean;
+    /**
+     * 
+     * @type {string}
+     * @memberof SystemConfigMapDto
+     */
+    'tileUrl': string;
+}
 /**
 /**
  * 
  * 
  * @export
  * @export
@@ -10825,6 +10881,35 @@ export class SearchApi extends BaseAPI {
  */
  */
 export const ServerInfoApiAxiosParamCreator = function (configuration?: Configuration) {
 export const ServerInfoApiAxiosParamCreator = function (configuration?: Configuration) {
     return {
     return {
+        /**
+         * 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        getServerConfig: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            const localVarPath = `/server-info/config`;
+            // 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;
+
+
+    
+            setSearchParams(localVarUrlObj, localVarQueryParameter);
+            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+
+            return {
+                url: toPathString(localVarUrlObj),
+                options: localVarRequestOptions,
+            };
+        },
         /**
         /**
          * 
          * 
          * @param {*} [options] Override http request option.
          * @param {*} [options] Override http request option.
@@ -11027,6 +11112,15 @@ export const ServerInfoApiAxiosParamCreator = function (configuration?: Configur
 export const ServerInfoApiFp = function(configuration?: Configuration) {
 export const ServerInfoApiFp = function(configuration?: Configuration) {
     const localVarAxiosParamCreator = ServerInfoApiAxiosParamCreator(configuration)
     const localVarAxiosParamCreator = ServerInfoApiAxiosParamCreator(configuration)
     return {
     return {
+        /**
+         * 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async getServerConfig(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ServerConfigDto>> {
+            const localVarAxiosArgs = await localVarAxiosParamCreator.getServerConfig(options);
+            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
+        },
         /**
         /**
          * 
          * 
          * @param {*} [options] Override http request option.
          * @param {*} [options] Override http request option.
@@ -11091,6 +11185,14 @@ export const ServerInfoApiFp = function(configuration?: Configuration) {
 export const ServerInfoApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
 export const ServerInfoApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
     const localVarFp = ServerInfoApiFp(configuration)
     const localVarFp = ServerInfoApiFp(configuration)
     return {
     return {
+        /**
+         * 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        getServerConfig(options?: AxiosRequestConfig): AxiosPromise<ServerConfigDto> {
+            return localVarFp.getServerConfig(options).then((request) => request(axios, basePath));
+        },
         /**
         /**
          * 
          * 
          * @param {*} [options] Override http request option.
          * @param {*} [options] Override http request option.
@@ -11149,6 +11251,16 @@ export const ServerInfoApiFactory = function (configuration?: Configuration, bas
  * @extends {BaseAPI}
  * @extends {BaseAPI}
  */
  */
 export class ServerInfoApi extends BaseAPI {
 export class ServerInfoApi extends BaseAPI {
+    /**
+     * 
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof ServerInfoApi
+     */
+    public getServerConfig(options?: AxiosRequestConfig) {
+        return ServerInfoApiFp(this.configuration).getServerConfig(options).then((request) => request(this.axios, this.basePath));
+    }
+
     /**
     /**
      * 
      * 
      * @param {*} [options] Override http request option.
      * @param {*} [options] Override http request option.

+ 6 - 0
mobile/openapi/.openapi-generator/FILES

@@ -97,6 +97,7 @@ doc/SearchExploreResponseDto.md
 doc/SearchFacetCountResponseDto.md
 doc/SearchFacetCountResponseDto.md
 doc/SearchFacetResponseDto.md
 doc/SearchFacetResponseDto.md
 doc/SearchResponseDto.md
 doc/SearchResponseDto.md
+doc/ServerConfigDto.md
 doc/ServerFeaturesDto.md
 doc/ServerFeaturesDto.md
 doc/ServerInfoApi.md
 doc/ServerInfoApi.md
 doc/ServerInfoResponseDto.md
 doc/ServerInfoResponseDto.md
@@ -116,6 +117,7 @@ doc/SystemConfigDto.md
 doc/SystemConfigFFmpegDto.md
 doc/SystemConfigFFmpegDto.md
 doc/SystemConfigJobDto.md
 doc/SystemConfigJobDto.md
 doc/SystemConfigMachineLearningDto.md
 doc/SystemConfigMachineLearningDto.md
+doc/SystemConfigMapDto.md
 doc/SystemConfigOAuthDto.md
 doc/SystemConfigOAuthDto.md
 doc/SystemConfigPasswordLoginDto.md
 doc/SystemConfigPasswordLoginDto.md
 doc/SystemConfigStorageTemplateDto.md
 doc/SystemConfigStorageTemplateDto.md
@@ -249,6 +251,7 @@ lib/model/search_explore_response_dto.dart
 lib/model/search_facet_count_response_dto.dart
 lib/model/search_facet_count_response_dto.dart
 lib/model/search_facet_response_dto.dart
 lib/model/search_facet_response_dto.dart
 lib/model/search_response_dto.dart
 lib/model/search_response_dto.dart
+lib/model/server_config_dto.dart
 lib/model/server_features_dto.dart
 lib/model/server_features_dto.dart
 lib/model/server_info_response_dto.dart
 lib/model/server_info_response_dto.dart
 lib/model/server_media_types_response_dto.dart
 lib/model/server_media_types_response_dto.dart
@@ -265,6 +268,7 @@ lib/model/system_config_dto.dart
 lib/model/system_config_f_fmpeg_dto.dart
 lib/model/system_config_f_fmpeg_dto.dart
 lib/model/system_config_job_dto.dart
 lib/model/system_config_job_dto.dart
 lib/model/system_config_machine_learning_dto.dart
 lib/model/system_config_machine_learning_dto.dart
+lib/model/system_config_map_dto.dart
 lib/model/system_config_o_auth_dto.dart
 lib/model/system_config_o_auth_dto.dart
 lib/model/system_config_password_login_dto.dart
 lib/model/system_config_password_login_dto.dart
 lib/model/system_config_storage_template_dto.dart
 lib/model/system_config_storage_template_dto.dart
@@ -382,6 +386,7 @@ test/search_explore_response_dto_test.dart
 test/search_facet_count_response_dto_test.dart
 test/search_facet_count_response_dto_test.dart
 test/search_facet_response_dto_test.dart
 test/search_facet_response_dto_test.dart
 test/search_response_dto_test.dart
 test/search_response_dto_test.dart
+test/server_config_dto_test.dart
 test/server_features_dto_test.dart
 test/server_features_dto_test.dart
 test/server_info_api_test.dart
 test/server_info_api_test.dart
 test/server_info_response_dto_test.dart
 test/server_info_response_dto_test.dart
@@ -401,6 +406,7 @@ test/system_config_dto_test.dart
 test/system_config_f_fmpeg_dto_test.dart
 test/system_config_f_fmpeg_dto_test.dart
 test/system_config_job_dto_test.dart
 test/system_config_job_dto_test.dart
 test/system_config_machine_learning_dto_test.dart
 test/system_config_machine_learning_dto_test.dart
+test/system_config_map_dto_test.dart
 test/system_config_o_auth_dto_test.dart
 test/system_config_o_auth_dto_test.dart
 test/system_config_password_login_dto_test.dart
 test/system_config_password_login_dto_test.dart
 test/system_config_storage_template_dto_test.dart
 test/system_config_storage_template_dto_test.dart

+ 3 - 0
mobile/openapi/README.md

@@ -142,6 +142,7 @@ Class | Method | HTTP request | Description
 *PersonApi* | [**updatePerson**](doc//PersonApi.md#updateperson) | **PUT** /person/{id} | 
 *PersonApi* | [**updatePerson**](doc//PersonApi.md#updateperson) | **PUT** /person/{id} | 
 *SearchApi* | [**getExploreData**](doc//SearchApi.md#getexploredata) | **GET** /search/explore | 
 *SearchApi* | [**getExploreData**](doc//SearchApi.md#getexploredata) | **GET** /search/explore | 
 *SearchApi* | [**search**](doc//SearchApi.md#search) | **GET** /search | 
 *SearchApi* | [**search**](doc//SearchApi.md#search) | **GET** /search | 
+*ServerInfoApi* | [**getServerConfig**](doc//ServerInfoApi.md#getserverconfig) | **GET** /server-info/config | 
 *ServerInfoApi* | [**getServerFeatures**](doc//ServerInfoApi.md#getserverfeatures) | **GET** /server-info/features | 
 *ServerInfoApi* | [**getServerFeatures**](doc//ServerInfoApi.md#getserverfeatures) | **GET** /server-info/features | 
 *ServerInfoApi* | [**getServerInfo**](doc//ServerInfoApi.md#getserverinfo) | **GET** /server-info | 
 *ServerInfoApi* | [**getServerInfo**](doc//ServerInfoApi.md#getserverinfo) | **GET** /server-info | 
 *ServerInfoApi* | [**getServerVersion**](doc//ServerInfoApi.md#getserverversion) | **GET** /server-info/version | 
 *ServerInfoApi* | [**getServerVersion**](doc//ServerInfoApi.md#getserverversion) | **GET** /server-info/version | 
@@ -266,6 +267,7 @@ Class | Method | HTTP request | Description
  - [SearchFacetCountResponseDto](doc//SearchFacetCountResponseDto.md)
  - [SearchFacetCountResponseDto](doc//SearchFacetCountResponseDto.md)
  - [SearchFacetResponseDto](doc//SearchFacetResponseDto.md)
  - [SearchFacetResponseDto](doc//SearchFacetResponseDto.md)
  - [SearchResponseDto](doc//SearchResponseDto.md)
  - [SearchResponseDto](doc//SearchResponseDto.md)
+ - [ServerConfigDto](doc//ServerConfigDto.md)
  - [ServerFeaturesDto](doc//ServerFeaturesDto.md)
  - [ServerFeaturesDto](doc//ServerFeaturesDto.md)
  - [ServerInfoResponseDto](doc//ServerInfoResponseDto.md)
  - [ServerInfoResponseDto](doc//ServerInfoResponseDto.md)
  - [ServerMediaTypesResponseDto](doc//ServerMediaTypesResponseDto.md)
  - [ServerMediaTypesResponseDto](doc//ServerMediaTypesResponseDto.md)
@@ -282,6 +284,7 @@ Class | Method | HTTP request | Description
  - [SystemConfigFFmpegDto](doc//SystemConfigFFmpegDto.md)
  - [SystemConfigFFmpegDto](doc//SystemConfigFFmpegDto.md)
  - [SystemConfigJobDto](doc//SystemConfigJobDto.md)
  - [SystemConfigJobDto](doc//SystemConfigJobDto.md)
  - [SystemConfigMachineLearningDto](doc//SystemConfigMachineLearningDto.md)
  - [SystemConfigMachineLearningDto](doc//SystemConfigMachineLearningDto.md)
+ - [SystemConfigMapDto](doc//SystemConfigMapDto.md)
  - [SystemConfigOAuthDto](doc//SystemConfigOAuthDto.md)
  - [SystemConfigOAuthDto](doc//SystemConfigOAuthDto.md)
  - [SystemConfigPasswordLoginDto](doc//SystemConfigPasswordLoginDto.md)
  - [SystemConfigPasswordLoginDto](doc//SystemConfigPasswordLoginDto.md)
  - [SystemConfigStorageTemplateDto](doc//SystemConfigStorageTemplateDto.md)
  - [SystemConfigStorageTemplateDto](doc//SystemConfigStorageTemplateDto.md)

+ 17 - 0
mobile/openapi/doc/ServerConfigDto.md

@@ -0,0 +1,17 @@
+# openapi.model.ServerConfigDto
+
+## Load the model package
+```dart
+import 'package:openapi/api.dart';
+```
+
+## Properties
+Name | Type | Description | Notes
+------------ | ------------- | ------------- | -------------
+**loginPageMessage** | **String** |  | 
+**mapTileUrl** | **String** |  | 
+**oauthButtonText** | **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 - 0
mobile/openapi/doc/ServerFeaturesDto.md

@@ -11,6 +11,7 @@ Name | Type | Description | Notes
 **clipEncode** | **bool** |  | 
 **clipEncode** | **bool** |  | 
 **configFile** | **bool** |  | 
 **configFile** | **bool** |  | 
 **facialRecognition** | **bool** |  | 
 **facialRecognition** | **bool** |  | 
+**map** | **bool** |  | 
 **oauth** | **bool** |  | 
 **oauth** | **bool** |  | 
 **oauthAutoLaunch** | **bool** |  | 
 **oauthAutoLaunch** | **bool** |  | 
 **passwordLogin** | **bool** |  | 
 **passwordLogin** | **bool** |  | 

+ 38 - 0
mobile/openapi/doc/ServerInfoApi.md

@@ -9,6 +9,7 @@ All URIs are relative to */api*
 
 
 Method | HTTP request | Description
 Method | HTTP request | Description
 ------------- | ------------- | -------------
 ------------- | ------------- | -------------
+[**getServerConfig**](ServerInfoApi.md#getserverconfig) | **GET** /server-info/config | 
 [**getServerFeatures**](ServerInfoApi.md#getserverfeatures) | **GET** /server-info/features | 
 [**getServerFeatures**](ServerInfoApi.md#getserverfeatures) | **GET** /server-info/features | 
 [**getServerInfo**](ServerInfoApi.md#getserverinfo) | **GET** /server-info | 
 [**getServerInfo**](ServerInfoApi.md#getserverinfo) | **GET** /server-info | 
 [**getServerVersion**](ServerInfoApi.md#getserverversion) | **GET** /server-info/version | 
 [**getServerVersion**](ServerInfoApi.md#getserverversion) | **GET** /server-info/version | 
@@ -17,6 +18,43 @@ Method | HTTP request | Description
 [**pingServer**](ServerInfoApi.md#pingserver) | **GET** /server-info/ping | 
 [**pingServer**](ServerInfoApi.md#pingserver) | **GET** /server-info/ping | 
 
 
 
 
+# **getServerConfig**
+> ServerConfigDto getServerConfig()
+
+
+
+### Example
+```dart
+import 'package:openapi/api.dart';
+
+final api_instance = ServerInfoApi();
+
+try {
+    final result = api_instance.getServerConfig();
+    print(result);
+} catch (e) {
+    print('Exception when calling ServerInfoApi->getServerConfig: $e\n');
+}
+```
+
+### Parameters
+This endpoint does not need any parameter.
+
+### Return type
+
+[**ServerConfigDto**](ServerConfigDto.md)
+
+### Authorization
+
+No authorization required
+
+### 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)
+
 # **getServerFeatures**
 # **getServerFeatures**
 > ServerFeaturesDto getServerFeatures()
 > ServerFeaturesDto getServerFeatures()
 
 

+ 1 - 0
mobile/openapi/doc/SystemConfigDto.md

@@ -11,6 +11,7 @@ Name | Type | Description | Notes
 **ffmpeg** | [**SystemConfigFFmpegDto**](SystemConfigFFmpegDto.md) |  | 
 **ffmpeg** | [**SystemConfigFFmpegDto**](SystemConfigFFmpegDto.md) |  | 
 **job** | [**SystemConfigJobDto**](SystemConfigJobDto.md) |  | 
 **job** | [**SystemConfigJobDto**](SystemConfigJobDto.md) |  | 
 **machineLearning** | [**SystemConfigMachineLearningDto**](SystemConfigMachineLearningDto.md) |  | 
 **machineLearning** | [**SystemConfigMachineLearningDto**](SystemConfigMachineLearningDto.md) |  | 
+**map** | [**SystemConfigMapDto**](SystemConfigMapDto.md) |  | 
 **oauth** | [**SystemConfigOAuthDto**](SystemConfigOAuthDto.md) |  | 
 **oauth** | [**SystemConfigOAuthDto**](SystemConfigOAuthDto.md) |  | 
 **passwordLogin** | [**SystemConfigPasswordLoginDto**](SystemConfigPasswordLoginDto.md) |  | 
 **passwordLogin** | [**SystemConfigPasswordLoginDto**](SystemConfigPasswordLoginDto.md) |  | 
 **storageTemplate** | [**SystemConfigStorageTemplateDto**](SystemConfigStorageTemplateDto.md) |  | 
 **storageTemplate** | [**SystemConfigStorageTemplateDto**](SystemConfigStorageTemplateDto.md) |  | 

+ 16 - 0
mobile/openapi/doc/SystemConfigMapDto.md

@@ -0,0 +1,16 @@
+# openapi.model.SystemConfigMapDto
+
+## Load the model package
+```dart
+import 'package:openapi/api.dart';
+```
+
+## Properties
+Name | Type | Description | Notes
+------------ | ------------- | ------------- | -------------
+**enabled** | **bool** |  | 
+**tileUrl** | **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)
+
+

+ 2 - 0
mobile/openapi/lib/api.dart

@@ -128,6 +128,7 @@ part 'model/search_explore_response_dto.dart';
 part 'model/search_facet_count_response_dto.dart';
 part 'model/search_facet_count_response_dto.dart';
 part 'model/search_facet_response_dto.dart';
 part 'model/search_facet_response_dto.dart';
 part 'model/search_response_dto.dart';
 part 'model/search_response_dto.dart';
+part 'model/server_config_dto.dart';
 part 'model/server_features_dto.dart';
 part 'model/server_features_dto.dart';
 part 'model/server_info_response_dto.dart';
 part 'model/server_info_response_dto.dart';
 part 'model/server_media_types_response_dto.dart';
 part 'model/server_media_types_response_dto.dart';
@@ -144,6 +145,7 @@ part 'model/system_config_dto.dart';
 part 'model/system_config_f_fmpeg_dto.dart';
 part 'model/system_config_f_fmpeg_dto.dart';
 part 'model/system_config_job_dto.dart';
 part 'model/system_config_job_dto.dart';
 part 'model/system_config_machine_learning_dto.dart';
 part 'model/system_config_machine_learning_dto.dart';
+part 'model/system_config_map_dto.dart';
 part 'model/system_config_o_auth_dto.dart';
 part 'model/system_config_o_auth_dto.dart';
 part 'model/system_config_password_login_dto.dart';
 part 'model/system_config_password_login_dto.dart';
 part 'model/system_config_storage_template_dto.dart';
 part 'model/system_config_storage_template_dto.dart';

+ 41 - 0
mobile/openapi/lib/api/server_info_api.dart

@@ -16,6 +16,47 @@ class ServerInfoApi {
 
 
   final ApiClient apiClient;
   final ApiClient apiClient;
 
 
+  /// Performs an HTTP 'GET /server-info/config' operation and returns the [Response].
+  Future<Response> getServerConfigWithHttpInfo() async {
+    // ignore: prefer_const_declarations
+    final path = r'/server-info/config';
+
+    // ignore: prefer_final_locals
+    Object? postBody;
+
+    final queryParams = <QueryParam>[];
+    final headerParams = <String, String>{};
+    final formParams = <String, String>{};
+
+    const contentTypes = <String>[];
+
+
+    return apiClient.invokeAPI(
+      path,
+      'GET',
+      queryParams,
+      postBody,
+      headerParams,
+      formParams,
+      contentTypes.isEmpty ? null : contentTypes.first,
+    );
+  }
+
+  Future<ServerConfigDto?> getServerConfig() async {
+    final response = await getServerConfigWithHttpInfo();
+    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) {
+      return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ServerConfigDto',) as ServerConfigDto;
+    
+    }
+    return null;
+  }
+
   /// Performs an HTTP 'GET /server-info/features' operation and returns the [Response].
   /// Performs an HTTP 'GET /server-info/features' operation and returns the [Response].
   Future<Response> getServerFeaturesWithHttpInfo() async {
   Future<Response> getServerFeaturesWithHttpInfo() async {
     // ignore: prefer_const_declarations
     // ignore: prefer_const_declarations

+ 4 - 0
mobile/openapi/lib/api_client.dart

@@ -349,6 +349,8 @@ class ApiClient {
           return SearchFacetResponseDto.fromJson(value);
           return SearchFacetResponseDto.fromJson(value);
         case 'SearchResponseDto':
         case 'SearchResponseDto':
           return SearchResponseDto.fromJson(value);
           return SearchResponseDto.fromJson(value);
+        case 'ServerConfigDto':
+          return ServerConfigDto.fromJson(value);
         case 'ServerFeaturesDto':
         case 'ServerFeaturesDto':
           return ServerFeaturesDto.fromJson(value);
           return ServerFeaturesDto.fromJson(value);
         case 'ServerInfoResponseDto':
         case 'ServerInfoResponseDto':
@@ -381,6 +383,8 @@ class ApiClient {
           return SystemConfigJobDto.fromJson(value);
           return SystemConfigJobDto.fromJson(value);
         case 'SystemConfigMachineLearningDto':
         case 'SystemConfigMachineLearningDto':
           return SystemConfigMachineLearningDto.fromJson(value);
           return SystemConfigMachineLearningDto.fromJson(value);
+        case 'SystemConfigMapDto':
+          return SystemConfigMapDto.fromJson(value);
         case 'SystemConfigOAuthDto':
         case 'SystemConfigOAuthDto':
           return SystemConfigOAuthDto.fromJson(value);
           return SystemConfigOAuthDto.fromJson(value);
         case 'SystemConfigPasswordLoginDto':
         case 'SystemConfigPasswordLoginDto':

+ 114 - 0
mobile/openapi/lib/model/server_config_dto.dart

@@ -0,0 +1,114 @@
+//
+// 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 ServerConfigDto {
+  /// Returns a new [ServerConfigDto] instance.
+  ServerConfigDto({
+    required this.loginPageMessage,
+    required this.mapTileUrl,
+    required this.oauthButtonText,
+  });
+
+  String loginPageMessage;
+
+  String mapTileUrl;
+
+  String oauthButtonText;
+
+  @override
+  bool operator ==(Object other) => identical(this, other) || other is ServerConfigDto &&
+     other.loginPageMessage == loginPageMessage &&
+     other.mapTileUrl == mapTileUrl &&
+     other.oauthButtonText == oauthButtonText;
+
+  @override
+  int get hashCode =>
+    // ignore: unnecessary_parenthesis
+    (loginPageMessage.hashCode) +
+    (mapTileUrl.hashCode) +
+    (oauthButtonText.hashCode);
+
+  @override
+  String toString() => 'ServerConfigDto[loginPageMessage=$loginPageMessage, mapTileUrl=$mapTileUrl, oauthButtonText=$oauthButtonText]';
+
+  Map<String, dynamic> toJson() {
+    final json = <String, dynamic>{};
+      json[r'loginPageMessage'] = this.loginPageMessage;
+      json[r'mapTileUrl'] = this.mapTileUrl;
+      json[r'oauthButtonText'] = this.oauthButtonText;
+    return json;
+  }
+
+  /// Returns a new [ServerConfigDto] instance and imports its values from
+  /// [value] if it's a [Map], null otherwise.
+  // ignore: prefer_constructors_over_static_methods
+  static ServerConfigDto? fromJson(dynamic value) {
+    if (value is Map) {
+      final json = value.cast<String, dynamic>();
+
+      return ServerConfigDto(
+        loginPageMessage: mapValueOfType<String>(json, r'loginPageMessage')!,
+        mapTileUrl: mapValueOfType<String>(json, r'mapTileUrl')!,
+        oauthButtonText: mapValueOfType<String>(json, r'oauthButtonText')!,
+      );
+    }
+    return null;
+  }
+
+  static List<ServerConfigDto> listFromJson(dynamic json, {bool growable = false,}) {
+    final result = <ServerConfigDto>[];
+    if (json is List && json.isNotEmpty) {
+      for (final row in json) {
+        final value = ServerConfigDto.fromJson(row);
+        if (value != null) {
+          result.add(value);
+        }
+      }
+    }
+    return result.toList(growable: growable);
+  }
+
+  static Map<String, ServerConfigDto> mapFromJson(dynamic json) {
+    final map = <String, ServerConfigDto>{};
+    if (json is Map && json.isNotEmpty) {
+      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
+      for (final entry in json.entries) {
+        final value = ServerConfigDto.fromJson(entry.value);
+        if (value != null) {
+          map[entry.key] = value;
+        }
+      }
+    }
+    return map;
+  }
+
+  // maps a json object with a list of ServerConfigDto-objects as value to a dart map
+  static Map<String, List<ServerConfigDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
+    final map = <String, List<ServerConfigDto>>{};
+    if (json is Map && json.isNotEmpty) {
+      // ignore: parameter_assignments
+      json = json.cast<String, dynamic>();
+      for (final entry in json.entries) {
+        map[entry.key] = ServerConfigDto.listFromJson(entry.value, growable: growable,);
+      }
+    }
+    return map;
+  }
+
+  /// The list of required keys that must be present in a JSON.
+  static const requiredKeys = <String>{
+    'loginPageMessage',
+    'mapTileUrl',
+    'oauthButtonText',
+  };
+}
+

+ 9 - 1
mobile/openapi/lib/model/server_features_dto.dart

@@ -16,6 +16,7 @@ class ServerFeaturesDto {
     required this.clipEncode,
     required this.clipEncode,
     required this.configFile,
     required this.configFile,
     required this.facialRecognition,
     required this.facialRecognition,
+    required this.map,
     required this.oauth,
     required this.oauth,
     required this.oauthAutoLaunch,
     required this.oauthAutoLaunch,
     required this.passwordLogin,
     required this.passwordLogin,
@@ -30,6 +31,8 @@ class ServerFeaturesDto {
 
 
   bool facialRecognition;
   bool facialRecognition;
 
 
+  bool map;
+
   bool oauth;
   bool oauth;
 
 
   bool oauthAutoLaunch;
   bool oauthAutoLaunch;
@@ -47,6 +50,7 @@ class ServerFeaturesDto {
      other.clipEncode == clipEncode &&
      other.clipEncode == clipEncode &&
      other.configFile == configFile &&
      other.configFile == configFile &&
      other.facialRecognition == facialRecognition &&
      other.facialRecognition == facialRecognition &&
+     other.map == map &&
      other.oauth == oauth &&
      other.oauth == oauth &&
      other.oauthAutoLaunch == oauthAutoLaunch &&
      other.oauthAutoLaunch == oauthAutoLaunch &&
      other.passwordLogin == passwordLogin &&
      other.passwordLogin == passwordLogin &&
@@ -60,6 +64,7 @@ class ServerFeaturesDto {
     (clipEncode.hashCode) +
     (clipEncode.hashCode) +
     (configFile.hashCode) +
     (configFile.hashCode) +
     (facialRecognition.hashCode) +
     (facialRecognition.hashCode) +
+    (map.hashCode) +
     (oauth.hashCode) +
     (oauth.hashCode) +
     (oauthAutoLaunch.hashCode) +
     (oauthAutoLaunch.hashCode) +
     (passwordLogin.hashCode) +
     (passwordLogin.hashCode) +
@@ -68,13 +73,14 @@ class ServerFeaturesDto {
     (tagImage.hashCode);
     (tagImage.hashCode);
 
 
   @override
   @override
-  String toString() => 'ServerFeaturesDto[clipEncode=$clipEncode, configFile=$configFile, facialRecognition=$facialRecognition, oauth=$oauth, oauthAutoLaunch=$oauthAutoLaunch, passwordLogin=$passwordLogin, search=$search, sidecar=$sidecar, tagImage=$tagImage]';
+  String toString() => 'ServerFeaturesDto[clipEncode=$clipEncode, configFile=$configFile, facialRecognition=$facialRecognition, map=$map, oauth=$oauth, oauthAutoLaunch=$oauthAutoLaunch, passwordLogin=$passwordLogin, search=$search, sidecar=$sidecar, tagImage=$tagImage]';
 
 
   Map<String, dynamic> toJson() {
   Map<String, dynamic> toJson() {
     final json = <String, dynamic>{};
     final json = <String, dynamic>{};
       json[r'clipEncode'] = this.clipEncode;
       json[r'clipEncode'] = this.clipEncode;
       json[r'configFile'] = this.configFile;
       json[r'configFile'] = this.configFile;
       json[r'facialRecognition'] = this.facialRecognition;
       json[r'facialRecognition'] = this.facialRecognition;
+      json[r'map'] = this.map;
       json[r'oauth'] = this.oauth;
       json[r'oauth'] = this.oauth;
       json[r'oauthAutoLaunch'] = this.oauthAutoLaunch;
       json[r'oauthAutoLaunch'] = this.oauthAutoLaunch;
       json[r'passwordLogin'] = this.passwordLogin;
       json[r'passwordLogin'] = this.passwordLogin;
@@ -95,6 +101,7 @@ class ServerFeaturesDto {
         clipEncode: mapValueOfType<bool>(json, r'clipEncode')!,
         clipEncode: mapValueOfType<bool>(json, r'clipEncode')!,
         configFile: mapValueOfType<bool>(json, r'configFile')!,
         configFile: mapValueOfType<bool>(json, r'configFile')!,
         facialRecognition: mapValueOfType<bool>(json, r'facialRecognition')!,
         facialRecognition: mapValueOfType<bool>(json, r'facialRecognition')!,
+        map: mapValueOfType<bool>(json, r'map')!,
         oauth: mapValueOfType<bool>(json, r'oauth')!,
         oauth: mapValueOfType<bool>(json, r'oauth')!,
         oauthAutoLaunch: mapValueOfType<bool>(json, r'oauthAutoLaunch')!,
         oauthAutoLaunch: mapValueOfType<bool>(json, r'oauthAutoLaunch')!,
         passwordLogin: mapValueOfType<bool>(json, r'passwordLogin')!,
         passwordLogin: mapValueOfType<bool>(json, r'passwordLogin')!,
@@ -151,6 +158,7 @@ class ServerFeaturesDto {
     'clipEncode',
     'clipEncode',
     'configFile',
     'configFile',
     'facialRecognition',
     'facialRecognition',
+    'map',
     'oauth',
     'oauth',
     'oauthAutoLaunch',
     'oauthAutoLaunch',
     'passwordLogin',
     'passwordLogin',

+ 9 - 1
mobile/openapi/lib/model/system_config_dto.dart

@@ -16,6 +16,7 @@ class SystemConfigDto {
     required this.ffmpeg,
     required this.ffmpeg,
     required this.job,
     required this.job,
     required this.machineLearning,
     required this.machineLearning,
+    required this.map,
     required this.oauth,
     required this.oauth,
     required this.passwordLogin,
     required this.passwordLogin,
     required this.storageTemplate,
     required this.storageTemplate,
@@ -28,6 +29,8 @@ class SystemConfigDto {
 
 
   SystemConfigMachineLearningDto machineLearning;
   SystemConfigMachineLearningDto machineLearning;
 
 
+  SystemConfigMapDto map;
+
   SystemConfigOAuthDto oauth;
   SystemConfigOAuthDto oauth;
 
 
   SystemConfigPasswordLoginDto passwordLogin;
   SystemConfigPasswordLoginDto passwordLogin;
@@ -41,6 +44,7 @@ class SystemConfigDto {
      other.ffmpeg == ffmpeg &&
      other.ffmpeg == ffmpeg &&
      other.job == job &&
      other.job == job &&
      other.machineLearning == machineLearning &&
      other.machineLearning == machineLearning &&
+     other.map == map &&
      other.oauth == oauth &&
      other.oauth == oauth &&
      other.passwordLogin == passwordLogin &&
      other.passwordLogin == passwordLogin &&
      other.storageTemplate == storageTemplate &&
      other.storageTemplate == storageTemplate &&
@@ -52,19 +56,21 @@ class SystemConfigDto {
     (ffmpeg.hashCode) +
     (ffmpeg.hashCode) +
     (job.hashCode) +
     (job.hashCode) +
     (machineLearning.hashCode) +
     (machineLearning.hashCode) +
+    (map.hashCode) +
     (oauth.hashCode) +
     (oauth.hashCode) +
     (passwordLogin.hashCode) +
     (passwordLogin.hashCode) +
     (storageTemplate.hashCode) +
     (storageTemplate.hashCode) +
     (thumbnail.hashCode);
     (thumbnail.hashCode);
 
 
   @override
   @override
-  String toString() => 'SystemConfigDto[ffmpeg=$ffmpeg, job=$job, machineLearning=$machineLearning, oauth=$oauth, passwordLogin=$passwordLogin, storageTemplate=$storageTemplate, thumbnail=$thumbnail]';
+  String toString() => 'SystemConfigDto[ffmpeg=$ffmpeg, job=$job, machineLearning=$machineLearning, map=$map, oauth=$oauth, passwordLogin=$passwordLogin, storageTemplate=$storageTemplate, thumbnail=$thumbnail]';
 
 
   Map<String, dynamic> toJson() {
   Map<String, dynamic> toJson() {
     final json = <String, dynamic>{};
     final json = <String, dynamic>{};
       json[r'ffmpeg'] = this.ffmpeg;
       json[r'ffmpeg'] = this.ffmpeg;
       json[r'job'] = this.job;
       json[r'job'] = this.job;
       json[r'machineLearning'] = this.machineLearning;
       json[r'machineLearning'] = this.machineLearning;
+      json[r'map'] = this.map;
       json[r'oauth'] = this.oauth;
       json[r'oauth'] = this.oauth;
       json[r'passwordLogin'] = this.passwordLogin;
       json[r'passwordLogin'] = this.passwordLogin;
       json[r'storageTemplate'] = this.storageTemplate;
       json[r'storageTemplate'] = this.storageTemplate;
@@ -83,6 +89,7 @@ class SystemConfigDto {
         ffmpeg: SystemConfigFFmpegDto.fromJson(json[r'ffmpeg'])!,
         ffmpeg: SystemConfigFFmpegDto.fromJson(json[r'ffmpeg'])!,
         job: SystemConfigJobDto.fromJson(json[r'job'])!,
         job: SystemConfigJobDto.fromJson(json[r'job'])!,
         machineLearning: SystemConfigMachineLearningDto.fromJson(json[r'machineLearning'])!,
         machineLearning: SystemConfigMachineLearningDto.fromJson(json[r'machineLearning'])!,
+        map: SystemConfigMapDto.fromJson(json[r'map'])!,
         oauth: SystemConfigOAuthDto.fromJson(json[r'oauth'])!,
         oauth: SystemConfigOAuthDto.fromJson(json[r'oauth'])!,
         passwordLogin: SystemConfigPasswordLoginDto.fromJson(json[r'passwordLogin'])!,
         passwordLogin: SystemConfigPasswordLoginDto.fromJson(json[r'passwordLogin'])!,
         storageTemplate: SystemConfigStorageTemplateDto.fromJson(json[r'storageTemplate'])!,
         storageTemplate: SystemConfigStorageTemplateDto.fromJson(json[r'storageTemplate'])!,
@@ -137,6 +144,7 @@ class SystemConfigDto {
     'ffmpeg',
     'ffmpeg',
     'job',
     'job',
     'machineLearning',
     'machineLearning',
+    'map',
     'oauth',
     'oauth',
     'passwordLogin',
     'passwordLogin',
     'storageTemplate',
     'storageTemplate',

+ 106 - 0
mobile/openapi/lib/model/system_config_map_dto.dart

@@ -0,0 +1,106 @@
+//
+// 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 SystemConfigMapDto {
+  /// Returns a new [SystemConfigMapDto] instance.
+  SystemConfigMapDto({
+    required this.enabled,
+    required this.tileUrl,
+  });
+
+  bool enabled;
+
+  String tileUrl;
+
+  @override
+  bool operator ==(Object other) => identical(this, other) || other is SystemConfigMapDto &&
+     other.enabled == enabled &&
+     other.tileUrl == tileUrl;
+
+  @override
+  int get hashCode =>
+    // ignore: unnecessary_parenthesis
+    (enabled.hashCode) +
+    (tileUrl.hashCode);
+
+  @override
+  String toString() => 'SystemConfigMapDto[enabled=$enabled, tileUrl=$tileUrl]';
+
+  Map<String, dynamic> toJson() {
+    final json = <String, dynamic>{};
+      json[r'enabled'] = this.enabled;
+      json[r'tileUrl'] = this.tileUrl;
+    return json;
+  }
+
+  /// Returns a new [SystemConfigMapDto] instance and imports its values from
+  /// [value] if it's a [Map], null otherwise.
+  // ignore: prefer_constructors_over_static_methods
+  static SystemConfigMapDto? fromJson(dynamic value) {
+    if (value is Map) {
+      final json = value.cast<String, dynamic>();
+
+      return SystemConfigMapDto(
+        enabled: mapValueOfType<bool>(json, r'enabled')!,
+        tileUrl: mapValueOfType<String>(json, r'tileUrl')!,
+      );
+    }
+    return null;
+  }
+
+  static List<SystemConfigMapDto> listFromJson(dynamic json, {bool growable = false,}) {
+    final result = <SystemConfigMapDto>[];
+    if (json is List && json.isNotEmpty) {
+      for (final row in json) {
+        final value = SystemConfigMapDto.fromJson(row);
+        if (value != null) {
+          result.add(value);
+        }
+      }
+    }
+    return result.toList(growable: growable);
+  }
+
+  static Map<String, SystemConfigMapDto> mapFromJson(dynamic json) {
+    final map = <String, SystemConfigMapDto>{};
+    if (json is Map && json.isNotEmpty) {
+      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
+      for (final entry in json.entries) {
+        final value = SystemConfigMapDto.fromJson(entry.value);
+        if (value != null) {
+          map[entry.key] = value;
+        }
+      }
+    }
+    return map;
+  }
+
+  // maps a json object with a list of SystemConfigMapDto-objects as value to a dart map
+  static Map<String, List<SystemConfigMapDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
+    final map = <String, List<SystemConfigMapDto>>{};
+    if (json is Map && json.isNotEmpty) {
+      // ignore: parameter_assignments
+      json = json.cast<String, dynamic>();
+      for (final entry in json.entries) {
+        map[entry.key] = SystemConfigMapDto.listFromJson(entry.value, growable: growable,);
+      }
+    }
+    return map;
+  }
+
+  /// The list of required keys that must be present in a JSON.
+  static const requiredKeys = <String>{
+    'enabled',
+    'tileUrl',
+  };
+}
+

+ 37 - 0
mobile/openapi/test/server_config_dto_test.dart

@@ -0,0 +1,37 @@
+//
+// 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 ServerConfigDto
+void main() {
+  // final instance = ServerConfigDto();
+
+  group('test ServerConfigDto', () {
+    // String loginPageMessage
+    test('to test the property `loginPageMessage`', () async {
+      // TODO
+    });
+
+    // String mapTileUrl
+    test('to test the property `mapTileUrl`', () async {
+      // TODO
+    });
+
+    // String oauthButtonText
+    test('to test the property `oauthButtonText`', () async {
+      // TODO
+    });
+
+
+  });
+
+}

+ 5 - 0
mobile/openapi/test/server_features_dto_test.dart

@@ -31,6 +31,11 @@ void main() {
       // TODO
       // TODO
     });
     });
 
 
+    // bool map
+    test('to test the property `map`', () async {
+      // TODO
+    });
+
     // bool oauth
     // bool oauth
     test('to test the property `oauth`', () async {
     test('to test the property `oauth`', () async {
       // TODO
       // TODO

+ 5 - 0
mobile/openapi/test/server_info_api_test.dart

@@ -17,6 +17,11 @@ void main() {
   // final instance = ServerInfoApi();
   // final instance = ServerInfoApi();
 
 
   group('tests for ServerInfoApi', () {
   group('tests for ServerInfoApi', () {
+    //Future<ServerConfigDto> getServerConfig() async
+    test('test getServerConfig', () async {
+      // TODO
+    });
+
     //Future<ServerFeaturesDto> getServerFeatures() async
     //Future<ServerFeaturesDto> getServerFeatures() async
     test('test getServerFeatures', () async {
     test('test getServerFeatures', () async {
       // TODO
       // TODO

+ 5 - 0
mobile/openapi/test/system_config_dto_test.dart

@@ -31,6 +31,11 @@ void main() {
       // TODO
       // TODO
     });
     });
 
 
+    // SystemConfigMapDto map
+    test('to test the property `map`', () async {
+      // TODO
+    });
+
     // SystemConfigOAuthDto oauth
     // SystemConfigOAuthDto oauth
     test('to test the property `oauth`', () async {
     test('to test the property `oauth`', () async {
       // TODO
       // TODO

+ 32 - 0
mobile/openapi/test/system_config_map_dto_test.dart

@@ -0,0 +1,32 @@
+//
+// 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 SystemConfigMapDto
+void main() {
+  // final instance = SystemConfigMapDto();
+
+  group('test SystemConfigMapDto', () {
+    // bool enabled
+    test('to test the property `enabled`', () async {
+      // TODO
+    });
+
+    // String tileUrl
+    test('to test the property `tileUrl`', () async {
+      // TODO
+    });
+
+
+  });
+
+}

+ 68 - 5
server/immich-openapi-specs.json

@@ -3342,6 +3342,27 @@
         ]
         ]
       }
       }
     },
     },
+    "/server-info/config": {
+      "get": {
+        "operationId": "getServerConfig",
+        "parameters": [],
+        "responses": {
+          "200": {
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ServerConfigDto"
+                }
+              }
+            },
+            "description": ""
+          }
+        },
+        "tags": [
+          "Server Info"
+        ]
+      }
+    },
     "/server-info/features": {
     "/server-info/features": {
       "get": {
       "get": {
         "operationId": "getServerFeatures",
         "operationId": "getServerFeatures",
@@ -6618,6 +6639,25 @@
         ],
         ],
         "type": "object"
         "type": "object"
       },
       },
+      "ServerConfigDto": {
+        "properties": {
+          "loginPageMessage": {
+            "type": "string"
+          },
+          "mapTileUrl": {
+            "type": "string"
+          },
+          "oauthButtonText": {
+            "type": "string"
+          }
+        },
+        "required": [
+          "oauthButtonText",
+          "loginPageMessage",
+          "mapTileUrl"
+        ],
+        "type": "object"
+      },
       "ServerFeaturesDto": {
       "ServerFeaturesDto": {
         "properties": {
         "properties": {
           "clipEncode": {
           "clipEncode": {
@@ -6629,6 +6669,9 @@
           "facialRecognition": {
           "facialRecognition": {
             "type": "boolean"
             "type": "boolean"
           },
           },
+          "map": {
+            "type": "boolean"
+          },
           "oauth": {
           "oauth": {
             "type": "boolean"
             "type": "boolean"
           },
           },
@@ -6649,15 +6692,16 @@
           }
           }
         },
         },
         "required": [
         "required": [
-          "configFile",
           "clipEncode",
           "clipEncode",
+          "configFile",
           "facialRecognition",
           "facialRecognition",
-          "sidecar",
-          "search",
-          "tagImage",
+          "map",
           "oauth",
           "oauth",
           "oauthAutoLaunch",
           "oauthAutoLaunch",
-          "passwordLogin"
+          "passwordLogin",
+          "sidecar",
+          "search",
+          "tagImage"
         ],
         ],
         "type": "object"
         "type": "object"
       },
       },
@@ -6989,6 +7033,9 @@
           "machineLearning": {
           "machineLearning": {
             "$ref": "#/components/schemas/SystemConfigMachineLearningDto"
             "$ref": "#/components/schemas/SystemConfigMachineLearningDto"
           },
           },
+          "map": {
+            "$ref": "#/components/schemas/SystemConfigMapDto"
+          },
           "oauth": {
           "oauth": {
             "$ref": "#/components/schemas/SystemConfigOAuthDto"
             "$ref": "#/components/schemas/SystemConfigOAuthDto"
           },
           },
@@ -7005,6 +7052,7 @@
         "required": [
         "required": [
           "ffmpeg",
           "ffmpeg",
           "machineLearning",
           "machineLearning",
+          "map",
           "oauth",
           "oauth",
           "passwordLogin",
           "passwordLogin",
           "storageTemplate",
           "storageTemplate",
@@ -7162,6 +7210,21 @@
         ],
         ],
         "type": "object"
         "type": "object"
       },
       },
+      "SystemConfigMapDto": {
+        "properties": {
+          "enabled": {
+            "type": "boolean"
+          },
+          "tileUrl": {
+            "type": "string"
+          }
+        },
+        "required": [
+          "enabled",
+          "tileUrl"
+        ],
+        "type": "object"
+      },
       "SystemConfigOAuthDto": {
       "SystemConfigOAuthDto": {
         "properties": {
         "properties": {
           "autoLaunch": {
           "autoLaunch": {

+ 11 - 6
server/src/domain/server-info/server-info.dto.ts

@@ -79,16 +79,21 @@ export class ServerMediaTypesResponseDto {
   sidecar!: string[];
   sidecar!: string[];
 }
 }
 
 
+export class ServerConfigDto {
+  oauthButtonText!: string;
+  loginPageMessage!: string;
+  mapTileUrl!: string;
+}
+
 export class ServerFeaturesDto implements FeatureFlags {
 export class ServerFeaturesDto implements FeatureFlags {
-  configFile!: boolean;
   clipEncode!: boolean;
   clipEncode!: boolean;
+  configFile!: boolean;
   facialRecognition!: boolean;
   facialRecognition!: boolean;
-  sidecar!: boolean;
-  search!: boolean;
-  tagImage!: boolean;
-
-  // TODO: use these instead of `POST oauth/config`
+  map!: boolean;
   oauth!: boolean;
   oauth!: boolean;
   oauthAutoLaunch!: boolean;
   oauthAutoLaunch!: boolean;
   passwordLogin!: boolean;
   passwordLogin!: boolean;
+  sidecar!: boolean;
+  search!: boolean;
+  tagImage!: boolean;
 }
 }

+ 26 - 14
server/src/domain/server-info/server-info.service.spec.ts

@@ -143,22 +143,34 @@ describe(ServerInfoService.name, () => {
     it('should respond the server version', () => {
     it('should respond the server version', () => {
       expect(sut.getVersion()).toEqual(serverVersion);
       expect(sut.getVersion()).toEqual(serverVersion);
     });
     });
+  });
+
+  describe('getFeatures', () => {
+    it('should respond the server features', async () => {
+      await expect(sut.getFeatures()).resolves.toEqual({
+        clipEncode: true,
+        facialRecognition: true,
+        map: true,
+        oauth: false,
+        oauthAutoLaunch: false,
+        passwordLogin: true,
+        search: true,
+        sidecar: true,
+        tagImage: true,
+        configFile: false,
+      });
+      expect(configMock.load).toHaveBeenCalled();
+    });
+  });
 
 
-    describe('getFeatures', () => {
-      it('should respond the server features', async () => {
-        await expect(sut.getFeatures()).resolves.toEqual({
-          clipEncode: true,
-          facialRecognition: true,
-          oauth: false,
-          oauthAutoLaunch: false,
-          passwordLogin: true,
-          search: true,
-          sidecar: true,
-          tagImage: true,
-          configFile: false,
-        });
-        expect(configMock.load).toHaveBeenCalled();
+  describe('getConfig', () => {
+    it('should respond the server configuration', async () => {
+      await expect(sut.getConfig()).resolves.toEqual({
+        loginPageMessage: '',
+        oauthButtonText: 'Login with OAuth',
+        mapTileUrl: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
       });
       });
+      expect(configMock.load).toHaveBeenCalled();
     });
     });
   });
   });
 
 

+ 14 - 0
server/src/domain/server-info/server-info.service.ts

@@ -5,6 +5,7 @@ import { IStorageRepository, StorageCore, StorageFolder } from '../storage';
 import { ISystemConfigRepository, SystemConfigCore } from '../system-config';
 import { ISystemConfigRepository, SystemConfigCore } from '../system-config';
 import { IUserRepository, UserStatsQueryResponse } from '../user';
 import { IUserRepository, UserStatsQueryResponse } from '../user';
 import {
 import {
+  ServerConfigDto,
   ServerFeaturesDto,
   ServerFeaturesDto,
   ServerInfoResponseDto,
   ServerInfoResponseDto,
   ServerMediaTypesResponseDto,
   ServerMediaTypesResponseDto,
@@ -55,6 +56,19 @@ export class ServerInfoService {
     return this.configCore.getFeatures();
     return this.configCore.getFeatures();
   }
   }
 
 
+  async getConfig(): Promise<ServerConfigDto> {
+    const config = await this.configCore.getConfig();
+
+    // TODO move to system config
+    const loginPageMessage = process.env.PUBLIC_LOGIN_PAGE_MESSAGE || '';
+
+    return {
+      loginPageMessage,
+      mapTileUrl: config.map.tileUrl,
+      oauthButtonText: config.oauth.buttonText,
+    };
+  }
+
   async getStats(): Promise<ServerStatsResponseDto> {
   async getStats(): Promise<ServerStatsResponseDto> {
     const userStats: UserStatsQueryResponse[] = await this.userRepository.getUserStats();
     const userStats: UserStatsQueryResponse[] = await this.userRepository.getUserStats();
     const serverStats = new ServerStatsResponseDto();
     const serverStats = new ServerStatsResponseDto();

+ 9 - 0
server/src/domain/system-config/dto/system-config-map.dto.ts

@@ -0,0 +1,9 @@
+import { IsBoolean, IsString } from 'class-validator';
+
+export class SystemConfigMapDto {
+  @IsBoolean()
+  enabled!: boolean;
+
+  @IsString()
+  tileUrl!: string;
+}

+ 6 - 0
server/src/domain/system-config/dto/system-config.dto.ts

@@ -5,6 +5,7 @@ import { IsObject, ValidateNested } from 'class-validator';
 import { SystemConfigFFmpegDto } from './system-config-ffmpeg.dto';
 import { SystemConfigFFmpegDto } from './system-config-ffmpeg.dto';
 import { SystemConfigJobDto } from './system-config-job.dto';
 import { SystemConfigJobDto } from './system-config-job.dto';
 import { SystemConfigMachineLearningDto } from './system-config-machine-learning.dto';
 import { SystemConfigMachineLearningDto } from './system-config-machine-learning.dto';
+import { SystemConfigMapDto } from './system-config-map.dto';
 import { SystemConfigOAuthDto } from './system-config-oauth.dto';
 import { SystemConfigOAuthDto } from './system-config-oauth.dto';
 import { SystemConfigPasswordLoginDto } from './system-config-password-login.dto';
 import { SystemConfigPasswordLoginDto } from './system-config-password-login.dto';
 import { SystemConfigStorageTemplateDto } from './system-config-storage-template.dto';
 import { SystemConfigStorageTemplateDto } from './system-config-storage-template.dto';
@@ -20,6 +21,11 @@ export class SystemConfigDto implements SystemConfig {
   @IsObject()
   @IsObject()
   machineLearning!: SystemConfigMachineLearningDto;
   machineLearning!: SystemConfigMachineLearningDto;
 
 
+  @Type(() => SystemConfigMapDto)
+  @ValidateNested()
+  @IsObject()
+  map!: SystemConfigMapDto;
+
   @Type(() => SystemConfigOAuthDto)
   @Type(() => SystemConfigOAuthDto)
   @ValidateNested()
   @ValidateNested()
   @IsObject()
   @IsObject()

+ 6 - 1
server/src/domain/system-config/system-config.core.ts

@@ -55,7 +55,6 @@ export const defaults = Object.freeze<SystemConfig>({
     [QueueName.THUMBNAIL_GENERATION]: { concurrency: 5 },
     [QueueName.THUMBNAIL_GENERATION]: { concurrency: 5 },
     [QueueName.VIDEO_CONVERSION]: { concurrency: 1 },
     [QueueName.VIDEO_CONVERSION]: { concurrency: 1 },
   },
   },
-
   machineLearning: {
   machineLearning: {
     enabled: process.env.IMMICH_MACHINE_LEARNING_ENABLED !== 'false',
     enabled: process.env.IMMICH_MACHINE_LEARNING_ENABLED !== 'false',
     url: process.env.IMMICH_MACHINE_LEARNING_URL || 'http://immich-machine-learning:3003',
     url: process.env.IMMICH_MACHINE_LEARNING_URL || 'http://immich-machine-learning:3003',
@@ -75,6 +74,10 @@ export const defaults = Object.freeze<SystemConfig>({
       maxDistance: 0.6,
       maxDistance: 0.6,
     },
     },
   },
   },
+  map: {
+    enabled: true,
+    tileUrl: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
+  },
   oauth: {
   oauth: {
     enabled: false,
     enabled: false,
     issuerUrl: '',
     issuerUrl: '',
@@ -108,6 +111,7 @@ export enum FeatureFlag {
   CLIP_ENCODE = 'clipEncode',
   CLIP_ENCODE = 'clipEncode',
   FACIAL_RECOGNITION = 'facialRecognition',
   FACIAL_RECOGNITION = 'facialRecognition',
   TAG_IMAGE = 'tagImage',
   TAG_IMAGE = 'tagImage',
+  MAP = 'map',
   SIDECAR = 'sidecar',
   SIDECAR = 'sidecar',
   SEARCH = 'search',
   SEARCH = 'search',
   OAUTH = 'oauth',
   OAUTH = 'oauth',
@@ -169,6 +173,7 @@ export class SystemConfigCore {
       [FeatureFlag.CLIP_ENCODE]: mlEnabled && config.machineLearning.clip.enabled,
       [FeatureFlag.CLIP_ENCODE]: mlEnabled && config.machineLearning.clip.enabled,
       [FeatureFlag.FACIAL_RECOGNITION]: mlEnabled && config.machineLearning.facialRecognition.enabled,
       [FeatureFlag.FACIAL_RECOGNITION]: mlEnabled && config.machineLearning.facialRecognition.enabled,
       [FeatureFlag.TAG_IMAGE]: mlEnabled && config.machineLearning.classification.enabled,
       [FeatureFlag.TAG_IMAGE]: mlEnabled && config.machineLearning.classification.enabled,
+      [FeatureFlag.MAP]: config.map.enabled,
       [FeatureFlag.SIDECAR]: true,
       [FeatureFlag.SIDECAR]: true,
       [FeatureFlag.SEARCH]: process.env.TYPESENSE_ENABLED !== 'false',
       [FeatureFlag.SEARCH]: process.env.TYPESENSE_ENABLED !== 'false',
 
 

+ 4 - 0
server/src/domain/system-config/system-config.service.spec.ts

@@ -73,6 +73,10 @@ const updatedConfig = Object.freeze<SystemConfig>({
       maxDistance: 0.6,
       maxDistance: 0.6,
     },
     },
   },
   },
+  map: {
+    enabled: true,
+    tileUrl: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
+  },
   oauth: {
   oauth: {
     autoLaunch: true,
     autoLaunch: true,
     autoRegister: true,
     autoRegister: true,

+ 7 - 0
server/src/immich/controllers/server-info.controller.ts

@@ -1,4 +1,5 @@
 import {
 import {
+  ServerConfigDto,
   ServerFeaturesDto,
   ServerFeaturesDto,
   ServerInfoResponseDto,
   ServerInfoResponseDto,
   ServerInfoService,
   ServerInfoService,
@@ -42,6 +43,12 @@ export class ServerInfoController {
     return this.service.getFeatures();
     return this.service.getFeatures();
   }
   }
 
 
+  @PublicRoute()
+  @Get('config')
+  getServerConfig(): Promise<ServerConfigDto> {
+    return this.service.getConfig();
+  }
+
   @AdminRoute()
   @AdminRoute()
   @Get('stats')
   @Get('stats')
   getStats(): Promise<ServerStatsResponseDto> {
   getStats(): Promise<ServerStatsResponseDto> {

+ 7 - 0
server/src/infra/entities/system-config.entity.ts

@@ -58,6 +58,9 @@ export enum SystemConfigKey {
   MACHINE_LEARNING_FACIAL_RECOGNITION_MIN_SCORE = 'machineLearning.facialRecognition.minScore',
   MACHINE_LEARNING_FACIAL_RECOGNITION_MIN_SCORE = 'machineLearning.facialRecognition.minScore',
   MACHINE_LEARNING_FACIAL_RECOGNITION_MAX_DISTANCE = 'machineLearning.facialRecognition.maxDistance',
   MACHINE_LEARNING_FACIAL_RECOGNITION_MAX_DISTANCE = 'machineLearning.facialRecognition.maxDistance',
 
 
+  MAP_ENABLED = 'map.enabled',
+  MAP_TILE_URL = 'map.tileUrl',
+
   OAUTH_ENABLED = 'oauth.enabled',
   OAUTH_ENABLED = 'oauth.enabled',
   OAUTH_ISSUER_URL = 'oauth.issuerUrl',
   OAUTH_ISSUER_URL = 'oauth.issuerUrl',
   OAUTH_CLIENT_ID = 'oauth.clientId',
   OAUTH_CLIENT_ID = 'oauth.clientId',
@@ -164,6 +167,10 @@ export interface SystemConfig {
       maxDistance: number;
       maxDistance: number;
     };
     };
   };
   };
+  map: {
+    enabled: boolean;
+    tileUrl: string;
+  };
   oauth: {
   oauth: {
     enabled: boolean;
     enabled: boolean;
     issuerUrl: string;
     issuerUrl: string;

+ 13 - 0
server/test/e2e/server-info.e2e-spec.ts

@@ -83,6 +83,7 @@ describe(`${ServerInfoController.name} (e2e)`, () => {
         clipEncode: true,
         clipEncode: true,
         configFile: false,
         configFile: false,
         facialRecognition: true,
         facialRecognition: true,
+        map: true,
         oauth: false,
         oauth: false,
         oauthAutoLaunch: false,
         oauthAutoLaunch: false,
         passwordLogin: true,
         passwordLogin: true,
@@ -93,6 +94,18 @@ describe(`${ServerInfoController.name} (e2e)`, () => {
     });
     });
   });
   });
 
 
+  describe('GET /server-info/config', () => {
+    it('should respond with the server configuration', async () => {
+      const { status, body } = await request(server).get('/server-info/config');
+      expect(status).toBe(200);
+      expect(body).toEqual({
+        loginPageMessage: '',
+        oauthButtonText: 'Login with OAuth',
+        mapTileUrl: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
+      });
+    });
+  });
+
   describe('GET /server-info/stats', () => {
   describe('GET /server-info/stats', () => {
     it('should require authentication', async () => {
     it('should require authentication', async () => {
       const { status, body } = await request(server).get('/server-info/stats');
       const { status, body } = await request(server).get('/server-info/stats');

+ 112 - 0
web/src/api/open-api/api.ts

@@ -2343,6 +2343,31 @@ export interface SearchResponseDto {
      */
      */
     'assets': SearchAssetResponseDto;
     'assets': SearchAssetResponseDto;
 }
 }
+/**
+ * 
+ * @export
+ * @interface ServerConfigDto
+ */
+export interface ServerConfigDto {
+    /**
+     * 
+     * @type {string}
+     * @memberof ServerConfigDto
+     */
+    'loginPageMessage': string;
+    /**
+     * 
+     * @type {string}
+     * @memberof ServerConfigDto
+     */
+    'mapTileUrl': string;
+    /**
+     * 
+     * @type {string}
+     * @memberof ServerConfigDto
+     */
+    'oauthButtonText': string;
+}
 /**
 /**
  * 
  * 
  * @export
  * @export
@@ -2367,6 +2392,12 @@ export interface ServerFeaturesDto {
      * @memberof ServerFeaturesDto
      * @memberof ServerFeaturesDto
      */
      */
     'facialRecognition': boolean;
     'facialRecognition': boolean;
+    /**
+     * 
+     * @type {boolean}
+     * @memberof ServerFeaturesDto
+     */
+    'map': boolean;
     /**
     /**
      * 
      * 
      * @type {boolean}
      * @type {boolean}
@@ -2810,6 +2841,12 @@ export interface SystemConfigDto {
      * @memberof SystemConfigDto
      * @memberof SystemConfigDto
      */
      */
     'machineLearning': SystemConfigMachineLearningDto;
     'machineLearning': SystemConfigMachineLearningDto;
+    /**
+     * 
+     * @type {SystemConfigMapDto}
+     * @memberof SystemConfigDto
+     */
+    'map': SystemConfigMapDto;
     /**
     /**
      * 
      * 
      * @type {SystemConfigOAuthDto}
      * @type {SystemConfigOAuthDto}
@@ -3050,6 +3087,25 @@ export interface SystemConfigMachineLearningDto {
      */
      */
     'url': string;
     'url': string;
 }
 }
+/**
+ * 
+ * @export
+ * @interface SystemConfigMapDto
+ */
+export interface SystemConfigMapDto {
+    /**
+     * 
+     * @type {boolean}
+     * @memberof SystemConfigMapDto
+     */
+    'enabled': boolean;
+    /**
+     * 
+     * @type {string}
+     * @memberof SystemConfigMapDto
+     */
+    'tileUrl': string;
+}
 /**
 /**
  * 
  * 
  * @export
  * @export
@@ -10825,6 +10881,35 @@ export class SearchApi extends BaseAPI {
  */
  */
 export const ServerInfoApiAxiosParamCreator = function (configuration?: Configuration) {
 export const ServerInfoApiAxiosParamCreator = function (configuration?: Configuration) {
     return {
     return {
+        /**
+         * 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        getServerConfig: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            const localVarPath = `/server-info/config`;
+            // 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;
+
+
+    
+            setSearchParams(localVarUrlObj, localVarQueryParameter);
+            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+
+            return {
+                url: toPathString(localVarUrlObj),
+                options: localVarRequestOptions,
+            };
+        },
         /**
         /**
          * 
          * 
          * @param {*} [options] Override http request option.
          * @param {*} [options] Override http request option.
@@ -11027,6 +11112,15 @@ export const ServerInfoApiAxiosParamCreator = function (configuration?: Configur
 export const ServerInfoApiFp = function(configuration?: Configuration) {
 export const ServerInfoApiFp = function(configuration?: Configuration) {
     const localVarAxiosParamCreator = ServerInfoApiAxiosParamCreator(configuration)
     const localVarAxiosParamCreator = ServerInfoApiAxiosParamCreator(configuration)
     return {
     return {
+        /**
+         * 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async getServerConfig(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ServerConfigDto>> {
+            const localVarAxiosArgs = await localVarAxiosParamCreator.getServerConfig(options);
+            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
+        },
         /**
         /**
          * 
          * 
          * @param {*} [options] Override http request option.
          * @param {*} [options] Override http request option.
@@ -11091,6 +11185,14 @@ export const ServerInfoApiFp = function(configuration?: Configuration) {
 export const ServerInfoApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
 export const ServerInfoApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
     const localVarFp = ServerInfoApiFp(configuration)
     const localVarFp = ServerInfoApiFp(configuration)
     return {
     return {
+        /**
+         * 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        getServerConfig(options?: AxiosRequestConfig): AxiosPromise<ServerConfigDto> {
+            return localVarFp.getServerConfig(options).then((request) => request(axios, basePath));
+        },
         /**
         /**
          * 
          * 
          * @param {*} [options] Override http request option.
          * @param {*} [options] Override http request option.
@@ -11149,6 +11251,16 @@ export const ServerInfoApiFactory = function (configuration?: Configuration, bas
  * @extends {BaseAPI}
  * @extends {BaseAPI}
  */
  */
 export class ServerInfoApi extends BaseAPI {
 export class ServerInfoApi extends BaseAPI {
+    /**
+     * 
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof ServerInfoApi
+     */
+    public getServerConfig(options?: AxiosRequestConfig) {
+        return ServerInfoApiFp(this.configuration).getServerConfig(options).then((request) => request(this.axios, this.basePath));
+    }
+
     /**
     /**
      * 
      * 
      * @param {*} [options] Override http request option.
      * @param {*} [options] Override http request option.

+ 1 - 1
web/src/lib/components/admin-page/jobs/jobs-panel.svelte

@@ -4,7 +4,7 @@
     NotificationType,
     NotificationType,
   } from '$lib/components/shared-components/notification/notification';
   } from '$lib/components/shared-components/notification/notification';
   import { AppRoute } from '$lib/constants';
   import { AppRoute } from '$lib/constants';
-  import { featureFlags } from '$lib/stores/feature-flags.store';
+  import { featureFlags } from '$lib/stores/server-config.store';
   import { handleError } from '$lib/utils/handle-error';
   import { handleError } from '$lib/utils/handle-error';
   import { AllJobStatusResponseDto, api, JobCommand, JobCommandDto, JobName } from '@api';
   import { AllJobStatusResponseDto, api, JobCommand, JobCommandDto, JobName } from '@api';
   import type { ComponentType } from 'svelte';
   import type { ComponentType } from 'svelte';

+ 98 - 0
web/src/lib/components/admin-page/settings/map-settings/map-settings.svelte

@@ -0,0 +1,98 @@
+<script lang="ts">
+  import {
+    notificationController,
+    NotificationType,
+  } from '$lib/components/shared-components/notification/notification';
+  import { handleError } from '$lib/utils/handle-error';
+  import { api, SystemConfigMapDto } from '@api';
+  import { isEqual } from 'lodash-es';
+  import { fade } from 'svelte/transition';
+  import SettingButtonsRow from '../setting-buttons-row.svelte';
+  import SettingSwitch from '../setting-switch.svelte';
+  import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
+
+  export let mapConfig: SystemConfigMapDto; // this is the config that is being edited
+  export let disabled = false;
+
+  let savedConfig: SystemConfigMapDto;
+  let defaultConfig: SystemConfigMapDto;
+
+  async function getConfigs() {
+    [savedConfig, defaultConfig] = await Promise.all([
+      api.systemConfigApi.getConfig().then((res) => res.data.map),
+      api.systemConfigApi.getDefaults().then((res) => res.data.map),
+    ]);
+  }
+
+  async function saveSetting() {
+    try {
+      const { data: current } = await api.systemConfigApi.getConfig();
+      const { data: updated } = await api.systemConfigApi.updateConfig({
+        systemConfigDto: { ...current, map: mapConfig },
+      });
+
+      mapConfig = { ...updated.map };
+      savedConfig = { ...updated.map };
+
+      notificationController.show({ message: 'Settings saved', type: NotificationType.Info });
+    } catch (error) {
+      handleError(error, 'Unable to save settings');
+    }
+  }
+
+  async function reset() {
+    const { data: resetConfig } = await api.systemConfigApi.getConfig();
+
+    mapConfig = { ...resetConfig.map };
+    savedConfig = { ...resetConfig.map };
+
+    notificationController.show({
+      message: 'Reset settings to the recent saved settings',
+      type: NotificationType.Info,
+    });
+  }
+
+  async function resetToDefault() {
+    const { data: configs } = await api.systemConfigApi.getDefaults();
+
+    mapConfig = { ...configs.map };
+    defaultConfig = { ...configs.map };
+
+    notificationController.show({
+      message: 'Reset map settings to default',
+      type: NotificationType.Info,
+    });
+  }
+</script>
+
+<div>
+  {#await getConfigs() then}
+    <div in:fade={{ duration: 500 }}>
+      <form autocomplete="off" on:submit|preventDefault>
+        <div class="ml-4 mt-4 flex flex-col gap-4">
+          <SettingSwitch title="ENABLED" {disabled} subtitle="Enable map features" bind:checked={mapConfig.enabled} />
+
+          <hr />
+
+          <SettingInputField
+            inputType={SettingInputFieldType.TEXT}
+            label="Tile URL"
+            desc="URL to a leaflet compatible tile server"
+            bind:value={mapConfig.tileUrl}
+            required={true}
+            disabled={disabled || !mapConfig.enabled}
+            isEdited={mapConfig.tileUrl !== savedConfig.tileUrl}
+          />
+
+          <SettingButtonsRow
+            on:reset={reset}
+            on:save={saveSetting}
+            on:reset-to-default={resetToDefault}
+            showResetToDefault={!isEqual(savedConfig, defaultConfig)}
+            {disabled}
+          />
+        </div>
+      </form>
+    </div>
+  {/await}
+</div>

+ 6 - 5
web/src/lib/components/asset-viewer/detail-panel.svelte

@@ -1,18 +1,19 @@
 <script lang="ts">
 <script lang="ts">
   import { page } from '$app/stores';
   import { page } from '$app/stores';
   import { locale } from '$lib/stores/preferences.store';
   import { locale } from '$lib/stores/preferences.store';
+  import { featureFlags, serverConfig } from '$lib/stores/server-config.store';
+  import { getAssetFilename } from '$lib/utils/asset-utils';
+  import { AlbumResponseDto, AssetResponseDto, ThumbnailFormat, api } from '@api';
   import type { LatLngTuple } from 'leaflet';
   import type { LatLngTuple } from 'leaflet';
   import { DateTime } from 'luxon';
   import { DateTime } from 'luxon';
+  import { createEventDispatcher } from 'svelte';
   import Calendar from 'svelte-material-icons/Calendar.svelte';
   import Calendar from 'svelte-material-icons/Calendar.svelte';
   import CameraIris from 'svelte-material-icons/CameraIris.svelte';
   import CameraIris from 'svelte-material-icons/CameraIris.svelte';
   import Close from 'svelte-material-icons/Close.svelte';
   import Close from 'svelte-material-icons/Close.svelte';
   import ImageOutline from 'svelte-material-icons/ImageOutline.svelte';
   import ImageOutline from 'svelte-material-icons/ImageOutline.svelte';
   import MapMarkerOutline from 'svelte-material-icons/MapMarkerOutline.svelte';
   import MapMarkerOutline from 'svelte-material-icons/MapMarkerOutline.svelte';
-  import { createEventDispatcher } from 'svelte';
-  import { AssetResponseDto, AlbumResponseDto, api, ThumbnailFormat } from '@api';
   import { asByteUnitString } from '../../utils/byte-units';
   import { asByteUnitString } from '../../utils/byte-units';
   import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
   import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
-  import { getAssetFilename } from '$lib/utils/asset-utils';
   import UserAvatar from '../shared-components/user-avatar.svelte';
   import UserAvatar from '../shared-components/user-avatar.svelte';
 
 
   export let asset: AssetResponseDto;
   export let asset: AssetResponseDto;
@@ -268,12 +269,12 @@
   </div>
   </div>
 </section>
 </section>
 
 
-{#if latlng}
+{#if latlng && $featureFlags.loaded && $featureFlags.map}
   <div class="h-[360px]">
   <div class="h-[360px]">
     {#await import('../shared-components/leaflet') then { Map, TileLayer, Marker }}
     {#await import('../shared-components/leaflet') then { Map, TileLayer, Marker }}
       <Map center={latlng} zoom={14}>
       <Map center={latlng} zoom={14}>
         <TileLayer
         <TileLayer
-          urlTemplate={'https://tile.openstreetmap.org/{z}/{x}/{y}.png'}
+          urlTemplate={$serverConfig.mapTileUrl}
           options={{
           options={{
             attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
             attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
           }}
           }}

+ 2 - 2
web/src/lib/components/forms/login-form.svelte

@@ -2,7 +2,7 @@
   import { goto } from '$app/navigation';
   import { goto } from '$app/navigation';
   import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
   import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
   import { AppRoute } from '$lib/constants';
   import { AppRoute } from '$lib/constants';
-  import { featureFlags } from '$lib/stores/feature-flags.store';
+  import { featureFlags, serverConfig } from '$lib/stores/server-config.store';
   import { getServerErrorMessage, handleError } from '$lib/utils/handle-error';
   import { getServerErrorMessage, handleError } from '$lib/utils/handle-error';
   import { api, oauth } from '@api';
   import { api, oauth } from '@api';
   import { createEventDispatcher, onMount } from 'svelte';
   import { createEventDispatcher, onMount } from 'svelte';
@@ -158,7 +158,7 @@
           <LoadingSpinner />
           <LoadingSpinner />
         </span>
         </span>
       {:else}
       {:else}
-        {$featureFlags.passwordLogin ? 'Login with OAuth' : 'Login'}
+        {$serverConfig.oauthButtonText}
       {/if}
       {/if}
     </Button>
     </Button>
   </div>
   </div>

+ 2 - 2
web/src/lib/components/shared-components/leaflet/tile-layer.svelte

@@ -1,6 +1,6 @@
 <script lang="ts">
 <script lang="ts">
-  import { onDestroy, onMount } from 'svelte';
   import { TileLayer, type TileLayerOptions } from 'leaflet';
   import { TileLayer, type TileLayerOptions } from 'leaflet';
+  import { onDestroy, onMount } from 'svelte';
   import { getMapContext } from './map.svelte';
   import { getMapContext } from './map.svelte';
 
 
   export let urlTemplate: string;
   export let urlTemplate: string;
@@ -15,6 +15,6 @@
   });
   });
 
 
   onDestroy(() => {
   onDestroy(() => {
-    if (tileLayer) tileLayer.remove();
+    tileLayer?.remove();
   });
   });
 </script>
 </script>

+ 1 - 1
web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte

@@ -16,7 +16,7 @@
   import IconButton from '$lib/components/elements/buttons/icon-button.svelte';
   import IconButton from '$lib/components/elements/buttons/icon-button.svelte';
   import Cog from 'svelte-material-icons/Cog.svelte';
   import Cog from 'svelte-material-icons/Cog.svelte';
   import UserAvatar from '../user-avatar.svelte';
   import UserAvatar from '../user-avatar.svelte';
-  import { featureFlags } from '$lib/stores/feature-flags.store';
+  import { featureFlags } from '$lib/stores/server-config.store';
   export let user: UserResponseDto;
   export let user: UserResponseDto;
   export let showUploadButton = true;
   export let showUploadButton = true;
 
 

+ 6 - 4
web/src/lib/components/shared-components/side-bar/side-bar.svelte

@@ -17,7 +17,7 @@
   import SideBarButton from './side-bar-button.svelte';
   import SideBarButton from './side-bar-button.svelte';
   import { locale } from '$lib/stores/preferences.store';
   import { locale } from '$lib/stores/preferences.store';
   import SideBarSection from './side-bar-section.svelte';
   import SideBarSection from './side-bar-section.svelte';
-  import { featureFlags } from '$lib/stores/feature-flags.store';
+  import { featureFlags } from '$lib/stores/server-config.store';
 
 
   const getStats = async (dto: AssetApiGetAssetStatsRequest) => {
   const getStats = async (dto: AssetApiGetAssetStatsRequest) => {
     const { data: stats } = await api.assetApi.getAssetStats(dto);
     const { data: stats } = await api.assetApi.getAssetStats(dto);
@@ -62,9 +62,11 @@
       <SideBarButton title="Explore" logo={Magnify} isSelected={$page.route.id === '/(user)/explore'} />
       <SideBarButton title="Explore" logo={Magnify} isSelected={$page.route.id === '/(user)/explore'} />
     </a>
     </a>
   {/if}
   {/if}
-  <a data-sveltekit-preload-data="hover" href={AppRoute.MAP} draggable="false">
-    <SideBarButton title="Map" logo={Map} isSelected={$page.route.id === '/(user)/map'} />
-  </a>
+  {#if $featureFlags.map}
+    <a data-sveltekit-preload-data="hover" href={AppRoute.MAP} draggable="false">
+      <SideBarButton title="Map" logo={Map} isSelected={$page.route.id === '/(user)/map'} />
+    </a>
+  {/if}
   <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"

+ 1 - 1
web/src/lib/components/user-settings-page/oauth-settings.svelte

@@ -1,6 +1,6 @@
 <script lang="ts">
 <script lang="ts">
   import { goto } from '$app/navigation';
   import { goto } from '$app/navigation';
-  import { featureFlags } from '$lib/stores/feature-flags.store';
+  import { featureFlags } from '$lib/stores/server-config.store';
   import { oauth, UserResponseDto } from '@api';
   import { oauth, UserResponseDto } from '@api';
   import { onMount } from 'svelte';
   import { onMount } from 'svelte';
   import { fade } from 'svelte/transition';
   import { fade } from 'svelte/transition';

+ 1 - 1
web/src/lib/components/user-settings-page/user-settings-list.svelte

@@ -1,7 +1,7 @@
 <script lang="ts">
 <script lang="ts">
   import { browser } from '$app/environment';
   import { browser } from '$app/environment';
   import { page } from '$app/stores';
   import { page } from '$app/stores';
-  import { featureFlags } from '$lib/stores/feature-flags.store';
+  import { featureFlags } from '$lib/stores/server-config.store';
   import { APIKeyResponseDto, AuthDeviceResponseDto, oauth, UserResponseDto } from '@api';
   import { APIKeyResponseDto, AuthDeviceResponseDto, oauth, UserResponseDto } from '@api';
   import SettingAccordion from '../admin-page/settings/setting-accordion.svelte';
   import SettingAccordion from '../admin-page/settings/setting-accordion.svelte';
   import ChangePasswordSettings from './change-password-settings.svelte';
   import ChangePasswordSettings from './change-password-settings.svelte';

+ 0 - 3
web/src/lib/constants.ts

@@ -1,6 +1,3 @@
-import { env } from '$env/dynamic/public';
-export const loginPageMessage: string | undefined = env.PUBLIC_LOGIN_PAGE_MESSAGE;
-
 export enum AssetAction {
 export enum AssetAction {
   ARCHIVE = 'archive',
   ARCHIVE = 'archive',
   UNARCHIVE = 'unarchive',
   UNARCHIVE = 'unarchive',

+ 0 - 22
web/src/lib/stores/feature-flags.store.ts

@@ -1,22 +0,0 @@
-import { api, ServerFeaturesDto } from '@api';
-import { writable } from 'svelte/store';
-
-export type FeatureFlags = ServerFeaturesDto & { loaded: boolean };
-
-export const featureFlags = writable<FeatureFlags>({
-  loaded: false,
-  clipEncode: true,
-  facialRecognition: true,
-  sidecar: true,
-  tagImage: true,
-  search: true,
-  oauth: false,
-  oauthAutoLaunch: false,
-  passwordLogin: true,
-  configFile: false,
-});
-
-export const loadFeatureFlags = async () => {
-  const { data } = await api.serverInfoApi.getServerFeatures();
-  featureFlags.update(() => ({ ...data, loaded: true }));
-};

+ 37 - 0
web/src/lib/stores/server-config.store.ts

@@ -0,0 +1,37 @@
+import { api, ServerConfigDto, ServerFeaturesDto } from '@api';
+import { writable } from 'svelte/store';
+
+export type FeatureFlags = ServerFeaturesDto & { loaded: boolean };
+
+export const featureFlags = writable<FeatureFlags>({
+  loaded: false,
+  clipEncode: true,
+  facialRecognition: true,
+  sidecar: true,
+  tagImage: true,
+  map: true,
+  search: true,
+  oauth: false,
+  oauthAutoLaunch: false,
+  passwordLogin: true,
+  configFile: false,
+});
+
+export type ServerConfig = ServerConfigDto & { loaded: boolean };
+
+export const serverConfig = writable<ServerConfig>({
+  loaded: false,
+  oauthButtonText: '',
+  mapTileUrl: '',
+  loginPageMessage: '',
+});
+
+export const loadConfig = async () => {
+  const [{ data: flags }, { data: config }] = await Promise.all([
+    api.serverInfoApi.getServerFeatures(),
+    api.serverInfoApi.getServerConfig(),
+  ]);
+
+  featureFlags.update(() => ({ ...flags, loaded: true }));
+  serverConfig.update(() => ({ ...config, loaded: true }));
+};

+ 71 - 66
web/src/routes/(user)/map/+page.svelte

@@ -1,16 +1,19 @@
 <script lang="ts">
 <script lang="ts">
+  import { goto } from '$app/navigation';
   import AssetViewer from '$lib/components/asset-viewer/asset-viewer.svelte';
   import AssetViewer from '$lib/components/asset-viewer/asset-viewer.svelte';
   import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
   import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
   import MapSettingsModal from '$lib/components/map-page/map-settings-modal.svelte';
   import MapSettingsModal from '$lib/components/map-page/map-settings-modal.svelte';
   import Portal from '$lib/components/shared-components/portal/portal.svelte';
   import Portal from '$lib/components/shared-components/portal/portal.svelte';
+  import { AppRoute } from '$lib/constants';
+  import { assetViewingStore } from '$lib/stores/asset-viewing.store';
   import { mapSettings } from '$lib/stores/preferences.store';
   import { mapSettings } from '$lib/stores/preferences.store';
+  import { featureFlags, serverConfig } from '$lib/stores/server-config.store';
   import { MapMarkerResponseDto, api } from '@api';
   import { MapMarkerResponseDto, api } from '@api';
   import { isEqual, omit } from 'lodash-es';
   import { isEqual, omit } from 'lodash-es';
+  import { DateTime, Duration } from 'luxon';
   import { onDestroy, onMount } from 'svelte';
   import { onDestroy, onMount } from 'svelte';
   import Cog from 'svelte-material-icons/Cog.svelte';
   import Cog from 'svelte-material-icons/Cog.svelte';
   import type { PageData } from './$types';
   import type { PageData } from './$types';
-  import { DateTime, Duration } from 'luxon';
-  import { assetViewingStore } from '$lib/stores/asset-viewing.store';
 
 
   export let data: PageData;
   export let data: PageData;
 
 
@@ -29,12 +32,12 @@
   });
   });
 
 
   onDestroy(() => {
   onDestroy(() => {
-    if (abortController) {
-      abortController.abort();
-    }
+    abortController?.abort();
     assetViewingStore.showAssetViewer(false);
     assetViewingStore.showAssetViewer(false);
   });
   });
 
 
+  $: $featureFlags.map || goto(AppRoute.PHOTOS);
+
   async function loadMapMarkers() {
   async function loadMapMarkers() {
     if (abortController) {
     if (abortController) {
       abortController.abort();
       abortController.abort();
@@ -98,70 +101,72 @@
   }
   }
 </script>
 </script>
 
 
-<UserPageLayout user={data.user} title={data.meta.title}>
-  <div class="isolate h-full w-full">
-    {#if leaflet}
-      {@const { Map, TileLayer, AssetMarkerCluster, Control } = leaflet}
-      <Map
-        center={[30, 0]}
-        zoom={3}
-        allowDarkMode={$mapSettings.allowDarkMode}
-        options={{
-          maxBounds: [
-            [-90, -180],
-            [90, 180],
-          ],
-          minZoom: 2,
-        }}
-      >
-        <TileLayer
-          urlTemplate={'https://tile.openstreetmap.org/{z}/{x}/{y}.png'}
+{#if $featureFlags.loaded && $featureFlags.map}
+  <UserPageLayout user={data.user} title={data.meta.title}>
+    <div class="isolate h-full w-full">
+      {#if leaflet}
+        {@const { Map, TileLayer, AssetMarkerCluster, Control } = leaflet}
+        <Map
+          center={[30, 0]}
+          zoom={3}
+          allowDarkMode={$mapSettings.allowDarkMode}
           options={{
           options={{
-            attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
+            maxBounds: [
+              [-90, -180],
+              [90, 180],
+            ],
+            minZoom: 2,
           }}
           }}
-        />
-        <AssetMarkerCluster
-          markers={mapMarkers}
-          on:view={({ detail }) => onViewAssets(detail.assetIds, detail.activeAssetIndex)}
-        />
-        <Control>
-          <button
-            class="flex h-8 w-8 items-center justify-center rounded-sm border-2 border-black/20 bg-white font-bold text-black/70 hover:bg-gray-50 focus:bg-gray-50"
-            title="Open map settings"
-            on:click={() => (showSettingsModal = true)}
-          >
-            <Cog size="100%" class="p-1" />
-          </button>
-        </Control>
-      </Map>
+        >
+          <TileLayer
+            urlTemplate={$serverConfig.mapTileUrl}
+            options={{
+              attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
+            }}
+          />
+          <AssetMarkerCluster
+            markers={mapMarkers}
+            on:view={({ detail }) => onViewAssets(detail.assetIds, detail.activeAssetIndex)}
+          />
+          <Control>
+            <button
+              class="flex h-8 w-8 items-center justify-center rounded-sm border-2 border-black/20 bg-white font-bold text-black/70 hover:bg-gray-50 focus:bg-gray-50"
+              title="Open map settings"
+              on:click={() => (showSettingsModal = true)}
+            >
+              <Cog size="100%" class="p-1" />
+            </button>
+          </Control>
+        </Map>
+      {/if}
+    </div>
+  </UserPageLayout>
+
+  <Portal target="body">
+    {#if $showAssetViewer}
+      <AssetViewer
+        asset={$viewingAsset}
+        showNavigation={viewingAssets.length > 1}
+        on:next={navigateNext}
+        on:previous={navigatePrevious}
+        on:close={() => assetViewingStore.showAssetViewer(false)}
+      />
     {/if}
     {/if}
-  </div>
-</UserPageLayout>
-
-<Portal target="body">
-  {#if $showAssetViewer}
-    <AssetViewer
-      asset={$viewingAsset}
-      showNavigation={viewingAssets.length > 1}
-      on:next={navigateNext}
-      on:previous={navigatePrevious}
-      on:close={() => assetViewingStore.showAssetViewer(false)}
+  </Portal>
+
+  {#if showSettingsModal}
+    <MapSettingsModal
+      settings={{ ...$mapSettings }}
+      on:close={() => (showSettingsModal = false)}
+      on:save={async ({ detail }) => {
+        const shouldUpdate = !isEqual(omit(detail, 'allowDarkMode'), omit($mapSettings, 'allowDarkMode'));
+        showSettingsModal = false;
+        $mapSettings = detail;
+
+        if (shouldUpdate) {
+          mapMarkers = await loadMapMarkers();
+        }
+      }}
     />
     />
   {/if}
   {/if}
-</Portal>
-
-{#if showSettingsModal}
-  <MapSettingsModal
-    settings={{ ...$mapSettings }}
-    on:close={() => (showSettingsModal = false)}
-    on:save={async ({ detail }) => {
-      const shouldUpdate = !isEqual(omit(detail, 'allowDarkMode'), omit($mapSettings, 'allowDarkMode'));
-      showSettingsModal = false;
-      $mapSettings = detail;
-
-      if (shouldUpdate) {
-        mapMarkers = await loadMapMarkers();
-      }
-    }}
-  />
 {/if}
 {/if}

+ 3 - 3
web/src/routes/+layout.svelte

@@ -14,7 +14,7 @@
   import AppleHeader from '$lib/components/shared-components/apple-header.svelte';
   import AppleHeader from '$lib/components/shared-components/apple-header.svelte';
   import FaviconHeader from '$lib/components/shared-components/favicon-header.svelte';
   import FaviconHeader from '$lib/components/shared-components/favicon-header.svelte';
   import { onMount } from 'svelte';
   import { onMount } from 'svelte';
-  import { loadFeatureFlags } from '$lib/stores/feature-flags.store';
+  import { loadConfig } from '$lib/stores/server-config.store';
   import { handleError } from '$lib/utils/handle-error';
   import { handleError } from '$lib/utils/handle-error';
   import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
   import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
   import { api } from '@api';
   import { api } from '@api';
@@ -37,9 +37,9 @@
 
 
   onMount(async () => {
   onMount(async () => {
     try {
     try {
-      await loadFeatureFlags();
+      await loadConfig();
     } catch (error) {
     } catch (error) {
-      handleError(error, 'Unable to load feature flags');
+      handleError(error, 'Unable to connect to server');
     }
     }
   });
   });
 
 

+ 23 - 17
web/src/routes/admin/system-settings/+page.svelte

@@ -3,6 +3,7 @@
   import FFmpegSettings from '$lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte';
   import FFmpegSettings from '$lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte';
   import JobSettings from '$lib/components/admin-page/settings/job-settings/job-settings.svelte';
   import JobSettings from '$lib/components/admin-page/settings/job-settings/job-settings.svelte';
   import MachineLearningSettings from '$lib/components/admin-page/settings/machine-learning-settings/machine-learning-settings.svelte';
   import MachineLearningSettings from '$lib/components/admin-page/settings/machine-learning-settings/machine-learning-settings.svelte';
+  import MapSettings from '$lib/components/admin-page/settings/map-settings/map-settings.svelte';
   import OAuthSettings from '$lib/components/admin-page/settings/oauth/oauth-settings.svelte';
   import OAuthSettings from '$lib/components/admin-page/settings/oauth/oauth-settings.svelte';
   import PasswordLoginSettings from '$lib/components/admin-page/settings/password-login/password-login-settings.svelte';
   import PasswordLoginSettings from '$lib/components/admin-page/settings/password-login/password-login-settings.svelte';
   import SettingAccordion from '$lib/components/admin-page/settings/setting-accordion.svelte';
   import SettingAccordion from '$lib/components/admin-page/settings/setting-accordion.svelte';
@@ -11,7 +12,7 @@
   import Button from '$lib/components/elements/buttons/button.svelte';
   import Button from '$lib/components/elements/buttons/button.svelte';
   import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
   import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
   import { downloadManager } from '$lib/stores/download';
   import { downloadManager } from '$lib/stores/download';
-  import { featureFlags } from '$lib/stores/feature-flags.store';
+  import { featureFlags } from '$lib/stores/server-config.store';
   import { downloadBlob } from '$lib/utils/asset-utils';
   import { downloadBlob } from '$lib/utils/asset-utils';
   import { SystemConfigDto, api, copyToClipboard } from '@api';
   import { SystemConfigDto, api, copyToClipboard } from '@api';
   import Alert from 'svelte-material-icons/Alert.svelte';
   import Alert from 'svelte-material-icons/Alert.svelte';
@@ -57,20 +58,6 @@
         <span class="pl-2">Export as JSON</span>
         <span class="pl-2">Export as JSON</span>
       </Button>
       </Button>
     </div>
     </div>
-    <SettingAccordion title="Thumbnail Settings" subtitle="Manage the resolution of thumbnail sizes">
-      <ThumbnailSettings disabled={$featureFlags.configFile} thumbnailConfig={configs.thumbnail} />
-    </SettingAccordion>
-
-    <SettingAccordion
-      title="Video Transcoding Settings"
-      subtitle="Manage the resolution and encoding information of the video files"
-    >
-      <FFmpegSettings disabled={$featureFlags.configFile} ffmpegConfig={configs.ffmpeg} />
-    </SettingAccordion>
-
-    <SettingAccordion title="Machine Learning Settings" subtitle="Manage model settings">
-      <MachineLearningSettings disabled={$featureFlags.configFile} machineLearningConfig={configs.machineLearning} />
-    </SettingAccordion>
 
 
     <SettingAccordion
     <SettingAccordion
       title="Job Settings"
       title="Job Settings"
@@ -80,14 +67,22 @@
       <JobSettings disabled={$featureFlags.configFile} jobConfig={configs.job} />
       <JobSettings disabled={$featureFlags.configFile} jobConfig={configs.job} />
     </SettingAccordion>
     </SettingAccordion>
 
 
-    <SettingAccordion title="Password Authentication" subtitle="Manage login with password settings">
-      <PasswordLoginSettings disabled={$featureFlags.configFile} passwordLoginConfig={configs.passwordLogin} />
+    <SettingAccordion title="Machine Learning Settings" subtitle="Manage model settings">
+      <MachineLearningSettings disabled={$featureFlags.configFile} machineLearningConfig={configs.machineLearning} />
+    </SettingAccordion>
+
+    <SettingAccordion title="Map Settings" subtitle="Manage map settings">
+      <MapSettings disabled={$featureFlags.configFile} mapConfig={configs.map} />
     </SettingAccordion>
     </SettingAccordion>
 
 
     <SettingAccordion title="OAuth Authentication" subtitle="Manage the login with OAuth settings">
     <SettingAccordion title="OAuth Authentication" subtitle="Manage the login with OAuth settings">
       <OAuthSettings disabled={$featureFlags.configFile} oauthConfig={configs.oauth} />
       <OAuthSettings disabled={$featureFlags.configFile} oauthConfig={configs.oauth} />
     </SettingAccordion>
     </SettingAccordion>
 
 
+    <SettingAccordion title="Password Authentication" subtitle="Manage login with password settings">
+      <PasswordLoginSettings disabled={$featureFlags.configFile} passwordLoginConfig={configs.passwordLogin} />
+    </SettingAccordion>
+
     <SettingAccordion
     <SettingAccordion
       title="Storage Template"
       title="Storage Template"
       subtitle="Manage the folder structure and file name of the upload asset"
       subtitle="Manage the folder structure and file name of the upload asset"
@@ -99,5 +94,16 @@
         user={data.user}
         user={data.user}
       />
       />
     </SettingAccordion>
     </SettingAccordion>
+
+    <SettingAccordion title="Thumbnail Settings" subtitle="Manage the resolution of thumbnail sizes">
+      <ThumbnailSettings disabled={$featureFlags.configFile} thumbnailConfig={configs.thumbnail} />
+    </SettingAccordion>
+
+    <SettingAccordion
+      title="Video Transcoding Settings"
+      subtitle="Manage the resolution and encoding information of the video files"
+    >
+      <FFmpegSettings disabled={$featureFlags.configFile} ffmpegConfig={configs.ffmpeg} />
+    </SettingAccordion>
   {/await}
   {/await}
 </section>
 </section>

+ 3 - 4
web/src/routes/auth/login/+page.svelte

@@ -3,18 +3,17 @@
   import LoginForm from '$lib/components/forms/login-form.svelte';
   import LoginForm from '$lib/components/forms/login-form.svelte';
   import FullscreenContainer from '$lib/components/shared-components/fullscreen-container.svelte';
   import FullscreenContainer from '$lib/components/shared-components/fullscreen-container.svelte';
   import { AppRoute } from '$lib/constants';
   import { AppRoute } from '$lib/constants';
-  import { loginPageMessage } from '$lib/constants';
-  import { featureFlags } from '$lib/stores/feature-flags.store';
+  import { featureFlags, serverConfig } from '$lib/stores/server-config.store';
   import type { PageData } from './$types';
   import type { PageData } from './$types';
 
 
   export let data: PageData;
   export let data: PageData;
 </script>
 </script>
 
 
 {#if $featureFlags.loaded}
 {#if $featureFlags.loaded}
-  <FullscreenContainer title={data.meta.title} showMessage={!!loginPageMessage}>
+  <FullscreenContainer title={data.meta.title} showMessage={!!$serverConfig.loginPageMessage}>
     <p slot="message">
     <p slot="message">
       <!-- eslint-disable-next-line svelte/no-at-html-tags -->
       <!-- eslint-disable-next-line svelte/no-at-html-tags -->
-      {@html loginPageMessage}
+      {@html $serverConfig.loginPageMessage}
     </p>
     </p>
 
 
     <LoginForm
     <LoginForm