add slideshow tool (not finished)

This commit is contained in:
Jeff Emmett 2024-12-10 12:05:51 -05:00
parent 9f54400f18
commit 7631f832b7
10 changed files with 290 additions and 5 deletions

View File

@ -21,6 +21,8 @@ import { createRoot } from "react-dom/client"
import { handleInitialPageLoad } from "./utils/handleInitialPageLoad"
import { DailyProvider } from "@daily-co/daily-react"
import Daily from "@daily-co/daily-js"
import { SlideTool } from "./tools/SlideTool"
import { SlideShape } from "./shapes/SlideShapeUtil"
inject()
@ -29,8 +31,15 @@ const customShapeUtils = [
VideoChatShape,
EmbedShape,
MarkdownShape,
SlideShape,
]
const customTools = [
ChatBoxTool,
VideoChatTool,
EmbedTool,
MarkdownTool,
SlideTool,
]
const customTools = [ChatBoxTool, VideoChatTool, EmbedTool, MarkdownTool]
const callObject = Daily.createCallObject()

View File

@ -0,0 +1,66 @@
import { useCallback, useEffect } from "react"
import { useEditor } from "tldraw"
import { useSlides, useCurrentSlide, moveToSlide } from "../hooks/useSlide"
export function SlideControls() {
const editor = useEditor()
const slides = useSlides()
const currentSlide = useCurrentSlide()
const currentIndex = currentSlide ? slides.indexOf(currentSlide) : -1
const nextSlide = useCallback(() => {
if (currentIndex < slides.length - 1) {
moveToSlide(editor, slides[currentIndex + 1])
}
}, [editor, slides, currentIndex])
const previousSlide = useCallback(() => {
if (currentIndex > 0) {
moveToSlide(editor, slides[currentIndex - 1])
}
}, [editor, slides, currentIndex])
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
if (e.key === "ArrowRight") nextSlide()
if (e.key === "ArrowLeft") previousSlide()
},
[nextSlide, previousSlide],
)
useEffect(() => {
window.addEventListener("keydown", handleKeyDown)
return () => window.removeEventListener("keydown", handleKeyDown)
}, [handleKeyDown])
if (slides.length === 0) return null
return (
<div
style={{
position: "fixed",
bottom: 20,
left: "50%",
transform: "translateX(-50%)",
background: "white",
padding: "8px 16px",
borderRadius: "8px",
boxShadow: "0 2px 4px rgba(0,0,0,0.1)",
display: "flex",
gap: "8px",
alignItems: "center",
}}
>
<button onClick={previousSlide} disabled={currentIndex === 0}>
</button>
<div>
{currentIndex + 1} / {slides.length}
</div>
<button onClick={nextSlide} disabled={currentIndex === slides.length - 1}>
</button>
</div>
)
}

View File

@ -0,0 +1,45 @@
import { useEditor } from "tldraw"
import { useSlides, useCurrentSlide, moveToSlide } from "../hooks/useSlide"
export function SlidesPanel() {
const editor = useEditor()
const slides = useSlides()
const currentSlide = useCurrentSlide()
return (
<div
style={{
position: "absolute",
right: 0,
top: 0,
height: "100%",
width: "240px",
background: "white",
boxShadow: "-1px 0 0 rgba(0,0,0,.1)",
display: "flex",
flexDirection: "column",
gap: "8px",
padding: "8px",
overflowY: "auto",
}}
>
<div style={{ fontWeight: "bold", padding: "8px" }}>
Slides ({slides.length})
</div>
{slides.map((slide, i) => (
<div
key={slide.id}
style={{
padding: "8px",
cursor: "pointer",
background: currentSlide === slide ? "#e0e0e0" : "transparent",
borderRadius: "4px",
}}
onClick={() => moveToSlide(editor, slide)}
>
Slide {i + 1}
</div>
))}
</div>
)
}

61
src/css/slides.css Normal file
View File

@ -0,0 +1,61 @@
.slides-panel {
display: flex;
flex-direction: column;
gap: 4px;
max-height: calc(100% - 110px);
margin: 50px 0px;
padding: 4px;
background-color: var(--color-low);
pointer-events: all;
border-top-right-radius: var(--radius-4);
border-bottom-right-radius: var(--radius-4);
overflow: auto;
border-right: 2px solid var(--color-background);
border-bottom: 2px solid var(--color-background);
border-top: 2px solid var(--color-background);
}
.slides-panel-button {
border-radius: var(--radius-4);
outline-offset: -1px;
cursor: pointer;
padding: 8px;
transition: background-color 0.2s ease;
}
.slides-panel-button:hover {
background-color: var(--color-hover);
}
.slides-panel-button.selected {
background-color: var(--color-selected);
}
.slide-shape-label {
pointer-events: all;
position: absolute;
background: var(--color-low);
padding: calc(12px * var(--tl-scale));
border-bottom-right-radius: calc(var(--radius-4) * var(--tl-scale));
font-size: calc(12px * var(--tl-scale));
color: var(--color-text);
white-space: nowrap;
}
/* Scrollbar styling */
.slides-panel::-webkit-scrollbar {
width: 8px;
}
.slides-panel::-webkit-scrollbar-track {
background: var(--color-low);
}
.slides-panel::-webkit-scrollbar-thumb {
background: var(--color-divider);
border-radius: 4px;
}
.slides-panel::-webkit-scrollbar-thumb:hover {
background: var(--color-text-3);
}

