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
120 changes: 2 additions & 118 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 23 additions & 9 deletions src/features/LoginForm.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { FormEvent, useState } from 'react';
import { Alert, Button, Form, Spinner } from 'react-bootstrap';
import { Alert, Button, Form, InputGroup, Spinner } from 'react-bootstrap';
import { Navigate, useLocation, useNavigate } from 'react-router-dom';
import EyeIcon from '../shared/components/EyeIcon';
import useAppParams from '../shared/hooks/useAppParams';
import { useSetLoginCredentialsMutation } from '../store/apiSlice';
import {
Expand Down Expand Up @@ -33,6 +34,7 @@ const LoginForm = () => {

const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [showPassword, setShowPassword] = useState(false);
const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);

Expand Down Expand Up @@ -102,14 +104,26 @@ const LoginForm = () => {
</Form.Group>
<Form.Group className="mb-4" controlId="password">
<Form.Label>Password</Form.Label>
<Form.Control
type="password"
autoComplete="current-password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
disabled={isLoading}
/>
<InputGroup>
<Form.Control
type={showPassword ? "text" : "password"}
autoComplete="current-password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
disabled={isLoading}
/>
<Button
type="button"
variant="outline-secondary"
onClick={() => setShowPassword((prev) => !prev)}
disabled={isLoading}
aria-label={showPassword ? "Hide password" : "Show password"}
aria-pressed={showPassword}
>
<EyeIcon slashed={showPassword} />
</Button>
</InputGroup>
</Form.Group>
<Button type="submit" className="w-100" disabled={isLoading}>
{isLoading ? (
Expand Down
65 changes: 65 additions & 0 deletions src/features/MultiviewLayoutCanvas.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
.canvas {
position: relative;
width: 100%;
background: #111;
overflow: hidden;
}

.canvasLight {
background: #ddd;
}

.tile {
position: absolute;
box-sizing: border-box;
border: 1px solid rgba(255, 255, 255, 0.35);
display: flex;
align-items: flex-end;
padding: 3px 5px;
font-size: 0.68rem;
color: #fff;
background-color: rgba(13, 110, 253, 0.35);
cursor: pointer;
transition: outline-color 0.1s, background-color 0.1s;
outline: 2px solid transparent;
outline-offset: -2px;

&:hover {
background-color: rgba(13, 110, 253, 0.5);
}
}

.tileEmpty {
background-color: rgba(120, 120, 120, 0.15);

&:hover {
background-color: rgba(120, 120, 120, 0.28);
}
}

.tileSelected {
outline-color: #ffc107;
background-color: rgba(255, 193, 7, 0.45);
}

.tileLabel {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);
}

.tileNumberBadge {
position: absolute;
top: 2px;
left: 3px;
font-size: 0.62rem;
opacity: 0.75;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);
}

.canvasMeta {
font-size: 0.68rem;
}

78 changes: 78 additions & 0 deletions src/features/MultiviewLayoutCanvas.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { MultiviewLayoutState, MultiviewTileState } from "../store/apiSlice";
import styles from "./MultiviewLayoutCanvas.module.scss";

export interface MultiviewLayoutCanvasProps {
/** Current multiview canvas/tile layout to render. */
layout: MultiviewLayoutState;
/** Resolves a device key (e.g. a tile's sourceDeviceKey) to a display name. */
resolveSourceName: (deviceKey: string) => string;
darkMode?: boolean;
/** Tile number to render as selected/highlighted, or null/undefined if none. */
selectedTileNumber?: number | null;
/** Called when a tile is clicked, with the full tile state. */
onTileClick?: (tile: MultiviewTileState) => void;
}

/**
* Renders a visual mock-up of what is actually displayed on the monitor fed by a single
* multiview-capable decoder: its canvas at the correct aspect ratio, with every tile
* positioned/sized/stacked to match and labeled with its routed source. Used inside a per-node
* popover in RoutingDeviceNode - purely additive, it does not alter the existing tie-line/route
* graph visualization in Routing.tsx.
*/
const MultiviewLayoutCanvas = ({
layout,
resolveSourceName,
darkMode,
selectedTileNumber,
onTileClick,
}: MultiviewLayoutCanvasProps) => {
const aspectRatio = layout.canvasWidth / layout.canvasHeight;

return (
<div>
<div className={`text-muted mb-1 ${styles.canvasMeta}`}>
{layout.canvasWidth}&times;{layout.canvasHeight}
</div>
<div
className={`${styles.canvas}${darkMode ? "" : ` ${styles.canvasLight}`}`}
style={{ aspectRatio: Number.isFinite(aspectRatio) && aspectRatio > 0 ? aspectRatio : 16 / 9 }}
>
{[...layout.tiles]
.sort((a, b) => a.zOrder - b.zOrder)
.map((tile) => {
const isEmpty = !tile.sourceDeviceKey;
const isSelected = selectedTileNumber === tile.tileNumber;
const sourceName = tile.sourceDeviceKey
? resolveSourceName(tile.sourceDeviceKey)
: "Empty";

return (
<div
key={tile.tileNumber}
className={`${styles.tile}${isEmpty ? ` ${styles.tileEmpty}` : ""}${isSelected ? ` ${styles.tileSelected}` : ""}`}
style={{
left: `${(tile.x / layout.canvasWidth) * 100}%`,
top: `${(tile.y / layout.canvasHeight) * 100}%`,
width: `${(tile.width / layout.canvasWidth) * 100}%`,
height: `${(tile.height / layout.canvasHeight) * 100}%`,
zIndex: tile.zOrder,
}}
title={`Tile ${tile.tileNumber}: ${sourceName}`}
onClick={(e) => {
e.stopPropagation();
onTileClick?.(tile);
}}
>
Comment on lines +51 to +66
<span className={styles.tileNumberBadge}>{tile.tileNumber}</span>
<span className={styles.tileLabel}>{sourceName}</span>
</div>
);
})}
</div>
</div>
);
};

export default MultiviewLayoutCanvas;

Loading