0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-01-07 12:02:44 +00:00

Merge branch 'master' into View-Modes

This commit is contained in:
Gazook89
2024-10-05 21:39:06 -05:00
57 changed files with 18440 additions and 16515 deletions

View File

@@ -1,7 +1,7 @@
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
require('./brewRenderer.less');
const React = require('react');
const { useState, useRef, useEffect } = React;
const { useState, useRef, useEffect, useCallback } = React;
const _ = require('lodash');
const MarkdownLegacy = require('naturalcrit/markdownLegacy.js');
@@ -49,23 +49,25 @@ let rawPages = [];
const BrewRenderer = (props)=>{
props = {
text : '',
style : '',
renderer : 'legacy',
theme : '5ePHB',
lang : '',
errors : [],
currentEditorPage : 0,
themeBundle : {},
text : '',
style : '',
renderer : 'legacy',
theme : '5ePHB',
lang : '',
errors : [],
currentEditorCursorPageNum : 1,
currentEditorViewPageNum : 1,
currentBrewRendererPageNum : 1,
themeBundle : {},
onPageChange : ()=>{},
...props
};
const [state, setState] = useState({
height : PAGE_HEIGHT,
isMounted : false,
visibility : 'hidden',
zoom : 100,
currentPageNumber : 1,
height : PAGE_HEIGHT,
isMounted : false,
visibility : 'hidden',
zoom : 100
});
const mainRef = useRef(null);
@@ -80,8 +82,6 @@ const BrewRenderer = (props)=>{
return ()=>{window.removeEventListener('resize', updateSize);};
}, []);
const updateSize = ()=>{
setState((prevState)=>({
...prevState,
@@ -89,27 +89,22 @@ const BrewRenderer = (props)=>{
}));
};
const getCurrentPage = (e) => {
const target = e.target;
const { scrollTop, clientHeight, scrollHeight } = target;
const updateCurrentPage = useCallback(_.throttle((e)=>{
const { scrollTop, clientHeight, scrollHeight } = e.target;
const totalScrollableHeight = scrollHeight - clientHeight;
const currentPageNumber = Math.max(Math.ceil((scrollTop / totalScrollableHeight) * rawPages.length), 1);
const currentPageNumber = Math.ceil((scrollTop / totalScrollableHeight) * rawPages.length);
setState((prevState) => ({
...prevState,
currentPageNumber : currentPageNumber || 1
}));
};
props.onPageChange(currentPageNumber);
}, 200), []);
const isInView = (index)=>{
if(!state.isMounted)
return false;
if(index == props.currentEditorPage) //Already rendered before this step
if(index == props.currentEditorCursorPageNum - 1) //Already rendered before this step
return false;
if(Math.abs(index - state.currentPageNumber) <= 3)
if(Math.abs(index - props.currentBrewRendererPageNum - 1) <= 3)
return true;
return false;
@@ -146,7 +141,7 @@ const BrewRenderer = (props)=>{
renderedPages.length = 0;
// Render currently-edited page first so cross-page effects (variables, links) can propagate out first
renderedPages[props.currentEditorPage] = renderPage(rawPages[props.currentEditorPage], props.currentEditorPage);
renderedPages[props.currentEditorCursorPageNum - 1] = renderPage(rawPages[props.currentEditorCursorPageNum - 1], props.currentEditorCursorPageNum - 1);
_.forEach(rawPages, (page, index)=>{
if((isInView(index) || !renderedPages[index]) && typeof window !== 'undefined'){
@@ -185,19 +180,18 @@ const BrewRenderer = (props)=>{
};
//Toolbar settings:
const handleZoom = (newZoom) => {
const handleZoom = (newZoom)=>{
setState((prevState)=>({
...prevState,
zoom : newZoom
}));
};
return (
<>
{/*render dummy page while iFrame is mounting.*/}
{!state.isMounted
? <div className='brewRenderer' onScroll={getCurrentPage}>
? <div className='brewRenderer' onScroll={updateCurrentPage}>
<div className='pages'>
{renderDummyPage(1)}
</div>
@@ -210,8 +204,7 @@ const BrewRenderer = (props)=>{
<NotificationPopup />
</div>
<ToolBar onZoomChange={handleZoom} currentPage={state.currentPageNumber} totalPages={rawPages.length}/>
<ToolBar onZoomChange={handleZoom} currentPage={props.currentBrewRendererPageNum} totalPages={rawPages.length}/>
{/*render in iFrame so broken code doesn't crash the site.*/}
<Frame id='BrewRenderer' initialContent={INITIAL_CONTENT}
@@ -220,7 +213,7 @@ const BrewRenderer = (props)=>{
onClick={()=>{emitClick();}}
>
<div className={'brewRenderer'}
onScroll={getCurrentPage}
onScroll={updateCurrentPage}
onKeyDown={handleControlKeys}
tabIndex={-1}
style={{ height: state.height }}>

View File

@@ -1,10 +1,9 @@
@import (multiple, less) 'shared/naturalcrit/styles/reset.less';
.brewRenderer {
padding-top : 30px;
overflow-y : scroll;
will-change : transform;
padding-top : 30px;
:where(.pages) {
margin : 30px 30px;
@@ -33,6 +32,7 @@
}
margin : 30px 0px;
& > :where(.page) {
width : 215.9mm;
height : 279.4mm;
@@ -42,20 +42,16 @@
box-shadow : 1px 4px 14px #000000;
}
}
&::-webkit-scrollbar {
width : 20px;
&:horizontal {
width : auto;
height : 20px;
}
&-thumb {
background : linear-gradient(90deg, #D3C1AF 15px, #00000000 15px);
&:horizontal { background : linear-gradient(0deg, #D3C1AF 15px, #00000000 15px); }
}
&-corner { visibility : hidden; }
}
}

View File

@@ -4,7 +4,7 @@ const _ = require('lodash');
import Dialog from '../../../components/dialog.jsx';
const DISMISS_KEY = 'dismiss_notification12-04-23';
const DISMISS_KEY = 'dismiss_notification04-09-24';
const DISMISS_BUTTON = <i className='fas fa-times dismiss' />;
const NotificationPopup = ()=>{
@@ -15,11 +15,12 @@ const NotificationPopup = ()=>{
<small>This website is always improving and we are still adding new features and squashing bugs. Keep the following in mind:</small>
</div>
<ul>
<li key='psa'>
<em>Don't store IMAGES in Google Drive</em><br />
Google Drive is not an image service, and will block images from being used
in brews if they get more views than expected. Google has confirmed they won't fix
this, so we recommend you look for another image hosting service such as imgur, ImgBB or Google Photos.
<li key='Vault'>
<em>Search brews with our new page!</em><br />
We have been working very hard in making this possible, now you can share your work and look at it in the new <a href='/vault'>Vault</a> page!
All PUBLISHED brews will be available to anyone searching there, by title or author, and filtering by renderer.
More features will be coming.
</li>
<li key='googleDriveFolder'>

View File

@@ -3,7 +3,6 @@ const React = require('react');
const { useState, useEffect } = React;
const _ = require('lodash');
import * as ZoomIcons from '../../../icons/icon-components/zoomIcons.jsx';
const MAX_ZOOM = 300;
const MIN_ZOOM = 10;
@@ -13,6 +12,7 @@ const ToolBar = ({ onZoomChange, currentPage, onPageChange, totalPages })=>{
const [zoomLevel, setZoomLevel] = useState(100);
const [pageNum, setPageNum] = useState(currentPage);
const [arrangement, setArrangement] = useState('single');
const [toolsVisible, setToolsVisible] = useState(true);
const modes = ['single', 'facing', 'flow'];
useEffect(()=>{
@@ -35,36 +35,26 @@ const ToolBar = ({ onZoomChange, currentPage, onPageChange, totalPages })=>{
}, [arrangement]);
const handleZoomButton = (delta)=>{
const newZoomLevel = _.round(_.clamp(zoomLevel + delta, MIN_ZOOM, MAX_ZOOM));
setZoomLevel(newZoomLevel);
const handleZoomButton = (zoom)=>{
setZoomLevel(_.round(_.clamp(zoom, MIN_ZOOM, MAX_ZOOM)));
};
const handlePageChange = (page)=>{
const regex = /[0-9]/;
if(regex.test(page)){
const num = parseInt(page); // input type is 'text', so `page` comes in as a string, not number.
setPageNum(num)
} else {
return;
}
const handlePageInput = (pageInput)=>{
if(/[0-9]/.test(pageInput))
setPageNum(parseInt(pageInput)); // input type is 'text', so `page` comes in as a string, not number.
};
const scrollToPage = (pageNumber)=>{
pageNumber = _.clamp(pageNumber, 1, totalPages);
const iframe = document.getElementById('BrewRenderer');
if(iframe && iframe.contentWindow) {
const brewRenderer = iframe.contentWindow.document.querySelector('.brewRenderer');
if(brewRenderer) {
const pages = brewRenderer.querySelectorAll('.page');
pages[pageNumber - 1]?.scrollIntoView({ block: 'start' });
}
}
const brewRenderer = iframe?.contentWindow?.document.querySelector('.brewRenderer');
const page = brewRenderer?.querySelector(`#p${pageNumber}`);
page?.scrollIntoView({ block: 'start' });
setPageNum(pageNumber);
};
const calculateZoom = (mode)=>{
const calculateChange = (mode)=>{
const iframe = document.getElementById('BrewRenderer');
const iframeWidth = iframe.getBoundingClientRect().width;
const iframeHeight = iframe.getBoundingClientRect().height;
@@ -74,30 +64,15 @@ const ToolBar = ({ onZoomChange, currentPage, onPageChange, totalPages })=>{
if(mode == 'fill'){
// find widest page, in case pages are different widths, so that the zoom is adapted to not cut the widest page off screen.
let widestPage = 0;
[...pages].forEach((page)=>{
const width = page.offsetWidth;
if(width > widestPage)
widestPage = width;
});
const widestPage = _.maxBy([...pages], 'offsetWidth').offsetWidth;
desiredZoom = (iframeWidth / widestPage) * 100;
} else if(mode == 'fit'){
// find the page with the largest single dim (height or width) so that zoom can be adapted to fit it.
let maxDimRatio = 0;
[...pages].forEach((page)=>{
const widthRatio = iframeWidth / page.offsetWidth;
const heightRatio = iframeHeight / page.offsetHeight;
const dimensionRatio = Math.min(widthRatio, heightRatio);
if(dimensionRatio < maxDimRatio || maxDimRatio == 0){
maxDimRatio = dimensionRatio;
}
});
desiredZoom = maxDimRatio * 100;
const minDimRatio = [...pages].reduce((minRatio, page)=>Math.min(minRatio, iframeWidth / page.offsetWidth, iframeHeight / page.offsetHeight), Infinity);
desiredZoom = minDimRatio * 100;
}
const margin = 5; // extra space so page isn't edge to edge (not truly "to fill")
@@ -112,26 +87,28 @@ const ToolBar = ({ onZoomChange, currentPage, onPageChange, totalPages })=>{
};
return (
<div className='toolBar'>
<div className={`toolBar ${toolsVisible ? 'visible' : 'hidden'}`}>
<button className='toggleButton' title={`${toolsVisible ? 'Hide' : 'Show'} Preview Toolbar`} onClick={()=>{setToolsVisible(!toolsVisible);}}><i className='fas fa-glasses' /></button>
{/*v=====----------------------< Zoom Controls >---------------------=====v*/}
<div className='group'>
<button
id='zoom-to-fill'
id='fill-width'
className='tool'
onClick={()=>handleZoomButton(calculateZoom('fill'))}
onClick={()=>handleZoomButton(zoomLevel + calculateChange('fill'))}
>
<ZoomIcons.FitWidth title='Fit to Width' style={{ width: '1.5em' }} />
<i className='fac fit-width' />
</button>
<button
id='zoom-to-fit'
className='tool'
onClick={()=>handleZoomButton(calculateZoom('fit'))}
onClick={()=>handleZoomButton(zoomLevel + calculateChange('fit'))}
>
<ZoomIcons.FitAll title='Zoom to Fit' style={{ width: '1.5em' }} />
<i className='fac zoom-to-fit' />
</button>
<button
id='zoom-out'
className='tool'
onClick={()=>handleZoomButton(-20)}
onClick={()=>handleZoomButton(zoomLevel - 20)}
disabled={zoomLevel <= MIN_ZOOM}
>
<i className='fas fa-magnifying-glass-minus' />
@@ -146,7 +123,7 @@ const ToolBar = ({ onZoomChange, currentPage, onPageChange, totalPages })=>{
max={MAX_ZOOM}
step='1'
value={zoomLevel}
onChange={(e)=>{setZoomLevel(parseInt(e.target.value));}}
onChange={(e)=>handleZoomButton(parseInt(e.target.value))}
/>
<datalist id='zoomLevels'>
<option value='100' />
@@ -155,13 +132,14 @@ const ToolBar = ({ onZoomChange, currentPage, onPageChange, totalPages })=>{
<button
id='zoom-in'
className='tool'
onClick={()=>handleZoomButton(20)}
onClick={()=>handleZoomButton(zoomLevel + 20)}
disabled={zoomLevel >= MAX_ZOOM}
>
<i className='fas fa-magnifying-glass-plus' />
</button>
</div>
{/*v=====----------------------< Page Controls >---------------------=====v*/}
<div className='group'>
<button
id='book-mode'
@@ -191,15 +169,14 @@ const ToolBar = ({ onZoomChange, currentPage, onPageChange, totalPages })=>{
inputMode='numeric'
pattern='[0-9]'
value={pageNum}
onClick={(e)=>{e.target.select()}}
onChange={(e)=>{handlePageChange(e.target.value);}}
onClick={(e)=>e.target.select()}
onChange={(e)=>handlePageInput(e.target.value)}
onBlur={()=>scrollToPage(pageNum)}
onKeyDown={(e)=>{e.key == 'Enter' ? scrollToPage(pageNum) : null;}}
onKeyDown={(e)=>e.key == 'Enter' && scrollToPage(pageNum)}
/>
<span id='page-count'>/ {totalPages}</span>
</div>
<button
id='next-page'
className='tool'
@@ -210,7 +187,6 @@ const ToolBar = ({ onZoomChange, currentPage, onPageChange, totalPages })=>{
</button>
</div>
</div>
);
};

View File

@@ -1,3 +1,5 @@
@import (less) './client/icons/customIcons.less';
.toolBar {
position : absolute;
z-index : 1;
@@ -13,6 +15,10 @@
font-family : 'Open Sans', sans-serif;
color : #CCCCCC;
background-color : #555555;
& > *:not(.toggleButton) {
opacity: 1;
transition: all .2s ease;
}
.group {
box-sizing : border-box;
@@ -38,7 +44,7 @@
border : 1px solid gray;
&:focus { outline : 1px solid #D3D3D3; }
// `.range-input` if generic to all range inputs, or `#zoom-input` if only for zoom slider
// `.range-input` if generic to all range inputs, or `#zoom-slider` if only for zoom slider
&.range-input {
padding : 2px 0;
color : #D3D3D3;
@@ -73,12 +79,9 @@
width : 4ch;
margin-right : 1ch;
text-align : center;
}
}
button {
box-sizing : content-box;
display : flex;
@@ -91,14 +94,35 @@
font-weight : unset;
color : inherit;
background-color : unset;
&:hover { background-color : #444444; }
&:focus { outline : 1px solid #D3D3D3; }
&:disabled {
color : #777777;
background-color : unset !important;
}
i {
font-size:1.2em;
}
}
&.hidden {
width: 32px;
transition: all .3s ease;
flex-wrap:nowrap;
overflow: hidden;
background-color: unset;
opacity: .5;
& > *:not(.toggleButton) {
opacity: 0;
transition: all .2s ease;
}
}
}
button.toggleButton {
z-index : 5;
position:absolute;
left: 0;
width: 32px;
min-width: unset;
}