rename _assets to assets
Former-commit-id: 3bb6cc662da9e9255bd61fef42430c271002fd49 [formerly eaf1785c4f85522e4eb66d00a6ae9dd9ecc4fcb4] [formerly addd3ffe1396e6df84cdc3e8787d57ffb2be3dc6 [formerly 800693ad49e76c880230eb8cd1bc4a95e8c39fff]] Former-commit-id: 6c24d30f26529457202f470620a0ea1d31772b13 [formerly 384d2af17fe100b9db91462eb41337f9dff855f4] Former-commit-id: 94f4933e12f97ee7468c884f041612498e07ba32
This commit is contained in:
54
assets/src/components/Editor.vue
Normal file
54
assets/src/components/Editor.vue
Normal file
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<form id="editor">
|
||||
<h2 v-if="hasMetadata">Metadata</h2>
|
||||
<textarea v-if="hasMetadata" id="metadata">{{ req.metadata }}</textarea>
|
||||
|
||||
<h2 v-if="hasMetadata">Body</h2>
|
||||
<textarea id="content">{{ req.content }}</textarea>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import CodeMirror from '@/codemirror'
|
||||
|
||||
export default {
|
||||
name: 'editor',
|
||||
computed: {
|
||||
...mapState(['req']),
|
||||
hasMetadata: function () {
|
||||
return (this.req.metadata !== undefined && this.req.metadata !== null)
|
||||
}
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
metadata: null,
|
||||
content: null
|
||||
}
|
||||
},
|
||||
mounted: function () {
|
||||
this.content = CodeMirror.fromTextArea(document.getElementById('content'), {
|
||||
lineNumbers: (this.req.language !== 'markdown'),
|
||||
viewportMargin: Infinity,
|
||||
autofocus: true
|
||||
})
|
||||
|
||||
CodeMirror.autoLoadMode(this.content, this.req.language)
|
||||
|
||||
// Prevent of going on if there is no metadata.
|
||||
if (!this.hasMetadata) {
|
||||
return
|
||||
}
|
||||
|
||||
this.metadata = CodeMirror.fromTextArea(document.getElementById('metadata'), {
|
||||
viewportMargin: Infinity
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
202
assets/src/components/Listing.vue
Normal file
202
assets/src/components/Listing.vue
Normal file
@@ -0,0 +1,202 @@
|
||||
<template>
|
||||
<div v-if="(req.numDirs + req.numFiles) == 0">
|
||||
<h2 class="message">
|
||||
<i class="material-icons">sentiment_dissatisfied</i>
|
||||
<span>It feels lonely here...</span>
|
||||
</h2>
|
||||
</div>
|
||||
<div v-else id="listing"
|
||||
:class="req.display"
|
||||
@drop="drop"
|
||||
@dragenter="dragEnter"
|
||||
@dragend="dragEnd">
|
||||
<div>
|
||||
<div class="item header">
|
||||
<div></div>
|
||||
<div>
|
||||
<p :class="{ active: nameSorted }" class="name" @click="sort('name')">
|
||||
<span>Name</span>
|
||||
<i class="material-icons">{{ nameIcon }}</i>
|
||||
</p>
|
||||
|
||||
<p :class="{ active: !nameSorted }" class="size" @click="sort('size')">
|
||||
<span>Size</span>
|
||||
<i class="material-icons">{{ sizeIcon }}</i>
|
||||
</p>
|
||||
|
||||
<p class="modified">Last modified</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 v-if="req.numDirs > 0">Folders</h2>
|
||||
<div v-if="req.numDirs > 0">
|
||||
<item v-for="(item, index) in req.items"
|
||||
v-if="item.isDir"
|
||||
:key="base64(item.name)"
|
||||
v-bind:index="index"
|
||||
v-bind:name="item.name"
|
||||
v-bind:isDir="item.isDir"
|
||||
v-bind:url="item.url"
|
||||
v-bind:modified="item.modified"
|
||||
v-bind:type="item.type"
|
||||
v-bind:size="item.size">
|
||||
</item>
|
||||
</div>
|
||||
|
||||
<h2 v-if="req.numFiles > 0">Files</h2>
|
||||
<div v-if="req.numFiles > 0">
|
||||
<item v-for="(item, index) in req.items"
|
||||
v-if="!item.isDir"
|
||||
:key="base64(item.name)"
|
||||
v-bind:index="index"
|
||||
v-bind:name="item.name"
|
||||
v-bind:isDir="item.isDir"
|
||||
v-bind:url="item.url"
|
||||
v-bind:modified="item.modified"
|
||||
v-bind:type="item.type"
|
||||
v-bind:size="item.size">
|
||||
</item>
|
||||
</div>
|
||||
|
||||
<input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" value="Upload" multiple>
|
||||
|
||||
<div v-show="$store.state.multiple" :class="{ active: $store.state.multiple }" id="multiple-selection">
|
||||
<p>Multiple selection enabled</p>
|
||||
<div @click="$store.commit('multiple', false)" tabindex="0" role="button" title="Clear" aria-label="Clear" class="action">
|
||||
<i class="material-icons" title="Clear">clear</i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapState} from 'vuex'
|
||||
import Item from './ListingItem'
|
||||
import api from '@/utils/api'
|
||||
import buttons from '@/utils/buttons'
|
||||
|
||||
export default {
|
||||
name: 'listing',
|
||||
components: { Item },
|
||||
computed: {
|
||||
...mapState(['req']),
|
||||
nameSorted () {
|
||||
return (this.req.sort === 'name')
|
||||
},
|
||||
ascOrdered () {
|
||||
return (this.req.order === 'asc')
|
||||
},
|
||||
nameIcon () {
|
||||
if (this.nameSorted && !this.ascOrdered) {
|
||||
return 'arrow_upward'
|
||||
}
|
||||
|
||||
return 'arrow_downward'
|
||||
},
|
||||
sizeIcon () {
|
||||
if (!this.nameSorted && this.ascOrdered) {
|
||||
return 'arrow_downward'
|
||||
}
|
||||
|
||||
return 'arrow_upward'
|
||||
}
|
||||
},
|
||||
mounted: function () {
|
||||
document.addEventListener('dragover', function (event) {
|
||||
event.preventDefault()
|
||||
}, false)
|
||||
|
||||
document.addEventListener('drop', this.drop, false)
|
||||
},
|
||||
methods: {
|
||||
base64: function (name) {
|
||||
return window.btoa(unescape(encodeURIComponent(name)))
|
||||
},
|
||||
dragEnter: function (event) {
|
||||
let items = document.getElementsByClassName('item')
|
||||
|
||||
Array.from(items).forEach(file => {
|
||||
file.style.opacity = 0.5
|
||||
})
|
||||
},
|
||||
dragEnd: function (event) {
|
||||
this.resetOpacity()
|
||||
},
|
||||
drop: function (event) {
|
||||
event.preventDefault()
|
||||
|
||||
let dt = event.dataTransfer
|
||||
let files = dt.files
|
||||
let el = event.target
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
if (el !== null && !el.classList.contains('item')) {
|
||||
el = el.parentElement
|
||||
}
|
||||
}
|
||||
|
||||
if (files.length > 0) {
|
||||
if (el !== null && el.classList.contains('item') && el.dataset.dir === 'true') {
|
||||
this.handleFiles(files, el.querySelector('.name').innerHTML + '/')
|
||||
return
|
||||
}
|
||||
|
||||
this.handleFiles(files, '')
|
||||
} else {
|
||||
this.resetOpacity()
|
||||
}
|
||||
},
|
||||
uploadInput: function (event) {
|
||||
this.handleFiles(event.currentTarget.files, '')
|
||||
},
|
||||
resetOpacity: function () {
|
||||
let items = document.getElementsByClassName('item')
|
||||
|
||||
Array.from(items).forEach(file => {
|
||||
file.style.opacity = 1
|
||||
})
|
||||
},
|
||||
handleFiles: function (files, base) {
|
||||
this.resetOpacity()
|
||||
|
||||
buttons.loading('upload')
|
||||
let promises = []
|
||||
|
||||
for (let file of files) {
|
||||
promises.push(api.post(this.$route.path + base + file.name, file))
|
||||
}
|
||||
|
||||
Promise.all(promises)
|
||||
.then(() => {
|
||||
buttons.done('upload')
|
||||
this.$store.commit('setReload', true)
|
||||
})
|
||||
.catch(e => {
|
||||
buttons.done('upload')
|
||||
// TODO: show error in box
|
||||
console.log(e)
|
||||
})
|
||||
|
||||
return false
|
||||
},
|
||||
sort (sort) {
|
||||
let order = 'desc'
|
||||
|
||||
if (sort === 'name') {
|
||||
if (this.nameIcon === 'arrow_upward') {
|
||||
order = 'asc'
|
||||
}
|
||||
} else {
|
||||
if (this.sizeIcon === 'arrow_upward') {
|
||||
order = 'asc'
|
||||
}
|
||||
}
|
||||
|
||||
document.cookie = `sort=${sort}; max-age=31536000; path=${this.$store.state.baseURL}`
|
||||
document.cookie = `order=${order}; max-age=31536000; path=${this.$store.state.baseURL}`
|
||||
this.$store.commit('setReload', true)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
115
assets/src/components/ListingItem.vue
Normal file
115
assets/src/components/ListingItem.vue
Normal file
@@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<div class="item"
|
||||
draggable="true"
|
||||
@dragstart="dragStart"
|
||||
@dragover="dragOver"
|
||||
@drop="drop"
|
||||
@click="click"
|
||||
@dblclick="open"
|
||||
:aria-selected="isSelected()">
|
||||
<div>
|
||||
<i class="material-icons">{{ icon() }}</i>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="name">{{ name }}</p>
|
||||
|
||||
<p v-if="isDir" class="size" data-order="-1">—</p>
|
||||
<p v-else class="size" :data-order="humanSize()">{{ humanSize() }}</p>
|
||||
|
||||
<p class="modified">
|
||||
<time :datetime="modified">{{ humanTime() }}</time>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapMutations, mapGetters, mapState } from 'vuex'
|
||||
import filesize from 'filesize'
|
||||
import moment from 'moment'
|
||||
import api from '@/utils/api'
|
||||
|
||||
export default {
|
||||
name: 'item',
|
||||
props: ['name', 'isDir', 'url', 'type', 'size', 'modified', 'index'],
|
||||
computed: {
|
||||
...mapState(['selected', 'req']),
|
||||
...mapGetters(['selectedCount'])
|
||||
},
|
||||
methods: {
|
||||
...mapMutations(['addSelected', 'removeSelected', 'resetSelected']),
|
||||
isSelected: function () {
|
||||
return (this.selected.indexOf(this.index) !== -1)
|
||||
},
|
||||
icon: function () {
|
||||
if (this.isDir) return 'folder'
|
||||
if (this.type === 'image') return 'insert_photo'
|
||||
if (this.type === 'audio') return 'volume_up'
|
||||
if (this.type === 'video') return 'movie'
|
||||
return 'insert_drive_file'
|
||||
},
|
||||
humanSize: function () {
|
||||
return filesize(this.size)
|
||||
},
|
||||
humanTime: function () {
|
||||
return moment(this.modified).fromNow()
|
||||
},
|
||||
dragStart: function (event) {
|
||||
if (this.selectedCount === 0) {
|
||||
this.addSelected(this.index)
|
||||
}
|
||||
},
|
||||
dragOver: function (event) {
|
||||
if (!this.isDir) return
|
||||
|
||||
event.preventDefault()
|
||||
let el = event.target
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
if (!el.classList.contains('item')) {
|
||||
el = el.parentElement
|
||||
}
|
||||
}
|
||||
|
||||
el.style.opacity = 1
|
||||
},
|
||||
drop: function (event) {
|
||||
if (!this.isDir) return
|
||||
event.preventDefault()
|
||||
|
||||
if (this.selectedCount === 0) return
|
||||
|
||||
let promises = []
|
||||
|
||||
for (let i of this.selected) {
|
||||
let url = this.req.items[i].url
|
||||
let name = this.req.items[i].name
|
||||
|
||||
promises.push(api.move(url, this.url + encodeURIComponent(name)))
|
||||
}
|
||||
|
||||
Promise.all(promises)
|
||||
.then(() => {
|
||||
this.$store.commit('setReload', true)
|
||||
})
|
||||
.catch(error => console.log(error))
|
||||
},
|
||||
click: function (event) {
|
||||
if (this.selectedCount !== 0) event.preventDefault()
|
||||
if (this.$store.state.selected.indexOf(this.index) === -1) {
|
||||
if (!event.ctrlKey && !this.$store.state.multiple) this.resetSelected()
|
||||
|
||||
this.addSelected(this.index)
|
||||
} else {
|
||||
this.removeSelected(this.index)
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
open: function (event) {
|
||||
this.$router.push({path: this.url})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
119
assets/src/components/Login.vue
Normal file
119
assets/src/components/Login.vue
Normal file
@@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<div id="login">
|
||||
<form @submit="submit">
|
||||
<img src="../assets/logo.svg" alt="File Manager">
|
||||
<h1>File Manager</h1>
|
||||
<div v-if="wrong" class="wrong">Wrong credentials</div>
|
||||
<input type="text" v-model="username" placeholder="Username">
|
||||
<input type="password" v-model="password" placeholder="Password">
|
||||
<input type="submit" value="Login">
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import auth from '@/utils/auth'
|
||||
|
||||
export default {
|
||||
name: 'login',
|
||||
data: function () {
|
||||
return {
|
||||
wrong: false,
|
||||
username: '',
|
||||
password: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
submit: function (event) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
let redirect = this.$route.query.redirect
|
||||
if (redirect === '' || redirect === undefined || redirect === null) {
|
||||
redirect = '/files/'
|
||||
}
|
||||
|
||||
auth.login(this.username, this.password)
|
||||
.then(() => {
|
||||
this.$router.push({ path: redirect })
|
||||
})
|
||||
.catch(e => {
|
||||
console.log(e)
|
||||
this.wrong = true
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#login {
|
||||
background: #fff;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#login img {
|
||||
width: 4em;
|
||||
height: 4em;
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
#login h1 {
|
||||
text-align: center;
|
||||
font-size: 2.5em;
|
||||
margin: .4em 0 .67em;
|
||||
}
|
||||
|
||||
#login form {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
max-width: 16em;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
#login input {
|
||||
width: 100%;
|
||||
width: 100%;
|
||||
margin: .5em 0 0;
|
||||
}
|
||||
|
||||
#login .wrong {
|
||||
background: #F44336;
|
||||
color: #fff;
|
||||
padding: .5em;
|
||||
text-align: center;
|
||||
animation: .2s opac forwards;
|
||||
}
|
||||
|
||||
@keyframes opac {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
#login input[type="text"],
|
||||
#login input[type="password"] {
|
||||
padding: .5em 1em;
|
||||
border: 1px solid #e9e9e9;
|
||||
transition: .2s ease border;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
#login input[type="text"]:focus,
|
||||
#login input[type="password"]:focus,
|
||||
#login input[type="text"]:hover,
|
||||
#login input[type="password"]:hover {
|
||||
border-color: #9f9f9f;
|
||||
}
|
||||
</style>
|
||||
|
||||
279
assets/src/components/Main.vue
Normal file
279
assets/src/components/Main.vue
Normal file
@@ -0,0 +1,279 @@
|
||||
<template>
|
||||
<div :class="{ multiple, loading }">
|
||||
<header>
|
||||
<div>
|
||||
<button @click="openSidebar" aria-label="Toggle sidebar" title="Toggle sidebar" class="action">
|
||||
<i class="material-icons">menu</i>
|
||||
</button>
|
||||
<img src="../assets/logo.svg" alt="File Manager">
|
||||
<search></search>
|
||||
</div>
|
||||
<div>
|
||||
<button @click="openSearch" aria-label="Search" title="Search" class="search-button action">
|
||||
<i class="material-icons">search</i>
|
||||
</button>
|
||||
|
||||
<button v-show="isEditor" aria-label="Save" class="action" id="save">
|
||||
<i class="material-icons" title="Save">save</i>
|
||||
</button>
|
||||
<rename-button v-show="!loading && showRenameButton"></rename-button>
|
||||
<move-button v-show="!loading && showMoveButton"></move-button>
|
||||
<delete-button v-show="!loading && showDeleteButton"></delete-button>
|
||||
<switch-button v-show="!loading && req.kind !== 'editor'"></switch-button>
|
||||
<download-button></download-button>
|
||||
<upload-button v-show="!loading && showUpload"></upload-button>
|
||||
<info-button></info-button>
|
||||
|
||||
<button v-show="isListing" @click="$store.commit('multiple', true)" aria-label="Select multiple" class="action">
|
||||
<i class="material-icons">check_circle</i>
|
||||
<span>Select</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<sidebar></sidebar>
|
||||
|
||||
<main>
|
||||
<div v-if="loading">
|
||||
<h2 class="message">
|
||||
<span>Loading...</span>
|
||||
</h2>
|
||||
</div>
|
||||
<div v-else-if="error">
|
||||
<h2 class="message" v-if="error === 404">
|
||||
<i class="material-icons">gps_off</i>
|
||||
<span>This location can't be reached.</span>
|
||||
</h2>
|
||||
<h2 class="message" v-else-if="error === 403">
|
||||
<i class="material-icons">error</i>
|
||||
<span>You're not welcome here.</span>
|
||||
</h2>
|
||||
<h2 class="message" v-else>
|
||||
<i class="material-icons">error_outline</i>
|
||||
<span>Something really went wrong.</span>
|
||||
</h2>
|
||||
</div>
|
||||
<editor v-else-if="isEditor"></editor>
|
||||
<listing v-else-if="isListing"></listing>
|
||||
<preview v-else-if="isPreview"></preview>
|
||||
</main>
|
||||
|
||||
<prompts></prompts>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Search from './Search'
|
||||
import Preview from './Preview'
|
||||
import Listing from './Listing'
|
||||
import Editor from './Editor'
|
||||
import Sidebar from './Sidebar'
|
||||
import Prompts from './prompts/Prompts'
|
||||
import InfoButton from './buttons/Info'
|
||||
import DeleteButton from './buttons/Delete'
|
||||
import RenameButton from './buttons/Rename'
|
||||
import UploadButton from './buttons/Upload'
|
||||
import DownloadButton from './buttons/Download'
|
||||
import SwitchButton from './buttons/SwitchView'
|
||||
import MoveButton from './buttons/Move'
|
||||
import css from '@/utils/css'
|
||||
import api from '@/utils/api'
|
||||
import {mapGetters, mapState} from 'vuex'
|
||||
|
||||
function updateColumnSizes () {
|
||||
let columns = Math.floor(document.querySelector('main').offsetWidth / 300)
|
||||
let items = css(['#listing.mosaic .item', '.mosaic#listing .item'])
|
||||
|
||||
if (columns === 0) columns = 1
|
||||
|
||||
items.style.width = `calc(${100 / columns}% - 1em)`
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'main',
|
||||
components: {
|
||||
Search,
|
||||
Preview,
|
||||
Listing,
|
||||
Editor,
|
||||
Sidebar,
|
||||
InfoButton,
|
||||
DeleteButton,
|
||||
RenameButton,
|
||||
DownloadButton,
|
||||
UploadButton,
|
||||
SwitchButton,
|
||||
MoveButton,
|
||||
Prompts
|
||||
},
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'selectedCount'
|
||||
]),
|
||||
...mapState([
|
||||
'req',
|
||||
'user',
|
||||
'reload',
|
||||
'multiple'
|
||||
]),
|
||||
isListing () {
|
||||
return this.req.kind === 'listing' && !this.loading
|
||||
},
|
||||
isPreview () {
|
||||
return this.req.kind === 'preview' && !this.loading
|
||||
},
|
||||
isEditor () {
|
||||
return this.req.kind === 'editor' && !this.loading
|
||||
},
|
||||
showUpload () {
|
||||
if (this.req.kind === 'editor') return false
|
||||
return this.user.allowNew
|
||||
},
|
||||
showDeleteButton () {
|
||||
if (this.req.kind === 'listing') {
|
||||
if (this.selectedCount === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
return this.user.allowEdit
|
||||
}
|
||||
|
||||
return this.user.allowEdit
|
||||
},
|
||||
showRenameButton () {
|
||||
if (this.req.kind === 'listing') {
|
||||
if (this.selectedCount === 1) {
|
||||
return this.user.allowEdit
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return this.user.allowEdit
|
||||
},
|
||||
showMoveButton () {
|
||||
if (this.req.kind !== 'listing') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (this.selectedCount > 0) {
|
||||
return this.user.allowEdit
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
loading: true,
|
||||
error: null
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.fetchData()
|
||||
// TODO: finish this box
|
||||
// this.$store.commit('showHover', 'error')
|
||||
},
|
||||
watch: {
|
||||
'$route': 'fetchData',
|
||||
'reload': function () {
|
||||
this.$store.commit('setReload', false)
|
||||
this.fetchData()
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
updateColumnSizes()
|
||||
window.addEventListener('resize', updateColumnSizes)
|
||||
window.addEventListener('keydown', (event) => {
|
||||
// Esc!
|
||||
if (event.keyCode === 27) {
|
||||
this.$store.commit('closeHovers')
|
||||
|
||||
// Unselect all files and folders.
|
||||
if (this.req.kind === 'listing') {
|
||||
let items = document.getElementsByClassName('item')
|
||||
Array.from(items).forEach(link => {
|
||||
link.setAttribute('aria-selected', false)
|
||||
})
|
||||
|
||||
this.$store.commit('resetSelected')
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Del!
|
||||
if (event.keyCode === 46) {
|
||||
if (this.showDeleteButton) {
|
||||
this.$store.commit('showHover', 'delete')
|
||||
}
|
||||
}
|
||||
|
||||
// F1!
|
||||
if (event.keyCode === 112) {
|
||||
event.preventDefault()
|
||||
this.$store.commit('showHover', 'help')
|
||||
}
|
||||
|
||||
// F2!
|
||||
if (event.keyCode === 113) {
|
||||
if (this.showRenameButton) {
|
||||
this.$store.commit('showHover', 'rename')
|
||||
}
|
||||
}
|
||||
|
||||
// CTRL + S
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
switch (String.fromCharCode(event.which).toLowerCase()) {
|
||||
case 's':
|
||||
event.preventDefault()
|
||||
|
||||
if (this.req.kind !== 'editor') {
|
||||
window.location = '?download=true'
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: save file on editor!
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
fetchData () {
|
||||
// Set loading to true and reset the error.
|
||||
this.loading = true
|
||||
this.error = null
|
||||
|
||||
// Reset selected items and multiple selection.
|
||||
this.$store.commit('resetSelected')
|
||||
this.$store.commit('multiple', false)
|
||||
this.$store.commit('closeHovers')
|
||||
|
||||
let url = this.$route.path
|
||||
if (url === '') url = '/'
|
||||
if (url[0] !== '/') url = '/' + url
|
||||
|
||||
api.fetch(url)
|
||||
.then((trueURL) => {
|
||||
if (!url.endsWith('/') && trueURL.endsWith('/')) {
|
||||
window.history.replaceState(window.history.state, document.title, window.location.pathname + '/')
|
||||
}
|
||||
|
||||
this.loading = false
|
||||
})
|
||||
.catch(error => {
|
||||
// TODO: 404, 403 and 500!
|
||||
console.log(error)
|
||||
this.error = error
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
openSidebar () {
|
||||
this.$store.commit('showHover', 'sidebar')
|
||||
},
|
||||
openSearch () {
|
||||
this.$store.commit('showHover', 'search')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
68
assets/src/components/Preview.vue
Normal file
68
assets/src/components/Preview.vue
Normal file
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<div id="previewer">
|
||||
<div class="bar">
|
||||
<button @click="back" class="action" aria-label="Close Preview" id="close">
|
||||
<i class="material-icons">close</i>
|
||||
</button>
|
||||
|
||||
<rename-button v-if="allowEdit()"></rename-button>
|
||||
<delete-button v-if="allowEdit()"></delete-button>
|
||||
<download-button></download-button>
|
||||
<info-button></info-button>
|
||||
</div>
|
||||
|
||||
<div class="preview">
|
||||
<img v-if="req.type == 'image'" :src="raw()">
|
||||
<audio v-else-if="req.type == 'audio'" :src="raw()" controls></audio>
|
||||
<video v-else-if="req.type == 'video'" :src="raw()" controls>
|
||||
Sorry, your browser doesn't support embedded videos,
|
||||
but don't worry, you can <a :href="download()">download it</a>
|
||||
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">Download <i class="material-icons">file_download</i></h2>
|
||||
</a>
|
||||
<pre v-else >{{ req.content }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import url from '@/utils/url'
|
||||
import InfoButton from './buttons/Info'
|
||||
import DeleteButton from './buttons/Delete'
|
||||
import RenameButton from './buttons/Rename'
|
||||
import DownloadButton from './buttons/Download'
|
||||
|
||||
export default {
|
||||
name: 'preview',
|
||||
components: {
|
||||
InfoButton,
|
||||
DeleteButton,
|
||||
RenameButton,
|
||||
DownloadButton
|
||||
},
|
||||
computed: mapState(['req']),
|
||||
methods: {
|
||||
download: function () {
|
||||
let url = `${this.$store.state.baseURL}/api/download/`
|
||||
url += this.req.url.slice(6)
|
||||
url += `?token=${this.$store.state.jwt}`
|
||||
|
||||
return url
|
||||
},
|
||||
raw: function () {
|
||||
return `${this.download()}&inline=true`
|
||||
},
|
||||
back: function (event) {
|
||||
let uri = url.removeLastDir(this.$route.path) + '/'
|
||||
this.$router.push({ path: uri })
|
||||
},
|
||||
allowEdit: function (event) {
|
||||
return this.$store.state.user.allowEdit
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
179
assets/src/components/Search.vue
Normal file
179
assets/src/components/Search.vue
Normal file
@@ -0,0 +1,179 @@
|
||||
<template>
|
||||
<div id="search" @click="open" v-bind:class="{ active , ongoing }">
|
||||
<div id="input">
|
||||
<button v-if="active" class="action" @click="close">
|
||||
<i class="material-icons">arrow_back</i>
|
||||
</button>
|
||||
<i v-else class="material-icons">search</i>
|
||||
<input type="text"
|
||||
@keyup="keyup"
|
||||
@keyup.enter="submit"
|
||||
v-model.trim="value"
|
||||
aria-label="Write here to search"
|
||||
:placeholder="placeholder">
|
||||
</div>
|
||||
|
||||
<div id="result">
|
||||
<div>
|
||||
<span v-if="search.length === 0 && commands.length === 0">{{ text }}</span>
|
||||
<ul v-else-if="search.length > 0">
|
||||
<li v-for="s in search">
|
||||
<router-link @click.native="close" :to="'./' + s">./{{ s }}</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul v-else-if="commands.length > 0">
|
||||
<li v-for="c in commands">{{ c }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p><i class="material-icons spin">autorenew</i></p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import url from '@/utils/url'
|
||||
import api from '@/utils/api'
|
||||
|
||||
export default {
|
||||
name: 'search',
|
||||
data: function () {
|
||||
return {
|
||||
value: '',
|
||||
ongoing: false,
|
||||
scrollable: null,
|
||||
search: [],
|
||||
commands: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(['user', 'show']),
|
||||
// Computed property for activeness of search.
|
||||
active () {
|
||||
return (this.show === 'search')
|
||||
},
|
||||
// Placeholder value.
|
||||
placeholder: function () {
|
||||
if (this.user.allowCommands && this.user.commands.length > 0) {
|
||||
return 'Search or execute a command...'
|
||||
}
|
||||
|
||||
return 'Search...'
|
||||
},
|
||||
// The text that is shown on the results' box while
|
||||
// there is no search result or command output to show.
|
||||
text: function () {
|
||||
if (this.ongoing) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (this.value.length === 0) {
|
||||
if (this.user.allowCommands && this.user.commands.length > 0) {
|
||||
return `Search or use one of your supported commands: ${this.user.commands.join(', ')}.`
|
||||
}
|
||||
|
||||
return 'Type and press enter to search.'
|
||||
}
|
||||
|
||||
if (!this.supported() || !this.user.allowCommands) {
|
||||
return 'Press enter to search.'
|
||||
} else {
|
||||
return 'Press enter to execute.'
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted: function () {
|
||||
// Gets the result div which will be scrollable.
|
||||
this.scrollable = document.querySelector('#search #result')
|
||||
|
||||
// Adds the keydown event on window for the ESC key, so
|
||||
// when it's pressed, it closes the search window.
|
||||
window.addEventListener('keydown', (event) => {
|
||||
if (event.keyCode === 27) {
|
||||
this.$store.commit('closeHovers')
|
||||
}
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
// Sets the search to active.
|
||||
open: function (event) {
|
||||
this.$store.commit('showHover', 'search')
|
||||
},
|
||||
// Closes the search and prevents the event
|
||||
// of propagating so it doesn't trigger the
|
||||
// click event on #search.
|
||||
close: function (event) {
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
this.$store.commit('closeHovers')
|
||||
},
|
||||
// Checks if the current input is a supported command.
|
||||
supported: function () {
|
||||
let pieces = this.value.split(' ')
|
||||
|
||||
for (let i = 0; i < this.user.commands.length; i++) {
|
||||
if (pieces[0] === this.user.commands[0]) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
// When the user presses a key, if it is ESC
|
||||
// then it will close the search box. Otherwise,
|
||||
// it will set the search box to active and clean
|
||||
// the search results, as well as commands'.
|
||||
keyup: function (event) {
|
||||
if (event.keyCode === 27) {
|
||||
this.close(event)
|
||||
return
|
||||
}
|
||||
|
||||
this.search.length = 0
|
||||
this.commands.length = 0
|
||||
},
|
||||
// Submits the input to the server and sets ongoing to true.
|
||||
submit: function (event) {
|
||||
this.ongoing = true
|
||||
|
||||
let path = this.$route.path
|
||||
if (this.$store.state.req.kind !== 'listing') {
|
||||
path = url.removeLastDir(path) + '/'
|
||||
}
|
||||
|
||||
// In case of being a command.
|
||||
if (this.supported() && this.user.allowCommands) {
|
||||
api.command(path, this.value,
|
||||
(event) => {
|
||||
this.commands.push(event.data)
|
||||
this.scrollable.scrollTop = this.scrollable.scrollHeight
|
||||
},
|
||||
(event) => {
|
||||
this.ongoing = false
|
||||
this.scrollable.scrollTop = this.scrollable.scrollHeight
|
||||
this.$store.commit('setReload', true)
|
||||
}
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// In case of being a search.
|
||||
api.search(path, this.value,
|
||||
(event) => {
|
||||
let url = event.data
|
||||
if (url[0] === '/') url = url.substring(1)
|
||||
|
||||
this.search.push(url)
|
||||
this.scrollable.scrollTop = this.scrollable.scrollHeight
|
||||
},
|
||||
(event) => {
|
||||
this.ongoing = false
|
||||
this.scrollable.scrollTop = this.scrollable.scrollHeight
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
72
assets/src/components/Sidebar.vue
Normal file
72
assets/src/components/Sidebar.vue
Normal file
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<nav :class="{active}">
|
||||
<router-link class="action" to="/files/" aria-label="My Files" title="My Files">
|
||||
<i class="material-icons">folder</i>
|
||||
<span>My Files</span>
|
||||
</router-link>
|
||||
|
||||
<div v-if="user.allowNew">
|
||||
<button @click="$store.commit('showHover', 'newDir')" aria-label="New directory" title="New directory" class="action">
|
||||
<i class="material-icons">create_new_folder</i>
|
||||
<span>New folder</span>
|
||||
</button>
|
||||
|
||||
<button @click="$store.commit('showHover', 'newFile')" aria-label="New file" title="New file" class="action">
|
||||
<i class="material-icons">note_add</i>
|
||||
<span>New file</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-for="plugin in plugins">
|
||||
<button v-for="action in plugin.sidebar" @click="action.click" :aria-label="action.name" :title="action.name" :key="action.name" class="action">
|
||||
<i class="material-icons">{{ action.icon }}</i>
|
||||
<span>{{ action.name }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<router-link class="action" to="/dashboard" aria-label="Settings" title="Settings">
|
||||
<i class="material-icons">settings_applications</i>
|
||||
<span>Settings</span>
|
||||
</router-link>
|
||||
|
||||
<button @click="logout" class="action" id="logout" aria-label="Log out" title="Logout">
|
||||
<i class="material-icons">exit_to_app</i>
|
||||
<span>Logout</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p class="credits">Served with <a rel="noopener noreferrer" href="https://github.com/hacdias/caddy-filemanager">File Manager</a>.<br><a @click="help">Help</a></p>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapState} from 'vuex'
|
||||
import auth from '@/utils/auth'
|
||||
|
||||
export default {
|
||||
name: 'sidebar',
|
||||
data: () => {
|
||||
return {
|
||||
plugins: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(['user']),
|
||||
active () {
|
||||
return this.$store.state.show === 'sidebar'
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
if (window.plugins !== undefined || window.plugins !== null) {
|
||||
this.plugins = window.plugins
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
help: function () {
|
||||
this.$store.commit('showHover', 'help')
|
||||
},
|
||||
logout: auth.logout
|
||||
}
|
||||
}
|
||||
</script>
|
||||
17
assets/src/components/buttons/Delete.vue
Normal file
17
assets/src/components/buttons/Delete.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<button @click="show" aria-label="Delete" title="Delete" class="action" id="delete-button">
|
||||
<i class="material-icons">delete</i>
|
||||
<span>Delete</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'delete-button',
|
||||
methods: {
|
||||
show: function (event) {
|
||||
this.$store.commit('showHover', 'delete')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
35
assets/src/components/buttons/Download.vue
Normal file
35
assets/src/components/buttons/Download.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<button @click="download" aria-label="Download" title="Download" class="action">
|
||||
<i class="material-icons">file_download</i>
|
||||
<span>Download</span>
|
||||
<span v-if="selectedCount > 0" class="counter">{{ selectedCount }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapGetters, mapState} from 'vuex'
|
||||
import api from '@/utils/api'
|
||||
|
||||
export default {
|
||||
name: 'download-button',
|
||||
computed: {
|
||||
...mapState(['req', 'selected']),
|
||||
...mapGetters(['selectedCount'])
|
||||
},
|
||||
methods: {
|
||||
download: function (event) {
|
||||
if (this.req.kind !== 'listing') {
|
||||
api.download(null, this.$route.path)
|
||||
return
|
||||
}
|
||||
|
||||
if (this.selectedCount === 1) {
|
||||
api.download(null, this.req.items[this.selected[0]].url)
|
||||
return
|
||||
}
|
||||
|
||||
this.$store.commit('showHover', 'download')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
17
assets/src/components/buttons/Info.vue
Normal file
17
assets/src/components/buttons/Info.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<button title="Info" aria-label="Info" class="action" @click="show">
|
||||
<i class="material-icons">info</i>
|
||||
<span>Info</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'info-button',
|
||||
methods: {
|
||||
show: function (event) {
|
||||
this.$store.commit('showHover', 'info')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
17
assets/src/components/buttons/Move.vue
Normal file
17
assets/src/components/buttons/Move.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<button @click="show" aria-label="Move" title="Move" class="action" id="move-button">
|
||||
<i class="material-icons">forward</i>
|
||||
<span>Move file</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'move-button',
|
||||
methods: {
|
||||
show: function (event) {
|
||||
this.$store.commit('showHover', 'move')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
17
assets/src/components/buttons/Rename.vue
Normal file
17
assets/src/components/buttons/Rename.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<button @click="show" aria-label="Rename" title="Rename" class="action" id="rename-button">
|
||||
<i class="material-icons">mode_edit</i>
|
||||
<span>Rename</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'rename-button',
|
||||
methods: {
|
||||
show: function (event) {
|
||||
this.$store.commit('showHover', 'rename')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
28
assets/src/components/buttons/SwitchView.vue
Normal file
28
assets/src/components/buttons/SwitchView.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<button @click="change" aria-label="Switch View" title="Switch View" class="action" id="switch-view-button">
|
||||
<i class="material-icons">{{ icon() }}</i>
|
||||
<span>Switch view</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'switch-button',
|
||||
methods: {
|
||||
change: function (event) {
|
||||
let display = 'mosaic'
|
||||
|
||||
if (this.$store.state.req.display === 'mosaic') {
|
||||
display = 'list'
|
||||
}
|
||||
|
||||
this.$store.commit('listingDisplay', display)
|
||||
document.cookie = `display=${display}; max-age=31536000; path=${this.$store.state.baseURL}`
|
||||
},
|
||||
icon: function () {
|
||||
if (this.$store.state.req.display === 'mosaic') return 'view_list'
|
||||
return 'view_module'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
17
assets/src/components/buttons/Upload.vue
Normal file
17
assets/src/components/buttons/Upload.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<button @click="upload" aria-label="Upload" title="Upload" class="action" id="upload-button">
|
||||
<i class="material-icons">file_upload</i>
|
||||
<span>Upload</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'upload-button',
|
||||
methods: {
|
||||
upload: function (event) {
|
||||
document.getElementById('upload-input').click()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
71
assets/src/components/prompts/Delete.vue
Normal file
71
assets/src/components/prompts/Delete.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<div class="prompt">
|
||||
<h3>Delete files</h3>
|
||||
<p v-show="req.kind !== 'listing'">Are you sure you want to delete this file/folder?</p>
|
||||
<p v-show="req.kind === 'listing'">Are you sure you want to delete {{ selectedCount }} file(s)?</p>
|
||||
<div>
|
||||
<button @click="submit" autofocus>Delete</button>
|
||||
<button @click="closeHovers" class="cancel">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapGetters, mapMutations, mapState} from 'vuex'
|
||||
import api from '@/utils/api'
|
||||
import url from '@/utils/url'
|
||||
import buttons from '@/utils/buttons'
|
||||
|
||||
export default {
|
||||
name: 'delete',
|
||||
computed: {
|
||||
...mapGetters(['selectedCount']),
|
||||
...mapState(['req', 'selected'])
|
||||
},
|
||||
methods: {
|
||||
...mapMutations(['closeHovers']),
|
||||
submit: function (event) {
|
||||
this.closeHovers()
|
||||
buttons.loading('delete')
|
||||
|
||||
if (this.req.kind !== 'listing') {
|
||||
api.delete(this.$route.path)
|
||||
.then(() => {
|
||||
buttons.done('delete')
|
||||
this.$router.push({path: url.removeLastDir(this.$route.path) + '/'})
|
||||
})
|
||||
.catch(error => {
|
||||
buttons.done('delete')
|
||||
// TODO: show error in prompt
|
||||
console.log(error)
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (this.selectedCount === 0) {
|
||||
// This shouldn't happen...
|
||||
return
|
||||
}
|
||||
|
||||
let promises = []
|
||||
|
||||
for (let index of this.selected) {
|
||||
promises.push(api.delete(this.req.items[index].url))
|
||||
}
|
||||
|
||||
Promise.all(promises)
|
||||
.then(() => {
|
||||
this.$store.commit('setReload', true)
|
||||
buttons.done('delete')
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error)
|
||||
this.$store.commit('setReload', true)
|
||||
buttons.done('delete')
|
||||
// TODO: show error in prompt
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
41
assets/src/components/prompts/Download.vue
Normal file
41
assets/src/components/prompts/Download.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<div class="prompt" id="download">
|
||||
<h3>Download files</h3>
|
||||
<p>Choose the format you want to download.</p>
|
||||
<button @click="download('zip')" autofocus>zip</button>
|
||||
<button @click="download('tar')" autofocus>tar</button>
|
||||
<button @click="download('targz')" autofocus>tar.gz</button>
|
||||
<button @click="download('tarbz2')" autofocus>tar.bz2</button>
|
||||
<button @click="download('tarxz')" autofocus>tar.xz</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapGetters, mapState} from 'vuex'
|
||||
import api from '@/utils/api'
|
||||
|
||||
export default {
|
||||
name: 'download',
|
||||
computed: {
|
||||
...mapState(['selected', 'req']),
|
||||
...mapGetters(['selectedCount'])
|
||||
},
|
||||
methods: {
|
||||
download: function (format) {
|
||||
if (this.selectedCount === 0) {
|
||||
api.download(format, this.$route.path)
|
||||
} else {
|
||||
let files = []
|
||||
|
||||
for (let i of this.selected) {
|
||||
files.push(this.req.items[i].url)
|
||||
}
|
||||
|
||||
api.download(format, ...files)
|
||||
}
|
||||
|
||||
this.$store.commit('closeHovers')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
23
assets/src/components/prompts/Error.vue
Normal file
23
assets/src/components/prompts/Error.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<div class="prompt error">
|
||||
<i class="material-icons">error_outline</i>
|
||||
<h3>Something went wrong</h3>
|
||||
<pre>{{ error }}</pre>
|
||||
<div>
|
||||
<button @click="$store.commit('closeHovers')" autofocus>Close</button>
|
||||
<button @click="reportIssue" class="cancel">Report Issue</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'error',
|
||||
props: ['error'],
|
||||
methods: {
|
||||
reportIssue () {
|
||||
window.open('https://github.com/hacdias/filemanager/issues/new')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
31
assets/src/components/prompts/Help.vue
Normal file
31
assets/src/components/prompts/Help.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<div class="prompt help">
|
||||
<h3>Help</h3>
|
||||
|
||||
<ul>
|
||||
<li><strong>F1</strong> - this information</li>
|
||||
<li><strong>F2</strong> - rename file</li>
|
||||
<li><strong>DEL</strong> - delete selected items</li>
|
||||
<li><strong>ESC</strong> - clear selection and/or close the prompt</li>
|
||||
<li><strong>CTRL + S</strong> - save a file or download the directory where you are</li>
|
||||
<li><strong>CTRL + Click</strong> - select multiple files or directories</li>
|
||||
<li><strong>Double click</strong> - open a file or directory</li>
|
||||
<li><strong>Click</strong> - select file or directory</li>
|
||||
</ul>
|
||||
|
||||
<p>Not available yet</p>
|
||||
|
||||
<ul>
|
||||
<li><strong>Alt + Click</strong> - select a group of files</li>
|
||||
</ul>
|
||||
|
||||
<div>
|
||||
<button type="submit" @click="$store.commit('closeHovers')" class="ok">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {name: 'help'}
|
||||
</script>
|
||||
|
||||
102
assets/src/components/prompts/Info.vue
Normal file
102
assets/src/components/prompts/Info.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<div class="prompt">
|
||||
<h3>File Information</h3>
|
||||
|
||||
<p v-show="selected.length > 1">{{ selected.length }} files selected.</p>
|
||||
|
||||
<p v-show="selected.length < 2"><strong>Display Name:</strong> {{ name() }}</p>
|
||||
<p><strong>Size:</strong> <span id="content_length"></span>{{ humanSize() }}</p>
|
||||
<p v-show="selected.length < 2"><strong>Last Modified:</strong> {{ humanTime() }}</p>
|
||||
|
||||
<section v-show="dir() && selected.length === 0">
|
||||
<p><strong>Number of files:</strong> {{ req.numFiles }}</p>
|
||||
<p><strong>Number of directories:</strong> {{ req.numDirs }}</p>
|
||||
</section>
|
||||
|
||||
<section v-show="!dir()">
|
||||
<p><strong>MD5:</strong> <code><a @click="checksum($event, 'md5')">show</a></code></p>
|
||||
<p><strong>SHA1:</strong> <code><a @click="checksum($event, 'sha1')">show</a></code></p>
|
||||
<p><strong>SHA256:</strong> <code><a @click="checksum($event, 'sha256')">show</a></code></p>
|
||||
<p><strong>SHA512:</strong> <code><a @click="checksum($event, 'sha512')">show</a></code></p>
|
||||
</section>
|
||||
|
||||
<div>
|
||||
<button type="submit" @click="$store.commit('closeHovers')" class="ok">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapState, mapGetters} from 'vuex'
|
||||
import filesize from 'filesize'
|
||||
import moment from 'moment'
|
||||
import api from '@/utils/api'
|
||||
|
||||
export default {
|
||||
name: 'info',
|
||||
computed: {
|
||||
...mapState(['req', 'selected']),
|
||||
...mapGetters(['selectedCount'])
|
||||
},
|
||||
methods: {
|
||||
humanSize: function () {
|
||||
if (this.selectedCount === 0 || this.req.kind !== 'listing') {
|
||||
return filesize(this.req.size)
|
||||
}
|
||||
|
||||
var sum = 0
|
||||
|
||||
for (let i = 0; i < this.selectedCount; i++) {
|
||||
sum += this.req.items[this.selected[i]].size
|
||||
}
|
||||
|
||||
return filesize(sum)
|
||||
},
|
||||
humanTime: function () {
|
||||
if (this.selectedCount === 0) {
|
||||
return moment(this.req.modified).fromNow()
|
||||
}
|
||||
|
||||
return moment(this.req.items[this.selected[0]]).fromNow()
|
||||
},
|
||||
name: function () {
|
||||
if (this.selectedCount === 0) {
|
||||
return this.req.name
|
||||
}
|
||||
|
||||
return this.req.items[this.selected[0]].name
|
||||
},
|
||||
dir: function () {
|
||||
if (this.selectedCount > 1) {
|
||||
// Don't show when multiple selected.
|
||||
return true
|
||||
}
|
||||
|
||||
if (this.selectedCount === 0) {
|
||||
return this.req.isDir
|
||||
}
|
||||
|
||||
return this.req.items[this.selected[0]].isDir
|
||||
},
|
||||
checksum: function (event, hash) {
|
||||
event.preventDefault()
|
||||
|
||||
let link
|
||||
|
||||
if (this.selectedCount) {
|
||||
link = this.req.items[this.selected[0]].url
|
||||
} else {
|
||||
link = this.$route.path
|
||||
}
|
||||
|
||||
api.checksum(link, hash)
|
||||
.then((hash) => {
|
||||
event.target.innerHTML = hash
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
150
assets/src/components/prompts/Move.vue
Normal file
150
assets/src/components/prompts/Move.vue
Normal file
@@ -0,0 +1,150 @@
|
||||
<template>
|
||||
<div class="prompt">
|
||||
<h3>Move</h3>
|
||||
<p>Choose new house for your file(s)/folder(s):</p>
|
||||
|
||||
<ul class="file-list">
|
||||
<li @click="select" @dblclick="next" :key="item.name" v-for="item in items" :data-url="item.url">{{ item.name }}</li>
|
||||
</ul>
|
||||
|
||||
<p>Currently navigating on: <code>{{ current }}</code>.</p>
|
||||
|
||||
<div>
|
||||
<button class="ok" @click="move">Move</button>
|
||||
<button class="cancel" @click="$store.commit('closeHovers')">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import url from '@/utils/url'
|
||||
import api from '@/utils/api'
|
||||
import buttons from '@/utils/buttons'
|
||||
|
||||
export default {
|
||||
name: 'move',
|
||||
data: function () {
|
||||
return {
|
||||
items: [],
|
||||
current: window.location.pathname
|
||||
}
|
||||
},
|
||||
computed: mapState(['req', 'selected', 'baseURL']),
|
||||
mounted: function () {
|
||||
if (this.$route.path !== '/files/') {
|
||||
this.items.push({
|
||||
name: '..',
|
||||
url: url.removeLastDir(this.$route.path) + '/'
|
||||
})
|
||||
}
|
||||
|
||||
if (this.req.kind === 'listing') {
|
||||
for (let item of this.req.items) {
|
||||
if (!item.isDir) continue
|
||||
|
||||
this.items.push({
|
||||
name: item.name,
|
||||
url: item.url
|
||||
})
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
move: function (event) {
|
||||
event.preventDefault()
|
||||
|
||||
let el = event.currentTarget
|
||||
let promises = []
|
||||
let dest = this.current
|
||||
buttons.loading('move')
|
||||
|
||||
let selected = el.querySelector('li[aria-selected=true]')
|
||||
if (selected !== null) {
|
||||
dest = selected.dataset.url
|
||||
}
|
||||
|
||||
for (let item of this.selected) {
|
||||
let from = this.req.items[item].url
|
||||
let to = dest + '/' + encodeURIComponent(this.req.items[item].name)
|
||||
to = to.replace('//', '/')
|
||||
|
||||
promises.push(api.move(from, to))
|
||||
}
|
||||
|
||||
this.$store.commit('showMove', false)
|
||||
|
||||
Promise.all(promises)
|
||||
.then(() => {
|
||||
buttons.done('move')
|
||||
this.$router.push({page: dest})
|
||||
})
|
||||
.catch(e => {
|
||||
buttons.done('move')
|
||||
// TODO: show error in prompt
|
||||
console.log(e)
|
||||
})
|
||||
},
|
||||
next: function (event) {
|
||||
let uri = event.currentTarget.dataset.url
|
||||
this.json(uri)
|
||||
.then((data) => {
|
||||
this.current = uri
|
||||
this.items = []
|
||||
|
||||
if (uri !== this.baseURL + '/') {
|
||||
this.items.push({
|
||||
name: '..',
|
||||
url: url.removeLastDir(uri) + '/'
|
||||
})
|
||||
}
|
||||
|
||||
let req = JSON.parse(data)
|
||||
for (let item of req.items) {
|
||||
if (!item.isDir) continue
|
||||
|
||||
this.items.push({
|
||||
name: item.name,
|
||||
url: item.uri
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch(e => console.log(e))
|
||||
},
|
||||
json: function (url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new XMLHttpRequest()
|
||||
request.open('GET', url)
|
||||
request.setRequestHeader('Accept', 'application/json')
|
||||
request.onload = () => {
|
||||
if (request.status === 200) {
|
||||
resolve(request.responseText)
|
||||
} else {
|
||||
reject(request.statusText)
|
||||
}
|
||||
}
|
||||
request.onerror = () => reject(request.statusText)
|
||||
request.send()
|
||||
})
|
||||
},
|
||||
select: function (event) {
|
||||
let el = event.currentTarget
|
||||
|
||||
if (el.getAttribute('aria-selected') === 'true') {
|
||||
el.setAttribute('aria-selected', false)
|
||||
return
|
||||
}
|
||||
|
||||
let el2 = this.$el.querySelector('li[aria-selected=true]')
|
||||
if (el2) {
|
||||
el2.setAttribute('aria-selected', false)
|
||||
}
|
||||
|
||||
el.setAttribute('aria-selected', true)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
51
assets/src/components/prompts/NewDir.vue
Normal file
51
assets/src/components/prompts/NewDir.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<div class="prompt">
|
||||
<h3>New directory</h3>
|
||||
<p>Write the name of the new directory.</p>
|
||||
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
|
||||
<div>
|
||||
<button class="ok" @click="submit">Create</button>
|
||||
<button class="cancel" @click="$store.commit('closeHovers')">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import url from '@/utils/url'
|
||||
import api from '@/utils/api'
|
||||
|
||||
export default {
|
||||
name: 'new-dir',
|
||||
data: function () {
|
||||
return {
|
||||
name: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
submit: function (event) {
|
||||
event.preventDefault()
|
||||
if (this.new === '') return
|
||||
|
||||
let uri = this.$route.path
|
||||
if (this.$store.state.req.kind !== 'listing') {
|
||||
uri = url.removeLastDir(uri) + '/'
|
||||
}
|
||||
|
||||
uri += this.name + '/'
|
||||
uri = uri.replace('//', '/')
|
||||
|
||||
api.post(uri)
|
||||
.then(() => {
|
||||
this.$router.push({ path: uri })
|
||||
})
|
||||
.catch(error => {
|
||||
// TODO: Show error message!
|
||||
console.log(error)
|
||||
})
|
||||
|
||||
this.$store.commit('closeHovers')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
51
assets/src/components/prompts/NewFile.vue
Normal file
51
assets/src/components/prompts/NewFile.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<div class="prompt">
|
||||
<h3>New file</h3>
|
||||
<p>Write the name of the new file.</p>
|
||||
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
|
||||
<div>
|
||||
<button class="ok" @click="submit">Create</button>
|
||||
<button class="cancel" @click="$store.commit('closeHovers')">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import url from '@/utils/url'
|
||||
import api from '@/utils/api'
|
||||
|
||||
export default {
|
||||
name: 'new-file',
|
||||
data: function () {
|
||||
return {
|
||||
name: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
submit: function (event) {
|
||||
event.preventDefault()
|
||||
if (this.new === '') return
|
||||
|
||||
let uri = this.$route.path
|
||||
if (this.$store.state.req.kind !== 'listing') {
|
||||
uri = url.removeLastDir(uri) + '/'
|
||||
}
|
||||
|
||||
uri += this.name
|
||||
uri = uri.replace('//', '/')
|
||||
|
||||
api.post(uri)
|
||||
.then(() => {
|
||||
this.$router.push({ path: uri })
|
||||
})
|
||||
.catch(error => {
|
||||
// TODO: show error message in a box
|
||||
console.log(error)
|
||||
})
|
||||
|
||||
this.$store.commit('closeHovers')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
63
assets/src/components/prompts/Prompts.vue
Normal file
63
assets/src/components/prompts/Prompts.vue
Normal file
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<div>
|
||||
<help v-if="showHelp" ></help>
|
||||
<download v-else-if="showDownload"></download>
|
||||
<new-file v-else-if="showNewFile"></new-file>
|
||||
<new-dir v-else-if="showNewDir"></new-dir>
|
||||
<rename v-else-if="showRename"></rename>
|
||||
<delete v-else-if="showDelete"></delete>
|
||||
<info v-else-if="showInfo"></info>
|
||||
<move v-else-if="showMove"></move>
|
||||
<error v-else-if="showError"></error>
|
||||
|
||||
<div v-show="showOverlay" @click="resetPrompts" class="overlay"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Help from './Help'
|
||||
import Info from './Info'
|
||||
import Delete from './Delete'
|
||||
import Rename from './Rename'
|
||||
import Download from './Download'
|
||||
import Move from './Move'
|
||||
import Error from './Error'
|
||||
import NewFile from './NewFile'
|
||||
import NewDir from './NewDir'
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'prompts',
|
||||
components: {
|
||||
Info,
|
||||
Delete,
|
||||
Rename,
|
||||
Error,
|
||||
Download,
|
||||
Move,
|
||||
NewFile,
|
||||
NewDir,
|
||||
Help
|
||||
},
|
||||
computed: {
|
||||
...mapState(['show']),
|
||||
showError: function () { return this.show === 'error' },
|
||||
showInfo: function () { return this.show === 'info' },
|
||||
showHelp: function () { return this.show === 'help' },
|
||||
showDelete: function () { return this.show === 'delete' },
|
||||
showRename: function () { return this.show === 'rename' },
|
||||
showMove: function () { return this.show === 'move' },
|
||||
showNewFile: function () { return this.show === 'newFile' },
|
||||
showNewDir: function () { return this.show === 'newDir' },
|
||||
showDownload: function () { return this.show === 'download' },
|
||||
showOverlay: function () {
|
||||
return (this.show !== null && this.show !== 'search')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
resetPrompts () {
|
||||
this.$store.commit('closeHovers')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
73
assets/src/components/prompts/Rename.vue
Normal file
73
assets/src/components/prompts/Rename.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<div class="prompt">
|
||||
<h3>Rename</h3>
|
||||
<p>Insert a new name for <code>{{ oldName() }}</code>:</p>
|
||||
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
|
||||
<div>
|
||||
<button @click="submit" type="submit">Rename</button>
|
||||
<button @click="cancel" class="cancel">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import url from '@/utils/url'
|
||||
import api from '@/utils/api'
|
||||
|
||||
export default {
|
||||
name: 'rename',
|
||||
data: function () {
|
||||
return {
|
||||
name: ''
|
||||
}
|
||||
},
|
||||
computed: mapState(['req', 'selected', 'selectedCount']),
|
||||
methods: {
|
||||
cancel: function (event) {
|
||||
this.$store.commit('closeHovers')
|
||||
},
|
||||
oldName: function () {
|
||||
if (this.req.kind !== 'listing') {
|
||||
return this.req.name
|
||||
}
|
||||
|
||||
if (this.selectedCount === 0 || this.selectedCount > 1) {
|
||||
// This shouldn't happen.
|
||||
return
|
||||
}
|
||||
|
||||
return this.req.items[this.selected[0]].name
|
||||
},
|
||||
submit: function (event) {
|
||||
let oldLink = ''
|
||||
let newLink = ''
|
||||
|
||||
if (this.req.kind !== 'listing') {
|
||||
oldLink = this.req.url
|
||||
} else {
|
||||
oldLink = this.req.items[this.selected[0]].url
|
||||
}
|
||||
|
||||
this.name = encodeURIComponent(this.name)
|
||||
newLink = url.removeLastDir(oldLink) + '/' + this.name
|
||||
|
||||
api.move(oldLink, newLink)
|
||||
.then(() => {
|
||||
if (this.req.kind !== 'listing') {
|
||||
this.$router.push({ path: newLink })
|
||||
return
|
||||
}
|
||||
// TODO: keep selected after reload?
|
||||
this.$store.commit('setReload', true)
|
||||
}).catch(error => {
|
||||
// TODO: show error message
|
||||
console.log(error)
|
||||
})
|
||||
|
||||
this.$store.commit('closeHovers')
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user