vue components refactoring

This commit is contained in:
Čarodej
2022-04-13 16:19:10 +02:00
parent 6a4bfa8bfe
commit 338f8664b7
251 changed files with 1068 additions and 1943 deletions

View File

@@ -0,0 +1,52 @@
<template>
<div class="action-button">
<x-icon size="12" class="icon text-theme dark-text-theme" v-if="icon === 'x'" />
<edit-2-icon size="12" class="icon text-theme dark-text-theme" v-if="icon === 'pencil-alt'" />
<span class="label">
<slot />
</span>
</div>
</template>
<script>
import { Edit2Icon, XIcon } from 'vue-feather-icons'
export default {
name: 'ActionButton',
props: ['icon'],
components: {
Edit2Icon,
XIcon,
},
}
</script>
<style lang="scss" scoped>
@import '../../../../sass/vuefilemanager/variables';
@import '../../../../sass/vuefilemanager/mixins';
.action-button {
cursor: pointer;
.label {
@include font-size(12);
font-weight: 600;
}
.icon {
@include font-size(10);
vertical-align: middle;
display: inline-block;
margin-right: 2px;
path,
circle,
line {
color: inherit;
}
}
}
.dark {
}
</style>

View File

@@ -0,0 +1,47 @@
<template>
<button
class="group mx-auto inline-block flex items-center whitespace-nowrap rounded-lg border-2 border-black px-7 py-2.5 dark:border-gray-300"
>
<span class="pr-1 text-lg font-extrabold">
{{ text }}
</span>
<refresh-cw-icon v-if="loading" size="20" class="vue-feather text-theme sync-alt -mr-1" />
<chevron-right-icon v-if="!loading && icon" size="20" class="vue-feather text-theme -mr-1" />
</button>
</template>
<script>
import { ChevronRightIcon, RefreshCwIcon } from 'vue-feather-icons'
export default {
name: 'AuthContent',
props: ['loading', 'icon', 'text'],
components: {
ChevronRightIcon,
RefreshCwIcon,
},
data() {
return {
isVisible: false,
}
},
created() {
this.isVisible = this.visible
},
}
</script>
<style scoped lang="scss">
.sync-alt {
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0);
}
100% {
transform: rotate(360deg);
}
}
</style>

View File

@@ -0,0 +1,118 @@
<template>
<button class="button-base" :class="buttonStyle" type="button">
<div v-if="loading" class="icon">
<refresh-cw-icon size="16" class="animate-spin" />
</div>
<div class="content">
<slot v-if="!loading" />
</div>
</button>
</template>
<script>
import { RefreshCwIcon } from 'vue-feather-icons'
export default {
name: 'ButtonBase',
props: ['buttonStyle', 'loading'],
components: {
RefreshCwIcon,
},
}
</script>
<style scoped lang="scss">
@import '../../../../sass/vuefilemanager/variables';
@import '../../../../sass/vuefilemanager/mixins';
.button-base {
@include font-size(15);
font-weight: 700;
cursor: pointer;
transition: 0.15s all ease;
border-radius: 8px;
border: 0;
padding: 10px 28px;
white-space: nowrap;
display: flex;
align-items: center;
justify-content: center;
.icon {
line-height: 1;
margin-right: 10px;
}
&:active {
transform: scale(0.95);
}
&.theme-solid {
.content {
color: white;
}
}
&.danger {
background: rgba($danger, 0.1);
.content {
color: $danger;
}
polyline,
path {
stroke: $danger;
}
}
&.danger-solid {
background: $danger;
.content {
color: white;
}
polyline,
path {
stroke: white;
}
}
&.secondary {
background: $light_background;
.content {
color: $text;
}
polyline,
path {
stroke: $text;
}
}
}
.dark {
.button-base {
&.secondary {
background: $dark_mode_foreground;
.content {
color: $dark_mode_text_primary;
}
polyline,
path {
color: inherit;
}
}
}
.popup-wrapper {
.button-base.secondary {
background: lighten($dark_mode_foreground, 3%);
}
}
}
</style>

View File

@@ -0,0 +1,48 @@
<template>
<label :class="buttonStyle" label="file" class="button file-input button-base">
<slot />
<input accept="*" v-show="false" @change="emmitFiles" id="file" type="file" name="files[]" multiple />
</label>
</template>
<script>
export default {
name: 'ButtonBase',
props: ['buttonStyle'],
data() {
return {
files: undefined,
}
},
methods: {
emmitFiles(e) {
this.$uploadFiles(e.target.files)
},
},
}
</script>
<style scoped lang="scss">
@import '../../../../sass/vuefilemanager/variables';
@import '../../../../sass/vuefilemanager/mixins';
.button-base {
@include font-size(15);
font-weight: 700;
cursor: pointer;
transition: 0.15s all ease;
border-radius: 8px;
border: 0;
padding: 10px 28px;
display: inline-block;
&:active {
transform: scale(0.95);
}
&.secondary {
color: $text;
background: $light_background;
}
}
</style>

View File

@@ -0,0 +1,94 @@
<template>
<button class="mr-2 inline-block rounded-xl bg-light-background py-2 px-3.5 dark:bg-2x-dark-foreground">
<div class="flex items-center">
<hard-drive-icon v-if="icon === 'hard-drive'" size="15" class="vue-feather dark-text-theme" />
<upload-cloud-icon v-if="icon === 'upload-cloud'" size="15" class="vue-feather dark-text-theme" />
<link-icon v-if="icon === 'share'" size="15" class="vue-feather dark-text-theme" />
<trash2-icon v-if="icon === 'trash2'" size="15" class="vue-feather dark-text-theme" />
<users-icon v-if="icon === 'users'" size="15" class="vue-feather dark-text-theme" />
<user-check-icon v-if="icon === 'user-check'" size="15" class="vue-feather dark-text-theme" />
<search-icon v-if="icon === 'search'" size="15" class="vue-feather dark-text-theme" />
<refresh-cw-icon v-if="icon === 'refresh'" size="15" class="vue-feather dark-text-theme" />
<download-icon v-if="icon === 'download'" size="15" class="vue-feather dark-text-theme" />
<copy-icon v-if="icon === 'copy'" size="15" class="vue-feather dark-text-theme" />
<filter-icon v-if="icon === 'filter'" size="15" class="vue-feather dark-text-theme" />
<credit-card-icon v-if="icon === 'credit-card'" size="15" class="vue-feather dark-text-theme" />
<folder-plus-icon v-if="icon === 'folder-plus'" size="15" class="vue-feather dark-text-theme" />
<list-icon v-if="icon === 'th-list'" size="15" class="vue-feather dark-text-theme" />
<trash-icon v-if="icon === 'trash'" size="15" class="vue-feather dark-text-theme" />
<grid-icon v-if="icon === 'th'" size="15" class="vue-feather dark-text-theme" />
<user-plus-icon v-if="icon === 'user-plus'" size="15" class="vue-feather dark-text-theme" />
<plus-icon v-if="icon === 'plus'" size="15" class="vue-feather dark-text-theme" />
<check-square-icon v-if="icon === 'check-square'" size="15" class="vue-feather dark-text-theme" />
<x-square-icon v-if="icon === 'x-square'" size="15" class="vue-feather dark-text-theme" />
<check-icon v-if="icon === 'check'" size="15" class="vue-feather dark-text-theme" />
<dollar-sign-icon v-if="icon === 'dollar-sign'" size="15" class="vue-feather dark-text-theme" />
<sorting-icon v-if="icon === 'preview-sorting'" class="vue-feather dark-text-theme preview-sorting" />
<cloud-plus-icon v-if="icon === 'cloud-plus'" class="vue-feather dark-text-theme preview-sorting" />
<span v-if="$slots.default" class="ml-2 text-sm font-bold">
<slot />
</span>
</div>
</button>
</template>
<script>
import {
UserCheckIcon,
HardDriveIcon,
UploadCloudIcon,
LinkIcon,
Trash2Icon,
UsersIcon,
SearchIcon,
RefreshCwIcon,
DownloadIcon,
CopyIcon,
FilterIcon,
DollarSignIcon,
CheckIcon,
XSquareIcon,
CheckSquareIcon,
FolderPlusIcon,
ListIcon,
GridIcon,
TrashIcon,
UserPlusIcon,
PlusIcon,
CreditCardIcon,
} from 'vue-feather-icons'
import CloudPlusIcon from '../../Icons/CloudPlusIcon'
import SortingIcon from '../../Icons/SortingIcon'
export default {
name: 'MobileActionButton',
props: ['icon'],
components: {
UserCheckIcon,
HardDriveIcon,
UploadCloudIcon,
LinkIcon,
Trash2Icon,
UsersIcon,
CheckSquareIcon,
DollarSignIcon,
CreditCardIcon,
FolderPlusIcon,
RefreshCwIcon,
CloudPlusIcon,
UserPlusIcon,
DownloadIcon,
SortingIcon,
XSquareIcon,
FilterIcon,
SearchIcon,
CheckIcon,
TrashIcon,
PlusIcon,
CopyIcon,
ListIcon,
GridIcon,
},
}
</script>

View File

@@ -0,0 +1,79 @@
<template>
<button class="mobile-action-button">
<div class="flex">
<cloud-plus-icon class="icon dark-text-theme" size="15" />
<label label="file" class="label button file-input button-base">
<slot />
<input @change="emmitFiles" v-show="false" id="file" type="file" name="files[]" multiple />
</label>
</div>
</button>
</template>
<script>
import { UploadCloudIcon } from 'vue-feather-icons'
import CloudPlusIcon from '../../Icons/CloudPlusIcon'
export default {
name: 'MobileActionButtonUpload',
components: {
CloudPlusIcon,
UploadCloudIcon,
},
methods: {
emmitFiles(e) {
this.$uploadFiles(e.target.files)
},
},
}
</script>
<style scoped lang="scss">
@import '../../../../sass/vuefilemanager/variables';
@import '../../../../sass/vuefilemanager/mixins';
.mobile-action-button {
background: $light_background;
margin-right: 8px;
border-radius: 8px;
padding: 7px 14px;
cursor: pointer;
border: none;
.flex {
display: flex;
align-items: center;
}
.icon {
vertical-align: middle;
margin-right: 10px;
@include font-size(14);
}
.label {
vertical-align: middle;
@include font-size(14);
font-weight: 700;
color: $text;
}
}
.dark {
.mobile-action-button {
background: $dark_mode_foreground;
path,
line,
polyline,
rect,
circle {
color: inherit;
}
.label {
color: $dark_mode_text_primary;
}
}
}
</style>

View File