39
src/hooks/useSlide.ts Normal file
View File

@ -0,0 +1,39 @@
import { EASINGS, Editor, atom, useEditor, useValue } from "tldraw"
import { TLFrameShape } from "tldraw"
// Create an atom for current slide state
export const $currentSlide = atom<TLFrameShape | null>("current slide", null)
// Function to move to a specific slide
export function moveToSlide(editor: Editor, frame: TLFrameShape) {
const bounds = editor.getShapePageBounds(frame.id)
if (!bounds) return
$currentSlide.set(frame)
editor.selectNone()
editor.zoomToBounds(bounds, {
animation: { duration: 500, easing: EASINGS.easeInOutCubic },
inset: 0,
})
}
// Hook to get all slides (frames)
export function useSlides() {
const editor = useEditor()
return useValue<TLFrameShape[]>("frame shapes", () => getSlides(editor), [
editor,
])
}
// Hook to get current slide
export function useCurrentSlide() {
return useValue($currentSlide)
}
// Helper to get all slides
export function getSlides(editor: Editor) {
return editor
.getSortedChildIdsForParent(editor.getCurrentPageId())
.map((id) => editor.getShape(id))
.filter((s): s is TLFrameShape => s?.type === "frame")
}

View File

@ -17,12 +17,22 @@ import { components } from "@/ui/components"
import { overrides } from "@/ui/overrides"
import { unfurlBookmarkUrl } from "../utils/unfurlBookmarkUrl"
import { handleInitialPageLoad } from "@/utils/handleInitialPageLoad"
import { SlideControls } from "@/components/SlideControls"
import { SlideShape } from "@/shapes/SlideShapeUtil"
import { SlideTool } from "@/tools/SlideTool"
import { SlidesPanel } from "@/components/SlidesPanel"
// Default to production URL if env var isn't available
export const WORKER_URL = "https://jeffemmett-canvas.jeffemmett.workers.dev"
const shapeUtils = [ChatBoxShape, VideoChatShape, EmbedShape, MarkdownShape]
const tools = [ChatBoxTool, VideoChatTool, EmbedTool, MarkdownTool] // Array of tools
const shapeUtils = [
ChatBoxShape,
VideoChatShape,
EmbedShape,
MarkdownShape,
SlideShape,
]
const tools = [ChatBoxTool, VideoChatTool, EmbedTool, MarkdownTool, SlideTool]
export function Board() {
const { slug } = useParams<{ slug: string }>()
@ -49,14 +59,16 @@ export function Board() {
tools={tools}
components={components}
overrides={overrides}
//maxZoom={20}
onMount={(editor) => {
setEditor(editor)
editor.registerExternalAssetHandler("url", unfurlBookmarkUrl)
editor.setCurrentTool("hand")
handleInitialPageLoad(editor)
}}
/>
>
<SlideControls />
{editor?.getCurrentTool().id === "Slide" && <SlidesPanel />}
</Tldraw>
</div>
)
}

View File

@ -0,0 +1,30 @@
import { BaseBoxShapeUtil, TLBaseBoxShape, TLBaseShape } from "tldraw"
export type ISlideShape = TLBaseShape<
"Slide",
{
w: number
h: number
currentSlide: number
}
>
export class SlideShape extends BaseBoxShapeUtil<ISlideShape & TLBaseBoxShape> {
static override type = "Slide"
getDefaultProps(): ISlideShape["props"] {
return {
w: 720, // Standard slide width
h: 405, // 16:9 aspect ratio
currentSlide: 0,
}
}
component(shape: ISlideShape) {
return null // Slides don't need visual representation
}
indicator(shape: ISlideShape & TLBaseBoxShape) {
return null
}
}

7
src/tools/SlideTool.ts Normal file
View File

@ -0,0 +1,7 @@
import { BaseBoxShapeTool } from "tldraw"
export class SlideTool extends BaseBoxShapeTool {
static override id = "Slide"
shapeType = "Slide"
override initial = "idle"
}

View File

@ -52,6 +52,14 @@ export function CustomToolbar() {
isSelected={tools["Markdown"].id === editor.getCurrentToolId()}
/>
)}
{tools["Slide"] && (
<TldrawUiMenuItem
{...tools["Slide"]}
icon="slideshow"
label="Slideshow"
isSelected={tools["Slide"].id === editor.getCurrentToolId()}
/>
)}
</DefaultToolbar>
)
}

View File

@ -44,6 +44,14 @@ export const overrides: TLUiOverrides = {
readonlyOk: true,
onSelect: () => editor.setCurrentTool("Markdown"),
},
Slide: {
id: "Slide",
icon: "slideshow",
label: "Slideshow",
kbd: "alt+s",
readonlyOk: true,
onSelect: () => editor.setCurrentTool("Slide"),
},
}
},
actions(editor, actions) {