Compare commits
50 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7e7679002 | ||
|
|
9a829fd594 | ||
|
|
624d61930c | ||
|
|
ee30e7711f | ||
|
|
6e5116aa27 | ||
|
|
34acffbb7b | ||
|
|
eb6f26c191 | ||
|
|
06f3e9744a | ||
|
|
cac5413424 | ||
|
|
037efc2eb8 | ||
|
|
7de22b53b8 | ||
|
|
ae19731015 | ||
|
|
8aa0797019 | ||
|
|
17b3a403a5 | ||
|
|
819d511690 | ||
|
|
30cfd06e3d | ||
|
|
c5558e6e41 | ||
|
|
c3b3099ebb | ||
|
|
b1d47daa69 | ||
|
|
f51e2d5ba1 | ||
|
|
386974657e | ||
|
|
f7858cd719 | ||
|
|
b355a5c058 | ||
|
|
50758b53f4 | ||
|
|
f9902d2bdb | ||
|
|
b1512d2b66 | ||
|
|
1cfd31756d | ||
|
|
9152c77543 | ||
|
|
c236db329f | ||
|
|
bf6e0abd96 | ||
|
|
8d8c756233 | ||
|
|
d5f0471ab7 | ||
|
|
f7aaae3f63 | ||
|
|
2515819026 | ||
|
|
7ad727d27d | ||
|
|
adbff03274 | ||
|
|
6b8a65382f | ||
|
|
4f7e6cbb52 | ||
|
|
40fe081962 | ||
|
|
ebf1325126 | ||
|
|
ea1b9febb7 | ||
|
|
f3edf63fb2 | ||
|
|
f13e7ba940 | ||
|
|
3ebe219e96 | ||
|
|
d838856711 | ||
|
|
610d55c26f | ||
|
|
ac044016ee | ||
|
|
58af3461a8 | ||
|
|
4191a6f9e0 | ||
|
|
c18ca4702d |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"port": 80,
|
||||
"address": "",
|
||||
"database": "/etc/database.db",
|
||||
"database": "/database.db",
|
||||
"scope": "/srv",
|
||||
"allowCommands": true,
|
||||
"allowEdit": true,
|
||||
|
||||
12
Dockerfile
12
Dockerfile
@@ -7,16 +7,16 @@ RUN apk add --no-cache git
|
||||
RUN go get ./...
|
||||
|
||||
WORKDIR /go/src/github.com/hacdias/filemanager/cmd/filemanager
|
||||
RUN go build -ldflags "-X main.version=$(git tag -l --points-at HEAD)"
|
||||
RUN CGO_ENABLED=0 go build -a
|
||||
RUN mv filemanager /go/bin/filemanager
|
||||
|
||||
FROM alpine:latest
|
||||
COPY --from=0 /go/bin/filemanager /usr/local/bin/filemanager
|
||||
FROM scratch
|
||||
COPY --from=0 /go/bin/filemanager /filemanager
|
||||
|
||||
VOLUME /srv
|
||||
EXPOSE 80
|
||||
|
||||
COPY Docker.json /etc/config.json
|
||||
COPY Docker.json /config.json
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/filemanager"]
|
||||
CMD ["--config", "/etc/config.json"]
|
||||
ENTRYPOINT ["/filemanager"]
|
||||
CMD ["--config", "/config.json"]
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
||||
<meta name="base" content="{{ .BaseURL }}">
|
||||
<meta name="staticgen" content="{{ .StaticGen }}">
|
||||
<meta name="noauth" content="{{ .NoAuth }}">
|
||||
<meta name="version" content="{{ .Version }}">
|
||||
<meta name="recaptcha" content="{{ .ReCaptchaKey }}">
|
||||
<title>File Manager</title>
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="{{ .BaseURL }}/static/img/icons/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="{{ .BaseURL }}/static/img/icons/favicon-16x16.png">
|
||||
@@ -22,6 +25,13 @@
|
||||
<!-- Add to home screen for Windows -->
|
||||
<meta name="msapplication-TileImage" content="{{ .BaseURL }}/static/img/icons/msapplication-icon-144x144.png">
|
||||
<meta name="msapplication-TileColor" content="#2979ff">
|
||||
|
||||
<script>CSS = "{{ .CSS }}"</script>
|
||||
|
||||
{{ if .ReCaptcha -}}
|
||||
<script src='https://www.google.com/recaptcha/api.js?render=explicit'></script>
|
||||
{{ end }}
|
||||
|
||||
<% for (var chunk of webpack.chunks) {
|
||||
for (var file of chunk.files) {
|
||||
if (file.match(/\.(js|css)$/)) { %>
|
||||
|
||||
@@ -1,18 +1,74 @@
|
||||
<template>
|
||||
<router-view></router-view>
|
||||
<router-view :dependencies="loaded" @update:css="updateCSS" @clean:css="cleanCSS"></router-view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'app',
|
||||
mounted: function () {
|
||||
// Remove loading animation.
|
||||
let loading = document.getElementById('loading')
|
||||
loading.classList.add('done')
|
||||
computed: mapState(['recaptcha']),
|
||||
data () {
|
||||
return {
|
||||
loaded: false
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
if (this.recaptcha.length === 0) {
|
||||
this.unload()
|
||||
return
|
||||
}
|
||||
|
||||
setTimeout(function () {
|
||||
loading.parentNode.removeChild(loading)
|
||||
}, 200)
|
||||
let check = () => {
|
||||
if (typeof window.grecaptcha === 'undefined') {
|
||||
setTimeout(check, 100)
|
||||
return
|
||||
}
|
||||
|
||||
this.unload()
|
||||
}
|
||||
|
||||
check()
|
||||
},
|
||||
methods: {
|
||||
unload () {
|
||||
this.loaded = true
|
||||
// Remove loading animation.
|
||||
let loading = document.getElementById('loading')
|
||||
loading.classList.add('done')
|
||||
|
||||
setTimeout(function () {
|
||||
loading.parentNode.removeChild(loading)
|
||||
}, 200)
|
||||
|
||||
this.updateCSS()
|
||||
},
|
||||
updateCSS (global = false) {
|
||||
let css = this.$store.state.css
|
||||
|
||||
if (typeof this.$store.state.user.css === 'string' && !global) {
|
||||
css += '\n' + this.$store.state.user.css
|
||||
}
|
||||
|
||||
this.removeCSS()
|
||||
|
||||
let style = document.createElement('style')
|
||||
style.title = 'custom-css'
|
||||
style.type = 'text/css'
|
||||
style.appendChild(document.createTextNode(css))
|
||||
document.head.appendChild(style)
|
||||
},
|
||||
removeCSS () {
|
||||
let style = document.querySelector('style[title="custom-css"]')
|
||||
if (style === undefined || style === null) {
|
||||
return
|
||||
}
|
||||
|
||||
style.parentElement.removeChild(style)
|
||||
},
|
||||
cleanCSS () {
|
||||
this.updateCSS(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="!$store.state.user.noAuth">
|
||||
<div v-if="!$store.state.noAuth">
|
||||
<router-link class="action" to="/settings" :aria-label="$t('sidebar.settings')" :title="$t('sidebar.settings')">
|
||||
<i class="material-icons">settings_applications</i>
|
||||
<span>{{ $t('sidebar.settings') }}</span>
|
||||
@@ -59,7 +59,7 @@
|
||||
</div>
|
||||
|
||||
<p class="credits">
|
||||
<span>{{ $t('sidebar.servedWith') }} <a rel="noopener noreferrer" href="https://github.com/hacdias/filemanager">File Manager</a>.</span>
|
||||
<span><a rel="noopener noreferrer" href="https://github.com/hacdias/filemanager">File Manager</a> v{{ version }}</span>
|
||||
<span><a @click="help">{{ $t('sidebar.help') }}</a></span>
|
||||
</p>
|
||||
</nav>
|
||||
@@ -72,7 +72,7 @@ import auth from '@/utils/auth'
|
||||
export default {
|
||||
name: 'sidebar',
|
||||
computed: {
|
||||
...mapState(['user', 'staticGen']),
|
||||
...mapState(['user', 'staticGen', 'version']),
|
||||
active () {
|
||||
return this.$store.state.show === 'sidebar'
|
||||
}
|
||||
|
||||
@@ -1,32 +1,35 @@
|
||||
<template>
|
||||
<button @click="change" :aria-label="$t('buttons.switchView')" :title="$t('buttons.switchView')" class="action" id="switch-view-button">
|
||||
<i class="material-icons">{{ icon() }}</i>
|
||||
<i class="material-icons">{{ icon }}</i>
|
||||
<span>{{ $t('buttons.switchView') }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapMutations } from 'vuex'
|
||||
import { updateUser } from '@/utils/api'
|
||||
|
||||
export default {
|
||||
name: 'switch-button',
|
||||
computed: {
|
||||
...mapState(['user']),
|
||||
icon: function () {
|
||||
if (this.user.viewMode === 'mosaic') return 'view_list'
|
||||
return 'view_module'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapMutations(['updateUser']),
|
||||
change: function (event) {
|
||||
// If we are on mobile we should close the dropdown.
|
||||
this.$store.commit('closeHovers')
|
||||
|
||||
let display = 'mosaic'
|
||||
let user = {...this.user}
|
||||
user.viewMode = (this.icon === 'view_list') ? 'list' : 'mosaic'
|
||||
|
||||
if (this.$store.state.req.display === 'mosaic') {
|
||||
display = 'list'
|
||||
}
|
||||
|
||||
this.$store.commit('listingDisplay', display)
|
||||
let path = this.$store.state.baseURL
|
||||
if (path === '') path = '/'
|
||||
document.cookie = `display=${display}; max-age=31536000; path=${path}`
|
||||
},
|
||||
icon: function () {
|
||||
if (this.$store.state.req.display === 'mosaic') return 'view_list'
|
||||
return 'view_module'
|
||||
updateUser(user, 'partial').then(() => {
|
||||
this.updateUser({ viewMode: user.viewMode })
|
||||
}).catch(this.$showError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ export default {
|
||||
})
|
||||
.catch(error => {
|
||||
buttons.done(button)
|
||||
this.$store.commit('showError', error)
|
||||
this.$showError(error)
|
||||
this.$store.commit('setSchedule', '')
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" multiple>
|
||||
</div>
|
||||
<div v-else id="listing"
|
||||
:class="req.display"
|
||||
:class="user.viewMode"
|
||||
@dragenter="dragEnter"
|
||||
@dragend="dragEnd">
|
||||
<div>
|
||||
@@ -98,7 +98,7 @@ export default {
|
||||
name: 'listing',
|
||||
components: { Item },
|
||||
computed: {
|
||||
...mapState(['req', 'selected']),
|
||||
...mapState(['req', 'selected', 'user']),
|
||||
nameSorted () {
|
||||
return (this.req.sort === 'name')
|
||||
},
|
||||
@@ -210,17 +210,13 @@ export default {
|
||||
if (this.$store.state.clipboard.key === 'x') {
|
||||
api.move(items).then(() => {
|
||||
this.$store.commit('setReload', true)
|
||||
}).catch(error => {
|
||||
this.$store.commit('showError', error)
|
||||
})
|
||||
}).catch(this.$showError)
|
||||
return
|
||||
}
|
||||
|
||||
api.copy(items).then(() => {
|
||||
this.$store.commit('setReload', true)
|
||||
}).catch(error => {
|
||||
this.$store.commit('showError', error)
|
||||
})
|
||||
}).catch(this.$showError)
|
||||
},
|
||||
resizeEvent () {
|
||||
// Update the columns size based on the window width.
|
||||
@@ -267,7 +263,7 @@ export default {
|
||||
.then(req => {
|
||||
this.checkConflict(files, req.items, base)
|
||||
})
|
||||
.catch(error => { console.log(error) })
|
||||
.catch(this.$showError)
|
||||
|
||||
return
|
||||
}
|
||||
@@ -348,7 +344,7 @@ export default {
|
||||
})
|
||||
.catch(error => {
|
||||
finish()
|
||||
this.$store.commit('showError', error)
|
||||
this.$showError(error)
|
||||
})
|
||||
|
||||
return false
|
||||
|
||||
@@ -109,21 +109,24 @@ export default {
|
||||
.then(() => {
|
||||
this.$store.commit('setReload', true)
|
||||
})
|
||||
.catch(error => {
|
||||
this.$store.commit('showError', error)
|
||||
})
|
||||
.catch(this.$showError)
|
||||
},
|
||||
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 {
|
||||
if (this.$store.state.selected.indexOf(this.index) !== -1) {
|
||||
this.removeSelected(this.index)
|
||||
return
|
||||
}
|
||||
|
||||
return false
|
||||
if (event.shiftKey && this.selected.length === 1) {
|
||||
let fi = (this.index > this.selected[0]) ? this.selected[0] : this.index
|
||||
let la = (this.index > this.selected[0]) ? this.index : this.selected[0]
|
||||
for (; fi <= la; fi++) this.addSelected(fi)
|
||||
return
|
||||
}
|
||||
|
||||
if (!event.ctrlKey && !this.$store.state.multiple) this.resetSelected()
|
||||
this.addSelected(this.index)
|
||||
},
|
||||
touchstart (event) {
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
|
||||
<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>
|
||||
<audio v-else-if="req.type == 'audio'" :src="raw()" autoplay controls></audio>
|
||||
<video v-else-if="req.type == 'video'" :src="raw()" autoplay 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!
|
||||
@@ -75,7 +75,7 @@ export default {
|
||||
this.listing = req
|
||||
this.updateLinks()
|
||||
})
|
||||
.catch(error => { console.log(error) })
|
||||
.catch(this.$showError)
|
||||
},
|
||||
beforeDestroy () {
|
||||
window.removeEventListener('keyup', this.key)
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
<template>
|
||||
<div class="prompt">
|
||||
<h3>{{ $t('prompts.copy') }}</h3>
|
||||
<p>{{ $t('prompts.copyMessage') }}</p>
|
||||
<div class="card floating">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('prompts.copy') }}</h2>
|
||||
</div>
|
||||
|
||||
<file-list @update:selected="val => dest = val"></file-list>
|
||||
<div class="card-content">
|
||||
<p>{{ $t('prompts.copyMessage') }}</p>
|
||||
<file-list @update:selected="val => dest = val"></file-list>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button class="ok"
|
||||
@click="copy"
|
||||
:aria-label="$t('buttons.copy')"
|
||||
:title="$t('buttons.copy')">{{ $t('buttons.copy') }}</button>
|
||||
<button class="cancel"
|
||||
<div class="card-action">
|
||||
<button class="cancel flat"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||
<button class="flat"
|
||||
@click="copy"
|
||||
:aria-label="$t('buttons.copy')"
|
||||
:title="$t('buttons.copy')">{{ $t('buttons.copy') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -56,7 +60,7 @@ export default {
|
||||
})
|
||||
.catch(error => {
|
||||
buttons.done('copy')
|
||||
this.$store.commit('showError', error)
|
||||
this.$showError(error)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
<template>
|
||||
<div class="prompt">
|
||||
<h3>{{ $t('prompts.deleteTitle') }}</h3>
|
||||
<p v-show="req.kind !== 'listing'">{{ $t('prompts.deleteMessageSingle') }}</p>
|
||||
<p v-show="req.kind === 'listing'">{{ $t('prompts.deleteMessageMultiple', { count: selectedCount}) }}</p>
|
||||
<div>
|
||||
<button @click="submit"
|
||||
:aria-label="$t('buttons.delete')"
|
||||
:title="$t('buttons.delete')">{{ $t('buttons.delete') }}</button>
|
||||
<button class="cancel"
|
||||
@click="$store.commit('closeHovers')"
|
||||
<div class="card floating">
|
||||
<div class="card-content">
|
||||
<p v-if="req.kind !== 'listing'">{{ $t('prompts.deleteMessageSingle') }}</p>
|
||||
<p v-else>{{ $t('prompts.deleteMessageMultiple', { count: selectedCount}) }}</p>
|
||||
</div>
|
||||
<div class="card-action">
|
||||
<button @click="$store.commit('closeHovers')"
|
||||
class="flat cancel"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||
<button @click="submit"
|
||||
class="flat"
|
||||
:aria-label="$t('buttons.delete')"
|
||||
:title="$t('buttons.delete')">{{ $t('buttons.delete') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -43,7 +45,7 @@ export default {
|
||||
})
|
||||
.catch(error => {
|
||||
buttons.done('delete')
|
||||
this.$store.commit('showError', error)
|
||||
this.$showError(error)
|
||||
})
|
||||
|
||||
return
|
||||
@@ -70,7 +72,7 @@ export default {
|
||||
.catch(error => {
|
||||
buttons.done('delete')
|
||||
this.$store.commit('setReload', true)
|
||||
this.$store.commit('showError', error)
|
||||
this.$showError(error)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
<template>
|
||||
<div class="prompt" id="download">
|
||||
<h3>{{ $t('prompts.download') }}</h3>
|
||||
<p>{{ $t('prompts.downloadMessage') }}</p>
|
||||
<div class="card floating" id="download">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('prompts.download') }}</h2>
|
||||
</div>
|
||||
|
||||
<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 class="card-content">
|
||||
<p>{{ $t('prompts.downloadMessage') }}</p>
|
||||
|
||||
<button class="block cancel" @click="download('zip')" autofocus>zip</button>
|
||||
<button class="block cancel" @click="download('tar')" autofocus>tar</button>
|
||||
<button class="block cancel" @click="download('targz')" autofocus>tar.gz</button>
|
||||
<button class="block cancel" @click="download('tarbz2')" autofocus>tar.bz2</button>
|
||||
<button class="block cancel" @click="download('tarxz')" autofocus>tar.xz</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
<template>
|
||||
<div class="prompt error">
|
||||
<i class="material-icons">error_outline</i>
|
||||
<h3>{{ $t('prompts.error') }}</h3>
|
||||
<pre>{{ $store.state.showMessage }}</pre>
|
||||
<div>
|
||||
<button @click="close"
|
||||
autofocus
|
||||
:aria-label="$t('buttons.close')"
|
||||
:title="$t('buttons.close')">{{ $t('buttons.close') }}</button>
|
||||
<button @click="reportIssue"
|
||||
class="cancel"
|
||||
:aria-label="$t('buttons.reportIssue')"
|
||||
:title="$t('buttons.reportIssue')">{{ $t('buttons.reportIssue') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'error',
|
||||
methods: {
|
||||
reportIssue () {
|
||||
window.open('https://github.com/hacdias/filemanager/issues/new')
|
||||
},
|
||||
close () {
|
||||
this.$store.commit('closeHovers')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -53,7 +53,7 @@ export default {
|
||||
// so we fetch the data from the previous directory.
|
||||
api.fetch(url.removeLastDir(this.$route.path))
|
||||
.then(this.fillOptions)
|
||||
.catch(this.showError)
|
||||
.catch(this.$showError)
|
||||
},
|
||||
methods: {
|
||||
fillOptions (req) {
|
||||
@@ -96,7 +96,7 @@ export default {
|
||||
|
||||
api.fetch(uri)
|
||||
.then(this.fillOptions)
|
||||
.catch(this.showError)
|
||||
.catch(this.$showError)
|
||||
},
|
||||
touchstart (event) {
|
||||
let url = event.currentTarget.dataset.url
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
<template>
|
||||
<div class="prompt help">
|
||||
<h3>{{ $t('help.help') }}</h3>
|
||||
<div class="card floating help">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('help.help') }}</h2>
|
||||
</div>
|
||||
|
||||
<ul>
|
||||
<li><strong>F1</strong> - {{ $t('help.f1') }}</li>
|
||||
<li><strong>F2</strong> - {{ $t('help.f2') }}</li>
|
||||
<li><strong>DEL</strong> - {{ $t('help.del') }}</li>
|
||||
<li><strong>ESC</strong> - {{ $t('help.esc') }}</li>
|
||||
<li><strong>CTRL + S</strong> - {{ $t('help.ctrl.s') }}</li>
|
||||
<li><strong>CTRL + F</strong> - {{ $t('help.ctrl.f') }}</li>
|
||||
<li><strong>CTRL + Click</strong> - {{ $t('help.ctrl.click') }}</li>
|
||||
<li><strong>Click</strong> - {{ $t('help.click') }}</li>
|
||||
<li><strong>Double click</strong> - {{ $t('help.doubleClick') }}</li>
|
||||
</ul>
|
||||
<div class="card-content">
|
||||
<ul>
|
||||
<li><strong>F1</strong> - {{ $t('help.f1') }}</li>
|
||||
<li><strong>F2</strong> - {{ $t('help.f2') }}</li>
|
||||
<li><strong>DEL</strong> - {{ $t('help.del') }}</li>
|
||||
<li><strong>ESC</strong> - {{ $t('help.esc') }}</li>
|
||||
<li><strong>CTRL + S</strong> - {{ $t('help.ctrl.s') }}</li>
|
||||
<li><strong>CTRL + F</strong> - {{ $t('help.ctrl.f') }}</li>
|
||||
<li><strong>CTRL + Click</strong> - {{ $t('help.ctrl.click') }}</li>
|
||||
<li><strong>Click</strong> - {{ $t('help.click') }}</li>
|
||||
<li><strong>Double click</strong> - {{ $t('help.doubleClick') }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="card-action">
|
||||
<button type="submit"
|
||||
@click="$store.commit('closeHovers')"
|
||||
class="ok"
|
||||
class="flat"
|
||||
:aria-label="$t('buttons.ok')"
|
||||
:title="$t('buttons.ok')">{{ $t('buttons.ok') }}</button>
|
||||
</div>
|
||||
|
||||
@@ -1,29 +1,33 @@
|
||||
<template>
|
||||
<div class="prompt">
|
||||
<h3>{{ $t('prompts.fileInfo') }}</h3>
|
||||
<div class="card floating">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('prompts.fileInfo') }}</h2>
|
||||
</div>
|
||||
|
||||
<p v-show="selected.length > 1">{{ $t('prompts.filesSelected', { count: selected.length }) }}</p>
|
||||
<div class="card-content">
|
||||
<p v-if="selected.length > 1">{{ $t('prompts.filesSelected', { count: selected.length }) }}</p>
|
||||
|
||||
<p v-show="selected.length < 2"><strong>{{ $t('prompts.displayName') }}</strong> {{ name() }}</p>
|
||||
<p><strong>{{ $t('prompts.size') }}:</strong> <span id="content_length"></span>{{ humanSize() }}</p>
|
||||
<p v-show="selected.length < 2"><strong>{{ $t('prompts.lastModified') }}:</strong> {{ humanTime() }}</p>
|
||||
<p v-if="selected.length < 2"><strong>{{ $t('prompts.displayName') }}</strong> {{ name() }}</p>
|
||||
<p><strong>{{ $t('prompts.size') }}:</strong> <span id="content_length"></span>{{ humanSize() }}</p>
|
||||
<p v-if="selected.length < 2"><strong>{{ $t('prompts.lastModified') }}:</strong> {{ humanTime() }}</p>
|
||||
|
||||
<section v-show="dir() && selected.length === 0">
|
||||
<p><strong>{{ $t('prompts.numberFiles') }}:</strong> {{ req.numFiles }}</p>
|
||||
<p><strong>{{ $t('prompts.numberDirs') }}:</strong> {{ req.numDirs }}</p>
|
||||
</section>
|
||||
<template v-if="dir() && selected.length === 0">
|
||||
<p><strong>{{ $t('prompts.numberFiles') }}:</strong> {{ req.numFiles }}</p>
|
||||
<p><strong>{{ $t('prompts.numberDirs') }}:</strong> {{ req.numDirs }}</p>
|
||||
</template>
|
||||
|
||||
<section v-show="!dir()">
|
||||
<p><strong>MD5:</strong> <code><a @click="checksum($event, 'md5')">{{ $t('prompts.show') }}</a></code></p>
|
||||
<p><strong>SHA1:</strong> <code><a @click="checksum($event, 'sha1')">{{ $t('prompts.show') }}</a></code></p>
|
||||
<p><strong>SHA256:</strong> <code><a @click="checksum($event, 'sha256')">{{ $t('prompts.show') }}</a></code></p>
|
||||
<p><strong>SHA512:</strong> <code><a @click="checksum($event, 'sha512')">{{ $t('prompts.show') }}</a></code></p>
|
||||
</section>
|
||||
<template v-if="!dir()">
|
||||
<p><strong>MD5:</strong> <code><a @click="checksum($event, 'md5')">{{ $t('prompts.show') }}</a></code></p>
|
||||
<p><strong>SHA1:</strong> <code><a @click="checksum($event, 'sha1')">{{ $t('prompts.show') }}</a></code></p>
|
||||
<p><strong>SHA256:</strong> <code><a @click="checksum($event, 'sha256')">{{ $t('prompts.show') }}</a></code></p>
|
||||
<p><strong>SHA512:</strong> <code><a @click="checksum($event, 'sha512')">{{ $t('prompts.show') }}</a></code></p>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="card-action">
|
||||
<button type="submit"
|
||||
@click="$store.commit('closeHovers')"
|
||||
class="ok"
|
||||
class="flat"
|
||||
:aria-label="$t('buttons.ok')"
|
||||
:title="$t('buttons.ok')">{{ $t('buttons.ok') }}</button>
|
||||
</div>
|
||||
@@ -111,7 +115,7 @@ export default {
|
||||
|
||||
api.checksum(link, hash)
|
||||
.then((hash) => { event.target.innerHTML = hash })
|
||||
.catch(error => { this.$store.commit('showError', error) })
|
||||
.catch(this.$showError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
<template>
|
||||
<div class="prompt">
|
||||
<h3>{{ $t('prompts.move') }}</h3>
|
||||
<p>{{ $t('prompts.moveMessage') }}</p>
|
||||
<div class="card floating">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('prompts.move') }}</h2>
|
||||
</div>
|
||||
|
||||
<file-list @update:selected="val => dest = val"></file-list>
|
||||
<div class="card-content">
|
||||
<file-list @update:selected="val => dest = val"></file-list>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button class="ok"
|
||||
@click="move"
|
||||
:aria-label="$t('buttons.move')"
|
||||
:title="$t('buttons.move')">{{ $t('buttons.move') }}</button>
|
||||
<button class="cancel"
|
||||
<div class="card-action">
|
||||
<button class="flat cancel"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||
<button class="flat"
|
||||
@click="move"
|
||||
:aria-label="$t('buttons.move')"
|
||||
:title="$t('buttons.move')">{{ $t('buttons.move') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -56,7 +59,7 @@ export default {
|
||||
})
|
||||
.catch(error => {
|
||||
buttons.done('move')
|
||||
this.$store.commit('showError', error)
|
||||
this.$showError(error)
|
||||
})
|
||||
|
||||
event.preventDefault()
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
<template>
|
||||
<div class="prompt">
|
||||
<h3>{{ $t('prompts.newFile') }}</h3>
|
||||
<p>{{ $t('prompts.newArchetype') }}</p>
|
||||
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
|
||||
<input type="text" @keyup.enter="submit" v-model.trim="archetype">
|
||||
<div>
|
||||
<button class="ok"
|
||||
@click="submit"
|
||||
:aria-label="$t('buttons.create')"
|
||||
:title="$t('buttons.create')">{{ $t('buttons.create') }}</button>
|
||||
<button class="cancel"
|
||||
<div class="card floating">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('prompts.newFile') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p>{{ $t('prompts.newArchetype') }}</p>
|
||||
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
|
||||
<input type="text" @keyup.enter="submit" v-model.trim="archetype">
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<button class="flat cancel"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||
<button class="flat"
|
||||
@click="submit"
|
||||
:aria-label="$t('buttons.create')"
|
||||
:title="$t('buttons.create')">{{ $t('buttons.create') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -37,9 +43,7 @@ export default {
|
||||
.then((url) => {
|
||||
this.$router.push({ path: url })
|
||||
})
|
||||
.catch(error => {
|
||||
this.$store.commit('showError', error)
|
||||
})
|
||||
.catch(this.$showError)
|
||||
},
|
||||
new (url, type) {
|
||||
url = removePrefix(url)
|
||||
@@ -47,7 +51,7 @@ export default {
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
request.open('POST', `${this.$store.state.baseURL}/api/resource${url}`, true)
|
||||
request.setRequestHeader('Authorization', `Bearer ${this.$store.state.jwt}`)
|
||||
if (!this.$store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${this.$store.state.jwt}`)
|
||||
request.setRequestHeader('Archetype', encodeURIComponent(type))
|
||||
|
||||
request.onload = () => {
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
<template>
|
||||
<div class="prompt">
|
||||
<h3>{{ $t('prompts.newDir') }}</h3>
|
||||
<p>{{ $t('prompts.newDirMessage') }}</p>
|
||||
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
|
||||
<div>
|
||||
<button class="ok"
|
||||
:aria-label="$t('buttons.create')"
|
||||
:title="$t('buttons.create')"
|
||||
@click="submit">{{ $t('buttons.create') }}</button>
|
||||
<button class="cancel"
|
||||
<div class="card floating">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('prompts.newDir') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p>{{ $t('prompts.newDirMessage') }}</p>
|
||||
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<button class="cancel flat"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||
<button class="flat"
|
||||
:aria-label="$t('buttons.create')"
|
||||
:title="$t('buttons.create')"
|
||||
@click="submit">{{ $t('buttons.create') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -43,7 +49,7 @@ export default {
|
||||
|
||||
api.post(uri)
|
||||
.then(() => { this.$router.push({ path: uri }) })
|
||||
.catch(error => { this.$store.commit('showError', error) })
|
||||
.catch(this.$showError)
|
||||
|
||||
// Close the prompt
|
||||
this.$store.commit('closeHovers')
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
<template>
|
||||
<div class="prompt">
|
||||
<h3>{{ $t('prompts.newFile') }}</h3>
|
||||
<p>{{ $t('prompts.newFileMessage') }}</p>
|
||||
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
|
||||
<div>
|
||||
<button class="ok"
|
||||
@click="submit"
|
||||
:aria-label="$t('buttons.create')"
|
||||
:title="$t('buttons.create')">{{ $t('buttons.create') }}</button>
|
||||
<button class="cancel"
|
||||
<div class="card floating">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('prompts.newFile') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p>{{ $t('prompts.newFileMessage') }}</p>
|
||||
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<button class="cancel flat"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||
<button class="flat"
|
||||
@click="submit"
|
||||
:aria-label="$t('buttons.create')"
|
||||
:title="$t('buttons.create')">{{ $t('buttons.create') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -44,7 +50,7 @@ export default {
|
||||
// Create the new file.
|
||||
api.post(uri)
|
||||
.then(() => { this.$router.push({ path: uri }) })
|
||||
.catch(error => { this.$store.commit('showError', error) })
|
||||
.catch(this.$showError)
|
||||
|
||||
// Close the prompt.
|
||||
this.$store.commit('closeHovers')
|
||||
|
||||
@@ -9,8 +9,6 @@
|
||||
<info v-else-if="showInfo"></info>
|
||||
<move v-else-if="showMove"></move>
|
||||
<copy v-else-if="showCopy"></copy>
|
||||
<error v-else-if="showError"></error>
|
||||
<success v-else-if="showSuccess"></success>
|
||||
<replace v-else-if="showReplace"></replace>
|
||||
<schedule v-else-if="show === 'schedule'"></schedule>
|
||||
<new-archetype v-else-if="show === 'new-archetype'"></new-archetype>
|
||||
@@ -27,8 +25,6 @@ import Rename from './Rename'
|
||||
import Download from './Download'
|
||||
import Move from './Move'
|
||||
import Copy from './Copy'
|
||||
import Error from './Error'
|
||||
import Success from './Success'
|
||||
import NewFile from './NewFile'
|
||||
import NewDir from './NewDir'
|
||||
import NewArchetype from './NewArchetype'
|
||||
@@ -47,9 +43,7 @@ export default {
|
||||
NewArchetype,
|
||||
Schedule,
|
||||
Rename,
|
||||
Error,
|
||||
Download,
|
||||
Success,
|
||||
Move,
|
||||
Copy,
|
||||
Share,
|
||||
@@ -70,8 +64,6 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
...mapState(['show', 'plugins']),
|
||||
showError: function () { return this.show === 'error' },
|
||||
showSuccess: function () { return this.show === 'success' },
|
||||
showInfo: function () { return this.show === 'info' },
|
||||
showHelp: function () { return this.show === 'help' },
|
||||
showDelete: function () { return this.show === 'delete' },
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
<template>
|
||||
<div class="prompt">
|
||||
<h3>{{ $t('prompts.rename') }}</h3>
|
||||
<p>{{ $t('prompts.renameMessage') }} <code>{{ oldName() }}</code>:</p>
|
||||
<div class="card floating">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('prompts.rename') }}</h2>
|
||||
</div>
|
||||
|
||||
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
|
||||
<div>
|
||||
<button @click="submit"
|
||||
type="submit"
|
||||
:aria-label="$t('buttons.rename')"
|
||||
:title="$t('buttons.rename')">{{ $t('buttons.rename') }}</button>
|
||||
<button class="cancel"
|
||||
<div class="card-content">
|
||||
<p>{{ $t('prompts.renameMessage') }} <code>{{ oldName() }}</code>:</p>
|
||||
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<button class="cancel flat"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||
<button @click="submit"
|
||||
class="flat"
|
||||
type="submit"
|
||||
:aria-label="$t('buttons.rename')"
|
||||
:title="$t('buttons.rename')">{{ $t('buttons.rename') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -68,7 +74,7 @@ export default {
|
||||
}
|
||||
this.$store.commit('setReload', true)
|
||||
}).catch(error => {
|
||||
this.$store.commit('showError', error)
|
||||
this.$showError(error)
|
||||
})
|
||||
|
||||
this.$store.commit('closeHovers')
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
<template>
|
||||
<div class="prompt">
|
||||
<h3>{{ $t('prompts.replace') }}</h3>
|
||||
<p>{{ $t('prompts.replaceMessage') }}</p>
|
||||
<div class="card floating">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('prompts.replace') }}</h2>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button class="ok"
|
||||
@click="showConfirm"
|
||||
:aria-label="$t('buttons.replace')"
|
||||
:title="$t('buttons.replace')">{{ $t('buttons.replace') }}</button>
|
||||
<button class="cancel"
|
||||
<div class="card-content">
|
||||
<p>{{ $t('prompts.replaceMessage') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<button class="flat cancel"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||
<button class="flat"
|
||||
@click="showConfirm"
|
||||
:aria-label="$t('buttons.replace')"
|
||||
:title="$t('buttons.replace')">{{ $t('buttons.replace') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
<template>
|
||||
<div class="prompt">
|
||||
<h3>{{ $t('prompts.schedule') }}</h3>
|
||||
<p>{{ $t('prompts.scheduleMessage') }}</p>
|
||||
<input autofocus type="datetime-local" v-model="date">
|
||||
<div>
|
||||
<button class="ok"
|
||||
@click="submit"
|
||||
:aria-label="$t('buttons.schedule')"
|
||||
:title="$t('buttons.schedule')">{{ $t('buttons.schedule') }}</button>
|
||||
<button class="cancel"
|
||||
<div class="card floating">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('prompts.schedule') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p>{{ $t('prompts.scheduleMessage') }}</p>
|
||||
<input autofocus type="datetime-local" v-model="date">
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<button class="cancel flat"
|
||||
@click="close"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||
<button class="falt"
|
||||
@click="submit"
|
||||
:aria-label="$t('buttons.schedule')"
|
||||
:title="$t('buttons.schedule')">{{ $t('buttons.schedule') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,51 +1,55 @@
|
||||
<template>
|
||||
<div class="prompt" id="share">
|
||||
<h3>{{ $t('buttons.share') }}</h3>
|
||||
<p></p>
|
||||
<ul>
|
||||
<li v-if="!hasPermanent">
|
||||
<a @click="getPermalink" :aria-label="$t('buttons.permalink')">{{ $t('buttons.permalink') }}</a>
|
||||
</li>
|
||||
<div class="card floating" id="share">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('buttons.share') }}</h2>
|
||||
</div>
|
||||
|
||||
<li v-for="link in links" :key="link.hash">
|
||||
<a :href="buildLink(link.hash)" target="_blank">
|
||||
<template v-if="link.expires">{{ humanTime(link.expireDate) }}</template>
|
||||
<template v-else>{{ $t('permanent') }}</template>
|
||||
</a>
|
||||
<div class="card-content">
|
||||
<ul>
|
||||
<li v-if="!hasPermanent">
|
||||
<a @click="getPermalink" :aria-label="$t('buttons.permalink')">{{ $t('buttons.permalink') }}</a>
|
||||
</li>
|
||||
|
||||
<button class="action"
|
||||
@click="deleteLink($event, link)"
|
||||
:aria-label="$t('buttons.delete')"
|
||||
:title="$t('buttons.delete')"><i class="material-icons">delete</i></button>
|
||||
<li v-for="link in links" :key="link.hash">
|
||||
<a :href="buildLink(link.hash)" target="_blank">
|
||||
<template v-if="link.expires">{{ humanTime(link.expireDate) }}</template>
|
||||
<template v-else>{{ $t('permanent') }}</template>
|
||||
</a>
|
||||
|
||||
<button class="action copy"
|
||||
:data-clipboard-text="buildLink(link.hash)"
|
||||
:aria-label="$t('buttons.copyToClipboard')"
|
||||
:title="$t('buttons.copyToClipboard')"><i class="material-icons">content_paste</i></button>
|
||||
</li>
|
||||
<button class="action"
|
||||
@click="deleteLink($event, link)"
|
||||
:aria-label="$t('buttons.delete')"
|
||||
:title="$t('buttons.delete')"><i class="material-icons">delete</i></button>
|
||||
|
||||
<li>
|
||||
<input autofocus
|
||||
type="number"
|
||||
max="2147483647"
|
||||
min="0"
|
||||
@keyup.enter="submit"
|
||||
v-model.trim="time">
|
||||
<select v-model="unit" :aria-label="$t('time.unit')">
|
||||
<option value="seconds">{{ $t('time.seconds') }}</option>
|
||||
<option value="minutes">{{ $t('time.minutes') }}</option>
|
||||
<option value="hours">{{ $t('time.hours') }}</option>
|
||||
<option value="days">{{ $t('time.days') }}</option>
|
||||
</select>
|
||||
<button class="action"
|
||||
@click="submit"
|
||||
:aria-label="$t('buttons.create')"
|
||||
:title="$t('buttons.create')"><i class="material-icons">add</i></button>
|
||||
</li>
|
||||
</ul>
|
||||
<button class="action copy-clipboard"
|
||||
:data-clipboard-text="buildLink(link.hash)"
|
||||
:aria-label="$t('buttons.copyToClipboard')"
|
||||
:title="$t('buttons.copyToClipboard')"><i class="material-icons">content_paste</i></button>
|
||||
</li>
|
||||
|
||||
<div>
|
||||
<button class="cancel"
|
||||
<li>
|
||||
<input autofocus
|
||||
type="number"
|
||||
max="2147483647"
|
||||
min="0"
|
||||
@keyup.enter="submit"
|
||||
v-model.trim="time">
|
||||
<select v-model="unit" :aria-label="$t('time.unit')">
|
||||
<option value="seconds">{{ $t('time.seconds') }}</option>
|
||||
<option value="minutes">{{ $t('time.minutes') }}</option>
|
||||
<option value="hours">{{ $t('time.hours') }}</option>
|
||||
<option value="days">{{ $t('time.days') }}</option>
|
||||
</select>
|
||||
<button class="action"
|
||||
@click="submit"
|
||||
:aria-label="$t('buttons.create')"
|
||||
:title="$t('buttons.create')"><i class="material-icons">add</i></button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<button class="flat"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.close')"
|
||||
:title="$t('buttons.close')">{{ $t('buttons.close') }}</button>
|
||||
@@ -54,7 +58,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapMutations } from 'vuex'
|
||||
import { mapState } from 'vuex'
|
||||
import { getShare, deleteShare, share } from '@/utils/api'
|
||||
import moment from 'moment'
|
||||
import Clipboard from 'clipboard'
|
||||
@@ -101,20 +105,25 @@ export default {
|
||||
})
|
||||
.catch(error => {
|
||||
if (error === 404) return
|
||||
this.showError(error)
|
||||
this.$showError(error)
|
||||
})
|
||||
},
|
||||
mounted () {
|
||||
this.clip = new Clipboard('.copy')
|
||||
this.clip = new Clipboard('.copy-clipboard')
|
||||
this.clip.on('success', (e) => {
|
||||
this.$showSuccess(this.$t('success.linkCopied'))
|
||||
})
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.clip.destroy()
|
||||
},
|
||||
methods: {
|
||||
...mapMutations([ 'showError' ]),
|
||||
submit: function (event) {
|
||||
if (!this.time) return
|
||||
|
||||
share(this.url, this.time, this.unit)
|
||||
.then(result => { this.links.push(result); this.sort() })
|
||||
.catch(error => { this.showError(error) })
|
||||
.catch(this.$showError)
|
||||
},
|
||||
getPermalink (event) {
|
||||
share(this.url)
|
||||
@@ -123,7 +132,7 @@ export default {
|
||||
this.sort()
|
||||
this.hasPermanent = true
|
||||
})
|
||||
.catch(error => { this.showError(error) })
|
||||
.catch(this.$showError)
|
||||
},
|
||||
deleteLink (event, link) {
|
||||
event.preventDefault()
|
||||
@@ -132,7 +141,7 @@ export default {
|
||||
if (!link.expires) this.hasPermanent = false
|
||||
this.links = this.links.filter(item => item.hash !== link.hash)
|
||||
})
|
||||
.catch(error => { this.showError(error) })
|
||||
.catch(this.$showError)
|
||||
},
|
||||
humanTime (time) {
|
||||
return moment(time).fromNow()
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
<template>
|
||||
<div class="prompt success">
|
||||
<i class="material-icons">done</i>
|
||||
<h3>{{ $store.state.showMessage }}</h3>
|
||||
<div>
|
||||
<button @click="close"
|
||||
:aria-label="$t('buttons.ok')"
|
||||
:title="$t('buttons.ok')"
|
||||
autofocus>{{ $t('buttons.ok') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'success',
|
||||
methods: {
|
||||
close () {
|
||||
this.$store.commit('closeHovers')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,9 +1,8 @@
|
||||
body {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
padding-top: 4em;
|
||||
background-color: #f8f8f8;
|
||||
user-select: none;
|
||||
color: #212121;
|
||||
background-color: #fafafa;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
* {
|
||||
@@ -66,6 +65,53 @@ button:hover {
|
||||
background-color: #1E88E5;
|
||||
}
|
||||
|
||||
input[type="submit"].block,
|
||||
button.block {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin: 0 0 1em;
|
||||
}
|
||||
|
||||
button.delete {
|
||||
background: #F44336;
|
||||
}
|
||||
|
||||
button.delete:hover {
|
||||
background: #D32F2F;
|
||||
}
|
||||
|
||||
button.cancel {
|
||||
background-color: #ECEFF1;
|
||||
color: #37474F;
|
||||
}
|
||||
|
||||
button.cancel:hover {
|
||||
background-color: #e9eaeb;
|
||||
}
|
||||
|
||||
button.flat,
|
||||
input[type="submit"].flat {
|
||||
color: #1E88E5;
|
||||
background: transparent;
|
||||
box-shadow: 0 0 0;
|
||||
border: 0;
|
||||
margin-left: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
button.flat:hover,
|
||||
input[type="submit"].flat:hover {
|
||||
background: rgba(0,0,0,0.05)
|
||||
}
|
||||
|
||||
button.flat.delete {
|
||||
color: #F44336;
|
||||
}
|
||||
|
||||
button.flat.cancel {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.mobile-only {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@@ -1,64 +1,34 @@
|
||||
.dashboard {
|
||||
max-width: 600px;
|
||||
box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px;
|
||||
border-radius: .5em;
|
||||
background: #fff;
|
||||
padding: 1em;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.dashboard a {
|
||||
a {
|
||||
color: inherit
|
||||
}
|
||||
|
||||
.dashboard h1 button {
|
||||
font-size: 0.5em;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.dashboard table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dashboard table th {
|
||||
font-weight: 500;
|
||||
color: #757575;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.dashboard table th,
|
||||
.dashboard table td {
|
||||
padding: .5em 0;
|
||||
}
|
||||
|
||||
.dashboard table td:last-child {
|
||||
width: 1em
|
||||
}
|
||||
|
||||
.dashboard > h1:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.dashboard form > p:last-child,
|
||||
form.dashboard > p:last-child {
|
||||
text-align: right
|
||||
}
|
||||
|
||||
.dashboard > *:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.dashboard select,
|
||||
.dashboard textarea,
|
||||
.dashboard input[type="text"],
|
||||
.dashboard input[type="password"] {
|
||||
padding: 0;
|
||||
line-height: 1.7;
|
||||
select,
|
||||
textarea,
|
||||
input[type="text"],
|
||||
input[type="password"] {
|
||||
padding: 0.5em 0;
|
||||
line-height: 1;
|
||||
display: block;
|
||||
border: 0;
|
||||
border-bottom: 1px solid #dddddd;
|
||||
transition: .2s ease border;
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
textarea {
|
||||
line-height: 1.15;
|
||||
padding: .5em;
|
||||
border: 1px solid #ddd;
|
||||
font-family: monospace;
|
||||
min-height: 10em;
|
||||
resize: none;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.dashboard #locale,
|
||||
@@ -69,49 +39,32 @@ form.dashboard > p:last-child {
|
||||
}
|
||||
|
||||
.dashboard #locale {
|
||||
border: 1px solid #dddddd;
|
||||
margin-top: .5em;
|
||||
}
|
||||
|
||||
.dashboard textarea:focus,
|
||||
.dashboard textarea:hover,
|
||||
.dashboard input[type="text"]:focus,
|
||||
.dashboard input[type="password"]:focus,
|
||||
.dashboard input[type="text"]:hover,
|
||||
.dashboard input[type="password"]:hover {
|
||||
textarea:focus,
|
||||
textarea:hover,
|
||||
input[type="text"]:focus,
|
||||
input[type="password"]:focus,
|
||||
input[type="text"]:hover,
|
||||
input[type="password"]:hover {
|
||||
border-color: #2979ff;
|
||||
}
|
||||
|
||||
.dashboard input.red {
|
||||
input.red {
|
||||
border-color: red;
|
||||
}
|
||||
|
||||
.dashboard input.green {
|
||||
input.green {
|
||||
border-color: green;
|
||||
}
|
||||
|
||||
.dashboard button.delete {
|
||||
background: #F44336;
|
||||
}
|
||||
|
||||
.dashboard button.delete:hover {
|
||||
background: #D32F2F;
|
||||
}
|
||||
|
||||
.dashboard textarea {
|
||||
line-height: 1.15;
|
||||
padding: .5em;
|
||||
border: 1px solid #ddd;
|
||||
font-family: monospace;
|
||||
min-height: 10em;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.dashboard p label {
|
||||
margin-bottom: .2em;
|
||||
display: block;
|
||||
font-size: .8em;
|
||||
font-weight: bold;
|
||||
font-weight: 500;
|
||||
color: rgba(0, 0, 0, 0.57);
|
||||
}
|
||||
|
||||
li code,
|
||||
@@ -131,21 +84,337 @@ p code {
|
||||
display: flex;
|
||||
color: rgb(84, 110, 122);
|
||||
font-weight: 500;
|
||||
padding: 0 0 1em;
|
||||
margin: 0 0 1em;
|
||||
font-size: .8em;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
text-align: center;
|
||||
justify-content: space-between;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.dashboard #nav li {
|
||||
width: 100%;
|
||||
padding: 0 0 1em;
|
||||
border-bottom: 2px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.dashboard #nav li:last-child {
|
||||
text-align: right
|
||||
.dashboard #nav li.active {
|
||||
border-color: #2196f3
|
||||
}
|
||||
|
||||
.dashboard #nav i {
|
||||
font-size: 1em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table tr {
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
|
||||
table tr:last-child {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
table th {
|
||||
font-weight: 500;
|
||||
color: #757575;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
table th,
|
||||
table td {
|
||||
padding: .5em 0;
|
||||
}
|
||||
|
||||
table td.small {
|
||||
width: 1em;
|
||||
}
|
||||
|
||||
table tr>*:first-child {
|
||||
padding-left: 1em;
|
||||
}
|
||||
|
||||
table tr>*:last-child {
|
||||
padding-right: 1em;
|
||||
}
|
||||
|
||||
.card {
|
||||
position: relative;
|
||||
margin: .5rem 0 1rem 0;
|
||||
background-color: #fff;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.card.floating {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 99999;
|
||||
max-width: 25em;
|
||||
width: 90%;
|
||||
max-height: 95%;
|
||||
z-index: 99999;
|
||||
animation: .1s show forwards;
|
||||
}
|
||||
|
||||
.card>*>*:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.card>*>*:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.card .card-title {
|
||||
padding: 1.5em 1em 1em;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.card .card-title>*:first-child {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.card>div {
|
||||
padding: 1em 1em;
|
||||
}
|
||||
|
||||
.card>div:first-child {
|
||||
padding-top: 1.5em;
|
||||
}
|
||||
|
||||
.card>div:last-child {
|
||||
padding-bottom: 1.5em;
|
||||
}
|
||||
|
||||
.card .card-title * {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.card .card-action {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.card .card-content.full {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.card h3 {
|
||||
color: rgba(0, 0, 0, 0.53);
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
margin: 2em 0 1em;
|
||||
}
|
||||
|
||||
.card-content table {
|
||||
margin: 0 -1em;
|
||||
width: calc(100% + 2em);
|
||||
}
|
||||
|
||||
.card code {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.card#download {
|
||||
max-width: 15em;
|
||||
}
|
||||
|
||||
.card#share ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.card#share ul li {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card#share ul li a {
|
||||
color: #2196F3;
|
||||
cursor: pointer;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.card#share ul li .action i {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.card#share ul li input,
|
||||
.card#share ul li select {
|
||||
padding: .2em;
|
||||
margin-right: .5em;
|
||||
border: 1px solid #dadada;
|
||||
}
|
||||
|
||||
.card#share .action.copy-clipboard::after {
|
||||
content: 'Copied!';
|
||||
position: absolute;
|
||||
left: -25%;
|
||||
width: 150%;
|
||||
font-size: .6em;
|
||||
text-align: center;
|
||||
background: #44a6f5;
|
||||
color: #fff;
|
||||
padding: .5em .2em;
|
||||
border-radius: .4em;
|
||||
top: -2em;
|
||||
transition: .1s ease opacity;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.card#share .action.copy-clipboard.active::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.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 - 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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.collapsible {
|
||||
border-top: 1px solid rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.collapsible:last-of-type {
|
||||
border-bottom: 1px solid rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.collapsible > input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.collapsible > label {
|
||||
padding: 1em 0;
|
||||
cursor: pointer;
|
||||
border-right: 0;
|
||||
border-left: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.collapsible > label * {
|
||||
margin: 0;
|
||||
color: rgba(0,0,0,0.57);
|
||||
}
|
||||
|
||||
.collapsible > label i {
|
||||
transition: .2s ease transform;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.collapsible .collapse {
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
transition: .2s ease all;
|
||||
}
|
||||
|
||||
.collapsible > input:checked ~ .collapse {
|
||||
padding-top: 1em;
|
||||
padding-bottom: 1em;
|
||||
max-height: 20em;
|
||||
}
|
||||
|
||||
.collapsible > input:checked ~ label i {
|
||||
transform: rotate(180deg)
|
||||
}
|
||||
|
||||
.card .collapsible {
|
||||
width: calc(100% + 2em);
|
||||
margin: 0 -1em;
|
||||
}
|
||||
|
||||
.card .collapsible > label {
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.card .collapsible .collapse {
|
||||
padding: 0 1em;
|
||||
}
|
||||
|
||||
@@ -122,8 +122,9 @@ header .search-button {
|
||||
#search input {
|
||||
width: 100%;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
background-color: transparent;
|
||||
line-height: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#search #result {
|
||||
|
||||
@@ -159,7 +159,7 @@
|
||||
|
||||
#listing.list .item.header {
|
||||
display: flex !important;
|
||||
background: #f8f8f8;
|
||||
background: #fafafa;
|
||||
position: fixed;
|
||||
width: calc(100% - 19em);
|
||||
top: 7em;
|
||||
|
||||
@@ -29,6 +29,14 @@
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
#login.recaptcha form {
|
||||
min-width: 304px;
|
||||
}
|
||||
|
||||
#login #recaptcha {
|
||||
margin: .5em 0 0;
|
||||
}
|
||||
|
||||
#login input {
|
||||
width: 100%;
|
||||
width: 100%;
|
||||
|
||||
@@ -1,208 +0,0 @@
|
||||
.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:not([type="submit"]) {
|
||||
width: 100%;
|
||||
border: 1px solid #dadada;
|
||||
line-height: 1;
|
||||
padding: .3em;
|
||||
margin: .3em 0;
|
||||
}
|
||||
|
||||
.prompt code {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.prompt div:last-of-type {
|
||||
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.success i,
|
||||
.prompt.error i {
|
||||
color: #F44336;
|
||||
display: block;
|
||||
margin: 0 auto .15em;
|
||||
text-align: center;
|
||||
font-size: 5em;
|
||||
}
|
||||
|
||||
.prompt.success h3,
|
||||
.prompt.error h3 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.prompt.error button:not(.cancel) {
|
||||
background-color: #F44336
|
||||
}
|
||||
|
||||
.prompt.success i {
|
||||
color: #8BC34A;
|
||||
}
|
||||
|
||||
.prompt.success button {
|
||||
background-color: #8BC34A;
|
||||
}
|
||||
|
||||
|
||||
/* * * * * * * * * * * * * * * *
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
.prompt#share ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.prompt#share ul li {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.prompt#share ul li a {
|
||||
color: #2196F3;
|
||||
cursor: pointer;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.prompt#share ul li .action i {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.prompt#share ul li input,
|
||||
.prompt#share ul li select {
|
||||
padding: .2em;
|
||||
margin-right: .5em;
|
||||
border: 1px solid #dadada;
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
@import "~normalize.css/normalize.css";
|
||||
@import "~noty/lib/noty.css";
|
||||
@import "./fonts.css";
|
||||
@import "./base.css";
|
||||
@import "./header.css";
|
||||
@import "./prompts.css";
|
||||
@import "./listing.css";
|
||||
@import "./editor.css";
|
||||
@import "./dashboard.css";
|
||||
@@ -180,6 +180,17 @@
|
||||
* PROMPT *
|
||||
* * * * * * * * * * * * * * * */
|
||||
|
||||
.noty_buttons {
|
||||
text-align: right;
|
||||
padding: 0 10px 10px !important;
|
||||
}
|
||||
|
||||
.noty_buttons button {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid rgba(0,0,0,0.1);
|
||||
box-shadow: 0 0 0 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* * * * * * * * * * * * * * * *
|
||||
* FOOTER *
|
||||
|
||||
@@ -26,11 +26,13 @@ buttons:
|
||||
publish: Publish
|
||||
selectMultiple: Select multiple
|
||||
schedule: Schedule
|
||||
switchView: Swicth view
|
||||
switchView: Switch view
|
||||
toggleSidebar: Toggle sidebar
|
||||
update: Update
|
||||
upload: Upload
|
||||
permalink: Get Permanent Link
|
||||
success:
|
||||
linkCopied: Link copied!
|
||||
errors:
|
||||
forbidden: You're not welcome here.
|
||||
internal: Something really went wrong.
|
||||
@@ -115,13 +117,14 @@ settings:
|
||||
commandsHelp: >
|
||||
Here you can set commands that are executed in the named events. You
|
||||
write one command per line. If the event is related to files, such as before and
|
||||
after saving, the environment variable "file" will be available with the path
|
||||
after saving, the environment variable "FILE" will be available with the path
|
||||
of the file.
|
||||
commandsUpdated: Commands updated!
|
||||
customStylesheet: Custom Stylesheet
|
||||
examples: Examples
|
||||
globalSettings: Global Settings
|
||||
language: Language
|
||||
lockPassword: Prevent the user from changing the password
|
||||
newPassword: Your new password
|
||||
newPasswordConfirm: Confirm your new password
|
||||
newUser: New User
|
||||
@@ -165,7 +168,6 @@ sidebar:
|
||||
myFiles: My files
|
||||
newFile: New file
|
||||
newFolder: New folder
|
||||
servedWith: Served with
|
||||
settings: Settings
|
||||
siteSettings: Site Settings
|
||||
hugoNew: Hugo New
|
||||
|
||||
@@ -115,7 +115,7 @@ settings:
|
||||
commandsHelp: >
|
||||
Ici vous pouvez définir des commandes qui seront exécutées lors de l'évènement correspondant.
|
||||
Vous devez indiquer une commande par ligne. Si l'évènement est en rapport avec des fichiers,
|
||||
par exemple avant et après enregistrement, la variable d'environement "file" sera disponible
|
||||
par exemple avant et après enregistrement, la variable d'environement "FILE" sera disponible
|
||||
et contiendra le chemin d'accès vers le fichier.
|
||||
commandsUpdated: Commandes mises à jour !
|
||||
customStylesheet: Feuille de style personnalisée
|
||||
@@ -161,7 +161,6 @@ sidebar:
|
||||
myFiles: Mes fichiers
|
||||
newFile: Nouveau fichier
|
||||
newFolder: Nouveau dossier
|
||||
servedWith: Géré avec
|
||||
settings: Paramètres
|
||||
siteSettings: Paramètres du site
|
||||
hugoNew: Nouveau Hugo
|
||||
|
||||
@@ -31,6 +31,8 @@ buttons:
|
||||
update: 更新
|
||||
upload: アップロード
|
||||
permalink: 固定リンク
|
||||
success:
|
||||
linkCopied: リンクがコピーされました!
|
||||
errors:
|
||||
forbidden: アクセスが拒否されました。
|
||||
internal: 内部エラーが発生しました。
|
||||
@@ -116,12 +118,13 @@ settings:
|
||||
ここで、名前付きイベントに実行するコマンドを設定することができます。\
|
||||
一行にコマンド一つを入力してください。\
|
||||
イベントはファイルに関連する場合、例えばファイル保存の前にまたは後で、\
|
||||
環境変数 file はファイルのパスに割り当てられます。"
|
||||
環境変数 FILE はファイルのパスに割り当てられます。"
|
||||
commandsUpdated: コマンドは更新されました!
|
||||
customStylesheet: カスタムスタイルシ ート
|
||||
examples: 例
|
||||
globalSettings: グローバル設定
|
||||
language: 言語
|
||||
lockPassowrd: 新しいパスワードを変更に禁止
|
||||
newPassword: 新しいパスワード
|
||||
newPasswordConfirm: 新しいパスワードを確認します
|
||||
newUser: 新しいユーザー
|
||||
@@ -165,7 +168,6 @@ sidebar:
|
||||
myFiles: 私のファイル
|
||||
newFile: 新しいファイルを作成
|
||||
newFolder: 新しいフォルダを作成
|
||||
servedWith: サービス提供者
|
||||
settings: 設定
|
||||
siteSettings: サイト設定
|
||||
hugoNew: Hugo New
|
||||
|
||||
@@ -31,6 +31,8 @@ buttons:
|
||||
update: Atualizar
|
||||
upload: Enviar
|
||||
permalink: Obter link permanente
|
||||
success:
|
||||
linkCopied: Link copiado!
|
||||
errors:
|
||||
forbidden: Tu não és bem-vindo aqui.
|
||||
internal: Algo correu bastante mal.
|
||||
@@ -137,12 +139,13 @@ settings:
|
||||
Pode definir um conjunto de comandos a executar em determiandos eventos.
|
||||
Deve escrever um comando por linha. Se o evento estiver relacionado com ficheiros,
|
||||
como antes e depois de guardar, irá existir uma variável de ambiente denominada
|
||||
"file" com o caminho do ficheiro.
|
||||
"FILE" com o caminho do ficheiro.
|
||||
commandsUpdated: Comandos atualizados!
|
||||
customStylesheet: Estilos Personalizados
|
||||
examples: Exemplos
|
||||
globalSettings: Configurações Globais
|
||||
language: Linguagem
|
||||
lockPassword: Não permitir que o utilizador altere a palavra-passe
|
||||
newPassword: Nova palavra-passe
|
||||
newPasswordConfirm: Confirme a nova palavra-passe
|
||||
newUser: Novo Utilizador
|
||||
@@ -190,7 +193,6 @@ sidebar:
|
||||
newFile: Novo ficheiro
|
||||
newFolder: Nova pasta
|
||||
preview: Pré-visualizar
|
||||
servedWith: Servido com
|
||||
settings: Configurações
|
||||
siteSettings: Configurações do Site
|
||||
time:
|
||||
|
||||
@@ -31,6 +31,8 @@ buttons:
|
||||
update: 更新
|
||||
upload: 上传
|
||||
permalink: 获取永久链接
|
||||
success:
|
||||
linkCopied: 链接已复制!
|
||||
errors:
|
||||
forbidden: 你被禁止访问。
|
||||
internal: 内部出现麻烦了。
|
||||
@@ -115,12 +117,13 @@ settings:
|
||||
commandsHelp: "\
|
||||
在这里,您可以设置在指定事件下执行的命令,一行一条。\
|
||||
若事件与文件相关,如“在保存文件前”,\
|
||||
则文件的路径会被赋值给环境变量 \"file\"。"
|
||||
则文件的路径会被赋值给环境变量 \"FILE\"。"
|
||||
commandsUpdated: 命令已更新!
|
||||
customStylesheet: 自定义样式表
|
||||
examples: 例子
|
||||
globalSettings: 全局设置
|
||||
language: 语言
|
||||
lockPassowrd: 禁止用户修改密码
|
||||
newPassword: 您的新密码
|
||||
newPasswordConfirm: 重输一遍新密码
|
||||
newUser: 新建用户
|
||||
@@ -163,7 +166,6 @@ sidebar:
|
||||
myFiles: 我的文件
|
||||
newFile: 新建文件
|
||||
newFolder: 新建文件夹
|
||||
servedWith: '服务提供者:'
|
||||
settings: 设置
|
||||
siteSettings: 网站设置
|
||||
hugoNew: Hugo New
|
||||
|
||||
@@ -4,7 +4,7 @@ buttons:
|
||||
close: 關閉
|
||||
copy: 複製
|
||||
copyFile: 複製檔案
|
||||
copyToClipboard: 複製到剪貼板
|
||||
copyToClipboard: 複製到剪貼簿
|
||||
create: 建立
|
||||
delete: 刪除
|
||||
download: 下載
|
||||
@@ -31,8 +31,10 @@ buttons:
|
||||
update: 更新
|
||||
upload: 上傳
|
||||
permalink: 獲取永久連結
|
||||
success:
|
||||
linkCopied: 連結已複製!
|
||||
errors:
|
||||
forbidden: 你被禁止訪問。
|
||||
forbidden: 你被禁止存取。
|
||||
internal: 內部出現麻煩了。
|
||||
notFound: 找不到檔案。
|
||||
files:
|
||||
@@ -45,7 +47,7 @@ files:
|
||||
lastModified: 最後修改
|
||||
loading: 讀取中...
|
||||
lonely: 這裡沒有任何檔案...
|
||||
metadata: 中繼資料
|
||||
metadata: 詮釋資料
|
||||
multipleSelectionEnabled: 多選模式已開啟
|
||||
name: 名稱
|
||||
size: 大小
|
||||
@@ -100,70 +102,70 @@ prompts:
|
||||
show: 顯示
|
||||
size: 大小
|
||||
schedule: 計畫
|
||||
scheduleMessage: 請選擇發佈這篇帖子的日期。
|
||||
newArchetype: 建立一個基於原型的新帖子。您的檔案將會建立在內容資料夾中。
|
||||
scheduleMessage: 請選擇發佈這篇貼文的日期。
|
||||
newArchetype: 建立一個基於原型的新貼文。您的檔案將會建立在內容資料夾中。
|
||||
settings:
|
||||
admin: 管理員
|
||||
administrator: 管理員
|
||||
allowCommands: 執行命令
|
||||
allowEdit: 編輯、重命名或刪除檔案/目錄
|
||||
allowNew: 創建新檔案和目錄
|
||||
allowPublish: 發佈新的帖子與頁面
|
||||
allowPublish: 發佈新的貼文與頁面
|
||||
avoidChanges: '(留空以避免更改)'
|
||||
changePassword: 更改密碼
|
||||
commands: 命令
|
||||
commandsHelp: "\
|
||||
在這裡,您可以設定在指定事件下執行的命令,一行一條。\
|
||||
若事件與檔案相關,如“在保存檔案前”,\
|
||||
則檔案的路徑會被賦值給環境變數 \"file\"。"
|
||||
則檔案的路徑會被賦值給環境變數 \"FILE\"。"
|
||||
commandsUpdated: 命令已更新!
|
||||
customStylesheet: 自定義樣式表
|
||||
examples: 例子
|
||||
examples: 範例
|
||||
globalSettings: 全域設定
|
||||
language: 語言
|
||||
lockPassword: 禁止使用者修改密碼
|
||||
newPassword: 您的新密碼
|
||||
newPasswordConfirm: 重輸一遍新密碼
|
||||
newUser: 建立用戶
|
||||
newUser: 建立使用者
|
||||
password: 密碼
|
||||
passwordUpdated: 密碼已更新!
|
||||
permissions: 權限
|
||||
permissionsHelp: "\
|
||||
您可以將該用戶設置為管理員,也可以單獨選擇各項權限。\
|
||||
您可以將該使用者設置為管理員,也可以單獨選擇各項權限。\
|
||||
如果選擇了“管理員”,則其他的選項會被自動勾上,\
|
||||
同時該用戶可以管理其他用戶。"
|
||||
同時該使用者可以管理其他使用者。"
|
||||
profileSettings: 設定檔設定
|
||||
ruleExample1: "\
|
||||
封鎖用戶訪問所有資料夾下任何以 . 開頭的檔案\
|
||||
封鎖使用者存取所有資料夾下任何以 . 開頭的檔案\
|
||||
(隱藏文件, 例如: .git, .gitignore)。"
|
||||
ruleExample2: 封鎖用戶訪問其目錄範圍的根目錄下名為 Caddyfile 的檔案。
|
||||
ruleExample2: 封鎖使用者存取其目錄範圍的根目錄下名為 Caddyfile 的檔案。
|
||||
rules: 規則
|
||||
rulesHelp1: "\
|
||||
您可以為該用戶製定一組黑名單或白名單式的規則,\
|
||||
被屏蔽的檔案將不會顯示在清單中,用戶也無權限訪問,\
|
||||
您可以為該使用者製定一組黑名單或白名單式的規則,\
|
||||
被屏蔽的檔案將不會顯示在清單中,使用者也無權限存取,\
|
||||
支持相對於目錄範圍的路徑。"
|
||||
rulesHelp2: "\
|
||||
每行一條規則,且必須以關鍵字 {0} 或 {1} 開頭。\
|
||||
如要使用規則運算式,請在加上 {2} 之後再附上運算式或路徑。"
|
||||
scope: 目錄範圍
|
||||
settingsUpdated: 設定已更新!
|
||||
user: 用戶
|
||||
userCommands: 用戶命令
|
||||
user: 使用者
|
||||
userCommands: 使用者命令
|
||||
userCommandsHelp: "\
|
||||
指定該用戶可以執行的命令,用空格分隔。\
|
||||
指定該使用者可以執行的命令,用空格分隔。\
|
||||
例如:"
|
||||
userCreated: 用戶已建立!
|
||||
userDeleted: 用戶已刪除!
|
||||
userManagement: 用戶管理
|
||||
username: 用戶名
|
||||
users: 用戶
|
||||
userUpdated: 用戶已更新!
|
||||
userCreated: 使用者已建立!
|
||||
userDeleted: 使用者已刪除!
|
||||
userManagement: 使用者管理
|
||||
username: 使用者名稱
|
||||
users: 使用者
|
||||
userUpdated: 使用者已更新!
|
||||
sidebar:
|
||||
help: 幫助
|
||||
logout: 登出
|
||||
myFiles: 我的檔案
|
||||
newFile: 建立檔案
|
||||
newFolder: 建立資料夾
|
||||
servedWith: '服務提供者:'
|
||||
settings: 設定
|
||||
siteSettings: 網站設定
|
||||
hugoNew: Hugo New
|
||||
@@ -187,9 +189,10 @@ languages:
|
||||
pt: Português
|
||||
ja: 日本語
|
||||
zhCN: 中文 (简体)
|
||||
zhTW: 中文 (繁體)
|
||||
time:
|
||||
unit: 時間單位
|
||||
seconds: 秒
|
||||
minutes: 分鐘
|
||||
hours: 小時
|
||||
days: 天
|
||||
days: 天
|
||||
|
||||
@@ -3,9 +3,46 @@ import App from './App'
|
||||
import store from './store'
|
||||
import router from './router'
|
||||
import i18n from './i18n'
|
||||
import Noty from 'noty'
|
||||
|
||||
Vue.config.productionTip = true
|
||||
|
||||
const notyDefault = {
|
||||
type: 'info',
|
||||
layout: 'bottomRight',
|
||||
timeout: 1000,
|
||||
progressBar: true
|
||||
}
|
||||
|
||||
Vue.prototype.$noty = function (opts) {
|
||||
new Noty(Object.assign({}, notyDefault, opts)).show()
|
||||
}
|
||||
|
||||
Vue.prototype.$showSuccess = function (message) {
|
||||
new Noty(Object.assign({}, notyDefault, {
|
||||
text: message,
|
||||
type: 'success'
|
||||
})).show()
|
||||
}
|
||||
|
||||
Vue.prototype.$showError = function (error) {
|
||||
let n = new Noty(Object.assign({}, notyDefault, {
|
||||
text: error,
|
||||
type: 'error',
|
||||
timeout: null,
|
||||
buttons: [
|
||||
Noty.button(i18n.t('buttons.reportIssue'), '', function () {
|
||||
window.open('https://github.com/hacdias/filemanager/issues/new')
|
||||
}),
|
||||
Noty.button(i18n.t('buttons.close'), '', function () {
|
||||
n.close()
|
||||
})
|
||||
]
|
||||
}))
|
||||
|
||||
n.show()
|
||||
}
|
||||
|
||||
/* eslint-disable no-new */
|
||||
new Vue({
|
||||
el: '#app',
|
||||
|
||||
@@ -3,10 +3,11 @@ import Router from 'vue-router'
|
||||
import Login from '@/views/Login'
|
||||
import Layout from '@/views/Layout'
|
||||
import Files from '@/views/Files'
|
||||
import Users from '@/views/Users'
|
||||
import User from '@/views/User'
|
||||
import GlobalSettings from '@/views/GlobalSettings'
|
||||
import ProfileSettings from '@/views/ProfileSettings'
|
||||
import Users from '@/views/Settings/Users'
|
||||
import User from '@/views/Settings/User'
|
||||
import Settings from '@/views/Settings'
|
||||
import GlobalSettings from '@/views/settings/Global'
|
||||
import ProfileSettings from '@/views/settings/Profile'
|
||||
import Error403 from '@/views/errors/403'
|
||||
import Error404 from '@/views/errors/404'
|
||||
import Error500 from '@/views/errors/500'
|
||||
@@ -49,22 +50,41 @@ const router = new Router({
|
||||
{
|
||||
path: '/settings',
|
||||
name: 'Settings',
|
||||
component: Settings,
|
||||
redirect: {
|
||||
path: '/settings/profile'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/settings/profile',
|
||||
name: 'Profile Settings',
|
||||
component: ProfileSettings
|
||||
},
|
||||
{
|
||||
path: '/settings/global',
|
||||
name: 'Global Settings',
|
||||
component: GlobalSettings,
|
||||
meta: {
|
||||
requiresAdmin: true
|
||||
}
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '/settings/profile',
|
||||
name: 'Profile Settings',
|
||||
component: ProfileSettings
|
||||
},
|
||||
{
|
||||
path: '/settings/global',
|
||||
name: 'Global Settings',
|
||||
component: GlobalSettings,
|
||||
meta: {
|
||||
requiresAdmin: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/settings/users',
|
||||
name: 'Users',
|
||||
component: Users,
|
||||
meta: {
|
||||
requiresAdmin: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/settings/users/*',
|
||||
name: 'User',
|
||||
component: User,
|
||||
meta: {
|
||||
requiresAdmin: true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/403',
|
||||
@@ -81,22 +101,6 @@ const router = new Router({
|
||||
name: 'Internal Server Error',
|
||||
component: Error500
|
||||
},
|
||||
{
|
||||
path: '/users',
|
||||
name: 'Users',
|
||||
component: Users,
|
||||
meta: {
|
||||
requiresAdmin: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/users/*',
|
||||
name: 'User',
|
||||
component: User,
|
||||
meta: {
|
||||
requiresAdmin: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/files',
|
||||
redirect: {
|
||||
|
||||
@@ -12,8 +12,16 @@ const state = {
|
||||
key: '',
|
||||
items: []
|
||||
},
|
||||
css: (() => {
|
||||
let css = window.CSS
|
||||
window.CSS = null
|
||||
return css
|
||||
})(),
|
||||
recaptcha: document.querySelector('meta[name="recaptcha"]').getAttribute('content'),
|
||||
staticGen: document.querySelector('meta[name="staticgen"]').getAttribute('content'),
|
||||
baseURL: document.querySelector('meta[name="base"]').getAttribute('content'),
|
||||
noAuth: (document.querySelector('meta[name="noauth"]').getAttribute('content') === 'true'),
|
||||
version: document.querySelector('meta[name="version"]').getAttribute('content'),
|
||||
jwt: '',
|
||||
progress: 0,
|
||||
schedule: '',
|
||||
|
||||
@@ -31,6 +31,7 @@ const mutations = {
|
||||
i18n.locale = value.locale
|
||||
state.user = value
|
||||
},
|
||||
setCSS: (state, value) => (state.css = value),
|
||||
setJWT: (state, value) => (state.jwt = value),
|
||||
multiple: (state, value) => (state.multiple = value),
|
||||
addSelected: (state, value) => (state.selected.push(value)),
|
||||
@@ -45,8 +46,12 @@ const mutations = {
|
||||
resetSelected: (state) => {
|
||||
state.selected = []
|
||||
},
|
||||
listingDisplay: (state, value) => {
|
||||
state.req.display = value
|
||||
updateUser: (state, value) => {
|
||||
if (typeof value !== 'object') return
|
||||
|
||||
for (let field in value) {
|
||||
state.user[field] = value[field]
|
||||
}
|
||||
},
|
||||
updateRequest: (state, value) => {
|
||||
state.req = value
|
||||
|
||||
@@ -18,7 +18,7 @@ export function fetch (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}`)
|
||||
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||
|
||||
request.onload = () => {
|
||||
switch (request.status) {
|
||||
@@ -41,7 +41,7 @@ export function remove (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}`)
|
||||
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||
|
||||
request.onload = () => {
|
||||
if (request.status === 200) {
|
||||
@@ -62,7 +62,7 @@ export function post (url, content = '', overwrite = false, onupload) {
|
||||
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}`)
|
||||
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||
|
||||
if (typeof onupload === 'function') {
|
||||
request.upload.onprogress = onupload
|
||||
@@ -95,7 +95,7 @@ export function put (url, content = '', publish = false, date = '') {
|
||||
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}`)
|
||||
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||
request.setRequestHeader('Publish', publish)
|
||||
|
||||
if (date !== '') {
|
||||
@@ -125,7 +125,7 @@ function moveCopy (items, copy = false) {
|
||||
promises.push(new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
request.open('PATCH', `${store.state.baseURL}/api/resource${from}`, true)
|
||||
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||
request.setRequestHeader('Destination', to)
|
||||
|
||||
if (copy) {
|
||||
@@ -162,7 +162,7 @@ export function checksum (url, algo) {
|
||||
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}`)
|
||||
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||
|
||||
request.onload = () => {
|
||||
if (request.status === 200) {
|
||||
@@ -226,7 +226,7 @@ export function getSettings () {
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
request.open('GET', `${store.state.baseURL}/api/settings/`, true)
|
||||
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||
|
||||
request.onload = () => {
|
||||
switch (request.status) {
|
||||
@@ -255,7 +255,7 @@ export function updateSettings (param, which) {
|
||||
|
||||
let request = new window.XMLHttpRequest()
|
||||
request.open('PUT', `${store.state.baseURL}/api/settings/`, true)
|
||||
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||
|
||||
request.onload = () => {
|
||||
switch (request.status) {
|
||||
@@ -278,7 +278,7 @@ export function getUsers () {
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
request.open('GET', `${store.state.baseURL}/api/users/`, true)
|
||||
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||
|
||||
request.onload = () => {
|
||||
switch (request.status) {
|
||||
@@ -299,7 +299,7 @@ export function getUser (id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
request.open('GET', `${store.state.baseURL}/api/users/${id}`, true)
|
||||
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||
|
||||
request.onload = () => {
|
||||
switch (request.status) {
|
||||
@@ -320,7 +320,7 @@ export function newUser (user) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
request.open('POST', `${store.state.baseURL}/api/users/`, true)
|
||||
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||
|
||||
request.onload = () => {
|
||||
switch (request.status) {
|
||||
@@ -345,7 +345,7 @@ export function updateUser (user, which) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
request.open('PUT', `${store.state.baseURL}/api/users/${user.ID}`, true)
|
||||
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||
|
||||
request.onload = () => {
|
||||
switch (request.status) {
|
||||
@@ -370,7 +370,7 @@ export function deleteUser (id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
request.open('DELETE', `${store.state.baseURL}/api/users/${id}`, true)
|
||||
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||
|
||||
request.onload = () => {
|
||||
switch (request.status) {
|
||||
@@ -395,7 +395,7 @@ export function getShare (url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
request.open('GET', `${store.state.baseURL}/api/share${url}`, true)
|
||||
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||
|
||||
request.onload = () => {
|
||||
if (request.status === 200) {
|
||||
@@ -414,7 +414,7 @@ export function deleteShare (hash) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
request.open('DELETE', `${store.state.baseURL}/api/share/${hash}`, true)
|
||||
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||
|
||||
request.onload = () => {
|
||||
if (request.status === 200) {
|
||||
@@ -439,7 +439,7 @@ export function share (url, expires = '', unit = 'hours') {
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
request.open('POST', url, true)
|
||||
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||
|
||||
request.onload = () => {
|
||||
if (request.status === 200) {
|
||||
|
||||
@@ -16,7 +16,7 @@ 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')}`)
|
||||
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${cookie('auth')}`)
|
||||
|
||||
request.onload = () => {
|
||||
if (request.status === 200) {
|
||||
@@ -31,8 +31,8 @@ function loggedIn () {
|
||||
})
|
||||
}
|
||||
|
||||
function login (user, password) {
|
||||
let data = {username: user, password: password}
|
||||
function login (user, password, captcha) {
|
||||
let data = {username: user, password: password, recaptcha: captcha}
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
request.open('POST', `${store.state.baseURL}/api/auth/get`, true)
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
<template>
|
||||
<div class="dashboard">
|
||||
<ul id="nav">
|
||||
<li>
|
||||
<router-link to="/settings/profile">
|
||||
<i class="material-icons">keyboard_arrow_left</i> {{ $t('settings.profileSettings') }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link to="/users">
|
||||
{{ $t('settings.userManagement') }} <i class="material-icons">keyboard_arrow_right</i>
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h1>{{ $t('settings.globalSettings') }}</h1>
|
||||
|
||||
<form @submit="saveStaticGen" v-if="$store.state.staticGen.length > 0">
|
||||
<h2>{{ capitalize($store.state.staticGen) }}</h2>
|
||||
|
||||
<p v-for="field in staticGen" :key="field.variable">
|
||||
<label v-if="field.type !== 'checkbox'">{{ field.name }}</label>
|
||||
<input v-if="field.type === 'text'" type="text" v-model.trim="field.value">
|
||||
<input v-else-if="field.type === 'checkbox'" type="checkbox" v-model.trim="field.value">
|
||||
<template v-if="field.type === 'checkbox'">{{ capitalize(field.name, 'caps') }}</template>
|
||||
</p>
|
||||
|
||||
<p><input type="submit" value="Save"></p>
|
||||
</form>
|
||||
|
||||
<form @submit="saveCommands">
|
||||
<h2>{{ $t('settings.commands') }}</h2>
|
||||
|
||||
<p class="small">{{ $t('settings.commandsHelp') }}</p>
|
||||
|
||||
<template v-for="command in commands">
|
||||
<h3>{{ capitalize(command.name) }}</h3>
|
||||
<textarea v-model.trim="command.value"></textarea>
|
||||
</template>
|
||||
|
||||
<p><input type="submit" value="Save"></p>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapMutations } from 'vuex'
|
||||
import { getSettings, updateSettings } from '@/utils/api'
|
||||
|
||||
export default {
|
||||
name: 'settings',
|
||||
data: function () {
|
||||
return {
|
||||
commands: [],
|
||||
staticGen: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState([ 'user' ])
|
||||
},
|
||||
created () {
|
||||
getSettings()
|
||||
.then(settings => {
|
||||
if (this.$store.state.staticGen.length > 0) {
|
||||
this.parseStaticGen(settings.staticGen)
|
||||
}
|
||||
|
||||
for (let key in settings.commands) {
|
||||
this.commands.push({
|
||||
name: key,
|
||||
value: settings.commands[key].join('\n')
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch(error => { this.showError(error) })
|
||||
},
|
||||
methods: {
|
||||
...mapMutations([ 'showSuccess', 'showError' ]),
|
||||
capitalize (name, where = '_') {
|
||||
if (where === 'caps') where = /(?=[A-Z])/
|
||||
let splitted = name.split(where)
|
||||
name = ''
|
||||
|
||||
for (let i = 0; i < splitted.length; i++) {
|
||||
name += splitted[i].charAt(0).toUpperCase() + splitted[i].slice(1) + ' '
|
||||
}
|
||||
|
||||
return name.slice(0, -1)
|
||||
},
|
||||
saveCommands (event) {
|
||||
event.preventDefault()
|
||||
|
||||
let commands = {}
|
||||
|
||||
for (let command of this.commands) {
|
||||
let value = command.value.split('\n')
|
||||
if (value.length === 1 && value[0] === '') {
|
||||
value = []
|
||||
}
|
||||
|
||||
commands[command.name] = value
|
||||
}
|
||||
|
||||
updateSettings(commands, 'commands')
|
||||
.then(() => { this.showSuccess(this.$t('settings.commandsUpdated')) })
|
||||
.catch(error => { this.showError(error) })
|
||||
},
|
||||
saveStaticGen (event) {
|
||||
event.preventDefault()
|
||||
let staticGen = {}
|
||||
|
||||
for (let field of this.staticGen) {
|
||||
staticGen[field.variable] = field.value
|
||||
|
||||
if (field.original === 'array') {
|
||||
let val = field.value.split(' ')
|
||||
if (val[0] === '') {
|
||||
val.shift()
|
||||
}
|
||||
|
||||
staticGen[field.variable] = val
|
||||
}
|
||||
}
|
||||
|
||||
updateSettings(staticGen, 'staticGen')
|
||||
.then(() => { this.showSuccess(this.$t('settings.settingsUpdated')) })
|
||||
.catch(error => { this.showError(error) })
|
||||
},
|
||||
parseStaticGen (staticgen) {
|
||||
for (let option of staticgen) {
|
||||
let value = option.value
|
||||
|
||||
let field = {
|
||||
name: option.name,
|
||||
variable: option.variable,
|
||||
type: 'text',
|
||||
original: 'text',
|
||||
value: value
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
field.original = 'array'
|
||||
field.value = value.join(' ')
|
||||
|
||||
this.staticGen.push(field)
|
||||
continue
|
||||
}
|
||||
|
||||
switch (typeof value) {
|
||||
case 'boolean':
|
||||
field.type = 'checkbox'
|
||||
field.original = 'boolean'
|
||||
break
|
||||
}
|
||||
|
||||
this.staticGen.push(field)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -6,7 +6,7 @@
|
||||
<site-header></site-header>
|
||||
<sidebar></sidebar>
|
||||
<main>
|
||||
<router-view v-on:css-updated="updateCSS"></router-view>
|
||||
<router-view @css="$emit('update:css')"></router-view>
|
||||
</main>
|
||||
<prompts></prompts>
|
||||
</div>
|
||||
@@ -34,23 +34,10 @@ export default {
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.updateCSS()
|
||||
this.$emit('update:css')
|
||||
},
|
||||
methods: {
|
||||
updateCSS () {
|
||||
let css = this.$store.state.user.css
|
||||
|
||||
let style = document.querySelector('style[title="user-css"]')
|
||||
if (style !== undefined && style !== null) {
|
||||
style.parentElement.removeChild(style)
|
||||
}
|
||||
|
||||
style = document.createElement('style')
|
||||
style.title = 'user-css'
|
||||
style.type = 'text/css'
|
||||
style.appendChild(document.createTextNode(css))
|
||||
document.head.appendChild(style)
|
||||
}
|
||||
beforeDestroy () {
|
||||
this.$emit('clean:css')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<template>
|
||||
<div id="login">
|
||||
<div id="login" :class="{ recaptcha: recaptcha.length > 0 }">
|
||||
<form @submit="submit">
|
||||
<img src="../assets/logo.svg" alt="File Manager">
|
||||
<h1>File Manager</h1>
|
||||
<div v-if="wrong" class="wrong">{{ $t("login.wrongCredentials") }}</div>
|
||||
<input type="text" v-model="username" :placeholder="$t('login.username')">
|
||||
<input type="password" v-model="password" :placeholder="$t('login.password')">
|
||||
<div v-if="recaptcha.length" id="recaptcha"></div>
|
||||
<input type="submit" :value="$t('login.submit')">
|
||||
</form>
|
||||
</div>
|
||||
@@ -13,9 +14,12 @@
|
||||
|
||||
<script>
|
||||
import auth from '@/utils/auth'
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'login',
|
||||
props: ['dependencies'],
|
||||
computed: mapState(['recaptcha']),
|
||||
data: function () {
|
||||
return {
|
||||
wrong: false,
|
||||
@@ -23,8 +27,23 @@ export default {
|
||||
password: ''
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
if (this.dependencies) this.setup()
|
||||
},
|
||||
watch: {
|
||||
dependencies: function (val) {
|
||||
if (val) this.setup()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
submit: function (event) {
|
||||
setup () {
|
||||
if (this.recaptcha.length === 0) return
|
||||
|
||||
window.grecaptcha.render('recaptcha', {
|
||||
sitekey: this.recaptcha
|
||||
})
|
||||
},
|
||||
submit (event) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
@@ -33,7 +52,17 @@ export default {
|
||||
redirect = '/files/'
|
||||
}
|
||||
|
||||
auth.login(this.username, this.password)
|
||||
let captcha = ''
|
||||
if (this.recaptcha.length > 0) {
|
||||
captcha = window.grecaptcha.getResponse()
|
||||
|
||||
if (captcha === '') {
|
||||
this.wrong = true
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
auth.login(this.username, this.password, captcha)
|
||||
.then(() => { this.$router.push({ path: redirect }) })
|
||||
.catch(() => { this.wrong = true })
|
||||
}
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
<template>
|
||||
<div class="dashboard">
|
||||
<ul id="nav" v-if="user.admin">
|
||||
<li>
|
||||
<router-link to="/settings/global">
|
||||
{{ $t('settings.globalSettings') }} <i class="material-icons">keyboard_arrow_right</i>
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h1>{{ $t('settings.profileSettings') }}</h1>
|
||||
|
||||
<form @submit="updateSettings">
|
||||
<h3>{{ $t('settings.language') }}</h3>
|
||||
<p><languages id="locale" :selected.sync="locale"></languages></p>
|
||||
<h3>{{ $t('settings.customStylesheet') }}</h3>
|
||||
<textarea v-model="css" name="css"></textarea>
|
||||
<p><input type="submit" :value="$t('buttons.update')"></p>
|
||||
</form>
|
||||
|
||||
<form @submit="updatePassword">
|
||||
<h3>{{ $t('settings.changePassword') }}</h3>
|
||||
<p><input :class="passwordClass" type="password" :placeholder="$t('settings.newPassword')" v-model="password" name="password"></p>
|
||||
<p><input :class="passwordClass" type="password" :placeholder="$t('settings.newPasswordConfirm')" v-model="passwordConf" name="password"></p>
|
||||
<p><input type="submit" :value="$t('buttons.update')"></p>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapMutations } from 'vuex'
|
||||
import { updateUser } from '@/utils/api'
|
||||
import Languages from '@/components/Languages'
|
||||
|
||||
export default {
|
||||
name: 'settings',
|
||||
components: {
|
||||
Languages
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
password: '',
|
||||
passwordConf: '',
|
||||
css: '',
|
||||
locale: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState([ 'user' ]),
|
||||
passwordClass () {
|
||||
if (this.password === '' && this.passwordConf === '') {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (this.password === this.passwordConf) {
|
||||
return 'green'
|
||||
}
|
||||
|
||||
return 'red'
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.css = this.user.css
|
||||
this.locale = this.user.locale
|
||||
},
|
||||
methods: {
|
||||
...mapMutations([ 'showSuccess' ]),
|
||||
updatePassword (event) {
|
||||
event.preventDefault()
|
||||
|
||||
if (this.password !== this.passwordConf) {
|
||||
return
|
||||
}
|
||||
|
||||
let user = {
|
||||
ID: this.$store.state.user.ID,
|
||||
password: this.password
|
||||
}
|
||||
|
||||
updateUser(user, 'password').then(location => {
|
||||
this.showSuccess(this.$t('settings.passwordUpdated'))
|
||||
}).catch(e => {
|
||||
this.$store.commit('showError', e)
|
||||
})
|
||||
},
|
||||
updateSettings (event) {
|
||||
event.preventDefault()
|
||||
|
||||
let user = {...this.$store.state.user}
|
||||
user.css = this.css
|
||||
user.locale = this.locale
|
||||
|
||||
updateUser(user, 'partial').then(location => {
|
||||
this.$store.commit('setUser', user)
|
||||
this.$emit('css-updated')
|
||||
this.showSuccess(this.$t('settings.settingsUpdated'))
|
||||
}).catch(e => {
|
||||
this.$store.commit('showError', e)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
20
assets/src/views/Settings.vue
Normal file
20
assets/src/views/Settings.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<div class="dashboard">
|
||||
<ul id="nav" v-if="user.admin">
|
||||
<li :class="{ active: $route.path === '/settings/profile' }"><router-link to="/settings/profile">{{ $t('settings.profileSettings') }}</router-link></li>
|
||||
<li :class="{ active: $route.path === '/settings/global' }"><router-link to="/settings/global">{{ $t('settings.globalSettings') }}</router-link></li>
|
||||
<li :class="{ active: $route.path === '/settings/users' }"><router-link to="/settings/users">{{ $t('settings.userManagement') }}</router-link></li>
|
||||
</ul>
|
||||
|
||||
<router-view @css="$emit('css')"></router-view>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'settings',
|
||||
computed: mapState([ 'user' ])
|
||||
}
|
||||
</script>
|
||||
@@ -1,51 +0,0 @@
|
||||
<template>
|
||||
<div class="dashboard">
|
||||
<ul id="nav">
|
||||
<li>
|
||||
<router-link to="/settings/global">
|
||||
<i class="material-icons">keyboard_arrow_left</i> {{ $t('settings.globalSettings') }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li></li>
|
||||
</ul>
|
||||
|
||||
<h1>{{ $t('settings.users') }} <router-link to="/users/new"><button>{{ $t('buttons.new') }}</button></router-link></h1>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>{{ $t('settings.username') }}</th>
|
||||
<th>{{ $t('settings.admin') }}</th>
|
||||
<th>{{ $t('settings.scope') }}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
|
||||
<tr v-for="user in users">
|
||||
<td>{{ user.username }}</td>
|
||||
<td><i v-if="user.admin" class="material-icons">done</i><i v-else class="material-icons">close</i></td>
|
||||
<td>{{ user.filesystem }}</td>
|
||||
<td><router-link :to="'/users/' + user.ID"><i class="material-icons">mode_edit</i></router-link></td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as api from '@/utils/api'
|
||||
|
||||
export default {
|
||||
name: 'users',
|
||||
data: function () {
|
||||
return {
|
||||
users: []
|
||||
}
|
||||
},
|
||||
created () {
|
||||
api.getUsers().then(users => {
|
||||
this.users = users
|
||||
}).catch(error => {
|
||||
this.$store.commit('showError', error)
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
187
assets/src/views/settings/Global.vue
Normal file
187
assets/src/views/settings/Global.vue
Normal file
@@ -0,0 +1,187 @@
|
||||
<template>
|
||||
<div class="dashboard">
|
||||
<form class="card" v-if="staticGen.length" @submit.prevent="saveStaticGen">
|
||||
<div class="card-title">
|
||||
<h2>{{ capitalize($store.state.staticGen) }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p v-for="field in staticGen" :key="field.variable">
|
||||
<label v-if="field.type !== 'checkbox'">{{ field.name }}</label>
|
||||
<input v-if="field.type === 'text'" type="text" v-model.trim="field.value">
|
||||
<input v-else-if="field.type === 'checkbox'" type="checkbox" v-model.trim="field.value">
|
||||
<template v-if="field.type === 'checkbox'">{{ capitalize(field.name, 'caps') }}</template>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<input class="flat" type="submit" :value="$t('buttons.update')">
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form class="card" @submit.prevent="saveCSS">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('settings.customStylesheet') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<textarea v-model="css"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<input class="flat" type="submit" :value="$t('buttons.update')">
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form class="card" @submit.prevent="saveCommands">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('settings.commands') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p class="small">{{ $t('settings.commandsHelp') }}</p>
|
||||
|
||||
<div v-for="command in commands" :key="command.name" class="collapsible">
|
||||
<input :id="command.name" type="checkbox">
|
||||
<label :for="command.name">
|
||||
<p>{{ capitalize(command.name) }}</p>
|
||||
<i class="material-icons">arrow_drop_down</i>
|
||||
</label>
|
||||
<div class="collapse">
|
||||
<textarea v-model.trim="command.value"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<input class="flat" type="submit" :value="$t('buttons.update')">
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import { getSettings, updateSettings } from '@/utils/api'
|
||||
|
||||
export default {
|
||||
name: 'settings',
|
||||
data: function () {
|
||||
return {
|
||||
commands: [],
|
||||
staticGen: [],
|
||||
css: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState([ 'user' ])
|
||||
},
|
||||
created () {
|
||||
getSettings()
|
||||
.then(settings => {
|
||||
if (this.$store.state.staticGen.length > 0) {
|
||||
this.parseStaticGen(settings.staticGen)
|
||||
}
|
||||
|
||||
for (let key in settings.commands) {
|
||||
this.commands.push({
|
||||
name: key,
|
||||
value: settings.commands[key].join('\n')
|
||||
})
|
||||
}
|
||||
|
||||
this.css = settings.css
|
||||
})
|
||||
.catch(this.$showError)
|
||||
},
|
||||
methods: {
|
||||
capitalize (name, where = '_') {
|
||||
if (where === 'caps') where = /(?=[A-Z])/
|
||||
let splitted = name.split(where)
|
||||
name = ''
|
||||
|
||||
for (let i = 0; i < splitted.length; i++) {
|
||||
name += splitted[i].charAt(0).toUpperCase() + splitted[i].slice(1) + ' '
|
||||
}
|
||||
|
||||
return name.slice(0, -1)
|
||||
},
|
||||
saveCommands (event) {
|
||||
let commands = {}
|
||||
|
||||
for (let command of this.commands) {
|
||||
let value = command.value.split('\n')
|
||||
if (value.length === 1 && value[0] === '') {
|
||||
value = []
|
||||
}
|
||||
|
||||
commands[command.name] = value
|
||||
}
|
||||
|
||||
updateSettings(commands, 'commands')
|
||||
.then(() => { this.$showSuccess(this.$t('settings.commandsUpdated')) })
|
||||
.catch(this.$showError)
|
||||
},
|
||||
saveCSS (event) {
|
||||
updateSettings(this.css, 'css')
|
||||
.then(() => {
|
||||
this.$showSuccess(this.$t('settings.settingsUpdated'))
|
||||
this.$store.commit('setCSS', this.css)
|
||||
this.$emit('css')
|
||||
})
|
||||
.catch(this.$showError)
|
||||
},
|
||||
saveStaticGen (event) {
|
||||
let staticGen = {}
|
||||
|
||||
for (let field of this.staticGen) {
|
||||
staticGen[field.variable] = field.value
|
||||
|
||||
if (field.original === 'array') {
|
||||
let val = field.value.split(' ')
|
||||
if (val[0] === '') {
|
||||
val.shift()
|
||||
}
|
||||
|
||||
staticGen[field.variable] = val
|
||||
}
|
||||
}
|
||||
|
||||
updateSettings(staticGen, 'staticGen')
|
||||
.then(() => { this.$showSuccess(this.$t('settings.settingsUpdated')) })
|
||||
.catch(this.$showError)
|
||||
},
|
||||
parseStaticGen (staticgen) {
|
||||
for (let option of staticgen) {
|
||||
let value = option.value
|
||||
|
||||
let field = {
|
||||
name: option.name,
|
||||
variable: option.variable,
|
||||
type: 'text',
|
||||
original: 'text',
|
||||
value: value
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
field.original = 'array'
|
||||
field.value = value.join(' ')
|
||||
|
||||
this.staticGen.push(field)
|
||||
continue
|
||||
}
|
||||
|
||||
switch (typeof value) {
|
||||
case 'boolean':
|
||||
field.type = 'checkbox'
|
||||
field.original = 'boolean'
|
||||
break
|
||||
}
|
||||
|
||||
this.staticGen.push(field)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
109
assets/src/views/settings/Profile.vue
Normal file
109
assets/src/views/settings/Profile.vue
Normal file
@@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<div class="dashboard">
|
||||
<form class="card" @submit="updateSettings">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('settings.profileSettings') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<h3>{{ $t('settings.language') }}</h3>
|
||||
<p><languages id="locale" :selected.sync="locale"></languages></p>
|
||||
<h3>{{ $t('settings.customStylesheet') }}</h3>
|
||||
<textarea v-model="css" name="css"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<input class="flat" type="submit" :value="$t('buttons.update')">
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form class="card" v-if="!user.lockPassword" @submit="updatePassword">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('settings.changePassword') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p><input :class="passwordClass" type="password" :placeholder="$t('settings.newPassword')" v-model="password" name="password"></p>
|
||||
<p><input :class="passwordClass" type="password" :placeholder="$t('settings.newPasswordConfirm')" v-model="passwordConf" name="password"></p>
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<input class="flat" type="submit" :value="$t('buttons.update')">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import { updateUser } from '@/utils/api'
|
||||
import Languages from '@/components/Languages'
|
||||
|
||||
export default {
|
||||
name: 'settings',
|
||||
components: {
|
||||
Languages
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
password: '',
|
||||
passwordConf: '',
|
||||
css: '',
|
||||
locale: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState([ 'user' ]),
|
||||
passwordClass () {
|
||||
if (this.password === '' && this.passwordConf === '') {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (this.password === this.passwordConf) {
|
||||
return 'green'
|
||||
}
|
||||
|
||||
return 'red'
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.css = this.user.css
|
||||
this.locale = this.user.locale
|
||||
},
|
||||
methods: {
|
||||
updatePassword (event) {
|
||||
event.preventDefault()
|
||||
|
||||
if (this.password !== this.passwordConf) {
|
||||
return
|
||||
}
|
||||
|
||||
let user = {
|
||||
ID: this.$store.state.user.ID,
|
||||
password: this.password
|
||||
}
|
||||
|
||||
updateUser(user, 'password').then(location => {
|
||||
this.$showSuccess(this.$t('settings.passwordUpdated'))
|
||||
}).catch(e => {
|
||||
this.$showError(e)
|
||||
})
|
||||
},
|
||||
updateSettings (event) {
|
||||
event.preventDefault()
|
||||
|
||||
let user = {...this.$store.state.user}
|
||||
user.css = this.css
|
||||
user.locale = this.locale
|
||||
|
||||
updateUser(user, 'partial').then(location => {
|
||||
this.$store.commit('setUser', user)
|
||||
this.$emit('css')
|
||||
this.$showSuccess(this.$t('settings.settingsUpdated'))
|
||||
}).catch(e => {
|
||||
this.$showError(e)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,77 +1,81 @@
|
||||
<template>
|
||||
<div>
|
||||
<form @submit="save" class="dashboard">
|
||||
<ul id="nav">
|
||||
<li>
|
||||
<router-link to="/users">
|
||||
<i class="material-icons">keyboard_arrow_left</i> {{ $t('settings.userManagement') }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li></li>
|
||||
</ul>
|
||||
<form @submit="save" class="card">
|
||||
<div class="card-title">
|
||||
<h2 v-if="id === 0">{{ $t('settings.newUser') }}</h2>
|
||||
<h2 v-else>{{ $t('settings.user') }} {{ username }}</h2>
|
||||
</div>
|
||||
|
||||
<h1 v-if="id === 0">{{ $t('settings.newUser') }}</h1>
|
||||
<h1 v-else>{{ $t('settings.user') }} {{ username }}</h1>
|
||||
<div class="card-content">
|
||||
|
||||
<p><label for="username">{{ $t('settings.username') }}</label><input type="text" v-model="username" id="username"></p>
|
||||
<p><label for="password">{{ $t('settings.password') }}</label><input type="password" :placeholder="passwordPlaceholder" v-model="password" id="password"></p>
|
||||
<p><label for="scope">{{ $t('settings.scope') }}</label><input type="text" v-model="filesystem" id="scope"></p>
|
||||
<p>
|
||||
<label for="locale">{{ $t('settings.language') }}</label>
|
||||
<languages id="locale" :selected.sync="locale"></languages>
|
||||
</p>
|
||||
<p><label for="username">{{ $t('settings.username') }}</label><input type="text" v-model="username" id="username"></p>
|
||||
<p><label for="password">{{ $t('settings.password') }}</label><input type="password" :placeholder="passwordPlaceholder" v-model="password" id="password"></p>
|
||||
<p><label for="scope">{{ $t('settings.scope') }}</label><input type="text" v-model="filesystem" id="scope"></p>
|
||||
<p>
|
||||
<label for="locale">{{ $t('settings.language') }}</label>
|
||||
<languages id="locale" :selected.sync="locale"></languages>
|
||||
</p>
|
||||
|
||||
<h2>{{ $t('settings.permissions') }}</h2>
|
||||
<p class="small">{{ $t('settings.permissionsHelp') }}</p>
|
||||
<p><input type="checkbox" :disabled="admin" v-model="lockPassword"> {{ $t('settings.lockPassword') }}</p>
|
||||
|
||||
<p><input type="checkbox" v-model="admin"> {{ $t('settings.administrator') }}</p>
|
||||
<p><input type="checkbox" :disabled="admin" v-model="allowNew"> {{ $t('settings.allowNew') }}</p>
|
||||
<p><input type="checkbox" :disabled="admin" v-model="allowEdit"> {{ $t('settings.allowEdit') }}</p>
|
||||
<p><input type="checkbox" :disabled="admin" v-model="allowCommands"> {{ $t('settings.allowCommands') }}</p>
|
||||
<p v-show="$store.state.staticGen.length"><input type="checkbox" :disabled="admin" v-model="allowPublish"> {{ $t('settings.allowPublish') }}</p>
|
||||
<h3>{{ $t('settings.permissions') }}</h3>
|
||||
<p class="small">{{ $t('settings.permissionsHelp') }}</p>
|
||||
|
||||
<h3>{{ $t('settings.userCommands') }}</h3>
|
||||
<p class="small">{{ $t('settings.userCommandsHelp') }} <i>git svn hg</i>.</p>
|
||||
<input type="text" v-model.trim="commands">
|
||||
<p><input type="checkbox" v-model="admin"> {{ $t('settings.administrator') }}</p>
|
||||
<p><input type="checkbox" :disabled="admin" v-model="allowNew"> {{ $t('settings.allowNew') }}</p>
|
||||
<p><input type="checkbox" :disabled="admin" v-model="allowEdit"> {{ $t('settings.allowEdit') }}</p>
|
||||
<p><input type="checkbox" :disabled="admin" v-model="allowCommands"> {{ $t('settings.allowCommands') }}</p>
|
||||
<p v-show="$store.state.staticGen.length"><input type="checkbox" :disabled="admin" v-model="allowPublish"> {{ $t('settings.allowPublish') }}</p>
|
||||
|
||||
<h2>{{ $t('settings.rules') }}</h2>
|
||||
<h3>{{ $t('settings.userCommands') }}</h3>
|
||||
<p class="small">{{ $t('settings.userCommandsHelp') }} <i>git svn hg</i>.</p>
|
||||
<input type="text" v-model.trim="commands">
|
||||
|
||||
<p class="small">{{ $t('settings.rulesHelp1') }}</p>
|
||||
<h3>{{ $t('settings.rules') }}</h3>
|
||||
|
||||
<i18n path="settings.rulesHelp2" tag="p" class="small">
|
||||
<code>allow</code><code>disallow</code><code>regex</code>
|
||||
</i18n>
|
||||
<p class="small">{{ $t('settings.rulesHelp1') }}</p>
|
||||
|
||||
<p class="small"><strong>{{ $t('settings.examples') }}</strong></p>
|
||||
<i18n path="settings.rulesHelp2" tag="p" class="small">
|
||||
<code>allow</code><code>disallow</code><code>regex</code>
|
||||
</i18n>
|
||||
|
||||
<ul class="small">
|
||||
<li><code>disallow regex \\/\\..+</code> - {{ $t('settings.ruleExample1') }}</li>
|
||||
<li><code>disallow /Caddyfile</code> - {{ $t('settings.ruleExample2') }}</li>
|
||||
</ul>
|
||||
<p class="small"><strong>{{ $t('settings.examples') }}</strong></p>
|
||||
|
||||
<textarea v-model.trim="rules"></textarea>
|
||||
<ul class="small">
|
||||
<li><code>disallow regex \\/\\..+</code> - {{ $t('settings.ruleExample1') }}</li>
|
||||
<li><code>disallow /Caddyfile</code> - {{ $t('settings.ruleExample2') }}</li>
|
||||
</ul>
|
||||
|
||||
<h2>{{ $t('settings.customStylesheet') }}</h2>
|
||||
<textarea v-model.trim="rules"></textarea>
|
||||
|
||||
<textarea name="css"></textarea>
|
||||
<h3>{{ $t('settings.customStylesheet') }}</h3>
|
||||
|
||||
<p>
|
||||
<button v-if="id !== 0" @click.prevent="deletePrompt" type="button" class="delete" :aria-label="$t('buttons.delete')" :title="$t('buttons.delete')">{{ $t('buttons.delete') }}</button>
|
||||
<input type="submit" :value="$t('buttons.save')">
|
||||
</p>
|
||||
<textarea name="css"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<button v-if="id !== 0" @click.prevent="deletePrompt" type="button" class="flat delete" :aria-label="$t('buttons.delete')" :title="$t('buttons.delete')">{{ $t('buttons.delete') }}</button>
|
||||
<input class="flat" type="submit" :value="$t('buttons.save')">
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div v-if="$store.state.show === 'deleteUser'" class="prompt">
|
||||
<h3>Delete User</h3>
|
||||
<p>Are you sure you want to delete this user?</p>
|
||||
<div>
|
||||
<button @click="deleteUser" autofocus>{{ $t('buttons.delete') }}</button>
|
||||
<button class="cancel"
|
||||
<div v-if="$store.state.show === 'deleteUser'" class="card floating">
|
||||
<div class="card-content">
|
||||
<p>Are you sure you want to delete this user?</p>
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<button class="cancel flat"
|
||||
@click="closeHovers"
|
||||
autofocus
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">
|
||||
{{ $t('buttons.cancel') }}
|
||||
</button>
|
||||
<button class="flat"
|
||||
@click="deleteUser">
|
||||
{{ $t('buttons.delete') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -93,6 +97,7 @@ export default {
|
||||
allowEdit: false,
|
||||
allowCommands: false,
|
||||
allowPublish: false,
|
||||
lockPassword: false,
|
||||
permissions: {},
|
||||
password: '',
|
||||
username: '',
|
||||
@@ -105,7 +110,7 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
passwordPlaceholder () {
|
||||
if (this.$route.path === '/users/new') return ''
|
||||
if (this.$route.path === '/settings/users/new') return ''
|
||||
return this.$t('settings.avoidChanges')
|
||||
}
|
||||
},
|
||||
@@ -120,6 +125,7 @@ export default {
|
||||
this.allowEdit = true
|
||||
this.allowNew = true
|
||||
this.allowPublish = true
|
||||
this.lockPassword = false
|
||||
for (let key in this.permissions) {
|
||||
this.permissions[key] = true
|
||||
}
|
||||
@@ -130,7 +136,7 @@ export default {
|
||||
fetchData () {
|
||||
let user = this.$route.params[0]
|
||||
|
||||
if (this.$route.path === '/users/new') {
|
||||
if (this.$route.path === '/settings/users/new') {
|
||||
user = 'base'
|
||||
}
|
||||
|
||||
@@ -141,6 +147,7 @@ export default {
|
||||
this.allowNew = user.allowNew
|
||||
this.allowEdit = user.allowEdit
|
||||
this.allowPublish = user.allowPublish
|
||||
this.lockPassword = user.lockPassword
|
||||
this.filesystem = user.filesystem
|
||||
this.username = user.username
|
||||
this.commands = user.commands.join(' ')
|
||||
@@ -166,7 +173,7 @@ export default {
|
||||
|
||||
this.rules = this.rules.trim()
|
||||
}).catch(() => {
|
||||
this.$router.push({ path: '/users/new' })
|
||||
this.$router.push({ path: '/settings/users/new' })
|
||||
})
|
||||
},
|
||||
capitalize (name) {
|
||||
@@ -187,6 +194,7 @@ export default {
|
||||
this.allowPublish = false
|
||||
this.permissins = {}
|
||||
this.allowCommands = false
|
||||
this.lockPassword = false
|
||||
this.password = ''
|
||||
this.username = ''
|
||||
this.filesystem = ''
|
||||
@@ -202,22 +210,22 @@ export default {
|
||||
event.preventDefault()
|
||||
|
||||
deleteUser(this.id).then(location => {
|
||||
this.$router.push({ path: '/users' })
|
||||
this.$store.commit('showSuccess', this.$t('settings.userDeleted'))
|
||||
this.$router.push({ path: '/settings/users' })
|
||||
this.$showSuccess(this.$t('settings.userDeleted'))
|
||||
}).catch(e => {
|
||||
this.$store.commit('showError', e)
|
||||
this.$showError(e)
|
||||
})
|
||||
},
|
||||
save (event) {
|
||||
event.preventDefault()
|
||||
let user = this.parseForm()
|
||||
|
||||
if (this.$route.path === '/users/new') {
|
||||
if (this.$route.path === '/settings/users/new') {
|
||||
newUser(user).then(location => {
|
||||
this.$router.push({ path: location })
|
||||
this.$store.commit('showSuccess', this.$t('settings.userCreated'))
|
||||
this.$showSuccess(this.$t('settings.userCreated'))
|
||||
}).catch(e => {
|
||||
this.$store.commit('showError', e)
|
||||
this.$showError(e)
|
||||
})
|
||||
|
||||
return
|
||||
@@ -228,9 +236,9 @@ export default {
|
||||
this.$store.commit('setUser', user)
|
||||
}
|
||||
|
||||
this.$store.commit('showSuccess', this.$t('settings.userUpdated'))
|
||||
this.$showSuccess(this.$t('settings.userUpdated'))
|
||||
}).catch(e => {
|
||||
this.$store.commit('showError', e)
|
||||
this.$showError(e)
|
||||
})
|
||||
},
|
||||
parseForm () {
|
||||
@@ -238,6 +246,7 @@ export default {
|
||||
ID: this.id,
|
||||
username: this.username,
|
||||
password: this.password,
|
||||
lockPassword: this.lockPassword,
|
||||
filesystem: this.filesystem,
|
||||
admin: this.admin,
|
||||
allowCommands: this.allowCommands,
|
||||
48
assets/src/views/settings/Users.vue
Normal file
48
assets/src/views/settings/Users.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<div class="card">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('settings.users') }}</h2>
|
||||
<router-link to="/settings/users/new"><button class="flat">{{ $t('buttons.new') }}</button></router-link>
|
||||
</div>
|
||||
|
||||
<div class="card-content full">
|
||||
<table>
|
||||
<tr>
|
||||
<th>{{ $t('settings.username') }}</th>
|
||||
<th>{{ $t('settings.admin') }}</th>
|
||||
<th>{{ $t('settings.scope') }}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
|
||||
<tr v-for="user in users" :key="user.id">
|
||||
<td>{{ user.username }}</td>
|
||||
<td><i v-if="user.admin" class="material-icons">done</i><i v-else class="material-icons">close</i></td>
|
||||
<td>{{ user.filesystem }}</td>
|
||||
<td class="small">
|
||||
<router-link :to="'/settings/users/' + user.ID"><i class="material-icons">mode_edit</i></router-link>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as api from '@/utils/api'
|
||||
|
||||
export default {
|
||||
name: 'users',
|
||||
data: function () {
|
||||
return {
|
||||
users: []
|
||||
}
|
||||
},
|
||||
created () {
|
||||
api.getUsers().then(users => {
|
||||
this.users = users
|
||||
}).catch(error => {
|
||||
this.$showError(error)
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -13,24 +13,24 @@ type ShareStore struct {
|
||||
|
||||
// Get gets a Share Link from an hash.
|
||||
func (s ShareStore) Get(hash string) (*fm.ShareLink, error) {
|
||||
var v *fm.ShareLink
|
||||
var v fm.ShareLink
|
||||
err := s.DB.One("Hash", hash, &v)
|
||||
if err == storm.ErrNotFound {
|
||||
return v, fm.ErrNotExist
|
||||
return nil, fm.ErrNotExist
|
||||
}
|
||||
|
||||
return v, err
|
||||
return &v, err
|
||||
}
|
||||
|
||||
// GetPermanent gets the permanent link from a path.
|
||||
func (s ShareStore) GetPermanent(path string) (*fm.ShareLink, error) {
|
||||
var v *fm.ShareLink
|
||||
var v fm.ShareLink
|
||||
err := s.DB.Select(q.Eq("Path", path), q.Eq("Expires", false)).First(&v)
|
||||
if err == storm.ErrNotFound {
|
||||
return v, fm.ErrNotExist
|
||||
return nil, fm.ErrNotExist
|
||||
}
|
||||
|
||||
return v, err
|
||||
return &v, err
|
||||
}
|
||||
|
||||
// GetByPath gets all the links for a specific path.
|
||||
@@ -48,6 +48,10 @@ func (s ShareStore) GetByPath(hash string) ([]*fm.ShareLink, error) {
|
||||
func (s ShareStore) Gets() ([]*fm.ShareLink, error) {
|
||||
var v []*fm.ShareLink
|
||||
err := s.DB.All(&v)
|
||||
if err == storm.ErrNotFound {
|
||||
return v, fm.ErrNotExist
|
||||
}
|
||||
|
||||
return v, err
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
"github.com/hacdias/fileutils"
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var databases = map[string]*storm.DB{}
|
||||
@@ -49,6 +48,8 @@ func Parse(c *caddy.Controller, plugin string) ([]*filemanager.FileManager, erro
|
||||
scope := "."
|
||||
database := ""
|
||||
noAuth := false
|
||||
reCaptchaKey := ""
|
||||
reCaptchaSecret := ""
|
||||
|
||||
if plugin != "" {
|
||||
baseURL = "/admin"
|
||||
@@ -147,6 +148,27 @@ func Parse(c *caddy.Controller, plugin string) ([]*filemanager.FileManager, erro
|
||||
}
|
||||
|
||||
u.CSS = string(css)
|
||||
case "view_mode":
|
||||
if !c.NextArg() {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
|
||||
u.ViewMode = c.Val()
|
||||
if u.ViewMode != "mosaic" && u.ViewMode != "list" {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
case "recaptcha_key":
|
||||
if !c.NextArg() {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
|
||||
reCaptchaKey = c.Val()
|
||||
case "recaptcha_secret":
|
||||
if !c.NextArg() {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
|
||||
reCaptchaSecret = c.Val()
|
||||
case "no_auth":
|
||||
if !c.NextArg() {
|
||||
noAuth = true
|
||||
@@ -205,10 +227,12 @@ func Parse(c *caddy.Controller, plugin string) ([]*filemanager.FileManager, erro
|
||||
}
|
||||
|
||||
m := &filemanager.FileManager{
|
||||
NoAuth: viper.GetBool("NoAuth"),
|
||||
BaseURL: "",
|
||||
PrefixURL: "",
|
||||
DefaultUser: u,
|
||||
NoAuth: noAuth,
|
||||
BaseURL: "",
|
||||
PrefixURL: "",
|
||||
ReCaptchaKey: reCaptchaKey,
|
||||
ReCaptchaSecret: reCaptchaSecret,
|
||||
DefaultUser: u,
|
||||
Store: &filemanager.Store{
|
||||
Config: bolt.ConfigStore{DB: db},
|
||||
Users: bolt.UsersStore{DB: db},
|
||||
|
||||
@@ -24,22 +24,26 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
addr string
|
||||
config string
|
||||
database string
|
||||
scope string
|
||||
commands string
|
||||
logfile string
|
||||
staticg string
|
||||
locale string
|
||||
port int
|
||||
noAuth bool
|
||||
allowCommands bool
|
||||
allowEdit bool
|
||||
allowNew bool
|
||||
allowPublish bool
|
||||
showVer bool
|
||||
version = "master"
|
||||
addr string
|
||||
config string
|
||||
database string
|
||||
scope string
|
||||
commands string
|
||||
logfile string
|
||||
staticg string
|
||||
locale string
|
||||
baseurl string
|
||||
prefixurl string
|
||||
viewMode string
|
||||
recaptchakey string
|
||||
recaptchasecret string
|
||||
port int
|
||||
noAuth bool
|
||||
allowCommands bool
|
||||
allowEdit bool
|
||||
allowNew bool
|
||||
allowPublish bool
|
||||
showVer bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -49,7 +53,12 @@ func init() {
|
||||
flag.StringVarP(&database, "database", "d", "./filemanager.db", "Database file")
|
||||
flag.StringVarP(&logfile, "log", "l", "stdout", "Errors logger; can use 'stdout', 'stderr' or file")
|
||||
flag.StringVarP(&scope, "scope", "s", ".", "Default scope option for new users")
|
||||
flag.StringVarP(&baseurl, "baseurl", "b", "", "Base URL")
|
||||
flag.StringVar(&commands, "commands", "git svn hg", "Default commands option for new users")
|
||||
flag.StringVar(&prefixurl, "prefixurl", "", "Prefix URL")
|
||||
flag.StringVar(&viewMode, "view-mode", "mosaic", "Default view mode for new users")
|
||||
flag.StringVar(&recaptchakey, "recaptcha-key", "", "ReCaptcha site key")
|
||||
flag.StringVar(&recaptchasecret, "recaptcha-secret", "", "ReCaptcha secret")
|
||||
flag.BoolVar(&allowCommands, "allow-commands", true, "Default allow commands option for new users")
|
||||
flag.BoolVar(&allowEdit, "allow-edit", true, "Default allow edit option for new users")
|
||||
flag.BoolVar(&allowPublish, "allow-publish", true, "Default allow publish option for new users")
|
||||
@@ -74,6 +83,11 @@ func setupViper() {
|
||||
viper.SetDefault("StaticGen", "")
|
||||
viper.SetDefault("Locale", "en")
|
||||
viper.SetDefault("NoAuth", false)
|
||||
viper.SetDefault("BaseURL", "")
|
||||
viper.SetDefault("PrefixURL", "")
|
||||
viper.SetDefault("ViewMode", "mosaic")
|
||||
viper.SetDefault("ReCaptchaKey", "")
|
||||
viper.SetDefault("ReCaptchaSecret", "")
|
||||
|
||||
viper.BindPFlag("Port", flag.Lookup("port"))
|
||||
viper.BindPFlag("Address", flag.Lookup("address"))
|
||||
@@ -88,21 +102,18 @@ func setupViper() {
|
||||
viper.BindPFlag("Locale", flag.Lookup("locale"))
|
||||
viper.BindPFlag("StaticGen", flag.Lookup("staticgen"))
|
||||
viper.BindPFlag("NoAuth", flag.Lookup("no-auth"))
|
||||
viper.BindPFlag("BaseURL", flag.Lookup("baseurl"))
|
||||
viper.BindPFlag("PrefixURL", flag.Lookup("prefixurl"))
|
||||
viper.BindPFlag("ViewMode", flag.Lookup("view-mode"))
|
||||
viper.BindPFlag("ReCaptchaKey", flag.Lookup("recaptcha-key"))
|
||||
viper.BindPFlag("ReCaptchaSecret", flag.Lookup("recaptcha-secret"))
|
||||
|
||||
viper.SetConfigName("filemanager")
|
||||
viper.AddConfigPath(".")
|
||||
}
|
||||
|
||||
func printVersion() {
|
||||
version = strings.TrimSpace(version)
|
||||
|
||||
if version == "" {
|
||||
fmt.Println("filemanager is at an untracked version")
|
||||
} else {
|
||||
version = strings.TrimPrefix(version, "v")
|
||||
fmt.Println("filemanager version", version)
|
||||
}
|
||||
|
||||
fmt.Println("filemanager version", filemanager.Version)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
@@ -176,9 +187,11 @@ func handler() http.Handler {
|
||||
}
|
||||
|
||||
fm := &filemanager.FileManager{
|
||||
NoAuth: viper.GetBool("NoAuth"),
|
||||
BaseURL: "",
|
||||
PrefixURL: "",
|
||||
NoAuth: viper.GetBool("NoAuth"),
|
||||
BaseURL: viper.GetString("BaseURL"),
|
||||
PrefixURL: viper.GetString("PrefixURL"),
|
||||
ReCaptchaKey: viper.GetString("ReCaptchaKey"),
|
||||
ReCaptchaSecret: viper.GetString("ReCaptchaSecret"),
|
||||
DefaultUser: &filemanager.User{
|
||||
AllowCommands: viper.GetBool("AllowCommands"),
|
||||
AllowEdit: viper.GetBool("AllowEdit"),
|
||||
@@ -190,6 +203,7 @@ func handler() http.Handler {
|
||||
CSS: "",
|
||||
Scope: viper.GetString("Scope"),
|
||||
FileSystem: fileutils.Dir(viper.GetString("Scope")),
|
||||
ViewMode: viper.GetString("ViewMode"),
|
||||
},
|
||||
Store: &filemanager.Store{
|
||||
Config: bolt.ConfigStore{DB: db},
|
||||
|
||||
2
file.go
2
file.go
@@ -67,8 +67,6 @@ type Listing struct {
|
||||
Sort string `json:"sort"`
|
||||
// And which order.
|
||||
Order string `json:"order"`
|
||||
// Displays in mosaic or list.
|
||||
Display string `json:"display"`
|
||||
}
|
||||
|
||||
// GetInfo gets the file information and, in case of error, returns the
|
||||
|
||||
@@ -3,6 +3,7 @@ package filemanager
|
||||
import (
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -20,6 +21,9 @@ import (
|
||||
"github.com/robfig/cron"
|
||||
)
|
||||
|
||||
// Version is the current File Manager version.
|
||||
const Version = "1.3.4"
|
||||
|
||||
var (
|
||||
ErrExist = errors.New("the resource already exists")
|
||||
ErrNotExist = errors.New("the resource does not exist")
|
||||
@@ -62,6 +66,10 @@ type FileManager struct {
|
||||
// there will only exist one user, called "admin".
|
||||
NoAuth bool
|
||||
|
||||
// ReCaptcha Site key and secret.
|
||||
ReCaptchaKey string
|
||||
ReCaptchaSecret string
|
||||
|
||||
// StaticGen is the static websit generator handler.
|
||||
StaticGen StaticGen
|
||||
|
||||
@@ -71,10 +79,28 @@ type FileManager struct {
|
||||
// A map of events to a slice of commands.
|
||||
Commands map[string][]string
|
||||
|
||||
// Global stylesheet.
|
||||
CSS string
|
||||
|
||||
// NewFS should build a new file system for a given path.
|
||||
NewFS FSBuilder
|
||||
}
|
||||
|
||||
var commandEvents = []string{
|
||||
"before_save",
|
||||
"after_save",
|
||||
"before_publish",
|
||||
"after_publish",
|
||||
"before_copy",
|
||||
"after_copy",
|
||||
"before_rename",
|
||||
"after_rename",
|
||||
"before_upload",
|
||||
"after_upload",
|
||||
"before_delete",
|
||||
"after_delete",
|
||||
}
|
||||
|
||||
// Command is a command function.
|
||||
type Command func(r *http.Request, m *FileManager, u *User) error
|
||||
|
||||
@@ -108,16 +134,39 @@ func (m *FileManager) Setup() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get the global CSS.
|
||||
err = m.Store.Config.Get("css", &m.CSS)
|
||||
if err != nil && err == ErrNotExist {
|
||||
err = m.Store.Config.Save("css", "")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Tries to get the event commands from the database.
|
||||
// If they don't exist, initialize them.
|
||||
err = m.Store.Config.Get("commands", &m.Commands)
|
||||
if err != nil && err == ErrNotExist {
|
||||
m.Commands = map[string][]string{
|
||||
"before_save": {},
|
||||
"after_save": {},
|
||||
"before_publish": {},
|
||||
"after_publish": {},
|
||||
|
||||
if err == nil {
|
||||
// Add hypothetically new command handlers.
|
||||
for _, command := range commandEvents {
|
||||
if _, ok := m.Commands[command]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
m.Commands[command] = []string{}
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil && err == ErrNotExist {
|
||||
m.Commands = map[string][]string{}
|
||||
|
||||
// Initialize the command handlers.
|
||||
for _, command := range commandEvents {
|
||||
m.Commands[command] = []string{}
|
||||
}
|
||||
|
||||
err = m.Store.Config.Save("commands", m.Commands)
|
||||
}
|
||||
|
||||
@@ -156,6 +205,14 @@ func (m *FileManager) Setup() error {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: remove this after 1.5
|
||||
for _, user := range users {
|
||||
if user.ViewMode != "list" && user.ViewMode != "mosaic" {
|
||||
user.ViewMode = "list"
|
||||
m.Store.Users.Update(user, "ViewMode")
|
||||
}
|
||||
}
|
||||
|
||||
m.DefaultUser.Username = ""
|
||||
m.DefaultUser.Password = ""
|
||||
|
||||
@@ -232,7 +289,7 @@ func (m FileManager) ShareCleaner() {
|
||||
}
|
||||
|
||||
// Runner runs the commands for a certain event type.
|
||||
func (m FileManager) Runner(event string, path string) error {
|
||||
func (m FileManager) Runner(event string, path string, destination string, user *User) error {
|
||||
commands := []string{}
|
||||
|
||||
// Get the commands from the File Manager instance itself.
|
||||
@@ -257,7 +314,15 @@ func (m FileManager) Runner(event string, path string) error {
|
||||
}
|
||||
|
||||
cmd := exec.Command(command, args...)
|
||||
cmd.Env = append(os.Environ(), "file="+path)
|
||||
cmd.Env = append(os.Environ(), fmt.Sprintf("FILE=%s", path))
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("ROOT=%s", string(user.Scope)))
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("TRIGGER=%s", event))
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("USERNAME=%s", user.Username))
|
||||
|
||||
if destination != "" {
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("DESTINATION=%s", destination))
|
||||
}
|
||||
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
@@ -286,6 +351,7 @@ var DefaultUser = User{
|
||||
AllowEdit: true,
|
||||
AllowNew: true,
|
||||
AllowPublish: true,
|
||||
LockPassword: false,
|
||||
Commands: []string{},
|
||||
Rules: []*Rule{},
|
||||
CSS: "",
|
||||
@@ -293,6 +359,7 @@ var DefaultUser = User{
|
||||
Locale: "en",
|
||||
Scope: ".",
|
||||
FileSystem: fileutils.Dir("."),
|
||||
ViewMode: "mosaic",
|
||||
}
|
||||
|
||||
// User contains the configuration for each user.
|
||||
@@ -325,6 +392,9 @@ type User struct {
|
||||
// Locale is the language of the user.
|
||||
Locale string `json:"locale"`
|
||||
|
||||
// Prevents the user to change its password.
|
||||
LockPassword bool `json:"lockPassword"`
|
||||
|
||||
// These indicate if the user can perform certain actions.
|
||||
AllowNew bool `json:"allowNew"` // Create files and folders
|
||||
AllowEdit bool `json:"allowEdit"` // Edit/rename files
|
||||
@@ -333,6 +403,9 @@ type User struct {
|
||||
|
||||
// Commands is the list of commands the user can execute.
|
||||
Commands []string `json:"commands"`
|
||||
|
||||
// User view mode for files and folders.
|
||||
ViewMode string `json:"viewMode"`
|
||||
}
|
||||
|
||||
// Allowed checks if the user has permission to access a directory/file.
|
||||
|
||||
59
http/auth.go
59
http/auth.go
@@ -1,8 +1,11 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -11,6 +14,45 @@ import (
|
||||
fm "github.com/hacdias/filemanager"
|
||||
)
|
||||
|
||||
type cred struct {
|
||||
Password string `json:"password"`
|
||||
Username string `json:"username"`
|
||||
Recaptcha string `json:"recaptcha"`
|
||||
}
|
||||
|
||||
// recaptcha checks the recaptcha code.
|
||||
func recaptcha(secret string, response string) (bool, error) {
|
||||
api := "https://www.google.com/recaptcha/api/siteverify"
|
||||
|
||||
body := url.Values{}
|
||||
body.Set("secret", secret)
|
||||
body.Add("response", response)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Post(api, "application/x-www-form-urlencoded", bytes.NewBufferString(body.Encode()))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
var data struct {
|
||||
Success bool `json:"success"`
|
||||
ChallengeTS time.Time `json:"challenge_ts"`
|
||||
Hostname string `json:"hostname"`
|
||||
ErrorCodes interface{} `json:"error-codes"`
|
||||
}
|
||||
|
||||
err = json.NewDecoder(resp.Body).Decode(&data)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return data.Success, nil
|
||||
}
|
||||
|
||||
// authHandler proccesses the authentication for the user.
|
||||
func authHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
// NoAuth instances shouldn't call this method.
|
||||
@@ -19,7 +61,7 @@ func authHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, er
|
||||
}
|
||||
|
||||
// Receive the credentials from the request and unmarshal them.
|
||||
var cred fm.User
|
||||
var cred cred
|
||||
if r.Body == nil {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
@@ -29,6 +71,19 @@ func authHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, er
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
// If ReCaptcha is enabled, check the code.
|
||||
if len(c.ReCaptchaSecret) > 0 {
|
||||
ok, err := recaptcha(c.ReCaptchaSecret, cred.Recaptcha)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return http.StatusForbidden, err
|
||||
}
|
||||
|
||||
if !ok {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Checks if the user exists.
|
||||
u, err := c.Store.Users.GetByUsername(cred.Username, c.NewFS)
|
||||
if err != nil {
|
||||
@@ -59,7 +114,6 @@ func renewAuthHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (in
|
||||
// claims is the JWT claims.
|
||||
type claims struct {
|
||||
fm.User
|
||||
NoAuth bool `json:"noAuth"`
|
||||
jwt.StandardClaims
|
||||
}
|
||||
|
||||
@@ -74,7 +128,6 @@ func printToken(c *fm.Context, w http.ResponseWriter) (int, error) {
|
||||
// Builds the claims.
|
||||
claims := claims{
|
||||
u,
|
||||
c.NoAuth,
|
||||
jwt.StandardClaims{
|
||||
ExpiresAt: time.Now().Add(time.Hour * 24).Unix(),
|
||||
Issuer: "File Manager",
|
||||
|
||||
@@ -25,7 +25,7 @@ func downloadHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int
|
||||
if r.URL.Query().Get("inline") == "true" {
|
||||
w.Header().Set("Content-Disposition", "inline")
|
||||
} else {
|
||||
w.Header().Set("Content-Disposition", "attachment; filename="+c.File.Name)
|
||||
w.Header().Set("Content-Disposition", "attachment; filename=\""+c.File.Name+"\"")
|
||||
}
|
||||
|
||||
http.ServeFile(w, r, c.File.Path)
|
||||
@@ -107,7 +107,7 @@ func downloadHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
w.Header().Set("Content-Disposition", "attachment; filename="+name)
|
||||
w.Header().Set("Content-Disposition", "attachment; filename=\""+name+"\"")
|
||||
_, err = io.Copy(w, file)
|
||||
return 0, err
|
||||
}
|
||||
|
||||
63
http/http.go
63
http/http.go
@@ -6,10 +6,10 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/asdine/storm"
|
||||
fm "github.com/hacdias/filemanager"
|
||||
)
|
||||
|
||||
@@ -54,11 +54,7 @@ func serve(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
// Check if this request is made to the service worker. If so,
|
||||
// pass it through a template to add the needed variables.
|
||||
if r.URL.Path == "/sw.js" {
|
||||
return renderFile(
|
||||
c, w,
|
||||
c.Assets.MustString("sw.js"),
|
||||
"application/javascript",
|
||||
)
|
||||
return renderFile(c, w, "sw.js")
|
||||
}
|
||||
|
||||
// Checks if this request is made to the static assets folder. If so, and
|
||||
@@ -96,11 +92,7 @@ func serve(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
w.Header().Set("x-content-type", "nosniff")
|
||||
w.Header().Set("x-xss-protection", "1; mode=block")
|
||||
|
||||
return renderFile(
|
||||
c, w,
|
||||
c.Assets.MustString("index.html"),
|
||||
"text/html",
|
||||
)
|
||||
return renderFile(c, w, "index.html")
|
||||
}
|
||||
|
||||
// staticHandler handles the static assets path.
|
||||
@@ -110,11 +102,7 @@ func staticHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int,
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
return renderFile(
|
||||
c, w,
|
||||
c.Assets.MustString("static/manifest.json"),
|
||||
"application/json",
|
||||
)
|
||||
return renderFile(c, w, "static/manifest.json")
|
||||
}
|
||||
|
||||
// apiHandler is the main entry point for the /api endpoint.
|
||||
@@ -220,11 +208,32 @@ func splitURL(path string) (string, string) {
|
||||
}
|
||||
|
||||
// renderFile renders a file using a template with some needed variables.
|
||||
func renderFile(c *fm.Context, w http.ResponseWriter, file string, contentType string) (int, error) {
|
||||
tpl := template.Must(template.New("file").Parse(file))
|
||||
func renderFile(c *fm.Context, w http.ResponseWriter, file string) (int, error) {
|
||||
tpl := template.Must(template.New("file").Parse(c.Assets.MustString(file)))
|
||||
|
||||
var contentType string
|
||||
switch filepath.Ext(file) {
|
||||
case ".html":
|
||||
contentType = "text/html"
|
||||
case ".js":
|
||||
contentType = "application/javascript"
|
||||
case ".json":
|
||||
contentType = "application/json"
|
||||
default:
|
||||
contentType = "text"
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", contentType+"; charset=utf-8")
|
||||
|
||||
data := map[string]interface{}{"BaseURL": c.RootURL()}
|
||||
data := map[string]interface{}{
|
||||
"BaseURL": c.RootURL(),
|
||||
"NoAuth": c.NoAuth,
|
||||
"Version": fm.Version,
|
||||
"CSS": template.CSS(c.CSS),
|
||||
"ReCaptcha": c.ReCaptchaKey != "" && c.ReCaptchaSecret != "",
|
||||
"ReCaptchaKey": c.ReCaptchaKey,
|
||||
"ReCaptchaSecret": c.ReCaptchaSecret,
|
||||
}
|
||||
|
||||
if c.StaticGen != nil {
|
||||
data["StaticGen"] = c.StaticGen.Name()
|
||||
@@ -242,12 +251,9 @@ func renderFile(c *fm.Context, w http.ResponseWriter, file string, contentType s
|
||||
// sharePage build the share page.
|
||||
func sharePage(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
s, err := c.Store.Share.Get(r.URL.Path)
|
||||
if err == storm.ErrNotFound {
|
||||
return renderFile(
|
||||
c, w,
|
||||
c.Assets.MustString("static/share/404.html"),
|
||||
"text/html",
|
||||
)
|
||||
if err == fm.ErrNotExist {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return renderFile(c, w, "static/share/404.html")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -256,11 +262,8 @@ func sharePage(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, erro
|
||||
|
||||
if s.Expires && s.ExpireDate.Before(time.Now()) {
|
||||
c.Store.Share.Delete(s.Hash)
|
||||
return renderFile(
|
||||
c, w,
|
||||
c.Assets.MustString("static/share/404.html"),
|
||||
"text/html",
|
||||
)
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return renderFile(c, w, "static/share/404.html")
|
||||
}
|
||||
|
||||
r.URL.Path = s.Path
|
||||
|
||||
@@ -38,7 +38,7 @@ func resourceHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int
|
||||
case http.MethodPut:
|
||||
// Before save command handler.
|
||||
path := filepath.Join(c.User.Scope, r.URL.Path)
|
||||
if err := c.Runner("before_save", path); err != nil {
|
||||
if err := c.Runner("before_save", path, "", c.User); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ func resourceHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int
|
||||
}
|
||||
|
||||
// After save command handler.
|
||||
if err := c.Runner("after_save", path); err != nil {
|
||||
if err := c.Runner("after_save", path, "", c.User); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
@@ -130,8 +130,6 @@ func listingHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int,
|
||||
}
|
||||
|
||||
listing.ApplySort()
|
||||
listing.Display = displayMode(w, r, cookieScope)
|
||||
|
||||
return renderJSON(w, f)
|
||||
}
|
||||
|
||||
@@ -141,12 +139,22 @@ func resourceDeleteHandler(c *fm.Context, w http.ResponseWriter, r *http.Request
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
// Fire the before trigger.
|
||||
if err := c.Runner("before_delete", r.URL.Path, "", c.User); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
// Remove the file or folder.
|
||||
err := c.User.FileSystem.RemoveAll(r.URL.Path)
|
||||
if err != nil {
|
||||
return ErrorToHTTP(err, true), err
|
||||
}
|
||||
|
||||
// Fire the after trigger.
|
||||
if err := c.Runner("after_delete", r.URL.Path, "", c.User); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
|
||||
@@ -160,7 +168,7 @@ func resourcePostPutHandler(c *fm.Context, w http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
|
||||
// Discard any invalid upload before returning to avoid connection
|
||||
// reset fm.Error.
|
||||
// reset error.
|
||||
defer func() {
|
||||
io.Copy(ioutil.Discard, r.Body)
|
||||
}()
|
||||
@@ -179,14 +187,19 @@ func resourcePostPutHandler(c *fm.Context, w http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
|
||||
// If using POST method, we are trying to create a new file so it is not
|
||||
// desirable to ovfm.Erride an already existent file. Thus, we check
|
||||
// desirable to override an already existent file. Thus, we check
|
||||
// if the file already exists. If so, we just return a 409 Conflict.
|
||||
if r.Method == http.MethodPost && r.Header.Get("Action") != "ovfm.Erride" {
|
||||
if r.Method == http.MethodPost && r.Header.Get("Action") != "override" {
|
||||
if _, err := c.User.FileSystem.Stat(r.URL.Path); err == nil {
|
||||
return http.StatusConflict, errors.New("There is already a file on that path")
|
||||
}
|
||||
}
|
||||
|
||||
// Fire the before trigger.
|
||||
if err := c.Runner("before_upload", r.URL.Path, "", c.User); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
// Create/Open the file.
|
||||
f, err := c.User.FileSystem.OpenFile(r.URL.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0776)
|
||||
if err != nil {
|
||||
@@ -218,6 +231,12 @@ func resourcePostPutHandler(c *fm.Context, w http.ResponseWriter, r *http.Reques
|
||||
// Writes the ETag Header.
|
||||
etag := fmt.Sprintf(`"%x%x"`, fi.ModTime().UnixNano(), fi.Size())
|
||||
w.Header().Set("ETag", etag)
|
||||
|
||||
// Fire the after trigger.
|
||||
if err := c.Runner("after_upload", r.URL.Path, "", c.User); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
|
||||
@@ -256,7 +275,7 @@ func resourcePublish(c *fm.Context, w http.ResponseWriter, r *http.Request) (int
|
||||
path := filepath.Join(c.User.Scope, r.URL.Path)
|
||||
|
||||
// Before save command handler.
|
||||
if err := c.Runner("before_publish", path); err != nil {
|
||||
if err := c.Runner("before_publish", path, "", c.User); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
@@ -266,7 +285,7 @@ func resourcePublish(c *fm.Context, w http.ResponseWriter, r *http.Request) (int
|
||||
}
|
||||
|
||||
// Executed the before publish command.
|
||||
if err := c.Runner("before_publish", path); err != nil {
|
||||
if err := c.Runner("before_publish", path, "", c.User); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
@@ -293,40 +312,36 @@ func resourcePatchHandler(c *fm.Context, w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
if action == "copy" {
|
||||
// Fire the after trigger.
|
||||
if err := c.Runner("before_copy", src, dst, c.User); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
// Copy the file.
|
||||
err = c.User.FileSystem.Copy(src, dst)
|
||||
|
||||
// Fire the after trigger.
|
||||
if err := c.Runner("after_copy", src, dst, c.User); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
} else {
|
||||
// Fire the after trigger.
|
||||
if err := c.Runner("before_rename", src, dst, c.User); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
// Rename the file.
|
||||
err = c.User.FileSystem.Rename(src, dst)
|
||||
|
||||
// Fire the after trigger.
|
||||
if err := c.Runner("after_rename", src, dst, c.User); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
}
|
||||
|
||||
return ErrorToHTTP(err, true), err
|
||||
}
|
||||
|
||||
// displayMode obtains the display mode from the Cookie.
|
||||
func displayMode(w http.ResponseWriter, r *http.Request, scope string) string {
|
||||
var displayMode string
|
||||
|
||||
// Checks the cookie.
|
||||
if displayCookie, err := r.Cookie("display"); err == nil {
|
||||
displayMode = displayCookie.Value
|
||||
}
|
||||
|
||||
// If it's invalid, set it to mosaic, which is the default.
|
||||
if displayMode == "" || (displayMode != "mosaic" && displayMode != "list") {
|
||||
displayMode = "mosaic"
|
||||
}
|
||||
|
||||
// Set the cookie.
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "display",
|
||||
Value: displayMode,
|
||||
MaxAge: 31536000,
|
||||
Path: scope,
|
||||
Secure: r.TLS != nil,
|
||||
})
|
||||
|
||||
return displayMode
|
||||
}
|
||||
|
||||
// handleSortOrder gets and stores for a Listing the 'sort' and 'order',
|
||||
// and reads 'limit' if given. The latter is 0 if not given. Sets cookies.
|
||||
func handleSortOrder(w http.ResponseWriter, r *http.Request, scope string) (sort string, order string, err error) {
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
type modifySettingsRequest struct {
|
||||
*modifyRequest
|
||||
Data struct {
|
||||
CSS string `json:"css"`
|
||||
Commands map[string][]string `json:"commands"`
|
||||
StaticGen map[string]interface{} `json:"staticGen"`
|
||||
} `json:"data"`
|
||||
@@ -61,6 +62,7 @@ func settingsHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int
|
||||
}
|
||||
|
||||
type settingsGetRequest struct {
|
||||
CSS string `json:"css"`
|
||||
Commands map[string][]string `json:"commands"`
|
||||
StaticGen []option `json:"staticGen"`
|
||||
}
|
||||
@@ -73,6 +75,7 @@ func settingsGetHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (
|
||||
result := &settingsGetRequest{
|
||||
Commands: c.Commands,
|
||||
StaticGen: []option{},
|
||||
CSS: c.CSS,
|
||||
}
|
||||
|
||||
if c.StaticGen != nil {
|
||||
@@ -114,6 +117,16 @@ func settingsPutHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
|
||||
// Update the global CSS.
|
||||
if mod.Which == "css" {
|
||||
if err := c.Store.Config.Save("css", mod.Data.CSS); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
c.CSS = mod.Data.CSS
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
|
||||
// Update the static generator options.
|
||||
if mod.Which == "staticGen" {
|
||||
err = mapstructure.Decode(mod.Data.StaticGen, c.StaticGen)
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/asdine/storm"
|
||||
fm "github.com/hacdias/filemanager"
|
||||
)
|
||||
|
||||
@@ -30,7 +29,7 @@ func shareHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, e
|
||||
func shareGetHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
path := filepath.Join(c.User.Scope, r.URL.Path)
|
||||
s, err := c.Store.Share.GetByPath(path)
|
||||
if err == storm.ErrNotFound {
|
||||
if err == fm.ErrNotExist {
|
||||
return http.StatusNotFound, nil
|
||||
}
|
||||
|
||||
@@ -45,6 +44,10 @@ func shareGetHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int
|
||||
}
|
||||
}
|
||||
|
||||
if len(s) == 0 {
|
||||
return http.StatusNotFound, nil
|
||||
}
|
||||
|
||||
return renderJSON(w, s)
|
||||
}
|
||||
|
||||
@@ -107,7 +110,7 @@ func sharePostHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (in
|
||||
|
||||
func shareDeleteHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
s, err := c.Store.Share.Get(strings.TrimPrefix(r.URL.Path, "/"))
|
||||
if err == storm.ErrNotFound {
|
||||
if err == fm.ErrNotExist {
|
||||
return http.StatusNotFound, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -194,7 +194,7 @@ func usersPostHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (in
|
||||
}
|
||||
|
||||
// Set the Location header and return.
|
||||
w.Header().Set("Location", "/users/"+strconv.Itoa(u.ID))
|
||||
w.Header().Set("Location", "/settings/users/"+strconv.Itoa(u.ID))
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
return 0, nil
|
||||
}
|
||||
@@ -268,12 +268,20 @@ func usersPutHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int
|
||||
return http.StatusBadRequest, err
|
||||
}
|
||||
|
||||
// If we're updating the default user. Only for NoAuth
|
||||
// implementations. Used to change the viewMode.
|
||||
if id == 0 && c.NoAuth {
|
||||
c.DefaultUser.ViewMode = u.ViewMode
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
|
||||
// Updates the CSS and locale.
|
||||
if which == "partial" {
|
||||
c.User.CSS = u.CSS
|
||||
c.User.Locale = u.Locale
|
||||
c.User.ViewMode = u.ViewMode
|
||||
|
||||
err = c.Store.Users.Update(c.User, "CSS", "Locale")
|
||||
err = c.Store.Users.Update(c.User, "CSS", "Locale", "ViewMode")
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
@@ -287,6 +295,10 @@ func usersPutHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int
|
||||
return http.StatusBadRequest, fm.ErrEmptyPassword
|
||||
}
|
||||
|
||||
if id == c.User.ID && c.User.LockPassword {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
c.User.Password, err = fm.HashPassword(u.Password)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
|
||||
5
package-lock.json
generated
5
package-lock.json
generated
@@ -4941,6 +4941,11 @@
|
||||
"resolved": "https://registry.npmjs.org/normalize.css/-/normalize.css-7.0.0.tgz",
|
||||
"integrity": "sha1-q/sd2CRwZ04DIrU86xqvQSk45L8="
|
||||
},
|
||||
"noty": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/noty/-/noty-3.1.2.tgz",
|
||||
"integrity": "sha512-5Rn8695fMcGTJdn8gIrG0sYXXNIEwc8BFq4EpyHF3rFwoAt7/kgN2lKrIZ0bUxtFZuYFcnfqwU+4uF6vm21bVg=="
|
||||
},
|
||||
"npm-run-path": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"filesize": "^3.5.10",
|
||||
"moment": "^2.18.1",
|
||||
"normalize.css": "^7.0.0",
|
||||
"noty": "^3.1.2",
|
||||
"vue": "^2.3.3",
|
||||
"vue-i18n": "^7.1.0",
|
||||
"vue-router": "^2.7.0",
|
||||
|
||||
24
publish.sh
Normal file
24
publish.sh
Normal file
@@ -0,0 +1,24 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "Building assets"
|
||||
./build.sh
|
||||
|
||||
echo "Updating version number to $1..."
|
||||
sed -i "s|(untracked)|$1|g" filemanager.go
|
||||
|
||||
echo "Commiting..."
|
||||
git add -A
|
||||
git commit -m "Version $1"
|
||||
git push
|
||||
|
||||
echo "Creating the tag..."
|
||||
git tag "v$1"
|
||||
git push --tags
|
||||
|
||||
echo "Commiting untracked version notice..."
|
||||
sed -i "s|$1|(untracked)|g" filemanager.go
|
||||
git add -A
|
||||
git commit -m "[ci skip] auto: setting untracked version"
|
||||
git push
|
||||
|
||||
echo "Done!"
|
||||
@@ -1 +1 @@
|
||||
6a08f1e90218ebb33745a5034efe896329ee9675
|
||||
be5b409fc6ffa5e4c331a99a624617b653373bee
|
||||
Reference in New Issue
Block a user