Merge remote-tracking branch 'origin/bulk-operations'

# Conflicts:
#	app/Http/Helpers/helpers.php
#	config/vuefilemanager.php
#	public/chunks/app-others.js
#	public/chunks/files~chunks/shared-files~chunks/shared-page~chunks/trash.js
#	public/js/main.js
#	public/mix-manifest.json
#	resources/js/components/FilesView/FileItemList.vue
#	resources/js/components/FilesView/FilePreview.vue
#	resources/js/helpers.js
#	resources/js/store/modules/fileFunctions.js
This commit is contained in:
Peter Papp
2020-12-21 17:02:49 +01:00
167 changed files with 6839 additions and 3151 deletions
+190 -154
View File
@@ -1,5 +1,5 @@
<template>
<div id="vue-file-manager" v-cloak>
<div id="vue-file-manager" v-cloak @click="unClick">
<!--System alerts-->
<Alert/>
@@ -7,21 +7,38 @@
<div id="application-wrapper" v-if="! isGuestLayout">
<!-- Full File Preview -->
<FileFullPreview />
<FileFullPreview/>
<!--Mobile Navigation-->
<MobileNavigation />
<MobileNavigation/>
<ProcessingPopup/>
<!--Confirm Popup-->
<Confirm />
<Confirm/>
<!--Share Item setup-->
<ShareCreate/>
<ShareEdit/>
<!--Rename folder or file item-->
<RenameItem/>
<!--Create folder in mobile version-->
<CreateFolder/>
<!--Move item setup-->
<MoveItem/>
<!-- Mobile Menu for Multiselected items -->
<MobileMultiSelectMenu/>
<!-- Drag & Drop UI -->
<DragUI/>
<!-- Mobile menu for selecting view and sorting -->
<MobileSortingAndPreview/>
<!--Mobile Menu-->
<MobileMenu/>
@@ -39,7 +56,7 @@
<router-view v-if="isGuestLayout"/>
<CookieDisclaimer />
<CookieDisclaimer/>
<!--Background vignette-->
<Vignette/>
@@ -47,168 +64,187 @@
</template>
<script>
import ToastrWrapper from '@/components/Others/Notifications/ToastrWrapper'
import FileFullPreview from '@/components/FilesView/FileFullPreview'
import MobileNavigation from '@/components/Others/MobileNavigation'
import CookieDisclaimer from '@/components/Others/CookieDisclaimer'
import MobileMenu from '@/components/FilesView/MobileMenu'
import ShareCreate from '@/components/Others/ShareCreate'
import Confirm from '@/components/Others/Popup/Confirm'
import ShareEdit from '@/components/Others/ShareEdit'
import MoveItem from '@/components/Others/MoveItem'
import Vignette from '@/components/Others/Vignette'
import MenuBar from '@/components/Sidebar/MenuBar'
import Alert from '@/components/FilesView/Alert'
import {includes} from 'lodash'
import {mapGetters} from 'vuex'
import {events} from "./bus"
import MobileSortingAndPreview from '@/components/FilesView/MobileSortingAndPreview'
import MobileMultiSelectMenu from '@/components/FilesView/MobileMultiSelectMenu'
import ToastrWrapper from '@/components/Others/Notifications/ToastrWrapper'
import ProcessingPopup from '@/components/FilesView/ProcessingPopup'
import FileFullPreview from '@/components/FilesView/FileFullPreview'
import MobileNavigation from '@/components/Others/MobileNavigation'
import CookieDisclaimer from '@/components/Others/CookieDisclaimer'
import CreateFolder from '@/components/Others/CreateFolder'
import MobileMenu from '@/components/FilesView/MobileMenu'
import ShareCreate from '@/components/Others/ShareCreate'
import Confirm from '@/components/Others/Popup/Confirm'
import RenameItem from '@/components/Others/RenameItem'
import ShareEdit from '@/components/Others/ShareEdit'
import MoveItem from '@/components/Others/MoveItem'
import Vignette from '@/components/Others/Vignette'
import DragUI from '@/components/FilesView/DragUI'
import MenuBar from '@/components/Sidebar/MenuBar'
import Alert from '@/components/FilesView/Alert'
import { includes } from 'lodash'
import { mapGetters } from 'vuex'
import { events } from './bus'
export default {
name: 'app',
components: {
MobileNavigation,
CookieDisclaimer,
FileFullPreview,
ToastrWrapper,
ShareCreate,
MobileMenu,
ShareEdit,
MoveItem,
Vignette,
Confirm,
MenuBar,
Alert,
},
computed: {
...mapGetters([
'isLogged', 'isGuest', 'config'
]),
isGuestLayout() {
return (includes([
'InstallationDisclaimer',
'SubscriptionService',
'StripeCredentials',
'SubscriptionPlans',
'ForgottenPassword',
'CreateNewPassword',
'EnvironmentSetup',
'VerifyByPassword',
'SaaSLandingPage',
'BillingsDetail',
'NotFoundShared',
'AdminAccount',
'PurchaseCode',
'DynamicPage',
'SharedPage',
'ContactUs',
'AppSetup',
'Database',
'Upgrade',
'SignIn',
'SignUp',
], this.$route.name)
)
}
},
data() {
return {
isScaledDown: false,
}
},
beforeMount() {
// Store config to vuex
this.$store.commit('INIT', {
authCookie: this.$root.$data.config.hasAuthCookie,
config: this.$root.$data.config,
rootDirectory: {
name: this.$t('locations.home'),
location: 'base',
unique_id: 0,
}
})
// Get installation state
let installation = this.$root.$data.config.installation
// Redirect to database verify code
if ( installation === 'setup-database') {
this.$router.push({name: 'PurchaseCode'})
}
// Redirect to starting installation process
if ( installation === 'setup-disclaimer' ) {
this.$router.push({name: 'InstallationDisclaimer'})
}
},
mounted() {
// Handle mobile navigation scale animation
events.$on('show:mobile-navigation', () => this.isScaledDown = true)
events.$on('hide:mobile-navigation', () => this.isScaledDown = false)
events.$on('mobileMenu:show', () => this.isScaledDown = true)
events.$on('fileItem:deselect', () => this.isScaledDown = false)
export default {
name: 'app',
components: {
MobileSortingAndPreview,
MobileMultiSelectMenu,
MobileNavigation,
CookieDisclaimer,
FileFullPreview,
ProcessingPopup,
ToastrWrapper,
CreateFolder,
ShareCreate,
MobileMenu,
RenameItem,
ShareEdit,
MoveItem,
Vignette,
Confirm,
MenuBar,
DragUI,
Alert
},
computed: {
...mapGetters([
'isLogged', 'isGuest', 'config'
]),
isGuestLayout() {
return (includes([
'InstallationDisclaimer',
'SubscriptionService',
'StripeCredentials',
'SubscriptionPlans',
'ForgottenPassword',
'CreateNewPassword',
'EnvironmentSetup',
'VerifyByPassword',
'SaaSLandingPage',
'BillingsDetail',
'NotFoundShared',
'AdminAccount',
'PurchaseCode',
'DynamicPage',
'SharedPage',
'ContactUs',
'AppSetup',
'Database',
'Upgrade',
'SignIn',
'SignUp'
], this.$route.name)
)
}
},
data() {
return {
isScaledDown: false
}
},
methods: {
unClick() {
events.$emit('unClick')
}
},
beforeMount() {
// Store config to vuex
this.$store.commit('INIT', {
authCookie: this.$root.$data.config.hasAuthCookie,
config: this.$root.$data.config,
rootDirectory: {
name: this.$t('locations.home'),
location: 'base',
unique_id: 0
}
})
// Get installation state
let installation = this.$root.$data.config.installation
// Redirect to database verify code
if (installation === 'setup-database')
this.$router.push({ name: 'PurchaseCode' })
// Redirect to starting installation process
if (installation === 'setup-disclaimer')
this.$router.push({ name: 'InstallationDisclaimer' })
},
mounted() {
this.$checkOS()
// Handle mobile navigation scale animation
events.$on('show:mobile-navigation', () => this.isScaledDown = true)
events.$on('hide:mobile-navigation', () => this.isScaledDown = false)
events.$on('mobileMenu:show', () => this.isScaledDown = true)
events.$on('fileItem:deselect', () => this.isScaledDown = false)
events.$on('mobileSortingAndPreview', state => this.isScaledDown = state)
}
}
</script>
<style lang="scss">
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@200;300;400;600;700;800;900&display=swap');
@import '@assets/vue-file-manager/_variables';
@import '@assets/vue-file-manager/_mixins';
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@200;300;400;600;700;800;900&display=swap');
@import '@assets/vue-file-manager/_variables';
@import '@assets/vue-file-manager/_mixins';
[v-cloak],
[v-cloak] > * {
display: none
[v-cloak],
[v-cloak] > * {
display: none
}
* {
outline: 0;
margin: 0;
padding: 0;
font-family: 'Nunito', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
box-sizing: border-box;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
font-size: 16px;
text-decoration: none;
color: $text;
}
#auth {
width: 100%;
height: 100%;
}
#vue-file-manager {
position: absolute;
width: 100%;
height: 100%;
overflow-y: auto;
scroll-behavior: smooth;
}
@media only screen and (max-width: 690px) {
.is-scaled-down {
@include transform(scale(0.95));
}
}
// Dark mode support
@media (prefers-color-scheme: dark) {
* {
outline: 0;
margin: 0;
padding: 0;
font-family: 'Nunito', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
box-sizing: border-box;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
font-size: 16px;
text-decoration: none;
color: $text;
color: $dark_mode_text_primary;
}
#auth {
width: 100%;
height: 100%;
}
body, html {
background: $dark_mode_background;
color: $dark_mode_text_primary;
#vue-file-manager {
position: absolute;
width: 100%;
height: 100%;
overflow-y: auto;
scroll-behavior:smooth;
}
@media only screen and (max-width: 690px) {
.is-scaled-down {
@include transform(scale(0.95));
}
}
// Dark mode support
@media (prefers-color-scheme: dark) {
* {
color: $dark_mode_text_primary;
}
body, html {
background: $dark_mode_background;
color: $dark_mode_text_primary;
img {
opacity: .95;
}
img {
opacity: .95;
}
}
}
</style>
+258 -410
View File
@@ -1,342 +1,223 @@
<template>
<div :style="{ top: positionY + 'px', left: positionX + 'px' }" @click="closeAndResetContextMenu" class="contextmenu" v-show="isVisible || showFromPreview" ref="contextmenu" :class="{ 'filePreviewFixed' : showFromPreview}">
<!-- ContextMenu for File Preview -->
<!-- File Preview -->
<div class="menu-options" id="menu-list" v-if="showFromPreview">
<ul class="menu-option-group">
<li class="menu-option" @click="moveItem">
<div class="icon">
<corner-down-right-icon size="17"></corner-down-right-icon>
</div>
<div class="text-label">
{{ $t('context_menu.move') }}
</div>
</li>
<li class="menu-option" @click="shareItem">
<div class="icon">
<link-icon size="17"></link-icon>
</div>
<div class="text-label">
{{
item.shared
? $t('context_menu.share_edit')
: $t('context_menu.share')
}}
</div>
</li>
<li class="menu-option" @click="deleteItem">
<div class="icon">
<trash-2-icon size="17"></trash-2-icon>
</div>
<div class="text-label">
{{ $t('context_menu.delete') }}
</div>
</li>
</ul>
<ul class="menu-option-group">
<li class="menu-option" @click="downloadItem" v-if="!isFolder">
<div class="icon">
<download-cloud-icon size="17"></download-cloud-icon>
</div>
<div class="text-label">
{{ $t('context_menu.download') }}
</div>
</li>
</ul>
<OptionGroup class="menu-option-group">
<Option @click.native="renameItem" :title="$t('context_menu.rename')" icon="rename"/>
<Option @click.native="moveItem" :title="$t('context_menu.move')" icon="move-item"/>
<Option @click.native="shareItem" v-if="$checkPermission('master')" :title="item.shared
? $t('context_menu.share_edit')
: $t('context_menu.share')" icon="share"/>
<Option @click.native="deleteItem" :title="$t('context_menu.delete')" icon="trash" class="menu-option"/>
</OptionGroup>
<OptionGroup>
<Option @click.native="downloadItem" v-if="!isFolder" :title="$t('context_menu.download')" icon="download"/>
</OptionGroup>
</div>
<!--ContextMenu for trash location-->
<div v-if="
$isThisLocation(['trash', 'trash-root']) && $checkPermission('master') && !showFromPreview
" id="menu-list" class="menu-options">
<ul class="menu-option-group">
<li class="menu-option" @click="$store.dispatch('restoreItem', item)" v-if="item">
<div class="icon">
<life-buoy-icon size="17"></life-buoy-icon>
</div>
<div class="text-label">
{{ $t('context_menu.restore') }}
</div>
</li>
<li class="menu-option" @click="deleteItem" v-if="item">
<div class="icon">
<trash-2-icon size="17"></trash-2-icon>
</div>
<div class="text-label">
{{ $t('context_menu.delete') }}
</div>
</li>
<li class="menu-option" @click="$store.dispatch('emptyTrash')">
<div class="icon">
<trash-icon size="17"></trash-icon>
</div>
<div class="text-label">
{{ $t('context_menu.empty_trash') }}
</div>
</li>
</ul>
<ul class="menu-option-group" v-if="item">
<li class="menu-option" @click="ItemDetail">
<div class="icon">
<eye-icon size="17"></eye-icon>
</div>
<div class="text-label">
{{ $t('context_menu.detail') }}
</div>
</li>
<li class="menu-option" @click="downloadItem" v-if="!isFolder">
<div class="icon">
<download-cloud-icon size="17"></download-cloud-icon>
</div>
<div class="text-label">
{{ $t('context_menu.download') }}
</div>
</li>
</ul>
<!-- Trash location-->
<div v-if="$isThisLocation(['trash', 'trash-root']) && $checkPermission('master') && !showFromPreview" id="menu-list" class="menu-options">
<!-- Single options -->
<OptionGroup v-if="multiSelectContextMenu">
<Option @click.native="restoreItem" v-if="item" :title="$t('context_menu.restore')" icon="restore"/>
<Option @click.native="deleteItem" :title="$t('context_menu.delete')" icon="trash"/>
<Option @click.native="emptyTrash" :title="$t('context_menu.empty_trash')" icon="empty-trash"/>
</OptionGroup>
<OptionGroup v-if="item && multiSelectContextMenu">
<Option @click.native="ItemDetail" :title="$t('context_menu.detail')" icon="detail"/>
<Option @click.native="downloadItem" v-if="!isFolder" :title="$t('context_menu.download')" icon="download"/>
</OptionGroup>
<!-- Multi options -->
<OptionGroup v-if="!multiSelectContextMenu">
<Option @click.native="deleteItem" :title="$t('context_menu.delete')" icon="trash"/>
<Option @click.native="emptyTrash" :title="$t('context_menu.empty_trash')" icon="empty-trash"/>
</OptionGroup>
<OptionGroup v-if="item && !multiSelectContextMenu && !hasFolder">
<Option @click.native="downloadItem" :title="$t('context_menu.download')" icon="download"/>
</OptionGroup>
</div>
<!--ContextMenu for Base location with MASTER permission-->
<!-- Shared location with MASTER permission-->
<div v-if="$isThisLocation(['shared']) && $checkPermission('master') && !showFromPreview" id="menu-list" class="menu-options">
<ul class="menu-option-group" v-if="item && isFolder">
<li class="menu-option" @click="addToFavourites">
<div class="icon">
<star-icon size="17"></star-icon>
</div>
<div class="text-label">
{{
isInFavourites
<!-- Single options -->
<OptionGroup class="menu-option-group" v-if="item && isFolder && multiSelectContextMenu">
<Option @click.native="addToFavourites" :title=" isInFavourites
? $t('context_menu.remove_from_favourites')
: $t('context_menu.add_to_favourites')" icon="favourites"/>
</OptionGroup>
<OptionGroup v-if="item && multiSelectContextMenu">
<Option @click.native="renameItem" :title="$t('context_menu.rename')" icon="rename"/>
<Option @click.native="shareItem" :title=" item.shared ? $t('context_menu.share_edit'): $t('context_menu.share')" icon="share"/>
<Option @click.native="deleteItem" :title="$t('context_menu.delete')" icon="trash"/>
</OptionGroup>
<OptionGroup v-if="item && multiSelectContextMenu">
<Option @click.native="ItemDetail" :title="$t('context_menu.detail')" icon="detail"/>
<Option @click.native="downloadItem" v-if="!isFolder" :title="$t('context_menu.download')" icon="download"/>
</OptionGroup>
<!-- Multi options -->
<OptionGroup class="menu-option-group" v-if="item && !hasFile && !multiSelectContextMenu">
<Option @click.native="addToFavourites" :title=" isInFavourites
? $t('context_menu.remove_from_favourites')
: $t('context_menu.add_to_favourites')
}}
</div>
</li>
</ul>
<ul class="menu-option-group" v-if="item">
<li class="menu-option" @click="shareItem">
<div class="icon">
<link-icon size="17"></link-icon>
</div>
<div class="text-label">
{{
item.shared
? $t('context_menu.share_edit')
: $t('context_menu.share')
}}
</div>
</li>
<li class="menu-option" @click="deleteItem">
<div class="icon">
<trash-2-icon size="17"></trash-2-icon>
</div>
<div class="text-label">
{{ $t('context_menu.delete') }}
</div>
</li>
</ul>
<ul class="menu-option-group" v-if="item">
<li class="menu-option" @click="ItemDetail" v-if="item">
<div class="icon">
<eye-icon size="17"></eye-icon>
</div>
<div class="text-label">
{{ $t('context_menu.detail') }}
</div>
</li>
<li class="menu-option" @click="downloadItem" v-if="!isFolder">
<div class="icon">
<download-cloud-icon size="17"></download-cloud-icon>
</div>
<div class="text-label">
{{ $t('context_menu.download') }}
</div>
</li>
</ul>
: $t('context_menu.add_to_favourites')" icon="favourites"/>
</OptionGroup>
<OptionGroup v-if="item && !multiSelectContextMenu">
<Option @click.native="shareCancel" :title="$t('context_menu.share_cancel')" icon="share"/>
<Option @click.native="deleteItem" :title="$t('context_menu.delete')" icon="trash"/>
</OptionGroup>
<OptionGroup v-if="item && !multiSelectContextMenu && !hasFolder">
<Option @click.native="downloadItem" :title="$t('context_menu.download')" icon="download"/>
</OptionGroup>
</div>
<!--ContextMenu for Base location with MASTER permission-->
<div v-if="
$isThisLocation(['base', 'participant_uploads', 'latest']) &&
$checkPermission('master') && !showFromPreview
" id="menu-list" class="menu-options">
<ul class="menu-option-group" v-if="!$isThisLocation(['participant_uploads', 'latest'])">
<li class="menu-option" @click="addToFavourites" v-if="item && isFolder">
<div class="icon">
<star-icon size="17"></star-icon>
</div>
<div class="text-label">
{{
isInFavourites
? $t('context_menu.remove_from_favourites')
: $t('context_menu.add_to_favourites')
}}
</div>
</li>
<li class="menu-option" @click="createFolder">
<div class="icon">
<folder-plus-icon size="17"></folder-plus-icon>
</div>
<div class="text-label">
{{ $t('context_menu.create_folder') }}
</div>
</li>
</ul>
<ul class="menu-option-group" v-if="item">
<li class="menu-option" @click="moveItem">
<div class="icon">
<corner-down-right-icon size="17"></corner-down-right-icon>
</div>
<div class="text-label">
{{ $t('context_menu.move') }}
</div>
</li>
<li class="menu-option" @click="shareItem">
<div class="icon">
<link-icon size="17"></link-icon>
</div>
<div class="text-label">
{{
item.shared
? $t('context_menu.share_edit')
: $t('context_menu.share')
}}
</div>
</li>
<li class="menu-option" @click="deleteItem">
<div class="icon">
<trash-2-icon size="17"></trash-2-icon>
</div>
<div class="text-label">
{{ $t('context_menu.delete') }}
</div>
</li>
</ul>
<ul class="menu-option-group" v-if="item">
<li class="menu-option" @click="ItemDetail">
<div class="icon">
<eye-icon size="17"></eye-icon>
</div>
<div class="text-label">
{{ $t('context_menu.detail') }}
</div>
</li>
<li class="menu-option" @click="downloadItem" v-if="!isFolder">
<div class="icon">
<download-cloud-icon size="17"></download-cloud-icon>
</div>
<div class="text-label">
{{ $t('context_menu.download') }}
</div>
</li>
</ul>
<!-- Base location with MASTER permission-->
<div v-if="$isThisLocation(['base', 'participant_uploads', 'latest']) && $checkPermission('master') && !showFromPreview" id="menu-list" class="menu-options">
<!-- Single options -->
<OptionGroup v-if="!$isThisLocation(['participant_uploads', 'latest']) && multiSelectContextMenu">
<Option @click.native="addToFavourites" v-if="item && isFolder " :title="isInFavourites
? $t('context_menu.remove_from_favourites')
: $t('context_menu.add_to_favourites')" icon="favourites"/>
<Option @click.native="createFolder" :title="$t('context_menu.create_folder')" icon="create-folder"/>
</OptionGroup>
<OptionGroup v-if="item && multiSelectContextMenu">
<Option @click.native="renameItem" :title="$t('context_menu.rename')" icon="rename"/>
<Option @click.native="moveItem" v-if="!$isThisLocation(['latest'])" :title="$t('context_menu.move')" icon="move-item"/>
<Option @click.native="shareItem" :title="item.shared
? $t('context_menu.share_edit')
: $t('context_menu.share')" icon="share"/>
<Option @click.native="deleteItem" :title="$t('context_menu.delete')" icon="trash"/>
</OptionGroup>
<OptionGroup v-if="item && multiSelectContextMenu ">
<Option @click.native="ItemDetail" :title="$t('context_menu.detail')" icon="detail"/>
<Option @click.native="downloadItem" v-if="!isFolder" :title="$t('context_menu.download')" icon="download"/>
</OptionGroup>
<!-- Multi options -->
<OptionGroup v-if="!$isThisLocation(['participant_uploads', 'latest']) && !multiSelectContextMenu">
<Option @click.native="addToFavourites" v-if="item && !hasFile" :title=" isInFavourites
? $t('context_menu.remove_from_favourites')
: $t('context_menu.add_to_favourites')" icon="favourites"/>
<Option @click.native="createFolder" :title="$t('context_menu.create_folder')" icon="create-folder"/>
</OptionGroup>
<OptionGroup v-if="item && !multiSelectContextMenu">
<Option @click.native="moveItem" v-if="!$isThisLocation(['latest'])" :title="$t('context_menu.move')" icon="move-item"/>
<Option @click.native="deleteItem" :title="$t('context_menu.delete')" icon="trash"/>
</OptionGroup>
<OptionGroup v-if="item && !multiSelectContextMenu && !hasFolder">
<Option @click.native="downloadItem" :title="$t('context_menu.download')" icon="download"/>
</OptionGroup>
</div>
<!--ContextMenu for Base location with EDITOR permission-->
<div v-if="$isThisLocation(['base', 'public']) && $checkPermission('editor') && !showFromPreview" id="menu-list" class="menu-options">
<ul class="menu-option-group">
<li class="menu-option" @click="createFolder">
<div class="icon">
<folder-plus-icon size="17"></folder-plus-icon>
</div>
<div class="text-label">
{{ $t('context_menu.create_folder') }}
</div>
</li>
</ul>
<ul class="menu-option-group" v-if="item">
<li class="menu-option" @click="moveItem">
<div class="icon">
<corner-down-right-icon size="17"></corner-down-right-icon>
</div>
<div class="text-label">
{{ $t('context_menu.move') }}
</div>
</li>
<li class="menu-option" @click="deleteItem">
<div class="icon">
<trash-2-icon size="17"></trash-2-icon>
</div>
<div class="text-label">
{{ $t('context_menu.delete') }}
</div>
</li>
</ul>
<ul class="menu-option-group" v-if="item">
<li class="menu-option" @click="ItemDetail">
<div class="icon">
<eye-icon size="17"></eye-icon>
</div>
<div class="text-label">
{{ $t('context_menu.detail') }}
</div>
</li>
<li class="menu-option" @click="downloadItem" v-if="!isFolder">
<div class="icon">
<download-cloud-icon size="17"></download-cloud-icon>
</div>
<div class="text-label">
{{ $t('context_menu.download') }}
</div>
</li>
</ul>
<!-- Base & Public location with EDITOR permission-->
<div v-if="$isThisLocation(['base', 'public']) && $checkPermission('editor') && !showFromPreview " id="menu-list" class="menu-options">
<!-- Single options -->
<OptionGroup v-if="multiSelectContextMenu">
<Option @click.native="createFolder" :title="$t('context_menu.create_folder')" icon="create-folder"/>
</OptionGroup>
<OptionGroup v-if="item && multiSelectContextMenu">
<Option @click.native="renameItem" :title=" $t('context_menu.rename')" icon="rename"/>
<Option @click.native="moveItem" :title="$t('context_menu.move')" icon="move-item"/>
<Option @click.native="deleteItem" :title="$t('context_menu.delete')" icon="trash"/>
</OptionGroup>
<OptionGroup v-if="item && multiSelectContextMenu">
<Option @click.native="ItemDetail" :title="$t('context_menu.detail')" icon="detail"/>
<Option @click.native="downloadItem" v-if="!isFolder" :title="$t('context_menu.download')" icon="download"/>
</OptionGroup>
<!-- Multi options -->
<OptionGroup v-if="!multiSelectContextMenu">
<Option @click.native="createFolder" :title="$t('context_menu.create_folder')" icon="create-folder"/>
</OptionGroup>
<OptionGroup v-if="item && !multiSelectContextMenu">
<Option @click.native="moveItem" :title="$t('context_menu.move')" icon="move-item"/>
<Option @click.native="deleteItem" :title="$t('context_menu.delete')" icon="trash"/>
</OptionGroup>
<OptionGroup v-if="item && !multiSelectContextMenu && !hasFolder">
<Option @click.native="downloadItem" :title="$t('context_menu.download')" icon="download"/>
</OptionGroup>
</div>
<!--ContextMenu for Base location with VISITOR permission-->
<div v-if="
$isThisLocation(['base', 'public']) && $checkPermission('visitor') && !showFromPreview
" id="menu-list" class="menu-options">
<ul class="menu-option-group" v-if="item">
<li class="menu-option" @click="ItemDetail">
<div class="icon">
<eye-icon size="17"></eye-icon>
</div>
<div class="text-label">
{{ $t('context_menu.detail') }}
</div>
</li>
<li class="menu-option" @click="downloadItem" v-if="!isFolder">
<div class="icon">
<download-cloud-icon size="17"></download-cloud-icon>
</div>
<div class="text-label">
{{ $t('context_menu.download') }}
</div>
</li>
</ul>
<!-- Base & Public location with VISITOR permission-->
<div v-if="$isThisLocation(['base', 'public']) && $checkPermission('visitor') && !showFromPreview" id="menu-list" class="menu-options">
<!-- Single options -->
<OptionGroup v-if="item && multiSelectContextMenu">
<Option @click.native="ItemDetail" :title="$t('context_menu.detail')" icon="detail"/>
<Option @click.native="downloadItem" v-if="!isFolder" :title="$t('context_menu.download')" icon="download"/>
</OptionGroup>
<!-- Multi options -->
<OptionGroup v-if="!multiSelectContextMenu && item ">
<Option @click.native="downloadItem" v-if="!hasFolder" :title="$t('context_menu.download')" icon="download"/>
<Option v-if="hasFolder" :title="$t('context_menu.no_options')" icon="no-options" class="no-options"/>
</OptionGroup>
</div>
</div>
</template>
<script>
import {
CornerDownRightIcon,
DownloadCloudIcon,
FolderPlusIcon,
LifeBuoyIcon,
Trash2Icon,
Edit2Icon,
TrashIcon,
StarIcon,
LinkIcon,
EyeIcon
} from 'vue-feather-icons'
import OptionGroup from '@/components/FilesView/OptionGroup'
import Option from '@/components/FilesView/Option'
import { mapGetters } from 'vuex'
import { events } from '@/bus'
export default {
name: 'ContextMenu',
components: {
CornerDownRightIcon,
DownloadCloudIcon,
FolderPlusIcon,
LifeBuoyIcon,
Trash2Icon,
Edit2Icon,
TrashIcon,
LinkIcon,
StarIcon,
EyeIcon
OptionGroup,
Option
},
computed: {
...mapGetters(['user']),
...mapGetters(['user', 'fileInfoDetail']),
hasFolder() {
// Check if selected items includes some folder
if (this.fileInfoDetail.find(item => item.type === 'folder'))
return true
},
hasFile() {
// Check if selected items includes some files
if (this.fileInfoDetail.find(item => item.type !== 'folder'))
return true
},
multiSelectContextMenu() {
// If is context Menu open on multi selected items open just options for the multi selected items
if (this.fileInfoDetail.length > 1 && this.fileInfoDetail.includes(this.item))
return false
// If is context Menu open for the non selected item open options for the single item
if (this.fileInfoDetail.length < 2 || !this.fileInfoDetail.includes(this.item))
return true
},
favourites() {
return this.user.relationships.favourites.data.attributes.folders
},
@@ -369,34 +250,27 @@ export default {
},
methods: {
emptyTrash() {
this.$store.dispatch('emptyTrash')
},
restoreItem() {
this.$store.dispatch('restoreItem', this.item)
},
shareCancel() {
this.$store.dispatch('shareCancel')
},
renameItem() {
let itemName = prompt(this.$t('popup_rename.title'), this.item.name)
if (itemName && itemName !== '') {
let item = {
unique_id: this.item.unique_id,
type: this.item.type,
name: itemName
}
this.$store.dispatch('renameItem', item)
// Change item name if is mobile device or prompted
if (this.$isMobile()) {
events.$emit('change:name', item)
}
}
events.$emit('popup:open', { name: 'rename-item', item: this.item })
},
moveItem() {
// Open move item popup
events.$emit('popup:open', { name: 'move', item: this.item })
events.$emit('popup:open', { name: 'move', item: [this.item] })
},
shareItem() {
if (this.item.shared) {
// Open share item popup
// Open edit share popup
events.$emit('popup:open', { name: 'share-edit', item: this.item })
} else {
// Open share item popup
// Open create share popup
events.$emit('popup:open', { name: 'share-create', item: this.item })
}
},
@@ -404,19 +278,27 @@ export default {
// Check if folder is in favourites and then add/remove from favourites
if (
this.favourites &&
!this.favourites.find((el) => el.unique_id == this.item.unique_id)
!this.favourites.find(el => el.unique_id == this.item.unique_id)
) {
this.$store.dispatch('addToFavourites', this.item)
// Add to favourite folder that is not selected
if (!this.fileInfoDetail.includes(this.item)) {
this.$store.dispatch('addToFavourites', this.item)
}
// Add to favourites all selected folders
if (this.fileInfoDetail.includes(this.item)) {
this.$store.dispatch('addToFavourites', null)
}
} else {
this.$store.dispatch('removeFromFavourites', this.item)
}
},
downloadItem() {
// Download file
this.$downloadFile(
this.item.file_url,
this.item.name + '.' + this.item.mimetype
)
if (this.fileInfoDetail.length > 1)
this.$store.dispatch('downloadFiles')
else {
this.$downloadFile(this.item.file_url, this.item.name + '.' + this.item.mimetype)
}
},
ItemDetail() {
// Dispatch load file info detail
@@ -426,12 +308,17 @@ export default {
this.$store.dispatch('fileInfoToggle', true)
},
deleteItem() {
// Dispatch remove item
this.$store.dispatch('deleteItem', this.item)
// If is context menu open on non selected item delete this single item
if (!this.fileInfoDetail.includes(this.item)) {
this.$store.dispatch('deleteItem', this.item)
}
// If is context menu open to multi selected items dele this selected items
if (this.fileInfoDetail.includes(this.item)) {
this.$store.dispatch('deleteItem')
}
},
createFolder() {
// Create folder
this.$createFolder(this.$t('popup_create_folder.folder_default_name'))
this.$store.dispatch('createFolder', this.$t('popup_create_folder.folder_default_name'))
},
closeAndResetContextMenu() {
// Close context menu
@@ -487,7 +374,6 @@ export default {
this.positionY = container.offsetTop + 51
}
}
},
watch: {
item(newValue, oldValue) {
@@ -530,7 +416,7 @@ export default {
setTimeout(() => this.showContextMenu(event, item), 10)
})
events.$on('contextMenu:hide', () => this.closeAndResetContextMenu())
events.$on('unClick', () => this.closeAndResetContextMenu())
events.$on('folder:actions', (folder) => {
// Store item
@@ -547,25 +433,27 @@ export default {
@import "@assets/vue-file-manager/_variables";
@import "@assets/vue-file-manager/_mixins";
.no-options {
/deep/ .text-label {
color: $text-muted !important;
}
/deep/ &:hover {
background: transparent;
}
/deep/ path,
/deep/ line,
/deep/ circle {
stroke: $text-muted !important;
}
}
.filePreviewFixed {
position: fixed !important;
display: flex;
}
.menu-option {
display: flex;
align-items: center;
.icon {
margin-right: 20px;
line-height: 0;
}
.text-label {
@include font-size(16);
}
}
.contextmenu {
min-width: 250px;
position: absolute;
@@ -585,65 +473,25 @@ export default {
width: 100%;
margin: 0;
padding: 0;
.menu-option-group {
padding: 5px 0;
border-bottom: 1px solid $light_mode_border;
&:first-child {
padding-top: 0;
}
&:last-child {
padding-bottom: 0;
border-bottom: none;
}
}
.menu-option {
white-space: nowrap;
font-weight: 700;
@include font-size(14);
padding: 15px 20px;
cursor: pointer;
width: 100%;
color: $text;
&:hover {
background: $light_background;
.text-label {
color: $theme;
}
path,
line,
polyline,
rect,
circle,
polygon {
stroke: $theme;
}
}
}
}
@media (prefers-color-scheme: dark) {
.contextmenu {
background: $dark_mode_foreground;
}
.no-options {
/deep/ .text-label {
color: $dark_mode_text_secondary !important;
}
.menu-options {
.menu-option-group {
border-color: $dark_mode_border_color;
}
/deep/ &:hover {
background: transparent;
}
.menu-option {
color: $dark_mode_text_primary;
&:hover {
background: rgba($theme, 0.1);
}
}
/deep/ path,
/deep/ line,
/deep/ circle {
stroke: $dark_mode_text_secondary !important;
}
}
}
@@ -0,0 +1,85 @@
<template>
<div v-if="isVisible" class="sorting-preview">
<SortingAndPreviewMenu />
</div>
</template>
<script>
import SortingAndPreviewMenu from '@/components/FilesView/SortingAndPreviewMenu'
import { events } from '@/bus'
export default {
name: 'DesktopSortingAndPreview',
components: {SortingAndPreviewMenu},
data () {
return {
isVisible: false
}
},
mounted () {
events.$on('sortingAndPreview', (state) => {
this.isVisible = state
})
events.$on('unClick', () => {
this.isVisible = false
})
}
}
</script>
<style scoped lang="scss">
@import "@assets/vue-file-manager/_variables";
@import "@assets/vue-file-manager/_mixins";
.sorting-preview {
min-width: 250px;
position: absolute;
z-index: 99;
box-shadow: $shadow;
background: white;
border-radius: 8px;
overflow: hidden;
right: 66px;
top: 63px;
&.showed {
display: block;
}
/deep/.menu-option {
&:hover {
background: $light_background;
.text-label {
color: $theme;
}
path,
/deep/ line,
/deep/ polyline,
rect,
circle,
polygon {
stroke: $theme !important;
}
}
}
}
@media (prefers-color-scheme: dark) {
.sorting-preview {
background: $dark_mode_foreground;
/deep/ .menu-option {
&:hover {
background: rgba($theme, 0.1);
}
}
}
}
</style>
@@ -1,411 +1,434 @@
<template>
<div id="desktop-toolbar">
<div class="toolbar-wrapper">
<!-- Go back-->
<div class="toolbar-go-back" v-if="homeDirectory">
<div @click="goBack" class="go-back-button">
<chevron-left-icon
size="17"
:class="{ 'is-active': browseHistory.length > 1 }"
class="icon-back"
></chevron-left-icon>
<div id="desktop-toolbar">
<div class="toolbar-wrapper">
<!-- Go back-->
<div class="toolbar-go-back" v-if="homeDirectory">
<div @click="goBack" class="go-back-button">
<chevron-left-icon size="17" :class="{ 'is-active': browseHistory.length > 1 }" class="icon-back"></chevron-left-icon>
<span class="back-directory-title">
<span class="back-directory-title">
{{ directoryName }}
</span>
<span
@click.stop="folderActions"
v-if="
<span @click.stop="folderActions" v-if="
browseHistory.length > 1 && $isThisLocation(['base', 'public'])
"
class="folder-options"
id="folder-actions"
>
<more-horizontal-icon
size="14"
class="icon-more"
></more-horizontal-icon>
" class="folder-options" id="folder-actions">
<more-horizontal-icon size="14" class="icon-more"></more-horizontal-icon>
</span>
</div>
</div>
</div>
</div>
<!-- Tools-->
<div class="toolbar-tools">
<!--Search bar-->
<div class="toolbar-button-wrapper">
<SearchBar />
</div>
<!-- Tools-->
<div class="toolbar-tools">
<!--Search bar-->
<div class="toolbar-button-wrapper">
<SearchBar/>
</div>
<!--Files controlls-->
<div
class="toolbar-button-wrapper"
v-if="$checkPermission(['master', 'editor'])"
>
<ToolbarButtonUpload
:class="{ 'is-inactive': canUploadInView || !hasCapacity }"
:action="$t('actions.upload')"
/>
<ToolbarButton
:class="{ 'is-inactive': canCreateFolderInView }"
@click.native="createFolder"
source="folder-plus"
:action="$t('actions.create_folder')"
/>
</div>
<!--Files controlls-->
<div class="toolbar-button-wrapper" v-if="$checkPermission(['master', 'editor'])">
<ToolbarButtonUpload :class="{ 'is-inactive': canUploadInView || !hasCapacity }" :action="$t('actions.upload')"/>
<ToolbarButton :class="{ 'is-inactive': canCreateFolderInView }" @click.native="createFolder" source="folder-plus" :action="$t('actions.create_folder')"/>
</div>
<div
class="toolbar-button-wrapper"
v-if="$checkPermission(['master', 'editor'])"
>
<ToolbarButton
source="move"
:class="{ 'is-inactive': canMoveInView }"
:action="$t('actions.move')"
@click.native="moveItem"
/>
<ToolbarButton
v-if="!$isThisLocation(['public'])"
source="share"
:class="{ 'is-inactive': canShareInView }"
:action="$t('actions.share')"
@click.native="shareItem"
/>
<ToolbarButton
source="trash"
:class="{ 'is-inactive': canDeleteInView }"
:action="$t('actions.delete')"
@click.native="deleteItem"
/>
</div>
<div class="toolbar-button-wrapper" v-if="$checkPermission(['master', 'editor'])">
<ToolbarButton source="move" :class="{ 'is-inactive': canMoveInView }" :action="$t('actions.move')" @click.native="moveItem"/>
<ToolbarButton v-if="!$isThisLocation(['public'])" source="share" :class="{ 'is-inactive': canShareInView }" :action="$t('actions.share')" @click.native="shareItem"/>
<ToolbarButton source="trash" :class="{ 'is-inactive': canDeleteInView }" :action="$t('actions.delete')" @click.native="deleteItem"/>
</div>
<!--View options-->
<div class="toolbar-button-wrapper">
<ToolbarButton
:source="preview"
:action="$t('actions.preview')"
@click.native="$store.dispatch('changePreviewType')"
/>
<ToolbarButton
:class="{ active: fileInfoVisible }"
@click.native="$store.dispatch('fileInfoToggle')"
source="info"
/>
<!--View options-->
<div class="toolbar-button-wrapper">
<ToolbarButton source="preview-sorting" class="preview-sorting" :action="$t('actions.sorting_view')" :class="{ active: sortingAndPreview }" @click.stop.native="sortingAndPreview = !sortingAndPreview"/>
<ToolbarButton :action="$t('actions.info_panel')" :class="{ active: fileInfoVisible }" @click.native="$store.dispatch('fileInfoToggle')" source="info"/>
</div>
</div>
</div>
</div>
<UploadProgress/>
</div>
<UploadProgress />
</div>
</template>
<script>
import ToolbarButtonUpload from "@/components/FilesView/ToolbarButtonUpload";
import { ChevronLeftIcon, MoreHorizontalIcon } from "vue-feather-icons";
import UploadProgress from "@/components/FilesView/UploadProgress";
import ToolbarButton from "@/components/FilesView/ToolbarButton";
import SearchBar from "@/components/FilesView/SearchBar";
import { mapGetters } from "vuex";
import { events } from "@/bus";
import { last } from "lodash";
import ToolbarButtonUpload from '@/components/FilesView/ToolbarButtonUpload'
import { ChevronLeftIcon, MoreHorizontalIcon } from 'vue-feather-icons'
import UploadProgress from '@/components/FilesView/UploadProgress'
import ToolbarButton from '@/components/FilesView/ToolbarButton'
import SearchBar from '@/components/FilesView/SearchBar'
import { mapGetters } from 'vuex'
import { events } from '@/bus'
import { last } from 'lodash'
export default {
name: "ToolBar",
components: {
ToolbarButtonUpload,
MoreHorizontalIcon,
ChevronLeftIcon,
UploadProgress,
ToolbarButton,
SearchBar,
},
computed: {
...mapGetters([
"FilePreviewType",
"fileInfoVisible",
"fileInfoDetail",
"currentFolder",
"browseHistory",
"homeDirectory",
]),
hasCapacity() {
// Check if set storage limitation
if (!this.$store.getters.config.storageLimit) return true;
name: 'ToolBar',
components: {
ToolbarButtonUpload,
MoreHorizontalIcon,
ChevronLeftIcon,
UploadProgress,
ToolbarButton,
SearchBar
},
computed: {
...mapGetters([
'FilePreviewType',
'fileInfoVisible',
'fileInfoDetail',
'currentFolder',
'browseHistory',
'homeDirectory'
]),
hasCapacity() {
// Check if set storage limitation
if (!this.$store.getters.config.storageLimit) return true
// Check if is loaded user
if (!this.$store.getters.user) return true;
// Check if is loaded user
if (!this.$store.getters.user) return true
// Check if user has storage
return (
this.$store.getters.user.relationships.storage.data.attributes.used <=
100
);
},
directoryName() {
return this.currentFolder
? this.currentFolder.name
: this.homeDirectory.name;
},
preview() {
return this.FilePreviewType === "list" ? "th" : "th-list";
},
canCreateFolderInView() {
return !this.$isThisLocation(["base", "public"]);
},
canDeleteInView() {
return !this.$isThisLocation([
"trash",
"trash-root",
"base",
"participant_uploads",
"latest",
"shared",
"public",
]);
},
canUploadInView() {
return !this.$isThisLocation(["base", "public"]);
},
canMoveInView() {
return !this.$isThisLocation([
"base",
"participant_uploads",
"latest",
"shared",
"public",
]);
},
canShareInView() {
return !this.$isThisLocation([
"base",
"participant_uploads",
"latest",
"shared",
"public",
]);
},
},
methods: {
goBack() {
// Get previous folder
let previousFolder = last(this.browseHistory);
// Check if user has storage
return (
this.$store.getters.user.relationships.storage.data.attributes.used <=
100
)
},
directoryName() {
return this.currentFolder
? this.currentFolder.name
: this.homeDirectory.name
},
preview() {
return this.FilePreviewType === 'list' ? 'th' : 'th-list'
},
canCreateFolderInView() {
return !this.$isThisLocation(['base', 'public'])
},
canDeleteInView() {
return !this.$isThisLocation([
'trash',
'trash-root',
'base',
'participant_uploads',
'latest',
'shared',
'public'
])
},
canUploadInView() {
return !this.$isThisLocation(['base', 'public'])
},
canMoveInView() {
return !this.$isThisLocation([
'base',
'participant_uploads',
'latest',
'shared',
'public'
])
},
canShareInView() {
let locations = [
'base',
'participant_uploads',
'latest',
'shared',
'public'
]
if (!previousFolder) return;
if (previousFolder.location === "trash-root") {
this.$store.dispatch("getTrash");
} else if (previousFolder.location === "shared") {
this.$store.dispatch("getShared");
} else {
if (this.$isThisLocation("public")) {
this.$store.dispatch("browseShared", [
{ folder: previousFolder, back: true, init: false },
]);
} else {
this.$store.dispatch("getFolder", [
{ folder: previousFolder, back: true, init: false },
]);
return !this.$isThisLocation(locations) || this.fileInfoDetail.length > 1
}
}
},
folderActions() {
events.$emit("folder:actions", this.currentFolder);
},
deleteItem() {
events.$emit("items:delete");
},
createFolder() {
this.$createFolder();
},
moveItem() {
events.$emit("popup:open", { name: "move", item: this.fileInfoDetail });
},
shareItem() {
if (this.fileInfoDetail) {
//ADD BY M
if (this.fileInfoDetail.shared) {
events.$emit("popup:open", {
name: "share-edit",
item: this.fileInfoDetail,
});
} else {
events.$emit("popup:open", {
name: "share-create",
item: this.fileInfoDetail,
});
data() {
return {
sortingAndPreview: false
}
}
},
},
};
watch: {
sortingAndPreview() {
if (this.sortingAndPreview) {
events.$emit('sortingAndPreview', true)
}
if (!this.sortingAndPreview) {
events.$emit('unClick')
}
}
},
methods: {
goBack() {
// Get previous folder
let previousFolder = last(this.browseHistory)
if (!previousFolder) return
if (previousFolder.location === 'trash-root') {
this.$store.dispatch('getTrash')
} else if (previousFolder.location === 'shared') {
this.$store.dispatch('getShared')
} else {
if (this.$isThisLocation('public')) {
this.$store.dispatch('browseShared', [
{ folder: previousFolder, back: true, init: false }
])
} else {
this.$store.dispatch('getFolder', [
{ folder: previousFolder, back: true, init: false }
])
}
}
},
folderActions() {
events.$emit('folder:actions', this.currentFolder)
},
deleteItem() {
if(this.fileInfoDetail.length > 0)
this.$store.dispatch('deleteItem')
},
createFolder() {
this.$store.dispatch('createFolder', this.$t('popup_create_folder.folder_default_name'))
},
moveItem() {
if(this.fileInfoDetail.length > 0)
events.$emit('popup:open', { name: 'move', item: this.fileInfoDetail })
},
shareItem() {
if (this.fileInfoDetail[0]) {
//ADD BY M
if (this.fileInfoDetail[0].shared) {
events.$emit('popup:open', {
name: 'share-edit',
item: this.fileInfoDetail[0]
})
} else {
events.$emit('popup:open', {
name: 'share-create',
item: this.fileInfoDetail[0]
})
}
}
}
},
mounted() {
// events.$on('sortingAndPreview', (state) => {
// this.sortingAndPreview = state
// })
events.$on('unClick', () => {
this.sortingAndPreview = false
})
}
}
</script>
<style scoped lang="scss">
@import "@assets/vue-file-manager/_variables";
@import "@assets/vue-file-manager/_mixins";
.preview-sorting {
/deep/ .label {
color: $text !important;
}
/deep/ .preview-sorting {
path, line, polyline, rect, circle {
stroke: $text !important;
}
}
&:hover {
/deep/ .preview-sorting {
path, line, polyline, rect, circle {
stroke: $theme !important;
}
}
}
}
.toolbar-wrapper {
padding-top: 10px;
padding-bottom: 10px;
display: flex;
position: relative;
z-index: 2;
padding-top: 10px;
padding-bottom: 10px;
display: flex;
position: relative;
z-index: 2;
> div {
flex-grow: 1;
align-self: center;
white-space: nowrap;
}
> div {
flex-grow: 1;
align-self: center;
white-space: nowrap;
}
}
.directory-name {
vertical-align: middle;
@include font-size(17);
color: $text;
font-weight: 700;
max-width: 220px;
overflow: hidden;
text-overflow: ellipsis;
display: inline-block;
}
.icon-back {
vertical-align: middle;
cursor: pointer;
margin-right: 6px;
opacity: 0.15;
pointer-events: none;
@include transition(150ms);
&.is-active {
opacity: 1;
pointer-events: initial;
}
}
.toolbar-go-back {
cursor: pointer;
.folder-options {
vertical-align: middle;
margin-left: 6px;
padding: 1px 4px;
line-height: 0;
border-radius: 3px;
@include transition(150ms);
svg circle {
@include transition(150ms);
}
&:hover {
background: $light_background;
svg circle {
stroke: $theme;
}
}
.icon-more {
vertical-align: middle;
}
}
.back-directory-title {
@include font-size(15);
line-height: 1;
@include font-size(17);
color: $text;
font-weight: 700;
max-width: 220px;
overflow: hidden;
text-overflow: ellipsis;
display: inline-block;
}
.icon-back {
vertical-align: middle;
color: $text;
}
cursor: pointer;
margin-right: 6px;
opacity: 0.15;
pointer-events: none;
@include transition(150ms);
&.is-active {
opacity: 1;
pointer-events: initial;
}
}
.toolbar-go-back {
cursor: pointer;
.folder-options {
vertical-align: middle;
margin-left: 6px;
padding: 1px 4px;
line-height: 0;
border-radius: 3px;
@include transition(150ms);
svg circle {
@include transition(150ms);
}
&:hover {
background: $light_background;
svg circle {
stroke: $theme;
}
}
.icon-more {
vertical-align: middle;
}
}
.back-directory-title {
@include font-size(15);
line-height: 1;
font-weight: 700;
overflow: hidden;
text-overflow: ellipsis;
display: inline-block;
vertical-align: middle;
color: $text;
}
}
.toolbar-position {
text-align: center;
text-align: center;
span {
@include font-size(17);
font-weight: 600;
}
span {
@include font-size(17);
font-weight: 600;
}
}
.toolbar-tools {
text-align: right;
text-align: right;
.toolbar-button-wrapper {
margin-left: 28px;
display: inline-block;
vertical-align: middle;
.toolbar-button-wrapper {
margin-left: 28px;
display: inline-block;
vertical-align: middle;
&:first-child {
margin-left: 0 !important;
}
}
.button {
margin-left: 5px;
&.active {
/deep/ svg {
line,
circle {
stroke: $theme;
&:first-child {
margin-left: 0 !important;
}
}
}
&.is-inactive {
opacity: 0.25;
pointer-events: none;
}
.button {
margin-left: 5px;
&:first-child {
margin-left: 0;
&.active {
/deep/ svg {
line,
circle,
rect {
stroke: $theme;
}
}
&.preview-sorting {
background: $light_background;
/deep/ .preview-sorting {
path, line, polyline, rect, circle {
stroke: $theme !important;
}
}
}
}
&.is-inactive {
opacity: 0.25;
pointer-events: none;
}
&:first-child {
margin-left: 0;
}
}
}
}
@media only screen and (max-width: 1024px) {
.toolbar-go-back .back-directory-title {
max-width: 120px;
}
.toolbar-tools {
.button {
margin-left: 0;
height: 40px;
width: 40px;
.toolbar-go-back .back-directory-title {
max-width: 120px;
}
.toolbar-button-wrapper {
margin-left: 25px;
.toolbar-tools {
.button {
margin-left: 0;
height: 40px;
width: 40px;
}
.toolbar-button-wrapper {
margin-left: 25px;
}
}
}
}
@media only screen and (max-width: 960px) {
#desktop-toolbar {
display: none;
}
#desktop-toolbar {
display: none;
}
}
@media (prefers-color-scheme: dark) {
.toolbar .directory-name {
color: $dark_mode_text_primary;
}
.toolbar-go-back {
.back-directory-title {
color: $dark_mode_text_primary;
.toolbar .directory-name {
color: $dark_mode_text_primary;
}
.folder-options {
&:hover {
background: $dark_mode_foreground;
}
.toolbar-go-back {
.back-directory-title {
color: $dark_mode_text_primary;
}
.folder-options {
&:hover {
background: $dark_mode_foreground;
}
}
}
.active {
&.preview-sorting {
background: $dark_mode_foreground !important;
}
}
.preview-sorting {
/deep/ .label {
color: $text !important;
}
/deep/ .preview-sorting {
path, line, polyline, rect, circle {
stroke: $dark_mode_text_primary !important;
}
}
}
}
}
</style>
@@ -0,0 +1,120 @@
<template>
<MultiSelected :title="title" :subtitle="subtitle" id="multi-select-ui" v-show="dragged" />
</template>
<script>
import MultiSelected from '@/components/FilesView/MultiSelected'
import {mapGetters} from 'vuex'
import {events} from '@/bus'
export default {
name:"DragUI",
components: {MultiSelected},
computed: {
...mapGetters(['fileInfoDetail']),
title(){
// Title for multiple selected items
if(this.fileInfoDetail.length > 1 && this.fileInfoDetail.includes(this.draggedItem)) {
return this.$t('file_detail.selected_multiple')
}
// Title for single item
if((this.fileInfoDetail.length < 2 || !this.fileInfoDetail.includes(this.draggedItem)) && this.draggedItem ) {
return this.draggedItem.name
}
},
subtitle(){
// Subtitle for multiple selected items
if(this.fileInfoDetail.length > 1 && this.fileInfoDetail.includes(this.draggedItem) ) {
return this.fileInfoDetail.length + ' ' + this.$tc('file_detail.items', this.fileInfoDetail.length)
}
if((this.fileInfoDetail.length < 2 || !this.fileInfoDetail.includes(this.draggedItem)) && this.draggedItem) {
// Subtitle for single folder
if(this.draggedItem.type === 'folder') {
return this.draggedItem.items == 0 ? this.$t('folder.empty') : this.$tc('folder.item_counts', this.draggedItem.items)
}
// Subtitle for single file
if(this.draggedItem !== 'folder' && this.draggedItem.mimetype){
return '.'+this.draggedItem.mimetype
}
}
},
},
data () {
return {
dragged: false,
draggedItem: undefined
}
},
mounted () {
// Hnadle Drag & Drop Ghost show
events.$on('dragstart', (data) => {
setTimeout(() => {
this.dragged = true
}, 50);
this.draggedItem = data
})
events.$on('drop', () => {
this.dragged = false
})
}
}
</script>
<style lang="scss" scoped>
@import '@assets/vue-file-manager/_variables';
@import '@assets/vue-file-manager/_mixins';
#multi-select-ui {
max-width: 300px;
min-width: 250px;
position: fixed;
z-index: 10;
pointer-events: none;
padding: 10px;
border-radius: 8px;
box-shadow: 0 7px 25px 1px rgba(0, 0, 0, 0.12);
background:white;
/deep/.text{
.title {
color: $text;
}
.count {
color: $text-muted;
}
}
/deep/.icon-wrapper {
.icon {
stroke: $theme;
}
}
}
@media (prefers-color-scheme: dark) {
#multi-select-ui {
background: $dark_mode_foreground;
/deep/.text {
.title {
color: $dark_mode_text_primary;
}
.count {
color: $dark_mode_text_secondary;
}
}
/deep/.icon-wrapper {
.icon {
stroke: $theme;
}
}
}
}
</style>
@@ -1,14 +1,16 @@
<template>
<div class="file-content" :class="{ 'is-offset': uploadingFilesCount, 'is-dragging': isDragging }"
<div class="file-content" id="file-content-id" :class="{ 'is-offset': uploadingFilesCount, 'is-dragging': isDragging }"
@dragover.prevent
@drop.stop.prevent="dropUpload($event)"
@dragover="dragEnter"
@dragleave="dragLeave"
@keydown.delete="deleteItems"
tabindex="-1"
>
<div
class="files-container"
ref="fileContainer"
:class="{'is-fileinfo-visible': fileInfoVisible && !$isMinimalScale() }"
:class="{'is-fileinfo-visible': fileInfoVisible && !$isMinimalScale() , 'mobile-multi-select' : mobileMultiSelect}"
@click.self="filesContainerClick"
>
<!--MobileToolbar-->
@@ -36,6 +38,7 @@
v-for="item in data"
:key="item.unique_id"
class="file-item"
:class="draggedItems.includes(item) ? 'dragged' : '' "
/>
</transition-group>
</div>
@@ -56,6 +59,7 @@
v-for="item in data"
:key="item.unique_id"
class="file-item"
:class="draggedItems.includes(item) ? 'dragged' : '' "
/>
</transition-group>
</div>
@@ -74,10 +78,15 @@
<!--File Info Panel-->
<div v-if="! $isMinimalScale()" class="file-info-container" :class="{ 'is-fileinfo-visible': fileInfoVisible }">
<!--File info panel-->
<FileInfoPanel v-if="fileInfoDetail"/>
<FileInfoPanel v-if="fileInfoDetail.length === 1"/>
<MultiSelected v-if="fileInfoDetail.length > 1"
:title="$t('file_detail.selected_multiple')"
:subtitle="this.fileInfoDetail.length + ' ' + $tc('file_detail.items', this.fileInfoDetail.length)"
/>
<!--If file info panel empty show message-->
<EmptyMessage v-if="!fileInfoDetail" :message="$t('messages.nothing_to_preview')" icon="eye-off"/>
<EmptyMessage v-if="fileInfoDetail.length === 0" :message="$t('messages.nothing_to_preview')" icon="eye-off"/>
</div>
</div>
</template>
@@ -85,6 +94,7 @@
<script>
import MobileToolbar from '@/components/FilesView/MobileToolbar'
import MobileActions from '@/components/FilesView/MobileActions'
import MultiSelected from '@/components/FilesView/MultiSelected'
import FileInfoPanel from '@/components/FilesView/FileInfoPanel'
import FileItemList from '@/components/FilesView/FileItemList'
import FileItemGrid from '@/components/FilesView/FileItemGrid'
@@ -99,6 +109,7 @@
components: {
MobileToolbar,
MobileActions,
MultiSelected,
FileInfoPanel,
FileItemList,
FileItemGrid,
@@ -126,14 +137,31 @@
isEmpty() {
return this.data.length == 0
},
draggedItems() {
//Set opacity for dragged items
if(!this.fileInfoDetail.includes(this.draggingId)){
return [this.draggingId]
}
if(this.fileInfoDetail.includes(this.draggingId)) {
return this.fileInfoDetail
}
}
},
data() {
return {
draggingId: undefined,
isDragging: false,
mobileMultiSelect: false,
}
},
methods: {
deleteItems() {
if(this.fileInfoDetail.length > 0 && this.$checkPermission('master') || this.$checkPermission('editor')) {
this.$store.dispatch('deleteItem')
}
},
dropUpload(event) {
// Upload external file
this.$uploadExternalFiles(event, this.currentFolder.unique_id)
@@ -147,6 +175,9 @@
this.isDragging = false
},
dragStart(data) {
let img = document.createElement('img')
img.src = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
event.dataTransfer.setDragImage(img, 0, 0)
events.$emit('dragstart', data)
@@ -156,12 +187,25 @@
dragFinish(data, event) {
if (event.dataTransfer.items.length == 0) {
// Prevent to drop on file or image
if (data.type !== 'folder' || this.draggingId === data) return
//Prevent move selected folder to folder if in beteewn selected folders
if(this.fileInfoDetail.find(item => item === data && this.fileInfoDetail.length > 1)) return
// Move folder to new parent
this.$store.dispatch('moveItem', [this.draggingId, data])
//Move item if is not included in selected items
if(!this.fileInfoDetail.includes(this.draggingId)){
this.$store.dispatch('moveItem', {to_item:data ,noSelectedItem:this.draggingId})
}
//Move selected items to folder
if(this.fileInfoDetail.length > 0 && this.fileInfoDetail.includes(this.draggingId)){
this.$store.dispatch('moveItem', {to_item:data ,noSelectedItem: null})
}
} else {
@@ -178,15 +222,27 @@
events.$emit('contextMenu:show', event, item)
},
filesContainerClick() {
// Deselect clicked item
events.$emit('fileItem:deselect')
// Hide context menu if is opened
events.$emit('contextMenu:hide')
// Deselect itms clicked by outside
this.$store.commit('CLEAR_FILEINFO_DETAIL')
}
},
created() {
events.$on('mobileSelecting:start' , () => {
this.mobileMultiSelect =true
})
events.$on('mobileSelecting:stop' , () => {
this.mobileMultiSelect = false
})
events.$on('drop', () => {
this.isDragging = false
setTimeout(() => {
this.draggingId = undefined
}, 10);
})
events.$on('fileItem:deselect', () =>
this.$store.commit('CLEAR_FILEINFO_DETAIL')
)
@@ -199,11 +255,6 @@
if (container) container.scrollTop = 0
})
// On items delete
events.$on('items:delete', () => {
this.$store.dispatch('deleteItem', this.fileInfoDetail)
})
}
}
</script>
@@ -212,6 +263,30 @@
@import '@assets/vue-file-manager/_variables';
@import '@assets/vue-file-manager/_mixins';
.file-list {
.dragged {
/deep/.is-dragenter {
border: 2px solid transparent;
}
}
}
.dragged {
opacity: 0.5;
}
#multi-selected {
position: fixed;
pointer-events: none;
z-index: 100;
}
.mobile-multi-select {
bottom: 50px !important;
top: 0px;
}
.button-upload {
display: block;
text-align: center;
@@ -1,5 +1,5 @@
<template>
<div class="file-info-content" v-if="fileInfoDetail">
<div class="file-info-content" v-if="fileInfoDetail.length === 1">
<div class="file-headline" spellcheck="false">
<FilePreview/>
@@ -14,37 +14,37 @@
</div>
</div>
<div class="file-info">
<span ref="name" class="name">{{ fileInfoDetail.name }}</span>
<span class="mimetype" v-if="fileInfoDetail.mimetype">.{{ fileInfoDetail.mimetype }}</span>
<span ref="name" class="name">{{ fileInfoDetail[0].name }}</span>
<span class="mimetype" v-if="fileInfoDetail[0].mimetype">.{{ fileInfoDetail[0].mimetype }}</span>
</div>
</div>
</div>
<!--Info list-->
<ListInfo>
<ListInfoItem v-if="fileInfoDetail.filesize"
<ListInfoItem v-if="fileInfoDetail[0].filesize"
:title="$t('file_detail.size')"
:content="fileInfoDetail.filesize">
:content="fileInfoDetail[0].filesize">
</ListInfoItem>
<ListInfoItem v-if="$checkPermission(['master']) && fileInfoDetail.user_scope !== 'master'"
<ListInfoItem v-if="$checkPermission(['master']) && fileInfoDetail[0].user_scope !== 'master'"
:title="$t('file_detail.author')"
:content="$t('file_detail.author_participant')">
</ListInfoItem>
<ListInfoItem
:title="$t('file_detail.created_at')"
:content="fileInfoDetail.created_at">
:content="fileInfoDetail[0].created_at">
</ListInfoItem>
<ListInfoItem v-if="$checkPermission(['master'])"
:title="$t('file_detail.where')">
<div class="action-button" @click="moveItem">
<span>{{ fileInfoDetail.parent ? fileInfoDetail.parent.name : $t('locations.home') }}</span>
<span>{{ fileInfoDetail[0].parent ? fileInfoDetail[0].parent.name : $t('locations.home') }}</span>
<edit-2-icon size="10" class="edit-icon"></edit-2-icon>
</div>
</ListInfoItem>
<ListInfoItem v-if="$checkPermission('master') && fileInfoDetail.shared"
<ListInfoItem v-if="$checkPermission('master') && fileInfoDetail[0].shared"
:title="$t('file_detail.shared')">
<div class="action-button" @click="shareItemOptions">
<span>{{ sharedInfo }}</span>
@@ -53,7 +53,7 @@
<div class="sharelink">
<lock-icon v-if="isLocked" @click="shareItemOptions" class="lock-icon" size="17"></lock-icon>
<unlock-icon v-if="! isLocked" @click="shareItemOptions" class="lock-icon" size="17"></unlock-icon>
<CopyInput class="copy-sharelink" size="small" :value="fileInfoDetail.shared.link"/>
<CopyInput class="copy-sharelink" size="small" :value="fileInfoDetail[0].shared.link"/>
</div>
</ListInfoItem>
@@ -93,21 +93,21 @@
computed: {
...mapGetters(['fileInfoDetail', 'permissionOptions']),
fileType() {
return this.fileInfoDetail.type
return this.fileInfoDetail[0].type
},
canShowMetaData() {
return this.fileInfoDetail.metadata && this.fileInfoDetail.metadata.ExifImageWidth
return this.fileInfoDetail[0].metadata && this.fileInfoDetail[0].metadata.ExifImageWidth
},
sharedInfo() {
// Get permission title
let title = this.permissionOptions.find(option => {
return option.value === this.fileInfoDetail.shared.permission
return option.value === this.fileInfoDetail[0].shared.permission
})
return title ? title.label : this.$t('shared.can_download')
},
sharedIcon() {
switch (this.fileInfoDetail.shared.permission) {
switch (this.fileInfoDetail[0].shared.permission) {
case 'editor':
return 'user-edit'
break
@@ -119,17 +119,17 @@
}
},
isLocked() {
return this.fileInfoDetail.shared.protected
return this.fileInfoDetail[0].shared.protected
}
},
methods: {
shareItemOptions() {
// Open share item popup
events.$emit('popup:open', {name: 'share-edit', item: this.fileInfoDetail})
events.$emit('popup:open', {name: 'share-edit', item: this.fileInfoDetail[0]})
},
moveItem() {
// Move item fire popup
events.$emit('popup:open', {name: 'move', item: this.fileInfoDetail})
events.$emit("popup:open", { name: "move", item: this.fileInfoDetail});
}
}
}
File diff suppressed because it is too large Load Diff
+527 -375
View File
@@ -1,222 +1,283 @@
<template>
<div class="file-wrapper" @click.stop="clickedItem" @dblclick="goToItem" spellcheck="false">
<!--List preview-->
<div
:draggable="canDrag"
@dragstart="$emit('dragstart')"
@drop="
$emit('drop')
area = false
"
@dragleave="dragLeave"
@dragover.prevent="dragEnter"
class="file-item"
:class="{ 'is-clicked': isClicked, 'is-dragenter': area }"
>
<!--Thumbnail for item-->
<div class="icon-item">
<!--If is file or image, then link item-->
<span v-if="isFile || (isImage && !data.thumbnail)" class="file-icon-text">
{{ data.mimetype | limitCharacters }}
</span>
<div class="file-wrapper" @click.stop="clickedItem" @dblclick="goToItem" spellcheck="false">
<!--List preview-->
<div :draggable="canDrag" @dragstart="$emit('dragstart')" @drop="
drop()
area = false" @dragleave="dragLeave" @dragover.prevent="dragEnter" class="file-item" :class="{'is-clicked' : isClicked , 'no-clicked' : !isClicked && this.$isMobile(), 'is-dragenter': area }">
<!-- MultiSelecting for the mobile version -->
<transition name="slide-from-left">
<div class="check-select" v-if="mobileMultiSelect">
<div class="select-box" :class="{'select-box-active' : isClicked } ">
<CheckIcon v-if="isClicked" class="icon" size="17"/>
</div>
</div>
</transition>
<!--Folder thumbnail-->
<FontAwesomeIcon v-if="isFile || (isImage && !data.thumbnail)" class="file-icon" icon="file" />
<!--Thumbnail for item-->
<div class="icon-item">
<!--If is file or image, then link item-->
<span v-if="isFile" class="file-icon-text">
{{ data.mimetype | limitCharacters }}
</span>
<!--Image thumbnail-->
<img loading="lazy" v-if="isImage && data.thumbnail" class="image" :src="data.thumbnail" :alt="data.name" />
<!--Folder thumbnail-->
<FontAwesomeIcon v-if="isFile" class="file-icon" icon="file"/>
<!--Else show only folder icon-->
<FontAwesomeIcon v-if="isFolder" :class="{ 'is-deleted': isDeleted }" class="folder-icon" icon="folder" />
</div>
<!--Image thumbnail-->
<img loading="lazy" v-if="isImage" class="image" :src="data.thumbnail" :alt="data.name"/>
<!--Name-->
<div class="item-name">
<!--Name-->
<b ref="name" @input="renameItem" :contenteditable="canEditName" class="name">
{{ itemName }}
</b>
<!--Else show only folder icon-->
<FontAwesomeIcon v-if="isFolder" :class="{ 'is-deleted': isDeleted }" class="folder-icon" icon="folder"/>
</div>
<div class="item-info">
<!--Shared Icon-->
<div v-if="$checkPermission('master') && data.shared" class="item-shared">
<link-icon size="12" class="shared-icon"></link-icon>
</div>
<!--Name-->
<div class="item-name">
<b ref="name" @input="renameItem" @keydown.delete.stop :contenteditable="canEditName" class="name">
{{ itemName }}
</b>
<!--Participant owner Icon-->
<div v-if="$checkPermission('master') && data.user_scope !== 'master'" class="item-shared">
<user-plus-icon size="12" class="shared-icon"></user-plus-icon>
</div>
<div class="item-info">
<!--Shared Icon-->
<div v-if="$checkPermission('master') && data.shared" class="item-shared">
<link-icon size="12" class="shared-icon"></link-icon>
</div>
<!--Filesize and timestamp-->
<span v-if="!isFolder" class="item-size">{{ data.filesize }}, {{ timeStamp }}</span>
<!--Participant owner Icon-->
<div v-if="$checkPermission('master') && data.user_scope !== 'master'" class="item-shared">
<user-plus-icon size="12" class="shared-icon"></user-plus-icon>
</div>
<!--Folder item counts-->
<span v-if="isFolder" class="item-length"> {{ folderItems == 0 ? $t('folder.empty') : $tc('folder.item_counts', folderItems) }}, {{ timeStamp }} </span>
</div>
</div>
<!--Filesize and timestamp-->
<span v-if="!isFolder" class="item-size">{{ data.filesize }}, {{ timeStamp }}</span>
<!--Go Next icon-->
<div class="actions" v-if="$isMobile() && !($checkPermission('visitor') && isFolder)">
<span @click.stop="showItemActions" class="show-actions">
<FontAwesomeIcon icon="ellipsis-v" class="icon-action"></FontAwesomeIcon>
</span>
</div>
</div>
</div>
<!--Folder item counts-->
<span v-if="isFolder" class="item-length"> {{ folderItems == 0 ? $t('folder.empty') : $tc('folder.item_counts', folderItems) }}, {{ timeStamp }} </span>
</div>
</div>
<!--Show item actions-->
<transition name="slide-from-right">
<div class="actions" v-if="$isMobile() && !($checkPermission('visitor') && isFolder || mobileMultiSelect)">
<span @click.stop="showItemActions" class="show-actions">
<FontAwesomeIcon icon="ellipsis-v" class="icon-action"></FontAwesomeIcon>
</span>
</div>
</transition>
</div>
</div>
</template>
<script>
import { LinkIcon, UserPlusIcon } from 'vue-feather-icons'
import { LinkIcon, UserPlusIcon, CheckIcon } from 'vue-feather-icons'
import { debounce } from 'lodash'
import { mapGetters } from 'vuex'
import { events } from '@/bus'
export default {
name: 'FileItemList',
props: ['data'],
components: {
UserPlusIcon,
LinkIcon
},
computed: {
...mapGetters(['FilePreviewType']),
isFolder() {
return this.data.type === 'folder'
},
isFile() {
return this.data.type !== 'folder' && this.data.type !== 'image'
},
isImage() {
return this.data.type === 'image'
},
isPdf() {
return this.data.mimetype === 'pdf'
},
isVideo() {
return this.data.type === 'video'
},
isAudio() {
let mimetypes = ['mpeg', 'mp3', 'mp4', 'wan', 'flac']
return mimetypes.includes(this.data.mimetype) && this.data.type === 'audio'
},
canEditName() {
return !this.$isMobile() && !this.$isThisLocation(['trash', 'trash-root']) && !this.$checkPermission('visitor') && !(this.sharedDetail && this.sharedDetail.type === 'file')
},
canDrag() {
return !this.isDeleted && this.$checkPermission(['master', 'editor'])
},
timeStamp() {
return this.data.deleted_at ? this.$t('item_thumbnail.deleted_at', { time: this.data.deleted_at }) : this.data.created_at
},
folderItems() {
return this.data.deleted_at ? this.data.trashed_items : this.data.items
},
isDeleted() {
return this.data.deleted_at ? true : false
}
},
filters: {
limitCharacters(str) {
if (str.length > 3) {
return str.substring(0, 3) + '...'
} else {
return str.substring(0, 3)
}
}
},
data() {
return {
isClicked: false,
area: false,
itemName: undefined
}
},
methods: {
showItemActions() {
// Load file info detail
this.$store.commit('GET_FILEINFO_DETAIL', this.data)
name: 'FileItemList',
props: ['data'],
components: {
UserPlusIcon,
LinkIcon,
CheckIcon
},
computed: {
...mapGetters(['FilePreviewType', 'fileInfoDetail']),
...mapGetters({ allData: 'data' }),
isClicked() {
return this.fileInfoDetail.some(element => element.unique_id == this.data.unique_id)
},
isFolder() {
return this.data.type === 'folder'
},
isFile() {
return this.data.type !== 'folder' && this.data.type !== 'image'
},
isImage() {
return this.data.type === 'image'
},
isPdf() {
return this.data.mimetype === 'pdf'
},
isVideo() {
return this.data.type === 'video'
},
isAudio() {
let mimetypes = ['mpeg', 'mp3', 'mp4', 'wan', 'flac']
return mimetypes.includes(this.data.mimetype) && this.data.type === 'audio'
},
canEditName() {
return !this.$isMobile() && !this.$isThisLocation(['trash', 'trash-root']) && !this.$checkPermission('visitor') && !(this.sharedDetail && this.sharedDetail.type === 'file')
},
canDrag() {
return !this.isDeleted && this.$checkPermission(['master', 'editor'])
},
timeStamp() {
return this.data.deleted_at ? this.$t('item_thumbnail.deleted_at', { time: this.data.deleted_at }) : this.data.created_at
},
folderItems() {
return this.data.deleted_at ? this.data.trashed_items : this.data.items
},
isDeleted() {
return this.data.deleted_at ? true : false
}
},
filters: {
limitCharacters(str) {
if (str.length > 3) {
return str.substring(0, 3) + '...'
} else {
return str.substring(0, 3)
}
}
},
data() {
return {
area: false,
itemName: undefined,
mobileMultiSelect: false
}
},
methods: {
drop() {
events.$emit('drop')
},
showItemActions() {
// Load file info detail
this.$store.commit('CLEAR_FILEINFO_DETAIL')
this.$store.commit('GET_FILEINFO_DETAIL', this.data)
events.$emit('mobileMenu:show')
},
dragEnter() {
if (this.data.type !== 'folder') return
events.$emit('mobileMenu:show')
},
dragEnter() {
if (this.data.type !== 'folder') return
this.area = true
},
dragLeave() {
this.area = false
},
clickedItem(e) {
events.$emit('contextMenu:hide')
events.$emit('fileItem:deselect')
this.area = true
},
dragLeave() {
this.area = false
},
clickedItem(e) {
events.$emit('unClick')
// Set clicked item
this.isClicked = true
if (!this.$isMobile()) {
// Open in mobile version on first click
if (this.$isMobile() && this.isFolder) {
// Go to folder
if (this.$isThisLocation('public')) {
this.$store.dispatch('browseShared', [{ folder: this.data, back: false, init: false }])
} else {
this.$store.dispatch('getFolder', [{ folder: this.data, back: false, init: false }])
}
}
if ((e.ctrlKey || e.metaKey) && !e.shiftKey) {
// Click + Ctrl
if (this.$isMobile()) {
if (this.isImage || this.isVideo || this.isAudio) {
events.$emit('fileFullPreview:show')
}
}
if (this.fileInfoDetail.some(item => item.unique_id === this.data.unique_id)) {
this.$store.commit('REMOVE_ITEM_FILEINFO_DETAIL', this.data)
} else {
this.$store.commit('GET_FILEINFO_DETAIL', this.data)
}
} else if (e.shiftKey) {
// Click + Shift
let lastItem = this.allData.indexOf(this.fileInfoDetail[this.fileInfoDetail.length - 1])
let clickedItem = this.allData.indexOf(this.data)
// Load file info detail
this.$store.commit('GET_FILEINFO_DETAIL', this.data)
// If Click + Shift + Ctrl dont remove already selected items
if (!e.ctrlKey && !e.metaKey) {
this.$store.commit('CLEAR_FILEINFO_DETAIL')
}
// Get target classname
let itemClass = e.target.className
//Shift selecting from top to bottom
if (lastItem < clickedItem) {
for (let i = lastItem; i <= clickedItem; i++) {
this.$store.commit('GET_FILEINFO_DETAIL', this.allData[i])
}
//Shift selecting from bottom to top
} else {
for (let i = lastItem; i >= clickedItem; i--) {
this.$store.commit('GET_FILEINFO_DETAIL', this.allData[i])
}
}
} else {
// Click
this.$store.commit('CLEAR_FILEINFO_DETAIL')
this.$store.commit('GET_FILEINFO_DETAIL', this.data)
}
}
if (['name', 'icon', 'file-link', 'file-icon-text'].includes(itemClass)) return
},
goToItem() {
if (this.isImage || this.isVideo || this.isAudio) {
events.$emit('fileFullPreview:show')
if (!this.mobileMultiSelect && this.$isMobile()) {
// Open in mobile version on first click
if (this.$isMobile() && this.isFolder) {
// Go to folder
if (this.$isThisLocation('public')) {
this.$store.dispatch('browseShared', [{ folder: this.data, back: false, init: false }])
} else {
this.$store.dispatch('getFolder', [{ folder: this.data, back: false, init: false }])
}
}
} else if (this.isFile || !this.isFolder && !this.isPdf && !this.isVideo && !this.isAudio && !this.isImage) {
this.$downloadFile(this.data.file_url, this.data.name + '.' + this.data.mimetype)
if (this.$isMobile()) {
if (this.isImage || this.isVideo || this.isAudio) {
this.$store.commit('GET_FILEINFO_DETAIL', this.data)
events.$emit('fileFullPreview:show')
}
}
}
} else if (this.isFolder) {
if (this.$isThisLocation('public')) {
this.$store.dispatch('browseShared', [{ folder: this.data, back: false, init: false }])
} else {
this.$store.dispatch('getFolder', [{ folder: this.data, back: false, init: false }])
}
}
},
renameItem: debounce(function(e) {
// Prevent submit empty string
if (e.target.innerText.trim() === '') return
if (this.mobileMultiSelect && this.$isMobile()) {
if (this.fileInfoDetail.some(item => item.unique_id === this.data.unique_id)) {
this.$store.commit('REMOVE_ITEM_FILEINFO_DETAIL', this.data)
} else {
this.$store.commit('GET_FILEINFO_DETAIL', this.data)
}
}
this.$store.dispatch('renameItem', {
unique_id: this.data.unique_id,
type: this.data.type,
name: e.target.innerText
})
}, 300)
},
created() {
this.itemName = this.data.name
// Get target classname
let itemClass = e.target.className
events.$on('fileItem:deselect', () => {
// Deselect file
this.isClicked = false
})
if (['name', 'icon', 'file-link', 'file-icon-text'].includes(itemClass)) return
},
goToItem() {
if (this.isImage || this.isVideo || this.isAudio) {
events.$emit('fileFullPreview:show')
// Change item name
events.$on('change:name', (item) => {
if (this.data.unique_id == item.unique_id) this.itemName = item.name
})
}
} else if (this.isFile || !this.isFolder && !this.isPdf && !this.isVideo && !this.isAudio && !this.isImage) {
this.$downloadFile(this.data.file_url, this.data.name + '.' + this.data.mimetype)
} else if (this.isFolder) {
//Clear selected items after open another folder
this.$store.commit('CLEAR_FILEINFO_DETAIL')
if (this.$isThisLocation('public')) {
this.$store.dispatch('browseShared', [{ folder: this.data, back: false, init: false }])
} else {
this.$store.dispatch('getFolder', [{ folder: this.data, back: false, init: false }])
}
}
},
renameItem: debounce(function(e) {
// Prevent submit empty string
if (e.target.innerText.trim() === '') return
this.$store.dispatch('renameItem', {
unique_id: this.data.unique_id,
type: this.data.type,
name: e.target.innerText
})
}, 300)
},
created() {
this.itemName = this.data.name
events.$on('mobileSelecting:start', () => {
this.mobileMultiSelect = true
this.$store.commit('CLEAR_FILEINFO_DETAIL')
})
events.$on('mobileSelecting:stop', () => {
this.mobileMultiSelect = false
this.$store.commit('CLEAR_FILEINFO_DETAIL')
})
// Change item name
events.$on('change:name', (item) => {
if (this.data.unique_id == item.unique_id) this.itemName = item.name
})
}
}
</script>
@@ -224,231 +285,322 @@ export default {
@import '@assets/vue-file-manager/_variables';
@import '@assets/vue-file-manager/_mixins';
.slide-from-left-move {
transition: transform 300s ease;
}
.slide-from-left-enter-active,
.slide-from-right-enter-active,
.slide-from-left-leave-active,
.slide-from-right-leave-active {
transition: all 300ms;
}
.slide-from-left-enter,
.slide-from-left-leave-to {
opacity: 0;
transform: translateX(-100%);
}
.slide-from-right-enter,
.slide-from-right-leave-to {
opacity: 0;
transform: translateX(100%);
}
.check-select {
margin-right: 15px;
margin-left: 6px;
.select-box {
width: 20px;
height: 20px;
background-color: darken($light_background, 5%);
display: flex;
justify-content: center;
align-items: center;
border-radius: 5px;
}
.select-box-active {
background-color: #f4f5f6;
.icon {
stroke: $text;
}
}
}
.file-wrapper {
user-select: none;
position: relative;
user-select: none;
position: relative;
&:hover {
border-color: transparent;
}
&:hover {
border-color: transparent;
}
.actions {
text-align: right;
width: 50px;
.actions {
text-align: right;
width: 50px;
.show-actions {
cursor: pointer;
padding: 12px 6px 12px;
.show-actions {
cursor: pointer;
padding: 12px 6px 12px;
.icon-action {
@include font-size(14);
.icon-action {
@include font-size(14);
path {
fill: $theme;
}
}
}
}
path {
fill: $theme;
}
}
}
}
.item-name {
display: block;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
.item-name {
display: block;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
.item-info {
display: block;
line-height: 1;
}
.item-info {
display: block;
line-height: 1;
}
.item-shared {
display: inline-block;
.item-shared {
display: inline-block;
.label {
@include font-size(12);
font-weight: 400;
color: $theme;
}
.label {
@include font-size(12);
font-weight: 400;
color: $theme;
}
.shared-icon {
vertical-align: middle;
.shared-icon {
vertical-align: middle;
path,
circle,
line {
stroke: $theme;
}
}
}
path,
circle,
line {
stroke: $theme;
}
}
}
.item-size,
.item-length {
@include font-size(11);
font-weight: 400;
color: rgba($text, 0.7);
}
.item-size,
.item-length {
@include font-size(11);
font-weight: 400;
color: rgba($text, 0.7);
}
.name {
white-space: nowrap;
.name {
white-space: nowrap;
&[contenteditable] {
-webkit-user-select: text;
user-select: text;
}
&[contenteditable] {
-webkit-user-select: text;
user-select: text;
}
&[contenteditable='true']:hover {
text-decoration: underline;
}
}
&[contenteditable='true']:hover {
text-decoration: underline;
}
}
.name {
color: $text;
@include font-size(14);
font-weight: 700;
max-height: 40px;
overflow: hidden;
text-overflow: ellipsis;
.name {
color: $text;
@include font-size(14);
font-weight: 700;
max-height: 40px;
overflow: hidden;
text-overflow: ellipsis;
&.actived {
max-height: initial;
}
}
}
&.actived {
max-height: initial;
}
}
}
&.selected {
.file-item {
background: $light_background;
}
}
&.selected {
.file-item {
background: $light_background;
}
}
.icon-item {
text-align: center;
position: relative;
flex: 0 0 50px;
line-height: 0;
margin-right: 20px;
.icon-item {
text-align: center;
position: relative;
flex: 0 0 50px;
line-height: 0;
margin-right: 20px;
.folder-icon {
@include font-size(52);
.folder-icon {
@include font-size(52);
path {
fill: $theme;
}
path {
fill: $theme;
}
&.is-deleted {
path {
fill: $dark_background;
}
}
}
&.is-deleted {
path {
fill: $dark_background;
}
}
}
.file-icon {
@include font-size(45);
.file-icon {
@include font-size(45);
path {
fill: #fafafc;
stroke: #dfe0e8;
stroke-width: 1;
}
}
path {
fill: #fafafc;
stroke: #dfe0e8;
stroke-width: 1;
}
}
.file-icon-text {
line-height: 1;
top: 40%;
@include font-size(11);
margin: 0 auto;
position: absolute;
text-align: center;
left: 0;
right: 0;
color: $theme;
font-weight: 600;
user-select: none;
max-width: 50px;
max-height: 20px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.file-icon-text {
line-height: 1;
top: 40%;
@include font-size(11);
margin: 0 auto;
position: absolute;
text-align: center;
left: 0;
right: 0;
color: $theme;
font-weight: 600;
user-select: none;
max-width: 50px;
max-height: 20px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.image {
object-fit: cover;
user-select: none;
max-width: 100%;
border-radius: 5px;
width: 50px;
height: 50px;
pointer-events: none;
}
}
.image {
object-fit: cover;
user-select: none;
max-width: 100%;
border-radius: 5px;
width: 50px;
height: 50px;
pointer-events: none;
}
}
.file-item {
border: 2px dashed transparent;
width: 100%;
display: flex;
align-items: center;
padding: 7px;
.file-item {
border: 2px dashed transparent;
width: 100%;
display: flex;
align-items: center;
padding: 7px;
&.is-dragenter {
border: 2px dashed $theme;
border-radius: 8px;
}
&.is-dragenter {
border: 2px dashed $theme;
border-radius: 8px;
}
&:hover,
&.is-clicked {
border-radius: 8px;
background: $light_background;
&.no-clicked {
background: white !important;
.item-name .name {
color: $theme;
}
}
}
.item-name {
.name {
color: $text !important;
}
}
}
&:hover,
&.is-clicked {
border-radius: 8px;
background: $light_background;
}
}
}
@media (prefers-color-scheme: dark) {
.file-wrapper {
.icon-item {
.file-icon {
path {
fill: $dark_mode_foreground;
stroke: #2f3c54;
}
}
.check-select {
.folder-icon {
&.is-deleted {
path {
fill: lighten($dark_mode_foreground, 5%);
}
}
}
}
.select-box {
background-color: lighten($dark_mode_foreground, 10%);
}
.file-item {
&:hover,
&.is-clicked {
background: $dark_mode_foreground;
.select-box-active {
background-color: lighten($dark_mode_foreground, 10%);
.file-icon {
path {
fill: $dark_mode_background;
}
}
}
}
.icon {
stroke: $theme;
}
}
}
.item-name {
.name {
color: $dark_mode_text_primary;
}
.file-wrapper {
.icon-item {
.file-icon {
path {
fill: $dark_mode_foreground;
stroke: #2f3c54;
}
}
.item-size,
.item-length {
color: $dark_mode_text_secondary;
}
}
}
.folder-icon {
&.is-deleted {
path {
fill: lighten($dark_mode_foreground, 5%);
}
}
}
}
.file-item {
&.no-clicked {
background: $dark_mode_background !important;
.file-icon {
path {
fill: $dark_mode_foreground !important;
stroke: #2F3C54;
}
}
.item-name {
.name {
color: $dark_mode_text_primary !important;
}
}
}
&:hover,
&.is-clicked {
background: $dark_mode_foreground;
.item-name .name {
color: $theme;
}
.file-icon {
path {
fill: $dark_mode_background;
}
}
}
}
.item-name {
.name {
color: $dark_mode_text_primary;
}
.item-size,
.item-length {
color: $dark_mode_text_secondary;
}
}
}
}
</style>
@@ -1,9 +1,9 @@
<template>
<div v-if="canBePreview" class="preview">
<img v-if="fileInfoDetail.type == 'image' && fileInfoDetail.thumbnail" :src="fileInfoDetail.thumbnail" :alt="fileInfoDetail.name" />
<audio v-else-if="fileInfoDetail.type == 'audio'" :src="fileInfoDetail.file_url" controlsList="nodownload" controls></audio>
<video v-else-if="fileInfoDetail.type == 'video'" controlsList="nodownload" disablePictureInPicture playsinline controls>
<source :src="fileInfoDetail.file_url" type="video/mp4">
<img v-if="fileInfoDetail[0].type == 'image'" :src="fileInfoDetail[0].thumbnail" :alt="fileInfoDetail[0].name" />
<audio v-else-if="fileInfoDetail[0].type == 'audio'" :src="fileInfoDetail[0].file_url" controlsList="nodownload" controls></audio>
<video v-else-if="fileInfoDetail[0].type == 'video'" controlsList="nodownload" disablePictureInPicture playsinline controls>
<source :src="fileInfoDetail[0].file_url" type="video/mp4">
</video>
</div>
</template>
@@ -17,9 +17,9 @@
computed: {
...mapGetters(['fileInfoDetail']),
canBePreview() {
return this.fileInfoDetail && ! includes([
return this.fileInfoDetail[0] && ! includes([
'folder', 'file'
], this.fileInfoDetail.type)
], this.fileInfoDetail[0].type)
}
},
}
@@ -26,7 +26,7 @@ export default {
filteredFiles() {
let filteredData = []
this.data.filter((element) => {
if (element.type == this.fileInfoDetail.type) {
if (element.type == this.fileInfoDetail[0].type) {
filteredData.push(element)
}
})
@@ -1,24 +1,24 @@
<template>
<div class="navigation-panel" v-if="fileInfoDetail">
<div class="navigation-panel" v-if="fileInfoDetail[0]">
<div class="name-wrapper">
<x-icon @click="closeFullPreview" size="22" class="icon-close"></x-icon>
<div class="name-count-wrapper">
<p class="title">{{ formatedName }}</p>
<p class="title">{{ fileInfoDetail[0].name }}</p>
<span class="file-count"> ({{ showingImageIndex + ' ' + $t('pronouns.of') + ' ' + filteredFiles.length }}) </span>
</div>
<span id="fast-preview-menu" class="fast-menu-icon" @click="menuOpen" v-if="$checkPermission(['master', 'editor'])">
<span id="fast-preview-menu" class="fast-menu-icon" @click.stop="menuOpen" v-if="$checkPermission(['master', 'editor'])">
<more-horizontal-icon class="more-icon" size="14"> </more-horizontal-icon>
</span>
</div>
<div class="created-at-wrapper">
<p>{{ fileInfoDetail.filesize }}, {{ fileInfoDetail.created_at }}</p>
<p>{{ fileInfoDetail[0].filesize }}, {{ fileInfoDetail[0].created_at }}</p>
</div>
<div class="navigation-icons">
<div class="navigation-tool-wrapper">
<ToolbarButton source="download" class="mobile-hide" @click.native="downloadItem" :action="$t('actions.download')" />
<ToolbarButton source="share" class="mobile-hide" :class="{ 'is-inactive': canShareInView }" :action="$t('actions.share')" @click.native="shareItem" />
<ToolbarButton v-if="this.fileInfoDetail.type === 'image'" source="print" :action="$t('actions.print')" @click.native="printMethod()" />
<ToolbarButton v-if="canShowShareView" :class="{ 'is-inactive': canShareInView }" @click.native="shareItem" source="share" class="mobile-hide" :action="$t('actions.share')" />
<ToolbarButton v-if="this.fileInfoDetail[0].type === 'image'" source="print" :action="$t('actions.print')" @click.native="printMethod()" />
</div>
</div>
</div>
@@ -39,7 +39,7 @@ export default {
filteredFiles() {
let files = []
this.data.filter((element) => {
if (element.type == this.fileInfoDetail.type) {
if (element.type == this.fileInfoDetail[0].type) {
files.push(element)
}
})
@@ -48,35 +48,17 @@ export default {
showingImageIndex() {
let activeIndex = ''
this.filteredFiles.filter((element, index) => {
if (element.unique_id == this.fileInfoDetail.unique_id) {
if (element.unique_id == this.fileInfoDetail[0].unique_id) {
activeIndex = index + 1
}
})
return activeIndex
},
formatedName() {
//Name length handling
let name = this.fileInfoDetail.name
let windowWidth = window.innerWidth
let nameLength
if (windowWidth < 410) {
nameLength = 18
} else {
nameLength = 27
}
if (name.lastIndexOf('.') > -1) {
return _.truncate(name.substring(0, name.lastIndexOf('.')), {
length: nameLength
})
} else {
return _.truncate(name, {
length: nameLength
})
}
},
canShowShareView() {
return this.$isThisLocation(['base', 'participant_uploads', 'latest', 'shared'])
},
canShareInView() {
return !this.$isThisLocation(['base', 'participant_uploads', 'latest', 'shared', 'public'])
return ! this.$isThisLocation(['base', 'participant_uploads', 'latest', 'shared'])
}
},
data() {
@@ -94,18 +76,18 @@ export default {
},
downloadItem() {
// Download file
this.$downloadFile(this.fileInfoDetail.file_url, this.fileInfoDetail.name + '.' + this.fileInfoDetail.mimetype)
this.$downloadFile(this.fileInfoDetail[0].file_url, this.fileInfoDetail[0].name + '.' + this.fileInfoDetail[0].mimetype)
},
shareItem() {
if (this.fileInfoDetail.shared) {
if (this.fileInfoDetail[0].shared) {
events.$emit('popup:open', {
name: 'share-edit',
item: this.fileInfoDetail
item: this.fileInfoDetail[0]
})
} else {
events.$emit('popup:open', {
name: 'share-create',
item: this.fileInfoDetail
item: this.fileInfoDetail[0]
})
}
},
@@ -113,7 +95,7 @@ export default {
if (this.$isMobile()) {
events.$emit('mobileMenu:show', 'showFromMediaPreview')
} else {
events.$emit('showContextMenuPreview:show', this.fileInfoDetail)
events.$emit('showContextMenuPreview:show', this.fileInfoDetail[0])
}
},
closeFullPreview() {
@@ -155,6 +137,7 @@ export default {
}
.title {
@include font-size(15);
max-width: 250px;
line-height: 1;
font-weight: 700;
overflow: hidden;
@@ -164,7 +147,10 @@ export default {
color: $text;
}
@media (max-width: 570px) {
.title,
.title{
max-width: 180px;
@include font-size(17);
}
.file-count {
@include font-size(17);
}
@@ -0,0 +1,14 @@
<template>
<svg width="13px" height="15px" viewBox="0 0 13 15" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="VueFileManager" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<g id="Storage-Alert-Copy" transform="translate(-888.000000, -238.000000)" stroke="#000000" stroke-width="1.6">
<g id="Sorting-Menu" transform="translate(865.000000, 67.000000)">
<g id="alphabet-icon" transform="translate(24.000000, 172.000000)">
<polyline id="Path" points="11.1999993 13.1999991 5.59999967 0.199999094 0 13.1999991 5.59999967 0.199999094"></polyline>
<line x1="2.25" y1="8" x2="8.75" y2="8" id="Line-2"></line>
</g>
</g>
</g>
</g>
</svg>
</template>
@@ -0,0 +1,20 @@
<template>
<svg
width="15px" height="15px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="VueFileManager" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<g id="Storage-Alert-Copy" transform="translate(-1092.000000, -28.000000)" stroke="#000000" stroke-width="1.4">
<g id="Toolbar" transform="translate(331.000000, 19.000000)">
<g id="Tools" transform="translate(581.000000, 9.000000)">
<g id="sort-icon" transform="translate(181.000000, 1.000000)">
<rect id="Rectangle" x="9.77777778" y="0" width="6.22222222" height="6.22222222"></rect>
<rect id="Rectangle" x="9.77777778" y="9.77777778" width="6.22222222" height="6.22222222"></rect>
<line x1="0" y1="2" x2="6" y2="2" id="Path"></line>
<line x1="0" y1="8" x2="6" y2="8" id="Path"></line>
<line x1="0" y1="14" x2="6" y2="14" id="Path"></line>
</g>
</g>
</g>
</g>
</g>
</svg>
</template>
@@ -89,7 +89,9 @@ import {split} from 'lodash'
export default {
name: 'ImageMetaData',
computed: {
...mapGetters(['fileInfoDetail']),
fileInfoDetail() {
return this.$store.getters.fileInfoDetail[0]
},
},
methods: {
formatGps(location, ref) {
@@ -1,14 +1,13 @@
<template>
<div class="media-full-preview" id="mediaPreview" v-if="this.isMedia && fileInfoDetail">
<div class="media-full-preview" id="mediaPreview" v-if="this.isMedia && fileInfoDetail[0]">
<div class="file-wrapper-preview" v-for="i in [currentIndex]" :key="i">
<div class="file-wrapper">
<audio class="file audio" :class="{ 'file-shadow': !isMobileDevice }" v-if="fileInfoDetail.type == 'audio'" :src="currentFile.file_url" controlsList="nodownload" controls></audio>
<img v-if="fileInfoDetail.type === 'image' && currentFile.thumbnail" class="file" :class="{ 'file-shadow': !isMobileDevice }" id="image" :src="currentFile.file_url" />
<div class="video-wrapper" v-if="fileInfoDetail.type === 'video' && currentFile.file_url">
<audio class="file audio" :class="{ 'file-shadow': !isMobileDevice }" v-if="fileInfoDetail[0].type == 'audio'" :src="currentFile.file_url" controlsList="nodownload" controls></audio>
<img v-if="fileInfoDetail[0].type === 'image' && currentFile.thumbnail" class="file" :class="{ 'file-shadow': !isMobileDevice }" id="image" :src="currentFile.file_url" />
<div class="video-wrapper" v-if="fileInfoDetail[0].type === 'video' && currentFile.file_url">
<video :src="currentFile.file_url" class="video" :class="{ 'file-shadow': !isMobileDevice }" controlsList="nodownload" disablePictureInPicture playsinline controls />
</div>
</div>
<!-- <spinner class="loading-spinner" v-if="!loaded && fileInfoDetail.type === 'image'" /> -->
</div>
</div>
</template>
@@ -33,7 +32,7 @@ export default {
return this.sliderFile[Math.abs(this.currentIndex) % this.sliderFile.length]
},
isMedia() {
return this.fileInfoDetail === 'image' || 'video' || 'audio'
return this.fileInfoDetail[0] === 'image' || 'video' || 'audio'
},
canShareInView() {
@@ -57,7 +56,8 @@ export default {
},
currentFile() {
//Handle actual view image in fileInfoDetail
if (this.fileInfoDetail) {
if (this.fileInfoDetail[0]) {
this.$store.commit('CLEAR_FILEINFO_DETAIL')
this.$store.commit('GET_FILEINFO_DETAIL', this.currentFile)
events.$emit('actualShowingImage:ContextMenu', this.currentFile)
// this.loaded = false
@@ -65,7 +65,7 @@ export default {
},
fileInfoDetail() {
//File delete handling - show next image after delete one
if (!this.fileInfoDetail) {
if (!this.fileInfoDetail[0]) {
this.currentIndex = this.currentIndex - 1
this.$store.commit('GET_FILEINFO_DETAIL', this.currentFile)
this.sliderFile = []
@@ -83,18 +83,15 @@ export default {
methods: {
filteredFiles() {
this.data.filter((element) => {
if (element.type == this.fileInfoDetail.type) {
if (element.type == this.fileInfoDetail[0].type) {
this.sliderFile.push(element)
}
})
this.choseActiveFile()
},
// onLoaded(event) {
// this.loaded = true
// },
choseActiveFile() {
this.sliderFile.forEach((element, index) => {
if (element.unique_id == this.fileInfoDetail.unique_id) {
if (element.unique_id == this.fileInfoDetail[0].unique_id) {
this.currentIndex = index
}
})
@@ -175,7 +172,7 @@ export default {
align-items: center;
.file-shadow {
box-shadow: 0 8px 40px rgba(17, 26, 52, 0.15);
box-shadow: 0 8px 40px rgba(17, 26, 52, 0.05);
}
.file {
@@ -231,7 +228,7 @@ export default {
background-color: $dark_mode_background;
.file-wrapper {
.file-shadow {
box-shadow: 0 8px 40px rgba(0, 0, 0, 0.3);
box-shadow: 0 8px 40px rgba(0, 0, 0, 0.1);
}
}
}
@@ -8,6 +8,10 @@
<grid-icon v-if="icon === 'th'" size="15" class="icon"></grid-icon>
<user-plus-icon v-if="icon === 'user-plus'" size="15" class="icon"></user-plus-icon>
<plus-icon v-if="icon === 'plus'" size="15" class="icon"></plus-icon>
<check-square-icon v-if="icon === 'check-square'" size="15" class="icon"></check-square-icon>
<x-square-icon v-if="icon === 'x-square'" size="15" class="icon"></x-square-icon>
<check-icon v-if="icon === 'check'" size="15" class="icon"></check-icon>
<sorting-and-preview-icon v-if="icon === 'preview-sorting'" size="15" class="icon preview-sorting"></sorting-and-preview-icon>
<span class="label">
<slot></slot>
</span>
@@ -16,7 +20,8 @@
</template>
<script>
import { FolderPlusIcon, ListIcon, GridIcon, TrashIcon, UserPlusIcon, PlusIcon, CreditCardIcon } from 'vue-feather-icons'
import { CheckIcon, XSquareIcon, CheckSquareIcon, FolderPlusIcon, ListIcon, GridIcon, TrashIcon, UserPlusIcon, PlusIcon, CreditCardIcon } from 'vue-feather-icons'
import SortingAndPreviewIcon from '@/components/FilesView/Icons/SortingAndPreviewIcon'
export default {
name: 'MobileActionButton',
@@ -24,9 +29,13 @@
'icon'
],
components: {
SortingAndPreviewIcon,
CheckSquareIcon,
CreditCardIcon,
FolderPlusIcon,
UserPlusIcon,
XSquareIcon,
CheckIcon,
TrashIcon,
PlusIcon,
ListIcon,
@@ -73,7 +82,7 @@
@include transform(scale(0.95));
}
&:hover {
/*&:hover {
background: rgba($theme, 0.1);
.icon {
@@ -85,7 +94,7 @@
.label {
color: $theme;
}
}
}*/
}
@media (prefers-color-scheme: dark) {
@@ -2,32 +2,58 @@
<div id="mobile-actions-wrapper">
<!--Actions for trash location with MASTER permission--->
<div v-if="$isThisLocation(['trash', 'trash-root']) && $checkPermission('master')" class="mobile-actions">
<MobileActionButton @click.native="switchPreview" :icon="previewIcon">
{{ previewText }}
</MobileActionButton>
<div v-if="trashLocationMenu && ! multiSelectMode" class="mobile-actions">
<MobileActionButton @click.native="$store.dispatch('emptyTrash')" icon="trash">
{{ $t('context_menu.empty_trash') }}
</MobileActionButton>
<MobileMultiSelectButton @click.native="enableMultiSelectMode">
{{ $t('context_menu.select') }}
</MobileMultiSelectButton>
<MobileActionButton class="preview-sorting" @click.native="showViewOptions" icon="preview-sorting">
{{$t('preview_sorting.preview_sorting_button')}}
</MobileActionButton>
</div>
<!--ContextMenu for Base location with MASTER permission-->
<div v-if="$isThisLocation(['base', 'public']) && $checkPermission(['master', 'editor'])" class="mobile-actions">
<MobileActionButton @click.native="createFolder" icon="folder-plus">
{{ $t('context_menu.add_folder') }}
</MobileActionButton>
<MobileActionButtonUpload>
{{ $t('context_menu.upload') }}
</MobileActionButtonUpload>
<MobileActionButton @click.native="switchPreview" :icon="previewIcon">
{{ previewText }}
</MobileActionButton>
</div>
<transition name="button">
<div v-if="baseLocationMasterMenu && ! multiSelectMode" class="mobile-actions">
<MobileActionButton @click.native="createFolder" icon="folder-plus" :class="{'is-inactive' : multiSelectMode}">
{{ $t('context_menu.add_folder') }}
</MobileActionButton>
<MobileActionButtonUpload :class="{'is-inactive' : multiSelectMode}">
{{ $t('context_menu.upload') }}
</MobileActionButtonUpload>
<MobileMultiSelectButton @click.native="enableMultiSelectMode">
{{ $t('context_menu.select') }}
</MobileMultiSelectButton>
<MobileActionButton class="preview-sorting" @click.native="showViewOptions" icon="preview-sorting">
{{$t('preview_sorting.preview_sorting_button')}}
</MobileActionButton>
</div>
</transition>
<!-- Selecting buttons -->
<transition name="button">
<div v-if="multiSelectMode" class="mobile-actions">
<MobileActionButton @click.native="selectAll" icon="check-square">
{{$t('mobile_selecting.select_all')}}
</MobileActionButton>
<MobileActionButton @click.native="deselectAll" icon="x-square">
{{$t('mobile_selecting.deselect_all')}}
</MobileActionButton>
<MobileActionButton @click.native="disableMultiSelectMode" icon="check">
{{$t('mobile_selecting.done')}}
</MobileActionButton>
</div>
</transition>
<!--ContextMenu for Base location with VISITOR permission-->
<div v-if="($isThisLocation(['base', 'shared', 'public']) && $checkPermission('visitor')) || ($isThisLocation(['latest', 'shared']) && $checkPermission('master'))" class="mobile-actions">
<MobileActionButton @click.native="switchPreview" :icon="previewIcon">
{{ previewText }}
<div v-if="baseLocationVisitorMenu && ! multiSelectMode" class="mobile-actions">
<MobileMultiSelectButton @click.native="enableMultiSelectMode">
{{ $t('context_menu.select') }}
</MobileMultiSelectButton>
<MobileActionButton class="preview-sorting" @click.native="showViewOptions" icon="preview-sorting">
{{$t('preview_sorting.preview_sorting_button')}}
</MobileActionButton>
</div>
@@ -38,16 +64,17 @@
<script>
import MobileActionButtonUpload from '@/components/FilesView/MobileActionButtonUpload'
import MobileMultiSelectButton from '@/components/FilesView/MobileMultiSelectButton'
import MobileActionButton from '@/components/FilesView/MobileActionButton'
import UploadProgress from '@/components/FilesView/UploadProgress'
import {mapGetters} from 'vuex'
import {debounce} from 'lodash'
import {events} from '@/bus'
export default {
name: 'MobileActions',
components: {
MobileActionButtonUpload,
MobileMultiSelectButton,
MobileActionButton,
UploadProgress,
},
@@ -56,26 +83,53 @@
previewIcon() {
return this.FilePreviewType === 'list' ? 'th' : 'th-list'
},
previewText() {
return this.FilePreviewType === 'list' ? this.$t('preview_type.grid') : this.$t('preview_type.list')
trashLocationMenu() {
return this.$isThisLocation(['trash', 'trash-root']) && this.$checkPermission('master')
},
baseLocationMasterMenu() {
return this.$isThisLocation(['base', 'public']) && this.$checkPermission(['master', 'editor'])
},
baseLocationVisitorMenu() {
return (this.$isThisLocation(['base', 'shared', 'public']) && this.$checkPermission('visitor')) || (this.$isThisLocation(['latest', 'shared']) && this.$checkPermission('master'))
},
},
data () {
return {
multiSelectMode: false,
mobileSortingAndPreview: false,
}
},
methods: {
switchPreview() {
this.$store.dispatch('changePreviewType')
selectAll() {
this.$store.commit('SELECT_ALL_FILES')
},
deselectAll() {
this.$store.commit('CLEAR_FILEINFO_DETAIL')
},
enableMultiSelectMode() {
this.multiSelectMode = true
events.$emit('mobileSelecting:start')
},
disableMultiSelectMode() {
this.multiSelectMode = false
events.$emit('mobileSelecting:stop')
},
showViewOptions() {
this.mobileSortingAndPreview = ! this.mobileSortingAndPreview
// Toggle mobile sorting
events.$emit('mobileSortingAndPreview', this.mobileSortingAndPreview)
events.$emit('mobileSortingAndPreviewVignette', this.mobileSortingAndPreview)
},
createFolder() {
if (this.$isMobile()) {
// Get folder name
let folderName = prompt(this.$t('popup_create_folder.title'))
// Create folder
if (folderName) this.$createFolder(folderName)
} else {
// Create folder
this.$createFolder(this.$t('popup_create_folder.folder_default_name'))
}
events.$emit('popup:open', {name: 'create-folder'})
},
},
mounted () {
events.$on('mobileSelecting:stop', () => this.multiSelectMode = false)
events.$on('mobileSortingAndPreview', state => this.mobileSortingAndPreview = state)
}
}
</script>
@@ -84,6 +138,37 @@
@import '@assets/vue-file-manager/_variables';
@import '@assets/vue-file-manager/_mixins';
.button-enter-active,
.button-leave-active {
transition: all 250ms;
}
.button-enter {
opacity: 0;
transform: translateY(-50%);
}
.button-leave-to {
opacity: 0;
transform: translateY(50%);
}
.button-leave-active {
position: absolute;
}
.preview-sorting {
background: $light_background !important;
/deep/ .label {
color: $text !important;
}
/deep/ .preview-sorting {
path, line, polyline, rect, circle {
stroke: $text !important;
}
}
}
#mobile-actions-wrapper {
display: none;
background: white;
@@ -92,11 +177,18 @@
z-index: 3;
}
.mobile-action-button {
&.is-inactive {
opacity: 0.25;
pointer-events: none;
}
}
.mobile-actions {
padding-top: 10px;
padding-bottom: 10px;
white-space: nowrap;
overflow-x: auto;
margin: 0 -15px;
padding: 10px 0 10px 15px;
}
@media only screen and (max-width: 960px) {
@@ -110,5 +202,16 @@
#mobile-actions-wrapper {
background: $dark_mode_background;
}
.preview-sorting {
background: $dark_mode_foreground !important;
/deep/ .label {
color: $dark_mode_text_primary !important;
}
/deep/ .preview-sorting {
path, line, polyline, rect, circle {
stroke: $theme !important;
}
}
}
}
</style>
@@ -7,11 +7,13 @@
class="options"
@click="closeAndResetContextMenu"
>
<div class="menu-wrapper">
<!--Item Thumbnail-->
<ThumbnailItem
class="item-thumbnail"
:item="fileInfoDetail"
:item="fileInfoDetail[0]"
info="metadata"
/>
@@ -26,8 +28,8 @@
<ul class="menu-option-group">
<li
class="menu-option"
@click="$store.dispatch('restoreItem', fileInfoDetail)"
v-if="fileInfoDetail"
@click="$store.dispatch('restoreItem', fileInfoDetail[0])"
v-if="fileInfoDetail[0]"
>
<div class="icon">
<life-buoy-icon size="17"></life-buoy-icon>
@@ -39,7 +41,7 @@
<li
class="menu-option delete"
@click="deleteItem"
v-if="fileInfoDetail"
v-if="fileInfoDetail[0]"
>
<div class="icon">
<trash-2-icon size="17"></trash-2-icon>
@@ -71,7 +73,7 @@
<li
class="menu-option"
@click="addToFavourites"
v-if="fileInfoDetail && isFolder"
v-if="fileInfoDetail[0] && isFolder"
>
<div class="icon">
<star-icon size="17"></star-icon>
@@ -87,7 +89,7 @@
</ul>
<ul class="menu-option-group">
<li class="menu-option" @click="renameItem" v-if="fileInfoDetail">
<li class="menu-option" @click="renameItem" v-if="fileInfoDetail[0]">
<div class="icon">
<edit-2-icon size="17"></edit-2-icon>
</div>
@@ -95,13 +97,13 @@
{{ $t("context_menu.rename") }}
</div>
</li>
<li class="menu-option" @click="shareItem" v-if="fileInfoDetail">
<li class="menu-option" @click="shareItem" v-if="fileInfoDetail[0]">
<div class="icon">
<link-icon size="17"></link-icon>
</div>
<div class="text-label">
{{
fileInfoDetail.shared
fileInfoDetail[0].shared
? $t("context_menu.share_edit")
: $t("context_menu.share")
}}
@@ -110,7 +112,7 @@
<li
class="menu-option delete"
@click="deleteItem"
v-if="fileInfoDetail"
v-if="fileInfoDetail[0]"
>
<div class="icon">
<trash-2-icon size="17"></trash-2-icon>
@@ -141,7 +143,7 @@
"
class="menu-options"
>
<ul class="menu-option-group" v-if="fileInfoDetail && isFolder">
<ul class="menu-option-group" v-if="fileInfoDetail[0] && isFolder">
<li class="menu-option" @click="addToFavourites">
<div class="icon">
<star-icon size="17"></star-icon>
@@ -157,7 +159,7 @@
</ul>
<ul class="menu-option-group">
<li class="menu-option" @click="renameItem" v-if="fileInfoDetail">
<li class="menu-option" @click="renameItem" v-if="fileInfoDetail[0]">
<div class="icon">
<edit-2-icon size="17"></edit-2-icon>
</div>
@@ -165,7 +167,7 @@
{{ $t("context_menu.rename") }}
</div>
</li>
<li class="menu-option" @click="moveItem" v-if="fileInfoDetail">
<li class="menu-option" @click="moveItem" v-if="fileInfoDetail[0]">
<div class="icon">
<corner-down-right-icon size="17"></corner-down-right-icon>
</div>
@@ -173,13 +175,13 @@
{{ $t("context_menu.move") }}
</div>
</li>
<li class="menu-option" @click="shareItem" v-if="fileInfoDetail">
<li class="menu-option" @click="shareItem" v-if="fileInfoDetail[0]">
<div class="icon">
<link-icon size="17"></link-icon>
</div>
<div class="text-label">
{{
fileInfoDetail.shared
fileInfoDetail[0].shared
? $t("context_menu.share_edit")
: $t("context_menu.share")
}}
@@ -188,7 +190,7 @@
<li
class="menu-option delete"
@click="deleteItem"
v-if="fileInfoDetail"
v-if="fileInfoDetail[0]"
>
<div class="icon">
<trash-2-icon size="17"></trash-2-icon>
@@ -219,7 +221,7 @@
class="menu-options"
>
<ul class="menu-option-group">
<li class="menu-option" @click="renameItem" v-if="fileInfoDetail">
<li class="menu-option" @click="renameItem" v-if="fileInfoDetail[0]">
<div class="icon">
<edit-2-icon size="17"></edit-2-icon>
</div>
@@ -227,7 +229,7 @@
{{ $t("context_menu.rename") }}
</div>
</li>
<li class="menu-option" @click="moveItem" v-if="fileInfoDetail">
<li class="menu-option" @click="moveItem" v-if="fileInfoDetail[0]">
<div class="icon">
<corner-down-right-icon size="17"></corner-down-right-icon>
</div>
@@ -290,6 +292,7 @@
<script>
import ThumbnailItem from "@/components/Others/ThumbnailItem";
import {
CornerDownRightIcon,
DownloadCloudIcon,
@@ -327,22 +330,22 @@ export default {
},
isInFavourites() {
return this.favourites.find(
(el) => el.unique_id == this.fileInfoDetail.unique_id
(el) => el.unique_id == this.fileInfoDetail[0].unique_id
);
},
isFile() {
return (
this.fileInfoDetail &&
this.fileInfoDetail.type !== "folder" &&
this.fileInfoDetail &&
this.fileInfoDetail.type !== "image"
this.fileInfoDetail[0] &&
this.fileInfoDetail[0].type !== "folder" &&
this.fileInfoDetail[0] &&
this.fileInfoDetail[0].type !== "image"
);
},
isImage() {
return this.fileInfoDetail && this.fileInfoDetail.type === "image";
return this.fileInfoDetail[0] && this.fileInfoDetail[0].type === "image";
},
isFolder() {
return this.fileInfoDetail && this.fileInfoDetail.type === "folder";
return this.fileInfoDetail[0] && this.fileInfoDetail[0].type === "folder";
},
},
data() {
@@ -353,21 +356,20 @@ export default {
},
methods: {
moveItem() {
// Open move item popup
events.$emit("popup:open", { name: "move", item: this.fileInfoDetail });
events.$emit('popup:open', { name: 'move', item: [this.fileInfoDetail[0]] })
},
shareItem() {
if (this.fileInfoDetail.shared) {
if (this.fileInfoDetail[0].shared) {
// Open share item popup
events.$emit("popup:open", {
name: "share-edit",
item: this.fileInfoDetail,
item: this.fileInfoDetail[0],
});
} else {
// Open share item popup
events.$emit("popup:open", {
name: "share-create",
item: this.fileInfoDetail,
item: this.fileInfoDetail[0],
});
}
},
@@ -375,56 +377,31 @@ export default {
if (
this.favourites &&
!this.favourites.find(
(el) => el.unique_id == this.fileInfoDetail.unique_id
(el) => el.unique_id == this.fileInfoDetail[0].unique_id
)
) {
this.$store.dispatch("addToFavourites", this.fileInfoDetail);
this.$store.dispatch("addToFavourites", this.fileInfoDetail[0]);
} else {
this.$store.dispatch("removeFromFavourites", this.fileInfoDetail);
this.$store.dispatch("removeFromFavourites", this.fileInfoDetail[0]);
}
},
downloadItem() {
this.$downloadFile(
this.fileInfoDetail.file_url,
this.fileInfoDetail.name + "." + this.fileInfoDetail.mimetype
this.fileInfoDetail[0].file_url,
this.fileInfoDetail[0].name + "." + this.fileInfoDetail[0].mimetype
);
},
deleteItem() {
this.$store.dispatch("deleteItem", this.fileInfoDetail);
this.$store.dispatch("deleteItem");
},
renameItem() {
let itemName = prompt(
this.$t("popup_rename.title"),
this.fileInfoDetail.name
);
if (itemName && itemName !== "") {
let item = {
unique_id: this.fileInfoDetail.unique_id,
type: this.fileInfoDetail.type,
name: itemName,
};
this.$store.dispatch("renameItem", item);
// Change item name if is mobile device or prompted
if (this.$isMobile()) {
events.$emit("change:name", item);
}
}
events.$emit('popup:open', { name: 'rename-item', item: this.fileInfoDetail[0] })
},
closeAndResetContextMenu() {
//If emit to show menu coming from MediaFullPreview dont reset data
if (this.showFromMediaPreview) {
this.isVisible = false;
this.showFromMediaPreview = false;
} else {
this.isVisible = false;
events.$emit("fileItem:deselect");
}
// Close context menu
// this.isVisible = false;
events.$emit('hide:mobile-navigation')
},
},
created() {
@@ -451,6 +428,15 @@ export default {
@import "@assets/vue-file-manager/_variables";
@import "@assets/vue-file-manager/_mixins";
.mobile-selected-menu {
display: flex;
margin-left: 15px;
margin-right: 15px;
.close-icon {
margin-left: auto !important;
}
}
.menu-option {
display: flex;
align-items: center;
@@ -0,0 +1,100 @@
<template>
<button class="mobile-action-button" :class="{'active' : mobileSelectingActive}">
<div class="flex" >
<CheckSquareIcon size="15" class="icon"></CheckSquareIcon>
<span class="label">
<slot></slot>
</span>
</div>
</button>
</template>
<script>
import {CheckSquareIcon} from "vue-feather-icons";
import {events} from '@/bus'
export default {
name: 'MobileActionButton',
props: [
'icon'
],
components: {
CheckSquareIcon
},
data () {
return {
mobileSelectingActive: false
}
},
mounted() {
events.$on('mobileSelecting:start' , () => {
this.mobileSelectingActive = true
})
events.$on('mobileSelecting:stop' , () => {
this.mobileSelectingActive = false
})
}
}
</script>
<style scoped lang="scss">
@import '@assets/vue-file-manager/_variables';
@import '@assets/vue-file-manager/_mixins';
.mobile-action-button {
background: $light_background;
margin-right: 15px;
border-radius: 8px;
padding: 7px 10px;
cursor: pointer;
border: none;
@include transition(150ms);
.flex {
display: flex;
align-items: center;
}
.icon {
margin-right: 10px;
@include font-size(14);
path, line, polyline, rect, circle {
@include transition(150ms);
}
}
.label {
@include transition(150ms);
@include font-size(14);
font-weight: 700;
color: $text;
}
}
.active {
.icon {
path, line, polyline, rect, circle {
stroke: $theme !important;
}
}
.label {
color: $theme !important;
}
}
@media (prefers-color-scheme: dark) {
.mobile-action-button {
background: $dark_mode_foreground;
path, line, polyline, rect, circle {
stroke: $theme;
}
.label {
color: $dark_mode_text_primary;
}
}
}
</style>
@@ -0,0 +1,156 @@
<template>
<transition name="context-menu">
<div class="multiselect-actions" v-if="mobileMultiSelect">
<ToolbarButton class="action-btn" v-if="!$isThisLocation(['trash', 'trash-root' , 'shared', 'latest']) && $checkPermission('master') || $checkPermission('editor')" source="move" :action="$t('actions.move')" :class="{'is-inactive' : fileInfoDetail.length < 1}" @click.native="moveItem"/>
<ToolbarButton class="action-btn" v-if="!$isThisLocation(['shared']) && $checkPermission('master') || $checkPermission('editor')" source="trash" :class="{'is-inactive' : fileInfoDetail.length < 1}" :action="$t('actions.delete')" @click.native="deleteItem"/>
<ToolbarButton class="action-btn" v-if="!$isThisLocation(['shared'])" source="download" :class="{'is-inactive': canDownloadItems}" :action="$t('actions.delete')" @click.native="downloadItem"/>
<ToolbarButton class="action-btn" source="shared-off" @click.native="shareCancel" v-if="$isThisLocation(['shared'])"/>
<ToolbarButton class="action-btn close-icon" source="close" :action="$t('actions.close')" @click.native="closeSelecting"/>
</div>
</transition>
</template>
<script>
import ToolbarButton from '@/components/FilesView/ToolbarButton'
import { events } from '@/bus'
import { mapGetters } from 'vuex'
export default {
name: 'MobileMultiSelectMenu',
components: { ToolbarButton },
computed: {
...mapGetters(['fileInfoDetail']),
canDownloadItems() {
return this.fileInfoDetail.filter(item => item.type === 'folder').length !== 0
}
},
data() {
return {
mobileMultiSelect: false
}
},
methods: {
shareCancel() {
this.$store.dispatch('shareCancel')
this.closeSelecting()
},
closeSelecting() {
events.$emit('mobileSelecting:stop')
},
downloadItem() {
if (this.fileInfoDetail.length > 1)
this.$store.dispatch('downloadFiles')
else {
this.$downloadFile(this.fileInfoDetail[0].file_url, this.fileInfoDetail[0].name + '.' + this.fileInfoDetail[0].mimetype)
}
this.closeSelecting()
},
moveItem() {
// Open move item popup
events.$emit('popup:open', { name: 'move', item: [this.fileInfoDetail[0]] })
},
deleteItem() {
//Delete items
this.$store.dispatch('deleteItem')
this.closeSelecting()
}
},
created() {
events.$on('mobileSelecting:start', () => {
this.mobileMultiSelect = true
})
events.$on('mobileSelecting:stop', () => {
this.mobileMultiSelect = false
})
}
}
</script>
<style scoped lang="scss">
@import "@assets/vue-file-manager/_variables";
@import "@assets/vue-file-manager/_mixins";
.multiselect-actions {
display: flex;
padding: 10px 15px;
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 9;
overflow: hidden;
background: white;
.action-btn {
margin-right: 25px;
&:last-child {
margin-right: 0;
}
}
.close-icon {
margin-left: auto !important;
}
}
.is-inactive {
opacity: 0.25 !important;
pointer-events: none !important;
.menu-option {
display: flex;
align-items: center;
}
.options {
&.is-active {
opacity: 1 !important;
pointer-events: initial !important;
}
}
}
@media (prefers-color-scheme: dark) {
.multiselect-actions {
background: $dark_mode_foreground;
}
}
// Transition
.context-menu-enter-active,
.fade-enter-active {
transition: all 200ms;
}
.context-menu-leave-active,
.fade-leave-active {
transition: all 200ms;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
.context-menu-enter,
.context-menu-leave-to {
opacity: 0;
transform: translateY(100%);
}
.context-menu-leave-active {
position: absolute;
}
</style>
@@ -0,0 +1,76 @@
<template>
<transition v-if="isVisible" name="preview-menu" >
<SortingAndPreviewMenu class="options"/>
</transition>
</template>
<script>
import SortingAndPreviewMenu from '@/components/FilesView/SortingAndPreviewMenu'
import { events } from '@/bus'
export default {
name: 'MobileSortingAndPreview',
components: {SortingAndPreviewMenu},
data () {
return {
isVisible: false
}
},
mounted () {
events.$on('mobileSortingAndPreview', (state) => {
this.isVisible = state
})
}
}
</script>
<style scoped lang="scss">
@import "@assets/vue-file-manager/_variables";
@import "@assets/vue-file-manager/_mixins";
.options {
position: absolute;
bottom: 0;
left: 0;
right: 0;
z-index: 99;
overflow: hidden;
background: white;
border-top-left-radius: 12px;
border-top-right-radius: 12px;
}
@media (prefers-color-scheme: dark) {
.options {
background: $dark_mode_background;
}
}
// Transition
.preview-menu-enter-active,
.fade-enter-active {
transition: all 200ms;
}
.preview-menu-leave-active,
.fade-leave-active {
transition: all 200ms;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
.preview-menu-enter,
.preview-menu-leave-to {
opacity: 0;
transform: translateY(100%);
}
.preview-menu-leave-active {
position: absolute;
}
</style>
@@ -52,6 +52,7 @@
methods: {
showMobileNavigation() {
events.$emit('show:mobile-navigation')
events.$emit('mobileSelecting:stop')
},
goBack() {
@@ -0,0 +1,86 @@
<template>
<div class="wrapper">
<div class="icon-wrapper">
<CheckSquareIcon class="icon" size="21"/>
</div>
<div class="text" >
<span class="title">{{title }}</span>
<span class="count">{{subtitle }}</span>
</div>
</div>
</template>
<script>
import {CheckSquareIcon} from "vue-feather-icons";
import {mapGetters} from 'vuex'
import {events} from '@/bus'
export default {
name:'MultiSelected',
props: [ 'title' , 'subtitle' ],
components: {CheckSquareIcon},
}
</script>
<style lang="scss" scoped>
@import '@assets/vue-file-manager/_variables';
@import '@assets/vue-file-manager/_mixins';
.wrapper {
display: flex;
justify-content: center;
.text{
padding-left: 10px;
width: 100%;
word-break: break-all;
.title {
@include font-size(14);
font-weight: 700;
line-height: 1.4;
display: block;
color: $text;
}
.count {
@include font-size(12);
font-weight: 600;
color: $theme;
display: block;
}
}
.icon-wrapper {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0;
text-align: center;
cursor: pointer;
white-space: nowrap;
outline: none;
border: none;
.icon {
stroke: $text;
}
}
}
@media (prefers-color-scheme: dark) {
.wrapper {
.text {
.title {
color: $dark_mode_text_primary;
}
.count {
color: $dark_mode_text_secondary;
}
}
.icon-wrapper {
.icon {
stroke: $theme;
}
}
}
}
</style>
@@ -0,0 +1,107 @@
<template>
<li class="menu-option">
<div class="icon">
<trash-2-icon v-if="icon === 'trash'" size="17"></trash-2-icon>
<life-buoy-icon v-if="icon === 'restore'" size="17"></life-buoy-icon>
<trash-icon v-if="icon === 'empty-trash'" size="17"></trash-icon>
<eye-icon v-if="icon ==='detail'" size="17"></eye-icon>
<download-cloud-icon v-if="icon === 'download'" size="17"></download-cloud-icon>
<edit2-icon v-if="icon === 'rename'" size="17"></edit2-icon>
<corner-down-right-icon v-if="icon === 'move-item'" size="17"></corner-down-right-icon>
<link-icon v-if="icon === 'share'" size="17"></link-icon>
<star-icon v-if="icon === 'favourites'" size="17"></star-icon>
<folder-plus-icon v-if="icon === 'create-folder'" size="17"></folder-plus-icon>
<smile-icon v-if="icon === 'no-options'" size="17"></smile-icon>
</div>
<div class="text-label">
{{ title }}
</div>
</li>
</template>
<script>
import {
CornerDownRightIcon,
DownloadCloudIcon,
FolderPlusIcon,
LifeBuoyIcon,
Trash2Icon,
Edit2Icon,
TrashIcon,
StarIcon,
LinkIcon,
EyeIcon,
SmileIcon
} from 'vue-feather-icons'
export default {
name: 'Option',
props:['title' , 'icon'],
components: {
CornerDownRightIcon,
DownloadCloudIcon,
FolderPlusIcon,
LifeBuoyIcon,
Trash2Icon,
SmileIcon,
Edit2Icon,
TrashIcon,
LinkIcon,
StarIcon,
EyeIcon,
}
}
</script>
<style scoped lang="scss">
@import "@assets/vue-file-manager/_variables";
@import "@assets/vue-file-manager/_mixins";
.menu-option {
white-space: nowrap;
font-weight: 700;
@include font-size(14);
padding: 15px 20px;
cursor: pointer;
width: 100%;
color: $text;
display: flex;
align-items: center;
.icon {
margin-right: 20px;
line-height: 0;
}
.text-label {
@include font-size(16);
}
&:hover {
background: $light_background;
.text-label {
color: $theme;
}
path,
line,
polyline,
rect,
circle,
polygon {
stroke: $theme;
}
}
}
@media (prefers-color-scheme: dark) {
.menu-option {
color: $dark_mode_text_primary;
&:hover {
background: rgba($theme, 0.1);
}
}
}
</style>
@@ -0,0 +1,36 @@
<template>
<ul class="menu-option-group">
<slot></slot>
</ul>
</template>
<script>
export default {
name: 'OptionGroup'
}
</script>
<style scoped lang="scss" scoped>
@import "@assets/vue-file-manager/_variables";
@import "@assets/vue-file-manager/_mixins";
.menu-option-group {
padding: 5px 0;
border-bottom: 1px solid $light_mode_border;
&:first-child {
padding-top: 0;
}
&:last-child {
padding-bottom: 0;
border-bottom: none;
}
}
@media (prefers-color-scheme: dark) {
.menu-option-group {
border-color: $dark_mode_border_color;
}
}
</style>
@@ -0,0 +1,138 @@
<template>
<transition name="popup">
<div class="popup" v-if="isZippingFiles">
<div class="popup-wrapper">
<div class="popup-content">
<div class="spinner-wrapper">
<Spinner/>
</div>
<h1 class="title">{{ $t('popup_zipping.title') }}</h1>
<p class="message">{{ $t('popup_zipping.message') }}</p>
</div>
</div>
</div>
</transition>
</template>
<script>
import Spinner from '@/components/FilesView/Spinner'
import { mapGetters } from 'vuex'
export default {
name: 'ProcessingPopup',
components: {
Spinner
},
computed: {
...mapGetters([
'isZippingFiles'
])
}
}
</script>
<style scoped lang="scss">
@import '@assets/vue-file-manager/_variables';
@import '@assets/vue-file-manager/_mixins';
.spinner-wrapper {
padding-bottom: 90px;
position: relative;
}
.popup {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 20;
overflow: auto;
height: 100%;
}
.popup-wrapper {
z-index: 12;
position: absolute;
left: 0;
right: 0;
max-width: 480px;
top: 50%;
transform: translateY(-50%) scale(1);
margin: 0 auto;
padding: 20px;
box-shadow: $light_mode_popup_shadow;
border-radius: 8px;
text-align: center;
background: white;
}
.popup-content {
.title {
@include font-size(22);
font-weight: 700;
color: $text;
}
.message {
@include font-size(16);
color: #333;
margin-top: 5px;
}
}
@media only screen and (max-width: 690px) {
.popup-wrapper {
padding: 20px;
left: 15px;
right: 15px;
}
.popup-content {
.title {
@include font-size(19);
}
.message {
@include font-size(15);
}
}
}
@media (prefers-color-scheme: dark) {
.popup-wrapper {
background: $dark_mode_background;
}
.popup-content {
.title {
color: $dark_mode_text_primary;
}
.message {
color: $dark_mode_text_secondary;
}
}
}
// Animations
.popup-enter-active {
animation: popup-in 0.35s 0.15s ease both;
}
.popup-leave-active {
animation: popup-in 0.15s ease reverse;
}
@keyframes popup-in {
0% {
opacity: 0;
transform: scale(0.7);
}
100% {
opacity: 1;
transform: scale(1);
}
}
</style>
@@ -19,7 +19,7 @@ export default {
width: 100%;
height: 5px;
background: $light_background;
margin-top: 5px;
margin-top: 6px;
border-radius: 10px;
span {
@@ -0,0 +1,226 @@
<template>
<div class="menu-options" id="menu-list">
<ul class="menu-option-group">
<li v-if="isList" class="menu-option" @click="changePreview('grid')">
<div class="icon">
<grid-icon size="17"/>
</div>
<div class="text-label">
{{ $t('preview_sorting.grid_view') }}
</div>
</li>
<li v-if="isGrid" class="menu-option" @click="changePreview('list')">
<div class="icon">
<list-icon size="17"/>
</div>
<div class="text-label">
{{ $t('preview_sorting.list_view') }}
</div>
</li>
</ul>
<ul class="menu-option-group">
<li class="menu-option" @click.stop="sort('created_at')">
<div class="icon">
<calendar-icon size="17"/>
</div>
<div class="text-label">
{{ $t('preview_sorting.sort_date') }}
</div>
<div class="show-icon">
<arrow-up-icon size="17" v-if="filter.field === 'created_at'" :class="{ 'arrow-down': filter.sort === 'ASC' }"/>
</div>
</li>
<li class="menu-option" @click.stop="sort('name')">
<div class="icon">
<alphabet-icon size="17" class="alphabet-icon"/>
</div>
<div class="text-label">
{{ $t('preview_sorting.sort_alphabet') }}
</div>
<div class="show-icon">
<arrow-up-icon size="17" v-if="filter.field === 'name'" :class="{ 'arrow-down': filter.sort === 'ASC' }"/>
</div>
</li>
</ul>
</div>
</template>
<script>
import { CalendarIcon, ListIcon, GridIcon, ArrowUpIcon, CheckIcon } from 'vue-feather-icons'
import AlphabetIcon from '@/components/FilesView/Icons/AlphabetIcon'
import { mapGetters } from 'vuex'
import { events } from '@/bus'
export default {
name: 'SortingAndPreviewMenu',
components: {
CalendarIcon,
AlphabetIcon,
ArrowUpIcon,
CheckIcon,
ListIcon,
GridIcon
},
computed: {
...mapGetters(['FilePreviewType']),
isGrid() {
return this.FilePreviewType === 'grid'
},
isList() {
return this.FilePreviewType === 'list'
}
},
data() {
return {
filter: {
sort: 'DESC',
field: undefined
}
}
},
methods: {
sort(field) {
this.filter.field = field
console.log(this.filter);
// Set sorting direction
if (this.filter.sort === 'DESC')
this.filter.sort = 'ASC'
else if (this.filter.sort === 'ASC')
this.filter.sort = 'DESC'
// Save to localStorage sorting options
localStorage.setItem('sorting', JSON.stringify({ sort: this.filter.sort, field: this.filter.field }))
// Update sorting state in vuex
this.$store.commit('UPDATE_SORTING')
// Get data using the application location
this.$getDataByLocation()
},
changePreview(previewType) {
this.$store.dispatch('changePreviewType', previewType)
if (this.$isMobile())
events.$emit('mobileSortingAndPreview', false)
events.$emit('mobileSortingAndPreviewVignette', this.mobileSortingAndPreview)
}
},
mounted() {
let sorting = JSON.parse(localStorage.getItem('sorting'))
// Set default sorting if in not setup in LocalStorage
this.filter.sort = sorting ? sorting.sort : 'DESC'
this.filter.field = sorting ? sorting.field : 'created_at'
}
}
</script>
<style scoped lang="scss">
@import "@assets/vue-file-manager/_variables";
@import "@assets/vue-file-manager/_mixins";
.show-icon {
margin-left: auto;
max-height: 19px;
.arrow-down {
@include transform(rotate(180deg));
}
}
.menu-option {
display: flex;
.icon {
margin-right: 20px;
line-height: 0;
.alphabet-icon {
/deep/ line,
/deep/ polyline {
stroke: $text;
}
}
}
.text-label {
@include font-size(16);
}
}
.sorting-preview {
min-width: 250px;
position: absolute;
z-index: 99;
box-shadow: $shadow;
background: white;
border-radius: 8px;
overflow: hidden;
right: 66px;
top: 63px;
&.showed {
display: block;
}
}
.menu-options {
list-style: none;
width: 100%;
margin: 0;
padding: 0;
.menu-option-group {
padding: 5px 0;
border-bottom: 1px solid $light_mode_border;
&:first-child {
padding-top: 0;
}
&:last-child {
padding-bottom: 0;
border-bottom: none;
}
}
.menu-option {
white-space: nowrap;
font-weight: 700;
@include font-size(14);
padding: 15px 20px;
cursor: pointer;
width: 100%;
color: $text;
}
}
@media (prefers-color-scheme: dark) {
.menu-options {
.menu-option-group {
border-color: $dark_mode_border_color;
}
.menu-option {
color: $dark_mode_text_primary;
.icon {
.alphabet-icon {
/deep/ line,
/deep/ polyline {
stroke: $dark_mode_text_primary;
}
}
}
}
}
}
</style>
@@ -19,6 +19,9 @@
<info-icon v-if="source === 'info'" size="19"></info-icon>
<grid-icon v-if="source === 'th'" size="19"></grid-icon>
<link-icon v-if="source === 'share'" size="19"></link-icon>
<x-icon v-if="source === 'close'" size="19"></x-icon>
<cloud-off-icon v-if="source === 'shared-off'" size="19"></cloud-off-icon>
<sorting-and-preview-icon v-if="source === 'preview-sorting'" size="19" class="preview-sorting"></sorting-and-preview-icon>
</button>
</template>
@@ -33,20 +36,26 @@ import {
CornerDownRightIcon,
LinkIcon,
DownloadCloudIcon,
XIcon,
PrinterIcon,
CloudOffIcon,
} from "vue-feather-icons";
import SortingAndPreviewIcon from '@/components/FilesView/Icons/SortingAndPreviewIcon'
export default {
name: "ToolbarButton",
props: ["source", "action"],
components: {
SortingAndPreviewIcon,
CornerDownRightIcon,
DownloadCloudIcon,
FolderPlusIcon,
CloudOffIcon,
PrinterIcon,
Trash2Icon,
Edit2Icon,
ListIcon,
XIcon,
GridIcon,
InfoIcon,
LinkIcon,
@@ -58,6 +67,13 @@ export default {
@import "@assets/vue-file-manager/_variables";
@import "@assets/vue-file-manager/_mixins";
.preview-sorting {
svg {
width: 19px;
height: 19px;
}
}
.button {
height: 42px;
width: 42px;
@@ -74,6 +90,14 @@ export default {
@include transition(150ms);
background: transparent;
&:hover {
.preview-sorting {
path, line, polyline, rect, circle {
stroke: $theme !important;
}
}
}
&:hover {
background: $light_background;
@@ -13,21 +13,28 @@
{{ $t('uploading.progress', {current:uploadingFilesCount.current, total: uploadingFilesCount.total, progress: uploadingFileProgress}) }}
</span>
</div>
<ProgressBar :progress="uploadingFileProgress" />
<div class="progress-wrapper">
<ProgressBar :progress="uploadingFileProgress" />
<span @click="cancelUpload" :title="$t('uploading.cancel')" class="cancel-icon">
<x-icon size="16" @click="cancelUpload"></x-icon>
</span>
</div>
</div>
</transition>
</template>
<script>
import ProgressBar from '@/components/FilesView/ProgressBar'
import { RefreshCwIcon } from 'vue-feather-icons'
import { RefreshCwIcon, XIcon } from 'vue-feather-icons'
import {mapGetters} from 'vuex'
import {events} from '@/bus'
export default {
name: 'UploadProgress',
components: {
RefreshCwIcon,
ProgressBar,
XIcon,
},
computed: {
...mapGetters([
@@ -35,6 +42,11 @@
'uploadingFilesCount',
'isProcessingFile',
])
},
methods: {
cancelUpload() {
events.$emit('cancel-upload')
}
}
}
</script>
@@ -78,6 +90,22 @@
position: relative;
z-index: 1;
.progress-wrapper {
display: flex;
.cancel-icon {
cursor: pointer;
padding: 0 13px;
&:hover {
line {
stroke: $theme;
}
}
}
}
.progress-title {
font-weight: 700;
text-align: center;
@@ -88,6 +116,16 @@
}
}
@media only screen and (max-width: 690px) {
.upload-progress {
.progress-wrapper .cancel-icon {
padding: 0 9px;
}
}
}
@media (prefers-color-scheme: dark) {
.progress-bar {
background: $dark_mode_foreground;
@@ -1,14 +1,15 @@
<template>
<div @click="fileViewClick"
@contextmenu.prevent.capture="contextMenu($event, undefined)"
<div @contextmenu.prevent.capture="contextMenu($event, undefined)"
id="files-view">
<ContextMenu/>
<DesktopSortingAndPreview/>
<DesktopToolbar/>
<FileBrowser/>
</div>
</template>
<script>
import DesktopSortingAndPreview from '@/components/FilesView/DesktopSortingAndPreview'
import DesktopToolbar from '@/components/FilesView/DesktopToolbar'
import FileBrowser from '@/components/FilesView/FileBrowser'
import ContextMenu from '@/components/FilesView/ContextMenu'
@@ -18,6 +19,7 @@
export default {
name: 'FilesView',
components: {
DesktopSortingAndPreview,
DesktopToolbar,
FileBrowser,
ContextMenu,
@@ -26,9 +28,6 @@
...mapGetters(['config']),
},
methods: {
fileViewClick() {
events.$emit('contextMenu:hide')
},
contextMenu(event, item) {
events.$emit('contextMenu:show', event, item)
},
@@ -0,0 +1,95 @@
<template>
<PopupWrapper name="create-folder">
<!--Title-->
<PopupHeader :title="$t('popup_create_folder.title')" icon="edit" />
<!--Content-->
<PopupContent>
<!--Form to set sharing-->
<ValidationObserver @submit.prevent="createFolder" ref="createForm" v-slot="{ invalid }" tag="form" class="form-wrapper">
<!--Set password-->
<ValidationProvider tag="div" mode="passive" class="input-wrapper password" name="Title" rules="required" v-slot="{ errors }">
<label class="input-label">{{ $t('popup_create_folder.label') }}:</label>
<input v-model="name" :class="{'is-error': errors[0]}" type="text" :placeholder="$t('popup_create_folder.placeholder')">
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</ValidationObserver>
</PopupContent>
<!--Actions-->
<PopupActions>
<ButtonBase
class="popup-button"
@click.native="$closePopup()"
button-style="secondary"
>{{ $t('popup_move_item.cancel') }}
</ButtonBase>
<ButtonBase
class="popup-button"
@click.native="createFolder"
button-style="theme"
>{{ $t('popup_create_folder.title') }}
</ButtonBase>
</PopupActions>
</PopupWrapper>
</template>
<script>
import {ValidationProvider, ValidationObserver} from 'vee-validate/dist/vee-validate.full'
import PopupWrapper from '@/components/Others/Popup/PopupWrapper'
import PopupActions from '@/components/Others/Popup/PopupActions'
import PopupContent from '@/components/Others/Popup/PopupContent'
import PopupHeader from '@/components/Others/Popup/PopupHeader'
import ThumbnailItem from '@/components/Others/ThumbnailItem'
import ActionButton from '@/components/Others/ActionButton'
import ButtonBase from '@/components/FilesView/ButtonBase'
import {required} from 'vee-validate/dist/rules'
import {events} from '@/bus'
import axios from 'axios'
export default {
name: 'CreateFolder',
components: {
ValidationProvider,
ValidationObserver,
ThumbnailItem,
ActionButton,
PopupWrapper,
PopupActions,
PopupContent,
PopupHeader,
ButtonBase,
required,
},
data() {
return {
name: undefined,
}
},
methods: {
async createFolder() {
// Validate fields
const isValid = await this.$refs.createForm.validate();
if (isValid) {
this.$store.dispatch('createFolder', this.name)
this.$closePopup()
}
},
},
}
</script>
<style scoped lang="scss">
@import "@assets/vue-file-manager/_inapp-forms.scss";
@import '@assets/vue-file-manager/_forms';
.item-thumbnail {
margin-bottom: 20px;
}
</style>
@@ -1,5 +1,9 @@
<template>
<div class="dropzone" :class="{ 'is-error': error }">
<div v-if="imagePreview" @click="resetImage" class="reset-image">
<x-icon size="14" class="close-icon"></x-icon>
</div>
<input
ref="file"
type="file"
@@ -26,7 +30,7 @@
</template>
<script>
import ImageIcon from "vue-feather-icons/icons/ImageIcon";
import { XIcon, ImageIcon } from 'vue-feather-icons'
export default {
name: 'ImageInput',
@@ -35,6 +39,7 @@
],
components: {
ImageIcon,
XIcon,
},
data() {
return {
@@ -47,6 +52,10 @@
},
},
methods: {
resetImage() {
this.imagePreview = undefined
this.$emit('input', undefined)
},
showImagePreview(event) {
const imgPath = event.target.files[0].name,
extn = imgPath
@@ -152,6 +161,30 @@
@include font-size(12);
}
}
.reset-image {
z-index: 2;
background: white;
border-radius: 50px;
display: block;
position: absolute;
right: 0;
top: 0;
cursor: pointer;
@include transform(translateY(-50%) translateX(50%));
padding: 0px 4px;
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.12);
.close-icon {
vertical-align: middle;
line {
path {
fill: $text;
}
}
}
}
}
@media (prefers-color-scheme: dark) {
@@ -58,7 +58,7 @@
{
icon: 'trash',
title: this.$t('menu.trash'),
routeName: 'Trash',
routeName: 'Files',
isVisible: true,
},
{
@@ -94,6 +94,10 @@
this.$store.dispatch('getLatest')
}
if (name === 'trash') {
this.$store.dispatch('getTrash')
}
if (name === 'hard-drive') {
this.$store.dispatch('getFolder', [{folder: this.homeDirectory, back: false, init: true}])
}
+67 -8
View File
@@ -11,8 +11,14 @@
<!--Folder tree-->
<div v-if="! isLoadingTree && navigation">
<ThumbnailItem class="item-thumbnail" :item="pickedItem" info="location"/>
<TreeMenu :disabled-by-id="pickedItem.unique_id" :depth="1" :nodes="items" v-for="items in navigation" :key="items.unique_id"/>
<ThumbnailItem v-if="fileInfoDetail.length < 2 || noSelectedItem" class="item-thumbnail" :item="pickedItem" info="location"/>
<MultiSelected class="multiple-selected"
:title="$t('file_detail.selected_multiple')"
:subtitle="this.fileInfoDetail.length + ' ' + $tc('file_detail.items', this.fileInfoDetail.length)"
v-if="fileInfoDetail.length > 1 && !noSelectedItem"/>
<TreeMenu :disabled-by-id="pickedItem" :depth="1" :nodes="items" v-for="items in navigation" :key="items.unique_id"/>
</div>
</PopupContent>
@@ -37,6 +43,7 @@
<script>
import PopupWrapper from '@/components/Others/Popup/PopupWrapper'
import PopupActions from '@/components/Others/Popup/PopupActions'
import MultiSelected from '@/components/FilesView/MultiSelected'
import PopupContent from '@/components/Others/Popup/PopupContent'
import PopupHeader from '@/components/Others/Popup/PopupHeader'
import ThumbnailItem from '@/components/Others/ThumbnailItem'
@@ -51,6 +58,7 @@
components: {
ThumbnailItem,
PopupWrapper,
MultiSelected,
PopupActions,
PopupContent,
PopupHeader,
@@ -59,26 +67,39 @@
Spinner,
},
computed: {
...mapGetters(['navigation']),
...mapGetters(['navigation', 'fileInfoDetail']),
},
data() {
return {
selectedFolder: undefined,
pickedItem: undefined,
isLoadingTree: true,
noSelectedItem: false
}
},
methods: {
moveItem() {
// Prevent empty submit
if (! this.selectedFolder) return
// Move item
this.$store.dispatch('moveItem', [this.pickedItem, this.selectedFolder])
//Prevent to move items to the same parent
if(this.fileInfoDetail.find(item => item.parent_id === this.selectedFolder.unique_id)) return
// Move item
if(!this.noSelectedItem){
this.$store.dispatch('moveItem', {to_item:this.selectedFolder ,noSelectedItem: null})
}
if(this.noSelectedItem){
this.$store.dispatch('moveItem', {to_item:this.selectedFolder ,noSelectedItem:this.pickedItem})
}
// Close popup
events.$emit('popup:close')
// If is mobile, close the selecting mod after done the move action
if(this.$isMobile())
events.$emit('mobileSelecting:stop')
},
},
mounted() {
@@ -97,7 +118,6 @@
events.$on('popup:open', args => {
if (args.name !== 'move') return
// Show tree spinner
this.isLoadingTree = true
@@ -107,7 +127,14 @@
})
// Store picked item
this.pickedItem = args.item
if(!this.fileInfoDetail.includes(args.item[0])){
this.pickedItem = args.item[0]
this.noSelectedItem = true
}
if(this.fileInfoDetail.includes(args.item[0])){
this.pickedItem = this.fileInfoDetail[0]
this.noSelectedItem = false
}
})
// Close popup
@@ -123,8 +150,40 @@
</script>
<style scoped lang="scss">
@import '@assets/vue-file-manager/_variables';
@import '@assets/vue-file-manager/_mixins';
.item-thumbnail {
margin-bottom: 20px;
}
.multiple-selected {
padding: 0 20px;;
margin-bottom: 20px;
/deep/.text{
.title {
color: $text;
}
.count {
color: $text-muted;
}
}
/deep/.icon-wrapper {
.icon {
stroke: $theme;
}
}
}
@media (prefers-color-scheme: dark) {
.multiple-selected {
/deep/.text {
.title {
color: $dark_mode_text_primary;
}
.count {
color: $dark_mode_text_secondary;
}
}
}
}
</style>
@@ -3,6 +3,7 @@
<div class="icon">
<corner-down-right-icon v-if="icon === 'move'" size="15" class="title-icon"></corner-down-right-icon>
<link-icon v-if="icon === 'share'" size="17" class="title-icon"></link-icon>
<edit2-icon v-if="icon === 'edit'" size="17" class="title-icon"></edit2-icon>
</div>
<div class="label">
<h1 class="title">{{ title }}</h1>
@@ -12,7 +13,7 @@
</template>
<script>
import {CornerDownRightIcon, LinkIcon, XIcon} from 'vue-feather-icons'
import {CornerDownRightIcon, LinkIcon, XIcon, Edit2Icon} from 'vue-feather-icons'
import {events} from '@/bus'
export default {
@@ -22,6 +23,7 @@
],
components: {
CornerDownRightIcon,
Edit2Icon,
LinkIcon,
XIcon,
},
@@ -43,11 +43,7 @@
})
// Close popup
events.$on('popup:close', () => {
// Close popup
this.isVisibleWrapper = false
})
events.$on('popup:close', () => this.isVisibleWrapper = false)
}
}
</script>
@@ -0,0 +1,120 @@
<template>
<PopupWrapper name="rename-item">
<!--Title-->
<PopupHeader :title="$t('popup_rename.title', {item: itemTypeTitle})" icon="edit" />
<!--Content-->
<PopupContent>
<!--Item Thumbnail-->
<ThumbnailItem class="item-thumbnail" :item="pickedItem" info="metadata"/>
<!--Form to set sharing-->
<ValidationObserver @submit.prevent="changeName" ref="renameForm" v-slot="{ invalid }" tag="form" class="form-wrapper">
<!--Set password-->
<ValidationProvider tag="div" mode="passive" class="input-wrapper password" name="Name" rules="required" v-slot="{ errors }">
<label class="input-label">{{ $t('popup_rename.label') }}:</label>
<input v-model="pickedItem.name" :class="{'is-error': errors[0]}" type="text" :placeholder="$t('popup_rename.placeholder')">
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</ValidationObserver>
</PopupContent>
<!--Actions-->
<PopupActions>
<ButtonBase
class="popup-button"
@click.native="$closePopup()"
button-style="secondary"
>{{ $t('popup_move_item.cancel') }}
</ButtonBase>
<ButtonBase
class="popup-button"
@click.native="changeName"
button-style="theme"
>{{ $t('popup_share_edit.save') }}
</ButtonBase>
</PopupActions>
</PopupWrapper>
</template>
<script>
import {ValidationProvider, ValidationObserver} from 'vee-validate/dist/vee-validate.full'
import PopupWrapper from '@/components/Others/Popup/PopupWrapper'
import PopupActions from '@/components/Others/Popup/PopupActions'
import PopupContent from '@/components/Others/Popup/PopupContent'
import PopupHeader from '@/components/Others/Popup/PopupHeader'
import ThumbnailItem from '@/components/Others/ThumbnailItem'
import ActionButton from '@/components/Others/ActionButton'
import ButtonBase from '@/components/FilesView/ButtonBase'
import {required} from 'vee-validate/dist/rules'
import {events} from '@/bus'
import axios from 'axios'
export default {
name: 'RenameItem',
components: {
ValidationProvider,
ValidationObserver,
ThumbnailItem,
ActionButton,
PopupWrapper,
PopupActions,
PopupContent,
PopupHeader,
ButtonBase,
required,
},
computed: {
itemTypeTitle() {
return this.pickedItem && this.pickedItem.type === 'folder' ? this.$t('types.folder') : this.$t('types.file')
},
},
data() {
return {
pickedItem: undefined,
}
},
methods: {
changeName() {
if (this.pickedItem.name && this.pickedItem.name !== '') {
let item = {
unique_id: this.pickedItem.unique_id,
type: this.pickedItem.type,
name: this.pickedItem.name
}
// Rename item request
this.$store.dispatch('renameItem', item)
// Rename item in view
events.$emit('change:name', item)
this.$closePopup()
}
},
},
mounted() {
// Show popup
events.$on('popup:open', args => {
if (args.name !== 'rename-item') return
// Store picked item
this.pickedItem = args.item
})
}
}
</script>
<style scoped lang="scss">
@import "@assets/vue-file-manager/_inapp-forms.scss";
@import '@assets/vue-file-manager/_forms';
.item-thumbnail {
margin-bottom: 20px;
}
</style>
+3 -12
View File
@@ -158,7 +158,7 @@
changePassword() {
this.canChangePassword = false
},
destroySharing() {
async destroySharing() {
// Set confirm button
if (! this.isConfirmedDestroy) {
@@ -170,18 +170,9 @@
this.isDeleting = true
// Send delete request
axios
.post('/api/share/' + this.pickedItem.shared.token, {
_method: 'delete'
})
.then(() => {
// Remove item from file browser
if ( this.isSharedLocation ) {
this.$store.commit('REMOVE_ITEM', this.pickedItem.unique_id)
}
// Flush shared data
this.$store.commit('FLUSH_SHARED', this.pickedItem.unique_id)
await this.$store.dispatch('shareCancel' , this.pickedItem)
.then((response) => {
// End deleting spinner button
setTimeout(() => this.isDeleting = false, 150)
+14 -1
View File
@@ -1,6 +1,6 @@
<template>
<!--Folder Icon-->
<div class="folder-item-wrapper" :class="{'is-inactive': disabledById && disabledById === nodes.unique_id}">
<div class="folder-item-wrapper" :class="{'is-inactive': disabledById && disabledById.unique_id === nodes.unique_id || !disableId} ">
<div class="folder-item" :class="{'is-selected': isSelected}" @click="getFolder" :style="indent">
<chevron-right-icon @click.stop="showTree" size="17" class="icon-arrow" :class="{'is-opened': isVisible, 'is-visible': nodes.folders.length !== 0}"></chevron-right-icon>
@@ -17,6 +17,7 @@
import TreeMenu from '@/components/Others/TreeMenu'
import {FolderIcon, ChevronRightIcon, HardDriveIcon} from 'vue-feather-icons'
import {events} from "@/bus"
import {mapGetters} from 'vuex'
export default {
name: 'TreeMenu',
@@ -30,9 +31,21 @@
TreeMenu,
},
computed: {
...mapGetters(['fileInfoDetail']),
indent() {
return { paddingLeft: this.depth * 20 + 'px' }
},
disableId() {
let canBeShow = true
if(this.fileInfoDetail.includes(this.disabledById)){
this.fileInfoDetail.map(item => {
if(item.unique_id === this.nodes.unique_id) {
canBeShow = false
}
})
}
return canBeShow
}
},
data() {
return {
@@ -1,15 +1,21 @@
<template>
<transition name="folder">
<div class="folder-item-wrapper">
<div class="folder-item-wrapper" >
<div class="folder-item" :class="{'is-selected': isSelected}" :style="indent" @click="getFolder">
<div class="folder-item" :class="{'is-selected': isSelected , 'is-dragenter': area, 'is-inactive': disabledFolder || disabled && draggedItem.length > 0 }"
:style="indent" @click="getFolder"
@dragover.prevent="dragEnter"
@dragleave="dragLeave"
@drop="dragFinish()"
>
<chevron-right-icon @click.stop="showTree" size="17" class="icon-arrow"
:class="{'is-opened': isVisible, 'is-visible': nodes.folders.length !== 0}"></chevron-right-icon>
<folder-icon size="17" class="icon"></folder-icon>
<span class="label">{{ nodes.name }}</span>
</div>
<TreeMenuNavigator :depth="depth + 1" v-if="isVisible" :nodes="item" v-for="item in nodes.folders"
<TreeMenuNavigator :disabled="disableChildren" :depth="depth + 1" v-if="isVisible" :nodes="item" v-for="item in nodes.folders"
:key="item.unique_id"/>
</div>
</transition>
@@ -18,12 +24,13 @@
<script>
import TreeMenuNavigator from '@/components/Others/TreeMenuNavigator'
import {FolderIcon, ChevronRightIcon} from 'vue-feather-icons'
import { mapGetters } from 'vuex'
import {events} from "@/bus"
export default {
name: 'TreeMenuNavigator',
props: [
'nodes', 'depth'
'nodes', 'depth' , 'disabled',
],
components: {
TreeMenuNavigator,
@@ -31,6 +38,32 @@
FolderIcon,
},
computed: {
...mapGetters(['fileInfoDetail']),
disabledFolder() {
let disableFolder = false
if(this.draggedItem.length > 0) {
this.draggedItem.forEach(item => {
//Disable the parent of the folder
if(item.type === "folder" && this.nodes.unique_id === item.parent_id){
disableFolder = true
}
//Disable the self folder with all children
if (this.nodes.unique_id === item.unique_id && item.type === 'folder') {
disableFolder = true
this.disableChildren = true
}
if(this.disabled) {
this.disableChildren = true
}
})
}else {
disableFolder = false
this.disableChildren = false
}
return disableFolder
},
indent() {
let offset = window.innerWidth <= 1024 ? 17 : 22;
@@ -44,15 +77,43 @@
return {
isVisible: false,
isSelected: false,
area:false,
draggedItem:[],
disableChildren:false,
}
},
methods: {
getFolder() {
dragFinish() {
// Move no selected item
if(!this.fileInfoDetail.includes(this.draggedItem[0])) {
this.$store.dispatch('moveItem', {to_item: this.nodes ,noSelectedItem:this.draggedItem[0]})
}
// Move all selected items
if(this.fileInfoDetail.includes(this.draggedItem[0])) {
this.$store.dispatch('moveItem', {to_item: this.nodes ,noSelectedItem:null})
}
this.draggedItem = []
this.area = false
events.$emit('drop')
},
dragEnter() {
this.area = true
},
dragLeave() {
this.area = false
},
getFolder() {
events.$emit('show-folder', this.nodes)
// Get folder content
this.$store.dispatch('getFolder', [{folder: this.nodes, back: false, init: false}])
// Go to folder
if (this.$isThisLocation('public')) {
this.$store.dispatch('browseShared', [{ folder: this.nodes, back: false, init: false }])
} else {
this.$store.dispatch('getFolder', [{ folder: this.nodes, back: false, init: false }])
}
},
showTree() {
this.isVisible = !this.isVisible
@@ -60,6 +121,22 @@
},
created() {
events.$on('drop' , () => {
this.draggedItem = []
})
//Get dragged item
events.$on('dragstart' , (data) => {
//If is dragged item not selected
if(!this.fileInfoDetail.includes(data)) {
this.draggedItem = [data]
}
//If are the dragged items selected
if(this.fileInfoDetail.includes(data)) {
this.draggedItem = this.fileInfoDetail
}
})
// Select clicked folder
events.$on('show-folder', node => {
this.isSelected = false
@@ -75,6 +152,16 @@
@import '@assets/vue-file-manager/_variables';
@import '@assets/vue-file-manager/_mixins';
.is-inactive {
opacity: 0.5;
pointer-events: none;
}
.is-dragenter {
border: 2px dashed $theme !important;
border-radius: 8px;
}
.folder-item {
display: block;
padding: 8px 0;
@@ -83,6 +170,7 @@
position: relative;
white-space: nowrap;
width: 100%;
border: 2px dashed transparent ;
.icon {
line-height: 0;
+13 -1
View File
@@ -1,14 +1,23 @@
<template>
<transition name="vignette">
<div v-if="isVisibleVignette" class="vignette" @click="closePopup"></div>
<div v-if="isVisible" class="vignette" @click="closePopup"></div>
</transition>
</template>
<script>
import {events} from '@/bus'
import { mapGetters } from 'vuex'
export default {
name: 'Vignette',
computed: {
...mapGetters([
'isZippingFiles'
]),
isVisible() {
return this.isZippingFiles || this.isVisibleVignette
},
},
data() {
return {
isVisibleVignette: false,
@@ -18,6 +27,7 @@
closePopup() {
events.$emit('popup:close')
events.$emit('mobileMenu:hide')
events.$emit('mobileSortingAndPreview', false)
}
},
created() {
@@ -30,6 +40,8 @@
events.$on('alert:open', () => this.isVisibleVignette = true)
events.$on('success:open', () => this.isVisibleVignette = true)
events.$on('confirm:open', () => this.isVisibleVignette = true)
events.$on('mobileSortingAndPreviewVignette', (state) => this.isVisibleVignette = state)
}
}
</script>
@@ -1,25 +1,109 @@
<template>
<div class="content-group">
<TextLabel>{{ title }}</TextLabel>
<slot></slot>
<div class="content-group" :class="{'is-collapsed': ! isVisible, 'collapsable': canCollapse}">
<div class="group-title" @click="hideGroup">
<TextLabel class="title">{{ title }}</TextLabel>
<chevron-up-icon v-if="canCollapseWrapper" size="12" class="icon" />
</div>
<transition name="list">
<div class="wrapper" v-show="isVisible">
<slot></slot>
</div>
</transition>
</div>
</template>
<script>
import TextLabel from '@/components/Others/TextLabel'
import { ChevronUpIcon } from 'vue-feather-icons'
export default {
name: 'ContentGroup',
props: ['title'],
props: ['title', 'canCollapse', 'slug'],
components: {
ChevronUpIcon,
TextLabel,
},
data() {
return {
isVisible: true,
canCollapseWrapper: false
}
},
methods: {
hideGroup() {
if (! this.canCollapseWrapper)
return
this.isVisible = !this.isVisible
localStorage.setItem('panel-group-' + this.slug, this.isVisible)
}
},
created() {
if (this.canCollapse) {
let savedVisibility = localStorage.getItem('panel-group-' + this.slug)
this.isVisible = savedVisibility ? !!JSON.parse(String(savedVisibility).toLowerCase()) : true
this.canCollapseWrapper = true
}
}
}
</script>
<style scoped lang="scss">
@import '@assets/vue-file-manager/_mixins';
.content-group {
margin-bottom: 30px;
transition: all 300ms;
.group-title {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 5px;
.title {
margin-bottom: 0;
}
.icon {
margin-right: 19px;
opacity: 0.25;
@include transition;
}
}
&.collapsable {
.group-title {
cursor: pointer;
}
}
&.is-collapsed {
margin-bottom: 15px;
.icon {
@include transform(rotate(180deg));
}
}
}
.list-enter,
.list-leave-to {
visibility: hidden;
height: 0;
margin: 0;
padding: 0;
opacity: 0;
}
.list-enter-active,
.list-leave-active {
transition: all 300ms;
}
</style>
+3 -9
View File
@@ -20,19 +20,13 @@
</div>
</router-link>
<router-link :to="{name: 'Trash'}" :title="$t('locations.trash')" class="icon-navigation-item trash">
<div class="button-icon">
<trash-2-icon size="19"></trash-2-icon>
</div>
</router-link>
<router-link :to="{name: 'Profile'}" :class="{'is-active': isUserProfileRoute}" class="icon-navigation-item settings">
<router-link :to="{name: 'Profile'}" :class="{'is-active': isUserProfileRoute}" :title="$t('locations.profile')" class="icon-navigation-item settings">
<div class="button-icon">
<user-icon size="19"></user-icon>
</div>
</router-link>
<router-link v-if="user.data.attributes.role === 'admin'" :to="{name: 'Dashboard'}" :class="{'is-active': $isThisRoute($route, adminRoutes)}" class="icon-navigation-item users">
<router-link v-if="user.data.attributes.role === 'admin'" :to="{name: 'Dashboard'}" :class="{'is-active': $isThisRoute($route, adminRoutes)}" :title="$t('locations.settings')" class="icon-navigation-item users">
<div class="button-icon">
<settings-icon size="19"></settings-icon>
</div>
@@ -41,7 +35,7 @@
<!--User avatar & Logout-->
<ul class="icon-navigation logout">
<li @click="$store.dispatch('logOut')" class="icon-navigation-item">
<li @click="$store.dispatch('logOut')" :title="$t('locations.logout')" class="icon-navigation-item">
<div class="button-icon">
<power-icon size="19"></power-icon>
</div>
+296 -289
View File
@@ -1,328 +1,335 @@
import i18n from '@/i18n/index'
import store from './store/index'
import {debounce, includes} from "lodash";
import {events} from './bus'
import { debounce, includes } from 'lodash'
import { events } from './bus'
import axios from 'axios'
import router from '@/router'
const Helpers = {
install(Vue) {
install(Vue) {
Vue.prototype.$updateText = debounce(function (route, name, value) {
let enableEmptyInput = ['mimetypes_blacklist' , 'google_analytics' , 'upload_limit']
if (value === '' && !enableEmptyInput.includes(name)) return
axios.post(this.$store.getters.api + route, {name, value, _method: 'patch'})
.catch(error => {
events.$emit('alert:open', {
title: this.$t('popup_error.title'),
message: this.$t('popup_error.message'),
})
})
}, 150)
Vue.prototype.$updateImage = function (route, name, image) {
// Create form
let formData = new FormData()
// Add image to form
formData.append('name', name)
formData.append(name, image)
formData.append('_method', 'PATCH')
axios.post(this.$store.getters.api + route, formData, {
headers: {
'Content-Type': 'multipart/form-data',
}
})
.catch(error => {
events.$emit('alert:open', {
title: this.$t('popup_error.title'),
message: this.$t('popup_error.message'),
})
})
}
Vue.prototype.$scrollTop = function () {
var container = document.getElementById('vue-file-manager')
if (container) {
container.scrollTop = 0
}
}
Vue.prototype.$getImage = function (source) {
return source ? this.$store.getters.config.host + '/' + source : ''
}
Vue.prototype.$getCreditCardBrand = function (brand) {
return `/assets/icons/${brand}.svg`
}
Vue.prototype.$getInvoiceLink = function (customer, id) {
return '/invoice/' + customer + '/' + id
}
Vue.prototype.$openImageOnNewTab = function (source) {
let win = window.open(source, '_blank')
win.focus()
}
Vue.prototype.$createFolder = function (folderName) {
this.$store.dispatch('createFolder', folderName)
}
Vue.prototype.$handleUploading = async function (files, parent_id) {
let fileBuffer = []
// Append the file list to fileBuffer array
Array.prototype.push.apply(fileBuffer, files);
let fileSucceed = 0
// Update files count in progressbar
store.commit('UPDATE_FILE_COUNT_PROGRESS', {
current: fileSucceed,
total: files.length
})
// Reset upload progress to 0
store.commit('UPLOADING_FILE_PROGRESS', 0)
// Get parent id
let parentFolder = this.$store.getters.currentFolder ? this.$store.getters.currentFolder.unique_id : 0
let rootFolder = parent_id ? parent_id : parentFolder
Vue.prototype.$updateText = debounce(function(route, name, value) {
// Upload files
do {
let file = fileBuffer.shift(),
chunks = []
let enableEmptyInput = ['mimetypes_blacklist', 'google_analytics']
// Calculate ceils
let size = this.$store.getters.config.chunkSize,
chunksCeil = Math.ceil(file.size / size);
if (value === '' && !enableEmptyInput.includes(name)) return
axios.post(this.$store.getters.api + route, { name, value, _method: 'patch' })
.catch(error => {
events.$emit('alert:open', {
title: this.$t('popup_error.title'),
message: this.$t('popup_error.message')
})
})
}, 150)
Vue.prototype.$updateImage = function(route, name, image) {
// Create form
let formData = new FormData()
// Add image to form
formData.append('name', name)
formData.append(name, image)
formData.append('_method', 'PATCH')
axios.post(this.$store.getters.api + route, formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
.catch(error => {
events.$emit('alert:open', {
title: this.$t('popup_error.title'),
message: this.$t('popup_error.message')
})
})
}
Vue.prototype.$scrollTop = function() {
var container = document.getElementById('vue-file-manager')
if (container) {
container.scrollTop = 0
}
}
Vue.prototype.$getImage = function(source) {
return source ? this.$store.getters.config.host + '/' + source : ''
}
Vue.prototype.$getCreditCardBrand = function(brand) {
return `/assets/icons/${brand}.svg`
}
Vue.prototype.$getInvoiceLink = function(customer, id) {
return '/invoice/' + customer + '/' + id
}
Vue.prototype.$openImageOnNewTab = function(source) {
let win = window.open(source, '_blank')
win.focus()
}
Vue.prototype.$handleUploading = async function(files, parent_id) {
let fileBuffer = []
// Create chunks
for (let i = 0; i < chunksCeil; i++) {
chunks.push(file.slice(
i * size, Math.min(i * size + size, file.size), file.type
));
}
// Append the file list to fileBuffer array
Array.prototype.push.apply(fileBuffer, files)
// Set Data
let formData = new FormData(),
uploadedSize = 0,
isNotGeneralError = true,
striped_name = file.name.replace(/[^A-Za-z 0-9 \.,\?""!@#\$%\^&\*\(\)-_=\+;:<>\/\\\|\}\{\[\]`~]*/g, ''),
filename = Array(16).fill(0).map(x => Math.random().toString(36).charAt(2)).join('') + '-' + striped_name + '.part'
let fileSucceed = 0
do {
let isLast = chunks.length === 1,
chunk = chunks.shift(),
attempts = 0
// Update files count in progressbar
store.commit('UPDATE_FILE_COUNT_PROGRESS', {
current: fileSucceed,
total: files.length
})
// Set form data
formData.set('file', chunk, filename);
formData.set('parent_id', rootFolder)
formData.set('is_last', isLast);
// Reset upload progress to 0
store.commit('UPLOADING_FILE_PROGRESS', 0)
// Upload chunks
do {
await store.dispatch('uploadFiles', {
form: formData,
fileSize: file.size,
totalUploadedSize: uploadedSize
}).then(() => {
uploadedSize = uploadedSize + chunk.size
}).catch((error) => {
// Get parent id
let parentFolder = this.$store.getters.currentFolder ? this.$store.getters.currentFolder.unique_id : 0
let rootFolder = parent_id ? parent_id : parentFolder
// Count attempts
attempts++
// Upload files
do {
let file = fileBuffer.shift(),
chunks = []
// Break uploading proccess
if (error.response.status === 500)
isNotGeneralError = false
// Calculate ceils
let size = this.$store.getters.config.chunkSize,
chunksCeil = Math.ceil(file.size / size)
//Break if mimetype of file is in blacklist or file size exceed upload limit
if(error.response.status === 415 || 413)
isNotGeneralError = false
// Create chunks
for (let i = 0; i < chunksCeil; i++) {
chunks.push(file.slice(
i * size, Math.min(i * size + size, file.size), file.type
))
}
// Show Error
if (attempts === 3)
this.$isSomethingWrong()
})
} while (isNotGeneralError && attempts !== 0 && attempts !== 3)
// Set Data
let formData = new FormData(),
uploadedSize = 0,
isNotGeneralError = true,
striped_name = file.name.replace(/[^A-Za-z 0-9 \.,\?""!@#\$%\^&\*\(\)-_=\+;:<>\/\\\|\}\{\[\]`~]*/g, ''),
filename = Array(16).fill(0).map(x => Math.random().toString(36).charAt(2)).join('') + '-' + striped_name + '.part'
} while (isNotGeneralError && chunks.length !== 0)
do {
let isLast = chunks.length === 1,
chunk = chunks.shift(),
attempts = 0
fileSucceed++
// Set form data
formData.set('file', chunk, filename)
formData.set('parent_id', rootFolder)
formData.set('is_last', isLast)
// Progress file log
store.commit('UPDATE_FILE_COUNT_PROGRESS', {
current: fileSucceed,
total: files.length
})
// Upload chunks
do {
await store.dispatch('uploadFiles', {
form: formData,
fileSize: file.size,
totalUploadedSize: uploadedSize
}).then(() => {
uploadedSize = uploadedSize + chunk.size
}).catch((error) => {
} while (fileBuffer.length !== 0)
// Count attempts
attempts++
store.commit('UPDATE_FILE_COUNT_PROGRESS', undefined)
}
// Break uploading proccess
if (error.response.status === 500)
isNotGeneralError = false
Vue.prototype.$uploadFiles = async function (files) {
//Break if mimetype of file is in blacklist
if (error.response.status === 415)
isNotGeneralError = false
if (files.length == 0) return
// Show Error
if (attempts === 3)
this.$isSomethingWrong()
})
} while (isNotGeneralError && attempts !== 0 && attempts !== 3)
if (!this.$checkFileMimetype(files) || !this.$checkUploadLimit(files)) return
this.$handleUploading(files, undefined)
}
} while (isNotGeneralError && chunks.length !== 0)
Vue.prototype.$uploadExternalFiles = async function (event, parent_id) {
fileSucceed++
// Prevent submit empty files
if (event.dataTransfer.items.length == 0) return
// Progress file log
store.commit('UPDATE_FILE_COUNT_PROGRESS', {
current: fileSucceed,
total: files.length
})
// Get files
let files = [...event.dataTransfer.items].map(item => item.getAsFile());
} while (fileBuffer.length !== 0)
this.$handleUploading(files, parent_id)
}
store.commit('UPDATE_FILE_COUNT_PROGRESS', undefined)
}
Vue.prototype.$downloadFile = function (url, filename) {
var anchor = document.createElement('a')
Vue.prototype.$uploadFiles = async function(files) {
anchor.href = url
if (files.length == 0) return
anchor.download = filename
if (!this.$checkFileMimetype(files)) return
document.body.appendChild(anchor)
this.$handleUploading(files, undefined)
}
anchor.click()
}
Vue.prototype.$closePopup = function () {
events.$emit('popup:close')
}
Vue.prototype.$isThisRoute = function (route, locations) {
return includes(locations, route.name)
}
Vue.prototype.$isThisLocation = function (location) {
// Get current location
let currentLocation = store.getters.currentFolder && store.getters.currentFolder.location ? store.getters.currentFolder.location : undefined
// Check if type is object
if (typeof location === 'Object' || location instanceof Object) {
return includes(location, currentLocation)
} else {
return currentLocation === location
}
}
Vue.prototype.$checkPermission = function (type) {
let currentPermission = store.getters.permission
// Check if type is object
if (typeof type === 'Object' || type instanceof Object) {
return includes(type, currentPermission)
} else {
return currentPermission === type
}
}
Vue.prototype.$uploadExternalFiles = async function(event, parent_id) {
Vue.prototype.$isMobile = function () {
const toMatch = [
/Android/i,
/webOS/i,
/iPhone/i,
/iPad/i,
/iPod/i,
/BlackBerry/i,
/Windows Phone/i
]
return toMatch.some(toMatchItem => {
return navigator.userAgent.match(toMatchItem)
})
}
// Prevent submit empty files
if (event.dataTransfer.items.length == 0) return
Vue.prototype.$isMinimalScale = function () {
let sizeType = store.getters.filesViewWidth
return sizeType === 'minimal-scale'
}
Vue.prototype.$isCompactScale = function () {
let sizeType = store.getters.filesViewWidth
return sizeType === 'compact-scale'
}
Vue.prototype.$isFullScale = function () {
let sizeType = store.getters.filesViewWidth
return sizeType === 'full-scale'
}
Vue.prototype.$isSomethingWrong = function () {
events.$emit('alert:open', {
title: this.$t('popup_error.title'),
message: this.$t('popup_error.message'),
})
}
Vue.prototype.$checkFileMimetype = function(files) {
let validated = true
let mimetypesBlacklist = store.getters.config.mimetypesBlacklist
for (let i = 0 ; i<files.length; i++ ) {
let fileType = files[i].type.split('/')
if(!fileType[0]) {
fileType[1] = _.last(files[i].name.split('.'))
}
if(mimetypesBlacklist.includes(fileType[1])) {
validated = false
events.$emit('alert:open', {
emoji: '😬😬😬',
title: i18n.t('popup_mimetypes_blacklist.title'),
message: i18n.t('popup_mimetypes_blacklist.message', {mimetype: fileType[1]}),
})
}
}
return validated
}
Vue.prototype.$checkUploadLimit = function (files) {
let uploadLimit = store.getters.config.uploadLimit
let validate = true
for (let i = 0 ; i<files.length; i++ ) {
if(uploadLimit != 0 && files[i].size > uploadLimit) {
validate = false
events.$emit('alert:open', {
emoji: '😟😟😟',
title: i18n.t('popup_upload_limit.title'),
message: i18n.t('popup_upload_limit.message', {uploadLimit: store.getters.config.uploadLimitFormatted}),
})
break
}
}
return validate
}
}
// Get files
let files = [...event.dataTransfer.items].map(item => item.getAsFile())
this.$handleUploading(files, parent_id)
}
Vue.prototype.$downloadFile = function(url, filename) {
var anchor = document.createElement('a')
anchor.href = url
anchor.download = filename
document.body.appendChild(anchor)
anchor.click()
}
Vue.prototype.$closePopup = function() {
events.$emit('popup:close')
}
Vue.prototype.$isThisRoute = function(route, locations) {
return includes(locations, route.name)
}
Vue.prototype.$isThisLocation = function(location) {
// Get current location
let currentLocation = store.getters.currentFolder && store.getters.currentFolder.location ? store.getters.currentFolder.location : undefined
// Check if type is object
if (typeof location === 'Object' || location instanceof Object) {
return includes(location, currentLocation)
} else {
return currentLocation === location
}
}
Vue.prototype.$checkPermission = function(type) {
let currentPermission = store.getters.permission
// Check if type is object
if (typeof type === 'Object' || type instanceof Object) {
return includes(type, currentPermission)
} else {
return currentPermission === type
}
}
Vue.prototype.$isMobile = function() {
const toMatch = [
/Android/i,
/webOS/i,
/iPhone/i,
/iPad/i,
/iPod/i,
/BlackBerry/i,
/Windows Phone/i
]
return toMatch.some(toMatchItem => {
return navigator.userAgent.match(toMatchItem)
})
}
Vue.prototype.$isMinimalScale = function() {
let sizeType = store.getters.filesViewWidth
return sizeType === 'minimal-scale'
}
Vue.prototype.$isCompactScale = function() {
let sizeType = store.getters.filesViewWidth
return sizeType === 'compact-scale'
}
Vue.prototype.$isFullScale = function() {
let sizeType = store.getters.filesViewWidth
return sizeType === 'full-scale'
}
Vue.prototype.$isSomethingWrong = function() {
events.$emit('alert:open', {
title: i18n.t('popup_error.title'),
message: i18n.t('popup_error.message')
})
}
Vue.prototype.$checkFileMimetype = function(files) {
let validated = true
let mimetypesBlacklist = store.getters.config.mimetypesBlacklist
for (let i = 0; i < files.length; i++) {
let fileType = files[i].type.split('/')
if (!fileType[0]) {
fileType[1] = _.last(files[i].name.split('.'))
}
if (mimetypesBlacklist.includes(fileType[1])) {
validated = false
events.$emit('alert:open', {
emoji: '😬😬😬',
title: i18n.t('popup_mimetypes_blacklist.title'),
message: i18n.t('popup_mimetypes_blacklist.message', { mimetype: fileType[1] })
})
}
}
return validated
}
Vue.prototype.$getDataByLocation = function() {
let folder = store.getters.currentFolder
let actions = {
'base': ['getFolder', [{ folder: folder, back: true, init: false, sorting: true }]],
'public': ['browseShared', [{ folder: folder, back: true, init: false, sorting: true }]],
'trash': ['getFolder', [{ folder: folder, back: true, init: false, sorting: true }]],
'participant_uploads': ['getParticipantUploads'],
'trash-root': ['getTrash'],
'latest': ['getLatest'],
'shared': ['getShared']
}
this.$store.dispatch(...actions[folder.location])
// Get dara of user with favourites tree
this.$store.dispatch('getAppData')
// Get data of Navigator tree
this.$store.dispatch('getFolderTree')
}
Vue.prototype.$checkOS = function() {
// Handle styled scrollbar for Windows
if (navigator.userAgent.indexOf('Windows') != -1) {
let body = document.body
body.classList.add('windows')
}
}
}
}
export default Helpers
export default Helpers
+40 -15
View File
@@ -5,7 +5,10 @@
"move": "Move item",
"preview": "更改预览",
"share": "Share item",
"upload": "上传文件"
"upload": "上传文件",
"close": "Close",
"sorting_view": "Sorting and View",
"info_panel": "Info panel"
},
"activation": {
"stripe": {
@@ -258,6 +261,7 @@
},
"alerts": {
"error_confirm": "哦,出了个问题!",
"leave_to_sign_in": "Do you really want to leave?",
"success_confirm": "太棒了!"
},
"context_menu": {
@@ -275,8 +279,23 @@
"rename": "重命名",
"restore": "恢复文件",
"share": "分享",
"share_cancel": "Cancel Sharing",
"share_edit": "编辑分享设定",
"upload": "上传"
"upload": "上传",
"select": "Select",
"no_options": "No Options Available"
},
"mobile_selecting": {
"select_all": "Select All",
"deselect_all": "Deselect All",
"done": "Done"
},
"preview_sorting": {
"grid_view": "Grid View",
"list_view": "List View",
"sort_date": "Sort By Date",
"sort_alphabet": "Sort By Aplhabet",
"preview_sorting_button": "View"
},
"cookie_disclaimer": {
"button": "cookies policy",
@@ -299,7 +318,9 @@
"created_at": "创建于",
"shared": "分享",
"size": "大小",
"where": "地址"
"where": "地址",
"selected_multiple": "Selected Multiple Items",
"items": "Items"
},
"file_detail_meta": {
"dimension": "Dimensions",
@@ -356,7 +377,10 @@
"locations": {
"home": "首页",
"shared": "已分享",
"trash": "垃圾箱"
"trash": "垃圾箱",
"profile": "Profile",
"settings": "Settings",
"logout": "Log Out"
},
"menu": {
"admin": "Admin",
@@ -505,9 +529,15 @@
"title": "You are trying to upload unsupported file type",
"message": "File of this type ({mimetype}) is not allowed to upload."
},
"popup_zipping": {
"title": "Zipping Your Files...",
"message": "Please wait until your files start downloading."
},
"popup_create_folder": {
"folder_default_name": "新文件夹",
"title": "请填入新文件夹名称"
"folder_default_name": "New Folder",
"title": "Create Folder",
"label": "Type Name",
"placeholder": "Type your name"
},
"popup_delete_card": {
"message": "此事件不可逆转,您的付款卡将被永久删除",
@@ -551,7 +581,9 @@
"title": "File is too large"
},
"popup_rename": {
"title": "修改文件/夹名称"
"title": "Rename Your {item}",
"label": "Edit Name",
"placeholder": "Type your title"
},
"popup_set_card": {
"message": "您的卡将被设置为默认卡,并且在以后的结算中始终会收取费用。",
@@ -581,14 +613,6 @@
"message": "您的订阅已重新激活,并且将按原始计费周期计费。",
"title": "订阅已取消"
},
"popup_trashed": {
"message": "现在,您的垃圾箱已经被完全清空。",
"title": "您的垃圾箱已清空!"
},
"preview_type": {
"grid": "方块",
"list": "列表"
},
"profile": {
"change_pass": "修改您的密码",
"profile_info": "用户信息",
@@ -720,6 +744,7 @@
"href": "Please confirm your payment."
},
"uploading": {
"cancel": "Cancel Uploading",
"processing_file": "Processing File...",
"progress_single_upload": "上传文件 {progress}%",
"progress": "上传文件 {progress}% - {current}/{total}"
+44 -19
View File
@@ -7,7 +7,10 @@
"share": "Share item",
"upload": "Upload file",
"download": "Download item",
"print": "Print item"
"print": "Print item",
"close": "Close",
"sorting_view": "Sorting and View",
"info_panel": "Info panel"
},
"activation": {
"stripe": {
@@ -260,6 +263,7 @@
},
"alerts": {
"error_confirm": "Thats horrible!",
"leave_to_sign_in": "Do you really want to leave?",
"success_confirm": "Awesome!"
},
"context_menu": {
@@ -277,8 +281,23 @@
"rename": "Rename",
"restore": "Restore",
"share": "Share",
"share_cancel": "Cancel Sharing",
"share_edit": "Edit Sharing",
"upload": "Upload"
"upload": "Upload",
"select": "Select",
"no_options": "No Options Available"
},
"mobile_selecting": {
"select_all": "Select All",
"deselect_all": "Deselect All",
"done": "Done"
},
"preview_sorting": {
"grid_view": "Grid View",
"list_view": "List View",
"sort_date": "Sort By Date",
"sort_alphabet": "Sort By Aplhabet",
"preview_sorting_button": "View"
},
"cookie_disclaimer": {
"button": "cookies policy",
@@ -301,7 +320,9 @@
"created_at": "Created at",
"shared": "Shared",
"size": "Size",
"where": "Where"
"where": "Where",
"selected_multiple": "Selected Multiple Items",
"items": "Items"
},
"file_detail_meta": {
"dimension": "Dimensions",
@@ -356,9 +377,12 @@
"original_location": "Original Location"
},
"locations": {
"home": "Home",
"home": "Files",
"shared": "Shared",
"trash": "Trash"
"trash": "Trash",
"profile": "Profile",
"settings": "Settings",
"logout": "Log Out"
},
"menu": {
"admin": "Administration",
@@ -507,10 +531,6 @@
"title": "You are trying to upload unsupported file type",
"message": "File of this type ({mimetype}) is not allowed to upload."
},
"popup_create_folder": {
"folder_default_name": "New Folder",
"title": "Please enter your new folder name"
},
"popup_delete_card": {
"message": "This event is irreversible and your payment card will be delete forever",
"title": "Are you sure?"
@@ -552,8 +572,20 @@
"message": "Sorry, your file is too large and can't be uploaded",
"title": "File is too large"
},
"popup_zipping": {
"title": "Zipping Your Files...",
"message": "Please wait until your files start downloading."
},
"popup_rename": {
"title": "Change your item name"
"title": "Rename Your {item}",
"label": "Edit Name",
"placeholder": "Type your title"
},
"popup_create_folder": {
"folder_default_name": "New Folder",
"title": "Create Folder",
"label": "Type Name",
"placeholder": "Type your name"
},
"popup_set_card": {
"message": "Your card will be set as default and will be always charged for the next billings.",
@@ -583,14 +615,6 @@
"message": "Your subscription was re-activated, and they will be billed on the original billing cycle.",
"title": "Subscription Was Resumed"
},
"popup_trashed": {
"message": "So now, you have clear and empty trash.",
"title": "Your trash was erased!"
},
"preview_type": {
"grid": "Grid",
"list": "List"
},
"profile": {
"change_pass": "Change Password",
"profile_info": "Profile Information",
@@ -676,7 +700,7 @@
"favourites": "Favourites",
"favourites_empty": "Drag here your favourite folder.",
"folders_empty": "Create some new folder.",
"home": "Home",
"home": "Files",
"latest": "Recent Uploads",
"locations_title": "Base",
"my_shared": "My Shared Items",
@@ -722,6 +746,7 @@
"href": "Please confirm your payment."
},
"uploading": {
"cancel": "Cancel Uploading",
"processing_file": "Processing File...",
"progress_single_upload": "Uploading File {progress}%",
"progress": "Uploading File {progress}% - {current}/{total}"
+39 -14
View File
@@ -7,7 +7,10 @@
"share": "Zdieľať položku",
"upload": "Nahrať súbory",
"download": "Stiahnuť položku",
"print": "Vytlačiť položku"
"print": "Vytlačiť položku",
"close": "Zatvoriť",
"sorting_view" : "Zoradenie a zobrazenie ",
"info_panel" : "Informačný panel"
},
"activation": {
"stripe": {
@@ -260,6 +263,7 @@
},
"alerts": {
"error_confirm": "To je hrozné!",
"leave_to_sign_in": "Naozaj chcete odísť?",
"success_confirm": "Skvelé!"
},
"context_menu": {
@@ -277,8 +281,23 @@
"rename": "Premenovať",
"restore": "Obnoviť",
"share": "Zdieľať",
"share_cancel": "Zrušenie zdieľania",
"share_edit": "Upraviť zdieľanie",
"upload": "Nahrať"
"upload": "Nahrať",
"select": "Výber",
"no_options": "Nie sú k dispozícii žiadne možnosti"
},
"mobile_selecting": {
"select_all": "Vybrať všetko",
"deselect_all": "Odznačiť všetko",
"done": "Hotovo"
},
"preview_sorting": {
"grid_view": "Mriežka",
"list_view": "List",
"sort_date": "Zoradiť podľa dátumu",
"sort_alphabet": "Zoradiť podľa náyvu",
"preview_sorting_button": "Zobrazenie"
},
"cookie_disclaimer": {
"button": "politikou cookies",
@@ -301,7 +320,9 @@
"created_at": "Vytvorené",
"shared": "Zdieľané",
"size": "Veľkosť",
"where": "Umiestnenie"
"where": "Umiestnenie",
"selected_multiple": "Vybratých viac položiek",
"items": "{count} Položky | {count} Položiek"
},
"file_detail_meta": {
"dimension": "Dimenzie",
@@ -358,7 +379,10 @@
"locations": {
"home": "Domov",
"shared": "Zdieľané",
"trash": "Kôš"
"trash": "Kôš",
"profile": "Profil",
"settings": "Nastavenia",
"logout": "Odhlásiť sa"
},
"menu": {
"admin": "Administrácia",
@@ -507,9 +531,15 @@
"title": "Ospravedlňujeme sa",
"message": "Nieje povolené nahrávať tento typ súboru ({mimetype})."
},
"popup_zipping": {
"title": "Súbory sa zipujú...",
"message": "Čakajte prosím, kým súbory sa nezačnú sťahovať."
},
"popup_create_folder": {
"folder_default_name": "Nový priečinok",
"title": "Prosím, vložte názov nového priečinka"
"title": "Vytvoriť priečinok",
"label": "Napíš meno",
"placeholder": "Prosím, vložte názov nového priečinka"
},
"popup_delete_card": {
"message": "Táto udalosť je nezvratná a vaša platobná karta bude navždy odstránená",
@@ -553,7 +583,9 @@
"title": "Súbor je príliš veľký"
},
"popup_rename": {
"title": "Zmeňte názov položky"
"title": "Zmeňte názov {item}",
"label": "Zmeniť názov",
"placeholder": "Napíš názov..."
},
"popup_set_card": {
"message": "Vaša karta bude nastavená ako predvolená a bude vám z nej vždy stiahnutá čiastka za nasledujúce obdobie fakturácie.",
@@ -583,14 +615,6 @@
"message": "Váš odber bol znova aktivovaný a budú vám účtované poplatky podľa pôvodného fakturačného cyklu.",
"title": "Predplatné bolo obnovené"
},
"popup_trashed": {
"message": "Od teraz máte prázdny a čistý kôš",
"title": "Váš kôš bol vymazaný!"
},
"preview_type": {
"grid": "Mriežka",
"list": "List"
},
"profile": {
"change_pass": "Zmeniť heslo",
"profile_info": "Profil",
@@ -722,6 +746,7 @@
"href": "Prosím potvrďte Vašu platbu."
},
"uploading": {
"cancel": "Zrušiť nahrávanie",
"processing_file": "Spracuvávam súbor...",
"progress_single_upload": "Nahrávam súbor {progress}%",
"progress": "Nahrávam súbory {progress}% - {current}/{total}"
+16
View File
@@ -5,6 +5,7 @@ import router from "./router";
import i18n from "./i18n/index.js";
import App from "./App.vue";
import store from "./store";
import {events} from "./bus";
import Helpers from "./helpers";
import { library } from "@fortawesome/fontawesome-svg-core";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
@@ -88,6 +89,19 @@ Vue.use(Helpers);
Vue.config.productionTip = false;
// Handle position of Drag & Drop Ghost
document.addEventListener('drag', (event) => {
let multiSelect = document.getElementById('multi-select-ui')
multiSelect.style.top = event.clientY + 20 + 'px'
multiSelect.style.left = event.clientX + 'px'
},false)
// Handle for drop
document.addEventListener("dragend", () => {
events.$emit('drop')
}, false);
var vueFileManager = new Vue({
i18n,
store,
@@ -97,3 +111,5 @@ var vueFileManager = new Vue({
},
render: (h) => h(App),
}).$mount("#app");
-9
View File
@@ -382,15 +382,6 @@ const routesUser = [
requiresAuth: true
},
},
{
name: 'Trash',
path: '/trash',
component: () =>
import(/* webpackChunkName: "chunks/trash" */ './views/FilePages/Trash'),
meta: {
requiresAuth: true
},
},
{
name: 'Settings',
path: '/settings',
+14 -2
View File
@@ -8,6 +8,10 @@ const defaultState = {
authorized: undefined,
homeDirectory: undefined,
requestedPlan: undefined,
sorting: {
sort: localStorage.getItem('sorting') ? JSON.parse(localStorage.getItem('sorting')).sort : 'DESC',
field: localStorage.getItem('sorting') ? JSON.parse(localStorage.getItem('sorting')).field : 'created_at',
},
roles: [
{
label: i18n.t('roles.admin'),
@@ -837,9 +841,10 @@ const defaultState = {
],
}
const actions = {
changePreviewType: ({commit, state}) => {
changePreviewType: ({commit, state}, preview) => {
// Get preview type
let previewType = state.FilePreviewType == 'grid' ? 'list' : 'grid'
let previewType = preview
// Store preview type to localStorage
localStorage.setItem('preview_type', previewType)
@@ -860,6 +865,10 @@ const actions = {
},
}
const mutations = {
UPDATE_SORTING(state) {
state.sorting.field = JSON.parse(localStorage.getItem('sorting')).field
state.sorting.sort = JSON.parse(localStorage.getItem('sorting')).sort
},
INIT(state, data) {
state.config = data.config
state.authorized = data.authCookie
@@ -901,6 +910,9 @@ const getters = {
config: state => state.config,
index: state => state.index,
roles: state => state.roles,
sorting: (state) => {
return {sorting: state.sorting , URI: '?sort=' + state.sorting.field + '&direction=' + state.sorting.sort}
},
}
export default {
+29 -35
View File
@@ -1,3 +1,4 @@
import Vue from "vue"
import axios from 'axios'
import {events} from '@/bus'
import router from '@/router'
@@ -5,7 +6,7 @@ import i18n from '@/i18n/index'
const defaultState = {
uploadingFilesCount: undefined,
fileInfoDetail: undefined,
fileInfoDetail: [],
currentFolder: undefined,
uploadingFileProgress: 0,
isProcessingFile: false,
@@ -32,12 +33,12 @@ const actions = {
// Set folder location
payload.folder.location = payload.folder.deleted_at || payload.folder.location === 'trash' ? 'trash' : 'base'
if (! payload.back)
if (! payload.back && !payload.sorting)
commit('STORE_PREVIOUS_FOLDER', getters.currentFolder)
let url = payload.folder.location === 'trash'
? '/folders/' + payload.folder.unique_id + '?trash=true'
: '/folders/' + payload.folder.unique_id
? '/folders/' + payload.folder.unique_id + getters.sorting.URI + '&trash=true'
: '/folders/' + payload.folder.unique_id + getters.sorting.URI
axios
.get(getters.api + url)
@@ -45,7 +46,7 @@ const actions = {
commit('LOADING_STATE', {loading: false, data: response.data})
commit('STORE_CURRENT_FOLDER', payload.folder)
if (payload.back)
if (payload.back && !payload.sorting)
commit('REMOVE_BROWSER_HISTORY')
events.$emit('scrollTop')
@@ -79,17 +80,18 @@ const actions = {
})
axios
.get(getters.api + '/latest')
.get(getters.api + '/latest' + getters.sorting.URI)
.then(response => {
commit('LOADING_STATE', {loading: false, data: response.data})
events.$emit('scrollTop')
})
.catch(() => isSomethingWrong())
.catch(() => Vue.prototype.$isSomethingWrong())
},
getShared: ({commit, getters}) => {
commit('LOADING_STATE', {loading: true, data: []})
commit('FLUSH_FOLDER_HISTORY')
let currentFolder = {
name: i18n.t('sidebar.my_shared'),
location: 'shared',
@@ -99,14 +101,14 @@ const actions = {
commit('STORE_CURRENT_FOLDER', currentFolder)
axios
.get(getters.api + '/shared-all')
.get(getters.api + '/shared-all' + getters.sorting.URI)
.then(response => {
commit('LOADING_STATE', {loading: false, data: response.data})
commit('STORE_PREVIOUS_FOLDER', currentFolder)
events.$emit('scrollTop')
})
.catch(() => isSomethingWrong())
.catch(() => Vue.prototype.$isSomethingWrong())
},
getParticipantUploads: ({commit, getters}) => {
commit('LOADING_STATE', {loading: true, data: []})
@@ -119,13 +121,13 @@ const actions = {
})
axios
.get(getters.api + '/participant-uploads')
.get(getters.api + '/participant-uploads' + getters.sorting.URI)
.then(response => {
commit('LOADING_STATE', {loading: false, data: response.data})
events.$emit('scrollTop')
})
.catch(() => isSomethingWrong())
.catch(() => Vue.prototype.$isSomethingWrong())
},
getTrash: ({commit, getters}) => {
commit('LOADING_STATE', {loading: true, data: []})
@@ -140,14 +142,14 @@ const actions = {
commit('STORE_CURRENT_FOLDER', trash)
axios
.get(getters.api + '/trash')
.get(getters.api + '/trash' + getters.sorting.URI)
.then(response => {
commit('LOADING_STATE', {loading: false, data: response.data})
commit('STORE_PREVIOUS_FOLDER', trash)
events.$emit('scrollTop')
})
.catch(() => isSomethingWrong())
.catch(() => Vue.prototype.$isSomethingWrong())
},
getSearchResult: ({commit, getters}, query) => {
commit('LOADING_STATE', {loading: true, data: []})
@@ -170,15 +172,7 @@ const actions = {
.then(response => {
commit('LOADING_STATE', {loading: false, data: response.data})
})
.catch(() => isSomethingWrong())
},
getFileDetail: ({commit, getters}, file) => {
axios
.get(getters.api + '/file-detail/' + file.unique_id)
.then(response => {
commit('LOAD_FILEINFO_DETAIL', response.data)
})
.catch(() => isSomethingWrong())
.catch(() => Vue.prototype.$isSomethingWrong())
},
getFolderTree: ({commit, getters}) => {
@@ -195,7 +189,7 @@ const actions = {
route = '/api/navigation'
axios
.get(route)
.get(route + getters.sorting.URI)
.then(response => {
resolve(response)
@@ -204,7 +198,7 @@ const actions = {
.catch((error) => {
reject(error)
isSomethingWrong()
Vue.prototype.$isSomethingWrong()
})
})
},
@@ -244,16 +238,24 @@ const mutations = {
if (item.unique_id == updatedFile.unique_id) item.name = updatedFile.name
})
},
REMOVE_ITEM_FILEINFO_DETAIL(state,item) {
state.fileInfoDetail = state.fileInfoDetail.filter(element => element.unique_id !== item.unique_id)
},
CLEAR_FILEINFO_DETAIL(state) {
state.fileInfoDetail = undefined
state.fileInfoDetail = []
},
LOAD_FILEINFO_DETAIL(state, item) {
state.fileInfoDetail = item
state.fileInfoDetail = []
state.fileInfoDetail.push(item)
},
GET_FILEINFO_DETAIL(state, item) {
let checkData = state.data.find(el => el.unique_id == item.unique_id)
if(state.fileInfoDetail.includes(checkData)) return
state.fileInfoDetail = checkData ? checkData : state.currentFolder
state.fileInfoDetail.push(checkData ? checkData : state.currentFolder)
},
SELECT_ALL_FILES(state){
state.fileInfoDetail = state.data
},
CHANGE_SEARCHING_STATE(state, searchState) {
state.isSearching = searchState
@@ -304,14 +306,6 @@ const getters = {
data: state => state.data,
}
// Show error message
function isSomethingWrong() {
events.$emit('alert:open', {
title: i18n.t('popup_error.title'),
message: i18n.t('popup_error.message'),
})
}
export default {
state: defaultState,
getters,
+347 -248
View File
@@ -1,258 +1,357 @@
import i18n from '@/i18n/index'
import router from '@/router'
import {events} from '@/bus'
import {last} from 'lodash'
import { events } from '@/bus'
import { last } from 'lodash'
import axios from 'axios'
import Vue from 'vue'
const actions = {
moveItem: ({commit, getters, dispatch}, [item_from, to_item]) => {
// Get route
let route = getters.sharedDetail && ! getters.sharedDetail.protected
? '/api/move/' + item_from.unique_id + '/public/' + router.currentRoute.params.token
: '/api/move/' + item_from.unique_id
axios
.post(route, {
from_type: item_from.type,
to_unique_id: to_item.unique_id,
_method: 'patch'
})
.then(() => {
commit('REMOVE_ITEM', item_from.unique_id)
commit('INCREASE_FOLDER_ITEM', to_item.unique_id)
if (item_from.type === 'folder' && getters.currentFolder.location !== 'public')
dispatch('getAppData')
})
.catch(() => isSomethingWrong())
},
createFolder: ({commit, getters, dispatch}, folderName) => {
// Get route
let route = getters.sharedDetail && ! getters.sharedDetail.protected
? '/api/create-folder/public/' + router.currentRoute.params.token
: '/api/create-folder'
axios
.post(route, {
parent_id: getters.currentFolder.unique_id,
name: folderName
})
.then(response => {
commit('ADD_NEW_FOLDER', response.data)
events.$emit('scrollTop')
if ( getters.currentFolder.location !== 'public' ) {
dispatch('getAppData')
}
})
.catch(() => isSomethingWrong())
},
renameItem: ({commit, getters, dispatch}, data) => {
// Updated name in favourites panel
if (getters.permission === 'master' && data.type === 'folder')
commit('UPDATE_NAME_IN_FAVOURITES', data)
// Get route
let route = getters.sharedDetail && ! getters.sharedDetail.protected
? '/api/rename-item/' + data.unique_id + '/public/' + router.currentRoute.params.token
: '/api/rename-item/' + data.unique_id
axios
.post(route, {
name: data.name,
type: data.type,
_method: 'patch'
})
.then(response => {
commit('CHANGE_ITEM_NAME', response.data)
if (data.type === 'folder' && getters.currentFolder.location !== 'public')
dispatch('getAppData')
})
.catch(() => isSomethingWrong())
},
uploadFiles: ({commit, getters}, {form, fileSize, totalUploadedSize}) => {
return new Promise((resolve, reject) => {
// Get route
let route = getters.sharedDetail && ! getters.sharedDetail.protected
? '/api/upload/public/' + router.currentRoute.params.token
: '/api/upload'
axios
.post(route, form, {
headers: {
'Content-Type': 'application/octet-stream'
},
onUploadProgress: event => {
var percentCompleted = Math.floor(((totalUploadedSize + event.loaded) / fileSize) * 100)
commit('UPLOADING_FILE_PROGRESS', percentCompleted >= 100 ? 100 : percentCompleted)
if (percentCompleted >= 100) {
commit('PROCESSING_FILE', true)
}
}
})
.then(response => {
commit('PROCESSING_FILE', false)
// Check if user is in uploading folder, if yes, than show new file
if (response.data.folder_id == getters.currentFolder.unique_id)
commit('ADD_NEW_ITEMS', response.data)
resolve(response)
})
.catch(error => {
commit('PROCESSING_FILE', false)
reject(error)
switch (error.response.status) {
case 423:
events.$emit('alert:open', {
emoji: '😬😬😬',
title: i18n.t('popup_exceed_limit.title'),
message: i18n.t('popup_exceed_limit.message')
})
break;
case 415:
events.$emit('alert:open', {
emoji: '😬😬😬',
title: i18n.t('popup_mimetypes_blacklist.title'),
message: i18n.t('popup_mimetypes_blacklist.message')
})
break;
case 413:
events.$emit('alert:open', {
emoji: '😟😟😟',
title: i18n.t('popup_upload_limit.title'),
message: i18n.t('popup_upload_limit.message', {uploadLimit: getters.config.uploadLimitFormatted})
})
break;
default:
events.$emit('alert:open', {
title: i18n.t('popup_error.title'),
message: i18n.t('popup_error.message'),
})
break;
}
// Reset uploader
commit('UPDATE_FILE_COUNT_PROGRESS', undefined)
})
})
},
restoreItem: ({commit, getters}, item) => {
let restoreToHome = false
// Check if file can be restored to home directory
if (getters.currentFolder.location === 'trash')
restoreToHome = true
// Remove file
commit('REMOVE_ITEM', item.unique_id)
// Remove file preview
commit('CLEAR_FILEINFO_DETAIL')
axios
.post(getters.api + '/restore-item/' + item.unique_id, {
type: item.type,
to_home: restoreToHome,
_method: 'patch'
})
.catch(() => isSomethingWrong())
},
deleteItem: ({commit, getters, dispatch}, data) => {
// Remove file
commit('REMOVE_ITEM', data.unique_id)
// Remove item from sidebar
if (getters.permission === 'master') {
if (data.type === 'folder')
commit('REMOVE_ITEM_FROM_FAVOURITES', data)
}
// Remove file preview
commit('CLEAR_FILEINFO_DETAIL')
// Get route
let route = getters.sharedDetail && ! getters.sharedDetail.protected
? '/api/remove-item/' + data.unique_id + '/public/' + router.currentRoute.params.token
: '/api/remove-item/' + data.unique_id
axios
.post(route, {
_method: 'delete',
data: {
type: data.type,
force_delete: data.deleted_at ? true : false,
},
})
.then(() => {
// If is folder, update app data
if (data.type === 'folder') {
if (data.unique_id === getters.currentFolder.unique_id) {
if ( getters.currentFolder.location === 'public' ) {
dispatch('browseShared', [{folder: last(getters.browseHistory), back: true, init: false}])
} else {
dispatch('getFolder', [{folder: last(getters.browseHistory), back: true, init: false}])
}
}
if ( getters.currentFolder.location !== 'public' )
dispatch('getAppData')
}
})
.catch(() => isSomethingWrong())
},
emptyTrash: ({commit, getters}) => {
// Clear file browser
commit('LOADING_STATE', {loading: true, data: []})
axios
.post(getters.api + '/empty-trash', {
_method: 'delete'
})
.then(() => {
commit('LOADING_STATE', {loading: false, data: []})
events.$emit('scrollTop')
// Remove file preview
commit('CLEAR_FILEINFO_DETAIL')
// Show success message
events.$emit('success:open', {
title: i18n.t('popup_trashed.title'),
message: i18n.t('popup_trashed.message'),
})
})
.catch(() => isSomethingWrong())
},
const defaultState = {
isZippingFiles: false,
}
// Show error message
function isSomethingWrong() {
events.$emit('alert:open', {
title: i18n.t('popup_error.title'),
message: i18n.t('popup_error.message'),
})
const actions = {
downloadFiles: ({ commit, getters }) => {
let files = []
// get unique_ids of selected files
getters.fileInfoDetail.forEach(file => files.push(file.unique_id))
// Get route
let route = getters.sharedDetail && !getters.sharedDetail.protected
? '/api/zip/public/' + router.currentRoute.params.token
: '/api/zip'
commit('ZIPPING_FILE_STATUS', true)
axios.post(route, {
files: files
})
.then(response => {
Vue.prototype.$downloadFile(response.data.url, response.data.name)
})
.catch(() => {
Vue.prototype.$isSomethingWrong()
})
.finally(() => {
commit('ZIPPING_FILE_STATUS', false)
})
},
moveItem: ({ commit, getters, dispatch }, { to_item, noSelectedItem }) => {
let itemsToMove = []
let items = [noSelectedItem]
// If coming no selected item dont get items to move from fileInfoDetail
if (!noSelectedItem)
items = getters.fileInfoDetail
items.forEach(data => itemsToMove.push({
'force_delete': data.deleted_at ? true : false,
'unique_id': data.unique_id,
'type': data.type
}))
// Remove file preview
if (!noSelectedItem)
commit('CLEAR_FILEINFO_DETAIL')
// Get route
let route = getters.sharedDetail && !getters.sharedDetail.protected
? '/api/move/public/' + router.currentRoute.params.token
: '/api/move'
axios
.post(route, {
_method: 'post',
to_unique_id: to_item.unique_id,
items: itemsToMove
})
.then(() => {
itemsToMove.forEach(item => {
commit('REMOVE_ITEM', item.unique_id)
commit('INCREASE_FOLDER_ITEM', to_item.unique_id)
if (item.type === 'folder')
dispatch('getAppData')
if (getters.currentFolder.location === 'public')
dispatch('getFolderTree')
})
})
.catch(() => Vue.prototype.$isSomethingWrong())
},
createFolder: ({ commit, getters, dispatch }, folderName) => {
// Get route
let route = getters.sharedDetail && !getters.sharedDetail.protected
? '/api/create-folder/public/' + router.currentRoute.params.token
: '/api/create-folder'
axios
.post(route, {
parent_id: getters.currentFolder.unique_id,
name: folderName
})
.then(response => {
commit('ADD_NEW_FOLDER', response.data)
events.$emit('scrollTop')
if (getters.currentFolder.location !== 'public')
dispatch('getAppData')
if (getters.currentFolder.location === 'public')
dispatch('getFolderTree')
})
.catch(() => Vue.prototype.$isSomethingWrong())
},
renameItem: ({ commit, getters, dispatch }, data) => {
// Updated name in favourites panel
if (getters.permission === 'master' && data.type === 'folder')
commit('UPDATE_NAME_IN_FAVOURITES', data)
// Get route
let route = getters.sharedDetail && !getters.sharedDetail.protected
? '/api/rename-item/' + data.unique_id + '/public/' + router.currentRoute.params.token
: '/api/rename-item/' + data.unique_id
axios
.post(route, {
name: data.name,
type: data.type,
_method: 'patch'
})
.then(response => {
commit('CHANGE_ITEM_NAME', response.data)
if (data.type === 'folder' && getters.currentFolder.location !== 'public')
dispatch('getAppData')
if (data.type === 'folder' && getters.currentFolder.location === 'public')
dispatch('getFolderTree')
})
.catch(() => Vue.prototype.$isSomethingWrong())
},
uploadFiles: ({ commit, getters }, { form, fileSize, totalUploadedSize }) => {
return new Promise((resolve, reject) => {
// Get route
let route = getters.sharedDetail && !getters.sharedDetail.protected
? '/api/upload/public/' + router.currentRoute.params.token
: '/api/upload'
// Create cancel token for axios cancelation
const CancelToken = axios.CancelToken
const source = CancelToken.source()
axios
.post(route, form, {
cancelToken: source.token,
headers: {
'Content-Type': 'application/octet-stream'
},
onUploadProgress: event => {
var percentCompleted = Math.floor(((totalUploadedSize + event.loaded) / fileSize) * 100)
commit('UPLOADING_FILE_PROGRESS', percentCompleted >= 100 ? 100 : percentCompleted)
if (percentCompleted >= 100) {
commit('PROCESSING_FILE', true)
}
}
})
.then(response => {
commit('PROCESSING_FILE', false)
// Check if user is in uploading folder, if yes, than show new file
if (response.data.folder_id == getters.currentFolder.unique_id)
commit('ADD_NEW_ITEMS', response.data)
resolve(response)
})
.catch(error => {
commit('PROCESSING_FILE', false)
reject(error)
switch (error.response.status) {
case 423:
events.$emit('alert:open', {
emoji: '😬😬😬',
title: i18n.t('popup_exceed_limit.title'),
message: i18n.t('popup_exceed_limit.message')
})
break
case 415:
events.$emit('alert:open', {
emoji: '😬😬😬',
title: i18n.t('popup_mimetypes_blacklist.title'),
message: i18n.t('popup_mimetypes_blacklist.message')
})
break
case 413:
events.$emit('alert:open', {
emoji: '😟😟😟',
title: i18n.t('popup_paylod_error.title'),
message: i18n.t('popup_paylod_error.message')
})
break
default:
events.$emit('alert:open', {
title: i18n.t('popup_error.title'),
message: i18n.t('popup_error.message')
})
break
}
// Reset uploader
commit('UPDATE_FILE_COUNT_PROGRESS', undefined)
})
// Cancel the upload request
events.$on('cancel-upload', () => {
source.cancel()
// Hide upload progress bar
commit('PROCESSING_FILE', false)
commit('UPDATE_FILE_COUNT_PROGRESS', undefined)
})
})
},
restoreItem: ({ commit, getters }, item) => {
let restoreToHome = false
// Check if file can be restored to home directory
if (getters.currentFolder.location === 'trash')
restoreToHome = true
// Remove file
commit('REMOVE_ITEM', item.unique_id)
// Remove file preview
commit('CLEAR_FILEINFO_DETAIL')
axios
.post(getters.api + '/restore-item/' + item.unique_id, {
type: item.type,
to_home: restoreToHome,
_method: 'patch'
})
.catch(() => Vue.prototype.$isSomethingWrong())
},
deleteItem: ({ commit, getters, dispatch }, noSelectedItem) => {
let itemsToDelete = []
let items = [noSelectedItem]
// If coming no selected item dont get items to move from fileInfoDetail
if (!noSelectedItem)
items = getters.fileInfoDetail
items.forEach(data => {
itemsToDelete.push({
'force_delete': data.deleted_at ? true : false,
'type': data.type,
'unique_id': data.unique_id
})
// Remove file
commit('REMOVE_ITEM', data.unique_id)
// Remove item from sidebar
if (getters.permission === 'master') {
if (data.type === 'folder')
commit('REMOVE_ITEM_FROM_FAVOURITES', data)
}
// Remove file
commit('REMOVE_ITEM', data.unique_id)
// Remove item from sidebar
if (getters.permission === 'master') {
if (data.type === 'folder')
commit('REMOVE_ITEM_FROM_FAVOURITES', data)
}
})
// Remove file preview
if (!noSelectedItem) {
commit('CLEAR_FILEINFO_DETAIL')
}
// Get route
let route = getters.sharedDetail && !getters.sharedDetail.protected
? '/api/remove-item/public/' + router.currentRoute.params.token
: '/api/remove-item'
axios
.post(route, {
_method: 'post',
data: itemsToDelete
})
.then(() => {
itemsToDelete.forEach(data => {
// If is folder, update app data
if (data.type === 'folder') {
if (data.unique_id === getters.currentFolder.unique_id) {
if (getters.currentFolder.location === 'public') {
dispatch('browseShared', [{ folder: last(getters.browseHistory), back: true, init: false }])
} else {
dispatch('getFolder', [{ folder: last(getters.browseHistory), back: true, init: false }])
}
}
}
})
if (getters.currentFolder.location !== 'public')
dispatch('getAppData')
if (getters.currentFolder.location === 'public')
dispatch('getFolderTree')
})
.catch(() => Vue.prototype.$isSomethingWrong())
},
emptyTrash: ({ commit, getters }) => {
// Clear file browser
commit('LOADING_STATE', { loading: true, data: [] })
axios
.post(getters.api + '/empty-trash', {
_method: 'delete'
})
.then(() => {
commit('LOADING_STATE', { loading: false, data: [] })
events.$emit('scrollTop')
// Remove file preview
commit('CLEAR_FILEINFO_DETAIL')
})
.catch(() => Vue.prototype.$isSomethingWrong())
}
}
const mutations = {
ZIPPING_FILE_STATUS(state, status) {
state.isZippingFiles = status
}
}
const getters = {
isZippingFiles: state => state.isZippingFiles
}
export default {
actions,
state: defaultState,
mutations,
actions,
getters
}
+46 -3
View File
@@ -32,7 +32,7 @@ const actions = {
events.$emit('clear-query')
}
if (! payload.back)
if (! payload.back && !payload.sorting)
commit('STORE_PREVIOUS_FOLDER', getters.currentFolder)
payload.folder.location = 'public'
@@ -43,13 +43,13 @@ const actions = {
return new Promise((resolve, reject) => {
axios
.get(route)
.get(route + getters.sorting.URI)
.then(response => {
commit('LOADING_STATE', {loading: false, data: response.data})
commit('STORE_CURRENT_FOLDER', payload.folder)
events.$emit('scrollTop')
if (payload.back)
if (payload.back && !payload.sorting)
commit('REMOVE_BROWSER_HISTORY')
resolve(response)
@@ -65,6 +65,49 @@ const actions = {
})
})
},
shareCancel: ({commit, getters} , singleItem) => {
return new Promise((resolve, reject) => {
let tokens = []
let items = [singleItem]
if(!singleItem) {
items = getters.fileInfoDetail
}
items.forEach(data => {
tokens.push(data.shared.token)
})
axios
.post('/api/share/cancel', {
_method: 'post',
tokens: tokens
})
.then(() => {
items.forEach(item => {
// Remove item from file browser
if ( getters.currentFolder , getters.currentFolder.location === 'shared' ) {
commit('REMOVE_ITEM', item.unique_id)
}
// Flush shared data
commit('FLUSH_SHARED', item.unique_id)
commit('CLEAR_FILEINFO_DETAIL')
})
resolve(true)
})
.catch((error) => {
isSomethingWrong()
reject(error)
})
})
},
getSingleFile: ({commit, state}) => {
let route = state.sharedDetail.protected
+46 -19
View File
@@ -2,6 +2,7 @@ import axios from 'axios'
import {events} from '@/bus'
import i18n from '@/i18n/index.js'
import router from '@/router'
import Vue from 'vue'
const defaultState = {
authorized: undefined,
@@ -11,9 +12,10 @@ const defaultState = {
const actions = {
getAppData: ({commit, getters}) => {
return new Promise((resolve, reject) => {
axios
.get(getters.api + '/user')
.get(getters.api + '/user' + getters.sorting.URI)
.then((response) => {
resolve(response)
@@ -48,37 +50,60 @@ const actions = {
})
},
addToFavourites: (context, folder) => {
let addFavourites = []
let items = [folder]
// If dont coming single folder get folders to add to favourites from fileInfoDetail
if(!folder)
items = context.getters.fileInfoDetail
items.forEach((data) => {
if(data.type === 'folder' ) {
if(context.getters.user.relationships.favourites.data.attributes.folders.find(folder => folder.unique_id === data.unique_id)) return
addFavourites.push({
'unique_id': data.unique_id
})
}
})
// If dont coming single folder clear the selected folders in fileInfoDetail
if(!folder) {
context.commit('CLEAR_FILEINFO_DETAIL')
}
let pushToFavorites = []
// Check is favorites already don't include some of pushed folders
items.map(data => {
if(!context.getters.user.relationships.favourites.data.attributes.folders.find(folder => folder.unique_id === data.unique_id)){
pushToFavorites.push(data)
}
})
// Add to storage
context.commit('ADD_TO_FAVOURITES', folder)
context.commit('ADD_TO_FAVOURITES', pushToFavorites)
axios
.post(context.getters.api + '/folders/favourites', {
unique_id: folder.unique_id
folders: addFavourites
})
.catch(() => {
// Show error message
events.$emit('alert:open', {
title: i18n.t('popup_error.title'),
message: i18n.t('popup_error.message'),
})
Vue.prototype.$isSomethingWrong()
})
},
removeFromFavourites: (context, folder) => {
removeFromFavourites: ({commit, getters, dispatch}, folder) => {
// Remove from storage
context.commit('REMOVE_ITEM_FROM_FAVOURITES', folder)
commit('REMOVE_ITEM_FROM_FAVOURITES', folder)
axios
.post(context.getters.api + '/folders/favourites/' + folder.unique_id, {
.post(getters.api + '/folders/favourites/' + folder.unique_id, {
_method: 'delete'
})
.catch(() => {
// Show error message
events.$emit('alert:open', {
title: i18n.t('popup_error.title'),
message: i18n.t('popup_error.message'),
})
Vue.prototype.$isSomethingWrong()
})
},
}
@@ -95,11 +120,13 @@ const mutations = {
state.app = undefined
},
ADD_TO_FAVOURITES(state, folder) {
folder.forEach(item => {
state.user.relationships.favourites.data.attributes.folders.push({
unique_id: folder.unique_id,
name: folder.name,
type: folder.type,
unique_id: item.unique_id,
name: item.name,
type: item.type,
})
})
},
UPDATE_NAME(state, name) {
state.user.data.attributes.name = name
+1 -1
View File
@@ -160,7 +160,7 @@
}
&.widgets-coll-3 {
grid-template-columns: repeat(auto-fill, 33%);
grid-template-columns: repeat(auto-fill, 33.3%);
}
.widget {
+170 -129
View File
@@ -3,8 +3,9 @@
<ContentSidebar>
<!--Empty storage warning-->
<ContentGroup v-if="config.storageLimit && storage.used > 95">
<UpgradeSidebarBanner />
<UpgradeSidebarBanner/>
</ContentGroup>
<!--Locations-->
@@ -18,8 +19,7 @@
{{ $t('sidebar.home') }}
</div>
</a>
<a class="menu-list-item link" :class="{'is-active': $isThisLocation(['latest'])}"
@click="getLatest">
<a class="menu-list-item link" :class="{'is-active': $isThisLocation(['latest'])}" @click="getLatest">
<div class="icon">
<upload-cloud-icon size="17"></upload-cloud-icon>
</div>
@@ -27,37 +27,35 @@
{{ $t('sidebar.latest') }}
</div>
</a>
<a class="menu-list-item link trash" :class="{'is-active-trash': $isThisLocation(['trash', 'trash-root'])}" @click="getTrash">
<div class="icon">
<trash2-icon size="17"></trash2-icon>
</div>
<div class="label">
{{ $t('locations.trash') }}
</div>
</a>
</div>
</ContentGroup>
<!--Navigator-->
<ContentGroup :title="$t('sidebar.navigator_title')" class="navigator">
<ContentGroup :title="$t('sidebar.navigator_title')" slug="navigator" :can-collapse="true" class="navigator">
<span class="empty-note navigator" v-if="tree.length == 0">
{{ $t('sidebar.folders_empty') }}
</span>
<TreeMenuNavigator class="folder-tree" :depth="0" :nodes="items" v-for="items in tree"
:key="items.unique_id"/>
<TreeMenuNavigator class="folder-tree" :depth="0" :nodes="items" v-for="items in tree" :key="items.unique_id"/>
</ContentGroup>
<!--Favourites-->
<ContentGroup :title="$t('sidebar.favourites')">
<ContentGroup :title="$t('sidebar.favourites')" slug="favourites" :can-collapse="true">
<div class="menu-list-wrapper vertical favourites"
:class="{ 'is-dragenter': area }"
@dragover.prevent="dragEnter"
@dragleave="dragLeave"
@drop="dragFinish($event)"
>
<div class="menu-list-wrapper vertical favourites" :class="{ 'is-dragenter': area }" @dragover.prevent="dragEnter" @dragleave="dragLeave" @drop="dragFinish($event)">
<transition-group tag="div" class="menu-list" name="folder-item">
<span class="empty-note favourites" v-if="favourites.length == 0" :key="0">
{{ $t('sidebar.favourites_empty') }}
</span>
<a @click.stop="openFolder(folder)"
class="menu-list-item"
:class="{'is-current': (folder && currentFolder) && (currentFolder.unique_id === folder.unique_id)}"
v-for="folder in favourites"
:key="folder.unique_id">
<a @click.stop="openFolder(folder)" class="menu-list-item" :class="{'is-current': (folder && currentFolder) && (currentFolder.unique_id === folder.unique_id)}" v-for="(folder, i) in favourites" :key="i">
<div>
<folder-icon size="17" class="folder-icon"></folder-icon>
<span class="label">{{ folder.name }}</span>
@@ -69,153 +67,196 @@
</ContentGroup>
</ContentSidebar>
<ContentFileView/>
</section>
</template>
<script>
import UpgradeSidebarBanner from '@/components/Others/UpgradeSidebarBanner'
import TreeMenuNavigator from '@/components/Others/TreeMenuNavigator'
import ContentFileView from '@/components/Others/ContentFileView'
import ContentSidebar from '@/components/Sidebar/ContentSidebar'
import ContentGroup from '@/components/Sidebar/ContentGroup'
import {mapGetters} from 'vuex'
import {events} from '@/bus'
import {
import UpgradeSidebarBanner from '@/components/Others/UpgradeSidebarBanner'
import TreeMenuNavigator from '@/components/Others/TreeMenuNavigator'
import MultiSelected from '@/components/FilesView/MultiSelected'
import ContentFileView from '@/components/Others/ContentFileView'
import ContentSidebar from '@/components/Sidebar/ContentSidebar'
import ContentGroup from '@/components/Sidebar/ContentGroup'
import { mapGetters } from 'vuex'
import { events } from '@/bus'
import {
UploadCloudIcon,
FolderIcon,
Trash2Icon,
HomeIcon,
XIcon
} from 'vue-feather-icons'
export default {
name: 'FilesView',
components: {
UpgradeSidebarBanner,
TreeMenuNavigator,
ContentFileView,
MultiSelected,
ContentSidebar,
UploadCloudIcon,
ContentGroup,
FolderIcon,
Trash2Icon,
HomeIcon,
XIcon,
} from 'vue-feather-icons'
export default {
name: 'FilesView',
components: {
UpgradeSidebarBanner,
TreeMenuNavigator,
ContentFileView,
ContentSidebar,
UploadCloudIcon,
ContentGroup,
FolderIcon,
HomeIcon,
XIcon,
XIcon
},
computed: {
...mapGetters(['user', 'homeDirectory', 'currentFolder', 'config', 'fileInfoDetail']),
favourites() {
return this.user.relationships.favourites.data.attributes.folders
},
computed: {
...mapGetters(['user', 'homeDirectory', 'currentFolder', 'config']),
favourites() {
return this.user.relationships.favourites.data.attributes.folders
},
tree() {
return this.user.relationships.tree.data.attributes.folders
},
storage() {
return this.$store.getters.user.relationships.storage.data.attributes
}
tree() {
return this.user.relationships.tree.data.attributes.folders
},
data() {
return {
area: false,
draggedItem: undefined,
}
storage() {
return this.$store.getters.user.relationships.storage.data.attributes
}
},
data() {
return {
area: false,
draggedItem: undefined
}
},
methods: {
getTrash() {
this.$store.dispatch('getTrash')
},
methods: {
getLatest() {
this.$store.dispatch('getLatest')
},
goHome() {
this.$store.dispatch('getFolder', [{folder: this.homeDirectory, back: false, init: true}])
},
openFolder(folder) {
this.$store.dispatch('getFolder', [{folder: folder, back: false, init: false}])
},
dragEnter() {
if (this.draggedItem && this.draggedItem.type !== 'folder') return
getLatest() {
this.$store.dispatch('getLatest')
},
goHome() {
this.$store.dispatch('getFolder', [{ folder: this.homeDirectory, back: false, init: true }])
},
openFolder(folder) {
this.$store.dispatch('getFolder', [{ folder: folder, back: false, init: false }])
},
dragEnter() {
if (this.draggedItem && this.draggedItem.type !== 'folder') return
this.area = true
},
dragLeave() {
this.area = false
},
dragFinish() {
this.area = false
if (this.fileInfoDetail.length > 0 && this.fileInfoDetail.find(item => item.type !== 'folder')) return
// Check if draged item is folder
if (this.draggedItem && this.draggedItem.type !== 'folder') return
this.area = true
},
dragLeave() {
this.area = false
},
dragFinish() {
this.area = false
// Check if folder exist in favourites
if (this.favourites.find(folder => folder.unique_id == this.draggedItem.unique_id)) return
events.$emit('drop')
// Store favourites folder
// Check if dragged item is folder
if (this.draggedItem && this.draggedItem.type !== 'folder') return
// Check if folder exist in favourites
if (this.favourites.find(folder => folder.unique_id == this.draggedItem.unique_id)) return
// Prevent to move folders to self
if (this.fileInfoDetail.length > 0 && this.fileInfoDetail.find(item => item.type !== 'folder')) return
// Store favourites folder
//Add to favourites non selected folder
if (!this.fileInfoDetail.includes(this.draggedItem)) {
this.$store.dispatch('addToFavourites', this.draggedItem)
},
removeFavourite(folder) {
this.$store.dispatch('removeFromFavourites', folder)
}
},
created() {
this.goHome()
// Listen for dragstart folder items
events.$on('dragstart', (item) => this.draggedItem = item)
//Add to favourites selected folders
if (this.fileInfoDetail.includes(this.draggedItem)) {
this.$store.dispatch('addToFavourites', null)
}
},
removeFavourite(folder) {
this.$store.dispatch('removeFromFavourites', folder)
}
},
created() {
this.goHome()
// Listen for dragstart folder items
events.$on('dragstart', (item) => {
this.draggedItem = item , this.dragInProgress = true
})
events.$on('drop', () => {
this.dragInProgress = false
})
},
beforeRouteLeave(to, from, next) {
// Inquire user about his willing to step back to sign in page
if (to.name === 'SignIn') {
if (window.confirm(this.$t('alerts.leave_to_sign_in'))) {
next()
} else {
next(false)
}
} else {
next()
}
}
}
</script>
<style lang="scss" scoped>
.empty-note {
&.navigator {
padding: 5px 25px 10px;
}
&.favourites {
padding: 5px 23px 10px;
}
}
.navigator {
width: 100%;
overflow-x: auto;
}
@media only screen and (max-width: 1024px) {
.empty-note {
&.navigator {
padding: 5px 25px 10px;
padding: 5px 20px 10px;
}
&.favourites {
padding: 5px 23px 10px;
padding: 5px 18px 10px;
}
}
}
.navigator {
width: 100%;
overflow-x: auto;
}
// Transition
.folder-item-move {
transition: transform 300s ease;
}
@media only screen and (max-width: 1024px) {
.folder-item-enter-active {
transition: all 300ms ease;
}
.empty-note {
.folder-item-leave-active {
transition: all 300ms;
}
&.navigator {
padding: 5px 20px 10px;
}
.folder-item-enter, .folder-item-leave-to /* .list-leave-active below version 2.1.8 */
{
opacity: 0;
transform: translateX(30px);
}
&.favourites {
padding: 5px 18px 10px;
}
}
}
// Transition
.folder-item-move {
transition: transform 300s ease;
}
.folder-item-enter-active {
transition: all 300ms ease;
}
.folder-item-leave-active {
transition: all 300ms;
}
.folder-item-enter, .folder-item-leave-to /* .list-leave-active below version 2.1.8 */
{
opacity: 0;
transform: translateX(30px);
}
.folder-item-leave-active {
position: absolute;
}
.folder-item-leave-active {
position: absolute;
}
</style>
-51
View File
@@ -1,51 +0,0 @@
<template>
<section id="viewport">
<ContentSidebar>
<!--Tools-->
<ContentGroup :title="$t('sidebar.tools_title')" class="navigator">
<div class="menu-list-wrapper vertical">
<div class="menu-list-item link" @click="emptyTrash()">
<div class="icon">
<trash-icon size="17"></trash-icon>
</div>
<div class="label">
{{ $t('context_menu.empty_trash') }}
</div>
</div>
</div>
</ContentGroup>
</ContentSidebar>
<ContentFileView/>
</section>
</template>
<script>
import ContentFileView from '@/components/Others/ContentFileView'
import ContentSidebar from '@/components/Sidebar/ContentSidebar'
import ContentGroup from '@/components/Sidebar/ContentGroup'
import {
TrashIcon,
} from 'vue-feather-icons'
export default {
name: 'FilesView',
components: {
ContentFileView,
ContentSidebar,
ContentGroup,
TrashIcon,
},
methods: {
emptyTrash() {
this.$store.dispatch('emptyTrash')
},
},
created() {
this.$store.dispatch('getTrash')
}
}
</script>
<style lang="scss" scoped>
</style>
+118 -29
View File
@@ -9,9 +9,21 @@
<!--Move item setup-->
<MoveItem />
<!-- Mobile Menu for Multi selected items -->
<MobileMultiSelectMenu/>
<!--Rename folder or file item-->
<RenameItem/>
<!-- Drag & Drop UI -->
<DragUI/>
<!--Mobile Menu-->
<MobileMenu/>
<!-- Mobile menu for selecting view and sorting -->
<MobileSortingAndPreview/>
<!--System alerts-->
<Alert />
@@ -19,7 +31,7 @@
<Vignette/>
<!--Password verification-->
<div v-if="currentPage === 'page-password'" id="password-view">
<div v-if="isPagePasswordVerification" id="password-view">
<!--Verify share link by password-->
<AuthContent class="center" name="password" :visible="true">
@@ -41,42 +53,82 @@
</AuthContent>
</div>
<!--File browser-->
<div v-if="currentPage === 'page-files'" id="files-view">
<div id="single-file" v-if="sharedDetail.type === 'file'">
<div class="single-file-wrapper">
<FileItemGrid v-if="sharedFile" :data="sharedFile" :context-menu="false"/>
<!--Single file page-->
<div v-if="sharedDetail.type === 'file' && isPageFiles" id="single-file">
<div class="single-file-wrapper">
<FileItemGrid v-if="sharedFile" :data="sharedFile" :context-menu="false"/>
<ButtonBase @click.native="download" class="download-button" button-style="theme">
{{ $t('page_shared.download_file') }}
</ButtonBase>
</div>
</div>
<div v-if="sharedDetail.type === 'folder'" @contextmenu.prevent.capture="contextMenu($event, undefined)" @click="fileViewClick">
<!--Context menu-->
<ContextMenu/>
<!--Desktop Toolbar-->
<DesktopToolbar/>
<!--File browser-->
<FileBrowser/>
<ButtonBase @click.native="download" class="download-button" button-style="theme">
{{ $t('page_shared.download_file') }}
</ButtonBase>
</div>
</div>
<!--Multiple items view page-->
<div v-if="sharedDetail.type === 'folder' && isPageFiles"
@contextmenu.prevent.capture="contextMenu($event, undefined)"
id="viewport">
<ContentSidebar v-if="navigationTree">
<!--Locations-->
<ContentGroup :title="$t('sidebar.locations_title')">
<div class="menu-list-wrapper vertical">
<a class="menu-list-item link" @click="goHome">
<div class="icon">
<home-icon size="17"></home-icon>
</div>
<div class="label">
{{ $t('sidebar.home') }}
</div>
</a>
</div>
</ContentGroup>
<!--Navigator-->
<ContentGroup :title="$t('sidebar.navigator_title')" class="navigator">
<span class="empty-note navigator" v-if="navigationTree.length == 0">
{{ $t('sidebar.folders_empty') }}
</span>
<TreeMenuNavigator class="folder-tree" :depth="0" :nodes="items" v-for="items in navigationTree" :key="items.unique_id"/>
</ContentGroup>
</ContentSidebar>
<div id="files-view">
<!--Context menu-->
<ContextMenu/>
<!--Desktop Toolbar-->
<DesktopToolbar/>
<!--File browser-->
<FileBrowser/>
<!-- Selecting preview list and sorting -->
<DesktopSortingAndPreview/>
</div>
</div>
</div>
</template>
<script>
import {ValidationProvider, ValidationObserver} from 'vee-validate/dist/vee-validate.full'
import MobileSortingAndPreview from '@/components/FilesView/MobileSortingAndPreview'
import MobileMultiSelectMenu from '@/components/FilesView/MobileMultiSelectMenu'
import DesktopSortingAndPreview from '@/components/FilesView/DesktopSortingAndPreview'
import TreeMenuNavigator from '@/components/Others/TreeMenuNavigator'
import FileFullPreview from '@/components/FilesView/FileFullPreview'
import DesktopToolbar from '@/components/FilesView/DesktopToolbar'
import FileFullPreview from "@/components/FilesView/FileFullPreview";
import ContentSidebar from '@/components/Sidebar/ContentSidebar'
import DragUI from '@/components/FilesView/DragUI'
import FileItemGrid from '@/components/FilesView/FileItemGrid'
import ContentGroup from '@/components/Sidebar/ContentGroup'
import FileBrowser from '@/components/FilesView/FileBrowser'
import ContextMenu from '@/components/FilesView/ContextMenu'
import ButtonBase from '@/components/FilesView/ButtonBase'
import MobileMenu from '@/components/FilesView/MobileMenu'
import AuthContent from '@/components/Auth/AuthContent'
import RenameItem from '@/components/Others/RenameItem'
import AuthButton from '@/components/Auth/AuthButton'
import Spinner from '@/components/FilesView/Spinner'
import MoveItem from '@/components/Others/MoveItem'
@@ -86,21 +138,33 @@
import {mapGetters} from 'vuex'
import {events} from '@/bus'
import axios from 'axios'
import {
HomeIcon,
} from 'vue-feather-icons'
export default {
name: 'SharedPage',
components: {
MobileSortingAndPreview,
MobileMultiSelectMenu,
ValidationProvider,
DesktopSortingAndPreview,
ValidationObserver,
TreeMenuNavigator,
FileFullPreview,
DesktopToolbar,
ContentSidebar,
DragUI,
FileItemGrid,
ContentGroup,
AuthContent,
FileBrowser,
ContextMenu,
AuthButton,
MobileMenu,
ButtonBase,
RenameItem,
HomeIcon,
MoveItem,
required,
Vignette,
@@ -108,7 +172,21 @@
Alert,
},
computed: {
...mapGetters(['config', 'sharedDetail', 'sharedFile']),
...mapGetters([
'config',
'sharedDetail',
'sharedFile',
'navigation'
]),
navigationTree() {
return this.navigation ? this.navigation[0].folders : undefined
},
isPageFiles() {
return this.currentPage === 'page-files'
},
isPagePasswordVerification() {
return this.currentPage === 'page-password'
}
},
data() {
return {
@@ -116,10 +194,14 @@
password: '',
isLoading: false,
isPageLoading: true,
currentPage: undefined
currentPage: undefined,
homeDirectory: undefined,
}
},
methods: {
goHome() {
this.$store.dispatch('browseShared', [{folder: this.homeDirectory, back: false, init: true}])
},
async authenticateProtected() {
// Validate fields
@@ -163,14 +245,17 @@
// Show folder
if (this.sharedDetail.type === 'folder') {
let homeDirectory = {
this.homeDirectory = {
unique_id: this.sharedDetail.item_id,
name: this.$t('locations.home'),
location: 'public',
}
// Get folder tree
this.$store.dispatch('getFolderTree')
// Load folder
this.$store.dispatch('browseShared', [{folder: homeDirectory, back: false, init: true}])
this.goHome()
}
// Get file
@@ -181,9 +266,6 @@
download() {
this.$downloadFile(this.sharedFile.file_url, this.sharedFile.name + '.' + this.sharedFile.mimetype)
},
fileViewClick() {
events.$emit('contextMenu:hide')
},
contextMenu(event, item) {
events.$emit('contextMenu:show', event, item)
},
@@ -288,4 +370,11 @@
}
}
.empty-note {
&.navigator {
padding: 5px 25px 10px;
}
}
</style>