add slideshow tool (not finished)
This commit is contained in:
parent
9f54400f18
commit
7631f832b7
11
src/App.tsx
11
src/App.tsx
|
|
@ -21,6 +21,8 @@ import { createRoot } from "react-dom/client"
|
||||||
import { handleInitialPageLoad } from "./utils/handleInitialPageLoad"
|
import { handleInitialPageLoad } from "./utils/handleInitialPageLoad"
|
||||||
import { DailyProvider } from "@daily-co/daily-react"
|
import { DailyProvider } from "@daily-co/daily-react"
|
||||||
import Daily from "@daily-co/daily-js"
|
import Daily from "@daily-co/daily-js"
|
||||||
|
import { SlideTool } from "./tools/SlideTool"
|
||||||
|
import { SlideShape } from "./shapes/SlideShapeUtil"
|
||||||
|
|
||||||
inject()
|
inject()
|
||||||
|
|
||||||
|
|
@ -29,8 +31,15 @@ const customShapeUtils = [
|
||||||
VideoChatShape,
|
VideoChatShape,
|
||||||
EmbedShape,
|
EmbedShape,
|
||||||
MarkdownShape,
|
MarkdownShape,
|
||||||
|
SlideShape,
|
||||||
|
]
|
||||||
|
const customTools = [
|
||||||
|
ChatBoxTool,
|
||||||
|
VideoChatTool,
|
||||||
|
EmbedTool,
|
||||||
|
MarkdownTool,
|
||||||
|
SlideTool,
|
||||||
]
|
]
|
||||||
const customTools = [ChatBoxTool, VideoChatTool, EmbedTool, MarkdownTool]
|
|
||||||
|
|
||||||
const callObject = Daily.createCallObject()
|
const callObject = Daily.createCallObject()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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")
|
||||||
|
}
|
||||||
|
|
@ -17,12 +17,22 @@ import { components } from "@/ui/components"
|
||||||
import { overrides } from "@/ui/overrides"
|
import { overrides } from "@/ui/overrides"
|
||||||
import { unfurlBookmarkUrl } from "../utils/unfurlBookmarkUrl"
|
import { unfurlBookmarkUrl } from "../utils/unfurlBookmarkUrl"
|
||||||
import { handleInitialPageLoad } from "@/utils/handleInitialPageLoad"
|
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
|
// Default to production URL if env var isn't available
|
||||||
export const WORKER_URL = "https://jeffemmett-canvas.jeffemmett.workers.dev"
|
export const WORKER_URL = "https://jeffemmett-canvas.jeffemmett.workers.dev"
|
||||||
|
|
||||||
const shapeUtils = [ChatBoxShape, VideoChatShape, EmbedShape, MarkdownShape]
|
const shapeUtils = [
|
||||||
const tools = [ChatBoxTool, VideoChatTool, EmbedTool, MarkdownTool] // Array of tools
|
ChatBoxShape,
|
||||||
|
VideoChatShape,
|
||||||
|
EmbedShape,
|
||||||
|
MarkdownShape,
|
||||||
|
SlideShape,
|
||||||
|
]
|
||||||
|
const tools = [ChatBoxTool, VideoChatTool, EmbedTool, MarkdownTool, SlideTool]
|
||||||
|
|
||||||
export function Board() {
|
export function Board() {
|
||||||
const { slug } = useParams<{ slug: string }>()
|
const { slug } = useParams<{ slug: string }>()
|
||||||
|
|
@ -49,14 +59,16 @@ export function Board() {
|
||||||
tools={tools}
|
tools={tools}
|
||||||
components={components}
|
components={components}
|
||||||
overrides={overrides}
|
overrides={overrides}
|
||||||
//maxZoom={20}
|
|
||||||
onMount={(editor) => {
|
onMount={(editor) => {
|
||||||
setEditor(editor)
|
setEditor(editor)
|
||||||
editor.registerExternalAssetHandler("url", unfurlBookmarkUrl)
|
editor.registerExternalAssetHandler("url", unfurlBookmarkUrl)
|
||||||
editor.setCurrentTool("hand")
|
editor.setCurrentTool("hand")
|
||||||
handleInitialPageLoad(editor)
|
handleInitialPageLoad(editor)
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
<SlideControls />
|
||||||
|
{editor?.getCurrentTool().id === "Slide" && <SlidesPanel />}
|
||||||
|
</Tldraw>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { BaseBoxShapeTool } from "tldraw"
|
||||||
|
|
||||||
|
export class SlideTool extends BaseBoxShapeTool {
|
||||||
|
static override id = "Slide"
|
||||||
|
shapeType = "Slide"
|
||||||
|
override initial = "idle"
|
||||||
|
}
|
||||||
|
|
@ -52,6 +52,14 @@ export function CustomToolbar() {
|
||||||
isSelected={tools["Markdown"].id === editor.getCurrentToolId()}
|
isSelected={tools["Markdown"].id === editor.getCurrentToolId()}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{tools["Slide"] && (
|
||||||
|
<TldrawUiMenuItem
|
||||||
|
{...tools["Slide"]}
|
||||||
|
icon="slideshow"
|
||||||
|
label="Slideshow"
|
||||||
|
isSelected={tools["Slide"].id === editor.getCurrentToolId()}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</DefaultToolbar>
|
</DefaultToolbar>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,14 @@ export const overrides: TLUiOverrides = {
|
||||||
readonlyOk: true,
|
readonlyOk: true,
|
||||||
onSelect: () => editor.setCurrentTool("Markdown"),
|
onSelect: () => editor.setCurrentTool("Markdown"),
|
||||||
},
|
},
|
||||||
|
Slide: {
|
||||||
|
id: "Slide",
|
||||||
|
icon: "slideshow",
|
||||||
|
label: "Slideshow",
|
||||||
|
kbd: "alt+s",
|
||||||
|
readonlyOk: true,
|
||||||
|
onSelect: () => editor.setCurrentTool("Slide"),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions(editor, actions) {
|
actions(editor, actions) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue