1
0
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:
Patrik J. Braun
2025-10-24 22:26:14 +02:00
parent 0d107cbfec
commit e103c59a56
11 changed files with 617 additions and 254 deletions

View File

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

View File

@@ -4,12 +4,15 @@ import {ServerExtensionsEntryConfig} from '../../../common/config/private/subcon
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 {
const c = Config.Extensions.extensions[this.extensionFolder] as ServerExtensionsEntryConfig;
const c = Config.Extensions.extensions[this.configKey] as ServerExtensionsEntryConfig;
return c?.configs as C;
}

View File

@@ -3,6 +3,7 @@ import * as fs from 'fs';
import * as path from 'path';
import {ServerExtensionsEntryConfig} from '../../../common/config/private/subconfigs/ServerExtensionsConfig';
import {ProjectPath} from '../../ProjectPath';
import {Utils} from '../../../common/Utils';
/**
@@ -14,7 +15,6 @@ export class ExtensionConfigTemplateLoader {
private static instance: ExtensionConfigTemplateLoader;
private loaded = false;
private extensionList: string[] = [];
private extensionTemplates: { folder: string, template?: { new(): unknown } }[] = [];
@@ -26,44 +26,6 @@ export class ExtensionConfigTemplateLoader {
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
* @param extFolder The folder name of the extension
@@ -99,16 +61,18 @@ export class ExtensionConfigTemplateLoader {
if (!ProjectPath.ExtensionFolder) {
throw new Error('Unknown extensions folder.');
}
if (!fs.existsSync(ProjectPath.ExtensionFolder)) {
return;
}
const newList = this.getExtensionFolders();
const loaded = Utils.equalsFilter(this.extensionList, newList);
this.extensionList = newList;
// already loaded
if (!this.loaded) {
if (!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) {
const extFolder = this.extensionList[i];
@@ -118,12 +82,55 @@ export class ExtensionConfigTemplateLoader {
}
}
}
this.loaded = true;
}
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) {
if (!this.extensionTemplates) {

View File

@@ -21,6 +21,7 @@ import {ExtensionListItem} from '../../../common/entities/extension/ExtensionLis
import {ExtensionConfigTemplateLoader} from './ExtensionConfigTemplateLoader';
import {Utils} from '../../../common/Utils';
import {UIExtensionDTO} from '../../../common/entities/extension/IClientUIConfig';
import {ExtensionConfigWrapper} from './ExtensionConfigWrapper';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const exec = util.promisify(require('child_process').exec);
const LOG_TAG = '[ExtensionManager]';
@@ -77,6 +78,10 @@ export class ExtensionManager implements IObjectManager {
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> {
if (!Config.Extensions.enabled) {
throw new Error('Extensions are disabled');
@@ -131,6 +136,83 @@ export class ExtensionManager implements IObjectManager {
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[] {
return Object.values(this.extObjects)
.filter(obj => !!obj.ui?.buttonConfigs?.length)
@@ -166,45 +248,45 @@ export class ExtensionManager implements IObjectManager {
ExtensionDecoratorObject.init(this.events);
}
private createUniqueExtensionObject(name: string, folder: string): IExtensionObject<unknown> {
let id = name;
if (this.extObjects[id]) {
private createUniqueExtensionObject(extensionName: string, folderName: string): IExtensionObject<unknown> {
let uniqueExtensionId = extensionName;
if (this.extObjects[uniqueExtensionId]) {
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]) {
this.extObjects[id] = new ExtensionObject(id, name, folder, this.router, this.events);
if (!this.extObjects[uniqueExtensionId]) {
this.extObjects[uniqueExtensionId] = new ExtensionObject(uniqueExtensionId, extensionName, folderName, this.router, this.events);
}
return this.extObjects[id];
return this.extObjects[uniqueExtensionId];
}
/**
* 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
*/
private async initSingleExtension(extId: string): Promise<void> {
const extConf: ServerExtensionsEntryConfig = Config.Extensions.extensions[extId] as ServerExtensionsEntryConfig;
private async initSingleExtension(configKey: string): Promise<void> {
const extConf: ServerExtensionsEntryConfig = Config.Extensions.extensions[configKey] as ServerExtensionsEntryConfig;
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;
}
const extFolder = extConf.path;
let extName = extFolder;
const folderName = extConf.path;
let extName = folderName;
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;
}
const extPath = path.join(ProjectPath.ExtensionFolder, extFolder);
const extPath = path.join(ProjectPath.ExtensionFolder, folderName);
const serverExtPath = path.join(extPath, 'server.js');
const packageJsonPath = path.join(extPath, 'package.json');
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;
}
@@ -227,8 +309,8 @@ export class ExtensionManager implements IObjectManager {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const ext = require(serverExtPath);
if (typeof ext?.init === 'function') {
Logger.debug(LOG_TAG, 'Running init on extension: ' + extFolder);
await ext?.init(this.createUniqueExtensionObject(extName, extFolder));
Logger.debug(LOG_TAG, 'Running init on extension: ' + folderName);
await ext?.init(this.createUniqueExtensionObject(extName, folderName));
}
}
@@ -246,16 +328,31 @@ export class ExtensionManager implements IObjectManager {
}
}
private async cleanUpExtensions() {
for (const extObj of Object.values(this.extObjects)) {
private async cleanUpSingleExtension(uniqueExtensionId: string): Promise<void> {
const extObj = this.extObjects[uniqueExtensionId];
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
const ext = require(serverExt);
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);
}
}
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);
}
}

