mirror of
https://github.com/marktext/marktext.git
synced 2025-05-03 00:01:19 +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.
|
||||
app.on('web-contents-created', (event, contents) => {
|
||||
contents.on('will-attach-webview', (event, webPreferences, params) => {
|
||||
console.warn('Prevented webview creation.')
|
||||
contents.on('will-attach-webview', event => {
|
||||
event.preventDefault()
|
||||
})
|
||||
contents.on('will-navigate', event => {
|
||||
console.warn('Prevented opening a link.')
|
||||
event.preventDefault()
|
||||
})
|
||||
contents.on('new-window', (event, url) => {
|
||||
console.warn('Prevented opening a new window.')
|
||||
contents.on('new-window', event => {
|
||||
event.preventDefault()
|
||||
})
|
||||
})
|
||||
|
@ -6,7 +6,7 @@ import { exists } from 'common/filesystem'
|
||||
import { hasMarkdownExtension } from 'common/filesystem/paths'
|
||||
import { getUniqueId } from '../utils'
|
||||
import { loadMarkdownFile } from '../filesystem/markdown'
|
||||
import { isLinux } from '../config'
|
||||
import { isLinux, isOsx } from '../config'
|
||||
|
||||
// TODO(refactor): Please see GH#1035.
|
||||
|
||||
@ -159,7 +159,8 @@ class Watcher {
|
||||
ignorePermissionErrors: true,
|
||||
|
||||
// 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
|
||||
awaitWriteFinish: {
|
||||
|
@ -18,6 +18,7 @@
|
||||
:text-direction="textDirection"
|
||||
></source-code>
|
||||
</div>
|
||||
<tab-notifications></tab-notifications>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -25,6 +26,7 @@
|
||||
import Tabs from './tabs.vue'
|
||||
import Editor from './editor.vue'
|
||||
import SourceCode from './sourceCode.vue'
|
||||
import TabNotifications from './notifications.vue'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
@ -61,7 +63,8 @@ export default {
|
||||
components: {
|
||||
Tabs,
|
||||
Editor,
|
||||
SourceCode
|
||||
SourceCode,
|
||||
TabNotifications
|
||||
}
|
||||
}
|
||||
</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 { isMixedLineEndings, lineEnding, adjustLineEndingOnSave, encoding, markdown, filename } = data
|
||||
const options = { encoding, lineEnding, adjustLineEndingOnSave }
|
||||
|
||||
// Create a new document and update few entires later.
|
||||
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))
|
||||
if (!tab) {
|
||||
// The tab may be closed in the meanwhile.
|
||||
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
|
||||
}
|
||||
|
||||
// Upate file content but not tab id.
|
||||
// Backup few entries that we need to restore later.
|
||||
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)
|
||||
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) {
|
||||
state.currentFile = tab
|
||||
const { id, cursor, history } = tab
|
||||
@ -236,6 +266,44 @@ const mutations = {
|
||||
// TODO: Remove "SET_GLOBAL_LINE_ENDING" because nowhere used.
|
||||
SET_GLOBAL_LINE_ENDING (state, 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,6 +456,9 @@ const actions = {
|
||||
})
|
||||
|
||||
ipcRenderer.on('mt::tab-save-failure', (e, tabId, msg) => {
|
||||
const { tabs } = state
|
||||
const tab = tabs.find(t => t.id === tabId)
|
||||
if (!tab) {
|
||||
notice.notify({
|
||||
title: 'Save failure',
|
||||
message: msg,
|
||||
@ -395,6 +466,15 @@ const actions = {
|
||||
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) {
|
||||
// TODO(watcher): Show (this) notification(s) per tab.
|
||||
const { filename, lineEnding } = markdownDocument
|
||||
notice.notify({
|
||||
title: 'Line Ending',
|
||||
message: `${filename} has mixed line endings which are automatically normalized to ${lineEnding.toUpperCase()}.`,
|
||||
type: 'primary',
|
||||
time: 20000,
|
||||
showConfirm: false
|
||||
commit('PUSH_TAB_NOTIFICATION', {
|
||||
tabId: id,
|
||||
msg: `${filename}" has mixed line endings which are automatically normalized to ${lineEnding.toUpperCase()}.`
|
||||
})
|
||||
}
|
||||
},
|
||||
@ -858,8 +934,8 @@ const actions = {
|
||||
LINTEN_FOR_EXPORT_SUCCESS ({ commit }) {
|
||||
ipcRenderer.on('AGANI::export-success', (e, { type, filePath }) => {
|
||||
notice.notify({
|
||||
title: 'Export',
|
||||
message: `Export ${path.basename(filePath)} successfully`,
|
||||
title: 'Exported successfully',
|
||||
message: `Exported "${path.basename(filePath)}" successfully!`,
|
||||
showConfirm: true
|
||||
})
|
||||
.then(() => {
|
||||
@ -893,31 +969,28 @@ const actions = {
|
||||
|
||||
LISTEN_FOR_FILE_CHANGE ({ commit, state, rootState }) {
|
||||
ipcRenderer.on('AGANI::update-file', (e, { type, change }) => {
|
||||
// TODO: A new "changed" notification from different files overwrite the old notification
|
||||
// and the old notification disappears. I think we should bind the notification to the tab.
|
||||
// TODO: We should only load the changed content if the user want to reload the document.
|
||||
|
||||
const { tabs } = state
|
||||
const { pathname } = change
|
||||
const tab = tabs.find(t => isSamePathSync(t.pathname, pathname))
|
||||
if (tab) {
|
||||
const { id, isSaved } = tab
|
||||
|
||||
const { id, isSaved, filename } = tab
|
||||
switch (type) {
|
||||
case 'unlink': {
|
||||
commit('SET_SAVE_STATUS_BY_TAB', { tab, status: false })
|
||||
notice.notify({
|
||||
title: 'File Removed on Disk',
|
||||
message: `${pathname} has been removed or moved.`,
|
||||
type: 'warning',
|
||||
time: 0,
|
||||
showConfirm: false
|
||||
commit('PUSH_TAB_NOTIFICATION', {
|
||||
tabId: id,
|
||||
msg: `"${filename}" has been removed on disk.`,
|
||||
style: 'warn',
|
||||
showConfirm: false,
|
||||
exclusiveType: 'file_changed'
|
||||
})
|
||||
break
|
||||
}
|
||||
case 'add':
|
||||
case 'change': {
|
||||
const { autoSave } = rootState.preferences
|
||||
const { filename } = change.data
|
||||
if (autoSave) {
|
||||
if (autoSaveTimers.has(id)) {
|
||||
const timer = autoSaveTimers.get(id)
|
||||
@ -933,16 +1006,16 @@ const actions = {
|
||||
}
|
||||
|
||||
commit('SET_SAVE_STATUS_BY_TAB', { tab, status: false })
|
||||
|
||||
notice.clear()
|
||||
notice.notify({
|
||||
title: 'File Changed on Disk',
|
||||
message: `${filename} has been changed on disk, do you want to reload it?`,
|
||||
commit('PUSH_TAB_NOTIFICATION', {
|
||||
tabId: id,
|
||||
msg: `"${filename}" has been changed on disk. Do you want to reload it?`,
|
||||
showConfirm: true,
|
||||
time: 0
|
||||
})
|
||||
.then(() => {
|
||||
exclusiveType: 'file_changed',
|
||||
action: status => {
|
||||
if (status) {
|
||||
commit('LOAD_CHANGE', change)
|
||||
}
|
||||
}
|
||||
})
|
||||
break
|
||||
}
|
||||
|
@ -30,7 +30,9 @@ export const defaultFileState = {
|
||||
index: -1,
|
||||
matches: [],
|
||||
value: ''
|
||||
}
|
||||
},
|
||||
// Per tab notifications
|
||||
notifications: []
|
||||
}
|
||||
|
||||
export const getOptionsFromState = file => {
|
||||
|
Loading…
Reference in New Issue
Block a user