@@ -0,0 +1,36 @@
<template>
<div
@click="$openSpotlight()"
class="relative cursor-pointer rounded-lg bg-light-background dark:bg-dark-foreground"
>
<div class="flex w-56 items-center justify-between px-4 py-2.5 text-left xl:w-64">
<div class="flex items-center">
<search-icon size="18" class="vue-feather text-gray-400 dark:text-gray-600" />
<span class="pl-2.5 text-xs font-bold text-gray-400 dark:text-gray-600">
{{ $t('search_anything') }}
</span>
</div>
<span
class="rounded border px-1 py-0.5 text-xs font-bold tracking-normal text-gray-400 dark:border-slate-200 dark:border-opacity-5 dark:text-gray-600"
>
{{ metaKeyIcon }}+K
</span>
</div>
</div>
</template>
<script>
import { SearchIcon } from 'vue-feather-icons'
export default {
name: 'SearchBarButton',
components: {
SearchIcon,
},
computed: {
metaKeyIcon() {
return this.$isApple() ? '⌘' : 'Ctrl'
},
},
}
</script>

View File

@@ -0,0 +1,41 @@
<template>
<div
v-if="config.allowedFacebookLogin || config.allowedGoogleLogin || config.allowedGithubLogin"
class="mb-10 flex items-center justify-center"
>
<div v-if="config.allowedFacebookLogin" class="mx-5 cursor-pointer">
<facebook-icon @click="socialiteRedirect('facebook')" />
</div>
<div v-if="config.allowedGithubLogin" class="mx-5 cursor-pointer">
<github-icon @click="socialiteRedirect('github')" />
</div>
<div v-if="config.allowedGoogleLogin" class="mx-5 cursor-pointer">
<google-icon @click="socialiteRedirect('google')" class="vue-feather"/>
</div>
</div>
</template>
<script>
import { FacebookIcon, GithubIcon } from 'vue-feather-icons'
import GoogleIcon from "../../Icons/GoogleIcon"
import { mapGetters } from 'vuex'
export default {
name: 'SocialLoginButtons',
components: {
FacebookIcon,
GoogleIcon,
GithubIcon,
},
computed: {
...mapGetters(['config']),
},
methods: {
socialiteRedirect(provider) {
this.$store.dispatch('socialiteRedirect', provider)
},
},
}
</script>

View File

@@ -0,0 +1,82 @@
<template>
<button
class="group inline-flex h-[42px] w-[42px] cursor-pointer items-center justify-center rounded-lg hover:bg-light-background dark:hover:bg-2x-dark-foreground"
:title="action"
>
<corner-down-right-icon v-if="source === 'move'" size="19" class="vue-feather group-hover-text-theme" />
<download-cloud-icon v-if="source === 'download'" size="19" class="vue-feather group-hover-text-theme" />
<folder-plus-icon v-if="source === 'folder-plus'" size="19" class="vue-feather group-hover-text-theme" />
<user-plus-icon v-if="source === 'user-plus'" size="19" class="vue-feather group-hover-text-theme" />
<zoom-in-icon v-if="source === 'zoom-in'" size="19" class="vue-feather group-hover-text-theme" />
<zoom-out-icon v-if="source === 'zoom-out'" size="19" class="vue-feather group-hover-text-theme" />
<edit-2-icon v-if="source === 'rename'" size="19" class="vue-feather group-hover-text-theme" />
<printer-icon v-if="source === 'print'" size="19" class="vue-feather group-hover-text-theme" />
<trash-2-icon v-if="source === 'trash'" size="19" class="vue-feather group-hover-text-theme" />
<list-icon v-if="source === 'th-list'" size="19" class="vue-feather group-hover-text-theme" />
<info-icon
v-if="source === 'info'"
size="19"
class="vue-feather group-hover-text-theme"
:class="{ 'text-theme': isVisibleSidebar }"
/>
<grid-icon v-if="source === 'th'" size="19" class="vue-feather group-hover-text-theme" />
<link-icon v-if="source === 'share'" size="19" class="vue-feather group-hover-text-theme" />
<x-icon v-if="source === 'close'" size="19" class="vue-feather group-hover-text-theme" />
<search-icon v-if="source === 'search'" size="19" class="vue-feather group-hover-text-theme" />
<cloud-off-icon v-if="source === 'shared-off'" size="19" class="vue-feather group-hover-text-theme" />
<sorting-icon v-if="source === 'preview-sorting'" class="vue-feather group-hover-text-theme scale-125" />
<CloudPlusIcon v-if="source === 'cloud-plus'" class="vue-feather group-hover-text-theme scale-125" />
</button>
</template>
<script>
import SortingIcon from '../../Icons/SortingIcon'
import CloudPlusIcon from '../../Icons/CloudPlusIcon'
import {
SearchIcon,
UserPlusIcon,
CornerDownRightIcon,
DownloadCloudIcon,
FolderPlusIcon,
CloudOffIcon,
PrinterIcon,
ZoomOutIcon,
ZoomInIcon,
Trash2Icon,
Edit2Icon,
GridIcon,
ListIcon,
InfoIcon,
LinkIcon,
XIcon,
} from 'vue-feather-icons'
import { mapGetters } from 'vuex'
export default {
name: 'ToolbarButton',
props: ['source', 'action'],
computed: {
...mapGetters(['isVisibleSidebar']),
},
components: {
SearchIcon,
CloudPlusIcon,
UserPlusIcon,
SortingIcon,
CornerDownRightIcon,
DownloadCloudIcon,
FolderPlusIcon,
CloudOffIcon,
PrinterIcon,
ZoomOutIcon,
ZoomInIcon,
Trash2Icon,
Edit2Icon,
ListIcon,
GridIcon,
InfoIcon,
LinkIcon,
XIcon,
},
}
</script>

View File

@@ -0,0 +1,265 @@
<template>
<div
:class="{
'bg-light-background dark:bg-dark-foreground': isClicked && canHover,
'dark:hover:bg-dark-foreground lg:hover:bg-light-background': canHover,
}"
class="relative z-0 flex h-48 select-none flex-wrap items-center justify-center rounded-lg border-2 border-dashed border-transparent px-1 pt-2 text-center sm:h-56 lg:h-60"
:draggable="canDrag"
spellcheck="false"
>
<!--MultiSelecting for the mobile version-->
<CheckBox v-if="isMultiSelectMode" :is-clicked="isClicked" class="mr-5" />
<div class="w-full">
<!--Item thumbnail-->
<div class="relative mx-auto">
<!--Emoji Icon-->
<Emoji
v-if="entry.data.attributes.emoji"
:emoji="entry.data.attributes.emoji"
class="mb-10 inline-block scale-150 transform text-5xl"
/>
<!--Folder Icon-->
<FolderIcon
v-if="isFolder && !entry.data.attributes.emoji"
:item="entry"
class="mt-3 mb-5 inline-block scale-150 transform lg:mt-2 lg:mb-8"
/>
<!--File Icon-->
<div
v-if="isFile || isVideo || isAudio || (isImage && !entry.data.attributes.thumbnail)"
class="relative mx-auto w-24"
>
<!--Member thumbnail for team folders-->
<MemberAvatar
v-if="user && canShowAuthor"
:size="38"
:is-border="true"
:member="entry.data.relationships.creator"
class="absolute right-2 -bottom-5 z-10 z-10 scale-75 transform lg:-bottom-7 lg:scale-100"
/>
<FileIconThumbnail
:entry="entry"
class="z-0 mt-5 mb-10 scale-125 transform lg:mb-12 lg:mt-6 lg:scale-150"
/>
</div>
<!--Image thumbnail-->
<div
v-if="isImage && entry.data.attributes.thumbnail"
class="relative mb-4 inline-block h-24 w-28 lg:h-28 lg:w-36"
>
<!--Member thumbnail for team folders-->
<MemberAvatar
v-if="user && canShowAuthor"
:size="38"
:is-border="true"
:member="entry.data.relationships.creator"
class="absolute -right-3 -bottom-2.5 z-10 scale-75 transform lg:scale-100"
/>
<img
class="h-full w-full rounded-lg object-cover shadow-lg pointer-events-none"
:src="imageSrc"
alt=""
loading="lazy"
@error="replaceByOriginal"
/>
</div>
</div>
<!--Item Info-->
<div class="text-center">
<!--Item Title-->
<span
class="inline-block w-full overflow-hidden text-ellipsis whitespace-nowrap text-sm font-bold leading-3 tracking-tight md:px-6"
:class="{ 'hover:underline': canEditName }"
ref="name"
@input="renameItem"
@keydown.delete.stop
@click.stop
@keydown.enter="$refs.name.blur()"
:contenteditable="canEditName"
>
{{ itemName }}
</span>
<!--Item sub line-->
<div class="flex items-center justify-center">
<!--File & Image sub line-->
<small v-if="!isFolder" class="block text-xs text-gray-500 dark:text-gray-500">
<link-icon size="12" class="text-theme dark-text-theme vue-feather inline-block mr-0.5 mb-1" />
{{ entry.data.attributes.filesize }},
<span class="hidden text-xs text-gray-500 dark:text-gray-500 lg:inline-block"
>{{ timeStamp }}</span
>
</small>
<!--Folder sub line-->
<small v-if="isFolder" class="block text-xs text-gray-500 dark:text-gray-500">
<link-icon v-if="canShowLinkIcon" size="12" class="text-theme dark-text-theme vue-feather mr-0.5 mb-1 inline-block" />
{{ folderItems === 0 ? $t('empty') : $tc('folder.item_counts', folderItems)
}}, <span class="hidden text-xs text-gray-500 dark:text-gray-500 lg:inline-block"
>{{ timeStamp }}</span
>
</small>
</div>
</div>
<!-- Mobile item action button-->
<div
v-if="mobileHandler && !isMultiSelectMode && $isMobile()"
class="relative flex items-center justify-center py-0.5 px-2"
>
<div @mouseup.stop="$openInDetailPanel(entry)" class="hidden p-2.5 sm:block">
<eye-icon size="18" class="vue-feather inline-block opacity-30" />
</div>
<div @mouseup.stop="showItemActions" class="p-2.5">
<MoreHorizontalIcon size="18" class="vue-feather text-theme dark-text-theme inline-block" />
</div>
</div>
</div>
</div>
</template>
<script>
import FolderIcon from '../../Icons/FolderIcon'
import { LinkIcon, MoreHorizontalIcon, EyeIcon } from 'vue-feather-icons'
import FileIconThumbnail from '../../Icons/FileIconThumbnail'
import MemberAvatar from '../Others/MemberAvatar'
import Emoji from '../../Emoji/Emoji'
import CheckBox from '../../Inputs/CheckBox'
import { debounce } from 'lodash'
import { mapGetters } from 'vuex'
import { events } from '../../../bus'
export default {
name: 'ItemGrid',
components: {
FileIconThumbnail,
MoreHorizontalIcon,
MemberAvatar,
FolderIcon,
CheckBox,
LinkIcon,
EyeIcon,
Emoji,
},
props: ['mobileHandler', 'entry', 'canHover'],
data() {
return {
mobileMultiSelect: false,
itemName: undefined,
imageSrc: undefined,
}
},
computed: {
...mapGetters(['isMultiSelectMode', 'clipboard', 'user']),
isClicked() {
return this.clipboard.some((element) => element.data.id === this.entry.data.id)
},
isAudio() {
return this.entry.data.type === 'audio'
},
isVideo() {
return this.entry.data.type === 'video'
},
isFile() {
return this.entry.data.type === 'file'
},
isImage() {
return this.entry.data.type === 'image'
},
isFolder() {
return this.entry.data.type === 'folder'
},
timeStamp() {
return this.entry.data.attributes.deleted_at
? this.$t('entry_thumbnail.deleted_at', {
time: this.entry.data.attributes.deleted_at,
})
: this.entry.data.attributes.created_at
},
canEditName() {
return (
!this.$isMobile() &&
!this.$isThisRoute(this.$route, ['Trash', 'SharedSingleFile']) &&
!this.$checkPermission('visitor')
)
},
folderItems() {
return this.entry.data.attributes.deleted_at
? this.entry.data.attributes.trashed_items
: this.entry.data.attributes.items
},
canShowAuthor() {
return (
this.$isThisRoute(this.$route, ['SharedWithMe', 'TeamFolders'])
&& !this.isFolder
&& this.entry.data.relationships.creator
&& this.user.data.id !== this.entry.data.relationships.creator.data.id
)
},
canShowLinkIcon() {
return this.entry.data.relationships.shared && !this.$isThisRoute(this.$route, ['SharedSingleFile'])
},
canDrag() {
return !this.isDeleted && this.$checkPermission(['master', 'editor'])
},
},
methods: {
getImageSrc() {
this.imageSrc = this.entry.data.attributes.mimetype === 'svg'
? this.entry.data.attributes.file_url
: this.entry.data.attributes.thumbnail.sm
},
replaceByOriginal() {
this.imageSrc = this.entry.data.attributes.file_url
},
showItemActions() {
this.$store.commit('CLIPBOARD_CLEAR')
this.$store.commit('ADD_ITEM_TO_CLIPBOARD', this.entry)
this.$showMobileMenu('file-menu')
events.$emit('mobile-context-menu:show', this.entry)
},
renameItem: debounce(function (e) {
// Prevent submit empty string
if (e.target.innerText.trim() === '') return
this.$store.dispatch('renameItem', {
id: this.entry.data.id,
type: this.entry.data.type,
name: e.target.innerText,
})
}, 300),
},
created() {
// Change item name
events.$on('change:name', (item) => {
if (this.entry.data.id === item.id) this.itemName = item.name
})
// Autofocus after newly created folder
events.$on('newFolder:focus', (id) => {
if (!this.$isMobile() && this.entry.data.id === id) {
this.$refs.name.focus()
document.execCommand('selectAll')
}
})
// Set item name to own component variable
this.itemName = this.entry.data.attributes.name
if (this.entry.data.type === 'image') {
this.getImageSrc()
}
},
}
</script>

