Fix Filter Sheet color & Reader Context menu (#991)
* fix: initialize once * fix: filter sheet color * fix: jump to chapter modal * move volume button to context menu * rework TTS * fix: cleanup * minor fix
This commit is contained in:
@@ -3,7 +3,7 @@ import { enableFreeze } from 'react-native-screens';
|
||||
|
||||
enableFreeze(true);
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import React from 'react';
|
||||
import { StatusBar } from 'react-native';
|
||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||
import LottieSplashScreen from 'react-native-lottie-splash-screen';
|
||||
@@ -18,7 +18,6 @@ import { deserializePlugins } from '@plugins/pluginManager';
|
||||
|
||||
import Main from './src/navigators/Main';
|
||||
import { BottomSheetModalProvider } from '@gorhom/bottom-sheet';
|
||||
import { usePlugins } from '@hooks/persisted';
|
||||
import { MMKVStorage } from '@utils/mmkv/mmkv';
|
||||
import { BACKGROUND_ACTION } from '@services/constants';
|
||||
|
||||
@@ -32,17 +31,13 @@ Notifications.setNotificationHandler({
|
||||
},
|
||||
});
|
||||
|
||||
const App = () => {
|
||||
const { refreshPlugins } = usePlugins();
|
||||
useEffect(() => {
|
||||
createTables();
|
||||
deserializePlugins().then(() => LottieSplashScreen.hide());
|
||||
refreshPlugins();
|
||||
if (!BackgroundService.isRunning()) {
|
||||
MMKVStorage.delete(BACKGROUND_ACTION);
|
||||
}
|
||||
}, []);
|
||||
createTables();
|
||||
deserializePlugins().then(() => LottieSplashScreen.hide());
|
||||
if (!BackgroundService.isRunning()) {
|
||||
MMKVStorage.delete(BACKGROUND_ACTION);
|
||||
}
|
||||
|
||||
const App = () => {
|
||||
return (
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
<AppErrorBoundary>
|
||||
|
||||
@@ -9,6 +9,7 @@ html {
|
||||
color: var(--theme-onSecondary);
|
||||
background-color: var(--theme-secondary);
|
||||
}
|
||||
|
||||
::-moz-selection {
|
||||
color: var(--theme-onSecondary);
|
||||
background-color: var(--theme-secondary);
|
||||
@@ -57,6 +58,7 @@ img {
|
||||
font-family: var(--readerSettings-fontFamily);
|
||||
font-size: 16px;
|
||||
border-width: 0;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.nextButton {
|
||||
@@ -86,24 +88,40 @@ img {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
visibility: hidden;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
#ToolWrapper {
|
||||
position: fixed;
|
||||
top: 25vh;
|
||||
right: 5vw;
|
||||
transition: 150ms;
|
||||
}
|
||||
|
||||
#ToolWrapper.hidden {
|
||||
opacity: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
#TTS-Controller {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#TTS-Controller button{
|
||||
background: var(--theme-surface-0-9);
|
||||
border: 0;
|
||||
}
|
||||
|
||||
#TTS-Controller svg {
|
||||
fill: var(--theme-onSurface);
|
||||
}
|
||||
|
||||
#ScrollBar {
|
||||
margin-top: 8px;
|
||||
width: 2.4rem;
|
||||
height: 45vh;
|
||||
border-radius: 1.2rem;
|
||||
background-color: var(--theme-surface-0-9);
|
||||
touch-action: none;
|
||||
font-size: 16px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.scrollbar-item {
|
||||
@@ -132,7 +150,6 @@ img {
|
||||
width: 0.1rem;
|
||||
height: 0;
|
||||
background-color: var(--theme-primary);
|
||||
transform: translateX(-0.1rem);
|
||||
}
|
||||
|
||||
#scrollbar-thumb-wrapper {
|
||||
@@ -171,45 +188,119 @@ img {
|
||||
color: var(--readerSettings-textColor);
|
||||
font-size: 1rem;
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.reader-footer-item{
|
||||
.reader-footer-item {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.tts {
|
||||
position: relative;
|
||||
bottom: 1rem;
|
||||
}
|
||||
|
||||
.tts > svg {
|
||||
fill: var(--theme-secondary);
|
||||
-webkit-transition: 0.3s;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
.tts::before{
|
||||
position: absolute;
|
||||
content: '';
|
||||
height: 40px;
|
||||
width: 2px;
|
||||
background-color: var(--theme-secondary);;
|
||||
transform-origin: top right;
|
||||
transform: translate(4px, 4px) rotate(-45deg);
|
||||
-webkit-transition: 0.3s;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
.tts.speak::before{
|
||||
height: 0;
|
||||
background-color: var(--theme-primary);
|
||||
}
|
||||
|
||||
.tts.speak > svg{
|
||||
fill: var(--theme-primary);
|
||||
}
|
||||
|
||||
tts.highlight {
|
||||
color: var(--theme-onSecondary);
|
||||
background-color: var(--theme-secondary);
|
||||
}
|
||||
|
||||
.contextMenu {
|
||||
position: absolute;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
background: var(--theme-surface-0-9);
|
||||
-webkit-backdrop-filter: blur(1px);
|
||||
backdrop-filter: blur(1px);
|
||||
position: fixed;
|
||||
top: var(--top);
|
||||
left: var(--left);
|
||||
-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;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
border-radius: 4px;
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,30 @@
|
||||
/**
|
||||
* https://fonts.google.com/icons
|
||||
* Filled
|
||||
* other settings are default
|
||||
*/
|
||||
const copyIcon =
|
||||
'<svg height="24" viewBox="0 -960 960 960" width="24"><path d="M360-240q-33 0-56.5-23.5T280-320v-480q0-33 23.5-56.5T360-880h360q33 0 56.5 23.5T800-800v480q0 33-23.5 56.5T720-240H360ZM200-80q-33 0-56.5-23.5T120-160v-560h80v560h440v80H200Z"/></svg>';
|
||||
const volumeIcon =
|
||||
'<svg height="24" viewBox="0 -960 960 960" width="24"><path d="M560-131v-82q90-26 145-100t55-168q0-94-55-168T560-749v-82q124 28 202 125.5T840-481q0 127-78 224.5T560-131ZM120-360v-240h160l200-200v640L280-360H120Zm440 40v-322q47 22 73.5 66t26.5 96q0 51-26.5 94.5T560-320Z"/></svg>';
|
||||
const selectAllIcon =
|
||||
'<svg height="24" viewBox="0 -960 960 960" width="24"><path d="M280-280v-400h400v400H280Zm80-80h240v-240H360v240ZM200-200v80q-33 0-56.5-23.5T120-200h80Zm-80-80v-80h80v80h-80Zm0-160v-80h80v80h-80Zm0-160v-80h80v80h-80Zm80-160h-80q0-33 23.5-56.5T200-840v80Zm80 640v-80h80v80h-80Zm0-640v-80h80v80h-80Zm160 640v-80h80v80h-80Zm0-640v-80h80v80h-80Zm160 640v-80h80v80h-80Zm0-640v-80h80v80h-80Zm160 640v-80h80q0 33-23.5 56.5T760-120Zm0-160v-80h80v80h-80Zm0-160v-80h80v80h-80Zm0-160v-80h80v80h-80Zm0-160v-80q33 0 56.5 23.5T840-760h-80Z"/></svg>';
|
||||
const translateIcon =
|
||||
'<svg height="24" viewBox="0 -960 960 960" width="24"><path d="m476-80 182-480h84L924-80h-84l-43-122H603L560-80h-84ZM160-200l-56-56 202-202q-35-35-63.5-80T190-640h84q20 39 40 68t48 58q33-33 68.5-92.5T484-720H40v-80h280v-80h80v80h280v80H564q-21 72-63 148t-83 116l96 98-30 82-122-125-202 201Zm468-72h144l-72-204-72 204Z"/></svg>';
|
||||
const resumeIcon =
|
||||
'<svg height="24" viewBox="0 -960 960 960" width="24"><path d="M240-240v-480h80v480h-80Zm160 0 400-240-400-240v480Z"/></svg>';
|
||||
const pauseIcon =
|
||||
'<svg height="24" viewBox="0 -960 960 960" width="24"><path d="M560-200v-560h160v560H560Zm-320 0v-560h160v560H240Z"/></svg>';
|
||||
const stopIcon =
|
||||
'<svg height="24" viewBox="0 -960 960 960" width="24"><path d="M320-320h320v-320H320v320ZM480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Z"/></svg>';
|
||||
const selectVolumeIcon =
|
||||
'<svg height="24" viewBox="0 -960 960 960" width="24"><path d="M560-131v-82q90-26 145-100t55-168q0-94-55-168T560-749v-82q124 28 202 125.5T840-481q0 127-78 224.5T560-131Zm-80-29L280-360H120v-240h160l200-200v640Zm80-160v-322q47 22 73.5 66t26.5 96q0 51-26.5 94.5T560-320ZM40-680v-240h240v80H120v160H40ZM680-40v-80h160v-160h80v240H680Z"/></svg>';
|
||||
/**
|
||||
* @type {import("./type").Reader}
|
||||
*/
|
||||
class Reader {
|
||||
constructor() {
|
||||
this.selection = window.getSelection();
|
||||
this.footerWrapper = document.getElementById('reader-footer-wrapper');
|
||||
this.percentage = document.getElementById('reader-percentage');
|
||||
this.battery = document.getElementById('reader-battery');
|
||||
@@ -121,11 +146,11 @@ class ToolWrapper {
|
||||
this.tools = [];
|
||||
}
|
||||
hide = () => {
|
||||
this.$.classList.add('d-none');
|
||||
this.$.classList.add('hidden');
|
||||
this.visible = false;
|
||||
};
|
||||
show = () => {
|
||||
this.$.classList.remove('d-none');
|
||||
this.$.classList.remove('hidden');
|
||||
this.visible = true;
|
||||
for (const tool of this.tools) {
|
||||
tool.onShow?.();
|
||||
@@ -144,12 +169,13 @@ class ScrollHandler {
|
||||
</div>
|
||||
<div class="scrollbar-item" id="scrollbar-slider">
|
||||
<div id="scrollbar-track">
|
||||
</div>
|
||||
<div id="scrollbar-progress">
|
||||
<div id="scrollbar-thumb-wrapper">
|
||||
<div id="scrollbar-thumb"></div>
|
||||
<div id="scrollbar-progress">
|
||||
<div id="scrollbar-thumb-wrapper">
|
||||
<div id="scrollbar-thumb"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="scrollbar-item scrollbar-text">
|
||||
100
|
||||
@@ -236,36 +262,54 @@ class SwipeHandler {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {import('./type').TextToSpeech}
|
||||
*/
|
||||
class TextToSpeech {
|
||||
constructor(reader) {
|
||||
this.reader = reader;
|
||||
this.$ = document.getElementById('TextToSpeech');
|
||||
this.chapter = document.querySelector('chapter');
|
||||
(this.leaf = this.chapter), (this.TTSWrapper = null); // swap these 2 elements
|
||||
(this.leaf = this.reader.chapter), (this.TTSWrapper = null); // swap these 2 elements
|
||||
this.TTSEle = null;
|
||||
this.speaking = false;
|
||||
this.icon = this.$.querySelector('.tts');
|
||||
this.reading = false;
|
||||
this.$ = document.getElementById('TTS-Controller');
|
||||
this.$.classList.add('d-none');
|
||||
this.$.onclick = () => {
|
||||
if (this.speaking) {
|
||||
this.icon.classList.remove('speak');
|
||||
this.stop();
|
||||
if (this.reading) {
|
||||
this.pause();
|
||||
} else {
|
||||
this.icon.classList.add('speak');
|
||||
const selection = window.getSelection();
|
||||
if (selection.type === 'Range') {
|
||||
if (this.leaf && this.TTSWrapper) {
|
||||
this.TTSWrapper.replaceWith(this.leaf);
|
||||
}
|
||||
this.leaf = selection.anchorNode;
|
||||
this.makeLeafSpeakable();
|
||||
this.speak();
|
||||
} else {
|
||||
this.next();
|
||||
}
|
||||
this.resume();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
start = () => {
|
||||
this.$.innerHTML = `<button>${pauseIcon}</button>`;
|
||||
this.$.classList.remove('d-none');
|
||||
this.stop();
|
||||
this.next();
|
||||
};
|
||||
|
||||
startHere = () => {
|
||||
this.$.innerHTML = `<button>${pauseIcon}</button>`;
|
||||
this.$.classList.remove('d-none');
|
||||
this.stop();
|
||||
if (this.reader.selection.type === 'Range') {
|
||||
if (this.leaf && this.TTSWrapper) {
|
||||
this.TTSWrapper.replaceWith(this.leaf);
|
||||
}
|
||||
this.leaf = this.reader.selection.anchorNode;
|
||||
this.makeLeafSpeakable();
|
||||
this.speak();
|
||||
} else {
|
||||
this.next();
|
||||
}
|
||||
};
|
||||
|
||||
resume = () => {
|
||||
this.$.innerHTML = `<button>${pauseIcon}</button>`;
|
||||
this.next();
|
||||
};
|
||||
|
||||
findLeaf() {
|
||||
while (this.leaf.firstChild) {
|
||||
this.leaf = this.leaf.firstChild;
|
||||
@@ -273,14 +317,14 @@ class TextToSpeech {
|
||||
}
|
||||
|
||||
findNextLeaf() {
|
||||
if (this.chapter.isSameNode(this.leaf)) {
|
||||
if (this.reader.chapter.isSameNode(this.leaf)) {
|
||||
this.findLeaf();
|
||||
} else if (this.leaf.nextSibling) {
|
||||
this.leaf = this.leaf.nextSibling;
|
||||
this.findLeaf();
|
||||
} else {
|
||||
this.leaf = this.leaf.parentNode;
|
||||
if (this.chapter.isSameNode(this.leaf)) {
|
||||
if (this.reader.chapter.isSameNode(this.leaf)) {
|
||||
return;
|
||||
}
|
||||
this.findNextLeaf();
|
||||
@@ -300,8 +344,8 @@ class TextToSpeech {
|
||||
}
|
||||
do {
|
||||
this.findNextLeaf();
|
||||
} while (!this.readable() && !this.chapter.isSameNode(this.leaf));
|
||||
if (this.chapter.isSameNode(this.leaf)) {
|
||||
} while (!this.readable() && !this.reader.chapter.isSameNode(this.leaf));
|
||||
if (this.reader.chapter.isSameNode(this.leaf)) {
|
||||
return;
|
||||
}
|
||||
this.makeLeafSpeakable();
|
||||
@@ -323,37 +367,176 @@ class TextToSpeech {
|
||||
}
|
||||
|
||||
next = () => {
|
||||
if (this.TTSEle) {
|
||||
this.TTSEle.classList.remove('highlight');
|
||||
}
|
||||
if (this.speaking) {
|
||||
if (this.TTSEle && this.TTSEle.nextElementSibling) {
|
||||
this.TTSEle = this.TTSEle.nextElementSibling;
|
||||
try {
|
||||
if (this.TTSEle) {
|
||||
this.TTSEle.classList.remove('highlight');
|
||||
}
|
||||
if (this.reading) {
|
||||
if (this.TTSEle && this.TTSEle.nextElementSibling) {
|
||||
this.TTSEle = this.TTSEle.nextElementSibling;
|
||||
} else {
|
||||
this.findNextTextNode();
|
||||
}
|
||||
} else {
|
||||
this.findNextTextNode();
|
||||
}
|
||||
} else {
|
||||
if (!this.TTSEle) {
|
||||
this.findNextTextNode();
|
||||
if (!this.TTSEle) {
|
||||
this.findNextTextNode();
|
||||
}
|
||||
}
|
||||
this.speak();
|
||||
} catch (e) {
|
||||
alert(e);
|
||||
}
|
||||
this.speak();
|
||||
};
|
||||
|
||||
stop = () => {
|
||||
this.$.classList.add('d-none');
|
||||
this.reading = false;
|
||||
if (this.leaf && this.TTSWrapper) {
|
||||
this.TTSWrapper.replaceWith(this.leaf);
|
||||
this.TTSWrapper = null;
|
||||
this.TTSEle = null;
|
||||
this.leaf = this.reader.chapter;
|
||||
}
|
||||
this.reader.post({ type: 'stop-speak' });
|
||||
};
|
||||
|
||||
pause = () => {
|
||||
this.$.innerHTML = `<button>${resumeIcon}</button>`;
|
||||
this.reading = false;
|
||||
this.reader.post({ type: 'stop-speak' });
|
||||
};
|
||||
|
||||
started = () => this.TTSWrapper !== null;
|
||||
|
||||
speak = () => {
|
||||
this.speaking = true;
|
||||
if (this.chapter.isSameNode(this.leaf)) {
|
||||
this.reading = true;
|
||||
if (this.reader.chapter.isSameNode(this.leaf)) {
|
||||
return;
|
||||
}
|
||||
this.TTSEle.classList.add('highlight');
|
||||
this.reader.post({ type: 'speak', data: this.TTSEle?.innerText.trim() });
|
||||
};
|
||||
|
||||
stop = () => {
|
||||
this.speaking = false;
|
||||
this.reader.post({ type: 'stop-speak' });
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {import("./type").ContextMenu}
|
||||
*/
|
||||
class ContextMenu {
|
||||
/**
|
||||
* @param {import("./type").Reader} reader
|
||||
*/
|
||||
constructor(reader) {
|
||||
this.reader = reader;
|
||||
this.items = {
|
||||
START_READING: this.renderItem({
|
||||
name: 'Start Reading',
|
||||
icon: volumeIcon,
|
||||
action: () => {
|
||||
tts.start();
|
||||
this.closeMenu();
|
||||
},
|
||||
}),
|
||||
START_HERE: this.renderItem({
|
||||
name: 'Start Here',
|
||||
icon: selectVolumeIcon,
|
||||
action: () => {
|
||||
tts.startHere();
|
||||
this.closeMenu();
|
||||
},
|
||||
}),
|
||||
COPY: this.renderItem({
|
||||
name: 'Copy',
|
||||
icon: copyIcon,
|
||||
action: () => {
|
||||
this.reader.post({
|
||||
type: 'copy',
|
||||
data: this.reader.selection.toString(),
|
||||
});
|
||||
},
|
||||
}),
|
||||
SELECT_ALL: this.renderItem({
|
||||
name: 'Select All',
|
||||
icon: selectAllIcon,
|
||||
action: () => {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(this.reader.chapter);
|
||||
this.reader.selection.removeAllRanges();
|
||||
this.reader.selection.addRange(range);
|
||||
},
|
||||
}),
|
||||
};
|
||||
this.commonItems = [this.items.COPY, this.items.SELECT_ALL];
|
||||
this.contextMenu = document.createElement('ul');
|
||||
this.contextMenu.classList.add('contextMenu');
|
||||
this.isOpened = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{name: string, icon: string, action: () => void}} data
|
||||
* @returns
|
||||
*/
|
||||
renderItem(data) {
|
||||
const item = document.createElement('li');
|
||||
const button = document.createElement('button');
|
||||
button.innerHTML = `${data.icon} <span>${data.name}</span>`;
|
||||
button.classList.add('contextMenu-button');
|
||||
item.classList.add('contextMenu-item');
|
||||
item.appendChild(button);
|
||||
button.addEventListener('click', event => {
|
||||
event.stopPropagation();
|
||||
data.action();
|
||||
});
|
||||
return item;
|
||||
}
|
||||
|
||||
renderMenu(items) {
|
||||
this.contextMenu.innerHTML = '';
|
||||
items.concat(this.commonItems).forEach((item, index) => {
|
||||
item.firstChild.setAttribute('style', `animation-delay: ${index * 0.1}s`);
|
||||
this.contextMenu.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
closeMenu() {
|
||||
if (this.isOpened) {
|
||||
this.isOpened = false;
|
||||
this.contextMenu.remove();
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
this.reader.chapter.addEventListener('click', () => {
|
||||
this.closeMenu(this.contextMenu);
|
||||
});
|
||||
document.addEventListener('contextmenu', e => {
|
||||
e.preventDefault();
|
||||
this.isOpened = true;
|
||||
if (this.reader.selection.type === 'Range') {
|
||||
this.renderMenu([this.items.START_HERE, this.items.START_READING]);
|
||||
} else {
|
||||
this.renderMenu([this.items.START_READING]);
|
||||
}
|
||||
const { clientX, clientY } = e;
|
||||
document.body.appendChild(this.contextMenu);
|
||||
const positionY =
|
||||
clientY + this.contextMenu.scrollHeight >= window.innerHeight
|
||||
? window.innerHeight - this.contextMenu.scrollHeight - 20
|
||||
: clientY;
|
||||
const positionX =
|
||||
clientX + this.contextMenu.scrollWidth >= window.innerWidth
|
||||
? window.innerWidth - this.contextMenu.scrollWidth - 20
|
||||
: clientX;
|
||||
this.contextMenu.setAttribute(
|
||||
'style',
|
||||
`--width: ${this.contextMenu.scrollWidth}px;
|
||||
--height: ${this.contextMenu.scrollHeight}px;
|
||||
--top: ${positionY}px;
|
||||
--left: ${positionX}px;`,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
var swipeHandler = new SwipeHandler();
|
||||
var toolWrapper = new ToolWrapper();
|
||||
@@ -361,6 +544,8 @@ try {
|
||||
var scrollHandler = new ScrollHandler(reader, toolWrapper);
|
||||
var tts = new TextToSpeech(reader);
|
||||
toolWrapper.tools = [scrollHandler, tts];
|
||||
const contextMenu = new ContextMenu(reader);
|
||||
contextMenu.init();
|
||||
} catch (e) {
|
||||
alert(e);
|
||||
}
|
||||
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import {
|
||||
ChapterGeneralSettings,
|
||||
ChapterReaderSettings,
|
||||
} from '@hooks/persisted/useSettings';
|
||||
import 'typescript';
|
||||
import 'typescript/lib/lib.dom';
|
||||
|
||||
interface Reader {
|
||||
selection: Selection;
|
||||
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: HTMLElement;
|
||||
TTSWrapper: HTMLElement;
|
||||
TTSEle: HTMLElement;
|
||||
reading: boolean;
|
||||
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;
|
||||
}
|
||||
+5
-7
@@ -21,14 +21,12 @@ export const createTables = () => {
|
||||
db.exec([{ sql: 'PRAGMA foreign_keys = ON', args: [] }], false, () => {});
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(createNovelTableQuery);
|
||||
tx.executeSql(createCategoriesTableQuery, [], () => {
|
||||
tx.executeSql(createCategoryDefaultQuery);
|
||||
tx.executeSql(createCategoryTriggerQuery);
|
||||
});
|
||||
tx.executeSql(createCategoriesTableQuery);
|
||||
tx.executeSql(createCategoryDefaultQuery);
|
||||
tx.executeSql(createCategoryTriggerQuery);
|
||||
tx.executeSql(createNovelCategoryTableQuery);
|
||||
tx.executeSql(createChapterTableQuery, [], () => {
|
||||
tx.executeSql(createChapterNovelIdIndexQuery);
|
||||
});
|
||||
tx.executeSql(createChapterTableQuery);
|
||||
tx.executeSql(createChapterNovelIdIndexQuery);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { NavigationContainer } from '@react-navigation/native';
|
||||
import { createStackNavigator } from '@react-navigation/stack';
|
||||
|
||||
import { setBarColor } from '@theme/utils/setBarColor';
|
||||
import { useAppSettings, useTheme } from '@hooks/persisted';
|
||||
import { useAppSettings, usePlugins, useTheme } from '@hooks/persisted';
|
||||
import { useGithubUpdateChecker } from '@hooks/common/githubUpdateChecker';
|
||||
|
||||
/**
|
||||
@@ -37,6 +37,7 @@ const Stack = createStackNavigator<RootStackParamList>();
|
||||
const MainNavigator = () => {
|
||||
const theme = useTheme();
|
||||
const { updateLibraryOnLaunch } = useAppSettings();
|
||||
const { refreshPlugins } = usePlugins();
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(async () => {
|
||||
@@ -52,6 +53,7 @@ const MainNavigator = () => {
|
||||
if (updateLibraryOnLaunch) {
|
||||
updateLibrary();
|
||||
}
|
||||
refreshPlugins();
|
||||
}, []);
|
||||
|
||||
const { isNewVersion, latestRelease } = useGithubUpdateChecker();
|
||||
|
||||
@@ -62,6 +62,7 @@ const FilterItem: React.FC<FilterItemProps> = ({
|
||||
<Menu
|
||||
style={{ flex: 1 }}
|
||||
visible={isVisible}
|
||||
contentStyle={{ backgroundColor: theme.surfaceVariant }}
|
||||
anchor={
|
||||
<Pressable
|
||||
style={{ flex: 1, width: screenWidth - 48 }}
|
||||
@@ -79,17 +80,18 @@ const FilterItem: React.FC<FilterItemProps> = ({
|
||||
},
|
||||
]}
|
||||
>
|
||||
{filter.label}
|
||||
{` ${filter.label} `}
|
||||
</Text>
|
||||
}
|
||||
value={value || 'whatever'}
|
||||
editable={false}
|
||||
outlineColor={isVisible ? theme.primary : theme.outline}
|
||||
textColor={isVisible ? theme.primary : theme.outline}
|
||||
theme={{ colors: { background: 'transparent' } }}
|
||||
outlineColor={isVisible ? theme.primary : theme.onSurface}
|
||||
textColor={isVisible ? theme.primary : theme.onSurface}
|
||||
right={
|
||||
<TextInput.Icon
|
||||
icon={isVisible ? 'chevron-up' : 'chevron-down'}
|
||||
color={isVisible ? theme.primary : theme.outline}
|
||||
color={isVisible ? theme.primary : theme.onSurface}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
@@ -102,6 +104,7 @@ const FilterItem: React.FC<FilterItemProps> = ({
|
||||
<Menu.Item
|
||||
key={val.label}
|
||||
title={val.label}
|
||||
titleStyle={{ color: theme.onSurfaceVariant }}
|
||||
onPress={() => {
|
||||
closeCard();
|
||||
setSelectedFilters(prevFilters => ({
|
||||
|
||||
@@ -27,6 +27,8 @@ const JumpToChapterModal = ({
|
||||
novel,
|
||||
chapterListRef,
|
||||
}: JumpToChapterModalProps) => {
|
||||
const minNumber = Math.min(...chapters.map(c => c.chapterNumber || -1));
|
||||
const maxNumber = Math.max(...chapters.map(c => c.chapterNumber || -1));
|
||||
const theme = useTheme();
|
||||
const [mode, setMode] = useState(false);
|
||||
const [openChapter, setOpenChapter] = useState(false);
|
||||
@@ -100,15 +102,20 @@ const JumpToChapterModal = ({
|
||||
const onSubmit = () => {
|
||||
if (!mode) {
|
||||
const num = Number(text);
|
||||
if (num && num > 0 && num <= chapters.length) {
|
||||
if (num && num >= minNumber && num <= maxNumber) {
|
||||
if (openChapter) {
|
||||
return navigateToChapter(chapters[num - 1]);
|
||||
const chapter = chapters.find(c => c.chapterNumber === num);
|
||||
if (chapter) {
|
||||
return navigateToChapter(chapter);
|
||||
}
|
||||
} else {
|
||||
const index = chapters.findIndex(c => c.chapterNumber === num);
|
||||
return scrollToIndex(index);
|
||||
}
|
||||
return scrollToIndex(num - 1);
|
||||
}
|
||||
return setError(
|
||||
getString('novelScreen.jumpToChapterModal.error.validChapterNumber') +
|
||||
` (${num <= 0 ? '≤ 0' : '≤ ' + chapters.length})`,
|
||||
` (${num < minNumber ? '≥ ' + minNumber : '≤ ' + maxNumber})`,
|
||||
);
|
||||
} else {
|
||||
const searchedChapters = chapters.filter(chap =>
|
||||
@@ -160,7 +167,7 @@ const JumpToChapterModal = ({
|
||||
mode
|
||||
? getString('novelScreen.jumpToChapterModal.chapterName')
|
||||
: getString('novelScreen.jumpToChapterModal.chapterNumber') +
|
||||
` (≤ ${chapters.length})`
|
||||
` (≥ ${minNumber}, ≤ ${maxNumber})`
|
||||
}
|
||||
onChangeText={onChangeText}
|
||||
onSubmitEditing={onSubmit}
|
||||
|
||||
@@ -24,6 +24,8 @@ 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';
|
||||
|
||||
type WebViewPostEvent = {
|
||||
type: string;
|
||||
@@ -166,6 +168,13 @@ const WebViewReader: 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={{
|
||||
@@ -187,20 +196,24 @@ const WebViewReader: FC<WebViewReaderProps> = props => {
|
||||
--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-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("${assetsUriPrefix}/fonts/${
|
||||
readerSettings.fontFamily
|
||||
}.ttf");
|
||||
src: url("file:///android_asset/fonts/${
|
||||
readerSettings.fontFamily
|
||||
}.ttf");
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="${assetsUriPrefix}/css/index.css">
|
||||
@@ -224,15 +237,10 @@ const WebViewReader: FC<WebViewReaderProps> = props => {
|
||||
>
|
||||
${html}
|
||||
</chapter>
|
||||
<div class="d-none" id="ToolWrapper">
|
||||
<div id="TextToSpeech" draggable="true">
|
||||
<span class="tts">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="40" viewBox="0 -960 960 960" width="40"><path d="M560-131v-82q90-26 145-100t55-168q0-94-55-168T560-749v-82q124 28 202 125.5T840-481q0 127-78 224.5T560-131ZM120-360v-240h160l200-200v640L280-360H120Zm440 40v-322q47 22 73.5 66t26.5 96q0 51-26.5 94.5T560-320Z"/></svg>
|
||||
</span>
|
||||
</div>
|
||||
<div class="hidden" id="ToolWrapper">
|
||||
<div id="TTS-Controller"></div>
|
||||
<div id="ScrollBar"></div>
|
||||
</div>
|
||||
|
||||
<div id="reader-footer-wrapper">
|
||||
<div id="reader-footer">
|
||||
<div id="reader-battery" class="reader-footer-item"></div>
|
||||
|
||||
Reference in New Issue
Block a user