diff --git a/.eslintignore b/.eslintignore
index 15a33742062..56f772040b4 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -531,9 +531,12 @@ packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/ExportDebu
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/ExportProfileButton.js
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteExportButton.test.js
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteExportButton.js
+packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteImportButton.js
+packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/TaskButton.js
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/exportAllFolders.js
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/exportDebugReport.js
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/exportProfile.js
+packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/makeImportExportCacheDirectory.js
packages/app-mobile/components/screens/ConfigScreen/SectionHeader.js
packages/app-mobile/components/screens/ConfigScreen/SectionSelector.js
packages/app-mobile/components/screens/ConfigScreen/SettingComponent.js
diff --git a/.gitignore b/.gitignore
index 7ecec5d2e9b..0b72bf9afe7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -509,9 +509,12 @@ packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/ExportDebu
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/ExportProfileButton.js
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteExportButton.test.js
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteExportButton.js
+packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteImportButton.js
+packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/TaskButton.js
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/exportAllFolders.js
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/exportDebugReport.js
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/exportProfile.js
+packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/makeImportExportCacheDirectory.js
packages/app-mobile/components/screens/ConfigScreen/SectionHeader.js
packages/app-mobile/components/screens/ConfigScreen/SectionSelector.js
packages/app-mobile/components/screens/ConfigScreen/SettingComponent.js
diff --git a/packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.tsx b/packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.tsx
index ea93d0b40d0..51abd881abb 100644
--- a/packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.tsx
+++ b/packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.tsx
@@ -18,7 +18,7 @@ import * as shared from '@joplin/lib/components/shared/config/config-shared';
import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry';
import biometricAuthenticate from '../../biometrics/biometricAuthenticate';
import configScreenStyles, { ConfigScreenStyles } from './configScreenStyles';
-import NoteExportButton, { exportButtonDescription, exportButtonTitle } from './NoteExportSection/NoteExportButton';
+import NoteExportButton, { exportButtonDescription, exportButtonDefaultTitle } from './NoteExportSection/NoteExportButton';
import SettingsButton from './SettingsButton';
import Clipboard from '@react-native-community/clipboard';
import { ReactElement, ReactNode } from 'react';
@@ -33,6 +33,7 @@ import PluginService, { PluginSettings } from '@joplin/lib/services/plugins/Plug
import PluginStates, { getSearchText as getPluginStatesSearchText } from './plugins/PluginStates';
import PluginUploadButton, { buttonLabel as pluginUploadButtonSearchText } from './plugins/PluginUploadButton';
import isInstallingPluginsAllowed from './plugins/utils/isPluginInstallingAllowed';
+import NoteImportButton, { importButtonDefaultTitle, importButtonDescription } from './NoteExportSection/NoteImportButton';
interface ConfigScreenState {
settings: any;
@@ -514,7 +515,11 @@ class ConfigScreenComponent extends BaseScreenComponent,
- [exportButtonTitle(), exportButtonDescription()],
+ [exportButtonDefaultTitle(), exportButtonDescription()],
+ );
+ addSettingComponent(
+ ,
+ [importButtonDefaultTitle(), importButtonDescription()],
);
addSettingComponent(
,
diff --git a/packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteExportButton.tsx b/packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteExportButton.tsx
index 29b99115833..54bea77dc8b 100644
--- a/packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteExportButton.tsx
+++ b/packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteExportButton.tsx
@@ -1,16 +1,15 @@
import * as React from 'react';
-import { Text, Alert, View } from 'react-native';
import { _ } from '@joplin/lib/locale';
import Logger from '@joplin/utils/Logger';
-import { ProgressBar } from 'react-native-paper';
-import { FunctionComponent, useCallback, useState } from 'react';
+import { FunctionComponent } from 'react';
import shim from '@joplin/lib/shim';
import { join } from 'path';
import Share from 'react-native-share';
-import exportAllFolders, { makeExportCacheDirectory } from './utils/exportAllFolders';
+import exportAllFolders from './utils/exportAllFolders';
import { ExportProgressState } from '@joplin/lib/services/interop/types';
import { ConfigScreenStyles } from '../configScreenStyles';
-import SettingsButton from '../SettingsButton';
+import makeImportExportCacheDirectory from './utils/makeImportExportCacheDirectory';
+import TaskButton, { OnProgressCallback, SetAfterCompleteListenerCallback, TaskStatus } from './TaskButton';
const logger = Logger.create('NoteExportButton');
@@ -18,99 +17,68 @@ interface Props {
styles: ConfigScreenStyles;
}
-enum ExportStatus {
- NotStarted,
- Exporting,
- Exported,
-}
-
-export const exportButtonTitle = () => _('Export all notes as JEX');
+export const exportButtonDefaultTitle = () => _('Export all notes as JEX');
export const exportButtonDescription = () => _('Share a copy of all notes in a file format that can be imported by Joplin on a computer.');
-const NoteExportButton: FunctionComponent = props => {
- const [exportStatus, setExportStatus] = useState(ExportStatus.NotStarted);
- const [exportProgress, setExportProgress] = useState(0);
- const [warnings, setWarnings] = useState('');
-
- const startExport = useCallback(async () => {
- // Don't run multiple exports at the same time.
- if (exportStatus === ExportStatus.Exporting) {
- return;
- }
-
- setExportStatus(ExportStatus.Exporting);
- const exportTargetPath = join(await makeExportCacheDirectory(), 'jex-export.jex');
- logger.info(`Exporting all folders to path ${exportTargetPath}`);
-
- try {
- // Initially, undetermined progress
- setExportProgress(undefined);
-
- const status = await exportAllFolders(exportTargetPath, (status, progress) => {
- if (progress !== null) {
- setExportProgress(progress);
- } else if (status === ExportProgressState.Closing || status === ExportProgressState.QueuingItems) {
- // We don't have a numeric progress value and the closing/queuing state may take a while.
- // Set a special progress value:
- setExportProgress(undefined);
- }
- });
+const getTitle = (taskStatus: TaskStatus) => {
+ if (taskStatus === TaskStatus.NotStarted) {
+ return exportButtonDefaultTitle();
+ } else if (taskStatus === TaskStatus.InProgress) {
+ return _('Exporting...');
+ } else {
+ return _('Exported successfully!');
+ }
+};
- setExportStatus(ExportStatus.Exported);
- setWarnings(status.warnings.join('\n'));
+const runExportTask = async (
+ onProgress: OnProgressCallback,
+ setAfterCompleteListener: SetAfterCompleteListenerCallback,
+) => {
+ const exportTargetPath = join(await makeImportExportCacheDirectory(), 'jex-export.jex');
+ logger.info(`Exporting all folders to path ${exportTargetPath}`);
+ setAfterCompleteListener(async (success: boolean) => {
+ if (success) {
await Share.open({
type: 'application/jex',
filename: 'export.jex',
url: `file://${exportTargetPath}`,
failOnCancel: false,
});
- } catch (e) {
- logger.error('Unable to export:', e);
-
- // Display a message to the user (e.g. in the case where the user is out of disk space).
- Alert.alert(_('Error'), _('Unable to export or share data. Reason: %s', e.toString()));
- setExportStatus(ExportStatus.NotStarted);
- } finally {
- await shim.fsDriver().remove(exportTargetPath);
}
- }, [exportStatus]);
+ await shim.fsDriver().remove(exportTargetPath);
+ });
+
+ // Initially, undetermined progress
+ onProgress(undefined);
+
+ const status = await exportAllFolders(exportTargetPath, (status, progress) => {
+ if (progress !== null) {
+ onProgress(progress);
+ } else if (status === ExportProgressState.Closing || status === ExportProgressState.QueuingItems) {
+ // We don't have a numeric progress value and the closing/queuing state may take a while.
+ // Set a special progress value:
+ onProgress(undefined);
+ }
+ });
- if (exportStatus === ExportStatus.NotStarted || exportStatus === ExportStatus.Exporting) {
- const progressComponent = (
-
- );
+ onProgress(1);
- const startOrCancelExportButton = (
-
- );
+ logger.info('Export complete');
- return startOrCancelExportButton;
- } else {
- const warningComponent = (
-
- {_('Warnings:\n%s', warnings)}
-
- );
+ return { warnings: status.warnings, success: true };
+};
- const exportSummary = (
-
- {_('Exported successfully!')}
- {warnings.length > 0 ? warningComponent : null}
-
- );
- return exportSummary;
- }
+const NoteExportButton: FunctionComponent = props => {
+ return (
+
+ );
};
export default NoteExportButton;
diff --git a/packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteImportButton.tsx b/packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteImportButton.tsx
new file mode 100644
index 00000000000..f23b3c8763c
--- /dev/null
+++ b/packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteImportButton.tsx
@@ -0,0 +1,73 @@
+import * as React from 'react';
+import { _ } from '@joplin/lib/locale';
+import Logger from '@joplin/utils/Logger';
+import { FunctionComponent } from 'react';
+import { join } from 'path';
+import { ConfigScreenStyles } from '../configScreenStyles';
+import InteropService from '@joplin/lib/services/interop/InteropService';
+import pickDocument from '../../../../utils/pickDocument';
+import makeImportExportCacheDirectory from './utils/makeImportExportCacheDirectory';
+import shim from '@joplin/lib/shim';
+import TaskButton, { OnProgressCallback, SetAfterCompleteListenerCallback, TaskStatus } from './TaskButton';
+
+const logger = Logger.create('NoteImportButton');
+
+interface Props {
+ styles: ConfigScreenStyles;
+}
+
+export const importButtonDefaultTitle = () => _('Import from JEX');
+export const importButtonDescription = () => _('Loads a notebook from a JEX file.');
+
+const getTitle = (taskStatus: TaskStatus) => {
+ if (taskStatus === TaskStatus.NotStarted) {
+ return importButtonDefaultTitle();
+ } else if (taskStatus === TaskStatus.InProgress) {
+ return _('Importing...');
+ } else {
+ return _('Imported successfully!');
+ }
+};
+
+const runImportTask = async (
+ _onProgress: OnProgressCallback,
+ setAfterCompleteListener: SetAfterCompleteListenerCallback,
+) => {
+ const importTargetPath = join(await makeImportExportCacheDirectory(), 'to-import.jex');
+ logger.info('Importing...');
+
+ setAfterCompleteListener(async (_success: boolean) => {
+ await shim.fsDriver().remove(importTargetPath);
+ });
+
+ const importFiles = await pickDocument(false);
+ if (importFiles.length === 0) {
+ return { success: false, warnings: [] };
+ }
+
+ const sourceFilePath = importFiles[0].uri;
+ await shim.fsDriver().copy(sourceFilePath, importTargetPath);
+
+
+ const status = await InteropService.instance().import({
+ path: importTargetPath,
+ format: 'jex',
+ });
+
+ logger.info('Imported successfully');
+
+ return { success: true, warnings: status.warnings };
+};
+
+const NoteImportButton: FunctionComponent = props => {
+ return (
+
+ );
+};
+
+export default NoteImportButton;
diff --git a/packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/TaskButton.tsx b/packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/TaskButton.tsx
new file mode 100644
index 00000000000..7f0b3d05fac
--- /dev/null
+++ b/packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/TaskButton.tsx
@@ -0,0 +1,119 @@
+import * as React from 'react';
+import { Alert, Text, View } from 'react-native';
+import { _ } from '@joplin/lib/locale';
+import { ProgressBar } from 'react-native-paper';
+import { FunctionComponent, useCallback, useState } from 'react';
+import { ConfigScreenStyles } from '../configScreenStyles';
+import SettingsButton from '../SettingsButton';
+import Logger from '@joplin/utils/Logger';
+
+// Undefined = indeterminant progress
+export type OnProgressCallback = (progressFraction: number|undefined)=> void;
+export type AfterCompleteListener = (success: boolean)=> Promise;
+export type SetAfterCompleteListenerCallback = (listener: AfterCompleteListener)=> void;
+
+const logger = Logger.create('TaskButton');
+
+interface TaskResult {
+ warnings: string[];
+ success: boolean;
+}
+
+export enum TaskStatus {
+ NotStarted,
+ InProgress,
+ Done,
+}
+
+interface Props {
+ taskName: string;
+ title: (status: TaskStatus)=> string;
+ description?: string;
+ styles: ConfigScreenStyles;
+ onRunTask: (
+ setProgress: OnProgressCallback,
+ setAfterCompleteListener: SetAfterCompleteListenerCallback,
+ )=> Promise;
+}
+
+const TaskButton: FunctionComponent = props => {
+ const [taskStatus, setTaskStatus] = useState(TaskStatus.NotStarted);
+ const [progress, setProgress] = useState(0);
+ const [warnings, setWarnings] = useState('');
+
+ const startTask = useCallback(async () => {
+ // Don't run multiple task instances at the same time.
+ if (taskStatus === TaskStatus.InProgress) {
+ return;
+ }
+
+ logger.info(`Starting task: ${props.taskName}`);
+
+ setTaskStatus(TaskStatus.InProgress);
+ let completedSuccessfully = false;
+ let afterCompleteListener: AfterCompleteListener = async () => {};
+
+ try {
+ // Initially, undetermined progress
+ setProgress(undefined);
+
+ const status = await props.onRunTask(setProgress, (afterComplete: AfterCompleteListener) => {
+ afterCompleteListener = afterComplete;
+ });
+
+ setWarnings(status.warnings.join('\n'));
+ if (status.success) {
+ setTaskStatus(TaskStatus.Done);
+ completedSuccessfully = true;
+ }
+ } catch (error) {
+ logger.error(`Task ${props.taskName} failed`);
+ Alert.alert(_('Error'), _('Task "%s" failed with error: %s', props.taskName, error.toString()));
+ } finally {
+ if (!completedSuccessfully) {
+ setTaskStatus(TaskStatus.NotStarted);
+ }
+
+ await afterCompleteListener(completedSuccessfully);
+ }
+ }, [props.onRunTask, props.taskName, taskStatus]);
+
+ if (taskStatus === TaskStatus.NotStarted || taskStatus === TaskStatus.InProgress) {
+ const progressComponent = (
+
+ );
+
+ const startOrCancelExportButton = (
+
+ );
+
+ return startOrCancelExportButton;
+ } else {
+ const warningComponent = (
+
+ {_('Warnings:\n%s', warnings)}
+
+ );
+
+ const taskSummary = (
+
+ {props.title(taskStatus)}
+ {warnings.length > 0 ? warningComponent : null}
+
+ );
+ return taskSummary;
+ }
+};
+
+export default TaskButton;
diff --git a/packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/exportAllFolders.ts b/packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/exportAllFolders.ts
index 16d9e090f9b..88fadcdc871 100644
--- a/packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/exportAllFolders.ts
+++ b/packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/exportAllFolders.ts
@@ -1,15 +1,6 @@
import Folder from '@joplin/lib/models/Folder';
import InteropService from '@joplin/lib/services/interop/InteropService';
import { ExportModuleOutputFormat, ExportOptions, FileSystemItem, OnExportProgressCallback } from '@joplin/lib/services/interop/types';
-import shim from '@joplin/lib/shim';
-
-import { CachesDirectoryPath } from 'react-native-fs';
-export const makeExportCacheDirectory = async () => {
- const targetDir = `${CachesDirectoryPath}/exports`;
- await shim.fsDriver().mkdir(targetDir);
-
- return targetDir;
-};
const exportFolders = async (path: string, onProgress: OnExportProgressCallback) => {
const folders = await Folder.all();
diff --git a/packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/makeImportExportCacheDirectory.ts b/packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/makeImportExportCacheDirectory.ts
new file mode 100644
index 00000000000..68419dc8631
--- /dev/null
+++ b/packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/makeImportExportCacheDirectory.ts
@@ -0,0 +1,12 @@
+
+import shim from '@joplin/lib/shim';
+import { CachesDirectoryPath } from 'react-native-fs';
+
+const makeExportCacheDirectory = async () => {
+ const targetDir = `${CachesDirectoryPath}/exports`;
+ await shim.fsDriver().mkdir(targetDir);
+
+ return targetDir;
+};
+
+export default makeExportCacheDirectory;