Monero installer implementation

This commit is contained in:
everoddandeven 2024-09-28 16:31:51 +02:00
parent 68987b9599
commit 7208b5ba9f
33 changed files with 2250 additions and 1272 deletions

View file

@ -38,8 +38,7 @@
],
"scripts": [
"node_modules/jquery/dist/jquery.min.js",
"node_modules/bootstrap/dist/js/bootstrap.bundle.min.js",
"node_modules/bootstrap-table/dist/bootstrap-table.js"
"node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"
],
"styles": [
"src/styles.scss",

View file

@ -4,6 +4,11 @@ import * as path from 'path';
import * as fs from 'fs';
import * as https from 'https';
import { createHash } from 'crypto';
import * as tar from 'tar';
const monerodFilePath: string = "/home/sidney/Documenti/monero-x86_64-linux-gnu-v0.18.3.4/monerod";
let win: BrowserWindow | null = null;
@ -138,11 +143,99 @@ function startMoneroDaemon(commandOptions: string[]): ChildProcessWithoutNullStr
monerodProcess.on('close', (code) => {
console.log(`monerod chiuso con codice: ${code}`);
win?.webContents.send('monero-stdout', `monerod exited with code: ${code}`);
win?.webContents.send('monero-close', code);
});
return monerodProcess;
}
// Funzione per il download
const downloadFile = (url: string, destination: string, onProgress: (progress: number) => void): Promise<void> => {
return new Promise((resolve, reject) => {
const file = fs.createWriteStream(destination);
https.get(url, (response) => {
if (response.statusCode === 200) {
const totalBytes = parseInt(response.headers['content-length'] || '0', 10);
let downloadedBytes = 0;
response.on('data', (chunk) => {
downloadedBytes += chunk.length;
const progress = (downloadedBytes / totalBytes) * 100;
onProgress(progress); // Notifica il progresso
});
response.pipe(file);
file.on('finish', () => {
file.close(() => resolve());
});
} else {
reject(new Error(`Failed to download: ${response.statusCode}`));
}
}).on('error', (err) => {
fs.unlink(destination, () => reject(err));
});
});
};
// Funzione per scaricare e verificare l'hash
const downloadAndVerifyHash = async (hashUrl: string, fileName: string, filePath: string): Promise<boolean> => {
const hashFilePath = path.join(app.getPath('temp'), 'monero_hashes.txt');
// Scarica il file di hash
await downloadFile(hashUrl, hashFilePath, () => {});
// Leggi il file di hash e cerca l'hash corrispondente
const hashContent = fs.readFileSync(hashFilePath, 'utf8');
const hashLines = hashContent.split('\n');
let expectedHash: string | null = null;
for (const line of hashLines) {
const match = line.match(/^(\w+)\s+(\S+)/);
if (match && match[2] === fileName) {
expectedHash = match[1];
break;
}
}
if (!expectedHash) {
throw new Error('Hash not found for the downloaded file.');
}
// Verifica l'hash del file scaricato
const calculatedHash = await verifyFileHash(filePath);
return calculatedHash === expectedHash;
};
// Funzione per verificare l'hash del file
const verifyFileHash = (filePath: string): Promise<string> => {
return new Promise((resolve, reject) => {
const hash = createHash('sha256');
const fileStream = fs.createReadStream(filePath);
fileStream.on('data', (data) => {
hash.update(data);
});
fileStream.on('end', () => {
resolve(hash.digest('hex'));
});
fileStream.on('error', (err) => {
reject(err);
});
});
};
// Funzione per estrarre tar.bz2
const extractTarBz2 = (filePath: string, destination: string): Promise<void> => {
return tar.x({
file: filePath,
cwd: destination,
});
};
try {
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
@ -175,6 +268,38 @@ try {
getMonerodVersion(configFilePath);
});
// Gestione IPC
ipcMain.handle('download-monero', async (event, downloadUrl: string, destination: string) => {
try {
const fileName = path.basename(downloadUrl);
const filePath = path.join(destination, fileName);
const hashUrl = 'https://www.getmonero.org/downloads/hashes.txt';
// Inizializza il progresso
event.sender.send('download-progress', { progress: 0, status: 'Starting download...' });
// Scarica il file Monero
await downloadFile(downloadUrl, filePath, (progress) => {
event.sender.send('download-progress', { progress, status: 'Downloading...' });
});
// Scarica e verifica l'hash
event.sender.send('download-progress', { progress: 100, status: 'Verifying hash...' });
await downloadAndVerifyHash(hashUrl, fileName, filePath);
// Estrai il file
event.sender.send('download-progress', { progress: 100, status: 'Extracting...' });
await extractTarBz2(filePath, destination);
event.sender.send('download-progress', { progress: 100, status: 'Download and extraction completed successfully.' });
} catch (error) {
event.sender.send('download-progress', { progress: 0, status: `Error: ${error}` });
throw new Error(`Error: ${error}`);
}
});
} catch (e) {
// Catch Error
// throw e;

2990
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -57,10 +57,13 @@
"bootstrap": "5.3.3",
"bootstrap-icons": "1.11.3",
"bootstrap-table": "1.23.4",
"crypto": "1.0.1",
"idb": "8.0.0",
"jquery": "3.7.1",
"rxjs": "7.8.1",
"tar": "7.4.3",
"tslib": "2.6.2",
"unbzip2-stream": "1.4.3",
"zone.js": "0.14.4"
},
"devDependencies": {
@ -81,6 +84,7 @@
"@types/jest": "29.5.12",
"@types/jquery": "3.5.30",
"@types/node": "20.12.7",
"@types/unbzip2-stream": "1.4.3",
"@typescript-eslint/eslint-plugin": "7.7.1",
"@typescript-eslint/parser": "7.7.1",
"conventional-changelog-cli": "4.1.0",

View file

@ -59,7 +59,6 @@ const httpLoaderFactory = (http: HttpClient): TranslateHttpLoader => new Transl
deps: [HttpClient]
}
}),
NavbarComponent,
LoadComponent
],
providers: [],

