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:
Henrique Dias
2017-07-06 08:07:33 +01:00
parent a3bafdc3e0
commit b50b0d57fe
232 changed files with 29544 additions and 0 deletions

View 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>

View 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>

View 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">&mdash;</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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>