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/biometrics/sensorInfo.js
|
||||||
packages/app-mobile/components/getResponsiveValue.js
|
packages/app-mobile/components/getResponsiveValue.js
|
||||||
packages/app-mobile/components/getResponsiveValue.test.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/Note.js
|
||||||
packages/app-mobile/components/screens/Notes.js
|
packages/app-mobile/components/screens/Notes.js
|
||||||
packages/app-mobile/components/screens/UpgradeSyncTargetScreen.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/biometrics/sensorInfo.js
|
||||||
packages/app-mobile/components/getResponsiveValue.js
|
packages/app-mobile/components/getResponsiveValue.js
|
||||||
packages/app-mobile/components/getResponsiveValue.test.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/Note.js
|
||||||
packages/app-mobile/components/screens/Notes.js
|
packages/app-mobile/components/screens/Notes.js
|
||||||
packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js
|
packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js
|
||||||
|
@ -1,29 +1,32 @@
|
|||||||
/* eslint-disable @typescript-eslint/explicit-member-accessibility */
|
/* eslint-disable @typescript-eslint/explicit-member-accessibility */
|
||||||
import Slider from '@react-native-community/slider';
|
import Slider from '@react-native-community/slider';
|
||||||
const React = require('react');
|
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 Setting, { AppType } from '@joplin/lib/models/Setting';
|
||||||
import NavService from '@joplin/lib/services/NavService';
|
import NavService from '@joplin/lib/services/NavService';
|
||||||
import ReportService from '@joplin/lib/services/ReportService';
|
import ReportService from '@joplin/lib/services/ReportService';
|
||||||
import SearchEngine from '@joplin/lib/services/searchengine/SearchEngine';
|
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 time from '@joplin/lib/time';
|
||||||
import shim from '@joplin/lib/shim';
|
import shim from '@joplin/lib/shim';
|
||||||
import setIgnoreTlsErrors from '../../utils/TlsUtils';
|
import setIgnoreTlsErrors from '../../../utils/TlsUtils';
|
||||||
import { reg } from '@joplin/lib/registry';
|
import { reg } from '@joplin/lib/registry';
|
||||||
import { State } from '@joplin/lib/reducer';
|
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 VersionInfo = require('react-native-version-info').default;
|
||||||
const { connect } = require('react-redux');
|
const { connect } = require('react-redux');
|
||||||
import ScreenHeader from '../ScreenHeader';
|
import ScreenHeader from '../../ScreenHeader';
|
||||||
const { _ } = require('@joplin/lib/locale');
|
const { _ } = require('@joplin/lib/locale');
|
||||||
const { BaseScreenComponent } = require('../base-screen.js');
|
const { BaseScreenComponent } = require('../../base-screen.js');
|
||||||
const { Dropdown } = require('../Dropdown.js');
|
const { Dropdown } = require('../../Dropdown');
|
||||||
const { themeStyle } = require('../global-style.js');
|
const { themeStyle } = require('../../global-style.js');
|
||||||
const shared = require('@joplin/lib/components/shared/config-shared.js');
|
const shared = require('@joplin/lib/components/shared/config-shared.js');
|
||||||
import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry';
|
import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry';
|
||||||
import { openDocumentTree } from '@joplin/react-native-saf-x';
|
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 {
|
class ConfigScreenComponent extends BaseScreenComponent {
|
||||||
public static navigationOptions(): any {
|
public static navigationOptions(): any {
|
||||||
@ -223,94 +226,11 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
|||||||
|
|
||||||
public styles() {
|
public styles() {
|
||||||
const themeId = this.props.themeId;
|
const themeId = this.props.themeId;
|
||||||
const theme = themeStyle(themeId);
|
|
||||||
|
|
||||||
if (this.styles_[themeId]) return this.styles_[themeId];
|
if (this.styles_[themeId]) return this.styles_[themeId];
|
||||||
this.styles_ = {};
|
this.styles_ = {};
|
||||||
|
|
||||||
const styles: any = {
|
this.styles_[themeId] = configScreenStyles(themeId);
|
||||||
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);
|
|
||||||
return this.styles_[themeId];
|
return this.styles_[themeId];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -388,28 +308,16 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderButton(key: string, title: string, clickHandler: ()=> void, options: any = null) {
|
private 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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View key={key} style={this.styles().settingContainer}>
|
<ConfigScreenButton
|
||||||
<View style={{ flex: 1, flexDirection: 'column' }}>
|
key={key}
|
||||||
<View style={{ flex: 1 }}>
|
title={title}
|
||||||
<Button title={title} onPress={clickHandler} disabled={!!options.disabled} />
|
clickHandler={clickHandler}
|
||||||
</View>
|
description={options?.description}
|
||||||
{options.statusComp}
|
statusComponent={options?.statusComp}
|
||||||
{descriptionComp}
|
styles={this.styles()}
|
||||||
</View>
|
/>
|
||||||
</View>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -642,12 +550,13 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
|||||||
settingComps.push(this.renderButton('profiles_buttons', _('Manage profiles'), this.manageProfilesButtonPress_));
|
settingComps.push(this.renderButton('profiles_buttons', _('Manage profiles'), this.manageProfilesButtonPress_));
|
||||||
settingComps.push(this.renderButton('status_button', _('Sync Status'), this.syncStatusButtonPress_));
|
settingComps.push(this.renderButton('status_button', _('Sync Status'), this.syncStatusButtonPress_));
|
||||||
settingComps.push(this.renderButton('log_button', _('Log'), this.logButtonPress_));
|
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.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') {
|
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.') }));
|
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') {
|
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',
|
'ts',
|
||||||
'tsx',
|
'tsx',
|
||||||
'js',
|
'js',
|
||||||
|
'jsx',
|
||||||
],
|
],
|
||||||
|
|
||||||
'transform': {
|
'transform': {
|
||||||
@ -14,6 +15,11 @@ module.exports = {
|
|||||||
testMatch: ['**/*.test.(ts|tsx)'],
|
testMatch: ['**/*.test.(ts|tsx)'],
|
||||||
|
|
||||||
testPathIgnorePatterns: ['<rootDir>/node_modules/'],
|
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,
|
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/fork-uslug': path.resolve(__dirname, '../fork-uslug/'),
|
||||||
'@joplin/react-native-saf-x': path.resolve(__dirname, '../react-native-saf-x/'),
|
'@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/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 = [];
|
const watchedFolders = [];
|
||||||
for (const [, v] of Object.entries(localPackages)) {
|
for (const [, v] of Object.entries(localPackages)) {
|
||||||
watchedFolders.push(v);
|
watchedFolders.push(v);
|
||||||
@ -49,7 +64,7 @@ module.exports = {
|
|||||||
// included in your reusable module as they would be imported when
|
// included in your reusable module as they would be imported when
|
||||||
// the module is actually used.
|
// the module is actually used.
|
||||||
//
|
//
|
||||||
localPackages,
|
remappedPackages,
|
||||||
{
|
{
|
||||||
get: (target, name) => {
|
get: (target, name) => {
|
||||||
if (target.hasOwnProperty(name)) {
|
if (target.hasOwnProperty(name)) {
|
||||||
|
@ -37,6 +37,7 @@
|
|||||||
"jsc-android": "241213.1.0",
|
"jsc-android": "241213.1.0",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"md5": "2.3.0",
|
"md5": "2.3.0",
|
||||||
|
"path-browserify": "1.0.1",
|
||||||
"prop-types": "15.8.1",
|
"prop-types": "15.8.1",
|
||||||
"punycode": "2.3.0",
|
"punycode": "2.3.0",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
@ -79,6 +80,7 @@
|
|||||||
"stream": "0.0.2",
|
"stream": "0.0.2",
|
||||||
"stream-browserify": "3.0.0",
|
"stream-browserify": "3.0.0",
|
||||||
"string-natural-compare": "3.0.1",
|
"string-natural-compare": "3.0.1",
|
||||||
|
"tar-stream": "3.1.6",
|
||||||
"timers": "0.1.1",
|
"timers": "0.1.1",
|
||||||
"url": "0.11.1"
|
"url": "0.11.1"
|
||||||
},
|
},
|
||||||
@ -101,12 +103,15 @@
|
|||||||
"@codemirror/view": "6.9.3",
|
"@codemirror/view": "6.9.3",
|
||||||
"@joplin/tools": "~2.12",
|
"@joplin/tools": "~2.12",
|
||||||
"@lezer/highlight": "1.1.4",
|
"@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",
|
"@tsconfig/react-native": "2.0.2",
|
||||||
"@types/fs-extra": "11.0.1",
|
"@types/fs-extra": "11.0.1",
|
||||||
"@types/jest": "29.5.1",
|
"@types/jest": "29.5.1",
|
||||||
"@types/react": "18.0.24",
|
"@types/react": "18.0.24",
|
||||||
"@types/react-native": "0.70.6",
|
"@types/react-native": "0.70.6",
|
||||||
"@types/react-redux": "7.1.25",
|
"@types/react-redux": "7.1.25",
|
||||||
|
"@types/tar-stream": "2.2.2",
|
||||||
"babel-jest": "29.2.1",
|
"babel-jest": "29.2.1",
|
||||||
"babel-plugin-module-resolver": "4.1.0",
|
"babel-plugin-module-resolver": "4.1.0",
|
||||||
"execa": "4.1.0",
|
"execa": "4.1.0",
|
||||||
@ -119,6 +124,8 @@
|
|||||||
"md5-file": "5.0.0",
|
"md5-file": "5.0.0",
|
||||||
"metro-react-native-babel-preset": "0.73.9",
|
"metro-react-native-babel-preset": "0.73.9",
|
||||||
"nodemon": "2.0.22",
|
"nodemon": "2.0.22",
|
||||||
|
"react-test-renderer": "18.2.0",
|
||||||
|
"sqlite3": "5.1.6",
|
||||||
"ts-jest": "29.1.0",
|
"ts-jest": "29.1.0",
|
||||||
"ts-loader": "9.4.4",
|
"ts-loader": "9.4.4",
|
||||||
"ts-node": "10.9.1",
|
"ts-node": "10.9.1",
|
||||||
|
@ -58,7 +58,7 @@ import JoplinDatabase from '@joplin/lib/JoplinDatabase';
|
|||||||
import Database from '@joplin/lib/database';
|
import Database from '@joplin/lib/database';
|
||||||
import NotesScreen from './components/screens/Notes';
|
import NotesScreen from './components/screens/Notes';
|
||||||
const { TagsScreen } = require('./components/screens/tags.js');
|
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 { FolderScreen } = require('./components/screens/folder.js');
|
||||||
const { LogScreen } = require('./components/screens/log.js');
|
const { LogScreen } = require('./components/screens/log.js');
|
||||||
const { StatusScreen } = require('./components/screens/status.js');
|
const { StatusScreen } = require('./components/screens/status.js');
|
||||||
@ -423,6 +423,20 @@ function decryptionWorker_resourceMetadataButNotBlobDecrypted() {
|
|||||||
ResourceFetcher.instance().scheduleAutoAddResources();
|
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
|
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||||
async function initialize(dispatch: Function) {
|
async function initialize(dispatch: Function) {
|
||||||
shimInit();
|
shimInit();
|
||||||
@ -439,6 +453,7 @@ async function initialize(dispatch: Function) {
|
|||||||
Setting.setConstant('env', __DEV__ ? 'dev' : 'prod');
|
Setting.setConstant('env', __DEV__ ? 'dev' : 'prod');
|
||||||
Setting.setConstant('appId', 'net.cozic.joplin-mobile');
|
Setting.setConstant('appId', 'net.cozic.joplin-mobile');
|
||||||
Setting.setConstant('appType', 'mobile');
|
Setting.setConstant('appType', 'mobile');
|
||||||
|
Setting.setConstant('tempDir', await initializeTempDir());
|
||||||
const resourceDir = getResourceDir(currentProfile, isSubProfile);
|
const resourceDir = getResourceDir(currentProfile, isSubProfile);
|
||||||
Setting.setConstant('resourceDir', resourceDir);
|
Setting.setConstant('resourceDir', resourceDir);
|
||||||
|
|
||||||
|
@ -1,10 +1,16 @@
|
|||||||
import FsDriverBase, { ReadDirStatsOptions } from '@joplin/lib/fs-driver-base';
|
import FsDriverBase, { ReadDirStatsOptions } from '@joplin/lib/fs-driver-base';
|
||||||
const RNFetchBlob = require('rn-fetch-blob').default;
|
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;
|
const DocumentPicker = require('react-native-document-picker').default;
|
||||||
import { openDocument } from '@joplin/react-native-saf-x';
|
import { openDocument } from '@joplin/react-native-saf-x';
|
||||||
import RNSAF, { Encoding, DocumentFileDetail, openDocumentTree } from '@joplin/react-native-saf-x';
|
import RNSAF, { Encoding, DocumentFileDetail, openDocumentTree } from '@joplin/react-native-saf-x';
|
||||||
import { Platform } from 'react-native';
|
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://';
|
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) {
|
public async readDirStats(path: string, options: any = null) {
|
||||||
if (!options) options = {};
|
if (!options) options = {};
|
||||||
if (!('recursive' in options)) options.recursive = false;
|
if (!('recursive' in options)) options.recursive = false;
|
||||||
|
|
||||||
const isScoped = isScopedUri(path);
|
const isScoped = isScopedUri(path);
|
||||||
|
|
||||||
let stats = [];
|
let stats: any[] = [];
|
||||||
try {
|
try {
|
||||||
if (isScoped) {
|
if (isScoped) {
|
||||||
stats = await RNSAF.listFiles(path);
|
stats = await RNSAF.listFiles(path);
|
||||||
@ -86,12 +88,15 @@ export default class FsDriverRN extends FsDriverBase {
|
|||||||
for (let i = 0; i < stats.length; i++) {
|
for (let i = 0; i < stats.length; i++) {
|
||||||
const stat = stats[i];
|
const stat = stats[i];
|
||||||
const relativePath = (isScoped ? stat.uri : stat.path).substr(path.length + 1);
|
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) {
|
if (isScoped) {
|
||||||
|
// readUriDirStatsHandleRecursion_ expects stat to have a URI property.
|
||||||
|
// Use the original stat.
|
||||||
output = await this.readUriDirStatsHandleRecursion_(stat, output, options);
|
output = await this.readUriDirStatsHandleRecursion_(stat, output, options);
|
||||||
} else {
|
} else {
|
||||||
output = await this.readDirStatsHandleRecursion_(path, stat, output, options);
|
output = await this.readDirStatsHandleRecursion_(path, standardStat, output, options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return output;
|
return output;
|
||||||
@ -135,6 +140,8 @@ export default class FsDriverRN extends FsDriverBase {
|
|||||||
await RNSAF.mkdir(path);
|
await RNSAF.mkdir(path);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Also creates parent directories: Works like mkdir -p
|
||||||
return RNFS.mkdir(path);
|
return RNFS.mkdir(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -148,8 +155,9 @@ export default class FsDriverRN extends FsDriverBase {
|
|||||||
}
|
}
|
||||||
return this.rnfsStatToStd_(r, path);
|
return this.rnfsStatToStd_(r, path);
|
||||||
} catch (error) {
|
} 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' }
|
// 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.
|
// which unfortunately does not have a proper error code. Can be ignored.
|
||||||
return null;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
@ -260,6 +268,53 @@ export default class FsDriverRN extends FsDriverBase {
|
|||||||
throw new Error(`Not implemented: md5File(): ${path}`);
|
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> {
|
public async getExternalDirectoryPath(): Promise<string | undefined> {
|
||||||
let directory;
|
let directory;
|
||||||
if (this.isUsingAndroidSAF()) {
|
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 shim from '../../shim';
|
||||||
import { _ } from '../../locale';
|
import { _ } from '../../locale';
|
||||||
import BaseItem from '../../models/BaseItem';
|
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 from './InteropService_Importer_Md';
|
||||||
import InteropService_Importer_Md_frontmatter from './InteropService_Importer_Md_frontmatter';
|
import InteropService_Importer_Md_frontmatter from './InteropService_Importer_Md_frontmatter';
|
||||||
import InteropService_Importer_Raw from './InteropService_Importer_Raw';
|
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_Jex from './InteropService_Exporter_Jex';
|
||||||
import InteropService_Exporter_Raw from './InteropService_Exporter_Raw';
|
import InteropService_Exporter_Raw from './InteropService_Exporter_Raw';
|
||||||
import InteropService_Exporter_Md from './InteropService_Exporter_Md';
|
import InteropService_Exporter_Md from './InteropService_Exporter_Md';
|
||||||
import InteropService_Exporter_Md_frontmatter from './InteropService_Exporter_Md_frontmatter';
|
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_Importer_Base from './InteropService_Importer_Base';
|
||||||
import InteropService_Exporter_Base from './InteropService_Exporter_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 { sprintf } = require('sprintf-js');
|
||||||
const { fileExtension } = require('../../path-utils');
|
const { fileExtension } = require('../../path-utils');
|
||||||
const EventEmitter = require('events');
|
const EventEmitter = require('events');
|
||||||
@ -89,19 +86,18 @@ export default class InteropService {
|
|||||||
fileExtensions: ['enex'],
|
fileExtensions: ['enex'],
|
||||||
sources: [FileSystemItem.File],
|
sources: [FileSystemItem.File],
|
||||||
description: _('Evernote Export File (as Markdown)'),
|
description: _('Evernote Export File (as Markdown)'),
|
||||||
importerClass: 'InteropService_Importer_EnexToMd',
|
supportsMobile: false,
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
}, () => new InteropService_Importer_EnexToMd()),
|
}, dynamicRequireModuleFactory('./InteropService_Importer_EnexToMd')),
|
||||||
|
|
||||||
makeImportModule({
|
makeImportModule({
|
||||||
format: 'enex',
|
format: 'enex',
|
||||||
fileExtensions: ['enex'],
|
fileExtensions: ['enex'],
|
||||||
sources: [FileSystemItem.File],
|
sources: [FileSystemItem.File],
|
||||||
description: _('Evernote Export File (as HTML)'),
|
description: _('Evernote Export File (as HTML)'),
|
||||||
// TODO: Consider doing this the same way as the multiple `md` importers are handled
|
supportsMobile: false,
|
||||||
importerClass: 'InteropService_Importer_EnexToHtml',
|
|
||||||
outputFormat: ImportModuleOutputFormat.Html,
|
outputFormat: ImportModuleOutputFormat.Html,
|
||||||
}, () => new InteropService_Importer_EnexToHtml()),
|
}, dynamicRequireModuleFactory('./InteropService_Importer_EnexToHtml')),
|
||||||
];
|
];
|
||||||
|
|
||||||
const exportModules = [
|
const exportModules = [
|
||||||
@ -136,13 +132,15 @@ export default class InteropService {
|
|||||||
target: FileSystemItem.File,
|
target: FileSystemItem.File,
|
||||||
isNoteArchive: false,
|
isNoteArchive: false,
|
||||||
description: _('HTML File'),
|
description: _('HTML File'),
|
||||||
}, () => new InteropService_Exporter_Html()),
|
supportsMobile: false,
|
||||||
|
}, dynamicRequireModuleFactory('./InteropService_Exporter_Html')),
|
||||||
|
|
||||||
makeExportModule({
|
makeExportModule({
|
||||||
format: 'html',
|
format: 'html',
|
||||||
target: FileSystemItem.Directory,
|
target: FileSystemItem.Directory,
|
||||||
description: _('HTML Directory'),
|
description: _('HTML Directory'),
|
||||||
}, () => new InteropService_Exporter_Html()),
|
supportsMobile: false,
|
||||||
|
}, dynamicRequireModuleFactory('./InteropService_Exporter_Html')),
|
||||||
];
|
];
|
||||||
|
|
||||||
this.defaultModules_ = (importModules as Module[]).concat(exportModules);
|
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) {
|
private findModuleByFormat_(type: ModuleType, format: string, target: FileSystemItem = null, outputFormat: ImportModuleOutputFormat = null) {
|
||||||
const modules = this.modules();
|
const modules = this.modules();
|
||||||
const matches = [];
|
const matches = [];
|
||||||
|
|
||||||
|
const isMobile = shim.mobilePlatform() !== '';
|
||||||
for (let i = 0; i < modules.length; i++) {
|
for (let i = 0; i < modules.length; i++) {
|
||||||
const m = modules[i];
|
const m = modules[i];
|
||||||
|
|
||||||
|
if (!m.supportsMobile && isMobile) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (m.format === format && m.type === type) {
|
if (m.format === format && m.type === type) {
|
||||||
if (!target && !outputFormat) {
|
if (!target && !outputFormat) {
|
||||||
matches.push(m);
|
matches.push(m);
|
||||||
@ -205,7 +210,7 @@ export default class InteropService {
|
|||||||
// explicit with which importer we want to use.
|
// explicit with which importer we want to use.
|
||||||
//
|
//
|
||||||
// https://github.com/laurent22/joplin/pull/1795#pullrequestreview-281574417
|
// 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);
|
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));
|
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 result: ImportExportResult = { warnings: [] };
|
||||||
const itemsToExport: any[] = [];
|
const itemsToExport: any[] = [];
|
||||||
|
|
||||||
|
options.onProgress?.(ExportProgressState.QueuingItems, null);
|
||||||
|
let totalItemsToProcess = 0;
|
||||||
|
|
||||||
const queueExportItem = (itemType: number, itemOrId: any) => {
|
const queueExportItem = (itemType: number, itemOrId: any) => {
|
||||||
|
totalItemsToProcess ++;
|
||||||
itemsToExport.push({
|
itemsToExport.push({
|
||||||
type: itemType,
|
type: itemType,
|
||||||
itemOrId: itemOrId,
|
itemOrId: itemOrId,
|
||||||
@ -373,6 +382,7 @@ export default class InteropService {
|
|||||||
await exporter.prepareForProcessingItemType(type, itemsToExport);
|
await exporter.prepareForProcessingItemType(type, itemsToExport);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let itemsProcessed = 0;
|
||||||
for (let typeOrderIndex = 0; typeOrderIndex < typeOrder.length; typeOrderIndex++) {
|
for (let typeOrderIndex = 0; typeOrderIndex < typeOrder.length; typeOrderIndex++) {
|
||||||
const type = typeOrder[typeOrderIndex];
|
const type = typeOrder[typeOrderIndex];
|
||||||
|
|
||||||
@ -414,9 +424,13 @@ export default class InteropService {
|
|||||||
console.error(error);
|
console.error(error);
|
||||||
result.warnings.push(error.message);
|
result.warnings.push(error.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
itemsProcessed++;
|
||||||
|
options.onProgress?.(ExportProgressState.Exporting, itemsProcessed / totalItemsToProcess);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
options.onProgress?.(ExportProgressState.Closing, null);
|
||||||
await exporter.close();
|
await exporter.close();
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
/* eslint @typescript-eslint/no-unused-vars: 0, no-unused-vars: ["error", { "argsIgnorePattern": ".*" }], */
|
/* eslint @typescript-eslint/no-unused-vars: 0, no-unused-vars: ["error", { "argsIgnorePattern": ".*" }], */
|
||||||
|
|
||||||
import Setting from '../../models/Setting';
|
import Setting from '../../models/Setting';
|
||||||
|
import shim from '../../shim';
|
||||||
|
|
||||||
export default class InteropService_Exporter_Base {
|
export default class InteropService_Exporter_Base {
|
||||||
private context_: any = {};
|
private context_: any = {};
|
||||||
@ -31,7 +32,7 @@ export default class InteropService_Exporter_Base {
|
|||||||
protected async temporaryDirectory_(createIt: boolean) {
|
protected async temporaryDirectory_(createIt: boolean) {
|
||||||
const md5 = require('md5');
|
const md5 = require('md5');
|
||||||
const tempDir = `${Setting.value('tempDir')}/${md5(Math.random() + Date.now())}`;
|
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;
|
return tempDir;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,6 @@ import InteropService_Exporter_Base from './InteropService_Exporter_Base';
|
|||||||
import InteropService_Exporter_Raw from './InteropService_Exporter_Raw';
|
import InteropService_Exporter_Raw from './InteropService_Exporter_Raw';
|
||||||
import shim from '../../shim';
|
import shim from '../../shim';
|
||||||
|
|
||||||
const fs = require('fs-extra');
|
|
||||||
|
|
||||||
export default class InteropService_Exporter_Jex extends InteropService_Exporter_Base {
|
export default class InteropService_Exporter_Jex extends InteropService_Exporter_Base {
|
||||||
|
|
||||||
private tempDir_: string;
|
private tempDir_: string;
|
||||||
@ -41,6 +39,6 @@ export default class InteropService_Exporter_Jex extends InteropService_Exporter
|
|||||||
cwd: this.tempDir_,
|
cwd: this.tempDir_,
|
||||||
}, filePaths);
|
}, filePaths);
|
||||||
|
|
||||||
await fs.remove(this.tempDir_);
|
await shim.fsDriver().remove(this.tempDir_);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
import { ImportExportResult } from './types';
|
import { ImportExportResult } from './types';
|
||||||
|
|
||||||
import Setting from '../../models/Setting';
|
import Setting from '../../models/Setting';
|
||||||
|
import shim from '../../shim';
|
||||||
|
|
||||||
export default class InteropService_Importer_Base {
|
export default class InteropService_Importer_Base {
|
||||||
|
|
||||||
@ -28,7 +29,7 @@ export default class InteropService_Importer_Base {
|
|||||||
protected async temporaryDirectory_(createIt: boolean) {
|
protected async temporaryDirectory_(createIt: boolean) {
|
||||||
const md5 = require('md5');
|
const md5 = require('md5');
|
||||||
const tempDir = `${Setting.value('tempDir')}/${md5(Math.random() + Date.now())}`;
|
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;
|
return tempDir;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,8 +5,6 @@ import InteropService_Importer_Raw from './InteropService_Importer_Raw';
|
|||||||
const { filename } = require('../../path-utils');
|
const { filename } = require('../../path-utils');
|
||||||
import shim from '../../shim';
|
import shim from '../../shim';
|
||||||
|
|
||||||
const fs = require('fs-extra');
|
|
||||||
|
|
||||||
export default class InteropService_Importer_Jex extends InteropService_Importer_Base {
|
export default class InteropService_Importer_Jex extends InteropService_Importer_Base {
|
||||||
public async exec(result: ImportExportResult) {
|
public async exec(result: ImportExportResult) {
|
||||||
const tempDir = await this.temporaryDirectory_(true);
|
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_);
|
await importer.init(tempDir, this.options_);
|
||||||
result = await importer.exec(result);
|
result = await importer.exec(result);
|
||||||
|
|
||||||
await fs.remove(tempDir);
|
await shim.fsDriver().remove(tempDir);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { _ } from '../../locale';
|
import { _ } from '../../locale';
|
||||||
|
import shim from '../../shim';
|
||||||
import InteropService_Exporter_Base from './InteropService_Exporter_Base';
|
import InteropService_Exporter_Base from './InteropService_Exporter_Base';
|
||||||
import InteropService_Importer_Base from './InteropService_Importer_Base';
|
import InteropService_Importer_Base from './InteropService_Importer_Base';
|
||||||
import { ExportOptions, FileSystemItem, ImportModuleOutputFormat, ImportOptions, ModuleType } from './types';
|
import { ExportOptions, FileSystemItem, ImportModuleOutputFormat, ImportOptions, ModuleType } from './types';
|
||||||
@ -10,6 +11,8 @@ interface BaseMetadata {
|
|||||||
description: string;
|
description: string;
|
||||||
isDefault: boolean;
|
isDefault: boolean;
|
||||||
|
|
||||||
|
supportsMobile: boolean;
|
||||||
|
|
||||||
// Returns the full label to be displayed in the UI.
|
// Returns the full label to be displayed in the UI.
|
||||||
fullLabel(moduleSource?: FileSystemItem): string;
|
fullLabel(moduleSource?: FileSystemItem): string;
|
||||||
|
|
||||||
@ -24,7 +27,6 @@ interface ImportMetadata extends BaseMetadata {
|
|||||||
type: ModuleType.Importer;
|
type: ModuleType.Importer;
|
||||||
|
|
||||||
sources: FileSystemItem[];
|
sources: FileSystemItem[];
|
||||||
importerClass: string;
|
|
||||||
outputFormat: ImportModuleOutputFormat;
|
outputFormat: ImportModuleOutputFormat;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,6 +49,7 @@ const defaultBaseMetadata = {
|
|||||||
fileExtensions: [] as string[],
|
fileExtensions: [] as string[],
|
||||||
description: '',
|
description: '',
|
||||||
isNoteArchive: true,
|
isNoteArchive: true,
|
||||||
|
supportsMobile: true,
|
||||||
isDefault: false,
|
isDefault: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -66,7 +69,6 @@ export const makeImportModule = (
|
|||||||
...defaultBaseMetadata,
|
...defaultBaseMetadata,
|
||||||
type: ModuleType.Importer,
|
type: ModuleType.Importer,
|
||||||
sources: [],
|
sources: [],
|
||||||
importerClass: '',
|
|
||||||
outputFormat: ImportModuleOutputFormat.Markdown,
|
outputFormat: ImportModuleOutputFormat.Markdown,
|
||||||
|
|
||||||
fullLabel: (moduleSource?: FileSystemItem) => {
|
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;
|
type Module = ImportModule|ExportModule;
|
||||||
export default Module;
|
export default Module;
|
||||||
|
@ -35,6 +35,14 @@ export interface ImportOptions {
|
|||||||
outputFormat?: ImportModuleOutputFormat;
|
outputFormat?: ImportModuleOutputFormat;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ExportProgressState {
|
||||||
|
QueuingItems,
|
||||||
|
Exporting,
|
||||||
|
Closing,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OnExportProgressCallback = (status: ExportProgressState, progress: number)=> void;
|
||||||
|
|
||||||
export interface ExportOptions {
|
export interface ExportOptions {
|
||||||
format?: string;
|
format?: string;
|
||||||
path?: string;
|
path?: string;
|
||||||
@ -46,6 +54,8 @@ export interface ExportOptions {
|
|||||||
plugins?: PluginStates;
|
plugins?: PluginStates;
|
||||||
customCss?: string;
|
customCss?: string;
|
||||||
packIntoSingleFile?: boolean;
|
packIntoSingleFile?: boolean;
|
||||||
|
|
||||||
|
onProgress?: OnExportProgressCallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ImportExportResult {
|
export interface ImportExportResult {
|
||||||
|
150
yarn.lock
150
yarn.lock
@ -4277,6 +4277,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@jest/source-map@npm:^29.4.3":
|
||||||
version: 29.4.3
|
version: 29.4.3
|
||||||
resolution: "@jest/source-map@npm: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/netinfo": 9.3.10
|
||||||
"@react-native-community/push-notification-ios": 1.11.0
|
"@react-native-community/push-notification-ios": 1.11.0
|
||||||
"@react-native-community/slider": 4.4.2
|
"@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
|
"@tsconfig/react-native": 2.0.2
|
||||||
"@types/fs-extra": 11.0.1
|
"@types/fs-extra": 11.0.1
|
||||||
"@types/jest": 29.5.1
|
"@types/jest": 29.5.1
|
||||||
"@types/react": 18.0.24
|
"@types/react": 18.0.24
|
||||||
"@types/react-native": 0.70.6
|
"@types/react-native": 0.70.6
|
||||||
"@types/react-redux": 7.1.25
|
"@types/react-redux": 7.1.25
|
||||||
|
"@types/tar-stream": 2.2.2
|
||||||
assert-browserify: 2.0.0
|
assert-browserify: 2.0.0
|
||||||
babel-jest: 29.2.1
|
babel-jest: 29.2.1
|
||||||
babel-plugin-module-resolver: 4.1.0
|
babel-plugin-module-resolver: 4.1.0
|
||||||
@ -4562,6 +4574,7 @@ __metadata:
|
|||||||
md5-file: 5.0.0
|
md5-file: 5.0.0
|
||||||
metro-react-native-babel-preset: 0.73.9
|
metro-react-native-babel-preset: 0.73.9
|
||||||
nodemon: 2.0.22
|
nodemon: 2.0.22
|
||||||
|
path-browserify: 1.0.1
|
||||||
prop-types: 15.8.1
|
prop-types: 15.8.1
|
||||||
punycode: 2.3.0
|
punycode: 2.3.0
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
@ -4599,11 +4612,14 @@ __metadata:
|
|||||||
react-native-webview: 12.4.0
|
react-native-webview: 12.4.0
|
||||||
react-native-zip-archive: 6.0.9
|
react-native-zip-archive: 6.0.9
|
||||||
react-redux: 8.0.7
|
react-redux: 8.0.7
|
||||||
|
react-test-renderer: 18.2.0
|
||||||
redux: 4.2.1
|
redux: 4.2.1
|
||||||
rn-fetch-blob: 0.12.0
|
rn-fetch-blob: 0.12.0
|
||||||
|
sqlite3: 5.1.6
|
||||||
stream: 0.0.2
|
stream: 0.0.2
|
||||||
stream-browserify: 3.0.0
|
stream-browserify: 3.0.0
|
||||||
string-natural-compare: 3.0.1
|
string-natural-compare: 3.0.1
|
||||||
|
tar-stream: 3.1.6
|
||||||
timers: 0.1.1
|
timers: 0.1.1
|
||||||
ts-jest: 29.1.0
|
ts-jest: 29.1.0
|
||||||
ts-loader: 9.4.4
|
ts-loader: 9.4.4
|
||||||
@ -7062,6 +7078,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@sindresorhus/is@npm:^4.0.0":
|
||||||
version: 4.2.0
|
version: 4.2.0
|
||||||
resolution: "@sindresorhus/is@npm:4.2.0"
|
resolution: "@sindresorhus/is@npm:4.2.0"
|
||||||
@ -7212,6 +7235,23 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@testing-library/react-hooks@npm:8.0.1":
|
||||||
version: 8.0.1
|
version: 8.0.1
|
||||||
resolution: "@testing-library/react-hooks@npm:8.0.1"
|
resolution: "@testing-library/react-hooks@npm:8.0.1"
|
||||||
@ -7234,6 +7274,23 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@tootallnate/once@npm:1":
|
||||||
version: 1.1.2
|
version: 1.1.2
|
||||||
resolution: "@tootallnate/once@npm:1.1.2"
|
resolution: "@tootallnate/once@npm:1.1.2"
|
||||||
@ -8086,6 +8143,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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:*":
|
"@types/tough-cookie@npm:*":
|
||||||
version: 4.0.1
|
version: 4.0.1
|
||||||
resolution: "@types/tough-cookie@npm:4.0.1"
|
resolution: "@types/tough-cookie@npm:4.0.1"
|
||||||
@ -9996,6 +10062,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"babel-core@npm:^7.0.0-bridge.0":
|
||||||
version: 7.0.0-bridge.0
|
version: 7.0.0-bridge.0
|
||||||
resolution: "babel-core@npm:7.0.0-bridge.0"
|
resolution: "babel-core@npm:7.0.0-bridge.0"
|
||||||
@ -16410,6 +16483,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"fast-glob@npm:^2.2.6":
|
||||||
version: 2.2.7
|
version: 2.2.7
|
||||||
resolution: "fast-glob@npm:2.2.7"
|
resolution: "fast-glob@npm:2.2.7"
|
||||||
@ -20450,6 +20530,18 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"jest-diff@npm:^29.3.1":
|
||||||
version: 29.3.1
|
version: 29.3.1
|
||||||
resolution: "jest-diff@npm:29.3.1"
|
resolution: "jest-diff@npm:29.3.1"
|
||||||
@ -20599,6 +20691,18 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"jest-matcher-utils@npm:^29.3.1":
|
||||||
version: 29.3.1
|
version: 29.3.1
|
||||||
resolution: "jest-matcher-utils@npm:29.3.1"
|
resolution: "jest-matcher-utils@npm:29.3.1"
|
||||||
@ -26222,6 +26326,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"path-browserify@npm:~0.0.0":
|
||||||
version: 0.0.1
|
version: 0.0.1
|
||||||
resolution: "path-browserify@npm:0.0.1"
|
resolution: "path-browserify@npm:0.0.1"
|
||||||
@ -27023,6 +27134,17 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"pretty-format@npm:^29.5.0":
|
||||||
version: 29.5.0
|
version: 29.5.0
|
||||||
resolution: "pretty-format@npm:29.5.0"
|
resolution: "pretty-format@npm:29.5.0"
|
||||||
@ -27442,6 +27564,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"queue@npm:6.0.2":
|
||||||
version: 6.0.2
|
version: 6.0.2
|
||||||
resolution: "queue@npm:6.0.2"
|
resolution: "queue@npm:6.0.2"
|
||||||
@ -30918,6 +31047,16 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"strict-uri-encode@npm:^2.0.0":
|
||||||
version: 2.0.0
|
version: 2.0.0
|
||||||
resolution: "strict-uri-encode@npm:2.0.0"
|
resolution: "strict-uri-encode@npm:2.0.0"
|
||||||
@ -31690,6 +31829,17 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"tar-stream@npm:^2.1.4":
|
||||||
version: 2.2.0
|
version: 2.2.0
|
||||||
resolution: "tar-stream@npm:2.2.0"
|
resolution: "tar-stream@npm:2.2.0"
|
||||||
|
Loading…
Reference in New Issue
Block a user