返回 presentation-ai
SlidesContainer.tsx
根目录 / src / components / presentation / slides / SlidesContainer.tsx
1 "use client";
2
3 import { closestCenter, DndContext } from "@dnd-kit/core";
4 import {
5 SortableContext,
6 verticalListSortingStrategy,
7 } from "@dnd-kit/sortable";
8 import { Plus, Presentation } from "lucide-react";
9
10 import { calculateHeightFromRatio } from "@/config/slideFormats";
11 import { usePresentationNavigation } from "@/hooks/presentation/usePresentationNavigation";
12 import { usePresentationSlides } from "@/hooks/presentation/usePresentationSlides";
13 import { usePresentingLoadingGate } from "@/hooks/presentation/usePresentingLoadingGate";
14 import { usePresentModeOrientation } from "@/hooks/presentation/usePresentModeOrientation";
15 import { useSlideChangeWatcher } from "@/hooks/presentation/useSlideChangeWatcher";
16 import { useSlideContentScaling } from "@/hooks/presentation/useSlideContentScaling";
17 import { useSlideOperations } from "@/hooks/presentation/useSlideOperations";
18 import { usePresentationState } from "@/states/presentation-state";
19 import { Button } from "../../ui/button";
20 import { Skeleton } from "../../ui/skeleton";
21 import { PresentModeHeader } from "../present-mode/PresentModeHeader";
22 import { PresentModePhoneOverlay } from "../present-mode/PresentModePhoneOverlay";
23 import { PresentModeProgressBar } from "../present-mode/PresentModeProgressBar";
24 import { PresentationThumbnailCaptureManager } from "./PresentationThumbnailCaptureManager";
25 import { SlideItem } from "./SlideItem";
26
27 interface SlidesContainerProps {
28 isGeneratingPresentation: boolean;
29 isReadOnly?: boolean;
30 }
31
32 export const SlidesContainer = ({
33 isGeneratingPresentation,
34 isReadOnly = false,
35 }: SlidesContainerProps) => {
36 const isPresenting = usePresentationState((s) => s.isPresenting);
37 const isPresentingLoading = usePresentationState(
38 (s) => s.isPresentingLoading,
39 );
40 const zoomLevel = usePresentationState((s) => s.zoomLevel);
41 const presentingScaleLocks = usePresentationState(
42 (s) => s.presentingScaleLocks,
43 );
44 const setIsPresentingLoading = usePresentationState(
45 (s) => s.setIsPresentingLoading,
46 );
47 const currentPresentationTitle = usePresentationState(
48 (s) => s.currentPresentationTitle,
49 );
50 const shouldShowExitHeader = usePresentationState(
51 (s) => s.shouldShowExitHeader,
52 );
53
54 // Use slideIds for rendering to prevent re-renders when slide content changes
55 const { slideIds, items, sensors, handleDragStart, handleDragEnd } =
56 usePresentationSlides();
57 const { addFirstSlide } = useSlideOperations();
58
59 // Use the slide change watcher to automatically save changes (disabled during generation)
60 useSlideChangeWatcher({
61 debounceDelay: 600,
62 enabled: !isGeneratingPresentation && !isReadOnly,
63 });
64
65 // Handle presentation navigation (keyboard and mouse)
66 usePresentationNavigation();
67 const { isPhoneViewport } = usePresentModeOrientation(isPresenting);
68
69 const slidesCount = slideIds.length;
70 const generatingPlaceholderScaling = useSlideContentScaling(
71 "M",
72 false,
73 "presentation",
74 { type: "ratio", value: "16:9" },
75 undefined,
76 zoomLevel,
77 );
78 const generatingPlaceholderWidth = Math.round(
79 generatingPlaceholderScaling.slideWidth *
80 Math.max(generatingPlaceholderScaling.scale, 0.1),
81 );
82 const generatingPlaceholderHeight = Math.round(
83 (calculateHeightFromRatio(generatingPlaceholderScaling.slideWidth, {
84 type: "ratio",
85 value: "16:9",
86 }).minHeightPx ?? 0) * Math.max(generatingPlaceholderScaling.scale, 0.1),
87 );
88
89 usePresentingLoadingGate({
90 isPresenting,
91 isPresentingLoading,
92 slideIds,
93 presentingScaleLocks,
94 setIsPresentingLoading,
95 clearDelayMs: 150,
96 });
97
98 return (
99 <DndContext
100 sensors={sensors}
101 collisionDetection={closestCenter}
102 onDragStart={handleDragStart}
103 onDragEnd={handleDragEnd}
104 >
105 <SortableContext items={items} strategy={verticalListSortingStrategy}>
106 {!isReadOnly && <PresentationThumbnailCaptureManager />}
107 <PresentModeHeader
108 presentationTitle={currentPresentationTitle}
109 showHeader={isPresenting && shouldShowExitHeader}
110 />
111 {isGeneratingPresentation && slidesCount === 0 && (
112 <div className="flex w-full justify-center">
113 <div
114 className="relative max-w-full"
115 style={{
116 width: `${generatingPlaceholderWidth}px`,
117 height: `${generatingPlaceholderHeight}px`,
118 }}
119 >
120 <Skeleton className="h-full w-full rounded-md" />
121 </div>
122 </div>
123 )}
124 {!isGeneratingPresentation &&
125 !isPresenting &&
126 slidesCount === 0 &&
127 !isReadOnly && (
128 <div className="mx-auto w-full max-w-5xl">
129 <div className="relative flex aspect-video w-full flex-col items-center justify-center rounded-2xl border border-dashed border-border/80 bg-background/85 p-8 text-center shadow">
130 <div className="absolute inset-5 rounded-xl border border-dashed border-border/50" />
131 <div className="relative z-10 flex flex-col items-center gap-4">
132 <span className="rounded-full border border-border/70 bg-muted/70 p-3">
133 <Presentation className="size-6 text-muted-foreground" />
134 </span>
135 <div className="space-y-1">
136 <p className="text-base font-semibold">
137 No slides yet
138 </p>
139 <p className="text-sm text-muted-foreground">
140 Start this presentation by adding your first slide.
141 </p>
142 </div>
143 <Button
144 onClick={() => addFirstSlide()}
145 className="gap-2"
146 size="sm"
147 >
148 <Plus className="size-4" />
149 Add first slide
150 </Button>
151 </div>
152 </div>
153 </div>
154 )}
155 {slideIds.map((slideId) => (
156 <SlideItem
157 key={slideId}
158 slideId={slideId}
159 isGeneratingPresentation={isGeneratingPresentation}
160 slidesCount={slidesCount}
161 isReadOnly={isReadOnly}
162 />
163 ))}
164
165 {isPresenting && (
166 <PresentModePhoneOverlay
167 slideIds={slideIds}
168 isPhoneViewport={isPhoneViewport}
169 />
170 )}
171 {isPresenting && <PresentModeProgressBar slideIds={slideIds} />}
172 </SortableContext>
173 </DndContext>
174 );
175 };
176
176 lines Plain Text