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 { 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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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 { 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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()}
|
||||
/>
|
||||
)}
|
||||
{tools["Slide"] && (
|
||||
<TldrawUiMenuItem
|
||||
{...tools["Slide"]}
|
||||
icon="slideshow"
|
||||
label="Slideshow"
|
||||
isSelected={tools["Slide"].id === editor.getCurrentToolId()}
|
||||
/>
|
||||
)}
|
||||
</DefaultToolbar>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue