Compare commits

...

19 Commits

Author SHA1 Message Date
Henrique Dias
cac5413424 Version 1.3.2
Former-commit-id: 90d5ad658b45bedcba302eb5ba31bd419742b6da [formerly 7fbf1df2fdf4319d6079a844d03fc68d0e6272bf] [formerly 8c9bc269c306ba49250a959200b86f1035ff5565 [formerly 6fce037280]]
Former-commit-id: 6da196d1aa360654f7fb61f02d448e6d278d52ff [formerly 272a9fd5dc6092af922aac72040fa0fbe4ecfc52]
Former-commit-id: 07fe419998104c357feb2376e61a9d3c3a65273b
2017-09-07 18:28:10 +01:00
Henrique Dias
037efc2eb8 bugfixes
Former-commit-id: 38f7bdc4de8a29be11376e2321aaedb027b84a57 [formerly 72daa72c1b48182002bf81ac374b54dad2bae465] [formerly 8498a21191a043432e0a857cbc4aae49761bfdbf [formerly 2ad805d793]]
Former-commit-id: 068d502148b08cfd8dc627739847888f018f4417 [formerly 882e3aff3891329a62374c448e839ba41edebb45]
Former-commit-id: 7b71062e2c6e86c5e1ce36daaeb265972c941ca5
2017-09-07 18:27:30 +01:00
Henrique Dias
7de22b53b8 Settings styles; close #228
Former-commit-id: b564ba4e357b2d4b18c7f9407395894eb5e18159 [formerly 84ef220a2b10b8ed501c4499ef03d99acc148546] [formerly 00056633e5c2e947201a2dd1ad3bb937821edf61 [formerly e3212cd076]]
Former-commit-id: 866b84d788a5f8a9767affdef17806dbac984db6 [formerly a22d95ba5726dd5ea757410249bef37654213a68]
Former-commit-id: 4600591829c0a41c5de9defc3e30fbac28815e25
2017-09-07 18:19:29 +01:00
Henrique Dias
ae19731015 Merge branch 'master' of https://github.com/hacdias/filemanager
Former-commit-id: cda66995085b2fe15ba1327f5f21d6cb1e7fbad9 [formerly 1e91b652c2c9ac12f7148a5fcee6826823f861ae] [formerly 6e9c1d23074a4ef51a8342014600a64421e37411 [formerly 35742fe91f]]
Former-commit-id: aaf02c9e8fae32fe0507d4375bce87a97637ee0e [formerly 4b2b1e3a10edbe38434ba663d6888cc462afe749]
Former-commit-id: 02ad43301dbc1d5dcfa4e9dd6aca07689d7df378
2017-09-07 16:39:03 +01:00
Henrique Dias
8aa0797019 build assets
Former-commit-id: 8ac0fd77c313bc4527531ba8fcfbab0723108d4e [formerly 83f9d8ce20bacf6259216c0d902957e4c2b9d798] [formerly 04e9fd8ecdf88a44f562da449ff08bb16c108f5f [formerly fcfc28d09e]]
Former-commit-id: 36785e0d78ac8f7af84f4a1071d4bf226eac2366 [formerly 874b8426ee0aa485fe29b962c7ad6e01d90758ad]
Former-commit-id: ed6bd42a0bf059265816edb0085abbc9fbfda4dc
2017-09-07 16:38:58 +01:00
Maxime Daniel
17b3a403a5 Add new triggers and improve environment variables (#238)
* Add copy/rename trigger and more environment variables

* Add after_upload and after_delete trigger

* Fix scope since merge with master

* Add new before triggers


Former-commit-id: 65f7d47840980e5e9f330ae29053b37f2b98bcdb [formerly 9ec3d017d7f072d1bce2492c3a3e9839aab679a0] [formerly 2359047e3b51594ec705f2baf30b73f3bfc4c6cd [formerly 51295f999d]]
Former-commit-id: f128ee9a69ee280f0488aeaa6c8d86337427bc80 [formerly 311dd0b0207a58427d51a07244c3b0ff310dfc6d]
Former-commit-id: 734321b412e9ec7a5d848f9579e134ebce58145f
2017-09-07 16:38:32 +01:00
Henrique Dias
819d511690 materialdesignish
Former-commit-id: 5624723eeb939734902eeaa6c2f132c4beffa911 [formerly a83456d3eda5175e2941b2deeb58b5da323ff678] [formerly a6b3ac7942dbf48d7d9b3f8db5e5041a93143f19 [formerly 4d6b54c63e]]
Former-commit-id: d03621c16b2c892701d678361b6c0a7d5dbec620 [formerly d818b6751e035f283e1c8390d7993a33a459a7dd]
Former-commit-id: 3539ee68532135467daa2cc175482769b1efb592
2017-09-07 16:37:11 +01:00
Henrique Dias
30cfd06e3d change location of database docker
Former-commit-id: f4b3c8ffe4d6772abed3064173ba0ea8efaa806a [formerly 4792bef0a22c9a132ef1a8d239e92bfcb319c38e] [formerly b0de425e325346a805635a3907f3441556ece17b [formerly 6d2fab77e8]]
Former-commit-id: be1ed94475486cf7d44b339f11b232e284efe8b9 [formerly 167c46931b5751dc7455551cadcc075f646742a5]
Former-commit-id: e8ccb7b598697255949abdcfd6ea760fd2e401f5
2017-09-07 14:19:05 +01:00
Henrique Dias
c5558e6e41 Merge branch 'master' of https://github.com/hacdias/filemanager
Former-commit-id: 068b3d896b21bf786e9a2b7758eece8fa5071058 [formerly 7d80cfb73a7481dee4c95c0f3c39a95d13fd2f7c] [formerly 2b4adf5432b9fd7bb49b244955676db4ba6c5fdd [formerly e7bdba8948]]
Former-commit-id: 4f37bb0dc50a5acbf8956f0ef19d4ecebc64a534 [formerly 4eb5ba6be4bc857fe05862485d76e060bcf37fe6]
Former-commit-id: 98af155a74f74287f36fa30d29b8036c73fa1302
2017-09-07 14:18:10 +01:00
Henrique Dias
c3b3099ebb 1st phase - Global CSS
Former-commit-id: 508c4ab746f994bb3f4f5e86ff1ca5e6dc873f3a [formerly 8a29c22f817f54abefe21116e46f21b24306b6b8] [formerly e23c1a85571f61877b67656ef361f1c15acfcb3d [formerly 67fb6f8a78]]
Former-commit-id: 71e9024cf9107ef4e40f95bd388068fe052ea4f0 [formerly 7861ffe8d20f0777ae48c4f159efd7e32b2204d9]
Former-commit-id: 7db04ac5016d182f13ac56911d0a10871fb261f3
2017-09-07 14:17:56 +01:00
Ricardo Gonçalves
b1d47daa69 Docker image built from scratch (#237)
* Docker image built from scratch

* ldflags aren't need anymore


Former-commit-id: c527464833492254cdd14ced0f182fd78fbf4dda [formerly 5d99678c525e565d5b39f3bd645906b4e997752c] [formerly 80463ad3ae8e60456d5725277125c800bb2d6f50 [formerly 55789107e0]]
Former-commit-id: cea3a7799a9bd767008cb1b854314f6414892aff [formerly e682f6a01994537199f8c178de7526b961591d2b]
Former-commit-id: e76349c3eef778dc5913fb7128ff2b69907e9433
2017-09-07 14:15:25 +01:00
Henrique Dias
f51e2d5ba1 fix #234
Former-commit-id: 7e0b0a321a0bf352eb3530fc8d1250ba04499c87 [formerly a431ecc1f8f4d79b6ef98529ca38b7a00ba332d2] [formerly cc09c86c0b996c5c01bd4b7e65671e545e3c1828 [formerly dd7cd110db]]
Former-commit-id: 7bd178d355de3228aa03ec3cbb814e52588ea7df [formerly 35095c2b6728944bea8173c3ea1904c632ab30b9]
Former-commit-id: f2e8e8539a5ffce98ea186f42fd183710bc2f9a8
2017-09-07 11:42:17 +01:00
Henrique Dias
386974657e Build assets
Former-commit-id: 3ac594e7a4f3e806f56ed23790a1a195c87d941e [formerly 6e6b55d1430bddce55b701c04da53c1d746b4168] [formerly e3b0d8c63cf8bacb88c90d2f4e1c5163a448e473 [formerly 59b99ee298]]
Former-commit-id: 5d32e7939ed0941e60ec56f5a7f21fd78216706c [formerly 9c8c8a52a0633de356322fbdc0c54a9f5395daaa]
Former-commit-id: dbd51a43c6e8bc3df50ae57aa98e60bb611ab066
2017-09-07 10:43:32 +01:00
Henrique Dias
f7858cd719 Default view mode
Former-commit-id: 9212e217514046246a2bde10319763b05998b223 [formerly 8643e0ba3b4246ac0320b5d96dee4ba95e74a346] [formerly 77adb69159dce9cc89b631107340812916a2e279 [formerly 5daacf0298]]
Former-commit-id: 1a264e8a297e2a5146db53cd6cc40b8baba778de [formerly 607685afb5b6c89703c0b6d82b3dcec07eb22bef]
Former-commit-id: 1732f02d1641236402976ae182ee03f7b936d0c1
2017-09-07 10:41:01 +01:00
Henrique Dias
b355a5c058 Close #232
Former-commit-id: ec2f7562e0830ebb98bc7b4d997d74f2e6d685a6 [formerly 281a652559131d195b76feefef5cf0303d312b1e] [formerly 11a192c2d6f5d9667c55fdcf3f028391f70f6793 [formerly d3d3cb3d4f]]
Former-commit-id: a55ef038e404c2e9b97a802bfbf562fac02a98cc [formerly 033ded413b4f7bd21b7ff60feced6b590203cc16]
Former-commit-id: 6fc7fedc5fc087790102a07c3db3060e82400411
2017-09-07 10:29:19 +01:00
Henrique Dias
50758b53f4 Autoplay music and videos #233
Former-commit-id: e6305724cc9b53b764ffc199cb0b114e4fa63984 [formerly 36e7978a12f653f156c01ee11c2f91d24f7a7ca8] [formerly 9927ed89f8c30a24141865a3a7a74ffa79180853 [formerly d555cc44a0]]
Former-commit-id: 42efd044dc45d99a8fcd16c29a1b903b54f99a9b [formerly 92f6b43d0420b782defa61c34d2eb5ae9e9244bf]
Former-commit-id: 8fa9ecdbd6cc57a0513c9a72b5ab0b029839af41
2017-09-07 10:07:24 +01:00
Kyle Bai
f9902d2bdb Update zh-tw translation (#236)
Former-commit-id: e2e8b607eaacbcea36a8d3389dd4502158d2ac8a [formerly d47475c6aab73ad3c0ddb08e90664a0d3d2d9547] [formerly d347499f05216a64cd8af1075525ca6b3b7c3c41 [formerly 4d1b2cbb14]]
Former-commit-id: 229b92d7ce1143d6f9768ef8f52b18f5ead937b8 [formerly 673c0452c0d7a677a7c6fbbef941e47701bafe47]
Former-commit-id: 7e8d93b3114d545139cb2f436cf16bdd481713c7
2017-09-07 09:41:03 +01:00
Henrique Dias
b1512d2b66 remove served with from translations
Former-commit-id: 8fd3db2a2303fd77e51230e4ca9336013021d55d [formerly 3a6d99cad26160eda920508962ffa4f23fe2acd4] [formerly 969fef4feac9234ac7d165e4b8c2d572cf41e493 [formerly cbba245f84]]
Former-commit-id: 85af99ac3e6c48967e1c5d11e0fc6403492dfb0d [formerly ef7e5d4f0b655ca32783c21a6678f5e2c6ff8a1d]
Former-commit-id: 88024e6c39480df5799d892f1a7517b5345b7e22
2017-09-03 10:54:48 +01:00
Henrique Dias
1cfd31756d untracked version Sun, Sep 3, 2017 10:51:45 AM
Former-commit-id: 3f9a066d3456fd0435b5df19a6071f403761c363 [formerly 7ed9f0a686c30d97af32dfb7fa35b1f4dbbcc9ca] [formerly 8f473d98da1799211e7e7fd4e26dc9481297ea7d [formerly c60e7b9c0a]]
Former-commit-id: 732ae3300efa4444b06406743f9b8cce813e6b99 [formerly 465966ff48d99ca33496ff9dcf2d51e375985d0f]
Former-commit-id: 8c61fc2054cbecd2f0b7354884634f4b73f6e77f
2017-09-03 10:51:45 +01:00
54 changed files with 1148 additions and 804 deletions

View File

@@ -1,7 +1,7 @@
{ {
"port": 80, "port": 80,
"address": "", "address": "",
"database": "/etc/database.db", "database": "/database.db",
"scope": "/srv", "scope": "/srv",
"allowCommands": true, "allowCommands": true,
"allowEdit": true, "allowEdit": true,

View File

@@ -7,16 +7,16 @@ RUN apk add --no-cache git
RUN go get ./... RUN go get ./...
WORKDIR /go/src/github.com/hacdias/filemanager/cmd/filemanager 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 RUN mv filemanager /go/bin/filemanager
FROM alpine:latest FROM scratch
COPY --from=0 /go/bin/filemanager /usr/local/bin/filemanager COPY --from=0 /go/bin/filemanager /filemanager
VOLUME /srv VOLUME /srv
EXPOSE 80 EXPOSE 80
COPY Docker.json /etc/config.json COPY Docker.json /config.json
ENTRYPOINT ["/usr/local/bin/filemanager"] ENTRYPOINT ["/filemanager"]
CMD ["--config", "/etc/config.json"] CMD ["--config", "/config.json"]

View File

@@ -24,6 +24,9 @@
<!-- Add to home screen for Windows --> <!-- Add to home screen for Windows -->
<meta name="msapplication-TileImage" content="{{ .BaseURL }}/static/img/icons/msapplication-icon-144x144.png"> <meta name="msapplication-TileImage" content="{{ .BaseURL }}/static/img/icons/msapplication-icon-144x144.png">
<meta name="msapplication-TileColor" content="#2979ff"> <meta name="msapplication-TileColor" content="#2979ff">
<script>CSS = "{{ .CSS }}"</script>
<% for (var chunk of webpack.chunks) { <% for (var chunk of webpack.chunks) {
for (var file of chunk.files) { for (var file of chunk.files) {
if (file.match(/\.(js|css)$/)) { %> if (file.match(/\.(js|css)$/)) { %>

View File

@@ -1,11 +1,11 @@
<template> <template>
<router-view></router-view> <router-view @update:css="updateCSS" @clean:css="cleanCSS"></router-view>
</template> </template>
<script> <script>
export default { export default {
name: 'app', name: 'app',
mounted: function () { mounted () {
// Remove loading animation. // Remove loading animation.
let loading = document.getElementById('loading') let loading = document.getElementById('loading')
loading.classList.add('done') loading.classList.add('done')
@@ -13,6 +13,36 @@ export default {
setTimeout(function () { setTimeout(function () {
loading.parentNode.removeChild(loading) loading.parentNode.removeChild(loading)
}, 200) }, 200)
this.updateCSS()
},
methods: {
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> </script>

View File

@@ -1,32 +1,35 @@
<template> <template>
<button @click="change" :aria-label="$t('buttons.switchView')" :title="$t('buttons.switchView')" class="action" id="switch-view-button"> <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> <span>{{ $t('buttons.switchView') }}</span>
</button> </button>
</template> </template>
<script> <script>
import { mapState, mapMutations } from 'vuex'
import { updateUser } from '@/utils/api'
export default { export default {
name: 'switch-button', name: 'switch-button',
computed: {
...mapState(['user']),
icon: function () {
if (this.user.viewMode === 'mosaic') return 'view_list'
return 'view_module'
}
},
methods: { methods: {
...mapMutations(['updateUser']),
change: function (event) { change: function (event) {
// If we are on mobile we should close the dropdown. // If we are on mobile we should close the dropdown.
this.$store.commit('closeHovers') 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') { updateUser(user, 'partial').then(() => {
display = 'list' this.updateUser({ viewMode: user.viewMode })
} }).catch(this.$showError)
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'
} }
} }
} }

View File

@@ -7,7 +7,7 @@
<input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" multiple> <input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" multiple>
</div> </div>
<div v-else id="listing" <div v-else id="listing"
:class="req.display" :class="user.viewMode"
@dragenter="dragEnter" @dragenter="dragEnter"
@dragend="dragEnd"> @dragend="dragEnd">
<div> <div>
@@ -98,7 +98,7 @@ export default {
name: 'listing', name: 'listing',
components: { Item }, components: { Item },
computed: { computed: {
...mapState(['req', 'selected']), ...mapState(['req', 'selected', 'user']),
nameSorted () { nameSorted () {
return (this.req.sort === 'name') return (this.req.sort === 'name')
}, },

View File

@@ -20,8 +20,8 @@
<div class="preview"> <div class="preview">
<img v-if="req.type == 'image'" :src="raw()"> <img v-if="req.type == 'image'" :src="raw()">
<audio v-else-if="req.type == 'audio'" :src="raw()" controls></audio> <audio v-else-if="req.type == 'audio'" :src="raw()" autoplay controls></audio>
<video v-else-if="req.type == 'video'" :src="raw()" controls> <video v-else-if="req.type == 'video'" :src="raw()" autoplay controls>
Sorry, your browser doesn't support embedded videos, Sorry, your browser doesn't support embedded videos,
but don't worry, you can <a :href="download()">download it</a> but don't worry, you can <a :href="download()">download it</a>
and watch it with your favorite video player! and watch it with your favorite video player!

View File

@@ -1,19 +1,23 @@
<template> <template>
<div class="prompt"> <div class="card floating">
<h3>{{ $t('prompts.copy') }}</h3> <div class="card-title">
<p>{{ $t('prompts.copyMessage') }}</p> <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> <div class="card-action">
<button class="ok" <button class="cancel flat"
@click="copy"
:aria-label="$t('buttons.copy')"
:title="$t('buttons.copy')">{{ $t('buttons.copy') }}</button>
<button class="cancel"
@click="$store.commit('closeHovers')" @click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button> :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>
</div> </div>
</template> </template>

View File

@@ -1,16 +1,18 @@
<template> <template>
<div class="prompt"> <div class="card floating">
<h3>{{ $t('prompts.deleteTitle') }}</h3> <div class="card-content">
<p v-show="req.kind !== 'listing'">{{ $t('prompts.deleteMessageSingle') }}</p> <p v-if="req.kind !== 'listing'">{{ $t('prompts.deleteMessageSingle') }}</p>
<p v-show="req.kind === 'listing'">{{ $t('prompts.deleteMessageMultiple', { count: selectedCount}) }}</p> <p v-else>{{ $t('prompts.deleteMessageMultiple', { count: selectedCount}) }}</p>
<div> </div>
<button @click="submit" <div class="card-action">
:aria-label="$t('buttons.delete')" <button @click="$store.commit('closeHovers')"
:title="$t('buttons.delete')">{{ $t('buttons.delete') }}</button> class="flat cancel"
<button class="cancel"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button> :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>
</div> </div>
</template> </template>

View File

@@ -1,13 +1,18 @@
<template> <template>
<div class="prompt" id="download"> <div class="card floating" id="download">
<h3>{{ $t('prompts.download') }}</h3> <div class="card-title">
<p>{{ $t('prompts.downloadMessage') }}</p> <h2>{{ $t('prompts.download') }}</h2>
</div>
<button @click="download('zip')" autofocus>zip</button> <div class="card-content">
<button @click="download('tar')" autofocus>tar</button> <p>{{ $t('prompts.downloadMessage') }}</p>
<button @click="download('targz')" autofocus>tar.gz</button>
<button @click="download('tarbz2')" autofocus>tar.bz2</button> <button class="block cancel" @click="download('zip')" autofocus>zip</button>
<button @click="download('tarxz')" autofocus>tar.xz</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> </div>
</template> </template>

View File

@@ -1,23 +1,27 @@
<template> <template>
<div class="prompt help"> <div class="card floating help">
<h3>{{ $t('help.help') }}</h3> <div class="card-title">
<h2>{{ $t('help.help') }}</h2>
</div>
<ul> <div class="card-content">
<li><strong>F1</strong> - {{ $t('help.f1') }}</li> <ul>
<li><strong>F2</strong> - {{ $t('help.f2') }}</li> <li><strong>F1</strong> - {{ $t('help.f1') }}</li>
<li><strong>DEL</strong> - {{ $t('help.del') }}</li> <li><strong>F2</strong> - {{ $t('help.f2') }}</li>
<li><strong>ESC</strong> - {{ $t('help.esc') }}</li> <li><strong>DEL</strong> - {{ $t('help.del') }}</li>
<li><strong>CTRL + S</strong> - {{ $t('help.ctrl.s') }}</li> <li><strong>ESC</strong> - {{ $t('help.esc') }}</li>
<li><strong>CTRL + F</strong> - {{ $t('help.ctrl.f') }}</li> <li><strong>CTRL + S</strong> - {{ $t('help.ctrl.s') }}</li>
<li><strong>CTRL + Click</strong> - {{ $t('help.ctrl.click') }}</li> <li><strong>CTRL + F</strong> - {{ $t('help.ctrl.f') }}</li>
<li><strong>Click</strong> - {{ $t('help.click') }}</li> <li><strong>CTRL + Click</strong> - {{ $t('help.ctrl.click') }}</li>
<li><strong>Double click</strong> - {{ $t('help.doubleClick') }}</li> <li><strong>Click</strong> - {{ $t('help.click') }}</li>
</ul> <li><strong>Double click</strong> - {{ $t('help.doubleClick') }}</li>
</ul>
</div>
<div> <div class="card-action">
<button type="submit" <button type="submit"
@click="$store.commit('closeHovers')" @click="$store.commit('closeHovers')"
class="ok" class="flat"
:aria-label="$t('buttons.ok')" :aria-label="$t('buttons.ok')"
:title="$t('buttons.ok')">{{ $t('buttons.ok') }}</button> :title="$t('buttons.ok')">{{ $t('buttons.ok') }}</button>
</div> </div>

View File

@@ -1,29 +1,33 @@
<template> <template>
<div class="prompt"> <div class="card floating">
<h3>{{ $t('prompts.fileInfo') }}</h3> <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 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><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.lastModified') }}:</strong> {{ humanTime() }}</p>
<section v-show="dir() && selected.length === 0"> <template v-if="dir() && selected.length === 0">
<p><strong>{{ $t('prompts.numberFiles') }}:</strong> {{ req.numFiles }}</p> <p><strong>{{ $t('prompts.numberFiles') }}:</strong> {{ req.numFiles }}</p>
<p><strong>{{ $t('prompts.numberDirs') }}:</strong> {{ req.numDirs }}</p> <p><strong>{{ $t('prompts.numberDirs') }}:</strong> {{ req.numDirs }}</p>
</section> </template>
<section v-show="!dir()"> <template v-if="!dir()">
<p><strong>MD5:</strong> <code><a @click="checksum($event, 'md5')">{{ $t('prompts.show') }}</a></code></p> <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>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>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> <p><strong>SHA512:</strong> <code><a @click="checksum($event, 'sha512')">{{ $t('prompts.show') }}</a></code></p>
</section> </template>
</div>
<div> <div class="card-action">
<button type="submit" <button type="submit"
@click="$store.commit('closeHovers')" @click="$store.commit('closeHovers')"
class="ok" class="flat"
:aria-label="$t('buttons.ok')" :aria-label="$t('buttons.ok')"
:title="$t('buttons.ok')">{{ $t('buttons.ok') }}</button> :title="$t('buttons.ok')">{{ $t('buttons.ok') }}</button>
</div> </div>

View File

@@ -1,19 +1,22 @@
<template> <template>
<div class="prompt"> <div class="card floating">
<h3>{{ $t('prompts.move') }}</h3> <div class="card-title">
<p>{{ $t('prompts.moveMessage') }}</p> <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> <div class="card-action">
<button class="ok" <button class="flat cancel"
@click="move"
:aria-label="$t('buttons.move')"
:title="$t('buttons.move')">{{ $t('buttons.move') }}</button>
<button class="cancel"
@click="$store.commit('closeHovers')" @click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button> :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>
</div> </div>
</template> </template>

View File

@@ -1,18 +1,24 @@
<template> <template>
<div class="prompt"> <div class="card floating">
<h3>{{ $t('prompts.newFile') }}</h3> <div class="card-title">
<p>{{ $t('prompts.newArchetype') }}</p> <h2>{{ $t('prompts.newFile') }}</h2>
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name"> </div>
<input type="text" @keyup.enter="submit" v-model.trim="archetype">
<div> <div class="card-content">
<button class="ok" <p>{{ $t('prompts.newArchetype') }}</p>
@click="submit" <input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
:aria-label="$t('buttons.create')" <input type="text" @keyup.enter="submit" v-model.trim="archetype">
:title="$t('buttons.create')">{{ $t('buttons.create') }}</button> </div>
<button class="cancel"
<div class="card-action">
<button class="flat cancel"
@click="$store.commit('closeHovers')" @click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button> :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>
</div> </div>
</template> </template>

View File

@@ -1,17 +1,23 @@
<template> <template>
<div class="prompt"> <div class="card floating">
<h3>{{ $t('prompts.newDir') }}</h3> <div class="card-title">
<p>{{ $t('prompts.newDirMessage') }}</p> <h2>{{ $t('prompts.newDir') }}</h2>
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name"> </div>
<div>
<button class="ok" <div class="card-content">
:aria-label="$t('buttons.create')" <p>{{ $t('prompts.newDirMessage') }}</p>
:title="$t('buttons.create')" <input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
@click="submit">{{ $t('buttons.create') }}</button> </div>
<button class="cancel"
<div class="card-action">
<button class="cancel flat"
@click="$store.commit('closeHovers')" @click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button> :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>
</div> </div>
</template> </template>

View File

@@ -1,17 +1,23 @@
<template> <template>
<div class="prompt"> <div class="card floating">
<h3>{{ $t('prompts.newFile') }}</h3> <div class="card-title">
<p>{{ $t('prompts.newFileMessage') }}</p> <h2>{{ $t('prompts.newFile') }}</h2>
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name"> </div>
<div>
<button class="ok" <div class="card-content">
@click="submit" <p>{{ $t('prompts.newFileMessage') }}</p>
:aria-label="$t('buttons.create')" <input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
:title="$t('buttons.create')">{{ $t('buttons.create') }}</button> </div>
<button class="cancel"
<div class="card-action">
<button class="cancel flat"
@click="$store.commit('closeHovers')" @click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button> :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>
</div> </div>
</template> </template>

View File

@@ -1,18 +1,24 @@
<template> <template>
<div class="prompt"> <div class="card floating">
<h3>{{ $t('prompts.rename') }}</h3> <div class="card-title">
<p>{{ $t('prompts.renameMessage') }} <code>{{ oldName() }}</code>:</p> <h2>{{ $t('prompts.rename') }}</h2>
</div>
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name"> <div class="card-content">
<div> <p>{{ $t('prompts.renameMessage') }} <code>{{ oldName() }}</code>:</p>
<button @click="submit" <input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
type="submit" </div>
:aria-label="$t('buttons.rename')"
:title="$t('buttons.rename')">{{ $t('buttons.rename') }}</button> <div class="card-action">
<button class="cancel" <button class="cancel flat"
@click="$store.commit('closeHovers')" @click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button> :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>
</div> </div>
</template> </template>

View File

@@ -1,17 +1,22 @@
<template> <template>
<div class="prompt"> <div class="card floating">
<h3>{{ $t('prompts.replace') }}</h3> <div class="card-title">
<p>{{ $t('prompts.replaceMessage') }}</p> <h2>{{ $t('prompts.replace') }}</h2>
</div>
<div> <div class="card-content">
<button class="ok" <p>{{ $t('prompts.replaceMessage') }}</p>
@click="showConfirm" </div>
:aria-label="$t('buttons.replace')"
:title="$t('buttons.replace')">{{ $t('buttons.replace') }}</button> <div class="card-action">
<button class="cancel" <button class="flat cancel"
@click="$store.commit('closeHovers')" @click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button> :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>
</div> </div>
</template> </template>

View File

@@ -1,17 +1,23 @@
<template> <template>
<div class="prompt"> <div class="card floating">
<h3>{{ $t('prompts.schedule') }}</h3> <div class="card-title">
<p>{{ $t('prompts.scheduleMessage') }}</p> <h2>{{ $t('prompts.schedule') }}</h2>
<input autofocus type="datetime-local" v-model="date"> </div>
<div>
<button class="ok" <div class="card-content">
@click="submit" <p>{{ $t('prompts.scheduleMessage') }}</p>
:aria-label="$t('buttons.schedule')" <input autofocus type="datetime-local" v-model="date">
:title="$t('buttons.schedule')">{{ $t('buttons.schedule') }}</button> </div>
<button class="cancel"
<div class="card-action">
<button class="cancel flat"
@click="close" @click="close"
:aria-label="$t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button> :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>
</div> </div>
</template> </template>

View File

@@ -1,51 +1,55 @@
<template> <template>
<div class="prompt" id="share"> <div class="card floating" id="share">
<h3>{{ $t('buttons.share') }}</h3> <div class="card-title">
<p></p> <h2>{{ $t('buttons.share') }}</h2>
<ul> </div>
<li v-if="!hasPermanent">
<a @click="getPermalink" :aria-label="$t('buttons.permalink')">{{ $t('buttons.permalink') }}</a>
</li>
<li v-for="link in links" :key="link.hash"> <div class="card-content">
<a :href="buildLink(link.hash)" target="_blank"> <ul>
<template v-if="link.expires">{{ humanTime(link.expireDate) }}</template> <li v-if="!hasPermanent">
<template v-else>{{ $t('permanent') }}</template> <a @click="getPermalink" :aria-label="$t('buttons.permalink')">{{ $t('buttons.permalink') }}</a>
</a> </li>
<button class="action" <li v-for="link in links" :key="link.hash">
@click="deleteLink($event, link)" <a :href="buildLink(link.hash)" target="_blank">
:aria-label="$t('buttons.delete')" <template v-if="link.expires">{{ humanTime(link.expireDate) }}</template>
:title="$t('buttons.delete')"><i class="material-icons">delete</i></button> <template v-else>{{ $t('permanent') }}</template>
</a>
<button class="action copy-clipboard" <button class="action"
:data-clipboard-text="buildLink(link.hash)" @click="deleteLink($event, link)"
:aria-label="$t('buttons.copyToClipboard')" :aria-label="$t('buttons.delete')"
:title="$t('buttons.copyToClipboard')"><i class="material-icons">content_paste</i></button> :title="$t('buttons.delete')"><i class="material-icons">delete</i></button>
</li>
<li> <button class="action copy-clipboard"
<input autofocus :data-clipboard-text="buildLink(link.hash)"
type="number" :aria-label="$t('buttons.copyToClipboard')"
max="2147483647" :title="$t('buttons.copyToClipboard')"><i class="material-icons">content_paste</i></button>
min="0" </li>
@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> <li>
<button class="cancel" <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')" @click="$store.commit('closeHovers')"
:aria-label="$t('buttons.close')" :aria-label="$t('buttons.close')"
:title="$t('buttons.close')">{{ $t('buttons.close') }}</button> :title="$t('buttons.close')">{{ $t('buttons.close') }}</button>

View File

@@ -1,8 +1,8 @@
body { body {
font-family: 'Roboto', sans-serif; font-family: 'Roboto', sans-serif;
padding-top: 4em; padding-top: 4em;
background-color: #f8f8f8; background-color: #fafafa;
color: #212121; color: #333333;
} }
* { * {
@@ -65,6 +65,53 @@ button:hover {
background-color: #1E88E5; 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 { .mobile-only {
display: none !important; display: none !important;
} }

View File

@@ -1,64 +1,34 @@
.dashboard { .dashboard {
max-width: 600px; 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; margin: 1em 0;
} }
.dashboard a { a {
color: inherit color: inherit
} }
.dashboard h1 button { select,
font-size: 0.5em; textarea,
float: right; input[type="text"],
} input[type="password"] {
padding: 0.5em 0;
.dashboard table { line-height: 1;
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;
display: block; display: block;
border: 0; border: 0;
border-bottom: 1px solid #dddddd; border-bottom: 1px solid #dddddd;
transition: .2s ease border; transition: .2s ease border;
width: 100%; 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, .dashboard #locale,
@@ -69,49 +39,32 @@ form.dashboard > p:last-child {
} }
.dashboard #locale { .dashboard #locale {
border: 1px solid #dddddd;
margin-top: .5em; margin-top: .5em;
} }
.dashboard textarea:focus, textarea:focus,
.dashboard textarea:hover, textarea:hover,
.dashboard input[type="text"]:focus, input[type="text"]:focus,
.dashboard input[type="password"]:focus, input[type="password"]:focus,
.dashboard input[type="text"]:hover, input[type="text"]:hover,
.dashboard input[type="password"]:hover { input[type="password"]:hover {
border-color: #2979ff; border-color: #2979ff;
} }
.dashboard input.red { input.red {
border-color: red; border-color: red;
} }
.dashboard input.green { input.green {
border-color: 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 { .dashboard p label {
margin-bottom: .2em; margin-bottom: .2em;
display: block; display: block;
font-size: .8em; font-size: .8em;
font-weight: bold; font-weight: 500;
color: rgba(0, 0, 0, 0.57);
} }
li code, li code,
@@ -141,7 +94,7 @@ p code {
.dashboard #nav li { .dashboard #nav li {
width: 100%; width: 100%;
padding: 0 0 1em; padding: 0 0 1em;
border-bottom: 1px solid rgba(0, 0, 0, 0.05); border-bottom: 2px solid rgba(0, 0, 0, 0.05);
} }
.dashboard #nav li.active { .dashboard #nav li.active {
@@ -152,3 +105,316 @@ p code {
font-size: 1em; font-size: 1em;
vertical-align: middle; 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;
}

View File

@@ -122,8 +122,9 @@ header .search-button {
#search input { #search input {
width: 100%; width: 100%;
border: 0; border: 0;
outline: 0;
background-color: transparent; background-color: transparent;
line-height: 0;
padding: 0;
} }
#search #result { #search #result {

View File

@@ -159,7 +159,7 @@
#listing.list .item.header { #listing.list .item.header {
display: flex !important; display: flex !important;
background: #f8f8f8; background: #fafafa;
position: fixed; position: fixed;
width: calc(100% - 19em); width: calc(100% - 19em);
top: 7em; top: 7em;

View File

@@ -1,229 +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;
}
.prompt#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;
}
.prompt#share .action.copy-clipboard.active::after {
opacity: 1;
}

View File

@@ -3,7 +3,6 @@
@import "./fonts.css"; @import "./fonts.css";
@import "./base.css"; @import "./base.css";
@import "./header.css"; @import "./header.css";
@import "./prompts.css";
@import "./listing.css"; @import "./listing.css";
@import "./editor.css"; @import "./editor.css";
@import "./dashboard.css"; @import "./dashboard.css";

View File

@@ -117,7 +117,7 @@ settings:
commandsHelp: > commandsHelp: >
Here you can set commands that are executed in the named events. You 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 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. of the file.
commandsUpdated: Commands updated! commandsUpdated: Commands updated!
customStylesheet: Custom Stylesheet customStylesheet: Custom Stylesheet
@@ -168,7 +168,6 @@ sidebar:
myFiles: My files myFiles: My files
newFile: New file newFile: New file
newFolder: New folder newFolder: New folder
servedWith: Served with
settings: Settings settings: Settings
siteSettings: Site Settings siteSettings: Site Settings
hugoNew: Hugo New hugoNew: Hugo New

View File

@@ -115,7 +115,7 @@ settings:
commandsHelp: > commandsHelp: >
Ici vous pouvez définir des commandes qui seront exécutées lors de l'évènement correspondant. 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, 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. et contiendra le chemin d'accès vers le fichier.
commandsUpdated: Commandes mises à jour ! commandsUpdated: Commandes mises à jour !
customStylesheet: Feuille de style personnalisée customStylesheet: Feuille de style personnalisée
@@ -161,7 +161,6 @@ sidebar:
myFiles: Mes fichiers myFiles: Mes fichiers
newFile: Nouveau fichier newFile: Nouveau fichier
newFolder: Nouveau dossier newFolder: Nouveau dossier
servedWith: Géré avec
settings: Paramètres settings: Paramètres
siteSettings: Paramètres du site siteSettings: Paramètres du site
hugoNew: Nouveau Hugo hugoNew: Nouveau Hugo

View File

@@ -118,7 +118,7 @@ settings:
ここで、名前付きイベントに実行するコマンドを設定することができます。\ ここで、名前付きイベントに実行するコマンドを設定することができます。\
一行にコマンド一つを入力してください。\ 一行にコマンド一つを入力してください。\
イベントはファイルに関連する場合、例えばファイル保存の前にまたは後で、\ イベントはファイルに関連する場合、例えばファイル保存の前にまたは後で、\
環境変数 file はファイルのパスに割り当てられます。" 環境変数 FILE はファイルのパスに割り当てられます。"
commandsUpdated: コマンドは更新されました! commandsUpdated: コマンドは更新されました!
customStylesheet: カスタムスタイルシ ート customStylesheet: カスタムスタイルシ ート
examples: examples:
@@ -168,7 +168,6 @@ sidebar:
myFiles: 私のファイル myFiles: 私のファイル
newFile: 新しいファイルを作成 newFile: 新しいファイルを作成
newFolder: 新しいフォルダを作成 newFolder: 新しいフォルダを作成
servedWith: サービス提供者
settings: 設定 settings: 設定
siteSettings: サイト設定 siteSettings: サイト設定
hugoNew: Hugo New hugoNew: Hugo New

View File

@@ -139,7 +139,7 @@ settings:
Pode definir um conjunto de comandos a executar em determiandos eventos. Pode definir um conjunto de comandos a executar em determiandos eventos.
Deve escrever um comando por linha. Se o evento estiver relacionado com ficheiros, 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 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! commandsUpdated: Comandos atualizados!
customStylesheet: Estilos Personalizados customStylesheet: Estilos Personalizados
examples: Exemplos examples: Exemplos
@@ -193,7 +193,6 @@ sidebar:
newFile: Novo ficheiro newFile: Novo ficheiro
newFolder: Nova pasta newFolder: Nova pasta
preview: Pré-visualizar preview: Pré-visualizar
servedWith: Servido com
settings: Configurações settings: Configurações
siteSettings: Configurações do Site siteSettings: Configurações do Site
time: time:

View File

@@ -117,7 +117,7 @@ settings:
commandsHelp: "\ commandsHelp: "\
在这里,您可以设置在指定事件下执行的命令,一行一条。\ 在这里,您可以设置在指定事件下执行的命令,一行一条。\
若事件与文件相关,如“在保存文件前”,\ 若事件与文件相关,如“在保存文件前”,\
则文件的路径会被赋值给环境变量 \"file\"。" 则文件的路径会被赋值给环境变量 \"FILE\"。"
commandsUpdated: 命令已更新! commandsUpdated: 命令已更新!
customStylesheet: 自定义样式表 customStylesheet: 自定义样式表
examples: 例子 examples: 例子
@@ -166,7 +166,6 @@ sidebar:
myFiles: 我的文件 myFiles: 我的文件
newFile: 新建文件 newFile: 新建文件
newFolder: 新建文件夹 newFolder: 新建文件夹
servedWith: '服务提供者:'
settings: 设置 settings: 设置
siteSettings: 网站设置 siteSettings: 网站设置
hugoNew: Hugo New hugoNew: Hugo New

View File

@@ -4,7 +4,7 @@ buttons:
close: 關閉 close: 關閉
copy: 複製 copy: 複製
copyFile: 複製檔案 copyFile: 複製檔案
copyToClipboard: 複製到剪貼 copyToClipboard: 複製到剪貼簿
create: 建立 create: 建立
delete: 刪除 delete: 刪除
download: 下載 download: 下載
@@ -34,7 +34,7 @@ buttons:
success: success:
linkCopied: 連結已複製! linkCopied: 連結已複製!
errors: errors:
forbidden: 你被禁止訪問 forbidden: 你被禁止存取
internal: 內部出現麻煩了。 internal: 內部出現麻煩了。
notFound: 找不到檔案。 notFound: 找不到檔案。
files: files:
@@ -47,7 +47,7 @@ files:
lastModified: 最後修改 lastModified: 最後修改
loading: 讀取中... loading: 讀取中...
lonely: 這裡沒有任何檔案... lonely: 這裡沒有任何檔案...
metadata: 中繼資料 metadata: 詮釋資料
multipleSelectionEnabled: 多選模式已開啟 multipleSelectionEnabled: 多選模式已開啟
name: 名稱 name: 名稱
size: 大小 size: 大小
@@ -102,71 +102,70 @@ prompts:
show: 顯示 show: 顯示
size: 大小 size: 大小
schedule: 計畫 schedule: 計畫
scheduleMessage: 請選擇發佈這篇帖子的日期。 scheduleMessage: 請選擇發佈這篇貼文的日期。
newArchetype: 建立一個基於原型的新帖子。您的檔案將會建立在內容資料夾中。 newArchetype: 建立一個基於原型的新貼文。您的檔案將會建立在內容資料夾中。
settings: settings:
admin: 管理員 admin: 管理員
administrator: 管理員 administrator: 管理員
allowCommands: 執行命令 allowCommands: 執行命令
allowEdit: 編輯、重命名或刪除檔案/目錄 allowEdit: 編輯、重命名或刪除檔案/目錄
allowNew: 創建新檔案和目錄 allowNew: 創建新檔案和目錄
allowPublish: 發佈新的帖子與頁面 allowPublish: 發佈新的貼文與頁面
avoidChanges: '(留空以避免更改)' avoidChanges: '(留空以避免更改)'
changePassword: 更改密碼 changePassword: 更改密碼
commands: 命令 commands: 命令
commandsHelp: "\ commandsHelp: "\
在這裡,您可以設定在指定事件下執行的命令,一行一條。\ 在這裡,您可以設定在指定事件下執行的命令,一行一條。\
若事件與檔案相關,如“在保存檔案前”,\ 若事件與檔案相關,如“在保存檔案前”,\
則檔案的路徑會被賦值給環境變數 \"file\"。" 則檔案的路徑會被賦值給環境變數 \"FILE\"。"
commandsUpdated: 命令已更新! commandsUpdated: 命令已更新!
customStylesheet: 自定義樣式表 customStylesheet: 自定義樣式表
examples: examples:
globalSettings: 全域設定 globalSettings: 全域設定
language: 語言 language: 語言
lockPassword: 禁止用戶修改密碼 lockPassword: 禁止使用者修改密碼
newPassword: 您的新密碼 newPassword: 您的新密碼
newPasswordConfirm: 重輸一遍新密碼 newPasswordConfirm: 重輸一遍新密碼
newUser: 建立用戶 newUser: 建立使用者
password: 密碼 password: 密碼
passwordUpdated: 密碼已更新! passwordUpdated: 密碼已更新!
permissions: 權限 permissions: 權限
permissionsHelp: "\ permissionsHelp: "\
您可以將該用戶設置為管理員,也可以單獨選擇各項權限。\ 您可以將該使用者設置為管理員,也可以單獨選擇各項權限。\
如果選擇了“管理員”,則其他的選項會被自動勾上,\ 如果選擇了“管理員”,則其他的選項會被自動勾上,\
同時該用戶可以管理其他用戶。" 同時該使用者可以管理其他使用者。"
profileSettings: 設定檔設定 profileSettings: 設定檔設定
ruleExample1: "\ ruleExample1: "\
封鎖用戶訪問所有資料夾下任何以 . 開頭的檔案\ 封鎖使用者存取所有資料夾下任何以 . 開頭的檔案\
(隱藏文件, 例如: .git, .gitignore)。" (隱藏文件, 例如: .git, .gitignore)。"
ruleExample2: 封鎖用戶訪問其目錄範圍的根目錄下名為 Caddyfile 的檔案。 ruleExample2: 封鎖使用者存取其目錄範圍的根目錄下名為 Caddyfile 的檔案。
rules: 規則 rules: 規則
rulesHelp1: "\ rulesHelp1: "\
您可以為該用戶製定一組黑名單或白名單式的規則,\ 您可以為該使用者製定一組黑名單或白名單式的規則,\
被屏蔽的檔案將不會顯示在清單中,用戶也無權限訪問,\ 被屏蔽的檔案將不會顯示在清單中,使用者也無權限存取,\
支持相對於目錄範圍的路徑。" 支持相對於目錄範圍的路徑。"
rulesHelp2: "\ rulesHelp2: "\
每行一條規則,且必須以關鍵字 {0} 或 {1} 開頭。\ 每行一條規則,且必須以關鍵字 {0} 或 {1} 開頭。\
如要使用規則運算式,請在加上 {2} 之後再附上運算式或路徑。" 如要使用規則運算式,請在加上 {2} 之後再附上運算式或路徑。"
scope: 目錄範圍 scope: 目錄範圍
settingsUpdated: 設定已更新! settingsUpdated: 設定已更新!
user: 用戶 user: 使用者
userCommands: 用戶命令 userCommands: 使用者命令
userCommandsHelp: "\ userCommandsHelp: "\
指定該用戶可以執行的命令,用空格分隔。\ 指定該使用者可以執行的命令,用空格分隔。\
例如:" 例如:"
userCreated: 用戶已建立! userCreated: 使用者已建立!
userDeleted: 用戶已刪除! userDeleted: 使用者已刪除!
userManagement: 用戶管理 userManagement: 使用者管理
username: 用戶名 username: 使用者名稱
users: 用戶 users: 使用者
userUpdated: 用戶已更新! userUpdated: 使用者已更新!
sidebar: sidebar:
help: 幫助 help: 幫助
logout: 登出 logout: 登出
myFiles: 我的檔案 myFiles: 我的檔案
newFile: 建立檔案 newFile: 建立檔案
newFolder: 建立資料夾 newFolder: 建立資料夾
servedWith: '服務提供者:'
settings: 設定 settings: 設定
siteSettings: 網站設定 siteSettings: 網站設定
hugoNew: Hugo New hugoNew: Hugo New
@@ -190,9 +189,10 @@ languages:
pt: Português pt: Português
ja: 日本語 ja: 日本語
zhCN: 中文 (简体) zhCN: 中文 (简体)
zhTW: 中文 (繁體)
time: time:
unit: 時間單位 unit: 時間單位
seconds: seconds:
minutes: 分鐘 minutes: 分鐘
hours: 小時 hours: 小時
days: days:

View File

@@ -26,7 +26,6 @@ Vue.prototype.$showSuccess = function (message) {
} }
Vue.prototype.$showError = function (error) { Vue.prototype.$showError = function (error) {
// TODO: add btns: close and report issue
let n = new Noty(Object.assign({}, notyDefault, { let n = new Noty(Object.assign({}, notyDefault, {
text: error, text: error,
type: 'error', type: 'error',

View File

@@ -3,10 +3,11 @@ import Router from 'vue-router'
import Login from '@/views/Login' import Login from '@/views/Login'
import Layout from '@/views/Layout' import Layout from '@/views/Layout'
import Files from '@/views/Files' import Files from '@/views/Files'
import Users from '@/views/Users' import Users from '@/views/Settings/Users'
import User from '@/views/User' import User from '@/views/Settings/User'
import GlobalSettings from '@/views/GlobalSettings' import Settings from '@/views/Settings'
import ProfileSettings from '@/views/ProfileSettings' import GlobalSettings from '@/views/settings/Global'
import ProfileSettings from '@/views/settings/Profile'
import Error403 from '@/views/errors/403' import Error403 from '@/views/errors/403'
import Error404 from '@/views/errors/404' import Error404 from '@/views/errors/404'
import Error500 from '@/views/errors/500' import Error500 from '@/views/errors/500'
@@ -49,22 +50,45 @@ const router = new Router({
{ {
path: '/settings', path: '/settings',
name: 'Settings', name: 'Settings',
redirect: { component: Settings,
path: '/settings/profile' children: [
} {
}, path: '/settings',
{ name: 'Settings',
path: '/settings/profile', redirect: {
name: 'Profile Settings', path: '/settings/profile'
component: ProfileSettings }
}, },
{ {
path: '/settings/global', path: '/settings/profile',
name: 'Global Settings', name: 'Profile Settings',
component: GlobalSettings, component: ProfileSettings
meta: { },
requiresAdmin: true {
} 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', path: '/403',
@@ -81,22 +105,6 @@ const router = new Router({
name: 'Internal Server Error', name: 'Internal Server Error',
component: Error500 component: Error500
}, },
{
path: '/users',
name: 'Users',
component: Users,
meta: {
requiresAdmin: true
}
},
{
path: '/users/*',
name: 'User',
component: User,
meta: {
requiresAdmin: true
}
},
{ {
path: '/files', path: '/files',
redirect: { redirect: {

View File

@@ -12,6 +12,11 @@ const state = {
key: '', key: '',
items: [] items: []
}, },
css: (() => {
let css = window.CSS
window.CSS = null
return css
})(),
staticGen: document.querySelector('meta[name="staticgen"]').getAttribute('content'), staticGen: document.querySelector('meta[name="staticgen"]').getAttribute('content'),
baseURL: document.querySelector('meta[name="base"]').getAttribute('content'), baseURL: document.querySelector('meta[name="base"]').getAttribute('content'),
noAuth: (document.querySelector('meta[name="noauth"]').getAttribute('content') === 'true'), noAuth: (document.querySelector('meta[name="noauth"]').getAttribute('content') === 'true'),

View File

@@ -31,6 +31,7 @@ const mutations = {
i18n.locale = value.locale i18n.locale = value.locale
state.user = value state.user = value
}, },
setCSS: (state, value) => (state.css = value),
setJWT: (state, value) => (state.jwt = value), setJWT: (state, value) => (state.jwt = value),
multiple: (state, value) => (state.multiple = value), multiple: (state, value) => (state.multiple = value),
addSelected: (state, value) => (state.selected.push(value)), addSelected: (state, value) => (state.selected.push(value)),
@@ -45,8 +46,12 @@ const mutations = {
resetSelected: (state) => { resetSelected: (state) => {
state.selected = [] state.selected = []
}, },
listingDisplay: (state, value) => { updateUser: (state, value) => {
state.req.display = value if (typeof value !== 'object') return
for (let field in value) {
state.user[field] = value[field]
}
}, },
updateRequest: (state, value) => { updateRequest: (state, value) => {
state.req = value state.req = value

View File

@@ -6,7 +6,7 @@
<site-header></site-header> <site-header></site-header>
<sidebar></sidebar> <sidebar></sidebar>
<main> <main>
<router-view v-on:css-updated="updateCSS"></router-view> <router-view @css="$emit('update:css')"></router-view>
</main> </main>
<prompts></prompts> <prompts></prompts>
</div> </div>
@@ -34,23 +34,10 @@ export default {
} }
}, },
mounted () { mounted () {
this.updateCSS() this.$emit('update:css')
}, },
methods: { beforeDestroy () {
updateCSS () { this.$emit('clean:css')
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)
}
} }
} }
</script> </script>

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

View File

@@ -1,48 +0,0 @@
<template>
<div class="dashboard">
<ul id="nav">
<li><router-link to="/settings/profile">{{ $t('settings.profileSettings') }}</router-link></li>
<li><router-link to="/settings/global">{{ $t('settings.globalSettings') }}</router-link></li>
<li class="active"><router-link to="/users">{{ $t('settings.userManagement') }}</router-link></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" :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><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.$showError(error)
})
}
}
</script>

View File

@@ -1,37 +1,61 @@
<template> <template>
<div class="dashboard"> <div class="dashboard">
<ul id="nav"> <form class="card" @submit.prevent="saveStaticGen">
<li><router-link to="/settings/profile">{{ $t('settings.profileSettings') }}</router-link></li> <div class="card-title">
<li class="active"><router-link to="/settings/global">{{ $t('settings.globalSettings') }}</router-link></li> <h2>{{ capitalize($store.state.staticGen) }}</h2>
<li><router-link to="/users">{{ $t('settings.userManagement') }}</router-link></li> </div>
</ul>
<h1>{{ $t('settings.globalSettings') }}</h1> <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>
<form @submit="saveStaticGen" v-if="$store.state.staticGen.length > 0"> <div class="card-action">
<h2>{{ capitalize($store.state.staticGen) }}</h2> <input class="flat" type="submit" :value="$t('buttons.update')">
</div>
<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>
<form @submit="saveCommands"> <form class="card" @submit.prevent="saveCSS">
<h2>{{ $t('settings.commands') }}</h2> <div class="card-title">
<h2>{{ $t('settings.customStylesheet') }}</h2>
</div>
<p class="small">{{ $t('settings.commandsHelp') }}</p> <div class="card-content">
<textarea v-model="css"></textarea>
</div>
<template v-for="command in commands"> <div class="card-action">
<h3>{{ capitalize(command.name) }}</h3> <input class="flat" type="submit" :value="$t('buttons.update')">
<textarea v-model.trim="command.value"></textarea> </div>
</template> </form>
<p><input type="submit" value="Save"></p> <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> </form>
</div> </div>
@@ -46,7 +70,8 @@ export default {
data: function () { data: function () {
return { return {
commands: [], commands: [],
staticGen: [] staticGen: [],
css: ''
} }
}, },
computed: { computed: {
@@ -55,6 +80,7 @@ export default {
created () { created () {
getSettings() getSettings()
.then(settings => { .then(settings => {
console.log(settings)
if (this.$store.state.staticGen.length > 0) { if (this.$store.state.staticGen.length > 0) {
this.parseStaticGen(settings.staticGen) this.parseStaticGen(settings.staticGen)
} }
@@ -65,8 +91,10 @@ export default {
value: settings.commands[key].join('\n') value: settings.commands[key].join('\n')
}) })
} }
this.css = settings.css
}) })
.catch(error => { this.$showError(error) }) .catch(this.$showError)
}, },
methods: { methods: {
capitalize (name, where = '_') { capitalize (name, where = '_') {
@@ -81,8 +109,6 @@ export default {
return name.slice(0, -1) return name.slice(0, -1)
}, },
saveCommands (event) { saveCommands (event) {
event.preventDefault()
let commands = {} let commands = {}
for (let command of this.commands) { for (let command of this.commands) {
@@ -96,10 +122,18 @@ export default {
updateSettings(commands, 'commands') updateSettings(commands, 'commands')
.then(() => { this.$showSuccess(this.$t('settings.commandsUpdated')) }) .then(() => { this.$showSuccess(this.$t('settings.commandsUpdated')) })
.catch(error => { this.$showError(error) }) .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) { saveStaticGen (event) {
event.preventDefault()
let staticGen = {} let staticGen = {}
for (let field of this.staticGen) { for (let field of this.staticGen) {
@@ -117,7 +151,7 @@ export default {
updateSettings(staticGen, 'staticGen') updateSettings(staticGen, 'staticGen')
.then(() => { this.$showSuccess(this.$t('settings.settingsUpdated')) }) .then(() => { this.$showSuccess(this.$t('settings.settingsUpdated')) })
.catch(error => { this.$showError(error) }) .catch(this.$showError)
}, },
parseStaticGen (staticgen) { parseStaticGen (staticgen) {
for (let option of staticgen) { for (let option of staticgen) {

View File

@@ -1,26 +1,35 @@
<template> <template>
<div class="dashboard"> <div class="dashboard">
<ul id="nav" v-if="user.admin"> <form class="card" @submit="updateSettings">
<li class="active"><router-link to="/settings/profile">{{ $t('settings.profileSettings') }}</router-link></li> <div class="card-title">
<li><router-link to="/settings/global">{{ $t('settings.globalSettings') }}</router-link></li> <h2>{{ $t('settings.profileSettings') }}</h2>
<li><router-link to="/users">{{ $t('settings.userManagement') }}</router-link></li> </div>
</ul>
<h1>{{ $t('settings.profileSettings') }}</h1> <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>
<form @submit="updateSettings"> <div class="card-action">
<h3>{{ $t('settings.language') }}</h3> <input class="flat" type="submit" :value="$t('buttons.update')">
<p><languages id="locale" :selected.sync="locale"></languages></p> </div>
<h3>{{ $t('settings.customStylesheet') }}</h3>
<textarea v-model="css" name="css"></textarea>
<p><input type="submit" :value="$t('buttons.update')"></p>
</form> </form>
<form v-if="!user.lockPassword" @submit="updatePassword"> <form class="card" v-if="!user.lockPassword" @submit="updatePassword">
<h3>{{ $t('settings.changePassword') }}</h3> <div class="card-title">
<p><input :class="passwordClass" type="password" :placeholder="$t('settings.newPassword')" v-model="password" name="password"></p> <h2>{{ $t('settings.changePassword') }}</h2>
<p><input :class="passwordClass" type="password" :placeholder="$t('settings.newPasswordConfirm')" v-model="passwordConf" name="password"></p> </div>
<p><input type="submit" :value="$t('buttons.update')"></p>
<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> </form>
</div> </div>
</template> </template>
@@ -89,7 +98,7 @@ export default {
updateUser(user, 'partial').then(location => { updateUser(user, 'partial').then(location => {
this.$store.commit('setUser', user) this.$store.commit('setUser', user)
this.$emit('css-updated') this.$emit('css')
this.$showSuccess(this.$t('settings.settingsUpdated')) this.$showSuccess(this.$t('settings.settingsUpdated'))
}).catch(e => { }).catch(e => {
this.$showError(e) this.$showError(e)

View File

@@ -1,76 +1,81 @@
<template> <template>
<div> <div>
<form @submit="save" class="dashboard"> <form @submit="save" class="card">
<ul id="nav"> <div class="card-title">
<li><router-link to="/settings/profile">{{ $t('settings.profileSettings') }}</router-link></li> <h2 v-if="id === 0">{{ $t('settings.newUser') }}</h2>
<li><router-link to="/settings/global">{{ $t('settings.globalSettings') }}</router-link></li> <h2 v-else>{{ $t('settings.user') }} {{ username }}</h2>
<li><router-link to="/users">{{ $t('settings.userManagement') }}</router-link></li> </div>
</ul>
<h1 v-if="id === 0">{{ $t('settings.newUser') }}</h1> <div class="card-content">
<h1 v-else>{{ $t('settings.user') }} {{ username }}</h1>
<p><label for="username">{{ $t('settings.username') }}</label><input type="text" v-model="username" id="username"></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="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="scope">{{ $t('settings.scope') }}</label><input type="text" v-model="filesystem" id="scope"></p>
<p> <p>
<label for="locale">{{ $t('settings.language') }}</label> <label for="locale">{{ $t('settings.language') }}</label>
<languages id="locale" :selected.sync="locale"></languages> <languages id="locale" :selected.sync="locale"></languages>
</p> </p>
<p><input type="checkbox" :disabled="admin" v-model="lockPassword"> {{ $t('settings.lockPassword') }}</p> <p><input type="checkbox" :disabled="admin" v-model="lockPassword"> {{ $t('settings.lockPassword') }}</p>
<h2>{{ $t('settings.permissions') }}</h2> <h3>{{ $t('settings.permissions') }}</h3>
<p class="small">{{ $t('settings.permissionsHelp') }}</p> <p class="small">{{ $t('settings.permissionsHelp') }}</p>
<p><input type="checkbox" v-model="admin"> {{ $t('settings.administrator') }}</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="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="allowEdit"> {{ $t('settings.allowEdit') }}</p>
<p><input type="checkbox" :disabled="admin" v-model="allowCommands"> {{ $t('settings.allowCommands') }}</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> <p v-show="$store.state.staticGen.length"><input type="checkbox" :disabled="admin" v-model="allowPublish"> {{ $t('settings.allowPublish') }}</p>
<h3>{{ $t('settings.userCommands') }}</h3> <h3>{{ $t('settings.userCommands') }}</h3>
<p class="small">{{ $t('settings.userCommandsHelp') }} <i>git svn hg</i>.</p> <p class="small">{{ $t('settings.userCommandsHelp') }} <i>git svn hg</i>.</p>
<input type="text" v-model.trim="commands"> <input type="text" v-model.trim="commands">
<h2>{{ $t('settings.rules') }}</h2> <h3>{{ $t('settings.rules') }}</h3>
<p class="small">{{ $t('settings.rulesHelp1') }}</p> <p class="small">{{ $t('settings.rulesHelp1') }}</p>
<i18n path="settings.rulesHelp2" tag="p" class="small"> <i18n path="settings.rulesHelp2" tag="p" class="small">
<code>allow</code><code>disallow</code><code>regex</code> <code>allow</code><code>disallow</code><code>regex</code>
</i18n> </i18n>
<p class="small"><strong>{{ $t('settings.examples') }}</strong></p> <p class="small"><strong>{{ $t('settings.examples') }}</strong></p>
<ul class="small"> <ul class="small">
<li><code>disallow regex \\/\\..+</code> - {{ $t('settings.ruleExample1') }}</li> <li><code>disallow regex \\/\\..+</code> - {{ $t('settings.ruleExample1') }}</li>
<li><code>disallow /Caddyfile</code> - {{ $t('settings.ruleExample2') }}</li> <li><code>disallow /Caddyfile</code> - {{ $t('settings.ruleExample2') }}</li>
</ul> </ul>
<textarea v-model.trim="rules"></textarea> <textarea v-model.trim="rules"></textarea>
<h2>{{ $t('settings.customStylesheet') }}</h2> <h3>{{ $t('settings.customStylesheet') }}</h3>
<textarea name="css"></textarea> <textarea name="css"></textarea>
</div>
<p> <div class="card-action">
<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> <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 type="submit" :value="$t('buttons.save')"> <input class="flat" type="submit" :value="$t('buttons.save')">
</p> </div>
</form> </form>
<div v-if="$store.state.show === 'deleteUser'" class="prompt"> <div v-if="$store.state.show === 'deleteUser'" class="card floating">
<h3>Delete User</h3> <div class="card-content">
<p>Are you sure you want to delete this user?</p> <p>Are you sure you want to delete this user?</p>
<div> </div>
<button @click="deleteUser" autofocus>{{ $t('buttons.delete') }}</button>
<button class="cancel" <div class="card-action">
<button class="cancel flat"
@click="closeHovers" @click="closeHovers"
autofocus
:aria-label="$t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')"> :title="$t('buttons.cancel')">
{{ $t('buttons.cancel') }} {{ $t('buttons.cancel') }}
</button> </button>
<button class="flat"
@click="deleteUser">
{{ $t('buttons.delete') }}
</button>
</div> </div>
</div> </div>
</div> </div>
@@ -105,7 +110,7 @@ export default {
}, },
computed: { computed: {
passwordPlaceholder () { passwordPlaceholder () {
if (this.$route.path === '/users/new') return '' if (this.$route.path === '/settings/users/new') return ''
return this.$t('settings.avoidChanges') return this.$t('settings.avoidChanges')
} }
}, },
@@ -131,7 +136,7 @@ export default {
fetchData () { fetchData () {
let user = this.$route.params[0] let user = this.$route.params[0]
if (this.$route.path === '/users/new') { if (this.$route.path === '/settings/users/new') {
user = 'base' user = 'base'
} }
@@ -168,7 +173,7 @@ export default {
this.rules = this.rules.trim() this.rules = this.rules.trim()
}).catch(() => { }).catch(() => {
this.$router.push({ path: '/users/new' }) this.$router.push({ path: '/settings/users/new' })
}) })
}, },
capitalize (name) { capitalize (name) {
@@ -205,7 +210,7 @@ export default {
event.preventDefault() event.preventDefault()
deleteUser(this.id).then(location => { deleteUser(this.id).then(location => {
this.$router.push({ path: '/users' }) this.$router.push({ path: '/settings/users' })
this.$showSuccess(this.$t('settings.userDeleted')) this.$showSuccess(this.$t('settings.userDeleted'))
}).catch(e => { }).catch(e => {
this.$showError(e) this.$showError(e)
@@ -215,7 +220,7 @@ export default {
event.preventDefault() event.preventDefault()
let user = this.parseForm() let user = this.parseForm()
if (this.$route.path === '/users/new') { if (this.$route.path === '/settings/users/new') {
newUser(user).then(location => { newUser(user).then(location => {
this.$router.push({ path: location }) this.$router.push({ path: location })
this.$showSuccess(this.$t('settings.userCreated')) this.$showSuccess(this.$t('settings.userCreated'))

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

View File

@@ -146,6 +146,15 @@ func Parse(c *caddy.Controller, plugin string) ([]*filemanager.FileManager, erro
} }
u.CSS = string(css) 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 "no_auth": case "no_auth":
if !c.NextArg() { if !c.NextArg() {
noAuth = true noAuth = true

View File

@@ -34,6 +34,7 @@ var (
locale string locale string
baseurl string baseurl string
prefixurl string prefixurl string
viewMode string
port int port int
noAuth bool noAuth bool
allowCommands bool allowCommands bool
@@ -53,6 +54,7 @@ func init() {
flag.StringVarP(&baseurl, "baseurl", "b", "", "Base URL") flag.StringVarP(&baseurl, "baseurl", "b", "", "Base URL")
flag.StringVar(&commands, "commands", "git svn hg", "Default commands option for new users") flag.StringVar(&commands, "commands", "git svn hg", "Default commands option for new users")
flag.StringVar(&prefixurl, "prefixurl", "", "Prefix URL") flag.StringVar(&prefixurl, "prefixurl", "", "Prefix URL")
flag.StringVar(&viewMode, "view-mode", "mosaic", "Default view mode for new users")
flag.BoolVar(&allowCommands, "allow-commands", true, "Default allow commands option for new users") 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(&allowEdit, "allow-edit", true, "Default allow edit option for new users")
flag.BoolVar(&allowPublish, "allow-publish", true, "Default allow publish option for new users") flag.BoolVar(&allowPublish, "allow-publish", true, "Default allow publish option for new users")
@@ -79,6 +81,7 @@ func setupViper() {
viper.SetDefault("NoAuth", false) viper.SetDefault("NoAuth", false)
viper.SetDefault("BaseURL", "") viper.SetDefault("BaseURL", "")
viper.SetDefault("PrefixURL", "") viper.SetDefault("PrefixURL", "")
viper.SetDefault("ViewMode", "mosaic")
viper.BindPFlag("Port", flag.Lookup("port")) viper.BindPFlag("Port", flag.Lookup("port"))
viper.BindPFlag("Address", flag.Lookup("address")) viper.BindPFlag("Address", flag.Lookup("address"))
@@ -95,6 +98,7 @@ func setupViper() {
viper.BindPFlag("NoAuth", flag.Lookup("no-auth")) viper.BindPFlag("NoAuth", flag.Lookup("no-auth"))
viper.BindPFlag("BaseURL", flag.Lookup("baseurl")) viper.BindPFlag("BaseURL", flag.Lookup("baseurl"))
viper.BindPFlag("PrefixURL", flag.Lookup("prefixurl")) viper.BindPFlag("PrefixURL", flag.Lookup("prefixurl"))
viper.BindPFlag("ViewMode", flag.Lookup("view-mode"))
viper.SetConfigName("filemanager") viper.SetConfigName("filemanager")
viper.AddConfigPath(".") viper.AddConfigPath(".")
@@ -189,6 +193,7 @@ func handler() http.Handler {
CSS: "", CSS: "",
Scope: viper.GetString("Scope"), Scope: viper.GetString("Scope"),
FileSystem: fileutils.Dir(viper.GetString("Scope")), FileSystem: fileutils.Dir(viper.GetString("Scope")),
ViewMode: viper.GetString("ViewMode"),
}, },
Store: &filemanager.Store{ Store: &filemanager.Store{
Config: bolt.ConfigStore{DB: db}, Config: bolt.ConfigStore{DB: db},

View File

@@ -67,8 +67,6 @@ type Listing struct {
Sort string `json:"sort"` Sort string `json:"sort"`
// And which order. // And which order.
Order string `json:"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 // GetInfo gets the file information and, in case of error, returns the

View File

@@ -3,6 +3,7 @@ package filemanager
import ( import (
"crypto/rand" "crypto/rand"
"errors" "errors"
"fmt"
"log" "log"
"net/http" "net/http"
"os" "os"
@@ -21,7 +22,7 @@ import (
) )
// Version is the current File Manager version. // Version is the current File Manager version.
const Version = "1.3.1" const Version = "1.3.2"
var ( var (
ErrExist = errors.New("the resource already exists") ErrExist = errors.New("the resource already exists")
@@ -74,10 +75,28 @@ type FileManager struct {
// A map of events to a slice of commands. // A map of events to a slice of commands.
Commands map[string][]string Commands map[string][]string
// Global stylesheet.
CSS string
// NewFS should build a new file system for a given path. // NewFS should build a new file system for a given path.
NewFS FSBuilder 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. // Command is a command function.
type Command func(r *http.Request, m *FileManager, u *User) error type Command func(r *http.Request, m *FileManager, u *User) error
@@ -111,16 +130,39 @@ func (m *FileManager) Setup() error {
return err 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. // Tries to get the event commands from the database.
// If they don't exist, initialize them. // If they don't exist, initialize them.
err = m.Store.Config.Get("commands", &m.Commands) err = m.Store.Config.Get("commands", &m.Commands)
if err != nil && err == ErrNotExist {
m.Commands = map[string][]string{ if err == nil {
"before_save": {}, // Add hypothetically new command handlers.
"after_save": {}, for _, command := range commandEvents {
"before_publish": {}, if _, ok := m.Commands[command]; ok {
"after_publish": {}, 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) err = m.Store.Config.Save("commands", m.Commands)
} }
@@ -235,7 +277,7 @@ func (m FileManager) ShareCleaner() {
} }
// Runner runs the commands for a certain event type. // 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{} commands := []string{}
// Get the commands from the File Manager instance itself. // Get the commands from the File Manager instance itself.
@@ -260,7 +302,15 @@ func (m FileManager) Runner(event string, path string) error {
} }
cmd := exec.Command(command, args...) 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.Stdin = os.Stdin
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
@@ -297,6 +347,7 @@ var DefaultUser = User{
Locale: "en", Locale: "en",
Scope: ".", Scope: ".",
FileSystem: fileutils.Dir("."), FileSystem: fileutils.Dir("."),
ViewMode: "mosaic",
} }
// User contains the configuration for each user. // User contains the configuration for each user.
@@ -340,6 +391,9 @@ type User struct {
// Commands is the list of commands the user can execute. // Commands is the list of commands the user can execute.
Commands []string `json:"commands"` 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. // Allowed checks if the user has permission to access a directory/file.

View File

@@ -25,7 +25,7 @@ func downloadHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int
if r.URL.Query().Get("inline") == "true" { if r.URL.Query().Get("inline") == "true" {
w.Header().Set("Content-Disposition", "inline") w.Header().Set("Content-Disposition", "inline")
} else { } 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) 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() defer file.Close()
w.Header().Set("Content-Disposition", "attachment; filename="+name) w.Header().Set("Content-Disposition", "attachment; filename=\""+name+"\"")
_, err = io.Copy(w, file) _, err = io.Copy(w, file)
return 0, err return 0, err
} }

View File

@@ -229,6 +229,7 @@ func renderFile(c *fm.Context, w http.ResponseWriter, file string) (int, error)
"BaseURL": c.RootURL(), "BaseURL": c.RootURL(),
"NoAuth": c.NoAuth, "NoAuth": c.NoAuth,
"Version": fm.Version, "Version": fm.Version,
"CSS": template.CSS(c.CSS),
} }
if c.StaticGen != nil { if c.StaticGen != nil {

View File

@@ -38,7 +38,7 @@ func resourceHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int
case http.MethodPut: case http.MethodPut:
// Before save command handler. // Before save command handler.
path := filepath.Join(c.User.Scope, r.URL.Path) 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 return http.StatusInternalServerError, err
} }
@@ -48,7 +48,7 @@ func resourceHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int
} }
// After save command handler. // 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 return http.StatusInternalServerError, err
} }
@@ -130,8 +130,6 @@ func listingHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int,
} }
listing.ApplySort() listing.ApplySort()
listing.Display = displayMode(w, r, cookieScope)
return renderJSON(w, f) return renderJSON(w, f)
} }
@@ -141,12 +139,22 @@ func resourceDeleteHandler(c *fm.Context, w http.ResponseWriter, r *http.Request
return http.StatusForbidden, nil 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. // Remove the file or folder.
err := c.User.FileSystem.RemoveAll(r.URL.Path) err := c.User.FileSystem.RemoveAll(r.URL.Path)
if err != nil { if err != nil {
return ErrorToHTTP(err, true), err 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 return http.StatusOK, nil
} }
@@ -187,6 +195,11 @@ func resourcePostPutHandler(c *fm.Context, w http.ResponseWriter, r *http.Reques
} }
} }
// 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. // Create/Open the file.
f, err := c.User.FileSystem.OpenFile(r.URL.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0776) f, err := c.User.FileSystem.OpenFile(r.URL.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0776)
if err != nil { if err != nil {
@@ -218,6 +231,12 @@ func resourcePostPutHandler(c *fm.Context, w http.ResponseWriter, r *http.Reques
// Writes the ETag Header. // Writes the ETag Header.
etag := fmt.Sprintf(`"%x%x"`, fi.ModTime().UnixNano(), fi.Size()) etag := fmt.Sprintf(`"%x%x"`, fi.ModTime().UnixNano(), fi.Size())
w.Header().Set("ETag", etag) 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 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) path := filepath.Join(c.User.Scope, r.URL.Path)
// Before save command handler. // 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 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. // 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 return http.StatusInternalServerError, err
} }
@@ -293,40 +312,36 @@ func resourcePatchHandler(c *fm.Context, w http.ResponseWriter, r *http.Request)
} }
if action == "copy" { 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) 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 { } 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) 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 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', // 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. // 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) { func handleSortOrder(w http.ResponseWriter, r *http.Request, scope string) (sort string, order string, err error) {

View File

@@ -13,6 +13,7 @@ import (
type modifySettingsRequest struct { type modifySettingsRequest struct {
*modifyRequest *modifyRequest
Data struct { Data struct {
CSS string `json:"css"`
Commands map[string][]string `json:"commands"` Commands map[string][]string `json:"commands"`
StaticGen map[string]interface{} `json:"staticGen"` StaticGen map[string]interface{} `json:"staticGen"`
} `json:"data"` } `json:"data"`
@@ -61,6 +62,7 @@ func settingsHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int
} }
type settingsGetRequest struct { type settingsGetRequest struct {
CSS string `json:"css"`
Commands map[string][]string `json:"commands"` Commands map[string][]string `json:"commands"`
StaticGen []option `json:"staticGen"` StaticGen []option `json:"staticGen"`
} }
@@ -73,6 +75,7 @@ func settingsGetHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (
result := &settingsGetRequest{ result := &settingsGetRequest{
Commands: c.Commands, Commands: c.Commands,
StaticGen: []option{}, StaticGen: []option{},
CSS: c.CSS,
} }
if c.StaticGen != nil { if c.StaticGen != nil {
@@ -114,6 +117,16 @@ func settingsPutHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (
return http.StatusOK, nil 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. // Update the static generator options.
if mod.Which == "staticGen" { if mod.Which == "staticGen" {
err = mapstructure.Decode(mod.Data.StaticGen, c.StaticGen) err = mapstructure.Decode(mod.Data.StaticGen, c.StaticGen)

View File

@@ -194,7 +194,7 @@ func usersPostHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (in
} }
// Set the Location header and return. // 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) w.WriteHeader(http.StatusCreated)
return 0, nil return 0, nil
} }
@@ -272,8 +272,9 @@ func usersPutHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int
if which == "partial" { if which == "partial" {
c.User.CSS = u.CSS c.User.CSS = u.CSS
c.User.Locale = u.Locale 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 { if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }

View File

@@ -18,7 +18,7 @@ git push --tags
echo "Commiting untracked version notice..." echo "Commiting untracked version notice..."
sed -i "s|$1|(untracked)|g" filemanager.go sed -i "s|$1|(untracked)|g" filemanager.go
git add -A git add -A
git commit -m "untracked version `date`" git commit -m "[ci skip] auto: setting untracked version"
git push git push
echo "Done!" echo "Done!"

View File

@@ -1 +1 @@
5361edece00958caab3c3fcff99e50f5dff981f5 36b9bba06c64cd83f3994bbb77fc36948d9d2dfe