This commit is contained in:
Lee Holmes 2024-06-10 10:49:40 -07:00 committed by GitHub
commit a665b5f0df
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 317 additions and 39 deletions

View File

@ -2,19 +2,34 @@
MarkText can automatically copy your images into a specified directory or handle images from clipboard. 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 ### Upload to cloud using selected uploader
Please see [here](IMAGE_UPLOADER_CONFIGRATION.md) for more information. Please see [here](IMAGE_UPLOADER_CONFIGRATION.md) for more information.
### Move to designated local folder ### 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:** **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. 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.
Note: The assets directory name must be a valid path name and MarkText need write access to the directory.
Examples for relative paths: Examples for relative paths:
@ -23,6 +38,7 @@ Examples for relative paths:
- `.`: current file directory - `.`: current file directory
- `assets/123` - `assets/123`
- `assets_${filename}` (add the document file name) - `assets_${filename}` (add the document file name)
- `assets/${year}/${month}` (save the assets into year and month subdirectories)
### Keep original location ### Keep original location

View File

@ -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', () => { ipcMain.on('mt::open-setting-window', () => {
this._openSettingsWindow() this._openSettingsWindow()
}) })

View File

@ -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) => { ipcMain.on('mt::set-user-data', (e, userData) => {
this.setItems(userData) this.setItems(userData)
}) })

View File

