@testdracul/media-frontend 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/.env.development +4 -0
  2. package/.env.example +3 -0
  3. package/.eslintrc.json +25 -0
  4. package/babel.config.js +5 -0
  5. package/dist/dracul-media-frontend.es.js +16238 -0
  6. package/dist/dracul-media-frontend.umd.js +586 -0
  7. package/dist/media-frontend.css +1 -0
  8. package/docs-en.md +45 -0
  9. package/docs-es.md +45 -0
  10. package/package.json +56 -0
  11. package/readme.md +36 -0
  12. package/src/components/CsvWebViewer/CsvWebViewer.vue +81 -0
  13. package/src/components/CsvWebViewer/index.ts +4 -0
  14. package/src/components/FileUpload/FileUpload.vue +94 -0
  15. package/src/components/FileUpload/index.ts +4 -0
  16. package/src/components/FileUploadButton/FileUploadButton.vue +127 -0
  17. package/src/components/FileUploadButton/index.ts +4 -0
  18. package/src/components/FileUploadExpiration/FileUploadExpiration.vue +274 -0
  19. package/src/components/FileUploadExpiration/index.ts +4 -0
  20. package/src/components/FileUploadExpress/FileUploadExpress.vue +208 -0
  21. package/src/components/FileUploadExpress/index.ts +4 -0
  22. package/src/components/FileView/FileView.vue +336 -0
  23. package/src/components/FileView/index.ts +4 -0
  24. package/src/components/GroupsShow/GroupsShow.vue +40 -0
  25. package/src/components/GroupsShow/index.ts +4 -0
  26. package/src/components/MediaField/MediaField.vue +62 -0
  27. package/src/components/MediaField/index.ts +4 -0
  28. package/src/components/PdfWebViewer/PdfWebViewer.vue +81 -0
  29. package/src/components/PdfWebViewer/index.ts +4 -0
  30. package/src/components/UsersShow/UsersShow.vue +39 -0
  31. package/src/components/UsersShow/index.ts +4 -0
  32. package/src/components/XlsxWebViewer/XlsxWebViewer.vue +70 -0
  33. package/src/components/XlsxWebViewer/index.ts +4 -0
  34. package/src/helpers/redeableBytes.ts +9 -0
  35. package/src/i18n/index.ts +22 -0
  36. package/src/i18n/messages/DocMessages.ts +31 -0
  37. package/src/i18n/messages/ExtraMessages.ts +29 -0
  38. package/src/i18n/messages/FileMessages.ts +223 -0
  39. package/src/i18n/messages/UserStorageMessages.ts +145 -0
  40. package/src/i18n/permissions/FilePermissionMessages.ts +50 -0
  41. package/src/i18n/permissions/OldPermissionMessages.ts +59 -0
  42. package/src/i18n/permissions/UserStoragePermissionMessages.ts +40 -0
  43. package/src/index.ts +70 -0
  44. package/src/mixins/readableBytesMixin.ts +9 -0
  45. package/src/pages/FileManagementPage/FileCreate/FileCreate.vue +108 -0
  46. package/src/pages/FileManagementPage/FileCreate/index.ts +3 -0
  47. package/src/pages/FileManagementPage/FileCrud/FileCrud.vue +133 -0
  48. package/src/pages/FileManagementPage/FileCrud/index.ts +4 -0
  49. package/src/pages/FileManagementPage/FileDelete/FileDelete.vue +61 -0
  50. package/src/pages/FileManagementPage/FileDelete/index.ts +3 -0
  51. package/src/pages/FileManagementPage/FileFilters/FileFilters.vue +150 -0
  52. package/src/pages/FileManagementPage/FileFilters/index.ts +3 -0
  53. package/src/pages/FileManagementPage/FileForm/FileForm.vue +184 -0
  54. package/src/pages/FileManagementPage/FileForm/UserCombobox.vue +66 -0
  55. package/src/pages/FileManagementPage/FileForm/index.ts +3 -0
  56. package/src/pages/FileManagementPage/FileList/FileEditButton.vue +410 -0
  57. package/src/pages/FileManagementPage/FileList/FileList.vue +178 -0
  58. package/src/pages/FileManagementPage/FileList/index.ts +4 -0
  59. package/src/pages/FileManagementPage/FileShow/FileShow.vue +23 -0
  60. package/src/pages/FileManagementPage/FileShow/FileShowData.vue +35 -0
  61. package/src/pages/FileManagementPage/FileShow/index.ts +3 -0
  62. package/src/pages/FileManagementPage/FileUpdate/FileUpdate.vue +107 -0
  63. package/src/pages/FileManagementPage/FileUpdate/index.ts +4 -0
  64. package/src/pages/FileManagementPage/index.vue +20 -0
  65. package/src/pages/MediaDocPage/MediaDocCard.vue +35 -0
  66. package/src/pages/MediaDocPage/MediaDocPage.vue +78 -0
  67. package/src/pages/UserStoragePage/UserStorage.vue +311 -0
  68. package/src/pages/UserStoragePage/UserStorageForm/UserStorageForm.vue +172 -0
  69. package/src/pages/UserStoragePage/UserStorageUpdate/UserStorageUpdate.vue +91 -0
  70. package/src/pages/UserStoragePage/index.vue +14 -0
  71. package/src/providers/FileMetricsProvider.ts +47 -0
  72. package/src/providers/FileProvider.ts +60 -0
  73. package/src/providers/UploadProvider.ts +32 -0
  74. package/src/providers/UserStorageProvider.ts +47 -0
  75. package/src/providers/gql/almacenamientoPorUsuario.graphql +10 -0
  76. package/src/providers/gql/cantidadArchivosPorUsuario.graphql +10 -0
  77. package/src/providers/gql/fetchMediaVariables.graphql +6 -0
  78. package/src/providers/gql/fileCreate.graphql +27 -0
  79. package/src/providers/gql/fileDelete.graphql +7 -0
  80. package/src/providers/gql/fileFetch.graphql +29 -0
  81. package/src/providers/gql/fileFind.graphql +29 -0
  82. package/src/providers/gql/fileGlobalMetrics.graphql +6 -0
  83. package/src/providers/gql/filePaginate.graphql +38 -0
  84. package/src/providers/gql/fileUpdate.graphql +29 -0
  85. package/src/providers/gql/fileUpload.graphql +29 -0
  86. package/src/providers/gql/fileUploadAnonymous.graphql +25 -0
  87. package/src/providers/gql/fileUserMetrics.graphql +9 -0
  88. package/src/providers/gql/userStorageFetch.graphql +17 -0
  89. package/src/providers/gql/userStorageFindByUser.graphql +17 -0
  90. package/src/providers/gql/userStorageUpdate.graphql +31 -0
  91. package/src/routes/index.ts +32 -0
  92. package/vite.config.ts +65 -0
  93. package/vue.config.js +22 -0
