mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-04-12 04:19:25 -07:00
153 lines
5.5 KiB
TypeScript
153 lines
5.5 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { CoverImage } from './CoverImage';
|
|
import { formatDuration, formatNumber, formatPercent } from '../../lib/formatters';
|
|
import { getStatsClient } from '../../hooks/useStatsApi';
|
|
import { buildLookupRateDisplay } from '../../lib/yomitan-lookup';
|
|
import { resolveMediaArtworkUrl } from '../../lib/media-library-grouping';
|
|
import type { MediaDetailData } from '../../types/stats';
|
|
|
|
interface MediaHeaderProps {
|
|
detail: NonNullable<MediaDetailData['detail']>;
|
|
initialKnownWordsSummary?: {
|
|
totalUniqueWords: number;
|
|
knownWordCount: number;
|
|
} | null;
|
|
onDeleteEpisode?: () => void;
|
|
isDeletingEpisode?: boolean;
|
|
}
|
|
|
|
export function MediaHeader({
|
|
detail,
|
|
initialKnownWordsSummary = null,
|
|
onDeleteEpisode,
|
|
isDeletingEpisode = false,
|
|
}: MediaHeaderProps) {
|
|
const knownTokenRate =
|
|
detail.totalLookupCount > 0 ? detail.totalLookupHits / detail.totalLookupCount : null;
|
|
const avgSessionMs =
|
|
detail.totalSessions > 0 ? Math.round(detail.totalActiveMs / detail.totalSessions) : 0;
|
|
const lookupRate = buildLookupRateDisplay(detail.totalYomitanLookupCount, detail.totalTokensSeen);
|
|
|
|
const [knownWordsSummary, setKnownWordsSummary] = useState<{
|
|
totalUniqueWords: number;
|
|
knownWordCount: number;
|
|
} | null>(initialKnownWordsSummary);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
getStatsClient()
|
|
.getMediaKnownWordsSummary(detail.videoId)
|
|
.then((data) => {
|
|
if (!cancelled) setKnownWordsSummary(data);
|
|
})
|
|
.catch(() => {
|
|
if (!cancelled) setKnownWordsSummary(null);
|
|
});
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [detail.videoId]);
|
|
|
|
return (
|
|
<div className="flex gap-4">
|
|
<CoverImage
|
|
videoId={detail.videoId}
|
|
title={detail.canonicalTitle}
|
|
src={resolveMediaArtworkUrl(detail, 'video')}
|
|
className="w-32 h-44 rounded-lg shrink-0"
|
|
/>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-start justify-between gap-2">
|
|
<h2 className="min-w-0 flex-1 text-lg font-bold text-ctp-text truncate">
|
|
{detail.canonicalTitle}
|
|
</h2>
|
|
{onDeleteEpisode != null ? (
|
|
<button
|
|
type="button"
|
|
onClick={onDeleteEpisode}
|
|
disabled={isDeletingEpisode}
|
|
className="shrink-0 text-xs text-ctp-red hover:opacity-75 transition-opacity disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
{isDeletingEpisode ? 'Deleting...' : 'Delete Episode'}
|
|
</button>
|
|
) : null}
|
|
</div>
|
|
{detail.channelName ? (
|
|
<div className="mt-1 text-sm text-ctp-subtext1 truncate">
|
|
{detail.channelUrl ? (
|
|
<a
|
|
href={detail.channelUrl}
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
className="hover:text-ctp-blue transition-colors"
|
|
>
|
|
{detail.channelName}
|
|
</a>
|
|
) : (
|
|
detail.channelName
|
|
)}
|
|
</div>
|
|
) : null}
|
|
<div className="grid grid-cols-2 gap-2 mt-3 text-sm">
|
|
<div>
|
|
<div className="text-ctp-blue font-medium">{formatDuration(detail.totalActiveMs)}</div>
|
|
<div className="text-xs text-ctp-overlay2">total watch time</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-ctp-cards-mined font-medium">
|
|
{formatNumber(detail.totalCards)}
|
|
</div>
|
|
<div className="text-xs text-ctp-overlay2">cards mined</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-ctp-mauve font-medium">{formatNumber(detail.totalTokensSeen)}</div>
|
|
<div className="text-xs text-ctp-overlay2">word occurrences</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-ctp-lavender font-medium">
|
|
{formatNumber(detail.totalYomitanLookupCount)}
|
|
</div>
|
|
<div className="text-xs text-ctp-overlay2">Yomitan lookups</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-ctp-sapphire font-medium">
|
|
{lookupRate?.shortValue ?? '\u2014'}
|
|
</div>
|
|
<div className="text-xs text-ctp-overlay2">
|
|
{lookupRate?.longValue ?? 'lookup rate'}
|
|
</div>
|
|
</div>
|
|
{knownWordsSummary && knownWordsSummary.totalUniqueWords > 0 ? (
|
|
<div>
|
|
<div className="text-ctp-green font-medium">
|
|
{formatNumber(knownWordsSummary.knownWordCount)} /{' '}
|
|
{formatNumber(knownWordsSummary.totalUniqueWords)}
|
|
</div>
|
|
<div className="text-xs text-ctp-overlay2">
|
|
known unique words (
|
|
{Math.round(
|
|
(knownWordsSummary.knownWordCount / knownWordsSummary.totalUniqueWords) * 100,
|
|
)}
|
|
%)
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div>
|
|
<div className="text-ctp-peach font-medium">{formatPercent(knownTokenRate)}</div>
|
|
<div className="text-xs text-ctp-overlay2">known word match rate</div>
|
|
</div>
|
|
)}
|
|
<div>
|
|
<div className="text-ctp-text font-medium">{detail.totalSessions}</div>
|
|
<div className="text-xs text-ctp-overlay2">sessions</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-ctp-text font-medium">{formatDuration(avgSessionMs)}</div>
|
|
<div className="text-xs text-ctp-overlay2">avg session</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|