View File

@@ -21,6 +21,13 @@ export class ExtensionObject<C> implements IExtensionObject<C> {
public readonly messengers;
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,
public readonly extensionName: string,
public readonly folder: string,

View File

@@ -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 {
this.buttonConfigs.push(buttonConfig);
// api path isn't set
if (!buttonConfig.apiPath) {
Logger.silly('[UIExtension]', 'Button config has no apiPath:' + buttonConfig.name);
if (!buttonConfig.apiPath && serverSB) {
Logger.warn('[UIExtension]', `Button config ${buttonConfig.name} has no apiPath, but has callback function. This is not supported.`);
return;
}
this.extensionObject.RESTApi.post.mediaJsonResponse([buttonConfig.apiPath], buttonConfig.minUserRole || UserRoles.LimitedGuest, !buttonConfig.skipDirectoryInvalidation, serverSB);

View File

@@ -10,6 +10,8 @@ export class ExtensionRouter {
public static route(app: Express): void {
this.addExtensionList(app);
this.addExtensionInstall(app);
this.addExtensionReload(app);
this.addExtensionDelete(app);
}
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
);
}
}

View File

@@ -17,4 +17,12 @@ export class ExtensionInstallerService {
public installExtension(extensionId: string): Promise<void> {
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});
}
}

View File

@@ -82,7 +82,8 @@
<ng-container *ngFor="let ck of getKeys(rStates)">
<ng-container *ngIf="!(rStates.value.__state[ck].shouldHide && rStates.value.__state[ck].shouldHide())">
<!-- 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="col-auto">
<h5>{{ rStates?.value.__state[ck].tags?.name || ck }}</h5>
@@ -98,7 +99,8 @@
</div>
</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--->
<app-settings-entry
@@ -127,12 +129,20 @@
<div class="headerless-sub-category-start">
<hr/>
</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>
</div>
<div class="col">
<hr/>
</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 class="mt-2 headerless-sub-category-content">
<ng-container

View File

