vue components refactoring

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

View File

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

View File

@@ -1,182 +0,0 @@
<template>
<PopupWrapper name="select-payment-method">
<PopupHeader :title="$t('select_payment_method')" icon="credit-card" />
<PopupContent style="padding: 0 20px">
<InfoBox v-if="!config.isPayPal && !config.isPaystack" class="!mb-0">
<p>{{ $t("not_any_payment_method") }}</p>
</InfoBox>
<!--PayPal implementation-->
<div
v-if="config.isPayPal"
:class="{
'mb-2 rounded-xl bg-light-background px-4 dark:bg-2x-dark-foreground': paypal.isMethodsLoaded,
}"
>
<PaymentMethod
@click.native="payByPayPal"
driver="paypal"
:description="config.paypal_payment_description"
>
<div v-if="paypal.isMethodLoading" class="translate-y-3 scale-50 transform">
<Spinner />
</div>
<span
v-if="!paypal.isMethodsLoaded"
:class="{ 'opacity-0': paypal.isMethodLoading }"
class="text-theme cursor-pointer text-sm font-bold"
>
{{ $t('select') }}
</span>
</PaymentMethod>
<!--PayPal Buttons-->
<div id="paypal-button-container"></div>
</div>
<!--Paystack implementation-->
<PaymentMethod
v-if="config.isPaystack"
driver="paystack"
:description="$t(config.paystack_payment_description)"
>
<div v-if="paystack.isGettingCheckoutLink" class="translate-y-3 scale-50 transform">
<Spinner />
</div>
<span
@click="payByPaystack()"
:class="{ 'opacity-0': paystack.isGettingCheckoutLink }"
class="text-theme cursor-pointer text-sm font-bold"
>
{{ $t('select') }}
</span>
</PaymentMethod>
</PopupContent>
<PopupActions>
<ButtonBase class="w-full" @click.native="$closePopup()" button-style="secondary">
{{ $t('cancel_payment') }}
</ButtonBase>
</PopupActions>
</PopupWrapper>
</template>
<script>
import PopupWrapper from './Popup/PopupWrapper'
import PopupActions from './Popup/PopupActions'
import PopupContent from './Popup/PopupContent'
import PopupHeader from './Popup/PopupHeader'
import ButtonBase from '../FilesView/ButtonBase'
import { loadScript } from '@paypal/paypal-js'
import PaymentMethod from './PaymentMethod'
import Spinner from '../FilesView/Spinner'
import InfoBox from "./Forms/InfoBox"
import { events } from '../../bus'
import { mapGetters } from 'vuex'
import axios from "axios";
export default {
name: 'ChargePaymentPopup',
components: {
PaymentMethod,
PopupWrapper,
PopupActions,
PopupContent,
PopupHeader,
ButtonBase,
Spinner,
InfoBox,
},
data() {
return {
paypal: {
isMethodsLoaded: false,
isMethodLoading: false,
},
paystack: {
isGettingCheckoutLink: false,
},
}
},
computed: {
...mapGetters(['singleChargeAmount', 'config', 'user']),
},
methods: {
payByPaystack() {
this.paystack.isGettingCheckoutLink = true
axios
.post('/api/paystack/checkout', {
amount: this.singleChargeAmount * 100,
})
.then((response) => {
window.location = response.data.data.authorization_url
})
},
async payByPayPal() {
if (this.paypal.isMethodLoading) {
return
}
this.paypal.isMethodLoading = true
let paypal
try {
paypal = await loadScript({
'client-id': this.config.paypal_client_id,
vault: true,
})
} catch (error) {
events.$emit('toaster', {
type: 'danger',
message: this.$t('failed_to_load_paypal'),
})
}
const userId = this.user.data.id
const amount = this.singleChargeAmount
this.paypal.isMethodsLoaded = true
this.paypal.isMethodLoading = false
const app = this
// Initialize paypal buttons for single charge
await paypal
.Buttons({
createOrder: function (data, actions) {
return actions.order.create({
purchase_units: [
{
amount: {
value: amount,
},
custom_id: userId,
},
],
})
},
onApprove: function () {
app.paymentSuccessful()
},
})
.render('#paypal-button-container')
},
paymentSuccessful() {
this.$closePopup()
events.$emit('toaster', {
type: 'success',
message: this.$t('payment_was_successfully_received'),
})
// todo: temporary reload function
setTimeout(() => document.location.reload(), 500)
},
},
created() {
events.$on('popup:close', () => (this.paypal.isMethodsLoaded = false))
},
}
</script>

View File

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

View File

@@ -1,132 +0,0 @@
<template>
<div class="color-pick-wrapper">
<label class="main-label">{{ $t('popup_rename.color_pick_label') }}:</label>
<ul class="color-wrapper">
<li v-for="(color, i) in colors" :key="i" @click="setColor(color)" class="single-color">
<check-icon v-if="color === selectedColor" class="color-icon" size="22" />
<span :style="{ background: color }" class="color-box"></span>
</li>
</ul>
</div>
</template>
<script>
import { CheckIcon } from 'vue-feather-icons'
import { mapGetters } from 'vuex'
export default {
name: 'ColorPicker',
props: ['pickedColor'],
components: { CheckIcon },
computed: {
...mapGetters(['config']),
},
data() {
return {
selectedColor: this.pickedColor,
colors: [
'#FE6F6F',
'#FE6F91',
'#FE6FC0',
'#FE6FF0',
'#DD6FFE',
'#AD6FFE',
'#7D6FFE',
'#6F90FE',
'#6FC0FE',
'#6FF0FE',
'#6FFEDD',
'#6FFEAD',
'#6FFE7D',
'#90FE6F',
'#C0FE6F',
'#F0FE6F',
'#FEDD6F',
'#FEAD6F',
'#FE7D6F',
'#4c4c4c',
'#06070B',
],
}
},
methods: {
setColor(value) {
this.selectedColor = value
this.$emit('input', value)
},
},
created() {
this.colors.push(this.config.app_color)
},
}
</script>
<style lang="scss" scoped>
@import '../../../sass/vuefilemanager/inapp-forms';
@import '../../../sass/vuefilemanager/forms';
.color-pick-wrapper {
.color-wrapper {
margin-bottom: 20px;
display: grid;
grid-template-columns: repeat(auto-fill, 32px);
justify-content: space-between;
gap: 7px;
.single-color {
height: 31px;
list-style: none;
border-radius: 8px;
cursor: pointer;
position: relative;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
.color-icon {
z-index: 2;
polyline {
stroke: white;
}
}
.color-box {
width: 100%;
height: 100%;
position: absolute;
display: block;
}
}
}
}
.main-label {
@include font-size(14);
font-weight: 700;
margin-bottom: 8px;
display: block;
}
.set-folder-icon {
position: relative;
}
.dark {
.color-pick-wrapper {
.color-wrapper {
.single-color {
&.active-color {
border: 2px solid;
}
&:hover {
border: 2px solid $dark_mode_text_primary;
}
}
}
}
}
</style>

View File

@@ -1,108 +0,0 @@
<template>
<PopupWrapper name="confirm-password">
<PopupHeader :title="$t('confirm_password')" icon="edit" />
<PopupContent>
<ValidationObserver @submit.prevent="confirmPassword" ref="passwordForm" v-slot="{ invalid }" tag="form">
<ValidationProvider tag="div" mode="passive" name="Password" rules="required" v-slot="{ errors }">
<AppInputText :title="$t('password')" :error="errors[0]" :is-last="true">
<input
v-model="password"
:class="{ '!border-rose-600': errors[0] }"
type="password"
ref="input"
class="focus-border-theme input-dark"
:placeholder="$t('page_sign_in.placeholder_password')"
/>
</AppInputText>
</ValidationProvider>
</ValidationObserver>
</PopupContent>
<PopupActions>
<ButtonBase class="w-full" @click.native="$closePopup()" button-style="secondary">
{{ $t('cancel') }}
</ButtonBase>
<ButtonBase
class="w-full"
@click.native="confirmPassword"
button-style="theme"
:loading="isLoading"
:disabled="isLoading"
>
{{ $t('confirm') }}
</ButtonBase>
</PopupActions>
</PopupWrapper>
</template>
<script>
import { ValidationProvider, ValidationObserver } from 'vee-validate/dist/vee-validate.full'
import PopupWrapper from './Popup/PopupWrapper'
import PopupActions from './Popup/PopupActions'
import PopupContent from './Popup/PopupContent'
import PopupHeader from './Popup/PopupHeader'
import ButtonBase from '../FilesView/ButtonBase'
import AppInputText from '../Admin/AppInputText'
import { required } from 'vee-validate/dist/rules'
import { events } from '../../bus'
import { mapGetters } from 'vuex'
import axios from 'axios'
export default {
name: 'ConfirmPassword',
components: {
ValidationProvider,
ValidationObserver,
AppInputText,
PopupWrapper,
PopupActions,
PopupContent,
PopupHeader,
ButtonBase,
required,
},
computed: {
...mapGetters(['user']),
},
data() {
return {
isLoading: false,
password: undefined,
args: undefined,
}
},
methods: {
confirmPassword() {
this.isLoading = true
axios
.post('/user/confirm-password', {
password: this.password,
})
.then(() => {
events.$emit('password:confirmed', this.args)
})
.catch((error) => {
if (error.response.status === 422) {
this.$refs.passwordForm.setErrors({
Password: this.$t('validation_errors.incorrect_password'),
})
}
})
.finally(() => {
this.isLoading = false
this.password = undefined
})
},
},
created() {
// Show popup
events.$on('popup:open', (args) => {
if (args.name !== 'confirm-password') return
this.args = args
})
},
}
</script>

View File

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

View File

@@ -1,113 +0,0 @@
<template>
<PopupWrapper name="create-folder">
<!--Title-->
<PopupHeader :title="$t('popup_create_folder.title')" icon="edit" />
<!--Content-->
<PopupContent>
<!--Form to set sharing-->
<ValidationObserver @submit.prevent="createFolder" ref="createForm" v-slot="{ invalid }" tag="form">
<!--Set folder name-->
<ValidationProvider tag="div" mode="passive" name="Title" rules="required" v-slot="{ errors }">
<AppInputText :title="$t('popup_create_folder.label')" :error="errors[0]">
<input
v-model="name"
:class="{ '!border-rose-600': errors[0] }"
type="text"
ref="input"
class="focus-border-theme input-dark"
:placeholder="$t('popup_create_folder.placeholder')"
/>
</AppInputText>
</ValidationProvider>
<AppInputSwitch
:title="$t('emoji_as_an_icon')"
:description="$t('replace_icon_with_emoji')"
:is-last="!isEmoji"
>
<SwitchInput v-model="isEmoji" :state="isEmoji" />
</AppInputSwitch>
<!--Set emoji-->
<EmojiPicker v-if="isEmoji" v-model="emoji" :default-emoji="emoji" />
</ValidationObserver>
</PopupContent>
<!--Actions-->
<PopupActions>
<ButtonBase class="w-full" @click.native="$closePopup()" button-style="secondary"
>{{ $t('cancel') }}
</ButtonBase>
<ButtonBase class="w-full" @click.native="createFolder" button-style="theme"
>{{ $t('popup_create_folder.title') }}
</ButtonBase>
</PopupActions>
</PopupWrapper>
</template>
<script>
import { ValidationProvider, ValidationObserver } from 'vee-validate/dist/vee-validate.full'
import PopupWrapper from './Popup/PopupWrapper'
import PopupActions from './Popup/PopupActions'
import PopupContent from './Popup/PopupContent'
import PopupHeader from './Popup/PopupHeader'
import ThumbnailItem from './ThumbnailItem'
import ButtonBase from '../FilesView/ButtonBase'
import { required } from 'vee-validate/dist/rules'
import AppInputSwitch from '../Admin/AppInputSwitch'
import AppInputText from '../Admin/AppInputText'
import SwitchInput from './Forms/SwitchInput'
import { events } from '../../bus'
import EmojiPicker from './EmojiPicker'
export default {
name: 'CreateFolderPopup',
components: {
AppInputSwitch,
SwitchInput,
EmojiPicker,
AppInputText,
ValidationProvider,
ValidationObserver,
ThumbnailItem,
PopupWrapper,
PopupActions,
PopupContent,
PopupHeader,
ButtonBase,
required,
},
data() {
return {
name: undefined,
isEmoji: false,
emoji: undefined,
}
},
methods: {
async createFolder() {
// Validate fields
const isValid = await this.$refs.createForm.validate()
if (!isValid) return
await this.$store.dispatch('createFolder', {
name: this.name,
emoji: this.emoji,
})
this.$closePopup()
this.name = undefined
this.isEmoji = false
this.emoji = undefined
},
},
mounted() {
events.$on('popup:open', ({ name }) => {
if (name === 'create-folder' && !this.$isMobile()) this.$nextTick(() => this.$refs.input.focus())
})
},
}
</script>

View File

