Feat: rewrite Reader in vanjs (#1194)
* setup van.js * scrollbar * Reader Footer * move state to reader object * scroll to previous progress * image modal * TTS bubble * swipe gestures * binoic reading & remove extra spacing * tap to scroll * tap & scroll to move page * page reader move chapter * display page in footer * hide progress bar in page reader mode * remember postion when switch reader mode * refactor: move initial scroll out of scrollbar * scroll bar in page reader mode * chapter ending content * fix: tts controller * fix: tts highlight & stopping * optional tts * custom js * update preview reader * cleanup * fix: css
This commit is contained in:
+2
-1
@@ -1,2 +1,3 @@
|
||||
**/node_modules
|
||||
android
|
||||
android/**/js/van.js
|
||||
android/**/js/text-vibe.js
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
* {
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
body {
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
html {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
chapter {
|
||||
/* height: calc(100% - 3% - var(--readerSettings-padding) - 2rem); */
|
||||
height: 90vh;
|
||||
display: flexbox;
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
column-gap: 0;
|
||||
column-width: 100vw;
|
||||
transition: 200ms;
|
||||
padding-top: calc(3% + var(--readerSettings-padding));
|
||||
}
|
||||
@@ -3,7 +3,6 @@ html {
|
||||
overflow-x: hidden;
|
||||
padding-top: var(--StatusBar-currentHeight);
|
||||
word-wrap: break-word;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::selection {
|
||||
@@ -19,7 +18,6 @@ html {
|
||||
body {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
padding: 0;
|
||||
padding-bottom: 40px;
|
||||
|
||||
font-size: var(--readerSettings-textSize);
|
||||
@@ -30,17 +28,23 @@ body {
|
||||
font-family: var(--readerSettings-fontFamily);
|
||||
}
|
||||
|
||||
chapter {
|
||||
width: 100%;
|
||||
display: block;
|
||||
body.page-reader {
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
chapter > * {
|
||||
max-width: 100vw;
|
||||
#LNReader-chapter {
|
||||
padding-left: var(--readerSettings-padding);
|
||||
padding-right: var(--readerSettings-padding);
|
||||
}
|
||||
|
||||
body.page-reader > #LNReader-chapter{
|
||||
height: calc(90vh);
|
||||
column-width: calc(100vw - var(--readerSettings-padding) * 2);
|
||||
column-gap: calc(var(--readerSettings-padding) * 2);
|
||||
transition: 200ms;
|
||||
}
|
||||
|
||||
hr {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
@@ -82,11 +86,10 @@ td {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.nextButton,
|
||||
.infoText {
|
||||
.next-button,
|
||||
.info-text {
|
||||
margin-left: var(--readerSettings-padding);
|
||||
margin-right: var(--readerSettings-padding);
|
||||
width: calc(100% - var(--readerSettings-padding) * 2);
|
||||
border-radius: 50px;
|
||||
border-width: 1;
|
||||
color: var(--theme-onPrimary);
|
||||
@@ -94,18 +97,18 @@ td {
|
||||
font-family: var(--readerSettings-fontFamily);
|
||||
font-size: 16px;
|
||||
border-width: 0;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.nextButton {
|
||||
.next-button {
|
||||
min-height: 40px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
padding: 0 16px;
|
||||
width: calc(100% - var(--readerSettings-padding) * 2);
|
||||
}
|
||||
|
||||
.infoText {
|
||||
.info-text {
|
||||
background-color: transparent;
|
||||
text-align: center;
|
||||
border: none;
|
||||
@@ -115,11 +118,6 @@ td {
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.chapterCtn {
|
||||
min-height: var(--chapterCtn-height);
|
||||
margin-bottom: auto;
|
||||
}
|
||||
|
||||
.d-none {
|
||||
display: none;
|
||||
}
|
||||
@@ -159,23 +157,6 @@ td {
|
||||
right: unset;
|
||||
}
|
||||
|
||||
#TTS-Controller {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#ToolWrapper.horizontal > #TTS-Controller {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
#TTS-Controller button {
|
||||
background: var(--theme-surface-0-9);
|
||||
border: 0;
|
||||
}
|
||||
|
||||
#TTS-Controller svg {
|
||||
fill: var(--theme-onSurface);
|
||||
}
|
||||
|
||||
#ToolWrapper > #ScrollBar {
|
||||
margin-top: 8px;
|
||||
width: 2.4rem;
|
||||
@@ -320,115 +301,11 @@ td {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
color: var(--theme-onSecondary);
|
||||
.highlight, .highlight *{
|
||||
color: var(--theme-onSecondary) !important;
|
||||
background-color: var(--theme-secondary);
|
||||
}
|
||||
|
||||
.contextMenu {
|
||||
position: fixed;
|
||||
background: var(--theme-surface-0-9);
|
||||
height: 0;
|
||||
top: var(--top);
|
||||
left: var(--left);
|
||||
overflow: hidden;
|
||||
-webkit-backdrop-filter: blur(1px);
|
||||
backdrop-filter: blur(1px);
|
||||
-webkit-animation: menuAnimation 0.4s 0s both;
|
||||
animation: menuAnimation 0.4s 0s both;
|
||||
transform-origin: left;
|
||||
list-style: none;
|
||||
margin: 4px;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 999999999;
|
||||
}
|
||||
|
||||
.contextMenu-item {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.contextMenu-button {
|
||||
color: var(--theme-onSurface);
|
||||
background: 0;
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
white-space: nowrap;
|
||||
padding: 6px 24px 6px 7px;
|
||||
text-align: left;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
-webkit-animation: menuItemAnimation 0.2s 0s both;
|
||||
animation: menuItemAnimation 0.2s 0s both;
|
||||
font-family: var(--readerSettings-fontFamily);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
-webkit-tap-highlight-color: var(--theme-rippleColor);
|
||||
}
|
||||
|
||||
.contextMenu-button svg {
|
||||
fill: var(--theme-onSurface);
|
||||
}
|
||||
|
||||
.contextMenu-button span {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
@-webkit-keyframes menuAnimation {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.5);
|
||||
}
|
||||
|
||||
100% {
|
||||
height: var(--height);
|
||||
opacity: 1;
|
||||
border-radius: 8px;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes menuAnimation {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.5);
|
||||
}
|
||||
|
||||
100% {
|
||||
height: var(--height);
|
||||
opacity: 1;
|
||||
border-radius: 8px;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes menuItemAnimation {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateX(-10px);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes menuItemAnimation {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateX(-10px);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
#Image-Modal {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
@@ -450,3 +327,41 @@ td {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#TTS-Controller {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 20px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
#TTS-Controller button {
|
||||
background: var(--theme-surface-0-9);
|
||||
outline: none;
|
||||
border-width: 0;
|
||||
display: flex;
|
||||
justify-items: center;
|
||||
align-items: center;
|
||||
padding: 4px;
|
||||
border-radius: 100%;
|
||||
transition: 1s;
|
||||
}
|
||||
|
||||
#TTS-Controller.active{
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#TTS-Controller.active button {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
#TTS-Controller svg {
|
||||
fill: var(--theme-onSurface);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
#TTS-Controller.active svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
@@ -0,0 +1,472 @@
|
||||
window.reader = new (function () {
|
||||
const {
|
||||
readerSettings,
|
||||
chapterGeneralSettings,
|
||||
novel,
|
||||
chapter,
|
||||
nextChapter,
|
||||
batteryLevel,
|
||||
autoSaveInterval,
|
||||
DEBUG,
|
||||
strings,
|
||||
} = initialReaderConfig;
|
||||
|
||||
// state
|
||||
this.hidden = van.state(true);
|
||||
this.batteryLevel = van.state(batteryLevel);
|
||||
this.readerSettings = van.state(readerSettings);
|
||||
this.generalSettings = van.state(chapterGeneralSettings);
|
||||
|
||||
this.chapterElement = document.querySelector('#LNReader-chapter');
|
||||
this.selection = window.getSelection();
|
||||
this.viewport = document.querySelector('meta[name=viewport]');
|
||||
|
||||
this.novel = novel;
|
||||
this.chapter = chapter;
|
||||
this.nextChapter = nextChapter;
|
||||
this.strings = strings;
|
||||
this.autoSaveInterval = autoSaveInterval;
|
||||
this.rawHTML = this.chapterElement.innerHTML;
|
||||
|
||||
//layout props
|
||||
this.paddingTop = parseInt(
|
||||
getComputedStyle(document.querySelector('html')).getPropertyValue(
|
||||
'padding-top',
|
||||
),
|
||||
10,
|
||||
);
|
||||
this.chapterHeight = this.chapterElement.scrollHeight + this.paddingTop;
|
||||
this.layoutHeight = window.screen.height;
|
||||
this.layoutWidth = window.screen.width;
|
||||
|
||||
this.post = obj => window.ReactNativeWebView.postMessage(JSON.stringify(obj));
|
||||
this.refresh = () => {
|
||||
if (this.generalSettings.val.pageReader) {
|
||||
this.chapterWidth = this.chapterElement.scrollWidth;
|
||||
} else {
|
||||
this.chapterHeight = this.chapterElement.scrollHeight + this.paddingTop;
|
||||
}
|
||||
};
|
||||
|
||||
van.derive(() => {
|
||||
const settings = this.readerSettings.val;
|
||||
document.documentElement.style.setProperty(
|
||||
'--readerSettings-theme',
|
||||
settings.theme,
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
'--readerSettings-padding',
|
||||
settings.padding + 'px',
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
'--readerSettings-textSize',
|
||||
settings.textSize + 'px',
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
'--readerSettings-textColor',
|
||||
settings.textColor,
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
'--readerSettings-textAlign',
|
||||
settings.textAlign,
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
'--readerSettings-lineHeight',
|
||||
settings.lineHeight,
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
'--readerSettings-fontFamily',
|
||||
settings.fontFamily,
|
||||
);
|
||||
if (settings.fontFamily) {
|
||||
new FontFace(
|
||||
settings.fontFamily,
|
||||
'url("file:///android_asset/fonts/' + settings.fontFamily + '.ttf")',
|
||||
)
|
||||
.load()
|
||||
.then(function (loadedFont) {
|
||||
document.fonts.add(loadedFont);
|
||||
});
|
||||
} else {
|
||||
// have no affect with a font declared in head
|
||||
document.fonts.forEach(fontFace => document.fonts.delete(fontFace));
|
||||
}
|
||||
});
|
||||
|
||||
setInterval(() => {
|
||||
if (!this.generalSettings.val.pageReader) {
|
||||
this.post({
|
||||
type: 'save',
|
||||
data: parseInt(
|
||||
((window.scrollY + this.layoutHeight) / this.chapterHeight) * 100,
|
||||
10,
|
||||
),
|
||||
});
|
||||
} else {
|
||||
this.post({
|
||||
type: 'save',
|
||||
data: parseInt(
|
||||
((pageReader.page.val + 1) / pageReader.totalPages.val) * 100,
|
||||
),
|
||||
});
|
||||
}
|
||||
}, this.autoSaveInterval);
|
||||
|
||||
if (DEBUG) {
|
||||
console = new Object();
|
||||
console.log = function (...data) {
|
||||
reader.post({ 'type': 'console', 'msg': data?.join(' ') });
|
||||
};
|
||||
console.debug = console.log;
|
||||
console.info = console.log;
|
||||
console.warn = console.log;
|
||||
console.error = console.log;
|
||||
}
|
||||
// end reader
|
||||
})();
|
||||
|
||||
window.tts = new (function () {
|
||||
this.readableNodeNames = [
|
||||
'#text',
|
||||
'B',
|
||||
'I',
|
||||
'SPAN',
|
||||
'EM',
|
||||
'BR',
|
||||
'STRONG',
|
||||
'A',
|
||||
];
|
||||
this.prevElement = null;
|
||||
this.currentElement = reader.chapterElement;
|
||||
this.started = false;
|
||||
this.reading = false;
|
||||
|
||||
this.readable = element => {
|
||||
ele = element ?? this.currentElement;
|
||||
if (
|
||||
ele.nodeName !== 'SPAN' &&
|
||||
this.readableNodeNames.includes(ele.nodeName)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (!ele.hasChildNodes()) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < ele.childNodes.length; i++) {
|
||||
if (!this.readableNodeNames.includes(ele.childNodes.item(i).nodeName)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// if can find a readable node, else stop tts
|
||||
this.findNextTextNode = () => {
|
||||
if (!this.started) {
|
||||
this.started = true;
|
||||
if (this.readable()) {
|
||||
return true;
|
||||
}
|
||||
} else if (this.currentElement.isSameNode(reader.chapterElement)) {
|
||||
return false;
|
||||
}
|
||||
// node is read -> next node or parent node
|
||||
if (this.currentElement.isSameNode(this.prevElement)) {
|
||||
this.prevElement = this.currentElement;
|
||||
if (this.currentElement.nextElementSibling) {
|
||||
this.currentElement = this.currentElement.nextElementSibling;
|
||||
} else {
|
||||
this.currentElement = this.currentElement.parentElement;
|
||||
}
|
||||
return this.findNextTextNode();
|
||||
}
|
||||
|
||||
if (this.readable()) {
|
||||
return true;
|
||||
}
|
||||
if (this.prevElement?.parentElement.isSameNode(this.currentElement)) {
|
||||
// is backtracking
|
||||
this.prevElement = this.currentElement;
|
||||
if (this.currentElement.nextElementSibling) {
|
||||
this.currentElement = this.currentElement.nextElementSibling;
|
||||
} else {
|
||||
this.currentElement = this.currentElement.parentElement;
|
||||
}
|
||||
return this.findNextTextNode();
|
||||
}
|
||||
if (this.currentElement.firstElementChild) {
|
||||
// go deeper
|
||||
this.prevElement = this.currentElement;
|
||||
this.currentElement = this.currentElement.firstElementChild;
|
||||
return this.findNextTextNode();
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
this.next = () => {
|
||||
try {
|
||||
this.currentElement?.classList?.remove('highlight');
|
||||
if (this.findNextTextNode()) {
|
||||
this.reading = true;
|
||||
this.speak();
|
||||
} else {
|
||||
this.reading = false;
|
||||
this.stop();
|
||||
document.getElementById('TTS-Controller').firstElementChild.innerHTML =
|
||||
volumnIcon;
|
||||
}
|
||||
} catch (e) {
|
||||
alert(e);
|
||||
}
|
||||
};
|
||||
|
||||
this.start = element => {
|
||||
this.stop();
|
||||
this.currentElement = element ?? reader.chapterElement;
|
||||
this.next();
|
||||
};
|
||||
|
||||
this.resume = () => {
|
||||
if (!this.reading) {
|
||||
if (
|
||||
this.currentElement &&
|
||||
this.currentElement.id !== 'LNReader-chapter'
|
||||
) {
|
||||
this.speak();
|
||||
this.reading = true;
|
||||
} else {
|
||||
this.next();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.pause = () => {
|
||||
this.reading = false;
|
||||
reader.post({ type: 'stop-speak' });
|
||||
};
|
||||
|
||||
this.stop = () => {
|
||||
reader.post({ type: 'stop-speak' });
|
||||
this.currentElement?.classList?.remove('highlight');
|
||||
this.prevElement = null;
|
||||
this.currentElement = reader.chapterElement;
|
||||
this.started = false;
|
||||
this.reading = false;
|
||||
};
|
||||
|
||||
this.speak = () => {
|
||||
this.prevElement = this.currentElement;
|
||||
this.currentElement.classList.add('highlight');
|
||||
reader.post({ type: 'speak', data: this.currentElement?.innerText });
|
||||
};
|
||||
})();
|
||||
|
||||
window.pageReader = new (function () {
|
||||
this.page = van.state(0);
|
||||
this.totalPages = van.state(0);
|
||||
|
||||
this.movePage = destPage => {
|
||||
destPage = parseInt(destPage);
|
||||
if (destPage < 0) {
|
||||
return reader.post({ type: 'prev' });
|
||||
}
|
||||
if (destPage >= this.totalPages.val) {
|
||||
return reader.post({ type: 'next' });
|
||||
}
|
||||
this.page.val = destPage;
|
||||
reader.chapterElement.style.transform =
|
||||
'translateX(-' + destPage * 100 + '%)';
|
||||
};
|
||||
|
||||
van.derive(() => {
|
||||
// ignore if initial or other states change
|
||||
if (
|
||||
reader.generalSettings.val.pageReader ===
|
||||
reader.generalSettings.oldVal.pageReader
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (reader.generalSettings.val.pageReader) {
|
||||
const ratio = Math.min(
|
||||
0.99,
|
||||
(window.scrollY + reader.layoutHeight) / reader.chapterHeight,
|
||||
);
|
||||
document.body.classList.add('page-reader');
|
||||
setTimeout(() => {
|
||||
reader.refresh();
|
||||
this.totalPages.val = parseInt(
|
||||
(reader.chapterWidth + reader.readerSettings.val.padding * 2) /
|
||||
reader.layoutWidth,
|
||||
);
|
||||
this.movePage(this.totalPages.val * ratio);
|
||||
}, 100);
|
||||
} else {
|
||||
reader.chapterElement.style = '';
|
||||
document.body.classList.remove('page-reader');
|
||||
setTimeout(() => {
|
||||
reader.refresh();
|
||||
window.scrollTo({
|
||||
top:
|
||||
(reader.chapterHeight * (this.page.val + 1)) / this.totalPages.val -
|
||||
reader.layoutHeight,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
window.addEventListener('DOMContentLoaded', async () => {
|
||||
// wait for content is rendered
|
||||
setTimeout(() => {
|
||||
reader.refresh();
|
||||
if (reader.generalSettings.val.pageReader) {
|
||||
pageReader.totalPages.val = parseInt(
|
||||
(reader.chapterWidth + reader.readerSettings.val.padding * 2) /
|
||||
reader.layoutWidth,
|
||||
);
|
||||
pageReader.movePage(
|
||||
parseInt(
|
||||
pageReader.totalPages.val *
|
||||
Math.min(0.99, reader.chapter.progress / 100),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
window.scrollTo({
|
||||
top:
|
||||
(reader.chapterHeight * reader.chapter.progress) / 100 -
|
||||
reader.layoutHeight,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
|
||||
// click handler
|
||||
(function () {
|
||||
const detectTapPosition = (x, y, horizontal) => {
|
||||
if (horizontal) {
|
||||
if (x < 0.33) {
|
||||
return 'left';
|
||||
}
|
||||
if (x > 0.66) {
|
||||
return 'right';
|
||||
}
|
||||
} else {
|
||||
if (y < 0.33) {
|
||||
return 'top';
|
||||
}
|
||||
if (y > 0.66) {
|
||||
return 'bottom';
|
||||
}
|
||||
}
|
||||
return 'center';
|
||||
};
|
||||
document.onclick = e => {
|
||||
const { clientX, clientY } = e;
|
||||
const { x, y } = {
|
||||
x: clientX / reader.layoutWidth,
|
||||
y: clientY / reader.layoutHeight,
|
||||
};
|
||||
if (reader.generalSettings.val.pageReader) {
|
||||
const position = detectTapPosition(x, y, true);
|
||||
if (position === 'left') {
|
||||
pageReader.movePage(pageReader.page.val - 1);
|
||||
return;
|
||||
}
|
||||
if (position === 'right') {
|
||||
pageReader.movePage(pageReader.page.val + 1);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (reader.generalSettings.val.tapToScroll) {
|
||||
const position = detectTapPosition(x, y, false);
|
||||
if (position === 'top') {
|
||||
window.scrollBy({
|
||||
top: -reader.layoutHeight * 0.75,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (position === 'bottom') {
|
||||
window.scrollBy({
|
||||
top: reader.layoutHeight * 0.75,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
reader.post({ type: 'hide' });
|
||||
};
|
||||
})();
|
||||
|
||||
// swipe handler
|
||||
(function () {
|
||||
this.initialX = null;
|
||||
this.initialY = null;
|
||||
|
||||
reader.chapterElement.addEventListener('touchstart', e => {
|
||||
this.initialX = e.changedTouches[0].screenX;
|
||||
this.initialY = e.changedTouches[0].screenY;
|
||||
});
|
||||
|
||||
reader.chapterElement.addEventListener('touchmove', e => {
|
||||
if (reader.generalSettings.val.pageReader) {
|
||||
const diffX =
|
||||
(e.changedTouches[0].screenX - this.initialX) / reader.layoutWidth;
|
||||
reader.chapterElement.style.transform =
|
||||
'translateX(-' + (pageReader.page.val - diffX) * 100 + '%)';
|
||||
}
|
||||
});
|
||||
|
||||
reader.chapterElement.addEventListener('touchend', e => {
|
||||
const diffX = e.changedTouches[0].screenX - this.initialX;
|
||||
const diffY = e.changedTouches[0].screenY - this.initialY;
|
||||
if (reader.generalSettings.val.pageReader) {
|
||||
const diffXPercentage = diffX / reader.layoutWidth;
|
||||
if (diffXPercentage < -0.3) {
|
||||
pageReader.movePage(pageReader.page.val + 1);
|
||||
} else if (diffXPercentage > 0.3) {
|
||||
pageReader.movePage(pageReader.page.val - 1);
|
||||
} else {
|
||||
pageReader.movePage(pageReader.page.val);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (
|
||||
e.target.id?.startsWith('scrollbar') ||
|
||||
e.target.id === 'Image-Modal-img'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
reader.generalSettings.val.swipeGestures &&
|
||||
Math.abs(diffX) > Math.abs(diffY) * 2 &&
|
||||
Math.abs(diffX) > 180
|
||||
) {
|
||||
if (diffX < 0 && this.initialX >= window.innerWidth / 2) {
|
||||
e.preventDefault();
|
||||
reader.post({ type: 'next' });
|
||||
} else if (diffX > 0 && this.initialX <= window.innerWidth / 2) {
|
||||
e.preventDefault();
|
||||
reader.post({ type: 'prev' });
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
// text options
|
||||
(function () {
|
||||
van.derive(() => {
|
||||
let html = reader.rawHTML;
|
||||
if (reader.generalSettings.val.bionicReading) {
|
||||
html = textVide.textVide(reader.rawHTML);
|
||||
}
|
||||
|
||||
if (reader.generalSettings.val.removeExtraParagraphSpacing) {
|
||||
html = html.replace(/<\s*br[^>]*>/gi, '\n').replace(/\n{2,}/g, '\n\n');
|
||||
}
|
||||
reader.chapterElement.innerHTML = html;
|
||||
});
|
||||
})();
|
||||
@@ -1,10 +0,0 @@
|
||||
const select = key => {
|
||||
return document.querySelector(key);
|
||||
};
|
||||
const chapter = select('chapter');
|
||||
const getInt = key => {
|
||||
return parseInt(chapter?.getAttribute('data-' + key), 10);
|
||||
};
|
||||
const setAttr = (key, val) => {
|
||||
chapter?.setAttribute(key, val);
|
||||
};
|
||||
@@ -1,65 +0,0 @@
|
||||
const bounds = document.querySelector('html').getBoundingClientRect();
|
||||
var page = 0;
|
||||
var pages = 0;
|
||||
|
||||
function setPages(newPages) {
|
||||
if (newPages) {
|
||||
pages = newPages;
|
||||
return;
|
||||
}
|
||||
const textWidth = chapter.scrollWidth;
|
||||
const layoutWidth = parseInt(
|
||||
document.querySelector('html').getBoundingClientRect().width,
|
||||
10,
|
||||
);
|
||||
pages = Math.round(textWidth / layoutWidth) - 1;
|
||||
}
|
||||
function setPage(newPage) {
|
||||
page = newPage ?? 0;
|
||||
}
|
||||
function getPages() {
|
||||
return pages;
|
||||
}
|
||||
function getPage() {
|
||||
return page;
|
||||
}
|
||||
function tapChapter(event) {
|
||||
const { clientX, clientY } = event;
|
||||
const { x, y } = { x: clientX / bounds.width, y: clientY / bounds.height };
|
||||
|
||||
if (y < 0.2) {
|
||||
movePage('prev');
|
||||
} else if (y > 0.8) {
|
||||
movePage('next');
|
||||
} else if (x < 0.33) {
|
||||
movePage('prev');
|
||||
} else if (x > 0.66) {
|
||||
movePage('next');
|
||||
} else {
|
||||
movePage();
|
||||
}
|
||||
}
|
||||
|
||||
function movePage(panel) {
|
||||
switch (panel) {
|
||||
case 'next':
|
||||
if (page === pages) {
|
||||
reader.post({ type: 'next' });
|
||||
} else {
|
||||
page++;
|
||||
}
|
||||
break;
|
||||
case 'prev':
|
||||
if (page === 0) {
|
||||
reader.post({ type: 'prev' });
|
||||
} else {
|
||||
page--;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
reader.post({ type: 'hide' });
|
||||
break;
|
||||
}
|
||||
|
||||
chapter.style.transform = 'translate(-' + page * 100 + '%)';
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
const volumnIcon =
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px" fill="#e8eaed"><path d="M552-152v-75q86-23 139-93.5T744-480q0-89-53.5-158.5T552-734v-75q116 25 190 117t74 211q0 119-73.5 211.5T552-152ZM144-385v-192h144l192-192v576L288-385H144Zm408 55v-302q45 20 70.5 61t25.5 90q0 49-25.5 89.5T552-330Z"/></svg>';
|
||||
const pauseIcon =
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px" fill="#e8eaed"><path d="M564-228v-504h168v504H564Zm-336 0v-504h168v504H228Z"/></svg>';
|
||||
const resumeIcon =
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px" fill="#e8eaed"><path d="M240-240v-480h72v480h-72Zm144 0 384-240-384-240v480Z"/></svg>';
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import { ChapterInfo, NovelInfo } from '@database/types';
|
||||
import {
|
||||
ChapterGeneralSettings,
|
||||
ChapterReaderSettings,
|
||||
} from '@hooks/persisted/useSettings';
|
||||
import { State } from './van';
|
||||
|
||||
export interface Reader {
|
||||
// element
|
||||
chapterElement: HTMLElement;
|
||||
viewport: HTMLMetaElement;
|
||||
selection: Selection;
|
||||
|
||||
// state
|
||||
hidden: State<boolean>;
|
||||
generalSettings: State<ChapterGeneralSettings>;
|
||||
readerSettings: State<ChapterReaderSettings>;
|
||||
batteryLevel: State<number>;
|
||||
|
||||
novel: NovelInfo;
|
||||
chapter: ChapterInfo;
|
||||
nextChapter?: ChapterInfo;
|
||||
autoSaveInterval: number;
|
||||
rawHTML: string;
|
||||
strings: {
|
||||
finished: string;
|
||||
nextChapter: string;
|
||||
noNextChapter: string;
|
||||
};
|
||||
|
||||
//layout props
|
||||
paddingTop: number;
|
||||
layoutHeight: number;
|
||||
layoutWidth: number;
|
||||
chapterHeight: number;
|
||||
chapterWidth: number;
|
||||
|
||||
post: (obj: Record<string, any>) => void;
|
||||
refresh: () => void;
|
||||
}
|
||||
|
||||
interface PageReader {
|
||||
page: State<number>;
|
||||
totalPages: State<number>;
|
||||
movePage: (page: number) => void;
|
||||
}
|
||||
|
||||
interface TTS {
|
||||
started: boolean;
|
||||
reading: boolean;
|
||||
start: (element?: HTMLElement) => void;
|
||||
resume: () => void;
|
||||
stop: () => void;
|
||||
pause: () => void;
|
||||
readable: (element?: HTMLElement) => void;
|
||||
}
|
||||
|
||||
declare global {
|
||||
const reader: Reader;
|
||||
const tts: TTS;
|
||||
const pageReader: PageReader;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,28 +0,0 @@
|
||||
function setup(progress, customJS) {
|
||||
document
|
||||
.querySelector('.chapterCtn')
|
||||
.addEventListener('click', e => onTap(e));
|
||||
|
||||
document.addEventListener('DOMContentLoaded', startup);
|
||||
|
||||
async function startup() {
|
||||
customJS;
|
||||
//? scroll to saved position
|
||||
if (reader.pageReader) {
|
||||
setTimeout(() => {
|
||||
setPages();
|
||||
scrollHandler.onDOMCreation(progress);
|
||||
}, 100);
|
||||
} else {
|
||||
scrollHandler.onDOMCreation(progress);
|
||||
}
|
||||
}
|
||||
function onTap(event) {
|
||||
if (!contextMenu.isOpened) {
|
||||
!reader.pageReader ? reader.post({ type: 'hide' }) : tapChapter(event);
|
||||
} else {
|
||||
contextMenu.isOpened = false;
|
||||
contextMenu.contextMenu.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
-69
@@ -1,69 +0,0 @@
|
||||
import {
|
||||
ChapterGeneralSettings,
|
||||
ChapterReaderSettings,
|
||||
} from '@hooks/persisted/useSettings';
|
||||
import 'typescript';
|
||||
import 'typescript/lib/lib.dom';
|
||||
|
||||
interface Reader {
|
||||
selection: Selection;
|
||||
viewport: HTMLMetaElement;
|
||||
footerWrapper: HTMLDivElement;
|
||||
percentage: HTMLDivElement;
|
||||
battery: HTMLDivElement;
|
||||
time: HTMLDivElement;
|
||||
paddingTop: number;
|
||||
chapter: HTMLElement;
|
||||
chapterHeight: number;
|
||||
layoutHeight: number;
|
||||
pluginId: string;
|
||||
novelId: string;
|
||||
chapterId: string;
|
||||
saveProgressInterval: NodeJS.Timeout;
|
||||
timeInterval: NodeJS.Timeout;
|
||||
refresh: () => void;
|
||||
post: (message: { type: string; data?: any }) => void;
|
||||
updateReaderSettings: (settings: ChapterReaderSettings) => void;
|
||||
updateGeneralSettings: (settings: ChapterGeneralSettings) => void;
|
||||
updateBatteryLevel: (level: number) => void;
|
||||
}
|
||||
|
||||
interface TextToSpeech {
|
||||
reader: Reader;
|
||||
leaf: HTMLUnknownElement;
|
||||
reading: boolean;
|
||||
readableTags: string[];
|
||||
start: () => void;
|
||||
startHere: () => void;
|
||||
resume: () => void;
|
||||
pause: () => void;
|
||||
stop: () => void;
|
||||
started: () => boolean;
|
||||
}
|
||||
|
||||
export enum ContextMenuItem {
|
||||
START_READING = 'START_READING',
|
||||
START_HERE = 'START_HERE',
|
||||
COPY = 'COPY',
|
||||
SELECT_ALL = 'SELECT_ALL',
|
||||
}
|
||||
interface ContextMenu {
|
||||
reader: Reader;
|
||||
contextMenu: HTMLUListElement;
|
||||
items: Record<ContextMenuItem, HTMLLIElement>;
|
||||
renderItem: (data: {
|
||||
name: string;
|
||||
icon: string;
|
||||
action: () => void;
|
||||
}) => HTMLLIElement;
|
||||
renderMenu: (items: HTMLLIElement[]) => void;
|
||||
closeMenu: (menu: HTMLUListElement) => void;
|
||||
init: () => void;
|
||||
}
|
||||
|
||||
interface ImageModal {
|
||||
reader: Reader;
|
||||
showing: boolean;
|
||||
show: (image: HTMLImageElement) => void;
|
||||
hide: () => void;
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
import 'typescript';
|
||||
import 'typescript/lib/lib.dom';
|
||||
export interface State<T> {
|
||||
val: T;
|
||||
readonly oldVal: T;
|
||||
readonly rawVal: T;
|
||||
}
|
||||
|
||||
// Defining readonly view of State<T> for covariance.
|
||||
// Basically we want StateView<string> to implement StateView<string | number>
|
||||
export type StateView<T> = Readonly<State<T>>;
|
||||
|
||||
export type Val<T> = State<T> | T;
|
||||
|
||||
export type Primitive = string | number | boolean | bigint;
|
||||
|
||||
export type PropValue = Primitive | ((e: any) => void) | null;
|
||||
|
||||
export type PropValueOrDerived =
|
||||
| PropValue
|
||||
| StateView<PropValue>
|
||||
| (() => PropValue);
|
||||
|
||||
export type Props = Record<string, PropValueOrDerived> & {
|
||||
class?: PropValueOrDerived;
|
||||
};
|
||||
|
||||
export type PropsWithKnownKeys<ElementType> = Partial<{
|
||||
[K in keyof ElementType]: PropValueOrDerived;
|
||||
}>;
|
||||
|
||||
export type ValidChildDomValue = Primitive | Node | null | undefined;
|
||||
|
||||
export type BindingFunc =
|
||||
| ((dom?: Node) => ValidChildDomValue)
|
||||
| ((dom?: Element) => Element);
|
||||
|
||||
export type ChildDom =
|
||||
| ValidChildDomValue
|
||||
| StateView<Primitive | null | undefined>
|
||||
| BindingFunc
|
||||
| readonly ChildDom[];
|
||||
|
||||
export type TagFunc<Result> = (
|
||||
first?: (Props & PropsWithKnownKeys<Result>) | ChildDom,
|
||||
...rest: readonly ChildDom[]
|
||||
) => Result;
|
||||
|
||||
type Tags = Readonly<Record<string, TagFunc<Element>>> & {
|
||||
[K in keyof HTMLElementTagNameMap]: TagFunc<HTMLElementTagNameMap[K]>;
|
||||
};
|
||||
|
||||
declare function state<T>(): State<T>;
|
||||
declare function state<T>(initVal: T): State<T>;
|
||||
|
||||
export interface Van {
|
||||
readonly state: typeof state;
|
||||
readonly derive: <T>(f: () => T) => State<T>;
|
||||
readonly add: (dom: Element, ...children: readonly ChildDom[]) => Element;
|
||||
readonly tags: Tags &
|
||||
((namespaceURI: string) => Readonly<Record<string, TagFunc<Element>>>);
|
||||
readonly hydrate: <T extends Node>(
|
||||
dom: T,
|
||||
f: (dom: T) => T | null | undefined,
|
||||
) => T;
|
||||
}
|
||||
|
||||
declare global {
|
||||
const van: Van;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{let e,t,r,o,n,l,s,f,i,h,w,a,d,u,_,c,S,g,y,b,m,v,j,x,O; s = Object.getPrototypeOf,i = {},h = s(f = {isConnected:1}),w = s(s),a = (e,t,r,o)=>(e ?? (setTimeout(r,o),new Set())).add(t),d = (e,t,o)=>{let n = r; r = t; try {return e(o);} catch (e){return console.error(e),o;} finally {r = n;}},u = e=>e.filter(e=>e.t?.isConnected),_ = e=>n = a(n,e,()=>{for (let e of n){e.o = u(e.o),e.l = u(e.l);}n = l;},1e3),c = {get val(){return r?.i?.add(this),this.rawVal;},get oldVal(){return r?.i?.add(this),this.h;},set val(o){r?.u?.add(this),o !== this.rawVal && (this.rawVal = o,this.o.length + this.l.length ? (t?.add(this),e = a(e,this,x)) : this.h = o);}},S = e=>({__proto__:c,rawVal:e,h:e,o:[],l:[]}),g = (e,t)=>{let r = {i:new Set(),u:new Set()},n = {f:e},l = o; o = []; let s = d(e,r,t); s = (s ?? document).nodeType ? s : new Text(s); for (let e of r.i){r.u.has(e) || (_(e),e.o.push(n));} for (let e of o){e.t = s;} return o = l,n.t = s;},y = (e,t = S(),r)=>{let n = {i:new Set(),u:new Set()},l = {f:e,s:t}; l.t = r ?? o?.push(l) ?? f,t.val = d(e,n,t.rawVal); for (let e of n.i){n.u.has(e) || (_(e),e.l.push(l));} return t;},b = (e,...t)=>{for (let r of t.flat(1 / 0)){let t = s(r ?? 0),o = t === c ? g(()=>r.val) : t === w ? g(r) : r; o != l && e.append(o);} return e;},m = (e,t,...r)=>{let [o,...n] = s(r[0] ?? 0) === h ? r : [{},...r],f = e ? document.createElementNS(e,t) : document.createElement(t); for (let [e,r] of Object.entries(o)){let o = t=>t ? Object.getOwnPropertyDescriptor(t,e) ?? o(s(t)) : l,n = t + ',' + e,h = i[n] ?? (i[n] = o(s(f))?.set ?? 0),a = e.startsWith('on') ? (t,r)=>{let o = e.slice(2); f.removeEventListener(o,r),f.addEventListener(o,t);} : h ? h.bind(f) : f.setAttribute.bind(f,e),d = s(r ?? 0); e.startsWith('on') || d === w && (r = y(r),d = c),d === c ? g(()=>(a(r.val,r.h),f)) : a(r);} return b(f,n);},v = e=>({get:(t,r)=>m.bind(l,e,r)}),j = (e,t)=>t ? t !== e && e.replaceWith(t) : e.remove(),x = ()=>{let r = 0,o = [...e].filter(e=>e.rawVal !== e.h); do {t = new Set(); for (let e of new Set(o.flatMap(e=>e.l = u(e.l)))){y(e.f,e.s,e.t),e.t = l;}} while (++r < 100 && (o = [...t]).length); let n = [...e].filter(e=>e.rawVal !== e.h); e = l; for (let e of new Set(n.flatMap(e=>e.o = u(e.o)))){j(e.t,g(e.f,e.t)),e.t = l;} for (let e of n){e.h = e.rawVal;}},O = {tags:new Proxy(e=>new Proxy(m,v(e)),v()),hydrate:(e,t)=>j(e,g(t,e)),add:b,state:S,derive:y},window.van = O;}
|
||||
Generated
+3
-2
@@ -56,7 +56,7 @@
|
||||
"react-native-shimmer-placeholder": "^2.0.9",
|
||||
"react-native-tab-view": "^3.5.2",
|
||||
"react-native-vector-icons": "^9.0.0",
|
||||
"react-native-webview": "^13.2.2",
|
||||
"react-native-webview": "^13.10.5",
|
||||
"react-native-zip-archive": "^7.0.0",
|
||||
"sanitize-html": "^2.13.0",
|
||||
"urlencode": "^2.0.0"
|
||||
@@ -15096,7 +15096,8 @@
|
||||
},
|
||||
"node_modules/react-native-webview": {
|
||||
"version": "13.10.5",
|
||||
"license": "MIT",
|
||||
"resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-13.10.5.tgz",
|
||||
"integrity": "sha512-ZfQJHyifuZop8cQB8HYpL9dxss9Cw9sOlMU+/yarfG/ugoL0rky/0tq9QmEydFMXjDSplSUb22OrRdFlqfXe0g==",
|
||||
"dependencies": {
|
||||
"escape-string-regexp": "2.0.0",
|
||||
"invariant": "2.2.4"
|
||||
|
||||
+1
-1
@@ -63,7 +63,7 @@
|
||||
"react-native-shimmer-placeholder": "^2.0.9",
|
||||
"react-native-tab-view": "^3.5.2",
|
||||
"react-native-vector-icons": "^9.0.0",
|
||||
"react-native-webview": "^13.2.2",
|
||||
"react-native-webview": "^13.10.5",
|
||||
"react-native-zip-archive": "^7.0.0",
|
||||
"sanitize-html": "^2.13.0",
|
||||
"urlencode": "^2.0.0"
|
||||
|
||||
@@ -85,6 +85,8 @@ export interface ChapterGeneralSettings {
|
||||
verticalSeekbar: boolean;
|
||||
removeExtraParagraphSpacing: boolean;
|
||||
bionicReading: boolean;
|
||||
tapToScroll: boolean;
|
||||
TTSEnable: boolean;
|
||||
}
|
||||
|
||||
export interface ReaderTheme {
|
||||
@@ -174,6 +176,8 @@ export const initialChapterGeneralSettings: ChapterGeneralSettings = {
|
||||
verticalSeekbar: true,
|
||||
removeExtraParagraphSpacing: false,
|
||||
bionicReading: false,
|
||||
tapToScroll: false,
|
||||
TTSEnable: true,
|
||||
};
|
||||
|
||||
export const initialChapterReaderSettings: ChapterReaderSettings = {
|
||||
|
||||
@@ -16,7 +16,7 @@ const ChapterLoadingScreen = () => {
|
||||
return (
|
||||
<View style={{ backgroundColor }}>
|
||||
<SkeletonLines
|
||||
containerMargin={padding + '%'}
|
||||
containerMargin={padding}
|
||||
containerHeight={'100%'}
|
||||
containerWidth={'100%'}
|
||||
color={
|
||||
|
||||
@@ -121,7 +121,6 @@ export const ChapterContent = ({
|
||||
html={chapterText}
|
||||
nextChapter={nextChapter}
|
||||
webViewRef={webViewRef}
|
||||
pageReader={pageReader}
|
||||
saveProgress={saveProgress}
|
||||
onPress={hideHeader}
|
||||
navigateChapter={navigateChapter}
|
||||
|
||||
@@ -31,9 +31,11 @@ const ReaderTab: React.FC = () => {
|
||||
<ReaderValueChange
|
||||
label={getString('readerScreen.bottomSheet.padding')}
|
||||
valueKey="padding"
|
||||
valueChange={0.5}
|
||||
min={1}
|
||||
max={10}
|
||||
valueChange={2}
|
||||
min={0}
|
||||
max={50}
|
||||
decimals={0}
|
||||
unit="px"
|
||||
/>
|
||||
<ReaderFontPicker />
|
||||
</View>
|
||||
@@ -54,6 +56,7 @@ const GeneralTab: React.FC = () => {
|
||||
pageReader = false,
|
||||
removeExtraParagraphSpacing,
|
||||
bionicReading,
|
||||
tapToScroll = false,
|
||||
setChapterGeneralSettings,
|
||||
} = useChapterGeneralSettings();
|
||||
|
||||
@@ -139,6 +142,12 @@ const GeneralTab: React.FC = () => {
|
||||
value={bionicReading}
|
||||
theme={theme}
|
||||
/>
|
||||
<ReaderSheetPreferenceItem
|
||||
label={getString('readerScreen.bottomSheet.tapToScroll')}
|
||||
onPress={() => setChapterGeneralSettings({ tapToScroll: !tapToScroll })}
|
||||
value={tapToScroll}
|
||||
theme={theme}
|
||||
/>
|
||||
<ReaderSheetPreferenceItem
|
||||
label={getString('readerScreen.bottomSheet.keepScreenOn')}
|
||||
onPress={() =>
|
||||
|
||||
@@ -17,6 +17,7 @@ interface ReaderValueChangeProps {
|
||||
decimals?: number;
|
||||
min?: number;
|
||||
max?: number;
|
||||
unit?: string;
|
||||
}
|
||||
|
||||
const ReaderValueChange: React.FC<ReaderValueChangeProps> = ({
|
||||
@@ -27,6 +28,7 @@ const ReaderValueChange: React.FC<ReaderValueChangeProps> = ({
|
||||
decimals = 1,
|
||||
min = 1.3,
|
||||
max = 2,
|
||||
unit = '%',
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const { setChapterReaderSettings, ...settings } = useChapterReaderSettings();
|
||||
@@ -50,7 +52,7 @@ const ReaderValueChange: React.FC<ReaderValueChangeProps> = ({
|
||||
theme={theme}
|
||||
/>
|
||||
<Text style={[styles.value, { color: theme.onSurface }]}>
|
||||
{`${((settings[valueKey] * 10) / 10).toFixed(decimals)}%`}
|
||||
{`${((settings[valueKey] * 10) / 10).toFixed(decimals)}${unit}`}
|
||||
</Text>
|
||||
<IconButtonV2
|
||||
name="plus"
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import {
|
||||
Dimensions,
|
||||
NativeEventEmitter,
|
||||
NativeModules,
|
||||
StatusBar,
|
||||
} from 'react-native';
|
||||
import { NativeEventEmitter, NativeModules, StatusBar } from 'react-native';
|
||||
import WebView from 'react-native-webview';
|
||||
import color from 'color';
|
||||
|
||||
@@ -24,8 +19,6 @@ import {
|
||||
} from '@hooks/persisted/useSettings';
|
||||
import { getBatteryLevelSync } from 'react-native-device-info';
|
||||
import * as Speech from 'expo-speech';
|
||||
import * as Clipboard from 'expo-clipboard';
|
||||
import { showToast } from '@utils/showToast';
|
||||
import { PLUGIN_STORAGE } from '@utils/Storages';
|
||||
import { useChapterContext } from '../ChapterContext';
|
||||
|
||||
@@ -36,12 +29,11 @@ type WebViewPostEvent = {
|
||||
|
||||
type WebViewReaderProps = {
|
||||
html: string;
|
||||
nextChapter: ChapterInfo;
|
||||
nextChapter?: ChapterInfo;
|
||||
webViewRef: React.RefObject<WebView>;
|
||||
saveProgress(percentage: number): void;
|
||||
onPress(): void;
|
||||
navigateChapter(position: 'NEXT' | 'PREV'): void;
|
||||
pageReader: boolean;
|
||||
};
|
||||
|
||||
const onLogMessage = (payload: { nativeEvent: { data: string } }) => {
|
||||
@@ -56,23 +48,22 @@ const onLogMessage = (payload: { nativeEvent: { data: string } }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const WebViewReader: React.FC<WebViewReaderProps> = props => {
|
||||
const {
|
||||
html,
|
||||
nextChapter,
|
||||
webViewRef,
|
||||
pageReader,
|
||||
saveProgress,
|
||||
onPress,
|
||||
navigateChapter,
|
||||
} = props;
|
||||
const { RNDeviceInfo } = NativeModules;
|
||||
const deviceInfoEmitter = new NativeEventEmitter(RNDeviceInfo);
|
||||
|
||||
const assetsUriPrefix = __DEV__
|
||||
? 'http://localhost:8081/assets'
|
||||
: 'file:///android_asset';
|
||||
|
||||
const WebViewReader: React.FC<WebViewReaderProps> = ({
|
||||
html,
|
||||
webViewRef,
|
||||
nextChapter,
|
||||
saveProgress,
|
||||
onPress,
|
||||
navigateChapter,
|
||||
}) => {
|
||||
const { novel, chapter } = useChapterContext();
|
||||
const assetsUriPrefix = useMemo(
|
||||
() => (__DEV__ ? 'http://localhost:8081/assets' : 'file:///android_asset'),
|
||||
[],
|
||||
);
|
||||
const { RNDeviceInfo } = NativeModules;
|
||||
const deviceInfoEmitter = new NativeEventEmitter(RNDeviceInfo);
|
||||
const theme = useTheme();
|
||||
const readerSettings = useMemo(
|
||||
() =>
|
||||
@@ -80,20 +71,13 @@ const WebViewReader: React.FC<WebViewReaderProps> = props => {
|
||||
initialChapterReaderSettings,
|
||||
[],
|
||||
);
|
||||
const {
|
||||
showScrollPercentage,
|
||||
swipeGestures,
|
||||
showBatteryAndTime,
|
||||
verticalSeekbar,
|
||||
bionicReading,
|
||||
} = useMemo(
|
||||
const chapterGeneralSettings = useMemo(
|
||||
() =>
|
||||
getMMKVObject<ChapterGeneralSettings>(CHAPTER_GENERAL_SETTINGS) ||
|
||||
initialChapterGeneralSettings,
|
||||
[],
|
||||
);
|
||||
const batteryLevel = useMemo(getBatteryLevelSync, []);
|
||||
const layoutHeight = Dimensions.get('window').height;
|
||||
const plugin = getPlugin(novel?.pluginId);
|
||||
const pluginCustomJS = `file://${PLUGIN_STORAGE}/${plugin?.id}/custom.js`;
|
||||
const pluginCustomCSS = `file://${PLUGIN_STORAGE}/${plugin?.id}/custom.css`;
|
||||
@@ -103,16 +87,16 @@ const WebViewReader: React.FC<WebViewReaderProps> = props => {
|
||||
switch (key) {
|
||||
case CHAPTER_READER_SETTINGS:
|
||||
webViewRef.current?.injectJavaScript(
|
||||
`reader.updateReaderSettings(${MMKVStorage.getString(
|
||||
`reader.readerSettings.val = ${MMKVStorage.getString(
|
||||
CHAPTER_READER_SETTINGS,
|
||||
)})`,
|
||||
)}`,
|
||||
);
|
||||
break;
|
||||
case CHAPTER_GENERAL_SETTINGS:
|
||||
webViewRef.current?.injectJavaScript(
|
||||
`reader.updateGeneralSettings(${MMKVStorage.getString(
|
||||
`reader.generalSettings.val = ${MMKVStorage.getString(
|
||||
CHAPTER_GENERAL_SETTINGS,
|
||||
)})`,
|
||||
)}`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
@@ -122,7 +106,7 @@ const WebViewReader: React.FC<WebViewReaderProps> = props => {
|
||||
'RNDeviceInfo_batteryLevelDidChange',
|
||||
(level: number) => {
|
||||
webViewRef.current?.injectJavaScript(
|
||||
`reader.updateBatteryLevel(${level})`,
|
||||
`reader.batteryLevel.val = ${level}`,
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -132,21 +116,9 @@ const WebViewReader: React.FC<WebViewReaderProps> = props => {
|
||||
mmkvListener.remove();
|
||||
};
|
||||
}, []);
|
||||
const debugging = `
|
||||
// Debug
|
||||
console = new Object();
|
||||
console.log = function(log) {
|
||||
reader.post({"type": "console", "msg": log});
|
||||
};
|
||||
console.debug = console.log;
|
||||
console.info = console.log;
|
||||
console.warn = console.log;
|
||||
console.error = console.log;
|
||||
`;
|
||||
|
||||
return (
|
||||
<WebView
|
||||
injectedJavaScript={debugging}
|
||||
ref={webViewRef}
|
||||
style={{ backgroundColor: readerSettings.theme }}
|
||||
allowFileAccess={true}
|
||||
@@ -189,156 +161,97 @@ const WebViewReader: React.FC<WebViewReaderProps> = props => {
|
||||
case 'stop-speak':
|
||||
Speech.stop();
|
||||
break;
|
||||
case 'copy':
|
||||
if (event.data && typeof event.data === 'string') {
|
||||
Clipboard.setStringAsync(event.data).then(() => {
|
||||
showToast(getString('common.copiedToClipboard', { name: '' }));
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}}
|
||||
source={{
|
||||
baseUrl: plugin?.site,
|
||||
baseUrl: !chapter.isDownloaded ? plugin?.site : undefined,
|
||||
headers: plugin?.imageRequestInit?.headers,
|
||||
method: plugin?.imageRequestInit?.method,
|
||||
body: plugin?.imageRequestInit?.body,
|
||||
html: `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
|
||||
<link rel="stylesheet" href="${assetsUriPrefix}/css/index.css">
|
||||
<style>
|
||||
:root {
|
||||
--StatusBar-currentHeight: ${StatusBar.currentHeight};
|
||||
--readerSettings-theme: ${readerSettings.theme};
|
||||
--readerSettings-padding: ${readerSettings.padding}%;
|
||||
--readerSettings-textSize: ${readerSettings.textSize}px;
|
||||
--readerSettings-textColor: ${readerSettings.textColor};
|
||||
--readerSettings-textAlign: ${readerSettings.textAlign};
|
||||
--readerSettings-lineHeight: ${readerSettings.lineHeight};
|
||||
--readerSettings-fontFamily: ${readerSettings.fontFamily};
|
||||
--theme-primary: ${theme.primary};
|
||||
--theme-onPrimary: ${theme.onPrimary};
|
||||
--theme-secondary: ${theme.secondary};
|
||||
--theme-tertiary: ${theme.tertiary};
|
||||
--theme-onTertiary: ${theme.onTertiary};
|
||||
--theme-onSecondary: ${theme.onSecondary};
|
||||
--theme-surface: ${theme.surface};
|
||||
--theme-surface-0-9: ${color(theme.surface)
|
||||
.alpha(0.9)
|
||||
.toString()};
|
||||
--theme-onSurface: ${theme.onSurface};
|
||||
--theme-surfaceVariant: ${theme.surfaceVariant};
|
||||
--theme-onSurfaceVariant: ${theme.onSurfaceVariant};
|
||||
--theme-outline: ${theme.outline};
|
||||
--theme-rippleColor: ${theme.rippleColor};
|
||||
--chapterCtn-height: ${layoutHeight - 140};
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: ${readerSettings.fontFamily};
|
||||
src: url("file:///android_asset/fonts/${
|
||||
readerSettings.fontFamily
|
||||
}.ttf");
|
||||
}
|
||||
</style>
|
||||
${
|
||||
pageReader
|
||||
? `
|
||||
<link rel="stylesheet" href="${assetsUriPrefix}/css/horizontal.css">
|
||||
`
|
||||
: ''
|
||||
}
|
||||
<link rel="stylesheet" href="${pluginCustomCSS}">
|
||||
<style>${readerSettings.customCSS}</style>
|
||||
<script>
|
||||
var initSettings = {
|
||||
showScrollPercentage: ${showScrollPercentage},
|
||||
swipeGestures: ${swipeGestures},
|
||||
showBatteryAndTime: ${showBatteryAndTime},
|
||||
verticalSeekbar: ${verticalSeekbar},
|
||||
bionicReading: ${bionicReading},
|
||||
}
|
||||
var batteryLevel = ${batteryLevel};
|
||||
var autoSaveInterval = 2222;
|
||||
var { NOVEL, CHAPTER } = ${JSON.stringify({
|
||||
NOVEL: novel,
|
||||
CHAPTER: chapter,
|
||||
})}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="chapterCtn">
|
||||
<chapter
|
||||
data-page-reader='${pageReader}'
|
||||
data-plugin-id='${novel.pluginId}'
|
||||
data-novel-id='${chapter.novelId}'
|
||||
data-chapter-id='${chapter.id}'
|
||||
>
|
||||
${html}
|
||||
</chapter>
|
||||
<div class="hidden" id="ToolWrapper">
|
||||
<div id="TTS-Controller"></div>
|
||||
<div id="ScrollBar"></div>
|
||||
</div>
|
||||
<div id="Image-Modal">
|
||||
<img id="Image-Modal-img">
|
||||
</div>
|
||||
<div id="reader-footer-wrapper">
|
||||
<div id="reader-footer">
|
||||
<div id="reader-battery" class="reader-footer-item"></div>
|
||||
<div id="reader-percentage" class="reader-footer-item"></div>
|
||||
<div id="reader-time" class="reader-footer-item"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
${
|
||||
!pageReader
|
||||
? `
|
||||
<div class="infoText">
|
||||
${getString(
|
||||
'readerScreen.finished',
|
||||
)}: ${chapter.name.trim()}
|
||||
</div>
|
||||
${
|
||||
nextChapter
|
||||
? `<button class="nextButton" onclick="reader.post({type:'next'})">
|
||||
${getString('readerScreen.nextChapter', {
|
||||
name: nextChapter.name,
|
||||
})}
|
||||
</button>`
|
||||
: `<div class="infoText">
|
||||
${getString('readerScreen.noNextChapter')}
|
||||
</div>`
|
||||
}`
|
||||
: ''
|
||||
}
|
||||
</body>
|
||||
<script>
|
||||
// Debug
|
||||
console = new Object();
|
||||
console.log = function(log) {
|
||||
reader.post({"type": "console", "msg": log});
|
||||
};
|
||||
console.debug = console.log;
|
||||
console.info = console.log;
|
||||
console.warn = console.log;
|
||||
console.error = console.log;
|
||||
</script>
|
||||
<script src="${assetsUriPrefix}/js/text-vibe.js"></script>
|
||||
<script src="${assetsUriPrefix}/js/default.js"></script>
|
||||
<script src="${assetsUriPrefix}/js/horizontalScroll.js"></script>
|
||||
<script src="${assetsUriPrefix}/js/index.js"></script>
|
||||
<script src="${assetsUriPrefix}/js/setup.js"></script>
|
||||
<script src="${assetsUriPrefix}/js/horizontalScroll.js"></script>
|
||||
<script src="${pluginCustomJS}"></script>
|
||||
<script>
|
||||
setup(${chapter.progress},${readerSettings.customJS})
|
||||
</script>
|
||||
</html>
|
||||
`,
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
|
||||
<link rel="stylesheet" href="${assetsUriPrefix}/css/index.css">
|
||||
<style>
|
||||
:root {
|
||||
--StatusBar-currentHeight: ${StatusBar.currentHeight}px;
|
||||
--readerSettings-theme: ${readerSettings.theme};
|
||||
--readerSettings-padding: ${readerSettings.padding}px;
|
||||
--readerSettings-textSize: ${readerSettings.textSize}px;
|
||||
--readerSettings-textColor: ${readerSettings.textColor};
|
||||
--readerSettings-textAlign: ${readerSettings.textAlign};
|
||||
--readerSettings-lineHeight: ${readerSettings.lineHeight};
|
||||
--readerSettings-fontFamily: ${readerSettings.fontFamily};
|
||||
--theme-primary: ${theme.primary};
|
||||
--theme-onPrimary: ${theme.onPrimary};
|
||||
--theme-secondary: ${theme.secondary};
|
||||
--theme-tertiary: ${theme.tertiary};
|
||||
--theme-onTertiary: ${theme.onTertiary};
|
||||
--theme-onSecondary: ${theme.onSecondary};
|
||||
--theme-surface: ${theme.surface};
|
||||
--theme-surface-0-9: ${color(theme.surface)
|
||||
.alpha(0.9)
|
||||
.toString()};
|
||||
--theme-onSurface: ${theme.onSurface};
|
||||
--theme-surfaceVariant: ${theme.surfaceVariant};
|
||||
--theme-onSurfaceVariant: ${theme.onSurfaceVariant};
|
||||
--theme-outline: ${theme.outline};
|
||||
--theme-rippleColor: ${theme.rippleColor};
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: ${readerSettings.fontFamily};
|
||||
src: url("file:///android_asset/fonts/${
|
||||
readerSettings.fontFamily
|
||||
}.ttf");
|
||||
}
|
||||
</style>
|
||||
|
||||
<link rel="stylesheet" href="${pluginCustomCSS}">
|
||||
<style>${readerSettings.customCSS}</style>
|
||||
</head>
|
||||
<body class="${
|
||||
chapterGeneralSettings.pageReader ? 'page-reader' : ''
|
||||
}">
|
||||
<div id="LNReader-chapter">
|
||||
${html}
|
||||
</div>
|
||||
<div id="reader-ui"></div>
|
||||
</body>
|
||||
<script>
|
||||
var initialReaderConfig = ${JSON.stringify({
|
||||
readerSettings,
|
||||
chapterGeneralSettings,
|
||||
novel,
|
||||
chapter,
|
||||
nextChapter,
|
||||
batteryLevel,
|
||||
autoSaveInterval: 2222,
|
||||
DEBUG: __DEV__,
|
||||
strings: {
|
||||
finished: `${getString(
|
||||
'readerScreen.finished',
|
||||
)}: ${chapter.name.trim()}`,
|
||||
nextChapter: getString('readerScreen.nextChapter', {
|
||||
name: nextChapter?.name,
|
||||
}),
|
||||
noNextChapter: getString('readerScreen.noNextChapter'),
|
||||
},
|
||||
})}
|
||||
</script>
|
||||
<script src="${assetsUriPrefix}/js/icons.js"></script>
|
||||
<script src="${assetsUriPrefix}/js/van.js"></script>
|
||||
<script src="${assetsUriPrefix}/js/text-vibe.js"></script>
|
||||
<script src="${assetsUriPrefix}/js/core.js"></script>
|
||||
<script src="${assetsUriPrefix}/js/index.js"></script>
|
||||
<script src="${pluginCustomJS}"></script>
|
||||
<script>
|
||||
${readerSettings.customJS}
|
||||
</script>
|
||||
</html>
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -40,13 +40,8 @@ export default function useChapter(webViewRef: RefObject<WebView>) {
|
||||
const [[nextChapter, prevChapter], setAdjacentChapter] = useState<
|
||||
ChapterInfo[]
|
||||
>([]);
|
||||
const {
|
||||
autoScroll,
|
||||
autoScrollInterval,
|
||||
autoScrollOffset,
|
||||
useVolumeButtons,
|
||||
removeExtraParagraphSpacing,
|
||||
} = useChapterGeneralSettings();
|
||||
const { autoScroll, autoScrollInterval, autoScrollOffset, useVolumeButtons } =
|
||||
useChapterGeneralSettings();
|
||||
const { incognitoMode } = useLibrarySettings();
|
||||
const [error, setError] = useState<string>();
|
||||
const { tracker } = useTracker();
|
||||
@@ -102,11 +97,7 @@ export default function useChapter(webViewRef: RefObject<WebView>) {
|
||||
})
|
||||
.catch(e => setError(e.message));
|
||||
}
|
||||
setChapterText(
|
||||
sanitizeChapterText(text, {
|
||||
removeExtraParagraphSpacing,
|
||||
}),
|
||||
);
|
||||
setChapterText(sanitizeChapterText(text));
|
||||
const [nextChap, prevChap] = await Promise.all([
|
||||
getNextChapter(chapter.novelId, chapter.id),
|
||||
getPrevChapter(chapter.novelId, chapter.id),
|
||||
@@ -161,10 +152,10 @@ export default function useChapter(webViewRef: RefObject<WebView>) {
|
||||
|
||||
const hideHeader = () => {
|
||||
if (!hidden) {
|
||||
webViewRef.current?.injectJavaScript('toolWrapper.hide()');
|
||||
webViewRef.current?.injectJavaScript('reader.hidden.val = true');
|
||||
setImmersiveMode();
|
||||
} else {
|
||||
webViewRef.current?.injectJavaScript('toolWrapper.show()');
|
||||
webViewRef.current?.injectJavaScript('reader.hidden.val = false');
|
||||
showStatusAndNavBar();
|
||||
}
|
||||
setHidden(!hidden);
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
import { getString } from '@strings/translations';
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
|
||||
interface Options {
|
||||
removeExtraParagraphSpacing?: boolean;
|
||||
}
|
||||
|
||||
export const sanitizeChapterText = (
|
||||
html: string,
|
||||
options?: Options,
|
||||
): string => {
|
||||
export const sanitizeChapterText = (html: string): string => {
|
||||
let text = sanitizeHtml(html, {
|
||||
allowedTags: sanitizeHtml.defaults.allowedTags.concat([
|
||||
'img',
|
||||
@@ -30,12 +23,5 @@ export const sanitizeChapterText = (
|
||||
},
|
||||
allowedSchemes: ['data', 'http', 'https', 'file'],
|
||||
});
|
||||
if (text) {
|
||||
if (options?.removeExtraParagraphSpacing) {
|
||||
text = text.replace(/<\s*br[^>]*>/gi, '\n').replace(/\n{2,}/g, '\n\n');
|
||||
}
|
||||
} else {
|
||||
text = getString('readerScreen.emptyChapterMessage');
|
||||
}
|
||||
return text;
|
||||
return text || getString('readerScreen.emptyChapterMessage');
|
||||
};
|
||||
|
||||
@@ -28,6 +28,7 @@ const GeneralSettings: React.FC = () => {
|
||||
autoScrollInterval = 10,
|
||||
autoScrollOffset = null,
|
||||
bionicReading = false,
|
||||
tapToScroll = false,
|
||||
setChapterGeneralSettings,
|
||||
} = useChapterGeneralSettings();
|
||||
|
||||
@@ -83,6 +84,12 @@ const GeneralSettings: React.FC = () => {
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<SettingSwitch
|
||||
label={getString('readerScreen.bottomSheet.tapToScroll')}
|
||||
value={tapToScroll}
|
||||
onPress={() => setChapterGeneralSettings({ tapToScroll: !tapToScroll })}
|
||||
theme={theme}
|
||||
/>
|
||||
<SettingSwitch
|
||||
label={getString('readerScreen.bottomSheet.pageReader')}
|
||||
value={pageReader}
|
||||
|
||||
@@ -102,8 +102,11 @@ const ReaderThemeSettings = () => {
|
||||
labelStyle={labelStyle}
|
||||
label={getString('readerScreen.bottomSheet.padding')}
|
||||
valueKey="padding"
|
||||
valueChange={0.5}
|
||||
max={10}
|
||||
valueChange={2}
|
||||
min={0}
|
||||
max={50}
|
||||
decimals={0}
|
||||
unit="px"
|
||||
/>
|
||||
<List.Item
|
||||
title={getString('readerScreen.bottomSheet.fontStyle')}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { Button, List } from '@components';
|
||||
import { useChapterReaderSettings, useTheme } from '@hooks/persisted';
|
||||
import { IconButtonV2, List } from '@components';
|
||||
import {
|
||||
useChapterGeneralSettings,
|
||||
useChapterReaderSettings,
|
||||
useTheme,
|
||||
} from '@hooks/persisted';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import VoicePickerModal from '../Modals/VoicePickerModal';
|
||||
import { useBoolean } from '@hooks';
|
||||
@@ -7,6 +11,7 @@ import { Portal } from 'react-native-paper';
|
||||
import { StyleSheet, View, Text } from 'react-native';
|
||||
import Slider from '@react-native-community/slider';
|
||||
import { getAvailableVoicesAsync, Voice } from 'expo-speech';
|
||||
import Switch from '@components/Switch/Switch';
|
||||
|
||||
export default function TextToSpeechSettings() {
|
||||
const theme = useTheme();
|
||||
@@ -19,6 +24,8 @@ export default function TextToSpeechSettings() {
|
||||
}, []);
|
||||
|
||||
const { tts, setChapterReaderSettings } = useChapterReaderSettings();
|
||||
const { TTSEnable = true, setChapterGeneralSettings } =
|
||||
useChapterGeneralSettings();
|
||||
const {
|
||||
value: voiceModalVisible,
|
||||
setTrue: showVoiceModal,
|
||||
@@ -28,61 +35,76 @@ export default function TextToSpeechSettings() {
|
||||
<>
|
||||
<View style={styles.row}>
|
||||
<List.SubHeader theme={theme}>Text to Speech</List.SubHeader>
|
||||
<Button
|
||||
mode="text"
|
||||
onPress={() => {
|
||||
setChapterReaderSettings({
|
||||
tts: {
|
||||
pitch: 1,
|
||||
rate: 1,
|
||||
voice: { name: 'System', language: 'System' } as Voice,
|
||||
},
|
||||
});
|
||||
}}
|
||||
title="Reset"
|
||||
icon="reload"
|
||||
/>
|
||||
<View style={styles.row}>
|
||||
<Switch
|
||||
theme={theme}
|
||||
value={TTSEnable}
|
||||
onValueChange={() => {
|
||||
setChapterGeneralSettings({
|
||||
TTSEnable: !TTSEnable,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<IconButtonV2
|
||||
name="reload"
|
||||
theme={theme}
|
||||
color={theme.primary}
|
||||
onPress={() => {
|
||||
setChapterReaderSettings({
|
||||
tts: {
|
||||
pitch: 1,
|
||||
rate: 1,
|
||||
voice: { name: 'System', language: 'System' } as Voice,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<List.Item
|
||||
title={'TTS voice'}
|
||||
description={tts?.voice?.name || 'System'}
|
||||
onPress={showVoiceModal}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Section>
|
||||
<Text style={[styles.label, { color: theme.onSurface }]}>
|
||||
Voice rate
|
||||
</Text>
|
||||
<Slider
|
||||
style={styles.slider}
|
||||
value={tts?.rate}
|
||||
minimumValue={0.1}
|
||||
maximumValue={5}
|
||||
step={0.1}
|
||||
minimumTrackTintColor={theme.primary}
|
||||
maximumTrackTintColor={theme.surfaceVariant}
|
||||
thumbTintColor={theme.primary}
|
||||
onSlidingComplete={value =>
|
||||
setChapterReaderSettings({ tts: { ...tts, rate: value } })
|
||||
}
|
||||
/>
|
||||
<Text style={[styles.label, { color: theme.onSurface }]}>
|
||||
Voice pitch
|
||||
</Text>
|
||||
<Slider
|
||||
style={styles.slider}
|
||||
value={tts?.pitch}
|
||||
minimumValue={0.1}
|
||||
maximumValue={5}
|
||||
step={0.1}
|
||||
minimumTrackTintColor={theme.primary}
|
||||
maximumTrackTintColor={theme.surfaceVariant}
|
||||
thumbTintColor={theme.primary}
|
||||
onSlidingComplete={value =>
|
||||
setChapterReaderSettings({ tts: { ...tts, pitch: value } })
|
||||
}
|
||||
/>
|
||||
</List.Section>
|
||||
{TTSEnable ? (
|
||||
<>
|
||||
<List.Item
|
||||
title={'TTS voice'}
|
||||
description={tts?.voice?.name || 'System'}
|
||||
onPress={showVoiceModal}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Section>
|
||||
<Text style={[styles.label, { color: theme.onSurface }]}>
|
||||
Voice rate
|
||||
</Text>
|
||||
<Slider
|
||||
style={styles.slider}
|
||||
value={tts?.rate}
|
||||
minimumValue={0.1}
|
||||
maximumValue={5}
|
||||
step={0.1}
|
||||
minimumTrackTintColor={theme.primary}
|
||||
maximumTrackTintColor={theme.surfaceVariant}
|
||||
thumbTintColor={theme.primary}
|
||||
onSlidingComplete={value =>
|
||||
setChapterReaderSettings({ tts: { ...tts, rate: value } })
|
||||
}
|
||||
/>
|
||||
<Text style={[styles.label, { color: theme.onSurface }]}>
|
||||
Voice pitch
|
||||
</Text>
|
||||
<Slider
|
||||
style={styles.slider}
|
||||
value={tts?.pitch}
|
||||
minimumValue={0.1}
|
||||
maximumValue={5}
|
||||
step={0.1}
|
||||
minimumTrackTintColor={theme.primary}
|
||||
maximumTrackTintColor={theme.surfaceVariant}
|
||||
thumbTintColor={theme.primary}
|
||||
onSlidingComplete={value =>
|
||||
setChapterReaderSettings({ tts: { ...tts, pitch: value } })
|
||||
}
|
||||
/>
|
||||
</List.Section>
|
||||
</>
|
||||
) : null}
|
||||
<View style={{ height: 16 }} />
|
||||
<Portal>
|
||||
<VoicePickerModal
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { View, ScrollView, StatusBar, Dimensions } from 'react-native';
|
||||
import { View, ScrollView, StatusBar } from 'react-native';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
@@ -23,8 +23,6 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import color from 'color';
|
||||
import { useBatteryLevel } from 'react-native-device-info';
|
||||
import * as Speech from 'expo-speech';
|
||||
import * as Clipboard from 'expo-clipboard';
|
||||
import { showToast } from '@utils/showToast';
|
||||
import TextToSpeechSettings from './Settings/TextToSpeechSettings';
|
||||
|
||||
export type TextAlignments =
|
||||
@@ -79,34 +77,21 @@ const SettingsReaderScreen = () => {
|
||||
'updatedTime': null,
|
||||
};
|
||||
const [hidden, setHidden] = useState(true);
|
||||
const layoutHeight = Dimensions.get('window').height;
|
||||
const batteryLevel = useBatteryLevel();
|
||||
const readerSettings = useChapterReaderSettings();
|
||||
const {
|
||||
showScrollPercentage,
|
||||
showBatteryAndTime,
|
||||
verticalSeekbar,
|
||||
bionicReading,
|
||||
pageReader,
|
||||
} = useChapterGeneralSettings();
|
||||
const chapterGeneralSettings = useChapterGeneralSettings();
|
||||
const READER_HEIGHT = 280;
|
||||
const assetsUriPrefix = useMemo(
|
||||
() => (__DEV__ ? 'http://localhost:8081/assets' : 'file:///android_asset'),
|
||||
[],
|
||||
);
|
||||
const dummyChapterInfo = {
|
||||
sourceId: 11,
|
||||
chapterId: 99,
|
||||
novelId: 20,
|
||||
novelName: 'novel name',
|
||||
chapterName: "chapter' name",
|
||||
};
|
||||
const webViewCSS = `
|
||||
<style>
|
||||
<link rel="stylesheet" href="${assetsUriPrefix}/css/index.css">
|
||||
<style>
|
||||
:root {
|
||||
--StatusBar-currentHeight: ${StatusBar.currentHeight};
|
||||
--readerSettings-theme: ${readerSettings.theme};
|
||||
--readerSettings-padding: ${readerSettings.padding}%;
|
||||
--readerSettings-padding: ${readerSettings.padding}px;
|
||||
--readerSettings-textSize: ${readerSettings.textSize}px;
|
||||
--readerSettings-textColor: ${readerSettings.textColor};
|
||||
--readerSettings-textAlign: ${readerSettings.textAlign};
|
||||
@@ -125,17 +110,17 @@ const SettingsReaderScreen = () => {
|
||||
--theme-onSurfaceVariant: ${theme.onSurfaceVariant};
|
||||
--theme-outline: ${theme.outline};
|
||||
--theme-rippleColor: ${theme.rippleColor};
|
||||
--chapterCtn-height: ${layoutHeight - 140};
|
||||
}
|
||||
@font-face {
|
||||
font-family: ${readerSettings.fontFamily};
|
||||
src: url("file:///android_asset/fonts/${readerSettings.fontFamily}.ttf");
|
||||
}
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: ${readerSettings.fontFamily};
|
||||
src: url("file:///android_asset/fonts/${
|
||||
readerSettings.fontFamily
|
||||
}.ttf");
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="${assetsUriPrefix}/css/index.css">
|
||||
<style>
|
||||
${readerSettings.customCSS}
|
||||
</style>
|
||||
|
||||
<style>${readerSettings.customCSS}</style>
|
||||
`;
|
||||
|
||||
const readerBackgroundColor = readerSettings.theme;
|
||||
@@ -168,9 +153,13 @@ const SettingsReaderScreen = () => {
|
||||
switch (event.type) {
|
||||
case 'hide':
|
||||
if (hidden) {
|
||||
webViewRef.current?.injectJavaScript('toolWrapper.show()');
|
||||
webViewRef.current?.injectJavaScript(
|
||||
'reader.hidden.val = true',
|
||||
);
|
||||
} else {
|
||||
webViewRef.current?.injectJavaScript('toolWrapper.hide()');
|
||||
webViewRef.current?.injectJavaScript(
|
||||
'reader.hidden.val = false',
|
||||
);
|
||||
}
|
||||
setHidden(!hidden);
|
||||
break;
|
||||
@@ -191,15 +180,6 @@ const SettingsReaderScreen = () => {
|
||||
case 'stop-speak':
|
||||
Speech.stop();
|
||||
break;
|
||||
case 'copy':
|
||||
if (event.data && typeof event.data === 'string') {
|
||||
Clipboard.setStringAsync(event.data).then(() => {
|
||||
showToast(
|
||||
getString('common.copiedToClipboard', { name: '' }),
|
||||
);
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}}
|
||||
source={{
|
||||
@@ -208,63 +188,44 @@ const SettingsReaderScreen = () => {
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
|
||||
${webViewCSS}
|
||||
|
||||
${
|
||||
pageReader
|
||||
? `
|
||||
<link rel="stylesheet" href="${assetsUriPrefix}/css/horizontal.css">
|
||||
`
|
||||
: ''
|
||||
}
|
||||
<script async>
|
||||
var initSettings = {
|
||||
showScrollPercentage: ${showScrollPercentage},
|
||||
swipeGestures: false,
|
||||
showBatteryAndTime: ${showBatteryAndTime},
|
||||
verticalSeekbar: ${verticalSeekbar},
|
||||
bionicReading: ${bionicReading},
|
||||
}
|
||||
var batteryLevel = ${batteryLevel};
|
||||
var autoSaveInterval = 2222;
|
||||
var { NOVEL, CHAPTER } = ${JSON.stringify({
|
||||
NOVEL: novel,
|
||||
CHAPTER: chapter,
|
||||
})}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="chapterCtn">
|
||||
<chapter
|
||||
data-page-reader='${pageReader}'
|
||||
data-novel-id='${dummyChapterInfo.novelId}'
|
||||
data-chapter-id='${dummyChapterInfo.chapterId}'
|
||||
>
|
||||
${dummyHTML}
|
||||
</chapter>
|
||||
<body class="${
|
||||
chapterGeneralSettings.pageReader ? 'page-reader' : ''
|
||||
}">
|
||||
<div id="LNReader-chapter">
|
||||
${dummyHTML}
|
||||
</div>
|
||||
<div class="hidden" id="ToolWrapper">
|
||||
<div id="TTS-Controller"></div>
|
||||
<div id="ScrollBar"></div>
|
||||
</div>
|
||||
<div id="Image-Modal">
|
||||
<img id="Image-Modal-img">
|
||||
</div>
|
||||
<div id="reader-footer-wrapper">
|
||||
<div id="reader-footer">
|
||||
<div id="reader-battery" class="reader-footer-item"></div>
|
||||
<div id="reader-percentage" class="reader-footer-item"></div>
|
||||
<div id="reader-time" class="reader-footer-item"></div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
<script src="${assetsUriPrefix}/js/text-vibe.js"></script>
|
||||
<script src="${assetsUriPrefix}/js/default.js"></script>
|
||||
<script src="${assetsUriPrefix}/js/horizontalScroll.js"></script>
|
||||
<script src="${assetsUriPrefix}/js/index.js"></script>
|
||||
<script src="${assetsUriPrefix}/js/setup.js"></script>
|
||||
<script>
|
||||
setup(0,${readerSettings.customJS})
|
||||
</script>
|
||||
<div id="reader-ui"></div>
|
||||
</body>
|
||||
<script>
|
||||
var initialReaderConfig = ${JSON.stringify({
|
||||
readerSettings,
|
||||
chapterGeneralSettings,
|
||||
novel,
|
||||
chapter,
|
||||
nextChapter: chapter,
|
||||
batteryLevel,
|
||||
autoSaveInterval: 2222,
|
||||
DEBUG: __DEV__,
|
||||
strings: {
|
||||
finished: `${getString(
|
||||
'readerScreen.finished',
|
||||
)}: ${chapter.name.trim()}`,
|
||||
nextChapter: getString('readerScreen.nextChapter', {
|
||||
name: chapter.name,
|
||||
}),
|
||||
noNextChapter: getString('readerScreen.noNextChapter'),
|
||||
},
|
||||
})}
|
||||
</script>
|
||||
<script src="${assetsUriPrefix}/js/icons.js"></script>
|
||||
<script src="${assetsUriPrefix}/js/van.js"></script>
|
||||
<script src="${assetsUriPrefix}/js/text-vibe.js"></script>
|
||||
<script src="${assetsUriPrefix}/js/core.js"></script>
|
||||
<script src="${assetsUriPrefix}/js/index.js"></script>
|
||||
<script>
|
||||
${readerSettings.customJS}
|
||||
</script>
|
||||
</html>
|
||||
`,
|
||||
}}
|
||||
|
||||
@@ -427,6 +427,7 @@
|
||||
"allowTextSelection": "Allow text selection",
|
||||
"autoscroll": "AutoScroll",
|
||||
"bionicReading": "Bionic Reading",
|
||||
"tapToScroll": "Tap to scroll",
|
||||
"color": "Color",
|
||||
"fontStyle": "Font style",
|
||||
"fullscreen": "Fullscreen",
|
||||
@@ -514,4 +515,4 @@
|
||||
"updatesLower": "updates",
|
||||
"updatingLibrary": "Updating library"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -349,6 +349,7 @@ export interface StringMap {
|
||||
'readerScreen.bottomSheet.allowTextSelection': 'string';
|
||||
'readerScreen.bottomSheet.autoscroll': 'string';
|
||||
'readerScreen.bottomSheet.bionicReading': 'string';
|
||||
'readerScreen.bottomSheet.tapToScroll': 'string';
|
||||
'readerScreen.bottomSheet.color': 'string';
|
||||
'readerScreen.bottomSheet.fontStyle': 'string';
|
||||
'readerScreen.bottomSheet.fullscreen': 'string';
|
||||
|
||||
Reference in New Issue
Block a user