mirror of
https://github.com/filegator/filegator.git
synced 2025-08-11 04:04:01 +02:00
initial commit
This commit is contained in:
568
frontend/views/Browser.vue
Normal file
568
frontend/views/Browser.vue
Normal file
@@ -0,0 +1,568 @@
|
||||
<template>
|
||||
<div id="dropzone" class="container"
|
||||
@dragover="dropZone = can('upload') && ! isLoading ? true : false"
|
||||
@dragleave="dropZone = false"
|
||||
@drop="dropZone = false">
|
||||
|
||||
<div id="loading" v-if="isLoading"></div>
|
||||
|
||||
<Upload v-if="can('upload')" v-show="dropZone == false" :files="files" :dropZone="dropZone"></Upload>
|
||||
|
||||
<b-upload v-if="dropZone && ! isLoading" @input="files = $event" multiple drag-drop>
|
||||
<b class="drop-info">{{ lang('Drop files to upload') }}</b>
|
||||
</b-upload>
|
||||
|
||||
<div class="container" v-if="!dropZone">
|
||||
|
||||
<Menu></Menu>
|
||||
|
||||
<div id="browser">
|
||||
|
||||
<div v-if="can('read')" class="is-flex is-justify-between">
|
||||
<div class="breadcrumb" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li v-for="(item, key) in breadcrumbs" v-if="item.name">
|
||||
<a @click="goTo(item.path)">{{ item.name }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<a class="is-paddingless" @click="selectDir">
|
||||
<b-icon icon="sitemap" class="is-marginless" size="is-small"></b-icon>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="actions is-flex is-justify-between">
|
||||
<div>
|
||||
<b-field v-if="can('upload') && ! checked.length" class="file is-inline-block">
|
||||
<b-upload @input="files = $event" multiple native>
|
||||
<a v-if="! checked.length" class="is-inline-block">
|
||||
<b-icon icon="upload" size="is-small"></b-icon> {{ lang('Add files') }}
|
||||
</a>
|
||||
</a>
|
||||
</b-upload>
|
||||
</b-field>
|
||||
<a v-if="can(['read', 'write']) && ! checked.length" class="is-inline-block">
|
||||
<b-dropdown aria-role="list" :disabled="checked.length > 0">
|
||||
<span slot="trigger">
|
||||
<b-icon icon="plus" size="is-small"></b-icon> {{ lang('New') }}
|
||||
</span>
|
||||
|
||||
<b-dropdown-item @click="create('dir')" aria-role="listitem">
|
||||
<b-icon icon="folder" size="is-small"></b-icon> {{ lang('Folder') }}
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item @click="create('file')" aria-role="listitem">
|
||||
<b-icon icon="file" size="is-small"></b-icon> {{ lang('File') }}
|
||||
</b-dropdown-item>
|
||||
|
||||
</b-dropdown>
|
||||
</a>
|
||||
<a v-if="can('batchdownload') && checked.length" @click="batchDownload" class="is-inline-block">
|
||||
<b-icon icon="download" size="is-small"></b-icon> {{ lang('Download') }}
|
||||
</a>
|
||||
<a v-if="can('write') && checked.length" @click="copy" class="is-inline-block">
|
||||
<b-icon icon="copy" size="is-small"></b-icon> {{ lang('Copy') }}
|
||||
</a>
|
||||
<a v-if="can('write') && checked.length" @click="move" class="is-inline-block">
|
||||
<b-icon icon="external-link-square-alt" size="is-small"></b-icon> {{ lang('Move') }}
|
||||
</a>
|
||||
<a v-if="can(['write', 'zip']) && checked.length" @click="zip" class="is-inline-block">
|
||||
<b-icon icon="file-archive" size="is-small"></b-icon> {{ lang('Zip') }}
|
||||
</a>
|
||||
<a v-if="can('write') && checked.length" @click="remove" class="is-inline-block">
|
||||
<b-icon icon="trash-alt" size="is-small"></b-icon> {{ lang('Delete') }}
|
||||
</a>
|
||||
</div>
|
||||
<div v-if="can('read')">
|
||||
<Pagination :perpage="perPage" @selected="perPage = $event"></Pagination>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<b-table v-if="can('read')"
|
||||
:data="content"
|
||||
:default-sort="defaultSort"
|
||||
:paginated="perPage > 0"
|
||||
:per-page="perPage"
|
||||
:current-page.sync="currentPage"
|
||||
:hoverable="true"
|
||||
:is-row-checkable="(row) => row.type != 'back'"
|
||||
:row-class="(row) => 'file-row type-'+row.type"
|
||||
:checked-rows.sync="checked"
|
||||
:loading="isLoading"
|
||||
checkable>
|
||||
<template slot-scope="props">
|
||||
|
||||
<b-table-column field="data.name" :label="lang('Name')" :custom-sort="sortByName" sortable>
|
||||
<a @click="itemClick(props.row)" class="is-block name">
|
||||
{{ props.row.name }}
|
||||
</a>
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column field="data.size" :label="lang('Size')" :custom-sort="sortBySize" sortable numeric width="150">
|
||||
{{ props.row.type == 'back' || props.row.type == 'dir' ? lang('Folder') : formatBytes(props.row.size) }}
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column field="data.time" :label="lang('Time')" :custom-sort="sortByTime" sortable numeric width="200">
|
||||
{{ props.row.time ? formatDate(props.row.time) : '' }}
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column class="action-padding" width="51">
|
||||
<b-dropdown v-if="props.row.type != 'back'" aria-role="list" position="is-bottom-left" :disabled="checked.length > 0">
|
||||
<button class="button is-small" slot="trigger">
|
||||
<b-icon icon="ellipsis-h" size="is-small"></b-icon>
|
||||
</button>
|
||||
|
||||
<b-dropdown-item v-if="props.row.type == 'file' && can('download')" @click="download(props.row)" aria-role="listitem">
|
||||
<b-icon icon="download" size="is-small"></b-icon> {{ lang('Download') }}
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item v-if="can('write')" @click="copy($event, props.row)" aria-role="listitem">
|
||||
<b-icon icon="copy" size="is-small"></b-icon> {{ lang('Copy') }}
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item v-if="can('write')" @click="move($event, props.row)" aria-role="listitem">
|
||||
<b-icon icon="external-link-square-alt" size="is-small"></b-icon> {{ lang('Move') }}
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item v-if="can('write')" @click="rename($event, props.row)" aria-role="listitem">
|
||||
<b-icon icon="file-signature" size="is-small"></b-icon> {{ lang('Rename') }}
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item v-if="can(['write', 'zip']) && isArchive(props.row)" @click="unzip($event, props.row)" aria-role="listitem">
|
||||
<b-icon icon="file-archive" size="is-small"></b-icon> {{ lang('Unzip') }}
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item v-if="can(['write', 'zip']) && ! isArchive(props.row)" @click="zip($event, props.row)" aria-role="listitem">
|
||||
<b-icon icon="file-archive" size="is-small"></b-icon> {{ lang('Zip') }}
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item v-if="can('write')" @click="remove($event, props.row)" aria-role="listitem">
|
||||
<b-icon icon="trash-alt" size="is-small"></b-icon> {{ lang('Delete') }}
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item v-if="props.row.type == 'file' && can('download')" v-clipboard:copy="getDownloadLink(props.row)" aria-role="listitem">
|
||||
<b-icon icon="clipboard" size="is-small"></b-icon> {{ lang('Copy link') }}
|
||||
</b-dropdown-item>
|
||||
|
||||
</b-dropdown>
|
||||
</b-table-column>
|
||||
|
||||
</template>
|
||||
|
||||
<template slot="bottom-left">
|
||||
<span>{{ lang('Selected', checked.length, totalCount) }}</span>
|
||||
</template>
|
||||
|
||||
</b-table>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from 'vue'
|
||||
import Menu from './partials/Menu'
|
||||
import Tree from './partials/Tree'
|
||||
import Pagination from './partials/Pagination'
|
||||
import Upload from './partials/Upload'
|
||||
import api from '../api/api'
|
||||
import VueClipboard from 'vue-clipboard2'
|
||||
|
||||
Vue.use(VueClipboard)
|
||||
|
||||
export default {
|
||||
name: 'Browser',
|
||||
components: { Menu, Tree, Pagination, Upload },
|
||||
data() {
|
||||
return {
|
||||
dropZone: false,
|
||||
perPage: "",
|
||||
currentPage: 1,
|
||||
checked: [],
|
||||
isLoading: false,
|
||||
defaultSort: ['data.name', 'desc'],
|
||||
files: [],
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.can('read')) {
|
||||
this.loadFiles()
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'$route' (to, from) {
|
||||
this.isLoading = true
|
||||
this.checked = []
|
||||
this.currentPage = 1
|
||||
api.changeDir({
|
||||
to: to.query.cd
|
||||
})
|
||||
.then(ret => {
|
||||
this.$store.commit('setCwd', {
|
||||
content: ret.files,
|
||||
location: ret.location,
|
||||
})
|
||||
this.isLoading = false
|
||||
})
|
||||
.catch(error => {
|
||||
this.isLoading = false
|
||||
this.handleError(error)
|
||||
})
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
breadcrumbs() {
|
||||
let path = ''
|
||||
let breadcrumbs = [{name: this.lang('Home'), path: '/'}]
|
||||
|
||||
_.forEach(_.split(this.$store.state.cwd.location, '/'), (dir) => {
|
||||
path += dir + '/'
|
||||
breadcrumbs.push({
|
||||
name: dir,
|
||||
path: path,
|
||||
})
|
||||
})
|
||||
|
||||
return breadcrumbs
|
||||
},
|
||||
content() {
|
||||
return this.$store.state.cwd.content
|
||||
},
|
||||
totalCount() {
|
||||
return _.sumBy(this.$store.state.cwd.content, (o) => {
|
||||
return o.type == 'file' || o.type == 'dir'
|
||||
}) || 0
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
loadFiles() {
|
||||
api.getDir({
|
||||
to: '',
|
||||
})
|
||||
.then(ret => {
|
||||
this.$store.commit('setCwd', {
|
||||
content: ret.files,
|
||||
location: ret.location,
|
||||
})
|
||||
})
|
||||
.catch(error => this.handleError(error))
|
||||
},
|
||||
goTo(path) {
|
||||
this.$router.push({ name: 'browser', query: { 'cd': path }})
|
||||
},
|
||||
getSelected() {
|
||||
return _.reduce(this.checked, function(result, value, key) {
|
||||
result.push(value)
|
||||
return result
|
||||
}, [])
|
||||
},
|
||||
itemClick(item) {
|
||||
if (item.type == 'dir' || item.type == 'back') {
|
||||
this.goTo(item.path)
|
||||
} else {
|
||||
this.download(item)
|
||||
}
|
||||
},
|
||||
selectDir() {
|
||||
this.$modal.open({
|
||||
parent: this,
|
||||
hasModalCard: true,
|
||||
component: Tree,
|
||||
events: {
|
||||
selected: dir => {
|
||||
this.goTo(dir.path)
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
copy(event, item) {
|
||||
this.$modal.open({
|
||||
parent: this,
|
||||
hasModalCard: true,
|
||||
component: Tree,
|
||||
events: {
|
||||
selected: dir => {
|
||||
this.isLoading = true
|
||||
api.copyItems({
|
||||
destination: dir.path,
|
||||
items: item ? [item] : this.getSelected(),
|
||||
})
|
||||
.then(res => {
|
||||
this.isLoading = false
|
||||
})
|
||||
.catch(error => {
|
||||
this.isLoading = false
|
||||
this.handleError(error)
|
||||
})
|
||||
this.checked = []
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
move(event, item) {
|
||||
this.$modal.open({
|
||||
parent: this,
|
||||
hasModalCard: true,
|
||||
component: Tree,
|
||||
events: {
|
||||
selected: dir => {
|
||||
this.isLoading = true
|
||||
api.moveItems({
|
||||
destination: dir.path,
|
||||
items: item ? [item] : this.getSelected(),
|
||||
})
|
||||
.then(res => {
|
||||
this.isLoading = false
|
||||
})
|
||||
.catch(error => {
|
||||
this.isLoading = false
|
||||
this.handleError(error)
|
||||
})
|
||||
this.checked = []
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
batchDownload() {
|
||||
let items = this.getSelected()
|
||||
|
||||
this.isLoading = true
|
||||
api.batchDownload({
|
||||
items: items,
|
||||
})
|
||||
.then(ret => {
|
||||
this.isLoading = false
|
||||
this.$dialog.alert({
|
||||
message: this.lang('Your file is ready'),
|
||||
confirmText: this.lang('Download'),
|
||||
onConfirm: () => {
|
||||
window.open(Vue.config.baseURL+'/batchdownload&uniqid='+ret.uniqid, '_blank')
|
||||
}
|
||||
})
|
||||
})
|
||||
.catch(error => {
|
||||
this.isLoading = false
|
||||
this.handleError(error)
|
||||
})
|
||||
},
|
||||
getDownloadLink(item) {
|
||||
return Vue.config.baseURL+'/download/'+btoa(item.path);
|
||||
},
|
||||
download(item) {
|
||||
window.open(this.getDownloadLink(item), '_blank')
|
||||
},
|
||||
search() {
|
||||
// TODO: create search logic
|
||||
},
|
||||
edit() {
|
||||
// TODO: create edit file logic
|
||||
},
|
||||
isArchive(item) {
|
||||
return item.type == 'file' && item.name.split('.').pop() == 'zip'
|
||||
},
|
||||
unzip(event, item) {
|
||||
this.$dialog.confirm({
|
||||
message: this.lang('Are you sure you want to do this?'),
|
||||
type: 'is-danger',
|
||||
cancelText: this.lang('Cancel'),
|
||||
confirmText: this.lang('Unzip'),
|
||||
onConfirm: () => {
|
||||
this.isLoading = true
|
||||
api.unzipItem({
|
||||
item: item.path,
|
||||
destination: this.$store.state.cwd.location,
|
||||
})
|
||||
.then(res => {
|
||||
this.isLoading = false
|
||||
this.loadFiles()
|
||||
})
|
||||
.catch(error => {
|
||||
this.isLoading = false
|
||||
this.handleError(error)
|
||||
})
|
||||
this.checked = []
|
||||
}
|
||||
})
|
||||
},
|
||||
zip(event, item) {
|
||||
this.$dialog.prompt({
|
||||
message: this.lang('Name'),
|
||||
cancelText: this.lang('Cancel'),
|
||||
confirmText: this.lang('Create'),
|
||||
inputAttrs: {
|
||||
value: this.$store.state.config.default_archive_name,
|
||||
placeholder: this.$store.state.config.default_archive_name,
|
||||
maxlength: 100,
|
||||
required: false,
|
||||
},
|
||||
onConfirm: (value) => {
|
||||
if (! value) {
|
||||
return;
|
||||
}
|
||||
this.isLoading = true
|
||||
api.zipItems({
|
||||
name: value,
|
||||
items: item ? [item] : this.getSelected(),
|
||||
destination: this.$store.state.cwd.location,
|
||||
})
|
||||
.then(ret => {
|
||||
this.isLoading = false
|
||||
this.loadFiles()
|
||||
})
|
||||
.catch(error => {
|
||||
this.isLoading = false
|
||||
this.handleError(error)
|
||||
})
|
||||
this.checked = []
|
||||
}
|
||||
})
|
||||
},
|
||||
rename(event, item) {
|
||||
this.$dialog.prompt({
|
||||
message: this.lang('New name'),
|
||||
cancelText: this.lang('Cancel'),
|
||||
confirmText: this.lang('Rename'),
|
||||
inputAttrs: {
|
||||
value: item ? item.name : this.getSelected()[0].name,
|
||||
maxlength: 100,
|
||||
required: false,
|
||||
},
|
||||
onConfirm: (value) => {
|
||||
this.isLoading = true
|
||||
api.renameItem({
|
||||
from: item.name,
|
||||
to: value,
|
||||
destination: this.$store.state.cwd.location,
|
||||
})
|
||||
.then(res => {
|
||||
this.isLoading = false
|
||||
this.loadFiles()
|
||||
})
|
||||
.catch(error => {
|
||||
this.isLoading = false
|
||||
this.handleError(error)
|
||||
})
|
||||
this.checked = []
|
||||
}
|
||||
})
|
||||
},
|
||||
create(type) {
|
||||
this.$dialog.prompt({
|
||||
cancelText: this.lang('Cancel'),
|
||||
confirmText: this.lang('Create'),
|
||||
inputAttrs: {
|
||||
placeholder: type == 'dir' ? 'MyFolder' : 'file.txt',
|
||||
maxlength: 100,
|
||||
required: false,
|
||||
},
|
||||
onConfirm: (value) => {
|
||||
this.isLoading = true
|
||||
api.createNew({
|
||||
type: type,
|
||||
name: value,
|
||||
destination: this.$store.state.cwd.location,
|
||||
})
|
||||
// TODO: cors is triggering this too early?
|
||||
.then(ret => {
|
||||
this.isLoading = false
|
||||
this.loadFiles()
|
||||
})
|
||||
.catch(error => {
|
||||
this.isLoading = false
|
||||
this.handleError(error)
|
||||
})
|
||||
this.checked = []
|
||||
}
|
||||
})
|
||||
},
|
||||
remove(event, item) {
|
||||
this.$dialog.confirm({
|
||||
message: this.lang('Are you sure you want to do this?'),
|
||||
type: 'is-danger',
|
||||
cancelText: this.lang('Cancel'),
|
||||
confirmText: this.lang('Delete'),
|
||||
onConfirm: () => {
|
||||
this.isLoading = true
|
||||
api.removeItems({
|
||||
items: item ? [item] : this.getSelected(),
|
||||
})
|
||||
.then(ret => {
|
||||
this.isLoading = false
|
||||
this.loadFiles()
|
||||
})
|
||||
.catch(error => {
|
||||
this.isLoading = false
|
||||
this.handleError(error)
|
||||
})
|
||||
this.checked = []
|
||||
}
|
||||
})
|
||||
},
|
||||
sortByName(a, b, isAsc) {
|
||||
return this.customSort(a, b, isAsc, 'name')
|
||||
},
|
||||
sortBySize(a, b, isAsc) {
|
||||
return this.customSort(a, b, isAsc, 'size')
|
||||
},
|
||||
sortByTime(a, b, isAsc) {
|
||||
return this.customSort(a, b, isAsc, 'time')
|
||||
},
|
||||
customSort(a, b, isAsc, param) {
|
||||
// TODO: firefox is broken
|
||||
if (b.type == 'back') return 1
|
||||
if (a.type == 'back') return -1
|
||||
if (b.type == 'dir' && a.type == 'dir') {
|
||||
return (a[param] < b[param]) || isAsc ? -1 : 1
|
||||
} else if (b.type == 'dir') {
|
||||
return 1
|
||||
} else if (a.type == 'dir') {
|
||||
return -1
|
||||
}
|
||||
if (_.isString(a[param])) {
|
||||
return (_.lowerCase(a[param]) < _.lowerCase(b[param])) || isAsc ? -1 : 1
|
||||
} else {
|
||||
return (a[param] < b[param]) || isAsc ? -1 : 1
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#loading {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
top: 0;
|
||||
left: 0;
|
||||
user-drag: none;
|
||||
user-select: none;
|
||||
-moz-user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
#dropzone {
|
||||
padding: 0;
|
||||
}
|
||||
#browser {
|
||||
margin: 50px auto 100px auto;
|
||||
}
|
||||
.breadcrumb a {
|
||||
font-weight: bold;
|
||||
}
|
||||
.actions {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.actions a {
|
||||
margin-right: 15px;
|
||||
}
|
||||
.file-row a {
|
||||
color: #373737;
|
||||
}
|
||||
.file-row.type-dir a.name {
|
||||
font-weight: bold
|
||||
}
|
||||
.action-padding {
|
||||
padding: 6px 12px;
|
||||
}
|
||||
.drop-info {
|
||||
margin: 20% auto;
|
||||
}
|
||||
</style>
|
Reference in New Issue
Block a user