mirror of
https://github.com/GNS3/gns3-web-ui.git
synced 2025-06-06 09:11:36 +00:00
Merge pull request #964 from GNS3/Project-readme-support
Project readme support
This commit is contained in:
commit
b68b871dd6
@ -67,6 +67,7 @@
|
|||||||
"d3-ng2-service": "^2.1.0",
|
"d3-ng2-service": "^2.1.0",
|
||||||
"file-saver": "^2.0.2",
|
"file-saver": "^2.0.2",
|
||||||
"ini": "^1.3.5",
|
"ini": "^1.3.5",
|
||||||
|
"marked": "^1.1.1",
|
||||||
"material-design-icons": "^3.0.1",
|
"material-design-icons": "^3.0.1",
|
||||||
"ng-circle-progress": "^1.6.0",
|
"ng-circle-progress": "^1.6.0",
|
||||||
"ng2-file-upload": "^1.3.0",
|
"ng2-file-upload": "^1.3.0",
|
||||||
|
@ -281,9 +281,12 @@ import { ChangeHostnameActionComponent } from './components/project-map/context-
|
|||||||
import { ChangeHostnameDialogComponent } from './components/project-map/change-hostname-dialog/change-hostname-dialog.component';
|
import { ChangeHostnameDialogComponent } from './components/project-map/change-hostname-dialog/change-hostname-dialog.component';
|
||||||
import { ApplianceInfoDialogComponent } from './components/project-map/new-template-dialog/appliance-info-dialog/appliance-info-dialog.component';
|
import { ApplianceInfoDialogComponent } from './components/project-map/new-template-dialog/appliance-info-dialog/appliance-info-dialog.component';
|
||||||
import { ResetLinkActionComponent } from './components/project-map/context-menu/actions/reset-link/reset-link-action.component';
|
import { ResetLinkActionComponent } from './components/project-map/context-menu/actions/reset-link/reset-link-action.component';
|
||||||
|
import { ReadmeEditorComponent } from './components/projects/edit-project-dialog/readme-editor/readme-editor.component';
|
||||||
|
import { MarkedDirective } from './directives/marked.directive';
|
||||||
import { InformationDialogComponent } from './components/dialogs/information-dialog.component';
|
import { InformationDialogComponent } from './components/dialogs/information-dialog.component';
|
||||||
import { TemplateNameDialogComponent } from './components/project-map/new-template-dialog/template-name-dialog/template-name-dialog.component';
|
import { TemplateNameDialogComponent } from './components/project-map/new-template-dialog/template-name-dialog/template-name-dialog.component';
|
||||||
import { UpdatesService } from './services/updates.service';
|
import { UpdatesService } from './services/updates.service';
|
||||||
|
import { ProjectReadmeComponent } from './components/project-map/project-readme/project-readme.component';
|
||||||
|
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
@ -470,6 +473,8 @@ import { UpdatesService } from './services/updates.service';
|
|||||||
ChangeHostnameActionComponent,
|
ChangeHostnameActionComponent,
|
||||||
ChangeHostnameDialogComponent,
|
ChangeHostnameDialogComponent,
|
||||||
ApplianceInfoDialogComponent,
|
ApplianceInfoDialogComponent,
|
||||||
|
ReadmeEditorComponent,
|
||||||
|
MarkedDirective,
|
||||||
InformationDialogComponent,
|
InformationDialogComponent,
|
||||||
TemplateNameDialogComponent,
|
TemplateNameDialogComponent,
|
||||||
ConfigureCustomAdaptersDialogComponent,
|
ConfigureCustomAdaptersDialogComponent,
|
||||||
@ -617,7 +622,8 @@ import { UpdatesService } from './services/updates.service';
|
|||||||
ChangeHostnameDialogComponent,
|
ChangeHostnameDialogComponent,
|
||||||
ApplianceInfoDialogComponent,
|
ApplianceInfoDialogComponent,
|
||||||
ConfigureCustomAdaptersDialogComponent,
|
ConfigureCustomAdaptersDialogComponent,
|
||||||
EditNetworkConfigurationDialogComponent
|
EditNetworkConfigurationDialogComponent,
|
||||||
|
ProjectReadmeComponent
|
||||||
],
|
],
|
||||||
bootstrap: [AppComponent]
|
bootstrap: [AppComponent]
|
||||||
})
|
})
|
||||||
|
@ -72,6 +72,7 @@ import { ThemeService } from '../../services/theme.service';
|
|||||||
import { Title } from '@angular/platform-browser';
|
import { Title } from '@angular/platform-browser';
|
||||||
import { NewTemplateDialogComponent } from './new-template-dialog/new-template-dialog.component';
|
import { NewTemplateDialogComponent } from './new-template-dialog/new-template-dialog.component';
|
||||||
import { NodeConsoleService } from '../../services/nodeConsole.service';
|
import { NodeConsoleService } from '../../services/nodeConsole.service';
|
||||||
|
import { ProjectReadmeComponent } from './project-readme/project-readme.component';
|
||||||
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -258,8 +259,7 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
|
|||||||
if (!project ) this.router.navigate(['/servers']);
|
if (!project ) this.router.navigate(['/servers']);
|
||||||
|
|
||||||
this.projectService.open(this.server, this.project.project_id);
|
this.projectService.open(this.server, this.project.project_id);
|
||||||
this.title.setTitle(this.project.name);
|
this.title.setTitle(this.project.name);
|
||||||
|
|
||||||
this.isInterfaceLabelVisible = this.mapSettingsService.showInterfaceLabels;
|
this.isInterfaceLabelVisible = this.mapSettingsService.showInterfaceLabels;
|
||||||
|
|
||||||
this.recentlyOpenedProjectService.setServerId(this.server.id.toString());
|
this.recentlyOpenedProjectService.setServerId(this.server.id.toString());
|
||||||
@ -277,6 +277,7 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
|
|||||||
.subscribe(
|
.subscribe(
|
||||||
(project: Project) => {
|
(project: Project) => {
|
||||||
this.onProjectLoad(project);
|
this.onProjectLoad(project);
|
||||||
|
if (this.mapSettingsService.openReadme) this.showReadme();
|
||||||
},
|
},
|
||||||
error => {
|
error => {
|
||||||
this.progressService.setError(error);
|
this.progressService.setError(error);
|
||||||
@ -897,6 +898,18 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
|
|||||||
instance.project = this.project;
|
instance.project = this.project;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public showReadme() {
|
||||||
|
const dialogRef = this.dialog.open(ProjectReadmeComponent, {
|
||||||
|
width: '600px',
|
||||||
|
height: '650px',
|
||||||
|
autoFocus: false,
|
||||||
|
disableClose: true
|
||||||
|
});
|
||||||
|
let instance = dialogRef.componentInstance;
|
||||||
|
instance.server = this.server;
|
||||||
|
instance.project = this.project;
|
||||||
|
}
|
||||||
|
|
||||||
public ngOnDestroy() {
|
public ngOnDestroy() {
|
||||||
this.nodeConsoleService.openConsoles = 0;
|
this.nodeConsoleService.openConsoles = 0;
|
||||||
this.title.setTitle('GNS3 Web UI');
|
this.title.setTitle('GNS3 Web UI');
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
<h1 mat-dialog-title>Project README</h1>
|
||||||
|
|
||||||
|
<div class="textWrapper" id="text"></div>
|
||||||
|
|
||||||
|
<div mat-dialog-actions>
|
||||||
|
<button mat-button (click)="onNoClick()" color="accent">Close</button>
|
||||||
|
</div>
|
@ -0,0 +1,4 @@
|
|||||||
|
.textWrapper {
|
||||||
|
height: 500px!important;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
import { Component, AfterViewInit } from '@angular/core';
|
||||||
|
import { MatDialogRef } from '@angular/material/dialog';
|
||||||
|
import { Server } from '../../../models/server';
|
||||||
|
import { Project } from '../../../models/project';
|
||||||
|
import { ProjectService } from '../../../services/project.service';
|
||||||
|
import * as marked from 'marked';
|
||||||
|
import { ElementRef } from '@angular/core';
|
||||||
|
import { Renderer2 } from '@angular/core';
|
||||||
|
import { ViewChild } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-project-readme',
|
||||||
|
templateUrl: './project-readme.component.html',
|
||||||
|
styleUrls: ['./project-readme.component.scss']
|
||||||
|
})
|
||||||
|
export class ProjectReadmeComponent implements AfterViewInit {
|
||||||
|
server: Server;
|
||||||
|
project: Project;
|
||||||
|
@ViewChild('text', {static: false}) text: ElementRef;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public dialogRef: MatDialogRef<ProjectReadmeComponent>,
|
||||||
|
private projectService: ProjectService,
|
||||||
|
private elementRef: ElementRef,
|
||||||
|
private renderer: Renderer2
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngAfterViewInit() {
|
||||||
|
let markdown = ``;
|
||||||
|
|
||||||
|
this.projectService.getReadmeFile(this.server, this.project.project_id).subscribe(file => {
|
||||||
|
if (file) {
|
||||||
|
markdown = file;
|
||||||
|
setTimeout(function(){
|
||||||
|
const markdownHtml = marked(markdown);
|
||||||
|
document.getElementById('text').innerHTML = markdownHtml;
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onNoClick() {
|
||||||
|
this.dialogRef.close();
|
||||||
|
}
|
||||||
|
}
|
@ -41,6 +41,11 @@
|
|||||||
Show interface labels at start
|
Show interface labels at start
|
||||||
</mat-checkbox>
|
</mat-checkbox>
|
||||||
</mat-tab>
|
</mat-tab>
|
||||||
|
|
||||||
|
<mat-tab label="Readme" *ngIf="server && project">
|
||||||
|
<app-readme-editor #editor [server]="server" [project]="project"></app-readme-editor>
|
||||||
|
</mat-tab>
|
||||||
|
|
||||||
<mat-tab label="Global variables">
|
<mat-tab label="Global variables">
|
||||||
<form [formGroup]="variableFormGroup">
|
<form [formGroup]="variableFormGroup">
|
||||||
<mat-form-field class="form-field">
|
<mat-form-field class="form-field">
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Component, OnInit, Injectable } from '@angular/core';
|
import { Component, OnInit, Injectable, ViewChild } from '@angular/core';
|
||||||
import { MatDialogRef } from '@angular/material/dialog';
|
import { MatDialogRef } from '@angular/material/dialog';
|
||||||
import { FormGroup, FormBuilder, FormControl, Validators } from '@angular/forms';
|
import { FormGroup, FormBuilder, FormControl, Validators } from '@angular/forms';
|
||||||
import { Server } from '../../../models/server';
|
import { Server } from '../../../models/server';
|
||||||
@ -6,6 +6,7 @@ import { Project, ProjectVariable } from '../../../models/project';
|
|||||||
import { ToasterService } from '../../../services/toaster.service';
|
import { ToasterService } from '../../../services/toaster.service';
|
||||||
import { NonNegativeValidator } from '../../../validators/non-negative-validator';
|
import { NonNegativeValidator } from '../../../validators/non-negative-validator';
|
||||||
import { ProjectService } from '../../../services/project.service';
|
import { ProjectService } from '../../../services/project.service';
|
||||||
|
import { ReadmeEditorComponent } from './readme-editor/readme-editor.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-edit-project-dialog',
|
selector: 'app-edit-project-dialog',
|
||||||
@ -13,6 +14,8 @@ import { ProjectService } from '../../../services/project.service';
|
|||||||
styleUrls: ['./edit-project-dialog.component.scss']
|
styleUrls: ['./edit-project-dialog.component.scss']
|
||||||
})
|
})
|
||||||
export class EditProjectDialogComponent implements OnInit {
|
export class EditProjectDialogComponent implements OnInit {
|
||||||
|
@ViewChild('editor') editor: ReadmeEditorComponent;
|
||||||
|
|
||||||
server: Server;
|
server: Server;
|
||||||
project: Project;
|
project: Project;
|
||||||
formGroup: FormGroup;
|
formGroup: FormGroup;
|
||||||
@ -89,8 +92,10 @@ export class EditProjectDialogComponent implements OnInit {
|
|||||||
this.project.auto_close = !this.project.auto_close;
|
this.project.auto_close = !this.project.auto_close;
|
||||||
|
|
||||||
this.projectService.update(this.server, this.project).subscribe((project: Project) => {
|
this.projectService.update(this.server, this.project).subscribe((project: Project) => {
|
||||||
this.toasterService.success(`Project ${project.name} updated.`);
|
this.projectService.postReadmeFile(this.server, this.project.project_id, this.editor.markdown).subscribe((response) => {
|
||||||
this.onNoClick();
|
this.toasterService.success(`Project ${project.name} updated.`);
|
||||||
|
this.onNoClick();
|
||||||
|
});
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
this.toasterService.error(`Fill all required fields with correct values.`);
|
this.toasterService.error(`Fill all required fields with correct values.`);
|
||||||
|
@ -0,0 +1,11 @@
|
|||||||
|
<mat-tab-group>
|
||||||
|
<mat-tab label="Edit">
|
||||||
|
<textarea class="editorWrapper" matInput type="text" [(ngModel)]="markdown"></textarea>
|
||||||
|
</mat-tab>
|
||||||
|
|
||||||
|
<mat-tab label="Preview">
|
||||||
|
<span *ngIf="markdown">
|
||||||
|
<div class="textWrapper" appMarked text={{markdown}}></div>
|
||||||
|
</span>
|
||||||
|
</mat-tab>
|
||||||
|
</mat-tab-group>
|
@ -0,0 +1,11 @@
|
|||||||
|
.textWrapper {
|
||||||
|
height: 500px!important;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editorWrapper {
|
||||||
|
background-color: white;
|
||||||
|
color: black;
|
||||||
|
height: 500px!important;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
import { Component, OnInit, ViewEncapsulation, Input } from '@angular/core';
|
||||||
|
import * as marked from 'marked';
|
||||||
|
import { ProjectService } from '../../../../services/project.service';
|
||||||
|
import { Server } from '../../../../models/server';
|
||||||
|
import { Project } from '../../../../models/project';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-readme-editor',
|
||||||
|
templateUrl: './readme-editor.component.html',
|
||||||
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
styleUrls: ['./readme-editor.component.scss']
|
||||||
|
})
|
||||||
|
export class ReadmeEditorComponent implements OnInit {
|
||||||
|
@Input() server: Server;
|
||||||
|
@Input() project: Project;
|
||||||
|
|
||||||
|
public markdown = ``;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private projectService: ProjectService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.projectService.getReadmeFile(this.server, this.project.project_id).subscribe(file => {
|
||||||
|
if (file) this.markdown = file;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -11,7 +11,8 @@
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<mat-checkbox [(ngModel)]="settings.crash_reports">Send anonymous crash reports</mat-checkbox><br/>
|
<mat-checkbox [(ngModel)]="settings.crash_reports">Send anonymous crash reports</mat-checkbox><br/>
|
||||||
<mat-checkbox [(ngModel)]="integrateLinksLabelsToLinks">Integrate link labels to links</mat-checkbox>
|
<mat-checkbox [(ngModel)]="integrateLinksLabelsToLinks">Integrate link labels to links</mat-checkbox><br/>
|
||||||
|
<mat-checkbox [(ngModel)]="openReadme">Automatically open project README files</mat-checkbox>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- <div>
|
<!-- <div>
|
||||||
|
@ -15,6 +15,7 @@ export class SettingsComponent implements OnInit {
|
|||||||
settings = { ...SettingsService.DEFAULTS };
|
settings = { ...SettingsService.DEFAULTS };
|
||||||
consoleCommand: string;
|
consoleCommand: string;
|
||||||
integrateLinksLabelsToLinks: boolean;
|
integrateLinksLabelsToLinks: boolean;
|
||||||
|
openReadme: boolean;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private settingsService: SettingsService,
|
private settingsService: SettingsService,
|
||||||
@ -29,12 +30,15 @@ export class SettingsComponent implements OnInit {
|
|||||||
this.settings = this.settingsService.getAll();
|
this.settings = this.settingsService.getAll();
|
||||||
this.consoleCommand = this.consoleService.command;
|
this.consoleCommand = this.consoleService.command;
|
||||||
this.integrateLinksLabelsToLinks = this.mapSettingsService.integrateLinkLabelsToLinks;
|
this.integrateLinksLabelsToLinks = this.mapSettingsService.integrateLinkLabelsToLinks;
|
||||||
|
this.openReadme = this.mapSettingsService.openReadme;
|
||||||
}
|
}
|
||||||
|
|
||||||
save() {
|
save() {
|
||||||
this.settingsService.setAll(this.settings);
|
this.settingsService.setAll(this.settings);
|
||||||
this.toaster.success('Settings have been saved.');
|
this.toaster.success('Settings have been saved.');
|
||||||
|
|
||||||
this.mapSettingsService.toggleIntegrateInterfaceLabels(this.integrateLinksLabelsToLinks);
|
this.mapSettingsService.toggleIntegrateInterfaceLabels(this.integrateLinksLabelsToLinks);
|
||||||
|
this.mapSettingsService.toggleOpenReadme(this.openReadme);
|
||||||
}
|
}
|
||||||
|
|
||||||
setDarkMode(value: boolean) {
|
setDarkMode(value: boolean) {
|
||||||
|
29
src/app/directives/marked.directive.ts
Normal file
29
src/app/directives/marked.directive.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { Directive, ElementRef, OnInit, Renderer2, Input, OnChanges } from '@angular/core';
|
||||||
|
import * as marked from 'marked';
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: '[appMarked]'
|
||||||
|
})
|
||||||
|
export class MarkedDirective implements OnInit, OnChanges {
|
||||||
|
@Input() text: string;
|
||||||
|
|
||||||
|
constructor(private elementRef: ElementRef,
|
||||||
|
private renderer: Renderer2) { }
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.updateText();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnChanges() {
|
||||||
|
this.updateText();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateText() {
|
||||||
|
const markText = this.text;
|
||||||
|
|
||||||
|
if (markText && markText.length > 0) {
|
||||||
|
const markdownHtml = marked(markText);
|
||||||
|
this.renderer.setProperty(this.elementRef.nativeElement, 'innerHTML', markdownHtml);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -13,10 +13,12 @@ export class MapSettingsService {
|
|||||||
|
|
||||||
public showInterfaceLabels: boolean = true;
|
public showInterfaceLabels: boolean = true;
|
||||||
public integrateLinkLabelsToLinks: boolean = true;
|
public integrateLinkLabelsToLinks: boolean = true;
|
||||||
|
public openReadme: boolean = true;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.isLayerNumberVisible = localStorage.getItem('layersVisibility') === 'true' ? true : false;
|
this.isLayerNumberVisible = localStorage.getItem('layersVisibility') === 'true' ? true : false;
|
||||||
if (localStorage.getItem('integrateLinkLabelsToLinks')) this.integrateLinkLabelsToLinks = localStorage.getItem('integrateLinkLabelsToLinks') === 'true' ? true : false;
|
if (localStorage.getItem('integrateLinkLabelsToLinks')) this.integrateLinkLabelsToLinks = localStorage.getItem('integrateLinkLabelsToLinks') === 'true' ? true : false;
|
||||||
|
if (localStorage.getItem('openReadme')) this.openReadme = localStorage.getItem('openReadme') === 'true' ? true : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
changeMapLockValue(value: boolean) {
|
changeMapLockValue(value: boolean) {
|
||||||
@ -56,4 +58,14 @@ export class MapSettingsService {
|
|||||||
localStorage.setItem('integrateLinkLabelsToLinks', 'false');
|
localStorage.setItem('integrateLinkLabelsToLinks', 'false');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleOpenReadme(value: boolean) {
|
||||||
|
this.openReadme = value;
|
||||||
|
localStorage.removeItem('openReadme');
|
||||||
|
if (value) {
|
||||||
|
localStorage.setItem('openReadme', 'true');
|
||||||
|
} else {
|
||||||
|
localStorage.setItem('openReadme', 'false');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,14 @@ export class ProjectService {
|
|||||||
this.projectListSubject.next(true);
|
this.projectListSubject.next(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getReadmeFile(server: Server, project_id: string) {
|
||||||
|
return this.httpServer.get<any>(server, `/projects/${project_id}/files/README.txt`);
|
||||||
|
}
|
||||||
|
|
||||||
|
postReadmeFile(server: Server, project_id: string, readme: string) {
|
||||||
|
return this.httpServer.post<any>(server, `/projects/${project_id}/files/README.txt`, readme);
|
||||||
|
}
|
||||||
|
|
||||||
get(server: Server, project_id: string) {
|
get(server: Server, project_id: string) {
|
||||||
return this.httpServer.get<Project>(server, `/projects/${project_id}`);
|
return this.httpServer.get<Project>(server, `/projects/${project_id}`);
|
||||||
}
|
}
|
||||||
|
@ -7684,6 +7684,11 @@ map-visit@^1.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
object-visit "^1.0.0"
|
object-visit "^1.0.0"
|
||||||
|
|
||||||
|
marked@^1.1.1:
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/marked/-/marked-1.1.1.tgz#e5d61b69842210d5df57b05856e0c91572703e6a"
|
||||||
|
integrity sha512-mJzT8D2yPxoPh7h0UXkB+dBj4FykPJ2OIfxAWeIHrvoHDkFxukV/29QxoFQoPM6RLEwhIFdJpmKBlqVM3s2ZIw==
|
||||||
|
|
||||||
matcher@^3.0.0:
|
matcher@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/matcher/-/matcher-3.0.0.tgz#bd9060f4c5b70aa8041ccc6f80368760994f30ca"
|
resolved "https://registry.yarnpkg.com/matcher/-/matcher-3.0.0.tgz#bd9060f4c5b70aa8041ccc6f80368760994f30ca"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user