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

21
assets/src/App.vue Normal file
View File

@@ -0,0 +1,21 @@
<template>
<router-view></router-view>
</template>
<script>
export default {
name: 'app',
mounted: function () {
let loading = document.getElementById('loading')
loading.classList.add('done')
setTimeout(function () {
loading.parentNode.removeChild(loading)
}, 200)
}
}
</script>
<style>
@import './css/styles.css';
</style>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,5 @@
<svg id="content" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 144 144">
<circle cx="72" cy="72" r="72" fill="#2979ff"/>
<circle cx="72" cy="72" r="48" fill="#40c4ff"/>
<circle cx="72" cy="72" r="24" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 235 B

View File

@@ -0,0 +1,60 @@
// Most of the code from this file comes from:
// https://github.com/codemirror/CodeMirror/blob/master/addon/mode/loadmode.js
import * as CodeMirror from 'codemirror'
import store from '@/store/store'
// Make CodeMirror available globally so the modes' can register themselves.
window.CodeMirror = CodeMirror
CodeMirror.modeURL = store.state.baseURL + '/static/js/codemirror/mode/%N/%N.js'
var loading = {}
function splitCallback (cont, n) {
var countDown = n
return function () {
if (--countDown === 0) cont()
}
}
function ensureDeps (mode, cont) {
var deps = CodeMirror.modes[mode].dependencies
if (!deps) return cont()
var missing = []
for (var i = 0; i < deps.length; ++i) {
if (!CodeMirror.modes.hasOwnProperty(deps[i])) missing.push(deps[i])
}
if (!missing.length) return cont()
var split = splitCallback(cont, missing.length)
for (i = 0; i < missing.length; ++i) CodeMirror.requireMode(missing[i], split)
}
CodeMirror.requireMode = function (mode, cont) {
if (typeof mode !== 'string') mode = mode.name
if (CodeMirror.modes.hasOwnProperty(mode)) return ensureDeps(mode, cont)
if (loading.hasOwnProperty(mode)) return loading[mode].push(cont)
var file = CodeMirror.modeURL.replace(/%N/g, mode)
var script = document.createElement('script')
script.src = file
var others = document.getElementsByTagName('script')[0]
var list = loading[mode] = [cont]
CodeMirror.on(script, 'load', function () {
ensureDeps(mode, function () {
for (var i = 0; i < list.length; ++i) list[i]()
})
})
others.parentNode.insertBefore(script, others)
}
CodeMirror.autoLoadMode = function (instance, mode) {
if (CodeMirror.modes.hasOwnProperty(mode)) return
CodeMirror.requireMode(mode, function () {
instance.setOption('mode', mode)
})
}
export default CodeMirror

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>

117
assets/src/css/base.css Normal file
View File

@@ -0,0 +1,117 @@
body {
font-family: 'Roboto', sans-serif;
padding-top: 4em;
background-color: #f8f8f8;
user-select: none;
}
* {
box-sizing: border-box;
}
*,
*:hover,
*:active,
*:focus {
outline: 0
}
a {
text-decoration: none;
}
img {
max-width: 100%;
}
audio,
video {
width: 100%;
}
pre {
padding: 1em;
border: 1px solid #e6e6e6;
border-radius: 0.5em;
background-color: #f5f5f5;
white-space: pre-wrap;
white-space: -moz-pre-wrap;
white-space: -pre-wrap;
white-space: -o-pre-wrap;
word-wrap: break-word;
}
input,
button {
outline: 0 !important;
}
input[type="submit"],
button {
border: 0;
padding: .5em 1em;
margin-left: .5em;
border-radius: .1em;
cursor: pointer;
background: #2196f3;
color: #fff;
border: 1px solid rgba(0, 0, 0, 0.05);
box-shadow: 0 0 5px rgba(0, 0, 0, 0.05);
transition: .1s ease all;
}
input[type="submit"]:hover,
button:hover {
background-color: #1E88E5;
}
.mobile-only {
display: none !important;
}
.container {
width: 95%;
max-width: 960px;
margin: 1em auto 0;
}
i.spin {
animation: 1s spin linear infinite;
}
#app {
transition: .2s ease padding;
}
#app.multiple {
padding-bottom: 4em;
}
nav {
width: 16em;
position: fixed;
top: 4em;
left: 0;
}
nav .action {
width: 100%;
display: block;
border-radius: 0;
font-size: 1.1em;
padding: .5em;
}
nav>div {
border-top: 1px solid rgba(0, 0, 0, 0.05);
}
nav .action>* {
vertical-align: middle;
}
main {
min-height: 1em;
margin: 0 1em 0 auto;
width: calc(100% - 19em);
}

View File

@@ -0,0 +1,5 @@
@import "~codemirror/lib/codemirror.css";
.CodeMirror {
height: auto;
}

137
assets/src/css/fonts.css Normal file
View File

@@ -0,0 +1,137 @@
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: local('Roboto'), local('Roboto-Regular'), url(../assets/fonts/roboto/normal-cyrillic-ext.woff2) format('woff2');
unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F;
}
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: local('Roboto'), local('Roboto-Regular'), url(../assets/fonts/roboto/normal-cyrillic.woff2) format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: local('Roboto'), local('Roboto-Regular'), url(../assets/fonts/roboto/normal-greek-ext.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: local('Roboto'), local('Roboto-Regular'), url(../assets/fonts/roboto/normal-greek.woff2) format('woff2');
unicode-range: U+0370-03FF;
}
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: local('Roboto'), local('Roboto-Regular'), url(../assets/fonts/roboto/normal-vietnamese.woff2) format('woff2');
unicode-range: U+0102-0103, U+1EA0-1EF9, U+20AB;
}
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: local('Roboto'), local('Roboto-Regular'), url(../assets/fonts/roboto/normal-latin-ext.woff2) format('woff2');
unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: local('Roboto'), local('Roboto-Regular'), url(../assets/fonts/roboto/normal-latin.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
}
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
src: local('Roboto Medium'), local('Roboto-Medium'), url(../assets/fonts/roboto/medium-cyrillic-ext.woff2) format('woff2');
unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F;
}
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
src: local('Roboto Medium'), local('Roboto-Medium'), url(../assets/fonts/roboto/medium-cyrillic.woff2) format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
src: local('Roboto Medium'), local('Roboto-Medium'), url(../assets/fonts/roboto/medium-greek-ext.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
src: local('Roboto Medium'), local('Roboto-Medium'), url(../assets/fonts/roboto/medium-greek.woff2) format('woff2');
unicode-range: U+0370-03FF;
}
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
src: local('Roboto Medium'), local('Roboto-Medium'), url(../assets/fonts/roboto/medium-vietnamese.woff2) format('woff2');
unicode-range: U+0102-0103, U+1EA0-1EF9, U+20AB;
}
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
src: local('Roboto Medium'), local('Roboto-Medium'), url(../assets/fonts/roboto/medium-latin-ext.woff2) format('woff2');
unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
src: local('Roboto Medium'), local('Roboto-Medium'), url(../assets/fonts/roboto/medium-latin.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
}
@font-face {
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
src: local('Material Icons'), local('MaterialIcons-Regular'), url(../assets/fonts/material/icons.woff2) format('woff2');
}
.prompt .file-list ul li:before,
.material-icons {
font-family: 'Material Icons';
font-weight: normal;
font-style: normal;
font-size: 24px;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
-moz-osx-font-smoothing: grayscale;
font-feature-settings: 'liga';
}

226
assets/src/css/header.css Normal file
View File

@@ -0,0 +1,226 @@
header {
z-index: 1000;
background-color: #fff;
border-bottom: 1px solid rgba(0, 0, 0, 0.075);
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
position: fixed;
top: 0;
left: 0;
width: 100%;
padding: 0;
display: flex;
}
header a,
header a:hover {
color: inherit;
}
header>div:first-child>.action,
header img {
margin-right: 1em;
}
header img {
height: 2.5em;
}
header>div:first-child>.action {
display: none;
}
header>div {
display: flex;
width: 100%;
padding: 0.5em 0.5em 0.5em 1em;
align-items: center;
}
header .action span {
display: none;
}
header>div div {
vertical-align: middle;
position: relative;
}
header>div:first-child {
height: 4em;
}
header>div:last-child {
justify-content: flex-end;
}
header .search-button {
display: none;
}
/* * * * * * * * * * * * * * * *
* MORE?? *
* * * * * * * * * * * * * * * */
#more {
display: none;
}
#file-only {
display: inline-block;
border-right: 1px solid rgba(0, 0, 0, 0.075);
padding-right: .3em;
margin-right: .3em;
transition: .2s ease opacity, visibility;
visibility: visible;
}
#file-only.disabled {
opacity: 0;
visibility: hidden;
}
#download ul.active {
top: 0;
right: 0;
}
#more ul.active {
right: .5em;
top: 4.5em;
}
/* * * * * * * * * * * * * * * *
* SEARCH BAR *
* * * * * * * * * * * * * * * */
#search {
position: relative;
height: 100%;
width: 100%;
max-width: 25em;
}
#search.active {
position: fixed;
top: 0;
right: 0;
width: 100%;
max-width: 100%;
height: 100%;
z-index: 9999;
}
#search #input {
background-color: #f5f5f5;
display: flex;
padding: 0.75em;
border-radius: 0.3em;
transition: .1s ease all;
align-items: center;
z-index: 2;
}
#search.active #input {
border-bottom: 1px solid rgba(0, 0, 0, 0.075);
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
background-color: #fff;
height: 4em;
}
#search.active>div {
border-radius: 0 !important;
}
#search.active i,
#search.active input {
color: #212121;
}
#search #input>.action,
#search #input>i {
margin-right: 0.3em;
user-select: none;
}
#search input {
width: 100%;
border: 0;
outline: 0;
background-color: transparent;
}
#search #result {
visibility: visible;
max-height: none;
background-color: #fff;
text-align: left;
color: #ccc;
padding: 0;
height: 0;
transition: .1s ease height, .1s ease padding;
overflow-x: hidden;
overflow-y: auto;
z-index: 1;
}
#search.active #result {
padding: .5em;
height: calc(100% - 4em);
}
#search ul {
padding: 0;
margin: 0;
list-style: none;
}
#search li {
margin-bottom: .5em;
}
#search #result div {
white-space: pre-wrap;
white-space: -moz-pre-wrap;
white-space: -pre-wrap;
white-space: -o-pre-wrap;
word-wrap: break-word;
}
#search #result p {
width: 100%;
text-align: center;
display: none;
margin: 0;
max-width: none;
}
#search.ongoing #result p {
display: block;
}
#search.active #result i {
color: #ccc;
text-align: center;
margin: 0 auto;
display: table;
}
#search::-webkit-input-placeholder {
color: rgba(255, 255, 255, .5);
}
#search:-moz-placeholder {
opacity: 1;
color: rgba(255, 255, 255, .5);
}
#search::-moz-placeholder {
opacity: 1;
color: rgba(255, 255, 255, .5);
}
#search:-ms-input-placeholder {
color: rgba(255, 255, 255, .5);
}

236
assets/src/css/listing.css Normal file
View File

