feat: Initial clean release of CHM
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
这个提交包含在:
@@ -0,0 +1,140 @@
|
||||
import * as React from 'react';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import { useCallback, useContext, useEffect, useRef, useState } from 'react';
|
||||
import ExtendedWebView from '../../ExtendedWebView';
|
||||
import { OnMessageEvent, WebViewControl } from '../../ExtendedWebView/types';
|
||||
import { WebViewErrorEvent } from 'react-native-webview/lib/WebViewTypes';
|
||||
import { clearAutosave, writeAutosave } from './autosave';
|
||||
import { DialogContext } from '../../DialogManager';
|
||||
import BackButtonService from '../../../services/BackButtonService';
|
||||
import useWebViewSetup, { ImageEditorControl } from '../../../contentScripts/imageEditorBundle/useWebViewSetup';
|
||||
|
||||
const logger = Logger.create('ImageEditor');
|
||||
|
||||
type OnSaveCallback = (svgData: string)=> Promise<void>;
|
||||
type OnCancelCallback = ()=> void;
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
resourceFilename: string|null;
|
||||
onSave: OnSaveCallback;
|
||||
onExit: OnCancelCallback;
|
||||
}
|
||||
|
||||
|
||||
const ImageEditor = (props: Props) => {
|
||||
const webViewRef = useRef<WebViewControl|null>(null);
|
||||
const [imageChanged, setImageChanged] = useState(false);
|
||||
|
||||
const editorControlRef = useRef<ImageEditorControl|null>(null);
|
||||
const dialogs = useContext(DialogContext);
|
||||
|
||||
const onRequestCloseEditor = useCallback((promptIfUnsaved = true) => {
|
||||
const discardChangesAndClose = async () => {
|
||||
await clearAutosave();
|
||||
props.onExit();
|
||||
};
|
||||
|
||||
if (!imageChanged || !promptIfUnsaved) {
|
||||
void discardChangesAndClose();
|
||||
return;
|
||||
}
|
||||
|
||||
dialogs.prompt(
|
||||
_('Save changes?'), _('This drawing may have unsaved changes.'), [
|
||||
{
|
||||
text: _('Discard changes'),
|
||||
onPress: discardChangesAndClose,
|
||||
style: 'destructive',
|
||||
},
|
||||
{
|
||||
text: _('Save changes'),
|
||||
onPress: () => {
|
||||
// saveDrawing calls props.onSave(...) which may close the
|
||||
// editor.
|
||||
void editorControlRef.current.saveThenExit();
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
}, [dialogs, props.onExit, imageChanged]);
|
||||
|
||||
useEffect(() => {
|
||||
const hardwareBackPressListener = () => {
|
||||
onRequestCloseEditor(true);
|
||||
return true;
|
||||
};
|
||||
BackButtonService.addHandler(hardwareBackPressListener);
|
||||
|
||||
return () => {
|
||||
BackButtonService.removeHandler(hardwareBackPressListener);
|
||||
};
|
||||
}, [onRequestCloseEditor]);
|
||||
|
||||
const { pageSetup, api: editorControl, webViewEventHandlers } = useWebViewSetup({
|
||||
webViewRef,
|
||||
themeId: props.themeId,
|
||||
onSetImageChanged: setImageChanged,
|
||||
onAutoSave: writeAutosave,
|
||||
onSave: props.onSave,
|
||||
onRequestCloseEditor,
|
||||
resourceFilename: props.resourceFilename,
|
||||
});
|
||||
editorControlRef.current = editorControl;
|
||||
|
||||
const [html, setHtml] = useState('');
|
||||
useEffect(() => {
|
||||
setHtml(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"/>
|
||||
|
||||
<style>
|
||||
${pageSetup.css}
|
||||
</style>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
`);
|
||||
|
||||
// Only set HTML initially (and don't reset). Changing the HTML reloads
|
||||
// the page.
|
||||
//
|
||||
// We need the HTML to initially have the correct CSS to prevent color
|
||||
// changes on load.
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const onError = useCallback((event: WebViewErrorEvent) => {
|
||||
logger.error('ImageEditor: WebView error: ', event);
|
||||
}, []);
|
||||
|
||||
const onWebViewMessage = webViewEventHandlers.onMessage;
|
||||
const onMessage = useCallback((event: OnMessageEvent) => {
|
||||
const data = event.nativeEvent.data;
|
||||
if (typeof data === 'string' && data.startsWith('error:')) {
|
||||
logger.error(data);
|
||||
return;
|
||||
}
|
||||
|
||||
onWebViewMessage(event);
|
||||
}, [onWebViewMessage]);
|
||||
|
||||
return (
|
||||
<ExtendedWebView
|
||||
html={html}
|
||||
injectedJavaScript={pageSetup.js}
|
||||
allowFileAccessFromJs={true}
|
||||
onMessage={onMessage}
|
||||
onLoadEnd={webViewEventHandlers.onLoadEnd}
|
||||
onError={onError}
|
||||
ref={webViewRef}
|
||||
webviewInstanceId={'image-editor-js-draw'}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageEditor;
|
||||
@@ -0,0 +1,26 @@
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import shim from '@joplin/lib/shim';
|
||||
|
||||
export const autosaveFilename = 'autosaved-drawing.joplin.svg';
|
||||
|
||||
const logger = Logger.create('ImageEditor/autosave');
|
||||
|
||||
export const getAutosaveFilepath = () => {
|
||||
return `${Setting.value('resourceDir')}/${autosaveFilename}`;
|
||||
};
|
||||
|
||||
export const writeAutosave = async (data: string) => {
|
||||
const filePath = getAutosaveFilepath();
|
||||
logger.info(`Auto-saving drawing to ${JSON.stringify(filePath)}`);
|
||||
|
||||
await shim.fsDriver().writeFile(filePath, data, 'utf8');
|
||||
};
|
||||
|
||||
export const readAutosave = async (): Promise<string|null> => {
|
||||
return await shim.fsDriver().readFile(getAutosaveFilepath());
|
||||
};
|
||||
|
||||
export const clearAutosave = async () => {
|
||||
await shim.fsDriver().remove(getAutosaveFilepath());
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
|
||||
const isEditableResource = (resourceMime: string) => {
|
||||
return resourceMime === 'image/svg+xml';
|
||||
};
|
||||
|
||||
export default isEditableResource;
|
||||
@@ -0,0 +1,37 @@
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { Alert } from 'react-native';
|
||||
import { clearAutosave, getAutosaveFilepath, readAutosave } from './autosave';
|
||||
|
||||
export type RestoreAutosaveCallback = (data: string)=> void;
|
||||
|
||||
const promptRestoreAutosave = async (onRestoreAutosave: RestoreAutosaveCallback) => {
|
||||
const autosavePath = getAutosaveFilepath();
|
||||
|
||||
if (await shim.fsDriver().exists(autosavePath)) {
|
||||
const title: string|null = null;
|
||||
const message = _(
|
||||
'An autosaved drawing was found. Attach a copy of it to the note?',
|
||||
);
|
||||
|
||||
Alert.alert(title, message, [
|
||||
{
|
||||
text: _('Discard'),
|
||||
onPress: async () => {
|
||||
await clearAutosave();
|
||||
},
|
||||
},
|
||||
{
|
||||
text: _('Attach'),
|
||||
onPress: async () => {
|
||||
const autosaveData = await readAutosave();
|
||||
await clearAutosave();
|
||||
|
||||
onRestoreAutosave(autosaveData);
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
export default promptRestoreAutosave;
|
||||
在新工单中引用
屏蔽一个用户