mirror of
https://github.com/zadam/trilium.git
synced 2025-10-28 10:08:52 +01:00
191 lines
6.6 KiB
TypeScript
191 lines
6.6 KiB
TypeScript
import { ViewModeProps } from "../interface";
|
|
import { useEffect, useLayoutEffect, useRef, useState } from "preact/hooks";
|
|
import Reveal from "reveal.js";
|
|
import slideBaseStylesheet from "reveal.js/dist/reveal.css?raw";
|
|
import slideCustomStylesheet from "./slidejs.css?raw";
|
|
import { buildPresentationModel, PresentationModel, PresentationSlideBaseModel } from "./model";
|
|
import ShadowDom from "../../react/ShadowDom";
|
|
import ActionButton from "../../react/ActionButton";
|
|
import "./index.css";
|
|
import { RefObject } from "preact";
|
|
import { openInCurrentNoteContext } from "../../../components/note_context";
|
|
import { useNoteLabelWithDefault, useTriliumEvent } from "../../react/hooks";
|
|
import { t } from "../../../services/i18n";
|
|
import { DEFAULT_THEME, loadPresentationTheme } from "./themes";
|
|
import FNote from "../../../entities/fnote";
|
|
|
|
export default function PresentationView({ note, noteIds }: ViewModeProps<{}>) {
|
|
const [ presentation, setPresentation ] = useState<PresentationModel>();
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
const [ api, setApi ] = useState<Reveal.Api>();
|
|
const stylesheets = usePresentationStylesheets(note);
|
|
|
|
function refresh() {
|
|
buildPresentationModel(note).then(setPresentation);
|
|
}
|
|
|
|
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
|
if (loadResults.getNoteIds().find(noteId => noteIds.includes(noteId))) {
|
|
refresh();
|
|
}
|
|
});
|
|
|
|
useLayoutEffect(refresh, [ note, noteIds ]);
|
|
|
|
return presentation && stylesheets && (
|
|
<>
|
|
<ShadowDom
|
|
className="presentation-container"
|
|
containerRef={containerRef}
|
|
>
|
|
{stylesheets.map(stylesheet => <style>{stylesheet}</style>)}
|
|
<Presentation presentation={presentation} setApi={setApi} />
|
|
</ShadowDom>
|
|
<ButtonOverlay containerRef={containerRef} api={api} />
|
|
</>
|
|
)
|
|
}
|
|
|
|
function usePresentationStylesheets(note: FNote) {
|
|
const [ themeName ] = useNoteLabelWithDefault(note, "presentation:theme", DEFAULT_THEME);
|
|
const [ stylesheets, setStylesheets ] = useState<string[]>();
|
|
|
|
useLayoutEffect(() => {
|
|
loadPresentationTheme(themeName).then((themeStylesheet) => {
|
|
setStylesheets([
|
|
slideBaseStylesheet,
|
|
themeStylesheet,
|
|
slideCustomStylesheet
|
|
].map(stylesheet => stylesheet.replace(/:root/g, ":host")));
|
|
});
|
|
}, [ themeName ]);
|
|
|
|
return stylesheets;
|
|
}
|
|
|
|
function ButtonOverlay({ containerRef, api }: { containerRef: RefObject<HTMLDivElement>, api: Reveal.Api | undefined }) {
|
|
const [ isOverviewActive, setIsOverviewActive ] = useState(false);
|
|
useEffect(() => {
|
|
if (!api) return;
|
|
setIsOverviewActive(api.isOverview());
|
|
const onEnabled = () => setIsOverviewActive(true);
|
|
const onDisabled = () => setIsOverviewActive(false);
|
|
api.on("overviewshown", onEnabled);
|
|
api.on("overviewhidden", onDisabled);
|
|
return () => {
|
|
api.off("overviewshown", onEnabled);
|
|
api.off("overviewhidden", onDisabled);
|
|
};
|
|
}, [ api ]);
|
|
|
|
return (
|
|
<div className="presentation-button-bar">
|
|
<div className="floating-buttons-children">
|
|
<ActionButton
|
|
className="floating-button"
|
|
icon="bx bx-edit"
|
|
text={t("presentation_view.edit-slide")}
|
|
noIconActionClass
|
|
onClick={e => {
|
|
const currentSlide = api?.getCurrentSlide();
|
|
const noteId = getNoteIdFromSlide(currentSlide);
|
|
|
|
if (noteId) {
|
|
openInCurrentNoteContext(e, noteId);
|
|
}
|
|
}}
|
|
/>
|
|
|
|
<ActionButton
|
|
className="floating-button"
|
|
icon="bx bx-grid-horizontal"
|
|
text={t("presentation_view.slide-overview")}
|
|
active={isOverviewActive}
|
|
noIconActionClass
|
|
onClick={() => api?.toggleOverview()}
|
|
/>
|
|
|
|
<ActionButton
|
|
className="floating-button"
|
|
icon="bx bx-fullscreen"
|
|
text={t("presentation_view.start-presentation")}
|
|
noIconActionClass
|
|
onClick={() => containerRef.current?.requestFullscreen()}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function Presentation({ presentation, setApi } : { presentation: PresentationModel, setApi: (api: Reveal.Api | undefined) => void }) {
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
const [revealApi, setRevealApi] = useState<Reveal.Api>();
|
|
|
|
useEffect(() => {
|
|
if (!containerRef.current) return;
|
|
|
|
const api = new Reveal(containerRef.current, {
|
|
transition: "slide",
|
|
embedded: true,
|
|
keyboardCondition(event) {
|
|
// Full-screen requests sometimes fail, we rely on the UI button instead.
|
|
if (event.key === "f") {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
},
|
|
});
|
|
api.initialize().then(() => {
|
|
setRevealApi(api);
|
|
setApi(api);
|
|
});
|
|
|
|
return () => {
|
|
api.destroy();
|
|
setRevealApi(undefined);
|
|
setApi(undefined);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
revealApi?.sync();
|
|
}, [ presentation, revealApi ]);
|
|
|
|
return (
|
|
<div ref={containerRef} className="reveal">
|
|
<div className="slides">
|
|
{presentation.slides?.map(slide => {
|
|
if (!slide.verticalSlides) {
|
|
return <Slide key={slide.noteId} slide={slide} />
|
|
} else {
|
|
return (
|
|
<section>
|
|
<Slide key={slide.noteId} slide={slide} />
|
|
{slide.verticalSlides.map(slide => <Slide key={slide.noteId} slide={slide} /> )}
|
|
</section>
|
|
);
|
|
}
|
|
})}
|
|
</div>
|
|
</div>
|
|
)
|
|
|
|
}
|
|
|
|
function Slide({ slide }: { slide: PresentationSlideBaseModel }) {
|
|
return (
|
|
<section
|
|
data-note-id={slide.noteId}
|
|
data-background-color={slide.backgroundColor}
|
|
data-background-gradient={slide.backgroundGradient}
|
|
dangerouslySetInnerHTML={slide.content}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function getNoteIdFromSlide(slide: HTMLElement | undefined) {
|
|
if (!slide) return;
|
|
return slide.dataset.noteId;
|
|
}
|