@@ -1,866 +0,0 @@
<template>
<PopupWrapper name="create-language">
<!--Title-->
<PopupHeader :title="$t('create_language')" icon="edit" />
<!--Content-->
<PopupContent class="!overflow-initial">
<!--Form to set sharing-->
<ValidationObserver @submit.prevent="createLanguage" ref="createForm" v-slot="{ invalid }" tag="form">
<ValidationProvider
tag="div"
mode="passive"
name="Language Locale"
rules="required"
v-slot="{ errors }"
>
<AppInputText :title="$t('select_locale')" :error="errors[0]">
<SelectInput
v-model="form.locale"
:options="locales"
:placeholder="$t('select_language_locale')"
:isError="errors[0]"
/>
</AppInputText>
</ValidationProvider>
<ValidationProvider tag="div" mode="passive" name="Language Name" rules="required" v-slot="{ errors }">
<AppInputText :title="$t('locale_name')" :error="errors[0]" :is-last="true">
<input
v-model="form.name"
:class="{ '!border-rose-600': errors[0] }"
type="text"
ref="input"
class="focus-border-theme input-dark"
:placeholder="$t('type_language_name')"
/>
</AppInputText>
</ValidationProvider>
</ValidationObserver>
</PopupContent>
<!--Actions-->
<PopupActions>
<ButtonBase class="w-full" @click.native="$closePopup()" button-style="secondary">
{{ $t('cancel') }}
</ButtonBase>
<ButtonBase
class="w-full"
@click.native="createLanguage"
button-style="theme"
:loading="isLoading"
:disabled="isLoading"
>
{{ $t('create_language') }}
</ButtonBase>
</PopupActions>
</PopupWrapper>
</template>
<script>
import AppInputText from '../Admin/AppInputText'
import { ValidationProvider, ValidationObserver } from 'vee-validate/dist/vee-validate.full'
import PopupWrapper from './Popup/PopupWrapper'
import PopupActions from './Popup/PopupActions'
import PopupContent from './Popup/PopupContent'
import PopupHeader from './Popup/PopupHeader'
import SelectInput from './Forms/SelectInput'
import ButtonBase from '../FilesView/ButtonBase'
import { required } from 'vee-validate/dist/rules'
import { events } from '../../bus'
import axios from 'axios'
export default {
name: 'CreateLanguage',
components: {
ValidationProvider,
ValidationObserver,
AppInputText,
PopupWrapper,
PopupActions,
PopupContent,
PopupHeader,
SelectInput,
ButtonBase,
required,
},
data() {
return {
form: {
name: undefined,
locale: undefined,
},
isLoading: false,
locales: [
{
value: 'ab',
label: 'Abkhaz',
},
{
value: 'aa',
label: 'Afar',
},
{
value: 'af',
label: 'Afrikaans',
},
{
value: 'ak',
label: 'Akan',
},
{
value: 'sq',
label: 'Albanian',
},
{
value: 'am',
label: 'Amharic',
},
{
value: 'ar',
label: 'Arabic',
},
{
value: 'an',
label: 'Aragonese',
},
{
value: 'hy',
label: 'Armenian',
},
{
value: 'as',
label: 'Assamese',
},
{
value: 'av',
label: 'Avaric',
},
{
value: 'ae',
label: 'Avestan',
},
{
value: 'ay',
label: 'Aymara',
},
{
value: 'az',
label: 'Azerbaijani',
},
{
value: 'bm',
label: 'Bambara',
},
{
value: 'ba',
label: 'Bashkir',
},
{
value: 'eu',
label: 'Basque',
},
{
value: 'be',
label: 'Belarusian',
},
{
value: 'bn',
label: 'Bengali; Bangla',
},
{
value: 'bh',
label: 'Bihari',
},
{
value: 'bi',
label: 'Bislama',
},
{
value: 'bs',
label: 'Bosnian',
},
{
value: 'br',
label: 'Breton',
},
{
value: 'bg',
label: 'Bulgarian',
},
{
value: 'my',
label: 'Burmese',
},
{
value: 'ca',
label: 'Catalan; Valencian',
},
{
value: 'ch',
label: 'Chamorro',
},
{
value: 'ce',
label: 'Chechen',
},
{
value: 'ny',
label: 'Chichewa; Chewa; Nyanja',
},
{
value: 'zh',
label: 'Chinese',
},
{
value: 'cv',
label: 'Chuvash',
},
{
value: 'kw',
label: 'Cornish',
},
{
value: 'co',
label: 'Corsican',
},
{
value: 'cr',
label: 'Cree',
},
{
value: 'hr',
label: 'Croatian',
},
{
value: 'cs',
label: 'Czech',
},
{
value: 'da',
label: 'Danish',
},
{
value: 'dv',
label: 'Divehi; Dhivehi; Maldivian;',
},
{
value: 'nl',
label: 'Dutch',
},
{
value: 'dz',
label: 'Dzongkha',
},
{
value: 'en',
label: 'English',
},
{
value: 'eo',
label: 'Esperanto',
},
{
value: 'et',
label: 'Estonian',
},
{
value: 'ee',
label: 'Ewe',
},
{
value: 'fo',
label: 'Faroese',
},
{
value: 'fj',
label: 'Fijian',
},
{
value: 'fi',
label: 'Finnish',
},
{
value: 'fr',
label: 'French',
},
{
value: 'ff',
label: 'Fula; Fulah; Pulaar; Pular',
},
{
value: 'gl',
label: 'Galician',
},
{
value: 'lg',
label: 'Ganda',
},
{
value: 'ka',
label: 'Georgian',
},
{
value: 'de',
label: 'German',
},
{
value: 'el',
label: 'Greek, Modern',
},
{
value: 'gn',
label: 'Guaraní',
},
{
value: 'gu',
label: 'Gujarati',
},
{
value: 'ht',
label: 'Haitian; Haitian Creole',
},
{
value: 'ha',
label: 'Hausa',
},
{
value: 'he',
label: 'Hebrew (modern)',
},
{
value: 'hz',
label: 'Herero',
},
{
value: 'hi',
label: 'Hindi',
},
{
value: 'ho',
label: 'Hiri Motu',
},
{
value: 'hu',
label: 'Hungarian',
},
{
value: 'ia',
label: 'Interlingua',
},
{
value: 'id',
label: 'Indonesian',
},
{
value: 'ie',
label: 'Interlingue',
},
{
value: 'ga',
label: 'Irish',
},
{
value: 'ig',
label: 'Igbo',
},
{
value: 'ik',
label: 'Inupiaq',
},
{
value: 'io',
label: 'Ido',
},
{
value: 'is',
label: 'Icelandic',
},
{
value: 'it',
label: 'Italian',
},
{
value: 'iu',
label: 'Inuktitut',
},
{
value: 'ja',
label: 'Japanese',
},
{
value: 'jv',
label: 'Javanese',
},
{
value: 'kl',
label: 'Kalaallisut, Greenlandic',
},
{
value: 'kn',
label: 'Kannada',
},
{
value: 'kr',
label: 'Kanuri',
},
{
value: 'ks',
label: 'Kashmiri',
},
{
value: 'kk',
label: 'Kazakh',
},
{
value: 'km',
label: 'Khmer',
},
{
value: 'ki',
label: 'Kikuyu, Gikuyu',
},
{
value: 'rw',
label: 'Kinyarwanda',
},
{
value: 'rn',
label: 'Kirundi',
},
{
value: 'ky',
label: 'Kyrgyz',
},
{
value: 'kv',
label: 'Komi',
},
{
value: 'kg',
label: 'Kongo',
},
{
value: 'ko',
label: 'Korean',
},
{
value: 'ku',
label: 'Kurdish',
},
{
value: 'kj',
label: 'Kwanyama, Kuanyama',
},
{
value: 'la',
label: 'Latin',
},
{
value: 'lb',
label: 'Luxembourgish, Letzeburgesch',
},
{
value: 'li',
label: 'Limburgish, Limburgan, Limburger',
},
{
value: 'ln',
label: 'Lingala',
},
{
value: 'lo',
label: 'Lao',
},
{
value: 'lt',
label: 'Lithuanian',
},
{
value: 'lu',
label: 'Luba-Katanga',
},
{
value: 'lv',
label: 'Latvian',
},
{
value: 'gv',
label: 'Manx',
},
{
value: 'mk',
label: 'Macedonian',
},
{
value: 'mg',
label: 'Malagasy',
},
{
value: 'ms',
label: 'Malay',
},
{
value: 'ml',
label: 'Malayalam',
},
{
value: 'mt',
label: 'Maltese',
},
{
value: 'mi',
label: 'MÄori',
},
{
value: 'mr',
label: 'Marathi',
},
{
value: 'mh',
label: 'Marshallese',
},
{
value: 'mn',
label: 'Mongolian',
},
{
value: 'na',
label: 'Nauru',
},
{
value: 'nv',
label: 'Navajo, Navaho',
},
{
value: 'nb',
label: 'Norwegian',
},
{
value: 'nd',
label: 'North Ndebele',
},
{
value: 'ne',
label: 'Nepali',
},
{
value: 'ng',
label: 'Ndonga',
},
{
value: 'nn',
label: 'Norwegian Nynorsk',
},
{
value: 'no',
label: 'Norwegian',
},
{
value: 'ii',
label: 'Nuosu',
},
{
value: 'oc',
label: 'Occitan',
},
{
value: 'oj',
label: 'Ojibwe, Ojibwa',
},
{
value: 'cu',
label: 'Old Church Slavonic, Church Slavic, Church Slavonic, Old Bulgarian, Old Slavonic',
},
{
value: 'om',
label: 'Oromo',
},
{
value: 'or',
label: 'Oriya',
},
{
value: 'os',
label: 'Ossetian, Ossetic',
},
{
value: 'pa',
label: 'Panjabi, Punjabi',
},
{
value: 'pi',
label: 'Pali',
},
{
value: 'fa',
label: 'Persian (Farsi)',
},
{
value: 'pl',
label: 'Polish',
},
{
value: 'ps',
label: 'Pashto, Pushto',
},
{
value: 'pt',
label: 'Portuguese',
},
{
value: 'qu',
label: 'Quechua',
},
{
value: 'rm',
label: 'Romansh',
},
{
value: 'ro',
label: 'Romanian',
},
{
value: 'ru',
label: 'Russian',
},
{
value: 'sa',
label: 'Sanskrit',
},
{
value: 'sc',
label: 'Sardinian',
},
{
value: 'sd',
label: 'Sindhi',
},
{
value: 'se',
label: 'Northern Sami',
},
{
value: 'sm',
label: 'Samoan',
},
{
value: 'sg',
label: 'Sango',
},
{
value: 'sr',
label: 'Serbian',
},
{
value: 'gd',
label: 'Scottish Gaelic',
},
{
value: 'sn',
label: 'Shona',
},
{
value: 'si',
label: 'Sinhala, Sinhalese',
},
{
value: 'sk',
label: 'Slovak',
},
{
value: 'sl',
label: 'Slovene',
},
{
value: 'so',
label: 'Somali',
},
{
value: 'st',
label: 'Southern Sotho',
},
{
value: 'az',
label: 'South Azerbaijani',
},
{
value: 'nr',
label: 'South Ndebele',
},
{
value: 'es',
label: 'Spanish; Castilian',
},
{
value: 'su',
label: 'Sundanese',
},
{
value: 'sw',
label: 'Swahili',
},
{
value: 'ss',
label: 'Swati',
},
{
value: 'sv',
label: 'Swedish',
},
{
value: 'ta',
label: 'Tamil',
},
{
value: 'te',
label: 'Telugu',
},
{
value: 'tg',
label: 'Tajik',
},
{
value: 'th',
label: 'Thai',
},
{
value: 'ti',
label: 'Tigrinya',
},
{
value: 'bo',
label: 'Tibetan Standard, Tibetan, Central',
},
{
value: 'tk',
label: 'Turkmen',
},
{
value: 'tl',
label: 'Tagalog',
},
{
value: 'tn',
label: 'Tswana',
},
{
value: 'to',
label: 'Tonga (Tonga Islands)',
},
{
value: 'tr',
label: 'Turkish',
},
{
value: 'ts',
label: 'Tsonga',
},
{
value: 'tt',
label: 'Tatar',
},
{
value: 'tw',
label: 'Twi',
},
{
value: 'ty',
label: 'Tahitian',
},
{
value: 'ug',
label: 'Uyghur, Uighur',
},
{
value: 'uk',
label: 'Ukrainian',
},
{
value: 'ur',
label: 'Urdu',
},
{
value: 'uz',
label: 'Uzbek',
},
{
value: 've',
label: 'Venda',
},
{
value: 'vi',
label: 'Vielabele',
},
{
value: 'vo',
label: 'Volapük',
},
{
value: 'wa',
label: 'Walloon',
},
{
value: 'cy',
label: 'Welsh',
},
{
value: 'wo',
label: 'Wolof',
},
{
value: 'fy',
label: 'Western Frisian',
},
{
value: 'xh',
label: 'Xhosa',
},
{
value: 'yi',
label: 'Yiddish',
},
{
value: 'yo',
label: 'Yoruba',
},
{
value: 'za',
label: 'Zhuang, Chuang',
},
{
value: 'zu',
label: 'Zulu',
},
],
}
},
methods: {
async createLanguage() {
// Validate fields
const isValid = await this.$refs.createForm.validate()
if (isValid) {
this.isLoading = true
axios
.post('/api/admin/languages', this.form)
.then((response) => {
events.$emit('reload:languages', response.data)
})
.catch(() => {
this.$isSomethingWrong()
})
.finally(() => {
this.form = {
name: undefined,
locale: undefined,
}
this.isLoading = false
this.$closePopup()
})
}
},
},
}
</script>

View File

@@ -1,134 +0,0 @@
<template>
<PopupWrapper name="create-personal-token">
<PopupHeader :title="$t('create_personal_token')" icon="key" />
<PopupContent>
<ValidationObserver
v-if="!token"
@submit.prevent="createTokenForm"
ref="createToken"
v-slot="{ invalid }"
tag="form"
>
<ValidationProvider tag="div" mode="passive" name="Token Name" rules="required|min:3" v-slot="{ errors }">
<AppInputText :title="$t('token_name')" :error="errors[0]" :is-last="true">
<input
v-model="name"
:class="{ '!border-rose-600': errors[0] }"
type="text"
ref="input"
class="focus-border-theme input-dark"
:placeholder="$t('popup_personal_token.plc')"
/>
</AppInputText>
</ValidationProvider>
</ValidationObserver>
<AppInputText v-if="token" :title="$t('popup_personal_token.your_token')" :is-last="true">
<CopyInput size="small" :str="token['plainTextToken']" />
<InfoBox style="margin-bottom: 0; margin-top: 20px">
<p v-html="$t('popup_personal_token.copy_token')"></p>
</InfoBox>
</AppInputText>
</PopupContent>
<PopupActions v-if="!token">
<ButtonBase class="w-full" @click.native="$closePopup()" button-style="secondary">
{{ $t('cancel') }}
</ButtonBase>
<ButtonBase
class="w-full"
@click.native="createTokenForm"
button-style="theme"
:loading="isLoading"
:disabled="isLoading"
>
{{ $t('create_token') }}
</ButtonBase>
</PopupActions>
<PopupActions v-if="token">
<ButtonBase class="w-full" @click.native="$closePopup" button-style="theme">
{{ $t('awesome_iam_done') }}
</ButtonBase>
</PopupActions>
</PopupWrapper>
</template>
<script>
import AppInputText from '../Admin/AppInputText'
import { ValidationProvider, ValidationObserver } from 'vee-validate/dist/vee-validate.full'
import PopupWrapper from './Popup/PopupWrapper'
import PopupActions from './Popup/PopupActions'
import PopupContent from './Popup/PopupContent'
import PopupHeader from './Popup/PopupHeader'
import CopyInput from './Forms/CopyInput'
import ButtonBase from '../FilesView/ButtonBase'
import InfoBox from './Forms/InfoBox'
import { required } from 'vee-validate/dist/rules'
import { events } from '../../bus'
import axios from 'axios'
export default {
name: 'CreatePersonalTokenPopup',
components: {
ValidationProvider,
ValidationObserver,
AppInputText,
PopupWrapper,
PopupActions,
PopupContent,
PopupHeader,
ButtonBase,
CopyInput,
required,
InfoBox,
},
data() {
return {
isLoading: false,
name: undefined,
token: undefined,
}
},
methods: {
async createTokenForm() {
const isValid = await this.$refs.createToken.validate()
if (!isValid) return
this.isLoading = true
axios
.post('/api/user/tokens', {
name: this.name,
})
.then((response) => {
this.token = response.data
events.$emit('reload-personal-access-tokens')
})
.catch(() => this.$isSomethingWrong())
.finally(() => {
this.isLoading = false
this.name = undefined
})
},
},
created() {
events.$on('popup:close', () => this.token = undefined)
}
}
</script>
<style lang="scss" scoped>
@import '../../../sass/vuefilemanager/inapp-forms';
@import '../../../sass/vuefilemanager/forms';
.dark {
.info-box {
background: lighten($dark_mode_foreground, 3%);
}
}
</style>

View File

@@ -1,210 +0,0 @@
<template>
<PopupWrapper name="create-file-request">
<!--Title-->
<PopupHeader :title="$t('create_file_request')" icon="upload" />
<!--Content-->
<PopupContent>
<!--Item Thumbnail-->
<ThumbnailItem v-if="pickedItem" class="mb-5" :item="pickedItem" />
<!--Form to set upload request-->
<ValidationObserver
v-if="!generatedUploadRequest"
@submit.prevent="createUploadRequest"
ref="createForm"
v-slot="{ invalid }"
tag="form"
>
<!--Set name-->
<ValidationProvider
tag="div"
mode="passive"
name="Name"
v-slot="{ errors }"
>
<AppInputText :title="$t('folder_name_optional')" :description="$t('folder_name_optional_description')" :error="errors[0]">
<input
v-model="form.name"
:class="{ '!border-rose-600': errors[0] }"
type="text"
ref="input"
class="focus-border-theme input-dark"
:placeholder="$t('type_name_')"
/>
</AppInputText>
</ValidationProvider>
<!--Set note-->
<ValidationProvider tag="div" mode="passive" name="Note" v-slot="{ errors }">
<AppInputText :title="$t('message_optional')" :description="$t('message_optional_description')" :error="errors[0]">
<textarea
v-model="form.notes"
rows="2"
:class="{ '!border-rose-600': errors[0] }"
type="text"
ref="input"
class="focus-border-theme input-dark"
:placeholder="$t('message_for_recipient')"
></textarea>
</AppInputText>
</ValidationProvider>
<!--Send Request by Email-->
<AppInputSwitch
:title="$t('send_request_by_email')"
:description="$t('send_request_by_email_description')"
:is-last="! shareViaEmail"
>
<SwitchInput v-model="shareViaEmail" :state="shareViaEmail" />
</AppInputSwitch>
<!--Set email-->
<ValidationProvider
v-if="shareViaEmail"
tag="div"
mode="passive"
name="Email"
rules="required"
v-slot="{ errors }"
>
<AppInputText :error="errors[0]" class="-mt-2" :is-last="true">
<input
v-model="form.email"
:class="{ '!border-rose-600': errors[0] }"
type="text"
ref="input"
class="focus-border-theme input-dark"
:placeholder="$t('type_email_')"
/>
</AppInputText>
</ValidationProvider>
</ValidationObserver>
<!--Copy generated link-->
<AppInputText v-if="generatedUploadRequest" :title="$t('copy_upload_request_link')" :is-last="true">
<CopyInput :str="generatedUploadRequest.data.attributes.url" />
</AppInputText>
</PopupContent>
<!--Actions-->
<PopupActions v-if="!generatedUploadRequest">
<ButtonBase class="w-full" @click.native="$closePopup()" button-style="secondary"
>{{ $t('cancel') }}
</ButtonBase>
<ButtonBase class="w-full" @click.native="createUploadRequest" :loading="isLoading" button-style="theme"
>{{ $t('create_request') }}
</ButtonBase>
</PopupActions>
<!--Actions-->
<PopupActions v-if="generatedUploadRequest">
<ButtonBase class="w-full" @click.native="$closePopup()" button-style="theme"
>{{ $t('awesome_iam_done') }}
</ButtonBase>
</PopupActions>
</PopupWrapper>
</template>
<script>
import {ValidationProvider, ValidationObserver} from 'vee-validate/dist/vee-validate.full'
import AppInputSwitch from '../Admin/AppInputSwitch'
import {required} from 'vee-validate/dist/rules'
import ButtonBase from '../FilesView/ButtonBase'
import AppInputText from '../Admin/AppInputText'
import PopupWrapper from './Popup/PopupWrapper'
import PopupActions from './Popup/PopupActions'
import PopupContent from './Popup/PopupContent'
import PopupHeader from './Popup/PopupHeader'
import SwitchInput from './Forms/SwitchInput'
import ThumbnailItem from './ThumbnailItem'
import CopyInput from './Forms/CopyInput'
import {events} from '../../bus'
import axios from 'axios'
export default {
name: 'CreateUploadRequestPopup',
components: {
ValidationProvider,
ValidationObserver,
AppInputSwitch,
ThumbnailItem,
AppInputText,
PopupWrapper,
PopupActions,
PopupContent,
SwitchInput,
PopupHeader,
ButtonBase,
CopyInput,
required,
},
data() {
return {
form: {
email: undefined,
notes: undefined,
folder_id: undefined,
name: undefined,
},
generatedUploadRequest: undefined,
shareViaEmail: false,
pickedItem: undefined,
isLoading: false,
}
},
methods: {
async createUploadRequest() {
// Validate fields
const isValid = await this.$refs.createForm.validate()
if (!isValid) return
this.isLoading = true
// Send request to get share link
axios
.post(`/api/upload-request`, this.form)
.then((response) => {
this.generatedUploadRequest = response.data
})
.catch(() => {
events.$emit('alert:open', {
title: this.$t('popup_error.title'),
message: this.$t('popup_error.message'),
})
})
.finally(() => {
this.isLoading = false
})
},
},
created() {
events.$on('popup:open', (args) => {
if (args.name === 'create-file-request') {
this.pickedItem = args.item
this.form.folder_id = args.item?.data.id
}
})
// Close popup
events.$on('popup:close', () => {
// Restore data
setTimeout(() => {
this.generatedUploadRequest = undefined
this.pickedItem = undefined
this.shareViaEmail = false
this.form = {
name: undefined,
email: undefined,
notes: undefined,
folder_id: undefined,
}
}, 150)
})
},
}
</script>

