mirror of
https://github.com/laurent22/joplin.git
synced 2025-01-26 18:58:21 +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) {
|
if (this.props.noteId) {
|
||||||
note = await Note.load(this.props.noteId);
|
note = await Note.load(this.props.noteId);
|
||||||
noteTags = this.props.noteTags || [];
|
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;
|
const folder = note ? Folder.byId(this.props.folders, note.parent_id) : null;
|
||||||
@ -612,10 +615,7 @@ class NoteTextComponent extends React.Component {
|
|||||||
}, 10);
|
}, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (note && note.body && Setting.value('sync.resourceDownloadMode') === 'auto') {
|
await this.handleResourceDownloadMode(note);
|
||||||
const resourceIds = await Note.linkedResourceIds(note.body);
|
|
||||||
await ResourceFetcher.instance().markForDownload(resourceIds);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (note) {
|
if (note) {
|
||||||
@ -655,6 +655,13 @@ class NoteTextComponent extends React.Component {
|
|||||||
defer();
|
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) {
|
async UNSAFE_componentWillReceiveProps(nextProps) {
|
||||||
if ('noteId' in nextProps && nextProps.noteId !== this.props.noteId) {
|
if ('noteId' in nextProps && nextProps.noteId !== this.props.noteId) {
|
||||||
await this.scheduleReloadNote(nextProps);
|
await this.scheduleReloadNote(nextProps);
|
||||||
|
@ -20,11 +20,14 @@ const { MarkupToHtml } = require('lib/joplin-renderer');
|
|||||||
const HtmlToMd = require('lib/HtmlToMd');
|
const HtmlToMd = require('lib/HtmlToMd');
|
||||||
const { _ } = require('lib/locale');
|
const { _ } = require('lib/locale');
|
||||||
const Note = require('lib/models/Note.js');
|
const Note = require('lib/models/Note.js');
|
||||||
|
const BaseModel = require('lib/BaseModel.js');
|
||||||
const Resource = require('lib/models/Resource.js');
|
const Resource = require('lib/models/Resource.js');
|
||||||
const { shim } = require('lib/shim');
|
const { shim } = require('lib/shim');
|
||||||
const TemplateUtils = require('lib/TemplateUtils');
|
const TemplateUtils = require('lib/TemplateUtils');
|
||||||
const { bridge } = require('electron').remote.require('./bridge');
|
const { bridge } = require('electron').remote.require('./bridge');
|
||||||
const { urlDecode } = require('lib/string-utils');
|
const { urlDecode } = require('lib/string-utils');
|
||||||
|
const ResourceFetcher = require('lib/services/ResourceFetcher.js');
|
||||||
|
const DecryptionWorker = require('lib/services/DecryptionWorker.js');
|
||||||
|
|
||||||
interface NoteTextProps {
|
interface NoteTextProps {
|
||||||
style: any,
|
style: any,
|
||||||
@ -139,7 +142,7 @@ function usePrevious(value:any):any {
|
|||||||
return ref.current;
|
return ref.current;
|
||||||
}
|
}
|
||||||
|
|
||||||
function initNoteState(n:any, setFormNote:Function, setDefaultEditorState:Function) {
|
async function initNoteState(n:any, setFormNote:Function, setDefaultEditorState:Function) {
|
||||||
let originalCss = '';
|
let originalCss = '';
|
||||||
if (n.markup_language === MarkupToHtml.MARKUP_LANGUAGE_HTML) {
|
if (n.markup_language === MarkupToHtml.MARKUP_LANGUAGE_HTML) {
|
||||||
const htmlToHtml = new HtmlToHtml();
|
const htmlToHtml = new HtmlToHtml();
|
||||||
@ -163,7 +166,17 @@ function initNoteState(n:any, setFormNote:Function, setDefaultEditorState:Functi
|
|||||||
setDefaultEditorState({
|
setDefaultEditorState({
|
||||||
value: n.body,
|
value: n.body,
|
||||||
markupLanguage: n.markup_language,
|
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> {
|
async function htmlToMarkdown(html:string):Promise<string> {
|
||||||
@ -192,6 +205,52 @@ async function formNoteToNote(formNote:FormNote):Promise<any> {
|
|||||||
return newNote;
|
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() {
|
async function attachResources() {
|
||||||
const filePaths = bridge().showOpenDialog({
|
const filePaths = bridge().showOpenDialog({
|
||||||
properties: ['openFile', 'createDirectory', 'multiSelections'],
|
properties: ['openFile', 'createDirectory', 'multiSelections'],
|
||||||
@ -309,7 +368,7 @@ function useWindowCommand(windowCommand:any, dispatch:Function, formNote:FormNot
|
|||||||
|
|
||||||
function NoteText2(props:NoteTextProps) {
|
function NoteText2(props:NoteTextProps) {
|
||||||
const [formNote, setFormNote] = useState<FormNote>(defaultNote());
|
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 prevSyncStarted = usePrevious(props.syncStarted);
|
||||||
|
|
||||||
const editorRef = useRef<any>();
|
const editorRef = useRef<any>();
|
||||||
@ -384,6 +443,28 @@ function NoteText2(props:NoteTextProps) {
|
|||||||
}
|
}
|
||||||
}, [props.isProvisional, formNote.id]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
// This is not exactly a hack but a bit ugly. If the note was changed (willChangeId > 0) but not
|
// 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
|
// 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
initNoteState(n, setFormNote, setDefaultEditorState);
|
await initNoteState(n, setFormNote, setDefaultEditorState);
|
||||||
};
|
};
|
||||||
|
|
||||||
loadNote();
|
loadNote();
|
||||||
@ -462,7 +543,7 @@ function NoteText2(props:NoteTextProps) {
|
|||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
if (!n) throw new Error(`Cannot find note with ID: ${props.noteId}`);
|
if (!n) throw new Error(`Cannot find note with ID: ${props.noteId}`);
|
||||||
reg.logger().debug('Loaded note:', n);
|
reg.logger().debug('Loaded note:', n);
|
||||||
initNoteState(n, setFormNote, setDefaultEditorState);
|
await initNoteState(n, setFormNote, setDefaultEditorState);
|
||||||
|
|
||||||
handleAutoFocus(!!n.is_todo);
|
handleAutoFocus(!!n.is_todo);
|
||||||
}
|
}
|
||||||
@ -675,6 +756,7 @@ function NoteText2(props:NoteTextProps) {
|
|||||||
attachResources: attachResources,
|
attachResources: attachResources,
|
||||||
disabled: waitingToSaveNote,
|
disabled: waitingToSaveNote,
|
||||||
joplinHtml: joplinHtml,
|
joplinHtml: joplinHtml,
|
||||||
|
theme: props.theme,
|
||||||
};
|
};
|
||||||
|
|
||||||
let editor = null;
|
let editor = null;
|
||||||
|
@ -2,14 +2,17 @@ import * as React from 'react';
|
|||||||
import { useState, useEffect, useCallback, useRef, forwardRef, useImperativeHandle } from 'react';
|
import { useState, useEffect, useCallback, useRef, forwardRef, useImperativeHandle } from 'react';
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
// 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 { MarkupToHtml } = require('lib/joplin-renderer');
|
||||||
const taboverride = require('taboverride');
|
const taboverride = require('taboverride');
|
||||||
const { reg } = require('lib/registry.js');
|
const { reg } = require('lib/registry.js');
|
||||||
|
const { _ } = require('lib/locale');
|
||||||
|
const { themeStyle, buildStyle } = require('../../theme.js');
|
||||||
|
|
||||||
interface TinyMCEProps {
|
interface TinyMCEProps {
|
||||||
style: any,
|
style: any,
|
||||||
|
theme: number,
|
||||||
onChange(event: OnChangeEvent): void,
|
onChange(event: OnChangeEvent): void,
|
||||||
onWillChange(event:any): void,
|
onWillChange(event:any): void,
|
||||||
onMessage(event:any): void,
|
onMessage(event:any): void,
|
||||||
@ -95,6 +98,30 @@ const joplinCommandToTinyMceCommands:JoplinCommandToTinyMceCommands = {
|
|||||||
'search': { name: 'SearchReplace' },
|
'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 loadedAssetFiles_:string[] = [];
|
||||||
let dispatchDidUpdateIID_:any = null;
|
let dispatchDidUpdateIID_:any = null;
|
||||||
let changeId_:number = 1;
|
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 rootIdRef = useRef<string>(`tinymce-${Date.now()}${Math.round(Math.random() * 10000)}`);
|
||||||
|
|
||||||
|
const styles = styles_(props);
|
||||||
|
const theme = themeStyle(props.theme);
|
||||||
|
|
||||||
const dispatchDidUpdate = (editor:any) => {
|
const dispatchDidUpdate = (editor:any) => {
|
||||||
if (dispatchDidUpdateIID_) clearTimeout(dispatchDidUpdateIID_);
|
if (dispatchDidUpdateIID_) clearTimeout(dispatchDidUpdateIID_);
|
||||||
dispatchDidUpdateIID_ = setTimeout(() => {
|
dispatchDidUpdateIID_ = setTimeout(() => {
|
||||||
@ -425,6 +455,11 @@ const TinyMCE = (props:TinyMCEProps, ref:any) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!editor) return () => {};
|
if (!editor) return () => {};
|
||||||
|
|
||||||
|
if (resourcesStatus(props.defaultEditorState.resourceInfos) !== 'ready') {
|
||||||
|
editor.setContent('');
|
||||||
|
return () => {};
|
||||||
|
}
|
||||||
|
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
|
||||||
const loadContent = async () => {
|
const loadContent = async () => {
|
||||||
@ -561,7 +596,25 @@ const TinyMCE = (props:TinyMCEProps, ref:any) => {
|
|||||||
};
|
};
|
||||||
}, [props.onWillChange, props.onChange, editor]);
|
}, [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);
|
export default forwardRef(TinyMCE);
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
|
const joplinRendererUtils = require('lib/joplin-renderer').utils;
|
||||||
|
const Resource = require('lib/models/Resource');
|
||||||
|
|
||||||
export interface DefaultEditorState {
|
export interface DefaultEditorState {
|
||||||
value: string,
|
value: string,
|
||||||
markupLanguage: number, // MarkupToHtml.MARKUP_LANGUAGE_XXX
|
markupLanguage: number, // MarkupToHtml.MARKUP_LANGUAGE_XXX
|
||||||
|
resourceInfos: any,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OnChangeEvent {
|
export interface OnChangeEvent {
|
||||||
@ -16,3 +20,13 @@ export interface EditorCommand {
|
|||||||
name: string,
|
name: string,
|
||||||
value: any,
|
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 }
|
{ color: style.color2 }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
style.textStyleMinor = Object.assign({}, style.textStyle,
|
||||||
|
{
|
||||||
|
color: style.colorFaded,
|
||||||
|
fontSize: style.fontSize * 0.8,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
style.urlStyle = Object.assign({}, style.textStyle,
|
style.urlStyle = Object.assign({}, style.textStyle,
|
||||||
{
|
{
|
||||||
textDecoration: 'underline',
|
textDecoration: 'underline',
|
||||||
|
@ -63,18 +63,38 @@ utils.loaderImage = function() {
|
|||||||
`;
|
`;
|
||||||
};
|
};
|
||||||
|
|
||||||
utils.resourceStatusImage = function(state) {
|
utils.resourceStatusImage = function(status) {
|
||||||
if (state === 'notDownloaded') return utils.notDownloadedResource();
|
if (status === 'notDownloaded') return utils.notDownloadedResource();
|
||||||
return utils.resourceStatusFile(state);
|
return utils.resourceStatusFile(status);
|
||||||
};
|
};
|
||||||
|
|
||||||
utils.resourceStatusFile = function(state) {
|
utils.resourceStatusFile = function(status) {
|
||||||
if (state === 'notDownloaded') return utils.notDownloadedResource();
|
if (status === 'notDownloaded') return utils.notDownloadedResource();
|
||||||
if (state === 'downloading') return utils.loaderImage();
|
if (status === 'downloading') return utils.loaderImage();
|
||||||
if (state === 'encrypted') return utils.loaderImage();
|
if (status === 'encrypted') return utils.loaderImage();
|
||||||
if (state === 'error') return utils.errorImage();
|
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) {
|
utils.resourceStatus = function(ResourceModel, resourceInfo) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user