View File

@@ -0,0 +1,245 @@
<template>
<div
:class="{
'bg-light-background dark:bg-dark-foreground': isClicked && highlight,
'hover:bg-light-background dark:hover:bg-dark-foreground': highlight,
}"
class="flex select-none items-center rounded-xl border-2 border-dashed border-transparent px-2.5 py-2"
:draggable="canDrag"
spellcheck="false"
>
<!--MultiSelecting for the mobile version-->
<CheckBox v-if="isMultiSelectMode" v-model="isChecked" :is-clicked="isClicked" class="mr-5" />
<!--Item thumbnail-->
<div class="relative w-16 shrink-0">
<!--Member thumbnail for team folders-->
<MemberAvatar
v-if="user && canShowAuthor"
:size="28"
:is-border="true"
:member="entry.data.relationships.creator"
class="absolute right-1.5 -bottom-2 z-10"
/>
<!--Emoji Icon-->
<Emoji
v-if="entry.data.attributes.emoji"
:emoji="entry.data.attributes.emoji"
class="ml-1 scale-110 transform text-5xl"
/>
<!--Folder Icon-->
<FolderIcon v-if="isFolder && !entry.data.attributes.emoji" :item="entry" />
<!--File Icon-->
<FileIconThumbnail
v-if="isFile || isVideo || isAudio || (isImage && !entry.data.attributes.thumbnail)"
:entry="entry"
class="pr-2"
/>
<!--Image thumbnail-->
<img
v-if="isImage && entry.data.attributes.thumbnail"
class="ml-0.5 h-12 w-12 rounded object-cover pointer-events-none"
:src="imageSrc"
alt=""
loading="lazy"
@error="replaceByOriginal"
/>
</div>
<!--Item Info-->
<div class="pl-2">
<!--Item Title-->
<span
class="mb-0.5 block overflow-hidden text-ellipsis whitespace-nowrap text-sm font-bold"
:class="{ 'hover:underline': canEditName }"
style="max-width: 240px"
ref="name"
@input="renameItem"
@keydown.delete.stop
@click.stop
@keydown.enter="$refs.name.blur()"
:contenteditable="canEditName"
>
{{ itemName }}
</span>
<!--Item sub line-->
<div class="flex items-center">
<!--Shared Icon-->
<div v-if="$checkPermission('master') && entry.data.relationships.shared">
<link-icon size="12" class="text-theme dark-text-theme vue-feather mr-1.5" />
</div>
<!--File & Image sub line-->
<small v-if="!isFolder" class="block text-xs text-gray-500 dark:text-gray-500">
{{ entry.data.attributes.filesize }}, {{ timeStamp }}
</small>
<!--Folder sub line-->
<small v-if="isFolder" class="block text-xs text-gray-500 dark:text-gray-500">
{{ folderItems === 0 ? $t('empty') : $tc('folder.item_counts', folderItems) }},
{{ timeStamp }}
</small>
</div>
</div>
<!-- Mobile item action button-->
<div v-if="mobileHandler && !isMultiSelectMode && $isMobile()" class="relative flex-grow pr-1 text-right">
<div
@mouseup.stop="$openInDetailPanel(entry)"
class="absolute right-10 -mr-4 hidden -translate-y-2/4 transform p-2.5 lg:block"
>
<eye-icon size="18" class="vue-feather inline-block opacity-30" />
</div>
<div @mouseup.stop="showItemActions" class="absolute right-0 -mr-4 -translate-y-2/4 transform p-2.5">
<MoreVerticalIcon size="18" class="vue-feather text-theme dark-text-theme inline-block" />
</div>
</div>
</div>
</template>
<script>
import Emoji from '../../Emoji/Emoji'
import FolderIcon from '../../Icons/FolderIcon'
import { LinkIcon, MoreVerticalIcon, EyeIcon } from 'vue-feather-icons'
import FileIconThumbnail from '../../Icons/FileIconThumbnail'
import MemberAvatar from '../Others/MemberAvatar'
import CheckBox from '../../Inputs/CheckBox'
import { debounce } from 'lodash'
import { mapGetters } from 'vuex'
import { events } from '../../../bus'
export default {
name: 'ItemList',
components: {
FileIconThumbnail,
MoreVerticalIcon,
MemberAvatar,
FolderIcon,
CheckBox,
LinkIcon,
EyeIcon,
Emoji,
},
props: ['mobileHandler', 'highlight', 'entry'],
watch: {
isChecked: function (val) {
if (val) {
this.$store.commit('ADD_ITEM_TO_CLIPBOARD', this.entry)
} else {
this.$store.commit('REMOVE_ITEM_FROM_CLIPBOARD', this.entry.data.id)
}
}
},
data() {
return {
mobileMultiSelect: false,
itemName: undefined,
isSelected: false,
isChecked: false,
imageSrc: undefined,
}
},
computed: {
...mapGetters(['isMultiSelectMode', 'clipboard', 'user']),
isClicked() {
return this.clipboard.some((element) => element.data.id === this.entry.data.id)
},
isVideo() {
return this.entry.data.type === 'video'
},
isAudio() {
return this.entry.data.type === 'audio'
},
isFile() {
return this.entry.data.type === 'file'
},
isImage() {
return this.entry.data.type === 'image'
},
isFolder() {
return this.entry.data.type === 'folder'
},
timeStamp() {
return this.entry.data.attributes.deleted_at
? this.$t('item_thumbnail.deleted_at', {
time: this.entry.data.attributes.deleted_at,
})
: this.entry.data.attributes.created_at
},
canEditName() {
return (
!this.$isMobile() &&
!this.$isThisRoute(this.$route, ['Trash']) &&
!this.$checkPermission('visitor') &&
!(this.sharedDetail && this.sharedDetail.attributes.type === 'file')
)
},
folderItems() {
return this.entry.data.attributes.deleted_at
? this.entry.data.attributes.trashed_items
: this.entry.data.attributes.items
},
canShowAuthor() {
return !this.isFolder && (this.entry.data.relationships.creator && this.user.data.id !== this.entry.data.relationships.creator.data.id)
},
canDrag() {
return !this.isDeleted && this.$checkPermission(['master', 'editor'])
},
},
methods: {
getImageSrc() {
this.imageSrc = this.entry.data.attributes.mimetype === 'svg'
? this.entry.data.attributes.file_url
: this.entry.data.attributes.thumbnail.xs
},
replaceByOriginal() {
this.imageSrc = this.entry.data.attributes.file_url
},
showItemActions() {
this.$store.commit('CLIPBOARD_CLEAR')
this.$store.commit('ADD_ITEM_TO_CLIPBOARD', this.entry)
this.$showMobileMenu('file-menu')
events.$emit('mobile-context-menu:show', this.entry)
},
renameItem: debounce(function (e) {
// Prevent submit empty string
if (e.target.innerText.trim() === '') return
this.$store.dispatch('renameItem', {
id: this.entry.data.id,
type: this.entry.data.type,
name: e.target.innerText,
})
}, 300),
},
created() {
// Change item name
events.$on('change:name', (item) => {
if (this.entry.data.id === item.id) {
this.itemName = item.name
}
})
// Autofocus after newly created folder
events.$on('newFolder:focus', (id) => {
if (!this.$isMobile() && this.entry.data.id === id) {
this.$refs.name.focus()
document.execCommand('selectAll')
}
})
// Set item name to own component variable
this.itemName = this.entry.data.attributes.name
if (this.entry.data.type === 'image') {
this.getImageSrc()
}
},
}
</script>

View File

