mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-01-09 20:12:41 +00:00
Merge pull request #3711 from G-Ambatte/experimentalLocalStorageHistory
Store limited Brew History in Local Storage
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
|
/*eslint max-lines: ["warn", {"max": 500, "skipBlankLines": true, "skipComments": true}]*/
|
||||||
require('./editor.less');
|
require('./editor.less');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
@@ -500,7 +500,9 @@ const Editor = createClass({
|
|||||||
currentEditorTheme={this.state.editorTheme}
|
currentEditorTheme={this.state.editorTheme}
|
||||||
updateEditorTheme={this.updateEditorTheme}
|
updateEditorTheme={this.updateEditorTheme}
|
||||||
snippetBundle={this.props.snippetBundle}
|
snippetBundle={this.props.snippetBundle}
|
||||||
cursorPos={this.codeEditor.current?.getCursorPosition() || {}} />
|
cursorPos={this.codeEditor.current?.getCursorPosition() || {}}
|
||||||
|
updateBrew={this.props.updateBrew}
|
||||||
|
/>
|
||||||
|
|
||||||
{this.renderEditor()}
|
{this.renderEditor()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ const createClass = require('create-react-class');
|
|||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const cx = require('classnames');
|
const cx = require('classnames');
|
||||||
|
|
||||||
|
import { getHistoryItems, historyExists } from '../../utils/versionHistory.js';
|
||||||
|
|
||||||
//Import all themes
|
//Import all themes
|
||||||
const ThemeSnippets = {};
|
const ThemeSnippets = {};
|
||||||
ThemeSnippets['Legacy_5ePHB'] = require('themes/Legacy/5ePHB/snippets.js');
|
ThemeSnippets['Legacy_5ePHB'] = require('themes/Legacy/5ePHB/snippets.js');
|
||||||
@@ -38,7 +40,8 @@ const Snippetbar = createClass({
|
|||||||
unfoldCode : ()=>{},
|
unfoldCode : ()=>{},
|
||||||
updateEditorTheme : ()=>{},
|
updateEditorTheme : ()=>{},
|
||||||
cursorPos : {},
|
cursorPos : {},
|
||||||
snippetBundle : []
|
snippetBundle : [],
|
||||||
|
updateBrew : ()=>{}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -46,7 +49,8 @@ const Snippetbar = createClass({
|
|||||||
return {
|
return {
|
||||||
renderer : this.props.renderer,
|
renderer : this.props.renderer,
|
||||||
themeSelector : false,
|
themeSelector : false,
|
||||||
snippets : []
|
snippets : [],
|
||||||
|
historyExists : false
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -59,13 +63,15 @@ const Snippetbar = createClass({
|
|||||||
|
|
||||||
componentDidUpdate : async function(prevProps) {
|
componentDidUpdate : async function(prevProps) {
|
||||||
if(prevProps.renderer != this.props.renderer || prevProps.theme != this.props.theme || prevProps.snippetBundle != this.props.snippetBundle) {
|
if(prevProps.renderer != this.props.renderer || prevProps.theme != this.props.theme || prevProps.snippetBundle != this.props.snippetBundle) {
|
||||||
const snippets = this.compileSnippets();
|
|
||||||
this.setState({
|
this.setState({
|
||||||
snippets : snippets
|
snippets : this.compileSnippets()
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
},
|
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
historyExists : historyExists(this.props.brew)
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
mergeCustomizer : function(oldValue, newValue, key) {
|
mergeCustomizer : function(oldValue, newValue, key) {
|
||||||
if(key == 'snippets') {
|
if(key == 'snippets') {
|
||||||
@@ -138,6 +144,36 @@ const Snippetbar = createClass({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
replaceContent : function(item){
|
||||||
|
return this.props.updateBrew(item);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderHistoryItems : function() {
|
||||||
|
const historyItems = getHistoryItems(this.props.brew);
|
||||||
|
|
||||||
|
return <div className='dropdown'>
|
||||||
|
{_.map(historyItems, (item, index)=>{
|
||||||
|
if(!item.savedAt) return;
|
||||||
|
|
||||||
|
const saveTime = new Date(item.savedAt);
|
||||||
|
const diffMs = new Date() - saveTime;
|
||||||
|
const diffSecs = Math.floor(diffMs / 1000);
|
||||||
|
|
||||||
|
let diffString = `about ${diffSecs} seconds ago`;
|
||||||
|
|
||||||
|
if(diffSecs > 60) diffString = `about ${Math.floor(diffSecs / 60)} minutes ago`;
|
||||||
|
if(diffSecs > (60 * 60)) diffString = `about ${Math.floor(diffSecs / (60 * 60))} hours ago`;
|
||||||
|
if(diffSecs > (24 * 60 * 60)) diffString = `about ${Math.floor(diffSecs / (24 * 60 * 60))} days ago`;
|
||||||
|
if(diffSecs > (7 * 24 * 60 * 60)) diffString = `about ${Math.floor(diffSecs / (7 * 24 * 60 * 60))} weeks ago`;
|
||||||
|
|
||||||
|
return <div className='snippet' key={index} onClick={()=>{this.replaceContent(item);}} >
|
||||||
|
<i className={`fas fa-${index+1}`} />
|
||||||
|
<span className='name' title={saveTime.toISOString()}>v{item.version} : {diffString}</span>
|
||||||
|
</div>;
|
||||||
|
})}
|
||||||
|
</div>;
|
||||||
|
},
|
||||||
|
|
||||||
renderEditorButtons : function(){
|
renderEditorButtons : function(){
|
||||||
if(!this.props.showEditButtons) return;
|
if(!this.props.showEditButtons) return;
|
||||||
|
|
||||||
@@ -158,6 +194,10 @@ const Snippetbar = createClass({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return <div className='editors'>
|
return <div className='editors'>
|
||||||
|
<div className={`editorTool snippetGroup history ${this.state.historyExists ? 'active' : ''}`} >
|
||||||
|
<i className='fas fa-clock-rotate-left' />
|
||||||
|
{this.state.historyExists && this.renderHistoryItems() }
|
||||||
|
</div>
|
||||||
<div className={`editorTool undo ${this.props.historySize.undo ? 'active' : ''}`}
|
<div className={`editorTool undo ${this.props.historySize.undo ? 'active' : ''}`}
|
||||||
onClick={this.props.undo} >
|
onClick={this.props.undo} >
|
||||||
<i className='fas fa-undo' />
|
<i className='fas fa-undo' />
|
||||||
|
|||||||
@@ -53,6 +53,21 @@
|
|||||||
font-size : 0.75em;
|
font-size : 0.75em;
|
||||||
color : inherit;
|
color : inherit;
|
||||||
}
|
}
|
||||||
|
&.history {
|
||||||
|
.tooltipLeft('History');
|
||||||
|
font-size : 0.75em;
|
||||||
|
color : grey;
|
||||||
|
position : relative;
|
||||||
|
&.active {
|
||||||
|
color : inherit;
|
||||||
|
}
|
||||||
|
&>.dropdown{
|
||||||
|
right : -1px;
|
||||||
|
&>.snippet{
|
||||||
|
padding-right : 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
&.editorTheme {
|
&.editorTheme {
|
||||||
.tooltipLeft('Editor Themes');
|
.tooltipLeft('Editor Themes');
|
||||||
font-size : 0.75em;
|
font-size : 0.75em;
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ const Markdown = require('naturalcrit/markdown.js');
|
|||||||
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
|
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
|
||||||
const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js');
|
const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js');
|
||||||
|
|
||||||
|
import { updateHistory, versionHistoryGarbageCollection } from '../../utils/versionHistory.js';
|
||||||
|
|
||||||
const googleDriveIcon = require('../../googleDrive.svg');
|
const googleDriveIcon = require('../../googleDrive.svg');
|
||||||
|
|
||||||
const SAVE_TIMEOUT = 3000;
|
const SAVE_TIMEOUT = 3000;
|
||||||
@@ -164,6 +166,16 @@ const EditPage = createClass({
|
|||||||
return !_.isEqual(this.state.brew, this.savedBrew);
|
return !_.isEqual(this.state.brew, this.savedBrew);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateBrew : function(newData){
|
||||||
|
this.setState((prevState)=>({
|
||||||
|
brew : {
|
||||||
|
...prevState.brew,
|
||||||
|
style : newData.style,
|
||||||
|
text : newData.text
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
trySave : function(immediate=false){
|
trySave : function(immediate=false){
|
||||||
if(!this.debounceSave) this.debounceSave = _.debounce(this.save, SAVE_TIMEOUT);
|
if(!this.debounceSave) this.debounceSave = _.debounce(this.save, SAVE_TIMEOUT);
|
||||||
if(this.hasChanges()){
|
if(this.hasChanges()){
|
||||||
@@ -216,6 +228,9 @@ const EditPage = createClass({
|
|||||||
htmlErrors : Markdown.validate(prevState.brew.text)
|
htmlErrors : Markdown.validate(prevState.brew.text)
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
updateHistory(this.state.brew);
|
||||||
|
versionHistoryGarbageCollection();
|
||||||
|
|
||||||
const transfer = this.state.saveGoogle == _.isNil(this.state.brew.googleId);
|
const transfer = this.state.saveGoogle == _.isNil(this.state.brew.googleId);
|
||||||
|
|
||||||
const brew = this.state.brew;
|
const brew = this.state.brew;
|
||||||
@@ -427,6 +442,7 @@ const EditPage = createClass({
|
|||||||
renderer={this.state.brew.renderer}
|
renderer={this.state.brew.renderer}
|
||||||
userThemes={this.props.userThemes}
|
userThemes={this.props.userThemes}
|
||||||
snippetBundle={this.state.themeBundle.snippets}
|
snippetBundle={this.state.themeBundle.snippets}
|
||||||
|
updateBrew={this.updateBrew}
|
||||||
onCursorPageChange={this.handleEditorCursorPageChange}
|
onCursorPageChange={this.handleEditorCursorPageChange}
|
||||||
onViewPageChange={this.handleEditorViewPageChange}
|
onViewPageChange={this.handleEditorViewPageChange}
|
||||||
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
|
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
|
||||||
|
|||||||
116
client/homebrew/utils/versionHistory.js
Normal file
116
client/homebrew/utils/versionHistory.js
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
export const HISTORY_PREFIX = 'HOMEBREWERY-HISTORY';
|
||||||
|
export const HISTORY_SLOTS = 5;
|
||||||
|
|
||||||
|
// History values in minutes
|
||||||
|
const DEFAULT_HISTORY_SAVE_DELAYS = {
|
||||||
|
'0' : 0,
|
||||||
|
'1' : 2,
|
||||||
|
'2' : 10,
|
||||||
|
'3' : 60,
|
||||||
|
'4' : 12 * 60,
|
||||||
|
'5' : 2 * 24 * 60
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_GARBAGE_COLLECT_DELAY = 28 * 24 * 60;
|
||||||
|
|
||||||
|
const HISTORY_SAVE_DELAYS = global.config?.historyData?.HISTORY_SAVE_DELAYS ?? DEFAULT_HISTORY_SAVE_DELAYS;
|
||||||
|
const GARBAGE_COLLECT_DELAY = global.config?.historyData?.GARBAGE_COLLECT_DELAY ?? DEFAULT_GARBAGE_COLLECT_DELAY;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function getKeyBySlot(brew, slot){
|
||||||
|
return `${HISTORY_PREFIX}-${brew.shareId}-${slot}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getVersionBySlot(brew, slot){
|
||||||
|
// Read stored brew data
|
||||||
|
// - If it exists, parse data to object
|
||||||
|
// - If it doesn't exist, pass default object
|
||||||
|
const key = getKeyBySlot(brew, slot);
|
||||||
|
const storedVersion = localStorage.getItem(key);
|
||||||
|
const output = storedVersion ? JSON.parse(storedVersion) : { expireAt: '2000-01-01T00:00:00.000Z', shareId: brew.shareId, noData: true };
|
||||||
|
return output;
|
||||||
|
};
|
||||||
|
|
||||||
|
function updateStoredBrew(brew, slot = 0) {
|
||||||
|
const archiveBrew = {
|
||||||
|
title : brew.title,
|
||||||
|
text : brew.text,
|
||||||
|
style : brew.style,
|
||||||
|
version : brew.version,
|
||||||
|
shareId : brew.shareId,
|
||||||
|
savedAt : brew?.savedAt || new Date(),
|
||||||
|
expireAt : new Date()
|
||||||
|
};
|
||||||
|
|
||||||
|
archiveBrew.expireAt.setMinutes(archiveBrew.expireAt.getMinutes() + HISTORY_SAVE_DELAYS[slot]);
|
||||||
|
|
||||||
|
const key = getKeyBySlot(brew, slot);
|
||||||
|
localStorage.setItem(key, JSON.stringify(archiveBrew));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function historyExists(brew){
|
||||||
|
return Object.keys(localStorage)
|
||||||
|
.some((key)=>{
|
||||||
|
return key.startsWith(`${HISTORY_PREFIX}-${brew.shareId}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadHistory(brew){
|
||||||
|
const history = {};
|
||||||
|
|
||||||
|
// Load data from local storage to History object
|
||||||
|
for (let i = 1; i <= HISTORY_SLOTS; i++){
|
||||||
|
history[i] = getVersionBySlot(brew, i);
|
||||||
|
};
|
||||||
|
|
||||||
|
return history;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateHistory(brew) {
|
||||||
|
const history = loadHistory(brew);
|
||||||
|
|
||||||
|
// Walk each version position
|
||||||
|
for (let slot = HISTORY_SLOTS; slot > 0; slot--){
|
||||||
|
const storedVersion = history[slot];
|
||||||
|
|
||||||
|
// If slot has expired, update all lower slots and break
|
||||||
|
if(new Date() >= new Date(storedVersion.expireAt)){
|
||||||
|
for (let updateSlot = slot - 1; updateSlot>0; updateSlot--){
|
||||||
|
// Move data from updateSlot to updateSlot + 1
|
||||||
|
!history[updateSlot]?.noData && updateStoredBrew(history[updateSlot], updateSlot + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update the most recent brew
|
||||||
|
updateStoredBrew(brew, 1);
|
||||||
|
|
||||||
|
// Break out of data checks because we found an expired value
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getHistoryItems(brew){
|
||||||
|
const historyArray = [];
|
||||||
|
|
||||||
|
for (let i = 1; i <= HISTORY_SLOTS; i++){
|
||||||
|
historyArray.push(getVersionBySlot(brew, i));
|
||||||
|
}
|
||||||
|
|
||||||
|
return historyArray;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function versionHistoryGarbageCollection(){
|
||||||
|
Object.keys(localStorage)
|
||||||
|
.filter((key)=>{
|
||||||
|
return key.startsWith(HISTORY_PREFIX);
|
||||||
|
})
|
||||||
|
.forEach((key)=>{
|
||||||
|
const collectAt = new Date(JSON.parse(localStorage.getItem(key)).savedAt);
|
||||||
|
collectAt.setMinutes(collectAt.getMinutes() + GARBAGE_COLLECT_DELAY);
|
||||||
|
if(new Date() > collectAt){
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
1121
server/app.js
1121
server/app.js
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user