View File

@@ -1,48 +0,0 @@
<template>
<div v-if="emoji">
<div
v-if="config.defaultEmoji === 'twemoji'"
v-html="transferEmoji"
style="font-size: inherit; transform: scale(0.95)"
></div>
<div v-if="config.defaultEmoji === 'applemoji'" style="font-size: inherit">
{{ emoji.char }}
</div>
</div>
</template>
<script>
import twemoji from 'twemoji'
import { mapGetters } from 'vuex'
export default {
name: 'Emoji',
props: ['emoji'],
data() {
return {
isApple: false,
sizeClass: undefined,
}
},
computed: {
...mapGetters(['config']),
transferEmoji() {
return twemoji.parse(this.emoji.char, {
folder: 'svg',
ext: '.svg',
attributes: () => ({
loading: 'lazy',
}),
})
},
},
}
</script>
<style lang="css">
.emoji {
height: 1em;
width: 1em;
font-size: inherit;
}
</style>

View File

@@ -1,194 +0,0 @@
<template>
<div>
<!-- Search field -->
<div class="relative mb-3 flex items-center">
<!-- Selected emoji preview -->
<div v-if="defaultEmoji" class="mr-3 select-none">
<Emoji :emoji="defaultEmoji" class="text-5xl" />
</div>
<!-- Search input -->
<input
@click="openList"
v-model="query"
class="focus-border-theme input-dark"
type="text"
:placeholder="$t('select_or_search_emoji')"
/>
</div>
<!-- Spinner -->
<div v-if="isOpen && !isLoaded" class="relative h-20 select-none">
<Spinner />
</div>
<!-- Emojis List -->
<div
v-if="isOpen && isLoaded && emojis"
@scroll="checkGroupInView"
id="group-box"
class="relative h-96 select-none overflow-y-auto lg:h-60 2xl:h-96"
>
<!-- Navigation of Emojis Groups -->
<ul
v-if="!query"
class="sticky top-0 z-10 flex items-center justify-between space-x-1 bg-white dark:bg-dark-background sm:dark:bg-4x-dark-foreground"
id="group-bar"
>
<li
@click.stop="scrollToGroup(group.name)"
v-for="(group, i) in emojis.groups"
:key="i"
class="flex h-14 w-14 cursor-pointer items-center justify-center rounded-xl hover:bg-light-background dark:hover:bg-2x-dark-foreground"
:class="{
'bg-light-background dark:bg-2x-dark-foreground': group.name === groupInView,
}"
>
<Emoji :emoji="group.emoji" class="text-3xl" />
</li>
</ul>
<!-- All Emojis -->
<div v-if="!query" v-for="(group, name) in allEmoji" :key="name" :id="`group-${name}`">
<label class="mt-4 mb-2 block text-sm font-bold">
{{ name }}
</label>
<ul class="space-between grid grid-cols-7 gap-4 md:grid-cols-9">
<li
@click="setEmoji(emoji)"
v-for="(emoji, i) in group"
:key="i"
class="flex cursor-pointer items-center justify-center"
>
<Emoji :emoji="emoji" class="text-4xl" />
</li>
</ul>
</div>
<!-- Searched emojis -->
<ul v-if="query" class="space-between grid grid-cols-7 gap-4 md:grid-cols-9">
<li
@click="setEmoji(emoji)"
v-for="(emoji, i) in filteredEmojis"
:key="i"
class="flex cursor-pointer items-center justify-center"
>
<Emoji :emoji="emoji" class="text-4xl" />
</li>
</ul>
<!-- Not found -->
<span class="ml-2 text-sm font-bold" v-if="filteredEmojis.length === 0 && query !== undefined">
{{ $t('there_is_nothing_smile') }}
</span>
</div>
</div>
</template>
<script>
import Spinner from '../FilesView/Spinner'
import Emoji from './Emoji'
import { debounce, groupBy } from 'lodash'
import { XIcon } from 'vue-feather-icons'
import { mapGetters } from 'vuex'
export default {
name: 'EmojiPicker',
props: ['defaultEmoji'],
components: {
Spinner,
Emoji,
XIcon,
},
computed: {
...mapGetters(['emojis']),
allEmoji() {
return groupBy(this.emojis.list, 'group')
},
},
data() {
return {
query: undefined,
filteredEmojis: [],
isOpen: false,
isLoaded: false,
groupInView: 'Smileys & Emotion',
}
},
watch: {
query: debounce(function (val) {
// Clear search results
this.filteredEmojis = []
// Reset query
if (val === '' || val === undefined) return
// Filter emojis by query
this.filteredEmojis = this.emojis.list.filter((emoji) => emoji.name.includes(val.toLowerCase()))
if (this.filteredEmojis.length === 0) {
//
}
}, 200),
},
methods: {
checkGroupInView: _.debounce(function () {
this.emojis.groups.forEach((group) => {
let element = document.getElementById(`group-${group.name}`).getBoundingClientRect()
let groupBox = document.getElementById('group-box').getBoundingClientRect()
// Check if the group is in the viewport of group-box
if (element.top < groupBox.top && element.bottom > groupBox.top) {
this.groupInView = group.name
}
})
}, 300),
scrollToGroup(name) {
let groupBar = document.getElementById('group-bar')
let groupBox = document.getElementById('group-box')
let group = document.getElementById(`group-${name}`)
groupBox.scrollTo({
top: group.offsetTop - groupBar.clientHeight - 5,
left: 0,
behavior: 'smooth',
})
this.groupInView = name
},
openList() {
// Open list if it's not opened
if (!this.isOpen) this.isOpen = true
// Load emojis from server just if not loaded already
if (this.isOpen && !this.emojis) {
axios
.get('/assets/emojis.json')
.then((response) => {
this.$store.commit('LOAD_EMOJIS_LIST', response.data)
})
.finally(() => (this.isLoaded = true))
}
// Simulate loading for the list processing
if (this.emojis) {
setTimeout(() => {
this.isLoaded = true
}, 20)
}
this.groupInView = 'Smileys & Emotion'
},
setEmoji(value) {
this.query = undefined
this.isOpen = false
this.$emit('input', value)
},
},
mounted() {
// Open list of there isn't set any emoji
if (!this.defaultEmoji) this.openList()
},
}
</script>

View File

@@ -10,7 +10,7 @@
<h1 class="title">{{ title }}</h1>
<h2 class="description">{{ description }}</h2>
</div>
<slot></slot>
<slot />
</div>
</div>
</template>

View File

@@ -1,64 +0,0 @@
<template>
<div class="relative flex items-center justify-center cursor-pointer h-14 w-14 overflow-hidden bg-light-background rounded-xl cursor-pointer z-10">
<input
ref="file"
type="file"
@change="showImagePreview($event)"
class="absolute top-0 bottom-0 left-0 right-0 z-10 w-full cursor-pointer opacity-0"
/>
<camera-icon v-if="!imagePreview" size="22" class="vue-feather text-gray-300" />
<img
v-if="imagePreview"
ref="image"
:src="imagePreview"
class="relative w-full h-full z-0 object-cover shadow-lg md:h-16 md:w-16"
alt="avatar"
/>
</div>
</template>
<script>
import { CameraIcon} from 'vue-feather-icons'
export default {
name: 'AvatarInput',
props: ['avatar'],
components: {
CameraIcon,
},
data() {
return {
imagePreview: undefined,
}
},
watch: {
imagePreview(val) {
this.$store.commit('UPDATE_AVATAR', val)
},
},
methods: {
showImagePreview(event) {
let imgPath = event.target.files[0].name,
extension = imgPath.substring(imgPath.lastIndexOf('.') + 1).toLowerCase()
if (['png', 'jpg', 'jpeg'].includes(extension)) {
let file = event.target.files[0],
reader = new FileReader()
reader.onload = () => (this.imagePreview = reader.result)
reader.readAsDataURL(file)
// Update user avatar
this.$updateImage('/user/settings', 'avatar', event.target.files[0])
} else {
alert(this.$t('wrong_image_error'))
}
},
},
created() {
// If there is default image then load
if (this.avatar) this.imagePreview = this.avatar
},
}
</script>

View File

@@ -1,52 +0,0 @@
<template>
<div @click="copyUrl" class="relative flex items-center">
<input ref="sel" :value="str" :id="id" type="text" class="focus-border-theme input-dark !pr-10" readonly />
<!--Copy icon-->
<div class="absolute right-0 px-4">
<copy-icon v-if="!isCopiedLink" size="16" class="hover-text-theme vue-feather cursor-pointer" />
<check-icon v-if="isCopiedLink" size="16" class="text-theme vue-feather cursor-pointer" />
</div>
</div>
</template>
<script>
import { CopyIcon, CheckIcon, SendIcon } from 'vue-feather-icons'
export default {
name: 'CopyInput',
props: ['size', 'str'],
components: {
CheckIcon,
CopyIcon,
SendIcon,
},
data() {
return {
isCopiedLink: false,
id: 'link-input-' + Math.floor(Math.random() * 10000000),
}
},
methods: {
copyUrl() {
// Get input value
let copyText = document.getElementById(this.id)
// select link
copyText.select()
copyText.setSelectionRange(0, 99999)
// Copy
document.execCommand('copy')
// Mark button as copied
this.isCopiedLink = true
// Reset copy button
setTimeout(() => {
this.isCopiedLink = false
}, 1000)
},
},
}
</script>

View File

@@ -1,199 +0,0 @@
<template>
<div class="relative flex items-center">
<input
ref="sel"
:value="item.data.relationships.shared.data.attributes.link"
:id="id"
type="text"
class="focus-border-theme w-full appearance-none rounded-lg border border-transparent bg-light-background py-2 pr-16 pl-3 text-sm font-bold dark:bg-2x-dark-foreground"
readonly
/>
<!--Copy icon-->
<div class="flex items-center">
<div @click="copyUrl" class="absolute right-9 p-1">
<copy-icon v-if="!isCopiedLink" size="14" class="hover-text-theme vue-feather cursor-pointer" />
<check-icon v-if="isCopiedLink" size="14" class="hover-text-theme vue-feather cursor-pointer" />
</div>
<div @click.stop.prevent="moreOptions" class="absolute right-2.5 p-1">
<more-horizontal-icon size="14" class="hover-text-theme vue-feather cursor-pointer" />
</div>
</div>
<!--Hidden options-->
<ul
v-if="isOpenedMoreOptions"
class="absolute top-12 left-0 right-0 z-10 select-none overflow-y-auto overflow-x-hidden rounded-lg shadow-xl"
>
<li
v-if="item.data.type !== 'folder' && !item.data.relationships.shared.data.attributes.protected"
@click="copyDirectLink"
class="block flex cursor-pointer items-center bg-white py-2.5 px-5 hover:bg-light-background dark:bg-2x-dark-foreground dark:hover:bg-4x-dark-foreground"
>
<div class="w-8">
<download-icon size="14" />
</div>
<span class="text-sm font-bold">
{{ $t('copy_direct_download_link') }}
</span>
</li>
<li
@click="getQrCode"
class="block flex cursor-pointer items-center bg-white py-2.5 px-5 hover:bg-light-background dark:bg-2x-dark-foreground dark:hover:bg-4x-dark-foreground"
>
<div class="w-8">
<camera-icon size="14" />
</div>
<span class="text-sm font-bold">
{{ $t('get_qr_code') }}
</span>
</li>
<li
@click="sendViaEmail"
class="block flex cursor-pointer items-center bg-white py-2.5 px-5 hover:bg-light-background dark:bg-2x-dark-foreground dark:hover:bg-4x-dark-foreground"
>
<div class="w-8">
<send-icon size="14" />
</div>
<span class="text-sm font-bold">
{{ $t('sharelink.share_via_email') }}
</span>
</li>
<li
@click="copyIframe"
class="block flex cursor-pointer items-center bg-white py-2.5 px-5 hover:bg-light-background dark:bg-2x-dark-foreground dark:hover:bg-4x-dark-foreground"
>
<div class="w-8">
<code-icon size="14" />
</div>
<span class="text-sm font-bold">
{{ $t('sharelink.copy_embed') }}
</span>
</li>
</ul>
<textarea
v-model="directLink"
ref="directLinkTextarea"
class="pointer-events-none absolute right-full opacity-0"
></textarea>
<textarea
v-model="iframeCode"
ref="iframe"
class="pointer-events-none absolute right-full opacity-0"
></textarea>
</div>
</template>
<script>
import { DownloadIcon, CameraIcon, CopyIcon, CheckIcon, SendIcon, MoreHorizontalIcon, CodeIcon } from 'vue-feather-icons'
import { events } from '../../../bus'
export default {
name: 'CopyShareLink',
props: ['item'],
components: {
MoreHorizontalIcon,
CameraIcon,
CheckIcon,
CopyIcon,
CodeIcon,
SendIcon,
DownloadIcon,
},
watch: {
'item': function () {
this.setClipboard()
}
},
data() {
return {
id: 'link-input-' + Math.floor(Math.random() * 10000000),
directLink: undefined,
iframeCode: undefined,
isCopiedLink: false,
isOpenedMoreOptions: false,
}
},
methods: {
moreOptions() {
this.isOpenedMoreOptions = !this.isOpenedMoreOptions
},
getQrCode() {
events.$emit('popup:open', {
name: 'share-edit',
item: this.item,
section: 'qr-code',
})
this.isOpenedMoreOptions = false
},
sendViaEmail() {
events.$emit('popup:open', {
name: 'share-edit',
item: this.item,
section: 'email-sharing',
})
this.isOpenedMoreOptions = false
},
copyDirectLink() {
let copyText = this.$refs.directLinkTextarea
copyText.select()
copyText.setSelectionRange(0, 99999)
document.execCommand('copy')
events.$emit('toaster', {
type: 'success',
message: this.$t('direct_link_copied'),
})
this.isOpenedMoreOptions = false
},
copyIframe() {
let copyText = this.$refs.iframe
copyText.select()
copyText.setSelectionRange(0, 99999)
document.execCommand('copy')
events.$emit('toaster', {
type: 'success',
message: this.$t('web_code_copied'),
})
this.isOpenedMoreOptions = false
},
copyUrl() {
// Get input value
let copyText = document.getElementById(this.id)
// select link
copyText.select()
copyText.setSelectionRange(0, 99999)
// Copy
document.execCommand('copy')
// Mark button as copied
this.isCopiedLink = true
// Reset copy button
setTimeout(() => {
this.isCopiedLink = false
}, 1000)
},
setClipboard() {
this.directLink = this.item.data.relationships.shared.data.attributes.link + '/direct'
this.iframeCode = `<iframe src="${this.item.data.relationships.shared.data.attributes.link}" width="790" height="400" allowfullscreen frameborder="0"></iframe>`
}
},
created() {
this.setClipboard()
}
}
</script>

View File

@@ -1,76 +0,0 @@
<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></slot>
</b>
</div>
</template>
<script>
import {
TrendingUpIcon,
WifiIcon,
ListIcon,
MailIcon,
InfoIcon,
DatabaseIcon,
UsersIcon,
ShieldIcon,
CreditCardIcon,
DollarSignIcon,
SmartphoneIcon,
HardDriveIcon,
BarChartIcon,
SettingsIcon,
FileTextIcon,
FrownIcon,
Edit2Icon,
BellIcon,
KeyIcon,
} from 'vue-feather-icons'
export default {
name: 'FormLabel',
props: ['icon'],
components: {
TrendingUpIcon,
WifiIcon,
ListIcon,
MailIcon,
InfoIcon,
DatabaseIcon,
UsersIcon,
CreditCardIcon,
DollarSignIcon,
SmartphoneIcon,
HardDriveIcon,
BarChartIcon,
SettingsIcon,
FileTextIcon,
ShieldIcon,
FrownIcon,
Edit2Icon,
BellIcon,
KeyIcon,
},
}
</script>

View File

