diff options
Diffstat (limited to 'src/site/components/grid/Grid.vue')
| -rw-r--r-- | src/site/components/grid/Grid.vue | 498 |
1 files changed, 498 insertions, 0 deletions
diff --git a/src/site/components/grid/Grid.vue b/src/site/components/grid/Grid.vue new file mode 100644 index 0000000..9e1ce6f --- /dev/null +++ b/src/site/components/grid/Grid.vue @@ -0,0 +1,498 @@ +<template> + <div> + <nav class="level"> + <div class="level-left"> + <div class="level-item"> + <slot name="pagination" /> + </div> + </div> + <!-- TODO: Externalize this so it can be saved as an user config (and between re-renders) --> + <div v-if="enableToolbar" class="level-right toolbar"> + <div class="level-item"> + <div class="block"> + <b-radio v-model="showList" name="name" :native-value="true"> + List + </b-radio> + <b-radio v-model="showList" name="name" :native-value="false"> + Grid + </b-radio> + </div> + </div> + </div> + </nav> + + <template v-if="!images.showList"> + <Waterfall + :gutter-width="10" + :gutter-height="4" + :options="{fitWidth: true}" + :item-width="width" + :items="gridFiles"> + <template v-slot="{item}"> + <template v-if="isPublic"> + <a + :href="`${item.url}`" + class="preview-container" + target="_blank" + @mouseenter.self.stop.prevent="item.preview && mouseOver(item.id)" + @mouseleave.self.stop.prevent="item.preview && mouseOut(item.id)"> + + <img :src="item.thumb ? item.thumb : blank"> + <div v-if="item.preview && isHovered(item.id)" class="preview"> + <video ref="video" class="preview" autoplay loop muted> + <source :src="item.preview" type="video/mp4"> + </video> + </div> + + <span v-if="!item.thumb && item.name" class="extension">{{ + item.name.split('.').pop() + }}</span> + </a> + </template> + <template v-else> + <img :src="item.thumb ? item.thumb : blank"> + <div v-if="item.preview && isHovered(item.id)" class="preview"> + <video ref="video" class="preview" autoplay loop muted> + <source :src="item.preview" type="video/mp4"> + </video> + </div> + + <span v-if="!item.thumb && item.name" class="extension">{{ item.name.split('.').pop() }}</span> + <div + v-if="!isPublic" + :class="{ fixed }" + class="actions" + @mouseenter.self.stop.prevent="item.preview && mouseOver(item.id)" + @mouseleave.self.stop.prevent="item.preview && mouseOut(item.id)"> + <b-tooltip label="Link" position="is-top"> + <a :href="`${item.url}`" target="_blank" class="btn"> + <i class="mdi mdi-open-in-new" /> + </a> + </b-tooltip> + <b-tooltip label="Edit" position="is-top"> + <a class="btn" @click="handleFileModal(item)"> + <i class="mdi mdi-pencil" /> + </a> + </b-tooltip> + <b-tooltip label="Delete" position="is-top"> + <a class="btn" @click="deleteFile(item)"> + <i class="mdi mdi-delete" /> + </a> + </b-tooltip> + <b-tooltip v-if="user && user.isAdmin" label="More info" position="is-top" class="more"> + <nuxt-link :to="`/dashboard/admin/file/${item.id}`"> + <i class="mdi mdi-dots-horizontal" /> + </nuxt-link> + </b-tooltip> + </div> + </template> + </template> + </Waterfall> + </template> + <div v-else> + <b-table :data="gridFiles || []" :mobile-cards="true"> + <b-table-column v-slot="props" field="url" label="URL"> + <a :href="props.row.url" target="_blank">{{ props.row.url }}</a> + </b-table-column> + + <b-table-column v-slot="props" field="albums" label="Albums" centered> + <template v-for="(album, index) in props.row.albums"> + <nuxt-link :key="index" :to="`/dashboard/albums/${album.id}`"> + {{ album.name }} + </nuxt-link> + <template v-if="index < props.row.albums.length - 1"> + , + </template> + </template> + + {{ props.row.username }} + </b-table-column> + + <b-table-column v-slot="props" field="uploaded" label="Uploaded" centered> + <span><timeago :since="props.row.createdAt" /></span> + </b-table-column> + + <b-table-column v-slot="props" field="purge" centered> + <b-tooltip label="Edit" position="is-top"> + <a class="btn" @click="handleFileModal(props.row)"> + <i class="mdi mdi-pencil" /> + </a> + </b-tooltip> + <b-tooltip label="Delete" position="is-top" class="is-danger"> + <a class="is-danger" @click="deleteFile(props.row)"> + <i class="mdi mdi-delete" /> + </a> + </b-tooltip> + <b-tooltip v-if="user && user.isAdmin" label="More info" position="is-top" class="more"> + <nuxt-link :to="`/dashboard/admin/file/${props.row.id}`"> + <i class="mdi mdi-dots-horizontal" /> + </nuxt-link> + </b-tooltip> + </b-table-column> + + <template slot="empty"> + <div class="has-text-centered"> + <i class="icon-misc-mood-sad" /> + </div> + <div class="has-text-centered"> + Nothing here + </div> + </template> + <template slot="footer"> + <div class="has-text-right has-text-default"> + Showing {{ files.length }} files ({{ total }} total) + </div> + </template> + </b-table> + </div> + + <b-modal class="imageinfo-modal" :active.sync="isAlbumsModalActive"> + <ImageInfo :file="modalData.file" :albums="modalData.albums" :tags="modalData.tags" /> + </b-modal> + </div> +</template> + +<script> +import { mapState } from 'vuex'; + +import Waterfall from './waterfall/Waterfall.vue'; +import ImageInfo from '~/components/image-modal/ImageInfo.vue'; + +export default { + components: { + Waterfall, + ImageInfo + }, + props: { + files: { + 'type': Array, + 'default': () => [] + }, + total: { + 'type': Number, + 'default': 0 + }, + fixed: { + 'type': Boolean, + 'default': false + }, + isPublic: { + 'type': Boolean, + 'default': false + }, + width: { + 'type': Number, + 'default': 150 + }, + enableSearch: { + 'type': Boolean, + 'default': true + }, + enableToolbar: { + 'type': Boolean, + 'default': true + } + }, + data() { + return { + searchTerm: null, + showList: false, + hoveredItems: [], + isAlbumsModalActive: false, + showingModalForFile: null, + filesOffsetWaterfall: 0, + filesOffsetEndWaterfall: 50, + filesPerPageWaterfall: 50, + modalData: { + file: null, + tags: null, + albums: null + } + }; + }, + computed: { + ...mapState({ + user: state => state.auth.user, + albums: state => state.albums.tinyDetails, + images: state => state.images + }), + blank() { + return require('@/assets/images/blank.png'); + }, + gridFiles() { + return (this.files || []).filter(v => !v.hideFromList); + } + }, + watch: { + showList: 'displayTypeChange' + }, + created() { + // TODO: Create a middleware for this + this.getAlbums(); + this.getTags(); + + this.showList = this.images.showList; + }, + methods: { + async search() { + const data = await this.$search.do(this.searchTerm, ['name', 'original', 'type', 'albums:name']); + console.log('> Search result data', data); // eslint-disable-line no-console + }, + deleteFile(file) { + // this.$emit('delete', file); + this.$buefy.dialog.confirm({ + title: 'Deleting file', + message: 'Are you sure you want to <b>delete</b> this file?', + confirmText: 'Delete File', + type: 'is-danger', + onConfirm: async () => { + try { + const response = await this.$store.dispatch('images/deleteFile', file.id); + + this.$buefy.toast.open(response.message); + } catch (e) { + this.$store.dispatch('alert/set', { text: e.message, error: true }, { root: true }); + } + } + }); + }, + isAlbumSelected(id) { + if (!this.showingModalForFile) return false; + const found = this.showingModalForFile.albums.find(el => el.id === id); + return Boolean(found && found.id); + }, + async openAlbumModal(file) { + const { id } = file; + this.showingModalForFile = file; + this.showingModalForFile.albums = []; + + try { + await this.$store.dispatch('images/getFileAlbums', id); + } catch (e) { + this.$store.dispatch('alert/set', { text: e.message, error: true }, { root: true }); + } + this.showingModalForFile.albums = this.images.fileAlbumsMap[id]; + + this.isAlbumsModalActive = true; + }, + async albumCheckboxClicked(add, id) { + try { + let response; + if (add) { + response = await this.$store.dispatch('images/addToAlbum', { + albumId: id, + fileId: this.showingModalForFile.id + }); + } else { + response = await this.$store.dispatch('images/removeFromAlbum', { + albumId: id, + fileId: this.showingModalForFile.id + }); + } + + this.$buefy.toast.open(response.message); + } catch (e) { + this.$store.dispatch('alert/set', { text: e.message, error: true }, { root: true }); + } + }, + async getAlbums() { + try { + await this.$store.dispatch('albums/getTinyDetails'); + } catch (e) { + this.$store.dispatch('alert/set', { text: e.message, error: true }, { root: true }); + } + }, + async handleFileModal(file) { + const { id } = file; + + try { + await this.$store.dispatch('images/fetchFileMeta', id); + this.modalData.file = this.images.fileExtraInfoMap[id]; + this.modalData.albums = this.images.fileAlbumsMap[id]; + this.modalData.tags = this.images.fileTagsMap[id]; + } catch (e) { + this.$store.dispatch('alert/set', { text: e.message, error: true }, { root: true }); + } + + this.isAlbumsModalActive = true; + }, + async getTags() { + try { + await this.$store.dispatch('tags/fetch'); + } catch (e) { + this.$store.dispatch('alert/set', { text: e.message, error: true }, { root: true }); + } + }, + mouseOver(id) { + const foundIndex = this.hoveredItems.indexOf(id); + if (foundIndex > -1) return; + this.hoveredItems.push(id); + }, + mouseOut(id) { + const foundIndex = this.hoveredItems.indexOf(id); + if (foundIndex > -1) this.hoveredItems.splice(foundIndex, 1); + }, + isHovered(id) { + return this.hoveredItems.includes(id); + }, + displayTypeChange(showList) { + this.$store.commit('images/setShowList', showList); + } + } +}; +</script> + +<style lang="scss" scoped> +@import '~/assets/styles/_colors.scss'; +.item-move { + transition: all 0.25s cubic-bezier(0.55, 0, 0.1, 1); +} + +div.toolbar { + padding: 1rem; + + .block { + text-align: right; + } +} + +span.extension { + position: absolute; + width: 100%; + height: 100%; + z-index: 0; + top: 0; + left: 0; + display: flex; + align-items: center; + justify-content: center; + font-size: 2rem; + pointer-events: none; + opacity: 0.75; + max-width: 150px; +} + +div.preview { + position: absolute; + top: 0px; + left: 0px; + width: 100%; + height: calc(100% - 6px); + overflow: hidden; +} + +.preview-container { + display: inline-block; +} + +div.actions { + opacity: 0; + transition: opacity 0.1s linear; + position: absolute; + top: 0px; + left: 0px; + width: 100%; + height: calc(100% - 6px); + // background: rgba(0, 0, 0, 0.5); + background: linear-gradient(to top, rgba(0, 0, 0, 0.5) 0px, rgba(0, 0, 0, 0) 60px), + linear-gradient(to bottom, rgba(0, 0, 0, 0.5) 0px, rgba(0, 0, 0, 0) 45px); + display: flex; + justify-content: center; + align-items: flex-end; + + span { + padding: 3px; + + &.more { + position: absolute; + top: 0; + right: 0; + } + + &:nth-child(1), + &:nth-child(2) { + align-items: flex-end; + padding-bottom: 10px; + } + + &:nth-child(3), + &:nth-child(4) { + justify-content: flex-end; + padding-bottom: 10px; + } + + a { + width: 30px; + height: 30px; + color: white; + justify-content: center; + align-items: center; + display: flex; + &.btn:before { + content: ''; + width: 30px; + height: 30px; + border: 1px solid white; + border-radius: 50%; + position: absolute; + } + } + } + + &.fixed { + position: relative; + opacity: 1; + background: none; + + a { + width: auto; + height: auto; + color: $defaultTextColor; + &:before { + display: none; + } + } + } +} + +.albums-container { + display: flex; + flex-direction: row; + flex-wrap: wrap; + .album { + flex-basis: 33%; + text-align: left; + } +} + +.hidden { + display: none; +} + +.waterfall { + margin: 0 auto; +} + +.waterfall-item:hover { + div.actions { + opacity: 1; + } +} + +.imageinfo-modal::-webkit-scrollbar { + width: 0px; /* Remove scrollbar space */ + background: transparent; /* Optional: just make scrollbar invisible */ +} + +i.mdi { + font-size: 16px; +} + +.imageinfo-modal{ + ::v-deep .modal-content { + @media screen and (max-width: 768px) { + min-height: 100vh; + } + } +} +</style> |