1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-03-01 15:11:21 +01:00

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;
}
/**
*
* @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
@ -2367,6 +2392,12 @@ export interface ServerFeaturesDto {
* @memberof ServerFeaturesDto
*/
'facialRecognition': boolean;
/**
*
* @type {boolean}
* @memberof ServerFeaturesDto
*/
'map': boolean;
/**
*
* @type {boolean}
@ -2810,6 +2841,12 @@ export interface SystemConfigDto {
* @memberof SystemConfigDto
*/
'machineLearning': SystemConfigMachineLearningDto;
/**
*
* @type {SystemConfigMapDto}
* @memberof SystemConfigDto
*/
'map': SystemConfigMapDto;
/**
*
* @type {SystemConfigOAuthDto}
@ -3050,6 +3087,25 @@ export interface SystemConfigMachineLearningDto {
*/
'url': string;
}
/**
*
* @export
* @interface SystemConfigMapDto
*/
export interface SystemConfigMapDto {
/**
*
* @type {boolean}
* @memberof SystemConfigMapDto
*/
'enabled': boolean;
/**
*
* @type {string}
* @memberof SystemConfigMapDto
*/
'tileUrl': string;
}
/**
*
* @export
@ -10825,6 +10881,35 @@ export class SearchApi extends BaseAPI {
*/
export const ServerInfoApiAxiosParamCreator = function (configuration?: Configuration) {
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.
@ -11027,6 +11112,15 @@ export const ServerInfoApiAxiosParamCreator = function (configuration?: Configur
export const ServerInfoApiFp = function(configuration?: Configuration) {
const localVarAxiosParamCreator = ServerInfoApiAxiosParamCreator(configuration)
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.
@ -11091,6 +11185,14 @@ export const ServerInfoApiFp = function(configuration?: Configuration) {
export const ServerInfoApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
const localVarFp = ServerInfoApiFp(configuration)
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.
@ -11149,6 +11251,16 @@ export const ServerInfoApiFactory = function (configuration?: Configuration, bas
* @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.

View file

@ -97,6 +97,7 @@ doc/SearchExploreResponseDto.md
doc/SearchFacetCountResponseDto.md
doc/SearchFacetResponseDto.md
doc/SearchResponseDto.md
doc/ServerConfigDto.md
doc/ServerFeaturesDto.md
doc/ServerInfoApi.md
doc/ServerInfoResponseDto.md
@ -116,6 +117,7 @@ doc/SystemConfigDto.md
doc/SystemConfigFFmpegDto.md
doc/SystemConfigJobDto.md
doc/SystemConfigMachineLearningDto.md
doc/SystemConfigMapDto.md
doc/SystemConfigOAuthDto.md
doc/SystemConfigPasswordLoginDto.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_response_dto.dart
lib/model/search_response_dto.dart
lib/model/server_config_dto.dart
lib/model/server_features_dto.dart
lib/model/server_info_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_job_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_password_login_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_response_dto_test.dart
test/search_response_dto_test.dart
test/server_config_dto_test.dart
test/server_features_dto_test.dart
test/server_info_api_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_job_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_password_login_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} |
*SearchApi* | [**getExploreData**](doc//SearchApi.md#getexploredata) | **GET** /search/explore |
*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* | [**getServerInfo**](doc//ServerInfoApi.md#getserverinfo) | **GET** /server-info |
*ServerInfoApi* | [**getServerVersion**](doc//ServerInfoApi.md#getserverversion) | **GET** /server-info/version |
@ -266,6 +267,7 @@ Class | Method | HTTP request | Description
- [SearchFacetCountResponseDto](doc//SearchFacetCountResponseDto.md)
- [SearchFacetResponseDto](doc//SearchFacetResponseDto.md)
- [SearchResponseDto](doc//SearchResponseDto.md)
- [ServerConfigDto](doc//ServerConfigDto.md)
- [ServerFeaturesDto](doc//ServerFeaturesDto.md)
- [ServerInfoResponseDto](doc//ServerInfoResponseDto.md)
- [ServerMediaTypesResponseDto](doc//ServerMediaTypesResponseDto.md)
@ -282,6 +284,7 @@ Class | Method | HTTP request | Description
- [SystemConfigFFmpegDto](doc//SystemConfigFFmpegDto.md)
- [SystemConfigJobDto](doc//SystemConfigJobDto.md)
- [SystemConfigMachineLearningDto](doc//SystemConfigMachineLearningDto.md)
- [SystemConfigMapDto](doc//SystemConfigMapDto.md)
- [SystemConfigOAuthDto](doc//SystemConfigOAuthDto.md)
- [SystemConfigPasswordLoginDto](doc//SystemConfigPasswordLoginDto.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** | |
**configFile** | **bool** | |
**facialRecognition** | **bool** | |
**map** | **bool** | |
**oauth** | **bool** | |
**oauthAutoLaunch** | **bool** | |
**passwordLogin** | **bool** | |

View file

@ -9,6 +9,7 @@ All URIs are relative to */api*
Method | HTTP request | Description
------------- | ------------- | -------------
[**getServerConfig**](ServerInfoApi.md#getserverconfig) | **GET** /server-info/config |
[**getServerFeatures**](ServerInfoApi.md#getserverfeatures) | **GET** /server-info/features |
[**getServerInfo**](ServerInfoApi.md#getserverinfo) | **GET** /server-info |
[**getServerVersion**](ServerInfoApi.md#getserverversion) | **GET** /server-info/version |
@ -17,6 +18,43 @@ Method | HTTP request | Description
[**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**
> ServerFeaturesDto getServerFeatures()

View file

@ -11,6 +11,7 @@ Name | Type | Description | Notes
**ffmpeg** | [**SystemConfigFFmpegDto**](SystemConfigFFmpegDto.md) | |
**job** | [**SystemConfigJobDto**](SystemConfigJobDto.md) | |
**machineLearning** | [**SystemConfigMachineLearningDto**](SystemConfigMachineLearningDto.md) | |
**map** | [**SystemConfigMapDto**](SystemConfigMapDto.md) | |
**oauth** | [**SystemConfigOAuthDto**](SystemConfigOAuthDto.md) | |
**passwordLogin** | [**SystemConfigPasswordLoginDto**](SystemConfigPasswordLoginDto.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_response_dto.dart';
part 'model/search_response_dto.dart';
part 'model/server_config_dto.dart';
part 'model/server_features_dto.dart';
part 'model/server_info_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_job_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_password_login_dto.dart';
part 'model/system_config_storage_template_dto.dart';

View file

@ -16,6 +16,47 @@ class ServerInfoApi {
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].
Future<Response> getServerFeaturesWithHttpInfo() async {
// ignore: prefer_const_declarations

View file

@ -349,6 +349,8 @@ class ApiClient {
return SearchFacetResponseDto.fromJson(value);
case 'SearchResponseDto':
return SearchResponseDto.fromJson(value);
case 'ServerConfigDto':
return ServerConfigDto.fromJson(value);
case 'ServerFeaturesDto':
return ServerFeaturesDto.fromJson(value);
case 'ServerInfoResponseDto':
@ -381,6 +383,8 @@ class ApiClient {
return SystemConfigJobDto.fromJson(value);
case 'SystemConfigMachineLearningDto':
return SystemConfigMachineLearningDto.fromJson(value);
case 'SystemConfigMapDto':
return SystemConfigMapDto.fromJson(value);
case 'SystemConfigOAuthDto':
return SystemConfigOAuthDto.fromJson(value);
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.configFile,
required this.facialRecognition,
required this.map,
required this.oauth,
required this.oauthAutoLaunch,
required this.passwordLogin,
@ -30,6 +31,8 @@ class ServerFeaturesDto {
bool facialRecognition;
bool map;
bool oauth;
bool oauthAutoLaunch;
@ -47,6 +50,7 @@ class ServerFeaturesDto {
other.clipEncode == clipEncode &&
other.configFile == configFile &&
other.facialRecognition == facialRecognition &&
other.map == map &&
other.oauth == oauth &&
other.oauthAutoLaunch == oauthAutoLaunch &&
other.passwordLogin == passwordLogin &&
@ -60,6 +64,7 @@ class ServerFeaturesDto {
(clipEncode.hashCode) +
(configFile.hashCode) +
(facialRecognition.hashCode) +
(map.hashCode) +
(oauth.hashCode) +
(oauthAutoLaunch.hashCode) +
(passwordLogin.hashCode) +
@ -68,13 +73,14 @@ class ServerFeaturesDto {
(tagImage.hashCode);
@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() {
final json = <String, dynamic>{};
json[r'clipEncode'] = this.clipEncode;
json[r'configFile'] = this.configFile;
json[r'facialRecognition'] = this.facialRecognition;
json[r'map'] = this.map;
json[r'oauth'] = this.oauth;
json[r'oauthAutoLaunch'] = this.oauthAutoLaunch;
json[r'passwordLogin'] = this.passwordLogin;
@ -95,6 +101,7 @@ class ServerFeaturesDto {
clipEncode: mapValueOfType<bool>(json, r'clipEncode')!,
configFile: mapValueOfType<bool>(json, r'configFile')!,
facialRecognition: mapValueOfType<bool>(json, r'facialRecognition')!,
map: mapValueOfType<bool>(json, r'map')!,
oauth: mapValueOfType<bool>(json, r'oauth')!,
oauthAutoLaunch: mapValueOfType<bool>(json, r'oauthAutoLaunch')!,
passwordLogin: mapValueOfType<bool>(json, r'passwordLogin')!,
@ -151,6 +158,7 @@ class ServerFeaturesDto {
'clipEncode',
'configFile',
'facialRecognition',
'map',
'oauth',
'oauthAutoLaunch',
'passwordLogin',

View file

@ -16,6 +16,7 @@ class SystemConfigDto {
required this.ffmpeg,
required this.job,
required this.machineLearning,
required this.map,
required this.oauth,
required this.passwordLogin,
required this.storageTemplate,
@ -28,6 +29,8 @@ class SystemConfigDto {
SystemConfigMachineLearningDto machineLearning;
SystemConfigMapDto map;
SystemConfigOAuthDto oauth;
SystemConfigPasswordLoginDto passwordLogin;
@ -41,6 +44,7 @@ class SystemConfigDto {
other.ffmpeg == ffmpeg &&
other.job == job &&
other.machineLearning == machineLearning &&
other.map == map &&
other.oauth == oauth &&
other.passwordLogin == passwordLogin &&
other.storageTemplate == storageTemplate &&
@ -52,19 +56,21 @@ class SystemConfigDto {
(ffmpeg.hashCode) +
(job.hashCode) +
(machineLearning.hashCode) +
(map.hashCode) +
(oauth.hashCode) +
(passwordLogin.hashCode) +
(storageTemplate.hashCode) +
(thumbnail.hashCode);
@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() {
final json = <String, dynamic>{};
json[r'ffmpeg'] = this.ffmpeg;
json[r'job'] = this.job;
json[r'machineLearning'] = this.machineLearning;
json[r'map'] = this.map;
json[r'oauth'] = this.oauth;
json[r'passwordLogin'] = this.passwordLogin;
json[r'storageTemplate'] = this.storageTemplate;
@ -83,6 +89,7 @@ class SystemConfigDto {
ffmpeg: SystemConfigFFmpegDto.fromJson(json[r'ffmpeg'])!,
job: SystemConfigJobDto.fromJson(json[r'job'])!,
machineLearning: SystemConfigMachineLearningDto.fromJson(json[r'machineLearning'])!,
map: SystemConfigMapDto.fromJson(json[r'map'])!,
oauth: SystemConfigOAuthDto.fromJson(json[r'oauth'])!,
passwordLogin: SystemConfigPasswordLoginDto.fromJson(json[r'passwordLogin'])!,
storageTemplate: SystemConfigStorageTemplateDto.fromJson(json[r'storageTemplate'])!,
@ -137,6 +144,7 @@ class SystemConfigDto {
'ffmpeg',
'job',
'machineLearning',
'map',
'oauth',
'passwordLogin',
'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
});
// bool map
test('to test the property `map`', () async {
// TODO
});
// bool oauth
test('to test the property `oauth`', () async {
// TODO

View file

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

View file

@ -31,6 +31,11 @@ void main() {
// TODO
});
// SystemConfigMapDto map
test('to test the property `map`', () async {
// TODO
});
// SystemConfigOAuthDto oauth
test('to test the property `oauth`', () async {
// 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": {
"get": {
"operationId": "getServerFeatures",
@ -6618,6 +6639,25 @@
],
"type": "object"
},
"ServerConfigDto": {
"properties": {
"loginPageMessage": {
"type": "string"
},
"mapTileUrl": {
"type": "string"
},
"oauthButtonText": {
"type": "string"
}
},
"required": [
"oauthButtonText",
"loginPageMessage",
"mapTileUrl"
],
"type": "object"
},
"ServerFeaturesDto": {
"properties": {
"clipEncode": {
@ -6629,6 +6669,9 @@
"facialRecognition": {
"type": "boolean"
},
"map": {
"type": "boolean"
},
"oauth": {
"type": "boolean"
},
@ -6649,15 +6692,16 @@
}
},
"required": [
"configFile",
"clipEncode",
"configFile",
"facialRecognition",
"sidecar",
"search",
"tagImage",
"map",
"oauth",
"oauthAutoLaunch",
"passwordLogin"
"passwordLogin",
"sidecar",
"search",
"tagImage"
],
"type": "object"
},
@ -6989,6 +7033,9 @@
"machineLearning": {
"$ref": "#/components/schemas/SystemConfigMachineLearningDto"
},
"map": {
"$ref": "#/components/schemas/SystemConfigMapDto"
},
"oauth": {
"$ref": "#/components/schemas/SystemConfigOAuthDto"
},
@ -7005,6 +7052,7 @@
"required": [
"ffmpeg",
"machineLearning",
"map",
"oauth",
"passwordLogin",
"storageTemplate",
@ -7162,6 +7210,21 @@
],
"type": "object"
},
"SystemConfigMapDto": {
"properties": {
"enabled": {
"type": "boolean"
},
"tileUrl": {
"type": "string"
}
},
"required": [
"enabled",
"tileUrl"
],
"type": "object"
},
"SystemConfigOAuthDto": {
"properties": {
"autoLaunch": {

View file

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

View file

@ -143,22 +143,34 @@ describe(ServerInfoService.name, () => {
it('should respond the server version', () => {
expect(sut.getVersion()).toEqual(serverVersion);
});
});
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('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('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 { IUserRepository, UserStatsQueryResponse } from '../user';
import {
ServerConfigDto,
ServerFeaturesDto,
ServerInfoResponseDto,
ServerMediaTypesResponseDto,
@ -55,6 +56,19 @@ export class ServerInfoService {
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> {
const userStats: UserStatsQueryResponse[] = await this.userRepository.getUserStats();
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 { SystemConfigJobDto } from './system-config-job.dto';
import { SystemConfigMachineLearningDto } from './system-config-machine-learning.dto';
import { SystemConfigMapDto } from './system-config-map.dto';
import { SystemConfigOAuthDto } from './system-config-oauth.dto';
import { SystemConfigPasswordLoginDto } from './system-config-password-login.dto';
import { SystemConfigStorageTemplateDto } from './system-config-storage-template.dto';
@ -20,6 +21,11 @@ export class SystemConfigDto implements SystemConfig {
@IsObject()
machineLearning!: SystemConfigMachineLearningDto;
@Type(() => SystemConfigMapDto)
@ValidateNested()
@IsObject()
map!: SystemConfigMapDto;
@Type(() => SystemConfigOAuthDto)
@ValidateNested()
@IsObject()

View file

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

View file

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

View file

@ -1,4 +1,5 @@
import {
ServerConfigDto,
ServerFeaturesDto,
ServerInfoResponseDto,
ServerInfoService,
@ -42,6 +43,12 @@ export class ServerInfoController {
return this.service.getFeatures();
}
@PublicRoute()
@Get('config')
getServerConfig(): Promise<ServerConfigDto> {
return this.service.getConfig();
}
@AdminRoute()
@Get('stats')
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_MAX_DISTANCE = 'machineLearning.facialRecognition.maxDistance',
MAP_ENABLED = 'map.enabled',
MAP_TILE_URL = 'map.tileUrl',
OAUTH_ENABLED = 'oauth.enabled',
OAUTH_ISSUER_URL = 'oauth.issuerUrl',
OAUTH_CLIENT_ID = 'oauth.clientId',
@ -164,6 +167,10 @@ export interface SystemConfig {
maxDistance: number;
};
};
map: {
enabled: boolean;
tileUrl: string;
};
oauth: {
enabled: boolean;
issuerUrl: string;

View file

@ -83,6 +83,7 @@ describe(`${ServerInfoController.name} (e2e)`, () => {
clipEncode: true,
configFile: false,
facialRecognition: true,
map: true,
oauth: false,
oauthAutoLaunch: false,
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', () => {
it('should require authentication', async () => {
const { status, body } = await request(server).get('/server-info/stats');

View file

@ -2343,6 +2343,31 @@ export interface SearchResponseDto {
*/
'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
@ -2367,6 +2392,12 @@ export interface ServerFeaturesDto {
* @memberof ServerFeaturesDto
*/
'facialRecognition': boolean;
/**
*
* @type {boolean}
* @memberof ServerFeaturesDto
*/
'map': boolean;
/**
*
* @type {boolean}
@ -2810,6 +2841,12 @@ export interface SystemConfigDto {
* @memberof SystemConfigDto
*/
'machineLearning': SystemConfigMachineLearningDto;
/**
*
* @type {SystemConfigMapDto}
* @memberof SystemConfigDto
*/
'map': SystemConfigMapDto;
/**
*
* @type {SystemConfigOAuthDto}
@ -3050,6 +3087,25 @@ export interface SystemConfigMachineLearningDto {
*/
'url': string;
}
/**
*
* @export
* @interface SystemConfigMapDto
*/
export interface SystemConfigMapDto {
/**
*
* @type {boolean}
* @memberof SystemConfigMapDto
*/
'enabled': boolean;
/**
*
* @type {string}
* @memberof SystemConfigMapDto
*/
'tileUrl': string;
}
/**
*
* @export
@ -10825,6 +10881,35 @@ export class SearchApi extends BaseAPI {
*/
export const ServerInfoApiAxiosParamCreator = function (configuration?: Configuration) {
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.
@ -11027,6 +11112,15 @@ export const ServerInfoApiAxiosParamCreator = function (configuration?: Configur
export const ServerInfoApiFp = function(configuration?: Configuration) {
const localVarAxiosParamCreator = ServerInfoApiAxiosParamCreator(configuration)
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.
@ -11091,6 +11185,14 @@ export const ServerInfoApiFp = function(configuration?: Configuration) {
export const ServerInfoApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
const localVarFp = ServerInfoApiFp(configuration)
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.
@ -11149,6 +11251,16 @@ export const ServerInfoApiFactory = function (configuration?: Configuration, bas
* @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.

View file

@ -4,7 +4,7 @@
NotificationType,
} from '$lib/components/shared-components/notification/notification';
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 { AllJobStatusResponseDto, api, JobCommand, JobCommandDto, JobName } from '@api';
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">
import { page } from '$app/stores';
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 { DateTime } from 'luxon';
import { createEventDispatcher } from 'svelte';
import Calendar from 'svelte-material-icons/Calendar.svelte';
import CameraIris from 'svelte-material-icons/CameraIris.svelte';
import Close from 'svelte-material-icons/Close.svelte';
import ImageOutline from 'svelte-material-icons/ImageOutline.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 ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
import { getAssetFilename } from '$lib/utils/asset-utils';
import UserAvatar from '../shared-components/user-avatar.svelte';
export let asset: AssetResponseDto;
@ -268,12 +269,12 @@
</div>
</section>
{#if latlng}
{#if latlng && $featureFlags.loaded && $featureFlags.map}
<div class="h-[360px]">
{#await import('../shared-components/leaflet') then { Map, TileLayer, Marker }}
<Map center={latlng} zoom={14}>
<TileLayer
urlTemplate={'https://tile.openstreetmap.org/{z}/{x}/{y}.png'}
urlTemplate={$serverConfig.mapTileUrl}
options={{
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
}}

View file

@ -2,7 +2,7 @@
import { goto } from '$app/navigation';
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
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 { api, oauth } from '@api';
import { createEventDispatcher, onMount } from 'svelte';
@ -158,7 +158,7 @@
<LoadingSpinner />
</span>
{:else}
{$featureFlags.passwordLogin ? 'Login with OAuth' : 'Login'}
{$serverConfig.oauthButtonText}
{/if}
</Button>
</div>

View file

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

View file

@ -16,7 +16,7 @@
import IconButton from '$lib/components/elements/buttons/icon-button.svelte';
import Cog from 'svelte-material-icons/Cog.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 showUploadButton = true;

View file

@ -17,7 +17,7 @@
import SideBarButton from './side-bar-button.svelte';
import { locale } from '$lib/stores/preferences.store';
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 { data: stats } = await api.assetApi.getAssetStats(dto);
@ -62,9 +62,11 @@
<SideBarButton title="Explore" logo={Magnify} isSelected={$page.route.id === '/(user)/explore'} />
</a>
{/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">
<SideBarButton
title="Sharing"

View file

@ -1,6 +1,6 @@
<script lang="ts">
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 { onMount } from 'svelte';
import { fade } from 'svelte/transition';

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { browser } from '$app/environment';
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 SettingAccordion from '../admin-page/settings/setting-accordion.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 {
ARCHIVE = 'archive',
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">
import { goto } from '$app/navigation';
import AssetViewer from '$lib/components/asset-viewer/asset-viewer.svelte';
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
import MapSettingsModal from '$lib/components/map-page/map-settings-modal.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 { featureFlags, serverConfig } from '$lib/stores/server-config.store';
import { MapMarkerResponseDto, api } from '@api';
import { isEqual, omit } from 'lodash-es';
import { DateTime, Duration } from 'luxon';
import { onDestroy, onMount } from 'svelte';
import Cog from 'svelte-material-icons/Cog.svelte';
import type { PageData } from './$types';
import { DateTime, Duration } from 'luxon';
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
export let data: PageData;
@ -29,12 +32,12 @@
});
onDestroy(() => {
if (abortController) {
abortController.abort();
}
abortController?.abort();
assetViewingStore.showAssetViewer(false);
});
$: $featureFlags.map || goto(AppRoute.PHOTOS);
async function loadMapMarkers() {
if (abortController) {
abortController.abort();
@ -98,70 +101,72 @@
}
</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={{
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>
{/if}
</div>
</UserPageLayout>
>
<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)}
<Portal target="body">
{#if $showAssetViewer}
<AssetViewer
asset={$viewingAsset}
showNavigation={viewingAssets.length > 1}
on:next={navigateNext}
on:previous={navigatePrevious}
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}
</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}

View file

@ -14,7 +14,7 @@
import AppleHeader from '$lib/components/shared-components/apple-header.svelte';
import FaviconHeader from '$lib/components/shared-components/favicon-header.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 { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
import { api } from '@api';
@ -37,9 +37,9 @@
onMount(async () => {
try {
await loadFeatureFlags();
await loadConfig();
} 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 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 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 PasswordLoginSettings from '$lib/components/admin-page/settings/password-login/password-login-settings.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 LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
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 { SystemConfigDto, api, copyToClipboard } from '@api';
import Alert from 'svelte-material-icons/Alert.svelte';
@ -57,20 +58,6 @@
<span class="pl-2">Export as JSON</span>
</Button>
</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
title="Job Settings"
@ -80,14 +67,22 @@
<JobSettings disabled={$featureFlags.configFile} jobConfig={configs.job} />
</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 title="OAuth Authentication" subtitle="Manage the login with OAuth settings">
<OAuthSettings disabled={$featureFlags.configFile} oauthConfig={configs.oauth} />
</SettingAccordion>
<SettingAccordion title="Password Authentication" subtitle="Manage login with password settings">
<PasswordLoginSettings disabled={$featureFlags.configFile} passwordLoginConfig={configs.passwordLogin} />
</SettingAccordion>
<SettingAccordion
title="Storage Template"
subtitle="Manage the folder structure and file name of the upload asset"
@ -99,5 +94,16 @@
user={data.user}
/>
</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}
</section>

View file

@ -3,18 +3,17 @@
import LoginForm from '$lib/components/forms/login-form.svelte';
import FullscreenContainer from '$lib/components/shared-components/fullscreen-container.svelte';
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';
export let data: PageData;
</script>
{#if $featureFlags.loaded}
<FullscreenContainer title={data.meta.title} showMessage={!!loginPageMessage}>
<FullscreenContainer title={data.meta.title} showMessage={!!$serverConfig.loginPageMessage}>
<p slot="message">
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html loginPageMessage}
{@html $serverConfig.loginPageMessage}
</p>
<LoginForm