;
}
diff --git a/client/admin/brewUtils/brewLookup/brewLookup.less b/client/admin/brewUtils/brewLookup/brewLookup.less
deleted file mode 100644
index da15e3a64..000000000
--- a/client/admin/brewUtils/brewLookup/brewLookup.less
+++ /dev/null
@@ -1,6 +0,0 @@
-.brewLookup {
- .cleanButton {
- display : inline-block;
- width : 100%;
- }
-}
\ No newline at end of file
diff --git a/client/admin/brewUtils/brewUtils.jsx b/client/admin/brewUtils/brewUtils.jsx
index de8c29895..bab2cb82f 100644
--- a/client/admin/brewUtils/brewUtils.jsx
+++ b/client/admin/brewUtils/brewUtils.jsx
@@ -1,6 +1,6 @@
const React = require('react');
const createClass = require('create-react-class');
-
+require('./brewUtils.less');
const BrewCleanup = require('./brewCleanup/brewCleanup.jsx');
const BrewLookup = require('./brewLookup/brewLookup.jsx');
diff --git a/client/admin/brewUtils/brewUtils.less b/client/admin/brewUtils/brewUtils.less
new file mode 100644
index 000000000..5bbbc3f69
--- /dev/null
+++ b/client/admin/brewUtils/brewUtils.less
@@ -0,0 +1,29 @@
+.brewUtil {
+ .result {
+ margin-top : 20px;
+ button {
+ margin-right : 10px;
+ background-color : @red;
+ }
+ }
+ .cleanButton {
+ display : inline-block;
+ width : 100%;
+ }
+}
+
+.stats {
+ position : relative;
+
+ .pending {
+ position : absolute;
+ top : 0.5em;
+ left : 100px;
+ width : 100%;
+ height : 100%;
+ }
+
+ &:has(.pending) { opacity : 0.5; }
+
+ dl { grid-template-columns : 200px 250px; }
+}
\ No newline at end of file
diff --git a/client/admin/brewUtils/stats/stats.jsx b/client/admin/brewUtils/stats/stats.jsx
index 85ce10610..7f96618f9 100644
--- a/client/admin/brewUtils/stats/stats.jsx
+++ b/client/admin/brewUtils/stats/stats.jsx
@@ -1,11 +1,8 @@
-require('./stats.less');
const React = require('react');
const createClass = require('create-react-class');
-const cx = require('classnames');
const request = require('superagent');
-
const Stats = createClass({
displayName : 'Stats',
getDefaultProps(){
@@ -14,7 +11,8 @@ const Stats = createClass({
getInitialState(){
return {
stats : {
- totalBrews : 0
+ totalBrews : 0,
+ totalPublishedBrews : 0
},
fetching : false
};
@@ -29,11 +27,13 @@ const Stats = createClass({
.finally(()=>this.setState({ fetching: false }));
},
render(){
- return
+ return
Stats
Total Brew Count
{this.state.stats.totalBrews}
+ Total Brews Published
+ {this.state.stats.totalPublishedBrews}
{this.state.fetching
diff --git a/client/admin/brewUtils/stats/stats.less b/client/admin/brewUtils/stats/stats.less
deleted file mode 100644
index b5a4612e1..000000000
--- a/client/admin/brewUtils/stats/stats.less
+++ /dev/null
@@ -1,13 +0,0 @@
-
-.Stats {
- position : relative;
-
- .pending {
- position : absolute;
- top : 0px;
- left : 0px;
- width : 100%;
- height : 100%;
- background-color : rgba(238,238,238, 0.5);
- }
-}
\ No newline at end of file
diff --git a/client/admin/lockTools/lockTools.jsx b/client/admin/lockTools/lockTools.jsx
new file mode 100644
index 000000000..9a28d330f
--- /dev/null
+++ b/client/admin/lockTools/lockTools.jsx
@@ -0,0 +1,342 @@
+/*eslint max-lines: ["warn", {"max": 500, "skipBlankLines": true, "skipComments": true}]*/
+require('./lockTools.less');
+const React = require('react');
+const createClass = require('create-react-class');
+
+import request from '../../homebrew/utils/request-middleware.js';
+
+const LockTools = createClass({
+ displayName : 'LockTools',
+ getInitialState : function() {
+ return {
+ fetching : false,
+ reviewCount : 0
+ };
+ },
+
+ componentDidMount : function() {
+ this.updateReviewCount();
+ },
+
+ updateReviewCount : async function() {
+ const newCount = await request.get('/api/lock/count')
+ .then((res)=>{return res.body?.count || 'Unknown';});
+ if(newCount != this.state.reviewCount){
+ this.setState({
+ reviewCount : newCount
+ });
+ }
+ },
+
+ updateLockData : function(lock){
+ this.setState({
+ lock : lock
+ });
+ },
+
+ render : function() {
+ return
+
Lock Count
+
Number of brews currently locked: {this.state.reviewCount}
+
REFRESH
+
+
+
+
+
+
+
+
+
+
+
+
+
;
+ }
+});
+
+const LockBrew = createClass({
+ displayName : 'LockBrew',
+ getInitialState : function() {
+ // Default values
+ return {
+ brewId : this.props.lock?.shareId || '',
+ code : this.props.lock?.code || 455,
+ editMessage : this.props.lock?.editMessage || '',
+ shareMessage : this.props.lock?.shareMessage || 'This Brew has been locked.',
+ result : {},
+ overwrite : false,
+ };
+ },
+
+ handleChange : function(e, varName) {
+ const output = {};
+ output[varName] = e.target.value;
+ this.setState(output);
+ },
+
+ submit : function(e){
+ e.preventDefault();
+ if(!this.state.editMessage) return;
+ const newLock = {
+ overwrite : this.state.overwrite,
+ code : parseInt(this.state.code) || 100,
+ editMessage : this.state.editMessage,
+ shareMessage : this.state.shareMessage,
+ applied : new Date
+ };
+
+ request.post(`/api/lock/${this.state.brewId}`)
+ .send(newLock)
+ .set('Content-Type', 'application/json')
+ .then((response)=>{
+ this.setState({ result: response.body });
+ })
+ .catch((err)=>{
+ this.setState({ result: err.response.body });
+ });
+ },
+
+ renderInput : function (name) {
+ return
this.handleChange(e, name)} autoComplete='off' required/>;
+ },
+
+ renderResult : function(){
+ return <>
+
Result:
+
+
+ {Object.keys(this.state.result).map((key, idx)=>{
+ return
+ {key}
+ {this.state.result[key].toString()}
+
+ ;
+ })}
+
+
+ >;
+ },
+
+ render : function() {
+ return
+
+
Lock Brew
+
+ {this.state.result && this.renderResult()}
+
+
+
Suggestions
+
+
Codes
+
+ 455 - Generic Lock
+ 456 - Copyright issues
+ 457 - Confidential Information Leakage
+ 458 - Sensitive Personal Information
+ 459 - Defamation or Libel
+ 460 - Hate Speech or Discrimination
+ 461 - Illegal Activities
+ 462 - Malware or Phishing
+ 463 - Plagiarism
+ 465 - Misrepresentation
+ 466 - Inappropriate Content
+
+
+
+
Messages
+
+ Private Message: This is the private message that is ONLY displayed to the authors of the locked brew. This message MUST specify exactly what actions must be taken in order to have the brew unlocked.
+ Public Message: This is the public message that is displayed to the EVERYONE that attempts to view the locked brew.
+
+
+
+
;
+ }
+});
+
+const LockTable = createClass({
+ displayName : 'LockTable',
+ getDefaultProps : function() {
+ return {
+ title : '',
+ text : '',
+ fetchURL : '/api/locks',
+ resultName : '',
+ propertyNames : ['shareId'],
+ loadBrew : ()=>{}
+ };
+ },
+
+ getInitialState : function() {
+ return {
+ result : '',
+ error : '',
+ searching : false
+ };
+ },
+
+ lockKey : React.createRef(0),
+
+ clickFn : function (){
+ this.setState({ searching: true, error: null });
+
+ request.get(this.props.fetchURL)
+ .then((res)=>this.setState({ result: res.body }))
+ .catch((err)=>this.setState({ result: err.response.body }))
+ .finally(()=>{
+ this.setState({ searching: false });
+ });
+ },
+
+ updateBrewLockData : function (lockData){
+ this.lockKey.current++;
+ const brewData = {
+ key : this.lockKey.current,
+ shareId : lockData.shareId,
+ code : lockData.lock.code,
+ editMessage : lockData.lock.editMessage,
+ shareMessage : lockData.lock.shareMessage
+ };
+ this.props.loadBrew(brewData);
+ },
+
+ render : function () {
+ return <>
+
+
+
{this.props.title}
+
+ REFRESH
+
+
+
+ {this.state.result[this.props.resultName] &&
+ <>
+
{this.props.text}: {this.state.result[this.props.resultName].length}
+
+
+
+ {this.props.propertyNames.map((name, idx)=>{
+ return {name} ;
+ })}
+ clip
+ load
+
+
+
+ {this.state.result[this.props.resultName].map((result, resultIdx)=>{
+ return
+ {this.props.propertyNames.map((name, nameIdx)=>{
+ return
+ {result[name].toString()}
+ ;
+ })}
+ {navigator.clipboard.writeText(result.shareId.toString());}}>
+ {this.updateBrewLockData(result);}}>
+ ;
+ })}
+
+
+ >
+ }
+
+ >;
+ }
+});
+
+const LockLookup = createClass({
+ displayName : 'LockLookup',
+ getDefaultProps : function() {
+ return {
+ fetchURL : '/api/lookup'
+ };
+ },
+
+ getInitialState : function() {
+ return {
+ query : '',
+ result : '',
+ error : '',
+ searching : false
+ };
+ },
+
+ handleChange(e){
+ this.setState({ query: e.target.value });
+ },
+
+ clickFn(){
+ this.setState({ searching: true, error: null });
+
+ request.put(`${this.props.fetchURL}/${this.state.query}`)
+ .then((res)=>this.setState({ result: res.body }))
+ .catch((err)=>this.setState({ result: err.response.body }))
+ .finally(()=>{
+ this.setState({ searching: false });
+ });
+ },
+
+ renderResult : function(){
+ return
+
Result:
+
+
+ {Object.keys(this.state.result).map((key, idx)=>{
+ return
+ {key}
+ {this.state.result[key].toString()}
+
+ ;
+ })}
+
+
+
;
+ },
+
+ render : function() {
+ return
+
{this.props.title}
+
+
+
+
+
+ {this.state.error
+ &&
{this.state.error.toString()}
+ }
+
+ {this.state.result && this.renderResult()}
+
;
+ }
+});
+
+module.exports = LockTools;
\ No newline at end of file
diff --git a/client/admin/lockTools/lockTools.less b/client/admin/lockTools/lockTools.less
new file mode 100644
index 000000000..1ec9c524a
--- /dev/null
+++ b/client/admin/lockTools/lockTools.less
@@ -0,0 +1,66 @@
+.lockTools {
+ .lockBrew {
+ columns : 2;
+
+ .lockForm {
+ break-inside : avoid;
+
+ label {
+ display : inline-block;
+ width : 100%;
+ line-height : 2.25em;
+ text-align : right;
+ input {
+ float : right;
+ width : 65%;
+ margin-left : 10px;
+ }
+ &.checkbox {
+ line-height: 1.5em;
+ input {
+ width : 1.5em;
+ height : 1.5em;
+ }
+ }
+ }
+ }
+
+ .lockSuggestions {
+ line-height : 1.2em;
+ break-inside : avoid;
+ columns : 2;
+ h2 { column-span : all; }
+ h3 { margin-top : 0px; }
+ b { font-weight : 600; }
+
+ .lockCodes { break-inside : avoid; }
+ }
+ }
+
+ .lockTable {
+ cursor : default;
+ break-inside : avoid;
+ .row:hover {
+ color : #000000;
+ background-color : #CCCCCC;
+ }
+ .icon {
+ cursor : pointer;
+ &:hover { text-shadow : 0px 0px 6px black; }
+ }
+ }
+
+ th, td {
+ padding : 4px 10px;
+ text-align : center;
+ }
+ table, td { border : 1px solid #333333; }
+
+ .brewLookup {
+ min-height : 175px;
+ break-inside : avoid;
+ h2 { margin-top : 0px; }
+ }
+
+ button i { padding-left : 5px; }
+}
\ No newline at end of file
diff --git a/client/admin/notificationUtils/notificationAdd/notificationAdd.less b/client/admin/notificationUtils/notificationAdd/notificationAdd.less
index 878da24c2..14bdabd03 100644
--- a/client/admin/notificationUtils/notificationAdd/notificationAdd.less
+++ b/client/admin/notificationUtils/notificationAdd/notificationAdd.less
@@ -6,31 +6,32 @@
.field {
display : grid;
- grid-template-columns : 120px 150px;
+ grid-template-columns : 120px 200px;
align-items : center;
justify-items : stretch;
width : 100%;
margin-bottom : 20px;
-
-
+
input {
height : 33px;
padding : 0px 10px;
margin-bottom : unset;
font-family : monospace;
+
+ &[type='date'] { width : 14ch; }
}
textarea {
width : 50ch;
min-height : 7em;
max-height : 20em;
- resize : vertical;
padding : 10px;
+ resize : vertical;
}
}
button {
- width: 200px;
+ width : 200px;
i { margin-right : 10px; }
}
diff --git a/client/admin/notificationUtils/notificationLookup/notificationLookup.less b/client/admin/notificationUtils/notificationLookup/notificationLookup.less
index 3f9b78310..65903213c 100644
--- a/client/admin/notificationUtils/notificationLookup/notificationLookup.less
+++ b/client/admin/notificationUtils/notificationLookup/notificationLookup.less
@@ -1,8 +1,8 @@
-
.notificationLookup {
width : 450px;
- height : fit-content;
+ height : fit-content;
+ .noNotification { margin-block : 20px; }
.notificationList {
display : flex;
flex-direction : column;
@@ -30,11 +30,6 @@
font-size : 20px;
font-weight : 900;
}
-
- dl dt{
- font-weight: 900;
- }
}
}
- .noNotification { margin-block : 20px; }
}
\ No newline at end of file
diff --git a/client/components/Anchored.less b/client/components/Anchored.less
index 4f0e2fa8f..aeb9f1d5f 100644
--- a/client/components/Anchored.less
+++ b/client/components/Anchored.less
@@ -1,13 +1,11 @@
.anchored-box {
- position:absolute;
- @supports (inset-block-start: anchor(bottom)){
- inset-block-start: anchor(bottom);
- }
- justify-self: anchor-center;
- visibility: hidden;
- &.active {
- visibility: visible;
+ position : absolute;
+ visibility : hidden;
+ justify-self : anchor-center;
+ @supports (inset-block-start: anchor(bottom)) {
+ inset-block-start : anchor(bottom);
}
+ &.active { visibility : visible; }
}
\ No newline at end of file
diff --git a/client/components/combobox.jsx b/client/components/combobox.jsx
index 5fcc154bc..ae9f1d7f8 100644
--- a/client/components/combobox.jsx
+++ b/client/components/combobox.jsx
@@ -45,6 +45,7 @@ const Combobox = createClass({
},
handleDropdown : function(show){
this.setState({
+ value : show ? '' : this.props.default,
showDropdown : show,
inputFocused : this.props.autoSuggest.clearAutoSuggestOnClick ? show : false
});
@@ -58,10 +59,10 @@ const Combobox = createClass({
this.props.onEntry(e);
});
},
- handleSelect : function(e){
+ handleSelect : function(value, data=value){
this.setState({
- value : e.currentTarget.getAttribute('data-value')
- }, ()=>{this.props.onSelect(this.state.value);});
+ value : value
+ }, ()=>{this.props.onSelect(data);});
;
},
renderTextInput : function(){
@@ -78,10 +79,11 @@ const Combobox = createClass({
if(!e.target.checkValidity()){
this.setState({
value : this.props.default
- }, ()=>this.props.onEntry(e));
+ });
}
}}
/>
+
);
},
@@ -92,11 +94,10 @@ const Combobox = createClass({
const filterOn = _.isString(this.props.autoSuggest.filterOn) ? [this.props.autoSuggest.filterOn] : this.props.autoSuggest.filterOn;
const filteredArrays = filterOn.map((attr)=>{
const children = dropdownChildren.filter((item)=>{
- if(suggestMethod === 'includes'){
+ if(suggestMethod === 'includes')
return item.props[attr]?.toLowerCase().includes(this.state.value.toLowerCase());
- } else if(suggestMethod === 'startsWith'){
+ if(suggestMethod === 'startsWith')
return item.props[attr]?.toLowerCase().startsWith(this.state.value.toLowerCase());
- }
});
return children;
});
@@ -111,7 +112,7 @@ const Combobox = createClass({
},
render : function () {
const dropdownChildren = this.state.options.map((child, i)=>{
- const clone = React.cloneElement(child, { onClick: (e)=>this.handleSelect(e) });
+ const clone = React.cloneElement(child, { onClick: ()=>this.handleSelect(child.props.value, child.props.data) });
return clone;
});
return (
diff --git a/client/components/combobox.less b/client/components/combobox.less
index 3810a874e..27f78356b 100644
--- a/client/components/combobox.less
+++ b/client/components/combobox.less
@@ -1,50 +1,46 @@
.dropdown-container {
- position:relative;
- input {
- width: 100%;
- }
- .dropdown-options {
- position:absolute;
- background-color: white;
- z-index: 100;
- width: 100%;
- border: 1px solid gray;
- overflow-y: auto;
- max-height: 200px;
+ position : relative;
+ input { width : 100%; }
+ .item i {
+ position : absolute;
+ right : 10px;
+ color : black;
+ }
+ .dropdown-options {
+ position : absolute;
+ z-index : 100;
+ width : 100%;
+ max-height : 200px;
+ overflow-y : auto;
+ background-color : white;
+ border : 1px solid gray;
- &::-webkit-scrollbar {
- width: 14px;
- }
- &::-webkit-scrollbar-track {
- background: #ffffff;
- }
- &::-webkit-scrollbar-thumb {
- background-color: #949494;
- border-radius: 10px;
- border: 3px solid #ffffff;
- }
-
- .item {
- position:relative;
- font-size: 11px;
- font-family: Open Sans;
- padding: 5px;
- cursor: default;
- margin: 0 3px;
- //border-bottom: 1px solid darkgray;
- &:hover {
- filter: brightness(120%);
- background-color: rgb(163, 163, 163);
- }
- .detail {
- width:100%;
- text-align: left;
- color: rgb(124, 124, 124);
- font-style:italic;
- font-size: 9px;
- }
- }
-
- }
+ &::-webkit-scrollbar { width : 14px; }
+ &::-webkit-scrollbar-track { background : #FFFFFF; }
+ &::-webkit-scrollbar-thumb {
+ background-color : #949494;
+ border : 3px solid #FFFFFF;
+ border-radius : 10px;
+ }
+ .item {
+ position : relative;
+ padding : 5px;
+ margin : 0 3px;
+ font-family : 'Open Sans';
+ font-size : 11px;
+ cursor : default;
+ &:hover {
+ background-color : rgb(163, 163, 163);
+ filter : brightness(120%);
+ }
+ .detail {
+ width : 100%;
+ font-size : 9px;
+ font-style : italic;
+ color : rgb(124, 124, 124);
+ text-align : left;
+ }
+ }
+ }
}
diff --git a/client/homebrew/brewRenderer/brewRenderer.jsx b/client/homebrew/brewRenderer/brewRenderer.jsx
index a82ea8b34..c391d8c43 100644
--- a/client/homebrew/brewRenderer/brewRenderer.jsx
+++ b/client/homebrew/brewRenderer/brewRenderer.jsx
@@ -19,12 +19,11 @@ const { printCurrentBrew } = require('../../../shared/helpers.js');
import HeaderNav from './headerNav/headerNav.jsx';
import { safeHTML } from './safeHTML.js';
-const PAGEBREAK_REGEX_V3 = /^(?=\\page(?: *{[^\n{}]*})?$)/m;
+const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m;
const PAGE_HEIGHT = 1056;
const INITIAL_CONTENT = dedent`
-
@@ -39,7 +38,7 @@ const BrewPage = (props)=>{
...props
};
const pageRef = useRef(null);
- const cleanText = safeHTML(props.contents);
+ const cleanText = safeHTML(`${props.contents}\n
\n`);
useEffect(()=>{
if(!pageRef.current) return;
@@ -117,6 +116,12 @@ const BrewRenderer = (props)=>{
pageShadows : true
});
+ //useEffect to store or gather toolbar state from storage
+ useEffect(()=>{
+ const toolbarState = JSON.parse(window.localStorage.getItem('hb_toolbarState'));
+ toolbarState && setDisplayOptions(toolbarState);
+ }, []);
+
const [headerState, setHeaderState] = useState(false);
const mainRef = useRef(null);
@@ -186,17 +191,19 @@ const BrewRenderer = (props)=>{
} else {
if(pageText.startsWith('\\page')) {
const firstLineTokens = Markdown.marked.lexer(pageText.split('\n', 1)[0])[0].tokens;
- const injectedTags = firstLineTokens.find((obj)=>obj.injectedTags !== undefined)?.injectedTags;
+ const injectedTags = firstLineTokens?.find((obj)=>obj.injectedTags !== undefined)?.injectedTags;
if(injectedTags) {
styles = { ...styles, ...injectedTags.styles };
- styles = _.mapKeys(styles, (v, k) => k.startsWith('--') ? k : _.camelCase(k)); // Convert CSS to camelCase for React
+ styles = _.mapKeys(styles, (v, k)=>k.startsWith('--') ? k : _.camelCase(k)); // Convert CSS to camelCase for React
classes = [classes, injectedTags.classes].join(' ').trim();
attributes = injectedTags.attributes;
}
pageText = pageText.includes('\n') ? pageText.substring(pageText.indexOf('\n') + 1) : ''; // Remove the \page line
}
+ // DO NOT REMOVE!!! REQUIRED FOR BACKWARDS COMPATIBILITY WITH NON-UPGRADABLE VERSIONS OF CHROME.
pageText += `\n\n \n\\column\n `; //Artificial column break at page end to emulate column-fill:auto (until `wide` is used, when column-fill:balance will reappear)
+
const html = Markdown.render(pageText, index);
return
;
@@ -272,6 +279,7 @@ const BrewRenderer = (props)=>{
const handleDisplayOptionsChange = (newDisplayOptions)=>{
setDisplayOptions(newDisplayOptions);
+ localStorage.setItem('hb_toolbarState', JSON.stringify(newDisplayOptions));
};
const pagesStyle = {
diff --git a/client/homebrew/brewRenderer/brewRenderer.less b/client/homebrew/brewRenderer/brewRenderer.less
index 68c688fb6..b0a3e9779 100644
--- a/client/homebrew/brewRenderer/brewRenderer.less
+++ b/client/homebrew/brewRenderer/brewRenderer.less
@@ -1,43 +1,39 @@
@import (multiple, less) 'shared/naturalcrit/styles/reset.less';
.brewRenderer {
+ height : 100vh;
+ padding-top : 60px;
overflow-y : scroll;
will-change : transform;
- padding-top : 60px;
- height : 100vh;
- &:has(.facing, .flow) {
- padding : 60px 30px;
- }
- &.deployment {
- background-color: darkred;
- }
+ &:has(.facing, .flow) { padding : 60px 30px; }
+ &.deployment { background-color : darkred; }
:where(.pages) {
&.facing {
- display: grid;
- grid-template-columns: repeat(2, auto);
- grid-template-rows: repeat(3, auto);
- gap: 10px 10px;
- justify-content: safe center;
+ display : grid;
+ grid-template-rows : repeat(3, auto);
+ grid-template-columns : repeat(2, auto);
+ gap : 10px 10px;
+ justify-content : safe center;
&.recto .page:first-child {
// sets first page on 'right' ('recto') of the preview, as if for a Cover page.
// todo: add a checkbox to toggle this setting
- grid-column-start: 2;
+ grid-column-start : 2;
}
& :where(.page) {
- margin-left: unset !important;
- margin-right: unset !important;
+ margin-right : unset !important;
+ margin-left : unset !important;
}
}
&.flow {
- display: flex;
- flex-wrap: wrap;
- gap: 10px;
- justify-content: safe center;
+ display : flex;
+ flex-wrap : wrap;
+ gap : 10px;
+ justify-content : safe center;
& :where(.page) {
- flex: 0 0 auto;
- margin-left: unset !important;
- margin-right: unset !important;
+ flex : 0 0 auto;
+ margin-right : unset !important;
+ margin-left : unset !important;
}
}
@@ -50,9 +46,7 @@
margin-left : auto;
box-shadow : 1px 4px 14px #000000;
}
- *[id] {
- scroll-margin-top:100px;
- }
+ *[id] { scroll-margin-top : 100px; }
}
&::-webkit-scrollbar {
width : 20px;
@@ -74,16 +68,18 @@
@media print {
.toolBar { display : none; }
.brewRenderer {
- height : 100%;
- padding-top : unset;
- overflow-y : unset;
+ height : 100%;
+ padding : unset;
+ overflow-y : unset;
+ &:has(.facing, .flow) {
+ padding : unset;
+ }
.pages {
- margin : 0px;
- zoom: 100% !important;
+ margin : 0px;
+ zoom : 100% !important;
+ display : block;
& > .page { box-shadow : unset; }
}
}
- .headerNav {
- visibility: hidden;
- }
+ .headerNav { visibility : hidden; }
}
\ No newline at end of file
diff --git a/client/homebrew/brewRenderer/headerNav/headerNav.jsx b/client/homebrew/brewRenderer/headerNav/headerNav.jsx
index 68963129f..04ced2585 100644
--- a/client/homebrew/brewRenderer/headerNav/headerNav.jsx
+++ b/client/homebrew/brewRenderer/headerNav/headerNav.jsx
@@ -3,7 +3,6 @@ require('./headerNav.less');
import * as React from 'react';
import * as _ from 'lodash';
-
const MAX_TEXT_LENGTH = 40;
const HeaderNav = React.forwardRef(({}, pagesRef)=>{
@@ -11,11 +10,30 @@ const HeaderNav = React.forwardRef(({}, pagesRef)=>{
const renderHeaderLinks = ()=>{
if(!pagesRef.current) return;
+ // Top Level Pages
+ // Pages that contain an element with a specified class (e.g. cover pages, table of contents)
+ // will NOT have its content scanned for navigation headers, instead displaying a custom label
+ // ---
+ // The property name is class that will be used for detecting the page is a top level page
+ // The property value is a function that returns the text to be used
+
+ const topLevelPages = {
+ '.frontCover' : (el, pageType)=>{ const text = getHeaderContent(el); return text ? `Cover: ${text}` : 'Cover Page'; },
+ '.insideCover' : (el, pageType)=>{ const text = getHeaderContent(el); return text ? `Interior: ${text}` : 'Interior Cover Page'; },
+ '.partCover' : (el, pageType)=>{ const text = getHeaderContent(el); return text ? `Section: ${text}` : 'Section Cover Page'; },
+ '.backCover' : (el, pageType)=>{ const text = getHeaderContent(el); return text ? `Back: ${text}` : 'Rear Cover Page'; },
+ '.toc' : ()=>{ return 'Table of Contents'; },
+ };
+
+ const getHeaderContent = (el)=>el.querySelector('h1')?.textContent;
+
+ const topLevelPageSelector = Object.keys(topLevelPages).join(',');
+
const selector = [
- '.pages > .page', // All page elements, which by definition have IDs
- '.page:not(:has(.toc)) > [id]', // All direct children of non-ToC .page with an ID (Legacy)
- '.page:not(:has(.toc)) > .columnWrapper > [id]', // All direct children of non-ToC .page > .columnWrapper with an ID (V3)
- '.page:not(:has(.toc)) h2', // All non-ToC H2 titles, like Monster frame titles
+ '.pages > .page', // All page elements, which by definition have IDs
+ `.page:not(:has(${topLevelPageSelector})) > [id]`, // All direct children of non-excluded .pages with an ID (Legacy)
+ `.page:not(:has(${topLevelPageSelector})) > .columnWrapper > [id]`, // All direct children of non-excluded .page > .columnWrapper with an ID (V3)
+ `.page:not(:has(${topLevelPageSelector})) h2`, // All non-excluded H2 titles, like Monster frame titles
];
const elements = pagesRef.current.querySelectorAll(selector.join(','));
if(!elements) return;
@@ -23,45 +41,35 @@ const HeaderNav = React.forwardRef(({}, pagesRef)=>{
// navList is a list of objects which have the following structure:
// {
- // depth : how deeply indented the item should be
- // text : the text to display in the nav link
- // link : the hyperlink to navigate to when clicked
- // className : [optional] the class to apply to the nav link for styling
+ // depth : how deeply indented the item should be
+ // text : the text to display in the nav link
+ // link : the hyperlink to navigate to when clicked
+ // className : [optional] the class to apply to the nav link for styling
// }
elements.forEach((el)=>{
- if(el.className.match(/\bpage\b/)) {
- let text = `Page ${el.id.slice(1)}`; // The ID of a page *should* always be equal to `p` followed by the page number
- if(el.querySelector('.toc')){ // If the page contains a table of contents, add "- Contents" to the display text
- text += ' - Contents';
- };
- navList.push({
- depth : 0, // Pages are always at the least indented level
- text : text,
- link : el.id,
- className : 'pageLink'
- });
- return;
- }
- if(el.localName.match(/^h[1-6]/)){ // Header elements H1 through H6
- navList.push({
- depth : el.localName[1], // Depth is set by the header level
- text : el.textContent, // Use `textContent` because `innerText` is affected by rendering, e.g. 'content-visibility: auto'
- link : el.id
- });
- return;
- }
- navList.push({
- depth : 7, // All unmatched elements with IDs are set to the maximum depth (7)
- text : el.textContent, // Use `textContent` because `innerText` is affected by rendering, e.g. 'content-visibility: auto'
+ const navEntry = { // Default structure of a navList entry
+ depth : 7, // All unmatched elements with IDs are set to the maximum depth (7)
+ text : el.textContent, // Use `textContent` because `innerText` is affected by rendering, e.g. 'content-visibility: auto'
link : el.id
- });
- });
+ };
+ if(el.classList.contains('page')) {
+ let text = `Page ${el.id.slice(1)}`; // Get the page # by trimming off the 'p' from the ID
+ const pageType = Object.keys(topLevelPages).find((pageType)=>el.querySelector(pageType));
+ if(pageType)
+ text += ` - ${topLevelPages[pageType](el, pageType)}`; // If a Top Level Page, add extra label
- return _.map(navList, (navItem, index)=>{
- return
;
+ navEntry.depth = 0; // Pages are always at the least indented level
+ navEntry.text = text;
+ navEntry.className = 'pageLink';
+ } else if(el.localName.match(/^h[1-6]/)){ // Header elements H1 through H6
+ navEntry.depth = el.localName[1]; // Depth is set by the header level
+ }
+ navList.push(navEntry);
});
+ return _.map(navList, (navItem, index)=>
+ );
};
return
@@ -69,8 +77,7 @@ const HeaderNav = React.forwardRef(({}, pagesRef)=>{
{renderHeaderLinks()}
;
-}
-);
+});
const HeaderNavItem = ({ link, text, depth, className })=>{
diff --git a/client/homebrew/brewRenderer/headerNav/headerNav.less b/client/homebrew/brewRenderer/headerNav/headerNav.less
index 8b35041d9..a5fd11f5e 100644
--- a/client/homebrew/brewRenderer/headerNav/headerNav.less
+++ b/client/homebrew/brewRenderer/headerNav/headerNav.less
@@ -1,45 +1,37 @@
.headerNav {
- position: fixed;
- top: 32px;
- left: 0px;
- padding: 5px 10px;
- background-color: #ccc;
- border-radius: 5px;
- max-height: calc(100vh - 32px);
- max-width: 40vw;
- overflow-y: auto;
- &.active {
- padding-bottom: 10px;
- .navIcon {
- padding-bottom: 10px;
- }
- }
- .navIcon {
- cursor: pointer;
+ position : fixed;
+ top : 32px;
+ left : 0px;
+ max-width : 40vw;
+ max-height : calc(100vh - 32px);
+ padding : 5px 10px;
+ overflow-y : auto;
+ background-color : #CCCCCC;
+ border-radius : 5px;
+ &.active {
+ padding-bottom : 10px;
+ .navIcon { padding-bottom : 10px; }
}
+ .navIcon { cursor : pointer; }
li {
- list-style-type: none;
+ list-style-type : none;
a {
- display: inline-block;
- width: 100%;
- font-family: 'Open Sans';
- font-size: 12px;
- padding: 2px;
- color: inherit;
- text-decoration: none;
- cursor: pointer;
- &:hover {
- text-decoration: underline;
- }
- &.pageLink {
- font-weight: 900;
- }
+ display : inline-block;
+ width : 100%;
+ padding : 2px;
+ font-family : 'Open Sans';
+ font-size : 12px;
+ color : inherit;
+ text-decoration : none;
+ cursor : pointer;
+ &:hover { text-decoration : underline; }
+ &.pageLink { font-weight : 900; }
- @depths: 1,2,3,4,5,6,7;
+ @depths: 0,1,2,3,4,5,6,7;
each(@depths, {
&.depth-@{value} {
- padding-left: ((@value - 1) * 0.5em);
+ padding-left: ((@value) * 0.5em);
}
});
}
diff --git a/client/homebrew/brewRenderer/notificationPopup/notificationPopup.less b/client/homebrew/brewRenderer/notificationPopup/notificationPopup.less
index be642f0fe..85d4c8365 100644
--- a/client/homebrew/brewRenderer/notificationPopup/notificationPopup.less
+++ b/client/homebrew/brewRenderer/notificationPopup/notificationPopup.less
@@ -86,8 +86,8 @@
width : 100%;
}
.blank {
- height : 1em;
- margin-top : 0;
+ height : 1em;
+ margin-top : 0;
& + * { margin-top : 0; }
}
}
\ No newline at end of file
diff --git a/client/homebrew/brewRenderer/toolBar/toolBar.jsx b/client/homebrew/brewRenderer/toolBar/toolBar.jsx
index f11d1f127..4f3e356a7 100644
--- a/client/homebrew/brewRenderer/toolBar/toolBar.jsx
+++ b/client/homebrew/brewRenderer/toolBar/toolBar.jsx
@@ -20,6 +20,11 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa
setPageNum(pageRange);
}, [visiblePages]);
+ useEffect(()=>{
+ const visibility = localStorage.getItem('hb_toolbarVisibility') === 'true';
+ setToolsVisible(visibility);
+ }, []);
+
const handleZoomButton = (zoom)=>{
handleOptionChange('zoomLevel', _.round(_.clamp(zoom, MIN_ZOOM, MAX_ZOOM)));
};
@@ -55,15 +60,30 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa
// find widest page, in case pages are different widths, so that the zoom is adapted to not cut the widest page off screen.
const widestPage = _.maxBy([...pages], 'offsetWidth').offsetWidth;
- desiredZoom = (iframeWidth / widestPage) * 100;
+ if(displayOptions.spread === 'facing')
+ desiredZoom = (iframeWidth / ((widestPage * 2) + parseInt(displayOptions.columnGap))) * 100;
+ else
+ desiredZoom = (iframeWidth / (widestPage + 20)) * 100;
} else if(mode == 'fit'){
- let minDimRatio;
// find the page with the largest single dim (height or width) so that zoom can be adapted to fit it.
- if(displayOptions.spread === 'facing')
- minDimRatio = [...pages].reduce((minRatio, page)=>Math.min(minRatio, iframeWidth / page.offsetWidth / 2), Infinity); // if 'facing' spread, fit two pages in view
+ let minDimRatio;
+ if(displayOptions.spread === 'active')
+ minDimRatio = [...pages].reduce(
+ (minRatio, page)=>Math.min(minRatio,
+ iframeWidth / page.offsetWidth,
+ iframeHeight / page.offsetHeight
+ ),
+ Infinity
+ );
else
- minDimRatio = [...pages].reduce((minRatio, page)=>Math.min(minRatio, iframeWidth / page.offsetWidth, iframeHeight / page.offsetHeight), Infinity);
+ minDimRatio = [...pages].reduce(
+ (minRatio, page)=>Math.min(minRatio,
+ iframeWidth / ((page.offsetWidth * 2) + parseInt(displayOptions.columnGap)),
+ iframeHeight / page.offsetHeight
+ ),
+ Infinity
+ );
desiredZoom = minDimRatio * 100;
}
@@ -77,7 +97,10 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa
return (