@@ -0,0 +1,236 @@
#listing h2 {
margin: 0 0 0 0.5em;
font-size: .9em;
color: rgba(0, 0, 0, 0.38);
font-weight: 500;
}
#listing .item div:last-of-type * {
text-overflow: ellipsis;
overflow: hidden;
}
#listing>div {
display: flex;
padding: 0;
flex-wrap: wrap;
justify-content: flex-start;
position: relative;
}
#listing .item {
background-color: #fff;
position: relative;
display: flex;
flex-wrap: nowrap;
color: #6f6f6f;
transition: .1s ease all;
align-items: center;
cursor: pointer;
}
#listing .item div:last-of-type {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
#listing .item p {
margin: 0;
}
#listing .item .size,
#listing .item .modified {
font-size: 0.9em;
}
#listing .item .name {
font-weight: bold;
}
#listing .item i {
font-size: 4em;
margin-right: 0.1em;
vertical-align: bottom;
}
.message {
text-align: center;
font-size: 2em;
margin: 1em auto;
display: block !important;
width: 95%;
color: rgba(0, 0, 0, 0.3);
font-weight: 500;
}
.message i {
font-size: 2.5em;
margin-bottom: .2em;
display: block;
}
#listing.mosaic {
padding-top: 1em;
}
#listing.mosaic .item {
width: calc(33% - 1em);
margin: .5em;
padding: 0.5em;
border-radius: 0.2em;
box-shadow: 0 1px 3px rgba(0, 0, 0, .06), 0 1px 2px rgba(0, 0, 0, .12);
}
#listing.mosaic .item:hover {
box-shadow: 0 1px 3px rgba(0, 0, 0, .12), 0 1px 2px rgba(0, 0, 0, .24) !important;
}
#listing.mosaic .header {
display: none;
}
#listing.mosaic .item div:first-of-type {
width: 5em;
}
#listing.mosaic .item div:last-of-type {
width: calc(100% - 5vw);
}
#listing.list {
flex-direction: column;
padding-top: 3.25em;
width: 100%;
max-width: 100%;
margin: 0;
}
#listing.list .item {
width: 100%;
margin: 0;
border: 1px solid rgba(0, 0, 0, 0.1);
padding: 1em;
border-top: 0;
}
#listing.list h2 {
display: none;
}
#listing .item[aria-selected=true] {
background: #2196f3 !important;
color: #fff !important;
}
#listing.list .item div:first-of-type {
width: 3em;
}
#listing.list .item div:first-of-type i {
font-size: 2em;
}
#listing.list .item div:last-of-type {
width: calc(100% - 3em);
display: flex;
align-items: center;
}
#listing.list .item .name {
width: 50%;
}
#listing.list .item .size {
width: 25%;
}
#listing .item.header {
display: none !important;
background-color: #ccc;
}
#listing.list .header i {
font-size: 1.5em;
vertical-align: middle;
margin-left: .2em;
}
#listing.list .item.header {
display: flex !important;
background: #f8f8f8;
position: fixed;
width: calc(100% - 19em);
top: 4em;
right: 1em;
z-index: 999;
padding: .85em;
border: 0;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}
#listing.list .item.header>div:first-child {
width: 0;
}
#listing.list .item.header .name {
margin-right: 3em;
}
#listing.list .header a {
color: inherit;
}
#listing.list .item.header>div:first-child {
width: 0;
}
#listing.list .name {
font-weight: normal;
}
#listing.list .item.header .name {
margin-right: 3em;
}
#listing.list .header span {
vertical-align: middle;
}
#listing.list .header i {
opacity: 0;
transition: .1s ease all;
}
#listing.list .header p:hover i,
#listing.list .header .active i {
opacity: 1;
}
#listing.list .item.header .active {
font-weight: bold;
}
#listing #multiple-selection {
position: fixed;
bottom: -4em;
left: 0;
z-index: 99999;
width: 100%;
background-color: #2196f3;
height: 4em;
display: flex !important;
padding: 0.5em 0.5em 0.5em 1em;
justify-content: space-between;
align-items: center;
transition: .2s ease bottom;
}
#listing #multiple-selection.active {
bottom: 0;
}
#listing #multiple-selection p,
#listing #multiple-selection i {
color: #fff;
}

49
assets/src/css/mobile.css Normal file
View File

@@ -0,0 +1,49 @@
@media (max-width: 1024px) {
nav {
width: 10em
}
}
@media (max-width: 1024px) {
#listing.list .item.header,
main {
width: calc(100% - 13em)
}
}
@media (max-width: 736px) {
nav {
top: 0;
z-index: 99999;
background: #fff;
height: 100%;
width: 16em;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
transition: .1s ease left;
left: -17em;
}
nav.active {
left: 0;
}
header .search-button,
header>div:first-child>.action {
display: inherit;
}
header img {
display: none;
}
#listing.list .item.header,
main {
width: calc(100% - 2em);
}
main {
margin: 0 1em;
width: calc(100% - 2em);
}
#search {
display: none;
}
#search.active {
display: block;
}
}

