feat: add framer motion for audio player, handle og images for transcripts

This commit is contained in:
gosangam
2026-05-09 11:59:48 +05:30
parent 9eb2a06b50
commit 326ba090ca
10 changed files with 1187 additions and 11826 deletions
+30 -6
View File
@@ -7,7 +7,7 @@ import {
getTranscription, getTranscription,
getAdjacentTranscriptions, getAdjacentTranscriptions,
} from "@/lib/transcriptions"; } from "@/lib/transcriptions";
import { buildOgMeta } from "@/lib/og"; import { buildOgMeta, ogForTranscription, extractQuoteFromHl } from "@/lib/og";
import { PageHeader } from "@/components/page-header"; import { PageHeader } from "@/components/page-header";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
@@ -18,27 +18,51 @@ import { PlayAudioButton } from "./play-audio-button";
export async function generateMetadata({ export async function generateMetadata({
params, params,
searchParams,
}: { }: {
params: Promise<{ slug: string }>; params: Promise<{ slug: string }>;
searchParams: Promise<Record<string, string | string[] | undefined>>;
}): Promise<Metadata> { }): Promise<Metadata> {
const { slug } = await params; const { slug } = await params;
const sp = await searchParams;
const t = await getTranscription(slug); const t = await getTranscription(slug);
if (!t) return {}; if (!t) return {};
const detail = [t.date, t.place].filter(Boolean).join(" · "); const detail = [t.date, t.place].filter(Boolean).join(" · ");
// Build paragraphs from content blocks for hl matching
const paragraphs = t.content.map((b) => {
const speakerPrefix = b.speaker ? `${b.speaker}: ` : "";
return speakerPrefix + b.text.replace(/<[^>]+>/g, "");
});
const hlQuote = extractQuoteFromHl(sp, paragraphs);
if (hlQuote) {
return buildOgMeta({ return buildOgMeta({
title: t.name, title: t.name,
detail: detail || undefined, detail: detail || undefined,
quote: quote: hlQuote,
t.content.length > 0 });
? t.content }
// Fallback: find a good Prabhupāda quote from the content
const spQuote = t.content
.find((b) => b.speaker === "Prabhupāda" && b.text.length > 50) .find((b) => b.speaker === "Prabhupāda" && b.text.length > 50)
?.text?.replace(/<[^>]+>/g, "") ?.text?.replace(/<[^>]+>/g, "")
.slice(0, 200) .slice(0, 200);
: undefined,
if (spQuote) {
return buildOgMeta({
title: t.name,
detail: detail || undefined,
quote: spQuote,
}); });
} }
return ogForTranscription(t.name, detail || undefined);
}
export default async function TranscriptionPage({ export default async function TranscriptionPage({
params, params,
}: { }: {
@@ -1,8 +1,12 @@
"use client"; "use client";
import { useAudioPlayer, type AudioTrack } from "@/components/audio-player"; import {
useAudioPlayer,
type AudioTrack,
AudioWaveAnimation,
} from "@/components/audio-player";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Play, Pause, Radio } from "lucide-react"; import { Play, Pause } from "lucide-react";
type PlayAudioButtonProps = { type PlayAudioButtonProps = {
audioUrl: string; audioUrl: string;
@@ -85,7 +89,11 @@ export function PlayAudioButton({
> >
{isThisPlaying ? ( {isThisPlaying ? (
<> <>
<Radio className="size-4 animate-pulse" /> <AudioWaveAnimation
paused={false}
colorClassName="bg-primary-foreground"
size="sm"
/>
<span>Now Playing</span> <span>Now Playing</span>
</> </>
) : ( ) : (
+2 -7
View File
@@ -3,7 +3,7 @@ export const dynamic = "force-dynamic";
import type { Metadata } from "next"; import type { Metadata } from "next";
import Link from "next/link"; import Link from "next/link";
import { PageHeader } from "@/components/page-header"; import { PageHeader } from "@/components/page-header";
import { buildOgMeta } from "@/lib/og"; import { ogForTranscriptions } from "@/lib/og";
import { import {
getTranscriptionYears, getTranscriptionYears,
getTranscriptionPlaces, getTranscriptionPlaces,
@@ -14,12 +14,7 @@ import { TranscriptionsNav } from "./transcriptions-nav";
import { TranscriptionsBrowser } from "./transcriptions-browser"; import { TranscriptionsBrowser } from "./transcriptions-browser";
export function generateMetadata(): Metadata { export function generateMetadata(): Metadata {
return buildOgMeta({ return ogForTranscriptions();
title: "Transcriptions",
detail: "3,700+ lectures & conversations (1966–1977)",
quote:
"Transcriptions of Śrīla Prabhupāda's lectures, morning walks, room conversations, and dialogues — the living voice of the Founder-Ācārya preserved in text.",
});
} }
export default async function TranscriptionsPage({ export default async function TranscriptionsPage({
+2 -2
View File
@@ -17,8 +17,8 @@ export async function generateMetadata({
const transcriptions = await getTranscriptionsByYear(year); const transcriptions = await getTranscriptionsByYear(year);
return buildOgMeta({ return buildOgMeta({
title: "Transcriptions", title: "Transcriptions",
detail: `${transcriptions.length} transcriptions from ${year}`, detail: `${transcriptions.length} lectures & conversations from ${year}`,
quote: `Lectures and conversations of Śrīla Prabhupāda from ${year}.`, quote: `Lectures and conversations of Śrīla Prabhupāda from ${year} — the living voice of the Founder-Ācārya preserved in text.`,
}); });
} }
+59
View File
@@ -0,0 +1,59 @@
"use client";
import { motion } from "framer-motion";
import { cn } from "@/lib/utils";
/**
* Animated audio wave bars — used in the minimized player pill
* and the "Now Playing" button.
*
* @param paused – when true the bars flatten to a static state
* @param colorClassName – Tailwind color class for the bars (default: "bg-current")
* @param className – additional classes on the container
* @param barCount – number of bars (default: 4)
* @param size – "sm" (10px tall) | "md" (12px tall, default) | "lg" (16px tall)
*/
export function AudioWaveAnimation({
paused = false,
colorClassName = "bg-current",
className,
barCount = 4,
size = "md",
}: {
paused?: boolean;
colorClassName?: string;
className?: string;
barCount?: number;
size?: "sm" | "md" | "lg";
}) {
const config = {
sm: { height: 10, minH: 2, barW: "w-[2px]", containerH: "h-2.5" },
md: { height: 12, minH: 3, barW: "w-[3px]", containerH: "h-3" },
lg: { height: 16, minH: 3, barW: "w-[3px]", containerH: "h-4" },
}[size];
return (
<div
className={cn("flex items-end gap-[2px]", config.containerH, className)}
>
{Array.from({ length: barCount }, (_, i) => (
<motion.div
key={i}
className={cn(config.barW, "rounded-full", colorClassName)}
animate={{
height: paused
? config.minH
: [config.minH, config.height, config.minH],
}}
transition={{
duration: 0.8,
repeat: paused ? 0 : Infinity,
repeatType: "mirror",
ease: "easeInOut",
delay: i * 0.15,
}}
/>
))}
</div>
);
}
+223 -30
View File
@@ -1,10 +1,12 @@
"use client"; "use client";
import { useRef, useCallback, useEffect } from "react"; import { useRef, useCallback, useEffect, useState } from "react";
import { useAudioPlayer } from "./audio-context"; import { useAudioPlayer } from "./audio-context";
import { AudioWaveAnimation } from "./audio-wave";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import Link from "next/link"; import Link from "next/link";
import Image from "next/image"; import Image from "next/image";
import { motion, AnimatePresence } from "framer-motion";
import { import {
Play, Play,
Pause, Pause,
@@ -18,6 +20,65 @@ import {
Loader2, Loader2,
} from "lucide-react"; } from "lucide-react";
// ---------------------------------------------------------------------------
// Mobile auto-minimize hook
// ---------------------------------------------------------------------------
const MINIMIZE_DELAY = 8000;
function useIsMobile() {
const [mobile, setMobile] = useState(false);
useEffect(() => {
const mq = window.matchMedia("(max-width: 767px)");
setMobile(mq.matches);
const handler = (e: MediaQueryListEvent) => setMobile(e.matches);
mq.addEventListener("change", handler);
return () => mq.removeEventListener("change", handler);
}, []);
return mobile;
}
/**
* Returns [minimized, setMinimized, resetTimer].
* Auto-minimizes after `MINIMIZE_DELAY` ms of inactivity on mobile.
* Call `resetTimer()` on any user interaction to restart the countdown.
*/
function useMobileMinimize(isMobile: boolean, hasTrack: boolean) {
const [minimized, setMinimized] = useState(false);
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const clearTimer = useCallback(() => {
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
}, []);
const startTimer = useCallback(() => {
clearTimer();
timerRef.current = setTimeout(() => setMinimized(true), MINIMIZE_DELAY);
}, [clearTimer]);
const resetTimer = useCallback(() => {
if (!isMobile) return;
setMinimized(false);
startTimer();
}, [isMobile, startTimer]);
// Start timer when track begins or mobile state changes
useEffect(() => {
if (isMobile && hasTrack) {
startTimer();
} else {
clearTimer();
setMinimized(false);
}
return clearTimer;
}, [isMobile, hasTrack, startTimer, clearTimer]);
return { minimized, setMinimized, resetTimer } as const;
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Constants // Constants
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -230,6 +291,30 @@ export function FloatingPlayer() {
} = useAudioPlayer(); } = useAudioPlayer();
const playerRef = useRef<HTMLDivElement>(null); const playerRef = useRef<HTMLDivElement>(null);
const [selectionToolbarVisible, setSelectionToolbarVisible] = useState(false);
const isMobile = useIsMobile();
const { minimized, setMinimized, resetTimer } = useMobileMinimize(
isMobile,
!!currentTrack,
);
// Watch for the selection toolbar appearing/disappearing in the DOM
useEffect(() => {
if (!currentTrack) return;
const check = () => {
const toolbar = document.querySelector("[data-selection-toolbar]");
// The toolbar wrapper is always present; check if it has visible children
setSelectionToolbarVisible(!!toolbar && toolbar.childElementCount > 0);
};
check();
const observer = new MutationObserver(check);
observer.observe(document.body, { childList: true, subtree: true });
return () => observer.disconnect();
}, [currentTrack]);
// Add bottom padding to body when player is visible // Add bottom padding to body when player is visible
useEffect(() => { useEffect(() => {
@@ -240,47 +325,65 @@ export function FloatingPlayer() {
}; };
}, [currentTrack]); }, [currentTrack]);
// Wrap interactions to reset the minimize timer
const interact = useCallback(
<T extends unknown[]>(fn: (...args: T) => void) =>
(...args: T) => {
resetTimer();
fn(...args);
},
[resetTimer],
);
const iTogglePlayPause = interact(togglePlayPause);
const iSeekTo = interact(seekTo);
const iSetVolume = interact(setVolume);
const iToggleMute = interact(toggleMute);
const iNext = interact(next);
const iPrevious = interact(previous);
const iStop = interact(stop);
// Keyboard shortcuts when player is focused // Keyboard shortcuts when player is focused
const handleKeyDown = useCallback( const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => { (e: React.KeyboardEvent) => {
switch (e.key) { switch (e.key) {
case " ": case " ":
e.preventDefault(); e.preventDefault();
togglePlayPause(); iTogglePlayPause();
break; break;
case "ArrowLeft": case "ArrowLeft":
e.preventDefault(); e.preventDefault();
seekTo(Math.max(0, currentTime - 10)); iSeekTo(Math.max(0, currentTime - 10));
break; break;
case "ArrowRight": case "ArrowRight":
e.preventDefault(); e.preventDefault();
seekTo(Math.min(duration, currentTime + 10)); iSeekTo(Math.min(duration, currentTime + 10));
break; break;
case "ArrowUp": case "ArrowUp":
e.preventDefault(); e.preventDefault();
setVolume(Math.min(1, volume + 0.05)); iSetVolume(Math.min(1, volume + 0.05));
break; break;
case "ArrowDown": case "ArrowDown":
e.preventDefault(); e.preventDefault();
setVolume(Math.max(0, volume - 0.05)); iSetVolume(Math.max(0, volume - 0.05));
break; break;
case "m": case "m":
case "M": case "M":
e.preventDefault(); e.preventDefault();
toggleMute(); iToggleMute();
break; break;
case "Escape": case "Escape":
e.preventDefault(); e.preventDefault();
stop(); iStop();
break; break;
} }
}, },
[ [
togglePlayPause, iTogglePlayPause,
seekTo, iSeekTo,
setVolume, iSetVolume,
toggleMute, iToggleMute,
stop, iStop,
currentTime, currentTime,
duration, duration,
volume, volume,
@@ -292,18 +395,105 @@ export function FloatingPlayer() {
const hasNext = currentIndex >= 0 && currentIndex < playlist.length - 1; const hasNext = currentIndex >= 0 && currentIndex < playlist.length - 1;
const hasPrev = currentIndex > 0; const hasPrev = currentIndex > 0;
const coverSrc = currentTrack.coverImage || FALLBACK_COVER; const coverSrc = currentTrack.coverImage || FALLBACK_COVER;
const progress = duration > 0 ? (currentTime / duration) * 100 : 0;
// ── Hide entirely when selection toolbar is active ──
if (selectionToolbarVisible) return null;
// ── Render ──
return ( return (
<div <AnimatePresence mode="popLayout">
{isMobile && minimized ? (
<motion.button
key="mini-pill"
data-audio-player
type="button"
onClick={() => {
setMinimized(false);
resetTimer();
}}
aria-label="Expand audio player"
initial={{ opacity: 0, scale: 0.5, bottom: -60 }}
animate={{
opacity: 1,
scale: 1,
bottom: selectionToolbarVisible ? 90 : 24,
}}
exit={{ opacity: 0, scale: 0.5, bottom: -60 }}
transition={{ type: "spring", damping: 20, stiffness: 400 }}
className={cn(
"fixed z-40 right-4",
"size-14 rounded-full",
"shadow-2xl shadow-black/30",
"overflow-hidden",
"ring-2 ring-white/20",
)}
>
{/* Cover image */}
<Image
src={coverSrc}
alt=""
fill
className="object-cover"
unoptimized
/>
{/* Dark overlay */}
<div className="absolute inset-0 bg-black/40" />
{/* Circular progress ring */}
<svg
className="absolute inset-0 size-full -rotate-90"
viewBox="0 0 56 56"
>
<circle
cx="28"
cy="28"
r="26"
fill="none"
stroke="white"
strokeOpacity={0.15}
strokeWidth="2"
/>
<circle
cx="28"
cy="28"
r="26"
fill="none"
stroke="white"
strokeOpacity={0.8}
strokeWidth="2"
strokeDasharray={2 * Math.PI * 26}
strokeDashoffset={2 * Math.PI * 26 * (1 - progress / 100)}
strokeLinecap="round"
className="transition-[stroke-dashoffset] duration-300"
/>
</svg>
{/* Audio wave indicator */}
<div className="absolute inset-0 flex items-center justify-center">
<AudioWaveAnimation
paused={!isPlaying}
colorClassName="bg-white/90"
/>
</div>
</motion.button>
) : (
<motion.div
key="full-player"
ref={playerRef} ref={playerRef}
data-audio-player data-audio-player
role="region" role="region"
aria-label="Audio player" aria-label="Audio player"
tabIndex={0} tabIndex={0}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
initial={{ opacity: 0, bottom: -80 }}
animate={{
opacity: 1,
bottom: selectionToolbarVisible ? 64 : 20,
}}
exit={{ opacity: 0, bottom: -80 }}
transition={{ type: "spring", damping: 25, stiffness: 300 }}
className={cn( className={cn(
// Centered floating bar // Centered floating bar
"fixed z-40 bottom-5 left-1/2 -translate-x-1/2", "fixed z-40 left-1/2 -translate-x-1/2",
// Narrower width // Narrower width
"w-[calc(100%-2rem)] max-w-[680px]", "w-[calc(100%-2rem)] max-w-[680px]",
// Forced dark surface for contrast regardless of theme // Forced dark surface for contrast regardless of theme
@@ -311,8 +501,6 @@ export function FloatingPlayer() {
"border border-white/10", "border border-white/10",
"rounded-2xl", "rounded-2xl",
"shadow-2xl shadow-black/25", "shadow-2xl shadow-black/25",
// Animation
"animate-in fade-in-0 slide-in-from-bottom-4 duration-300",
// Clip seeker to rounded top corners // Clip seeker to rounded top corners
"overflow-hidden", "overflow-hidden",
)} )}
@@ -321,7 +509,7 @@ export function FloatingPlayer() {
<ProgressBar <ProgressBar
currentTime={currentTime} currentTime={currentTime}
duration={duration} duration={duration}
onSeek={seekTo} onSeek={iSeekTo}
thin thin
/> />
@@ -364,7 +552,7 @@ export function FloatingPlayer() {
{/* Center: Transport controls */} {/* Center: Transport controls */}
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<TransportButton <TransportButton
onClick={previous} onClick={iPrevious}
disabled={!hasPrev && currentTime <= 3} disabled={!hasPrev && currentTime <= 3}
label="Previous track" label="Previous track"
className="text-white/60 hover:text-white hover:bg-white/10" className="text-white/60 hover:text-white hover:bg-white/10"
@@ -373,7 +561,7 @@ export function FloatingPlayer() {
</TransportButton> </TransportButton>
<TransportButton <TransportButton
onClick={togglePlayPause} onClick={iTogglePlayPause}
label={isPlaying ? "Pause" : "Play"} label={isPlaying ? "Pause" : "Play"}
size="large" size="large"
className="bg-white text-black hover:bg-white/90" className="bg-white text-black hover:bg-white/90"
@@ -388,7 +576,7 @@ export function FloatingPlayer() {
</TransportButton> </TransportButton>
<TransportButton <TransportButton
onClick={next} onClick={iNext}
disabled={!hasNext} disabled={!hasNext}
label="Next track" label="Next track"
className="text-white/60 hover:text-white hover:bg-white/10" className="text-white/60 hover:text-white hover:bg-white/10"
@@ -403,7 +591,7 @@ export function FloatingPlayer() {
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<button <button
type="button" type="button"
onClick={toggleMute} onClick={iToggleMute}
aria-label={isMuted ? "Unmute" : "Mute"} aria-label={isMuted ? "Unmute" : "Mute"}
className="size-7 inline-flex items-center justify-center rounded-md text-white/50 hover:text-white hover:bg-white/10 transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring" className="size-7 inline-flex items-center justify-center rounded-md text-white/50 hover:text-white hover:bg-white/10 transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring"
> >
@@ -419,7 +607,7 @@ export function FloatingPlayer() {
max={100} max={100}
step={1} step={1}
value={isMuted ? 0 : Math.round(volume * 100)} value={isMuted ? 0 : Math.round(volume * 100)}
onChange={(e) => setVolume(Number(e.target.value) / 100)} onChange={(e) => iSetVolume(Number(e.target.value) / 100)}
aria-label="Volume" aria-label="Volume"
className={cn( className={cn(
"w-16 h-1 appearance-none bg-white/15 rounded-full cursor-pointer", "w-16 h-1 appearance-none bg-white/15 rounded-full cursor-pointer",
@@ -451,7 +639,7 @@ export function FloatingPlayer() {
{/* Close */} {/* Close */}
<button <button
type="button" type="button"
onClick={stop} onClick={iStop}
aria-label="Close player" aria-label="Close player"
className="size-7 inline-flex items-center justify-center rounded-md text-white/50 hover:text-white hover:bg-white/10 transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring" className="size-7 inline-flex items-center justify-center rounded-md text-white/50 hover:text-white hover:bg-white/10 transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring"
> >
@@ -461,7 +649,10 @@ export function FloatingPlayer() {
</div> </div>
{/* ── Mobile layout ── */} {/* ── Mobile layout ── */}
<div className="flex md:hidden flex-col px-4 py-3 gap-2.5"> <div
className="flex md:hidden flex-col px-4 py-3 gap-2.5"
onPointerDown={resetTimer}
>
{/* Time row — right below seeker */} {/* Time row — right below seeker */}
<div className="flex items-center justify-between px-0.5"> <div className="flex items-center justify-between px-0.5">
<span className="text-[10px] tabular-nums text-white/40 select-none"> <span className="text-[10px] tabular-nums text-white/40 select-none">
@@ -511,7 +702,7 @@ export function FloatingPlayer() {
{/* Close */} {/* Close */}
<button <button
type="button" type="button"
onClick={stop} onClick={iStop}
aria-label="Close player" aria-label="Close player"
className="size-8 inline-flex items-center justify-center rounded-full text-white/50 hover:text-white hover:bg-white/10 transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring shrink-0" className="size-8 inline-flex items-center justify-center rounded-full text-white/50 hover:text-white hover:bg-white/10 transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring shrink-0"
> >
@@ -522,7 +713,7 @@ export function FloatingPlayer() {
{/* Row 2: Prev + Play/Pause + Next — centered */} {/* Row 2: Prev + Play/Pause + Next — centered */}
<div className="flex items-center justify-center gap-4 py-1"> <div className="flex items-center justify-center gap-4 py-1">
<TransportButton <TransportButton
onClick={previous} onClick={iPrevious}
disabled={!hasPrev && currentTime <= 3} disabled={!hasPrev && currentTime <= 3}
label="Previous track" label="Previous track"
className="text-white/60 hover:text-white hover:bg-white/10 !size-10" className="text-white/60 hover:text-white hover:bg-white/10 !size-10"
@@ -531,7 +722,7 @@ export function FloatingPlayer() {
</TransportButton> </TransportButton>
<TransportButton <TransportButton
onClick={togglePlayPause} onClick={iTogglePlayPause}
label={isPlaying ? "Pause" : "Play"} label={isPlaying ? "Pause" : "Play"}
size="large" size="large"
className="bg-white text-black hover:bg-white/90 !size-12" className="bg-white text-black hover:bg-white/90 !size-12"
@@ -546,7 +737,7 @@ export function FloatingPlayer() {
</TransportButton> </TransportButton>
<TransportButton <TransportButton
onClick={next} onClick={iNext}
disabled={!hasNext} disabled={!hasNext}
label="Next track" label="Next track"
className="text-white/60 hover:text-white hover:bg-white/10 !size-10" className="text-white/60 hover:text-white hover:bg-white/10 !size-10"
@@ -555,6 +746,8 @@ export function FloatingPlayer() {
</TransportButton> </TransportButton>
</div> </div>
</div> </div>
</div> </motion.div>
)}
</AnimatePresence>
); );
} }
+1
View File
@@ -1,3 +1,4 @@
export { AudioPlayerProvider, useAudioPlayer } from "./audio-context"; export { AudioPlayerProvider, useAudioPlayer } from "./audio-context";
export type { AudioTrack } from "./audio-context"; export type { AudioTrack } from "./audio-context";
export { FloatingPlayer } from "./floating-player"; export { FloatingPlayer } from "./floating-player";
export { AudioWaveAnimation } from "./audio-wave";
-11440
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -15,6 +15,7 @@
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"dotenv": "^17.4.2", "dotenv": "^17.4.2",
"framer-motion": "^12.38.0",
"lucide-react": "^1.8.0", "lucide-react": "^1.8.0",
"mongodb": "^7.1.1", "mongodb": "^7.1.1",
"next": "16.2.2", "next": "16.2.2",
+623 -103
View File
File diff suppressed because it is too large Load Diff