Compare commits

..

20 Commits

Author SHA1 Message Date
Michael Ochmann 862b386c44 releasing version 0.4.0 1 year ago
Michael Ochmann 6b60e02ba1 removed old code 1 year ago
Michael Ochmann 6461d432f2 added clipboard actions 1 year ago
Michael Ochmann c8e2f4636b window now maximizable by double click onto toolbar 1 year ago
Michael Ochmann b29a8ed78a finished first editor implementation 1 year ago
Michael Ochmann 6073710de5 added basic editor 1 year ago
Michael Ochmann 3c3b81c585 added "recent files" functionality 1 year ago
Michael Ochmann 22b1617afe added `nodemon` in `npm run dev` command 2 years ago
Michael Ochmann df6fdbcdf8 fixed null access on `meta` 2 years ago
Michael Ochmann 14c8849c25 added usage and installation info 2 years ago
Michael Ochmann f2afaf3257 colors and font now controllable on a per slideshow basis 2 years ago
Michael Ochmann 96bb12b411 added escape for `'` 2 years ago
Michael Ochmann ef8a1b0c0d refactored meta data parsing 2 years ago
Michael Ochmann f26603df3a added horizontal lines 2 years ago
Michael Ochmann 3379d13168 UI: added more color settings: 2 years ago
Michael Ochmann fb9c7b3c3e Frontend: Markdown parsing: added "br" 2 years ago
Michael Ochmann f29069c3d6 added escape sequences for special chars 2 years ago
Michael Ochmann d74650e32e more tweaks for windoof 3 years ago
Michael Ochmann 9b8da8be9e added windows build and minor tweaks for `win32` platform 3 years ago
Michael Ochmann 38cb3e3295 . 3 years ago
  1. 1
      .gitignore
  2. 66
      README.md
  3. 5
      contextAPI.js
  4. 11263
      package-lock.json
  5. 25
      package.json
  6. 46
      src/Ation.js
  7. 15
      src/MainMenu.js
  8. 55
      src/Parser.js
  9. 3
      src/SettingsManager.js
  10. 28
      src/WindowManager.js
  11. 17622
      src/ui/package-lock.json
  12. 1
      src/ui/package.json
  13. 15
      src/ui/src/assets/css/_slide.scss
  14. 2
      src/ui/src/assets/css/_variables.scss
  15. 13
      src/ui/src/assets/css/_window.scss
  16. 51
      src/ui/src/components/Ation.js
  17. 36
      src/ui/src/components/Editor.js
  18. 16
      src/ui/src/components/KeyboardControl.js
  19. 58
      src/ui/src/components/SlideItem.js
  20. 24
      src/ui/src/components/SlidesList.js
  21. 2
      src/ui/src/components/Tips.js
  22. 13
      src/ui/src/components/Toolbar.js
  23. 30
      src/ui/src/components/settings/Appearance.js
  24. 3
      src/ui/src/models/Mode.js

1
.gitignore vendored

