diff --git a/index.ts b/index.ts index 3527946..cbe8172 100644 --- a/index.ts +++ b/index.ts @@ -1,13 +1,13 @@ -import { app, BrowserWindow } from "electron"; +import { app, BrowserWindow, protocol } from "electron"; import { join } from "path"; import { createArchive } from "./src/main/createArchive"; import { openFileDialog, saveFileDialog } from "./src/main/dialogs"; - +import url from "url" import { deleteCodePrezTempFolder, openCodePrezArchive, } from "./src/main/openAndCloseCodePrezFiles"; -import { markdownHighlight } from "./src/renderer/utils/utils"; +import { markdownRenderer } from "./src/main/markdownRenderer" const createWindow = () => { const win = new BrowserWindow({ @@ -48,15 +48,25 @@ const createWindow = () => { if (!archivePath) return; const presentationData = await openCodePrezArchive(archivePath); - const createSection = separate(presentationData); + const createSection = separateAndRender(presentationData); + + if (!presentationData) return; - if (!presentationData) { - return null; - } presentationData.presentationFileContent = createSection || ([] as any); win.webContents.send("set-codeprez-data", presentationData); }); + //Set to maximize + win.webContents.ipc.on("maximized-app", () => { + win.setFullScreen(false) + }) + + //Set to fullscreen + win.webContents.ipc.on("fullscreen-app", () => { + win.setFullScreen(true) + }) + + win.once("ready-to-show", async () => { win.show(); @@ -70,16 +80,29 @@ const createWindow = () => { return win; }; -const separate = (data: any) => { +const separateAndRender = (data: any) => { const slides = data?.presentationFileContent?.split(/^---$/gm); + const createSection = slides?.map((slide: any, index: any) => { - const dataMd: string = markdownHighlight().render(slide); + const dataMd: string = markdownRenderer(data.presentationPath).render(slide); return dataMd; }); return createSection; }; + const initialize = async () => { - await app.whenReady(); + await app.whenReady().then(() => { + protocol.registerFileProtocol('codeprez', (request, callback) => { + try { + const filePath = url.fileURLToPath('file://' + request.url.slice('codeprez:/'.length)) + callback(filePath) + } + catch (error) { + console.error(error) + callback("404") + } + }) + }); const win = createWindow(); }; diff --git a/package.json b/package.json index 5eb9c18..2bf18e4 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ }, "devDependencies": { "@types/decompress": "^4.2.4", + "@types/markdown-it": "^12.2.3", "electron": "^23.1.1" } } diff --git a/src/App.tsx b/src/App.tsx index 8ffb852..1cdf70e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,6 +5,7 @@ import { HashRouter, Route, Routes } from "react-router-dom"; import { SideBar } from "./components/SideBar"; import { Home } from "./components/Home"; import { Presentation } from "./components/Presentation"; +import { SlideViewer } from "./renderer/components/SlideViewer" import "highlight.js/styles/github.css"; function App() { @@ -16,13 +17,18 @@ function App() { height: "100%", }} > + + - + diff --git a/src/assets/css/Home.css b/src/assets/css/Home.css index 14d0029..5f2d174 100644 --- a/src/assets/css/Home.css +++ b/src/assets/css/Home.css @@ -28,3 +28,15 @@ color: var(--pink); border-color: var(--pink); } + +.go-to-viewer-button { + color:var(--black-grey); + border-color: var(--black-grey); + background-color: var(--pink); +} + +.go-to-viewer-button:hover { + background-color: var(--black-grey); + color: var(--pink); + border-color: var(--pink); +} diff --git a/src/assets/css/NewPrez.css b/src/assets/css/NewPrez.css index b0a6c0d..bb024b7 100644 --- a/src/assets/css/NewPrez.css +++ b/src/assets/css/NewPrez.css @@ -41,9 +41,6 @@ margin: 1em 0; } - - - .sendingButton{ cursor: pointer; border:none; diff --git a/src/assets/css/Presentation.css b/src/assets/css/Presentation.css new file mode 100644 index 0000000..cd7c8b9 --- /dev/null +++ b/src/assets/css/Presentation.css @@ -0,0 +1,23 @@ +.presentation-page { + overflow-y: auto; + overflow-x: hidden; + width: 100%; +} + +.presentation-page::-webkit-scrollbar { + width: 5px; +} + +.presentation-page::-webkit-scrollbar-thumb { + background: #666; +} + +.presentation-header { + display: flex; + justify-content: center; + align-items: center; +} + +.authors { + display: flex; +} \ No newline at end of file diff --git a/src/assets/css/SideBar.css b/src/assets/css/SideBar.css index f5fba37..b12a0c6 100644 --- a/src/assets/css/SideBar.css +++ b/src/assets/css/SideBar.css @@ -13,6 +13,13 @@ gap:1em; } +.sidebar .slide-preview { + max-width: inherit; + max-height: inherit; + overflow-y: auto; + overflow-x: hidden; +} + button{ cursor: pointer; display: flex; diff --git a/src/assets/css/Slide.css b/src/assets/css/Slide.css new file mode 100644 index 0000000..1f82fe0 --- /dev/null +++ b/src/assets/css/Slide.css @@ -0,0 +1,80 @@ +section { + /* height: 100vh; */ + border-radius: 10px; + box-shadow: 0 0 20px 10px rgba(0, 0, 0, 0.2); + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + /* flex-wrap: wrap; */ + background: var(--black-grey); + box-sizing: border-box; + /* padding: 0 40px; */ +} + +h1 { + font-size: 2em; +} + +section p img { + max-height: 80%; + max-width: 80%; +} + +.slide-container p { + display: flex; + justify-content: center; + align-items: center; + margin: 10px; +} + +pre { + border-radius: 10px; + min-width: 20%; + max-width: 90%; + max-height: 60%; + display: flex; + justify-content: space-between; + align-items: center; + box-sizing: border-box; +} + +pre code { + border-radius: inherit; + width: 100%; + max-height: 100%; + overflow: auto; +} + +strong { + margin: 0 4px; +} + +code:not(pre > code) { + background-color: var(--dark-grey); + border-radius: 5px; + padding: 2px; + color: var(--white); + margin: 2px; +} + +pre button { + width: 40px; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; +} + +.scale-preview { + transform : scale(0.85); +} + +.scale-sidebar { + transform : scale(0.7); + /* height: 300px; */ + width: 100%; + max-height: 300px; +} \ No newline at end of file diff --git a/src/assets/css/SlideViewer.css b/src/assets/css/SlideViewer.css new file mode 100644 index 0000000..9a6d1f8 --- /dev/null +++ b/src/assets/css/SlideViewer.css @@ -0,0 +1,27 @@ +.viewer-container { + height: 100vh; + scroll-snap-type: y mandatory; + overflow-y: auto; + overflow-x: hidden; + scroll-behavior: smooth; + + -ms-overflow-style: none; + scrollbar-width: none; /* Firefox */ +} + +.viewer-container::-webkit-scrollbar { + width: 5px; +} + +.viewer-container::-webkit-scrollbar-thumb { + background: #666; +} + +.viewer-container:focus { + outline:none; +} + +.viewer-slide { + scroll-snap-align: start; + box-sizing: border-box; +} \ No newline at end of file diff --git a/src/assets/css/index.css b/src/assets/css/index.css index e9627a6..79ceac2 100644 --- a/src/assets/css/index.css +++ b/src/assets/css/index.css @@ -27,15 +27,17 @@ body { background-color: var(--background-color); color: var(--white); + overflow: hidden; } -h1 { +.new-prez h1, .home h1, .presentation-title { border:#B4B4B4 1px solid; text-align: center; color: white; padding:0.5em 1em; margin:10px; border-radius: 10px; + text-decoration: none; } diff --git a/src/components/NavigationButton.tsx b/src/components/NavigationButton.tsx index fa08474..acf2536 100644 --- a/src/components/NavigationButton.tsx +++ b/src/components/NavigationButton.tsx @@ -1,7 +1,7 @@ import React, { FC } from 'react'; -import { useNavigate } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router-dom'; interface NavigationButtonProp { - goTo: 'prez' | 'add'; + goTo: 'prez' | 'add' |'viewer'; withIcon?: boolean; } @@ -10,20 +10,32 @@ export const NavigationButton: FC = ({ withIcon, }) => { const navigate = useNavigate(); + const {state} = useLocation(); + const createCodePrez = () => { navigate('/add'); }; - const launchPresentation = () => { - console.log("launchPrez"); - navigate('/prez'); + const openPresentation = () => { + window.api.getPresentationData(navigateToPresentation); }; + + const navigateToPresentation = (data: PresentationData) => { + navigate('/prez', { + state: data + }); + } + + const playPresentation = () => { + navigate('/viewer', {state: state}); + } + if (goTo === 'prez') { return ( - ); @@ -36,6 +48,15 @@ export const NavigationButton: FC = ({ )} ); + } else if (goTo == "viewer") { + return ( + + ) } return <>; }; diff --git a/src/components/Presentation.tsx b/src/components/Presentation.tsx index 094bcd5..14fd784 100644 --- a/src/components/Presentation.tsx +++ b/src/components/Presentation.tsx @@ -1,42 +1,37 @@ import { useState, useEffect } from "react"; +import "../assets/css/Presentation.css" import { Slide } from "./Slide"; +import { useLocation } from "react-router-dom"; +import { NavigationButton } from './NavigationButton'; +import { SlideShow } from "../renderer/components/SlideShow"; //TODO: Search for a way to import the css file from the presentation folder and display images with the folder temp export const Presentation = () => { - const [presentationData, setPresentationData] = - useState(); + const [presentationData, setPresentationData] = useState(); - useEffect(() => { - console.log("Je vais dans le useEffect combien de fois ?"); - window.api.getPresentationData(setPresentationData); - }, []); - - /* const updateCSS = () => { - if (presentationData?.presentationPath) { - const head = document.head; - const link = document.createElement("link"); + const {state} = useLocation() - link.type = "text/css"; - link.rel = "stylesheet"; - link.href = presentationData?.presentationPath + "/style.css"; - head.appendChild(link); - import(presentationData?.presentationPath + "/style.css"); - } - }; - updateCSS(); */ + useEffect(() => { + setPresentationData(state); + }, [state]); return ( -
- {/* */} -

{presentationData?.presentationConfig.title}

- {Array.isArray(presentationData?.presentationFileContent) && - presentationData?.presentationFileContent.map( - (slide, index) => {slide} - )} +
+
+

{presentationData?.presentationConfig.title}

+ +
+
); }; diff --git a/src/components/SideBar.tsx b/src/components/SideBar.tsx index a83b345..7deb407 100644 --- a/src/components/SideBar.tsx +++ b/src/components/SideBar.tsx @@ -2,7 +2,8 @@ import { Outlet, useNavigate } from 'react-router-dom'; import '../assets/css/SideBar.css'; import CodePrezLogo from '../assets/logo.svg'; import { NavigationButton } from './NavigationButton'; -export const SideBar = () => { + +export const SideBar = () => { const navigate = useNavigate(); const home = () => { @@ -11,10 +12,6 @@ export const SideBar = () => { return (
-

CodePrez

diff --git a/src/components/Slide.tsx b/src/components/Slide.tsx index 8b297af..6276a89 100644 --- a/src/components/Slide.tsx +++ b/src/components/Slide.tsx @@ -1,9 +1,30 @@ -export const Slide = ({ children }: any) => { +import { useEffect } from "react"; +import "../assets/css/Slide.css" + +type Props = { + children: string | React.ReactElement[]; + slideScale: "preview" | "sidebar" | "slideViewer", + className?: string +} + +export const Slide = ({ children, slideScale, className }: Props) => { + + if(typeof children == "string") { return ( -
+
+
); + } else { + return ( +
+ {children} +
+ ) + } + }; diff --git a/src/main/createArchive.ts b/src/main/createArchive.ts index a0773dc..03ccc47 100644 --- a/src/main/createArchive.ts +++ b/src/main/createArchive.ts @@ -55,7 +55,7 @@ export const createArchive = async (data: CreationCodePrezProps, file: string) = archive.directory(data.envDirectoryPath, 'env'); } if (data.assetsDirectoryPath) { - archive.directory(data.assetsDirectoryPath, 'css'); + archive.directory(data.assetsDirectoryPath, 'assets'); } archive.finalize().then(() => { diff --git a/src/main/example.codeprez b/src/main/example.codeprez deleted file mode 100644 index 9d90631..0000000 Binary files a/src/main/example.codeprez and /dev/null differ diff --git a/src/main/markdownRenderer.ts b/src/main/markdownRenderer.ts new file mode 100644 index 0000000..bb8088a --- /dev/null +++ b/src/main/markdownRenderer.ts @@ -0,0 +1,124 @@ +import { nativeImage } from "electron"; +import hljs from "highlight.js"; +import MarkdownIt from "markdown-it"; +import path from "path"; +import fs from 'fs'; +import Token from "markdown-it/lib/token"; +import Renderer from "markdown-it/lib/renderer"; + +type RendererRulesArguments = { + tokens: Token[], + idx: number, + options: MarkdownIt.Options, + env: any, + self: Renderer, + presentationPath: string +} + +type CodeFileRendererRulesArguments = { + codeFilePath: string, + href: string | null +} + +export function markdownRenderer(presentationPath: string) { + + const markdownOptions: MarkdownIt.Options = { + highlight: function (str: string, lang: string) { + if (lang && hljs.getLanguage(lang)) { + try { + if(lang == "bash") { + return ( + '
' +
+                            hljs.highlight(str, {
+                                language: lang,
+                                ignoreIllegals: true,
+                            }).value
+                            + '
' + ) + } else { + return ( + '
' +
+                            hljs.highlight(str, {
+                                language: lang,
+                                ignoreIllegals: true,
+                            }).value +
+                            "
" + ); + } + } catch (__) {} + } + + return ( + '
' +
+                md.utils.escapeHtml(str) +
+                "
" + ); + }, + } + + const md = MarkdownIt(markdownOptions); + + //Include images + md.renderer.rules.image = (tokens, idx, options, env, self) => imageRendererRules({tokens, idx, options, env, self, presentationPath}); + + //Include code via file + md.renderer.rules.link_open = (tokens, idx, options, env, self) => linkRendererRules({tokens, idx, options, env, self, presentationPath}) + + return md; +} + +const imageRendererRules = ({tokens, idx, options, env, self, presentationPath}: RendererRulesArguments) => { + const token = tokens[idx]; + + const src = token.attrGet("src"); + + const regexForSrc = /\.\/assets/gm; + + const srcWithPresentationPath = (src?.match(regexForSrc)) ? path.join("codeprez:/", presentationPath, src) : src + token.attrSet('src', srcWithPresentationPath || "") + + return self.renderToken(tokens, idx, options) +} + +const linkRendererRules = ({tokens, idx, options, env, self, presentationPath}: RendererRulesArguments) => { + const token = tokens[idx]; + + const href = token.attrGet("href") + const regexPathFile = /^\.\/(.+\.\w+)/gm; + const relativeCodeFilePath = href?.match(regexPathFile) ?? []; + + if(relativeCodeFilePath.length) { + const codeFilePath = path.join(presentationPath, relativeCodeFilePath[0] ?? "") + return codeFileRendererRules({codeFilePath: codeFilePath, href}); + } else { + return self.renderToken(tokens, idx, options); + } + +} + +const codeFileRendererRules = ({codeFilePath, href}: CodeFileRendererRulesArguments) => { + + const regexLineNumber = /#(\d+)-(\d+)/; + const regexFileExtension = /\.(\w+)(?:#\d+-\d+)?$/; + + const lineNumbers = href?.match(regexLineNumber) ?? []; + const startNumber = lineNumbers[1]; + const endNumber = lineNumbers[2]; + + const fileExtension = href?.match(regexFileExtension) ?? []; + + const file = fs.readFileSync(codeFilePath, { encoding: 'utf8' }); + const lines = file.split("\n").slice(Number(startNumber)-1, Number(endNumber)); + + const codeToDisplay = (lineNumbers.length) ? lines.join("\n") : file; + + // I must use a concatenate string instead of `` because of tabulation and break line + return ( + "
" +
+            hljs.highlight(codeToDisplay, {
+                    language: fileExtension[1] || "",
+                    ignoreIllegals: true,
+                }).value +
+        "
" + ) +} diff --git a/src/main/openAndCloseCodePrezFiles.ts b/src/main/openAndCloseCodePrezFiles.ts index 8b1aac7..786910f 100644 --- a/src/main/openAndCloseCodePrezFiles.ts +++ b/src/main/openAndCloseCodePrezFiles.ts @@ -22,6 +22,8 @@ const decompressCodePrezArchive = async ( export const openCodePrezArchive = async (archivePath: string) => { const presentationPath = path.join(tempPath, "codeprez"); //useful for assets, style.css and env + deleteCodePrezTempFolder(); + const files = await decompressCodePrezArchive( archivePath, presentationPath @@ -38,13 +40,20 @@ export const openCodePrezArchive = async (archivePath: string) => { ?.data.toString(); const presentationConfig = JSON.parse(plainPresentationConfig ?? "{}"); - return { presentationConfig, presentationFileContent, presentationPath }; + const presentationStyle = files + .find((file) => file.path == "style.css") + ?.data.toString(); + + return { presentationConfig, presentationFileContent, presentationPath, presentationStyle }; }; -export const deleteCodePrezTempFolder = () => { +export const deleteCodePrezTempFolder = async () => { const presentationPath = path.join(tempPath, "codeprez"); - fs.rmdir(presentationPath, (err) => { - console.error(err); - }); + try { + fs.rmSync(presentationPath, { recursive: true }); + } catch(e) { + console.error(e); + return; + } }; diff --git a/src/preload/preload.ts b/src/preload/preload.ts index 2b1c208..e657dc8 100644 --- a/src/preload/preload.ts +++ b/src/preload/preload.ts @@ -12,6 +12,8 @@ export interface CreationCodePrezProps { export type ContextBridgeApi = { getPresentationData: (setPresentationData: Function) => void, + setAppToFullScreen: () => void, + setAppToMaximized: () => void, sendExecuteCommand: (command: string) => void, openFileDialog: (type: "md" | "css" | "env" | "assets", callback: Function) => null; createCodePrez: ({ mdFilePath, cssFilePath, envDirectoryPath, assetsDirectoryPath, title, duration, authors }: CreationCodePrezProps) => null; @@ -22,6 +24,12 @@ contextBridge.exposeInMainWorld("api", { ipcRenderer.send("open-presentation", { type: "codeprez" }); ipcRenderer.once("set-codeprez-data", (event, data) => setPresentationData(data)) }, + setAppToFullScreen: () => { + ipcRenderer.send("fullscreen-app") + }, + setAppToMaximized: () => { + ipcRenderer.send("maximized-app") + }, sendExecuteCommand: (command: string) => { ipcRenderer.send("execute-command", { command }) }, diff --git a/src/renderer/components/SlideShow.tsx b/src/renderer/components/SlideShow.tsx new file mode 100644 index 0000000..24962a1 --- /dev/null +++ b/src/renderer/components/SlideShow.tsx @@ -0,0 +1,35 @@ +import { useState, useEffect } from "react"; +import { useLocation, useNavigate } from 'react-router-dom'; +import { Slide } from '../../components/Slide'; +import "../../assets/css/SlideViewer.css" + +type Props = { + config?: PresentationConfig, + content: string[], + style?: string, + slideScale: "slideViewer" | "preview" | "sidebar" +} + +export const SlideShow = (props: Props) => { + const {config, content, style, slideScale} = props; + + return ( + <> + +

{config?.title}

+ +
+

Créé par : {config?.authors.join(", ")}

+
+
+ {Array.isArray(content) && content.map((slide, index) => ( + + {slide} + + ))} + + + ); +}; diff --git a/src/renderer/components/SlideViewer.tsx b/src/renderer/components/SlideViewer.tsx new file mode 100644 index 0000000..da9bb16 --- /dev/null +++ b/src/renderer/components/SlideViewer.tsx @@ -0,0 +1,50 @@ +import { useState, useEffect } from "react"; +import { useLocation, useNavigate } from 'react-router-dom'; +import { Slide } from '../../components/Slide'; +import "../../assets/css/SlideViewer.css" +import { SlideShow } from './SlideShow'; + +export const SlideViewer = () => { + const [presentationData, setPresentationData] = useState(); + + const {state} = useLocation() + const navigate = useNavigate() + + useEffect(() => { + window.api.setAppToFullScreen(); + setPresentationData(state); + + document.addEventListener("keydown", escapeListener) + document.getElementById('viewer-container')?.focus(); //To focus on the div when the component is mounted (to be scrollable with Space or Arrow) + }, [state]); + + const escapeListener = (e: any) => { + if(e.key == "Escape") { + closeViewer(); + + //Remove listener to prevent multiple listener (we don't use {once: true} because of condition Escape) + document.removeEventListener("keydown", escapeListener); + } + } + + const closeViewer = () => { + window.api.setAppToMaximized(); + navigate("/prez", {state}) + } + + return ( +
+ +
+ ); +}; diff --git a/src/renderer/utils/utils.ts b/src/renderer/utils/utils.ts index a697926..693da49 100644 --- a/src/renderer/utils/utils.ts +++ b/src/renderer/utils/utils.ts @@ -1,28 +1 @@ -import hljs from "highlight.js"; - -var md = markdownHighlight(); - -export function markdownHighlight() { - return require("markdown-it")({ - highlight: function (str: string, lang: string) { - if (lang && hljs.getLanguage(lang)) { - try { - return ( - '
' +
-                        hljs.highlight(str, {
-                            language: lang,
-                            ignoreIllegals: true,
-                        }).value +
-                        "
" - ); - } catch (__) {} - } - - return ( - '
' +
-                md.utils.escapeHtml(str) +
-                "
" - ); - }, - }); -} +export {} \ No newline at end of file diff --git a/src/types.d.ts b/src/types.d.ts index 2b96695..9c46e07 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -2,6 +2,7 @@ type PresentationData = { presentationConfig: PresentationConfig; presentationFileContent: string[] | string; presentationPath: string; + presentationStyle: string; }; type PresentationConfig = { diff --git a/yarn.lock b/yarn.lock index 8aa8581..d917b88 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2063,6 +2063,24 @@ dependencies: "@types/node" "*" +"@types/linkify-it@*": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-3.0.2.tgz#fd2cd2edbaa7eaac7e7f3c1748b52a19143846c9" + integrity sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA== + +"@types/markdown-it@^12.2.3": + version "12.2.3" + resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-12.2.3.tgz#0d6f6e5e413f8daaa26522904597be3d6cd93b51" + integrity sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ== + dependencies: + "@types/linkify-it" "*" + "@types/mdurl" "*" + +"@types/mdurl@*": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-1.0.2.tgz#e2ce9d83a613bacf284c7be7d491945e39e1f8e9" + integrity sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA== + "@types/mime@*": version "3.0.1" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10"