一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
138 行
4.5 KiB
TypeScript
138 行
4.5 KiB
TypeScript
import * as React from 'react';
|
|
import { _ } from '@joplin/lib/locale';
|
|
import Logger from '@joplin/utils/Logger';
|
|
import { FunctionComponent, useContext } from 'react';
|
|
import { join, basename } 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';
|
|
import { FolderEntity } from '@joplin/lib/services/database/types';
|
|
import Folder from '@joplin/lib/models/Folder';
|
|
import { fileExtension } from '@joplin/lib/path-utils';
|
|
import { NoteLockPrompts, promptForImportedNoteLockKey } from '@joplin/lib/services/noteLock/noteLockPrompts';
|
|
import { ErrorCode } from '@joplin/lib/errors';
|
|
import { DialogContext } from '../../../DialogManager';
|
|
|
|
const logger = Logger.create('NoteImportButton');
|
|
|
|
interface Props {
|
|
styles: ConfigScreenStyles;
|
|
defaultTitle: string;
|
|
description: string;
|
|
format: string;
|
|
activeFolder?: FolderEntity;
|
|
// Navigating to the Config screen does nothing while it is already open, so the screen switches its own section.
|
|
onOpenNoteLockSetup: ()=> void;
|
|
}
|
|
|
|
export const importedFolderTitle = () => {
|
|
return _('Imported Notes');
|
|
};
|
|
|
|
export const textImportExtensions = ['md', 'markdown', 'txt', 'html'];
|
|
|
|
const importedFolder = async () => {
|
|
let folder = await Folder.loadByFields({
|
|
title: importedFolderTitle(),
|
|
deleted_time: 0,
|
|
});
|
|
if (!folder) {
|
|
folder = await Folder.save({ title: importedFolderTitle() });
|
|
}
|
|
return folder;
|
|
};
|
|
|
|
const NoteImportButton: FunctionComponent<Props> = props => {
|
|
const dialogs = useContext(DialogContext);
|
|
const noteLockPrompts: NoteLockPrompts = {
|
|
promptPassword: label => dialogs.promptForText(label, '', true),
|
|
goToNoteLockSetup: props.onOpenNoteLockSetup,
|
|
};
|
|
|
|
const getTitle = (taskStatus: TaskStatus) => {
|
|
if (taskStatus === TaskStatus.InProgress) {
|
|
return _('Importing...');
|
|
} else {
|
|
return props.defaultTitle;
|
|
}
|
|
};
|
|
|
|
const runImportTask = async (
|
|
_onProgress: OnProgressCallback,
|
|
setAfterCompleteListener: SetAfterCompleteListenerCallback,
|
|
) => {
|
|
logger.info(`Importing ${props.format}...`);
|
|
|
|
const importFiles = await pickDocument({ multiple: false });
|
|
if (importFiles.length === 0) {
|
|
logger.info('Canceled.');
|
|
return { success: false, warnings: [] };
|
|
}
|
|
|
|
const sourceFileUri = importFiles[0].uri;
|
|
const sourceFilePath = Platform.select({
|
|
default: sourceFileUri,
|
|
ios: decodeURIComponent(sourceFileUri),
|
|
});
|
|
|
|
let validExtensions = ['jex'];
|
|
if (props.format === 'txt') validExtensions = textImportExtensions;
|
|
|
|
// importFiles[0].fileName can be null on iOS
|
|
const sourceFileName = importFiles[0].fileName ?? basename(sourceFilePath);
|
|
const extension = fileExtension(sourceFileName).toLowerCase();
|
|
|
|
if (!validExtensions.includes(extension)) {
|
|
throw new Error(_('Unsupported file extension. Expected: %s', validExtensions.join(', ')));
|
|
}
|
|
|
|
const importTargetPath = join(await makeImportExportCacheDirectory(), sourceFileName);
|
|
setAfterCompleteListener(async (_success: boolean) => {
|
|
await shim.fsDriver().remove(importTargetPath);
|
|
});
|
|
|
|
await shim.fsDriver().copy(sourceFilePath, importTargetPath);
|
|
|
|
let activeFolderId = props.activeFolder ? props.activeFolder.id : null;
|
|
if (props.format === 'txt' && !activeFolderId) {
|
|
activeFolderId = (await importedFolder()).id;
|
|
}
|
|
|
|
try {
|
|
const status = await InteropService.instance().import({
|
|
path: importTargetPath,
|
|
format: props.format,
|
|
destinationFolderId: activeFolderId,
|
|
onNoteLockKey: backupKey => promptForImportedNoteLockKey(backupKey, noteLockPrompts),
|
|
});
|
|
|
|
logger.info('Imported successfully');
|
|
return { success: true, warnings: status.warnings };
|
|
} catch (error) {
|
|
if (error.code === ErrorCode.Cancelled) {
|
|
logger.info('Canceled.');
|
|
return { success: false, warnings: [] };
|
|
}
|
|
logger.error('Import failed with error', error);
|
|
throw new Error(_('Import failed. Make sure a %s file was selected.\nDetails: %s', props.format, error.toString()));
|
|
}
|
|
};
|
|
|
|
return (
|
|
<TaskButton
|
|
taskName={props.defaultTitle}
|
|
description={props.description}
|
|
buttonLabel={getTitle}
|
|
finishedLabel={_('Imported successfully!')}
|
|
styles={props.styles}
|
|
onRunTask={runImportTask}
|
|
/>
|
|
);
|
|
};
|
|
|
|
export default NoteImportButton;
|