Fix: revert to app storage

This commit is contained in:
nyagami
2024-06-08 01:42:47 +07:00
parent 30265471b5
commit b7bbbbe962
16 changed files with 48 additions and 274 deletions
+4
View File
@@ -5,6 +5,10 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
<!-- Storage -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!-- For background jobs -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
@@ -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)
+1 -3
View File
@@ -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<void> => {
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,
+1 -3
View File
@@ -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))) {
+1 -2
View File
@@ -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: '<PREFIX>_<novel.pluginId>_<novel.path>',
@@ -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);
+8 -8
View File
@@ -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<RootStackParamList>();
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 <OnboardingScreen />;
}
return (
<NavigationContainer theme={{ colors: theme, dark: theme.isDark }}>
{isNewVersion && <NewUpdateDialog newVersion={latestRelease} />}
+6 -10
View File
@@ -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<string, any> = {
'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]) => {
+10 -42
View File
@@ -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>(OnboardingStep.PICK_THEME);
const [step] = useState<OnboardingStep>(OnboardingStep.PICK_THEME);
const renderStep = () => {
switch (step) {
case OnboardingStep.PICK_THEME:
return <PickThemeStep />;
case OnboardingStep.STORAGE_LOCATION:
return (
<StorageStep
rootStorage={rootStorage}
onPathChange={setRootStorage}
/>
);
default:
return <PickThemeStep />;
}
@@ -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 <PickThemeStep />;
}
};
useBackHandler(() => {
if (step === OnboardingStep.STORAGE_LOCATION) {
setStep(OnboardingStep.PICK_THEME);
return true;
}
return false;
});
return (
<SafeAreaView style={[styles.root, { backgroundColor: theme.background }]}>
<Image
@@ -91,25 +70,14 @@ export default function OnboardingScreen() {
</View>
<View style={{ flex: 1 }} />
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
{step !== OnboardingStep.STORAGE_LOCATION ? (
<Button
style={{ flex: 1 }}
title="Next"
mode="contained"
onPress={() => setStep(step + 1)}
/>
) : null}
{step === OnboardingStep.STORAGE_LOCATION ? (
<Button
style={{ flex: 1 }}
title="Complete"
mode="contained"
onPress={() => {
setAppStorage(rootStorage);
}}
disabled={!rootStorage}
/>
) : null}
<Button
style={{ flex: 1 }}
title="Complete"
mode="contained"
onPress={() => {
MMKVStorage.set('IS_ONBOARDED', true);
}}
/>
</View>
</SafeAreaView>
);
-146
View File
@@ -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<boolean | undefined>(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 (
<View style={{ paddingHorizontal: 16 }}>
<View style={styles.section}>
<View>
<Text style={[styles.title, { color: theme.onSurface }]}>
App permissions
</Text>
<Text style={[{ color: theme.onSurfaceVariant }]}>
Read and write external storage.
</Text>
</View>
<Button
mode="outlined"
compact
style={styles.grantButton}
loading={granted === undefined}
onPress={() => {
if (granted === false) {
grantPermissions();
}
}}
>
{granted === true ? (
<MaterialCommunityIcons name="check" size={24} />
) : (
<Text style={{ color: theme.primary, fontWeight: '600' }}>
{granted === undefined ? 'Checking' : 'Allow'}
</Text>
)}
</Button>
</View>
<View style={styles.section}>
<View>
<Text style={[styles.title, { color: theme.onSurface }]}>
Select an empty folder.
</Text>
{rootStorage ? (
<Text
style={{
color: theme.onSurfaceVariant,
fontSize: 12,
fontWeight: '500',
}}
>
Selected folder: {rootStorage}
</Text>
) : null}
</View>
</View>
<Button
labelStyle={{
flex: 1,
}}
title="Select a folder"
mode="contained"
onPress={pickFolder}
disabled={!granted}
/>
</View>
);
}
const styles = StyleSheet.create({
section: {
flexDirection: 'row',
marginBottom: 16,
justifyContent: 'space-between',
},
title: {
fontSize: 16,
fontWeight: '600',
},
grantButton: {
alignSelf: 'center',
paddingHorizontal: 8,
},
});
+1 -2
View File
@@ -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<DrawerLayoutAndroid>(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);
+1 -3
View File
@@ -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'));
+1 -3
View File
@@ -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'));
+5 -7
View File
@@ -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);
});
+1 -4
View File
@@ -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<number> => {
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<string[]> => {
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'));
}
+1 -2
View File
@@ -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;
+3 -37
View File
@@ -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';