mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-21 09:38:01 +02:00
Mobile: Add JEX export (#8428)
This commit is contained in:
parent
ac66332a4e
commit
6ce8865719
@ -422,7 +422,12 @@ packages/app-mobile/components/biometrics/biometricAuthenticate.js
|
||||
packages/app-mobile/components/biometrics/sensorInfo.js
|
||||
packages/app-mobile/components/getResponsiveValue.js
|
||||
packages/app-mobile/components/getResponsiveValue.test.js
|
||||
packages/app-mobile/components/screens/ConfigScreen.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/ConfigScreenButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteExportButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteExportButton.test.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/exportAllFolders.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/configScreenStyles.js
|
||||
packages/app-mobile/components/screens/Note.js
|
||||
packages/app-mobile/components/screens/Notes.js
|
||||
packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js
|
||||
|
7
.gitignore
vendored
7
.gitignore
vendored
@ -407,7 +407,12 @@ packages/app-mobile/components/biometrics/biometricAuthenticate.js
|
||||
packages/app-mobile/components/biometrics/sensorInfo.js
|
||||
packages/app-mobile/components/getResponsiveValue.js
|
||||
packages/app-mobile/components/getResponsiveValue.test.js
|
||||
packages/app-mobile/components/screens/ConfigScreen.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/ConfigScreenButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteExportButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteExportButton.test.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/exportAllFolders.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/configScreenStyles.js
|
||||
packages/app-mobile/components/screens/Note.js
|
||||
packages/app-mobile/components/screens/Notes.js
|
||||
packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js
|
||||
|
@ -1,29 +1,32 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-member-accessibility */
|
||||
import Slider from '@react-native-community/slider';
|
||||
const React = require('react');
|
||||
import { Platform, Linking, View, Switch, StyleSheet, ScrollView, Text, Button, TouchableOpacity, TextInput, Alert, PermissionsAndroid, TouchableNativeFeedback } from 'react-native';
|
||||
import { Platform, Linking, View, Switch, ScrollView, Text, Button, TouchableOpacity, TextInput, Alert, PermissionsAndroid, TouchableNativeFeedback } from 'react-native';
|
||||
import Setting, { AppType } from '@joplin/lib/models/Setting';
|
||||
import NavService from '@joplin/lib/services/NavService';
|
||||
import ReportService from '@joplin/lib/services/ReportService';
|
||||
import SearchEngine from '@joplin/lib/services/searchengine/SearchEngine';
|
||||
import checkPermissions from '../../utils/checkPermissions';
|
||||
import checkPermissions from '../../../utils/checkPermissions';
|
||||
import time from '@joplin/lib/time';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import setIgnoreTlsErrors from '../../utils/TlsUtils';
|
||||
import setIgnoreTlsErrors from '../../../utils/TlsUtils';
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
import { State } from '@joplin/lib/reducer';
|
||||
const { BackButtonService } = require('../../services/back-button.js');
|
||||
const { BackButtonService } = require('../../../services/back-button.js');
|
||||
const VersionInfo = require('react-native-version-info').default;
|
||||
const { connect } = require('react-redux');
|
||||
import ScreenHeader from '../ScreenHeader';
|
||||
import ScreenHeader from '../../ScreenHeader';
|
||||
const { _ } = require('@joplin/lib/locale');
|
||||
const { BaseScreenComponent } = require('../base-screen.js');
|
||||
const { Dropdown } = require('../Dropdown.js');
|
||||
const { themeStyle } = require('../global-style.js');
|
||||
const { BaseScreenComponent } = require('../../base-screen.js');
|
||||
const { Dropdown } = require('../../Dropdown');
|
||||
const { themeStyle } = require('../../global-style.js');
|
||||
const shared = require('@joplin/lib/components/shared/config-shared.js');
|
||||
import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry';
|
||||
import { openDocumentTree } from '@joplin/react-native-saf-x';
|
||||
import biometricAuthenticate from '../biometrics/biometricAuthenticate';
|
||||
import biometricAuthenticate from '../../biometrics/biometricAuthenticate';
|
||||
import configScreenStyles from './configScreenStyles';
|
||||
import NoteExportButton from './NoteExportSection/NoteExportButton';
|
||||
import ConfigScreenButton from './ConfigScreenButton';
|
||||
|
||||
class ConfigScreenComponent extends BaseScreenComponent {
|
||||
public static navigationOptions(): any {
|
||||
@ -223,94 +226,11 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
||||
|
||||
public styles() {
|
||||
const themeId = this.props.themeId;
|
||||
const theme = themeStyle(themeId);
|
||||
|
||||
if (this.styles_[themeId]) return this.styles_[themeId];
|
||||
this.styles_ = {};
|
||||
|
||||
const styles: any = {
|
||||
body: {
|
||||
flex: 1,
|
||||
justifyContent: 'flex-start',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
settingContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: theme.dividerColor,
|
||||
paddingTop: theme.marginTop,
|
||||
paddingBottom: theme.marginBottom,
|
||||
paddingLeft: theme.marginLeft,
|
||||
paddingRight: theme.marginRight,
|
||||
},
|
||||
settingText: {
|
||||
color: theme.color,
|
||||
fontSize: theme.fontSize,
|
||||
flex: 1,
|
||||
paddingRight: 5,
|
||||
},
|
||||
descriptionText: {
|
||||
color: theme.colorFaded,
|
||||
fontSize: theme.fontSizeSmaller,
|
||||
flex: 1,
|
||||
},
|
||||
sliderUnits: {
|
||||
color: theme.color,
|
||||
fontSize: theme.fontSize,
|
||||
marginRight: 10,
|
||||
},
|
||||
settingDescriptionText: {
|
||||
color: theme.colorFaded,
|
||||
fontSize: theme.fontSizeSmaller,
|
||||
flex: 1,
|
||||
paddingLeft: theme.marginLeft,
|
||||
paddingRight: theme.marginRight,
|
||||
paddingBottom: theme.marginBottom,
|
||||
},
|
||||
permissionText: {
|
||||
color: theme.color,
|
||||
fontSize: theme.fontSize,
|
||||
flex: 1,
|
||||
marginTop: 10,
|
||||
},
|
||||
settingControl: {
|
||||
color: theme.color,
|
||||
flex: 1,
|
||||
},
|
||||
textInput: {
|
||||
color: theme.color,
|
||||
},
|
||||
};
|
||||
|
||||
styles.settingContainerNoBottomBorder = { ...styles.settingContainer, borderBottomWidth: 0,
|
||||
paddingBottom: theme.marginBottom / 2 };
|
||||
|
||||
styles.settingControl.borderBottomWidth = 1;
|
||||
styles.settingControl.borderBottomColor = theme.dividerColor;
|
||||
|
||||
styles.switchSettingText = { ...styles.settingText };
|
||||
styles.switchSettingText.width = '80%';
|
||||
|
||||
styles.switchSettingContainer = { ...styles.settingContainer };
|
||||
styles.switchSettingContainer.flexDirection = 'row';
|
||||
styles.switchSettingContainer.justifyContent = 'space-between';
|
||||
|
||||
styles.linkText = { ...styles.settingText };
|
||||
styles.linkText.borderBottomWidth = 1;
|
||||
styles.linkText.borderBottomColor = theme.color;
|
||||
styles.linkText.flex = 0;
|
||||
styles.linkText.fontWeight = 'normal';
|
||||
|
||||
styles.headerWrapperStyle = { ...styles.settingContainer, ...theme.headerWrapperStyle };
|
||||
|
||||
styles.switchSettingControl = { ...styles.settingControl };
|
||||
delete styles.switchSettingControl.color;
|
||||
// styles.switchSettingControl.width = '20%';
|
||||
styles.switchSettingControl.flex = 0;
|
||||
|
||||
this.styles_[themeId] = StyleSheet.create(styles);
|
||||
this.styles_[themeId] = configScreenStyles(themeId);
|
||||
return this.styles_[themeId];
|
||||
}
|
||||
|
||||
@ -388,28 +308,16 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
||||
);
|
||||
}
|
||||
|
||||
renderButton(key: string, title: string, clickHandler: ()=> void, options: any = null) {
|
||||
if (!options) options = {};
|
||||
|
||||
let descriptionComp = null;
|
||||
if (options.description) {
|
||||
descriptionComp = (
|
||||
<View style={{ flex: 1, marginTop: 10 }}>
|
||||
<Text style={this.styles().descriptionText}>{options.description}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
private renderButton(key: string, title: string, clickHandler: ()=> void, options: any = null) {
|
||||
return (
|
||||
<View key={key} style={this.styles().settingContainer}>
|
||||
<View style={{ flex: 1, flexDirection: 'column' }}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Button title={title} onPress={clickHandler} disabled={!!options.disabled} />
|
||||
</View>
|
||||
{options.statusComp}
|
||||
{descriptionComp}
|
||||
</View>
|
||||
</View>
|
||||
<ConfigScreenButton
|
||||
key={key}
|
||||
title={title}
|
||||
clickHandler={clickHandler}
|
||||
description={options?.description}
|
||||
statusComponent={options?.statusComp}
|
||||
styles={this.styles()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -642,12 +550,13 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
||||
settingComps.push(this.renderButton('profiles_buttons', _('Manage profiles'), this.manageProfilesButtonPress_));
|
||||
settingComps.push(this.renderButton('status_button', _('Sync Status'), this.syncStatusButtonPress_));
|
||||
settingComps.push(this.renderButton('log_button', _('Log'), this.logButtonPress_));
|
||||
if (Platform.OS === 'android') {
|
||||
settingComps.push(this.renderButton('export_report_button', this.state.creatingReport ? _('Creating report...') : _('Export Debug Report'), this.exportDebugButtonPress_, { disabled: this.state.creatingReport }));
|
||||
}
|
||||
settingComps.push(this.renderButton('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.') }));
|
||||
|
||||
settingComps.push(this.renderHeader('export', _('Export')));
|
||||
settingComps.push(<NoteExportButton key={'export_as_jex_button'} styles={this.styles()} />);
|
||||
|
||||
if (shim.mobilePlatform() === 'android') {
|
||||
settingComps.push(this.renderButton('export_report_button', this.state.creatingReport ? _('Creating report...') : _('Export Debug Report'), this.exportDebugButtonPress_, { disabled: this.state.creatingReport }));
|
||||
settingComps.push(this.renderButton('export_data', this.state.profileExportStatus === 'exporting' ? _('Exporting profile...') : _('Export profile'), this.exportProfileButtonPress_, { disabled: this.state.profileExportStatus === 'exporting', description: _('For debugging purpose only: export your profile to an external SD card.') }));
|
||||
|
||||
if (this.state.profileExportStatus === 'prompt') {
|
@ -0,0 +1,38 @@
|
||||
|
||||
import * as React from 'react';
|
||||
import { FunctionComponent, ReactNode } from 'react';
|
||||
import { View, Text, Button } from 'react-native';
|
||||
import { ConfigScreenStyles } from './configScreenStyles';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
description: string;
|
||||
clickHandler: ()=> void;
|
||||
styles: ConfigScreenStyles;
|
||||
disabled?: boolean;
|
||||
statusComponent?: ReactNode;
|
||||
}
|
||||
|
||||
const ConfigScreenButton: FunctionComponent<Props> = props => {
|
||||
let descriptionComp = null;
|
||||
if (props.description) {
|
||||
descriptionComp = (
|
||||
<View style={{ flex: 1, marginTop: 10 }}>
|
||||
<Text style={props.styles.descriptionText}>{props.description}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={props.styles.settingContainer}>
|
||||
<View style={{ flex: 1, flexDirection: 'column' }}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Button title={props.title} onPress={props.clickHandler} disabled={!!props.disabled} />
|
||||
</View>
|
||||
{props.statusComponent}
|
||||
{descriptionComp}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
export default ConfigScreenButton;
|
@ -0,0 +1,57 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
import * as React from 'react';
|
||||
import { setImmediate } from 'timers';
|
||||
|
||||
// Required by some libraries (setImmediate is not supported in most browsers,
|
||||
// so is removed by jsdom).
|
||||
window.setImmediate = setImmediate;
|
||||
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { act, fireEvent, render, waitFor } from '@testing-library/react-native';
|
||||
import { expect, describe, beforeEach, test, jest } from '@jest/globals';
|
||||
import '@testing-library/jest-native/extend-expect';
|
||||
import { createNTestNotes, setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
import configScreenStyles from '../configScreenStyles';
|
||||
import { type ShareOptions } from 'react-native-share';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import NoteExportButton from './NoteExportButton';
|
||||
|
||||
jest.mock('react-native-share', () => {
|
||||
const Share = {
|
||||
open: (_options: ShareOptions) => jest.fn(),
|
||||
};
|
||||
return { default: Share };
|
||||
});
|
||||
|
||||
describe('NoteExportButton', () => {
|
||||
beforeEach(async () => {
|
||||
await setupDatabaseAndSynchronizer(1);
|
||||
await switchClient(1);
|
||||
|
||||
const folder1 = await Folder.save({ title: 'folder1' });
|
||||
await createNTestNotes(10, folder1);
|
||||
|
||||
const folder2 = await Folder.save({ title: 'Folder 2 🙂' });
|
||||
await createNTestNotes(10, folder2);
|
||||
});
|
||||
|
||||
test('should show "Exported successfully!" after clicking "Export"', async () => {
|
||||
const styles = configScreenStyles(Setting.THEME_DARK);
|
||||
const view = render(<NoteExportButton
|
||||
styles={styles}
|
||||
/>);
|
||||
|
||||
const exportButton = view.getByText(_('Export all notes as JEX'));
|
||||
await act(() => fireEvent.press(exportButton));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(view.queryByText(_('Exported successfully!'))).not.toBeNull()
|
||||
);
|
||||
|
||||
// With the default folder setup, there should be no warnings
|
||||
expect(view.queryByText(/Warnings/g)).toBeNull();
|
||||
});
|
||||
});
|
@ -0,0 +1,114 @@
|
||||
import * as React from 'react';
|
||||
import { Text, Alert, View } from 'react-native';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import Logger from '@joplin/lib/Logger';
|
||||
import { ProgressBar } from 'react-native-paper';
|
||||
import { FunctionComponent, useCallback, useState } from 'react';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { join } from 'path';
|
||||
import Share from 'react-native-share';
|
||||
import exportAllFolders, { makeExportCacheDirectory } from './exportAllFolders';
|
||||
import { ExportProgressState } from '@joplin/lib/services/interop/types';
|
||||
import { ConfigScreenStyles } from '../configScreenStyles';
|
||||
import ConfigScreenButton from '../ConfigScreenButton';
|
||||
|
||||
const logger = Logger.create('NoteExportButton');
|
||||
|
||||
interface Props {
|
||||
styles: ConfigScreenStyles;
|
||||
}
|
||||
|
||||
enum ExportStatus {
|
||||
NotStarted,
|
||||
Exporting,
|
||||
Exported,
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
setExportStatus(ExportStatus.Exported);
|
||||
setWarnings(status.warnings.join('\n'));
|
||||
|
||||
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]);
|
||||
|
||||
if (exportStatus === ExportStatus.NotStarted || exportStatus === ExportStatus.Exporting) {
|
||||
const progressComponent = (
|
||||
<ProgressBar
|
||||
visible={exportStatus === ExportStatus.Exporting}
|
||||
indeterminate={exportProgress === undefined}
|
||||
progress={exportProgress}/>
|
||||
);
|
||||
const descriptionText = _('Share a copy of all notes in a file format that can be imported by Joplin on a computer.');
|
||||
|
||||
const startOrCancelExportButton = (
|
||||
<ConfigScreenButton
|
||||
title={exportStatus === ExportStatus.Exporting ? _('Exporting...') : _('Export all notes as JEX')}
|
||||
disabled={exportStatus === ExportStatus.Exporting}
|
||||
description={exportStatus === ExportStatus.NotStarted ? descriptionText : null}
|
||||
statusComponent={progressComponent}
|
||||
clickHandler={startExport}
|
||||
styles={props.styles}
|
||||
/>
|
||||
);
|
||||
|
||||
return startOrCancelExportButton;
|
||||
} else {
|
||||
const warningComponent = (
|
||||
<Text style={props.styles.warningText}>
|
||||
{_('Warnings:\n%s', warnings)}
|
||||
</Text>
|
||||
);
|
||||
|
||||
const exportSummary = (
|
||||
<View style={props.styles.settingContainer}>
|
||||
<Text style={props.styles.descriptionText}>{_('Exported successfully!')}</Text>
|
||||
{warnings.length > 0 ? warningComponent : null}
|
||||
</View>
|
||||
);
|
||||
return exportSummary;
|
||||
}
|
||||
};
|
||||
|
||||
export default NoteExportButton;
|
@ -0,0 +1,29 @@
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
import InteropService from '@joplin/lib/services/interop/InteropService';
|
||||
import { 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();
|
||||
|
||||
const sourceFolderIds = folders.map(folder => folder.id);
|
||||
const exportOptions: ExportOptions = {
|
||||
sourceFolderIds,
|
||||
path,
|
||||
format: 'jex',
|
||||
target: FileSystemItem.File,
|
||||
onProgress,
|
||||
};
|
||||
|
||||
return await InteropService.instance().export(exportOptions);
|
||||
};
|
||||
|
||||
export default exportFolders;
|
@ -0,0 +1,137 @@
|
||||
import { TextStyle, ViewStyle, StyleSheet } from 'react-native';
|
||||
const { themeStyle } = require('../../global-style.js');
|
||||
|
||||
export interface ConfigScreenStyles {
|
||||
body: ViewStyle;
|
||||
|
||||
settingContainer: ViewStyle;
|
||||
settingContainerNoBottomBorder: ViewStyle;
|
||||
headerWrapperStyle: ViewStyle;
|
||||
|
||||
settingText: TextStyle;
|
||||
linkText: TextStyle;
|
||||
descriptionText: TextStyle;
|
||||
warningText: TextStyle;
|
||||
|
||||
sliderUnits: TextStyle;
|
||||
settingDescriptionText: TextStyle;
|
||||
permissionText: TextStyle;
|
||||
textInput: TextStyle;
|
||||
|
||||
switchSettingText: TextStyle;
|
||||
switchSettingContainer: ViewStyle;
|
||||
switchSettingControl: TextStyle;
|
||||
|
||||
settingControl: TextStyle;
|
||||
}
|
||||
|
||||
const configScreenStyles = (themeId: number): ConfigScreenStyles => {
|
||||
const theme = themeStyle(themeId);
|
||||
|
||||
const settingContainerStyle: ViewStyle = {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: theme.dividerColor,
|
||||
paddingTop: theme.marginTop,
|
||||
paddingBottom: theme.marginBottom,
|
||||
paddingLeft: theme.marginLeft,
|
||||
paddingRight: theme.marginRight,
|
||||
};
|
||||
|
||||
const settingTextStyle: TextStyle = {
|
||||
color: theme.color,
|
||||
fontSize: theme.fontSize,
|
||||
flex: 1,
|
||||
paddingRight: 5,
|
||||
};
|
||||
|
||||
const settingControlStyle: TextStyle = {
|
||||
color: theme.color,
|
||||
flex: 1,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: theme.dividerColor,
|
||||
};
|
||||
|
||||
const styles: ConfigScreenStyles = {
|
||||
body: {
|
||||
flex: 1,
|
||||
justifyContent: 'flex-start',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
settingContainer: settingContainerStyle,
|
||||
settingContainerNoBottomBorder: {
|
||||
...settingContainerStyle,
|
||||
borderBottomWidth: 0,
|
||||
paddingBottom: theme.marginBottom / 2,
|
||||
},
|
||||
settingText: settingTextStyle,
|
||||
descriptionText: {
|
||||
color: theme.colorFaded,
|
||||
fontSize: theme.fontSizeSmaller,
|
||||
flex: 1,
|
||||
},
|
||||
linkText: {
|
||||
...settingTextStyle,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: theme.color,
|
||||
flex: 0,
|
||||
fontWeight: 'normal',
|
||||
},
|
||||
warningText: {
|
||||
color: theme.color,
|
||||
backgroundColor: theme.warningBackgroundColor,
|
||||
fontSize: theme.fontSizeSmaller,
|
||||
},
|
||||
|
||||
sliderUnits: {
|
||||
color: theme.color,
|
||||
fontSize: theme.fontSize,
|
||||
marginRight: 10,
|
||||
},
|
||||
settingDescriptionText: {
|
||||
color: theme.colorFaded,
|
||||
fontSize: theme.fontSizeSmaller,
|
||||
flex: 1,
|
||||
paddingLeft: theme.marginLeft,
|
||||
paddingRight: theme.marginRight,
|
||||
paddingBottom: theme.marginBottom,
|
||||
},
|
||||
permissionText: {
|
||||
color: theme.color,
|
||||
fontSize: theme.fontSize,
|
||||
flex: 1,
|
||||
marginTop: 10,
|
||||
},
|
||||
settingControl: settingControlStyle,
|
||||
textInput: {
|
||||
color: theme.color,
|
||||
},
|
||||
|
||||
switchSettingText: {
|
||||
...settingTextStyle,
|
||||
width: '80%',
|
||||
},
|
||||
switchSettingContainer: {
|
||||
...settingContainerStyle,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
|
||||
headerWrapperStyle: {
|
||||
...settingContainerStyle,
|
||||
...theme.headerWrapperStyle,
|
||||
},
|
||||
|
||||
switchSettingControl: {
|
||||
...settingControlStyle,
|
||||
color: undefined,
|
||||
flex: 0,
|
||||
},
|
||||
};
|
||||
|
||||
return StyleSheet.create(styles);
|
||||
};
|
||||
|
||||
export default configScreenStyles;
|
@ -5,6 +5,7 @@ module.exports = {
|
||||
'ts',
|
||||
'tsx',
|
||||
'js',
|
||||
'jsx',
|
||||
],
|
||||
|
||||
'transform': {
|
||||
@ -14,6 +15,11 @@ module.exports = {
|
||||
testMatch: ['**/*.test.(ts|tsx)'],
|
||||
|
||||
testPathIgnorePatterns: ['<rootDir>/node_modules/'],
|
||||
setupFilesAfterEnv: ['./jest.setup.js'],
|
||||
|
||||
// Do transform most packages in node_modules (transformations correct unrecognized
|
||||
// import syntax)
|
||||
transformIgnorePatterns: ['<rootDir>/node_modules/jest'],
|
||||
|
||||
slowTestThreshold: 40,
|
||||
};
|
||||
|
35
packages/app-mobile/jest.setup.js
Normal file
35
packages/app-mobile/jest.setup.js
Normal file
@ -0,0 +1,35 @@
|
||||
/* eslint-disable jest/require-top-level-describe */
|
||||
|
||||
const { afterEachCleanUp, afterAllCleanUp } = require('@joplin/lib/testing/test-utils.js');
|
||||
const { shimInit } = require('@joplin/lib/shim-init-node.js');
|
||||
const { mkdir, rm } = require('fs-extra');
|
||||
const path = require('path');
|
||||
const { tmpdir } = require('os');
|
||||
const uuid = require('@joplin/lib/uuid').default;
|
||||
const sqlite3 = require('sqlite3');
|
||||
|
||||
shimInit({ nodeSqlite: sqlite3 });
|
||||
|
||||
|
||||
// react-native-fs's CachesDirectoryPath export doesn't work in a testing environment.
|
||||
// Use a temporary folder instead.
|
||||
const tempDirectoryPath = path.join(tmpdir(), `appmobile-test-${uuid.createNano()}`);
|
||||
|
||||
jest.doMock('react-native-fs', () => {
|
||||
return {
|
||||
CachesDirectoryPath: tempDirectoryPath,
|
||||
};
|
||||
});
|
||||
|
||||
beforeAll(async () => {
|
||||
await mkdir(tempDirectoryPath);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await afterEachCleanUp();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await afterAllCleanUp();
|
||||
await rm(tempDirectoryPath, { recursive: true });
|
||||
});
|
@ -20,8 +20,23 @@ const localPackages = {
|
||||
'@joplin/fork-uslug': path.resolve(__dirname, '../fork-uslug/'),
|
||||
'@joplin/react-native-saf-x': path.resolve(__dirname, '../react-native-saf-x/'),
|
||||
'@joplin/react-native-alarm-notification': path.resolve(__dirname, '../react-native-alarm-notification/'),
|
||||
|
||||
'@joplin/fork-sax': path.resolve(__dirname, '../fork-sax/'),
|
||||
};
|
||||
|
||||
const remappedPackages = {
|
||||
...localPackages,
|
||||
};
|
||||
|
||||
// Some packages aren't available in react-native and thus must be replaced by browserified
|
||||
// versions. For example, this allows us to `import {resolve} from 'path'` rather than
|
||||
// `const { resolve } = require('path-browserify')` ('path-browerify' doesn't have its own type
|
||||
// definitions).
|
||||
const browserifiedPackages = ['path'];
|
||||
for (const package of browserifiedPackages) {
|
||||
remappedPackages[package] = path.resolve(__dirname, `./node_modules/${package}-browserify/`);
|
||||
}
|
||||
|
||||
const watchedFolders = [];
|
||||
for (const [, v] of Object.entries(localPackages)) {
|
||||
watchedFolders.push(v);
|
||||
@ -49,7 +64,7 @@ module.exports = {
|
||||
// included in your reusable module as they would be imported when
|
||||
// the module is actually used.
|
||||
//
|
||||
localPackages,
|
||||
remappedPackages,
|
||||
{
|
||||
get: (target, name) => {
|
||||
if (target.hasOwnProperty(name)) {
|
||||
|
@ -37,6 +37,7 @@
|
||||
"jsc-android": "241213.1.0",
|
||||
"lodash": "4.17.21",
|
||||
"md5": "2.3.0",
|
||||
"path-browserify": "1.0.1",
|
||||
"prop-types": "15.8.1",
|
||||
"punycode": "2.3.0",
|
||||
"react": "18.2.0",
|
||||
@ -79,6 +80,7 @@
|
||||
"stream": "0.0.2",
|
||||
"stream-browserify": "3.0.0",
|
||||
"string-natural-compare": "3.0.1",
|
||||
"tar-stream": "3.1.6",
|
||||
"timers": "0.1.1",
|
||||
"url": "0.11.1"
|
||||
},
|
||||
@ -101,12 +103,15 @@
|
||||
"@codemirror/view": "6.9.3",
|
||||
"@joplin/tools": "~2.12",
|
||||
"@lezer/highlight": "1.1.4",
|
||||
"@testing-library/jest-native": "5.4.2",
|
||||
"@testing-library/react-native": "12.1.2",
|
||||
"@tsconfig/react-native": "2.0.2",
|
||||
"@types/fs-extra": "11.0.1",
|
||||
"@types/jest": "29.5.1",
|
||||
"@types/react": "18.0.24",
|
||||
"@types/react-native": "0.70.6",
|
||||
"@types/react-redux": "7.1.25",
|
||||
"@types/tar-stream": "2.2.2",
|
||||
"babel-jest": "29.2.1",
|
||||
"babel-plugin-module-resolver": "4.1.0",
|
||||
"execa": "4.1.0",
|
||||
@ -119,6 +124,8 @@
|
||||
"md5-file": "5.0.0",
|
||||
"metro-react-native-babel-preset": "0.73.9",
|
||||
"nodemon": "2.0.22",
|
||||
"react-test-renderer": "18.2.0",
|
||||
"sqlite3": "5.1.6",
|
||||
"ts-jest": "29.1.0",
|
||||
"ts-loader": "9.4.4",
|
||||
"ts-node": "10.9.1",
|
||||
|
@ -58,7 +58,7 @@ import JoplinDatabase from '@joplin/lib/JoplinDatabase';
|
||||
import Database from '@joplin/lib/database';
|
||||
import NotesScreen from './components/screens/Notes';
|
||||
const { TagsScreen } = require('./components/screens/tags.js');
|
||||
import ConfigScreen from './components/screens/ConfigScreen';
|
||||
import ConfigScreen from './components/screens/ConfigScreen/ConfigScreen';
|
||||
const { FolderScreen } = require('./components/screens/folder.js');
|
||||
const { LogScreen } = require('./components/screens/log.js');
|
||||
const { StatusScreen } = require('./components/screens/status.js');
|
||||
@ -423,6 +423,20 @@ function decryptionWorker_resourceMetadataButNotBlobDecrypted() {
|
||||
ResourceFetcher.instance().scheduleAutoAddResources();
|
||||
}
|
||||
|
||||
const initializeTempDir = async () => {
|
||||
const tempDir = `${getProfilesRootDir()}/tmp`;
|
||||
|
||||
// Re-create the temporary directory.
|
||||
try {
|
||||
await shim.fsDriver().remove(tempDir);
|
||||
} catch (_error) {
|
||||
// The logger may not exist yet. Do nothing.
|
||||
}
|
||||
|
||||
await shim.fsDriver().mkdir(tempDir);
|
||||
return tempDir;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
async function initialize(dispatch: Function) {
|
||||
shimInit();
|
||||
@ -439,6 +453,7 @@ async function initialize(dispatch: Function) {
|
||||
Setting.setConstant('env', __DEV__ ? 'dev' : 'prod');
|
||||
Setting.setConstant('appId', 'net.cozic.joplin-mobile');
|
||||
Setting.setConstant('appType', 'mobile');
|
||||
Setting.setConstant('tempDir', await initializeTempDir());
|
||||
const resourceDir = getResourceDir(currentProfile, isSubProfile);
|
||||
Setting.setConstant('resourceDir', resourceDir);
|
||||
|
||||
|
@ -1,10 +1,16 @@
|
||||
import FsDriverBase, { ReadDirStatsOptions } from '@joplin/lib/fs-driver-base';
|
||||
const RNFetchBlob = require('rn-fetch-blob').default;
|
||||
const RNFS = require('react-native-fs');
|
||||
import * as RNFS from 'react-native-fs';
|
||||
const DocumentPicker = require('react-native-document-picker').default;
|
||||
import { openDocument } from '@joplin/react-native-saf-x';
|
||||
import RNSAF, { Encoding, DocumentFileDetail, openDocumentTree } from '@joplin/react-native-saf-x';
|
||||
import { Platform } from 'react-native';
|
||||
import * as tar from 'tar-stream';
|
||||
import { resolve } from 'path';
|
||||
import { Buffer } from 'buffer';
|
||||
import Logger from '@joplin/lib/Logger';
|
||||
|
||||
const logger = Logger.create('fs-driver-rn');
|
||||
|
||||
const ANDROID_URI_PREFIX = 'content://';
|
||||
|
||||
@ -61,17 +67,13 @@ export default class FsDriverRN extends FsDriverBase {
|
||||
};
|
||||
}
|
||||
|
||||
public async isDirectory(path: string): Promise<boolean> {
|
||||
return (await this.stat(path)).isDirectory();
|
||||
}
|
||||
|
||||
public async readDirStats(path: string, options: any = null) {
|
||||
if (!options) options = {};
|
||||
if (!('recursive' in options)) options.recursive = false;
|
||||
|
||||
const isScoped = isScopedUri(path);
|
||||
|
||||
let stats = [];
|
||||
let stats: any[] = [];
|
||||
try {
|
||||
if (isScoped) {
|
||||
stats = await RNSAF.listFiles(path);
|
||||
@ -86,12 +88,15 @@ export default class FsDriverRN extends FsDriverBase {
|
||||
for (let i = 0; i < stats.length; i++) {
|
||||
const stat = stats[i];
|
||||
const relativePath = (isScoped ? stat.uri : stat.path).substr(path.length + 1);
|
||||
output.push(this.rnfsStatToStd_(stat, relativePath));
|
||||
const standardStat = this.rnfsStatToStd_(stat, relativePath);
|
||||
output.push(standardStat);
|
||||
|
||||
if (isScoped) {
|
||||
// readUriDirStatsHandleRecursion_ expects stat to have a URI property.
|
||||
// Use the original stat.
|
||||
output = await this.readUriDirStatsHandleRecursion_(stat, output, options);
|
||||
} else {
|
||||
output = await this.readDirStatsHandleRecursion_(path, stat, output, options);
|
||||
output = await this.readDirStatsHandleRecursion_(path, standardStat, output, options);
|
||||
}
|
||||
}
|
||||
return output;
|
||||
@ -135,6 +140,8 @@ export default class FsDriverRN extends FsDriverBase {
|
||||
await RNSAF.mkdir(path);
|
||||
return;
|
||||
}
|
||||
|
||||
// Also creates parent directories: Works like mkdir -p
|
||||
return RNFS.mkdir(path);
|
||||
}
|
||||
|
||||
@ -148,8 +155,9 @@ export default class FsDriverRN extends FsDriverBase {
|
||||
}
|
||||
return this.rnfsStatToStd_(r, path);
|
||||
} catch (error) {
|
||||
if (error && ((error.message && error.message.indexOf('exist') >= 0) || error.code === 'ENOENT')) {
|
||||
if (error && (error.code === 'ENOENT' || !(await this.exists(path)))) {
|
||||
// Probably { [Error: File does not exist] framesToPop: 1, code: 'EUNSPECIFIED' }
|
||||
// or { [Error: The file {file} couldn’t be opened because there is no such file.], code: 'ENSCOCOAERRORDOMAIN260' }
|
||||
// which unfortunately does not have a proper error code. Can be ignored.
|
||||
return null;
|
||||
} else {
|
||||
@ -260,6 +268,53 @@ export default class FsDriverRN extends FsDriverBase {
|
||||
throw new Error(`Not implemented: md5File(): ${path}`);
|
||||
}
|
||||
|
||||
public async tarExtract(_options: any) {
|
||||
throw new Error('Not implemented: tarExtract');
|
||||
}
|
||||
|
||||
public async tarCreate(options: any, filePaths: string[]) {
|
||||
// Choose a default cwd if not given
|
||||
const cwd = options.cwd ?? RNFS.DocumentDirectoryPath;
|
||||
const file = resolve(cwd, options.file);
|
||||
|
||||
if (await this.exists(file)) {
|
||||
throw new Error('Error! Destination already exists');
|
||||
}
|
||||
|
||||
const pack = tar.pack();
|
||||
|
||||
for (const path of filePaths) {
|
||||
const absPath = resolve(cwd, path);
|
||||
const stat = await this.stat(absPath);
|
||||
const sizeBytes: number = stat.size;
|
||||
|
||||
const entry = pack.entry({ name: path, size: sizeBytes }, (error) => {
|
||||
if (error) {
|
||||
logger.error(`Tar error: ${error}`);
|
||||
}
|
||||
});
|
||||
|
||||
const chunkSize = 1024 * 100; // 100 KiB
|
||||
for (let offset = 0; offset < sizeBytes; offset += chunkSize) {
|
||||
// The RNFS documentation suggests using base64 for binary files.
|
||||
const part = await RNFS.read(absPath, chunkSize, offset, 'base64');
|
||||
entry.write(Buffer.from(part, 'base64'));
|
||||
}
|
||||
entry.end();
|
||||
}
|
||||
|
||||
pack.finalize();
|
||||
|
||||
// The streams used by tar-stream seem not to support a chunk size
|
||||
// (it seems despite the typings provided).
|
||||
let data: number[]|null = null;
|
||||
while ((data = pack.read()) !== null) {
|
||||
const buff = Buffer.from(data);
|
||||
const base64Data = buff.toString('base64');
|
||||
await this.appendFile(file, base64Data, 'base64');
|
||||
}
|
||||
}
|
||||
|
||||
public async getExternalDirectoryPath(): Promise<string | undefined> {
|
||||
let directory;
|
||||
if (this.isUsingAndroidSAF()) {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ModuleType, FileSystemItem, ImportModuleOutputFormat, ImportOptions, ExportOptions, ImportExportResult } from './types';
|
||||
import { ModuleType, FileSystemItem, ImportModuleOutputFormat, ImportOptions, ExportOptions, ImportExportResult, ExportProgressState } from './types';
|
||||
import shim from '../../shim';
|
||||
import { _ } from '../../locale';
|
||||
import BaseItem from '../../models/BaseItem';
|
||||
@ -12,16 +12,13 @@ import InteropService_Importer_Jex from './InteropService_Importer_Jex';
|
||||
import InteropService_Importer_Md from './InteropService_Importer_Md';
|
||||
import InteropService_Importer_Md_frontmatter from './InteropService_Importer_Md_frontmatter';
|
||||
import InteropService_Importer_Raw from './InteropService_Importer_Raw';
|
||||
import InteropService_Importer_EnexToMd from './InteropService_Importer_EnexToMd';
|
||||
import InteropService_Importer_EnexToHtml from './InteropService_Importer_EnexToHtml';
|
||||
import InteropService_Exporter_Jex from './InteropService_Exporter_Jex';
|
||||
import InteropService_Exporter_Raw from './InteropService_Exporter_Raw';
|
||||
import InteropService_Exporter_Md from './InteropService_Exporter_Md';
|
||||
import InteropService_Exporter_Md_frontmatter from './InteropService_Exporter_Md_frontmatter';
|
||||
import InteropService_Exporter_Html from './InteropService_Exporter_Html';
|
||||
import InteropService_Importer_Base from './InteropService_Importer_Base';
|
||||
import InteropService_Exporter_Base from './InteropService_Exporter_Base';
|
||||
import Module, { makeExportModule, makeImportModule } from './Module';
|
||||
import Module, { dynamicRequireModuleFactory, makeExportModule, makeImportModule } from './Module';
|
||||
const { sprintf } = require('sprintf-js');
|
||||
const { fileExtension } = require('../../path-utils');
|
||||
const EventEmitter = require('events');
|
||||
@ -89,19 +86,18 @@ export default class InteropService {
|
||||
fileExtensions: ['enex'],
|
||||
sources: [FileSystemItem.File],
|
||||
description: _('Evernote Export File (as Markdown)'),
|
||||
importerClass: 'InteropService_Importer_EnexToMd',
|
||||
supportsMobile: false,
|
||||
isDefault: true,
|
||||
}, () => new InteropService_Importer_EnexToMd()),
|
||||
}, dynamicRequireModuleFactory('./InteropService_Importer_EnexToMd')),
|
||||
|
||||
makeImportModule({
|
||||
format: 'enex',
|
||||
fileExtensions: ['enex'],
|
||||
sources: [FileSystemItem.File],
|
||||
description: _('Evernote Export File (as HTML)'),
|
||||
// TODO: Consider doing this the same way as the multiple `md` importers are handled
|
||||
importerClass: 'InteropService_Importer_EnexToHtml',
|
||||
supportsMobile: false,
|
||||
outputFormat: ImportModuleOutputFormat.Html,
|
||||
}, () => new InteropService_Importer_EnexToHtml()),
|
||||
}, dynamicRequireModuleFactory('./InteropService_Importer_EnexToHtml')),
|
||||
];
|
||||
|
||||
const exportModules = [
|
||||
@ -136,13 +132,15 @@ export default class InteropService {
|
||||
target: FileSystemItem.File,
|
||||
isNoteArchive: false,
|
||||
description: _('HTML File'),
|
||||
}, () => new InteropService_Exporter_Html()),
|
||||
supportsMobile: false,
|
||||
}, dynamicRequireModuleFactory('./InteropService_Exporter_Html')),
|
||||
|
||||
makeExportModule({
|
||||
format: 'html',
|
||||
target: FileSystemItem.Directory,
|
||||
description: _('HTML Directory'),
|
||||
}, () => new InteropService_Exporter_Html()),
|
||||
supportsMobile: false,
|
||||
}, dynamicRequireModuleFactory('./InteropService_Exporter_Html')),
|
||||
];
|
||||
|
||||
this.defaultModules_ = (importModules as Module[]).concat(exportModules);
|
||||
@ -164,8 +162,15 @@ export default class InteropService {
|
||||
private findModuleByFormat_(type: ModuleType, format: string, target: FileSystemItem = null, outputFormat: ImportModuleOutputFormat = null) {
|
||||
const modules = this.modules();
|
||||
const matches = [];
|
||||
|
||||
const isMobile = shim.mobilePlatform() !== '';
|
||||
for (let i = 0; i < modules.length; i++) {
|
||||
const m = modules[i];
|
||||
|
||||
if (!m.supportsMobile && isMobile) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (m.format === format && m.type === type) {
|
||||
if (!target && !outputFormat) {
|
||||
matches.push(m);
|
||||
@ -205,7 +210,7 @@ export default class InteropService {
|
||||
// explicit with which importer we want to use.
|
||||
//
|
||||
// https://github.com/laurent22/joplin/pull/1795#pullrequestreview-281574417
|
||||
private newModuleFromPath_(type: ModuleType, options: any) {
|
||||
private newModuleFromPath_(type: ModuleType, options: ExportOptions&ImportOptions) {
|
||||
const moduleMetadata = this.findModuleByFormat_(type, options.format, options.target);
|
||||
if (!moduleMetadata) throw new Error(_('Cannot load "%s" module for format "%s" and target "%s"', type, options.format, options.target));
|
||||
|
||||
@ -289,7 +294,11 @@ export default class InteropService {
|
||||
const result: ImportExportResult = { warnings: [] };
|
||||
const itemsToExport: any[] = [];
|
||||
|
||||
options.onProgress?.(ExportProgressState.QueuingItems, null);
|
||||
let totalItemsToProcess = 0;
|
||||
|
||||
const queueExportItem = (itemType: number, itemOrId: any) => {
|
||||
totalItemsToProcess ++;
|
||||
itemsToExport.push({
|
||||
type: itemType,
|
||||
itemOrId: itemOrId,
|
||||
@ -373,6 +382,7 @@ export default class InteropService {
|
||||
await exporter.prepareForProcessingItemType(type, itemsToExport);
|
||||
}
|
||||
|
||||
let itemsProcessed = 0;
|
||||
for (let typeOrderIndex = 0; typeOrderIndex < typeOrder.length; typeOrderIndex++) {
|
||||
const type = typeOrder[typeOrderIndex];
|
||||
|
||||
@ -414,9 +424,13 @@ export default class InteropService {
|
||||
console.error(error);
|
||||
result.warnings.push(error.message);
|
||||
}
|
||||
|
||||
itemsProcessed++;
|
||||
options.onProgress?.(ExportProgressState.Exporting, itemsProcessed / totalItemsToProcess);
|
||||
}
|
||||
}
|
||||
|
||||
options.onProgress?.(ExportProgressState.Closing, null);
|
||||
await exporter.close();
|
||||
|
||||
return result;
|
||||
|
@ -1,6 +1,7 @@
|
||||
/* eslint @typescript-eslint/no-unused-vars: 0, no-unused-vars: ["error", { "argsIgnorePattern": ".*" }], */
|
||||
|
||||
import Setting from '../../models/Setting';
|
||||
import shim from '../../shim';
|
||||
|
||||
export default class InteropService_Exporter_Base {
|
||||
private context_: any = {};
|
||||
@ -31,7 +32,7 @@ export default class InteropService_Exporter_Base {
|
||||
protected async temporaryDirectory_(createIt: boolean) {
|
||||
const md5 = require('md5');
|
||||
const tempDir = `${Setting.value('tempDir')}/${md5(Math.random() + Date.now())}`;
|
||||
if (createIt) await require('fs-extra').mkdirp(tempDir);
|
||||
if (createIt) await shim.fsDriver().mkdir(tempDir);
|
||||
return tempDir;
|
||||
}
|
||||
}
|
||||
|
@ -3,8 +3,6 @@ import InteropService_Exporter_Base from './InteropService_Exporter_Base';
|
||||
import InteropService_Exporter_Raw from './InteropService_Exporter_Raw';
|
||||
import shim from '../../shim';
|
||||
|
||||
const fs = require('fs-extra');
|
||||
|
||||
export default class InteropService_Exporter_Jex extends InteropService_Exporter_Base {
|
||||
|
||||
private tempDir_: string;
|
||||
@ -41,6 +39,6 @@ export default class InteropService_Exporter_Jex extends InteropService_Exporter
|
||||
cwd: this.tempDir_,
|
||||
}, filePaths);
|
||||
|
||||
await fs.remove(this.tempDir_);
|
||||
await shim.fsDriver().remove(this.tempDir_);
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
import { ImportExportResult } from './types';
|
||||
|
||||
import Setting from '../../models/Setting';
|
||||
import shim from '../../shim';
|
||||
|
||||
export default class InteropService_Importer_Base {
|
||||
|
||||
@ -28,7 +29,7 @@ export default class InteropService_Importer_Base {
|
||||
protected async temporaryDirectory_(createIt: boolean) {
|
||||
const md5 = require('md5');
|
||||
const tempDir = `${Setting.value('tempDir')}/${md5(Math.random() + Date.now())}`;
|
||||
if (createIt) await require('fs-extra').mkdirp(tempDir);
|
||||
if (createIt) await shim.fsDriver().mkdir(tempDir);
|
||||
return tempDir;
|
||||
}
|
||||
}
|
||||
|
@ -5,8 +5,6 @@ import InteropService_Importer_Raw from './InteropService_Importer_Raw';
|
||||
const { filename } = require('../../path-utils');
|
||||
import shim from '../../shim';
|
||||
|
||||
const fs = require('fs-extra');
|
||||
|
||||
export default class InteropService_Importer_Jex extends InteropService_Importer_Base {
|
||||
public async exec(result: ImportExportResult) {
|
||||
const tempDir = await this.temporaryDirectory_(true);
|
||||
@ -29,7 +27,7 @@ export default class InteropService_Importer_Jex extends InteropService_Importer
|
||||
await importer.init(tempDir, this.options_);
|
||||
result = await importer.exec(result);
|
||||
|
||||
await fs.remove(tempDir);
|
||||
await shim.fsDriver().remove(tempDir);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { _ } from '../../locale';
|
||||
import shim from '../../shim';
|
||||
import InteropService_Exporter_Base from './InteropService_Exporter_Base';
|
||||
import InteropService_Importer_Base from './InteropService_Importer_Base';
|
||||
import { ExportOptions, FileSystemItem, ImportModuleOutputFormat, ImportOptions, ModuleType } from './types';
|
||||
@ -10,6 +11,8 @@ interface BaseMetadata {
|
||||
description: string;
|
||||
isDefault: boolean;
|
||||
|
||||
supportsMobile: boolean;
|
||||
|
||||
// Returns the full label to be displayed in the UI.
|
||||
fullLabel(moduleSource?: FileSystemItem): string;
|
||||
|
||||
@ -24,7 +27,6 @@ interface ImportMetadata extends BaseMetadata {
|
||||
type: ModuleType.Importer;
|
||||
|
||||
sources: FileSystemItem[];
|
||||
importerClass: string;
|
||||
outputFormat: ImportModuleOutputFormat;
|
||||
}
|
||||
|
||||
@ -47,6 +49,7 @@ const defaultBaseMetadata = {
|
||||
fileExtensions: [] as string[],
|
||||
description: '',
|
||||
isNoteArchive: true,
|
||||
supportsMobile: true,
|
||||
isDefault: false,
|
||||
};
|
||||
|
||||
@ -66,7 +69,6 @@ export const makeImportModule = (
|
||||
...defaultBaseMetadata,
|
||||
type: ModuleType.Importer,
|
||||
sources: [],
|
||||
importerClass: '',
|
||||
outputFormat: ImportModuleOutputFormat.Markdown,
|
||||
|
||||
fullLabel: (moduleSource?: FileSystemItem) => {
|
||||
@ -119,5 +121,16 @@ export const makeExportModule = (
|
||||
};
|
||||
};
|
||||
|
||||
// A module factory that uses dynamic requires.
|
||||
// TODO: This is currently only used because some importers/exporters import libraries that
|
||||
// don't work on mobile (e.g. htmlpack or fs). These importers/exporters should be migrated
|
||||
// to fs so that this can be removed.
|
||||
export const dynamicRequireModuleFactory = (fileName: string) => {
|
||||
return () => {
|
||||
const ModuleClass = shim.requireDynamic(fileName).default;
|
||||
return new ModuleClass();
|
||||
};
|
||||
};
|
||||
|
||||
type Module = ImportModule|ExportModule;
|
||||
export default Module;
|
||||
|
@ -35,6 +35,14 @@ export interface ImportOptions {
|
||||
outputFormat?: ImportModuleOutputFormat;
|
||||
}
|
||||
|
||||
export enum ExportProgressState {
|
||||
QueuingItems,
|
||||
Exporting,
|
||||
Closing,
|
||||
}
|
||||
|
||||
export type OnExportProgressCallback = (status: ExportProgressState, progress: number)=> void;
|
||||
|
||||
export interface ExportOptions {
|
||||
format?: string;
|
||||
path?: string;
|
||||
@ -46,6 +54,8 @@ export interface ExportOptions {
|
||||
plugins?: PluginStates;
|
||||
customCss?: string;
|
||||
packIntoSingleFile?: boolean;
|
||||
|
||||
onProgress?: OnExportProgressCallback;
|
||||
}
|
||||
|
||||
export interface ImportExportResult {
|
||||
|
150
yarn.lock
150
yarn.lock
@ -4277,6 +4277,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@jest/schemas@npm:^29.6.0":
|
||||
version: 29.6.0
|
||||
resolution: "@jest/schemas@npm:29.6.0"
|
||||
dependencies:
|
||||
"@sinclair/typebox": ^0.27.8
|
||||
checksum: c00511c69cf89138a7d974404d3a5060af375b5a52b9c87215d91873129b382ca11c1ff25bd6d605951404bb381ddce5f8091004a61e76457da35db1f5c51365
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@jest/source-map@npm:^29.4.3":
|
||||
version: 29.4.3
|
||||
resolution: "@jest/source-map@npm:29.4.3"
|
||||
@ -4535,12 +4544,15 @@ __metadata:
|
||||
"@react-native-community/netinfo": 9.3.10
|
||||
"@react-native-community/push-notification-ios": 1.11.0
|
||||
"@react-native-community/slider": 4.4.2
|
||||
"@testing-library/jest-native": 5.4.2
|
||||
"@testing-library/react-native": 12.1.2
|
||||
"@tsconfig/react-native": 2.0.2
|
||||
"@types/fs-extra": 11.0.1
|
||||
"@types/jest": 29.5.1
|
||||
"@types/react": 18.0.24
|
||||
"@types/react-native": 0.70.6
|
||||
"@types/react-redux": 7.1.25
|
||||
"@types/tar-stream": 2.2.2
|
||||
assert-browserify: 2.0.0
|
||||
babel-jest: 29.2.1
|
||||
babel-plugin-module-resolver: 4.1.0
|
||||
@ -4562,6 +4574,7 @@ __metadata:
|
||||
md5-file: 5.0.0
|
||||
metro-react-native-babel-preset: 0.73.9
|
||||
nodemon: 2.0.22
|
||||
path-browserify: 1.0.1
|
||||
prop-types: 15.8.1
|
||||
punycode: 2.3.0
|
||||
react: 18.2.0
|
||||
@ -4599,11 +4612,14 @@ __metadata:
|
||||
react-native-webview: 12.4.0
|
||||
react-native-zip-archive: 6.0.9
|
||||
react-redux: 8.0.7
|
||||
react-test-renderer: 18.2.0
|
||||
redux: 4.2.1
|
||||
rn-fetch-blob: 0.12.0
|
||||
sqlite3: 5.1.6
|
||||
stream: 0.0.2
|
||||
stream-browserify: 3.0.0
|
||||
string-natural-compare: 3.0.1
|
||||
tar-stream: 3.1.6
|
||||
timers: 0.1.1
|
||||
ts-jest: 29.1.0
|
||||
ts-loader: 9.4.4
|
||||
@ -7062,6 +7078,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@sinclair/typebox@npm:^0.27.8":
|
||||
version: 0.27.8
|
||||
resolution: "@sinclair/typebox@npm:0.27.8"
|
||||
checksum: 00bd7362a3439021aa1ea51b0e0d0a0e8ca1351a3d54c606b115fdcc49b51b16db6e5f43b4fe7a28c38688523e22a94d49dd31168868b655f0d4d50f032d07a1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@sindresorhus/is@npm:^4.0.0":
|
||||
version: 4.2.0
|
||||
resolution: "@sindresorhus/is@npm:4.2.0"
|
||||
@ -7212,6 +7235,23 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@testing-library/jest-native@npm:5.4.2":
|
||||
version: 5.4.2
|
||||
resolution: "@testing-library/jest-native@npm:5.4.2"
|
||||
dependencies:
|
||||
chalk: ^4.1.2
|
||||
jest-diff: ^29.0.1
|
||||
jest-matcher-utils: ^29.0.1
|
||||
pretty-format: ^29.0.3
|
||||
redent: ^3.0.0
|
||||
peerDependencies:
|
||||
react: ">=16.0.0"
|
||||
react-native: ">=0.59"
|
||||
react-test-renderer: ">=16.0.0"
|
||||
checksum: 0c9e868a07a2bb0f4bec21213153d61a7fc464e9f24c8d47fde5c7851ddaa25ed5ac76fb92ca81f88d6af005bb5d89518fcceb1d49ac702f40ccf4967bee082e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@testing-library/react-hooks@npm:8.0.1":
|
||||
version: 8.0.1
|
||||
resolution: "@testing-library/react-hooks@npm:8.0.1"
|
||||
@ -7234,6 +7274,23 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@testing-library/react-native@npm:12.1.2":
|
||||
version: 12.1.2
|
||||
resolution: "@testing-library/react-native@npm:12.1.2"
|
||||
dependencies:
|
||||
pretty-format: ^29.0.0
|
||||
peerDependencies:
|
||||
jest: ">=28.0.0"
|
||||
react: ">=16.8.0"
|
||||
react-native: ">=0.59"
|
||||
react-test-renderer: ">=16.8.0"
|
||||
peerDependenciesMeta:
|
||||
jest:
|
||||
optional: true
|
||||
checksum: 912fc961f213a8fa171b9b980d6f4edd8f11a012498fcf1b8e0d3ac1d20e85b61469a80914fda893aa48cb0d4b3f6075ec2723c58dae96eeac0ee1cd6e6daa3e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tootallnate/once@npm:1":
|
||||
version: 1.1.2
|
||||
resolution: "@tootallnate/once@npm:1.1.2"
|
||||
@ -8086,6 +8143,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/tar-stream@npm:2.2.2":
|
||||
version: 2.2.2
|
||||
resolution: "@types/tar-stream@npm:2.2.2"
|
||||
dependencies:
|
||||
"@types/node": "*"
|
||||
checksum: 4b33bc0d53770e952d6e2e8acb8889190510326a3e255d0c6edd94136d6027ecae939a7b49188d1d02d774328d9a3742ff633d505709d1a1200b3413c88d793d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/tough-cookie@npm:*":
|
||||
version: 4.0.1
|
||||
resolution: "@types/tough-cookie@npm:4.0.1"
|
||||
@ -9996,6 +10062,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"b4a@npm:^1.6.4":
|
||||
version: 1.6.4
|
||||
resolution: "b4a@npm:1.6.4"
|
||||
checksum: 81b086f9af1f8845fbef4476307236bda3d660c158c201db976f19cdce05f41f93110ab6b12fd7a2696602a490cc43d5410ee36a56d6eef93afb0d6ca69ac3b2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"babel-core@npm:^7.0.0-bridge.0":
|
||||
version: 7.0.0-bridge.0
|
||||
resolution: "babel-core@npm:7.0.0-bridge.0"
|
||||
@ -16410,6 +16483,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fast-fifo@npm:^1.1.0, fast-fifo@npm:^1.2.0":
|
||||
version: 1.3.0
|
||||
resolution: "fast-fifo@npm:1.3.0"
|
||||
checksum: edc589b818eede61d0048f399daf67cbc5ef736588669482a20f37269b4808356e54ab89676fd8fa59b26c216c11e5ac57335cc70dca54fbbf692d4acde10de6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fast-glob@npm:^2.2.6":
|
||||
version: 2.2.7
|
||||
resolution: "fast-glob@npm:2.2.7"
|
||||
@ -20450,6 +20530,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jest-diff@npm:^29.0.1, jest-diff@npm:^29.6.1":
|
||||
version: 29.6.1
|
||||
resolution: "jest-diff@npm:29.6.1"
|
||||
dependencies:
|
||||
chalk: ^4.0.0
|
||||
diff-sequences: ^29.4.3
|
||||
jest-get-type: ^29.4.3
|
||||
pretty-format: ^29.6.1
|
||||
checksum: c6350178ca27d92c7fd879790fb2525470c1ff1c5d29b1834a240fecd26c6904fb470ebddb98dc96dd85389c56c3b50e6965a1f5203e9236d213886ed9806219
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jest-diff@npm:^29.3.1":
|
||||
version: 29.3.1
|
||||
resolution: "jest-diff@npm:29.3.1"
|
||||
@ -20599,6 +20691,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jest-matcher-utils@npm:^29.0.1":
|
||||
version: 29.6.1
|
||||
resolution: "jest-matcher-utils@npm:29.6.1"
|
||||
dependencies:
|
||||
chalk: ^4.0.0
|
||||
jest-diff: ^29.6.1
|
||||
jest-get-type: ^29.4.3
|
||||
pretty-format: ^29.6.1
|
||||
checksum: d2efa6aed6e4820758b732b9fefd315c7fa4508ee690da656e1c5ac4c1a0f4cee5b04c9719ee1fda9aeb883b4209186c145089ced521e715b9fa70afdfa4a9c6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jest-matcher-utils@npm:^29.3.1":
|
||||
version: 29.3.1
|
||||
resolution: "jest-matcher-utils@npm:29.3.1"
|
||||
@ -26222,6 +26326,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"path-browserify@npm:1.0.1":
|
||||
version: 1.0.1
|
||||
resolution: "path-browserify@npm:1.0.1"
|
||||
checksum: c6d7fa376423fe35b95b2d67990060c3ee304fc815ff0a2dc1c6c3cfaff2bd0d572ee67e18f19d0ea3bbe32e8add2a05021132ac40509416459fffee35200699
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"path-browserify@npm:~0.0.0":
|
||||
version: 0.0.1
|
||||
resolution: "path-browserify@npm:0.0.1"
|
||||
@ -27023,6 +27134,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"pretty-format@npm:^29.0.3, pretty-format@npm:^29.6.1":
|
||||
version: 29.6.1
|
||||
resolution: "pretty-format@npm:29.6.1"
|
||||
dependencies:
|
||||
"@jest/schemas": ^29.6.0
|
||||
ansi-styles: ^5.0.0
|
||||
react-is: ^18.0.0
|
||||
checksum: 6f923a2379a37a425241dc223d76f671c73c4f37dba158050575a54095867d565c068b441843afdf3d7c37bed9df4bbadf46297976e60d4149972b779474203a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"pretty-format@npm:^29.5.0":
|
||||
version: 29.5.0
|
||||
resolution: "pretty-format@npm:29.5.0"
|
||||
@ -27442,6 +27564,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"queue-tick@npm:^1.0.1":
|
||||
version: 1.0.1
|
||||
resolution: "queue-tick@npm:1.0.1"
|
||||
checksum: 57c3292814b297f87f792fbeb99ce982813e4e54d7a8bdff65cf53d5c084113913289d4a48ec8bbc964927a74b847554f9f4579df43c969a6c8e0f026457ad01
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"queue@npm:6.0.2":
|
||||
version: 6.0.2
|
||||
resolution: "queue@npm:6.0.2"
|
||||
@ -30918,6 +31047,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"streamx@npm:^2.15.0":
|
||||
version: 2.15.0
|
||||
resolution: "streamx@npm:2.15.0"
|
||||
dependencies:
|
||||
fast-fifo: ^1.1.0
|
||||
queue-tick: ^1.0.1
|
||||
checksum: 6f1dcdc326d57fa4ec0c2aade730b701d28e4e206047c230c6b3f6ac25b28f79809533342dd3e11861237dbd14f3af9ab83be972f569ccdf5eddc5c7ffeb657a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"strict-uri-encode@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "strict-uri-encode@npm:2.0.0"
|
||||
@ -31690,6 +31829,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tar-stream@npm:3.1.6":
|
||||
version: 3.1.6
|
||||
resolution: "tar-stream@npm:3.1.6"
|
||||
dependencies:
|
||||
b4a: ^1.6.4
|
||||
fast-fifo: ^1.2.0
|
||||
streamx: ^2.15.0
|
||||
checksum: f3627f918581976e954ff03cb8d370551053796b82564f8c7ca8fac84c48e4d042026d0854fc222171a34ff9c682b72fae91be9c9b0a112d4c54f9e4f443e9c5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tar-stream@npm:^2.1.4":
|
||||
version: 2.2.0
|
||||
resolution: "tar-stream@npm:2.2.0"
|
||||
|
Loading…
Reference in New Issue
Block a user