{
32 | if (!this.promise) {
33 | return;
34 | }
35 | console.log('EventChannel', 'unlisten all');
36 | await this.promise?.then(list => list.forEach(f => f?.()));
37 | this.promise = undefined;
38 | }
39 |
40 | public async send(payload?: P): Promise {
41 | console.debug('event-channel::tx', this.token, payload);
42 | return emit(`${this.token}:tx`, payload);
43 | }
44 |
45 | public async close(payload?: P): Promise {
46 | console.debug('event-channel::close', this.token, payload);
47 | return emit(`${this.token}:close`, payload);
48 | }
49 |
50 | abstract onReceive(payload: RxPayload): void;
51 |
52 | abstract onClose(payload: ClosePayload): void;
53 |
54 | }
55 |
--------------------------------------------------------------------------------
/src/app/core/services/dev-mode.service.ts:
--------------------------------------------------------------------------------
1 | import {Injectable, NgZone} from '@angular/core';
2 | import {BackendClient} from "./backend-client";
3 | import {Device} from "../../types";
4 |
5 | @Injectable({
6 | providedIn: 'root'
7 | })
8 | export class DevModeService extends BackendClient {
9 |
10 | constructor(zone: NgZone) {
11 | super(zone, "dev-mode");
12 | }
13 |
14 | async status(device: Device): Promise {
15 | return this.invoke('status', {device});
16 | }
17 |
18 | async token(device: Device): Promise {
19 | return this.invoke('token', {device});
20 | }
21 | }
22 |
23 | export interface DevModeStatus {
24 | token?: string;
25 | remaining?: string;
26 | }
27 |
--------------------------------------------------------------------------------
/src/app/core/services/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./app-manager.service";
2 | export * from "./device-manager.service";
3 | export * from "./apps-repo.service";
4 | export * from "./dev-mode.service";
5 | export * from './update.service';
6 |
--------------------------------------------------------------------------------
/src/app/core/services/local-file.service.ts:
--------------------------------------------------------------------------------
1 | import {Injectable, NgZone} from "@angular/core";
2 | import {BackendClient} from "./backend-client";
3 | import {ProgressCallback, progressChannel} from "./progress-callback";
4 |
5 | @Injectable({
6 | providedIn: 'root'
7 | })
8 | export class LocalFileService extends BackendClient {
9 | constructor(zone: NgZone) {
10 | super(zone, 'local-file');
11 | }
12 |
13 | async checksum(path: string, algorithm: 'sha256'): Promise {
14 | return this.invoke('checksum', {path, algorithm});
15 | }
16 |
17 | async remove(path: string, recursive: boolean = false): Promise {
18 | await this.invoke('remove', {path, recursive});
19 | }
20 |
21 | async copy(source: string, target: string, progress?: ProgressCallback): Promise {
22 | const onProgress = progressChannel(progress);
23 | await this.invoke('copy', {source, target, onProgress});
24 | }
25 |
26 | async tempPath(extension: string): Promise {
27 | return this.invoke('temp_path', {extension});
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/app/core/services/progress-callback.ts:
--------------------------------------------------------------------------------
1 | import {Channel} from "@tauri-apps/api/core";
2 |
3 | export type ProgressCallback = (copied: number, total: number) => void;
4 |
5 | interface ProgressPayload {
6 | copied: number;
7 | total: number;
8 | }
9 |
10 | export function progressChannel(progress?: ProgressCallback) {
11 | const onProgress = new Channel();
12 | onProgress.onmessage = (e: ProgressPayload) => {
13 | progress?.(e.copied, e.total);
14 | }
15 | return onProgress;
16 | }
17 |
--------------------------------------------------------------------------------
/src/app/core/services/remote-command.service.spec.ts:
--------------------------------------------------------------------------------
1 | import {RemoteCommandService} from "./remote-command.service";
2 | import {TestBed} from "@angular/core/testing";
3 | import {NewDeviceWithLocalPrivateKey} from "../../types";
4 |
5 | describe('RemoteCommandService', () => {
6 | let service: RemoteCommandService;
7 | let device = {
8 | name: 'test',
9 | host: '192.168.89.33',
10 | port: 22,
11 | username: 'root',
12 | profile: 'ose',
13 | privateKey: {
14 | openSsh: 'id_rsa'
15 | }
16 | };
17 |
18 | beforeEach(() => {
19 | service = TestBed.inject(RemoteCommandService);
20 | });
21 |
22 | it('should be created', () => {
23 | expect(service).toBeTruthy();
24 | });
25 |
26 | it('should successfully perform uname command', async () => {
27 | let output = service.exec(device, 'uname -a', 'utf-8');
28 | await expectAsync(output).toBeResolved();
29 | });
30 |
31 | it('should run false command with exception', async () => {
32 | let output = service.exec(device, 'false', 'utf-8');
33 | await expectAsync(output).toBeRejected();
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/src/app/core/services/update.service.ts:
--------------------------------------------------------------------------------
1 | import {fetch} from '@tauri-apps/plugin-http';
2 | import {Injectable} from '@angular/core';
3 | import {SemVer} from 'semver';
4 |
5 | @Injectable({
6 | providedIn: 'root'
7 | })
8 | export class UpdateService {
9 |
10 | async getRecentRelease(): Promise {
11 | return fetch('https://api.github.com/repos/webosbrew/dev-manager-desktop/releases/latest', {
12 | headers: {'accept': 'application/vnd.github.v3+json'},
13 |
14 | }).then(async res => new ReleaseImpl(await res.json()));
15 | }
16 |
17 | get ignoreUntil(): SemVer | null {
18 | try {
19 | const value = localStorage.getItem('devManager:ignoreVersionUntil');
20 | if (!value) return null;
21 | return new SemVer(value, true);
22 | } catch (e) {
23 | return null;
24 | }
25 | }
26 |
27 | set ignoreUntil(value: SemVer | null) {
28 | if (value?.version) {
29 | localStorage.setItem('devManager:ignoreVersionUntil', value?.version);
30 | } else {
31 | localStorage.removeItem('devManager:ignoreVersionUntil');
32 | }
33 | }
34 | }
35 |
36 | export interface Release {
37 | readonly html_url: string;
38 | readonly tag_name: string;
39 | readonly body: string;
40 | }
41 |
42 | class ReleaseImpl implements Release {
43 | html_url: string = '';
44 | tag_name: string = '';
45 | body: string = '';
46 |
47 | constructor(data: Partial) {
48 | Object.assign(this, data);
49 | }
50 |
51 | }
52 |
--------------------------------------------------------------------------------
/src/app/debug/crashes/crashes.component.html:
--------------------------------------------------------------------------------
1 | @if (reportsError) {
2 |
5 | } @else {
6 |
7 |
8 |
9 |
No crash reports
10 |
If a native application crashes, the crash report will be available here.
11 |
12 |
13 |
14 | -
16 |
17 |
{{ report.title }}
18 | {{ report.summary }}
19 |
20 |
21 |
25 |
26 |
27 |
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/src/app/debug/crashes/crashes.component.scss:
--------------------------------------------------------------------------------
1 | pre {
2 | //user-select: all;
3 | cursor: text;
4 | overflow-x: visible;
5 | }
6 |
7 | .report-entry {
8 | * {
9 | max-lines: 1;
10 | text-overflow: ellipsis;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/app/debug/crashes/details/details.component.html:
--------------------------------------------------------------------------------
1 |
12 | {{report.content | async}}
13 |
16 |
--------------------------------------------------------------------------------
/src/app/debug/crashes/details/details.component.scss:
--------------------------------------------------------------------------------
1 | .modal-dialog-scrollable .modal-body {
2 | overflow-x: auto;
3 | }
4 |
--------------------------------------------------------------------------------
/src/app/debug/crashes/details/details.component.ts:
--------------------------------------------------------------------------------
1 | import {Component} from '@angular/core';
2 | import {CrashReport} from "../../../core/services";
3 | import {firstValueFrom} from "rxjs";
4 | import {save as showSaveDialog} from "@tauri-apps/plugin-dialog";
5 | import {ProgressDialogComponent} from "../../../shared/components/progress-dialog/progress-dialog.component";
6 | import {writeTextFile} from "@tauri-apps/plugin-fs";
7 | import {NgbActiveModal, NgbModal} from "@ng-bootstrap/ng-bootstrap";
8 |
9 | @Component({
10 | selector: 'app-crash-details',
11 | templateUrl: './details.component.html',
12 | styleUrls: ['./details.component.scss']
13 | })
14 | export class DetailsComponent {
15 |
16 | constructor(public report: CrashReport, public modal: NgbActiveModal, private modals: NgbModal) {
17 | }
18 |
19 | async copyReport(report: CrashReport): Promise {
20 | await navigator.clipboard.writeText(await firstValueFrom(report.content));
21 | }
22 |
23 | async saveReport(report: CrashReport): Promise {
24 | let target: string | null;
25 | try {
26 | target = await showSaveDialog({
27 | defaultPath: `${report.saveName}.txt`,
28 | });
29 | } catch (e) {
30 | return;
31 | }
32 | if (!target) {
33 | return;
34 | }
35 | const progress = ProgressDialogComponent.open(this.modals);
36 | try {
37 | await writeTextFile(target, await firstValueFrom(report.content));
38 | } finally {
39 | progress.close();
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/app/debug/debug-routing.module.ts:
--------------------------------------------------------------------------------
1 | import {NgModule} from '@angular/core';
2 | import {RouterModule, Routes} from '@angular/router';
3 | import {DebugComponent} from "./debug.component";
4 | import {CrashesComponent} from "./crashes/crashes.component";
5 |
6 | const routes: Routes = [{
7 | path: '',
8 | component: DebugComponent,
9 | children: [
10 | {
11 | path: '#crashes',
12 | component: CrashesComponent
13 | }
14 | ]
15 | }];
16 |
17 | @NgModule({
18 | imports: [RouterModule.forChild(routes)],
19 | exports: [RouterModule]
20 | })
21 | export class DebugRoutingModule {
22 | }
23 |
--------------------------------------------------------------------------------
/src/app/debug/debug.component.html:
--------------------------------------------------------------------------------
1 |
2 |
4 | -
5 | Crash Reports
6 |
7 |
8 |
9 |
10 | @if (device && device.username !== 'prisoner') {
11 | -
12 | Syslog
13 |
14 |
15 |
16 |
17 | -
18 | dmesg
19 |
20 |
21 |
22 |
23 | -
24 | ls-monitor
25 |
26 |
27 |
28 |
29 | }
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/src/app/debug/debug.component.scss:
--------------------------------------------------------------------------------
1 | .debug-content {
2 | .tab-pane {
3 | height: 100% !important;
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/app/debug/debug.component.ts:
--------------------------------------------------------------------------------
1 | import {Component, OnDestroy, OnInit, ViewEncapsulation} from '@angular/core';
2 | import {ActivatedRoute, Router} from "@angular/router";
3 | import {DeviceManagerService} from "../core/services";
4 | import {Device} from "../types";
5 | import {Subscription} from "rxjs";
6 | import {NgbNavChangeEvent} from "@ng-bootstrap/ng-bootstrap";
7 | import {Location} from "@angular/common";
8 |
9 | @Component({
10 | selector: 'app-debug',
11 | templateUrl: './debug.component.html',
12 | styleUrls: ['./debug.component.scss'],
13 | encapsulation: ViewEncapsulation.None,
14 | })
15 | export class DebugComponent implements OnInit, OnDestroy {
16 | device: Device | null = null;
17 | activeTab: string = 'crashes';
18 |
19 | private subscriptions: Subscription = new Subscription();
20 |
21 | constructor(public route: ActivatedRoute, private location: Location, private deviceManager: DeviceManagerService) {
22 | }
23 |
24 | ngOnInit(): void {
25 | this.subscriptions.add(this.deviceManager.selected$.subscribe((selected) => {
26 | this.device = selected;
27 | }));
28 | this.subscriptions.add(this.route.fragment.subscribe(frag => frag && (this.activeTab = frag)));
29 | }
30 |
31 | ngOnDestroy(): void {
32 | this.subscriptions.unsubscribe();
33 | }
34 |
35 | tabChange($event: NgbNavChangeEvent) {
36 | $event.preventDefault();
37 | const nextId = $event.nextId;
38 | this.activeTab = nextId;
39 | this.location.replaceState(`${this.location.path(false)}#${nextId}`);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/app/debug/debug.module.ts:
--------------------------------------------------------------------------------
1 | import {NgModule} from '@angular/core';
2 | import {CommonModule} from '@angular/common';
3 | import {DebugRoutingModule} from "./debug-routing.module";
4 | import {DebugComponent} from './debug.component';
5 | import {NgbNavModule, NgbTooltipModule} from "@ng-bootstrap/ng-bootstrap";
6 | import {CrashesComponent} from "./crashes/crashes.component";
7 | import {SharedModule} from "../shared/shared.module";
8 | import {PmLogComponent} from './pmlog/pmlog.component';
9 | import {LogReaderComponent} from './log-reader/log-reader.component';
10 | import {TerminalModule} from "../terminal";
11 | import {DmesgComponent} from "./dmesg/dmesg.component";
12 | import {PmLogControlComponent} from './pmlog/control/control.component';
13 | import {SetContextComponent} from './pmlog/set-context/set-context.component';
14 | import {FormsModule, ReactiveFormsModule} from "@angular/forms";
15 | import {DetailsComponent} from './crashes/details/details.component';
16 | import {LsMonitorComponent} from "./ls-monitor/ls-monitor.component";
17 | import {DetailsComponent as LsMonitorDetailsComponent} from "./ls-monitor/details/details.component";
18 |
19 | import {ObjectHighlightPipe} from "./ls-monitor/object-highlight.pipe";
20 |
21 | import hljs from 'highlight.js'
22 | import json from 'highlight.js/lib/languages/json';
23 | import {SearchBarDirective} from "../shared/directives";
24 | import {SizeCalculatorComponent} from "../shared/components/term-size-calculator/size-calculator.component";
25 |
26 | @NgModule({
27 | declarations: [
28 | DebugComponent,
29 | CrashesComponent,
30 | PmLogComponent,
31 | LogReaderComponent,
32 | DmesgComponent,
33 | PmLogControlComponent,
34 | SetContextComponent,
35 | DetailsComponent,
36 | LsMonitorComponent,
37 | LsMonitorDetailsComponent,
38 | ObjectHighlightPipe,
39 | ],
40 | imports: [
41 | CommonModule,
42 | DebugRoutingModule,
43 | NgbNavModule,
44 | NgbTooltipModule,
45 | SharedModule,
46 | TerminalModule,
47 | FormsModule,
48 | ReactiveFormsModule,
49 | SearchBarDirective,
50 | SizeCalculatorComponent,
51 | ]
52 | })
53 | export class DebugModule {
54 | constructor() {
55 | hljs.registerLanguage('json', json);
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/app/debug/dmesg/dmesg.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
11 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/app/debug/dmesg/dmesg.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/app/debug/dmesg/dmesg.component.scss
--------------------------------------------------------------------------------
/src/app/debug/dmesg/dmesg.component.ts:
--------------------------------------------------------------------------------
1 | import {Component, Input} from '@angular/core';
2 | import {Device} from "../../types";
3 | import {RemoteCommandService} from "../../core/services/remote-command.service";
4 | import {from, identity, mergeMap, Observable} from "rxjs";
5 | import {LogMessage, RemoteLogService} from "../../core/services/remote-log.service";
6 |
7 | @Component({
8 | selector: 'app-dmesg',
9 | templateUrl: './dmesg.component.html',
10 | styleUrls: ['./dmesg.component.scss']
11 | })
12 | export class DmesgComponent {
13 |
14 | logs?: Observable;
15 |
16 | private deviceField: Device | null = null;
17 |
18 | constructor(private cmd: RemoteCommandService, private log: RemoteLogService) {
19 | }
20 |
21 |
22 | get device(): Device | null {
23 | return this.deviceField;
24 | }
25 |
26 | @Input()
27 | set device(device: Device | null) {
28 | this.deviceField = device;
29 | this.logs = undefined;
30 | if (device) {
31 | this.reload(device);
32 | }
33 | }
34 |
35 | async clearBuffer(): Promise {
36 | const device = this.device;
37 | if (!device) {
38 | return;
39 | }
40 | await this.log.dmesgClear(device);
41 | this.reload(device);
42 | }
43 |
44 | private reload(device: Device) {
45 | this.logs = from(this.log.dmesg(device)).pipe(mergeMap(identity));
46 | }
47 |
48 | }
49 |
--------------------------------------------------------------------------------
/src/app/debug/index.ts:
--------------------------------------------------------------------------------
1 | export {DebugModule} from './debug.module';
2 |
--------------------------------------------------------------------------------
/src/app/debug/log-reader/log-reader.component.html:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
11 |
13 | {{ match.resultIndex + 1 }}
14 | / {{ match.resultCount }}
15 |
19 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/app/debug/log-reader/log-reader.component.scss:
--------------------------------------------------------------------------------
1 | ul.pmlog {
2 | background-color: black;
3 | list-style: none;
4 | overflow-y: scroll;
5 |
6 | li.log-line {
7 | * {
8 | color: white;
9 | }
10 |
11 | &.warning * {
12 | color: orange;
13 | }
14 |
15 | &.err * {
16 | color: red;
17 | }
18 |
19 | &.debug * {
20 | color: lightgray;
21 | }
22 |
23 | &.notice * {
24 | color: limegreen;
25 | }
26 |
27 | &.crit {
28 | color: white;
29 | background-color: red;
30 | }
31 |
32 | &.alert {
33 | color: orange;
34 | background-color: red;
35 | }
36 |
37 | &.emerg {
38 | color: black;
39 | background-color: red;
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/app/debug/ls-monitor/details/details.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Data |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | {{ message.information }}
21 | |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
Type: {{ selected.type }}
29 |
Sender: {{ selected.sender }}
30 |
Destination: {{ selected.destination }}{{ selected.methodCategory }}/{{ selected.method }}
33 |
34 |
35 |
{{ raw }}
36 |
37 |
38 |
--------------------------------------------------------------------------------
/src/app/debug/ls-monitor/details/details.component.scss:
--------------------------------------------------------------------------------
1 | .message-selected {
2 | min-height: 50%;
3 | }
4 |
--------------------------------------------------------------------------------
/src/app/debug/ls-monitor/details/details.component.ts:
--------------------------------------------------------------------------------
1 | import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
2 | import {CallEntry, MonitorMessageItem} from "../ls-monitor.component";
3 |
4 | @Component({
5 | selector: 'app-ls-monitor-details',
6 | templateUrl: './details.component.html',
7 | styleUrls: ['../ls-monitor.component.scss', './details.component.scss']
8 | })
9 | export class DetailsComponent {
10 |
11 | detailsField!: CallEntry;
12 |
13 | @Output()
14 | closeClick = new EventEmitter();
15 |
16 | selectedMessage?: MonitorMessageItem;
17 |
18 | @Input()
19 | set details(value: CallEntry) {
20 | this.detailsField = value;
21 | this.selectedMessage = value.messages[0];
22 | }
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/src/app/debug/ls-monitor/ls-monitor.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
12 |
15 |
18 |
19 |
20 |
21 |
23 |
24 |
25 |
26 | Name |
27 | Sender |
28 | Information |
29 |
30 |
31 |
32 |
33 | {{ row.name }} |
34 | {{ row.sender }} |
35 | {{ row.information }} |
36 |
37 |
38 |
39 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/src/app/debug/ls-monitor/ls-monitor.component.scss:
--------------------------------------------------------------------------------
1 | .ls-monitor {
2 | display: grid;
3 | grid-template-rows: auto 1fr;
4 | grid-template-columns: auto 1fr;
5 |
6 | .top-bar {
7 | grid-row: 1;
8 | grid-column: 2;
9 | }
10 |
11 | .side-bar {
12 | grid-row: 1 / span 2;
13 | grid-column: 1;
14 | }
15 |
16 | .table-container {
17 | grid-row: 2;
18 | grid-column: 2;
19 | }
20 | }
21 |
22 | .table-container {
23 | align-items: start;
24 | }
25 |
26 | .details-opened {
27 | width: 23em;
28 | min-width: 23em;
29 | max-width: 23em;
30 | }
31 |
32 |
33 | table {
34 |
35 | th {
36 | position: sticky;
37 | top: 0;
38 | }
39 |
40 | th, td {
41 | font-size: small;
42 |
43 | &.name-col {
44 | width: 23em;
45 | min-width: 23em;
46 | max-width: 23em;
47 | }
48 |
49 | &.sender-col {
50 | width: 15em;
51 | min-width: 15em;
52 | max-width: 15em;
53 | }
54 |
55 | &.info-col {
56 | width: 100%;
57 | }
58 | }
59 |
60 |
61 | td.name-col {
62 | white-space: nowrap;
63 | overflow: hidden;
64 | text-overflow: ellipsis;
65 |
66 | direction: rtl;
67 | text-align: left;
68 | }
69 |
70 | td.sender-col {
71 | white-space: nowrap;
72 | overflow: hidden;
73 | text-overflow: ellipsis;
74 |
75 | direction: rtl;
76 | text-align: left;
77 | }
78 |
79 | td.info-col {
80 | white-space: nowrap;
81 | overflow: hidden;
82 | text-overflow: ellipsis;
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/app/debug/ls-monitor/object-highlight.pipe.ts:
--------------------------------------------------------------------------------
1 | import {Pipe, PipeTransform} from '@angular/core';
2 |
3 | import hljs from 'highlight.js/lib/core';
4 |
5 | @Pipe({name: 'objectHighlight'})
6 | export class ObjectHighlightPipe implements PipeTransform {
7 |
8 | transform(value: unknown): string {
9 | return hljs.highlight(JSON.stringify(value, undefined, 2), {
10 | language: 'json',
11 | ignoreIllegals: true
12 | }).value;
13 | }
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/src/app/debug/pmlog/control/control.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Context |
6 | Level |
7 |
8 |
9 |
10 |
11 |
12 |
13 | |
14 |
15 |
16 |
20 | |
21 |
22 |
23 |
24 |
25 | |
26 |
27 |
31 | |
32 |
33 |
34 |
35 |
36 |
40 |
--------------------------------------------------------------------------------
/src/app/debug/pmlog/control/control.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/app/debug/pmlog/control/control.component.scss
--------------------------------------------------------------------------------
/src/app/debug/pmlog/pmlog.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
8 |
9 |
12 |
16 |
17 |
18 |
19 |
20 |
24 |
25 |
--------------------------------------------------------------------------------
/src/app/debug/pmlog/pmlog.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/app/debug/pmlog/pmlog.component.scss
--------------------------------------------------------------------------------
/src/app/debug/pmlog/set-context/set-context.component.html:
--------------------------------------------------------------------------------
1 |
14 |
18 |
--------------------------------------------------------------------------------
/src/app/debug/pmlog/set-context/set-context.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/app/debug/pmlog/set-context/set-context.component.scss
--------------------------------------------------------------------------------
/src/app/debug/pmlog/set-context/set-context.component.ts:
--------------------------------------------------------------------------------
1 | import {Component} from '@angular/core';
2 | import {NgbActiveModal, NgbModal} from "@ng-bootstrap/ng-bootstrap";
3 | import {UntypedFormBuilder, UntypedFormGroup} from "@angular/forms";
4 | import {LOG_LEVELS} from "../control/control.component";
5 | import {PrefLogLevel} from "../../../core/services/remote-log.service";
6 |
7 | @Component({
8 | selector: 'app-pmlog-set-context',
9 | templateUrl: './set-context.component.html',
10 | styleUrls: ['./set-context.component.scss']
11 | })
12 | export class SetContextComponent {
13 | public formGroup: UntypedFormGroup;
14 | public logLevels = LOG_LEVELS;
15 |
16 | constructor(public modal: NgbActiveModal, fb: UntypedFormBuilder) {
17 | this.formGroup = fb.group({
18 | context: [''],
19 | level: ['none'],
20 | })
21 | }
22 |
23 | public static async prompt(modals: NgbModal): Promise {
24 | return modals.open(SetContextComponent).result.catch(() => undefined);
25 | }
26 |
27 | }
28 |
29 | export declare class SetContextResult {
30 | context: string;
31 | level: PrefLogLevel;
32 | }
33 |
--------------------------------------------------------------------------------
/src/app/devices/devices.component.html:
--------------------------------------------------------------------------------
1 |
22 |
--------------------------------------------------------------------------------
/src/app/devices/devices.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/app/devices/devices.component.scss
--------------------------------------------------------------------------------
/src/app/devices/devices.component.ts:
--------------------------------------------------------------------------------
1 | import {Component, Inject, Optional} from '@angular/core';
2 | import {DeviceManagerService} from "../core/services";
3 | import {Observable} from "rxjs";
4 | import {Device, NewDevice} from "../types";
5 | import {AsyncPipe} from "@angular/common";
6 | import {NgbCollapse, NgbModal} from "@ng-bootstrap/ng-bootstrap";
7 | import {AddDeviceModule} from "../add-device/add-device.module";
8 | import {InlineEditorComponent} from "./inline-editor/inline-editor.component";
9 | import {HomeComponent} from "../home/home.component";
10 | import {RemoveConfirmation, RemoveDeviceComponent} from "../remove-device/remove-device.component";
11 |
12 | @Component({
13 | selector: 'app-devices',
14 | standalone: true,
15 | imports: [
16 | AsyncPipe,
17 | AddDeviceModule,
18 | NgbCollapse,
19 | InlineEditorComponent
20 | ],
21 | templateUrl: './devices.component.html',
22 | styleUrl: './devices.component.scss'
23 | })
24 | export class DevicesComponent {
25 | public devices$: Observable;
26 |
27 | editingDevice: Device | undefined;
28 |
29 | constructor(
30 | @Optional() @Inject(HomeComponent) public home: HomeComponent,
31 | public deviceManager: DeviceManagerService,
32 | private modals: NgbModal,
33 | ) {
34 | this.devices$ = deviceManager.devices$;
35 | }
36 |
37 | async deleteDevice(device: Device) {
38 | let answer: RemoveConfirmation;
39 | try {
40 | let a = await RemoveDeviceComponent.confirm(this.modals, device);
41 | if (!a) {
42 | return;
43 | }
44 | answer = a;
45 | } catch (e) {
46 | return;
47 | }
48 | await this.deviceManager.removeDevice(device.name, answer.deleteSshKey);
49 | this.editingDevice = undefined;
50 | }
51 |
52 | async saveDevice(device: NewDevice) {
53 | await this.deviceManager.addDevice(device);
54 | this.editingDevice = undefined;
55 | }
56 |
57 | addDevice() {
58 | this.home?.openSetupDevice(true);
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/app/devices/inline-editor/inline-editor.component.html:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
9 |
12 |
15 |
16 |
--------------------------------------------------------------------------------
/src/app/devices/inline-editor/inline-editor.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/app/devices/inline-editor/inline-editor.component.scss
--------------------------------------------------------------------------------
/src/app/devices/inline-editor/inline-editor.component.spec.ts:
--------------------------------------------------------------------------------
1 | import {ComponentFixture, TestBed} from '@angular/core/testing';
2 |
3 | import {InlineEditorComponent} from './inline-editor.component';
4 | import {Device} from "../../types";
5 |
6 | describe('InlineEditorComponent', async () => {
7 | let component: InlineEditorComponent;
8 | let fixture: ComponentFixture;
9 |
10 | beforeEach(async () => {
11 | await TestBed.configureTestingModule({
12 | imports: [InlineEditorComponent]
13 | }).compileComponents();
14 |
15 | fixture = TestBed.createComponent(InlineEditorComponent);
16 | component = fixture.componentInstance;
17 | component.device = {
18 | name: 'test',
19 | host: '192.168.1.1',
20 | port: 22,
21 | username: 'root',
22 | profile: 'ose',
23 | privateKey: {openSsh: 'test'},
24 | };
25 | fixture.detectChanges();
26 | });
27 |
28 | it('should create', () => {
29 | expect(component).toBeTruthy();
30 | });
31 | });
32 |
--------------------------------------------------------------------------------
/src/app/devices/inline-editor/inline-editor.component.ts:
--------------------------------------------------------------------------------
1 | import {Component, EventEmitter, Input, Output} from '@angular/core';
2 | import {AppPrivKeyName, Device, NewDevice, NewDeviceAuthentication} from "../../types";
3 | import {
4 | DeviceEditorComponent,
5 | OpenSshLocalKeyValue,
6 | SetupAuthInfoUnion
7 | } from "../../add-device/device-editor/device-editor.component";
8 |
9 | @Component({
10 | selector: 'app-device-inline-editor',
11 | standalone: true,
12 | templateUrl: './inline-editor.component.html',
13 | imports: [
14 | DeviceEditorComponent
15 | ],
16 | styleUrl: './inline-editor.component.scss'
17 | })
18 | export class InlineEditorComponent {
19 | @Input()
20 | device!: Device;
21 |
22 | @Output()
23 | save: EventEmitter = new EventEmitter();
24 |
25 | @Output()
26 | remove: EventEmitter = new EventEmitter();
27 |
28 | @Output()
29 | closed: EventEmitter = new EventEmitter();
30 |
31 | get deviceAuth(): SetupAuthInfoUnion {
32 | if (this.device.password) {
33 | return {type: NewDeviceAuthentication.Password, value: this.device.password};
34 | } else if (this.device.username === 'prisoner') {
35 | return {
36 | type: NewDeviceAuthentication.DevKey,
37 | value: this.device.passphrase!
38 | };
39 | } else if (this.device.privateKey?.openSsh === AppPrivKeyName) {
40 | return {
41 | type: NewDeviceAuthentication.AppKey,
42 | value: null,
43 | };
44 | } else {
45 | return {
46 | type: NewDeviceAuthentication.LocalKey,
47 | value: new OpenSshLocalKeyValue(this.device.privateKey!.openSsh, this.device.passphrase),
48 | }
49 | }
50 | }
51 |
52 | doSave(editor: DeviceEditorComponent) {
53 | editor.submit().then(dev => this.save.emit(dev));
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/app/files/attrs-permissions.pipe.ts:
--------------------------------------------------------------------------------
1 | import {Pipe, PipeTransform} from '@angular/core';
2 |
3 | @Pipe({
4 | name: 'attrsPermissions'
5 | })
6 | export class AttrsPermissionsPipe implements PipeTransform {
7 |
8 | transform(mode: number): string {
9 | return `${str((mode >> 6) & 7)}${str((mode >> 3) & 7)}${str(mode & 7)}`;
10 | }
11 |
12 | }
13 |
14 | function str(bits: number): string {
15 | return `${bits & 4 ? 'r' : '-'}${bits & 2 ? 'w' : '-'}${bits & 1 ? 'x' : '-'}`;
16 | }
17 |
--------------------------------------------------------------------------------
/src/app/files/create-directory-message/create-directory-message.component.html:
--------------------------------------------------------------------------------
1 |
3 |
--------------------------------------------------------------------------------
/src/app/files/create-directory-message/create-directory-message.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/app/files/create-directory-message/create-directory-message.component.scss
--------------------------------------------------------------------------------
/src/app/files/create-directory-message/create-directory-message.component.ts:
--------------------------------------------------------------------------------
1 | import {Component} from '@angular/core';
2 | import {MessageDialogComponent} from "../../shared/components/message-dialog/message-dialog.component";
3 | import {FormControl, Validators} from "@angular/forms";
4 |
5 | @Component({
6 | selector: 'app-create-directory-message',
7 | templateUrl: './create-directory-message.component.html',
8 | styleUrls: ['./create-directory-message.component.scss']
9 | })
10 | export class CreateDirectoryMessageComponent {
11 | public formControl: FormControl;
12 |
13 | constructor(private parent: MessageDialogComponent) {
14 | this.formControl = new FormControl('', {
15 | nonNullable: true,
16 | validators: [
17 | Validators.required,
18 | Validators.pattern(/^[^\\/:*?"<>|]+$/),
19 | Validators.pattern(/[^.]$/)
20 | ]
21 | });
22 | this.formControl.statusChanges.subscribe((v) => {
23 | parent.positiveDisabled = v !== 'VALID';
24 | });
25 | parent.positiveDisabled = this.formControl.invalid;
26 | parent.positiveAction = () => this.formControl.value;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/app/files/files-routing.module.ts:
--------------------------------------------------------------------------------
1 | import {NgModule} from '@angular/core';
2 | import {RouterModule, Routes} from '@angular/router';
3 | import {FilesComponent} from "./files.component";
4 |
5 | const routes: Routes = [{
6 | path: '',
7 | component: FilesComponent,
8 | }];
9 |
10 | @NgModule({
11 | imports: [RouterModule.forChild(routes)],
12 | exports: [RouterModule]
13 | })
14 | export class FilesRoutingModule {
15 | }
16 |
--------------------------------------------------------------------------------
/src/app/files/files-table/files-table.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Name |
5 | Last Modified |
6 | Size |
7 | Permission |
8 | Owner |
9 | Group |
10 |
11 |
12 |
13 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | {{ file.filename }}
27 |
28 |
29 | {{ link.target ?? '?' }}
30 |
31 | |
32 | {{ file.mtime * 1000 | date:'short' }}
33 | |
34 |
35 |
36 | {{ file.size | filesize:sizeOptions }}
37 |
38 | -
39 | |
40 | {{ file.mode }} |
41 |
42 | {{ file.user }}
43 | |
44 |
45 | {{ file.group }}
46 | |
47 |
48 |
49 |
50 | -
51 |
--------------------------------------------------------------------------------
/src/app/files/files-table/files-table.component.scss:
--------------------------------------------------------------------------------
1 | table {
2 | th {
3 | position: sticky;
4 | top: 0;
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/src/app/files/files.component.scss:
--------------------------------------------------------------------------------
1 | .breadcrumb-bar {
2 | nav {
3 | width: fit-content;
4 | --bs-breadcrumb-divider: '';
5 | }
6 | }
7 |
8 | .stat-bar {
9 | .breadcrumb {
10 | flex-wrap: nowrap;
11 | align-items: center;
12 | }
13 |
14 | .breadcrumb-item {
15 | position: relative;
16 | }
17 | }
18 |
19 | .loading, .loading * {
20 | cursor: progress;
21 | }
22 |
--------------------------------------------------------------------------------
/src/app/files/files.module.ts:
--------------------------------------------------------------------------------
1 | import {NgModule} from '@angular/core';
2 | import {CommonModule} from '@angular/common';
3 |
4 | import {FilesRoutingModule} from './files-routing.module';
5 | import {FilesComponent} from "./files.component";
6 | import {AttrsPermissionsPipe} from "./attrs-permissions.pipe";
7 | import {
8 | NgbDropdown,
9 | NgbDropdownItem,
10 | NgbDropdownMenu,
11 | NgbDropdownToggle,
12 | NgbTooltipModule
13 | } from "@ng-bootstrap/ng-bootstrap";
14 | import {FilesTableComponent} from './files-table/files-table.component';
15 | import {SharedModule} from "../shared/shared.module";
16 | import {CreateDirectoryMessageComponent} from './create-directory-message/create-directory-message.component';
17 | import {ReactiveFormsModule} from "@angular/forms";
18 |
19 |
20 | @NgModule({
21 | declarations: [
22 | FilesComponent,
23 | AttrsPermissionsPipe,
24 | FilesTableComponent,
25 | CreateDirectoryMessageComponent,
26 | ],
27 | imports: [
28 | CommonModule,
29 | FilesRoutingModule,
30 | NgbTooltipModule,
31 | SharedModule,
32 | NgbDropdown,
33 | NgbDropdownItem,
34 | NgbDropdownMenu,
35 | NgbDropdownToggle,
36 | ReactiveFormsModule,
37 | ]
38 | })
39 | export class FilesModule {
40 | }
41 |
--------------------------------------------------------------------------------
/src/app/files/index.ts:
--------------------------------------------------------------------------------
1 | export {FilesModule} from './files.module';
2 |
--------------------------------------------------------------------------------
/src/app/home/device-chooser/device-chooser.component.html:
--------------------------------------------------------------------------------
1 |
4 |
5 | -
7 | {{ device.name }}
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/app/home/device-chooser/device-chooser.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/app/home/device-chooser/device-chooser.component.scss
--------------------------------------------------------------------------------
/src/app/home/device-chooser/device-chooser.component.ts:
--------------------------------------------------------------------------------
1 | import {Component} from '@angular/core';
2 | import {NgbActiveModal} from "@ng-bootstrap/ng-bootstrap";
3 | import {DeviceManagerService} from "../../core/services";
4 | import {AsyncPipe, NgForOf} from "@angular/common";
5 |
6 | @Component({
7 | selector: 'app-device-chooser',
8 | standalone: true,
9 | imports: [
10 | AsyncPipe,
11 | NgForOf
12 | ],
13 | templateUrl: './device-chooser.component.html',
14 | styleUrl: './device-chooser.component.scss'
15 | })
16 | export class DeviceChooserComponent {
17 | constructor(
18 | public modal: NgbActiveModal,
19 | public deviceManager: DeviceManagerService,
20 | ) {
21 |
22 | }
23 |
24 | protected readonly parent = parent;
25 | }
26 |
--------------------------------------------------------------------------------
/src/app/home/home.component.scss:
--------------------------------------------------------------------------------
1 | .nav {
2 | .nav-item {
3 | margin: 5px;
4 |
5 | .nav-link {
6 | padding: 5px 12px;
7 | i.bi {
8 | font-size: 30px;
9 | }
10 | }
11 | }
12 |
13 | .device-check {
14 | width: 1em;
15 | }
16 |
17 | .dropdown-toggle:after {
18 | display: none;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/app/home/nav-more/nav-more.component.html:
--------------------------------------------------------------------------------
1 |
2 |
Device tools
3 |
8 |
Settings
9 |
14 |
About this app
15 |
16 | -
17 |
18 |
19 |
20 |
22 | GitHub
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/app/home/nav-more/nav-more.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/app/home/nav-more/nav-more.component.scss
--------------------------------------------------------------------------------
/src/app/home/nav-more/nav-more.component.ts:
--------------------------------------------------------------------------------
1 | import {Component} from '@angular/core';
2 | import {ActivatedRoute, RouterLink} from "@angular/router";
3 | import {HomeComponent} from "../home.component";
4 | import ReleaseInfo from '../../../release.json';
5 | import {SharedModule} from "../../shared/shared.module";
6 | import {ExternalLinkDirective} from "../../shared/directives";
7 |
8 | @Component({
9 | selector: 'app-nav-more',
10 | standalone: true,
11 | imports: [
12 | RouterLink,
13 | SharedModule,
14 | ExternalLinkDirective
15 | ],
16 | templateUrl: './nav-more.component.html',
17 | styleUrl: './nav-more.component.scss'
18 | })
19 | export class NavMoreComponent {
20 | homeRoute: ActivatedRoute | null;
21 | readonly appVersion: string;
22 |
23 | constructor(
24 | public route: ActivatedRoute,
25 | public parent: HomeComponent,
26 | ) {
27 | this.homeRoute = route.parent;
28 | this.appVersion = ReleaseInfo.version;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/app/info/devmode-countdown.pipe.ts:
--------------------------------------------------------------------------------
1 | import {Pipe, PipeTransform} from '@angular/core';
2 | import {Observable, of, timer} from "rxjs";
3 | import {DateTime, Duration, DurationLikeObject} from "luxon";
4 | import {map} from "rxjs/operators";
5 |
6 | @Pipe({
7 | name: 'devmodeCountdown'
8 | })
9 | export class DevmodeCountdownPipe implements PipeTransform {
10 |
11 | transform(value?: string): Observable {
12 | const remainingMatches = RegExp(/^(?\d+):(?\d+):(?\d+)$/)
13 | .exec(value ?? '');
14 | if (remainingMatches) {
15 | const expireDate = DateTime.now().plus(Duration.fromDurationLike(remainingMatches.groups as
16 | Pick));
17 | return timer(0, 1000).pipe(map(() => expireDate
18 | .diffNow('seconds').toFormat('hh:mm:ss')));
19 | } else {
20 | return of("--:--");
21 | }
22 | }
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/src/app/info/index.ts:
--------------------------------------------------------------------------------
1 | export {InfoModule} from './info.module';
2 |
--------------------------------------------------------------------------------
/src/app/info/info-routing.module.ts:
--------------------------------------------------------------------------------
1 | import {NgModule} from '@angular/core';
2 | import {RouterModule, Routes} from '@angular/router';
3 | import {InfoComponent} from "./info.component";
4 |
5 | const routes: Routes = [{
6 | path: '',
7 | component: InfoComponent,
8 | }];
9 |
10 | @NgModule({
11 | imports: [RouterModule.forChild(routes)],
12 | exports: [RouterModule]
13 | })
14 | export class InfoRoutingModule {
15 | }
16 |
--------------------------------------------------------------------------------
/src/app/info/info.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/app/info/info.component.scss
--------------------------------------------------------------------------------
/src/app/info/info.module.ts:
--------------------------------------------------------------------------------
1 | import {NgModule} from '@angular/core';
2 | import {CommonModule} from '@angular/common';
3 |
4 | import {InfoRoutingModule} from './info-routing.module';
5 | import {InfoComponent} from "./info.component";
6 | import {SharedModule} from "../shared/shared.module";
7 | import {DevmodeCountdownPipe} from './devmode-countdown.pipe';
8 | import {NgbDropdownModule} from "@ng-bootstrap/ng-bootstrap";
9 | import {FormsModule} from "@angular/forms";
10 | import {ExternalLinkDirective} from "../shared/directives";
11 |
12 |
13 | @NgModule({
14 | declarations: [
15 | InfoComponent,
16 | DevmodeCountdownPipe,
17 | ],
18 | imports: [
19 | CommonModule,
20 | InfoRoutingModule,
21 | SharedModule,
22 | NgbDropdownModule,
23 | FormsModule,
24 | ExternalLinkDirective,
25 | ]
26 | })
27 | export class InfoModule {
28 | }
29 |
--------------------------------------------------------------------------------
/src/app/info/renew-script/renew-script.component.scss:
--------------------------------------------------------------------------------
1 | pre, code {
2 | white-space: pre-wrap;
3 | user-select: all;
4 | cursor: text;
5 | }
6 |
7 | // TODO: Remove after BS 5
8 | .top-0 {
9 | top: 0 !important;
10 | }
11 |
12 | .right-0 {
13 | right: 0 !important;
14 | }
15 |
--------------------------------------------------------------------------------
/src/app/info/renew-script/renew-script.component.ts:
--------------------------------------------------------------------------------
1 | import {Component, Inject, OnInit} from '@angular/core';
2 | import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap';
3 | import {Device} from '../../types';
4 | import {DeviceManagerService, DevModeStatus} from '../../core/services';
5 | import {noop} from 'rxjs';
6 | import {save as showSaveDialog} from '@tauri-apps/plugin-dialog'
7 | import {writeTextFile} from '@tauri-apps/plugin-fs';
8 | import renewScriptTemplate from './renew-script.sh';
9 | import Mustache from 'mustache';
10 |
11 | @Component({
12 | selector: 'app-renew-script',
13 | templateUrl: './renew-script.component.html',
14 | styleUrls: ['./renew-script.component.scss']
15 | })
16 | export class RenewScriptComponent implements OnInit {
17 |
18 | public renewScriptContent?: string;
19 |
20 | constructor(
21 | public modal: NgbActiveModal,
22 | private deviceManager: DeviceManagerService,
23 | @Inject('device') public device: Device,
24 | @Inject('devMode') public devMode: DevModeStatus,
25 | ) {
26 | }
27 |
28 | ngOnInit(): void {
29 | this.deviceManager.readPrivKey(this.device).then(key => {
30 | this.renewScriptContent = Mustache.render(renewScriptTemplate, {
31 | device: this.device,
32 | keyContent: key.trim(),
33 | }, undefined, {
34 | escape: (v) => v,
35 | });
36 | });
37 | }
38 |
39 | async copyScript(content: string): Promise {
40 | await navigator.clipboard.writeText(content);
41 | }
42 |
43 | saveScript(content: string): void {
44 | showSaveDialog({
45 | defaultPath: `renew-devmode-${this.device.name}.sh`
46 | }).then(value => {
47 | if (!value) {
48 | return;
49 | }
50 | return writeTextFile(value, content);
51 | }).catch(noop);
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/app/info/renew-script/renew-script.sh.ts:
--------------------------------------------------------------------------------
1 | // language=shell
2 | export default `#!/bin/sh
3 |
4 | # WARNING: Do not run this as root!
5 | # If $SESSION_TOKEN_CACHE is a symlink, its target wil be overwritten.
6 |
7 | DEVICE_NAME='{{device.name}}'
8 | DEVICE_HOST='{{device.host}}'
9 | DEVICE_PORT='{{device.port}}'
10 | DEVICE_USERNAME='{{device.username}}'
11 | DEVICE_PASSPHRASE='{{device.passphrase}}'
12 |
13 | umask 077
14 |
15 | if ! TEMP_KEY_DIR="$(mktemp -d)"; then
16 | echo "Failed to create random temporary directory for key; using fallback" >&2
17 | TEMP_KEY_DIR="/tmp/renew-script.$$"
18 | if ! mkdir "\${TEMP_KEY_DIR}"; then
19 | echo "Fallback temporary directory \${TEMP_KEY_DIR} already exists" >&2
20 | exit 1
21 | fi
22 | fi
23 |
24 | PRIV_KEY_FILE="\${TEMP_KEY_DIR}/webos_privkey_\${DEVICE_NAME}"
25 |
26 | cat >"\${PRIV_KEY_FILE}" <&2
47 | SESSION_TOKEN=$(cat "\${SESSION_TOKEN_CACHE}")
48 | else
49 | echo "Got SESSION_TOKEN from TV - writing to \${SESSION_TOKEN_CACHE}" >&2
50 | echo "$SESSION_TOKEN" >"\${SESSION_TOKEN_CACHE}"
51 | fi
52 |
53 | if [ -z "$SESSION_TOKEN" ]; then
54 | echo "Unable to get token" >&2
55 | exit 1
56 | fi
57 |
58 | CHECK_RESULT=$(curl --max-time 3 -s "https://developer.lge.com/secure/ResetDevModeSession.dev?sessionToken=$SESSION_TOKEN")
59 |
60 | echo "\${CHECK_RESULT}"`;
61 |
--------------------------------------------------------------------------------
/src/app/remove-device/remove-device.component.html:
--------------------------------------------------------------------------------
1 |
4 |
5 |
Delete "{{ device.name }}"?
6 | @if (canDeleteSshKey) {
7 |
8 |
9 |
10 |
11 | }
12 |
13 |
17 |
--------------------------------------------------------------------------------
/src/app/remove-device/remove-device.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/app/remove-device/remove-device.component.scss
--------------------------------------------------------------------------------
/src/app/remove-device/remove-device.component.ts:
--------------------------------------------------------------------------------
1 | import {Component, Inject, Injector} from '@angular/core';
2 | import {NgbActiveModal, NgbModal} from "@ng-bootstrap/ng-bootstrap";
3 | import {Device} from "../types";
4 |
5 | @Component({
6 | selector: 'app-remove-device',
7 | templateUrl: './remove-device.component.html',
8 | styleUrls: ['./remove-device.component.scss']
9 | })
10 | export class RemoveDeviceComponent {
11 |
12 | public deleteSshKey: boolean = false;
13 |
14 | constructor(@Inject('device') public device: Device, public modal: NgbActiveModal) {
15 | }
16 |
17 | get canDeleteSshKey(): boolean {
18 | return this.device.privateKey?.openSsh?.startsWith("webos_") === true;
19 | }
20 |
21 | confirmDeletion() {
22 | this.modal.close({
23 | deleteSshKey: this.deleteSshKey,
24 | });
25 | }
26 |
27 | static confirm(service: NgbModal, device: Device): Promise {
28 | return service.open(RemoveDeviceComponent, {
29 | centered: true,
30 | size: 'lg',
31 | scrollable: true,
32 | injector: Injector.create({
33 | providers: [{provide: 'device', useValue: device}]
34 | })
35 | }).result.catch(() => null);
36 | }
37 | }
38 |
39 | export interface RemoveConfirmation {
40 | deleteSshKey: boolean;
41 | }
42 |
--------------------------------------------------------------------------------
/src/app/shared/components/error-card/error-card.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | @if (title) {
4 |
{{ title }}
5 | }
6 |
{{ error.message }}
7 | @let details = $any(error).details;
8 | @if (details) {
9 |
{{ details }}
10 | }
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/app/shared/components/error-card/error-card.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/app/shared/components/error-card/error-card.component.scss
--------------------------------------------------------------------------------
/src/app/shared/components/error-card/error-card.component.ts:
--------------------------------------------------------------------------------
1 | import {Component, EventEmitter, Input, Output} from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-error-card',
5 | templateUrl: './error-card.component.html',
6 | styleUrls: ['./error-card.component.scss']
7 | })
8 | export class ErrorCardComponent {
9 | @Input()
10 | title?: string;
11 |
12 | @Input()
13 | error!: Error;
14 |
15 | @Output()
16 | retry: EventEmitter = new EventEmitter();
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/src/app/shared/components/loading-card/loading-card.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Loading...
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/app/shared/components/loading-card/loading-card.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/app/shared/components/loading-card/loading-card.component.scss
--------------------------------------------------------------------------------
/src/app/shared/components/loading-card/loading-card.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-loading-card',
5 | templateUrl: './loading-card.component.html',
6 | styleUrls: ['./loading-card.component.scss']
7 | })
8 | export class LoadingCardComponent {
9 |
10 | }
11 |
--------------------------------------------------------------------------------
/src/app/shared/components/message-dialog/message-dialog.component.html:
--------------------------------------------------------------------------------
1 | @if (title) {
2 |
5 | }
6 |
7 | @switch (messageType) {
8 | @case ('string') {
9 |
{{ message }}
10 | }
11 | @case ('component') {
12 |
13 | }
14 | }
15 | @let details = error && $any(error).details;
16 | @if (details) {
17 |
18 |
19 |
{{ details }}
20 |
21 | }
22 |
23 |
42 |
--------------------------------------------------------------------------------
/src/app/shared/components/message-dialog/message-dialog.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/app/shared/components/message-dialog/message-dialog.component.scss
--------------------------------------------------------------------------------
/src/app/shared/components/message-dialog/message-trace/message-trace.component.html:
--------------------------------------------------------------------------------
1 |
2 | {{ message }}
3 |
4 | Details
5 |
6 |
7 |
8 |
9 |
10 | {{ error }}
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/app/shared/components/message-dialog/message-trace/message-trace.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/app/shared/components/message-dialog/message-trace/message-trace.component.scss
--------------------------------------------------------------------------------
/src/app/shared/components/message-dialog/message-trace/message-trace.component.ts:
--------------------------------------------------------------------------------
1 | import {Component} from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-message-trace',
5 | templateUrl: './message-trace.component.html',
6 | styleUrls: ['./message-trace.component.scss']
7 | })
8 | export class MessageTraceComponent {
9 |
10 | message: string = '';
11 | error: any;
12 | detailsCollapsed: boolean = true;
13 |
14 | constructor() {
15 | }
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/src/app/shared/components/page-not-found/page-not-found.component.html:
--------------------------------------------------------------------------------
1 |
2 | page-not-found works!
3 |
4 |
--------------------------------------------------------------------------------
/src/app/shared/components/page-not-found/page-not-found.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/app/shared/components/page-not-found/page-not-found.component.scss
--------------------------------------------------------------------------------
/src/app/shared/components/page-not-found/page-not-found.component.ts:
--------------------------------------------------------------------------------
1 | import {Component} from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-page-not-found',
5 | templateUrl: './page-not-found.component.html',
6 | styleUrls: ['./page-not-found.component.scss']
7 | })
8 | export class PageNotFoundComponent {
9 | constructor() {
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/app/shared/components/progress-dialog/progress-dialog.component.html:
--------------------------------------------------------------------------------
1 |
2 |
@if (message) {
3 | {{ message }}
4 | } @else {
5 | Please wait
6 | }
7 |
0" class="mt-2">
9 | @if (secondaryProgress !== undefined) {
10 |
0" class="mt-2">
12 |
13 | }
14 |
15 |
--------------------------------------------------------------------------------
/src/app/shared/components/progress-dialog/progress-dialog.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/app/shared/components/progress-dialog/progress-dialog.component.scss
--------------------------------------------------------------------------------
/src/app/shared/components/progress-dialog/progress-dialog.component.ts:
--------------------------------------------------------------------------------
1 | import {Component, NgZone} from '@angular/core';
2 | import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap';
3 |
4 | @Component({
5 | selector: 'app-progress-dialog',
6 | templateUrl: './progress-dialog.component.html',
7 | styleUrls: ['./progress-dialog.component.scss']
8 | })
9 | export class ProgressDialogComponent {
10 |
11 | message?: string;
12 | progress?: number;
13 |
14 | secondaryProgress?: number;
15 |
16 | protected readonly isNaN = isNaN;
17 |
18 | constructor(private zone: NgZone) {
19 | }
20 |
21 | update(message?: string, progress?: number): void {
22 | this.zone.run(() => {
23 | this.message = message;
24 | this.progress = progress;
25 | });
26 | }
27 |
28 | updateSecondary(message?: string, progress?: number): void {
29 | this.zone.run(() => {
30 | this.secondaryProgress = progress;
31 | });
32 | }
33 |
34 | static open(service: NgbModal): NgbModalRef {
35 | return service.open(ProgressDialogComponent, {
36 | centered: true,
37 | backdrop: 'static',
38 | keyboard: false
39 | });
40 | }
41 |
42 | }
43 |
--------------------------------------------------------------------------------
/src/app/shared/components/stat-storage-info/stat-storage-info.component.html:
--------------------------------------------------------------------------------
1 | @if (storage) {
2 |
4 |
5 |
6 | {{ storage.available * 1024 | filesize:sizeOptions }} / {{ storage.total * 1024 | filesize:sizeOptions }} free
7 |
8 |
9 |
11 |
12 | {{ storage.available * 1024 | filesize:sizeOptions }} / {{ storage.total * 1024 | filesize:sizeOptions }} free
13 |
14 |
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/src/app/shared/components/stat-storage-info/stat-storage-info.component.scss:
--------------------------------------------------------------------------------
1 | :host {
2 | position: relative;
3 | }
4 |
5 | small {
6 | font-size: xx-small;
7 | }
8 |
9 | .progress {
10 | height: 1.25em;
11 | }
12 |
13 | .progress-bar {
14 | z-index: 10;
15 | }
16 |
--------------------------------------------------------------------------------
/src/app/shared/components/stat-storage-info/stat-storage-info.component.ts:
--------------------------------------------------------------------------------
1 | import {Component, Input} from '@angular/core';
2 | import {Device, StorageInfo} from "../../../types";
3 | import {DeviceManagerService} from "../../../core/services";
4 | import {FileSizeOptionsBase} from "filesize";
5 |
6 | @Component({
7 | selector: 'app-stat-storage-info',
8 | templateUrl: './stat-storage-info.component.html',
9 | styleUrls: ['./stat-storage-info.component.scss']
10 | })
11 | export class StatStorageInfoComponent {
12 |
13 | private deviceField: Device | null = null;
14 | private locationField: string | null = null;
15 |
16 | storage: StorageInfo | null = null;
17 |
18 | sizeOptions: FileSizeOptionsBase = {round: 0, standard: "jedec"};
19 |
20 | constructor(private service: DeviceManagerService) {
21 |
22 | }
23 |
24 | get device(): Device | null {
25 | return this.deviceField;
26 | }
27 |
28 | @Input()
29 | set device(value: Device | null) {
30 | const changed = this.deviceField !== value;
31 | this.deviceField = value;
32 | if (changed) {
33 | this.storage = null;
34 | this.refresh();
35 | }
36 | }
37 |
38 | get location(): string | null {
39 | return this.locationField;
40 | }
41 |
42 | @Input()
43 | set location(value: string | null) {
44 | const changed = this.locationField !== value;
45 | this.locationField = value;
46 | if (changed) {
47 | this.storage = null;
48 | this.refresh();
49 | }
50 | }
51 |
52 | public refresh(): void {
53 | const device = this.deviceField;
54 | if (!device) {
55 | return;
56 | }
57 | this.service.getStorageInfo(device, this.locationField || undefined)
58 | .catch(() => null).then(info => {
59 | if (!info) {
60 | return;
61 | }
62 | this.storage = info;
63 | });
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/app/shared/components/term-size-calculator/size-calculator.component.html:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/app/shared/components/term-size-calculator/size-calculator.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/app/shared/components/term-size-calculator/size-calculator.component.scss
--------------------------------------------------------------------------------
/src/app/shared/constants.ts:
--------------------------------------------------------------------------------
1 |
2 | export const APP_ID_HBCHANNEL = 'org.webosbrew.hbchannel';
3 |
--------------------------------------------------------------------------------
/src/app/shared/directives/external-link.directive.ts:
--------------------------------------------------------------------------------
1 | import {Directive, HostListener} from '@angular/core';
2 | import {open} from "@tauri-apps/plugin-shell";
3 | import {noop} from "rxjs";
4 |
5 | @Directive({
6 | selector: '[appExternalLink]',
7 | standalone: true
8 | })
9 | export class ExternalLinkDirective {
10 |
11 | @HostListener('click', ['$event'])
12 | onClick(e: Event): boolean {
13 | const href = (e.currentTarget as HTMLAnchorElement)?.href;
14 | if (!href) {
15 | return false;
16 | }
17 | if (open && this.isLinkExternal(href)) {
18 | open(href).then(noop);
19 | return false;
20 | } else {
21 | window.open(href, '_blank');
22 | return false;
23 | }
24 | }
25 |
26 | private isLinkExternal(link: string) {
27 | const url = new URL(link);
28 | if (location.protocol == 'file:' && url.protocol != location.protocol) return true;
29 | return !url.hostname.endsWith(location.hostname);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/app/shared/directives/index.ts:
--------------------------------------------------------------------------------
1 | export * from './external-link.directive';
2 | export * from './search-bar.directive';
3 |
--------------------------------------------------------------------------------
/src/app/shared/directives/search-bar.directive.spec.ts:
--------------------------------------------------------------------------------
1 | import {SearchBarDirective} from './search-bar.directive';
2 | import {Component} from "@angular/core";
3 | import {TestBed} from "@angular/core/testing";
4 |
5 | describe('SearchBarDirective', () => {
6 |
7 | it('should create an instance', () => {
8 | let component = TestBed.createComponent(TestSearchBarDirectiveHostComponent);
9 | });
10 | });
11 |
12 | @Component({
13 | selector: 'app-test-search-bar-directive-host',
14 | template: ``
16 | })
17 | class TestSearchBarDirectiveHostComponent {
18 | }
19 |
--------------------------------------------------------------------------------
/src/app/shared/directives/search-bar.directive.ts:
--------------------------------------------------------------------------------
1 | import {Directive, ElementRef, EventEmitter, HostListener, Input, OnDestroy, OnInit, Output} from '@angular/core';
2 | import {parse as parseQuery, SearchParserResult} from 'search-query-parser';
3 | import {debounceTime, Subject, Subscription} from "rxjs";
4 |
5 | @Directive({
6 | selector: 'input[appSearchBar]',
7 | standalone: true,
8 | })
9 | export class SearchBarDirective implements OnInit, OnDestroy {
10 |
11 | @Output()
12 | query: EventEmitter = new EventEmitter();
13 |
14 | private keywordsField: string[] = [];
15 | private rangesField: string[] = [];
16 | private emitChanges: Subject = new Subject();
17 | private changesSubscription!: Subscription;
18 |
19 | constructor(private hostRef: ElementRef) {
20 | }
21 |
22 |
23 | @Input()
24 | set keywords(value: string | undefined) {
25 | this.keywordsField = value?.split(',') || [];
26 | this.emitChanges.next();
27 | }
28 |
29 | @Input()
30 | set ranges(value: string | undefined) {
31 | this.rangesField = value?.split(',') || [];
32 | this.emitChanges.next();
33 | }
34 |
35 | ngOnInit(): void {
36 | this.changesSubscription = this.emitChanges.pipe(debounceTime(50)).subscribe(() => this.emitChange());
37 | this.emitChanges.next();
38 | }
39 |
40 | ngOnDestroy() {
41 | this.changesSubscription.unsubscribe();
42 | this.emitChanges.complete();
43 | }
44 |
45 | @HostListener('change')
46 | inputChanged(): void {
47 | this.emitChanges.next();
48 | }
49 |
50 | private emitChange(): void {
51 | this.query.emit(parseQuery(this.hostRef.nativeElement?.value || '', {
52 | keywords: this.keywordsField,
53 | ranges: this.rangesField,
54 | alwaysArray: true,
55 | tokenize: true,
56 | }));
57 | }
58 |
59 | }
60 |
61 |
62 | export type TokenizedSearchParserResult = SearchParserResult & { text?: string[] };
63 |
--------------------------------------------------------------------------------
/src/app/shared/operators.ts:
--------------------------------------------------------------------------------
1 | export function isNonNull(value: T): value is NonNullable {
2 | return value != null;
3 | }
4 |
--------------------------------------------------------------------------------
/src/app/shared/pipes/filesize.pipe.ts:
--------------------------------------------------------------------------------
1 | import {Pipe, PipeTransform} from "@angular/core";
2 | import {filesize, FileSizeOptionsBase} from 'filesize';
3 |
4 | @Pipe({
5 | name: 'filesize'
6 | })
7 | export class FilesizePipe implements PipeTransform {
8 |
9 | transform(bytes: number, options: Partial): string {
10 | return filesize(bytes, {output: "string", ...options});
11 | }
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/src/app/shared/pipes/trust-uri.pipe.ts:
--------------------------------------------------------------------------------
1 | import {Pipe, PipeTransform} from "@angular/core";
2 | import {DomSanitizer} from '@angular/platform-browser';
3 |
4 | @Pipe({
5 | name: 'trustUri'
6 | })
7 | export class TrustUriPipe implements PipeTransform {
8 |
9 | constructor(private sanitizer: DomSanitizer) {
10 | }
11 |
12 | transform(uri?: string) {
13 | return uri && this.sanitizer.bypassSecurityTrustUrl(uri);
14 | }
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/src/app/shared/shared.module.ts:
--------------------------------------------------------------------------------
1 | import {CommonModule} from '@angular/common';
2 | import {NgModule} from '@angular/core';
3 | import {FormsModule} from '@angular/forms';
4 | import {PageNotFoundComponent} from './components/page-not-found/page-not-found.component';
5 | import {TrustUriPipe} from './pipes/trust-uri.pipe';
6 | import {MessageDialogComponent} from './components/message-dialog/message-dialog.component';
7 | import {ProgressDialogComponent} from './components/progress-dialog/progress-dialog.component';
8 | import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
9 | import {MessageTraceComponent} from './components/message-dialog/message-trace/message-trace.component';
10 | import {ErrorCardComponent} from './components/error-card/error-card.component';
11 | import {ExternalLinkDirective} from "./directives";
12 | import {LoadingCardComponent} from './components/loading-card/loading-card.component';
13 | import {StatStorageInfoComponent} from './components/stat-storage-info/stat-storage-info.component';
14 | import {FilesizePipe} from "./pipes/filesize.pipe";
15 |
16 | @NgModule({
17 | declarations: [
18 | PageNotFoundComponent,
19 | TrustUriPipe,
20 | FilesizePipe,
21 | MessageDialogComponent,
22 | ProgressDialogComponent,
23 | MessageTraceComponent,
24 | ErrorCardComponent,
25 | LoadingCardComponent,
26 | StatStorageInfoComponent,
27 | ],
28 | imports: [CommonModule, FormsModule, NgbModule,
29 | ExternalLinkDirective],
30 | exports: [
31 | PageNotFoundComponent,
32 | TrustUriPipe,
33 | FilesizePipe,
34 | MessageDialogComponent,
35 | ProgressDialogComponent,
36 | MessageTraceComponent,
37 | ErrorCardComponent,
38 | LoadingCardComponent,
39 | StatStorageInfoComponent
40 | ]
41 | })
42 | export class SharedModule {
43 | }
44 |
--------------------------------------------------------------------------------
/src/app/shared/xterm/config.ts:
--------------------------------------------------------------------------------
1 | import {ITerminalOptions} from "@xterm/xterm";
2 |
3 | export const TERMINAL_CONFIG: Partial = {
4 | fontFamily: 'monospace',
5 | };
6 |
--------------------------------------------------------------------------------
/src/app/shared/xterm/web-links.ts:
--------------------------------------------------------------------------------
1 | import {WebLinksAddon} from "@xterm/addon-web-links";
2 | import {open} from "@tauri-apps/plugin-shell";
3 | import {noop} from "rxjs";
4 |
5 | export class AppWebLinksAddon extends WebLinksAddon {
6 | constructor() {
7 | super((event, uri) => {
8 | event.preventDefault();
9 | open(uri).then(noop);
10 | });
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/app/terminal/dumb/dumb.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Interactive terminal is not supported for this connection. Check
here for
5 | more info.
6 |
7 | @for (item of logs; track item.id) {
8 |
9 |
10 |
11 |
{{ item.input }}
12 |
13 |
14 |
15 |
{{ item.output }}
16 |
17 |
18 |
19 | }
20 |
21 |
22 |
26 | @if (working) {
27 |
28 | Executing...
29 |
30 | }
31 |
32 |
33 |
--------------------------------------------------------------------------------
/src/app/terminal/dumb/dumb.component.scss:
--------------------------------------------------------------------------------
1 | .container {
2 | font-family: monospace;
3 | color: white;
4 | overflow-y: scroll;
5 | }
6 |
7 | .logs {
8 | }
9 |
10 | .prompt {
11 | textarea {
12 | resize: none;
13 | background: none;
14 | border: none;
15 | font-size: 0.875rem;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/app/terminal/index.ts:
--------------------------------------------------------------------------------
1 | export {TerminalModule} from './terminal.module';
2 |
--------------------------------------------------------------------------------
/src/app/terminal/pty/pty.component.html:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/app/terminal/pty/pty.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/app/terminal/pty/pty.component.scss
--------------------------------------------------------------------------------
/src/app/terminal/terminal-routing.module.ts:
--------------------------------------------------------------------------------
1 | import {NgModule} from '@angular/core';
2 | import {RouterModule, Routes} from '@angular/router';
3 | import {TerminalComponent} from "./terminal.component";
4 |
5 | const routes: Routes = [{
6 | path: '',
7 | component: TerminalComponent
8 | }];
9 |
10 | @NgModule({
11 | imports: [RouterModule.forChild(routes)],
12 | exports: [RouterModule]
13 | })
14 | export class TerminalRoutingModule {
15 | }
16 |
--------------------------------------------------------------------------------
/src/app/terminal/terminal.component.scss:
--------------------------------------------------------------------------------
1 | .tabs-container {
2 | background-color: black;
3 | }
4 |
5 | ul.terminal-tabs {
6 | overflow-y: hidden;
7 | overflow-x: scroll;
8 | -webkit-overflow-scrolling: touch;
9 | &::-webkit-scrollbar {
10 | display: none;
11 | }
12 | }
13 |
14 | .terminal-tab-page {
15 | position: absolute;
16 | margin: auto;
17 | left: 0;
18 | top: 0;
19 | right: 0;
20 | bottom: 0;
21 | }
22 |
23 | .terminal-closed {
24 |
25 | }
26 |
27 | .terminal-placeholder {
28 | .xterm {
29 | height: 100% !important;
30 |
31 | .xterm-viewport {
32 | width: 100% !important;
33 | height: 100% !important;
34 | }
35 | }
36 | }
37 |
38 | .terminal-resize {
39 | position: absolute;
40 | margin: auto;
41 | top: 0;
42 | bottom: 0;
43 | left: 0;
44 | right: 0;
45 | width: fit-content;
46 | height: min-content;
47 | padding: 5px 10px;
48 |
49 | color: white;
50 | background-color: rgba($color: #000000, $alpha: 0.5);
51 | border-radius: 4px;
52 | }
53 |
--------------------------------------------------------------------------------
/src/app/terminal/terminal.module.ts:
--------------------------------------------------------------------------------
1 | import {NgModule} from '@angular/core';
2 | import {CommonModule} from '@angular/common';
3 |
4 | import {TerminalRoutingModule} from './terminal-routing.module';
5 | import {NgbDropdownModule, NgbNavModule} from "@ng-bootstrap/ng-bootstrap";
6 | import {TerminalComponent} from "./terminal.component";
7 | import {PtyComponent} from "./pty/pty.component";
8 | import {FormsModule} from "@angular/forms";
9 | import {SharedModule} from "../shared/shared.module";
10 | import {AutosizeModule} from "ngx-autosize";
11 | import {DumbComponent} from "./dumb/dumb.component";
12 |
13 |
14 | @NgModule({
15 | imports: [
16 | CommonModule,
17 | NgbNavModule,
18 | DumbComponent,
19 | PtyComponent,
20 | TerminalComponent,
21 | TerminalRoutingModule,
22 | AutosizeModule,
23 | NgbDropdownModule,
24 | FormsModule,
25 | ]
26 | })
27 | export class TerminalModule {
28 | }
29 |
--------------------------------------------------------------------------------
/src/app/types/device-manager.ts:
--------------------------------------------------------------------------------
1 | import {Device} from "./device";
2 |
3 |
4 | export declare interface CrashReportEntry {
5 | device: Device;
6 | path: string;
7 | }
8 |
9 |
10 | export declare interface DevicePrivateKey {
11 | data: string;
12 | privatePEM?: string;
13 | }
14 |
15 | export declare interface RawPackageInfo {
16 | id: string;
17 | type: string;
18 | title: string;
19 | appDescription?: string;
20 | vendor: string;
21 | version: string;
22 | folderPath: string;
23 | icon: string;
24 | }
25 |
26 | export declare interface PackageInfo extends RawPackageInfo {
27 | iconUri?: string;
28 | }
29 |
30 | export declare interface StorageInfo {
31 | total: number;
32 | used: number;
33 | available: number;
34 | }
35 |
--------------------------------------------------------------------------------
/src/app/types/device.ts:
--------------------------------------------------------------------------------
1 | export declare interface Device {
2 | name: string;
3 | host: string;
4 | port: number;
5 | username: 'prisoner' | 'root' | string;
6 | profile: 'ose';
7 | privateKey?: { openSsh: string };
8 | passphrase?: string;
9 | password?: string;
10 | description?: string;
11 | default?: boolean;
12 | indelible?: boolean;
13 | files?: string;
14 | }
15 |
16 | export enum NewDeviceAuthentication {
17 | Password = 'password',
18 | LocalKey = 'localKey',
19 | AppKey = 'appKey',
20 | DevKey = 'devKey',
21 | }
22 |
23 | export const AppPrivKeyName = 'id_devman';
24 |
25 | export declare interface NewDeviceBase extends Omit {
26 | new: true;
27 | name: string;
28 | description?: string;
29 | host: string;
30 | port: number;
31 | username: string;
32 | }
33 |
34 | export declare interface NewDeviceWithPassword extends NewDeviceBase {
35 | password: string;
36 | }
37 |
38 | export declare interface NewDeviceWithLocalPrivateKey extends NewDeviceBase {
39 | privateKey: {
40 | openSsh: string;
41 | };
42 | passphrase?: string;
43 | }
44 |
45 | export declare interface NewDeviceWithAppPrivateKey extends NewDeviceBase {
46 | privateKey: {
47 | openSsh: typeof AppPrivKeyName;
48 | };
49 | passphrase?: string;
50 | }
51 |
52 | export declare interface NewDeviceWithDevicePrivateKey extends NewDeviceBase {
53 | privateKey: {
54 | openSshData: string;
55 | };
56 | passphrase: string;
57 | }
58 |
59 | export type NewDevice =
60 | NewDeviceWithPassword
61 | | NewDeviceWithLocalPrivateKey
62 | | NewDeviceWithAppPrivateKey
63 | | NewDeviceWithDevicePrivateKey;
64 |
65 | export type DeviceLike = Device | NewDevice;
66 |
--------------------------------------------------------------------------------
/src/app/types/file-session.ts:
--------------------------------------------------------------------------------
1 | import {ProgressCallback} from "../core/services/progress-callback";
2 |
3 | export type FileType = '-' | 'd' | 'c' | 'b' | 's' | 'p' | 'l' | '';
4 |
5 | export declare interface FileItem {
6 | filename: string;
7 | type: FileType;
8 | mode: string;
9 | user?: string;
10 | group?: string;
11 | size: number,
12 | mtime: number,
13 | link?: LinkInfo;
14 | access?: PermInfo;
15 | }
16 |
17 | export declare interface LinkInfo {
18 | target?: string;
19 | broken?: boolean;
20 | }
21 |
22 | export declare interface PermInfo {
23 | read: boolean;
24 | write: boolean;
25 | execute: boolean;
26 | }
27 |
28 | export declare interface FileSession {
29 |
30 | ls(path: string): Promise;
31 |
32 | rm(path: string, recursive: boolean): Promise;
33 |
34 | get(remotePath: string, localPath: string): Promise;
35 |
36 | put(localPath: string, remotePath: string): Promise;
37 |
38 | mkdir(path: string): Promise;
39 |
40 | getTemp(remotePath: string, progress?: ProgressCallback): Promise;
41 |
42 | uploadBatch(sources: string[], pwd: string, fileCb: (name: string, index: number, total: number) => void,
43 | progressCb: ProgressCallback, failCb: (name: string, e: Error) => Promise): Promise;
44 |
45 | home(): Promise;
46 | }
47 |
--------------------------------------------------------------------------------
/src/app/types/index.ts:
--------------------------------------------------------------------------------
1 | export * from './device-manager';
2 | export * from './file-session';
3 | export * from './device';
4 |
--------------------------------------------------------------------------------
/src/app/types/luna-apis.ts:
--------------------------------------------------------------------------------
1 | import {LunaResponse} from "../core/services/remote-luna.service";
2 |
3 | export declare interface SystemInfo extends LunaResponse {
4 | firmwareVersion: string;
5 | modelName: string;
6 | sdkVersion: string;
7 | otaId: string;
8 | }
9 |
10 | export declare interface OsInfo extends LunaResponse {
11 | device_name: string;
12 | webos_manufacturing_version: string;
13 | webos_release: string;
14 | }
15 |
16 | export declare interface HomebrewChannelConfiguration extends LunaResponse {
17 | root: boolean,
18 | telnetDisabled: boolean,
19 | failsafe: boolean,
20 | sshdEnabled: boolean,
21 | blockUpdates: boolean
22 | }
23 |
--------------------------------------------------------------------------------
/src/app/update-details/update-details.component.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/app/update-details/update-details.component.scss:
--------------------------------------------------------------------------------
1 | .update-note {
2 |
3 | img {
4 | max-width: 100%;
5 | }
6 |
7 | }
8 |
--------------------------------------------------------------------------------
/src/app/update-details/update-details.component.ts:
--------------------------------------------------------------------------------
1 | import {Component, Inject, SecurityContext, ViewEncapsulation} from '@angular/core';
2 | import {DomSanitizer} from '@angular/platform-browser';
3 | import * as marked from 'marked';
4 | import {Release} from '../core/services';
5 |
6 | @Component({
7 | selector: 'app-update-details',
8 | templateUrl: './update-details.component.html',
9 | styleUrls: ['./update-details.component.scss'],
10 | encapsulation: ViewEncapsulation.None,
11 | })
12 | export class UpdateDetailsComponent {
13 |
14 | public bodyHtml: string;
15 |
16 | constructor(@Inject('release') public release: Release, sanitizer: DomSanitizer) {
17 | this.bodyHtml = sanitizer.sanitize(SecurityContext.HTML, marked.marked(release.body)) || 'No description.';
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/assets/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/assets/.gitkeep
--------------------------------------------------------------------------------
/src/assets/icons/electron.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/assets/icons/electron.bmp
--------------------------------------------------------------------------------
/src/assets/icons/favicon.256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/assets/icons/favicon.256x256.png
--------------------------------------------------------------------------------
/src/assets/icons/favicon.512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/assets/icons/favicon.512x512.png
--------------------------------------------------------------------------------
/src/assets/icons/favicon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/assets/icons/favicon.icns
--------------------------------------------------------------------------------
/src/assets/icons/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/assets/icons/favicon.ico
--------------------------------------------------------------------------------
/src/assets/icons/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/assets/icons/favicon.png
--------------------------------------------------------------------------------
/src/assets/images/hint-devmode-passphrase.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/assets/images/hint-devmode-passphrase.png
--------------------------------------------------------------------------------
/src/assets/images/hint-key-server.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/assets/images/hint-key-server.png
--------------------------------------------------------------------------------
/src/environments/environment.dev.ts:
--------------------------------------------------------------------------------
1 | export const AppConfig = {
2 | production: false,
3 | environment: 'development'
4 | };
5 |
--------------------------------------------------------------------------------
/src/environments/environment.prod.ts:
--------------------------------------------------------------------------------
1 | export const AppConfig = {
2 | production: true,
3 | environment: 'production'
4 | };
5 |
--------------------------------------------------------------------------------
/src/environments/environment.ts:
--------------------------------------------------------------------------------
1 | export const AppConfig = {
2 | production: false,
3 | environment: 'local'
4 | };
5 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Device Manager for webOS
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import {enableProdMode} from '@angular/core';
2 | import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
3 |
4 | import {AppModule} from './app/app.module';
5 | import {AppConfig} from './environments/environment';
6 | import ReleaseInfo from './release.json';
7 | import {browserTracingIntegration, defaultStackParser, init as initSentry} from "@sentry/angular";
8 |
9 | initSentry({
10 | dsn: "https://93c623f5a47940f0b7bac7d0d5f6a91f@o4504977150377984.ingest.sentry.io/4504978685689856",
11 | tracePropagationTargets: [],
12 | integrations: [
13 | browserTracingIntegration()
14 | ],
15 | enabled: !!ReleaseInfo.version,
16 | environment: AppConfig.environment,
17 | release: ReleaseInfo.version || 'local',
18 | stackParser: (stack: string, skipFirst?: number) => {
19 | // noinspection HttpUrlsUsage
20 | stack = stack.replace(/@tauri:\/\//g, "@http://");
21 | return defaultStackParser(stack, skipFirst);
22 | },
23 | beforeBreadcrumb: (breadcrumb) => {
24 | return breadcrumb.level !== 'debug' ? breadcrumb : null;
25 | },
26 | beforeSendTransaction: () => {
27 | return null;
28 | },
29 | beforeSend: (event, hint) => {
30 | const originalException: any = hint.originalException;
31 | if (originalException && originalException['reason']) {
32 | if (originalException['unhandled'] !== true) {
33 | return null;
34 | }
35 | }
36 | return event;
37 | },
38 | // Set tracesSampleRate to 1.0 to capture 100%
39 | // of transactions for performance monitoring.
40 | // We recommend adjusting this value in production
41 | tracesSampleRate: 1.0,
42 | });
43 |
44 | if (AppConfig.production) {
45 | enableProdMode();
46 | }
47 |
48 | platformBrowserDynamic()
49 | .bootstrapModule(AppModule, {
50 | preserveWhitespaces: false
51 | })
52 | .catch(err => console.error(err));
53 |
54 | const darkTheme = window.matchMedia('(prefers-color-scheme: dark)');
55 |
56 | document.documentElement.setAttribute('data-bs-theme', darkTheme.matches ? 'dark' : 'light');
57 | if (darkTheme.addEventListener) {
58 | darkTheme.addEventListener('change', (media) => {
59 | document.documentElement.setAttribute('data-bs-theme', media.matches ? 'dark' : 'light');
60 | });
61 | } else {
62 | // noinspection JSDeprecatedSymbols
63 | darkTheme.addListener?.((ev) => document.documentElement.setAttribute(
64 | 'data-bs-theme', ev.matches ? 'dark' : 'light'));
65 | }
66 |
--------------------------------------------------------------------------------
/src/polyfills-test.ts:
--------------------------------------------------------------------------------
1 | import 'core-js/es/reflect';
2 | import 'zone.js';
3 |
--------------------------------------------------------------------------------
/src/proxy.conf.json:
--------------------------------------------------------------------------------
1 | {
2 | "/repo": {
3 | "target": "http://localhost:8010/",
4 | "secure": false,
5 | "changeOrigin": true,
6 | "pathRewrite": {
7 | "^/repo": ""
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/release.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": ""
3 | }
4 |
--------------------------------------------------------------------------------
/src/styles.scss:
--------------------------------------------------------------------------------
1 | @use 'bootstrap' as bs;
2 | @use 'bootstrap-icons';
3 | @use '@xterm/xterm';
4 |
5 | @import './styles/no-select';
6 | @import './styles/app-item';
7 | @import './styles/overflow';
8 | @import './styles/terminal';
9 | @import './styles/shared';
10 |
11 |
12 | @import 'highlight.js/scss/github.scss' screen and (prefers-color-scheme: light);
13 | @import 'highlight.js/scss/github-dark.scss' screen and (prefers-color-scheme: dark);
14 |
15 | /*
16 | * Content
17 | */
18 |
19 | html, body {
20 | height: 100%;
21 | width: 100%;
22 | overflow: hidden;
23 | }
24 |
25 | .bg-panel {
26 | @extend .bg-dark-subtle;
27 | }
28 |
--------------------------------------------------------------------------------
/src/styles/app-item.scss:
--------------------------------------------------------------------------------
1 | li.app-item {
2 |
3 | .app-desc {
4 | display: flex;
5 | flex-flow: row;
6 | min-height: 60px;
7 | vertical-align: middle;
8 |
9 | img.app-icon {
10 | width: 48px;
11 | height: 48px;
12 | margin-top: auto;
13 | margin-bottom: auto;
14 | }
15 |
16 | .app-headline {
17 | margin-top: auto;
18 | margin-bottom: auto;
19 |
20 | .app-title {
21 | font-weight: bold;
22 | font-size: larger;
23 | }
24 |
25 | .app-description {
26 | }
27 | }
28 | }
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/src/styles/no-select.scss:
--------------------------------------------------------------------------------
1 | /* disable selection */
2 | :not(input):not(textarea),
3 | :not(input):not(textarea)::after,
4 | :not(input):not(textarea)::before {
5 | user-select: none;
6 | cursor: default;
7 | }
8 |
9 | input, button, textarea, :focus {
10 | outline: none;
11 | }
12 |
13 | button, a {
14 | cursor: pointer !important;
15 |
16 | ::before {
17 | cursor: pointer !important;
18 | }
19 |
20 | * {
21 | cursor: pointer !important;
22 | }
23 | }
24 |
25 | /* disable image and anchor dragging */
26 | a:not([draggable=true]), img:not([draggable=true]) {
27 | user-drag: none;
28 | }
29 |
30 | a[href^="http://"],
31 | a[href^="https://"],
32 | a[href^="ftp://"] {
33 | user-drag: auto;
34 | }
35 |
36 | .user-select-text * {
37 | user-select: text !important;
38 | cursor: text !important;
39 | }
40 |
--------------------------------------------------------------------------------
/src/styles/overflow.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/styles/overflow.scss
--------------------------------------------------------------------------------
/src/styles/shared.scss:
--------------------------------------------------------------------------------
1 | .manager-toolbar {
2 | height: 60px;
3 | flex: 0 0 auto;
4 | }
5 |
6 | .ng-touched.ng-invalid {
7 | @extend .is-invalid;
8 | }
9 |
10 |
11 | .stat-bar {
12 | height: 30px;
13 | }
14 |
15 | .storage-info-bar {
16 | width: 25%;
17 | max-width: 110px;
18 | }
19 |
20 | i.bi.larger::before {
21 | display: block;
22 | font-size: 1.5em;
23 | margin: 0 -0.5em;
24 | }
25 |
--------------------------------------------------------------------------------
/src/styles/terminal.scss:
--------------------------------------------------------------------------------
1 |
2 | .terminal-container {
3 | position: absolute;
4 | top: 0;
5 | bottom: 0;
6 | left: 0;
7 | right: 0;
8 |
9 | .xterm {
10 | height: 100% !important;
11 | padding: 5px;
12 |
13 | .xterm-viewport {
14 | //width: 100% !important;
15 | height: 100% !important;
16 |
17 | overflow-y: scroll;
18 | overflow-x: hidden;
19 | }
20 | }
21 |
22 | }
23 |
24 | .terminal-search-bar {
25 | right: 0;
26 | }
27 |
--------------------------------------------------------------------------------
/src/typings.d.ts:
--------------------------------------------------------------------------------
1 | /* SystemJS module definition */
2 | declare const nodeModule: NodeModule;
3 | interface NodeModule {
4 | id: string;
5 | }
6 | interface Window {
7 | process: any;
8 | require: any;
9 | }
10 |
11 | declare module '*.sh' {
12 | const content: string;
13 | export default content;
14 | }
15 |
--------------------------------------------------------------------------------
/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
3 | {
4 | "extends": "./tsconfig.json",
5 | "compilerOptions": {
6 | "outDir": "./out-tsc/app",
7 | "types": []
8 | },
9 | "files": [
10 | "src/main.ts",
11 | "src/polyfills.ts",
12 | ],
13 | "include": [
14 | "src/**/*.d.ts"
15 | ]
16 | }
17 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
3 | {
4 | "compileOnSave": false,
5 | "compilerOptions": {
6 | "outDir": "./dist/out-tsc",
7 | "strict": true,
8 | "noImplicitOverride": true,
9 | "noPropertyAccessFromIndexSignature": true,
10 | "noImplicitReturns": true,
11 | "noFallthroughCasesInSwitch": true,
12 | "skipLibCheck": true,
13 | "isolatedModules": true,
14 | "esModuleInterop": true,
15 | "sourceMap": true,
16 | "declaration": false,
17 | "experimentalDecorators": true,
18 | "moduleResolution": "bundler",
19 | "importHelpers": true,
20 | "target": "ES2022",
21 | "module": "ES2022",
22 | "lib": [
23 | "ES2022",
24 | "dom"
25 | ]
26 | },
27 | "angularCompilerOptions": {
28 | "enableI18nLegacyMessageIdFormat": false,
29 | "strictInjectionParameters": true,
30 | "strictInputAccessModifiers": true,
31 | "strictTemplates": true
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
3 | {
4 | "extends": "./tsconfig.json",
5 | "compilerOptions": {
6 | "outDir": "./out-tsc/spec",
7 | "types": [
8 | "jasmine"
9 | ]
10 | },
11 | "include": [
12 | "src/**/*.spec.ts",
13 | "src/**/*.d.ts"
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------