mirror of
https://github.com/VueFileManager/vuefilemanager.git
synced 2026-04-19 16:32:15 +00:00
vue components refactoring
This commit is contained in:
52
resources/js/components/UI/Buttons/ActionButton.vue
Normal file
52
resources/js/components/UI/Buttons/ActionButton.vue
Normal 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>
|
||||
47
resources/js/components/UI/Buttons/AuthButton.vue
Normal file
47
resources/js/components/UI/Buttons/AuthButton.vue
Normal 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>
|
||||
118
resources/js/components/UI/Buttons/ButtonBase.vue
Normal file
118
resources/js/components/UI/Buttons/ButtonBase.vue
Normal 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>
|
||||
48
resources/js/components/UI/Buttons/ButtonUpload.vue
Normal file
48
resources/js/components/UI/Buttons/ButtonUpload.vue
Normal 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>
|
||||
94
resources/js/components/UI/Buttons/MobileActionButton.vue
Normal file
94
resources/js/components/UI/Buttons/MobileActionButton.vue
Normal 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>
|
||||
@@ -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>
|
||||
36
resources/js/components/UI/Buttons/SearchBarButton.vue
Normal file
36
resources/js/components/UI/Buttons/SearchBarButton.vue
Normal 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>
|
||||
41
resources/js/components/UI/Buttons/SocialLoginButtons.vue
Normal file
41
resources/js/components/UI/Buttons/SocialLoginButtons.vue
Normal 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>
|
||||
82
resources/js/components/UI/Buttons/ToolbarButton.vue
Normal file
82
resources/js/components/UI/Buttons/ToolbarButton.vue
Normal 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>
|
||||
265
resources/js/components/UI/Entries/ItemGrid.vue
Normal file
265
resources/js/components/UI/Entries/ItemGrid.vue
Normal 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>
|
||||
245
resources/js/components/UI/Entries/ItemList.vue
Normal file
245
resources/js/components/UI/Entries/ItemList.vue
Normal 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>
|
||||
140
resources/js/components/UI/Entries/ThumbnailItem.vue
Normal file
140
resources/js/components/UI/Entries/ThumbnailItem.vue
Normal 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>
|
||||
38
resources/js/components/UI/Labels/ColorLabel.vue
Normal file
38
resources/js/components/UI/Labels/ColorLabel.vue
Normal 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>
|
||||
76
resources/js/components/UI/Labels/FormLabel.vue
Normal file
76
resources/js/components/UI/Labels/FormLabel.vue
Normal 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>
|
||||
44
resources/js/components/UI/Labels/LogoHeadline.vue
Normal file
44
resources/js/components/UI/Labels/LogoHeadline.vue
Normal 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>
|
||||
33
resources/js/components/UI/Labels/SectionTitle.vue
Normal file
33
resources/js/components/UI/Labels/SectionTitle.vue
Normal 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>
|
||||
37
resources/js/components/UI/Labels/TextLabel.vue
Normal file
37
resources/js/components/UI/Labels/TextLabel.vue
Normal 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>
|
||||
35
resources/js/components/UI/Labels/TitlePreview.vue
Normal file
35
resources/js/components/UI/Labels/TitlePreview.vue
Normal 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>
|
||||
16
resources/js/components/UI/List/ListInfo.vue
Normal file
16
resources/js/components/UI/List/ListInfo.vue
Normal 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>
|
||||
19
resources/js/components/UI/List/ListInfoItem.vue
Normal file
19
resources/js/components/UI/List/ListInfoItem.vue
Normal 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>
|
||||
52
resources/js/components/UI/Others/AlertBox.vue
Normal file
52
resources/js/components/UI/Others/AlertBox.vue
Normal 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>
|
||||
51
resources/js/components/UI/Others/CardNavigation.vue
Normal file
51
resources/js/components/UI/Others/CardNavigation.vue
Normal 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>
|
||||
46
resources/js/components/UI/Others/CookieDisclaimer.vue
Normal file
46
resources/js/components/UI/Others/CookieDisclaimer.vue
Normal 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>
|
||||
79
resources/js/components/UI/Others/DragUI.vue
Normal file
79
resources/js/components/UI/Others/DragUI.vue
Normal 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>
|
||||
90
resources/js/components/UI/Others/ImageMetaData.vue
Normal file
90
resources/js/components/UI/Others/ImageMetaData.vue
Normal 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>
|
||||
112
resources/js/components/UI/Others/InfoBox.vue
Normal file
112
resources/js/components/UI/Others/InfoBox.vue
Normal 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>
|
||||
72
resources/js/components/UI/Others/MemberAvatar.vue
Normal file
72
resources/js/components/UI/Others/MemberAvatar.vue
Normal 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>
|
||||
44
resources/js/components/UI/Others/ProgressBar.vue
Normal file
44
resources/js/components/UI/Others/ProgressBar.vue
Normal 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>
|
||||
44
resources/js/components/UI/Others/Spinner.vue
Normal file
44
resources/js/components/UI/Others/Spinner.vue
Normal 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>
|
||||
131
resources/js/components/UI/Others/UploadProgress.vue
Normal file
131
resources/js/components/UI/Others/UploadProgress.vue
Normal 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>
|
||||
39
resources/js/components/UI/Others/UserHeadline.vue
Normal file
39
resources/js/components/UI/Others/UserHeadline.vue
Normal 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>
|
||||
77
resources/js/components/UI/Others/Vignette.vue
Normal file
77
resources/js/components/UI/Others/Vignette.vue
Normal 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>
|
||||
42
resources/js/components/UI/Popover/PopoverItem.vue
Normal file
42
resources/js/components/UI/Popover/PopoverItem.vue
Normal 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>
|
||||
11
resources/js/components/UI/Popover/PopoverWrapper.vue
Normal file
11
resources/js/components/UI/Popover/PopoverWrapper.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div class="relative">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'PopoverWrapper',
|
||||
}
|
||||
</script>
|
||||
64
resources/js/components/UI/ProgressChart/DotLabel.vue
Normal file
64
resources/js/components/UI/ProgressChart/DotLabel.vue
Normal 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>
|
||||
102
resources/js/components/UI/ProgressChart/ProgressLine.vue
Normal file
102
resources/js/components/UI/ProgressChart/ProgressLine.vue
Normal 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>
|
||||
54
resources/js/components/UI/Table/DatatableCell.vue
Normal file
54
resources/js/components/UI/Table/DatatableCell.vue
Normal 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>
|
||||
70
resources/js/components/UI/Table/DatatableCellImage.vue
Normal file
70
resources/js/components/UI/Table/DatatableCellImage.vue
Normal 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>
|
||||
297
resources/js/components/UI/Table/DatatableWrapper.vue
Normal file
297
resources/js/components/UI/Table/DatatableWrapper.vue
Normal 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>
|
||||
150
resources/js/components/UI/Trees/TreeMenu.vue
Normal file
150
resources/js/components/UI/Trees/TreeMenu.vue
Normal 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>
|
||||
156
resources/js/components/UI/Trees/TreeMenuNavigator.vue
Normal file
156
resources/js/components/UI/Trees/TreeMenuNavigator.vue
Normal 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>
|
||||
Reference in New Issue
Block a user