@@ -7,7 +7,7 @@ import {WebConfig} from '../../../../../common/config/private/WebConfig';
import {JobProgressDTO} from '../../../../../common/entities/job/JobProgressDTO';
import {JobDTOUtils} from '../../../../../common/entities/job/JobDTO';
import {ScheduledJobsService} from '../scheduled-jobs.service';
import { UntypedFormControl, FormsModule } from '@angular/forms';
import {FormsModule, UntypedFormControl} from '@angular/forms';
import {Subscription} from 'rxjs';
import {IWebConfigClassPrivate} from 'typeconfig/src/decorators/class/IWebConfigClass';
import {ConfigPriority, TAGS} from '../../../../../common/config/public/ClientConfig';
@@ -17,11 +17,12 @@ import {WebConfigClassBuilder} from 'typeconfig/web';
import {ErrorDTO} from '../../../../../common/entities/Error';
import {ISettingsComponent} from './ISettingsComponent';
import {CustomSettingsEntries} from './CustomSettingsEntries';
import { NgIconComponent } from '@ng-icons/core';
import { NgIf, NgTemplateOutlet, NgFor, AsyncPipe } from '@angular/common';
import { SettingsEntryComponent } from './settings-entry/settings-entry.component';
import { JobButtonComponent } from '../workflow/button/job-button.settings.component';
import { JobProgressComponent } from '../workflow/progress/job-progress.settings.component';
import {NgIconComponent} from '@ng-icons/core';
import {AsyncPipe, NgFor, NgIf, NgTemplateOutlet} from '@angular/common';
import {SettingsEntryComponent} from './settings-entry/settings-entry.component';
import {JobButtonComponent} from '../workflow/button/job-button.settings.component';
import {JobProgressComponent} from '../workflow/progress/job-progress.settings.component';
import {ExtensionInstallerService} from '../extension-installer/extension-installer.service';
interface ConfigState {
@@ -76,13 +77,11 @@ export class TemplateComponent implements OnInit, OnChanges, OnDestroy, ISetting
public error: string = null;
public changed = false;
public states: RecursiveState = {} as RecursiveState;
public readonly ConfigStyle = ConfigStyle;
protected name: string;
protected sliceFN?: (s: IWebConfigClassPrivate<TAGS> & WebConfig) => ConfigState;
private subscription: Subscription = null;
private settingsSubscription: Subscription = null;
protected sliceFN?: (s: IWebConfigClassPrivate<TAGS> & WebConfig) => ConfigState;
public readonly ConfigStyle = ConfigStyle;
constructor(
protected authService: AuthenticationService,
@@ -90,9 +89,22 @@ export class TemplateComponent implements OnInit, OnChanges, OnDestroy, ISetting
protected notification: NotificationService,
public settingsService: SettingsService,
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 {
if (!this.ConfigPath) {
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 {
if (this.subscription != null) {
this.subscription.unsubscribe();
@@ -144,7 +155,6 @@ export class TemplateComponent implements OnInit, OnChanges, OnDestroy, ISetting
}
}
setSliceFN(sliceFN?: (s: IWebConfigClassPrivate<TAGS> & WebConfig) => ConfigState) {
if (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) => {
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);
}
public async save(): Promise<boolean> {
this.inProgress = true;
this.error = '';
@@ -314,11 +311,6 @@ export class TemplateComponent implements OnInit, OnChanges, OnDestroy, ISetting
return false;
}
private async getSettings(): Promise<void> {
await this.settingsService.getSettings();
this.changed = false;
}
getKeys(states: any): string[] {
if (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 || {})];
}
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;
}
}

View File

@@ -1,64 +1,141 @@
import { enableProdMode, Injectable, importProvidersFrom } from '@angular/core';
import { environment } from './environments/environment';
import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi, HttpClient } from '@angular/common/http';
import { ErrorInterceptor } from './app/model/network/helper/error.interceptor';
import { UrlSerializer, DefaultUrlSerializer, UrlTree } from '@angular/router';
import { HAMMER_GESTURE_CONFIG, HammerGestureConfig, BrowserModule, HammerModule, bootstrapApplication } from '@angular/platform-browser';
import { StringifySortingMethod } from './app/pipes/StringifySortingMethod';
import { NetworkService } from './app/model/network/network.service';
import { ShareService } from './app/ui/gallery/share.service';
import { UserService } from './app/model/network/user.service';
import { AlbumsService } from './app/ui/albums/albums.service';
import { GalleryCacheService } from './app/ui/gallery/cache.gallery.service';
import { ContentService } from './app/ui/gallery/content.service';
import { ContentLoaderService } from './app/ui/gallery/contentLoader.service';
import { FilterService } from './app/ui/gallery/filter/filter.service';
import { GallerySortingService } from './app/ui/gallery/navigator/sorting.service';
import { GalleryNavigatorService } from './app/ui/gallery/navigator/navigator.service';
import { MapService } from './app/ui/gallery/map/map.service';
import { BlogService } from './app/ui/gallery/blog/blog.service';
import { SearchQueryParserService } from './app/ui/gallery/search/search-query-parser.service';
import { AutoCompleteService } from './app/ui/gallery/search/autocomplete.service';
import { AuthenticationService } from './app/model/network/authentication.service';
import { ThumbnailLoaderService } from './app/ui/gallery/thumbnailLoader.service';
import { ThumbnailManagerService } from './app/ui/gallery/thumbnailManager.service';
import { NotificationService } from './app/model/notification.service';
import { FullScreenService } from './app/ui/gallery/fullscreen.service';
import { NavigationService } from './app/model/navigation.service';
import { SettingsService } from './app/ui/settings/settings.service';
import { SeededRandomService } from './app/model/seededRandom.service';
import { OverlayService } from './app/ui/gallery/overlay.service';
import { QueryService } from './app/model/query.service';
import { ThemeService } from './app/model/theme.service';
import { DuplicateService } from './app/ui/duplicates/duplicates.service';
import { FacesService } from './app/ui/faces/faces.service';
import { VersionService } from './app/model/version.service';
import { ScheduledJobsService } from './app/ui/settings/scheduled-jobs.service';
import { BackendtextService } from './app/model/backendtext.service';
import { CookieService } from 'ngx-cookie-service';
import { GPXFilesFilterPipe } from './app/pipes/GPXFilesFilterPipe';
import { MDFilesFilterPipe } from './app/pipes/MDFilesFilterPipe';
import { FileSizePipe } from './app/pipes/FileSizePipe';
import { DatePipe } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { provideAnimations } from '@angular/platform-browser/animations';
import { AppRoutingModule } from './app/app.routing';
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 { 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 {enableProdMode, importProvidersFrom, Injectable} from '@angular/core';
import {environment} from './environments/environment';
import {HTTP_INTERCEPTORS, HttpClient, provideHttpClient, withInterceptorsFromDi} from '@angular/common/http';
import {ErrorInterceptor} from './app/model/network/helper/error.interceptor';
import {DefaultUrlSerializer, UrlSerializer, UrlTree} from '@angular/router';
import {bootstrapApplication, BrowserModule, HAMMER_GESTURE_CONFIG, HammerGestureConfig, HammerModule} from '@angular/platform-browser';
import {StringifySortingMethod} from './app/pipes/StringifySortingMethod';
import {NetworkService} from './app/model/network/network.service';
import {ShareService} from './app/ui/gallery/share.service';
import {UserService} from './app/model/network/user.service';
import {AlbumsService} from './app/ui/albums/albums.service';
import {GalleryCacheService} from './app/ui/gallery/cache.gallery.service';
import {ContentService} from './app/ui/gallery/content.service';
import {ContentLoaderService} from './app/ui/gallery/contentLoader.service';
import {FilterService} from './app/ui/gallery/filter/filter.service';
import {GallerySortingService} from './app/ui/gallery/navigator/sorting.service';
import {GalleryNavigatorService} from './app/ui/gallery/navigator/navigator.service';
import {MapService} from './app/ui/gallery/map/map.service';
import {BlogService} from './app/ui/gallery/blog/blog.service';
import {SearchQueryParserService} from './app/ui/gallery/search/search-query-parser.service';
import {AutoCompleteService} from './app/ui/gallery/search/autocomplete.service';
import {AuthenticationService} from './app/model/network/authentication.service';
import {ThumbnailLoaderService} from './app/ui/gallery/thumbnailLoader.service';
import {ThumbnailManagerService} from './app/ui/gallery/thumbnailManager.service';
import {NotificationService} from './app/model/notification.service';
import {FullScreenService} from './app/ui/gallery/fullscreen.service';
import {NavigationService} from './app/model/navigation.service';
import {SettingsService} from './app/ui/settings/settings.service';
import {SeededRandomService} from './app/model/seededRandom.service';
import {OverlayService} from './app/ui/gallery/overlay.service';
import {QueryService} from './app/model/query.service';
import {ThemeService} from './app/model/theme.service';
import {DuplicateService} from './app/ui/duplicates/duplicates.service';
import {FacesService} from './app/ui/faces/faces.service';
import {VersionService} from './app/model/version.service';
import {ScheduledJobsService} from './app/ui/settings/scheduled-jobs.service';
import {BackendtextService} from './app/model/backendtext.service';
import {CookieService} from 'ngx-cookie-service';
import {GPXFilesFilterPipe} from './app/pipes/GPXFilesFilterPipe';
import {MDFilesFilterPipe} from './app/pipes/MDFilesFilterPipe';
import {FileSizePipe} from './app/pipes/FileSizePipe';
import {DatePipe} from '@angular/common';
import {FormsModule} from '@angular/forms';
import {provideAnimations} from '@angular/platform-browser/animations';
import {AppRoutingModule} from './app/app.routing';
import {NgIconsModule} from '@ng-icons/core';
import {
ionAddOutline,
ionAlbumsOutline,
ionAppsOutline,
ionArrowDownOutline,
ionArrowUpOutline,
ionBrowsersOutline,
ionBrushOutline,
ionCalendarOutline,
ionCameraOutline,
ionChatboxOutline,
ionCheckmarkOutline,
ionChevronBackOutline,
ionChevronDownOutline,
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 {MarkerFactory} from './app/ui/gallery/map/MarkerFactory';
import {DurationPipe} from './app/pipes/DurationPipe';
@@ -123,15 +200,15 @@ bootstrapApplication(AppComponent, {
ionTimeOutline, ionCheckmarkOutline, ionPulseOutline, ionResizeOutline,
ionCloudOutline, ionChatboxOutline, ionServerOutline, ionFileTrayFullOutline, ionBrushOutline,
ionBrowsersOutline, ionUnlinkOutline, ionSquareOutline, ionGridOutline,
ionAppsOutline, ionOpenOutline, ionRefresh, ionExtensionPuzzleOutline, ionList, ionPencil
ionAppsOutline, ionOpenOutline, ionRefresh, ionExtensionPuzzleOutline, ionList, ionPencil, ionReload
}), ClipboardModule, TooltipModule.forRoot(), ToastrModule.forRoot(),
ModalModule.forRoot(), CollapseModule.forRoot(), PopoverModule.forRoot(),
BsDropdownModule.forRoot(), BsDatepickerModule.forRoot(), TimepickerModule.forRoot(),
LoadingBarModule, LeafletModule, LeafletMarkerClusterModule,
MarkdownModule.forRoot({ loader: HttpClient })),
{ provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true },
{ provide: UrlSerializer, useClass: CustomUrlSerializer },
{ provide: HAMMER_GESTURE_CONFIG, useClass: MyHammerConfig },
MarkdownModule.forRoot({loader: HttpClient})),
{provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true},
{provide: UrlSerializer, useClass: CustomUrlSerializer},
{provide: HAMMER_GESTURE_CONFIG, useClass: MyHammerConfig},
StringifySortingMethod,
NetworkService,
ShareService,