@ -349,6 +349,16 @@
], ],
"default": "path" "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": { "imagePreferRelativeDirectory": {
"description": "Image--Whether to prefer the relative image directory.", "description": "Image--Whether to prefer the relative image directory.",
"type": "boolean", "type": "boolean",

View File

@ -160,14 +160,14 @@ const dragDropCtrl = ContentState => {
try { try {
const newSrc = await this.muya.options.imageAction(path, id, name) const newSrc = await this.muya.options.imageAction(path, id, name)
const { src } = getImageSrc(path) const { src } = getImageSrc(path, this.muya.options)
if (src) { if (src) {
this.stateRender.urlMap.set(newSrc, src) this.stateRender.urlMap.set(newSrc, src)
} }
const imageWrapper = this.muya.container.querySelector(`span[data-id=${id}]`) const imageWrapper = this.muya.container.querySelector(`span[data-id=${id}]`)
if (imageWrapper) { if (imageWrapper) {
const imageInfo = getImageInfo(imageWrapper) const imageInfo = getImageInfo(imageWrapper, this.muya.options)
this.replaceImage(imageInfo, { this.replaceImage(imageInfo, {
alt: name, alt: name,
src: newSrc src: newSrc

View File

@ -284,7 +284,7 @@ const formatCtrl = ContentState => {
if (startNode) { if (startNode) {
const imageWrapper = startNode.closest('.ag-inline-image') const imageWrapper = startNode.closest('.ag-inline-image')
if (imageWrapper && imageWrapper.classList.contains('ag-empty-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', { this.muya.eventCenter.dispatch('muya-image-selector', {
reference: imageWrapper, reference: imageWrapper,
imageInfo, imageInfo,

View File

@ -150,7 +150,7 @@ const pasteCtrl = ContentState => {
return null return null
} }
const { src } = getImageSrc(imagePath) const { src } = getImageSrc(imagePath, this.muya.options)
if (src) { if (src) {
this.stateRender.urlMap.set(newSrc, src) this.stateRender.urlMap.set(newSrc, src)
} }
@ -158,7 +158,7 @@ const pasteCtrl = ContentState => {
const imageWrapper = this.muya.container.querySelector(`span[data-id=${id}]`) const imageWrapper = this.muya.container.querySelector(`span[data-id=${id}]`)
if (imageWrapper) { if (imageWrapper) {
const imageInfo = getImageInfo(imageWrapper) const imageInfo = getImageInfo(imageWrapper, this.muya.options)
this.replaceImage(imageInfo, { this.replaceImage(imageInfo, {
src: newSrc src: newSrc
}) })
@ -225,7 +225,7 @@ const pasteCtrl = ContentState => {
const imageWrapper = this.muya.container.querySelector(`span[data-id=${id}]`) const imageWrapper = this.muya.container.querySelector(`span[data-id=${id}]`)
if (imageWrapper) { if (imageWrapper) {
const imageInfo = getImageInfo(imageWrapper) const imageInfo = getImageInfo(imageWrapper, this.muya.options)
this.replaceImage(imageInfo, { this.replaceImage(imageInfo, {
src: newSrc src: newSrc
}) })

View File

@ -127,7 +127,7 @@ class ClickEvent {
} }
// Handle delete inline iamge by click delete icon. // Handle delete inline iamge by click delete icon.
if (imageDelete && imageWrapper) { if (imageDelete && imageWrapper) {
const imageInfo = getImageInfo(imageWrapper) const imageInfo = getImageInfo(imageWrapper, this.muya.options)
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
// hide image selector if needed. // hide image selector if needed.
@ -152,7 +152,7 @@ class ClickEvent {
// Handle image click, to select the current image // Handle image click, to select the current image
if (target.tagName === 'IMG' && imageWrapper) { if (target.tagName === 'IMG' && imageWrapper) {
// Handle select image // Handle select image
const imageInfo = getImageInfo(imageWrapper) const imageInfo = getImageInfo(imageWrapper, this.muya.options)
event.preventDefault() event.preventDefault()
eventCenter.dispatch('select-image', imageInfo) eventCenter.dispatch('select-image', imageInfo)
// Handle show image toolbar // Handle show image toolbar
@ -197,7 +197,7 @@ class ClickEvent {
return rect return rect
} }
} }
const imageInfo = getImageInfo(imageWrapper) const imageInfo = getImageInfo(imageWrapper, this.muya.options)
eventCenter.dispatch('muya-image-selector', { eventCenter.dispatch('muya-image-selector', {
reference, reference,
imageInfo, imageInfo,

View File

@ -103,7 +103,7 @@ class Keyboard {
case EVENT_KEYS.Space: { case EVENT_KEYS.Space: {
if (contentState.selectedImage) { if (contentState.selectedImage) {
const { token } = 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) { if (src) {
eventCenter.dispatch('preview-image', { eventCenter.dispatch('preview-image', {
data: src data: src

View File

@ -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({}, MUYA_DEFAULT_OPTION, options)
this.options = Object.assign(this.options, preferences)
const { markdown } = this.options const { markdown } = this.options
this.markdown = markdown this.markdown = markdown
this.container = getContainer(container, this.options) this.container = getContainer(container, this.options)

View File

@ -143,7 +143,7 @@ export default function renderLeafBlock (parent, block, activeBlocks, matches, u
const imgs = doc.documentElement.querySelectorAll('img') const imgs = doc.documentElement.querySelectorAll('img')
for (const img of imgs) { for (const img of imgs) {
const src = img.getAttribute('src') const src = img.getAttribute('src')
const imageInfo = getImageInfo(src) const imageInfo = getImageInfo(src, this.muya.options)
img.setAttribute('src', imageInfo.src) img.setAttribute('src', imageInfo.src)
} }

View File

@ -22,7 +22,7 @@ const renderIcon = (h, className, icon) => {
// I dont want operate dom directly, is there any better method? need help! // I dont want operate dom directly, is there any better method? need help!
export default function image (h, cursor, block, token, outerClass) { 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 { selectedImage } = this.muya.contentState
const data = { const data = {
dataset: { dataset: {

View File

@ -14,7 +14,7 @@ export default function referenceImage (h, cursor, block, token, outerClass) {
if (this.labels.has((rawSrc).toLowerCase())) { if (this.labels.has((rawSrc).toLowerCase())) {
({ href, title } = this.labels.get(rawSrc.toLowerCase())) ({ href, title } = this.labels.get(rawSrc.toLowerCase()))
} }
const imageInfo = getImageInfo(href) const imageInfo = getImageInfo(href, this.muya.options)
const { src } = imageInfo const { src } = imageInfo
let id let id
let isSuccess let isSuccess

View File

@ -4,6 +4,8 @@ import { patch, h } from '../../parser/render/snabbdom'
import { EVENT_KEYS, URL_REG, isWin } from '../../config' import { EVENT_KEYS, URL_REG, isWin } from '../../config'
import { getUniqueId, getImageInfo as getImageSrc } from '../../utils' import { getUniqueId, getImageInfo as getImageSrc } from '../../utils'
import { getImageInfo } from '../../utils/getImageInfo' import { getImageInfo } from '../../utils/getImageInfo'
import fs from 'fs-extra'
import { ipcRenderer } from 'electron'
import './index.css' 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) { async handleKeyUp (event) {
const { key } = event const { key } = event
if ( if (
@ -236,6 +245,24 @@ class ImageSelector extends BaseFloat {
return this.replaceImageAsync(this.state) 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 }) => { replaceImageAsync = async ({ alt, src, title }) => {
if (!this.muya.options.imageAction || URL_REG.test(src)) { if (!this.muya.options.imageAction || URL_REG.test(src)) {
const { alt: oldAlt, src: oldSrc, title: oldTitle } = this.imageInfo.token.attrs const { alt: oldAlt, src: oldSrc, title: oldTitle } = this.imageInfo.token.attrs
@ -255,14 +282,14 @@ class ImageSelector extends BaseFloat {
try { try {
const newSrc = await this.muya.options.imageAction(src, id, alt) 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) { if (localPath) {
this.muya.contentState.stateRender.urlMap.set(newSrc, localPath) this.muya.contentState.stateRender.urlMap.set(newSrc, localPath)
} }
const imageWrapper = this.muya.container.querySelector(`span[data-id=${id}]`) const imageWrapper = this.muya.container.querySelector(`span[data-id=${id}]`)
if (imageWrapper) { if (imageWrapper) {
const imageInfo = getImageInfo(imageWrapper) const imageInfo = getImageInfo(imageWrapper, this.muya.options)
this.muya.contentState.replaceImage(imageInfo, { this.muya.contentState.replaceImage(imageInfo, {
alt, alt,
src: newSrc, src: newSrc,
@ -270,8 +297,7 @@ class ImageSelector extends BaseFloat {
}) })
} }
} catch (error) { } catch (error) {
// TODO: Notify user about an error. ipcRenderer.send('mt::show-user-notification-dialog', 'Error while updating image', error)
console.error('Unexpected error on image action:', error)
} }
} else { } else {
this.hide() this.hide()
@ -302,6 +328,9 @@ class ImageSelector extends BaseFloat {
}, { }, {
label: 'Embed link', label: 'Embed link',
value: 'link' value: 'link'
}, {
label: 'Rename',
value: 'rename'
}] }]
if (this.unsplash) { if (this.unsplash) {
@ -418,6 +447,34 @@ class ImageSelector extends BaseFloat {
}, `${isFullMode ? 'simple mode' : 'full mode'}.`) }, `${isFullMode ? 'simple mode' : 'full mode'}.`)
]) ])
bodyContent = [inputWrapper, embedButton, bottomDes] 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 { } else {
const searchInput = h('input.search', { const searchInput = h('input.search', {
props: { props: {

View File

@ -611,7 +611,7 @@ const importRegister = ContentState => {
const rawSrc = label + backlash.second const rawSrc = label + backlash.second
if (render.labels.has((rawSrc).toLowerCase())) { if (render.labels.has((rawSrc).toLowerCase())) {
const { href } = render.labels.get(rawSrc.toLowerCase()) const { href } = render.labels.get(rawSrc.toLowerCase())
const { src } = getImageInfo(href) const { src } = getImageInfo(href, this.muya.options)
if (src) { if (src) {
results.add(src) results.add(src)
} }

View File

@ -251,14 +251,30 @@ export const checkImageContentType = url => {
* Return image information and correct the relative image path if needed. * Return image information and correct the relative image path if needed.
* *
* @param {string} src Image url * @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 imageExtension = IMAGE_EXT_REG.test(src)
const isUrl = URL_REG.test(src) || (imageExtension && /^file:\/\/.+/.test(src)) const isUrl = URL_REG.test(src) || (imageExtension && /^file:\/\/.+/.test(src))
// Treat an URL with valid extension as image. // Treat an URL with valid extension as image.
if (imageExtension) { 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:/". // NOTE: Check both "C:\" and "C:/" because we're using "file:///C:/".
const isAbsoluteLocal = /^(?:\/|\\\\|[a-zA-Z]:\\|[a-zA-Z]:\/).+/.test(src) const isAbsoluteLocal = /^(?:\/|\\\\|[a-zA-Z]:\\|[a-zA-Z]:\/).+/.test(src)
@ -272,11 +288,16 @@ export const getImageInfo = (src, baseUrl = window.DIRNAME) => {
src src
} }
} else { } else {
// Correct relative path on desktop. If we resolve a absolute path "path.resolve" doesn't do anything. // Correct relative path on desktop.
// NOTE: We don't need to convert Windows styled path to UNIX style because Chromium handels this internal. // 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 { return {
isUnknownType: false, isUnknownType: false,
src: 'file://' + require('path').resolve(baseUrl, src) src: 'file://' + src
} }
} }
} else if (isUrl && !imageExtension) { } else if (isUrl && !imageExtension) {

View File

@ -156,6 +156,8 @@ export default {
autoCheck: state => state.preferences.autoCheck, autoCheck: state => state.preferences.autoCheck,
editorLineWidth: state => state.preferences.editorLineWidth, editorLineWidth: state => state.preferences.editorLineWidth,
imageInsertAction: state => state.preferences.imageInsertAction, imageInsertAction: state => state.preferences.imageInsertAction,
serverFolderPath: state => state.preferences.serverFolderPath,
localFolderPath: state => state.preferences.localFolderPath,
imagePreferRelativeDirectory: state => state.preferences.imagePreferRelativeDirectory, imagePreferRelativeDirectory: state => state.preferences.imagePreferRelativeDirectory,
imageRelativeDirectoryName: state => state.preferences.imageRelativeDirectoryName, imageRelativeDirectoryName: state => state.preferences.imageRelativeDirectoryName,
imageFolderPath: state => state.preferences.imageFolderPath, 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. // Create spell check wrapper and enable spell checking if preferred.
this.spellchecker = new SpellChecker(spellcheckerEnabled, spellcheckerLanguage) this.spellchecker = new SpellChecker(spellcheckerEnabled, spellcheckerLanguage)
@ -717,7 +719,15 @@ export default {
// Filename w/o extension // Filename w/o extension
? filename.replace(/\.[^/.]+$/, '') ? 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) const resolvedImageFolderPath = getResolvedImagePath(imageFolderPath)
@ -768,6 +778,20 @@ export default {
alt 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 return destImagePath
}, },

View File

@ -0,0 +1,50 @@
<template>
<div class="user-notification-dialog">
<el-dialog
:visible.sync="showUserNotificationDialog"
:show-close="true"
:modal="true"
custom-class="ag-dialog-table"
width="400px"
>
<h3>{{ title }}</h3>
<span class="text">{{ message }}</span>
</el-dialog>
</div>
</template>
<script>
import bus from '../../bus'
export default {
data () {
this.title = ''
this.message = ''
return {
showUserNotificationDialog: false
}
},
created () {
require('electron').ipcRenderer.on('showUserNotificationDialog', this.showDialog)
},
beforeDestroy () {
require('electron').ipcRenderer.off('showUserNotificationDialog', this.showDialog)
},
methods: {
showDialog (e, title, message) {
this.title = title
this.message = message
this.showUserNotificationDialog = true
bus.$emit('editor-blur')
}
}
}
</script>
<style scoped>
.text {
white-space: pre-wrap;
word-break: break-word;
}
</style>

View File

@ -31,6 +31,7 @@
<export-setting-dialog></export-setting-dialog> <export-setting-dialog></export-setting-dialog>
<rename></rename> <rename></rename>
<tweet></tweet> <tweet></tweet>
<userNotification></userNotification>
<import-modal></import-modal> <import-modal></import-modal>
</div> </div>
</div> </div>
@ -48,6 +49,7 @@ import ExportSettingDialog from '@/components/exportSettings'
import Rename from '@/components/rename' import Rename from '@/components/rename'
import Tweet from '@/components/tweet' import Tweet from '@/components/tweet'
import ImportModal from '@/components/import' import ImportModal from '@/components/import'
import UserNotification from '@/components/userNotification'
import { loadingPageMixins } from '@/mixins' import { loadingPageMixins } from '@/mixins'
import { mapState } from 'vuex' import { mapState } from 'vuex'
import bus from '@/bus' import bus from '@/bus'
@ -65,6 +67,7 @@ export default {
ExportSettingDialog, ExportSettingDialog,
Rename, Rename,
Tweet, Tweet,
UserNotification,
ImportModal, ImportModal,
CommandPalette CommandPalette
}, },

View File

@ -1,6 +1,9 @@
<template> <template>
<section class="image-folder"> <section class="image-folder">
<h5>Global or relative image folder</h5> <h5>Global or relative image folder</h5>
<div class="footnote">
Use variables such as <code>${filename}</code> in paths to automatically insert the document file name.
</div>
<text-box description="Global image folder" :input="imageFolderPath" <text-box description="Global image folder" :input="imageFolderPath"
:regexValidator="/^(?:$|([a-zA-Z]:)?[\/\\].*$)/" :defaultValue="folderPathPlaceholder" :regexValidator="/^(?:$|([a-zA-Z]:)?[\/\\].*$)/" :defaultValue="folderPathPlaceholder"
:onChange="value => modifyImageFolderPath(value)"></text-box> :onChange="value => modifyImageFolderPath(value)"></text-box>
@ -20,9 +23,6 @@
:regexValidator="/^(?:$|(?![a-zA-Z]:)[^\/\\].*$)/" :regexValidator="/^(?:$|(?![a-zA-Z]:)[^\/\\].*$)/"
:defaultValue="relativeDirectoryNamePlaceholder" :defaultValue="relativeDirectoryNamePlaceholder"
:onChange="value => onSelectChange('imageRelativeDirectoryName', value)"></text-box> :onChange="value => onSelectChange('imageRelativeDirectoryName', value)"></text-box>
<div class="footnote">
Include <code>${filename}</code> in the text-box above to automatically insert the document file name.
</div>
</template> </template>
</compound> </compound>
</section> </section>

View File

@ -0,0 +1,50 @@
<template>
<section class="server-path-folder">
<h5>Maintain server paths during editing and preview</h5>
<text-box description="Server path (i.e.: /images/)" :input="serverFolderPath"
:onChange="value => onSelectChange('serverFolderPath', value)"></text-box>
<text-box description="Local path (relative or absolute)" :input="localFolderPath"
:onChange="value => modifyLocalFolderPath(value)"></text-box>
<div>
<el-button size="mini" @click="modifyLocalFolderPath(undefined)">Browse...</el-button>
</div>
</section>
</template>
<script>
import { mapState } from 'vuex'
import TextBox from '@/prefComponents/common/textBox'
export default {
components: {
TextBox
},
data () {
return {
}
},
computed: {
...mapState({
serverFolderPath: state => state.preferences.serverFolderPath,
localFolderPath: state => state.preferences.localFolderPath
})
},
methods: {
modifyLocalFolderPath (value) {
return this.$store.dispatch('SET_LOCAL_FOLDER_PATH', value)
},
onSelectChange (type, value) {
this.$store.dispatch('SET_SINGLE_PREFERENCE', { type, value })
}
}
}
</script>
<style scoped>
.image-folder .footnote {
font-size: 13px;
& code {
font-size: 13px;
}
}
</style>

View File

@ -13,6 +13,8 @@
:onChange="value => onSelectChange('imageInsertAction', value)"></CurSelect> :onChange="value => onSelectChange('imageInsertAction', value)"></CurSelect>
</section> </section>
<Separator /> <Separator />
<ServerPathSetting v-if="imageInsertAction === 'folder' || imageInsertAction === 'path'" />
<Separator />
<FolderSetting v-if="imageInsertAction === 'folder' || imageInsertAction === 'path'" /> <FolderSetting v-if="imageInsertAction === 'folder' || imageInsertAction === 'path'" />
<Uploader v-if="imageInsertAction === 'upload'" /> <Uploader v-if="imageInsertAction === 'upload'" />
</div> </div>
@ -23,12 +25,14 @@ import Separator from '../common/separator'
import Uploader from './components/uploader' import Uploader from './components/uploader'
import CurSelect from '@/prefComponents/common/select' import CurSelect from '@/prefComponents/common/select'
import FolderSetting from './components/folderSetting' import FolderSetting from './components/folderSetting'
import ServerPathSetting from './components/serverPathSetting'
import { imageActions } from './config' import { imageActions } from './config'
export default { export default {
components: { components: {
Separator, Separator,
CurSelect, CurSelect,
ServerPathSetting,
FolderSetting, FolderSetting,
Uploader Uploader
}, },

View File

@ -20,7 +20,7 @@ class MarkdownPrint {
const images = printContainer.getElementsByTagName('img') const images = printContainer.getElementsByTagName('img')
for (const image of images) { for (const image of images) {
const rawSrc = image.getAttribute('src') const rawSrc = image.getAttribute('src')
image.src = getImageInfo(rawSrc).src image.src = getImageInfo(rawSrc, this.muya.options).src
} }
} }
document.body.appendChild(printContainer) document.body.appendChild(printContainer)

View File

@ -35,6 +35,8 @@ const state = {
textDirection: 'ltr', textDirection: 'ltr',
hideQuickInsertHint: false, hideQuickInsertHint: false,
imageInsertAction: 'folder', imageInsertAction: 'folder',
serverFolderPath: '',
localFolderPath: '',
imagePreferRelativeDirectory: false, imagePreferRelativeDirectory: false,
imageRelativeDirectoryName: 'assets', imageRelativeDirectoryName: 'assets',
hideLinkPopup: false, hideLinkPopup: false,
@ -137,6 +139,10 @@ const actions = {
ipcRenderer.send('mt::ask-for-modify-image-folder-path', value) 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 }) { SELECT_DEFAULT_DIRECTORY_TO_OPEN ({ commit }) {
ipcRenderer.send('mt::select-default-directory-to-open') ipcRenderer.send('mt::select-default-directory-to-open')
}, },

View File

@ -79,14 +79,22 @@ export const moveImageToFolder = async (pathname, image, outputDir) => {
const filename = path.basename(imagePath) const filename = path.basename(imagePath)
const extname = path.extname(imagePath) const extname = path.extname(imagePath)
const noHashPath = path.join(outputDir, filename) const noHashPath = path.join(outputDir, filename)
// If the file doesn't need to be moved, return it
if (noHashPath === imagePath) { if (noHashPath === imagePath) {
return imagePath return imagePath
} }
const hash = getContentHash(imagePath)
// To avoid name conflict. let targetPath = noHashPath
const hashFilePath = path.join(outputDir, `${hash}${extname}`) if (fs.existsSync(targetPath)) {
await fs.copy(imagePath, hashFilePath) // If a file already exists in the target location, use the image's content hash to avoid
return hashFilePath // name conflict.
const hash = getContentHash(imagePath)
targetPath = path.join(outputDir, `${hash}${extname}`)
}
await fs.copy(imagePath, targetPath)
return targetPath
} else { } else {
return Promise.resolve(image) return Promise.resolve(image)
} }

View File

@ -31,6 +31,8 @@
"textDirection": "ltr", "textDirection": "ltr",
"hideQuickInsertHint": false, "hideQuickInsertHint": false,
"imageInsertAction": "path", "imageInsertAction": "path",
"serverFolderPath": "",
"localFolderPath": "",
"imagePreferRelativeDirectory": false, "imagePreferRelativeDirectory": false,
"imageRelativeDirectoryName": "assets", "imageRelativeDirectoryName": "assets",
"hideLinkPopup": false, "hideLinkPopup": false,