1
0
mirror of https://github.com/bpatrik/pigallery2.git synced 2024-12-23 01:27:14 +02:00

Cleaning up config #569

This commit is contained in:
Patrik J. Braun 2022-12-31 00:20:42 +01:00
parent 875d120df8
commit c191549270
70 changed files with 95 additions and 3593 deletions

View File

@ -100,9 +100,10 @@ import {GallerySortingService} from './ui/gallery/navigator/sorting.service';
import {FilterService} from './ui/gallery/filter/filter.service';
import {TemplateComponent} from './ui/settings/template/template.component';
import {AbstractSettingsService} from './ui/settings/_abstract/abstract.settings.service';
import { WorkflowComponent } from './ui/settings/workflow/workflow.component';
import {WorkflowComponent} from './ui/settings/workflow/workflow.component';
import {JobProgressComponent} from './ui/settings/jobs/progress/job-progress.settings.component';
import {JobButtonComponent} from './ui/settings/jobs/button/job-button.settings.component';
import {GalleryStatisticComponent} from './ui/settings/gallery-statistic/gallery-statistic.component';
@Injectable()
export class MyHammerConfig extends HammerGestureConfig {
@ -222,24 +223,8 @@ Marker.prototype.options.icon = iconDefault;
TemplateComponent,
JobProgressComponent,
JobButtonComponent,
/* UserMangerSettingsComponent,
DatabaseSettingsComponent,
MapSettingsComponent,
ThumbnailSettingsComponent,
VideoSettingsComponent,
PhotoSettingsComponent,
MetaFileSettingsComponent,
SearchSettingsComponent,
ShareSettingsComponent,
RandomPhotoSettingsComponent,
FacesSettingsComponent,
AlbumsSettingsComponent,
OtherSettingsComponent,
IndexingSettingsComponent,
JobsSettingsComponent,
JobProgressComponent,
JobButtonComponent,
PreviewSettingsComponent,*/
WorkflowComponent,
GalleryStatisticComponent,
// Pipes
StringifyRole,
@ -252,7 +237,6 @@ Marker.prototype.options.icon = iconDefault;
StringifySearchQuery,
FileDTOToPathPipe,
PhotoFilterPipe,
WorkflowComponent,
],
providers: [
{provide: HTTP_INTERCEPTORS, useClass: CSRFInterceptor, multi: true},

View File

@ -91,7 +91,14 @@
#tmpl
icon="list"
[ConfigPath]="cp"
[hidden]="!tmpl.HasAvailableSettings"></app-settings-template>
[hidden]="!tmpl.HasAvailableSettings">
<ng-container
*ngIf="cp=='Indexing'">
<br/>
<hr class="mt-2"/>
<app-settings-gallery-statistic></app-settings-gallery-statistic>
</ng-container>
</app-settings-template>
<!-- <app-settings-template #setting #server
icon="list"
[ConfigPath]="'Server'"

View File

@ -1,42 +0,0 @@
<form #settingsForm="ngForm" class="form-horizontal">
<div class="card mb-4">
<h5 class="card-header">
<span class="oi oi-{{icon}}"></span> {{Name}}
<div class="switch-wrapper">
<bSwitch
class="switch"
name="enabled"
switch-on-color="success"
[switch-inverse]="true"
switch-off-text="Disabled"
switch-on-text="Enabled"
i18n-switch-off-text
i18n-switch-on-text
[switch-disabled]="inProgress"
[switch-handle-width]="100"
[switch-label-width]="20"
[(ngModel)]="states.enabled.value">
</bSwitch>
</div>
</h5>
<div class="card-body">
<div [hidden]="!error" class="alert alert-danger" role="alert"><strong>Error: </strong>{{error}}</div>
<ng-container i18n>Shows albums tab in the top bar and enables creating saved searches.</ng-container>
<br/>
<ng-container i18n>Note: custom albums are not supported, see:</ng-container>&nbsp;
<a href="https://github.com/bpatrik/pigallery2/issues/301">#301</a>
<button class="btn btn-success float-end"
[disabled]="!settingsForm.form.valid || !changed || inProgress"
(click)="save()" i18n>Save
</button>
<button class="btn btn-secondary float-end"
[disabled]=" !changed || inProgress"
(click)="reset()" i18n>Reset
</button>
</div>
</div>
</form>

View File

@ -1,41 +0,0 @@
import {Component} from '@angular/core';
import {AlbumsSettingsService} from './albums.settings.service';
import {SettingsComponentDirective} from '../_abstract/abstract.settings.component';
import {AuthenticationService} from '../../../model/network/authentication.service';
import {NavigationService} from '../../../model/navigation.service';
import {NotificationService} from '../../../model/notification.service';
import {ClientAlbumConfig} from '../../../../../common/config/public/ClientConfig';
import {SettingsService} from '../settings.service';
@Component({
selector: 'app-settings-albums',
templateUrl: './albums.settings.component.html',
styleUrls: [
'./albums.settings.component.css',
'../_abstract/abstract.settings.component.css',
],
providers: [AlbumsSettingsService],
})
export class AlbumsSettingsComponent extends SettingsComponentDirective<ClientAlbumConfig> {
constructor(
authService: AuthenticationService,
navigation: NavigationService,
settingsService: AlbumsSettingsService,
notification: NotificationService,
globalSettingsService: SettingsService
) {
super(
$localize`Albums`,
'grid-two-up',
authService,
navigation,
settingsService,
notification,
globalSettingsService,
(s) => s.Album
);
}
}

View File

@ -1,27 +0,0 @@
import { Injectable } from '@angular/core';
import { NetworkService } from '../../../model/network/network.service';
import { SettingsService } from '../settings.service';
import { AbstractSettingsService } from '../_abstract/abstract.settings.service';
import { ClientAlbumConfig } from '../../../../../common/config/public/ClientConfig';
@Injectable()
export class AlbumsSettingsService extends AbstractSettingsService<ClientAlbumConfig> {
constructor(
private networkService: NetworkService,
settingsService: SettingsService
) {
super(settingsService);
}
public isSupported(): boolean {
return this.settingsService.settings.value.Map.enabled === true;
}
hasAvailableSettings(): boolean {
return false;
}
public updateSettings(settings: ClientAlbumConfig): Promise<void> {
return this.networkService.putJson('/settings/albums', { settings });
}
}

View File

@ -1,86 +0,0 @@
<form #settingsForm="ngForm" class="form-horizontal">
<div class="card mb-4">
<h5 class="card-header">
<span class="oi oi-{{icon}}"></span> {{Name}}
</h5>
<div class="card-body">
<div [hidden]="!error" class="alert alert-danger" role="alert"><strong>Error: </strong>{{error}}</div>
<app-settings-entry
name="Type"
[optionMap]="dbTypesMap"
[ngModel]="states.type"
i18n-name
[required]="true">
<small *ngIf="states.type.value == DatabaseType.mysql"
class="form-text text-muted" i18n>Install manually mysql node module to use mysql (npm install mysql)
</small>
</app-settings-entry>
<app-settings-entry
name="Database folder"
description="All file-based data will be stored here (sqlite database, user database in case of memory db, job history data)"
[ngModel]="states.dbFolder"
i18n-name i18n-description
[dockerWarning]="(settingsService.Settings | async).Environment.isDocker"
[required]="true">
</app-settings-entry>
<ng-container *ngIf="states.type.value == DatabaseType.mysql">
<app-settings-entry
name="Host"
[ngModel]="states.mysql.host"
i18n-name
[required]="true">
</app-settings-entry>
<app-settings-entry
name="Port"
[ngModel]="states.mysql.port"
i18n-name
[required]="true">
</app-settings-entry>
<app-settings-entry
name="Database"
[ngModel]="states.mysql.database"
i18n-name
[required]="true">
</app-settings-entry>
<app-settings-entry
name="Username"
[ngModel]="states.mysql.username"
placeholder="username"
i18n-name
[required]="true">
</app-settings-entry>
<app-settings-entry
name="Password"
[ngModel]="states.mysql.password"
placeholder="password"
i18n-name
[required]="true">
</app-settings-entry>
</ng-container>
<button class="btn btn-success float-end"
[disabled]="!settingsForm.form.valid || !changed || inProgress"
(click)="save()" i18n>Save
</button>
<button class="btn btn-secondary float-end"
[disabled]=" !changed || inProgress"
(click)="reset()" i18n>Reset
</button>
</div>
</div>
</form>

View File

@ -1,63 +0,0 @@
import {Component, OnInit} from '@angular/core';
import {AuthenticationService} from '../../../model/network/authentication.service';
import {Utils} from '../../../../../common/Utils';
import {NotificationService} from '../../../model/notification.service';
import {NavigationService} from '../../../model/navigation.service';
import {SettingsComponentDirective} from '../_abstract/abstract.settings.component';
import {DatabaseSettingsService} from './database.settings.service';
import {
DatabaseType,
ServerDataBaseConfig,
} from '../../../../../common/config/private/PrivateConfig';
import {SettingsService} from '../settings.service';
@Component({
selector: 'app-settings-database',
templateUrl: './database.settings.component.html',
styleUrls: [
'./database.settings.component.css',
'../_abstract/abstract.settings.component.css',
],
providers: [DatabaseSettingsService],
})
export class DatabaseSettingsComponent
extends SettingsComponentDirective<ServerDataBaseConfig>
implements OnInit {
public types = Utils.enumToArray(DatabaseType);
public DatabaseType = DatabaseType;
constructor(
authService: AuthenticationService,
navigation: NavigationService,
settingsService: DatabaseSettingsService,
notification: NotificationService,
globalSettingsService: SettingsService
) {
super(
$localize`Database`,
'list',
authService,
navigation,
settingsService,
notification,
globalSettingsService,
(s) => s.Database
);
}
ngOnInit(): void {
super.ngOnInit();
}
dbTypesMap = (v: { key: number; value: string }) => {
if (v.key === DatabaseType.sqlite) {
v.value += ' ' + $localize`(recommended)`;
} else if (v.value === DatabaseType[DatabaseType.memory]) {
v.value += ' ' + $localize`(deprecated, will be removed)`;
}
return v;
};
}

View File

@ -1,19 +0,0 @@
import { Injectable } from '@angular/core';
import { NetworkService } from '../../../model/network/network.service';
import { AbstractSettingsService } from '../_abstract/abstract.settings.service';
import { SettingsService } from '../settings.service';
import { ServerDataBaseConfig } from '../../../../../common/config/private/PrivateConfig';
@Injectable()
export class DatabaseSettingsService extends AbstractSettingsService<ServerDataBaseConfig> {
constructor(
private networkService: NetworkService,
settingsService: SettingsService
) {
super(settingsService);
}
public updateSettings(settings: ServerDataBaseConfig): Promise<void> {
return this.networkService.putJson('/settings/database', { settings });
}
}

View File

@ -1,3 +0,0 @@
.panel-info {
text-align: center;
}

View File

@ -1,60 +0,0 @@
<form #settingsForm="ngForm">
<div class="card mb-4"
[ngClass]="states.enabled.value && !settingsService.isSupported()?'panel-warning':''">
<h5 class="card-header">
<span class="oi oi-{{icon}}"></span> {{Name}}
<div class="switch-wrapper"
[class.changed-settings]="states.enabled.value != states.enabled.default">
<bSwitch
class="switch"
name="enabled"
switch-on-color="success"
[switch-inverse]="true"
switch-off-text="Disabled"
switch-on-text="Enabled"
i18n-switch-off-text
i18n-switch-on-text
[switch-disabled]="inProgress || (!states.enabled.value && !settingsService.isSupported())"
[switch-handle-width]="100"
[switch-label-width]="20"
[(ngModel)]="states.enabled.value">
</bSwitch>
</div>
</h5>
<div class="card-body">
<div [hidden]="!error" class="alert alert-danger" role="alert"><strong>Error: </strong>{{error}}</div>
<ng-container *ngIf="states.enabled.value || settingsService.isSupported()">
<app-settings-entry
name="Override keywords"
[ngModel]="states.keywordsToPersons"
description="If a photo has the same face (person) name and keyword, the app removes the duplicate, keeping the face only."
i18n-name i18n-description>
</app-settings-entry>
<app-settings-entry
name="Face starring right"
[ngModel]="states.writeAccessMinRole"
description="Required minimum right to star (favourite) a face."
i18n-name i18n-description>
</app-settings-entry>
</ng-container>
<div class="panel-info" *ngIf="(!states.enabled.value && !settingsService.isSupported())" i18n>
Faces are not supported with these settings.
</div>
<button class="btn btn-success float-end"
[disabled]="!settingsForm.form.valid || !changed || inProgress"
(click)="save()" i18n>Save
</button>
<button class="btn btn-secondary float-end"
[disabled]=" !changed || inProgress"
(click)="reset()" i18n>Reset
</button>
</div>
</div>
</form>

View File

@ -1,48 +0,0 @@
import {Component} from '@angular/core';
import {SettingsComponentDirective} from '../_abstract/abstract.settings.component';
import {AuthenticationService} from '../../../model/network/authentication.service';
import {NavigationService} from '../../../model/navigation.service';
import {NotificationService} from '../../../model/notification.service';
import {FacesSettingsService} from './faces.settings.service';
import {Utils} from '../../../../../common/Utils';
import {UserRoles} from '../../../../../common/entities/UserDTO';
import {ClientFacesConfig} from '../../../../../common/config/public/ClientConfig';
import {SettingsService} from '../settings.service';
@Component({
selector: 'app-settings-faces',
templateUrl: './faces.settings.component.html',
styleUrls: [
'./faces.settings.component.css',
'../_abstract/abstract.settings.component.css',
],
providers: [FacesSettingsService],
})
export class FacesSettingsComponent extends SettingsComponentDirective<ClientFacesConfig> {
public readonly userRoles = Utils.enumToArray(UserRoles)
.filter((r) => r.key !== UserRoles.LimitedGuest)
.filter((r) => r.key <= this.authService.user.value.role)
.sort((a, b) => a.key - b.key);
constructor(
authService: AuthenticationService,
navigation: NavigationService,
settingsService: FacesSettingsService,
notification: NotificationService,
globalSettingsService: SettingsService
) {
super(
$localize`Faces`,
'people',
authService,
navigation,
settingsService,
notification,
globalSettingsService,
(s) => s.Faces
);
}
}

View File

@ -1,32 +0,0 @@
import { Injectable } from '@angular/core';
import { NetworkService } from '../../../model/network/network.service';
import { SettingsService } from '../settings.service';
import { AbstractSettingsService } from '../_abstract/abstract.settings.service';
import { ClientFacesConfig } from '../../../../../common/config/public/ClientConfig';
import { DatabaseType } from '../../../../../common/config/private/PrivateConfig';
@Injectable()
export class FacesSettingsService extends AbstractSettingsService<ClientFacesConfig> {
constructor(
private networkService: NetworkService,
settingsService: SettingsService
) {
super(settingsService);
}
hasAvailableSettings(): boolean {
return false;
}
public isSupported(): boolean {
return (
this.settingsService.settings.value.Database.type !==
DatabaseType.memory &&
this.settingsService.settings.value.Search.enabled === true
);
}
public updateSettings(settings: ClientFacesConfig): Promise<void> {
return this.networkService.putJson('/settings/faces', { settings });
}
}

View File

@ -0,0 +1,26 @@
<div class="row statics">
<div class="col-md-2 col-12" i18n>
Statistic:
</div>
<div class="col-md-2 col-6">
<span class="oi oi-folder" title="Folders" i18n-title aria-hidden="true"> </span>
{{settingsService.statistic.value ? settingsService.statistic.value.directories : '...'}}
</div>
<div class="col-md-2 col-6">
<span class="oi oi-camera-slr" title="Photos" i18n-title aria-hidden="true"> </span>
{{settingsService.statistic.value ? settingsService.statistic.value.photos : '...'}}
</div>
<div class="col-md-2 col-6">
<span class="oi oi-video" title="Videos" i18n-title aria-hidden="true"> </span>
{{settingsService.statistic.value ? settingsService.statistic.value.videos : '...'}}
</div>
<div class="col-md-2 col-6">
<span class="oi oi-people" title="Persons" i18n-title aria-hidden="true"> </span>
{{settingsService.statistic.value ? settingsService.statistic.value.persons : '...'}}
</div>
<div class="col-md-2 col-6">
<span class="oi oi-pie-chart" title="Size" i18n-title aria-hidden="true"> </span>
{{settingsService.statistic.value ? (settingsService.statistic.value.diskUsage | fileSize) : '...'}}
</div>
</div>

View File

@ -0,0 +1,19 @@
import {Component, OnInit} from '@angular/core';
import {SettingsService} from '../settings.service';
@Component({
selector: 'app-settings-gallery-statistic',
templateUrl: './gallery-statistic.component.html',
styleUrls: ['./gallery-statistic.component.css']
})
export class GalleryStatisticComponent implements OnInit {
constructor(
public settingsService: SettingsService
) {
}
ngOnInit(): void {
}
}

View File

@ -1,12 +0,0 @@
.buttons-row {
margin-top: 10px;
margin-bottom: 20px;
}
.statics span.oi {
margin-right: 3px;
}
.progress-details {
font-weight: bold;
}

View File

@ -1,131 +0,0 @@
<form #settingsForm="ngForm" class="form-horizontal">
<div class="card mb-4">
<h5 class="card-header">
<span class="oi oi-{{icon}}"></span> {{Name}}
</h5>
<div class="card-body">
<div [hidden]="!error" class="alert alert-danger" role="alert"><strong>Error: </strong>{{error}}</div>
<ng-container *ngIf="configPriority>0">
<app-settings-entry
name="Index cache timeout [ms]"
description="If there was no indexing in this time, it reindexes. (skipped if indexes are in DB and sensitivity is low)"
i18n-description i18n-name
[ngModel]="states.cachedFolderTimeout"
[required]="true">
</app-settings-entry>
<app-settings-entry
name="Folder reindexing sensitivity"
[ngModel]="states.reIndexingSensitivity"
description="Set the reindexing sensitivity. High value check the folders for change more often."
i18n-description i18n-name
[required]="true">
</app-settings-entry>
<app-settings-entry
name="Exclude Folder List"
i18n-name
placeholder="/media/images/family/private;private;family/private"
[ngModel]="states.excludeFolderList">
<small class="form-text text-muted">
<ng-container i18n>Folders to exclude from indexing</ng-container>
<br/>
<ng-container
i18n>';' separated strings. If an entry starts with '/' it is treated as an absolute path. If it doesn't
start with '/' but contains a '/', the path is relative to the image directory. If it doesn't contain a
'/', any folder with this name will be excluded.
</ng-container>
</small>
</app-settings-entry>
<app-settings-entry
name="Exclude File List"
i18n-name
placeholder=".ignore;.pg2ignore"
[ngModel]="states.excludeFileList">
<small class="form-text text-muted">
<ng-container i18n>Files that mark a folder to be excluded from indexing</ng-container>
<br/>
<ng-container
i18n>';' separated strings. Any folder that contains a file with this name will be excluded from
indexing.
</ng-container>
</small>
</app-settings-entry>
<button class="btn btn-success float-end"
[disabled]="!settingsForm.form.valid || !changed || inProgress"
(click)="save()" i18n>Save
</button>
<button class="btn btn-secondary float-end"
[disabled]=" !changed || inProgress"
(click)="reset()" i18n>Reset
</button>
<br/>
<hr/>
</ng-container>
<div class="alert alert-secondary" role="alert">
<ng-container i18n>If you add a new folder to your gallery, the site indexes it automatically.</ng-container>&nbsp;
<ng-container i18n>If you would like to trigger indexing manually, click index button.</ng-container>
<br/>
(
<ng-container i18n>Note: search only works among the indexed directories</ng-container>
)
</div>
<app-settings-job-progress [progress]="Progress"></app-settings-job-progress>
<app-settings-job-button #indexingButton
[soloRun]="true"
(jobError)="error=$event"
[allowParallelRun]="false"
[jobName]="indexingJobName"
[config]="Config"></app-settings-job-button>
<app-settings-job-button class="ms-md-2 mt-2 mt-md-0"
[danger]="true"
[soloRun]="true"
(jobError)="error=$event"
[allowParallelRun]="false"
[disabled]="indexingButton.Running"
[jobName]="resetJobName"></app-settings-job-button>
<hr/>
<div class="row statics">
<div class="col-md-2 col-12" i18n>
Statistic:
</div>
<div class="col-md-2 col-6">
<span class="oi oi-folder" title="Folders" i18n-title aria-hidden="true"> </span>
{{settingsService.statistic.value ? settingsService.statistic.value.directories : '...'}}
</div>
<div class="col-md-2 col-6">
<span class="oi oi-camera-slr" title="Photos" i18n-title aria-hidden="true"> </span>
{{settingsService.statistic.value ? settingsService.statistic.value.photos : '...'}}
</div>
<div class="col-md-2 col-6">
<span class="oi oi-video" title="Videos" i18n-title aria-hidden="true"> </span>
{{settingsService.statistic.value ? settingsService.statistic.value.videos : '...'}}
</div>
<div class="col-md-2 col-6">
<span class="oi oi-people" title="Persons" i18n-title aria-hidden="true"> </span>
{{settingsService.statistic.value ? settingsService.statistic.value.persons : '...'}}
</div>
<div class="col-md-2 col-6">
<span class="oi oi-pie-chart" title="Size" i18n-title aria-hidden="true"> </span>
{{settingsService.statistic.value ? (settingsService.statistic.value.diskUsage | fileSize) : '...'}}
</div>
</div>
</div>
</div>
</form>

View File

@ -1,162 +0,0 @@
import {Component, OnDestroy, OnInit} from '@angular/core';
import {IndexingSettingsService} from './indexing.settings.service';
import {AuthenticationService} from '../../../model/network/authentication.service';
import {NavigationService} from '../../../model/navigation.service';
import {NotificationService} from '../../../model/notification.service';
import {ErrorDTO} from '../../../../../common/entities/Error';
import {SettingsComponentDirective} from '../_abstract/abstract.settings.component';
import {Utils} from '../../../../../common/Utils';
import {ScheduledJobsService} from '../scheduled-jobs.service';
import {
DefaultsJobs,
JobDTOUtils,
} from '../../../../../common/entities/job/JobDTO';
import {
JobProgressDTO,
JobProgressStates,
} from '../../../../../common/entities/job/JobProgressDTO';
import {
ReIndexingSensitivity,
ServerIndexingConfig,
} from '../../../../../common/config/private/PrivateConfig';
import {SettingsService} from '../settings.service';
@Component({
selector: 'app-settings-indexing',
templateUrl: './indexing.settings.component.html',
styleUrls: [
'./indexing.settings.component.css',
'../_abstract/abstract.settings.component.css',
],
providers: [IndexingSettingsService],
})
export class IndexingSettingsComponent
extends SettingsComponentDirective<
ServerIndexingConfig,
IndexingSettingsService
>
implements OnInit, OnDestroy {
types: { key: number; value: string }[] = [];
JobProgressStates = JobProgressStates;
readonly indexingJobName = DefaultsJobs[DefaultsJobs.Indexing];
readonly resetJobName = DefaultsJobs[DefaultsJobs['Database Reset']];
constructor(
authService: AuthenticationService,
navigation: NavigationService,
settingsService: IndexingSettingsService,
public jobsService: ScheduledJobsService,
notification: NotificationService,
globalSettingsService: SettingsService
) {
super(
$localize`Folder indexing`,
'pie-chart',
authService,
navigation,
settingsService,
notification,
globalSettingsService,
(s) => s.Indexing
);
}
get Config(): any {
return {indexChangesOnly: true};
}
get Progress(): JobProgressDTO {
return this.jobsService.progress.value[
JobDTOUtils.getHashName(DefaultsJobs[DefaultsJobs.Indexing], this.Config)
];
}
ngOnDestroy(): void {
super.ngOnDestroy();
this.jobsService.unsubscribeFromProgress();
}
async ngOnInit(): Promise<void> {
super.ngOnInit();
this.jobsService.subscribeToProgress();
this.types = Utils.enumToArray(ReIndexingSensitivity);
this.types.forEach((v) => {
switch (v.value) {
case 'low':
v.value = $localize`low`;
break;
case 'medium':
v.value = $localize`medium`;
break;
case 'high':
v.value = $localize`high`;
break;
}
});
}
async index(): Promise<boolean> {
this.inProgress = true;
this.error = '';
try {
await this.jobsService.start(
DefaultsJobs[DefaultsJobs.Indexing],
this.Config
);
this.notification.info($localize`Folder indexing started`);
this.inProgress = false;
return true;
} catch (err) {
console.log(err);
if (err.message) {
this.error = (err as ErrorDTO).message;
}
}
this.inProgress = false;
return false;
}
async cancelIndexing(): Promise<boolean> {
this.inProgress = true;
this.error = '';
try {
await this.jobsService.stop(DefaultsJobs[DefaultsJobs.Indexing]);
this.notification.info($localize`Folder indexing interrupted`);
this.inProgress = false;
return true;
} catch (err) {
console.log(err);
if (err.message) {
this.error = (err as ErrorDTO).message;
}
}
this.inProgress = false;
return false;
}
async resetDatabase(): Promise<boolean> {
this.inProgress = true;
this.error = '';
try {
await this.jobsService.start(
DefaultsJobs[DefaultsJobs['Database Reset']]
);
this.notification.info($localize`Resetting database`);
this.inProgress = false;
return true;
} catch (err) {
console.log(err);
if (err.message) {
this.error = (err as ErrorDTO).message;
}
}
this.inProgress = false;
return false;
}
}

View File

@ -1,59 +0,0 @@
import { Injectable } from '@angular/core';
import { NetworkService } from '../../../model/network/network.service';
import { SettingsService } from '../settings.service';
import { AbstractSettingsService } from '../_abstract/abstract.settings.service';
import { BehaviorSubject } from 'rxjs';
import { StatisticDTO } from '../../../../../common/entities/settings/StatisticDTO';
import { ScheduledJobsService } from '../scheduled-jobs.service';
import { DefaultsJobs } from '../../../../../common/entities/job/JobDTO';
import { first } from 'rxjs/operators';
import {
DatabaseType,
ServerIndexingConfig,
} from '../../../../../common/config/private/PrivateConfig';
@Injectable()
export class IndexingSettingsService extends AbstractSettingsService<ServerIndexingConfig> {
public statistic: BehaviorSubject<StatisticDTO>;
constructor(
private networkService: NetworkService,
private jobsService: ScheduledJobsService,
settingsService: SettingsService
) {
super(settingsService);
this.statistic = new BehaviorSubject(null);
settingsService.settings.pipe(first()).subscribe(() => {
if (this.isSupported()) {
this.loadStatistic();
}
});
this.jobsService.onJobFinish.subscribe((jobName: string) => {
if (
jobName === DefaultsJobs[DefaultsJobs.Indexing] ||
jobName === DefaultsJobs[DefaultsJobs['Database Reset']]
) {
if (this.isSupported()) {
this.loadStatistic();
}
}
});
}
public updateSettings(settings: ServerIndexingConfig): Promise<void> {
return this.networkService.putJson('/settings/indexing', { settings });
}
public isSupported(): boolean {
return (
this.settingsService.settings.value.Database.type !==
DatabaseType.memory
);
}
async loadStatistic(): Promise<void> {
this.statistic.next(
await this.networkService.getJson<StatisticDTO>('/admin/statistic')
);
}
}

View File

@ -1,17 +0,0 @@
.card {
margin-bottom: 1rem;
}
.job-control-button {
margin-top: -5px;
margin-bottom: -5px;
}
.clickable {
cursor: pointer;
}
.separator {
width: 2px;
}

View File

@ -1,269 +0,0 @@
<form #settingsForm="ngForm" class="form-horizontal">
<div class="card mb-4">
<h5 class="card-header">
<span class="oi oi-{{icon}}"></span> {{Name}}
</h5>
<div class="card-body">
<div [hidden]="!error" class="alert alert-danger" role="alert"><strong>Error: </strong>{{error}}</div>
<div *ngFor="let schedule of sortedSchedules() as schedules; let i= index">
<div class="card bg-light {{shouldIdent(schedule,schedules[i-1])? 'ms-4' : ''}}">
<div class="card-header">
<div class="d-flex justify-content-between">
<div class="clickable"
(click)="showDetails[schedule.name]=!showDetails[schedule.name]">
<span class="oi oi-chevron-{{showDetails[schedule.name] ? 'bottom' : 'right'}}"></span>
{{schedule.name}} @<!--
-->
<ng-container [ngSwitch]="schedule.trigger.type">
<ng-container *ngSwitchCase="JobTriggerType.periodic">
<ng-container i18n>every</ng-container>
{{periods[$any(schedule.trigger).periodicity]}} {{atTimeLocal($any(schedule.trigger).atTime) | date:"HH:mm (z)"}}
</ng-container>
<ng-container
*ngSwitchCase="JobTriggerType.scheduled">{{$any(schedule.trigger).time | date:"medium"}}</ng-container>
<ng-container *ngSwitchCase="JobTriggerType.never" i18n>never</ng-container>
<ng-container *ngSwitchCase="JobTriggerType.after">
<ng-container i18n>after</ng-container>
: {{$any(schedule.trigger).afterScheduleName}}
</ng-container>
</ng-container>
</div>
<div>
<button class="btn btn-danger job-control-button ms-0" (click)="remove(schedule)"><span
class="oi oi-trash"></span>
</button>
<app-settings-job-button class="job-control-button ms-md-2 mt-2 mt-md-0"
(jobError)="error=$event"
[allowParallelRun]="schedule.allowParallelRun"
[jobName]="schedule.jobName" [config]="schedule.config"
[shortName]="true"></app-settings-job-button>
</div>
</div>
</div>
<div class="card-body" [hidden]="!showDetails[schedule.name]">
<div class="row">
<div class="col-md-12">
<div class="mb-1 row">
<label class="col-md-2 control-label" [for]="'jobName'+i" i18n>Job:</label>
<div class="col-md-4">
{{backendTextService.getJobName(schedule.jobName)}}
</div>
<div class="col-md-6">
<app-settings-job-button class="float-end"
[jobName]="schedule.jobName"
[allowParallelRun]="schedule.allowParallelRun"
(jobError)="error=$event"
[config]="schedule.config"></app-settings-job-button>
</div>
</div>
<div class="mb-1 row">
<label class="col-md-2 control-label" [for]="'repeatType'+i" i18n>Periodicity:</label>
<div class="col-md-10">
<select class="form-select" [(ngModel)]="schedule.trigger.type"
(ngModelChange)="jobTriggerTypeChanged($event,schedule)"
[name]="'repeatType'+i" required>
<option *ngFor="let jobTrigger of JobTriggerTypeMap"
[ngValue]="jobTrigger.key">{{jobTrigger.value}}
</option>
</select>
<small class="form-text text-muted"
i18n>Set the time to run the job.
</small>
</div>
</div>
<div class="mb-3 row" *ngIf="schedule.trigger.type == JobTriggerType.after">
<label class="col-md-2 control-label" [for]="'triggerAfter'+i" i18n>After:</label>
<div class="col-md-10">
<select class="form-select" [(ngModel)]="schedule.trigger.afterScheduleName"
[name]="'triggerAfter'+i" required>
<ng-container *ngFor="let sch of states.scheduled.value">
<option *ngIf="sch.name !== schedule.name"
[ngValue]="sch.name">{{sch.name}}
</option>
</ng-container>
</select>
<small class="form-text text-muted"
i18n>The job will run after that job finishes.
</small>
</div>
</div>
<div class="mb-3 row" *ngIf="schedule.trigger.type == JobTriggerType.scheduled">
<label class="col-md-2 control-label" [for]="'triggerTime'+i" i18n>At:</label>
<div class="col-md-10">
<app-timestamp-datepicker
[name]="'triggerTime'+i"
(timestampChange)="onOptionChange()"
[(timestamp)]="schedule.trigger.time"></app-timestamp-datepicker>
</div>
</div>
<div class="mb-3 row" *ngIf="schedule.trigger.type == JobTriggerType.periodic">
<label class="col-md-2 control-label" [for]="'periodicity'+i" i18n>At:</label>
<div class="col-md-10">
<select
class="form-select" [(ngModel)]="schedule.trigger.periodicity"
[name]="'periodicity' + i"
required>
<option *ngFor="let period of periods; let i = index"
[ngValue]="i">
<ng-container i18n>every</ng-container>
{{period}}
</option>
</select>
<app-timestamp-timepicker
[name]="'atTime'+i"
(timestampChange)="onOptionChange()"
[(timestamp)]="schedule.trigger.atTime"></app-timestamp-timepicker>
</div>
</div>
<div class="mb-3 row">
<label class="col-md-2 control-label" [for]="'allowParallelRun'+i" i18n>Allow parallel run</label>
<div class="col-md-10">
<bSwitch
class="switch"
[name]="'allowParallelRun'+'_'+i"
[id]="'allowParallelRun'+'_'+i"
switch-on-color="primary"
[switch-inverse]="true"
switch-off-text="Disabled"
switch-on-text="Enabled"
i18n-switch-off-text i18n-switch-on-text
[switch-handle-width]="100"
[switch-label-width]="20"
[(ngModel)]="schedule.allowParallelRun">
</bSwitch>
<small class="form-text text-muted"
i18n>Enables the job to start even if another job is already running.
</small>
</div>
</div>
</div>
</div>
<ng-container *ngIf="getConfigTemplate(schedule.jobName) ">
<hr/>
<div *ngFor="let configEntry of getConfigTemplate(schedule.jobName)">
<div class="mb-3 row">
<label class="col-md-2 control-label"
[for]="configEntry.id+'_'+i">{{backendTextService.get(configEntry.name)}}:</label>
<div class="col-md-10">
<ng-container [ngSwitch]="configEntry.type">
<ng-container *ngSwitchCase="'boolean'">
<bSwitch
class="switch"
[name]="configEntry.id+'_'+i"
[id]="configEntry.id+'_'+i"
switch-on-color="primary"
[switch-inverse]="true"
switch-off-text="Disabled"
switch-on-text="Enabled"
i18n-switch-off-text i18n-switch-on-text
[switch-handle-width]="100"
[switch-label-width]="20"
[(ngModel)]="schedule.config[configEntry.id]">
</bSwitch>
</ng-container>
<ng-container *ngSwitchCase="'string'">
<input type="text" class="form-control" [name]="configEntry.id+'_'+i"
[id]="configEntry.id+'_'+i"
[(ngModel)]="schedule.config[configEntry.id]" required>
</ng-container>
<ng-container *ngSwitchCase="'number'">
<input type="number" class="form-control" [name]="configEntry.id+'_'+i"
[id]="configEntry.id+'_'+i"
[(ngModel)]="schedule.config[configEntry.id]" required>
</ng-container>
<ng-container *ngSwitchCase="'number-array'">
<input type="text" class="form-control"
[name]="configEntry.id+'_'+i"
[id]="configEntry.id+'_'+i"
(ngModelChange)="setNumberArray(schedule.config,configEntry.id,$event)"
[ngModel]="getNumberArray(schedule.config,configEntry.id)" required>
</ng-container>
</ng-container>
<small class="form-text text-muted">
<ng-container *ngIf="configEntry.type == 'number-array'" i18n>';' separated integers.
</ng-container>
{{backendTextService.get(configEntry.description)}}
</small>
</div>
</div>
</div>
</ng-container>
</div>
<app-settings-job-progress
class="card-footer bg-transparent"
*ngIf="getProgress(schedule)"
[progress]="getProgress(schedule)">
</app-settings-job-progress>
</div>
</div>
<button class="btn btn-success float-end"
[disabled]="!settingsForm.form.valid || !changed || inProgress"
(click)="save()" i18n>Save
</button>
<button class="btn btn-secondary float-end"
[disabled]=" !changed || inProgress"
(click)="reset()" i18n>Reset
</button>
<button class="btn btn-primary float-end"
(click)="prepareNewJob()" i18n>+ Add Job
</button>
</div>
</div>
</form>
<!-- Modal -->
<div bsModal #jobModal="bs-modal" class="modal fade" id="jobModal" tabindex="-1" role="dialog"
aria-labelledby="jobModalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="jobModalLabel" i18n>Add new job</h5>
<button type="button" class="btn-close" (click)="jobModal.hide()" data-dismiss="modal" aria-label="Close">
</button>
</div>
<form #jobModalForm="ngForm">
<div class="modal-body">
<select class="form-select" (change)="jobTypeChanged(newSchedule)" [(ngModel)]="newSchedule.jobName"
name="newJobName" required>
<option *ngFor="let availableJob of settingsService.availableJobs | async"
[ngValue]="availableJob.Name">{{backendTextService.getJobName(availableJob.Name)}}
</option>
</select>
<small class="form-text text-muted"
i18n>Select a job to schedule.
</small>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" (click)="jobModal.hide()" i18n>Close</button>
<button type="button" class="btn btn-primary" data-dismiss="modal"
(click)="addNewJob()"
[disabled]="!jobModalForm.form.valid" i18n>Add Job
</button>
</div>
</form>
</div>
</div>
</div>

View File

@ -1,262 +0,0 @@
import {Component, OnChanges, OnDestroy, OnInit, ViewChild,} from '@angular/core';
import {JobsSettingsService} from './jobs.settings.service';
import {AuthenticationService} from '../../../model/network/authentication.service';
import {NavigationService} from '../../../model/navigation.service';
import {NotificationService} from '../../../model/notification.service';
import {SettingsComponentDirective} from '../_abstract/abstract.settings.component';
import {ScheduledJobsService} from '../scheduled-jobs.service';
import {
AfterJobTrigger,
JobScheduleDTO,
JobScheduleDTOUtils,
JobTriggerType,
NeverJobTrigger,
PeriodicJobTrigger,
ScheduledJobTrigger,
} from '../../../../../common/entities/job/JobScheduleDTO';
import {ConfigTemplateEntry} from '../../../../../common/entities/job/JobDTO';
import {ModalDirective} from 'ngx-bootstrap/modal';
import {JobProgressDTO, JobProgressStates,} from '../../../../../common/entities/job/JobProgressDTO';
import {BackendtextService} from '../../../model/backendtext.service';
import {ServerJobConfig} from '../../../../../common/config/private/PrivateConfig';
import {ConfigPriority} from '../../../../../common/config/public/ClientConfig';
import {SettingsService} from '../settings.service';
@Component({
selector: 'app-settings-jobs',
templateUrl: './jobs.settings.component.html',
styleUrls: [
'./jobs.settings.component.css',
'../_abstract/abstract.settings.component.css',
],
providers: [JobsSettingsService],
})
export class JobsSettingsComponent
extends SettingsComponentDirective<ServerJobConfig, JobsSettingsService>
implements OnInit, OnDestroy, OnChanges {
@ViewChild('jobModal', {static: false}) public jobModal: ModalDirective;
disableButtons = false;
JobTriggerTypeMap: { key: number; value: string }[];
JobTriggerType = JobTriggerType;
periods: string[] = [];
showDetails: { [key: string]: boolean } = {};
JobProgressStates = JobProgressStates;
newSchedule: JobScheduleDTO = {
name: '',
config: null,
jobName: '',
trigger: {
type: JobTriggerType.never,
},
allowParallelRun: false,
};
constructor(
authService: AuthenticationService,
navigation: NavigationService,
settingsService: JobsSettingsService,
public jobsService: ScheduledJobsService,
public backendTextService: BackendtextService,
notification: NotificationService,
globalSettingsService: SettingsService
) {
super(
$localize`Jobs`,
'project',
authService,
navigation,
settingsService,
notification,
globalSettingsService,
(s) => s.Jobs
);
this.hasAvailableSettings = this.configPriority > ConfigPriority.basic;
this.JobTriggerTypeMap = [
{key: JobTriggerType.after, value: $localize`after`},
{key: JobTriggerType.never, value: $localize`never`},
{key: JobTriggerType.periodic, value: $localize`periodic`},
{key: JobTriggerType.scheduled, value: $localize`scheduled`},
];
this.periods = [
$localize`Monday`, // 0
$localize`Tuesday`, // 1
$localize`Wednesday`, // 2
$localize`Thursday`,
$localize`Friday`,
$localize`Saturday`,
$localize`Sunday`,
$localize`day`,
]; // 7
}
atTimeLocal(atTime: number): Date {
const d = new Date();
d.setUTCHours(Math.floor(atTime / 60));
d.setUTCMinutes(Math.floor(atTime % 60));
return d;
}
getConfigTemplate(JobName: string): ConfigTemplateEntry[] {
const job = this.settingsService.availableJobs.value.find(
(t) => t.Name === JobName
);
if (job && job.ConfigTemplate && job.ConfigTemplate.length > 0) {
return job.ConfigTemplate;
}
return null;
}
ngOnInit(): void {
super.ngOnInit();
this.jobsService.subscribeToProgress();
this.settingsService.getAvailableJobs().catch(console.error);
}
ngOnDestroy(): void {
super.ngOnDestroy();
this.jobsService.unsubscribeFromProgress();
}
remove(schedule: JobScheduleDTO): void {
this.states.scheduled.value.splice(
this.states.scheduled.value.indexOf(schedule),
1
);
}
jobTypeChanged(schedule: JobScheduleDTO): void {
const job = this.settingsService.availableJobs.value.find(
(t) => t.Name === schedule.jobName
);
schedule.config = schedule.config || {};
if (job.ConfigTemplate) {
job.ConfigTemplate.forEach(
(ct) => (schedule.config[ct.id] = ct.defaultValue)
);
}
}
prepareNewJob(): void {
const jobName = this.settingsService.availableJobs.value[0].Name;
this.newSchedule = {
name: 'new job',
jobName,
config: {},
trigger: {
type: JobTriggerType.never,
},
allowParallelRun: false,
};
const job = this.settingsService.availableJobs.value.find(
(t) => t.Name === jobName
);
this.newSchedule.config = this.newSchedule.config || {};
if (job.ConfigTemplate) {
job.ConfigTemplate.forEach(
(ct) => (this.newSchedule.config[ct.id] = ct.defaultValue)
);
}
this.jobModal.show();
}
jobTriggerTypeChanged(
triggerType: JobTriggerType,
schedule: JobScheduleDTO
): void {
schedule.trigger = {type: triggerType} as NeverJobTrigger;
switch (triggerType) {
case JobTriggerType.scheduled:
(schedule.trigger as unknown as ScheduledJobTrigger).time = Date.now();
break;
case JobTriggerType.periodic:
(schedule.trigger as unknown as PeriodicJobTrigger).periodicity = null;
(schedule.trigger as unknown as PeriodicJobTrigger).atTime = null;
break;
}
}
setNumberArray(configElement: any, id: string, value: string): void {
value = value.replace(new RegExp(',', 'g'), ';');
value = value.replace(new RegExp(' ', 'g'), ';');
configElement[id] = value
.split(';')
.map((s: string) => parseInt(s, 10))
.filter((i: number) => !isNaN(i) && i > 0);
}
getNumberArray(configElement: any, id: string): string {
return configElement[id].join('; ');
}
public shouldIdent(curr: JobScheduleDTO, prev: JobScheduleDTO): boolean {
return (
curr &&
curr.trigger.type === JobTriggerType.after &&
prev &&
prev.name === curr.trigger.afterScheduleName
);
}
public sortedSchedules(): JobScheduleDTO[] {
return (this.states.scheduled.value as JobScheduleDTO[])
.slice()
.sort((a: JobScheduleDTO, b: JobScheduleDTO) => {
return (
this.getNextRunningDate(a, this.states.scheduled.value) -
this.getNextRunningDate(b, this.states.scheduled.value)
);
});
}
addNewJob(): void {
const jobName = this.newSchedule.jobName;
const count = this.states.scheduled.value.filter(
(s: JobScheduleDTO) => s.jobName === jobName
).length;
this.newSchedule.name =
count === 0
? jobName
: this.backendTextService.getJobName(jobName) + ' ' + (count + 1);
this.states.scheduled.value.push(this.newSchedule);
this.jobModal.hide();
}
getProgress(schedule: JobScheduleDTO): JobProgressDTO {
return this.jobsService.getProgress(schedule);
}
private getNextRunningDate(
sch: JobScheduleDTO,
list: JobScheduleDTO[],
depth = 0
): number {
if (depth > list.length) {
return 0;
}
if (sch.trigger.type === JobTriggerType.never) {
return (
list
.map((s) => s.name)
.sort()
.indexOf(sch.name) * -1
);
}
if (sch.trigger.type === JobTriggerType.after) {
const parent = list.find(
(s) => s.name === (sch.trigger as AfterJobTrigger).afterScheduleName
);
if (parent) {
return this.getNextRunningDate(parent, list, depth + 1) + 0.001;
}
}
const d = JobScheduleDTOUtils.getNextRunningDate(new Date(), sch);
return d !== null ? d.getTime() : 0;
}
}

View File

@ -1,38 +0,0 @@
import { Injectable } from '@angular/core';
import { NetworkService } from '../../../model/network/network.service';
import { SettingsService } from '../settings.service';
import { AbstractSettingsService } from '../_abstract/abstract.settings.service';
import { BehaviorSubject } from 'rxjs';
import { JobDTO } from '../../../../../common/entities/job/JobDTO';
import { ServerJobConfig } from '../../../../../common/config/private/PrivateConfig';
@Injectable()
export class JobsSettingsService extends AbstractSettingsService<ServerJobConfig> {
public availableJobs: BehaviorSubject<JobDTO[]>;
constructor(
private networkService: NetworkService,
settingsService: SettingsService
) {
super(settingsService);
this.availableJobs = new BehaviorSubject([]);
}
public updateSettings(settings: ServerJobConfig): Promise<void> {
return this.networkService.putJson('/settings/jobs', { settings });
}
hasAvailableSettings(): boolean {
return false;
}
public isSupported(): boolean {
return true;
}
public async getAvailableJobs(): Promise<void> {
this.availableJobs.next(
await this.networkService.getJson<JobDTO[]>('/admin/jobs/available')
);
}
}

View File

@ -1,9 +0,0 @@
.custom-layer-info {
margin-top: -2rem;
margin-bottom: 1rem;
}
.custom-layer-container {
margin-bottom: 1rem;
}

View File

@ -1,109 +0,0 @@
<form #settingsForm="ngForm" class="form-horizontal">
<div class="card mb-4">
<h5 class="card-header">
<span class="oi oi-{{icon}}"></span> {{Name}}
<div class="switch-wrapper">
<bSwitch
class="switch"
name="enabled"
switch-on-color="success"
[switch-inverse]="true"
switch-off-text="Disabled"
switch-on-text="Enabled"
i18n-switch-off-text
i18n-switch-on-text
[switch-disabled]="inProgress"
[switch-handle-width]="100"
[switch-label-width]="20"
[(ngModel)]="states.enabled.value">
</bSwitch>
</div>
</h5>
<div class="card-body">
<div [hidden]="!error" class="alert alert-danger" role="alert"><strong>Error: </strong>{{error}}</div>
<ng-container *ngIf="states.enabled.value">
<app-settings-entry
name="Use image markers"
description="Map will use thumbnail images as markers instead of the default pin."
i18n-name i18n-description=""
[ngModel]="states.useImageMarkers">
</app-settings-entry>
<app-settings-entry
name="Map provider"
i18n-name
[ngModel]="states.mapProvider"
[required]="true">
</app-settings-entry>
<div class="container custom-layer-container" *ngIf="states.mapProvider.value === MapProviders.Custom">
<table class="table table-hover">
<thead>
<tr>
<th i18n>Name</th>
<th i18n>Tile Url*</th>
<th></th>
</tr>
</thead>
<tr *ngFor="let layer of states.customLayers.value; let i = index">
<td><input type="text" class="form-control" placeholder="Street"
[(ngModel)]="layer.name"
[name]="'tileName-'+i" [id]="'tileName-'+i" required></td>
<td>
<input type="text" class="form-control" placeholder="http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
[(ngModel)]="layer.url"
[name]="'tileUrl-'+i" [id]="'tileUrl-'+i" required>
</td>
<td>
<button [disabled]="states.customLayers.value.length == 1" (click)="removeLayer(layer)"
[ngClass]="states.customLayers.value.length > 1? 'btn-danger':'btn-secondary'"
class="btn float-end">
<span class="oi oi-trash" aria-hidden="true" aria-label="Delete"></span>
</button>
</td>
</tr>
</table>
<div class="row justify-content-end">
<small class="form-text text-muted custom-layer-info">
<ng-container i18n>*The map module will use these urls to fetch the map tiles.</ng-container>
</small>
</div>
<div class="row justify-content-end">
<button class="btn btn-primary"
(click)="addNewLayer()" i18n>+ Add Layer
</button>
</div>
</div>
<app-settings-entry
*ngIf="states.mapProvider.value === MapProviders.Mapbox"
name="Mapbox access token"
i18n-name
placeholder="Mapbox access token"
[ngModel]="states.mapboxAccessToken"
[required]="true">
<small class="form-text text-muted">
<ng-container i18n>MapBox needs an access token to work, create one at</ng-container>
&nbsp;<a href="https://www.mapbox.com">https://www.mapbox.com</a>.
</small>
</app-settings-entry>
</ng-container>
<button class="btn btn-success float-end"
[disabled]="!settingsForm.form.valid || !changed || inProgress"
(click)="save()" i18n>Save
</button>
<button class="btn btn-secondary float-end"
[disabled]=" !changed || inProgress"
(click)="reset()" i18n>Reset
</button>
</div>
</div>
</form>

View File

@ -1,65 +0,0 @@
import {Component} from '@angular/core';
import {MapSettingsService} from './map.settings.service';
import {SettingsComponentDirective} from '../_abstract/abstract.settings.component';
import {AuthenticationService} from '../../../model/network/authentication.service';
import {NavigationService} from '../../../model/navigation.service';
import {NotificationService} from '../../../model/notification.service';
import {Utils} from '../../../../../common/Utils';
import {
ClientMapConfig,
MapLayers,
MapProviders,
} from '../../../../../common/config/public/ClientConfig';
import {SettingsService} from '../settings.service';
@Component({
selector: 'app-settings-map',
templateUrl: './map.settings.component.html',
styleUrls: [
'./map.settings.component.css',
'../_abstract/abstract.settings.component.css',
],
providers: [MapSettingsService],
})
export class MapSettingsComponent extends SettingsComponentDirective<ClientMapConfig> {
public mapProviders: { key: number; value: string }[] = [];
public MapProviders = MapProviders;
constructor(
authService: AuthenticationService,
navigation: NavigationService,
settingsService: MapSettingsService,
notification: NotificationService,
globalSettingsService: SettingsService
) {
super(
$localize`Map`,
'map-marker',
authService,
navigation,
settingsService,
notification,
globalSettingsService,
(s) => s.Map
);
this.mapProviders = Utils.enumToArray(MapProviders);
}
addNewLayer(): void {
this.states.customLayers.value.push({
name: 'Layer-' + this.states.customLayers.value.length,
url: '',
});
}
removeLayer(layer: MapLayers): void {
this.states.customLayers.value.splice(
this.states.customLayers.value.indexOf(layer),
1
);
}
}

View File

@ -1,23 +0,0 @@
import { Injectable } from '@angular/core';
import { NetworkService } from '../../../model/network/network.service';
import { SettingsService } from '../settings.service';
import { AbstractSettingsService } from '../_abstract/abstract.settings.service';
import { ClientMapConfig } from '../../../../../common/config/public/ClientConfig';
@Injectable()
export class MapSettingsService extends AbstractSettingsService<ClientMapConfig> {
constructor(
private networkService: NetworkService,
settingsService: SettingsService
) {
super(settingsService);
}
hasAvailableSettings(): boolean {
return false;
}
public updateSettings(settings: ClientMapConfig): Promise<void> {
return this.networkService.putJson('/settings/map', { settings });
}
}

View File

@ -1,80 +0,0 @@
<form #settingsForm="ngForm" class="form-horizontal">
<div class="card mb-4">
<h5 class="card-header">
<span class="oi oi-{{icon}}"></span> {{Name}}
</h5>
<div class="card-body">
<div [hidden]="!error" class="alert alert-danger" role="alert"><strong>Error: </strong>{{error}}</div>
<app-settings-entry
name="Markdown files"
description="Reads *.md files in a directory and shows the next to the map."
i18n-description i18n-name
[ngModel]="states.markdown">
</app-settings-entry>
<app-settings-entry
name="*.pg2conf files"
description="Reads *.pg2conf files (You can use it for custom sorting and save search (albums))."
i18n-description i18n-name
[ngModel]="states.pg2conf">
</app-settings-entry>
<hr/>
<app-settings-entry
name="*.gpx files"
description="Reads *.gpx files and renders them on the map."
i18n-description i18n-name
[disabled]="!(settingsService.Settings | async).Map.enabled"
[ngModel]="states.gpx">
</app-settings-entry>
<app-settings-entry
name="*.gpx compression"
description="Enables *.gpx file compression."
link="https://github.com/bpatrik/pigallery2/issues/504"
linkText="See 504."
i18n-description i18n-name
[disabled]="!(settingsService.Settings | async).Map.enabled || !states.gpx.value"
[ngModel]="states.GPXCompressing.enabled">
</app-settings-entry>
<app-settings-entry
name="OnTheFly *.gpx compression"
description="Enables on the fly *.gpx compression."
i18n-description i18n-name
[disabled]="!(settingsService.Settings | async).Map.enabled || !states.GPXCompressing.enabled.value || !states.gpx.value"
[ngModel]="states.server.GPXCompressing.onTheFly">
</app-settings-entry>
<button class="btn btn-success float-end"
[disabled]="!settingsForm.form.valid || !changed || inProgress"
(click)="save()" i18n>Save
</button>
<button class="btn btn-secondary float-end"
[disabled]=" !changed || inProgress"
(click)="reset()" i18n>Reset
</button>
<div [hidden]="!states.GPXCompressing.enabled.value || !states.gpx.value">
<app-settings-job-button class="mt-2 mt-md-0 float-left"
[soloRun]="true"
(jobError)="error=$event"
[allowParallelRun]="false"
[jobName]="jobName"></app-settings-job-button>
<ng-container *ngIf="Progress != null">
<br/>
<hr/>
<app-settings-job-progress [progress]="Progress"></app-settings-job-progress>
</ng-container>
</div>
</div>
</div>
</form>

View File

@ -1,56 +0,0 @@
import {Component} from '@angular/core';
import {MetaFileSettingsService} from './metafile.settings.service';
import {SettingsComponentDirective} from '../_abstract/abstract.settings.component';
import {AuthenticationService} from '../../../model/network/authentication.service';
import {NavigationService} from '../../../model/navigation.service';
import {NotificationService} from '../../../model/notification.service';
import {ClientMetaFileConfig, ClientPhotoConfig} from '../../../../../common/config/public/ClientConfig';
import {ServerMetaFileConfig, ServerPhotoConfig} from '../../../../../common/config/private/PrivateConfig';
import {DefaultsJobs, JobDTOUtils} from '../../../../../common/entities/job/JobDTO';
import {JobProgressDTO, JobProgressStates} from '../../../../../common/entities/job/JobProgressDTO';
import {ScheduledJobsService} from '../scheduled-jobs.service';
import {SettingsService} from '../settings.service';
@Component({
selector: 'app-settings-meta-file',
templateUrl: './metafile.settings.component.html',
styleUrls: [
'./metafile.settings.component.css',
'../_abstract/abstract.settings.component.css',
],
providers: [MetaFileSettingsService],
})
export class MetaFileSettingsComponent extends SettingsComponentDirective<ServerMetaFileConfig> {
constructor(
authService: AuthenticationService,
navigation: NavigationService,
settingsService: MetaFileSettingsService,
notification: NotificationService,
public jobsService: ScheduledJobsService,
globalSettingsService: SettingsService
) {
super(
$localize`Meta file`,
'file',
authService,
navigation,
settingsService,
notification,
globalSettingsService,
(s) => (s.MetaFile)
);
}
readonly jobName = DefaultsJobs[DefaultsJobs['GPX Compression']];
get Progress(): JobProgressDTO {
return this.jobsService.progress.value[
JobDTOUtils.getHashName(DefaultsJobs[DefaultsJobs['GPX Compression']])
];
}
}

View File

@ -1,28 +0,0 @@
import {Injectable} from '@angular/core';
import {NetworkService} from '../../../model/network/network.service';
import {SettingsService} from '../settings.service';
import {AbstractSettingsService} from '../_abstract/abstract.settings.service';
import {ClientMetaFileConfig} from '../../../../../common/config/public/ClientConfig';
import {ServerMetaFileConfig} from '../../../../../common/config/private/PrivateConfig';
@Injectable()
export class MetaFileSettingsService extends AbstractSettingsService<ServerMetaFileConfig> {
constructor(
private networkService: NetworkService,
settingsService: SettingsService
) {
super(settingsService);
}
public isSupported(): boolean {
return this.settingsService.settings.value.Map.enabled === true;
}
hasAvailableSettings(): boolean {
return false;
}
public updateSettings(settings: ServerMetaFileConfig): Promise<void> {
return this.networkService.putJson('/settings/metafile', {settings});
}
}

View File

@ -1,137 +0,0 @@
<form #settingsForm="ngForm" class="form-horizontal">
<div class="card mb-4">
<h5 class="card-header">
<span class="oi oi-{{icon}}"></span> {{Name}}
</h5>
<div class="card-body">
<div [hidden]="!error" class="alert alert-danger" role="alert"><strong i18n>Error: </strong>{{error}}</div>
<p class="title" i18n>Threads:</p>
<app-settings-entry
name="Threading"
description="Runs directory scanning in a different thread."
i18n-description i18n-name
[ngModel]="states.Server.enabled">
</app-settings-entry>
<app-settings-entry
name="Thumbnail threads"
description="Number of threads that are used to generate thumbnails. If auto, number of CPU cores -1 threads will be used."
i18n-description i18n-name
[ngModel]="states.Server.thumbnailThreads"
[options]="threads"
[required]="true">
</app-settings-entry>
<hr/>
<p class="title" i18n>Misc:</p>
<app-settings-entry
name="Scroll based thumbnail generation"
description="Those thumbnails get higher priority that are visible on the screen."
i18n-description i18n-name
[ngModel]="states.Client.enableOnScrollThumbnailPrioritising">
</app-settings-entry>
<app-settings-entry
name="Lazy image rendering"
description="Shows only the required amount of photos at once. Renders more if page bottom is reached."
i18n-description i18n-name
[ngModel]="states.Client.enableOnScrollRendering">
</app-settings-entry>
<app-settings-entry
name="Cache"
description="Caches directory contents and search results for better performance."
i18n-description i18n-name
[ngModel]="states.Client.enableCache">
</app-settings-entry>
<app-settings-entry
name="Caption first naming"
description="Show the caption (IPTC 120) tags from the EXIF data instead of the filenames."
i18n-description i18n-name
[ngModel]="states.Client.captionFirstNaming">
</app-settings-entry>
<app-settings-entry
name="Download Zip"
description="[Experimental: does not work for searches] Enable download zip of a directory contents"
i18n-description i18n-name
[ngModel]="states.Client.enableDownloadZip">
</app-settings-entry>
<app-settings-entry
name="Directory flattening"
description="[Experimental: won't work if the gallery multiple folders with the same path] Adds a button to flattens the file structure, by listing the content of all subdirectories."
link="https://github.com/bpatrik/pigallery2/issues/174"
linkText="See 174."
i18n-description i18n-name
[ngModel]="states.Client.enableDirectoryFlattening">
</app-settings-entry>
<hr/>
<p class="title" i18n>Navigation bar:</p>
<app-settings-entry
name="Show item count"
description="Show the number of items (photos) in the folder."
i18n-description i18n-name
[ngModel]="states.Client.NavBar.showItemCount"
[required]="true">
</app-settings-entry>
<app-settings-entry
name="Default photo sorting method"
i18n-name
[ngModel]="states.Client.defaultPhotoSortingMethod"
[optionMap]="sortingMap"
[required]="true">
</app-settings-entry>
<app-settings-entry
name="Default photo sorting method for search results"
i18n-name
[ngModel]="states.Client.defaultSearchSortingMethod"
link="https://github.com/bpatrik/pigallery2/issues/566"
linkText="See 566."
[optionMap]="sortingMap"
[required]="true">
</app-settings-entry>
<app-settings-entry
name="Sort directories by date"
description="If enabled, directories will be sorted by date, like photos, otherwise by name. Directory date is the last modification time of that directory not the creation date of the oldest photo."
i18n-description i18n-name
[ngModel]="states.Client.enableDirectorySortingByDate"
[required]="true">
</app-settings-entry>
<app-settings-entry
name="Custom HTML Head"
description="Injects the content of this between the <head></head> HTML tags of the app. (You can use it add analytics or custom code to the app)"
[ngModel]="states.Client.customHTMLHead"
i18n-name>
</app-settings-entry>
<button class="btn btn-success float-end"
[disabled]="!settingsForm.form.valid || !changed || inProgress"
(click)="save()" i18n>Save
</button>
<button class="btn btn-secondary float-end"
[disabled]=" !changed || inProgress"
(click)="reset()" i18n>Reset
</button>
</div>
</div>
</form>

View File

@ -1,76 +0,0 @@
import {Component, OnChanges} from '@angular/core';
import {SettingsComponentDirective} from '../_abstract/abstract.settings.component';
import {AuthenticationService} from '../../../model/network/authentication.service';
import {NavigationService} from '../../../model/navigation.service';
import {NotificationService} from '../../../model/notification.service';
import {OtherSettingsService} from './other.settings.service';
import {Utils} from '../../../../../common/Utils';
import {SortingMethods} from '../../../../../common/entities/SortingMethods';
import {StringifySortingMethod} from '../../../pipes/StringifySortingMethod';
import {ConfigPriority} from '../../../../../common/config/public/ClientConfig';
import {SettingsService} from '../settings.service';
@Component({
selector: 'app-settings-other',
templateUrl: './other.settings.component.html',
styleUrls: [
'./other.settings.component.css',
'../_abstract/abstract.settings.component.css',
],
providers: [OtherSettingsService],
})
export class OtherSettingsComponent
extends SettingsComponentDirective<any>
implements OnChanges {
types: { key: number; value: string }[] = [];
threads: { key: number; value: string }[] = [
{key: 0, value: 'auto'},
].concat(Utils.createRange(1, 100).map((v) => ({key: v, value: '' + v})));
sortingMap: any;
constructor(
authService: AuthenticationService,
navigation: NavigationService,
settingsService: OtherSettingsService,
notification: NotificationService,
private formatter: StringifySortingMethod,
globalSettingsService: SettingsService
) {
super(
$localize`Other`,
'target',
authService,
navigation,
settingsService,
notification,
globalSettingsService,
(s) => ({
Server: s.Gallery,
})
);
this.sortingMap = (v: { key: number; value: string }) => {
v.value = this.formatter.transform(v.key);
return v;
};
this.types = Utils.enumToArray(SortingMethods);
this.hasAvailableSettings = this.configPriority > ConfigPriority.basic;
}
ngOnChanges(): void {
this.hasAvailableSettings = this.configPriority > ConfigPriority.basic;
}
public async save(): Promise<boolean> {
const val = await super.save();
if (val === true) {
this.notification.info(
$localize`Restart the server to apply the new settings`,
$localize`Info`
);
}
return val;
}
}

View File

@ -1,18 +0,0 @@
import { Injectable } from '@angular/core';
import { NetworkService } from '../../../model/network/network.service';
import { AbstractSettingsService } from '../_abstract/abstract.settings.service';
import { SettingsService } from '../settings.service';
@Injectable()
export class OtherSettingsService extends AbstractSettingsService<any> {
constructor(
private networkService: NetworkService,
settingsService: SettingsService
) {
super(settingsService);
}
public updateSettings(settings: any): Promise<void> {
return this.networkService.putJson('/settings/other', { settings });
}
}

View File

@ -1,4 +0,0 @@
.buttons-row {
margin-top: 10px;
margin-bottom: 20px;
}

View File

@ -1,71 +0,0 @@
<form #settingsForm="ngForm" class="form-horizontal">
<div class="card mb-4">
<h5 class="card-header">
<span class="oi oi-{{icon}}"></span> {{Name}}
</h5>
<div class="card-body">
<div [hidden]="!error" class="alert alert-danger" role="alert"><strong>Error: </strong>{{error}}</div>
<p class="title" i18n>Photo converting:</p>
<app-settings-entry
name="Converting"
description="Downsizes photos for faster preview loading. (Zooming in to the photo loads the original)."
i18n-description i18n-name
[ngModel]="states.client.Converting.enabled">
</app-settings-entry>
<app-settings-entry
name="On the fly converting"
description="Converts photos on the fly, when they are requested."
i18n-description i18n-name
[ngModel]="states.server.Converting.onTheFly"
[disabled]="!states.client.Converting.enabled.value">
</app-settings-entry>
<app-settings-entry
name="Resolution"
description="The shorter edge of the converted photo will be scaled down to this, while keeping the aspect ratio."
i18n-description i18n-name
[ngModel]="states.server.Converting.resolution"
[options]="resolutions"
[disabled]="!states.client.Converting.enabled.value"
[required]="true">
</app-settings-entry>
<app-settings-entry
name="Load full resolution image on zoom"
description="Enables loading the full resolution image on zoom in the ligthbox (preview)."
i18n-description i18n-name
[ngModel]="states.client.loadFullImageOnZoom"
[disabled]="!states.client.Converting.enabled.value">
</app-settings-entry>
<button class="btn btn-success float-end"
[disabled]="!settingsForm.form.valid || !changed || inProgress"
(click)="save()" i18n>Save
</button>
<button class="btn btn-secondary float-end"
[disabled]=" !changed || inProgress"
(click)="reset()" i18n>Reset
</button>
<div [hidden]="!states.client.Converting.enabled.value">
<app-settings-job-button class="mt-2 mt-md-0 float-left"
[soloRun]="true"
(jobError)="error=$event"
[allowParallelRun]="false"
[jobName]="jobName"></app-settings-job-button>
<ng-container *ngIf="Progress != null">
<br/>
<hr/>
<app-settings-job-progress [progress]="Progress"></app-settings-job-progress>
</ng-container>
</div>
</div>
</div>
</form>

View File

@ -1,72 +0,0 @@
import {Component} from '@angular/core';
import {PhotoSettingsService} from './photo.settings.service';
import {SettingsComponentDirective} from '../_abstract/abstract.settings.component';
import {AuthenticationService} from '../../../model/network/authentication.service';
import {NavigationService} from '../../../model/navigation.service';
import {NotificationService} from '../../../model/notification.service';
import {ScheduledJobsService} from '../scheduled-jobs.service';
import {
DefaultsJobs,
JobDTOUtils,
} from '../../../../../common/entities/job/JobDTO';
import {
JobProgressDTO,
JobProgressStates,
} from '../../../../../common/entities/job/JobProgressDTO';
import {ServerPhotoConfig} from '../../../../../common/config/private/PrivateConfig';
import {ClientPhotoConfig} from '../../../../../common/config/public/ClientConfig';
import {SettingsService} from '../settings.service';
@Component({
selector: 'app-settings-photo',
templateUrl: './photo.settings.component.html',
styleUrls: [
'./photo.settings.component.css',
'../_abstract/abstract.settings.component.css',
],
providers: [PhotoSettingsService],
})
export class PhotoSettingsComponent extends SettingsComponentDirective<ServerPhotoConfig> {
readonly resolutionTypes = [720, 1080, 1440, 2160, 4320];
resolutions: { key: number; value: string }[] = [];
readonly jobName = DefaultsJobs[DefaultsJobs['Photo Converting']];
constructor(
authService: AuthenticationService,
navigation: NavigationService,
settingsService: PhotoSettingsService,
public jobsService: ScheduledJobsService,
notification: NotificationService,
globalSettingsService: SettingsService
) {
super(
$localize`Photo`,
'camera-slr',
authService,
navigation,
settingsService,
notification,
globalSettingsService,
(s) => s.Media.Photo
);
const currentRes =
settingsService.Settings.value.Media.Photo.Converting.resolution;
if (this.resolutionTypes.indexOf(currentRes) === -1) {
this.resolutionTypes.push(currentRes);
}
this.resolutions = this.resolutionTypes.map((e) => ({
key: e,
value: e + 'px',
}));
}
get Progress(): JobProgressDTO {
return this.jobsService.progress.value[
JobDTOUtils.getHashName(DefaultsJobs[DefaultsJobs['Photo Converting']])
];
}
}

View File

@ -1,20 +0,0 @@
import { Injectable } from '@angular/core';
import { NetworkService } from '../../../model/network/network.service';
import { SettingsService } from '../settings.service';
import { AbstractSettingsService } from '../_abstract/abstract.settings.service';
import { ServerPhotoConfig } from '../../../../../common/config/private/PrivateConfig';
import { ClientPhotoConfig } from '../../../../../common/config/public/ClientConfig';
@Injectable()
export class PhotoSettingsService extends AbstractSettingsService<ServerPhotoConfig> {
constructor(
private networkService: NetworkService,
settingsService: SettingsService
) {
super(settingsService);
}
public updateSettings(settings: ServerPhotoConfig): Promise<void> {
return this.networkService.putJson('/settings/photo', { settings });
}
}

View File

@ -1,62 +0,0 @@
<form #settingsForm="ngForm" class="form-horizontal">
<div class="card mb-4">
<h5 class="card-header">
<span class="oi oi-{{icon}}"></span> {{Name}}
</h5>
<div class="card-body">
<div [hidden]="!error" class="alert alert-danger" role="alert"><strong>Error: </strong>{{error}}</div>
<app-settings-entry
name="Preview Filter query"
description="Filters the sub-folders with this search query"
i18n-description i18n-name
[ngModel]="states.SearchQuery"
[typeOverride]="'SearchQuery'"
[required]="true">
</app-settings-entry>
<app-settings-entry
name="Preview Sorting"
description="If multiple preview is available sorts them by these methods and selects the first one."
i18n-description i18n-name
[ngModel]="states.Sorting"
[required]="true">
</app-settings-entry>
<button class="btn btn-success float-end"
[disabled]="!settingsForm.form.valid || !changed || inProgress"
(click)="save()" i18n>Save
</button>
<button class="btn btn-secondary float-end"
[disabled]=" !changed || inProgress"
(click)="reset()" i18n>Reset
</button>
<app-settings-job-button class="mt-2 mt-md-0 float-left"
#previewFillingButton
[soloRun]="true"
(jobError)="error=$event"
[jobName]="jobName"
[allowParallelRun]="false"
[config]="Config"></app-settings-job-button>
<app-settings-job-button class="ms-md-2 mt-2 mt-md-0"
[danger]="true"
[soloRun]="true"
(jobError)="error=$event"
[allowParallelRun]="false"
[disabled]="previewFillingButton.Running"
[jobName]="resetJobName"></app-settings-job-button>
<ng-container *ngIf="Progress != null">
<br/>
<hr/>
<app-settings-job-progress [progress]="Progress"></app-settings-job-progress>
</ng-container>
</div>
</div>
</form>

View File

@ -1,71 +0,0 @@
import {Component, OnInit} from '@angular/core';
import {SettingsComponentDirective} from '../_abstract/abstract.settings.component';
import {AuthenticationService} from '../../../model/network/authentication.service';
import {NavigationService} from '../../../model/navigation.service';
import {NotificationService} from '../../../model/notification.service';
import {
DefaultsJobs,
JobDTOUtils,
} from '../../../../../common/entities/job/JobDTO';
import {ScheduledJobsService} from '../scheduled-jobs.service';
import {
JobProgressDTO,
JobProgressStates,
} from '../../../../../common/entities/job/JobProgressDTO';
import {ServerPreviewConfig} from '../../../../../common/config/private/PrivateConfig';
import {PreviewSettingsService} from './preview.settings.service';
import {SettingsService} from '../settings.service';
@Component({
selector: 'app-settings-preview',
templateUrl: './preview.settings.component.html',
styleUrls: [
'./preview.settings.component.css',
'../_abstract/abstract.settings.component.css',
],
providers: [PreviewSettingsService],
})
export class PreviewSettingsComponent
extends SettingsComponentDirective<ServerPreviewConfig>
implements OnInit {
JobProgressStates = JobProgressStates;
readonly jobName = DefaultsJobs[DefaultsJobs['Preview Filling']];
readonly resetJobName = DefaultsJobs[DefaultsJobs['Preview Reset']];
constructor(
authService: AuthenticationService,
navigation: NavigationService,
settingsService: PreviewSettingsService,
notification: NotificationService,
public jobsService: ScheduledJobsService,
globalSettingsService: SettingsService
) {
super(
$localize`Preview`,
'image',
authService,
navigation,
settingsService,
notification,
globalSettingsService,
(s) => s.Preview
);
}
get Config(): unknown {
return {};
}
get Progress(): JobProgressDTO {
return this.jobsService.progress.value[
JobDTOUtils.getHashName(this.jobName, this.Config)
];
}
ngOnInit(): void {
super.ngOnInit();
}
}

View File

@ -1,23 +0,0 @@
import { Injectable } from '@angular/core';
import { NetworkService } from '../../../model/network/network.service';
import { AbstractSettingsService } from '../_abstract/abstract.settings.service';
import { SettingsService } from '../settings.service';
import { ServerPreviewConfig } from '../../../../../common/config/private/PrivateConfig';
@Injectable()
export class PreviewSettingsService extends AbstractSettingsService<ServerPreviewConfig> {
constructor(
private networkService: NetworkService,
settingsService: SettingsService
) {
super(settingsService);
}
hasAvailableSettings(): boolean {
return false;
}
public updateSettings(settings: ServerPreviewConfig): Promise<void> {
return this.networkService.putJson('/settings/preview', { settings });
}
}

View File

@ -1,56 +0,0 @@
<form #settingsForm="ngForm">
<div class="card mb-4"
[ngClass]="states.enabled.value && !settingsService.isSupported()?'panel-warning':''">
<h5 class="card-header">
<span class="oi oi-{{icon}}"></span> {{Name}}
<div class="switch-wrapper">
<bSwitch
class="switch"
name="enabled"
switch-on-color="success"
[switch-inverse]="true"
switch-off-text="Disabled"
switch-on-text="Enabled"
i18n-switch-off-text
i18n-switch-on-text
[switch-disabled]="inProgress || !settingsService.isSupported()"
[switch-handle-width]="100"
[switch-label-width]="20"
[(ngModel)]="states.enabled.value">
</bSwitch>
</div>
</h5>
<div class="card-body">
<div [hidden]="!error" class="alert alert-danger" role="alert"><strong>Error: </strong>{{error}}</div>
<ng-container *ngIf="states.enabled.value || settingsService.isSupported()">
<div class="alert alert-secondary" role="alert">
<ng-container i18n>
This feature enables you to generate 'random photo' urls.
That URL returns a photo random selected from your gallery.
You can use the url with 3rd party application like random changing desktop background.
</ng-container>
<br/>
<ng-container i18n>
Note: With the current implementation, random link also requires login. See:
</ng-container>
<a href="https://github.com/bpatrik/pigallery2/issues/392">#392</a>
</div>
</ng-container>
<div class="panel-info" *ngIf="(!states.enabled.value && !settingsService.isSupported())" i18n>
Random Photo is not supported with these settings
</div>
<button class="btn btn-success float-end"
[disabled]="!settingsForm.form.valid || !changed || inProgress"
(click)="save()" i18n>Save
</button>
<button class="btn btn-secondary float-end"
[disabled]=" !changed || inProgress"
(click)="reset()" i18n>Reset
</button>
</div>
</div>
</form>

View File

@ -1,41 +0,0 @@
import {Component} from '@angular/core';
import {SettingsComponentDirective} from '../_abstract/abstract.settings.component';
import {AuthenticationService} from '../../../model/network/authentication.service';
import {NavigationService} from '../../../model/navigation.service';
import {NotificationService} from '../../../model/notification.service';
import {RandomPhotoSettingsService} from './random-photo.settings.service';
import {ClientRandomPhotoConfig} from '../../../../../common/config/public/ClientConfig';
import {SettingsService} from '../settings.service';
@Component({
selector: 'app-settings-random-photo',
templateUrl: './random-photo.settings.component.html',
styleUrls: [
'./random-photo.settings.component.css',
'../_abstract/abstract.settings.component.css',
],
providers: [RandomPhotoSettingsService],
})
export class RandomPhotoSettingsComponent extends SettingsComponentDirective<ClientRandomPhotoConfig> {
constructor(
authService: AuthenticationService,
navigation: NavigationService,
settingsService: RandomPhotoSettingsService,
notification: NotificationService,
globalSettingsService: SettingsService
) {
super(
$localize`Random Photo`,
'random',
authService,
navigation,
settingsService,
notification,
globalSettingsService,
(s) => s.RandomPhoto
);
}
}

View File

@ -1,31 +0,0 @@
import { Injectable } from '@angular/core';
import { NetworkService } from '../../../model/network/network.service';
import { SettingsService } from '../settings.service';
import { AbstractSettingsService } from '../_abstract/abstract.settings.service';
import { DatabaseType } from '../../../../../common/config/private/PrivateConfig';
import { ClientSearchConfig } from '../../../../../common/config/public/ClientConfig';
@Injectable()
export class RandomPhotoSettingsService extends AbstractSettingsService<ClientSearchConfig> {
constructor(
private networkService: NetworkService,
settingsService: SettingsService
) {
super(settingsService);
}
public hasAvailableSettings(): boolean {
return false;
}
public isSupported(): boolean {
return (
this.settingsService.settings.value.Database.type !==
DatabaseType.memory
);
}
public updateSettings(settings: ClientSearchConfig): Promise<void> {
return this.networkService.putJson('/settings/randomPhoto', { settings });
}
}

View File

@ -1,3 +0,0 @@
.panel-info {
text-align: center;
}

View File

@ -1,74 +0,0 @@
<form #settingsForm="ngForm">
<div class="card mb-4"
[ngClass]="states.enabled.value && !settingsService.isSupported()?'panel-warning':''">
<h5 class="card-header">
<span class="oi oi-{{icon}}"></span> {{Name}}
<div class="switch-wrapper">
<bSwitch
class="switch"
name="enabled"
switch-on-color="success"
[switch-inverse]="true"
switch-off-text="Disabled"
switch-on-text="Enabled"
i18n-switch-off-text
i18n-switch-on-text
[switch-disabled]="inProgress || !settingsService.isSupported()"
[switch-handle-width]="100"
[switch-label-width]="20"
[(ngModel)]="states.enabled.value">
</bSwitch>
</div>
</h5>
<div class="card-body">
<div [hidden]="!error" class="alert alert-danger" role="alert"><strong>Error: </strong>{{error}}</div>
<ng-container *ngIf="states.enabled.value || settingsService.isSupported()">
<app-settings-entry
name="Autocomplete"
description="Show hints while typing search query."
i18n-description i18n-name
[ngModel]="states.AutoComplete.enabled">
</app-settings-entry>
<app-settings-entry
name="Maximum media result"
description="Maximum number of photos and videos that listed in one search result"
i18n-description i18n-name
[ngModel]="states.maxMediaResult"
[required]="true">
</app-settings-entry>
<app-settings-entry
name="List directories"
description="Search will also return with directories"
i18n-description i18n-name
[ngModel]="states.listDirectories">
</app-settings-entry>
<app-settings-entry
name="List metafiles"
description="Search also returns with metafiles from directories that contain a media file of the matched search result"
i18n-description i18n-name
[ngModel]="states.listMetafiles">
</app-settings-entry>
</ng-container>
<div class="panel-info" *ngIf="(!states.enabled.value && !settingsService.isSupported())" i18n>
Search is not supported with these settings
</div>
<button class="btn btn-success float-end"
[disabled]="!settingsForm.form.valid || !changed || inProgress"
(click)="save()" i18n>Save
</button>
<button class="btn btn-secondary float-end"
[disabled]=" !changed || inProgress"
(click)="reset()" i18n>Reset
</button>
</div>
</div>
</form>

View File

@ -1,41 +0,0 @@
import {Component} from '@angular/core';
import {SettingsComponentDirective} from '../_abstract/abstract.settings.component';
import {AuthenticationService} from '../../../model/network/authentication.service';
import {NavigationService} from '../../../model/navigation.service';
import {NotificationService} from '../../../model/notification.service';
import {SearchSettingsService} from './search.settings.service';
import {ClientSearchConfig} from '../../../../../common/config/public/ClientConfig';
import {SettingsService} from '../settings.service';
@Component({
selector: 'app-settings-search',
templateUrl: './search.settings.component.html',
styleUrls: [
'./search.settings.component.css',
'../_abstract/abstract.settings.component.css',
],
providers: [SearchSettingsService],
})
export class SearchSettingsComponent extends SettingsComponentDirective<ClientSearchConfig> {
constructor(
authService: AuthenticationService,
navigation: NavigationService,
settingsService: SearchSettingsService,
notification: NotificationService,
globalSettingsService: SettingsService
) {
super(
$localize`Search`,
'magnifying-glass',
authService,
navigation,
settingsService,
notification,
globalSettingsService,
(s) => s.Search
);
}
}

View File

@ -1,31 +0,0 @@
import { Injectable } from '@angular/core';
import { NetworkService } from '../../../model/network/network.service';
import { SettingsService } from '../settings.service';
import { AbstractSettingsService } from '../_abstract/abstract.settings.service';
import { DatabaseType } from '../../../../../common/config/private/PrivateConfig';
import { ClientSearchConfig } from '../../../../../common/config/public/ClientConfig';
@Injectable()
export class SearchSettingsService extends AbstractSettingsService<ClientSearchConfig> {
constructor(
private networkService: NetworkService,
settingsService: SettingsService
) {
super(settingsService);
}
hasAvailableSettings(): boolean {
return false;
}
public isSupported(): boolean {
return (
this.settingsService.settings.value.Database.type !==
DatabaseType.memory
);
}
public updateSettings(settings: ClientSearchConfig): Promise<void> {
return this.networkService.putJson('/settings/search', { settings });
}
}

View File

@ -1,5 +1,5 @@
import {Injectable} from '@angular/core';
import {BehaviorSubject} from 'rxjs';
import {BehaviorSubject, first} from 'rxjs';
import {NetworkService} from '../../model/network/network.service';
import {WebConfig} from '../../../../common/config/private/WebConfig';
@ -7,7 +7,9 @@ import {WebConfigClassBuilder} from 'typeconfig/src/decorators/builders/WebConfi
import {ConfigPriority} from '../../../../common/config/public/ClientConfig';
import {CookieNames} from '../../../../common/CookieNames';
import {CookieService} from 'ngx-cookie-service';
import {JobDTO} from '../../../../common/entities/job/JobDTO';
import {DefaultsJobs, JobDTO} from '../../../../common/entities/job/JobDTO';
import {StatisticDTO} from '../../../../common/entities/settings/StatisticDTO';
import {ScheduledJobsService} from './scheduled-jobs.service';
@Injectable()
export class SettingsService {
@ -15,9 +17,12 @@ export class SettingsService {
public settings: BehaviorSubject<WebConfig>;
private fetchingSettings = false;
public availableJobs: BehaviorSubject<JobDTO[]>;
public statistic: BehaviorSubject<StatisticDTO>;
constructor(private networkService: NetworkService,
private jobsService: ScheduledJobsService,
private cookieService: CookieService) {
this.statistic = new BehaviorSubject(null);
this.availableJobs = new BehaviorSubject([]);
this.settings = new BehaviorSubject<WebConfig>(new WebConfig());
this.getSettings().catch(console.error);
@ -25,9 +30,21 @@ export class SettingsService {
if (this.cookieService.check(CookieNames.configPriority)) {
this.configPriority =
parseInt(this.cookieService.get(CookieNames.configPriority));
}
this.settings.pipe(first()).subscribe(() => {
this.loadStatistic();
});
this.jobsService.onJobFinish.subscribe((jobName: string) => {
if (
jobName === DefaultsJobs[DefaultsJobs.Indexing] ||
jobName === DefaultsJobs[DefaultsJobs['Database Reset']]
) {
this.loadStatistic();
}
});
}
public async getAvailableJobs(): Promise<void> {
@ -60,6 +77,11 @@ export class SettingsService {
this.configPriority.toString(),
365 * 50
);
}
async loadStatistic(): Promise<void> {
this.statistic.next(
await this.networkService.getJson<StatisticDTO>('/admin/statistic')
);
}
}

View File

@ -1,7 +0,0 @@
.panel-info {
text-align: center;
}
.share-settings-save-buttons .btn {
margin-bottom: 10px;
}

View File

@ -1,93 +0,0 @@
<form #settingsForm="ngForm">
<div class="card mb-4"
[ngClass]="states.enabled.value && !settingsService.isSupported()?'panel-warning':''">
<h5 class="card-header">
<span class="oi oi-{{icon}}"></span> {{Name}}
<div class="switch-wrapper">
<bSwitch
class="switch"
name="enabled"
switch-on-color="success"
[switch-inverse]="true"
switch-off-text="Disabled"
switch-on-text="Enabled"
i18n-switch-off-text
i18n-switch-on-text
[switch-disabled]="inProgress || !settingsService.isSupported()"
[switch-handle-width]="100"
[switch-label-width]="20"
[(ngModel)]="states.enabled.value">
</bSwitch>
</div>
</h5>
<div class="card-body">
<div [hidden]="!error" class="alert alert-danger" role="alert"><strong>Error: </strong>{{error}}</div>
<ng-container *ngIf="states.enabled.value || settingsService.isSupported()">
<app-settings-entry
name="Password protected"
description="Enables password protected sharing links."
i18n-description i18n-name
[ngModel]="states.passwordProtected"
[required]="true">
</app-settings-entry>
</ng-container>
<div class="panel-info" *ngIf="(!states.enabled.value && !settingsService.isSupported())" i18n>
Sharing is not supported with these settings
</div>
<div class="share-settings-save-buttons">
<button class="btn btn-success float-end"
[disabled]="!settingsForm.form.valid || !changed || inProgress"
(click)="save()" i18n>Save
</button>
<button class="btn btn-secondary float-end"
[disabled]=" !changed || inProgress"
(click)="reset()" i18n>Reset
</button>
</div>
<br>
<br>
<hr/>
<p class="title" i18n>Shared links:</p>
<ng-container *ngIf="shares && shares.length >0">
<table class="table table-hover">
<thead>
<tr>
<th i18n>Key</th>
<th i18n>Folder</th>
<th i18n>Creator</th>
<th i18n>Expires</th>
<th></th>
</tr>
</thead>
<tbody>
<tr *ngFor="let share of shares">
<td>{{share.sharingKey}}</td>
<td>{{share.path}}</td>
<td>{{share.creator.name}}</td>
<td>{{share.expires | date}}</td>
<td>
<button (click)="deleteSharing(share)" class="btn btn-danger float-end">
<span class="oi oi-trash" aria-hidden="true" aria-label="Delete"></span>
</button>
</td>
</tr>
</tbody>
</table>
</ng-container>
<ng-container *ngIf="!shares || shares.length == 0">
<div class="panel-info" i18n>
No sharing was created.
</div>
</ng-container>
</div>
</div>
</form>

View File

@ -1,65 +0,0 @@
import {Component, OnInit} from '@angular/core';
import {SettingsComponentDirective} from '../_abstract/abstract.settings.component';
import {AuthenticationService} from '../../../model/network/authentication.service';
import {NavigationService} from '../../../model/navigation.service';
import {NotificationService} from '../../../model/notification.service';
import {ShareSettingsService} from './share.settings.service';
import {ClientSharingConfig} from '../../../../../common/config/public/ClientConfig';
import {SharingDTO} from '../../../../../common/entities/SharingDTO';
import {SettingsService} from '../settings.service';
@Component({
selector: 'app-settings-share',
templateUrl: './share.settings.component.html',
styleUrls: [
'./share.settings.component.css',
'../_abstract/abstract.settings.component.css',
],
providers: [ShareSettingsService],
})
export class ShareSettingsComponent
extends SettingsComponentDirective<ClientSharingConfig, ShareSettingsService>
implements OnInit {
public shares: SharingDTO[] = [];
constructor(
authService: AuthenticationService,
navigation: NavigationService,
settingsService: ShareSettingsService,
notification: NotificationService,
globalSettingsService: SettingsService
) {
super(
$localize`Share`,
'share',
authService,
navigation,
settingsService,
notification,
globalSettingsService,
(s) => s.Sharing
);
}
ngOnInit(): void {
super.ngOnInit();
this.getSharingList();
}
async deleteSharing(sharing: SharingDTO): Promise<void> {
await this.settingsService.deleteSharing(sharing);
await this.getSharingList();
}
private async getSharingList(): Promise<void> {
try {
this.shares = await this.settingsService.getSharingList();
} catch (err) {
this.shares = [];
throw err;
}
}
}

View File

@ -1,44 +0,0 @@
import { Injectable } from '@angular/core';
import { NetworkService } from '../../../model/network/network.service';
import { SettingsService } from '../settings.service';
import { AbstractSettingsService } from '../_abstract/abstract.settings.service';
import { ClientSharingConfig } from '../../../../../common/config/public/ClientConfig';
import { SharingDTO } from '../../../../../common/entities/SharingDTO';
import { DatabaseType } from '../../../../../common/config/private/PrivateConfig';
@Injectable()
export class ShareSettingsService extends AbstractSettingsService<ClientSharingConfig> {
constructor(
private networkService: NetworkService,
settingsService: SettingsService
) {
super(settingsService);
}
hasAvailableSettings(): boolean {
return false;
}
public isSupported(): boolean {
return (
this.settingsService.settings.value.Database.type !==
DatabaseType.memory &&
this.settingsService.settings.value.Users.authenticationRequired === true
);
}
public updateSettings(settings: ClientSharingConfig): Promise<void> {
return this.networkService.putJson('/settings/share', { settings });
}
public getSharingList(): Promise<SharingDTO[]> {
if (!this.settingsService.settings.value.Sharing.enabled) {
return Promise.resolve([]);
}
return this.networkService.getJson('/share/list');
}
public deleteSharing(sharing: SharingDTO): Promise<void> {
return this.networkService.deleteJson('/share/' + sharing.sharingKey);
}
}

View File

@ -34,14 +34,19 @@
*ngIf="states.value.enabled === false">
{{Name}} <span i18n>config is not supported with these settings.</span>
</div>
<button class="btn btn-success float-end"
[disabled]="settingsForm.form.invalid || !changed || inProgress"
(click)="save()" i18n>Save
</button>
<button class="btn btn-secondary float-end"
[disabled]=" !changed || inProgress"
(click)="reset()" i18n>Reset
</button>
<div class="row">
<div class="col">
<button class="btn btn-success float-end"
[disabled]="settingsForm.form.invalid || !changed || inProgress"
(click)="save()" i18n>Save
</button>
<button class="btn btn-secondary float-end"
[disabled]=" !changed || inProgress"
(click)="reset()" i18n>Reset
</button>
</div>
</div>
<ng-content></ng-content>
</div>
</div>

View File

@ -1,74 +0,0 @@
<form #settingsForm="ngForm" class="form-horizontal">
<div class="card mb-4">
<h5 class="card-header">
<span class="oi oi-{{icon}}"></span> {{Name}}
</h5>
<div class="card-body">
<div [hidden]="!error" class="alert alert-danger" role="alert"><strong>Error: </strong>{{error}}</div>
<app-settings-entry
name="Thumbnail Quality"
description="High quality may be slow."
i18n-description i18n-name
[ngModel]="states.server.qualityPriority">
</app-settings-entry>
<app-settings-entry
name="Icon size"
description="Icon size (used on maps)."
i18n-description i18n-name
[ngModel]="states.client.iconSize"
[required]="true">
</app-settings-entry>
<app-settings-entry
name="Thumbnail sizes"
i18n-name
placeholder="240; 480"
[ngModel]="states.client.thumbnailSizes"
[required]="true">
<small class="form-text text-muted">
<ng-container i18n>Size of the thumbnails.</ng-container>
<br/>
<ng-container i18n>The best matching size will be generated. (More sizes give better quality, but use more
storage and CPU to render.)
</ng-container>
<br/>
<ng-container i18n>';' separated integers. If size is 240, that shorter side of the thumbnail will have 160
pixels.
</ng-container>
</small>
</app-settings-entry>
<button class="btn btn-success float-end"
[disabled]="!settingsForm.form.valid || !changed || inProgress"
(click)="save()" i18n>Save
</button>
<button class="btn btn-secondary float-end"
[disabled]=" !changed || inProgress"
(click)="reset()" i18n>Reset
</button>
<app-settings-job-button class="mt-2 mt-md-0 float-left"
[soloRun]="true"
(jobError)="error=$event"
[jobName]="jobName"
[allowParallelRun]="false"
[config]="Config"></app-settings-job-button>
<ng-container *ngIf="Progress != null">
<br/>
<hr/>
<app-settings-job-progress [progress]="Progress"></app-settings-job-progress>
</ng-container>
</div>
</div>
</form>

View File

@ -1,65 +0,0 @@
import {Component, OnInit} from '@angular/core';
import {SettingsComponentDirective} from '../_abstract/abstract.settings.component';
import {AuthenticationService} from '../../../model/network/authentication.service';
import {NavigationService} from '../../../model/navigation.service';
import {NotificationService} from '../../../model/notification.service';
import {ThumbnailSettingsService} from './thumbnail.settings.service';
import {DefaultsJobs, JobDTOUtils,} from '../../../../../common/entities/job/JobDTO';
import {ScheduledJobsService} from '../scheduled-jobs.service';
import {JobProgressDTO, JobProgressStates,} from '../../../../../common/entities/job/JobProgressDTO';
import {ServerThumbnailConfig} from '../../../../../common/config/private/PrivateConfig';
import {ClientThumbnailConfig} from '../../../../../common/config/public/ClientConfig';
import {SettingsService} from '../settings.service';
@Component({
selector: 'app-settings-thumbnail',
templateUrl: './thumbnail.settings.component.html',
styleUrls: [
'./thumbnail.settings.component.css',
'../_abstract/abstract.settings.component.css',
],
providers: [ThumbnailSettingsService],
})
export class ThumbnailSettingsComponent
extends SettingsComponentDirective<ServerThumbnailConfig>
implements OnInit {
JobProgressStates = JobProgressStates;
readonly jobName = DefaultsJobs[DefaultsJobs['Thumbnail Generation']];
constructor(
authService: AuthenticationService,
navigation: NavigationService,
settingsService: ThumbnailSettingsService,
notification: NotificationService,
public jobsService: ScheduledJobsService,
globalSettingsService: SettingsService
) {
super(
$localize`Thumbnail`,
'image',
authService,
navigation,
settingsService,
notification,
globalSettingsService,
(s) => s.Media.Thumbnail
);
}
get Config(): { sizes: number[] } {
return {sizes: [this.states.client.thumbnailSizes.original[0]]};
}
get Progress(): JobProgressDTO {
return this.jobsService.progress.value[
JobDTOUtils.getHashName(this.jobName, this.Config)
];
}
ngOnInit(): void {
super.ngOnInit();
}
}

View File

@ -1,23 +0,0 @@
import { Injectable } from '@angular/core';
import { NetworkService } from '../../../model/network/network.service';
import { AbstractSettingsService } from '../_abstract/abstract.settings.service';
import { SettingsService } from '../settings.service';
import { ServerThumbnailConfig } from '../../../../../common/config/private/PrivateConfig';
@Injectable()
export class ThumbnailSettingsService extends AbstractSettingsService<ServerThumbnailConfig> {
constructor(
private networkService: NetworkService,
settingsService: SettingsService
) {
super(settingsService);
}
hasAvailableSettings(): boolean {
return false;
}
public updateSettings(settings: ServerThumbnailConfig): Promise<void> {
return this.networkService.putJson('/settings/thumbnail', { settings });
}
}

View File

@ -1,97 +0,0 @@
<div class="card mb-4">
<h5 class="card-header">
<span class="oi oi-{{icon}}"></span> {{Name}}
<div class="switch-wrapper">
<bSwitch
class="switch"
name="enabled"
switch-on-color="success"
[switch-inverse]="true"
[switch-off-text]="text.Disabled"
[switch-on-text]="text.Enabled"
[switch-handle-width]="100"
[switch-label-width]="20"
[switch-disabled]="inProgress"
[(ngModel)]="enabled"
(changeState)="switched($event)">
</bSwitch>
</div>
</h5>
<div class="card-body">
<div [hidden]="!error" class="alert alert-danger" role="alert"><strong>Error: </strong>{{error}}</div>
<ng-container *ngIf="enabled">
<table class="table table-hover">
<thead>
<tr>
<th i18n>Name</th>
<th i18n>Role</th>
<th></th>
</tr>
</thead>
<tbody>
<tr *ngFor="let user of users">
<td>{{user.name}}</td>
<td *ngIf="canModifyUser(user)">
<select class="form-select" [(ngModel)]="user.role" (change)="updateRole(user)" required>
<option *ngFor="let repository of userRoles" [value]="repository.key">
{{repository.value}}
</option>
</select>
</td>
<td *ngIf="!canModifyUser(user)">
{{user.role | stringifyRole}}
</td>
<td>
<button [disabled]="!canModifyUser(user)" (click)="deleteUser(user)"
[ngClass]="canModifyUser(user)? 'btn-danger':'btn-secondary'"
class="btn float-end">
<span class="oi oi-trash" aria-hidden="true" aria-label="Delete"></span>
</button>
</td>
</tr>
</tbody>
</table>
<button class="btn btn-primary float-end"
(click)="initNewUser()" i18n>+ Add user
</button>
</ng-container>
<div class="panel-info" *ngIf="!enabled" i18n>
To protect the site with password / have login enable this.
</div>
</div>
</div>
<!-- Modal -->
<div bsModal #userModal="bs-modal" class="modal fade" id="userModal" tabindex="-1" role="dialog"
aria-labelledby="userModalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="userModalLabel" i18n>Add new User</h5>
<button type="button" class="btn-close" (click)="userModal.hide()" data-dismiss="modal" aria-label="Close">
</button>
</div>
<form #NewUserForm="ngForm">
<div class="modal-body">
<input type="text" class="form-control" i18n-placeholder placeholder="Username"
[(ngModel)]="newUser.name" name="name" required>
<input type="password" class="form-control" i18n-placeholder placeholder="Password"
[(ngModel)]="newUser.password" name="password" autocomplete="off" required>
<select class="form-select" [(ngModel)]="newUser.role" name="role" required>
<option *ngFor="let repository of userRoles" [value]="repository.key">{{repository.value}}
</option>
</select>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" (click)="userModal.hide()" i18n>Close</button>
<button type="button" class="btn btn-primary" data-dismiss="modal"
(click)="addNewUser()"
[disabled]="!NewUserForm.form.valid" i18n>Add User
</button>
</div>
</form>
</div>
</div>
</div>

View File

@ -1,160 +0,0 @@
import {Component, OnInit, ViewChild} from '@angular/core';
import {AuthenticationService} from '../../../model/network/authentication.service';
import {UserDTO, UserRoles} from '../../../../../common/entities/UserDTO';
import {Utils} from '../../../../../common/Utils';
import {UserManagerSettingsService} from './usermanager.settings.service';
import {ModalDirective} from 'ngx-bootstrap/modal';
import {NavigationService} from '../../../model/navigation.service';
import {NotificationService} from '../../../model/notification.service';
import {ErrorCodes, ErrorDTO} from '../../../../../common/entities/Error';
import {ISettingsComponent} from '../_abstract/ISettingsComponent';
@Component({
selector: 'app-settings-usermanager',
templateUrl: './usermanager.settings.component.html',
styleUrls: [
'./usermanager.settings.component.css',
'../_abstract/abstract.settings.component.css',
],
providers: [UserManagerSettingsService],
})
export class UserMangerSettingsComponent implements OnInit, ISettingsComponent {
@ViewChild('userModal', {static: false}) public childModal: ModalDirective;
public newUser = {} as UserDTO;
public userRoles: { key: number; value: string }[] = [];
public users: UserDTO[] = [];
public enabled = true;
public error: string = null;
public inProgress = false;
Name: string;
HasAvailableSettings = true;
Changed = false;
icon = 'person';
text = {
Enabled: 'Enabled',
Disabled: 'Disabled',
Low: 'Low',
High: 'High',
};
constructor(
private authService: AuthenticationService,
private navigation: NavigationService,
private userSettings: UserManagerSettingsService,
private notification: NotificationService
) {
this.Name = $localize`Password protection`;
this.text.Enabled = $localize`Enabled`;
this.text.Disabled = $localize`Disabled`;
this.text.Low = $localize`Low`;
this.text.High = $localize`High`;
}
ngOnInit(): void {
if (
!this.authService.isAuthenticated() ||
this.authService.user.value.role < UserRoles.Admin
) {
this.navigation.toLogin();
return;
}
this.userRoles = Utils.enumToArray(UserRoles)
.filter((r) => r.key !== UserRoles.LimitedGuest)
.filter((r) => r.key <= this.authService.user.value.role)
.sort((a, b) => a.key - b.key);
this.getSettings();
this.getUsersList();
}
canModifyUser(user: UserDTO): boolean {
const currentUser = this.authService.user.value;
if (!currentUser) {
return false;
}
return currentUser.name !== user.name && currentUser.role >= user.role;
}
async switched(event: {
previousValue: false;
currentValue: true;
}): Promise<void> {
this.inProgress = true;
this.error = '';
this.enabled = event.currentValue;
try {
await this.userSettings.updateSettings(this.enabled);
await this.getSettings();
if (this.enabled === true) {
this.notification.success(
$localize`Password protection enabled`,
$localize`Success`
);
this.notification.info($localize`Server restart is recommended.`);
this.getUsersList();
} else {
this.notification.success(
$localize`Password protection disabled`,
$localize`Success`
);
}
} catch (err) {
console.error(err);
if (err.message) {
this.error = (err as ErrorDTO).message;
}
}
this.inProgress = false;
}
initNewUser(): void {
this.newUser = {role: UserRoles.User} as UserDTO;
this.childModal.show();
}
async addNewUser(): Promise<void> {
try {
await this.userSettings.createUser(this.newUser);
await this.getUsersList();
this.childModal.hide();
} catch (e) {
const err: ErrorDTO = e;
this.notification.error(
err.message + ', ' + err.details,
$localize`User creation error!`
);
}
}
async updateRole(user: UserDTO): Promise<void> {
await this.userSettings.updateRole(user);
await this.getUsersList();
this.childModal.hide();
}
async deleteUser(user: UserDTO): Promise<void> {
await this.userSettings.deleteUser(user);
await this.getUsersList();
this.childModal.hide();
}
private async getSettings(): Promise<void> {
this.enabled = await this.userSettings.getSettings();
}
private async getUsersList(): Promise<void> {
try {
this.users = await this.userSettings.getUsers();
} catch (err) {
this.users = [];
if ((err as ErrorDTO).code !== ErrorCodes.USER_MANAGEMENT_DISABLED) {
throw err;
}
}
}
}

View File

@ -1,39 +0,0 @@
import { Injectable } from '@angular/core';
import { UserDTO } from '../../../../../common/entities/UserDTO';
import { NetworkService } from '../../../model/network/network.service';
import { WebConfig } from '../../../../../common/config/private/WebConfig';
@Injectable()
export class UserManagerSettingsService {
constructor(private networkService: NetworkService) {}
public createUser(user: UserDTO): Promise<string> {
return this.networkService.putJson('/user', { newUser: user });
}
public async getSettings(): Promise<boolean> {
return (await this.networkService.getJson<Promise<WebConfig>>('/settings'))
.Users.authenticationRequired;
}
public updateSettings(settings: boolean): Promise<void> {
return this.networkService.putJson('/settings/authentication', {
settings,
});
}
public getUsers(): Promise<Array<UserDTO>> {
return this.networkService.getJson('/user/list');
}
public deleteUser(user: UserDTO): Promise<void> {
return this.networkService.deleteJson('/user/' + user.id);
}
public updateRole(user: UserDTO): Promise<void> {
return this.networkService.postJson('/user/' + user.id + '/role', {
newRole: user.role,
});
}
}

View File

@ -1,160 +0,0 @@
<form #settingsForm="ngForm" class="form-horizontal">
<div class="card mb-4">
<h5 class="card-header">
<span class="oi oi-{{icon}}"></span> {{Name}}
<div class="switch-wrapper">
<bSwitch
class="switch"
name="enabled"
switch-on-color="success"
[switch-inverse]="true"
switch-off-text="Disabled"
switch-on-text="Enabled"
i18n-switch-off-text
i18n-switch-on-text
[switch-disabled]="inProgress"
[switch-handle-width]="100"
[switch-label-width]="20"
[(ngModel)]="states.client.enabled.value">
</bSwitch>
</div>
</h5>
<div class="card-body">
<div [hidden]="!error" class="alert alert-danger" role="alert"><strong>Error: </strong>{{error}}</div>
<div class="alert alert-secondary" role="alert">
<ng-container i18n>Video support uses ffmpeg. ffmpeg and ffprobe binaries need to be available in the PATH or
the
@ffmpeg-installer/ffmpeg and @ffprobe-installer/ffprobe optional node packages need to be installed.
</ng-container>
</div>
<hr/>
<p class="title" i18n>Video transcoding:</p>
<div class="alert alert-secondary" role="alert">
<ng-container i18n>To ensure smooth video playback, video transcoding is recommended to a lower bit rate than
the
server's upload rate.
</ng-container>&nbsp;
<ng-container i18n>The transcoded videos will be save to the thumbnail folder.</ng-container>&nbsp;
<ng-container i18n>You can trigger the transcoding manually, but you can also create an automatic encoding job
in
advanced settings mode.
</ng-container>&nbsp;
</div>
<app-settings-entry
name="Format"
i18n-name
[ngModel]="states.server.transcoding.format"
(ngModelChange)="formatChanged($event)"
[options]="formats"
[required]="true">
</app-settings-entry>
<app-settings-entry
name="Codec"
i18n-name
[ngModel]="states.server.transcoding.codec"
[options]="codecs[states.server.transcoding.format.value]"
[required]="true">
</app-settings-entry>
<app-settings-entry
name="Resolution"
description="The height of the output video will be scaled down to this, while keeping the aspect ratio."
i18n-name i18n-description
[ngModel]="states.server.transcoding.resolution"
(change)="updateBitRate()"
[options]="resolutions"
[required]="true">
</app-settings-entry>
<app-settings-entry
name="FPS"
description="Target frame per second (fps) of the output video will be scaled down this this."
i18n-name i18n-description
[ngModel]="states.server.transcoding.fps"
(change)="updateBitRate()"
[options]="fps"
[required]="true">
</app-settings-entry>
<div class="mb-3 row"
[class.changed-settings]="states.server.transcoding.bitRate.value !== states.server.transcoding.bitRate.default">
<label class="col-md-2 control-label" for="bitRate" i18n>Bit rate</label>
<div class="col-md-10">
<div class="input-group">
<input type="number" class="form-control" placeholder="2"
id="bitRate"
min="0"
step="0.1"
max="1000"
[(ngModel)]="bitRate"
name="bitRate" required>
<div class="input-group-append">
<span class="input-group-text">mbps</span>
</div>
</div>
<small class="form-text text-muted" i18n>Target bit rate of the output video will be scaled down this this.
This should be less than the
upload rate of your home server.</small>
</div>
</div>
<app-settings-entry
name="CRF"
description="The range of the Constant Rate Factor (CRF) scale is 0–51, where 0 is lossless, 23 is the default, and 51 is worst quality possible."
i18n-name i18n-description
[ngModel]="states.server.transcoding.crf"
[required]="true">
</app-settings-entry>
<app-settings-entry
name="Preset"
description="A preset is a collection of options that will provide a certain encoding speed to compression ratio. A slower preset will provide better compression (compression is quality per filesize)."
i18n-name i18n-description
[ngModel]="states.server.transcoding.preset"
[required]="true">
</app-settings-entry>
<app-settings-entry
name="Custom Options"
description="; separated values. It will be sent to ffmpeg as it is, as custom options."
placeholder="-pass 2; -minrate 1M; -maxrate 1M; -bufsize 2M"
i18n-name i18n-description
[ngModel]="states.server.transcoding.customOptions"
[allowSpaces]="true"
[required]="true">
</app-settings-entry>
<button class="btn btn-success float-end"
[disabled]="!settingsForm.form.valid || !changed || inProgress"
(click)="save()" i18n>Save
</button>
<button class="btn btn-secondary float-end"
[disabled]=" !changed || inProgress"
(click)="reset()" i18n>Reset
</button>
<app-settings-job-button class="mt-2 mt-md-0 float-left"
[soloRun]="true"
(jobError)="error=$event"
[allowParallelRun]="false"
[jobName]="jobName"></app-settings-job-button>
<ng-container *ngIf="Progress != null">
<br/>
<hr/>
<app-settings-job-progress [progress]="Progress"></app-settings-job-progress>
</ng-container>
</div>
</div>
</form>

View File

@ -1,142 +0,0 @@
import { Component } from '@angular/core';
import { VideoSettingsService } from './video.settings.service';
import { SettingsComponentDirective } from '../_abstract/abstract.settings.component';
import { AuthenticationService } from '../../../model/network/authentication.service';
import { NavigationService } from '../../../model/navigation.service';
import { NotificationService } from '../../../model/notification.service';
import { ScheduledJobsService } from '../scheduled-jobs.service';
import {
DefaultsJobs,
JobDTOUtils,
} from '../../../../../common/entities/job/JobDTO';
import {
JobProgressDTO,
JobProgressStates,
} from '../../../../../common/entities/job/JobProgressDTO';
import {
ServerVideoConfig,
videoCodecType,
videoFormatType,
videoResolutionType,
} from '../../../../../common/config/private/PrivateConfig';
import { ClientVideoConfig } from '../../../../../common/config/public/ClientConfig';
import {SettingsService} from '../settings.service';
@Component({
selector: 'app-settings-video',
templateUrl: './video.settings.component.html',
styleUrls: [
'./video.settings.component.css',
'../_abstract/abstract.settings.component.css',
],
providers: [VideoSettingsService],
})
export class VideoSettingsComponent extends SettingsComponentDirective<ServerVideoConfig> {
readonly resolutionTypes: videoResolutionType[] = [
360, 480, 720, 1080, 1440, 2160, 4320,
];
resolutions: { key: number; value: string }[] = [];
codecs: { [key: string]: { key: videoCodecType; value: videoCodecType }[] } =
{
webm: ['libvpx', 'libvpx-vp9'].map((e: videoCodecType) => ({
key: e,
value: e,
})),
mp4: ['libx264', 'libx265'].map((e: videoCodecType) => ({
key: e,
value: e,
})),
};
formats: { key: videoFormatType; value: videoFormatType }[] = [
'mp4',
'webm',
].map((e: videoFormatType) => ({ key: e, value: e }));
fps = [24, 25, 30, 48, 50, 60].map((e) => ({ key: e, value: e }));
JobProgressStates = JobProgressStates;
readonly jobName = DefaultsJobs[DefaultsJobs['Video Converting']];
constructor(
authService: AuthenticationService,
navigation: NavigationService,
settingsService: VideoSettingsService,
public jobsService: ScheduledJobsService,
notification: NotificationService,
globalSettingsService: SettingsService
) {
super(
$localize`Video`,
'video',
authService,
navigation,
settingsService,
notification,
globalSettingsService,
(s) => s.Media.Video
);
const currentRes =
settingsService.Settings.value.Media.Video.transcoding.resolution;
if (this.resolutionTypes.indexOf(currentRes) === -1) {
this.resolutionTypes.push(currentRes);
}
this.resolutions = this.resolutionTypes.map((e) => ({
key: e,
value: e + 'px',
}));
}
get Progress(): JobProgressDTO {
return this.jobsService.progress.value[
JobDTOUtils.getHashName(DefaultsJobs[DefaultsJobs['Video Converting']])
];
}
get bitRate(): number {
return this.states.server.transcoding.bitRate.value / 1024 / 1024;
}
set bitRate(value: number) {
this.states.server.transcoding.bitRate.value = Math.round(
value * 1024 * 1024
);
}
getRecommendedBitRate(resolution: number, fps: number): number {
let bitRate = 1024 * 1024;
if (resolution <= 360) {
bitRate = 1024 * 1024;
} else if (resolution <= 480) {
bitRate = 2.5 * 1024 * 1024;
} else if (resolution <= 720) {
bitRate = 5 * 1024 * 1024;
} else if (resolution <= 1080) {
bitRate = 8 * 1024 * 1024;
} else if (resolution <= 1440) {
bitRate = 16 * 1024 * 1024;
} else {
bitRate = 40 * 1024 * 1024;
}
if (fps > 30) {
bitRate *= 1.5;
}
return bitRate;
}
updateBitRate(): void {
this.states.server.transcoding.bitRate.value = this.getRecommendedBitRate(
this.states.server.transcoding.resolution.value,
this.states.server.transcoding.fps.value
);
}
formatChanged(format: videoFormatType): void {
this.states.server.transcoding.codec.value = this.codecs[format][0].key;
}
}

View File

@ -1,20 +0,0 @@
import { Injectable } from '@angular/core';
import { NetworkService } from '../../../model/network/network.service';
import { SettingsService } from '../settings.service';
import { AbstractSettingsService } from '../_abstract/abstract.settings.service';
import { ClientVideoConfig } from '../../../../../common/config/public/ClientConfig';
import { ServerVideoConfig } from '../../../../../common/config/private/PrivateConfig';
@Injectable()
export class VideoSettingsService extends AbstractSettingsService<ServerVideoConfig> {
constructor(
private networkService: NetworkService,
settingsService: SettingsService
) {
super(settingsService);
}
public updateSettings(settings: ServerVideoConfig): Promise<void> {
return this.networkService.putJson('/settings/video', { settings });
}
}