feat: implement current time display components

Adds current time display functionality across HTML and React platforms:
- Core state definition for current time with duration support
- HTML component with shadow DOM and span rendering
- React component following hook-style architecture
- Integration into both HTML and React default skins
- Proper exports and component registration

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Christian Pillsbury
2025-09-12 15:59:15 -07:00
committed by Christian Pillsbury
co-authored by Claude
parent 6db4c3c4b8
commit 5bd0a154db
8 changed files with 249 additions and 1 deletions
@@ -0,0 +1,65 @@
/**
* @fileoverview Current time display component state definition
*
* This module provides the component state definition for current time display
* components across HTML, React, and React Native platforms. The current time
* display is a read-only component that shows the current playback time of media.
*/
/**
* State interface for current time display components
*/
export interface CurrentTimeDisplayState {
/** The current time value in seconds */
currentTime: number | undefined;
/** The total duration in seconds (for future functionality) */
duration: number | undefined;
}
/**
* Current time display component state definition following VJS-10 patterns.
* This provides a read-only display component that shows the current playback time.
*/
export const currentTimeDisplayStateDefinition = {
/**
* Keys from the media store that this component depends on
*/
keys: ['currentTime', 'duration'] as const,
/**
* Transform raw media store state into current time display component state
* @param rawState - Raw state from media store
* @returns Transformed state for current time display component
*/
stateTransform: (rawState: Record<string, any>): CurrentTimeDisplayState => {
const { currentTime, duration } = rawState;
return {
currentTime,
duration,
};
},
/**
* Current time display is read-only, so no request methods are needed
* @param _dispatch - Dispatch function (unused)
* @returns Empty object (no request methods)
*/
createRequestMethods: (_dispatch: (action: { type: string; detail?: any }) => void) => ({}),
} as const;
/**
* Type helper to extract the state type from the current time display state definition
*/
export type CurrentTimeDisplayStateDefinition = typeof currentTimeDisplayStateDefinition;
/**
* Type helper to extract the transformed state type
*/
export type CurrentTimeDisplayComponentState = ReturnType<typeof currentTimeDisplayStateDefinition.stateTransform>;
/**
* Type helper to extract the request methods type (empty for read-only component)
*/
export type CurrentTimeDisplayRequestMethods = ReturnType<typeof currentTimeDisplayStateDefinition.createRequestMethods>;
+1
View File
@@ -9,4 +9,5 @@ export * from './component-state-definitions/volume-range';
export * from './component-state-definitions/time-range';
export * from './component-state-definitions/fullscreen-button';
export * from './component-state-definitions/duration-display';
export * from './component-state-definitions/current-time-display';
export * from './utils/time';
@@ -0,0 +1,114 @@
import {
toConnectedHTMLComponent,
StateHook,
PropsHook,
} from '../utils/component-factory';
import { currentTimeDisplayStateDefinition, formatDisplayTime } from '@vjs-10/media-store';
import { namedNodeMapToObject } from '../utils/element-utils.js';
export function getTemplateHTML(
this: typeof CurrentTimeDisplayBase,
_attrs: Record<string, string>,
_props: Record<string, any> = {},
) {
return /* html */ `
<span></span>
`;
}
export class CurrentTimeDisplayBase extends HTMLElement {
static shadowRootOptions = {
mode: 'open' as ShadowRootMode,
};
static getTemplateHTML = getTemplateHTML;
_state:
| {
currentTime: number | undefined;
duration: number | undefined;
}
| undefined;
constructor() {
super();
if (!this.shadowRoot) {
this.attachShadow(
(this.constructor as typeof CurrentTimeDisplayBase).shadowRootOptions,
);
const attrs = namedNodeMapToObject(this.attributes);
const html = (
this.constructor as typeof CurrentTimeDisplayBase
).getTemplateHTML(attrs);
const shadowRoot = this.shadowRoot as unknown as ShadowRoot;
shadowRoot.setHTMLUnsafe
? shadowRoot.setHTMLUnsafe(html)
: (shadowRoot.innerHTML = html);
}
}
get currentTime() {
return this._state?.currentTime;
}
get duration() {
return this._state?.duration;
}
_update(_props: any, state: any) {
this._state = state;
// Update the span content with formatted current time
const spanElement = this.shadowRoot?.querySelector('span') as HTMLElement;
if (spanElement) {
spanElement.textContent = formatDisplayTime(state.currentTime);
}
}
}
/**
* CurrentTimeDisplay state hook - equivalent to React's useCurrentTimeDisplayState
* Handles media store state subscription and transformation
*/
export const useCurrentTimeDisplayState: StateHook<{
currentTime: number | undefined;
duration: number | undefined;
}> = {
keys: [...currentTimeDisplayStateDefinition.keys],
transform: (rawState, _mediaStore) => ({
...currentTimeDisplayStateDefinition.stateTransform(rawState),
// Current time display is read-only, so no request methods needed
}),
};
/**
* CurrentTimeDisplay props hook - equivalent to React's useCurrentTimeDisplayProps
* Handles element attributes and properties based on state
*/
export const useCurrentTimeDisplayProps: PropsHook<{
currentTime: number | undefined;
duration: number | undefined;
}> = (_state, _element) => {
const baseProps: Record<string, any> = {};
return baseProps;
};
/**
* Connected CurrentTimeDisplay component using hook-style architecture
* Equivalent to React's CurrentTimeDisplay = toConnectedComponent(...)
*/
export const CurrentTimeDisplay = toConnectedHTMLComponent(
CurrentTimeDisplayBase,
useCurrentTimeDisplayState,
useCurrentTimeDisplayProps,
'CurrentTimeDisplay',
);
// Register the custom element
if (!globalThis.customElements.get('media-current-time-display')) {
// @ts-ignore - Custom element constructor compatibility
globalThis.customElements.define('media-current-time-display', CurrentTimeDisplay);
}
export default CurrentTimeDisplay;
+1
View File
@@ -7,6 +7,7 @@ export { MuteButton } from './components/media-mute-button.js';
export { VolumeRange } from './components/media-volume-range.js';
export { FullscreenButton } from './components/media-fullscreen-button.js';
export { DurationDisplay } from './components/media-duration-display.js';
export { CurrentTimeDisplay } from './components/media-current-time-display.js';
export function defineVjsPlayer() {
/** @TODO - Reimplement me (at least as a POC) (CJP) */
@@ -7,6 +7,7 @@ import '../components/media-volume-range';
import '../components/media-time-range';
import '../components/media-fullscreen-button';
import '../components/media-duration-display';
import '../components/media-current-time-display';
import '@vjs-10/html-icons';
export function getTemplateHTML() {
@@ -99,6 +100,7 @@ export function getTemplateHTML() {
<media-play-icon class="icon play-icon"></media-play-icon>
<media-pause-icon class="icon pause-icon"></media-pause-icon>
</media-play-button>
<media-current-time-display></media-current-time-display>
<media-time-range></media-time-range>
<media-duration-display></media-duration-display>
<media-mute-button class="button">
@@ -0,0 +1,62 @@
import {
shallowEqual,
useMediaSelector,
useMediaStore,
} from '@vjs-10/react-media-store';
import * as React from 'react';
import { toConnectedComponent } from '../utils/component-factory';
import { currentTimeDisplayStateDefinition, formatDisplayTime } from '@vjs-10/media-store';
export const useCurrentTimeDisplayState = (_props: any) => {
const mediaStore = useMediaStore();
/** @TODO Fix type issues with hooks (CJP) */
const mediaState = useMediaSelector(
currentTimeDisplayStateDefinition.stateTransform,
shallowEqual,
);
// Current time display is read-only, no request methods needed
return {
currentTime: mediaState.currentTime,
duration: mediaState.duration,
} as const;
};
export type useCurrentTimeDisplayState = typeof useCurrentTimeDisplayState;
export type CurrentTimeDisplayState = ReturnType<useCurrentTimeDisplayState>;
export const useCurrentTimeDisplayProps = (
props: React.PropsWithChildren<{ [k: string]: any }>,
state: ReturnType<typeof useCurrentTimeDisplayState>,
) => {
const baseProps: Record<string, any> = {
/** external props spread last to allow for overriding */
...props,
};
return baseProps;
};
export type useCurrentTimeDisplayProps = typeof useCurrentTimeDisplayProps;
type CurrentTimeDisplayProps = ReturnType<useCurrentTimeDisplayProps>;
export const renderCurrentTimeDisplay = (
props: CurrentTimeDisplayProps,
state: CurrentTimeDisplayState,
) => {
return (
<span {...props}>
{formatDisplayTime(state.currentTime)}
</span>
);
};
export type renderCurrentTimeDisplay = typeof renderCurrentTimeDisplay;
export const CurrentTimeDisplay = toConnectedComponent(
useCurrentTimeDisplayState,
useCurrentTimeDisplayProps,
renderCurrentTimeDisplay,
'CurrentTimeDisplay',
);
export default CurrentTimeDisplay;
+2 -1
View File
@@ -10,4 +10,5 @@ export { PlayButton } from './components/PlayButton';
export { MuteButton } from './components/MuteButton';
export { VolumeRange } from './components/VolumeRange';
export { FullscreenButton } from './components/FullscreenButton';
export { DurationDisplay } from './components/DurationDisplay';
export { DurationDisplay } from './components/DurationDisplay';
export { CurrentTimeDisplay } from './components/CurrentTimeDisplay';
@@ -6,6 +6,7 @@ import { VolumeRange } from '../components/VolumeRange';
import { TimeRange } from '../components/TimeRange';
import { FullscreenButton } from '../components/FullscreenButton';
import { DurationDisplay } from '../components/DurationDisplay';
import { CurrentTimeDisplay } from '../components/CurrentTimeDisplay';
import { MediaContainer } from '../components/MediaContainer';
import {
VolumeHighIcon,
@@ -30,6 +31,7 @@ export const MediaSkinDefault: React.FC<{ children: React.ReactNode }> = ({
<PlayIcon className={styles.PlayIcon}></PlayIcon>
<PauseIcon className={styles.PauseIcon}></PauseIcon>
</PlayButton>
<CurrentTimeDisplay />
<TimeRange className={styles.TimeRange} />
<DurationDisplay />
{/* @ts-ignore */}