@@ -1,97 +0,0 @@
<template>
<div
class="relative flex h-[175px] items-center justify-center rounded-lg bg-light-background dark:bg-2x-dark-foreground"
:class="{ 'is-error': error }"
>
<!--Reset Image-->
<div
v-if="imagePreview"
@click="resetImage"
class="absolute right-0 top-0 z-[9] flex h-7 w-7 -translate-y-3 translate-x-3 cursor-pointer items-center justify-center rounded-md rounded-full dark:bg-4x-dark-foreground bg-white shadow-lg"
>
<x-icon size="14" class="vue-feather dark:text-gray-500" />
</div>
<input
@change="showImagePreview($event)"
ref="file"
type="file"
class="absolute top-0 left-0 right-0 bottom-0 z-10 w-full cursor-pointer opacity-0"
/>
<!--Default image preview-->
<img
v-if="imagePreview"
:src="imagePreview"
ref="image"
class="absolute h-full w-full object-contain py-4 px-12"
/>
<!--Drop image zone-->
<div v-if="!isData" class="text-center">
<image-icon size="34" class="vue-feather text-theme mb-4 inline-block" />
<b class="block text-base font-bold leading-3">
{{ $te('input_image.title') ? $t('input_image.title') : 'Upload Image' }}
</b>
<small class="text-xs text-gray-500">
{{
$te('input_image.supported')
? $t('input_image.supported')
: 'Supported formats are .png, .jpg, .jpeg.'
}}
</small>
</div>
</div>
</template>
<script>
import { XIcon, ImageIcon } from 'vue-feather-icons'
export default {
name: 'ImageInput',
props: ['image', 'error'],
components: {
ImageIcon,
XIcon,
},
data() {
return {
imagePreview: undefined,
}
},
computed: {
isData() {
return !(typeof this.imagePreview === 'undefined' || this.imagePreview === '')
},
},
methods: {
resetImage() {
this.imagePreview = undefined
this.$emit('input', undefined)
},
showImagePreview(event) {
const imgPath = event.target.files[0].name,
extn = imgPath.substring(imgPath.lastIndexOf('.') + 1).toLowerCase()
if (['png', 'jpg', 'jpeg', 'svg'].includes(extn)) {
const file = event.target.files[0],
reader = new FileReader()
reader.onload = () => (this.imagePreview = reader.result)
reader.readAsDataURL(file)
// Update user avatar
this.$emit('input', event.target.files[0])
} else {
alert(this.$t('wrong_image_error'))
}
},
},
created() {
// If has default image then load
if (this.image) this.imagePreview = this.image
},
}
</script>

View File

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

View File

@@ -1,99 +0,0 @@
<template>
<div
class="focus-border-theme input-dark focus-within-border-theme"
:class="{'!border-rose-600':isError, '!py-3.5': !emails.length, '!px-2 !pt-[10px] !pb-0.5': emails.length}"
>
<div @click="$refs.input.focus()" class="flex flex-wrap cursor-text items-center" style="grid-template-columns: auto minmax(0,1fr);">
<div
class="whitespace-nowrap flex items-center rounded-lg bg-theme-100 mr-2 mb-2 py-1 px-2 cursor-pointer w-fit"
@click="removeEmail(email)"
v-for="(email, i) in emails"
:key="i"
>
<small class="text-sm text-theme mr-1">
{{ email }}
</small>
<x-icon class="vue-feather text-theme" size="14" />
</div>
<input
@keydown.delete="removeLastEmail($event)"
@keyup="handleEmail()"
v-model="email"
:size="inputSize"
class="w-auto font-bold text-sm bg-transparent"
:class="{'mb-2': emails.length}"
:placeholder="placeHolder"
autocomplete="new-password"
ref="input"
/>
</div>
</div>
</template>
<script>
import { XIcon } from 'vue-feather-icons'
import { events } from '../../../bus'
export default {
name: 'MultiEmailInput',
components: { XIcon },
props: ['isError', 'label'],
computed: {
placeHolder() {
return !this.emails.length ? this.$t('shared_form.email_placeholder') : ''
},
inputSize() {
return this.email && this.email.length > 14 ? this.email.length : 14
},
},
data() {
return {
emails: [],
email: undefined,
}
},
methods: {
removeEmail(email) {
this.emails = this.emails.filter((item) => item !== email)
// After remove email send new emails list to parent
events.$emit('emailsInputValues', this.emails)
},
removeLastEmail(event) {
// If is input empty and presse backspace remove last email from array
if (event.code === 'Backspace' && this.email === '') this.emails.pop()
},
handleEmail() {
if (! this.email.length)
return;
// Get index of @ and last dot
let lastDot = this.email.lastIndexOf('.')
let at = this.email.indexOf('@')
// Check if is after @ some dot, if email have @ anf if dont have more like one
if (lastDot < at || at === -1 || this.email.match(/@/g).length > 1) return
// First email dont need to be separated by comma or space to be sended
if (this.emails.length === 0) events.$emit('emailsInputValues', [this.email])
// After come or backspace push the single email to array or emails
if (this.email.includes(',') || this.email.includes(' ')) {
let email = this.email.replace(/[","," "]/, '')
this.email = ''
// Push single email to aray of emails
this.emails.push(email)
events.$emit('emailsInputValues', this.emails)
}
},
},
created() {
this.$nextTick(() => {
this.$refs.input.focus()
})
},
}
</script>

View File

@@ -1,113 +0,0 @@
<template>
<div class="search-bar">
<div v-if="!query" class="icon">
<search-icon size="19" />
</div>
<div @click="clearInput" v-if="query" class="icon">
<x-icon class="pointer" size="19"></x-icon>
</div>
<input
v-model="query"
@input="$emit('input', query)"
class="query focus-border-theme"
type="text"
name="searchInput"
:placeholder="$t('search_translations')"
/>
</div>
</template>
<script>
import { SearchIcon, XIcon } from 'vue-feather-icons'
export default {
name: 'SearchInput',
components: {
SearchIcon,
XIcon,
},
data() {
return {
query: undefined,
}
},
methods: {
clearInput() {
this.query = undefined
this.$emit('reset-query')
},
},
}
</script>
<style lang="scss" scoped>
@import '../../../../sass/vuefilemanager/variables';
@import '../../../../sass/vuefilemanager/mixins';
@import '../../../../sass/vuefilemanager/forms';
.search-bar {
position: relative;
width: 100%;
border-radius: 8px;
input {
background: $light_background;
border-radius: 8px;
outline: 0;
padding: 9px 20px 9px 43px;
font-weight: 700;
@include font-size(16);
width: 100%;
height: 50px;
min-width: 175px;
transition: 0.15s all ease;
border: 1px solid transparent;
-webkit-appearance: none;
box-shadow: none;
&::placeholder {
color: $light_text;
@include font-size(14);
font-weight: 700;
}
&:focus {
border-width: 1px;
border-style: solid;
}
&:focus + .icon {
path {
color: inherit;
}
}
}
.icon {
height: 100%;
position: absolute;
top: 0;
left: 0;
padding: 11px 15px;
display: flex;
align-items: center;
circle,
line {
color: $light_text;
}
.pointer {
cursor: pointer;
}
}
}
.dark {
.search-bar {
input {
background: $dark_mode_foreground;
}
}
}
</style>

View File

@@ -1,85 +0,0 @@
<template>
<div class="select-box">
<div
class="box-item active-bg-theme-100 active-border-theme"
:class="{ active: item.value === input }"
@click="getSelectedValue(item)"
v-for="(item, i) in data"
:key="i"
>
<span class="box-value active-text-theme">{{ item.label }}</span>
</div>
</div>
</template>
<script>
export default {
name: 'SelectBoxInput',
props: ['data', 'value'],
data() {
return {
input: undefined,
}
},
methods: {
getSelectedValue(item) {
if (!this.input || this.input !== item.value) this.input = item.value
else this.input = undefined
this.$emit('input', this.input)
},
},
created() {
if (this.value) this.input = this.value
},
}
</script>
<style lang="scss" scoped>
@import '../../../../sass/vuefilemanager/variables';
@import '../../../../sass/vuefilemanager/mixins';
@import '../../../../sass/vuefilemanager/inapp-forms';
@import '../../../../sass/vuefilemanager/forms';
.select-box {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
flex-direction: row;
margin-bottom: 10px;
.box-item {
margin-bottom: 10px;
padding: 12px 4px;
text-align: center;
background: $light_background;
border-radius: 8px;
font-weight: 700;
border: 2px solid $light_background;
cursor: pointer;
flex-direction: column;
flex-basis: 55px;
.box-value {
@include font-size(15);
}
}
}
@media only screen and (max-width: 960px) {
.select-box {
.box-item {
flex-basis: calc(34% - 10px);
}
}
}
.dark {
.select-box {
.box-item {
border-color: $dark_mode_border_color;
background: lighten($dark_mode_foreground, 3%);
}
}
}
</style>

View File

@@ -1,276 +0,0 @@
<template>
<div class="select">
<!--Area-->
<div
class="input-area rounded-lg bg-light-background dark:bg-2x-dark-foreground"
:class="{ 'is-active': isOpen, '!border-rose-600': isError }"
@click="openMenu"
>
<!--If is selected-->
<div class="selected flex w-full items-center" v-if="selected">
<div class="option-icon" v-if="selected.icon">
<user-icon v-if="selected.icon === 'user'" size="14" class="vue-feather text-theme" />
<edit2-icon v-if="selected.icon === 'user-edit'" size="14" class="vue-feather text-theme" />
</div>
<span class="option-value inline-block w-full overflow-hidden text-ellipsis whitespace-nowrap pl-2">
{{ selected.label }}
</span>
</div>
<!--If is empty-->
<div class="not-selected" v-if="!selected">
<span class="option-value placehoder">{{ placeholder }}</span>
</div>
<chevron-down-icon size="19" class="chevron" />
</div>
<!--Options-->
<transition name="slide-in">
<div class="input-options rounded-lg" v-if="isOpen">
<div v-if="options.length > 5" class="select-search">
<input
v-model="query"
ref="search"
type="text"
:placeholder="$te('search_in_list') ? $t('search_in_list') : 'Search in list...'"
class="search-input focus-border-theme rounded-lg"
/>
</div>
<ul class="option-list">
<li class="option-item" @click="selectOption(option)" v-for="(option, i) in optionList" :key="i">
<div class="option-icon" v-if="option.icon">
<user-icon v-if="option.icon === 'user'" size="14" />
<edit2-icon v-if="option.icon === 'user-edit'" size="14" />
</div>
<span class="option-value">
{{ $t(option.label) }}
</span>
</li>
</ul>
</div>
</transition>
</div>
</template>
<script>
import { ChevronDownIcon, Edit2Icon, UserIcon } from 'vue-feather-icons'
import { debounce, omitBy } from 'lodash'
export default {
name: 'SelectInput',
props: ['placeholder', 'options', 'isError', 'default'],
components: {
Edit2Icon,
UserIcon,
ChevronDownIcon,
},
watch: {
query: debounce(function (val) {
this.searchedResults = omitBy(this.options, (string) => {
return !string.label.toLowerCase().includes(val.toLowerCase())
})
}, 200),
},
computed: {
isSearching() {
return this.searchedResults && this.query !== ''
},
optionList() {
return this.isSearching ? this.searchedResults : this.options
},
},
data() {
return {
searchedResults: undefined,
selected: undefined,
isOpen: false,
query: '',
}
},
methods: {
selectOption(option) {
// Emit selected
this.$emit('input', option.value)
this.$emit('change', option.value)
// Get selected
this.selected = option
// Close menu
this.isOpen = false
},
openMenu() {
this.isOpen = !this.isOpen
if (this.$refs.search && this.isOpen) {
this.$nextTick(() => this.$refs.search.focus())
}
},
},
created() {
if (this.default) this.selected = this.options.find((option) => option.value === this.default)
},
}
</script>
<style lang="scss" scoped>
@import '../../../../sass/vuefilemanager/variables';
@import '../../../../sass/vuefilemanager/mixins';
/* TODO: refactor to the tailwind */
.select {
position: relative;
user-select: none;
width: 100%;
}
.select-search {
background: white;
position: sticky;
top: 0;
padding: 13px;
.search-input {
border: 1px solid transparent;
background: $light_background;
@include transition(150ms);
@include font-size(14);
padding: 13px 20px;
appearance: none;
font-weight: 700;
outline: 0;
width: 100%;
}
}
.input-options {
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.12);
background: white;
position: absolute;
overflow: hidden;
top: 65px;
left: 0;
right: 0;
z-index: 9;
max-height: 295px;
overflow-y: auto;
.option-item {
padding: 13px 20px;
display: block;
cursor: pointer;
&:hover {
color: $theme;
background: $light_background;
}
&:last-child {
border-bottom: none;
}
}
}
.input-area {
border-width: 1px;
border-style: solid;
border-color: transparent;
justify-content: space-between;
@include transition(150ms);
align-items: center;
padding: 13px 20px;
display: flex;
outline: 0;
width: 100%;
cursor: pointer;
.chevron {
@include transition(150ms);
}
&.is-active {
//box-shadow: 0 0 7px rgba($theme, 0.3);
.chevron {
@include transform(rotate(180deg));
}
}
&.is-error {
border-color: $danger;
box-shadow: 0 0 7px rgba($danger, 0.3);
}
}
.option-icon {
width: 20px;
display: inline-block;
@include font-size(10);
}
.option-value {
@include font-size(14);
font-weight: 700;
vertical-align: middle;
&.placehoder {
color: rgba($text, 0.5);
}
}
.slide-in-enter-active {
transition: all 150ms ease;
}
.slide-in-enter /* .list-leave-active below version 2.1.8 */ {
opacity: 0;
transform: translateY(-50px);
}
.dark {
.select-search {
background: $dark_mode_foreground;
.search-input {
background: $dark_mode_background;
}
}
.popup-wrapper {
.input-area {
background: lighten($dark_mode_foreground, 3%);
}
}
.input-options {
background: $dark_mode_foreground;
.option-item {
border-bottom: none;
&:hover {
background: lighten($dark_mode_foreground, 5%);
.option-icon {
path,
circle {
color: inherit;
}
}
}
&:last-child {
border-bottom: none;
}
}
}
.option-value {
&.placehoder {
color: $dark_mode_text_secondary;
}
}
}
</style>

View File

@@ -1,142 +0,0 @@
<template>
<div class="setup-box" :class="theme">
<b class="title">{{ title }}</b>
<p class="description">{{ description }}</p>
<slot></slot>
</div>
</template>
<script>
export default {
name: 'SetupBox',
props: ['title', 'description', 'theme'],
}
</script>
<style lang="scss" scoped>
@import '../../../../sass/vuefilemanager/variables';
@import '../../../../sass/vuefilemanager/mixins';
.setup-box {
padding: 20px;
border-radius: 8px;
margin-bottom: 30px;
.title {
@include font-size(21);
margin-bottom: 5px;
display: block;
font-weight: 700;
}
.description {
@include font-size(15);
line-height: 1.5;
margin-bottom: 20px;
}
&.base {
background: $light_background;
}
&.danger {
background: $light_background;
.title {
color: $danger;
}
}
/deep/ input {
&[type='text'],
&[type='number'],
.input-area {
background: white;
}
}
/deep/ .input-area {
background: white;
}
/deep/ .form {
margin-top: 20px;
&.block-form {
max-width: 450px;
.single-line-form {
display: flex;
.submit-button {
margin-left: 20px;
}
}
}
}
}
@media only screen and (max-width: 960px) {
.setup-box {
/deep/ .form {
&.block-form {
max-width: 100%;
}
input {
min-width: initial;
}
}
}
}
@media only screen and (max-width: 690px) {
.setup-box {
padding: 15px;
.title {
@include font-size(17);
margin-bottom: 10px;
}
.description {
@include font-size(14);
}
/deep/ .form.block-form {
.single-line-form {
display: block;
.submit-button {
margin-left: 0;
margin-top: 10px;
}
}
}
}
}
.dark {
.setup-box {
&.base {
background: $dark_mode_foreground;
}
&.danger {
background: $dark_mode_foreground;
}
/deep/ input {
&[type='text'],
&[type='number'],
.input-area {
background: $dark_mode_background;
}
}
/deep/ .input-area {
background: $dark_mode_background;
}
}
}
</style>

View File

