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:
nyagami
2024-03-08 18:51:39 +07:00
committed by GitHub
parent 607941ea1b
commit db478130e6
9 changed files with 479 additions and 128 deletions
+7 -12
View File
@@ -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>
+130 -39
View File
@@ -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);
}
}
+234 -49
View File
@@ -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
View File
@@ -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
View File
@@ -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);
});
};
+3 -1
View File
@@ -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}
+19 -11
View File
@@ -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>