mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-24 10:27:10 +02:00
Add support for editable resources
This commit is contained in:
parent
a8b8da4732
commit
e43e3c198a
@ -96,6 +96,7 @@ ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/fence.js
|
|||||||
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/mermaid.js
|
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/mermaid.js
|
||||||
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/sanitize_html.js
|
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/sanitize_html.js
|
||||||
ReactNativeClient/lib/JoplinServerApi.js
|
ReactNativeClient/lib/JoplinServerApi.js
|
||||||
|
ReactNativeClient/lib/services/ResourceEditWatcher.js
|
||||||
ReactNativeClient/PluginAssetsLoader.js
|
ReactNativeClient/PluginAssetsLoader.js
|
||||||
ReactNativeClient/setUpQuickActions.js
|
ReactNativeClient/setUpQuickActions.js
|
||||||
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD
|
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -86,6 +86,7 @@ ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/fence.js
|
|||||||
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/mermaid.js
|
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/mermaid.js
|
||||||
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/sanitize_html.js
|
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/sanitize_html.js
|
||||||
ReactNativeClient/lib/JoplinServerApi.js
|
ReactNativeClient/lib/JoplinServerApi.js
|
||||||
|
ReactNativeClient/lib/services/ResourceEditWatcher.js
|
||||||
ReactNativeClient/PluginAssetsLoader.js
|
ReactNativeClient/PluginAssetsLoader.js
|
||||||
ReactNativeClient/setUpQuickActions.js
|
ReactNativeClient/setUpQuickActions.js
|
||||||
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD
|
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD
|
||||||
|
@ -23,6 +23,7 @@ const InteropServiceHelper = require('./InteropServiceHelper.js');
|
|||||||
const ResourceService = require('lib/services/ResourceService');
|
const ResourceService = require('lib/services/ResourceService');
|
||||||
const ClipperServer = require('lib/ClipperServer');
|
const ClipperServer = require('lib/ClipperServer');
|
||||||
const ExternalEditWatcher = require('lib/services/ExternalEditWatcher');
|
const ExternalEditWatcher = require('lib/services/ExternalEditWatcher');
|
||||||
|
const ResourceEditWatcher = require('lib/services/ResourceEditWatcher').default;
|
||||||
const { bridge } = require('electron').remote.require('./bridge');
|
const { bridge } = require('electron').remote.require('./bridge');
|
||||||
const { shell, webFrame, clipboard } = require('electron');
|
const { shell, webFrame, clipboard } = require('electron');
|
||||||
const Menu = bridge().Menu;
|
const Menu = bridge().Menu;
|
||||||
@ -1505,6 +1506,8 @@ class Application extends BaseApplication {
|
|||||||
ExternalEditWatcher.instance().setLogger(reg.logger());
|
ExternalEditWatcher.instance().setLogger(reg.logger());
|
||||||
ExternalEditWatcher.instance().dispatch = this.store().dispatch;
|
ExternalEditWatcher.instance().dispatch = this.store().dispatch;
|
||||||
|
|
||||||
|
ResourceEditWatcher.instance().initialize(reg.logger(), this.store().dispatch);
|
||||||
|
|
||||||
RevisionService.instance().runInBackground();
|
RevisionService.instance().runInBackground();
|
||||||
|
|
||||||
this.updateMenuItemStates();
|
this.updateMenuItemStates();
|
||||||
|
@ -158,7 +158,10 @@ const TinyMCE = (props:NoteBodyEditorProps, ref:any) => {
|
|||||||
const markupToHtml = useRef(null);
|
const markupToHtml = useRef(null);
|
||||||
markupToHtml.current = props.markupToHtml;
|
markupToHtml.current = props.markupToHtml;
|
||||||
|
|
||||||
const lastOnChangeEventContent = useRef<string>('');
|
const lastOnChangeEventInfo = useRef<any>({
|
||||||
|
content: null,
|
||||||
|
resourceInfos: null,
|
||||||
|
});
|
||||||
|
|
||||||
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 editorRef = useRef<any>(null);
|
const editorRef = useRef<any>(null);
|
||||||
@ -761,10 +764,17 @@ const TinyMCE = (props:NoteBodyEditorProps, ref:any) => {
|
|||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
|
||||||
const loadContent = async () => {
|
const loadContent = async () => {
|
||||||
if (lastOnChangeEventContent.current !== props.content) {
|
if (lastOnChangeEventInfo.current.content !== props.content || lastOnChangeEventInfo.current.resourceInfos !== props.resourceInfos) {
|
||||||
|
console.info('RELOAD CONTENT');
|
||||||
|
|
||||||
const result = await props.markupToHtml(props.contentMarkupLanguage, props.content, markupRenderOptions({ resourceInfos: props.resourceInfos }));
|
const result = await props.markupToHtml(props.contentMarkupLanguage, props.content, markupRenderOptions({ resourceInfos: props.resourceInfos }));
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
lastOnChangeEventContent.current = props.content;
|
|
||||||
|
lastOnChangeEventInfo.current = {
|
||||||
|
content: props.content,
|
||||||
|
resourceInfos: props.resourceInfos,
|
||||||
|
};
|
||||||
|
|
||||||
editor.setContent(result.html);
|
editor.setContent(result.html);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -859,7 +869,7 @@ const TinyMCE = (props:NoteBodyEditorProps, ref:any) => {
|
|||||||
|
|
||||||
if (!editor) return;
|
if (!editor) return;
|
||||||
|
|
||||||
lastOnChangeEventContent.current = contentMd;
|
lastOnChangeEventInfo.current.content = contentMd;
|
||||||
|
|
||||||
props_onChangeRef.current({
|
props_onChangeRef.current({
|
||||||
changeId: changeId,
|
changeId: changeId,
|
||||||
|
@ -7,6 +7,8 @@ const { clipboard } = require('electron');
|
|||||||
const { toSystemSlashes } = require('lib/path-utils');
|
const { toSystemSlashes } = require('lib/path-utils');
|
||||||
const { _ } = require('lib/locale');
|
const { _ } = require('lib/locale');
|
||||||
|
|
||||||
|
import ResourceEditWatcher from '../../../lib/services/ResourceEditWatcher';
|
||||||
|
|
||||||
export enum ContextMenuItemType {
|
export enum ContextMenuItemType {
|
||||||
None = '',
|
None = '',
|
||||||
Image = 'image',
|
Image = 'image',
|
||||||
@ -42,9 +44,12 @@ export function menuItems():ContextMenuItems {
|
|||||||
open: {
|
open: {
|
||||||
label: _('Open...'),
|
label: _('Open...'),
|
||||||
onAction: async (options:ContextMenuOptions) => {
|
onAction: async (options:ContextMenuOptions) => {
|
||||||
const { resourcePath } = await resourceInfo(options);
|
try {
|
||||||
const ok = bridge().openExternal(`file://${resourcePath}`);
|
await ResourceEditWatcher.instance().openAndWatch(options.resourceId);
|
||||||
if (!ok) bridge().showErrorMessageBox(_('This file could not be opened: %s', resourcePath));
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
bridge().showErrorMessageBox(error.message);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
isActive: (itemType:ContextMenuItemType) => itemType === ContextMenuItemType.Image || itemType === ContextMenuItemType.Resource,
|
isActive: (itemType:ContextMenuItemType) => itemType === ContextMenuItemType.Image || itemType === ContextMenuItemType.Resource,
|
||||||
},
|
},
|
||||||
|
@ -11,6 +11,7 @@ const Setting = require('lib/models/Setting');
|
|||||||
const { reg } = require('lib/registry.js');
|
const { reg } = require('lib/registry.js');
|
||||||
const ResourceFetcher = require('lib/services/ResourceFetcher.js');
|
const ResourceFetcher = require('lib/services/ResourceFetcher.js');
|
||||||
const DecryptionWorker = require('lib/services/DecryptionWorker.js');
|
const DecryptionWorker = require('lib/services/DecryptionWorker.js');
|
||||||
|
const ResourceEditWatcher = require('lib/services/ResourceEditWatcher.js').default;
|
||||||
|
|
||||||
export interface OnLoadEvent {
|
export interface OnLoadEvent {
|
||||||
formNote: FormNote,
|
formNote: FormNote,
|
||||||
@ -30,12 +31,14 @@ function installResourceChangeHandler(onResourceChangeHandler: Function) {
|
|||||||
ResourceFetcher.instance().on('downloadComplete', onResourceChangeHandler);
|
ResourceFetcher.instance().on('downloadComplete', onResourceChangeHandler);
|
||||||
ResourceFetcher.instance().on('downloadStarted', onResourceChangeHandler);
|
ResourceFetcher.instance().on('downloadStarted', onResourceChangeHandler);
|
||||||
DecryptionWorker.instance().on('resourceDecrypted', onResourceChangeHandler);
|
DecryptionWorker.instance().on('resourceDecrypted', onResourceChangeHandler);
|
||||||
|
ResourceEditWatcher.instance().on('resourceChange', onResourceChangeHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
function uninstallResourceChangeHandler(onResourceChangeHandler: Function) {
|
function uninstallResourceChangeHandler(onResourceChangeHandler: Function) {
|
||||||
ResourceFetcher.instance().off('downloadComplete', onResourceChangeHandler);
|
ResourceFetcher.instance().off('downloadComplete', onResourceChangeHandler);
|
||||||
ResourceFetcher.instance().off('downloadStarted', onResourceChangeHandler);
|
ResourceFetcher.instance().off('downloadStarted', onResourceChangeHandler);
|
||||||
DecryptionWorker.instance().off('resourceDecrypted', onResourceChangeHandler);
|
DecryptionWorker.instance().off('resourceDecrypted', onResourceChangeHandler);
|
||||||
|
ResourceEditWatcher.instance().off('resourceChange', onResourceChangeHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function useFormNote(dependencies:HookDependencies) {
|
export default function useFormNote(dependencies:HookDependencies) {
|
||||||
|
@ -10,6 +10,7 @@ const { urlDecode } = require('lib/string-utils');
|
|||||||
const urlUtils = require('lib/urlUtils');
|
const urlUtils = require('lib/urlUtils');
|
||||||
const ResourceFetcher = require('lib/services/ResourceFetcher.js');
|
const ResourceFetcher = require('lib/services/ResourceFetcher.js');
|
||||||
const { reg } = require('lib/registry.js');
|
const { reg } = require('lib/registry.js');
|
||||||
|
import ResourceEditWatcher from '../../../lib/services/ResourceEditWatcher';
|
||||||
|
|
||||||
export default function useMessageHandler(scrollWhenReady:any, setScrollWhenReady:Function, editorRef:any, setLocalSearchResultCount:Function, dispatch:Function, formNote:FormNote) {
|
export default function useMessageHandler(scrollWhenReady:any, setScrollWhenReady:Function, editorRef:any, setLocalSearchResultCount:Function, dispatch:Function, formNote:FormNote) {
|
||||||
return useCallback(async (event: any) => {
|
return useCallback(async (event: any) => {
|
||||||
@ -17,7 +18,7 @@ export default function useMessageHandler(scrollWhenReady:any, setScrollWhenRead
|
|||||||
const args = event.args;
|
const args = event.args;
|
||||||
const arg0 = args && args.length >= 1 ? args[0] : null;
|
const arg0 = args && args.length >= 1 ? args[0] : null;
|
||||||
|
|
||||||
if (msg !== 'percentScroll') console.info(`Got ipc-message: ${msg}`, args);
|
if (msg !== 'percentScroll') console.info(`Got ipc-message: ${msg}`, arg0);
|
||||||
|
|
||||||
if (msg.indexOf('error:') === 0) {
|
if (msg.indexOf('error:') === 0) {
|
||||||
const s = msg.split(':');
|
const s = msg.split(':');
|
||||||
@ -60,8 +61,13 @@ export default function useMessageHandler(scrollWhenReady:any, setScrollWhenRead
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const filePath = Resource.fullPath(item);
|
|
||||||
bridge().openItem(filePath);
|
try {
|
||||||
|
await ResourceEditWatcher.instance().openAndWatch(item.id);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
bridge().showErrorMessageBox(error.message);
|
||||||
|
}
|
||||||
} else if (item.type_ === BaseModel.TYPE_NOTE) {
|
} else if (item.type_ === BaseModel.TYPE_NOTE) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'FOLDER_AND_NOTE_SELECT',
|
type: 'FOLDER_AND_NOTE_SELECT',
|
||||||
|
@ -23,8 +23,9 @@ function installRule(markdownIt, mdOptions, ruleOptions) {
|
|||||||
let icon = '';
|
let icon = '';
|
||||||
let hrefAttr = '#';
|
let hrefAttr = '#';
|
||||||
let mime = '';
|
let mime = '';
|
||||||
|
let resourceId = '';
|
||||||
if (isResourceUrl) {
|
if (isResourceUrl) {
|
||||||
const resourceId = resourceHrefInfo.itemId;
|
resourceId = resourceHrefInfo.itemId;
|
||||||
|
|
||||||
const result = ruleOptions.resources[resourceId];
|
const result = ruleOptions.resources[resourceId];
|
||||||
const resourceStatus = utils.resourceStatus(ruleOptions.ResourceModel, result);
|
const resourceStatus = utils.resourceStatus(ruleOptions.ResourceModel, result);
|
||||||
@ -62,7 +63,7 @@ function installRule(markdownIt, mdOptions, ruleOptions) {
|
|||||||
// https://github.com/laurent22/joplin/issues/2030
|
// https://github.com/laurent22/joplin/issues/2030
|
||||||
href = href.replace(/'/g, '%27');
|
href = href.replace(/'/g, '%27');
|
||||||
|
|
||||||
let js = `${ruleOptions.postMessageSyntax}(${JSON.stringify(href)}); return false;`;
|
let js = `${ruleOptions.postMessageSyntax}(${JSON.stringify(href)}, { resourceId: ${JSON.stringify(resourceId)} }); return false;`;
|
||||||
if (hrefAttr.indexOf('#') === 0 && href.indexOf('#') === 0) js = ''; // If it's an internal anchor, don't add any JS since the webview is going to handle navigating to the right place
|
if (hrefAttr.indexOf('#') === 0 && href.indexOf('#') === 0) js = ''; // If it's an internal anchor, don't add any JS since the webview is going to handle navigating to the right place
|
||||||
|
|
||||||
if (ruleOptions.plainResourceRendering || pluginOptions.linkRenderingType === 2) {
|
if (ruleOptions.plainResourceRendering || pluginOptions.linkRenderingType === 2) {
|
||||||
|
@ -141,6 +141,7 @@ utils.imageReplacement = function(ResourceModel, src, resources, resourceBaseUrl
|
|||||||
if (ResourceModel.isSupportedImageMimeType(mime)) {
|
if (ResourceModel.isSupportedImageMimeType(mime)) {
|
||||||
let newSrc = `./${ResourceModel.filename(resource)}`;
|
let newSrc = `./${ResourceModel.filename(resource)}`;
|
||||||
if (resourceBaseUrl) newSrc = resourceBaseUrl + newSrc;
|
if (resourceBaseUrl) newSrc = resourceBaseUrl + newSrc;
|
||||||
|
newSrc += `?t=${resource.updated_time}`;
|
||||||
return {
|
return {
|
||||||
'data-resource-id': resource.id,
|
'data-resource-id': resource.id,
|
||||||
src: newSrc,
|
src: newSrc,
|
||||||
|
@ -149,7 +149,7 @@ class Note extends BaseItem {
|
|||||||
const id = resourceIds[i];
|
const id = resourceIds[i];
|
||||||
const resource = await Resource.load(id);
|
const resource = await Resource.load(id);
|
||||||
if (!resource) continue;
|
if (!resource) continue;
|
||||||
const resourcePath = options.useAbsolutePaths ? `file://${Resource.fullPath(resource)}` : Resource.relativePath(resource);
|
const resourcePath = options.useAbsolutePaths ? `${`file://${Resource.fullPath(resource)}` + '?t='}${resource.updated_time}` : Resource.relativePath(resource);
|
||||||
body = body.replace(new RegExp(`:/${id}`, 'gi'), markdownUtils.escapeLinkUrl(resourcePath));
|
body = body.replace(new RegExp(`:/${id}`, 'gi'), markdownUtils.escapeLinkUrl(resourcePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -174,7 +174,7 @@ class Note extends BaseItem {
|
|||||||
this.logger().info('replaceResourceExternalToInternalLinks', 'options:', options, 'pathsToTry:', pathsToTry, 'body:', body);
|
this.logger().info('replaceResourceExternalToInternalLinks', 'options:', options, 'pathsToTry:', pathsToTry, 'body:', body);
|
||||||
|
|
||||||
for (const basePath of pathsToTry) {
|
for (const basePath of pathsToTry) {
|
||||||
const reString = `${pregQuote(`${basePath}/`)}[a-zA-Z0-9.]+`;
|
const reString = `${pregQuote(`${basePath}/`)}[a-zA-Z0-9.]+\\?t=[0-9]+`;
|
||||||
const re = new RegExp(reString, 'gi');
|
const re = new RegExp(reString, 'gi');
|
||||||
body = body.replace(re, match => {
|
body = body.replace(re, match => {
|
||||||
const id = Resource.pathToId(match);
|
const id = Resource.pathToId(match);
|
||||||
|
@ -67,6 +67,7 @@ class Resource extends BaseItem {
|
|||||||
return Resource.fsDriver_;
|
return Resource.fsDriver_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DEPRECATED IN FAVOUR OF friendlySafeFilename()
|
||||||
static friendlyFilename(resource) {
|
static friendlyFilename(resource) {
|
||||||
let output = safeFilename(resource.title); // Make sure not to allow spaces or any special characters as it's not supported in HTTP headers
|
let output = safeFilename(resource.title); // Make sure not to allow spaces or any special characters as it's not supported in HTTP headers
|
||||||
if (!output) output = resource.id;
|
if (!output) output = resource.id;
|
||||||
@ -91,6 +92,15 @@ class Resource extends BaseItem {
|
|||||||
return resource.id + extension;
|
return resource.id + extension;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static friendlySafeFilename(resource) {
|
||||||
|
let ext = resource.extension;
|
||||||
|
if (!ext) ext = resource.mime ? mime.toFileExtension(resource.mime) : '';
|
||||||
|
const safeExt = ext ? pathUtils.safeFileExtension(ext).toLowerCase() : '';
|
||||||
|
let title = resource.title ? resource.title : resource.id;
|
||||||
|
if (safeExt && pathUtils.fileExtension(title).toLowerCase() === safeExt) title = pathUtils.filename(title);
|
||||||
|
return pathUtils.friendlySafeFilename(title) + (safeExt ? `.${safeExt}` : '');
|
||||||
|
}
|
||||||
|
|
||||||
static relativePath(resource, encryptedBlob = false) {
|
static relativePath(resource, encryptedBlob = false) {
|
||||||
return `${Setting.value('resourceDirName')}/${this.filename(resource, encryptedBlob)}`;
|
return `${Setting.value('resourceDirName')}/${this.filename(resource, encryptedBlob)}`;
|
||||||
}
|
}
|
||||||
|
160
ReactNativeClient/lib/services/ResourceEditWatcher.ts
Normal file
160
ReactNativeClient/lib/services/ResourceEditWatcher.ts
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
const { Logger } = require('lib/logger.js');
|
||||||
|
const Setting = require('lib/models/Setting');
|
||||||
|
const Resource = require('lib/models/Resource');
|
||||||
|
const { shim } = require('lib/shim');
|
||||||
|
const EventEmitter = require('events');
|
||||||
|
const chokidar = require('chokidar');
|
||||||
|
const { bridge } = require('electron').remote.require('./bridge');
|
||||||
|
const { _ } = require('lib/locale');
|
||||||
|
|
||||||
|
interface WatchedItem {
|
||||||
|
[key: string]: {
|
||||||
|
resourceId: string,
|
||||||
|
updatedTime: number,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ResourceEditWatcher {
|
||||||
|
|
||||||
|
private static instance_:ResourceEditWatcher;
|
||||||
|
|
||||||
|
private logger_:any;
|
||||||
|
// private dispatch:Function;
|
||||||
|
private watcher_:any;
|
||||||
|
private chokidar_:any;
|
||||||
|
private watchedItems_:WatchedItem = {};
|
||||||
|
private eventEmitter_:any;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.logger_ = new Logger();
|
||||||
|
// this.dispatch = () => {};
|
||||||
|
this.watcher_ = null;
|
||||||
|
this.chokidar_ = chokidar;
|
||||||
|
this.eventEmitter_ = new EventEmitter();
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize(logger:any/* , dispatch:Function*/) {
|
||||||
|
this.logger_ = logger;
|
||||||
|
// this.dispatch = dispatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
static instance() {
|
||||||
|
if (this.instance_) return this.instance_;
|
||||||
|
this.instance_ = new ResourceEditWatcher();
|
||||||
|
return this.instance_;
|
||||||
|
}
|
||||||
|
|
||||||
|
tempDir() {
|
||||||
|
return Setting.value('tempDir');
|
||||||
|
}
|
||||||
|
|
||||||
|
logger() {
|
||||||
|
return this.logger_;
|
||||||
|
}
|
||||||
|
|
||||||
|
on(eventName:string, callback:Function) {
|
||||||
|
return this.eventEmitter_.on(eventName, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
off(eventName:string, callback:Function) {
|
||||||
|
return this.eventEmitter_.removeListener(eventName, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
private watch(fileToWatch:string) {
|
||||||
|
if (!this.chokidar_) return;
|
||||||
|
|
||||||
|
if (!this.watcher_) {
|
||||||
|
this.watcher_ = this.chokidar_.watch(fileToWatch);
|
||||||
|
this.watcher_.on('all', async (event:any, path:string) => {
|
||||||
|
this.logger().info(`ResourceEditWatcher: Event: ${event}: ${path}`);
|
||||||
|
|
||||||
|
if (event === 'unlink') {
|
||||||
|
// File are unwatched in the stopWatching functions below. When we receive an unlink event
|
||||||
|
// here it might be that the file is quickly moved to a different location and replaced by
|
||||||
|
// another file with the same name, as it happens with emacs. So because of this
|
||||||
|
// we keep watching anyway.
|
||||||
|
// See: https://github.com/laurent22/joplin/issues/710#issuecomment-420997167
|
||||||
|
// this.watcher_.unwatch(path);
|
||||||
|
} else if (event === 'change') {
|
||||||
|
const watchedItem = this.watchedItems_[path];
|
||||||
|
const stat = await shim.fsDriver().stat(path);
|
||||||
|
const updatedTime = stat.mtime.getTime();
|
||||||
|
|
||||||
|
if (watchedItem.updatedTime === updatedTime) {
|
||||||
|
// chokidar is buggy and emits "change" events even when nothing has changed
|
||||||
|
// so double-check the modified time and skip processing if there's no change.
|
||||||
|
// In particular it emits two such events just after the file has been copied
|
||||||
|
// in openAndWatch().
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!watchedItem) {
|
||||||
|
this.logger().error(`ResourceEditWatcher: could not find IDs from path: ${path}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resourceId = watchedItem.resourceId;
|
||||||
|
|
||||||
|
await shim.updateResourceBlob(resourceId, path);
|
||||||
|
|
||||||
|
this.watchedItems_[path] = {
|
||||||
|
resourceId: resourceId,
|
||||||
|
updatedTime: updatedTime,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.eventEmitter_.emit('resourceChange', { id: resourceId });
|
||||||
|
|
||||||
|
// TODO: handle race conditions
|
||||||
|
} else if (event === 'error') {
|
||||||
|
this.logger().error('ResourceEditWatcher: error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Hack to support external watcher on some linux applications (gedit, gvim, etc)
|
||||||
|
// taken from https://github.com/paulmillr/chokidar/issues/591
|
||||||
|
// @ts-ignore Leave unused path variable
|
||||||
|
this.watcher_.on('raw', async (event:string, path:string, options:any) => {
|
||||||
|
if (event === 'rename') {
|
||||||
|
this.watcher_.unwatch(options.watchedPath);
|
||||||
|
this.watcher_.add(options.watchedPath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.watcher_.add(fileToWatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.watcher_;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async openAndWatch(resourceId:string) {
|
||||||
|
let editFilePath = this.resourceIdToPath(resourceId);
|
||||||
|
|
||||||
|
if (!editFilePath) {
|
||||||
|
const resource = await Resource.load(resourceId);
|
||||||
|
if (!(await Resource.isReady(resource))) throw new Error(_('This attachment is not downloaded or not decrypted yet'));
|
||||||
|
const sourceFilePath = Resource.fullPath(resource);
|
||||||
|
editFilePath = await shim.fsDriver().findUniqueFilename(`${this.tempDir()}/${Resource.friendlySafeFilename(resource)}`);
|
||||||
|
await shim.fsDriver().copy(sourceFilePath, editFilePath);
|
||||||
|
const stat = await shim.fsDriver().stat(editFilePath);
|
||||||
|
|
||||||
|
this.watchedItems_[editFilePath] = {
|
||||||
|
resourceId: resourceId,
|
||||||
|
updatedTime: stat.mtime.getTime(),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.watch(editFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
bridge().openItem(editFilePath);
|
||||||
|
|
||||||
|
this.logger().info(`ResourceEditWatcher: Started watching ${editFilePath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private resourceIdToPath(resourceId:string):string {
|
||||||
|
for (const path in this.watchedItems_) {
|
||||||
|
const item = this.watchedItems_[path];
|
||||||
|
if (item.resourceId === resourceId) return path;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -144,7 +144,7 @@ function shimInit() {
|
|||||||
|
|
||||||
shim.createResourceFromPath = async function(filePath, defaultProps = null, options = null) {
|
shim.createResourceFromPath = async function(filePath, defaultProps = null, options = null) {
|
||||||
options = Object.assign({
|
options = Object.assign({
|
||||||
resizeLargeImages: 'always', // 'always' or 'ask'
|
resizeLargeImages: 'always', // 'always', 'ask' or 'never'
|
||||||
}, options);
|
}, options);
|
||||||
|
|
||||||
const readChunk = require('read-chunk');
|
const readChunk = require('read-chunk');
|
||||||
@ -182,7 +182,7 @@ function shimInit() {
|
|||||||
|
|
||||||
const targetPath = Resource.fullPath(resource);
|
const targetPath = Resource.fullPath(resource);
|
||||||
|
|
||||||
if (['image/jpeg', 'image/jpg', 'image/png'].includes(resource.mime)) {
|
if (options.resizeLargeImages !== 'never' && ['image/jpeg', 'image/jpg', 'image/png'].includes(resource.mime)) {
|
||||||
const ok = await handleResizeImage_(filePath, targetPath, resource.mime, options.resizeLargeImages);
|
const ok = await handleResizeImage_(filePath, targetPath, resource.mime, options.resizeLargeImages);
|
||||||
if (!ok) return null;
|
if (!ok) return null;
|
||||||
} else {
|
} else {
|
||||||
@ -205,6 +205,17 @@ function shimInit() {
|
|||||||
return Resource.save(resource, { isNew: true });
|
return Resource.save(resource, { isNew: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
shim.updateResourceBlob = async function(resourceId, newBlobFilePath) {
|
||||||
|
const resource = await Resource.load(resourceId);
|
||||||
|
const fileStat = await shim.fsDriver().stat(newBlobFilePath);
|
||||||
|
await shim.fsDriver().copy(newBlobFilePath, Resource.fullPath(resource));
|
||||||
|
|
||||||
|
await Resource.save({
|
||||||
|
id: resource.id,
|
||||||
|
size: fileStat.size,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
shim.attachFileToNoteBody = async function(noteBody, filePath, position = null, options = null) {
|
shim.attachFileToNoteBody = async function(noteBody, filePath, position = null, options = null) {
|
||||||
options = Object.assign({}, {
|
options = Object.assign({}, {
|
||||||
createFileURL: false,
|
createFileURL: false,
|
||||||
|
Loading…
Reference in New Issue
Block a user