diff --git a/api/src/main/kotlin/nebulosa/api/mounts/MountController.kt b/api/src/main/kotlin/nebulosa/api/mounts/MountController.kt index 5601da2e6..9cf50ca60 100644 --- a/api/src/main/kotlin/nebulosa/api/mounts/MountController.kt +++ b/api/src/main/kotlin/nebulosa/api/mounts/MountController.kt @@ -142,7 +142,7 @@ class MountController( val id = pathParameters[ID].notNull() val mount = connectionService.mount(id) ?: return val rate = queryParameters["rate"].notNullOrBlank() - mountService.slewRate(mount, mount.slewRates.first { it.name == rate }) + mountService.slewRate(mount, mount.slewRates.first { it.value == rate }) } private fun move(ctx: RoutingContext) = with(ctx.call) { diff --git a/api/src/main/kotlin/nebulosa/api/mounts/MountMoveTask.kt b/api/src/main/kotlin/nebulosa/api/mounts/MountMoveTask.kt index 4f228bfa7..6b9f83757 100644 --- a/api/src/main/kotlin/nebulosa/api/mounts/MountMoveTask.kt +++ b/api/src/main/kotlin/nebulosa/api/mounts/MountMoveTask.kt @@ -24,7 +24,7 @@ data class MountMoveTask( LOG.d { debug("Mount Move started. mount={}, request={}", mount, request) } mount.slewRates.takeIf { !request.speed.isNullOrBlank() } - ?.find { it.name == request.speed } + ?.find { it.value == request.speed } ?.also { mount.slewRate(it) } start() diff --git a/desktop/src/app/about/about.component.html b/desktop/src/app/about/about.component.html index 88293872e..68b8b481d 100644 --- a/desktop/src/app/about/about.component.html +++ b/desktop/src/app/about/about.component.html @@ -13,16 +13,13 @@

- Nebulosa - - + Nebulosa + +

{{ description }} @@ -57,13 +54,14 @@

- @for (dep of dependencies; track $index) { - - {{ dep.name }} ({{ dep.version }}) - + @for (dep of dependencies; track dep.name) { + @if (dep.link) { + + {{ dep.name }} ({{ dep.version }}) + + } }
@@ -76,12 +74,12 @@ href="https://www.paypal.com/donate/?hosted_button_id=U8TGGJTKSZUCA"> + style="height: 28px" /> - diff --git a/desktop/src/app/alignment/alignment.component.html b/desktop/src/app/alignment/alignment.component.html index 8ec67c881..3688976ad 100644 --- a/desktop/src/app/alignment/alignment.component.html +++ b/desktop/src/app/alignment/alignment.component.html @@ -7,14 +7,11 @@ [devices]="cameras" [(device)]="camera" (deviceChange)="cameraChanged()" /> - + (action)="showCameraDialog()" + tooltip="Camera" /> @if (tab === 0) { -
- - -
+ @if (method === 'TPPA') { +
+ + +
+ }
@@ -62,64 +60,40 @@
- - - - +
- - - - +
- - - - +
- - - - +
- + [(value)]="tppaRequest.stopTrackingWhenDone" + (valueChange)="savePreference()" />
@@ -141,66 +115,51 @@
@if (pausingOrPaused) { - + severity="success" /> } @else if (!running) { - + severity="success" /> + } @else { + } - - - + + class="ml-4" + tooltip="View image" + tooltipPosition="top" />
- - - - +
- - - - +
- - + - + + class="ml-4" + tooltip="View image" + tooltipPosition="top" />
- + (action)="tab === 0 ? tppaInfo.toggle($event) : darvInfo.toggle($event)" /> ('cameraExposure') get pausingOrPaused() { return this.status === 'PAUSING' || this.status === 'PAUSED' @@ -170,7 +169,7 @@ export class AlignmentComponent implements AfterViewInit, OnDestroy, Tickable { this.tppaResult.altitudeErrorDirection = event.altitudeErrorDirection this.tppaResult.totalError = event.totalError } else if (event.state === 'FINISHED') { - this.cameraExposure.reset() + this.cameraExposure().reset() } else if (event.state === 'SOLVED' || event.state === 'SLEWED') { this.tppaResult.failed = false this.tppaResult.rightAscension = event.rightAscension @@ -180,7 +179,7 @@ export class AlignmentComponent implements AfterViewInit, OnDestroy, Tickable { } if (event.capture.state !== 'CAPTURE_FINISHED') { - this.cameraExposure.handleCameraCaptureEvent(event.capture, true) + this.cameraExposure().handleCameraCaptureEvent(event.capture, true) } }) } @@ -190,7 +189,7 @@ export class AlignmentComponent implements AfterViewInit, OnDestroy, Tickable { if (event.camera.id === this.camera?.id) { ngZone.run(() => { this.status = event.state - this.running = this.cameraExposure.handleCameraCaptureEvent(event.capture) + this.running = this.cameraExposure().handleCameraCaptureEvent(event.capture) if (event.state === 'FORWARD' || event.state === 'BACKWARD') { this.darvResult.direction = event.direction @@ -243,7 +242,7 @@ export class AlignmentComponent implements AfterViewInit, OnDestroy, Tickable { const mount = await this.api.mount(this.mount.id) Object.assign(this.mount, mount) this.loadPreference() - this.tppaRequest.stepSpeed = mount.slewRate?.name + this.tppaRequest.stepSpeed = mount.slewRate?.value } } diff --git a/desktop/src/app/app.component.html b/desktop/src/app/app.component.html index 3090141e0..6881ec522 100644 --- a/desktop/src/app/app.component.html +++ b/desktop/src/app/app.component.html @@ -1,62 +1,45 @@ -
- -
{{ title }}
-
{{ subTitle }}
-
- - - - - - - -
+@if (showTopBar) { +
+ +
{{ title }}
+
{{ subTitle }}
+
+ @if (topMenu.length) { + + } + @if (!modal) { + @if (topMenu.length) { + + } + @if (pinned) { + + } @else { + + } + + @if (maximizable) { + + } + } + +
+}
diff --git a/desktop/src/app/app.component.ts b/desktop/src/app/app.component.ts index 45bd251f0..f73dd1a34 100644 --- a/desktop/src/app/app.component.ts +++ b/desktop/src/app/app.component.ts @@ -2,7 +2,7 @@ import { Component, ElementRef, HostListener, NgZone, OnDestroy, inject } from ' import { Title } from '@angular/platform-browser' import hotkeys from 'hotkeys-js' import { APP_CONFIG } from '../environments/environment' -import { MenuItem } from '../shared/components/menu-item/menu-item.component' +import { MenuItem } from '../shared/components/menu-item.component' import { ConfirmationService } from '../shared/services/confirmation.service' import { ElectronService } from '../shared/services/electron.service' diff --git a/desktop/src/app/app.module.ts b/desktop/src/app/app.module.ts index 45af10547..bd5cdd25c 100644 --- a/desktop/src/app/app.module.ts +++ b/desktop/src/app/app.module.ts @@ -45,20 +45,30 @@ import { TieredMenuModule } from 'primeng/tieredmenu' import { ToastModule } from 'primeng/toast' import { TooltipModule } from 'primeng/tooltip' import { TreeModule } from 'primeng/tree' -import { CameraExposureComponent } from '../shared/components/camera-exposure/camera-exposure.component' -import { CameraInfoComponent } from '../shared/components/camera-info/camera-info.component' -import { DeviceChooserComponent } from '../shared/components/device-chooser/device-chooser.component' -import { DeviceListMenuComponent } from '../shared/components/device-list-menu/device-list-menu.component' -import { DeviceNameComponent } from '../shared/components/device-name/device-name.component' -import { DialogMenuComponent } from '../shared/components/dialog-menu/dialog-menu.component' -import { HistogramComponent } from '../shared/components/histogram/histogram.component' -import { LocationComponent } from '../shared/components/location/location.dialog' -import { MapComponent } from '../shared/components/map/map.component' -import { MenuBarComponent } from '../shared/components/menu-bar/menu-bar.component' -import { MenuItemComponent } from '../shared/components/menu-item/menu-item.component' -import { MoonComponent } from '../shared/components/moon/moon.component' -import { PathChooserComponent } from '../shared/components/path-chooser/path-chooser.component' -import { SlideMenuComponent } from '../shared/components/slide-menu/slide-menu.component' +import { ButtonImageComponent } from '../shared/components/button-image.component' +import { ButtonComponent } from '../shared/components/button.component' +import { CameraExposureComponent } from '../shared/components/camera-exposure.component' +import { CameraInfoComponent } from '../shared/components/camera-info.component' +import { CheckboxComponent } from '../shared/components/checkbox.component' +import { DeviceChooserComponent } from '../shared/components/device-chooser.component' +import { DeviceListMenuComponent } from '../shared/components/device-list-menu.component' +import { DeviceNameComponent } from '../shared/components/device-name.component' +import { DialogMenuComponent } from '../shared/components/dialog-menu.component' +import { DropdownComponent, DropdownEnumComponent, DropdownItemComponent } from '../shared/components/dropdown.component' +import { HistogramComponent } from '../shared/components/histogram.component' +import { IndicatorComponent } from '../shared/components/indicator.component' +import { InputNumberComponent } from '../shared/components/input-number.component' +import { InputTextComponent } from '../shared/components/input-text.component' +import { LocationComponent } from '../shared/components/location.component' +import { MapComponent } from '../shared/components/map.component' +import { MenuBarComponent } from '../shared/components/menu-bar.component' +import { MenuItemComponent } from '../shared/components/menu-item.component' +import { MoonComponent } from '../shared/components/moon.component' +import { PathChooserComponent } from '../shared/components/path-chooser.component' +import { SelectButtonEnumComponent, SelectButtonItemComponent } from '../shared/components/select-button.component' +import { SlideMenuComponent } from '../shared/components/slide-menu.component' +import { SwitchComponent } from '../shared/components/switch.component' +import { TagComponent } from '../shared/components/tag.component' import { ConfirmDialogComponent } from '../shared/dialogs/confirm/confirm.dialog' import { NoDropdownDirective } from '../shared/directives/no-dropdown.directive' import { SpinnableNumberDirective } from '../shared/directives/spinnable-number.directive' @@ -109,21 +119,27 @@ import { SettingsComponent } from './settings/settings.component' AppComponent, AtlasComponent, AutoFocusComponent, + ButtonComponent, + ButtonImageComponent, CalculatorComponent, CalibrationComponent, CameraComponent, - CameraInfoComponent, CameraExposureComponent, + CameraInfoComponent, + CheckboxComponent, ConfirmDialogComponent, CrossHairComponent, DeviceChooserComponent, DeviceListMenuComponent, DeviceNameComponent, DialogMenuComponent, + DropdownComponent, + DropdownItemComponent, + DropdownEnumComponent, DropdownOptionsPipe, DustCapComponent, - EnumPipe, EnumDropdownPipe, + EnumPipe, EnvPipe, ExposureTimeComponent, ExposureTimePipe, @@ -136,9 +152,11 @@ import { SettingsComponent } from './settings/settings.component' HistogramComponent, HomeComponent, ImageComponent, - SpinnableNumberDirective, + IndicatorComponent, INDIComponent, INDIPropertyComponent, + InputNumberComponent, + InputTextComponent, LightBoxComponent, LocationComponent, MapComponent, @@ -149,9 +167,14 @@ import { SettingsComponent } from './settings/settings.component' NoDropdownDirective, PathChooserComponent, RotatorComponent, + SelectButtonEnumComponent, + SelectButtonItemComponent, SequencerComponent, SettingsComponent, SlideMenuComponent, + SpinnableNumberDirective, + TagComponent, + SwitchComponent, WinPipe, ], imports: [ diff --git a/desktop/src/app/atlas/atlas.component.html b/desktop/src/app/atlas/atlas.component.html index 93a870912..9b1d2b352 100644 --- a/desktop/src/app/atlas/atlas.component.html +++ b/desktop/src/app/atlas/atlas.component.html @@ -21,29 +21,28 @@ - - SDO/HMI - -
-
-
- @for (season of sun.seasons; track $index) { -
- - - {{ seasonName(season.name) | uppercase }} - - {{ season.dateTime | date: 'MM-dd HH:mm' }} -
- } + @if (sun.seasons.length) { +
+ +
+ @for (season of sun.seasons; track $index) { +
+ + + {{ seasonName(season.name) | uppercase }} + + {{ season.dateTime | date: 'MM-dd HH:mm' }} +
+ } +
-
+ }
-
-
-
- Diameter - {{ moon.phases.current.diameter }} arcsec - {{ (moon.phases.current.diameter / 60).toFixed(2) }} arcmin -
-
- Age - {{ moon.phases.current.age }} d -
-
- Lunation - {{ moon.phases.current.lunation }} -
-
- Sub-Solar Lon / Lat - {{ moon.phases.current.subSolarLon }}° / {{ moon.phases.current.subSolarLat }}° -
-
- Sub-Earth Lon / Lat - {{ moon.phases.current.subEarthLon }}° / {{ moon.phases.current.subEarthLat }}° -
-
- Position Angle - {{ moon.phases.current.posAngle }}° + @if (moon.phases) { +
+
+
+ Diameter + {{ moon.phases.current.diameter }} arcsec + {{ (moon.phases.current.diameter / 60).toFixed(2) }} arcmin +
+
+ Age + {{ moon.phases.current.age }} d +
+
+ Lunation + {{ moon.phases.current.lunation }} +
+
+ Sub-Solar Lon / Lat + {{ moon.phases.current.subSolarLon }}° / {{ moon.phases.current.subSolarLat }}° +
+
+ Sub-Earth Lon / Lat + {{ moon.phases.current.subEarthLon }}° / {{ moon.phases.current.subEarthLat }}° +
+
+ Position Angle + {{ moon.phases.current.posAngle }}° +
+
-
- - NASA/SVS - +
+ @for (phase of moon.phases.phases; track $index) { +
+ + @let phaseIcon = phase.name === 'NEW_MOON' ? 'new' : phase.name === 'FIRST_QUARTER' ? 'first-quarter' : phase.name === 'FULL_MOON' ? 'full' : 'last-quarter'; + + {{ phase.name | enum | uppercase }} + + {{ phase.dateTime | date: 'dd HH:mm' }} +
+ }
-
- @for (phase of moon.phases.phases; track $index) { -
- - @let phaseIcon = phase.name === 'NEW_MOON' ? 'new' : phase.name === 'FIRST_QUARTER' ? 'first-quarter' : phase.name === 'FULL_MOON' ? 'full' : 'last-quarter'; - - {{ phase.name | enum | uppercase }} - - {{ phase.dateTime | date: 'dd HH:mm' }} -
- } -
-
+ }
- - - - - + + severity="info" />
- - - - - - - - - + + + severity="info" />
- - - - +
- - + + tooltip="Filter" />
@@ -416,29 +380,19 @@
- - - - +
- - + + tooltip="Filter" />
@@ -481,10 +435,9 @@ {{ item.id }} {{ item.name }} - + @for (group of item.groups; track $index) { + + } @@ -505,114 +458,70 @@
- - - - +
- - - - +
- - - - +
- - - - +
- - - - +
- - - - +
- - - - +
- - - - +
- - - - +
- - - - +
- - - - +
@@ -626,37 +535,29 @@
- - + - + - + + label="Frame" />
@@ -675,30 +576,25 @@
- + (action)="ephemerisMenu.show(ephemerisModel)" /> @if (body.name && canFavorite) { - + tooltip="{{ favorited ? 'Unfavorite' : 'Favorite' }}" + (action)="favorite()" /> } {{ body.name }} - + @for (tag of body.tags; track $index) { + + }
@@ -727,117 +623,63 @@ [style]="{ width: '80vw' }">
- - - - +
- - - - +
- - - - +
- - @let constellations = ['ALL'].concat('CONSTELLATION' | dropdownOptions); - - - + @let constellations = ['ALL'].concat('CONSTELLATION' | dropdownOptions); +
- - @let skyObjectTypes = ['ALL'].concat('SKY_OBJECT_TYPE' | dropdownOptions); - - - + @let skyObjectTypes = ['ALL'].concat('SKY_OBJECT_TYPE' | dropdownOptions); +
- - - - +
- - - - +
- + (action)="searchSkyObject()" /> @@ -852,29 +694,24 @@ @let groups = 'SATELLITE_GROUP_TYPE' | dropdownOptions; @for (group of groups; track $index) { - + [noWrap]="true" /> }
- - + + (action)="searchSatellite()" /> @@ -886,10 +723,9 @@ [style]="{ width: '80vw' }">
-
@@ -914,55 +750,35 @@ styleClass="w-full" />
-
- Date Time - -
+
- - - - - - - - + +
- - - - +
@@ -995,15 +811,11 @@ {{ item.type | enum }} {{ item.name }} - + tooltip="Remove" + (action)="deleteFavorite(item)" /> @@ -1013,7 +825,6 @@ diff --git a/desktop/src/app/atlas/atlas.component.ts b/desktop/src/app/atlas/atlas.component.ts index 96a517a40..34cf981da 100644 --- a/desktop/src/app/atlas/atlas.component.ts +++ b/desktop/src/app/atlas/atlas.component.ts @@ -1,4 +1,4 @@ -import { AfterContentInit, AfterViewInit, Component, HostListener, NgZone, OnDestroy, OnInit, ViewChild, ViewEncapsulation, inject } from '@angular/core' +import { AfterContentInit, AfterViewInit, Component, HostListener, NgZone, OnDestroy, OnInit, ViewEncapsulation, inject, viewChild } from '@angular/core' import { ActivatedRoute } from '@angular/router' import { Chart, ChartData, ChartOptions } from 'chart.js' import zoomPlugin from 'chartjs-plugin-zoom' @@ -6,8 +6,8 @@ import { UIChart } from 'primeng/chart' import { ListboxChangeEvent } from 'primeng/listbox' import { OverlayPanel } from 'primeng/overlaypanel' import { timer } from 'rxjs' -import { DeviceListMenuComponent } from '../../shared/components/device-list-menu/device-list-menu.component' -import { SlideMenuItem } from '../../shared/components/menu-item/menu-item.component' +import { DeviceListMenuComponent } from '../../shared/components/device-list-menu.component' +import { SlideMenuItem } from '../../shared/components/menu-item.component' import { ONE_DECIMAL_PLACE_FORMATTER, TWO_DIGITS_FORMATTER } from '../../shared/constants' import { AngularService } from '../../shared/services/angular.service' import { ApiService } from '../../shared/services/api.service' @@ -346,17 +346,10 @@ export class AtlasComponent implements OnInit, AfterContentInit, AfterViewInit, }, ] - @ViewChild('deviceMenu') - private readonly deviceMenu!: DeviceListMenuComponent - - @ViewChild('dateTimeAndLocationPanel') - private readonly dateTimeAndLocationPanel!: OverlayPanel - - @ViewChild('favoritesPanel') - private readonly favoritesPanel!: OverlayPanel - - @ViewChild('chart') - private readonly chart!: UIChart + private readonly deviceMenu = viewChild.required('deviceMenu') + private readonly dateTimeAndLocationPanel = viewChild.required('dateTimeAndLocationPanel') + private readonly favoritesPanel = viewChild.required('favoritesPanel') + private readonly chart = viewChild.required('chart') get body() { switch (this.tab) { @@ -409,7 +402,7 @@ export class AtlasComponent implements OnInit, AfterContentInit, AfterViewInit, icon: 'mdi mdi-bookmark', tooltip: 'Favorites', command: (e) => { - this.favoritesPanel.toggle(e.originalEvent) + this.favoritesPanel().toggle(e.originalEvent) }, }) @@ -417,7 +410,7 @@ export class AtlasComponent implements OnInit, AfterContentInit, AfterViewInit, icon: 'mdi mdi-calendar', tooltip: 'Date Time and Location', command: (e) => { - this.dateTimeAndLocationPanel.toggle(e.originalEvent) + this.dateTimeAndLocationPanel().toggle(e.originalEvent) }, }) @@ -858,7 +851,7 @@ export class AtlasComponent implements OnInit, AfterContentInit, AfterViewInit, [24.0, 90], ] - this.chart.refresh() + this.chart().refresh() } private async refreshChart(force: boolean = false) { @@ -908,7 +901,7 @@ export class AtlasComponent implements OnInit, AfterContentInit, AfterViewInit, } } finally { this.updateAltitudeChart() - this.chart.refresh() + this.chart().refresh() } } @@ -978,6 +971,6 @@ export class AtlasComponent implements OnInit, AfterContentInit, AfterViewInit, private async executeMount(action: (mount: Mount) => void | Promise, showConfirmation: boolean = true) { const mounts = await this.api.mounts() - return this.deviceService.executeAction(this.deviceMenu, mounts, action, showConfirmation) + return this.deviceService.executeAction(this.deviceMenu(), mounts, action, showConfirmation) } } diff --git a/desktop/src/app/autofocus/autofocus.component.html b/desktop/src/app/autofocus/autofocus.component.html index 359e8a478..88c9db393 100644 --- a/desktop/src/app/autofocus/autofocus.component.html +++ b/desktop/src/app/autofocus/autofocus.component.html @@ -6,14 +6,11 @@ [devices]="cameras" [(device)]="camera" (deviceChange)="cameraChanged()" /> - + (action)="showCameraDialog()" + tooltip="Camera" />
- - - - +
- - - - +
- - - - +
@@ -72,118 +57,72 @@ leftIcon="mdi mdi-wrench">
- - - - +
- - - - +
- - - - +
- - - - +
- - - - +
- - - - +
- - - - +
- - - - +
@@ -205,30 +144,24 @@
- - + - + + class="ml-4" + tooltip="View image" + tooltipPosition="top" />
diff --git a/desktop/src/app/autofocus/autofocus.component.ts b/desktop/src/app/autofocus/autofocus.component.ts index 2e5bf3e66..8b4878f0d 100644 --- a/desktop/src/app/autofocus/autofocus.component.ts +++ b/desktop/src/app/autofocus/autofocus.component.ts @@ -1,8 +1,8 @@ -import { AfterViewInit, Component, HostListener, NgZone, OnDestroy, ViewChild, inject } from '@angular/core' +import { AfterViewInit, Component, HostListener, NgZone, OnDestroy, inject, viewChild } from '@angular/core' import { ChartData, ChartOptions } from 'chart.js' import { Point } from 'electron' import { UIChart } from 'primeng/chart' -import { CameraExposureComponent } from '../../shared/components/camera-exposure/camera-exposure.component' +import { CameraExposureComponent } from '../../shared/components/camera-exposure.component' import { ApiService } from '../../shared/services/api.service' import { BrowserWindowService } from '../../shared/services/browser-window.service' import { ElectronService } from '../../shared/services/electron.service' @@ -205,11 +205,8 @@ export class AutoFocusComponent implements AfterViewInit, OnDestroy, Tickable { ], } - @ViewChild('cameraExposure') - private readonly cameraExposure!: CameraExposureComponent - - @ViewChild('chart') - private readonly chart!: UIChart + private readonly cameraExposure = viewChild.required('cameraExposure') + private readonly chart = viewChild.required('chart') private get trendLineLeftDataset() { return this.chartData.datasets[0] @@ -314,7 +311,7 @@ export class AutoFocusComponent implements AfterViewInit, OnDestroy, Tickable { this.starHFD = event.starHFD if (event.capture) { - this.cameraExposure.handleCameraCaptureEvent(event.capture, true) + this.cameraExposure().handleCameraCaptureEvent(event.capture, true) } if (state === 'CURVE_FITTED') { @@ -440,7 +437,7 @@ export class AutoFocusComponent implements AfterViewInit, OnDestroy, Tickable { zoom.limits!['x']!.max = scales['x']!.max zoom.limits!['y']!.max = scales['y']!.max - this.chart.refresh() + this.chart().refresh() } private clearChart() { @@ -450,7 +447,7 @@ export class AutoFocusComponent implements AfterViewInit, OnDestroy, Tickable { dataset.data = [] } - this.chart.refresh() + this.chart().refresh() } private loadPreference() { diff --git a/desktop/src/app/calculator/calculator.component.html b/desktop/src/app/calculator/calculator.component.html index 40d22736c..865e4d02c 100644 --- a/desktop/src/app/calculator/calculator.component.html +++ b/desktop/src/app/calculator/calculator.component.html @@ -1,27 +1,21 @@
- - -
- {{ item?.formula?.title }} -
-
- -
- {{ item.formula.title }} + +
+ {{ item.formula.title }} + + @if (!selected) { {{ item.formula.description }} -
-
- + } +
+
+
diff --git a/desktop/src/app/calculator/formula/formula.component.html b/desktop/src/app/calculator/formula/formula.component.html index d3115cbd5..6320fb7e0 100644 --- a/desktop/src/app/calculator/formula/formula.component.html +++ b/desktop/src/app/calculator/formula/formula.component.html @@ -1,30 +1,24 @@ -

{{ formula.description }}

+@let mFormula = formula(); + +

{{ mFormula.description }}

- +
- @for (item of formula.operands; track $index) { + @for (item of mFormula.operands; track item.label) {
{{ item.prefix }}
- - - - +
{{ item.suffix }} @@ -34,24 +28,18 @@

=

- {{ formula.result.prefix }} - - - - - {{ formula.result.suffix }} -
-
- + {{ mFormula.result.prefix }} + + {{ mFormula.result.suffix }}
+@if (mFormula.tip) { +
+ +
+} diff --git a/desktop/src/app/calculator/formula/formula.component.ts b/desktop/src/app/calculator/formula/formula.component.ts index 181e86e6d..802281f4c 100644 --- a/desktop/src/app/calculator/formula/formula.component.ts +++ b/desktop/src/app/calculator/formula/formula.component.ts @@ -1,4 +1,4 @@ -import { Component, Input } from '@angular/core' +import { Component, input } from '@angular/core' import type { CalculatorFormula } from '../../../shared/types/calculator.types' @Component({ @@ -7,14 +7,14 @@ import type { CalculatorFormula } from '../../../shared/types/calculator.types' styleUrls: ['formula.component.scss'], }) export class FormulaComponent { - @Input({ required: true }) - protected readonly formula!: CalculatorFormula + protected readonly formula = input.required() calculateFormula() { - const result = this.formula.calculate(...this.formula.operands.map((e) => e.value)) + const formula = this.formula() + const result = formula.calculate(...formula.operands.map((e) => e.value)) if (result !== undefined) { - this.formula.result.value = result + formula.result.value = result } } } diff --git a/desktop/src/app/calibration/calibration.component.html b/desktop/src/app/calibration/calibration.component.html index 53a446168..c65a706bb 100644 --- a/desktop/src/app/calibration/calibration.component.html +++ b/desktop/src/app/calibration/calibration.component.html @@ -1,22 +1,22 @@
- @for (key of groups; track $index) { + @for (key of groups; track key) { @let value = frames.get(key) ?? [];
- - + + severity="success" + (action)="openDirectoryToUpload(key)" />
{{ this.frames.get(key)?.length ?? 0 }} frames
@@ -25,12 +25,9 @@ [model]="groupModel" (onShow)="activeGroup = key" [popup]="true" /> - + (action)="groupMenu.toggle($event)" />
@@ -52,55 +49,39 @@
- +
{{ item.type }}
{{ item.exposureTime | exposureTime }} {{ item.width }}x{{ item.height }} {{ item.binX }}x{{ item.binY }} - GAIN: {{ item.gain }} - - {{ item.temperature }}°C - + @if (item.gain) { + GAIN: {{ item.gain }} + } + @if (item.temperature < 100 && item.temperature > -100) { + {{ item.temperature }}°C + }
{{ item.path }}
- - + - + + tooltip="Remove" + (action)="deleteFrame(item, frameListBox)" />
@@ -122,22 +103,16 @@ [style]="{ width: '80vw', maxWidth: '240px' }">
- - - - +
- + (action)="groupDialog.save?.()" /> diff --git a/desktop/src/app/calibration/calibration.component.ts b/desktop/src/app/calibration/calibration.component.ts index e4d4fbbed..786850f51 100644 --- a/desktop/src/app/calibration/calibration.component.ts +++ b/desktop/src/app/calibration/calibration.component.ts @@ -1,6 +1,6 @@ -import { AfterViewInit, Component, HostListener, OnDestroy, QueryList, ViewChildren, ViewEncapsulation, inject } from '@angular/core' +import { AfterViewInit, Component, HostListener, OnDestroy, ViewEncapsulation, inject, viewChildren } from '@angular/core' import { Listbox } from 'primeng/listbox' -import { MenuItem } from '../../shared/components/menu-item/menu-item.component' +import { MenuItem } from '../../shared/components/menu-item.component' import { SEPARATOR_MENU_ITEM } from '../../shared/constants' import { ApiService } from '../../shared/services/api.service' import { BrowserWindowService } from '../../shared/services/browser-window.service' @@ -135,8 +135,7 @@ export class CalibrationComponent implements AfterViewInit, OnDestroy { }, ] - @ViewChildren('frameListBox') - private readonly frameListBoxes!: QueryList + private readonly frameListBoxes = viewChildren('frameListBox') get groups() { return Array.from(this.frames.keys()).sort(textComparator) @@ -403,7 +402,7 @@ export class CalibrationComponent implements AfterViewInit, OnDestroy { } private markFrameListBoxesForCheck() { - for (const box of this.frameListBoxes) { + for (const box of this.frameListBoxes()) { box.cd.markForCheck() } } diff --git a/desktop/src/app/camera/camera.component.html b/desktop/src/app/camera/camera.component.html index 407d6f879..73b6f7d5c 100644 --- a/desktop/src/app/camera/camera.component.html +++ b/desktop/src/app/camera/camera.component.html @@ -2,168 +2,146 @@
- - + @if (camera.connected) { + + } @else { + + }
- - - -
-
- - - - - - - - - {{ preference.request.savePath || camera.capturesPath }} - -
-
- + (action)="openCameraImage()" + tooltip="View image" /> + @if (hasCalibration) { + + } + @if (canShowMenu) { + + }
+ @if (canShowSavePath) { +
+ @if (request.autoSave) { + + } @else { + + } + @if (request.autoSubFolderMode === 'OFF') { + + } @else if (request.autoSubFolderMode === 'NOON') { + + } @else if (request.autoSubFolderMode === 'MIDNIGHT') { + + } + @if (camera && (preference.request.savePath || camera.capturesPath)) { + + + @if (preference.request.savePath || camera.capturesPath) { + + } + {{ preference.request.savePath || camera.capturesPath }} + + } +
+ } + @if (canShowInfo) { +
+ +
+ }
-
- Cooler ({{ camera.coolerPower | number: '1.1-1' }}%) - + + [(value)]="camera!.cooler" + (valueChange)="toggleCooler()" />
-
- Dew heater - + + [(value)]="camera!.dewHeater" />
- - - - - + + tooltip="Apply" />
@@ -183,356 +161,244 @@ class="w-full" />
- - - - +
Exposure Mode - + [options]="'EXPOSURE_MODE' | dropdownOptions" + [(value)]="preference.exposureMode" + (valueChange)="savePreference()" />
- - - - +
- - - - +
- - - - +
- - - - +
- - - - +
- - - - +
-
- Subframe - + + [(value)]="preference.subFrame" + (valueChange)="savePreference()" />
-
- + + tooltip="Full size" />
- - - - +
- - - - +
- - - - +
- - - - +
- - - - +
- - - - - - - + @if (hasCalibration) { + + } + @if (hasDither) { + + } + @if (hasLiveStacking) { + + } + @if (preference.mount) { + + } + @if (preference.focuser) { + + } + @if (preference.wheel) { + + } + @if (preference.rotator) { + + }
@if (pausingOrPaused) { - + @if (canStartOrAbort) { + + } } @else if (!running) { - + @if (canStartOrAbort) { + + } + } + @if (canStartOrAbort && running && !pausingOrPaused) { + + } + @if (canStartOrAbort) { + + } + @if (canSave) { + } - - -
Dither -
- Enabled - +
+
-
- RA only - + + [(value)]="dither.request.raOnly" + (valueChange)="savePreference()" />
- - - - +
- - - - +
@@ -604,43 +456,36 @@
Live Stacking -
- Enabled - +
+
- - - - -
-
- 32-bit (slower) - +
+
+ + [(value)]="liveStacking.request.use32Bits" + (valueChange)="savePreference()" />
- + [(value)]="liveStacking.request.useCalibrationGroup" + (valueChange)="savePreference()" />
- - - - - + + (action)="resetCameraCaptureNamingFormat('LIGHT')" + tooltip="Reset" />
- - - - - + + (action)="resetCameraCaptureNamingFormat('DARK')" + tooltip="Reset" />
- - - - - + + (action)="resetCameraCaptureNamingFormat('FLAT')" + tooltip="Reset" />
- - - - - + + (action)="resetCameraCaptureNamingFormat('BIAS')" + tooltip="Reset" />
diff --git a/desktop/src/app/camera/camera.component.ts b/desktop/src/app/camera/camera.component.ts index b2436f489..790e6aa63 100644 --- a/desktop/src/app/camera/camera.component.ts +++ b/desktop/src/app/camera/camera.component.ts @@ -1,7 +1,7 @@ -import { AfterContentInit, Component, HostListener, inject, NgZone, OnDestroy, ViewChild } from '@angular/core' +import { AfterContentInit, Component, HostListener, inject, NgZone, OnDestroy, viewChild } from '@angular/core' import { ActivatedRoute } from '@angular/router' -import { CameraExposureComponent } from '../../shared/components/camera-exposure/camera-exposure.component' -import { MenuItemCommandEvent, SlideMenuItem } from '../../shared/components/menu-item/menu-item.component' +import { CameraExposureComponent } from '../../shared/components/camera-exposure.component' +import { MenuItemCommandEvent, SlideMenuItem } from '../../shared/components/menu-item.component' import { SEPARATOR_MENU_ITEM } from '../../shared/constants' import { ApiService } from '../../shared/services/api.service' import { BrowserWindowService } from '../../shared/services/browser-window.service' @@ -123,11 +123,10 @@ export class CameraComponent implements AfterContentInit, OnDestroy, Tickable { format: this.request.namingFormat, } - @ViewChild('cameraExposure') - private readonly cameraExposure?: CameraExposureComponent + private readonly cameraExposure = viewChild('cameraExposure') get status() { - return this.cameraExposure?.currentState ?? 'IDLE' + return this.cameraExposure()?.currentState ?? 'IDLE' } get pausingOrPaused() { @@ -219,7 +218,7 @@ export class CameraComponent implements AfterContentInit, OnDestroy, Tickable { this.electronService.on('CAMERA.CAPTURE_ELAPSED', (event) => { if (event.camera.id === this.camera.id) { ngZone.run(() => { - this.running = this.cameraExposure?.handleCameraCaptureEvent(event) ?? false + this.running = this.cameraExposure()?.handleCameraCaptureEvent(event) ?? false }) } }) diff --git a/desktop/src/app/camera/exposure-time.component.html b/desktop/src/app/camera/exposure-time.component.html index fe9cffe41..10dc08676 100644 --- a/desktop/src/app/camera/exposure-time.component.html +++ b/desktop/src/app/camera/exposure-time.component.html @@ -1,31 +1,22 @@
- - - - + -
diff --git a/desktop/src/app/camera/exposure-time.component.ts b/desktop/src/app/camera/exposure-time.component.ts index 8569e4831..8f1777e8b 100644 --- a/desktop/src/app/camera/exposure-time.component.ts +++ b/desktop/src/app/camera/exposure-time.component.ts @@ -1,5 +1,5 @@ -import { AfterViewInit, ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges, ViewEncapsulation } from '@angular/core' -import { MenuItem } from '../../shared/components/menu-item/menu-item.component' +import { AfterViewInit, ChangeDetectionStrategy, Component, OnChanges, SimpleChanges, ViewEncapsulation, input, model, signal } from '@angular/core' +import { MenuItem } from '../../shared/components/menu-item.component' import type { ExposureTimeUnit } from '../../shared/types/camera.types' @Component({ @@ -9,44 +9,15 @@ import type { ExposureTimeUnit } from '../../shared/types/camera.types' changeDetection: ChangeDetectionStrategy.OnPush, }) export class ExposureTimeComponent implements AfterViewInit, OnChanges { - @Input({ required: true }) - protected exposureTime: number = 0 - - @Output() - readonly exposureTimeChange = new EventEmitter() - - @Input() - protected unit: ExposureTimeUnit = 'MICROSECOND' - - @Output() - readonly unitChange = new EventEmitter() - - @Input() - protected readonly min: number = 0 - - @Input() - protected readonly max: number = 600000000 - - @Input() - protected readonly disabled: boolean = false - - @Input() - protected readonly canExposureTime: boolean = true - - @Input() - protected readonly canExposureTimeUnit: boolean = true - - @Input() - protected readonly normalized: boolean = true - - @Input() - protected readonly label?: string - - protected readonly current = { - exposureTime: this.exposureTime, - min: this.min, - max: this.max, - } + readonly exposureTime = model.required() + readonly unit = model('MICROSECOND') + readonly min = input(0) + readonly max = input(600000000) + readonly disabled = input(false) + readonly canExposureTime = input(true) + readonly canExposureTimeUnit = input(true) + readonly normalized = input(true) + readonly label = input() protected readonly model: MenuItem[] = [ { @@ -75,7 +46,11 @@ export class ExposureTimeComponent implements AfterViewInit, OnChanges { }, ] - private exposureTimeInMicroseconds = 0 + protected currentExposureTime = 0 + protected currentMin = 0 + protected currentMax = 0 + + private readonly exposureTimeInMicroseconds = signal(0) ngOnChanges(changes: SimpleChanges) { for (const key in changes) { @@ -88,39 +63,39 @@ export class ExposureTimeComponent implements AfterViewInit, OnChanges { this.exposureTimeUnitChanged(change.currentValue) break case 'exposureTime': - this.exposureTimeChanged(change.currentValue, 'MICROSECOND', this.normalized && this.exposureTimeInMicroseconds !== change.currentValue) + this.exposureTimeChanged(change.currentValue, 'MICROSECOND', this.normalized() && this.exposureTimeInMicroseconds !== change.currentValue) break case 'min': case 'max': this.exposureTimeMinMaxChanged() break case 'normalized': - this.normalize(this.exposureTime) + this.normalize(this.exposureTime()) break } } } ngAfterViewInit() { - this.updateExposureTime(this.current.exposureTime, this.unit, this.unit) + this.updateExposureTime(this.currentExposureTime, this.unit(), this.unit()) } protected exposureTimeUnitChanged(value: ExposureTimeUnit) { - this.updateExposureTime(this.current.exposureTime, value, this.unit, false) + this.updateExposureTime(this.currentExposureTime, value, this.unit(), false) } - protected exposureTimeChanged(value: number, from: ExposureTimeUnit = this.unit, normalize: boolean = false) { - this.updateExposureTime(value, this.unit, from, normalize) + protected exposureTimeChanged(value: number, from: ExposureTimeUnit = this.unit(), normalize: boolean = false) { + this.updateExposureTime(value, this.unit(), from, normalize) } protected exposureTimeMinMaxChanged() { - this.updateExposureTime(this.current.exposureTime, this.unit, this.unit, false) + this.updateExposureTime(this.currentExposureTime, this.unit(), this.unit(), false) } protected exposureTimeUnitWheeled(event: WheelEvent) { if (event.deltaY) { const units: ExposureTimeUnit[] = ['MINUTE', 'SECOND', 'MILLISECOND', 'MICROSECOND'] - const index = units.indexOf(this.unit) + const index = units.indexOf(this.unit()) if (index >= 0) { if (event.deltaY > 0) { @@ -134,17 +109,17 @@ export class ExposureTimeComponent implements AfterViewInit, OnChanges { } } - private updateExposureTime(value: number, unit: ExposureTimeUnit, from: ExposureTimeUnit, normalize: boolean = this.normalized) { + private updateExposureTime(value: number, unit: ExposureTimeUnit, from: ExposureTimeUnit, normalize: boolean = this.normalized()) { const a = ExposureTimeComponent.exposureUnitFactor(from) const b = ExposureTimeComponent.exposureUnitFactor(unit) if (!a || !b) return - this.current.min = Math.max(1, Math.trunc(((this.min || 1) * b) / 60000000)) - this.current.max = Math.max(1, Math.trunc(((this.max || 600000000) * b) / 60000000)) - this.current.exposureTime = Math.max(1, Math.trunc((value * b) / a)) + this.currentMin = Math.max(1, Math.trunc(((this.min() || 1) * b) / 60000000)) + this.currentMax = Math.max(1, Math.trunc(((this.max() || 600000000) * b) / 60000000)) + this.currentExposureTime = Math.max(1, Math.trunc((value * b) / a)) - const exposureTimeInMicroseconds = Math.trunc((this.current.exposureTime * 60000000) / b) + const exposureTimeInMicroseconds = Math.trunc((value * 60000000) / a) if (normalize) { if (this.normalize(exposureTimeInMicroseconds)) { @@ -152,20 +127,18 @@ export class ExposureTimeComponent implements AfterViewInit, OnChanges { } } - if (this.exposureTime !== exposureTimeInMicroseconds) { - this.exposureTime = exposureTimeInMicroseconds - this.exposureTimeInMicroseconds = exposureTimeInMicroseconds - this.exposureTimeChange.emit(exposureTimeInMicroseconds) + if (this.exposureTime() !== exposureTimeInMicroseconds) { + this.exposureTime.set(exposureTimeInMicroseconds) + this.exposureTimeInMicroseconds.set(exposureTimeInMicroseconds) } - if (this.unit !== unit) { - this.unit = unit - this.unitChange.emit(unit) + if (this.unit() !== unit) { + this.unit.set(unit) } } private normalize(exposureTime: number) { - if (!this.normalized) { + if (!this.normalized()) { return false } diff --git a/desktop/src/app/dustcap/dustcap.component.html b/desktop/src/app/dustcap/dustcap.component.html index 8b8d05a6a..b8f90fafd 100644 --- a/desktop/src/app/dustcap/dustcap.component.html +++ b/desktop/src/app/dustcap/dustcap.component.html @@ -2,24 +2,21 @@
- - + @if (dustCap.connected) { + + } @else { + + }
@@ -32,21 +29,19 @@
@if (dustCap.parked) { - + (action)="togglePark()" /> } @else { - + (action)="togglePark()" /> }
diff --git a/desktop/src/app/filterwheel/filterwheel.component.html b/desktop/src/app/filterwheel/filterwheel.component.html index 609629f40..60163b8ae 100644 --- a/desktop/src/app/filterwheel/filterwheel.component.html +++ b/desktop/src/app/filterwheel/filterwheel.component.html @@ -2,169 +2,126 @@
- - -
-
- - {{ moving ? 'moving' : 'idle' }} - - + @if (wheel.connected) { + + } @else { + + }
+ @if (canShowInfo) { +
+ + {{ moving ? 'moving' : 'idle' }} + @if (wheel.connected && currentFilter) { + @if (currentFilter.position) { + + } + + } +
+ }
- - - -
- - - - - - {{ filter?.name }} -
-
- -
- - - - - - {{ item.name }} -
-
-
- -
+ +
+ @let mItem = selected ? filter : item; + + + @if (mItem) { + + + @if (mItem.dark) { + + } + } + + {{ mItem?.name }} +
+
+
- -
-
-
-
- - - - -
-
- + (action)="moveToSelectedFilter()" />
-
-
- - - +
+ +
+
+ +
+
+ } +
+
+ + - - + [(value)]="focuserOffset" + (valueChange)="focusOffsetChanged()" /> +
-
-
-
- + } + @if (canApply) { +
+
+ +
-
+ }
diff --git a/desktop/src/app/filterwheel/filterwheel.component.ts b/desktop/src/app/filterwheel/filterwheel.component.ts index a47d6b5ab..cc0e3e527 100644 --- a/desktop/src/app/filterwheel/filterwheel.component.ts +++ b/desktop/src/app/filterwheel/filterwheel.component.ts @@ -1,7 +1,6 @@ import { AfterContentInit, Component, HostListener, NgZone, OnDestroy, inject } from '@angular/core' import { ActivatedRoute } from '@angular/router' import hotkeys from 'hotkeys-js' -import { CheckboxChangeEvent } from 'primeng/checkbox' import { Subject, Subscription, debounceTime } from 'rxjs' import { ApiService } from '../../shared/services/api.service' import { BrowserWindowService } from '../../shared/services/browser-window.service' @@ -312,8 +311,8 @@ export class FilterWheelComponent implements AfterContentInit, OnDestroy, Tickab } } - protected shutterToggled(filter: Filter, event: CheckboxChangeEvent) { - this.filters.forEach((e) => (e.dark = !!event.checked && e === filter)) + protected shutterToggled(filter: Filter, checked: boolean) { + this.filters.forEach((e) => (e.dark = checked && e === filter)) this.preference.shutterPosition = this.filters.find((e) => e.dark)?.position ?? 0 this.savePreference() } diff --git a/desktop/src/app/flat-wizard/flat-wizard.component.html b/desktop/src/app/flat-wizard/flat-wizard.component.html index 3447998fb..cfcc6416b 100644 --- a/desktop/src/app/flat-wizard/flat-wizard.component.html +++ b/desktop/src/app/flat-wizard/flat-wizard.component.html @@ -7,14 +7,11 @@ [devices]="cameras" [(device)]="camera" (deviceChange)="cameraChanged()" /> - + (action)="showCameraDialog()" + tooltip="Camera" />
- - - - +
- - - - +
- - - - +
- - - - +
@@ -136,22 +97,18 @@
- - + + severity="danger" />
diff --git a/desktop/src/app/flat-wizard/flat-wizard.component.ts b/desktop/src/app/flat-wizard/flat-wizard.component.ts index 90b4dc68d..f4e20af63 100644 --- a/desktop/src/app/flat-wizard/flat-wizard.component.ts +++ b/desktop/src/app/flat-wizard/flat-wizard.component.ts @@ -1,5 +1,5 @@ -import { AfterViewInit, Component, HostListener, NgZone, OnDestroy, ViewChild, inject } from '@angular/core' -import { CameraExposureComponent } from '../../shared/components/camera-exposure/camera-exposure.component' +import { AfterViewInit, Component, HostListener, NgZone, OnDestroy, inject, viewChild } from '@angular/core' +import { CameraExposureComponent } from '../../shared/components/camera-exposure.component' import { AngularService } from '../../shared/services/angular.service' import { ApiService } from '../../shared/services/api.service' import { BrowserWindowService } from '../../shared/services/browser-window.service' @@ -34,11 +34,9 @@ export class FlatWizardComponent implements AfterViewInit, OnDestroy, Tickable { protected request = this.preference.request protected filters: Filter[] = [] - protected running = false - @ViewChild('cameraExposure') - private readonly cameraExposure!: CameraExposureComponent + private readonly cameraExposure = viewChild.required('cameraExposure') get meanTargetMin() { return Math.floor(this.request.meanTarget - (this.request.meanTolerance * this.request.meanTarget) / 100) @@ -59,10 +57,10 @@ export class FlatWizardComponent implements AfterViewInit, OnDestroy, Tickable { ngZone.run(() => { if (event.state === 'EXPOSURING' && event.capture && event.camera.id === this.camera?.id) { this.running = true - this.cameraExposure.handleCameraCaptureEvent(event.capture, true) + this.cameraExposure().handleCameraCaptureEvent(event.capture, true) } else { this.running = false - this.cameraExposure.reset() + this.cameraExposure().reset() if (event.state === 'CAPTURED') { this.angularService.message('Flat frame captured') diff --git a/desktop/src/app/focuser/focuser.component.html b/desktop/src/app/focuser/focuser.component.html index fef2984dc..4df6df5ac 100644 --- a/desktop/src/app/focuser/focuser.component.html +++ b/desktop/src/app/focuser/focuser.component.html @@ -2,128 +2,89 @@
- - + @if (focuser.connected) { + + } @else { + + }
{{ focuser.moving ? 'moving' : 'idle' }}
-
- - {{ focuser.temperature | number: '1.2-2' }}°C -
+ @if (focuser.hasThermometer) { +
+ + {{ focuser.temperature | number: '1.2-2' }}°C +
+ }
- - - - +
- - + + tooltip="Sync" />
- - - - - - + + + tooltip="Move Out" />
- - - - - + + tooltip="Move To" />
diff --git a/desktop/src/app/framing/framing.component.html b/desktop/src/app/framing/framing.component.html index 2dbc23b9c..58030f8b9 100644 --- a/desktop/src/app/framing/framing.component.html +++ b/desktop/src/app/framing/framing.component.html @@ -1,135 +1,79 @@
- - - - +
- - - - +
- - - - +
- - - - +
- - - - - + + (action)="fov.showDialog = true" + tooltip="Calculate" />
- - - - +
- - - -
- {{ item?.regime }} ({{ item?.skyFraction | percent: '1.1-1' }}) - {{ item?.id }} -
-
- -
- {{ item.regime }} ({{ item?.skyFraction | percent: '1.1-1' }}) - {{ item.id }} -
-
-
- -
+ +
+ {{ item?.regime }} ({{ item?.skyFraction | percent: '1.1-1' }}) + {{ item?.id }} +
+
+
- + (action)="frame()" />
@@ -152,51 +96,33 @@ [style]="{ width: '80vw' }">
- - - - - - - - - + + + [(value)]="preference.updateFovOnChange" + (valueChange)="savePreference()" />
{{ fov.computed.toFixed(3) }}° - + (action)="computeFOV(true)" />
diff --git a/desktop/src/app/guider/guider.component.html b/desktop/src/app/guider/guider.component.html index 721c59d1d..4ee7064f4 100644 --- a/desktop/src/app/guider/guider.component.html +++ b/desktop/src/app/guider/guider.component.html @@ -6,50 +6,36 @@ leftIcon="mdi mdi-target">
- - - - +
- - - - +
- - + @if (guider.connected) { + + } @else { + + }
- - - - +
- - - - +
- - - - +
- - - - +
- - - - +
- - - - +
- - - - +
- - - - +
@@ -182,52 +132,32 @@ leftIcon="mdi mdi-cog">
- - - - +
- - - - +
- - - - +
@@ -235,24 +165,20 @@ class="mt-1 absolute" style="top: -12px; right: 10px">
- - - + + severity="danger" />
@@ -274,149 +200,101 @@
- - - - +
- - - - +
- + (action)="guidePulseStart('NORTH', 'WEST')" + icon="mdi mdi-lg mdi-arrow-top-left-thick" />
- + (action)="guidePulseStart('NORTH')" + icon="mdi mdi-lg mdi-arrow-up-thick" />
- + (action)="guidePulseStart('NORTH', 'EAST')" + icon="mdi mdi-lg mdi-arrow-top-right-thick" />
- + (action)="guidePulseStart('WEST')" + icon="mdi mdi-lg mdi-arrow-left-thick" />
- + severity="danger" />
- + (action)="guidePulseStart('EAST')" + icon="mdi mdi-lg mdi-arrow-right-thick" />
- + (action)="guidePulseStart('SOUTH', 'WEST')" + icon="mdi mdi-lg mdi-arrow-bottom-left-thick" />
- + (action)="guidePulseStart('SOUTH')" + icon="mdi mdi-lg mdi-arrow-down-thick" />
- + (action)="guidePulseStart('SOUTH', 'EAST')" + icon="mdi mdi-lg mdi-arrow-bottom-right-thick" />
- - - - +
- - - - +
diff --git a/desktop/src/app/guider/guider.component.ts b/desktop/src/app/guider/guider.component.ts index 5fac7586c..d23ee8352 100644 --- a/desktop/src/app/guider/guider.component.ts +++ b/desktop/src/app/guider/guider.component.ts @@ -1,4 +1,4 @@ -import { AfterViewInit, Component, HostListener, NgZone, OnDestroy, OnInit, ViewChild, inject } from '@angular/core' +import { AfterViewInit, Component, HostListener, NgZone, OnDestroy, OnInit, inject, viewChild } from '@angular/core' import { Chart, ChartData, ChartOptions } from 'chart.js' import zoomPlugin from 'chartjs-plugin-zoom' import { UIChart } from 'primeng/chart' @@ -26,9 +26,7 @@ export class GuiderComponent implements OnInit, AfterViewInit, OnDestroy, Tickab protected readonly chartInfo = structuredClone(DEFAULT_GUIDER_CHART_INFO) private readonly guideHistory: GuiderHistoryStep[] = [] - - @ViewChild('chart') - private readonly chart!: UIChart + private readonly chart = viewChild.required('chart') get stopped() { return this.guider.state === 'STOPPED' @@ -351,7 +349,7 @@ export class GuiderComponent implements OnInit, AfterViewInit, OnDestroy, Tickab this.chartData.datasets[2].data = this.guideHistory.map((e) => (e.guideStep?.raDuration ?? 0) / durationScale(e.guideStep?.raDirection)) this.chartData.datasets[3].data = this.guideHistory.map((e) => (e.guideStep?.decDuration ?? 0) / durationScale(e.guideStep?.decDirection)) - this.chart.refresh() + this.chart().refresh() } protected async guideOutputChanged() { diff --git a/desktop/src/app/home/home.component.html b/desktop/src/app/home/home.component.html index 9469a67ad..64e507565 100644 --- a/desktop/src/app/home/home.component.html +++ b/desktop/src/app/home/home.component.html @@ -1,343 +1,231 @@
- - - - -
- {{ connection?.name }} -
-
- -
-
- {{ item.name }} - {{ item.type }} | {{ item.host }}:{{ item.port }} -
- - {{ (item.connectedAt | date: 'yyyy-MM-dd HH:mm:ss') ?? 'never' }} -
-
-
- - + tooltip="New connection" /> + + @if (selected) { +
+ {{ connection?.name }} +
+ } @else { +
+
+ {{ item.name }} + {{ item.type }} | {{ item.host }}:{{ item.port }} +
+ + {{ (item.connectedAt | date: 'yyyy-MM-dd HH:mm:ss') ?? 'never' }}
- - - - - - +
+ + +
+
+ } +
+ + @if (!connected) { + + } @else { + + }
-
-
-
- New version: - {{ newVersion }} + @if (newVersion) { +
+
+
+ New version: + {{ newVersion }} +
+ + +
- - -
-
+ }
-
- - -
Camera
-
-
-
- - -
Mount
-
-
-
- - -
Guider
-
-
-
- - -
Filter Wheel
-
-
-
- - -
Focuser
-
-
-
- - -
Rotator
-
-
-
- - -
Dome
-
-
-
- - -
Auxiliary
-
-
+ + + + + + + + @if (preference.showAuxiliary && hasAuxiliary) { -
- - -
Switch
-
-
-
- - -
Light Box
-
-
-
- - -
Dust Cap
-
-
+ + + } -
- - -
Sky Atlas
-
-
-
- - -
Alignment
-
-
-
- - -
Sequencer
-
-
-
- - -
Image Viewer
-
-
-
- - -
Framing
-
-
-
- - -
Auto Focus
-
-
-
- - -
Flat Wizard
-
-
-
- - -
Calibration
-
-
-
- - -
INDI
-
-
-
- - -
Calculator
-
-
-
- - -
Settings
-
-
-
- - -
About
-
-
-
- - -
+ + + + + + + + + + + + +
@@ -349,53 +237,38 @@ [style]="{ width: '90vw' }">
- - - - +
- - - - +
- - - - +
- + [(value)]="connectionDialog.connection.type" />
- + (action)="saveConnection()" /> diff --git a/desktop/src/app/home/home.component.scss b/desktop/src/app/home/home.component.scss deleted file mode 100644 index fac79d445..000000000 --- a/desktop/src/app/home/home.component.scss +++ /dev/null @@ -1,51 +0,0 @@ -neb-home { - .buttons { - p-button { - display: contents; - - > button { - min-height: 56px; - max-height: 56px; - display: flex; - - img { - height: 32px; - } - } - - &.p-disabled { - img { - filter: grayscale(1); - } - } - } - } - - .indicators { - top: 50%; - right: 0.6rem; - - .indicator { - background-color: #3f3f46; - width: 0.7rem; - height: 0.7rem; - transition: - background-color 0.2s, - color 0.2s, - border-color 0.2s, - box-shadow 0.2s, - outline-color 0.2s; - border-radius: 50%; - cursor: pointer; - margin: 1px; - - &.selected { - background-color: #60a5fa; - } - } - } - - .grid::-webkit-scrollbar { - display: none; - } -} diff --git a/desktop/src/app/home/home.component.ts b/desktop/src/app/home/home.component.ts index 01afb6a60..cde50ffd6 100644 --- a/desktop/src/app/home/home.component.ts +++ b/desktop/src/app/home/home.component.ts @@ -1,8 +1,8 @@ -import { AfterContentInit, Component, inject, NgZone, ViewChild, ViewEncapsulation } from '@angular/core' +import { AfterContentInit, Component, inject, NgZone, viewChild, ViewEncapsulation } from '@angular/core' import packageJson from '../../../package.json' with { type: 'json' } -import { DeviceChooserComponent } from '../../shared/components/device-chooser/device-chooser.component' -import { DeviceConnectionCommandEvent, DeviceListMenuComponent } from '../../shared/components/device-list-menu/device-list-menu.component' -import { MenuItem, SlideMenuItem } from '../../shared/components/menu-item/menu-item.component' +import { DeviceChooserComponent } from '../../shared/components/device-chooser.component' +import { DeviceConnectionCommandEvent, DeviceListMenuComponent } from '../../shared/components/device-list-menu.component' +import { MenuItem, SlideMenuItem } from '../../shared/components/menu-item.component' import { ApiService } from '../../shared/services/api.service' import { BrowserWindowService } from '../../shared/services/browser-window.service' import { ElectronService } from '../../shared/services/electron.service' @@ -26,7 +26,6 @@ function scrollPageOf(element: Element) { @Component({ selector: 'neb-home', templateUrl: 'home.component.html', - styleUrls: ['home.component.scss'], encapsulation: ViewEncapsulation.None, }) export class HomeComponent implements AfterContentInit { @@ -82,8 +81,7 @@ export class HomeComponent implements AfterContentInit { } } - @ViewChild('deviceMenu') - private readonly deviceMenu!: DeviceListMenuComponent + private readonly deviceMenu = viewChild.required('deviceMenu') get connected() { return !!this.connection && this.connection.connected @@ -515,7 +513,7 @@ export class HomeComponent implements AfterContentInit { if (devices.length === 0) return - const device = await this.deviceMenu.show(devices, undefined, type) + const device = await this.deviceMenu().show(devices, undefined, type) if (device && device !== 'NONE') { await this.openDeviceWindow(device) @@ -575,7 +573,7 @@ export class HomeComponent implements AfterContentInit { await this.browserWindowService.openAlignment({ bringToFront: true }) break case 'SEQUENCER': { - const device = await this.deviceMenu.show(this.cameras, undefined, 'CAMERA') + const device = await this.deviceMenu().show(this.cameras, undefined, 'CAMERA') if (device && device !== 'NONE') { await this.browserWindowService.openSequencer(device, { bringToFront: true }) @@ -669,7 +667,7 @@ export class HomeComponent implements AfterContentInit { } let page = 0 - const scrollChidren = document.getElementsByClassName('scroll-child') + const scrollChidren = document.querySelectorAll('[scroll-page]') for (let i = 0; i < scrollChidren.length; i++) { const child = scrollChidren[i] @@ -684,14 +682,8 @@ export class HomeComponent implements AfterContentInit { event.stopImmediatePropagation() } - protected scrollTo(event: Event, page: number) { - this.page = page - this.scrollToPage(page) - event.stopImmediatePropagation() - } - protected scrollToPage(page: number) { - const scrollChidren = document.getElementsByClassName('scroll-child') + const scrollChidren = document.querySelectorAll('[scroll-page]') for (let i = 0; i < scrollChidren.length; i++) { const child = scrollChidren[i] diff --git a/desktop/src/app/image/image.component.html b/desktop/src/app/image/image.component.html index e63a2dc8c..ec86da3b7 100644 --- a/desktop/src/app/image/image.component.html +++ b/desktop/src/app/image/image.component.html @@ -32,81 +32,86 @@ (mousemove)="imageMouseMoved($event)" [class.cursor-crosshair]="isMouseCoordinateVisible" /> - + @if (preference.crossHair && zoom.panZoom) { + + } - - - - - {{ (a.star ?? a.dso ?? a.minorPlanet)?.name?.join(' · ') }} - - - + @if (!transformation.mirrorHorizontal && !transformation.mirrorVertical && annotation.visible) { + + @for (a of annotation.displayOnlyFiltered ? annotation.filtered : annotation.data; track a) { + + + + {{ (a.star ?? a.dso ?? a.minorPlanet)?.name?.join(' · ') }} + + + } + + } - - - - - {{ s.hfd.toFixed(1) }} - - - + @if (!transformation.mirrorHorizontal && !transformation.mirrorVertical && starDetector.visible) { + + @for (s of starDetector.stars; track s) { + + + + {{ s.hfd.toFixed(1) }} + + + } + + } - - @for (item of fov.fovs; track $index) { - @if (item.enabled && item.computed) { - + @if (imageInfo) { + + @for (item of fov.fovs; track $index) { + @if (item.enabled && item.computed) { + + } } - } - + + }
- + @if (hasROI) { + + }
-
- X: {{ imageROI.area.x.toFixed(0) }} Y: {{ imageROI.area.y.toFixed(0) }} W: {{ imageROI.area.width.toFixed(0) }} H: {{ imageROI.area.height.toFixed(0) }} - -
+@if (hasROI) { +
+ X: {{ imageROI.area.x.toFixed(0) }} Y: {{ imageROI.area.y.toFixed(0) }} W: {{ imageROI.area.width.toFixed(0) }} H: {{ imageROI.area.height.toFixed(0) }} + +
+}
Stars & DSOs - +
-
@@ -196,40 +196,29 @@
Minor Planets - +
- - - - +
-
@if (annotation.request.minorPlanetsMagLimit >= 20 || annotation.request.includeMinorPlanetsWithoutMagnitude) { @@ -260,57 +249,29 @@
- - - - - - - - - - - - + + +
- + [(value)]="annotation.displayOnlyFiltered" />
- + (action)="annotateImage()" /> @@ -355,129 +314,93 @@ [modal]="false" [style]="{ width: 'min-content', minWidth: '325px' }">
- + @if (astronomicalObject.info) {
- - - - +
- - - - +
- - - - -
-
- - - - -
-
- - - - -
-
- - - - +
-
- - - - + @if (astronomicalObject.info.constellation) { +
+ +
+ } + @if (astronomicalObject.info.magnitude) { +
+ +
+ } + @if (astronomicalObject.info.type) { +
+ +
+ } + @if (astronomicalObject.info.distance) { +
+ +
+ } + } + @if (astronomicalObject.info?.type) { + - - + }
- - + - + - + + label="Frame" />
@@ -491,213 +414,141 @@ class="pointer-events-none">
- - - - - + + [(value)]="solver.request.blind" + (valueChange)="savePreference()" />
- - - - +
- - - - +
- - - - +
- - - - +
- - - - +
- - - - +
- - - - +
- - - - +
- - - - +
- - - - +
- - - - +
- - + - + - + + label="Frame" />
- - + (action)="solverStart()" />
@@ -711,30 +562,17 @@
- - - - - - - - + +
-
- - - - +
-
- - - - - + - + + (action)="restoreAutoStretchMeanBackground()" + tooltip="Reset" />
- - + - + + (action)="stretchImage()" /> @@ -831,57 +643,33 @@ class="pointer-events-none">
- +
- - - - +
- - - - +
- + (action)="scnrImage()" /> @@ -926,108 +714,66 @@ @if (statistics.statistics && imageInfo) {
- + [options]="'IMAGE_CHANNEL' | dropdownOptions" + [(value)]="statistics.channel" + (valueChange)="computeStatistics(true)" />
- - - - +
- - - - +
- - - - +
- - - - +
- - - - +
- - - - +
- - - - +
- - - - +
- - - - +
- - - - +
-
- - + - - -
-
- - +
+ } + @if (starDetector.request.type === 'SIRIL') { +
+ - - -
+ [(value)]="starDetector.request.maxStars" /> +
+ }
COMPUTED
- - - - +
- - - - +
- - - - +
- - - - +
SELECTED @@ -1150,56 +855,38 @@
- - - - +
- - - - +
- - - - +
- - - - +
- + (action)="detectStars()" /> @@ -1220,237 +907,142 @@ [style]="{ width: '23px', height: '23px' }" />
- - - - - + (action)="showFOVTelescopeDialog()" + tooltip="Choose telescope" /> +
- - - - +
Resolution (px)
- + (action)="showFOVCameraDialog()" + tooltip="Choose camera" />
Pixel Size (µm)
- - - - +
- - - - +
- - - - +
- - - - +
- - - - +
- - - - +
- - - - +
- + tooltip="Add" + (action)="addFOV()" />
-
- @for (item of fov.fovs; track $index) { -
-
- -
-
- - - - - - - - @if (item.computed) { - - - - } -
-
- - + @if (fov.fovs.length) { +
+ @for (item of fov.fovs; track $index) { +
+
+ +
+
+ + + + + + + + @if (item.computed) { + + + + } +
+
+ + +
-
- } -
+ } +
+ }
@@ -1488,13 +1080,11 @@
- + (action)="chooseCamera()" /> @@ -1531,13 +1121,11 @@
- + (action)="chooseTelescope()" /> @@ -1559,115 +1147,73 @@ class="w-full" />
- + [(value)]="saveAs.format" />
@if (imageInfo) {
- + tooltip="ROI Size" + (action)="useROIAreaForSaveAs()" />
- - - - +
- - - - +
- + tooltip="Image Size" + (action)="useImageAreaForSaveAs()" />
- - - - +
- - - - +
}
- - - - +
- Transformed - +
- + (action)="saveImageAs()" /> @@ -1679,15 +1225,13 @@ [style]="{ width: 'min-content', minWidth: '338px' }">
- -
@@ -1710,55 +1254,43 @@ [ngModel]="rotation.transformation.angle + 360" (ngModelChange)="rotate($event - 360)" spinnableNumber /> - - - - +
- - + - + - + + (action)="rotate(270)" />
-
-
-
X: {{ mouseCoordinate.x }}
-
Y: {{ mouseCoordinate.y }}
-
α: {{ mouseCoordinate.alpha }}
-
δ: {{ mouseCoordinate.delta }}
-
l: {{ mouseCoordinate.l }}
-
b: {{ mouseCoordinate.b }}
+@if (mouseCoordinate && isMouseCoordinateVisible) { +
+
+
X: {{ mouseCoordinate.x }}
+
Y: {{ mouseCoordinate.y }}
+
α: {{ mouseCoordinate.alpha }}
+
δ: {{ mouseCoordinate.delta }}
+
l: {{ mouseCoordinate.l }}
+
b: {{ mouseCoordinate.b }}
+
-
+} diff --git a/desktop/src/app/image/image.component.ts b/desktop/src/app/image/image.component.ts index 53ce41422..c1ccbb7cb 100644 --- a/desktop/src/app/image/image.component.ts +++ b/desktop/src/app/image/image.component.ts @@ -1,11 +1,11 @@ -import { AfterViewInit, Component, ElementRef, HostListener, NgZone, OnDestroy, ViewChild, inject } from '@angular/core' +import { AfterViewInit, Component, ElementRef, HostListener, NgZone, OnDestroy, inject, viewChild } from '@angular/core' import { ActivatedRoute } from '@angular/router' import hotkeys from 'hotkeys-js' import { NgxLegacyMoveableComponent, OnDrag, OnResize, OnRotate } from 'ngx-moveable' import { ContextMenu } from 'primeng/contextmenu' -import { DeviceListMenuComponent } from '../../shared/components/device-list-menu/device-list-menu.component' -import { HistogramComponent } from '../../shared/components/histogram/histogram.component' -import { MenuItem } from '../../shared/components/menu-item/menu-item.component' +import { DeviceListMenuComponent } from '../../shared/components/device-list-menu.component' +import { HistogramComponent } from '../../shared/components/histogram.component' +import { MenuItem } from '../../shared/components/menu-item.component' import { SEPARATOR_MENU_ITEM } from '../../shared/constants' import { AngularService } from '../../shared/services/angular.service' import { ApiService } from '../../shared/services/api.service' @@ -372,26 +372,13 @@ export class ImageComponent implements AfterViewInit, OnDestroy { ], } - @ViewChild('image') - private readonly image!: ElementRef - - @ViewChild('roi') - private readonly roi!: ElementRef - - @ViewChild('menu') - private readonly menu!: ContextMenu - - @ViewChild('deviceMenu') - private readonly deviceMenu!: DeviceListMenuComponent - - @ViewChild('histogram') - private readonly histogram?: HistogramComponent - - @ViewChild('detectedStarCanvas') - private readonly detectedStarCanvas!: ElementRef - - @ViewChild('moveable') - private readonly moveable!: NgxLegacyMoveableComponent + private readonly image = viewChild.required>('image') + private readonly roi = viewChild.required>('roi') + private readonly menu = viewChild.required('menu') + private readonly deviceMenu = viewChild.required('deviceMenu') + private readonly histogram = viewChild('histogram') + private readonly detectedStarCanvas = viewChild.required>('detectedStarCanvas') + private readonly moveable = viewChild.required('moveable') get isMouseCoordinateVisible() { return this.mouseCoordinate.show && !!this.mouseCoordinate.interpolator && !this.transformation.mirrorHorizontal && !this.transformation.mirrorVertical @@ -651,8 +638,9 @@ export class ImageComponent implements AfterViewInit, OnDestroy { } this.calibrationMenuItem.items = menu - this.menu.model = this.contextMenuModel - this.menu.cd.markForCheck() + const menuValue = this.menu() + menuValue.model = this.contextMenuModel + menuValue.cd.markForCheck() if (reloadImage) { await this.loadImage() @@ -686,7 +674,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { const { target, transform } = event target.style.transform = transform - const rect = this.moveable.getRect() + const rect = this.moveable().getRect() this.imageROI.area.x = Math.trunc(rect.left) this.imageROI.area.y = Math.trunc(rect.top) } @@ -695,7 +683,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { const { target, width, height, transform } = event target.style.transform = transform - const rect = this.moveable.getRect() + const rect = this.moveable().getRect() target.style.width = `${width}px` this.imageROI.area.x = Math.trunc(rect.left) @@ -766,7 +754,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { Object.assign(this.solver.solved, DEFAULT_IMAGE_SOLVED) - this.histogram?.update([]) + this.histogram()?.update([]) } protected async computeStatistics(force: boolean = false) { @@ -777,11 +765,12 @@ export class ImageComponent implements AfterViewInit, OnDestroy { const statistics = await this.api.imageStatistics(path, transformation, this.statistics.channel, this.imageData.camera) this.statistics.statistics = statistics - if (this.histogram) { - this.histogram.update(statistics.histogram) + const histogram = this.histogram() + if (histogram) { + histogram.update(statistics.histogram) } else { setTimeout(() => { - this.histogram?.update(statistics.histogram) + this.histogram()?.update(statistics.histogram) }, 1000) } } @@ -850,9 +839,9 @@ export class ImageComponent implements AfterViewInit, OnDestroy { protected drawDetectedStar(star: DetectedStar) { Object.assign(this.starDetector.selected, star) - const canvas = this.detectedStarCanvas.nativeElement + const canvas = this.detectedStarCanvas().nativeElement const ctx = canvas.getContext('2d') - ctx?.drawImage(this.image.nativeElement, star.x - 8, star.y - 8, 16, 16, 0, 0, canvas.width, canvas.height) + ctx?.drawImage(this.image().nativeElement, star.x - 8, star.y - 8, 16, 16, 0, 0, canvas.width, canvas.height) } protected async loadImage() { @@ -896,7 +885,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { } private async loadImageFromPath(path: string) { - const image = this.image.nativeElement + const image = this.image().nativeElement const transformation = this.makeImageTransformation() const { info, blob } = await this.api.openImage(path, transformation, this.imageData.camera) @@ -958,7 +947,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { this.mouseMountCoordinate.y = event.offsetY if (contextMenu) { - this.menu.show(event) + this.menu().show(event) } } @@ -967,7 +956,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { } private imageMouseMovedWithCoordinates(x: number, y: number) { - if (!this.menu.visible() && this.mouseCoordinate.interpolator) { + if (!this.menu().visible() && this.mouseCoordinate.interpolator) { Object.assign(this.mouseCoordinate, this.mouseCoordinate.interpolator.interpolateAsText(x, y, true, true, false)) this.mouseCoordinate.x = x this.mouseCoordinate.y = y @@ -1143,7 +1132,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { private zoomIn() { if (this.zoom.panZoom) { const { innerWidth, innerHeight } = window - const { offsetTop, offsetLeft } = this.image.nativeElement.parentElement!.parentElement! + const { offsetTop, offsetLeft } = this.image().nativeElement.parentElement!.parentElement! this.zoom.panZoom.zoomIn({ clientX: innerWidth / 2 + offsetLeft / 2, clientY: innerHeight / 2 + offsetTop / 2 }) } } @@ -1151,7 +1140,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { private zoomOut() { if (this.zoom.panZoom) { const { innerWidth, innerHeight } = window - const { offsetTop, offsetLeft } = this.image.nativeElement.parentElement!.parentElement! + const { offsetTop, offsetLeft } = this.image().nativeElement.parentElement!.parentElement! this.zoom.panZoom.zoomOut({ clientX: innerWidth / 2 + offsetLeft / 2, clientY: innerHeight / 2 + offsetTop / 2 }) } } @@ -1165,12 +1154,12 @@ export class ImageComponent implements AfterViewInit, OnDestroy { private resetZoom(fitToScreen: boolean = false, center: boolean = true) { if (this.zoom.panZoom) { if (fitToScreen) { - const { width: iw, height: ih } = this.image.nativeElement + const { width: iw, height: ih } = this.image().nativeElement const angle = this.rotation.transformation.angle * (Math.PI / 180.0) const nw = Math.abs(iw * Math.cos(angle)) + Math.abs(ih * Math.sin(angle)) const nh = Math.abs(iw * Math.sin(angle)) + Math.abs(ih * Math.cos(angle)) - const { clientWidth: cw, clientHeight: ch } = this.image.nativeElement.parentElement!.parentElement! - const { offsetTop } = this.image.nativeElement.parentElement!.parentElement! + const { clientWidth: cw, clientHeight: ch } = this.image().nativeElement.parentElement!.parentElement! + const { offsetTop } = this.image().nativeElement.parentElement!.parentElement! const factor = Math.min(cw / nw, (ch - offsetTop) / nh) this.zoom.panZoom.zoom(factor) } else { @@ -1303,7 +1292,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { } protected imageLoaded() { - const image = this.image.nativeElement + const image = this.image().nativeElement const wrapper = image.parentElement const owner = wrapper?.parentElement @@ -1323,7 +1312,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { if (e.shiftKey) { this.rotateWithWheel(e) } else { - if (e.target === owner || e.target === wrapper || e.target === image || e.target === this.roi.nativeElement || (e.target as HTMLElement).tagName === 'circle') { + if (e.target === owner || e.target === wrapper || e.target === image || e.target === this.roi().nativeElement || (e.target as HTMLElement).tagName === 'circle') { panZoom.zoomWithWheel(e) } } @@ -1488,11 +1477,11 @@ export class ImageComponent implements AfterViewInit, OnDestroy { private async executeCamera(action: (camera: Camera) => void | Promise, showConfirmation: boolean = true) { const cameras = await this.api.cameras() - return this.deviceService.executeAction(this.deviceMenu, cameras, action, showConfirmation) + return this.deviceService.executeAction(this.deviceMenu(), cameras, action, showConfirmation) } private async executeMount(action: (mount: Mount) => void | Promise, showConfirmation: boolean = true) { const mounts = await this.api.mounts() - return this.deviceService.executeAction(this.deviceMenu, mounts, action, showConfirmation) + return this.deviceService.executeAction(this.deviceMenu(), mounts, action, showConfirmation) } } diff --git a/desktop/src/app/indi/indi.component.html b/desktop/src/app/indi/indi.component.html index 7a37ebbbf..608bc962b 100644 --- a/desktop/src/app/indi/indi.component.html +++ b/desktop/src/app/indi/indi.component.html @@ -1,17 +1,12 @@
- - - - +
- + (action)="showLog = true" + severity="success" />
-
- -
-
- -
+ @for (property of properties; track property.name) { +
+ @if (!showLog && property.group === group) { + + } +
+ } + @if (showLog) { +
+ +
+ }
diff --git a/desktop/src/app/indi/indi.component.ts b/desktop/src/app/indi/indi.component.ts index 99898d7be..752481629 100644 --- a/desktop/src/app/indi/indi.component.ts +++ b/desktop/src/app/indi/indi.component.ts @@ -1,4 +1,4 @@ -import { AfterViewInit, Component, HostListener, NgZone, OnDestroy, ViewChild, ViewEncapsulation, inject } from '@angular/core' +import { AfterViewInit, Component, HostListener, NgZone, OnDestroy, ViewEncapsulation, inject, viewChild } from '@angular/core' import { ActivatedRoute } from '@angular/router' import { MenuItem } from 'primeng/api' import { Listbox } from 'primeng/listbox' @@ -27,8 +27,7 @@ export class INDIComponent implements AfterViewInit, OnDestroy { protected showLog = false protected messages: string[] = [] - @ViewChild('listbox') - protected readonly messageBox!: Listbox + private readonly messageBox = viewChild.required('messageBox') constructor() { const app = inject(AppComponent) @@ -66,7 +65,7 @@ export class INDIComponent implements AfterViewInit, OnDestroy { ngZone.run(() => { if (event.message) { this.messages.splice(0, 0, event.message) - this.messageBox.cd.markForCheck() + this.messageBox().cd.markForCheck() } }) } @@ -116,7 +115,7 @@ export class INDIComponent implements AfterViewInit, OnDestroy { } } - protected async deviceChanged(device: Device) { + protected async deviceChanged(device?: Device) { if (this.device) { await this.api.indiUnlisten(this.device) } @@ -124,8 +123,11 @@ export class INDIComponent implements AfterViewInit, OnDestroy { this.device = device await this.updateProperties() - await this.api.indiListen(device) - this.messages = await this.api.indiMessages(device) + + if (device) { + await this.api.indiListen(device) + this.messages = await this.api.indiMessages(device) + } } protected changeGroup(group: string) { @@ -199,6 +201,10 @@ export class INDIComponent implements AfterViewInit, OnDestroy { } this.updateGroups() + } else { + this.properties = [] + this.groups = [] + this.group = '' } } diff --git a/desktop/src/app/indi/property/indi-property.component.html b/desktop/src/app/indi/property/indi-property.component.html index c1fbb23b5..09cc6209f 100644 --- a/desktop/src/app/indi/property/indi-property.component.html +++ b/desktop/src/app/indi/property/indi-property.component.html @@ -1,142 +1,113 @@ +@let mProperty = property(); +
- - {{ property.label }} + + {{ mProperty.label }}
-
- - - -
-
- -
-
- - - -
-
-
-
-
- - - - -
-
- - - - -
+ @if (mProperty.type === 'SWITCH') { + @if (mProperty.rule === 'ONE_OF_MANY') { +
+ @for (item of mProperty.items; track item.name) { + + }
-
-
- -
-
-
-
-
-
- - - - -
-
- - - - -
+ } + @if (mProperty.rule === 'AT_MOST_ONE') { +
+ @for (item of mProperty.items; track item.name) { + + } +
+ } + @if (mProperty.rule === 'ANY_OF_MANY') { +
+ @for (item of mProperty.items; track item.name) { + + } +
+ } + } @else if (mProperty.type === 'NUMBER') { +
+
+ @for (item of mProperty.items; track item.name) { +
+ @if (mProperty.perm !== 'WO') { +
+ +
+ } + @if (mProperty.perm !== 'RO') { +
+ +
+ } +
+ } +
+
+ @if (mProperty.perm !== 'RO') { + + }
-
- + } @else if (mProperty.type === 'TEXT') { +
+
+ @for (item of mProperty.items; track item.name) { +
+ @if (mProperty.perm !== 'WO') { +
+ +
+ } + @if (mProperty.perm !== 'RO') { +
+ +
+ } +
+ } +
+
+ @if (mProperty.perm !== 'RO') { + + } +
-
+ }
diff --git a/desktop/src/app/indi/property/indi-property.component.ts b/desktop/src/app/indi/property/indi-property.component.ts index 19ea8e510..2710b65d6 100644 --- a/desktop/src/app/indi/property/indi-property.component.ts +++ b/desktop/src/app/indi/property/indi-property.component.ts @@ -1,4 +1,4 @@ -import { AfterContentInit, Component, EventEmitter, Input, Output, ViewEncapsulation } from '@angular/core' +import { Component, ViewEncapsulation, effect, input, output } from '@angular/core' import type { INDIProperty, INDIPropertyItem, INDISendProperty, INDISendPropertyItem } from '../../../shared/types/device.types' @Component({ @@ -7,32 +7,29 @@ import type { INDIProperty, INDIPropertyItem, INDISendProperty, INDISendProperty styleUrls: ['indi-property.component.scss'], encapsulation: ViewEncapsulation.None, }) -export class INDIPropertyComponent implements AfterContentInit { - @Input({ required: true }) - protected property!: INDIProperty +export class INDIPropertyComponent { + readonly property = input.required() + readonly disabled = input(false) + readonly send = output() - @Input() - protected disabled = false - - @Output() - readonly send = new EventEmitter() - - ngAfterContentInit() { - for (const item of this.property.items) { - if (!item.valueToSend) { - item.valueToSend = `${item.value}` + constructor() { + effect(() => { + for (const item of this.property().items) { + if (!item.valueToSend) { + item.valueToSend = `${item.value}` + } } - } + }) } sendSwitch(item: INDIPropertyItem) { const property: INDISendProperty = { - name: this.property.name, + name: this.property().name, type: 'SWITCH', items: [ { name: item.name, - value: this.property.rule === 'ANY_OF_MANY' ? !item.value : true, + value: this.property().rule === 'ANY_OF_MANY' ? !item.value : true, }, ], } @@ -43,12 +40,12 @@ export class INDIPropertyComponent implements AfterContentInit { sendNumber() { const items: INDISendPropertyItem[] = [] - for (const item of this.property.items) { + for (const item of this.property().items) { items.push({ name: item.name, value: item.valueToSend }) } const property: INDISendProperty = { - name: this.property.name, + name: this.property().name, type: 'NUMBER', items, } @@ -59,12 +56,12 @@ export class INDIPropertyComponent implements AfterContentInit { sendText() { const items: INDISendPropertyItem[] = [] - for (const item of this.property.items) { + for (const item of this.property().items) { items.push({ name: item.name, value: item.valueToSend }) } const property: INDISendProperty = { - name: this.property.name, + name: this.property().name, type: 'TEXT', items, } diff --git a/desktop/src/app/lightbox/lightbox.component.html b/desktop/src/app/lightbox/lightbox.component.html index cd6c53cc2..08b07e332 100644 --- a/desktop/src/app/lightbox/lightbox.component.html +++ b/desktop/src/app/lightbox/lightbox.component.html @@ -2,35 +2,28 @@
- - + @if (lightBox.connected) { + + } @else { + + }
-
- Enabled - -
+
- - + @if (mount.connected) { + + } @else { + + }
- + tooltip="Remote Control" />
@@ -43,123 +34,79 @@
- - - - +
- - - - +
- - - - +
- - - - +
- - - - +
- - - - +
- - - - +
- - - - +
- - - - +
- - - - +
- + (action)="ephemerisMenu.show(ephemerisModel)" />
- + [(value)]="preference.targetCoordinateType" + (valueChange)="computeTargetCoordinates()" />
@@ -186,28 +133,18 @@
- - - - +
- - - - +
+ (onDropdownClick)="targetMenu.show(targetCoordinateModel)" />
- + icon="mdi mdi-arrow-top-left-thick" />
- + icon="mdi mdi-arrow-up-thick" />
- + icon="mdi mdi-arrow-top-right-thick" />
- + icon="mdi mdi-arrow-left-thick" />
- + (action)="abort()" />
- + icon="mdi mdi-arrow-right-thick" />
- + icon="mdi mdi-arrow-bottom-left-thick" />
- + icon="mdi mdi-arrow-down-thick" />
- + icon="mdi mdi-arrow-bottom-right-thick" />
-
- Tracking - + + [(value)]="tracking" + (valueChange)="trackingToggled()" />
- - - + } @else { + + } + + (action)="home()" + tooltip="Home" + severity="info" />
- - - - +
- - - - +
@@ -393,64 +304,52 @@ [style]="{ maxWidth: '90vw' }">
- - - - +
- - - - +
- - - - +
- - Use together with the - - Stellarium Mobile Plus - - - - Use together with the - - Stellarium - - + @if (remoteControl.protocol === 'LX200') { + + Use together with the + + Stellarium Mobile Plus + + + } @else if (remoteControl.protocol === 'STELLARIUM') { + + Use together with the + + Stellarium + + + }
- + (action)="startRemoteControl()" />
{{ item.host }}:{{ item.port }}
- + tooltip="Stop" + (action)="stopRemoteControl(item.protocol)" />
@@ -488,9 +384,7 @@ diff --git a/desktop/src/app/mount/mount.component.ts b/desktop/src/app/mount/mount.component.ts index 27bbb98dd..b745ea31a 100644 --- a/desktop/src/app/mount/mount.component.ts +++ b/desktop/src/app/mount/mount.component.ts @@ -2,7 +2,7 @@ import { AfterContentInit, Component, HostListener, NgZone, OnDestroy, inject } import { ActivatedRoute } from '@angular/router' import hotkeys from 'hotkeys-js' import { Subject, Subscription, interval, throttleTime } from 'rxjs' -import { SlideMenuItem } from '../../shared/components/menu-item/menu-item.component' +import { SlideMenuItem } from '../../shared/components/menu-item.component' import { SEPARATOR_MENU_ITEM } from '../../shared/constants' import { AngularService } from '../../shared/services/angular.service' import { ApiService } from '../../shared/services/api.service' @@ -11,7 +11,7 @@ import { ElectronService } from '../../shared/services/electron.service' import { PreferenceService } from '../../shared/services/preference.service' import { Tickable, Ticker } from '../../shared/services/ticker.service' import { BodyTabType, ComputedLocation, DEFAULT_COMPUTED_LOCATION } from '../../shared/types/atlas.types' -import { DEFAULT_MOUNT, DEFAULT_MOUNT_PREFERENCE, DEFAULT_MOUNT_REMOTE_CONTROL_DIALOG, Mount, MountRemoteControlProtocol, MountSlewDirection, SlewRate, TrackMode } from '../../shared/types/mount.types' +import { DEFAULT_MOUNT, DEFAULT_MOUNT_PREFERENCE, DEFAULT_MOUNT_REMOTE_CONTROL_DIALOG, Mount, MountRemoteControlProtocol, MountSlewDirection, TrackMode } from '../../shared/types/mount.types' import { AppComponent } from '../app.component' @Component({ @@ -40,7 +40,7 @@ export class MountComponent implements AfterContentInit, OnDestroy, Tickable { protected tracking = false protected trackMode: TrackMode = 'SIDEREAL' - protected slewRate?: SlewRate + protected slewRate?: string protected slewingDirection?: MountSlewDirection protected readonly ephemerisModel: SlideMenuItem[] = [ @@ -451,7 +451,7 @@ export class MountComponent implements AfterContentInit, OnDestroy, Tickable { private update() { if (this.mount.id) { this.trackMode = this.mount.trackMode - this.slewRate = this.mount.slewRate + this.slewRate = this.mount.slewRate?.value ?? this.mount.slewRates[0]?.value this.tracking = this.mount.tracking this.computeCoordinatePublisher.next() diff --git a/desktop/src/app/rotator/rotator.component.html b/desktop/src/app/rotator/rotator.component.html index 1161cc306..bcb4dabca 100644 --- a/desktop/src/app/rotator/rotator.component.html +++ b/desktop/src/app/rotator/rotator.component.html @@ -2,26 +2,21 @@
- - + @if (rotator.connected) { + + } @else { + + }
@@ -32,92 +27,65 @@
- - - - +
- - + -
- Reversed - +
+ + [(value)]="rotator.reversed" + (valueChange)="reverse($event)" />
- - - - - + - + + tooltip="Move" />
-
-
- + @if (canApply) { +
+
+ +
-
+ }
diff --git a/desktop/src/app/sequencer/sequencer.component.html b/desktop/src/app/sequencer/sequencer.component.html index a5744b8b9..6e8869c06 100644 --- a/desktop/src/app/sequencer/sequencer.component.html +++ b/desktop/src/app/sequencer/sequencer.component.html @@ -99,122 +99,95 @@ (cdkDropListDropped)="drop($event)" class="grid px-4 mt-1 flex align-items-center gap-0 overflow-y-auto" style="max-height: calc(100vh - 200px)"> - - -
-
- #{{ i + 1 }} + @for (sequence of plan.sequences; track sequence; let i = $index) { + + +
+
+ #{{ i + 1 }} +
+
+ + + + @if (plan.liveStacking.enabled) { + + @if (plan.liveStacking.useCalibrationGroup) { + + } + } +
+
+ + + + +
-
- - - - - + +
+
+
-
- - - - +
+
- -
-
- -
-
- -
-
- + + }
@@ -227,44 +200,27 @@
- - - - - - - - + +
- + tooltip="{{ 'Auto sub folder: ' + plan.autoSubFolderMode }}" + (action)="toggleAutoSubFolder()" />
- - - - - + + (action)="resetCameraCaptureNamingFormat('LIGHT')" + tooltip="Reset" />
- - - - - + + (action)="resetCameraCaptureNamingFormat('DARK')" + tooltip="Reset" />
- - - - - + + (action)="resetCameraCaptureNamingFormat('FLAT')" + tooltip="Reset" />
- - - - - + + (action)="resetCameraCaptureNamingFormat('BIAS')" + tooltip="Reset" />
@@ -351,50 +287,35 @@ pTooltip="Dither" tooltipPosition="bottom"> - + [(value)]="plan.dither.enabled" + (valueChange)="savePreference()" />
- - - - - - - - - + [(value)]="plan.dither.raOnly" + (valueChange)="savePreference()" /> + +
@@ -404,106 +325,78 @@ pTooltip="Auto Focus" tooltipPosition="bottom"> - + [(value)]="plan.autoFocus.enabled" + (valueChange)="savePreference()" />
- - + + [(value)]="plan.autoFocus.onFilterChange" + (valueChange)="savePreference()" />
- - - - - + [(value)]="plan.autoFocus.afterElapsedTimeEnabled" + (valueChange)="savePreference()" /> +
- - - - - + [(value)]="plan.autoFocus.afterExposuresEnabled" + (valueChange)="savePreference()" /> +
- - - - - + [(value)]="plan.autoFocus.afterHFDIncreaseEnabled" + (valueChange)="savePreference()" /> +
- - - - - + [(value)]="plan.autoFocus.afterTemperatureChangeEnabled" + (valueChange)="savePreference()" /> +
@@ -514,41 +407,32 @@ pTooltip="Live Stacking" tooltipPosition="bottom"> - + [(value)]="plan.liveStacking.enabled" + (valueChange)="savePreference()" />
- - - - -
- 32-bit (slower) - +
+ + [(value)]="plan.liveStacking.use32Bits" + (valueChange)="savePreference()" />
- + [(value)]="plan.liveStacking.useCalibrationGroup" + (valueChange)="savePreference()" />
@if (pausingOrPaused) { - + severity="success" /> } @else if (!running) { - + severity="success" /> + } @else if (canStart) { + } - - + severity="danger" />
- + (action)="add()" />
@@ -633,115 +507,96 @@ [style]="{ maxWidth: '400px' }">
- - + + (action)="selectSequenceProperty(false)" />
- + [(value)]="property.properties.EXPOSURE_TIME" />
- + [(value)]="property.properties.EXPOSURE_AMOUNT" />
- + [(value)]="property.properties.EXPOSURE_DELAY" />
- + [(value)]="property.properties.FRAME_TYPE" />
- + [(value)]="property.properties.X" />
- + [(value)]="property.properties.Y" />
- + [(value)]="property.properties.WIDTH" />
- + [(value)]="property.properties.HEIGHT" />
- + [(value)]="property.properties.BIN" />
- + [(value)]="property.properties.FRAME_FORMAT" />
- + [(value)]="property.properties.GAIN" />
- + [(value)]="property.properties.OFFSET" />
- -
-
- + [(value)]="property.properties.CALIBRATION_GROUP" />
+ @if (plan.liveStacking.enabled) { +
+ +
+ }
- + (action)="copySequencePropertyToSequencies()" /> diff --git a/desktop/src/app/sequencer/sequencer.component.ts b/desktop/src/app/sequencer/sequencer.component.ts index a91d1e27b..0f7747c62 100644 --- a/desktop/src/app/sequencer/sequencer.component.ts +++ b/desktop/src/app/sequencer/sequencer.component.ts @@ -1,9 +1,10 @@ import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop' -import { AfterContentInit, Component, HostListener, inject, NgZone, OnDestroy, QueryList, ViewChildren, ViewEncapsulation } from '@angular/core' +import { AfterContentInit, Component, HostListener, inject, NgZone, OnDestroy, viewChildren, ViewEncapsulation } from '@angular/core' import { ActivatedRoute } from '@angular/router' -import { CameraExposureComponent } from '../../shared/components/camera-exposure/camera-exposure.component' -import { DialogMenuComponent } from '../../shared/components/dialog-menu/dialog-menu.component' -import { MenuItem, SlideMenuItem } from '../../shared/components/menu-item/menu-item.component' +import { CameraExposureComponent } from '../../shared/components/camera-exposure.component' +import { DialogMenuComponent } from '../../shared/components/dialog-menu.component' +import { DropdownItem } from '../../shared/components/dropdown.component' +import { MenuItem, SlideMenuItem } from '../../shared/components/menu-item.component' import { SEPARATOR_MENU_ITEM } from '../../shared/constants' import { AngularService } from '../../shared/services/angular.service' import { ApiService } from '../../shared/services/api.service' @@ -11,7 +12,6 @@ import { BrowserWindowService } from '../../shared/services/browser-window.servi import { ElectronService } from '../../shared/services/electron.service' import { PreferenceService } from '../../shared/services/preference.service' import { Tickable, Ticker } from '../../shared/services/ticker.service' -import { DropdownItem } from '../../shared/types/angular.types' import { JsonFile } from '../../shared/types/app.types' import { Camera, cameraCaptureNamingFormatWithDefault, FrameType, updateCameraStartCaptureFromCamera } from '../../shared/types/camera.types' import { Focuser } from '../../shared/types/focuser.types' @@ -57,7 +57,7 @@ export class SequencerComponent implements AfterContentInit, OnDestroy, Tickable protected path?: string // NOTE: Remove the "plan.sequences.length <= 1" on layout if add more options - protected readonly sequenceModel: SlideMenuItem[] = [ + private readonly sequenceModel: SlideMenuItem[] = [ { icon: 'mdi mdi-content-copy', label: 'Apply to all', @@ -167,8 +167,7 @@ export class SequencerComponent implements AfterContentInit, OnDestroy, Tickable }, } - @ViewChildren('cameraExposure') - private readonly cameraExposures!: QueryList + private readonly cameraExposures = viewChildren('cameraExposure') get canStart() { return !!this.plan.camera?.connected && !!this.plan.sequences.find((e) => e.enabled) @@ -274,7 +273,7 @@ export class SequencerComponent implements AfterContentInit, OnDestroy, Tickable if (captureEvent) { const index = event.id - 1 - this.cameraExposures.get(index)?.handleCameraCaptureEvent(captureEvent) + this.cameraExposures().at(index)?.handleCameraCaptureEvent(captureEvent) } }) }) @@ -539,7 +538,7 @@ export class SequencerComponent implements AfterContentInit, OnDestroy, Tickable this.savePreference() } - protected showSequenceMenu(sequence: Sequence, dialogMenu: DialogMenuComponent) { + protected showSequenceMenu(sequence: Sequence, menu: DialogMenuComponent) { this.property.sequence = sequence const index = this.plan.sequences.indexOf(sequence) @@ -555,7 +554,7 @@ export class SequencerComponent implements AfterContentInit, OnDestroy, Tickable this.sequenceModel[7].visible = this.sequenceModel[3].visible if (this.sequenceModel.find((e) => e.visible)) { - dialogMenu.show() + menu.show(this.sequenceModel) } } @@ -649,8 +648,8 @@ export class SequencerComponent implements AfterContentInit, OnDestroy, Tickable protected async start() { if (this.plan.camera) { - for (let i = 0; i < this.cameraExposures.length; i++) { - this.cameraExposures.get(i)?.reset() + for (let i = 0; i < this.cameraExposures().length; i++) { + this.cameraExposures().at(i)?.reset() } // FOCUS OFFSET diff --git a/desktop/src/app/settings/settings.component.html b/desktop/src/app/settings/settings.component.html index 63b7ac5d2..0e0b8b5a9 100644 --- a/desktop/src/app/settings/settings.component.html +++ b/desktop/src/app/settings/settings.component.html @@ -1,337 +1,222 @@
-
-
-
- + @if (tab === 'GENERAL') { +
+
+
+ +
-
-
-
-
- - +
+
+ - -
- {{ preference.location.name || '?' }} -
-
- - - -
- - + [(value)]="preference.location" + (valueChange)="locationChanged($event)" + emptyMessage="No location available" /> +
+ + +
+
+
+
-
-
-
-
-
-
-
- - - - -
-
- -
- @if (plateSolverType === 'ASTROMETRY_NET_ONLINE') { + } @else if (tab === 'PLATE_SOLVER') { +
+
- - - - -
-
- - - - +
- } - @if (plateSolverType !== 'PIXINSIGHT') { -
- - + +
+ } @else { +
+ +
+
+ +
+ } + @if (plateSolverType !== 'PIXINSIGHT') { +
+ - - -
-
- - +
+
+ - - -
- } - @if (plateSolverType === 'PIXINSIGHT') { -
- - +
+ } @else { +
+ - - -
- } -
-
-
-
-
- - - - + [(value)]="plateSolver.slot" + (valueChange)="savePreference()" /> +
+ }
-
- -
-
- - - - -
-
- - + } @else if (tab === 'STAR_DETECTOR') { +
+
+
+ +
+
+ - - + (pathChange)="savePreference()" /> +
+
+ +
+ @if (starDetectorType === 'PIXINSIGHT') { +
+ +
+ }
-
-
-
-
- - - - -
-
- -
-
- - +
+
+ +
+
+ - - + [(path)]="liveStacker.executablePath" + label="Executable path" + (pathChange)="savePreference()" /> +
+ @if (liveStackerType === 'PIXINSIGHT') { +
+ +
+ }
-
-
-
-
- - - - - -
-
- - - - - -
-
- - - - - -
-
- - - - - + } @else if (tab === 'CAPTURE_NAMING_FORMAT') { +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
-
+ }
+ + + @if (label()) { +
{{ label() }}
+ } + + `, + styles: ` + neb-button-image { + .p-disabled { + img { + filter: grayscale(1); + } + } + } + `, + encapsulation: ViewEncapsulation.None, +}) +export class ButtonImageComponent { + readonly label = input() + readonly image = input.required() + readonly imageHeight = input('16px') + readonly tooltip = input() + readonly tooltipPosition = input<'right' | 'left' | 'top' | 'bottom'>('bottom') + readonly disabled = input(false) + readonly severity = input<'success' | 'info' | 'warning' | 'danger' | 'help' | 'primary' | 'secondary' | 'contrast'>() + readonly action = output() +} diff --git a/desktop/src/shared/components/button.component.ts b/desktop/src/shared/components/button.component.ts new file mode 100644 index 000000000..1b8f70745 --- /dev/null +++ b/desktop/src/shared/components/button.component.ts @@ -0,0 +1,46 @@ +import { Component, input, output, ViewEncapsulation } from '@angular/core' + +@Component({ + selector: 'neb-button', + template: ` + + + + `, + styles: ` + neb-button { + .p-button { + &.p-button-icon-only { + aspect-ratio: 1; + } + + &:has(.mdi-lg) { + height: 3.25rem; + } + } + } + `, + encapsulation: ViewEncapsulation.None, +}) +export class ButtonComponent { + readonly label = input() + readonly icon = input() + readonly tooltip = input() + readonly tooltipPosition = input<'right' | 'left' | 'top' | 'bottom'>('bottom') + readonly rounded = input(true) + readonly disabled = input(false) + readonly severity = input<'success' | 'info' | 'warning' | 'danger' | 'help' | 'primary' | 'secondary' | 'contrast'>() + readonly action = output() +} diff --git a/desktop/src/shared/components/camera-exposure.component.ts b/desktop/src/shared/components/camera-exposure.component.ts new file mode 100644 index 000000000..4c1fdb494 --- /dev/null +++ b/desktop/src/shared/components/camera-exposure.component.ts @@ -0,0 +1,137 @@ +import { Component, input, model, ViewEncapsulation } from '@angular/core' +import { CameraCaptureEvent, CameraCaptureState, DEFAULT_CAMERA_CAPTURE_INFO, DEFAULT_CAMERA_STEP_INFO } from '../types/camera.types' + +@Component({ + selector: 'neb-camera-exposure', + template: ` +
+ + + {{ info() || state || 'IDLE' | enum | lowercase }} + + + + + {{ capture.count }} + @if (!capture.looping) { + / {{ capture.amount }} + } + + @if (!capture.looping) { + + + {{ capture.progress * 100 | number: '1.1-1' }} + + } + @if (capture.looping) { + + + {{ capture.elapsedTime | exposureTime }} + + } @else { + + @if (showRemainingTime()) { + + + {{ capture.remainingTime | exposureTime }} + + } @else { + + + {{ capture.elapsedTime | exposureTime }} + + } + + } + @if (capture.amount !== 1 && (state === 'EXPOSURING' || state === 'WAITING')) { + + @if (showRemainingTime()) { + + + {{ step.remainingTime | exposureTime }} + + } @else { + + + {{ step.elapsedTime | exposureTime }} + + } + + + + {{ step.progress * 100 | number: '1.1-1' }} + + } + +
+ `, + styles: ` + neb-camera-exposure { + min-height: 29px; + width: 100%; + + .state { + padding: 1px 6px; + height: 13px; + } + } + `, + encapsulation: ViewEncapsulation.None, +}) +export class CameraExposureComponent { + readonly info = input() + readonly showRemainingTime = model(true) + + protected step = structuredClone(DEFAULT_CAMERA_STEP_INFO) + protected capture = structuredClone(DEFAULT_CAMERA_CAPTURE_INFO) + protected state: CameraCaptureState = 'IDLE' + + get currentState() { + return this.state + } + + handleCameraCaptureEvent(event: Omit, looping: boolean = false) { + this.capture.elapsedTime = event.captureElapsedTime + this.capture.remainingTime = event.captureRemainingTime + this.capture.progress = event.captureProgress + this.capture.count = event.exposureCount + this.capture.amount = event.exposureAmount + if (looping) this.capture.looping = looping + + this.step.elapsedTime = event.stepElapsedTime + this.step.remainingTime = event.stepRemainingTime + this.step.progress = event.stepProgress + + if (event.state === 'EXPOSURING') { + this.state = 'EXPOSURING' + } else if (event.state === 'WAITING') { + this.step.elapsedTime = event.stepElapsedTime + this.step.remainingTime = event.stepRemainingTime + this.step.progress = event.stepProgress + this.state = event.state + } else if (event.state === 'CAPTURE_STARTED') { + this.capture.looping = looping || event.exposureAmount <= 0 + this.capture.amount = event.exposureAmount + this.state = 'EXPOSURING' + } else if (event.state === 'EXPOSURE_STARTED') { + this.state = 'EXPOSURING' + } else if (event.state === 'IDLE' || event.state === 'CAPTURE_FINISHED') { + this.reset() + } else if (event.state !== 'EXPOSURE_FINISHED') { + this.state = event.state + } + + return this.state !== 'CAPTURE_FINISHED' && this.state !== 'IDLE' + } + + reset() { + this.state = 'IDLE' + + this.step = structuredClone(DEFAULT_CAMERA_STEP_INFO) + this.capture = structuredClone(DEFAULT_CAMERA_CAPTURE_INFO) + } +} diff --git a/desktop/src/shared/components/camera-exposure/camera-exposure.component.html b/desktop/src/shared/components/camera-exposure/camera-exposure.component.html deleted file mode 100644 index 43e5f5026..000000000 --- a/desktop/src/shared/components/camera-exposure/camera-exposure.component.html +++ /dev/null @@ -1,66 +0,0 @@ -
- - - {{ info || state || 'IDLE' | enum | lowercase }} - - - - - - {{ capture.count }} - / {{ capture.amount }} - - @if (!capture.looping) { - - - {{ capture.progress * 100 | number: '1.1-1' }} - - } - @if (capture.looping) { - - - {{ capture.elapsedTime | exposureTime }} - - } @else { - - - - {{ capture.remainingTime | exposureTime }} - - - - {{ capture.elapsedTime | exposureTime }} - - - } - - @if (capture.amount !== 1 && (state === 'EXPOSURING' || state === 'WAITING')) { - - - - {{ step.remainingTime | exposureTime }} - - - - {{ step.elapsedTime | exposureTime }} - - - - - {{ step.progress * 100 | number: '1.1-1' }} - - } - -
diff --git a/desktop/src/shared/components/camera-exposure/camera-exposure.component.scss b/desktop/src/shared/components/camera-exposure/camera-exposure.component.scss deleted file mode 100644 index c16fb707b..000000000 --- a/desktop/src/shared/components/camera-exposure/camera-exposure.component.scss +++ /dev/null @@ -1,29 +0,0 @@ -:host { - min-height: 29px; - width: 100%; - - .state { - padding: 1px 6px; - height: 13px; - - &.percentage { - min-width: 50px; - } - - &.time { - min-width: 56px; - } - - &.counter { - min-width: 78px; - } - - .mdi:before { - font-size: 0.9rem !important; - } - } - - .mdi-information::before { - font-size: 0.9rem !important; - } -} diff --git a/desktop/src/shared/components/camera-exposure/camera-exposure.component.ts b/desktop/src/shared/components/camera-exposure/camera-exposure.component.ts deleted file mode 100644 index ddcd59bee..000000000 --- a/desktop/src/shared/components/camera-exposure/camera-exposure.component.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { Component, Input } from '@angular/core' -import { CameraCaptureEvent, CameraCaptureState, DEFAULT_CAMERA_CAPTURE_INFO, DEFAULT_CAMERA_STEP_INFO } from '../../types/camera.types' - -@Component({ - selector: 'neb-camera-exposure', - templateUrl: 'camera-exposure.component.html', - styleUrls: ['camera-exposure.component.scss'], -}) -export class CameraExposureComponent { - @Input() - protected info?: string - - @Input() - protected showRemainingTime: boolean = true - - @Input() - protected readonly step = structuredClone(DEFAULT_CAMERA_STEP_INFO) - - @Input() - protected readonly capture = structuredClone(DEFAULT_CAMERA_CAPTURE_INFO) - - protected state: CameraCaptureState = 'IDLE' - - get currentState() { - return this.state - } - - handleCameraCaptureEvent(event: Omit, looping: boolean = false) { - this.capture.elapsedTime = event.captureElapsedTime - this.capture.remainingTime = event.captureRemainingTime - this.capture.progress = event.captureProgress - this.capture.count = event.exposureCount - this.capture.amount = event.exposureAmount - if (looping) this.capture.looping = looping - this.step.elapsedTime = event.stepElapsedTime - this.step.remainingTime = event.stepRemainingTime - this.step.progress = event.stepProgress - - if (event.state === 'EXPOSURING') { - this.state = 'EXPOSURING' - } else if (event.state === 'WAITING') { - this.step.elapsedTime = event.stepElapsedTime - this.step.remainingTime = event.stepRemainingTime - this.step.progress = event.stepProgress - this.state = event.state - } else if (event.state === 'CAPTURE_STARTED') { - this.capture.looping = looping || event.exposureAmount <= 0 - this.capture.amount = event.exposureAmount - this.state = 'EXPOSURING' - } else if (event.state === 'EXPOSURE_STARTED') { - this.state = 'EXPOSURING' - } else if (event.state === 'IDLE' || event.state === 'CAPTURE_FINISHED') { - this.reset() - } else if (event.state !== 'EXPOSURE_FINISHED') { - this.state = event.state - } - - return this.state !== 'CAPTURE_FINISHED' && this.state !== 'IDLE' - } - - reset() { - this.state = 'IDLE' - - Object.assign(this.step, DEFAULT_CAMERA_STEP_INFO) - Object.assign(this.capture, DEFAULT_CAMERA_CAPTURE_INFO) - } -} diff --git a/desktop/src/shared/components/camera-info.component.ts b/desktop/src/shared/components/camera-info.component.ts new file mode 100644 index 000000000..78043a87b --- /dev/null +++ b/desktop/src/shared/components/camera-info.component.ts @@ -0,0 +1,127 @@ +import { Component, ViewEncapsulation, input, output } from '@angular/core' +import type { CameraStartCapture } from '../types/camera.types' +import type { Focuser } from '../types/focuser.types' +import type { Rotator } from '../types/rotator.types' +import type { Wheel } from '../types/wheel.types' + +@Component({ + selector: 'neb-camera-info', + template: ` +
+ @let mInfo = info(); + + @if (hasType()) { +
+ + {{ mInfo.frameType }} +
+ } + @if (hasExposure() && mInfo.exposureTime) { +
+ + {{ mInfo.exposureAmount || '∞' }} / {{ mInfo.exposureTime | exposureTime }} +
+ } + @if (mInfo.exposureDelay) { +
+ + {{ mInfo.exposureDelay * 1000000 | exposureTime }} +
+ } + @if (mInfo.x !== undefined && mInfo.y !== undefined && mInfo.width && mInfo.height) { +
+ + {{ mInfo.x }} {{ mInfo.y }} {{ mInfo.width }} {{ mInfo.height }} +
+ } + @if (mInfo.binX && mInfo.binY) { +
+ + {{ mInfo.binX }}x{{ mInfo.binY }} +
+ } + @if (mInfo.gain) { +
+ + {{ mInfo.gain }} +
+ } + @if (mInfo.offset) { +
+ + {{ mInfo.offset }} +
+ } + @if (mInfo.frameFormat) { +
+ + {{ mInfo.frameFormat }} +
+ } + @if (hasFilter) { +
+
+ + {{ filter }} +
+ @if (canRemoveFilter() && !disabled()) { + + } +
+ } + @if (hasFilter && focuser() && mInfo.focusOffset) { +
+ + {{ mInfo.focusOffset }} +
+ } + @if (rotator() && mInfo.angle >= 0) { +
+
+ + {{ mInfo.angle.toFixed(1) }}° +
+ @if (canRemoveAngle() && !disabled()) { + + } +
+ } +
+ `, + encapsulation: ViewEncapsulation.None, +}) +export class CameraInfoComponent { + readonly info = input.required() + readonly disabled = input(false) + readonly wheel = input() + readonly focuser = input() + readonly rotator = input() + readonly hasType = input(true) + readonly hasExposure = input(true) + readonly canRemoveFilter = input(false) + readonly canRemoveAngle = input(false) + readonly filterRemoved = output() + readonly angleRemoved = output() + + get hasFilter() { + const wheel = this.wheel() + return !!wheel && !!this.info().filterPosition && wheel.connected + } + + get filter() { + const wheel = this.wheel() + const info = this.info() + + if (wheel && info.filterPosition) { + return wheel.names[info.filterPosition - 1] || `#${info.filterPosition}` + } else { + return undefined + } + } +} diff --git a/desktop/src/shared/components/camera-info/camera-info.component.html b/desktop/src/shared/components/camera-info/camera-info.component.html deleted file mode 100644 index 8aa2004d4..000000000 --- a/desktop/src/shared/components/camera-info/camera-info.component.html +++ /dev/null @@ -1,86 +0,0 @@ -
-
- - {{ info.frameType }} -
-
- - {{ info.exposureAmount || '∞' }} / {{ info.exposureTime | exposureTime }} -
-
- - {{ info.exposureDelay * 1000000 | exposureTime }} -
-
- - {{ info.x }} {{ info.y }} {{ info.width }} {{ info.height }} -
-
- - {{ info.binX }}x{{ info.binY }} -
-
- - {{ info.gain }} -
-
- - {{ info.offset }} -
-
- - {{ info.frameFormat }} -
-
-
- - {{ filter }} -
- -
-
- - {{ info.focusOffset }} -
-
-
- - {{ info.angle.toFixed(1) }}° -
- -
-
diff --git a/desktop/src/shared/components/camera-info/camera-info.component.ts b/desktop/src/shared/components/camera-info/camera-info.component.ts deleted file mode 100644 index d80380794..000000000 --- a/desktop/src/shared/components/camera-info/camera-info.component.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { Component, EventEmitter, Input, Output, ViewEncapsulation } from '@angular/core' -import type { CameraStartCapture } from '../../types/camera.types' -import type { Focuser } from '../../types/focuser.types' -import type { Rotator } from '../../types/rotator.types' -import type { Wheel } from '../../types/wheel.types' - -@Component({ - selector: 'neb-camera-info', - templateUrl: 'camera-info.component.html', - encapsulation: ViewEncapsulation.None, -}) -export class CameraInfoComponent { - @Input({ required: true }) - protected readonly info!: CameraStartCapture - - @Input() - protected readonly wheel?: Wheel - - @Input() - protected readonly focuser?: Focuser - - @Input() - protected readonly rotator?: Rotator - - @Input() - protected readonly hasType: boolean = true - - @Input() - protected readonly hasExposure: boolean = true - - @Input() - protected readonly canRemoveFilter = false - - @Output() - protected readonly filterRemoved = new EventEmitter() - - @Input() - protected readonly canRemoveAngle = false - - @Output() - protected readonly angleRemoved = new EventEmitter() - - @Input() - protected readonly disabled?: boolean = false - - get hasFilter() { - return !!this.wheel && !!this.info.filterPosition && this.wheel.connected - } - - get filter() { - if (this.wheel && this.info.filterPosition) { - return this.wheel.names[this.info.filterPosition - 1] || `#${this.info.filterPosition}` - } else { - return undefined - } - } -} diff --git a/desktop/src/shared/components/checkbox.component.ts b/desktop/src/shared/components/checkbox.component.ts new file mode 100644 index 000000000..30b5edad9 --- /dev/null +++ b/desktop/src/shared/components/checkbox.component.ts @@ -0,0 +1,37 @@ +import { Component, input, model, output, ViewEncapsulation } from '@angular/core' +import { CheckboxChangeEvent } from 'primeng/checkbox' + +@Component({ + selector: 'neb-checkbox', + template: ` + + `, + styles: ` + neb-checkbox { + .vertical { + flex-direction: column; + gap: 4px; + + .p-checkbox-label { + margin-left: 0px; + } + } + } + `, + encapsulation: ViewEncapsulation.None, +}) +export class CheckboxComponent { + readonly label = input() + readonly value = model(false) + readonly disabled = input(false) + readonly noWrap = input(false) + readonly vertical = input(false) + readonly action = output() +} diff --git a/desktop/src/shared/components/device-chooser/device-chooser.component.ts b/desktop/src/shared/components/device-chooser.component.ts similarity index 54% rename from desktop/src/shared/components/device-chooser/device-chooser.component.ts rename to desktop/src/shared/components/device-chooser.component.ts index 9bd25a720..740f1fffa 100644 --- a/desktop/src/shared/components/device-chooser/device-chooser.component.ts +++ b/desktop/src/shared/components/device-chooser.component.ts @@ -1,62 +1,66 @@ -import { Component, EventEmitter, Input, Output, ViewChild, ViewEncapsulation, inject } from '@angular/core' -import { ApiService } from '../../services/api.service' -import { Device } from '../../types/device.types' -import { Undefinable } from '../../utils/types' -import { DeviceConnectionCommandEvent, DeviceListMenuComponent } from '../device-list-menu/device-list-menu.component' -import { MenuItem } from '../menu-item/menu-item.component' +import { Component, ViewEncapsulation, inject, input, model, output, viewChild } from '@angular/core' +import { ApiService } from '../services/api.service' +import { Device } from '../types/device.types' +import { DeviceConnectionCommandEvent, DeviceListMenuComponent } from './device-list-menu.component' +import { MenuItem } from './menu-item.component' @Component({ selector: 'neb-device-chooser', - templateUrl: 'device-chooser.component.html', + template: ` + +
+ +
+ {{ title() }} + @let mDevice = device(); + + @if (mDevice && mDevice.id) { + {{ mDevice.name }} + } @else { + {{ noDeviceMessage() || 'Choose a device' }} + } +
+
+
+ + + `, encapsulation: ViewEncapsulation.None, }) export class DeviceChooserComponent { private readonly api = inject(ApiService) - - @Input({ required: true }) - protected readonly title!: string - - @Input() - protected readonly noDeviceMessage?: string - - @Input({ required: true }) - protected readonly icon!: string - - @Input({ required: true }) - protected readonly devices!: T[] - - @Input() - protected readonly hasNone: boolean = false - - @Input() - protected device?: T - - @Input() - protected readonly disabled?: boolean - - @Output() - readonly deviceChange = new EventEmitter() - - @Output() - readonly deviceConnect = new EventEmitter() - - @Output() - readonly deviceDisconnect = new EventEmitter() - - @ViewChild('deviceMenu') - private readonly deviceMenu!: DeviceListMenuComponent + readonly title = input.required() + readonly noDeviceMessage = input() + readonly icon = input.required() + readonly devices = input.required() + readonly hasNone = input(false) + readonly device = model() + readonly disabled = input() + readonly deviceConnect = output() + readonly deviceDisconnect = output() + + private readonly deviceMenu = viewChild.required('deviceMenu') async show() { - const device = await this.deviceMenu.show(this.devices, this.device) + const device = await this.deviceMenu().show(this.devices(), this.device()) if (device) { - this.device = device === 'NONE' ? undefined : device - this.deviceChange.emit(this.device) + this.device.set(device === 'NONE' ? undefined : device) } } hide() { - this.deviceMenu.hide() + this.deviceMenu().hide() } protected async deviceConnected(event: DeviceConnectionCommandEvent) { @@ -108,7 +112,7 @@ export class DeviceChooserComponent { item.disabled = true - return new Promise>((resolve) => { + return new Promise((resolve) => { let counter = 0 const timer = setTimeout(async () => { diff --git a/desktop/src/shared/components/device-chooser/device-chooser.component.html b/desktop/src/shared/components/device-chooser/device-chooser.component.html deleted file mode 100644 index 34892b60a..000000000 --- a/desktop/src/shared/components/device-chooser/device-chooser.component.html +++ /dev/null @@ -1,26 +0,0 @@ - -
- -
- {{ title }} - @if (device && device.id) { - {{ device.name }} - } @else { - {{ noDeviceMessage || 'Choose a device' }} - } -
-
-
- - diff --git a/desktop/src/shared/components/device-list-menu/device-list-menu.component.ts b/desktop/src/shared/components/device-list-menu.component.ts similarity index 51% rename from desktop/src/shared/components/device-list-menu/device-list-menu.component.ts rename to desktop/src/shared/components/device-list-menu.component.ts index 093669583..0b24231a4 100644 --- a/desktop/src/shared/components/device-list-menu/device-list-menu.component.ts +++ b/desktop/src/shared/components/device-list-menu.component.ts @@ -1,12 +1,12 @@ -import { Component, EventEmitter, Input, Output, ViewChild, ViewEncapsulation, inject } from '@angular/core' -import { SEPARATOR_MENU_ITEM } from '../../constants' -import { AngularService } from '../../services/angular.service' -import { isGuideHead } from '../../types/camera.types' -import { Device } from '../../types/device.types' -import { deviceComparator } from '../../utils/comparators' -import { Undefinable } from '../../utils/types' -import { DialogMenuComponent } from '../dialog-menu/dialog-menu.component' -import { MenuItem, SlideMenuItem } from '../menu-item/menu-item.component' +import { Component, ViewEncapsulation, effect, inject, input, output, viewChild } from '@angular/core' +import { SEPARATOR_MENU_ITEM } from '../constants' +import { AngularService } from '../services/angular.service' +import { isGuideHead } from '../types/camera.types' +import { Device } from '../types/device.types' +import { deviceComparator } from '../utils/comparators' +import { Undefinable } from '../utils/types' +import { DialogMenuComponent } from './dialog-menu.component' +import { MenuItem, SlideMenuItem } from './menu-item.component' export interface DeviceConnectionCommandEvent { device: Device @@ -15,44 +15,47 @@ export interface DeviceConnectionCommandEvent { @Component({ selector: 'neb-device-list-menu', - templateUrl: 'device-list-menu.component.html', - styleUrls: ['device-list-menu.component.scss'], + template: ` + + `, + styles: ` + neb-device-list-menu { + .p-menuitem-link { + padding: 0.5rem 0.75rem; + min-height: 43px; + } + } + `, encapsulation: ViewEncapsulation.None, }) export class DeviceListMenuComponent { private readonly angularService = inject(AngularService) - @Input() - protected readonly model: SlideMenuItem[] = [] - - @Input() - protected readonly modelAtFirst: boolean = true - - @Input() - protected readonly disableIfDeviceIsNotConnected: boolean = true - - @Input() - protected header?: string + readonly model = input([]) + readonly modelAtFirst = input(true) + readonly disableIfDeviceIsNotConnected = input(true) + readonly header = input() + readonly hasNone = input(false) + readonly toolbarBuilder = input<(device: Device) => MenuItem[]>() + readonly deviceConnect = output() + readonly deviceDisconnect = output() - @Input() - protected readonly hasNone: boolean = false + readonly menu = viewChild.required('menu') - @Input() - protected readonly toolbarBuilder?: (device: Device) => MenuItem[] + protected currentHeader?: string - @Output() - readonly deviceConnect = new EventEmitter() - - @Output() - readonly deviceDisconnect = new EventEmitter() - - @ViewChild('menu') - private readonly menu!: DialogMenuComponent + constructor() { + effect(() => { + this.currentHeader = this.header() + }) + } show(devices: T[], selected?: NoInfer, header?: string) { const model: SlideMenuItem[] = [] - if (header) this.header = header + this.currentHeader = header || this.header() return new Promise>((resolve) => { if (devices.length <= 0) { @@ -62,7 +65,7 @@ export class DeviceListMenuComponent { } const populateWithModel = () => { - for (const item of this.model) { + for (const item of this.model()) { model.push({ ...item, command: (event) => { @@ -73,19 +76,20 @@ export class DeviceListMenuComponent { } } - const subscription = this.menu.visibleChange.subscribe((visible) => { + const subscription = this.menu().visible.subscribe((visible) => { if (!visible) { subscription.unsubscribe() resolve(undefined) } }) - if (this.model.length > 0 && this.modelAtFirst) { + const modelAtFirst = this.modelAtFirst() + if (this.model().length > 0 && modelAtFirst) { populateWithModel() model.push(SEPARATOR_MENU_ITEM) } - if (this.hasNone) { + if (this.hasNone()) { model.push({ icon: 'mdi mdi-close', label: 'None', @@ -98,12 +102,12 @@ export class DeviceListMenuComponent { } for (const device of devices.sort(deviceComparator)) { - const toolbarMenu = this.toolbarBuilder?.(device) ?? [] + const toolbarMenu = this.toolbarBuilder()?.(device) ?? [] model.push({ label: device.name, selected: selected === device, - disabled: this.disableIfDeviceIsNotConnected && !device.connected, + disabled: this.disableIfDeviceIsNotConnected() && !device.connected, slideMenu: [], toolbarMenu: [ ...toolbarMenu, @@ -126,16 +130,16 @@ export class DeviceListMenuComponent { }) } - if (this.model.length > 0 && !this.modelAtFirst) { + if (this.model().length > 0 && !modelAtFirst) { model.push(SEPARATOR_MENU_ITEM) populateWithModel() } - this.menu.show(model) + this.menu().show(model) }) } hide() { - this.menu.hide() + this.menu().hide() } } diff --git a/desktop/src/shared/components/device-list-menu/device-list-menu.component.html b/desktop/src/shared/components/device-list-menu/device-list-menu.component.html deleted file mode 100644 index 2b1bab0aa..000000000 --- a/desktop/src/shared/components/device-list-menu/device-list-menu.component.html +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/desktop/src/shared/components/device-list-menu/device-list-menu.component.scss b/desktop/src/shared/components/device-list-menu/device-list-menu.component.scss deleted file mode 100644 index ef8a048d3..000000000 --- a/desktop/src/shared/components/device-list-menu/device-list-menu.component.scss +++ /dev/null @@ -1,6 +0,0 @@ -neb-device-list-menu { - .p-menuitem-link { - padding: 0.5rem 0.75rem; - min-height: 43px; - } -} diff --git a/desktop/src/shared/components/device-name.component.ts b/desktop/src/shared/components/device-name.component.ts new file mode 100644 index 000000000..62a5d0c91 --- /dev/null +++ b/desktop/src/shared/components/device-name.component.ts @@ -0,0 +1,19 @@ +import { Component, ViewEncapsulation, input } from '@angular/core' +import type { Device } from '../types/device.types' + +@Component({ + selector: 'neb-device-name', + template: ` +
+ {{ device().name }} +
+ DRIVER: {{ device().driverName }} + VERSION: {{ device().driverVersion }} +
+
+ `, + encapsulation: ViewEncapsulation.None, +}) +export class DeviceNameComponent { + readonly device = input.required() +} diff --git a/desktop/src/shared/components/device-name/device-name.component.ts b/desktop/src/shared/components/device-name/device-name.component.ts deleted file mode 100644 index 90c43e0c0..000000000 --- a/desktop/src/shared/components/device-name/device-name.component.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Component, Input, ViewEncapsulation } from '@angular/core' -import type { Device } from '../../types/device.types' - -@Component({ - selector: 'neb-device-name', - template: ` -
- {{ device.name }} -
- DRIVER: {{ device.driverName }} - VERSION: {{ device.driverVersion }} -
-
- `, - encapsulation: ViewEncapsulation.None, -}) -export class DeviceNameComponent { - @Input({ required: true }) - readonly device!: Device -} diff --git a/desktop/src/shared/components/dialog-menu.component.ts b/desktop/src/shared/components/dialog-menu.component.ts new file mode 100644 index 000000000..00605e96c --- /dev/null +++ b/desktop/src/shared/components/dialog-menu.component.ts @@ -0,0 +1,86 @@ +import { Component, ViewEncapsulation, effect, input, model } from '@angular/core' +import { MenuItemCommandEvent, SlideMenuItem } from './menu-item.component' + +@Component({ + selector: 'neb-dialog-menu', + template: ` + + @if (currentHeader) { + {{ currentHeader }} + } + @if (visible()) { + + } + + `, + styles: ` + neb-dialog-menu { + .p-menuitem-content { + border-radius: 4px; + } + } + `, + encapsulation: ViewEncapsulation.None, +}) +export class DialogMenuComponent { + readonly visible = model(false) + readonly header = input() + readonly updateHeaderWithMenuLabel = input(true) + + protected model: SlideMenuItem[] = [] + protected currentHeader = this.header() + private readonly navigationHeader: string[] = [] + + constructor() { + effect(() => { + this.currentHeader = this.header() + }) + } + + show(model: SlideMenuItem[]) { + this.model = model + this.currentHeader = this.header() + this.visible.set(true) + } + + hide() { + this.visible.set(false) + this.navigationHeader.length = 0 + } + + protected next(event: MenuItemCommandEvent) { + if (!event.item?.slideMenu?.length) { + this.hide() + } else { + this.navigationHeader.push(this.currentHeader ?? '') + + if (this.updateHeaderWithMenuLabel()) { + this.currentHeader = event.item.label + } + } + } + + back() { + if (this.navigationHeader.length) { + const header = this.navigationHeader.splice(this.navigationHeader.length - 1, 1)[0] + + if (this.updateHeaderWithMenuLabel()) { + this.currentHeader = header + } + } + } +} diff --git a/desktop/src/shared/components/dialog-menu/dialog-menu.component.html b/desktop/src/shared/components/dialog-menu/dialog-menu.component.html deleted file mode 100644 index 69c950f67..000000000 --- a/desktop/src/shared/components/dialog-menu/dialog-menu.component.html +++ /dev/null @@ -1,23 +0,0 @@ - - - {{ currentHeader }} - - - diff --git a/desktop/src/shared/components/dialog-menu/dialog-menu.component.scss b/desktop/src/shared/components/dialog-menu/dialog-menu.component.scss deleted file mode 100644 index 01a42993d..000000000 --- a/desktop/src/shared/components/dialog-menu/dialog-menu.component.scss +++ /dev/null @@ -1,5 +0,0 @@ -neb-dialog-menu { - .p-menuitem-content { - border-radius: 4px; - } -} diff --git a/desktop/src/shared/components/dialog-menu/dialog-menu.component.ts b/desktop/src/shared/components/dialog-menu/dialog-menu.component.ts deleted file mode 100644 index 362444891..000000000 --- a/desktop/src/shared/components/dialog-menu/dialog-menu.component.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges, ViewEncapsulation } from '@angular/core' -import { Undefinable } from '../../utils/types' -import { MenuItemCommandEvent, SlideMenuItem } from '../menu-item/menu-item.component' - -@Component({ - selector: 'neb-dialog-menu', - templateUrl: 'dialog-menu.component.html', - styleUrls: ['dialog-menu.component.scss'], - encapsulation: ViewEncapsulation.None, -}) -export class DialogMenuComponent implements OnChanges { - @Input() - protected visible = false - - @Output() - readonly visibleChange = new EventEmitter() - - @Input() - protected model: SlideMenuItem[] = [] - - @Input() - protected header?: string - - @Input() - protected updateHeaderWithMenuLabel: boolean = true - - protected currentHeader = this.header - private readonly navigationHeader: Undefinable[] = [] - - ngOnChanges(changes: SimpleChanges) { - for (const key in changes) { - if (key === 'header') { - this.currentHeader = changes[key].currentValue as string - } - } - } - - show(model?: SlideMenuItem[]) { - if (model?.length) this.model = model - this.currentHeader = this.header - this.visible = true - this.visibleChange.emit(true) - } - - hide() { - this.visible = false - this.navigationHeader.length = 0 - this.visibleChange.emit(false) - } - - protected next(event: MenuItemCommandEvent) { - if (!event.item?.slideMenu?.length) { - this.hide() - } else { - this.navigationHeader.push(this.currentHeader) - - if (this.updateHeaderWithMenuLabel) { - this.currentHeader = event.item.label - } - } - } - - back() { - if (this.navigationHeader.length) { - const header = this.navigationHeader.splice(this.navigationHeader.length - 1, 1)[0] - - if (this.updateHeaderWithMenuLabel) { - this.currentHeader = header - } - } - } -} diff --git a/desktop/src/shared/components/dropdown.component.ts b/desktop/src/shared/components/dropdown.component.ts new file mode 100644 index 000000000..e90482bc3 --- /dev/null +++ b/desktop/src/shared/components/dropdown.component.ts @@ -0,0 +1,169 @@ +import { Component, input, model, Signal, TemplateRef, viewChild, ViewEncapsulation, WritableSignal } from '@angular/core' +import { Dropdown } from 'primeng/dropdown' + +export interface DropdownItem { + label: string + value: T +} + +abstract class DropdownBaseComponent { + abstract readonly label: Signal + abstract readonly options: Signal + abstract readonly value: WritableSignal + abstract readonly disabled: Signal + abstract readonly filter: Signal + abstract readonly emptyMessage: Signal + protected abstract readonly dropdown: Signal + + hide() { + this.dropdown().hide() + } +} + +@Component({ + selector: 'neb-dropdown', + template: ` + + + + @if (itemTemplate()) { + + } @else { +
+ {{ itemLabel(item) }} +
+ } +
+ + @if (itemTemplate()) { + + } @else { +
+ {{ itemLabel(item) }} +
+ } +
+
+ +
+ `, + styles: ` + neb-dropdown { + width: 100%; + } + `, + encapsulation: ViewEncapsulation.None, +}) +export class DropdownComponent extends DropdownBaseComponent { + readonly label = input() + readonly options = input([]) + readonly value = model() + readonly optionLabel = input('name') + readonly optionValue = input() + readonly disabled = input(false) + readonly filter = input(false) + readonly filterFields = input() + readonly emptyMessage = input('Not available') + readonly itemTemplate = input>() + protected readonly dropdown = viewChild.required('dropdown') + + protected itemLabel(item: unknown) { + return (item as Record)[this.optionLabel()] ?? `${item}` + } +} + +@Component({ + selector: 'neb-dropdown-item', + template: ` + + + + + `, + styles: ` + neb-dropdown-item { + width: 100%; + } + `, + encapsulation: ViewEncapsulation.None, +}) +export class DropdownItemComponent extends DropdownBaseComponent, T> { + readonly label = input() + readonly options = input[]>([]) + readonly value = model() + readonly disabled = input(false) + readonly filter = input(false) + readonly emptyMessage = input('Not available') + readonly dropdown = viewChild.required('dropdown') +} + +@Component({ + selector: 'neb-dropdown-enum', + template: ` + + + + + `, + styles: ` + neb-dropdown-enum { + width: 100%; + } + `, + encapsulation: ViewEncapsulation.None, +}) +export class DropdownEnumComponent extends DropdownBaseComponent { + readonly label = input() + readonly options = input([]) + readonly value = model() + readonly filter = input(false) + readonly disabled = input(false) + readonly emptyMessage = input('Not available') + readonly dropdown = viewChild.required('dropdown') +} diff --git a/desktop/src/shared/components/histogram/histogram.component.ts b/desktop/src/shared/components/histogram.component.ts similarity index 71% rename from desktop/src/shared/components/histogram/histogram.component.ts rename to desktop/src/shared/components/histogram.component.ts index a8595842d..b0f446456 100644 --- a/desktop/src/shared/components/histogram/histogram.component.ts +++ b/desktop/src/shared/components/histogram.component.ts @@ -1,23 +1,28 @@ -import { AfterViewInit, Component, ElementRef, ViewChild, ViewEncapsulation } from '@angular/core' -import { ImageHistrogram } from '../../types/image.types' +import { Component, ElementRef, ViewEncapsulation, effect, viewChild } from '@angular/core' +import { ImageHistrogram } from '../types/image.types' @Component({ selector: 'neb-histogram', - templateUrl: 'histogram.component.html', + template: ` + + `, encapsulation: ViewEncapsulation.None, }) -export class HistogramComponent implements AfterViewInit { - @ViewChild('canvas') - private readonly canvas!: ElementRef +export class HistogramComponent { + private readonly canvas = viewChild.required>('canvas') private ctx?: CanvasRenderingContext2D | null - ngAfterViewInit() { - this.ctx = this.canvas.nativeElement.getContext('2d') + constructor() { + effect(() => { + this.ctx = this.canvas().nativeElement.getContext('2d') + }) } update(data: ImageHistrogram, dontClear: boolean = false) { - const canvas = this.canvas.nativeElement + const canvas = this.canvas().nativeElement if (!dontClear || !data.length) { this.ctx?.clearRect(0, 0, canvas.width, canvas.height) @@ -36,7 +41,7 @@ export class HistogramComponent implements AfterViewInit { private drawColorGraph(data: ImageHistrogram, max: number, start: number = 0, end: number = data.length - 1, color: string | CanvasGradient | CanvasPattern) { if (this.ctx) { - const canvas = this.canvas.nativeElement + const canvas = this.canvas().nativeElement const graphHeight = canvas.height const graphWidth = canvas.width diff --git a/desktop/src/shared/components/histogram/histogram.component.html b/desktop/src/shared/components/histogram/histogram.component.html deleted file mode 100644 index 15903fdb1..000000000 --- a/desktop/src/shared/components/histogram/histogram.component.html +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/desktop/src/shared/components/indicator.component.ts b/desktop/src/shared/components/indicator.component.ts new file mode 100644 index 000000000..b66137642 --- /dev/null +++ b/desktop/src/shared/components/indicator.component.ts @@ -0,0 +1,42 @@ +import { Component, input, model, ViewEncapsulation } from '@angular/core' + +@Component({ + selector: 'neb-indicator', + template: ` +
+ @for (item of [].constructor(count()); track $index) { + + } +
+ `, + styles: ` + neb-indicator { + .indicator { + background-color: #3f3f46; + width: 0.7rem; + height: 0.7rem; + transition: + background-color 0.2s, + color 0.2s, + border-color 0.2s, + box-shadow 0.2s, + outline-color 0.2s; + border-radius: 50%; + cursor: pointer; + margin: 1px; + + &.selected { + background-color: #60a5fa; + } + } + } + `, + encapsulation: ViewEncapsulation.None, +}) +export class IndicatorComponent { + readonly count = input.required() + readonly position = model(0) +} diff --git a/desktop/src/shared/components/input-number.component.ts b/desktop/src/shared/components/input-number.component.ts new file mode 100644 index 000000000..5695e8065 --- /dev/null +++ b/desktop/src/shared/components/input-number.component.ts @@ -0,0 +1,59 @@ +import { Component, computed, input, model, ViewEncapsulation } from '@angular/core' + +@Component({ + selector: 'neb-input-number', + template: ` + + + + + `, + styles: ` + neb-input-number { + display: flex; + align-items: center; + + .p-button-icon-only.p-inputnumber-button { + width: 2rem; + } + + .p-inputtext { + border: 1px solid rgba(255, 255, 255, 0) !important; + } + } + `, + encapsulation: ViewEncapsulation.None, +}) +export class InputNumberComponent { + readonly label = input() + readonly min = input() + readonly max = input() + readonly step = input(1) + readonly value = model(0) + readonly disabled = input(false) + readonly fractionDigits = input(0) + readonly format = input(true) + readonly suffix = input() + readonly placeholder = input() + readonly readonly = input(false) + + protected readonly minFractionDigits = computed(() => Math.max(this.fractionDigits(), this.step() < 1 ? 1 : 0)) + protected readonly maxFractionDigits = computed(() => Math.max(this.fractionDigits(), this.minFractionDigits())) +} diff --git a/desktop/src/shared/components/input-text.component.ts b/desktop/src/shared/components/input-text.component.ts new file mode 100644 index 000000000..96ed72086 --- /dev/null +++ b/desktop/src/shared/components/input-text.component.ts @@ -0,0 +1,42 @@ +import { Component, input, model, ViewEncapsulation } from '@angular/core' + +@Component({ + selector: 'neb-input-text', + template: ` + + + + + `, + styles: ` + neb-input-text { + width: 100%; + display: flex; + align-items: center; + + .p-inputtext { + border: 1px solid rgba(255, 255, 255, 0) !important; + } + } + `, + encapsulation: ViewEncapsulation.None, +}) +export class InputTextComponent { + readonly label = input() + readonly maxLength = input(256) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + readonly value = model() + readonly disabled = input(false) + readonly placeholder = input('') + readonly readonly = input(false) + readonly tooltip = input() +} diff --git a/desktop/src/shared/components/location.component.ts b/desktop/src/shared/components/location.component.ts new file mode 100644 index 000000000..59ed85b7a --- /dev/null +++ b/desktop/src/shared/components/location.component.ts @@ -0,0 +1,75 @@ +import { AfterViewInit, Component, input, output, viewChild, ViewEncapsulation } from '@angular/core' +import type { Location } from '../types/atlas.types' +import { MapComponent } from './map.component' + +@Component({ + selector: 'neb-location', + template: ` + @let mLocation = location(); + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ `, + encapsulation: ViewEncapsulation.None, +}) +export class LocationComponent implements AfterViewInit { + readonly location = input.required() + readonly update = output() + + readonly map = viewChild.required('map') + + ngAfterViewInit() { + this.map().refresh() + } + + protected locationUpdated() { + this.update.emit() + } +} diff --git a/desktop/src/shared/components/location/location.dialog.html b/desktop/src/shared/components/location/location.dialog.html deleted file mode 100644 index 8bb727601..000000000 --- a/desktop/src/shared/components/location/location.dialog.html +++ /dev/null @@ -1,94 +0,0 @@ -
-
- - - - -
-
- - - - -
-
- - - - -
-
- - - - -
-
- - - - -
-
- -
-
- diff --git a/desktop/src/shared/components/location/location.dialog.ts b/desktop/src/shared/components/location/location.dialog.ts deleted file mode 100644 index 992b70750..000000000 --- a/desktop/src/shared/components/location/location.dialog.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { AfterViewInit, Component, EventEmitter, Input, Output, ViewChild, inject } from '@angular/core' -import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog' -import type { Location } from '../../types/atlas.types' -import { DEFAULT_LOCATION } from '../../types/atlas.types' -import { MapComponent } from '../map/map.component' - -@Component({ - selector: 'neb-location', - templateUrl: 'location.dialog.html', -}) -export class LocationComponent implements AfterViewInit { - private readonly dialogRef = inject(DynamicDialogRef, { optional: true }) - - @ViewChild('map') - private readonly map?: MapComponent - - @Input() - readonly location!: Location - - @Output() - readonly locationChange = new EventEmitter() - - get isDialog() { - return !!this.dialogRef - } - - constructor() { - const config = inject>(DynamicDialogConfig, { optional: true }) - - if (config) { - this.location = config.data ?? structuredClone(DEFAULT_LOCATION) - } - } - - ngAfterViewInit() { - this.map?.refresh() - } - - save() { - this.dialogRef?.close(this.location) - } - - locationChanged() { - if (!this.isDialog) { - this.locationChange.emit(this.location) - } - } -} diff --git a/desktop/src/shared/components/map/map.component.ts b/desktop/src/shared/components/map.component.ts similarity index 61% rename from desktop/src/shared/components/map/map.component.ts rename to desktop/src/shared/components/map.component.ts index 48306a04d..e22396888 100644 --- a/desktop/src/shared/components/map/map.component.ts +++ b/desktop/src/shared/components/map.component.ts @@ -1,26 +1,27 @@ -import { AfterViewInit, Component, ElementRef, EventEmitter, Input, OnChanges, Output, ViewChild } from '@angular/core' +import { AfterViewInit, Component, ElementRef, OnChanges, ViewEncapsulation, model, viewChild } from '@angular/core' import * as L from 'leaflet' @Component({ selector: 'neb-map', - templateUrl: 'map.component.html', - styleUrls: ['map.component.scss'], + template: ` +
+ `, + styles: ` + neb-map { + display: block; + width: 100%; + } + `, + encapsulation: ViewEncapsulation.None, }) export class MapComponent implements AfterViewInit, OnChanges { - @Input() - protected latitude = 0 - - @Output() - readonly latitudeChange = new EventEmitter() - - @Input() - protected longitude = 0 - - @Output() - readonly longitudeChange = new EventEmitter() + readonly latitude = model(0) + readonly longitude = model(0) - @ViewChild('map') - private readonly mapRef!: ElementRef + private readonly mapRef = viewChild.required>('map') private map?: L.Map private marker?: L.Marker @@ -35,15 +36,15 @@ export class MapComponent implements AfterViewInit, OnChanges { }) ngAfterViewInit() { - this.map = L.map(this.mapRef.nativeElement, { - center: { lat: this.latitude, lng: this.longitude }, + this.map = L.map(this.mapRef().nativeElement, { + center: { lat: this.latitude(), lng: this.longitude() }, zoom: 5, doubleClickZoom: false, }) this.map.on('dblclick', (event) => { - this.latitudeChange.emit(event.latlng.lat) - this.longitudeChange.emit(event.latlng.lng) + this.latitude.set(event.latlng.lat) + this.longitude.set(event.latlng.lng) this.updateMarker(event.latlng) }) @@ -60,7 +61,7 @@ export class MapComponent implements AfterViewInit, OnChanges { ngOnChanges() { if (this.map) { - const coordinate: L.LatLngLiteral = { lat: this.latitude, lng: this.longitude } + const coordinate: L.LatLngLiteral = { lat: this.latitude(), lng: this.longitude() } this.map.setView(coordinate) this.updateMarker(coordinate) } diff --git a/desktop/src/shared/components/map/map.component.html b/desktop/src/shared/components/map/map.component.html deleted file mode 100644 index 6a925dbe8..000000000 --- a/desktop/src/shared/components/map/map.component.html +++ /dev/null @@ -1,4 +0,0 @@ -
diff --git a/desktop/src/shared/components/map/map.component.scss b/desktop/src/shared/components/map/map.component.scss deleted file mode 100644 index 2486878d8..000000000 --- a/desktop/src/shared/components/map/map.component.scss +++ /dev/null @@ -1,4 +0,0 @@ -:host { - display: block; - width: 100%; -} diff --git a/desktop/src/shared/components/menu-bar.component.ts b/desktop/src/shared/components/menu-bar.component.ts new file mode 100644 index 000000000..5d91afdfc --- /dev/null +++ b/desktop/src/shared/components/menu-bar.component.ts @@ -0,0 +1,77 @@ +import { Component, input, output, ViewEncapsulation } from '@angular/core' +import { MenuItem } from './menu-item.component' + +export interface SplitButtonClickEvent { + event: MouseEvent + item: MenuItem +} + +@Component({ + selector: 'neb-menu-bar', + template: ` +
+ @for (item of model(); track item; let i = $index) { + + @if (item.visible !== false) { + @if (item.toggleable) { + @if (item.visible) { +
+ +
+ } + } @else if (item.checkable) { + @if (item.visible) { +
+ +
+ } + } @else if (item.label && item.splitButtonMenu?.length) { + + } @else { + @if (item.badge) { + + } + + } + } +
+ } +
+ `, + encapsulation: ViewEncapsulation.None, +}) +export class MenuBarComponent { + readonly model = input.required() + readonly splitButtonClick = output() +} diff --git a/desktop/src/shared/components/menu-bar/menu-bar.component.html b/desktop/src/shared/components/menu-bar/menu-bar.component.html deleted file mode 100644 index 49fce541b..000000000 --- a/desktop/src/shared/components/menu-bar/menu-bar.component.html +++ /dev/null @@ -1,59 +0,0 @@ -
- - @if (item.visible !== false) { - @if (item.toggleable) { -
- {{ item.label }} - -
- } @else if (item.checkable) { -
- {{ item.label }} - -
- } @else if (item.label && item.splitButtonMenu?.length) { - - } @else { - - - } - } -
-
diff --git a/desktop/src/shared/components/menu-bar/menu-bar.component.ts b/desktop/src/shared/components/menu-bar/menu-bar.component.ts deleted file mode 100644 index 5924970d3..000000000 --- a/desktop/src/shared/components/menu-bar/menu-bar.component.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core' -import { MenuItem } from '../menu-item/menu-item.component' - -export interface SplitButtonClickEvent { - event: MouseEvent - item: MenuItem -} - -@Component({ - selector: 'neb-menu-bar', - templateUrl: 'menu-bar.component.html', -}) -export class MenuBarComponent { - @Input({ required: true }) - protected readonly model!: MenuItem[] - - @Output() - readonly splitButtonClick = new EventEmitter() -} diff --git a/desktop/src/shared/components/menu-item/menu-item.component.ts b/desktop/src/shared/components/menu-item.component.ts similarity index 58% rename from desktop/src/shared/components/menu-item/menu-item.component.ts rename to desktop/src/shared/components/menu-item.component.ts index 8a24a01ac..aa1065aaf 100644 --- a/desktop/src/shared/components/menu-item/menu-item.component.ts +++ b/desktop/src/shared/components/menu-item.component.ts @@ -1,7 +1,7 @@ -import { Component, Input, ViewEncapsulation } from '@angular/core' +import { Component, ViewEncapsulation, input } from '@angular/core' import { CheckboxChangeEvent } from 'primeng/checkbox' import { InputSwitchChangeEvent } from 'primeng/inputswitch' -import { Severity, TooltipPosition } from '../../types/angular.types' +import { Severity, TooltipPosition } from '../types/angular.types' export interface MenuItemCommandEvent { originalEvent?: Event @@ -55,10 +55,32 @@ export interface SlideMenuItem extends MenuItem { @Component({ selector: 'neb-menu-item', - templateUrl: 'menu-item.component.html', + template: ` + @let mItem = item(); + + +
+ + {{ mItem.label }} +
+ @if (mItem.toolbarMenu?.length) { + + } + @if (mItem.checkable) { + + } + @if (mItem.items?.length || mItem.slideMenu?.length) { + + } +
+ `, encapsulation: ViewEncapsulation.None, }) export class MenuItemComponent { - @Input({ required: true }) - protected readonly item!: MenuItem + readonly item = input.required() } diff --git a/desktop/src/shared/components/menu-item/menu-item.component.html b/desktop/src/shared/components/menu-item/menu-item.component.html deleted file mode 100644 index b65c070a2..000000000 --- a/desktop/src/shared/components/menu-item/menu-item.component.html +++ /dev/null @@ -1,20 +0,0 @@ - -
- - {{ item.label }} -
- @if (item.toolbarMenu?.length) { - - } - @if (item.checkable) { - - } - @if (item.items?.length || item.slideMenu?.length) { - - } -
diff --git a/desktop/src/shared/components/moon/moon.component.ts b/desktop/src/shared/components/moon.component.ts similarity index 69% rename from desktop/src/shared/components/moon/moon.component.ts rename to desktop/src/shared/components/moon.component.ts index 330d31c08..1866ba7b1 100644 --- a/desktop/src/shared/components/moon/moon.component.ts +++ b/desktop/src/shared/components/moon.component.ts @@ -1,25 +1,23 @@ -import { AfterViewInit, Component, ElementRef, Input, OnChanges, ViewChild, ViewEncapsulation } from '@angular/core' +import { AfterViewInit, Component, ElementRef, OnChanges, ViewEncapsulation, input, viewChild } from '@angular/core' @Component({ selector: 'neb-moon', - templateUrl: 'moon.component.html', + template: ` + + `, encapsulation: ViewEncapsulation.None, }) export class MoonComponent implements AfterViewInit, OnChanges { - @Input() - protected height = 256 + readonly height = input(256) + readonly width = input(256) + readonly illuminationRatio = input(0) + readonly waning = input(false) - @Input() - protected width = 256 - - @Input() - protected illuminationRatio = 0 - - @Input() - protected waning = false - - @ViewChild('moon') - private readonly moon?: ElementRef + private readonly moonRef = viewChild.required>('moon') ngAfterViewInit() { this.draw() @@ -31,8 +29,7 @@ export class MoonComponent implements AfterViewInit, OnChanges { // Adapted from https://codepen.io/ardathksheyna/pen/adMyXx. private draw() { - const canvas = this.moon?.nativeElement - if (!canvas) return + const canvas = this.moonRef().nativeElement const ctx = canvas.getContext('2d')! ctx.clearRect(0, 0, canvas.width, canvas.height) @@ -54,9 +51,9 @@ export class MoonComponent implements AfterViewInit, OnChanges { let x1 = Math.ceil(Math.cos(angle) * cx) const y1 = Math.ceil(Math.sin(angle) * cy) const w = x1 * 2 - let x2 = Math.floor(w * this.illuminationRatio) + let x2 = Math.floor(w * this.illuminationRatio()) - if (this.waning) { + if (this.waning()) { x1 = cx + x1 x2 = x1 - (w - x2) } else { diff --git a/desktop/src/shared/components/moon/moon.component.html b/desktop/src/shared/components/moon/moon.component.html deleted file mode 100644 index 960765765..000000000 --- a/desktop/src/shared/components/moon/moon.component.html +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/desktop/src/shared/components/path-chooser.component.ts b/desktop/src/shared/components/path-chooser.component.ts new file mode 100644 index 000000000..a8f3ff28d --- /dev/null +++ b/desktop/src/shared/components/path-chooser.component.ts @@ -0,0 +1,49 @@ +import { Component, inject, input, model, ViewEncapsulation } from '@angular/core' +import { ElectronService } from '../services/electron.service' + +@Component({ + selector: 'neb-path-chooser', + template: ` +
+ + +
+ `, + encapsulation: ViewEncapsulation.None, +}) +export class PathChooserComponent { + private readonly electronService = inject(ElectronService) + + readonly key = input.required() + readonly label = input() + readonly placeholder = input() + readonly path = model() + readonly disabled = input(false) + readonly readonly = input(false) + readonly directory = input.required() + readonly save = input(false) + + protected async choosePath() { + const key = this.key() + const lastPath = localStorage.getItem(key) || undefined + const defaultPath = lastPath && !this.directory() ? window.path.dirname(lastPath) : lastPath + + const path = await (this.directory() ? this.electronService.openDirectory({ defaultPath }) + : this.save() ? this.electronService.saveFile({ defaultPath }) + : this.electronService.openFile({ defaultPath })) + + if (path) { + this.path.set(path) + localStorage.setItem(key, path) + } + } +} diff --git a/desktop/src/shared/components/path-chooser/path-chooser.component.html b/desktop/src/shared/components/path-chooser/path-chooser.component.html deleted file mode 100644 index 8dd76b81f..000000000 --- a/desktop/src/shared/components/path-chooser/path-chooser.component.html +++ /dev/null @@ -1,23 +0,0 @@ -
- - - - - -
diff --git a/desktop/src/shared/components/path-chooser/path-chooser.component.ts b/desktop/src/shared/components/path-chooser/path-chooser.component.ts deleted file mode 100644 index ae7368fc3..000000000 --- a/desktop/src/shared/components/path-chooser/path-chooser.component.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { Component, EventEmitter, Input, Output, inject } from '@angular/core' -import { ElectronService } from '../../services/electron.service' -import { PreferenceService } from '../../services/preference.service' - -@Component({ - selector: 'neb-path-chooser', - templateUrl: 'path-chooser.component.html', -}) -export class PathChooserComponent { - private readonly electronService = inject(ElectronService) - private readonly preferenceService = inject(PreferenceService) - - @Input({ required: true }) - protected readonly key!: string - - @Input() - protected readonly label?: string - - @Input() - protected readonly placeholder?: string - - @Input() - protected readonly disabled: boolean = false - - @Input() - protected readonly readonly: boolean = false - - @Input({ required: true }) - protected readonly directory: boolean = false - - @Input() - protected readonly save: boolean = false - - @Input() - protected path?: string - - @Output() - readonly pathChange = new EventEmitter() - - protected async choosePath() { - const preference = this.preferenceService.pathChooser.get() - const lastPath = preference[this.key] || undefined - const defaultPath = lastPath && !this.directory ? window.path.dirname(lastPath) : lastPath - - const path = await (this.directory ? this.electronService.openDirectory({ defaultPath }) - : this.save ? this.electronService.saveFile({ defaultPath }) - : this.electronService.openFile({ defaultPath })) - - if (path) { - this.path = path - this.pathChange.emit(path) - - preference[this.key] = path - this.preferenceService.pathChooser.set(preference) - } - } -} diff --git a/desktop/src/shared/components/select-button.component.ts b/desktop/src/shared/components/select-button.component.ts new file mode 100644 index 000000000..4359c1196 --- /dev/null +++ b/desktop/src/shared/components/select-button.component.ts @@ -0,0 +1,71 @@ +import { Component, input, model, Signal, viewChild, ViewEncapsulation, WritableSignal } from '@angular/core' +import { SelectButton } from 'primeng/selectbutton' +import { DropdownItem } from './dropdown.component' + +abstract class SelectButtonBaseComponent { + abstract readonly options: Signal + abstract readonly value: WritableSignal + abstract readonly disabled: Signal + protected abstract readonly button: Signal +} + +@Component({ + selector: 'neb-select-button-item', + template: ` + + `, + styles: ` + neb-select-button-enum { + width: 100%; + + .p-button { + font-size: 0.875rem; + padding: 0.652625rem 0.65625rem; + } + } + `, + encapsulation: ViewEncapsulation.None, +}) +export class SelectButtonItemComponent extends SelectButtonBaseComponent, T> { + readonly options = input[]>([]) + readonly value = model() + readonly disabled = input(false) + readonly button = viewChild.required('button') +} + +@Component({ + selector: 'neb-select-button-enum', + template: ` + + `, + styles: ` + neb-select-button-enum { + width: 100%; + + .p-button { + font-size: 0.875rem; + padding: 0.652625rem 0.65625rem; + } + } + `, + encapsulation: ViewEncapsulation.None, +}) +export class SelectButtonEnumComponent extends SelectButtonBaseComponent { + readonly options = input([]) + readonly value = model() + readonly disabled = input(false) + readonly button = viewChild.required('button') +} diff --git a/desktop/src/shared/components/slide-menu/slide-menu.component.ts b/desktop/src/shared/components/slide-menu.component.ts similarity index 52% rename from desktop/src/shared/components/slide-menu/slide-menu.component.ts rename to desktop/src/shared/components/slide-menu.component.ts index 133ad6aeb..5889271ac 100644 --- a/desktop/src/shared/components/slide-menu/slide-menu.component.ts +++ b/desktop/src/shared/components/slide-menu.component.ts @@ -1,32 +1,44 @@ -import { Component, ElementRef, EventEmitter, Input, OnInit, Output, TemplateRef, ViewEncapsulation } from '@angular/core' -import type { Nullable } from '../../utils/types' -import { MenuItemCommandEvent, SlideMenuItem } from '../menu-item/menu-item.component' +import { Component, ElementRef, OnInit, TemplateRef, ViewEncapsulation, input, output } from '@angular/core' +import { MenuItemCommandEvent, SlideMenuItem } from './menu-item.component' @Component({ selector: 'neb-slide-menu', - templateUrl: 'slide-menu.component.html', + template: ` +
+ + + + + + @if (currentMenu !== model()) { + + } +
+ `, encapsulation: ViewEncapsulation.None, }) export class SlideMenuComponent implements OnInit { - @Input({ required: true }) - readonly model!: SlideMenuItem[] - - @Input() - readonly appendTo: Nullable | string> - - @Output() - readonly forward = new EventEmitter() - - @Output() - readonly backward = new EventEmitter() + readonly model = input.required() + readonly appendTo = input | 'body' | undefined | null>() + readonly forward = output() + readonly backward = output() protected currentMenu!: SlideMenuItem[] private readonly navigation: SlideMenuItem[][] = [] ngOnInit() { - this.processMenu(this.model, 0) - this.currentMenu = this.model + const model = this.model() + this.processMenu(model, 0) + this.currentMenu = model } back(event: MouseEvent) { diff --git a/desktop/src/shared/components/slide-menu/slide-menu.component.html b/desktop/src/shared/components/slide-menu/slide-menu.component.html deleted file mode 100644 index 58f9a4bf2..000000000 --- a/desktop/src/shared/components/slide-menu/slide-menu.component.html +++ /dev/null @@ -1,18 +0,0 @@ -
- - - - - - -
diff --git a/desktop/src/shared/components/switch.component.ts b/desktop/src/shared/components/switch.component.ts new file mode 100644 index 000000000..e69a51531 --- /dev/null +++ b/desktop/src/shared/components/switch.component.ts @@ -0,0 +1,22 @@ +import { Component, input, model, output, ViewEncapsulation } from '@angular/core' +import { InputSwitchChangeEvent } from 'primeng/inputswitch' + +@Component({ + selector: 'neb-switch', + template: ` +
+ {{ label() }} + +
+ `, + encapsulation: ViewEncapsulation.None, +}) +export class SwitchComponent { + readonly label = input() + readonly value = model(false) + readonly disabled = input(false) + readonly action = output() +} diff --git a/desktop/src/shared/components/tag.component.ts b/desktop/src/shared/components/tag.component.ts new file mode 100644 index 000000000..34ca5cdc0 --- /dev/null +++ b/desktop/src/shared/components/tag.component.ts @@ -0,0 +1,74 @@ +import { Component, ElementRef, inject, input, output, ViewEncapsulation } from '@angular/core' + +export type TagSeverity = 'success' | 'secondary' | 'info' | 'warning' | 'danger' | 'contrast' | undefined + +export type TagSize = 'large' | 'normal' + +@Component({ + selector: 'neb-tag, neb-info, neb-success, neb-warn, neb-error', + template: ` + + `, + styles: ` + neb-tag, + neb-info, + neb-success, + neb-warn, + neb-error { + display: contents; + + .p-element { + display: contents; + } + + .p-tag { + border-radius: 2px; + padding: 1.5px 4px; + display: inline-block; + min-height: 12.1px; + + &.large { + font-size: 1.2rem; + line-height: 15px; + padding: 3px 8px; + } + + .p-tag-icon, + .p-tag-value { + line-height: 12.1px; + vertical-align: middle; + } + + .p-tag-icon { + &.mdi::before { + font-size: 0.8rem; + } + } + } + } + `, + encapsulation: ViewEncapsulation.None, +}) +export class TagComponent { + private readonly elementRef = inject(ElementRef) + + readonly label = input() + readonly icon = input() + readonly disabled = input(false) + readonly size = input('normal') + readonly action = output() + readonly severity = input(this.severityFromTagName((this.elementRef.nativeElement as HTMLElement).tagName.toLowerCase())) + + private severityFromTagName(tagName: string): TagSeverity { + if (tagName.endsWith('-info')) return 'info' + else if (tagName.endsWith('-success')) return 'success' + else if (tagName.endsWith('-warn')) return 'warning' + else if (tagName.endsWith('-error')) return 'danger' + return undefined + } +} diff --git a/desktop/src/shared/constants.ts b/desktop/src/shared/constants.ts index cf7016eb0..0ab23073f 100644 --- a/desktop/src/shared/constants.ts +++ b/desktop/src/shared/constants.ts @@ -1,4 +1,4 @@ -import type { MenuItem, SlideMenuItem } from './components/menu-item/menu-item.component' +import type { MenuItem, SlideMenuItem } from './components/menu-item.component' export const EVERY_MINUTE_CRON_TIME = '0 */1 * * * *' diff --git a/desktop/src/shared/dialogs/confirm/confirm.dialog.html b/desktop/src/shared/dialogs/confirm/confirm.dialog.html index 655011566..dc874d812 100644 --- a/desktop/src/shared/dialogs/confirm/confirm.dialog.html +++ b/desktop/src/shared/dialogs/confirm/confirm.dialog.html @@ -1,17 +1,13 @@ {{ message }} diff --git a/desktop/src/shared/pipes/enum-dropdown.pipe.ts b/desktop/src/shared/pipes/enum-dropdown.pipe.ts index 9f38969eb..f36c61ac3 100644 --- a/desktop/src/shared/pipes/enum-dropdown.pipe.ts +++ b/desktop/src/shared/pipes/enum-dropdown.pipe.ts @@ -1,5 +1,5 @@ import { Pipe, PipeTransform, inject } from '@angular/core' -import { DropdownItem } from '../types/angular.types' +import { DropdownItem } from '../components/dropdown.component' import { EnumPipe } from './enum.pipe' @Pipe({ name: 'enumDropdown' }) diff --git a/desktop/src/shared/pipes/enum.pipe.ts b/desktop/src/shared/pipes/enum.pipe.ts index 9f6862dc8..53f75a933 100644 --- a/desktop/src/shared/pipes/enum.pipe.ts +++ b/desktop/src/shared/pipes/enum.pipe.ts @@ -7,7 +7,7 @@ import { DeviceType } from '../types/device.types' import { FlatWizardState } from '../types/flat-wizard.types' import { GuideDirection, GuideState, GuiderPlotMode, GuiderYAxisUnit } from '../types/guider.types' import { Bitpix, ImageChannel, ImageFilterType, SCNRProtectionMethod } from '../types/image.types' -import { MountRemoteControlProtocol } from '../types/mount.types' +import { MountRemoteControlProtocol, TrackMode } from '../types/mount.types' import { PlateSolverType } from '../types/platesolver.types' import { SequencerCaptureMode, SequencerState } from '../types/sequencer.types' import { StarDetectorType } from '../types/stardetector.types' @@ -44,6 +44,7 @@ export type EnumPipeKey = | ImageChannel | MoonPhase | DeviceType + | TrackMode | 'ALL' @Pipe({ name: 'enum' }) @@ -139,6 +140,7 @@ export class EnumPipe implements PipeTransform { CRV: 'Corvus', CUBESAT: 'CubeSats', CURVE_FITTED: 'Curve fitted', + CUSTOM: 'Custom', CVN: 'Canes Venatici', CYG: 'Cygnus', DARK_CLOUD_NEBULA: 'Dark Cloud (nebula)', @@ -249,6 +251,7 @@ export class EnumPipe implements PipeTransform { IRIDIUM_NEXT: 'Iridium NEXT', IRIDIUM: 'Iridium', IRREGULAR_VARIABLE: 'Irregular Variable', + KING: 'King', LAC: 'Lacerta', LAST_30_DAYS: `Last 30 Days' Launches`, LAST_QUARTER: 'Last Quarter', @@ -268,6 +271,7 @@ export class EnumPipe implements PipeTransform { LOW_MASS_X_RAY_BINARY: 'Low Mass X-ray Binary', LOW_SURFACE_BRIGHTNESS_GALAXY: 'Low Surface Brightness Galaxy', LUMINANCE: 'Luminance', + LUNAR: 'Lunar', LUP: 'Lupus', LX200: 'LX200', LYN: 'Lynx', @@ -387,10 +391,12 @@ export class EnumPipe implements PipeTransform { SGE: 'Sagitta', SGR: 'Sagittarius', SHORT: 'Short', + SIDEREAL: 'Sidereal', SINGLE: 'Single', SIRIL: 'Siril', SLEWED: 'Slewed', SLEWING: 'Slewing', + SOLAR: 'Solar', SOLVED: 'Solved', SOLVING: 'Solving', SOUTH: 'South', diff --git a/desktop/src/shared/services/api.service.ts b/desktop/src/shared/services/api.service.ts index 63ce56ba5..a992bc78d 100644 --- a/desktop/src/shared/services/api.service.ts +++ b/desktop/src/shared/services/api.service.ts @@ -14,7 +14,7 @@ import { GuideDirection, GuideOutput, Guider, GuiderHistoryStep, SettleInfo } fr import { ConnectionStatus, ConnectionType, GitHubRelease } from '../types/home.types' import { AnnotateImageRequest, CoordinateInterpolation, DetectedStar, FOVCamera, FOVTelescope, ImageAnalyzed, ImageAnnotation, ImageChannel, ImageInfo, ImageMousePosition, ImageSaveDialog, ImageSolved, ImageStatistics, ImageTransformation } from '../types/image.types' import { LightBox } from '../types/lightbox.types' -import { CelestialLocationType, Mount, MountRemoteControl, MountRemoteControlProtocol, SlewRate, TrackMode } from '../types/mount.types' +import { CelestialLocationType, Mount, MountRemoteControl, MountRemoteControlProtocol, TrackMode } from '../types/mount.types' import { PlateSolverRequest } from '../types/platesolver.types' import { Rotator } from '../types/rotator.types' import { SequencerPlan } from '../types/sequencer.types' @@ -158,8 +158,8 @@ export class ApiService { return this.http.put(`mounts/${mount.id}/track-mode?mode=${mode}`) } - mountSlewRate(mount: Mount, rate: SlewRate) { - return this.http.put(`mounts/${mount.id}/slew-rate?rate=${rate.name}`) + mountSlewRate(mount: Mount, rate: string) { + return this.http.put(`mounts/${mount.id}/slew-rate?rate=${rate}`) } mountMove(mount: Mount, direction: GuideDirection, enabled: boolean) { diff --git a/desktop/src/shared/services/browser-window.service.ts b/desktop/src/shared/services/browser-window.service.ts index 5e97d74c5..cf6b0cdcb 100644 --- a/desktop/src/shared/services/browser-window.service.ts +++ b/desktop/src/shared/services/browser-window.service.ts @@ -31,12 +31,12 @@ export class BrowserWindowService { } openCamera(data: Camera, preference: WindowPreference = {}) { - Object.assign(preference, { icon: 'camera', width: 400, height: 477 }) + Object.assign(preference, { icon: 'camera', width: 390, height: 477 }) return this.openWindow({ preference, data, id: `camera.${data.name}`, path: 'camera' }) } openCameraDialog(data: CameraDialogInput, preference: WindowPreference = {}) { - Object.assign(preference, { icon: 'camera', width: 400, height: 424 }) + Object.assign(preference, { icon: 'camera', width: 390, height: 424 }) return this.openModal({ preference, data, id: `camera.${data.camera.name}.modal`, path: 'camera' }) } diff --git a/desktop/src/shared/services/device.service.ts b/desktop/src/shared/services/device.service.ts index aecb8e380..5c9939d1f 100644 --- a/desktop/src/shared/services/device.service.ts +++ b/desktop/src/shared/services/device.service.ts @@ -1,5 +1,5 @@ import { inject, Injectable } from '@angular/core' -import { DeviceListMenuComponent } from '../components/device-list-menu/device-list-menu.component' +import { DeviceListMenuComponent } from '../components/device-list-menu.component' import { Device } from '../types/device.types' import { AngularService } from './angular.service' diff --git a/desktop/src/shared/services/preference.service.ts b/desktop/src/shared/services/preference.service.ts index c0ed69790..75291cd83 100644 --- a/desktop/src/shared/services/preference.service.ts +++ b/desktop/src/shared/services/preference.service.ts @@ -56,7 +56,6 @@ export class PreferenceService { readonly guider: PreferenceData readonly framing: PreferenceData readonly settings: PreferenceData - readonly pathChooser: PreferenceData> constructor() { this.home = this.create('home', () => structuredClone(DEFAULT_HOME_PREFERENCE), homePreferenceWithDefault) @@ -67,7 +66,6 @@ export class PreferenceService { this.guider = this.create('guider', () => structuredClone(DEFAULT_GUIDER_PREFERENCE), guiderPreferenceWithDefault) this.framing = this.create('framing', () => structuredClone(DEFAULT_FRAMING_PREFERENCE), framingPreferenceWithDefault) this.settings = this.create('settings', () => structuredClone(DEFAULT_SETTINGS_PREFERENCE), settingsPreferenceWithDefault) - this.pathChooser = this.create>('pathChooser', () => ({}) as Record) } create(key: string, defaultValue: T | (() => T), withDefault?: (value: T) => T) { diff --git a/desktop/src/shared/types/angular.types.ts b/desktop/src/shared/types/angular.types.ts index 7abbd0d26..844185944 100644 --- a/desktop/src/shared/types/angular.types.ts +++ b/desktop/src/shared/types/angular.types.ts @@ -2,11 +2,6 @@ export type Severity = 'success' | 'info' | 'warning' | 'danger' export type TooltipPosition = 'right' | 'left' | 'top' | 'bottom' -export interface DropdownItem { - label: string - value: T -} - export function extractDateTime(date: Date) { return [extractDate(date), extractTime(date)] } diff --git a/desktop/src/shared/types/mount.types.ts b/desktop/src/shared/types/mount.types.ts index cd1c34b2f..aaada0bd2 100644 --- a/desktop/src/shared/types/mount.types.ts +++ b/desktop/src/shared/types/mount.types.ts @@ -1,3 +1,4 @@ +import type { DropdownItem } from '../components/dropdown.component' import type { Angle, EquatorialCoordinate } from './atlas.types' import type { Device } from './device.types' import type { GPS } from './gps.types' @@ -7,7 +8,7 @@ export type PierSide = 'EAST' | 'WEST' | 'NEITHER' export type TargetCoordinateType = 'J2000' | 'JNOW' -export type TrackMode = 'SIDEREAL' | ' LUNAR' | 'SOLAR' | 'KING' | 'CUSTOM' +export type TrackMode = 'SIDEREAL' | 'LUNAR' | 'SOLAR' | 'KING' | 'CUSTOM' export type CelestialLocationType = 'ZENITH' | 'NORTH_POLE' | 'SOUTH_POLE' | 'GALACTIC_CENTER' | 'MERIDIAN_EQUATOR' | 'MERIDIAN_ECLIPTIC' | 'EQUATOR_ECLIPTIC' @@ -19,10 +20,7 @@ export type OrdinalDirection = 'NW' | 'NE' | 'SW' | 'SE' export type MountSlewDirection = CardinalDirection | OrdinalDirection -export interface SlewRate { - name: string - label: string -} +export type SlewRate = DropdownItem export interface Parkable { canPark: boolean diff --git a/desktop/src/shared/types/platesolver.types.ts b/desktop/src/shared/types/platesolver.types.ts index 6ac9b2fb6..23da4c5a6 100644 --- a/desktop/src/shared/types/platesolver.types.ts +++ b/desktop/src/shared/types/platesolver.types.ts @@ -16,7 +16,7 @@ export interface PlateSolverRequest extends PlateSolverSettings { blind: boolean centerRA: Angle centerDEC: Angle - radius: Angle + radius: number pixelSize: number focalLength: number width: number diff --git a/desktop/src/shared/types/stardetector.types.ts b/desktop/src/shared/types/stardetector.types.ts index 5093e3ccf..81775bbd5 100644 --- a/desktop/src/shared/types/stardetector.types.ts +++ b/desktop/src/shared/types/stardetector.types.ts @@ -8,8 +8,8 @@ export interface StarDetectorSettings { export interface StarDetectionRequest extends StarDetectorSettings { type: StarDetectorType - minSNR?: number - maxStars?: number + minSNR: number + maxStars: number } export const DEFAULT_STAR_DETECTOR_SETTINGS: StarDetectorSettings = { diff --git a/desktop/src/styles.scss b/desktop/src/styles.scss index c26152acc..3f8890eff 100644 --- a/desktop/src/styles.scss +++ b/desktop/src/styles.scss @@ -38,6 +38,10 @@ body { font-family: 'Roboto'; } +a { + display: inline-flex; +} + a:any-link { color: $infoButtonBg; } @@ -92,35 +96,33 @@ p-table { width: 100%; } -.mdi:before { - display: inline-flex; - justify-content: center; +.mdi { align-items: center; - aspect-ratio: 1; -} - -.mdi:before { - font-size: 1.25rem; -} - -.mdi.mdi-sm:before { - font-size: 1.05rem; -} - -.mdi.mdi-lg:before { - font-size: 2rem; -} + justify-content: center; + vertical-align: middle; + line-height: 0px; + + &::before { + display: inline-flex; + justify-content: center; + align-items: center; + aspect-ratio: 1; + font-size: 1.25rem; + line-height: 0px; + vertical-align: middle; + } -span.p-inputnumber { - width: 100%; -} + &.mdi-sm::before { + font-size: 1rem; + } -p-inputnumber .p-inputnumber-buttons-stacked input { - width: 100%; -} + &.mdi-xs::before { + font-size: 0.9rem; + } -.p-inputnumber-button-group .p-button.p-button-icon-only { - width: 2rem; + &.mdi-lg::before { + font-size: 2rem; + } } .p-selectbutton { @@ -190,21 +192,10 @@ p-dropdown, } .border-0, -.p-selectbutton.border-0 .p-button, -.p-inputwrapper.border-0 .p-inputtext, -.p-inputnumber.border-0 .p-inputtext { +.p-selectbutton.border-0 .p-button { border: 1px solid rgba(255, 255, 255, 0) !important; } -.p-button { - white-space: nowrap; - max-width: 100%; - - &.p-button-icon-only { - min-width: fit-content; - } -} - p-calendar.border-0 .p-calendar-w-btn { border: 0 !important; } @@ -218,18 +209,6 @@ p-calendar.border-0 .p-calendar-w-btn { opacity: 1; } -.p-tag { - border-radius: 2px; - padding: 1px 4px; - display: flex; - min-height: 1rem; - - .p-tag-icon, - .p-tag-value { - line-height: normal; - } -} - .text-overflow-scroll { display: inline-block; white-space: nowrap; @@ -299,6 +278,7 @@ p-dropdown *, p-dropdownitem *, p-tieredmenu *, .p-multiselect-header *, +.p-dropdown-header *, .no-draggable-region, .p-button.p-dialog-header-close { -webkit-app-region: no-drag; @@ -519,3 +499,7 @@ p-tieredmenu *, ::-webkit-scrollbar-corner { background-color: transparent; } + +.no-scrollbar::-webkit-scrollbar { + display: none; +} diff --git a/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/mounts/ASCOMMount.kt b/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/mounts/ASCOMMount.kt index f5bf593f3..aa337700e 100644 --- a/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/mounts/ASCOMMount.kt +++ b/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/mounts/ASCOMMount.kt @@ -215,12 +215,12 @@ data class ASCOMMount( } override fun slewRate(rate: SlewRate) { - slewRate = slewRates.firstOrNull { it.name == rate.name } ?: return + slewRate = slewRates.firstOrNull { it.value == rate.value } ?: return sender.fireOnEventReceived(MountSlewRateChanged(this)) } private fun moveAxis(axisType: AxisType, negative: Boolean, enabled: Boolean) { - val rate = slewRate?.name?.let { axisRates[it] }?.second ?: return LOG.d { warn("axisRate is null") } + val rate = slewRate?.value?.let { axisRates[it] }?.second ?: return LOG.d { warn("axisRate is null") } if (enabled) { service.moveAxis(device.number, axisType, if (negative) -(rate.toDouble()) else rate.toDouble()).doRequest() diff --git a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/mount/INDIMount.kt b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/mount/INDIMount.kt index 26c87c55b..f273bc778 100644 --- a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/mount/INDIMount.kt +++ b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/mount/INDIMount.kt @@ -107,8 +107,8 @@ internal open class INDIMount( val name = message.firstOnSwitch().name - if (slewRate?.name != name) { - slewRate = slewRates.firstOrNull { it.name == name } + if (slewRate?.value != name) { + slewRate = slewRates.firstOrNull { it.value == name } sender.fireOnEventReceived(MountSlewRateChanged(this)) } } @@ -292,7 +292,7 @@ internal open class INDIMount( override fun slewRate(rate: SlewRate) { if (rate in slewRates) { - sendNewSwitch("TELESCOPE_SLEW_RATE", rate.name to true) + sendNewSwitch("TELESCOPE_SLEW_RATE", rate.value to true) } } diff --git a/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/mount/SlewRate.kt b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/mount/SlewRate.kt index 1b749bd4a..1d94a6e54 100644 --- a/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/mount/SlewRate.kt +++ b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/mount/SlewRate.kt @@ -1,3 +1,3 @@ package nebulosa.indi.device.mount -data class SlewRate(val name: String, val label: String) +data class SlewRate(val value: String, val label: String)