168
assets/src/css/prompts.css Normal file
View File

@@ -0,0 +1,168 @@
.prompt {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: #fff;
border: 1px solid rgba(0, 0, 0, 0.075);
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
padding: 2em;
max-width: 25em;
width: 90%;
max-height: 95%;
z-index: 99999;
animation: .1s show forwards;
}
.overlay {
background-color: rgba(0, 0, 0, 0.5);
position: fixed;
top: 0;
left: 0;
height: 100%;
width: 100%;
z-index: 9999;
animation: .1s show forwards;
}
.prompt h3 {
margin: 0;
font-weight: 500;
font-size: 1.5em;
}
.prompt p {
font-size: .9em;
color: rgba(0, 0, 0, 0.8);
margin: .5em 0 1em;
}
.prompt input {
width: 100%;
border: 1px solid #dadada;
line-height: 1;
padding: .3em;
}
.prompt code {
word-wrap: break-word;
}
.prompt div {
margin-top: 1em;
display: flex;
justify-content: flex-start;
flex-direction: row-reverse;
}
.prompt .cancel {
background-color: #ECEFF1;
color: #37474F;
}
.prompt .cancel:hover {
background-color: #e9eaeb;
}
.prompt.error i {
color: #F44336;
display: block;
margin: 0 auto .15em;
text-align: center;
font-size: 5em;
}
.prompt.error h3 {
text-align: center;
}
.prompt.error button:not(.cancel) {
background-color: #F44336
}
/* * * * * * * * * * * * * * * *
* PROMPT - MOVE *
* * * * * * * * * * * * * * * */
.file-list {
max-height: 50vh;
overflow: auto;
list-style: none;
margin: 0;
padding: 0;
width: 100%;
}
.file-list li {
width: 100%;
user-select: none;
border-radius: .2em;
padding: .3em;
}
.file-list li[aria-selected=true] {
background: #2196f3 !important;
color: #fff !important;
transition: .1s ease all;
}
.file-list li:hover {
background-color: #e9eaeb;
cursor: pointer;
}
.file-list li:before {
content: "folder";
color: #6f6f6f;
vertical-align: middle;
line-height: 1.4;
font-family: 'Material Icons';
font-size: 1.75em;
margin-right: .25em;
}
.file-list li[aria-selected=true]:before {
color: white;
}
.prompt#download {
max-width: 15em;
}
.prompt#download button {
width: 100%;
display: block;
margin: 0 0 1em;
background-color: #ECEFF1;
color: #37474F;
}
.prompt#download button:last-of-type {
margin-bottom: 0;
}
.help {
max-width: 24em;
}
.help ul {
padding: 0;
margin: 1em 0;
list-style: none;
}
@keyframes show {
0% {
display: none;
opacity: 0;
}
1% {
display: block;
opacity: 0;
}
100% {
display: block;
opacity: 1;
}
}

196
assets/src/css/styles.css Normal file
View File

