diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index f11d3f8b..04080be3 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -5,6 +5,10 @@
+
+
+
+
diff --git a/android/app/src/main/java/com/rajarsheechatterjee/FileManager/FileManager.kt b/android/app/src/main/java/com/rajarsheechatterjee/FileManager/FileManager.kt
index e03da03c..b24c32e7 100644
--- a/android/app/src/main/java/com/rajarsheechatterjee/FileManager/FileManager.kt
+++ b/android/app/src/main/java/com/rajarsheechatterjee/FileManager/FileManager.kt
@@ -179,8 +179,10 @@ class FileManager(context: ReactApplicationContext) :
fun mkdir(filepath: String, promise: Promise) {
try {
val file = File(filepath)
- val created = file.mkdirs()
- if (!created) throw Exception("Directory could not be created")
+ if(!file.exists()){
+ val created = file.mkdirs()
+ if (!created) throw Exception("Directory could not be created")
+ }
promise.resolve(null)
} catch (e: Exception) {
promise.reject(e)
diff --git a/src/database/queries/ChapterQueries.ts b/src/database/queries/ChapterQueries.ts
index 88bbdd46..2c701755 100644
--- a/src/database/queries/ChapterQueries.ts
+++ b/src/database/queries/ChapterQueries.ts
@@ -11,7 +11,7 @@ import { Update } from '../types';
import { noop } from 'lodash-es';
import { getString } from '@strings/translations';
import FileManager from '@native/FileManager';
-import { getAppStorages } from '@utils/Storages';
+import { NOVEL_STORAGE } from '@utils/Storages';
const db = SQLite.openDatabase('lnreader.db');
const insertChapterQuery = `
@@ -247,7 +247,6 @@ const downloadFiles = async (
chapterId: number,
): Promise => {
try {
- const { NOVEL_STORAGE } = getAppStorages();
const folder = await createChapterFolder(NOVEL_STORAGE, {
pluginId: plugin.id,
novelId,
@@ -307,7 +306,6 @@ const deleteDownloadedFiles = async (
chapterId: number,
) => {
try {
- const { NOVEL_STORAGE } = getAppStorages();
const path = await createChapterFolder(NOVEL_STORAGE, {
pluginId,
novelId,
diff --git a/src/database/queries/NovelQueries.ts b/src/database/queries/NovelQueries.ts
index 5dd283cb..00dbda8c 100644
--- a/src/database/queries/NovelQueries.ts
+++ b/src/database/queries/NovelQueries.ts
@@ -12,7 +12,7 @@ import { noop } from 'lodash-es';
import { getString } from '@strings/translations';
import { BackupNovel, NovelInfo } from '../types';
import { SourceNovel } from '@plugins/types';
-import { getAppStorages } from '@utils/Storages';
+import { NOVEL_STORAGE } from '@utils/Storages';
import FileManager from '@native/FileManager';
export const insertNovelAndChapters = async (
@@ -45,7 +45,6 @@ export const insertNovelAndChapters = async (
if (novelId) {
const promises = [insertChapters(novelId, sourceNovel.chapters)];
if (sourceNovel.cover) {
- const { NOVEL_STORAGE } = getAppStorages();
const novelDir = NOVEL_STORAGE + '/' + pluginId + '/' + novelId;
await FileManager.mkdir(novelDir);
const novelCoverUri = 'file://' + novelDir + '/cover.png';
@@ -271,7 +270,6 @@ export const updateNovelInfo = async (info: NovelInfo) => {
export const pickCustomNovelCover = async (novel: NovelInfo) => {
const image = await DocumentPicker.getDocumentAsync({ type: 'image/*' });
if (image.assets && image.assets[0]) {
- const { NOVEL_STORAGE } = getAppStorages();
const novelDir = NOVEL_STORAGE + '/' + novel.pluginId + '/' + novel.id;
let novelCoverUri = 'file://' + novelDir + '/cover.png';
if (!(await FileManager.exists(novelDir))) {
diff --git a/src/hooks/persisted/useNovel.ts b/src/hooks/persisted/useNovel.ts
index 324fbac9..dbb13b35 100644
--- a/src/hooks/persisted/useNovel.ts
+++ b/src/hooks/persisted/useNovel.ts
@@ -28,7 +28,7 @@ import { useCallback, useEffect, useState } from 'react';
import { getString } from '@strings/translations';
import dayjs from 'dayjs';
import { parseChapterNumber } from '@utils/parseChapterNumber';
-import { getAppStorages } from '@utils/Storages';
+import { NOVEL_STORAGE } from '@utils/Storages';
import FileManager from '@native/FileManager';
// store key: '__',
@@ -417,7 +417,6 @@ export const deleteCachedNovels = async () => {
`${NOVEL_SETTINSG_PREFIX}_${novel.pluginId}_${novel.path}`,
);
MMKVStorage.delete(`${LAST_READ_PREFIX}_${novel.pluginId}_${novel.path}`);
- const { NOVEL_STORAGE } = getAppStorages();
const novelDir = NOVEL_STORAGE + '/' + novel.pluginId + '/' + novel.id;
if (await FileManager.exists(novelDir)) {
await FileManager.unlink(novelDir);
diff --git a/src/navigators/Main.tsx b/src/navigators/Main.tsx
index 7d58f936..eb1a3952 100644
--- a/src/navigators/Main.tsx
+++ b/src/navigators/Main.tsx
@@ -35,17 +35,15 @@ import { updateLibrary } from '@services/updates';
import WebviewScreen from '@screens/WebviewScreen/WebviewScreen';
import { RootStackParamList } from './types';
import Color from 'color';
-import OnboardingScreen from '../screens/onboarding/OnboardingScreen';
-import { APP_STORAGE_KEY } from '@utils/Storages';
-import { useMMKVString } from 'react-native-mmkv';
-
+import { useMMKVBoolean } from 'react-native-mmkv';
+import OnboardingScreen from '@screens/onboarding/OnboardingScreen';
const Stack = createStackNavigator();
const MainNavigator = () => {
const theme = useTheme();
- const [ROOT_STORAGE] = useMMKVString(APP_STORAGE_KEY);
const { updateLibraryOnLaunch } = useAppSettings();
const { refreshPlugins } = usePlugins();
+ const [isOnboarded] = useMMKVBoolean('IS_ONBOARDED');
useEffect(() => {
const timer = setTimeout(async () => {
@@ -62,16 +60,18 @@ const MainNavigator = () => {
if (updateLibraryOnLaunch) {
updateLibrary();
}
- if (ROOT_STORAGE) {
+ if (isOnboarded) {
// hack this helps app has enough time to initialize database;
refreshPlugins();
}
- }, [ROOT_STORAGE]);
+ }, [isOnboarded]);
const { isNewVersion, latestRelease } = useGithubUpdateChecker();
- if (!ROOT_STORAGE) {
+
+ if (!isOnboarded) {
return ;
}
+
return (
{isNewVersion && }
diff --git a/src/plugins/pluginManager.ts b/src/plugins/pluginManager.ts
index bfdee4dc..a51fd68c 100644
--- a/src/plugins/pluginManager.ts
+++ b/src/plugins/pluginManager.ts
@@ -17,12 +17,9 @@ import { Parser } from 'htmlparser2';
import FileManager from '@native/FileManager';
import { getRepositoriesFromDb } from '@database/queries/RepositoryQueries';
import { showToast } from '@utils/showToast';
-import { getAppStorages } from '@utils/Storages';
+import { PLUGIN_STORAGE } from '@utils/Storages';
-const _getPluginsFilePath = () => {
- const { PLUGIN_STORAGE } = getAppStorages();
- return PLUGIN_STORAGE + '/plugins.json';
-};
+const pluginFilePath = PLUGIN_STORAGE + '/plugins.json';
const packages: Record = {
'htmlparser2': { Parser },
@@ -72,8 +69,8 @@ const serializePlugin = async (
installed: boolean,
) => {
if (!serializedPlugins) {
- if (await FileManager.exists(_getPluginsFilePath())) {
- const content = await FileManager.readFile(_getPluginsFilePath());
+ if (await FileManager.exists(pluginFilePath)) {
+ const content = await FileManager.readFile(pluginFilePath);
serializedPlugins = JSON.parse(content);
} else {
serializedPlugins = {};
@@ -84,18 +81,17 @@ const serializePlugin = async (
} else {
serializedPlugins[pluginId] = undefined;
}
- const { PLUGIN_STORAGE } = getAppStorages();
if (!(await FileManager.exists(PLUGIN_STORAGE))) {
await FileManager.mkdir(PLUGIN_STORAGE);
}
await FileManager.writeFile(
- _getPluginsFilePath(),
+ pluginFilePath,
JSON.stringify(serializedPlugins),
);
};
const deserializePlugins = () => {
- return FileManager.readFile(_getPluginsFilePath())
+ return FileManager.readFile(pluginFilePath)
.then(content => {
serializedPlugins = JSON.parse(content);
Object.entries(serializedPlugins).forEach(([pluginId, script]) => {
diff --git a/src/screens/onboarding/OnboardingScreen.tsx b/src/screens/onboarding/OnboardingScreen.tsx
index f0a3f80c..96c95761 100644
--- a/src/screens/onboarding/OnboardingScreen.tsx
+++ b/src/screens/onboarding/OnboardingScreen.tsx
@@ -5,30 +5,19 @@ import { Image, StyleSheet, View } from 'react-native';
import { Button } from '@components';
import PickThemeStep from './PickThemeStep';
import { useState } from 'react';
-import StorageStep from './StorageStep';
-import { setAppStorage } from '@utils/Storages';
-import { useBackHandler } from '@hooks/index';
+import { MMKVStorage } from '@utils/mmkv/mmkv';
enum OnboardingStep {
PICK_THEME,
- STORAGE_LOCATION,
}
export default function OnboardingScreen() {
const theme = useTheme();
- const [rootStorage, setRootStorage] = useState('');
- const [step, setStep] = useState(OnboardingStep.PICK_THEME);
+ const [step] = useState(OnboardingStep.PICK_THEME);
const renderStep = () => {
switch (step) {
case OnboardingStep.PICK_THEME:
return ;
- case OnboardingStep.STORAGE_LOCATION:
- return (
-
- );
default:
return ;
}
@@ -37,21 +26,11 @@ export default function OnboardingScreen() {
switch (step) {
case OnboardingStep.PICK_THEME:
return 'Pick a theme';
- case OnboardingStep.STORAGE_LOCATION:
- return 'Select storage for LNReader';
default:
return ;
}
};
- useBackHandler(() => {
- if (step === OnboardingStep.STORAGE_LOCATION) {
- setStep(OnboardingStep.PICK_THEME);
- return true;
- }
- return false;
- });
-
return (
- {step !== OnboardingStep.STORAGE_LOCATION ? (
-
);
diff --git a/src/screens/onboarding/StorageStep.tsx b/src/screens/onboarding/StorageStep.tsx
deleted file mode 100644
index 15d9a469..00000000
--- a/src/screens/onboarding/StorageStep.tsx
+++ /dev/null
@@ -1,146 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import { useTheme } from '@hooks/persisted';
-import { PermissionsAndroid, StyleSheet, View } from 'react-native';
-import { Button } from '@components';
-import FileManager from '@native/FileManager';
-import { showToast } from '@utils/showToast';
-import { Text } from 'react-native-paper';
-import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
-
-interface StorageStepProps {
- rootStorage: string;
- onPathChange: (path: string) => void;
-}
-
-export default function StorageStep({
- rootStorage,
- onPathChange,
-}: StorageStepProps) {
- const [granted, setGranted] = useState(undefined);
- const theme = useTheme();
- const pickFolder = () => {
- FileManager.pickFolder().then(path => {
- if (path) {
- FileManager.readDir(path).then(res => {
- if (res.length > 0) {
- showToast('Please select an empty folder');
- } else {
- onPathChange(path);
- }
- });
- }
- });
- };
- const grantPermissions = () => {
- PermissionsAndroid.requestMultiple([
- PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE,
- PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE,
- ]).then(value => {
- if (
- value['android.permission.READ_EXTERNAL_STORAGE'] === 'granted' &&
- value['android.permission.WRITE_EXTERNAL_STORAGE'] === 'granted'
- ) {
- setGranted(true);
- } else {
- setGranted(false);
- }
- });
- };
- useEffect(() => {
- Promise.all([
- PermissionsAndroid.check(
- PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE,
- ),
- PermissionsAndroid.check(
- PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE,
- ),
- ])
- .then(([readGranted, writeGRanted]) => {
- if (readGranted && writeGRanted) {
- setGranted(true);
- } else {
- setGranted(false);
- }
- })
- .catch(() => {
- setGranted(false);
- });
- }, []);
- return (
-
-
-
-
- App permissions
-
-
- Read and write external storage.
-
-
- {
- if (granted === false) {
- grantPermissions();
- }
- }}
- >
- {granted === true ? (
-
- ) : (
-
- {granted === undefined ? 'Checking' : 'Allow'}
-
- )}
-
-
-
-
-
- Select an empty folder.
-
- {rootStorage ? (
-
- Selected folder: {rootStorage}
-
- ) : null}
-
-
-
-
-
- );
-}
-
-const styles = StyleSheet.create({
- section: {
- flexDirection: 'row',
- marginBottom: 16,
- justifyContent: 'space-between',
- },
- title: {
- fontSize: 16,
- fontWeight: '600',
- },
- grantButton: {
- alignSelf: 'center',
- paddingHorizontal: 8,
- },
-});
diff --git a/src/screens/reader/ReaderScreen.tsx b/src/screens/reader/ReaderScreen.tsx
index 70956f62..812f4eb0 100644
--- a/src/screens/reader/ReaderScreen.tsx
+++ b/src/screens/reader/ReaderScreen.tsx
@@ -51,7 +51,7 @@ import { ChapterInfo } from '@database/types';
import WebView, { WebViewNavigation } from 'react-native-webview';
import { getString } from '@strings/translations';
import FileManager from '@native/FileManager';
-import { getAppStorages } from '@utils/Storages';
+import { NOVEL_STORAGE } from '@utils/Storages';
const Chapter = ({ route, navigation }: ChapterScreenProps) => {
const drawerRef = useRef(null);
@@ -168,7 +168,6 @@ export const ChapterContent = ({
const getChapter = async () => {
try {
- const { NOVEL_STORAGE } = getAppStorages();
const filePath = `${NOVEL_STORAGE}/${novel.pluginId}/${chapter.novelId}/${chapter.id}/index.html`;
if (await FileManager.exists(filePath)) {
sourceChapter.chapterText = await FileManager.readFile(filePath);
diff --git a/src/services/backup/drive/index.ts b/src/services/backup/drive/index.ts
index 8db87ce7..995159d5 100644
--- a/src/services/backup/drive/index.ts
+++ b/src/services/backup/drive/index.ts
@@ -9,7 +9,7 @@ import { getString } from '@strings/translations';
import { CACHE_DIR_PATH, prepareBackupData, restoreData } from '../utils';
import { download, updateMetadata, uploadMedia } from '@api/drive/request';
import { ZipBackupName } from '../types';
-import { getAppStorages } from '@utils/Storages';
+import { ROOT_STORAGE } from '@utils/Storages';
interface TaskData {
delay: number;
@@ -18,7 +18,6 @@ interface TaskData {
const driveBackupAction = async (taskData?: TaskData) => {
try {
- const { ROOT_STORAGE } = getAppStorages();
MMKVStorage.set(BACKGROUND_ACTION, BackgoundAction.BACKUP);
if (!taskData) {
@@ -127,7 +126,6 @@ export const createBackup = async (backupFolder: DriveFile) => {
const driveRestoreAction = async (taskData?: TaskData) => {
try {
- const { ROOT_STORAGE } = getAppStorages();
MMKVStorage.set(BACKGROUND_ACTION, BackgoundAction.RESTORE);
if (!taskData) {
throw new Error(getString('backupScreen.noDataProvided'));
diff --git a/src/services/backup/remote/index.ts b/src/services/backup/remote/index.ts
index 863debe4..3b1ffc42 100644
--- a/src/services/backup/remote/index.ts
+++ b/src/services/backup/remote/index.ts
@@ -7,7 +7,7 @@ import { BACKGROUND_ACTION, BackgoundAction } from '@services/constants';
import { getString } from '@strings/translations';
import { CACHE_DIR_PATH, prepareBackupData, restoreData } from '../utils';
import { ZipBackupName } from '../types';
-import { getAppStorages } from '@utils/Storages';
+import { ROOT_STORAGE } from '@utils/Storages';
interface TaskData {
delay: number;
@@ -17,7 +17,6 @@ interface TaskData {
const remoteBackupAction = async (taskData?: TaskData) => {
try {
- const { ROOT_STORAGE } = getAppStorages();
MMKVStorage.set(BACKGROUND_ACTION, BackgoundAction.BACKUP);
if (!taskData) {
throw new Error(getString('backupScreen.noDataProvided'));
@@ -111,7 +110,6 @@ export const createBackup = async (host: string, backupFolder: string) => {
const remoteRestoreAction = async (taskData?: TaskData) => {
try {
- const { ROOT_STORAGE } = getAppStorages();
MMKVStorage.set(BACKGROUND_ACTION, BackgoundAction.RESTORE);
if (!taskData) {
throw new Error(getString('backupScreen.noDataProvided'));
diff --git a/src/services/backup/utils.ts b/src/services/backup/utils.ts
index 74f898f5..03484c52 100644
--- a/src/services/backup/utils.ts
+++ b/src/services/backup/utils.ts
@@ -17,12 +17,10 @@ import {
import { BackupCategory } from '@database/types';
import { BackupEntryName } from './types';
import FileManager from '@native/FileManager';
-import { getAppStorages } from '@utils/Storages';
+import { ROOT_STORAGE } from '@utils/Storages';
+
+const APP_STORAGE_URI = 'file://' + ROOT_STORAGE;
-const _getAppStorageUriPrefix = () => {
- const { ROOT_STORAGE } = getAppStorages();
- return 'file://' + ROOT_STORAGE;
-};
export const CACHE_DIR_PATH =
FileManager.ExternalCachesDirectoryPath + '/BackupData';
@@ -79,7 +77,7 @@ export const prepareBackupData = async (cacheDirPath: string) => {
JSON.stringify({
chapters: chapters,
...novel,
- cover: novel.cover?.replace(_getAppStorageUriPrefix(), ''),
+ cover: novel.cover?.replace(APP_STORAGE_URI, ''),
}),
);
}
@@ -124,7 +122,7 @@ export const restoreData = async (cacheDirPath: string) => {
await FileManager.readFile(item.path).then(content => {
const backupNovel = JSON.parse(content);
if (!backupNovel.cover?.startsWith('http')) {
- backupNovel.cover = _getAppStorageUriPrefix() + backupNovel.cover;
+ backupNovel.cover = APP_STORAGE_URI + backupNovel.cover;
}
return _restoreNovelAndChapters(backupNovel);
});
diff --git a/src/services/epub/import.ts b/src/services/epub/import.ts
index 8fd07edf..b36ae045 100644
--- a/src/services/epub/import.ts
+++ b/src/services/epub/import.ts
@@ -17,7 +17,7 @@ import { getString } from '@strings/translations';
import { showToast } from '@utils/showToast';
import FileManager from '@native/FileManager';
import EpubUtil from '@native/EpubUtil';
-import { getAppStorages } from '@utils/Storages';
+import { NOVEL_STORAGE } from '@utils/Storages';
interface TaskData {
delay: number;
@@ -33,7 +33,6 @@ const insertLocalNovel = (
artist?: string,
summary?: string,
): Promise => {
- const { NOVEL_STORAGE } = getAppStorages();
return new Promise((resolve, reject) => {
db.transaction(tx => {
tx.executeSql(
@@ -88,7 +87,6 @@ const insertLocalChapter = (
path: string,
releaseTime: string,
): Promise => {
- const { NOVEL_STORAGE } = getAppStorages();
return new Promise((resolve, reject) => {
db.transaction(tx => {
tx.executeSql(
@@ -153,7 +151,6 @@ const insertLocalChapter = (
const importEpubAction = async (taskData?: TaskData) => {
try {
- const { NOVEL_STORAGE } = getAppStorages();
if (!taskData) {
throw new Error(getString('backupScreen.noDataProvided'));
}
diff --git a/src/services/updates/LibraryUpdateQueries.ts b/src/services/updates/LibraryUpdateQueries.ts
index 44c678fd..e4a3d138 100644
--- a/src/services/updates/LibraryUpdateQueries.ts
+++ b/src/services/updates/LibraryUpdateQueries.ts
@@ -4,8 +4,8 @@ import { downloadChapter } from '../../database/queries/ChapterQueries';
import * as SQLite from 'expo-sqlite';
import { ChapterItem, SourceNovel } from '@plugins/types';
import { LOCAL_PLUGIN_ID } from '@plugins/pluginManager';
-import { getAppStorages } from '@utils/Storages';
import FileManager from '@native/FileManager';
+import { NOVEL_STORAGE } from '@utils/Storages';
const db = SQLite.openDatabase('lnreader.db');
const updateNovelMetadata = (
@@ -13,7 +13,6 @@ const updateNovelMetadata = (
novelId: number,
novel: SourceNovel,
) => {
- const { NOVEL_STORAGE } = getAppStorages();
return new Promise(async (resolve, reject) => {
let { name, cover, summary, author, artist, genres, status, totalPages } =
novel;
diff --git a/src/utils/Storages.ts b/src/utils/Storages.ts
index d0431e8e..b2b34b4d 100644
--- a/src/utils/Storages.ts
+++ b/src/utils/Storages.ts
@@ -1,39 +1,5 @@
import FileManager from '@native/FileManager';
-import { MMKVStorage } from '@utils/mmkv/mmkv';
-export const APP_STORAGE_KEY = 'APP_STORAGE_KEY';
-
-const NOVEL_STORAGE_PATH = 'Novels';
-const PLUGIN_STORAGE_PATH = 'Plugins';
-const DEFAULT_ROOT_STORAGE = FileManager.ExternalDirectoryPath;
-interface AppStorage {
- ROOT_STORAGE: string;
- NOVEL_STORAGE: string;
- PLUGIN_STORAGE: string;
-}
-
-/**
- * Call this funtion in a context instead of global.
- * Because if rootStorage is changed, app would not be updated if the function is called globally
- */
-export const getAppStorages = (): AppStorage => {
- const rootStorage =
- MMKVStorage.getString(APP_STORAGE_KEY) || DEFAULT_ROOT_STORAGE;
- return {
- ROOT_STORAGE: rootStorage,
- NOVEL_STORAGE: rootStorage + '/' + NOVEL_STORAGE_PATH,
- PLUGIN_STORAGE: rootStorage + '/' + PLUGIN_STORAGE_PATH,
- };
-};
-
-export const setAppStorage = async (rootStorage: string) => {
- if (!(await FileManager.exists(rootStorage))) {
- throw new Error('Folder does not exist!');
- }
- const novelStorage = rootStorage + '/' + NOVEL_STORAGE_PATH;
- const pluginStorage = rootStorage + '/' + PLUGIN_STORAGE_PATH;
- for (const folder of [novelStorage, pluginStorage]) {
- await FileManager.mkdir(folder);
- }
- MMKVStorage.set(APP_STORAGE_KEY, rootStorage);
-};
+export const ROOT_STORAGE = FileManager.ExternalDirectoryPath;
+export const PLUGIN_STORAGE = ROOT_STORAGE + '/Plugins';
+export const NOVEL_STORAGE = ROOT_STORAGE + '/Novels';