mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-01-24 07:43:01 +00:00
Compare commits
78 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d192a064d6 | ||
|
|
cccb531e17 | ||
|
|
6414e73e7d | ||
|
|
41daf8d172 | ||
|
|
4c897fdeb5 | ||
|
|
89ce4de354 | ||
|
|
43095507ee | ||
|
|
eb7fbbe018 | ||
|
|
869958ec38 | ||
|
|
57a48100d3 | ||
|
|
8538e4fadb | ||
|
|
9a002511a3 | ||
|
|
3fa3a52e05 | ||
|
|
4fe920dac3 | ||
|
|
71dff5fbf9 | ||
|
|
26419d2ccb | ||
|
|
f02fe2d8f3 | ||
|
|
318fb53eb2 | ||
|
|
6a32b7427b | ||
|
|
5886bd65e5 | ||
|
|
9c5f80cbdb | ||
|
|
79d8956c4f | ||
|
|
2e491b3556 | ||
|
|
d9a8afa272 | ||
|
|
209195202c | ||
|
|
64235c844a | ||
|
|
5d000a4599 | ||
|
|
380e593b42 | ||
|
|
169f089d08 | ||
|
|
b3977ed141 | ||
|
|
9800561de7 | ||
|
|
166af08e6a | ||
|
|
48f17f7c5e | ||
|
|
87c9f52222 | ||
|
|
c80b7ffd66 | ||
|
|
5f16ce3dbd | ||
|
|
b5ff26f857 | ||
|
|
578b01bbb1 | ||
|
|
67467e0099 | ||
|
|
da21bf20f9 | ||
|
|
df7fcf1e5f | ||
|
|
e1e661976d | ||
|
|
7bdeeee9ef | ||
|
|
becf35d336 | ||
|
|
d7585767c9 | ||
|
|
f9bb6209b7 | ||
|
|
13702a2f62 | ||
|
|
a6a684c89e | ||
|
|
862fa7de89 | ||
|
|
b671cf7b02 | ||
|
|
d5dbe0b4ba | ||
|
|
c2cf695c17 | ||
|
|
5f2f3a6f3d | ||
|
|
bbb812cb06 | ||
|
|
c051580545 | ||
|
|
6e72fe2600 | ||
|
|
03602ae1e0 | ||
|
|
8de738a146 | ||
|
|
6960beb739 | ||
|
|
6748639ec5 | ||
|
|
e5651807fd | ||
|
|
9adf6dee61 | ||
|
|
03527a1f95 | ||
|
|
651863b0f7 | ||
|
|
450ecd24b7 | ||
|
|
995cfa2aa4 | ||
|
|
5eecb5ea20 | ||
|
|
0885473b66 | ||
|
|
eabff4f6b2 | ||
|
|
a773df25d0 | ||
|
|
b07f75ac36 | ||
|
|
ed5fbadd73 | ||
|
|
7abf45e8ba | ||
|
|
bbae62e0b7 | ||
|
|
a9d71078d3 | ||
|
|
0ca53f8db6 | ||
|
|
6f52b8473f | ||
|
|
9ecd53267f |
9
.github/dependabot.yml
vendored
9
.github/dependabot.yml
vendored
@@ -5,6 +5,15 @@ updates:
|
||||
schedule:
|
||||
interval: daily
|
||||
open-pull-requests-limit: 99
|
||||
groups:
|
||||
dev-dependencies:
|
||||
dependency-type: "development"
|
||||
patterns: ["*"]
|
||||
update-types: ["patch", "minor"]
|
||||
prod-dependencies:
|
||||
dependency-type: "production"
|
||||
patterns: ["*"]
|
||||
update-types: ["patch", "minor"]
|
||||
ignore:
|
||||
- dependency-name: eslint
|
||||
versions:
|
||||
|
||||
11
changelog.md
11
changelog.md
@@ -88,6 +88,15 @@ pre {
|
||||
## changelog
|
||||
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
|
||||
|
||||
### Wednesday 7/09/2025 - v3.19.1
|
||||
|
||||
{{taskList
|
||||
##### calculuschild
|
||||
* [x] Send diffs instead of full file on save - should help with timeout/disconnect errors
|
||||
}}
|
||||
|
||||
\column
|
||||
|
||||
### Thursday 05/22/2025 - v3.19.0
|
||||
|
||||
{{taskList
|
||||
@@ -130,7 +139,7 @@ Fixes issue [#4146](https://github.com/naturalcrit/homebrewery/issues/4146)
|
||||
New snippet found at {{openSans **:fas_pencil: TEXT EDITOR :fas_arrow_right: :fas_bookmark: PAGE NUMBERING :fas_arrow_right: :fas_arrow_down_1_9: VARIABLE AUTO PAGE NUMBER**}}
|
||||
|
||||
##### 5e-Cleric
|
||||
* [x] Fix search bar covering up snippet bar
|
||||
* [x] Fix search bar covering up snippet bar (3 times)
|
||||
|
||||
Fixes issue [#4098](https://github.com/naturalcrit/homebrewery/issues/4098)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
@import 'naturalcrit/styles/animations.less';
|
||||
@import 'naturalcrit/styles/colors.less';
|
||||
@import 'naturalcrit/styles/tooltip.less';
|
||||
@import './themes/fonts/iconFonts/fontAwesome.less';
|
||||
|
||||
@import 'font-awesome/css/font-awesome.css';
|
||||
|
||||
|
||||
@@ -113,13 +113,14 @@ const BrewRenderer = (props)=>{
|
||||
zoomLevel : 100,
|
||||
spread : 'single',
|
||||
startOnRight : true,
|
||||
pageShadows : true
|
||||
pageShadows : true,
|
||||
rowGap : 5,
|
||||
columnGap : 10,
|
||||
});
|
||||
|
||||
//useEffect to store or gather toolbar state from storage
|
||||
useEffect(()=>{
|
||||
const toolbarState = JSON.parse(window.localStorage.getItem('hb_toolbarState'));
|
||||
console.log('toolbar state:', toolbarState);
|
||||
toolbarState && setDisplayOptions(toolbarState);
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -21,8 +21,9 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa
|
||||
}, [visiblePages]);
|
||||
|
||||
useEffect(()=>{
|
||||
const visibility = localStorage.getItem('hb_toolbarVisibility') === 'true';
|
||||
setToolsVisible(visibility);
|
||||
const Visibility = localStorage.getItem('hb_toolbarVisibility');
|
||||
if (Visibility) setToolsVisible(Visibility === 'true');
|
||||
|
||||
}, []);
|
||||
|
||||
const handleZoomButton = (zoom)=>{
|
||||
@@ -68,7 +69,7 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa
|
||||
} 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 minDimRatio;
|
||||
if(displayOptions.spread === 'active')
|
||||
if(displayOptions.spread === 'single')
|
||||
minDimRatio = [...pages].reduce(
|
||||
(minRatio, page)=>Math.min(minRatio,
|
||||
iframeWidth / page.offsetWidth,
|
||||
@@ -165,7 +166,7 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa
|
||||
id='single-spread'
|
||||
className='tool'
|
||||
title='Single Page'
|
||||
onClick={()=>{handleOptionChange('spread', 'active');}}
|
||||
onClick={()=>{handleOptionChange('spread', 'single');}}
|
||||
aria-checked={displayOptions.spread === 'single'}
|
||||
><i className='fac single-spread' /></button>
|
||||
<button role='radio'
|
||||
|
||||
@@ -175,6 +175,10 @@
|
||||
opacity : 0;
|
||||
transition : all 0.2s ease;
|
||||
}
|
||||
|
||||
.toggleButton button i {
|
||||
filter: drop-shadow(0 0 2px black) drop-shadow(0 0 1px black);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,9 +188,4 @@
|
||||
z-index : 5;
|
||||
display : flex;
|
||||
height : 100%;
|
||||
|
||||
button i {
|
||||
filter: drop-shadow(0 0 2px black) drop-shadow(0 0 1px black);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,6 @@ const EDITOR_THEME_KEY = 'HOMEBREWERY-EDITOR-THEME';
|
||||
|
||||
const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m;
|
||||
const SNIPPETBREAK_REGEX_V3 = /^\\snippet\ .*$/;
|
||||
const SNIPPETBAR_HEIGHT = 25;
|
||||
const DEFAULT_STYLE_TEXT = dedent`
|
||||
/*=======--- Example CSS styling ---=======*/
|
||||
/* Any CSS here will apply to your document! */
|
||||
@@ -60,8 +59,9 @@ const Editor = createClass({
|
||||
},
|
||||
getInitialState : function() {
|
||||
return {
|
||||
editorTheme : this.props.editorTheme,
|
||||
view : 'text' //'text', 'style', 'meta', 'snippet'
|
||||
editorTheme : this.props.editorTheme,
|
||||
view : 'text', //'text', 'style', 'meta', 'snippet'
|
||||
snippetbarHeight : 25
|
||||
};
|
||||
},
|
||||
|
||||
@@ -88,6 +88,7 @@ const Editor = createClass({
|
||||
editorTheme : editorTheme
|
||||
});
|
||||
}
|
||||
this.setState({ snippetbarHeight: document.querySelector('.editor > .snippetBar').offsetHeight });
|
||||
},
|
||||
|
||||
componentDidUpdate : function(prevProps, prevState, snapshot) {
|
||||
@@ -412,6 +413,9 @@ const Editor = createClass({
|
||||
//Called when there are changes to the editor's dimensions
|
||||
update : function(){
|
||||
this.codeEditor.current?.updateSize();
|
||||
const snipHeight = document.querySelector('.editor > .snippetBar').offsetHeight;
|
||||
if(snipHeight !== this.state.snippetbarHeight)
|
||||
this.setState({ snippetbarHeight: snipHeight });
|
||||
},
|
||||
|
||||
updateEditorTheme : function(newTheme){
|
||||
@@ -436,7 +440,8 @@ const Editor = createClass({
|
||||
value={this.props.brew.text}
|
||||
onChange={this.props.onTextChange}
|
||||
editorTheme={this.state.editorTheme}
|
||||
rerenderParent={this.rerenderParent} />
|
||||
rerenderParent={this.rerenderParent}
|
||||
style={{ height: `calc(100% - ${this.state.snippetbarHeight}px)` }} />
|
||||
</>;
|
||||
}
|
||||
if(this.isStyle()){
|
||||
@@ -449,7 +454,8 @@ const Editor = createClass({
|
||||
onChange={this.props.onStyleChange}
|
||||
enableFolding={true}
|
||||
editorTheme={this.state.editorTheme}
|
||||
rerenderParent={this.rerenderParent} />
|
||||
rerenderParent={this.rerenderParent}
|
||||
style={{ height: `calc(100% - ${this.state.snippetbarHeight}px)` }} />
|
||||
</>;
|
||||
}
|
||||
if(this.isMeta()){
|
||||
@@ -478,7 +484,8 @@ const Editor = createClass({
|
||||
onChange={this.props.onSnipChange}
|
||||
enableFolding={true}
|
||||
editorTheme={this.state.editorTheme}
|
||||
rerenderParent={this.rerenderParent} />
|
||||
rerenderParent={this.rerenderParent}
|
||||
style={{ height: `calc(100% - ${this.state.snippetbarHeight}px)` }} />
|
||||
</>;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
height : 100%;
|
||||
container : editor / inline-size;
|
||||
.codeEditor {
|
||||
height : 100%;
|
||||
height : calc(100% - 25px);
|
||||
.CodeMirror { height : 100%; }
|
||||
.pageLine, .snippetLine {
|
||||
background : #33333328;
|
||||
@@ -108,9 +108,4 @@
|
||||
span { padding : 2px 5px; }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@container editor (width < 683px) {
|
||||
.editor .codeEditor .CodeMirror { height : calc(100% - 51px);}
|
||||
.homePage .editor .codeEditor .CodeMirror { height : calc(100% - 25px);}
|
||||
}
|
||||
@@ -23,14 +23,15 @@ const ErrorNavItem = createClass({
|
||||
|
||||
const error = this.props.error;
|
||||
const response = error.response;
|
||||
const status = response.status;
|
||||
const HBErrorCode = response.body?.HBErrorCode;
|
||||
const message = response.body?.message;
|
||||
const status = response?.status;
|
||||
const errorCode = error.code
|
||||
const HBErrorCode = response?.body?.HBErrorCode;
|
||||
const message = response?.body?.message;
|
||||
let errMsg = '';
|
||||
try {
|
||||
errMsg += `${error.toString()}\n\n`;
|
||||
errMsg += `\`\`\`\n${error.stack}\n`;
|
||||
errMsg += `${JSON.stringify(response.error, null, ' ')}\n\`\`\``;
|
||||
errMsg += `${JSON.stringify(response?.error, null, ' ')}\n\`\`\``;
|
||||
console.log(errMsg);
|
||||
} catch (e){}
|
||||
|
||||
@@ -73,7 +74,7 @@ const ErrorNavItem = createClass({
|
||||
</Nav.item>;
|
||||
}
|
||||
|
||||
if(response.body?.errors?.[0].reason == 'storageQuotaExceeded') {
|
||||
if(response?.body?.errors?.[0].reason == 'storageQuotaExceeded') {
|
||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||
Oops!
|
||||
<div className='errorContainer' onClick={clearError}>
|
||||
@@ -82,7 +83,7 @@ const ErrorNavItem = createClass({
|
||||
</Nav.item>;
|
||||
}
|
||||
|
||||
if(response.req.url.match(/^\/api.*Google.*$/m)){
|
||||
if(response?.req.url.match(/^\/api.*Google.*$/m)){
|
||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||
Oops!
|
||||
<div className='errorContainer' onClick={clearError}>
|
||||
@@ -129,6 +130,18 @@ const ErrorNavItem = createClass({
|
||||
</Nav.item>;
|
||||
}
|
||||
|
||||
if(errorCode === 'ECONNABORTED') {
|
||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||
Oops!
|
||||
<div className='errorContainer' onClick={clearError}>
|
||||
The request to the server was interrupted or timed out.
|
||||
This can happen due to a network issue, or if
|
||||
trying to save a particularly large brew.
|
||||
Please check your internet connection and try again.
|
||||
</div>
|
||||
</Nav.item>;
|
||||
}
|
||||
|
||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||
Oops!
|
||||
<div className='errorContainer'>
|
||||
|
||||
@@ -5,7 +5,7 @@ const moment = require('moment');
|
||||
import request from '../../../../utils/request-middleware.js';
|
||||
|
||||
const googleDriveIcon = require('../../../../googleDrive.svg');
|
||||
const homebreweryIcon = require('../../../../thumbnail.png');
|
||||
const homebreweryIcon = require('../../../../thumbnail.svg');
|
||||
const dedent = require('dedent-tabs').default;
|
||||
|
||||
const BrewItem = ({
|
||||
|
||||
@@ -115,15 +115,15 @@
|
||||
}
|
||||
}
|
||||
.googleDriveIcon {
|
||||
height : 18px;
|
||||
padding : 0px;
|
||||
margin : -5px;
|
||||
height : 18px;
|
||||
}
|
||||
.homebreweryIcon {
|
||||
position : relative;
|
||||
top : 5px;
|
||||
left : -5px;
|
||||
height : 24px;
|
||||
mix-blend-mode : darken;
|
||||
position : relative;
|
||||
padding : 0px;
|
||||
top : 5px;
|
||||
left : -7.5px;
|
||||
height : 18px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ require('./editPage.less');
|
||||
const React = require('react');
|
||||
const _ = require('lodash');
|
||||
const createClass = require('create-react-class');
|
||||
import {makePatches, applyPatches, stringifyPatches, parsePatches} from '@sanity/diff-match-patch';
|
||||
import { md5 } from 'hash-wasm';
|
||||
|
||||
import request from '../../utils/request-middleware.js';
|
||||
const { Meta } = require('vitreum/headtags');
|
||||
@@ -47,7 +49,7 @@ const EditPage = createClass({
|
||||
return {
|
||||
brew : this.props.brew,
|
||||
isSaving : false,
|
||||
isPending : false,
|
||||
unsavedChanges : false,
|
||||
alertTrashedGoogleBrew : this.props.brew.trashed,
|
||||
alertLoginToTransfer : false,
|
||||
saveGoogle : this.props.brew.googleId ? true : false,
|
||||
@@ -85,7 +87,7 @@ const EditPage = createClass({
|
||||
});
|
||||
|
||||
window.onbeforeunload = ()=>{
|
||||
if(this.state.isSaving || this.state.isPending){
|
||||
if(this.state.isSaving || this.state.unsavedChanges){
|
||||
return 'You have unsaved changes!';
|
||||
}
|
||||
};
|
||||
@@ -104,9 +106,9 @@ const EditPage = createClass({
|
||||
},
|
||||
componentDidUpdate : function(){
|
||||
const hasChange = this.hasChanges();
|
||||
if(this.state.isPending != hasChange){
|
||||
if(this.state.unsavedChanges != hasChange){
|
||||
this.setState({
|
||||
isPending : hasChange
|
||||
unsavedChanges : hasChange
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -156,9 +158,9 @@ const EditPage = createClass({
|
||||
if(htmlErrors.length) htmlErrors = Markdown.validate(snippet);
|
||||
|
||||
this.setState((prevState)=>({
|
||||
brew : { ...prevState.brew, snippets: snippet },
|
||||
isPending : true,
|
||||
htmlErrors : htmlErrors,
|
||||
brew : { ...prevState.brew, snippets: snippet },
|
||||
unsavedChanges : true,
|
||||
htmlErrors : htmlErrors,
|
||||
}), ()=>{if(this.state.autoSave) this.trySave();});
|
||||
},
|
||||
|
||||
@@ -196,12 +198,19 @@ const EditPage = createClass({
|
||||
|
||||
trySave : function(immediate=false){
|
||||
if(!this.debounceSave) this.debounceSave = _.debounce(this.save, SAVE_TIMEOUT);
|
||||
if(this.hasChanges()){
|
||||
if(this.state.isSaving)
|
||||
return;
|
||||
|
||||
if(immediate) {
|
||||
this.debounceSave();
|
||||
} else {
|
||||
this.debounceSave.cancel();
|
||||
this.debounceSave.flush();
|
||||
return;
|
||||
}
|
||||
if(immediate) this.debounceSave.flush();
|
||||
|
||||
if(this.hasChanges())
|
||||
this.debounceSave();
|
||||
else
|
||||
this.debounceSave.cancel();
|
||||
},
|
||||
|
||||
handleGoogleClick : function(){
|
||||
@@ -215,8 +224,7 @@ const EditPage = createClass({
|
||||
confirmGoogleTransfer : !prevState.confirmGoogleTransfer
|
||||
}));
|
||||
this.setState({
|
||||
error : null,
|
||||
isSaving : false
|
||||
error : null
|
||||
});
|
||||
},
|
||||
|
||||
@@ -232,9 +240,8 @@ const EditPage = createClass({
|
||||
toggleGoogleStorage : function(){
|
||||
this.setState((prevState)=>({
|
||||
saveGoogle : !prevState.saveGoogle,
|
||||
isSaving : false,
|
||||
error : null
|
||||
}), ()=>this.save());
|
||||
}), ()=>this.trySave(true));
|
||||
},
|
||||
|
||||
save : async function(){
|
||||
@@ -249,11 +256,17 @@ const EditPage = createClass({
|
||||
await updateHistory(this.state.brew).catch(console.error);
|
||||
await versionHistoryGarbageCollection().catch(console.error);
|
||||
|
||||
const transfer = this.state.saveGoogle == _.isNil(this.state.brew.googleId);
|
||||
const preSaveSnapshot = { ...this.state.brew }
|
||||
|
||||
const brew = this.state.brew;
|
||||
//Prepare content to send to server
|
||||
const brew = { ...this.state.brew };
|
||||
brew.pageCount = ((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1;
|
||||
brew.patches = makePatches(this.savedBrew.text, brew.text);
|
||||
brew.hash = await md5(this.savedBrew.text);
|
||||
brew.text = undefined;
|
||||
brew.textBin = undefined;
|
||||
|
||||
const transfer = this.state.saveGoogle == _.isNil(this.state.brew.googleId);
|
||||
const params = `${transfer ? `?${this.state.saveGoogle ? 'saveToGoogle' : 'removeFromGoogle'}=true` : ''}`;
|
||||
const res = await request
|
||||
.put(`/api/update/${brew.editId}${params}`)
|
||||
@@ -265,20 +278,28 @@ const EditPage = createClass({
|
||||
if(!res) return;
|
||||
|
||||
this.savedBrew = {
|
||||
...this.state.brew,
|
||||
...preSaveSnapshot,
|
||||
googleId : res.body.googleId ? res.body.googleId : null,
|
||||
editId : res.body.editId,
|
||||
shareId : res.body.shareId,
|
||||
version : res.body.version
|
||||
};
|
||||
history.replaceState(null, null, `/edit/${this.savedBrew.editId}`);
|
||||
|
||||
this.setState(()=>({
|
||||
brew : this.savedBrew,
|
||||
isPending : false,
|
||||
isSaving : false,
|
||||
unsavedTime : new Date()
|
||||
}));
|
||||
this.setState((prevState) => ({
|
||||
brew: {
|
||||
...prevState.brew,
|
||||
googleId : res.body.googleId ? res.body.googleId : null,
|
||||
editId : res.body.editId,
|
||||
shareId : res.body.shareId,
|
||||
version : res.body.version
|
||||
},
|
||||
isSaving : false,
|
||||
unsavedTime : new Date()
|
||||
}), ()=>{
|
||||
this.setState({ unsavedChanges : this.hasChanges() });
|
||||
});
|
||||
|
||||
history.replaceState(null, null, `/edit/${this.savedBrew.editId}`);
|
||||
},
|
||||
|
||||
renderGoogleDriveIcon : function(){
|
||||
@@ -336,7 +357,7 @@ const EditPage = createClass({
|
||||
}
|
||||
|
||||
// #2 - Unsaved changes exist, autosave is OFF and warning timer has expired, show AUTOSAVE WARNING
|
||||
if(this.state.isPending && this.state.autoSaveWarning){
|
||||
if(this.state.unsavedChanges && this.state.autoSaveWarning){
|
||||
this.setAutosaveWarning();
|
||||
const elapsedTime = Math.round((new Date() - this.state.unsavedTime) / 1000 / 60);
|
||||
const text = elapsedTime == 0 ? 'Autosave is OFF.' : `Autosave is OFF, and you haven't saved for ${elapsedTime} minutes.`;
|
||||
@@ -351,7 +372,7 @@ const EditPage = createClass({
|
||||
|
||||
// #3 - Unsaved changes exist, click to save, show SAVE NOW
|
||||
// Use trySave(true) instead of save() to use debounced save function
|
||||
if(this.state.isPending){
|
||||
if(this.state.unsavedChanges){
|
||||
return <Nav.item className='save' onClick={()=>this.trySave(true)} color='blue' icon='fas fa-save'>Save Now</Nav.item>;
|
||||
}
|
||||
// #4 - No unsaved changes, autosave is ON, show AUTO-SAVED
|
||||
|
||||
@@ -148,7 +148,6 @@ const NewPage = createClass({
|
||||
|
||||
this.setState((prevState)=>({
|
||||
brew : { ...prevState.brew, snippets: snippet },
|
||||
isPending : true,
|
||||
htmlErrors : htmlErrors,
|
||||
}), ()=>{if(this.state.autoSave) this.trySave();});
|
||||
},
|
||||
|
||||
64
client/homebrew/thumbnail.svg
Normal file
64
client/homebrew/thumbnail.svg
Normal file
@@ -0,0 +1,64 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
viewBox="0 0 94.65 94.6"
|
||||
version="1.1"
|
||||
id="svg11"
|
||||
sodipodi:docname="thumbnail.svg"
|
||||
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<sodipodi:namedview
|
||||
id="namedview13"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#111111"
|
||||
borderopacity="1"
|
||||
inkscape:showpageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="1"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showgrid="false"
|
||||
inkscape:zoom="8.4989431"
|
||||
inkscape:cx="38.887188"
|
||||
inkscape:cy="47.417661"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1043"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg11" />
|
||||
<defs
|
||||
id="defs4">
|
||||
<style
|
||||
id="style2">.cls-1{fill:#ed1f24;}</style>
|
||||
</defs>
|
||||
<title
|
||||
id="title6">NaturalCritLogo</title>
|
||||
<g
|
||||
id="Layer_2"
|
||||
data-name="Layer 2"
|
||||
style="fill:#000000;stroke:#000000">
|
||||
<g
|
||||
id="base"
|
||||
style="fill:#000000;stroke:#000000">
|
||||
<path
|
||||
id="D20"
|
||||
class="cls-1"
|
||||
d="M63.45.09s-45.91,12.4-46,12.45a.71.71,0,0,0-.15.08l-.15.1-.12.11a1.07,1.07,0,0,0-.14.16l-.09.11-.12.23,0,.06L.2,54.9a1.59,1.59,0,0,0,.11,1.69L29.36,94h0l0,0,.08.08.08.08.09.09.08.06.13.07a0,0,0,0,0,0,0,1.59,1.59,0,0,0,.27.12l.13.05.06,0a1.55,1.55,0,0,0,.37,0,1.63,1.63,0,0,0,.31,0l45.67-8.3.16,0,.11,0,.12,0,.06,0s0,0,0,0l.06,0a1.65,1.65,0,0,0,.36-.28l0-.06a1.6,1.6,0,0,0,.26-.38s0,0,0,0v0h0a.14.14,0,0,1,0-.06L94.52,43.74a1.4,1.4,0,0,0,.11-.4.41.41,0,0,0,0-.11,1.13,1.13,0,0,0,0-.26.66.66,0,0,0,0-.14,2,2,0,0,0-.06-.26l0-.11a2.68,2.68,0,0,0-.18-.33v0L65.29.6C64.77-.31,63.45.09,63.45.09ZM74.9,81.7l-28.81-18L78.5,38.49ZM44.1,61l-11-40.17L77,35.39ZM82,37.78l8.92,5.95L79,73.48Zm4.46-1.1-4.6-3.06L75.69,21.36Zm-9.26-4.8-42.07-14,28.05-14ZM30.56,16.34l-6.49-2.16L47.85,7.7Zm-11.35-.21L27.88,19,7.64,45Zm10.73,5.76L40.78,61.64,4.64,54.42Zm10.82,43.2L30.26,89.6,5.75,58.09Zm3.16,1.24L71.74,83.72l-38.26,7Z"
|
||||
style="fill:#000000;fill-opacity:1;stroke:#000000" />
|
||||
</g>
|
||||
</g>
|
||||
<metadata
|
||||
id="metadata1">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:title>NaturalCritLogo</dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
4680
package-lock.json
generated
4680
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
42
package.json
42
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "homebrewery",
|
||||
"description": "Create authentic looking D&D homebrews using only markdown",
|
||||
"version": "3.19.0",
|
||||
"version": "3.19.1",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"npm": "^10.8.x",
|
||||
@@ -84,24 +84,26 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.27.1",
|
||||
"@babel/plugin-transform-runtime": "^7.27.1",
|
||||
"@babel/preset-env": "^7.27.2",
|
||||
"@babel/plugin-transform-runtime": "^7.28.0",
|
||||
"@babel/preset-env": "^7.28.0",
|
||||
"@babel/preset-react": "^7.27.1",
|
||||
"@babel/runtime": "^7.27.1",
|
||||
"@googleapis/drive": "^12.1.0",
|
||||
"@babel/runtime": "^7.27.6",
|
||||
"@googleapis/drive": "^13.0.1",
|
||||
"@sanity/diff-match-patch": "^3.2.0",
|
||||
"body-parser": "^2.2.0",
|
||||
"classnames": "^2.5.1",
|
||||
"codemirror": "^5.65.6",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"core-js": "^3.42.0",
|
||||
"core-js": "^3.43.0",
|
||||
"cors": "^2.8.5",
|
||||
"create-react-class": "^15.7.0",
|
||||
"dedent-tabs": "^0.10.3",
|
||||
"expr-eval": "^2.0.2",
|
||||
"express": "^5.1.0",
|
||||
"express-async-handler": "^1.2.0",
|
||||
"express-static-gzip": "2.2.0",
|
||||
"express-static-gzip": "3.0.0",
|
||||
"fs-extra": "11.3.0",
|
||||
"hash-wasm": "^4.12.0",
|
||||
"idb-keyval": "^6.2.2",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jwt-simple": "^0.5.6",
|
||||
@@ -110,40 +112,40 @@
|
||||
"marked": "15.0.12",
|
||||
"marked-alignment-paragraphs": "^1.0.0",
|
||||
"marked-definition-lists": "^1.0.1",
|
||||
"marked-emoji": "^2.0.0",
|
||||
"marked-emoji": "^2.0.1",
|
||||
"marked-extended-tables": "^2.0.1",
|
||||
"marked-gfm-heading-id": "^4.0.1",
|
||||
"marked-gfm-heading-id": "^4.1.2",
|
||||
"marked-nonbreaking-spaces": "^1.0.1",
|
||||
"marked-smartypants-lite": "^1.0.3",
|
||||
"marked-subsuper-text": "^1.0.3",
|
||||
"markedLegacy": "npm:marked@^0.3.19",
|
||||
"moment": "^2.30.1",
|
||||
"mongoose": "^8.14.3",
|
||||
"mongoose": "^8.16.1",
|
||||
"nanoid": "5.1.5",
|
||||
"nconf": "^0.13.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-frame-component": "^4.1.3",
|
||||
"react-router": "^7.6.0",
|
||||
"romans": "^3.0.0",
|
||||
"react-router": "^7.6.3",
|
||||
"romans": "^3.1.0",
|
||||
"sanitize-filename": "1.6.3",
|
||||
"superagent": "^10.2.1",
|
||||
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git",
|
||||
"written-number": "^0.11.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@stylistic/stylelint-plugin": "^3.1.2",
|
||||
"babel-plugin-transform-import-meta": "^2.3.2",
|
||||
"eslint": "^9.27.0",
|
||||
"eslint-plugin-jest": "^28.11.0",
|
||||
"@stylistic/stylelint-plugin": "^3.1.3",
|
||||
"babel-plugin-transform-import-meta": "^2.3.3",
|
||||
"eslint": "^9.30.1",
|
||||
"eslint-plugin-jest": "^29.0.1",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"globals": "^16.1.0",
|
||||
"jest": "^29.7.0",
|
||||
"globals": "^16.3.0",
|
||||
"jest": "^30.0.4",
|
||||
"jest-expect-message": "^1.1.3",
|
||||
"jsdom-global": "^3.0.2",
|
||||
"postcss-less": "^6.0.0",
|
||||
"stylelint": "^16.19.1",
|
||||
"stylelint-config-recess-order": "^6.0.0",
|
||||
"stylelint": "^16.21.1",
|
||||
"stylelint-config-recess-order": "^7.1.0",
|
||||
"stylelint-config-recommended": "^16.0.0",
|
||||
"supertest": "^7.1.1"
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"codemirror/addon/selection/active-line.js",
|
||||
"codemirror/addon/hint/show-hint.js",
|
||||
"moment",
|
||||
"superagent"
|
||||
"superagent",
|
||||
"@sanity/diff-match-patch"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -404,6 +404,7 @@ app.get('/new/:id', asyncHandler(getBrew('share')), asyncHandler(async(req, res,
|
||||
renderer : req.brew.renderer,
|
||||
theme : req.brew.theme,
|
||||
tags : req.brew.tags,
|
||||
snippets : req.brew.snippets
|
||||
};
|
||||
req.brew = _.defaults(brew, DEFAULT_BREW);
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ import Markdown from '../shared/naturalcrit/markdown.js';
|
||||
import yaml from 'js-yaml';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
import { nanoid } from 'nanoid';
|
||||
import {makePatches, applyPatches, stringifyPatches, parsePatch} from '@sanity/diff-match-patch';
|
||||
import { md5 } from 'hash-wasm';
|
||||
import { splitTextStyleAndMetadata,
|
||||
brewSnippetsToJSON } from '../shared/helpers.js';
|
||||
import checkClientVersion from './middleware/check-client-version.js';
|
||||
@@ -337,21 +339,30 @@ const api = {
|
||||
// Initialize brew from request and body, destructure query params, and set the initial value for the after-save method
|
||||
const brewFromClient = api.excludePropsFromUpdate(req.body);
|
||||
const brewFromServer = req.brew;
|
||||
if(brewFromServer.version && brewFromClient.version && brewFromServer.version > brewFromClient.version) {
|
||||
console.log(`Version mismatch on brew ${brewFromClient.editId}`);
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
return res.status(409).send(JSON.stringify({ message: `The brew has been changed on a different device. Please save your changes elsewhere, refresh, and try again.` }));
|
||||
}
|
||||
splitTextStyleAndMetadata(brewFromServer);
|
||||
|
||||
brewFromServer.hash = await md5(brewFromServer.text);
|
||||
|
||||
if((brewFromServer?.version !== brewFromClient?.version) || (brewFromServer?.hash !== brewFromClient?.hash)) {
|
||||
if(brewFromClient?.version !== brewFromClient?.version)
|
||||
console.log(`Version mismatch on brew ${brewFromClient.editId}`);
|
||||
if(brewFromServer?.hash !== brewFromClient?.hash) {
|
||||
console.log(`Hash mismatch on brew ${brewFromClient.editId}`);
|
||||
}
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
return res.status(409).send(JSON.stringify({ message: `The server copy is out of sync with the saved brew. Please save your changes elsewhere, refresh, and try again.` }));
|
||||
}
|
||||
|
||||
let brew = _.assign(brewFromServer, brewFromClient);
|
||||
brew.title = brew.title.trim();
|
||||
brew.description = brew.description.trim() || '';
|
||||
brew.text = applyPatches(brewFromClient.patches, brewFromServer.text)[0];
|
||||
brew.text = api.mergeBrewText(brew);
|
||||
|
||||
let brew = _.assign(brewFromServer, brewFromClient);
|
||||
const googleId = brew.googleId;
|
||||
const { saveToGoogle, removeFromGoogle } = req.query;
|
||||
let afterSave = async ()=>true;
|
||||
|
||||
brew.title = brew.title.trim();
|
||||
brew.description = brew.description.trim() || '';
|
||||
brew.text = api.mergeBrewText(brew);
|
||||
|
||||
if(brew.googleId && removeFromGoogle) {
|
||||
// If the google id exists and we're removing it from google, set afterSave to delete the google brew and mark the brew's google id as undefined
|
||||
afterSave = async ()=>{
|
||||
@@ -412,6 +423,8 @@ const api = {
|
||||
const after = await afterSave();
|
||||
if(!after) return;
|
||||
|
||||
saved.textBin = undefined; // Remove textBin from the saved object to save bandwidth
|
||||
|
||||
res.status(200).send(saved);
|
||||
},
|
||||
deleteGoogleBrew : async (account, id, editId, res)=>{
|
||||
@@ -482,8 +495,8 @@ const api = {
|
||||
};
|
||||
|
||||
router.post('/api', checkClientVersion, asyncHandler(api.newBrew));
|
||||
router.put('/api/:id', checkClientVersion, asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew));
|
||||
router.put('/api/update/:id', checkClientVersion, asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew));
|
||||
router.put('/api/:id', checkClientVersion, asyncHandler(api.getBrew('edit', false)), asyncHandler(api.updateBrew));
|
||||
router.put('/api/update/:id', checkClientVersion, asyncHandler(api.getBrew('edit', false)), asyncHandler(api.updateBrew));
|
||||
router.delete('/api/:id', checkClientVersion, asyncHandler(api.deleteBrew));
|
||||
router.get('/api/remove/:id', checkClientVersion, asyncHandler(api.deleteBrew));
|
||||
router.get('/api/theme/:renderer/:id', asyncHandler(api.getThemeBundle));
|
||||
|
||||
41
themes/assets/naturalCritLogoBlack.svg
Normal file
41
themes/assets/naturalCritLogoBlack.svg
Normal file
@@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
viewBox="0 0 94.65 94.6"
|
||||
version="1.1"
|
||||
id="svg11"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<defs
|
||||
id="defs4">
|
||||
<style
|
||||
id="style2">.cls-1{fill:#ed1f24;}</style>
|
||||
</defs>
|
||||
<title
|
||||
id="title6">NaturalCritLogo</title>
|
||||
<g
|
||||
id="Layer_2"
|
||||
data-name="Layer 2"
|
||||
style="fill:#000000">
|
||||
<g
|
||||
id="base"
|
||||
style="fill:#000000">
|
||||
<path
|
||||
id="D20"
|
||||
class="cls-1"
|
||||
d="M63.45.09s-45.91,12.4-46,12.45a.71.71,0,0,0-.15.08l-.15.1-.12.11a1.07,1.07,0,0,0-.14.16l-.09.11-.12.23,0,.06L.2,54.9a1.59,1.59,0,0,0,.11,1.69L29.36,94h0l0,0,.08.08.08.08.09.09.08.06.13.07a0,0,0,0,0,0,0,1.59,1.59,0,0,0,.27.12l.13.05.06,0a1.55,1.55,0,0,0,.37,0,1.63,1.63,0,0,0,.31,0l45.67-8.3.16,0,.11,0,.12,0,.06,0s0,0,0,0l.06,0a1.65,1.65,0,0,0,.36-.28l0-.06a1.6,1.6,0,0,0,.26-.38s0,0,0,0v0h0a.14.14,0,0,1,0-.06L94.52,43.74a1.4,1.4,0,0,0,.11-.4.41.41,0,0,0,0-.11,1.13,1.13,0,0,0,0-.26.66.66,0,0,0,0-.14,2,2,0,0,0-.06-.26l0-.11a2.68,2.68,0,0,0-.18-.33v0L65.29.6C64.77-.31,63.45.09,63.45.09ZM74.9,81.7l-28.81-18L78.5,38.49ZM44.1,61l-11-40.17L77,35.39ZM82,37.78l8.92,5.95L79,73.48Zm4.46-1.1-4.6-3.06L75.69,21.36Zm-9.26-4.8-42.07-14,28.05-14ZM30.56,16.34l-6.49-2.16L47.85,7.7Zm-11.35-.21L27.88,19,7.64,45Zm10.73,5.76L40.78,61.64,4.64,54.42Zm10.82,43.2L30.26,89.6,5.75,58.09Zm3.16,1.24L71.74,83.72l-38.26,7Z"
|
||||
style="fill:#000000;fill-opacity:1" />
|
||||
</g>
|
||||
</g>
|
||||
<metadata
|
||||
id="metadata1">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:title>NaturalCritLogo</dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
Reference in New Issue
Block a user