@@ -32,92 +27,65 @@
-
-
+
-
-
Reversed
-
+
+
+ [(value)]="rotator.reversed"
+ (valueChange)="reverse($event)" />
-
-
-
-
-
+
-
+
+ tooltip="Move" />
-
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" />
- 32"
- [rounded]="true"
icon="mdi mdi-plus"
- size="large"
severity="success"
- (onClick)="add()" />
+ (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 }}
-
-
= 0"
- class="flex flex-row gap-1 align-items-center relative">
-
-
- {{ 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