Compare commits

...

14 Commits

Author SHA1 Message Date
Rajarshee Chatterjee fda59f825f Fix library filters and minor fixes 2021-05-14 21:00:55 +05:30
Rajarshee Chatterjee 67096eb1ff Add library sort/filter sheet and notification icon 2021-05-14 18:30:15 +05:30
Rajarshee Chatterjee 3a156f32a0 Save scroll percentage and add new theme 2021-05-14 12:03:32 +05:30
Rajarshee Chatterjee 555552a64d Update app icon and splash screen 2021-05-14 09:14:31 +05:30
Rajarshee Chatterjee 53fc800b90 Minor fixes 2021-05-14 08:56:46 +05:30
Rajarshee Chatterjee d1e10433e2 Add bottomsheet for chapter actions 2021-05-13 08:21:46 +05:30
Rajarshee Chatterjee cc53e309ad Fix last read chapter getting reset on opening another novel (fixes #19) 2021-05-12 14:20:48 +05:30
Rajarshee Chatterjee 2170b77607 Add swipe to mark chapter read/ bookmark logic 2021-05-12 10:00:46 +05:30
Rajarshee Chatterjee 632c2e47c7 Add scroll percentage in chapter screen (fixes #17) 2021-05-11 08:43:42 +05:30
Rajarshee Chatterjee 5e2c4d4e27 Add searchbar in updates screen 2021-05-10 09:15:09 +05:30
Rajarshee Chatterjee a21800f208 Minor fixes 2021-05-09 10:10:09 +05:30
Rajarshee Chatterjee 5f943fcc92 Merge branch 'main' 2021-05-09 10:00:59 +05:30
Rajarshee Chatterjee 8f254b4ef5 Add Migration (#15)
* Refactor

* Add migration

* Finish migration

* Add progress bar
2021-05-09 09:57:21 +05:30
Rajarshee Chatterjee 773913048c Refactor 2021-05-07 09:36:07 +05:30
63 changed files with 2128 additions and 793 deletions
+6 -2
View File
@@ -4,17 +4,21 @@
"slug": "LNReader",
"scheme": "lnreader",
"version": "1.0.16",
"orientation": "portrait",
"orientation": "default",
"icon": "./assets/icon.png",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "cover",
"backgroundColor": "#000000"
"backgroundColor": "#222831"
},
"updates": {
"fallbackToCacheTimeout": 0,
"enabled": false
},
"notification": {
"icon": "./assets/notification.png",
"color": "#00adb5"
},
"assetBundlePatterns": ["**/*"],
"ios": {
"supportsTablet": true
Binary file not shown.
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 266 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 36 KiB

+5
View File
@@ -7530,6 +7530,11 @@
"resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-3.0.0.tgz",
"integrity": "sha512-35II5oxTaVp3OP8y0eLPOPpQkxG4fRKQ+dL1YSE1we5kCZFOU0l/Rn0T79HbyUu1LPwUZr6lZupPs0ULnRyMuQ=="
},
"react-native-swipe-list-view": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/react-native-swipe-list-view/-/react-native-swipe-list-view-3.2.7.tgz",
"integrity": "sha512-lyTspC/vBco4yRFGou6X8m/ce1eGtSCxGAxlEzpZDin97kwOQTeV+PuQxoGFiCs9wgLQCoFHvRmOuIreSn8fkQ=="
},
"react-native-vector-icons": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-7.1.0.tgz",
+5 -4
View File
@@ -17,16 +17,19 @@
"@react-navigation/stack": "^5.12.8",
"expo": "^41.0.1",
"expo-app-loading": "^1.0.1",
"expo-background-fetch": "~9.1.0",
"expo-document-picker": "^9.1.2",
"expo-file-system": "~11.0.2",
"expo-font": "~9.1.0",
"expo-linear-gradient": "~9.1.0",
"expo-linking": "~2.2.3",
"expo-media-library": "~12.0.2",
"expo-notifications": "~0.11.6",
"expo-permissions": "^12.0.1",
"expo-sharing": "~9.1.2",
"expo-sqlite": "~9.1.0",
"expo-status-bar": "~1.0.4",
"expo-task-manager": "~9.1.0",
"expo-web-browser": "~9.1.0",
"moment": "^2.29.1",
"qs": "^6.10.1",
@@ -42,6 +45,7 @@
"react-native-reanimated": "~2.1.0",
"react-native-safe-area-context": "3.2.0",
"react-native-screens": "~3.0.0",
"react-native-swipe-list-view": "^3.2.7",
"react-native-vector-icons": "^7.1.0",
"react-native-web": "~0.13.12",
"react-native-webview": "11.2.3",
@@ -50,10 +54,7 @@
"redux-devtools-extension": "^2.13.9",
"redux-persist": "^6.0.0",
"redux-thunk": "^2.3.0",
"rn-sliding-up-panel": "^2.4.4",
"expo-notifications": "~0.11.6",
"expo-background-fetch": "~9.1.0",
"expo-task-manager": "~9.1.0"
"rn-sliding-up-panel": "^2.4.4"
},
"devDependencies": {
"@babel/core": "~7.9.0"
+12 -18
View File
@@ -1,23 +1,17 @@
import React from "react";
import { View } from "react-native";
import { useSelector } from "react-redux";
const BottomSheetHandle = () => {
const theme = useSelector((state) => state.settingsReducer.theme);
return (
<>
<View
style={{
backgroundColor: theme.textColorHint,
height: 5,
width: 30,
borderRadius: 50,
top: 10,
alignSelf: "center",
}}
/>
</>
);
};
const BottomSheetHandle = ({ theme }) => (
<View
style={{
backgroundColor: theme.textColorHint,
height: 5,
width: 30,
borderRadius: 50,
top: 10,
alignSelf: "center",
}}
/>
);
export default BottomSheetHandle;
+11 -2
View File
@@ -14,10 +14,19 @@ const ListSubHeader = ({ children, theme }) => (
</List.Subheader>
);
const ListItem = ({ title, description, icon, onPress, theme, right }) => (
const ListItem = ({
title,
description,
icon,
onPress,
theme,
right,
titleStyle,
}) => (
<List.Item
title={title}
titleStyle={{ color: theme.textColorPrimary }}
style={[!right && { paddingVertical: 12 }]}
titleStyle={[{ color: theme.textColorPrimary }, titleStyle]}
description={description}
descriptionStyle={{ color: theme.textColorSecondary }}
descriptionNumberOfLines={1}
+11
View File
@@ -18,6 +18,8 @@ export const Searchbar = ({
clearSearchbar,
onChangeText,
onSubmitEditing,
migrate,
onPressMigrate,
}) => {
const searchRef = useRef(null);
const dispatch = useDispatch();
@@ -99,6 +101,15 @@ export const Searchbar = ({
onPress={onPressRight}
/>
)}
{migrate && (
<IconButton
icon={migrate}
color={theme.textColorSecondary}
style={{ marginRight: 0 }}
size={23}
onPress={onPressMigrate}
/>
)}
</View>
</TouchableRipple>
);
+54 -2
View File
@@ -84,6 +84,19 @@ export const markChapterRead = async (chapterId) => {
});
};
const markChapterUnreadQuery = `UPDATE chapters SET \`read\` = 0 WHERE chapterId = ?`;
export const markChapterUnread = async (chapterId) => {
db.transaction((tx) => {
tx.executeSql(
markChapterUnreadQuery,
[chapterId],
(tx, res) => {},
(tx, error) => console.log(error)
);
});
};
const isChapterDownloadedQuery = `SELECT * FROM downloads WHERE downloadChapterId=?`;
export const isChapterDownloaded = async (chapterId) => {
@@ -108,12 +121,12 @@ export const isChapterDownloaded = async (chapterId) => {
const downloadChapterQuery = `INSERT INTO downloads (downloadChapterId, chapterName, chapterText, prevChapter, nextChapter) VALUES (?, ?, ?, ?, ?)`;
export const downloadChapter = async (
extensionId,
sourceId,
novelUrl,
chapterUrl,
chapterId
) => {
const downloadUrl = `https://lnreader-extensions.vercel.app/api/${extensionId}/novel/${novelUrl}${chapterUrl}`;
const downloadUrl = `https://lnreader-extensions.vercel.app/api/${sourceId}/novel/${novelUrl}${chapterUrl}`;
const response = await fetch(downloadUrl);
const chapter = await response.json();
@@ -179,3 +192,42 @@ export const getLastReadChapter = async (novelId) => {
});
});
};
const bookmarkChapterQuery = `UPDATE chapters SET bookmark = ? WHERE chapterId = ?`;
export const bookmarkChapter = async (bookmark, chapterId) => {
db.transaction((tx) => {
tx.executeSql(
bookmarkChapterQuery,
[!bookmark, chapterId],
(tx, res) => {},
(txObj, error) => console.log("Error ", error)
);
});
};
const markPreviuschaptersReadQuery = `UPDATE chapters SET \`read\` = 1 WHERE chapterId < ? AND novelId = ?`;
export const markPreviuschaptersRead = async (chapterId, novelId) => {
db.transaction((tx) => {
tx.executeSql(
markPreviuschaptersReadQuery,
[chapterId, novelId],
(tx, res) => {},
(tx, error) => console.log(error)
);
});
};
const markPreviousChaptersUnreadQuery = `UPDATE chapters SET \`read\` = 0 WHERE chapterId < ? AND novelId = ?`;
export const markPreviousChaptersUnread = async (chapterId, novelId) => {
db.transaction((tx) => {
tx.executeSql(
markPreviousChaptersUnreadQuery,
[chapterId, novelId],
(tx, res) => {},
(tx, error) => console.log(error)
);
});
};
+5 -5
View File
@@ -1,7 +1,7 @@
import * as SQLite from "expo-sqlite";
const db = SQLite.openDatabase("lnreader.db");
const getLibraryQuery = `
const getLibraryQuery = (sort, filter) => `
SELECT novels.*, C.chaptersUnread, D.chaptersDownloaded
FROM novels
LEFT JOIN (
@@ -18,15 +18,15 @@ const getLibraryQuery = `
GROUP BY chapters.novelId
) AS D
ON novels.novelId = D.novelId
WHERE novels.followed = 1
GROUP BY novels.novelId
WHERE novels.followed = 1 ${filter ? "AND " + filter : ""}
${sort ? "ORDER BY " + sort : ""}
`;
export const getLibrary = () => {
export const getLibrary = (sort, filter) => {
return new Promise((resolve, reject) =>
db.transaction((tx) => {
tx.executeSql(
getLibraryQuery,
getLibraryQuery(sort, filter),
null,
(txObj, { rows: { _array } }) => {
// console.log(_array);
+35 -1
View File
@@ -1,7 +1,7 @@
import * as SQLite from "expo-sqlite";
const db = SQLite.openDatabase("lnreader.db");
import { fetchChapters } from "../../Services/Source/source";
import { fetchChapters, fetchNovel } from "../../Services/Source/source";
import { insertChapters } from "./ChapterQueries";
const insertNovelQuery = `INSERT INTO novels (novelUrl, sourceUrl, sourceId, source, novelName, novelCover, novelSummary, author, artist, status, genre) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
@@ -123,3 +123,37 @@ export const restoreLibrary = async (novel) => {
)
);
};
const migrateNovelQuery = `INSERT INTO novels (novelUrl, sourceUrl, sourceId, source, novelName, novelCover, novelSummary, author, artist, status, genre, followed) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
export const migrateNovel = async (sourceId, novelUrl) => {
const novel = await fetchNovel(sourceId, novelUrl);
db.transaction((tx) =>
tx.executeSql(
migrateNovelQuery,
[
novel.novelUrl,
novel.sourceUrl,
novel.sourceId,
novel.source,
novel.novelName,
novel.novelCover,
novel.novelSummary,
novel.author,
novel.artist,
novel.status,
novel.genre,
1,
],
async (txObj, { insertId }) => {
const chapters = await fetchChapters(
novel.sourceId,
novel.novelUrl
);
await insertChapters(insertId, chapters);
},
(txObj, error) => console.log("Error ", error)
)
);
};
+45 -13
View File
@@ -1,5 +1,4 @@
import { useEffect } from "react";
import { useSelector } from "react-redux";
import { shallowEqual, useSelector } from "react-redux";
const useTheme = () => {
const theme = useSelector((state) => state.settingsReducer.theme);
@@ -33,44 +32,73 @@ const useNovel = () => {
return novel;
};
const useChapter = () => {
const chapter = useSelector((state) => state.chapterReducer);
return chapter;
};
const findNovel = (novelId) => {
let novel = useSelector((state) => state.preferenceReducer.novelSettings);
novel = novel.find((novel) => novel.novelId === novelId);
novel = novel[novelId];
return novel;
};
const usePreferences = (novelId) => {
let sort, filter;
let sort, filter, position;
const novel = findNovel(novelId);
if (novel) {
sort = novel.sort ?? "";
filter = novel.filter ?? "";
sort = novel.sort;
filter = novel.filter;
position = novel.position;
}
return { sort, filter };
return { sort, filter, position };
};
const useSavedSettings = () => {
const settings = useSelector(
(state) => state.preferenceReducer.novelSettings
);
return settings;
};
const useContinueReading = (chapters, novelId) => {
let chapter, chapterId, novel;
let lastReadChapter, chapterId, novel, position;
novel = findNovel(novelId);
if (novel) {
chapterId = novel.lastRead;
position = novel.position;
}
if (chapterId) {
chapter = chapters.find((obj) => obj.chapterId === chapterId);
lastReadChapter = chapters.find(
(obj) => obj.chapterId === chapterId && obj.read === 0
);
}
if (!chapter) {
chapter = chapters.find((obj) => obj.read === 0);
if (!lastReadChapter) {
lastReadChapter = chapters.find((obj) => obj.read === 0);
}
return chapter;
return { lastReadChapter, position };
};
const useTrackingStatus = () => {
const tracker = useSelector((state) => state.trackerReducer);
return tracker;
};
const useLibraryFilters = () => {
const filters = useSelector((state) => state.libraryReducer.filters);
return filters;
};
export {
@@ -80,5 +108,9 @@ export {
useSettings,
usePreferences,
useContinueReading,
useTrackingStatus,
useNovel,
useChapter,
useSavedSettings,
useLibraryFilters,
};
+12
View File
@@ -9,6 +9,9 @@ import Novel from "../Screens/Novel/Novel";
import Chapter from "../Screens/Chapter/Chapter";
import Extension from "../Screens/Extension/Extension";
import GlobalSearch from "../Screens/Browse/GlobalSearch";
import Migration from "../Screens/Browse/Migration";
import SourceNovels from "../Screens/Browse/SourceNovels";
import MigrateNovel from "../Screens/Browse/MigrateNovel";
import MoreStack from "./More";
import { setStatusBarStyle } from "../Hooks/setStatusBarStyle";
@@ -60,6 +63,15 @@ const MainNavigator = () => {
name="GlobalSearch"
component={GlobalSearch}
/>
<Stack.Screen name="Migration" component={Migration} />
<Stack.Screen
name="SourceNovels"
component={SourceNovels}
/>
<Stack.Screen
name="MigrateNovel"
component={MigrateNovel}
/>
</Stack.Navigator>
</View>
</NavigationContainer>
+3 -1
View File
@@ -74,13 +74,15 @@ const Browse = ({ navigation }) => {
<Searchbar
theme={theme}
placeholder="Search Source"
left={"magnify"}
left="magnify"
onPressLeft={clearSearchbar}
searchText={searchText}
onChangeText={onChangeText}
clearSearchbar={clearSearchbar}
right="book-search"
onPressRight={() => navigation.navigate("GlobalSearch")}
migrate="weather-cloudy-arrow-right"
onPressMigrate={() => navigation.navigate("Migration")}
/>
<FlatList
+26 -3
View File
@@ -14,6 +14,8 @@ import NovelCover from "../../Components/NovelCover";
import NovelList from "../../Components/NovelList";
import EmptyView from "../../Components/EmptyView";
import GlobalSearchNovelList from "./components/GlobalSearchNovelList";
import { ProgressBar } from "react-native-paper";
import { showToast } from "../../Hooks/showToast";
const GlobalSearch = ({ navigation }) => {
const theme = useTheme();
@@ -22,6 +24,7 @@ const GlobalSearch = ({ navigation }) => {
const [searchText, setSearchText] = useState("");
const [searchResults, setSearchResults] = useState("");
const { sources, pinned } = useSelector((state) => state.sourceReducer);
const [progress, setProgress] = useState(0);
const library = useLibrary();
@@ -62,8 +65,26 @@ const GlobalSearch = ({ navigation }) => {
},
]);
setLoading(false);
});
}, 3000 * index)
})
.catch((error) => {
showToast(error.message);
setSearchResults((searchResults) => [
...searchResults,
{
sourceId: source.sourceId,
sourceName: source.sourceName,
sourceLanguage: source.sourceLanguage,
novels: [],
},
]);
setLoading(false);
})
.finally(() =>
setProgress(
(progress) => progress + 1 / pinnedSources.length
)
);
}, 1000 * index)
);
};
@@ -103,7 +124,9 @@ const GlobalSearch = ({ navigation }) => {
onSubmitEditing={onSubmitEditing}
clearSearchbar={clearSearchbar}
/>
{progress < 1 && progress > 0 && pinned && (
<ProgressBar color={theme.colorAccent} progress={progress} />
)}
<FlatList
contentContainerStyle={{
flexGrow: 1,
+164
View File
@@ -0,0 +1,164 @@
import React, { useEffect, useState } from "react";
import {
StyleSheet,
View,
Text,
FlatList,
ActivityIndicator,
} from "react-native";
import { ProgressBar } from "react-native-paper";
import { useLibrary, useTheme } from "../../Hooks/reduxHooks";
import { useSelector } from "react-redux";
import EmptyView from "../../Components/EmptyView";
import MigrationNovelList from "./components/MigrationNovelList";
import { Appbar } from "../../Components/Appbar";
import { showToast } from "../../Hooks/showToast";
const GlobalSearch = ({ navigation, route }) => {
const { sourceId, novelName } = route.params;
const theme = useTheme();
const [progress, setProgress] = useState(0);
const [loading, setLoading] = useState(false);
const [searchResults, setSearchResults] = useState("");
const { sources, pinned } = useSelector((state) => state.sourceReducer);
const library = useLibrary();
const pinnedSources = sources.filter(
(source) =>
pinned.indexOf(source.sourceId) !== -1 &&
source.sourceId !== sourceId
);
const getSearchResults = () => {
const getSearchUrl = (sourceId) => {
if (sourceId === 1) {
return `https://lnreader-extensions.vercel.app/api/1/search/?s=${novelName}&?o=rating`;
} else {
return `https://lnreader-extensions.vercel.app/api/${sourceId}/search/?s=${novelName}`;
}
};
pinnedSources.map((source, index) =>
setTimeout(() => {
setLoading(true);
fetch(getSearchUrl(source.sourceId))
.then((response) => response.json())
.then((json) => {
setSearchResults((searchResults) => [
...searchResults,
{
sourceId: source.sourceId,
sourceName: source.sourceName,
sourceLanguage: source.sourceLanguage,
novels: json,
},
]);
setLoading(false);
})
.catch((error) => {
showToast(error.message);
setSearchResults((searchResults) => [
...searchResults,
{
sourceId: source.sourceId,
sourceName: source.sourceName,
sourceLanguage: source.sourceLanguage,
novels: [],
},
]);
setLoading(false);
});
setProgress((progress) => progress + 1 / pinnedSources.length);
}, 1000 * index)
);
};
useEffect(() => {
getSearchResults();
}, []);
const renderItem = ({ item }) => (
<>
<View style={{ padding: 8, paddingVertical: 16 }}>
<Text style={{ color: theme.textColorPrimary }}>
{item.sourceName}
</Text>
<Text style={{ color: theme.textColorSecondary, fontSize: 12 }}>
{item.sourceLanguage}
</Text>
</View>
<MigrationNovelList
data={item.novels}
theme={theme}
library={library}
navigation={navigation}
/>
</>
);
return (
<View
style={[
styles.container,
{ backgroundColor: theme.colorPrimaryDark },
]}
>
<Appbar
title={novelName}
onBackAction={() => navigation.goBack()}
/>
{progress < 1 && pinned && (
<ProgressBar color={theme.colorAccent} progress={progress} />
)}
<FlatList
contentContainerStyle={{
flexGrow: 1,
padding: 4,
}}
data={searchResults}
keyExtractor={(item) => item.sourceId.toString()}
renderItem={renderItem}
extraData={pinned}
ListEmptyComponent={
<>
{!loading && (
<EmptyView
icon="__φ(..)"
description={`Search a novel in your pinned sources ${
pinned.length === 0
? "(No sources pinned)"
: ""
}`}
/>
)}
</>
}
ListFooterComponent={
loading && (
<View
style={{
flex: 1,
justifyContent: "center",
padding: 16,
}}
>
<ActivityIndicator
size="large"
color={theme.colorAccent}
/>
</View>
)
}
/>
</View>
);
};
export default GlobalSearch;
const styles = StyleSheet.create({
container: { flex: 1 },
});
+69
View File
@@ -0,0 +1,69 @@
import React from "react";
import { StyleSheet, View, FlatList, Text } from "react-native";
import { useLibrary, useTheme } from "../../Hooks/reduxHooks";
import { useSelector } from "react-redux";
import { Appbar } from "../../Components/Appbar";
import MigrationSourceCard from "./components/MigrationSourceCard";
const GlobalSearch = ({ navigation }) => {
const theme = useTheme();
const { sources } = useSelector((state) => state.sourceReducer);
const library = useLibrary();
const novelsPerSource = (sourceId) =>
library.filter((novel) => novel.sourceId === sourceId).length;
const renderItem = ({ item }) => (
<MigrationSourceCard
item={item}
theme={theme}
noOfNovels={novelsPerSource(item.sourceId)}
buttonLabel="All"
onPress={() => navigation.navigate("SourceNovels", item.sourceId)}
/>
);
return (
<View
style={[
styles.container,
{ backgroundColor: theme.colorPrimaryDark },
]}
>
<Appbar
title="Select Source"
onBackAction={() => navigation.goBack()}
/>
<FlatList
data={sources}
keyExtractor={(item) => item.sourceId.toString()}
renderItem={renderItem}
ListHeaderComponent={
<Text
style={{
color: theme.textColorSecondary,
padding: 20,
paddingBottom: 10,
textTransform: "uppercase",
}}
>
Select a Source To Migrate From
</Text>
}
/>
</View>
);
};
export default GlobalSearch;
const styles = StyleSheet.create({
container: { flex: 1 },
containerStyle: {
padding: 20,
margin: 20,
borderRadius: 6,
},
});
+83
View File
@@ -0,0 +1,83 @@
import React from "react";
import { StyleSheet, View, FlatList, Text } from "react-native";
import { useLibrary, useTheme } from "../../Hooks/reduxHooks";
import EmptyView from "../../Components/EmptyView";
import { Appbar } from "../../Components/Appbar";
import ListView from "../../Components/ListView";
const SourceNovels = ({ navigation, route }) => {
const sourceId = route.params;
const theme = useTheme();
const library = useLibrary();
const sourceNovels = library.filter((novel) => novel.sourceId === sourceId);
const renderItem = ({ item }) => (
<ListView
item={item}
theme={theme}
onPress={() =>
navigation.navigate("MigrateNovel", {
sourceId: item.sourceId,
novelName: item.novelName,
})
}
/>
);
return (
<View
style={[
styles.container,
{ backgroundColor: theme.colorPrimaryDark },
]}
>
<Appbar
title="Select Novel"
onBackAction={() => navigation.goBack()}
/>
<FlatList
data={sourceNovels}
keyExtractor={(item) => item.novelId.toString()}
renderItem={renderItem}
ListEmptyComponent={
<Text
style={{
color: theme.textColorSecondary,
padding: 20,
textAlign: "center",
}}
>
Your library does not have any novels from this source
</Text>
}
/>
{/* <Portal>
<MigrationDialog
migrationDialog={migrationDialog}
hideMigrationDialog={hideMigrationDIalog}
theme={theme}
selectedNovel={selectedNovel}
loading={loading}
migrationNovel={migrationNovel}
/>
<SourceToMigrateDialog
selectedNovel={selectedNovel}
sourceToMigrateDialog={sourceToMigrateDialog}
hideSourceToMigrateDialog={hideSourceToMigrateDialog}
sources={sources}
theme={theme}
showMigrationDialog={showMigrationDialog}
selectSourceToMigrate={selectSourceToMigrate}
/>
</Portal> */}
</View>
);
};
export default SourceNovels;
const styles = StyleSheet.create({
container: { flex: 1 },
});
@@ -0,0 +1,132 @@
import React, { useState } from "react";
import { StyleSheet, FlatList, Text, View } from "react-native";
import { Portal, Modal, Button } from "react-native-paper";
import { migrateNovel } from "../../../Database/queries/NovelQueries";
import { showToast } from "../../../Hooks/showToast";
import GlobalSearchNovelCover from "./GlobalSearchNovelCover";
const MigrationNovelList = ({ data, theme, library, navigation }) => {
const [selectedNovel, setSelectedNovel] = useState(false);
const [migrateNovelDialog, setMigrateNovelDialog] = useState(false);
const showMigrateNovelDialog = () => setMigrateNovelDialog(true);
const hideMigrateNovelDialog = () => setMigrateNovelDialog(false);
const checkIFInLibrary = (sourceId, novelUrl) => {
return library.some(
(obj) => obj.novelUrl === novelUrl && obj.sourceId === sourceId
);
};
const renderItem = ({ item }) => (
<GlobalSearchNovelCover
item={item}
onPress={() =>
showModal(item.extensionId, item.novelUrl, item.novelName)
}
libraryStatus={checkIFInLibrary(item.extensionId, item.novelUrl)}
/>
);
const showModal = (sourceId, novelUrl, novelName) => {
if (checkIFInLibrary(sourceId, novelUrl)) {
showToast("Novel already in library");
} else {
setSelectedNovel({ sourceId, novelUrl, novelName });
showMigrateNovelDialog();
}
};
return (
<>
<FlatList
contentContainerStyle={styles.flatListCont}
horizontal={true}
data={data}
keyExtractor={(item) => item.novelUrl}
renderItem={renderItem}
ListEmptyComponent={
<Text
style={{
color: theme.textColorSecondary,
padding: 8,
paddingVertical: 4,
}}
>
No results found
</Text>
}
/>
<Portal>
<Modal
visible={migrateNovelDialog}
onDismiss={hideMigrateNovelDialog}
contentContainerStyle={{
padding: 20,
margin: 20,
borderRadius: 6,
backgroundColor: theme.colorPrimaryDark,
}}
>
<Text
style={{
color: theme.textColorPrimary,
fontSize: 18,
}}
>
{`Migrate ${selectedNovel.novelName}?`}
</Text>
<View
style={{
flexDirection: "row",
justifyContent: "flex-end",
}}
>
<Button
style={{ marginTop: 30 }}
labelStyle={{
color: theme.colorAccent,
letterSpacing: 0,
textTransform: "none",
}}
onPress={hideMigrateNovelDialog}
>
Cancel
</Button>
<Button
style={{ marginTop: 30 }}
labelStyle={{
color: theme.colorAccent,
letterSpacing: 0,
textTransform: "none",
}}
theme={{ colors: { primary: theme.colorAccent } }}
onPress={() => {
migrateNovel(
selectedNovel.sourceId,
selectedNovel.novelUrl
);
hideMigrateNovelDialog();
showToast(
`${selectedNovel.novelName} migrated to new source.`
);
}}
>
Migrate
</Button>
</View>
</Modal>
</Portal>
</>
);
};
export default MigrationNovelList;
const styles = StyleSheet.create({
flatListCont: {
flexGrow: 1,
paddingVertical: 8,
paddingHorizontal: 4,
},
});
@@ -0,0 +1,74 @@
import React from "react";
import { View, Text, Image, StyleSheet } from "react-native";
import { TouchableRipple } from "react-native-paper";
const MigrationSourceCard = ({ item, theme, noOfNovels, onPress }) => {
const { sourceId, sourceName, sourceCover, sourceLanguage, status } = item;
return (
<TouchableRipple
style={styles.extensionCard}
onPress={onPress}
rippleColor={theme.rippleColor}
>
<>
<Image
source={{ uri: sourceCover }}
style={styles.extensionIcon}
resizeMode="contain"
/>
<View style={styles.extensionDetails}>
<View>
<Text
style={{
color: theme.textColorPrimary,
fontSize: 14,
}}
>
{sourceName} {` (${noOfNovels || 0})`}
</Text>
<View style={{ flexDirection: "row" }}>
<Text
style={{
color: theme.textColorSecondary,
fontSize: 12,
}}
>
{sourceLanguage}
</Text>
</View>
</View>
</View>
</>
</TouchableRipple>
);
};
export default MigrationSourceCard;
const styles = StyleSheet.create({
extensionCard: {
flexDirection: "row",
alignItems: "center",
marginVertical: 4,
paddingVertical: 8,
paddingHorizontal: 20,
},
extensionIcon: {
width: 40,
height: 40,
borderRadius: 8,
},
extensionDetails: {
flex: 1,
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginLeft: 16,
},
sourceStatus: {
color: "#C14033",
fontSize: 12,
marginLeft: 5,
},
});
+143 -144
View File
@@ -1,87 +1,66 @@
import React, { useState, useEffect, useRef } from "react";
import { StyleSheet, View, Text, ActivityIndicator } from "react-native";
import {
StyleSheet,
View,
Text,
ActivityIndicator,
ScrollView,
} from "react-native";
import Constants from "expo-constants";
import { Appbar, Provider, Portal } from "react-native-paper";
import { Appbar, Portal } from "react-native-paper";
import { CollapsibleHeaderScrollView } from "react-native-collapsible-header-views";
import BottomSheet from "./components/BottomSheet";
import ReaderSheet from "./components/ReaderSheet";
import { insertHistoryAction } from "../../redux/history/history.actions";
import {
getChapterAction,
markChapterReadAction,
} from "../../redux/novel/novel.actions";
import { connect, useSelector, useDispatch } from "react-redux";
import { parseChapterNumber } from "../../Services/updates";
saveScrollPosition,
} from "../../redux/chapter/chapter.actions";
import { markChapterReadAction } from "../../redux/novel/novel.actions";
import { updateChaptersRead } from "../../redux/tracker/tracker.actions";
const ChapterItem = ({
route,
navigation,
theme,
insertHistoryAction,
getChapterAction,
chapter,
loading,
reader,
}) => {
const {
chapterId,
extensionId,
chapterUrl,
novelUrl,
novelId,
} = route.params;
import { useDispatch } from "react-redux";
import { parseChapterNumber } from "../../Services/updates";
import {
useChapter,
useReaderSettings,
useTheme,
useTrackingStatus,
} from "../../Hooks/reduxHooks";
import {
readerBackground,
readerLineHeight,
readerTextColor,
} from "./readerStyleController";
let _panel = useRef(null);
const Chapter = ({ route, navigation }) => {
const { chapterId, sourceId, chapterUrl, novelUrl, novelId, position } =
route.params;
let readerSheetRef = useRef(null);
const theme = useTheme();
const reader = useReaderSettings();
const dispatch = useDispatch();
const { tracker, trackedNovels } = useTrackingStatus();
const trackedNovels = useSelector(
(state) => state.trackerReducer.trackedNovels
);
const tracker = useSelector((state) => state.trackerReducer.tracker);
const isTracked = trackedNovels.find((obj) => obj.novelId === novelId);
let isTracked = false;
const { chapter, loading } = useChapter();
if (!loading) {
isTracked = trackedNovels.find((obj) => obj.novelId === novelId);
}
const [scrollPercentage, setScrollPercentage] = useState(0);
useEffect(() => {
getChapterAction(extensionId, chapterUrl, novelUrl, chapterId);
insertHistoryAction(novelId, chapterId);
dispatch(getChapterAction(sourceId, novelUrl, chapterUrl, chapterId));
dispatch(insertHistoryAction(novelId, chapterId));
}, []);
const readerBackground = (val) => {
const backgroundColor = {
1: "#000000",
2: "#FFFFFF",
3: "#F4ECD8",
};
let chapterNumber;
return backgroundColor[val];
};
const readerTextColor = (val) => {
const textColor = val === 1 ? "rgba(255,255,255,0.7)" : "#000000";
return textColor;
};
const chapterNumber = !loading && parseChapterNumber(chapter.chapterName);
const readerLineHeight = (val) => {
const lineHeight = {
12: 20,
14: 22,
16: 25,
18: 26,
20: 28,
};
return lineHeight[val];
};
if (!loading) {
chapterNumber = parseChapterNumber(chapter.chapterName);
}
const isCloseToBottom = ({
layoutMeasurement,
@@ -95,16 +74,77 @@ const ChapterItem = ({
);
};
const updateTracker = () =>
isTracked &&
chapterNumber &&
chapterNumber > isTracked.my_list_status.num_chapters_read &&
dispatch(
updateChaptersRead(
isTracked.id,
tracker.access_token,
chapterNumber
)
);
const navigateToPreviousChapter = () =>
navigation.replace("Chapter", {
chapterUrl: chapter.prevChapter,
chapterId: chapterId - 1,
sourceId,
novelUrl,
novelId,
chapterName: chapter.chapterName,
});
const navigateToNextChapter = () =>
navigation.replace("Chapter", {
chapterUrl: chapter.nextChapter,
sourceId,
novelUrl,
novelId,
chapterId: chapterId + 1,
chapterName: chapter.chapterName,
});
const readerStyles = [
{
paddingVertical: 16,
paddingBottom: 32,
fontSize: reader.textSize,
color: readerTextColor(reader.theme),
lineHeight: readerLineHeight(reader.textSize),
textAlign: reader.textAlign,
paddingHorizontal: `${reader.padding}%`,
},
reader.fontFamily && {
fontFamily: reader.fontFamily,
},
];
const onScroll = ({ nativeEvent }) => {
const position =
nativeEvent.contentOffset.y + nativeEvent.layoutMeasurement.height;
const percentage = Math.round(
(position / nativeEvent.contentSize.height) * 100
);
setScrollPercentage(percentage);
dispatch(saveScrollPosition(position, percentage, chapterId, novelId));
if (isCloseToBottom(nativeEvent)) {
dispatch(markChapterReadAction(chapterId, novelId));
updateTracker();
}
};
return (
<Provider>
<>
<CollapsibleHeaderScrollView
headerContainerBackgroundColor="rgba(0,0,0,0.4)"
CollapsibleHeaderComponent={
<Appbar.Header
style={{
backgroundColor: "transparent",
elevation: 0,
}}
style={{ backgroundColor: "transparent", elevation: 0 }}
>
<Appbar.BackAction
onPress={() => navigation.goBack()}
@@ -122,70 +162,32 @@ const ChapterItem = ({
icon="chevron-left"
size={26}
disabled={!chapter.prevChapter}
onPress={() => {
navigation.replace("Chapter", {
chapterUrl: chapter.prevChapter,
chapterId: chapterId - 1,
extensionId,
novelUrl,
novelId,
chapterName: chapter.chapterName,
});
}}
onPress={navigateToPreviousChapter}
color="#FFFFFF"
/>
<Appbar.Action
icon="chevron-right"
size={26}
disabled={!chapter.nextChapter}
onPress={() => {
navigation.replace("Chapter", {
chapterUrl: chapter.nextChapter,
extensionId,
novelUrl,
novelId,
chapterId: chapterId + 1,
chapterName: chapter.chapterName,
});
}}
color="#FFFFFF"
/>
<Appbar.Action
icon="dots-vertical"
size={26}
onPress={() =>
_panel.current.show({ velocity: -1.5 })
}
onPress={navigateToNextChapter}
color="#FFFFFF"
/>
</>
)}
<Appbar.Action
icon="dots-vertical"
size={26}
onPress={() => readerSheetRef.current.show()}
color="#FFFFFF"
/>
</Appbar.Header>
}
headerHeight={100}
headerHeight={Constants.statusBarHeight + 60}
contentContainerStyle={[
styles.container,
{ backgroundColor: readerBackground(reader.theme) },
]}
onScroll={({ nativeEvent }) => {
if (isCloseToBottom(nativeEvent)) {
dispatch(markChapterReadAction(chapterId, novelId));
if (
isTracked &&
chapterNumber &&
chapterNumber >
isTracked.my_list_status.num_chapters_read
) {
dispatch(
updateChaptersRead(
isTracked.id,
tracker.access_token,
chapterNumber
)
);
}
}
}}
onScroll={onScroll}
>
{loading ? (
<View style={{ flex: 1, justifyContent: "center" }}>
@@ -195,47 +197,44 @@ const ChapterItem = ({
/>
</View>
) : (
<Text
style={[
{
paddingVertical: 15,
fontSize: reader.textSize,
color: readerTextColor(reader.theme),
lineHeight: readerLineHeight(reader.textSize),
textAlign: reader.textAlign,
paddingHorizontal: `${reader.padding}%`,
},
reader.fontFamily && {
fontFamily: reader.fontFamily,
},
]}
>
<Text style={readerStyles}>
{chapter.chapterText.trim()}
</Text>
)}
<Portal>
<BottomSheet bottomSheetRef={_panel} />
<ReaderSheet
theme={theme}
reader={reader}
dispatch={dispatch}
bottomSheetRef={readerSheetRef}
/>
</Portal>
</CollapsibleHeaderScrollView>
</Provider>
<Text
style={[
styles.scrollPercentage,
{ color: readerTextColor(reader.theme) },
]}
>
{scrollPercentage + "%"}
</Text>
</>
);
};
const mapStateToProps = (state) => ({
theme: state.settingsReducer.theme,
chapter: state.novelReducer.chapter,
loading: state.novelReducer.chapterLoading,
reader: state.settingsReducer.reader,
});
export default connect(mapStateToProps, {
insertHistoryAction,
getChapterAction,
})(ChapterItem);
export default Chapter;
const styles = StyleSheet.create({
container: {
flexGrow: 1,
paddingVertical: 10,
},
scrollPercentage: {
position: "absolute",
bottom: 12,
alignSelf: "center",
fontFamily: "noto-sans",
fontWeight: "bold",
zIndex: 1,
},
});
@@ -1,12 +1,10 @@
import React from "react";
import { Animated, StyleSheet, View, Text, Dimensions } from "react-native";
import { StyleSheet, View, Text, Dimensions } from "react-native";
import { ToggleButton, IconButton, Chip } from "react-native-paper";
import Slider from "@react-native-community/slider";
import Bottomsheet from "rn-sliding-up-panel";
import { fonts } from "../../../Services/utils/constants";
import { connect } from "react-redux";
import {
setReaderFont,
updateReaderPadding,
@@ -16,26 +14,17 @@ import {
} from "../../../redux/settings/settings.actions";
import BottomSheetHandle from "../../../Components/BottomSheetHandle";
const ChapterBottomSheet = ({
bottomSheetRef,
theme,
reader,
updateReaderTextSize,
updateReaderTheme,
updateReaderTextAlign,
updateReaderPadding,
setReaderFont,
}) => {
const ReaderSheet = ({ theme, reader, dispatch, bottomSheetRef }) => {
return (
<Bottomsheet
animatedValue={new Animated.Value(0)}
ref={bottomSheetRef}
draggableRange={{ top: 400, bottom: 0 }}
snappingPoints={[0, 400]}
showBackdrop={false}
showBackdrop={true}
backdropOpacity={0}
>
<View style={styles.contentContainer}>
<BottomSheetHandle />
<BottomSheetHandle theme={theme} />
<View style={styles.readerSettingsContainer}>
<Text
style={{
@@ -57,7 +46,9 @@ const ChapterBottomSheet = ({
minimumTrackTintColor={theme.colorAccent}
maximumTrackTintColor="#000000"
thumbTintColor={theme.colorAccent}
onValueChange={(value) => updateReaderTextSize(value)}
onValueChange={(value) =>
dispatch(updateReaderTextSize(value))
}
/>
<View
style={{
@@ -81,7 +72,7 @@ const ChapterBottomSheet = ({
</Text>
<ToggleButton.Row
onValueChange={(value) =>
updateReaderTheme(value)
dispatch(updateReaderTheme(value))
}
value={reader.theme}
style={{ marginTop: 10 }}
@@ -131,7 +122,9 @@ const ChapterBottomSheet = ({
</Text>
<ToggleButton.Row
onValueChange={(value) =>
updateReaderTextAlign(value ?? "left")
dispatch(
updateReaderTextAlign(value ?? "left")
)
}
value={reader.textAlign}
style={{ marginTop: 10 }}
@@ -189,7 +182,9 @@ const ChapterBottomSheet = ({
size={26}
disabled={reader.padding <= 0 ? true : false}
onPress={() =>
updateReaderPadding(reader.padding - 1)
dispatch(
updateReaderPadding(reader.padding - 1)
)
}
/>
<Text
@@ -206,7 +201,9 @@ const ChapterBottomSheet = ({
size={26}
disabled={reader.padding >= 10 ? true : false}
onPress={() =>
updateReaderPadding(reader.padding + 1)
dispatch(
updateReaderPadding(reader.padding + 1)
)
}
/>
</View>
@@ -229,7 +226,9 @@ const ChapterBottomSheet = ({
{fonts.map((font, index) => (
<Chip
key={index}
onPress={() => setReaderFont(font.fontFamily)}
onPress={() =>
dispatch(setReaderFont(font.fontFamily))
}
textStyle={{ fontFamily: font.fontFamily }}
style={{
borderWidth: 0,
@@ -252,18 +251,9 @@ const ChapterBottomSheet = ({
);
};
const mapStateToProps = (state) => ({
theme: state.settingsReducer.theme,
reader: state.settingsReducer.reader,
});
const mapStateToProps = (state) => ({});
export default connect(mapStateToProps, {
updateReaderTextSize,
updateReaderTheme,
updateReaderTextAlign,
updateReaderPadding,
setReaderFont,
})(ChapterBottomSheet);
export default ReaderSheet;
const styles = StyleSheet.create({
contentContainer: {
@@ -0,0 +1,27 @@
export const readerBackground = (val) => {
const backgroundColor = {
1: "#000000",
2: "#FFFFFF",
3: "#F4ECD8",
};
return backgroundColor[val];
};
export const readerTextColor = (val) => {
const textColor = val === 1 ? "rgba(255,255,255,0.7)" : "#000000";
return textColor;
};
export const readerLineHeight = (val) => {
const lineHeight = {
12: 20,
14: 22,
16: 25,
18: 26,
20: 28,
};
return lineHeight[val];
};
+11
View File
@@ -8,6 +8,7 @@ import EmptyView from "../../Components/EmptyView";
import { Searchbar } from "../../Components/Searchbar";
import { useTheme, useLibrary } from "../../Hooks/reduxHooks";
import NovelList from "../../Components/NovelList";
import { showToast } from "../../Hooks/showToast";
const Extension = ({ navigation, route }) => {
const { sourceId, sourceName, sourceUrl } = route.params;
@@ -42,6 +43,11 @@ const Extension = ({ navigation, route }) => {
.then((json) => {
setNovels(json);
setLoading(false);
})
.catch((error) => {
setNovels([]);
setLoading(false);
showToast(error.message);
});
};
@@ -58,6 +64,11 @@ const Extension = ({ navigation, route }) => {
.then((json) => {
setNovels(json);
setLoading(false);
})
.catch((error) => {
setNovels([]);
setLoading(false);
showToast(error.message);
});
};
+1 -1
View File
@@ -45,7 +45,7 @@ const History = ({
<FlatList
contentContainerStyle={{ flexGrow: 1, padding: 8 }}
data={history}
keyExtractor={(item) => item.novelUrl.toString()}
keyExtractor={(item) => item.novelId.toString()}
renderItem={renderHistoryCard}
ListFooterComponent={
loading && (
@@ -73,7 +73,7 @@ const HistoryCard = ({ item, deleteHistoryAction, navigation, theme }) => {
navigation.navigate("Chapter", {
chapterId: item.historyChapterId,
chapterUrl: item.chapterUrl,
extensionId: item.sourceId,
sourceId: item.sourceId,
novelUrl: item.novelUrl,
novelId: item.novelId,
chapterName: item.chapterName,
+42 -13
View File
@@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react";
import React, { useState, useCallback, useRef } from "react";
import {
StyleSheet,
Text,
@@ -6,6 +6,7 @@ import {
RefreshControl,
ActivityIndicator,
} from "react-native";
import { useFocusEffect } from "@react-navigation/native";
import { useSelector, useDispatch } from "react-redux";
import { Searchbar } from "../../Components/Searchbar";
@@ -20,19 +21,30 @@ import {
import { updateLibraryAction } from "../../redux/updates/updates.actions";
import { useTheme } from "../../Hooks/reduxHooks";
import { setNovel } from "../../redux/novel/novel.actions";
import { Portal } from "react-native-paper";
import LibraryFilterSheet from "./components/LibraryFilterSheet";
const LibraryScreen = ({ navigation }) => {
const theme = useTheme();
const dispatch = useDispatch();
const { loading, novels } = useSelector((state) => state.libraryReducer);
const libraryFilterSheetRef = useRef(null);
const {
loading,
novels,
filters: { sort, filter },
} = useSelector((state) => state.libraryReducer);
const [refreshing, setRefreshing] = useState(false);
const [searchText, setSearchText] = useState("");
useEffect(() => {
dispatch(getLibraryAction());
}, [getLibraryAction]);
useFocusEffect(
useCallback(() => {
setSearchText("");
dispatch(getLibraryAction(sort, filter));
}, [getLibraryAction, sort, filter])
);
const onRefresh = () => {
setRefreshing(true);
@@ -41,13 +53,13 @@ const LibraryScreen = ({ navigation }) => {
};
const clearSearchbar = () => {
dispatch(getLibraryAction());
dispatch(getLibraryAction(sort, filter));
setSearchText("");
};
const onChangeText = (text) => {
setSearchText(text);
dispatch(searchLibraryAction(text));
dispatch(searchLibraryAction(text, sort, filter));
};
const renderItem = ({ item }) => (
@@ -69,6 +81,10 @@ const LibraryScreen = ({ navigation }) => {
/>
);
// if (!loading) {
// console.log(novels);
// }
const listEmptyComponent = () =>
searchText !== "" ? (
<Text style={[styles.emptySearch, { color: theme.colorAccent }]}>
@@ -95,17 +111,30 @@ const LibraryScreen = ({ navigation }) => {
clearSearchbar={clearSearchbar}
onChangeText={onChangeText}
left="magnify"
right="dots-vertical"
onPressRight={() => libraryFilterSheetRef.current.show()}
theme={theme}
/>
{loading ? (
<ActivityIndicator size="small" color={theme.colorAccent} />
) : (
<NovelList
data={novels}
renderItem={renderItem}
refreshControl={refreshControl()}
ListEmptyComponent={listEmptyComponent}
/>
<>
<NovelList
data={novels}
renderItem={renderItem}
refreshControl={refreshControl()}
ListEmptyComponent={listEmptyComponent}
/>
<Portal>
<LibraryFilterSheet
bottomSheetRef={libraryFilterSheetRef}
theme={theme}
dispatch={dispatch}
sort={sort}
filter={filter}
/>
</Portal>
</>
)}
</View>
</>
@@ -0,0 +1,254 @@
import React, { useState } from "react";
import { StyleSheet, View, Text, Animated } from "react-native";
import { List, RadioButton, Checkbox } from "react-native-paper";
import Bottomsheet from "rn-sliding-up-panel";
import BottomSheetHandle from "../../../Components/BottomSheetHandle";
import { ListItem } from "../../../Components/List";
import { filterLibrary } from "../../../redux/library/library.actions";
const LibraryFilterSheet = ({
bottomSheetRef,
dispatch,
sort,
filter,
theme,
}) => {
const [animatedValue] = useState(new Animated.Value(0));
return (
<Bottomsheet
animatedValue={animatedValue}
ref={bottomSheetRef}
draggableRange={{ top: 540, bottom: 0 }}
snappingPoints={[0, 540]}
>
<View
style={[
styles.contentContainer,
{ backgroundColor: theme.colorPrimary },
]}
>
<BottomSheetHandle theme={theme} />
<View style={{ marginTop: 24 }}>
<Text
style={{
color: theme.colorAccent,
paddingHorizontal: 16,
paddingBottom: 8,
textTransform: "uppercase",
}}
>
SORT
</Text>
<ListItem
title="Date Added"
theme={theme}
right={
sort === "novels.novelId ASC"
? "arrow-up"
: sort === "novels.novelId DESC"
? "arrow-down"
: null
}
onPress={() =>
sort === "novels.novelId ASC"
? dispatch(
filterLibrary(
"novels.novelId DESC",
filter
)
)
: dispatch(
filterLibrary(
"novels.novelId ASC",
filter
)
)
}
/>
<ListItem
title="Alphabetically"
theme={theme}
right={
sort === "novels.novelName ASC"
? "arrow-up"
: sort === "novels.novelName DESC"
? "arrow-down"
: null
}
onPress={() =>
sort === "novels.novelName ASC"
? dispatch(
filterLibrary(
"novels.novelName DESC",
filter
)
)
: dispatch(
filterLibrary(
"novels.novelName ASC",
filter
)
)
}
/>
<ListItem
title="Unread"
theme={theme}
right={
sort === "novels.unread ASC"
? "arrow-up"
: sort === "novels.unread DESC"
? "arrow-down"
: null
}
onPress={() =>
sort === "novels.unread ASC"
? dispatch(
filterLibrary(
"novels.unread DESC",
filter
)
)
: dispatch(
filterLibrary("novels.unread ASC", filter)
)
}
/>
<ListItem
title="Downloaded"
theme={theme}
right={
sort === "chaptersDownloaded ASC"
? "arrow-up"
: sort === "chaptersDownloaded DESC"
? "arrow-down"
: null
}
onPress={() =>
sort === "chaptersDownloaded ASC"
? dispatch(
filterLibrary(
"chaptersDownloaded DESC",
filter
)
)
: dispatch(
filterLibrary(
"chaptersDownloaded ASC",
filter
)
)
}
/>
<ListItem
title="Total Chapters"
theme={theme}
right={
sort === "chaptersUnread ASC"
? "arrow-up"
: sort === "chaptersUnread DESC"
? "arrow-down"
: null
}
onPress={() =>
sort === "chaptersUnread ASC"
? dispatch(
filterLibrary(
"chaptersUnread DESC",
filter
)
)
: dispatch(
filterLibrary(
"chaptersUnread ASC",
filter
)
)
}
/>
</View>
<View>
<Text
style={{
color: theme.colorAccent,
paddingHorizontal: 16,
paddingVertical: 8,
textTransform: "uppercase",
}}
>
Filter
</Text>
<Checkbox.Item
label="Downloaded"
labelStyle={{ color: theme.textColorPrimary }}
color={theme.colorAccent}
uncheckedColor={theme.textColorHint}
status={
filter === "chaptersDownloaded > 0"
? "checked"
: "unchecked"
}
onPress={() =>
filter === "chaptersDownloaded > 0"
? dispatch(filterLibrary(sort, ""))
: dispatch(
filterLibrary(
sort,
"chaptersDownloaded > 0"
)
)
}
/>
<Checkbox.Item
label="Unread"
labelStyle={{ color: theme.textColorPrimary }}
color={theme.colorAccent}
uncheckedColor={theme.textColorHint}
status={
filter === "unread = 1" ? "checked" : "unchecked"
}
onPress={() =>
filter === "unread = 1"
? dispatch(filterLibrary(sort, ""))
: dispatch(filterLibrary(sort, "unread = 1"))
}
/>
<Checkbox.Item
label="Completed"
labelStyle={{ color: theme.textColorPrimary }}
color={theme.colorAccent}
uncheckedColor={theme.textColorHint}
status={
filter === "chaptersUnread IS NULL"
? "checked"
: "unchecked"
}
onPress={() =>
filter === "chaptersUnread IS NULL"
? dispatch(filterLibrary(sort, ""))
: dispatch(
filterLibrary(
sort,
"chaptersUnread IS NULL"
)
)
}
/>
</View>
</View>
</Bottomsheet>
);
};
export default LibraryFilterSheet;
const styles = StyleSheet.create({
contentContainer: {
flex: 1,
borderTopRightRadius: 8,
borderTopLeftRadius: 8,
},
});
@@ -16,6 +16,7 @@ const ThemeModal = ({
{ id: 2, name: "Dark", statusBar: "light" },
{ id: 3, name: "Midnight Dusk", statusBar: "light" },
{ id: 4, name: "Lime", statusBar: "light" },
{ id: 5, name: "Iris Blue", statusBar: "light" },
];
const renderThemeCheckboxes = () =>
+68 -46
View File
@@ -1,52 +1,52 @@
import React, { useEffect, useRef, useState } from "react";
import { StyleSheet, View, FlatList, RefreshControl } from "react-native";
import { Provider, Portal } from "react-native-paper";
import { useDispatch } from "react-redux";
import {
bookmarkChapterAction,
getNovelAction,
markChapterReadAction,
markChapterUnreadAction,
sortAndFilterChapters,
updateNovelAction,
} from "../../redux/novel/novel.actions";
import {
useContinueReading,
useNovel,
usePreferences,
useTheme,
} from "../../Hooks/reduxHooks";
import { showToast } from "../../Hooks/showToast";
import ChapterItem from "./components/ChapterItem";
import NovelInfoHeader from "./components/NovelHeader";
import ChaptersSettingsSheet from "./components/ChaptersSettingsSheet";
import { useDispatch } from "react-redux";
import {
getNovelAction,
sortAndFilterChapters,
updateNovelAction,
} from "../../redux/novel/novel.actions";
import TrackSheet from "./components/Tracker/TrackSheet";
import { showToast } from "../../Hooks/showToast";
import ChapterActionsSheet from "./components/ChapterActionsSheet";
const Novel = ({ route, navigation }) => {
const item = route.params;
const {
sourceId,
novelUrl,
novelName,
novelCover,
followed,
novelId,
} = item;
const { sourceId, novelUrl, novelName, followed, novelId } = item;
const theme = useTheme();
const dispatch = useDispatch();
const { novel, chapters, loading, fetching } = useNovel();
const { novel, chapters, loading, updating, downloading } = useNovel();
const [selected, setSelected] = useState();
let chaptersSettingsSheetRef = useRef(null);
let trackerSheetRef = useRef(null);
let chapterActionsSheetRef = useRef(null);
const { sort, filter } = usePreferences(
novelId || (!loading && novel.novelId)
const { sort = "ORDER BY chapterId ASC", filter = "" } = usePreferences(
novel.novelId
);
let lastRead = useContinueReading(
let { lastReadChapter, position } = useContinueReading(
chapters,
novelId || (!loading && novel.novelId)
novel.novelId
);
useEffect(() => {
@@ -56,35 +56,19 @@ const Novel = ({ route, navigation }) => {
}, [getNovelAction]);
const onRefresh = () => {
dispatch(updateNovelAction(sourceId, novelUrl, novelId));
dispatch(updateNovelAction(sourceId, novelUrl, novelId, sort, filter));
showToast(`Updated ${novelName}`);
};
const refreshControl = () => (
<RefreshControl
onRefresh={onRefresh}
refreshing={fetching}
refreshing={updating}
colors={[theme.textColorPrimary]}
progressBackgroundColor={theme.colorPrimary}
/>
);
const ListHeaderComponent = () => (
<NovelInfoHeader
item={item}
novel={novel}
theme={theme}
filter={filter}
loading={loading}
lastRead={lastRead}
dispatch={dispatch}
chapters={chapters}
navigation={navigation}
trackerSheetRef={trackerSheetRef}
chaptersSettingsSheetRef={chaptersSettingsSheetRef}
/>
);
const renderItem = ({ item }) => (
<ChapterItem
theme={theme}
@@ -93,6 +77,12 @@ const Novel = ({ route, navigation }) => {
dispatch={dispatch}
sourceId={sourceId}
navigation={navigation}
lastReadChapter={lastReadChapter}
position={position}
downloading={downloading}
selected={selected}
setSelected={setSelected}
chapterActionsSheetRef={chapterActionsSheetRef}
/>
);
@@ -112,7 +102,21 @@ const Novel = ({ route, navigation }) => {
windowSize={15}
initialNumToRender={7}
renderItem={renderItem}
ListHeaderComponent={ListHeaderComponent}
ListHeaderComponent={
<NovelInfoHeader
item={item}
novel={novel}
theme={theme}
filter={filter}
loading={loading}
lastRead={lastReadChapter}
dispatch={dispatch}
chapters={chapters}
navigation={navigation}
trackerSheetRef={trackerSheetRef}
chaptersSettingsSheetRef={chaptersSettingsSheetRef}
/>
}
refreshControl={refreshControl()}
/>
{!loading && (
@@ -122,16 +126,28 @@ const Novel = ({ route, navigation }) => {
bottomSheetRef={chaptersSettingsSheetRef}
dispatch={dispatch}
sortAndFilterChapters={sortAndFilterChapters}
novelId={novelId || novel.novelId}
savedSort={sort}
savedFilter={filter}
novelId={novel.novelId}
sort={sort}
theme={theme}
filter={filter}
/>
<TrackSheet
bottomSheetRef={trackerSheetRef}
novelId={novelId || novel.novelId}
novelName={novelName || novel.novelName}
novelId={novel.novelId}
novelName={novel.novelName}
theme={theme}
/>
<ChapterActionsSheet
theme={theme}
selected={selected}
chapters={chapters}
dispatch={dispatch}
setSelected={setSelected}
bottomSheetRef={chapterActionsSheetRef}
markChapterReadAction={markChapterReadAction}
markChapterUnreadAction={markChapterUnreadAction}
bookmarkChapterAction={bookmarkChapterAction}
/>
</Portal>
)}
</View>
@@ -143,4 +159,10 @@ export default Novel;
const styles = StyleSheet.create({
container: { flex: 1 },
rowBack: {
alignItems: "center",
flex: 1,
flexDirection: "row",
justifyContent: "space-between",
},
});
@@ -0,0 +1,175 @@
import React, { useState } from "react";
import { StyleSheet, View, Text, Animated } from "react-native";
import { List } from "react-native-paper";
import Bottomsheet from "rn-sliding-up-panel";
import BottomSheetHandle from "../../../Components/BottomSheetHandle";
import { ListItem } from "../../../Components/List";
import {
markPreviousChaptersReadAction,
markPreviousChaptersUnreadAction,
} from "../../../redux/novel/novel.actions";
const ChapterActionsSheet = ({
bottomSheetRef,
selected,
theme,
markChapterReadAction,
markChapterUnreadAction,
dispatch,
bookmarkChapterAction,
setSelected,
}) => {
const [animatedValue] = useState(new Animated.Value(0));
return (
<Bottomsheet
animatedValue={animatedValue}
ref={bottomSheetRef}
draggableRange={{ top: 270, bottom: 0 }}
snappingPoints={[0, 270]}
>
<View
style={[
styles.contentContainer,
{ backgroundColor: theme.colorPrimary },
]}
>
<Text
style={{
color: theme.textColorPrimary,
textAlign: "center",
paddingTop: 16,
paddingBottom: 8,
fontSize: 16,
}}
>
{selected?.chapterName}
</Text>
<List.Item
title="Mark previous chapters as read"
titleStyle={{ color: theme.textColorPrimary, fontSize: 15 }}
left={() => (
<List.Icon
color={theme.textColorPrimary}
icon="eye-check"
style={{ marginVertical: 0 }}
/>
)}
onPress={() => {
dispatch(
markPreviousChaptersReadAction(
selected?.chapterId,
selected?.novelId
)
);
// bottomSheetRef.current.hide();
}}
rippleColor={theme.rippleColor}
/>
<List.Item
title="Mark previous chapters as unread"
titleStyle={{ color: theme.textColorPrimary, fontSize: 15 }}
left={() => (
<List.Icon
color={theme.textColorPrimary}
icon="eye-off"
style={{ marginVertical: 0 }}
/>
)}
onPress={() => {
dispatch(
markPreviousChaptersUnreadAction(
selected?.chapterId,
selected?.novelId
)
);
// bottomSheetRef.current.hide();
}}
rippleColor={theme.rippleColor}
/>
<List.Item
title={
selected?.read
? "Mark chapter as unread"
: "Mark chapter as read"
}
titleStyle={{ color: theme.textColorPrimary, fontSize: 15 }}
left={() => (
<List.Icon
color={theme.textColorPrimary}
icon={selected?.read ? "eye-off" : "eye"}
style={{ marginVertical: 0 }}
/>
)}
onPress={() => {
if (selected?.read) {
dispatch(
markChapterUnreadAction(selected?.chapterId)
);
setSelected((selected) => ({
...selected,
read: 0,
}));
} else {
dispatch(
markChapterReadAction(
selected?.chapterId,
selected?.novelId
)
);
setSelected((selected) => ({
...selected,
read: 1,
}));
}
// bottomSheetRef.current.hide();
}}
rippleColor={theme.rippleColor}
/>
<List.Item
title={
selected?.bookmark
? "Remove bookmark"
: "Bookmark chapter"
}
titleStyle={{ color: theme.textColorPrimary, fontSize: 15 }}
left={() => (
<List.Icon
color={theme.textColorPrimary}
icon={
selected?.bookmark ? "bookmark-off" : "bookmark"
}
style={{ marginVertical: 0 }}
/>
)}
onPress={() => {
dispatch(
bookmarkChapterAction(
selected?.bookmark,
selected?.chapterId
)
);
setSelected((selected) => ({
...selected,
bookmark: !selected.bookmark,
}));
// bottomSheetRef.current.hide();
}}
rippleColor={theme.rippleColor}
/>
</View>
</Bottomsheet>
);
};
export default ChapterActionsSheet;
const styles = StyleSheet.create({
contentContainer: {
flex: 1,
borderTopRightRadius: 8,
borderTopLeftRadius: 8,
},
});
+112 -47
View File
@@ -1,7 +1,6 @@
import React, { useState } from "react";
import { View, Text, ActivityIndicator, StyleSheet } from "react-native";
import { TouchableRipple, IconButton, Menu } from "react-native-paper";
import { useNavigation } from "@react-navigation/native";
import {
deleteChapterAction,
@@ -15,13 +14,32 @@ const ChapterItem = ({
dispatch,
theme,
navigation,
downloading,
position,
setSelected,
chapterActionsSheetRef,
}) => {
const [visible, setVisible] = useState(false);
const openMenu = () => setVisible(true);
const closeMenu = () => setVisible(false);
const [deleteChapterMenu, setDeleteChapterMenu] = useState(false);
const showDeleteChapterMenu = () => setDeleteChapterMenu(true);
const hideDeleteChapterMenu = () => setDeleteChapterMenu(false);
const navigateToChapter = () =>
navigation.navigate("Chapter", {
sourceId,
novelUrl,
novelId: chapter.novelId,
chapterId: chapter.chapterId,
chapterUrl: chapter.chapterUrl,
chapterName: chapter.chapterName,
position:
(position &&
position[chapter.chapterId] &&
position[chapter.chapterId].position) ||
0,
});
const displayDownloadButton = () => {
if (chapter.downloaded === 3) {
if (downloading.indexOf(chapter.chapterId) !== -1) {
return (
<ActivityIndicator
color={theme.textColorHint}
@@ -32,15 +50,15 @@ const ChapterItem = ({
} else if (chapter.downloaded === 1) {
return (
<Menu
visible={visible}
onDismiss={closeMenu}
visible={deleteChapterMenu}
onDismiss={hideDeleteChapterMenu}
anchor={
<IconButton
icon="check-circle"
animated
color={theme.textColorPrimary}
size={25}
onPress={openMenu}
onPress={showDeleteChapterMenu}
style={{ margin: 2 }}
/>
}
@@ -84,53 +102,100 @@ const ChapterItem = ({
}
};
const bookmarkButton = () => {
if (chapter.bookmark) {
return (
<IconButton
icon="bookmark"
color={theme.colorAccent}
size={18}
style={{ marginLeft: 2 }}
/>
);
}
};
return (
<TouchableRipple
style={styles.chapterCardContainer}
onPress={() =>
navigation.navigate("Chapter", {
chapterId: chapter.chapterId,
chapterUrl: chapter.chapterUrl,
extensionId: sourceId,
novelUrl: novelUrl,
chapterName: chapter.chapterName,
novelId: chapter.novelId,
})
}
style={[
styles.chapterCardContainer,
{ backgroundColor: theme.colorPrimaryDark },
]}
onPress={navigateToChapter}
onLongPress={() => {
setSelected(chapter);
chapterActionsSheetRef.current.show();
}}
rippleColor={theme.rippleColor}
>
<>
<View>
<Text
style={[
{
color: theme.textColorPrimary,
},
chapter.read === 1 && {
color: theme.textColorHint,
},
]}
numberOfLines={1}
<View
style={{
flexDirection: "row",
alignItems: "center",
}}
>
{chapter.chapterName.substring(0, 50)}
</Text>
{chapter.releaseDate && (
<Text
style={[
{
color: theme.textColorSecondary,
{bookmarkButton()}
<View>
<Text
style={[
{ color: theme.textColorPrimary },
chapter.read && {
color: theme.textColorHint,
},
]}
numberOfLines={1}
>
{chapter.chapterName.substring(0, 50)}
</Text>
<View
style={{
flexDirection: "row",
marginTop: 5,
fontSize: 12,
},
chapter.read === 1 && {
color: theme.textColorHint,
},
]}
numberOfLines={1}
>
{chapter.releaseDate}
</Text>
)}
}}
>
{chapter.releaseDate && (
<Text
style={[
{
color: theme.textColorSecondary,
fontSize: 12,
},
chapter.read && {
color: theme.textColorHint,
},
]}
numberOfLines={1}
>
{chapter.releaseDate}
</Text>
)}
{position &&
!chapter.read &&
position[chapter.chapterId] &&
position[chapter.chapterId].percentage <
95 && (
<Text
style={[
{
color: theme.textColorSecondary,
fontSize: 12,
marginLeft: 0,
},
chapter.releaseDate && {
marginLeft: 5,
},
]}
numberOfLines={1}
>
{position[chapter.chapterId]
.percentage + "%"}
</Text>
)}
</View>
</View>
</View>
</View>
<View>{displayDownloadButton()}</View>
</>
@@ -1,35 +1,27 @@
import React, { useState } from "react";
import { StyleSheet, View, Text, Animated } from "react-native";
import { List, RadioButton, Checkbox } from "react-native-paper";
import Bottomsheet from "rn-sliding-up-panel";
import { useSelector } from "react-redux";
import BottomSheetHandle from "../../../Components/BottomSheetHandle";
const ChaptersSettingsSheet = ({
bottomSheetRef,
novelId,
sortAndFilterChapters,
dispatch,
savedSort,
savedFilter,
sort,
filter,
theme,
}) => {
const [sort, setSort] = useState(savedSort || "ORDER BY chapterId ASC");
const [filter, setFilter] = useState(savedFilter || "");
const sortChapters = (val) => {
setSort(val);
const sortChapters = (val) =>
dispatch(sortAndFilterChapters(novelId, val, filter));
};
const filterChapters = (val) => {
setFilter(val);
const filterChapters = (val) =>
dispatch(sortAndFilterChapters(novelId, sort, val));
};
const checkBoxStyle = { flexDirection: "row", alignItems: "center" };
const theme = useSelector((state) => state.settingsReducer.theme);
const [animatedValue] = useState(new Animated.Value(0));
return (
@@ -42,12 +34,10 @@ const ChaptersSettingsSheet = ({
<View
style={[
styles.contentContainer,
{
backgroundColor: theme.colorPrimary,
},
{ backgroundColor: theme.colorPrimary },
]}
>
<View style={styles.bottomSheetHandle} />
<BottomSheetHandle theme={theme} />
<List.Subheader
style={{
color: theme.textColorPrimary,
@@ -185,12 +175,4 @@ const styles = StyleSheet.create({
borderTopRightRadius: 8,
borderTopLeftRadius: 8,
},
bottomSheetHandle: {
backgroundColor: "rgba(255,255,255,0.25)",
height: 4,
width: 50,
borderRadius: 50,
top: 10,
alignSelf: "center",
},
});
@@ -2,15 +2,11 @@ import React from "react";
import { StyleSheet } from "react-native";
import { Chip } from "react-native-paper";
import { MaterialCommunityIcons } from "@expo/vector-icons";
import { useDispatch } from "react-redux";
import { followNovelAction } from "../../../../redux/novel/novel.actions";
const FollowChip = ({ followed, novel, theme }) => {
const dispatch = useDispatch();
const FollowChip = ({ followed, novel, theme, dispatch }) => {
const getIconName = () => (followed ? "heart" : "heart-outline");
const getChipLabel = () => (followed ? "In Library" : "Add to library");
return (
@@ -21,7 +21,7 @@ const ReadButton = ({ navigation, novel, chapters, theme, lastRead }) => {
chapterUrl: lastRead.chapterUrl,
novelUrl: novel.novelUrl,
novelId: lastRead.novelId,
extensionId: novel.sourceId,
sourceId: novel.sourceId,
chapterName: lastRead.chapterName,
});
}}
@@ -5,7 +5,6 @@ import { MaterialCommunityIcons } from "@expo/vector-icons";
const TrackerChip = ({ isTracked, trackerSheetRef, theme }) => {
const getIconName = () => (isTracked ? "check" : "sync");
const getChipLabel = () => (isTracked ? "Tracked" : "Tracking");
return (
+15 -9
View File
@@ -15,6 +15,7 @@ import FollowChip from "./Info/FollowChip";
import TrackerChip from "./Info/TrackerChip";
import NovelSummary from "./Info/NovelSummary";
import ReadButton from "./Info/ReadButton";
import { useTrackingStatus } from "../../../Hooks/reduxHooks";
const NovelInfoHeader = ({
item,
@@ -31,10 +32,7 @@ const NovelInfoHeader = ({
}) => {
const [downloadMenu, showDownloadMenu] = useState(false);
const tracker = useSelector((state) => state.trackerReducer.tracker);
const trackedNovels = useSelector(
(state) => state.trackerReducer.trackedNovels
);
const { tracker, trackedNovels } = useTrackingStatus();
let isTracked = false;
@@ -110,6 +108,7 @@ const NovelInfoHeader = ({
theme={theme}
followed={novel.followed}
novel={novel}
dispatch={dispatch}
/>
{tracker && (
<TrackerChip
@@ -170,6 +169,17 @@ const NovelInfoHeader = ({
}
/>
</Menu>
{/* <IconButton
onPress={() =>
navigation.navigate("MigrateNovel", {
sourceId: novel.sourceId,
novelName: novel.novelName,
})
}
icon="weather-cloudy-arrow-right"
color={theme.colorAccent}
size={21}
/> */}
</View>
<NovelSummary
summary={novel.novelSummary}
@@ -198,11 +208,7 @@ const NovelInfoHeader = ({
/>
<TouchableRipple
style={styles.bottomsheet}
onPress={() =>
chaptersSettingsSheetRef.current.show({
velocity: -1.5,
})
}
onPress={() => chaptersSettingsSheetRef.current.show()}
rippleColor={theme.rippleColor}
>
<>
+49 -24
View File
@@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react";
import React, { useState, useEffect, useCallback } from "react";
import {
StyleSheet,
View,
@@ -6,6 +6,7 @@ import {
ActivityIndicator,
RefreshControl,
} from "react-native";
import { useFocusEffect } from "@react-navigation/native";
import { useDispatch, useSelector } from "react-redux";
import EmptyView from "../../Components/EmptyView";
import {
@@ -15,17 +16,22 @@ import {
import { Appbar } from "./components/Appbar";
import UpdatesItem from "./components/UpdatesItem";
import { useTheme } from "../../Hooks/reduxHooks";
import { Searchbar } from "../../Components/Searchbar";
const Updates = ({ navigation }) => {
const theme = useTheme();
const dispatch = useDispatch();
const { updates, loading } = useSelector((state) => state.updatesReducer);
const [searchText, setSearchText] = useState("");
const [searchResults, setSearchResults] = useState([]);
const [refreshing, setRefreshing] = useState(false);
useEffect(() => {
dispatch(getUpdatesAction());
}, [getUpdatesAction, updates]);
useFocusEffect(
useCallback(() => {
dispatch(getUpdatesAction());
}, [getUpdatesAction])
);
const onRefresh = async () => {
setRefreshing(true);
@@ -37,7 +43,7 @@ const Updates = ({ navigation }) => {
navigation.navigate("Chapter", {
chapterId: item.chapterId,
chapterUrl: item.chapterUrl,
extensionId: item.sourceId,
sourceId: item.sourceId,
novelUrl: item.novelUrl,
chapterName: item.chapterName,
novelId: item.novelId,
@@ -64,26 +70,45 @@ const Updates = ({ navigation }) => {
/>
);
const clearSearchbar = () => {
setSearchText("");
};
const onChangeText = (text) => {
setSearchText(text);
let results = updates.filter((update) =>
update.novelName.toLowerCase().includes(text.toLowerCase())
);
setSearchResults(results);
};
return (
<>
<Appbar theme={theme} dispatch={dispatch} />
<View
style={[
styles.container,
{ backgroundColor: theme.colorPrimaryDark },
]}
>
<FlatList
contentContainerStyle={styles.flatList}
data={updates}
keyExtractor={(item) => item.updateId.toString()}
renderItem={renderItem}
ListFooterComponent={ListFooterComponent()}
ListEmptyComponent={ListEmptyComponent()}
refreshControl={refreshControl()}
/>
</View>
</>
<View
style={[
styles.container,
{ backgroundColor: theme.colorPrimaryDark },
]}
>
<Searchbar
placeholder="Search Updates"
searchText={searchText}
clearSearchbar={clearSearchbar}
onChangeText={onChangeText}
left="magnify"
theme={theme}
right="reload"
onPressRight={() => dispatch(updateLibraryAction())}
/>
<FlatList
contentContainerStyle={styles.flatList}
data={searchText ? searchResults : updates}
keyExtractor={(item) => item.updateId.toString()}
renderItem={renderItem}
ListFooterComponent={ListFooterComponent()}
ListEmptyComponent={ListEmptyComponent()}
refreshControl={refreshControl()}
/>
</View>
);
};
@@ -32,6 +32,7 @@ const UpdateCard = ({ item, theme, onPress }) => {
color: theme.textColorPrimary,
fontSize: 12,
}}
numberOfLines={1}
>
{`${item.chapterName} · ${moment(
item.updateTime
+2
View File
@@ -1,3 +1,5 @@
import { showToast } from "../../Hooks/showToast";
export const fetchSources = async () => {
const url = `https://lnreader-extensions.vercel.app/api/`;
+35 -3
View File
@@ -5,10 +5,23 @@ import { getLibrary } from "../Database/queries/LibraryQueries";
import { restoreLibrary } from "../Database/queries/NovelQueries";
import { showToast } from "../Hooks/showToast";
import * as Notifications from "expo-notifications";
Notifications.setNotificationHandler({
handleNotification: async () => {
return {
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: true,
};
},
});
export const createBackup = async () => {
const novels = await getLibrary();
const permissions = await StorageAccessFramework.requestDirectoryPermissionsAsync();
const permissions =
await StorageAccessFramework.requestDirectoryPermissionsAsync();
if (!permissions.granted) {
return;
@@ -35,10 +48,29 @@ export const createBackup = async () => {
export const restoreBackup = async () => {
const backup = await DocumentPicker.getDocumentAsync();
Notifications.scheduleNotificationAsync({
content: { title: "Restoring backup" },
trigger: null,
});
if (backup.uri) {
let novels = await StorageAccessFramework.readAsStringAsync(backup.uri);
novels = await JSON.parse(novels);
await novels.map((novel) => restoreLibrary(novel));
showToast("Restored backup");
novels.map((novel, index) => {
setTimeout(async () => {
if (index + 1 === novels.length) {
Notifications.scheduleNotificationAsync({
content: {
title: "Library Updated",
body: novels.length + " novels restored",
},
trigger: null,
});
}
await restoreLibrary(novel);
}, 1000 * index);
});
}
};
+16 -2
View File
@@ -113,13 +113,27 @@ export const updateNovelChapters = async (extensionId, novelUrl, novelId) => {
export const updateAllNovels = async () => {
const libraryNovels = await getLibrary();
Notifications.scheduleNotificationAsync({
identifier: "updatingNotif",
content: { title: "Updating library" },
trigger: null,
});
libraryNovels.map((novel, index) =>
setTimeout(async () => {
updateNovelChapters(novel.sourceId, novel.novelUrl, novel.novelId);
await updateNovelChapters(
novel.sourceId,
novel.novelUrl,
novel.novelId
);
console.log(novel.novelName + " Updated");
if (index + 1 === libraryNovels.length) {
Notifications.scheduleNotificationAsync({
content: { title: "Library Updated" },
content: {
title: "Library Updated",
body: libraryNovels.length + " novels updated",
},
trigger: null,
});
}
+15
View File
@@ -0,0 +1,15 @@
export const parseChapterNumber = (chapterName) => {
chapterName = chapterName.toLowerCase();
chapterName = chapterName.replace(/volume (\d+)/, "");
const basic = chapterName.match(/ch (\d+)/);
const occurrence = chapterName.match(/\d+/);
if (basic) {
return basic[0];
} else if (occurrence) {
return occurrence[0];
} else {
return 0;
}
};
+1
View File
@@ -8,6 +8,7 @@ export const fonts = {
domine: require("../../assets/fonts/Domine-VariableFont_wght.ttf"),
lato: require("../../assets/fonts/Lato-Regular.ttf"),
"noto-sans": require("../../assets/fonts/NotoSans-Regular.ttf"),
"noto-sans-bold": require("../../assets/fonts/NotoSans-Bold.ttf"),
"open-sans": require("../../assets/fonts/OpenSans-Regular.ttf"),
"pt-serif": require("../../assets/fonts/PTSerif-Regular.ttf"),
};
+27 -4
View File
@@ -12,7 +12,7 @@ export const theme = {
};
/**
* Light Theme
* Light
*/
export const lightTheme = {
id: 1,
@@ -34,7 +34,7 @@ export const lightTheme = {
};
/**
* Dark Theme
* Dark
*/
export const darkTheme = {
id: 2,
@@ -56,7 +56,7 @@ export const darkTheme = {
};
/**
* Amoled Dark Theme
* Amoled Dark
*/
export const amoledDarkTheme = {
id: 0,
@@ -78,7 +78,7 @@ export const amoledDarkTheme = {
};
/**
* Midnight Dusk Theme
* Midnight Dusk
*/
export const midnightDuskTheme = {
id: 3,
@@ -99,6 +99,10 @@ export const midnightDuskTheme = {
statusBar: "light",
};
/**
* Lime
*/
export const limeTheme = {
id: 4,
name: "Lime",
@@ -117,3 +121,22 @@ export const limeTheme = {
colorButtonText: "#16151D",
statusBar: "light",
};
export const irisBlueTheme = {
id: 5,
name: "Iris Blue",
colorPrimary: "#393e46",
colorPrimaryDark: "#222831",
colorAccent: "#00adb5",
textColorPrimary: "#FFFFFF",
textColorSecondary: "rgba(255,255,255,0.7)",
textColorHint: "rgba(255,255,255,0.5)",
rippleColor: "rgba(255,255,255,0.2)",
searchBarColor: "#393e46",
menuColor: "#393e46",
filterColor: "#FFEB3B",
dividerColor: "rgba(255,255,255,0.1)",
colorDisabled: "#393e46",
colorButtonText: "#16151D",
statusBar: "light",
};
+25 -88
View File
@@ -1,94 +1,31 @@
import { ToastAndroid } from "react-native";
import {
CHAPTER_READ,
GET_CHAPTER,
CHAPTER_LOADING,
CHAPTER_DOWNLOADING,
CHAPTER_DOWNLOADED,
CHAPTER_DELETED,
} from "./chapter.types";
import { getChapter } from "../../Database/queries/ChapterQueries";
import { fetchChapter } from "../../Services/Source/source";
import { SAVE_SCROLL_POSITION } from "../preferences/preference.types";
import { CHAPTER_LOADING, GET_CHAPTER } from "./chapter.types";
import {
markChapterRead,
isChapterDownloaded,
getChapter,
downloadChapter,
deleteChapter,
} from "../../Database/queries/ChapterQueries";
import { SET_LAST_READ } from "../preferences/preference.types";
export const getChapterAction =
(sourceId, novelUrl, chapterUrl, chapterId) => async (dispatch) => {
dispatch({ type: CHAPTER_LOADING });
export const getChapterAction = (
extensionId,
chapterUrl,
novelUrl,
novelId
) => async (dispatch) => {
dispatch({ type: CHAPTER_LOADING });
/**
* Check if chapter is downloaded.
*/
let chapter = await getChapter(chapterId);
let chapter = await getChapter(novelId);
if (!chapter) {
/**
* Fetch chapter from source.
*/
chapter = await fetchChapter(sourceId, novelUrl, chapterUrl);
}
if (!chapter) {
chapter = await fetchChapter(extensionId, novelUrl, chapterUrl);
}
console.log(isDownloaded);
dispatch({ type: GET_CHAPTER, payload: chapter });
};
dispatch({ type: GET_CHAPTER, payload: chapter });
};
// export const markChapterReadAction = (chapterId, novelId) => async (
// dispatch
// ) => {
// await markChapterRead(chapterId);
// console.log("DIspatchedbsjs");
// dispatch({
// type: CHAPTER_READ,
// payload: { chapterId },
// });
// dispatch({
// type: SET_LAST_READ,
// payload: { novelId, chapterId: chapterId + 1 },
// });
// console.log("Set Last Read Diaptched", 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 saveScrollPosition =
(position, percentage, chapterId, novelId) => async (dispatch) => {
dispatch({
type: SAVE_SCROLL_POSITION,
payload: { position, percentage, chapterId, novelId },
});
};
+4 -48
View File
@@ -1,15 +1,7 @@
import {
CHAPTER_READ,
GET_CHAPTER,
CHAPTER_LOADING,
CHAPTER_DOWNLOADING,
CHAPTER_DOWNLOADED,
CHAPTER_DELETED,
} from "./chapter.types";
import { CHAPTER_LOADING, GET_CHAPTER } from "./chapter.types";
const initialState = {
chapter: null,
downloading: [],
chapter: {},
loading: true,
};
@@ -17,46 +9,10 @@ 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,
};
return { ...state, loading: 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
),
};
return { ...state, chapter: payload, loading: false };
default:
return state;
}
+1 -5
View File
@@ -1,6 +1,2 @@
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 CHAPTER_LOADING = "CHAPTER_LOADING";
+1 -3
View File
@@ -24,9 +24,7 @@ export const getHistoryAction = () => async (dispatch) => {
export const insertHistoryAction = (novelId, chapterId) => async (dispatch) => {
await insertHistory(novelId, chapterId);
dispatch({
type: UPDATE_NOVEL_HISTORY,
});
// dispatch({ type: UPDATE_NOVEL_HISTORY });
dispatch({
type: SET_LAST_READ,
+2 -2
View File
@@ -19,8 +19,8 @@ const historyReducer = (state = initialState, action) => {
return { ...state, loading: true };
case GET_HISTORY:
return { ...state, history: payload, loading: false };
case UPDATE_NOVEL_HISTORY:
return state;
// case UPDATE_NOVEL_HISTORY:
// return state;
case CLEAR_NOVEL_HISTORY:
return {
...state,
+30 -11
View File
@@ -1,6 +1,8 @@
import {
GET_LIBRARY_NOVELS,
GET_LIBRARY_SEARCH_RESULTS,
SET_LIBRARY_LOADING,
SORT_FILTER_LIBRARY,
} from "./library.types";
import {
getLibrary,
@@ -8,8 +10,9 @@ import {
} from "../../Database/queries/LibraryQueries";
import { restoreBackup } from "../../Services/backup";
export const getLibraryAction = () => async (dispatch) => {
const res = await getLibrary();
export const getLibraryAction = (sort, filter) => async (dispatch) => {
// console.log("1.", sort, filter);
const res = await getLibrary(sort, filter);
dispatch({
type: GET_LIBRARY_NOVELS,
@@ -17,22 +20,38 @@ export const getLibraryAction = () => async (dispatch) => {
});
};
export const searchLibraryAction = (searchText) => async (dispatch) => {
const res = await searchLibrary(searchText);
export const searchLibraryAction =
(searchText, sort, filter) => async (dispatch) => {
const res = await searchLibrary(searchText, sort, filter);
dispatch({
type: GET_LIBRARY_SEARCH_RESULTS,
payload: res,
});
};
export const filterLibrary = (sort, filter) => async (dispatch) => {
dispatch({ type: SORT_FILTER_LIBRARY, payload: { sort, filter } });
const res = await getLibrary(sort, filter);
dispatch({
type: GET_LIBRARY_SEARCH_RESULTS,
type: GET_LIBRARY_NOVELS,
payload: res,
});
};
export const restoreLibraryAction = () => async (dispatch) => {
await restoreBackup();
dispatch({ type: SET_LIBRARY_LOADING });
const res = await getLibrary();
const res = await restoreBackup();
dispatch({
type: GET_LIBRARY_NOVELS,
payload: res,
});
if (res) {
const novels = await getLibrary();
dispatch({
type: GET_LIBRARY_NOVELS,
payload: novels,
});
}
};
+8 -7
View File
@@ -1,16 +1,14 @@
import {
FOLLOW_NOVEL,
UNFOLLOW_NOVEL,
UPDATE_IN_LIBRARY,
} from "../novel/novel.types";
import {
GET_LIBRARY_NOVELS,
GET_LIBRARY_SEARCH_RESULTS,
SET_LIBRARY_LOADING,
SORT_FILTER_LIBRARY,
} from "./library.types";
const initialState = {
novels: [],
searchResults: [],
filters: { sort: "novels.novelId ASC", filter: "" },
loading: true,
};
@@ -18,14 +16,17 @@ const libraryReducer = (state = initialState, action) => {
const { type, payload } = action;
switch (type) {
case SET_LIBRARY_LOADING:
return { ...state, loading: true };
case GET_LIBRARY_NOVELS:
return { ...state, novels: payload, loading: false };
case GET_LIBRARY_SEARCH_RESULTS:
return { ...state, novels: payload, loading: false };
case UPDATE_IN_LIBRARY:
case SORT_FILTER_LIBRARY:
console.log("2.", payload.sort, payload.filter);
return {
...state,
novels: [...state.novels, { novelUrl: payload.novelUrl }],
filters: { sort: payload.sort, filter: payload.filter },
};
default:
return state;
+3
View File
@@ -1,2 +1,5 @@
export const GET_LIBRARY_NOVELS = "GET_LIBRARY_NOVELS";
export const GET_LIBRARY_SEARCH_RESULTS = "GET_LIBRARY_SEARCH_RESULTS";
export const SET_LIBRARY_LOADING = "SET_LIBRARY_LOADING";
export const SORT_FILTER_LIBRARY = "SORT_FILTER_LIBRARY";
+170 -155
View File
@@ -8,17 +8,19 @@ import {
SET_NOVEL,
UPDATE_IN_LIBRARY,
CHAPTER_READ,
GET_CHAPTER,
CHAPTER_LOADING,
CHAPTER_DOWNLOADING,
CHAPTER_DOWNLOADED,
CHAPTER_DELETED,
UPDATE_NOVEL,
SET_NOVEL_SETTINGS,
CHAPTER_UNREAD,
BOOKMARK_CHAPTER,
MARK_PREVIOUS_CHAPTERS_READ,
MARK_PREVIOUS_CHAPTERS_UNREAD,
} from "./novel.types";
import { updateNovel } from "../../Services/updates";
import { fetchChapter, fetchNovel } from "../../Services/Source/source";
import { fetchNovel } from "../../Services/Source/source";
import {
followNovel,
insertNovel,
@@ -28,10 +30,12 @@ import {
getChapters,
insertChapters,
markChapterRead,
isChapterDownloaded,
getChapter,
downloadChapter,
deleteChapter,
markChapterUnread,
bookmarkChapter,
markPreviuschaptersRead,
markPreviousChaptersUnread,
} from "../../Database/queries/ChapterQueries";
import { deleteNovelUpdates } from "../../Database/queries/UpdateQueries";
import {
@@ -40,71 +44,84 @@ import {
} from "../preferences/preference.types";
import { GET_LIBRARY_NOVELS } from "../library/library.types";
import { getLibrary } from "../../Database/queries/LibraryQueries";
import { showToast } from "../../Hooks/showToast";
export const setNovel = (novel) => async (dispatch) => {
dispatch({ type: SET_NOVEL, payload: novel });
};
export const getNovelAction = (
followed,
sourceId,
novelUrl,
novelId,
sort,
filter
) => async (dispatch) => {
dispatch({ type: LOADING_NOVEL });
export const getNovelAction =
(followed, sourceId, novelUrl, novelId, sort, filter) =>
async (dispatch) => {
dispatch({ type: LOADING_NOVEL });
if (followed === 1) {
if (followed === 1) {
/**
* If novel is followed directly get the chapters from db.
*/
const chapters = await getChapters(novelId, sort, filter);
dispatch({
type: GET_CHAPTERS,
payload: chapters,
});
} else {
dispatch({ type: FETCHING_NOVEL });
/**
* Check if novel is cached.
*/
const novel = await getNovel(sourceId, novelUrl);
if (novel) {
/**
* Get chapters from db.
*/
novel.chapters = await getChapters(novel.novelId, sort, filter);
dispatch({
type: GET_NOVEL,
payload: novel,
});
} else {
/**
* Fetch novel from source.
*/
const fetchedNovel = await fetchNovel(sourceId, novelUrl);
/**
* Insert novel in db.
*/
const fetchedNovelId = await insertNovel(fetchedNovel);
await insertChapters(fetchedNovelId, fetchedNovel.chapters);
/**
* Get novel from db.
*/
const novel = await getNovel(sourceId, novelUrl);
novel.chapters = await getChapters(novel.novelId);
dispatch({
type: GET_NOVEL,
payload: novel,
});
}
}
};
export const sortAndFilterChapters =
(novelId, sort, filter) => async (dispatch) => {
const chapters = await getChapters(novelId, sort, filter);
dispatch({
type: GET_CHAPTERS,
payload: chapters,
});
} else {
dispatch({ type: FETCHING_NOVEL });
const novel = await getNovel(sourceId, novelUrl);
if (novel) {
novel.chapters = await getChapters(novel.novelId, sort, filter);
dispatch({
type: GET_NOVEL,
payload: novel,
});
} else {
const fetchedNovel = await fetchNovel(sourceId, novelUrl);
const fetchedNovelId = await insertNovel(fetchedNovel);
await insertChapters(fetchedNovelId, fetchedNovel.chapters);
const novel = await getNovel(sourceId, novelUrl);
novel.chapters = await getChapters(novel.novelId);
dispatch({
type: GET_NOVEL,
payload: novel,
});
}
}
};
export const sortAndFilterChapters = (novelId, sort, filter) => async (
dispatch
) => {
const chapters = await getChapters(novelId, sort, filter);
dispatch({
type: GET_CHAPTERS,
payload: chapters,
});
dispatch({
type: SET_CHAPTER_LIST_PREF,
payload: { novelId, sort, filter },
});
};
dispatch({
type: SET_CHAPTER_LIST_PREF,
payload: { novelId, sort, filter },
});
};
export const followNovelAction = (novel) => async (dispatch) => {
await followNovel(novel.followed, novel.novelId);
@@ -125,107 +142,106 @@ export const followNovelAction = (novel) => async (dispatch) => {
payload: res,
});
ToastAndroid.show(
!novel.followed ? "Added to library" : "Removed from library",
ToastAndroid.SHORT
);
showToast(!novel.followed ? "Added to library" : "Removed from library");
};
export const getChapterAction = (
extensionId,
chapterUrl,
novelUrl,
novelId
) => async (dispatch) => {
dispatch({ type: CHAPTER_LOADING });
export const bookmarkChapterAction =
(bookmark, chapterId) => async (dispatch) => {
await bookmarkChapter(bookmark, chapterId);
const isDownloaded = await isChapterDownloaded(novelId);
let chapter;
dispatch({
type: BOOKMARK_CHAPTER,
payload: { bookmark, chapterId },
});
};
if (isDownloaded) {
chapter = await getChapter(novelId);
} else {
chapter = await fetchChapter(extensionId, novelUrl, chapterUrl);
}
dispatch({ type: GET_CHAPTER, payload: chapter });
export const markChapterReadAction =
(chapterId, novelId) => async (dispatch) => {
await markChapterRead(chapterId);
dispatch({ type: CHAPTER_READ, payload: { chapterId } });
dispatch({
type: SET_LAST_READ,
payload: { novelId, chapterId: chapterId + 1 },
});
};
export const markPreviousChaptersReadAction =
(chapterId, novelId) => async (dispatch) => {
await markPreviuschaptersRead(chapterId, novelId);
dispatch({ type: MARK_PREVIOUS_CHAPTERS_READ, payload: chapterId });
};
export const markPreviousChaptersUnreadAction =
(chapterId, novelId) => async (dispatch) => {
await markPreviousChaptersUnread(chapterId, novelId);
dispatch({ type: MARK_PREVIOUS_CHAPTERS_UNREAD, payload: chapterId });
};
export const markChapterUnreadAction = (chapterId) => async (dispatch) => {
await markChapterUnread(chapterId);
dispatch({ type: CHAPTER_UNREAD, payload: { chapterId } });
};
export const markChapterReadAction = (chapterId, novelId) => async (
dispatch
) => {
await markChapterRead(chapterId);
dispatch({ type: CHAPTER_READ, payload: { chapterId } });
dispatch({
type: SET_LAST_READ,
payload: { novelId, chapterId: chapterId + 1 },
});
};
export const downloadChapterAction =
(extensionId, novelUrl, chapterUrl, chapterName, chapterId) =>
async (dispatch) => {
dispatch({
type: CHAPTER_DOWNLOADING,
payload: chapterId,
});
export const downloadChapterAction = (
extensionId,
novelUrl,
chapterUrl,
chapterName,
chapterId
) => async (dispatch) => {
dispatch({
type: CHAPTER_DOWNLOADING,
payload: chapterId,
});
await downloadChapter(extensionId, novelUrl, chapterUrl, chapterId);
await downloadChapter(extensionId, novelUrl, chapterUrl, chapterId);
dispatch({
type: CHAPTER_DOWNLOADED,
payload: chapterId,
});
dispatch({
type: CHAPTER_DOWNLOADED,
payload: chapterId,
});
ToastAndroid.show(`Downloaded ${chapterName}`, ToastAndroid.SHORT);
};
ToastAndroid.show(`Downloaded ${chapterName}`, ToastAndroid.SHORT);
};
export const downloadAllChaptersAction =
(extensionId, novelUrl, chapters) => async (dispatch) => {
await chapters.map((chapter, index) => {
setTimeout(async () => {
dispatch({
type: CHAPTER_DOWNLOADING,
payload: chapter.chapterId,
});
export const downloadAllChaptersAction = (
extensionId,
novelUrl,
chapters
) => async (dispatch) => {
await chapters.map((chapter, index) => {
setTimeout(async () => {
dispatch({
type: CHAPTER_DOWNLOADING,
payload: chapter.chapterId,
});
if (!chapter.downloaded) {
await downloadChapter(
extensionId,
novelUrl,
chapter.chapterUrl,
chapter.chapterId
);
}
if (!chapter.downloaded) {
await downloadChapter(
extensionId,
novelUrl,
chapter.chapterUrl,
chapter.chapterId
);
}
dispatch({
type: CHAPTER_DOWNLOADED,
payload: chapter.chapterId,
});
}, 1000 * index);
});
dispatch({
type: CHAPTER_DOWNLOADED,
payload: chapter.chapterId,
});
}, 1000 * index);
});
showToast(`All chapters downloaded`);
};
ToastAndroid.show(`All chapters downloaded`, ToastAndroid.SHORT);
};
export const deleteChapterAction =
(chapterId, chapterName) => async (dispatch) => {
await deleteChapter(chapterId);
export const deleteChapterAction = (chapterId, chapterName) => async (
dispatch
) => {
await deleteChapter(chapterId);
dispatch({
type: CHAPTER_DELETED,
payload: chapterId,
});
dispatch({
type: CHAPTER_DELETED,
payload: chapterId,
});
ToastAndroid.show(`Deleted ${chapterName}`, ToastAndroid.SHORT);
};
showToast(`Deleted ${chapterName}`);
};
export const deleteAllChaptersAction = (chapters) => async (dispatch) => {
await chapters.map((chapter) => {
@@ -237,21 +253,20 @@ export const deleteAllChaptersAction = (chapters) => async (dispatch) => {
});
});
ToastAndroid.show(`Deleted all chapters`, ToastAndroid.SHORT);
showToast("Deleted all chapters");
};
export const updateNovelAction = (sourceId, novelUrl, novelId) => async (
dispatch
) => {
dispatch({ type: FETCHING_NOVEL });
export const updateNovelAction =
(sourceId, novelUrl, novelId, sort, filter) => async (dispatch) => {
dispatch({ type: FETCHING_NOVEL });
await updateNovel(sourceId, novelUrl, novelId);
await updateNovel(sourceId, novelUrl, novelId);
let novel = await getNovel(sourceId, novelUrl);
let chapters = await getChapters(novel.novelId);
let novel = await getNovel(sourceId, novelUrl);
let chapters = await getChapters(novel.novelId, sort, filter);
dispatch({
type: UPDATE_NOVEL,
payload: { novel, chapters },
});
};
dispatch({
type: UPDATE_NOVEL,
payload: { novel, chapters },
});
};
+50 -21
View File
@@ -5,24 +5,24 @@ import {
GET_CHAPTERS,
LOADING_NOVEL,
GET_NOVEL,
GET_CHAPTER,
FETCHING_NOVEL,
SET_NOVEL,
UPDATE_IN_LIBRARY,
CHAPTER_READ,
CHAPTER_LOADING,
UPDATE_NOVEL,
UPDATE_LAST_READ,
CHAPTER_UNREAD,
BOOKMARK_CHAPTER,
MARK_PREVIOUS_CHAPTERS_READ,
MARK_PREVIOUS_CHAPTERS_UNREAD,
} from "./novel.types";
const initialState = {
novel: null,
novel: {},
chapters: [],
chapter: null,
downloading: [],
chapterLoading: true,
loading: true,
fetching: false,
updating: false,
downloading: [],
};
const novelReducer = (state = initialState, action) => {
@@ -32,7 +32,7 @@ const novelReducer = (state = initialState, action) => {
case LOADING_NOVEL:
return { ...state, loading: true };
case FETCHING_NOVEL:
return { ...state, fetching: true };
return { ...state, updating: true };
case SET_NOVEL:
return { ...state, novel: payload };
case GET_NOVEL:
@@ -41,14 +41,14 @@ const novelReducer = (state = initialState, action) => {
novel: payload,
chapters: payload.chapters,
loading: false,
fetching: false,
updating: false,
};
case GET_CHAPTERS:
return {
...state,
chapters: payload,
loading: false,
fetching: false,
updating: false,
};
case UPDATE_IN_LIBRARY:
return {
@@ -61,7 +61,7 @@ const novelReducer = (state = initialState, action) => {
novel: payload.novel,
chapters: payload.chapters,
loading: false,
fetching: false,
updating: false,
};
case CHAPTER_READ:
return {
@@ -72,22 +72,29 @@ const novelReducer = (state = initialState, action) => {
: chapter
),
};
case CHAPTER_LOADING:
return {
...state,
chapterLoading: true,
};
case GET_CHAPTER:
return { ...state, chapter: payload, chapterLoading: false };
case CHAPTER_DOWNLOADING:
case CHAPTER_UNREAD:
return {
...state,
chapters: state.chapters.map((chapter) =>
chapter.chapterId === payload
? { ...chapter, downloaded: 3 }
chapter.chapterId === payload.chapterId
? { ...chapter, read: 0 }
: chapter
),
};
case BOOKMARK_CHAPTER:
return {
...state,
chapters: state.chapters.map((chapter) =>
chapter.chapterId === payload.chapterId
? { ...chapter, bookmark: !payload.bookmark }
: chapter
),
};
case CHAPTER_DOWNLOADING:
return {
...state,
downloading: [...state.downloading, payload],
};
case CHAPTER_DOWNLOADED:
return {
...state,
@@ -96,6 +103,9 @@ const novelReducer = (state = initialState, action) => {
? { ...chapter, downloaded: 1 }
: chapter
),
downloading: state.downloading.filter(
(chapterId) => chapterId !== payload
),
};
case CHAPTER_DELETED:
return {
@@ -111,6 +121,25 @@ const novelReducer = (state = initialState, action) => {
...state,
novel: { ...state.novel, lastRead: payload },
};
case MARK_PREVIOUS_CHAPTERS_READ:
return {
...state,
chapters: state.chapters.map((chapter) =>
chapter.chapterId < payload
? { ...chapter, read: 1 }
: chapter
),
};
case MARK_PREVIOUS_CHAPTERS_UNREAD:
return {
...state,
chapters: state.chapters.map((chapter) =>
chapter.chapterId < payload
? { ...chapter, read: 0 }
: chapter
),
};
default:
return state;
}
+5 -3
View File
@@ -12,10 +12,12 @@ 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 CHAPTER_UNREAD = "CHAPTER_UNREAD";
export const GET_CHAPTER = "GET_CHAPTER";
export const FOLLOW_NOVEL = "FOLLOW_NOVEL";
export const UNFOLLOW_NOVEL = "UNFOLLOW_NOVEL";
export const BOOKMARK_CHAPTER = "BOOKMARK_CHAPTER";
export const SET_NOVEL_SETTINGS = "SET_NOVEL_SETTINGS";
export const MARK_PREVIOUS_CHAPTERS_READ = "MARK_PREVIOUS_CHAPTERS_READ";
export const MARK_PREVIOUS_CHAPTERS_UNREAD = "MARK_PREVIOUS_CHAPTERS_UNREAD";
+39 -36
View File
@@ -1,7 +1,11 @@
import { SET_CHAPTER_LIST_PREF, SET_LAST_READ } from "./preference.types";
import {
SAVE_SCROLL_POSITION,
SET_CHAPTER_LIST_PREF,
SET_LAST_READ,
} from "./preference.types";
const initialState = {
novelSettings: [],
novelSettings: {},
};
const novelReducer = (state = initialState, action) => {
@@ -11,44 +15,43 @@ const novelReducer = (state = initialState, action) => {
case SET_LAST_READ:
return {
...state,
novelSettings: !state.novelSettings.some(
(novel) => novel.novelId === payload.novelId
)
? [
{
novelId: payload.novelId,
lastRead: payload.chapterId,
},
]
: state.novelSettings.map((novel) =>
novel.novelId === payload.novelId
? { ...novel, lastRead: payload.chapterId }
: novel
),
novelSettings: {
...state.novelSettings,
[payload.novelId]: {
...state.novelSettings[payload.novelId],
lastRead: payload.chapterId,
},
},
};
case SET_CHAPTER_LIST_PREF:
return {
...state,
novelSettings: state.novelSettings.some(
(novel) => novel.novelId === payload.novelId
)
? state.novelSettings.map((novel) =>
novel.novelId === payload.novelId
? {
...novel,
sort: payload.sort,
filter: payload.filter,
}
: novel
)
: [
...state.novelSettings,
{
novelId: payload.novelId,
sort: payload.sort,
filter: payload.filter,
},
],
novelSettings: {
...state.novelSettings,
[payload.novelId]: {
...state.novelSettings[payload.novelId],
sort: payload.sort,
filter: payload.filter,
},
},
};
case SAVE_SCROLL_POSITION:
return {
...state,
novelSettings: {
...state.novelSettings,
[payload.novelId]: {
...state.novelSettings[payload.novelId],
position: {
...state.novelSettings[payload.novelId].position,
[payload.chapterId]: {
position: payload.position,
percentage: payload.percentage,
},
},
},
},
};
default:
return state;
@@ -1,2 +1,3 @@
export const SET_CHAPTER_LIST_PREF = "SET_CHAPTER_LIST_PREF";
export const SET_LAST_READ = "SET_LAST_READ";
export const SAVE_SCROLL_POSITION = "SAVE_SCROLL_POSITION";
+3 -1
View File
@@ -15,6 +15,7 @@ import {
darkTheme,
midnightDuskTheme,
limeTheme,
irisBlueTheme,
} from "../../Theme/theme";
const themes = {
@@ -23,6 +24,7 @@ const themes = {
2: darkTheme,
3: midnightDuskTheme,
4: limeTheme,
5: irisBlueTheme,
};
const initialState = {
@@ -35,7 +37,7 @@ const initialState = {
padding: 5,
fontFamily: null,
},
theme: themes[2],
theme: themes[5],
};
/**
+3 -1
View File
@@ -6,6 +6,7 @@ import thunk from "redux-thunk";
import settingsReducer from "./settings/settings.reducer";
import libraryReducer from "./library/library.reducer";
import sourceReducer from "./source/source.reducers";
import chapterReducer from "./chapter/chapter.reducer";
import novelReducer from "./novel/novel.reducer";
import historyReducer from "./history/history.reducer";
import updatesReducer from "./updates/updates.reducer";
@@ -15,7 +16,7 @@ import preferenceReducer from "./preferences/preference.reducer";
const persistConfig = {
key: "root",
storage: AsyncStorage,
blacklist: ["novelReducer"],
blacklist: ["novelReducer", "chapterReducer"],
};
const persistedReducer = persistReducer(
@@ -26,6 +27,7 @@ const persistedReducer = persistReducer(
sourceReducer,
novelReducer,
historyReducer,
chapterReducer,
updatesReducer,
trackerReducer,
preferenceReducer,
+1
View File
@@ -2,6 +2,7 @@ import { GET_UPDATES } from "./updates.types";
import { getUpdates } from "../../Database/queries/UpdateQueries";
import { updateAllNovels } from "../../Services/updates";
import { showToast } from "../../Hooks/showToast";
export const getUpdatesAction = () => async (dispatch) => {