mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 18:36:30 +01:00 
			
		
		
		
	opened file change detection now useable on all note types
This commit is contained in:
		| @@ -36,6 +36,7 @@ import SearchResultWidget from "../widgets/search_result.js"; | |||||||
| import SyncStatusWidget from "../widgets/sync_status.js"; | import SyncStatusWidget from "../widgets/sync_status.js"; | ||||||
| import ScrollingContainer from "../widgets/containers/scrolling_container.js"; | import ScrollingContainer from "../widgets/containers/scrolling_container.js"; | ||||||
| import RootContainer from "../widgets/containers/root_container.js"; | import RootContainer from "../widgets/containers/root_container.js"; | ||||||
|  | import NoteUpdateStatusWidget from "../widgets/note_update_status.js"; | ||||||
|  |  | ||||||
| const RIGHT_PANE_CSS = ` | const RIGHT_PANE_CSS = ` | ||||||
| <style> | <style> | ||||||
| @@ -177,6 +178,7 @@ export default class DesktopLayout { | |||||||
|                             .child(new InheritedAttributesWidget()) |                             .child(new InheritedAttributesWidget()) | ||||||
|                         ) |                         ) | ||||||
|                     ) |                     ) | ||||||
|  |                     .child(new NoteUpdateStatusWidget()) | ||||||
|                     .child( |                     .child( | ||||||
|                         new TabCachingWidget(() => new ScrollingContainer() |                         new TabCachingWidget(() => new ScrollingContainer() | ||||||
|                             .child(new SqlTableSchemasWidget()) |                             .child(new SqlTableSchemasWidget()) | ||||||
|   | |||||||
| @@ -11,6 +11,10 @@ function fileModificationUploaded(noteId) { | |||||||
|     delete fileModificationStatus[noteId]; |     delete fileModificationStatus[noteId]; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | function ignoreModification(noteId) { | ||||||
|  |     delete fileModificationStatus[noteId]; | ||||||
|  | } | ||||||
|  |  | ||||||
| ws.subscribeToMessages(async message => { | ws.subscribeToMessages(async message => { | ||||||
|     if (message.type !== 'openedFileUpdated') { |     if (message.type !== 'openedFileUpdated') { | ||||||
|         return; |         return; | ||||||
| @@ -27,5 +31,6 @@ ws.subscribeToMessages(async message => { | |||||||
|  |  | ||||||
| export default { | export default { | ||||||
|     getFileModificationStatus, |     getFileModificationStatus, | ||||||
|     fileModificationUploaded |     fileModificationUploaded, | ||||||
|  |     ignoreModification | ||||||
| } | } | ||||||
|   | |||||||
| @@ -44,7 +44,7 @@ async function getRenderedContent(note, options = {}) { | |||||||
|         const $openButton = $('<button class="file-open btn btn-primary" type="button">Open</button>'); |         const $openButton = $('<button class="file-open btn btn-primary" type="button">Open</button>'); | ||||||
|  |  | ||||||
|         $downloadButton.on('click', () => openService.downloadFileNote(note.noteId)); |         $downloadButton.on('click', () => openService.downloadFileNote(note.noteId)); | ||||||
|         $openButton.on('click', () => openService.openFileNote(note.noteId)); |         $openButton.on('click', () => openService.openNoteExternally(note.noteId)); | ||||||
|  |  | ||||||
|         // open doesn't work for protected notes since it works through browser which isn't in protected session |         // open doesn't work for protected notes since it works through browser which isn't in protected session | ||||||
|         $openButton.toggle(!note.isProtected); |         $openButton.toggle(!note.isProtected); | ||||||
|   | |||||||
| @@ -21,9 +21,9 @@ function downloadFileNote(noteId) { | |||||||
|     download(url); |     download(url); | ||||||
| } | } | ||||||
|  |  | ||||||
| async function openFileNote(noteId) { | async function openNoteExternally(noteId) { | ||||||
|     if (utils.isElectron()) { |     if (utils.isElectron()) { | ||||||
|         const resp = await server.post("notes/" + noteId + "/saveToTmpDir"); |         const resp = await server.post("notes/" + noteId + "/save-to-tmp-dir"); | ||||||
|  |  | ||||||
|         const electron = utils.dynamicRequire('electron'); |         const electron = utils.dynamicRequire('electron'); | ||||||
|         const res = await electron.shell.openPath(resp.tmpFilePath); |         const res = await electron.shell.openPath(resp.tmpFilePath); | ||||||
| @@ -66,7 +66,7 @@ function getHost() { | |||||||
| export default { | export default { | ||||||
|     download, |     download, | ||||||
|     downloadFileNote, |     downloadFileNote, | ||||||
|     openFileNote, |     openNoteExternally, | ||||||
|     downloadNoteRevision, |     downloadNoteRevision, | ||||||
|     getUrlForDownload |     getUrlForDownload | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,11 +1,12 @@ | |||||||
| import TabAwareWidget from "./tab_aware_widget.js"; | import TabAwareWidget from "./tab_aware_widget.js"; | ||||||
| import protectedSessionService from "../services/protected_session.js"; | import protectedSessionService from "../services/protected_session.js"; | ||||||
|  | import openService from "../services/open.js"; | ||||||
|  |  | ||||||
| const TPL = ` | const TPL = ` | ||||||
| <div class="dropdown note-actions"> | <div class="dropdown note-actions"> | ||||||
|     <style> |     <style> | ||||||
|     .note-actions .dropdown-menu { |     .note-actions .dropdown-menu { | ||||||
|         width: 15em; |         width: 20em; | ||||||
|     } |     } | ||||||
|      |      | ||||||
|     .note-actions .dropdown-item[disabled], .note-actions .dropdown-item[disabled]:hover { |     .note-actions .dropdown-item[disabled], .note-actions .dropdown-item[disabled]:hover { | ||||||
| @@ -84,6 +85,7 @@ const TPL = ` | |||||||
|         <a data-trigger-command="showNoteRevisions" class="dropdown-item show-note-revisions-button">Revisions</a> |         <a data-trigger-command="showNoteRevisions" class="dropdown-item show-note-revisions-button">Revisions</a> | ||||||
|         <a data-trigger-command="showLinkMap" class="dropdown-item show-link-map-button"><kbd data-command="showLinkMap"></kbd> Link map</a> |         <a data-trigger-command="showLinkMap" class="dropdown-item show-link-map-button"><kbd data-command="showLinkMap"></kbd> Link map</a> | ||||||
|         <a data-trigger-command="showNoteSource" class="dropdown-item show-source-button"><kbd data-command="showNoteSource"></kbd> Note source</a> |         <a data-trigger-command="showNoteSource" class="dropdown-item show-source-button"><kbd data-command="showNoteSource"></kbd> Note source</a> | ||||||
|  |         <a data-trigger-command="openNoteExternally" class="dropdown-item open-note-externally-button"><kbd data-command="openNoteExternally"></kbd> Open note externally</a> | ||||||
|         <a class="dropdown-item import-files-button">Import files</a> |         <a class="dropdown-item import-files-button">Import files</a> | ||||||
|         <a class="dropdown-item export-note-button">Export note</a> |         <a class="dropdown-item export-note-button">Export note</a> | ||||||
|         <a data-trigger-command="printActiveNote" class="dropdown-item print-note-button"><kbd data-command="printActiveNote"></kbd> Print note</a> |         <a data-trigger-command="printActiveNote" class="dropdown-item print-note-button"><kbd data-command="printActiveNote"></kbd> Print note</a> | ||||||
| @@ -119,6 +121,9 @@ export default class NoteActionsWidget extends TabAwareWidget { | |||||||
|  |  | ||||||
|         this.$widget.on('click', '.dropdown-item', |         this.$widget.on('click', '.dropdown-item', | ||||||
|             () => this.$widget.find('.dropdown-toggle').dropdown('toggle')); |             () => this.$widget.find('.dropdown-toggle').dropdown('toggle')); | ||||||
|  |  | ||||||
|  |         this.$openNoteExternallyButton = this.$widget.find(".open-note-externally-button"); | ||||||
|  |         this.$openNoteExternallyButton.on('click', () => openService.openNoteExternally(this.noteId)); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     refreshWithNote(note) { |     refreshWithNote(note) { | ||||||
|   | |||||||
							
								
								
									
										64
									
								
								src/public/app/widgets/note_update_status.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								src/public/app/widgets/note_update_status.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,64 @@ | |||||||
|  | import TabAwareWidget from "./tab_aware_widget.js"; | ||||||
|  | import server from "../services/server.js"; | ||||||
|  | import fileWatcher from "../services/file_watcher.js"; | ||||||
|  |  | ||||||
|  | const TPL = ` | ||||||
|  | <div class="dropdown note-update-status-widget alert alert-warning"> | ||||||
|  |     <style> | ||||||
|  |         .note-update-status-widget { | ||||||
|  |             margin: 10px; | ||||||
|  |         } | ||||||
|  |     </style> | ||||||
|  |      | ||||||
|  |     <p>File <code class="file-path"></code> has been last modified on <span class="file-last-modified"></span>.</p>  | ||||||
|  |          | ||||||
|  |     <div style="display: flex; flex-direction: row; justify-content: space-evenly;"> | ||||||
|  |         <button class="btn btn-sm file-upload-button">Upload modified file</button> | ||||||
|  |          | ||||||
|  |         <button class="btn btn-sm ignore-this-change-button">Ignore this change</button> | ||||||
|  |     </div> | ||||||
|  | </div>`; | ||||||
|  |  | ||||||
|  | export default class NoteUpdateStatusWidget extends TabAwareWidget { | ||||||
|  |     isEnabled() { | ||||||
|  |         return super.isEnabled() | ||||||
|  |             && !!fileWatcher.getFileModificationStatus(this.noteId); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     doRender() { | ||||||
|  |         this.$widget = $(TPL); | ||||||
|  |         this.overflowing(); | ||||||
|  |  | ||||||
|  |         this.$filePath = this.$widget.find(".file-path"); | ||||||
|  |         this.$fileLastModified = this.$widget.find(".file-last-modified"); | ||||||
|  |         this.$fileUploadButton = this.$widget.find(".file-upload-button"); | ||||||
|  |  | ||||||
|  |         this.$fileUploadButton.on("click", async () => { | ||||||
|  |             await server.post(`notes/${this.noteId}/upload-modified-file`, { | ||||||
|  |                 filePath: this.$filePath.text() | ||||||
|  |             }); | ||||||
|  |  | ||||||
|  |             fileWatcher.fileModificationUploaded(this.noteId); | ||||||
|  |             this.refresh(); | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         this.$ignoreThisChangeButton = this.$widget.find(".ignore-this-change-button"); | ||||||
|  |         this.$ignoreThisChangeButton.on('click', () => { | ||||||
|  |             fileWatcher.ignoreModification(this.noteId); | ||||||
|  |             this.refresh(); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     refreshWithNote(note) { | ||||||
|  |         const status = fileWatcher.getFileModificationStatus(note.noteId); | ||||||
|  |  | ||||||
|  |         this.$filePath.text(status.filePath); | ||||||
|  |         this.$fileLastModified.text(dayjs.unix(status.lastModifiedMs / 1000).format("HH:mm:ss")); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     openedFileUpdatedEvent(data) { | ||||||
|  |         if (data.noteId === this.noteId) { | ||||||
|  |             this.refresh(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -82,7 +82,7 @@ export default class FilePropertiesWidget extends TabAwareWidget { | |||||||
|         this.$uploadNewRevisionInput = this.$widget.find(".file-upload-new-revision-input"); |         this.$uploadNewRevisionInput = this.$widget.find(".file-upload-new-revision-input"); | ||||||
|  |  | ||||||
|         this.$downloadButton.on('click', () => openService.downloadFileNote(this.noteId)); |         this.$downloadButton.on('click', () => openService.downloadFileNote(this.noteId)); | ||||||
|         this.$openButton.on('click', () => openService.openFileNote(this.noteId)); |         this.$openButton.on('click', () => openService.openNoteExternally(this.noteId)); | ||||||
|  |  | ||||||
|         this.$uploadNewRevisionButton.on("click", () => { |         this.$uploadNewRevisionButton.on("click", () => { | ||||||
|             this.$uploadNewRevisionInput.trigger("click"); |             this.$uploadNewRevisionInput.trigger("click"); | ||||||
|   | |||||||
| @@ -26,6 +26,8 @@ const TPL = ` | |||||||
|     <div class="no-print" style="display: flex; justify-content: space-evenly; margin: 10px;"> |     <div class="no-print" style="display: flex; justify-content: space-evenly; margin: 10px;"> | ||||||
|         <button class="image-download btn btn-sm btn-primary" type="button">Download</button> |         <button class="image-download btn btn-sm btn-primary" type="button">Download</button> | ||||||
|  |  | ||||||
|  |         <button class="image-open btn btn-sm btn-primary" type="button">Open</button> | ||||||
|  |  | ||||||
|         <button class="image-copy-to-clipboard btn btn-sm btn-primary" type="button">Copy to clipboard</button> |         <button class="image-copy-to-clipboard btn btn-sm btn-primary" type="button">Copy to clipboard</button> | ||||||
|  |  | ||||||
|         <button class="image-upload-new-revision btn btn-sm btn-primary" type="button">Upload new revision</button> |         <button class="image-upload-new-revision btn btn-sm btn-primary" type="button">Upload new revision</button> | ||||||
| @@ -59,6 +61,9 @@ export default class ImagePropertiesWidget extends TabAwareWidget { | |||||||
|         this.$fileType = this.$widget.find(".image-filetype"); |         this.$fileType = this.$widget.find(".image-filetype"); | ||||||
|         this.$fileSize = this.$widget.find(".image-filesize"); |         this.$fileSize = this.$widget.find(".image-filesize"); | ||||||
|  |  | ||||||
|  |         this.$openButton = this.$widget.find(".image-open"); | ||||||
|  |         this.$openButton.on('click', () => openService.openNoteExternally(this.noteId)); | ||||||
|  |  | ||||||
|         this.$imageDownloadButton = this.$widget.find(".image-download"); |         this.$imageDownloadButton = this.$widget.find(".image-download"); | ||||||
|         this.$imageDownloadButton.on('click', () => openService.downloadFileNote(this.noteId)); |         this.$imageDownloadButton.on('click', () => openService.downloadFileNote(this.noteId)); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -24,12 +24,6 @@ const TPL = ` | |||||||
|         } |         } | ||||||
|     </style> |     </style> | ||||||
|      |      | ||||||
|     <div class="file-watcher-wrapper alert alert-warning"> |  | ||||||
|         <p>File <code class="file-watcher-path"></code> has been last modified on <span class="file-watcher-last-modified"></span>.</p>  |  | ||||||
|          |  | ||||||
|         <button class="btn btn-sm file-watcher-upload-button">Upload modified file</button> |  | ||||||
|     </div> |  | ||||||
|      |  | ||||||
|     <pre class="file-preview-content"></pre> |     <pre class="file-preview-content"></pre> | ||||||
|      |      | ||||||
|     <div class="file-preview-not-available alert alert-info"> |     <div class="file-preview-not-available alert alert-info"> | ||||||
| @@ -54,22 +48,6 @@ export default class FileTypeWidget extends TypeWidget { | |||||||
|         this.$pdfPreview = this.$widget.find(".pdf-preview"); |         this.$pdfPreview = this.$widget.find(".pdf-preview"); | ||||||
|         this.$videoPreview = this.$widget.find(".video-preview"); |         this.$videoPreview = this.$widget.find(".video-preview"); | ||||||
|         this.$audioPreview = this.$widget.find(".audio-preview"); |         this.$audioPreview = this.$widget.find(".audio-preview"); | ||||||
|  |  | ||||||
|         this.$fileWatcherWrapper = this.$widget.find(".file-watcher-wrapper"); |  | ||||||
|         this.$fileWatcherWrapper.hide(); |  | ||||||
|  |  | ||||||
|         this.$fileWatcherPath = this.$widget.find(".file-watcher-path"); |  | ||||||
|         this.$fileWatcherLastModified = this.$widget.find(".file-watcher-last-modified"); |  | ||||||
|         this.$fileWatcherUploadButton = this.$widget.find(".file-watcher-upload-button"); |  | ||||||
|  |  | ||||||
|         this.$fileWatcherUploadButton.on("click", async () => { |  | ||||||
|             await server.post(`notes/${this.noteId}/upload-modified-file`, { |  | ||||||
|                 filePath: this.$fileWatcherPath.text() |  | ||||||
|             }); |  | ||||||
|  |  | ||||||
|             fileWatcher.fileModificationUploaded(this.noteId); |  | ||||||
|             this.refreshFileWatchingStatus(); |  | ||||||
|         }); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async doRefresh(note) { |     async doRefresh(note) { | ||||||
| @@ -107,22 +85,5 @@ export default class FileTypeWidget extends TypeWidget { | |||||||
|         else { |         else { | ||||||
|             this.$previewNotAvailable.show(); |             this.$previewNotAvailable.show(); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         this.refreshFileWatchingStatus(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     refreshFileWatchingStatus() { |  | ||||||
|         const status = fileWatcher.getFileModificationStatus(this.noteId); |  | ||||||
|  |  | ||||||
|         this.$fileWatcherWrapper.toggle(!!status); |  | ||||||
|  |  | ||||||
|         if (status) { |  | ||||||
|             this.$fileWatcherPath.text(status.filePath); |  | ||||||
|             this.$fileWatcherLastModified.text(dayjs.unix(status.lastModifiedMs / 1000).format("HH:mm:ss")); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     openedFileUpdatedEvent(data) { |  | ||||||
|         this.refreshFileWatchingStatus(); |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ | |||||||
| const protectedSessionService = require('../../services/protected_session'); | const protectedSessionService = require('../../services/protected_session'); | ||||||
| const repository = require('../../services/repository'); | const repository = require('../../services/repository'); | ||||||
| const utils = require('../../services/utils'); | const utils = require('../../services/utils'); | ||||||
|  | const log = require('../../services/log'); | ||||||
| const noteRevisionService = require('../../services/note_revisions'); | const noteRevisionService = require('../../services/note_revisions'); | ||||||
| const tmp = require('tmp'); | const tmp = require('tmp'); | ||||||
| const fs = require('fs'); | const fs = require('fs'); | ||||||
| @@ -122,6 +123,8 @@ function saveToTmpDir(req) { | |||||||
|     fs.writeSync(tmpObj.fd, note.getContent()); |     fs.writeSync(tmpObj.fd, note.getContent()); | ||||||
|     fs.closeSync(tmpObj.fd); |     fs.closeSync(tmpObj.fd); | ||||||
|  |  | ||||||
|  |     log.info(`Saved temporary file for note ${noteId} into ${tmpObj.name}`); | ||||||
|  |  | ||||||
|     if (utils.isElectron()) { |     if (utils.isElectron()) { | ||||||
|         chokidar.watch(tmpObj.name).on('change', (path, stats) => { |         chokidar.watch(tmpObj.name).on('change', (path, stats) => { | ||||||
|             ws.sendMessageToAllClients({ |             ws.sendMessageToAllClients({ | ||||||
| @@ -130,8 +133,6 @@ function saveToTmpDir(req) { | |||||||
|                 lastModifiedMs: stats.atimeMs, |                 lastModifiedMs: stats.atimeMs, | ||||||
|                 filePath: tmpObj.name |                 filePath: tmpObj.name | ||||||
|             }); |             }); | ||||||
|  |  | ||||||
|             console.log(stats, path); |  | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -285,6 +285,8 @@ function uploadModifiedFile(req) { | |||||||
|         return [404, `Note ${noteId} has not been found`]; |         return [404, `Note ${noteId} has not been found`]; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     log.info(`Updating note ${noteId} with content from ${filePath}`); | ||||||
|  |  | ||||||
|     noteRevisionService.createNoteRevision(note); |     noteRevisionService.createNoteRevision(note); | ||||||
|  |  | ||||||
|     const fileContent = fs.readFileSync(filePath); |     const fileContent = fs.readFileSync(filePath); | ||||||
|   | |||||||
| @@ -186,7 +186,7 @@ function register(app) { | |||||||
|     route(GET, '/api/notes/:noteId/download', [auth.checkApiAuthOrElectron], filesRoute.downloadFile); |     route(GET, '/api/notes/:noteId/download', [auth.checkApiAuthOrElectron], filesRoute.downloadFile); | ||||||
|     // this "hacky" path is used for easier referencing of CSS resources |     // this "hacky" path is used for easier referencing of CSS resources | ||||||
|     route(GET, '/api/notes/download/:noteId', [auth.checkApiAuthOrElectron], filesRoute.downloadFile); |     route(GET, '/api/notes/download/:noteId', [auth.checkApiAuthOrElectron], filesRoute.downloadFile); | ||||||
|     apiRoute(POST, '/api/notes/:noteId/saveToTmpDir', filesRoute.saveToTmpDir); |     apiRoute(POST, '/api/notes/:noteId/save-to-tmp-dir', filesRoute.saveToTmpDir); | ||||||
|  |  | ||||||
|     apiRoute(GET, '/api/notes/:noteId/attributes', attributesRoute.getEffectiveNoteAttributes); |     apiRoute(GET, '/api/notes/:noteId/attributes', attributesRoute.getEffectiveNoteAttributes); | ||||||
|     apiRoute(POST, '/api/notes/:noteId/attributes', attributesRoute.addNoteAttribute); |     apiRoute(POST, '/api/notes/:noteId/attributes', attributesRoute.addNoteAttribute); | ||||||
|   | |||||||
| @@ -352,6 +352,12 @@ const DEFAULT_KEYBOARD_ACTIONS = [ | |||||||
|         defaultShortcuts: [], |         defaultShortcuts: [], | ||||||
|         scope: "window" |         scope: "window" | ||||||
|     }, |     }, | ||||||
|  |     { | ||||||
|  |         actionName: "openNoteExternally", | ||||||
|  |         defaultShortcuts: [], | ||||||
|  |         description: "Open note as a file with default application", | ||||||
|  |         scope: "window" | ||||||
|  |     }, | ||||||
|     { |     { | ||||||
|         actionName: "renderActiveNote", |         actionName: "renderActiveNote", | ||||||
|         defaultShortcuts: [], |         defaultShortcuts: [], | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user