@@ -1,103 +0,0 @@
<template>
<div>
<div class="switch-content">
<label class="input-label" v-if="label"> {{ label }}: </label>
<small class="input-info" v-if="info">
{{ info }}
</small>
</div>
<div class="switch-content text-right">
<div class="switch" :class="{ active: state }" @click="changeState">
<div class="switch-button"></div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'SwitchInput',
props: ['label', 'name', 'state', 'info', 'input', 'isDisabled'],
data() {
return {
isSwitched: undefined,
}
},
methods: {
changeState() {
if (this.isDisabled) return
this.isSwitched = !this.isSwitched
this.$emit('input', this.isSwitched)
},
},
mounted() {
this.isSwitched = this.state
},
}
</script>
<style lang="scss" scoped>
@import '../../../../sass/vuefilemanager/variables';
@import '../../../../sass/vuefilemanager/mixins';
.input-wrapper {
display: flex;
width: 100%;
.input-label {
color: $text;
}
.switch-content {
width: 100%;
&:last-child {
width: 80px;
}
}
}
.switch {
width: 50px;
height: 28px;
border-radius: 50px;
display: block;
background: #f1f1f5;
position: relative;
@include transition;
.switch-button {
@include transition;
width: 22px;
height: 22px;
border-radius: 50px;
display: block;
background: white;
position: absolute;
top: 3px;
left: 3px;
box-shadow: 0 2px 4px rgba(37, 38, 94, 0.1);
cursor: pointer;
}
&.active {
.switch-button {
left: 25px;
}
}
}
.dark {
.switch {
background: $dark_mode_foreground;
}
.popup-wrapper {
.switch {
background: lighten($dark_mode_foreground, 3%);
}
}
}
</style>

View File

@@ -1,25 +0,0 @@
<template>
<div class="page-tab">
<div id="loader" v-show="isLoading">
<Spinner></Spinner>
</div>
<slot v-show="!isLoading"></slot>
</div>
</template>
<script>
import Spinner from '../../FilesView/Spinner'
export default {
name: 'PageTab',
props: ['isLoading'],
components: {
Spinner,
},
}
</script>
<style lang="scss" scoped>
@import '../../../../sass/vuefilemanager/variables';
@import '../../../../sass/vuefilemanager/mixins';
</style>

View File

@@ -1,20 +0,0 @@
<template>
<div class="page-tab-group">
<slot></slot>
</div>
</template>
<script>
export default {
name: 'PageTabGroup',
}
</script>
<style lang="scss" scoped>
@import '../../../../sass/vuefilemanager/variables';
@import '../../../../sass/vuefilemanager/mixins';
.page-tab-group {
margin-bottom: 65px;
}
</style>

View File

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

View File

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

View File

@@ -1,214 +0,0 @@
<template>
<MenuMobile name="user-navigation">
<!--User avatar-->
<UserHeadline v-if="!clickedSubmenu" class="p-5 pb-3" />
<!--User estimate-->
<div
v-if="config.subscriptionType === 'metered' && user && user.data.meta.usages && !clickedSubmenu"
class="block px-5 pt-2"
>
<div class="rounded-lg bg-light-background px-3 py-1.5 dark:bg-4x-dark-foreground">
<span class="text-sm font-semibold">
{{ $t('current_estimated_usage') }}
</span>
<span class="text-theme text-sm font-bold">
{{ user.data.meta.usages.costEstimate }}
</span>
</div>
</div>
<!--Go back button-->
<div v-if="clickedSubmenu" @click.stop="showSubmenu(undefined)" class="flex items-center p-5 pb-4">
<chevron-left-icon size="19" class="vue-feather text-theme mr-2 -ml-1" />
<span class="text-theme text-sm font-bold">
{{ backTitle }}
</span>
</div>
<!--Menu links-->
<MenuMobileGroup>
<!--Main navigation-->
<OptionGroup v-if="!clickedSubmenu">
<Option
@click.native="goToFiles"
:title="$t('menu.files')"
icon="hard-drive"
:is-hover-disabled="true"
/>
<Option
@click.native.stop="showSubmenu('settings')"
:title="$t('settings')"
icon="user"
arrow="right"
:is-hover-disabled="true"
/>
<Option
v-if="isAdmin"
@click.native.stop="showSubmenu('admin')"
:title="$t('administration')"
icon="settings"
arrow="right"
:is-hover-disabled="true"
/>
</OptionGroup>
<OptionGroup v-if="!clickedSubmenu">
<Option @click.native="logOut" :title="$t('logout')" icon="power" :is-hover-disabled="true" />
</OptionGroup>
<!--Submenu: User settings-->
<OptionGroup v-if="clickedSubmenu === 'settings'">
<Option
@click.native="goToRoute('Profile')"
:title="$t('menu.profile')"
icon="user"
:is-hover-disabled="true"
/>
<Option
@click.native="goToRoute('Password')"
:title="$t('menu.password')"
icon="lock"
:is-hover-disabled="true"
/>
<Option
@click.native="goToRoute('Storage')"
:title="$t('storage')"
icon="hard-drive"
:is-hover-disabled="true"
/>
<Option
@click.native="goToRoute('Billing')"
v-if="config.subscriptionType !== 'none'"
:title="$t('billing')"
icon="cloud"
:is-hover-disabled="true"
/>
</OptionGroup>
<!--Submenu: Admin settings-->
<OptionGroup v-if="clickedSubmenu === 'admin'">
<Option
@click.native="goToRoute('Dashboard')"
:title="$t('dashboard')"
icon="box"
:is-hover-disabled="true"
/>
<Option
@click.native="goToRoute('Users')"
:title="$t('users')"
icon="users"
:is-hover-disabled="true"
/>
<Option
@click.native="goToRoute('AppOthers')"
:title="$t('settings')"
icon="settings"
:is-hover-disabled="true"
/>
</OptionGroup>
<!--Submenu: Content settings-->
<OptionGroup v-if="clickedSubmenu === 'admin'">
<Option
@click.native="goToRoute('Pages')"
:title="$t('pages')"
icon="monitor"
:is-hover-disabled="true"
/>
<Option
@click.native="goToRoute('Language')"
:title="$t('languages')"
icon="globe"
:is-hover-disabled="true"
/>
</OptionGroup>
<!--Submenu: Billing settings-->
<OptionGroup v-if="clickedSubmenu === 'admin' && config.subscriptionType !== 'none'">
<Option
@click.native="goToRoute('AppPayments')"
:title="$t('payments')"
icon="credit-card"
:is-hover-disabled="true"
/>
<Option
@click.native="goToRoute('Subscriptions')"
v-if="config.subscriptionType === 'fixed'"
:title="$t('subscriptions')"
icon="credit-card"
:is-hover-disabled="true"
/>
<Option
@click.native="goToRoute('Plans')"
:title="$t('plans')"
icon="database"
:is-hover-disabled="true"
/>
<Option
@click.native="goToRoute('Invoices')"
:title="$t('transactions')"
icon="file-text"
:is-hover-disabled="true"
/>
</OptionGroup>
</MenuMobileGroup>
</MenuMobile>
</template>
<script>
import MenuMobileGroup from '../Mobile/MenuMobileGroup'
import OptionGroup from '../FilesView/OptionGroup'
import UserHeadline from '../Sidebar/UserHeadline'
import MenuMobile from '../Mobile/MenuMobile'
import Option from '../FilesView/Option'
import { ChevronLeftIcon } from 'vue-feather-icons'
import { mapGetters } from 'vuex'
export default {
name: 'MobileNavigation',
components: {
ChevronLeftIcon,
MenuMobileGroup,
UserHeadline,
OptionGroup,
MenuMobile,
Option,
},
computed: {
...mapGetters(['config', 'user']),
isAdmin() {
return this.user && this.user.data.attributes.role === 'admin'
},
backTitle() {
let location = {
settings: this.$t('settings'),
admin: this.$t('administration'),
}
return this.$t('go_back_from_x', {location: location[this.clickedSubmenu]})
},
},
data() {
return {
clickedSubmenu: undefined,
}
},
methods: {
goToRoute(route) {
this.$router.push({ name: route })
this.clickedSubmenu = undefined
},
showSubmenu(name) {
this.clickedSubmenu = name
},
goToFiles() {
if (this.$route.name !== 'Files') this.$router.push({ name: 'Files' })
this.$store.dispatch('getFolder')
},
logOut() {
this.$store.dispatch('logOut')
},
},
}
</script>

View File

@@ -1,160 +0,0 @@
<template>
<PopupWrapper name="move">
<!--Title-->
<PopupHeader :title="$t('popup_move_item.title')" icon="move" />
<!--Content-->
<PopupContent v-if="pickedItem" class="h-full pb-6 lg:max-h-96 sm:overflow-y-auto md:pb-0">
<!--Show Spinner when loading folders-->
<Spinner v-if="isLoadingTree" />
<!--Folder tree-->
<div v-if="!isLoadingTree && navigation">
<ThumbnailItem v-if="clipboard.length === 1 || isSelectedItem" class="mb-5" :item="pickedItem" />
<TitlePreview
class="mb-4"
icon="check-square"
:title="$t('selected_multiple')"
:subtitle="clipboard.length + ' ' + $tc('items', clipboard.length)"
v-if="clipboard.length > 1 && !isSelectedItem"
/>
<TreeMenu
class="-mx-4"
:disabled-by-id="pickedItem"
:depth="1"
:nodes="items"
v-for="items in navigation"
:key="items.id"
/>
</div>
</PopupContent>
<!--Actions-->
<PopupActions>
<ButtonBase class="w-full" @click.native="$closePopup()" button-style="secondary"
>{{ $t('cancel') }}
</ButtonBase>
<ButtonBase class="w-full" @click.native="moveItem" :button-style="selectedFolder ? 'theme' : 'secondary'"
>{{ $t('popup_move_item.submit') }}
</ButtonBase>
</PopupActions>
</PopupWrapper>
</template>
<script>
import PopupWrapper from './Popup/PopupWrapper'
import PopupActions from './Popup/PopupActions'
import TitlePreview from '../FilesView/TitlePreview'
import PopupContent from './Popup/PopupContent'
import PopupHeader from './Popup/PopupHeader'
import ThumbnailItem from './ThumbnailItem'
import ButtonBase from '../FilesView/ButtonBase'
import Spinner from '../FilesView/Spinner'
import TreeMenu from './TreeMenu'
import { isArray } from 'lodash'
import { mapGetters } from 'vuex'
import { events } from '../../bus'
export default {
name: 'MoveItemPopup',
components: {
ThumbnailItem,
TitlePreview,
PopupWrapper,
PopupActions,
PopupContent,
PopupHeader,
ButtonBase,
TreeMenu,
Spinner,
},
computed: {
...mapGetters(['navigation', 'clipboard']),
},
data() {
return {
selectedFolder: undefined,
pickedItem: undefined,
isLoadingTree: true,
isSelectedItem: false,
}
},
methods: {
moveItem() {
// Prevent empty submit
if (!this.selectedFolder) return
// Prevent to move items to the same parent
if (
isArray(this.selectedFolder) &&
this.clipboard.find((item) => item.parent_id === this.selectedFolder.id)
)
return
// Move item
this.$store.dispatch('moveItem', {
to_item: this.selectedFolder,
item: this.isSelectedItem ? this.pickedItem : undefined,
})
// Close popup
events.$emit('popup:close')
// If is mobile, close the selecting mod after done the move action
if (this.$isMobile())
this.$store.commit('DISABLE_MULTISELECT_MODE')
},
},
mounted() {
events.$on('pick-folder', (folder) => {
if (folder.id === this.pickedItem.data.id) {
this.selectedFolder = undefined
} else if (!folder.id && folder.location === 'base') {
this.selectedFolder = 'base'
} else {
this.selectedFolder = folder
}
})
// Show Move item popup
events.$on('popup:open', (args) => {
if (args.name !== 'move') return
// Show tree spinner
this.isLoadingTree = true
// Get folder tree and hide spinner
if (this.$isThisRoute(this.$route, ['SharedWithMe'])) {
this.$store.dispatch('getTeamFolderTree').then(() => {
this.isLoadingTree = false
})
} else {
this.$store.dispatch('getFolderTree').then(() => {
this.isLoadingTree = false
})
}
// Store picked item
if (!this.clipboard.includes(args.item[0])) {
this.pickedItem = args.item[0]
this.isSelectedItem = true
}
if (this.clipboard.includes(args.item[0])) {
this.pickedItem = this.clipboard[0]
this.isSelectedItem = false
}
})
// Close popup
events.$on('popup:close', () => {
// Clear selected folder
setTimeout(() => {
this.selectedFolder = undefined
}, 150)
})
},
}
</script>

View File

@@ -1,45 +0,0 @@
<template>
<transition appear name="fade">
<div
class="relative w-full overflow-hidden rounded-xl bg-opacity-80 p-4 shadow-lg backdrop-blur-2xl"
:class="{
'bg-red-50 dark:bg-2x-dark-foreground': item.type === 'danger',
'bg-green-50 dark:bg-2x-dark-foreground': item.type === 'success',
}"
>
<!--Content-->
<div class="flex items-center justify-between">
<div class="flex items-start">
<check-icon v-if="item.type === 'success'" size="22" class="vue-feather dark:text-green-600 text-green-600" />
<x-icon v-if="item.type === 'danger'" size="22" class="vue-feather dark:text-red-600 text-red-600" />
<p
class="px-4 font-bold"
:class="{
'text-green-600': item.type === 'success',
'text-red-600': item.type === 'danger',
}"
>
{{ item.message }}
</p>
</div>
</div>
</div>
</transition>
</template>
<script>
import { CheckIcon, XIcon } from 'vue-feather-icons'
export default {
name: 'Toaster',
components: {
CheckIcon,
XIcon,
},
props: [
'barColor',
'item',
],
}
</script>

View File

@@ -1,79 +0,0 @@
<template>
<div
v-if="toasters.length || notifications.length"
class="fixed bottom-4 right-4 left-4 z-50 sm:w-[360px] sm:left-auto lg:bottom-8 lg:right-8"
>
<ToasterWrapper
v-for="notification in notifications"
:key="notification.data.id"
class="mt-4 overflow-hidden rounded-xl dark:bg-2x-dark-foreground bg-white/80 backdrop-blur-2xl shadow-xl"
bar-color="bg-theme"
>
<Notification :notification="notification" class="z-10 !mb-0 !px-4 !pt-4 !pb-5" />
</ToasterWrapper>
<ToasterWrapper
v-for="(toaster, i) in toasters"
:key="i"
class="mt-4 overflow-hidden rounded-xl shadow-xl"
:bar-color="getToasterColor(toaster)"
>
<Toaster :item="toaster" />
</ToasterWrapper>
</div>
</template>
<script>
import Notification from '../../Notifications/Notification'
import ToasterWrapper from './ToasterWrapper'
import {events} from '../../../bus'
import Toaster from './Toaster'
export default {
components: {
ToasterWrapper,
Notification,
Toaster,
},
data() {
return {
notifications: [],
toasters: [],
}
},
methods: {
getToasterColor(toaster) {
return {
danger: 'bg-red-400',
success: 'bg-green-400',
}[toaster.type]
}
},
created() {
events.$on('toaster', (toaster) => this.toasters.push(toaster))
events.$on('notification', (notification) => this.notifications.push(notification))
/*events.$emit('notification', {
data: {
type: 'file-request',
id: 'df954d23-f9d4-4677-85c8-abfd48aaa090',
attributes: {
action: {
type: 'route',
params: {
route: 'Files',
button: 'Show Files',
id: 'ae37b1d8-c147-489a-83ab-2a3c7cb86263',
},
},
created_at: '',
description: "Your file request for 'Multi Level Folder' folder was filled successfully.",
read_at: '',
title: 'File Request Filled',
category: 'file-request',
},
},
})*/
},
}
</script>

View File

@@ -1,64 +0,0 @@
<template>
<transition v-if="isActive" appear name="fade">
<div class="relative">
<div @click="isActive = false" class="absolute z-[20] right-0 top-0 cursor-pointer p-2">
<x-icon size="16" class="vue-feather text-black opacity-10 dark:text-white" />
</div>
<slot />
<!--Progress bar-->
<div class="absolute bottom-0 left-0 right-0 z-20">
<span class="bar-animation block h-1 w-0" :class="barColor"></span>
</div>
</div>
</transition>
</template>
<script>
import { XIcon } from 'vue-feather-icons'
export default {
name: 'Toast',
props: [
'barColor'
],
components: {
XIcon,
},
data() {
return {
isActive: 1,
}
},
created() {
setTimeout(() => (this.isActive = 0), 6000)
},
}
</script>
<style lang="scss" scoped>
.bar-animation {
animation: progressbar 6s linear both;
}
@keyframes progressbar {
0% {
width: 0;
}
100% {
width: 100%;
}
}
.fade-enter-active,
.fade-leave-active {
transition: 0.3s ease;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
transform: translateX(100%);
}
</style>

View File

