mirror of
https://github.com/laurent22/joplin.git
synced 2025-01-23 18:53:36 +02:00
Mobile: Support importing from JEX files (#10269)
This commit is contained in:
parent
ce672915da
commit
2ae08ff46e
@ -567,9 +567,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/ExportProfileButton.js
|
||||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteExportButton.test.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/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/exportAllFolders.js
|
||||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/exportDebugReport.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/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/SectionHeader.js
|
||||||
packages/app-mobile/components/screens/ConfigScreen/SectionSelector.js
|
packages/app-mobile/components/screens/ConfigScreen/SectionSelector.js
|
||||||
packages/app-mobile/components/screens/ConfigScreen/SettingComponent.js
|
packages/app-mobile/components/screens/ConfigScreen/SettingComponent.js
|
||||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -547,9 +547,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/ExportProfileButton.js
|
||||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteExportButton.test.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/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/exportAllFolders.js
|
||||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/exportDebugReport.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/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/SectionHeader.js
|
||||||
packages/app-mobile/components/screens/ConfigScreen/SectionSelector.js
|
packages/app-mobile/components/screens/ConfigScreen/SectionSelector.js
|
||||||
packages/app-mobile/components/screens/ConfigScreen/SettingComponent.js
|
packages/app-mobile/components/screens/ConfigScreen/SettingComponent.js
|
||||||
|
@ -18,7 +18,7 @@ import * as shared from '@joplin/lib/components/shared/config/config-shared';
|
|||||||
import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry';
|
import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry';
|
||||||
import biometricAuthenticate from '../../biometrics/biometricAuthenticate';
|
import biometricAuthenticate from '../../biometrics/biometricAuthenticate';
|
||||||
import configScreenStyles, { ConfigScreenStyles } from './configScreenStyles';
|
import configScreenStyles, { ConfigScreenStyles } from './configScreenStyles';
|
||||||
import NoteExportButton, { exportButtonDescription, exportButtonTitle } from './NoteExportSection/NoteExportButton';
|
import NoteExportButton, { exportButtonDescription, exportButtonDefaultTitle } from './NoteExportSection/NoteExportButton';
|
||||||
import SettingsButton from './SettingsButton';
|
import SettingsButton from './SettingsButton';
|
||||||
import Clipboard from '@react-native-community/clipboard';
|
import Clipboard from '@react-native-community/clipboard';
|
||||||
import { ReactElement, ReactNode } from 'react';
|
import { ReactElement, ReactNode } from 'react';
|
||||||
@ -31,6 +31,7 @@ import { Button, TextInput } from 'react-native-paper';
|
|||||||
import PluginService, { PluginSettings } from '@joplin/lib/services/plugins/PluginService';
|
import PluginService, { PluginSettings } from '@joplin/lib/services/plugins/PluginService';
|
||||||
import PluginStates, { getSearchText as getPluginStatesSearchText } from './plugins/PluginStates';
|
import PluginStates, { getSearchText as getPluginStatesSearchText } from './plugins/PluginStates';
|
||||||
import PluginUploadButton, { canInstallPluginsFromFile, buttonLabel as pluginUploadButtonSearchText } from './plugins/PluginUploadButton';
|
import PluginUploadButton, { canInstallPluginsFromFile, buttonLabel as pluginUploadButtonSearchText } from './plugins/PluginUploadButton';
|
||||||
|
import NoteImportButton, { importButtonDefaultTitle, importButtonDescription } from './NoteExportSection/NoteImportButton';
|
||||||
|
|
||||||
interface ConfigScreenState {
|
interface ConfigScreenState {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
@ -524,10 +525,14 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
|
|||||||
addSettingButton('fix_search_engine_index', this.state.fixingSearchIndex ? _('Fixing search index...') : _('Fix search index'), this.fixSearchEngineIndexButtonPress_, { disabled: this.state.fixingSearchIndex, description: _('Use this to rebuild the search index if there is a problem with search. It may take a long time depending on the number of notes.') });
|
addSettingButton('fix_search_engine_index', this.state.fixingSearchIndex ? _('Fixing search index...') : _('Fix search index'), this.fixSearchEngineIndexButtonPress_, { disabled: this.state.fixingSearchIndex, description: _('Use this to rebuild the search index if there is a problem with search. It may take a long time depending on the number of notes.') });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (section.name === 'export') {
|
if (section.name === 'importOrExport') {
|
||||||
addSettingComponent(
|
addSettingComponent(
|
||||||
<NoteExportButton key='export_as_jex_button' styles={this.styles()} />,
|
<NoteExportButton key='export_as_jex_button' styles={this.styles()} />,
|
||||||
[exportButtonTitle(), exportButtonDescription()],
|
[exportButtonDefaultTitle(), exportButtonDescription()],
|
||||||
|
);
|
||||||
|
addSettingComponent(
|
||||||
|
<NoteImportButton key='import_as_jex_button' styles={this.styles()} />,
|
||||||
|
[importButtonDefaultTitle(), importButtonDescription()],
|
||||||
);
|
);
|
||||||
addSettingComponent(
|
addSettingComponent(
|
||||||
<ExportDebugReportButton key='export_report_button' styles={this.styles()}/>,
|
<ExportDebugReportButton key='export_report_button' styles={this.styles()}/>,
|
||||||
|
@ -1,16 +1,15 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Text, Alert, View } from 'react-native';
|
|
||||||
import { _ } from '@joplin/lib/locale';
|
import { _ } from '@joplin/lib/locale';
|
||||||
import Logger from '@joplin/utils/Logger';
|
import Logger from '@joplin/utils/Logger';
|
||||||
import { ProgressBar } from 'react-native-paper';
|
import { FunctionComponent } from 'react';
|
||||||
import { FunctionComponent, useCallback, useState } from 'react';
|
|
||||||
import shim from '@joplin/lib/shim';
|
import shim from '@joplin/lib/shim';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import Share from 'react-native-share';
|
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 { ExportProgressState } from '@joplin/lib/services/interop/types';
|
||||||
import { ConfigScreenStyles } from '../configScreenStyles';
|
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');
|
const logger = Logger.create('NoteExportButton');
|
||||||
|
|
||||||
@ -18,99 +17,67 @@ interface Props {
|
|||||||
styles: ConfigScreenStyles;
|
styles: ConfigScreenStyles;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ExportStatus {
|
export const exportButtonDefaultTitle = () => _('Export all notes as JEX');
|
||||||
NotStarted,
|
|
||||||
Exporting,
|
|
||||||
Exported,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const exportButtonTitle = () => _('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.');
|
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> = props => {
|
const getTitle = (taskStatus: TaskStatus) => {
|
||||||
const [exportStatus, setExportStatus] = useState<ExportStatus>(ExportStatus.NotStarted);
|
if (taskStatus === TaskStatus.InProgress) {
|
||||||
const [exportProgress, setExportProgress] = useState<number|undefined>(0);
|
return _('Exporting...');
|
||||||
const [warnings, setWarnings] = useState<string>('');
|
} else {
|
||||||
|
return exportButtonDefaultTitle();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const startExport = useCallback(async () => {
|
const runExportTask = async (
|
||||||
// Don't run multiple exports at the same time.
|
onProgress: OnProgressCallback,
|
||||||
if (exportStatus === ExportStatus.Exporting) {
|
setAfterCompleteListener: SetAfterCompleteListenerCallback,
|
||||||
return;
|
) => {
|
||||||
}
|
const exportTargetPath = join(await makeImportExportCacheDirectory(), 'jex-export.jex');
|
||||||
|
logger.info(`Exporting all folders to path ${exportTargetPath}`);
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
setExportStatus(ExportStatus.Exported);
|
|
||||||
setWarnings(status.warnings.join('\n'));
|
|
||||||
|
|
||||||
|
setAfterCompleteListener(async (success: boolean) => {
|
||||||
|
if (success) {
|
||||||
await Share.open({
|
await Share.open({
|
||||||
type: 'application/jex',
|
type: 'application/jex',
|
||||||
filename: 'export.jex',
|
filename: 'export.jex',
|
||||||
url: `file://${exportTargetPath}`,
|
url: `file://${exportTargetPath}`,
|
||||||
failOnCancel: false,
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
if (exportStatus === ExportStatus.NotStarted || exportStatus === ExportStatus.Exporting) {
|
// Initially, undetermined progress
|
||||||
const progressComponent = (
|
onProgress(undefined);
|
||||||
<ProgressBar
|
|
||||||
visible={exportStatus === ExportStatus.Exporting}
|
|
||||||
indeterminate={exportProgress === undefined}
|
|
||||||
progress={exportProgress}/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const startOrCancelExportButton = (
|
const status = await exportAllFolders(exportTargetPath, (status, progress) => {
|
||||||
<SettingsButton
|
if (progress !== null) {
|
||||||
title={exportStatus === ExportStatus.Exporting ? _('Exporting...') : exportButtonTitle()}
|
onProgress(progress);
|
||||||
disabled={exportStatus === ExportStatus.Exporting}
|
} else if (status === ExportProgressState.Closing || status === ExportProgressState.QueuingItems) {
|
||||||
description={exportStatus === ExportStatus.NotStarted ? exportButtonDescription() : null}
|
// We don't have a numeric progress value and the closing/queuing state may take a while.
|
||||||
statusComponent={progressComponent}
|
// Set a special progress value:
|
||||||
clickHandler={startExport}
|
onProgress(undefined);
|
||||||
styles={props.styles}
|
}
|
||||||
/>
|
});
|
||||||
);
|
|
||||||
|
|
||||||
return startOrCancelExportButton;
|
onProgress(1);
|
||||||
} else {
|
|
||||||
const warningComponent = (
|
|
||||||
<Text style={props.styles.styleSheet.warningText}>
|
|
||||||
{_('Warnings:\n%s', warnings)}
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
|
|
||||||
const exportSummary = (
|
logger.info('Export complete');
|
||||||
<View style={props.styles.styleSheet.settingContainer}>
|
|
||||||
<Text style={props.styles.styleSheet.descriptionText}>{_('Exported successfully!')}</Text>
|
return { warnings: status.warnings, success: true };
|
||||||
{warnings.length > 0 ? warningComponent : null}
|
};
|
||||||
</View>
|
|
||||||
);
|
const NoteExportButton: FunctionComponent<Props> = props => {
|
||||||
return exportSummary;
|
return (
|
||||||
}
|
<TaskButton
|
||||||
|
taskName={exportButtonDefaultTitle()}
|
||||||
|
buttonLabel={getTitle}
|
||||||
|
finishedLabel={_('Exported successfully!')}
|
||||||
|
description={exportButtonDescription()}
|
||||||
|
styles={props.styles}
|
||||||
|
onRunTask={runExportTask}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default NoteExportButton;
|
export default NoteExportButton;
|
||||||
|
@ -0,0 +1,83 @@
|
|||||||
|
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';
|
||||||
|
import { Platform } from 'react-native';
|
||||||
|
|
||||||
|
const logger = Logger.create('NoteImportButton');
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
styles: ConfigScreenStyles;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exported for search filtering
|
||||||
|
export const importButtonDefaultTitle = () => _('Import from JEX');
|
||||||
|
export const importButtonDescription = () => _('Import notes from a JEX (Joplin Export) file.');
|
||||||
|
|
||||||
|
const getTitle = (taskStatus: TaskStatus) => {
|
||||||
|
if (taskStatus === TaskStatus.InProgress) {
|
||||||
|
return _('Importing...');
|
||||||
|
} else {
|
||||||
|
return importButtonDefaultTitle();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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) {
|
||||||
|
logger.info('Canceled.');
|
||||||
|
return { success: false, warnings: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceFileUri = importFiles[0].uri;
|
||||||
|
const sourceFilePath = Platform.select({
|
||||||
|
android: sourceFileUri,
|
||||||
|
ios: decodeURI(sourceFileUri),
|
||||||
|
});
|
||||||
|
await shim.fsDriver().copy(sourceFilePath, importTargetPath);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const status = await InteropService.instance().import({
|
||||||
|
path: importTargetPath,
|
||||||
|
format: 'jex',
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info('Imported successfully');
|
||||||
|
return { success: true, warnings: status.warnings };
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Import failed with error', error);
|
||||||
|
throw new Error(_('Import failed. Make sure a JEX file was selected.\nDetails: %s', error.toString()));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const NoteImportButton: FunctionComponent<Props> = props => {
|
||||||
|
return (
|
||||||
|
<TaskButton
|
||||||
|
taskName={importButtonDefaultTitle()}
|
||||||
|
description={importButtonDescription()}
|
||||||
|
buttonLabel={getTitle}
|
||||||
|
finishedLabel={_('Imported successfully!')}
|
||||||
|
styles={props.styles}
|
||||||
|
onRunTask={runImportTask}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NoteImportButton;
|
@ -0,0 +1,114 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { Alert, Text } 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 = indeterminate progress
|
||||||
|
export type OnProgressCallback = (progressFraction: number|undefined)=> void;
|
||||||
|
export type AfterCompleteListener = (success: boolean)=> Promise<void>;
|
||||||
|
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;
|
||||||
|
buttonLabel: (status: TaskStatus)=> string;
|
||||||
|
finishedLabel: string;
|
||||||
|
description?: string;
|
||||||
|
styles: ConfigScreenStyles;
|
||||||
|
onRunTask: (
|
||||||
|
setProgress: OnProgressCallback,
|
||||||
|
setAfterCompleteListener: SetAfterCompleteListenerCallback,
|
||||||
|
)=> Promise<TaskResult>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TaskButton: FunctionComponent<Props> = props => {
|
||||||
|
const [taskStatus, setTaskStatus] = useState<TaskStatus>(TaskStatus.NotStarted);
|
||||||
|
const [progress, setProgress] = useState<number|undefined>(0);
|
||||||
|
const [warnings, setWarnings] = useState<string>('');
|
||||||
|
|
||||||
|
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`, error);
|
||||||
|
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]);
|
||||||
|
|
||||||
|
let statusComponent = (
|
||||||
|
<ProgressBar
|
||||||
|
visible={taskStatus === TaskStatus.InProgress}
|
||||||
|
indeterminate={progress === undefined}
|
||||||
|
progress={progress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
if (taskStatus === TaskStatus.Done && warnings.length > 0) {
|
||||||
|
statusComponent = (
|
||||||
|
<Text style={props.styles.styleSheet.warningText}>
|
||||||
|
{_('Completed with warnings:\n%s', warnings)}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let buttonDescription = props.description;
|
||||||
|
if (taskStatus === TaskStatus.Done) {
|
||||||
|
buttonDescription = props.finishedLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsButton
|
||||||
|
title={props.buttonLabel(taskStatus)}
|
||||||
|
disabled={taskStatus === TaskStatus.InProgress}
|
||||||
|
description={buttonDescription}
|
||||||
|
statusComponent={statusComponent}
|
||||||
|
clickHandler={startTask}
|
||||||
|
styles={props.styles}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TaskButton;
|
@ -1,15 +1,6 @@
|
|||||||
import Folder from '@joplin/lib/models/Folder';
|
import Folder from '@joplin/lib/models/Folder';
|
||||||
import InteropService from '@joplin/lib/services/interop/InteropService';
|
import InteropService from '@joplin/lib/services/interop/InteropService';
|
||||||
import { ExportModuleOutputFormat, ExportOptions, FileSystemItem, OnExportProgressCallback } from '@joplin/lib/services/interop/types';
|
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 exportFolders = async (path: string, onProgress: OnExportProgressCallback) => {
|
||||||
const folders = await Folder.all();
|
const folders = await Folder.all();
|
||||||
|
@ -0,0 +1,12 @@
|
|||||||
|
|
||||||
|
import shim from '@joplin/lib/shim';
|
||||||
|
import { CachesDirectoryPath } from 'react-native-fs';
|
||||||
|
|
||||||
|
const makeImportExportCacheDirectory = async () => {
|
||||||
|
const targetDir = `${CachesDirectoryPath}/exports`;
|
||||||
|
await shim.fsDriver().mkdir(targetDir);
|
||||||
|
|
||||||
|
return targetDir;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default makeImportExportCacheDirectory;
|
@ -236,7 +236,7 @@ export const settingsSections = createSelector(
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
output.push(...([
|
output.push(...([
|
||||||
'tools', 'export', 'moreInfo',
|
'tools', 'importOrExport', 'moreInfo',
|
||||||
].map(name => {
|
].map(name => {
|
||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
|
@ -2711,7 +2711,7 @@ class Setting extends BaseModel {
|
|||||||
'server',
|
'server',
|
||||||
'keymap',
|
'keymap',
|
||||||
'tools',
|
'tools',
|
||||||
'export',
|
'importOrExport',
|
||||||
'moreInfo',
|
'moreInfo',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@ -2776,7 +2776,7 @@ class Setting extends BaseModel {
|
|||||||
if (name === 'keymap') return _('Keyboard Shortcuts');
|
if (name === 'keymap') return _('Keyboard Shortcuts');
|
||||||
if (name === 'joplinCloud') return _('Joplin Cloud');
|
if (name === 'joplinCloud') return _('Joplin Cloud');
|
||||||
if (name === 'tools') return _('Tools');
|
if (name === 'tools') return _('Tools');
|
||||||
if (name === 'export') return _('Export');
|
if (name === 'importOrExport') return _('Import and Export');
|
||||||
if (name === 'moreInfo') return _('More information');
|
if (name === 'moreInfo') return _('More information');
|
||||||
|
|
||||||
if (this.customSections_[name] && this.customSections_[name].label) return this.customSections_[name].label;
|
if (this.customSections_[name] && this.customSections_[name].label) return this.customSections_[name].label;
|
||||||
@ -2804,7 +2804,7 @@ class Setting extends BaseModel {
|
|||||||
'note': _('Geolocation, spellcheck, editor toolbar, image resize'),
|
'note': _('Geolocation, spellcheck, editor toolbar, image resize'),
|
||||||
'revisionService': _('Toggle note history, keep notes for'),
|
'revisionService': _('Toggle note history, keep notes for'),
|
||||||
'tools': _('Logs, profiles, sync status'),
|
'tools': _('Logs, profiles, sync status'),
|
||||||
'export': _('Export your data'),
|
'importOrExport': _('Import or export your data'),
|
||||||
'plugins': _('Enable or disable plugins'),
|
'plugins': _('Enable or disable plugins'),
|
||||||
'moreInfo': _('Donate, website'),
|
'moreInfo': _('Donate, website'),
|
||||||
};
|
};
|
||||||
@ -2846,7 +2846,7 @@ class Setting extends BaseModel {
|
|||||||
'keymap': 'fa fa-keyboard',
|
'keymap': 'fa fa-keyboard',
|
||||||
'joplinCloud': 'fa fa-cloud',
|
'joplinCloud': 'fa fa-cloud',
|
||||||
'tools': 'fa fa-toolbox',
|
'tools': 'fa fa-toolbox',
|
||||||
'export': 'fa fa-file-export',
|
'importOrExport': 'fa fa-file-export',
|
||||||
'moreInfo': 'fa fa-info-circle',
|
'moreInfo': 'fa fa-info-circle',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user