From 548335cc426c0f675012843ca71b8a9ca8c87daa Mon Sep 17 00:00:00 2001 From: Zach Hannum Date: Sun, 19 Jun 2022 23:15:45 -0400 Subject: [PATCH] Feature/generate pdf (#124) * Update theme, fix colors * Fix section rename bug * Add generate buttons and loader * Remove active id listener in paged previewer * Add API framework for generating PDF * Create window for paged renderer * Creating separate page entry for paged renderer * print to pdf once paged renderer is complete * Minor tweaks * update icons * Refactor modals * Switch to using modal for generating book, open pdf file in file explorer/finder when done * Add platforms checkbox to generate book modal * Fix packaged build and bump version Co-authored-by: Zach Hannum --- .erb/configs/webpack.config.renderer.dev.ts | 34 +- .erb/configs/webpack.config.renderer.prod.ts | 22 +- app/main/main.ts | 8 +- app/main/pdf/generatePdf.ts | 48 ++ app/main/pdf/index.ts | 1 + app/main/pdf/pdfListeners.ts | 12 + app/main/preload.ts | 19 +- app/renderer/components/ContextMenu.tsx | 2 +- .../components/MoreOptionsSidebarItem.tsx | 6 +- .../components/MoreOptionsSidebarMenu.tsx | 66 ++- app/renderer/components/NewBookModal.tsx | 115 ---- app/renderer/components/PagedPreviewer.tsx | 225 +++++++ app/renderer/components/PagedRenderer.tsx | 232 +------- app/renderer/components/Pane.tsx | 2 +- app/renderer/components/ScrollContainer.tsx | 12 +- .../components/SectionContextMenu.tsx | 9 +- .../components/SidebarProjectContent.tsx | 14 +- .../components/SidebarProjectSectionItem.tsx | 87 --- .../components/SidebarProjectSections.tsx | 4 +- .../TreeView/components/TreeItem/TreeItem.tsx | 19 +- .../components/codemirror/extensions/code.ts | 14 +- .../codemirror/extensions/search.ts | 14 +- .../components/codemirror/extensions/theme.ts | 2 +- app/renderer/components/index.ts | 5 +- app/renderer/controls/Button.tsx | 92 ++- app/renderer/controls/Checkbox.tsx | 108 ++++ app/renderer/controls/IconButton.tsx | 9 +- app/renderer/controls/TextField.tsx | 2 +- app/renderer/controls/index.ts | 1 + app/renderer/hooks/index.ts | 1 + app/renderer/hooks/useOnBookPdfGenerated.ts | 13 + app/renderer/icons/CheckIcon.tsx | 20 + app/renderer/icons/ExitIcon.tsx | 6 +- app/renderer/icons/GenerateBookIcon.tsx | 20 + app/renderer/icons/HelpIcon.tsx | 2 +- app/renderer/icons/InfoIcon.tsx | 20 + app/renderer/icons/NewBookIcon.tsx | 6 +- app/renderer/icons/NewFileIcon.tsx | 18 +- app/renderer/icons/NewFolderIcon.tsx | 7 +- app/renderer/icons/OpenBookIcon.tsx | 6 +- app/renderer/icons/PreviewIcon.tsx | 13 +- app/renderer/icons/SaveIcon.tsx | 6 +- app/renderer/icons/SectionDeleteIcon.tsx | 3 +- app/renderer/icons/SectionDuplicateIcon.tsx | 3 +- app/renderer/icons/SectionOpenIcon.tsx | 8 +- app/renderer/icons/SectionRenameIcon.tsx | 10 +- app/renderer/icons/SettingsIcon.tsx | 7 +- app/renderer/icons/SidebarClosedIcon.tsx | 9 +- app/renderer/icons/SidebarOpenIcon.tsx | 10 +- app/renderer/icons/UpdateIcon.tsx | 6 +- app/renderer/icons/index.ts | 3 + app/renderer/modals/GenerateBookModal.tsx | 91 +++ app/renderer/modals/Modal.tsx | 65 ++ app/renderer/modals/NewBookModal.tsx | 71 +++ app/renderer/modals/index.ts | 1 + app/renderer/paged.ejs | 15 + app/renderer/paged.tsx | 27 + app/renderer/panes/MainPane.tsx | 2 +- app/renderer/panes/Modals.tsx | 18 +- app/renderer/panes/PreviewPane.tsx | 59 +- app/renderer/panes/SidebarPane.tsx | 12 +- app/renderer/preload.d.ts | 8 +- .../store/slices/createAppStateSlice.ts | 6 + .../store/slices/createProjectSlice.ts | 7 + app/renderer/theme/theme.ts | 4 +- app/renderer/utils/buildBook.ts | 37 ++ app/renderer/utils/buildPdf.ts | 19 + app/renderer/utils/projectUtils.ts | 10 +- app/types/types.ts | 7 + package.json | 554 +++++++++--------- release/app/package.json | 2 +- yarn.lock | 281 ++++++++- 72 files changed, 1754 insertions(+), 923 deletions(-) create mode 100644 app/main/pdf/generatePdf.ts create mode 100644 app/main/pdf/index.ts create mode 100644 app/main/pdf/pdfListeners.ts delete mode 100644 app/renderer/components/NewBookModal.tsx create mode 100644 app/renderer/components/PagedPreviewer.tsx mode change 100755 => 100644 app/renderer/components/PagedRenderer.tsx delete mode 100644 app/renderer/components/SidebarProjectSectionItem.tsx create mode 100644 app/renderer/controls/Checkbox.tsx create mode 100644 app/renderer/hooks/useOnBookPdfGenerated.ts create mode 100644 app/renderer/icons/CheckIcon.tsx create mode 100644 app/renderer/icons/GenerateBookIcon.tsx create mode 100644 app/renderer/icons/InfoIcon.tsx create mode 100644 app/renderer/modals/GenerateBookModal.tsx create mode 100644 app/renderer/modals/Modal.tsx create mode 100644 app/renderer/modals/NewBookModal.tsx create mode 100644 app/renderer/modals/index.ts create mode 100755 app/renderer/paged.ejs create mode 100755 app/renderer/paged.tsx create mode 100644 app/renderer/utils/buildBook.ts create mode 100755 app/renderer/utils/buildPdf.ts diff --git a/.erb/configs/webpack.config.renderer.dev.ts b/.erb/configs/webpack.config.renderer.dev.ts index 3a34c83..64a6d51 100644 --- a/.erb/configs/webpack.config.renderer.dev.ts +++ b/.erb/configs/webpack.config.renderer.dev.ts @@ -46,16 +46,23 @@ const configuration: webpack.Configuration = { target: ['web', 'electron-renderer'], - entry: [ - `webpack-dev-server/client?http://localhost:${port}/dist`, - 'webpack/hot/only-dev-server', - path.join(webpackPaths.srcRendererPath, 'index.tsx'), - ], + entry: { + main: [ + `webpack-dev-server/client?http://localhost:${port}/dist`, + 'webpack/hot/only-dev-server', + path.join(webpackPaths.srcRendererPath, 'index.tsx'), + ], + paged: [ + `webpack-dev-server/client?http://localhost:${port}/dist`, + 'webpack/hot/only-dev-server', + path.join(webpackPaths.srcRendererPath, 'paged.tsx'), + ], + }, output: { path: webpackPaths.distRendererPath, publicPath: '/', - filename: 'renderer.dev.js', + filename: '[name].dev.js', library: { type: 'umd', }, @@ -134,6 +141,21 @@ const configuration: webpack.Configuration = { new HtmlWebpackPlugin({ filename: path.join('index.html'), template: path.join(webpackPaths.srcRendererPath, 'index.ejs'), + chunks: ['main'], + minify: { + collapseWhitespace: true, + removeAttributeQuotes: true, + removeComments: true, + }, + isBrowser: false, + env: process.env.NODE_ENV, + isDevelopment: process.env.NODE_ENV !== 'production', + nodeModules: webpackPaths.appNodeModulesPath, + }), + new HtmlWebpackPlugin({ + filename: path.join('paged.html'), + template: path.join(webpackPaths.srcRendererPath, 'paged.ejs'), + chunks: ['paged'], minify: { collapseWhitespace: true, removeAttributeQuotes: true, diff --git a/.erb/configs/webpack.config.renderer.prod.ts b/.erb/configs/webpack.config.renderer.prod.ts index 5f381ce..ea1bc33 100644 --- a/.erb/configs/webpack.config.renderer.prod.ts +++ b/.erb/configs/webpack.config.renderer.prod.ts @@ -32,12 +32,15 @@ const configuration: webpack.Configuration = { target: ['web', 'electron-renderer'], - entry: [path.join(webpackPaths.srcRendererPath, 'index.tsx')], + entry: { + main: [path.join(webpackPaths.srcRendererPath, 'index.tsx')], + paged: [path.join(webpackPaths.srcRendererPath, 'paged.tsx')], + }, output: { path: webpackPaths.distRendererPath, publicPath: './', - filename: 'renderer.js', + filename: '[name].js', library: { type: 'umd', }, @@ -105,7 +108,7 @@ const configuration: webpack.Configuration = { }), new MiniCssExtractPlugin({ - filename: 'style.css', + filename: '[name].css', }), new BundleAnalyzerPlugin({ @@ -114,6 +117,7 @@ const configuration: webpack.Configuration = { new HtmlWebpackPlugin({ filename: 'index.html', + chunks: ['main'], template: path.join(webpackPaths.srcRendererPath, 'index.ejs'), minify: { collapseWhitespace: true, @@ -123,6 +127,18 @@ const configuration: webpack.Configuration = { isBrowser: false, isDevelopment: process.env.NODE_ENV !== 'production', }), + new HtmlWebpackPlugin({ + filename: 'paged.html', + chunks: ['paged'], + template: path.join(webpackPaths.srcRendererPath, 'paged.ejs'), + minify: { + collapseWhitespace: true, + removeAttributeQuotes: true, + removeComments: true, + }, + isBrowser: false, + isDevelopment: process.env.NODE_ENV !== 'production', + }), ], }; diff --git a/app/main/main.ts b/app/main/main.ts index 2e61315..d5607bb 100644 --- a/app/main/main.ts +++ b/app/main/main.ts @@ -15,6 +15,7 @@ import log from 'electron-log'; import MenuBuilder from './menu'; import { getPlatformWindowSettings, resolveHtmlPath } from './util'; import { setupProjectListeners } from './project'; +import { setupPdfListeners } from './pdf'; export default class AppUpdater { constructor() { @@ -85,8 +86,8 @@ const createWindow = async () => { mainWindow = new BrowserWindow({ show: false, - width: 1024, - height: 728, + width: 1200, + height: 700, minWidth: 800, minHeight: 600, icon: getAssetPath('icon.png'), @@ -131,7 +132,7 @@ const createWindow = async () => { // Open urls in the user's browser mainWindow.webContents.setWindowOpenHandler((edata) => { shell.openExternal(edata.url); - return { action: 'deny' }; + return { action: 'allow' }; }); // Remove this if your app does not use auto updates @@ -139,6 +140,7 @@ const createWindow = async () => { new AppUpdater(); setupProjectListeners(mainWindow); + setupPdfListeners(mainWindow); }; /** diff --git a/app/main/pdf/generatePdf.ts b/app/main/pdf/generatePdf.ts new file mode 100644 index 0000000..1faea9d --- /dev/null +++ b/app/main/pdf/generatePdf.ts @@ -0,0 +1,48 @@ +import { BrowserWindow, ipcMain, app, shell } from 'electron'; +import fs from 'fs'; +import path from 'path'; +import { resolveHtmlPath } from '../util'; +import { PagedBookContents } from 'types/types'; + +const generatePdf = ( + mainWindow: BrowserWindow, + pdfBookContent: PagedBookContents +) => { + console.log('Creating pdf render window'); + const pdfWindow = new BrowserWindow({ + show: false, + parent: mainWindow, + width: 500, + height: 500, + minWidth: 800, + minHeight: 600, + webPreferences: { + preload: app.isPackaged + ? path.join(__dirname, 'preload.js') + : path.join(__dirname, '../../../.erb/dll/preload.js'), + }, + }); + pdfWindow.loadURL(resolveHtmlPath('paged.html')); + pdfWindow.webContents.once('dom-ready', () => { + console.log('sending paged contents'); + pdfWindow.webContents.send('pagedContents', pdfBookContent); + }); + ipcMain.once('pagedRenderComplete', (_event, _arg) => { + pdfWindow.webContents.printToPDF({}).then((buffer: Buffer) => { + const filePath = path.join( + app.getPath('downloads'), + pdfBookContent.title.toLowerCase().trim().replace(/\s+/g, '_') + '.pdf' + ); + fs.writeFile(filePath, buffer, (err) => { + if (err) { + console.log(err); + } + }); + pdfWindow.close(); + mainWindow.webContents.send('pdfGenerated', buffer); + shell.showItemInFolder(filePath); + }); + }); +}; + +export default generatePdf; diff --git a/app/main/pdf/index.ts b/app/main/pdf/index.ts new file mode 100644 index 0000000..3a6045e --- /dev/null +++ b/app/main/pdf/index.ts @@ -0,0 +1 @@ +export { default as setupPdfListeners } from './pdfListeners'; diff --git a/app/main/pdf/pdfListeners.ts b/app/main/pdf/pdfListeners.ts new file mode 100644 index 0000000..845be3e --- /dev/null +++ b/app/main/pdf/pdfListeners.ts @@ -0,0 +1,12 @@ +import { ipcMain } from 'electron'; +import { BrowserWindow } from 'electron'; +import { PagedBookContents } from '../../types/types'; +import generatePdf from './generatePdf'; + +const setupProjectListeners = (mainWindow: BrowserWindow) => { + ipcMain.on('generatePdf', async (_event, arg: PagedBookContents) => { + generatePdf(mainWindow, arg); + }); +}; + +export default setupProjectListeners; diff --git a/app/main/preload.ts b/app/main/preload.ts index f025dfc..d9b55f8 100644 --- a/app/main/preload.ts +++ b/app/main/preload.ts @@ -1,5 +1,9 @@ import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron'; -import type { BookDetails, ProjectData } from '../types/types'; +import type { + BookDetails, + ProjectData, + PagedBookContents, +} from '../types/types'; contextBridge.exposeInMainWorld('electron', { ipcRenderer: { @@ -7,7 +11,7 @@ contextBridge.exposeInMainWorld('electron', { ipcRenderer.send('ipc-example', 'ping'); }, on(channel: string, func: (...args: unknown[]) => void) { - const validChannels = ['ipc-example', 'window']; + const validChannels = ['ipc-example', 'window', 'pagedContents']; if (validChannels.includes(channel)) { const subscription = (_event: IpcRendererEvent, ...args: unknown[]) => func(...args); @@ -20,7 +24,7 @@ contextBridge.exposeInMainWorld('electron', { return undefined; }, once(channel: string, func: (...args: unknown[]) => void) { - const validChannels = ['ipc-example', 'window']; + const validChannels = ['ipc-example', 'window', 'pagedContents']; if (validChannels.includes(channel)) { // Deliberately strip event as it includes `sender` ipcRenderer.once(channel, (_event, ...args) => func(...args)); @@ -49,3 +53,12 @@ contextBridge.exposeInMainWorld('projectApi', { onOpenProject: (func: (projectData: ProjectData) => void) => ipcRenderer.on('openProject', (_event, arg) => func(arg)), }); + +contextBridge.exposeInMainWorld('pagedApi', { + generateBookPdf: (pagedBookContents: PagedBookContents) => { + ipcRenderer.send('generatePdf', pagedBookContents); + }, + onBookPdfGenerated: (func: (pdfStream: Buffer) => void) => + ipcRenderer.on('pdfGenerated', (_event, arg) => func(arg)), + pagedRenderComplete: () => ipcRenderer.send('pagedRenderComplete'), +}); diff --git a/app/renderer/components/ContextMenu.tsx b/app/renderer/components/ContextMenu.tsx index b1f5033..8407671 100755 --- a/app/renderer/components/ContextMenu.tsx +++ b/app/renderer/components/ContextMenu.tsx @@ -26,7 +26,7 @@ const StyledContextMenu = styled.div` opacity: ${(p) => (p.show ? '1' : '0')}; transform: ${(p) => (p.show ? 'scale(1)' : 'scale(.7)')}; animation: ${onOpenKeyframes} 100ms ease-in-out alternate; - background-color: ${(p) => Color(p.theme.contextMenuBg).lighten(0.2).hex()}; + background-color: ${(p) => Color(p.theme.contextMenuBg).lighten(0.2).hsl().string()}; border: ${(p) => p.theme.contextMenuDivider} 1px solid; backdrop-filter: blur(40px); border-radius: 10px; diff --git a/app/renderer/components/MoreOptionsSidebarItem.tsx b/app/renderer/components/MoreOptionsSidebarItem.tsx index 63620c5..dab9709 100644 --- a/app/renderer/components/MoreOptionsSidebarItem.tsx +++ b/app/renderer/components/MoreOptionsSidebarItem.tsx @@ -63,8 +63,8 @@ const MoreOptionsSidebarItem = ({ label, }: MoreOptionsSidebarItemProps) => { const theme = useTheme(); - const menuItemHoverColor = Color(theme.contextMenuBg).lighten(0.6).hex(); - const menuItemActiveColor = Color(theme.contextMenuBg).darken(0.2).hex(); + const menuItemHoverColor = Color(theme.contextMenuBg).lighten(0.6).hsl().string(); + const menuItemActiveColor = Color(theme.contextMenuBg).darken(0.2).hsl().string(); return ( {label} diff --git a/app/renderer/components/MoreOptionsSidebarMenu.tsx b/app/renderer/components/MoreOptionsSidebarMenu.tsx index d44bac8..fb89279 100644 --- a/app/renderer/components/MoreOptionsSidebarMenu.tsx +++ b/app/renderer/components/MoreOptionsSidebarMenu.tsx @@ -12,8 +12,9 @@ import { PreviewIcon, UpdateIcon, SaveIcon, + InfoIcon, + GenerateBookIcon, } from '../icons'; -import icon from '../../../assets/icon.png'; import useStore from '../store/useStore'; import { saveProject } from '../utils/projectUtils'; @@ -29,9 +30,11 @@ const MoreOptionsSidebarMenu = () => { const [showMenu, setShowMenu] = useState(false); const buttonRef = useRef(null); const isProjectOpen = useStore((state) => state.isProjectOpen); + const activeSectionId = useStore((state) => state.activeSectionId); const previewEnabled = useStore((state) => state.previewEnabled); const setPreviewEnabled = useStore((state) => state.setPreviewEnabled); const setNewBookModalOpen = useStore((state) => state.setNewBookModalOpen); + const setGenerateBookModalOpen = useStore((state) => state.setGenerateBookModalOpen); const [menuPosition, setMenuPosition] = useState({ x: 0, y: 0 }); const getMenuPosition = (): Position => { @@ -87,28 +90,45 @@ const MoreOptionsSidebarMenu = () => { window.projectApi.openProject(); }} /> - } - rightElement={⌘S} - label="Save Book" - onClick={() => { - setShowMenu(false); - saveProject(); - }} - /> - } - rightElement={ - + } + rightElement={⌘S} + label="Save Book" + onClick={() => { + if (isProjectOpen) { + setShowMenu(false); + saveProject(); + } + }} /> - } - label="Preview" - /> + } + rightElement={⌘G} + label="Generate Book" + onClick={() => { + setShowMenu(false); + setGenerateBookModalOpen(true); + }} + /> + } + rightElement={ + + } + label="Preview" + /> + + )} + { /> } + iconElement={} label="About" /> p.theme.modalFgText}; - user-select: none; -`; -const StyledModalContent = styled.div` - display: flex; - flex-direction: column; - flex-wrap: nowrap; - gap: 10px; - margin: 35px; - align-content: center; - justify-content: center; - align-items: center; -`; - -Modal.setAppElement('#root'); - -type NewBookModalProps = { - isOpen: boolean; - onRequestClose: () => void; -}; - -const NewBookModal = ({ isOpen, onRequestClose }: NewBookModalProps) => { - const theme = useTheme(); - const formRef = useRef(null); - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - const target = e.target as typeof e.target & { - book: { value: string }; - author: { value: string }; - series: { value: string }; - }; - window.projectApi.createProject({ - bookTitle: target.book.value, - authorName: target.author.value, - seriesName: target.series.value, - }); - onRequestClose(); - }; - return ( - -
- -
-
New Book
- - - - -
- - - - -
-
- - ); -}; - -export default NewBookModal; diff --git a/app/renderer/components/PagedPreviewer.tsx b/app/renderer/components/PagedPreviewer.tsx new file mode 100644 index 0000000..ff61fe3 --- /dev/null +++ b/app/renderer/components/PagedPreviewer.tsx @@ -0,0 +1,225 @@ +import { useRef, useEffect, useState, useMemo } from 'react'; +import styled, { css } from 'styled-components'; +import { Polisher, Chunker, initializeHandlers } from 'pagedjs'; +import { debounce } from 'lodash'; +import { baseStylesheet } from '../pagedjs/defaultPageCss'; +import { useResizeObserver } from '../hooks'; +import useStore from '../store/useStore'; +import { parseChapterContentToHtml } from 'renderer/utils/buildBook'; + +const StyledRenderer = styled.div` + height: 100%; + width: 70%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +`; + +const PageContainer = styled.div` + overflow: hidden; + box-shadow: rgba(0, 0, 0, 0.7) 0px 20px 35px; + background-color: ${(p) => p.theme.paperBg}; + border-radius: 5px; + .pagedjs_pages { + display: flex; + flex-direction: row; + } + .pagedjs_page { + background-color: ${(p) => p.theme.paperBg}; + } + height: var(--pagedjs-height); + width: var(--pagedjs-width); +`; + +const PagedStagingContainer = styled.div` + visibility: hidden; + position: absolute; + top: -100000px; +`; + +type StyledLoaderProps = { + loading: boolean; +}; + +type ScalerProps = { + scale: number; +}; +const Scaler = styled.div` + transform: scale(${(p) => p.scale}); + position: relative; +`; + +type PagedPreviewerProps = { + pageNumber: number; + onPageOverflow: (pageNumber: number) => void; +}; + +const PagedPreviewer = ({ pageNumber, onPageOverflow }: PagedPreviewerProps) => { + const pageContainerRef = useRef(null); + const pagedStageRef = useRef(null); + const rendererRef = useRef(null); + /* Testing dynamic css changes */ + const printParagraphFontSize = useStore( + (state) => state.printParagraphFontSize + ); + const previewContent = useStore((state) => state.previewContent); + const activeSectionId = useStore((state) => state.activeSectionId); + const polisher = useRef(null); + const chunker = useRef(null); + const [scale, setScale] = useState(0.5); + const [prevPage, setPrevPage] = useState(1); + const [page, setPage] = useState(1); + const [overflow, setOverflow] = useState(false); + const buildingPreview = useRef(false); + + const setPageCounterIncrement = (pageIncrement: number) => { + document.documentElement.style.setProperty( + '--pagedjs-page-counter-increment', + `${pageIncrement}` + ); + }; + + /* Navigate to new Page */ + useEffect(() => { + const pageContainer = pageContainerRef.current; + console.log(`navigating to page ${page}`); + if (pageContainer) { + const navigatePage = pageContainer.querySelector( + `[data-page-number="${page}"]` + ); + if (navigatePage) { + console.log(`page ${page} found.`); + navigatePage.style.display = ''; + navigatePage.scrollIntoView(); + if (prevPage != page) { + console.log( + `previous page ${prevPage} is different than current page, hiding previous page.` + ); + const prevPageElement = pageContainer.querySelector( + `[data-page-number="${prevPage}"]` + ); + if (prevPageElement && !overflow) { + prevPageElement.style.display = 'none'; + } + } + if (!overflow) { + setPageCounterIncrement(page); + } else if (overflow) { + setOverflow(false); + } + setPrevPage(page); + } else if (page !== 1) { + console.log(`Overflow detected, resetting to ${prevPage}`); + /* There should be always at least one page, so no overflow on page 1 */ + setOverflow(true); + onPageOverflow(prevPage); + } + } + }, [page]); + + const handleResize = (height: number, width: number) => { + if (pageContainerRef.current) { + const newScale = Math.min( + height / pageContainerRef.current.offsetHeight, + width / pageContainerRef.current.offsetWidth + ); + if (newScale !== Infinity) { + setScale(newScale); + } + } + }; + + useEffect(() => { + console.log(`Controls attempting to set page to ${pageNumber}`); + setPage(pageNumber); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [pageNumber]); + + useResizeObserver(rendererRef, 100, handleResize); + + const setPagedContent = async (htmlContent: string) => { + const template = document.querySelector('#flow'); + if (template) { + template.innerHTML = htmlContent; + const container = pageContainerRef.current; + const pagedStage = pagedStageRef.current; + console.log('Destroying previous polisher and chunker'); + if (polisher.current) polisher.current.destroy(); + if (chunker.current) chunker.current.destroy(); + polisher.current = new Polisher(); + chunker.current = new Chunker(); + if (polisher.current && chunker.current && pagedStage) { + pagedStage.style.display = ''; + polisher.current.setup(); + initializeHandlers(chunker.current, polisher.current); + console.log('Adding stylesheet'); + await polisher.current.add({ + '': baseStylesheet({ + paragraphFontSize: printParagraphFontSize, + }).toString(), + }); + console.log('Starting flow...'); + await chunker.current.flow(template.content, pagedStage); + console.log( + 'Flow complete! Copying flowed content to preview container.' + ); + pagedStage.style.display = 'none'; + if (container) { + container.innerHTML = ''; + pagedStage.childNodes.forEach((node) => { + container.appendChild(node.cloneNode(true)); + }); + } + + if (container) { + const paged = container.children[0]; + if (paged) { + const pageNum = Math.min(page, paged.children.length); + setPage(pageNum); + onPageOverflow(pageNum); + console.log( + `page is ${page}, max page length: ${paged.children.length}, setting page to ${pageNum}` + ); + for (let i = 0; i < paged.children.length; i += 1) { + if (i === pageNum - 1) continue; + (paged.children[i] as HTMLElement).style.display = 'none'; + } + } + } + } + } + }; + + const updatePreview = async () => { + if (!buildingPreview.current) { + buildingPreview.current = true; + const { previewContent } = useStore.getState(); + console.log('parsing preview content'); + const html = parseChapterContentToHtml(previewContent); + console.log('setting paged content'); + await setPagedContent(html); + buildingPreview.current = false; + } + }; + + const updatePreviewDebounce = useMemo( + () => debounce(updatePreview, 500, { trailing: true }), + [page] + ); + + useEffect(() => { + updatePreviewDebounce(); + }, [previewContent]); + + return ( + + + + + + + ); +}; + +export default PagedPreviewer; diff --git a/app/renderer/components/PagedRenderer.tsx b/app/renderer/components/PagedRenderer.tsx old mode 100755 new mode 100644 index 0786b3b..cf6c346 --- a/app/renderer/components/PagedRenderer.tsx +++ b/app/renderer/components/PagedRenderer.tsx @@ -1,239 +1,49 @@ -import { useRef, useEffect, useState, useMemo } from 'react'; -import styled, { css } from 'styled-components'; +import { useRef, useEffect } from 'react'; import { Polisher, Chunker, initializeHandlers } from 'pagedjs'; -import { debounce } from 'lodash'; -import { unified } from 'unified'; -import remarkParse from 'remark-parse'; -import remarkRehype from 'remark-rehype'; -import rehypeStringify from 'rehype-stringify'; -import { rehypeSection } from '../utils/unified'; -import { baseStylesheet } from '../pagedjs/defaultPageCss'; -import { useResizeObserver } from '../hooks'; -import useStore from '../store/useStore'; +import { PagedBookContents } from 'types/types'; -const StyledRenderer = styled.div` - height: 70%; - width: 70%; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; -`; - -const PageContainer = styled.div` - overflow: hidden; - box-shadow: rgba(0, 0, 0, 0.7) 0px 20px 35px; - background-color: ${(p) => p.theme.paperBg}; - border-radius: 5px; - .pagedjs_pages { - display: flex; - flex-direction: row; - } - .pagedjs_page { - background-color: ${(p) => p.theme.paperBg}; - } - height: var(--pagedjs-height); - width: var(--pagedjs-width); -`; - -const PagedStagingContainer = styled.div` - visibility: hidden; - position: absolute; - top: -100000px; -`; - -type StyledLoaderProps = { - loading: boolean; -}; - -type ScalerProps = { - scale: number; -}; -const Scaler = styled.div` - transform: scale(${(p) => p.scale}); - position: relative; -`; - -type PagedRendererProps = { - pageNumber: number; - onPageOverflow: (pageNumber: number) => void; -}; - -const PagedRenderer = ({ pageNumber, onPageOverflow }: PagedRendererProps) => { +const PagedRenderer = () => { const pageContainerRef = useRef(null); - const pagedStageRef = useRef(null); - const rendererRef = useRef(null); - /* Testing dynamic css changes */ - const printParagraphFontSize = useStore( - (state) => state.printParagraphFontSize - ); - const previewContent = useStore((state) => state.previewContent); - const activeSectionId = useStore((state) => state.activeSectionId); + const polisher = useRef(null); const chunker = useRef(null); - const [scale, setScale] = useState(0.5); - const [prevPage, setPrevPage] = useState(1); - const [page, setPage] = useState(1); - const [overflow, setOverflow] = useState(false); - const buildingPreview = useRef(false); - - const setPageCounterIncrement = (pageIncrement: number) => { - document.documentElement.style.setProperty( - '--pagedjs-page-counter-increment', - `${pageIncrement}` - ); - }; useEffect(() => { - setPage(1); - onPageOverflow(1); - }, [activeSectionId]); - - /* Navigate to new Page */ - useEffect(() => { - const pageContainer = pageContainerRef.current; - console.log(`navigating to page ${page}`); - if (pageContainer) { - const navigatePage = pageContainer.querySelector( - `[data-page-number="${page}"]` - ); - if (navigatePage) { - console.log(`page ${page} found.`); - navigatePage.style.display = ''; - navigatePage.scrollIntoView(); - if (prevPage != page) { - console.log( - `previous page ${prevPage} is different than current page, hiding previous page.` - ); - const prevPageElement = pageContainer.querySelector( - `[data-page-number="${prevPage}"]` - ); - if (prevPageElement && !overflow) { - prevPageElement.style.display = 'none'; - } - } - if (!overflow) { - setPageCounterIncrement(page); - } else if (overflow) { - setOverflow(false); - } - setPrevPage(page); - } else if (page !== 1) { - console.log(`Overflow detected, resetting to ${prevPage}`); - /* There should be always at least one page, so no overflow on page 1 */ - setOverflow(true); - onPageOverflow(prevPage); - } - } - }, [page]); - - const handleResize = (height: number, width: number) => { - if (pageContainerRef.current) { - const newScale = Math.min( - height / pageContainerRef.current.offsetHeight, - width / pageContainerRef.current.offsetWidth - ); - if (newScale !== Infinity) { - setScale(newScale); - } - } - }; - - useEffect(() => { - console.log(`Controls attempting to set page to ${pageNumber}`); - setPage(pageNumber); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [pageNumber]); - - useResizeObserver(rendererRef, 100, handleResize); - - const setPagedContent = async (htmlContent: string) => { + console.log("Setting paged content listener"); + window.electron.ipcRenderer.once('pagedContents', (arg) => { + const { html, css } = arg as PagedBookContents; + console.log(html); + setPagedContent(html, css); + }); + }, []); + + const setPagedContent = async (html: string, css: string) => { const template = document.querySelector('#flow'); + console.log(template); if (template) { - template.innerHTML = htmlContent; + template.innerHTML = html; const container = pageContainerRef.current; - const pagedStage = pagedStageRef.current; console.log('Destroying previous polisher and chunker'); if (polisher.current) polisher.current.destroy(); if (chunker.current) chunker.current.destroy(); polisher.current = new Polisher(); chunker.current = new Chunker(); - if (polisher.current && chunker.current && pagedStage) { - pagedStage.style.display = ''; + if (polisher.current && chunker.current) { polisher.current.setup(); initializeHandlers(chunker.current, polisher.current); console.log('Adding stylesheet'); await polisher.current.add({ - '': baseStylesheet({ - paragraphFontSize: printParagraphFontSize, - }).toString(), + '': css, }); console.log('Starting flow...'); - await chunker.current.flow(template.content, pagedStage); - console.log( - 'Flow complete! Copying flowed content to preview container.' - ); - pagedStage.style.display = 'none'; - if (container) { - container.innerHTML = ''; - pagedStage.childNodes.forEach((node) => { - container.appendChild(node.cloneNode(true)); - }); - } - - if (container) { - const paged = container.children[0]; - if (paged) { - const pageNum = Math.min(page, paged.children.length); - setPage(pageNum); - onPageOverflow(pageNum); - console.log( - `page is ${page}, max page length: ${paged.children.length}, setting page to ${pageNum}` - ); - for (let i = 0; i < paged.children.length; i += 1) { - if (i === pageNum - 1) continue; - (paged.children[i] as HTMLElement).style.display = 'none'; - } - } - } + await chunker.current.flow(template.content, container); + console.log('Flow complete!'); + window.pagedApi.pagedRenderComplete(); } } }; - const updatePreview = async () => { - if (!buildingPreview.current) { - buildingPreview.current = true; - const { previewContent } = useStore.getState(); - console.log('parsing preview content'); - const html = unified() - .use(remarkParse) - .use(remarkRehype) - .use(rehypeSection) - .use(rehypeStringify) - .processSync(previewContent); - console.log('setting paged content'); - await setPagedContent(html.value.toString()); - buildingPreview.current = false; - } - }; - - const updatePreviewDebounce = useMemo( - () => debounce(updatePreview, 500, { trailing: true }), - [page] - ); - - useEffect(() => { - updatePreviewDebounce(); - }, [previewContent]); - - return ( - - - - - - - ); + return
; }; export default PagedRenderer; diff --git a/app/renderer/components/Pane.tsx b/app/renderer/components/Pane.tsx index 922abc7..5c0659e 100644 --- a/app/renderer/components/Pane.tsx +++ b/app/renderer/components/Pane.tsx @@ -82,7 +82,7 @@ const Pane = ({ styleMixin, }: PaneProps) => { const [width, setWidth] = useState(defaultWidth); - const resizerHoverColor = Color(backgroundColor).alpha(1).lighten(0.5).hex(); + const resizerHoverColor = Color(backgroundColor).alpha(1).lighten(0.5).hsl().string(); const handleResize = (resizeEvent: React.MouseEvent) => { const startSize = parseInt(width, 10); diff --git a/app/renderer/components/ScrollContainer.tsx b/app/renderer/components/ScrollContainer.tsx index d69ff31..7ba8fa8 100755 --- a/app/renderer/components/ScrollContainer.tsx +++ b/app/renderer/components/ScrollContainer.tsx @@ -1,6 +1,6 @@ import React from 'react'; import styled, { css } from 'styled-components'; - +import Color from 'color'; const Scroller = styled.div` overflow-y: overlay; @@ -26,8 +26,8 @@ const Scroller = styled.div` /* display: none; */ } ::-webkit-scrollbar-thumb { - background-color: inherit; - background-color: rgba(0, 0, 0, 0.1); + background-color: ${(p) => + Color(p.theme.mainBg).alpha(1).darken(0.2).hsl().string()}; } &:hover { -webkit-mask-position: left top; @@ -36,6 +36,7 @@ const Scroller = styled.div` css` ::-webkit-scrollbar-thumb { border-radius: 4px; + cursor: pointer; } `} `; @@ -44,11 +45,8 @@ type Props = { children: React.ReactNode; }; - const ScrollContainer = ({ children }: Props) => ( - - {children} - + {children} ); export default ScrollContainer; diff --git a/app/renderer/components/SectionContextMenu.tsx b/app/renderer/components/SectionContextMenu.tsx index 095b216..dfaabc9 100644 --- a/app/renderer/components/SectionContextMenu.tsx +++ b/app/renderer/components/SectionContextMenu.tsx @@ -39,10 +39,10 @@ const StyledContextMenuItem = styled.div` color: ${(p) => p.theme.mainFgTextSecondary}; &:hover { - background-color: ${(p) => Color(p.theme.contextMenuBg).lighten(0.6).hex()}; + background-color: ${(p) => Color(p.theme.contextMenuBg).lighten(0.6).hsl().string()}; } &:active { - background-color: ${(p) => Color(p.theme.contextMenuBg).darken(0.2).hex()}; + background-color: ${(p) => Color(p.theme.contextMenuBg).darken(0.2).hsl().string()}; } transition: background-color 100ms ease-in-out; @@ -99,8 +99,11 @@ const SectionContextMenu = () => { }; const handleDelete = () => { - const { content, setContentArray } = useStore.getState(); + const { content, setContentArray, activeSectionId, setActiveSectionId } = useStore.getState(); setContentArray(removeItem(content, id)); + if(id === activeSectionId) { + setActiveSectionId(''); + } setShowMenu(false); }; diff --git a/app/renderer/components/SidebarProjectContent.tsx b/app/renderer/components/SidebarProjectContent.tsx index 8971fe3..5be0362 100644 --- a/app/renderer/components/SidebarProjectContent.tsx +++ b/app/renderer/components/SidebarProjectContent.tsx @@ -48,7 +48,7 @@ const StyledAnchor = styled.a` cursor: pointer; color: ${(p) => p.theme.buttonPrimaryBg}; &:hover { - color: ${(p) => Color(p.theme.buttonPrimaryBg).darken(0.1).hex()}; + color: ${(p) => Color(p.theme.buttonPrimaryBg).darken(0.1).hsl().string()}; } `; @@ -76,17 +76,17 @@ const SidebarProjectContent = () => { You haven’t opened a project yet. - Or start a new project. To learn more about how to use Calamus, you can always check out our help pages. diff --git a/app/renderer/components/SidebarProjectSectionItem.tsx b/app/renderer/components/SidebarProjectSectionItem.tsx deleted file mode 100644 index 073bc20..0000000 --- a/app/renderer/components/SidebarProjectSectionItem.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { useState, useRef, useEffect } from 'react'; -import styled, { css } from 'styled-components'; -import Color from 'color'; -import { updateSectionName } from '../utils/projectUtils'; - -const StyledItem = styled.div` - cursor: ${(p) => (p.contentEditable ? 'text' : 'pointer')}; - color: ${(p) => p.theme.sidebarFgText}; - font-size: 0.9em; - width: 95%; - box-sizing: border-box; - padding: 3px 6px; - border-radius: 5px; - &:hover { - background-color: ${(p) => - p.contentEditable - ? Color(p.theme.sidebarBg).alpha(1).darken(0.6).hex() - : Color(p.theme.sidebarBg).alpha(1).lighten(0.8).hex()}; - } - ${(p) => - p.contentEditable && - css` - background-color: ${(p) => Color(p.theme.sidebarBg).alpha(1).darken(0.6).hex()}; - :focus { - outline: none; - } - `} - transition: background-color 100ms ease-in-out; -`; - -type SidebarProjectSectionItemProps = { - value: string; - addingNew: boolean; -}; - -const SidebarProjectSectionItem = ({ - value, - addingNew, -}: SidebarProjectSectionItemProps) => { - const [isEditable, setIsEditable] = useState(false); - const itemRef = useRef(null); - useEffect(() => { - if (addingNew) { - setIsEditable(true); - itemRef.current?.scrollTo({ behavior: 'smooth' }); - setTimeout(() => { - itemRef.current?.focus(); - }); - } - }, []); - const handleDoubleClick = () => { - setIsEditable(true); - setTimeout(() => { - itemRef.current?.focus(); - }, 10); - }; - const handleBlur = () => { - setIsEditable(false); - - const newValue = itemRef.current?.innerText; - if (newValue) updateSectionName(value, newValue); - }; - const handleKeyDown = (event: React.KeyboardEvent) => { - if (event.code === 'Enter') { - event.preventDefault(); - handleBlur(); - } else if (event.code === 'Escape') { - event.preventDefault(); - if (itemRef.current) itemRef.current.innerText = value; - setIsEditable(false); - } - }; - return ( - - {value} - - ); -}; - -export default SidebarProjectSectionItem; diff --git a/app/renderer/components/SidebarProjectSections.tsx b/app/renderer/components/SidebarProjectSections.tsx index fa3eea4..d7585fe 100644 --- a/app/renderer/components/SidebarProjectSections.tsx +++ b/app/renderer/components/SidebarProjectSections.tsx @@ -65,7 +65,9 @@ const SectionsContainer = styled.div` } ::-webkit-scrollbar-thumb { background-color: inherit; - background-color: ${(p) => Color(p.theme.sidebarBg).darken(0.1).hex()}; + background-color: ${(p) => + Color(p.theme.sidebarBg).alpha(1).darken(0.2).hsl().string()}; + cursor: pointer; } &:hover { -webkit-mask-position: left top; diff --git a/app/renderer/components/TreeView/components/TreeItem/TreeItem.tsx b/app/renderer/components/TreeView/components/TreeItem/TreeItem.tsx index 3781e2f..e498aba 100755 --- a/app/renderer/components/TreeView/components/TreeItem/TreeItem.tsx +++ b/app/renderer/components/TreeView/components/TreeItem/TreeItem.tsx @@ -86,7 +86,7 @@ const StyledTreeItem = styled.div` p.ghost && css` margin-left: 10px; - background-color: ${Color(p.theme.sidebarBg).lighten(0.3).hex()}; + background-color: ${Color(p.theme.sidebarBg).lighten(0.3).hsl().string()}; > * { /* Items are hidden using height and opacity to retain focus */ opacity: 0; @@ -100,24 +100,24 @@ const StyledTreeItem = styled.div` cursor: pointer; ${p.contextOpen && css` - background-color: ${Color(p.theme.sidebarBg).lighten(0.3).hex()}; + background-color: ${Color(p.theme.sidebarBg).lighten(0.3).hsl().string()}; `} &:hover { - background-color: ${Color(p.theme.sidebarBg).lighten(0.3).hex()}; + background-color: ${Color(p.theme.sidebarBg).lighten(0.3).hsl().string()}; } ${p.isActiveInEditor && css` - background-color: ${Color(p.theme.buttonPrimaryBg)}; + background-color: ${Color(p.theme.buttonPrimaryBg).hsl().string()}; &:hover { - background-color: ${Color(p.theme.buttonPrimaryBg)}; + background-color: ${Color(p.theme.buttonPrimaryBg).hsl().string()}; } `} ${p.isEditable && css` cursor: text; - background-color: ${Color(p.theme.sidebarBg).darken(0.2).hex()}; + background-color: ${Color(p.theme.sidebarBg).darken(0.2).hsl().string()}; &:hover { - background-color: ${Color(p.theme.sidebarBg).darken(0.2).hex()}}; + background-color: ${Color(p.theme.sidebarBg).darken(0.2).hsl().string()}}; } `} `} @@ -125,7 +125,7 @@ const StyledTreeItem = styled.div` ${(p) => p.clone && css` - background-color: ${Color(p.theme.sidebarBg).alpha(0.8).lighten(0.3).hex()}; + background-color: ${Color(p.theme.sidebarBg).alpha(0.8).lighten(0.3).hsl().string()}; `} transition: background-color 100ms ease-in-out; @@ -254,6 +254,7 @@ export const TreeItem = forwardRef( const handleBlur = () => { setIsEditable(false); const newValue = textRef.current?.innerText; + console.log(newValue); if (newValue) { const success = updateSectionName(value, newValue); if (!success) { @@ -273,8 +274,10 @@ export const TreeItem = forwardRef( }, []); const handleKeyDown = (event: React.KeyboardEvent) => { if (event.code === 'Enter') { + console.log("What is happening"); event.preventDefault(); setIsEditable(false); + handleBlur(); } else if (event.code === 'Escape') { event.preventDefault(); if (textRef.current) textRef.current.innerText = value; diff --git a/app/renderer/components/codemirror/extensions/code.ts b/app/renderer/components/codemirror/extensions/code.ts index eb331a1..4bdbbe9 100644 --- a/app/renderer/components/codemirror/extensions/code.ts +++ b/app/renderer/components/codemirror/extensions/code.ts @@ -15,7 +15,7 @@ import { selectionIntersection } from './hideMarkdown'; const codeDecorationsBaseTheme = (theme: DefaultTheme) => EditorView.baseTheme({ '.cm-inline-code-hidden': { - backgroundColor: `${Color(theme.mainBg).darken(0.2).hex()}`, + backgroundColor: `${Color(theme.mainBg).darken(0.2).hsl().string()}`, paddingLeft: '5px', paddingRight: '5px', borderRadius: '5px', @@ -26,7 +26,7 @@ const codeDecorationsBaseTheme = (theme: DefaultTheme) => display: 'inline-block', }, '.cm-inline-code-mark-left': { - backgroundColor: `${Color(theme.mainBg).darken(0.2).hex()}`, + backgroundColor: `${Color(theme.mainBg).darken(0.2).hsl().string()}`, borderRadius: '5px 0px 0px 5px', paddingTop: '3px', paddingBottom: '3px', @@ -36,7 +36,7 @@ const codeDecorationsBaseTheme = (theme: DefaultTheme) => display: 'inline-block', }, '.cm-inline-code-mark-right': { - backgroundColor: `${Color(theme.mainBg).darken(0.2).hex()}`, + backgroundColor: `${Color(theme.mainBg).darken(0.2).hsl().string()}`, borderRadius: '0px 5px 5px 0px', paddingTop: '3px', paddingBottom: '3px', @@ -46,7 +46,7 @@ const codeDecorationsBaseTheme = (theme: DefaultTheme) => display: 'inline-block', }, '.cm-inline-code-content': { - backgroundColor: `${Color(theme.mainBg).darken(0.2).hex()}`, + backgroundColor: `${Color(theme.mainBg).darken(0.2).hsl().string()}`, fontFamily: 'Roboto Mono', paddingTop: '3px', paddingBottom: '3px', @@ -54,7 +54,7 @@ const codeDecorationsBaseTheme = (theme: DefaultTheme) => display: 'inline-block', }, '.cm-fenced-code-line-first': { - backgroundColor: `${Color(theme.mainBg).darken(0.2).hex()}`, + backgroundColor: `${Color(theme.mainBg).darken(0.2).hsl().string()}`, fontFamily: 'Roboto Mono', paddingLeft: '5px', paddingRight: '5px', @@ -63,14 +63,14 @@ const codeDecorationsBaseTheme = (theme: DefaultTheme) => boxSizing: 'border-box', }, '.cm-fenced-code-line': { - backgroundColor: `${Color(theme.mainBg).darken(0.2).hex()}`, + backgroundColor: `${Color(theme.mainBg).darken(0.2).hsl().string()}`, fontFamily: 'Roboto Mono', paddingLeft: '5px', paddingRight: '5px', boxSizing: 'border-box', }, '.cm-fenced-code-line-last': { - backgroundColor: `${Color(theme.mainBg).darken(0.2).hex()}`, + backgroundColor: `${Color(theme.mainBg).darken(0.2).hsl().string()}`, fontFamily: 'Roboto Mono', paddingLeft: '5px', paddingRight: '5px', diff --git a/app/renderer/components/codemirror/extensions/search.ts b/app/renderer/components/codemirror/extensions/search.ts index 4c562c2..1949b1c 100644 --- a/app/renderer/components/codemirror/extensions/search.ts +++ b/app/renderer/components/codemirror/extensions/search.ts @@ -7,7 +7,7 @@ import type {DefaultTheme} from 'styled-components'; const search = (theme: DefaultTheme): Extension => { const searchTheme = EditorView.baseTheme({ '.cm-panels': { - backgroundColor: `${Color(theme.contextMenuBg).alpha(1).hex()}`, + backgroundColor: `${Color(theme.contextMenuBg).alpha(1).hsl().string()}`, borderRadius: '10px 10px 0px 0px', boxShadow: '0 4px 8px 0 rgba(0, 0, 0, 0.2)', padding: '10px', @@ -27,12 +27,12 @@ const search = (theme: DefaultTheme): Extension => { fontSize: '1em', '&:focus': { outline: 'none', - backgroundColor: `${Color(theme.textInputBg['altTwo']).lighten(0.2).hex()}`, + backgroundColor: `${Color(theme.textInputBg['altTwo']).lighten(0.2).hsl().string()}`, }, }, '.cm-button': { all: 'unset', - backgroundColor: `${Color(theme.contextMenuBg).lighten(0.3).alpha(1).hex()}`, + backgroundColor: `${Color(theme.contextMenuBg).lighten(0.3).alpha(1).hsl().string()}`, color: `${theme.buttonPrimaryText}`, padding: '0px 5px', fontFamily: 'Poppins', @@ -42,10 +42,10 @@ const search = (theme: DefaultTheme): Extension => { transition: 'background-color 200ms ease-in-out', cursor: 'pointer', '&:hover': { - backgroundColor: `${Color(theme.contextMenuBg).lighten(0.5).alpha(1).hex()}`, + backgroundColor: `${Color(theme.contextMenuBg).lighten(0.5).alpha(1).hsl().string()}`, }, '&:active': { - backgroundColor: `${Color(theme.contextMenuBg).lighten(0.1).alpha(1).hex()}`, + backgroundColor: `${Color(theme.contextMenuBg).lighten(0.1).alpha(1).hsl().string()}`, }, }, '.cm-panel.cm-search label': { @@ -63,10 +63,10 @@ const search = (theme: DefaultTheme): Extension => { color: `${theme.contextMenuFg}`, cursor: 'pointer', '&:hover': { - color: `${Color(theme.contextMenuFg).lighten(0.2).hex()}`, + color: `${Color(theme.contextMenuFg).lighten(0.2).hsl().string()}`, }, '&:active': { - color: `${Color(theme.contextMenuFg).darken(0.2).hex()}`, + color: `${Color(theme.contextMenuFg).darken(0.2).hsl().string()}`, }, }, '.cm-searchMatch': { diff --git a/app/renderer/components/codemirror/extensions/theme.ts b/app/renderer/components/codemirror/extensions/theme.ts index be07757..ee998fd 100755 --- a/app/renderer/components/codemirror/extensions/theme.ts +++ b/app/renderer/components/codemirror/extensions/theme.ts @@ -9,7 +9,7 @@ const theme = (theme: DefaultTheme): Extension => { '&.cm-editor': {}, '&.cm-editor.cm-focused': { outline: 'none' }, '&.cm-focused .cm-selectionBackground, ::selection': { - backgroundColor: `${Color(theme.mainBg).lighten(0.6).hex()}`, + backgroundColor: `${Color(theme.mainBg).lighten(0.6).hsl().string()}`, }, '.cm-scroller': { fontFamily: 'Poppins', diff --git a/app/renderer/components/index.ts b/app/renderer/components/index.ts index 64a91ca..50cea6b 100644 --- a/app/renderer/components/index.ts +++ b/app/renderer/components/index.ts @@ -2,10 +2,11 @@ export { default as MoreOptionsSidebarItem } from './MoreOptionsSidebarItem'; export { default as MoreOptionsSidebarMenu } from './MoreOptionsSidebarMenu'; export { default as SidebarProjectContent } from './SidebarProjectContent'; export { default as ScrollContainer } from './ScrollContainer'; +export { default as PagedPreviewer } from './PagedPreviewer'; export { default as PagedRenderer } from './PagedRenderer'; export { default as Pane } from './Pane'; export { SortableTree } from './TreeView/SortableTree'; export { default as SectionContextMenu } from './SectionContextMenu'; export { default as ContextMenu } from './ContextMenu'; -export { default as TooltipText} from './TooltipText'; -export { default as Editor} from './codemirror/Editor'; +export { default as TooltipText } from './TooltipText'; +export { default as Editor } from './codemirror/Editor'; diff --git a/app/renderer/controls/Button.tsx b/app/renderer/controls/Button.tsx index 300789d..5a5a6c3 100644 --- a/app/renderer/controls/Button.tsx +++ b/app/renderer/controls/Button.tsx @@ -1,51 +1,107 @@ -import styled, { useTheme } from 'styled-components'; +import styled, { useTheme, css } from 'styled-components'; import Color from 'color'; +import { BounceLoader } from 'react-spinners'; type StyledButtonProps = { hoverBackgroundcolor?: string; activeBackgroundColor?: string; + loading?: boolean; + disabled?: boolean; }; const StyledButton = styled.span` height: 30px; + position: relative; line-height: 30px; - padding-left: 30px; - padding-right: 30px; - cursor: pointer; + padding-left: 20px; + padding-right: 20px; user-select: none; background-color: ${(p) => p.theme.buttonPrimaryBg}; - &:hover { - background-color: ${(p) => p.hoverBackgroundcolor}; - } - &:active { - background-color: ${(p) => p.activeBackgroundColor}; - } color: ${(p) => p.theme.buttonPrimaryText}; border-radius: 10px; text-align: center; - transition: background-color 100ms ease-in-out; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 0.9em; + + ${(p) => + !p.loading && + !p.disabled && + css` + cursor: pointer; + &:hover { + background-color: ${p.hoverBackgroundcolor}; + } + &:active { + background-color: ${p.activeBackgroundColor}; + } + transition: background-color 100ms ease-in-out; + `} +`; + +type StyledLoaderProps = { + loading: boolean; + disabled: boolean; +}; +const StyledLoader = styled.div` + position: absolute; + height: 100%; + width: 100%; + z-index: 2; + top: 0; + left: 0; + background-color: #00000065; + display: flex; + align-items: center; + justify-content: center; + align-content: center; + opacity: 0; + ${(p) => + (p.loading || p.disabled) && + css` + opacity: 1; + `} + transition: opacity 100ms ease-in-out; `; type ButtonProps = { - label: string; - onClick: () => void; + children?: React.ReactNode; + onClick?: () => void; + loading?: boolean; + disabled?: boolean; }; -const Button = ({ label, onClick }: ButtonProps) => { +const Button = ({ + children, + onClick, + loading = false, + disabled = false, +}: ButtonProps) => { const theme = useTheme(); - const hoverColor = Color(theme.buttonPrimaryBg).lighten(0.05).hex(); - const activeColor = Color(theme.buttonPrimaryBg).darken(0.05).hex(); + const hoverColor = Color(theme.buttonPrimaryBg).lighten(0.05).hsl().string(); + const activeColor = Color(theme.buttonPrimaryBg).darken(0.05).hsl().string(); return ( { + if (!loading && !disabled && onClick) { + onClick(); + } + }} + loading={loading} + disabled={disabled} > - {label} + {children} + + + ); }; diff --git a/app/renderer/controls/Checkbox.tsx b/app/renderer/controls/Checkbox.tsx new file mode 100644 index 0000000..8b96f84 --- /dev/null +++ b/app/renderer/controls/Checkbox.tsx @@ -0,0 +1,108 @@ +import styled, { useTheme, css } from 'styled-components'; +import Color from 'color'; +import { useToggle } from '../hooks'; +import { CheckIcon } from '../icons'; + +const StyledLabel = styled.label` + display: flex; + align-items: center; + justify-content: center; + color: ${(p) => p.theme.checkFg}; + gap: 8px; + user-select: none; + cursor: pointer; +`; + +const StyledInputCheckbox = styled.input.attrs({ type: 'checkbox' })` + border: 0; + clip: rect(0 0 0 0); + clippath: inset(50%); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + white-space: nowrap; + width: 1px; +`; + +type StyledCheckboxProps = { + checked: boolean; +}; + +const StyledCheckbox = styled.div` + height: 20px; + width: 20px; + border-radius: 7px; + display: flex; + align-items: center; + justify-content: center; + background-color: ${(p) => p.theme.checkUnselectedBg}; + color: ${(p) => p.theme.checkFg}; + &:hover { + background-color: ${(p) => + Color(p.theme.checkUnselectedBg).lighten(0.1).hsl().toString()}; + } + &:active { + background-color: ${(p) => + Color(p.theme.checkUnselectedBg).darken(0.1).hsl().toString()}; + } + ${(p) => + p.checked && + css` + background-color: ${p.theme.checkSelectedBg}; + &:hover { + background-color: ${(p) => + Color(p.theme.checkSelectedBg).lighten(0.05).hsl().toString()}; + } + &:active { + background-color: ${(p) => + Color(p.theme.checkSelectedBg).darken(0.05).hsl().toString()}; + } + `} + + transition: background-color 100ms ease-in-out; +`; + +const StyledCheck = styled.div` + opacity: 0; + ${(p) => + p.checked && + css` + opacity: 1; + `} + transition: opacity 100ms ease-in-out; +`; + +type CheckboxProps = { + label?: string; + checked: boolean; + onChange: (checked: boolean) => void; +}; + +const Checkbox = ({ + label = '', + checked, + onChange, + ...props +}: CheckboxProps) => { + const theme = useTheme(); + return ( + + + { + onChange(e.target.checked); + }} + /> + + + + + {label} + + ); +}; + +export default Checkbox; diff --git a/app/renderer/controls/IconButton.tsx b/app/renderer/controls/IconButton.tsx index e99c785..a924b81 100755 --- a/app/renderer/controls/IconButton.tsx +++ b/app/renderer/controls/IconButton.tsx @@ -97,16 +97,15 @@ const IconButton = React.forwardRef( ) => { const hoverForegroundColor = Color(foregroundColor) .lighten(colorAdjustment) - .hex(); + .hsl().string(); const hoverBackgroundColor = Color(backgroundColor) - .lighten(colorAdjustment) - .hex(); + .lighten(colorAdjustment).hsl().string(); const activeForegroundColor = Color(foregroundColor) .darken(colorAdjustment) - .hex(); + .hsl().string(); const activeBackgroundColor = Color(backgroundColor) .darken(colorAdjustment) - .hex(); + .hsl().string(); return ( {label !== undefined && {label}} diff --git a/app/renderer/controls/index.ts b/app/renderer/controls/index.ts index 6240289..4a252e4 100644 --- a/app/renderer/controls/index.ts +++ b/app/renderer/controls/index.ts @@ -3,3 +3,4 @@ export { default as ToggleSwitch } from './ToggleSwitch'; export { default as WinControls } from './WinControls'; export { default as TextField } from './TextField'; export { default as Button } from './Button'; +export { default as Checkbox } from './Checkbox'; diff --git a/app/renderer/hooks/index.ts b/app/renderer/hooks/index.ts index b440234..1a7ed79 100644 --- a/app/renderer/hooks/index.ts +++ b/app/renderer/hooks/index.ts @@ -6,3 +6,4 @@ export { default as useOpenProject } from './useOpenProject'; export { default as useOnWheel } from './useOnWheel'; export { default as useOnClickOutside } from './useOnClickOutside'; export { default as useIsHovering } from './useIsHovering'; +export { default as useOnBookPdfGenerated } from './useOnBookPdfGenerated'; diff --git a/app/renderer/hooks/useOnBookPdfGenerated.ts b/app/renderer/hooks/useOnBookPdfGenerated.ts new file mode 100644 index 0000000..901b3a3 --- /dev/null +++ b/app/renderer/hooks/useOnBookPdfGenerated.ts @@ -0,0 +1,13 @@ +import { useEffect } from 'react'; +import useStore from '../store/useStore'; + +const useOnBookPdfGenerated = (callback: () => void) => { + useEffect(() => { + window.pagedApi.onBookPdfGenerated((pdfStream: Buffer) => { + + callback(); + }); + }, []); +}; + +export default useOnBookPdfGenerated; diff --git a/app/renderer/icons/CheckIcon.tsx b/app/renderer/icons/CheckIcon.tsx new file mode 100644 index 0000000..7037a9b --- /dev/null +++ b/app/renderer/icons/CheckIcon.tsx @@ -0,0 +1,20 @@ +import Icon from './Icon'; +import { IconProps, IconPropDefaults } from './type'; + +const CheckIcon = (props: IconProps) => { + return ( + + + + ); +}; + +CheckIcon.defaultProps = { + ...IconPropDefaults, +}; + +export default CheckIcon; diff --git a/app/renderer/icons/ExitIcon.tsx b/app/renderer/icons/ExitIcon.tsx index da017a8..8e2b8e5 100644 --- a/app/renderer/icons/ExitIcon.tsx +++ b/app/renderer/icons/ExitIcon.tsx @@ -4,7 +4,11 @@ import { IconProps, IconPropDefaults } from './type'; const ExitIcon = (props: IconProps) => { return ( - + ); }; diff --git a/app/renderer/icons/GenerateBookIcon.tsx b/app/renderer/icons/GenerateBookIcon.tsx new file mode 100644 index 0000000..b647c98 --- /dev/null +++ b/app/renderer/icons/GenerateBookIcon.tsx @@ -0,0 +1,20 @@ +import Icon from './Icon'; +import { IconProps, IconPropDefaults } from './type'; + +const GenerateBookIcon = (props: IconProps) => { + return ( + + + + ); +}; + +GenerateBookIcon.defaultProps = { + ...IconPropDefaults, +}; + +export default GenerateBookIcon; diff --git a/app/renderer/icons/HelpIcon.tsx b/app/renderer/icons/HelpIcon.tsx index 288bb6a..b3860b8 100644 --- a/app/renderer/icons/HelpIcon.tsx +++ b/app/renderer/icons/HelpIcon.tsx @@ -7,7 +7,7 @@ const HelpIcon = (props: IconProps) => { ); diff --git a/app/renderer/icons/InfoIcon.tsx b/app/renderer/icons/InfoIcon.tsx new file mode 100644 index 0000000..84f96a6 --- /dev/null +++ b/app/renderer/icons/InfoIcon.tsx @@ -0,0 +1,20 @@ +import Icon from './Icon'; +import { IconProps, IconPropDefaults } from './type'; + +const InfoIcon = (props: IconProps) => { + return ( + + + + ); +}; + +InfoIcon.defaultProps = { + ...IconPropDefaults, +}; + +export default InfoIcon; diff --git a/app/renderer/icons/NewBookIcon.tsx b/app/renderer/icons/NewBookIcon.tsx index 1c424f9..69a4038 100644 --- a/app/renderer/icons/NewBookIcon.tsx +++ b/app/renderer/icons/NewBookIcon.tsx @@ -4,7 +4,11 @@ import { IconProps, IconPropDefaults } from './type'; const NewBookIcon = (props: IconProps) => { return ( - + ); }; diff --git a/app/renderer/icons/NewFileIcon.tsx b/app/renderer/icons/NewFileIcon.tsx index 8437b1e..3539c08 100644 --- a/app/renderer/icons/NewFileIcon.tsx +++ b/app/renderer/icons/NewFileIcon.tsx @@ -4,19 +4,11 @@ import { IconProps, IconPropDefaults } from './type'; const NewFileIcon = (props: IconProps) => { return ( - - - - - - - - - {' '} + ); }; diff --git a/app/renderer/icons/NewFolderIcon.tsx b/app/renderer/icons/NewFolderIcon.tsx index 42d5838..585a970 100644 --- a/app/renderer/icons/NewFolderIcon.tsx +++ b/app/renderer/icons/NewFolderIcon.tsx @@ -5,10 +5,9 @@ const NewFolderIcon = (props: IconProps) => { return ( - ); diff --git a/app/renderer/icons/OpenBookIcon.tsx b/app/renderer/icons/OpenBookIcon.tsx index d02865b..04a620c 100644 --- a/app/renderer/icons/OpenBookIcon.tsx +++ b/app/renderer/icons/OpenBookIcon.tsx @@ -4,7 +4,11 @@ import { IconProps, IconPropDefaults } from './type'; const OpenBookIcon = (props: IconProps) => { return ( - + ); }; diff --git a/app/renderer/icons/PreviewIcon.tsx b/app/renderer/icons/PreviewIcon.tsx index 9d16d1a..9b8c923 100644 --- a/app/renderer/icons/PreviewIcon.tsx +++ b/app/renderer/icons/PreviewIcon.tsx @@ -4,14 +4,11 @@ import { IconProps, IconPropDefaults } from './type'; const PreviewIcon = (props: IconProps) => { return ( - - - - - - - - + ); }; diff --git a/app/renderer/icons/SaveIcon.tsx b/app/renderer/icons/SaveIcon.tsx index 21e53ec..5317a02 100644 --- a/app/renderer/icons/SaveIcon.tsx +++ b/app/renderer/icons/SaveIcon.tsx @@ -4,7 +4,11 @@ import { IconProps, IconPropDefaults } from './type'; const SaveIcon = (props: IconProps) => { return ( - + ); }; diff --git a/app/renderer/icons/SectionDeleteIcon.tsx b/app/renderer/icons/SectionDeleteIcon.tsx index 46cf947..29fccdd 100644 --- a/app/renderer/icons/SectionDeleteIcon.tsx +++ b/app/renderer/icons/SectionDeleteIcon.tsx @@ -4,11 +4,10 @@ import { IconProps, IconPropDefaults } from './type'; const SectionDeleteIcon = (props: IconProps) => { return ( - ); diff --git a/app/renderer/icons/SectionDuplicateIcon.tsx b/app/renderer/icons/SectionDuplicateIcon.tsx index 688643a..adca4ab 100644 --- a/app/renderer/icons/SectionDuplicateIcon.tsx +++ b/app/renderer/icons/SectionDuplicateIcon.tsx @@ -7,9 +7,8 @@ const SectionDuplicateIcon = (props: IconProps) => { - ); }; diff --git a/app/renderer/icons/SectionOpenIcon.tsx b/app/renderer/icons/SectionOpenIcon.tsx index dcee706..e882908 100644 --- a/app/renderer/icons/SectionOpenIcon.tsx +++ b/app/renderer/icons/SectionOpenIcon.tsx @@ -4,16 +4,10 @@ import { IconProps, IconPropDefaults } from './type'; const SectionOpenIcon = (props: IconProps) => { return ( - - ); diff --git a/app/renderer/icons/SectionRenameIcon.tsx b/app/renderer/icons/SectionRenameIcon.tsx index e14a6d6..04833e1 100644 --- a/app/renderer/icons/SectionRenameIcon.tsx +++ b/app/renderer/icons/SectionRenameIcon.tsx @@ -4,11 +4,11 @@ import { IconProps, IconPropDefaults } from './type'; const SectionRenameIcon = (props: IconProps) => { return ( - - - - - + ); }; diff --git a/app/renderer/icons/SettingsIcon.tsx b/app/renderer/icons/SettingsIcon.tsx index 73d1a9d..ae119aa 100644 --- a/app/renderer/icons/SettingsIcon.tsx +++ b/app/renderer/icons/SettingsIcon.tsx @@ -7,13 +7,8 @@ const SettingsIcon = (props: IconProps) => { - {' '} ); }; diff --git a/app/renderer/icons/SidebarClosedIcon.tsx b/app/renderer/icons/SidebarClosedIcon.tsx index bc02f3e..a69477a 100755 --- a/app/renderer/icons/SidebarClosedIcon.tsx +++ b/app/renderer/icons/SidebarClosedIcon.tsx @@ -4,8 +4,13 @@ import { IconProps, IconPropDefaults } from './type'; const SidebarClosedIcon = (props: IconProps) => { return ( - - + + + ); }; diff --git a/app/renderer/icons/SidebarOpenIcon.tsx b/app/renderer/icons/SidebarOpenIcon.tsx index c52710b..3829a65 100755 --- a/app/renderer/icons/SidebarOpenIcon.tsx +++ b/app/renderer/icons/SidebarOpenIcon.tsx @@ -4,8 +4,14 @@ import { IconProps, IconPropDefaults } from './type'; const SidebarOpenIcon = (props: IconProps) => { return ( - - + + ); }; diff --git a/app/renderer/icons/UpdateIcon.tsx b/app/renderer/icons/UpdateIcon.tsx index ab3f05f..faaeedb 100644 --- a/app/renderer/icons/UpdateIcon.tsx +++ b/app/renderer/icons/UpdateIcon.tsx @@ -4,7 +4,11 @@ import { IconProps, IconPropDefaults } from './type'; const UpdateIcon = ({ size, color }: IconProps) => { return ( - + ); }; diff --git a/app/renderer/icons/index.ts b/app/renderer/icons/index.ts index 015389d..04d2f3e 100755 --- a/app/renderer/icons/index.ts +++ b/app/renderer/icons/index.ts @@ -25,3 +25,6 @@ export { default as SectionDeleteIcon } from './SectionDeleteIcon'; export { default as SectionDuplicateIcon } from './SectionDuplicateIcon'; export { default as SectionOpenIcon } from './SectionOpenIcon'; export { default as SectionRenameIcon } from './SectionRenameIcon'; +export { default as GenerateBookIcon } from './GenerateBookIcon'; +export { default as InfoIcon } from './InfoIcon'; +export { default as CheckIcon } from './CheckIcon'; diff --git a/app/renderer/modals/GenerateBookModal.tsx b/app/renderer/modals/GenerateBookModal.tsx new file mode 100644 index 0000000..c5a261f --- /dev/null +++ b/app/renderer/modals/GenerateBookModal.tsx @@ -0,0 +1,91 @@ +import { useRef, useState } from 'react'; +import styled from 'styled-components'; +import Modal from './Modal'; +import type { ModalProps } from './Modal'; +import { Button, Checkbox } from '../controls'; +import { buildBookPdf } from 'renderer/utils/buildPdf'; +import { useOnBookPdfGenerated, useToggle } from 'renderer/hooks'; + +const StyledModalContent = styled.div` + display: flex; + color: ${(p) => p.theme.contextMenuFg}; + box-sizing: border-box; + flex-direction: column; + flex-wrap: nowrap; + gap: 10px; + margin: 30px; + align-content: center; + justify-content: center; + align-items: flex-start; +`; + +const StyledButtonDiv = styled.div` + display: flex; + align-content: center; + justify-content: center; + align-items: center; + margin: 20px; +`; + +const CheckboxDiv = styled.div` + display: flex; + margin: 20px 40px; + align-content: center; + justify-content: center; + align-items: center; +`; + +const ModalTitle = styled.span` + font-size: 1.1em; +`; + +const GenerateBookModal = (props: ModalProps) => { + const formRef = useRef(null); + const [isBuildingPdf, setIsBuildingPdf] = useState(false); + const [pdfPlatformToggleValue, togglePdfPlatformToggleValue] = + useToggle(false); + + useOnBookPdfGenerated(() => { + setIsBuildingPdf(false); + }); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + const target = e.target as typeof e.target & { + book: { value: string }; + author: { value: string }; + series: { value: string }; + }; + setIsBuildingPdf(true); + buildBookPdf(); + }; + return ( + +
+ + Output Platforms + + + + + + + +
+
+ ); +}; + +export default GenerateBookModal; diff --git a/app/renderer/modals/Modal.tsx b/app/renderer/modals/Modal.tsx new file mode 100644 index 0000000..f09b603 --- /dev/null +++ b/app/renderer/modals/Modal.tsx @@ -0,0 +1,65 @@ +import ReactModal from 'react-modal'; +import styled, { useTheme } from 'styled-components'; +import { IconButton } from '../controls'; +import { ModalExitIcon } from '../icons'; + +const StyledModalTitle = styled.div` + display: flex; + flex-direction: row; + flex-wrap: no-wrap; + justify-content: space-between; + align-items: center; + padding: 10px; + font-size: 1.1em; + width: 400px; + color: ${(p) => p.theme.modalFgText}; + user-select: none; +`; + +ReactModal.setAppElement('#root'); + +export type ModalProps = { + title: string; + isOpen: boolean; + onRequestClose: () => void; + children?: React.ReactNode; +}; + +const Modal = ({ title, isOpen, onRequestClose, children }: ModalProps) => { + const theme = useTheme(); + + return ( + + <> + +
+
{title}
+ + + + + {children} + + + ); +}; + +export default Modal; diff --git a/app/renderer/modals/NewBookModal.tsx b/app/renderer/modals/NewBookModal.tsx new file mode 100644 index 0000000..2a287e6 --- /dev/null +++ b/app/renderer/modals/NewBookModal.tsx @@ -0,0 +1,71 @@ +import { useRef } from 'react'; +import styled from 'styled-components'; +import Modal from './Modal'; +import type { ModalProps } from './Modal'; +import { TextField, Button } from '../controls'; + +const StyledModalContent = styled.div` + display: flex; + flex-direction: column; + flex-wrap: nowrap; + gap: 10px; + margin: 35px; + align-content: center; + justify-content: center; + align-items: center; +`; + +const NewBookModal = (props: ModalProps) => { + const { onRequestClose } = props; + const formRef = useRef(null); + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + const target = e.target as typeof e.target & { + book: { value: string }; + author: { value: string }; + series: { value: string }; + }; + window.projectApi.createProject({ + bookTitle: target.book.value, + authorName: target.author.value, + seriesName: target.series.value, + }); + onRequestClose(); + }; + return ( + +
+ + + + +
+ + + + + ); +}; + +export default NewBookModal; diff --git a/app/renderer/modals/index.ts b/app/renderer/modals/index.ts new file mode 100644 index 0000000..c263b79 --- /dev/null +++ b/app/renderer/modals/index.ts @@ -0,0 +1 @@ +export { default as NewBookModal } from './NewBookModal'; diff --git a/app/renderer/paged.ejs b/app/renderer/paged.ejs new file mode 100755 index 0000000..0f68818 --- /dev/null +++ b/app/renderer/paged.ejs @@ -0,0 +1,15 @@ + + + + + + + Calamus Paged + + + +
+