Skip to content
Open
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
2 changes: 2 additions & 0 deletions eink/backend/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ def setup_routes(app):
static_dist = os.path.join(os.path.dirname(__file__), "static_dist")

if os.path.exists(static_dist):
print(f"DEBUG: Serving static files from: {static_dist}")

# Serve index.html at the root
async def index(_): # noqa: U101
return web.FileResponse(os.path.join(static_dist, "index.html"))
Expand Down
2 changes: 1 addition & 1 deletion eink/e2e/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export default defineConfig({
timeout: 15000,
},
webServer: process.env.CI ? undefined : {
command: `cd ../.. && export DATA_DIR=${DATA_DIR} && export MEDIA_DIR=${DATA_DIR}/media && export INGRESS_PORT=${INGRESS_PORT} && cd eink && PYTHONPATH=. backend/.venv/bin/python3 -m backend.main`,
command: `export DATA_DIR=${DATA_DIR} && export MEDIA_DIR=${path.join(DATA_DIR, 'media')} && export INGRESS_PORT=${INGRESS_PORT} && cd ${path.resolve(__dirname, '../..')} && cd eink && PYTHONPATH=. backend/.venv/bin/python3 -m backend.main`,
url: `http://localhost:${INGRESS_PORT}/api/ping`,
reuseExistingServer: true,
stdout: 'pipe',
Expand Down
7 changes: 3 additions & 4 deletions eink/e2e/tests/scene_item_fitting.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,8 @@ test.describe('Scene Item Fitting', () => {

// 4. Verify auto-fit: Check that scaling factor is updated
const scalingInput = dialog.locator('input[type="number"]').first();
const scalingValue = await scalingInput.inputValue();

expect(parseInt(scalingValue)).not.toBe(100);
await expect(scalingInput).not.toHaveValue('100');
const autoFittedValue = await scalingInput.inputValue();

// Check offsets are 0
const offsetXInput = dialog.locator('input[type="number"]').nth(1);
Expand All @@ -87,6 +86,6 @@ test.describe('Scene Item Fitting', () => {
await dialog.locator('button:has-text("FIT")').click();

// Verify it returned to the auto-fitted value
await expect(scalingInput).toHaveValue(scalingValue);
await expect(scalingInput).toHaveValue(autoFittedValue);
});
});
16 changes: 6 additions & 10 deletions eink/frontend/src/app-root.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { LitElement, html, css } from 'lit';
import { customElement, state, query } from 'lit/decorators.js';
import { HaStateController } from './controllers/HaStateController';
import { NavigationController } from './controllers/NavigationController';

import './components/shell/app-header';
import './components/layout/side-bar';
Expand All @@ -21,7 +22,8 @@ import { BaseResourceView } from './components/views/base-resource-view';

@customElement('app-root')
export class AppRoot extends LitElement {
private state = new HaStateController(this);
private navigation = new NavigationController(this);
private state = new HaStateController(this, this.navigation);

static styles = css`
:host {
Expand Down Expand Up @@ -185,13 +187,7 @@ export class AppRoot extends LitElement {
});

if (confirmed) {
this.state.updateActiveLayout({
items: this.state.activeLayout?.items.filter(i => i.id !== e.detail.id)
});
if (this.state.selectedItemId === e.detail.id) {
this.state.selectItem(null);
}
this.state.showMessage('Item deleted', 'success');
this.state.deleteLayoutItem(e.detail.id);
}
}

Expand All @@ -209,10 +205,10 @@ export class AppRoot extends LitElement {
@discard-changes="${this._handleDiscard}"
@delete-item="${this._onHeaderDeleteItem}"
@add-item="${this._onHeaderAddItem}"
@toggle-view-mode="${() => this.state.setViewMode(this.state.viewMode === 'graphical' ? 'yaml' : 'graphical')}"
@toggle-view-mode="${() => this.navigation.setViewMode(this.state.viewMode === 'graphical' ? 'yaml' : 'graphical')}"
@set-section="${async (e: CustomEvent) => {
if (await this._requestNavigationConfirmation()) {
this.state.setSection(e.detail);
this.navigation.setSection(e.detail);
}
}}"
></app-header>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import './scene-item-settings-dialog';
import { SceneItemSettingsDialog } from './scene-item-settings-dialog';

Expand Down Expand Up @@ -45,11 +45,29 @@ describe('SceneItemSettingsDialog', () => {
};

beforeEach(async () => {
// Mock global Image for preview loading
// @ts-ignore
global.Image = class {
onload: any = null;
onerror: any = null;
set src(_: string) {
if (this.onload) setTimeout(() => this.onload(), 0);
}
get width() { return 100; }
get height() { return 100; }
crossOrigin: string = '';
};

element = document.createElement('scene-item-settings-dialog') as SceneItemSettingsDialog;
document.body.appendChild(element);
await element.updateComplete;
});

afterEach(() => {
if (element.parentNode) document.body.removeChild(element);
vi.clearAllMocks();
});

it('populates initial image items', async () => {
await element.show(mockItem, mockLayout as any, mockDisplayTypes as any);
await element.updateComplete;
Expand Down
24 changes: 18 additions & 6 deletions eink/frontend/src/components/dialogs/scene-item-settings-dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,13 @@ export class SceneItemSettingsDialog extends LitElement {
private _cachedPreviewData: any = null;
private _lastPreviewSource: string = '';

disconnectedCallback() {
super.disconnectedCallback();
if (this._updateTimer) {
clearTimeout(this._updateTimer);
}
}

async show(item: any, layout: Layout, displayTypes: DisplayType[]) {
this.item = item;
this._layout = layout;
Expand Down Expand Up @@ -649,7 +656,7 @@ export class SceneItemSettingsDialog extends LitElement {
this._offsetY = 0;

// Automatically fill new image
this._fillImage();
this._applyImageFitting('fill', image);

this._isAddingImage = false;
this.requestUpdate();
Expand Down Expand Up @@ -818,10 +825,15 @@ export class SceneItemSettingsDialog extends LitElement {
this._applyImageFitting('fill');
}

private _applyImageFitting(mode: 'fit' | 'fill') {
if (!this._selectedImageId || !this.item || !this.item.displays || this.item.displays.length === 0) return;
const image = this._availableImages.find(i => i.id === this._selectedImageId);
if (!image) return;
private _applyImageFitting(mode: 'fit' | 'fill', imageToUse?: ImageMetadata) {
if (!this.item || !this.item.displays || this.item.displays.length === 0) return;

let image = imageToUse;
if (!image && this._selectedImageId) {
image = this._availableImages.find(i => i.id === this._selectedImageId);
}

if (!image || !image.dimensions || image.dimensions.width === 0 || image.dimensions.height === 0) return;

const panelBB = this._panelBoundingBox;
const firstDisplayId = this.item.displays[0];
Expand Down Expand Up @@ -849,7 +861,7 @@ export class SceneItemSettingsDialog extends LitElement {
this._offsetY = Math.round((targetHeightPx - scaledHeight) / 2);

// Update the item data
const img = this.item.images.find((i: any) => i.image_id === this._selectedImageId);
const img = this.item.images.find((i: any) => i.image_id === image!.id);
if (img) {
img.scaling_factor = this._scalingFactor;
img.offset = { x: this._offsetX, y: this._offsetY };
Expand Down
16 changes: 11 additions & 5 deletions eink/frontend/src/components/shell/app-header.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { LitElement, html, css } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { commonStyles } from '../../styles/common-styles';
import { type AppSection } from '../../controllers/HaStateController';
import { type AppSection } from '../../controllers/NavigationController';
import { type AppMessage } from '../../controllers/HaStateController';

@customElement('app-header')
export class AppHeader extends LitElement {
Expand Down Expand Up @@ -101,7 +102,7 @@ export class AppHeader extends LitElement {

@property({ type: String }) activeSection: AppSection = 'layouts';
@property({ type: Boolean }) connected = false;
@property({ type: String }) message = '';
@property({ type: Object }) message: string | AppMessage | null = null;
@property({ type: Boolean }) isSaving = false;
@property({ type: Boolean }) isDirty = false;
@property({ type: Boolean }) canDelete = false;
Expand Down Expand Up @@ -155,10 +156,14 @@ export class AppHeader extends LitElement {
</div>

<div class="header-actions">
${this.message ? html`<span class="message-badge">${this.message}</span>` : ''}
${this.message
? (typeof this.message === 'string'
? html`<span class="message-badge info">${this.message}</span>`
: html`<span class="message-badge ${this.message.type}">${this.message.text}</span>`)
: ''}

${this.activeSection !== 'images' ? html`
<button class="secondary icon-button" @click="${() => this._dispatch('toggle-view-mode')}" title="Switch to ${this.viewMode === 'graphical' ? 'YAML' : 'Graphical'} Mode">
<button class="secondary icon-button" @click="${() => this._dispatch('toggle-view-mode')}" title="${this.viewMode === 'graphical' ? 'Switch to YAML Mode' : 'Switch to Graphical Mode'}">
<span class="material-icons">${this.viewMode === 'graphical' ? 'code' : 'dashboard'}</span>
</button>
` : ''}
Expand All @@ -173,7 +178,8 @@ export class AppHeader extends LitElement {
</button>
` : ''}

<button class="secondary icon-button" @click="${() => this._dispatch('delete-item')}" ?disabled="${!this.canDelete || this.isSaving}" title="Delete Current Item">
<button class="secondary icon-button" @click="${() => this._dispatch('delete-item')}" ?disabled="${!this.canDelete || this.isSaving}"
title="Delete Current Item">
<span class="material-icons" style="color: var(--danger-colour);">delete</span>
</button>

Expand Down
2 changes: 1 addition & 1 deletion eink/frontend/src/components/views/layouts-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,7 @@ export class LayoutsView extends BaseResourceView {
<layout-settings-dialog @save="${(e: CustomEvent) => {
this.dispatchEvent(new CustomEvent('update-active-layout', { detail: e.detail.settings }));
this._checkDirty();
this.showMessage(this.activeLayout?.id ? 'Settings applied' : 'Draft settings applied', 'success');
this.showMessage('Settings applied', 'success');
}}"></layout-settings-dialog>
`;
}
Expand Down
28 changes: 26 additions & 2 deletions eink/frontend/src/controllers/HaStateController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,33 @@ describe('HaStateController', () => {
beforeEach(() => {
mockHost = {
addController: vi.fn(),
requestUpdate: vi.fn()
requestUpdate: vi.fn(),
dispatchEvent: vi.fn(),
};
controller = new HaStateController(mockHost);
const mockNavigation = {
activeSection: 'layouts',
viewMode: 'graphical',
selectedItemId: null,
selectedImageId: null,
selectedDisplayTypeId: null,
activeSceneId: null,
isAddingNew: false,
activeLayoutId: null,
setSection: vi.fn().mockImplementation(function(this: any, s: any) {
this.activeSection = s;
mockHost.requestUpdate();
}),
setViewMode: vi.fn().mockImplementation(function(this: any, m: any) {
this.viewMode = m;
mockHost.requestUpdate();
}),
selectItem: vi.fn(),
selectImage: vi.fn(),
selectDisplayType: vi.fn(),
selectScene: vi.fn(),
updateHash: vi.fn()
};
controller = new HaStateController(mockHost, mockNavigation as any);
});

it('initializes with default values', () => {
Expand Down
Loading
Loading