1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-11-23 22:36:32 +02:00
Files
joplin/packages/app-mobile/contentScripts/imageEditorBundle/useWebViewSetup.tsx
2025-07-29 20:25:43 +01:00

201 lines
6.2 KiB
TypeScript

import * as React from 'react';
import { _ } from '@joplin/lib/locale';
import Setting from '@joplin/lib/models/Setting';
import shim from '@joplin/lib/shim';
import { themeStyle } from '@joplin/lib/theme';
import { Theme } from '@joplin/lib/themes/type';
import { useCallback, useEffect, useMemo, useRef } from 'react';
import { Platform } from 'react-native';
import useEditorMessenger from './utils/useEditorMessenger';
import { WebViewControl } from '../../components/ExtendedWebView/types';
import { LocalizedStrings } from './contentScript/types';
import { SetUpResult } from '../types';
type OnSaveCallback = (svgData: string)=> Promise<void>;
type OnCancelCallback = ()=> void;
interface Props {
themeId: number;
resourceFilename: string|null;
onSave: OnSaveCallback;
onAutoSave: OnSaveCallback;
onRequestCloseEditor: OnCancelCallback;
onSetImageChanged: (changed: boolean)=> void;
webViewRef: React.RefObject<WebViewControl>;
}
export interface ImageEditorControl {
saveThenExit(): Promise<void>;
}
const useCss = (editorTheme: Theme) => {
return useMemo(() => {
// Ensure we have contrast between the background and selection. Some themes
// have the same backgroundColor and selectionColor2. (E.g. Aritim Dark)
let selectionBackgroundColor = editorTheme.selectedColor2;
if (selectionBackgroundColor === editorTheme.backgroundColor) {
selectionBackgroundColor = editorTheme.selectedColor;
}
return `
:root .imageEditorContainer {
--background-color-1: ${editorTheme.backgroundColor};
--foreground-color-1: ${editorTheme.color};
--background-color-2: ${editorTheme.backgroundColor3};
--foreground-color-2: ${editorTheme.color3};
--background-color-3: ${editorTheme.raisedBackgroundColor};
--foreground-color-3: ${editorTheme.raisedColor};
--selection-background-color: ${editorTheme.backgroundColorHover3};
--selection-foreground-color: ${editorTheme.color3};
--primary-action-foreground-color: ${editorTheme.color4};
--primary-shadow-color: ${editorTheme.colorFaded};
width: 100vw;
height: 100vh;
box-sizing: border-box;
}
body, html {
padding: 0;
margin: 0;
overflow: hidden;
}
/* Hide the scrollbar. See scrollbar accessibility concerns
(https://developer.mozilla.org/en-US/docs/Web/CSS/scrollbar-width#accessibility_concerns)
for why this isn't done in js-draw itself. */
.toolbar-tool-row::-webkit-scrollbar {
display: none;
height: 0;
}
/* Hide the save/close icons on small screens. This isn't done in the upstream
js-draw repository partially because it isn't as well localized as Joplin
(icons can be used to suggest the meaning of a button when a translation is
unavailable). */
.toolbar-edge-toolbar:not(.one-row) .toolwidget-tag--save .toolbar-icon,
.toolbar-edge-toolbar:not(.one-row) .toolwidget-tag--exit .toolbar-icon {
display: none;
}
`;
}, [editorTheme]);
};
const useWebViewSetup = ({
webViewRef,
themeId,
resourceFilename,
onSetImageChanged,
onSave,
onAutoSave,
onRequestCloseEditor,
}: Props): SetUpResult<ImageEditorControl> => {
const editorTheme: Theme = themeStyle(themeId);
// A set of localization overrides (Joplin is better localized than js-draw).
// All localizable strings (some unused?) can be found at
// https://github.com/personalizedrefrigerator/js-draw/blob/main/.github/ISSUE_TEMPLATE/translation-js-draw-new.yml
const localizedStrings: LocalizedStrings = useMemo(() => ({
save: _('Save'),
close: _('Close'),
undo: _('Undo'),
redo: _('Redo'),
}), []);
const appInfo = useMemo(() => {
return {
name: 'Joplin',
description: `v${shim.appVersion()}`,
};
}, []);
const injectedJavaScript = useMemo(() => `
if (window.imageEditorControl === undefined) {
${shim.injectedJs('imageEditorBundle')}
const messenger = imageEditorBundle.createMessenger(() => window.imageEditorControl);
window.imageEditorControl = imageEditorBundle.createJsDrawEditor(
messenger.remoteApi,
${JSON.stringify(Setting.value('imageeditor.jsdrawToolbar'))},
${JSON.stringify(Setting.value('locale'))},
${JSON.stringify(localizedStrings)},
${JSON.stringify({
appInfo,
...(shim.mobilePlatform() === 'web' ? {
// Use the browser-default clipboard API on web.
clipboardApi: null,
} : {}),
})},
);
}
`, [localizedStrings, appInfo]);
const onReadyToLoadData = useCallback(async () => {
const getInitialInjectedData = async () => {
// On mobile, it's faster to load the image within the WebView with an XMLHttpRequest.
// In this case, the image is loaded elsewhere.
if (Platform.OS !== 'web') {
return undefined;
}
// On web, however, this doesn't work, so the image needs to be loaded here.
if (!resourceFilename) {
return '';
}
return await shim.fsDriver().readFile(resourceFilename, 'utf-8');
};
// It can take some time for initialSVGData to be transferred to the WebView.
// Thus, do so after the main content has been loaded.
webViewRef.current.injectJS(`(async () => {
if (window.imageEditorControl) {
const initialSVGPath = ${JSON.stringify(resourceFilename)};
const initialTemplateData = ${JSON.stringify(Setting.value('imageeditor.imageTemplate'))};
const initialData = ${JSON.stringify(await getInitialInjectedData())};
imageEditorControl.loadImageOrTemplate(initialSVGPath, initialTemplateData, initialData);
}
})();`);
}, [webViewRef, resourceFilename]);
const messenger = useEditorMessenger({
webViewRef,
setImageChanged: onSetImageChanged,
onReadyToLoadData,
onSave,
onAutoSave,
onRequestCloseEditor,
});
const messengerRef = useRef(messenger);
messengerRef.current = messenger;
const css = useCss(editorTheme);
useEffect(() => {
void messengerRef.current.remoteApi.onThemeUpdate(css);
}, [css]);
const editorControl = useMemo((): ImageEditorControl => {
return {
saveThenExit: () => messenger.remoteApi.saveThenExit(),
};
}, [messenger]);
return useMemo(() => {
return {
pageSetup: {
js: injectedJavaScript,
css,
},
api: editorControl,
webViewEventHandlers: {
onLoadEnd: messenger.onWebViewLoaded,
onMessage: messenger.onWebViewMessage,
},
};
}, [editorControl, messenger, injectedJavaScript, css]);
};
export default useWebViewSetup;