@ -6,4 +6,5 @@ node_modules
dist
assets/*.png
assets/*.icns
assets/*.ico
!assets/dmg_background.png

@ -1,2 +1,66 @@
# ation
– a simple keynote software for Markdown files – written in `electron`
– a simple keynote software for markdown files – written using `electron`
![ation logo](./assets/appIcon.svg)
## Usage
After opening any markdown file you can press <kbd>Tab</kbd> to show all
available keybindings. The keybindings are chosen, so they work with commont
presenters like the *"Logitech R400"*.
### Markdown
`ation` tries to make a presentation out of any ordinary markdown file. It
considers `H1` titles to be a standalone slide and `H2` titles to start a new
slide. There also is a special template for a "cover slide", which is
provisioned by meta data at the top of your markdown file.
### Meta data
To keep compatibility with other markdown parsers, `ation` does not implement a
`YAML` front matter or similar, but tries to parse the first `HTML` comment in
your markdown file as meta data. The meta data is specified as simple key-value
pairs, one pair per line, a color (`:`) seperating the key from the value.
**Possible values**
| Key | Type | Example Value |
| ---------------- | ------------------- | ---------------------------- |
| title | string | My Keynote |
| subtitle | string | an introduction into `ation` |
| author | string | MikO, Massive Dynamic |
| email | email address | miko@mail.tld |
| icon | path *(relative)* | ./my_logo.svg |
| color_highlight | CSS color value | #4994DA |
| color_background | CSS color value | #1A1A1A |
| color_text | CSS color value | #DDDDDD |
| font | CSS font identifier | Iosevka |
**Example**
```html
<!--
title : My Keynote
subtitle : an introduction into `ation`
author : MikO, Massive Dynamic
email : miko@mail.tld
icon : ./my_logo.svg
color_highlight : #4994DA
color_background : #1A1A1A
color_text : #DDDDDD
font : Iosevka
-->
```
The color values override the ones that have been set in `ation`'s settings on a
per slideshow basis.
## Installation
Either choose a pre-built release from the [releases page][release] or build
`ation` yourself running
```bash
npm i
npm run dist
```
Releases are currently only available for `macOS` and `Windows`.
[release]: releases

@ -22,11 +22,14 @@ contextBridge.exposeInMainWorld("api", {
},
openFile : filePath => ipcRenderer.send("WindowManager::openFile", filePath),
closeFile : () => ipcRenderer.send("Ation::closeFile"),
saveFile : async newContent => await ipcRenderer.invoke("Ation::saveFile", newContent),
clearCache : () => webFrame.clearCache(),
appVersion : async () => await ipcRenderer.invoke("Ation::appVersion"),
fonts : async () => await ipcRenderer.invoke("FontManager::fonts"),
resize : size => ipcRenderer.invoke("SettingsManager::resize", size)
resize : size => ipcRenderer.invoke("WindowManager::resize", size),
fullscreen : fullscreen => ipcRenderer.invoke("WindowManager::presentFullscreen", fullscreen),
maximize : () => ipcRenderer.send("WindowManager::toggleMaximize")
});
contextBridge.exposeInMainWorld("appSettings", {

11263
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -1,16 +1,16 @@
{
"name": "ation",
"version": "0.3.2",
"version": "0.4.0",
"description": "a simple presentation software",
"main": "main.js",
"scripts": {
"start": "electron .",
"dev": "concurrently \"cd src/ui && cross-env BROWSER=none npm start\" \"wait-on tcp:3000 && electron .\"",
"dev": "concurrently \"cd src/ui && cross-env BROWSER=none npm start\" \"wait-on tcp:3000 && nodemon --watch src --exec 'electron .'\"",
"build": "npm i && npm run build:assets && npm run build:react",
"build:react": "cd src/ui && npm i && npm run build",
"build:assets": "icon-gen -i assets/appIcon.svg -o ./assets --icns && xattr -cr assets/appIcon.svg",
"dist": "npm run clean && npm run build && electron-builder -m",
"clean": "rm -rf node_modules && rm -rf src/public/build/* && rm -rf src/public/node_modules && rm -rf dist && rm -f assets/*.icns"
"build:assets": "icon-gen -i assets/appIcon.svg -o ./assets --icns --ico --ico-name appIcon && xattr -cr assets/appIcon.svg",
"dist": "npm run clean && npm run build && electron-builder -mw",
"clean": "rm -rf node_modules && rm -rf src/public/build/* && rm -rf src/public/node_modules && rm -rf dist && rm -f assets/*.icns rm -f assets/*.ico"
},
"author": "Michael Ochmann <miko@massivedynamic.eu>",
"license": "MIT",
@ -30,10 +30,20 @@
"assets/*.svg",
"package.json"
],
"win": {
"target": "nsis",
"icon": "assets/appIcon.ico"
},
"mac": {
"category": "public.app-category.productivity",
"target": [
"dmg"
{
"target": "dmg",
"arch": [
"x64",
"arm64"
]
}
],
"icon": "assets/app.icns",
"darkModeSupport": true,
@ -69,11 +79,12 @@
}
},
"devDependencies": {
"electron": "^21.0.0",
"concurrently": "^7.4.0",
"cross-env": "^7.0.3",
"electron": "^21.0.0",
"electron-builder": "^23.3.3",
"icon-gen": "^3.0.1",
"nodemon": "^3.0.1",
"wait-on": "^6.0.1"
}
}

@ -13,6 +13,7 @@ const {parser} = require("./Parser");
app.commandLine.appendSwitch("disable-http-cache");
const RECENT_FILES = 5;
class Ation {
windowManager;
fontManager;
@ -20,27 +21,40 @@ class Ation {
mainMenu;
watcher;
currentFile;
fileToOpen;
recentFiles;
constructor() {
if (Ation.Instances > 0)
throw new Error("Only one Instance of Ation possible");
this.fileToOpen = null;
this.currentFile = "";
this.watcher = null;
this.windowManager = new WindowManager(this);
this.fontManager = new FontManager();
this.settingsManager = new SettingsManager(this);
this.recentFiles = this.settingsManager.get("recentFiles", []);
this.mainMenu = new MainMenu(this);
ipcMain.handle("Ation::appVersion", () => AppInfo.version);
ipcMain.handle("Ation::saveFile", async (_, newContent) => this.saveFile(newContent));
ipcMain.on("Ation::closeFile", () => this.closeFile());
app.on("open-file", (_, path) => {
this.fileToOpen = path;
});
ipcMain.handle("Ation::appVersion", () => AppInfo.version);
ipcMain.on("Ation::closeFile", () => this.closeFile());
app.whenReady().then(async () => {
this.settingsManager.change();
// this is a hack for windows, because they do not send the
// `open-file` event, but pass a "cli parameter" as second
// argument
if (!this.fileToOpen) {
if (process.argv.length >= 2)
this.fileToOpen = process.argv[1];
}
if (this.fileToOpen)
this.openFile(this.fileToOpen);
protocol.registerFileProtocol("slideimg", (request, callback) => {
@ -54,6 +68,27 @@ class Ation {
});
}
async saveFile(newContent) {
return fs.writeFile(this.currentFile, newContent, { encoding : "utf-8" })
}
addRecentFile(filePath) {
if (this.recentFiles.includes(filePath)) {
const position = this.recentFiles.indexOf(filePath);
this.recentFiles.splice(position, 1),
this.recentFiles.push(filePath);
} else if (this.recentFiles.length < RECENT_FILES) {
this.recentFiles.push(filePath);
} else {
this.recentFiles.shift();
this.recentFiles.push(filePath);
}
this.settingsManager.set("recentFiles", this.recentFiles);
this.mainMenu.refresh();
}
async openFile(filePath = null, change = false) {
if (!this.windowManager.mainWindow)
return null;
@ -64,7 +99,7 @@ class Ation {
filters : [
{
name : "Markdown files",
extensions : [".md"]
extensions : ["md"]
}
]
});
@ -89,8 +124,9 @@ class Ation {
else
this.openFile(this.currentFile, true);
});
this.windowManager.mainWindow.send("Ation::openFile", [basePath, data, fileContents]);
this.addRecentFile(filePath);
Menu.getApplicationMenu().getMenuItemById("close-file").enabled = this.currentFile !== "";
this.windowManager.mainWindow.send("Ation::openFile", [basePath, data]);
} catch (error) {
console.log(error);
return;

@ -1,11 +1,16 @@
"use strict";
const {Menu, app} = require("electron");
const path = require("path");
class MainMenu {
constructor(parentApp) {
this.app = parentApp;
this.menu = null;
this.refresh();
}
refresh() {
this.buildItems();
Menu.setApplicationMenu(this.menu);
}
@ -52,9 +57,19 @@ class MainMenu {
role : "windowMenu"
}
];
template[1].submenu.push({
id : "recent-files",
label : "Recent files",
enabled : this.app.recentFiles.length > 0,
...(this.app.recentFiles.length > 0 ? {submenu : this.app.recentFiles.map(item => ({
label : path.basename(item),
click : () => this.app.openFile("" + item)
}))} : {}),
});
this.menu = Menu.buildFromTemplate(template);
}
}
module.exports = MainMenu;

@ -28,13 +28,32 @@ class Slide {
}
}
class SlideDeck {
metaData;
slides;
constructor() {
this.metaData = null;
this.slides = [];
}
push(slide) {
this.slides.push(slide);
}
unshift(slide) {
this.slides.unshift(slide);
}
}
const tokenize = string => {
//console.log(util.inspect(marked.Lexer.rules.block.listItemStart, true, null, true));
return new Lexer({gfm : true}).lex(string);
};
const injectTitle = (deck, meta) => {
const injectTitle = (deck) => {
const title = [];
const meta = deck.metaData;
if (meta?.icon)
title.push({
@ -93,11 +112,29 @@ const injectTitle = (deck, meta) => {
return deck;
}
const parseMetaData = source => {
const meta = {};
for (const attribute of source.split('\n')) {
const trimmed = attribute.trim();
if (trimmed.length < 1 || (trimmed.charAt(0) === '/' && trimmed.charAt(1) === '/'))
continue;
const [key, value] = trimmed.split(':');
if (key.trim().length < 1 || value.trim().length < 1)
continue;
meta[key.trim()] = value.trim();
}
console.table(meta);
return meta;
};
const parser = string => {
const tokenStream = tokenize(string);
const slideDeck = [];
const slideDeck = new SlideDeck();
let currentSlide = new Slide();
let metaData = null;
for (const token of tokenStream) {
if (token.type === "space" || (token.type === "heading" && token.depth === 1) || token.type === "html") {
@ -105,14 +142,10 @@ const parser = string => {
slideDeck.push(currentSlide);
currentSlide = new Slide();
}
if (!metaData && token.type === "html" && token.text.charAt(2) === '-') {
// basically, if this is the forst comment, we expect meta data
if (!slideDeck.metaData && token.type === "html" && token.text.charAt(2) === '-') {
const metaString = token.text.replace(/(<!--|-->)/gi, "");
const meta = {};
for (const attribute of metaString.split(';')) {
const [key, value] = attribute.split(':');
meta[key.trim()] = value.trim();
}
metaData = meta;
slideDeck.metaData = parseMetaData(metaString);
}
if (token.type === "heading")
@ -124,7 +157,7 @@ const parser = string => {
if (currentSlide.content.length > 0)
slideDeck.push(currentSlide);
return injectTitle(slideDeck, metaData);
return injectTitle(slideDeck);
};
module.exports = {

@ -20,7 +20,6 @@ class SettingsManager {
}
this.data = JSON.parse(fsn.readFileSync(SettingsManager.File, {encoding : "utf-8"}));
ipcMain.handle("SettingsManager::resize", (_, height) => app.windowManager.windows.settings.setSize(800, height, true));
ipcMain.handle("SettingsManager::get", (_, key, defaultValue = null) => this.get(key, defaultValue));
ipcMain.handle("SettingsManager::set", (_, key, value) => this.set(key, value));
ipcMain.handle("SettingsManager::all", () => this.data);
@ -39,7 +38,7 @@ class SettingsManager {
}
change() {
this.app.windowManager.mainWindow.send("SettingsManager::change", this.data);
this.app.windowManager.mainWindow?.send("SettingsManager::change", this.data);
}
save() {

@ -2,7 +2,6 @@
const {app, BrowserWindow, ipcMain, globalShortcut} = require("electron");
const path = require("path");
const util = require("util");
const {isDevelopment} = require("./Util");
@ -19,12 +18,15 @@ class WindowManager {
ipcMain.on("WindowManager::openFileDialog", () => this.app.openFile());
ipcMain.on("WindowManager::openFile", (_, path) => this.app.openFile(path));
ipcMain.on("WindowManager::toggleMaximize", () => this.mainWindow?.isMaximized() ? this.mainWindow?.unmaximize() : this.mainWindow?.maximize());
ipcMain.handle("WindowManager::resize", (_, height) => this.windows.settings.setSize(800, height + (process.platform === "win32" ? 50 : 0), true));
ipcMain.handle("WindowManager::presentFullscreen", (_, fullscreen) => this.windows.main.setFullScreen(fullscreen));
}
init() {
this.mainWindow = WindowManager._CreateWindow({
fullscreen : false,
fullscreenable : true
fullscreen : false,
fullscreenable : true
});
this.windows.settings = WindowManager._CreateWindow({
height : 300,
@ -34,6 +36,13 @@ class WindowManager {
show : false
}, this.mainWindow, false);
// on windows and linux we need to hide the main menu in the settings
// window, because it would look odd.
if (["win32", "linux"].includes(process.platform)) {
this.windows.settings.removeMenu();
}
this.windows.settings.on("close", event => {
event.preventDefault();
this.windows.settings.hide();
@ -61,12 +70,13 @@ class WindowManager {
static _CreateWindow(options = null, parent = null, show = true) {
const windowOptions = {
width : 800,
height : 600,
show : false,
devTools : isDevelopment(),
titleBarStyle : "hiddenInset",
webPreferences : {
width : 800,
height : 600,
show : false,
devTools : isDevelopment(),
titleBarStyle : "hiddenInset",
autoHideMenuBar : process.platform === "win32",
webPreferences : {
contextIsolation : true,
preload : path.join(__dirname, "..", "contextAPI.js")
},

17622
src/ui/package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -4,6 +4,7 @@
"private": true,
"homepage": "./",
"dependencies": {
"@monaco-editor/react": "^4.6.0",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",

@ -9,8 +9,8 @@
text-align: center;
aspect-ratio: 1.333;
overflow: hidden;
background: color(background);
color: color(foreground);
background: color(slideBackground);
color: color(slideForeground);
h1, h2, h3, h4, h5 {
margin: 0 0 1em 0;
@ -83,7 +83,8 @@
p {
font-style: italic;
font-size: 1em;
color: color(scrollbar);
color: color(slideForeground);
opacity: 0.3;
margin: 0;
line-height: normal;
@ -121,8 +122,14 @@
color: color(hightlight);
}
hr {
display: block;
border: 0.5px solid color(hightlight);
width: 100%;
}
pre {
font-size: 0.3em;
//font-size: 0.3em;
width: 90%;
padding: 1em !important;

@ -1,7 +1,9 @@
$colors : (
//background : #272822,
background : #1a1a1a,
slideBackground : #1a1a1a,
foreground : #ddd,
slideForeground : #ddd,
fadedForeground : #888,
sidebarBackground : #333,
mainBackground : #222,

@ -11,6 +11,10 @@
grid-column: 1 / span 2;
}
&.edit {
//grid-template-columns: 10% auto;
}
main {
background: color(mainBackground);
overflow-y: auto;
@ -28,6 +32,15 @@
font-size: calc(3vw * 0.7);
}
}
&.edit {
display: grid;
grid-template-columns: 1fr 2fr;
& > .slide:first-child {
font-size: calc(3vw / 3 * 0.7);
}
}
}
&.fullscreen {

@ -1,4 +1,4 @@
import React, {useEffect, useState, useContext} from "react";
import React, {useEffect, useState, useContext, useCallback} from "react";
import SlidesList from "./SlidesList";
import Mode from "../models/Mode";
@ -8,29 +8,34 @@ import Blackout from "./Blackout";
import NoFile from "./NoFile";
import Toolbar from "./Toolbar";
import Tips from "./Tips";
import Editor from "./Editor";
import SlideContext from "../shared/SlideContext";
import SettingsContext from "../shared/SettingsContext";
const Ation = () => {
const {font, highlightColor} = useContext(SettingsContext);
const [mode, setMode] = useState(Mode.NORMAL);
const [deck, setDeck] = useState([]);
const [slide, setSlide] = useState(0);
const [basePath, setBasePath] = useState("");
const [showTips, setShowTips] = useState(false);
const [version, setVersion] = useState("0.0.0");
const {font, highlightColor, backgroundColor, color} = useContext(SettingsContext);
const [mode, setMode] = useState(Mode.NORMAL);
const [deck, setDeck] = useState([]);
const [meta, setMeta] = useState(null);
const [slide, setSlide] = useState(0);
const [basePath, setBasePath] = useState("");
const [showTips, setShowTips] = useState(false);
const [version, setVersion] = useState("0.0.0");
const [source, setSource] = useState("");
useEffect(() => {
window.api.onFileOpen(presentation => {
window.api.clearCache();
const [newBasePath, slideDeck] = presentation;
const [newBasePath, slideDeck, fileContents] = presentation;
if (!slideDeck)
return;
if (slide >= slideDeck.length)
setSlide(0);
setSource(fileContents);
setMeta(slideDeck.metaData);
setBasePath(newBasePath);
setDeck(slideDeck);
setDeck(slideDeck.slides);
});
window.api.onFileClose(() => {
setBasePath("");
@ -44,7 +49,14 @@ const Ation = () => {
const openFile = () => {
window.api.openFileDialog();
}
};
const toggleEdit = useCallback(() => {
if (mode === Mode.NORMAL)
setMode(Mode.EDIT);
else if (mode === Mode.EDIT)
setMode(Mode.NORMAL);
}, [mode]);
return (
<>
@ -52,16 +64,21 @@ const Ation = () => {
<NoFile openFile={openFile} />
: (
<SlideContext.Provider value={{slide, setSlide, mode, setMode, basePath, slideCount : deck.length}}>
<section className={`window${mode === Mode.PRESENT ? " fullscreen" : ""}`}>
<Toolbar openFile={openFile} setShowTips={setShowTips} version={version} />
<SlidesList deck={deck} />
<main className="main" style={{"--color-hightlight" : highlightColor}}>
<Slide data={deck[slide] || null} style={{fontFamily : font}}/>
<section className={`window${mode === Mode.PRESENT ? " fullscreen" : (mode === Mode.EDIT ? " edit" : "")}`}>
<Toolbar openFile={openFile} setShowTips={setShowTips} version={version} toggleEdit={toggleEdit} />
<SlidesList deck={deck} meta={meta} font={font} />
<main className={`main ${mode === Mode.EDIT ? "edit" : ""}`} style={{
"--color-hightlight" : meta?.color_highlight || highlightColor,
"--color-slideBackground" : meta?.color_background || backgroundColor,
"--color-slideForeground" : meta?.color_text || color
}}>
<Slide data={deck[slide] || null} style={{fontFamily : meta?.font || font}}/>
<Editor show={mode === Mode.EDIT} source={source} toggleEdit={toggleEdit} />
</main>
<Tips show={showTips} />
</section>
<Blackout show={mode === Mode.BLACKOUT} />
<KeyboardControl mode={mode} setMode={setMode} deck={deck} openFile={openFile} setShowTips={setShowTips} />
<KeyboardControl mode={mode} setMode={setMode} deck={deck} openFile={openFile} setShowTips={setShowTips} toggleEdit={toggleEdit} />
</SlideContext.Provider>
)}
</>

@ -0,0 +1,36 @@
import React from "react";
import Monaco from "@monaco-editor/react";
const Editor = ({show, source}) => {
const editorMount = (instance, monaco) => {
// NOTE: I have no idea, why we have to re-add these ourselves, but this
// code works, so be it. --MikO
instance.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyC, () => instance.trigger("source","editor.action.clipboardCopyAction"));
instance.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyV, () => instance.trigger("source","editor.action.clipboardPasteAction"));
instance.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyX, () => instance.trigger("source","editor.action.clipboardCutAction"));
instance.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => window.api.saveFile(instance.getValue()));
}
if (!show)
return <></>;
return (
<>
<Monaco
defaultLanguage="markdown"
height="100%"
options={{
minimap : {
enabled : false
}
}}
onMount={editorMount}
keepCurrentModel={true}
defaultValue={source}
theme="vs-dark" />
</>
);
};
export default Editor;

@ -3,7 +3,7 @@ import {useContext, useEffect, version} from "react";
import SlideContext from "../shared/SlideContext";
import Mode from "../models/Mode";
const KeyboardControl = ({openFile, mode, setMode, deck, setShowTips}) => {
const KeyboardControl = ({openFile, mode, setMode, deck, setShowTips, toggleEdit}) => {
const {slide, setSlide} = useContext(SlideContext);
useEffect(() => {
@ -18,6 +18,8 @@ const KeyboardControl = ({openFile, mode, setMode, deck, setShowTips}) => {
}
const keyHandler = event => {
const commandOrControl = event.metaKey || event.ctrlKey;
switch(event.key) {
case "Escape":
setShowTips(false);
@ -25,7 +27,13 @@ const KeyboardControl = ({openFile, mode, setMode, deck, setShowTips}) => {
default:
break;
}
if (!mode || !setMode || !deck || !setShowTips)
if (!mode || !setMode || !deck || !setShowTips)
return;
if (commandOrControl && event.key === "e")
toggleEdit();
if (mode === Mode.EDIT)
return;
switch (event.key) {
@ -51,7 +59,7 @@ const KeyboardControl = ({openFile, mode, setMode, deck, setShowTips}) => {
setSlide(slide + 1);
break;
case "b": // PRESENTER
if (mode === Mode.NORMAL)
if (mode === Mode.NORMAL || mode === Mode.EDIT)
return;
setMode(mode === Mode.BLACKOUT ? Mode.PRESENT : Mode.BLACKOUT);
break;
@ -85,7 +93,7 @@ const KeyboardControl = ({openFile, mode, setMode, deck, setShowTips}) => {
window.removeEventListener("fullscreenchange", fullscreenHandler);
}
}, [slide, setSlide, mode, setMode, deck, setShowTips, openFile]);
}, [slide, setSlide, mode, setMode, deck, setShowTips, openFile, toggleEdit]);
};
export default KeyboardControl;

@ -5,6 +5,15 @@ import {monokai} from "react-syntax-highlighter/dist/esm/styles/hljs";
import SlideContext from "../shared/SlideContext";
const EscapeSequences = {
"&quot;" : '"',
"&lt;" : "<",
"&gt;" : ">",
"&amp;" : "&",
"&#39;" : "'"
};
const EscapeSequencesRegex = new RegExp(Object.keys(EscapeSequences).join('|'), "g");
const Children = ({items}) => {
if (items instanceof Array)
return <>{items.map((child, index) => <SlideItem item={child} key={index} />)}</>;
@ -14,8 +23,11 @@ const Children = ({items}) => {
const SlideItem = ({item}) => {
const {basePath} = useContext(SlideContext);
const processEscapeSequences = text => {
return text.replace(EscapeSequencesRegex, match => EscapeSequences[match]);
};
const content = useMemo(() => {
switch (item.type) {
case "heading":
const level = item.level || item.depth;
@ -50,6 +62,8 @@ const SlideItem = ({item}) => {
: null}
</span>
)
case "hr":
return <hr />
case "blockquote":
return <blockquote><Children items={item.tokens} /></blockquote>
case "paragraph":
@ -64,19 +78,25 @@ const SlideItem = ({item}) => {
return <i><Children items={item.tokens} /></i>
case "html":
return <span dangerouslySetInnerHTML={{__html : item.raw}}></span>
case "br":
return <br />;
case "link":
case "text":
return <>{item.tokens ? <Children items={item.tokens} /> : item.text}</>
return <>{item.tokens ? <Children items={item.tokens} /> : processEscapeSequences(item.text)}</>
case "escape":
switch(item.text) {
case "&lt;":
return '<';
case "&gt;":
return '>';
case '"':
case "&quot;":
return '"';
case "&#39;":
return "'";
default:
return "";
}
break;
default:
return "UNKNOWN ITEM" + JSON.stringify(item);
}
@ -89,34 +109,4 @@ const SlideItem = ({item}) => {
);
};
export default SlideItem;
/* {
"type": "paragraph",
"raw": "This is a very fancy **bold** keynote",
"text": "This is a very fancy **bold** keynote",
"tokens": [
{
"type": "text",
"raw": "This is a very fancy ",
"text": "This is a very fancy "
},
{
"type": "strong",
"raw": "**bold**",
"text": "bold",
"tokens": [
{
"type": "text",
"raw": "bold",
"text": "bold"
}
]
},
{
"type": "text",
"raw": " keynote",
"text": " keynote"
}
]
} */
export default SlideItem;

