Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 17 additions & 9 deletions apps/control/src/app/advanced-view.component.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { Component, computed, inject, signal } from '@angular/core';
import { Component, computed, inject, linkedSignal } from '@angular/core';
import { MatRippleModule } from '@angular/material/core';
import { TranslatePipe } from '@placeos/components';
import { ControlStateService } from './control-state.service';
import { OutputDisplayComponent } from './ui/output-display.component';

const PAGE_SIZE = 6;

@Component({
selector: 'control-advanced-view',
template: `
Expand Down Expand Up @@ -63,18 +65,24 @@ import { OutputDisplayComponent } from './ui/output-display.component';
export class ControlAdvancedViewComponent {
private _state = inject(ControlStateService);

public readonly page = signal(0);

public readonly outputs = this._state.output_list;

private readonly _page_total = computed(() =>
Math.max(1, Math.ceil((this.outputs()?.length || 0) / PAGE_SIZE)),
);
/** Selected page. Moves to the last page when the output list shrinks. */
public readonly page = linkedSignal<number, number>({
source: this._page_total,
computation: (total, previous) =>
Math.min(previous?.value ?? 0, total - 1),
});

public readonly paged_outputs = computed(() => {
const all = this.outputs();
const p = this.page();
return all.slice(p * 6, (p + 1) * 6);
return this.outputs().slice(p * PAGE_SIZE, (p + 1) * PAGE_SIZE);
});

public readonly page_count = computed(() => {
const len = this.outputs()?.length || 0;
return new Array(Math.floor(len / 6) + 1).fill(0);
});
public readonly page_count = computed(() =>
new Array(this._page_total()).fill(0),
);
}
4 changes: 2 additions & 2 deletions apps/control/src/app/status-bar.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ import { DurationPipe } from './ui/duration.pipe';
[disabled]="!rec_status || rec_status === 'stopped'"
binding
[sys]="id"
mod="Capture"
[mod]="capture_mod()?.mod"
onEvent="click"
exec="stop"
>
Expand All @@ -79,7 +79,7 @@ import { DurationPipe } from './ui/duration.pipe';
class="rounded-none"
binding
[sys]="id"
mod="Capture"
[mod]="capture_mod()?.mod"
onEvent="click"
[exec]="rec_status === 'playing' ? 'pause' : 'start'"
>
Expand Down
43 changes: 29 additions & 14 deletions apps/control/src/app/tabbed-view/tab-outlet.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@ import { TVControlsComponent } from './tv-controls.component';
template: `
<i
binding
[sys]="id"
[sys]="id()"
mod="HearingAugmentation"
bind="join_code"
[(model)]="join_code"
></i>
<i
binding
[sys]="id"
[sys]="id()"
mod="HearingAugmentation"
bind="has_t_coil"
[(model)]="hearing_tloop"
Expand All @@ -45,8 +45,18 @@ import { TVControlsComponent } from './tv-controls.component';
<a
matRipple
class="bg-base-100 text-base-content mx-1 flex h-24 w-32 flex-col items-center justify-center overflow-hidden rounded-t rounded-b-none leading-tight opacity-60 shadow-sm"
[routerLink]="['/tabbed', id, tab.id || tab.name]"
routerLinkActive="opacity-100! text-secondary!"
[routerLink]="['/tabbed', id(), tab.id || tab.name]"
[class.opacity-100!]="
(tab.id || tab.name) === active_tab()
"
[class.text-secondary!]="
(tab.id || tab.name) === active_tab()
"
[attr.aria-current]="
(tab.id || tab.name) === active_tab()
? 'page'
: null
"
queryParamsHandling="merge"
(click)="onAction()"
>
Expand All @@ -60,7 +70,7 @@ import { TVControlsComponent } from './tv-controls.component';
}
<div class="absolute top-0 right-0 bottom-2 flex space-x-2">
<voice-assistant
[system_id]="id"
[system_id]="id()"
[enabled]="system()?.voice_control"
></voice-assistant>
@if (join_code) {
Expand Down Expand Up @@ -255,8 +265,7 @@ export class TabOutletComponent extends AsyncHandler {
private _router = inject(Router);

public hearing_tloop = false;
public readonly id = this._service.id;
public readonly active_tab = signal('');
public readonly id = this._service.system_id;
public readonly hide_present_all = this._service.hide_present_all;
public readonly outputs = this._service.output_list;
/** Whether any visible output has a source routed to it */
Expand All @@ -283,6 +292,17 @@ export class TabOutletComponent extends AsyncHandler {
);
});

/** Route tab, then the driver's selected tab, then the first tab */
public readonly active_tab = computed(() => {
const first = this.tabs()[0];
return (
this._route_tab() ||
this._selected_tab() ||
Comment thread
greptile-apps[bot] marked this conversation as resolved.
first?.id ||
first?.name ||
''
);
});
private _user_action = signal(false);
/** Driver's selected tab. A separate computed so other system changes do not re-run the tab sync. */
private _selected_tab = computed(() => this.system()?.selected_tab);
Expand All @@ -305,19 +325,14 @@ export class TabOutletComponent extends AsyncHandler {

constructor() {
super();
effect(() => {
const tab = this._route_tab();
if (tab) this.active_tab.set(tab);
});
effect(() => {
const selected_tab = this._selected_tab();
this.timeout(
'update_tab',
() => {
if (selected_tab) {
this.active_tab.set(selected_tab);
this._router.navigate(
['/tabbed', this.id, selected_tab],
['/tabbed', this.id(), selected_tab],
{
queryParamsHandling: 'merge',
},
Expand All @@ -331,7 +346,7 @@ export class TabOutletComponent extends AsyncHandler {
const available_inputs = this._available_inputs();
const tabs = this.tabs();
const selected_input = this.system()?.selected_input;
const active_tab = this._route_tab();
const active_tab = this.active_tab();
const user_action = this._user_action();
this.timeout(
'inputs',
Expand Down
19 changes: 19 additions & 0 deletions apps/control/src/app/tabbed-view/tabbed-route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { UrlMatchResult, UrlSegment } from '@angular/router';

/**
* Matches `tabbed/:system` with an optional `/:tab`.
* One route config for both forms lets the router reuse the tabbed view
* when the tab changes, instead of building it again.
*/
export function tabbedRouteMatcher(
segments: UrlSegment[],
): UrlMatchResult | null {
const [root, system, tab] = segments;
if (root?.path !== 'tabbed' || !system || segments.length > 3) {
return null;
}
return {
consumed: segments,
posParams: tab ? { system, tab } : { system },
};
}
13 changes: 11 additions & 2 deletions apps/control/src/app/tabbed-view/tabbed-view.component.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Component, computed, effect, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { ActivatedRoute } from '@angular/router';
import { ActivatedRoute, Router } from '@angular/router';

import { SettingsService, VERSION } from '@placeos/common';
import {
Expand Down Expand Up @@ -128,6 +128,7 @@ import { TabOutletComponent } from './tab-outlet.component';
})
export class ControlTabbedViewComponent {
private _route = inject(ActivatedRoute);
private _router = inject(Router);
private _state = inject(ControlStateService);
private _changelog = inject(ChangelogService);
private _settings = inject(SettingsService);
Expand Down Expand Up @@ -166,7 +167,15 @@ export class ControlTabbedViewComponent {

effect(() => {
const params = this._query_param_map();
if (params.get('join') === 'true') this._state.selectMeeting();
if (params.get('join') !== 'true') return;
this._state.selectMeeting();
// Open the meeting list once. Tab links merge query params.
this._router.navigate([], {
relativeTo: this._route,
queryParams: { join: null },
queryParamsHandling: 'merge',
replaceUrl: true,
});
});
}
}
36 changes: 21 additions & 15 deletions apps/control/src/app/ui/joystick.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import {
} from '@angular/core';
import { IconComponent } from '@placeos/components';

/** Distance from the centre, as a fraction of the radius, that does not move the camera */
const DEAD_ZONE = 0.25;

export enum JoystickTilt {
Down = 'down',
Up = 'up',
Expand Down Expand Up @@ -120,17 +123,24 @@ export class JoystickComponent implements OnDestroy {

public handlePan(event: PointerEvent) {
if (!this._box) return;
const point = { x: event.clientX, y: event.clientY };
const box_point = {
y: this._box.top + this._box.height / 2,
x: this._box.left + this._box.width / 2,
};
const angle =
(Math.atan2(point.y - box_point.y, point.x - box_point.x) * 180) /
Math.PI;
const { tilt: tiltInput, pan: panInput } = this;
const tilt = tiltInput();
const pan = panInput();
const dx = event.clientX - (this._box.left + this._box.width / 2);
const dy = event.clientY - (this._box.top + this._box.height / 2);
const tilt = this.tilt();
const pan = this.pan();
if (Math.hypot(dx, dy) < (this._box.width / 2) * DEAD_ZONE) {
this.tilt.set(JoystickTilt.Stop);
this.pan.set(JoystickPan.Stop);
} else {
this._setDirection((Math.atan2(dy, dx) * 180) / Math.PI);
}
const tiltValue = this.tilt();
if (tilt !== tiltValue) this.tiltChange.emit(tiltValue);
const panValue = this.pan();
if (pan !== panValue) this.panChange.emit(panValue);
}

/** Set pan and tilt from an angle in degrees, where 0 is right and 90 is down */
private _setDirection(angle: number) {
this.tilt.set(
angle >= 150 || angle <= -150 || (angle > -30 && angle < 30)
? JoystickTilt.Stop
Expand All @@ -145,10 +155,6 @@ export class JoystickComponent implements OnDestroy {
? JoystickPan.Left
: JoystickPan.Right,
);
const tiltValue = this.tilt();
if (tilt !== tiltValue) this.tiltChange.emit(tiltValue);
const panValue = this.pan();
if (pan !== panValue) this.panChange.emit(panValue);
}

/** Never leave a camera moving when the joystick is removed mid-gesture */
Expand Down
2 changes: 1 addition & 1 deletion apps/control/src/app/ui/lighting-tooltip.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import {
btn
matRipple
class="w-64"
[class.inverse]="state === light.state"
[class.inverse]="state !== light.state"
binding
onEvent="click"
[sys]="id"
Expand Down
2 changes: 1 addition & 1 deletion apps/control/src/app/ui/source-select.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ import { ControlStateService, RoomInput } from '../control-state.service';
source
class="w-48"
[class.inverse]="
input.id === details()?.source
input.id !== details()?.source
"
(click)="selectSource(input)"
>
Expand Down
4 changes: 4 additions & 0 deletions apps/control/src/app/ui/voice-assistant.component.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
Component,
DestroyRef,
ElementRef,
computed,
effect,
Expand Down Expand Up @@ -147,6 +148,9 @@ export class VoiceAssistantComponent {
if (typeof enabled === 'boolean') this._service.setEnabled(enabled);
});

// Stop listening when the panel leaves the room view
inject(DestroyRef).onDestroy(() => this._service.setEnabled(false));
Comment thread
greptile-apps[bot] marked this conversation as resolved.

// Write bar heights directly each frame to skip change detection
effect((onCleanup) => {
const bar_els = this._bar_els();
Expand Down
Loading
Loading