nextcloud-notes/src/App.vue

343 lines
8.4 KiB
Vue
Raw Normal View History

2019-05-22 21:40:03 +02:00
<template>
2022-10-03 10:43:47 +02:00
<NcContent app-name="notes" :content-class="{loading: loading.notes}">
<NcAppNavigation :class="{loading: loading.notes, 'icon-error': error}">
<NcAppNavigationNew
2019-05-22 21:40:03 +02:00
v-show="!loading.notes && !error"
:text="t('notes', 'New note')"
@click="onNewNote"
2022-10-03 10:43:47 +02:00
>
<PlusIcon slot="icon" :size="20" />
</NcAppNavigationNew>
2019-05-22 21:40:03 +02:00
2020-06-19 20:11:35 +02:00
<template #list>
<NavigationList v-show="!loading.notes"
:filtered-notes="filteredNotes"
:category="filter.category"
@category-selected="onSelectCategory"
@note-deleted="onNoteDeleted"
/>
2022-10-18 08:53:18 +02:00
<NcAppNavigationItem
:title="t('notes', 'Help')"
:pinned="true"
@click.prevent="openHelp"
>
<InfoIcon slot="icon" :size="20" />
</NcAppNavigationItem>
<AppHelp :open.sync="helpVisible" />
2020-06-19 20:11:35 +02:00
</template>
2019-05-22 21:40:03 +02:00
2020-06-28 21:27:22 +02:00
<template #footer>
<AppSettings v-if="!loading.notes && error !== true" @reload="reloadNotes" />
</template>
2022-10-03 10:43:47 +02:00
</NcAppNavigation>
2019-05-22 21:40:03 +02:00
2022-10-03 10:43:47 +02:00
<NcAppContent v-if="error">
<div style="margin: 2em;">
<h2>{{ t('notes', 'Error') }}</h2>
<p>{{ error }}</p>
2020-07-14 20:08:00 +02:00
<p>{{ t('notes', 'Please see Nextcloud server log for details.') }}</p>
</div>
2022-10-03 10:43:47 +02:00
</NcAppContent>
<router-view v-else />
2019-05-22 21:40:03 +02:00
<router-view name="sidebar" />
2022-10-03 10:43:47 +02:00
</NcContent>
2019-05-22 21:40:03 +02:00
</template>
<script>
import {
2022-10-03 10:43:47 +02:00
NcAppContent,
NcAppNavigation,
NcAppNavigationNew,
2022-10-18 08:53:18 +02:00
NcAppNavigationItem,
2022-10-03 10:43:47 +02:00
NcContent,
2019-10-25 14:00:07 +02:00
} from '@nextcloud/vue'
import { showSuccess, TOAST_UNDO_TIMEOUT, TOAST_PERMANENT_TIMEOUT } from '@nextcloud/dialogs'
2020-08-23 22:04:05 +02:00
import '@nextcloud/dialogs/styles/toast.scss'
2020-02-19 21:35:19 +01:00
2022-10-18 08:53:18 +02:00
import InfoIcon from 'vue-material-design-icons/Information.vue'
2022-10-03 10:43:47 +02:00
import PlusIcon from 'vue-material-design-icons/Plus.vue'
import AppSettings from './components/AppSettings.vue'
import NavigationList from './components/NavigationList.vue'
2022-10-18 08:53:18 +02:00
import AppHelp from './components/AppHelp.vue'
import { config } from './config.js'
import { fetchNotes, noteExists, createNote, undoDeleteNote } from './NotesService.js'
import store from './store.js'
2019-05-22 21:40:03 +02:00
export default {
name: 'App',
components: {
2022-10-18 08:53:18 +02:00
AppHelp,
2019-05-22 21:40:03 +02:00
AppSettings,
2022-10-18 08:53:18 +02:00
InfoIcon,
2019-05-22 21:40:03 +02:00
NavigationList,
2022-10-03 10:43:47 +02:00
NcAppContent,
NcAppNavigation,
NcAppNavigationNew,
2022-10-18 08:53:18 +02:00
NcAppNavigationItem,
2022-10-03 10:43:47 +02:00
NcContent,
PlusIcon,
2019-05-22 21:40:03 +02:00
},
2020-07-12 20:25:35 +02:00
data() {
2019-05-22 21:40:03 +02:00
return {
filter: {
category: null,
},
loading: {
notes: true,
2019-05-22 21:40:03 +02:00
create: false,
},
error: false,
2019-12-23 17:40:19 +01:00
undoNotification: null,
undoTimer: null,
deletedNotes: [],
refreshTimer: null,
2022-10-18 08:53:18 +02:00
helpVisible: false,
2019-05-22 21:40:03 +02:00
}
},
computed: {
notes() {
return store.state.notes.notes
2019-05-22 21:40:03 +02:00
},
filteredNotes() {
const notes = this.notes.filter(note => {
if (this.filter.category !== null
&& this.filter.category !== note.category
&& !note.category.startsWith(this.filter.category + '/')) {
return false
}
return true
})
function cmpRecent(a, b) {
if (a.favorite && !b.favorite) return -1
if (!a.favorite && b.favorite) return 1
return b.modified - a.modified
}
function cmpCategory(a, b) {
const cmpCat = a.category.localeCompare(b.category)
if (cmpCat !== 0) return cmpCat
if (a.favorite && !b.favorite) return -1
if (!a.favorite && b.favorite) return 1
return a.title.localeCompare(b.title)
}
notes.sort(this.filter.category === null ? cmpRecent : cmpCategory)
return notes
},
},
created() {
store.commit('setDocumentTitle', document.title)
window.addEventListener('beforeunload', this.onClose)
document.addEventListener('visibilitychange', this.onVisibilityChange)
2019-05-22 21:40:03 +02:00
this.loadNotes()
},
destroyed() {
document.removeEventListener('visibilitychange', this.onVisibilityChange)
this.stopRefreshTimer()
},
2019-05-22 21:40:03 +02:00
methods: {
loadNotes() {
2020-02-19 21:35:19 +01:00
fetchNotes()
2019-05-22 21:40:03 +02:00
.then(data => {
if (data === null) {
// nothing changed
return
}
if (data.notes !== null) {
this.error = false
this.routeDefault(data.lastViewedNote)
2020-07-14 20:08:00 +02:00
} else if (this.loading.notes) {
// only show error state if not loading in background
this.error = data.errorMessage
2020-07-14 20:08:00 +02:00
} else {
console.error('Server error while updating list of notes: ' + data.errorMessage)
}
2019-05-22 21:40:03 +02:00
})
.catch(() => {
// only show error state if not loading in background
if (this.loading.notes) {
this.error = true
}
2019-05-22 21:40:03 +02:00
})
2020-02-19 21:35:19 +01:00
.then(() => {
2019-05-22 21:40:03 +02:00
this.loading.notes = false
this.startRefreshTimer(config.interval.notes.refresh)
2019-05-22 21:40:03 +02:00
})
},
startRefreshTimer(seconds) {
if (this.refreshTimer === null && document.visibilityState === 'visible') {
this.refreshTimer = setTimeout(() => {
this.refreshTimer = null
this.loadNotes()
}, seconds * 1000)
}
},
stopRefreshTimer() {
if (this.refreshTimer !== null) {
clearTimeout(this.refreshTimer)
this.refreshTimer = null
}
},
onVisibilityChange() {
if (document.visibilityState === 'visible') {
this.startRefreshTimer(config.interval.notes.refreshAfterHidden)
} else {
this.stopRefreshTimer()
}
},
2019-05-22 21:40:03 +02:00
reloadNotes() {
if (this.$route.path !== '/') {
this.$router.push('/')
}
store.commit('removeAllNotes')
2021-03-12 21:59:59 +01:00
store.commit('clearSyncCache')
this.loading.notes = true
2019-05-22 21:40:03 +02:00
this.loadNotes()
},
routeDefault(defaultNoteId) {
2020-02-19 21:35:19 +01:00
if (this.$route.name !== 'note' || !noteExists(this.$route.params.noteId)) {
if (noteExists(defaultNoteId)) {
2019-05-22 21:40:03 +02:00
this.routeToNote(defaultNoteId)
} else {
this.routeFirst()
}
}
},
routeFirst() {
2019-10-26 22:11:28 +02:00
const availableNotes = this.filteredNotes.filter(note => !note.error && !note.deleting)
2019-05-22 21:40:03 +02:00
if (availableNotes.length > 0) {
this.routeToNote(availableNotes[0].id)
} else {
if (this.$route.name !== 'welcome') {
this.$router.push({ name: 'welcome' })
}
2019-05-22 21:40:03 +02:00
}
},
routeToNote(id, query) {
const noteId = id.toString()
if (this.$route.name !== 'note' || this.$route.params.noteId !== noteId) {
2019-12-23 17:40:19 +01:00
this.$router.push({
name: 'note',
params: { noteId },
query,
2019-12-23 17:40:19 +01:00
})
}
2019-05-22 21:40:03 +02:00
},
2022-10-18 08:53:18 +02:00
openHelp() {
this.helpVisible = true
},
2019-05-22 21:40:03 +02:00
onNewNote() {
if (this.loading.create) {
return
}
this.loading.create = true
createNote(this.filter.category)
2019-05-22 21:40:03 +02:00
.then(note => {
this.routeToNote(note.id, { new: null })
2019-05-22 21:40:03 +02:00
})
.catch(() => {
})
.finally(() => {
2019-05-22 21:40:03 +02:00
this.loading.create = false
})
},
onSelectCategory(category) {
this.filter.category = category
2020-01-03 12:23:56 +01:00
const appNavigation = document.querySelector('#app-navigation > ul')
if (appNavigation) {
appNavigation.scrollTop = 0
}
2019-05-22 21:40:03 +02:00
},
2019-12-23 17:40:19 +01:00
onNoteDeleted(note) {
this.deletedNotes.push(note)
this.clearUndoTimer()
let label
if (this.deletedNotes.length === 1) {
label = this.t('notes', 'Deleted {title}', { title: note.title })
} else {
2020-12-28 17:23:11 +01:00
label = this.n('notes', 'Deleted {number} note', 'Deleted {number} notes', this.deletedNotes.length, { number: this.deletedNotes.length })
2019-12-23 17:40:19 +01:00
}
if (this.undoNotification === null) {
const action = '<button class="undo">' + this.t('notes', 'Undo Delete') + '</button>'
this.undoNotification = showSuccess(
'<span class="deletedLabel">' + label + '</span> ' + action,
{ isHTML: true, timeout: TOAST_PERMANENT_TIMEOUT, onRemove: this.onUndoNotificationClosed }
2019-12-23 17:40:19 +01:00
)
this.undoNotification.toastElement.getElementsByClassName('undo')
.forEach(element => { element.onclick = this.onUndoDelete })
} else {
this.undoNotification.toastElement.getElementsByClassName('deletedLabel')
.forEach(element => { element.textContent = label })
}
this.undoTimer = setTimeout(this.onRemoveUndoNotification, TOAST_UNDO_TIMEOUT)
2019-12-23 17:40:19 +01:00
this.routeFirst()
},
clearUndoTimer() {
if (this.undoTimer) {
clearTimeout(this.undoTimer)
this.undoTimer = null
}
},
onUndoDelete() {
2020-09-27 17:42:53 +02:00
const number = this.deletedNotes.length
2019-12-23 17:40:19 +01:00
this.deletedNotes.forEach(note => undoDeleteNote(note))
this.onRemoveUndoNotification()
2020-09-27 17:42:53 +02:00
if (number === 1) {
showSuccess(this.t('notes', 'Note recovered'))
} else {
2020-12-28 17:23:11 +01:00
showSuccess(this.n('notes', 'Recovered {number} note', 'Recovered {number} notes', number, { number }))
2020-09-27 17:42:53 +02:00
}
2019-12-23 17:40:19 +01:00
},
onUndoNotificationClosed() {
if (this.undoNotification) {
this.undoNotification = null
this.onRemoveUndoNotification()
}
},
onRemoveUndoNotification() {
this.deletedNotes = []
if (this.undoNotification) {
this.undoNotification.hideToast()
this.undoNotification = null
}
this.clearUndoTimer()
},
2019-05-22 21:40:03 +02:00
onClose(event) {
if (!this.notes.every(note => !note.unsaved)) {
event.preventDefault()
return this.t('notes', 'There are unsaved notes. Leaving the page will discard all changes!')
}
},
},
}
</script>