@@ -1,94 +0,0 @@
<template>
<PopupWrapper name="notifications-mobile">
<!--Title-->
<PopupHeader :title="$t('notifications')" icon="bell" />
<!--Content-->
<PopupContent v-if="readNotifications && unreadNotifications">
<MobileActionButton
v-if="readNotifications.length || unreadNotifications.length"
@click.native="$store.dispatch('deleteAllNotifications')"
icon="check-square"
class="mb-2 dark:!bg-4x-dark-foreground"
>
{{ $t('clear_all') }}
</MobileActionButton>
<p v-if="!readNotifications.length && !unreadNotifications.length" class="text-sm text-gray-500">
{{ $t("not_any_notifications") }}
</p>
<b
v-if="unreadNotifications.length"
class="dark-text-theme mt-1.5 mb-2.5 block px-2.5 text-xs text-gray-400"
>
{{ $t('unread') }}
</b>
<Notification
:notification="notification"
v-for="notification in unreadNotifications"
:key="notification.id"
/>
<b v-if="readNotifications.length" class="dark-text-theme mt-2.5 mb-2.5 block px-2.5 text-xs text-gray-400">
{{ $t('read') }}
</b>
<Notification
:notification="notification"
v-for="notification in readNotifications"
:key="notification.id"
/>
</PopupContent>
<!--Actions-->
<PopupActions>
<ButtonBase class="w-full" @click.native="$closePopup()" button-style="secondary">
{{ $t('close') }}
</ButtonBase>
</PopupActions>
</PopupWrapper>
</template>
<script>
import MobileActionButton from '../FilesView/MobileActionButton'
import Notification from '../Notifications/Notification'
import ButtonBase from '../FilesView/ButtonBase'
import PopupWrapper from './Popup/PopupWrapper'
import PopupActions from './Popup/PopupActions'
import PopupContent from './Popup/PopupContent'
import PopupHeader from './Popup/PopupHeader'
import vClickOutside from 'v-click-outside'
import { mapGetters } from 'vuex'
export default {
name: 'NotificationsPopup',
components: {
MobileActionButton,
Notification,
PopupWrapper,
PopupActions,
PopupContent,
PopupHeader,
ButtonBase,
},
directives: {
clickOutside: vClickOutside.directive,
},
computed: {
...mapGetters(['user', 'config']),
readNotifications() {
return this.user?.data.relationships.readNotifications.data
},
unreadNotifications() {
return this.user?.data.relationships.unreadNotifications.data
},
},
methods: {
clickOutside() {
if (this.isVisibleNotificationCenter) this.$store.commit('CLOSE_NOTIFICATION_CENTER')
},
},
}
</script>

View File

@@ -1,83 +0,0 @@
<template>
<div class="page-header">
<div class="go-back" v-if="canBack" @click="$router.back()">
<chevron-left-icon size="17" />
</div>
<div class="content">
<h1 class="title">{{ title }}</h1>
</div>
</div>
</template>
<script>
import { ChevronLeftIcon } from 'vue-feather-icons'
export default {
name: 'PageHeader',
props: ['title', 'canBack'],
components: {
ChevronLeftIcon,
},
}
</script>
<style lang="scss" scoped>
@import '../../../sass/vuefilemanager/variables';
@import '../../../sass/vuefilemanager/mixins';
.page-header {
display: flex;
align-items: center;
background: white;
z-index: 9;
width: 100%;
position: sticky;
top: 0;
padding-top: 20px;
padding-bottom: 20px;
.title {
@include font-size(18);
font-weight: 700;
color: $text;
}
.go-back {
margin-right: 10px;
cursor: pointer;
svg {
vertical-align: middle;
margin-top: -4px;
}
}
}
@media only screen and (max-width: 960px) {
.page-header {
.title {
@include font-size(18);
}
}
}
@media only screen and (max-width: 690px) {
.page-header {
display: none;
}
}
.dark {
.page-header {
background: $dark_mode_background;
.title {
color: $dark_mode_text_primary;
}
.icon path {
fill: $theme;
}
}
}
</style>

View File

@@ -1,19 +0,0 @@
<template>
<div class="flex items-center justify-between border-b border-dashed border-light py-4 dark:border-opacity-5">
<div>
<img :src="$getPaymentLogo(driver)" :alt="driver" class="h-6" />
<small class="block pt-2 text-xs leading-4 dark:text-gray-500 text-gray-500">
{{ description }}
</small>
</div>
<div v-if="$slots.default" class="bg-theme-200 relative inline-block rounded-lg px-3 py-1">
<slot></slot>
</div>
</div>
</template>
<script>
export default {
name: 'PaymentMethod',
props: ['description', 'driver'],
}
</script>

View File

@@ -1,220 +0,0 @@
<template>
<div class="plans-wrapper" v-if="plans">
<article class="plan" v-for="(plan, i) in plans" :key="i">
<div class="plan-wrapper">
<header class="plan-header">
<div class="icon">
<hard-drive-icon class="text-theme" size="26" />
</div>
<h1 class="title">{{ plan.data.attributes.name }}</h1>
<h2 class="description">
{{ plan.data.attributes.description }}
</h2>
</header>
<section class="plan-features">
<b class="storage-size">{{ plan.data.attributes.capacity_formatted }}</b>
<span class="storage-description">{{ $t('page_pricing_tables.storage_capacity') }}</span>
</section>
<footer class="plan-footer">
<b class="price text-theme">
{{ plan.data.attributes.price }}/{{ $t('mo.') }}
<small v-if="plan.data.attributes.tax_rates.length > 0" class="vat-disclaimer">{{
$t('page_pricing_tables.vat_excluded')
}}</small>
</b>
<ButtonBase
@click.native="selectPlan(plan)"
type="submit"
button-style="secondary"
class="sign-in-button"
>
{{ $t('get_it') }}
</ButtonBase>
</footer>
</div>
</article>
</div>
</template>
<script>
import ButtonBase from '../FilesView/ButtonBase'
import { HardDriveIcon } from 'vue-feather-icons'
import { mapGetters } from 'vuex'
import axios from 'axios'
export default {
name: 'PlanPricingTables',
components: {
HardDriveIcon,
ButtonBase,
},
props: ['customRoute'],
data() {
return {
plans: undefined,
}
},
computed: {
...mapGetters(['user']),
},
methods: {
selectPlan(plan) {
this.$emit('selected-plan', plan)
let route = this.customRoute ? this.customRoute : 'UpgradeBilling'
this.$router.push({ name: route })
},
},
created() {
axios.get('/api/pricing').then((response) => {
this.plans = response.data.filter((plan) => {
return plan.data.attributes.capacity > this.user.data.attributes.max_storage_amount
})
this.$emit('load', false)
})
},
}
</script>
<style lang="scss" scoped>
@import '../../../sass/vuefilemanager/variables';
@import '../../../sass/vuefilemanager/mixins';
.plan {
text-align: center;
flex: 0 0 33%;
padding: 0 25px;
margin-bottom: 45px;
.plan-wrapper {
box-shadow: 0 7px 20px 5px hsla(220, 36%, 16%, 0.03);
padding: 25px;
border-radius: 8px;
@include transition;
&:hover {
@include transform(translateY(-20px) scale(1.05));
box-shadow: 0 15px 25px 5px hsla(220, 36%, 16%, 0.08);
}
}
.plan-header {
.icon {
path,
line,
polyline,
rect,
circle {
color: inherit;
}
}
.title {
@include font-size(22);
font-weight: 800;
}
.description {
@include font-size(14);
font-weight: 600;
}
}
.plan-features {
margin: 65px 0;
.storage-size {
@include font-size(48);
font-weight: 900;
line-height: 1.1;
}
.storage-description {
display: block;
@include font-size(15);
font-weight: 800;
}
}
.plan-footer {
.sign-in-button {
width: 100%;
text-align: center;
}
.price {
@include font-size(18);
display: block;
margin-bottom: 20px;
.vat-disclaimer {
@include font-size(11);
color: $text;
display: block;
font-weight: 300;
opacity: 0.45;
margin-top: 5px;
}
}
}
}
.plans-wrapper {
display: flex;
flex-wrap: wrap;
margin: 0 -25px;
justify-content: center;
}
@media only screen and (max-width: 960px) {
.plans-wrapper {
display: block;
margin: 0;
}
}
.dark {
.plan {
.plan-wrapper {
background: $dark_mode_foreground;
}
.plan-header {
.title {
color: $dark_mode_text_primary;
}
.description {
color: $dark_mode_text_secondary;
}
}
.plan-features {
.storage-size {
color: $dark_mode_text_primary;
}
.storage-description {
color: $dark_mode_text_primary;
}
}
.plan-footer {
.sign-in-button {
background: rgba($theme, 0.1);
/deep/ .content {
color: $theme;
}
}
.price {
.vat-disclaimer {
color: $dark_mode_text_primary;
}
}
}
}
}
</style>

View File

@@ -1,77 +0,0 @@
<template>
<PopupWrapper>
<div class="flex h-full -translate-y-7 transform items-center justify-center px-8 text-center md:translate-y-0">
<div>
<img src="https://twemoji.maxcdn.com/v/13.1.0/svg/1f914.svg" alt="" class="mx-auto mb-4 w-20 md:mt-6 min-h-[80px]" />
<h1 v-if="title" class="mb-2 text-2xl font-bold">
{{ title }}
</h1>
<p v-if="message" class="mb-4 text-sm">
{{ message }}
</p>
</div>
</div>
<PopupActions>
<ButtonBase @click.native="closePopup" button-style="secondary" class="w-full"
>{{ $t('cancel') }}
</ButtonBase>
<ButtonBase @click.native="confirm" :button-style="buttonColor" class="w-full"
>{{ $t('yes_iam_sure') }}
</ButtonBase>
</PopupActions>
</PopupWrapper>
</template>
<script>
import PopupWrapper from './PopupWrapper'
import PopupActions from './PopupActions'
import ButtonBase from '../../FilesView/ButtonBase'
import { events } from '../../../bus'
export default {
name: 'ConfirmPopup',
components: {
PopupWrapper,
PopupActions,
ButtonBase,
},
data() {
return {
confirmationData: [],
message: undefined,
title: undefined,
buttonColor: undefined,
}
},
methods: {
closePopup() {
events.$emit('popup:close')
},
confirm() {
// Close popup
events.$emit('popup:close')
// Confirmation popup
events.$emit('action:confirmed', this.confirmationData)
// Clear confirmation data
this.confirmationData = []
},
},
mounted() {
// Show confirm
events.$on('confirm:open', (args) => {
this.title = args.title
this.message = args.message
this.confirmationData = args.action
this.buttonColor = 'danger'
if (args.buttonColor) {
this.buttonColor = args.buttonColor
}
})
},
}
</script>

View File

@@ -1,11 +0,0 @@
<template>
<div class="absolute bottom-0 left-0 right-0 flex items-center space-x-4 px-6 py-4 pb-6 md:relative">
<slot />
</div>
</template>
<script>
export default {
name: 'PopupActions',
}
</script>

View File

@@ -1,15 +0,0 @@
<template>
<div
:class="type"
class="absolute top-16 bottom-24 left-0 right-0 h-auto overflow-auto px-6 md:relative md:top-0 md:bottom-0"
>
<slot />
</div>
</template>
<script>
export default {
name: 'PopupContent',
props: ['type'],
}
</script>

View File

@@ -1,64 +0,0 @@
<template>
<div class="flex items-center justify-between px-6 pt-6 pb-6">
<div class="flex items-center">
<div class="mr-3">
<upload-cloud-icon v-if="icon === 'upload'" size="18" class="vue-feather text-theme" />
<corner-down-right-icon v-if="icon === 'move'" size="18" class="vue-feather text-theme" />
<share-icon v-if="icon === 'share'" size="18" class="vue-feather text-theme" />
<edit2-icon v-if="icon === 'edit'" size="18" class="vue-feather text-theme" />
<key-icon v-if="icon === 'key'" size="18" class="vue-feather text-theme" />
<users-icon v-if="icon === 'users'" size="18" class="vue-feather text-theme" />
<user-plus-icon v-if="icon === 'user-plus'" size="18" class="vue-feather text-theme" />
<credit-card-icon v-if="icon === 'credit-card'" size="18" class="vue-feather text-theme" />
<bell-icon v-if="icon === 'bell'" size="18" class="vue-feather text-theme" />
</div>
<b class="text-base font-bold">
{{ title }}
</b>
</div>
<div @click="closePopup" class="-m-3 cursor-pointer p-3">
<x-icon size="14" class="hover-text-theme vue-feather" />
</div>
</div>
</template>
<script>
import {
BellIcon,
UploadCloudIcon,
CreditCardIcon,
KeyIcon,
UserPlusIcon,
CornerDownRightIcon,
LinkIcon,
XIcon,
Edit2Icon,
ShareIcon,
UsersIcon,
} from 'vue-feather-icons'
import { events } from '../../../bus'
export default {
name: 'PopupHeader',
props: ['title', 'icon'],
components: {
BellIcon,
UploadCloudIcon,
CornerDownRightIcon,
CreditCardIcon,
UserPlusIcon,
UsersIcon,
ShareIcon,
Edit2Icon,
LinkIcon,
KeyIcon,
XIcon,
},
methods: {
closePopup() {
events.$emit('popup:close')
},
},
}
</script>

View File

