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

* feat: server config

* chore: open api

* fix: redirect /map to /photos when disabled
This commit is contained in:
Jason Rasmussen 2023-09-08 22:51:46 -04:00 committed by GitHub
parent 3edade6761
commit f1db257628
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 1103 additions and 162 deletions

View File

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

View File

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

View File

@ -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
mobile/openapi/doc/ServerConfigDto.md generated Normal file
View File

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

View File

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

View File

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

View File

@ -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
mobile/openapi/doc/SystemConfigMapDto.md generated Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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", "map",
"search",
"tagImage",
"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": {

View File

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

View File

@ -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', () => { describe('getFeatures', () => {
it('should respond the server features', async () => { it('should respond the server features', async () => {
await expect(sut.getFeatures()).resolves.toEqual({ await expect(sut.getFeatures()).resolves.toEqual({
clipEncode: true, clipEncode: true,
facialRecognition: true, facialRecognition: true,
oauth: false, map: true,
oauthAutoLaunch: false, oauth: false,
passwordLogin: true, oauthAutoLaunch: false,
search: true, passwordLogin: true,
sidecar: true, search: true,
tagImage: true, sidecar: true,
configFile: false, tagImage: true,
}); configFile: false,
expect(configMock.load).toHaveBeenCalled();
}); });
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();
}); });
}); });

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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"> {#if $featureFlags.map}
<SideBarButton title="Map" logo={Map} isSelected={$page.route.id === '/(user)/map'} /> <a data-sveltekit-preload-data="hover" href={AppRoute.MAP} draggable="false">
</a> <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"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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}> {#if $featureFlags.loaded && $featureFlags.map}
<div class="isolate h-full w-full"> <UserPageLayout user={data.user} title={data.meta.title}>
{#if leaflet} <div class="isolate h-full w-full">
{@const { Map, TileLayer, AssetMarkerCluster, Control } = leaflet} {#if leaflet}
<Map {@const { Map, TileLayer, AssetMarkerCluster, Control } = leaflet}
center={[30, 0]} <Map
zoom={3} center={[30, 0]}
allowDarkMode={$mapSettings.allowDarkMode} zoom={3}
options={{ allowDarkMode={$mapSettings.allowDarkMode}
maxBounds: [
[-90, -180],
[90, 180],
],
minZoom: 2,
}}
>
<TileLayer
urlTemplate={'https://tile.openstreetmap.org/{z}/{x}/{y}.png'}
options={{ options={{
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>', maxBounds: [
[-90, -180],
[90, 180],
],
minZoom: 2,
}} }}
/> >
<AssetMarkerCluster <TileLayer
markers={mapMarkers} urlTemplate={$serverConfig.mapTileUrl}
on:view={({ detail }) => onViewAssets(detail.assetIds, detail.activeAssetIndex)} options={{
/> attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
<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" <AssetMarkerCluster
title="Open map settings" markers={mapMarkers}
on:click={() => (showSettingsModal = true)} on:view={({ detail }) => onViewAssets(detail.assetIds, detail.activeAssetIndex)}
> />
<Cog size="100%" class="p-1" /> <Control>
</button> <button
</Control> 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"
</Map> title="Open map settings"
{/if} on:click={() => (showSettingsModal = true)}
</div> >
</UserPageLayout> <Cog size="100%" class="p-1" />
</button>
</Control>
</Map>
{/if}
</div>
</UserPageLayout>
<Portal target="body"> <Portal target="body">
{#if $showAssetViewer} {#if $showAssetViewer}
<AssetViewer <AssetViewer
asset={$viewingAsset} asset={$viewingAsset}
showNavigation={viewingAssets.length > 1} showNavigation={viewingAssets.length > 1}
on:next={navigateNext} on:next={navigateNext}
on:previous={navigatePrevious} on:previous={navigatePrevious}
on:close={() => assetViewingStore.showAssetViewer(false)} on:close={() => assetViewingStore.showAssetViewer(false)}
/>
{/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}
</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}

View File

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

View File

@ -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"> <SettingAccordion title="Machine Learning Settings" subtitle="Manage model settings">
<PasswordLoginSettings disabled={$featureFlags.configFile} passwordLoginConfig={configs.passwordLogin} /> <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>

View File

@ -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, serverConfig } from '$lib/stores/server-config.store';
import { featureFlags } from '$lib/stores/feature-flags.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