View file

@ -87,12 +87,16 @@ export class DaemonService {
private daemonRunning?: boolean;
private url: string = "http://127.0.0.1:28081";
public settings: DaemonSettings;
//private url: string = "http://node2.monerodevs.org:28089";
//private url: string = "https://testnet.xmr.ditatompel.com";
//private url: string = "https://xmr.yemekyedim.com:18081";
//private url: string = "https://moneronode.org:18081";
public stopping: boolean = false;
public starting: boolean = false;
public readonly onDaemonStatusChanged: EventEmitter<boolean> = new EventEmitter<boolean>();
public readonly onDaemonStopStart: EventEmitter<void> = new EventEmitter<void>();
public readonly onDaemonStopEnd: EventEmitter<void> = new EventEmitter<void>();
private readonly headers: { [key: string]: string } = {
"Access-Control-Allow-Headers": "*", // this will allow all CORS requests
@ -102,6 +106,19 @@ export class DaemonService {
constructor(private httpClient: HttpClient, private electronService: ElectronService) {
this.openDbPromise = this.openDatabase();
this.settings = this.loadSettings();
if (this.electronService.isElectron) {
this.electronService.ipcRenderer.on('monero-close', (event, code: number | null) => {
this.onClose();
});
}
}
private onClose(): void {
this.daemonRunning = false;
this.stopping = false;
this.onDaemonStatusChanged.emit(false);
this.onDaemonStopEnd.emit();
}
private async openDatabase(): Promise<IDBPDatabase> {
@ -238,6 +255,8 @@ export class DaemonService {
return;
}
this.starting = true;
console.log("Starting daemon");
const settings = await this.getSettings();
this.electronService.ipcRenderer.send('start-monerod', settings.toCommandOptions());
@ -254,8 +273,8 @@ export class DaemonService {
this.onDaemonStatusChanged.emit(false);
}
setTimeout(() => {
}, 500)
this.starting = false;
}
public async isRunning(force: boolean = false): Promise<boolean> {
@ -662,6 +681,8 @@ export class DaemonService {
console.warn("Daemon not running");
return;
}
this.stopping = true;
this.onDaemonStopStart.emit();
const response = await this.callRpc(new StopDaemonRequest());
console.log(response);
@ -670,8 +691,13 @@ export class DaemonService {
throw new Error(`Could not stop daemon: ${response.status}`);
}
if (this.electronService.isElectron) {
return;
}
this.daemonRunning = false;
this.onDaemonStatusChanged.emit(false);
this.onDaemonStopEnd.emit();
}
public async setLimit(limitDown: number, limitUp: number): Promise<{ limitDown: number, limitUp: number }> {
@ -746,5 +772,9 @@ export class DaemonService {
return await this.update('download', path);
}
public getGuiVersion(): string {
return "0.1.0-alpha";
}
}

View file

@ -1 +1,3 @@
export * from './electron/electron.service';
export * from './daemon/daemon.service';
export { MoneroInstallerService } from './monero-installer/monero-installer.service';

View file

@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { MoneroInstallerService } from './monero-installer.service';
describe('MoneroInstallerService', () => {
let service: MoneroInstallerService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(MoneroInstallerService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View file

@ -0,0 +1,22 @@
import { Injectable } from '@angular/core';
import { ElectronService } from '../electron/electron.service';
@Injectable({
providedIn: 'root'
})
export class MoneroInstallerService {
constructor(private electronService: ElectronService) {}
downloadMonero(downloadUrl: string, destination: string): Promise<void> {
return new Promise((resolve, reject) => {
this.electronService.ipcRenderer.invoke('download-monero', downloadUrl, destination)
.then(() => resolve())
.catch((error) => reject(error));
this.electronService.ipcRenderer.on('download-progress', (event, { progress, status }) => {
console.log(`Progress: ${progress}% - ${status}`);
// Qui puoi aggiornare lo stato di progresso nel tuo componente
});
});
}
}

View file

@ -13,3 +13,4 @@
</div>
<app-daemon-not-running></app-daemon-not-running>
<app-daemon-stopping></app-daemon-stopping>

View file

@ -11,8 +11,9 @@
</div>
</div>
<app-daemon-not-running></app-daemon-not-running>
<app-daemon-stopping></app-daemon-stopping>
<div *ngIf="daemonRunning" class="tab-content" id="pills-tabContent">
<div *ngIf="daemonRunning && !stoppingDaemon" class="tab-content" id="pills-tabContent">
<div class="tab-pane fade show active" id="pills-home" role="tabpanel" aria-labelledby="pills-home-tab" tabindex="0">
<div class="row d-flex justify-content-center">

View file

@ -10,6 +10,7 @@ import { DaemonInfo } from '../../../common/DaemonInfo';
import * as $ from 'jquery';
import * as bootstrapTable from 'bootstrap-table';
import { LogsService } from '../logs/logs.service';
import { ElectronService } from '../../core/services';
@Component({
selector: 'app-detail',
@ -52,7 +53,10 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
public cards: Card[];
constructor(private router: Router,private daemonService: DaemonService, private navbarService: NavbarService, private logsService: LogsService, private ngZone: NgZone) {
constructor(
private router: Router,private daemonService: DaemonService,
private navbarService: NavbarService, private logsService: LogsService,
private ngZone: NgZone, private electronService: ElectronService) {
this.daemonRunning = false;
this.startingDaemon = false;
this.stoppingDaemon = false;
@ -91,6 +95,9 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
this.daemonService.onDaemonStatusChanged.subscribe((running: boolean) => {
this.ngZone.run(() => {
this.daemonRunning = running;
if (!running && this.stoppingDaemon) {
this.stoppingDaemon = false;
}
})
});
}
@ -174,13 +181,13 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
await this.daemonService.stopDaemon();
this.daemonRunning = false;
if(!this.electronService.isElectron) this.daemonRunning = false;
}
catch (error) {
console.error(error);
}
this.stoppingDaemon = false;
if(!this.electronService.isElectron) this.stoppingDaemon = false;
}
private onNavigationEnd(): void {
@ -208,10 +215,10 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
}
private createCards(): Card[] {
if (!this.daemonRunning) {
if (!this.daemonRunning && !this.daemonService.starting) {
return [];
}
if (this.isLoading) {
if (this.isLoading || this.daemonService.starting) {
return this.createLoadingCards();
}
return [

View file

@ -7,14 +7,33 @@
</div>
<div *ngIf="daemonRunning" class="row d-flex justify-content-center">
@for(card of cards; track card.header) {
<div class="card text-bg-dark m-3 text-center" style="max-width: 18rem;">
<div class="card-header">{{card.header}}</div>
<div class="card-body">
<h5 class="card-title">{{card.content}}</h5>
@if(!loading) {
@for(card of cards; track card.header) {
<div class="card text-bg-dark m-3 text-center" style="max-width: 18rem;">
<div class="card-header">{{card.header}}</div>
<div class="card-body">
<h5 class="card-title">{{card.content}}</h5>
</div>
</div>
</div>
}
}
@else {
@for(card of cards; track card.header) {
<div class="card text-bg-dark m-3 text-center" style="max-width: 18rem;">
<div class="card-header">{{card.header}}</div>
<div class="card-body">
<p class="card-text placeholder-glow">
<span class="placeholder col-7"></span>
<span class="placeholder col-4"></span>
<span class="placeholder col-4"></span>
<span class="placeholder col-6"></span>
<span class="placeholder col-8"></span>
</p>
</div>
</div>
}
}
</div>
<app-daemon-not-running></app-daemon-not-running>
<app-daemon-stopping></app-daemon-stopping>

View file

@ -2,6 +2,7 @@ import { AfterViewInit, Component, NgZone } from '@angular/core';
import { DaemonService } from '../../core/services/daemon/daemon.service';
import { NavigationEnd, Router } from '@angular/router';
import { NavbarService } from '../../shared/components/navbar/navbar.service';
import { SimpleBootstrapCard } from '../../shared/utils';
@Component({
selector: 'app-hard-fork-info',
@ -9,7 +10,7 @@ import { NavbarService } from '../../shared/components/navbar/navbar.service';
styleUrl: './hard-fork-info.component.scss'
})
export class HardForkInfoComponent implements AfterViewInit {
public cards: Card[];
public cards: SimpleBootstrapCard[];
private earliestHeight: number;
private enabled: boolean;
private threshold: number;
@ -20,6 +21,8 @@ export class HardForkInfoComponent implements AfterViewInit {
public daemonRunning: boolean;
public loading: boolean = false;
constructor(private router: Router, private daemonService: DaemonService, private navbarService: NavbarService, private ngZone: NgZone) {
this.cards = [];
this.enabled = false;
@ -65,36 +68,37 @@ export class HardForkInfoComponent implements AfterViewInit {
if (!await this.daemonService.isRunning()) {
return;
}
const info = await this.daemonService.hardForkInfo();
this.cards = this.createCards();
this.earliestHeight = info.earliestHeight;
this.threshold = info.threshold;
this.blockVersion = info.version;
this.votes = info.votes;
this.voting = info.voting;
this.window = info.window;
this.loading = true;
try {
const info = await this.daemonService.hardForkInfo();
this.earliestHeight = info.earliestHeight;
this.threshold = info.threshold;
this.blockVersion = info.version;
this.votes = info.votes;
this.voting = info.voting;
this.window = info.window;
}
catch(error) {
console.error(error);
}
this.loading = false;
}
private createCards(): Card[] {
private createCards(): SimpleBootstrapCard[] {
return [
new Card('Status', this.enabled ? 'enabled' : 'disabled'),
new Card('Earliest height', `${this.earliestHeight}`),
new Card('Threshold', `${this.threshold}`),
new Card('Block version', `${this.blockVersion}`),
new Card('Votes', `${this.votes}`),
new Card('Voting', `${this.voting}`),
new Card('Window', `${this.window}`)
new SimpleBootstrapCard('Status', this.enabled ? 'enabled' : 'disabled'),
new SimpleBootstrapCard('Earliest height', `${this.earliestHeight}`),
new SimpleBootstrapCard('Threshold', `${this.threshold}`),
new SimpleBootstrapCard('Block version', `${this.blockVersion}`),
new SimpleBootstrapCard('Votes', `${this.votes}`),
new SimpleBootstrapCard('Voting', `${this.voting}`),
new SimpleBootstrapCard('Window', `${this.window}`)
]
}
}
class Card {
public header: string;
public content: string;
constructor(header: string, content: string) {
this.header = header;
this.content = content;
}
}

View file

@ -18,18 +18,22 @@ export class LogsComponent implements AfterViewInit {
return this.logsService.lines;
}
private scrollToBottom(): void {
this.lines;
const terminalOutput = <HTMLDivElement | null>document.getElementById('terminalOutput');
if (terminalOutput) {
terminalOutput.style.width = `${window.innerWidth}`;
console.log(`scrolling from ${terminalOutput.offsetTop} to ${terminalOutput.scrollHeight}`)
terminalOutput.scrollBy(0, terminalOutput.scrollHeight)
}
}
private onLog(): void {
if (this.logTerminal) this.logTerminal.nativeElement.scrollTop = this.logTerminal.nativeElement.scrollHeight;
// Scorri automaticamente in basso
setTimeout(() => {
this.ngZone.run(() => {
this.lines;
const terminalOutput = <HTMLDivElement | null>document.getElementById('terminalOutput');
if (terminalOutput) {
terminalOutput.style.width = `${window.innerWidth}`;
console.log(`scrolling from ${terminalOutput.offsetTop} to ${terminalOutput.scrollHeight}`)
terminalOutput.scrollBy(0, terminalOutput.scrollHeight)
}
this.scrollToBottom();
})
}, 100);
@ -41,5 +45,6 @@ export class LogsComponent implements AfterViewInit {
ngAfterViewInit(): void {
this.navbarService.removeLinks();
this.scrollToBottom();
}
}

View file

@ -8,6 +8,7 @@ import { ElectronService } from '../../core/services';
export class LogsService {
public readonly onLog: EventEmitter<string> = new EventEmitter<string>();
public readonly lines: string[] = [];
private readonly ansiRegex: RegExp = /\u001b\[[0-9;]*m/g;
constructor(private electronService: ElectronService, private ngZone: NgZone) {
if (this.electronService.isElectron) {
@ -17,10 +18,14 @@ export class LogsService {
}
public cleanLog(message: string): string {
return message.replace(this.ansiRegex, '').replace(/[\r\n]+/g, '\n').trim();
}
public log(message: string): void {
this.ngZone.run(() => {
this.lines.push(message);
this.onLog.emit(message);
this.lines.push(this.cleanLog(message));
this.onLog.emit(this.cleanLog(message));
});
}

View file

@ -84,3 +84,4 @@
</div>
<app-daemon-not-running></app-daemon-not-running>
<app-daemon-stopping></app-daemon-stopping>

View file

@ -12,3 +12,4 @@
</div>
<app-daemon-not-running></app-daemon-not-running>
<app-daemon-stopping></app-daemon-stopping>

View file

@ -165,3 +165,4 @@
<app-daemon-not-running></app-daemon-not-running>
<app-daemon-stopping></app-daemon-stopping>

View file

@ -39,3 +39,7 @@
}
}
</div>
<hr class="my-4">
<button class="w-100 btn btn-primary btn-lg" type="submit" (click)="upgrade()">Upgrade</button>

View file

@ -4,7 +4,7 @@ import { NavbarLink } from '../../shared/components/navbar/navbar.model';
import { DaemonService } from '../../core/services/daemon/daemon.service';
import { SimpleBootstrapCard } from '../../shared/utils';
import { DaemonVersion } from '../../../common/DaemonVersion';
import { ElectronService } from '../../core/services';
import { ElectronService, MoneroInstallerService } from '../../core/services';
@Component({
selector: 'app-version',
@ -17,7 +17,9 @@ export class VersionComponent implements AfterViewInit {
public currentVersion?: DaemonVersion;
public latestVersion?: DaemonVersion;
constructor(private navbarService: NavbarService, private daemonService: DaemonService, private electronService: ElectronService) {
public downloadPath: string = '/home/sidney/monerod/';
constructor(private navbarService: NavbarService, private daemonService: DaemonService, private electronService: ElectronService, private moneroInstaller: MoneroInstallerService) {
this.links = [
new NavbarLink('pills-overview-tab', '#pills-overview', 'pills-overview', true, 'Overview')
];
@ -26,15 +28,17 @@ export class VersionComponent implements AfterViewInit {
private createCards(): SimpleBootstrapCard[] {
return [
new SimpleBootstrapCard('Current version', this.currentVersion ? this.currentVersion.fullname : '', this.currentVersion == null),
new SimpleBootstrapCard('Latest version', this.latestVersion ? this.latestVersion.fullname : '', this.latestVersion == null)
new SimpleBootstrapCard('GUI Version', this.daemonService.getGuiVersion()),
new SimpleBootstrapCard('Current Monerod version', this.currentVersion ? this.currentVersion.fullname : '', this.currentVersion == null),
new SimpleBootstrapCard('Latest Monerod version', this.latestVersion ? this.latestVersion.fullname : '', this.latestVersion == null)
];
}
private createErrorCards(): SimpleBootstrapCard[] {
return [
new SimpleBootstrapCard('Current version', 'Error', false),
new SimpleBootstrapCard('Latest version', 'Error', false)
new SimpleBootstrapCard('GUI Version', this.daemonService.getGuiVersion()),
new SimpleBootstrapCard('Current Monerod version', 'Error', false),
new SimpleBootstrapCard('Latest Monerod version', 'Error', false)
];
}
@ -59,4 +63,21 @@ export class VersionComponent implements AfterViewInit {
this.latestVersion = latestVersion;
}
public downloadProgress: number = 100;
public downloadStatus : string = '';
public async upgrade(): Promise<void> {
const downloadUrl = 'https://downloads.getmonero.org/cli/linux64'; // Cambia in base al sistema
const destination = '/home/sidney/'; // Aggiorna con il percorso desiderato
this.moneroInstaller.downloadMonero(downloadUrl, destination)
.then(() => {
console.log('Download completato con successo.');
})
.catch((error) => {
console.error('Errore:', error);
});
}
}

View file

@ -1,11 +1,15 @@
<div *ngIf="!daemonRunning" class="h-100 p-5 text-bg-dark rounded-3 m-4 text-center">
<h2><i class="bi bi-exclamation-diamond m-4"></i> Daemon not running</h2>
<p>Start monero daemon</p>
<h2 *ngIf="!startingDaemon"><i class="bi bi-exclamation-diamond m-4"></i> Daemon not running</h2>
<p *ngIf="!startingDaemon">Start monero daemon</p>
<h2 *ngIf="startingDaemon"><i class="bi bi-play-fill m-4"></i> Daemon is starting</h2>
<p *ngIf="startingDaemon">Starting monero daemon</p>
<button *ngIf="!startingDaemon" class="btn btn-outline-light" type="button" (click)="startDaemon()"><i class="bi bi-play-fill"></i> Start</button>
<button *ngIf="startingDaemon" class="btn btn-outline-light" type="button" disabled>
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
Starting daemon
Starting monerod
</button>
&nbsp;
<button routerLink="/settings" class="btn btn-outline-light" type="button" [disabled]="startingDaemon"><i class="bi bi-gear"></i> Configure</button>
<button *ngIf="!startingDaemon" routerLink="/settings" class="btn btn-outline-light" type="button"><i class="bi bi-gear"></i> Configure</button>
</div>

View file

@ -9,7 +9,11 @@ import { DaemonService } from '../../../core/services/daemon/daemon.service';
export class DaemonNotRunningComponent {
public daemonRunning: boolean = false;
public startingDaemon: boolean = false;
public get startingDaemon(): boolean {
return this.daemonService.starting;
}
private stoppingDaemon: boolean = false;
@ -33,8 +37,6 @@ export class DaemonNotRunningComponent {
return;
}
this.startingDaemon = true;
setTimeout(async () => {
try {
await this.daemonService.startDaemon();
@ -44,8 +46,6 @@ export class DaemonNotRunningComponent {
console.error(error);
this.daemonRunning = false;
}
this.startingDaemon = false;
}, 500);
}

View file

@ -0,0 +1,8 @@
<div *ngIf="stopping" class="h-100 p-5 text-bg-dark rounded-3 m-4 text-center">
<h2><i class="bi bi-power m-4"></i> Daemon is stopping</h2>
<!--<p>Stopping monero daemon</p>-->
<button class="btn btn-outline-light" type="button" disabled>
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
Stopping daemon
</button>
</div>

View file

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DaemonStoppingComponent } from './daemon-stopping.component';
describe('DaemonStoppingComponent', () => {
let component: DaemonStoppingComponent;
let fixture: ComponentFixture<DaemonStoppingComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [DaemonStoppingComponent]
})
.compileComponents();
fixture = TestBed.createComponent(DaemonStoppingComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View file

@ -0,0 +1,19 @@
import { Component } from '@angular/core';
import { DaemonService } from '../../../core/services/daemon/daemon.service';
@Component({
selector: 'app-daemon-stopping',
templateUrl: './daemon-stopping.component.html',
styleUrl: './daemon-stopping.component.scss'
})
export class DaemonStoppingComponent {
public get stopping(): boolean {
return this.daemonService.stopping;
}
constructor(private daemonService: DaemonService) {
}
}

View file

@ -1,4 +1,4 @@
export * from './page-not-found/page-not-found.component';
export * from './sidebar/sidebar.component';
export * from './daemon-not-running/daemon-not-running.component';
export * from './daemon-stopping/daemon-stopping.component';

View file

@ -23,6 +23,29 @@
<strong>Monero Daemon</strong>
</span>
<ul class="navbar-nav flex-row">
<li *ngIf="!running && !stopping && !starting" class="nav-item text-nowrap">
<button class="nav-link px-3 text-white" type="button" data-bs-toggle="collapse" aria-expanded="false" aria-label="Start daemon">
<i class="bi bi-play-fill"></i>
</button>
</li>
<li *ngIf="running && !stopping && !starting" class="nav-item text-nowrap">
<button class="nav-link px-3 text-white" type="button" data-bs-toggle="collapse" aria-expanded="false" aria-label="Stop daemon">
<i class="bi bi-stop-fill"></i>
</button>
</li>
<li *ngIf="running && !stopping && !starting" class="nav-item text-nowrap">
<button class="nav-link px-3 text-white" type="button" data-bs-toggle="collapse" aria-expanded="false" aria-label="Restart daemon">
<i class="bi bi-arrow-clockwise"></i>
</button>
</li>
<li class="nav-item text-nowrap">
<button class="nav-link px-3 text-white" type="button" data-bs-toggle="offcanvas" aria-label="Quit" [disabled]="stopping || starting">
<i class="bi bi-power"></i>
</button>
</li>
</ul>
<ul class="navbar-nav flex-row d-md-none">
<li class="nav-item text-nowrap">
<button class="nav-link px-3 text-white" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSearch" aria-controls="navbarSearch" aria-expanded="false" aria-label="Toggle search">

View file

@ -1,21 +1,46 @@
import { Component } from '@angular/core';
import { Component, NgZone } from '@angular/core';
import { NavbarService } from './navbar.service';
import { NavbarLink } from './navbar.model';
import { DaemonService } from '../../../core/services/daemon/daemon.service';
@Component({
selector: 'app-navbar',
standalone: true,
imports: [],
templateUrl: './navbar.component.html',
styleUrl: './navbar.component.scss'
})
export class NavbarComponent {
private _running: boolean = false;
public get navbarLinks(): NavbarLink[] {
return this.navbarService.links;
}
constructor(private navbarService: NavbarService) {
public get running(): boolean {
return this._running;
}
public get starting(): boolean {
return this.daemonService.starting;
}
public get stopping(): boolean {
return this.daemonService.stopping;
}
constructor(private navbarService: NavbarService, private daemonService: DaemonService, private ngZone: NgZone) {
this.daemonService.isRunning().then((running: boolean) => {
this.ngZone.run(() => {
this._running = running;
});
});
this.daemonService.onDaemonStatusChanged.subscribe((running: boolean) => {
this.ngZone.run(() => {
this._running = running;
});
});
}
}

View file

@ -3,14 +3,15 @@ import { CommonModule } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
import { PageNotFoundComponent, SidebarComponent, DaemonNotRunningComponent } from './components/';
import { PageNotFoundComponent, SidebarComponent, DaemonNotRunningComponent, DaemonStoppingComponent } from './components/';
import { WebviewDirective } from './directives/';
import { FormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { NavbarComponent } from './components/navbar/navbar.component';
@NgModule({
declarations: [PageNotFoundComponent, SidebarComponent, DaemonNotRunningComponent, WebviewDirective],
declarations: [PageNotFoundComponent, SidebarComponent, DaemonNotRunningComponent, DaemonStoppingComponent, NavbarComponent, WebviewDirective],
imports: [CommonModule, TranslateModule, FormsModule, RouterModule],
exports: [TranslateModule, WebviewDirective, FormsModule, SidebarComponent, DaemonNotRunningComponent]
exports: [TranslateModule, WebviewDirective, FormsModule, SidebarComponent, DaemonNotRunningComponent, DaemonStoppingComponent, NavbarComponent]
})
export class SharedModule {}

View file

@ -7,6 +7,7 @@
"sourceMap": true,
"declaration": false,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowJs": true,

View file

@ -5,6 +5,7 @@
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"module": "commonjs",
"target": "es2015",
"types": [