@@ -0,0 +1,140 @@
<template>
<div class="flex select-none items-center rounded-xl" spellcheck="false">
<!--Item thumbnail-->
<div class="relative w-16">
<!--Member thumbnail for team folders-->
<MemberAvatar
v-if="user && canShowAuthor"
:size="28"
:is-border="true"
:member="item.data.relationships.creator"
class="absolute right-1.5 -bottom-2 z-10"
/>
<!--Emoji Icon-->
<Emoji
v-if="item.data.attributes.emoji"
:emoji="item.data.attributes.emoji"
class="ml-1 scale-110 transform text-5xl"
/>
<!--Folder Icon-->
<FolderIcon v-if="isFolder && !item.data.attributes.emoji" :item="item" />
<!--File Icon-->
<FileIconThumbnail
v-if="isFile || isVideo || isAudio || (isImage && !item.data.attributes.thumbnail)"
:entry="item"
class="pr-2"
/>
<!--Image thumbnail-->
<img
v-if="isImage && item.data.attributes.thumbnail"
class="ml-0.5 h-12 w-12 rounded object-cover"
:src="item.data.attributes.thumbnail.xs"
:alt="item.data.attributes.name"
loading="lazy"
/>
</div>
<!--Item Info-->
<div class="pl-2">
<!--Item Title-->
<b
class="mb-0.5 block overflow-hidden text-ellipsis whitespace-nowrap text-sm hover:underline"
style="max-width: 240px"
>
{{ item.data.attributes.name }}
</b>
<!--Item sub line-->
<div class="flex items-center">
<!--Shared Icon-->
<div v-if="$checkPermission('master') && item.data.relationships.shared">
<link-icon size="12" class="text-theme dark-text-theme vue-feather mr-1.5" />
</div>
<!--File & Image sub line-->
<small v-if="!isFolder" class="block text-xs text-gray-500">
{{ item.data.attributes.filesize }}, {{ timeStamp }}
</small>
<!--Folder sub line-->
<small v-if="isFolder" class="block text-xs text-gray-500">
{{ folderItems === 0 ? $t('empty') : $tc('folder.item_counts', folderItems) }},
{{ timeStamp }}
</small>
</div>
</div>
</div>
</template>
<script>
import { LinkIcon, EyeIcon } from 'vue-feather-icons'
import FileIconThumbnail from '../../Icons/FileIconThumbnail'
import MemberAvatar from '../Others/MemberAvatar'
import Emoji from '../../Emoji/Emoji'
import { mapGetters } from 'vuex'
import FolderIcon from '../../Icons/FolderIcon'
export default {
name: 'ThumbnailItem',
props: ['setFolderIcon', 'item', 'info'],
components: {
FileIconThumbnail,
MemberAvatar,
FolderIcon,
Emoji,
LinkIcon,
EyeIcon,
},
computed: {
...mapGetters(['isMultiSelectMode', 'clipboard', 'user']),
isClicked() {
return this.clipboard.some((element) => element.data.id === this.item.data.id)
},
isVideo() {
return this.item.data.type === 'video'
},
isAudio() {
return this.item.data.type === 'audio'
},
isFile() {
return this.item.data.type === 'file'
},
isImage() {
return this.item.data.type === 'image'
},
isFolder() {
return this.item.data.type === 'folder'
},
timeStamp() {
return this.item.data.attributes.deleted_at
? this.$t('item_thumbnail.deleted_at', {
time: this.item.data.attributes.deleted_at,
})
: this.item.data.attributes.created_at
},
canEditName() {
return (
!this.$isMobile() &&
!this.$isThisRoute(this.$route, ['Trash']) &&
!this.$checkPermission('visitor') &&
!(this.sharedDetail && this.sharedDetail.attributes.type === 'file')
)
},
folderItems() {
return this.item.data.attributes.deleted_at
? this.item.data.attributes.trashed_items
: this.item.data.attributes.items
},
canShowAuthor() {
return !this.isFolder && (this.item.data.relationships.creator && this.user.data.id !== this.item.data.relationships.creator.data.id)
},
canDrag() {
return !this.isDeleted && this.$checkPermission(['master', 'editor'])
},
},
}
</script>

View File

@@ -0,0 +1,38 @@
<template>
<b class="color-label inline-block rounded-lg py-1 px-2 text-xs font-bold capitalize" :class="color">
<slot />
</b>
</template>
<script>
export default {
name: 'ColorLabel',
props: ['color'],
}
</script>
<style lang="scss" scoped>
@import '../../../../sass/vuefilemanager/variables';
.color-label {
&.purple {
color: $purple;
background: rgba($purple, 0.1);
}
&.yellow {
color: $yellow;
background: rgba($yellow, 0.1);
}
&.green {
color: $theme;
background: rgba($theme, 0.1);
}
&.red {
color: $danger;
background: rgba($danger, 0.1);
}
}
</style>

View File

@@ -0,0 +1,76 @@
<template>
<div class="mb-8 flex items-center">
<edit-2-icon v-if="!icon" size="22" class="vue-feather text-theme dark-text-theme mr-3" />
<frown-icon v-if="icon === 'frown'" size="22" class="vue-feather text-theme dark-text-theme mr-3" />
<list-icon v-if="icon === 'list'" size="22" class="vue-feather text-theme dark-text-theme mr-3" />
<info-icon v-if="icon === 'info'" size="22" class="vue-feather text-theme dark-text-theme mr-3" />
<database-icon v-if="icon === 'database'" size="22" class="vue-feather text-theme dark-text-theme mr-3" />
<file-text-icon v-if="icon === 'file-text'" size="22" class="vue-feather text-theme dark-text-theme mr-3" />
<dollar-sign-icon v-if="icon === 'dollar'" size="22" class="vue-feather text-theme dark-text-theme mr-3" />
<credit-card-icon v-if="icon === 'credit-card'" size="22" class="vue-feather text-theme dark-text-theme mr-3" />
<bar-chart-icon v-if="icon === 'bar-chart'" size="22" class="vue-feather text-theme dark-text-theme mr-3" />
<settings-icon v-if="icon === 'settings'" size="22" class="vue-feather text-theme dark-text-theme mr-3" />
<hard-drive-icon v-if="icon === 'hard-drive'" size="22" class="vue-feather text-theme dark-text-theme mr-3" />
<mail-icon v-if="icon === 'mail'" size="22" class="vue-feather text-theme dark-text-theme mr-3" />
<smartphone-icon v-if="icon === 'smartphone'" size="22" class="vue-feather text-theme dark-text-theme mr-3" />
<shield-icon v-if="icon === 'shield'" size="22" class="vue-feather text-theme dark-text-theme mr-3" />
<bell-icon v-if="icon === 'bell'" size="22" class="vue-feather text-theme dark-text-theme mr-3" />
<key-icon v-if="icon === 'key'" size="22" class="vue-feather text-theme dark-text-theme mr-3" />
<users-icon v-if="icon === 'users'" size="22" class="vue-feather text-theme dark-text-theme mr-3" />
<wifi-icon v-if="icon === 'wifi'" size="22" class="vue-feather text-theme dark-text-theme mr-3" />
<trending-up-icon v-if="icon === 'trending-up'" size="22" class="vue-feather text-theme dark-text-theme mr-3" />
<b class="text-md font-bold dark:text-gray-200 sm:text-lg">
<slot />
</b>
</div>
</template>
<script>
import {
TrendingUpIcon,
WifiIcon,
ListIcon,
MailIcon,
InfoIcon,
DatabaseIcon,
UsersIcon,
ShieldIcon,
CreditCardIcon,
DollarSignIcon,
SmartphoneIcon,
HardDriveIcon,
BarChartIcon,
SettingsIcon,
FileTextIcon,
FrownIcon,
Edit2Icon,
BellIcon,
KeyIcon,
} from 'vue-feather-icons'
export default {
name: 'FormLabel',
props: ['icon'],
components: {
TrendingUpIcon,
WifiIcon,
ListIcon,
MailIcon,
InfoIcon,
DatabaseIcon,
UsersIcon,
CreditCardIcon,
DollarSignIcon,
SmartphoneIcon,
HardDriveIcon,
BarChartIcon,
SettingsIcon,
FileTextIcon,
ShieldIcon,
FrownIcon,
Edit2Icon,
BellIcon,
KeyIcon,
},
}
</script>

View File

@@ -0,0 +1,44 @@
<template>
<div class="mb-14">
<!--Custom content-->
<slot />
<!--Default application logo-->
<div v-if="!$slots.default">
<!--Image logo-->
<img
v-if="config.app_logo"
class="mx-auto mb-6 h-16 md:h-20 mb-10"
:src="$getImage(logoSrc)"
:alt="config.app_name"
/>
<!--Text logo if image isn't available-->
<b v-if="!config.app_logo" class="mb-10 block text-xl font-bold">
{{ config.app_name }}
</b>
</div>
<h1 class="mb-0.5 text-3xl font-extrabold md:text-4xl">
{{ title }}
</h1>
<h2 class="text-xl font-normal md:text-2xl">
{{ description }}
</h2>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
name: 'Headline',
props: ['description', 'title'],
computed: {
...mapGetters(['config', 'isDarkMode']),
logoSrc() {
return this.isDarkMode && this.config.app_logo ? this.config.app_logo_dark : this.config.app_logo
}
},
}
</script>

View File

@@ -0,0 +1,33 @@
<template>
<b class="text-label">
<slot />
</b>
</template>
<script>
export default {
name: 'SectionTitle',
}
</script>
<style lang="scss" scoped>
@import '../../../../sass/vuefilemanager/variables';
@import '../../../../sass/vuefilemanager/mixins';
.text-label {
@include font-size(12);
color: #afafaf;
font-weight: 700;
display: block;
margin-bottom: 20px;
}
@media only screen and (max-width: 1024px) {
}
.dark {
.text-label {
color: $theme;
}
}
</style>

View File

@@ -0,0 +1,37 @@
<template>
<b class="text-label">
<slot />
</b>
</template>
<script>
export default {
name: 'TextLabel',
}
</script>
<style lang="scss" scoped>
@import '../../../../sass/vuefilemanager/variables';
@import '../../../../sass/vuefilemanager/mixins';
.text-label {
padding-left: 25px;
@include font-size(12);
color: #afafaf;
font-weight: 700;
display: block;
margin-bottom: 5px;
}
@media only screen and (max-width: 1024px) {
.text-label {
padding-left: 20px;
}
}
.dark {
.text-label {
opacity: 0.35;
}
}
</style>

View File

