You've already forked pigallery2
mirror of
https://github.com/bpatrik/pigallery2.git
synced 2025-11-25 22:32:52 +02:00
implement extension deleting and reloading #743
This commit is contained in:
@@ -79,4 +79,96 @@ export class ExtensionMWs {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async reloadExtension(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Extract the config key (folder name) from the request body
|
||||||
|
// Note: The request parameter is called 'path' for backwards compatibility,
|
||||||
|
// but it represents the config key used in Config.Extensions.extensions
|
||||||
|
const configKey = req.body.path;
|
||||||
|
|
||||||
|
if (!configKey) {
|
||||||
|
return next(
|
||||||
|
new ErrorDTO(
|
||||||
|
ErrorCodes.INPUT_ERROR,
|
||||||
|
'Extension config key is required'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call cleanUp and init on the ExtensionManager
|
||||||
|
await ObjectManagers.getInstance().ExtensionManager.reloadExtension(configKey);
|
||||||
|
|
||||||
|
// Set the result to an empty object (success)
|
||||||
|
req.resultPipe = { success: true };
|
||||||
|
return next();
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
return next(
|
||||||
|
new ErrorDTO(
|
||||||
|
ErrorCodes.JOB_ERROR,
|
||||||
|
'Extension reload error: ' + err.toString(),
|
||||||
|
err
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return next(
|
||||||
|
new ErrorDTO(
|
||||||
|
ErrorCodes.JOB_ERROR,
|
||||||
|
'Extension reload error: ' + JSON.stringify(err, null, ' '),
|
||||||
|
err
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async deleteExtension(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Extract the config key (folder name) from the request body
|
||||||
|
// Note: The request parameter is called 'path' for backwards compatibility,
|
||||||
|
// but it represents the config key used in Config.Extensions.extensions
|
||||||
|
const configKey = req.body.path;
|
||||||
|
|
||||||
|
if (!configKey) {
|
||||||
|
return next(
|
||||||
|
new ErrorDTO(
|
||||||
|
ErrorCodes.INPUT_ERROR,
|
||||||
|
'Extension config key is required'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call deleteExtension on the ExtensionManager to cleanup and remove folder
|
||||||
|
await ObjectManagers.getInstance().ExtensionManager.deleteExtension(configKey);
|
||||||
|
|
||||||
|
// Set the result to an empty object (success)
|
||||||
|
req.resultPipe = { success: true };
|
||||||
|
return next();
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
return next(
|
||||||
|
new ErrorDTO(
|
||||||
|
ErrorCodes.JOB_ERROR,
|
||||||
|
'Extension deletion error: ' + err.toString(),
|
||||||
|
err
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return next(
|
||||||
|
new ErrorDTO(
|
||||||
|
ErrorCodes.JOB_ERROR,
|
||||||
|
'Extension deletion error: ' + JSON.stringify(err, null, ' '),
|
||||||
|
err
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,15 @@ import {ServerExtensionsEntryConfig} from '../../../common/config/private/subcon
|
|||||||
|
|
||||||
export class ExtensionConfig<C> implements IExtensionConfig<C> {
|
export class ExtensionConfig<C> implements IExtensionConfig<C> {
|
||||||
|
|
||||||
constructor(private readonly extensionFolder: string) {
|
/**
|
||||||
|
* @param configKey - The key used in Config.Extensions.extensions map (matches the extension's folder name)
|
||||||
|
*/
|
||||||
|
constructor(private readonly configKey: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public getConfig(): C {
|
public getConfig(): C {
|
||||||
const c = Config.Extensions.extensions[this.extensionFolder] as ServerExtensionsEntryConfig;
|
const c = Config.Extensions.extensions[this.configKey] as ServerExtensionsEntryConfig;
|
||||||
|
|
||||||
return c?.configs as C;
|
return c?.configs as C;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import * as fs from 'fs';
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import {ServerExtensionsEntryConfig} from '../../../common/config/private/subconfigs/ServerExtensionsConfig';
|
import {ServerExtensionsEntryConfig} from '../../../common/config/private/subconfigs/ServerExtensionsConfig';
|
||||||
import {ProjectPath} from '../../ProjectPath';
|
import {ProjectPath} from '../../ProjectPath';
|
||||||
|
import {Utils} from '../../../common/Utils';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -14,7 +15,6 @@ export class ExtensionConfigTemplateLoader {
|
|||||||
|
|
||||||
private static instance: ExtensionConfigTemplateLoader;
|
private static instance: ExtensionConfigTemplateLoader;
|
||||||
|
|
||||||
private loaded = false;
|
|
||||||
private extensionList: string[] = [];
|
private extensionList: string[] = [];
|
||||||
private extensionTemplates: { folder: string, template?: { new(): unknown } }[] = [];
|
private extensionTemplates: { folder: string, template?: { new(): unknown } }[] = [];
|
||||||
|
|
||||||
@@ -26,44 +26,6 @@ export class ExtensionConfigTemplateLoader {
|
|||||||
return this.instance;
|
return this.instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads a single extension template
|
|
||||||
* @param extFolder The folder name of the extension
|
|
||||||
* @returns The extension template object if the extension is valid, null otherwise
|
|
||||||
*/
|
|
||||||
private loadSingleExtensionTemplate(extFolder: string): { folder: string, template?: { new(): unknown } } | null {
|
|
||||||
if (!ProjectPath.ExtensionFolder) {
|
|
||||||
throw new Error('Unknown extensions folder.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const extPath = path.join(ProjectPath.ExtensionFolder, extFolder);
|
|
||||||
const configExtPath = path.join(extPath, 'config.js');
|
|
||||||
const serverExtPath = path.join(extPath, 'server.js');
|
|
||||||
|
|
||||||
// if server.js is missing, it's not a valid extension
|
|
||||||
if (!fs.existsSync(serverExtPath)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let template: { folder: string, template?: { new(): unknown } } = { folder: extFolder };
|
|
||||||
|
|
||||||
if (fs.existsSync(configExtPath)) {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
||||||
const extCfg = require(configExtPath);
|
|
||||||
if (typeof extCfg?.initConfig === 'function') {
|
|
||||||
extCfg?.initConfig({
|
|
||||||
setConfigTemplate: (templateClass: { new(): unknown }): void => {
|
|
||||||
template = { folder: extFolder, template: templateClass };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return template;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads a single extension template and adds it to the config
|
* Loads a single extension template and adds it to the config
|
||||||
* @param extFolder The folder name of the extension
|
* @param extFolder The folder name of the extension
|
||||||
@@ -99,31 +61,76 @@ export class ExtensionConfigTemplateLoader {
|
|||||||
if (!ProjectPath.ExtensionFolder) {
|
if (!ProjectPath.ExtensionFolder) {
|
||||||
throw new Error('Unknown extensions folder.');
|
throw new Error('Unknown extensions folder.');
|
||||||
}
|
}
|
||||||
// already loaded
|
|
||||||
if (!this.loaded) {
|
|
||||||
this.extensionTemplates = [];
|
|
||||||
if (fs.existsSync(ProjectPath.ExtensionFolder)) {
|
|
||||||
this.extensionList = (fs
|
|
||||||
.readdirSync(ProjectPath.ExtensionFolder))
|
|
||||||
.filter((f): boolean =>
|
|
||||||
fs.statSync(path.join(ProjectPath.ExtensionFolder, f)).isDirectory()
|
|
||||||
);
|
|
||||||
this.extensionList.sort();
|
|
||||||
|
|
||||||
for (let i = 0; i < this.extensionList.length; ++i) {
|
if (!fs.existsSync(ProjectPath.ExtensionFolder)) {
|
||||||
const extFolder = this.extensionList[i];
|
return;
|
||||||
const template = this.loadSingleExtensionTemplate(extFolder);
|
}
|
||||||
if (template) {
|
|
||||||
this.extensionTemplates.push(template);
|
|
||||||
}
|
const newList = this.getExtensionFolders();
|
||||||
|
const loaded = Utils.equalsFilter(this.extensionList, newList);
|
||||||
|
this.extensionList = newList;
|
||||||
|
// already loaded
|
||||||
|
if (!loaded) {
|
||||||
|
this.extensionTemplates = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < this.extensionList.length; ++i) {
|
||||||
|
const extFolder = this.extensionList[i];
|
||||||
|
const template = this.loadSingleExtensionTemplate(extFolder);
|
||||||
|
if (template) {
|
||||||
|
this.extensionTemplates.push(template);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.loaded = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setTemplatesToConfig(config);
|
this.setTemplatesToConfig(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads a single extension template
|
||||||
|
* @param extFolder The folder name of the extension
|
||||||
|
* @returns The extension template object if the extension is valid, null otherwise
|
||||||
|
*/
|
||||||
|
private loadSingleExtensionTemplate(extFolder: string): { folder: string, template?: { new(): unknown } } | null {
|
||||||
|
if (!ProjectPath.ExtensionFolder) {
|
||||||
|
throw new Error('Unknown extensions folder.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const extPath = path.join(ProjectPath.ExtensionFolder, extFolder);
|
||||||
|
const configExtPath = path.join(extPath, 'config.js');
|
||||||
|
const serverExtPath = path.join(extPath, 'server.js');
|
||||||
|
|
||||||
|
// if server.js is missing, it's not a valid extension
|
||||||
|
if (!fs.existsSync(serverExtPath)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let template: { folder: string, template?: { new(): unknown } } = {folder: extFolder};
|
||||||
|
|
||||||
|
if (fs.existsSync(configExtPath)) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const extCfg = require(configExtPath);
|
||||||
|
if (typeof extCfg?.initConfig === 'function') {
|
||||||
|
extCfg?.initConfig({
|
||||||
|
setConfigTemplate: (templateClass: { new(): unknown }): void => {
|
||||||
|
template = {folder: extFolder, template: templateClass};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getExtensionFolders() {
|
||||||
|
const list = (fs
|
||||||
|
.readdirSync(ProjectPath.ExtensionFolder))
|
||||||
|
.filter((f): boolean =>
|
||||||
|
fs.statSync(path.join(ProjectPath.ExtensionFolder, f)).isDirectory()
|
||||||
|
);
|
||||||
|
list.sort();
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
private setTemplatesToConfig(config: PrivateConfigClass) {
|
private setTemplatesToConfig(config: PrivateConfigClass) {
|
||||||
if (!this.extensionTemplates) {
|
if (!this.extensionTemplates) {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {ExtensionListItem} from '../../../common/entities/extension/ExtensionLis
|
|||||||
import {ExtensionConfigTemplateLoader} from './ExtensionConfigTemplateLoader';
|
import {ExtensionConfigTemplateLoader} from './ExtensionConfigTemplateLoader';
|
||||||
import {Utils} from '../../../common/Utils';
|
import {Utils} from '../../../common/Utils';
|
||||||
import {UIExtensionDTO} from '../../../common/entities/extension/IClientUIConfig';
|
import {UIExtensionDTO} from '../../../common/entities/extension/IClientUIConfig';
|
||||||
|
import {ExtensionConfigWrapper} from './ExtensionConfigWrapper';
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const exec = util.promisify(require('child_process').exec);
|
const exec = util.promisify(require('child_process').exec);
|
||||||
const LOG_TAG = '[ExtensionManager]';
|
const LOG_TAG = '[ExtensionManager]';
|
||||||
@@ -77,6 +78,10 @@ export class ExtensionManager implements IObjectManager {
|
|||||||
this.extObjects = {};
|
this.extObjects = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Install an extension from the repository
|
||||||
|
* @param extensionId - The repository extension ID (not to be confused with the unique internal extensionId used in extObjects)
|
||||||
|
*/
|
||||||
public async installExtension(extensionId: string): Promise<void> {
|
public async installExtension(extensionId: string): Promise<void> {
|
||||||
if (!Config.Extensions.enabled) {
|
if (!Config.Extensions.enabled) {
|
||||||
throw new Error('Extensions are disabled');
|
throw new Error('Extensions are disabled');
|
||||||
@@ -131,6 +136,83 @@ export class ExtensionManager implements IObjectManager {
|
|||||||
Logger.debug(LOG_TAG, `Extension ${extensionId} installed successfully`);
|
Logger.debug(LOG_TAG, `Extension ${extensionId} installed successfully`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reload an extension by cleaning up and re-initializing it
|
||||||
|
* @param configKey - The key used in Config.Extensions.extensions map (typically the folder name)
|
||||||
|
*/
|
||||||
|
public async reloadExtension(configKey: string): Promise<void> {
|
||||||
|
if (!Config.Extensions.enabled) {
|
||||||
|
throw new Error('Extensions are disabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.debug(LOG_TAG, `Reloading extension with config key: ${configKey}`);
|
||||||
|
|
||||||
|
// Find the unique extension ID by matching the folder name
|
||||||
|
let uniqueExtensionId: string = null;
|
||||||
|
for (const id of Object.keys(this.extObjects)) {
|
||||||
|
if (this.extObjects[id].folder === configKey) {
|
||||||
|
uniqueExtensionId = id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!uniqueExtensionId) {
|
||||||
|
throw new Error(`Extension with config key ${configKey} not found in configuration`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up the extension
|
||||||
|
await this.cleanUpSingleExtension(uniqueExtensionId);
|
||||||
|
|
||||||
|
// Re-initialize the extension
|
||||||
|
await this.initSingleExtension(configKey);
|
||||||
|
|
||||||
|
Logger.debug(LOG_TAG, `Extension ${uniqueExtensionId} reloaded successfully`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an extension by cleaning up, removing its folder, and removing it from configuration
|
||||||
|
* @param configKey - The key used in Config.Extensions.extensions map (typically the folder name)
|
||||||
|
*/
|
||||||
|
public async deleteExtension(configKey: string): Promise<void> {
|
||||||
|
if (!Config.Extensions.enabled) {
|
||||||
|
throw new Error('Extensions are disabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.debug(LOG_TAG, `Deleting extension with config key: ${configKey}`);
|
||||||
|
|
||||||
|
// Find the unique extension ID by matching the folder name
|
||||||
|
let uniqueExtensionId: string = null;
|
||||||
|
for (const id of Object.keys(this.extObjects)) {
|
||||||
|
if (this.extObjects[id].folder === configKey) {
|
||||||
|
uniqueExtensionId = id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!uniqueExtensionId) {
|
||||||
|
// The extension does not have an extension object, probably it had no init function
|
||||||
|
Logger.silly(LOG_TAG, `Extension with config key ${configKey} not found in configuration`);
|
||||||
|
}else{
|
||||||
|
// Clean up the extension
|
||||||
|
await this.cleanUpSingleExtension(uniqueExtensionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the extension folder
|
||||||
|
const extPath = path.join(ProjectPath.ExtensionFolder, configKey);
|
||||||
|
if (fs.existsSync(extPath)) {
|
||||||
|
Logger.silly(LOG_TAG, `Removing extension folder: ${extPath}`);
|
||||||
|
fs.rmSync(extPath, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from configuration
|
||||||
|
const original = await ExtensionConfigWrapper.original();
|
||||||
|
original.Extensions.extensions.removeProperty(configKey);
|
||||||
|
await original.save();
|
||||||
|
Config.Extensions.extensions.removeProperty(configKey);
|
||||||
|
|
||||||
|
Logger.debug(LOG_TAG, `Extension ${configKey} deleted successfully`);
|
||||||
|
}
|
||||||
|
|
||||||
getUIExtensionConfigs(): UIExtensionDTO[] {
|
getUIExtensionConfigs(): UIExtensionDTO[] {
|
||||||
return Object.values(this.extObjects)
|
return Object.values(this.extObjects)
|
||||||
.filter(obj => !!obj.ui?.buttonConfigs?.length)
|
.filter(obj => !!obj.ui?.buttonConfigs?.length)
|
||||||
@@ -166,45 +248,45 @@ export class ExtensionManager implements IObjectManager {
|
|||||||
ExtensionDecoratorObject.init(this.events);
|
ExtensionDecoratorObject.init(this.events);
|
||||||
}
|
}
|
||||||
|
|
||||||
private createUniqueExtensionObject(name: string, folder: string): IExtensionObject<unknown> {
|
private createUniqueExtensionObject(extensionName: string, folderName: string): IExtensionObject<unknown> {
|
||||||
let id = name;
|
let uniqueExtensionId = extensionName;
|
||||||
if (this.extObjects[id]) {
|
if (this.extObjects[uniqueExtensionId]) {
|
||||||
let i = 0;
|
let i = 0;
|
||||||
while (this.extObjects[`${name}_${++i}`]) { /* empty */
|
while (this.extObjects[`${extensionName}_${++i}`]) { /* empty */
|
||||||
}
|
}
|
||||||
id = `${name}_${++i}`;
|
uniqueExtensionId = `${extensionName}_${++i}`;
|
||||||
}
|
}
|
||||||
if (!this.extObjects[id]) {
|
if (!this.extObjects[uniqueExtensionId]) {
|
||||||
this.extObjects[id] = new ExtensionObject(id, name, folder, this.router, this.events);
|
this.extObjects[uniqueExtensionId] = new ExtensionObject(uniqueExtensionId, extensionName, folderName, this.router, this.events);
|
||||||
}
|
}
|
||||||
return this.extObjects[id];
|
return this.extObjects[uniqueExtensionId];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize a single extension
|
* Initialize a single extension
|
||||||
* @param extId The id of the extension
|
* @param configKey The key used in Config.Extensions.extensions map (typically the folder name)
|
||||||
* @returns Promise that resolves when the extension is initialized
|
* @returns Promise that resolves when the extension is initialized
|
||||||
*/
|
*/
|
||||||
private async initSingleExtension(extId: string): Promise<void> {
|
private async initSingleExtension(configKey: string): Promise<void> {
|
||||||
const extConf: ServerExtensionsEntryConfig = Config.Extensions.extensions[extId] as ServerExtensionsEntryConfig;
|
const extConf: ServerExtensionsEntryConfig = Config.Extensions.extensions[configKey] as ServerExtensionsEntryConfig;
|
||||||
if (!extConf) {
|
if (!extConf) {
|
||||||
Logger.silly(LOG_TAG, `Skipping ${extId} initiation. Extension config is missing.`);
|
Logger.silly(LOG_TAG, `Skipping ${configKey} initiation. Extension config is missing.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const extFolder = extConf.path;
|
const folderName = extConf.path;
|
||||||
let extName = extFolder;
|
let extName = folderName;
|
||||||
|
|
||||||
if (extConf.enabled === false) {
|
if (extConf.enabled === false) {
|
||||||
Logger.silly(LOG_TAG, `Skipping ${extFolder} initiation. Extension is disabled.`);
|
Logger.silly(LOG_TAG, `Skipping ${folderName} initiation. Extension is disabled.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const extPath = path.join(ProjectPath.ExtensionFolder, extFolder);
|
const extPath = path.join(ProjectPath.ExtensionFolder, folderName);
|
||||||
const serverExtPath = path.join(extPath, 'server.js');
|
const serverExtPath = path.join(extPath, 'server.js');
|
||||||
const packageJsonPath = path.join(extPath, 'package.json');
|
const packageJsonPath = path.join(extPath, 'package.json');
|
||||||
|
|
||||||
if (!fs.existsSync(serverExtPath)) {
|
if (!fs.existsSync(serverExtPath)) {
|
||||||
Logger.silly(LOG_TAG, `Skipping ${extFolder} server initiation. server.js does not exists`);
|
Logger.silly(LOG_TAG, `Skipping ${folderName} server initiation. server.js does not exists`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,8 +309,8 @@ export class ExtensionManager implements IObjectManager {
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const ext = require(serverExtPath);
|
const ext = require(serverExtPath);
|
||||||
if (typeof ext?.init === 'function') {
|
if (typeof ext?.init === 'function') {
|
||||||
Logger.debug(LOG_TAG, 'Running init on extension: ' + extFolder);
|
Logger.debug(LOG_TAG, 'Running init on extension: ' + folderName);
|
||||||
await ext?.init(this.createUniqueExtensionObject(extName, extFolder));
|
await ext?.init(this.createUniqueExtensionObject(extName, folderName));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,16 +328,31 @@ export class ExtensionManager implements IObjectManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async cleanUpExtensions() {
|
private async cleanUpSingleExtension(uniqueExtensionId: string): Promise<void> {
|
||||||
for (const extObj of Object.values(this.extObjects)) {
|
const extObj = this.extObjects[uniqueExtensionId];
|
||||||
const serverExt = path.join(extObj.folder, 'server.js');
|
if (!extObj) {
|
||||||
|
Logger.silly(LOG_TAG, `Extension ${uniqueExtensionId} not found in extObjects, skipping cleanup`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverExt = path.join(extObj.folder, 'server.js');
|
||||||
|
if (fs.existsSync(serverExt)) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const ext = require(serverExt);
|
const ext = require(serverExt);
|
||||||
if (typeof ext?.cleanUp === 'function') {
|
if (typeof ext?.cleanUp === 'function') {
|
||||||
Logger.debug(LOG_TAG, 'Running Init on extension:' + extObj.extensionName);
|
Logger.debug(LOG_TAG, 'Running cleanUp on extension: ' + extObj.extensionName);
|
||||||
await ext?.cleanUp(extObj);
|
await ext?.cleanUp(extObj);
|
||||||
}
|
}
|
||||||
extObj.messengers.cleanUp();
|
}
|
||||||
|
extObj.messengers.cleanUp();
|
||||||
|
|
||||||
|
// Remove from extObjects
|
||||||
|
delete this.extObjects[uniqueExtensionId];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async cleanUpExtensions() {
|
||||||
|
for (const uniqueExtensionId of Object.keys(this.extObjects)) {
|
||||||
|
await this.cleanUpSingleExtension(uniqueExtensionId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,13 @@ export class ExtensionObject<C> implements IExtensionObject<C> {
|
|||||||
public readonly messengers;
|
public readonly messengers;
|
||||||
public readonly ui;
|
public readonly ui;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param extensionId - Unique ID used internally to track this extension instance (may have _1, _2 suffix if name collision occurs)
|
||||||
|
* @param extensionName - Display name of the extension (typically from package.json)
|
||||||
|
* @param folder - Folder name where the extension is stored (also used as config key in Config.Extensions.extensions)
|
||||||
|
* @param extensionRouter - Express router for extension REST API endpoints
|
||||||
|
* @param events - Extension events for hooking into gallery functionality
|
||||||
|
*/
|
||||||
constructor(public readonly extensionId: string,
|
constructor(public readonly extensionId: string,
|
||||||
public readonly extensionName: string,
|
public readonly extensionName: string,
|
||||||
public readonly folder: string,
|
public readonly folder: string,
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ export class UIExtension<C> implements IUIExtension<C> {
|
|||||||
public addMediaButton(buttonConfig: IClientMediaButtonConfig, serverSB: (params: ParamsDictionary, body: any, user: UserDTO, media: MediaEntity, repository: Repository<MediaEntity>) => Promise<void>): void {
|
public addMediaButton(buttonConfig: IClientMediaButtonConfig, serverSB: (params: ParamsDictionary, body: any, user: UserDTO, media: MediaEntity, repository: Repository<MediaEntity>) => Promise<void>): void {
|
||||||
this.buttonConfigs.push(buttonConfig);
|
this.buttonConfigs.push(buttonConfig);
|
||||||
// api path isn't set
|
// api path isn't set
|
||||||
if (!buttonConfig.apiPath) {
|
if (!buttonConfig.apiPath && serverSB) {
|
||||||
Logger.silly('[UIExtension]', 'Button config has no apiPath:' + buttonConfig.name);
|
Logger.warn('[UIExtension]', `Button config ${buttonConfig.name} has no apiPath, but has callback function. This is not supported.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.extensionObject.RESTApi.post.mediaJsonResponse([buttonConfig.apiPath], buttonConfig.minUserRole || UserRoles.LimitedGuest, !buttonConfig.skipDirectoryInvalidation, serverSB);
|
this.extensionObject.RESTApi.post.mediaJsonResponse([buttonConfig.apiPath], buttonConfig.minUserRole || UserRoles.LimitedGuest, !buttonConfig.skipDirectoryInvalidation, serverSB);
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ export class ExtensionRouter {
|
|||||||
public static route(app: Express): void {
|
public static route(app: Express): void {
|
||||||
this.addExtensionList(app);
|
this.addExtensionList(app);
|
||||||
this.addExtensionInstall(app);
|
this.addExtensionInstall(app);
|
||||||
|
this.addExtensionReload(app);
|
||||||
|
this.addExtensionDelete(app);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static addExtensionList(app: Express): void {
|
private static addExtensionList(app: Express): void {
|
||||||
@@ -34,4 +36,26 @@ export class ExtensionRouter {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static addExtensionReload(app: Express): void {
|
||||||
|
app.post(
|
||||||
|
[ExtensionManager.EXTENSION_API_PATH+'/reload'],
|
||||||
|
AuthenticationMWs.authenticate,
|
||||||
|
AuthenticationMWs.authorise(UserRoles.Admin),
|
||||||
|
ServerTimingMWs.addServerTiming,
|
||||||
|
ExtensionMWs.reloadExtension,
|
||||||
|
RenderingMWs.renderResult
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static addExtensionDelete(app: Express): void {
|
||||||
|
app.post(
|
||||||
|
[ExtensionManager.EXTENSION_API_PATH+'/delete'],
|
||||||
|
AuthenticationMWs.authenticate,
|
||||||
|
AuthenticationMWs.authorise(UserRoles.Admin),
|
||||||
|
ServerTimingMWs.addServerTiming,
|
||||||
|
ExtensionMWs.deleteExtension,
|
||||||
|
RenderingMWs.renderResult
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,4 +17,12 @@ export class ExtensionInstallerService {
|
|||||||
public installExtension(extensionId: string): Promise<void> {
|
public installExtension(extensionId: string): Promise<void> {
|
||||||
return this.networkService.postJson('/extension/install', {id: extensionId});
|
return this.networkService.postJson('/extension/install', {id: extensionId});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public reloadExtension(extensionPath: string): Promise<void> {
|
||||||
|
return this.networkService.postJson('/extension/reload', {path: extensionPath});
|
||||||
|
}
|
||||||
|
|
||||||
|
public deleteExtension(extensionPath: string): Promise<void> {
|
||||||
|
return this.networkService.postJson('/extension/delete', {path: extensionPath});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,7 +82,8 @@
|
|||||||
<ng-container *ngFor="let ck of getKeys(rStates)">
|
<ng-container *ngFor="let ck of getKeys(rStates)">
|
||||||
<ng-container *ngIf="!(rStates.value.__state[ck].shouldHide && rStates.value.__state[ck].shouldHide())">
|
<ng-container *ngIf="!(rStates.value.__state[ck].shouldHide && rStates.value.__state[ck].shouldHide())">
|
||||||
<!-- is array -->
|
<!-- is array -->
|
||||||
<ng-container *ngIf="rStates.value.__state[ck].isConfigArrayType && isExpandableArrayConfig(rStates.value.__state[ck])">
|
<ng-container
|
||||||
|
*ngIf="rStates.value.__state[ck].isConfigArrayType && isExpandableArrayConfig(rStates.value.__state[ck])">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<h5>{{ rStates?.value.__state[ck].tags?.name || ck }}</h5>
|
<h5>{{ rStates?.value.__state[ck].tags?.name || ck }}</h5>
|
||||||
@@ -98,7 +99,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container *ngIf="!rStates.value.__state[ck].isConfigArrayType || !isExpandableArrayConfig(rStates.value.__state[ck])">
|
<ng-container
|
||||||
|
*ngIf="!rStates.value.__state[ck].isConfigArrayType || !isExpandableArrayConfig(rStates.value.__state[ck])">
|
||||||
|
|
||||||
<!-- simple entries or complex once's but with custom UI--->
|
<!-- simple entries or complex once's but with custom UI--->
|
||||||
<app-settings-entry
|
<app-settings-entry
|
||||||
@@ -127,12 +129,20 @@
|
|||||||
<div class="headerless-sub-category-start">
|
<div class="headerless-sub-category-start">
|
||||||
<hr/>
|
<hr/>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto headerless-sub-category-title d-flex align-items-center ">
|
<div class="col-auto headerless-sub-category-title d-flex align-items-center">
|
||||||
<h5>{{ rStates?.value.__state[ck].tags?.name || ck }}</h5>
|
<h5>{{ rStates?.value.__state[ck].tags?.name || ck }}</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<hr/>
|
<hr/>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-auto" *ngIf="isExtension(rStates?.value.__state[ck])">
|
||||||
|
<button (click)="removeExtension(rStates?.value.__state[ck])" class="btn float-end btn-danger ">
|
||||||
|
<ng-icon name="ionTrashOutline" title="Delete" i18n-title></ng-icon>
|
||||||
|
</button>
|
||||||
|
<button (click)="reloadExtension(rStates?.value.__state[ck])" class="btn float-end btn-primary ">
|
||||||
|
<ng-icon name="ionReload" title="Restart" i18n-title></ng-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2 headerless-sub-category-content">
|
<div class="mt-2 headerless-sub-category-content">
|
||||||
<ng-container
|
<ng-container
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {WebConfig} from '../../../../../common/config/private/WebConfig';
|
|||||||
import {JobProgressDTO} from '../../../../../common/entities/job/JobProgressDTO';
|
import {JobProgressDTO} from '../../../../../common/entities/job/JobProgressDTO';
|
||||||
import {JobDTOUtils} from '../../../../../common/entities/job/JobDTO';
|
import {JobDTOUtils} from '../../../../../common/entities/job/JobDTO';
|
||||||
import {ScheduledJobsService} from '../scheduled-jobs.service';
|
import {ScheduledJobsService} from '../scheduled-jobs.service';
|
||||||
import { UntypedFormControl, FormsModule } from '@angular/forms';
|
import {FormsModule, UntypedFormControl} from '@angular/forms';
|
||||||
import {Subscription} from 'rxjs';
|
import {Subscription} from 'rxjs';
|
||||||
import {IWebConfigClassPrivate} from 'typeconfig/src/decorators/class/IWebConfigClass';
|
import {IWebConfigClassPrivate} from 'typeconfig/src/decorators/class/IWebConfigClass';
|
||||||
import {ConfigPriority, TAGS} from '../../../../../common/config/public/ClientConfig';
|
import {ConfigPriority, TAGS} from '../../../../../common/config/public/ClientConfig';
|
||||||
@@ -17,11 +17,12 @@ import {WebConfigClassBuilder} from 'typeconfig/web';
|
|||||||
import {ErrorDTO} from '../../../../../common/entities/Error';
|
import {ErrorDTO} from '../../../../../common/entities/Error';
|
||||||
import {ISettingsComponent} from './ISettingsComponent';
|
import {ISettingsComponent} from './ISettingsComponent';
|
||||||
import {CustomSettingsEntries} from './CustomSettingsEntries';
|
import {CustomSettingsEntries} from './CustomSettingsEntries';
|
||||||
import { NgIconComponent } from '@ng-icons/core';
|
import {NgIconComponent} from '@ng-icons/core';
|
||||||
import { NgIf, NgTemplateOutlet, NgFor, AsyncPipe } from '@angular/common';
|
import {AsyncPipe, NgFor, NgIf, NgTemplateOutlet} from '@angular/common';
|
||||||
import { SettingsEntryComponent } from './settings-entry/settings-entry.component';
|
import {SettingsEntryComponent} from './settings-entry/settings-entry.component';
|
||||||
import { JobButtonComponent } from '../workflow/button/job-button.settings.component';
|
import {JobButtonComponent} from '../workflow/button/job-button.settings.component';
|
||||||
import { JobProgressComponent } from '../workflow/progress/job-progress.settings.component';
|
import {JobProgressComponent} from '../workflow/progress/job-progress.settings.component';
|
||||||
|
import {ExtensionInstallerService} from '../extension-installer/extension-installer.service';
|
||||||
|
|
||||||
|
|
||||||
interface ConfigState {
|
interface ConfigState {
|
||||||
@@ -57,10 +58,10 @@ export interface RecursiveState extends ConfigState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-settings-template',
|
selector: 'app-settings-template',
|
||||||
templateUrl: './template.component.html',
|
templateUrl: './template.component.html',
|
||||||
styleUrls: ['./template.component.css'],
|
styleUrls: ['./template.component.css'],
|
||||||
imports: [FormsModule, NgIconComponent, NgIf, NgTemplateOutlet, NgFor, SettingsEntryComponent, JobButtonComponent, JobProgressComponent, AsyncPipe]
|
imports: [FormsModule, NgIconComponent, NgIf, NgTemplateOutlet, NgFor, SettingsEntryComponent, JobButtonComponent, JobProgressComponent, AsyncPipe]
|
||||||
})
|
})
|
||||||
export class TemplateComponent implements OnInit, OnChanges, OnDestroy, ISettingsComponent {
|
export class TemplateComponent implements OnInit, OnChanges, OnDestroy, ISettingsComponent {
|
||||||
|
|
||||||
@@ -76,13 +77,11 @@ export class TemplateComponent implements OnInit, OnChanges, OnDestroy, ISetting
|
|||||||
public error: string = null;
|
public error: string = null;
|
||||||
public changed = false;
|
public changed = false;
|
||||||
public states: RecursiveState = {} as RecursiveState;
|
public states: RecursiveState = {} as RecursiveState;
|
||||||
|
public readonly ConfigStyle = ConfigStyle;
|
||||||
protected name: string;
|
protected name: string;
|
||||||
|
protected sliceFN?: (s: IWebConfigClassPrivate<TAGS> & WebConfig) => ConfigState;
|
||||||
private subscription: Subscription = null;
|
private subscription: Subscription = null;
|
||||||
private settingsSubscription: Subscription = null;
|
private settingsSubscription: Subscription = null;
|
||||||
protected sliceFN?: (s: IWebConfigClassPrivate<TAGS> & WebConfig) => ConfigState;
|
|
||||||
|
|
||||||
public readonly ConfigStyle = ConfigStyle;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected authService: AuthenticationService,
|
protected authService: AuthenticationService,
|
||||||
@@ -90,9 +89,22 @@ export class TemplateComponent implements OnInit, OnChanges, OnDestroy, ISetting
|
|||||||
protected notification: NotificationService,
|
protected notification: NotificationService,
|
||||||
public settingsService: SettingsService,
|
public settingsService: SettingsService,
|
||||||
public jobsService: ScheduledJobsService,
|
public jobsService: ScheduledJobsService,
|
||||||
|
private extensionService: ExtensionInstallerService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get Name(): string {
|
||||||
|
return this.changed ? this.name + '*' : this.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
get Changed(): boolean {
|
||||||
|
return this.changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
get HasAvailableSettings(): boolean {
|
||||||
|
return !this.states?.shouldHide || !this.states?.shouldHide();
|
||||||
|
}
|
||||||
|
|
||||||
ngOnChanges(): void {
|
ngOnChanges(): void {
|
||||||
if (!this.ConfigPath) {
|
if (!this.ConfigPath) {
|
||||||
this.setSliceFN(c => ({value: c as any, isConfigType: true, type: WebConfig} as any));
|
this.setSliceFN(c => ({value: c as any, isConfigType: true, type: WebConfig} as any));
|
||||||
@@ -134,7 +146,6 @@ export class TemplateComponent implements OnInit, OnChanges, OnDestroy, ISetting
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
if (this.subscription != null) {
|
if (this.subscription != null) {
|
||||||
this.subscription.unsubscribe();
|
this.subscription.unsubscribe();
|
||||||
@@ -144,7 +155,6 @@ export class TemplateComponent implements OnInit, OnChanges, OnDestroy, ISetting
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
setSliceFN(sliceFN?: (s: IWebConfigClassPrivate<TAGS> & WebConfig) => ConfigState) {
|
setSliceFN(sliceFN?: (s: IWebConfigClassPrivate<TAGS> & WebConfig) => ConfigState) {
|
||||||
if (sliceFN) {
|
if (sliceFN) {
|
||||||
this.sliceFN = sliceFN;
|
this.sliceFN = sliceFN;
|
||||||
@@ -154,18 +164,6 @@ export class TemplateComponent implements OnInit, OnChanges, OnDestroy, ISetting
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get Name(): string {
|
|
||||||
return this.changed ? this.name + '*' : this.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
get Changed(): boolean {
|
|
||||||
return this.changed;
|
|
||||||
}
|
|
||||||
|
|
||||||
get HasAvailableSettings(): boolean {
|
|
||||||
return !this.states?.shouldHide || !this.states?.shouldHide();
|
|
||||||
}
|
|
||||||
|
|
||||||
onNewSettings = (s: IWebConfigClassPrivate<TAGS> & WebConfig) => {
|
onNewSettings = (s: IWebConfigClassPrivate<TAGS> & WebConfig) => {
|
||||||
this.states = this.sliceFN(s.clone()) as RecursiveState;
|
this.states = this.sliceFN(s.clone()) as RecursiveState;
|
||||||
|
|
||||||
@@ -289,7 +287,6 @@ export class TemplateComponent implements OnInit, OnChanges, OnDestroy, ISetting
|
|||||||
return c.isConfigArrayType && !CustomSettingsEntries.iS(c);
|
return c.isConfigArrayType && !CustomSettingsEntries.iS(c);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public async save(): Promise<boolean> {
|
public async save(): Promise<boolean> {
|
||||||
this.inProgress = true;
|
this.inProgress = true;
|
||||||
this.error = '';
|
this.error = '';
|
||||||
@@ -314,11 +311,6 @@ export class TemplateComponent implements OnInit, OnChanges, OnDestroy, ISetting
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getSettings(): Promise<void> {
|
|
||||||
await this.settingsService.getSettings();
|
|
||||||
this.changed = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
getKeys(states: any): string[] {
|
getKeys(states: any): string[] {
|
||||||
if (states.keys) {
|
if (states.keys) {
|
||||||
return states.keys;
|
return states.keys;
|
||||||
@@ -356,4 +348,50 @@ export class TemplateComponent implements OnInit, OnChanges, OnDestroy, ISetting
|
|||||||
}
|
}
|
||||||
return this.jobsService.progress.value[JobDTOUtils.getHashName(uiJob.job, uiJob.config || {})];
|
return this.jobsService.progress.value[JobDTOUtils.getHashName(uiJob.job, uiJob.config || {})];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected isExtension(c: ConfigState): boolean {
|
||||||
|
return (c?.value?.__propPath as any).substring(0, (c?.value?.__propPath as any).lastIndexOf('.')) == 'Extensions.extensions';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async removeExtension(c: ConfigState): Promise<void> {
|
||||||
|
const extensionPath = c.value.__state['path'].value;
|
||||||
|
try {
|
||||||
|
this.inProgress = true;
|
||||||
|
await this.extensionService.deleteExtension(extensionPath);
|
||||||
|
await this.settingsService.getSettings();
|
||||||
|
this.notification.success(
|
||||||
|
$localize`Extension deleted successfully`,
|
||||||
|
$localize`Success`
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
this.notification.error($localize`Failed to delete extension`, err);
|
||||||
|
} finally {
|
||||||
|
this.inProgress = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected async reloadExtension(c: ConfigState): Promise<void> {
|
||||||
|
const extensionPath = c.value.__state['path'].value;
|
||||||
|
try {
|
||||||
|
this.inProgress = true;
|
||||||
|
await this.extensionService.reloadExtension(extensionPath);
|
||||||
|
await this.settingsService.getSettings();
|
||||||
|
this.notification.success(
|
||||||
|
$localize`Extension reloaded successfully`,
|
||||||
|
$localize`Success`
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
this.notification.error($localize`Failed to reload extension`, err);
|
||||||
|
} finally {
|
||||||
|
this.inProgress = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getSettings(): Promise<void> {
|
||||||
|
await this.settingsService.getSettings();
|
||||||
|
this.changed = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,64 +1,141 @@
|
|||||||
import { enableProdMode, Injectable, importProvidersFrom } from '@angular/core';
|
import {enableProdMode, importProvidersFrom, Injectable} from '@angular/core';
|
||||||
import { environment } from './environments/environment';
|
import {environment} from './environments/environment';
|
||||||
import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi, HttpClient } from '@angular/common/http';
|
import {HTTP_INTERCEPTORS, HttpClient, provideHttpClient, withInterceptorsFromDi} from '@angular/common/http';
|
||||||
import { ErrorInterceptor } from './app/model/network/helper/error.interceptor';
|
import {ErrorInterceptor} from './app/model/network/helper/error.interceptor';
|
||||||
import { UrlSerializer, DefaultUrlSerializer, UrlTree } from '@angular/router';
|
import {DefaultUrlSerializer, UrlSerializer, UrlTree} from '@angular/router';
|
||||||
import { HAMMER_GESTURE_CONFIG, HammerGestureConfig, BrowserModule, HammerModule, bootstrapApplication } from '@angular/platform-browser';
|
import {bootstrapApplication, BrowserModule, HAMMER_GESTURE_CONFIG, HammerGestureConfig, HammerModule} from '@angular/platform-browser';
|
||||||
import { StringifySortingMethod } from './app/pipes/StringifySortingMethod';
|
import {StringifySortingMethod} from './app/pipes/StringifySortingMethod';
|
||||||
import { NetworkService } from './app/model/network/network.service';
|
import {NetworkService} from './app/model/network/network.service';
|
||||||
import { ShareService } from './app/ui/gallery/share.service';
|
import {ShareService} from './app/ui/gallery/share.service';
|
||||||
import { UserService } from './app/model/network/user.service';
|
import {UserService} from './app/model/network/user.service';
|
||||||
import { AlbumsService } from './app/ui/albums/albums.service';
|
import {AlbumsService} from './app/ui/albums/albums.service';
|
||||||
import { GalleryCacheService } from './app/ui/gallery/cache.gallery.service';
|
import {GalleryCacheService} from './app/ui/gallery/cache.gallery.service';
|
||||||
import { ContentService } from './app/ui/gallery/content.service';
|
import {ContentService} from './app/ui/gallery/content.service';
|
||||||
import { ContentLoaderService } from './app/ui/gallery/contentLoader.service';
|
import {ContentLoaderService} from './app/ui/gallery/contentLoader.service';
|
||||||
import { FilterService } from './app/ui/gallery/filter/filter.service';
|
import {FilterService} from './app/ui/gallery/filter/filter.service';
|
||||||
import { GallerySortingService } from './app/ui/gallery/navigator/sorting.service';
|
import {GallerySortingService} from './app/ui/gallery/navigator/sorting.service';
|
||||||
import { GalleryNavigatorService } from './app/ui/gallery/navigator/navigator.service';
|
import {GalleryNavigatorService} from './app/ui/gallery/navigator/navigator.service';
|
||||||
import { MapService } from './app/ui/gallery/map/map.service';
|
import {MapService} from './app/ui/gallery/map/map.service';
|
||||||
import { BlogService } from './app/ui/gallery/blog/blog.service';
|
import {BlogService} from './app/ui/gallery/blog/blog.service';
|
||||||
import { SearchQueryParserService } from './app/ui/gallery/search/search-query-parser.service';
|
import {SearchQueryParserService} from './app/ui/gallery/search/search-query-parser.service';
|
||||||
import { AutoCompleteService } from './app/ui/gallery/search/autocomplete.service';
|
import {AutoCompleteService} from './app/ui/gallery/search/autocomplete.service';
|
||||||
import { AuthenticationService } from './app/model/network/authentication.service';
|
import {AuthenticationService} from './app/model/network/authentication.service';
|
||||||
import { ThumbnailLoaderService } from './app/ui/gallery/thumbnailLoader.service';
|
import {ThumbnailLoaderService} from './app/ui/gallery/thumbnailLoader.service';
|
||||||
import { ThumbnailManagerService } from './app/ui/gallery/thumbnailManager.service';
|
import {ThumbnailManagerService} from './app/ui/gallery/thumbnailManager.service';
|
||||||
import { NotificationService } from './app/model/notification.service';
|
import {NotificationService} from './app/model/notification.service';
|
||||||
import { FullScreenService } from './app/ui/gallery/fullscreen.service';
|
import {FullScreenService} from './app/ui/gallery/fullscreen.service';
|
||||||
import { NavigationService } from './app/model/navigation.service';
|
import {NavigationService} from './app/model/navigation.service';
|
||||||
import { SettingsService } from './app/ui/settings/settings.service';
|
import {SettingsService} from './app/ui/settings/settings.service';
|
||||||
import { SeededRandomService } from './app/model/seededRandom.service';
|
import {SeededRandomService} from './app/model/seededRandom.service';
|
||||||
import { OverlayService } from './app/ui/gallery/overlay.service';
|
import {OverlayService} from './app/ui/gallery/overlay.service';
|
||||||
import { QueryService } from './app/model/query.service';
|
import {QueryService} from './app/model/query.service';
|
||||||
import { ThemeService } from './app/model/theme.service';
|
import {ThemeService} from './app/model/theme.service';
|
||||||
import { DuplicateService } from './app/ui/duplicates/duplicates.service';
|
import {DuplicateService} from './app/ui/duplicates/duplicates.service';
|
||||||
import { FacesService } from './app/ui/faces/faces.service';
|
import {FacesService} from './app/ui/faces/faces.service';
|
||||||
import { VersionService } from './app/model/version.service';
|
import {VersionService} from './app/model/version.service';
|
||||||
import { ScheduledJobsService } from './app/ui/settings/scheduled-jobs.service';
|
import {ScheduledJobsService} from './app/ui/settings/scheduled-jobs.service';
|
||||||
import { BackendtextService } from './app/model/backendtext.service';
|
import {BackendtextService} from './app/model/backendtext.service';
|
||||||
import { CookieService } from 'ngx-cookie-service';
|
import {CookieService} from 'ngx-cookie-service';
|
||||||
import { GPXFilesFilterPipe } from './app/pipes/GPXFilesFilterPipe';
|
import {GPXFilesFilterPipe} from './app/pipes/GPXFilesFilterPipe';
|
||||||
import { MDFilesFilterPipe } from './app/pipes/MDFilesFilterPipe';
|
import {MDFilesFilterPipe} from './app/pipes/MDFilesFilterPipe';
|
||||||
import { FileSizePipe } from './app/pipes/FileSizePipe';
|
import {FileSizePipe} from './app/pipes/FileSizePipe';
|
||||||
import { DatePipe } from '@angular/common';
|
import {DatePipe} from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
import {FormsModule} from '@angular/forms';
|
||||||
import { provideAnimations } from '@angular/platform-browser/animations';
|
import {provideAnimations} from '@angular/platform-browser/animations';
|
||||||
import { AppRoutingModule } from './app/app.routing';
|
import {AppRoutingModule} from './app/app.routing';
|
||||||
import { NgIconsModule } from '@ng-icons/core';
|
import {NgIconsModule} from '@ng-icons/core';
|
||||||
import { ionDownloadOutline, ionFunnelOutline, ionGitBranchOutline, ionArrowDownOutline, ionArrowUpOutline, ionStarOutline, ionStar, ionCalendarOutline, ionPersonOutline, ionShuffleOutline, ionPeopleOutline, ionMenuOutline, ionShareSocialOutline, ionImagesOutline, ionLinkOutline, ionSearchOutline, ionHammerOutline, ionCopyOutline, ionAlbumsOutline, ionSettingsOutline, ionLogOutOutline, ionChevronForwardOutline, ionChevronDownOutline, ionChevronBackOutline, ionTrashOutline, ionSaveOutline, ionAddOutline, ionRemoveOutline, ionTextOutline, ionFolderOutline, ionDocumentOutline, ionDocumentTextOutline, ionImageOutline, ionPricetagOutline, ionLocationOutline, ionSunnyOutline, ionMoonOutline, ionVideocamOutline, ionInformationCircleOutline, ionInformationOutline, ionContractOutline, ionExpandOutline, ionCloseOutline, ionTimerOutline, ionPlayOutline, ionPauseOutline, ionVolumeMediumOutline, ionVolumeMuteOutline, ionCameraOutline, ionWarningOutline, ionLockClosedOutline, ionChevronUpOutline, ionFlagOutline, ionGlobeOutline, ionPieChartOutline, ionStopOutline, ionTimeOutline, ionCheckmarkOutline, ionPulseOutline, ionResizeOutline, ionCloudOutline, ionChatboxOutline, ionServerOutline, ionFileTrayFullOutline, ionBrushOutline, ionBrowsersOutline, ionUnlinkOutline, ionSquareOutline, ionGridOutline, ionAppsOutline, ionOpenOutline, ionRefresh, ionExtensionPuzzleOutline, ionList, ionPencil } from '@ng-icons/ionicons';
|
import {
|
||||||
import { ClipboardModule } from 'ngx-clipboard';
|
ionAddOutline,
|
||||||
import { TooltipModule } from 'ngx-bootstrap/tooltip';
|
ionAlbumsOutline,
|
||||||
import { ToastrModule } from 'ngx-toastr';
|
ionAppsOutline,
|
||||||
import { ModalModule } from 'ngx-bootstrap/modal';
|
ionArrowDownOutline,
|
||||||
import { CollapseModule } from 'ngx-bootstrap/collapse';
|
ionArrowUpOutline,
|
||||||
import { PopoverModule } from 'ngx-bootstrap/popover';
|
ionBrowsersOutline,
|
||||||
import { BsDropdownModule } from 'ngx-bootstrap/dropdown';
|
ionBrushOutline,
|
||||||
import { BsDatepickerModule } from 'ngx-bootstrap/datepicker';
|
ionCalendarOutline,
|
||||||
import { TimepickerModule } from 'ngx-bootstrap/timepicker';
|
ionCameraOutline,
|
||||||
import { LoadingBarModule } from '@ngx-loading-bar/core';
|
ionChatboxOutline,
|
||||||
import { LeafletModule } from '@bluehalo/ngx-leaflet';
|
ionCheckmarkOutline,
|
||||||
import { LeafletMarkerClusterModule } from '@bluehalo/ngx-leaflet-markercluster';
|
ionChevronBackOutline,
|
||||||
import { MarkdownModule } from 'ngx-markdown';
|
ionChevronDownOutline,
|
||||||
import { AppComponent } from './app/app.component';
|
ionChevronForwardOutline,
|
||||||
|
ionChevronUpOutline,
|
||||||
|
ionCloseOutline,
|
||||||
|
ionCloudOutline,
|
||||||
|
ionContractOutline,
|
||||||
|
ionCopyOutline,
|
||||||
|
ionDocumentOutline,
|
||||||
|
ionDocumentTextOutline,
|
||||||
|
ionDownloadOutline,
|
||||||
|
ionExpandOutline,
|
||||||
|
ionExtensionPuzzleOutline,
|
||||||
|
ionFileTrayFullOutline,
|
||||||
|
ionFlagOutline,
|
||||||
|
ionFolderOutline,
|
||||||
|
ionFunnelOutline,
|
||||||
|
ionGitBranchOutline,
|
||||||
|
ionGlobeOutline,
|
||||||
|
ionGridOutline,
|
||||||
|
ionHammerOutline,
|
||||||
|
ionImageOutline,
|
||||||
|
ionImagesOutline,
|
||||||
|
ionInformationCircleOutline,
|
||||||
|
ionInformationOutline,
|
||||||
|
ionLinkOutline,
|
||||||
|
ionList,
|
||||||
|
ionLocationOutline,
|
||||||
|
ionLockClosedOutline,
|
||||||
|
ionLogOutOutline,
|
||||||
|
ionMenuOutline,
|
||||||
|
ionMoonOutline,
|
||||||
|
ionOpenOutline,
|
||||||
|
ionPauseOutline,
|
||||||
|
ionPencil,
|
||||||
|
ionPeopleOutline,
|
||||||
|
ionPersonOutline,
|
||||||
|
ionPieChartOutline,
|
||||||
|
ionPlayOutline,
|
||||||
|
ionPricetagOutline,
|
||||||
|
ionPulseOutline,
|
||||||
|
ionRefresh,
|
||||||
|
ionRemoveOutline,
|
||||||
|
ionResizeOutline,
|
||||||
|
ionSaveOutline,
|
||||||
|
ionSearchOutline,
|
||||||
|
ionServerOutline,
|
||||||
|
ionSettingsOutline,
|
||||||
|
ionShareSocialOutline,
|
||||||
|
ionShuffleOutline,
|
||||||
|
ionSquareOutline,
|
||||||
|
ionStar,
|
||||||
|
ionStarOutline,
|
||||||
|
ionStopOutline,
|
||||||
|
ionSunnyOutline,
|
||||||
|
ionTextOutline,
|
||||||
|
ionTimeOutline,
|
||||||
|
ionTimerOutline,
|
||||||
|
ionTrashOutline,
|
||||||
|
ionUnlinkOutline,
|
||||||
|
ionVideocamOutline,
|
||||||
|
ionVolumeMediumOutline,
|
||||||
|
ionVolumeMuteOutline,
|
||||||
|
ionWarningOutline,
|
||||||
|
ionReload
|
||||||
|
} from '@ng-icons/ionicons';
|
||||||
|
import {ClipboardModule} from 'ngx-clipboard';
|
||||||
|
import {TooltipModule} from 'ngx-bootstrap/tooltip';
|
||||||
|
import {ToastrModule} from 'ngx-toastr';
|
||||||
|
import {ModalModule} from 'ngx-bootstrap/modal';
|
||||||
|
import {CollapseModule} from 'ngx-bootstrap/collapse';
|
||||||
|
import {PopoverModule} from 'ngx-bootstrap/popover';
|
||||||
|
import {BsDropdownModule} from 'ngx-bootstrap/dropdown';
|
||||||
|
import {BsDatepickerModule} from 'ngx-bootstrap/datepicker';
|
||||||
|
import {TimepickerModule} from 'ngx-bootstrap/timepicker';
|
||||||
|
import {LoadingBarModule} from '@ngx-loading-bar/core';
|
||||||
|
import {LeafletModule} from '@bluehalo/ngx-leaflet';
|
||||||
|
import {LeafletMarkerClusterModule} from '@bluehalo/ngx-leaflet-markercluster';
|
||||||
|
import {MarkdownModule} from 'ngx-markdown';
|
||||||
|
import {AppComponent} from './app/app.component';
|
||||||
import {Marker} from 'leaflet';
|
import {Marker} from 'leaflet';
|
||||||
import {MarkerFactory} from './app/ui/gallery/map/MarkerFactory';
|
import {MarkerFactory} from './app/ui/gallery/map/MarkerFactory';
|
||||||
import {DurationPipe} from './app/pipes/DurationPipe';
|
import {DurationPipe} from './app/pipes/DurationPipe';
|
||||||
@@ -100,77 +177,77 @@ export class CustomUrlSerializer implements UrlSerializer {
|
|||||||
Marker.prototype.options.icon = MarkerFactory.defIcon;
|
Marker.prototype.options.icon = MarkerFactory.defIcon;
|
||||||
|
|
||||||
bootstrapApplication(AppComponent, {
|
bootstrapApplication(AppComponent, {
|
||||||
providers: [
|
providers: [
|
||||||
importProvidersFrom(BrowserModule, HammerModule, FormsModule, AppRoutingModule, NgIconsModule.withIcons({
|
importProvidersFrom(BrowserModule, HammerModule, FormsModule, AppRoutingModule, NgIconsModule.withIcons({
|
||||||
ionDownloadOutline, ionFunnelOutline,
|
ionDownloadOutline, ionFunnelOutline,
|
||||||
ionGitBranchOutline, ionArrowDownOutline, ionArrowUpOutline,
|
ionGitBranchOutline, ionArrowDownOutline, ionArrowUpOutline,
|
||||||
ionStarOutline, ionStar, ionCalendarOutline, ionPersonOutline, ionShuffleOutline,
|
ionStarOutline, ionStar, ionCalendarOutline, ionPersonOutline, ionShuffleOutline,
|
||||||
ionPeopleOutline,
|
ionPeopleOutline,
|
||||||
ionMenuOutline, ionShareSocialOutline,
|
ionMenuOutline, ionShareSocialOutline,
|
||||||
ionImagesOutline, ionLinkOutline, ionSearchOutline, ionHammerOutline, ionCopyOutline,
|
ionImagesOutline, ionLinkOutline, ionSearchOutline, ionHammerOutline, ionCopyOutline,
|
||||||
ionAlbumsOutline, ionSettingsOutline, ionLogOutOutline,
|
ionAlbumsOutline, ionSettingsOutline, ionLogOutOutline,
|
||||||
ionChevronForwardOutline, ionChevronDownOutline, ionChevronBackOutline,
|
ionChevronForwardOutline, ionChevronDownOutline, ionChevronBackOutline,
|
||||||
ionTrashOutline, ionSaveOutline, ionAddOutline, ionRemoveOutline,
|
ionTrashOutline, ionSaveOutline, ionAddOutline, ionRemoveOutline,
|
||||||
ionTextOutline, ionFolderOutline, ionDocumentOutline, ionDocumentTextOutline, ionImageOutline,
|
ionTextOutline, ionFolderOutline, ionDocumentOutline, ionDocumentTextOutline, ionImageOutline,
|
||||||
ionPricetagOutline, ionLocationOutline,
|
ionPricetagOutline, ionLocationOutline,
|
||||||
ionSunnyOutline, ionMoonOutline, ionVideocamOutline,
|
ionSunnyOutline, ionMoonOutline, ionVideocamOutline,
|
||||||
ionInformationCircleOutline,
|
ionInformationCircleOutline,
|
||||||
ionInformationOutline, ionContractOutline, ionExpandOutline, ionCloseOutline,
|
ionInformationOutline, ionContractOutline, ionExpandOutline, ionCloseOutline,
|
||||||
ionTimerOutline,
|
ionTimerOutline,
|
||||||
ionPlayOutline, ionPauseOutline, ionVolumeMediumOutline, ionVolumeMuteOutline,
|
ionPlayOutline, ionPauseOutline, ionVolumeMediumOutline, ionVolumeMuteOutline,
|
||||||
ionCameraOutline, ionWarningOutline, ionLockClosedOutline, ionChevronUpOutline,
|
ionCameraOutline, ionWarningOutline, ionLockClosedOutline, ionChevronUpOutline,
|
||||||
ionFlagOutline, ionGlobeOutline, ionPieChartOutline, ionStopOutline,
|
ionFlagOutline, ionGlobeOutline, ionPieChartOutline, ionStopOutline,
|
||||||
ionTimeOutline, ionCheckmarkOutline, ionPulseOutline, ionResizeOutline,
|
ionTimeOutline, ionCheckmarkOutline, ionPulseOutline, ionResizeOutline,
|
||||||
ionCloudOutline, ionChatboxOutline, ionServerOutline, ionFileTrayFullOutline, ionBrushOutline,
|
ionCloudOutline, ionChatboxOutline, ionServerOutline, ionFileTrayFullOutline, ionBrushOutline,
|
||||||
ionBrowsersOutline, ionUnlinkOutline, ionSquareOutline, ionGridOutline,
|
ionBrowsersOutline, ionUnlinkOutline, ionSquareOutline, ionGridOutline,
|
||||||
ionAppsOutline, ionOpenOutline, ionRefresh, ionExtensionPuzzleOutline, ionList, ionPencil
|
ionAppsOutline, ionOpenOutline, ionRefresh, ionExtensionPuzzleOutline, ionList, ionPencil, ionReload
|
||||||
}), ClipboardModule, TooltipModule.forRoot(), ToastrModule.forRoot(),
|
}), ClipboardModule, TooltipModule.forRoot(), ToastrModule.forRoot(),
|
||||||
ModalModule.forRoot(), CollapseModule.forRoot(), PopoverModule.forRoot(),
|
ModalModule.forRoot(), CollapseModule.forRoot(), PopoverModule.forRoot(),
|
||||||
BsDropdownModule.forRoot(), BsDatepickerModule.forRoot(), TimepickerModule.forRoot(),
|
BsDropdownModule.forRoot(), BsDatepickerModule.forRoot(), TimepickerModule.forRoot(),
|
||||||
LoadingBarModule, LeafletModule, LeafletMarkerClusterModule,
|
LoadingBarModule, LeafletModule, LeafletMarkerClusterModule,
|
||||||
MarkdownModule.forRoot({ loader: HttpClient })),
|
MarkdownModule.forRoot({loader: HttpClient})),
|
||||||
{ provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true },
|
{provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true},
|
||||||
{ provide: UrlSerializer, useClass: CustomUrlSerializer },
|
{provide: UrlSerializer, useClass: CustomUrlSerializer},
|
||||||
{ provide: HAMMER_GESTURE_CONFIG, useClass: MyHammerConfig },
|
{provide: HAMMER_GESTURE_CONFIG, useClass: MyHammerConfig},
|
||||||
StringifySortingMethod,
|
StringifySortingMethod,
|
||||||
NetworkService,
|
NetworkService,
|
||||||
ShareService,
|
ShareService,
|
||||||
UserService,
|
UserService,
|
||||||
AlbumsService,
|
AlbumsService,
|
||||||
GalleryCacheService,
|
GalleryCacheService,
|
||||||
ContentService,
|
ContentService,
|
||||||
ContentLoaderService,
|
ContentLoaderService,
|
||||||
FilterService,
|
FilterService,
|
||||||
GallerySortingService,
|
GallerySortingService,
|
||||||
GalleryNavigatorService,
|
GalleryNavigatorService,
|
||||||
MapService,
|
MapService,
|
||||||
BlogService,
|
BlogService,
|
||||||
SearchQueryParserService,
|
SearchQueryParserService,
|
||||||
AutoCompleteService,
|
AutoCompleteService,
|
||||||
AuthenticationService,
|
AuthenticationService,
|
||||||
ThumbnailLoaderService,
|
ThumbnailLoaderService,
|
||||||
ThumbnailManagerService,
|
ThumbnailManagerService,
|
||||||
NotificationService,
|
NotificationService,
|
||||||
FullScreenService,
|
FullScreenService,
|
||||||
NavigationService,
|
NavigationService,
|
||||||
SettingsService,
|
SettingsService,
|
||||||
SeededRandomService,
|
SeededRandomService,
|
||||||
OverlayService,
|
OverlayService,
|
||||||
QueryService,
|
QueryService,
|
||||||
ThemeService,
|
ThemeService,
|
||||||
DuplicateService,
|
DuplicateService,
|
||||||
FacesService,
|
FacesService,
|
||||||
VersionService,
|
VersionService,
|
||||||
ScheduledJobsService,
|
ScheduledJobsService,
|
||||||
BackendtextService,
|
BackendtextService,
|
||||||
CookieService,
|
CookieService,
|
||||||
GPXFilesFilterPipe,
|
GPXFilesFilterPipe,
|
||||||
MDFilesFilterPipe,
|
MDFilesFilterPipe,
|
||||||
FileSizePipe,
|
FileSizePipe,
|
||||||
DatePipe,
|
DatePipe,
|
||||||
DurationPipe,
|
DurationPipe,
|
||||||
provideHttpClient(withInterceptorsFromDi()),
|
provideHttpClient(withInterceptorsFromDi()),
|
||||||
provideAnimations()
|
provideAnimations()
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
.catch((err) => console.error(err));
|
.catch((err) => console.error(err));
|
||||||
|
|||||||
Reference in New Issue
Block a user