文件
chen 301194963b
一些检查失败了
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
feat: Initial clean release of CHM
2026-11-04 11:29:39 +08:00

202 行
7.3 KiB
TypeScript

import CommandService, { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
import InteropService from '@joplin/lib/services/interop/InteropService';
import { FileSystemItem, ImportModuleOutputFormat, ModuleType } from '@joplin/lib/services/interop/types';
import bridge from '../../../services/bridge';
import { WindowControl } from '../utils/useWindowControl';
import { _ } from '@joplin/lib/locale';
import makeDiscourseDebugUrl from '@joplin/lib/makeDiscourseDebugUrl';
import PluginService from '@joplin/lib/services/plugins/PluginService';
import Setting from '@joplin/lib/models/Setting';
import { PackageInfo } from '@joplin/lib/versionInfo';
import shim from '@joplin/lib/shim';
import { ImportModule } from '@joplin/lib/services/interop/Module';
import { isRecoverableError } from '@joplin/lib/import-enex';
import Logger from '@joplin/utils/Logger';
import { Dispatch } from 'redux';
import { MasterKeyEntity } from '@joplin/lib/services/e2ee/types';
import { promptForImportedNoteLockKey } from '@joplin/lib/services/noteLock/noteLockPrompts';
import noteLockPrompts from '../../../utils/noteLockPrompts';
import { ErrorCode } from '@joplin/lib/errors';
const packageInfo: PackageInfo = require('../../../packageInfo.js');
const logger = Logger.create('importFrom');
const makeImportNoteLockKeyHandler = (modalMessage: string, dispatch: Dispatch) => async (backupKey: MasterKeyEntity) => {
// The password prompts are DOM dialogs and would render behind the import modal overlay.
void CommandService.instance().execute('hideModalMessage');
try {
return await promptForImportedNoteLockKey(backupKey, noteLockPrompts(dispatch));
} finally {
void CommandService.instance().execute('showModalMessage', modalMessage);
}
};
export const declaration: CommandDeclaration = {
name: 'importFrom',
label: () => _('Import...'),
};
export interface ImportCommandOptions {
sourcePath: string|undefined;
sourceType: FileSystemItem;
destinationFolderId: string|null;
importFormat: string;
outputFormat: ImportModuleOutputFormat;
}
const findImportModule = async (commandOptions: ImportCommandOptions|null, control: WindowControl) => {
if (commandOptions) {
const module = InteropService.instance().findModuleByFormat(
ModuleType.Importer, commandOptions.importFormat, commandOptions.sourceType, commandOptions.outputFormat);
if (module) {
return module as ImportModule;
}
}
const importModules = InteropService.instance().modules().filter(module => module.type === ModuleType.Importer) as ImportModule[];
return await control.showPrompt({
label: _('Select the type of file to be imported:'),
value: '',
suggestions: importModules.map(module => {
const label = module.fullLabel();
return {
key: `${module.type}--${label}`,
value: module,
label: module.fullLabel(),
};
}),
});
};
const promptForSourcePath = async (module: ImportModule, sourceType: FileSystemItem|undefined) => {
if (!sourceType) {
if (!module.sources.includes(FileSystemItem.Directory)) {
sourceType = FileSystemItem.File;
}
if (!module.sources.includes(FileSystemItem.File)) {
sourceType = FileSystemItem.Directory;
}
}
if (sourceType === FileSystemItem.File) {
return await bridge().showOpenDialog({
filters: [{ name: module.description, extensions: module.fileExtensions }],
});
} else if (sourceType === FileSystemItem.Directory) {
return await bridge().showOpenDialog({
properties: ['openDirectory', 'createDirectory'],
});
} else {
return await bridge().showOpenDialog({
properties: ['openDirectory', 'openFile'],
});
}
};
export const runtime = (control: WindowControl): CommandRuntime => {
return {
// Since this can be run from "go to anything", partialOptions needs to support being null or empty.
execute: async (context: CommandContext, options: ImportCommandOptions|undefined) => {
const importModule = await findImportModule(options, control);
if (!importModule) return null; // E.g. if cancelled
let sourcePath = options?.sourcePath ?? await promptForSourcePath(importModule, options?.sourceType);
if (Array.isArray(sourcePath)) {
sourcePath = sourcePath[0];
}
// Handle the case where the directory picker action was cancelled
if (!sourcePath) return null;
if (!options) {
const isDirectory = await shim.fsDriver().isDirectory(sourcePath);
const importsMultipleNotes = importModule.isNoteArchive || isDirectory;
const destinationFolderId = importsMultipleNotes ? null : context.state.selectedFolderId;
const importFormat = importModule.format;
const outputFormat = importModule.outputFormat;
options = {
sourcePath,
destinationFolderId,
importFormat,
outputFormat,
sourceType: isDirectory ? FileSystemItem.Directory : FileSystemItem.File,
};
}
if (options.importFormat === 'one' && !shim.isWindows()) {
const response = bridge().showMessageBox(
_('This notebook contains Windows-specific files. For maximum compatibility, consider importing the notebook from Windows.'),
{
type: 'warning',
buttons: [_('Import anyway'), _('Cancel')],
defaultId: 0,
cancelId: 1,
},
);
if (response === 1) return null;
}
const modalMessage = _('Importing from "%s" as "%s" format. Please wait...', sourcePath, options.importFormat);
void CommandService.instance().execute('showModalMessage', modalMessage);
const errors: (string|Error)[] = [];
const importOptions = {
path: sourcePath,
format: options.importFormat,
outputFormat: options.outputFormat,
onProgress: (status: Record<string, unknown>) => {
const statusStrings: string[] = Object.keys(status).map((key: string) => {
return `${key}: ${status[key]}`;
});
void CommandService.instance().execute('showModalMessage', `${modalMessage}\n\n${statusStrings.join('\n')}`);
},
onError: (error: string|Error) => {
// Recovered errors are logged but not reported, since the
// notes were imported successfully anyway.
if (!isRecoverableError(error)) errors.push(error);
console.warn(error);
},
destinationFolderId: options.destinationFolderId,
onNoteLockKey: makeImportNoteLockKeyHandler(modalMessage, context.dispatch),
};
const service = InteropService.instance();
try {
const result = await service.import(importOptions);
// eslint-disable-next-line no-console
console.info('Import result: ', result);
} catch (error) {
if (error.code !== ErrorCode.Cancelled) {
logger.error(error);
bridge().showErrorMessageBox(error.message);
}
}
void CommandService.instance().execute('hideModalMessage');
if (errors.length) {
const response = bridge().showErrorMessageBox('There were some errors importing the notes - check the console for more details.\n\nPlease consider sending a bug report to the forum!', {
buttons: [_('Close'), _('Send bug report')],
});
context.dispatch({ type: 'NOTE_DEVTOOLS_SET', value: true });
if (response === 1) {
const url = makeDiscourseDebugUrl(
`Error importing notes from format: ${options.importFormat}`,
`- Input format: ${options.importFormat}\n- Output format: ${options.outputFormat}`,
errors,
packageInfo,
PluginService.instance(),
Setting.value('plugins.states'),
);
void bridge().openExternal(url);
}
}
},
enabledCondition: '',
};
};