From 7e26e547ea0b98c6366d6cf1b3800b5f253767a7 Mon Sep 17 00:00:00 2001 From: Christian Pillsbury Date: Fri, 12 Sep 2025 10:30:18 -0700 Subject: [PATCH] feat(media-store): add comprehensive time formatting utilities MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add time formatting functions ported from Media Chrome: - formatTime() - formats seconds to HH:MM:SS or MM:SS - formatAsTimePhrase() - creates accessibility descriptions - isValidDuration() - validates duration values - formatDuration() - combines formatting with fallback handling These utilities provide the foundation for time-based display components across HTML, React, and React Native platforms. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- packages/core/media-store/src/index.ts | 4 +- packages/core/media-store/src/utils/time.ts | 160 ++++++++++++++++++++ 2 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 packages/core/media-store/src/utils/time.ts diff --git a/packages/core/media-store/src/index.ts b/packages/core/media-store/src/index.ts index 474f72ea..2c95b121 100644 --- a/packages/core/media-store/src/index.ts +++ b/packages/core/media-store/src/index.ts @@ -7,4 +7,6 @@ export * from './component-state-definitions/play-button'; export * from './component-state-definitions/mute-button'; export * from './component-state-definitions/volume-range'; export * from './component-state-definitions/time-range'; -export * from './component-state-definitions/fullscreen-button'; \ No newline at end of file +export * from './component-state-definitions/fullscreen-button'; +export * from './component-state-definitions/duration-display'; +export * from './utils/time'; \ No newline at end of file diff --git a/packages/core/media-store/src/utils/time.ts b/packages/core/media-store/src/utils/time.ts new file mode 100644 index 00000000..086d01ff --- /dev/null +++ b/packages/core/media-store/src/utils/time.ts @@ -0,0 +1,160 @@ +/** + * @fileoverview Time formatting utilities for media components + * + * This module provides utilities for formatting time values in various formats + * suitable for display in media player UI components. Based on patterns from + * Media Chrome but adapted for VJS-10 architecture. + */ + +/** + * Checks if a value is a valid number (not NaN, null, undefined, or Infinity) + */ +function isValidNumber(value: unknown): value is number { + return typeof value === 'number' && isFinite(value); +} + +const UnitLabels = [ + { + singular: 'hour', + plural: 'hours', + }, + { + singular: 'minute', + plural: 'minutes', + }, + { + singular: 'second', + plural: 'seconds', + }, +] as const; + +const toTimeUnitPhrase = (timeUnitValue: number, unitIndex: number): string => { + const unitLabel = + timeUnitValue === 1 + ? UnitLabels[unitIndex]?.singular + : UnitLabels[unitIndex]?.plural; + + return `${timeUnitValue} ${unitLabel}`; +}; + +/** + * Converts numeric seconds into a human-readable phrase for accessibility + * @param seconds - A (positive or negative) time, represented as seconds + * @returns The time, represented as a phrase of hours, minutes, and seconds + * + * @example + * formatAsTimePhrase(3661) // "1 hour, 1 minute, 1 second" + * formatAsTimePhrase(90) // "1 minute, 30 seconds" + * formatAsTimePhrase(-30) // "30 seconds remaining" + */ +export function formatAsTimePhrase(seconds: number): string { + if (!isValidNumber(seconds)) return ''; + + const positiveSeconds = Math.abs(seconds); + const negative = positiveSeconds !== seconds; + const secondsDateTime = new Date(0, 0, 0, 0, 0, positiveSeconds, 0); + const timeParts = [ + secondsDateTime.getHours(), + secondsDateTime.getMinutes(), + secondsDateTime.getSeconds(), + ]; + + const timeString = timeParts + // Convert non-0 values to a string of the value plus its unit + .map( + (timeUnitValue, index) => + timeUnitValue && toTimeUnitPhrase(timeUnitValue, index) + ) + // Ignore/exclude any 0 values + .filter((x) => x) + // join into a single comma-separated string phrase + .join(', '); + + // If the time was negative, assume it represents some remaining amount of time/"count down". + const negativeSuffix = negative ? ' remaining' : ''; + + return `${timeString}${negativeSuffix}`; +} + +/** + * Converts a time, in numeric seconds, to a formatted string representation + * of the form [HH:[MM:]]SS, where hours and minutes are optional, either + * based on the value of `seconds` or (optionally) based on the value of `guide`. + * + * @param seconds - The total time you'd like formatted, in seconds + * @param guide - A number in seconds that represents how many units you'd want + * to show. This ensures consistent formatting between e.g. 35s and 4834s. + * @returns A string representation of the time, with expected units + * + * @example + * formatTime(90) // "1:30" + * formatTime(3661) // "1:01:01" + * formatTime(35, 3600) // "0:35" (guided by 1-hour duration) + * formatTime(NaN) // "0:00" + * formatTime(Infinity) // "0:00" + */ +export function formatTime(seconds: number, guide?: number): string { + // Handle negative values + let negative = false; + + if (seconds < 0) { + negative = true; + seconds = 0 - seconds; + } + + seconds = seconds < 0 ? 0 : seconds; + + let s: number | string = Math.floor(seconds % 60); + let m: number | string = Math.floor((seconds / 60) % 60); + let h: number | string = Math.floor(seconds / 3600); + + const gm = guide ? Math.floor((guide / 60) % 60) : 0; + const gh = guide ? Math.floor(guide / 3600) : 0; + + // Handle invalid times + if (isNaN(seconds) || seconds === Infinity) { + // '-' is false for all relational operators (e.g. <, >=) so this setting + // will add the minimum number of fields specified by the guide + h = m = s = '0'; + } + + // Check if we need to show hours + const showHours = (h as number) > 0 || gh > 0; + const hoursString = showHours ? h + ':' : ''; + + // If hours are showing, we may need to add a leading zero. + // Always show at least one digit of minutes. + const minutesString = ((showHours || gm >= 10) && (m as number) < 10 ? '0' + m : m) + ':'; + + // Check if leading zero is needed for seconds + const secondsString = (s as number) < 10 ? '0' + s : s; + + return (negative ? '-' : '') + hoursString + minutesString + secondsString; +} + +/** + * Checks if a duration value should be considered valid for display + * @param duration - The duration value to check + * @returns True if the duration is a valid, displayable number + */ +export function isValidDuration(duration: unknown): duration is number { + return isValidNumber(duration) && duration >= 0; +} + +/** + * Formats a duration with fallback handling for invalid values + * @param duration - The duration to format in seconds + * @param guide - Optional guide duration for consistent formatting + * @param fallback - Fallback text when duration is invalid (default: "--:--") + * @returns Formatted duration string or fallback + */ +export function formatDuration( + duration: unknown, + guide?: number, + fallback: string = '--:--' +): string { + if (!isValidDuration(duration)) { + return fallback; + } + return formatTime(duration, guide); +} \ No newline at end of file