Merge pull request #1026 from ramiresviana/fixes

This commit is contained in:
Oleg Lobanov 2020-07-17 17:41:17 +02:00 committed by GitHub
commit 1790df2090
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 489 additions and 144 deletions

View File

@ -2,6 +2,7 @@ package fileutils
import ( import (
"io" "io"
"os"
"path/filepath" "path/filepath"
"github.com/spf13/afero" "github.com/spf13/afero"
@ -25,7 +26,7 @@ func CopyFile(fs afero.Fs, source, dest string) error {
} }
// Create the destination file. // Create the destination file.
dst, err := fs.Create(dest) dst, err := fs.OpenFile(dest, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0775)
if err != nil { if err != nil {
return err return err
} }

View File

@ -1,7 +1,7 @@
:root { :root {
--background: #121212; --background: #141D24;
--surfacePrimary: #171819; --surfacePrimary: #20292F;
--surfaceSecondary: #212528; --surfaceSecondary: #3A4147;
--divider: rgba(255, 255, 255, 0.12); --divider: rgba(255, 255, 255, 0.12);
--icon: #ffffff; --icon: #ffffff;
--textPrimary: rgba(255, 255, 255, 0.87); --textPrimary: rgba(255, 255, 255, 0.87);
@ -16,7 +16,7 @@ body {
#loading { #loading {
background: var(--background); background: var(--background);
} }
#loading .spinner div { #loading .spinner div, #previewer .loading .spinner div {
background: var(--icon); background: var(--icon);
} }
@ -30,25 +30,34 @@ header {
#search #input { #search #input {
background: var(--surfaceSecondary); background: var(--surfaceSecondary);
border-color: var(--surfacePrimary);
} }
#search.active #input, #search #input input::placeholder {
#search.active .boxes { color: var(--textSecondary);
}
#search.active #input {
background: var(--surfacePrimary); background: var(--surfacePrimary);
} }
#search.active input { #search.active input {
color: var(--textPrimary); color: var(--textPrimary);
} }
#search.active #result { #search #result {
background: var(--background); background: var(--background);
color: var(--textPrimary); color: var(--textPrimary);
} }
#search.active .boxes h3 { #search .boxes {
background: var(--surfaceSecondary);
}
#search .boxes h3 {
color: var(--textPrimary); color: var(--textPrimary);
} }
.action { .action {
color: var(--textPrimary) !important; color: var(--textPrimary) !important;
} }
.action:hover {
background-color: rgba(255, 255, 255, .1);
}
.action i { .action i {
color: var(--icon) !important; color: var(--icon) !important;
} }
@ -93,6 +102,10 @@ nav > div {
background: var(--background); background: var(--background);
} }
.message {
color: var(--textPrimary);
}
.card { .card {
background: var(--surfacePrimary); background: var(--surfacePrimary);
color: var(--textPrimary); color: var(--textPrimary);
@ -106,9 +119,23 @@ nav > div {
.dashboard p label { .dashboard p label {
color: var(--textPrimary); color: var(--textPrimary);
} }
.card#share ul li input,
.card#share ul li select,
.input { .input {
background: var(--surfaceSecondary); background: var(--surfaceSecondary);
color: var(--textPrimary); color: var(--textPrimary);
border: 1px solid rgba(255, 255, 255, 0.05);
}
.input:hover,
.input:focus {
border-color: rgba(255, 255, 255, 0.15);
}
.input--red {
background: #73302D;
}
.input--green {
background: #147A41;
} }
.dashboard #nav li, .dashboard #nav li,
@ -119,10 +146,27 @@ nav > div {
color: var(--textPrimary); color: var(--textPrimary);
} }
table th {
color: var(--textSecondary);
}
.file-list li:hover {
background: var(--surfaceSecondary);
}
.file-list li:before {
color: var(--textSecondary);
}
.file-list li[aria-selected=true]:before {
color: var(--icon);
}
.shell { .shell {
background: var(--surfacePrimary); background: var(--surfacePrimary);
color: var(--textPrimary); color: var(--textPrimary);
} }
.shell__result {
border-top: 1px solid var(--divider);
}
#editor-container { #editor-container {
background: var(--background); background: var(--background);
@ -146,3 +190,11 @@ nav > div {
background: var(--surfaceSecondary) !important; background: var(--surfaceSecondary) !important;
} }
} }
.share__box, .share__box__download {
background: var(--surfaceSecondary) !important;
color: var(--textPrimary);
}
.share__box__download {
border-bottom-color: var(--divider);
}

View File

@ -94,9 +94,6 @@ export async function post (url, content = '', overwrite = false, onupload) {
request.upload.onprogress = onupload request.upload.onprogress = onupload
} }
// Send a message to user before closing the tab during file upload
window.onbeforeunload = () => "Files are being uploaded."
request.onload = () => { request.onload = () => {
if (request.status === 200) { if (request.status === 200) {
resolve(request.responseText) resolve(request.responseText)
@ -112,29 +109,28 @@ export async function post (url, content = '', overwrite = false, onupload) {
} }
request.send(content) request.send(content)
// Upload is done no more message before closing the tab })
}).finally(() => { window.onbeforeunload = null })
} }
function moveCopy (items, copy = false) { function moveCopy (items, copy = false, overwrite = false, rename = false) {
let promises = [] let promises = []
for (let item of items) { for (let item of items) {
const from = removePrefix(item.from) const from = removePrefix(item.from)
const to = encodeURIComponent(removePrefix(item.to)) const to = encodeURIComponent(removePrefix(item.to))
const url = `${from}?action=${copy ? 'copy' : 'rename'}&destination=${to}` const url = `${from}?action=${copy ? 'copy' : 'rename'}&destination=${to}&override=${overwrite}&rename=${rename}`
promises.push(resourceAction(url, 'PATCH')) promises.push(resourceAction(url, 'PATCH'))
} }
return Promise.all(promises) return Promise.all(promises)
} }
export function move (items) { export function move (items, overwrite = false, rename = false) {
return moveCopy(items) return moveCopy(items, false, overwrite, rename)
} }
export function copy (items) { export function copy (items, overwrite = false, rename = false) {
return moveCopy(items, true) return moveCopy(items, true, overwrite, rename)
} }
export async function checksum (url, algo) { export async function checksum (url, algo) {

View File

@ -10,10 +10,12 @@
@mouseup="mouseUp" @mouseup="mouseUp"
@wheel="wheelMove" @wheel="wheelMove"
> >
<img :src="src" class="image-ex-img" ref="imgex" @load="setCenter"> <img :src="src" class="image-ex-img image-ex-img-center" ref="imgex" @load="onLoad">
</div> </div>
</template> </template>
<script> <script>
import throttle from 'lodash.throttle'
export default { export default {
props: { props: {
src: String, src: String,
@ -50,7 +52,12 @@ export default {
inDrag: false, inDrag: false,
lastTouchDistance: 0, lastTouchDistance: 0,
moveDisabled: false, moveDisabled: false,
disabledTimer: null disabledTimer: null,
imageLoaded: false,
position: {
center: { x: 0, y: 0 },
relative: { x: 0, y: 0 }
}
} }
}, },
mounted() { mounted() {
@ -63,24 +70,47 @@ export default {
if (getComputedStyle(container).height === "0px") { if (getComputedStyle(container).height === "0px") {
container.style.height = "100%" container.style.height = "100%"
} }
window.addEventListener('resize', this.onResize)
},
beforeDestroy () {
window.removeEventListener('resize', this.onResize)
document.removeEventListener('mouseup', this.onMouseUp)
}, },
methods: { methods: {
onLoad() {
let img = this.$refs.imgex
this.imageLoaded = true
if (img === undefined) {
return
}
img.classList.remove('image-ex-img-center')
this.setCenter()
img.classList.add('image-ex-img-ready')
document.addEventListener('mouseup', this.onMouseUp)
},
onMouseUp() {
this.inDrag = false
},
onResize: throttle(function() {
if (this.imageLoaded) {
this.setCenter()
this.doMove(this.position.relative.x, this.position.relative.y)
}
}, 100),
setCenter() { setCenter() {
let container = this.$refs.container let container = this.$refs.container
let img = this.$refs.imgex let img = this.$refs.imgex
let rate = Math.min( this.position.center.x = Math.floor((container.clientWidth - img.clientWidth) / 2)
container.clientWidth / img.clientWidth, this.position.center.y = Math.floor((container.clientHeight - img.clientHeight) / 2)
container.clientHeight / img.clientHeight
) img.style.left = this.position.center.x + 'px'
if (!this.autofill && rate > 1) { img.style.top = this.position.center.y + 'px'
rate = 1
}
// height will be auto set
img.width = Math.floor(img.clientWidth * rate)
img.style.top = `${Math.floor((container.clientHeight - img.clientHeight) / 2)}px`
img.style.left = `${Math.floor((container.clientWidth - img.clientWidth) / 2)}px`
document.addEventListener('mouseup', () => this.inDrag = false )
}, },
mousedownStart(event) { mousedownStart(event) {
this.lastX = null this.lastX = null
@ -159,8 +189,22 @@ export default {
}, },
doMove(x, y) { doMove(x, y) {
let style = this.$refs.imgex.style let style = this.$refs.imgex.style
style.left = `${this.pxStringToNumber(style.left) + x}px` let posX = this.pxStringToNumber(style.left) + x
style.top = `${this.pxStringToNumber(style.top) + y}px` let posY = this.pxStringToNumber(style.top) + y
style.left = posX + 'px'
style.top = posY + 'px'
this.position.relative.x = Math.abs(this.position.center.x - posX)
this.position.relative.y = Math.abs(this.position.center.y - posY)
if (posX < this.position.center.x) {
this.position.relative.x = this.position.relative.x * -1
}
if (posY < this.position.center.y) {
this.position.relative.y = this.position.relative.y * -1
}
}, },
wheelMove(event) { wheelMove(event) {
this.scale += (event.wheelDeltaY / 100) * this.zoomStep this.scale += (event.wheelDeltaY / 100) * this.zoomStep
@ -185,9 +229,20 @@ export default {
} }
.image-ex-img { .image-ex-img {
position: absolute;
}
.image-ex-img-center {
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
position: absolute;
transition: none;
}
.image-ex-img-ready {
left: 0; left: 0;
top: 0; top: 0;
position: absolute;
transition: transform 0.1s ease; transition: transform 0.1s ease;
} }
</style> </style>

View File

@ -248,7 +248,8 @@ export default {
this.$store.commit('updateClipboard', { this.$store.commit('updateClipboard', {
key: key, key: key,
items: items items: items,
path: this.$route.path
}) })
}, },
paste (event) { paste (event) {
@ -261,23 +262,56 @@ export default {
for (let item of this.$store.state.clipboard.items) { for (let item of this.$store.state.clipboard.items) {
const from = item.from.endsWith('/') ? item.from.slice(0, -1) : item.from const from = item.from.endsWith('/') ? item.from.slice(0, -1) : item.from
const to = this.$route.path + item.name const to = this.$route.path + item.name
items.push({ from, to }) items.push({ from, to, name: item.name })
} }
if (items.length === 0) { if (items.length === 0) {
return return
} }
if (this.$store.state.clipboard.key === 'x') { let action = (overwrite, rename) => {
api.move(items).then(() => { api.copy(items, overwrite, rename).then(() => {
this.$store.commit('setReload', true) this.$store.commit('setReload', true)
}).catch(this.$showError) }).catch(this.$showError)
}
if (this.$store.state.clipboard.key === 'x') {
action = (overwrite, rename) => {
api.move(items, overwrite, rename).then(() => {
this.$store.commit('resetClipboard')
this.$store.commit('setReload', true)
}).catch(this.$showError)
}
}
if (this.$store.state.clipboard.path == this.$route.path) {
action(false, true)
return return
} }
api.copy(items).then(() => { let conflict = upload.checkConflict(items, this.req.items)
this.$store.commit('setReload', true)
}).catch(this.$showError) let overwrite = false
let rename = false
if (conflict) {
this.$store.commit('showHover', {
prompt: 'replace-rename',
confirm: (event, option) => {
overwrite = option == 'overwrite'
rename = option == 'rename'
event.preventDefault()
this.$store.commit('closeHovers')
action(overwrite, rename)
}
})
return
}
action(overwrite, rename)
}, },
resizeEvent () { resizeEvent () {
// Update the columns size based on the window width. // Update the columns size based on the window width.

View File

@ -36,6 +36,7 @@ import { mapMutations, mapGetters, mapState } from 'vuex'
import filesize from 'filesize' import filesize from 'filesize'
import moment from 'moment' import moment from 'moment'
import { files as api } from '@/api' import { files as api } from '@/api'
import * as upload from '@/utils/upload'
export default { export default {
name: 'item', name: 'item',
@ -110,26 +111,61 @@ export default {
el.style.opacity = 1 el.style.opacity = 1
}, },
drop: function (event) { drop: async function (event) {
if (!this.canDrop) return if (!this.canDrop) return
event.preventDefault() event.preventDefault()
if (this.selectedCount === 0) return if (this.selectedCount === 0) return
let el = event.target
for (let i = 0; i < 5; i++) {
if (el !== null && !el.classList.contains('item')) {
el = el.parentElement
}
}
let items = [] let items = []
for (let i of this.selected) { for (let i of this.selected) {
items.push({ items.push({
from: this.req.items[i].url, from: this.req.items[i].url,
to: this.url + this.req.items[i].name to: this.url + this.req.items[i].name,
name: this.req.items[i].name
}) })
}
let base = el.querySelector('.name').innerHTML + '/'
let path = this.$route.path + base
let baseItems = (await api.fetch(path)).items
let action = (overwrite, rename) => {
api.move(items, overwrite, rename).then(() => {
this.$store.commit('setReload', true)
}).catch(this.$showError)
} }
api.move(items) let conflict = upload.checkConflict(items, baseItems)
.then(() => {
this.$store.commit('setReload', true) let overwrite = false
let rename = false
if (conflict) {
this.$store.commit('showHover', {
prompt: 'replace-rename',
confirm: (event, option) => {
overwrite = option == 'overwrite'
rename = option == 'rename'
event.preventDefault()
this.$store.commit('closeHovers')
action(overwrite, rename)
}
}) })
.catch(this.$showError)
return
}
action(overwrite, rename)
}, },
click: function (event) { click: function (event) {
if (this.selectedCount !== 0) event.preventDefault() if (this.selectedCount !== 0) event.preventDefault()

View File

@ -5,10 +5,22 @@
<i class="material-icons">close</i> <i class="material-icons">close</i>
</button> </button>
<rename-button v-if="user.perm.rename"></rename-button> <div class="title">
<delete-button v-if="user.perm.delete"></delete-button> <span>{{ this.name }}</span>
<download-button v-if="user.perm.download"></download-button> </div>
<info-button></info-button>
<rename-button :disabled="loading" v-if="user.perm.rename"></rename-button>
<delete-button :disabled="loading" v-if="user.perm.delete"></delete-button>
<download-button :disabled="loading" v-if="user.perm.download"></download-button>
<info-button :disabled="loading"></info-button>
</div>
<div class="loading" v-if="loading">
<div class="spinner">
<div class="bounce1"></div>
<div class="bounce2"></div>
<div class="bounce3"></div>
</div>
</div> </div>
<button class="action" @click="prev" v-show="hasPrevious" :aria-label="$t('buttons.previous')" :title="$t('buttons.previous')"> <button class="action" @click="prev" v-show="hasPrevious" :aria-label="$t('buttons.previous')" :title="$t('buttons.previous')">
@ -18,25 +30,27 @@
<i class="material-icons">chevron_right</i> <i class="material-icons">chevron_right</i>
</button> </button>
<div class="preview"> <template v-if="!loading">
<ExtendedImage v-if="req.type == 'image'" :src="raw"></ExtendedImage> <div class="preview">
<audio v-else-if="req.type == 'audio'" :src="raw" autoplay controls></audio> <ExtendedImage v-if="req.type == 'image'" :src="raw"></ExtendedImage>
<video v-else-if="req.type == 'video'" :src="raw" autoplay controls> <audio v-else-if="req.type == 'audio'" :src="raw" autoplay controls></audio>
<track <video v-else-if="req.type == 'video'" :src="raw" autoplay controls>
kind="captions" <track
v-for="(sub, index) in subtitles" kind="captions"
:key="index" v-for="(sub, index) in subtitles"
:src="sub" :key="index"
:label="'Subtitle ' + index" :default="index === 0"> :src="sub"
Sorry, your browser doesn't support embedded videos, :label="'Subtitle ' + index" :default="index === 0">
but don't worry, you can <a :href="download">download it</a> Sorry, your browser doesn't support embedded videos,
and watch it with your favorite video player! but don't worry, you can <a :href="download">download it</a>
</video> and watch it with your favorite video player!
<object v-else-if="req.extension == '.pdf'" class="pdf" :data="raw"></object> </video>
<a v-else-if="req.type == 'blob'" :href="download"> <object v-else-if="req.extension == '.pdf'" class="pdf" :data="raw"></object>
<h2 class="message">{{ $t('buttons.download') }} <i class="material-icons">file_download</i></h2> <a v-else-if="req.type == 'blob'" :href="download">
</a> <h2 class="message">{{ $t('buttons.download') }} <i class="material-icons">file_download</i></h2>
</div> </a>
</div>
</template>
</div> </div>
</template> </template>
@ -72,11 +86,12 @@ export default {
previousLink: '', previousLink: '',
nextLink: '', nextLink: '',
listing: null, listing: null,
name: '',
subtitles: [] subtitles: []
} }
}, },
computed: { computed: {
...mapState(['req', 'user', 'oldReq', 'jwt']), ...mapState(['req', 'user', 'oldReq', 'jwt', 'loading']),
hasPrevious () { hasPrevious () {
return (this.previousLink !== '') return (this.previousLink !== '')
}, },
@ -96,30 +111,24 @@ export default {
return `${this.previewUrl}&inline=true` return `${this.previewUrl}&inline=true`
} }
}, },
watch: {
$route: function () {
this.updatePreview()
}
},
async mounted () { async mounted () {
window.addEventListener('keyup', this.key) window.addEventListener('keyup', this.key)
this.$store.commit('setPreviewMode', true)
if (this.req.subtitles) { this.listing = this.oldReq.items
this.subtitles = this.req.subtitles.map(sub => `${baseURL}/api/raw${sub}?auth=${this.jwt}&inline=true`) this.updatePreview()
}
try {
if (this.oldReq.items) {
this.updateLinks(this.oldReq.items)
} else {
const path = url.removeLastDir(this.$route.path)
const res = await api.fetch(path)
this.updateLinks(res.items)
}
} catch (e) {
this.$showError(e)
}
}, },
beforeDestroy () { beforeDestroy () {
window.removeEventListener('keyup', this.key) window.removeEventListener('keyup', this.key)
this.$store.commit('setPreviewMode', false)
}, },
methods: { methods: {
back () { back () {
this.$store.commit('setPreviewMode', false)
let uri = url.removeLastDir(this.$route.path) + '/' let uri = url.removeLastDir(this.$route.path) + '/'
this.$router.push({ path: uri }) this.$router.push({ path: uri })
}, },
@ -138,22 +147,42 @@ export default {
if (this.hasPrevious) this.prev() if (this.hasPrevious) this.prev()
} }
}, },
updateLinks (items) { async updatePreview () {
for (let i = 0; i < items.length; i++) { if (this.req.subtitles) {
if (items[i].name !== this.req.name) { this.subtitles = this.req.subtitles.map(sub => `${baseURL}/api/raw${sub}?auth=${this.jwt}&inline=true`)
}
let dirs = this.$route.fullPath.split("/")
this.name = decodeURIComponent(dirs[dirs.length - 1])
if (!this.listing) {
try {
const path = url.removeLastDir(this.$route.path)
const res = await api.fetch(path)
this.listing = res.items
} catch (e) {
this.$showError(e)
}
}
this.previousLink = ''
this.nextLink = ''
for (let i = 0; i < this.listing.length; i++) {
if (this.listing[i].name !== this.name) {
continue continue
} }
for (let j = i - 1; j >= 0; j--) { for (let j = i - 1; j >= 0; j--) {
if (mediaTypes.includes(items[j].type)) { if (mediaTypes.includes(this.listing[j].type)) {
this.previousLink = items[j].url this.previousLink = this.listing[j].url
break break
} }
} }
for (let j = i + 1; j < items.length; j++) { for (let j = i + 1; j < this.listing.length; j++) {
if (mediaTypes.includes(items[j].type)) { if (mediaTypes.includes(this.listing[j].type)) {
this.nextLink = items[j].url this.nextLink = this.listing[j].url
break break
} }
} }

View File

@ -16,7 +16,6 @@
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button> :title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
<button class="button button--flat" <button class="button button--flat"
@click="copy" @click="copy"
:disabled="$route.path === dest"
:aria-label="$t('buttons.copy')" :aria-label="$t('buttons.copy')"
:title="$t('buttons.copy')">{{ $t('buttons.copy') }}</button> :title="$t('buttons.copy')">{{ $t('buttons.copy') }}</button>
</div> </div>
@ -28,6 +27,7 @@ import { mapState } from 'vuex'
import FileList from './FileList' import FileList from './FileList'
import { files as api } from '@/api' import { files as api } from '@/api'
import buttons from '@/utils/buttons' import buttons from '@/utils/buttons'
import * as upload from '@/utils/upload'
export default { export default {
name: 'copy', name: 'copy',
@ -42,25 +42,66 @@ export default {
methods: { methods: {
copy: async function (event) { copy: async function (event) {
event.preventDefault() event.preventDefault()
buttons.loading('copy')
let items = [] let items = []
// Create a new promise for each file. // Create a new promise for each file.
for (let item of this.selected) { for (let item of this.selected) {
items.push({ items.push({
from: this.req.items[item].url, from: this.req.items[item].url,
to: this.dest + encodeURIComponent(this.req.items[item].name) to: this.dest + encodeURIComponent(this.req.items[item].name),
name: this.req.items[item].name
}) })
} }
try { let action = async (overwrite, rename) => {
await api.copy(items) buttons.loading('copy')
buttons.success('copy')
this.$router.push({ path: this.dest }) await api.copy(items, overwrite, rename).then(() => {
} catch (e) { buttons.success('copy')
buttons.done('copy')
this.$showError(e) if (this.$route.path === this.dest) {
this.$store.commit('setReload', true)
return
}
this.$router.push({ path: this.dest })
}).catch((e) => {
buttons.done('copy')
this.$showError(e)
})
} }
if (this.$route.path === this.dest) {
this.$store.commit('closeHovers')
action(false, true)
return
}
let dstItems = (await api.fetch(this.dest)).items
let conflict = upload.checkConflict(items, dstItems)
let overwrite = false
let rename = false
if (conflict) {
this.$store.commit('showHover', {
prompt: 'replace-rename',
confirm: (event, option) => {
overwrite = option == 'overwrite'
rename = option == 'rename'
event.preventDefault()
this.$store.commit('closeHovers')
action(overwrite, rename)
}
})
return
}
action(overwrite, rename)
} }
} }
} }

View File

@ -41,19 +41,7 @@ export default {
} }
}, },
mounted () { mounted () {
// If we're showing this on a listing, this.fillOptions(this.req)
// we can use the current request object
// to fill the move options.
if (this.req.kind === 'listing') {
this.fillOptions(this.req)
return
}
// Otherwise, we must be on a preview or editor
// so we fetch the data from the previous directory.
files.fetch(url.removeLastDir(this.$route.path))
.then(this.fillOptions)
.catch(this.$showError)
}, },
methods: { methods: {
fillOptions (req) { fillOptions (req) {

View File

@ -27,6 +27,7 @@ import { mapState } from 'vuex'
import FileList from './FileList' import FileList from './FileList'
import { files as api } from '@/api' import { files as api } from '@/api'
import buttons from '@/utils/buttons' import buttons from '@/utils/buttons'
import * as upload from '@/utils/upload'
export default { export default {
name: 'move', name: 'move',
@ -41,26 +42,51 @@ export default {
methods: { methods: {
move: async function (event) { move: async function (event) {
event.preventDefault() event.preventDefault()
buttons.loading('move')
let items = [] let items = []
for (let item of this.selected) { for (let item of this.selected) {
items.push({ items.push({
from: this.req.items[item].url, from: this.req.items[item].url,
to: this.dest + encodeURIComponent(this.req.items[item].name) to: this.dest + encodeURIComponent(this.req.items[item].name),
name: this.req.items[item].name
}) })
} }
try { let action = async (overwrite, rename) => {
api.move(items) buttons.loading('move')
buttons.success('move')
this.$router.push({ path: this.dest }) await api.move(items, overwrite, rename).then(() => {
} catch (e) { buttons.success('move')
buttons.done('move') this.$router.push({ path: this.dest })
this.$showError(e) }).catch((e) => {
buttons.done('move')
this.$showError(e)
})
} }
event.preventDefault() let dstItems = (await api.fetch(this.dest)).items
let conflict = upload.checkConflict(items, dstItems)
let overwrite = false
let rename = false
if (conflict) {
this.$store.commit('showHover', {
prompt: 'replace-rename',
confirm: (event, option) => {
overwrite = option == 'overwrite'
rename = option == 'rename'
event.preventDefault()
this.$store.commit('closeHovers')
action(overwrite, rename)
}
})
return
}
action(overwrite, rename)
} }
} }
} }

View File

@ -16,6 +16,7 @@ import Copy from './Copy'
import NewFile from './NewFile' import NewFile from './NewFile'
import NewDir from './NewDir' import NewDir from './NewDir'
import Replace from './Replace' import Replace from './Replace'
import ReplaceRename from './ReplaceRename'
import Share from './Share' import Share from './Share'
import Upload from './Upload' import Upload from './Upload'
import { mapState } from 'vuex' import { mapState } from 'vuex'
@ -35,6 +36,7 @@ export default {
NewDir, NewDir,
Help, Help,
Replace, Replace,
ReplaceRename,
Upload Upload
}, },
data: function () { data: function () {
@ -52,7 +54,7 @@ export default {
return return
let prompt = this.$refs.currentComponent; let prompt = this.$refs.currentComponent;
// Enter // Enter
if (event.keyCode == 13) { if (event.keyCode == 13) {
switch (this.show) { switch (this.show) {
@ -87,6 +89,7 @@ export default {
'newDir', 'newDir',
'download', 'download',
'replace', 'replace',
'replace-rename',
'share', 'share',
'upload' 'upload'
].indexOf(this.show) >= 0; ].indexOf(this.show) >= 0;

View File

@ -0,0 +1,35 @@
<template>
<div class="card floating">
<div class="card-title">
<h2>{{ $t('prompts.replace') }}</h2>
</div>
<div class="card-content">
<p>{{ $t('prompts.replaceMessage') }}</p>
</div>
<div class="card-action">
<button class="button button--flat button--grey"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
<button class="button button--flat button--blue"
@click="(event) => showConfirm(event, 'rename')"
:aria-label="$t('buttons.rename')"
:title="$t('buttons.rename')">{{ $t('buttons.rename') }}</button>
<button class="button button--flat button--red"
@click="(event) => showConfirm(event, 'overwrite')"
:aria-label="$t('buttons.replace')"
:title="$t('buttons.replace')">{{ $t('buttons.replace') }}</button>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
name: 'replace-rename',
computed: mapState(['showConfirm'])
}
</script>

View File

@ -25,8 +25,8 @@
background: var(--red); background: var(--red);
} }
.button--red:hover { .button--blue {
background: var(--dark-red); background: var(--blue);
} }
.button--flat { .button--flat {

View File

@ -125,8 +125,13 @@
height: 3.7em; height: 3.7em;
} }
#previewer .action:first-of-type { #previewer .bar .title {
margin-right: auto; margin-right: auto;
padding: 0 1em;
line-height: 2.7em;
overflow: hidden;
word-break: break-word;
color: #fff;
} }
#previewer .action i { #previewer .action i {
@ -219,6 +224,11 @@
font-size: 1.2em; font-size: 1.2em;
} }
#previewer .loading {
height: 100%;
width: 100%;
}
#editor-container #editor { #editor-container #editor {
height: calc(100vh - 8.2em); height: calc(100vh - 8.2em);
} }

View File

@ -23,7 +23,8 @@ const state = {
show: null, show: null,
showShell: false, showShell: false,
showMessage: null, showMessage: null,
showConfirm: null showConfirm: null,
previewMode: false
} }
export default new Vuex.Store({ export default new Vuex.Store({

View File

@ -37,6 +37,11 @@ const mutations = {
} }
} }
const beforeUnload = (event) => {
event.preventDefault()
event.returnValue = ''
}
const actions = { const actions = {
upload: (context, item) => { upload: (context, item) => {
let uploadsCount = Object.keys(context.state.uploads).length; let uploadsCount = Object.keys(context.state.uploads).length;
@ -45,6 +50,7 @@ const actions = {
let isUploadsEmpty = uploadsCount == 0 let isUploadsEmpty = uploadsCount == 0
if (isQueueEmpty && isUploadsEmpty) { if (isQueueEmpty && isUploadsEmpty) {
window.addEventListener('beforeunload', beforeUnload)
buttons.loading('upload') buttons.loading('upload')
} }
@ -67,6 +73,7 @@ const actions = {
let canProcess = isBellowLimit && !isQueueEmpty let canProcess = isBellowLimit && !isQueueEmpty
if (isFinished) { if (isFinished) {
window.removeEventListener('beforeunload', beforeUnload)
buttons.success('upload') buttons.success('upload')
context.commit('reset') context.commit('reset')
context.commit('setReload', true, { root: true }) context.commit('setReload', true, { root: true })

View File

@ -78,10 +78,14 @@ const mutations = {
updateClipboard: (state, value) => { updateClipboard: (state, value) => {
state.clipboard.key = value.key state.clipboard.key = value.key
state.clipboard.items = value.items state.clipboard.items = value.items
state.clipboard.path = value.path
}, },
resetClipboard: (state) => { resetClipboard: (state) => {
state.clipboard.key = '' state.clipboard.key = ''
state.clipboard.items = [] state.clipboard.items = []
},
setPreviewMode(state, value) {
state.previewMode = value
} }
} }

View File

@ -6,10 +6,7 @@ export function checkConflict(files, items) {
items = [] items = []
} }
let folder_upload = false let folder_upload = files[0].fullPath !== undefined
if (files[0].fullPath !== undefined) {
folder_upload = true
}
let conflict = false let conflict = false
for (let i = 0; i < files.length; i++) { for (let i = 0; i < files.length; i++) {
@ -69,7 +66,7 @@ export function scanFiles(dt) {
const dir = { const dir = {
isDir: true, isDir: true,
size: 0, size: 0,
path: `${directory}${entry.name}` fullPath: `${directory}${entry.name}`
} }
contents.push(dir) contents.push(dir)
@ -112,7 +109,7 @@ export function handleFiles(files, path, overwrite = false) {
if (file.isDir) { if (file.isDir) {
itemPath = path itemPath = path
let folders = file.path.split("/") let folders = file.fullPath.split("/")
for (let i = 0; i < folders.length; i++) { for (let i = 0; i < folders.length; i++) {
let folder = folders[i] let folder = folders[i]

View File

@ -10,14 +10,15 @@
<router-link :to="link.url">{{ link.name }}</router-link> <router-link :to="link.url">{{ link.name }}</router-link>
</span> </span>
</div> </div>
<div v-if="error"> <div v-if="error">
<not-found v-if="error.message === '404'"></not-found> <not-found v-if="error.message === '404'"></not-found>
<forbidden v-else-if="error.message === '403'"></forbidden> <forbidden v-else-if="error.message === '403'"></forbidden>
<internal-error v-else></internal-error> <internal-error v-else></internal-error>
</div> </div>
<preview v-else-if="isPreview"></preview>
<editor v-else-if="isEditor"></editor> <editor v-else-if="isEditor"></editor>
<listing :class="{ multiple }" v-else-if="isListing"></listing> <listing :class="{ multiple }" v-else-if="isListing"></listing>
<preview v-else-if="isPreview"></preview>
<div v-else> <div v-else>
<h2 class="message"> <h2 class="message">
<span>{{ $t('files.loading') }}</span> <span>{{ $t('files.loading') }}</span>
@ -65,7 +66,7 @@ export default {
'show' 'show'
]), ]),
isPreview () { isPreview () {
return !this.loading && !this.isListing && !this.isEditor return !this.loading && !this.isListing && !this.isEditor || this.loading && this.$store.state.previewMode
}, },
breadcrumbs () { breadcrumbs () {
let parts = this.$route.path.split('/') let parts = this.$route.path.split('/')

View File

@ -127,6 +127,10 @@ var resourcePostPutHandler = withUser(func(w http.ResponseWriter, r *http.Reques
return nil return nil
}, action, r.URL.Path, "", d.user) }, action, r.URL.Path, "", d.user)
if err != nil {
_ = d.user.Fs.RemoveAll(r.URL.Path)
}
return errToStatus(err), err return errToStatus(err), err
}) })
@ -144,6 +148,31 @@ var resourcePatchHandler = withUser(func(w http.ResponseWriter, r *http.Request,
return http.StatusForbidden, nil return http.StatusForbidden, nil
} }
override := r.URL.Query().Get("override") == "true"
rename := r.URL.Query().Get("rename") == "true"
if !override && !rename {
if _, err = d.user.Fs.Stat(dst); err == nil {
return http.StatusConflict, nil
}
}
if rename {
counter := 1
dir, name := filepath.Split(dst)
ext := filepath.Ext(name)
base := strings.TrimSuffix(name, ext)
for {
if _, err = d.user.Fs.Stat(dst); err != nil {
break
}
new := fmt.Sprintf("%s(%d)%s", base, counter, ext)
dst = filepath.Join(dir, new)
counter++
}
}
err = d.RunHook(func() error { err = d.RunHook(func() error {
switch action { switch action {
// TODO: use enum // TODO: use enum