mirror of
https://github.com/marktext/marktext.git
synced 2025-05-02 22:10:24 +08:00
Add per-tab notifications (#1377)
* Add per-tab notifications * fix: file watcher depth on macOS * Free array reference
This commit is contained in:
parent
1cac5dbe52
commit
b386630d3c
@ -100,16 +100,13 @@ class App {
|
|||||||
|
|
||||||
// Prevent to load webview and opening links or new windows via HTML/JS.
|
// Prevent to load webview and opening links or new windows via HTML/JS.
|
||||||
app.on('web-contents-created', (event, contents) => {
|
app.on('web-contents-created', (event, contents) => {
|
||||||
contents.on('will-attach-webview', (event, webPreferences, params) => {
|
contents.on('will-attach-webview', event => {
|
||||||
console.warn('Prevented webview creation.')
|
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
})
|
})
|
||||||
contents.on('will-navigate', event => {
|
contents.on('will-navigate', event => {
|
||||||
console.warn('Prevented opening a link.')
|
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
})
|
})
|
||||||
contents.on('new-window', (event, url) => {
|
contents.on('new-window', event => {
|
||||||
console.warn('Prevented opening a new window.')
|
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -6,7 +6,7 @@ import { exists } from 'common/filesystem'
|
|||||||
import { hasMarkdownExtension } from 'common/filesystem/paths'
|
import { hasMarkdownExtension } from 'common/filesystem/paths'
|
||||||
import { getUniqueId } from '../utils'
|
import { getUniqueId } from '../utils'
|
||||||
import { loadMarkdownFile } from '../filesystem/markdown'
|
import { loadMarkdownFile } from '../filesystem/markdown'
|
||||||
import { isLinux } from '../config'
|
import { isLinux, isOsx } from '../config'
|
||||||
|
|
||||||
// TODO(refactor): Please see GH#1035.
|
// TODO(refactor): Please see GH#1035.
|
||||||
|
|
||||||
@ -159,7 +159,8 @@ class Watcher {
|
|||||||
ignorePermissionErrors: true,
|
ignorePermissionErrors: true,
|
||||||
|
|
||||||
// Just to be sure when a file is replaced with a directory don't watch recursively.
|
// Just to be sure when a file is replaced with a directory don't watch recursively.
|
||||||
depth: type === 'file' ? 0 : undefined,
|
depth: type === 'file'
|
||||||
|
? (isOsx ? 1 : 0) : undefined,
|
||||||
|
|
||||||
// Please see GH#1043
|
// Please see GH#1043
|
||||||
awaitWriteFinish: {
|
awaitWriteFinish: {
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
:text-direction="textDirection"
|
:text-direction="textDirection"
|
||||||
></source-code>
|
></source-code>
|
||||||
</div>
|
</div>
|
||||||
|
<tab-notifications></tab-notifications>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -25,6 +26,7 @@
|
|||||||
import Tabs from './tabs.vue'
|
import Tabs from './tabs.vue'
|
||||||
import Editor from './editor.vue'
|
import Editor from './editor.vue'
|
||||||
import SourceCode from './sourceCode.vue'
|
import SourceCode from './sourceCode.vue'
|
||||||
|
import TabNotifications from './notifications.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
@ -61,7 +63,8 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
Tabs,
|
Tabs,
|
||||||
Editor,
|
Editor,
|
||||||
SourceCode
|
SourceCode,
|
||||||
|
TabNotifications
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
123
src/renderer/components/editorWithTabs/notifications.vue
Normal file
123
src/renderer/components/editorWithTabs/notifications.vue
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="currentNotification"
|
||||||
|
class="editor-notifications"
|
||||||
|
:class="currentNotification.style"
|
||||||
|
:style="{'max-width': showSideBar ? `calc(100vw - ${sideBarWidth}px` : '100vw' }"
|
||||||
|
>
|
||||||
|
<div class="msg">
|
||||||
|
{{ currentNotification.msg }}
|
||||||
|
</div>
|
||||||
|
<div class="controls">
|
||||||
|
<div>
|
||||||
|
<span
|
||||||
|
class="inline-button"
|
||||||
|
v-if="currentNotification.showConfirm"
|
||||||
|
@click.stop="handleClick(true)"
|
||||||
|
>
|
||||||
|
Ok
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="inline-button"
|
||||||
|
@click.stop="handleClick(false)"
|
||||||
|
>
|
||||||
|
<svg class="close-icon icon" aria-hidden="true">
|
||||||
|
<use id="default-close-icon" xlink:href="#icon-close-small"></use>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapState } from 'vuex'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data () {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
currentFile: state => state.editor.currentFile,
|
||||||
|
showSideBar: state => state.layout.showSideBar,
|
||||||
|
sideBarWidth: state => state.layout.sideBarWidth
|
||||||
|
}),
|
||||||
|
currentNotification () {
|
||||||
|
const notifications = this.currentFile.notifications
|
||||||
|
if (!notifications || notifications.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return notifications[0]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleClick (status) {
|
||||||
|
const notifications = this.currentFile.notifications
|
||||||
|
if (!notifications || notifications.length === 0) {
|
||||||
|
console.error('notifications::handleClick: Cannot find notification on stack.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = notifications.shift()
|
||||||
|
const action = item.action
|
||||||
|
if (action) {
|
||||||
|
action(status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.editor-notifications {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
max-height: 100px;
|
||||||
|
margin-top: 4px;
|
||||||
|
background: var(--notificationPrimaryBg);
|
||||||
|
color: var(--notificationPrimaryColor);
|
||||||
|
padding: 8px 10px;
|
||||||
|
user-select: none;
|
||||||
|
overflow: hidden;
|
||||||
|
&.warn {
|
||||||
|
background: var(--notificationWarningBg);
|
||||||
|
color: var(--notificationWarningColor);
|
||||||
|
}
|
||||||
|
&.crit {
|
||||||
|
background: var(--notificationErrorBg);
|
||||||
|
color: var(--notificationErrorColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.msg {
|
||||||
|
font-size: 13px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
& > div {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
& .inline-button:not(:last-child) {
|
||||||
|
margin-right: 3px;
|
||||||
|
}
|
||||||
|
& .inline-button {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
& .inline-button:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -96,29 +96,59 @@ const mutations = {
|
|||||||
const { data, pathname } = change
|
const { data, pathname } = change
|
||||||
const { isMixedLineEndings, lineEnding, adjustLineEndingOnSave, encoding, markdown, filename } = data
|
const { isMixedLineEndings, lineEnding, adjustLineEndingOnSave, encoding, markdown, filename } = data
|
||||||
const options = { encoding, lineEnding, adjustLineEndingOnSave }
|
const options = { encoding, lineEnding, adjustLineEndingOnSave }
|
||||||
|
|
||||||
|
// Create a new document and update few entires later.
|
||||||
const newFileState = getSingleFileState({ markdown, filename, pathname, options })
|
const newFileState = getSingleFileState({ markdown, filename, pathname, options })
|
||||||
if (isMixedLineEndings) {
|
|
||||||
notice.notify({
|
|
||||||
title: 'Line Ending',
|
|
||||||
message: `${filename} has mixed line endings which are automatically normalized to ${lineEnding.toUpperCase()}.`,
|
|
||||||
type: 'primary',
|
|
||||||
time: 20000,
|
|
||||||
showConfirm: false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const tab = tabs.find(t => isSamePathSync(t.pathname, pathname))
|
const tab = tabs.find(t => isSamePathSync(t.pathname, pathname))
|
||||||
if (!tab) {
|
if (!tab) {
|
||||||
// The tab may be closed in the meanwhile.
|
// The tab may be closed in the meanwhile.
|
||||||
console.error('LOAD_CHANGE: Cannot find tab in tab list.')
|
console.error('LOAD_CHANGE: Cannot find tab in tab list.')
|
||||||
|
notice.notify({
|
||||||
|
title: 'Error loading tab',
|
||||||
|
message: 'There was an error while loading the file change because the tab cannot be found.',
|
||||||
|
type: 'error',
|
||||||
|
time: 20000,
|
||||||
|
showConfirm: false
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upate file content but not tab id.
|
// Backup few entries that we need to restore later.
|
||||||
const oldId = tab.id
|
const oldId = tab.id
|
||||||
|
const oldNotifications = tab.notifications
|
||||||
|
let oldHistory = null
|
||||||
|
if (tab.history.index >= 0 && tab.history.stack.length >= 1) {
|
||||||
|
// Allow to restore the old document.
|
||||||
|
oldHistory = {
|
||||||
|
stack: [tab.history.stack[tab.history.index]],
|
||||||
|
index: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Free reference from array
|
||||||
|
tab.history.index--
|
||||||
|
tab.history.stack.pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update file content and restore some entries.
|
||||||
Object.assign(tab, newFileState)
|
Object.assign(tab, newFileState)
|
||||||
tab.id = oldId
|
tab.id = oldId
|
||||||
|
tab.notifications = oldNotifications
|
||||||
|
if (oldHistory) {
|
||||||
|
tab.history = oldHistory
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMixedLineEndings) {
|
||||||
|
tab.notifications.push({
|
||||||
|
msg: `"${filename}" has mixed line endings which are automatically normalized to ${lineEnding.toUpperCase()}.`,
|
||||||
|
showConfirm: false,
|
||||||
|
style: 'info',
|
||||||
|
exclusiveType: '',
|
||||||
|
action: () => {}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload the editor if the tab is currently opened.
|
||||||
if (pathname === currentFile.pathname) {
|
if (pathname === currentFile.pathname) {
|
||||||
state.currentFile = tab
|
state.currentFile = tab
|
||||||
const { id, cursor, history } = tab
|
const { id, cursor, history } = tab
|
||||||
@ -236,6 +266,44 @@ const mutations = {
|
|||||||
// TODO: Remove "SET_GLOBAL_LINE_ENDING" because nowhere used.
|
// TODO: Remove "SET_GLOBAL_LINE_ENDING" because nowhere used.
|
||||||
SET_GLOBAL_LINE_ENDING (state, ending) {
|
SET_GLOBAL_LINE_ENDING (state, ending) {
|
||||||
state.lineEnding = ending
|
state.lineEnding = ending
|
||||||
|
},
|
||||||
|
|
||||||
|
// Push a tab specific notification on stack that never disappears.
|
||||||
|
PUSH_TAB_NOTIFICATION (state, data) {
|
||||||
|
const defaultAction = () => {}
|
||||||
|
const { tabId, msg } = data
|
||||||
|
const action = data.action || defaultAction
|
||||||
|
const showConfirm = data.showConfirm || false
|
||||||
|
const style = data.style || 'info'
|
||||||
|
// Whether only one notification should exist.
|
||||||
|
const exclusiveType = data.exclusiveType || ''
|
||||||
|
|
||||||
|
const { tabs } = state
|
||||||
|
const tab = tabs.find(t => t.id === tabId)
|
||||||
|
if (!tab) {
|
||||||
|
console.error('PUSH_TAB_NOTIFICATION: Cannot find tab in tab list.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { notifications } = tab
|
||||||
|
|
||||||
|
// Remove the old notification if only one should exist.
|
||||||
|
if (exclusiveType) {
|
||||||
|
const index = notifications.findIndex(n => n.exclusiveType === exclusiveType)
|
||||||
|
if (index >= 0) {
|
||||||
|
// Reorder current notification
|
||||||
|
notifications.splice(index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push new notification on stack.
|
||||||
|
notifications.push({
|
||||||
|
msg,
|
||||||
|
showConfirm,
|
||||||
|
style,
|
||||||
|
exclusiveType,
|
||||||
|
action: action
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -388,12 +456,24 @@ const actions = {
|
|||||||
})
|
})
|
||||||
|
|
||||||
ipcRenderer.on('mt::tab-save-failure', (e, tabId, msg) => {
|
ipcRenderer.on('mt::tab-save-failure', (e, tabId, msg) => {
|
||||||
notice.notify({
|
const { tabs } = state
|
||||||
title: 'Save failure',
|
const tab = tabs.find(t => t.id === tabId)
|
||||||
message: msg,
|
if (!tab) {
|
||||||
type: 'error',
|
notice.notify({
|
||||||
time: 20000,
|
title: 'Save failure',
|
||||||
showConfirm: false
|
message: msg,
|
||||||
|
type: 'error',
|
||||||
|
time: 20000,
|
||||||
|
showConfirm: false
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
commit('SET_SAVE_STATUS_BY_TAB', { tab, status: false })
|
||||||
|
commit('PUSH_TAB_NOTIFICATION', {
|
||||||
|
tabId,
|
||||||
|
msg: `There was an error while saving: ${msg}`,
|
||||||
|
style: 'crit'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@ -703,14 +783,10 @@ const actions = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isMixedLineEndings) {
|
if (isMixedLineEndings) {
|
||||||
// TODO(watcher): Show (this) notification(s) per tab.
|
|
||||||
const { filename, lineEnding } = markdownDocument
|
const { filename, lineEnding } = markdownDocument
|
||||||
notice.notify({
|
commit('PUSH_TAB_NOTIFICATION', {
|
||||||
title: 'Line Ending',
|
tabId: id,
|
||||||
message: `${filename} has mixed line endings which are automatically normalized to ${lineEnding.toUpperCase()}.`,
|
msg: `${filename}" has mixed line endings which are automatically normalized to ${lineEnding.toUpperCase()}.`
|
||||||
type: 'primary',
|
|
||||||
time: 20000,
|
|
||||||
showConfirm: false
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -858,8 +934,8 @@ const actions = {
|
|||||||
LINTEN_FOR_EXPORT_SUCCESS ({ commit }) {
|
LINTEN_FOR_EXPORT_SUCCESS ({ commit }) {
|
||||||
ipcRenderer.on('AGANI::export-success', (e, { type, filePath }) => {
|
ipcRenderer.on('AGANI::export-success', (e, { type, filePath }) => {
|
||||||
notice.notify({
|
notice.notify({
|
||||||
title: 'Export',
|
title: 'Exported successfully',
|
||||||
message: `Export ${path.basename(filePath)} successfully`,
|
message: `Exported "${path.basename(filePath)}" successfully!`,
|
||||||
showConfirm: true
|
showConfirm: true
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@ -893,31 +969,28 @@ const actions = {
|
|||||||
|
|
||||||
LISTEN_FOR_FILE_CHANGE ({ commit, state, rootState }) {
|
LISTEN_FOR_FILE_CHANGE ({ commit, state, rootState }) {
|
||||||
ipcRenderer.on('AGANI::update-file', (e, { type, change }) => {
|
ipcRenderer.on('AGANI::update-file', (e, { type, change }) => {
|
||||||
// TODO: A new "changed" notification from different files overwrite the old notification
|
// TODO: We should only load the changed content if the user want to reload the document.
|
||||||
// and the old notification disappears. I think we should bind the notification to the tab.
|
|
||||||
|
|
||||||
const { tabs } = state
|
const { tabs } = state
|
||||||
const { pathname } = change
|
const { pathname } = change
|
||||||
const tab = tabs.find(t => isSamePathSync(t.pathname, pathname))
|
const tab = tabs.find(t => isSamePathSync(t.pathname, pathname))
|
||||||
if (tab) {
|
if (tab) {
|
||||||
const { id, isSaved } = tab
|
const { id, isSaved, filename } = tab
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'unlink': {
|
case 'unlink': {
|
||||||
commit('SET_SAVE_STATUS_BY_TAB', { tab, status: false })
|
commit('SET_SAVE_STATUS_BY_TAB', { tab, status: false })
|
||||||
notice.notify({
|
commit('PUSH_TAB_NOTIFICATION', {
|
||||||
title: 'File Removed on Disk',
|
tabId: id,
|
||||||
message: `${pathname} has been removed or moved.`,
|
msg: `"${filename}" has been removed on disk.`,
|
||||||
type: 'warning',
|
style: 'warn',
|
||||||
time: 0,
|
showConfirm: false,
|
||||||
showConfirm: false
|
exclusiveType: 'file_changed'
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'add':
|
case 'add':
|
||||||
case 'change': {
|
case 'change': {
|
||||||
const { autoSave } = rootState.preferences
|
const { autoSave } = rootState.preferences
|
||||||
const { filename } = change.data
|
|
||||||
if (autoSave) {
|
if (autoSave) {
|
||||||
if (autoSaveTimers.has(id)) {
|
if (autoSaveTimers.has(id)) {
|
||||||
const timer = autoSaveTimers.get(id)
|
const timer = autoSaveTimers.get(id)
|
||||||
@ -933,17 +1006,17 @@ const actions = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
commit('SET_SAVE_STATUS_BY_TAB', { tab, status: false })
|
commit('SET_SAVE_STATUS_BY_TAB', { tab, status: false })
|
||||||
|
commit('PUSH_TAB_NOTIFICATION', {
|
||||||
notice.clear()
|
tabId: id,
|
||||||
notice.notify({
|
msg: `"${filename}" has been changed on disk. Do you want to reload it?`,
|
||||||
title: 'File Changed on Disk',
|
|
||||||
message: `${filename} has been changed on disk, do you want to reload it?`,
|
|
||||||
showConfirm: true,
|
showConfirm: true,
|
||||||
time: 0
|
exclusiveType: 'file_changed',
|
||||||
|
action: status => {
|
||||||
|
if (status) {
|
||||||
|
commit('LOAD_CHANGE', change)
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.then(() => {
|
|
||||||
commit('LOAD_CHANGE', change)
|
|
||||||
})
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
@ -30,7 +30,9 @@ export const defaultFileState = {
|
|||||||
index: -1,
|
index: -1,
|
||||||
matches: [],
|
matches: [],
|
||||||
value: ''
|
value: ''
|
||||||
}
|
},
|
||||||
|
// Per tab notifications
|
||||||
|
notifications: []
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getOptionsFromState = file => {
|
export const getOptionsFromState = file => {
|
||||||
|
Loading…
Reference in New Issue
Block a user