Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9ef8cab475 | |||
| 7ea8ebacd1 | |||
| e171dfebc1 | |||
| 9f6ffc23bf | |||
| 8906253527 | |||
| 817e4dc9d1 |
@@ -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
|
||||
);
|
||||
})({});
|
||||
@@ -0,0 +1,163 @@
|
||||
package com.rajarsheechatterjee.EpubUtil;
|
||||
|
||||
import android.util.Xml;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.facebook.react.bridge.Promise;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.bridge.WritableArray;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.bridge.WritableNativeArray;
|
||||
import com.facebook.react.bridge.WritableNativeMap;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Objects;
|
||||
|
||||
public class EpubUtil extends ReactContextBaseJavaModule {
|
||||
EpubUtil(ReactApplicationContext context){super(context);}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getName() {
|
||||
return "EpubUtil";
|
||||
}
|
||||
|
||||
private XmlPullParser initParse(File file) throws XmlPullParserException, IOException {
|
||||
XmlPullParser parser = Xml.newPullParser();
|
||||
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
|
||||
parser.setInput(new FileInputStream(file), null);
|
||||
parser.nextTag();
|
||||
return parser;
|
||||
}
|
||||
|
||||
private String readText(XmlPullParser parser) throws IOException, XmlPullParserException {
|
||||
String result = "";
|
||||
if (parser.next() == XmlPullParser.TEXT) {
|
||||
result = parser.getText();
|
||||
parser.nextTag();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private String cleanUrl(String url){
|
||||
if(url != null){
|
||||
return url.replaceFirst("#[^.]+?$", "");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void parseNovelAndChapters(String epubDirPath, Promise promise) {
|
||||
try {
|
||||
File containerFile = new File(epubDirPath, "META-INF/container.xml");
|
||||
File contentMetaFile = new File(epubDirPath, getContentMetaFilePath(containerFile));
|
||||
String contentDir = contentMetaFile.getParent();
|
||||
|
||||
ReadableMap novel = getNovelMetadata(contentMetaFile, contentDir);
|
||||
promise.resolve(novel);
|
||||
} catch (XmlPullParserException | IOException e) {
|
||||
promise.reject(e.getCause());
|
||||
}
|
||||
}
|
||||
|
||||
private String getContentMetaFilePath(File file) throws XmlPullParserException, IOException {
|
||||
XmlPullParser parser = initParse(file);
|
||||
while (parser.next() != XmlPullParser.END_TAG){
|
||||
@Nullable String tag = parser.getName();
|
||||
if(tag != null && tag.equals("rootfile")) {
|
||||
return parser.getAttributeValue(null, "full-path");
|
||||
}
|
||||
}
|
||||
return "OEBPS/content.opf"; // default
|
||||
}
|
||||
private ReadableMap getNovelMetadata(File file, String contentDir) throws XmlPullParserException, IOException {
|
||||
WritableMap novel = new WritableNativeMap();
|
||||
WritableArray chapters = new WritableNativeArray();
|
||||
XmlPullParser parser = initParse(file);
|
||||
HashMap<String, String> refMap = new HashMap<>();
|
||||
HashMap<String, String> tocMap = new HashMap<>();
|
||||
File tocFile = new File(contentDir, "toc.ncx");
|
||||
if(tocFile.exists()){
|
||||
XmlPullParser tocParser = initParse(tocFile);
|
||||
String label = null;
|
||||
while (tocParser.next() != XmlPullParser.END_DOCUMENT){
|
||||
String tag = tocParser.getName();
|
||||
if(tag != null){
|
||||
if(tag.equals("text")){
|
||||
label = readText(tocParser);
|
||||
}else if(tag.equals("content")){
|
||||
String href = cleanUrl(tocParser.getAttributeValue(null, "src"));
|
||||
if(href != null){
|
||||
tocMap.put(href, label);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
String cover = null;
|
||||
while (parser.next() != XmlPullParser.END_DOCUMENT){
|
||||
@Nullable String tag = parser.getName();
|
||||
if(tag != null){
|
||||
switch (tag) {
|
||||
case "item": {
|
||||
String id = parser.getAttributeValue(null, "id");
|
||||
String href = parser.getAttributeValue(null, "href");
|
||||
if (id != null) {
|
||||
refMap.put(id, href);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "itemref": {
|
||||
String idRef = parser.getAttributeValue(null, "idref");
|
||||
String href = refMap.get(idRef);
|
||||
if (href != null) {
|
||||
WritableMap chapter = new WritableNativeMap();
|
||||
chapter.putString("path", contentDir + "/" + href);
|
||||
String name = tocMap.get(href);
|
||||
chapter.putString("name", name == null ? href : name);
|
||||
chapters.pushMap(chapter);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "title":
|
||||
novel.putString("name", readText(parser));
|
||||
break;
|
||||
case "creator":
|
||||
novel.putString("author", readText(parser));
|
||||
break;
|
||||
case "contributor":
|
||||
novel.putString("artist", readText(parser));
|
||||
break;
|
||||
case "description":
|
||||
novel.putString("summary", readText(parser));
|
||||
break;
|
||||
case "meta":
|
||||
String metaName = parser.getAttributeValue(null, "name");
|
||||
if(metaName != null && metaName.equals("cover")){
|
||||
cover = parser.getAttributeValue(null, "content");
|
||||
}
|
||||
break;
|
||||
}
|
||||
parser.next();
|
||||
}
|
||||
}
|
||||
if(cover != null){
|
||||
String coverPath = contentDir + "/" + refMap.get(cover);
|
||||
novel.putString("cover", coverPath);
|
||||
}
|
||||
novel.putArray("chapters", chapters);
|
||||
return novel;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.rajarsheechatterjee.EpubUtil;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.uimanager.ViewManager;
|
||||
import com.rajarsheechatterjee.ZipArchive.ZipArchive;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class EpubUtilPackage implements ReactPackage {
|
||||
@NonNull
|
||||
@Override
|
||||
public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactApplicationContext) {
|
||||
List<NativeModule> modules = new ArrayList<>();
|
||||
try {
|
||||
modules.add(new EpubUtil(reactApplicationContext));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return modules;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext reactApplicationContext) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
|
||||
import com.facebook.react.defaults.DefaultReactNativeHost;
|
||||
import com.facebook.soloader.SoLoader;
|
||||
|
||||
import com.rajarsheechatterjee.EpubUtil.EpubUtilPackage;
|
||||
import com.rajarsheechatterjee.NavigationBarColor.NavigationBarColorPackage;
|
||||
import com.rajarsheechatterjee.TextFile.TextFilePackage;
|
||||
import com.rajarsheechatterjee.VolumeButtonListener.VolumeButtonListenerPackage;
|
||||
@@ -38,6 +39,7 @@ public class MainApplication extends Application implements ReactApplication {
|
||||
packages.add(new VolumeButtonListenerPackage());
|
||||
packages.add(new ZipArchivePackage());
|
||||
packages.add(new TextFilePackage());
|
||||
packages.add(new EpubUtilPackage());
|
||||
return packages;
|
||||
}
|
||||
|
||||
|
||||
+42
-51
@@ -23,9 +23,6 @@ public class NavigationBarColorModule extends ReactContextBaseJavaModule {
|
||||
public static final String REACT_CLASS = "NavigationBarColor";
|
||||
private static final String ERROR_NO_ACTIVITY = "E_NO_ACTIVITY";
|
||||
private static final String ERROR_NO_ACTIVITY_MESSAGE = "Tried to change the navigation bar while not attached to an Activity";
|
||||
private static final String ERROR_API_LEVEL = "API_LEVEl";
|
||||
private static final String ERROR_API_LEVEL_MESSAGE = "Only Android Oreo and above is supported";
|
||||
private static ReactApplicationContext reactContext = null;
|
||||
private static final int UI_FLAG_HIDE_NAV_BAR = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION // hide nav bar
|
||||
@@ -33,7 +30,6 @@ public class NavigationBarColorModule extends ReactContextBaseJavaModule {
|
||||
|
||||
public NavigationBarColorModule(ReactApplicationContext context) {
|
||||
super(context);
|
||||
reactContext = context;
|
||||
}
|
||||
|
||||
public void setNavigationBarTheme(Activity activity, Boolean light) {
|
||||
@@ -66,61 +62,56 @@ public class NavigationBarColorModule extends ReactContextBaseJavaModule {
|
||||
@ReactMethod
|
||||
public void changeNavigationBarColor(final String color, final Boolean light, final Boolean animated, final Promise promise) {
|
||||
final WritableMap map = Arguments.createMap();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
if (getCurrentActivity() != null) {
|
||||
try {
|
||||
final Window window = getCurrentActivity().getWindow();
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (color.equals("transparent") || color.equals("translucent")) {
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
|
||||
if (color.equals("transparent")) {
|
||||
window.setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
|
||||
} else {
|
||||
window.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION, WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
|
||||
}
|
||||
setNavigationBarTheme(getCurrentActivity(), light);
|
||||
map.putBoolean("success", true);
|
||||
promise.resolve(map);
|
||||
return;
|
||||
if (getCurrentActivity() != null) {
|
||||
try {
|
||||
final Window window = getCurrentActivity().getWindow();
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (color.equals("transparent") || color.equals("translucent")) {
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
|
||||
if (color.equals("transparent")) {
|
||||
window.setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
|
||||
} else {
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
|
||||
}
|
||||
if (animated) {
|
||||
Integer colorFrom = window.getNavigationBarColor();
|
||||
Integer colorTo = Color.parseColor(String.valueOf(color));
|
||||
//window.setNavigationBarColor(colorTo);
|
||||
ValueAnimator colorAnimation = ValueAnimator.ofObject(new ArgbEvaluator(), colorFrom, colorTo);
|
||||
colorAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
|
||||
@Override
|
||||
public void onAnimationUpdate(ValueAnimator animator) {
|
||||
window.setNavigationBarColor((Integer) animator.getAnimatedValue());
|
||||
}
|
||||
});
|
||||
colorAnimation.start();
|
||||
} else {
|
||||
window.setNavigationBarColor(Color.parseColor(String.valueOf(color)));
|
||||
window.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION, WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
|
||||
}
|
||||
setNavigationBarTheme(getCurrentActivity(), light);
|
||||
WritableMap map = Arguments.createMap();
|
||||
map.putBoolean("success", true);
|
||||
promise.resolve(map);
|
||||
return;
|
||||
} else {
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
|
||||
}
|
||||
});
|
||||
} catch (IllegalViewOperationException e) {
|
||||
map.putBoolean("success", false);
|
||||
promise.reject("error", e);
|
||||
}
|
||||
|
||||
} else {
|
||||
promise.reject(ERROR_NO_ACTIVITY, new Throwable(ERROR_NO_ACTIVITY_MESSAGE));
|
||||
|
||||
if (animated) {
|
||||
Integer colorFrom = window.getNavigationBarColor();
|
||||
Integer colorTo = Color.parseColor(color);
|
||||
//window.setNavigationBarColor(colorTo);
|
||||
ValueAnimator colorAnimation = ValueAnimator.ofObject(new ArgbEvaluator(), colorFrom, colorTo);
|
||||
colorAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
|
||||
@Override
|
||||
public void onAnimationUpdate(ValueAnimator animator) {
|
||||
window.setNavigationBarColor((Integer) animator.getAnimatedValue());
|
||||
}
|
||||
});
|
||||
colorAnimation.start();
|
||||
} else {
|
||||
window.setNavigationBarColor(Color.parseColor(color));
|
||||
}
|
||||
setNavigationBarTheme(getCurrentActivity(), light);
|
||||
WritableMap map = Arguments.createMap();
|
||||
map.putBoolean("success", true);
|
||||
promise.resolve(map);
|
||||
}
|
||||
});
|
||||
} catch (IllegalViewOperationException e) {
|
||||
map.putBoolean("success", false);
|
||||
promise.reject("error", e);
|
||||
}
|
||||
|
||||
} else {
|
||||
promise.reject(ERROR_API_LEVEL, new Throwable(ERROR_API_LEVEL_MESSAGE));
|
||||
promise.reject(ERROR_NO_ACTIVITY, new Throwable(ERROR_NO_ACTIVITY_MESSAGE));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Generated
+144
-5
@@ -31,8 +31,10 @@
|
||||
"expo-speech": "~11.3.0",
|
||||
"expo-sqlite": "~11.3.3",
|
||||
"expo-web-browser": "~12.3.2",
|
||||
"htmlparser2": "^9.1.0",
|
||||
"i18n-js": "^3.8.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"protobufjs": "^7.2.6",
|
||||
"qs": "^6.11.2",
|
||||
"react": "18.2.0",
|
||||
"react-native": "0.72.10",
|
||||
@@ -4328,6 +4330,60 @@
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@protobufjs/aspromise": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
|
||||
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="
|
||||
},
|
||||
"node_modules/@protobufjs/base64": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
|
||||
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="
|
||||
},
|
||||
"node_modules/@protobufjs/codegen": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
|
||||
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="
|
||||
},
|
||||
"node_modules/@protobufjs/eventemitter": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
|
||||
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="
|
||||
},
|
||||
"node_modules/@protobufjs/fetch": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
|
||||
"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
|
||||
"dependencies": {
|
||||
"@protobufjs/aspromise": "^1.1.1",
|
||||
"@protobufjs/inquire": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@protobufjs/float": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
|
||||
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="
|
||||
},
|
||||
"node_modules/@protobufjs/inquire": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
|
||||
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="
|
||||
},
|
||||
"node_modules/@protobufjs/path": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
|
||||
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="
|
||||
},
|
||||
"node_modules/@protobufjs/pool": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
|
||||
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="
|
||||
},
|
||||
"node_modules/@protobufjs/utf8": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
|
||||
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
|
||||
},
|
||||
"node_modules/@react-native-community/cli": {
|
||||
"version": "11.3.10",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-11.3.10.tgz",
|
||||
@@ -6701,6 +6757,25 @@
|
||||
"htmlparser2": "^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/sanitize-html/node_modules/htmlparser2": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
|
||||
"integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
"https://github.com/fb55/htmlparser2?sponsor=1",
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.3",
|
||||
"domutils": "^3.0.1",
|
||||
"entities": "^4.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/scheduler": {
|
||||
"version": "0.16.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz",
|
||||
@@ -8218,6 +8293,24 @@
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
},
|
||||
"node_modules/cheerio/node_modules/htmlparser2": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
|
||||
"integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
|
||||
"funding": [
|
||||
"https://github.com/fb55/htmlparser2?sponsor=1",
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.3",
|
||||
"domutils": "^3.0.1",
|
||||
"entities": "^4.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/chownr": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
|
||||
@@ -10960,9 +11053,9 @@
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||
},
|
||||
"node_modules/htmlparser2": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
|
||||
"integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz",
|
||||
"integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==",
|
||||
"funding": [
|
||||
"https://github.com/fb55/htmlparser2?sponsor=1",
|
||||
{
|
||||
@@ -10973,8 +11066,8 @@
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.3",
|
||||
"domutils": "^3.0.1",
|
||||
"entities": "^4.4.0"
|
||||
"domutils": "^3.1.0",
|
||||
"entities": "^4.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
@@ -13462,6 +13555,11 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/long": {
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz",
|
||||
"integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q=="
|
||||
},
|
||||
"node_modules/loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
@@ -15675,6 +15773,29 @@
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
},
|
||||
"node_modules/protobufjs": {
|
||||
"version": "7.2.6",
|
||||
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.6.tgz",
|
||||
"integrity": "sha512-dgJaEDDL6x8ASUZ1YqWciTRrdOuYNzoOf27oHNfdyvKqHr5i0FV7FSLU+aIeFjyFgVxrpTOtQUi0BLLBymZaBw==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@protobufjs/aspromise": "^1.1.2",
|
||||
"@protobufjs/base64": "^1.1.2",
|
||||
"@protobufjs/codegen": "^2.0.4",
|
||||
"@protobufjs/eventemitter": "^1.1.0",
|
||||
"@protobufjs/fetch": "^1.1.0",
|
||||
"@protobufjs/float": "^1.0.2",
|
||||
"@protobufjs/inquire": "^1.1.0",
|
||||
"@protobufjs/path": "^1.1.2",
|
||||
"@protobufjs/pool": "^1.1.0",
|
||||
"@protobufjs/utf8": "^1.1.0",
|
||||
"@types/node": ">=13.7.0",
|
||||
"long": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pump": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
|
||||
@@ -16697,6 +16818,24 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sanitize-html/node_modules/htmlparser2": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
|
||||
"integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
|
||||
"funding": [
|
||||
"https://github.com/fb55/htmlparser2?sponsor=1",
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.3",
|
||||
"domutils": "^3.0.1",
|
||||
"entities": "^4.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sanitize-html/node_modules/is-plain-object": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
|
||||
|
||||
@@ -37,8 +37,10 @@
|
||||
"expo-speech": "~11.3.0",
|
||||
"expo-sqlite": "~11.3.3",
|
||||
"expo-web-browser": "~12.3.2",
|
||||
"htmlparser2": "^9.1.0",
|
||||
"i18n-js": "^3.8.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"protobufjs": "^7.2.6",
|
||||
"qs": "^6.11.2",
|
||||
"react": "18.2.0",
|
||||
"react-native": "0.72.10",
|
||||
|
||||
@@ -65,12 +65,8 @@ export const insertChapters = async (
|
||||
});
|
||||
};
|
||||
|
||||
const getPageChaptersQuery = (
|
||||
sort = 'ORDER BY position ASC',
|
||||
filter = '',
|
||||
page = '1',
|
||||
) =>
|
||||
`SELECT * FROM Chapter WHERE novelId = ? AND page = '${page}' ${filter} ${sort}`;
|
||||
const getPageChaptersQuery = (sort = 'ORDER BY position ASC', filter = '') =>
|
||||
`SELECT * FROM Chapter WHERE novelId = ? AND page = ? ${filter} ${sort}`;
|
||||
|
||||
export const getCustomPages = (
|
||||
novelId: number,
|
||||
@@ -108,8 +104,8 @@ export const getPageChapters = (
|
||||
return new Promise(resolve =>
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
getPageChaptersQuery(sort, filter, page),
|
||||
[novelId],
|
||||
getPageChaptersQuery(sort, filter),
|
||||
[novelId, page || '1'],
|
||||
(txObj, { rows }) => resolve((rows as any)._array),
|
||||
txnErrorCallback,
|
||||
);
|
||||
|
||||
@@ -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 =>
|
||||
|
||||
@@ -28,7 +28,6 @@ import { useCallback, useEffect, useState } from 'react';
|
||||
import { NovelDownloadFolder } from '@utils/constants/download';
|
||||
import * as RNFS from 'react-native-fs';
|
||||
import { getString } from '@strings/translations';
|
||||
import { ChapterItem } from '@plugins/types';
|
||||
import dayjs from 'dayjs';
|
||||
import { parseChapterNumber } from '@utils/parseChapterNumber';
|
||||
|
||||
@@ -326,6 +325,12 @@ export const useNovel = (novelPath: string, pluginId: string) => {
|
||||
if (!novel) {
|
||||
return;
|
||||
}
|
||||
if (sourceNovel.latestChapter) {
|
||||
setMMKVObject(
|
||||
`${NOVEL_LATEST_CHAPTER_PREFIX}_${novel.id}`,
|
||||
sourceNovel.latestChapter,
|
||||
);
|
||||
}
|
||||
}
|
||||
let pages: string[];
|
||||
if (novel.totalPages > 0) {
|
||||
@@ -349,7 +354,11 @@ export const useNovel = (novelPath: string, pluginId: string) => {
|
||||
} else {
|
||||
pages = (await getCustomPages(novel.id)).map(c => c.page);
|
||||
}
|
||||
setPages(pages);
|
||||
if (pages.length) {
|
||||
setPages(pages);
|
||||
} else {
|
||||
setPages(['1']);
|
||||
}
|
||||
setNovel(novel);
|
||||
}, []);
|
||||
|
||||
@@ -373,26 +382,8 @@ export const useNovel = (novelPath: string, pluginId: string) => {
|
||||
};
|
||||
});
|
||||
await insertChapters(novel.id, sourceChapters);
|
||||
const latestChapkey = `${NOVEL_LATEST_CHAPTER_PREFIX}_${novel.id}`;
|
||||
const latestChapter = getMMKVObject<ChapterItem>(latestChapkey);
|
||||
if (
|
||||
sourcePage.latestChapter &&
|
||||
sourcePage.latestChapter.path !== latestChapter?.path
|
||||
) {
|
||||
setMMKVObject(latestChapkey, sourcePage.latestChapter);
|
||||
setMMKVObject(
|
||||
hasUpdatesKey,
|
||||
hasUpdates?.map((val, idx) => {
|
||||
if (idx !== pageIndex) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
hasUpdates[pageIndex] = false;
|
||||
setMMKVObject(hasUpdatesKey, hasUpdates);
|
||||
}
|
||||
hasUpdates[pageIndex] = false;
|
||||
setMMKVObject(hasUpdatesKey, hasUpdates);
|
||||
chapters = await _getPageChapters(
|
||||
novel.id,
|
||||
novelSettings.sort,
|
||||
@@ -401,8 +392,8 @@ export const useNovel = (novelPath: string, pluginId: string) => {
|
||||
);
|
||||
}
|
||||
setChapters(chapters);
|
||||
setLoading(false);
|
||||
}
|
||||
setLoading(false);
|
||||
}, [novel, novelSettings, pageIndex]);
|
||||
useEffect(() => {
|
||||
getNovel();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { locale } from 'expo-localization';
|
||||
import { Language, languagesMapping } from '@utils/constants/languages';
|
||||
import { languagesMapping } from '@utils/constants/languages';
|
||||
import { orderBy } from 'lodash-es';
|
||||
import { useMMKVObject } from 'react-native-mmkv';
|
||||
import { PluginItem } from '@plugins/types';
|
||||
@@ -23,17 +23,13 @@ export const FILTERED_INSTALLED_PLUGINS = 'FILTERED_INSTALLED_PLUGINS';
|
||||
|
||||
const defaultLang = languagesMapping[locale.split('-')[0]] || 'English';
|
||||
|
||||
export type PluginsMap = Record<Language, PluginItem[] | undefined>;
|
||||
|
||||
export default function usePlugins() {
|
||||
const [lastUsedPlugin, setLastUsedPlugin] =
|
||||
useMMKVObject<PluginItem>(LAST_USED_PLUGIN);
|
||||
const [languagesFilter = [defaultLang], setLanguagesFilter] =
|
||||
useMMKVObject<Language[]>(LANGUAGES_FILTER);
|
||||
const [
|
||||
filteredAvailablePlugins = {} as PluginsMap,
|
||||
setFilteredAvailablePlugins,
|
||||
] = useMMKVObject<PluginsMap>(FILTERED_AVAILABLE_PLUGINS);
|
||||
useMMKVObject<string[]>(LANGUAGES_FILTER);
|
||||
const [filteredAvailablePlugins = [], setFilteredAvailablePlugins] =
|
||||
useMMKVObject<PluginItem[]>(FILTERED_AVAILABLE_PLUGINS);
|
||||
const [filteredInstalledPlugins = [], setFilteredInstalledPlugins] =
|
||||
useMMKVObject<PluginItem[]>(FILTERED_INSTALLED_PLUGINS);
|
||||
/**
|
||||
@@ -41,54 +37,51 @@ export default function usePlugins() {
|
||||
* We cant use the languagesFilter directly because it is updated only after component's lifecycle end.
|
||||
* And toggleLanguagFilter triggers filterPlugins before lifecycle end.
|
||||
*/
|
||||
const filterPlugins = useCallback((filter: Language[]) => {
|
||||
const filterPlugins = useCallback((filter: string[]) => {
|
||||
const installedPlugins =
|
||||
getMMKVObject<PluginItem[]>(INSTALLED_PLUGINS) || [];
|
||||
const availablePlugins =
|
||||
getMMKVObject<PluginsMap>(AVAILABLE_PLUGINS) || ({} as PluginsMap);
|
||||
getMMKVObject<PluginItem[]>(AVAILABLE_PLUGINS) || [];
|
||||
setFilteredInstalledPlugins(
|
||||
installedPlugins.filter(plg => filter.includes(plg.lang)),
|
||||
);
|
||||
setFilteredAvailablePlugins(
|
||||
filter.reduce((pre, cur) => {
|
||||
pre[cur] = orderBy(availablePlugins[cur], 'name');
|
||||
return pre;
|
||||
}, {} as PluginsMap),
|
||||
orderBy(
|
||||
availablePlugins.filter(plg => filter.includes(plg.lang)),
|
||||
'name',
|
||||
),
|
||||
);
|
||||
}, []);
|
||||
|
||||
const refreshPlugins = useCallback(() => {
|
||||
const installedPlugins =
|
||||
getMMKVObject<PluginItem[]>(INSTALLED_PLUGINS) || [];
|
||||
return fetchPlugins().then(fetchedPluginsMap => {
|
||||
for (const key in fetchedPluginsMap) {
|
||||
const lang = key as Language;
|
||||
fetchedPluginsMap[lang] = fetchedPluginsMap[lang].filter(plg => {
|
||||
const finded = installedPlugins.find(v => v.id === plg.id);
|
||||
if (finded) {
|
||||
if (newer(plg.version, finded.version)) {
|
||||
finded.hasUpdate = true;
|
||||
finded.iconUrl = plg.iconUrl;
|
||||
finded.url = plg.url;
|
||||
if (finded.id === lastUsedPlugin?.id) {
|
||||
setLastUsedPlugin(finded);
|
||||
}
|
||||
return fetchPlugins().then(fetchedPlugins => {
|
||||
fetchedPlugins.filter(plg => {
|
||||
const finded = installedPlugins.find(v => v.id === plg.id);
|
||||
if (finded) {
|
||||
if (newer(plg.version, finded.version)) {
|
||||
finded.hasUpdate = true;
|
||||
finded.iconUrl = plg.iconUrl;
|
||||
finded.url = plg.url;
|
||||
if (finded.id === lastUsedPlugin?.id) {
|
||||
setLastUsedPlugin(finded);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
setMMKVObject(INSTALLED_PLUGINS, installedPlugins);
|
||||
setMMKVObject(AVAILABLE_PLUGINS, fetchedPluginsMap);
|
||||
filterPlugins(languagesFilter);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
setMMKVObject(INSTALLED_PLUGINS, installedPlugins);
|
||||
setMMKVObject(AVAILABLE_PLUGINS, fetchedPlugins);
|
||||
filterPlugins(languagesFilter);
|
||||
});
|
||||
}, [languagesFilter]);
|
||||
|
||||
const toggleLanguageFilter = (lang: Language) => {
|
||||
const toggleLanguageFilter = (lang: string) => {
|
||||
const newFilter = languagesFilter.includes(lang)
|
||||
? languagesFilter.filter(l => l !== lang)
|
||||
: [...languagesFilter, lang];
|
||||
: [lang, ...languagesFilter];
|
||||
setLanguagesFilter(newFilter);
|
||||
filterPlugins(newFilter);
|
||||
};
|
||||
@@ -106,10 +99,7 @@ export default function usePlugins() {
|
||||
const installedPlugins =
|
||||
getMMKVObject<PluginItem[]>(INSTALLED_PLUGINS) || [];
|
||||
const availablePlugins =
|
||||
getMMKVObject<PluginsMap>(AVAILABLE_PLUGINS) || ({} as PluginsMap);
|
||||
availablePlugins[plugin.lang] = availablePlugins[plugin.lang]?.filter(
|
||||
plg => plg.id !== plugin.id,
|
||||
);
|
||||
getMMKVObject<PluginItem[]>(AVAILABLE_PLUGINS) || [];
|
||||
|
||||
const actualPlugin: PluginItem = {
|
||||
...plugin,
|
||||
@@ -119,7 +109,10 @@ export default function usePlugins() {
|
||||
if (!installedPlugins.some(plg => plg.id === plugin.id)) {
|
||||
setMMKVObject(INSTALLED_PLUGINS, [...installedPlugins, actualPlugin]);
|
||||
}
|
||||
setMMKVObject(AVAILABLE_PLUGINS, availablePlugins);
|
||||
setMMKVObject(
|
||||
AVAILABLE_PLUGINS,
|
||||
availablePlugins.filter(plg => plg.id !== plugin.id),
|
||||
);
|
||||
filterPlugins(languagesFilter);
|
||||
} else {
|
||||
throw new Error(
|
||||
@@ -136,14 +129,11 @@ export default function usePlugins() {
|
||||
const installedPlugins =
|
||||
getMMKVObject<PluginItem[]>(INSTALLED_PLUGINS) || [];
|
||||
const availablePlugins =
|
||||
getMMKVObject<PluginsMap>(AVAILABLE_PLUGINS) || ({} as PluginsMap);
|
||||
getMMKVObject<PluginItem[]>(AVAILABLE_PLUGINS) || [];
|
||||
|
||||
// safe
|
||||
if (!availablePlugins[plugin.lang]?.some(_plg => _plg.id === plugin.id)) {
|
||||
availablePlugins[plugin.lang] = [
|
||||
...(availablePlugins[plugin.lang] || []),
|
||||
plugin,
|
||||
];
|
||||
if (!availablePlugins.some(_plg => _plg.id === plugin.id)) {
|
||||
availablePlugins.push(plugin);
|
||||
setMMKVObject(AVAILABLE_PLUGINS, availablePlugins);
|
||||
}
|
||||
setMMKVObject(
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { SourceNovel } from '@plugins/types';
|
||||
import { NativeModules } from 'react-native';
|
||||
interface EpubUtilInterface {
|
||||
parseNovelAndChapters: (epubDirPath: string) => Promise<SourceNovel>;
|
||||
}
|
||||
const { EpubUtil } = NativeModules;
|
||||
|
||||
export default EpubUtil as EpubUtilInterface;
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getUserAgent } from '@hooks/persisted/useUserAgent';
|
||||
import { parse as parseProto } from 'protobufjs';
|
||||
|
||||
type FetchInit = {
|
||||
headers?: Record<string, string> | Headers;
|
||||
@@ -111,3 +112,95 @@ export const fetchText = async (
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
function base64ToBytesArr(str: string) {
|
||||
const abc = [
|
||||
...'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/',
|
||||
]; // base64 alphabet
|
||||
let result = [];
|
||||
|
||||
for (let i = 0; i < str.length / 4; i++) {
|
||||
let chunk = [...str.slice(4 * i, 4 * i + 4)];
|
||||
let bin = chunk
|
||||
.map(x => abc.indexOf(x).toString(2).padStart(6, '0'))
|
||||
.join('');
|
||||
let bytes = bin.match(/.{1,8}/g)?.map(x => +('0b' + x)) || [];
|
||||
result.push(
|
||||
...bytes.slice(
|
||||
0,
|
||||
3 - Number(str[4 * i + 2] === '=') - Number(str[4 * i + 3] === '='),
|
||||
),
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
interface ProtoRequestInit {
|
||||
// merged .proto file
|
||||
proto: string;
|
||||
requestType: string;
|
||||
requestData?: any;
|
||||
responseType: string;
|
||||
}
|
||||
|
||||
const BYTE_MARK = BigInt((1 << 8) - 1);
|
||||
|
||||
export const fetchProto = async function (
|
||||
protoInit: ProtoRequestInit,
|
||||
url: string,
|
||||
init?: FetchInit,
|
||||
) {
|
||||
const protoRoot = parseProto(protoInit.proto).root;
|
||||
const RequestMessge = protoRoot.lookupType(protoInit.requestType);
|
||||
if (RequestMessge.verify(protoInit.requestData)) {
|
||||
throw new Error('Invalid Proto');
|
||||
}
|
||||
// encode request data
|
||||
const encodedrequest = RequestMessge.encode(protoInit.requestData).finish();
|
||||
const requestLength = BigInt(encodedrequest.length);
|
||||
const headers = new Uint8Array(
|
||||
Array(5)
|
||||
.fill(0)
|
||||
.map((v, idx) => {
|
||||
if (idx === 0) {
|
||||
return 0;
|
||||
}
|
||||
return Number((requestLength >> BigInt(8 * (5 - idx - 1))) & BYTE_MARK);
|
||||
}),
|
||||
);
|
||||
init = await makeInit(init);
|
||||
const bodyArray = new Uint8Array(headers.length + encodedrequest.length);
|
||||
bodyArray.set(headers, 0);
|
||||
bodyArray.set(encodedrequest, headers.length);
|
||||
|
||||
return fetch(url, {
|
||||
method: 'POST',
|
||||
...init,
|
||||
body: bodyArray,
|
||||
} as RequestInit)
|
||||
.then(r => r.blob())
|
||||
.then(blob => {
|
||||
// decode response data
|
||||
return new Promise((resolve, reject) => {
|
||||
const fr = new FileReader();
|
||||
fr.onloadend = () => {
|
||||
const payload = new Uint8Array(
|
||||
base64ToBytesArr(
|
||||
fr.result.slice(FILE_READER_PREFIX_LENGTH) as string,
|
||||
),
|
||||
);
|
||||
const length = Number(
|
||||
BigInt(payload[1] << 24) |
|
||||
BigInt(payload[2] << 16) |
|
||||
BigInt(payload[3] << 8) |
|
||||
BigInt(payload[4]),
|
||||
);
|
||||
const ResponseMessage = protoRoot.lookupType(protoInit.responseType);
|
||||
resolve(ResponseMessage.decode(payload.slice(5, 5 + length)));
|
||||
};
|
||||
fr.onerror = () => reject();
|
||||
fr.onabort = () => reject();
|
||||
fr.readAsDataURL(blob);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import RNFS from 'react-native-fs';
|
||||
import { PluginDownloadFolder } from '@utils/constants/download';
|
||||
import { newer } from '@utils/compareVersion';
|
||||
import { Language } from '@utils/constants/languages';
|
||||
|
||||
// packages for plugins
|
||||
import { load } from 'cheerio';
|
||||
@@ -10,20 +9,22 @@ import qs from 'qs';
|
||||
import { NovelStatus, Plugin, PluginItem } from './types';
|
||||
import { FilterTypes } from './types/filterTypes';
|
||||
import { isUrlAbsolute } from './helpers/isAbsoluteUrl';
|
||||
import { fetchApi, fetchFile, fetchText } from './helpers/fetch';
|
||||
import { fetchApi, fetchFile, fetchProto, fetchText } from './helpers/fetch';
|
||||
import { defaultCover } from './helpers/constants';
|
||||
import { encode, decode } from 'urlencode';
|
||||
import { Parser } from 'htmlparser2';
|
||||
import TextFile from '@native/TextFile';
|
||||
|
||||
const pluginsFilePath = PluginDownloadFolder + '/plugins.json';
|
||||
|
||||
const packages: Record<string, any> = {
|
||||
'htmlparser2': { Parser },
|
||||
'cheerio': { load },
|
||||
'dayjs': dayjs,
|
||||
'qs': qs,
|
||||
'urlencode': { encode, decode },
|
||||
'@libs/novelStatus': { NovelStatus },
|
||||
'@libs/fetch': { fetchApi, fetchFile, fetchText },
|
||||
'@libs/fetch': { fetchApi, fetchFile, fetchText, fetchProto },
|
||||
'@libs/isAbsoluteUrl': { isUrlAbsolute },
|
||||
'@libs/filterInputs': { FilterTypes },
|
||||
'@libs/defaultCover': { defaultCover },
|
||||
@@ -125,13 +126,13 @@ const updatePlugin = async (plugin: PluginItem) => {
|
||||
return installPlugin(plugin.url);
|
||||
};
|
||||
|
||||
const fetchPlugins = (): Promise<Record<Language, Array<PluginItem>>> => {
|
||||
const fetchPlugins = (): Promise<PluginItem[]> => {
|
||||
// plugins host
|
||||
const githubUsername = 'LNReader';
|
||||
const githubRepository = 'lnreader-sources';
|
||||
|
||||
const pluginsTag = 'v2.1.0';
|
||||
return fetch(
|
||||
`https://raw.githubusercontent.com/${githubUsername}/${githubRepository}/dist/.dist/plugins.min.json`,
|
||||
`https://raw.githubusercontent.com/${githubUsername}/${githubRepository}/plugins/${pluginsTag}/.dist/plugins.min.json`,
|
||||
).then(res => res.json());
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { FilterToValues, Filters } from './filterTypes';
|
||||
import { Language } from '@utils/constants/languages';
|
||||
|
||||
export interface NovelItem {
|
||||
name: string;
|
||||
@@ -33,11 +32,11 @@ export interface SourceNovel extends NovelItem {
|
||||
status?: NovelStatus;
|
||||
chapters: ChapterItem[];
|
||||
totalPages?: number;
|
||||
latestChapter?: ChapterItem;
|
||||
}
|
||||
|
||||
export interface SourcePage {
|
||||
chapters: ChapterItem[];
|
||||
latestChapter?: ChapterItem;
|
||||
}
|
||||
|
||||
export interface PopularNovelsOptions<Q extends Filters> {
|
||||
@@ -50,7 +49,7 @@ export interface PluginItem {
|
||||
id: string;
|
||||
name: string;
|
||||
site: string;
|
||||
lang: Language;
|
||||
lang: string;
|
||||
version: string;
|
||||
url: string; // the url of raw code
|
||||
iconUrl: string;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Appbar, List, SwitchItem } from '@components';
|
||||
|
||||
import { useBrowseSettings, usePlugins, useTheme } from '@hooks/persisted';
|
||||
import { getString } from '@strings/translations';
|
||||
import { availableLanguages, languages } from '@utils/constants/languages';
|
||||
import { languages } from '@utils/constants/languages';
|
||||
import { BrowseSettingsScreenProp } from '@navigators/types';
|
||||
|
||||
const BrowseSettings = ({ navigation }: BrowseSettingsScreenProp) => {
|
||||
@@ -61,10 +61,10 @@ const BrowseSettings = ({ navigation }: BrowseSettingsScreenProp) => {
|
||||
</>
|
||||
}
|
||||
keyExtractor={item => item}
|
||||
data={availableLanguages}
|
||||
data={languages}
|
||||
renderItem={({ item }) => (
|
||||
<SwitchItem
|
||||
label={languages[item]}
|
||||
label={item}
|
||||
value={languagesFilter.includes(item)}
|
||||
onPress={() => toggleLanguageFilter(item)}
|
||||
theme={theme}
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
import { coverPlaceholderColor } from '@theme/colors';
|
||||
import { ThemeColors } from '@theme/types';
|
||||
import { Swipeable } from 'react-native-gesture-handler';
|
||||
import { languages } from '@utils/constants/languages';
|
||||
import { getString } from '@strings/translations';
|
||||
import { BrowseScreenProps } from '@navigators/types';
|
||||
import { Button, IconButtonV2 } from '@components';
|
||||
@@ -30,6 +29,7 @@ import Animated, {
|
||||
useSharedValue,
|
||||
withTiming,
|
||||
} from 'react-native-reanimated';
|
||||
import { groupBy } from 'lodash-es';
|
||||
|
||||
interface AvailableTabProps {
|
||||
searchText: string;
|
||||
@@ -168,7 +168,7 @@ export const InstalledTab = memo(
|
||||
numberOfLines={1}
|
||||
style={[{ color: theme.onSurfaceVariant }, styles.addition]}
|
||||
>
|
||||
{`${languages[item.lang]} - ${item.version}`}
|
||||
{`${item.lang} - ${item.version}`}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
@@ -297,7 +297,7 @@ const AvailablePluginCard = ({
|
||||
textStyles,
|
||||
]}
|
||||
>
|
||||
{`${languages[plugin.lang]} - ${plugin.version}`}
|
||||
{`${plugin.lang} - ${plugin.version}`}
|
||||
</Animated.Text>
|
||||
</Animated.View>
|
||||
</Animated.View>
|
||||
@@ -337,22 +337,20 @@ export const AvailableTab = memo(({ searchText, theme }: AvailableTabProps) => {
|
||||
|
||||
const sections = useMemo(() => {
|
||||
const list = [];
|
||||
for (const language of languagesFilter) {
|
||||
if (filteredAvailablePlugins[language]) {
|
||||
let plugins;
|
||||
if (searchText) {
|
||||
plugins = filteredAvailablePlugins[language]?.filter(plg =>
|
||||
const group = groupBy(
|
||||
searchText
|
||||
? filteredAvailablePlugins.filter(plg =>
|
||||
plg.name.toLocaleLowerCase().includes(searchText.toLowerCase()),
|
||||
);
|
||||
} else {
|
||||
plugins = filteredAvailablePlugins[language];
|
||||
}
|
||||
if (plugins && plugins.length) {
|
||||
list.push({
|
||||
header: language,
|
||||
data: plugins,
|
||||
});
|
||||
}
|
||||
)
|
||||
: filteredAvailablePlugins,
|
||||
'lang',
|
||||
);
|
||||
for (const language of languagesFilter) {
|
||||
if (group[language]?.length) {
|
||||
list.push({
|
||||
header: language,
|
||||
data: group[language],
|
||||
});
|
||||
}
|
||||
}
|
||||
return list;
|
||||
|
||||
@@ -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(){
|
||||
|
||||
+28
-106
@@ -16,14 +16,14 @@ import { LOCAL_PLUGIN_ID } from '@plugins/pluginManager';
|
||||
import { MMKVStorage } from '@utils/mmkv/mmkv';
|
||||
import { BACKGROUND_ACTION, BackgoundAction } from '@services/constants';
|
||||
import { getString } from '@strings/translations';
|
||||
import { ChapterItem, SourceNovel } from '@plugins/types';
|
||||
import { load as parseXML } from 'cheerio';
|
||||
import { showToast } from '@utils/showToast';
|
||||
import TextFile from '@native/TextFile';
|
||||
import EpubUtil from '@native/EpubUtil';
|
||||
|
||||
interface TaskData {
|
||||
delay: number;
|
||||
sourceUri: string;
|
||||
filename?: string;
|
||||
}
|
||||
|
||||
const insertLocalNovel = (
|
||||
@@ -31,12 +31,17 @@ const insertLocalNovel = (
|
||||
path: string,
|
||||
cover?: string,
|
||||
author?: string,
|
||||
artist?: string,
|
||||
summary?: string,
|
||||
): Promise<number> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
"INSERT INTO Novel(name, path, cover, author, pluginId, inLibrary, isLocal) VALUES(?, ?, ?, ?, 'local', 1, 1)",
|
||||
[name, path, cover || null, author || null],
|
||||
`
|
||||
INSERT INTO
|
||||
Novel(name, path, pluginId, inLibrary, isLocal)
|
||||
VALUES(?, ?, 'local', 1, 1)`,
|
||||
[name, path],
|
||||
async (txObj, resultSet) => {
|
||||
if (resultSet.insertId) {
|
||||
await updateNovelCategoryById(resultSet.insertId, [2]);
|
||||
@@ -49,9 +54,11 @@ const insertLocalNovel = (
|
||||
await RNFS.moveFile(cover, newCoverPath);
|
||||
}
|
||||
await updateNovelInfo({
|
||||
pluginId: LOCAL_PLUGIN_ID,
|
||||
id: resultSet.insertId,
|
||||
pluginId: LOCAL_PLUGIN_ID,
|
||||
author: author,
|
||||
artist: artist,
|
||||
summary: summary,
|
||||
path: NovelDownloadFolder + '/local/' + resultSet.insertId,
|
||||
cover: newCoverPath,
|
||||
name: name,
|
||||
@@ -144,92 +151,6 @@ const insertLocalChapter = (
|
||||
});
|
||||
};
|
||||
|
||||
const getParent = (filePath: string) => {
|
||||
return filePath.replace(/[/\\][^/\\]+$/, '');
|
||||
};
|
||||
|
||||
const parseNovelAndChapters = async (
|
||||
epubDirPath: string,
|
||||
): Promise<SourceNovel> => {
|
||||
const containerText = await TextFile.readFile(
|
||||
`${epubDirPath}/META-INF/container.xml`,
|
||||
);
|
||||
const contentPath =
|
||||
epubDirPath + '/' + parseXML(containerText)('rootfile').attr('full-path');
|
||||
const contentDir = getParent(contentPath);
|
||||
const contentText = await TextFile.readFile(contentPath);
|
||||
const parsedContent = parseXML(contentText);
|
||||
const novelName = parsedContent('dc\\:title').text().trim();
|
||||
const author = parsedContent('dc\\:creator').text().trim();
|
||||
const description = parsedContent('dc\\:description').text().trim();
|
||||
const coverRef = parsedContent('meta[name="cover"]').attr('content');
|
||||
let cover = '';
|
||||
if (coverRef) {
|
||||
cover =
|
||||
contentDir + '/' + parsedContent(`item[id="${coverRef}"]`).attr('href');
|
||||
}
|
||||
BackgroundService.updateNotification({
|
||||
progressBar: {
|
||||
value: 1,
|
||||
max: 4,
|
||||
},
|
||||
});
|
||||
const tocPath = contentDir + '/' + 'toc.ncx';
|
||||
const tocMap: Record<string, string> = {};
|
||||
if (await RNFS.exists(tocPath)) {
|
||||
const tocText = await TextFile.readFile(tocPath);
|
||||
const toc = parseXML(tocText);
|
||||
toc('navPoint').each(function () {
|
||||
const href = toc(this).find('content').attr('src');
|
||||
const name = toc(this).find('text').text().trim();
|
||||
if (href && name) {
|
||||
tocMap[href] = name;
|
||||
}
|
||||
});
|
||||
}
|
||||
BackgroundService.updateNotification({
|
||||
progressBar: {
|
||||
value: 2,
|
||||
max: 4,
|
||||
},
|
||||
});
|
||||
const itemMap: Record<string, string> = {};
|
||||
parsedContent('item').each(function () {
|
||||
itemMap[this.attribs.id] = this.attribs.href;
|
||||
});
|
||||
BackgroundService.updateNotification({
|
||||
progressBar: {
|
||||
value: 3,
|
||||
max: 4,
|
||||
},
|
||||
});
|
||||
const chapters: ChapterItem[] = parsedContent('itemref')
|
||||
.toArray()
|
||||
.map(ele => {
|
||||
const href = itemMap[ele.attribs.idref];
|
||||
return {
|
||||
name: tocMap[href],
|
||||
path: `${contentDir}/${href}`,
|
||||
};
|
||||
});
|
||||
BackgroundService.updateNotification({
|
||||
progressBar: {
|
||||
value: 4,
|
||||
max: 4,
|
||||
indeterminate: true,
|
||||
},
|
||||
});
|
||||
const novel: SourceNovel = {
|
||||
name: novelName,
|
||||
author: author,
|
||||
cover: cover,
|
||||
path: contentDir + novelName, // temporary
|
||||
chapters: chapters,
|
||||
summary: description,
|
||||
};
|
||||
return novel;
|
||||
};
|
||||
|
||||
const importEpubAction = async (taskData?: TaskData) => {
|
||||
try {
|
||||
if (!taskData) {
|
||||
@@ -241,25 +162,23 @@ const importEpubAction = async (taskData?: TaskData) => {
|
||||
if (await RNFS.exists(epubDirPath)) {
|
||||
await RNFS.unlink(epubDirPath);
|
||||
}
|
||||
await RNFS.mkdir(epubDirPath).catch(e => {
|
||||
throw e;
|
||||
});
|
||||
await RNFS.mkdir(epubDirPath);
|
||||
MMKVStorage.set(BACKGROUND_ACTION, BackgoundAction.IMPORT_EPUB);
|
||||
await ZipArchive.unzip(epubFilePath, epubDirPath).catch(e => {
|
||||
throw e;
|
||||
});
|
||||
await ZipArchive.unzip(epubFilePath, epubDirPath);
|
||||
await sleep(taskData.delay);
|
||||
const novel = await parseNovelAndChapters(epubDirPath).catch(e => {
|
||||
throw e;
|
||||
});
|
||||
|
||||
const novel = await EpubUtil.parseNovelAndChapters(epubDirPath);
|
||||
if (!novel.name) {
|
||||
novel.name = taskData.filename?.replace('.epub', '') || 'Untitled';
|
||||
}
|
||||
const novelId = await insertLocalNovel(
|
||||
novel.name,
|
||||
novel.path,
|
||||
epubDirPath + novel.name, // temporary
|
||||
novel.cover,
|
||||
novel.author,
|
||||
).catch(e => {
|
||||
throw e;
|
||||
});
|
||||
novel.artist,
|
||||
novel.summary,
|
||||
);
|
||||
const now = dayjs().toISOString();
|
||||
const filePathSet = new Set<string>();
|
||||
if (novel.chapters) {
|
||||
@@ -296,7 +215,6 @@ const importEpubAction = async (taskData?: TaskData) => {
|
||||
}
|
||||
}
|
||||
await sleep(taskData.delay);
|
||||
// move static files
|
||||
const novelDir = NovelDownloadFolder + '/local/' + novelId;
|
||||
BackgroundService.updateNotification({
|
||||
taskTitle: getString('advancedSettingsScreen.importStaticFiles'),
|
||||
@@ -364,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) {
|
||||
|
||||
@@ -2,51 +2,140 @@ import { fetchImage, fetchNovel } from '../plugin/fetch';
|
||||
import { downloadChapter } from '../../database/queries/ChapterQueries';
|
||||
|
||||
import * as SQLite from 'expo-sqlite';
|
||||
import { SourceNovel } from '@plugins/types';
|
||||
import { ChapterItem, SourceNovel } from '@plugins/types';
|
||||
import { LOCAL_PLUGIN_ID } from '@plugins/pluginManager';
|
||||
import { NovelDownloadFolder } from '@utils/constants/download';
|
||||
import * as RNFS from 'react-native-fs';
|
||||
import { getMMKVObject, setMMKVObject } from '@utils/mmkv/mmkv';
|
||||
import { NOVEL_PAGE_UPDATES_PREFIX } from '@hooks/persisted/useNovel';
|
||||
import {
|
||||
NOVEL_LATEST_CHAPTER_PREFIX,
|
||||
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,53 +156,20 @@ 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 latestChapterKey = `${NOVEL_LATEST_CHAPTER_PREFIX}_${novelId}`;
|
||||
const latestChapter = getMMKVObject<ChapterItem>(latestChapterKey);
|
||||
if (novel.latestChapter && novel.latestChapter.path !== latestChapter?.path) {
|
||||
const hasUpdatesKey = `${NOVEL_PAGE_UPDATES_PREFIX}_${novelId}`;
|
||||
const hasUpdates = getMMKVObject<boolean[]>(hasUpdatesKey);
|
||||
if (hasUpdates) {
|
||||
setMMKVObject(
|
||||
hasUpdatesKey,
|
||||
hasUpdates.map(() => true),
|
||||
);
|
||||
});
|
||||
});
|
||||
const key = `${NOVEL_PAGE_UPDATES_PREFIX}_${novelId}`;
|
||||
const hasUpdates = getMMKVObject<boolean[]>(key);
|
||||
if (hasUpdates) {
|
||||
setMMKVObject(
|
||||
key,
|
||||
hasUpdates.map(() => true),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -2,43 +2,22 @@
|
||||
// https://en.wikipedia.org/wiki/IETF_language_tag
|
||||
// https://en.wikipedia.org/wiki/List_of_language_names
|
||||
|
||||
export const languages = {
|
||||
Arabic: 'العربية',
|
||||
Chinese: '中文, 汉语, 漢語',
|
||||
English: 'English',
|
||||
French: 'Français',
|
||||
Indonesian: 'Bahasa Indonesia',
|
||||
Japanese: '日本語',
|
||||
Korean: '조선말, 한국어',
|
||||
Polish: 'Polski',
|
||||
Portuguese: 'Português',
|
||||
Russian: 'Русский',
|
||||
Spanish: 'Español',
|
||||
Thai: 'ไทย',
|
||||
Turkish: 'Türkçe',
|
||||
Ukrainian: 'Українська',
|
||||
Vietnamese: 'Tiếng Việt',
|
||||
} as const;
|
||||
|
||||
export type Language = keyof typeof languages;
|
||||
export type NativeLanguage = (typeof languages)[Language];
|
||||
|
||||
export const languagesMapping: Record<string, Language | undefined> = {
|
||||
'ab': 'Arabic',
|
||||
'zh': 'Chinese',
|
||||
export const languagesMapping: Record<string, string> = {
|
||||
'ab': 'العربية',
|
||||
'zh': '中文, 汉语, 漢語',
|
||||
'en': 'English',
|
||||
'fr': 'French',
|
||||
'id': 'Indonesian',
|
||||
'ja': 'Japanese',
|
||||
'ko': 'Korean',
|
||||
'pl': 'Polish',
|
||||
'pt': 'Portuguese',
|
||||
'ru': 'Russian',
|
||||
'es': 'Spanish',
|
||||
'th': 'Thai',
|
||||
'tr': 'Turkish',
|
||||
'uk': 'Ukrainian',
|
||||
'vi': 'Vietnamese',
|
||||
'fr': 'Français',
|
||||
'id': 'Bahasa Indonesia',
|
||||
'ja': '日本語',
|
||||
'ko': '조선말, 한국어',
|
||||
'pl': 'Polski',
|
||||
'pt': 'Português',
|
||||
'ru': 'Русский',
|
||||
'es': 'Español',
|
||||
'th': 'ไทย',
|
||||
'tr': 'Türkçe',
|
||||
'uk': 'Українська',
|
||||
'vi': 'Tiếng Việt',
|
||||
};
|
||||
|
||||
export const availableLanguages = Object.keys(languages) as Language[];
|
||||
export const languages = Object.values(languagesMapping);
|
||||
|
||||
Reference in New Issue
Block a user