Compare commits
67 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a870442022 | |||
| a9a176297e | |||
| f4c0a647fb | |||
| 06e7e45df0 | |||
| c538bf4956 | |||
| e1679a41f1 | |||
| 3c2f4a59b7 | |||
| 8c13b1ff12 | |||
| 45083e5fd1 | |||
| 3b37c1cb33 | |||
| 48187d7d54 | |||
| 3af0b1afbc | |||
| b9695e7f56 | |||
| 7c068aa7d5 | |||
| 8b182f1e31 | |||
| 5029513424 | |||
| f4c6a3b4a1 | |||
| 27f5e20107 | |||
| c249b52304 | |||
| c179e0e5bd | |||
| b78382cc03 | |||
| 9df3694c04 | |||
| dfd300c358 | |||
| 76c108597d | |||
| 23ff02674e | |||
| 55e2371f5d | |||
| 4eb225cdf3 | |||
| 3cb41c8bfb | |||
| 05511e073e | |||
| 085b9b9eb3 | |||
| 9290d59f77 | |||
| c0345ae9a9 | |||
| c12a7f378a | |||
| d3c2d130e1 | |||
| 134c72a683 | |||
| 4c326e35c0 | |||
| 419420efd1 | |||
| 871fc905ce | |||
| 5a8c652f68 | |||
| 97f2f84fa5 | |||
| 65a3e8fd2d | |||
| d32cb41475 | |||
| 5bb3be576e | |||
| 2f09b7e29e | |||
| b672aba6ff | |||
| 80363d6016 | |||
| 195fcaf9b2 | |||
| 5ec08b3add | |||
| 625cdff212 | |||
| 83afe9c254 | |||
| 404f59323a | |||
| 20ad556039 | |||
| 14c994486c | |||
| c6884458a1 | |||
| 9dab2eccaa | |||
| 2e21a9fd33 | |||
| 781e8df740 | |||
| fa4d810ccb | |||
| 37f531dd93 | |||
| 1ea98ec031 | |||
| 65e5377345 | |||
| 2b78350ad9 | |||
| 69217b415d | |||
| 8c04ee3d33 | |||
| 1f78188fd1 | |||
| 5fb9d70036 | |||
| b3ec043e32 |
Binary file not shown.
|
Before Width: | Height: | Size: 55 KiB |
@@ -6,31 +6,28 @@ import AppLoading from "expo-app-loading";
|
||||
|
||||
import Router from "./src/navigation/Router";
|
||||
|
||||
import { createTables, createIndexes } from "./src/services/db";
|
||||
import { setStatusBar } from "./src/services/asyncStorage";
|
||||
|
||||
// Redyx
|
||||
// Redux
|
||||
import { Provider } from "react-redux";
|
||||
import { persistor, store } from "./src/redux/store/configureStore";
|
||||
import { persistor, store } from "./src/redux/store";
|
||||
import { PersistGate } from "redux-persist/lib/integration/react";
|
||||
import { createDB, deleteDb } from "./src/database/DBHelper";
|
||||
|
||||
const getFonts = () =>
|
||||
Font.loadAsync({
|
||||
"pt-sans-bold": require("./assets/fonts/PTSansNarrow-Bold.ttf"),
|
||||
"arbutus-slab": require("./assets/fonts/ArbutusSlab-Regular.ttf"),
|
||||
domine: require("./assets/fonts/Domine-VariableFont_wght.ttf"),
|
||||
lato: require("./assets/fonts/Lato-Regular.ttf"),
|
||||
"noto-sans": require("./assets/fonts/NotoSans-Regular.ttf"),
|
||||
"open-sans": require("./assets/fonts/OpenSans-Regular.ttf"),
|
||||
"pt-serif": require("./assets/fonts/PTSerif-Regular.ttf"),
|
||||
});
|
||||
|
||||
const App = () => {
|
||||
const [fontsLoaded, setFontsLoaded] = useState(false);
|
||||
|
||||
setStatusBar();
|
||||
|
||||
const createDb = () => {
|
||||
createTables();
|
||||
createIndexes();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
createDb();
|
||||
createDB();
|
||||
}, []);
|
||||
|
||||
if (fontsLoaded) {
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
"backgroundColor": "#000000"
|
||||
},
|
||||
"updates": {
|
||||
"fallbackToCacheTimeout": 0
|
||||
"fallbackToCacheTimeout": 0,
|
||||
"enabled": false
|
||||
},
|
||||
"assetBundlePatterns": ["**/*"],
|
||||
"ios": {
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 8.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.7 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 24 KiB |
Generated
+13
-381
@@ -1512,122 +1512,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@jimp/bmp": {
|
||||
"version": "0.16.1",
|
||||
"resolved": "https://registry.npmjs.org/@jimp/bmp/-/bmp-0.16.1.tgz",
|
||||
"integrity": "sha512-iwyNYQeBawrdg/f24x3pQ5rEx+/GwjZcCXd3Kgc+ZUd+Ivia7sIqBsOnDaMZdKCBPlfW364ekexnlOqyVa0NWg==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.7.2",
|
||||
"@jimp/utils": "^0.16.1",
|
||||
"bmp-js": "^0.1.0"
|
||||
}
|
||||
},
|
||||
"@jimp/core": {
|
||||
"version": "0.16.1",
|
||||
"resolved": "https://registry.npmjs.org/@jimp/core/-/core-0.16.1.tgz",
|
||||
"integrity": "sha512-la7kQia31V6kQ4q1kI/uLimu8FXx7imWVajDGtwUG8fzePLWDFJyZl0fdIXVCL1JW2nBcRHidUot6jvlRDi2+g==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.7.2",
|
||||
"@jimp/utils": "^0.16.1",
|
||||
"any-base": "^1.1.0",
|
||||
"buffer": "^5.2.0",
|
||||
"exif-parser": "^0.1.12",
|
||||
"file-type": "^9.0.0",
|
||||
"load-bmfont": "^1.3.1",
|
||||
"mkdirp": "^0.5.1",
|
||||
"phin": "^2.9.1",
|
||||
"pixelmatch": "^4.0.2",
|
||||
"tinycolor2": "^1.4.1"
|
||||
}
|
||||
},
|
||||
"@jimp/custom": {
|
||||
"version": "0.16.1",
|
||||
"resolved": "https://registry.npmjs.org/@jimp/custom/-/custom-0.16.1.tgz",
|
||||
"integrity": "sha512-DNUAHNSiUI/j9hmbatD6WN/EBIyeq4AO0frl5ETtt51VN1SvE4t4v83ZA/V6ikxEf3hxLju4tQ5Pc3zmZkN/3A==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.7.2",
|
||||
"@jimp/core": "^0.16.1"
|
||||
}
|
||||
},
|
||||
"@jimp/gif": {
|
||||
"version": "0.16.1",
|
||||
"resolved": "https://registry.npmjs.org/@jimp/gif/-/gif-0.16.1.tgz",
|
||||
"integrity": "sha512-r/1+GzIW1D5zrP4tNrfW+3y4vqD935WBXSc8X/wm23QTY9aJO9Lw6PEdzpYCEY+SOklIFKaJYUAq/Nvgm/9ryw==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.7.2",
|
||||
"@jimp/utils": "^0.16.1",
|
||||
"gifwrap": "^0.9.2",
|
||||
"omggif": "^1.0.9"
|
||||
}
|
||||
},
|
||||
"@jimp/jpeg": {
|
||||
"version": "0.16.1",
|
||||
"resolved": "https://registry.npmjs.org/@jimp/jpeg/-/jpeg-0.16.1.tgz",
|
||||
"integrity": "sha512-8352zrdlCCLFdZ/J+JjBslDvml+fS3Z8gttdml0We759PnnZGqrnPRhkOEOJbNUlE+dD4ckLeIe6NPxlS/7U+w==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.7.2",
|
||||
"@jimp/utils": "^0.16.1",
|
||||
"jpeg-js": "0.4.2"
|
||||
}
|
||||
},
|
||||
"@jimp/plugin-resize": {
|
||||
"version": "0.16.1",
|
||||
"resolved": "https://registry.npmjs.org/@jimp/plugin-resize/-/plugin-resize-0.16.1.tgz",
|
||||
"integrity": "sha512-u4JBLdRI7dargC04p2Ha24kofQBk3vhaf0q8FwSYgnCRwxfvh2RxvhJZk9H7Q91JZp6wgjz/SjvEAYjGCEgAwQ==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.7.2",
|
||||
"@jimp/utils": "^0.16.1"
|
||||
}
|
||||
},
|
||||
"@jimp/png": {
|
||||
"version": "0.16.1",
|
||||
"resolved": "https://registry.npmjs.org/@jimp/png/-/png-0.16.1.tgz",
|
||||
"integrity": "sha512-iyWoCxEBTW0OUWWn6SveD4LePW89kO7ZOy5sCfYeDM/oTPLpR8iMIGvZpZUz1b8kvzFr27vPst4E5rJhGjwsdw==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.7.2",
|
||||
"@jimp/utils": "^0.16.1",
|
||||
"pngjs": "^3.3.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"pngjs": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz",
|
||||
"integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@jimp/tiff": {
|
||||
"version": "0.16.1",
|
||||
"resolved": "https://registry.npmjs.org/@jimp/tiff/-/tiff-0.16.1.tgz",
|
||||
"integrity": "sha512-3K3+xpJS79RmSkAvFMgqY5dhSB+/sxhwTFA9f4AVHUK0oKW+u6r52Z1L0tMXHnpbAdR9EJ+xaAl2D4x19XShkQ==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.7.2",
|
||||
"utif": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"@jimp/types": {
|
||||
"version": "0.16.1",
|
||||
"resolved": "https://registry.npmjs.org/@jimp/types/-/types-0.16.1.tgz",
|
||||
"integrity": "sha512-g1w/+NfWqiVW4CaXSJyD28JQqZtm2eyKMWPhBBDCJN9nLCN12/Az0WFF3JUAktzdsEC2KRN2AqB1a2oMZBNgSQ==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.7.2",
|
||||
"@jimp/bmp": "^0.16.1",
|
||||
"@jimp/gif": "^0.16.1",
|
||||
"@jimp/jpeg": "^0.16.1",
|
||||
"@jimp/png": "^0.16.1",
|
||||
"@jimp/tiff": "^0.16.1",
|
||||
"timm": "^1.6.1"
|
||||
}
|
||||
},
|
||||
"@jimp/utils": {
|
||||
"version": "0.16.1",
|
||||
"resolved": "https://registry.npmjs.org/@jimp/utils/-/utils-0.16.1.tgz",
|
||||
"integrity": "sha512-8fULQjB0x4LzUSiSYG6ZtQl355sZjxbv8r9PPAuYHzS9sGiSHJQavNqK/nKnpDsVkU88/vRGcE7t3nMU0dEnVw==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.7.2",
|
||||
"regenerator-runtime": "^0.13.3"
|
||||
}
|
||||
},
|
||||
"@react-native-async-storage/async-storage": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-1.13.2.tgz",
|
||||
@@ -2166,15 +2050,10 @@
|
||||
"@types/istanbul-lib-report": "*"
|
||||
}
|
||||
},
|
||||
"@types/lodash": {
|
||||
"version": "4.14.166",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.166.tgz",
|
||||
"integrity": "sha512-A3YT/c1oTlyvvW/GQqG86EyqWNrT/tisOIh2mW3YCgcx71TNjiTZA3zYZWA5BCmtsOTXjhliy4c4yEkErw6njA=="
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "14.14.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.14.tgz",
|
||||
"integrity": "sha512-UHnOPWVWV1z+VV8k6L1HhG7UbGBgIdghqF3l9Ny9ApPghbjICXkUJSd/b9gOgQfjM1r+37cipdw/HJ3F6ICEnQ=="
|
||||
"version": "14.14.35",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.35.tgz",
|
||||
"integrity": "sha512-Lt+wj8NVPx0zUmUwumiVXapmaLUcAk3yPuHCFVXras9k5VT9TdhJqKqGVUQCD60OTMCl0qxJ57OiTL0Mic3Iag=="
|
||||
},
|
||||
"@types/stack-utils": {
|
||||
"version": "1.0.1",
|
||||
@@ -2303,11 +2182,6 @@
|
||||
"resolved": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz",
|
||||
"integrity": "sha1-qCJQ3bABXponyoLoLqYDu/pF768="
|
||||
},
|
||||
"any-base": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/any-base/-/any-base-1.1.0.tgz",
|
||||
"integrity": "sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg=="
|
||||
},
|
||||
"anymatch": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz",
|
||||
@@ -2583,11 +2457,6 @@
|
||||
"resolved": "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.18.0.tgz",
|
||||
"integrity": "sha512-vE52okJvzsVWhcgUHOv+69OG3Mdg151xyn41aVQN/5W5S+S43qZhxECtYLAEHMSFWX6Mv5IZrzj3T5+JqXfj5Q=="
|
||||
},
|
||||
"bmp-js": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bmp-js/-/bmp-js-0.1.0.tgz",
|
||||
"integrity": "sha1-4Fpj95amwf8l9Hcex62twUjAcjM="
|
||||
},
|
||||
"bplist-creator": {
|
||||
"version": "0.0.8",
|
||||
"resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.0.8.tgz",
|
||||
@@ -2660,15 +2529,6 @@
|
||||
"node-int64": "^0.4.0"
|
||||
}
|
||||
},
|
||||
"buffer": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||
"requires": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.1.13"
|
||||
}
|
||||
},
|
||||
"buffer-alloc": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz",
|
||||
@@ -2688,11 +2548,6 @@
|
||||
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
|
||||
"integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI="
|
||||
},
|
||||
"buffer-equal": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-0.0.1.tgz",
|
||||
"integrity": "sha1-kbx0sR6kBbyRa8aqkI+q+ltKrEs="
|
||||
},
|
||||
"buffer-fill": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz",
|
||||
@@ -3236,11 +3091,6 @@
|
||||
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
|
||||
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
|
||||
},
|
||||
"dom-walk": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz",
|
||||
"integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w=="
|
||||
},
|
||||
"ee-first": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
@@ -3408,11 +3258,6 @@
|
||||
"strip-eof": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"exif-parser": {
|
||||
"version": "0.1.12",
|
||||
"resolved": "https://registry.npmjs.org/exif-parser/-/exif-parser-0.1.12.tgz",
|
||||
"integrity": "sha1-WKnS1ywCwfbwKg70qRZicrd2CSI="
|
||||
},
|
||||
"expand-brackets": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz",
|
||||
@@ -3857,11 +3702,6 @@
|
||||
"escape-string-regexp": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"file-type": {
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-type/-/file-type-9.0.0.tgz",
|
||||
"integrity": "sha512-Qe/5NJrgIOlwijpq3B7BEpzPFcgzggOTagZmkXQY4LA6bsXKTUstK7Wp12lEJ/mLKTpvIZxmIuRcLYWT6ov9lw=="
|
||||
},
|
||||
"file-uri-to-path": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||
@@ -4042,15 +3882,6 @@
|
||||
"resolved": "https://registry.npmjs.org/getenv/-/getenv-0.7.0.tgz",
|
||||
"integrity": "sha1-ObkYOHB+IIb9HPbvh3fRyT4UZJ4="
|
||||
},
|
||||
"gifwrap": {
|
||||
"version": "0.9.2",
|
||||
"resolved": "https://registry.npmjs.org/gifwrap/-/gifwrap-0.9.2.tgz",
|
||||
"integrity": "sha512-fcIswrPaiCDAyO8xnWvHSZdWChjKXUanKKpAiWWJ/UTkEi/aYKn5+90e7DE820zbEaVR9CE2y4z9bzhQijZ0BA==",
|
||||
"requires": {
|
||||
"image-q": "^1.1.1",
|
||||
"omggif": "^1.0.10"
|
||||
}
|
||||
},
|
||||
"glob": {
|
||||
"version": "7.1.6",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
|
||||
@@ -4064,15 +3895,6 @@
|
||||
"path-is-absolute": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz",
|
||||
"integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==",
|
||||
"requires": {
|
||||
"min-document": "^2.19.0",
|
||||
"process": "^0.11.10"
|
||||
}
|
||||
},
|
||||
"globals": {
|
||||
"version": "11.12.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
|
||||
@@ -4190,16 +4012,6 @@
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
}
|
||||
},
|
||||
"ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="
|
||||
},
|
||||
"image-q": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/image-q/-/image-q-1.1.1.tgz",
|
||||
"integrity": "sha1-/IQJlmRGC5DKhi2TALa/u7+/gFY="
|
||||
},
|
||||
"image-size": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/image-size/-/image-size-0.6.3.tgz",
|
||||
@@ -4436,11 +4248,6 @@
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
|
||||
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8="
|
||||
},
|
||||
"is-function": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz",
|
||||
"integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ=="
|
||||
},
|
||||
"is-map": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz",
|
||||
@@ -4899,11 +4706,6 @@
|
||||
"resolved": "https://registry.npmjs.org/jetifier/-/jetifier-1.6.6.tgz",
|
||||
"integrity": "sha512-JNAkmPeB/GS2tCRqUzRPsTOHpGDah7xP18vGJfIjZC+W2sxEHbxgJxetIjIqhjQ3yYbYNEELkM/spKLtwoOSUQ=="
|
||||
},
|
||||
"jpeg-js": {
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.2.tgz",
|
||||
"integrity": "sha512-+az2gi/hvex7eLTMTlbRLOhH6P6WFdk2ITI8HJsaH2VqYO0I594zXSYEP+tf4FW+8Cy68ScDXoAsQdyQanv3sw=="
|
||||
},
|
||||
"js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
@@ -4985,21 +4787,6 @@
|
||||
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
|
||||
"integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="
|
||||
},
|
||||
"load-bmfont": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/load-bmfont/-/load-bmfont-1.4.1.tgz",
|
||||
"integrity": "sha512-8UyQoYmdRDy81Brz6aLAUhfZLwr5zV0L3taTQ4hju7m6biuwiWiJXjPhBJxbUQJA8PrkvJ/7Enqmwk2sM14soA==",
|
||||
"requires": {
|
||||
"buffer-equal": "0.0.1",
|
||||
"mime": "^1.3.4",
|
||||
"parse-bmfont-ascii": "^1.0.3",
|
||||
"parse-bmfont-binary": "^1.0.5",
|
||||
"parse-bmfont-xml": "^1.1.4",
|
||||
"phin": "^2.9.1",
|
||||
"xhr": "^2.0.1",
|
||||
"xtend": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"locate-path": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz",
|
||||
@@ -5943,14 +5730,6 @@
|
||||
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz",
|
||||
"integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ=="
|
||||
},
|
||||
"min-document": {
|
||||
"version": "2.19.0",
|
||||
"resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz",
|
||||
"integrity": "sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU=",
|
||||
"requires": {
|
||||
"dom-walk": "^0.1.0"
|
||||
}
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
||||
@@ -6079,27 +5858,6 @@
|
||||
"resolved": "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.12.0.tgz",
|
||||
"integrity": "sha512-HZ3XehqShTFj9gHauRJ3Bri9eiCTOII7/crtXzURtT14NdnOFs9Ia5E82W7z3izVBNx760tqwddxrBJVG52Y1Q=="
|
||||
},
|
||||
"node-vibrant": {
|
||||
"version": "3.1.6",
|
||||
"resolved": "https://registry.npmjs.org/node-vibrant/-/node-vibrant-3.1.6.tgz",
|
||||
"integrity": "sha512-Wlc/hQmBMOu6xon12ZJHS2N3M+I6J8DhrD3Yo6m5175v8sFkVIN+UjhKVRcO+fqvre89ASTpmiFEP3nPO13SwA==",
|
||||
"requires": {
|
||||
"@jimp/custom": "^0.16.1",
|
||||
"@jimp/plugin-resize": "^0.16.1",
|
||||
"@jimp/types": "^0.16.1",
|
||||
"@types/lodash": "^4.14.53",
|
||||
"@types/node": "^10.11.7",
|
||||
"lodash": "^4.17.20",
|
||||
"url": "^0.11.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node": {
|
||||
"version": "10.17.50",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.50.tgz",
|
||||
"integrity": "sha512-vwX+/ija9xKc/z9VqMCdbf4WYcMTGsI0I/L/6shIF3qXURxZOhPQlPRHtjTpiNhAwn0paMJzlOQqw6mAGEQnTA=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"noop-fn": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/noop-fn/-/noop-fn-1.0.0.tgz",
|
||||
@@ -6242,11 +6000,6 @@
|
||||
"isobject": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"omggif": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.10.tgz",
|
||||
"integrity": "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw=="
|
||||
},
|
||||
"on-finished": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
|
||||
@@ -6333,35 +6086,6 @@
|
||||
"resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz",
|
||||
"integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M="
|
||||
},
|
||||
"pako": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
||||
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
|
||||
},
|
||||
"parse-bmfont-ascii": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/parse-bmfont-ascii/-/parse-bmfont-ascii-1.0.6.tgz",
|
||||
"integrity": "sha1-Eaw8P/WPfCAgqyJ2kHkQjU36AoU="
|
||||
},
|
||||
"parse-bmfont-binary": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/parse-bmfont-binary/-/parse-bmfont-binary-1.0.6.tgz",
|
||||
"integrity": "sha1-0Di0dtPp3Z2x4RoLDlOiJ5K2kAY="
|
||||
},
|
||||
"parse-bmfont-xml": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/parse-bmfont-xml/-/parse-bmfont-xml-1.1.4.tgz",
|
||||
"integrity": "sha512-bjnliEOmGv3y1aMEfREMBJ9tfL3WR0i0CKPj61DnSLaoxWR3nLrsQrEbCId/8rF4NyRF0cCqisSVXyQYWM+mCQ==",
|
||||
"requires": {
|
||||
"xml-parse-from-string": "^1.0.0",
|
||||
"xml2js": "^0.4.5"
|
||||
}
|
||||
},
|
||||
"parse-headers": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.3.tgz",
|
||||
"integrity": "sha512-QhhZ+DCCit2Coi2vmAKbq5RGTRcQUOE2+REgv8vdyu7MnYx2eZztegqtTx99TZ86GTIwqiy3+4nQTWZ2tgmdCA=="
|
||||
},
|
||||
"parse-json": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz",
|
||||
@@ -6411,11 +6135,6 @@
|
||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
|
||||
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw=="
|
||||
},
|
||||
"phin": {
|
||||
"version": "2.9.3",
|
||||
"resolved": "https://registry.npmjs.org/phin/-/phin-2.9.3.tgz",
|
||||
"integrity": "sha512-CzFr90qM24ju5f88quFC/6qohjC144rehe5n6DH900lgXmUe86+xCKc10ev56gRKC4/BkHUoG4uSiQgBiIXwDA=="
|
||||
},
|
||||
"pify": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
|
||||
@@ -6429,21 +6148,6 @@
|
||||
"node-modules-regexp": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"pixelmatch": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-4.0.2.tgz",
|
||||
"integrity": "sha1-j0fc7FARtHe2fbA8JDvB8wheiFQ=",
|
||||
"requires": {
|
||||
"pngjs": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"pngjs": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz",
|
||||
"integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"pkg-dir": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz",
|
||||
@@ -6605,11 +6309,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"process": {
|
||||
"version": "0.11.10",
|
||||
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||
"integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI="
|
||||
},
|
||||
"process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
@@ -6654,11 +6353,6 @@
|
||||
"once": "^1.3.1"
|
||||
}
|
||||
},
|
||||
"punycode": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
|
||||
"integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0="
|
||||
},
|
||||
"qs": {
|
||||
"version": "6.9.4",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz",
|
||||
@@ -6674,11 +6368,6 @@
|
||||
"strict-uri-encode": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"querystring": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
|
||||
"integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA="
|
||||
},
|
||||
"querystringify": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
|
||||
@@ -6699,14 +6388,6 @@
|
||||
"prop-types": "^15.6.2"
|
||||
}
|
||||
},
|
||||
"react-color-extractor": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/react-color-extractor/-/react-color-extractor-1.1.2.tgz",
|
||||
"integrity": "sha512-AIWzlSuS8IudaXOFscJzNaY7NeGldoGy8k4spyXriXlXkGwUPk7IWsJDr0ibSYzOFVuTfKyhfiSnc1E5fDAfxw==",
|
||||
"requires": {
|
||||
"node-vibrant": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"react-devtools-core": {
|
||||
"version": "4.10.1",
|
||||
"resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-4.10.1.tgz",
|
||||
@@ -7270,6 +6951,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"redux-devtools-extension": {
|
||||
"version": "2.13.9",
|
||||
"resolved": "https://registry.npmjs.org/redux-devtools-extension/-/redux-devtools-extension-2.13.9.tgz",
|
||||
"integrity": "sha512-cNJ8Q/EtjhQaZ71c8I9+BPySIBVEKssbPpskBfsXqb8HJ002A3KRVHfeRzwRo6mGPqsm7XuHTqNSNeS1Khig0A=="
|
||||
},
|
||||
"redux-persist": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/redux-persist/-/redux-persist-6.0.0.tgz",
|
||||
@@ -7288,6 +6974,11 @@
|
||||
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz",
|
||||
"integrity": "sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw=="
|
||||
},
|
||||
"reflect-metadata": {
|
||||
"version": "0.1.13",
|
||||
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz",
|
||||
"integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg=="
|
||||
},
|
||||
"regenerate": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
|
||||
@@ -8102,21 +7793,11 @@
|
||||
"resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-1.1.0.tgz",
|
||||
"integrity": "sha1-dkpaEa9QVhkhsTPztE5hhofg9cM="
|
||||
},
|
||||
"timm": {
|
||||
"version": "1.7.1",
|
||||
"resolved": "https://registry.npmjs.org/timm/-/timm-1.7.1.tgz",
|
||||
"integrity": "sha512-IjZc9KIotudix8bMaBW6QvMuq64BrJWFs1+4V0lXwWGQZwH+LnX87doAYhem4caOEusRP9/g6jVDQmZ8XOk1nw=="
|
||||
},
|
||||
"tiny-queue": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/tiny-queue/-/tiny-queue-0.2.1.tgz",
|
||||
"integrity": "sha1-JaZ/LG4lOyypQZd7XvdELvl6YEY="
|
||||
},
|
||||
"tinycolor2": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.2.tgz",
|
||||
"integrity": "sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA=="
|
||||
},
|
||||
"tmp": {
|
||||
"version": "0.0.33",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
|
||||
@@ -8381,15 +8062,6 @@
|
||||
"resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz",
|
||||
"integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI="
|
||||
},
|
||||
"url": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz",
|
||||
"integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=",
|
||||
"requires": {
|
||||
"punycode": "1.3.2",
|
||||
"querystring": "0.2.0"
|
||||
}
|
||||
},
|
||||
"url-parse": {
|
||||
"version": "1.4.7",
|
||||
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.7.tgz",
|
||||
@@ -8412,14 +8084,6 @@
|
||||
"object-assign": "^4.1.1"
|
||||
}
|
||||
},
|
||||
"utif": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/utif/-/utif-2.0.1.tgz",
|
||||
"integrity": "sha512-Z/S1fNKCicQTf375lIP9G8Sa1H/phcysstNrrSdZKj1f9g58J4NMgb5IgiEZN9/nLMPDwF0W7hdOe9Qq2IYoLg==",
|
||||
"requires": {
|
||||
"pako": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
@@ -8616,17 +8280,6 @@
|
||||
"uuid": "^3.3.2"
|
||||
}
|
||||
},
|
||||
"xhr": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/xhr/-/xhr-2.6.0.tgz",
|
||||
"integrity": "sha512-/eCGLb5rxjx5e3mF1A7s+pLlR6CGyqWN91fv1JgER5mVWg1MZmlhBvy9kjcsOdRk8RrIujotWyJamfyrp+WIcA==",
|
||||
"requires": {
|
||||
"global": "~4.4.0",
|
||||
"is-function": "^1.0.1",
|
||||
"parse-headers": "^2.0.0",
|
||||
"xtend": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"xml-js": {
|
||||
"version": "1.6.11",
|
||||
"resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz",
|
||||
@@ -8635,27 +8288,6 @@
|
||||
"sax": "^1.2.4"
|
||||
}
|
||||
},
|
||||
"xml-parse-from-string": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/xml-parse-from-string/-/xml-parse-from-string-1.0.1.tgz",
|
||||
"integrity": "sha1-qQKekp09vN7RafPG4oI42VpdWig="
|
||||
},
|
||||
"xml2js": {
|
||||
"version": "0.4.23",
|
||||
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
|
||||
"integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==",
|
||||
"requires": {
|
||||
"sax": ">=0.6.0",
|
||||
"xmlbuilder": "~11.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"xmlbuilder": {
|
||||
"version": "11.0.1",
|
||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
|
||||
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"xmlbuilder": {
|
||||
"version": "9.0.7",
|
||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz",
|
||||
|
||||
+51
-49
@@ -1,51 +1,53 @@
|
||||
{
|
||||
"main": "node_modules/expo/AppEntry.js",
|
||||
"scripts": {
|
||||
"start": "expo start",
|
||||
"android": "expo start --android",
|
||||
"ios": "expo start --ios",
|
||||
"web": "expo start --web",
|
||||
"eject": "expo eject"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-native-async-storage/async-storage": "^1.13.2",
|
||||
"@react-native-community/masked-view": "0.1.10",
|
||||
"@react-native-community/netinfo": "^5.9.7",
|
||||
"@react-native-community/slider": "^3.0.3",
|
||||
"@react-navigation/material-bottom-tabs": "^5.3.10",
|
||||
"@react-navigation/native": "^5.8.10",
|
||||
"@react-navigation/stack": "^5.12.8",
|
||||
"expo": "~40.0.0",
|
||||
"expo-app-loading": "^1.0.1",
|
||||
"expo-font": "^8.4.0",
|
||||
"expo-linear-gradient": "^8.4.0",
|
||||
"expo-sharing": "~8.5.0",
|
||||
"expo-sqlite": "~8.5.0",
|
||||
"expo-status-bar": "~1.0.3",
|
||||
"expo-web-browser": "~8.6.0",
|
||||
"moment": "^2.29.1",
|
||||
"react": "16.13.1",
|
||||
"react-color-extractor": "^1.1.2",
|
||||
"react-dom": "16.13.1",
|
||||
"react-native": "https://github.com/expo/react-native/archive/sdk-40.0.1.tar.gz",
|
||||
"react-native-collapsible-header-views": "^1.1.2",
|
||||
"react-native-gesture-handler": "^1.8.0",
|
||||
"react-native-offline": "^5.7.0",
|
||||
"react-native-paper": "^4.5.0",
|
||||
"react-native-reanimated": "^1.13.2",
|
||||
"react-native-safe-area-context": "3.1.9",
|
||||
"react-native-screens": "~2.15.0",
|
||||
"react-native-vector-icons": "^7.1.0",
|
||||
"react-native-web": "~0.13.12",
|
||||
"react-native-webview": "11.0.0",
|
||||
"react-redux": "^7.2.2",
|
||||
"redux": "^4.0.5",
|
||||
"redux-persist": "^6.0.0",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"rn-sliding-up-panel": "^2.4.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "~7.9.0"
|
||||
},
|
||||
"private": true
|
||||
"main": "node_modules/expo/AppEntry.js",
|
||||
"scripts": {
|
||||
"start": "expo start",
|
||||
"android": "expo start --android",
|
||||
"ios": "expo start --ios",
|
||||
"web": "expo start --web",
|
||||
"eject": "expo eject"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-native-async-storage/async-storage": "^1.13.2",
|
||||
"@react-native-community/masked-view": "0.1.10",
|
||||
"@react-native-community/netinfo": "^5.9.7",
|
||||
"@react-native-community/slider": "^3.0.3",
|
||||
"@react-navigation/material-bottom-tabs": "^5.3.10",
|
||||
"@react-navigation/native": "^5.8.10",
|
||||
"@react-navigation/stack": "^5.12.8",
|
||||
"expo": "~40.0.0",
|
||||
"expo-app-loading": "^1.0.1",
|
||||
"expo-font": "^8.4.0",
|
||||
"expo-linear-gradient": "^8.4.0",
|
||||
"expo-sharing": "~8.5.0",
|
||||
"expo-sqlite": "~8.5.0",
|
||||
"expo-status-bar": "~1.0.3",
|
||||
"expo-web-browser": "~8.6.0",
|
||||
"moment": "^2.29.1",
|
||||
"react": "16.13.1",
|
||||
"react-dom": "16.13.1",
|
||||
"react-native": "https://github.com/expo/react-native/archive/sdk-40.0.1.tar.gz",
|
||||
"react-native-collapsible-header-views": "^1.1.2",
|
||||
"react-native-gesture-handler": "^1.8.0",
|
||||
"react-native-offline": "^5.7.0",
|
||||
"react-native-paper": "^4.5.0",
|
||||
"react-native-reanimated": "^1.13.2",
|
||||
"react-native-safe-area-context": "3.1.9",
|
||||
"react-native-screens": "~2.15.0",
|
||||
"react-native-vector-icons": "^7.1.0",
|
||||
"react-native-web": "~0.13.12",
|
||||
"react-native-webview": "11.0.0",
|
||||
"react-redux": "^7.2.2",
|
||||
"redux": "^4.0.5",
|
||||
"redux-devtools-extension": "^2.13.9",
|
||||
"redux-persist": "^6.0.0",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rn-sliding-up-panel": "^2.4.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "~7.9.0",
|
||||
"@types/node": "^14.14.35"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
import React, { useRef } from "react";
|
||||
import { View, TextInput, StyleSheet } from "react-native";
|
||||
import {
|
||||
TouchableRipple,
|
||||
IconButton,
|
||||
Appbar as MaterialAppbar,
|
||||
} from "react-native-paper";
|
||||
import Constants from "expo-constants";
|
||||
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
export const Appbar = ({ title, onBackAction, style }) => {
|
||||
const theme = useSelector((state) => state.themeReducer.theme);
|
||||
|
||||
return (
|
||||
<MaterialAppbar.Header
|
||||
style={[{ backgroundColor: theme.colorPrimary }, style]}
|
||||
>
|
||||
{onBackAction && (
|
||||
<MaterialAppbar.BackAction onPress={onBackAction} />
|
||||
)}
|
||||
<MaterialAppbar.Content
|
||||
title={title}
|
||||
titleStyle={{ color: theme.textColorPrimary }}
|
||||
/>
|
||||
</MaterialAppbar.Header>
|
||||
);
|
||||
};
|
||||
|
||||
export const SearchAppbar = ({
|
||||
placeholder,
|
||||
getSearchResults,
|
||||
setSearchText,
|
||||
searchText,
|
||||
getNovels,
|
||||
setLoading,
|
||||
screen,
|
||||
openWebView,
|
||||
}) => {
|
||||
const searchRef = useRef(null);
|
||||
|
||||
const navigation = useNavigation();
|
||||
|
||||
const theme = useSelector((state) => state.themeReducer.theme);
|
||||
|
||||
return (
|
||||
<TouchableRipple
|
||||
borderless
|
||||
onPress={() => searchRef.current.focus()}
|
||||
style={[
|
||||
styles.searchAppbarContainer,
|
||||
{ backgroundColor: theme.searchBarColor },
|
||||
]}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<View style={{ flex: 1, flexDirection: "row" }}>
|
||||
{screen === "Library" ? (
|
||||
<IconButton
|
||||
icon="magnify"
|
||||
color={theme.textColorSecondary}
|
||||
style={{ marginLeft: 0 }}
|
||||
size={23}
|
||||
/>
|
||||
) : (
|
||||
<IconButton
|
||||
icon="arrow-left"
|
||||
color={theme.textColorSecondary}
|
||||
style={{ marginLeft: 0 }}
|
||||
size={23}
|
||||
onPress={() => navigation.goBack()}
|
||||
/>
|
||||
)}
|
||||
<TextInput
|
||||
ref={searchRef}
|
||||
style={{
|
||||
fontSize: 16,
|
||||
color: theme.textColorSecondary,
|
||||
}}
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor={theme.textColorSecondary}
|
||||
onChangeText={(text) => {
|
||||
setSearchText(text);
|
||||
if (screen === "Library") {
|
||||
getSearchResults(text);
|
||||
}
|
||||
}}
|
||||
onSubmitEditing={() => {
|
||||
if (screen === "Extension") {
|
||||
getSearchResults(searchText);
|
||||
}
|
||||
}}
|
||||
defaultValue={searchText}
|
||||
/>
|
||||
</View>
|
||||
{searchText !== "" && (
|
||||
<IconButton
|
||||
icon="close"
|
||||
color={theme.textColorSecondary}
|
||||
style={{ marginRight: 0 }}
|
||||
size={23}
|
||||
onPress={() => {
|
||||
setLoading?.(true);
|
||||
getNovels();
|
||||
setSearchText("");
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{openWebView && (
|
||||
<IconButton
|
||||
icon="earth"
|
||||
color={theme.textColorSecondary}
|
||||
style={{ marginRight: 0 }}
|
||||
size={23}
|
||||
onPress={openWebView}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</TouchableRipple>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
searchAppbarContainer: {
|
||||
marginTop: Constants.statusBarHeight + 8,
|
||||
height: 48,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 16,
|
||||
marginBottom: 12,
|
||||
borderRadius: 8,
|
||||
marginHorizontal: 12,
|
||||
elevation: 2,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
const BottomSheetHandle = () => {
|
||||
const theme = useSelector((state) => state.themeReducer.theme);
|
||||
return (
|
||||
<>
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: theme.textColorHintDark,
|
||||
height: 5,
|
||||
width: 30,
|
||||
borderRadius: 50,
|
||||
top: 10,
|
||||
alignSelf: "center",
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BottomSheetHandle;
|
||||
@@ -0,0 +1,48 @@
|
||||
import React from "react";
|
||||
import { StyleSheet, Text, View } from "react-native";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
const EmptyView = ({ icon, description }) => {
|
||||
const theme = useSelector((state) => state.themeReducer.theme);
|
||||
|
||||
return (
|
||||
<View style={styles.emptyViewContainer}>
|
||||
<Text
|
||||
style={[
|
||||
styles.emptyViewIcon,
|
||||
{ color: theme.textColorSecondary },
|
||||
]}
|
||||
>
|
||||
{icon}
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
styles.emptyViewText,
|
||||
{ color: theme.textColorSecondary },
|
||||
]}
|
||||
>
|
||||
{description}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmptyView;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
emptyViewContainer: {
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
emptyViewIcon: {
|
||||
fontSize: 45,
|
||||
fontWeight: "bold",
|
||||
},
|
||||
emptyViewText: {
|
||||
fontWeight: "bold",
|
||||
marginTop: 10,
|
||||
textAlign: "center",
|
||||
paddingHorizontal: 30,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
import React from "react";
|
||||
import { StyleSheet } from "react-native";
|
||||
import { List, Divider as PaperDivider } from "react-native-paper";
|
||||
|
||||
const ListSection = ({ children }) => (
|
||||
<List.Section style={styles.listSection}>{children}</List.Section>
|
||||
);
|
||||
|
||||
const ListSubHeader = ({ children, theme }) => (
|
||||
<List.Subheader
|
||||
style={[styles.listSubHeader, { color: theme.colorAccentDark }]}
|
||||
>
|
||||
{children}
|
||||
</List.Subheader>
|
||||
);
|
||||
|
||||
const ListItem = ({ title, description, icon, onPress, theme }) => (
|
||||
<List.Item
|
||||
title={title}
|
||||
titleStyle={{ color: theme.textColorPrimary }}
|
||||
description={description}
|
||||
descriptionStyle={{ color: theme.textColorSecondary }}
|
||||
descriptionNumberOfLines={1}
|
||||
left={() =>
|
||||
icon && (
|
||||
<List.Icon
|
||||
color={theme.colorAccentDark}
|
||||
icon={icon}
|
||||
style={{ marginVertical: 0 }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
onPress={onPress}
|
||||
rippleColor={theme.rippleColor}
|
||||
/>
|
||||
);
|
||||
|
||||
const Divider = ({ theme }) => (
|
||||
<PaperDivider
|
||||
style={{
|
||||
backgroundColor: theme.dividerColor,
|
||||
height: 1,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
export { ListSection, ListSubHeader, ListItem, Divider };
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
listSection: {
|
||||
flex: 1,
|
||||
marginVertical: 0,
|
||||
},
|
||||
listSubHeader: {
|
||||
paddingBottom: 5,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,212 @@
|
||||
import React from "react";
|
||||
import { StyleSheet, View, Text, ImageBackground, Image } from "react-native";
|
||||
import { LinearGradient } from "expo-linear-gradient";
|
||||
import { TouchableRipple } from "react-native-paper";
|
||||
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
const NovelCover = ({ item, onPress, libraryStatus }) => {
|
||||
const displayMode = useSelector(
|
||||
(state) => state.settingsReducer.displayMode
|
||||
);
|
||||
|
||||
const theme = useSelector((state) => state.themeReducer.theme);
|
||||
|
||||
const itemsPerRow = useSelector(
|
||||
(state) => state.settingsReducer.itemsPerRow
|
||||
);
|
||||
|
||||
const height = {
|
||||
1: 550,
|
||||
2: 280,
|
||||
3: 180,
|
||||
4: 140,
|
||||
5: 100,
|
||||
6: 80,
|
||||
7: 70,
|
||||
8: 58,
|
||||
9: 55,
|
||||
10: 50,
|
||||
};
|
||||
|
||||
return displayMode !== 2 ? (
|
||||
<View style={{ flex: 1 / itemsPerRow }}>
|
||||
<TouchableRipple
|
||||
borderless
|
||||
centered
|
||||
rippleColor={theme.rippleColor}
|
||||
style={styles.opac}
|
||||
onPress={onPress}
|
||||
>
|
||||
<>
|
||||
{item.novelCover && (
|
||||
<ImageBackground
|
||||
source={{ uri: item.novelCover }}
|
||||
style={{ height: height[itemsPerRow] }}
|
||||
imageStyle={[
|
||||
{ borderRadius: 4 },
|
||||
libraryStatus && { opacity: 0.5 },
|
||||
]}
|
||||
progressiveRenderingEnabled={true}
|
||||
>
|
||||
<View style={{ flexDirection: "row", margin: 4 }}>
|
||||
{item.chaptersDownloaded && (
|
||||
<Text
|
||||
style={[
|
||||
{
|
||||
flex: 0,
|
||||
backgroundColor: "#47a84a",
|
||||
color: "#FFFFFF",
|
||||
borderTopLeftRadius: 4,
|
||||
borderBottomLeftRadius: 4,
|
||||
paddingTop: 2,
|
||||
paddingHorizontal: 4,
|
||||
alignSelf: "flex-start",
|
||||
fontSize: 12,
|
||||
},
|
||||
!item.chaptersUnread && {
|
||||
borderTopRightRadius: 4,
|
||||
borderBottomRightRadius: 4,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{item.chaptersDownloaded}
|
||||
</Text>
|
||||
)}
|
||||
{item.chaptersUnread && (
|
||||
<Text
|
||||
style={[
|
||||
{
|
||||
flex: 0,
|
||||
backgroundColor: "#2979FF",
|
||||
color: "#FFFFFF",
|
||||
borderTopRightRadius: 4,
|
||||
borderBottomRightRadius: 4,
|
||||
paddingTop: 2,
|
||||
paddingHorizontal: 4,
|
||||
alignSelf: "flex-start",
|
||||
fontSize: 12,
|
||||
},
|
||||
!item.chaptersDownloaded && {
|
||||
borderTopLeftRadius: 4,
|
||||
borderBottomLeftRadius: 4,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{item.chaptersUnread}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
{displayMode === 0 && (
|
||||
<View style={styles.titleContainer}>
|
||||
<LinearGradient
|
||||
colors={[
|
||||
"transparent",
|
||||
"rgba(0,0,0,0.6)",
|
||||
]}
|
||||
style={styles.linearGradient}
|
||||
>
|
||||
<Text
|
||||
numberOfLines={2}
|
||||
style={[
|
||||
styles.title,
|
||||
{
|
||||
color:
|
||||
"rgba(255,255,255,1)",
|
||||
},
|
||||
]}
|
||||
>
|
||||
{item.novelName}
|
||||
</Text>
|
||||
</LinearGradient>
|
||||
</View>
|
||||
)}
|
||||
</ImageBackground>
|
||||
)}
|
||||
{displayMode === 1 && (
|
||||
<Text
|
||||
numberOfLines={2}
|
||||
style={[
|
||||
styles.title,
|
||||
{
|
||||
color: theme.textColorPrimary,
|
||||
padding: 4,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{item.novelName}
|
||||
</Text>
|
||||
)}
|
||||
</>
|
||||
</TouchableRipple>
|
||||
</View>
|
||||
) : (
|
||||
<TouchableRipple
|
||||
borderless
|
||||
centered
|
||||
rippleColor={theme.rippleColor}
|
||||
style={styles.listView}
|
||||
onPress={onPress}
|
||||
>
|
||||
<>
|
||||
<Image
|
||||
source={{ uri: item.novelCover }}
|
||||
style={styles.extensionIcon}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
flex: 1,
|
||||
color: theme.textColorPrimary,
|
||||
marginLeft: 16,
|
||||
fontSize: 15,
|
||||
paddingRight: 8,
|
||||
}}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{item.novelName}
|
||||
</Text>
|
||||
</>
|
||||
</TouchableRipple>
|
||||
);
|
||||
};
|
||||
|
||||
export default NovelCover;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
// logo: {
|
||||
// height: 180,
|
||||
// },
|
||||
titleContainer: {
|
||||
flex: 1,
|
||||
justifyContent: "flex-end",
|
||||
borderRadius: 4,
|
||||
},
|
||||
title: {
|
||||
fontFamily: "pt-sans-bold",
|
||||
fontSize: 14,
|
||||
padding: 8,
|
||||
},
|
||||
|
||||
linearGradient: {
|
||||
bottom: -1,
|
||||
borderRadius: 4,
|
||||
},
|
||||
opac: {
|
||||
paddingHorizontal: 4.5,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 4,
|
||||
},
|
||||
extensionIcon: {
|
||||
width: 42,
|
||||
height: 42,
|
||||
borderRadius: 4,
|
||||
},
|
||||
listView: {
|
||||
flex: 1,
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 12,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
borderRadius: 4,
|
||||
},
|
||||
});
|
||||
@@ -1,129 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
import { Animated, StyleSheet, View, Text, Dimensions } from "react-native";
|
||||
import { ToggleButton } from "react-native-paper";
|
||||
import Slider from "@react-native-community/slider";
|
||||
import Bottomsheet from "rn-sliding-up-panel";
|
||||
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
import { saveReaderTheme } from "../../services/asyncStorage";
|
||||
|
||||
const ChapterBottomSheet = ({
|
||||
bottomSheetRef,
|
||||
setSize,
|
||||
size,
|
||||
readerTheme,
|
||||
setReaderTheme,
|
||||
}) => {
|
||||
const theme = useSelector((state) => state.themeReducer.theme);
|
||||
|
||||
return (
|
||||
<Bottomsheet
|
||||
animatedValue={new Animated.Value(0)}
|
||||
ref={bottomSheetRef}
|
||||
draggableRange={{ top: 200, bottom: 0 }}
|
||||
snappingPoints={[0, 200]}
|
||||
showBackdrop={false}
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
styles.contentContainer,
|
||||
{
|
||||
backgroundColor: "rgba(0,0,0,0.4)",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: theme.textColorHintDark,
|
||||
height: 5,
|
||||
width: 30,
|
||||
borderRadius: 50,
|
||||
top: 10,
|
||||
alignSelf: "center",
|
||||
}}
|
||||
/>
|
||||
|
||||
<View style={{ flex: 1, alignItems: "center", paddingTop: 30 }}>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorPrimaryDark,
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
Text Size
|
||||
</Text>
|
||||
<Slider
|
||||
style={{
|
||||
width: Dimensions.get("window").width,
|
||||
height: 40,
|
||||
}}
|
||||
value={size}
|
||||
minimumValue={12}
|
||||
maximumValue={20}
|
||||
step={4}
|
||||
minimumTrackTintColor={theme.colorAccentDark}
|
||||
maximumTrackTintColor="#000000"
|
||||
thumbTintColor={theme.colorAccentDark}
|
||||
onValueChange={(value) => setSize(value)}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorPrimaryDark,
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
Reader Theme
|
||||
</Text>
|
||||
<ToggleButton.Row
|
||||
onValueChange={(value) => {
|
||||
setReaderTheme(value);
|
||||
saveReaderTheme(value);
|
||||
}}
|
||||
value={readerTheme}
|
||||
style={{ marginTop: 10 }}
|
||||
>
|
||||
<ToggleButton
|
||||
icon="format-text"
|
||||
color="white"
|
||||
value={1}
|
||||
style={{
|
||||
backgroundColor: "black",
|
||||
marginHorizontal: 10,
|
||||
}}
|
||||
/>
|
||||
<ToggleButton
|
||||
icon="format-text"
|
||||
color="black"
|
||||
value={2}
|
||||
style={{
|
||||
backgroundColor: "white",
|
||||
marginHorizontal: 10,
|
||||
}}
|
||||
/>
|
||||
<ToggleButton
|
||||
icon="format-text"
|
||||
color="black"
|
||||
value={3}
|
||||
style={{
|
||||
backgroundColor: "#F4ECD8",
|
||||
marginHorizontal: 10,
|
||||
}}
|
||||
/>
|
||||
</ToggleButton.Row>
|
||||
</View>
|
||||
</View>
|
||||
</Bottomsheet>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChapterBottomSheet;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
contentContainer: {
|
||||
flex: 1,
|
||||
// paddingTop: ,
|
||||
// alignItems: "center",
|
||||
},
|
||||
});
|
||||
@@ -1,19 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
import { Appbar } from "react-native-paper";
|
||||
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
export const CustomAppbar = ({ title, onBackAction }) => {
|
||||
const theme = useSelector((state) => state.themeReducer.theme);
|
||||
|
||||
return (
|
||||
<Appbar.Header style={{ backgroundColor: theme.colorDarkPrimary }}>
|
||||
{onBackAction && <Appbar.BackAction onPress={onBackAction} />}
|
||||
<Appbar.Content
|
||||
title={title}
|
||||
titleStyle={{ color: theme.textColorPrimaryDark }}
|
||||
/>
|
||||
</Appbar.Header>
|
||||
);
|
||||
};
|
||||
@@ -1,99 +0,0 @@
|
||||
import React, { useState } from "react";
|
||||
import { StyleSheet, View, Text, ImageBackground } from "react-native";
|
||||
import { LinearGradient } from "expo-linear-gradient";
|
||||
import { TouchableRipple } from "react-native-paper";
|
||||
import { theme } from "../../theme/theme";
|
||||
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
|
||||
const NovelCover = ({ item, onPress, libraryStatus }) => {
|
||||
const [mode, setMode] = useState();
|
||||
AsyncStorage.getItem("@display_mode").then((value) => setMode(value));
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1 / 3 }}>
|
||||
<TouchableRipple
|
||||
borderless
|
||||
centered
|
||||
rippleColor={theme.rippleColorDark}
|
||||
style={styles.opac}
|
||||
onPress={onPress}
|
||||
>
|
||||
<>
|
||||
{item.novelCover ? (
|
||||
<ImageBackground
|
||||
source={{
|
||||
uri: item.novelCover,
|
||||
}}
|
||||
style={styles.logo}
|
||||
imageStyle={[
|
||||
{ borderRadius: 4 },
|
||||
libraryStatus && { opacity: 0.4 },
|
||||
]}
|
||||
progressiveRenderingEnabled={true}
|
||||
>
|
||||
{(!mode || mode === "compact") && (
|
||||
<View style={styles.titleContainer}>
|
||||
<LinearGradient
|
||||
colors={[
|
||||
"transparent",
|
||||
"rgba(0,0,0,0.6)",
|
||||
]}
|
||||
style={styles.linearGradient}
|
||||
>
|
||||
<Text
|
||||
numberOfLines={2}
|
||||
style={styles.title}
|
||||
>
|
||||
{item.novelName}
|
||||
</Text>
|
||||
</LinearGradient>
|
||||
</View>
|
||||
)}
|
||||
</ImageBackground>
|
||||
) : (
|
||||
<View>
|
||||
<Text style={{ color: "white" }}>HAHAHAHAH</Text>
|
||||
</View>
|
||||
)}
|
||||
{mode === "comfortable" && (
|
||||
<Text numberOfLines={2} style={styles.title}>
|
||||
{item.novelName}
|
||||
</Text>
|
||||
)}
|
||||
</>
|
||||
</TouchableRipple>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default NovelCover;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
logo: {
|
||||
height: 173,
|
||||
borderRadius: 4,
|
||||
},
|
||||
titleContainer: {
|
||||
flex: 1,
|
||||
justifyContent: "flex-end",
|
||||
borderRadius: 4,
|
||||
},
|
||||
title: {
|
||||
fontFamily: "pt-sans-bold",
|
||||
fontSize: 14,
|
||||
color: "white",
|
||||
padding: 8,
|
||||
textShadowColor: "rgba(0, 0, 0, 0.75)",
|
||||
textShadowOffset: { width: -1, height: 1 },
|
||||
textShadowRadius: 10,
|
||||
},
|
||||
linearGradient: {
|
||||
borderRadius: 4,
|
||||
},
|
||||
opac: {
|
||||
paddingHorizontal: 3.5,
|
||||
paddingVertical: 3.5,
|
||||
borderRadius: 4,
|
||||
},
|
||||
});
|
||||
@@ -1,27 +0,0 @@
|
||||
import React from "react";
|
||||
import { TextInput, StyleSheet } from "react-native";
|
||||
import { theme } from "../../theme/theme";
|
||||
|
||||
const SearchBar = ({ searchText, onChangeText, onSubmitEditing }) => (
|
||||
<TextInput
|
||||
autoFocus
|
||||
placeholder="Search..."
|
||||
defaultValue={searchText}
|
||||
style={styles.searchBar}
|
||||
placeholderTextColor={theme.textColorHintDark}
|
||||
blurOnSubmit={true}
|
||||
onChangeText={onChangeText}
|
||||
onSubmitEditing={onSubmitEditing}
|
||||
/>
|
||||
);
|
||||
|
||||
export default SearchBar;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
searchBar: {
|
||||
flex: 1,
|
||||
fontSize: 18,
|
||||
color: "white",
|
||||
marginLeft: 15,
|
||||
},
|
||||
});
|
||||
@@ -1,115 +0,0 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { View, Text, ActivityIndicator } from "react-native";
|
||||
import { TouchableRipple, IconButton } from "react-native-paper";
|
||||
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
const ChapterCard = ({
|
||||
navigation,
|
||||
novelUrl,
|
||||
chapter,
|
||||
downloadChapter,
|
||||
extensionId,
|
||||
downloading,
|
||||
}) => {
|
||||
const [downloaded, setDownloaded] = useState(chapter.downloaded);
|
||||
|
||||
const theme = useSelector((state) => state.themeReducer.theme);
|
||||
|
||||
return (
|
||||
<TouchableRipple
|
||||
style={{
|
||||
paddingHorizontal: 15,
|
||||
paddingVertical: 7,
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
// borderTopColor: "rgba(255,255,255,.12)",
|
||||
// borderTopWidth: 1,
|
||||
}}
|
||||
onPress={() =>
|
||||
navigation.navigate("ChapterItem", {
|
||||
chapterUrl: chapter.chapterUrl,
|
||||
extensionId,
|
||||
novelUrl: novelUrl,
|
||||
chapterName: chapter.chapterName,
|
||||
})
|
||||
}
|
||||
rippleColor={theme.rippleColorDark}
|
||||
>
|
||||
<>
|
||||
<View>
|
||||
<Text
|
||||
style={[
|
||||
{
|
||||
color: theme.textColorPrimaryDark,
|
||||
},
|
||||
chapter.read === 1 && {
|
||||
color: theme.textColorHintDark,
|
||||
},
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{chapter.chapterName.substring(0, 50)}
|
||||
</Text>
|
||||
{chapter.releaseDate && (
|
||||
<Text
|
||||
style={[
|
||||
{
|
||||
color: theme.textColorSecondaryDark,
|
||||
marginTop: 5,
|
||||
fontSize: 12,
|
||||
},
|
||||
chapter.read === 1 && {
|
||||
color: theme.textColorHintDark,
|
||||
},
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{chapter.releaseDate}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
<View>
|
||||
{downloading.downloading &&
|
||||
downloading.chapterUrl === chapter.chapterUrl ? (
|
||||
<ActivityIndicator
|
||||
color={theme.textColorHintDark}
|
||||
size={25}
|
||||
style={{ margin: 3.5, padding: 5 }}
|
||||
/>
|
||||
) : (
|
||||
<IconButton
|
||||
icon={
|
||||
downloaded
|
||||
? "check-circle"
|
||||
: "arrow-down-circle-outline"
|
||||
}
|
||||
animated
|
||||
color={
|
||||
downloaded
|
||||
? theme.textColorPrimaryDark
|
||||
: theme.textColorHintDark
|
||||
}
|
||||
size={25}
|
||||
onPress={() => {
|
||||
downloadChapter(
|
||||
downloaded ? 1 : 0,
|
||||
extensionId,
|
||||
novelUrl,
|
||||
chapter.chapterUrl,
|
||||
chapter.chapterName
|
||||
);
|
||||
setDownloaded(!downloaded);
|
||||
}}
|
||||
style={{ margin: 2 }}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</>
|
||||
</TouchableRipple>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChapterCard;
|
||||
@@ -1,35 +0,0 @@
|
||||
import React, { useState } from "react";
|
||||
import { Checkbox } from "react-native-paper";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
export const DisplayCheckbox = ({ displayMode, onPress, value }) => {
|
||||
const theme = useSelector((state) => state.themeReducer.theme);
|
||||
|
||||
return (
|
||||
<Checkbox.Item
|
||||
label={value.charAt(0).toUpperCase() + value.slice(1)}
|
||||
labelStyle={{ color: theme.textColorPrimaryDark }}
|
||||
status={displayMode === value ? "checked" : "unchecked"}
|
||||
mode="ios"
|
||||
uncheckedColor={theme.textColorSecondaryDark}
|
||||
color={theme.colorAccentDark}
|
||||
onPress={onPress}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const ThemeCheckbox = ({ onPress, label, checked }) => {
|
||||
const theme = useSelector((state) => state.themeReducer.theme);
|
||||
|
||||
return (
|
||||
<Checkbox.Item
|
||||
label={label}
|
||||
labelStyle={{ color: theme.textColorPrimaryDark }}
|
||||
status={checked ? "checked" : "unchecked"}
|
||||
mode="ios"
|
||||
uncheckedColor={theme.textColorSecondaryDark}
|
||||
color={theme.colorAccentDark}
|
||||
onPress={onPress}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
import * as SQLite from "expo-sqlite";
|
||||
import {
|
||||
createNovelTableQuery,
|
||||
createUrlIndexQuery,
|
||||
createLibraryIndexQuery,
|
||||
} from "./tables/NovelTable";
|
||||
import {
|
||||
createChapterTableQuery,
|
||||
createNovelIdIndexQuery,
|
||||
createUnreadChaptersIndexQuery,
|
||||
} from "./tables/ChapterTable";
|
||||
import {
|
||||
createHistoryTableQuery,
|
||||
createChapterIdIndexQuery,
|
||||
} from "./tables/HistoryTable";
|
||||
import { createDownloadTableQuery } from "./tables/DownloadTable";
|
||||
import {
|
||||
createUpdatesTableQuery,
|
||||
updatesSeedDataQuery,
|
||||
} from "./tables/UpdateTable";
|
||||
|
||||
/**
|
||||
* Database Version = 2
|
||||
*/
|
||||
|
||||
const dbName = "lnreader.db";
|
||||
|
||||
const db = SQLite.openDatabase(dbName);
|
||||
|
||||
export const createDB = () => {
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(createNovelTableQuery);
|
||||
tx.executeSql(createChapterTableQuery);
|
||||
tx.executeSql(createHistoryTableQuery);
|
||||
tx.executeSql(createDownloadTableQuery);
|
||||
tx.executeSql(createUpdatesTableQuery);
|
||||
|
||||
//Db indexes
|
||||
tx.executeSql(createUrlIndexQuery);
|
||||
tx.executeSql(createLibraryIndexQuery);
|
||||
tx.executeSql(createNovelIdIndexQuery);
|
||||
tx.executeSql(createUnreadChaptersIndexQuery);
|
||||
tx.executeSql(createChapterIdIndexQuery);
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteDb = () => {
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql("DROP TABLE novels");
|
||||
tx.executeSql("DROP TABLE chapters");
|
||||
tx.executeSql("DROP TABLE history");
|
||||
tx.executeSql("DROP TABLE downloads");
|
||||
tx.executeSql("DROP TABLE updates");
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,201 @@
|
||||
import * as SQLite from "expo-sqlite";
|
||||
const db = SQLite.openDatabase("lnreader.db");
|
||||
|
||||
const insertChaptersQuery = `INSERT INTO chapters (chapterUrl, chapterName, releaseDate, novelId) values (?, ?, ?, ?)`;
|
||||
|
||||
export const insertChapters = async (novelId, chapters) => {
|
||||
db.transaction((tx) => {
|
||||
chapters.map(
|
||||
(chapter) =>
|
||||
tx.executeSql(
|
||||
insertChaptersQuery,
|
||||
[
|
||||
chapter.chapterUrl,
|
||||
chapter.chapterName,
|
||||
chapter.releaseDate,
|
||||
novelId,
|
||||
],
|
||||
(txObj, res) => {},
|
||||
(txObj, error) =>
|
||||
console.log(
|
||||
"Error ",
|
||||
error,
|
||||
" Chapter URL: ",
|
||||
chapter.chapterUrl,
|
||||
" Novel ID ",
|
||||
novelId
|
||||
)
|
||||
),
|
||||
(txObj, res) => {
|
||||
console.log("Success");
|
||||
},
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const getChaptersQuery = (sort, filter) =>
|
||||
`SELECT * FROM chapters WHERE novelId = ? ${filter ?? ""} ${sort ?? ""}`;
|
||||
|
||||
export const getChapters = (novelId, sort, filter) => {
|
||||
return new Promise((resolve, reject) =>
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
getChaptersQuery(sort, filter),
|
||||
[novelId],
|
||||
(txObj, { rows: { _array } }) => {
|
||||
resolve(_array);
|
||||
},
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const getChapterQuery = `SELECT * FROM downloads WHERE downloadChapterId = ?`;
|
||||
|
||||
export const getChapter = async (chapterId) => {
|
||||
return new Promise((resolve, reject) =>
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
getChapterQuery,
|
||||
[chapterId],
|
||||
(tx, results) => {
|
||||
resolve(results.rows.item(0));
|
||||
},
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const markChapterReadQuery = `UPDATE chapters SET \`read\` = 1 WHERE chapterId = ?`;
|
||||
|
||||
export const markChapterRead = async (chapterId) => {
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
markChapterReadQuery,
|
||||
[chapterId],
|
||||
(tx, res) => {},
|
||||
(tx, error) => console.log(error)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const isChapterDownloadedQuery = `SELECT * FROM downloads WHERE downloadChapterId=?`;
|
||||
|
||||
export const isChapterDownloaded = async (chapterId) => {
|
||||
return new Promise((resolve, reject) =>
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
isChapterDownloadedQuery,
|
||||
[chapterId],
|
||||
(txObj, res) => {
|
||||
if (res.rows.length !== 0) {
|
||||
resolve(true);
|
||||
} else {
|
||||
resolve(false);
|
||||
}
|
||||
},
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const downloadChapterQuery = `INSERT INTO downloads (downloadChapterId, chapterName, chapterText, prevChapter, nextChapter) VALUES (?, ?, ?, ?, ?)`;
|
||||
|
||||
export const downloadChapter = async (
|
||||
extensionId,
|
||||
novelUrl,
|
||||
chapterUrl,
|
||||
chapterId
|
||||
) => {
|
||||
const downloadUrl = `https://lnreader-extensions.herokuapp.com/api/${extensionId}/novel/${novelUrl}${chapterUrl}`;
|
||||
|
||||
// console.log(downloadUrl);
|
||||
|
||||
const response = await fetch(downloadUrl);
|
||||
const chapter = await response.json();
|
||||
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
`UPDATE chapters SET downloaded = 1 WHERE chapterId = ?`,
|
||||
[chapterId]
|
||||
);
|
||||
tx.executeSql(
|
||||
downloadChapterQuery,
|
||||
[
|
||||
chapterId,
|
||||
chapter.chapterName,
|
||||
chapter.chapterText,
|
||||
chapter.prevChapter,
|
||||
chapter.nextChapter,
|
||||
],
|
||||
(tx, res) =>
|
||||
console.log(`Downloaded Chapter ${chapter.chapterUrl}`),
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteChapter = async (chapterId) => {
|
||||
const updateIsDownloadedQuery = `UPDATE chapters SET downloaded = 0 WHERE chapterId=?`;
|
||||
const deleteChapterQuery = `DELETE FROM downloads WHERE downloadChapterId=?`;
|
||||
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
updateIsDownloadedQuery,
|
||||
[chapterId],
|
||||
(tx, res) => console.log(`Chaptermarked not downapdeod deleted`),
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
);
|
||||
tx.executeSql(
|
||||
deleteChapterQuery,
|
||||
[chapterId],
|
||||
(tx, res) => console.log(`Chapter deleted`),
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
// export const deleteAllChapters = async (novelId) => {
|
||||
// const updateIsDownloadedQuery = `UPDATE chapters SET downloaded = 0 WHERE novelId=?`;
|
||||
// const deleteChapterQuery = `DELETE FROM downloads WHERE downloadChapterId=?`;
|
||||
|
||||
// db.transaction((tx) => {
|
||||
// tx.executeSql(
|
||||
// updateIsDownloadedQuery,
|
||||
// [chapterId],
|
||||
// (tx, res) => console.log(`Chaptermarked not downapdeod deleted`),
|
||||
// (txObj, error) => console.log("Error ", error)
|
||||
// );
|
||||
// tx.executeSql(
|
||||
// deleteChapterQuery,
|
||||
// [chapterId],
|
||||
// (tx, res) => console.log(`Chapter deleted`),
|
||||
// (txObj, error) => console.log("Error ", error)
|
||||
// );
|
||||
// });
|
||||
// };
|
||||
|
||||
const getLastReadChapterQuery = `
|
||||
SELECT chapters.*
|
||||
FROM history
|
||||
JOIN chapters
|
||||
ON history.historyChapterId = chapters.chapterId
|
||||
WHERE history.historyNovelId = ?
|
||||
`;
|
||||
|
||||
export const getLastReadChapter = async (novelId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
getLastReadChapterQuery,
|
||||
[novelId],
|
||||
(tx, { rows }) => resolve(rows.item(0)),
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
import * as SQLite from "expo-sqlite";
|
||||
const db = SQLite.openDatabase("lnreader.db");
|
||||
|
||||
const getHistoryQuery = `
|
||||
SELECT history.*, chapters.*, novels.*
|
||||
FROM history
|
||||
JOIN chapters
|
||||
ON history.historyChapterId = chapters.chapterId
|
||||
JOIN novels
|
||||
ON history.historyNovelId = novels.novelId
|
||||
ORDER BY history.historyTimeRead DESC`;
|
||||
|
||||
export const getHistory = async () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
getHistoryQuery,
|
||||
null,
|
||||
(txObj, { rows: { _array } }) => {
|
||||
resolve(_array);
|
||||
},
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// const insertHistoryQuery =
|
||||
|
||||
export const insertHistory = async (novelId, chapterId) => {
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
"INSERT OR REPLACE INTO history (historyNovelId, historyChapterId, historyTimeRead) VALUES (?, ?, (datetime('now','localtime')))",
|
||||
[novelId, chapterId],
|
||||
(txObj, res) => {},
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
);
|
||||
tx.executeSql("UPDATE novels SET unread = 0 WHERE novelId = ?", [
|
||||
novelId,
|
||||
]);
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteHistory = async (novelId) => {
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql("DELETE FROM history WHERE historyNovelId = ?", [
|
||||
novelId,
|
||||
]);
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteAllHistory = () => {
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql("DELETE FROM history; VACCUM;");
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
import * as SQLite from "expo-sqlite";
|
||||
const db = SQLite.openDatabase("lnreader.db");
|
||||
|
||||
const getLibraryQuery = `
|
||||
SELECT novels.*, C.chaptersUnread, D.chaptersDownloaded
|
||||
FROM novels
|
||||
LEFT JOIN (
|
||||
SELECT chapters.novelId, COUNT(*) AS chaptersUnread
|
||||
FROM chapters
|
||||
WHERE chapters.read = 0
|
||||
GROUP BY chapters.novelId
|
||||
) AS C
|
||||
ON novels.novelId = C.novelId
|
||||
LEFT JOIN (
|
||||
SELECT chapters.novelId, COUNT(*) AS chaptersDownloaded
|
||||
FROM chapters
|
||||
WHERE chapters.downloaded = 1
|
||||
GROUP BY chapters.novelId
|
||||
) AS D
|
||||
ON novels.novelId = D.novelId
|
||||
WHERE novels.followed = 1
|
||||
GROUP BY novels.novelId
|
||||
`;
|
||||
|
||||
export const getLibrary = () => {
|
||||
return new Promise((resolve, reject) =>
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
getLibraryQuery,
|
||||
null,
|
||||
(txObj, { rows: { _array } }) => {
|
||||
// console.log(_array);
|
||||
resolve(_array);
|
||||
},
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
// const searchLibraryQuery = ``;
|
||||
|
||||
export const searchLibrary = (searchText) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
`
|
||||
SELECT novels.*, C.chaptersUnread, D.chaptersDownloaded
|
||||
FROM novels
|
||||
LEFT JOIN (
|
||||
SELECT chapters.novelId, COUNT(*) AS chaptersUnread
|
||||
FROM chapters
|
||||
WHERE chapters.read = 0
|
||||
GROUP BY chapters.novelId
|
||||
) AS C
|
||||
ON novels.novelId = C.novelId
|
||||
LEFT JOIN (
|
||||
SELECT chapters.novelId, COUNT(*) AS chaptersDownloaded
|
||||
FROM chapters
|
||||
WHERE chapters.downloaded = 1
|
||||
GROUP BY chapters.novelId
|
||||
) AS D
|
||||
ON novels.novelId = D.novelId
|
||||
WHERE novels.followed = 1 AND novelName LIKE '%${searchText}%'
|
||||
GROUP BY novels.novelId
|
||||
`,
|
||||
null,
|
||||
(txObj, { rows: { _array } }) => resolve(_array),
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,87 @@
|
||||
import * as SQLite from "expo-sqlite";
|
||||
const db = SQLite.openDatabase("lnreader.db");
|
||||
|
||||
const insertNovelQuery = `INSERT INTO novels (novelUrl, sourceUrl, sourceId, source, novelName, novelCover, novelSummary, author, artist, status, genre) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
|
||||
|
||||
export const insertNovel = async (novel) => {
|
||||
return new Promise((resolve, reject) =>
|
||||
db.transaction((tx) =>
|
||||
tx.executeSql(
|
||||
insertNovelQuery,
|
||||
[
|
||||
novel.novelUrl,
|
||||
novel.sourceUrl,
|
||||
novel.sourceId,
|
||||
novel.source,
|
||||
novel.novelName,
|
||||
novel.novelCover,
|
||||
novel.novelSummary,
|
||||
novel.author,
|
||||
novel.artist,
|
||||
novel.status,
|
||||
novel.genre,
|
||||
],
|
||||
(txObj, { insertId }) => resolve(insertId),
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export const followNovel = async (followed, novelId) => {
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
"UPDATE novels SET followed = ? WHERE novelId = ?",
|
||||
[!followed, novelId],
|
||||
(tx, res) => {},
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const checkNovelInCacheQuery = `SELECT * FROM novels WHERE novelUrl=? LIMIT 1`;
|
||||
|
||||
export const checkNovelInCache = (novelUrl) => {
|
||||
return new Promise((resolve, reject) =>
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
checkNovelInCacheQuery,
|
||||
[novelUrl],
|
||||
(txObj, res) => {
|
||||
if (res.rows.length !== 0) {
|
||||
resolve(true);
|
||||
} else {
|
||||
resolve(false);
|
||||
}
|
||||
},
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
export const getNovel = async (novelUrl) => {
|
||||
return new Promise((resolve, reject) =>
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
`SELECT * FROM novels WHERE novelUrl = ?`,
|
||||
[novelUrl],
|
||||
(txObj, { rows }) => resolve(rows.item(0)),
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
export const deleteNovelCache = () => {
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
"DELETE FROM novels WHERE followed = 0",
|
||||
null,
|
||||
(txObj, res) => {
|
||||
console.log("Cleared Novel Cache");
|
||||
},
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import * as SQLite from "expo-sqlite";
|
||||
const db = SQLite.openDatabase("lnreader.db");
|
||||
|
||||
const getHistoryQuery = `
|
||||
SELECT chapters.chapterId, chapters.chapterUrl, chapters.chapterName, novels.novelUrl, novels.novelId, novels.novelCover, novels.novelName, novels.sourceId, updates.updateTime, updates.updateId
|
||||
FROM updates
|
||||
JOIN chapters
|
||||
ON updates.chapterId = chapters.chapterId
|
||||
JOIN novels
|
||||
ON updates.novelId = novels.novelId
|
||||
ORDER BY updates.updateTime DESC`;
|
||||
|
||||
export const getUpdates = async () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
getHistoryQuery,
|
||||
null,
|
||||
(txObj, { rows: { _array } }) => {
|
||||
resolve(_array);
|
||||
},
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
export const createChapterTableQuery = `
|
||||
CREATE TABLE IF NOT EXISTS chapters(
|
||||
chapterId INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
chapterUrl TEXT,
|
||||
novelId INTEGER NOT NULL,
|
||||
chapterName TEXT NOT NULL,
|
||||
releaseDate TEXT,
|
||||
bookmark BOOLEAN NOT NULL DEFAULT 0,
|
||||
\`read\` INTEGER NOT NULL DEFAULT 0,
|
||||
downloaded INTEGER NOT NULL DEFAULT 0,
|
||||
UNIQUE(novelId, chapterUrl),
|
||||
FOREIGN KEY (novelId) REFERENCES novels(novelId)
|
||||
ON DELETE CASCADE
|
||||
)`;
|
||||
|
||||
export const createNovelIdIndexQuery = `CREATE INDEX IF NOT EXISTS chapterNovelIdIndex ON chapters(novelId)`;
|
||||
|
||||
export const createUnreadChaptersIndexQuery = `CREATE INDEX IF NOT EXISTS chapterUnreadByNovelIndex ON chapters(novelId, \`read\`)`;
|
||||
@@ -0,0 +1,11 @@
|
||||
export const createDownloadTableQuery = `
|
||||
CREATE TABLE IF NOT EXISTS downloads(
|
||||
downloadId INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
downloadChapterId INTEGER NOT NULL,
|
||||
chapterName TEXT,
|
||||
chapterText TEXT,
|
||||
nextChapter TEXT,
|
||||
prevChapter TEXT,
|
||||
FOREIGN KEY (downloadChapterId) REFERENCES chapters(chapterId)
|
||||
ON DELETE CASCADE
|
||||
)`;
|
||||
@@ -0,0 +1,11 @@
|
||||
export const createHistoryTableQuery = `
|
||||
CREATE TABLE IF NOT EXISTS history(
|
||||
historyId INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
historyChapterId INTEGER NOT NULL,
|
||||
historyNovelId INTEGER NOT NULL UNIQUE,
|
||||
historyTimeRead DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (historyChapterId) REFERENCES chapters(chapterId)
|
||||
ON DELETE CASCADE
|
||||
)`;
|
||||
|
||||
export const createChapterIdIndexQuery = `CREATE INDEX IF NOT EXISTS historyChapterIdIndex ON history(historyChapterId)`;
|
||||
@@ -0,0 +1,21 @@
|
||||
export const createNovelTableQuery = `
|
||||
CREATE TABLE IF NOT EXISTS novels(
|
||||
novelId INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
novelUrl TEXT NOT NULL,
|
||||
sourceUrl TEXT UNIQUE NOT NULL,
|
||||
sourceId INTEGER NOT NULL,
|
||||
source TEXT NOT NULL,
|
||||
novelName TEXT NOT NULL,
|
||||
novelCover TEXT NOT NULL,
|
||||
novelSummary TEXT,
|
||||
author TEXT,
|
||||
artist TEXT,
|
||||
status TEXT,
|
||||
genre TEXT,
|
||||
followed INTEGER DEFAULT 0,
|
||||
unread INTEGER DEFAULT 1
|
||||
)`;
|
||||
|
||||
export const createUrlIndexQuery = `CREATE INDEX IF NOT EXISTS novelUrlIndex ON novels(novelUrl)`;
|
||||
|
||||
export const createLibraryIndexQuery = `CREATE INDEX IF NOT EXISTS novelFollowedIndex ON novels(followed)`;
|
||||
@@ -0,0 +1,11 @@
|
||||
export const createUpdatesTableQuery = `
|
||||
CREATE TABLE IF NOT EXISTS updates(
|
||||
updateId INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
chapterId INTEGER NOT NULL,
|
||||
novelId INTEGER NOT NULL,
|
||||
updateTime DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(novelId, chapterId),
|
||||
FOREIGN KEY (chapterId) REFERENCES chapters(chapterId),
|
||||
FOREIGN KEY (novelId) REFERENCES novels(novelId)
|
||||
ON DELETE CASCADE
|
||||
)`;
|
||||
+49
-57
@@ -1,26 +1,27 @@
|
||||
import React from "react";
|
||||
import React, { useEffect } from "react";
|
||||
import {
|
||||
createStackNavigator,
|
||||
TransitionPresets,
|
||||
} from "@react-navigation/stack";
|
||||
import { createMaterialBottomTabNavigator } from "@react-navigation/material-bottom-tabs";
|
||||
import { Provider } from "react-native-paper";
|
||||
import { setStatusBarStyle } from "expo-status-bar";
|
||||
|
||||
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
|
||||
import Library from "../screens/Library";
|
||||
import History from "../screens/history/History";
|
||||
import Browse from "../screens/Browse";
|
||||
import Library from "../screens/Library/Library";
|
||||
import Updates from "../screens/Updates/Updates";
|
||||
import History from "../screens/History/History";
|
||||
import Browse from "../screens/Browse/Browse";
|
||||
|
||||
import More from "../screens/more/More";
|
||||
import About from "../screens/more/About";
|
||||
import Settings from "../screens/more/Settings";
|
||||
import More from "../screens/More/More";
|
||||
import About from "../screens/More/About";
|
||||
import Settings from "../screens/More/Settings";
|
||||
|
||||
import NovelItem from "../screens/NovelItem";
|
||||
import ChapterItem from "../screens/ChapterItem";
|
||||
import NovelItem from "../screens/Novel/Novel";
|
||||
import ChapterItem from "../screens/Chapter/Chapter";
|
||||
|
||||
// Extensions
|
||||
import BoxNovel from "../screens/extensions/boxnovel/BoxNovel";
|
||||
import ReadLightNovel from "../screens/extensions/readlightnovel/ReadLightNovel";
|
||||
import Extension from "../screens/Extension/Extension";
|
||||
|
||||
const Stack = createStackNavigator();
|
||||
|
||||
@@ -31,40 +32,24 @@ const stackNavigatorConfig = {
|
||||
...TransitionPresets.RevealFromBottomAndroid,
|
||||
};
|
||||
|
||||
const MoreStack = () => {
|
||||
const SettingsStack = () => {
|
||||
return (
|
||||
<Stack.Navigator screenOptions={stackNavigatorConfig}>
|
||||
<Stack.Screen name="More" component={More} />
|
||||
<Stack.Screen name="About" component={About} />
|
||||
<Stack.Screen name="Settings" component={Settings} />
|
||||
</Stack.Navigator>
|
||||
);
|
||||
};
|
||||
|
||||
const BoxNovelStack = () => {
|
||||
return (
|
||||
<Stack.Navigator screenOptions={stackNavigatorConfig}>
|
||||
<Stack.Screen name="BoxNovel" component={BoxNovel} />
|
||||
</Stack.Navigator>
|
||||
);
|
||||
};
|
||||
|
||||
const ReadLightNovelStack = () => {
|
||||
return (
|
||||
<Stack.Navigator screenOptions={stackNavigatorConfig}>
|
||||
<Stack.Screen name="ReadLightNovel" component={ReadLightNovel} />
|
||||
</Stack.Navigator>
|
||||
);
|
||||
};
|
||||
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
const BottomNavigator = () => {
|
||||
const theme = useSelector((state) => state.themeReducer.theme);
|
||||
|
||||
return (
|
||||
<Tab.Navigator
|
||||
shifting={false}
|
||||
barStyle={{ backgroundColor: theme.colorDarkPrimary }}
|
||||
barStyle={{ backgroundColor: theme.colorPrimary }}
|
||||
activeColor={theme.colorAccentDark}
|
||||
>
|
||||
<Tab.Screen
|
||||
@@ -80,7 +65,7 @@ const BottomNavigator = () => {
|
||||
),
|
||||
}}
|
||||
/>
|
||||
{/*
|
||||
|
||||
<Tab.Screen
|
||||
name="Updates"
|
||||
component={Updates}
|
||||
@@ -93,7 +78,7 @@ const BottomNavigator = () => {
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/> */}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="History"
|
||||
component={History}
|
||||
@@ -122,7 +107,7 @@ const BottomNavigator = () => {
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="More"
|
||||
component={MoreStack}
|
||||
component={More}
|
||||
options={{
|
||||
tabBarIcon: ({ color }) => (
|
||||
<MaterialCommunityIcons
|
||||
@@ -138,30 +123,37 @@ const BottomNavigator = () => {
|
||||
};
|
||||
|
||||
const Router = () => {
|
||||
const theme = useSelector((state) => state.themeReducer.theme);
|
||||
|
||||
useEffect(() => setStatusBarStyle(theme.id === 1 ? "dark" : "light"), []);
|
||||
|
||||
return (
|
||||
<Stack.Navigator screenOptions={stackNavigatorConfig}>
|
||||
<Stack.Screen name="Router" component={BottomNavigator} />
|
||||
<Stack.Screen
|
||||
name="ChapterItem"
|
||||
component={ChapterItem}
|
||||
options={{ ...TransitionPresets.SlideFromRightIOS }}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="NovelItem"
|
||||
component={NovelItem}
|
||||
options={{
|
||||
headerTitle: "",
|
||||
headerShown: true,
|
||||
headerTransparent: true,
|
||||
headerTintColor: "white",
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen name="BoxNovelStack" component={BoxNovelStack} />
|
||||
<Stack.Screen
|
||||
name="ReadLightNovelStack"
|
||||
component={ReadLightNovelStack}
|
||||
/>
|
||||
</Stack.Navigator>
|
||||
<Provider>
|
||||
<Stack.Navigator screenOptions={stackNavigatorConfig}>
|
||||
<Stack.Screen name="Router" component={BottomNavigator} />
|
||||
<Stack.Screen
|
||||
name="ChapterItem"
|
||||
component={ChapterItem}
|
||||
options={{ ...TransitionPresets.SlideFromRightIOS }}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="NovelItem"
|
||||
component={NovelItem}
|
||||
options={{
|
||||
headerTitle: "",
|
||||
headerShown: true,
|
||||
headerTransparent: true,
|
||||
headerTintColor: "white",
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen name="SettingsStack" component={SettingsStack} />
|
||||
<Stack.Screen name="Extension" component={Extension} />
|
||||
{/* <Stack.Screen
|
||||
name="ExtensionStack"
|
||||
component={ExtensionStack}
|
||||
/> */}
|
||||
</Stack.Navigator>
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { GET_LIBRARY_NOVELS } from "./types";
|
||||
import { getLibraryFromDb } from "../../services/db";
|
||||
|
||||
export const getLibraryNovels = () => async (dispatch) => {
|
||||
getLibraryFromDb().then((res) => console.log(res));
|
||||
|
||||
dispatch({
|
||||
type: GET_LIBRARY_NOVELS,
|
||||
payload: res.data,
|
||||
});
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
import { SWITCH_THEME } from "./types";
|
||||
|
||||
export const switchTheme = (theme) => (dispatch) => {
|
||||
dispatch({
|
||||
type: SWITCH_THEME,
|
||||
payload: theme,
|
||||
});
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
export const SWITCH_THEME = "SWITCH_THEME";
|
||||
|
||||
export const GET_LIBRARY_NOVELS = "GET_LIBRARY_NOVELS";
|
||||
@@ -0,0 +1,82 @@
|
||||
import { ToastAndroid } from "react-native";
|
||||
|
||||
import {
|
||||
CHAPTER_READ,
|
||||
GET_CHAPTER,
|
||||
CHAPTER_LOADING,
|
||||
CHAPTER_DOWNLOADING,
|
||||
CHAPTER_DOWNLOADED,
|
||||
CHAPTER_DELETED,
|
||||
} from "./chapter.types";
|
||||
|
||||
import { fetchChapter } from "../../source/Source";
|
||||
|
||||
import {
|
||||
markChapterRead,
|
||||
isChapterDownloaded,
|
||||
getChapter,
|
||||
downloadChapter,
|
||||
deleteChapter,
|
||||
} from "../../database/queries/ChapterQueries";
|
||||
|
||||
export const getChapterAction = (
|
||||
extensionId,
|
||||
chapterUrl,
|
||||
novelUrl,
|
||||
novelId
|
||||
) => async (dispatch) => {
|
||||
dispatch({ type: CHAPTER_LOADING });
|
||||
|
||||
const isDownloaded = await isChapterDownloaded(novelId);
|
||||
let chapter;
|
||||
|
||||
if (isDownloaded) {
|
||||
chapter = await getChapter(novelId);
|
||||
} else {
|
||||
chapter = await fetchChapter(extensionId, novelUrl, chapterUrl);
|
||||
}
|
||||
console.log(isDownloaded);
|
||||
dispatch({ type: GET_CHAPTER, payload: chapter });
|
||||
};
|
||||
|
||||
export const markChapterReadAction = (chapterId) => async (dispatch) => {
|
||||
await markChapterRead(chapterId);
|
||||
dispatch({ type: CHAPTER_READ, payload: { chapterId } });
|
||||
};
|
||||
|
||||
export const downloadChapterAction = (
|
||||
extensionId,
|
||||
novelUrl,
|
||||
chapterUrl,
|
||||
chapterName,
|
||||
chapterId
|
||||
) => async (dispatch) => {
|
||||
dispatch({
|
||||
type: CHAPTER_DOWNLOADING,
|
||||
payload: {
|
||||
chapterId,
|
||||
},
|
||||
});
|
||||
|
||||
await downloadChapter(extensionId, novelUrl, chapterUrl, chapterId);
|
||||
|
||||
dispatch({
|
||||
type: CHAPTER_DOWNLOADED,
|
||||
payload: { chapterId },
|
||||
});
|
||||
|
||||
ToastAndroid.show(`Downloaded ${chapterName}`, ToastAndroid.SHORT);
|
||||
};
|
||||
|
||||
export const deleteChapterAction = (chapterId, chapterName) => async (
|
||||
dispatch
|
||||
) => {
|
||||
await deleteChapter(chapterId);
|
||||
|
||||
dispatch({
|
||||
type: CHAPTER_DELETED,
|
||||
payload: { chapterId },
|
||||
});
|
||||
|
||||
ToastAndroid.show(`Deleted ${chapterName}`, ToastAndroid.SHORT);
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
import {
|
||||
CHAPTER_READ,
|
||||
GET_CHAPTER,
|
||||
CHAPTER_LOADING,
|
||||
CHAPTER_DOWNLOADING,
|
||||
CHAPTER_DOWNLOADED,
|
||||
CHAPTER_DELETED,
|
||||
} from "./chapter.types";
|
||||
|
||||
const initialState = {
|
||||
chapter: null,
|
||||
downloading: [],
|
||||
loading: true,
|
||||
};
|
||||
|
||||
const chapterReducer = (state = initialState, action) => {
|
||||
const { type, payload } = action;
|
||||
|
||||
switch (type) {
|
||||
case CHAPTER_READ:
|
||||
return {
|
||||
...state,
|
||||
chapters: state.chapters.map((chapter) =>
|
||||
chapter.chapterId === payload.chapterId
|
||||
? { ...chapter, read: 1 }
|
||||
: chapter
|
||||
),
|
||||
};
|
||||
case CHAPTER_LOADING:
|
||||
return {
|
||||
...state,
|
||||
chapterLoading: true,
|
||||
};
|
||||
case GET_CHAPTER:
|
||||
return { ...state, chapter: payload, chapterLoading: false };
|
||||
case CHAPTER_DOWNLOADING:
|
||||
return {
|
||||
...state,
|
||||
downloading: payload.chapterId,
|
||||
};
|
||||
case CHAPTER_DOWNLOADED:
|
||||
return {
|
||||
...state,
|
||||
downloading: null,
|
||||
chapters: state.chapters.map((chapter) =>
|
||||
chapter.chapterId === payload.chapterId
|
||||
? { ...chapter, downloaded: 1 }
|
||||
: chapter
|
||||
),
|
||||
};
|
||||
case CHAPTER_DELETED:
|
||||
return {
|
||||
...state,
|
||||
chapters: state.chapters.map((chapter) =>
|
||||
chapter.chapterId === payload.chapterId
|
||||
? { ...chapter, downloaded: 0 }
|
||||
: chapter
|
||||
),
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default chapterReducer;
|
||||
@@ -0,0 +1,6 @@
|
||||
export const CHAPTER_DOWNLOADED = "CHAPTER_DOWNLOADED";
|
||||
export const CHAPTER_DELETED = "CHAPTER_DELETED";
|
||||
export const CHAPTER_DOWNLOADING = "CHAPTER_DOWNLOADING";
|
||||
export const CHAPTER_READ = "CHAPTER_READ";
|
||||
export const CHAPTER_LOADING = "CHAPTER_LOADING";
|
||||
export const GET_CHAPTER = "GET_CHAPTER";
|
||||
@@ -0,0 +1,36 @@
|
||||
import {
|
||||
UPDATE_NOVEL_HISTORY,
|
||||
CLEAR_NOVEL_HISTORY,
|
||||
GET_HISTORY,
|
||||
LOAD_HISTORY,
|
||||
} from "./history.types";
|
||||
import {
|
||||
getHistory,
|
||||
insertHistory,
|
||||
deleteHistory,
|
||||
} from "../../database/queries/HistoryQueries";
|
||||
|
||||
export const getHistoryAction = () => async (dispatch) => {
|
||||
dispatch({ type: LOAD_HISTORY });
|
||||
|
||||
const history = await getHistory();
|
||||
|
||||
dispatch({ type: GET_HISTORY, payload: history });
|
||||
};
|
||||
|
||||
export const insertHistoryAction = (novelId, chapterId) => async (dispatch) => {
|
||||
await insertHistory(novelId, chapterId);
|
||||
|
||||
dispatch({
|
||||
type: UPDATE_NOVEL_HISTORY,
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteHistoryAction = (novelId) => async (dispatch) => {
|
||||
await deleteHistory(novelId);
|
||||
|
||||
dispatch({
|
||||
type: CLEAR_NOVEL_HISTORY,
|
||||
payload: { novelId },
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
import {
|
||||
CLEAR_NOVEL_HISTORY,
|
||||
GET_HISTORY,
|
||||
LOAD_HISTORY,
|
||||
UPDATE_NOVEL_HISTORY,
|
||||
} from "./history.types";
|
||||
|
||||
const initialState = {
|
||||
history: [],
|
||||
loading: true,
|
||||
};
|
||||
|
||||
const historyReducer = (state = initialState, action) => {
|
||||
const { type, payload } = action;
|
||||
|
||||
switch (type) {
|
||||
case LOAD_HISTORY:
|
||||
return { ...state, loading: true };
|
||||
case GET_HISTORY:
|
||||
return { ...state, history: payload, loading: false };
|
||||
case UPDATE_NOVEL_HISTORY:
|
||||
return state;
|
||||
case CLEAR_NOVEL_HISTORY:
|
||||
return {
|
||||
...state,
|
||||
history: state.history.filter(
|
||||
(item) => item.novelId !== payload.novelId
|
||||
),
|
||||
};
|
||||
// return {
|
||||
// ...state,
|
||||
// history: state.history.map((novel) =>
|
||||
// novel.novelUrl === payload.novelUrl ? payload : novel
|
||||
// ),
|
||||
// };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default historyReducer;
|
||||
@@ -0,0 +1,4 @@
|
||||
export const LOAD_HISTORY = "LOAD_HISTORY";
|
||||
export const GET_HISTORY = "GET_HISTORY";
|
||||
export const UPDATE_NOVEL_HISTORY = "UPDATE_NOVEL_HISTORY";
|
||||
export const CLEAR_NOVEL_HISTORY = "CLEAR_NOVEL_HISTORY";
|
||||
@@ -0,0 +1,26 @@
|
||||
import {
|
||||
GET_LIBRARY_NOVELS,
|
||||
GET_LIBRARY_SEARCH_RESULTS,
|
||||
} from "./library.types";
|
||||
import {
|
||||
getLibrary,
|
||||
searchLibrary,
|
||||
} from "../../database/queries/LibraryQueries";
|
||||
|
||||
export const getLibraryAction = () => async (dispatch) => {
|
||||
const res = await getLibrary();
|
||||
|
||||
dispatch({
|
||||
type: GET_LIBRARY_NOVELS,
|
||||
payload: res,
|
||||
});
|
||||
};
|
||||
|
||||
export const searchLibraryAction = (searchText) => async (dispatch) => {
|
||||
const res = await searchLibrary(searchText);
|
||||
|
||||
dispatch({
|
||||
type: GET_LIBRARY_SEARCH_RESULTS,
|
||||
payload: res,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import { FOLLOW_NOVEL, UNFOLLOW_NOVEL } from "../novel/novel.types";
|
||||
import {
|
||||
GET_LIBRARY_NOVELS,
|
||||
GET_LIBRARY_SEARCH_RESULTS,
|
||||
} from "./library.types";
|
||||
|
||||
const initialState = {
|
||||
novels: [],
|
||||
searchResults: [],
|
||||
loading: true,
|
||||
};
|
||||
|
||||
const libraryReducer = (state = initialState, action) => {
|
||||
const { type, payload } = action;
|
||||
|
||||
switch (type) {
|
||||
case GET_LIBRARY_NOVELS:
|
||||
return { ...state, novels: payload, loading: false };
|
||||
case GET_LIBRARY_SEARCH_RESULTS:
|
||||
return { ...state, searchResults: payload, loading: false };
|
||||
case FOLLOW_NOVEL:
|
||||
return {
|
||||
...state,
|
||||
novels: [...state.novels, { ...payload, followed: 1 }],
|
||||
};
|
||||
case UNFOLLOW_NOVEL:
|
||||
return {
|
||||
...state,
|
||||
novels: state.novels.filter(
|
||||
(novel) => novel.novelId !== payload
|
||||
),
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default libraryReducer;
|
||||
@@ -0,0 +1,2 @@
|
||||
export const GET_LIBRARY_NOVELS = "GET_LIBRARY_NOVELS";
|
||||
export const GET_LIBRARY_SEARCH_RESULTS = "GET_LIBRARY_SEARCH_RESULTS";
|
||||
@@ -0,0 +1,235 @@
|
||||
import { ToastAndroid } from "react-native";
|
||||
|
||||
import {
|
||||
LOADING_NOVEL,
|
||||
GET_NOVEL,
|
||||
GET_CHAPTERS,
|
||||
FETCHING_NOVEL,
|
||||
SET_NOVEL,
|
||||
UPDATE_IN_LIBRARY,
|
||||
CHAPTER_READ,
|
||||
GET_CHAPTER,
|
||||
CHAPTER_LOADING,
|
||||
CHAPTER_DOWNLOADING,
|
||||
CHAPTER_DOWNLOADED,
|
||||
CHAPTER_DELETED,
|
||||
UPDATE_NOVEL,
|
||||
} from "./novel.types";
|
||||
|
||||
import { updateNovel } from "../../services/updates";
|
||||
import { fetchChapter, fetchNovel } from "../../source/Source";
|
||||
import {
|
||||
followNovel,
|
||||
insertNovel,
|
||||
getNovel,
|
||||
} from "../../database/queries/NovelQueries";
|
||||
import {
|
||||
getChapters,
|
||||
insertChapters,
|
||||
markChapterRead,
|
||||
isChapterDownloaded,
|
||||
getChapter,
|
||||
downloadChapter,
|
||||
deleteChapter,
|
||||
} from "../../database/queries/ChapterQueries";
|
||||
|
||||
export const setNovel = (novel) => async (dispatch) => {
|
||||
dispatch({ type: SET_NOVEL, payload: novel });
|
||||
};
|
||||
|
||||
export const getNovelAction = (
|
||||
followed,
|
||||
sourceId,
|
||||
novelUrl,
|
||||
novelId,
|
||||
sort,
|
||||
filter
|
||||
) => async (dispatch) => {
|
||||
dispatch({ type: LOADING_NOVEL });
|
||||
|
||||
if (followed === 1) {
|
||||
const chapters = await getChapters(novelId, sort, filter);
|
||||
|
||||
dispatch({
|
||||
type: GET_CHAPTERS,
|
||||
payload: chapters,
|
||||
});
|
||||
} else {
|
||||
dispatch({ type: FETCHING_NOVEL });
|
||||
|
||||
const novel = await getNovel(novelUrl);
|
||||
|
||||
if (novel) {
|
||||
novel.chapters = await getChapters(novel.novelId, sort, filter);
|
||||
dispatch({
|
||||
type: GET_NOVEL,
|
||||
payload: novel,
|
||||
});
|
||||
} else {
|
||||
/**
|
||||
* Fetch novel from source
|
||||
*/
|
||||
const fetchedNovel = await fetchNovel(sourceId, novelUrl);
|
||||
|
||||
/**
|
||||
* Insert novel in db
|
||||
*/
|
||||
const fetchedNovelId = await insertNovel(fetchedNovel);
|
||||
await insertChapters(fetchedNovelId, fetchedNovel.chapters);
|
||||
|
||||
/**
|
||||
* Get Novel from db
|
||||
*/
|
||||
const novel = await getNovel(novelUrl);
|
||||
novel.chapters = await getChapters(novel.novelId);
|
||||
|
||||
dispatch({
|
||||
type: GET_NOVEL,
|
||||
payload: novel,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const sortAndFilterChapters = (novelUrl, filter, sort) => async (
|
||||
dispatch
|
||||
) => {
|
||||
// const chapters = await getChaptersFromDb(novelUrl, filter, sort);
|
||||
// dispatch({
|
||||
// type: GET_CHAPTERS,
|
||||
// payload: chapters,
|
||||
// });
|
||||
};
|
||||
|
||||
export const followNovelAction = (novel) => async (dispatch) => {
|
||||
await followNovel(novel.followed, novel.novelId);
|
||||
|
||||
dispatch({
|
||||
type: UPDATE_IN_LIBRARY,
|
||||
payload: !novel.followed,
|
||||
});
|
||||
|
||||
ToastAndroid.show(
|
||||
!novel.followed ? "Added to library" : "Removed from library",
|
||||
ToastAndroid.SHORT
|
||||
);
|
||||
};
|
||||
|
||||
export const getChapterAction = (
|
||||
extensionId,
|
||||
chapterUrl,
|
||||
novelUrl,
|
||||
novelId
|
||||
) => async (dispatch) => {
|
||||
dispatch({ type: CHAPTER_LOADING });
|
||||
|
||||
const isDownloaded = await isChapterDownloaded(novelId);
|
||||
let chapter;
|
||||
|
||||
if (isDownloaded) {
|
||||
chapter = await getChapter(novelId);
|
||||
} else {
|
||||
chapter = await fetchChapter(extensionId, novelUrl, chapterUrl);
|
||||
}
|
||||
dispatch({ type: GET_CHAPTER, payload: chapter });
|
||||
};
|
||||
|
||||
export const markChapterReadAction = (chapterId) => async (dispatch) => {
|
||||
await markChapterRead(chapterId);
|
||||
dispatch({ type: CHAPTER_READ, payload: { chapterId } });
|
||||
};
|
||||
|
||||
export const downloadChapterAction = (
|
||||
extensionId,
|
||||
novelUrl,
|
||||
chapterUrl,
|
||||
chapterName,
|
||||
chapterId
|
||||
) => async (dispatch) => {
|
||||
dispatch({
|
||||
type: CHAPTER_DOWNLOADING,
|
||||
payload: chapterId,
|
||||
});
|
||||
|
||||
await downloadChapter(extensionId, novelUrl, chapterUrl, chapterId);
|
||||
|
||||
dispatch({
|
||||
type: CHAPTER_DOWNLOADED,
|
||||
payload: chapterId,
|
||||
});
|
||||
|
||||
ToastAndroid.show(`Downloaded ${chapterName}`, ToastAndroid.SHORT);
|
||||
};
|
||||
|
||||
export const downloadAllChaptersAction = (
|
||||
extensionId,
|
||||
novelUrl,
|
||||
chapters
|
||||
) => async (dispatch) => {
|
||||
await chapters.map((chapter, index) => {
|
||||
setTimeout(async () => {
|
||||
dispatch({
|
||||
type: CHAPTER_DOWNLOADING,
|
||||
payload: chapter.chapterId,
|
||||
});
|
||||
|
||||
if (!chapter.downloaded) {
|
||||
await downloadChapter(
|
||||
extensionId,
|
||||
novelUrl,
|
||||
chapter.chapterUrl,
|
||||
chapter.chapterId
|
||||
);
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: CHAPTER_DOWNLOADED,
|
||||
payload: chapter.chapterId,
|
||||
});
|
||||
}, 1000 * index);
|
||||
});
|
||||
|
||||
ToastAndroid.show(`All chapters downloaded`, ToastAndroid.SHORT);
|
||||
};
|
||||
|
||||
export const deleteChapterAction = (chapterId, chapterName) => async (
|
||||
dispatch
|
||||
) => {
|
||||
await deleteChapter(chapterId);
|
||||
|
||||
dispatch({
|
||||
type: CHAPTER_DELETED,
|
||||
payload: chapterId,
|
||||
});
|
||||
|
||||
ToastAndroid.show(`Deleted ${chapterName}`, ToastAndroid.SHORT);
|
||||
};
|
||||
|
||||
export const deleteAllChaptersAction = (chapters) => async (dispatch) => {
|
||||
await chapters.map((chapter) => {
|
||||
deleteChapter(chapter.chapterId);
|
||||
|
||||
dispatch({
|
||||
type: CHAPTER_DELETED,
|
||||
payload: chapter.chapterId,
|
||||
});
|
||||
});
|
||||
|
||||
ToastAndroid.show(`Deleted all chapters`, ToastAndroid.SHORT);
|
||||
};
|
||||
|
||||
export const updateNovelAction = (extensionId, novelUrl, novelId) => async (
|
||||
dispatch
|
||||
) => {
|
||||
dispatch({ type: FETCHING_NOVEL });
|
||||
|
||||
await updateNovel(extensionId, novelUrl, novelId);
|
||||
|
||||
let novel = await getNovel(novelUrl);
|
||||
let chapters = await getChapters(novel.novelId);
|
||||
|
||||
dispatch({
|
||||
type: UPDATE_NOVEL,
|
||||
payload: { novel, chapters },
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,121 @@
|
||||
import {
|
||||
CHAPTER_DOWNLOADING,
|
||||
CHAPTER_DOWNLOADED,
|
||||
CHAPTER_DELETED,
|
||||
GET_CHAPTERS,
|
||||
LOADING_NOVEL,
|
||||
GET_NOVEL,
|
||||
GET_CHAPTER,
|
||||
FETCHING_NOVEL,
|
||||
SET_NOVEL,
|
||||
UPDATE_IN_LIBRARY,
|
||||
CHAPTER_READ,
|
||||
CHAPTER_LOADING,
|
||||
UPDATE_NOVEL,
|
||||
UPDATE_LAST_READ,
|
||||
} from "./novel.types";
|
||||
|
||||
const initialState = {
|
||||
novel: null,
|
||||
chapters: [],
|
||||
chapter: null,
|
||||
downloading: [],
|
||||
chapterLoading: true,
|
||||
loading: true,
|
||||
fetching: false,
|
||||
};
|
||||
|
||||
const novelReducer = (state = initialState, action) => {
|
||||
const { type, payload } = action;
|
||||
|
||||
switch (type) {
|
||||
case LOADING_NOVEL:
|
||||
return { ...state, loading: true };
|
||||
case FETCHING_NOVEL:
|
||||
return { ...state, fetching: true };
|
||||
case SET_NOVEL:
|
||||
return { ...state, novel: payload };
|
||||
case GET_NOVEL:
|
||||
return {
|
||||
...state,
|
||||
novel: payload,
|
||||
chapters: payload.chapters,
|
||||
loading: false,
|
||||
fetching: false,
|
||||
};
|
||||
case GET_CHAPTERS:
|
||||
return {
|
||||
...state,
|
||||
chapters: payload,
|
||||
loading: false,
|
||||
fetching: false,
|
||||
};
|
||||
case UPDATE_IN_LIBRARY:
|
||||
return {
|
||||
...state,
|
||||
novel: { ...state.novel, followed: payload },
|
||||
};
|
||||
case UPDATE_NOVEL:
|
||||
return {
|
||||
...state,
|
||||
novel: payload.novel,
|
||||
chapters: payload.chapters,
|
||||
loading: false,
|
||||
fetching: false,
|
||||
};
|
||||
case CHAPTER_READ:
|
||||
return {
|
||||
...state,
|
||||
chapters: state.chapters.map((chapter) =>
|
||||
chapter.chapterId === payload.chapterId
|
||||
? { ...chapter, read: 1 }
|
||||
: chapter
|
||||
),
|
||||
};
|
||||
case CHAPTER_LOADING:
|
||||
return {
|
||||
...state,
|
||||
chapterLoading: true,
|
||||
};
|
||||
case GET_CHAPTER:
|
||||
return { ...state, chapter: payload, chapterLoading: false };
|
||||
case CHAPTER_DOWNLOADING:
|
||||
return {
|
||||
...state,
|
||||
chapters: state.chapters.map((chapter) =>
|
||||
chapter.chapterId === payload
|
||||
? { ...chapter, downloaded: 3 }
|
||||
: chapter
|
||||
),
|
||||
};
|
||||
case CHAPTER_DOWNLOADED:
|
||||
return {
|
||||
...state,
|
||||
// downloading: state.downloading.filter((id) => id === payload),
|
||||
chapters: state.chapters.map((chapter) =>
|
||||
chapter.chapterId === payload
|
||||
? { ...chapter, downloaded: 1 }
|
||||
: chapter
|
||||
),
|
||||
};
|
||||
case CHAPTER_DELETED:
|
||||
return {
|
||||
...state,
|
||||
chapters: state.chapters.map((chapter) =>
|
||||
chapter.chapterId === payload
|
||||
? { ...chapter, downloaded: 0 }
|
||||
: chapter
|
||||
),
|
||||
};
|
||||
|
||||
case UPDATE_LAST_READ:
|
||||
return {
|
||||
...state,
|
||||
novel: { ...state.novel, lastRead: payload },
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default novelReducer;
|
||||
@@ -0,0 +1,19 @@
|
||||
export const LOADING_NOVEL = "LOADING_NOVEL";
|
||||
export const FETCHING_NOVEL = "FETCHING_NOVEL";
|
||||
export const LOADING_CHAPTERS = "LOADING_CHAPTERS";
|
||||
export const GET_NOVEL = "GET_NOVEL";
|
||||
export const SET_NOVEL = "SET_NOVEL";
|
||||
export const UPDATE_NOVEL = "UPDATE_NOVEL";
|
||||
export const UPDATE_IN_LIBRARY = "UPDATE_IN_LIBRARY";
|
||||
export const GET_CHAPTERS = "GET_CHAPTERS";
|
||||
export const UPDATE_LAST_READ = "UPDATE_LAST_READ";
|
||||
|
||||
export const CHAPTER_DOWNLOADED = "CHAPTER_DOWNLOADED";
|
||||
export const CHAPTER_DELETED = "CHAPTER_DELETED";
|
||||
export const CHAPTER_DOWNLOADING = "CHAPTER_DOWNLOADING";
|
||||
export const CHAPTER_READ = "CHAPTER_READ";
|
||||
export const CHAPTER_LOADING = "CHAPTER_LOADING";
|
||||
export const GET_CHAPTER = "GET_CHAPTER";
|
||||
|
||||
export const FOLLOW_NOVEL = "FOLLOW_NOVEL";
|
||||
export const UNFOLLOW_NOVEL = "UNFOLLOW_NOVEL";
|
||||
@@ -1,18 +0,0 @@
|
||||
import { amoledDarkTheme } from "../../theme/theme";
|
||||
|
||||
const initialState = {
|
||||
theme: amoledDarkTheme,
|
||||
};
|
||||
|
||||
const themeReducer = (state = initialState, action) => {
|
||||
const { type, payload } = action;
|
||||
|
||||
switch (type) {
|
||||
case "SWITCH_THEME":
|
||||
return { theme: payload };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default themeReducer;
|
||||
@@ -0,0 +1,58 @@
|
||||
import {
|
||||
SET_DISPLAY_MODE,
|
||||
SET_ITEMS_PER_ROW,
|
||||
UPDATE_READER_TEXT_SIZE,
|
||||
UPDATE_READER_THEME,
|
||||
UPDATE_READER_TEXT_ALIGN,
|
||||
UPDATE_READER_PADDING,
|
||||
SET_READER_FONT,
|
||||
} from "./settings.types";
|
||||
|
||||
export const setDisplayMode = (val) => (dispatch) => {
|
||||
dispatch({
|
||||
type: SET_DISPLAY_MODE,
|
||||
payload: val,
|
||||
});
|
||||
};
|
||||
|
||||
export const setItemsPerRow = (val) => (dispatch) => {
|
||||
dispatch({
|
||||
type: SET_ITEMS_PER_ROW,
|
||||
payload: val,
|
||||
});
|
||||
};
|
||||
|
||||
export const updateReaderTheme = (val) => (dispatch) => {
|
||||
dispatch({
|
||||
type: UPDATE_READER_THEME,
|
||||
payload: val,
|
||||
});
|
||||
};
|
||||
|
||||
export const updateReaderTextSize = (val) => (dispatch) => {
|
||||
dispatch({
|
||||
type: UPDATE_READER_TEXT_SIZE,
|
||||
payload: val,
|
||||
});
|
||||
};
|
||||
|
||||
export const updateReaderTextAlign = (val) => (dispatch) => {
|
||||
dispatch({
|
||||
type: UPDATE_READER_TEXT_ALIGN,
|
||||
payload: val,
|
||||
});
|
||||
};
|
||||
|
||||
export const updateReaderPadding = (val) => (dispatch) => {
|
||||
dispatch({
|
||||
type: UPDATE_READER_PADDING,
|
||||
payload: val,
|
||||
});
|
||||
};
|
||||
|
||||
export const setReaderFont = (val) => (dispatch) => {
|
||||
dispatch({
|
||||
type: SET_READER_FONT,
|
||||
payload: val,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
import {
|
||||
SET_DISPLAY_MODE,
|
||||
SET_ITEMS_PER_ROW,
|
||||
SET_READER_FONT,
|
||||
UPDATE_READER_PADDING,
|
||||
UPDATE_READER_TEXT_ALIGN,
|
||||
UPDATE_READER_TEXT_SIZE,
|
||||
UPDATE_READER_THEME,
|
||||
} from "./settings.types";
|
||||
|
||||
const initialState = {
|
||||
displayMode: 0,
|
||||
itemsPerRow: 3,
|
||||
reader: {
|
||||
theme: 1,
|
||||
textSize: 16,
|
||||
textAlign: "left",
|
||||
padding: 5,
|
||||
fontFamily: null,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Display Mode
|
||||
* 0 -> Compact
|
||||
* 1 -> Comfortable
|
||||
* 2 -> List
|
||||
*/
|
||||
|
||||
/**
|
||||
* Reader Settings
|
||||
* theme
|
||||
* 1 -> Dark
|
||||
* 2 -> White
|
||||
* 3 -> Sepia
|
||||
*/
|
||||
|
||||
const settingsReducer = (state = initialState, action) => {
|
||||
const { type, payload } = action;
|
||||
|
||||
switch (type) {
|
||||
case SET_DISPLAY_MODE:
|
||||
return {
|
||||
...state,
|
||||
displayMode: payload,
|
||||
itemsPerRow: payload === 2 ? 1 : state.itemsPerRow,
|
||||
};
|
||||
case SET_ITEMS_PER_ROW:
|
||||
return {
|
||||
...state,
|
||||
itemsPerRow: state.displayMode !== 2 ? payload : 1,
|
||||
};
|
||||
case UPDATE_READER_THEME:
|
||||
return { ...state, reader: { ...state.reader, theme: payload } };
|
||||
case UPDATE_READER_TEXT_SIZE:
|
||||
return { ...state, reader: { ...state.reader, textSize: payload } };
|
||||
case UPDATE_READER_TEXT_ALIGN:
|
||||
return {
|
||||
...state,
|
||||
reader: { ...state.reader, textAlign: payload },
|
||||
};
|
||||
case UPDATE_READER_PADDING:
|
||||
return {
|
||||
...state,
|
||||
reader: { ...state.reader, padding: payload },
|
||||
};
|
||||
case SET_READER_FONT:
|
||||
return {
|
||||
...state,
|
||||
reader: { ...state.reader, fontFamily: payload },
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default settingsReducer;
|
||||
@@ -0,0 +1,7 @@
|
||||
export const SET_DISPLAY_MODE = "SET_DISPLAY_MODE";
|
||||
export const SET_ITEMS_PER_ROW = "SET_ITEMS_PER_ROW";
|
||||
export const UPDATE_READER_THEME = "UPDATE_READER_THEME";
|
||||
export const UPDATE_READER_TEXT_SIZE = "UPDATE_READER_TEXT_SIZE";
|
||||
export const UPDATE_READER_TEXT_ALIGN = "UPDATE_READER_TEXT_ALIGN";
|
||||
export const UPDATE_READER_PADDING = "UPDATE_READER_PADDING";
|
||||
export const SET_READER_FONT = "SET_READER_FONT";
|
||||
@@ -0,0 +1,11 @@
|
||||
import { GET_SOURCES } from "./source.types";
|
||||
import { fetchSources } from "../../source/Source";
|
||||
|
||||
export const getSourcesAction = () => async (dispatch) => {
|
||||
const res = await fetchSources();
|
||||
|
||||
dispatch({
|
||||
type: GET_SOURCES,
|
||||
payload: res,
|
||||
});
|
||||
};
|
||||
@@ -1,5 +1,7 @@
|
||||
import { GET_SOURCES } from "./source.types";
|
||||
|
||||
const initialState = {
|
||||
novels: [],
|
||||
extensions: [],
|
||||
loading: true,
|
||||
};
|
||||
|
||||
@@ -7,8 +9,8 @@ const libraryReducer = (state = initialState, action) => {
|
||||
const { type, payload } = action;
|
||||
|
||||
switch (type) {
|
||||
case "GET_LIBRARY_NOVELS":
|
||||
return { novels: payload, loading: false };
|
||||
case GET_SOURCES:
|
||||
return { extensions: payload, loading: false };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export const GET_SOURCES = "GET_SOURCES";
|
||||
@@ -0,0 +1,34 @@
|
||||
import { createStore, applyMiddleware, combineReducers } from "redux";
|
||||
import { persistStore, persistReducer } from "redux-persist";
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import thunk from "redux-thunk";
|
||||
|
||||
import themeReducer from "./theme/theme.reducer";
|
||||
import settingsReducer from "./settings/settings.reducer";
|
||||
import libraryReducer from "./library/library.reducer";
|
||||
import extensionReducer from "./source/source.reducers";
|
||||
import novelReducer from "./novel/novel.reducer";
|
||||
import historyReducer from "./history/history.reducer";
|
||||
import updatesReducer from "./updates/updates.reducer";
|
||||
|
||||
const persistConfig = {
|
||||
key: "root",
|
||||
storage: AsyncStorage,
|
||||
blacklist: ["novelReducer"],
|
||||
};
|
||||
|
||||
const persistedReducer = persistReducer(
|
||||
persistConfig,
|
||||
combineReducers({
|
||||
themeReducer,
|
||||
settingsReducer,
|
||||
libraryReducer,
|
||||
extensionReducer,
|
||||
novelReducer,
|
||||
historyReducer,
|
||||
updatesReducer,
|
||||
})
|
||||
);
|
||||
|
||||
export const store = createStore(persistedReducer, applyMiddleware(thunk));
|
||||
export const persistor = persistStore(store);
|
||||
@@ -1,20 +0,0 @@
|
||||
import { createStore, applyMiddleware, combineReducers } from "redux";
|
||||
import { persistStore, persistReducer } from "redux-persist";
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import thunk from "redux-thunk";
|
||||
|
||||
import themeReducer from "../reducers/theme";
|
||||
import libraryReducer from "../reducers/library";
|
||||
|
||||
const persistConfig = {
|
||||
key: "root",
|
||||
storage: AsyncStorage,
|
||||
};
|
||||
|
||||
const persistedReducer = persistReducer(
|
||||
persistConfig,
|
||||
combineReducers({ themeReducer, libraryReducer })
|
||||
);
|
||||
|
||||
export const store = createStore(persistedReducer, applyMiddleware(thunk));
|
||||
export const persistor = persistStore(store);
|
||||
@@ -0,0 +1,8 @@
|
||||
import { SWITCH_THEME } from "./theme.types";
|
||||
|
||||
export const switchTheme = (val) => (dispatch) => {
|
||||
dispatch({
|
||||
type: SWITCH_THEME,
|
||||
payload: val,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import {
|
||||
amoledDarkTheme,
|
||||
lightTheme,
|
||||
darkTheme,
|
||||
midnightDuskTheme,
|
||||
} from "../../theme/theme";
|
||||
|
||||
const themes = {
|
||||
0: amoledDarkTheme,
|
||||
1: lightTheme,
|
||||
2: darkTheme,
|
||||
3: midnightDuskTheme,
|
||||
};
|
||||
|
||||
const initialState = {
|
||||
theme: themes[0],
|
||||
};
|
||||
|
||||
const themeReducer = (state = initialState, action) => {
|
||||
const { type, payload } = action;
|
||||
|
||||
switch (type) {
|
||||
case "SWITCH_THEME":
|
||||
return { ...state, theme: themes[payload] };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default themeReducer;
|
||||
@@ -0,0 +1 @@
|
||||
export const SWITCH_THEME = "SWITCH_THEME";
|
||||
@@ -0,0 +1,9 @@
|
||||
import { GET_UPDATES } from "./updates.types";
|
||||
|
||||
import { getUpdates } from "../../database/queries/UpdateQueries";
|
||||
|
||||
export const getUpdatesAction = () => async (dispatch) => {
|
||||
const updates = await getUpdates();
|
||||
|
||||
dispatch({ type: GET_UPDATES, payload: updates });
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import { LOAD_UPDATES, GET_UPDATES } from "./updates.types";
|
||||
|
||||
const initialState = {
|
||||
updates: [],
|
||||
loading: true,
|
||||
};
|
||||
|
||||
const updateReducer = (state = initialState, action) => {
|
||||
const { type, payload } = action;
|
||||
|
||||
switch (type) {
|
||||
case LOAD_UPDATES:
|
||||
return { ...state, loading: true };
|
||||
case GET_UPDATES:
|
||||
return { ...state, updates: payload, loading: false };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default updateReducer;
|
||||
@@ -0,0 +1,2 @@
|
||||
export const GET_UPDATES = "GET_UPDATES";
|
||||
export const LOAD_UPDATES = "LOAD_UPDATES";
|
||||
@@ -1,144 +0,0 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import {
|
||||
StyleSheet,
|
||||
View,
|
||||
Text,
|
||||
FlatList,
|
||||
Image,
|
||||
ActivityIndicator,
|
||||
} from "react-native";
|
||||
import { TouchableRipple, Button } from "react-native-paper";
|
||||
import { CustomAppbar } from "../components/common/Appbar";
|
||||
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
const Browse = ({ navigation }) => {
|
||||
const theme = useSelector((state) => state.themeReducer.theme);
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [extensions, setExtensions] = useState();
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`https://lnreader-extensions.herokuapp.com/api/`)
|
||||
.then((response) => response.json())
|
||||
.then((json) => {
|
||||
setExtensions(json);
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CustomAppbar title="Browse" />
|
||||
<View
|
||||
style={[
|
||||
styles.container,
|
||||
{ backgroundColor: theme.colorDarkPrimaryDark },
|
||||
]}
|
||||
>
|
||||
{!loading ? (
|
||||
<FlatList
|
||||
data={extensions}
|
||||
keyExtractor={(item) => item.sourceId.toString()}
|
||||
renderItem={({ item }) => (
|
||||
<TouchableRipple
|
||||
style={styles.sourceCard}
|
||||
onPress={() =>
|
||||
navigation.navigate(
|
||||
item.sourceName + "Stack"
|
||||
)
|
||||
}
|
||||
rippleColor={theme.rippleColorDark}
|
||||
>
|
||||
<>
|
||||
<Image
|
||||
source={{
|
||||
uri: item.sourceCover,
|
||||
}}
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 6,
|
||||
}}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
marginLeft: 15,
|
||||
flex: 1,
|
||||
justifyContent: "space-between",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<View>
|
||||
<Text
|
||||
style={{
|
||||
color:
|
||||
theme.textColorPrimaryDark,
|
||||
fontSize: 14,
|
||||
}}
|
||||
>
|
||||
{item.sourceName}
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
color:
|
||||
theme.textColorSecondaryDark,
|
||||
fontSize: 12,
|
||||
}}
|
||||
>
|
||||
{item.sourceLanguage}
|
||||
</Text>
|
||||
</View>
|
||||
<View>
|
||||
<Button
|
||||
labelStyle={{
|
||||
letterSpacing: 0,
|
||||
}}
|
||||
uppercase={false}
|
||||
color={theme.colorAccentDark}
|
||||
onPress={() =>
|
||||
navigation.navigate(
|
||||
item.sourceName +
|
||||
"Stack"
|
||||
)
|
||||
}
|
||||
>
|
||||
Browse
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
</TouchableRipple>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<View style={{ padding: 20 }}>
|
||||
<ActivityIndicator
|
||||
size="small"
|
||||
color={theme.colorAccentDark}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Browse;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
sourceCard: {
|
||||
paddingVertical: 10,
|
||||
marginVertical: 5,
|
||||
paddingHorizontal: 20,
|
||||
borderRadius: 6,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { StyleSheet, View, FlatList, ActivityIndicator } from "react-native";
|
||||
import { Appbar } from "../../components/Appbar";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import { getSourcesAction } from "../../redux/source/source.actions";
|
||||
|
||||
import ExtensionCard from "./components/ExtensionCard";
|
||||
|
||||
const Browse = ({ theme, extensions, loading, getSourcesAction }) => {
|
||||
useEffect(() => {
|
||||
getSourcesAction();
|
||||
}, [getSourcesAction]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Appbar title="Browse" />
|
||||
<View
|
||||
style={[
|
||||
styles.container,
|
||||
{ backgroundColor: theme.colorPrimaryDark },
|
||||
]}
|
||||
>
|
||||
<FlatList
|
||||
data={extensions}
|
||||
keyExtractor={(item) => item.sourceId.toString()}
|
||||
renderItem={({ item }) => <ExtensionCard item={item} />}
|
||||
ListEmptyComponent={
|
||||
loading && (
|
||||
<ActivityIndicator
|
||||
size="small"
|
||||
color={theme.colorAccentDark}
|
||||
style={{ marginTop: 16 }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
theme: state.themeReducer.theme,
|
||||
extensions: state.extensionReducer.extensions,
|
||||
loading: state.extensionReducer.loading,
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, { getSourcesAction })(Browse);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,110 @@
|
||||
import React from "react";
|
||||
import { View, Text, Image, StyleSheet } from "react-native";
|
||||
import { TouchableRipple, Button } from "react-native-paper";
|
||||
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
const ExtensionCard = ({ item }) => {
|
||||
const { sourceName, sourceCover, sourceLanguage, status } = item;
|
||||
|
||||
const navigation = useNavigation();
|
||||
|
||||
const theme = useSelector((state) => state.themeReducer.theme);
|
||||
|
||||
return (
|
||||
<TouchableRipple
|
||||
style={styles.extensionCard}
|
||||
onPress={() =>
|
||||
navigation.navigate("Extension", {
|
||||
sourceId: item.sourceId,
|
||||
sourceName: item.sourceName,
|
||||
sourceUrl: item.sourceUrl,
|
||||
})
|
||||
}
|
||||
rippleColor={theme.rippleColor}
|
||||
borderless
|
||||
>
|
||||
<>
|
||||
<Image
|
||||
source={{ uri: sourceCover }}
|
||||
style={styles.extensionIcon}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
<View style={styles.extensionDetails}>
|
||||
<View>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorPrimary,
|
||||
fontSize: 14,
|
||||
}}
|
||||
>
|
||||
{sourceName}
|
||||
</Text>
|
||||
<View style={{ flexDirection: "row" }}>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorSecondary,
|
||||
fontSize: 12,
|
||||
}}
|
||||
>
|
||||
{sourceLanguage}
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
color: "#D63B2F",
|
||||
fontSize: 12,
|
||||
marginLeft: 5,
|
||||
}}
|
||||
>
|
||||
{status === 0 && "(Not Working)"}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View>
|
||||
<Button
|
||||
labelStyle={{
|
||||
letterSpacing: 0,
|
||||
}}
|
||||
uppercase={false}
|
||||
color={theme.colorAccentDark}
|
||||
onPress={() =>
|
||||
navigation.navigate("Extension", {
|
||||
sourceId: item.sourceId,
|
||||
sourceName: item.sourceName,
|
||||
sourceUrl: item.sourceUrl,
|
||||
})
|
||||
}
|
||||
>
|
||||
Browse
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
</TouchableRipple>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExtensionCard;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
extensionCard: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
marginVertical: 4,
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 20,
|
||||
},
|
||||
extensionIcon: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 8,
|
||||
},
|
||||
extensionDetails: {
|
||||
flex: 1,
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginLeft: 16,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,212 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { StyleSheet, View, Text, ActivityIndicator } from "react-native";
|
||||
|
||||
import { Appbar, Provider, Portal } from "react-native-paper";
|
||||
import { CollapsibleHeaderScrollView } from "react-native-collapsible-header-views";
|
||||
|
||||
import BottomSheet from "./components/BottomSheet";
|
||||
|
||||
import { insertHistoryAction } from "../../redux/history/history.actions";
|
||||
import {
|
||||
getChapterAction,
|
||||
markChapterReadAction,
|
||||
} from "../../redux/novel/novel.actions";
|
||||
|
||||
import { connect } from "react-redux";
|
||||
|
||||
const ChapterItem = ({
|
||||
route,
|
||||
navigation,
|
||||
theme,
|
||||
insertHistoryAction,
|
||||
markChapterReadAction,
|
||||
getChapterAction,
|
||||
chapter,
|
||||
loading,
|
||||
reader,
|
||||
}) => {
|
||||
const {
|
||||
chapterId,
|
||||
extensionId,
|
||||
chapterUrl,
|
||||
novelUrl,
|
||||
novelId,
|
||||
} = route.params;
|
||||
|
||||
let _panel = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
getChapterAction(extensionId, chapterUrl, novelUrl, chapterId);
|
||||
insertHistoryAction(novelId, chapterId);
|
||||
}, []);
|
||||
|
||||
const readerBackground = (val) => {
|
||||
const backgroundColor = {
|
||||
1: "#000000",
|
||||
2: "#FFFFFF",
|
||||
3: "#F4ECD8",
|
||||
};
|
||||
|
||||
return backgroundColor[val];
|
||||
};
|
||||
|
||||
const readerTextColor = (val) => {
|
||||
const textColor = val === 1 ? "rgba(255,255,255,0.7)" : "#000000";
|
||||
|
||||
return textColor;
|
||||
};
|
||||
|
||||
const readerLineHeight = (val) => {
|
||||
const lineHeight = {
|
||||
12: 20,
|
||||
14: 22,
|
||||
16: 25,
|
||||
18: 26,
|
||||
20: 28,
|
||||
};
|
||||
return lineHeight[val];
|
||||
};
|
||||
|
||||
const isCloseToBottom = ({
|
||||
layoutMeasurement,
|
||||
contentOffset,
|
||||
contentSize,
|
||||
}) => {
|
||||
const paddingToBottom = 40;
|
||||
return (
|
||||
layoutMeasurement.height + contentOffset.y >=
|
||||
contentSize.height - paddingToBottom
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Provider>
|
||||
<CollapsibleHeaderScrollView
|
||||
headerContainerBackgroundColor="rgba(0,0,0,0.4)"
|
||||
CollapsibleHeaderComponent={
|
||||
<Appbar.Header
|
||||
style={{
|
||||
backgroundColor: "transparent",
|
||||
elevation: 0,
|
||||
}}
|
||||
>
|
||||
<Appbar.BackAction
|
||||
onPress={() => navigation.goBack()}
|
||||
color="#FFFFFF"
|
||||
size={26}
|
||||
style={{ marginRight: 0 }}
|
||||
/>
|
||||
<Appbar.Content
|
||||
title={loading ? "Chapter" : chapter.chapterName}
|
||||
titleStyle={{ color: "#FFFFFF" }}
|
||||
/>
|
||||
{!loading && (
|
||||
<>
|
||||
<Appbar.Action
|
||||
icon="chevron-left"
|
||||
size={26}
|
||||
disabled={!chapter.prevChapter}
|
||||
onPress={() => {
|
||||
navigation.replace("ChapterItem", {
|
||||
chapterUrl: chapter.prevChapter,
|
||||
chapterId: chapterId - 1,
|
||||
extensionId,
|
||||
novelUrl,
|
||||
novelId,
|
||||
chapterName: chapter.chapterName,
|
||||
});
|
||||
}}
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
<Appbar.Action
|
||||
icon="chevron-right"
|
||||
size={26}
|
||||
disabled={!chapter.nextChapter}
|
||||
onPress={() => {
|
||||
navigation.replace("ChapterItem", {
|
||||
chapterUrl: chapter.nextChapter,
|
||||
extensionId,
|
||||
novelUrl,
|
||||
novelId,
|
||||
chapterId: chapterId + 1,
|
||||
chapterName: chapter.chapterName,
|
||||
});
|
||||
}}
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
<Appbar.Action
|
||||
icon="dots-vertical"
|
||||
size={26}
|
||||
onPress={() =>
|
||||
_panel.current.show({ velocity: -1.5 })
|
||||
}
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Appbar.Header>
|
||||
}
|
||||
headerHeight={100}
|
||||
contentContainerStyle={[
|
||||
styles.container,
|
||||
{ backgroundColor: readerBackground(reader.theme) },
|
||||
]}
|
||||
onScroll={({ nativeEvent }) => {
|
||||
if (isCloseToBottom(nativeEvent)) {
|
||||
markChapterReadAction(chapterId);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{loading ? (
|
||||
<View style={{ flex: 1, justifyContent: "center" }}>
|
||||
<ActivityIndicator
|
||||
size={50}
|
||||
color={theme.colorAccentDark}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<Text
|
||||
style={[
|
||||
{
|
||||
paddingVertical: 15,
|
||||
fontSize: reader.textSize,
|
||||
color: readerTextColor(reader.theme),
|
||||
lineHeight: readerLineHeight(reader.textSize),
|
||||
textAlign: reader.textAlign,
|
||||
paddingHorizontal: `${reader.padding}%`,
|
||||
},
|
||||
reader.fontFamily && {
|
||||
fontFamily: reader.fontFamily,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{chapter.chapterText.trim()}
|
||||
</Text>
|
||||
)}
|
||||
<Portal>
|
||||
<BottomSheet bottomSheetRef={_panel} />
|
||||
</Portal>
|
||||
</CollapsibleHeaderScrollView>
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
theme: state.themeReducer.theme,
|
||||
chapter: state.novelReducer.chapter,
|
||||
loading: state.novelReducer.chapterLoading,
|
||||
reader: state.settingsReducer.reader,
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, {
|
||||
insertHistoryAction,
|
||||
markChapterReadAction,
|
||||
getChapterAction,
|
||||
})(ChapterItem);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexGrow: 1,
|
||||
paddingVertical: 10,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,276 @@
|
||||
import React from "react";
|
||||
import { Animated, StyleSheet, View, Text, Dimensions } from "react-native";
|
||||
import { ToggleButton, IconButton, Chip } from "react-native-paper";
|
||||
import Slider from "@react-native-community/slider";
|
||||
import Bottomsheet from "rn-sliding-up-panel";
|
||||
import { fonts } from "../../../utils/constants";
|
||||
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import {
|
||||
setReaderFont,
|
||||
updateReaderPadding,
|
||||
updateReaderTextAlign,
|
||||
updateReaderTextSize,
|
||||
updateReaderTheme,
|
||||
} from "../../../redux/settings/settings.actions";
|
||||
import BottomSheetHandle from "../../../components/BottomSheetHandle";
|
||||
|
||||
const ChapterBottomSheet = ({
|
||||
bottomSheetRef,
|
||||
theme,
|
||||
reader,
|
||||
updateReaderTextSize,
|
||||
updateReaderTheme,
|
||||
updateReaderTextAlign,
|
||||
updateReaderPadding,
|
||||
setReaderFont,
|
||||
}) => {
|
||||
return (
|
||||
<Bottomsheet
|
||||
animatedValue={new Animated.Value(0)}
|
||||
ref={bottomSheetRef}
|
||||
draggableRange={{ top: 400, bottom: 0 }}
|
||||
snappingPoints={[0, 400]}
|
||||
showBackdrop={false}
|
||||
>
|
||||
<View style={styles.contentContainer}>
|
||||
<BottomSheetHandle />
|
||||
<View style={styles.readerSettingsContainer}>
|
||||
<Text
|
||||
style={{
|
||||
color: "#FFFFFF",
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
Text Size
|
||||
</Text>
|
||||
<Slider
|
||||
style={{
|
||||
width: Dimensions.get("window").width,
|
||||
height: 40,
|
||||
}}
|
||||
value={reader.textSize}
|
||||
minimumValue={12}
|
||||
maximumValue={20}
|
||||
step={2}
|
||||
minimumTrackTintColor={theme.colorAccentDark}
|
||||
maximumTrackTintColor="#000000"
|
||||
thumbTintColor={theme.colorAccentDark}
|
||||
onValueChange={(value) => updateReaderTextSize(value)}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: "#FFFFFF",
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
Reader Theme
|
||||
</Text>
|
||||
<ToggleButton.Row
|
||||
onValueChange={(value) =>
|
||||
updateReaderTheme(value)
|
||||
}
|
||||
value={reader.theme}
|
||||
style={{ marginTop: 10 }}
|
||||
>
|
||||
<ToggleButton
|
||||
icon="format-text"
|
||||
color="#FFFFFF"
|
||||
value={1}
|
||||
style={{
|
||||
backgroundColor: "#000000",
|
||||
marginHorizontal: 10,
|
||||
}}
|
||||
/>
|
||||
<ToggleButton
|
||||
icon="format-text"
|
||||
color="#000000"
|
||||
value={2}
|
||||
style={{
|
||||
backgroundColor: "#FFFFFF",
|
||||
marginHorizontal: 10,
|
||||
}}
|
||||
/>
|
||||
<ToggleButton
|
||||
icon="format-text"
|
||||
color="#000000"
|
||||
value={3}
|
||||
style={{
|
||||
backgroundColor: "#F4ECD8",
|
||||
marginHorizontal: 10,
|
||||
}}
|
||||
/>
|
||||
</ToggleButton.Row>
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: "#FFFFFF",
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
Text Align
|
||||
</Text>
|
||||
<ToggleButton.Row
|
||||
onValueChange={(value) =>
|
||||
updateReaderTextAlign(value ?? "left")
|
||||
}
|
||||
value={reader.textAlign}
|
||||
style={{ marginTop: 10 }}
|
||||
>
|
||||
<ToggleButton
|
||||
icon="format-align-left"
|
||||
color="#000000"
|
||||
value="left"
|
||||
style={{
|
||||
backgroundColor: "#FFFFFF",
|
||||
marginHorizontal: 10,
|
||||
}}
|
||||
/>
|
||||
<ToggleButton
|
||||
icon="format-align-justify"
|
||||
color="#000000"
|
||||
value="justify"
|
||||
style={{
|
||||
backgroundColor: "#FFFFFF",
|
||||
marginHorizontal: 10,
|
||||
}}
|
||||
/>
|
||||
<ToggleButton
|
||||
icon="format-align-right"
|
||||
color="#000000"
|
||||
value="right"
|
||||
style={{
|
||||
backgroundColor: "#FFFFFF",
|
||||
marginHorizontal: 10,
|
||||
}}
|
||||
/>
|
||||
</ToggleButton.Row>
|
||||
</View>
|
||||
</View>
|
||||
<Text
|
||||
style={{
|
||||
color: "#FFFFFF",
|
||||
fontWeight: "bold",
|
||||
marginTop: 10,
|
||||
}}
|
||||
>
|
||||
Padding
|
||||
</Text>
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
flexDirection: "row",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
icon="minus"
|
||||
color={theme.colorAccentDark}
|
||||
size={26}
|
||||
disabled={reader.padding <= 0 ? true : false}
|
||||
onPress={() =>
|
||||
updateReaderPadding(reader.padding - 1)
|
||||
}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
color: "#FFFFFF",
|
||||
paddingHorizontal: 50,
|
||||
}}
|
||||
>
|
||||
{`${reader.padding}%`}
|
||||
</Text>
|
||||
<IconButton
|
||||
icon="plus"
|
||||
color={theme.colorAccentDark}
|
||||
size={26}
|
||||
disabled={reader.padding >= 10 ? true : false}
|
||||
onPress={() =>
|
||||
updateReaderPadding(reader.padding + 1)
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
<Text
|
||||
style={{
|
||||
color: "#FFFFFF",
|
||||
fontWeight: "bold",
|
||||
marginTop: 10,
|
||||
}}
|
||||
>
|
||||
Font Style
|
||||
</Text>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
{fonts.map((font, index) => (
|
||||
<Chip
|
||||
key={index}
|
||||
onPress={() => setReaderFont(font.fontFamily)}
|
||||
textStyle={{ fontFamily: font.fontFamily }}
|
||||
style={{
|
||||
borderWidth: 0,
|
||||
marginHorizontal: 5,
|
||||
marginVertical: 5,
|
||||
}}
|
||||
selected={
|
||||
reader.fontFamily === font.fontFamily
|
||||
? true
|
||||
: false
|
||||
}
|
||||
>
|
||||
{font.name}
|
||||
</Chip>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Bottomsheet>
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
theme: state.themeReducer.theme,
|
||||
reader: state.settingsReducer.reader,
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, {
|
||||
updateReaderTextSize,
|
||||
updateReaderTheme,
|
||||
updateReaderTextAlign,
|
||||
updateReaderPadding,
|
||||
setReaderFont,
|
||||
})(ChapterBottomSheet);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
contentContainer: {
|
||||
flex: 1,
|
||||
borderTopLeftRadius: 8,
|
||||
borderTopRightRadius: 8,
|
||||
backgroundColor: "rgba(0,0,0,0.4)",
|
||||
},
|
||||
readerSettingsContainer: { flex: 1, alignItems: "center", paddingTop: 30 },
|
||||
});
|
||||
@@ -1,315 +0,0 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { StyleSheet, View, Text, ActivityIndicator } from "react-native";
|
||||
|
||||
import { Appbar, Provider, Portal } from "react-native-paper";
|
||||
import { theme } from "../theme/theme";
|
||||
import { CollapsibleHeaderScrollView } from "react-native-collapsible-header-views";
|
||||
|
||||
import BottomSheet from "../components/chapter/BottomSheet";
|
||||
|
||||
import * as SQLite from "expo-sqlite";
|
||||
const db = SQLite.openDatabase("lnreader.db");
|
||||
|
||||
import { fetchChapterFromSource } from "../services/api";
|
||||
|
||||
import { getReaderTheme } from "../services/asyncStorage";
|
||||
|
||||
const ChapterItem = ({ route, navigation }) => {
|
||||
const { extensionId, chapterUrl, novelUrl, chapterName } = route.params;
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const [chapter, setChapter] = useState();
|
||||
|
||||
const [size, setSize] = useState(16);
|
||||
|
||||
const [readerTheme, setReaderTheme] = useState(1);
|
||||
|
||||
getReaderTheme().then((value) => setReaderTheme(value));
|
||||
|
||||
const setHistory = () => {
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
"INSERT INTO HistoryTable (chapterUrl, novelUrl, chapterName, lastRead) VALUES ( ?, ?, ?, (datetime('now','localtime')))",
|
||||
[chapterUrl, novelUrl, chapterName],
|
||||
(tx, res) =>
|
||||
console.log(
|
||||
"Inserted into history table: " + novelUrl + chapterUrl
|
||||
),
|
||||
(tx, error) => console.log(error)
|
||||
);
|
||||
tx.executeSql(
|
||||
"UPDATE LibraryTable SET lastRead = ?, lastReadName = ?, unread = 0 WHERE novelUrl = ?",
|
||||
[chapterUrl, chapterName, novelUrl],
|
||||
(tx, res) =>
|
||||
console.log("Set Last Read " + novelUrl + chapterUrl),
|
||||
(tx, error) => console.log(error)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const updateHistory = () => {
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
"UPDATE HistoryTable SET lastRead = (datetime('now','localtime')), chapterName=?, chapterUrl=? WHERE novelUrl = ?",
|
||||
[chapterName, chapterUrl, novelUrl],
|
||||
(tx, res) =>
|
||||
console.log("Updated into history table: " + novelUrl),
|
||||
(tx, error) => console.log(error)
|
||||
);
|
||||
tx.executeSql(
|
||||
"UPDATE LibraryTable SET lastRead = ?, lastReadName = ?, unread = 0 WHERE novelUrl = ?",
|
||||
[chapterUrl, chapterName, novelUrl],
|
||||
(tx, res) =>
|
||||
console.log("Set Last read" + novelUrl + chapterUrl),
|
||||
(tx, error) => console.log(error)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const setRead = () => {
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
"UPDATE ChapterTable SET `read` = 1 WHERE chapterUrl = ?",
|
||||
[chapterUrl],
|
||||
(tx, res) => console.log("Updated readStatus: " + novelUrl),
|
||||
(tx, error) => console.log(error)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const checkIfInHistory = () => {
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
"SELECT * FROM HistoryTable WHERE novelUrl=?",
|
||||
[novelUrl],
|
||||
(txObj, res) => {
|
||||
if (res.rows.length === 0) {
|
||||
console.log("Not In History");
|
||||
setHistory();
|
||||
} else {
|
||||
console.log("In History");
|
||||
updateHistory();
|
||||
}
|
||||
},
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
// const updateLastRead = () => {
|
||||
// db.transaction((tx) => {
|
||||
|
||||
// tx.executeSql(
|
||||
// "UPDATE LibraryTable SET lastRead = ?, lastReadName = ?, unread = 0 WHERE novelUrl = ?",
|
||||
// [chapter.nextChapter, chapterName, novelUrl],
|
||||
// (tx, res) =>
|
||||
// console.log("Set Last Read " + novelUrl + chapterUrl),
|
||||
// (tx, error) => console.log(error)
|
||||
// );
|
||||
// });
|
||||
// };
|
||||
|
||||
const checkIfDownloaded = () => {
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
"SELECT * FROM DownloadsTable WHERE chapterUrl=? AND novelUrl=?",
|
||||
[chapterUrl, novelUrl],
|
||||
(txObj, res) => {
|
||||
if (res.rows.length === 0) {
|
||||
// console.log("Not Downloaded");
|
||||
fetchChapterFromSource(
|
||||
extensionId,
|
||||
novelUrl,
|
||||
chapterUrl
|
||||
).then((res) => {
|
||||
setChapter(res);
|
||||
setLoading(false);
|
||||
});
|
||||
} else {
|
||||
// console.log("Already Downloaded");
|
||||
getChapterFromDB();
|
||||
}
|
||||
checkIfInHistory();
|
||||
},
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const getChapterFromDB = () => {
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
`SELECT * FROM DownloadsTable WHERE chapterUrl = ?`,
|
||||
[chapterUrl],
|
||||
(tx, results) => {
|
||||
setChapter(results.rows.item(0));
|
||||
setLoading(false);
|
||||
},
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
checkIfDownloaded();
|
||||
}, []);
|
||||
|
||||
const isCloseToBottom = ({
|
||||
layoutMeasurement,
|
||||
contentOffset,
|
||||
contentSize,
|
||||
}) => {
|
||||
const paddingToBottom = 0;
|
||||
return (
|
||||
layoutMeasurement.height + contentOffset.y >=
|
||||
contentSize.height - paddingToBottom
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Provider>
|
||||
<CollapsibleHeaderScrollView
|
||||
headerContainerBackgroundColor={"rgba(0,0,0,0.4)"}
|
||||
CollapsibleHeaderComponent={
|
||||
<Appbar.Header
|
||||
style={{
|
||||
backgroundColor: "transparent",
|
||||
elevation: 0,
|
||||
}}
|
||||
>
|
||||
<Appbar.BackAction
|
||||
onPress={() => {
|
||||
// navigation.navigate("NovelItem", {
|
||||
// novelUrl,
|
||||
// extensionId,
|
||||
// navigatingFrom: 0,
|
||||
// });
|
||||
navigation.goBack();
|
||||
}}
|
||||
color={"white"}
|
||||
size={26}
|
||||
style={{ marginRight: 0 }}
|
||||
/>
|
||||
<Appbar.Content
|
||||
title={loading ? "Chapter" : chapter.chapterName}
|
||||
titleStyle={{ color: theme.textColorPrimaryDark }}
|
||||
/>
|
||||
{!loading && (
|
||||
<>
|
||||
<Appbar.Action
|
||||
icon="chevron-left"
|
||||
size={26}
|
||||
disabled={!chapter.prevChapter}
|
||||
onPress={() => {
|
||||
navigation.push("ChapterItem", {
|
||||
chapterUrl: chapter.prevChapter,
|
||||
extensionId,
|
||||
novelUrl,
|
||||
chapterName: chapter.chapterName,
|
||||
});
|
||||
}}
|
||||
color={"white"}
|
||||
/>
|
||||
<Appbar.Action
|
||||
icon="chevron-right"
|
||||
size={26}
|
||||
disabled={!chapter.nextChapter}
|
||||
onPress={() => {
|
||||
navigation.push("ChapterItem", {
|
||||
chapterUrl: chapter.nextChapter,
|
||||
extensionId,
|
||||
novelUrl,
|
||||
chapterName: chapter.chapterName,
|
||||
});
|
||||
}}
|
||||
color={"white"}
|
||||
/>
|
||||
<Appbar.Action
|
||||
icon="dots-vertical"
|
||||
size={26}
|
||||
onPress={() =>
|
||||
_panel.show({ velocity: -1.5 })
|
||||
}
|
||||
color={"white"}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Appbar.Header>
|
||||
}
|
||||
headerHeight={100}
|
||||
contentContainerStyle={[
|
||||
styles.container,
|
||||
readerTheme === 1 && {
|
||||
backgroundColor: theme.colorDarkPrimary,
|
||||
},
|
||||
readerTheme === 2 && {
|
||||
backgroundColor: "white",
|
||||
},
|
||||
readerTheme === 3 && {
|
||||
backgroundColor: "#F4ECD8",
|
||||
},
|
||||
]}
|
||||
onScroll={({ nativeEvent }) => {
|
||||
// _panel.hide();
|
||||
if (isCloseToBottom(nativeEvent)) {
|
||||
// console.log("Scroll End Reached");
|
||||
setRead();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* <Button mode="contained" onPress={() => setHistory()}>
|
||||
Set History
|
||||
</Button> */}
|
||||
{loading ? (
|
||||
<View style={{ flex: 1, justifyContent: "center" }}>
|
||||
<ActivityIndicator
|
||||
size={50}
|
||||
color={theme.colorAccentDark}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
<Text
|
||||
style={[
|
||||
{
|
||||
paddingVertical: 15,
|
||||
fontSize: size,
|
||||
},
|
||||
readerTheme === 1
|
||||
? { color: theme.textColorSecondaryDark }
|
||||
: { color: "black" },
|
||||
size === 16
|
||||
? { lineHeight: 25 }
|
||||
: size === 20
|
||||
? { lineHeight: 28 }
|
||||
: size === 12 && { lineHeight: 20 },
|
||||
]}
|
||||
>
|
||||
{chapter.chapterText.trim()}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
<Portal>
|
||||
<BottomSheet
|
||||
bottomSheetRef={(c) => (_panel = c)}
|
||||
setSize={setSize}
|
||||
size={size}
|
||||
setReaderTheme={setReaderTheme}
|
||||
readerTheme={readerTheme}
|
||||
/>
|
||||
</Portal>
|
||||
</CollapsibleHeaderScrollView>
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChapterItem;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexGrow: 1,
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 10,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,142 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { StyleSheet, View, FlatList, ActivityIndicator } from "react-native";
|
||||
import { Provider } from "react-native-paper";
|
||||
import * as WebBrowser from "expo-web-browser";
|
||||
|
||||
import NovelCover from "../../components/NovelCover";
|
||||
import EmptyView from "../../components/EmptyView";
|
||||
import { SearchAppbar } from "../../components/Appbar";
|
||||
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
const Extension = ({ navigation, route }) => {
|
||||
const { sourceId, sourceName, sourceUrl } = route.params;
|
||||
|
||||
const theme = useSelector((state) => state.themeReducer.theme);
|
||||
const library = useSelector((state) => state.libraryReducer.novels);
|
||||
const itemsPerRow = useSelector(
|
||||
(state) => state.settingsReducer.itemsPerRow
|
||||
);
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [novels, setNovels] = useState();
|
||||
|
||||
const [searchText, setSearchText] = useState("");
|
||||
|
||||
const getSourceUrl = () => {
|
||||
if (sourceId === 1) {
|
||||
return `https://lnreader-extensions.herokuapp.com/api/1/novels/1/?o=rating`;
|
||||
} else {
|
||||
return `https://lnreader-extensions.herokuapp.com/api/${sourceId}/novels/`;
|
||||
}
|
||||
};
|
||||
|
||||
const getSearchUrl = () => {
|
||||
if (sourceId === 1) {
|
||||
return `https://lnreader-extensions.herokuapp.com/api/1/search/?s=${searchText}&?o=rating`;
|
||||
} else {
|
||||
return `https://lnreader-extensions.herokuapp.com/api/${sourceId}/search/?s=${searchText}`;
|
||||
}
|
||||
};
|
||||
|
||||
const getNovels = () => {
|
||||
fetch(getSourceUrl())
|
||||
.then((response) => response.json())
|
||||
.then((json) => {
|
||||
setNovels(json);
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const getSearchResults = (searchText) => {
|
||||
setLoading(true);
|
||||
fetch(getSearchUrl())
|
||||
.then((response) => response.json())
|
||||
.then((json) => {
|
||||
setNovels(json);
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const checkIFInLibrary = (id) => {
|
||||
return library.some((obj) => obj.novelUrl === id);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getNovels();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Provider>
|
||||
<View
|
||||
style={[
|
||||
styles.container,
|
||||
{ backgroundColor: theme.colorPrimaryDark },
|
||||
]}
|
||||
>
|
||||
<SearchAppbar
|
||||
screen="Extension"
|
||||
placeholder={`Search ${sourceName}`}
|
||||
searchText={searchText}
|
||||
setSearchText={setSearchText}
|
||||
getSearchResults={getSearchResults}
|
||||
getNovels={getNovels}
|
||||
setLoading={setLoading}
|
||||
openWebView={() => WebBrowser.openBrowserAsync(sourceUrl)}
|
||||
/>
|
||||
|
||||
{loading ? (
|
||||
<View style={{ flex: 1, justifyContent: "center" }}>
|
||||
<ActivityIndicator
|
||||
size={60}
|
||||
color={theme.colorAccentDark}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<FlatList
|
||||
contentContainerStyle={{ flexGrow: 1 }}
|
||||
numColumns={itemsPerRow}
|
||||
key={itemsPerRow}
|
||||
data={novels}
|
||||
showsVerticalScrollIndicator={false}
|
||||
keyExtractor={(item) => item.novelUrl}
|
||||
renderItem={({ item }) => (
|
||||
<NovelCover
|
||||
item={item}
|
||||
onPress={() =>
|
||||
navigation.navigate("NovelItem", {
|
||||
novelName: item.novelName,
|
||||
novelCover: item.novelCover,
|
||||
novelUrl: item.novelUrl,
|
||||
sourceId: item.extensionId,
|
||||
})
|
||||
}
|
||||
libraryStatus={checkIFInLibrary(item.novelUrl)}
|
||||
/>
|
||||
)}
|
||||
ListEmptyComponent={
|
||||
novels.length === 0 && (
|
||||
<EmptyView
|
||||
icon="(; ̄Д ̄)"
|
||||
description="No results found"
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default Extension;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
padding: 4,
|
||||
},
|
||||
contentContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
@@ -1,63 +1,55 @@
|
||||
import React, { useState, useCallback } from "react";
|
||||
import { useFocusEffect } from "@react-navigation/native";
|
||||
|
||||
import { StyleSheet, View, FlatList, ActivityIndicator } from "react-native";
|
||||
|
||||
import { Appbar } from "../../components/Appbar";
|
||||
import HistoryCard from "./components/HistoryCard";
|
||||
|
||||
import { connect } from "react-redux";
|
||||
import EmptyView from "../../components/EmptyView";
|
||||
|
||||
import {
|
||||
StyleSheet,
|
||||
View,
|
||||
Text,
|
||||
FlatList,
|
||||
ActivityIndicator,
|
||||
} from "react-native";
|
||||
|
||||
import { CustomAppbar } from "../../components/common/Appbar";
|
||||
import HistoryCard from "../../components/history/HistoryCard";
|
||||
|
||||
import { getHistoryFromDb, deleteChapterHistory } from "../../services/db";
|
||||
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
const History = ({ navigation }) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [history, setHistory] = useState();
|
||||
|
||||
const theme = useSelector((state) => state.themeReducer.theme);
|
||||
|
||||
const getHistory = async () => {
|
||||
await getHistoryFromDb().then((res) => setHistory(res));
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const deleteHistory = (novelId) => {
|
||||
deleteChapterHistory(novelId);
|
||||
getHistory();
|
||||
};
|
||||
getHistoryAction,
|
||||
deleteHistoryAction,
|
||||
} from "../../redux/history/history.actions";
|
||||
|
||||
const History = ({
|
||||
navigation,
|
||||
theme,
|
||||
history,
|
||||
loading,
|
||||
getHistoryAction,
|
||||
deleteHistoryAction,
|
||||
}) => {
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
getHistory();
|
||||
}, [])
|
||||
getHistoryAction();
|
||||
}, [getHistoryAction])
|
||||
);
|
||||
|
||||
const renderHistoryCard = ({ item }) => (
|
||||
<HistoryCard
|
||||
item={item}
|
||||
deleteHistoryAction={deleteHistoryAction}
|
||||
navigation={navigation}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CustomAppbar title="History" />
|
||||
<Appbar title="History" />
|
||||
<View
|
||||
style={[
|
||||
styles.container,
|
||||
{ backgroundColor: theme.colorDarkPrimaryDark },
|
||||
{ backgroundColor: theme.colorPrimaryDark },
|
||||
]}
|
||||
>
|
||||
<FlatList
|
||||
contentContainerStyle={{ flex: 1 }}
|
||||
contentContainerStyle={{ flexGrow: 1 }}
|
||||
data={history}
|
||||
keyExtractor={(item) => item.historyId.toString()}
|
||||
renderItem={({ item }) => (
|
||||
<HistoryCard
|
||||
item={item}
|
||||
deleteHistory={deleteHistory}
|
||||
navigation={navigation}
|
||||
/>
|
||||
)}
|
||||
keyExtractor={(item) => item.novelUrl.toString()}
|
||||
renderItem={renderHistoryCard}
|
||||
ListFooterComponent={
|
||||
loading && (
|
||||
<ActivityIndicator
|
||||
@@ -68,34 +60,10 @@ const History = ({ navigation }) => {
|
||||
}
|
||||
ListEmptyComponent={
|
||||
!loading && (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorSecondaryDark,
|
||||
fontSize: 45,
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
(˘・_・˘)
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorSecondaryDark,
|
||||
fontWeight: "bold",
|
||||
marginTop: 10,
|
||||
textAlign: "center",
|
||||
paddingHorizontal: 30,
|
||||
}}
|
||||
>
|
||||
Nothing read recently.
|
||||
</Text>
|
||||
</View>
|
||||
<EmptyView
|
||||
icon="(˘・_・˘)"
|
||||
description="Nothing read recently"
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
@@ -104,17 +72,20 @@ const History = ({ navigation }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default History;
|
||||
const mapStateToProps = (state) => ({
|
||||
theme: state.themeReducer.theme,
|
||||
history: state.historyReducer.history,
|
||||
loading: state.historyReducer.loading,
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, {
|
||||
getHistoryAction,
|
||||
deleteHistoryAction,
|
||||
})(History);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
padding: 10,
|
||||
},
|
||||
historyCard: {
|
||||
marginTop: 10,
|
||||
borderRadius: 4,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
},
|
||||
});
|
||||
|
||||
+25
-33
@@ -7,16 +7,13 @@ import moment from "moment";
|
||||
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
const HistoryCard = ({ item, deleteHistory, navigation }) => {
|
||||
const HistoryCard = ({ item, deleteHistoryAction, navigation }) => {
|
||||
const theme = useSelector((state) => state.themeReducer.theme);
|
||||
|
||||
return (
|
||||
<TouchableRipple
|
||||
style={[
|
||||
styles.historyCard,
|
||||
// { backgroundColor: theme.colorDarkPrimary },
|
||||
]}
|
||||
rippleColor={theme.rippleColorDark}
|
||||
style={styles.historyCard}
|
||||
rippleColor={theme.rippleColor}
|
||||
borderless
|
||||
onPress={() =>
|
||||
navigation.navigate("NovelItem", {
|
||||
@@ -24,31 +21,19 @@ const HistoryCard = ({ item, deleteHistory, navigation }) => {
|
||||
novelCover: item.novelCover,
|
||||
extensionId: item.extensionId,
|
||||
novelUrl: item.novelUrl,
|
||||
navigatingFrom: 0,
|
||||
})
|
||||
}
|
||||
>
|
||||
<>
|
||||
<Image
|
||||
source={{ uri: item.novelCover }}
|
||||
style={{
|
||||
height: 80,
|
||||
width: 57,
|
||||
borderRadius: 4,
|
||||
}}
|
||||
style={styles.historyCover}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
marginLeft: 15,
|
||||
flex: 1,
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<View style={styles.historyText}>
|
||||
<View style={{ flex: 0.9 }}>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorPrimaryDark,
|
||||
color: theme.textColorPrimary,
|
||||
fontWeight: "bold",
|
||||
fontSize: 16,
|
||||
}}
|
||||
@@ -58,13 +43,13 @@ const HistoryCard = ({ item, deleteHistory, navigation }) => {
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorSecondaryDark,
|
||||
color: theme.textColorSecondary,
|
||||
marginTop: 2,
|
||||
}}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{`${item.chapterName} - ${moment(
|
||||
item.lastRead
|
||||
item.historyTimeRead
|
||||
).calendar()}`}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -77,24 +62,24 @@ const HistoryCard = ({ item, deleteHistory, navigation }) => {
|
||||
<IconButton
|
||||
icon="delete-outline"
|
||||
size={22}
|
||||
color={theme.textColorPrimaryDark}
|
||||
color={theme.textColorPrimary}
|
||||
style={{
|
||||
marginRight: 0,
|
||||
}}
|
||||
onPress={() => deleteHistory(item.novelUrl)}
|
||||
onPress={() => deleteHistoryAction(item.novelId)}
|
||||
/>
|
||||
<IconButton
|
||||
icon="play"
|
||||
size={24}
|
||||
color={theme.textColorPrimaryDark}
|
||||
color={theme.textColorPrimary}
|
||||
style={{ marginRight: 10 }}
|
||||
onPress={() =>
|
||||
navigation.navigate("ChapterItem", {
|
||||
chapterId: item.historyChapterId,
|
||||
chapterUrl: item.chapterUrl,
|
||||
extensionId: item.extensionId,
|
||||
extensionId: item.sourceId,
|
||||
novelUrl: item.novelUrl,
|
||||
novelName: item.novelName,
|
||||
novelCover: item.novelCover,
|
||||
novelId: item.novelId,
|
||||
chapterName: item.chapterName,
|
||||
})
|
||||
}
|
||||
@@ -110,13 +95,20 @@ export default HistoryCard;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
historyCard: {
|
||||
// backgroundColor: theme.colorDarkPrimary,
|
||||
// paddingVertical: 10,
|
||||
// marginVertical: 5,
|
||||
// paddingHorizontal: 20,
|
||||
marginBottom: 15,
|
||||
borderRadius: 4,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
},
|
||||
historyCover: {
|
||||
height: 80,
|
||||
width: 57,
|
||||
borderRadius: 4,
|
||||
},
|
||||
historyText: {
|
||||
marginLeft: 15,
|
||||
flex: 1,
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
},
|
||||
});
|
||||
@@ -1,220 +0,0 @@
|
||||
import React, { useState, useCallback } from "react";
|
||||
import {
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
TextInput,
|
||||
RefreshControl,
|
||||
ActivityIndicator,
|
||||
ToastAndroid,
|
||||
} from "react-native";
|
||||
import { Appbar } from "react-native-paper";
|
||||
import { FlatList } from "react-native-gesture-handler";
|
||||
import { useFocusEffect } from "@react-navigation/native";
|
||||
|
||||
import NovelCover from "../components/common/NovelCover";
|
||||
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
import { getLibraryFromDb, searchInLibrary } from "../services/db";
|
||||
|
||||
import { getLibraryNovels } from "../redux/actions/library";
|
||||
|
||||
const LibraryScreen = ({ navigation }) => {
|
||||
const theme = useSelector((state) => state.themeReducer.theme);
|
||||
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [novels, setNovels] = useState();
|
||||
|
||||
const [searchBar, setSearchBar] = useState(false);
|
||||
const [searchText, setSearchText] = useState("");
|
||||
|
||||
const getLibrary = async () => {
|
||||
await getLibraryFromDb().then((res) => {
|
||||
setNovels(res);
|
||||
});
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
};
|
||||
|
||||
const searchNovel = async (searchText) => {
|
||||
await searchInLibrary(searchText).then((res) => setNovels(res));
|
||||
setRefreshing(false);
|
||||
};
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
setSearchBar(false);
|
||||
getLibrary();
|
||||
}, [])
|
||||
);
|
||||
|
||||
const onRefresh = () => {
|
||||
ToastAndroid.show("Updating library", ToastAndroid.SHORT);
|
||||
setRefreshing(true);
|
||||
getLibrary();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Appbar.Header style={{ backgroundColor: theme.colorDarkPrimary }}>
|
||||
{!searchBar ? (
|
||||
<>
|
||||
<Appbar.Content
|
||||
title="Library"
|
||||
titleStyle={{ color: theme.textColorPrimaryDark }}
|
||||
/>
|
||||
<Appbar.Action
|
||||
icon="magnify"
|
||||
onPress={() => setSearchBar(true)}
|
||||
color={theme.textColorPrimaryDark}
|
||||
/>
|
||||
<Appbar.Action
|
||||
color={theme.textColorPrimaryDark}
|
||||
icon="filter-variant"
|
||||
disabled
|
||||
// onPress={() => _panel.show({ velocity: -1.5 })}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Appbar.BackAction
|
||||
onPress={() => {
|
||||
setSearchBar(false);
|
||||
setSearchText("");
|
||||
getLibrary();
|
||||
}}
|
||||
color={theme.textColorPrimaryDark}
|
||||
size={24}
|
||||
/>
|
||||
<TextInput
|
||||
placeholder="Search..."
|
||||
defaultValue={searchText}
|
||||
style={{
|
||||
fontSize: 18,
|
||||
flex: 1,
|
||||
color: theme.textColorPrimaryDark,
|
||||
marginLeft: 15,
|
||||
}}
|
||||
autoFocus
|
||||
placeholderTextColor={theme.textColorHintDark}
|
||||
blurOnSubmit={true}
|
||||
onChangeText={(text) => {
|
||||
setSearchText(text);
|
||||
searchNovel(text);
|
||||
}}
|
||||
/>
|
||||
{searchText !== "" && (
|
||||
<Appbar.Action
|
||||
icon="close"
|
||||
onPress={() => {
|
||||
setSearchText("");
|
||||
// getLibrary();
|
||||
}}
|
||||
color={theme.textColorPrimaryDark}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Appbar.Header>
|
||||
<View
|
||||
style={[
|
||||
styles.container,
|
||||
{ backgroundColor: theme.colorDarkPrimaryDark },
|
||||
]}
|
||||
>
|
||||
<FlatList
|
||||
contentContainerStyle={{ flex: 1 }}
|
||||
numColumns={3}
|
||||
data={novels}
|
||||
showsVerticalScrollIndicator={false}
|
||||
keyExtractor={(item) => item.novelUrl}
|
||||
renderItem={({ item }) => (
|
||||
<NovelCover
|
||||
item={item}
|
||||
onPress={() =>
|
||||
navigation.navigate("NovelItem", {
|
||||
...item,
|
||||
navigatingFrom: 1,
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={onRefresh}
|
||||
colors={["white"]}
|
||||
progressBackgroundColor={theme.colorAccentDark}
|
||||
/>
|
||||
}
|
||||
ListEmptyComponent={
|
||||
!loading ? (
|
||||
!searchBar ? (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorSecondaryDark,
|
||||
fontSize: 45,
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
Σ(ಠ_ಠ)
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorSecondaryDark,
|
||||
fontWeight: "bold",
|
||||
marginTop: 10,
|
||||
textAlign: "center",
|
||||
paddingHorizontal: 30,
|
||||
}}
|
||||
>
|
||||
Your library is empty. Add series to
|
||||
your library from Browse.
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
searchBar &&
|
||||
searchText !== "" && (
|
||||
<Text
|
||||
style={{
|
||||
color: theme.colorAccentDark,
|
||||
fontWeight: "bold",
|
||||
marginTop: 10,
|
||||
textAlign: "center",
|
||||
paddingHorizontal: 30,
|
||||
}}
|
||||
>
|
||||
{`"${searchText}" not in library`}
|
||||
</Text>
|
||||
)
|
||||
)
|
||||
) : (
|
||||
<ActivityIndicator
|
||||
size="small"
|
||||
color={theme.colorAccentDark}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default LibraryScreen;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
padding: 4,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,152 @@
|
||||
import React, { useState, useCallback, useEffect } from "react";
|
||||
import {
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
RefreshControl,
|
||||
ActivityIndicator,
|
||||
ToastAndroid,
|
||||
} from "react-native";
|
||||
import { FlatList } from "react-native-gesture-handler";
|
||||
import { useFocusEffect } from "@react-navigation/native";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import NovelCover from "../../components/NovelCover";
|
||||
import EmptyView from "../../components/EmptyView";
|
||||
|
||||
import {
|
||||
getLibraryAction,
|
||||
searchLibraryAction,
|
||||
} from "../../redux/library/library.actions";
|
||||
import { SearchAppbar } from "../../components/Appbar";
|
||||
import { setNovel } from "../../redux/novel/novel.actions";
|
||||
import { updateAllNovels } from "../../services/updates";
|
||||
|
||||
const LibraryScreen = ({
|
||||
navigation,
|
||||
theme,
|
||||
novels,
|
||||
loading,
|
||||
getLibraryAction,
|
||||
searchLibraryAction,
|
||||
setNovel,
|
||||
searchResults,
|
||||
itemsPerRow,
|
||||
}) => {
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
const [searchText, setSearchText] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
getLibraryAction();
|
||||
}, [getLibraryAction, novels]);
|
||||
|
||||
/**
|
||||
* TODO: fix refreshing
|
||||
*/
|
||||
const onRefresh = () => {
|
||||
ToastAndroid.show("Updating library", ToastAndroid.SHORT);
|
||||
setRefreshing(true);
|
||||
updateAllNovels();
|
||||
setRefreshing(false);
|
||||
};
|
||||
|
||||
const redirectToNovel = (item) => {
|
||||
navigation.navigate("NovelItem", item);
|
||||
setNovel(item);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<View
|
||||
style={[
|
||||
styles.container,
|
||||
{ backgroundColor: theme.colorPrimaryDark },
|
||||
]}
|
||||
>
|
||||
<SearchAppbar
|
||||
screen="Library"
|
||||
placeholder="Search Library"
|
||||
getNovels={getLibraryAction}
|
||||
getSearchResults={searchLibraryAction}
|
||||
searchText={searchText}
|
||||
setSearchText={setSearchText}
|
||||
/>
|
||||
{loading ? (
|
||||
<ActivityIndicator
|
||||
size="small"
|
||||
color={theme.colorAccentDark}
|
||||
/>
|
||||
) : (
|
||||
<FlatList
|
||||
contentContainerStyle={{
|
||||
flexGrow: 1,
|
||||
paddingBottom: 8,
|
||||
}}
|
||||
numColumns={itemsPerRow}
|
||||
key={itemsPerRow}
|
||||
data={!searchText ? novels : searchResults}
|
||||
keyExtractor={(item) => item.novelUrl}
|
||||
renderItem={({ item }) => (
|
||||
<NovelCover
|
||||
item={item}
|
||||
onPress={() => redirectToNovel(item)}
|
||||
/>
|
||||
)}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={onRefresh}
|
||||
colors={["white"]}
|
||||
progressBackgroundColor={theme.colorAccentDark}
|
||||
/>
|
||||
}
|
||||
ListEmptyComponent={
|
||||
searchText !== "" ? (
|
||||
<Text
|
||||
style={[
|
||||
styles.emptySearch,
|
||||
{ color: theme.colorAccentDark },
|
||||
]}
|
||||
>
|
||||
{`"${searchText}" not in library`}
|
||||
</Text>
|
||||
) : (
|
||||
<EmptyView
|
||||
icon="Σ(ಠ_ಠ)"
|
||||
description="Your library is empty. Add series to your library from Browse."
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
theme: state.themeReducer.theme,
|
||||
itemsPerRow: state.settingsReducer.itemsPerRow,
|
||||
novels: state.libraryReducer.novels,
|
||||
searchResults: state.libraryReducer.searchResults,
|
||||
loading: state.libraryReducer.loading,
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, {
|
||||
getLibraryAction,
|
||||
searchLibraryAction,
|
||||
setNovel,
|
||||
})(LibraryScreen);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
padding: 4,
|
||||
},
|
||||
emptySearch: {
|
||||
marginTop: 8,
|
||||
textAlign: "center",
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
});
|
||||
+56
-65
@@ -1,79 +1,70 @@
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
import * as Linking from "expo-linking";
|
||||
import { List, Divider } from "react-native-paper";
|
||||
|
||||
import { CustomAppbar } from "../../components/common/Appbar";
|
||||
import { Appbar } from "../../components/Appbar";
|
||||
|
||||
import { useSelector } from "react-redux";
|
||||
import { Divider, ListItem, ListSection } from "../../components/List";
|
||||
|
||||
const AboutScreen = ({ navigation }) => {
|
||||
const theme = useSelector((state) => state.themeReducer.theme);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CustomAppbar
|
||||
title="About"
|
||||
onBackAction={() => navigation.goBack()}
|
||||
/>
|
||||
<List.Section
|
||||
style={{
|
||||
flex: 1,
|
||||
marginTop: 0,
|
||||
backgroundColor: theme.colorDarkPrimaryDark,
|
||||
marginBottom: 0,
|
||||
}}
|
||||
>
|
||||
<List.Item
|
||||
titleStyle={{ color: theme.textColorPrimaryDark }}
|
||||
title="Version"
|
||||
descriptionStyle={{ color: theme.textColorSecondaryDark }}
|
||||
description="Stable 1.0.17"
|
||||
/>
|
||||
<List.Item
|
||||
titleStyle={{ color: theme.textColorPrimaryDark }}
|
||||
title="Build Time"
|
||||
descriptionStyle={{ color: theme.textColorSecondaryDark }}
|
||||
description="09-03-21 09:46 AM"
|
||||
/>
|
||||
|
||||
<List.Item
|
||||
titleStyle={{ color: theme.textColorPrimaryDark }}
|
||||
title="What's new"
|
||||
onPress={() =>
|
||||
Linking.openURL(
|
||||
"https://github.com/rajarsheechatterjee/lnreader/commits/master"
|
||||
)
|
||||
}
|
||||
rippleColor={theme.rippleColorDark}
|
||||
/>
|
||||
<Divider />
|
||||
|
||||
<List.Item
|
||||
titleStyle={{ color: theme.textColorPrimaryDark }}
|
||||
descriptionStyle={{ color: theme.textColorSecondaryDark }}
|
||||
title="Github"
|
||||
description="https://github.com/rajarsheechatterjee/lnreader"
|
||||
onPress={() =>
|
||||
Linking.openURL(
|
||||
"https://github.com/rajarsheechatterjee/lnreader"
|
||||
)
|
||||
}
|
||||
rippleColor={theme.rippleColorDark}
|
||||
/>
|
||||
<List.Item
|
||||
titleStyle={{ color: theme.textColorPrimaryDark }}
|
||||
descriptionStyle={{ color: theme.textColorSecondaryDark }}
|
||||
title="Extensions"
|
||||
description="https://github.com/rajarsheechatterjee/lnreader-extensions"
|
||||
descriptionNumberOfLines={1}
|
||||
onPress={() =>
|
||||
Linking.openURL(
|
||||
"https://github.com/rajarsheechatterjee/lnreader-extensions"
|
||||
)
|
||||
}
|
||||
rippleColor={theme.rippleColorDark}
|
||||
/>
|
||||
</List.Section>
|
||||
<Appbar title="About" onBackAction={() => navigation.goBack()} />
|
||||
<View style={{ flex: 1, backgroundColor: theme.colorPrimaryDark }}>
|
||||
<ListSection>
|
||||
<ListItem
|
||||
title="Version"
|
||||
description="Stable 1.0.13"
|
||||
theme={theme}
|
||||
/>
|
||||
<ListItem
|
||||
title="Build Time"
|
||||
description="24-04-21 21:00 PM"
|
||||
theme={theme}
|
||||
/>
|
||||
<ListItem
|
||||
title="What's new"
|
||||
onPress={() =>
|
||||
Linking.openURL(
|
||||
"https://github.com/rajarsheechatterjee/lnreader/commits/main"
|
||||
)
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<Divider theme={theme} />
|
||||
<ListItem
|
||||
title="Discord"
|
||||
description="https://discord.gg/QdcWN4MD63"
|
||||
onPress={() =>
|
||||
Linking.openURL("https://discord.gg/QdcWN4MD63")
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<ListItem
|
||||
title="Github"
|
||||
description="https://github.com/rajarsheechatterjee/lnreader"
|
||||
onPress={() =>
|
||||
Linking.openURL(
|
||||
"https://github.com/rajarsheechatterjee/lnreader"
|
||||
)
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<ListItem
|
||||
title="Extensions"
|
||||
description="https://github.com/rajarsheechatterjee/lnreader-extensions"
|
||||
onPress={() =>
|
||||
Linking.openURL(
|
||||
"https://github.com/rajarsheechatterjee/lnreader-extensions"
|
||||
)
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
</ListSection>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+48
-37
@@ -1,49 +1,60 @@
|
||||
import React from "react";
|
||||
|
||||
import { StyleSheet } from "react-native";
|
||||
import { List } from "react-native-paper";
|
||||
|
||||
import { CustomAppbar } from "../../components/common/Appbar";
|
||||
|
||||
import { StyleSheet, View, Image } from "react-native";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
import { Appbar } from "../../components/Appbar";
|
||||
import { ListItem, ListSection } from "../../components/List";
|
||||
|
||||
const MoreScreen = ({ navigation }) => {
|
||||
const theme = useSelector((state) => state.themeReducer.theme);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CustomAppbar title="More" />
|
||||
<List.Section
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: theme.colorDarkPrimaryDark,
|
||||
marginTop: 0,
|
||||
marginBottom: 0,
|
||||
}}
|
||||
>
|
||||
<List.Item
|
||||
titleStyle={{ color: theme.textColorPrimaryDark }}
|
||||
title="Settings"
|
||||
left={() => (
|
||||
<List.Icon
|
||||
color={theme.colorAccentDark}
|
||||
icon="cog-outline"
|
||||
<Appbar title="More" style={{ elevation: 0 }} />
|
||||
<View style={{ flex: 1, backgroundColor: theme.colorPrimaryDark }}>
|
||||
<View style={{ overflow: "hidden", paddingBottom: 4 }}>
|
||||
<View
|
||||
style={{
|
||||
paddingTop: 20,
|
||||
paddingBottom: 30,
|
||||
alignItems: "center",
|
||||
backgroundColor: theme.colorPrimary,
|
||||
elevation: 4,
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
source={require("../../../assets/logo.png")}
|
||||
style={{
|
||||
height: 60,
|
||||
width: 60,
|
||||
tintColor: theme.textColorPrimary,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
onPress={() => navigation.navigate("Settings")}
|
||||
/>
|
||||
<List.Item
|
||||
titleStyle={{ color: theme.textColorPrimaryDark }}
|
||||
title="About"
|
||||
left={() => (
|
||||
<List.Icon
|
||||
color={theme.colorAccentDark}
|
||||
icon="information-outline"
|
||||
/>
|
||||
)}
|
||||
onPress={() => navigation.navigate("About")}
|
||||
/>
|
||||
</List.Section>
|
||||
</View>
|
||||
</View>
|
||||
<ListSection>
|
||||
<ListItem
|
||||
title="Settings"
|
||||
icon="cog-outline"
|
||||
onPress={() =>
|
||||
navigation.navigate("SettingsStack", {
|
||||
screen: "Settings",
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<ListItem
|
||||
title="About"
|
||||
icon="information-outline"
|
||||
onPress={() =>
|
||||
navigation.navigate("SettingsStack", {
|
||||
screen: "About",
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
</ListSection>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+109
-178
@@ -1,203 +1,128 @@
|
||||
import React, { useState } from "react";
|
||||
import { View, ToastAndroid } from "react-native";
|
||||
import { setStatusBarStyle } from "expo-status-bar";
|
||||
import { List } from "react-native-paper";
|
||||
|
||||
import { View } from "react-native";
|
||||
import { connect } from "react-redux";
|
||||
import { switchTheme } from "../../redux/actions/theme";
|
||||
import {
|
||||
amoledDarkTheme,
|
||||
darkTheme,
|
||||
midnightDuskTheme,
|
||||
lightTheme,
|
||||
} from "../../theme/theme";
|
||||
|
||||
import { CustomAppbar } from "../../components/common/Appbar";
|
||||
import { switchTheme } from "../../redux/theme/theme.actions";
|
||||
import {
|
||||
DisplayCheckbox,
|
||||
ThemeCheckbox,
|
||||
} from "../../components/settings/Checkbox";
|
||||
|
||||
import {
|
||||
setAppTheme,
|
||||
getAppTheme,
|
||||
getDisplayMode,
|
||||
setDisplayMode,
|
||||
} from "../../services/asyncStorage";
|
||||
setItemsPerRow,
|
||||
} from "../../redux/settings/settings.actions";
|
||||
import { deleteAllHistory } from "../../database/queries/HistoryQueries";
|
||||
import { deleteNovelCache } from "../../database/queries/NovelQueries";
|
||||
|
||||
import { Appbar } from "../../components/Appbar";
|
||||
import {
|
||||
deleteHistory,
|
||||
deleteNovelsNotInLibrary,
|
||||
deleteDatabase,
|
||||
} from "../../services/db";
|
||||
Divider,
|
||||
ListItem,
|
||||
ListSection,
|
||||
ListSubHeader,
|
||||
} from "../../components/List";
|
||||
import DisplayModeModal from "./components/DisplayModeModal";
|
||||
import GridSizeModal from "./components/GridSizeModal";
|
||||
import ThemeModal from "./components/ThemeModal";
|
||||
|
||||
const SettingsScreen = ({ navigation, theme, switchTheme }) => {
|
||||
const desciptionStyles = {
|
||||
color: theme.textColorSecondaryDark,
|
||||
const SettingsScreen = ({
|
||||
navigation,
|
||||
theme,
|
||||
themeCode,
|
||||
displayMode,
|
||||
itemsPerRow,
|
||||
switchTheme,
|
||||
setDisplayMode,
|
||||
setItemsPerRow,
|
||||
}) => {
|
||||
const displayModeLabel = (displayMode) => {
|
||||
const label = {
|
||||
0: "Compact Grid",
|
||||
1: "Comfortable Grid",
|
||||
2: "List",
|
||||
};
|
||||
|
||||
return label[displayMode];
|
||||
};
|
||||
|
||||
const titleStyles = { color: theme.textColorPrimaryDark };
|
||||
// Display Mode Modal
|
||||
const [displayModalVisible, setDisplayModalVisible] = useState(false);
|
||||
const showDisplayModal = () => setDisplayModalVisible(true);
|
||||
const hideDisplayModal = () => setDisplayModalVisible(false);
|
||||
|
||||
const [displayMode, setDisplay] = useState("compact");
|
||||
const [applicationTheme, setApplicationTheme] = useState("amoledDarkTheme");
|
||||
// Theme Modal
|
||||
const [themeModalVisible, setthemeModalVisible] = useState(false);
|
||||
const showthemeModal = () => setthemeModalVisible(true);
|
||||
const hidethemeModal = () => setthemeModalVisible(false);
|
||||
|
||||
getAppTheme().then((res) => setApplicationTheme(res));
|
||||
|
||||
getDisplayMode().then((res) => setDisplay(res));
|
||||
// Grid Size Modal
|
||||
const [gridSizeModalVisible, setGridSizeModalVisible] = useState(false);
|
||||
const showGridSizeModal = () => setGridSizeModalVisible(true);
|
||||
const hideGridSizeModal = () => setGridSizeModalVisible(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CustomAppbar
|
||||
title="Settings"
|
||||
onBackAction={() => navigation.goBack()}
|
||||
/>
|
||||
<Appbar title="Settings" onBackAction={() => navigation.goBack()} />
|
||||
<View
|
||||
style={{ flex: 1, backgroundColor: theme.colorDarkPrimaryDark }}
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: theme.colorPrimaryDark,
|
||||
paddingVertical: 8,
|
||||
}}
|
||||
>
|
||||
<List.Section
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: theme.colorDarkPrimaryDark,
|
||||
}}
|
||||
>
|
||||
<List.Subheader
|
||||
style={{
|
||||
color: theme.colorAccentDark,
|
||||
paddingBottom: 5,
|
||||
}}
|
||||
>
|
||||
Data
|
||||
</List.Subheader>
|
||||
<List.Item
|
||||
titleStyle={titleStyles}
|
||||
<ListSection>
|
||||
<ListSubHeader theme={theme}>Data</ListSubHeader>
|
||||
<ListItem
|
||||
title="Clear database"
|
||||
descriptionStyle={desciptionStyles}
|
||||
description="Delete history for novels not in your library"
|
||||
onPress={() => deleteNovelsNotInLibrary()}
|
||||
rippleColor={theme.rippleColorDark}
|
||||
onPress={() => deleteNovelCache()}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Item
|
||||
titleStyle={titleStyles}
|
||||
<ListItem
|
||||
title="Clear history"
|
||||
descriptionStyle={desciptionStyles}
|
||||
description="Delete reading history for all novels"
|
||||
onPress={() => deleteHistory()}
|
||||
rippleColor={theme.rippleColorDark}
|
||||
onPress={() => deleteAllHistory()}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Subheader
|
||||
style={{
|
||||
color: theme.colorAccentDark,
|
||||
paddingBottom: 5,
|
||||
}}
|
||||
>
|
||||
Display
|
||||
</List.Subheader>
|
||||
<View>
|
||||
<DisplayCheckbox
|
||||
value="comfortable"
|
||||
displayMode={displayMode}
|
||||
onPress={() => {
|
||||
setDisplay("comfortable");
|
||||
setDisplayMode("comfortable");
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<View>
|
||||
<DisplayCheckbox
|
||||
value="compact"
|
||||
displayMode={displayMode}
|
||||
onPress={() => {
|
||||
setDisplay("compact");
|
||||
setDisplayMode("compact");
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
{/* <List.Subheader
|
||||
style={{
|
||||
color: theme.colorAccentDark,
|
||||
paddingBottom: 5,
|
||||
}}
|
||||
>
|
||||
Devloper options
|
||||
</List.Subheader>
|
||||
<List.Item
|
||||
titleStyle={titleStyles}
|
||||
title="Delete database"
|
||||
descriptionStyle={desciptionStyles}
|
||||
description="Delete entire database"
|
||||
rippleColor={theme.rippleColorDark}
|
||||
onPress={() => deleteDatabase()}
|
||||
/> */}
|
||||
|
||||
<List.Subheader
|
||||
style={{
|
||||
color: theme.colorAccentDark,
|
||||
paddingBottom: 5,
|
||||
}}
|
||||
>
|
||||
Theme
|
||||
</List.Subheader>
|
||||
<View>
|
||||
<ThemeCheckbox
|
||||
label="Light Theme"
|
||||
checked={
|
||||
applicationTheme === "lightTheme" ? true : false
|
||||
}
|
||||
onPress={() => {
|
||||
switchTheme(lightTheme);
|
||||
setApplicationTheme(`lightTheme`);
|
||||
setAppTheme("lightTheme");
|
||||
setStatusBarStyle("dark");
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<View>
|
||||
<ThemeCheckbox
|
||||
label="Dark Theme"
|
||||
checked={
|
||||
applicationTheme === "darkTheme" ? true : false
|
||||
}
|
||||
onPress={() => {
|
||||
switchTheme(darkTheme);
|
||||
setApplicationTheme("darkTheme");
|
||||
setAppTheme("darkTheme");
|
||||
setStatusBarStyle("light");
|
||||
}}
|
||||
/>
|
||||
<View>
|
||||
<ThemeCheckbox
|
||||
label="Amoled Dark Theme"
|
||||
checked={
|
||||
applicationTheme === "amoledDarkTheme"
|
||||
? true
|
||||
: false
|
||||
}
|
||||
onPress={() => {
|
||||
switchTheme(amoledDarkTheme);
|
||||
setApplicationTheme(`amoledDarkTheme`);
|
||||
setAppTheme("amoledDarkTheme");
|
||||
setStatusBarStyle("light");
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<View>
|
||||
<ThemeCheckbox
|
||||
label="Midnight Dusk Theme"
|
||||
checked={
|
||||
applicationTheme === "midnightDuskTheme"
|
||||
? true
|
||||
: false
|
||||
}
|
||||
onPress={() => {
|
||||
switchTheme(midnightDuskTheme);
|
||||
setApplicationTheme("midnightDuskTheme");
|
||||
setAppTheme("midnightDuskTheme");
|
||||
setStatusBarStyle("light");
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</List.Section>
|
||||
<Divider theme={theme} />
|
||||
<ListSubHeader theme={theme}>Display</ListSubHeader>
|
||||
<ListItem
|
||||
title="Display Mode"
|
||||
description={displayModeLabel(displayMode)}
|
||||
onPress={showDisplayModal}
|
||||
theme={theme}
|
||||
/>
|
||||
<ListItem
|
||||
title="Items per row in library"
|
||||
description={`${itemsPerRow} items per row`}
|
||||
onPress={showGridSizeModal}
|
||||
theme={theme}
|
||||
/>
|
||||
<Divider theme={theme} />
|
||||
<ListSubHeader theme={theme}>Theme</ListSubHeader>
|
||||
<ListItem
|
||||
title="Theme"
|
||||
description={theme.name}
|
||||
onPress={showthemeModal}
|
||||
theme={theme}
|
||||
/>
|
||||
</ListSection>
|
||||
<DisplayModeModal
|
||||
displayMode={displayMode}
|
||||
displayModalVisible={displayModalVisible}
|
||||
hideDisplayModal={hideDisplayModal}
|
||||
setDisplayMode={setDisplayMode}
|
||||
theme={theme}
|
||||
/>
|
||||
<GridSizeModal
|
||||
itemsPerRow={itemsPerRow}
|
||||
gridSizeModalVisible={gridSizeModalVisible}
|
||||
hideGridSizeModal={hideGridSizeModal}
|
||||
setItemsPerRow={setItemsPerRow}
|
||||
theme={theme}
|
||||
/>
|
||||
<ThemeModal
|
||||
themeModalVisible={themeModalVisible}
|
||||
hidethemeModal={hidethemeModal}
|
||||
switchTheme={switchTheme}
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
@@ -205,6 +130,12 @@ const SettingsScreen = ({ navigation, theme, switchTheme }) => {
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
theme: state.themeReducer.theme,
|
||||
displayMode: state.settingsReducer.displayMode,
|
||||
itemsPerRow: state.settingsReducer.itemsPerRow,
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, { switchTheme })(SettingsScreen);
|
||||
export default connect(mapStateToProps, {
|
||||
switchTheme,
|
||||
setDisplayMode,
|
||||
setItemsPerRow,
|
||||
})(SettingsScreen);
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import React from "react";
|
||||
import { StyleSheet } from "react-native";
|
||||
import { Portal, Modal, Checkbox } from "react-native-paper";
|
||||
|
||||
const DisplayModeModal = ({
|
||||
displayMode,
|
||||
displayModalVisible,
|
||||
setDisplayMode,
|
||||
hideDisplayModal,
|
||||
theme,
|
||||
}) => {
|
||||
const displayModes = [
|
||||
{ displayMode: 0, label: "Compact Grid" },
|
||||
{ displayMode: 1, label: "Comfortable Grid" },
|
||||
{ displayMode: 2, label: "List" },
|
||||
];
|
||||
|
||||
const renderCheckboxes = () => {
|
||||
return displayModes.map((mode) => (
|
||||
<Checkbox.Item
|
||||
key={mode.displayMode}
|
||||
label={mode.label}
|
||||
labelStyle={{ color: theme.textColorPrimary }}
|
||||
status={
|
||||
displayMode === mode.displayMode ? "checked" : "unchecked"
|
||||
}
|
||||
mode="ios"
|
||||
uncheckedColor={theme.textColorSecondary}
|
||||
color={theme.colorAccentDark}
|
||||
onPress={() => setDisplayMode(mode.displayMode)}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Modal
|
||||
visible={displayModalVisible}
|
||||
onDismiss={hideDisplayModal}
|
||||
contentContainerStyle={[
|
||||
styles.containerStyle,
|
||||
{ backgroundColor: theme.colorPrimaryDark },
|
||||
]}
|
||||
>
|
||||
{renderCheckboxes()}
|
||||
</Modal>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default DisplayModeModal;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
containerStyle: {
|
||||
padding: 20,
|
||||
margin: 20,
|
||||
borderRadius: 6,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
import React from "react";
|
||||
import { Text, StyleSheet } from "react-native";
|
||||
import { Portal, Modal } from "react-native-paper";
|
||||
import Slider from "@react-native-community/slider";
|
||||
|
||||
const GridSizeModal = ({
|
||||
itemsPerRow,
|
||||
gridSizeModalVisible,
|
||||
hideGridSizeModal,
|
||||
setItemsPerRow,
|
||||
theme,
|
||||
}) => {
|
||||
return (
|
||||
<Portal>
|
||||
<Modal
|
||||
visible={gridSizeModalVisible}
|
||||
onDismiss={hideGridSizeModal}
|
||||
contentContainerStyle={[
|
||||
styles.containerStyle,
|
||||
{ backgroundColor: theme.colorPrimaryDark },
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.modalHeader,
|
||||
{ color: theme.textColorPrimary },
|
||||
]}
|
||||
>
|
||||
Grid size
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
styles.modalDescription,
|
||||
{ color: theme.textColorSecondary },
|
||||
]}
|
||||
>
|
||||
{`${itemsPerRow} per row`}
|
||||
</Text>
|
||||
<Slider
|
||||
style={styles.slider}
|
||||
value={itemsPerRow}
|
||||
minimumValue={1}
|
||||
maximumValue={5}
|
||||
step={1}
|
||||
minimumTrackTintColor={theme.colorAccentDark}
|
||||
maximumTrackTintColor="#000000"
|
||||
thumbTintColor={theme.colorAccentDark}
|
||||
onValueChange={(value) => setItemsPerRow(value)}
|
||||
/>
|
||||
</Modal>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default GridSizeModal;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
containerStyle: {
|
||||
padding: 20,
|
||||
margin: 20,
|
||||
borderRadius: 6,
|
||||
},
|
||||
modalHeader: {
|
||||
fontSize: 18,
|
||||
marginBottom: 10,
|
||||
},
|
||||
modalDescription: {
|
||||
marginBottom: 4,
|
||||
},
|
||||
slider: {
|
||||
width: "100%",
|
||||
height: 40,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
import React from "react";
|
||||
import { StyleSheet } from "react-native";
|
||||
import { Portal, Modal, Checkbox } from "react-native-paper";
|
||||
|
||||
import { setStatusBarStyle } from "expo-status-bar";
|
||||
|
||||
const ThemeModal = ({
|
||||
themeModalVisible,
|
||||
hidethemeModal,
|
||||
switchTheme,
|
||||
theme,
|
||||
}) => {
|
||||
const themes = [
|
||||
{ id: 0, name: "AMOLED Dark", statusBar: "light" },
|
||||
{ id: 1, name: "Light", statusBar: "dark" },
|
||||
{ id: 2, name: "Dark", statusBar: "light" },
|
||||
{ id: 3, name: "Midnight Dusk", statusBar: "light" },
|
||||
];
|
||||
|
||||
const renderThemeCheckboxes = () =>
|
||||
themes.map((item) => (
|
||||
<Checkbox.Item
|
||||
key={item.id}
|
||||
label={item.name}
|
||||
labelStyle={{ color: theme.textColorPrimary }}
|
||||
status={theme.id === item.id ? "checked" : "unchecked"}
|
||||
mode="ios"
|
||||
color={theme.colorAccentDark}
|
||||
onPress={() => {
|
||||
switchTheme(item.id);
|
||||
setStatusBarStyle(item.statusBar);
|
||||
}}
|
||||
/>
|
||||
));
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Modal
|
||||
visible={themeModalVisible}
|
||||
onDismiss={hidethemeModal}
|
||||
contentContainerStyle={[
|
||||
styles.containerStyle,
|
||||
{ backgroundColor: theme.colorPrimaryDark },
|
||||
]}
|
||||
>
|
||||
{renderThemeCheckboxes()}
|
||||
</Modal>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemeModal;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
containerStyle: {
|
||||
padding: 20,
|
||||
margin: 20,
|
||||
borderRadius: 6,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,156 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
StyleSheet,
|
||||
View,
|
||||
FlatList,
|
||||
RefreshControl,
|
||||
ToastAndroid,
|
||||
} from "react-native";
|
||||
import { Provider, Portal } from "react-native-paper";
|
||||
|
||||
import ChapterCard from "./components/ChapterCard";
|
||||
import NovelInfoHeader from "./components/NovelHeader";
|
||||
import { BottomSheet } from "./components/BottomSheet";
|
||||
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import {
|
||||
getNovelAction,
|
||||
followNovelAction,
|
||||
sortAndFilterChapters,
|
||||
updateNovelAction,
|
||||
downloadAllChaptersAction,
|
||||
deleteAllChaptersAction,
|
||||
} from "../../redux/novel/novel.actions";
|
||||
|
||||
const Novel = ({
|
||||
route,
|
||||
theme,
|
||||
novel,
|
||||
chapters,
|
||||
loading,
|
||||
getNovelAction,
|
||||
fetching,
|
||||
followNovelAction,
|
||||
sortAndFilterChapters,
|
||||
updateNovelAction,
|
||||
downloadAllChaptersAction,
|
||||
deleteAllChaptersAction,
|
||||
}) => {
|
||||
const {
|
||||
sourceId,
|
||||
novelUrl,
|
||||
novelName,
|
||||
novelCover,
|
||||
followed,
|
||||
novelId,
|
||||
} = route.params;
|
||||
|
||||
let _panel = useRef(null); // Bottomsheet ref
|
||||
|
||||
const [sort, setSort] = useState("ORDER BY chapterId ASC");
|
||||
const [filter, setFilter] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
getNovelAction(followed, sourceId, novelUrl, novelId, sort, filter);
|
||||
}, [getNovelAction, followNovelAction, sort, filter]);
|
||||
|
||||
let lastRead = !loading && chapters.find((obj) => obj.read === 0);
|
||||
|
||||
const renderChapterCard = ({ item }) => (
|
||||
<ChapterCard
|
||||
novelUrl={novelUrl}
|
||||
extensionId={sourceId}
|
||||
chapter={item}
|
||||
/>
|
||||
);
|
||||
|
||||
const onRefresh = () => {
|
||||
updateNovelAction(sourceId, novelUrl, novelId);
|
||||
ToastAndroid.show(`Updated ${novelName}`, ToastAndroid.SHORT);
|
||||
};
|
||||
|
||||
return (
|
||||
<Provider>
|
||||
<View
|
||||
style={[
|
||||
styles.container,
|
||||
{ backgroundColor: theme.colorPrimaryDark },
|
||||
]}
|
||||
>
|
||||
<FlatList
|
||||
data={!loading && chapters}
|
||||
keyExtractor={(item) => item.chapterId.toString()}
|
||||
removeClippedSubviews={true}
|
||||
maxToRenderPerBatch={5}
|
||||
windowSize={15}
|
||||
initialNumToRender={7}
|
||||
renderItem={renderChapterCard}
|
||||
ListHeaderComponent={
|
||||
<NovelInfoHeader
|
||||
theme={theme}
|
||||
item={{ novelName, novelCover }}
|
||||
novel={novel}
|
||||
noOfChapters={chapters?.length}
|
||||
followNovelAction={followNovelAction}
|
||||
loading={loading}
|
||||
lastRead={lastRead}
|
||||
bottomSheetRef={_panel}
|
||||
downloadAllChapters={() =>
|
||||
downloadAllChaptersAction(
|
||||
sourceId,
|
||||
novelUrl,
|
||||
chapters
|
||||
)
|
||||
}
|
||||
deleteAllChapters={() =>
|
||||
deleteAllChaptersAction(chapters)
|
||||
}
|
||||
/>
|
||||
}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={fetching}
|
||||
onRefresh={onRefresh}
|
||||
colors={[theme.textColorPrimary]}
|
||||
progressBackgroundColor={theme.colorPrimary}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Portal>
|
||||
<BottomSheet
|
||||
novelUrl={novelUrl}
|
||||
bottomSheetRef={_panel}
|
||||
sortAndFilterChapters={sortAndFilterChapters}
|
||||
sort={sort}
|
||||
filter={filter}
|
||||
setSort={setSort}
|
||||
setFilter={setFilter}
|
||||
/>
|
||||
</Portal>
|
||||
</View>
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
const mapStateToProps = (state) => ({
|
||||
theme: state.themeReducer.theme,
|
||||
novel: state.novelReducer.novel,
|
||||
chapters: state.novelReducer.chapters,
|
||||
loading: state.novelReducer.loading,
|
||||
fetching: state.novelReducer.fetching,
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, {
|
||||
getNovelAction,
|
||||
followNovelAction,
|
||||
sortAndFilterChapters,
|
||||
updateNovelAction,
|
||||
downloadAllChaptersAction,
|
||||
deleteAllChaptersAction,
|
||||
})(Novel);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
@@ -1,29 +1,17 @@
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import { StyleSheet, View, Text } from "react-native";
|
||||
import { List, RadioButton, Checkbox } from "react-native-paper";
|
||||
import Bottomsheet from "rn-sliding-up-panel";
|
||||
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
const BottomSheetHandle = () => (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: "rgba(255,255,255,0.25)",
|
||||
height: 4,
|
||||
width: 50,
|
||||
borderRadius: 50,
|
||||
top: 10,
|
||||
alignSelf: "center",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
export const BottomSheet = ({
|
||||
bottomSheetRef,
|
||||
sort,
|
||||
filter,
|
||||
sortChapters,
|
||||
filterChapters,
|
||||
setSort,
|
||||
setFilter,
|
||||
novelUrl,
|
||||
}) => {
|
||||
const checkBoxStyle = { flexDirection: "row", alignItems: "center" };
|
||||
|
||||
@@ -39,14 +27,14 @@ export const BottomSheet = ({
|
||||
style={[
|
||||
styles.contentContainer,
|
||||
{
|
||||
backgroundColor: theme.colorDarkPrimary,
|
||||
backgroundColor: theme.colorPrimary,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<BottomSheetHandle />
|
||||
<View style={styles.bottomSheetHandle} />
|
||||
<List.Subheader
|
||||
style={{
|
||||
color: theme.textColorPrimaryDark,
|
||||
color: theme.textColorPrimary,
|
||||
fontSize: 20,
|
||||
}}
|
||||
>
|
||||
@@ -54,9 +42,8 @@ export const BottomSheet = ({
|
||||
</List.Subheader>
|
||||
<View>
|
||||
<RadioButton.Group
|
||||
onValueChange={(newValue) => sortChapters(newValue)}
|
||||
onValueChange={(newValue) => setSort(newValue)}
|
||||
value={sort}
|
||||
c
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
@@ -77,7 +64,7 @@ export const BottomSheet = ({
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorPrimaryDark,
|
||||
color: theme.textColorPrimary,
|
||||
}}
|
||||
>
|
||||
Newest to oldest
|
||||
@@ -92,11 +79,11 @@ export const BottomSheet = ({
|
||||
<RadioButton.Item
|
||||
uncheckedColor={theme.textColorHintDark}
|
||||
color={theme.colorAccentDark}
|
||||
value=""
|
||||
value="ORDER BY chapterId ASC"
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorPrimaryDark,
|
||||
color: theme.textColorPrimary,
|
||||
}}
|
||||
>
|
||||
Oldest to newest
|
||||
@@ -108,7 +95,7 @@ export const BottomSheet = ({
|
||||
|
||||
<List.Subheader
|
||||
style={{
|
||||
color: theme.textColorPrimaryDark,
|
||||
color: theme.textColorPrimary,
|
||||
fontSize: 20,
|
||||
paddingVertical: 10,
|
||||
}}
|
||||
@@ -120,9 +107,9 @@ export const BottomSheet = ({
|
||||
status={filter === "" ? "checked" : "unchecked"}
|
||||
uncheckedColor={theme.textColorHintDark}
|
||||
color={theme.colorAccentDark}
|
||||
onPress={() => filterChapters("")}
|
||||
onPress={() => setFilter("")}
|
||||
/>
|
||||
<Text style={{ color: theme.textColorPrimaryDark }}>
|
||||
<Text style={{ color: theme.textColorPrimary }}>
|
||||
Show all
|
||||
</Text>
|
||||
</View>
|
||||
@@ -133,9 +120,9 @@ export const BottomSheet = ({
|
||||
}
|
||||
uncheckedColor={theme.textColorHintDark}
|
||||
color={theme.colorAccentDark}
|
||||
onPress={() => filterChapters("AND `read`=1")}
|
||||
onPress={() => setFilter("AND `read`=1")}
|
||||
/>
|
||||
<Text style={{ color: theme.textColorPrimaryDark }}>
|
||||
<Text style={{ color: theme.textColorPrimary }}>
|
||||
Show read chapters
|
||||
</Text>
|
||||
</View>
|
||||
@@ -146,9 +133,9 @@ export const BottomSheet = ({
|
||||
}
|
||||
uncheckedColor={theme.textColorHintDark}
|
||||
color={theme.colorAccentDark}
|
||||
onPress={() => filterChapters("AND `read`=0")}
|
||||
onPress={() => setFilter("AND `read`=0")}
|
||||
/>
|
||||
<Text style={{ color: theme.textColorPrimaryDark }}>
|
||||
<Text style={{ color: theme.textColorPrimary }}>
|
||||
Show unread chapters
|
||||
</Text>
|
||||
</View>
|
||||
@@ -161,9 +148,9 @@ export const BottomSheet = ({
|
||||
}
|
||||
uncheckedColor={theme.textColorHintDark}
|
||||
color={theme.colorAccentDark}
|
||||
onPress={() => filterChapters("AND downloaded=1")}
|
||||
onPress={() => setFilter("AND downloaded=1")}
|
||||
/>
|
||||
<Text style={{ color: theme.textColorPrimaryDark }}>
|
||||
<Text style={{ color: theme.textColorPrimary }}>
|
||||
Show downloaded chapters
|
||||
</Text>
|
||||
</View>
|
||||
@@ -175,5 +162,15 @@ export const BottomSheet = ({
|
||||
const styles = StyleSheet.create({
|
||||
contentContainer: {
|
||||
flex: 1,
|
||||
borderTopRightRadius: 8,
|
||||
borderTopLeftRadius: 8,
|
||||
},
|
||||
bottomSheetHandle: {
|
||||
backgroundColor: "rgba(255,255,255,0.25)",
|
||||
height: 4,
|
||||
width: 50,
|
||||
borderRadius: 50,
|
||||
top: 10,
|
||||
alignSelf: "center",
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,160 @@
|
||||
import React, { useState } from "react";
|
||||
import { View, Text, ActivityIndicator, StyleSheet } from "react-native";
|
||||
import { TouchableRipple, IconButton, Menu } from "react-native-paper";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
|
||||
import {
|
||||
deleteChapterAction,
|
||||
downloadChapterAction,
|
||||
} from "../../../redux/novel/novel.actions";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
const ChapterCard = ({
|
||||
novelUrl,
|
||||
chapter,
|
||||
extensionId,
|
||||
theme,
|
||||
// downloading,
|
||||
downloadChapterAction,
|
||||
deleteChapterAction,
|
||||
}) => {
|
||||
const navigation = useNavigation();
|
||||
|
||||
const [visible, setVisible] = useState(false);
|
||||
const openMenu = () => setVisible(true);
|
||||
const closeMenu = () => setVisible(false);
|
||||
|
||||
const displayDownloadButton = () => {
|
||||
if (chapter.downloaded === 3) {
|
||||
return (
|
||||
<ActivityIndicator
|
||||
color={theme.textColorHintDark}
|
||||
size={25}
|
||||
style={{ margin: 3.5, padding: 5 }}
|
||||
/>
|
||||
);
|
||||
} else if (chapter.downloaded === 1) {
|
||||
return (
|
||||
<Menu
|
||||
visible={visible}
|
||||
onDismiss={closeMenu}
|
||||
anchor={
|
||||
<IconButton
|
||||
icon="check-circle"
|
||||
animated
|
||||
color={theme.textColorPrimary}
|
||||
size={25}
|
||||
onPress={openMenu}
|
||||
style={{ margin: 2 }}
|
||||
/>
|
||||
}
|
||||
contentStyle={{ backgroundColor: theme.menuColor }}
|
||||
>
|
||||
<Menu.Item
|
||||
onPress={() =>
|
||||
deleteChapterAction(
|
||||
chapter.chapterId,
|
||||
chapter.chapterName
|
||||
)
|
||||
}
|
||||
title="Delete"
|
||||
titleStyle={{ color: theme.textColorPrimary }}
|
||||
/>
|
||||
</Menu>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<IconButton
|
||||
icon="arrow-down-circle-outline"
|
||||
animated
|
||||
color={theme.textColorHintDark}
|
||||
size={25}
|
||||
onPress={() => {
|
||||
downloadChapterAction(
|
||||
extensionId,
|
||||
novelUrl,
|
||||
chapter.chapterUrl,
|
||||
chapter.chapterName,
|
||||
chapter.chapterId
|
||||
);
|
||||
// console.log(downloading);
|
||||
}}
|
||||
style={{ margin: 2 }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<TouchableRipple
|
||||
style={styles.chapterCardContainer}
|
||||
onPress={() =>
|
||||
navigation.navigate("ChapterItem", {
|
||||
chapterId: chapter.chapterId,
|
||||
chapterUrl: chapter.chapterUrl,
|
||||
extensionId,
|
||||
novelUrl: novelUrl,
|
||||
chapterName: chapter.chapterName,
|
||||
novelId: chapter.novelId,
|
||||
})
|
||||
}
|
||||
rippleColor={theme.rippleColor}
|
||||
>
|
||||
<>
|
||||
<View>
|
||||
<Text
|
||||
style={[
|
||||
{
|
||||
color: theme.textColorPrimary,
|
||||
},
|
||||
chapter.read === 1 && {
|
||||
color: theme.textColorHintDark,
|
||||
},
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{chapter.chapterName.substring(0, 50)}
|
||||
</Text>
|
||||
{chapter.releaseDate && (
|
||||
<Text
|
||||
style={[
|
||||
{
|
||||
color: theme.textColorSecondary,
|
||||
marginTop: 5,
|
||||
fontSize: 12,
|
||||
},
|
||||
chapter.read === 1 && {
|
||||
color: theme.textColorHintDark,
|
||||
},
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{chapter.releaseDate}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
<View>{displayDownloadButton()}</View>
|
||||
</>
|
||||
</TouchableRipple>
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
theme: state.themeReducer.theme,
|
||||
// downloading: state.novelReducer.downloading,
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, {
|
||||
downloadChapterAction,
|
||||
deleteChapterAction,
|
||||
})(ChapterCard);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
chapterCardContainer: {
|
||||
paddingHorizontal: 15,
|
||||
paddingVertical: 7,
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
},
|
||||
});
|
||||
+187
-140
@@ -8,28 +8,40 @@ import {
|
||||
FlatList,
|
||||
Share,
|
||||
} from "react-native";
|
||||
import { TouchableRipple, IconButton, Button, Chip } from "react-native-paper";
|
||||
import {
|
||||
TouchableRipple,
|
||||
IconButton,
|
||||
Button,
|
||||
Chip,
|
||||
Menu,
|
||||
} from "react-native-paper";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import * as WebBrowser from "expo-web-browser";
|
||||
|
||||
import { LinearGradient } from "expo-linear-gradient";
|
||||
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
||||
|
||||
import { connect } from "react-redux";
|
||||
const NovelInfoHeader = ({
|
||||
item,
|
||||
novel,
|
||||
noOfChapters,
|
||||
insertNovelInLib,
|
||||
followNovelAction,
|
||||
loading,
|
||||
navigatingFrom,
|
||||
libraryStatus,
|
||||
bottomSheetRef,
|
||||
theme,
|
||||
lastRead,
|
||||
downloadAllChapters,
|
||||
deleteAllChapters,
|
||||
}) => {
|
||||
const navigation = useNavigation();
|
||||
|
||||
const [more, setMore] = useState(navigatingFrom === 2 ? true : false);
|
||||
const [more, setMore] = useState(
|
||||
!loading && novel.followed !== 1 ? true : false
|
||||
);
|
||||
|
||||
const [visible, setVisible] = useState(false);
|
||||
const openMenu = () => setVisible(true);
|
||||
const closeMenu = () => setVisible(false);
|
||||
|
||||
const renderGenreChip = ({ item }) => (
|
||||
<Text
|
||||
@@ -38,7 +50,7 @@ const NovelInfoHeader = ({
|
||||
{
|
||||
color: theme.colorAccentDark,
|
||||
borderColor: theme.colorAccentDark,
|
||||
backgroundColor: theme.colorDarkPrimary,
|
||||
backgroundColor: theme.colorPrimary,
|
||||
},
|
||||
]}
|
||||
>
|
||||
@@ -47,23 +59,19 @@ const NovelInfoHeader = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<View style={{ flexGrow: 1 }}>
|
||||
<ImageBackground
|
||||
source={{
|
||||
uri: item.novelCover,
|
||||
}}
|
||||
source={{ uri: item.novelCover }}
|
||||
style={styles.background}
|
||||
>
|
||||
<LinearGradient
|
||||
colors={["rgba(0,0,0,0.2)", theme.colorDarkPrimaryDark]}
|
||||
colors={["rgba(0,0,0,0.2)", theme.colorPrimaryDark]}
|
||||
style={styles.linearGradient}
|
||||
>
|
||||
<View style={styles.detailsContainer}>
|
||||
<View>
|
||||
<Image
|
||||
source={{
|
||||
uri: item.novelCover,
|
||||
}}
|
||||
source={{ uri: item.novelCover }}
|
||||
style={styles.logo}
|
||||
/>
|
||||
</View>
|
||||
@@ -72,9 +80,7 @@ const NovelInfoHeader = ({
|
||||
numberOfLines={2}
|
||||
style={[
|
||||
styles.name,
|
||||
{
|
||||
color: theme.textColorPrimaryDark,
|
||||
},
|
||||
{ color: theme.textColorPrimary },
|
||||
]}
|
||||
>
|
||||
{item.novelName}
|
||||
@@ -83,33 +89,29 @@ const NovelInfoHeader = ({
|
||||
<>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorSecondaryDark,
|
||||
color: theme.textColorSecondary,
|
||||
marginVertical: 3,
|
||||
fontSize: 14,
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
numberOfLines={2}
|
||||
>
|
||||
{novel["Author(s)"] &&
|
||||
novel["Author(s)"].replace(
|
||||
",",
|
||||
", "
|
||||
)}
|
||||
{novel.author.replace(",", ", ")}
|
||||
</Text>
|
||||
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorSecondaryDark,
|
||||
color: theme.textColorSecondary,
|
||||
marginVertical: 3,
|
||||
fontSize: 14,
|
||||
}}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{novel["Status"]}
|
||||
{novel.status}
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorSecondaryDark,
|
||||
// marginVertical: 3,
|
||||
color: theme.textColorSecondary,
|
||||
fontSize: 14,
|
||||
}}
|
||||
numberOfLines={1}
|
||||
@@ -124,19 +126,13 @@ const NovelInfoHeader = ({
|
||||
</ImageBackground>
|
||||
{!loading && (
|
||||
<>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<View style={styles.buttonsContainer}>
|
||||
<Chip
|
||||
mode="outlined"
|
||||
icon={() => (
|
||||
<MaterialCommunityIcons
|
||||
name={
|
||||
libraryStatus
|
||||
novel.followed
|
||||
? "heart"
|
||||
: "heart-outline"
|
||||
}
|
||||
@@ -144,38 +140,22 @@ const NovelInfoHeader = ({
|
||||
color={theme.colorAccentDark}
|
||||
/>
|
||||
)}
|
||||
onPress={() => insertNovelInLib()}
|
||||
onPress={() => followNovelAction(novel)}
|
||||
style={[
|
||||
{
|
||||
backgroundColor: theme.colorDarkPrimaryDark,
|
||||
marginRight: 2,
|
||||
// borderColor: "rgba(255,255,255,0.121)",
|
||||
// borderWidth: 1,
|
||||
justifyContent: "center",
|
||||
height: 30,
|
||||
alignItems: "center",
|
||||
marginLeft: 15,
|
||||
paddingHorizontal: 3,
|
||||
shadowOpacity: 0,
|
||||
shadowOffset: 0,
|
||||
},
|
||||
// libraryStatus && {
|
||||
// backgroundColor: "rgba(41,121,255,0.38)",
|
||||
// },
|
||||
{ backgroundColor: theme.colorPrimaryDark },
|
||||
styles.toggleFavourite,
|
||||
]}
|
||||
textStyle={{
|
||||
fontWeight: "bold",
|
||||
color: theme.textColorPrimaryDark,
|
||||
color: theme.textColorPrimary,
|
||||
}}
|
||||
>
|
||||
{libraryStatus ? "In Library" : "Add to library"}
|
||||
{novel.followed ? "In Library" : "Add to library"}
|
||||
</Chip>
|
||||
|
||||
<IconButton
|
||||
onPress={() =>
|
||||
WebBrowser.openBrowserAsync(
|
||||
novel.sourceUrl && novel.sourceUrl
|
||||
)
|
||||
WebBrowser.openBrowserAsync(novel.sourceUrl)
|
||||
}
|
||||
icon="earth"
|
||||
color={theme.colorAccentDark}
|
||||
@@ -183,37 +163,59 @@ const NovelInfoHeader = ({
|
||||
/>
|
||||
<IconButton
|
||||
onPress={() =>
|
||||
Share.share({
|
||||
message: novel.sourceUrl,
|
||||
})
|
||||
Share.share({ message: novel.sourceUrl })
|
||||
}
|
||||
icon="share-variant"
|
||||
color={theme.colorAccentDark}
|
||||
size={21}
|
||||
/>
|
||||
|
||||
<Menu
|
||||
visible={visible}
|
||||
onDismiss={closeMenu}
|
||||
anchor={
|
||||
<IconButton
|
||||
icon="download"
|
||||
color={theme.colorAccentDark}
|
||||
size={21}
|
||||
onPress={openMenu}
|
||||
/>
|
||||
}
|
||||
contentStyle={{ backgroundColor: theme.menuColor }}
|
||||
>
|
||||
<Menu.Item
|
||||
title="Download all"
|
||||
style={{ backgroundColor: theme.menuColor }}
|
||||
titleStyle={{ color: theme.textColorPrimary }}
|
||||
onPress={downloadAllChapters}
|
||||
/>
|
||||
<Menu.Item
|
||||
title="Delete downloads"
|
||||
style={{ backgroundColor: theme.menuColor }}
|
||||
titleStyle={{ color: theme.textColorPrimary }}
|
||||
onPress={deleteAllChapters}
|
||||
/>
|
||||
</Menu>
|
||||
</View>
|
||||
|
||||
{novel.novelSummary !== "" && (
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: 15,
|
||||
marginBottom: 10,
|
||||
// marginTop: 5,
|
||||
paddingHorizontal: 16,
|
||||
marginBottom: 8,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorPrimaryDark,
|
||||
marginTop: 5,
|
||||
paddingVertical: 5,
|
||||
fontSize: 15,
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
style={[
|
||||
{ color: theme.textColorPrimary },
|
||||
styles.summaryHeader,
|
||||
]}
|
||||
>
|
||||
About
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorSecondaryDark,
|
||||
color: theme.textColorSecondary,
|
||||
lineHeight: 20,
|
||||
}}
|
||||
numberOfLines={more ? 100 : 2}
|
||||
@@ -223,15 +225,13 @@ const NovelInfoHeader = ({
|
||||
{novel.novelSummary}
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.colorAccentDark,
|
||||
fontWeight: "bold",
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
right: 15,
|
||||
backgroundColor: theme.colorDarkPrimaryDark,
|
||||
paddingLeft: 5,
|
||||
}}
|
||||
style={[
|
||||
{
|
||||
color: theme.colorAccentDark,
|
||||
backgroundColor: theme.colorPrimaryDark,
|
||||
},
|
||||
styles.moreBtn,
|
||||
]}
|
||||
onPress={() => setMore(!more)}
|
||||
>
|
||||
{more ? "Less " : "More "}
|
||||
@@ -243,105 +243,104 @@ const NovelInfoHeader = ({
|
||||
style={
|
||||
novel.novelSummary === ""
|
||||
? { marginTop: 20 }
|
||||
: { marginTop: 10 }
|
||||
: { marginTop: 12 }
|
||||
}
|
||||
contentContainerStyle={{
|
||||
paddingHorizontal: 15,
|
||||
paddingBottom: 15,
|
||||
}}
|
||||
contentContainerStyle={styles.genreContainer}
|
||||
horizontal
|
||||
data={novel["Genre(s)"] && novel["Genre(s)"].split(",")}
|
||||
data={novel.genre && novel.genre.split(",")}
|
||||
keyExtractor={(item) => item}
|
||||
renderItem={renderGenreChip}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
/>
|
||||
|
||||
<Button
|
||||
color="white"
|
||||
style={{
|
||||
backgroundColor: theme.colorAccentDark,
|
||||
marginHorizontal: 15,
|
||||
marginTop: 8,
|
||||
marginBottom: 15,
|
||||
}}
|
||||
uppercase={false}
|
||||
labelStyle={{ letterSpacing: 0 }}
|
||||
onPress={() =>
|
||||
navigation.navigate("ChapterItem", {
|
||||
chapterUrl: novel.lastRead,
|
||||
novelUrl: novel.novelUrl,
|
||||
extensionId: novel.extensionId,
|
||||
chapterName: novel.lastReadName,
|
||||
})
|
||||
}
|
||||
>
|
||||
{novel.unread && novel.unread === 1
|
||||
? `Start reading ${novel.lastReadName}`
|
||||
: `Continue reading ${novel.lastReadName}`}
|
||||
</Button>
|
||||
|
||||
{noOfChapters > 0 &&
|
||||
(lastRead ? (
|
||||
<Button
|
||||
color="white"
|
||||
style={[
|
||||
{ backgroundColor: theme.colorAccentDark },
|
||||
styles.startButton,
|
||||
]}
|
||||
uppercase={false}
|
||||
labelStyle={{ letterSpacing: 0 }}
|
||||
onPress={() => {
|
||||
navigation.navigate("ChapterItem", {
|
||||
chapterId: lastRead.chapterId,
|
||||
chapterUrl: lastRead.chapterUrl,
|
||||
novelUrl: novel.novelUrl,
|
||||
novelId: lastRead.novelId,
|
||||
extensionId: novel.sourceId,
|
||||
chapterName: lastRead.chapterName,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{novel.unread
|
||||
? `Start reading `
|
||||
: `Continue reading `}
|
||||
{lastRead.chapterName}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
color={theme.textColorPrimary}
|
||||
style={[
|
||||
{
|
||||
backgroundColor: theme.textColorPrimary,
|
||||
opacity: 0.2,
|
||||
},
|
||||
styles.startButton,
|
||||
]}
|
||||
uppercase={false}
|
||||
labelStyle={{ letterSpacing: 0 }}
|
||||
>
|
||||
All chapters read
|
||||
</Button>
|
||||
))}
|
||||
<TouchableRipple
|
||||
style={{
|
||||
flex: 1,
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
paddingRight: 12.5,
|
||||
}}
|
||||
style={styles.bottomsheet}
|
||||
onPress={() =>
|
||||
bottomSheetRef.current.show({ velocity: -1.5 })
|
||||
}
|
||||
rippleColor={theme.rippleColorDark}
|
||||
rippleColor={theme.rippleColor}
|
||||
>
|
||||
<>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorPrimaryDark,
|
||||
paddingHorizontal: 15,
|
||||
paddingVertical: 5,
|
||||
fontSize: 16,
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
style={[
|
||||
{ color: theme.textColorPrimary },
|
||||
styles.chapters,
|
||||
]}
|
||||
>
|
||||
{noOfChapters + " Chapters "}
|
||||
{noOfChapters + " Chapters"}
|
||||
</Text>
|
||||
<IconButton
|
||||
icon="filter-variant"
|
||||
color={theme.textColorPrimaryDark}
|
||||
color={theme.textColorPrimary}
|
||||
size={24}
|
||||
/>
|
||||
</>
|
||||
</TouchableRipple>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
theme: state.themeReducer.theme,
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(NovelInfoHeader);
|
||||
export default NovelInfoHeader;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
nameContainer: {
|
||||
flex: 1,
|
||||
marginHorizontal: 15,
|
||||
paddingTop: 10,
|
||||
marginHorizontal: 16,
|
||||
paddingTop: 8,
|
||||
},
|
||||
background: {
|
||||
height: 285,
|
||||
// backgroundColor: theme.colorDarkPrimaryDark,
|
||||
},
|
||||
linearGradient: {
|
||||
// flex: 1,
|
||||
height: 286,
|
||||
},
|
||||
detailsContainer: {
|
||||
flex: 1,
|
||||
flexDirection: "row",
|
||||
margin: 15,
|
||||
margin: 16,
|
||||
paddingTop: 90,
|
||||
},
|
||||
logo: {
|
||||
@@ -364,4 +363,52 @@ const styles = StyleSheet.create({
|
||||
fontWeight: "bold",
|
||||
fontSize: 18,
|
||||
},
|
||||
buttonsContainer: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
},
|
||||
toggleFavourite: {
|
||||
height: 32,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginLeft: 16,
|
||||
marginRight: 4,
|
||||
paddingHorizontal: 4,
|
||||
borderWidth: 0,
|
||||
elevation: 0,
|
||||
},
|
||||
chapters: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 4,
|
||||
fontSize: 16,
|
||||
fontWeight: "bold",
|
||||
},
|
||||
bottomsheet: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
paddingRight: 12,
|
||||
},
|
||||
summaryHeader: {
|
||||
marginTop: 5,
|
||||
paddingVertical: 5,
|
||||
fontSize: 15,
|
||||
fontWeight: "bold",
|
||||
},
|
||||
moreBtn: {
|
||||
fontWeight: "bold",
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
right: 16,
|
||||
paddingLeft: 6,
|
||||
},
|
||||
startButton: {
|
||||
marginTop: 8,
|
||||
marginBottom: 16,
|
||||
marginHorizontal: 16,
|
||||
},
|
||||
genreContainer: {
|
||||
paddingHorizontal: 15,
|
||||
paddingBottom: 15,
|
||||
},
|
||||
});
|
||||
@@ -1,252 +0,0 @@
|
||||
import React, { useState, useCallback, useRef } from "react";
|
||||
import { useFocusEffect } from "@react-navigation/native";
|
||||
import {
|
||||
StyleSheet,
|
||||
View,
|
||||
FlatList,
|
||||
RefreshControl,
|
||||
ToastAndroid,
|
||||
} from "react-native";
|
||||
import { Provider, Portal } from "react-native-paper";
|
||||
|
||||
import ChapterCard from "../components/novel/ChapterCard";
|
||||
import NovelInfoHeader from "../components/novel/NovelHeader";
|
||||
import { BottomSheet } from "../components/novel/BottomSheet";
|
||||
|
||||
import { fetchNovelFromSource, fetchChaptersFromSource } from "../services/api";
|
||||
import {
|
||||
insertNovelInfoInDb,
|
||||
insertChaptersInDb,
|
||||
getChaptersFromDb,
|
||||
checkNovelInDb,
|
||||
getNovelInfoFromDb,
|
||||
toggleFavourite,
|
||||
downloadOrDeleteChapter,
|
||||
} from "../services/db";
|
||||
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
const NovelItem = ({ route, navigation }) => {
|
||||
const item = route.params;
|
||||
const theme = useSelector((state) => state.themeReducer.theme);
|
||||
|
||||
const {
|
||||
extensionId,
|
||||
novelUrl,
|
||||
navigatingFrom,
|
||||
novelName,
|
||||
novelCover,
|
||||
} = route.params;
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
const [novel, setNovel] = useState(item);
|
||||
const [chapters, setChapters] = useState();
|
||||
|
||||
const [libraryStatus, setlibraryStatus] = useState(item.libraryStatus);
|
||||
|
||||
const [sort, setSort] = useState("");
|
||||
const [filter, setFilter] = useState("");
|
||||
|
||||
const [downloading, setDownloading] = useState({
|
||||
downloading: false,
|
||||
chapterUrl: "",
|
||||
});
|
||||
|
||||
let _panel = useRef(null); // Bottomsheet ref
|
||||
|
||||
/**
|
||||
* Fetch novel and chapters from source and set loading false
|
||||
*/
|
||||
|
||||
const getNovelFromSource = async (extensionId, novelUrl) => {
|
||||
await fetchNovelFromSource(extensionId, novelUrl).then((response) => {
|
||||
setNovel(response);
|
||||
insertNovelInfoInDb(response);
|
||||
});
|
||||
await fetchChaptersFromSource(extensionId, novelUrl).then(
|
||||
(response) => {
|
||||
setChapters(response);
|
||||
insertChaptersInDb(novelUrl, response);
|
||||
}
|
||||
);
|
||||
setlibraryStatus(0);
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get chapters from db and set loading false
|
||||
*/
|
||||
|
||||
const getChapters = async (novelUrl) => {
|
||||
await getChaptersFromDb(novelUrl, filter, sort).then((response) =>
|
||||
setChapters(response)
|
||||
);
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get novel and chapters from database
|
||||
*/
|
||||
|
||||
const getNovelFromDb = async () => {
|
||||
await getNovelInfoFromDb(novelUrl).then((res) => {
|
||||
setNovel(res);
|
||||
setlibraryStatus(res.libraryStatus);
|
||||
});
|
||||
getChapters(novelUrl);
|
||||
};
|
||||
|
||||
/**
|
||||
* Insert or remove novel from library
|
||||
*/
|
||||
|
||||
const insertNovelInLib = async () => {
|
||||
toggleFavourite(libraryStatus, novelUrl).then((res) => {
|
||||
setlibraryStatus(res);
|
||||
ToastAndroid.show(
|
||||
res ? "Added to library" : "Removed from library",
|
||||
ToastAndroid.SHORT
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Download chapter from source
|
||||
*
|
||||
* If downloaded then delete from db
|
||||
*/
|
||||
|
||||
const downloadChapter = (
|
||||
downloadStatus,
|
||||
extensionId,
|
||||
novelUrl,
|
||||
chapterUrl,
|
||||
chapterName
|
||||
) => {
|
||||
setDownloading({ downloading: true, chapterUrl: chapterUrl });
|
||||
downloadOrDeleteChapter(
|
||||
downloadStatus,
|
||||
extensionId,
|
||||
novelUrl,
|
||||
chapterUrl
|
||||
).then((res) => {
|
||||
setDownloading({ downloading: false });
|
||||
ToastAndroid.show(
|
||||
!downloadStatus
|
||||
? `Downloaded ${chapterName}`
|
||||
: `Deleted ${chapterName}`,
|
||||
ToastAndroid.SHORT
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the novel is in db. If not, fetch from source
|
||||
*
|
||||
* If navigating from library then directly get chapters from db
|
||||
*/
|
||||
|
||||
const checkIfExistsInDb = () => {
|
||||
if (navigatingFrom === 1) {
|
||||
getChapters(novelUrl);
|
||||
} else {
|
||||
setRefreshing(true);
|
||||
checkNovelInDb(novelUrl).then((res) => {
|
||||
if (res) {
|
||||
getNovelFromDb(novelUrl);
|
||||
} else {
|
||||
getNovelFromSource(extensionId, novelUrl);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
checkIfExistsInDb();
|
||||
}, [sort, filter])
|
||||
);
|
||||
|
||||
const onRefresh = async () => {
|
||||
setRefreshing(true);
|
||||
checkIfExistsInDb();
|
||||
};
|
||||
|
||||
const renderChapterCard = ({ item }) => (
|
||||
<ChapterCard
|
||||
navigation={navigation}
|
||||
novelUrl={novelUrl}
|
||||
extensionId={extensionId}
|
||||
chapter={item}
|
||||
downloadChapter={downloadChapter}
|
||||
downloading={downloading}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Provider>
|
||||
<View
|
||||
style={[
|
||||
styles.container,
|
||||
{ backgroundColor: theme.colorDarkPrimaryDark },
|
||||
]}
|
||||
>
|
||||
<FlatList
|
||||
data={chapters}
|
||||
extraData={[sort, filter]}
|
||||
showsVerticalScrollIndicator={false}
|
||||
keyExtractor={(item) => item.chapterUrl}
|
||||
removeClippedSubviews={true}
|
||||
maxToRenderPerBatch={10}
|
||||
windowSize={15}
|
||||
initialNumToRender={7}
|
||||
renderItem={renderChapterCard}
|
||||
ListHeaderComponent={() => (
|
||||
<NovelInfoHeader
|
||||
item={{
|
||||
novelCover: novelCover,
|
||||
novelName: novelName,
|
||||
}}
|
||||
novel={novel}
|
||||
noOfChapters={!loading && chapters.length}
|
||||
libraryStatus={libraryStatus}
|
||||
insertNovelInLib={insertNovelInLib}
|
||||
navigatingFrom={navigatingFrom}
|
||||
loading={loading}
|
||||
bottomSheetRef={_panel}
|
||||
/>
|
||||
)}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={onRefresh}
|
||||
colors={[theme.textColorPrimaryDark]}
|
||||
progressBackgroundColor={theme.colorDarkPrimary}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Portal>
|
||||
<BottomSheet
|
||||
bottomSheetRef={_panel}
|
||||
sort={sort}
|
||||
filter={filter}
|
||||
sortChapters={setSort}
|
||||
filterChapters={setFilter}
|
||||
/>
|
||||
</Portal>
|
||||
</View>
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default NovelItem;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
@@ -1,3 +1,67 @@
|
||||
/**
|
||||
* Updates Screen
|
||||
*/
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import { useFocusEffect } from "@react-navigation/native";
|
||||
import { StyleSheet, View, FlatList, ActivityIndicator } from "react-native";
|
||||
|
||||
import { connect } from "react-redux";
|
||||
import EmptyView from "../../components/EmptyView";
|
||||
import { getUpdatesAction } from "../../redux/updates/updates.actions";
|
||||
import { Appbar } from "./components/Appbar";
|
||||
import UpdateCard from "./components/UpdateCard";
|
||||
|
||||
const Updates = ({ theme, updates, loading, getUpdatesAction }) => {
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
getUpdatesAction();
|
||||
}, [getUpdatesAction])
|
||||
);
|
||||
|
||||
const renderUpdateCard = ({ item }) => <UpdateCard item={item} />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Appbar />
|
||||
<View
|
||||
style={[
|
||||
styles.container,
|
||||
{ backgroundColor: theme.colorPrimaryDark },
|
||||
]}
|
||||
>
|
||||
<FlatList
|
||||
contentContainerStyle={{ flexGrow: 1, paddingVertical: 8 }}
|
||||
data={updates}
|
||||
keyExtractor={(item) => item.updateId.toString()}
|
||||
renderItem={renderUpdateCard}
|
||||
ListFooterComponent={
|
||||
loading && (
|
||||
<ActivityIndicator
|
||||
size="small"
|
||||
color={theme.colorAccentDark}
|
||||
/>
|
||||
)
|
||||
}
|
||||
ListEmptyComponent={
|
||||
!loading && (
|
||||
<EmptyView
|
||||
icon="(˘・_・˘)"
|
||||
description="No recent updates"
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
theme: state.themeReducer.theme,
|
||||
updates: state.updatesReducer.updates,
|
||||
loading: state.updatesReducer.loading,
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, { getUpdatesAction })(Updates);
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import React from "react";
|
||||
import { Appbar as MaterialAppbar } from "react-native-paper";
|
||||
|
||||
import { useSelector } from "react-redux";
|
||||
import { updateAllNovels } from "../../../services/updates";
|
||||
|
||||
export const Appbar = () => {
|
||||
const theme = useSelector((state) => state.themeReducer.theme);
|
||||
|
||||
return (
|
||||
<MaterialAppbar.Header style={{ backgroundColor: theme.colorPrimary }}>
|
||||
<MaterialAppbar.Content
|
||||
title="Updates"
|
||||
titleStyle={{ color: theme.textColorPrimary }}
|
||||
/>
|
||||
<MaterialAppbar.Action icon="reload" onPress={updateAllNovels} />
|
||||
</MaterialAppbar.Header>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
import React from "react";
|
||||
import { View, Text, Image, StyleSheet } from "react-native";
|
||||
import { TouchableRipple, Button } from "react-native-paper";
|
||||
import moment from "moment";
|
||||
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
const UpdateCard = ({ item }) => {
|
||||
const navigation = useNavigation();
|
||||
|
||||
const theme = useSelector((state) => state.themeReducer.theme);
|
||||
|
||||
return (
|
||||
<TouchableRipple
|
||||
style={styles.updateCard}
|
||||
onPress={() =>
|
||||
// console.log(item);
|
||||
navigation.navigate("ChapterItem", {
|
||||
chapterId: item.chapterId,
|
||||
chapterUrl: item.chapterUrl,
|
||||
extensionId: item.sourceId,
|
||||
novelUrl: item.novelUrl,
|
||||
chapterName: item.chapterName,
|
||||
novelId: item.novelId,
|
||||
})
|
||||
}
|
||||
rippleColor={theme.rippleColor}
|
||||
borderless
|
||||
>
|
||||
<>
|
||||
<Image
|
||||
source={{ uri: item.novelCover }}
|
||||
style={styles.updateIcon}
|
||||
/>
|
||||
<View style={styles.extensionDetails}>
|
||||
<View>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorPrimary,
|
||||
fontSize: 14,
|
||||
}}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{item.novelName}
|
||||
</Text>
|
||||
<View style={{ flexDirection: "row" }}>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorPrimary,
|
||||
fontSize: 12,
|
||||
}}
|
||||
>
|
||||
{`${item.chapterName} · ${moment(
|
||||
item.updateTime
|
||||
).calendar()}`}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
</TouchableRipple>
|
||||
);
|
||||
};
|
||||
|
||||
export default UpdateCard;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
updateCard: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 12,
|
||||
// borderRadius: 4,
|
||||
},
|
||||
updateIcon: {
|
||||
width: 42,
|
||||
height: 42,
|
||||
borderRadius: 4,
|
||||
},
|
||||
extensionDetails: {
|
||||
flex: 1,
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginLeft: 16,
|
||||
},
|
||||
});
|
||||
@@ -1,238 +0,0 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { StyleSheet, View, FlatList, ActivityIndicator } from "react-native";
|
||||
import { Appbar, Provider, Portal } from "react-native-paper";
|
||||
|
||||
import NovelCover from "../../../components/common/NovelCover";
|
||||
import SearchBar from "../../../components/common/SearchBar";
|
||||
import { BottomSheet } from "./filters/BottomSheet";
|
||||
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
import * as SQLite from "expo-sqlite";
|
||||
const db = SQLite.openDatabase("lnreader.db");
|
||||
|
||||
const AllNovels = ({ navigation }) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [loadingMore, setLoadingMore] = useState(false);
|
||||
|
||||
const [novels, setNovels] = useState([]);
|
||||
const [libraryNovels, setlibraryNovels] = useState([]);
|
||||
|
||||
const [sort, setSort] = useState("rating");
|
||||
const [pageNo, setPageNo] = useState(1);
|
||||
|
||||
const [searchBar, setSearchBar] = useState(false);
|
||||
const [searchText, setSearchText] = useState("");
|
||||
|
||||
const [searched, setSearched] = useState(0);
|
||||
|
||||
const theme = useSelector((state) => state.themeReducer.theme);
|
||||
|
||||
const getNovels = () => {
|
||||
fetch(
|
||||
`https://lnreader-extensions.herokuapp.com/api/1/novels/${pageNo}/?o=${sort}`
|
||||
)
|
||||
.then((response) => response.json())
|
||||
.then((json) => {
|
||||
setPageNo(pageNo + 1);
|
||||
setNovels((novels) => novels.concat(json));
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const getLibraryNovels = () => {
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
"SELECT novelUrl FROM LibraryTable WHERE libraryStatus = 1 AND extensionId = 1",
|
||||
null,
|
||||
(tx, { rows: { _array } }) => {
|
||||
setlibraryNovels(_array);
|
||||
},
|
||||
(tx, error) => console.log(error)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const onEndReached = ({
|
||||
layoutMeasurement,
|
||||
contentOffset,
|
||||
contentSize,
|
||||
}) => {
|
||||
const paddingToBottom = 5;
|
||||
return (
|
||||
layoutMeasurement.height + contentOffset.y >=
|
||||
contentSize.height - paddingToBottom
|
||||
);
|
||||
};
|
||||
|
||||
const getSearchResults = (searchText) => {
|
||||
setLoading(true);
|
||||
fetch(
|
||||
`https://lnreader-extensions.herokuapp.com/api/1/search/?s=${searchText}&?o=${sort}`
|
||||
)
|
||||
.then((response) => response.json())
|
||||
.then((json) => {
|
||||
setNovels(json);
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const checkIFInLibrary = (id) => {
|
||||
return libraryNovels.some((obj) => obj.novelUrl === id);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getLibraryNovels();
|
||||
getNovels();
|
||||
}, [sort]);
|
||||
|
||||
const sortNovels = () => {
|
||||
setLoading(true);
|
||||
if (searched) {
|
||||
getSearchResults();
|
||||
} else {
|
||||
getNovels();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Provider>
|
||||
<Appbar.Header style={{ backgroundColor: theme.colorDarkPrimary }}>
|
||||
{!searchBar ? (
|
||||
<>
|
||||
<Appbar.BackAction
|
||||
onPress={() => navigation.goBack()}
|
||||
color={theme.textColorPrimaryDark}
|
||||
/>
|
||||
|
||||
<Appbar.Content
|
||||
title="Box Novel"
|
||||
titleStyle={{ color: theme.textColorPrimaryDark }}
|
||||
/>
|
||||
<Appbar.Action
|
||||
icon="magnify"
|
||||
onPress={() => setSearchBar(true)}
|
||||
color={theme.textColorPrimaryDark}
|
||||
/>
|
||||
<Appbar.Action
|
||||
icon="filter-variant"
|
||||
onPress={() => _panel.show({ velocity: -1.5 })}
|
||||
color={theme.textColorPrimaryDark}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Appbar.BackAction
|
||||
onPress={() => {
|
||||
if (searched) {
|
||||
setLoading(true);
|
||||
setPageNo(1);
|
||||
setNovels([]);
|
||||
getNovels();
|
||||
}
|
||||
setSearchBar(false);
|
||||
setSearchText("");
|
||||
}}
|
||||
color={theme.textColorPrimaryDark}
|
||||
/>
|
||||
<SearchBar
|
||||
searchText={searchText}
|
||||
onChangeText={(text) => setSearchText(text)}
|
||||
onSubmitEditing={() => {
|
||||
if (searchText !== "") {
|
||||
setSort("rating");
|
||||
getSearchResults(searchText);
|
||||
setSearched(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{searchText !== "" && (
|
||||
<Appbar.Action
|
||||
icon="close"
|
||||
onPress={() => {
|
||||
setSearchText("");
|
||||
}}
|
||||
color={theme.textColorPrimaryDark}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Appbar.Header>
|
||||
<View
|
||||
style={[
|
||||
styles.container,
|
||||
{ backgroundColor: theme.colorDarkPrimaryDark },
|
||||
]}
|
||||
>
|
||||
{loading ? (
|
||||
<View style={{ flex: 1, justifyContent: "center" }}>
|
||||
<ActivityIndicator
|
||||
size={60}
|
||||
color={theme.colorAccentDark}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<FlatList
|
||||
contentContainerStyle={styles.list}
|
||||
numColumns={3}
|
||||
data={novels}
|
||||
showsVerticalScrollIndicator={false}
|
||||
keyExtractor={(item) => item.novelUrl}
|
||||
ListFooterComponent={() =>
|
||||
loadingMore && (
|
||||
<ActivityIndicator
|
||||
color={theme.colorAccentDark}
|
||||
size="small"
|
||||
style={{ marginTop: 20, marginBottom: 80 }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
renderItem={({ item }) => (
|
||||
<NovelCover
|
||||
item={item}
|
||||
onPress={() => {
|
||||
navigation.navigate("NovelItem", {
|
||||
...item,
|
||||
navigatingFrom: 2,
|
||||
});
|
||||
}}
|
||||
libraryStatus={
|
||||
checkIFInLibrary(item.novelUrl)
|
||||
? true
|
||||
: false
|
||||
}
|
||||
/>
|
||||
)}
|
||||
onScroll={({ nativeEvent }) => {
|
||||
if (onEndReached(nativeEvent)) {
|
||||
setLoadingMore(true);
|
||||
getNovels();
|
||||
console.log("End Reached");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
<Portal>
|
||||
<BottomSheet
|
||||
bottomSheetRef={(c) => (_panel = c)}
|
||||
setSort={setSort}
|
||||
sort={sort}
|
||||
/>
|
||||
</Portal>
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default AllNovels;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
padding: 3,
|
||||
},
|
||||
|
||||
contentContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
@@ -1,185 +0,0 @@
|
||||
import React from "react";
|
||||
import { StyleSheet, View } from "react-native";
|
||||
import { List } from "react-native-paper";
|
||||
import Bottomsheet from "rn-sliding-up-panel";
|
||||
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
export const BottomSheet = ({
|
||||
bottomSheetRef,
|
||||
setSort,
|
||||
sort,
|
||||
setRefreshing,
|
||||
}) => {
|
||||
const theme = useSelector((state) => state.themeReducer.theme);
|
||||
|
||||
return (
|
||||
<Bottomsheet
|
||||
ref={bottomSheetRef}
|
||||
draggableRange={{ top: 370, bottom: 0 }}
|
||||
snappingPoints={[0, 370]}
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
styles.contentContainer,
|
||||
{
|
||||
backgroundColor: theme.colorDarkPrimary,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: theme.textColorHintDark,
|
||||
height: 5,
|
||||
width: 30,
|
||||
borderRadius: 50,
|
||||
top: 10,
|
||||
alignSelf: "center",
|
||||
}}
|
||||
/>
|
||||
<List.Section
|
||||
title="Sort"
|
||||
titleStyle={{
|
||||
color: theme.colorAccentDark,
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
style={{
|
||||
color: theme.colorAccentDark,
|
||||
// backgroundColor: theme.colorAccentDark,
|
||||
}}
|
||||
expanded
|
||||
>
|
||||
<List.Item
|
||||
right={(props) =>
|
||||
sort === "latest" && (
|
||||
<List.Icon
|
||||
{...props}
|
||||
size
|
||||
icon="arrow-up"
|
||||
color={theme.colorAccentDark}
|
||||
/>
|
||||
)
|
||||
}
|
||||
title="Latest"
|
||||
titleStyle={{
|
||||
fontSize: 15,
|
||||
color: theme.textColorPrimaryDark,
|
||||
}}
|
||||
onPress={() => setSort("latest")}
|
||||
/>
|
||||
<List.Item
|
||||
right={(props) =>
|
||||
sort === "alphabet" && (
|
||||
<List.Icon
|
||||
{...props}
|
||||
size
|
||||
icon="arrow-up"
|
||||
color={theme.colorAccentDark}
|
||||
/>
|
||||
)
|
||||
}
|
||||
title="A-Z"
|
||||
titleStyle={{
|
||||
fontSize: 15,
|
||||
color: theme.textColorPrimaryDark,
|
||||
}}
|
||||
onPress={() => {
|
||||
setSort("alphabet");
|
||||
setRefreshing(true);
|
||||
}}
|
||||
/>
|
||||
<List.Item
|
||||
right={(props) =>
|
||||
sort === "rating" && (
|
||||
<List.Icon
|
||||
{...props}
|
||||
size
|
||||
icon="arrow-up"
|
||||
color={theme.colorAccentDark}
|
||||
/>
|
||||
)
|
||||
}
|
||||
title="Rating"
|
||||
titleStyle={{
|
||||
fontSize: 15,
|
||||
color: theme.textColorPrimaryDark,
|
||||
}}
|
||||
onPress={() => {
|
||||
setSort("rating");
|
||||
setRefreshing(true);
|
||||
}}
|
||||
/>
|
||||
<List.Item
|
||||
right={(props) =>
|
||||
sort === "trending" && (
|
||||
<List.Icon
|
||||
{...props}
|
||||
size
|
||||
icon="arrow-up"
|
||||
color={theme.colorAccentDark}
|
||||
/>
|
||||
)
|
||||
}
|
||||
title="Trending"
|
||||
titleStyle={{
|
||||
fontSize: 15,
|
||||
color: theme.textColorPrimaryDark,
|
||||
}}
|
||||
onPress={() => {
|
||||
setSort("trending");
|
||||
setRefreshing(true);
|
||||
}}
|
||||
/>
|
||||
<List.Item
|
||||
right={(props) =>
|
||||
sort === "views" && (
|
||||
<List.Icon
|
||||
{...props}
|
||||
size
|
||||
icon="arrow-up"
|
||||
color={theme.colorAccentDark}
|
||||
/>
|
||||
)
|
||||
}
|
||||
title="Most Views"
|
||||
titleStyle={{
|
||||
fontSize: 15,
|
||||
color: theme.textColorPrimaryDark,
|
||||
}}
|
||||
onPress={() => {
|
||||
setSort("views");
|
||||
setRefreshing(true);
|
||||
}}
|
||||
/>
|
||||
<List.Item
|
||||
right={(props) =>
|
||||
sort === "new-manga" && (
|
||||
<List.Icon
|
||||
{...props}
|
||||
size
|
||||
icon="arrow-up"
|
||||
color={theme.colorAccentDark}
|
||||
/>
|
||||
)
|
||||
}
|
||||
title="New"
|
||||
titleStyle={{
|
||||
fontSize: 15,
|
||||
color: theme.textColorPrimaryDark,
|
||||
}}
|
||||
onPress={() => {
|
||||
setSort("new-manga");
|
||||
setRefreshing(true);
|
||||
}}
|
||||
/>
|
||||
</List.Section>
|
||||
</View>
|
||||
</Bottomsheet>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
contentContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
@@ -1,183 +0,0 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { StyleSheet, View, FlatList, ActivityIndicator } from "react-native";
|
||||
import { Appbar, Provider } from "react-native-paper";
|
||||
|
||||
import NovelCover from "../../../components/common/NovelCover";
|
||||
import SearchBar from "../../../components/common/SearchBar";
|
||||
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
import * as SQLite from "expo-sqlite";
|
||||
const db = SQLite.openDatabase("lnreader.db");
|
||||
|
||||
const ReadLightNovel = ({ navigation }) => {
|
||||
const theme = useSelector((state) => state.themeReducer.theme);
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const [novels, setNovels] = useState();
|
||||
const [libraryNovels, setlibraryNovels] = useState([]);
|
||||
|
||||
const [searchBar, setSearchBar] = useState(false);
|
||||
const [searchText, setSearchText] = useState("");
|
||||
|
||||
const [searched, setSearched] = useState(false);
|
||||
|
||||
const getNovels = () => {
|
||||
fetch(`https://lnreader-extensions.herokuapp.com/api/2/novels/`)
|
||||
.then((response) => response.json())
|
||||
.then((json) => {
|
||||
setNovels(json);
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const getSearchResults = (searchText) => {
|
||||
setLoading(true);
|
||||
fetch(
|
||||
`https://lnreader-extensions.herokuapp.com/api/2/search/?s=${searchText}`
|
||||
)
|
||||
.then((response) => response.json())
|
||||
.then((json) => {
|
||||
setNovels(json);
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const getLibraryNovels = () => {
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
"SELECT novelUrl FROM LibraryTable WHERE libraryStatus = 1 AND extensionId = 2",
|
||||
null,
|
||||
(tx, { rows: { _array } }) => {
|
||||
setlibraryNovels(_array);
|
||||
},
|
||||
(tx, error) => console.log(error)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const checkIFInLibrary = (id) => {
|
||||
return libraryNovels.some((obj) => obj.novelUrl === id);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getNovels();
|
||||
getLibraryNovels();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Provider>
|
||||
<Appbar.Header style={{ backgroundColor: theme.colorDarkPrimary }}>
|
||||
{!searchBar ? (
|
||||
<>
|
||||
<Appbar.BackAction
|
||||
color={theme.textColorPrimaryDark}
|
||||
onPress={() => navigation.goBack()}
|
||||
/>
|
||||
|
||||
<Appbar.Content
|
||||
color={theme.textColorPrimaryDark}
|
||||
title="ReadLightNovel"
|
||||
titleStyle={{ color: theme.textColorPrimaryDark }}
|
||||
/>
|
||||
<Appbar.Action
|
||||
color={theme.textColorPrimaryDark}
|
||||
icon="magnify"
|
||||
onPress={() => setSearchBar(true)}
|
||||
/>
|
||||
<Appbar.Action
|
||||
icon="filter-variant"
|
||||
disabled
|
||||
color={theme.textColorPrimaryDark}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Appbar.BackAction
|
||||
onPress={() => {
|
||||
if (searched) {
|
||||
setLoading(true);
|
||||
getNovels();
|
||||
}
|
||||
setSearchBar(false);
|
||||
setSearchText("");
|
||||
}}
|
||||
color={theme.textColorPrimaryDark}
|
||||
/>
|
||||
<SearchBar
|
||||
searchText={searchText}
|
||||
onChangeText={(text) => setSearchText(text)}
|
||||
onSubmitEditing={() => {
|
||||
if (searchText !== "") {
|
||||
getSearchResults(searchText);
|
||||
setSearched(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{searchText !== "" && (
|
||||
<Appbar.Action
|
||||
icon="close"
|
||||
onPress={() => {
|
||||
setSearchText("");
|
||||
}}
|
||||
color={theme.textColorPrimaryDark}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Appbar.Header>
|
||||
<View
|
||||
style={[
|
||||
styles.container,
|
||||
{ backgroundColor: theme.colorDarkPrimaryDark },
|
||||
]}
|
||||
>
|
||||
{loading ? (
|
||||
<View style={{ flex: 1, justifyContent: "center" }}>
|
||||
<ActivityIndicator
|
||||
size={60}
|
||||
color={theme.colorAccentDark}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<FlatList
|
||||
contentContainerStyle={styles.list}
|
||||
numColumns={3}
|
||||
data={novels}
|
||||
showsVerticalScrollIndicator={false}
|
||||
keyExtractor={(item) => item.novelUrl}
|
||||
renderItem={({ item }) => (
|
||||
<NovelCover
|
||||
item={item}
|
||||
onPress={() =>
|
||||
navigation.navigate("NovelItem", {
|
||||
...item,
|
||||
navigatingFrom: 2,
|
||||
})
|
||||
}
|
||||
libraryStatus={
|
||||
checkIFInLibrary(item.novelUrl)
|
||||
? true
|
||||
: false
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReadLightNovel;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
padding: 4,
|
||||
},
|
||||
contentContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
@@ -1,47 +0,0 @@
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import { setStatusBarStyle } from "expo-status-bar";
|
||||
|
||||
export const saveReaderTheme = async (val) => {
|
||||
AsyncStorage.setItem("@reader_theme", JSON.stringify(val));
|
||||
};
|
||||
|
||||
export const getReaderTheme = async () => {
|
||||
let theme = await AsyncStorage.getItem("@reader_theme").then((value) => {
|
||||
if (value) return JSON.parse(value);
|
||||
});
|
||||
|
||||
return theme;
|
||||
};
|
||||
|
||||
export const setAppTheme = async (val) => {
|
||||
AsyncStorage.setItem("@application_theme", JSON.stringify(val));
|
||||
};
|
||||
|
||||
export const getAppTheme = async () => {
|
||||
let theme = await AsyncStorage.getItem("@application_theme").then(
|
||||
(value) => {
|
||||
if (value) return JSON.parse(value);
|
||||
}
|
||||
);
|
||||
|
||||
return theme;
|
||||
};
|
||||
|
||||
export const getDisplayMode = async () => {
|
||||
let displayMode = await AsyncStorage.getItem("@display_mode");
|
||||
return displayMode;
|
||||
};
|
||||
|
||||
export const setDisplayMode = async (val) => {
|
||||
AsyncStorage.setItem("@display_mode", val);
|
||||
};
|
||||
|
||||
export const setStatusBar = () => {
|
||||
AsyncStorage.getItem("@application_theme").then((value) => {
|
||||
if (JSON.parse(value) === "lightTheme") {
|
||||
setStatusBarStyle("dark");
|
||||
} else {
|
||||
setStatusBarStyle("light");
|
||||
}
|
||||
});
|
||||
};
|
||||
+15
-3
@@ -20,9 +20,11 @@ export const fetchNovelFromSource = async (extensionId, novelUrl) => {
|
||||
sourceUrl: res.sourceUrl,
|
||||
source: res.sourceName,
|
||||
unread: 1,
|
||||
libraryStatus: 0,
|
||||
lastRead: res.novelChapters[0].chapterUrl,
|
||||
lastReadName: res.novelChapters[0].chapterName,
|
||||
extensionId: res.extensionId,
|
||||
novelChapters: res.novelChapters,
|
||||
};
|
||||
|
||||
return novel;
|
||||
@@ -53,12 +55,22 @@ export const fetchChapterFromSource = async (
|
||||
chapterUrl
|
||||
) => {
|
||||
const url = `https://lnreader-extensions.herokuapp.com/api/${extensionId}/novel/${novelUrl}${chapterUrl}`;
|
||||
console.log(
|
||||
`https://lnreader-extensions.herokuapp.com/api/${extensionId}/novel/${novelUrl}${chapterUrl}`
|
||||
);
|
||||
|
||||
let res = await fetch(url);
|
||||
let chapter = await res.json();
|
||||
|
||||
return chapter;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch list of extensions
|
||||
*/
|
||||
|
||||
export const fetchExtensionList = async () => {
|
||||
const url = `https://lnreader-extensions.herokuapp.com/api/`;
|
||||
|
||||
let res = await fetch(url);
|
||||
let extensions = await res.json();
|
||||
|
||||
return extensions;
|
||||
};
|
||||
|
||||
@@ -1,375 +0,0 @@
|
||||
import * as SQLite from "expo-sqlite";
|
||||
const db = SQLite.openDatabase("lnreader.db");
|
||||
|
||||
/**
|
||||
* Create tables
|
||||
*/
|
||||
|
||||
export const createTables = async () => {
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
"CREATE TABLE IF NOT EXISTS LibraryTable (novelId INTEGER PRIMARY KEY AUTOINCREMENT,novelUrl VARCHAR(255) UNIQUE, novelName VARCHAR(255), novelCover VARCHAR(255), novelSummary TEXT, `Author(s)` VARCHAR(255), `Genre(s)` VARCHAR(255), Status VARCHAR(255), extensionId INTEGER, libraryStatus INTEGER DEFAULT 0, unread INTEGER DEFAULT 1, lastRead VARCHAR(255), lastReadName VARCHAR(255), sourceUrl VARCHAR(255), source VARCHAR(255))",
|
||||
null,
|
||||
(tx, results) => console.log("Library Table Created"),
|
||||
(tx, error) => console.log(error)
|
||||
);
|
||||
|
||||
tx.executeSql(
|
||||
"CREATE TABLE IF NOT EXISTS ChapterTable (chapterId INTEGER PRIMARY KEY AUTOINCREMENT, chapterUrl VARCHAR(255), chapterName VARCHAR(255), releaseDate VARCHAR(255), `read` INTEGER DEFAULT 0, downloaded INTEGER DEFAULT 0,novelUrl VARCHAR(255), FOREIGN KEY (novelUrl) REFERENCES LibraryTable(novelUrl) ON DELETE CASCADE)",
|
||||
null,
|
||||
(tx, results) => console.log("Chapter Table Created"),
|
||||
(tx, error) => console.log(error)
|
||||
);
|
||||
tx.executeSql(
|
||||
"CREATE TABLE IF NOT EXISTS HistoryTable (historyId INTEGER PRIMARY KEY AUTOINCREMENT, chapterUrl VARCHAR(255), novelUrl VARCHAR(255) UNIQUE, chapterName VARCHAR(255), lastRead DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (novelUrl) REFERENCES LibraryTable(novelUrl) ON DELETE CASCADE)",
|
||||
null,
|
||||
(tx, results) => console.log("History Table Created"),
|
||||
(tx, error) => console.log(error)
|
||||
);
|
||||
|
||||
tx.executeSql(
|
||||
"CREATE TABLE IF NOT EXISTS DownloadsTable (downloadId INTEGER PRIMARY KEY AUTOINCREMENT, chapterUrl VARCHAR(255) UNIQUE, novelUrl VARCHAR(255), chapterName VARCHAR(255), chapterText VARCHAR(255), nextChapter VARCHAR(255), prevChapter VARCHAR(255), FOREIGN KEY (novelUrl) REFERENCES LibraryTable(novelUrl) ON DELETE CASCADE)",
|
||||
null,
|
||||
(tx, results) => console.log("Downloads Table Created"),
|
||||
(tx, error) => console.log(error)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Query to create index novelUrl and chapterUrl
|
||||
*/
|
||||
|
||||
export const createIndexes = async () => {
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
"CREATE INDEX IF NOT EXISTS LibraryTable_novelUrl ON LibraryTable(novelUrl)",
|
||||
null,
|
||||
(tx, results) => console.log("Library Index Created"),
|
||||
(tx, error) => console.log(error)
|
||||
);
|
||||
|
||||
tx.executeSql(
|
||||
"CREATE INDEX IF NOT EXISTS ChapterTable_novelUrl ON ChapterTable(novelUrl)",
|
||||
null,
|
||||
(tx, results) => console.log("Chapter Index Created"),
|
||||
(tx, error) => console.log(error)
|
||||
);
|
||||
tx.executeSql(
|
||||
"CREATE INDEX IF NOT EXISTS HistoryTable_novelUrl ON HistoryTable(novelUrl)",
|
||||
null,
|
||||
(tx, results) => console.log("History Index Created"),
|
||||
(tx, error) => console.log(error)
|
||||
);
|
||||
|
||||
tx.executeSql(
|
||||
"CREATE INDEX IF NOT EXISTS DownloadsTable_chapterUrl ON DownloadsTable(chapterUrl)",
|
||||
null,
|
||||
(tx, results) => console.log("Downloads Index Created"),
|
||||
(tx, error) => console.log(error)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export const insertNovelInfoInDb = (novel) => {
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
"INSERT INTO LibraryTable (novelUrl, novelName, novelCover, novelSummary, `Author(s)`, `Genre(s)`, Status, extensionId, lastRead, lastReadName, sourceUrl, source) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
[
|
||||
novel.novelUrl,
|
||||
novel.novelName,
|
||||
novel.novelCover,
|
||||
novel.novelSummary,
|
||||
novel["Author(s)"],
|
||||
novel["Genre(s)"],
|
||||
novel.Status,
|
||||
novel.extensionId,
|
||||
novel.lastRead,
|
||||
novel.lastReadName,
|
||||
novel.sourceUrl,
|
||||
novel.source,
|
||||
],
|
||||
(txObj, res) => {
|
||||
console.log("Inserted Novel Info in Db");
|
||||
},
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export const insertChaptersInDb = (novelUrl, chapters) => {
|
||||
db.transaction((tx) => {
|
||||
chapters.map((chapter) =>
|
||||
tx.executeSql(
|
||||
"INSERT INTO ChapterTable (chapterUrl, chapterName, releaseDate, novelUrl) values (?, ?, ?, ?)",
|
||||
[
|
||||
chapter.chapterUrl,
|
||||
chapter.chapterName,
|
||||
chapter.releaseDate,
|
||||
novelUrl,
|
||||
]
|
||||
)
|
||||
);
|
||||
});
|
||||
console.log("Inserted Chapters in Db");
|
||||
};
|
||||
|
||||
/**
|
||||
* Get novel info from db
|
||||
*/
|
||||
|
||||
export const getNovelInfoFromDb = (novelUrl) => {
|
||||
return new Promise((resolve, reject) =>
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
`SELECT * FROM LibraryTable WHERE novelUrl=?`,
|
||||
[novelUrl],
|
||||
(txObj, { rows }) => {
|
||||
resolve(rows.item(0));
|
||||
},
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get novel chapters from db
|
||||
*/
|
||||
|
||||
export const getChaptersFromDb = (novelUrl, filter, sort) => {
|
||||
return new Promise((resolve, reject) =>
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
`SELECT * FROM ChapterTable WHERE novelUrl=? ${filter} ${sort}`,
|
||||
[novelUrl],
|
||||
(txObj, { rows: { _array } }) => {
|
||||
resolve(_array);
|
||||
},
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if novel is cached
|
||||
*/
|
||||
|
||||
export const checkNovelInDb = (novelUrl) => {
|
||||
return new Promise((resolve, reject) =>
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
"SELECT * FROM LibraryTable WHERE novelUrl=? LIMIT 1",
|
||||
[novelUrl],
|
||||
(txObj, res) => {
|
||||
if (res.rows.length !== 0) {
|
||||
resolve(true);
|
||||
} else {
|
||||
resolve(false);
|
||||
}
|
||||
},
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Updte library status of novel
|
||||
*/
|
||||
|
||||
export const toggleFavourite = async (libraryStatus, novelUrl) => {
|
||||
return new Promise((resolve, reject) =>
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
"UPDATE LibraryTable SET libraryStatus = ? WHERE novelUrl=?",
|
||||
[!libraryStatus, novelUrl],
|
||||
(tx, res) => {
|
||||
resolve(!libraryStatus);
|
||||
},
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch chapter from source and download or delete from db
|
||||
*/
|
||||
|
||||
export const downloadOrDeleteChapter = async (
|
||||
downloadStatus,
|
||||
extensionId,
|
||||
novelUrl,
|
||||
chapterUrl
|
||||
) => {
|
||||
if (downloadStatus === 0) {
|
||||
const response = await fetch(
|
||||
`https://lnreader-extensions.herokuapp.com/api/${extensionId}/novel/${novelUrl}${chapterUrl}`
|
||||
);
|
||||
|
||||
const chapter = await response.json();
|
||||
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
`UPDATE ChapterTable SET downloaded = 1 WHERE chapterUrl = ?`,
|
||||
[chapterUrl]
|
||||
);
|
||||
tx.executeSql(
|
||||
`INSERT INTO DownloadsTable (chapterUrl, novelUrl, chapterName, chapterText, prevChapter, nextChapter) VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
chapterUrl,
|
||||
novelUrl,
|
||||
chapter.chapterName,
|
||||
chapter.chapterText,
|
||||
chapter.prevChapter,
|
||||
chapter.nextChapter,
|
||||
],
|
||||
(tx, res) => console.log(`Downloaded Chapter ${chapterUrl}`),
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
);
|
||||
});
|
||||
} else {
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
`UPDATE ChapterTable SET downloaded = 0 WHERE chapterUrl = ?`,
|
||||
[chapterUrl]
|
||||
);
|
||||
tx.executeSql(
|
||||
`DELETE FROM DownloadsTable WHERE chapterUrl = ?`,
|
||||
[chapterUrl],
|
||||
(tx, res) => console.log(`Chapter deleted`),
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get reading history
|
||||
*/
|
||||
|
||||
export const getHistoryFromDb = async () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
"SELECT HistoryTable.chapterUrl, HistoryTable.historyId, HistoryTable.chapterName, HistoryTable.lastRead, LibraryTable.novelName, LibraryTable.novelCover, LibraryTable.novelUrl, LibraryTable.extensionId, LibraryTable.libraryStatus FROM HistoryTable INNER JOIN LibraryTable ON HistoryTable.novelUrl = LibraryTable.novelUrl ORDER BY HistoryTable.lastRead DESC",
|
||||
null,
|
||||
(txObj, { rows: { _array } }) => {
|
||||
resolve(_array);
|
||||
},
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteChapterHistory = (novelId) => {
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
"DELETE FROM HistoryTable WHERE novelUrl = ?",
|
||||
[novelId],
|
||||
(txObj, res) => {},
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get novels from library
|
||||
*/
|
||||
|
||||
export const getLibraryFromDb = async () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
"SELECT * FROM LibraryTable WHERE libraryStatus=1",
|
||||
null,
|
||||
(txObj, { rows: { _array } }) => {
|
||||
resolve(_array);
|
||||
},
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Search novel in library
|
||||
*/
|
||||
|
||||
export const searchInLibrary = async (searchText) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
`SELECT * FROM LibraryTable WHERE libraryStatus=1 AND novelName LIKE '%${searchText}%'`,
|
||||
null,
|
||||
(txObj, { rows: { _array } }) => {
|
||||
resolve(_array);
|
||||
},
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteHistory = () => {
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql("DELETE FROM HistoryTable");
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteNovelsNotInLibrary = () => {
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
"DELETE FROM LibraryTable WHERE libraryStatus=0",
|
||||
null,
|
||||
(txObj, res) => {
|
||||
console.log("Deleted");
|
||||
},
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete entire database
|
||||
*/
|
||||
export const deleteDatabase = () => {
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
"DROP TABLE LibraryTable",
|
||||
null,
|
||||
(txObj, { rows: { _array } }) => {
|
||||
console.log("DELETED LIB TABLE");
|
||||
},
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
);
|
||||
tx.executeSql(
|
||||
"DROP TABLE ChapterTable",
|
||||
null,
|
||||
(txObj, { rows: { _array } }) => {
|
||||
console.log("DELETED CHAP TABLE");
|
||||
},
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
);
|
||||
tx.executeSql(
|
||||
"DROP TABLE HistoryTable",
|
||||
null,
|
||||
(txObj, { rows: { _array } }) => {
|
||||
console.log("DELETED History TABLE");
|
||||
},
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
);
|
||||
tx.executeSql(
|
||||
"DROP TABLE DownloadsTable",
|
||||
null,
|
||||
(txObj, { rows: { _array } }) => {
|
||||
console.log("DELETED Downloads TABLE");
|
||||
ToastAndroid.show("Database deleted", ToastAndroid.SHORT);
|
||||
},
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
import * as SQLite from "expo-sqlite";
|
||||
import { getLibrary } from "../database/queries/LibraryQueries";
|
||||
import { fetchNovel } from "../source/Source";
|
||||
const db = SQLite.openDatabase("lnreader.db");
|
||||
import { ToastAndroid } from "react-native";
|
||||
|
||||
const updateNovelDetails = async (novel, novelId) => {
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
"UPDATE novels SET novelName = ?, novelCover = ?, novelSummary = ?, author = ?, artist = ?, genre = ?, status = ? WHERE novelId = ?",
|
||||
[
|
||||
novel.novelName,
|
||||
novel.novelCover,
|
||||
novel.novelSummary,
|
||||
novel.author,
|
||||
novel.artist,
|
||||
novel.genre,
|
||||
novel.status,
|
||||
novelId,
|
||||
],
|
||||
(txObj, res) => {},
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export const updateNovel = async (extensionId, novelUrl, novelId) => {
|
||||
let novel = await fetchNovel(extensionId, novelUrl);
|
||||
// console.log(novel);
|
||||
await updateNovelDetails(novel, novelId);
|
||||
|
||||
db.transaction((tx) => {
|
||||
novel.chapters.map((chapter) =>
|
||||
tx.executeSql(
|
||||
"INSERT OR IGNORE INTO chapters (chapterUrl, chapterName, releaseDate, novelId) values (?, ?, ?, ?)",
|
||||
[
|
||||
chapter.chapterUrl,
|
||||
chapter.chapterName,
|
||||
chapter.releaseDate,
|
||||
novelId,
|
||||
],
|
||||
(txObj, { insertId }) => {
|
||||
if (insertId !== -1) {
|
||||
tx.executeSql(
|
||||
"INSERT OR IGNORE INTO updates (chapterId, novelId, updateTime) values (?, ?, (datetime('now','localtime')))",
|
||||
[insertId, novelId],
|
||||
(txObj, res) => {
|
||||
console.log(
|
||||
"Inserted Chapter Id -> " +
|
||||
insertId +
|
||||
" Novel Id " +
|
||||
novelId
|
||||
);
|
||||
},
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
);
|
||||
}
|
||||
},
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export const updateAllNovels = async () => {
|
||||
ToastAndroid.show("Updating library", ToastAndroid.SHORT);
|
||||
|
||||
const libraryNovels = await getLibrary();
|
||||
|
||||
libraryNovels.map((novel, index) =>
|
||||
setTimeout(async () => {
|
||||
updateNovel(novel.sourceId, novel.novelUrl, novel.novelId);
|
||||
console.log(novel.novelName + " Updated");
|
||||
}, 3000 * index)
|
||||
);
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user