From 7631f832b703d89aab7d9ffe19c6f8dbdaa97069 Mon Sep 17 00:00:00 2001
From: Jeff Emmett <46964190+Jeff-Emmett@users.noreply.github.com>
Date: Tue, 10 Dec 2024 12:05:51 -0500
Subject: [PATCH] add slideshow tool (not finished)
---
src/App.tsx | 11 +++++-
src/components/SlideControls.tsx | 66 ++++++++++++++++++++++++++++++++
src/components/SlidesPanel.tsx | 45 ++++++++++++++++++++++
src/css/slides.css | 61 +++++++++++++++++++++++++++++
src/hooks/useSlide.ts | 39 +++++++++++++++++++
src/routes/Board.tsx | 20 ++++++++--
src/shapes/SlideShapeUtil.tsx | 30 +++++++++++++++
src/tools/SlideTool.ts | 7 ++++
src/ui/CustomToolbar.tsx | 8 ++++
src/ui/overrides.tsx | 8 ++++
10 files changed, 290 insertions(+), 5 deletions(-)
create mode 100644 src/components/SlideControls.tsx
create mode 100644 src/components/SlidesPanel.tsx
create mode 100644 src/css/slides.css
create mode 100644 src/hooks/useSlide.ts
create mode 100644 src/shapes/SlideShapeUtil.tsx
create mode 100644 src/tools/SlideTool.ts
diff --git a/src/App.tsx b/src/App.tsx
index f083919..3cc8b43 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -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()
diff --git a/src/components/SlideControls.tsx b/src/components/SlideControls.tsx
new file mode 100644
index 0000000..6ec32a8
--- /dev/null
+++ b/src/components/SlideControls.tsx
@@ -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 (
+
+
+
+ {currentIndex + 1} / {slides.length}
+
+
+
+ )
+}
diff --git a/src/components/SlidesPanel.tsx b/src/components/SlidesPanel.tsx
new file mode 100644
index 0000000..0100c07
--- /dev/null
+++ b/src/components/SlidesPanel.tsx
@@ -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 (
+
+
+ Slides ({slides.length})
+
+ {slides.map((slide, i) => (
+
moveToSlide(editor, slide)}
+ >
+ Slide {i + 1}
+
+ ))}
+
+ )
+}
diff --git a/src/css/slides.css b/src/css/slides.css
new file mode 100644
index 0000000..5a229e1
--- /dev/null
+++ b/src/css/slides.css
@@ -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);
+}
\ No newline at end of file
diff --git a/src/hooks/useSlide.ts b/src/hooks/useSlide.ts
new file mode 100644
index 0000000..8eea7b7
--- /dev/null
+++ b/src/hooks/useSlide.ts
@@ -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("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("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")
+}
diff --git a/src/routes/Board.tsx b/src/routes/Board.tsx
index 503e42d..d0b3e44 100644
--- a/src/routes/Board.tsx
+++ b/src/routes/Board.tsx
@@ -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)
}}
- />
+ >
+
+ {editor?.getCurrentTool().id === "Slide" && }
+
)
}
diff --git a/src/shapes/SlideShapeUtil.tsx b/src/shapes/SlideShapeUtil.tsx
new file mode 100644
index 0000000..575451e
--- /dev/null
+++ b/src/shapes/SlideShapeUtil.tsx
@@ -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 {
+ 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
+ }
+}
diff --git a/src/tools/SlideTool.ts b/src/tools/SlideTool.ts
new file mode 100644
index 0000000..1c7f065
--- /dev/null
+++ b/src/tools/SlideTool.ts
@@ -0,0 +1,7 @@
+import { BaseBoxShapeTool } from "tldraw"
+
+export class SlideTool extends BaseBoxShapeTool {
+ static override id = "Slide"
+ shapeType = "Slide"
+ override initial = "idle"
+}
diff --git a/src/ui/CustomToolbar.tsx b/src/ui/CustomToolbar.tsx
index 1b974c6..264d249 100644
--- a/src/ui/CustomToolbar.tsx
+++ b/src/ui/CustomToolbar.tsx
@@ -52,6 +52,14 @@ export function CustomToolbar() {
isSelected={tools["Markdown"].id === editor.getCurrentToolId()}
/>
)}
+ {tools["Slide"] && (
+
+ )}
)
}
diff --git a/src/ui/overrides.tsx b/src/ui/overrides.tsx
index d4a276c..97b5cb1 100644
--- a/src/ui/overrides.tsx
+++ b/src/ui/overrides.tsx
@@ -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) {