@ -4,13 +4,13 @@ import Slide from "./Slide";
import SlideContext from "../shared/SlideContext";
import SettingsContext from "../shared/SettingsContext";
const SlidesList = ({deck}) => {
const {highlightColor} = useContext(SettingsContext);
const {slide, setSlide} = useContext(SlideContext);
const container = useRef();
const current = useRef();
const [scale, setScale] = useState(1);
const [first, setFirst] = useState(true);
const SlidesList = ({deck, meta, font, style}) => {
const {highlightColor, backgroundColor, color} = useContext(SettingsContext);
const {slide, setSlide} = useContext(SlideContext);
const container = useRef();
const current = useRef();
const [scale, setScale] = useState(1);
const [first, setFirst] = useState(true);
const sizeChange = useCallback(() => {
if (!container.current)
@ -34,7 +34,7 @@ const SlidesList = ({deck}) => {
if (!current.current)
return;
current.current.scrollIntoView({inline : "nearest"});
}, [current, slide]);
}, [current, slide, first]);
useEffect(() => {
const resizeListener = () => {
@ -49,10 +49,14 @@ const SlidesList = ({deck}) => {
}, [sizeChange, deck]);
return (
<aside className="slides-list" ref={container} style={{"--color-hightlight" : highlightColor}}>
<aside className="slides-list" ref={container} style={{
"--color-hightlight" : meta?.color_highlight || highlightColor,
"--color-slideBackground" : meta?.color_background || backgroundColor,
"--color-slideForeground" : meta?.color_text || color
}}>
{deck.map((currentSlide, index) => (
<div className="slide-wrap" key={index}>
<Slide data={currentSlide} className={index === slide ? "active" : ""} style={{transform : `scale(${scale}) translateX(0)`}} onClick={() => setSlide(index)} ref={index === slide ? current : null} />
<Slide data={currentSlide} className={index === slide ? "active" : ""} style={{transform : `scale(${scale}) translateX(0)`, fontFamily: meta?.font || font}} onClick={() => setSlide(index)} ref={index === slide ? current : null} />
</div>
))}
</aside>

@ -5,6 +5,8 @@ const Cheatsheet = Object.freeze([
["Stop presentation", "ESC"],
["Open file", "⌘+O"],
["Close file", "⌘+W"],
["Save file", "⌘+S"],
["Toggle editor", "⌘+E"],
["Next slide", "→, Page up"],
["Last slide", "←, Page down"],
["Black screen out", "B"],

@ -1,28 +1,33 @@
import React, {useContext} from "react";
import {Folder2Open, Cast, InfoCircle, XSquare} from "react-bootstrap-icons";
import {Folder2Open, Cast, InfoCircle, XSquare, LayoutSplit} from "react-bootstrap-icons";
import SlideContext from "../shared/SlideContext";
import Mode from "../models/Mode";
import {ReactComponent as Logo} from "../assets/images/logo_ation.svg";
const Toolbar = ({openFile, setShowTips, version}) => {
const Toolbar = ({openFile, setShowTips, version, toggleEdit}) => {
const {setMode, setSlide, slideCount} = useContext(SlideContext);
const present = () => {
setMode(Mode.PRESENT);
setSlide(0);
document.documentElement.requestFullscreen();
}
};
const toggleMaximize = () => {
window.api.maximize();
};
return (
<nav className="toolbar">
<nav className="toolbar" onDoubleClick={toggleMaximize}>
<button onClick={openFile} title="Open file [⌘+O]"><Folder2Open /></button>
<button onClick={present} title="Start presentation [F5]"><Cast /></button>
<Logo />
<button onClick={() => window.api.closeFile()} title="Close file [⌘+W]" disabled={slideCount < 1}><XSquare /></button>
<button onClick={() => setShowTips(true)} title="Show tips [TAB]"><InfoCircle /></button>
<button onClick={toggleEdit} title="Toggle editor [⌘+E]"><LayoutSplit /></button>
<small>v{version}</small>
</nav>
);

@ -1,8 +1,10 @@
import React, {useState, useEffect} from "react";
const Appearance = ({fonts}) => {
const [font, setFont] = useState("");
const [highlightColor, setHighlightColor] = useState("");
const [font, setFont] = useState("");
const [highlightColor, setHighlightColor] = useState("");
const [backgroundColor, setBackgroundColor] = useState("");
const [color, setColor] = useState("");
const changeFont = event => {
const value = event.target.value;
@ -16,9 +18,25 @@ const Appearance = ({fonts}) => {
window.appSettings.set("highlightColor", value);
};
const changeBackground = event => {
const value = event.target.value;
setBackgroundColor(value);
window.appSettings.set("backgroundColor", value);
};
const changeForegroundColor = event => {
const value = event.target.value;
setColor(value);
window.appSettings.set("color", value);
};
useEffect(() => {
(async setFont => setFont(await window.appSettings.get("font", "Iosevka")))(setFont);
(async setHighlightColor => setHighlightColor(await window.appSettings.get("highlightColor", "#e6c17b")))(setHighlightColor);
(async () => {
setBackgroundColor(await window.appSettings.get("backgroundColor", "#1a1a1a"));
setHighlightColor(await window.appSettings.get("highlightColor", "#e6c17b"));
setFont(await window.appSettings.get("font", "Iosevka"));
setColor(await window.appSettings.get("color", "#dddddd"));
})(setFont, setHighlightColor, setBackgroundColor, setColor);
}, []);
return (
@ -29,6 +47,10 @@ const Appearance = ({fonts}) => {
</select>
<label>Global highlight color:</label>
<input type="color" value={highlightColor} onChange={changeColor} />
<label>Slide background color:</label>
<input type="color" value={backgroundColor} onChange={changeBackground} />
<label>Slide font color:</label>
<input type="color" value={color} onChange={changeForegroundColor} />
</section>
);
};

@ -1,7 +1,8 @@
const Mode = Object.freeze({
NORMAL : 1,
PRESENT : 2,
BLACKOUT : 3
BLACKOUT : 3,
EDIT : 4
});
export default Mode;
Loading…
Cancel
Save