mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
done
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @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.
|
||||
@@ -12,7 +12,7 @@
|
||||
export interface CurrentTimeDisplayState {
|
||||
/** The current time value in seconds */
|
||||
currentTime: number | undefined;
|
||||
|
||||
|
||||
/** The total duration in seconds (for future functionality) */
|
||||
duration: number | undefined;
|
||||
}
|
||||
@@ -34,7 +34,7 @@ export const currentTimeDisplayStateDefinition = {
|
||||
*/
|
||||
stateTransform: (rawState: Record<string, any>): CurrentTimeDisplayState => {
|
||||
const { currentTime, duration } = rawState;
|
||||
|
||||
|
||||
return {
|
||||
currentTime,
|
||||
duration,
|
||||
@@ -62,4 +62,6 @@ export type CurrentTimeDisplayComponentState = ReturnType<typeof currentTimeDisp
|
||||
/**
|
||||
* Type helper to extract the request methods type (empty for read-only component)
|
||||
*/
|
||||
export type CurrentTimeDisplayRequestMethods = ReturnType<typeof currentTimeDisplayStateDefinition.createRequestMethods>;
|
||||
export type CurrentTimeDisplayRequestMethods = ReturnType<
|
||||
typeof currentTimeDisplayStateDefinition.createRequestMethods
|
||||
>;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @fileoverview Duration display component state definition
|
||||
*
|
||||
*
|
||||
* This module provides the component state definition for duration display
|
||||
* components across HTML, React, and React Native platforms. The duration
|
||||
* display is a read-only component that shows the total duration of media.
|
||||
@@ -31,7 +31,7 @@ export const durationDisplayStateDefinition = {
|
||||
*/
|
||||
stateTransform: (rawState: Record<string, any>): DurationDisplayState => {
|
||||
const { duration } = rawState;
|
||||
|
||||
|
||||
return {
|
||||
duration,
|
||||
};
|
||||
@@ -58,4 +58,4 @@ export type DurationDisplayComponentState = ReturnType<typeof durationDisplaySta
|
||||
/**
|
||||
* Type helper to extract the request methods type (empty for read-only component)
|
||||
*/
|
||||
export type DurationDisplayRequestMethods = ReturnType<typeof durationDisplayStateDefinition.createRequestMethods>;
|
||||
export type DurationDisplayRequestMethods = ReturnType<typeof durationDisplayStateDefinition.createRequestMethods>;
|
||||
|
||||
@@ -10,27 +10,22 @@ export interface FullscreenButtonMethods {
|
||||
export interface FullscreenButtonStateDefinition {
|
||||
keys: string[];
|
||||
stateTransform: (rawState: any) => FullscreenButtonState;
|
||||
createRequestMethods: (
|
||||
dispatch: (action: { type: string; detail?: any }) => void,
|
||||
) => FullscreenButtonMethods;
|
||||
createRequestMethods: (dispatch: (action: { type: string; detail?: any }) => void) => FullscreenButtonMethods;
|
||||
}
|
||||
|
||||
/**
|
||||
* FullscreenButton state definition
|
||||
* Defines the core state logic that can be shared between implementations
|
||||
*/
|
||||
export const fullscreenButtonStateDefinition: FullscreenButtonStateDefinition =
|
||||
{
|
||||
keys: ['fullscreen'],
|
||||
export const fullscreenButtonStateDefinition: FullscreenButtonStateDefinition = {
|
||||
keys: ['fullscreen'],
|
||||
|
||||
stateTransform: (rawState: any): FullscreenButtonState => ({
|
||||
fullscreen: rawState.fullscreen ?? false,
|
||||
}),
|
||||
stateTransform: (rawState: any): FullscreenButtonState => ({
|
||||
fullscreen: rawState.fullscreen ?? false,
|
||||
}),
|
||||
|
||||
createRequestMethods: (dispatch): FullscreenButtonMethods => ({
|
||||
requestEnterFullscreen: () =>
|
||||
dispatch({ type: 'fullscreenrequest', detail: true }),
|
||||
requestExitFullscreen: () =>
|
||||
dispatch({ type: 'fullscreenrequest', detail: false }),
|
||||
}),
|
||||
};
|
||||
createRequestMethods: (dispatch): FullscreenButtonMethods => ({
|
||||
requestEnterFullscreen: () => dispatch({ type: 'fullscreenrequest', detail: true }),
|
||||
requestExitFullscreen: () => dispatch({ type: 'fullscreenrequest', detail: false }),
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -11,9 +11,7 @@ export interface MuteButtonMethods {
|
||||
export interface MuteButtonStateDefinition {
|
||||
keys: string[];
|
||||
stateTransform: (rawState: any) => MuteButtonState;
|
||||
createRequestMethods: (
|
||||
dispatch: (action: { type: string }) => void,
|
||||
) => MuteButtonMethods;
|
||||
createRequestMethods: (dispatch: (action: { type: string }) => void) => MuteButtonMethods;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -32,4 +30,4 @@ export const muteButtonStateDefinition: MuteButtonStateDefinition = {
|
||||
requestMute: () => dispatch({ type: 'muterequest' }),
|
||||
requestUnmute: () => dispatch({ type: 'unmuterequest' }),
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -10,9 +10,7 @@ export interface PlayButtonMethods {
|
||||
export interface PlayButtonStateDefinition {
|
||||
keys: string[];
|
||||
stateTransform: (rawState: any) => PlayButtonState;
|
||||
createRequestMethods: (
|
||||
dispatch: (action: { type: string }) => void,
|
||||
) => PlayButtonMethods;
|
||||
createRequestMethods: (dispatch: (action: { type: string }) => void) => PlayButtonMethods;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -30,4 +28,4 @@ export const playButtonStateDefinition: PlayButtonStateDefinition = {
|
||||
requestPlay: () => dispatch({ type: 'playrequest' }),
|
||||
requestPause: () => dispatch({ type: 'pauserequest' }),
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -10,9 +10,7 @@ export interface TimeRangeMethods {
|
||||
export interface TimeRangeStateDefinition {
|
||||
keys: string[];
|
||||
stateTransform: (rawState: any) => TimeRangeState;
|
||||
createRequestMethods: (
|
||||
dispatch: (action: { type: string; detail?: any }) => void,
|
||||
) => TimeRangeMethods;
|
||||
createRequestMethods: (dispatch: (action: { type: string; detail?: any }) => void) => TimeRangeMethods;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -30,4 +28,4 @@ export const timeRangeStateDefinition: TimeRangeStateDefinition = {
|
||||
dispatch({ type: 'seekrequest', detail: time });
|
||||
},
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -11,9 +11,7 @@ export interface VolumeRangeMethods {
|
||||
export interface VolumeRangeStateDefinition {
|
||||
keys: string[];
|
||||
stateTransform: (rawState: any) => VolumeRangeState;
|
||||
createRequestMethods: (
|
||||
dispatch: (action: { type: string; detail?: any }) => void,
|
||||
) => VolumeRangeMethods;
|
||||
createRequestMethods: (dispatch: (action: { type: string; detail?: any }) => void) => VolumeRangeMethods;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,16 +11,13 @@ export type EventOrAction<D = undefined> = {
|
||||
target?: EventTarget;
|
||||
};
|
||||
|
||||
export type FacadeGetter<T, D = T> = (
|
||||
stateOwners: StateOwners,
|
||||
event?: EventOrAction<D>,
|
||||
) => T;
|
||||
export type FacadeGetter<T, D = T> = (stateOwners: StateOwners, event?: EventOrAction<D>) => T;
|
||||
|
||||
export type FacadeSetter<T> = (value: T, stateOwners: StateOwners) => void;
|
||||
|
||||
export type StateOwnerUpdateHandler<T> = (
|
||||
handler: (value?: T) => void,
|
||||
stateOwners: StateOwners,
|
||||
stateOwners: StateOwners
|
||||
) => (() => void) | void;
|
||||
|
||||
export type ReadonlyFacadeProp<T, D = T> = {
|
||||
@@ -32,9 +29,7 @@ export type FacadeProp<T, S = T, D = T> = ReadonlyFacadeProp<T, D> & {
|
||||
set: FacadeSetter<S>;
|
||||
/** @TODO We probably need to refactor this for more complex cases where we can't simply translate to a setter */
|
||||
actions: {
|
||||
[k: string]: (
|
||||
val: Pick<CustomEvent<any>, 'type' | 'detail'>,
|
||||
) => ReturnType<FacadeGetter<T, D>>;
|
||||
[k: string]: (val: Pick<CustomEvent<any>, 'type' | 'detail'>) => ReturnType<FacadeGetter<T, D>>;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -67,14 +62,14 @@ export function createMediaStore({
|
||||
const hasChanges = Object.entries(nextStateOwners).some(
|
||||
([key, value]) => stateOwners[key as keyof StateOwners] !== value
|
||||
);
|
||||
|
||||
|
||||
if (!hasChanges) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clean up existing handlers
|
||||
Object.entries(stateUpdateHandlerCleanups).forEach(([stateName, cleanups]) => {
|
||||
cleanups.forEach(cleanup => cleanup?.());
|
||||
cleanups.forEach((cleanup) => cleanup?.());
|
||||
stateUpdateHandlerCleanups[stateName] = [];
|
||||
});
|
||||
|
||||
@@ -84,7 +79,7 @@ export function createMediaStore({
|
||||
// Set up new handlers
|
||||
Object.entries(stateMediator).forEach(([stateName, stateObject]) => {
|
||||
const { get, stateOwnersUpdateHandlers = [] } = stateObject;
|
||||
|
||||
|
||||
if (!stateUpdateHandlerCleanups[stateName]) {
|
||||
stateUpdateHandlerCleanups[stateName] = [];
|
||||
}
|
||||
@@ -96,7 +91,7 @@ export function createMediaStore({
|
||||
};
|
||||
|
||||
// Execute each stateOwnersUpdateHandler
|
||||
stateOwnersUpdateHandlers.forEach(setupHandler => {
|
||||
stateOwnersUpdateHandlers.forEach((setupHandler) => {
|
||||
const cleanup = setupHandler(updateHandler, stateOwners);
|
||||
if (typeof cleanup === 'function') {
|
||||
stateUpdateHandlerCleanups[stateName]?.push(cleanup);
|
||||
@@ -115,10 +110,7 @@ export function createMediaStore({
|
||||
updateStateOwners({ container: detail });
|
||||
} else {
|
||||
for (const stateObject of Object.values(stateMediator).filter(
|
||||
(
|
||||
stateMediatorEntry,
|
||||
): stateMediatorEntry is FacadeProp<any, any, any> =>
|
||||
'set' in stateMediatorEntry,
|
||||
(stateMediatorEntry): stateMediatorEntry is FacadeProp<any, any, any> => 'set' in stateMediatorEntry
|
||||
)) {
|
||||
const { set, actions } = stateObject;
|
||||
if (actions[type]) {
|
||||
@@ -135,10 +127,13 @@ export function createMediaStore({
|
||||
},
|
||||
|
||||
getKeys(keys: string[]) {
|
||||
return keys.reduce((acc, k) => {
|
||||
acc[k] = getKey(store, k);
|
||||
return acc;
|
||||
}, {} as { [k: string]: any });
|
||||
return keys.reduce(
|
||||
(acc, k) => {
|
||||
acc[k] = getKey(store, k);
|
||||
return acc;
|
||||
},
|
||||
{} as { [k: string]: any }
|
||||
);
|
||||
},
|
||||
|
||||
subscribeKeys(keys: string[], callback: (state: any) => void) {
|
||||
@@ -152,10 +147,7 @@ export function createMediaStore({
|
||||
};
|
||||
}
|
||||
|
||||
function getInitialState(
|
||||
stateMediator: Partial<StateMediator> & Pick<StateMediator, 'paused'>,
|
||||
stateOwners: any,
|
||||
) {
|
||||
function getInitialState(stateMediator: Partial<StateMediator> & Pick<StateMediator, 'paused'>, stateOwners: any) {
|
||||
let initialState: any = {};
|
||||
for (const [stateName, { get }] of Object.entries(stateMediator)) {
|
||||
initialState[stateName] = get(stateOwners);
|
||||
|
||||
@@ -10,4 +10,4 @@ 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';
|
||||
export * from './utils/time';
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { playable } from './state-mediators/playable';
|
||||
import { audible } from './state-mediators/audible';
|
||||
import { temporal } from './state-mediators/temporal';
|
||||
import { fullscreenable } from './state-mediators/fullscreenable';
|
||||
import { createMediaStore as factory } from './factory';
|
||||
import { audible } from './state-mediators/audible';
|
||||
import { fullscreenable } from './state-mediators/fullscreenable';
|
||||
import { playable } from './state-mediators/playable';
|
||||
import { temporal } from './state-mediators/temporal';
|
||||
|
||||
// Example of default media store with default state mediator definitions. (CJP)
|
||||
// NOTE: We can also change the API to take an array of stateMediators (or either/both) (CJP)
|
||||
const stateMediator = { ...playable, ...audible, ...temporal, ...fullscreenable };
|
||||
type Params = Partial<Parameters<typeof factory>[0]>;
|
||||
export const createMediaStore = (params: Params = {}) =>
|
||||
factory({ stateMediator, ...params });
|
||||
export const createMediaStore = (params: Params = {}) => factory({ stateMediator, ...params });
|
||||
|
||||
@@ -16,12 +16,12 @@ export const audible = {
|
||||
(handler: (value?: boolean) => void, stateOwners: any) => {
|
||||
const { media } = stateOwners;
|
||||
if (!media) return;
|
||||
|
||||
|
||||
const eventHandler = () => handler();
|
||||
media.addEventListener('volumechange', eventHandler);
|
||||
|
||||
|
||||
return () => media.removeEventListener('volumechange', eventHandler);
|
||||
}
|
||||
},
|
||||
],
|
||||
actions: {
|
||||
/** @TODO Refactor me to play more nicely with side effects that don't/can't correlate with set() API or aren't simple 1:1 with getter vs. setter (CJP) */
|
||||
@@ -48,18 +48,16 @@ export const audible = {
|
||||
(handler: (value?: number) => void, stateOwners: any) => {
|
||||
const { media } = stateOwners;
|
||||
if (!media) return;
|
||||
|
||||
|
||||
const eventHandler = () => handler();
|
||||
media.addEventListener('volumechange', eventHandler);
|
||||
|
||||
|
||||
return () => media.removeEventListener('volumechange', eventHandler);
|
||||
}
|
||||
},
|
||||
],
|
||||
actions: {
|
||||
/** @TODO Refactor me to play more nicely with side effects that don't/can't correlate with set() API (CJP) */
|
||||
volumerequest: (
|
||||
{ detail }: Pick<CustomEvent<any>, 'detail'> = { detail: 0 },
|
||||
) => +detail,
|
||||
volumerequest: ({ detail }: Pick<CustomEvent<any>, 'detail'> = { detail: 0 }) => +detail,
|
||||
},
|
||||
},
|
||||
// NOTE: This could be (re)implemented as "derived state" in some manner (e.g. selectors but also other patterns/conventions) if preferred. (CJP)
|
||||
@@ -76,12 +74,12 @@ export const audible = {
|
||||
(handler: (value?: 'high' | 'medium' | 'low' | 'off') => void, stateOwners: any) => {
|
||||
const { media } = stateOwners;
|
||||
if (!media) return;
|
||||
|
||||
|
||||
const eventHandler = () => handler();
|
||||
media.addEventListener('volumechange', eventHandler);
|
||||
|
||||
|
||||
return () => media.removeEventListener('volumechange', eventHandler);
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -46,13 +46,10 @@ export const fullscreenable = {
|
||||
'fullscreenElement' in doc
|
||||
? 'fullscreenElement'
|
||||
: 'webkitFullscreenElement' in doc
|
||||
? 'webkitFullscreenElement'
|
||||
: undefined;
|
||||
? 'webkitFullscreenElement'
|
||||
: undefined;
|
||||
|
||||
if (
|
||||
fullscreenElementKey &&
|
||||
!(fullscreenElementKey in (currentRoot || {}))
|
||||
) {
|
||||
if (fullscreenElementKey && !(fullscreenElementKey in (currentRoot || {}))) {
|
||||
// For older Safari versions, use composed node containment check
|
||||
return containsComposedNode(currentFullscreenElement, container);
|
||||
}
|
||||
@@ -60,8 +57,7 @@ export const fullscreenable = {
|
||||
// Traverse shadow DOM layers looking for our container
|
||||
while (fullscreenElementKey && currentRoot?.[fullscreenElementKey]) {
|
||||
if (currentRoot[fullscreenElementKey] === container) return true;
|
||||
if (currentRoot[fullscreenElementKey]?.contains?.(container))
|
||||
return true;
|
||||
if (currentRoot[fullscreenElementKey]?.contains?.(container)) return true;
|
||||
currentRoot = currentRoot[fullscreenElementKey]?.shadowRoot;
|
||||
}
|
||||
}
|
||||
@@ -113,12 +109,7 @@ export const fullscreenable = {
|
||||
if (!globalThis?.document) return;
|
||||
|
||||
const eventHandler = () => handler();
|
||||
const events = [
|
||||
'fullscreenchange',
|
||||
'webkitfullscreenchange',
|
||||
'mozfullscreenchange',
|
||||
'MSFullscreenChange',
|
||||
];
|
||||
const events = ['fullscreenchange', 'webkitfullscreenchange', 'mozfullscreenchange', 'MSFullscreenChange'];
|
||||
|
||||
events.forEach((event) => {
|
||||
globalThis.document.addEventListener(event, eventHandler);
|
||||
@@ -133,9 +124,7 @@ export const fullscreenable = {
|
||||
],
|
||||
actions: {
|
||||
/** Toggle fullscreen state or explicitly enter/exit based on detail */
|
||||
fullscreenrequest: (
|
||||
{ detail }: Pick<CustomEvent<any>, 'detail'> = { detail: undefined },
|
||||
) => {
|
||||
fullscreenrequest: ({ detail }: Pick<CustomEvent<any>, 'detail'> = { detail: undefined }) => {
|
||||
// If detail is provided, use it; otherwise toggle current state
|
||||
if (typeof detail === 'boolean') {
|
||||
return detail;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export { audible } from './audible';
|
||||
export { playable } from './playable';
|
||||
export { temporal } from './temporal';
|
||||
export { fullscreenable } from './fullscreenable';
|
||||
export { fullscreenable } from './fullscreenable';
|
||||
|
||||
@@ -12,13 +12,13 @@ export const playable = {
|
||||
(handler: (value?: boolean) => void, stateOwners: any) => {
|
||||
const { media } = stateOwners;
|
||||
if (!media) return;
|
||||
|
||||
|
||||
const eventHandler = () => handler();
|
||||
const events = ['play', 'playing', 'pause', 'emptied'];
|
||||
events.forEach(event => media.addEventListener(event, eventHandler));
|
||||
|
||||
return () => events.forEach(event => media.removeEventListener(event, eventHandler));
|
||||
}
|
||||
events.forEach((event) => media.addEventListener(event, eventHandler));
|
||||
|
||||
return () => events.forEach((event) => media.removeEventListener(event, eventHandler));
|
||||
},
|
||||
],
|
||||
actions: {
|
||||
/** @TODO Refactor me to play more nicely with side effects that don't/can't correlate with set() API (CJP) */
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
const isValidNumber = (value: any): value is number => {
|
||||
return (
|
||||
typeof value === 'number' && !Number.isNaN(value) && Number.isFinite(value)
|
||||
);
|
||||
return typeof value === 'number' && !Number.isNaN(value) && Number.isFinite(value);
|
||||
};
|
||||
|
||||
export const temporal = {
|
||||
@@ -19,19 +17,17 @@ export const temporal = {
|
||||
(handler: (value?: number) => void, stateOwners: any) => {
|
||||
const { media } = stateOwners;
|
||||
if (!media) return;
|
||||
|
||||
|
||||
const eventHandler = () => handler();
|
||||
const events = ['timeupdate', 'loadedmetadata'];
|
||||
events.forEach(event => media.addEventListener(event, eventHandler));
|
||||
|
||||
return () => events.forEach(event => media.removeEventListener(event, eventHandler));
|
||||
}
|
||||
events.forEach((event) => media.addEventListener(event, eventHandler));
|
||||
|
||||
return () => events.forEach((event) => media.removeEventListener(event, eventHandler));
|
||||
},
|
||||
],
|
||||
actions: {
|
||||
/** @TODO Support more sophisticated seeking patterns like seek-to-live, relative seeking, etc. (CJP) */
|
||||
seekrequest: (
|
||||
{ detail }: Pick<CustomEvent<any>, 'detail'> = { detail: 0 },
|
||||
) => +detail,
|
||||
seekrequest: ({ detail }: Pick<CustomEvent<any>, 'detail'> = { detail: 0 }) => +detail,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -40,11 +36,7 @@ export const temporal = {
|
||||
const { media } = stateOwners;
|
||||
|
||||
// Return 0 if no media or invalid duration
|
||||
if (
|
||||
!media?.duration ||
|
||||
Number.isNaN(media.duration) ||
|
||||
!Number.isFinite(media.duration)
|
||||
) {
|
||||
if (!media?.duration || Number.isNaN(media.duration) || !Number.isFinite(media.duration)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -54,13 +46,13 @@ export const temporal = {
|
||||
(handler: (value?: number) => void, stateOwners: any) => {
|
||||
const { media } = stateOwners;
|
||||
if (!media) return;
|
||||
|
||||
|
||||
const eventHandler = () => handler();
|
||||
const events = ['loadedmetadata', 'durationchange', 'emptied'];
|
||||
events.forEach(event => media.addEventListener(event, eventHandler));
|
||||
|
||||
return () => events.forEach(event => media.removeEventListener(event, eventHandler));
|
||||
}
|
||||
events.forEach((event) => media.addEventListener(event, eventHandler));
|
||||
|
||||
return () => events.forEach((event) => media.removeEventListener(event, eventHandler));
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -82,13 +74,13 @@ export const temporal = {
|
||||
(handler: (value?: [number, number] | undefined) => void, stateOwners: any) => {
|
||||
const { media } = stateOwners;
|
||||
if (!media) return;
|
||||
|
||||
|
||||
const eventHandler = () => handler();
|
||||
const events = ['loadedmetadata', 'emptied', 'progress', 'seekablechange'];
|
||||
events.forEach(event => media.addEventListener(event, eventHandler));
|
||||
|
||||
return () => events.forEach(event => media.removeEventListener(event, eventHandler));
|
||||
}
|
||||
events.forEach((event) => media.addEventListener(event, eventHandler));
|
||||
|
||||
return () => events.forEach((event) => media.removeEventListener(event, eventHandler));
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -29,10 +29,7 @@ const UnitLabels = [
|
||||
] as const;
|
||||
|
||||
const toTimeUnitPhrase = (timeUnitValue: number, unitIndex: number): string => {
|
||||
const unitLabel =
|
||||
timeUnitValue === 1
|
||||
? UnitLabels[unitIndex]?.singular
|
||||
: UnitLabels[unitIndex]?.plural;
|
||||
const unitLabel = timeUnitValue === 1 ? UnitLabels[unitIndex]?.singular : UnitLabels[unitIndex]?.plural;
|
||||
|
||||
return `${timeUnitValue} ${unitLabel}`;
|
||||
};
|
||||
@@ -53,18 +50,11 @@ export function formatAsTimePhrase(seconds: number): string {
|
||||
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 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),
|
||||
)
|
||||
.map((timeUnitValue, index) => timeUnitValue && toTimeUnitPhrase(timeUnitValue, index))
|
||||
// Ignore/exclude any 0 values
|
||||
.filter((x) => x)
|
||||
// join into a single comma-separated string phrase
|
||||
@@ -124,8 +114,7 @@ export function formatTime(seconds: number, guide?: number): string {
|
||||
|
||||
// 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) + ':';
|
||||
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;
|
||||
@@ -140,11 +129,7 @@ export function formatTime(seconds: number, guide?: number): string {
|
||||
* @param fallback - Fallback text when time is invalid (default: "--:--")
|
||||
* @returns Formatted time string or fallback
|
||||
*/
|
||||
export function formatDisplayTime(
|
||||
time: unknown,
|
||||
guide?: number,
|
||||
fallback: string = '--:--',
|
||||
): string {
|
||||
export function formatDisplayTime(time: unknown, guide?: number, fallback: string = '--:--'): string {
|
||||
if (!isValidNumber(time)) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user