You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-07-06 23:56:13 +02:00
This commit is contained in:
@ -295,6 +295,7 @@ packages/app-desktop/gui/NoteEditor/utils/useNoteSearchBar.js
|
|||||||
packages/app-desktop/gui/NoteEditor/utils/usePluginEditorView.test.js
|
packages/app-desktop/gui/NoteEditor/utils/usePluginEditorView.test.js
|
||||||
packages/app-desktop/gui/NoteEditor/utils/usePluginEditorView.js
|
packages/app-desktop/gui/NoteEditor/utils/usePluginEditorView.js
|
||||||
packages/app-desktop/gui/NoteEditor/utils/usePluginServiceRegistration.js
|
packages/app-desktop/gui/NoteEditor/utils/usePluginServiceRegistration.js
|
||||||
|
packages/app-desktop/gui/NoteEditor/utils/useResourceUnwatcher.js
|
||||||
packages/app-desktop/gui/NoteEditor/utils/useScheduleSaveCallbacks.js
|
packages/app-desktop/gui/NoteEditor/utils/useScheduleSaveCallbacks.js
|
||||||
packages/app-desktop/gui/NoteEditor/utils/useScrollWhenReadyOptions.js
|
packages/app-desktop/gui/NoteEditor/utils/useScrollWhenReadyOptions.js
|
||||||
packages/app-desktop/gui/NoteEditor/utils/useSearchMarkers.js
|
packages/app-desktop/gui/NoteEditor/utils/useSearchMarkers.js
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -270,6 +270,7 @@ packages/app-desktop/gui/NoteEditor/utils/useNoteSearchBar.js
|
|||||||
packages/app-desktop/gui/NoteEditor/utils/usePluginEditorView.test.js
|
packages/app-desktop/gui/NoteEditor/utils/usePluginEditorView.test.js
|
||||||
packages/app-desktop/gui/NoteEditor/utils/usePluginEditorView.js
|
packages/app-desktop/gui/NoteEditor/utils/usePluginEditorView.js
|
||||||
packages/app-desktop/gui/NoteEditor/utils/usePluginServiceRegistration.js
|
packages/app-desktop/gui/NoteEditor/utils/usePluginServiceRegistration.js
|
||||||
|
packages/app-desktop/gui/NoteEditor/utils/useResourceUnwatcher.js
|
||||||
packages/app-desktop/gui/NoteEditor/utils/useScheduleSaveCallbacks.js
|
packages/app-desktop/gui/NoteEditor/utils/useScheduleSaveCallbacks.js
|
||||||
packages/app-desktop/gui/NoteEditor/utils/useScrollWhenReadyOptions.js
|
packages/app-desktop/gui/NoteEditor/utils/useScrollWhenReadyOptions.js
|
||||||
packages/app-desktop/gui/NoteEditor/utils/useSearchMarkers.js
|
packages/app-desktop/gui/NoteEditor/utils/useSearchMarkers.js
|
||||||
|
@ -40,6 +40,8 @@ export interface AppWindowState extends WindowState {
|
|||||||
visibleDialogs: VisibleDialogs;
|
visibleDialogs: VisibleDialogs;
|
||||||
dialogs: AppStateDialog[];
|
dialogs: AppStateDialog[];
|
||||||
devToolsVisible: boolean;
|
devToolsVisible: boolean;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
|
watchedResources: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BackgroundWindowStates {
|
interface BackgroundWindowStates {
|
||||||
@ -62,8 +64,6 @@ export interface AppState extends State, AppWindowState {
|
|||||||
modalOverlayMessage: string|null;
|
modalOverlayMessage: string|null;
|
||||||
|
|
||||||
// Extra reducer keys go here
|
// Extra reducer keys go here
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
|
||||||
watchedResources: any;
|
|
||||||
mainLayout: LayoutItem;
|
mainLayout: LayoutItem;
|
||||||
isResettingLayout: boolean;
|
isResettingLayout: boolean;
|
||||||
}
|
}
|
||||||
@ -76,6 +76,7 @@ export const createAppDefaultWindowState = (): AppWindowState => {
|
|||||||
noteVisiblePanes: ['editor', 'viewer'],
|
noteVisiblePanes: ['editor', 'viewer'],
|
||||||
editorCodeView: true,
|
editorCodeView: true,
|
||||||
devToolsVisible: false,
|
devToolsVisible: false,
|
||||||
|
watchedResources: {},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -653,6 +653,7 @@ class Application extends BaseApplication {
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
(action: any) => { this.store().dispatch(action); },
|
(action: any) => { this.store().dispatch(action); },
|
||||||
(path: string) => bridge().openItem(path),
|
(path: string) => bridge().openItem(path),
|
||||||
|
() => this.store().getState().windowId,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Forwards the local event to the global event manager, so that it can
|
// Forwards the local event to the global event manager, so that it can
|
||||||
|
@ -59,6 +59,7 @@ import { EditorActivationCheckFilterObject } from '@joplin/lib/services/plugins/
|
|||||||
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||||
import WebviewController from '@joplin/lib/services/plugins/WebviewController';
|
import WebviewController from '@joplin/lib/services/plugins/WebviewController';
|
||||||
import AsyncActionQueue, { IntervalType } from '@joplin/lib/AsyncActionQueue';
|
import AsyncActionQueue, { IntervalType } from '@joplin/lib/AsyncActionQueue';
|
||||||
|
import useResourceUnwatcher from './utils/useResourceUnwatcher';
|
||||||
|
|
||||||
const debounce = require('debounce');
|
const debounce = require('debounce');
|
||||||
|
|
||||||
@ -358,6 +359,8 @@ function NoteEditorContent(props: NoteEditorProps) {
|
|||||||
const windowId = useContext(WindowIdContext);
|
const windowId = useContext(WindowIdContext);
|
||||||
const onMessage = useMessageHandler(scrollWhenReady, clearScrollWhenReady, windowId, editorRef, setLocalSearchResultCount, props.dispatch, formNote, htmlToMarkdown, markupToHtml);
|
const onMessage = useMessageHandler(scrollWhenReady, clearScrollWhenReady, windowId, editorRef, setLocalSearchResultCount, props.dispatch, formNote, htmlToMarkdown, markupToHtml);
|
||||||
|
|
||||||
|
useResourceUnwatcher({ noteId: formNote.id, windowId });
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
const externalEditWatcher_noteChange = useCallback((event: any) => {
|
const externalEditWatcher_noteChange = useCallback((event: any) => {
|
||||||
if (event.id === formNote.id) {
|
if (event.id === formNote.id) {
|
||||||
@ -729,7 +732,7 @@ const mapStateToProps = (state: AppState, ownProps: ConnectProps) => {
|
|||||||
selectedSearchId: windowState.selectedSearchId,
|
selectedSearchId: windowState.selectedSearchId,
|
||||||
customCss: state.customViewerCss,
|
customCss: state.customViewerCss,
|
||||||
noteVisiblePanes: windowState.noteVisiblePanes,
|
noteVisiblePanes: windowState.noteVisiblePanes,
|
||||||
watchedResources: state.watchedResources,
|
watchedResources: windowState.watchedResources,
|
||||||
highlightedWords: state.highlightedWords,
|
highlightedWords: state.highlightedWords,
|
||||||
plugins: state.pluginService.plugins,
|
plugins: state.pluginService.plugins,
|
||||||
pluginHtmlContents: state.pluginService.pluginHtmlContents,
|
pluginHtmlContents: state.pluginService.pluginHtmlContents,
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
import ResourceEditWatcher from '@joplin/lib/services/ResourceEditWatcher';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
noteId: string;
|
||||||
|
windowId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useResourceUnwatcher = ({ noteId, windowId }: Props) => {
|
||||||
|
useEffect(() => {
|
||||||
|
// All resources associated with the current window should no longer be watched after:
|
||||||
|
// 1. The editor unloads, or
|
||||||
|
// 2. The note shown in the editor changes.
|
||||||
|
// Unwatching in a cleanup callback handles both cases.
|
||||||
|
return () => {
|
||||||
|
void ResourceEditWatcher.instance().stopWatchingAll(windowId);
|
||||||
|
};
|
||||||
|
}, [noteId, windowId]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useResourceUnwatcher;
|
@ -1,7 +1,6 @@
|
|||||||
import { RefObject, useCallback, useEffect, useRef, useState } from 'react';
|
import { RefObject, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { NoteBodyEditorRef, ScrollOptions, ScrollOptionTypes } from './types';
|
import { NoteBodyEditorRef, ScrollOptions, ScrollOptionTypes } from './types';
|
||||||
import usePrevious from '@joplin/lib/hooks/usePrevious';
|
import usePrevious from '@joplin/lib/hooks/usePrevious';
|
||||||
import ResourceEditWatcher from '@joplin/lib/services/ResourceEditWatcher';
|
|
||||||
import type { EditorScrollPercents } from '../../../app.reducer';
|
import type { EditorScrollPercents } from '../../../app.reducer';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -30,8 +29,6 @@ const useScrollWhenReadyOptions = ({ noteId, selectedNoteHash, lastEditorScrollP
|
|||||||
type: selectedNoteHash ? ScrollOptionTypes.Hash : ScrollOptionTypes.Percent,
|
type: selectedNoteHash ? ScrollOptionTypes.Hash : ScrollOptionTypes.Percent,
|
||||||
value: selectedNoteHash ? selectedNoteHash : lastScrollPercent,
|
value: selectedNoteHash ? selectedNoteHash : lastScrollPercent,
|
||||||
});
|
});
|
||||||
|
|
||||||
void ResourceEditWatcher.instance().stopWatchingAll();
|
|
||||||
}, [noteId, previousNoteId, selectedNoteHash, editorRef]);
|
}, [noteId, previousNoteId, selectedNoteHash, editorRef]);
|
||||||
|
|
||||||
const clearScrollWhenReady = useCallback(() => {
|
const clearScrollWhenReady = useCallback(() => {
|
||||||
|
@ -9,10 +9,14 @@ import { ResourceEntity } from '../database/types';
|
|||||||
const EventEmitter = require('events');
|
const EventEmitter = require('events');
|
||||||
const chokidar = require('chokidar');
|
const chokidar = require('chokidar');
|
||||||
|
|
||||||
|
type WindowId = string;
|
||||||
|
|
||||||
interface WatchedItem {
|
interface WatchedItem {
|
||||||
resourceId: string;
|
resourceId: string;
|
||||||
|
title: string;
|
||||||
lastFileUpdatedTime: number;
|
lastFileUpdatedTime: number;
|
||||||
lastResourceUpdatedTime: number;
|
lastResourceUpdatedTime: number;
|
||||||
|
watchedByWindows: WindowId[];
|
||||||
path: string;
|
path: string;
|
||||||
asyncSaveQueue: AsyncActionQueue;
|
asyncSaveQueue: AsyncActionQueue;
|
||||||
size: number;
|
size: number;
|
||||||
@ -23,6 +27,7 @@ interface WatchedItems {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type OpenItemFn = (path: string)=> void;
|
type OpenItemFn = (path: string)=> void;
|
||||||
|
type GetWindowIdFn = ()=> string;
|
||||||
|
|
||||||
export default class ResourceEditWatcher {
|
export default class ResourceEditWatcher {
|
||||||
|
|
||||||
@ -41,6 +46,7 @@ export default class ResourceEditWatcher {
|
|||||||
private eventEmitter_: any;
|
private eventEmitter_: any;
|
||||||
private tempDir_ = '';
|
private tempDir_ = '';
|
||||||
private openItem_: OpenItemFn;
|
private openItem_: OpenItemFn;
|
||||||
|
private getActiveWindowId_: GetWindowIdFn;
|
||||||
|
|
||||||
public constructor() {
|
public constructor() {
|
||||||
this.logger_ = new Logger();
|
this.logger_ = new Logger();
|
||||||
@ -51,10 +57,11 @@ export default class ResourceEditWatcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types, @typescript-eslint/no-explicit-any -- Old code before rule was applied, Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/ban-types, @typescript-eslint/no-explicit-any -- Old code before rule was applied, Old code before rule was applied
|
||||||
public initialize(logger: any, dispatch: Function, openItem: OpenItemFn) {
|
public initialize(logger: any, dispatch: Function, openItem: OpenItemFn, getWindowId: GetWindowIdFn) {
|
||||||
this.logger_ = logger;
|
this.logger_ = logger;
|
||||||
this.dispatch = dispatch;
|
this.dispatch = dispatch;
|
||||||
this.openItem_ = openItem;
|
this.openItem_ = openItem;
|
||||||
|
this.getActiveWindowId_ = getWindowId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static instance() {
|
public static instance() {
|
||||||
@ -239,6 +246,7 @@ export default class ResourceEditWatcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async watch(resourceId: string): Promise<WatchedItem> {
|
private async watch(resourceId: string): Promise<WatchedItem> {
|
||||||
|
const sourceWindowId = this.getActiveWindowId_();
|
||||||
let watchedItem = this.watchedItemByResourceId(resourceId);
|
let watchedItem = this.watchedItemByResourceId(resourceId);
|
||||||
|
|
||||||
if (!watchedItem) {
|
if (!watchedItem) {
|
||||||
@ -246,8 +254,10 @@ export default class ResourceEditWatcher {
|
|||||||
|
|
||||||
watchedItem = {
|
watchedItem = {
|
||||||
resourceId: resourceId,
|
resourceId: resourceId,
|
||||||
|
title: '',
|
||||||
lastFileUpdatedTime: 0,
|
lastFileUpdatedTime: 0,
|
||||||
lastResourceUpdatedTime: 0,
|
lastResourceUpdatedTime: 0,
|
||||||
|
watchedByWindows: [sourceWindowId],
|
||||||
asyncSaveQueue: new AsyncActionQueue(1000),
|
asyncSaveQueue: new AsyncActionQueue(1000),
|
||||||
path: '',
|
path: '',
|
||||||
size: -1,
|
size: -1,
|
||||||
@ -261,6 +271,10 @@ export default class ResourceEditWatcher {
|
|||||||
watchedItem.lastFileUpdatedTime = stat.mtime.getTime();
|
watchedItem.lastFileUpdatedTime = stat.mtime.getTime();
|
||||||
watchedItem.lastResourceUpdatedTime = resource.updated_time;
|
watchedItem.lastResourceUpdatedTime = resource.updated_time;
|
||||||
watchedItem.size = stat.size;
|
watchedItem.size = stat.size;
|
||||||
|
watchedItem.title = resource.title;
|
||||||
|
// Reset the watching window list to handle the case where the active window
|
||||||
|
// was changed while loading the resource.
|
||||||
|
watchedItem.watchedByWindows = [this.getActiveWindowId_()];
|
||||||
|
|
||||||
this.watchFile(editFilePath);
|
this.watchFile(editFilePath);
|
||||||
|
|
||||||
@ -269,6 +283,18 @@ export default class ResourceEditWatcher {
|
|||||||
id: resource.id,
|
id: resource.id,
|
||||||
title: resource.title,
|
title: resource.title,
|
||||||
});
|
});
|
||||||
|
} else if (!watchedItem.watchedByWindows.includes(sourceWindowId)) {
|
||||||
|
watchedItem = {
|
||||||
|
...this.watchedItems_[resourceId],
|
||||||
|
watchedByWindows: [...watchedItem.watchedByWindows, sourceWindowId],
|
||||||
|
};
|
||||||
|
this.watchedItems_[resourceId] = watchedItem;
|
||||||
|
|
||||||
|
this.dispatch({
|
||||||
|
type: 'RESOURCE_EDIT_WATCHER_SET',
|
||||||
|
id: watchedItem.resourceId,
|
||||||
|
title: watchedItem.title,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger().info(`ResourceEditWatcher: Started watching ${watchedItem.path}`);
|
this.logger().info(`ResourceEditWatcher: Started watching ${watchedItem.path}`);
|
||||||
@ -318,16 +344,26 @@ export default class ResourceEditWatcher {
|
|||||||
this.logger().info(`ResourceEditWatcher: Stopped watching ${item.path}`);
|
this.logger().info(`ResourceEditWatcher: Stopped watching ${item.path}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async stopWatchingAll() {
|
public async stopWatchingAll(sourceWindow: string) {
|
||||||
const promises = [];
|
const promises = [];
|
||||||
for (const resourceId in this.watchedItems_) {
|
for (const resourceId in this.watchedItems_) {
|
||||||
const item = this.watchedItems_[resourceId];
|
let item = this.watchedItems_[resourceId];
|
||||||
promises.push(this.stopWatching(item.resourceId));
|
|
||||||
|
if (item.watchedByWindows.includes(sourceWindow)) {
|
||||||
|
const otherWatchingWindows = item.watchedByWindows.filter(id => id !== sourceWindow);
|
||||||
|
item = { ...item, watchedByWindows: otherWatchingWindows };
|
||||||
|
this.watchedItems_[resourceId] = item;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.watchedByWindows.length === 0) {
|
||||||
|
promises.push(this.stopWatching(item.resourceId));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
|
|
||||||
this.dispatch({
|
this.dispatch({
|
||||||
type: 'RESOURCE_EDIT_WATCHER_CLEAR',
|
type: 'RESOURCE_EDIT_WATCHER_CLEAR',
|
||||||
|
windowId: sourceWindow,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import produce, { Draft } from 'immer';
|
import produce, { Draft } from 'immer';
|
||||||
|
import { defaultWindowId, stateUtils } from '../../reducer';
|
||||||
|
|
||||||
export const defaultState = {
|
export const defaultState = {
|
||||||
watchedResources: {},
|
watchedResources: {},
|
||||||
@ -20,14 +21,28 @@ const reducer = produce((draft: Draft<any>, action: any) => {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'RESOURCE_EDIT_WATCHER_REMOVE':
|
case 'RESOURCE_EDIT_WATCHER_REMOVE':
|
||||||
|
// RESOURCE_EDIT_WATCHER_REMOVE signals that a resource is no longer being watched.
|
||||||
|
// As such, it should be removed from all windows' resource lists:
|
||||||
|
for (const windowId in draft.backgroundWindows) {
|
||||||
|
// watchedResources is per-window only on desktop:
|
||||||
|
if ('watchedResources' in draft.backgroundWindows[windowId]) {
|
||||||
|
delete draft.backgroundWindows[windowId].watchedResources[action.id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
delete draft.watchedResources[action.id];
|
delete draft.watchedResources[action.id];
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'RESOURCE_EDIT_WATCHER_CLEAR':
|
case 'RESOURCE_EDIT_WATCHER_CLEAR': {
|
||||||
|
const windowState = stateUtils.windowStateById(draft, action.windowId ?? defaultWindowId);
|
||||||
|
|
||||||
draft.watchedResources = {};
|
// The window may have already been closed.
|
||||||
|
const windowExists = !!windowState;
|
||||||
|
if (windowExists) {
|
||||||
|
windowState.watchedResources = {};
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -155,4 +155,5 @@ tablist
|
|||||||
Edubirdie
|
Edubirdie
|
||||||
Useviral
|
Useviral
|
||||||
ldaps
|
ldaps
|
||||||
Bluesky
|
Bluesky
|
||||||
|
unwatcher
|
||||||
|
Reference in New Issue
Block a user