mirror of
https://github.com/laurent22/joplin.git
synced 2025-01-11 18:24:43 +02:00
Desktop: WYSIWYG: Handle resource download mode
This commit is contained in:
parent
0d736bcb58
commit
6bd0250ef8
@ -427,6 +427,9 @@ class NoteTextComponent extends React.Component {
|
||||
if (this.props.noteId) {
|
||||
note = await Note.load(this.props.noteId);
|
||||
noteTags = this.props.noteTags || [];
|
||||
await this.handleResourceDownloadMode(note);
|
||||
} else {
|
||||
console.warn('Trying to load a note with no ID - should no longer be possible');
|
||||
}
|
||||
|
||||
const folder = note ? Folder.byId(this.props.folders, note.parent_id) : null;
|
||||
@ -612,10 +615,7 @@ class NoteTextComponent extends React.Component {
|
||||
}, 10);
|
||||
}
|
||||
|
||||
if (note && note.body && Setting.value('sync.resourceDownloadMode') === 'auto') {
|
||||
const resourceIds = await Note.linkedResourceIds(note.body);
|
||||
await ResourceFetcher.instance().markForDownload(resourceIds);
|
||||
}
|
||||
await this.handleResourceDownloadMode(note);
|
||||
}
|
||||
|
||||
if (note) {
|
||||
@ -655,6 +655,13 @@ class NoteTextComponent extends React.Component {
|
||||
defer();
|
||||
}
|
||||
|
||||
async handleResourceDownloadMode(note) {
|
||||
if (note && note.body && Setting.value('sync.resourceDownloadMode') === 'auto') {
|
||||
const resourceIds = await Note.linkedResourceIds(note.body);
|
||||
await ResourceFetcher.instance().markForDownload(resourceIds);
|
||||
}
|
||||
}
|
||||
|
||||
async UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
if ('noteId' in nextProps && nextProps.noteId !== this.props.noteId) {
|
||||
await this.scheduleReloadNote(nextProps);
|
||||
|
@ -20,11 +20,14 @@ const { MarkupToHtml } = require('lib/joplin-renderer');
|
||||
const HtmlToMd = require('lib/HtmlToMd');
|
||||
const { _ } = require('lib/locale');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Resource = require('lib/models/Resource.js');
|
||||
const { shim } = require('lib/shim');
|
||||
const TemplateUtils = require('lib/TemplateUtils');
|
||||
const { bridge } = require('electron').remote.require('./bridge');
|
||||
const { urlDecode } = require('lib/string-utils');
|
||||
const ResourceFetcher = require('lib/services/ResourceFetcher.js');
|
||||
const DecryptionWorker = require('lib/services/DecryptionWorker.js');
|
||||
|
||||
interface NoteTextProps {
|
||||
style: any,
|
||||
@ -139,7 +142,7 @@ function usePrevious(value:any):any {
|
||||
return ref.current;
|
||||
}
|
||||
|
||||
function initNoteState(n:any, setFormNote:Function, setDefaultEditorState:Function) {
|
||||
async function initNoteState(n:any, setFormNote:Function, setDefaultEditorState:Function) {
|
||||
let originalCss = '';
|
||||
if (n.markup_language === MarkupToHtml.MARKUP_LANGUAGE_HTML) {
|
||||
const htmlToHtml = new HtmlToHtml();
|
||||
@ -163,7 +166,17 @@ function initNoteState(n:any, setFormNote:Function, setDefaultEditorState:Functi
|
||||
setDefaultEditorState({
|
||||
value: n.body,
|
||||
markupLanguage: n.markup_language,
|
||||
resourceInfos: await attachedResources(n.body),
|
||||
});
|
||||
|
||||
await handleResourceDownloadMode(n.body);
|
||||
}
|
||||
|
||||
async function handleResourceDownloadMode(noteBody:string) {
|
||||
if (noteBody && Setting.value('sync.resourceDownloadMode') === 'auto') {
|
||||
const resourceIds = await Note.linkedResourceIds(noteBody);
|
||||
await ResourceFetcher.instance().markForDownload(resourceIds);
|
||||
}
|
||||
}
|
||||
|
||||
async function htmlToMarkdown(html:string):Promise<string> {
|
||||
@ -192,6 +205,52 @@ async function formNoteToNote(formNote:FormNote):Promise<any> {
|
||||
return newNote;
|
||||
}
|
||||
|
||||
let resourceCache_:any = {};
|
||||
|
||||
function clearResourceCache() {
|
||||
resourceCache_ = {};
|
||||
}
|
||||
|
||||
async function attachedResources(noteBody:string):Promise<any> {
|
||||
if (!noteBody) return {};
|
||||
const resourceIds = await Note.linkedItemIdsByType(BaseModel.TYPE_RESOURCE, noteBody);
|
||||
|
||||
const output:any = {};
|
||||
for (let i = 0; i < resourceIds.length; i++) {
|
||||
const id = resourceIds[i];
|
||||
|
||||
if (resourceCache_[id]) {
|
||||
output[id] = resourceCache_[id];
|
||||
} else {
|
||||
const resource = await Resource.load(id);
|
||||
const localState = await Resource.localState(resource);
|
||||
|
||||
const o = {
|
||||
item: resource,
|
||||
localState: localState,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
resourceCache_[id] = o;
|
||||
output[id] = o;
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
function installResourceHandling(refreshResourceHandler:Function) {
|
||||
ResourceFetcher.instance().on('downloadComplete', refreshResourceHandler);
|
||||
ResourceFetcher.instance().on('downloadStarted', refreshResourceHandler);
|
||||
DecryptionWorker.instance().on('resourceDecrypted', refreshResourceHandler);
|
||||
}
|
||||
|
||||
function uninstallResourceHandling(refreshResourceHandler:Function) {
|
||||
ResourceFetcher.instance().off('downloadComplete', refreshResourceHandler);
|
||||
ResourceFetcher.instance().off('downloadStarted', refreshResourceHandler);
|
||||
DecryptionWorker.instance().off('resourceDecrypted', refreshResourceHandler);
|
||||
}
|
||||
|
||||
async function attachResources() {
|
||||
const filePaths = bridge().showOpenDialog({
|
||||
properties: ['openFile', 'createDirectory', 'multiSelections'],
|
||||
@ -309,7 +368,7 @@ function useWindowCommand(windowCommand:any, dispatch:Function, formNote:FormNot
|
||||
|
||||
function NoteText2(props:NoteTextProps) {
|
||||
const [formNote, setFormNote] = useState<FormNote>(defaultNote());
|
||||
const [defaultEditorState, setDefaultEditorState] = useState<DefaultEditorState>({ value: '', markupLanguage: MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN });
|
||||
const [defaultEditorState, setDefaultEditorState] = useState<DefaultEditorState>({ value: '', markupLanguage: MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN, resourceInfos: {} });
|
||||
const prevSyncStarted = usePrevious(props.syncStarted);
|
||||
|
||||
const editorRef = useRef<any>();
|
||||
@ -384,6 +443,28 @@ function NoteText2(props:NoteTextProps) {
|
||||
}
|
||||
}, [props.isProvisional, formNote.id]);
|
||||
|
||||
const refreshResource = useCallback(async function(event) {
|
||||
if (!defaultEditorState.value) return;
|
||||
|
||||
const resourceIds = await Note.linkedResourceIds(defaultEditorState.value);
|
||||
if (resourceIds.indexOf(event.id) >= 0) {
|
||||
clearResourceCache();
|
||||
const e = {
|
||||
...defaultEditorState,
|
||||
resourceInfos: await attachedResources(defaultEditorState.value),
|
||||
};
|
||||
setDefaultEditorState(e);
|
||||
}
|
||||
}, [defaultEditorState]);
|
||||
|
||||
useEffect(() => {
|
||||
installResourceHandling(refreshResource);
|
||||
|
||||
return () => {
|
||||
uninstallResourceHandling(refreshResource);
|
||||
};
|
||||
}, [defaultEditorState]);
|
||||
|
||||
useEffect(() => {
|
||||
// This is not exactly a hack but a bit ugly. If the note was changed (willChangeId > 0) but not
|
||||
// yet saved, we need to save it now before the component is unmounted. However, we can't put
|
||||
@ -420,7 +501,7 @@ function NoteText2(props:NoteTextProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
initNoteState(n, setFormNote, setDefaultEditorState);
|
||||
await initNoteState(n, setFormNote, setDefaultEditorState);
|
||||
};
|
||||
|
||||
loadNote();
|
||||
@ -462,7 +543,7 @@ function NoteText2(props:NoteTextProps) {
|
||||
if (cancelled) return;
|
||||
if (!n) throw new Error(`Cannot find note with ID: ${props.noteId}`);
|
||||
reg.logger().debug('Loaded note:', n);
|
||||
initNoteState(n, setFormNote, setDefaultEditorState);
|
||||
await initNoteState(n, setFormNote, setDefaultEditorState);
|
||||
|
||||
handleAutoFocus(!!n.is_todo);
|
||||
}
|
||||
@ -675,6 +756,7 @@ function NoteText2(props:NoteTextProps) {
|
||||
attachResources: attachResources,
|
||||
disabled: waitingToSaveNote,
|
||||
joplinHtml: joplinHtml,
|
||||
theme: props.theme,
|
||||
};
|
||||
|
||||
let editor = null;
|
||||
|
@ -2,14 +2,17 @@ import * as React from 'react';
|
||||
import { useState, useEffect, useCallback, useRef, forwardRef, useImperativeHandle } from 'react';
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { DefaultEditorState, OnChangeEvent, TextEditorUtils, EditorCommand } from '../utils/NoteText';
|
||||
import { DefaultEditorState, OnChangeEvent, TextEditorUtils, EditorCommand, resourcesStatus } from '../utils/NoteText';
|
||||
|
||||
const { MarkupToHtml } = require('lib/joplin-renderer');
|
||||
const taboverride = require('taboverride');
|
||||
const { reg } = require('lib/registry.js');
|
||||
const { _ } = require('lib/locale');
|
||||
const { themeStyle, buildStyle } = require('../../theme.js');
|
||||
|
||||
interface TinyMCEProps {
|
||||
style: any,
|
||||
theme: number,
|
||||
onChange(event: OnChangeEvent): void,
|
||||
onWillChange(event:any): void,
|
||||
onMessage(event:any): void,
|
||||
@ -95,6 +98,30 @@ const joplinCommandToTinyMceCommands:JoplinCommandToTinyMceCommands = {
|
||||
'search': { name: 'SearchReplace' },
|
||||
};
|
||||
|
||||
function styles_(props:TinyMCEProps) {
|
||||
return buildStyle('TinyMCE', props.theme, (/* theme:any */) => {
|
||||
return {
|
||||
disabledOverlay: {
|
||||
zIndex: 10,
|
||||
position: 'absolute',
|
||||
backgroundColor: 'white',
|
||||
opacity: 0.7,
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
padding: 20,
|
||||
paddingTop: 50,
|
||||
textAlign: 'center',
|
||||
},
|
||||
rootStyle: {
|
||||
position: 'relative',
|
||||
...props.style,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
let loadedAssetFiles_:string[] = [];
|
||||
let dispatchDidUpdateIID_:any = null;
|
||||
let changeId_:number = 1;
|
||||
@ -114,6 +141,9 @@ const TinyMCE = (props:TinyMCEProps, ref:any) => {
|
||||
|
||||
const rootIdRef = useRef<string>(`tinymce-${Date.now()}${Math.round(Math.random() * 10000)}`);
|
||||
|
||||
const styles = styles_(props);
|
||||
const theme = themeStyle(props.theme);
|
||||
|
||||
const dispatchDidUpdate = (editor:any) => {
|
||||
if (dispatchDidUpdateIID_) clearTimeout(dispatchDidUpdateIID_);
|
||||
dispatchDidUpdateIID_ = setTimeout(() => {
|
||||
@ -425,6 +455,11 @@ const TinyMCE = (props:TinyMCEProps, ref:any) => {
|
||||
useEffect(() => {
|
||||
if (!editor) return () => {};
|
||||
|
||||
if (resourcesStatus(props.defaultEditorState.resourceInfos) !== 'ready') {
|
||||
editor.setContent('');
|
||||
return () => {};
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
const loadContent = async () => {
|
||||
@ -561,7 +596,25 @@ const TinyMCE = (props:TinyMCEProps, ref:any) => {
|
||||
};
|
||||
}, [props.onWillChange, props.onChange, editor]);
|
||||
|
||||
return <div style={props.style} id={rootIdRef.current}/>;
|
||||
function renderDisabledOverlay() {
|
||||
const status = resourcesStatus(props.defaultEditorState.resourceInfos);
|
||||
if (status === 'ready') return null;
|
||||
|
||||
const message = _('Please wait for all attachments to be downloaded and decrypted. You may also switch the layout and edit the note in Markdown mode.');
|
||||
return (
|
||||
<div style={styles.disabledOverlay}>
|
||||
<p style={theme.textStyle}>{message}</p>
|
||||
<p style={theme.textStyleMinor}>{`Status: ${status}`}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={styles.rootStyle}>
|
||||
{renderDisabledOverlay()}
|
||||
<div style={{ width: '100%', height: '100%' }} id={rootIdRef.current}/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default forwardRef(TinyMCE);
|
||||
|
@ -1,6 +1,10 @@
|
||||
const joplinRendererUtils = require('lib/joplin-renderer').utils;
|
||||
const Resource = require('lib/models/Resource');
|
||||
|
||||
export interface DefaultEditorState {
|
||||
value: string,
|
||||
markupLanguage: number, // MarkupToHtml.MARKUP_LANGUAGE_XXX
|
||||
resourceInfos: any,
|
||||
}
|
||||
|
||||
export interface OnChangeEvent {
|
||||
@ -16,3 +20,13 @@ export interface EditorCommand {
|
||||
name: string,
|
||||
value: any,
|
||||
}
|
||||
|
||||
export function resourcesStatus(resourceInfos:any) {
|
||||
let lowestIndex = joplinRendererUtils.resourceStatusIndex('ready');
|
||||
for (const id in resourceInfos) {
|
||||
const s = joplinRendererUtils.resourceStatus(Resource, resourceInfos[id]);
|
||||
const idx = joplinRendererUtils.resourceStatusIndex(s);
|
||||
if (idx < lowestIndex) lowestIndex = idx;
|
||||
}
|
||||
return joplinRendererUtils.resourceStatusName(lowestIndex);
|
||||
}
|
||||
|
@ -304,6 +304,13 @@ function addExtraStyles(style) {
|
||||
{ color: style.color2 }
|
||||
);
|
||||
|
||||
style.textStyleMinor = Object.assign({}, style.textStyle,
|
||||
{
|
||||
color: style.colorFaded,
|
||||
fontSize: style.fontSize * 0.8,
|
||||
},
|
||||
);
|
||||
|
||||
style.urlStyle = Object.assign({}, style.textStyle,
|
||||
{
|
||||
textDecoration: 'underline',
|
||||
|
@ -63,18 +63,38 @@ utils.loaderImage = function() {
|
||||
`;
|
||||
};
|
||||
|
||||
utils.resourceStatusImage = function(state) {
|
||||
if (state === 'notDownloaded') return utils.notDownloadedResource();
|
||||
return utils.resourceStatusFile(state);
|
||||
utils.resourceStatusImage = function(status) {
|
||||
if (status === 'notDownloaded') return utils.notDownloadedResource();
|
||||
return utils.resourceStatusFile(status);
|
||||
};
|
||||
|
||||
utils.resourceStatusFile = function(state) {
|
||||
if (state === 'notDownloaded') return utils.notDownloadedResource();
|
||||
if (state === 'downloading') return utils.loaderImage();
|
||||
if (state === 'encrypted') return utils.loaderImage();
|
||||
if (state === 'error') return utils.errorImage();
|
||||
utils.resourceStatusFile = function(status) {
|
||||
if (status === 'notDownloaded') return utils.notDownloadedResource();
|
||||
if (status === 'downloading') return utils.loaderImage();
|
||||
if (status === 'encrypted') return utils.loaderImage();
|
||||
if (status === 'error') return utils.errorImage();
|
||||
|
||||
throw new Error(`Unknown state: ${state}`);
|
||||
throw new Error(`Unknown status: ${status}`);
|
||||
};
|
||||
|
||||
utils.resourceStatusIndex = function(status) {
|
||||
if (status === 'error') return -1;
|
||||
if (status === 'notDownloaded') return 0;
|
||||
if (status === 'downloading') return 1;
|
||||
if (status === 'encrypted') return 2;
|
||||
if (status === 'ready') return 10;
|
||||
|
||||
throw new Error(`Unknown status: ${status}`);
|
||||
};
|
||||
|
||||
utils.resourceStatusName = function(index) {
|
||||
if (index === -1) return 'error';
|
||||
if (index === 0) return 'notDownloaded';
|
||||
if (index === 1) return 'downloading';
|
||||
if (index === 2) return 'encrypted';
|
||||
if (index === 10) return 'ready';
|
||||
|
||||
throw new Error(`Unknown index: ${index}`);
|
||||
};
|
||||
|
||||
utils.resourceStatus = function(ResourceModel, resourceInfo) {
|
||||
|
Loading…
Reference in New Issue
Block a user