audio player respects group context
This commit is contained in:
@@ -180,6 +180,7 @@ export default async function TranscriptionPage({
|
||||
subtitle={[t.date, t.place].filter(Boolean).join(" · ")}
|
||||
sourceUrl={`/transcriptions/${t.slug}`}
|
||||
type={t.type}
|
||||
fallbackYear={t.year}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -5,6 +5,11 @@ import {
|
||||
type AudioTrack,
|
||||
AudioWaveAnimation,
|
||||
} from "@/components/audio-player";
|
||||
import {
|
||||
rememberedQueue,
|
||||
useTranscriptQueue,
|
||||
yearQueue,
|
||||
} from "@/components/audio-player/transcript-queue";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Play } from "lucide-react";
|
||||
|
||||
@@ -17,6 +22,11 @@ type PlayAudioButtonProps = {
|
||||
className?: string;
|
||||
/** Compact icon-only variant for list items */
|
||||
variant?: "default" | "compact";
|
||||
/**
|
||||
* On a lecture's own page: its year, queued as a fallback when the lecture
|
||||
* wasn't opened from a list (search, a shared link…).
|
||||
*/
|
||||
fallbackYear?: number | null;
|
||||
};
|
||||
|
||||
export function PlayAudioButton({
|
||||
@@ -27,8 +37,18 @@ export function PlayAudioButton({
|
||||
type,
|
||||
className,
|
||||
variant = "default",
|
||||
fallbackYear,
|
||||
}: PlayAudioButtonProps) {
|
||||
const { currentTrack, isPlaying, play, togglePlayPause } = useAudioPlayer();
|
||||
const {
|
||||
currentTrack,
|
||||
isPlaying,
|
||||
play,
|
||||
playPlaylist,
|
||||
attachQueue,
|
||||
togglePlayPause,
|
||||
} = useAudioPlayer();
|
||||
// The list this button sits in (a transcription list row), if any.
|
||||
const listQueue = useTranscriptQueue();
|
||||
|
||||
const isThisTrack = currentTrack?.url === audioUrl;
|
||||
const isThisPlaying = isThisTrack && isPlaying;
|
||||
@@ -44,7 +64,25 @@ export function PlayAudioButton({
|
||||
sourceUrl,
|
||||
type: type ?? "lecture",
|
||||
};
|
||||
// Queue the list this recording was found in, so Next / Previous
|
||||
// follow it: the list the button sits in, or — on a lecture's own
|
||||
// page — the list it was opened from.
|
||||
const queue =
|
||||
listQueue ??
|
||||
rememberedQueue(sourceUrl.replace(/^\/transcriptions\//, ""));
|
||||
const at = queue?.findIndex((q) => q.url === audioUrl) ?? -1;
|
||||
if (queue && at >= 0 && queue.length > 1) {
|
||||
playPlaylist(queue, at);
|
||||
return;
|
||||
}
|
||||
// No list to follow: start this lecture now — audio must begin inside
|
||||
// the tap, or iOS blocks it — then queue its year once that loads.
|
||||
play(track);
|
||||
if (fallbackYear) {
|
||||
void yearQueue(fallbackYear).then((tracks) => {
|
||||
if (tracks && tracks.length > 1) attachQueue(tracks);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,15 @@ import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ChevronLeft, ChevronRight, Menu, Calendar, Clock, MapPin } from "lucide-react";
|
||||
import {
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Menu,
|
||||
Calendar,
|
||||
Clock,
|
||||
MapPin,
|
||||
} from "lucide-react";
|
||||
import { TranscriptQueue } from "@/components/audio-player/transcript-queue";
|
||||
import { PlayAudioButton } from "@/app/(app)/(reader)/transcriptions/[slug]/play-audio-button";
|
||||
import { parseChapter, parseVerse } from "@/lib/book-reference";
|
||||
import {
|
||||
@@ -417,6 +425,13 @@ export function TranscriptionBookBrowser({
|
||||
contentRef.current?.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
};
|
||||
|
||||
// Next / Previous run through every chapter's lectures in display order,
|
||||
// so playback carries on into the next chapter.
|
||||
const queueItems = useMemo(
|
||||
() => flat.flatMap((f) => f.chapter.transcriptions),
|
||||
[flat],
|
||||
);
|
||||
|
||||
const activeIdx = flat.findIndex((f) => f.chapter.key === activeKey);
|
||||
const active = activeIdx >= 0 ? flat[activeIdx] : null;
|
||||
const prev = activeIdx > 0 ? flat[activeIdx - 1] : null;
|
||||
@@ -463,42 +478,44 @@ export function TranscriptionBookBrowser({
|
||||
|
||||
{/* Main content */}
|
||||
<main ref={contentRef} className="flex-1 min-w-0 px-6 py-8 space-y-8">
|
||||
{/* Top navigation */}
|
||||
<ChapterNav
|
||||
prev={prev}
|
||||
next={next}
|
||||
sections={sections}
|
||||
activeKey={activeKey}
|
||||
onSelect={handleSelect}
|
||||
drawerTitle={title}
|
||||
/>
|
||||
<TranscriptQueue items={queueItems}>
|
||||
{/* Top navigation */}
|
||||
<ChapterNav
|
||||
prev={prev}
|
||||
next={next}
|
||||
sections={sections}
|
||||
activeKey={activeKey}
|
||||
onSelect={handleSelect}
|
||||
drawerTitle={title}
|
||||
/>
|
||||
|
||||
{/* Content — every lecture of the active chapter (all years/places) */}
|
||||
{!active || active.chapter.transcriptions.length === 0 ? (
|
||||
<div className="py-12 text-center text-muted-foreground">
|
||||
<p className="text-lg font-medium">
|
||||
No transcriptions for this chapter
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<Card className="overflow-hidden">
|
||||
<CardContent className="p-0 divide-y divide-border">
|
||||
{active.chapter.transcriptions.map((t) => (
|
||||
<LectureRow key={t.slug} t={t} />
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
{/* Content — every lecture of the active chapter (all years/places) */}
|
||||
{!active || active.chapter.transcriptions.length === 0 ? (
|
||||
<div className="py-12 text-center text-muted-foreground">
|
||||
<p className="text-lg font-medium">
|
||||
No transcriptions for this chapter
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<Card className="overflow-hidden">
|
||||
<CardContent className="p-0 divide-y divide-border">
|
||||
{active.chapter.transcriptions.map((t) => (
|
||||
<LectureRow key={t.slug} t={t} />
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Bottom navigation */}
|
||||
<ChapterNav
|
||||
prev={prev}
|
||||
next={next}
|
||||
sections={sections}
|
||||
activeKey={activeKey}
|
||||
onSelect={handleSelect}
|
||||
drawerTitle={title}
|
||||
/>
|
||||
{/* Bottom navigation */}
|
||||
<ChapterNav
|
||||
prev={prev}
|
||||
next={next}
|
||||
sections={sections}
|
||||
activeKey={activeKey}
|
||||
onSelect={handleSelect}
|
||||
drawerTitle={title}
|
||||
/>
|
||||
</TranscriptQueue>
|
||||
</main>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -9,6 +9,7 @@ import { PageHeader } from "@/components/layout/page-header";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Calendar, Clock } from "lucide-react";
|
||||
import { TranscriptQueue } from "@/components/audio-player/transcript-queue";
|
||||
import { PlayAudioButton } from "@/app/(app)/(reader)/transcriptions/[slug]/play-audio-button";
|
||||
|
||||
export async function generateMetadata({
|
||||
@@ -55,58 +56,61 @@ export default async function TranscriptionsByPlacePage({
|
||||
</PageHeader>
|
||||
|
||||
<main className="max-w-5xl mx-auto px-6 py-8">
|
||||
<Card className="overflow-hidden">
|
||||
<CardContent className="p-0 divide-y divide-border">
|
||||
{transcriptions.map((t) => (
|
||||
<Link
|
||||
key={t.slug}
|
||||
href={`/transcriptions/${t.slug}`}
|
||||
className="flex items-center px-5 py-3.5 gap-3 hover:bg-muted transition-colors"
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<p className="font-medium text-foreground text-sm">
|
||||
{t.name}
|
||||
</p>
|
||||
<Badge
|
||||
variant={t.type === "lecture" ? "default" : "secondary"}
|
||||
className="text-[10px] px-1.5 py-0"
|
||||
>
|
||||
{t.type === "lecture" ? "Lecture" : "Conversation"}
|
||||
</Badge>
|
||||
{/* Next / Previous in the player follow this list. */}
|
||||
<TranscriptQueue items={transcriptions}>
|
||||
<Card className="overflow-hidden">
|
||||
<CardContent className="p-0 divide-y divide-border">
|
||||
{transcriptions.map((t) => (
|
||||
<Link
|
||||
key={t.slug}
|
||||
href={`/transcriptions/${t.slug}`}
|
||||
className="flex items-center px-5 py-3.5 gap-3 hover:bg-muted transition-colors"
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<p className="font-medium text-foreground text-sm">
|
||||
{t.name}
|
||||
</p>
|
||||
<Badge
|
||||
variant={t.type === "lecture" ? "default" : "secondary"}
|
||||
className="text-[10px] px-1.5 py-0"
|
||||
>
|
||||
{t.type === "lecture" ? "Lecture" : "Conversation"}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 mt-1 text-xs text-muted-foreground">
|
||||
{t.date && (
|
||||
<span className="flex items-center gap-1">
|
||||
<Calendar className="size-3" />
|
||||
{t.date}
|
||||
</span>
|
||||
)}
|
||||
{t.reference && (
|
||||
<span className="text-primary/70">{t.reference}</span>
|
||||
)}
|
||||
{t.durationMinutes && t.durationMinutes > 0 && (
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock className="size-3" />
|
||||
{t.durationMinutes} min
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 mt-1 text-xs text-muted-foreground">
|
||||
{t.date && (
|
||||
<span className="flex items-center gap-1">
|
||||
<Calendar className="size-3" />
|
||||
{t.date}
|
||||
</span>
|
||||
)}
|
||||
{t.reference && (
|
||||
<span className="text-primary/70">{t.reference}</span>
|
||||
)}
|
||||
{t.durationMinutes && t.durationMinutes > 0 && (
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock className="size-3" />
|
||||
{t.durationMinutes} min
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{t.audioUrl && (
|
||||
<PlayAudioButton
|
||||
audioUrl={t.audioUrl}
|
||||
title={t.name}
|
||||
subtitle={[t.date, t.place].filter(Boolean).join(" · ")}
|
||||
sourceUrl={`/transcriptions/${t.slug}`}
|
||||
type={t.type}
|
||||
variant="compact"
|
||||
/>
|
||||
)}
|
||||
</Link>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
{t.audioUrl && (
|
||||
<PlayAudioButton
|
||||
audioUrl={t.audioUrl}
|
||||
title={t.name}
|
||||
subtitle={[t.date, t.place].filter(Boolean).join(" · ")}
|
||||
sourceUrl={`/transcriptions/${t.slug}`}
|
||||
type={t.type}
|
||||
variant="compact"
|
||||
/>
|
||||
)}
|
||||
</Link>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TranscriptQueue>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
Clock,
|
||||
MapPin,
|
||||
} from "lucide-react";
|
||||
import { TranscriptQueue } from "@/components/audio-player/transcript-queue";
|
||||
import { PlayAudioButton } from "@/app/(app)/(reader)/transcriptions/[slug]/play-audio-button";
|
||||
import {
|
||||
Drawer,
|
||||
@@ -481,6 +482,16 @@ export function TranscriptionTypeBrowser({
|
||||
[activeGroup],
|
||||
);
|
||||
|
||||
// Next / Previous run through the whole list in display order — year by
|
||||
// year, month by month — so playback carries on past the year on screen.
|
||||
const queueItems = useMemo(
|
||||
() =>
|
||||
yearGroups.flatMap((yg) =>
|
||||
buildMonthGroups(yg.transcriptions).flatMap((mg) => mg.transcriptions),
|
||||
),
|
||||
[yearGroups],
|
||||
);
|
||||
|
||||
const activeYearLabel = activeGroup?.label ?? "Unknown";
|
||||
const drawerTitle = typeLabel;
|
||||
|
||||
@@ -520,46 +531,48 @@ export function TranscriptionTypeBrowser({
|
||||
|
||||
{/* Main content */}
|
||||
<main ref={contentRef} className="flex-1 min-w-0 px-6 py-8 space-y-8">
|
||||
{/* Top navigation */}
|
||||
<YearNav
|
||||
prev={prev}
|
||||
next={next}
|
||||
groups={yearGroups}
|
||||
activeYear={activeYear}
|
||||
onSelect={handleYearSelect}
|
||||
drawerTitle={drawerTitle}
|
||||
/>
|
||||
<TranscriptQueue items={queueItems}>
|
||||
{/* Top navigation */}
|
||||
<YearNav
|
||||
prev={prev}
|
||||
next={next}
|
||||
groups={yearGroups}
|
||||
activeYear={activeYear}
|
||||
onSelect={handleYearSelect}
|
||||
drawerTitle={drawerTitle}
|
||||
/>
|
||||
|
||||
{/* Content — transcriptions grouped by month */}
|
||||
{monthGroups.length === 0 ? (
|
||||
<div className="py-12 text-center text-muted-foreground">
|
||||
<p className="text-lg font-medium">
|
||||
No transcriptions for this year
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<Card className="overflow-hidden">
|
||||
<CardContent className="p-0 divide-y divide-border">
|
||||
{monthGroups.map((mg, i) => (
|
||||
<MonthSection
|
||||
key={mg.month}
|
||||
group={mg}
|
||||
defaultOpen={i === 0 || monthGroups.length <= 3}
|
||||
/>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
{/* Content — transcriptions grouped by month */}
|
||||
{monthGroups.length === 0 ? (
|
||||
<div className="py-12 text-center text-muted-foreground">
|
||||
<p className="text-lg font-medium">
|
||||
No transcriptions for this year
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<Card className="overflow-hidden">
|
||||
<CardContent className="p-0 divide-y divide-border">
|
||||
{monthGroups.map((mg, i) => (
|
||||
<MonthSection
|
||||
key={mg.month}
|
||||
group={mg}
|
||||
defaultOpen={i === 0 || monthGroups.length <= 3}
|
||||
/>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Bottom navigation */}
|
||||
<YearNav
|
||||
prev={prev}
|
||||
next={next}
|
||||
groups={yearGroups}
|
||||
activeYear={activeYear}
|
||||
onSelect={handleYearSelect}
|
||||
drawerTitle={drawerTitle}
|
||||
/>
|
||||
{/* Bottom navigation */}
|
||||
<YearNav
|
||||
prev={prev}
|
||||
next={next}
|
||||
groups={yearGroups}
|
||||
activeYear={activeYear}
|
||||
onSelect={handleYearSelect}
|
||||
drawerTitle={drawerTitle}
|
||||
/>
|
||||
</TranscriptQueue>
|
||||
</main>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Calendar, ChevronRight, Clock, MapPin } from "lucide-react";
|
||||
import { TranscriptQueue } from "@/components/audio-player/transcript-queue";
|
||||
import { PlayAudioButton } from "@/app/(app)/(reader)/transcriptions/[slug]/play-audio-button";
|
||||
|
||||
type TranscriptionItem = {
|
||||
@@ -169,6 +170,12 @@ export function TranscriptionYearBrowser({
|
||||
return groups;
|
||||
}, [transcriptions]);
|
||||
|
||||
// Display order — month by month — is the order Next / Previous play in.
|
||||
const queueItems = useMemo(
|
||||
() => monthGroups.flatMap((g) => g.items),
|
||||
[monthGroups],
|
||||
);
|
||||
|
||||
const lectureCount = transcriptions.filter(
|
||||
(t) => t.type === "lecture",
|
||||
).length;
|
||||
@@ -202,19 +209,21 @@ export function TranscriptionYearBrowser({
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<Card className="overflow-hidden">
|
||||
<CardContent className="p-0 divide-y divide-border">
|
||||
{monthGroups.map((mg, i) => (
|
||||
<MonthSection
|
||||
key={mg.month}
|
||||
month={mg.month}
|
||||
label={mg.label}
|
||||
items={mg.items}
|
||||
defaultOpen={i === 0 || monthGroups.length <= 3}
|
||||
/>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<TranscriptQueue items={queueItems}>
|
||||
<Card className="overflow-hidden">
|
||||
<CardContent className="p-0 divide-y divide-border">
|
||||
{monthGroups.map((mg, i) => (
|
||||
<MonthSection
|
||||
key={mg.month}
|
||||
month={mg.month}
|
||||
label={mg.label}
|
||||
items={mg.items}
|
||||
defaultOpen={i === 0 || monthGroups.length <= 3}
|
||||
/>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TranscriptQueue>
|
||||
)}
|
||||
</main>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user