feat: rework preview and editor
fix filebrowser/filebrowser#750, fix filebrowser/filebrowser#553 Preview: - Opens and cycle all files types. - Support for text, mardown and html. - Added filename title, esc key shortcut and editor button. Editor: - Shows on full screen overlay. - Added esc key shortcut.
This commit is contained in:
parent
77eef5dc12
commit
b0693906eb
@ -13,6 +13,7 @@
|
||||
"clipboard": "^2.0.4",
|
||||
"js-base64": "^2.5.1",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"marked": "^0.8.0",
|
||||
"material-design-icons": "^3.0.1",
|
||||
"moment": "^2.24.0",
|
||||
"normalize.css": "^8.0.1",
|
||||
|
||||
@ -124,6 +124,12 @@ nav > div {
|
||||
color: var(--textPrimary);
|
||||
}
|
||||
|
||||
#previewer .text,
|
||||
#previewer .markdown,
|
||||
#previewer .html {
|
||||
background: var(--surfacePrimary);
|
||||
}
|
||||
|
||||
@media (max-width: 736px) {
|
||||
#file-selection {
|
||||
background: var(--surfaceSecondary) !important;
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
<search v-if="isLogged"></search>
|
||||
</div>
|
||||
<div>
|
||||
<template v-if="isLogged">
|
||||
<template v-if="isLogged && isListing">
|
||||
<button @click="openSearch" :aria-label="$t('buttons.search')" :title="$t('buttons.search')" class="search-button action">
|
||||
<i class="material-icons">search</i>
|
||||
</button>
|
||||
|
||||
17
frontend/src/components/buttons/Edit.vue
Normal file
17
frontend/src/components/buttons/Edit.vue
Normal file
@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<button @click="show" :aria-label="$t('buttons.edit')" :title="$t('buttons.edit')" class="action" id="edit-button">
|
||||
<i class="material-icons">mode_edit</i>
|
||||
<span>{{ $t('buttons.edit') }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'edit-button',
|
||||
methods: {
|
||||
show: function () {
|
||||
this.$store.commit('toggleEditor')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<button @click="show" :aria-label="$t('buttons.rename')" :title="$t('buttons.rename')" class="action" id="rename-button">
|
||||
<i class="material-icons">mode_edit</i>
|
||||
<i class="material-icons">label</i>
|
||||
<span>{{ $t('buttons.rename') }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
@ -1,5 +1,18 @@
|
||||
<template>
|
||||
<form id="editor"></form>
|
||||
<div id="previewer">
|
||||
<div class="bar">
|
||||
<button @click="back" class="action" :title="$t('files.closePreview')" :aria-label="$t('files.closePreview')" id="close">
|
||||
<i class="material-icons">close</i>
|
||||
</button>
|
||||
<span class="title">{{ req.name + ((contentChanged) ? '*' : '') }}</span>
|
||||
<button @click="save" v-show="user.perm.modify" :aria-label="$t('buttons.save')" :title="$t('buttons.save')" class="action" id="save-button">
|
||||
<i class="material-icons">save</i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="editor">
|
||||
<form id="editor"></form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@ -10,55 +23,50 @@ import buttons from '@/utils/buttons'
|
||||
import ace from 'ace-builds/src-min-noconflict/ace.js'
|
||||
import modelist from 'ace-builds/src-min-noconflict/ext-modelist.js'
|
||||
import 'ace-builds/webpack-resolver'
|
||||
import { theme } from '@/utils/constants'
|
||||
|
||||
export default {
|
||||
name: 'editor',
|
||||
computed: {
|
||||
...mapState(['req'])
|
||||
...mapState(['req', 'user', 'previewContent'])
|
||||
},
|
||||
data: function () {
|
||||
return {}
|
||||
return {
|
||||
editor: null,
|
||||
contentChanged: false
|
||||
}
|
||||
},
|
||||
created () {
|
||||
window.addEventListener('keydown', this.keyEvent)
|
||||
document.getElementById('save-button').addEventListener('click', this.save)
|
||||
},
|
||||
beforeDestroy () {
|
||||
window.removeEventListener('keydown', this.keyEvent)
|
||||
document.getElementById('save-button').removeEventListener('click', this.save)
|
||||
this.editor.destroy();
|
||||
},
|
||||
mounted: function () {
|
||||
const fileContent = this.req.content || '';
|
||||
|
||||
this.editor = ace.edit('editor', {
|
||||
maxLines: Infinity,
|
||||
minLines: 20,
|
||||
value: fileContent,
|
||||
value: this.previewContent,
|
||||
showPrintMargin: false,
|
||||
readOnly: this.req.type === 'textImmutable',
|
||||
theme: 'ace/theme/chrome',
|
||||
theme: 'ace/theme/twilight',
|
||||
mode: modelist.getModeForPath(this.req.name).mode,
|
||||
wrap: true
|
||||
})
|
||||
|
||||
if (theme == 'dark') {
|
||||
this.editor.setTheme("ace/theme/twilight");
|
||||
}
|
||||
this.editor.on('change', () => {
|
||||
this.contentChanged = true
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
keyEvent (event) {
|
||||
if (!event.ctrlKey && !event.metaKey) {
|
||||
return
|
||||
}
|
||||
let key = String.fromCharCode(event.which).toLowerCase()
|
||||
|
||||
if (String.fromCharCode(event.which).toLowerCase() !== 's') {
|
||||
return
|
||||
if ((event.ctrlKey || event.metaKey) && key == 's') {
|
||||
event.preventDefault()
|
||||
this.save()
|
||||
} else if (event.which === 27) { // Esc
|
||||
this.$store.commit('toggleEditor')
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
this.save()
|
||||
},
|
||||
async save () {
|
||||
const button = 'save'
|
||||
@ -66,11 +74,18 @@ export default {
|
||||
|
||||
try {
|
||||
await api.put(this.$route.path, this.editor.getValue())
|
||||
|
||||
this.$store.commit('setPreviewContent', this.editor.getValue())
|
||||
this.contentChanged = false
|
||||
|
||||
buttons.success(button)
|
||||
} catch (e) {
|
||||
buttons.done(button)
|
||||
this.$showError(e)
|
||||
}
|
||||
},
|
||||
back () {
|
||||
this.$store.commit('toggleEditor')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,7 +4,8 @@
|
||||
<button @click="back" class="action" :title="$t('files.closePreview')" :aria-label="$t('files.closePreview')" id="close">
|
||||
<i class="material-icons">close</i>
|
||||
</button>
|
||||
|
||||
<span class="title">{{ req.name }}</span>
|
||||
<edit-button v-if="isFileEditable"></edit-button>
|
||||
<rename-button v-if="user.perm.rename"></rename-button>
|
||||
<delete-button v-if="user.perm.delete"></delete-button>
|
||||
<download-button v-if="user.perm.download"></download-button>
|
||||
@ -33,29 +34,37 @@
|
||||
and watch it with your favorite video player!
|
||||
</video>
|
||||
<object v-else-if="req.extension == '.pdf'" class="pdf" :data="raw"></object>
|
||||
<a v-else-if="req.type == 'blob'" :href="download">
|
||||
<h2 class="message">{{ $t('buttons.download') }} <i class="material-icons">file_download</i></h2>
|
||||
</a>
|
||||
<div class="html" v-else-if="req.extension == '.html'"> <iframe :src="getHtmlContent()"></iframe> </div>
|
||||
<div class="markdown" v-else-if="isMarkdown" v-html="getMarkdownContent()"></div>
|
||||
<div class="text" v-else-if="req.type == 'text'">{{ previewContent }}</div>
|
||||
<div v-else>
|
||||
<a :href="download">
|
||||
<h2 class="message">{{ $t('buttons.download') }} <i class="material-icons">file_download</i></h2>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import { mapGetters, mapState } from 'vuex'
|
||||
import url from '@/utils/url'
|
||||
import { baseURL } from '@/utils/constants'
|
||||
import { files as api } from '@/api'
|
||||
import marked from 'marked'
|
||||
import InfoButton from '@/components/buttons/Info'
|
||||
import DeleteButton from '@/components/buttons/Delete'
|
||||
import RenameButton from '@/components/buttons/Rename'
|
||||
import DownloadButton from '@/components/buttons/Download'
|
||||
import EditButton from '@/components/buttons/Edit'
|
||||
import ExtendedImage from './ExtendedImage'
|
||||
|
||||
const mediaTypes = [
|
||||
"image",
|
||||
"video",
|
||||
"audio",
|
||||
"blob"
|
||||
const markdownExtesions = [
|
||||
'.md',
|
||||
'.mkdn',
|
||||
'.mdwn',
|
||||
'.mdown',
|
||||
'.markdown'
|
||||
]
|
||||
|
||||
export default {
|
||||
@ -65,6 +74,7 @@ export default {
|
||||
DeleteButton,
|
||||
RenameButton,
|
||||
DownloadButton,
|
||||
EditButton,
|
||||
ExtendedImage
|
||||
},
|
||||
data: function () {
|
||||
@ -76,7 +86,8 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(['req', 'user', 'oldReq', 'jwt']),
|
||||
...mapGetters(['isFileEditable']),
|
||||
...mapState(['req', 'user', 'oldReq', 'jwt', 'previewContent']),
|
||||
hasPrevious () {
|
||||
return (this.previousLink !== '')
|
||||
},
|
||||
@ -88,10 +99,13 @@ export default {
|
||||
},
|
||||
raw () {
|
||||
return `${this.download}&inline=true`
|
||||
},
|
||||
isMarkdown () {
|
||||
return markdownExtesions.includes(this.req.extension)
|
||||
}
|
||||
},
|
||||
async mounted () {
|
||||
window.addEventListener('keyup', this.key)
|
||||
window.addEventListener('keydown', this.key)
|
||||
|
||||
if (this.req.subtitles) {
|
||||
this.subtitles = this.req.subtitles.map(sub => `${baseURL}/api/raw${sub}?auth=${this.jwt}&inline=true`)
|
||||
@ -110,7 +124,7 @@ export default {
|
||||
}
|
||||
},
|
||||
beforeDestroy () {
|
||||
window.removeEventListener('keyup', this.key)
|
||||
window.removeEventListener('keydown', this.key)
|
||||
},
|
||||
methods: {
|
||||
back () {
|
||||
@ -130,6 +144,8 @@ export default {
|
||||
if (this.hasNext) this.next()
|
||||
} else if (event.which === 37) { // left arrow
|
||||
if (this.hasPrevious) this.prev()
|
||||
} else if (event.which == 27) { // esc
|
||||
this.back()
|
||||
}
|
||||
},
|
||||
updateLinks (items) {
|
||||
@ -139,14 +155,14 @@ export default {
|
||||
}
|
||||
|
||||
for (let j = i - 1; j >= 0; j--) {
|
||||
if (mediaTypes.includes(items[j].type)) {
|
||||
if (!items[j].isDir) {
|
||||
this.previousLink = items[j].url
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for (let j = i + 1; j < items.length; j++) {
|
||||
if (mediaTypes.includes(items[j].type)) {
|
||||
if (!items[j].isDir) {
|
||||
this.nextLink = items[j].url
|
||||
break
|
||||
}
|
||||
@ -154,6 +170,12 @@ export default {
|
||||
|
||||
return
|
||||
}
|
||||
},
|
||||
getHtmlContent () {
|
||||
return 'data:text/html;base64,' + btoa(unescape(encodeURIComponent(this.previewContent)))
|
||||
},
|
||||
getMarkdownContent () {
|
||||
return marked(this.previewContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -125,8 +125,13 @@
|
||||
height: 3.7em;
|
||||
}
|
||||
|
||||
#previewer .action:first-of-type {
|
||||
#previewer .title {
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
line-height: 2.4;
|
||||
padding: 0 8px;
|
||||
margin-right: auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#previewer .action i {
|
||||
@ -148,6 +153,40 @@
|
||||
height: calc(100vh - 9.7em);
|
||||
}
|
||||
|
||||
#previewer .editor {
|
||||
max-height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#previewer .text {
|
||||
height: 100%;
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
background: white;
|
||||
text-align: left;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
#previewer .markdown {
|
||||
height: 100%;
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
background: white;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#previewer .html {
|
||||
height: 100%;
|
||||
background: white;
|
||||
text-align: left;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
#previewer .html iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#previewer .preview pre {
|
||||
text-align: left;
|
||||
overflow: auto;
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
"create": "Create",
|
||||
"delete": "Delete",
|
||||
"download": "Download",
|
||||
"edit": "Edit",
|
||||
"info": "Info",
|
||||
"more": "More",
|
||||
"move": "Move",
|
||||
|
||||
@ -2,7 +2,10 @@ const getters = {
|
||||
isLogged: state => state.user !== null,
|
||||
isFiles: state => !state.loading && state.route.name === 'Files',
|
||||
isListing: (state, getters) => getters.isFiles && state.req.isDir,
|
||||
isEditor: (state, getters) => getters.isFiles && (state.req.type === 'text' || state.req.type === 'textImmutable'),
|
||||
isPreview: (state, getters) => !state.loading && !getters.isListing && !getters.isEditor,
|
||||
isEditor: (state, getters) => getters.isFiles && state.showEditor,
|
||||
isFileEditable: (state) => state.req.type === 'text' || state.req.type === 'textImmutable',
|
||||
getPreviewContent: state => state.previewContent,
|
||||
selectedCount: state => state.selected.length
|
||||
}
|
||||
|
||||
|
||||
@ -22,7 +22,9 @@ const state = {
|
||||
show: null,
|
||||
showShell: false,
|
||||
showMessage: null,
|
||||
showConfirm: null
|
||||
showConfirm: null,
|
||||
showEditor: false,
|
||||
previewContent: null
|
||||
}
|
||||
|
||||
export default new Vuex.Store({
|
||||
|
||||
@ -85,6 +85,12 @@ const mutations = {
|
||||
},
|
||||
setProgress: (state, value) => {
|
||||
state.progress = value
|
||||
},
|
||||
toggleEditor: (state) => (
|
||||
state.showEditor = !state.showEditor
|
||||
),
|
||||
setPreviewContent: (state, value) => {
|
||||
state.previewContent = value
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -10,7 +10,6 @@ const logoURL = `/${staticURL}/img/logo.svg`
|
||||
const noAuth = window.FileBrowser.NoAuth
|
||||
const authMethod = window.FileBrowser.AuthMethod
|
||||
const loginPage = window.FileBrowser.LoginPage
|
||||
const theme = window.FileBrowser.Theme
|
||||
|
||||
export {
|
||||
name,
|
||||
@ -23,6 +22,5 @@ export {
|
||||
version,
|
||||
noAuth,
|
||||
authMethod,
|
||||
loginPage,
|
||||
theme
|
||||
loginPage
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<div id="breadcrumbs">
|
||||
<div v-if="isListing" id="breadcrumbs">
|
||||
<router-link to="/files/" :aria-label="$t('files.home')" :title="$t('files.home')">
|
||||
<i class="material-icons">home</i>
|
||||
</router-link>
|
||||
@ -53,6 +53,7 @@ export default {
|
||||
...mapGetters([
|
||||
'selectedCount',
|
||||
'isListing',
|
||||
'isPreview',
|
||||
'isEditor',
|
||||
'isFiles'
|
||||
]),
|
||||
@ -63,9 +64,6 @@ export default {
|
||||
'multiple',
|
||||
'loading'
|
||||
]),
|
||||
isPreview () {
|
||||
return !this.loading && !this.isListing && !this.isEditor
|
||||
},
|
||||
breadcrumbs () {
|
||||
let parts = this.$route.path.split('/')
|
||||
|
||||
@ -151,6 +149,9 @@ export default {
|
||||
|
||||
this.$store.commit('updateRequest', res)
|
||||
document.title = res.name
|
||||
|
||||
if (res.content)
|
||||
this.$store.commit('setPreviewContent', res.content)
|
||||
} catch (e) {
|
||||
this.error = e
|
||||
} finally {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user