diff --git a/docs/IMAGES.md b/docs/IMAGES.md index de3c87ff..cfd4d82b 100644 --- a/docs/IMAGES.md +++ b/docs/IMAGES.md @@ -2,19 +2,34 @@ MarkText can automatically copy your images into a specified directory or handle images from clipboard. +### Maintaining server paths during editing and preview + +When editing documents, you may want image paths to represent absolute paths on the server that will ultimately host them (such as `/images/myImage.png`) rather than +local filesystem paths (such as `C:\assets\static\images\myImage.png`). You can use _Maintain server paths during editing and preview_ to support this scenario. + +When MarkText sees a specified server path for an image in your document, it will instead look into the local path you've provided when previewing images. Your document will retain the original server path as specified. + + ### Upload to cloud using selected uploader Please see [here](IMAGE_UPLOADER_CONFIGRATION.md) for more information. ### Move to designated local folder -All images are automatically copied into the specified local directory that may be relative. +This option automatically copies images into the specified local directory. This path may be a relative path, and may include variables like `${filename}`. The local resource directory is used if the file is not saved. This directory must be a valid path name and MarkText need write access to the directory. + +The following are valid variables for use in the image path: + +- `${filename}`: The document's file name, without path or extension +- `${year}`: The current year +- `${month}`: The current month, in 2-digit format +- `${day}`: The current day, in 2-digit format + +If you have specified the option to _Maintain server paths during editing and preview_, MarkText will replace local filesystem paths with the corresponding server path. **Prefer relative assets folder:** -When this option is enabled, all images are copied relative to the opened file. The root directory is used when a project is opened and no variables are used. You can specify the path via the *relative image folder name* text box and include variables like `${filename}` to add the file name to the relative directory. The local resource directory is used if the file is not saved. - -Note: The assets directory name must be a valid path name and MarkText need write access to the directory. +When this option is enabled, all images are copied relative to the opened file. The root directory is used when a project is opened and no variables are used. You can specify the path via the *relative image folder name* text box. Examples for relative paths: @@ -23,6 +38,7 @@ Examples for relative paths: - `.`: current file directory - `assets/123` - `assets_${filename}` (add the document file name) +- `assets/${year}/${month}` (save the assets into year and month subdirectories) ### Keep original location diff --git a/src/main/app/index.js b/src/main/app/index.js index 3424b6a2..0ad80232 100644 --- a/src/main/app/index.js +++ b/src/main/app/index.js @@ -544,6 +544,11 @@ class App { } }) + ipcMain.on('mt::show-user-notification-dialog', async (e, title, message) => { + const win = BrowserWindow.fromWebContents(e.sender) + win.webContents.send('showUserNotificationDialog', title, message) + }) + ipcMain.on('mt::open-setting-window', () => { this._openSettingsWindow() }) diff --git a/src/main/dataCenter/index.js b/src/main/dataCenter/index.js index 1927956b..b6aa7c07 100644 --- a/src/main/dataCenter/index.js +++ b/src/main/dataCenter/index.js @@ -178,6 +178,26 @@ class DataCenter extends EventEmitter { } }) + ipcMain.on('mt::ask-for-modify-local-folder-path', async (e, imagePath) => { + if (!imagePath) { + const win = BrowserWindow.fromWebContents(e.sender) + const { filePaths } = await dialog.showOpenDialog(win, { + properties: ['openDirectory', 'createDirectory'] + }) + if (filePaths && filePaths[0]) { + imagePath = filePaths[0] + } + } + if (imagePath) { + // Ensure they have a path separator at the end of their local folder path + if ((!imagePath.endsWith('/')) && (!imagePath.endsWith('\\'))) { + const pathSeparator = imagePath.replace(/[^\\/]/g, '')[0] + imagePath = imagePath + pathSeparator + } + this.setItem('localFolderPath', imagePath) + } + }) + ipcMain.on('mt::set-user-data', (e, userData) => { this.setItems(userData) }) diff --git a/src/main/preferences/schema.json b/src/main/preferences/schema.json index 1904059a..254834e5 100644 --- a/src/main/preferences/schema.json +++ b/src/main/preferences/schema.json @@ -349,6 +349,16 @@ ], "default": "path" }, + "serverFolderPath": { + "description": "If specified, a server path of image URLs to maintain during editing and preview", + "type": "string", + "default": "" + }, + "localFolderPath": { + "description": "The local path that corresponds to serverFolderPath during editing and preview", + "type": "string", + "default": "" + }, "imagePreferRelativeDirectory": { "description": "Image--Whether to prefer the relative image directory.", "type": "boolean", diff --git a/src/muya/lib/contentState/dragDropCtrl.js b/src/muya/lib/contentState/dragDropCtrl.js index 7388e40b..1307e63d 100644 --- a/src/muya/lib/contentState/dragDropCtrl.js +++ b/src/muya/lib/contentState/dragDropCtrl.js @@ -160,14 +160,14 @@ const dragDropCtrl = ContentState => { try { const newSrc = await this.muya.options.imageAction(path, id, name) - const { src } = getImageSrc(path) + const { src } = getImageSrc(path, this.muya.options) if (src) { this.stateRender.urlMap.set(newSrc, src) } const imageWrapper = this.muya.container.querySelector(`span[data-id=${id}]`) if (imageWrapper) { - const imageInfo = getImageInfo(imageWrapper) + const imageInfo = getImageInfo(imageWrapper, this.muya.options) this.replaceImage(imageInfo, { alt: name, src: newSrc diff --git a/src/muya/lib/contentState/formatCtrl.js b/src/muya/lib/contentState/formatCtrl.js index cc0e6b2a..c1108c83 100644 --- a/src/muya/lib/contentState/formatCtrl.js +++ b/src/muya/lib/contentState/formatCtrl.js @@ -284,7 +284,7 @@ const formatCtrl = ContentState => { if (startNode) { const imageWrapper = startNode.closest('.ag-inline-image') if (imageWrapper && imageWrapper.classList.contains('ag-empty-image')) { - const imageInfo = getImageInfo(imageWrapper) + const imageInfo = getImageInfo(imageWrapper, this.muya.options) this.muya.eventCenter.dispatch('muya-image-selector', { reference: imageWrapper, imageInfo, diff --git a/src/muya/lib/contentState/pasteCtrl.js b/src/muya/lib/contentState/pasteCtrl.js index 0dd641c0..ce4e528c 100644 --- a/src/muya/lib/contentState/pasteCtrl.js +++ b/src/muya/lib/contentState/pasteCtrl.js @@ -150,7 +150,7 @@ const pasteCtrl = ContentState => { return null } - const { src } = getImageSrc(imagePath) + const { src } = getImageSrc(imagePath, this.muya.options) if (src) { this.stateRender.urlMap.set(newSrc, src) } @@ -158,7 +158,7 @@ const pasteCtrl = ContentState => { const imageWrapper = this.muya.container.querySelector(`span[data-id=${id}]`) if (imageWrapper) { - const imageInfo = getImageInfo(imageWrapper) + const imageInfo = getImageInfo(imageWrapper, this.muya.options) this.replaceImage(imageInfo, { src: newSrc }) @@ -225,7 +225,7 @@ const pasteCtrl = ContentState => { const imageWrapper = this.muya.container.querySelector(`span[data-id=${id}]`) if (imageWrapper) { - const imageInfo = getImageInfo(imageWrapper) + const imageInfo = getImageInfo(imageWrapper, this.muya.options) this.replaceImage(imageInfo, { src: newSrc }) diff --git a/src/muya/lib/eventHandler/clickEvent.js b/src/muya/lib/eventHandler/clickEvent.js index 7a8c96b1..1893271f 100644 --- a/src/muya/lib/eventHandler/clickEvent.js +++ b/src/muya/lib/eventHandler/clickEvent.js @@ -127,7 +127,7 @@ class ClickEvent { } // Handle delete inline iamge by click delete icon. if (imageDelete && imageWrapper) { - const imageInfo = getImageInfo(imageWrapper) + const imageInfo = getImageInfo(imageWrapper, this.muya.options) event.preventDefault() event.stopPropagation() // hide image selector if needed. @@ -152,7 +152,7 @@ class ClickEvent { // Handle image click, to select the current image if (target.tagName === 'IMG' && imageWrapper) { // Handle select image - const imageInfo = getImageInfo(imageWrapper) + const imageInfo = getImageInfo(imageWrapper, this.muya.options) event.preventDefault() eventCenter.dispatch('select-image', imageInfo) // Handle show image toolbar @@ -197,7 +197,7 @@ class ClickEvent { return rect } } - const imageInfo = getImageInfo(imageWrapper) + const imageInfo = getImageInfo(imageWrapper, this.muya.options) eventCenter.dispatch('muya-image-selector', { reference, imageInfo, diff --git a/src/muya/lib/eventHandler/keyboard.js b/src/muya/lib/eventHandler/keyboard.js index c4462cf8..e5fa9719 100644 --- a/src/muya/lib/eventHandler/keyboard.js +++ b/src/muya/lib/eventHandler/keyboard.js @@ -103,7 +103,7 @@ class Keyboard { case EVENT_KEYS.Space: { if (contentState.selectedImage) { const { token } = contentState.selectedImage - const { src } = getImageInfo(token.src || token.attrs.src) + const { src } = getImageInfo(token.src || token.attrs.src, this.muya.options) if (src) { eventCenter.dispatch('preview-image', { data: src diff --git a/src/muya/lib/index.js b/src/muya/lib/index.js index 5ab021e6..f1e38cde 100644 --- a/src/muya/lib/index.js +++ b/src/muya/lib/index.js @@ -23,8 +23,10 @@ class Muya { }) } - constructor (container, options) { + constructor (container, options, preferences) { this.options = Object.assign({}, MUYA_DEFAULT_OPTION, options) + this.options = Object.assign(this.options, preferences) + const { markdown } = this.options this.markdown = markdown this.container = getContainer(container, this.options) diff --git a/src/muya/lib/parser/render/renderBlock/renderLeafBlock.js b/src/muya/lib/parser/render/renderBlock/renderLeafBlock.js index 8405891a..8c5c0cc1 100644 --- a/src/muya/lib/parser/render/renderBlock/renderLeafBlock.js +++ b/src/muya/lib/parser/render/renderBlock/renderLeafBlock.js @@ -143,7 +143,7 @@ export default function renderLeafBlock (parent, block, activeBlocks, matches, u const imgs = doc.documentElement.querySelectorAll('img') for (const img of imgs) { const src = img.getAttribute('src') - const imageInfo = getImageInfo(src) + const imageInfo = getImageInfo(src, this.muya.options) img.setAttribute('src', imageInfo.src) } diff --git a/src/muya/lib/parser/render/renderInlines/image.js b/src/muya/lib/parser/render/renderInlines/image.js index 194ef355..8f112965 100644 --- a/src/muya/lib/parser/render/renderInlines/image.js +++ b/src/muya/lib/parser/render/renderInlines/image.js @@ -22,7 +22,7 @@ const renderIcon = (h, className, icon) => { // I dont want operate dom directly, is there any better method? need help! export default function image (h, cursor, block, token, outerClass) { - const imageInfo = getImageInfo(token.attrs.src) + const imageInfo = getImageInfo(token.attrs.src, this.muya.options) const { selectedImage } = this.muya.contentState const data = { dataset: { diff --git a/src/muya/lib/parser/render/renderInlines/referenceImage.js b/src/muya/lib/parser/render/renderInlines/referenceImage.js index dcdacd5f..692d3dcc 100644 --- a/src/muya/lib/parser/render/renderInlines/referenceImage.js +++ b/src/muya/lib/parser/render/renderInlines/referenceImage.js @@ -14,7 +14,7 @@ export default function referenceImage (h, cursor, block, token, outerClass) { if (this.labels.has((rawSrc).toLowerCase())) { ({ href, title } = this.labels.get(rawSrc.toLowerCase())) } - const imageInfo = getImageInfo(href) + const imageInfo = getImageInfo(href, this.muya.options) const { src } = imageInfo let id let isSuccess diff --git a/src/muya/lib/ui/imageSelector/index.js b/src/muya/lib/ui/imageSelector/index.js index 70e506b9..4cecec90 100644 --- a/src/muya/lib/ui/imageSelector/index.js +++ b/src/muya/lib/ui/imageSelector/index.js @@ -4,6 +4,8 @@ import { patch, h } from '../../parser/render/snabbdom' import { EVENT_KEYS, URL_REG, isWin } from '../../config' import { getUniqueId, getImageInfo as getImageSrc } from '../../utils' import { getImageInfo } from '../../utils/getImageInfo' +import fs from 'fs-extra' +import { ipcRenderer } from 'electron' import './index.css' @@ -190,6 +192,13 @@ class ImageSelector extends BaseFloat { } } + renameInputKeyDown (event) { + if (event.key === EVENT_KEYS.Enter) { + event.stopPropagation() + this.handleRenameButtonClick() + } + } + async handleKeyUp (event) { const { key } = event if ( @@ -236,6 +245,24 @@ class ImageSelector extends BaseFloat { return this.replaceImageAsync(this.state) } + handleRenameButtonClick () { + const oldSrc = this.imageInfo.token.attrs.src + let { src: newLocalPath } = getImageSrc(this.state.src, this.muya.options) + let { src: oldLocalPath } = getImageSrc(oldSrc, this.muya.options) + + newLocalPath = newLocalPath.replace('file://', '') + oldLocalPath = oldLocalPath.replace('file://', '') + + try { + fs.renameSync(oldLocalPath, newLocalPath) + } catch (error) { + this.state.src = oldSrc + ipcRenderer.send('mt::show-user-notification-dialog', 'Could not rename file', error) + } + + return this.replaceImageAsync(this.state) + } + replaceImageAsync = async ({ alt, src, title }) => { if (!this.muya.options.imageAction || URL_REG.test(src)) { const { alt: oldAlt, src: oldSrc, title: oldTitle } = this.imageInfo.token.attrs @@ -255,14 +282,14 @@ class ImageSelector extends BaseFloat { try { const newSrc = await this.muya.options.imageAction(src, id, alt) - const { src: localPath } = getImageSrc(src) + const { src: localPath } = getImageSrc(src, this.muya.options) if (localPath) { this.muya.contentState.stateRender.urlMap.set(newSrc, localPath) } const imageWrapper = this.muya.container.querySelector(`span[data-id=${id}]`) if (imageWrapper) { - const imageInfo = getImageInfo(imageWrapper) + const imageInfo = getImageInfo(imageWrapper, this.muya.options) this.muya.contentState.replaceImage(imageInfo, { alt, src: newSrc, @@ -270,8 +297,7 @@ class ImageSelector extends BaseFloat { }) } } catch (error) { - // TODO: Notify user about an error. - console.error('Unexpected error on image action:', error) + ipcRenderer.send('mt::show-user-notification-dialog', 'Error while updating image', error) } } else { this.hide() @@ -302,6 +328,9 @@ class ImageSelector extends BaseFloat { }, { label: 'Embed link', value: 'link' + }, { + label: 'Rename', + value: 'rename' }] if (this.unsplash) { @@ -418,6 +447,34 @@ class ImageSelector extends BaseFloat { }, `${isFullMode ? 'simple mode' : 'full mode'}.`) ]) bodyContent = [inputWrapper, embedButton, bottomDes] + } else if (tab === 'rename') { + const srcInput = h('input.src', { + props: { + placeholder: 'New image link or local path', + value: src + }, + on: { + input: event => { + this.inputHandler(event, 'src') + }, + paste: event => { + this.inputHandler(event, 'src') + }, + keydown: event => { + this.renameInputKeyDown(event) + } + } + }) + + const inputWrapper = h('div.input-container', [srcInput]) + const renameButton = h('button.muya-button.role-button.link', { + on: { + click: event => { + this.handleRenameButtonClick() + } + } + }, 'Rename Image') + bodyContent = [inputWrapper, renameButton] } else { const searchInput = h('input.search', { props: { diff --git a/src/muya/lib/utils/importMarkdown.js b/src/muya/lib/utils/importMarkdown.js index 5f367804..f1a4f492 100644 --- a/src/muya/lib/utils/importMarkdown.js +++ b/src/muya/lib/utils/importMarkdown.js @@ -611,7 +611,7 @@ const importRegister = ContentState => { const rawSrc = label + backlash.second if (render.labels.has((rawSrc).toLowerCase())) { const { href } = render.labels.get(rawSrc.toLowerCase()) - const { src } = getImageInfo(href) + const { src } = getImageInfo(href, this.muya.options) if (src) { results.add(src) } diff --git a/src/muya/lib/utils/index.js b/src/muya/lib/utils/index.js index c28f6d16..aff9376c 100644 --- a/src/muya/lib/utils/index.js +++ b/src/muya/lib/utils/index.js @@ -251,14 +251,30 @@ export const checkImageContentType = url => { * Return image information and correct the relative image path if needed. * * @param {string} src Image url - * @param {string} baseUrl Base path; used on desktop to fix the relative image path. + * @param {object} options The muya options object representing user preferences. */ -export const getImageInfo = (src, baseUrl = window.DIRNAME) => { +export const getImageInfo = (src, options) => { + const baseUrl = window.DIRNAME const imageExtension = IMAGE_EXT_REG.test(src) const isUrl = URL_REG.test(src) || (imageExtension && /^file:\/\/.+/.test(src)) // Treat an URL with valid extension as image. if (imageExtension) { + if (options) { + const { serverFolderPath, localFolderPath } = options + + // Do server / local path mapping during preview if desired + if (serverFolderPath) { + if (src.toLowerCase().includes(serverFolderPath.toLowerCase())) { + const escapedServerFolderPath = serverFolderPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + const regex = new RegExp(escapedServerFolderPath, 'ig') + src = src.replace(regex, localFolderPath) + src = src.replace('//', '/') + src = src.replace('\\\\', '\\') + } + } + } + // NOTE: Check both "C:\" and "C:/" because we're using "file:///C:/". const isAbsoluteLocal = /^(?:\/|\\\\|[a-zA-Z]:\\|[a-zA-Z]:\/).+/.test(src) @@ -272,11 +288,16 @@ export const getImageInfo = (src, baseUrl = window.DIRNAME) => { src } } else { - // Correct relative path on desktop. If we resolve a absolute path "path.resolve" doesn't do anything. - // NOTE: We don't need to convert Windows styled path to UNIX style because Chromium handels this internal. + // Correct relative path on desktop. + // NOTE: We don't need to convert Windows styled path to UNIX style because Chromium handles this internally. + if (!isAbsoluteLocal) { + src = require('path').resolve(baseUrl, src) + } else { + src = require('path').resolve(src) + } return { isUnknownType: false, - src: 'file://' + require('path').resolve(baseUrl, src) + src: 'file://' + src } } } else if (isUrl && !imageExtension) { diff --git a/src/renderer/components/editorWithTabs/editor.vue b/src/renderer/components/editorWithTabs/editor.vue index 0b407039..343fe09c 100644 --- a/src/renderer/components/editorWithTabs/editor.vue +++ b/src/renderer/components/editorWithTabs/editor.vue @@ -156,6 +156,8 @@ export default { autoCheck: state => state.preferences.autoCheck, editorLineWidth: state => state.preferences.editorLineWidth, imageInsertAction: state => state.preferences.imageInsertAction, + serverFolderPath: state => state.preferences.serverFolderPath, + localFolderPath: state => state.preferences.localFolderPath, imagePreferRelativeDirectory: state => state.preferences.imagePreferRelativeDirectory, imageRelativeDirectoryName: state => state.preferences.imageRelativeDirectoryName, imageFolderPath: state => state.preferences.imageFolderPath, @@ -549,7 +551,7 @@ export default { }) } - const { container } = this.editor = new Muya(ele, options) + const { container } = this.editor = new Muya(ele, options, this.$store.state.preferences) // Create spell check wrapper and enable spell checking if preferred. this.spellchecker = new SpellChecker(spellcheckerEnabled, spellcheckerLanguage) @@ -717,7 +719,15 @@ export default { // Filename w/o extension ? filename.replace(/\.[^/.]+$/, '') : '' - return imagePath.replace(/\${filename}/g, replacement) + imagePath = imagePath.replace(/\${filename}/g, replacement) + + // Support year (numeric), month (2-digit), and day (2-digit) for image path replacements + const today = new Date() + imagePath = imagePath.replace(/\${year}/g, today.toLocaleDateString('en-US', { year: 'numeric' })) + imagePath = imagePath.replace(/\${month}/g, today.toLocaleDateString('en-US', { month: '2-digit' })) + imagePath = imagePath.replace(/\${day}/g, today.toLocaleDateString('en-US', { day: '2-digit' })) + + return imagePath } const resolvedImageFolderPath = getResolvedImagePath(imageFolderPath) @@ -768,6 +778,20 @@ export default { alt }) } + + // Map local paths to server paths if the user desires + if (this.serverFolderPath) { + if (destImagePath.toLowerCase().includes(this.localFolderPath.toLowerCase())) { + const escapedLocalFolderPath = this.localFolderPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + const regex = new RegExp(escapedLocalFolderPath, 'ig') + destImagePath = destImagePath.replace(regex, this.serverFolderPath) + + // Convert slash direction since the user is wanting URLs to represent server paths + destImagePath = destImagePath.replace(/\\/g, '/') + destImagePath = destImagePath.replace(/\/\//g, '/') + } + } + return destImagePath }, diff --git a/src/renderer/components/userNotification/index.vue b/src/renderer/components/userNotification/index.vue new file mode 100644 index 00000000..0c782397 --- /dev/null +++ b/src/renderer/components/userNotification/index.vue @@ -0,0 +1,50 @@ + + + + + diff --git a/src/renderer/pages/app.vue b/src/renderer/pages/app.vue index cd1784a7..d206e8b9 100644 --- a/src/renderer/pages/app.vue +++ b/src/renderer/pages/app.vue @@ -31,6 +31,7 @@ + @@ -48,6 +49,7 @@ import ExportSettingDialog from '@/components/exportSettings' import Rename from '@/components/rename' import Tweet from '@/components/tweet' import ImportModal from '@/components/import' +import UserNotification from '@/components/userNotification' import { loadingPageMixins } from '@/mixins' import { mapState } from 'vuex' import bus from '@/bus' @@ -65,6 +67,7 @@ export default { ExportSettingDialog, Rename, Tweet, + UserNotification, ImportModal, CommandPalette }, diff --git a/src/renderer/prefComponents/image/components/folderSetting/index.vue b/src/renderer/prefComponents/image/components/folderSetting/index.vue index 804f723d..ee76c79b 100644 --- a/src/renderer/prefComponents/image/components/folderSetting/index.vue +++ b/src/renderer/prefComponents/image/components/folderSetting/index.vue @@ -1,6 +1,9 @@ diff --git a/src/renderer/prefComponents/image/components/serverPathSetting/index.vue b/src/renderer/prefComponents/image/components/serverPathSetting/index.vue new file mode 100644 index 00000000..2e23fba1 --- /dev/null +++ b/src/renderer/prefComponents/image/components/serverPathSetting/index.vue @@ -0,0 +1,50 @@ + + + + + diff --git a/src/renderer/prefComponents/image/index.vue b/src/renderer/prefComponents/image/index.vue index aadaf568..d86c7444 100644 --- a/src/renderer/prefComponents/image/index.vue +++ b/src/renderer/prefComponents/image/index.vue @@ -13,6 +13,8 @@ :onChange="value => onSelectChange('imageInsertAction', value)"> + + @@ -23,12 +25,14 @@ import Separator from '../common/separator' import Uploader from './components/uploader' import CurSelect from '@/prefComponents/common/select' import FolderSetting from './components/folderSetting' +import ServerPathSetting from './components/serverPathSetting' import { imageActions } from './config' export default { components: { Separator, CurSelect, + ServerPathSetting, FolderSetting, Uploader }, diff --git a/src/renderer/services/printService.js b/src/renderer/services/printService.js index 136761aa..6f8c32d5 100644 --- a/src/renderer/services/printService.js +++ b/src/renderer/services/printService.js @@ -20,7 +20,7 @@ class MarkdownPrint { const images = printContainer.getElementsByTagName('img') for (const image of images) { const rawSrc = image.getAttribute('src') - image.src = getImageInfo(rawSrc).src + image.src = getImageInfo(rawSrc, this.muya.options).src } } document.body.appendChild(printContainer) diff --git a/src/renderer/store/preferences.js b/src/renderer/store/preferences.js index 76a66009..251fdafe 100644 --- a/src/renderer/store/preferences.js +++ b/src/renderer/store/preferences.js @@ -35,6 +35,8 @@ const state = { textDirection: 'ltr', hideQuickInsertHint: false, imageInsertAction: 'folder', + serverFolderPath: '', + localFolderPath: '', imagePreferRelativeDirectory: false, imageRelativeDirectoryName: 'assets', hideLinkPopup: false, @@ -137,6 +139,10 @@ const actions = { ipcRenderer.send('mt::ask-for-modify-image-folder-path', value) }, + SET_LOCAL_FOLDER_PATH ({ commit }, value) { + ipcRenderer.send('mt::ask-for-modify-local-folder-path', value) + }, + SELECT_DEFAULT_DIRECTORY_TO_OPEN ({ commit }) { ipcRenderer.send('mt::select-default-directory-to-open') }, diff --git a/src/renderer/util/fileSystem.js b/src/renderer/util/fileSystem.js index d2b55137..ae5efd35 100644 --- a/src/renderer/util/fileSystem.js +++ b/src/renderer/util/fileSystem.js @@ -79,14 +79,22 @@ export const moveImageToFolder = async (pathname, image, outputDir) => { const filename = path.basename(imagePath) const extname = path.extname(imagePath) const noHashPath = path.join(outputDir, filename) + + // If the file doesn't need to be moved, return it if (noHashPath === imagePath) { return imagePath } - const hash = getContentHash(imagePath) - // To avoid name conflict. - const hashFilePath = path.join(outputDir, `${hash}${extname}`) - await fs.copy(imagePath, hashFilePath) - return hashFilePath + + let targetPath = noHashPath + if (fs.existsSync(targetPath)) { + // If a file already exists in the target location, use the image's content hash to avoid + // name conflict. + const hash = getContentHash(imagePath) + targetPath = path.join(outputDir, `${hash}${extname}`) + } + + await fs.copy(imagePath, targetPath) + return targetPath } else { return Promise.resolve(image) } diff --git a/static/preference.json b/static/preference.json index a3e60cc2..8446bb0f 100644 --- a/static/preference.json +++ b/static/preference.json @@ -31,6 +31,8 @@ "textDirection": "ltr", "hideQuickInsertHint": false, "imageInsertAction": "path", + "serverFolderPath": "", + "localFolderPath": "", "imagePreferRelativeDirectory": false, "imageRelativeDirectoryName": "assets", "hideLinkPopup": false,