-
-
Notifications
You must be signed in to change notification settings - Fork 4.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Mobile: Support importing from JEX files (#10269)
- Loading branch information
1 parent
ce67291
commit 2ae08ff
Showing
10 changed files
with
278 additions
and
100 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
133 changes: 50 additions & 83 deletions
133
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteExportButton.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,116 +1,83 @@ | ||
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'); | ||
|
||
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> = props => { | ||
const [exportStatus, setExportStatus] = useState<ExportStatus>(ExportStatus.NotStarted); | ||
const [exportProgress, setExportProgress] = useState<number|undefined>(0); | ||
const [warnings, setWarnings] = useState<string>(''); | ||
|
||
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.InProgress) { | ||
return _('Exporting...'); | ||
} else { | ||
return exportButtonDefaultTitle(); | ||
} | ||
}; | ||
|
||
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 = ( | ||
<ProgressBar | ||
visible={exportStatus === ExportStatus.Exporting} | ||
indeterminate={exportProgress === undefined} | ||
progress={exportProgress}/> | ||
); | ||
onProgress(1); | ||
|
||
const startOrCancelExportButton = ( | ||
<SettingsButton | ||
title={exportStatus === ExportStatus.Exporting ? _('Exporting...') : exportButtonTitle()} | ||
disabled={exportStatus === ExportStatus.Exporting} | ||
description={exportStatus === ExportStatus.NotStarted ? exportButtonDescription() : null} | ||
statusComponent={progressComponent} | ||
clickHandler={startExport} | ||
styles={props.styles} | ||
/> | ||
); | ||
logger.info('Export complete'); | ||
|
||
return startOrCancelExportButton; | ||
} else { | ||
const warningComponent = ( | ||
<Text style={props.styles.styleSheet.warningText}> | ||
{_('Warnings:\n%s', warnings)} | ||
</Text> | ||
); | ||
return { warnings: status.warnings, success: true }; | ||
}; | ||
|
||
const exportSummary = ( | ||
<View style={props.styles.styleSheet.settingContainer}> | ||
<Text style={props.styles.styleSheet.descriptionText}>{_('Exported successfully!')}</Text> | ||
{warnings.length > 0 ? warningComponent : null} | ||
</View> | ||
); | ||
return exportSummary; | ||
} | ||
const NoteExportButton: FunctionComponent<Props> = props => { | ||
return ( | ||
<TaskButton | ||
taskName={exportButtonDefaultTitle()} | ||
buttonLabel={getTitle} | ||
finishedLabel={_('Exported successfully!')} | ||
description={exportButtonDescription()} | ||
styles={props.styles} | ||
onRunTask={runExportTask} | ||
/> | ||
); | ||
}; | ||
|
||
export default NoteExportButton; |
83 changes: 83 additions & 0 deletions
83
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteImportButton.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; |
Oops, something went wrong.