@@ -1,89 +0,0 @@
<template>
<transition name="popup">
<div
v-if="isVisibleWrapper"
@click.self="closePopup"
class="popup fixed top-0 left-0 right-0 bottom-0 z-50 grid h-full overflow-y-auto p-10 lg:absolute"
>
<div
class="fixed top-0 bottom-0 left-0 right-0 z-10 m-auto w-full bg-white shadow-xl dark:bg-dark-foreground md:relative md:w-[490px] md:rounded-xl"
>
<slot />
</div>
</div>
</transition>
</template>
<script>
import { events } from '../../../bus'
export default {
name: 'PopupWrapper',
props: ['name'],
data() {
return {
isVisibleWrapper: false,
}
},
methods: {
closePopup() {
events.$emit('popup:close')
},
},
created() {
// Open called popup
events.$on('popup:open', ({ name }) => {
if (this.name === name) this.isVisibleWrapper = true
if (this.name !== name) this.isVisibleWrapper = false
})
// Open called popup
events.$on('confirm:open', ({ name }) => {
if (this.name === name) this.isVisibleWrapper = true
})
// Close popup
events.$on('popup:close', () => (this.isVisibleWrapper = false))
},
}
</script>
<style lang="scss" scoped>
.popup-leave-active {
animation: popup-slide-in 0.15s ease reverse;
}
@media only screen and (min-width: 960px) {
.popup-enter-active {
animation: popup-slide-in 0.25s 0.1s ease both;
}
@keyframes popup-slide-in {
0% {
opacity: 0;
transform: translateY(100px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
}
@media only screen and (max-width: 960px) {
.popup-enter-active {
animation: popup-slide-in 0.35s 0.15s ease both;
}
@keyframes popup-slide-in {
0% {
transform: translateY(100%);
}
100% {
transform: translateY(0);
}
}
}
</style>

View File

@@ -1,175 +0,0 @@
<template>
<PopupWrapper name="rename-item">
<!--Title-->
<PopupHeader :title="$t('popup_rename.title', { item: itemTypeTitle })" icon="edit" />
<!--Content-->
<PopupContent>
<!--Item Thumbnail-->
<ThumbnailItem class="mb-5" :item="pickedItem" :setFolderIcon="{ emoji: emoji, color: null }" />
<!--Form to set sharing-->
<ValidationObserver @submit.prevent="changeName" ref="renameForm" v-slot="{ invalid }" tag="form">
<!--Update item name-->
<ValidationProvider tag="div" mode="passive" name="Name" rules="required" v-slot="{ errors }">
<AppInputText
:title="$t('popup_rename.label')"
:error="errors[0]"
:is-last="pickedItem.data.type !== 'folder'"
>
<div class="relative flex items-center">
<input
v-model="pickedItem.data.attributes.name"
:class="{ '!border-rose-600': errors[0] }"
ref="input"
type="text"
class="!pr-10 focus-border-theme input-dark"
:placeholder="$t('popup_rename.placeholder')"
/>
<div @click="pickedItem.data.attributes.name = ''" class="absolute right-0 p-4 cursor-pointer">
<x-icon class="hover-text-theme" size="14" />
</div>
</div>
</AppInputText>
</ValidationProvider>
<!--Emoji-->
<AppInputSwitch
v-if="pickedItem.data.type === 'folder'"
:title="$t('emoji_as_an_icon')"
:description="$t('replace_icon_with_emoji')"
:is-last="!isEmoji"
>
<SwitchInput v-model="isEmoji" :state="isEmoji" />
</AppInputSwitch>
<!--Set emoji-->
<EmojiPicker
v-if="pickedItem.data.type === 'folder' && isEmoji"
v-model="emoji"
:default-emoji="emoji"
/>
</ValidationObserver>
</PopupContent>
<!--Actions-->
<PopupActions>
<ButtonBase class="w-full" @click.native="$closePopup()" button-style="secondary">
{{ $t('cancel') }}
</ButtonBase>
<ButtonBase class="w-full" @click.native="changeName" button-style="theme">
{{ $t('popup_share_edit.save') }}
</ButtonBase>
</PopupActions>
</PopupWrapper>
</template>
<script>
import { ValidationProvider, ValidationObserver } from 'vee-validate/dist/vee-validate.full'
import PopupWrapper from './Popup/PopupWrapper'
import PopupActions from './Popup/PopupActions'
import PopupContent from './Popup/PopupContent'
import PopupHeader from './Popup/PopupHeader'
import ThumbnailItem from './ThumbnailItem'
import ButtonBase from '../FilesView/ButtonBase'
import AppInputSwitch from '../Admin/AppInputSwitch'
import AppInputText from '../Admin/AppInputText'
import { required } from 'vee-validate/dist/rules'
import SwitchInput from './Forms/SwitchInput'
import EmojiPicker from './EmojiPicker'
import { XIcon } from 'vue-feather-icons'
import { events } from '../../bus'
export default {
name: 'RenameItemPopup',
components: {
ValidationProvider,
ValidationObserver,
EmojiPicker,
AppInputSwitch,
SwitchInput,
AppInputText,
ThumbnailItem,
PopupWrapper,
PopupActions,
PopupContent,
PopupHeader,
ButtonBase,
required,
XIcon,
},
computed: {
itemTypeTitle() {
return this.pickedItem && this.pickedItem.data.type === 'folder'
? this.$t('folder')
: this.$t('file')
},
},
watch: {
isEmoji(val) {
if (!val) {
events.$emit('setFolderIcon', { emoji: undefined })
this.emoji = undefined
} else {
events.$emit('setFolderIcon', { emoji: this.emoji })
}
},
emoji(val) {
events.$emit('setFolderIcon', {
emoji: val,
})
},
},
data() {
return {
pickedItem: undefined,
isEmoji: false,
emoji: undefined,
}
},
methods: {
changeName() {
if (this.pickedItem.data.attributes.name && this.pickedItem.data.attributes.name !== '') {
let item = {
id: this.pickedItem.data.id,
type: this.pickedItem.data.type,
name: this.pickedItem.data.attributes.name,
}
item['emoji'] = this.emoji || null
if (!this.isEmoji) item['emoji'] = null
// Rename item request
this.$store.dispatch('renameItem', item)
// Rename item in view
events.$emit('change:name', item)
this.$closePopup()
}
},
},
mounted() {
// Show popup
events.$on('popup:open', (args) => {
if (args.name !== 'rename-item') return
this.isEmoji = false
if (!this.$isMobile()) {
this.$nextTick(() => this.$refs.input.focus())
}
// Set default emoji if exist
if (args.item.data.attributes.emoji) {
this.isEmoji = true
this.emoji = args.item.data.attributes.emoji
}
// Store picked item
this.pickedItem = args.item
})
},
}
</script>

View File

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

View File

@@ -1,304 +0,0 @@
<template>
<PopupWrapper name="share-create">
<!--Title-->
<PopupHeader :title="$t('popup_share_create.title', { item: itemTypeTitle })" icon="share" />
<!--Content-->
<PopupContent class="!overflow-initial">
<!--Item Thumbnail-->
<ThumbnailItem class="mb-5" :item="pickedItem" />
<!--Form to set sharing-->
<ValidationObserver
v-if="!isGeneratedShared"
@submit.prevent
ref="shareForm"
v-slot="{ invalid }"
tag="form"
>
<!--Permission Select-->
<ValidationProvider
v-if="isFolder"
tag="div"
mode="passive"
name="Permission"
rules="required"
v-slot="{ errors }"
>
<AppInputText :title="$t('permission')" :error="errors[0]">
<SelectInput
v-model="shareOptions.permission"
:options="$translateSelectOptions(permissionOptions)"
:placeholder="$t('shared_form.placeholder_permission')"
:isError="errors[0]"
/>
</AppInputText>
</ValidationProvider>
<!--Password Switch-->
<div>
<AppInputSwitch
:title="$t('password_protected')"
:description="$t('popup.share.password_description')"
>
<SwitchInput
v-model="shareOptions.isPassword"
class="switch"
:state="shareOptions.isPassword"
/>
</AppInputSwitch>
<!--Set password-->
<ValidationProvider
v-if="shareOptions.isPassword"
tag="div"
mode="passive"
name="Password"
rules="required"
v-slot="{ errors }"
>
<AppInputText :error="errors[0]" class="-mt-2">
<input
v-model="shareOptions.password"
:class="{ '!border-rose-600': errors[0] }"
type="text"
class="focus-border-theme input-dark"
:placeholder="$t('page_sign_in.placeholder_password')"
/>
</AppInputText>
</ValidationProvider>
</div>
<!--Expiration switch-->
<div>
<AppInputSwitch :title="$t('expiration')" :description="$t('popup.share.expiration_description')">
<SwitchInput v-model="isExpiration" class="switch" :state="isExpiration" />
</AppInputSwitch>
<!--Set expiration-->
<AppInputText v-if="isExpiration" class="-mt-2">
<SelectBoxInput
v-model="shareOptions.expiration"
:data="$translateSelectOptions(expirationList)"
class="box"
/>
</AppInputText>
</div>
<!--Send on emails switch-->
<div>
<AppInputSwitch
:title="$t('popup.share.email_send')"
:description="$t('popup.share.email_description')"
:is-last="!isEmailSharing"
>
<SwitchInput v-model="isEmailSharing" class="switch" :state="isEmailSharing" />
</AppInputSwitch>
<!--Emails-->
<ValidationProvider
v-if="isEmailSharing"
tag="div"
mode="passive"
name="Email"
rules="required"
v-slot="{ errors }"
class="-mt-2 mb-1"
>
<AppInputText :error="errors[0]" class="-mt-2" :is-last="true">
<MultiEmailInput
rules="required"
v-model="shareOptions.emails"
:label="$t('recipients')"
:is-error="errors[0]"
/>
</AppInputText>
</ValidationProvider>
</div>
</ValidationObserver>
<!--Copy generated link-->
<AppInputText v-if="isGeneratedShared" :title="$t('get_your_link')" :is-last="true">
<CopyShareLink :item="pickedItem" />
</AppInputText>
</PopupContent>
<!--Actions-->
<PopupActions>
<ButtonBase v-if="!isGeneratedShared" class="w-full" @click.native="$closePopup()" button-style="secondary">
{{ $t('cancel') }}
</ButtonBase>
<ButtonBase
class="w-full"
@click.native="submitShareOptions"
button-style="theme"
:loading="isLoading"
:disabled="isLoading"
>
{{ submitButtonText }}
</ButtonBase>
</PopupActions>
</PopupWrapper>
</template>
<script>
import AppInputText from '../Admin/AppInputText'
import AppInputSwitch from '../Admin/AppInputSwitch'
import { ValidationProvider, ValidationObserver } from 'vee-validate/dist/vee-validate.full'
import SelectBoxInput from './Forms/SelectBoxInput'
import PopupWrapper from './Popup/PopupWrapper'
import PopupActions from './Popup/PopupActions'
import PopupContent from './Popup/PopupContent'
import PopupHeader from './Popup/PopupHeader'
import MultiEmailInput from './Forms/MultiEmailInput'
import SwitchInput from './Forms/SwitchInput'
import SelectInput from './Forms/SelectInput'
import ThumbnailItem from './ThumbnailItem'
import ActionButton from './ActionButton'
import CopyShareLink from './Forms/CopyShareLink'
import ButtonBase from '../FilesView/ButtonBase'
import InfoBox from './Forms/InfoBox'
import { LinkIcon, MailIcon } from 'vue-feather-icons'
import { required } from 'vee-validate/dist/rules'
import { mapGetters } from 'vuex'
import { events } from '../../bus'
import axios from 'axios'
export default {
name: 'ShareCreatePopup',
components: {
ValidationProvider,
ValidationObserver,
AppInputText,
AppInputSwitch,
SelectBoxInput,
ThumbnailItem,
ActionButton,
PopupWrapper,
PopupActions,
PopupContent,
PopupHeader,
MultiEmailInput,
SelectInput,
SwitchInput,
ButtonBase,
CopyShareLink,
MailIcon,
required,
LinkIcon,
InfoBox,
},
computed: {
...mapGetters(['permissionOptions', 'expirationList']),
itemTypeTitle() {
return this.pickedItem && this.pickedItem.data.type === 'folder'
? this.$t('folder')
: this.$t('file')
},
isFolder() {
return this.pickedItem && this.pickedItem.data.type === 'folder'
},
submitButtonText() {
return this.isGeneratedShared ? this.$t('awesome_iam_done') : this.$t('generate_link')
},
},
watch: {
isExpiration(val) {
if (!val) this.shareOptions.expiration = undefined
},
},
data() {
return {
isExpiration: false,
isEmailSharing: false,
shareOptions: {
isPassword: false,
expiration: undefined,
password: undefined,
permission: undefined,
type: undefined,
id: undefined,
emails: undefined,
},
pickedItem: undefined,
isGeneratedShared: false,
isLoading: false,
sharedViaEmail: false,
}
},
methods: {
async submitShareOptions() {
// If shared was generated, then close popup
if (this.isGeneratedShared) {
events.$emit('popup:close')
return
}
// Validate fields
const isValid = await this.$refs.shareForm.validate()
if (!isValid) return
this.isLoading = true
// Send request to get share link
axios
.post(`/api/share`, this.shareOptions)
.then((response) => {
// End loading
this.isGeneratedShared = true
this.$store.commit('UPDATE_SHARED_ITEM', response.data)
this.pickedItem.data.relationships.shared = response.data
})
.catch(() => {
events.$emit('alert:open', {
title: this.$t('popup_error.title'),
message: this.$t('popup_error.message'),
})
// End loading
this.isLoading = false
})
.finally(() => {
this.isLoading = false
})
},
},
mounted() {
events.$on('emailsInputValues', (emails) => (this.shareOptions.emails = emails))
// Show popup
events.$on('popup:open', (args) => {
if (args.name !== 'share-create') return
// Store picked item
this.pickedItem = args.item
this.shareOptions.type = args.item.data.type
this.shareOptions.id = args.item.data.id
})
// Close popup
events.$on('popup:close', () => {
// Restore data
setTimeout(() => {
this.isGeneratedShared = false
this.isExpiration = false
this.isEmailSharing = false
this.shareOptions = {
isPassword: false,
expiration: undefined,
password: undefined,
permission: undefined,
type: undefined,
id: undefined,
emails: undefined,
}
}, 150)
})
},
}
</script>

View File

@@ -1,402 +0,0 @@
<template>
<PopupWrapper name="share-edit">
<!--Title-->
<PopupHeader :title="popupTitle" icon="share" />
<!--Qr Code-->
<div v-if="pickedItem && activeSection === 'qr-code'">
<PopupContent class="flex items-center justify-center">
<div v-if="!qrCode" class="relative my-8">
<Spinner />
</div>
<div v-if="qrCode" v-html="qrCode" class="my-5 overflow-hidden rounded-xl"></div>
</PopupContent>
<PopupActions>
<ButtonBase class="w-full" @click.native="showSection(undefined)" button-style="secondary">
{{ $t('show_details') }}
</ButtonBase>
<ButtonBase class="w-full" @click.native="$closePopup()" button-style="theme">
{{ $t('awesome_iam_done') }}
</ButtonBase>
</PopupActions>
</div>
<!--Share via email-->
<div v-if="pickedItem && activeSection === 'email-sharing'">
<PopupContent>
<!--Item Thumbnail-->
<ThumbnailItem class="mb-4" :item="pickedItem" />
<ValidationObserver @submit.prevent v-slot="{ invalid }" ref="shareEmail" tag="form">
<ValidationProvider tag="div" mode="passive" name="Email" rules="required" v-slot="{ errors }">
<AppInputText title="Share with" :error="errors[0]" :is-last="true">
<MultiEmailInput
rules="required"
v-model="emails"
:label="$t('shared_form.label_send_to_recipients')"
:is-error="errors[0]"
/>
</AppInputText>
</ValidationProvider>
</ValidationObserver>
</PopupContent>
<PopupActions>
<ButtonBase class="w-full" @click.native="showSection(undefined)" button-style="secondary">
{{ $t('show_details') }}
</ButtonBase>
<ButtonBase
class="w-full"
@click.native="sendViaEmail"
button-style="theme"
:loading="isLoading"
:disabled="isLoading"
>
{{ $t('send') }}
</ButtonBase>
</PopupActions>
</div>
<!--Update sharing-->
<div v-if="pickedItem && !activeSection">
<PopupContent class="!overflow-initial">
<!--Item Thumbnail-->
<ThumbnailItem class="mb-5" :item="pickedItem" />
<!--Get share link-->
<AppInputText :title="$t('get_your_link')">
<CopyShareLink :item="pickedItem" />
</AppInputText>
<ValidationObserver @submit.prevent ref="shareForm" v-slot="{ invalid }" tag="form">
<!--Permission Select-->
<ValidationProvider
v-if="isFolder"
tag="div"
mode="passive"
name="Permission"
rules="required"
v-slot="{ errors }"
>
<AppInputText :title="$t('permission')" :error="errors[0]">
<SelectInput
v-model="shareOptions.permission"
:options="$translateSelectOptions(permissionOptions)"
:default="shareOptions.permission"
:placeholder="$t('shared_form.placeholder_permission')"
:isError="errors[0]"
/>
</AppInputText>
</ValidationProvider>
<!--Password Switch-->
<div>
<AppInputSwitch
:title="$t('password_protected')"
:description="$t('popup.share.password_description')"
>
<SwitchInput
v-model="shareOptions.isProtected"
class="switch"
:state="shareOptions.isProtected"
/>
</AppInputSwitch>
<ActionButton
v-if="
pickedItem.data.relationships.shared.data.attributes.protected &&
canChangePassword &&
shareOptions.isProtected
"
@click.native="changePassword"
class="mb-6 -mt-4"
>
{{ $t('popup_share_edit.change_pass') }}
</ActionButton>
<!--Set password-->
<ValidationProvider
v-if="shareOptions.isProtected && !canChangePassword"
tag="div"
mode="passive"
name="Password"
rules="required"
v-slot="{ errors }"
>
<AppInputText :error="errors[0]" class="-mt-2">
<input
v-model="shareOptions.password"
:class="{ '!border-rose-600': errors[0] }"
type="text"
class="focus-border-theme input-dark"
:placeholder="$t('page_sign_in.placeholder_password')"
/>
</AppInputText>
</ValidationProvider>
</div>
<!--Expiration switch-->
<div>
<AppInputSwitch
:title="$t('expiration')"
:description="$t('popup.share.expiration_description')"
:is-last="!shareOptions.expiration"
>
<SwitchInput
v-model="shareOptions.expiration"
class="switch"
:state="shareOptions.expiration ? 1 : 0"
/>
</AppInputSwitch>
<!--Set expiration-->
<AppInputText v-if="shareOptions.expiration" class="-mt-2" :is-last="true">
<SelectBoxInput
v-model="shareOptions.expiration"
:data="$translateSelectOptions(expirationList)"
:value="shareOptions.expiration"
class="box"
/>
</AppInputText>
</div>
</ValidationObserver>
</PopupContent>
<PopupActions>
<ButtonBase
class="w-full"
@click.native="destroySharing"
:button-style="destroyButtonStyle"
:loading="isDeleting"
:disabled="isDeleting"
>
{{ destroyButtonText }}
</ButtonBase>
<ButtonBase
class="w-full"
@click.native="updateShareOptions"
button-style="theme"
:loading="isLoading"
:disabled="isLoading"
>
{{ $t('store_changes') }}
</ButtonBase>
</PopupActions>
</div>
</PopupWrapper>
</template>
<script>
import { ValidationProvider, ValidationObserver } from 'vee-validate/dist/vee-validate.full'
import MultiEmailInput from './Forms/MultiEmailInput'
import SelectBoxInput from './Forms/SelectBoxInput'
import CopyShareLink from './Forms/CopyShareLink'
import PopupWrapper from './Popup/PopupWrapper'
import PopupActions from './Popup/PopupActions'
import PopupContent from './Popup/PopupContent'
import PopupHeader from './Popup/PopupHeader'
import SwitchInput from './Forms/SwitchInput'
import SelectInput from './Forms/SelectInput'
import ThumbnailItem from './ThumbnailItem'
import ActionButton from './ActionButton'
import ButtonBase from '../FilesView/ButtonBase'
import AppInputSwitch from '../Admin/AppInputSwitch'
import AppInputText from '../Admin/AppInputText'
import { required } from 'vee-validate/dist/rules'
import Spinner from '../FilesView/Spinner'
import { events } from '../../bus'
import { mapGetters } from 'vuex'
import axios from 'axios'
export default {
name: 'ShareEditPopup',
components: {
ValidationProvider,
ValidationObserver,
MultiEmailInput,
AppInputSwitch,
SelectBoxInput,
ThumbnailItem,
CopyShareLink,
ActionButton,
PopupWrapper,
PopupActions,
AppInputText,
PopupContent,
PopupHeader,
SelectInput,
SwitchInput,
ButtonBase,
required,
Spinner,
},
computed: {
...mapGetters(['permissionOptions', 'expirationList', 'user']),
popupTitle() {
return (
{
'qr-code': this.$t('get_qr_code'),
'email-sharing': this.$t('share_with_multiple_emails'),
}[this.activeSection] || this.$t('popup_share_edit.title')
)
},
isFolder() {
return this.pickedItem && this.pickedItem.data.type === 'folder'
},
destroyButtonText() {
return this.isConfirmedDestroy ? this.$t('popup_share_edit.confirm') : this.$t('popup_share_edit.stop')
},
destroyButtonStyle() {
return this.isConfirmedDestroy ? 'danger-solid' : 'secondary'
},
},
watch: {
'shareOptions.expiration': function (val) {
if (!val) {
this.shareOptions.expiration = undefined
}
},
},
data() {
return {
activeSection: undefined,
shareOptions: undefined,
pickedItem: undefined,
emails: undefined,
qrCode: undefined,
isConfirmedDestroy: false,
canChangePassword: false,
isMoreOptions: false,
isDeleting: false,
isLoading: false,
}
},
methods: {
getQrCode() {
axios
.get(`/api/share/${this.shareOptions.token}/qr`)
.then((response) => {
this.qrCode = response.data
})
.catch(() => this.$isSomethingWrong())
},
showSection(section = undefined) {
this.activeSection = section
},
changePassword() {
this.canChangePassword = false
},
async sendViaEmail() {
// Validate email field
const isValid = await this.$refs.shareEmail.validate()
if (!isValid) return
this.isLoading = true
axios
.post(`/api/share/${this.shareOptions.token}/email`, {
emails: this.emails,
})
.catch(() => {
this.$isSomethingWrong()
})
.finally(() => {
this.isLoading = false
this.$closePopup()
})
},
async destroySharing() {
// Set confirm button
if (!this.isConfirmedDestroy) {
this.isConfirmedDestroy = true
return
}
// Start deleting spinner button
this.isDeleting = true
// Send delete request
await this.$store
.dispatch('shareCancel', this.pickedItem)
.catch(() => {
// End deleting spinner button
this.isDeleting = false
})
.finally(() => this.$closePopup())
},
async updateShareOptions() {
// Validate fields
const isValid = await this.$refs.shareForm.validate()
if (!isValid) return
this.isLoading = true
// Send request to get share link
axios
.post('/api/share/' + this.shareOptions.token, {
permission: this.shareOptions.permission,
protected: this.shareOptions.isProtected,
expiration: this.shareOptions.expiration,
password: this.shareOptions.password ? this.shareOptions.password : undefined,
_method: 'patch',
})
.then((response) => {
// Update shared data
this.$store.commit('UPDATE_SHARED_ITEM', response.data)
events.$emit('popup:close')
})
.catch(() => {
this.$isSomethingWrong()
})
.finally(() => {
this.isLoading = false
})
},
},
mounted() {
events.$on('emailsInputValues', (emails) => (this.emails = emails))
// Show popup
events.$on('popup:open', (args) => {
if (args.name !== 'share-edit') return
// Store picked item
this.pickedItem = args.item
// Store shared options
this.shareOptions = {
id: args.item.data.relationships.shared.data.id,
token: args.item.data.relationships.shared.data.attributes.token,
expiration: args.item.data.relationships.shared.data.attributes.expire_in,
isProtected: args.item.data.relationships.shared.data.attributes.protected,
permission: args.item.data.relationships.shared.data.attributes.permission,
password: undefined,
}
if (args.section) this.activeSection = args.section
if (args.section === 'qr-code') this.getQrCode()
this.canChangePassword = args.item.data.relationships.shared.data.attributes.protected
})
events.$on('popup:close', () => {
// Reset data
setTimeout(() => {
this.isDeleting = false
this.isConfirmedDestroy = false
this.canChangePassword = false
this.shareOptions = undefined
this.pickedItem = undefined
this.activeSection = undefined
this.qrCode = undefined
}, 150)
})
},
}
</script>

View File

@@ -1,20 +0,0 @@
<template>
<div v-if="isActive">
<slot></slot>
</div>
</template>
<script>
export default {
name: 'TabOption',
props: ['title', 'icon', 'selected'],
data() {
return {
isActive: false,
}
},
mounted() {
this.isActive = this.selected
},
}
</script>

View File

@@ -1,124 +0,0 @@
<template>
<div>
<div class="tab-wrapper">
<div
class="tab"
:class="{ active: tab.isActive }"
@click="selectTab(tab)"
v-for="(tab, i) in tabs"
:key="i"
>
<!--Icon-->
<mail-icon v-if="tab.icon === 'email'" class="tab-icon text-theme dark-text-theme" size="17" />
<link-icon v-if="tab.icon === 'link'" class="tab-icon text-theme dark-text-theme" size="17" />
<smile-icon v-if="tab.icon === 'emoji'" class="tab-icon text-theme dark-text-theme" size="17" />
<folder-icon v-if="tab.icon === 'folder'" class="tab-icon text-theme dark-text-theme" size="17" />
<!--Title-->
<b class="tab-title">
{{ tab.title }}
</b>
</div>
</div>
<slot></slot>
</div>
</template>
<script>
import { LinkIcon, MailIcon, SmileIcon, FolderIcon } from 'vue-feather-icons'
export default {
name: 'TabWrapper',
components: {
LinkIcon,
MailIcon,
SmileIcon,
FolderIcon,
},
data() {
return {
tabs: [],
}
},
methods: {
selectTab(selectedTab) {
this.tabs.forEach((tab) => {
tab.isActive = tab.title == selectedTab.title
})
},
},
mounted() {
this.tabs = this.$children
},
}
</script>
<style scoped lang="scss">
@import '../../../sass/vuefilemanager/inapp-forms';
@import '../../../sass/vuefilemanager/forms';
.tab-wrapper {
display: flex;
justify-content: center;
margin-bottom: 20px;
cursor: pointer;
align-items: center;
background: white;
color: $text;
border-radius: 8px;
overflow: hidden;
border: 1px solid #e8e9eb;
.tab-title {
@include font-size(14);
}
.tab {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
padding: 8px;
&.active {
background: $light_background;
.tab-title {
color: $text;
}
}
}
.tab-icon {
margin-right: 10px;
path,
circle,
line,
polyline {
color: inherit !important;
}
}
}
.dark {
.tab-wrapper {
background: $dark_mode_foreground;
border-color: transparent;
.tab.active {
background: lighten($dark_mode_foreground, 7%);
.tab-title {
color: $dark_mode_text_primary;
}
}
}
.popup-wrapper {
.tab-wrapper {
background: lighten($dark_mode_foreground, 2%);
}
}
}
</style>

View File

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

View File

@@ -1,70 +0,0 @@
<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 '../../FilesView/MemberAvatar'
export default {
name: 'DatatableCellImage',
props: ['member', 'title', 'description', 'image-size'],
components: {
MemberAvatar,
},
}
</script>
<style lang="scss" scoped>
@import '../../../../sass/vuefilemanager/variables';
@import '../../../../sass/vuefilemanager/mixins';
.info {
.name,
.description {
max-width: 150px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: block;
}
.name {
@include font-size(15);
line-height: 1;
color: $text;
}
.description {
color: $text-muted;
@include font-size(12);
}
}
.dark {
.cell-image-thumbnail {
.image {
img {
&.blurred {
display: none;
}
}
}
.info {
.name {
color: $dark_mode_text_primary;
}
.description {
color: $dark_mode_text_secondary;
}
}
}
}
</style>

View File

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

View File

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

View File

@@ -1,24 +0,0 @@
<template>
<b class="theme-label">
<slot></slot>
</b>
</template>
<script>
export default {
name: 'TextLabel',
}
</script>
<style lang="scss" scoped>
@import '../../../sass/vuefilemanager/variables';
@import '../../../sass/vuefilemanager/mixins';
.theme-label {
@include font-size(14);
color: $theme;
font-weight: 600;
display: block;
margin-bottom: 20px;
}
</style>

View File

@@ -1,140 +0,0 @@
<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 '../FilesView/FileIconThumbnail'
import MemberAvatar from '../FilesView/MemberAvatar'
import Emoji from './Emoji'
import { mapGetters } from 'vuex'
import FolderIcon from '../FilesView/FolderIcon'
export default {
name: 'ThumbnailItem',
props: ['setFolderIcon', 'item', 'info'],
components: {
FileIconThumbnail,
MemberAvatar,
FolderIcon,
Emoji,
LinkIcon,
EyeIcon,
},
computed: {
...mapGetters(['isMultiSelectMode', 'clipboard', 'user']),
isClicked() {
return this.clipboard.some((element) => element.data.id === this.item.data.id)
},
isVideo() {
return this.item.data.type === 'video'
},
isAudio() {
return this.item.data.type === 'audio'
},
isFile() {
return this.item.data.type === 'file'
},
isImage() {
return this.item.data.type === 'image'
},
isFolder() {
return this.item.data.type === 'folder'
},
timeStamp() {
return this.item.data.attributes.deleted_at
? this.$t('item_thumbnail.deleted_at', {
time: this.item.data.attributes.deleted_at,
})
: this.item.data.attributes.created_at
},
canEditName() {
return (
!this.$isMobile() &&
!this.$isThisRoute(this.$route, ['Trash']) &&
!this.$checkPermission('visitor') &&
!(this.sharedDetail && this.sharedDetail.attributes.type === 'file')
)
},
folderItems() {
return this.item.data.attributes.deleted_at
? this.item.data.attributes.trashed_items
: this.item.data.attributes.items
},
canShowAuthor() {
return !this.isFolder && (this.item.data.relationships.creator && this.user.data.id !== this.item.data.relationships.creator.data.id)
},
canDrag() {
return !this.isDeleted && this.$checkPermission(['master', 'editor'])
},
},
}
</script>

View File

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

View File

@@ -1,156 +0,0 @@
<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>

View File

@@ -1,132 +0,0 @@
<template>
<PopupWrapper name="two-factor-qr-setup">
<PopupHeader :title="$t('set_up_2fa_app')" icon="edit" />
<PopupContent>
<div v-if="qrCode" class="flex justify-center">
<div v-html="qrCode" class="my-5"></div>
</div>
<InfoBox style="margin-bottom: 0">
<p v-html="$t('popup_2fa.help')"></p>
</InfoBox>
<ValidationObserver @submit.prevent="confirm2FaSetup" ref="codeForm" v-slot="{ invalid }" tag="form" class="mt-5">
<ValidationProvider tag="div" mode="passive" name="Code" rules="required" v-slot="{ errors }">
<AppInputText :title="$t('confirm')" :error="errors[0]" :is-last="true">
<input
v-model="code"
:class="{ '!border-rose-600': errors[0] }"
type="text"
ref="input"
class="focus-border-theme input-dark"
:placeholder="$t('paste_code_from_2fa_app')"
/>
</AppInputText>
</ValidationProvider>
</ValidationObserver>
</PopupContent>
<PopupActions>
<ButtonBase @click.native="confirm2FaSetup" class="w-full" button-style="theme" :loading="isLoading">
{{ $t('confirm_your_code') }}
</ButtonBase>
</PopupActions>
</PopupWrapper>
</template>
<script>
import { ValidationProvider, ValidationObserver } from 'vee-validate/dist/vee-validate.full'
import { required } from 'vee-validate/dist/rules'
import ButtonBase from '../FilesView/ButtonBase'
import AppInputText from '../Admin/AppInputText'
import PopupWrapper from './Popup/PopupWrapper'
import PopupActions from './Popup/PopupActions'
import PopupContent from './Popup/PopupContent'
import PopupHeader from './Popup/PopupHeader'
import InfoBox from './Forms/InfoBox'
import { events } from '../../bus'
import axios from 'axios'
export default {
name: 'TwoFactorQrSetupPopup',
components: {
ValidationProvider,
ValidationObserver,
AppInputText,
PopupWrapper,
PopupActions,
PopupContent,
PopupHeader,
ButtonBase,
required,
InfoBox,
},
data() {
return {
qrCode: undefined,
isLoading: false,
code: undefined
}
},
methods: {
async confirm2FaSetup() {
// Validate fields
const isValid = await this.$refs.codeForm.validate()
if (!isValid) return
this.isLoading = true
axios
.post('/user/confirmed-two-factor-authentication', {code: this.code})
.then(() => {
this.$store.commit('CHANGE_TWO_FACTOR_AUTHENTICATION_STATE', true)
this.$closePopup()
events.$emit('toaster', {
type: 'success',
message: this.$t('popup_2fa.toaster_enabled'),
})
})
.catch((error) => {
if (error.response.status === 422) {
this.$refs.codeForm.setErrors({
'Code': error.response.data.errors['code'][0],
})
}
})
.finally(() => this.isLoading = false)
},
enable() {
axios
.post('/user/two-factor-authentication')
.then(() => {
this.getQrCode()
})
.catch(() => {
this.$isSomethingWrong()
})
},
getQrCode() {
axios
.get('/user/two-factor-qr-code')
.then((response) => {
this.qrCode = response.data.svg
})
.catch(() => {
this.$isSomethingWrong()
})
},
},
created() {
events.$on('popup:open', (args) => {
if (args.name !== 'two-factor-qr-setup') return
this.enable()
})
},
}
</script>

View File

@@ -1,200 +0,0 @@
<template>
<PopupWrapper name="two-factor-recovery-codes">
<PopupHeader :title="$t('your_security_codes')" icon="key" />
<PopupContent style="padding: 0 20px">
<div class="mobile-actions">
<MobileActionButton @click.native="copyCodes" icon="copy">
{{ $t('copy') }}
</MobileActionButton>
<MobileActionButton @click.native="downloadCodes" icon="download">
{{ $t('download') }}
</MobileActionButton>
<MobileActionButton @click.native="regenerateCodes" icon="refresh">
{{ $t('regenerate_codes') }}
</MobileActionButton>
</div>
<ul v-if="!isLoading" class="codes-list">
<li v-for="(code, i) in codes" :key="i">{{ code }}</li>
</ul>
<div v-if="isLoading" class="spinner-wrapper">
<Spinner />
</div>
<textarea v-model="inputCodes" ref="codes" class="codes-output"></textarea>
<InfoBox style="margin-bottom: 0">
<p v-html="$t('popup_2fa.popup_codes_disclaimer')"></p>
</InfoBox>
</PopupContent>
<PopupActions>
<ButtonBase class="w-full" @click.native="$closePopup()" button-style="theme">
{{ $t('awesome_iam_done') }}
</ButtonBase>
</PopupActions>
</PopupWrapper>
</template>
<script>
import MobileActionButton from '../FilesView/MobileActionButton'
import PopupWrapper from './Popup/PopupWrapper'
import PopupActions from './Popup/PopupActions'
import PopupContent from './Popup/PopupContent'
import PopupHeader from './Popup/PopupHeader'
import ButtonBase from '../FilesView/ButtonBase'
import InfoBox from './Forms/InfoBox'
import Spinner from '../FilesView/Spinner'
import { mapGetters } from 'vuex'
import { events } from '../../bus'
import axios from 'axios'
export default {
name: 'TwoFactorRecoveryCodesPopup',
components: {
MobileActionButton,
PopupWrapper,
PopupActions,
PopupContent,
PopupHeader,
ButtonBase,
Spinner,
InfoBox,
},
data() {
return {
codes: undefined,
isLoading: true,
inputCodes: undefined,
}
},
computed: {
...mapGetters(['user']),
},
methods: {
copyCodes() {
let copyText = this.$refs.codes
copyText.select()
copyText.setSelectionRange(0, 99999)
document.execCommand('copy')
events.$emit('toaster', {
type: 'success',
message: this.$t('popup_2fa.toaster_codes_copied'),
})
},
downloadCodes() {
// Create txt content
let recoveryCodes = 'data:x-application/xml;charset=utf-8,' + escape(this.codes.join('\n'))
// Create download link
let downloadLink = document.createElement('a')
downloadLink.href = recoveryCodes
downloadLink.download = 'recovery-codes.txt'
// Download .txt
document.body.appendChild(downloadLink)
downloadLink.click()
document.body.removeChild(downloadLink)
},
regenerateCodes() {
this.isLoading = true
axios
.post('/user/two-factor-recovery-codes')
.then(() => {
this.getRecoveryCodes()
events.$emit('toaster', {
type: 'success',
message: this.$t('popup_2fa.toaster_codes_regenerated'),
})
})
.catch(() => {
this.$isSomethingWrong()
})
.finally(() => (this.isLoading = false))
},
getRecoveryCodes() {
axios
.get('/user/two-factor-recovery-codes')
.then((response) => {
this.codes = response.data
this.inputCodes = response.data.join('\n')
})
.catch(() => {
this.$isSomethingWrong()
})
.finally(() => (this.isLoading = false))
},
},
created() {
events.$on('popup:open', ({ name }) => {
if ('two-factor-recovery-codes' === name) this.getRecoveryCodes()
})
},
}
</script>
<style lang="scss" scoped>
@import '../../../sass/vuefilemanager/variables';
@import '../../../sass/vuefilemanager/mixins';
.mobile-actions {
white-space: nowrap;
overflow-x: auto;
margin: 0 -20px;
padding: 10px 0 10px 20px;
}
.codes-list {
margin: 5px 0 15px;
padding-left: 30px;
li {
@include font-size(14);
font-weight: bold;
padding: 10px 0;
border-bottom: 1px solid $light_mode_border;
list-style: circle;
&:last-child {
border-bottom: none;
}
}
}
.codes-output {
position: absolute;
right: -9999px;
}
.spinner-wrapper {
height: 339px;
position: relative;
.spinner {
top: 46% !important;
}
}
.dark {
.codes-list {
li {
border-color: $dark_mode_border_color;
}
}
.info-box,
.mobile-action-button {
background: lighten($dark_mode_foreground, 3%);
}
}
</style>

View File

@@ -1,77 +0,0 @@
<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>