1
0
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:
Laurent Cozic 2020-04-02 18:16:11 +01:00
parent 0d736bcb58
commit 6bd0250ef8
6 changed files with 202 additions and 19 deletions

View File

@ -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);

View File

@ -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;

View File

@ -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);

View File

@ -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);
}

View File

@ -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',

View File

@ -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) {