@@ -0,0 +1,196 @@
@import "~normalize.css/normalize.css";
@import "./fonts.css";
@import "./base.css";
@import "./header.css";
@import "./prompts.css";
@import "./listing.css";
@import "./editor.css";
/* * * * * * * * * * * * * * * *
* ACTION *
* * * * * * * * * * * * * * * */
.action {
display: inline-block;
cursor: pointer;
transition: 0.2s ease all;
border: 0;
margin: 0;
color: #546E7A;
border-radius: 50%;
background: transparent;
padding: 0;
box-shadow: 0 0 0 0;
vertical-align: middle;
text-align: left;
position: relative;
}
.action.disabled {
opacity: 0.2;
cursor: not-allowed;
}
.action i {
padding: 0.4em;
-webkit-transition: 0.2s ease-in-out all;
transition: 0.2s ease-in-out all;
border-radius: 50%;
}
.action:hover {
background-color: rgba(0, 0, 0, .1);
}
.action ul {
position: absolute;
top: 0;
color: #7d7d7d;
list-style: none;
margin: 0;
padding: 0;
flex-direction: column;
display: flex;
}
.action ul li {
line-height: 1;
padding: .7em;
transition: .1s ease background-color;
}
.action ul li:hover {
background-color: rgba(0, 0, 0, 0.04);
}
#click-overlay {
display: none;
position: fixed;
cursor: pointer;
top: 0;
left: 0;
height: 100%;
width: 100%;
}
#click-overlay.active {
display: block;
}
.action .counter {
display: block;
position: absolute;
bottom: 0;
right: 0;
background: #2196f3;
color: #fff;
border-radius: 50%;
font-size: .75em;
width: 1.5em;
height: 1.5em;
text-align: center;
line-height: 1.25em;
border: 2px solid white;
}
/* PREVIEWER */
#previewer {
background-color: rgba(0, 0, 0, 0.9);
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 9999;
overflow: hidden;
}
#previewer .bar {
width: 100%;
text-align: right;
display: flex;
padding: 0.5em 0.5em 0.5em 1em;
height: 3.7em;
}
#previewer .action:first-of-type {
margin-right: auto;
}
#previewer .action i {
color: #fff;
}
#previewer .action:hover {
background-color: rgba(255, 255, 255, 0.3)
}
#previewer .action span {
display: none;
}
#previewer .preview {
margin: 2em auto 4em;
max-width: 80%;
text-align: center;
height: calc(100vh - 9.7em);
}
#previewer .preview pre {
text-align: left;
overflow: auto;
}
#previewer .preview pre,
#previewer .preview video,
#previewer .preview img {
max-height: 100%;
margin: 0;
}
#previewer .pdf {
width: 100%;
height: 100%;
}
#previewer h2.message {
color: rgba(255, 255, 255, 0.5)
}
/* * * * * * * * * * * * * * * *
* PROMPT *
* * * * * * * * * * * * * * * */
/* * * * * * * * * * * * * * * *
* FOOTER *
* * * * * * * * * * * * * * * */
.credits {
font-size: 0.6em;
margin: 3em 2.5em;
color: #a5a5a5;
}
.credits a,
.credits a:hover {
color: inherit;
cursor: pointer;
}
/* * * * * * * * * * * * * * * *
* ANIMATIONS *
* * * * * * * * * * * * * * * */
@keyframes spin {
100% {
-webkit-transform: rotate(-360deg);
transform: rotate(-360deg);
}
}
@import './mobile.css';

15
assets/src/main.js Normal file
View File

@@ -0,0 +1,15 @@
import Vue from 'vue'
import App from './App'
import store from './store/store'
import router from './router'
Vue.config.productionTip = true
/* eslint-disable no-new */
new Vue({
el: '#app',
store,
router,
template: '<App/>',
components: { App }
})

View File

@@ -0,0 +1,81 @@
import Vue from 'vue'
import Router from 'vue-router'
import Login from '@/components/Login'
import Main from '@/components/Main'
import auth from '@/utils/auth.js'
Vue.use(Router)
const router = new Router({
base: document.querySelector('meta[name="base"]').getAttribute('content'),
mode: 'history',
routes: [
{
path: '/login',
name: 'Login',
component: Login,
beforeEnter: function (to, from, next) {
auth.loggedIn()
.then(() => {
next({ path: '/files' })
})
.catch(() => {
document.title = 'Login'
next()
})
}
},
{
path: '/',
redirect: {
path: '/files/'
}
},
{
path: '/*',
component: Main,
meta: {
requiresAuth: true
},
children: [
{
path: '/files/*',
name: 'Files'
},
{
path: '/dashboard',
name: 'Dashboard'
},
{
path: '/*',
redirect: {
name: 'Files'
}
}
]
}
]
})
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requiresAuth)) {
// this route requires auth, check if logged in
// if not, redirect to login page.
auth.loggedIn()
.then(() => {
next()
})
.catch(e => {
next({
path: '/login',
query: { redirect: to.fullPath }
})
})
return
}
next()
})
export default router

View File

@@ -0,0 +1,5 @@
const getters = {
selectedCount: state => state.selected.length
}
export default getters

View File

