mirror of
https://github.com/immich-app/immich.git
synced 2024-12-29 15:11:58 +00: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:
parent
3edade6761
commit
f1db257628
48 changed files with 658 additions and 160 deletions
112
cli/src/api/open-api/api.ts
generated
112
cli/src/api/open-api/api.ts
generated
|
@ -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.
|
||||
|
|
6
mobile/openapi/.openapi-generator/FILES
generated
6
mobile/openapi/.openapi-generator/FILES
generated
|
@ -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
|
||||
|
|
BIN
mobile/openapi/README.md
generated
BIN
mobile/openapi/README.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/ServerConfigDto.md
generated
Normal file
BIN
mobile/openapi/doc/ServerConfigDto.md
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/doc/ServerFeaturesDto.md
generated
BIN
mobile/openapi/doc/ServerFeaturesDto.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/ServerInfoApi.md
generated
BIN
mobile/openapi/doc/ServerInfoApi.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/SystemConfigDto.md
generated
BIN
mobile/openapi/doc/SystemConfigDto.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/SystemConfigMapDto.md
generated
Normal file
BIN
mobile/openapi/doc/SystemConfigMapDto.md
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/lib/api.dart
generated
BIN
mobile/openapi/lib/api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/server_info_api.dart
generated
BIN
mobile/openapi/lib/api/server_info_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api_client.dart
generated
BIN
mobile/openapi/lib/api_client.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/server_config_dto.dart
generated
Normal file
BIN
mobile/openapi/lib/model/server_config_dto.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/lib/model/server_features_dto.dart
generated
BIN
mobile/openapi/lib/model/server_features_dto.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/system_config_dto.dart
generated
BIN
mobile/openapi/lib/model/system_config_dto.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/system_config_map_dto.dart
generated
Normal file
BIN
mobile/openapi/lib/model/system_config_map_dto.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/test/server_config_dto_test.dart
generated
Normal file
BIN
mobile/openapi/test/server_config_dto_test.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/test/server_features_dto_test.dart
generated
BIN
mobile/openapi/test/server_features_dto_test.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/server_info_api_test.dart
generated
BIN
mobile/openapi/test/server_info_api_test.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/system_config_dto_test.dart
generated
BIN
mobile/openapi/test/system_config_dto_test.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/system_config_map_dto_test.dart
generated
Normal file
BIN
mobile/openapi/test/system_config_map_dto_test.dart
generated
Normal file
Binary file not shown.
|
@ -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": {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
import { IsBoolean, IsString } from 'class-validator';
|
||||
|
||||
export class SystemConfigMapDto {
|
||||
@IsBoolean()
|
||||
enabled!: boolean;
|
||||
|
||||
@IsString()
|
||||
tileUrl!: string;
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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',
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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');
|
||||
|
|
112
web/src/api/open-api/api.ts
generated
112
web/src/api/open-api/api.ts
generated
|
@ -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.
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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>
|
|
@ -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: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
|
||||
}}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
import { api, ServerFeaturesDto } from '@api';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export type FeatureFlags = ServerFeaturesDto & { loaded: boolean };
|
||||
|
||||
export const featureFlags = writable<FeatureFlags>({
|
||||
loaded: false,
|
||||
clipEncode: true,
|
||||
facialRecognition: true,
|
||||
sidecar: true,
|
||||
tagImage: true,
|
||||
search: true,
|
||||
oauth: false,
|
||||
oauthAutoLaunch: false,
|
||||
passwordLogin: true,
|
||||
configFile: false,
|
||||
});
|
||||
|
||||
export const loadFeatureFlags = async () => {
|
||||
const { data } = await api.serverInfoApi.getServerFeatures();
|
||||
featureFlags.update(() => ({ ...data, loaded: true }));
|
||||
};
|
37
web/src/lib/stores/server-config.store.ts
Normal file
37
web/src/lib/stores/server-config.store.ts
Normal 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 }));
|
||||
};
|
|
@ -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: '© <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: '© <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}
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue