Merge pull request #1026 from ramiresviana/fixes
This commit is contained in:
		
						commit
						1790df2090
					
				|  | @ -2,6 +2,7 @@ package fileutils | |||
| 
 | ||||
| import ( | ||||
| 	"io" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 
 | ||||
| 	"github.com/spf13/afero" | ||||
|  | @ -25,7 +26,7 @@ func CopyFile(fs afero.Fs, source, dest string) error { | |||
| 	} | ||||
| 
 | ||||
| 	// Create the destination file.
 | ||||
| 	dst, err := fs.Create(dest) | ||||
| 	dst, err := fs.OpenFile(dest, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0775) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| :root { | ||||
|   --background: #121212; | ||||
|   --surfacePrimary: #171819; | ||||
|   --surfaceSecondary: #212528; | ||||
|   --background: #141D24; | ||||
|   --surfacePrimary: #20292F; | ||||
|   --surfaceSecondary: #3A4147; | ||||
|   --divider: rgba(255, 255, 255, 0.12); | ||||
|   --icon: #ffffff; | ||||
|   --textPrimary: rgba(255, 255, 255, 0.87); | ||||
|  | @ -16,7 +16,7 @@ body { | |||
| #loading { | ||||
|   background: var(--background); | ||||
| } | ||||
| #loading .spinner div { | ||||
| #loading .spinner div, #previewer .loading .spinner div { | ||||
|   background: var(--icon); | ||||
| } | ||||
| 
 | ||||
|  | @ -30,25 +30,34 @@ header { | |||
| 
 | ||||
| #search #input { | ||||
|   background: var(--surfaceSecondary); | ||||
|   border-color: var(--surfacePrimary); | ||||
| } | ||||
| #search.active #input, | ||||
| #search.active .boxes { | ||||
| #search #input input::placeholder { | ||||
|   color: var(--textSecondary); | ||||
| } | ||||
| #search.active #input { | ||||
|   background: var(--surfacePrimary); | ||||
| } | ||||
| #search.active input { | ||||
|   color: var(--textPrimary); | ||||
| } | ||||
| #search.active #result { | ||||
| #search #result { | ||||
|   background: var(--background); | ||||
|   color: var(--textPrimary); | ||||
| } | ||||
| #search.active .boxes h3 { | ||||
| #search .boxes { | ||||
|   background: var(--surfaceSecondary); | ||||
| } | ||||
| #search .boxes h3 { | ||||
|   color: var(--textPrimary); | ||||
| } | ||||
| 
 | ||||
| .action { | ||||
|   color: var(--textPrimary) !important; | ||||
| } | ||||
| .action:hover { | ||||
|   background-color: rgba(255, 255, 255, .1); | ||||
| } | ||||
| .action i { | ||||
|   color: var(--icon) !important; | ||||
| } | ||||
|  | @ -93,6 +102,10 @@ nav > div { | |||
|   background: var(--background); | ||||
| } | ||||
| 
 | ||||
| .message { | ||||
|   color: var(--textPrimary); | ||||
| } | ||||
| 
 | ||||
| .card { | ||||
|   background: var(--surfacePrimary); | ||||
|   color: var(--textPrimary); | ||||
|  | @ -106,9 +119,23 @@ nav > div { | |||
| .dashboard p label { | ||||
|   color: var(--textPrimary); | ||||
| } | ||||
| .card#share ul li input, | ||||
| .card#share ul li select, | ||||
| .input { | ||||
|   background: var(--surfaceSecondary); | ||||
|   color: var(--textPrimary); | ||||
|   border: 1px solid rgba(255, 255, 255, 0.05); | ||||
| } | ||||
| .input:hover, | ||||
| .input:focus { | ||||
|   border-color: rgba(255, 255, 255, 0.15); | ||||
| } | ||||
| .input--red { | ||||
|   background: #73302D; | ||||
| } | ||||
| 
 | ||||
| .input--green { | ||||
|   background: #147A41; | ||||
| } | ||||
| 
 | ||||
| .dashboard #nav li, | ||||
|  | @ -119,10 +146,27 @@ nav > div { | |||
|   color: var(--textPrimary); | ||||
| } | ||||
| 
 | ||||
| table th { | ||||
|   color: var(--textSecondary); | ||||
| } | ||||
| 
 | ||||
| .file-list li:hover { | ||||
|   background: var(--surfaceSecondary); | ||||
| } | ||||
| .file-list li:before { | ||||
|   color: var(--textSecondary); | ||||
| } | ||||
| .file-list li[aria-selected=true]:before { | ||||
|   color: var(--icon); | ||||
| } | ||||
| 
 | ||||
| .shell { | ||||
|   background: var(--surfacePrimary); | ||||
|   color: var(--textPrimary); | ||||
| } | ||||
| .shell__result { | ||||
|   border-top: 1px solid var(--divider); | ||||
| } | ||||
| 
 | ||||
| #editor-container { | ||||
|   background: var(--background); | ||||
|  | @ -146,3 +190,11 @@ nav > div { | |||
|     background: var(--surfaceSecondary) !important; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .share__box, .share__box__download { | ||||
|   background: var(--surfaceSecondary) !important; | ||||
|   color: var(--textPrimary); | ||||
| } | ||||
| .share__box__download { | ||||
|   border-bottom-color: var(--divider); | ||||
| } | ||||
|  | @ -94,9 +94,6 @@ export async function post (url, content = '', overwrite = false, onupload) { | |||
|       request.upload.onprogress = onupload | ||||
|     } | ||||
| 
 | ||||
|     // Send a message to user before closing the tab during file upload
 | ||||
|     window.onbeforeunload = () => "Files are being uploaded." | ||||
| 
 | ||||
|     request.onload = () => { | ||||
|       if (request.status === 200) { | ||||
|         resolve(request.responseText) | ||||
|  | @ -112,29 +109,28 @@ export async function post (url, content = '', overwrite = false, onupload) { | |||
|     } | ||||
| 
 | ||||
|     request.send(content) | ||||
|     // Upload is done no more message before closing the tab 
 | ||||
|   }).finally(() => { window.onbeforeunload = null }) | ||||
|   }) | ||||
| } | ||||
| 
 | ||||
| function moveCopy (items, copy = false) { | ||||
| function moveCopy (items, copy = false, overwrite = false, rename = false) { | ||||
|   let promises = [] | ||||
| 
 | ||||
|   for (let item of items) { | ||||
|     const from = removePrefix(item.from) | ||||
|     const to = encodeURIComponent(removePrefix(item.to)) | ||||
|     const url = `${from}?action=${copy ? 'copy' : 'rename'}&destination=${to}` | ||||
|     const url = `${from}?action=${copy ? 'copy' : 'rename'}&destination=${to}&override=${overwrite}&rename=${rename}` | ||||
|     promises.push(resourceAction(url, 'PATCH')) | ||||
|   } | ||||
| 
 | ||||
|   return Promise.all(promises) | ||||
| } | ||||
| 
 | ||||
| export function move (items) { | ||||
|   return moveCopy(items) | ||||
| export function move (items, overwrite = false, rename = false) { | ||||
|   return moveCopy(items, false, overwrite, rename) | ||||
| } | ||||
| 
 | ||||
| export function copy (items) { | ||||
|   return moveCopy(items, true) | ||||
| export function copy (items, overwrite = false, rename = false) { | ||||
|   return moveCopy(items, true, overwrite, rename) | ||||
| } | ||||
| 
 | ||||
| export async function checksum (url, algo) { | ||||
|  |  | |||
|  | @ -10,10 +10,12 @@ | |||
|     @mouseup="mouseUp" | ||||
|     @wheel="wheelMove" | ||||
|   > | ||||
|     <img :src="src" class="image-ex-img" ref="imgex" @load="setCenter"> | ||||
|     <img :src="src" class="image-ex-img image-ex-img-center" ref="imgex" @load="onLoad"> | ||||
|   </div> | ||||
| </template> | ||||
| <script> | ||||
| import throttle from 'lodash.throttle' | ||||
| 
 | ||||
| export default { | ||||
|   props: { | ||||
|     src: String, | ||||
|  | @ -50,7 +52,12 @@ export default { | |||
|       inDrag: false, | ||||
|       lastTouchDistance: 0, | ||||
|       moveDisabled: false, | ||||
|       disabledTimer: null | ||||
|       disabledTimer: null, | ||||
|       imageLoaded: false, | ||||
|       position: { | ||||
|         center: { x: 0, y: 0 }, | ||||
|         relative: { x: 0, y: 0 } | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   mounted() { | ||||
|  | @ -63,24 +70,47 @@ export default { | |||
|     if (getComputedStyle(container).height === "0px") { | ||||
|       container.style.height = "100%" | ||||
|     } | ||||
| 
 | ||||
|     window.addEventListener('resize', this.onResize) | ||||
|   }, | ||||
|   beforeDestroy () { | ||||
|     window.removeEventListener('resize', this.onResize) | ||||
|     document.removeEventListener('mouseup', this.onMouseUp) | ||||
|   }, | ||||
|   methods: { | ||||
|     onLoad() { | ||||
|       let img = this.$refs.imgex | ||||
| 
 | ||||
|       this.imageLoaded = true | ||||
| 
 | ||||
|       if (img === undefined) { | ||||
|         return | ||||
|       } | ||||
| 
 | ||||
|       img.classList.remove('image-ex-img-center') | ||||
|       this.setCenter() | ||||
|       img.classList.add('image-ex-img-ready') | ||||
| 
 | ||||
|       document.addEventListener('mouseup', this.onMouseUp) | ||||
|     }, | ||||
|     onMouseUp() { | ||||
|       this.inDrag = false | ||||
|     }, | ||||
|     onResize: throttle(function() { | ||||
|       if (this.imageLoaded) { | ||||
|         this.setCenter() | ||||
|         this.doMove(this.position.relative.x, this.position.relative.y) | ||||
|       } | ||||
|     }, 100), | ||||
|     setCenter() { | ||||
|       let container = this.$refs.container | ||||
|       let img = this.$refs.imgex | ||||
| 
 | ||||
|       let rate = Math.min( | ||||
|         container.clientWidth / img.clientWidth, | ||||
|         container.clientHeight / img.clientHeight | ||||
|       ) | ||||
|       if (!this.autofill && rate > 1) { | ||||
|         rate = 1 | ||||
|       } | ||||
|       // height will be auto set | ||||
|       img.width = Math.floor(img.clientWidth * rate) | ||||
|       img.style.top = `${Math.floor((container.clientHeight - img.clientHeight) / 2)}px` | ||||
|       img.style.left = `${Math.floor((container.clientWidth - img.clientWidth) / 2)}px` | ||||
|       document.addEventListener('mouseup', () => this.inDrag = false ) | ||||
|       this.position.center.x = Math.floor((container.clientWidth - img.clientWidth) / 2) | ||||
|       this.position.center.y = Math.floor((container.clientHeight - img.clientHeight) / 2) | ||||
| 
 | ||||
|       img.style.left = this.position.center.x + 'px' | ||||
|       img.style.top = this.position.center.y + 'px' | ||||
|     }, | ||||
|     mousedownStart(event) { | ||||
|       this.lastX = null | ||||
|  | @ -159,8 +189,22 @@ export default { | |||
|     }, | ||||
|     doMove(x, y) { | ||||
|       let style = this.$refs.imgex.style | ||||
|       style.left = `${this.pxStringToNumber(style.left) + x}px` | ||||
|       style.top = `${this.pxStringToNumber(style.top) + y}px` | ||||
|       let posX = this.pxStringToNumber(style.left) + x | ||||
|       let posY = this.pxStringToNumber(style.top) + y | ||||
| 
 | ||||
|       style.left = posX + 'px' | ||||
|       style.top = posY + 'px' | ||||
| 
 | ||||
|       this.position.relative.x =  Math.abs(this.position.center.x - posX) | ||||
|       this.position.relative.y =  Math.abs(this.position.center.y - posY) | ||||
| 
 | ||||
|       if (posX < this.position.center.x) { | ||||
|         this.position.relative.x = this.position.relative.x * -1 | ||||
|       } | ||||
| 
 | ||||
|       if (posY < this.position.center.y) { | ||||
|         this.position.relative.y = this.position.relative.y * -1 | ||||
|       } | ||||
|     }, | ||||
|     wheelMove(event) { | ||||
|       this.scale += (event.wheelDeltaY / 100) * this.zoomStep | ||||
|  | @ -185,9 +229,20 @@ export default { | |||
| } | ||||
| 
 | ||||
| .image-ex-img { | ||||
|   position: absolute; | ||||
| } | ||||
| 
 | ||||
| .image-ex-img-center { | ||||
|   left: 50%; | ||||
|   top: 50%; | ||||
|   transform: translate(-50%, -50%); | ||||
|   position: absolute; | ||||
|   transition: none; | ||||
| } | ||||
| 
 | ||||
| .image-ex-img-ready { | ||||
|   left: 0; | ||||
|   top: 0; | ||||
|   position: absolute; | ||||
|   transition: transform 0.1s ease; | ||||
| } | ||||
| </style> | ||||
|  |  | |||
|  | @ -248,7 +248,8 @@ export default { | |||
| 
 | ||||
|       this.$store.commit('updateClipboard', { | ||||
|         key: key, | ||||
|         items: items | ||||
|         items: items, | ||||
|         path: this.$route.path | ||||
|       }) | ||||
|     }, | ||||
|     paste (event) { | ||||
|  | @ -261,23 +262,56 @@ export default { | |||
|       for (let item of this.$store.state.clipboard.items) { | ||||
|         const from = item.from.endsWith('/') ? item.from.slice(0, -1) : item.from | ||||
|         const to = this.$route.path + item.name | ||||
|         items.push({ from, to }) | ||||
|         items.push({ from, to, name: item.name }) | ||||
|       } | ||||
| 
 | ||||
|       if (items.length === 0) { | ||||
|         return | ||||
|       } | ||||
| 
 | ||||
|       if (this.$store.state.clipboard.key === 'x') { | ||||
|         api.move(items).then(() => { | ||||
|       let action = (overwrite, rename) => { | ||||
|         api.copy(items, overwrite, rename).then(() => { | ||||
|           this.$store.commit('setReload', true) | ||||
|         }).catch(this.$showError) | ||||
|       } | ||||
| 
 | ||||
|       if (this.$store.state.clipboard.key === 'x') { | ||||
|         action = (overwrite, rename) => { | ||||
|           api.move(items, overwrite, rename).then(() => { | ||||
|             this.$store.commit('resetClipboard') | ||||
|             this.$store.commit('setReload', true) | ||||
|           }).catch(this.$showError) | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       if (this.$store.state.clipboard.path == this.$route.path) { | ||||
|         action(false, true) | ||||
| 
 | ||||
|         return | ||||
|       } | ||||
| 
 | ||||
|       api.copy(items).then(() => { | ||||
|         this.$store.commit('setReload', true) | ||||
|       }).catch(this.$showError) | ||||
|       let conflict = upload.checkConflict(items, this.req.items) | ||||
| 
 | ||||
|       let overwrite = false | ||||
|       let rename = false | ||||
| 
 | ||||
|       if (conflict) { | ||||
|         this.$store.commit('showHover', { | ||||
|           prompt: 'replace-rename', | ||||
|           confirm: (event, option) => { | ||||
|             overwrite = option == 'overwrite' | ||||
|             rename = option == 'rename' | ||||
| 
 | ||||
|             event.preventDefault() | ||||
|             this.$store.commit('closeHovers') | ||||
|             action(overwrite, rename) | ||||
|           } | ||||
|         }) | ||||
| 
 | ||||
|         return | ||||
|       } | ||||
| 
 | ||||
|       action(overwrite, rename) | ||||
|     }, | ||||
|     resizeEvent () { | ||||
|       // Update the columns size based on the window width. | ||||
|  |  | |||
|  | @ -36,6 +36,7 @@ import { mapMutations, mapGetters, mapState } from 'vuex' | |||
| import filesize from 'filesize' | ||||
| import moment from 'moment' | ||||
| import { files as api } from '@/api' | ||||
| import * as upload  from '@/utils/upload' | ||||
| 
 | ||||
| export default { | ||||
|   name: 'item', | ||||
|  | @ -110,26 +111,61 @@ export default { | |||
| 
 | ||||
|       el.style.opacity = 1 | ||||
|     }, | ||||
|     drop: function (event) { | ||||
|     drop: async function (event) { | ||||
|       if (!this.canDrop) return | ||||
|       event.preventDefault() | ||||
| 
 | ||||
|       if (this.selectedCount === 0) return | ||||
| 
 | ||||
|       let el = event.target | ||||
|       for (let i = 0; i < 5; i++) { | ||||
|         if (el !== null && !el.classList.contains('item')) { | ||||
|           el = el.parentElement | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       let items = [] | ||||
| 
 | ||||
|       for (let i of this.selected) { | ||||
|         items.push({ | ||||
|           from: this.req.items[i].url, | ||||
|           to: this.url + this.req.items[i].name | ||||
|           to: this.url + this.req.items[i].name, | ||||
|           name: this.req.items[i].name | ||||
|         }) | ||||
|       }       | ||||
| 
 | ||||
|       let base = el.querySelector('.name').innerHTML + '/' | ||||
|       let path = this.$route.path + base | ||||
|       let baseItems = (await api.fetch(path)).items | ||||
| 
 | ||||
|       let action = (overwrite, rename) => { | ||||
|         api.move(items, overwrite, rename).then(() => { | ||||
|           this.$store.commit('setReload', true) | ||||
|         }).catch(this.$showError) | ||||
|       } | ||||
| 
 | ||||
|       api.move(items) | ||||
|         .then(() => { | ||||
|           this.$store.commit('setReload', true) | ||||
|       let conflict = upload.checkConflict(items, baseItems) | ||||
| 
 | ||||
|       let overwrite = false | ||||
|       let rename = false | ||||
| 
 | ||||
|       if (conflict) { | ||||
|         this.$store.commit('showHover', { | ||||
|           prompt: 'replace-rename', | ||||
|           confirm: (event, option) => { | ||||
|             overwrite = option == 'overwrite' | ||||
|             rename = option == 'rename' | ||||
| 
 | ||||
|             event.preventDefault() | ||||
|             this.$store.commit('closeHovers') | ||||
|             action(overwrite, rename) | ||||
|           } | ||||
|         }) | ||||
|         .catch(this.$showError) | ||||
| 
 | ||||
|         return | ||||
|       } | ||||
| 
 | ||||
|       action(overwrite, rename) | ||||
|     }, | ||||
|     click: function (event) { | ||||
|       if (this.selectedCount !== 0) event.preventDefault() | ||||
|  |  | |||
|  | @ -5,10 +5,22 @@ | |||
|         <i class="material-icons">close</i> | ||||
|       </button> | ||||
| 
 | ||||
|       <rename-button v-if="user.perm.rename"></rename-button> | ||||
|       <delete-button v-if="user.perm.delete"></delete-button> | ||||
|       <download-button v-if="user.perm.download"></download-button> | ||||
|       <info-button></info-button> | ||||
|       <div class="title"> | ||||
|         <span>{{ this.name }}</span> | ||||
|       </div> | ||||
| 
 | ||||
|       <rename-button :disabled="loading" v-if="user.perm.rename"></rename-button> | ||||
|       <delete-button :disabled="loading" v-if="user.perm.delete"></delete-button> | ||||
|       <download-button :disabled="loading" v-if="user.perm.download"></download-button> | ||||
|       <info-button :disabled="loading"></info-button> | ||||
|     </div> | ||||
| 
 | ||||
|     <div class="loading" v-if="loading"> | ||||
|       <div class="spinner"> | ||||
|         <div class="bounce1"></div> | ||||
|         <div class="bounce2"></div> | ||||
|         <div class="bounce3"></div> | ||||
|       </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <button class="action" @click="prev" v-show="hasPrevious" :aria-label="$t('buttons.previous')" :title="$t('buttons.previous')"> | ||||
|  | @ -18,25 +30,27 @@ | |||
|       <i class="material-icons">chevron_right</i> | ||||
|     </button> | ||||
| 
 | ||||
|     <div class="preview"> | ||||
|       <ExtendedImage v-if="req.type == 'image'" :src="raw"></ExtendedImage> | ||||
|       <audio v-else-if="req.type == 'audio'" :src="raw" autoplay controls></audio> | ||||
|       <video v-else-if="req.type == 'video'" :src="raw" autoplay controls> | ||||
|         <track | ||||
|           kind="captions" | ||||
|           v-for="(sub, index) in subtitles" | ||||
|           :key="index" | ||||
|           :src="sub" | ||||
|           :label="'Subtitle ' + index" :default="index === 0"> | ||||
|         Sorry, your browser doesn't support embedded videos, | ||||
|         but don't worry, you can <a :href="download">download it</a> | ||||
|         and watch it with your favorite video player! | ||||
|       </video> | ||||
|       <object v-else-if="req.extension == '.pdf'" class="pdf" :data="raw"></object> | ||||
|       <a v-else-if="req.type == 'blob'" :href="download"> | ||||
|         <h2 class="message">{{ $t('buttons.download') }} <i class="material-icons">file_download</i></h2> | ||||
|       </a> | ||||
|     </div> | ||||
|     <template v-if="!loading"> | ||||
|       <div class="preview"> | ||||
|         <ExtendedImage v-if="req.type == 'image'" :src="raw"></ExtendedImage> | ||||
|         <audio v-else-if="req.type == 'audio'" :src="raw" autoplay controls></audio> | ||||
|         <video v-else-if="req.type == 'video'" :src="raw" autoplay controls> | ||||
|           <track | ||||
|             kind="captions" | ||||
|             v-for="(sub, index) in subtitles" | ||||
|             :key="index" | ||||
|             :src="sub" | ||||
|             :label="'Subtitle ' + index" :default="index === 0"> | ||||
|           Sorry, your browser doesn't support embedded videos, | ||||
|           but don't worry, you can <a :href="download">download it</a> | ||||
|           and watch it with your favorite video player! | ||||
|         </video> | ||||
|         <object v-else-if="req.extension == '.pdf'" class="pdf" :data="raw"></object> | ||||
|         <a v-else-if="req.type == 'blob'" :href="download"> | ||||
|           <h2 class="message">{{ $t('buttons.download') }} <i class="material-icons">file_download</i></h2> | ||||
|         </a> | ||||
|       </div> | ||||
|     </template> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
|  | @ -72,11 +86,12 @@ export default { | |||
|       previousLink: '', | ||||
|       nextLink: '', | ||||
|       listing: null, | ||||
|       name: '', | ||||
|       subtitles: [] | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     ...mapState(['req', 'user', 'oldReq', 'jwt']), | ||||
|     ...mapState(['req', 'user', 'oldReq', 'jwt', 'loading']), | ||||
|     hasPrevious () { | ||||
|       return (this.previousLink !== '') | ||||
|     }, | ||||
|  | @ -96,30 +111,24 @@ export default { | |||
|       return `${this.previewUrl}&inline=true` | ||||
|     } | ||||
|   }, | ||||
|   watch: { | ||||
|     $route: function () { | ||||
|       this.updatePreview() | ||||
|     } | ||||
|   }, | ||||
|   async mounted () { | ||||
|     window.addEventListener('keyup', this.key) | ||||
| 
 | ||||
|     if (this.req.subtitles) { | ||||
|       this.subtitles = this.req.subtitles.map(sub => `${baseURL}/api/raw${sub}?auth=${this.jwt}&inline=true`) | ||||
|     } | ||||
| 
 | ||||
|     try { | ||||
|       if (this.oldReq.items) { | ||||
|         this.updateLinks(this.oldReq.items) | ||||
|       } else { | ||||
|         const path = url.removeLastDir(this.$route.path) | ||||
|         const res = await api.fetch(path) | ||||
|         this.updateLinks(res.items) | ||||
|       } | ||||
|     } catch (e) { | ||||
|       this.$showError(e) | ||||
|     } | ||||
|     this.$store.commit('setPreviewMode', true) | ||||
|     this.listing = this.oldReq.items | ||||
|     this.updatePreview() | ||||
|   }, | ||||
|   beforeDestroy () { | ||||
|     window.removeEventListener('keyup', this.key) | ||||
|     this.$store.commit('setPreviewMode', false) | ||||
|   }, | ||||
|   methods: { | ||||
|     back () { | ||||
|       this.$store.commit('setPreviewMode', false) | ||||
|       let uri = url.removeLastDir(this.$route.path) + '/' | ||||
|       this.$router.push({ path: uri }) | ||||
|     }, | ||||
|  | @ -138,22 +147,42 @@ export default { | |||
|         if (this.hasPrevious) this.prev() | ||||
|       } | ||||
|     }, | ||||
|     updateLinks (items) { | ||||
|       for (let i = 0; i < items.length; i++) { | ||||
|         if (items[i].name !== this.req.name) { | ||||
|     async updatePreview () { | ||||
|       if (this.req.subtitles) { | ||||
|         this.subtitles = this.req.subtitles.map(sub => `${baseURL}/api/raw${sub}?auth=${this.jwt}&inline=true`) | ||||
|       } | ||||
| 
 | ||||
|       let dirs = this.$route.fullPath.split("/") | ||||
|       this.name = decodeURIComponent(dirs[dirs.length - 1]) | ||||
| 
 | ||||
|       if (!this.listing) { | ||||
|         try { | ||||
|           const path = url.removeLastDir(this.$route.path) | ||||
|           const res = await api.fetch(path) | ||||
|           this.listing = res.items | ||||
|         } catch (e) { | ||||
|           this.$showError(e) | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       this.previousLink = '' | ||||
|       this.nextLink = '' | ||||
| 
 | ||||
|       for (let i = 0; i < this.listing.length; i++) { | ||||
|         if (this.listing[i].name !== this.name) { | ||||
|           continue | ||||
|         } | ||||
| 
 | ||||
|         for (let j = i - 1; j >= 0; j--) { | ||||
|           if (mediaTypes.includes(items[j].type)) { | ||||
|             this.previousLink = items[j].url | ||||
|           if (mediaTypes.includes(this.listing[j].type)) { | ||||
|             this.previousLink = this.listing[j].url | ||||
|             break | ||||
|           } | ||||
|         } | ||||
| 
 | ||||
|         for (let j = i + 1; j < items.length; j++) { | ||||
|           if (mediaTypes.includes(items[j].type)) { | ||||
|             this.nextLink = items[j].url | ||||
|         for (let j = i + 1; j < this.listing.length; j++) { | ||||
|           if (mediaTypes.includes(this.listing[j].type)) { | ||||
|             this.nextLink = this.listing[j].url | ||||
|             break | ||||
|           } | ||||
|         } | ||||
|  |  | |||
|  | @ -16,7 +16,6 @@ | |||
|         :title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button> | ||||
|       <button class="button button--flat" | ||||
|         @click="copy" | ||||
|         :disabled="$route.path === dest" | ||||
|         :aria-label="$t('buttons.copy')" | ||||
|         :title="$t('buttons.copy')">{{ $t('buttons.copy') }}</button> | ||||
|     </div> | ||||
|  | @ -28,6 +27,7 @@ import { mapState } from 'vuex' | |||
| import FileList from './FileList' | ||||
| import { files as api } from '@/api' | ||||
| import buttons from '@/utils/buttons' | ||||
| import * as upload  from '@/utils/upload' | ||||
| 
 | ||||
| export default { | ||||
|   name: 'copy', | ||||
|  | @ -42,25 +42,66 @@ export default { | |||
|   methods: { | ||||
|     copy: async function (event) { | ||||
|       event.preventDefault() | ||||
|       buttons.loading('copy') | ||||
|       let items = [] | ||||
| 
 | ||||
|       // Create a new promise for each file. | ||||
|       for (let item of this.selected) { | ||||
|         items.push({ | ||||
|           from: this.req.items[item].url, | ||||
|           to: this.dest + encodeURIComponent(this.req.items[item].name) | ||||
|           to: this.dest + encodeURIComponent(this.req.items[item].name), | ||||
|           name: this.req.items[item].name | ||||
|         }) | ||||
|       } | ||||
| 
 | ||||
|       try { | ||||
|         await api.copy(items) | ||||
|         buttons.success('copy') | ||||
|         this.$router.push({ path: this.dest }) | ||||
|       } catch (e) { | ||||
|         buttons.done('copy') | ||||
|         this.$showError(e) | ||||
|       let action = async (overwrite, rename) => { | ||||
|         buttons.loading('copy') | ||||
| 
 | ||||
|         await api.copy(items, overwrite, rename).then(() => { | ||||
|           buttons.success('copy') | ||||
| 
 | ||||
|           if (this.$route.path === this.dest) { | ||||
|             this.$store.commit('setReload', true) | ||||
| 
 | ||||
|             return | ||||
|           } | ||||
| 
 | ||||
|           this.$router.push({ path: this.dest }) | ||||
|         }).catch((e) => { | ||||
|           buttons.done('copy') | ||||
|           this.$showError(e) | ||||
|         }) | ||||
|       } | ||||
| 
 | ||||
|       if (this.$route.path === this.dest) { | ||||
|         this.$store.commit('closeHovers') | ||||
|         action(false, true) | ||||
| 
 | ||||
|         return | ||||
|       } | ||||
| 
 | ||||
|       let dstItems = (await api.fetch(this.dest)).items | ||||
|       let conflict = upload.checkConflict(items, dstItems) | ||||
| 
 | ||||
|       let overwrite = false | ||||
|       let rename = false | ||||
| 
 | ||||
|       if (conflict) { | ||||
|         this.$store.commit('showHover', { | ||||
|           prompt: 'replace-rename', | ||||
|           confirm: (event, option) => { | ||||
|             overwrite = option == 'overwrite' | ||||
|             rename = option == 'rename' | ||||
| 
 | ||||
|             event.preventDefault() | ||||
|             this.$store.commit('closeHovers') | ||||
|             action(overwrite, rename) | ||||
|           } | ||||
|         }) | ||||
| 
 | ||||
|         return | ||||
|       } | ||||
| 
 | ||||
|       action(overwrite, rename) | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -41,19 +41,7 @@ export default { | |||
|     } | ||||
|   }, | ||||
|   mounted () { | ||||
|     // If we're showing this on a listing, | ||||
|     // we can use the current request object | ||||
|     // to fill the move options. | ||||
|     if (this.req.kind === 'listing') { | ||||
|       this.fillOptions(this.req) | ||||
|       return | ||||
|     } | ||||
| 
 | ||||
|     // Otherwise, we must be on a preview or editor | ||||
|     // so we fetch the data from the previous directory. | ||||
|     files.fetch(url.removeLastDir(this.$route.path)) | ||||
|       .then(this.fillOptions) | ||||
|       .catch(this.$showError) | ||||
|     this.fillOptions(this.req) | ||||
|   }, | ||||
|   methods: { | ||||
|     fillOptions (req) { | ||||
|  |  | |||
|  | @ -27,6 +27,7 @@ import { mapState } from 'vuex' | |||
| import FileList from './FileList' | ||||
| import { files as api } from '@/api' | ||||
| import buttons from '@/utils/buttons' | ||||
| import * as upload  from '@/utils/upload' | ||||
| 
 | ||||
| export default { | ||||
|   name: 'move', | ||||
|  | @ -41,26 +42,51 @@ export default { | |||
|   methods: { | ||||
|     move: async function (event) { | ||||
|       event.preventDefault() | ||||
|       buttons.loading('move') | ||||
|       let items = [] | ||||
| 
 | ||||
|       for (let item of this.selected) { | ||||
|         items.push({ | ||||
|           from: this.req.items[item].url, | ||||
|           to: this.dest + encodeURIComponent(this.req.items[item].name) | ||||
|           to: this.dest + encodeURIComponent(this.req.items[item].name), | ||||
|           name: this.req.items[item].name | ||||
|         }) | ||||
|       } | ||||
| 
 | ||||
|       try { | ||||
|         api.move(items) | ||||
|         buttons.success('move') | ||||
|         this.$router.push({ path: this.dest }) | ||||
|       } catch (e) { | ||||
|         buttons.done('move') | ||||
|         this.$showError(e) | ||||
|       let action = async (overwrite, rename) => { | ||||
|         buttons.loading('move') | ||||
| 
 | ||||
|         await api.move(items, overwrite, rename).then(() => { | ||||
|           buttons.success('move') | ||||
|           this.$router.push({ path: this.dest }) | ||||
|         }).catch((e) => { | ||||
|           buttons.done('move') | ||||
|           this.$showError(e) | ||||
|         }) | ||||
|       } | ||||
| 
 | ||||
|       event.preventDefault() | ||||
|       let dstItems = (await api.fetch(this.dest)).items | ||||
|       let conflict = upload.checkConflict(items, dstItems) | ||||
| 
 | ||||
|       let overwrite = false | ||||
|       let rename = false | ||||
| 
 | ||||
|       if (conflict) { | ||||
|         this.$store.commit('showHover', { | ||||
|           prompt: 'replace-rename', | ||||
|           confirm: (event, option) => { | ||||
|             overwrite = option == 'overwrite' | ||||
|             rename = option == 'rename' | ||||
| 
 | ||||
|             event.preventDefault() | ||||
|             this.$store.commit('closeHovers') | ||||
|             action(overwrite, rename) | ||||
|           } | ||||
|         }) | ||||
| 
 | ||||
|         return | ||||
|       } | ||||
| 
 | ||||
|       action(overwrite, rename) | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -16,6 +16,7 @@ import Copy from './Copy' | |||
| import NewFile from './NewFile' | ||||
| import NewDir from './NewDir' | ||||
| import Replace from './Replace' | ||||
| import ReplaceRename from './ReplaceRename' | ||||
| import Share from './Share' | ||||
| import Upload from './Upload' | ||||
| import { mapState } from 'vuex' | ||||
|  | @ -35,6 +36,7 @@ export default { | |||
|     NewDir, | ||||
|     Help, | ||||
|     Replace, | ||||
|     ReplaceRename, | ||||
|     Upload | ||||
|   }, | ||||
|   data: function () { | ||||
|  | @ -52,7 +54,7 @@ export default { | |||
|       return | ||||
| 
 | ||||
|       let prompt = this.$refs.currentComponent; | ||||
|        | ||||
| 
 | ||||
|       // Enter | ||||
|       if (event.keyCode == 13) { | ||||
|         switch (this.show) { | ||||
|  | @ -87,6 +89,7 @@ export default { | |||
|         'newDir', | ||||
|         'download', | ||||
|         'replace', | ||||
|         'replace-rename', | ||||
|         'share', | ||||
|         'upload' | ||||
|       ].indexOf(this.show) >= 0; | ||||
|  |  | |||
|  | @ -0,0 +1,35 @@ | |||
| <template> | ||||
|   <div class="card floating"> | ||||
|     <div class="card-title"> | ||||
|       <h2>{{ $t('prompts.replace') }}</h2> | ||||
|     </div> | ||||
| 
 | ||||
|     <div class="card-content"> | ||||
|       <p>{{ $t('prompts.replaceMessage') }}</p> | ||||
|     </div> | ||||
| 
 | ||||
|     <div class="card-action"> | ||||
|       <button class="button button--flat button--grey" | ||||
|         @click="$store.commit('closeHovers')" | ||||
|         :aria-label="$t('buttons.cancel')" | ||||
|         :title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button> | ||||
|       <button class="button button--flat button--blue" | ||||
|         @click="(event) => showConfirm(event, 'rename')" | ||||
|         :aria-label="$t('buttons.rename')" | ||||
|         :title="$t('buttons.rename')">{{ $t('buttons.rename') }}</button> | ||||
|       <button class="button button--flat button--red" | ||||
|         @click="(event) => showConfirm(event, 'overwrite')" | ||||
|         :aria-label="$t('buttons.replace')" | ||||
|         :title="$t('buttons.replace')">{{ $t('buttons.replace') }}</button> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| import { mapState } from 'vuex' | ||||
| 
 | ||||
| export default { | ||||
|   name: 'replace-rename', | ||||
|   computed: mapState(['showConfirm']) | ||||
| } | ||||
| </script> | ||||
|  | @ -25,8 +25,8 @@ | |||
|   background: var(--red); | ||||
| } | ||||
| 
 | ||||
| .button--red:hover { | ||||
|   background: var(--dark-red); | ||||
| .button--blue { | ||||
|   background: var(--blue); | ||||
| } | ||||
| 
 | ||||
| .button--flat { | ||||
|  |  | |||
|  | @ -125,8 +125,13 @@ | |||
|   height: 3.7em; | ||||
| } | ||||
| 
 | ||||
| #previewer .action:first-of-type { | ||||
| #previewer .bar .title { | ||||
|   margin-right: auto; | ||||
|   padding: 0 1em; | ||||
|   line-height: 2.7em; | ||||
|   overflow: hidden; | ||||
|   word-break: break-word; | ||||
|   color: #fff; | ||||
| } | ||||
| 
 | ||||
| #previewer .action i { | ||||
|  | @ -219,6 +224,11 @@ | |||
|   font-size: 1.2em; | ||||
| } | ||||
| 
 | ||||
| #previewer .loading { | ||||
|   height: 100%; | ||||
|   width: 100%; | ||||
| } | ||||
| 
 | ||||
| #editor-container #editor { | ||||
|   height: calc(100vh - 8.2em); | ||||
| } | ||||
|  |  | |||
|  | @ -23,7 +23,8 @@ const state = { | |||
|   show: null, | ||||
|   showShell: false, | ||||
|   showMessage: null, | ||||
|   showConfirm: null | ||||
|   showConfirm: null, | ||||
|   previewMode: false | ||||
| } | ||||
| 
 | ||||
| export default new Vuex.Store({ | ||||
|  |  | |||
|  | @ -37,6 +37,11 @@ const mutations = { | |||
|   } | ||||
| } | ||||
| 
 | ||||
| const beforeUnload = (event) => { | ||||
|   event.preventDefault() | ||||
|   event.returnValue = '' | ||||
| } | ||||
| 
 | ||||
| const actions = { | ||||
|   upload: (context, item) => { | ||||
|     let uploadsCount = Object.keys(context.state.uploads).length; | ||||
|  | @ -45,6 +50,7 @@ const actions = { | |||
|     let isUploadsEmpty = uploadsCount == 0 | ||||
| 
 | ||||
|     if (isQueueEmpty && isUploadsEmpty) { | ||||
|       window.addEventListener('beforeunload', beforeUnload) | ||||
|       buttons.loading('upload') | ||||
|     } | ||||
| 
 | ||||
|  | @ -67,6 +73,7 @@ const actions = { | |||
|     let canProcess = isBellowLimit && !isQueueEmpty | ||||
| 
 | ||||
|     if (isFinished) { | ||||
|       window.removeEventListener('beforeunload', beforeUnload) | ||||
|       buttons.success('upload') | ||||
|       context.commit('reset') | ||||
|       context.commit('setReload', true, { root: true }) | ||||
|  |  | |||
|  | @ -78,10 +78,14 @@ const mutations = { | |||
|   updateClipboard: (state, value) => { | ||||
|     state.clipboard.key = value.key | ||||
|     state.clipboard.items = value.items | ||||
|     state.clipboard.path = value.path | ||||
|   }, | ||||
|   resetClipboard: (state) => { | ||||
|     state.clipboard.key = '' | ||||
|     state.clipboard.items = [] | ||||
|   }, | ||||
|   setPreviewMode(state, value) { | ||||
|     state.previewMode = value | ||||
|   } | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -6,10 +6,7 @@ export function checkConflict(files, items) { | |||
|     items = [] | ||||
|   } | ||||
| 
 | ||||
|   let folder_upload = false | ||||
|   if (files[0].fullPath !== undefined) { | ||||
|     folder_upload = true | ||||
|   } | ||||
|   let folder_upload = files[0].fullPath !== undefined | ||||
| 
 | ||||
|   let conflict = false | ||||
|   for (let i = 0; i < files.length; i++) { | ||||
|  | @ -69,7 +66,7 @@ export function scanFiles(dt) { | |||
|         const dir = { | ||||
|           isDir: true, | ||||
|           size: 0, | ||||
|           path: `${directory}${entry.name}` | ||||
|           fullPath: `${directory}${entry.name}` | ||||
|         } | ||||
| 
 | ||||
|         contents.push(dir) | ||||
|  | @ -112,7 +109,7 @@ export function handleFiles(files, path, overwrite = false) { | |||
| 
 | ||||
|     if (file.isDir) { | ||||
|       itemPath = path | ||||
|       let folders = file.path.split("/") | ||||
|       let folders = file.fullPath.split("/") | ||||
| 
 | ||||
|       for (let i = 0; i < folders.length; i++) { | ||||
|         let folder = folders[i] | ||||
|  |  | |||
|  | @ -10,14 +10,15 @@ | |||
|         <router-link :to="link.url">{{ link.name }}</router-link> | ||||
|       </span> | ||||
|     </div> | ||||
| 
 | ||||
|     <div v-if="error"> | ||||
|       <not-found v-if="error.message === '404'"></not-found> | ||||
|       <forbidden v-else-if="error.message === '403'"></forbidden> | ||||
|       <internal-error v-else></internal-error> | ||||
|     </div> | ||||
|     <preview v-else-if="isPreview"></preview> | ||||
|     <editor v-else-if="isEditor"></editor> | ||||
|     <listing :class="{ multiple }" v-else-if="isListing"></listing> | ||||
|     <preview v-else-if="isPreview"></preview> | ||||
|     <div v-else> | ||||
|       <h2 class="message"> | ||||
|         <span>{{ $t('files.loading') }}</span> | ||||
|  | @ -65,7 +66,7 @@ export default { | |||
|       'show' | ||||
|     ]), | ||||
|     isPreview () { | ||||
|       return !this.loading && !this.isListing && !this.isEditor | ||||
|       return !this.loading && !this.isListing && !this.isEditor || this.loading && this.$store.state.previewMode | ||||
|     }, | ||||
|     breadcrumbs () { | ||||
|       let parts = this.$route.path.split('/') | ||||
|  |  | |||
|  | @ -127,6 +127,10 @@ var resourcePostPutHandler = withUser(func(w http.ResponseWriter, r *http.Reques | |||
| 		return nil | ||||
| 	}, action, r.URL.Path, "", d.user) | ||||
| 
 | ||||
| 	if err != nil { | ||||
| 		_ = d.user.Fs.RemoveAll(r.URL.Path) | ||||
| 	} | ||||
| 
 | ||||
| 	return errToStatus(err), err | ||||
| }) | ||||
| 
 | ||||
|  | @ -144,6 +148,31 @@ var resourcePatchHandler = withUser(func(w http.ResponseWriter, r *http.Request, | |||
| 		return http.StatusForbidden, nil | ||||
| 	} | ||||
| 
 | ||||
| 	override := r.URL.Query().Get("override") == "true" | ||||
| 	rename := r.URL.Query().Get("rename") == "true" | ||||
| 
 | ||||
| 	if !override && !rename { | ||||
| 		if _, err = d.user.Fs.Stat(dst); err == nil { | ||||
| 			return http.StatusConflict, nil | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if rename { | ||||
| 		counter := 1 | ||||
| 		dir, name := filepath.Split(dst) | ||||
| 		ext := filepath.Ext(name) | ||||
| 		base := strings.TrimSuffix(name, ext) | ||||
| 
 | ||||
| 		for { | ||||
| 			if _, err = d.user.Fs.Stat(dst); err != nil { | ||||
| 				break | ||||
| 			} | ||||
| 			new := fmt.Sprintf("%s(%d)%s", base, counter, ext) | ||||
| 			dst = filepath.Join(dir, new) | ||||
| 			counter++ | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	err = d.RunHook(func() error { | ||||
| 		switch action { | ||||
| 		// TODO: use enum
 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue