1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-10-31 00:07:48 +02:00

Mobile: Resolves #9017: Support pasting images (#10751)

This commit is contained in:
Henry Heino
2024-07-16 11:28:05 -07:00
committed by GitHub
parent 2d984ce9a8
commit 71f70f4d2c
15 changed files with 181 additions and 49 deletions

View File

@@ -27,6 +27,21 @@ export const initCodeMirror = (
initialText,
settings,
onPasteFile: async (data) => {
const reader = new FileReader();
return new Promise<void>((resolve, reject) => {
reader.onload = async () => {
const dataUrl = reader.result as string;
const base64 = dataUrl.replace(/^data:.*;base64,/, '');
await messenger.remoteApi.onPasteFile(data.type, base64);
resolve();
};
reader.onerror = () => reject(new Error('Failed to load file.'));
reader.readAsDataURL(data);
});
},
onLogMessage: message => {
void messenger.remoteApi.logMessage(message);
},

View File

@@ -38,7 +38,7 @@ describe('NoteEditor', () => {
onChange={()=>{}}
onSelectionChange={()=>{}}
onUndoRedoDepthChange={()=>{}}
onAttach={()=>{}}
onAttach={async ()=>{}}
plugins={{}}
/>
</MenuProvider>,

View File

@@ -26,11 +26,14 @@ import { WebViewErrorEvent } from 'react-native-webview/lib/RNCWebViewNativeComp
import Logger from '@joplin/utils/Logger';
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
import useEditorCommandHandler from './hooks/useEditorCommandHandler';
import { join, dirname } from 'path';
import * as mimeUtils from '@joplin/lib/mime-utils';
import uuid from '@joplin/lib/uuid';
type ChangeEventHandler = (event: ChangeEvent)=> void;
type UndoRedoDepthChangeHandler = (event: UndoRedoDepthChangeEvent)=> void;
type SelectionChangeEventHandler = (event: SelectionRangeChangeEvent)=> void;
type OnAttachCallback = ()=> void;
type OnAttachCallback = (filePath?: string)=> Promise<void>;
const logger = Logger.create('NoteEditor');
@@ -373,6 +376,9 @@ function NoteEditor(props: Props, ref: any) {
const onEditorEvent = useRef((_event: EditorEvent) => {});
const onAttachRef = useRef(props.onAttach);
onAttachRef.current = props.onAttach;
const editorMessenger = useMemo(() => {
const localApi: WebViewToEditorApi = {
async onEditorEvent(event) {
@@ -381,6 +387,16 @@ function NoteEditor(props: Props, ref: any) {
async logMessage(message) {
logger.debug('CodeMirror:', message);
},
async onPasteFile(type, data) {
const tempFilePath = join(Setting.value('tempDir'), `paste.${uuid.createNano()}.${mimeUtils.toFileExtension(type)}`);
await shim.fsDriver().mkdir(dirname(tempFilePath));
try {
await shim.fsDriver().writeFile(tempFilePath, data, 'base64');
await onAttachRef.current(tempFilePath);
} finally {
await shim.fsDriver().remove(tempFilePath);
}
},
};
const messenger = new RNToWebViewMessenger<WebViewToEditorApi, EditorBodyControl>(
'editor', webviewRef, localApi,

View File

@@ -57,4 +57,5 @@ export interface SelectionRange {
export interface WebViewToEditorApi {
onEditorEvent(event: EditorEvent): Promise<void>;
logMessage(message: string): Promise<void>;
onPasteFile(type: string, dataBase64: string): Promise<void>;
}

View File

@@ -6,10 +6,9 @@ import UndoRedoService from '@joplin/lib/services/UndoRedoService';
import NoteBodyViewer from '../NoteBodyViewer/NoteBodyViewer';
import checkPermissions from '../../utils/checkPermissions';
import NoteEditor from '../NoteEditor/NoteEditor';
import { Size } from '@joplin/utils/types';
const FileViewer = require('react-native-file-viewer').default;
const React = require('react');
import { Keyboard, View, TextInput, StyleSheet, Linking, Image, Share, NativeSyntheticEvent } from 'react-native';
import { Keyboard, View, TextInput, StyleSheet, Linking, Share, NativeSyntheticEvent } from 'react-native';
import { Platform, PermissionsAndroid } from 'react-native';
const { connect } = require('react-redux');
// const { MarkdownEditor } = require('@joplin/lib/../MarkdownEditor/index.js');
@@ -36,7 +35,6 @@ import { BaseScreenComponent } from '../base-screen';
import { themeStyle, editorFont } from '../global-style';
const { dialogs } = require('../../utils/dialogs.js');
const DialogBox = require('react-native-dialogbox').default;
import ImageResizer from '@bam.tech/react-native-image-resizer';
import shared, { BaseNoteScreenComponent } from '@joplin/lib/components/shared/note-screen-shared';
import { Asset, ImagePickerResponse, launchImageLibrary } from 'react-native-image-picker';
import SelectDateTimeDialog from '../SelectDateTimeDialog';
@@ -65,6 +63,8 @@ import debounce from '../../utils/debounce';
import { focus } from '@joplin/lib/utils/focusHandler';
import CommandService from '@joplin/lib/services/CommandService';
import * as urlUtils from '@joplin/lib/urlUtils';
import getImageDimensions from '../../utils/image/getImageDimensions';
import resizeImage from '../../utils/image/resizeImage';
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const emptyArray: any[] = [];
@@ -682,24 +682,9 @@ class NoteScreenComponent extends BaseScreenComponent<Props, State> implements B
return result;
}
public async imageDimensions(uri: string): Promise<Size> {
return new Promise((resolve, reject) => {
Image.getSize(
uri,
(width: number, height: number) => {
resolve({ width: width, height: height });
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
(error: any) => {
reject(error);
},
);
});
}
public async resizeImage(localFilePath: string, targetPath: string, mimeType: string) {
const maxSize = Resource.IMAGE_MAX_DIMENSION;
const dimensions = await this.imageDimensions(localFilePath);
const dimensions = await getImageDimensions(localFilePath);
reg.logger().info('Original dimensions ', dimensions);
const saveOriginalImage = async () => {
@@ -711,30 +696,14 @@ class NoteScreenComponent extends BaseScreenComponent<Props, State> implements B
dimensions.height = maxSize;
reg.logger().info('New dimensions ', dimensions);
const format = mimeType === 'image/png' ? 'PNG' : 'JPEG';
reg.logger().info(`Resizing image ${localFilePath}`);
const resizedImage = await ImageResizer.createResizedImage(
localFilePath,
dimensions.width,
dimensions.height,
format,
85, // quality
undefined, // rotation
undefined, // outputPath
true, // keep metadata
);
const resizedImagePath = resizedImage.uri;
reg.logger().info('Resized image ', resizedImagePath);
reg.logger().info(`Moving ${resizedImagePath} => ${targetPath}`);
await shim.fsDriver().copy(resizedImagePath, targetPath);
try {
await shim.fsDriver().unlink(resizedImagePath);
} catch (error) {
reg.logger().warn('Error when unlinking cached file: ', error);
}
await resizeImage({
inputPath: localFilePath,
outputPath: targetPath,
maxWidth: dimensions.width,
maxHeight: dimensions.height,
quality: 85,
format: mimeType === 'image/png' ? 'PNG' : 'JPEG',
});
return true;
};
@@ -1140,11 +1109,19 @@ class NoteScreenComponent extends BaseScreenComponent<Props, State> implements B
const buttonId = await dialogs.pop(this, _('Choose an option'), buttons);
if (buttonId === 'takePhoto') this.takePhoto_onPress();
if (buttonId === 'attachFile') void this.attachFile_onPress();
if (buttonId === 'attachPhoto') void this.attachPhoto_onPress();
if (buttonId === 'takePhoto') await this.takePhoto_onPress();
if (buttonId === 'attachFile') await this.attachFile_onPress();
if (buttonId === 'attachPhoto') await this.attachPhoto_onPress();
}
public onAttach = async (filePath?: string) => {
if (filePath) {
await this.attachFile({ uri: filePath }, 'all');
} else {
await this.showAttachMenu();
}
};
// private vosk_:Vosk;
// private async getVosk() {
@@ -1585,7 +1562,7 @@ class NoteScreenComponent extends BaseScreenComponent<Props, State> implements B
onChange={this.onMarkdownEditorTextChange}
onSelectionChange={this.onMarkdownEditorSelectionChange}
onUndoRedoDepthChange={this.onUndoRedoDepthChange}
onAttach={() => this.showAttachMenu()}
onAttach={this.onAttach}
readOnly={this.state.readOnly}
plugins={this.props.plugins}
style={{