@@ -0,0 +1,25 @@
const mutations = {
closeHovers: state => { state.show = null },
showHover: (state, value) => { state.show = value },
setReload: (state, value) => { state.reload = value },
setUser: (state, value) => (state.user = value),
setJWT: (state, value) => (state.jwt = value),
multiple: (state, value) => (state.multiple = value),
addSelected: (state, value) => (state.selected.push(value)),
removeSelected: (state, value) => {
let i = state.selected.indexOf(value)
if (i === -1) return
state.selected.splice(i, 1)
},
resetSelected: (state) => {
state.selected = []
},
listingDisplay: (state, value) => {
state.req.display = value
},
updateRequest: (state, value) => {
state.req = value
}
}
export default mutations

24
assets/src/store/store.js Normal file
View File

@@ -0,0 +1,24 @@
import Vue from 'vue'
import Vuex from 'vuex'
import mutations from './mutations'
import getters from './getters'
Vue.use(Vuex)
const state = {
user: {},
req: {},
baseURL: document.querySelector('meta[name="base"]').getAttribute('content'),
jwt: '',
reload: false,
selected: [],
multiple: false,
show: null
}
export default new Vuex.Store({
strict: process.env.NODE_ENV !== 'production',
state,
getters,
mutations
})

203
assets/src/utils/api.js Normal file
View File

@@ -0,0 +1,203 @@
import store from '../store/store'
const ssl = (window.location.protocol === 'https:')
function removePrefix (url) {
if (url.startsWith('/files')) {
return url.slice(6)
}
return url
}
function fetch (url) {
url = removePrefix(url)
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('GET', `${store.state.baseURL}/api/resource${url}`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => {
switch (request.status) {
case 200:
let req = JSON.parse(request.responseText)
store.commit('updateRequest', req)
document.title = req.name
resolve(req.url)
break
default:
reject(request.status)
break
}
}
request.onerror = (error) => reject(error)
request.send()
})
}
function rm (url) {
url = removePrefix(url)
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('DELETE', `${store.state.baseURL}/api/resource${url}`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => {
if (request.status === 200) {
resolve(request.responseText)
} else {
reject(request.responseText)
}
}
request.onerror = (error) => reject(error)
request.send()
})
}
function post (url, content = '') {
url = removePrefix(url)
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('POST', `${store.state.baseURL}/api/resource${url}`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => {
if (request.status === 200) {
resolve(request.responseText)
} else {
reject(request.responseText)
}
}
request.onerror = (error) => reject(error)
request.send(content)
})
}
function put (url, content = '') {
url = removePrefix(url)
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('PUT', `${store.state.baseURL}/api/resource${url}`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => {
if (request.status === 200) {
resolve(request.responseText)
} else {
reject(request.responseText)
}
}
request.onerror = (error) => reject(error)
request.send(content)
})
}
function move (oldLink, newLink) {
oldLink = removePrefix(oldLink)
newLink = removePrefix(newLink)
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('POST', `${store.state.baseURL}/api/resource${oldLink}`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.setRequestHeader('Destination', newLink)
request.onload = () => {
if (request.status === 200) {
resolve(request.responseText)
} else {
reject(request.responseText)
}
}
request.onerror = (error) => reject(error)
request.send()
})
}
function checksum (url, algo) {
url = removePrefix(url)
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('GET', `${store.state.baseURL}/api/checksum${url}?algo=${algo}`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => {
if (request.status === 200) {
resolve(request.responseText)
} else {
reject(request.responseText)
}
}
request.onerror = (error) => reject(error)
request.send()
})
}
function command (url, command, onmessage, onclose) {
let protocol = (ssl ? 'wss:' : 'ws:')
url = removePrefix(url)
url = `${protocol}//${window.location.hostname}${store.state.baseURL}/api/command${url}?token=${store.state.jwt}`
let conn = new window.WebSocket(url)
conn.onopen = () => conn.send(command)
conn.onmessage = onmessage
conn.onclose = onclose
}
function search (url, search, onmessage, onclose) {
let protocol = (ssl ? 'wss:' : 'ws:')
url = removePrefix(url)
url = `${protocol}//${window.location.hostname}${store.state.baseURL}/api/search${url}?token=${store.state.jwt}`
let conn = new window.WebSocket(url)
conn.onopen = () => conn.send(search)
conn.onmessage = onmessage
conn.onclose = onclose
}
function download (format, ...files) {
let url = `${store.state.baseURL}/api/download`
if (files.length === 1) {
url += removePrefix(files[0]) + '?'
} else {
let arg = ''
for (let file of files) {
arg += removePrefix(file) + ','
}
arg = arg.substring(0, arg.length - 1)
arg = encodeURIComponent(arg)
url += `/?files=${arg}&`
}
url += `token=${store.state.jwt}`
if (format !== null) {
url += `&format=${format}`
}
window.open(url)
}
export default {
delete: rm,
fetch,
checksum,
move,
put,
post,
command,
search,
download
}

