mirror of
https://github.com/marktext/marktext.git
synced 2025-05-02 09:40:47 +08:00
Merge 5ffc026a28
into 11c8cc1e19
This commit is contained in:
commit
a665b5f0df
@ -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
|
||||
|
||||
|
@ -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()
|
||||
})
|
||||
|
@ -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)
|
||||
})
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
})
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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: {
|
||||
|
@ -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
|
||||
|
@ -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: {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
},
|
||||
|
||||
|
50
src/renderer/components/userNotification/index.vue
Normal file
50
src/renderer/components/userNotification/index.vue
Normal 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>
|
@ -31,6 +31,7 @@
|
||||
<export-setting-dialog></export-setting-dialog>
|
||||
<rename></rename>
|
||||
<tweet></tweet>
|
||||
<userNotification></userNotification>
|
||||
<import-modal></import-modal>
|
||||
</div>
|
||||
</div>
|
||||
@ -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
|
||||
},
|
||||
|
@ -1,6 +1,9 @@
|
||||
<template>
|
||||
<section class="image-folder">
|
||||
<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"
|
||||
:regexValidator="/^(?:$|([a-zA-Z]:)?[\/\\].*$)/" :defaultValue="folderPathPlaceholder"
|
||||
:onChange="value => modifyImageFolderPath(value)"></text-box>
|
||||
@ -20,9 +23,6 @@
|
||||
:regexValidator="/^(?:$|(?![a-zA-Z]:)[^\/\\].*$)/"
|
||||
:defaultValue="relativeDirectoryNamePlaceholder"
|
||||
: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>
|
||||
</compound>
|
||||
</section>
|
||||
|
@ -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>
|
@ -13,6 +13,8 @@
|
||||
:onChange="value => onSelectChange('imageInsertAction', value)"></CurSelect>
|
||||
</section>
|
||||
<Separator />
|
||||
<ServerPathSetting v-if="imageInsertAction === 'folder' || imageInsertAction === 'path'" />
|
||||
<Separator />
|
||||
<FolderSetting v-if="imageInsertAction === 'folder' || imageInsertAction === 'path'" />
|
||||
<Uploader v-if="imageInsertAction === 'upload'" />
|
||||
</div>
|
||||
@ -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
|
||||
},
|
||||
|
@ -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)
|
||||
|
@ -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')
|
||||
},
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -31,6 +31,8 @@
|
||||
"textDirection": "ltr",
|
||||
"hideQuickInsertHint": false,
|
||||
"imageInsertAction": "path",
|
||||
"serverFolderPath": "",
|
||||
"localFolderPath": "",
|
||||
"imagePreferRelativeDirectory": false,
|
||||
"imageRelativeDirectoryName": "assets",
|
||||
"hideLinkPopup": false,
|
||||
|
Loading…
Reference in New Issue
Block a user