@@ -0,0 +1,35 @@
<template>
<div class="flex items-start">
<div class="mr-2">
<CheckSquareIcon v-if="icon === 'check-square'" class="text-theme vue-feather" size="19" />
<image-icon v-if="icon === 'image'" class="text-theme vue-feather" size="19" />
<video-icon v-if="icon === 'video'" class="text-theme vue-feather" size="19" />
<folder-icon v-if="icon === 'folder'" class="text-theme vue-feather" size="19" />
<file-icon v-if="icon === 'file'" class="text-theme vue-feather" size="19" />
</div>
<div>
<b class="block w-52 overflow-hidden text-ellipsis whitespace-nowrap text-sm font-bold 2xl:w-72">
{{ title }}
</b>
<small class="block text-xs font-bold text-gray-400">
{{ subtitle }}
</small>
</div>
</div>
</template>
<script>
import { CheckSquareIcon, FolderIcon, ImageIcon, VideoIcon, FileIcon } from 'vue-feather-icons'
export default {
name: 'TitlePreview',
props: ['subtitle', 'title', 'icon'],
components: {
CheckSquareIcon,
FolderIcon,
ImageIcon,
VideoIcon,
FileIcon,
},
}
</script>

View File

@@ -0,0 +1,16 @@
<template>
<ul class="list-info">
<slot />
</ul>
</template>
<script>
export default {
name: 'ListInfo',
}
</script>
<style lang="scss" scoped>
@import '../../../../sass/vuefilemanager/variables';
@import '../../../../sass/vuefilemanager/mixins';
</style>

View File

@@ -0,0 +1,19 @@
<template>
<div class="mb-4">
<small class="text-theme block text-xs font-bold">
{{ title }}
</small>
<b v-if="content" class="inline-block text-sm font-bold">
{{ content }}
</b>
<slot v-if="$slots.default" />
</div>
</template>
<script>
export default {
name: 'ListInfoItem',
props: ['content', 'title'],
}
</script>

View File

@@ -0,0 +1,52 @@
<template>
<div
class="mb-6 flex cursor-pointer items-center rounded-xl p-5 shadow-card"
:class="{
'dark:bg-green-700/30 bg-green-200': color === 'green',
'dark:bg-rose-700/30 bg-rose-200': color === 'rose',
}"
>
<refresh-cw-icon
v-if="isLoading"
size="18"
class="vue-feather mr-4 shrink-0 animate-spin"
:class="{
'text-green-700 dark:text-green-500': color === 'green',
'text-rose-700 dark:text-rose-500': color === 'rose',
}"
/>
<alert-octagon-icon
v-if="!isLoading"
size="18"
class="vue-feather mr-4 shrink-0"
:class="{
'text-green-700 dark:text-green-500': color === 'green',
'text-rose-700 dark:text-rose-500': color === 'rose',
}"
/>
<p
class="text-sm text-green-700 dark:text-green-500"
:class="{
'text-green-700 dark:text-green-500': color === 'green',
'text-rose-700 dark:text-rose-500': color === 'rose',
}"
>
<slot />
</p>
</div>
</template>
<script>
import {AlertOctagonIcon, RefreshCwIcon} from "vue-feather-icons";
export default {
name: 'AlertBox',
props: [
'isLoading',
'color',
],
components: {
AlertOctagonIcon,
RefreshCwIcon,
}
}
</script>

View File

@@ -0,0 +1,51 @@
<template>
<div id="card-navigation" style="height: 62px" class="mb-7">
<div
:class="{
'fixed top-0 left-0 right-0 z-10 rounded-none bg-white bg-opacity-50 px-6 backdrop-blur-lg backdrop-filter dark:bg-dark-foreground':
fixedNav,
}"
>
<div class="overflow-x-auto whitespace-nowrap">
<router-link
class="border-bottom-theme inline-block border-b-2 border-transparent px-4 py-5 text-sm font-bold"
:class="{
'text-theme': routeName === page.route,
'text-gray-600 dark:text-gray-100': routeName !== page.route,
}"
v-for="(page, i) in pages"
:to="{ name: page.route }"
:key="i"
replace
>
{{ page.title }}
</router-link>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'CardNavigation',
props: ['pages'],
computed: {
routeName() {
return this.$route.name
},
},
data() {
return {
fixedNav: false,
}
},
created() {
// Handle fixed mobile navigation
window.addEventListener('scroll', () => {
let card = document.getElementById('card-navigation')
this.fixedNav = card.getBoundingClientRect().top < 0
})
},
}
</script>

View File

@@ -0,0 +1,46 @@
<template>
<div
v-if="isVisibleDisclaimer"
class="fixed bottom-0 left-0 right-0 z-20 w-full rounded-tl-xl rounded-tr-lg bg-white p-4 shadow-xl dark:bg-dark-foreground sm:left-16 sm:right-auto sm:w-56 sm:p-3"
>
<span @click="closeDisclaimer" class="absolute -right-1 -top-1 cursor-pointer p-3">
<x-icon size="10" />
</span>
<i18n path="cookie_disclaimer.description" tag="p" class="text-xs">
<router-link :to="{ name: 'DynamicPage', params: { slug: 'cookie-policy' } }" class="text-theme text-xs">
{{ $t('cookie_disclaimer.button') }}
</router-link>
</i18n>
</div>
</template>
<script>
import { XIcon } from 'vue-feather-icons'
import { mapGetters } from 'vuex'
export default {
name: 'CookieDisclaimer',
components: {
XIcon,
},
computed: {
...mapGetters(['config']),
},
data() {
return {
isVisibleDisclaimer: false,
}
},
methods: {
closeDisclaimer() {
localStorage.setItem('isHiddenDisclaimer', 'true')
this.isVisibleDisclaimer = false
},
},
created() {
this.isVisibleDisclaimer =
this.config.installation === 'installation-done' && !localStorage.getItem('isHiddenDisclaimer')
},
}
</script>

View File

@@ -0,0 +1,79 @@
<template>
<div
v-show="isVisible"
id="drag-ui"
class="pointer-events-none fixed z-20 w-64 rounded-xl bg-white p-5 shadow-lg dark:bg-dark-foreground"
>
<TitlePreview icon="check-square" :title="title" :subtitle="subtitle" />
</div>
</template>
<script>
import TitlePreview from '../Labels/TitlePreview'
import { mapGetters } from 'vuex'
import { events } from '../../../bus'
export default {
name: 'DragUI',
components: {
TitlePreview,
},
computed: {
...mapGetters(['clipboard']),
title() {
let filesLength = this.clipboard.length,
hasDraggedItem = this.clipboard.includes(this.draggedItem)
// Title for multiple selected items
if (filesLength > 1 && hasDraggedItem) {
return this.$t('selected_multiple')
}
// Title for single item
if ((filesLength < 2 || !hasDraggedItem) && this.draggedItem) {
return this.draggedItem.data.attributes.name
}
},
subtitle() {
let filesLength = this.clipboard.length,
hasDraggedItem = this.clipboard.includes(this.draggedItem)
// Subtitle for multiple selected items
if (filesLength > 1 && hasDraggedItem) {
return filesLength + ' ' + this.$tc('items', filesLength)
}
if ((filesLength < 2 || !hasDraggedItem) && this.draggedItem) {
// Subtitle for single folder
if (this.draggedItem.data.type === 'folder') {
return this.draggedItem.items == 0
? this.$t('empty')
: this.$tc('folder.item_counts', this.draggedItem.items)
}
// Subtitle for single file
if (this.draggedItem.data.type !== 'folder' && this.draggedItem.data.attributes.mimetype) {
return '.' + this.draggedItem.data.attributes.mimetype
}
}
},
},
data() {
return {
isVisible: false,
draggedItem: undefined,
}
},
created() {
events.$on('dragstart', (data) => {
this.draggedItem = data
setTimeout(() => {
this.isVisible = true
}, 100)
})
events.$on('drop', () => (this.isVisible = false))
},
}
</script>

View File

@@ -0,0 +1,90 @@
<template>
<div>
<div class="flex items-center justify-between pt-0.5 pb-2" v-if="clipboard.data.attributes.date_time_original">
<b class="font-bold text-sm">{{ $t('time_data') }}</b>
<b class="font-bold text-sm">{{ clipboard.data.attributes.date_time_original }}</b>
</div>
<div class="flex items-center justify-between py-2" v-if="clipboard.data.attributes.artist">
<b class="font-bold text-sm">{{ $t('author') }}</b>
<b class="font-bold text-sm">{{ clipboard.data.attributes.artist }}</b>
</div>
<div class="flex items-center justify-between py-2" v-if="clipboard.data.attributes.width && clipboard.data.attributes.height">
<b class="font-bold text-sm">{{ $t('dimension') }}</b>
<b class="font-bold text-sm">{{ clipboard.data.attributes.width }}x{{ clipboard.data.attributes.height }}</b>
</div>
<div class="flex items-center justify-between py-2" v-if="clipboard.data.attributes.x_resolution && clipboard.data.attributes.y_resolution">
<b class="font-bold text-sm">{{ $t('resolution') }}</b>
<b class="font-bold text-sm">{{ clipboard.data.attributes.x_resolution }}x{{ clipboard.data.attributes.y_resolution }}</b>
</div>
<div class="flex items-center justify-between py-2" v-if="clipboard.data.attributes.color_space">
<b class="font-bold text-sm"> {{ $t('color_space') }}</b>
<b class="font-bold text-sm">{{ clipboard.data.attributes.color_space }}</b>
</div>
<div class="flex items-center justify-between py-2" v-if="clipboard.data.attributes.make">
<b class="font-bold text-sm">{{ $t('make') }}</b>
<b class="font-bold text-sm">{{ clipboard.data.attributes.make }}</b>
</div>
<div class="flex items-center justify-between py-2" v-if="clipboard.data.attributes.model">
<b class="font-bold text-sm">{{ $t('model') }}</b>
<b class="font-bold text-sm">{{ clipboard.data.attributes.model }}</b>
</div>
<div class="flex items-center justify-between py-2" v-if="clipboard.data.attributes.aperture_value">
<b class="font-bold text-sm">{{ $t('aperture_value') }}</b>
<b class="font-bold text-sm"> {{ clipboard.data.attributes.aperture_value }} </b>
</div>
<div class="flex items-center justify-between py-2" v-if="clipboard.data.attributes.exposure_time">
<b class="font-bold text-sm">{{ $t('exposure') }}</b>
<b class="font-bold text-sm">{{ clipboard.data.attributes.exposure_time }}</b>
</div>
<div class="flex items-center justify-between py-2" v-if="clipboard.data.attributes.focal_length">
<b class="font-bold text-sm">{{ $t('focal') }}</b>
<b class="font-bold text-sm">{{ clipboard.data.attributes.focal_length }}</b>
</div>
<div class="flex items-center justify-between py-2" v-if="clipboard.data.attributes.iso">
<b class="font-bold text-sm">{{ $t('iso') }}</b>
<b class="font-bold text-sm">{{ clipboard.data.attributes.iso }}</b>
</div>
<div class="flex items-center justify-between py-2" v-if="clipboard.data.attributes.aperture_f_number">
<b class="font-bold text-sm">{{ $t('aperature') }}</b>
<b class="font-bold text-sm">{{ clipboard.data.attributes.aperture_f_number }}</b>
</div>
<div class="flex items-center justify-between py-2" v-if="clipboard.data.attributes.ccd_width">
<b class="font-bold text-sm">{{ $t('camera_lens') }}</b>
<b class="font-bold text-sm">{{ clipboard.data.attributes.ccd_width }}</b>
</div>
<div class="flex items-center justify-between py-2" v-if="clipboard.data.attributes.longitude">
<b class="font-bold text-sm">{{ $t('longitude') }}</b>
<b class="font-bold text-sm">{{ clipboard.data.attributes.longitude }}</b>
</div>
<div class="flex items-center justify-between py-2" v-if="clipboard.data.attributes.latitude">
<b class="font-bold text-sm">{{ $t('latitude') }}</b>
<b class="font-bold text-sm">{{ clipboard.data.attributes.latitude }}</b>
</div>
</div>
</template>
<script>
export default {
name: 'ImageMetaData',
computed: {
clipboard() {
return this.$store.getters.clipboard[0].data.relationships.exif
},
},
}
</script>

