From 5bd0a154dbba01d2a5d11eb1f548fe4baa581675 Mon Sep 17 00:00:00 2001 From: Christian Pillsbury Date: Fri, 12 Sep 2025 11:03:50 -0700 Subject: [PATCH] feat: implement current time display components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../current-time-display.ts | 65 ++++++++++ packages/core/media-store/src/index.ts | 1 + .../components/media-current-time-display.ts | 114 ++++++++++++++++++ packages/html/html/src/index.ts | 1 + .../html/html/src/skins/media-skin-default.ts | 2 + .../src/components/CurrentTimeDisplay.tsx | 62 ++++++++++ packages/react/react/src/index.tsx | 3 +- .../react/src/skins/MediaSkinDefault.tsx | 2 + 8 files changed, 249 insertions(+), 1 deletion(-) create mode 100644 packages/core/media-store/src/component-state-definitions/current-time-display.ts create mode 100644 packages/html/html/src/components/media-current-time-display.ts create mode 100644 packages/react/react/src/components/CurrentTimeDisplay.tsx diff --git a/packages/core/media-store/src/component-state-definitions/current-time-display.ts b/packages/core/media-store/src/component-state-definitions/current-time-display.ts new file mode 100644 index 00000000..f3cb2082 --- /dev/null +++ b/packages/core/media-store/src/component-state-definitions/current-time-display.ts @@ -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): 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; + +/** + * Type helper to extract the request methods type (empty for read-only component) + */ +export type CurrentTimeDisplayRequestMethods = ReturnType; \ No newline at end of file diff --git a/packages/core/media-store/src/index.ts b/packages/core/media-store/src/index.ts index 2c95b121..ec91a5e3 100644 --- a/packages/core/media-store/src/index.ts +++ b/packages/core/media-store/src/index.ts @@ -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'; \ No newline at end of file diff --git a/packages/html/html/src/components/media-current-time-display.ts b/packages/html/html/src/components/media-current-time-display.ts new file mode 100644 index 00000000..99269ae5 --- /dev/null +++ b/packages/html/html/src/components/media-current-time-display.ts @@ -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, + _props: Record = {}, +) { + return /* html */ ` + + `; +} + +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 = {}; + 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; \ No newline at end of file diff --git a/packages/html/html/src/index.ts b/packages/html/html/src/index.ts index 7096366a..4d6ac187 100644 --- a/packages/html/html/src/index.ts +++ b/packages/html/html/src/index.ts @@ -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) */ diff --git a/packages/html/html/src/skins/media-skin-default.ts b/packages/html/html/src/skins/media-skin-default.ts index 33254c15..5802f465 100644 --- a/packages/html/html/src/skins/media-skin-default.ts +++ b/packages/html/html/src/skins/media-skin-default.ts @@ -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() { + diff --git a/packages/react/react/src/components/CurrentTimeDisplay.tsx b/packages/react/react/src/components/CurrentTimeDisplay.tsx new file mode 100644 index 00000000..43331527 --- /dev/null +++ b/packages/react/react/src/components/CurrentTimeDisplay.tsx @@ -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; + +export const useCurrentTimeDisplayProps = ( + props: React.PropsWithChildren<{ [k: string]: any }>, + state: ReturnType, +) => { + const baseProps: Record = { + /** external props spread last to allow for overriding */ + ...props, + }; + + return baseProps; +}; + +export type useCurrentTimeDisplayProps = typeof useCurrentTimeDisplayProps; +type CurrentTimeDisplayProps = ReturnType; + +export const renderCurrentTimeDisplay = ( + props: CurrentTimeDisplayProps, + state: CurrentTimeDisplayState, +) => { + return ( + + {formatDisplayTime(state.currentTime)} + + ); +}; + +export type renderCurrentTimeDisplay = typeof renderCurrentTimeDisplay; + +export const CurrentTimeDisplay = toConnectedComponent( + useCurrentTimeDisplayState, + useCurrentTimeDisplayProps, + renderCurrentTimeDisplay, + 'CurrentTimeDisplay', +); +export default CurrentTimeDisplay; \ No newline at end of file diff --git a/packages/react/react/src/index.tsx b/packages/react/react/src/index.tsx index 61025275..ac128710 100644 --- a/packages/react/react/src/index.tsx +++ b/packages/react/react/src/index.tsx @@ -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'; \ No newline at end of file +export { DurationDisplay } from './components/DurationDisplay'; +export { CurrentTimeDisplay } from './components/CurrentTimeDisplay'; \ No newline at end of file diff --git a/packages/react/react/src/skins/MediaSkinDefault.tsx b/packages/react/react/src/skins/MediaSkinDefault.tsx index c354efea..0973b89f 100644 --- a/packages/react/react/src/skins/MediaSkinDefault.tsx +++ b/packages/react/react/src/skins/MediaSkinDefault.tsx @@ -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 }> = ({ + {/* @ts-ignore */}