Compare commits

..

41 Commits

Author SHA1 Message Date
Rajarshee Chatterjee 4eb225cdf3 Rewrite database 2021-03-31 09:43:07 +05:30
Rajarshee Chatterjee 3cb41c8bfb Fix history table 2021-03-31 09:35:21 +05:30
Rajarshee Chatterjee 05511e073e Update redux folder structure 2021-03-30 23:39:55 +05:30
Rajarshee Chatterjee 085b9b9eb3 Fix updating novels 2021-03-30 09:17:20 +05:30
Rajarshee Chatterjee 9290d59f77 Create history queries 2021-03-29 15:59:45 +05:30
Rajarshee Chatterjee c0345ae9a9 Add downloaded and unread badges 2021-03-29 12:17:37 +05:30
Rajarshee Chatterjee c12a7f378a Move out database logic 2021-03-29 08:56:24 +05:30
Rajarshee Chatterjee d3c2d130e1 Create table and index queries 2021-03-26 09:34:25 +05:30
Rajarshee Chatterjee 134c72a683 Create database version 2 2021-03-26 08:39:39 +05:30
Rajarshee Chatterjee 4c326e35c0 Add updating a novel 2021-03-23 21:11:57 +05:30
Rajarshee Chatterjee 419420efd1 Add items per row settings 2021-03-23 18:34:58 +05:30
Rajarshee Chatterjee 871fc905ce Fix sorting and filter options 2021-03-23 07:59:08 +05:30
Rajarshee Chatterjee 5a8c652f68 Move delete chapter logic into redux 2021-03-22 21:34:04 +05:30
Rajarshee Chatterjee 97f2f84fa5 Move download chapter logic into redux 2021-03-22 20:52:39 +05:30
Rajarshee Chatterjee 65a3e8fd2d Fix statusbar 2021-03-22 14:23:32 +05:30
Rajarshee Chatterjee d32cb41475 Move some reader logic into redux 2021-03-22 09:20:20 +05:30
Rajarshee Chatterjee 5bb3be576e Fix chapters not marked as read 2021-03-22 07:38:09 +05:30
Rajarshee Chatterjee 2f09b7e29e Fix unique constraint in DownloadsTable 2021-03-22 07:26:32 +05:30
Rajarshee Chatterjee b672aba6ff Move chapter logic into redux 2021-03-21 21:15:24 +05:30
Rajarshee Chatterjee 80363d6016 Fix chapters flatlist not responding while loading 2021-03-21 18:39:16 +05:30
Rajarshee Chatterjee 195fcaf9b2 Move history logic into redux 2021-03-21 15:39:10 +05:30
Rajarshee Chatterjee 5ec08b3add Move more novel logic into redux 2021-03-21 11:11:09 +05:30
Rajarshee Chatterjee 625cdff212 Use redux for novel details and chapters 2021-03-21 09:18:49 +05:30
Rajarshee Chatterjee 83afe9c254 Refactor search and extension UI 2021-03-20 14:28:16 +05:30
Rajarshee Chatterjee 404f59323a Update version 2021-03-20 14:13:21 +05:30
Rajarshee Chatterjee 20ad556039 Change theme variables 2021-03-20 13:58:51 +05:30
Rajarshee Chatterjee 14c994486c Create ExtensionCard 2021-03-20 13:55:48 +05:30
Rajarshee Chatterjee c6884458a1 Update BoxNovel extension UI and refactor settings 2021-03-20 12:37:27 +05:30
Rajarshee Chatterjee 9dab2eccaa Update ReadLightNovel extension UI 2021-03-20 11:53:18 +05:30
Rajarshee Chatterjee 2e21a9fd33 UI improvements 2021-03-20 08:55:32 +05:30
Rajarshee Chatterjee 781e8df740 Redo previous changes 2021-03-16 14:46:41 +05:30
Rajarshee Chatterjee fa4d810ccb Revert dependency updates 2021-03-16 11:56:44 +05:30
Rajarshee Chatterjee 37f531dd93 Update dependencies 2021-03-16 10:33:16 +05:30
Rajarshee Chatterjee 1ea98ec031 Revert dependency updates 2021-03-16 09:58:15 +05:30
Rajarshee Chatterjee 65e5377345 Refactor history screen 2021-03-16 06:49:05 +05:30
Rajarshee Chatterjee 2b78350ad9 Refactor library screen 2021-03-16 06:13:10 +05:30
Rajarshee Chatterjee 69217b415d Update dependencies 2021-03-16 05:18:22 +05:30
Rajarshee Chatterjee 8c04ee3d33 Minor cleanup 2021-03-15 20:20:22 +05:30
Rajarshee Chatterjee 1f78188fd1 Use redux for library states 2021-03-15 17:10:39 +05:30
Rajarshee Chatterjee 5fb9d70036 Use modals for theme and display settings 2021-03-15 09:52:44 +05:30
Rajarshee Chatterjee b3ec043e32 Use redux to store user preferences 2021-03-15 09:04:26 +05:30
75 changed files with 3562 additions and 2433 deletions
+4 -13
View File
@@ -6,13 +6,11 @@ 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 } from "./src/database/DBHelper";
const getFonts = () =>
Font.loadAsync({
@@ -22,15 +20,8 @@ const getFonts = () =>
const App = () => {
const [fontsLoaded, setFontsLoaded] = useState(false);
setStatusBar();
const createDb = () => {
createTables();
createIndexes();
};
useEffect(() => {
createDb();
createDB();
}, []);
if (fontsLoaded) {
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 24 KiB

+413 -3
View File
@@ -2140,6 +2140,11 @@
"resolved": "https://registry.npmjs.org/@redux-saga/types/-/types-1.1.0.tgz",
"integrity": "sha512-afmTuJrylUU/0OtqzaRkbyYFFNgCF73Bvel/sw90pvGrWIZ+vyoIJqA6eMSoA6+nb443kTmulmBtC9NerXboNg=="
},
"@sqltools/formatter": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.2.tgz",
"integrity": "sha512-/5O7Fq6Vnv8L6ucmPjaWbVG1XkP4FO+w5glqfkIsq3Xw4oyNAdJddbnYodNDAfjVUvo/rrSCTom4kAND7T1o5Q=="
},
"@types/hammerjs": {
"version": "2.0.36",
"resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.36.tgz",
@@ -2172,9 +2177,9 @@
"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",
@@ -2308,6 +2313,11 @@
"resolved": "https://registry.npmjs.org/any-base/-/any-base-1.1.0.tgz",
"integrity": "sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg=="
},
"any-promise": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
"integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8="
},
"anymatch": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz",
@@ -2317,6 +2327,11 @@
"normalize-path": "^2.1.1"
}
},
"app-root-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.0.0.tgz",
"integrity": "sha512-qMcx+Gy2UZynHjOHOIXPNvpf+9cjvk3cWrBBK7zg4gH9+clobJRb9NGzcT7mQTcV/6Gm/1WelUtqxVXnNlrwcw=="
},
"argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
@@ -2826,6 +2841,116 @@
"restore-cursor": "^2.0.0"
}
},
"cli-highlight": {
"version": "2.1.10",
"resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.10.tgz",
"integrity": "sha512-CcPFD3JwdQ2oSzy+AMG6j3LRTkNjM82kzcSKzoVw6cLanDCJNlsLjeqVTOTfOfucnWv5F0rmBemVf1m9JiIasw==",
"requires": {
"chalk": "^4.0.0",
"highlight.js": "^10.0.0",
"mz": "^2.4.0",
"parse5": "^5.1.1",
"parse5-htmlparser2-tree-adapter": "^6.0.0",
"yargs": "^16.0.0"
},
"dependencies": {
"ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"requires": {
"color-convert": "^2.0.1"
}
},
"chalk": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
},
"cliui": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
"integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
"requires": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^7.0.0"
}
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"requires": {
"color-name": "~1.1.4"
}
},
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
},
"strip-ansi": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
"integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
"requires": {
"ansi-regex": "^5.0.0"
}
},
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"requires": {
"has-flag": "^4.0.0"
}
},
"wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"requires": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
}
},
"y18n": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.5.tgz",
"integrity": "sha512-hsRUr4FFrvhhRH12wOdfs38Gy7k2FFzB9qgN9v3aLykRq0dRcdcpz5C9FxdS2NuhOrI/628b/KSTJ3rwHysYSg=="
},
"yargs": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
"integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
"requires": {
"cliui": "^7.0.2",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.0",
"y18n": "^5.0.5",
"yargs-parser": "^20.2.2"
}
},
"yargs-parser": {
"version": "20.2.7",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.7.tgz",
"integrity": "sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw=="
}
}
},
"cli-spinners": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.5.0.tgz",
@@ -3241,6 +3366,11 @@
"resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz",
"integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w=="
},
"dotenv": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz",
"integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw=="
},
"ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -3849,6 +3979,11 @@
}
}
},
"figlet": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/figlet/-/figlet-1.5.0.tgz",
"integrity": "sha512-ZQJM4aifMpz6H19AW1VqvZ7l4pOE9p7i/3LyxgO2kp+PO/VcDYNqIHEMtkccqIhTXMKci4kjueJr/iCQEaT/Ww=="
},
"figures": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz",
@@ -4091,6 +4226,21 @@
"function-bind": "^1.1.1"
}
},
"has-ansi": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
"integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=",
"requires": {
"ansi-regex": "^2.0.0"
},
"dependencies": {
"ansi-regex": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8="
}
}
},
"has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
@@ -4150,6 +4300,11 @@
}
}
},
"highlight.js": {
"version": "10.7.1",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.1.tgz",
"integrity": "sha512-S6G97tHGqJ/U8DsXcEdnACbirtbx58Bx9CzIVeYli8OuswCfYI/LsXH2EiGcoGio1KAC3x4mmUwulOllJ2ZyRA=="
},
"hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
@@ -6006,6 +6161,16 @@
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz",
"integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s="
},
"mz": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
"integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
"requires": {
"any-promise": "^1.0.0",
"object-assign": "^4.0.1",
"thenify-all": "^1.0.0"
}
},
"nan": {
"version": "2.14.2",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz",
@@ -6338,6 +6503,11 @@
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
},
"parent-require": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/parent-require/-/parent-require-1.0.0.tgz",
"integrity": "sha1-dGoWdjgIOoYLDu9nMssn7UbDKXc="
},
"parse-bmfont-ascii": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/parse-bmfont-ascii/-/parse-bmfont-ascii-1.0.6.tgz",
@@ -6376,6 +6546,26 @@
"resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz",
"integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA=="
},
"parse5": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz",
"integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug=="
},
"parse5-htmlparser2-tree-adapter": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz",
"integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==",
"requires": {
"parse5": "^6.0.1"
},
"dependencies": {
"parse5": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz",
"integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw=="
}
}
},
"parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -7120,6 +7310,14 @@
"prop-types": "^15.7.2"
}
},
"react-native-input-spinner": {
"version": "1.7.7",
"resolved": "https://registry.npmjs.org/react-native-input-spinner/-/react-native-input-spinner-1.7.7.tgz",
"integrity": "sha512-6zYFU2uhChJ+JC/5yujlZu+r5PYQSYIXluUqTNR3jOCKai0dlCkWiz7d3K2rM7Q2SAIiw4bSBR6mWZIuhL+DEg==",
"requires": {
"prop-types": "^15.7.2"
}
},
"react-native-iphone-x-helper": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/react-native-iphone-x-helper/-/react-native-iphone-x-helper-1.3.1.tgz",
@@ -7270,6 +7468,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 +7491,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",
@@ -7633,6 +7841,15 @@
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
},
"sha.js": {
"version": "2.4.11",
"resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
"integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
"requires": {
"inherits": "^2.0.1",
"safe-buffer": "^5.0.1"
}
},
"shebang-command": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
@@ -8078,6 +8295,22 @@
}
}
},
"thenify": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
"integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
"requires": {
"any-promise": "^1.0.0"
}
},
"thenify-all": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
"integrity": "sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY=",
"requires": {
"thenify": ">= 3.1.0 < 4"
}
},
"throat": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/throat/-/throat-4.1.0.tgz",
@@ -8178,6 +8411,11 @@
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
},
"tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"type-fest": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz",
@@ -8188,6 +8426,131 @@
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
},
"typeorm": {
"version": "0.2.31",
"resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.2.31.tgz",
"integrity": "sha512-dVvCEVHH48DG0QPXAKfo0l6ecQrl3A8ucGP4Yw4myz4YEDMProebTQo8as83uyES+nrwCbu3qdkL4ncC2+qcMA==",
"requires": {
"@sqltools/formatter": "1.2.2",
"app-root-path": "^3.0.0",
"buffer": "^5.5.0",
"chalk": "^4.1.0",
"cli-highlight": "^2.1.10",
"debug": "^4.1.1",
"dotenv": "^8.2.0",
"glob": "^7.1.6",
"js-yaml": "^3.14.0",
"mkdirp": "^1.0.4",
"reflect-metadata": "^0.1.13",
"sha.js": "^2.4.11",
"tslib": "^1.13.0",
"xml2js": "^0.4.23",
"yargonaut": "^1.1.2",
"yargs": "^16.0.3"
},
"dependencies": {
"ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"requires": {
"color-convert": "^2.0.1"
}
},
"chalk": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
},
"cliui": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
"integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
"requires": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^7.0.0"
}
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"requires": {
"color-name": "~1.1.4"
}
},
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
},
"mkdirp": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="
},
"strip-ansi": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
"integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
"requires": {
"ansi-regex": "^5.0.0"
}
},
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"requires": {
"has-flag": "^4.0.0"
}
},
"wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"requires": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
}
},
"y18n": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.5.tgz",
"integrity": "sha512-hsRUr4FFrvhhRH12wOdfs38Gy7k2FFzB9qgN9v3aLykRq0dRcdcpz5C9FxdS2NuhOrI/628b/KSTJ3rwHysYSg=="
},
"yargs": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
"integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
"requires": {
"cliui": "^7.0.2",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.0",
"y18n": "^5.0.5",
"yargs-parser": "^20.2.2"
}
},
"yargs-parser": {
"version": "20.2.7",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.7.tgz",
"integrity": "sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw=="
}
}
},
"typescript-compare": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/typescript-compare/-/typescript-compare-0.0.2.tgz",
@@ -8694,6 +9057,53 @@
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
"yargonaut": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/yargonaut/-/yargonaut-1.1.4.tgz",
"integrity": "sha512-rHgFmbgXAAzl+1nngqOcwEljqHGG9uUZoPjsdZEs1w5JW9RXYzrSvH/u70C1JE5qFi0qjsdhnUX/dJRpWqitSA==",
"requires": {
"chalk": "^1.1.1",
"figlet": "^1.1.1",
"parent-require": "^1.0.0"
},
"dependencies": {
"ansi-regex": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8="
},
"ansi-styles": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
"integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4="
},
"chalk": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
"requires": {
"ansi-styles": "^2.2.1",
"escape-string-regexp": "^1.0.2",
"has-ansi": "^2.0.0",
"strip-ansi": "^3.0.0",
"supports-color": "^2.0.0"
}
},
"strip-ansi": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
"requires": {
"ansi-regex": "^2.0.0"
}
},
"supports-color": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
"integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc="
}
}
},
"yargs": {
"version": "15.4.1",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
+7 -2
View File
@@ -30,6 +30,7 @@
"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-input-spinner": "^1.7.7",
"react-native-offline": "^5.7.0",
"react-native-paper": "^4.5.0",
"react-native-reanimated": "^1.13.2",
@@ -40,12 +41,16 @@
"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",
"rn-sliding-up-panel": "^2.4.4"
"reflect-metadata": "^0.1.13",
"rn-sliding-up-panel": "^2.4.4",
"typeorm": "^0.2.31"
},
"devDependencies": {
"@babel/core": "~7.9.0"
"@babel/core": "~7.9.0",
"@types/node": "^14.14.35"
},
"private": true
}
+131 -9
View File
@@ -1,19 +1,141 @@
import React from "react";
import { Appbar } from "react-native-paper";
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 CustomAppbar = ({ title, onBackAction }) => {
export const Appbar = ({ title, onBackAction }) => {
const theme = useSelector((state) => state.themeReducer.theme);
return (
<Appbar.Header style={{ backgroundColor: theme.colorDarkPrimary }}>
{onBackAction && <Appbar.BackAction onPress={onBackAction} />}
<Appbar.Content
<MaterialAppbar.Header style={{ backgroundColor: theme.colorPrimary }}>
{onBackAction && (
<MaterialAppbar.BackAction onPress={onBackAction} />
)}
<MaterialAppbar.Content
title={title}
titleStyle={{ color: theme.textColorPrimaryDark }}
titleStyle={{ color: theme.textColorPrimary }}
/>
</Appbar.Header>
</MaterialAppbar.Header>
);
};
export const SearchAppbar = ({
placeholder,
getSearchResults,
setSearchText,
searchText,
getNovels,
setLoading,
screen,
onFilter,
}) => {
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("");
}}
/>
)}
<IconButton
icon="filter-variant"
color={theme.textColorSecondary}
style={{ marginRight: 0 }}
size={23}
disabled={typeof onFilter === "function" ? false : true}
/**
* TODO
*/
onPress={() => onFilter?.()}
/>
</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;
+52
View File
@@ -0,0 +1,52 @@
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,
},
});
+146 -33
View File
@@ -1,38 +1,103 @@
import React, { useState } from "react";
import { StyleSheet, View, Text, ImageBackground } from "react-native";
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 { theme } from "../../theme/theme";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { useSelector } from "react-redux";
const NovelCover = ({ item, onPress, libraryStatus }) => {
const [mode, setMode] = useState();
AsyncStorage.getItem("@display_mode").then((value) => setMode(value));
const displayMode = useSelector(
(state) => state.settingsReducer.displayMode
);
return (
<View style={{ flex: 1 / 3 }}>
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.rippleColorDark}
rippleColor={theme.rippleColor}
style={styles.opac}
onPress={onPress}
>
<>
{item.novelCover ? (
{item.novelCover && (
<ImageBackground
source={{
uri: item.novelCover,
}}
style={styles.logo}
source={{ uri: item.novelCover }}
style={{ height: height[itemsPerRow] }}
imageStyle={[
{ borderRadius: 4 },
libraryStatus && { opacity: 0.4 },
libraryStatus && { opacity: 0.5 },
]}
progressiveRenderingEnabled={true}
>
{(!mode || mode === "compact") && (
<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={[
@@ -43,7 +108,13 @@ const NovelCover = ({ item, onPress, libraryStatus }) => {
>
<Text
numberOfLines={2}
style={styles.title}
style={[
styles.title,
{
color:
"rgba(255,255,255,1)",
},
]}
>
{item.novelName}
</Text>
@@ -51,29 +122,60 @@ const NovelCover = ({ item, onPress, libraryStatus }) => {
</View>
)}
</ImageBackground>
) : (
<View>
<Text style={{ color: "white" }}>HAHAHAHAH</Text>
</View>
)}
{mode === "comfortable" && (
<Text numberOfLines={2} style={styles.title}>
{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: 173,
borderRadius: 4,
},
// logo: {
// height: 180,
// },
titleContainer: {
flex: 1,
justifyContent: "flex-end",
@@ -82,18 +184,29 @@ const styles = StyleSheet.create({
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: {
bottom: -1,
borderRadius: 4,
},
opac: {
paddingHorizontal: 3.5,
paddingVertical: 3.5,
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,
},
});
-27
View File
@@ -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,
},
});
+12 -6
View File
@@ -1,17 +1,23 @@
import React, { useState } from "react";
import React 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);
const label = {
0: "Compact Grid",
1: "Comfortable Grid",
2: "List",
};
return (
<Checkbox.Item
label={value.charAt(0).toUpperCase() + value.slice(1)}
labelStyle={{ color: theme.textColorPrimaryDark }}
label={label[value]}
labelStyle={{ color: theme.textColorPrimary }}
status={displayMode === value ? "checked" : "unchecked"}
mode="ios"
uncheckedColor={theme.textColorSecondaryDark}
uncheckedColor={theme.textColorSecondary}
color={theme.colorAccentDark}
onPress={onPress}
/>
@@ -24,10 +30,10 @@ export const ThemeCheckbox = ({ onPress, label, checked }) => {
return (
<Checkbox.Item
label={label}
labelStyle={{ color: theme.textColorPrimaryDark }}
labelStyle={{ color: theme.textColorPrimary }}
status={checked ? "checked" : "unchecked"}
mode="ios"
uncheckedColor={theme.textColorSecondaryDark}
uncheckedColor={theme.textColorSecondary}
color={theme.colorAccentDark}
onPress={onPress}
/>
+49
View File
@@ -0,0 +1,49 @@
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";
/**
* 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);
//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");
});
};
+160
View File
@@ -0,0 +1,160 @@
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,
])
);
});
};
const getChaptersQuery = `SELECT * FROM chapters WHERE novelId = ? ORDER BY chapterId ASC`;
export const getChapters = (novelId) => {
return new Promise((resolve, reject) =>
db.transaction((tx) => {
tx.executeSql(
getChaptersQuery,
[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}`;
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)
);
});
};
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)
);
});
});
};
+56
View File
@@ -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) VALUES (?, ?)",
[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;");
});
};
+54
View File
@@ -0,0 +1,54 @@
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 * FROM novels WHERE followed = 1 AND novelName LIKE '%${searchText}%'`,
null,
(txObj, { rows: { _array } }) => resolve(_array),
(txObj, error) => console.log("Error ", error)
);
});
});
};
+87
View File
@@ -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("Deleted");
},
(txObj, error) => console.log("Error ", error)
);
});
};
+18
View File
@@ -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\`)`;
+11
View File
@@ -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(chapter_id)
ON DELETE CASCADE
)`;
+11
View File
@@ -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)`;
+21
View File
@@ -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)`;
+44 -35
View File
@@ -1,26 +1,28 @@
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 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 BoxNovel from "../screens/Extensions/boxnovel/BoxNovel";
import ReadLightNovel from "../screens/Extensions/readlightnovel/ReadLightNovel";
const Stack = createStackNavigator();
@@ -61,10 +63,11 @@ 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
@@ -138,30 +141,36 @@ const BottomNavigator = () => {
};
const Router = () => {
const themeCode = useSelector((state) => state.themeReducer.themeCode);
useEffect(() => setStatusBarStyle(themeCode === 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="BoxNovelStack" component={BoxNovelStack} />
<Stack.Screen
name="ReadLightNovelStack"
component={ReadLightNovelStack}
/>
</Stack.Navigator>
</Provider>
);
};
-11
View File
@@ -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,
});
};
-8
View File
@@ -1,8 +0,0 @@
import { SWITCH_THEME } from "./types";
export const switchTheme = (theme) => (dispatch) => {
dispatch({
type: SWITCH_THEME,
payload: theme,
});
};
+81
View File
@@ -0,0 +1,81 @@
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);
}
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);
};
+65
View File
@@ -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;
+6
View File
@@ -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";
+36
View File
@@ -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 },
});
};
+41
View File
@@ -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;
+4
View File
@@ -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";
+24
View File
@@ -0,0 +1,24 @@
import { GET_LIBRARY_NOVELS } 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) => {
console.log(searchText);
const res = await searchLibrary(searchText);
dispatch({
type: GET_LIBRARY_NOVELS,
payload: res,
});
};
+32
View File
@@ -0,0 +1,32 @@
import { FOLLOW_NOVEL, UNFOLLOW_NOVEL } from "../novel/novel.types";
import { GET_LIBRARY_NOVELS } from "./library.types";
const initialState = {
novels: [],
loading: true,
};
const libraryReducer = (state = initialState, action) => {
const { type, payload } = action;
switch (type) {
case GET_LIBRARY_NOVELS:
return { novels: 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;
@@ -1,3 +1 @@
export const SWITCH_THEME = "SWITCH_THEME";
export const GET_LIBRARY_NOVELS = "GET_LIBRARY_NOVELS";
+212
View File
@@ -0,0 +1,212 @@
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,
UPDATE_LAST_READ,
FOLLOW_NOVEL,
UNFOLLOW_NOVEL,
} from "./novel.types";
import { updateNovel } from "../../services/updates";
import { fetchChapter, fetchNovel } from "../../source/Source";
import {
followNovel,
insertNovel,
checkNovelInCache,
getNovel,
} from "../../database/queries/NovelQueries";
import {
getChapters,
insertChapters,
markChapterRead,
isChapterDownloaded,
getChapter,
downloadChapter,
deleteChapter,
getLastReadChapter,
} from "../../database/queries/ChapterQueries";
export const setNovel = (novel) => async (dispatch) => {
dispatch({ type: SET_NOVEL, payload: novel });
};
export const getNovelAction = (followed, sourceId, novelUrl, novelId) => async (
dispatch
) => {
dispatch({ type: LOADING_NOVEL });
if (followed === 1) {
const chapters = await getChapters(novelId);
dispatch({
type: GET_CHAPTERS,
payload: chapters,
});
} else {
dispatch({ type: FETCHING_NOVEL });
const inCache = await checkNovelInCache(novelUrl);
if (inCache) {
const novel = await getNovel(novelUrl);
novel.chapters = await getChapters(novel.novelId);
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);
if (!novel.followed) {
dispatch({
type: FOLLOW_NOVEL,
payload: novel,
});
} else {
dispatch({
type: UNFOLLOW_NOVEL,
payload: 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 deleteChapterAction = (chapterId, chapterName) => async (
dispatch
) => {
await deleteChapter(chapterId);
dispatch({
type: CHAPTER_DELETED,
payload: { chapterId },
});
ToastAndroid.show(`Deleted ${chapterName}`, ToastAndroid.SHORT);
};
export const updateNovelAction = (extensionId, novelUrl, novelId) => async (
dispatch
) => {
dispatch({ type: FETCHING_NOVEL });
const updatedNovel = await updateNovel(extensionId, novelUrl, novelId);
dispatch({
type: UPDATE_NOVEL,
payload: { novel: updatedNovel, chapters: updatedNovel.chapters },
});
};
export const getLastReadChapterAction = (novelId) => async (dispatch) => {
const lastReadChapter = await getLastReadChapter(novelId);
dispatch({
type: UPDATE_LAST_READ,
payload: lastReadChapter,
});
};
+126
View File
@@ -0,0 +1,126 @@
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: {
...state.novel,
novelName: payload.novel.novelName,
novelUrl: payload.novel.novelUrl,
novelSummary: payload.novel.novelSummary,
author: payload.novel.author,
artist: payload.novel.artist,
genre: payload.novel.genre,
status: payload.novel.status,
},
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,
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
),
};
case UPDATE_LAST_READ:
return {
...state,
novel: { ...state.novel, lastRead: payload },
};
default:
return state;
}
};
export default novelReducer;
+19
View File
@@ -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";
-18
View File
@@ -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;
+35
View File
@@ -0,0 +1,35 @@
import {
SET_DISPLAY_MODE,
SET_ITEMS_PER_ROW,
UPDATE_READER_TEXT_SIZE,
UPDATE_READER_THEME,
} from "./settings.types";
export const setDisplayMode = (val) => (dispatch) => {
dispatch({
type: SET_DISPLAY_MODE,
payload: val,
});
};
export const setItemsPerRow = (val) => (dispatch) => {
console.log("called" + val);
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,
});
};
+56
View File
@@ -0,0 +1,56 @@
import {
SET_DISPLAY_MODE,
SET_ITEMS_PER_ROW,
UPDATE_READER_TEXT_SIZE,
UPDATE_READER_THEME,
} from "./settings.types";
const initialState = {
displayMode: 0,
itemsPerRow: 3,
reader: {
theme: 1,
textSize: 16,
},
};
/**
* 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 } };
default:
return state;
}
};
export default settingsReducer;
+4
View File
@@ -0,0 +1,4 @@
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";
+11
View File
@@ -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;
}
+1
View File
@@ -0,0 +1 @@
export const GET_SOURCES = "GET_SOURCES";
+32
View File
@@ -0,0 +1,32 @@
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";
const persistConfig = {
key: "root",
storage: AsyncStorage,
blacklist: ["novelReducer"],
};
const persistedReducer = persistReducer(
persistConfig,
combineReducers({
themeReducer,
settingsReducer,
libraryReducer,
extensionReducer,
novelReducer,
historyReducer,
})
);
export const store = createStore(persistedReducer, applyMiddleware(thunk));
export const persistor = persistStore(store);
-20
View File
@@ -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);
+15
View File
@@ -0,0 +1,15 @@
import { SWITCH_THEME } from "./theme.types";
/**
* 0 -> Amoled Dark Theme
* 1 -> Light Theme
* 2 -> Dark Theme
* 3 -> Midnight Dusk Theme
*/
export const switchTheme = (val) => (dispatch) => {
dispatch({
type: SWITCH_THEME,
payload: val,
});
};
+31
View File
@@ -0,0 +1,31 @@
import {
amoledDarkTheme,
lightTheme,
darkTheme,
midnightDuskTheme,
} from "../../theme/theme";
const themes = {
0: amoledDarkTheme,
1: lightTheme,
2: darkTheme,
3: midnightDuskTheme,
};
const initialState = {
themeCode: 0,
theme: themes[0],
};
const themeReducer = (state = initialState, action) => {
const { type, payload } = action;
switch (type) {
case "SWITCH_THEME":
return { ...state, themeCode: payload, theme: themes[payload] };
default:
return state;
}
};
export default themeReducer;
+1
View File
@@ -0,0 +1 @@
export const SWITCH_THEME = "SWITCH_THEME";
-144
View File
@@ -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",
},
});
+56
View File
@@ -0,0 +1,56 @@
import React, { useEffect } from "react";
import { StyleSheet, View, FlatList, ActivityIndicator } from "react-native";
import { Appbar } from "../../components/common/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();
}, []);
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,
paddingHorizontal: 4,
},
});
@@ -0,0 +1,92 @@
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 } = item;
const navigation = useNavigation();
const theme = useSelector((state) => state.themeReducer.theme);
return (
<TouchableRipple
style={styles.extensionCard}
onPress={() => navigation.navigate(sourceName + "Stack")}
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>
<Text
style={{
color: theme.textColorSecondary,
fontSize: 12,
}}
>
{sourceLanguage}
</Text>
</View>
<View>
<Button
labelStyle={{
letterSpacing: 0,
}}
uppercase={false}
color={theme.colorAccentDark}
onPress={() =>
navigation.navigate(sourceName + "Stack")
}
>
Browse
</Button>
</View>
</View>
</>
</TouchableRipple>
);
};
export default ExtensionCard;
const styles = StyleSheet.create({
extensionCard: {
flexDirection: "row",
alignItems: "center",
marginVertical: 4,
paddingVertical: 8,
paddingHorizontal: 20,
borderRadius: 4,
},
extensionIcon: {
width: 40,
height: 40,
borderRadius: 8,
},
extensionDetails: {
flex: 1,
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginLeft: 16,
},
});
+202
View File
@@ -0,0 +1,202 @@
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,
16: 25,
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.push("ChapterItem", {
chapterUrl: chapter.prevChapter,
extensionId,
novelUrl,
chapterName: chapter.chapterName,
});
}}
color="#FFFFFF"
/>
<Appbar.Action
icon="chevron-right"
size={26}
disabled={!chapter.nextChapter}
onPress={() => {
navigation.push("ChapterItem", {
chapterUrl: chapter.nextChapter,
extensionId,
novelUrl,
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),
},
]}
>
{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,
paddingHorizontal: 10,
paddingVertical: 10,
},
});
@@ -5,19 +5,21 @@ 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 { connect } from "react-redux";
import { saveReaderTheme } from "../../services/asyncStorage";
import {
updateReaderTextSize,
updateReaderTheme,
} from "../../../redux/settings/settings.actions";
import BottomSheetHandle from "../../../components/common/BottomSheetHandle";
const ChapterBottomSheet = ({
bottomSheetRef,
setSize,
size,
readerTheme,
setReaderTheme,
theme,
reader,
updateReaderTextSize,
updateReaderTheme,
}) => {
const theme = useSelector((state) => state.themeReducer.theme);
return (
<Bottomsheet
animatedValue={new Animated.Value(0)}
@@ -26,29 +28,12 @@ const ChapterBottomSheet = ({
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 }}>
<View style={styles.contentContainer}>
<BottomSheetHandle />
<View style={styles.readerSettingsContainer}>
<Text
style={{
color: theme.textColorPrimaryDark,
color: theme.textColorPrimary,
fontWeight: "bold",
}}
>
@@ -59,52 +44,49 @@ const ChapterBottomSheet = ({
width: Dimensions.get("window").width,
height: 40,
}}
value={size}
value={reader.textSize}
minimumValue={12}
maximumValue={20}
step={4}
minimumTrackTintColor={theme.colorAccentDark}
maximumTrackTintColor="#000000"
thumbTintColor={theme.colorAccentDark}
onValueChange={(value) => setSize(value)}
onValueChange={(value) => updateReaderTextSize(value)}
/>
<Text
style={{
color: theme.textColorPrimaryDark,
color: theme.textColorPrimary,
fontWeight: "bold",
}}
>
Reader Theme
</Text>
<ToggleButton.Row
onValueChange={(value) => {
setReaderTheme(value);
saveReaderTheme(value);
}}
value={readerTheme}
onValueChange={(value) => updateReaderTheme(value)}
value={reader.theme}
style={{ marginTop: 10 }}
>
<ToggleButton
icon="format-text"
color="white"
color="#FFFFFF"
value={1}
style={{
backgroundColor: "black",
backgroundColor: "#000000",
marginHorizontal: 10,
}}
/>
<ToggleButton
icon="format-text"
color="black"
color="#000000"
value={2}
style={{
backgroundColor: "white",
backgroundColor: "#FFFFFF",
marginHorizontal: 10,
}}
/>
<ToggleButton
icon="format-text"
color="black"
color="#000000"
value={3}
style={{
backgroundColor: "#F4ECD8",
@@ -118,12 +100,22 @@ const ChapterBottomSheet = ({
);
};
export default ChapterBottomSheet;
const mapStateToProps = (state) => ({
theme: state.themeReducer.theme,
reader: state.settingsReducer.reader,
});
export default connect(mapStateToProps, {
updateReaderTextSize,
updateReaderTheme,
})(ChapterBottomSheet);
const styles = StyleSheet.create({
contentContainer: {
flex: 1,
// paddingTop: ,
// alignItems: "center",
borderTopLeftRadius: 8,
borderTopRightRadius: 8,
backgroundColor: "rgba(0,0,0,0.4)",
},
readerSettingsContainer: { flex: 1, alignItems: "center", paddingTop: 30 },
});
-315
View File
@@ -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,
},
});
+48 -77
View File
@@ -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/common/Appbar";
import HistoryCard from "./components/HistoryCard";
import { connect } from "react-redux";
import EmptyView from "../../components/common/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",
},
});
@@ -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,7 +43,7 @@ const HistoryCard = ({ item, deleteHistory, navigation }) => {
</Text>
<Text
style={{
color: theme.textColorSecondaryDark,
color: theme.textColorSecondary,
marginTop: 2,
}}
numberOfLines={1}
@@ -77,16 +62,16 @@ 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", {
@@ -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",
},
});
-220
View File
@@ -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,
},
});
+149
View File
@@ -0,0 +1,149 @@
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/common/NovelCover";
import EmptyView from "../../components/common/EmptyView";
import {
getLibraryAction,
searchLibraryAction,
} from "../../redux/library/library.actions";
import { SearchAppbar } from "../../components/common/Appbar";
import { setNovel } from "../../redux/novel/novel.actions";
const LibraryScreen = ({
navigation,
theme,
novels,
loading,
getLibraryAction,
searchLibraryAction,
setNovel,
itemsPerRow,
}) => {
const [refreshing, setRefreshing] = useState(false);
const [searchText, setSearchText] = useState("");
useEffect(() => {
getLibraryAction();
}, [getLibraryAction]);
/**
* TODO: fix refreshing
*/
const onRefresh = () => {
ToastAndroid.show("Updating library", ToastAndroid.SHORT);
setRefreshing(true);
getLibraryAction();
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={novels}
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,
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,
},
});
+18 -21
View File
@@ -2,7 +2,7 @@ import React from "react";
import * as Linking from "expo-linking";
import { List, Divider } from "react-native-paper";
import { CustomAppbar } from "../../components/common/Appbar";
import { Appbar } from "../../components/common/Appbar";
import { useSelector } from "react-redux";
@@ -11,46 +11,43 @@ const AboutScreen = ({ navigation }) => {
return (
<>
<CustomAppbar
title="About"
onBackAction={() => navigation.goBack()}
/>
<Appbar title="About" onBackAction={() => navigation.goBack()} />
<List.Section
style={{
flex: 1,
marginTop: 0,
backgroundColor: theme.colorDarkPrimaryDark,
backgroundColor: theme.colorPrimaryDark,
marginBottom: 0,
}}
>
<List.Item
titleStyle={{ color: theme.textColorPrimaryDark }}
titleStyle={{ color: theme.textColorPrimary }}
title="Version"
descriptionStyle={{ color: theme.textColorSecondaryDark }}
description="Stable 1.0.17"
descriptionStyle={{ color: theme.textColorSecondary }}
description="Stable 1.0.10"
/>
<List.Item
titleStyle={{ color: theme.textColorPrimaryDark }}
titleStyle={{ color: theme.textColorPrimary }}
title="Build Time"
descriptionStyle={{ color: theme.textColorSecondaryDark }}
description="09-03-21 09:46 AM"
descriptionStyle={{ color: theme.textColorSecondary }}
description="21-03-21 11:10 AM"
/>
<List.Item
titleStyle={{ color: theme.textColorPrimaryDark }}
titleStyle={{ color: theme.textColorPrimary }}
title="What's new"
onPress={() =>
Linking.openURL(
"https://github.com/rajarsheechatterjee/lnreader/commits/master"
"https://github.com/rajarsheechatterjee/lnreader/commits/main"
)
}
rippleColor={theme.rippleColorDark}
rippleColor={theme.rippleColor}
/>
<Divider />
<List.Item
titleStyle={{ color: theme.textColorPrimaryDark }}
descriptionStyle={{ color: theme.textColorSecondaryDark }}
titleStyle={{ color: theme.textColorPrimary }}
descriptionStyle={{ color: theme.textColorSecondary }}
title="Github"
description="https://github.com/rajarsheechatterjee/lnreader"
onPress={() =>
@@ -58,11 +55,11 @@ const AboutScreen = ({ navigation }) => {
"https://github.com/rajarsheechatterjee/lnreader"
)
}
rippleColor={theme.rippleColorDark}
rippleColor={theme.rippleColor}
/>
<List.Item
titleStyle={{ color: theme.textColorPrimaryDark }}
descriptionStyle={{ color: theme.textColorSecondaryDark }}
titleStyle={{ color: theme.textColorPrimary }}
descriptionStyle={{ color: theme.textColorSecondary }}
title="Extensions"
description="https://github.com/rajarsheechatterjee/lnreader-extensions"
descriptionNumberOfLines={1}
@@ -71,7 +68,7 @@ const AboutScreen = ({ navigation }) => {
"https://github.com/rajarsheechatterjee/lnreader-extensions"
)
}
rippleColor={theme.rippleColorDark}
rippleColor={theme.rippleColor}
/>
</List.Section>
</>
+5 -5
View File
@@ -3,7 +3,7 @@ import React from "react";
import { StyleSheet } from "react-native";
import { List } from "react-native-paper";
import { CustomAppbar } from "../../components/common/Appbar";
import { Appbar } from "../../components/common/Appbar";
import { useSelector } from "react-redux";
@@ -12,17 +12,17 @@ const MoreScreen = ({ navigation }) => {
return (
<>
<CustomAppbar title="More" />
<Appbar title="More" />
<List.Section
style={{
flex: 1,
backgroundColor: theme.colorDarkPrimaryDark,
backgroundColor: theme.colorPrimaryDark,
marginTop: 0,
marginBottom: 0,
}}
>
<List.Item
titleStyle={{ color: theme.textColorPrimaryDark }}
titleStyle={{ color: theme.textColorPrimary }}
title="Settings"
left={() => (
<List.Icon
@@ -33,7 +33,7 @@ const MoreScreen = ({ navigation }) => {
onPress={() => navigation.navigate("Settings")}
/>
<List.Item
titleStyle={{ color: theme.textColorPrimaryDark }}
titleStyle={{ color: theme.textColorPrimary }}
title="About"
left={() => (
<List.Icon
+191 -122
View File
@@ -1,63 +1,81 @@
import React, { useState } from "react";
import { View, ToastAndroid } from "react-native";
import { View, Text } from "react-native";
import { setStatusBarStyle } from "expo-status-bar";
import { List } from "react-native-paper";
import { List, Modal, Portal } from "react-native-paper";
import InputSpinner from "react-native-input-spinner";
import { connect } from "react-redux";
import { switchTheme } from "../../redux/actions/theme";
import { switchTheme } from "../../redux/theme/theme.actions";
import {
amoledDarkTheme,
darkTheme,
midnightDuskTheme,
lightTheme,
} from "../../theme/theme";
setDisplayMode,
setItemsPerRow,
} from "../../redux/settings/settings.actions";
import { CustomAppbar } from "../../components/common/Appbar";
import { Appbar } from "../../components/common/Appbar";
import {
DisplayCheckbox,
ThemeCheckbox,
} from "../../components/settings/Checkbox";
import {
setAppTheme,
getAppTheme,
getDisplayMode,
import { deleteAllHistory } from "../../database/queries/HistoryQueries";
import { deleteNovelCache } from "../../database/queries/NovelQueries";
// import { deleteDb } from "../../database/DBHelper";
const SettingsScreen = ({
navigation,
theme,
themeCode,
displayMode,
itemsPerRow,
switchTheme,
setDisplayMode,
} from "../../services/asyncStorage";
import {
deleteHistory,
deleteNovelsNotInLibrary,
deleteDatabase,
} from "../../services/db";
const SettingsScreen = ({ navigation, theme, switchTheme }) => {
const desciptionStyles = {
color: theme.textColorSecondaryDark,
setItemsPerRow,
}) => {
const display = {
0: "Compact Grid",
1: "Comfortable Grid",
2: "List",
};
const titleStyles = { color: theme.textColorPrimaryDark };
const themes = [
{ themeCode: 0, label: "AMOLED Dark Theme", statusBar: "light" },
{ themeCode: 1, label: "Light Theme", statusBar: "dark" },
{ themeCode: 2, label: "Dark Theme", statusBar: "light" },
{ themeCode: 3, label: "Midnight Dusk Theme", statusBar: "light" },
];
const [displayMode, setDisplay] = useState("compact");
const [applicationTheme, setApplicationTheme] = useState("amoledDarkTheme");
const desciptionStyles = {
color: theme.textColorSecondary,
};
getAppTheme().then((res) => setApplicationTheme(res));
const titleStyles = { color: theme.textColorPrimary };
getDisplayMode().then((res) => setDisplay(res));
/**
* Display Mode Modal
*/
const [displayModalVisible, setDisplayModalVisible] = useState(false);
const showDisplayModal = () => setDisplayModalVisible(true);
const hideDisplayModal = () => setDisplayModalVisible(false);
/**
* Change Theme Modal
*/
const [themeModalVisible, setthemeModalVisible] = useState(false);
const showthemeModal = () => setthemeModalVisible(true);
const hidethemeModal = () => setthemeModalVisible(false);
const [itemsModalVisible, setitemsModalVisible] = useState(false);
const showitemsModal = () => setitemsModalVisible(true);
const hideitemsModal = () => setitemsModalVisible(false);
return (
<>
<CustomAppbar
title="Settings"
onBackAction={() => navigation.goBack()}
/>
<View
style={{ flex: 1, backgroundColor: theme.colorDarkPrimaryDark }}
>
<Appbar title="Settings" onBackAction={() => navigation.goBack()} />
<View style={{ flex: 1, backgroundColor: theme.colorPrimaryDark }}>
<List.Section
style={{
flex: 1,
backgroundColor: theme.colorDarkPrimaryDark,
backgroundColor: theme.colorPrimaryDark,
}}
>
<List.Subheader
@@ -73,16 +91,16 @@ const SettingsScreen = ({ navigation, theme, switchTheme }) => {
title="Clear database"
descriptionStyle={desciptionStyles}
description="Delete history for novels not in your library"
onPress={() => deleteNovelsNotInLibrary()}
rippleColor={theme.rippleColorDark}
onPress={() => deleteNovelCache()}
rippleColor={theme.rippleColor}
/>
<List.Item
titleStyle={titleStyles}
title="Clear history"
descriptionStyle={desciptionStyles}
description="Delete reading history for all novels"
onPress={() => deleteHistory()}
rippleColor={theme.rippleColorDark}
onPress={() => deleteAllHistory()}
rippleColor={theme.rippleColor}
/>
<List.Subheader
style={{
@@ -92,26 +110,95 @@ const SettingsScreen = ({ navigation, theme, switchTheme }) => {
>
Display
</List.Subheader>
<View>
<DisplayCheckbox
value="comfortable"
displayMode={displayMode}
onPress={() => {
setDisplay("comfortable");
setDisplayMode("comfortable");
<List.Item
titleStyle={titleStyles}
title="Display Mode"
descriptionStyle={desciptionStyles}
description={display[displayMode]}
onPress={showDisplayModal}
rippleColor={theme.rippleColor}
/>
<Portal>
<Modal
visible={displayModalVisible}
onDismiss={hideDisplayModal}
contentContainerStyle={{
backgroundColor: theme.colorPrimaryDark,
padding: 20,
margin: 20,
borderRadius: 6,
}}
/>
</View>
<View>
<DisplayCheckbox
value="compact"
displayMode={displayMode}
onPress={() => {
setDisplay("compact");
setDisplayMode("compact");
>
<DisplayCheckbox
value={1}
displayMode={displayMode}
onPress={() => setDisplayMode(1)}
/>
<DisplayCheckbox
value={0}
displayMode={displayMode}
onPress={() => setDisplayMode(0)}
/>
<DisplayCheckbox
value={2}
displayMode={displayMode}
onPress={() => setDisplayMode(2)}
/>
</Modal>
</Portal>
<List.Item
titleStyle={titleStyles}
title="Items per row in library"
descriptionStyle={desciptionStyles}
description={itemsPerRow}
onPress={showitemsModal}
rippleColor={theme.rippleColor}
/>
<Portal>
<Modal
visible={itemsModalVisible}
onDismiss={hideitemsModal}
contentContainerStyle={{
backgroundColor: theme.colorPrimaryDark,
padding: 20,
margin: 20,
borderRadius: 6,
}}
/>
</View>
>
<Text
style={{
color: theme.textColorPrimary,
fontSize: 18,
marginBottom: 10,
}}
>
Items per row
</Text>
<InputSpinner
style={{
paddingHorizontal: 16,
paddingVertical: 16,
elevation: 0,
}}
max={10}
min={1}
skin="paper"
background={theme.colorPrimaryDark}
colorMax={theme.colorAccentDark}
colorLeft={theme.colorAccentDark}
colorRight={theme.colorAccentDark}
buttonTextColor={theme.textColorPrimary}
colorPress={"white"}
colorMin={theme.colorAccentDark}
value={itemsPerRow}
onChange={(num) => setItemsPerRow(num)}
inputStyle={{
color: theme.textColorPrimary,
}}
/>
</Modal>
</Portal>
{/* <List.Subheader
style={{
color: theme.colorAccentDark,
@@ -125,10 +212,9 @@ const SettingsScreen = ({ navigation, theme, switchTheme }) => {
title="Delete database"
descriptionStyle={desciptionStyles}
description="Delete entire database"
rippleColor={theme.rippleColorDark}
onPress={() => deleteDatabase()}
rippleColor={theme.rippleColor}
onPress={() => deleteDb()}
/> */}
<List.Subheader
style={{
color: theme.colorAccentDark,
@@ -137,66 +223,42 @@ const SettingsScreen = ({ navigation, theme, switchTheme }) => {
>
Theme
</List.Subheader>
<View>
<ThemeCheckbox
label="Light Theme"
checked={
applicationTheme === "lightTheme" ? true : false
}
onPress={() => {
switchTheme(lightTheme);
setApplicationTheme(`lightTheme`);
setAppTheme("lightTheme");
setStatusBarStyle("dark");
<List.Item
titleStyle={titleStyles}
title="Theme"
descriptionStyle={desciptionStyles}
description={themes[themeCode].label}
onPress={showthemeModal}
rippleColor={theme.rippleColor}
/>
<Portal>
<Modal
visible={themeModalVisible}
onDismiss={hidethemeModal}
contentContainerStyle={{
backgroundColor: theme.colorPrimaryDark,
padding: 20,
margin: 20,
borderRadius: 6,
}}
/>
</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>
>
{themes.map((item, index) => (
<ThemeCheckbox
key={index}
label={themes[item.themeCode].label}
checked={
themeCode === item.themeCode
? true
: false
}
onPress={() => {
switchTheme(item.themeCode);
setStatusBarStyle(item.statusBar);
}}
/>
))}
</Modal>
</Portal>
</List.Section>
</View>
</>
@@ -204,7 +266,14 @@ const SettingsScreen = ({ navigation, theme, switchTheme }) => {
};
const mapStateToProps = (state) => ({
themeCode: state.themeReducer.themeCode,
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);
+124
View File
@@ -0,0 +1,124 @@
import React, { useEffect, useRef } from "react";
import { StyleSheet, View, FlatList, RefreshControl } 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,
} from "../../redux/novel/novel.actions";
const Novel = ({
route,
theme,
novel,
chapters,
loading,
getNovelAction,
fetching,
followNovelAction,
sortAndFilterChapters,
updateNovelAction,
}) => {
const {
sourceId,
novelUrl,
novelName,
novelCover,
followed,
novelId,
} = route.params;
let _panel = useRef(null); // Bottomsheet ref
useEffect(() => {
getNovelAction(followed, sourceId, novelUrl, novelId);
}, []);
const renderChapterCard = ({ item }) => (
<ChapterCard
novelUrl={novelUrl}
extensionId={sourceId}
chapter={item}
/>
);
const onRefresh = () => {
updateNovelAction(sourceId, novelUrl, novelId);
};
return (
<Provider>
<View
style={[
styles.container,
{ backgroundColor: theme.colorPrimaryDark },
]}
>
<FlatList
data={!loading && chapters}
keyExtractor={(item) => item.chapterUrl}
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}
bottomSheetRef={_panel}
firstChapter={chapters[0]}
/>
}
refreshControl={
<RefreshControl
refreshing={fetching}
onRefresh={onRefresh}
colors={[theme.textColorPrimary]}
progressBackgroundColor={theme.colorPrimary}
/>
}
/>
<Portal>
<BottomSheet
novelUrl={novelUrl}
bottomSheetRef={_panel}
sortAndFilterChapters={sortAndFilterChapters}
/>
</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,
})(Novel);
const styles = StyleSheet.create({
container: {
flex: 1,
},
});
@@ -1,32 +1,29 @@
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,
sortAndFilterChapters,
novelUrl,
}) => {
const checkBoxStyle = { flexDirection: "row", alignItems: "center" };
const [chapterFlags, setChapterFlags] = useState({ sort: "", filter: "" });
const sortChapters = (sort) => {
setChapterFlags({ ...chapterFlags, sort: sort });
sortAndFilterChapters(novelUrl, chapterFlags.filter, sort);
};
const filterChapters = (filter) => {
setChapterFlags({ ...chapterFlags, filter: filter });
sortAndFilterChapters(novelUrl, filter, chapterFlags.sort);
};
const theme = useSelector((state) => state.themeReducer.theme);
return (
@@ -39,14 +36,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,
}}
>
@@ -55,8 +52,7 @@ export const BottomSheet = ({
<View>
<RadioButton.Group
onValueChange={(newValue) => sortChapters(newValue)}
value={sort}
c
value={chapterFlags.sort}
>
<View
style={{
@@ -77,7 +73,7 @@ export const BottomSheet = ({
/>
<Text
style={{
color: theme.textColorPrimaryDark,
color: theme.textColorPrimary,
}}
>
Newest to oldest
@@ -96,7 +92,7 @@ export const BottomSheet = ({
/>
<Text
style={{
color: theme.textColorPrimaryDark,
color: theme.textColorPrimary,
}}
>
Oldest to newest
@@ -108,7 +104,7 @@ export const BottomSheet = ({
<List.Subheader
style={{
color: theme.textColorPrimaryDark,
color: theme.textColorPrimary,
fontSize: 20,
paddingVertical: 10,
}}
@@ -117,45 +113,51 @@ export const BottomSheet = ({
</List.Subheader>
<View style={checkBoxStyle}>
<Checkbox.Item
status={filter === "" ? "checked" : "unchecked"}
status={
chapterFlags.filter === "" ? "checked" : "unchecked"
}
uncheckedColor={theme.textColorHintDark}
color={theme.colorAccentDark}
onPress={() => filterChapters("")}
/>
<Text style={{ color: theme.textColorPrimaryDark }}>
<Text style={{ color: theme.textColorPrimary }}>
Show all
</Text>
</View>
<View style={checkBoxStyle}>
<Checkbox.Item
status={
filter === "AND `read`=1" ? "checked" : "unchecked"
chapterFlags.filter === "AND `read`=1"
? "checked"
: "unchecked"
}
uncheckedColor={theme.textColorHintDark}
color={theme.colorAccentDark}
onPress={() => filterChapters("AND `read`=1")}
/>
<Text style={{ color: theme.textColorPrimaryDark }}>
<Text style={{ color: theme.textColorPrimary }}>
Show read chapters
</Text>
</View>
<View style={checkBoxStyle}>
<Checkbox.Item
status={
filter === "AND `read`=0" ? "checked" : "unchecked"
chapterFlags.filter === "AND `read`=0"
? "checked"
: "unchecked"
}
uncheckedColor={theme.textColorHintDark}
color={theme.colorAccentDark}
onPress={() => filterChapters("AND `read`=0")}
/>
<Text style={{ color: theme.textColorPrimaryDark }}>
<Text style={{ color: theme.textColorPrimary }}>
Show unread chapters
</Text>
</View>
<View style={checkBoxStyle}>
<Checkbox.Item
status={
filter === "AND downloaded=1"
chapterFlags.filter === "AND downloaded=1"
? "checked"
: "unchecked"
}
@@ -163,7 +165,7 @@ export const BottomSheet = ({
color={theme.colorAccentDark}
onPress={() => filterChapters("AND downloaded=1")}
/>
<Text style={{ color: theme.textColorPrimaryDark }}>
<Text style={{ color: theme.textColorPrimary }}>
Show downloaded chapters
</Text>
</View>
@@ -175,5 +177,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",
},
});
@@ -1,49 +1,46 @@
import React, { useState } from "react";
import { View, Text, ActivityIndicator } from "react-native";
import React from "react";
import { View, Text, ActivityIndicator, StyleSheet } from "react-native";
import { TouchableRipple, IconButton } from "react-native-paper";
import { useNavigation } from "@react-navigation/native";
import { useSelector } from "react-redux";
import {
deleteChapterAction,
downloadChapterAction,
} from "../../../redux/novel/novel.actions";
import { connect } from "react-redux";
const ChapterCard = ({
navigation,
novelUrl,
chapter,
downloadChapter,
extensionId,
theme,
downloading,
downloadChapterAction,
deleteChapterAction,
}) => {
const [downloaded, setDownloaded] = useState(chapter.downloaded);
const theme = useSelector((state) => state.themeReducer.theme);
const navigation = useNavigation();
return (
<TouchableRipple
style={{
paddingHorizontal: 15,
paddingVertical: 7,
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
// borderTopColor: "rgba(255,255,255,.12)",
// borderTopWidth: 1,
}}
style={styles.chapterCardContainer}
onPress={() =>
navigation.navigate("ChapterItem", {
chapterId: chapter.chapterId,
chapterUrl: chapter.chapterUrl,
extensionId,
novelUrl: novelUrl,
chapterName: chapter.chapterName,
novelId: chapter.novelId,
})
}
rippleColor={theme.rippleColorDark}
rippleColor={theme.rippleColor}
>
<>
<View>
<Text
style={[
{
color: theme.textColorPrimaryDark,
color: theme.textColorPrimary,
},
chapter.read === 1 && {
color: theme.textColorHintDark,
@@ -57,7 +54,7 @@ const ChapterCard = ({
<Text
style={[
{
color: theme.textColorSecondaryDark,
color: theme.textColorSecondary,
marginTop: 5,
fontSize: 12,
},
@@ -72,8 +69,7 @@ const ChapterCard = ({
)}
</View>
<View>
{downloading.downloading &&
downloading.chapterUrl === chapter.chapterUrl ? (
{downloading === chapter.chapterId ? (
<ActivityIndicator
color={theme.textColorHintDark}
size={25}
@@ -82,26 +78,32 @@ const ChapterCard = ({
) : (
<IconButton
icon={
downloaded
chapter.downloaded
? "check-circle"
: "arrow-down-circle-outline"
}
animated
color={
downloaded
? theme.textColorPrimaryDark
chapter.downloaded
? theme.textColorPrimary
: theme.textColorHintDark
}
size={25}
onPress={() => {
downloadChapter(
downloaded ? 1 : 0,
extensionId,
novelUrl,
chapter.chapterUrl,
chapter.chapterName
);
setDownloaded(!downloaded);
if (!chapter.downloaded) {
downloadChapterAction(
extensionId,
novelUrl,
chapter.chapterUrl,
chapter.chapterName,
chapter.chapterId
);
} else {
deleteChapterAction(
chapter.chapterId,
chapter.chapterName
);
}
}}
style={{ margin: 2 }}
/>
@@ -112,4 +114,22 @@ const ChapterCard = ({
);
};
export default ChapterCard;
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",
},
});
@@ -15,21 +15,21 @@ 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,
firstChapter,
}) => {
const navigation = useNavigation();
const [more, setMore] = useState(navigatingFrom === 2 ? true : false);
const [more, setMore] = useState(
!loading && novel.followed !== 1 ? true : false
);
const renderGenreChip = ({ item }) => (
<Text
@@ -38,7 +38,7 @@ const NovelInfoHeader = ({
{
color: theme.colorAccentDark,
borderColor: theme.colorAccentDark,
backgroundColor: theme.colorDarkPrimary,
backgroundColor: theme.colorPrimary,
},
]}
>
@@ -47,23 +47,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 +68,7 @@ const NovelInfoHeader = ({
numberOfLines={2}
style={[
styles.name,
{
color: theme.textColorPrimaryDark,
},
{ color: theme.textColorPrimary },
]}
>
{item.novelName}
@@ -83,33 +77,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 +114,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,31 +128,17 @@ 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
@@ -192,28 +162,25 @@ const NovelInfoHeader = ({
size={21}
/>
</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 +190,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,14 +208,11 @@ 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}
@@ -258,90 +220,79 @@ const NovelInfoHeader = ({
<Button
color="white"
style={{
backgroundColor: theme.colorAccentDark,
marginHorizontal: 15,
marginTop: 8,
marginBottom: 15,
}}
style={[
{ backgroundColor: theme.colorAccentDark },
styles.startButton,
]}
uppercase={false}
labelStyle={{ letterSpacing: 0 }}
onPress={() =>
navigation.navigate("ChapterItem", {
chapterUrl: novel.lastRead,
novelUrl: novel.novelUrl,
extensionId: novel.extensionId,
chapterName: novel.lastReadName,
})
}
// onPress={() => {
// console.log(firstChapter.chapterName);
// navigation.navigate("ChapterItem", {
// chapterUrl:
// novel.lastRead.chapterUrl ??
// firstChapter.chapterUrl,
// novelUrl: novel.novelUrl,
// extensionId: novel.extensionId,
// chapterName:
// novel.lastRead.chapterUrl ??
// firstChapter.chapterName,
// });
// }}
>
{novel.unread && novel.unread === 1
? `Start reading ${novel.lastReadName}`
: `Continue reading ${novel.lastReadName}`}
{/* {novel.lastRead
? `Continue reading ${novel.lastRead.chapterName}`
: `Start reading ${firstChapter.chapterName}`} */}
Start reading
</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 +315,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,
},
});
-252
View File
@@ -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,
},
});
+37 -137
View File
@@ -1,70 +1,41 @@
import React, { useEffect, useState } from "react";
import React, { useEffect, useState, useRef } from "react";
import { StyleSheet, View, FlatList, ActivityIndicator } from "react-native";
import { Appbar, Provider, Portal } from "react-native-paper";
import { Provider, Portal } from "react-native-paper";
import NovelCover from "../../../components/common/NovelCover";
import SearchBar from "../../../components/common/SearchBar";
import { SearchAppbar } from "../../../components/common/Appbar";
import { BottomSheet } from "./filters/BottomSheet";
import { useSelector } from "react-redux";
import * as SQLite from "expo-sqlite";
const db = SQLite.openDatabase("lnreader.db");
const BoxNovel = ({ navigation }) => {
const theme = useSelector((state) => state.themeReducer.theme);
const itemsPerRow = useSelector(
(state) => state.settingsReducer.itemsPerRow
);
const library = useSelector((state) => state.libraryReducer.novels);
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);
let bottomSheetRef = useRef(null);
const getNovels = () => {
fetch(
`https://lnreader-extensions.herokuapp.com/api/1/novels/${pageNo}/?o=${sort}`
`https://lnreader-extensions.herokuapp.com/api/1/novels/1/?o=${sort}`
)
.then((response) => response.json())
.then((json) => {
setPageNo(pageNo + 1);
setNovels((novels) => novels.concat(json));
setNovels(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(
@@ -78,92 +49,33 @@ const AllNovels = ({ navigation }) => {
};
const checkIFInLibrary = (id) => {
return libraryNovels.some((obj) => obj.novelUrl === id);
return library.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 },
{ backgroundColor: theme.colorPrimaryDark },
]}
>
<SearchAppbar
screen="Extension"
placeholder="Search BoxNovel"
searchText={searchText}
setSearchText={setSearchText}
getSearchResults={getSearchResults}
getNovels={getNovels}
setLoading={setLoading}
onFilter={() =>
bottomSheetRef.current.show({ velocity: -1.5 })
}
/>
{loading ? (
<View style={{ flex: 1, justifyContent: "center" }}>
<ActivityIndicator
@@ -174,26 +86,20 @@ const AllNovels = ({ navigation }) => {
) : (
<FlatList
contentContainerStyle={styles.list}
numColumns={3}
numColumns={itemsPerRow}
key={itemsPerRow}
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,
novelName: item.novelName,
novelCover: item.novelCover,
novelUrl: item.novelUrl,
sourceId: item.extensionId,
});
}}
libraryStatus={
@@ -203,33 +109,27 @@ const AllNovels = ({ navigation }) => {
}
/>
)}
onScroll={({ nativeEvent }) => {
if (onEndReached(nativeEvent)) {
setLoadingMore(true);
getNovels();
console.log("End Reached");
}
}}
/>
)}
</View>
<Portal>
<BottomSheet
bottomSheetRef={(c) => (_panel = c)}
bottomSheetRef={bottomSheetRef}
setSort={setSort}
sort={sort}
setLoading={setLoading}
/>
</Portal>
</Provider>
);
};
export default AllNovels;
export default BoxNovel;
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 3,
padding: 4,
},
contentContainer: {
@@ -5,14 +5,18 @@ import Bottomsheet from "rn-sliding-up-panel";
import { useSelector } from "react-redux";
export const BottomSheet = ({
bottomSheetRef,
setSort,
sort,
setRefreshing,
}) => {
export const BottomSheet = ({ bottomSheetRef, setSort, sort, setLoading }) => {
const theme = useSelector((state) => state.themeReducer.theme);
const sorting = [
{ label: "Latest", sortFlag: "latest" },
{ label: "A-Z", sortFlag: "aplhabet" },
{ label: "Rating", sortFlag: "rating" },
{ label: "Trending", sortFlag: "trending" },
{ label: "Most Views", sortFlag: "views" },
{ label: "New", sortFlag: "new-manga" },
];
return (
<Bottomsheet
ref={bottomSheetRef}
@@ -22,20 +26,14 @@ export const BottomSheet = ({
<View
style={[
styles.contentContainer,
{
backgroundColor: theme.colorDarkPrimary,
},
{ backgroundColor: theme.colorPrimary },
]}
>
<View
style={{
backgroundColor: theme.textColorHintDark,
height: 5,
width: 30,
borderRadius: 50,
top: 10,
alignSelf: "center",
}}
style={[
styles.bottomSheetHandle,
{ backgroundColor: theme.textColorPrimary },
]}
/>
<List.Section
title="Sort"
@@ -43,135 +41,32 @@ export const BottomSheet = ({
color: theme.colorAccentDark,
fontWeight: "bold",
}}
style={{
color: theme.colorAccentDark,
// backgroundColor: theme.colorAccentDark,
}}
style={{ color: 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);
}}
/>
{sorting.map((item, index) => (
<List.Item
key={index}
right={() =>
sort === item.sortFlag && (
<List.Icon
icon="arrow-up"
color={theme.colorAccentDark}
style={styles.sortArrow}
/>
)
}
title={item.label}
titleStyle={{
fontSize: 15,
color: theme.textColorPrimary,
}}
onPress={() => {
setLoading(true);
setSort(item.sortFlag);
}}
/>
))}
</List.Section>
</View>
</Bottomsheet>
@@ -181,5 +76,19 @@ export const BottomSheet = ({
const styles = StyleSheet.create({
contentContainer: {
flex: 1,
borderTopRightRadius: 8,
borderTopLeftRadius: 8,
},
bottomSheetHandle: {
opacity: 0.5,
height: 4,
width: 32,
borderRadius: 50,
top: 8,
alignSelf: "center",
},
sortArrow: {
marginVertical: 0,
paddingVertical: 0,
},
});
@@ -1,28 +1,26 @@
import React, { useEffect, useState } from "react";
import { StyleSheet, View, FlatList, ActivityIndicator } from "react-native";
import { Appbar, Provider } from "react-native-paper";
import { Provider } from "react-native-paper";
import NovelCover from "../../../components/common/NovelCover";
import SearchBar from "../../../components/common/SearchBar";
import { SearchAppbar } from "../../../components/common/Appbar";
import { useSelector } from "react-redux";
import * as SQLite from "expo-sqlite";
const db = SQLite.openDatabase("lnreader.db");
import EmptyView from "../../../components/common/EmptyView";
const ReadLightNovel = ({ navigation }) => {
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 [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())
@@ -44,95 +42,32 @@ const ReadLightNovel = ({ navigation }) => {
});
};
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);
return library.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 },
{ backgroundColor: theme.colorPrimaryDark },
]}
>
<SearchAppbar
screen="Extension"
placeholder="Search ReadLightNovel"
searchText={searchText}
setSearchText={setSearchText}
getSearchResults={getSearchResults}
getNovels={getNovels}
setLoading={setLoading}
/>
{loading ? (
<View style={{ flex: 1, justifyContent: "center" }}>
<ActivityIndicator
@@ -142,8 +77,9 @@ const ReadLightNovel = ({ navigation }) => {
</View>
) : (
<FlatList
contentContainerStyle={styles.list}
numColumns={3}
contentContainerStyle={{ flexGrow: 1 }}
numColumns={itemsPerRow}
key={itemsPerRow}
data={novels}
showsVerticalScrollIndicator={false}
keyExtractor={(item) => item.novelUrl}
@@ -152,8 +88,10 @@ const ReadLightNovel = ({ navigation }) => {
item={item}
onPress={() =>
navigation.navigate("NovelItem", {
...item,
navigatingFrom: 2,
novelName: item.novelName,
novelCover: item.novelCover,
novelUrl: item.novelUrl,
sourceId: item.extensionId,
})
}
libraryStatus={
@@ -163,6 +101,15 @@ const ReadLightNovel = ({ navigation }) => {
}
/>
)}
ListEmptyComponent={
searchText !== "" &&
novels.length === 0 && (
<EmptyView
icon="(; ̄Д ̄)"
description="No results found"
/>
)
}
/>
)}
</View>
-47
View File
@@ -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
View File
@@ -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;
};
-375
View File
@@ -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)
);
});
};
+47
View File
@@ -0,0 +1,47 @@
import * as SQLite from "expo-sqlite";
import { fetchNovel } from "../source/Source";
const db = SQLite.openDatabase("lnreader.db");
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, res) => {},
(txObj, error) => console.log("Error ", error)
)
);
});
return novel;
};
+42
View File
@@ -0,0 +1,42 @@
export const fetchSources = async () => {
const url = `https://lnreader-extensions.herokuapp.com/api/`;
let res = await fetch(url);
let sources = await res.json();
return sources;
};
export const fetchNovel = async (sourceId, novelUrl) => {
const url = `https://lnreader-extensions.herokuapp.com/api/${sourceId}/novel/${novelUrl}`;
let data = await fetch(url);
let res = await data.json();
const novel = {
novelUrl: res.novelUrl,
sourceUrl: res.sourceUrl,
source: res.sourceName,
sourceId: res.extensionId,
novelName: res.novelName,
novelCover: res.novelCover,
novelSummary: res.novelSummary,
author: res["Author(s)"],
artist: res["Artist(s)"],
status: res.Status,
genre: res["Genre(s)"],
followed: 0,
chapters: res.novelChapters,
};
return novel;
};
export const fetchChapter = async (sourceId, novelUrl, chapterUrl) => {
const url = `https://lnreader-extensions.herokuapp.com/api/${sourceId}/novel/${novelUrl}${chapterUrl}`;
let res = await fetch(url);
let chapter = await res.json();
return chapter;
};
+29 -25
View File
@@ -2,63 +2,67 @@
* Application Colors
*/
export const theme = {
colorDarkPrimary: "#000000",
colorDarkPrimaryDark: "#000000",
colorPrimary: "#000000",
colorPrimaryDark: "#000000",
colorAccentDark: "#3399FF",
textColorPrimaryDark: "#FFFFFF",
textColorSecondaryDark: "rgba(255,255,255,0.7)",
textColorPrimary: "#FFFFFF",
textColorSecondary: "rgba(255,255,255,0.7)",
textColorHintDark: "rgba(255,255,255,0.5)",
rippleColorDark: "rgba(255,255,255,0.2)",
rippleColor: "rgba(255,255,255,0.2)",
};
/**
* Light Theme
*/
export const lightTheme = {
colorDarkPrimary: "#FFFFFF",
colorDarkPrimaryDark: "#FAFAFA",
colorPrimary: "#FFFFFF",
colorPrimaryDark: "#FAFAFA",
colorAccentDark: "#2979FF",
textColorPrimaryDark: "#000000",
textColorSecondaryDark: "rgba(0,0,0,0.54)",
textColorPrimary: "#000000",
textColorSecondary: "rgba(0,0,0,0.54)",
textColorHintDark: "rgba(0,0,0,0.38)",
rippleColorDark: "#C2C2C2",
rippleColor: "rgba(0,0,0,0.12)",
searchBarColor: "#FFFFFF",
};
/**
* Dark Theme
*/
export const darkTheme = {
colorDarkPrimary: "#242529",
colorDarkPrimaryDark: "#202125",
colorPrimary: "#242529",
colorPrimaryDark: "#202125",
colorAccentDark: "#3399FF",
textColorPrimaryDark: "#FFFFFF",
textColorSecondaryDark: "rgba(255,255,255,0.7)",
textColorPrimary: "#FFFFFF",
textColorSecondary: "rgba(255,255,255,0.7)",
textColorHintDark: "rgba(255,255,255,0.5)",
rippleColorDark: "rgba(255,255,255,0.2)",
rippleColor: "rgba(255,255,255,0.2)",
searchBarColor: "#303135",
};
/**
* Amoled Dark Theme
*/
export const amoledDarkTheme = {
colorDarkPrimary: "#000000",
colorDarkPrimaryDark: "#000000",
colorPrimary: "#000000",
colorPrimaryDark: "#000000",
colorAccentDark: "#3399FF",
textColorPrimaryDark: "#FFFFFF",
textColorSecondaryDark: "rgba(255,255,255,0.7)",
textColorPrimary: "#FFFFFF",
textColorSecondary: "rgba(255,255,255,0.7)",
textColorHintDark: "rgba(255,255,255,0.5)",
rippleColorDark: "rgba(255,255,255,0.2)",
rippleColor: "rgba(255,255,255,0.2)",
searchBarColor: "#1F1F1F",
};
/**
* Midnight Dusk Theme
*/
export const midnightDuskTheme = {
colorDarkPrimary: "#201F27",
colorDarkPrimaryDark: "#16151D",
colorPrimary: "#201F27",
colorPrimaryDark: "#16151D",
colorAccentDark: "#F02475",
textColorPrimaryDark: "#FFFFFF",
textColorSecondaryDark: "rgba(255,255,255,0.7)",
textColorPrimary: "#FFFFFF",
textColorSecondary: "rgba(255,255,255,0.7)",
textColorHintDark: "rgba(255,255,255,0.5)",
rippleColorDark: "rgba(255,255,255,0.2)",
rippleColor: "rgba(255,255,255,0.2)",
searchBarColor: "#201F27",
};