View File

@@ -0,0 +1,112 @@
<template>
<div class="info-box" :class="type">
<slot />
</div>
</template>
<script>
export default {
name: 'InfoBox',
props: ['type'],
}
</script>
<style lang="scss" scoped>
@import '../../../../sass/vuefilemanager/variables';
@import '../../../../sass/vuefilemanager/mixins';
.info-box {
padding: 20px;
border-radius: 10px;
margin-bottom: 32px;
background: $light_background;
text-align: left;
&.error {
background: rgba($danger, 0.1);
p,
a {
color: $danger;
}
a {
text-decoration: underline;
}
}
p {
font-size: 15px;
line-height: 1.6;
word-break: break-word;
font-weight: 600;
/deep/ a {
font-size: 15px;
}
/deep/ b {
font-size: 15px;
font-weight: 700;
}
}
b {
font-weight: 700;
}
a {
font-weight: 700;
@include font-size(15);
line-height: 1.6;
}
ul {
margin-top: 15px;
display: block;
li {
display: block;
a {
display: block;
}
}
}
}
@media only screen and (max-width: 690px) {
.info-box {
padding: 15px;
}
}
.dark {
.info-box {
background: $dark_mode_foreground;
&.error {
background: rgba($danger, 0.1);
p,
a {
color: $danger;
}
a {
text-decoration: underline;
}
}
p {
color: $dark_mode_text_primary;
}
ul {
li {
color: $dark_mode_text_primary;
}
}
}
}
</style>

View File

@@ -0,0 +1,72 @@
<template>
<div class="shrink-0 grow-0">
<img
:style="{ width: size + 'px', height: size + 'px' }"
v-if="member.data.attributes.avatar"
:src="avatar"
:class="[
borderRadius,
{
'border-3 border-white dark:border-dark-background': isBorder,
},
]"
class="object-cover mx-auto"
/>
<div
v-else
class="flex items-center justify-center mx-auto"
:class="[
borderRadius,
{
'border-3 border-white dark:border-dark-background': isBorder,
'dark:bg-4x-dark-foreground bg-light-background': !member.data.attributes.color,
},
]"
:style="{
width: size + 'px',
height: size + 'px',
background: member.data.attributes.color ? member.data.attributes.color : '',
}"
>
<span :class="fontSize" class="font-extrabold uppercase text-white">
{{ letter }}
</span>
</div>
</div>
</template>
<script>
export default {
name: 'MemberAvatar',
props: ['isBorder', 'member', 'size'],
computed: {
letter() {
let string = this.member.data.attributes.name
? this.member.data.attributes.name
: this.member.data.attributes.email
return string.substr(0, 1)
},
borderRadius() {
return this.size > 32 ? 'rounded-xl' : 'rounded-lg'
},
fontSize() {
if (this.size > 42) {
return 'text-lg'
} else if (this.size > 32) {
return 'text-base'
} else {
return 'text-sm'
}
},
avatar() {
if (this.size >= 52) {
return this.member.data.attributes.avatar.md
} else if (this.size > 32) {
return this.member.data.attributes.avatar.sm
} else {
return this.member.data.attributes.avatar.xs
}
},
},
}
</script>

View File

@@ -0,0 +1,44 @@
<template>
<div class="progress-bar">
<span class="bg-theme" :style="{ width: progress + '%' }"></span>
</div>
</template>
<script>
export default {
name: 'ProgressBar',
props: ['progress'],
}
</script>
<style scoped lang="scss">
@import '../../../../sass/vuefilemanager/variables';
@import '../../../../sass/vuefilemanager/mixins';
.progress-bar {
width: 100%;
height: 5px;
background: $light_background;
margin-top: 6px;
border-radius: 10px;
span {
display: block;
height: 100%;
border-radius: 10px;
max-width: 100%;
}
}
.dark {
.progress-bar {
background: $dark_mode_foreground;
}
}
@media only screen and (min-width: 680px) {
.dark .progress-bar {
background: $dark_mode_foreground;
}
}
</style>

View File

@@ -0,0 +1,44 @@
<template>
<div id="loading-bar-spinner" class="spinner">
<div class="spinner-icon border-top-theme border-left-theme"></div>
</div>
</template>
<script>
export default {
name: 'Spinner',
}
</script>
<style scoped lang="scss">
@import '../../../../sass/vuefilemanager/variables';
@import '../../../../sass/vuefilemanager/mixins';
#loading-bar-spinner.spinner {
left: 50%;
margin-left: -20px;
top: 50%;
margin-top: -20px;
position: absolute;
z-index: 19 !important;
animation: loading-bar-spinner 400ms linear infinite;
}
#loading-bar-spinner.spinner .spinner-icon {
width: 40px;
height: 40px;
border: solid 4px transparent;
//border-top-color: $theme !important;
//border-left-color: $theme !important;
border-radius: 50%;
}
@keyframes loading-bar-spinner {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>

View File

@@ -0,0 +1,131 @@
<template>
<transition name="info-panel">
<div v-if="fileQueue.length > 0" class="upload-progress">
<div class="progress-title">
<!--Is processing-->
<span v-if="isProcessingFile" class="flex items-center justify-center">
<refresh-cw-icon size="12" class="sync-alt text-theme" />
{{ $t('uploading.processing_file') }}
</span>
<!--Multi file upload-->
<span v-if="!isProcessingFile && fileQueue.length > 0">
{{
$t('uploading.progress', {
current: filesInQueueUploaded,
total: filesInQueueTotal,
progress: uploadingProgress,
})
}}
</span>
</div>
<div class="progress-wrapper">
<ProgressBar :progress="uploadingProgress" />
<span @click="cancelUpload" :title="$t('uploading.cancel')" class="cancel-icon">
<x-icon size="16" @click="cancelUpload" class="hover-text-theme"></x-icon>
</span>
</div>
</div>
</transition>
</template>
<script>
import ProgressBar from './ProgressBar'
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([
'filesInQueueUploaded',
'filesInQueueTotal',
'uploadingProgress',
'isProcessingFile',
'fileQueue',
]),
},
methods: {
cancelUpload() {
events.$emit('cancel-upload')
},
},
}
</script>
<style scoped lang="scss">
@import '../../../../sass/vuefilemanager/variables';
@import '../../../../sass/vuefilemanager/mixins';
.sync-alt {
animation: spin 1s linear infinite;
margin-right: 5px;
polyline,
path {
color: inherit;
}
}
@keyframes spin {
0% {
transform: rotate(0);
}
100% {
transform: rotate(360deg);
}
}
.info-panel-enter-active,
.info-panel-leave-active {
transition: all 0.3s ease;
}
.info-panel-enter,
.info-panel-leave-to {
opacity: 0;
transform: translateY(-100%);
}
.upload-progress {
width: 100%;
position: relative;
z-index: 1;
.progress-wrapper {
display: flex;
.cancel-icon {
cursor: pointer;
padding: 0 7px 0 13px;
&:hover {
line {
color: inherit;
}
}
}
}
.progress-title {
font-weight: 700;
text-align: center;
span {
@include font-size(14);
}
}
}
.dark {
.progress-bar {
background: $dark_mode_foreground;
}
}
</style>

View File

@@ -0,0 +1,39 @@
<template>
<div class="flex items-center justify-between">
<div class="flex items-center leading-none">
<MemberAvatar :size="52" :is-border="false" :member="user" />
<div class="pl-4">
<b class="mb-1 block font-bold leading-none">
{{ user.data.relationships.settings.data.attributes.name }}
</b>
<span class="text-theme text-sm font-semibold leading-none">
{{ user.data.attributes.email }}
</span>
</div>
</div>
<NotificationBell @click.native="openNotificationPopup" />
</div>
</template>
<script>
import MemberAvatar from './MemberAvatar'
import NotificationBell from '../../Notifications/Components/NotificationBell'
import { events } from '../../../bus'
import { mapGetters } from 'vuex'
export default {
name: 'UserHeadline',
components: {
NotificationBell,
MemberAvatar,
},
computed: {
...mapGetters(['user']),
},
methods: {
openNotificationPopup() {
events.$emit('popup:open', { name: 'notifications-mobile' })
},
},
}
</script>

View File

