Compare commits

..

6 Commits

Author SHA1 Message Date
K1ngfish3r 9ef8cab475 fix chapter.page when containing special character (#1015)
* fix page when containing special character

* page always defaults to 1

* AND page = ?
2024-03-26 22:29:19 +07:00
nyagami 7ea8ebacd1 Update plugin sanbox (#1014)
* add htmlparser2

* fix typo

* flatten plugins
2024-03-26 17:46:38 +07:00
nyagami e171dfebc1 Update lastest chapter logic (#1011)
update lastest chapter logic
2024-03-24 13:55:23 +07:00
nyagami 9f6ffc23bf 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
2024-03-23 11:50:35 +07:00
nyagami 8906253527 Feat: protobuf support (#1003)
* feat: protobuf support

* fix: spead operator is slower

* fix: redundant JSON encoding

* add fetchProto to plugin sandbox
2024-03-21 16:37:07 +07:00
nyagami 817e4dc9d1 Cheaper epub parsing (#1007)
* cheaper epub parsing

* cleaner process

* no empty page list

* more epub detail

* fix nullable
2024-03-20 13:37:55 +07:00
28 changed files with 1129 additions and 489 deletions
+100 -15
View File
@@ -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 {
+55 -7
View File
@@ -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' });
}
}
};
+111
View File
@@ -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;
}
@@ -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));
}
}
+144 -5
View File
@@ -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",
+2
View File
@@ -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",
+4 -8
View File
@@ -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,
);
+1 -1
View File
@@ -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 =>
+14 -23
View File
@@ -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();
+37 -47
View File
@@ -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(
+2
View File
@@ -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 = {
+8
View File
@@ -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;
+93
View File
@@ -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);
});
});
};
+7 -6
View File
@@ -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());
};
+2 -3
View File
@@ -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;
+3 -3
View File
@@ -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}
+16 -18
View File
@@ -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
View File
@@ -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) {
+135 -79
View File
@@ -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),
);
}
}
};
+67 -68
View File
@@ -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");
}
}
};
+16 -37
View File
@@ -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);