feat: Add Change Default Category (Closes #383)
This commit is contained in:
@@ -14,7 +14,7 @@ import color from 'color';
|
||||
interface Props {
|
||||
label: string;
|
||||
status: boolean;
|
||||
onPress: () => void;
|
||||
onPress?: () => void;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
labelStyle?: StyleProp<TextStyle>;
|
||||
theme: MD3ThemeType;
|
||||
|
||||
@@ -11,3 +11,5 @@ export { default as SwitchItem } from './SwitchItem/SwitchItem';
|
||||
export { default as List } from './List/List';
|
||||
export { default as ColorPreferenceItem } from './ColorPreferenceItem/ColorPreferenceItem';
|
||||
export { default as LoadingMoreIndicator } from './LoadingMoreIndicator/LoadingMoreIndicator';
|
||||
export { Checkbox } from './Checkbox/Checkbox';
|
||||
export { RadioButton } from './RadioButton/RadioButton';
|
||||
|
||||
@@ -12,7 +12,7 @@ import { txnErrorCallback } from '../utils/helpers';
|
||||
import { noop } from 'lodash';
|
||||
|
||||
const insertNovelQuery =
|
||||
'INSERT INTO novels (novelUrl, sourceUrl, sourceId, source, novelName, novelCover, novelSummary, author, artist, status, genre) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
|
||||
'INSERT INTO novels (novelUrl, sourceUrl, sourceId, source, novelName, novelCover, novelSummary, author, artist, status, genre, categoryIds) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
|
||||
|
||||
export const insertNovel = async novel => {
|
||||
return new Promise((resolve, reject) =>
|
||||
@@ -31,11 +31,10 @@ export const insertNovel = async novel => {
|
||||
novel.artist,
|
||||
novel.status,
|
||||
novel.genre,
|
||||
JSON.stringify(novel.categoryIds),
|
||||
],
|
||||
(txObj, { insertId }) => resolve(insertId),
|
||||
(txObj, error) => {
|
||||
// console.log('Error ', error)
|
||||
},
|
||||
txnErrorCallback,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -69,6 +69,7 @@ export const insertNovelInLibrary = async (
|
||||
sourceId: number,
|
||||
novelUrl: string,
|
||||
inLibrary: boolean,
|
||||
defaultCategoryId: number,
|
||||
) => {
|
||||
if (inLibrary) {
|
||||
showToast(getString('browseScreen.removeFromLibrary'));
|
||||
@@ -131,10 +132,11 @@ export const insertNovelInLibrary = async (
|
||||
artist,
|
||||
status,
|
||||
genre,
|
||||
followed
|
||||
followed,
|
||||
categoryIds
|
||||
)
|
||||
VALUES
|
||||
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)`,
|
||||
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?)`,
|
||||
[
|
||||
novel.novelUrl,
|
||||
novel.sourceUrl,
|
||||
@@ -147,6 +149,7 @@ export const insertNovelInLibrary = async (
|
||||
novel.artist,
|
||||
novel.status,
|
||||
novel.genre,
|
||||
JSON.stringify([defaultCategoryId]),
|
||||
],
|
||||
(txObj, { insertId }) => {
|
||||
novel.chapters?.map(chapter => {
|
||||
|
||||
@@ -6,3 +6,6 @@ export { default as useChapter } from './useChapter';
|
||||
export { default as useNextAndPrevChapter } from './useNextAndPrevChapter';
|
||||
export { default as useNovelTrackerInfo } from './useNovelTrackerInfo';
|
||||
export { default as useBoolean } from './useBoolean';
|
||||
export { useTheme } from './useTheme';
|
||||
export { default as useCategories } from './useCategories';
|
||||
export { useCategorySettings, useLibrarySettings } from './useSettings';
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { getCategoriesFromDb } from '@database/queries/CategoryQueries';
|
||||
import { Category } from '@database/types';
|
||||
|
||||
const useCategories = () => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [categories, setCategories] = useState<Category[]>([]);
|
||||
const [error, setError] = useState<string>();
|
||||
|
||||
const getCategories = async () => {
|
||||
try {
|
||||
const res = await getCategoriesFromDb();
|
||||
setCategories(res);
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
setError(err.message);
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getCategories();
|
||||
}, []);
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
categories,
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
||||
export default useCategories;
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useMMKVObject } from 'react-native-mmkv';
|
||||
|
||||
import { LibrarySettings } from '../utils/constants/settings';
|
||||
import { CategorySettings, LibrarySettings } from '../utils/constants/settings';
|
||||
|
||||
export const useLibrarySettings = () => {
|
||||
const [librarySettings, setSettings] =
|
||||
@@ -14,3 +13,16 @@ export const useLibrarySettings = () => {
|
||||
setLibrarySettings,
|
||||
};
|
||||
};
|
||||
|
||||
export const useCategorySettings = () => {
|
||||
const [categorySettings, setSettings] =
|
||||
useMMKVObject<CategorySettings>('CategorySettings');
|
||||
|
||||
const setCategorySettings = (value: Partial<CategorySettings>) =>
|
||||
setSettings({ ...categorySettings, ...value });
|
||||
|
||||
return {
|
||||
...categorySettings,
|
||||
setCategorySettings,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -14,6 +14,7 @@ import DownloadQueue from '../screens/more/DownloadQueueScreen';
|
||||
import Downloads from '../screens/more/DownloadsScreen';
|
||||
import AppearanceSettings from '../screens/settings/SettingsAppearanceScreen';
|
||||
import CategoriesScreen from '@screens/Categories/CategoriesScreen';
|
||||
import LibrarySettings from '@screens/settings/SettingsLibraryScreen/SettingsLibraryScreen';
|
||||
|
||||
const Stack = createStackNavigator();
|
||||
|
||||
@@ -28,6 +29,7 @@ const SettingsStack = () => (
|
||||
<Stack.Screen name="BackupSettings" component={BackupSettings} />
|
||||
<Stack.Screen name="AppearanceSettings" component={AppearanceSettings} />
|
||||
<Stack.Screen name="AdvancedSettings" component={AdvancedSettings} />
|
||||
<Stack.Screen name="LibrarySettings" component={LibrarySettings} />
|
||||
</Stack.Navigator>
|
||||
);
|
||||
|
||||
|
||||
@@ -64,7 +64,8 @@ export const setNovel = novel => async dispatch => {
|
||||
};
|
||||
|
||||
export const getNovelAction =
|
||||
(followed, sourceId, novelUrl, novelId, sort, filter) => async dispatch => {
|
||||
(followed, sourceId, novelUrl, novelId, sort, filter, defaultCategoryId) =>
|
||||
async dispatch => {
|
||||
try {
|
||||
dispatch({ type: LOADING_NOVEL });
|
||||
|
||||
@@ -104,7 +105,10 @@ export const getNovelAction =
|
||||
/**
|
||||
* Insert novel in db.
|
||||
*/
|
||||
const fetchedNovelId = await insertNovel(fetchedNovel);
|
||||
const fetchedNovelId = await insertNovel({
|
||||
...fetchedNovel,
|
||||
categoryIds: [defaultCategoryId],
|
||||
});
|
||||
await insertChapters(fetchedNovelId, fetchedNovel.chapters);
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,7 +15,7 @@ import NovelList from '@components/NovelList';
|
||||
import NovelCover from '@components/NovelCover';
|
||||
import FilterBottomSheet from './components/BottomSheet';
|
||||
|
||||
import { useSearch } from '../../hooks';
|
||||
import { useCategorySettings, useSearch } from '../../hooks';
|
||||
import { useTheme } from '@hooks/useTheme';
|
||||
import { useBrowseSource, useSearchSource } from './useBrowseSource';
|
||||
|
||||
@@ -59,6 +59,7 @@ const BrowseSourceScreen: React.FC<BrowseSourceScreenProps> = ({ route }) => {
|
||||
clearFilters,
|
||||
} = useBrowseSource(sourceId, showLatestNovels);
|
||||
|
||||
const { defaultCategoryId = 1 } = useCategorySettings();
|
||||
const {
|
||||
isSearching,
|
||||
searchResults,
|
||||
@@ -155,7 +156,12 @@ const BrowseSourceScreen: React.FC<BrowseSourceScreenProps> = ({ route }) => {
|
||||
];
|
||||
}
|
||||
});
|
||||
insertNovelInLibrary(sourceId, item.novelUrl, inLibrary);
|
||||
insertNovelInLibrary(
|
||||
sourceId,
|
||||
item.novelUrl,
|
||||
inLibrary,
|
||||
defaultCategoryId,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -64,6 +64,7 @@ import FadeView from '../../components/Common/CrossFadeView';
|
||||
import DownloadCustomChapterModal from './components/DownloadCustomChapterModal';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import useBoolean from '@hooks/useBoolean';
|
||||
import { useCategorySettings } from '@hooks/useSettings';
|
||||
|
||||
const Novel = ({ route, navigation }) => {
|
||||
const item = route.params;
|
||||
@@ -104,9 +105,19 @@ const Novel = ({ route, navigation }) => {
|
||||
novel.novelId,
|
||||
);
|
||||
|
||||
const { defaultCategoryId = 1 } = useCategorySettings();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(
|
||||
getNovelAction(followed, sourceId, novelUrl, novelId, sort, filter),
|
||||
getNovelAction(
|
||||
followed,
|
||||
sourceId,
|
||||
novelUrl,
|
||||
novelId,
|
||||
sort,
|
||||
filter,
|
||||
defaultCategoryId,
|
||||
),
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [getNovelAction]);
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import React from 'react';
|
||||
import { FlatList, StyleSheet } from 'react-native';
|
||||
import { Button, Dialog } from 'react-native-paper';
|
||||
|
||||
import { RadioButton } from '@components';
|
||||
|
||||
import { getString } from '@strings/translations';
|
||||
import { useTheme } from '@hooks';
|
||||
|
||||
import { Category } from '@database/types';
|
||||
import { getDialogBackground } from '@theme/colors';
|
||||
|
||||
interface DefaultCategoryDialogProps {
|
||||
visible: boolean;
|
||||
hideDialog: () => void;
|
||||
categories: Category[];
|
||||
defaultCategoryId: number;
|
||||
setDefaultCategory: (categoryId: number) => void;
|
||||
}
|
||||
|
||||
const DefaultCategoryDialog: React.FC<DefaultCategoryDialogProps> = ({
|
||||
categories,
|
||||
defaultCategoryId,
|
||||
hideDialog,
|
||||
visible,
|
||||
setDefaultCategory,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
visible={visible}
|
||||
onDismiss={hideDialog}
|
||||
style={{ backgroundColor: getDialogBackground(theme) }}
|
||||
>
|
||||
<Dialog.Title style={{ color: theme.textColorPrimary }}>
|
||||
{getString('categories.defaultCategory')}
|
||||
</Dialog.Title>
|
||||
<FlatList
|
||||
style={styles.scrollArea}
|
||||
initialNumToRender={10}
|
||||
data={categories}
|
||||
renderItem={({ item }) => (
|
||||
<RadioButton
|
||||
status={item.id === defaultCategoryId}
|
||||
label={item.name}
|
||||
onPress={() => setDefaultCategory(item.id)}
|
||||
theme={theme}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Dialog.Actions>
|
||||
<Button
|
||||
theme={{ colors: { primary: theme.primary } }}
|
||||
onPress={hideDialog}
|
||||
>
|
||||
{getString('common.cancel')}
|
||||
</Button>
|
||||
</Dialog.Actions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default DefaultCategoryDialog;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
scrollArea: {
|
||||
maxHeight: 480,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
import React from 'react';
|
||||
import { Appbar, List } from '@components';
|
||||
import { getString } from '@strings/translations';
|
||||
import {
|
||||
useBoolean,
|
||||
useCategories,
|
||||
useCategorySettings,
|
||||
useTheme,
|
||||
} from '@hooks';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { Portal } from 'react-native-paper';
|
||||
import DefaultCategoryDialog from './DefaultCategoryDialog';
|
||||
|
||||
const SettingsLibraryScreen = () => {
|
||||
const theme = useTheme();
|
||||
const { goBack, navigate } = useNavigation();
|
||||
const { defaultCategoryId, setCategorySettings } = useCategorySettings();
|
||||
const { categories } = useCategories();
|
||||
|
||||
const defaultCategoryDialog = useBoolean();
|
||||
|
||||
const setDefaultCategoryId = (categoryId: number) =>
|
||||
setCategorySettings({ defaultCategoryId: categoryId });
|
||||
|
||||
return (
|
||||
<>
|
||||
<Appbar
|
||||
title={getString('library')}
|
||||
handleGoBack={goBack}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Section>
|
||||
<List.Item
|
||||
title={getString('categories.header')}
|
||||
description={`${categories.length} ${getString(
|
||||
'common.categories',
|
||||
).toLowerCase()}`}
|
||||
onPress={() =>
|
||||
navigate('MoreStack' as never, { screen: 'Categories' } as never)
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Item
|
||||
title={getString('categories.defaultCategory')}
|
||||
description={
|
||||
categories.find(category => category.id === defaultCategoryId)?.name
|
||||
}
|
||||
onPress={defaultCategoryDialog.setTrue}
|
||||
theme={theme}
|
||||
/>
|
||||
</List.Section>
|
||||
<Portal>
|
||||
<DefaultCategoryDialog
|
||||
categories={categories}
|
||||
defaultCategoryId={defaultCategoryId || 1}
|
||||
visible={defaultCategoryDialog.value}
|
||||
hideDialog={defaultCategoryDialog.setFalse}
|
||||
setDefaultCategory={setDefaultCategoryId}
|
||||
/>
|
||||
</Portal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsLibraryScreen;
|
||||
@@ -24,6 +24,26 @@ const SettingsScreen = ({ navigation }) => {
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Item
|
||||
title="Appearance"
|
||||
icon="palette-outline"
|
||||
onPress={() =>
|
||||
navigation.navigate('SettingsStack', {
|
||||
screen: 'AppearanceSettings',
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Item
|
||||
title={getString('library')}
|
||||
icon="book-variant-multiple"
|
||||
onPress={() =>
|
||||
navigation.navigate('SettingsStack', {
|
||||
screen: 'LibrarySettings',
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Item
|
||||
title={getString('moreScreen.settingsScreen.readerSettings.title')}
|
||||
icon="book-open-outline"
|
||||
@@ -54,16 +74,6 @@ const SettingsScreen = ({ navigation }) => {
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Item
|
||||
title="Appearance"
|
||||
icon="palette-outline"
|
||||
onPress={() =>
|
||||
navigation.navigate('SettingsStack', {
|
||||
screen: 'AppearanceSettings',
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Item
|
||||
title="Advanced"
|
||||
icon="code-tags"
|
||||
|
||||
@@ -15,3 +15,9 @@ export interface LibrarySettings {
|
||||
incognitoMode?: boolean;
|
||||
downloadedOnlyMode?: boolean;
|
||||
}
|
||||
|
||||
export interface CategorySettings {
|
||||
defaultCategoryId?: number;
|
||||
includedCategoriesInUpdate?: number[];
|
||||
excludedCategoriesInUpdate?: number[];
|
||||
}
|
||||
|
||||
@@ -156,14 +156,15 @@
|
||||
"addCategories": "Add category",
|
||||
"editCategories": "Rename category",
|
||||
"setCategories": "Set categories",
|
||||
"header": "Edit Categories",
|
||||
"header": "Edit categories",
|
||||
"emptyMsg": "You have no categories. Tap the plus button to create one for organizing your library",
|
||||
"setModalEmptyMsg": "You have no categories. Tap the Edit button to create one for organizing your library",
|
||||
"deleteModal": {
|
||||
"header": "Delete category",
|
||||
"desc": "Do you wish to delete category"
|
||||
},
|
||||
"duplicateError": "A category with this name already exists!"
|
||||
"duplicateError": "A category with this name already exists!",
|
||||
"defaultCategory": "Default category"
|
||||
},
|
||||
"settings": {
|
||||
"icognitoMode": "Incognito mode",
|
||||
|
||||
@@ -120,6 +120,7 @@ export interface StringMap {
|
||||
'categories.deleteModal.header': 'string';
|
||||
'categories.deleteModal.desc': 'string';
|
||||
'categories.duplicateError': 'string';
|
||||
'categories.defaultCategory': 'string';
|
||||
'settings.icognitoMode': 'string';
|
||||
'settings.downloadedOnly': 'string';
|
||||
}
|
||||
|
||||
+3
-1
@@ -18,10 +18,12 @@
|
||||
"@components/*": ["src/components/*"],
|
||||
"@database/*": ["src/database/*"],
|
||||
"@hooks/*": ["src/hooks/*"],
|
||||
"@hooks": ["src/hooks/index"],
|
||||
"@screens/*": ["src/screens/*"],
|
||||
"@strings/*": ["strings/*"],
|
||||
"@redux/*": ["src/redux/*"],
|
||||
"@theme/*": ["src/theme/*"]
|
||||
"@theme/*": ["src/theme/*"],
|
||||
"@utils/*": ["src/utils/*"]
|
||||
}
|
||||
},
|
||||
"exclude": [
|
||||
|
||||
Reference in New Issue
Block a user