diff --git a/application/client/src/app/ui/tabs/plugins/details/component.ts b/application/client/src/app/ui/tabs/plugins/details/component.ts index 52b0f85a0..6d3ce8d76 100644 --- a/application/client/src/app/ui/tabs/plugins/details/component.ts +++ b/application/client/src/app/ui/tabs/plugins/details/component.ts @@ -1,9 +1,23 @@ -import { Component, ChangeDetectorRef, Input } from '@angular/core'; +import { + Component, + ChangeDetectorRef, + Input, + ViewChild, + ElementRef, + AfterViewInit, + AfterContentInit, + OnDestroy, +} from '@angular/core'; +import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { Ilc, IlcInterface } from '@env/decorators/component'; import { Initial } from '@env/decorators/initial'; import { ChangesDetector } from '@ui/env/extentions/changes'; +import { micromark } from 'micromark'; import { PluginDesc } from '../desc'; import { Provider } from '../provider'; +import { bridge } from '@service/bridge'; + +import * as dom from '@ui/env/dom'; @Component({ selector: 'app-plugins-manager-details', @@ -13,13 +27,106 @@ import { Provider } from '../provider'; }) @Initial() @Ilc() -export class Details extends ChangesDetector { +export class Details extends ChangesDetector implements AfterViewInit, AfterContentInit, OnDestroy { @Input() public provider!: Provider; @Input() public plugin!: PluginDesc; - constructor(cdRef: ChangeDetectorRef) { + @ViewChild('content') contentRef!: ElementRef; + + public readme: SafeHtml = ''; + public loading: boolean = false; + + protected async load(): Promise { + const drop = () => { + this.loading = false; + this.readme = ''; + this.detectChanges(); + }; + this.loading = true; + this.links().unbind(); + this.detectChanges(); + if (!this.plugin.path) { + return drop(); + } + const delimiter = await bridge.folders().delimiter(); + const path = `${this.plugin.path.filename}${delimiter}README.md`; + if (!(await bridge.files().exists(path))) { + return drop(); + } + bridge + .files() + .read(path) + .then((content: string) => { + this.readme = this.sanitizer.bypassSecurityTrustHtml(micromark(content)); + this.detectChanges(); + this.links().bind(); + }) + .catch((err: Error) => { + this.log().error(`Fail to read "${path}": ${err.message}`); + this.readme = ''; + }) + .finally(() => { + this.loading = false; + this.detectChanges(); + }); + } + + protected links(): { + bind(): void; + unbind(): void; + } { + return { + bind: (): void => { + const links = this.contentRef.nativeElement.querySelectorAll('a'); + if (links === null) { + return; + } + links.forEach((link: HTMLAnchorElement) => { + link.addEventListener('click', this.redirect); + }); + }, + unbind: (): void => { + const links = this.contentRef.nativeElement.querySelectorAll('a'); + if (links === null) { + return; + } + links.forEach((link: HTMLAnchorElement) => { + link.removeEventListener('click', this.redirect); + }); + }, + }; + } + + protected safeLoad(): void { + this.load().catch((err: Error) => { + this.log().error(`Fail to load plugin's details: ${err.message}`); + }); + } + + protected redirect(event: MouseEvent): void { + dom.stop(event); + // TODO: safe openening URL + } + + constructor(cdRef: ChangeDetectorRef, protected readonly sanitizer: DomSanitizer) { super(cdRef); } + + public ngAfterViewInit(): void { + this.safeLoad(); + } + + public ngAfterContentInit(): void { + this.env().subscriber.register( + this.provider.subjects.get().selected.subscribe(() => { + this.safeLoad(); + }), + ); + } + + public ngOnDestroy(): void { + this.links().unbind(); + } } export interface Details extends IlcInterface {} diff --git a/application/client/src/app/ui/tabs/plugins/details/styles.less b/application/client/src/app/ui/tabs/plugins/details/styles.less index 60919c817..9fed6c1fa 100644 --- a/application/client/src/app/ui/tabs/plugins/details/styles.less +++ b/application/client/src/app/ui/tabs/plugins/details/styles.less @@ -1,12 +1,33 @@ @import '../../../styles/variables.less'; :host { - position: relative; + position: absolute; + display: flex; + flex-direction: column; + top: 12px; + left: 12px; + bottom: 12px; + right: 12px; + overflow: hidden; & div.info { - margin: 0 6px; - overflow: hidden; + position: relative; + margin: 0 6px 12px 0; & p { text-align: left; } } + & div.actions { + position: relative; + display: flex; + margin: 0 0 12px 0; + flex-direction: row; + justify-content: end; + } + & div.readme { + position: relative; + overflow-y: auto; + overflow-x: hidden; + flex: auto; + + } } diff --git a/application/client/src/app/ui/tabs/plugins/details/template.html b/application/client/src/app/ui/tabs/plugins/details/template.html index 39598102b..ff5cc3262 100644 --- a/application/client/src/app/ui/tabs/plugins/details/template.html +++ b/application/client/src/app/ui/tabs/plugins/details/template.html @@ -2,3 +2,14 @@

{{plugin.name}}

{{plugin.desc}}

+
+ + + +
+
+ + + +
+
diff --git a/application/client/src/app/ui/tabs/plugins/list/component.ts b/application/client/src/app/ui/tabs/plugins/list/component.ts index 130033b80..14f7fb58b 100644 --- a/application/client/src/app/ui/tabs/plugins/list/component.ts +++ b/application/client/src/app/ui/tabs/plugins/list/component.ts @@ -6,7 +6,7 @@ import { Provider } from '../provider'; import { PluginDesc } from '../desc'; export enum Target { - Active, + Installed, Available, } @@ -41,8 +41,8 @@ export class List extends ChangesDetector implements AfterContentInit { public getTitle(): string { switch (this.target) { - case Target.Active: - return 'Active Plugins'; + case Target.Installed: + return 'Installed Plugins'; case Target.Available: return 'Available Plugins'; } @@ -50,7 +50,7 @@ export class List extends ChangesDetector implements AfterContentInit { protected update() { switch (this.target) { - case Target.Active: + case Target.Installed: this.plugins = this.provider.get().active(); break; case Target.Available: diff --git a/application/client/src/app/ui/tabs/plugins/plugin/component.ts b/application/client/src/app/ui/tabs/plugins/plugin/component.ts index e90a84156..7c4fc30d1 100644 --- a/application/client/src/app/ui/tabs/plugins/plugin/component.ts +++ b/application/client/src/app/ui/tabs/plugins/plugin/component.ts @@ -1,4 +1,4 @@ -import { Component, ChangeDetectorRef, Input, HostListener } from '@angular/core'; +import { Component, ChangeDetectorRef, Input, HostListener, HostBinding } from '@angular/core'; import { Ilc, IlcInterface } from '@env/decorators/component'; import { Initial } from '@env/decorators/initial'; import { ChangesDetector } from '@ui/env/extentions/changes'; @@ -20,6 +20,13 @@ export class Plugin extends ChangesDetector { @HostListener('click', ['$event']) onClick(_event: MouseEvent) { this.provider.select(this.plugin.entity.dir_path); } + @HostBinding('class') get getClass() { + return !this.provider.selected + ? '' + : this.provider.selected.entity.dir_path === this.plugin.entity.dir_path + ? 'selected' + : ''; + } constructor(cdRef: ChangeDetectorRef) { super(cdRef); } diff --git a/application/client/src/app/ui/tabs/plugins/plugin/styles.less b/application/client/src/app/ui/tabs/plugins/plugin/styles.less index 75640804c..8bc25c6ce 100644 --- a/application/client/src/app/ui/tabs/plugins/plugin/styles.less +++ b/application/client/src/app/ui/tabs/plugins/plugin/styles.less @@ -6,6 +6,16 @@ align-items: center; flex-direction: row; cursor: default; + &.selected { + &::after{ + position: absolute; + content: ''; + height: 100%; + left: -2px; + width: 2px; + background: @scheme-color-accent; + } + } & div.icon { & mat-icon { font-size: 18px; diff --git a/application/client/src/app/ui/tabs/plugins/template.html b/application/client/src/app/ui/tabs/plugins/template.html index 5d6cfe5e5..38e45bb5e 100644 --- a/application/client/src/app/ui/tabs/plugins/template.html +++ b/application/client/src/app/ui/tabs/plugins/template.html @@ -15,11 +15,7 @@
-