// ==UserScript==
// @name Smart Flow for WebNovel
// @description A Script that lets you modify the formatting of webnovel stories. You can customise font type, font size, text width & line height. + Plus more in future updates
// @version 1.1
// @author Grimlock7
// @license MIT
// @namespace smartflow-webnovel
// @match https://www.webnovel.com/book/*/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
let smartFlowEnabled = false;
let dragEnabled = true;
const defaultSettings = {
fontFamily: 'Georgia',
fontSize: '22px',
lineHeight: '1.8',
maxWidth: '1800px',
autoEnable: false
};
const loadSettings = () => {
const saved = localStorage.getItem('smartflow-settings');
return saved ? JSON.parse(saved) : { ...defaultSettings };
};
const saveSettings = (settings) => {
localStorage.setItem('smartflow-settings', JSON.stringify(settings));
};
const updateStyles = (settings) => {
const style = document.getElementById('smartflow-style');
if (style) style.remove();
const newStyle = document.createElement('style');
newStyle.id = 'smartflow-style';
newStyle.textContent = `
.smartflow-container {
max-width: ${settings.maxWidth};
margin: 0 auto;
padding: 0 20px;
font-family: ${settings.fontFamily};
}
.smartflow-paragraph {
font-size: ${settings.fontSize};
line-height: ${settings.lineHeight};
word-break: break-word;
hyphens: auto;
white-space: normal;
text-align: justify;
margin-bottom: 1em;
}
`;
document.head.appendChild(newStyle);
};
const updateLiveStyles = (settings) => {
updateStyles(settings);
document.querySelectorAll('.smartflow-container').forEach(container => {
container.style.maxWidth = settings.maxWidth;
container.style.fontFamily = settings.fontFamily;
});
document.querySelectorAll('.smartflow-paragraph').forEach(p => {
p.style.fontSize = settings.fontSize;
p.style.lineHeight = settings.lineHeight;
p.style.fontFamily = settings.fontFamily;
});
};
const detectStoryContainer = () => {
return Array.from(document.querySelectorAll('div'))
.find(div => div.innerText.length > 500 && div.offsetHeight > 300);
};
const applySmartFlow = (div, settings) => {
if (!div || div.classList.contains('smartflow-container')) return;
div.classList.add('smartflow-container');
updateStyles(settings);
const blocks = div.querySelectorAll('p, div');
blocks.forEach(el => {
el.classList.add('smartflow-paragraph');
el.style.fontSize = settings.fontSize;
el.style.lineHeight = settings.lineHeight;
el.style.fontFamily = settings.fontFamily;
});
};
const removeSmartFlow = (div) => {
if (!div) return;
// Remove SmartFlow container class
div.classList.remove('smartflow-container');
// Remove SmartFlow paragraph class and inline styles
const blocks = div.querySelectorAll('.smartflow-paragraph');
blocks.forEach(el => {
el.classList.remove('smartflow-paragraph');
el.style.fontSize = '';
el.style.lineHeight = '';
el.style.fontFamily = '';
el.style.maxWidth = '';
});
// Remove injected style block
const style = document.getElementById('smartflow-style');
if (style) style.remove();
};
const updatePanelPosition = () => {
const btn = document.getElementById('smartflow-toggle-btn');
const panel = document.getElementById('smartflow-panel');
if (!btn || !panel) return;
const rect = btn.getBoundingClientRect();
panel.style.top = rect.bottom + 10 + 'px';
panel.style.right = (window.innerWidth - rect.right) + 'px';
};
const createSettingsPanel = () => {
const settings = loadSettings();
const panel = document.createElement('div');
panel.id = 'smartflow-panel';
panel.style.position = 'fixed';
panel.style.zIndex = '9999';
panel.style.background = '#fff';
panel.style.border = '1px solid #ccc';
panel.style.borderRadius = '6px';
panel.style.padding = '12px';
panel.style.boxShadow = '0 2px 8px rgba(0,0,0,0.2)';
panel.style.display = 'none';
panel.style.minWidth = '240px';
panel.innerHTML = `
<label>Font:
<select id="sf-font">
<option value="Arial">Arial</option>
<option value="Georgia">Georgia</option>
<option value="sans-serif">Sans-serif</option>
<option value="serif">Serif</option>
<option value="Times New Roman">Times New Roman</option>
</select>
</label><br><br>
<label>Font Size:
<input type="range" id="sf-size" min="12" max="36" value="${parseInt(settings.fontSize)}">
<span id="sf-size-label">${settings.fontSize}</span>
</label><br><br>
<label>Line Height:
<input type="range" id="sf-line" min="1.0" max="3.0" step="0.1" value="${parseFloat(settings.lineHeight)}">
<span id="sf-line-label">${settings.lineHeight}</span>
</label><br><br>
<label>Text Width:
<input type="range" id="sf-width" min="800" max="1800" step="50" value="${parseInt(settings.maxWidth)}">
<span id="sf-width-label">${settings.maxWidth}</span> px
</label><br><br>
<label>
<input type="checkbox" id="sf-auto" ${settings.autoEnable ? 'checked' : ''}>
Auto Enable SmartFlow
</label><br><br>
<button id="sf-toggle">SmartFlow OFF </button><br><br>
</label><br><br>
<button id="sf-reset-pos">Reset Button Position</button> `;
document.body.appendChild(panel);
document.getElementById('sf-font').value = settings.fontFamily;
const getSettings = () => ({
fontFamily: document.getElementById('sf-font').value,
fontSize: document.getElementById('sf-size').value + 'px',
lineHeight: document.getElementById('sf-line').value,
maxWidth: document.getElementById('sf-width').value + 'px',
autoEnable: document.getElementById('sf-auto').checked
});
const toggleBtn = document.getElementById('sf-toggle');
toggleBtn.style.backgroundColor = smartFlowEnabled ? 'green' : 'red';
toggleBtn.style.color = '#fff';
toggleBtn.style.border = 'none';
toggleBtn.style.padding = '6px 10px';
toggleBtn.style.borderRadius = '4px';
toggleBtn.style.cursor = 'pointer';
toggleBtn.addEventListener('click', () => {
const storyDiv = detectStoryContainer();
if (!storyDiv) return;
smartFlowEnabled = !smartFlowEnabled;
if (smartFlowEnabled) {
applySmartFlow(storyDiv, loadSettings());
toggleBtn.textContent = 'SmartFlow ON';
toggleBtn.style.backgroundColor = 'green';
} else {
removeSmartFlow(storyDiv);
toggleBtn.textContent = 'SmartFlow OFF';
toggleBtn.style.backgroundColor = 'red';
}
});
const handleSettingChange = () => {
const newSettings = getSettings();
saveSettings(newSettings);
if (smartFlowEnabled) {
updateLiveStyles(newSettings);
}
document.getElementById('sf-size-label').textContent = document.getElementById('sf-size').value + 'px';
document.getElementById('sf-line-label').textContent = document.getElementById('sf-line').value;
document.getElementById('sf-width-label').textContent = document.getElementById('sf-width').value;
};
['sf-font', 'sf-size', 'sf-line', 'sf-width', 'sf-auto'].forEach(id => {
document.getElementById(id).addEventListener('input', handleSettingChange);
});
document.getElementById('sf-reset-pos').addEventListener('click', () => {
localStorage.removeItem('smartflow-button-pos');
const btn = document.getElementById('smartflow-toggle-btn');
if (btn) {
btn.style.top = '10px';
btn.style.left = 'auto';
btn.style.right = '460px';
updatePanelPosition();
}
});
};
const createToggleButton = () => {
const savedPos = JSON.parse(localStorage.getItem('smartflow-button-pos')) || {
top: 10,
anchor: 'right',
offset: 460
};
const btn = document.createElement('button');
btn.textContent = 'SmartFlow';
btn.id = 'smartflow-toggle-btn';
btn.style.position = 'fixed';
btn.style.top = savedPos.top + 'px';
btn.style.zIndex = '9999';
btn.style.padding = '8px 12px';
btn.style.fontSize = '14px';
btn.style.background = '#222';
btn.style.color = '#fff';
btn.style.border = 'none';
btn.style.borderRadius = '4px';
btn.style.cursor = 'move';
btn.style.boxShadow = '0 2px 6px rgba(0,0,0,0.3)';
btn.style.userSelect = 'none';
btn.style.whiteSpace = 'normal';
btn.style.wordBreak = 'break-word';
btn.style.maxWidth = '160px';
if (savedPos.anchor === 'left') {
btn.style.left = savedPos.offset + 'px';
btn.style.right = 'auto';
} else {
btn.style.right = savedPos.offset + 'px';
btn.style.left = 'auto';
}
let isDragging = false;
let offsetX = 0, offsetY = 0;
btn.addEventListener('mousedown', (e) => {
if (!dragEnabled) {
const panel = document.getElementById('smartflow-panel');
if (panel) {
panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
updatePanelPosition();
}
return;
}
isDragging = false;
offsetX = e.clientX;
offsetY = e.clientY;
const startTop = btn.offsetTop;
const startLeft = btn.offsetLeft;
const startRight = window.innerWidth - btn.offsetLeft - btn.offsetWidth;
const onMouseMove = (e) => {
const deltaX = e.clientX - offsetX;
const deltaY = e.clientY - offsetY;
if (Math.abs(deltaX) > 3 || Math.abs(deltaY) > 3) {
isDragging = true;
}
const newTop = Math.min(Math.max(startTop + deltaY, 0), window.innerHeight - btn.offsetHeight);
btn.style.top = newTop + 'px';
let anchor, offset;
if (e.clientX < window.innerWidth / 2) {
anchor = 'left';
offset = Math.min(Math.max(startLeft + deltaX, 0), window.innerWidth - btn.offsetWidth);
btn.style.left = offset + 'px';
btn.style.right = 'auto';
} else {
anchor = 'right';
offset = Math.min(Math.max(startRight - deltaX, 0), window.innerWidth - btn.offsetWidth);
btn.style.right = offset + 'px';
btn.style.left = 'auto';
}
localStorage.setItem('smartflow-button-pos', JSON.stringify({ top: newTop, anchor, offset }));
updatePanelPosition();
};
const onMouseUp = () => {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
if (!isDragging) {
const panel = document.getElementById('smartflow-panel');
if (panel) {
panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
updatePanelPosition();
}
}
};
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
});
document.body.appendChild(btn);
updatePanelPosition();
};
const watchUrlChange = () => {
let lastUrl = location.href;
setInterval(() => {
if (location.href !== lastUrl) {
lastUrl = location.href;
const storyDiv = detectStoryContainer();
if (storyDiv && smartFlowEnabled) {
applySmartFlow(storyDiv, loadSettings());
}
}
}, 1000);
};
// Initialize everything
createSettingsPanel();
createToggleButton();
watchUrlChange();
const settings = loadSettings();
if (settings.autoEnable) {
const storyDiv = detectStoryContainer();
if (storyDiv) {
smartFlowEnabled = true;
applySmartFlow(storyDiv, settings);
document.getElementById('smartflow-panel').style.display = 'block';
}
}
})();