60
assets/src/utils/auth.js Normal file
View File

@@ -0,0 +1,60 @@
import cookie from './cookie'
import store from '@/store/store'
import router from '@/router'
function parseToken (token) {
document.cookie = `auth=${token}; max-age=86400; path=${store.state.baseURL}`
let res = token.split('.')
let user = JSON.parse(window.atob(res[1]))
store.commit('setJWT', token)
store.commit('setUser', user)
}
function loggedIn () {
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('GET', `${store.state.baseURL}/api/auth/renew`, true)
request.setRequestHeader('Authorization', `Bearer ${cookie('auth')}`)
request.onload = () => {
if (request.status === 200) {
parseToken(request.responseText)
resolve()
} else {
reject()
}
}
request.onerror = () => reject()
request.send()
})
}
function login (user, password) {
let data = {username: user, password: password}
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('POST', `${store.state.baseURL}/api/auth/get`, true)
request.onload = () => {
if (request.status === 200) {
parseToken(request.responseText)
resolve()
} else {
reject(request.responseText)
}
}
request.onerror = () => reject()
request.send(JSON.stringify(data))
})
}
function logout () {
document.cookie = `auth='nothing'; max-age=0; path=${store.state.baseURL}`
router.push({path: '/login'})
}
export default {
loggedIn: loggedIn,
login: login,
logout: logout
}

View File

@@ -0,0 +1,39 @@
function loading (button) {
let el = document.querySelector(`#${button}-button > i`)
if (el === undefined || el === null) {
console.log('Error getting button ' + button)
return
}
el.dataset.icon = el.innerHTML
el.style.opacity = 0
setTimeout(() => {
el.classList.add('spin')
el.innerHTML = 'autorenew'
el.style.opacity = 1
}, 200)
}
function done (button, success = true) {
let el = document.querySelector(`#${button}-button > i`)
if (el === undefined || el === null) {
console.log('Error getting button ' + button)
return
}
el.style.opacity = 0
setTimeout(() => {
el.classList.remove('spin')
el.innerHTML = el.dataset.icon
el.style.opacity = 1
}, 200)
}
export default {
loading,
done
}

View File

@@ -0,0 +1,4 @@
export default function (name) {
let re = new RegExp('(?:(?:^|.*;\\s*)' + name + '\\s*\\=\\s*([^;]*).*$)|^.*$')
return document.cookie.replace(re, '$1')
}

28
assets/src/utils/css.js Normal file
View File

@@ -0,0 +1,28 @@
export default function getRule (rules) {
for (let i = 0; i < rules.length; i++) {
rules[i] = rules[i].toLowerCase()
}
let result = null
let find = Array.prototype.find
find.call(document.styleSheets, styleSheet => {
result = find.call(styleSheet.cssRules, cssRule => {
let found = false
if (cssRule instanceof window.CSSStyleRule) {
for (let i = 0; i < rules.length; i++) {
if (cssRule.selectorText.toLowerCase() === rules[i]) {
found = true
}
}
}
return found
})
return result != null
})
return result
}

12
assets/src/utils/url.js Normal file
View File

@@ -0,0 +1,12 @@
function removeLastDir (url) {
var arr = url.split('/')
if (arr.pop() === '') {
arr.pop()
}
return arr.join('/')
}
export default {
removeLastDir: removeLastDir
}