feat: Enhance Table component with new props and styles for better customization and usability

feat: Implement StreamCast package with detailed module structure and initialization
refactor: Simplify player module by re-exporting single-function modules for better maintainability
feat: Add detailed documentation for player controls and overlay components
feat: Improve scene management with new component structures and enhanced functionality
feat: Revamp scheduling facade with modular rendering and action handling
This commit is contained in:
2025-12-30 11:30:36 +00:00
parent d90dab645c
commit 14df5c377d
13 changed files with 698 additions and 188 deletions

View File

@@ -1,21 +1,59 @@
import React from 'react'
import styles from '../../styles/components/Table.module.scss'
export type TableSize = 'small' | 'medium' | 'large'
export type TablePadding = 'checkbox' | 'none' | 'normal'
export type TableAlign = 'left' | 'center' | 'right' | 'justify'
export interface TableProps extends React.TableHTMLAttributes<HTMLTableElement> {
children?: React.ReactNode
/** Size variant */
size?: TableSize
/** Enable sticky header */
stickyHeader?: boolean
/** Enable striped rows */
striped?: boolean
/** Enable bordered variant */
bordered?: boolean
/** Compact mode */
compact?: boolean
}
export const Table: React.FC<TableProps> = ({ children, className = '', ...props }) => (
<table className={`table ${className}`} {...props}>
{children}
</table>
)
export const Table: React.FC<TableProps> = ({
children,
className = '',
size = 'medium',
stickyHeader = false,
striped = false,
bordered = false,
compact = false,
...props
}) => {
const classes = [
styles.table,
styles[size],
stickyHeader && styles.stickyHeader,
striped && styles.striped,
bordered && styles.bordered,
compact && styles.compact,
className,
]
.filter(Boolean)
.join(' ')
return (
<table className={classes} {...props}>
{children}
</table>
)
}
export interface TableHeadProps extends React.HTMLAttributes<HTMLTableSectionElement> {
children?: React.ReactNode
}
export const TableHead: React.FC<TableHeadProps> = ({ children, className = '', ...props }) => (
<thead className={`table-head ${className}`} {...props}>
<thead className={`${styles.head} ${className}`} {...props}>
{children}
</thead>
)
@@ -25,7 +63,7 @@ export interface TableBodyProps extends React.HTMLAttributes<HTMLTableSectionEle
}
export const TableBody: React.FC<TableBodyProps> = ({ children, className = '', ...props }) => (
<tbody className={`table-body ${className}`} {...props}>
<tbody className={`${styles.body} ${className}`} {...props}>
{children}
</tbody>
)
@@ -35,36 +73,88 @@ export interface TableFooterProps extends React.HTMLAttributes<HTMLTableSectionE
}
export const TableFooter: React.FC<TableFooterProps> = ({ children, className = '', ...props }) => (
<tfoot className={`table-footer ${className}`} {...props}>
<tfoot className={`${styles.footer} ${className}`} {...props}>
{children}
</tfoot>
)
export interface TableRowProps extends React.HTMLAttributes<HTMLTableRowElement> {
children?: React.ReactNode
/** Enable hover effect */
hover?: boolean
/** Selected state */
selected?: boolean
/** Make row clickable */
onClick?: React.MouseEventHandler<HTMLTableRowElement>
}
export const TableRow: React.FC<TableRowProps> = ({ children, hover, selected, className = '', ...props }) => (
<tr
className={`table-row ${hover ? 'table-row--hover' : ''} ${selected ? 'table-row--selected' : ''} ${className}`}
{...props}
>
{children}
</tr>
)
export const TableRow: React.FC<TableRowProps> = ({
children,
hover,
selected,
onClick,
className = '',
...props
}) => {
const classes = [
styles.row,
hover && styles.hover,
selected && styles.selected,
onClick && styles.clickable,
className,
]
.filter(Boolean)
.join(' ')
return (
<tr className={classes} onClick={onClick} {...props}>
{children}
</tr>
)
}
export interface TableCellProps extends React.TdHTMLAttributes<HTMLTableCellElement> {
children?: React.ReactNode
/** Render as th element */
header?: boolean
align?: 'left' | 'center' | 'right'
/** Text alignment */
align?: TableAlign
/** Cell padding variant */
padding?: TablePadding
/** Scope for header cells */
scope?: 'col' | 'row' | 'colgroup' | 'rowgroup'
/** Enable sorting on this cell */
sortDirection?: 'asc' | 'desc' | false
}
export const TableCell: React.FC<TableCellProps> = ({ children, header, align, className = '', ...props }) => {
export const TableCell: React.FC<TableCellProps> = ({
children,
header,
align = 'left',
padding = 'normal',
scope,
sortDirection,
className = '',
...props
}) => {
const Tag = header ? 'th' : 'td'
const classes = [
styles.cell,
align && styles[`align${align.charAt(0).toUpperCase() + align.slice(1)}`],
padding === 'checkbox' && styles.paddingCheckbox,
padding === 'none' && styles.paddingNone,
className,
]
.filter(Boolean)
.join(' ')
return (
<Tag className={`table-cell ${align ? `table-cell--${align}` : ''} ${className}`} {...props}>
<Tag
className={classes}
scope={header ? scope : undefined}
aria-sort={sortDirection ? (sortDirection === 'asc' ? 'ascending' : 'descending') : undefined}
{...props}
>
{children}
</Tag>
)
@@ -72,10 +162,22 @@ export const TableCell: React.FC<TableCellProps> = ({ children, header, align, c
export interface TableContainerProps extends React.HTMLAttributes<HTMLDivElement> {
children?: React.ReactNode
/** Max height for scrollable container */
maxHeight?: number | string
}
export const TableContainer: React.FC<TableContainerProps> = ({ children, className = '', ...props }) => (
<div className={`table-container ${className}`} {...props}>
export const TableContainer: React.FC<TableContainerProps> = ({
children,
maxHeight,
className = '',
style,
...props
}) => (
<div
className={`${styles.tableContainer} ${className}`}
style={{ ...style, maxHeight }}
{...props}
>
{children}
</div>
)
@@ -84,19 +186,34 @@ export interface TablePaginationLabelDisplayedRowsArgs {
from: number
to: number
count: number
page: number
}
export interface TablePaginationProps extends React.HTMLAttributes<HTMLDivElement> {
/** Total number of rows */
count?: number
/** Current page (0-indexed) */
page?: number
/** Number of rows per page */
rowsPerPage?: number
/** Options for rows per page dropdown */
rowsPerPageOptions?: number[]
onPageChange?: (event: React.MouseEvent<HTMLButtonElement>, page: number) => void
/** Callback for page change */
onPageChange?: (event: React.MouseEvent<HTMLButtonElement> | null, page: number) => void
/** Callback for rows per page change */
onRowsPerPageChange?: (event: React.ChangeEvent<HTMLSelectElement>) => void
labelRowsPerPage?: string
labelDisplayedRows?: (args: TablePaginationLabelDisplayedRowsArgs) => string
/** Label for rows per page dropdown */
labelRowsPerPage?: React.ReactNode
/** Custom label for displayed rows */
labelDisplayedRows?: (args: TablePaginationLabelDisplayedRowsArgs) => React.ReactNode
/** Show first page button */
showFirstButton?: boolean
/** Show last page button */
showLastButton?: boolean
/** Disable the back button beyond first page */
backIconButtonDisabled?: boolean
/** Disable the next button beyond last page */
nextIconButtonDisabled?: boolean
}
export const TablePagination: React.FC<TablePaginationProps> = ({
@@ -107,23 +224,46 @@ export const TablePagination: React.FC<TablePaginationProps> = ({
onPageChange,
onRowsPerPageChange,
labelRowsPerPage = 'Rows per page:',
labelDisplayedRows = ({ from, to, count }) => `${from}${to} of ${count}`,
labelDisplayedRows = ({ from, to, count }) =>
`${from}${to} of ${count !== -1 ? count : `more than ${to}`}`,
showFirstButton = false,
showLastButton = false,
backIconButtonDisabled,
nextIconButtonDisabled,
className = '',
...props
}) => {
const from = count === 0 ? 0 : page * rowsPerPage + 1
const to = Math.min((page + 1) * rowsPerPage, count)
const totalPages = Math.ceil(count / rowsPerPage)
const to = count !== -1 ? Math.min((page + 1) * rowsPerPage, count) : (page + 1) * rowsPerPage
const totalPages = count !== -1 ? Math.ceil(count / rowsPerPage) : -1
const handleFirstPage = (e: React.MouseEvent<HTMLButtonElement>) => {
onPageChange?.(e, 0)
}
const handlePrevPage = (e: React.MouseEvent<HTMLButtonElement>) => {
onPageChange?.(e, page - 1)
}
const handleNextPage = (e: React.MouseEvent<HTMLButtonElement>) => {
onPageChange?.(e, page + 1)
}
const handleLastPage = (e: React.MouseEvent<HTMLButtonElement>) => {
onPageChange?.(e, Math.max(0, totalPages - 1))
}
const isBackDisabled = backIconButtonDisabled ?? page === 0
const isNextDisabled = nextIconButtonDisabled ?? (totalPages !== -1 && page >= totalPages - 1)
return (
<div className={`table-pagination ${className}`} {...props}>
<span className="table-pagination-label">{labelRowsPerPage}</span>
<div className={`${styles.pagination} ${className}`} {...props}>
<span className={styles.paginationLabel}>{labelRowsPerPage}</span>
<select
className="table-pagination-select"
className={styles.paginationSelect}
value={rowsPerPage}
onChange={(e) => onRowsPerPageChange?.(e)}
aria-label="Rows per page"
>
{rowsPerPageOptions.map((opt) => (
<option key={opt} value={opt}>
@@ -131,38 +271,58 @@ export const TablePagination: React.FC<TablePaginationProps> = ({
</option>
))}
</select>
<span className="table-pagination-displayed">{labelDisplayedRows({ from, to, count })}</span>
<div className="table-pagination-actions">
<span className={styles.paginationDisplayed}>
{labelDisplayedRows({ from, to, count, page })}
</span>
<div className={styles.paginationActions}>
{showFirstButton && (
<button
className="table-pagination-btn"
disabled={page === 0}
onClick={(e) => onPageChange?.(e, 0)}
className={styles.paginationBtn}
disabled={isBackDisabled}
onClick={handleFirstPage}
aria-label="Go to first page"
type="button"
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<polyline points="11 17 6 12 11 7" />
<polyline points="18 17 13 12 18 7" />
</svg>
</button>
)}
<button
className="table-pagination-btn"
disabled={page === 0}
onClick={(e) => onPageChange?.(e, page - 1)}
className={styles.paginationBtn}
disabled={isBackDisabled}
onClick={handlePrevPage}
aria-label="Go to previous page"
type="button"
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<polyline points="15 18 9 12 15 6" />
</svg>
</button>
<button
className="table-pagination-btn"
disabled={page >= totalPages - 1}
onClick={(e) => onPageChange?.(e, page + 1)}
className={styles.paginationBtn}
disabled={isNextDisabled}
onClick={handleNextPage}
aria-label="Go to next page"
type="button"
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<polyline points="9 18 15 12 9 6" />
</svg>
</button>
{showLastButton && (
<button
className="table-pagination-btn"
disabled={page >= totalPages - 1}
onClick={(e) => onPageChange?.(e, totalPages - 1)}
className={styles.paginationBtn}
disabled={isNextDisabled}
onClick={handleLastPage}
aria-label="Go to last page"
type="button"
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<polyline points="13 17 18 12 13 7" />
<polyline points="6 17 11 12 6 7" />
</svg>
</button>
)}
</div>
@@ -171,12 +331,18 @@ export const TablePagination: React.FC<TablePaginationProps> = ({
}
export interface TableSortLabelProps extends React.HTMLAttributes<HTMLSpanElement> {
/** Whether this column is currently sorted */
active?: boolean
/** Sort direction */
direction?: 'asc' | 'desc'
/** Click handler */
onClick?: (event: React.MouseEvent<HTMLSpanElement> | React.KeyboardEvent<HTMLSpanElement>) => void
/** Label content */
children?: React.ReactNode
/** Hide sort icon when inactive */
hideSortIcon?: boolean
IconComponent?: React.ComponentType<{ className?: string }>
/** Custom icon component */
IconComponent?: React.ComponentType<{ className?: string; direction?: 'asc' | 'desc' }>
}
export const TableSortLabel: React.FC<TableSortLabelProps> = ({
@@ -189,23 +355,37 @@ export const TableSortLabel: React.FC<TableSortLabelProps> = ({
className = '',
...props
}) => {
const classes = [styles.sortLabel, active && styles.active, className].filter(Boolean).join(' ')
const sortIcon = IconComponent ? (
<IconComponent className="table-sort-icon" />
<IconComponent className={`${styles.sortIcon} ${styles[direction]}`} direction={direction} />
) : (
<span className="table-sort-icon">{direction === 'asc' ? '↑' : '↓'}</span>
<span className={`${styles.sortIcon} ${styles[direction]}`}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M12 5v14M5 12l7-7 7 7" />
</svg>
</span>
)
const handleKeyDown = (e: React.KeyboardEvent<HTMLSpanElement>) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
onClick?.(e)
}
}
return (
<span
className={`table-sort-label ${active ? 'table-sort-label--active' : ''} ${className}`}
className={classes}
onClick={onClick}
onKeyDown={handleKeyDown}
role="button"
tabIndex={0}
onKeyDown={(e) => e.key === 'Enter' && onClick?.(e)}
aria-pressed={active}
{...props}
>
{children}
{!hideSortIcon && (active || !hideSortIcon) && sortIcon}
{(!hideSortIcon || active) && sortIcon}
</span>
)
}

View File

@@ -0,0 +1,347 @@
/* Table component styles */
/* ===== Table Container ===== */
.tableContainer {
width: 100%;
overflow-x: auto;
border-radius: var(--shape-border-radius, 4px);
&::-webkit-scrollbar {
height: 8px;
}
&::-webkit-scrollbar-track {
background: var(--color-background-default, #f5f5f5);
}
&::-webkit-scrollbar-thumb {
background: var(--color-text-disabled, #bdbdbd);
border-radius: 4px;
}
}
/* ===== Base Table ===== */
.table {
width: 100%;
border-collapse: collapse;
border-spacing: 0;
font-family: var(--font-family-body, 'IBM Plex Sans', sans-serif);
font-size: var(--font-size-body1, 14px);
background-color: var(--color-background-paper, #fff);
/* Size variants */
&.small {
.cell {
padding: 6px 12px;
font-size: var(--font-size-body2, 13px);
}
}
&.medium {
.cell {
padding: 12px 16px;
}
}
&.large {
.cell {
padding: 16px 20px;
font-size: var(--font-size-body1, 14px);
}
}
/* Sticky header */
&.stickyHeader {
.head {
position: sticky;
top: 0;
z-index: 2;
background-color: var(--color-background-paper, #fff);
}
}
}
/* ===== Table Head ===== */
.head {
background-color: var(--color-background-default, #fafafa);
.cell {
font-weight: 600;
color: var(--color-text-primary, rgba(0, 0, 0, 0.87));
border-bottom: 2px solid var(--color-divider, rgba(0, 0, 0, 0.12));
}
}
/* ===== Table Body ===== */
.body {
.row:last-child .cell {
border-bottom: none;
}
}
/* ===== Table Footer ===== */
.footer {
background-color: var(--color-background-default, #fafafa);
.cell {
font-weight: 500;
border-top: 2px solid var(--color-divider, rgba(0, 0, 0, 0.12));
border-bottom: none;
}
}
/* ===== Table Row ===== */
.row {
transition: background-color 150ms ease-in-out;
&.hover:hover {
background-color: var(--color-action-hover, rgba(0, 0, 0, 0.04));
}
&.selected {
background-color: var(--color-primary-light, rgba(25, 118, 210, 0.08));
&:hover {
background-color: var(--color-primary-light, rgba(25, 118, 210, 0.12));
}
}
&.clickable {
cursor: pointer;
}
}
/* Striped rows */
.striped .body .row:nth-child(odd) {
background-color: var(--color-action-hover, rgba(0, 0, 0, 0.02));
&:hover {
background-color: var(--color-action-hover, rgba(0, 0, 0, 0.06));
}
}
/* ===== Table Cell ===== */
.cell {
padding: 12px 16px;
text-align: left;
border-bottom: 1px solid var(--color-divider, rgba(0, 0, 0, 0.12));
vertical-align: middle;
color: var(--color-text-primary, rgba(0, 0, 0, 0.87));
/* Alignment */
&.alignLeft {
text-align: left;
}
&.alignCenter {
text-align: center;
}
&.alignRight {
text-align: right;
}
&.alignJustify {
text-align: justify;
}
/* Padding variants */
&.paddingCheckbox {
width: 48px;
padding: 0 4px;
}
&.paddingNone {
padding: 0;
}
}
/* ===== Sort Label ===== */
.sortLabel {
display: inline-flex;
align-items: center;
gap: 4px;
cursor: pointer;
user-select: none;
font-weight: inherit;
color: inherit;
transition: color 150ms ease-in-out;
&:hover {
color: var(--color-text-primary, rgba(0, 0, 0, 0.87));
}
&:focus {
outline: none;
color: var(--color-primary-main, #1976d2);
}
&.active {
color: var(--color-text-primary, rgba(0, 0, 0, 0.87));
}
&:not(.active) {
color: var(--color-text-secondary, rgba(0, 0, 0, 0.6));
.sortIcon {
opacity: 0;
transition: opacity 150ms ease-in-out;
}
&:hover .sortIcon {
opacity: 0.5;
}
}
}
.sortIcon {
display: inline-flex;
align-items: center;
font-size: 16px;
opacity: 1;
transition: transform 200ms ease-in-out, opacity 150ms ease-in-out;
&.asc {
transform: rotate(0deg);
}
&.desc {
transform: rotate(180deg);
}
}
/* ===== Pagination ===== */
.pagination {
display: flex;
align-items: center;
justify-content: flex-end;
padding: 12px 16px;
gap: 16px;
font-size: var(--font-size-body2, 13px);
color: var(--color-text-secondary, rgba(0, 0, 0, 0.6));
border-top: 1px solid var(--color-divider, rgba(0, 0, 0, 0.12));
&.toolbar {
min-height: 52px;
padding: 8px 2px;
}
}
.paginationLabel {
flex-shrink: 0;
}
.paginationSelect {
appearance: none;
padding: 6px 24px 6px 8px;
border: 1px solid var(--color-divider, rgba(0, 0, 0, 0.23));
border-radius: 4px;
font-size: inherit;
font-family: inherit;
background-color: var(--color-background-paper, #fff);
cursor: pointer;
transition: border-color 150ms ease-in-out;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23666' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 4px center;
background-size: 16px;
&:hover {
border-color: var(--color-text-primary, rgba(0, 0, 0, 0.87));
}
&:focus {
outline: none;
border-color: var(--color-primary-main, #1976d2);
}
}
.paginationSpacer {
flex: 1 1 100%;
}
.paginationDisplayed {
flex-shrink: 0;
margin-left: 8px;
}
.paginationActions {
display: flex;
align-items: center;
gap: 4px;
flex-shrink: 0;
}
.paginationBtn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
padding: 0;
border: none;
border-radius: 50%;
background-color: transparent;
color: var(--color-text-secondary, rgba(0, 0, 0, 0.6));
cursor: pointer;
transition: background-color 150ms ease-in-out, color 150ms ease-in-out;
&:hover:not(:disabled) {
background-color: var(--color-action-hover, rgba(0, 0, 0, 0.04));
color: var(--color-text-primary, rgba(0, 0, 0, 0.87));
}
&:disabled {
opacity: 0.4;
cursor: not-allowed;
}
svg {
width: 20px;
height: 20px;
}
}
/* ===== Bordered variant ===== */
.bordered {
border: 1px solid var(--color-divider, rgba(0, 0, 0, 0.12));
border-radius: var(--shape-border-radius, 4px);
.cell {
border: 1px solid var(--color-divider, rgba(0, 0, 0, 0.12));
}
}
/* ===== Compact variant ===== */
.compact {
.cell {
padding: 4px 8px;
font-size: var(--font-size-caption, 12px);
}
}
/* ===== Dark mode support ===== */
@media (prefers-color-scheme: dark) {
.table {
background-color: var(--color-background-paper-dark, #1e1e1e);
}
.head {
background-color: var(--color-background-default-dark, #121212);
}
.cell {
color: var(--color-text-primary-dark, #fff);
border-color: var(--color-divider-dark, rgba(255, 255, 255, 0.12));
}
.row.hover:hover {
background-color: var(--color-action-hover-dark, rgba(255, 255, 255, 0.08));
}
.paginationSelect {
background-color: var(--color-background-paper-dark, #1e1e1e);
border-color: var(--color-divider-dark, rgba(255, 255, 255, 0.23));
color: var(--color-text-primary-dark, #fff);
}
}

View File

@@ -1,9 +1,45 @@
-- Stream Cast initialization
--- Stream Cast package entry point
--- Provides streaming and broadcasting components
---@class StreamCast
---@field name string Package name
---@field version string Package version
---@field player StreamPlayerModule Player module
---@field scenes StreamScenesModule Scenes module
---@field schedule StreamScheduleModule Schedule module
---@field init fun(): { name: string, version: string, loaded: boolean }
---@class StreamPlayerModule
---@field PLAYING PlayerState
---@field PAUSED PlayerState
---@field BUFFERING PlayerState
---@field OFFLINE PlayerState
---@field render fun(stream: Stream): VideoPlayerComponent
---@field render_controls fun(state: PlayerState): PlayerControlsComponent
---@field render_status fun(state: PlayerState, viewers: number): StatusBarComponent
---@class StreamScenesModule
---@field render_scene fun(scene: Scene): ScenePreviewComponent
---@field render_list fun(scenes: Scene[]): SceneListComponent
---@field switch fun(scene_id: string): SwitchSceneAction
---@field create fun(name: string, sources?: table[]): CreateSceneAction
---@class StreamScheduleModule
---@field render_item fun(stream: ScheduledStream): ScheduleItemComponent
---@field render_list fun(streams: ScheduledStream[]): ScheduleListComponent
---@field create fun(data: { title: string, start_time: string, duration?: number }): ScheduleStreamAction
---@field cancel fun(stream_id: string): CancelStreamAction
local M = {}
M.name = "stream_cast"
M.version = "1.0.0"
M.player = require("player")
M.scenes = require("scenes")
M.schedule = require("schedule")
--- Initialize the stream cast module
---@return { name: string, version: string, loaded: boolean }
function M.init()
return {
name = M.name,

View File

@@ -1,48 +1,24 @@
-- Stream player controls
--- Stream player facade
--- Re-exports single-function modules for backward compatibility
---@type PlayerState
local PLAYING = "playing"
---@type PlayerState
local PAUSED = "paused"
---@type PlayerState
local BUFFERING = "buffering"
---@type PlayerState
local OFFLINE = "offline"
local M = {}
M.PLAYING = "playing"
M.PAUSED = "paused"
M.BUFFERING = "buffering"
M.OFFLINE = "offline"
M.PLAYING = PLAYING
M.PAUSED = PAUSED
M.BUFFERING = BUFFERING
M.OFFLINE = OFFLINE
function M.render(stream)
return {
type = "video_player",
props = {
src = stream.url,
poster = stream.thumbnail,
autoplay = true,
controls = true
}
}
end
function M.render_controls(state)
return {
type = "player_controls",
children = {
{ type = "button", props = { icon = state == M.PLAYING and "pause" or "play" } },
{ type = "volume_slider", props = { min = 0, max = 100 } },
{ type = "button", props = { icon = "maximize" } }
}
}
end
function M.render_status(state, viewers)
local colors = {
playing = "success",
paused = "warning",
buffering = "info",
offline = "error"
}
return {
type = "status_bar",
children = {
{ type = "badge", props = { label = state, color = colors[state] } },
{ type = "text", props = { text = viewers .. " viewers" } }
}
}
end
M.render = require("render_player")
M.render_controls = require("render_player_controls")
M.render_status = require("render_status")
return M

View File

@@ -1,4 +1,8 @@
-- Stream player controls
--- Stream player controls configuration
---@param show_play? boolean Show play button (default true)
---@param show_volume? boolean Show volume slider (default true)
---@param show_fullscreen? boolean Show fullscreen button (default true)
---@return PlayerControlsConfig Controls configuration
local function controls(show_play, show_volume, show_fullscreen)
return {
type = "player_controls",

View File

@@ -1,4 +1,9 @@
-- Stream player module
--- Stream player sub-module
--- Provides controls and overlay components
---@class StreamPlayerSubModule
---@field controls fun(show_play?: boolean, show_volume?: boolean, show_fullscreen?: boolean): PlayerControlsConfig
---@field overlay fun(title: string, subtitle: string): PlayerOverlayComponent
local player = {
controls = require("player.controls"),
overlay = require("player.overlay")

View File

@@ -1,4 +1,7 @@
-- Stream player overlay
--- Stream player overlay component
---@param title string Overlay title
---@param subtitle string Overlay subtitle
---@return PlayerOverlayComponent Overlay component
local function overlay(title, subtitle)
return {
type = "player_overlay",

View File

@@ -1,44 +1,11 @@
-- Scene management
--- Scene management facade
--- Re-exports single-function modules for backward compatibility
local M = {}
function M.render_scene(scene)
return {
type = "scene_preview",
props = {
id = scene.id,
name = scene.name,
thumbnail = scene.thumbnail,
active = scene.active or false
}
}
end
function M.render_list(scenes)
local items = {}
for _, scene in ipairs(scenes) do
table.insert(items, M.render_scene(scene))
end
return {
type = "scene_list",
children = items
}
end
function M.switch(scene_id)
return {
action = "switch_scene",
scene_id = scene_id
}
end
function M.create(name, sources)
return {
action = "create_scene",
data = {
name = name,
sources = sources or {}
}
}
end
M.render_scene = require("render_scene")
M.render_list = require("render_scene_list")
M.switch = require("switch_scene")
M.create = require("create_scene")
return M

View File

@@ -1,5 +1,19 @@
-- Stream scene camera component
local function camera(id, label, source)
--- Stream scene camera component
---@param id string Camera ID
---@param label string Camera label
---@param source string Camera source
---@return SceneCameraComponent Camera component
---@class SceneCameraComponent
---@field type "scene_camera" Component type
---@field id string Camera ID
---@field label string Camera label
---@field source string Source identifier
---Create a camera scene source
---@param id string Camera ID
---@param label string Camera label
---@param source string Source identifier
---@return SceneCameraComponentlocal function camera(id, label, source)
return {
type = "scene_camera",
id = id,

View File

@@ -1,4 +1,10 @@
-- Stream scenes module
--- Stream scenes sub-module
--- Provides camera, screen, and layout components
---@class StreamScenesSubModule
---@field camera fun(id: string, label: string, source: string): SceneCameraComponent
---@field screen fun(id: string, label: string): SceneScreenComponent
---@field layout fun(type: "single"|"pip"|"split", sources?: table[]): SceneLayoutComponent
local scenes = {
camera = require("scenes.camera"),
screen = require("scenes.screen"),

View File

@@ -1,8 +1,11 @@
-- Stream scene layout component
--- Stream scene layout component
---@param type "single"|"pip"|"split" Layout type
---@param sources? table[] Layout sources
---@return SceneLayoutComponent Layout component
local function layout(type, sources)
return {
type = "scene_layout",
layoutType = type, -- "single", "pip", "split"
layoutType = type,
sources = sources or {}
}
end

View File

@@ -1,4 +1,7 @@
-- Stream scene screen share component
--- Stream scene screen share component
---@param id string Screen ID
---@param label string Screen label
---@return SceneScreenComponent Screen component
local function screen(id, label)
return {
type = "scene_screen",

View File

@@ -1,45 +1,11 @@
-- Stream scheduling
--- Stream scheduling facade
--- Re-exports single-function modules for backward compatibility
local M = {}
function M.render_item(stream)
return {
type = "schedule_item",
props = {
title = stream.title,
start_time = stream.start_time,
duration = stream.duration,
thumbnail = stream.thumbnail
}
}
end
function M.render_list(streams)
local items = {}
for _, stream in ipairs(streams) do
table.insert(items, M.render_item(stream))
end
return {
type = "schedule_list",
children = items
}
end
function M.create(data)
return {
action = "schedule_stream",
data = {
title = data.title,
start_time = data.start_time,
duration = data.duration or 60
}
}
end
function M.cancel(stream_id)
return {
action = "cancel_stream",
stream_id = stream_id
}
end
M.render_item = require("render_schedule_item")
M.render_list = require("render_schedule_list")
M.create = require("schedule_stream")
M.cancel = require("cancel_stream")
return M