@@ -0,0 +1,274 @@
1
+ <template>
2
+ <v-container>
3
+ <v-row>
4
+ <v-col cols="12" sm="6" md="6">
5
+ <date-time-input
6
+ v-model="expirationDate"
7
+ :label="$t('media.file.expirationDate')"
8
+ prepend-inner-icon="mdi-calendar-clock"
9
+ color="secondary"
10
+ hide-details
11
+ :rules="fileExpirationTimeRules"/>
12
+ </v-col>
13
+
14
+ <v-col cols="12" sm="6" md="6">
15
+ <v-select
16
+ prepend-inner-icon="mdi-eye"
17
+ v-model="isPublic"
18
+ :items="[{title: 'Público', value: true}, {title: 'Privado', value: false}]"
19
+ :label="$t('media.file.visibility')"
20
+ ></v-select>
21
+ </v-col>
22
+
23
+
24
+ <v-col cols="12" sm="12" md="12" >
25
+ <v-combobox
26
+ prepend-inner-icon="mdi-tag-multiple"
27
+ v-model="tags"
28
+ :label="$t('media.file.tags')"
29
+ multiple
30
+ chips
31
+ color="secondary"
32
+ item-color="secondary"
33
+ ></v-combobox>
34
+ </v-col>
35
+
36
+ <v-col cols="12" sm="12" md="12" >
37
+ <v-text-field
38
+ prepend-inner-icon="mdi-text-box"
39
+ name="filename"
40
+ v-model="description"
41
+ :label="$t('media.file.description')"
42
+ :placeholder="$t('media.file.description')"
43
+ color="secondary"
44
+ ></v-text-field>
45
+ </v-col>
46
+ </v-row>
47
+
48
+ <v-container class="mb-0 pb-0">
49
+ <input type="file"
50
+ style="display: none"
51
+ ref="fileInput"
52
+ :accept="accept"
53
+ @change="onFilePicked"
54
+ :disabled="disableUploadButton"
55
+ />
56
+
57
+ <v-menu
58
+ v-model="showErrorMessage"
59
+ :min-width="200"
60
+ :close-on-content-click="false"
61
+ :close-on-click="false"
62
+ offset-x
63
+ >
64
+ <template v-slot:activator="{ props }">
65
+ <v-btn @click="pickFile()"
66
+ fab
67
+ :color="getState.color"
68
+ :loading="loading"
69
+ :size="xLarge ? 'x-large' : undefined"
70
+ v-bind="props"
71
+ >
72
+ <v-avatar v-if="isImage">
73
+ <v-img :src="getSrc" alt="image"/>
74
+ </v-avatar>
75
+ <v-icon v-else-if="isAudio">mdi-headset</v-icon>
76
+ <v-icon v-else-if="isVideo">mdi-videocam</v-icon>
77
+ <v-icon v-else>{{ getState.icon }}</v-icon>
78
+ </v-btn>
79
+ </template>
80
+
81
+ <v-card :style="{width: '280px'}" elevation="0">
82
+ <v-card-text class="pb-0 pa-0">
83
+ <v-alert class="mb-0" border="start" type="error" variant="outlined">
84
+ {{ errorMessage }}
85
+ </v-alert>
86
+ </v-card-text>
87
+ <v-card-actions class="justify-center">
88
+ <v-btn text color="primary" @click="resetUpload" class="ml-2">OK</v-btn>
89
+ </v-card-actions>
90
+ </v-card>
91
+
92
+ </v-menu>
93
+
94
+ <p class="mb-0 mt-5">{{(file) == null ? $t('media.file.chooseFile') : file.name}}</p>
95
+ </v-container>
96
+ </v-container>
97
+ </template>
98
+
99
+ <script setup>
100
+ import { ref, computed, onMounted } from 'vue'
101
+ import { useI18n } from 'vue-i18n'
102
+ import uploadProvider from "../../providers/UploadProvider";
103
+ import UserStorageProvider from "../../providers/UserStorageProvider"
104
+ import { DateTimeInput } from '@testdracul/dayjs-frontend';
105
+ import dayjs from 'dayjs'
106
+
107
+ const { t } = useI18n()
108
+
109
+ const props = defineProps({
110
+ autoSubmit: {type: Boolean, default: false},
111
+ accept: {type: String, default: '*'},
112
+ xLarge: {type: Boolean, default: false},
113
+ })
114
+
115
+ const emit = defineEmits(['filePicked', 'fileUploaded'])
116
+
117
+ const INITIAL = 'initial'
118
+ const SELECTED = 'selected'
119
+ const UPLOADED = 'uploaded'
120
+ const ERROR = 'error'
121
+
122
+ const dialog = ref(false)
123
+ const dialogTitle = ref(null)
124
+ const title = 'media.file.creating'
125
+ const errorMessage = ref(null)
126
+ const showErrorMessage = ref(null)
127
+ const inputErrors = ref({})
128
+ const file = ref(null)
129
+ const type = ref(null)
130
+ const uploadedFile = ref(null)
131
+ const state = ref(INITIAL)
132
+ const maxFileSize = ref(0)
133
+ const expirationDate = ref(null)
134
+ const fileExpirationTime = ref(null)
135
+
136
+ const disableUploadButton = ref(false)
137
+
138
+ const loading = ref(false)
139
+ const isPublic = ref(false)
140
+ const description = ref(null)
141
+ const tags = ref([])
142
+ const fileInput = ref(null)
143
+
144
+ const states = {
145
+ initial: { color: 'blue-grey', icon: 'mdi-cloud-upload' },
146
+ selected: { color: 'cyan-darken-3', icon: 'mdi-publish' },
147
+ loading: { color: 'amber-darken-3', icon: '' },
148
+ uploaded: { color: 'green-darken-3', icon: 'mdi-magnify-plus' },
149
+ error: { color: 'red-darken-3', icon: 'mdi-alert' },
150
+ }
151
+
152
+ const getState = computed(() => {
153
+ if (loading.value) return states.loading
154
+ return states[state.value]
155
+ })
156
+
157
+ const isImage = computed(() => !!(uploadedFile.value && uploadedFile.value.type === 'image'))
158
+ const isAudio = computed(() => !!(uploadedFile.value && uploadedFile.value.type === 'audio'))
159
+ const isVideo = computed(() => !!(uploadedFile.value && uploadedFile.value.type === 'video'))
160
+ const getSrc = computed(() => (uploadedFile.value && uploadedFile.value.url) ? uploadedFile.value.url : null)
161
+
162
+ const getDifferenceInDays = computed(() => {
163
+ if (expirationDate.value) {
164
+ const today = dayjs();
165
+ const expDate = dayjs(expirationDate.value);
166
+ if (!expDate.isValid()) return null;
167
+ return expDate.diff(today, 'day');
168
+ }
169
+ return null;
170
+ })
171
+
172
+ const fileExpirationTimeRules = computed(() => [
173
+ () => {
174
+ disableUploadButton.value = true;
175
+ if (getDifferenceInDays.value === null) {
176
+ disableUploadButton.value = false;
177
+ return true;
178
+ }
179
+
180
+ if (getDifferenceInDays.value < 0) {
181
+ return t("media.userStorage.fileExpirationTimeOlderThanToday")
182
+ } else if (fileExpirationTime.value && (getDifferenceInDays.value >= fileExpirationTime.value)) {
183
+ return `${t("media.userStorage.fileExpirationLimitExceeded")} ${fileExpirationTime.value} ${t("media.file.days")}`
184
+ }
185
+ disableUploadButton.value = false;
186
+ return true
187
+ }
188
+ ])
189
+
190
+ const pickFile = () => {
191
+ if (state.value === INITIAL) {
192
+ fileInput.value.click()
193
+ } else if (state.value === SELECTED) {
194
+ upload()
195
+ } else if (state.value === UPLOADED || state.value === ERROR) {
196
+ dialog.value = true
197
+ }
198
+ }
199
+
200
+ const onFilePicked = (e) => {
201
+ file.value = e.target.files[0]
202
+ state.value = SELECTED
203
+ const fileSize = e.target.files[0].size ? e.target.files[0].size / (1024 * 1024) : null;
204
+ emit('filePicked', fileSize);
205
+ if (props.autoSubmit) {
206
+ upload(fileSize)
207
+ }
208
+ }
209
+
210
+ const findUserStorage = () => {
211
+ return UserStorageProvider.findUserStorageByUser().then((res) => {
212
+ if (res.data.userStorageFindByUser && res.data.userStorageFindByUser.maxFileSize) {
213
+ maxFileSize.value = res.data.userStorageFindByUser.maxFileSize;
214
+ fileExpirationTime.value = res.data.userStorageFindByUser.fileExpirationTime;
215
+ }
216
+ }).catch(err => console.error(err))
217
+ }
218
+
219
+ const upload = async (fileSize) => {
220
+ if (file.value && state.value !== UPLOADED && fileSize <= maxFileSize.value) {
221
+ loading.value = true;
222
+
223
+ // Aseguramos que la fecha tenga una hora por defecto si no se seleccionó (ej. fin del día)
224
+ let finalExpiration = expirationDate.value;
225
+ if (finalExpiration) {
226
+ const d = dayjs(finalExpiration);
227
+ if (d.hour() === 0 && d.minute() === 0) {
228
+ finalExpiration = d.hour(23).minute(59).toISOString();
229
+ }
230
+ }
231
+
232
+ await uploadProvider.uploadFile(file.value, finalExpiration, isPublic.value, description.value, tags.value).then(result => {
233
+ uploadedFile.value = result.data.fileUpload;
234
+ setState(UPLOADED);
235
+ }).catch((err) => {
236
+ console.error("ERROR", err)
237
+ setState(ERROR);
238
+ setErrorMessage(err.message);
239
+ showErrorMessage.value = true;
240
+ }).finally(() => loading.value = false);
241
+ emit('fileUploaded', uploadedFile.value);
242
+ return uploadedFile.value;
243
+ } else {
244
+ setErrorFileExceeded();
245
+ }
246
+ }
247
+
248
+ const resetUpload = () => {
249
+ showErrorMessage.value = false;
250
+ setState(INITIAL);
251
+ }
252
+
253
+ const setErrorFileExceeded = () => {
254
+ setState(ERROR);
255
+ setErrorMessage(`${t("media.file.fileSizeExceeded")} ${maxFileSize.value} Mb`);
256
+ showErrorMessage.value = true;
257
+ }
258
+
259
+ const setState = (newState) => {
260
+ state.value = newState;
261
+ }
262
+
263
+ const setErrorMessage = (msg) => {
264
+ errorMessage.value = msg;
265
+ }
266
+
267
+ onMounted(() => {
268
+ findUserStorage();
269
+ })
270
+ </script>
271
+
272
+ <style scoped>
273
+
274
+ </style>
@@ -0,0 +1,4 @@
1
+ import FileUploadExpiration from "./FileUploadExpiration";
2
+
3
+ export {FileUploadExpiration}
4
+ export default FileUploadExpiration
@@ -0,0 +1,208 @@
1
+ <template>
2
+ <div>
3
+
4
+ <input type="file"
5
+ style="display: none"
6
+ ref="fileInput"
7
+ :accept="accept"
8
+ @change="onFilePicked"
9
+ />
10
+
11
+ <v-menu
12
+ v-model="showErrorMessage"
13
+ :min-width="200"
14
+ :close-on-content-click="false"
15
+ :close-on-click="false"
16
+ offset-x
17
+ >
18
+ <template v-slot:activator="{ props }">
19
+ <v-btn @click="pickFile()"
20
+ fab
21
+ :color="getState.color"
22
+ :loading="loading"
23
+ :size="xLarge ? 'x-large' : undefined"
24
+ v-bind="props"
25
+ >
26
+ <v-avatar v-if="isImage">
27
+ <v-img :src="getSrc" alt="image"/>
28
+ </v-avatar>
29
+ <v-icon v-else-if="isAudio">mdi-headset</v-icon>
30
+ <v-icon v-else-if="isVideo">mdi-videocam</v-icon>
31
+ <v-icon v-else>{{ getState.icon }}</v-icon>
32
+ </v-btn>
33
+ </template>
34
+
35
+ <v-card :style="{width: '280px'}" elevation="0">
36
+ <v-card-text class="pb-0 pa-0">
37
+ <v-alert class="mb-0" border="start" type="error" variant="outlined">
38
+ {{ errorMessage }}
39
+ </v-alert>
40
+ </v-card-text>
41
+ <v-card-actions class="justify-center">
42
+ <v-btn text color="primary" @click="resetUpload" class="ml-2">OK</v-btn>
43
+ </v-card-actions>
44
+ </v-card>
45
+
46
+ </v-menu>
47
+ </div>
48
+ </template>
49
+
50
+ <script setup>
51
+ import { ref, computed, onMounted } from 'vue'
52
+ import { useI18n } from 'vue-i18n'
53
+ import uploadProvider from "../../providers/UploadProvider";
54
+ import UserStorageProvider from "../../providers/UserStorageProvider"
55
+
56
+ const { t } = useI18n()
57
+
58
+ const props = defineProps({
59
+ autoSubmit: {type: Boolean, default: false},
60
+ accept: {type: String, default: '*'},
61
+ xLarge: {type: Boolean, default: false},
62
+ })
63
+
64
+ const emit = defineEmits(['fileUploaded'])
65
+
66
+ const INITIAL = 'initial'
67
+ const SELECTED = 'selected'
68
+ const UPLOADED = 'uploaded'
69
+ const ERROR = 'error'
70
+
71
+ const dialog = ref(false)
72
+ const dialogTitle = ref(null)
73
+ const title = 'media.file.creating'
74
+ const showErrorMessage = ref(false)
75
+ const errorMessage = ref('')
76
+ const inputErrors = ref({})
77
+ const file = ref(null)
78
+ const type = ref(null)
79
+ const uploadedFile = ref(null)
80
+ const state = ref(INITIAL)
81
+ const maxFileSize = ref(0)
82
+ const loading = ref(false)
83
+ const fileInput = ref(null)
84
+
85
+ const states = {
86
+ initial: {
87
+ color: 'blue-grey',
88
+ icon: 'mdi-cloud-upload'
89
+ },
90
+ selected: {
91
+ color: 'cyan-darken-3',
92
+ icon: 'mdi-publish'
93
+ },
94
+ loading: {
95
+ color: 'amber-darken-3',
96
+ icon: ''
97
+ },
98
+ uploaded: {
99
+ color: 'green-darken-3',
100
+ icon: 'mdi-check'
101
+ },
102
+ error: {
103
+ color: 'red-darken-3',
104
+ icon: 'mdi-alert'
105
+ }
106
+ }
107
+
108
+ const getState = computed(() => {
109
+ if (loading.value) return states.loading
110
+ return states[state.value]
111
+ })
112
+
113
+ const isImage = computed(() => {
114
+ return (uploadedFile.value && uploadedFile.value.type === 'image') ? true : false
115
+ })
116
+
117
+ const isAudio = computed(() => {
118
+ return (uploadedFile.value && uploadedFile.value.type === 'audio') ? true : false
119
+ })
120
+
121
+ const isVideo = computed(() => {
122
+ return (uploadedFile.value && uploadedFile.value.type === 'video') ? true : false
123
+ })
124
+
125
+ const getSrc = computed(() => {
126
+ if (uploadedFile.value && uploadedFile.value.url) {
127
+ return uploadedFile.value.url
128
+ }
129
+ return null
130
+ })
131
+
132
+ const pickFile = () => {
133
+ if (state.value === INITIAL) {
134
+ fileInput.value.click()
135
+ } else if (state.value === SELECTED) {
136
+ upload()
137
+ } else if (state.value === UPLOADED || state.value === ERROR) {
138
+ dialog.value = true
139
+ }
140
+ }
141
+
142
+ const onFilePicked = (e) => {
143
+ file.value = e.target.files[0]
144
+ state.value = SELECTED
145
+ const fileSize = e.target.files[0].size ? e.target.files[0].size / (1024 * 1024) : null;
146
+ if (props.autoSubmit) {
147
+ upload(fileSize)
148
+ }
149
+ }
150
+
151
+ const findUserStorage = () => {
152
+ return UserStorageProvider.findUserStorageByUser().then((res) => {
153
+ if (res.data.userStorageFindByUser && res.data.userStorageFindByUser.maxFileSize) {
154
+ maxFileSize.value = res.data.userStorageFindByUser.maxFileSize;
155
+ }
156
+ }).catch(
157
+ err => console.error(err)
158
+ )
159
+ }
160
+
161
+ const upload = (fileSize) => {
162
+ if (file.value && state.value != UPLOADED && fileSize <= maxFileSize.value) {
163
+ loading.value = true
164
+ uploadProvider.uploadFile(file.value)
165
+ .then(result => {
166
+ setState(UPLOADED);
167
+ uploadedFile.value = result.data.fileUpload
168
+ emit('fileUploaded', result.data.fileUpload)
169
+ })
170
+ .catch(err => {
171
+ console.log("ERROR", err)
172
+ setState(ERROR);
173
+ setErrorMessage(err.message)
174
+ showErrorMessage.value = true
175
+ })
176
+ .finally(() => loading.value = false)
177
+ } else {
178
+ setErrorFileExceeded()
179
+ }
180
+ }
181
+
182
+ const resetUpload = () => {
183
+ showErrorMessage.value = false;
184
+ setState(INITIAL);
185
+ }
186
+
187
+ const setErrorFileExceeded = () => {
188
+ setState(ERROR);
189
+ setErrorMessage(`${t("media.file.fileSizeExceeded")} ${maxFileSize.value} Mb`);
190
+ showErrorMessage.value = true;
191
+ }
192
+
193
+ const setState = (newState) => {
194
+ state.value = newState;
195
+ }
196
+
197
+ const setErrorMessage = (msg) => {
198
+ errorMessage.value = msg;
199
+ }
200
+
201
+ onMounted(() => {
202
+ findUserStorage();
203
+ })
204
+ </script>
205
+
206
+ <style scoped>
207
+
208
+ </style>
@@ -0,0 +1,4 @@
1
+ import FileUploadExpress from "./FileUploadExpress";
2
+
3
+ export {FileUploadExpress}
4
+ export default FileUploadExpress