From 93f5871f1a7a1ccb270c67cc34fa215ca8c63d6f Mon Sep 17 00:00:00 2001 From: nyagami Date: Thu, 22 Feb 2024 18:51:16 +0700 Subject: [PATCH] Optimizations and bug fixes (#964) * upgrade react-native-webview * optimise: no render reader content when change reader settings * optimise: no render reader content when change general settings * fix: missing page when open reader * fix: library only downloaded filter * fix: center reader footer * fix: reader footer padding * fix: stop loading after fetch chapters * showToast if cancel epub * optimise: plugin list * fix: remove LNReader path * optimise: replace RNFS utf-8 by native * fix: add description as epub summary * feat: hot reload for assets files (reader statics files) --- android/app/src/main/assets/css/index.css | 26 ++- android/app/src/main/assets/js/index.js | 95 +++++++++- .../LNReader/MainApplication.java | 2 + .../TextFile/TextFile.java | 53 ++++++ .../TextFile/TextFilePackage.java | 32 ++++ metro.config.js | 42 ++++- package-lock.json | 8 +- package.json | 2 +- reader_playground/README.md | 5 - reader_playground/template.index.html | 71 ------- src/database/queries/ChapterQueries.ts | 5 +- src/database/queries/LibraryQueries.ts | 46 ++--- src/hooks/common/useFullscreenMode.ts | 5 +- src/hooks/persisted/useNovel.ts | 173 +++++++++--------- src/hooks/persisted/useSettings.ts | 4 +- src/native/TextFile.ts | 8 + src/plugins/pluginManager.ts | 36 ++-- .../browse/components/PluginSection.tsx | 35 ++-- src/screens/reader/ReaderScreen.tsx | 7 +- .../BottomInfoBar/BottomInfoBar.tsx | 72 -------- .../reader/components/ChapterDrawer.tsx | 2 +- .../reader/components/WebViewReader.tsx | 111 ++++++++--- src/services/backup/utils.ts | 15 +- src/services/epub/import.ts | 17 +- src/utils/constants/download.ts | 2 +- 25 files changed, 521 insertions(+), 353 deletions(-) create mode 100644 android/app/src/main/java/com/rajarsheechatterjee/TextFile/TextFile.java create mode 100644 android/app/src/main/java/com/rajarsheechatterjee/TextFile/TextFilePackage.java delete mode 100644 reader_playground/README.md delete mode 100644 reader_playground/template.index.html create mode 100644 src/native/TextFile.ts delete mode 100644 src/screens/reader/components/BottomInfoBar/BottomInfoBar.tsx diff --git a/android/app/src/main/assets/css/index.css b/android/app/src/main/assets/css/index.css index c46f82825..d747a1b7d 100644 --- a/android/app/src/main/assets/css/index.css +++ b/android/app/src/main/assets/css/index.css @@ -11,6 +11,7 @@ body { padding-bottom: 40px; font-size: var(--readerSettings-textSize); + background-color: var(--readerSettings-theme); color: var(--readerSettings-textColor); text-align: var(--readerSettings-textAlign); line-height: var(--readerSettings-lineHeight); @@ -76,6 +77,10 @@ img { display: none; } +.hidden { + visibility: hidden; +} + #ScrollBar { position: fixed; right: 5vw; @@ -136,20 +141,29 @@ img { background-color: var(--theme-primary); } -#reader-percentage { +#reader-footer-wrapper { position: fixed; - padding-top: 0.5rem; - min-height: 2rem; + bottom: 0; + left: 0; width: 100%; +} + +#reader-footer { + display: flex; + flex: 1; + justify-content: space-between; + min-height: 2rem; + padding: 0.5rem 2rem 0 2rem; background-color: var(--readerSettings-theme); color: var(--readerSettings-textColor); - bottom: 0; - left: 0; - right: 0; font-size: 1rem; text-align: center; } +.reader-footer-item{ + display: flex; +} + t-t-s.tts-highlight { color: var(--theme-onSecondary); background-color: var(--theme-secondary); diff --git a/android/app/src/main/assets/js/index.js b/android/app/src/main/assets/js/index.js index a505ec0c5..f5e6777ac 100644 --- a/android/app/src/main/assets/js/index.js +++ b/android/app/src/main/assets/js/index.js @@ -1,7 +1,9 @@ class Reader { constructor() { - this.percentage = - showScrollPercentage && document.getElementById('reader-percentage'); + this.footerWrapper = document.getElementById('reader-footer-wrapper'); + this.percentage = document.getElementById('reader-percentage'); + this.battery = document.getElementById('reader-battery'); + this.time = document.getElementById('reader-time'); this.paddingTop = parseInt( getComputedStyle(document.querySelector('html')).getPropertyValue( 'padding-top', @@ -23,12 +25,91 @@ class Reader { }), autoSaveInterval, ); + this.time.innerText = new Date().toLocaleTimeString(undefined, { + hour: '2-digit', + minute: '2-digit', + }); + this.timeInterval = setInterval(() => { + this.time.innerText = new Date().toLocaleTimeString(undefined, { + hour: '2-digit', + minute: '2-digit', + }); + }, 60000); + this.updateBatteryLevel(batteryLevel); + this.updateGeneralSettings(initSettings); } refresh = () => { this.chapterHeight = this.chapter.scrollHeight + this.paddingTop; }; post = obj => window.ReactNativeWebView.postMessage(JSON.stringify(obj)); + updateReaderSettings = settings => { + document.documentElement.style.setProperty( + '--readerSettings-theme', + settings.theme, + ); + document.documentElement.style.setProperty( + '--readerSettings-padding', + settings.padding + '%', + ); + document.documentElement.style.setProperty( + '--readerSettings-textSize', + settings.textSize + 'px', + ); + document.documentElement.style.setProperty( + '--readerSettings-textColor', + settings.textColor, + ); + document.documentElement.style.setProperty( + '--readerSettings-textAlign', + settings.textAlign, + ); + document.documentElement.style.setProperty( + '--readerSettings-lineHeight', + settings.lineHeight, + ); + document.documentElement.style.setProperty( + '--readerSettings-fontFamily', + settings.fontFamily, + ); + new FontFace( + settings.fontFamily, + 'url("file:///android_asset/fonts/' + settings.fontFamily + '.ttf")', + ) + .load() + .then(function (loadedFont) { + document.fonts.add(loadedFont); + }); + }; + updateGeneralSettings = settings => { + this.showScrollPercentage = settings.showScrollPercentage; + this.showBatteryAndTime = settings.showBatteryAndTime; + if (settings.swipeGestures) { + swipeHandler.enable(); + } else { + swipeHandler.disable(); + } + if (!this.showScrollPercentage && !this.showBatteryAndTime) { + this.footerWrapper.classList.add('d-none'); + } else { + this.footerWrapper.classList.remove('d-none'); + if (this.showScrollPercentage) { + this.percentage.classList.remove('hidden'); + } else { + this.percentage.classList.add('hidden'); + } + if (this.showBatteryAndTime) { + this.battery.classList.remove('hidden'); + this.time.classList.remove('hidden'); + } else { + this.battery.classList.add('hidden'); + this.time.classList.add('hidden'); + } + } + }; + updateBatteryLevel = level => { + this.battery.innerText = Math.ceil(level * 100) + '%'; + }; } class ScrollHandler { constructor(reader) { @@ -109,13 +190,9 @@ class ScrollHandler { } class SwipeHandler { - constructor(reader) { - this.reader = reader; + constructor() { this.initialX = null; this.initialY = null; - if (swipeGestures) { - this.enable(); - } } touchStartHandler = e => { @@ -128,7 +205,7 @@ class SwipeHandler { let diffY = e.changedTouches[0].screenY - this.initialY; if (Math.abs(diffX) > Math.abs(diffY) && Math.abs(diffX) > 10) { e.preventDefault(); - this.reader.post({ type: diffX < 0 ? 'next' : 'prev' }); + reader.post({ type: diffX < 0 ? 'next' : 'prev' }); } }; @@ -172,7 +249,7 @@ class TextToSpeech { }; } +var swipeHandler = new SwipeHandler(); var reader = new Reader(); var scrollHandler = new ScrollHandler(reader); -var swipeHandler = new SwipeHandler(reader); var tts = new TextToSpeech(reader); diff --git a/android/app/src/main/java/com/rajarsheechatterjee/LNReader/MainApplication.java b/android/app/src/main/java/com/rajarsheechatterjee/LNReader/MainApplication.java index ecefb3248..9e320fb5b 100644 --- a/android/app/src/main/java/com/rajarsheechatterjee/LNReader/MainApplication.java +++ b/android/app/src/main/java/com/rajarsheechatterjee/LNReader/MainApplication.java @@ -13,6 +13,7 @@ import com.facebook.soloader.SoLoader; import com.rajarsheechatterjee.NavigationBarColor.NavigationBarColorPackage; +import com.rajarsheechatterjee.TextFile.TextFilePackage; import com.rajarsheechatterjee.VolumeButtonListener.VolumeButtonListenerPackage; import com.rajarsheechatterjee.ZipArchive.ZipArchivePackage; @@ -36,6 +37,7 @@ protected List getPackages() { packages.add(new NavigationBarColorPackage()); packages.add(new VolumeButtonListenerPackage()); packages.add(new ZipArchivePackage()); + packages.add(new TextFilePackage()); return packages; } diff --git a/android/app/src/main/java/com/rajarsheechatterjee/TextFile/TextFile.java b/android/app/src/main/java/com/rajarsheechatterjee/TextFile/TextFile.java new file mode 100644 index 000000000..be5fd6463 --- /dev/null +++ b/android/app/src/main/java/com/rajarsheechatterjee/TextFile/TextFile.java @@ -0,0 +1,53 @@ +package com.rajarsheechatterjee.TextFile; + +import androidx.annotation.NonNull; + +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 java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.FileWriter; + +public class TextFile extends ReactContextBaseJavaModule { + TextFile(ReactApplicationContext context) { + super(context); + } + + @NonNull + @Override + public String getName() { + return "TextFile"; + } + + @ReactMethod + public void writeFile(String path, String content, Promise promise) { + try { + FileWriter fw = new FileWriter(path); + fw.write(content); + fw.close(); + promise.resolve(null); + } catch (Exception e) { + promise.reject(e); + } + } + + @ReactMethod void readFile(String path, Promise promise) { + try { + StringBuilder sb = new StringBuilder(); + BufferedReader br = new BufferedReader(new FileReader(path)); + String line; + while ((line = br.readLine()) != null) sb.append(line).append('\n'); + promise.resolve(sb.toString()); + } catch (Exception e) { + promise.reject(e); + } + + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/rajarsheechatterjee/TextFile/TextFilePackage.java b/android/app/src/main/java/com/rajarsheechatterjee/TextFile/TextFilePackage.java new file mode 100644 index 000000000..2b480e7ee --- /dev/null +++ b/android/app/src/main/java/com/rajarsheechatterjee/TextFile/TextFilePackage.java @@ -0,0 +1,32 @@ +package com.rajarsheechatterjee.TextFile; + +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 java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class TextFilePackage implements ReactPackage { + @NonNull + @Override + public List createNativeModules(@NonNull ReactApplicationContext reactApplicationContext) { + List modules = new ArrayList<>(); + try { + modules.add(new TextFile(reactApplicationContext)); + } catch (Exception e) { + throw new RuntimeException(e); + } + return modules; + } + + @NonNull + @Override + public List createViewManagers(@NonNull ReactApplicationContext reactApplicationContext) { + return Collections.emptyList(); + } +} diff --git a/metro.config.js b/metro.config.js index 9430b0f9b..0dabc4893 100644 --- a/metro.config.js +++ b/metro.config.js @@ -1,4 +1,44 @@ // Learn more https://docs.expo.io/guides/customizing-metro +const path = require('path'); +const fs = require('fs'); +const { mergeConfig } = require('metro-config'); const { getDefaultConfig } = require('expo/metro-config'); +const defaultConfig = getDefaultConfig(__dirname); -module.exports = getDefaultConfig(__dirname); +const map = { + '.ico': 'image/x-icon', + '.html': 'text/html', + '.js': 'text/javascript', + '.json': 'application/json', + '.css': 'text/css', + '.png': 'image/png', + '.jpg': 'image/jpeg', +}; +const customConfig = { + server: { + port: 8081, + enhanceMiddleware: (metroMiddleware, metroServer) => { + return (request, res, next) => { + const filePath = path.join( + __dirname, + 'android/app/src/main', + request._parsedUrl.path || '', + ); + const ext = path.parse(filePath).ext; + if (fs.existsSync(filePath)) { + try { + const data = fs.readFileSync(filePath); + res.setHeader('Content-type', map[ext] || 'text/plain'); + res.end(data); + } catch (err) { + res.statusCode = 500; + res.end(`Error getting the file: ${err}.`); + } + } else { + return metroMiddleware(request, res, next); + } + }; + }, + }, +}; +module.exports = mergeConfig(defaultConfig, customConfig); diff --git a/package-lock.json b/package-lock.json index 1c1b383e6..78f584f20 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,7 +53,7 @@ "react-native-shimmer-placeholder": "^2.0.9", "react-native-tab-view": "^3.5.2", "react-native-vector-icons": "^9.0.0", - "react-native-webview": "11.26.0", + "react-native-webview": "^13.8.1", "sanitize-html": "^2.7.0", "urlencode": "^2.0.0" }, @@ -14716,9 +14716,9 @@ } }, "node_modules/react-native-webview": { - "version": "11.26.0", - "resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-11.26.0.tgz", - "integrity": "sha512-4T4CKRm8xlaQDz9h/bCMPGAvtkesrhkRWqCX9FDJEzBToaVUIsV0ZOqtC4w/JSnCtFKKYiaC1ReJtCGv+4mFeQ==", + "version": "13.8.1", + "resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-13.8.1.tgz", + "integrity": "sha512-7Jqm1WzWJrOWraBAXQfKtr/Uo5Jw/IJHzC40jYLwgV/eVGmLJ9BpGKw6QVw7wpRkjmTZ2Typ4B1aHJLJJQFslA==", "dependencies": { "escape-string-regexp": "2.0.0", "invariant": "2.2.4" diff --git a/package.json b/package.json index c81faa46a..3439dcfd6 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "react-native-shimmer-placeholder": "^2.0.9", "react-native-tab-view": "^3.5.2", "react-native-vector-icons": "^9.0.0", - "react-native-webview": "11.26.0", + "react-native-webview": "^13.8.1", "sanitize-html": "^2.7.0", "urlencode": "^2.0.0" }, diff --git a/reader_playground/README.md b/reader_playground/README.md deleted file mode 100644 index 274223295..000000000 --- a/reader_playground/README.md +++ /dev/null @@ -1,5 +0,0 @@ -1. Create `index.html` with [template](./template.index.html) content in this folder/ -2. Replace `${hmtl}` by your chapter text. -3. Edit files in `/android/app/src/main/assets` folder. -4. Open index.html in browse for quick test. -5. rebuild the app to test in real reader. diff --git a/reader_playground/template.index.html b/reader_playground/template.index.html deleted file mode 100644 index 8499a5d00..000000000 --- a/reader_playground/template.index.html +++ /dev/null @@ -1,71 +0,0 @@ - - - - - - - - - - - -
- - ${html} - -
-
-
- - - diff --git a/src/database/queries/ChapterQueries.ts b/src/database/queries/ChapterQueries.ts index c6ba30089..4efc5b18e 100644 --- a/src/database/queries/ChapterQueries.ts +++ b/src/database/queries/ChapterQueries.ts @@ -12,6 +12,7 @@ import { Plugin } from '@plugins/types'; import { Update } from '../types'; import { noop } from 'lodash-es'; import { getString } from '@strings/translations'; +import TextFile from '@native/TextFile'; const db = SQLite.openDatabase('lnreader.db'); @@ -233,7 +234,7 @@ const createChapterFolder = async ( if (!(await RNFS.exists(p))) { await RNFS.mkdir(p); if (nomedia) { - await RNFS.writeFile(nomediaPath, ',', 'utf8'); + await TextFile.writeFile(nomediaPath, ','); } } }; @@ -271,7 +272,7 @@ const downloadFiles = async ( RNFS.writeFile(fileurl, imageb64, 'base64'); } } - RNFS.writeFile(folder + 'index.html', loadedCheerio.html()); + TextFile.writeFile(folder + 'index.html', loadedCheerio.html()); } catch (error) { throw error; } diff --git a/src/database/queries/LibraryQueries.ts b/src/database/queries/LibraryQueries.ts index 807fd70ea..0f49704f4 100644 --- a/src/database/queries/LibraryQueries.ts +++ b/src/database/queries/LibraryQueries.ts @@ -57,28 +57,31 @@ export const getLibraryNovelsFromDb = ( }; const getLibraryWithCategoryQuery = ` - SELECT - NIL.*, chaptersUnread, chaptersDownloaded, lastReadAt, lastUpdatedAt - FROM + SELECT * + FROM ( - SELECT - Novel.*, - category, - categoryId - FROM - Novel LEFT JOIN ( - SELECT NovelId, name as category, categoryId FROM (NovelCategory JOIN Category ON NovelCategory.categoryId = Category.id) - ) as NC ON Novel.id = NC.novelId - WHERE inLibrary = 1 - ) as NIL - LEFT JOIN - ( - SELECT - SUM(unread) as chaptersUnread, SUM(isDownloaded) as chaptersDownloaded, - novelId, MAX(readTime) as lastReadAt, MAX(updatedTime) as lastUpdatedAt - FROM Chapter - GROUP BY novelId - ) as C ON NIL.id = C.novelId + SELECT NIL.*, chaptersUnread, chaptersDownloaded, lastReadAt, lastUpdatedAt + FROM + ( + SELECT + Novel.*, + category, + categoryId + FROM + Novel LEFT JOIN ( + SELECT NovelId, name as category, categoryId FROM (NovelCategory JOIN Category ON NovelCategory.categoryId = Category.id) + ) as NC ON Novel.id = NC.novelId + WHERE inLibrary = 1 + ) as NIL + LEFT JOIN + ( + SELECT + SUM(unread) as chaptersUnread, SUM(isDownloaded) as chaptersDownloaded, + novelId, MAX(readTime) as lastReadAt, MAX(updatedTime) as lastUpdatedAt + FROM Chapter + GROUP BY novelId + ) as C ON NIL.id = C.novelId + ) WHERE 1 = 1 `; export const getLibraryWithCategory = ({ @@ -107,7 +110,6 @@ export const getLibraryWithCategory = ({ if (sortOrder) { query += ` ORDER BY ${sortOrder} `; } - return new Promise(resolve => db.transaction(tx => { tx.executeSql( diff --git a/src/hooks/common/useFullscreenMode.ts b/src/hooks/common/useFullscreenMode.ts index eef31e5b8..80fb74821 100644 --- a/src/hooks/common/useFullscreenMode.ts +++ b/src/hooks/common/useFullscreenMode.ts @@ -15,11 +15,8 @@ import { const useFullscreenMode = () => { const { addListener } = useNavigation(); - const readerSettings = useChapterReaderSettings(); + const { theme: backgroundColor } = useChapterReaderSettings(); const { fullScreenMode } = useChapterGeneralSettings(); - - const backgroundColor = readerSettings.theme; - const theme = useTheme(); const setImmersiveMode = useCallback(() => { diff --git a/src/hooks/persisted/useNovel.ts b/src/hooks/persisted/useNovel.ts index 9de23249a..becbe910a 100644 --- a/src/hooks/persisted/useNovel.ts +++ b/src/hooks/persisted/useNovel.ts @@ -123,43 +123,6 @@ export const useNovel = (novelPath: string, pluginId: string) => { useMMKVObject( `${NOVEL_SETTINSG_PREFIX}_${pluginId}_${novelPath}`, ); - const getNovel = async () => { - let novel = await _getNovel(novelPath, pluginId); - if (!novel) { - const sourceNovel = await fetchNovel(pluginId, novelPath).catch(() => { - throw new Error(getString('updatesScreen.unableToGetNovel')); - }); - await insertNovelAndChapters(pluginId, sourceNovel); - novel = await _getNovel(novelPath, pluginId); - if (!novel) { - return; - } - } - let pages: string[]; - if (novel.totalPages > 0) { - pages = Array(novel.totalPages) - .fill(0) - .map((v, idx) => String(idx + 1)); - const key = `${NOVEL_PAGE_UPDATES_PREFIX}_${novel.id}`; - const hasUpdates = getMMKVObject(key); - if (hasUpdates) { - if (pages.length > hasUpdates.length) { - setMMKVObject( - key, - hasUpdates.concat( - Array(pages.length - hasUpdates.length).fill(false), - ), - ); - } - } else { - setMMKVObject(key, Array(novel.totalPages).fill(false)); - } - } else { - pages = (await getCustomPages(novel.id)).map(c => c.page); - } - setPages(pages); - setNovel(novel); - }; const openPage = useCallback((index: number) => { setPageIndex(index); @@ -330,63 +293,103 @@ export const useNovel = (novelPath: string, pluginId: string) => { [novel], ); - useEffect(() => { - getNovel().then(() => setLoading(false)); + const getNovel = useCallback(async () => { + let novel = await _getNovel(novelPath, pluginId); + if (!novel) { + const sourceNovel = await fetchNovel(pluginId, novelPath).catch(() => { + throw new Error(getString('updatesScreen.unableToGetNovel')); + }); + await insertNovelAndChapters(pluginId, sourceNovel); + novel = await _getNovel(novelPath, pluginId); + if (!novel) { + return; + } + } + let pages: string[]; + if (novel.totalPages > 0) { + pages = Array(novel.totalPages) + .fill(0) + .map((v, idx) => String(idx + 1)); + const key = `${NOVEL_PAGE_UPDATES_PREFIX}_${novel.id}`; + const hasUpdates = getMMKVObject(key); + if (hasUpdates) { + if (pages.length > hasUpdates.length) { + setMMKVObject( + key, + hasUpdates.concat( + Array(pages.length - hasUpdates.length).fill(false), + ), + ); + } + } else { + setMMKVObject(key, Array(novel.totalPages).fill(false)); + } + } else { + pages = (await getCustomPages(novel.id)).map(c => c.page); + } + setPages(pages); + setNovel(novel); }, []); - useEffect(() => { - const getChapters = async () => { - const page = pages[pageIndex]; - if (novel && page) { - const hasUpdatesKey = `${NOVEL_PAGE_UPDATES_PREFIX}_${novel.id}`; - const hasUpdates = getMMKVObject(hasUpdatesKey); - let chapters = await _getPageChapters( + const getChapters = useCallback(async () => { + const page = pages[pageIndex]; + if (novel && page) { + const hasUpdatesKey = `${NOVEL_PAGE_UPDATES_PREFIX}_${novel.id}`; + const hasUpdates = getMMKVObject(hasUpdatesKey); + let chapters = await _getPageChapters( + novel.id, + novelSettings.sort, + novelSettings.filter, + page, + ); + if (hasUpdates && (hasUpdates[pageIndex] || !chapters.length)) { + const sourcePage = await fetchPage(pluginId, novelPath, page); + const sourceChapters = sourcePage.chapters.map(ch => { + return { + ...ch, + page, + }; + }); + await insertChapters(novel.id, sourceChapters); + const latestChapkey = `${NOVEL_LATEST_CHAPTER_PREFIX}_${novel.id}`; + const latestChapter = getMMKVObject(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); + } + chapters = await _getPageChapters( novel.id, novelSettings.sort, novelSettings.filter, page, ); - if (hasUpdates && (hasUpdates[pageIndex] || !chapters.length)) { - const sourcePage = await fetchPage(pluginId, novelPath, page); - const sourceChapters = sourcePage.chapters.map(ch => { - return { - ...ch, - page, - }; - }); - await insertChapters(novel.id, sourceChapters); - const latestChapkey = `${NOVEL_LATEST_CHAPTER_PREFIX}_${novel.id}`; - const latestChapter = getMMKVObject(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); - } - chapters = await _getPageChapters( - novel.id, - novelSettings.sort, - novelSettings.filter, - page, - ); - } - setChapters(chapters); } - }; - getChapters(); + setChapters(chapters); + if (loading) { + setLoading(false); + } + } }, [novel, novelSettings, pageIndex]); + useEffect(() => { + getNovel(); + }, []); + useEffect(() => { + getChapters(); + }, [getChapters]); return { loading, diff --git a/src/hooks/persisted/useSettings.ts b/src/hooks/persisted/useSettings.ts index 53de1f9da..b6f1ce46a 100644 --- a/src/hooks/persisted/useSettings.ts +++ b/src/hooks/persisted/useSettings.ts @@ -147,7 +147,7 @@ const initialBrowseSettings: BrowseSettings = { showAniList: true, }; -const initialChapterGeneralSettings: ChapterGeneralSettings = { +export const initialChapterGeneralSettings: ChapterGeneralSettings = { fullScreenMode: true, swipeGestures: false, showScrollPercentage: true, @@ -160,7 +160,7 @@ const initialChapterGeneralSettings: ChapterGeneralSettings = { removeExtraParagraphSpacing: false, }; -const initialChapterReaderSettings: ChapterReaderSettings = { +export const initialChapterReaderSettings: ChapterReaderSettings = { theme: '#292832', textColor: '#CCCCCC', textSize: 16, diff --git a/src/native/TextFile.ts b/src/native/TextFile.ts new file mode 100644 index 000000000..df6a15874 --- /dev/null +++ b/src/native/TextFile.ts @@ -0,0 +1,8 @@ +import { NativeModules } from 'react-native'; +interface ZipArchiveInterface { + writeFile: (path: string, content: string) => Promise; + readFile: (path: string) => Promise; +} +const { TextFile } = NativeModules; + +export default TextFile as ZipArchiveInterface; diff --git a/src/plugins/pluginManager.ts b/src/plugins/pluginManager.ts index ad7a86539..7660656b0 100644 --- a/src/plugins/pluginManager.ts +++ b/src/plugins/pluginManager.ts @@ -13,6 +13,7 @@ import { fetchApi, fetchFile, fetchText } from './helpers/fetch'; import { defaultCover } from './helpers/constants'; import { encode, decode } from 'urlencode'; import { getString } from '@strings/translations'; +import TextFile from '@native/TextFile'; const pluginsFilePath = PluginDownloadFolder + '/plugins.json'; @@ -48,39 +49,42 @@ const initPlugin = (rawCode: string) => { }; const plugins: Record = {}; +let serializedPlugins: Record; const serializePlugin = async ( pluginId: string, rawCode: string, installed: boolean, ) => { - let serializedPlugins: Record = {}; - if (await RNFS.exists(pluginsFilePath)) { - const content = await RNFS.readFile(pluginsFilePath); + if (!serializedPlugins && (await RNFS.exists(pluginsFilePath))) { + const content = await TextFile.readFile(pluginsFilePath); serializedPlugins = JSON.parse(content); } if (installed) { serializedPlugins[pluginId] = rawCode; } else { - delete serializedPlugins[pluginId]; + serializedPlugins[pluginId] = undefined; } if (!(await RNFS.exists(PluginDownloadFolder))) { await RNFS.mkdir(PluginDownloadFolder); } - await RNFS.writeFile(pluginsFilePath, JSON.stringify(serializedPlugins)); + await TextFile.writeFile(pluginsFilePath, JSON.stringify(serializedPlugins)); }; const deserializePlugins = async () => { - if (await RNFS.exists(pluginsFilePath)) { - const content = await RNFS.readFile(pluginsFilePath); - const serializedPlugins: Record = JSON.parse(content); - for (const pluginId in serializedPlugins) { - const plugin = initPlugin(serializedPlugins[pluginId]); - if (plugin) { - plugins[pluginId] = plugin; + await TextFile.readFile(pluginsFilePath) + .then(content => { + serializedPlugins = JSON.parse(content); + for (const script of Object.values(serializedPlugins)) { + const plugin = initPlugin(script as string); + if (plugin) { + plugins[plugin.id] = plugin; + } } - } - } + }) + .catch(() => { + // nothing to read + }); }; const installPlugin = async (url: string): Promise => { @@ -108,8 +112,8 @@ const installPlugin = async (url: string): Promise => { }; const uninstallPlugin = async (_plugin: PluginItem) => { - delete plugins[_plugin.id]; - serializePlugin(_plugin.id, '', false); + plugins[_plugin.id] = undefined; + return serializePlugin(_plugin.id, '', false); }; const updatePlugin = async (plugin: PluginItem) => { diff --git a/src/screens/browse/components/PluginSection.tsx b/src/screens/browse/components/PluginSection.tsx index 23451edbb..669236ded 100644 --- a/src/screens/browse/components/PluginSection.tsx +++ b/src/screens/browse/components/PluginSection.tsx @@ -2,7 +2,13 @@ import React, { memo, useCallback, useState } from 'react'; import { PluginItem } from '@plugins/types'; import { getString } from '@strings/translations'; import { ThemeColors } from '@theme/types'; -import { RefreshControl, SectionList, StyleSheet, Text } from 'react-native'; +import { + RefreshControl, + SectionList, + SectionListRenderItem, + StyleSheet, + Text, +} from 'react-native'; import PluginCard from './PluginCard'; import { usePlugins } from '@hooks/persisted'; import { BrowseScreenProps } from '@navigators/types'; @@ -45,6 +51,21 @@ const PluginSection = ({ }, [], ); + const renderItem: SectionListRenderItem = useCallback( + ({ item }) => ( + + ), + [], + ); + return ( )} - renderItem={({ item }) => ( - - )} + renderItem={renderItem} refreshControl={ installedTab ? ( <> diff --git a/src/screens/reader/ReaderScreen.tsx b/src/screens/reader/ReaderScreen.tsx index 4704b3fb0..feeaba5e3 100644 --- a/src/screens/reader/ReaderScreen.tsx +++ b/src/screens/reader/ReaderScreen.tsx @@ -42,7 +42,6 @@ import WebViewReader from './components/WebViewReader'; import { useFullscreenMode, useTextToSpeech } from '@hooks'; import ReaderBottomSheetV2 from './components/ReaderBottomSheet/ReaderBottomSheet'; import { defaultTo } from 'lodash-es'; -import BottomInfoBar from './components/BottomInfoBar/BottomInfoBar'; import { sanitizeChapterText } from './utils/sanitizeChapterText'; import ChapterDrawer from './components/ChapterDrawer'; import ChapterLoadingScreen from './ChapterLoadingScreen/ChapterLoadingScreen'; @@ -52,6 +51,7 @@ import { ChapterInfo } from '@database/types'; import WebView, { WebViewNavigation } from 'react-native-webview'; import { NovelDownloadFolder } from '@utils/constants/download'; import { getString } from '@strings/translations'; +import TextFile from '@native/TextFile'; const Chapter = ({ route, navigation }: ChapterScreenProps) => { const drawerRef = useRef(null); @@ -102,7 +102,6 @@ export const ChapterContent = ({ const theme = useTheme(); const { - swipeGestures, useVolumeButtons, autoScroll, autoScrollInterval, @@ -170,7 +169,7 @@ export const ChapterContent = ({ try { const filePath = `${NovelDownloadFolder}/${novel.pluginId}/${chapter.novelId}/${chapter.id}/index.html`; if (await RNFS.exists(filePath)) { - sourceChapter.chapterText = await RNFS.readFile(filePath); + sourceChapter.chapterText = await TextFile.readFile(filePath); } else { await fetchChapter(novel.pluginId, chapter.path) .then(res => { @@ -352,7 +351,6 @@ export const ChapterContent = ({ - {!hidden && ( <> diff --git a/src/screens/reader/components/BottomInfoBar/BottomInfoBar.tsx b/src/screens/reader/components/BottomInfoBar/BottomInfoBar.tsx deleted file mode 100644 index 5dd0d6d5a..000000000 --- a/src/screens/reader/components/BottomInfoBar/BottomInfoBar.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { StyleSheet, Text, View } from 'react-native'; -import React, { useEffect, useState } from 'react'; -import { useBatteryLevel } from 'react-native-device-info'; -import dayjs from 'dayjs'; - -import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { - useChapterGeneralSettings, - useChapterReaderSettings, -} from '@hooks/persisted'; - -const BottomInfoBar = () => { - const { bottom: bottomInset } = useSafeAreaInsets(); - const { showBatteryAndTime, fullScreenMode } = useChapterGeneralSettings(); - - const { textColor } = useChapterReaderSettings(); - - const batteryLevel = useBatteryLevel(); - - const [currentTime, setCurrentTime] = useState(); - - useEffect(() => { - const timeInterval = setInterval(() => { - setCurrentTime(new Date().toISOString()); - }, 60000); - - return () => clearInterval(timeInterval); - }, []); - - if (showBatteryAndTime) { - return ( - - {showBatteryAndTime && batteryLevel ? ( - - {Math.ceil(batteryLevel * 100) + '%'} - - ) : null} - {showBatteryAndTime ? ( - - {dayjs(currentTime).format('LT')} - - ) : null} - - ); - } else { - return null; - } -}; - -export default BottomInfoBar; - -const styles = StyleSheet.create({ - container: { - position: 'absolute', - paddingVertical: 10, - left: 0, - right: 0, - bottom: 0, - zIndex: 1, - flexDirection: 'row', - paddingHorizontal: 32, - alignItems: 'center', - justifyContent: 'space-between', - }, -}); diff --git a/src/screens/reader/components/ChapterDrawer.tsx b/src/screens/reader/components/ChapterDrawer.tsx index 2ac47adf8..72c5238ce 100644 --- a/src/screens/reader/components/ChapterDrawer.tsx +++ b/src/screens/reader/components/ChapterDrawer.tsx @@ -176,7 +176,7 @@ const ChapterDrawer = ({ pageIndex = 0; } setPageIndex(pageIndex); - }, [chapter]); + }, [chapter, pages]); return ( {getString('common.chapters')} diff --git a/src/screens/reader/components/WebViewReader.tsx b/src/screens/reader/components/WebViewReader.tsx index a64cfba04..f19f9a432 100644 --- a/src/screens/reader/components/WebViewReader.tsx +++ b/src/screens/reader/components/WebViewReader.tsx @@ -1,17 +1,28 @@ -import { FC } from 'react'; -import { Dimensions, StatusBar } from 'react-native'; +import { FC, useEffect, useMemo } from 'react'; +import { + Dimensions, + NativeEventEmitter, + NativeModules, + StatusBar, +} from 'react-native'; import WebView, { WebViewNavigation } from 'react-native-webview'; import color from 'color'; -import { - useChapterGeneralSettings, - useChapterReaderSettings, - useTheme, -} from '@hooks/persisted'; +import { useTheme } from '@hooks/persisted'; import { ChapterInfo } from '@database/types'; import { getString } from '@strings/translations'; import { getPlugin } from '@plugins/pluginManager'; +import { MMKVStorage, getMMKVObject } from '@utils/mmkv/mmkv'; +import { + CHAPTER_GENERAL_SETTINGS, + CHAPTER_READER_SETTINGS, + ChapterGeneralSettings, + ChapterReaderSettings, + initialChapterGeneralSettings, + initialChapterReaderSettings, +} from '@hooks/persisted/useSettings'; +import { getBatteryLevelSync } from 'react-native-device-info'; type WebViewPostEvent = { type: string; @@ -26,7 +37,6 @@ type WebViewReaderProps = { chapter: ChapterInfo; }; html: string; - swipeGestures: boolean; nextChapter: ChapterInfo; webViewRef: React.RefObject; saveProgress(percentage: number): void; @@ -40,7 +50,6 @@ const WebViewReader: FC = props => { const { data, html, - swipeGestures, nextChapter, webViewRef, saveProgress, @@ -49,14 +58,64 @@ const WebViewReader: FC = props => { navigateToChapterBySwipe, onWebViewNavigationStateChange, } = props; - + const assetsUriPrefix = useMemo( + () => (__DEV__ ? 'http://localhost:8081/assets' : 'file:///android_asset'), + [], + ); + const { RNDeviceInfo } = NativeModules; + const deviceInfoEmitter = new NativeEventEmitter(RNDeviceInfo); const theme = useTheme(); const { novel, chapter } = data; - const readerSettings = useChapterReaderSettings(); - const { showScrollPercentage } = useChapterGeneralSettings(); - + const readerSettings = useMemo( + () => + getMMKVObject(CHAPTER_READER_SETTINGS) || + initialChapterReaderSettings, + [], + ); + const { showScrollPercentage, swipeGestures, showBatteryAndTime } = useMemo( + () => + getMMKVObject(CHAPTER_GENERAL_SETTINGS) || + initialChapterGeneralSettings, + [], + ); + const batteryLevel = useMemo(getBatteryLevelSync, []); const layoutHeight = Dimensions.get('window').height; const plugin = getPlugin(novel?.pluginId); + + useEffect(() => { + const mmkvListener = MMKVStorage.addOnValueChangedListener(key => { + switch (key) { + case CHAPTER_READER_SETTINGS: + webViewRef.current?.injectJavaScript( + `reader.updateReaderSettings(${MMKVStorage.getString( + CHAPTER_READER_SETTINGS, + )})`, + ); + break; + case CHAPTER_GENERAL_SETTINGS: + webViewRef.current?.injectJavaScript( + `reader.updateGeneralSettings(${MMKVStorage.getString( + CHAPTER_GENERAL_SETTINGS, + )})`, + ); + break; + } + }); + + const subscription = deviceInfoEmitter.addListener( + 'RNDeviceInfo_batteryLevelDidChange', + (level: number) => { + webViewRef.current?.injectJavaScript( + `reader.updateBatteryLevel(${level})`, + ); + }, + ); + + return () => { + subscription.remove(); + mmkvListener.remove(); + }; + }); return ( = props => { } @font-face { font-family: ${readerSettings.fontFamily}; - src: url("file:///android_asset/fonts/${ - readerSettings.fontFamily - }.ttf"); + src: url("${assetsUriPrefix}/fonts/${ + readerSettings.fontFamily + }.ttf"); } - + @@ -148,7 +211,13 @@ const WebViewReader: FC = props => { ${html}
-
+
${getString( @@ -167,7 +236,7 @@ const WebViewReader: FC = props => {
` } - +