mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-14 21:26:15 -08:00
feat(ui): shared cinematic detail pieces (chip, light primary, icon button, more menu, cast row, trailer dialog)
Adds the building blocks of the redesigned VOD details and dashboard hero: a pill meta chip, the light primary button with a two-line label, a 44px ghost icon button with tooltip, the right-aligned "…" dropdown menu that flips upward and hosts the alternative-sources panel, hero credits plus a Cast & crew row, a horizontal rail with prev/next arrows, a Similar rail and the trailer modal. Shared detail utilities (season suffix, remaining time, duration, short country names) move to portal/shared/util, with the new CTA and rating theme tokens and the i18n keys for all locales. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
b6357da6af
commit
3fd65c935c
63 files changed
+3025
-92
No files matched your search
@@ -5,10 +5,14 @@ import {
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
import type { GlobalRecentItem } from '@iptvnator/workspace/dashboard/data-access';
|
||||
|
||||
export interface DashboardRemainingLabel {
|
||||
readonly key: string;
|
||||
readonly params: Record<string, number>;
|
||||
}
|
||||
import {
|
||||
formatRemainingLabel,
|
||||
playbackProgressPercent,
|
||||
type RemainingTimeLabel,
|
||||
} from '@iptvnator/portal/shared/util';
|
||||
|
||||
export type DashboardRemainingLabel = RemainingTimeLabel;
|
||||
export { formatRemainingLabel, playbackProgressPercent };
|
||||
|
||||
export function buildPlaybackPositionReloadKey(
|
||||
items: readonly Pick<
|
||||
@@ -29,66 +33,6 @@ export function isContinueWatchingRecentItem(
|
||||
return item.type === 'movie' || item.type === 'series';
|
||||
}
|
||||
|
||||
export function playbackProgressPercent(
|
||||
position: { positionSeconds: number; durationSeconds?: number } | null
|
||||
): number | null {
|
||||
if (
|
||||
!position ||
|
||||
position.durationSeconds == null ||
|
||||
position.durationSeconds <= 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const ratio = position.positionSeconds / position.durationSeconds;
|
||||
if (!Number.isFinite(ratio)) {
|
||||
return null;
|
||||
}
|
||||
// Integer percent keeps "92% watched" out of "92.4% watched" territory,
|
||||
// and matches the resolution of a 3px-tall progress bar on a 280px card.
|
||||
return Math.max(0, Math.min(100, Math.floor(ratio * 100)));
|
||||
}
|
||||
|
||||
export function formatRemainingLabel(
|
||||
position: { positionSeconds: number; durationSeconds?: number } | null
|
||||
): DashboardRemainingLabel | null {
|
||||
if (
|
||||
!position ||
|
||||
position.durationSeconds == null ||
|
||||
position.durationSeconds <= 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const remaining = Math.max(
|
||||
0,
|
||||
Math.round(position.durationSeconds - position.positionSeconds)
|
||||
);
|
||||
if (remaining < 60) {
|
||||
return {
|
||||
key: 'WORKSPACE.DASHBOARD.REMAINING_SECONDS',
|
||||
params: { seconds: remaining },
|
||||
};
|
||||
}
|
||||
const totalMinutes = Math.round(remaining / 60);
|
||||
if (totalMinutes < 60) {
|
||||
return {
|
||||
key: 'WORKSPACE.DASHBOARD.REMAINING_MINUTES',
|
||||
params: { minutes: totalMinutes },
|
||||
};
|
||||
}
|
||||
const hours = Math.floor(totalMinutes / 60);
|
||||
const minutes = totalMinutes % 60;
|
||||
if (minutes === 0) {
|
||||
return {
|
||||
key: 'WORKSPACE.DASHBOARD.REMAINING_HOURS',
|
||||
params: { hours },
|
||||
};
|
||||
}
|
||||
return {
|
||||
key: 'WORKSPACE.DASHBOARD.REMAINING_HOURS_MINUTES',
|
||||
params: { hours, minutes },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* "S1·E5" for an item whose progress is tracked per episode. Keyed on the
|
||||
* WATCH kind: a Stalker embedded-VOD / lazy `is_series` show routes as a
|
||||
|
||||
Reference in new issue
Block a user