@@ -0,0 +1,77 @@
<template>
<transition name="vignette">
<div
v-if="isVisible"
class="vignette dark:bg-2x-dark-background bg-dark-background bg-opacity-[0.35] dark:bg-opacity-[0.70]"
@click="closePopup"
></div>
</transition>
</template>
<script>
import { events } from '../../../bus'
import { mapGetters } from 'vuex'
export default {
name: 'Vignette',
computed: {
...mapGetters(['processingPopup']),
isVisible() {
return this.processingPopup || this.isVisibleVignette
},
},
data() {
return {
isVisibleVignette: false,
}
},
methods: {
closePopup() {
events.$emit('popup:close')
events.$emit('spotlight:hide')
events.$emit('mobile-menu:hide')
},
},
created() {
// Show vignette
events.$on('popup:open', () => (this.isVisibleVignette = true))
events.$on('alert:open', () => (this.isVisibleVignette = true))
events.$on('success:open', () => (this.isVisibleVignette = true))
events.$on('confirm:open', () => (this.isVisibleVignette = true))
// Hide vignette
events.$on('popup:close', () => (this.isVisibleVignette = false))
},
}
</script>
<style lang="scss" scoped>
@import '../../../../sass/vuefilemanager/variables';
@import '../../../../sass/vuefilemanager/mixins';
.vignette {
position: fixed;
top: 0;
right: 0;
left: 0;
bottom: 0;
z-index: 40;
}
.vignette-enter-active {
animation: vignette-in 0.15s linear;
}
.vignette-leave-active {
animation: vignette-in 0.15s cubic-bezier(0.4, 0, 1, 1) reverse;
}
@keyframes vignette-in {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
</style>

View File

@@ -0,0 +1,42 @@
<template>
<div v-if="isVisible">
<!--Overlay component-->
<div
@click.capture="hidePopover"
class="absolute top-12 z-20 w-60 overflow-hidden rounded-xl bg-white shadow-xl dark:bg-dark-foreground"
:class="{ 'right-0': side === 'left', 'left-0': side === 'right' }"
>
<slot />
</div>
<!--Clickable layer to close overlays-->
<div @click="hidePopover" class="fixed top-0 left-0 right-0 bottom-0 z-10 cursor-pointer"></div>
</div>
</template>
<script>
import { events } from '../../../bus'
export default {
name: 'PopoverItem',
props: ['side', 'name'],
data() {
return {
isVisible: false,
}
},
methods: {
hidePopover() {
setTimeout(() => (this.isVisible = false), 10)
},
},
mounted() {
events.$on('popover:open', (name) => {
if (this.name === name) {
this.isVisible = !this.isVisible
}
})
events.$on('popover:close', () => this.isVisible = false)
},
}
</script>

View File

@@ -0,0 +1,11 @@
<template>
<div class="relative">
<slot />
</div>
</template>
<script>
export default {
name: 'PopoverWrapper',
}
</script>

View File

@@ -0,0 +1,64 @@
<template>
<div class="label">
<span :class="['label-dot', color]"></span>
<b class="label-title">
{{ title }}
</b>
</div>
</template>
<script>
export default {
name: 'DotLabel',
props: ['color', 'title'],
}
</script>
<style lang="scss" scoped>
.label {
display: flex;
align-items: center;
.label-dot {
margin-right: 10px;
width: 8px;
height: 8px;
display: block;
border-radius: 8px;
flex: none;
&.success {
background: #0abb87;
}
&.danger {
background: #fd397a;
}
&.warning {
background: #ffb822;
}
&.info {
background: #5578eb;
}
&.primary {
background: red;
}
&.purple {
background: #9d66fe;
}
&.secondary {
background: #e1e1ef;
}
}
.label-title {
font-size: 16px;
font-weight: 700;
}
}
</style>

View File

@@ -0,0 +1,102 @@
<template>
<div>
<div class="mb-4 flex h-2.5 items-center rounded bg-light-300 dark:bg-2x-dark-foreground">
<div
v-for="(chart, i) in data"
:key="i"
:style="{
width: (chart.progress > 1 ? chart.progress : 0) + '%',
}"
class="chart-wrapper"
>
<!--<DotLabel class="label" :class="{'offset-top': chart.progress < 5}" :color="chart.color" :title="chart.value" />-->
<!--Only singe line-->
<span
v-if="data.length === 1"
:class="[
{
'rounded-tl-lg rounded-bl-lg border-r-2 border-white dark:border-gray-800':
chart.progress < 100,
'rounded-lg border-none': chart.progress >= 100,
},
chart.color,
]"
class="chart-progress block h-2.5 w-full"
>
</span>
<!--Multiple line-->
<span
v-if="data.length > 1 && chart.progress > 0"
:class="[
{
'rounded-tl-lg rounded-bl-lg border-r-2 border-white dark:border-gray-800': i === 0,
'border-r-2 border-white dark:border-gray-800': i < data.length - 1,
'rounded-tr-lg rounded-br-lg': i === data.length - 1,
},
chart.color,
]"
class="chart-progress block h-2.5 w-full"
></span>
</div>
</div>
<footer class="flex w-full items-center overflow-x-auto">
<DotLabel v-for="(chart, i) in data" :key="i" :color="chart.color" :title="chart.title" class="mr-5" />
</footer>
</div>
</template>
<script>
import DotLabel from './DotLabel'
export default {
name: 'ProgressLine',
props: ['data'],
components: {
DotLabel,
},
}
</script>
<style lang="scss" scoped>
.chart-progress {
&.success {
background: #0abb87;
box-shadow: 0 3px 10px rgba(#0abb87, 0.5);
}
&.danger {
background: #fd397a;
box-shadow: 0 3px 10px rgba(#fd397a, 0.5);
}
&.warning {
background: #ffb822;
box-shadow: 0 3px 10px rgba(#ffb822, 0.5);
}
&.info {
background: #5578eb;
box-shadow: 0 3px 10px rgba(#5578eb, 0.5);
}
&.purple {
background: #9d66fe;
box-shadow: 0 3px 10px rgba(#9d66fe, 0.5);
}
&.secondary {
background: #e1e1ef;
box-shadow: 0 3px 10px rgba(#e1e1ef, 0.5);
}
}
.dark .chart-progress {
&.secondary {
background: #282a2f !important;
box-shadow: 0 3px 10px rgba(#282a2f, 0.5) !important;
}
}
</style>

View File

@@ -0,0 +1,54 @@
<template>
<tr class="table-row">
<td class="table-cell" v-for="(collumn, index) in normalizedColumns" :key="index">
<span>{{ collumn }}</span>
</td>
</tr>
</template>
<script>
export default {
props: ['data'],
computed: {
normalizedColumns() {
// Remove ID from object
if (this.data['id']) delete this.data['id']
// Return object
return Object.values(this.data)
},
},
}
</script>
<style lang="scss" scoped>
@import '../../../../sass/vuefilemanager/variables';
@import '../../../../sass/vuefilemanager/mixins';
.table-row {
border-radius: 8px;
&:hover {
background: $light_background;
}
.table-cell {
padding-top: 15px;
padding-bottom: 15px;
&:first-child {
padding-left: 15px;
}
&:last-child {
padding-right: 15px;
text-align: right;
}
span {
@include font-size(16);
font-weight: bold;
}
}
}
</style>

View File

@@ -0,0 +1,70 @@
<template>
<div class="flex shrink-0 grow-0 items-center">
<MemberAvatar class="mr-3 shrink-0" :is-border="false" :size="52" :member="member" />
<div class="info">
<b class="name" v-if="title">{{ title }}</b>
<span class="description" v-if="description">{{ description }}</span>
</div>
</div>
</template>
<script>
import MemberAvatar from '../Others/MemberAvatar'
export default {
name: 'DatatableCellImage',
props: ['member', 'title', 'description', 'image-size'],
components: {
MemberAvatar,
},
}
</script>
<style lang="scss" scoped>
@import '../../../../sass/vuefilemanager/variables';
@import '../../../../sass/vuefilemanager/mixins';
.info {
.name,
.description {
max-width: 150px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: block;
}
.name {
@include font-size(15);
line-height: 1;
color: $text;
}
.description {
color: $text-muted;
@include font-size(12);
}
}
.dark {
.cell-image-thumbnail {
.image {
img {
&.blurred {
display: none;
}
}
}
.info {
.name {
color: $dark_mode_text_primary;
}
.description {
color: $dark_mode_text_secondary;
}
}
}
}
</style>

View File

@@ -0,0 +1,297 @@
<template>
<div class="w-full">
<table v-if="hasData" class="w-full">
<thead>
<tr class="whitespace-nowrap">
<th
class="text-left"
v-for="(column, index) in columns"
@click="sort(column.field, column.sortable)"
:key="index"
:class="{
'sortable cursor-pointer': column.sortable,
'text-right': Object.values(columns).length - 1 === index,
}"
v-if="!column.hidden"
>
<span class="text-xs text-gray-400 dark:text-gray-500">
{{ $t(column.label) }}
</span>
<chevron-up-icon
v-if="column.sortable"
:class="{ 'arrow-down': filter.sort === 'ASC' }"
class="vue-feather inline-block text-gray-300 dark:text-gray-500"
size="12"
/>
</th>
</tr>
</thead>
<tbody class="table-body">
<slot v-for="row in data.data" :row="row">
<DatatableCell :data="row" :key="row.id" />
</slot>
</tbody>
</table>
<!--Empty data slot-->
<slot v-if="!isLoading && !hasData" name="empty-page" />
<!--Paginator-->
<div v-if="paginator && hasData" class="mt-6 sm:flex sm:items-center sm:justify-between">
<!--Show if there is only 6 pages-->
<ul v-if="data.meta.total > 15 && data.meta.last_page <= 6" class="pagination flex justify-center items-center">
<!--Go previous icon-->
<li class="previous inline-block p-1">
<a
@click="goToPage(pageIndex - 1)"
class="page-link"
:class="{
'cursor-default opacity-20': pageIndex === 1,
}"
>
<chevron-left-icon size="14" class="inline-block" />
</a>
</li>
<li
v-for="(page, index) in data.meta.last_page"
:key="index"
class="inline-block p-1"
@click="goToPage(page)"
>
<a
class="page-link"
:class="{
'bg-light-background dark:bg-4x-dark-foreground dark:text-gray-300': pageIndex === page,
}"
>
{{ page }}
</a>
</li>
<!--Go next icon-->
<li class="next inline-block p-1">
<a
@click="goToPage(pageIndex + 1)"
class="page-link"
:class="{
'cursor-default opacity-20': pageIndex === data.meta.last_page,
}"
>
<chevron-right-icon size="14" class="inline-block" />
</a>
</li>
</ul>
<!--Show if there is more than 6 pages-->
<ul v-if="data.meta.total > 15 && data.meta.last_page > 6" class="pagination flex justify-center items-center">
<!--Go previous icon-->
<li class="previous inline-block p-1">
<a
@click="goToPage(pageIndex - 1)"
class="page-link"
:class="{
'cursor-default opacity-20': pageIndex === 1,
}"
>
<chevron-left-icon size="14" class="inline-block" />
</a>
</li>
<!--Show first Page-->
<li class="inline-block p-1" v-if="pageIndex >= 5" @click="goToPage(1)">
<a class="page-link"> 1 </a>
</li>
<li
v-if="pageIndex < 5"
v-for="(page, index) in 5"
:key="index"
class="inline-block p-1"
@click="goToPage(page)"
>
<a
class="page-link"
:class="{
'bg-light-background dark:bg-4x-dark-foreground dark:text-gray-300': pageIndex === page,
}"
>
{{ page }}
</a>
</li>
<li class="inline-block p-1" v-if="pageIndex >= 5">
<a class="page-link">...</a>
</li>
<!--Floated Pages-->
<li
v-if="pageIndex >= 5 && pageIndex < data.meta.last_page - 3"
v-for="(page, index) in floatPages"
:key="index"
class="inline-block p-1"
@click="goToPage(page)"
>
<a
class="page-link"
:class="{
'bg-light-background dark:bg-4x-dark-foreground dark:text-gray-300': pageIndex === page,
}"
>
{{ page }}
</a>
</li>
<li class="inline-block p-1" v-if="pageIndex < data.meta.last_page - 3">
<a class="page-link">...</a>
</li>
<li
v-if="pageIndex > data.meta.last_page - 4"
v-for="(page, index) in 5"
:key="index"
class="inline-block p-1"
@click="goToPage(data.meta.last_page - (4 - index))"
>
<a
class="page-link"
:class="{
'bg-light-background dark:bg-4x-dark-foreground dark:text-gray-300':
pageIndex === data.meta.last_page - (4 - index),
}"
>
{{ data.meta.last_page - (4 - index) }}
</a>
</li>
<!--Show last page-->
<li
class="inline-block p-1"
v-if="pageIndex < data.meta.last_page - 3"
@click="goToPage(data.meta.last_page)"
>
<a class="page-link">
{{ data.meta.last_page }}
</a>
</li>
<!--Go next icon-->
<li class="next inline-block p-1">
<a
@click="goToPage(pageIndex + 1)"
class="page-link"
:class="{
'cursor-default opacity-20': pageIndex === data.meta.last_page,
}"
>
<chevron-right-icon size="14" class="inline-block" />
</a>
</li>
</ul>
<span class="text-xs text-gray-600 dark:text-gray-500 block text-center sm:mt-0 mt-4">
{{ $t('paginator', {from: data.meta.from, to: data.meta.to, total: data.meta.total}) }}
</span>
</div>
</div>
</template>
<script>
import { ChevronUpIcon, ChevronLeftIcon, ChevronRightIcon } from 'vue-feather-icons'
import DatatableCell from './DatatableCell'
import axios from 'axios'
export default {
name: 'DatatableWrapper',
props: ['paginator', 'tableData', 'columns', 'scope', 'api'],
components: {
ChevronRightIcon,
ChevronLeftIcon,
DatatableCell,
ChevronUpIcon,
},
computed: {
hasData() {
return this.data && this.data.data && this.data.data.length > 0
},
floatPages() {
return [this.pageIndex - 1, this.pageIndex, this.pageIndex + 1]
},
},
data() {
return {
data: undefined,
isLoading: true,
pageIndex: 1,
filter: {
sort: 'DESC',
field: undefined,
},
}
},
methods: {
goToPage(index) {
if (index > this.data.meta.last_page || index === 0) return
this.pageIndex = index
this.getPage(index)
},
sort(field, sortable) {
// Prevent sortable if is disabled
if (!sortable) return
// Set filter
this.filter.field = field
// Set sorting direction
if (this.filter.sort === 'DESC') {
this.filter.sort = 'ASC'
} else if (this.filter.sort === 'ASC') {
this.filter.sort = 'DESC'
}
this.getPage(this.pageIndex)
},
getPage(page) {
// Get api URI
this.URI = this.api
// Set page index
if (this.paginator) this.URI = this.URI + '?page=' + page
// Add filder URI if is defined sorting
if (this.filter.field)
this.URI =
this.URI +
(this.paginator ? '&' : '?') +
'sort=' +
this.filter.field +
'&direction=' +
this.filter.sort
this.isLoading = true
// Get data
axios
.get(this.URI)
.then((response) => {
this.data = response.data
this.$emit('data', response.data)
})
.catch(() => this.$isSomethingWrong())
.finally(() => {
this.$emit('init', true)
this.isLoading = false
})
},
},
created() {
if (this.api) this.getPage(this.pageIndex)
if (this.tableData) (this.data = this.tableData), (this.isLoading = false)
},
}
</script>

