Fix bugs & Reader improvement (#1009)
* fix: wrong chapter order * fix: reader footer styles * make swipe getures less sensitive * fix: update only ongoing novels option * synchonize update library * add horizontal scrollbar * update swipe gesture sensitive * no swipe when image modal is visible * add bionic reading * fix: import untitled epub
This commit is contained in:
@@ -97,7 +97,23 @@ img {
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
right: 5vw;
|
||||
transition: 150ms;
|
||||
}
|
||||
|
||||
#ToolWrapper.horizontal {
|
||||
top: unset;
|
||||
right: unset;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
bottom: 120px;
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 500px) {
|
||||
#ToolWrapper.horizontal {
|
||||
bottom: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
#ToolWrapper.hidden {
|
||||
@@ -105,10 +121,20 @@ img {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
#ToolWrapper.hidden.horizontal {
|
||||
opacity: 0;
|
||||
bottom: 0;
|
||||
right: unset;
|
||||
}
|
||||
|
||||
#TTS-Controller {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#ToolWrapper.horizontal > #TTS-Controller {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
#TTS-Controller button{
|
||||
background: var(--theme-surface-0-9);
|
||||
border: 0;
|
||||
@@ -118,7 +144,7 @@ img {
|
||||
fill: var(--theme-onSurface);
|
||||
}
|
||||
|
||||
#ScrollBar {
|
||||
#ToolWrapper > #ScrollBar {
|
||||
margin-top: 8px;
|
||||
width: 2.4rem;
|
||||
height: 45vh;
|
||||
@@ -130,35 +156,35 @@ img {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.scrollbar-item {
|
||||
#ScrollBar > .scrollbar-item {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--theme-onSurface);
|
||||
}
|
||||
|
||||
.scrollbar-item.scrollbar-text {
|
||||
#ScrollBar > .scrollbar-item.scrollbar-text {
|
||||
height: 12.5%;
|
||||
}
|
||||
|
||||
.scrollbar-item#scrollbar-slider {
|
||||
#ScrollBar > .scrollbar-item#scrollbar-slider {
|
||||
height: 75%;
|
||||
align-items: unset;
|
||||
}
|
||||
|
||||
#scrollbar-track {
|
||||
#ScrollBar #scrollbar-track {
|
||||
width: 0.1rem;
|
||||
height: 100%;
|
||||
background-color: var(--theme-outline);
|
||||
}
|
||||
|
||||
#scrollbar-progress {
|
||||
width: 0.1rem;
|
||||
#ScrollBar #scrollbar-progress {
|
||||
width: 100%;
|
||||
height: 0;
|
||||
background-color: var(--theme-primary);
|
||||
}
|
||||
|
||||
#scrollbar-thumb-wrapper {
|
||||
#ScrollBar #scrollbar-thumb-wrapper {
|
||||
position: relative;
|
||||
top: 100%;
|
||||
height: 2rem;
|
||||
@@ -169,7 +195,62 @@ img {
|
||||
transform: translate(-0.95rem, -1rem);
|
||||
}
|
||||
|
||||
#scrollbar-thumb {
|
||||
#ScrollBar #scrollbar-thumb {
|
||||
top: 100%;
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
border-radius: 100%;
|
||||
background-color: var(--theme-primary);
|
||||
}
|
||||
|
||||
#ToolWrapper.horizontal > #ScrollBar{
|
||||
display: flex;
|
||||
width: 80vw;
|
||||
height: 2.4rem;
|
||||
min-height: unset;
|
||||
align-items: center;
|
||||
margin-top: unset;
|
||||
}
|
||||
|
||||
#ToolWrapper.horizontal > #ScrollBar > .scrollbar-item {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
color: var(--theme-onSurface);
|
||||
}
|
||||
|
||||
#ToolWrapper.horizontal > #ScrollBar > .scrollbar-item.scrollbar-text {
|
||||
width: 12.5%;
|
||||
}
|
||||
|
||||
#ToolWrapper.horizontal > #ScrollBar > .scrollbar-item#scrollbar-slider {
|
||||
width: 75%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#ToolWrapper.horizontal > #ScrollBar #scrollbar-track {
|
||||
height: 0.1rem;
|
||||
width: 100%;
|
||||
background-color: var(--theme-outline);
|
||||
}
|
||||
|
||||
#ToolWrapper.horizontal > #ScrollBar #scrollbar-progress {
|
||||
height: 100%;
|
||||
width: 0;
|
||||
background-color: var(--theme-primary);
|
||||
}
|
||||
|
||||
#ToolWrapper.horizontal > #ScrollBar #scrollbar-thumb-wrapper {
|
||||
position: relative;
|
||||
left: 100%;
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transform: translate(-0.95rem, -1rem);
|
||||
}
|
||||
|
||||
#ToolWrapper.horizontal > #ScrollBar #scrollbar-thumb {
|
||||
top: 100%;
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
@@ -186,10 +267,8 @@ img {
|
||||
|
||||
#reader-footer {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
justify-content: space-between;
|
||||
min-height: 2rem;
|
||||
padding: 0.5rem 2rem 0 2rem;
|
||||
flex: 3;
|
||||
padding: 0.2rem 2rem;
|
||||
background-color: var(--readerSettings-theme);
|
||||
color: var(--readerSettings-textColor);
|
||||
font-size: 1rem;
|
||||
@@ -198,7 +277,13 @@ img {
|
||||
}
|
||||
|
||||
.reader-footer-item {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
}
|
||||
.reader-footer-item:first-child {
|
||||
text-align: left;
|
||||
}
|
||||
.reader-footer-item:last-child {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
tts.highlight {
|
||||
|
||||
@@ -36,6 +36,7 @@ class Reader {
|
||||
),
|
||||
);
|
||||
this.chapter = document.querySelector('chapter');
|
||||
this.rawHTML = this.chapter.innerHTML;
|
||||
this.chapterHeight = this.chapter.scrollHeight + this.paddingTop;
|
||||
this.layoutHeight = window.innerHeight;
|
||||
this.pluginId = this.chapter.getAttribute('data-plugin-id');
|
||||
@@ -54,11 +55,13 @@ class Reader {
|
||||
this.time.innerText = new Date().toLocaleTimeString(undefined, {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
});
|
||||
this.timeInterval = setInterval(() => {
|
||||
this.time.innerText = new Date().toLocaleTimeString(undefined, {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
});
|
||||
}, 60000);
|
||||
this.updateBatteryLevel(batteryLevel);
|
||||
@@ -110,6 +113,13 @@ class Reader {
|
||||
updateGeneralSettings = settings => {
|
||||
this.showScrollPercentage = settings.showScrollPercentage;
|
||||
this.showBatteryAndTime = settings.showBatteryAndTime;
|
||||
this.verticalSeekbar = settings.verticalSeekbar;
|
||||
this.bionicReading = settings.bionicReading;
|
||||
if (this.bionicReading) {
|
||||
this.chapter.innerHTML = textVide.textVide(this.rawHTML);
|
||||
} else {
|
||||
this.chapter.innerHTML = this.rawHTML;
|
||||
}
|
||||
if (settings.swipeGestures) {
|
||||
swipeHandler.enable();
|
||||
} else {
|
||||
@@ -132,6 +142,15 @@ class Reader {
|
||||
this.time.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
if (!this.verticalSeekbar) {
|
||||
toolWrapper.$.classList.add('horizontal');
|
||||
} else {
|
||||
toolWrapper.$.classList.remove('horizontal');
|
||||
}
|
||||
if (scrollHandler) {
|
||||
scrollHandler.refresh();
|
||||
scrollHandler.update();
|
||||
}
|
||||
};
|
||||
updateBatteryLevel = level => {
|
||||
this.battery.innerText = Math.ceil(level * 100) + '%';
|
||||
@@ -194,7 +213,11 @@ class ScrollHandler {
|
||||
this.thumb.ontouchend = () => (this.lock = false);
|
||||
this.thumb.ontouchmove = e => {
|
||||
const ratio =
|
||||
(e.changedTouches[0].clientY - this.sliderOffsetY) / this.sliderHeight;
|
||||
((this.reader.verticalSeekbar
|
||||
? e.changedTouches[0].clientY
|
||||
: e.changedTouches[0].clientX) -
|
||||
this.sliderOffsetY) /
|
||||
this.sliderHeight;
|
||||
this.update(ratio < 0 ? 0 : ratio);
|
||||
};
|
||||
}
|
||||
@@ -208,7 +231,13 @@ class ScrollHandler {
|
||||
}
|
||||
const percentage = parseInt(ratio * 100);
|
||||
if (this.toolWrapper.visible) {
|
||||
this.progress.style.height = percentage + '%';
|
||||
if (this.reader.verticalSeekbar) {
|
||||
this.progress.style.height = percentage + '%';
|
||||
this.progress.style.width = '100%';
|
||||
} else {
|
||||
this.progress.style.height = '100%';
|
||||
this.progress.style.width = percentage + '%';
|
||||
}
|
||||
this.percentage.innerText = percentage;
|
||||
}
|
||||
if (this.lock) {
|
||||
@@ -222,8 +251,13 @@ class ScrollHandler {
|
||||
}
|
||||
};
|
||||
refresh = () => {
|
||||
this.sliderHeight = this.slider.clientHeight;
|
||||
this.sliderOffsetY = this.slider.getBoundingClientRect().top;
|
||||
if (this.reader.verticalSeekbar) {
|
||||
this.sliderHeight = this.slider.clientHeight;
|
||||
this.sliderOffsetY = this.slider.getBoundingClientRect().top;
|
||||
} else {
|
||||
this.sliderHeight = this.slider.clientWidth;
|
||||
this.sliderOffsetY = this.slider.getBoundingClientRect().left;
|
||||
}
|
||||
};
|
||||
onShow = () => {
|
||||
this.reader.refresh();
|
||||
@@ -243,12 +277,26 @@ class SwipeHandler {
|
||||
this.initialY = e.changedTouches[0].screenY;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {TouchEvent} e
|
||||
*/
|
||||
touchEndHandler = e => {
|
||||
let diffX = e.changedTouches[0].screenX - this.initialX;
|
||||
let diffY = e.changedTouches[0].screenY - this.initialY;
|
||||
if (Math.abs(diffX) > Math.abs(diffY) && Math.abs(diffX) > 10) {
|
||||
e.preventDefault();
|
||||
reader.post({ type: diffX < 0 ? 'next' : 'prev' });
|
||||
if (
|
||||
e.target.id?.startsWith('scrollbar') ||
|
||||
e.target.id === 'Image-Modal-img'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (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' });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
var textVide = (function (L) {
|
||||
'use strict';
|
||||
const f = t => t == null || t === '',
|
||||
h = (t, n) =>
|
||||
Object.keys(t).reduce((e, s) => (n(e[s]) && delete e[s], e), t),
|
||||
I = (t, n) => ({ ...n, ...h(t, f) }),
|
||||
_ = ['<b>', '</b>'],
|
||||
y = t =>
|
||||
I(t, {
|
||||
sep: _,
|
||||
fixationPoint: 1,
|
||||
ignoreHtmlTag: !0,
|
||||
ignoreHtmlEntity: !0,
|
||||
}),
|
||||
u = [
|
||||
[0, 4, 12, 17, 24, 29, 35, 42, 48],
|
||||
[1, 2, 7, 10, 13, 14, 19, 22, 25, 28, 31, 34, 37, 40, 43, 46, 49],
|
||||
[
|
||||
1, 2, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31, 33, 35, 37,
|
||||
39, 41, 43, 45, 47, 49,
|
||||
],
|
||||
[
|
||||
0, 2, 4, 5, 6, 8, 9, 11, 14, 15, 17, 18, 20, 0, 21, 23, 24, 26, 27, 29,
|
||||
30, 32, 33, 35, 36, 38, 39, 41, 42, 44, 45, 47, 48,
|
||||
],
|
||||
[
|
||||
0, 2, 3, 5, 6, 7, 8, 10, 11, 12, 14, 15, 17, 19, 20, 21, 23, 24, 25, 26,
|
||||
28, 29, 30, 32, 33, 34, 35, 37, 38, 39, 41, 42, 43, 44, 46, 47, 48,
|
||||
],
|
||||
],
|
||||
A = (t, n) => {
|
||||
const { length: i } = t,
|
||||
e = u[n - 1] ?? u[0],
|
||||
s = e.findIndex(c => i <= c);
|
||||
let o = i - s;
|
||||
return s === -1 && (o = i - e.length), Math.max(o, 0);
|
||||
},
|
||||
R = (t, n) =>
|
||||
typeof n === 'string' ? `${n}${t}${n}` : `${n[0]}${t}${n[1]}`,
|
||||
m = t =>
|
||||
Array.from(t).map(n => {
|
||||
const i = n.index,
|
||||
[e] = n,
|
||||
{ length: s } = e;
|
||||
return [i, i + s - 1];
|
||||
}),
|
||||
H = /(<!--[\s\S]*?-->)|(<[^>]*>)/g,
|
||||
N = t => {
|
||||
const n = t.matchAll(H),
|
||||
e = m(n).reverse();
|
||||
return s => {
|
||||
const o = s.index,
|
||||
c = e.find(([r]) => o > r);
|
||||
if (!c) {
|
||||
return !1;
|
||||
}
|
||||
const [, a] = c;
|
||||
return o < a;
|
||||
};
|
||||
},
|
||||
O = /(&[\w#]+;)/g,
|
||||
M = t => {
|
||||
const n = t.matchAll(O),
|
||||
e = m(n).reverse();
|
||||
return s => {
|
||||
const o = s.index,
|
||||
c = e.find(([r]) => o > r);
|
||||
if (!c) {
|
||||
return !1;
|
||||
}
|
||||
const [, a] = c;
|
||||
return o < a;
|
||||
};
|
||||
},
|
||||
x = /(\p{L}|\p{Nd})*\p{L}(\p{L}|\p{Nd})*/gu,
|
||||
F = (t, n = {}) => {
|
||||
if (!(t != null && t.length)) {
|
||||
return '';
|
||||
}
|
||||
const {
|
||||
fixationPoint: i,
|
||||
sep: e,
|
||||
ignoreHtmlTag: s,
|
||||
ignoreHtmlEntity: o,
|
||||
} = y(n),
|
||||
c = Array.from(t.matchAll(x));
|
||||
let a = '',
|
||||
l = 0,
|
||||
r;
|
||||
s && (r = N(t));
|
||||
let d;
|
||||
o && (d = M(t));
|
||||
for (const g of c) {
|
||||
if ((r == null ? void 0 : r(g)) || (d == null ? void 0 : d(g))) {
|
||||
continue;
|
||||
}
|
||||
const [D] = g,
|
||||
T = g.index,
|
||||
E = T + A(D, i),
|
||||
U = t.slice(l, T);
|
||||
(a += U), T !== E && (a += R(t.slice(T, E), e)), (l = E);
|
||||
}
|
||||
const G = t.slice(l);
|
||||
return a + G;
|
||||
};
|
||||
return (
|
||||
(L.textVide = F),
|
||||
Object.defineProperty(L, Symbol.toStringTag, { value: 'Module' }),
|
||||
L
|
||||
);
|
||||
})({});
|
||||
@@ -41,7 +41,7 @@ export const getLibraryNovelsFromDb = (
|
||||
let getLibraryNovelsQuery = 'SELECT * FROM Novel WHERE inLibrary = 1';
|
||||
|
||||
if (onlyOngoingNovels) {
|
||||
getLibraryNovelsQuery += " AND status NOT LIKE 'Completed'";
|
||||
getLibraryNovelsQuery += " AND status = 'Ongoing'";
|
||||
}
|
||||
|
||||
return new Promise(resolve =>
|
||||
|
||||
@@ -81,6 +81,7 @@ export interface ChapterGeneralSettings {
|
||||
autoScrollOffset: number | null;
|
||||
verticalSeekbar: boolean;
|
||||
removeExtraParagraphSpacing: boolean;
|
||||
bionicReading: boolean;
|
||||
}
|
||||
|
||||
export interface ReaderTheme {
|
||||
@@ -158,6 +159,7 @@ export const initialChapterGeneralSettings: ChapterGeneralSettings = {
|
||||
autoScrollOffset: null,
|
||||
verticalSeekbar: true,
|
||||
removeExtraParagraphSpacing: false,
|
||||
bionicReading: false,
|
||||
};
|
||||
|
||||
export const initialChapterReaderSettings: ChapterReaderSettings = {
|
||||
|
||||
@@ -33,12 +33,13 @@ const GeneralTab: React.FC = () => {
|
||||
const {
|
||||
fullScreenMode,
|
||||
autoScroll,
|
||||
// verticalSeekbar,
|
||||
verticalSeekbar,
|
||||
showBatteryAndTime,
|
||||
showScrollPercentage,
|
||||
useVolumeButtons,
|
||||
swipeGestures,
|
||||
removeExtraParagraphSpacing,
|
||||
bionicReading,
|
||||
setChapterGeneralSettings,
|
||||
} = useChapterGeneralSettings();
|
||||
|
||||
@@ -58,14 +59,14 @@ const GeneralTab: React.FC = () => {
|
||||
value={autoScroll}
|
||||
theme={theme}
|
||||
/>
|
||||
{/* <ReaderSheetPreferenceItem
|
||||
<ReaderSheetPreferenceItem
|
||||
label={getString('readerScreen.bottomSheet.verticalSeekbar')}
|
||||
onPress={() =>
|
||||
setChapterGeneralSettings({ verticalSeekbar: !verticalSeekbar })
|
||||
}
|
||||
value={verticalSeekbar}
|
||||
theme={theme}
|
||||
/> */}
|
||||
/>
|
||||
<ReaderSheetPreferenceItem
|
||||
label={getString('readerScreen.bottomSheet.showBatteryAndTime')}
|
||||
onPress={() =>
|
||||
@@ -110,6 +111,14 @@ const GeneralTab: React.FC = () => {
|
||||
value={useVolumeButtons}
|
||||
theme={theme}
|
||||
/>
|
||||
<ReaderSheetPreferenceItem
|
||||
label={getString('readerScreen.bottomSheet.bionicReading')}
|
||||
onPress={() =>
|
||||
setChapterGeneralSettings({ bionicReading: !bionicReading })
|
||||
}
|
||||
value={bionicReading}
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -75,7 +75,13 @@ const WebViewReader: FC<WebViewReaderProps> = props => {
|
||||
initialChapterReaderSettings,
|
||||
[],
|
||||
);
|
||||
const { showScrollPercentage, swipeGestures, showBatteryAndTime } = useMemo(
|
||||
const {
|
||||
showScrollPercentage,
|
||||
swipeGestures,
|
||||
showBatteryAndTime,
|
||||
verticalSeekbar,
|
||||
bionicReading,
|
||||
} = useMemo(
|
||||
() =>
|
||||
getMMKVObject<ChapterGeneralSettings>(CHAPTER_GENERAL_SETTINGS) ||
|
||||
initialChapterGeneralSettings,
|
||||
@@ -223,6 +229,8 @@ const WebViewReader: FC<WebViewReaderProps> = props => {
|
||||
showScrollPercentage: ${showScrollPercentage},
|
||||
swipeGestures: ${swipeGestures},
|
||||
showBatteryAndTime: ${showBatteryAndTime},
|
||||
verticalSeekbar: ${verticalSeekbar},
|
||||
bionicReading: ${bionicReading},
|
||||
}
|
||||
var batteryLevel = ${batteryLevel};
|
||||
var autoSaveInterval = 2222;
|
||||
@@ -269,6 +277,7 @@ const WebViewReader: FC<WebViewReaderProps> = props => {
|
||||
</div>`
|
||||
}
|
||||
</body>
|
||||
<script src="${assetsUriPrefix}/js/text-vibe.js"></script>
|
||||
<script src="${assetsUriPrefix}/js/index.js"></script>
|
||||
<script>
|
||||
async function fn(){
|
||||
|
||||
@@ -20,11 +20,12 @@ const GeneralSettings: React.FC = () => {
|
||||
|
||||
const {
|
||||
useVolumeButtons = false,
|
||||
// verticalSeekbar = true,
|
||||
verticalSeekbar = true,
|
||||
swipeGestures = false,
|
||||
autoScroll = false,
|
||||
autoScrollInterval = 10,
|
||||
autoScrollOffset = null,
|
||||
bionicReading = false,
|
||||
setChapterGeneralSettings,
|
||||
} = useChapterGeneralSettings();
|
||||
|
||||
@@ -41,17 +42,15 @@ const GeneralSettings: React.FC = () => {
|
||||
</List.SubHeader>
|
||||
|
||||
{/* will add this later xD */}
|
||||
{/* <SettingSwitch
|
||||
<SettingSwitch
|
||||
label={getString('readerScreen.bottomSheet.verticalSeekbar')}
|
||||
description={getString(
|
||||
'readerSettings.verticalSeekbarDesc',
|
||||
)}
|
||||
description={getString('readerSettings.verticalSeekbarDesc')}
|
||||
value={verticalSeekbar}
|
||||
onPress={() =>
|
||||
setChapterGeneralSettings({ verticalSeekbar: !verticalSeekbar })
|
||||
}
|
||||
theme={theme}
|
||||
/> */}
|
||||
/>
|
||||
<SettingSwitch
|
||||
label={getString('readerScreen.bottomSheet.volumeButtonsScroll')}
|
||||
value={useVolumeButtons}
|
||||
@@ -68,6 +67,14 @@ const GeneralSettings: React.FC = () => {
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<SettingSwitch
|
||||
label={getString('readerScreen.bottomSheet.bionicReading')}
|
||||
value={bionicReading}
|
||||
onPress={() =>
|
||||
setChapterGeneralSettings({ bionicReading: !bionicReading })
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<SettingSwitch
|
||||
label={getString('readerScreen.bottomSheet.autoscroll')}
|
||||
value={autoScroll}
|
||||
|
||||
@@ -48,8 +48,12 @@ const SettingsReaderScreen = () => {
|
||||
const layoutHeight = Dimensions.get('window').height;
|
||||
const batteryLevel = useBatteryLevel();
|
||||
const readerSettings = useChapterReaderSettings();
|
||||
const { showScrollPercentage, showBatteryAndTime } =
|
||||
useChapterGeneralSettings();
|
||||
const {
|
||||
showScrollPercentage,
|
||||
showBatteryAndTime,
|
||||
verticalSeekbar,
|
||||
bionicReading,
|
||||
} = useChapterGeneralSettings();
|
||||
const READER_HEIGHT = 280;
|
||||
const assetsUriPrefix = useMemo(
|
||||
() => (__DEV__ ? 'http://localhost:8081/assets' : 'file:///android_asset'),
|
||||
@@ -165,6 +169,8 @@ const SettingsReaderScreen = () => {
|
||||
showScrollPercentage: ${showScrollPercentage},
|
||||
swipeGestures: false,
|
||||
showBatteryAndTime: ${showBatteryAndTime},
|
||||
verticalSeekbar: ${verticalSeekbar},
|
||||
bionicReading: ${bionicReading},
|
||||
}
|
||||
var batteryLevel = ${batteryLevel};
|
||||
var autoSaveInterval = 2222;
|
||||
@@ -193,6 +199,7 @@ const SettingsReaderScreen = () => {
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
<script src="${assetsUriPrefix}/js/text-vibe.js"></script>
|
||||
<script src="${assetsUriPrefix}/js/index.js"></script>
|
||||
<script>
|
||||
async function fn(){
|
||||
|
||||
@@ -23,6 +23,7 @@ import EpubUtil from '@native/EpubUtil';
|
||||
interface TaskData {
|
||||
delay: number;
|
||||
sourceUri: string;
|
||||
filename?: string;
|
||||
}
|
||||
|
||||
const insertLocalNovel = (
|
||||
@@ -167,6 +168,9 @@ const importEpubAction = async (taskData?: TaskData) => {
|
||||
await sleep(taskData.delay);
|
||||
|
||||
const novel = await EpubUtil.parseNovelAndChapters(epubDirPath);
|
||||
if (!novel.name) {
|
||||
novel.name = taskData.filename?.replace('.epub', '') || 'Untitled';
|
||||
}
|
||||
const novelId = await insertLocalNovel(
|
||||
novel.name,
|
||||
epubDirPath + novel.name, // temporary
|
||||
@@ -278,7 +282,11 @@ export const importEpub = async () => {
|
||||
taskDesc: getString('common.parsing'),
|
||||
taskIcon: { name: 'notification_icon', type: 'drawable' },
|
||||
color: '#00adb5',
|
||||
parameters: { delay: 500, sourceUri: epubFile.assets[0].uri },
|
||||
parameters: {
|
||||
delay: 500,
|
||||
sourceUri: epubFile.assets[0].uri,
|
||||
filename: epubFile.assets[0].name,
|
||||
},
|
||||
progressBar: { value: 0, max: 4 },
|
||||
});
|
||||
} catch (e: any) {
|
||||
|
||||
@@ -10,43 +10,129 @@ import { getMMKVObject, setMMKVObject } from '@utils/mmkv/mmkv';
|
||||
import { NOVEL_PAGE_UPDATES_PREFIX } from '@hooks/persisted/useNovel';
|
||||
const db = SQLite.openDatabase('lnreader.db');
|
||||
|
||||
const updateNovelMetadata = async (
|
||||
const updateNovelMetadata = (
|
||||
pluginId: string,
|
||||
novelId: number,
|
||||
novel: SourceNovel,
|
||||
) => {
|
||||
let { name, cover, summary, author, artist, genres, status, totalPages } =
|
||||
novel;
|
||||
const novelDir = NovelDownloadFolder + '/' + pluginId + '/' + novelId;
|
||||
if (cover) {
|
||||
const novelCoverUri = 'file://' + novelDir + '/cover.png';
|
||||
await fetchImage(pluginId, cover).then(base64 => {
|
||||
if (base64) {
|
||||
cover = novelCoverUri;
|
||||
return RNFS.writeFile(novelCoverUri, base64, 'base64');
|
||||
}
|
||||
return new Promise(async (resolve, reject) => {
|
||||
let { name, cover, summary, author, artist, genres, status, totalPages } =
|
||||
novel;
|
||||
const novelDir = NovelDownloadFolder + '/' + pluginId + '/' + novelId;
|
||||
if (!(await RNFS.exists(novelDir))) {
|
||||
await RNFS.mkdir(novelDir);
|
||||
}
|
||||
if (cover) {
|
||||
const novelCoverUri = 'file://' + novelDir + '/cover.png';
|
||||
await fetchImage(pluginId, cover)
|
||||
.then(base64 => {
|
||||
if (base64) {
|
||||
cover = novelCoverUri;
|
||||
return RNFS.writeFile(novelCoverUri, base64, 'base64');
|
||||
}
|
||||
})
|
||||
.catch(reject);
|
||||
cover += '?' + Date.now();
|
||||
}
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
`UPDATE Novel SET
|
||||
name = ?, cover = ?, summary = ?, author = ?, artist = ?,
|
||||
genres = ?, status = ?, totalPages = ?
|
||||
WHERE id = ?
|
||||
`,
|
||||
[
|
||||
name,
|
||||
cover || null,
|
||||
summary || null,
|
||||
author || 'unknown',
|
||||
artist || null,
|
||||
genres || null,
|
||||
status || null,
|
||||
totalPages || 0,
|
||||
novelId,
|
||||
],
|
||||
() => resolve(null),
|
||||
(txObj, error) => {
|
||||
reject(error);
|
||||
return false;
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const updateNovelChapters = (
|
||||
pluginId: string,
|
||||
novelId: number,
|
||||
novel: SourceNovel,
|
||||
downloadNewChapters?: boolean,
|
||||
) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.transaction(async tx => {
|
||||
const newChapters: { id: number; path: string }[] = [];
|
||||
for (let position = 0; position < novel.chapters.length; position++) {
|
||||
const { name, path, releaseTime, page } = novel.chapters[position];
|
||||
tx.executeSql(
|
||||
`
|
||||
INSERT INTO Chapter (path, name, releaseTime, novelId, updatedTime, page, position)
|
||||
SELECT ?, ?, ?, ?, datetime('now','localtime'), ?, ?
|
||||
WHERE NOT EXISTS (SELECT id FROM Chapter WHERE path = ? AND novelId = ?);
|
||||
`,
|
||||
[
|
||||
path,
|
||||
name,
|
||||
releaseTime || null,
|
||||
novelId,
|
||||
page || '1',
|
||||
position,
|
||||
path,
|
||||
novelId,
|
||||
],
|
||||
(txObj, { insertId }) => {
|
||||
if (insertId) {
|
||||
if (downloadNewChapters) {
|
||||
newChapters.push({ id: insertId, path: path });
|
||||
}
|
||||
} else {
|
||||
tx.executeSql(
|
||||
`
|
||||
UPDATE Chapter SET
|
||||
name = ?, releaseTime = ?, updatedTime = datetime('now','localtime'), page = ?, position = ?
|
||||
WHERE path = ? AND novelId = ? AND (name != ? OR releaseTime != ? OR page != ?);
|
||||
`,
|
||||
[
|
||||
name,
|
||||
releaseTime || null,
|
||||
page || '1',
|
||||
position,
|
||||
path,
|
||||
novelId,
|
||||
name,
|
||||
releaseTime || null,
|
||||
page || '1',
|
||||
],
|
||||
undefined,
|
||||
(txObj, error) => {
|
||||
reject(error);
|
||||
return false;
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
(txObj, error) => {
|
||||
reject(error);
|
||||
return false;
|
||||
},
|
||||
);
|
||||
}
|
||||
if (downloadNewChapters) {
|
||||
for (const { id, path } of newChapters) {
|
||||
await downloadChapter(pluginId, novelId, id, path).catch(reject);
|
||||
}
|
||||
}
|
||||
resolve(null);
|
||||
});
|
||||
cover += '?' + Date.now();
|
||||
}
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
`UPDATE Novel SET
|
||||
name = ?, cover = ?, summary = ?, author = ?, artist = ?,
|
||||
genres = ?, status = ?, totalPages = ?
|
||||
WHERE id = ?
|
||||
`,
|
||||
[
|
||||
name,
|
||||
cover || null,
|
||||
summary || null,
|
||||
author || 'unknown',
|
||||
artist || null,
|
||||
genres || null,
|
||||
status || null,
|
||||
totalPages || 0,
|
||||
novelId,
|
||||
],
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -67,46 +153,9 @@ const updateNovel = async (
|
||||
const { downloadNewChapters, refreshNovelMetadata } = options;
|
||||
const novel = await fetchNovel(pluginId, novelPath);
|
||||
if (refreshNovelMetadata) {
|
||||
updateNovelMetadata(pluginId, novelId, novel);
|
||||
await updateNovelMetadata(pluginId, novelId, novel);
|
||||
}
|
||||
db.transaction(tx => {
|
||||
novel.chapters.forEach(chapter => {
|
||||
const { name, path, releaseTime, page } = chapter;
|
||||
tx.executeSql(
|
||||
`
|
||||
INSERT INTO Chapter (path, name, releaseTime, novelId, updatedTime, page)
|
||||
SELECT ?, ?, ?, ?, datetime('now','localtime'), ?
|
||||
WHERE NOT EXISTS (SELECT id FROM Chapter WHERE path = ? AND novelId = ?);
|
||||
`,
|
||||
[path, name, releaseTime || null, novelId, page || '1', path, novelId],
|
||||
(txObj, { insertId }) => {
|
||||
if (insertId) {
|
||||
if (downloadNewChapters) {
|
||||
downloadChapter(pluginId, novelId, insertId, path);
|
||||
}
|
||||
} else {
|
||||
tx.executeSql(
|
||||
`
|
||||
UPDATE Chapter SET
|
||||
name = ?, releaseTime = ?, updatedTime = datetime('now','localtime'), page = ?
|
||||
WHERE path = ? AND novelId = ? AND (name != ? OR releaseTime != ? OR page != ?);
|
||||
`,
|
||||
[
|
||||
name,
|
||||
releaseTime || null,
|
||||
page || '1',
|
||||
path,
|
||||
novelId,
|
||||
name,
|
||||
releaseTime || null,
|
||||
page || '1',
|
||||
],
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
await updateNovelChapters(pluginId, novelId, novel, downloadNewChapters);
|
||||
const key = `${NOVEL_PAGE_UPDATES_PREFIX}_${novelId}`;
|
||||
const hasUpdates = getMMKVObject<boolean[]>(key);
|
||||
if (hasUpdates) {
|
||||
|
||||
@@ -19,8 +19,63 @@ import { getString } from '@strings/translations';
|
||||
|
||||
interface TaskData {
|
||||
delay: number;
|
||||
libraryNovels: LibraryNovelInfo[];
|
||||
options: UpdateNovelOptions;
|
||||
}
|
||||
|
||||
const libraryUpdateBackgroundAction = async (taskData?: TaskData) => {
|
||||
try {
|
||||
if (!taskData) {
|
||||
return;
|
||||
}
|
||||
const { delay, libraryNovels, options } = taskData;
|
||||
MMKVStorage.set(BACKGROUND_ACTION, BackgoundAction.UPDATE_LIBRARY);
|
||||
MMKVStorage.set(LAST_UPDATE_TIME, dayjs().format('YYYY-MM-DD HH:mm:ss'));
|
||||
|
||||
for (
|
||||
let i = 0;
|
||||
BackgroundService.isRunning() && i < libraryNovels.length;
|
||||
i++
|
||||
) {
|
||||
try {
|
||||
if (BackgroundService.isRunning()) {
|
||||
await updateNovel(
|
||||
libraryNovels[i].pluginId,
|
||||
libraryNovels[i].path,
|
||||
libraryNovels[i].id,
|
||||
options,
|
||||
);
|
||||
|
||||
/**
|
||||
* Update notification
|
||||
*/
|
||||
await BackgroundService.updateNotification({
|
||||
taskTitle: '(' + (i + 1) + '/' + libraryNovels.length + ')',
|
||||
taskDesc: libraryNovels[i].name,
|
||||
progressBar: { max: libraryNovels.length, value: i + 1 },
|
||||
});
|
||||
await sleep(delay ?? 1000);
|
||||
}
|
||||
} catch (error: any) {
|
||||
showToast(libraryNovels[i].name + ': ' + error.message);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
MMKVStorage.delete(BACKGROUND_ACTION);
|
||||
Notifications.scheduleNotificationAsync({
|
||||
content: {
|
||||
title: getString('updatesScreen.libraryUpdated'),
|
||||
body: getString('updatesScreen.novelsUpdated', {
|
||||
num: taskData?.libraryNovels.length || 0,
|
||||
}),
|
||||
},
|
||||
trigger: null,
|
||||
});
|
||||
BackgroundService.stop();
|
||||
}
|
||||
};
|
||||
|
||||
const updateLibrary = async (categoryId?: number) => {
|
||||
const { downloadNewChapters, refreshNovelMetadata, onlyUpdateOngoingNovels } =
|
||||
getMMKVObject<AppSettings>(APP_SETTINGS) || {};
|
||||
@@ -30,10 +85,11 @@ const updateLibrary = async (categoryId?: number) => {
|
||||
};
|
||||
|
||||
let libraryNovels: LibraryNovelInfo[] = [];
|
||||
|
||||
if (categoryId) {
|
||||
libraryNovels = await getLibraryWithCategory({
|
||||
filter: `categoryId = ${categoryId}`,
|
||||
filter:
|
||||
`categoryId = ${categoryId}` +
|
||||
(onlyUpdateOngoingNovels ? " AND status = 'Ongoing'" : ''),
|
||||
});
|
||||
} else {
|
||||
libraryNovels = (await getLibraryNovelsFromDb(
|
||||
@@ -47,80 +103,23 @@ const updateLibrary = async (categoryId?: number) => {
|
||||
taskDesc: '(0/' + libraryNovels.length + ')',
|
||||
taskIcon: { name: 'notification_icon', type: 'drawable' },
|
||||
color: '#00adb5',
|
||||
parameters: { delay: 1000 },
|
||||
parameters: { delay: 1000, libraryNovels, options },
|
||||
linkingURI: 'lnreader://updates',
|
||||
progressBar: { max: libraryNovels.length, value: 0 },
|
||||
};
|
||||
|
||||
const libraryUpdateBackgroundAction = async (taskData?: TaskData) => {
|
||||
try {
|
||||
MMKVStorage.set(BACKGROUND_ACTION, BackgoundAction.UPDATE_LIBRARY);
|
||||
MMKVStorage.set(LAST_UPDATE_TIME, dayjs().format('YYYY-MM-DD HH:mm:ss'));
|
||||
for (
|
||||
let i = 0;
|
||||
BackgroundService.isRunning() && i < libraryNovels.length;
|
||||
i++
|
||||
) {
|
||||
try {
|
||||
if (BackgroundService.isRunning()) {
|
||||
await updateNovel(
|
||||
libraryNovels[i].pluginId,
|
||||
libraryNovels[i].path,
|
||||
libraryNovels[i].id,
|
||||
options,
|
||||
);
|
||||
|
||||
/**
|
||||
* Update notification
|
||||
*/
|
||||
await BackgroundService.updateNotification({
|
||||
taskTitle: libraryNovels[i].name,
|
||||
taskDesc: '(' + (i + 1) + '/' + libraryNovels.length + ')',
|
||||
progressBar: { max: libraryNovels.length, value: i + 1 },
|
||||
});
|
||||
|
||||
/**
|
||||
* When updating library is finished
|
||||
*/
|
||||
if (libraryNovels.length === i + 1) {
|
||||
Notifications.scheduleNotificationAsync({
|
||||
content: {
|
||||
title: getString('updatesScreen.libraryUpdated'),
|
||||
body: getString('updatesScreen.novelsUpdated', {
|
||||
num: libraryNovels.length,
|
||||
}),
|
||||
},
|
||||
trigger: null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const nextNovelIndex = i + 1;
|
||||
|
||||
if (
|
||||
nextNovelIndex in libraryNovels &&
|
||||
libraryNovels[nextNovelIndex].pluginId ===
|
||||
libraryNovels[i].pluginId
|
||||
) {
|
||||
await sleep(taskData?.delay ?? 0);
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
showToast(libraryNovels[i].name + ': ' + error.message);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
MMKVStorage.delete(BACKGROUND_ACTION);
|
||||
BackgroundService.stop();
|
||||
}
|
||||
};
|
||||
|
||||
if (libraryNovels.length > 0 && !MMKVStorage.getString(BACKGROUND_ACTION)) {
|
||||
const anotherBackground = MMKVStorage.getString(BACKGROUND_ACTION);
|
||||
if (libraryNovels.length > 0 && !anotherBackground) {
|
||||
await BackgroundService.start<TaskData>(
|
||||
libraryUpdateBackgroundAction,
|
||||
notificationOptions,
|
||||
);
|
||||
} else {
|
||||
if (anotherBackground) {
|
||||
showToast(getString('browseScreen.migration.anotherServiceIsRunning'));
|
||||
} else {
|
||||
showToast("There's no novel to be updated");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user