View File

@@ -0,0 +1,150 @@
<template>
<div
:class="{
'pointer-events-none opacity-50': (disabledById && disabledById.data.id === nodes.id) || !disableId || (isRootDepth && !nodes.folders.length && nodes.location !== 'files'),
'mb-2.5': isRootDepth,
}"
>
<div
:style="indent"
class="relative relative flex cursor-pointer select-none items-center whitespace-nowrap px-1.5 transition-all duration-150"
>
<!--Arrow icon-->
<span @click.stop="showTree" class="-m-2 p-2">
<chevron-right-icon
:class="{
'rotate-90 transform': isVisible,
'text-theme dark-text-theme': isSelectedItem,
'opacity-100': nodes.folders.length !== 0,
}"
class="vue-feather mr-2 opacity-0 transition-all duration-300"
size="17"
/>
</span>
<!--Item icon-->
<hard-drive-icon
v-if="['public', 'files', 'upload-request'].includes(nodes.location)"
size="17"
class="icon vue-feather shrink-0"
:class="{ 'text-theme dark-text-theme': isSelectedItem }"
/>
<users-icon
v-if="nodes.location === 'team-folders'"
size="17"
class="icon vue-feather shrink-0"
:class="{ 'text-theme dark-text-theme': isSelectedItem }"
/>
<user-plus-icon
v-if="nodes.location === 'shared-with-me'"
size="17"
class="icon vue-feather shrink-0"
:class="{ 'text-theme dark-text-theme': isSelectedItem }"
/>
<folder-icon
v-if="!nodes.location"
size="17"
class="icon vue-feather shrink-0"
:class="{ 'text-theme dark-text-theme': isSelectedItem }"
/>
<!--Item label-->
<b
@click="getFolder"
class="lg:py-2 py-3.5 ml-3 inline-block overflow-x-hidden text-ellipsis whitespace-nowrap text-xs font-bold transition-all duration-150"
:class="{'text-theme': isSelectedItem }"
>
{{ nodes.name }}
</b>
</div>
<!--Children-->
<tree-node
:disabled-by-id="disabledById"
:depth="depth + 1"
v-if="isVisible"
:nodes="item"
v-for="item in nodes.folders"
:key="item.id"
/>
</div>
</template>
<script>
import { FolderIcon, ChevronRightIcon, HardDriveIcon, UsersIcon, UserPlusIcon } from 'vue-feather-icons'
import { events } from '../../../bus'
import { mapGetters } from 'vuex'
export default {
name: 'TreeMenu',
props: ['disabledById', 'nodes', 'depth'],
components: {
ChevronRightIcon,
HardDriveIcon,
UserPlusIcon,
FolderIcon,
UsersIcon,
'tree-node': () => import('./TreeMenu'),
},
computed: {
...mapGetters(['clipboard']),
indent() {
return { paddingLeft: this.depth * 20 + 'px' }
},
disableId() {
let canBeShow = true
if (this.clipboard.includes(this.disabledById)) {
this.clipboard.map((item) => {
if (item.data.id === this.nodes.id) {
canBeShow = false
}
})
}
return canBeShow
},
isRootDepth() {
return this.depth === 1
},
isSelectedItem() {
return (this.isSelected && this.nodes.isMovable) || (this.isSelected && !this.isRootDepth)
},
},
data() {
return {
isVisible: false,
isSelected: false,
isInactive: false,
}
},
methods: {
getFolder() {
if ((this.isRootDepth && this.nodes.isMovable) || !this.isRootDepth) {
events.$emit('show-folder-item', this.nodes)
events.$emit('pick-folder', this.nodes)
}
},
showTree() {
this.isVisible = !this.isVisible
},
},
mounted() {
// Show first location
if (this.depth === 1 && this.nodes.isOpen) this.isVisible = true
// Select clicked folder
events.$on('pick-folder', (node) => {
this.isSelected = false
if (this.nodes.id === node.id) this.isSelected = true
})
// Select clicked folder
events.$on('show-folder-item', (node) => {
this.isSelected = false
if (this.nodes.id === node.id) this.isSelected = true
})
},
}
</script>

View File

@@ -0,0 +1,156 @@
<template>
<div>
<div
@click="goToFolder"
class="flex cursor-pointer items-center rounded-lg border-2 border-dashed border-transparent py-2.5"
:class="{
'border-theme': area,
'pointer-events-none opacity-50': disabledFolder || (disabled && draggedItem.length > 0),
}"
:style="indent"
@dragover.prevent="dragEnter"
@dragleave="dragLeave"
@drop="dragFinish()"
>
<div @click.stop.prevent="showTree" class="-my-2 -ml-2 cursor-pointer p-2">
<chevron-right-icon
size="17"
class="vue-feather"
:class="{
'rotate-90 transform': isVisible,
'opacity-0': nodes.folders.length === 0,
}"
/>
</div>
<folder-icon size="17" class="vue-feather mr-2.5 shrink-0" :class="{ 'text-theme': isSelected }" />
<b
class="max-w-1 overflow-hidden text-ellipsis whitespace-nowrap text-xs font-bold"
:class="{ 'text-theme': isSelected }"
>
{{ nodes.name }}
</b>
</div>
<tree-node
:disabled="disableChildren"
:depth="depth + 1"
v-if="isVisible"
:nodes="item"
v-for="item in nodes.folders"
:key="item.id"
/>
</div>
</template>
<script>
import { FolderIcon, ChevronRightIcon } from 'vue-feather-icons'
import { mapGetters } from 'vuex'
import { events } from '../../../bus'
export default {
name: 'TreeMenuNavigator',
props: ['disabled', 'nodes', 'depth'],
components: {
'tree-node': () => import('./TreeMenuNavigator'),
ChevronRightIcon,
FolderIcon,
},
computed: {
...mapGetters(['clipboard']),
isSelected() {
return this.$route.params.id === this.nodes.id
},
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.id === item.parent_id) {
disableFolder = true
}
//Disable the self folder with all children
if (this.nodes.id === item.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 ? 14 : 18
return {
paddingLeft: this.depth === 0 ? 0 : offset * this.depth + 'px',
}
},
},
data() {
return {
disableChildren: false,
isVisible: false,
draggedItem: [],
area: false,
}
},
methods: {
goToFolder() {
this.$goToFileView(this.nodes.id)
},
dragFinish() {
// Move no selected item
if (!this.clipboard.includes(this.draggedItem[0])) {
this.$store.dispatch('moveItem', {
to_item: this.nodes,
item: this.draggedItem[0],
})
}
// Move all selected items
if (this.clipboard.includes(this.draggedItem[0])) {
this.$store.dispatch('moveItem', {
to_item: this.nodes,
item: null,
})
}
this.draggedItem = []
this.area = false
events.$emit('drop')
},
dragEnter() {
this.area = true
},
dragLeave() {
this.area = false
},
showTree() {
this.isVisible = !this.isVisible
},
},
created() {
events.$on('drop', () => {
this.draggedItem = []
})
//Get dragged item
events.$on('dragstart', (data) => {
//If is dragged item not selected
if (!this.clipboard.includes(data)) {
this.draggedItem = [data]
}
//If are the dragged items selected
if (this.clipboard.includes(data)) {
this.draggedItem = this.clipboard
}
})
},
}
</script>