diff options
| author | Zephyrrus <[email protected]> | 2020-07-20 23:01:45 +0300 |
|---|---|---|
| committer | Zephyrrus <[email protected]> | 2020-07-20 23:01:45 +0300 |
| commit | 18bb451f793677a5bbfdc2c14128bae33c66dfde (patch) | |
| tree | 12e5e02d793b11bfac3dafd5078a1f0b2464d6ce /src/site/components | |
| parent | fix: return the edited/changed/delete entity from API (diff) | |
| download | host.fuwn.me-18bb451f793677a5bbfdc2c14128bae33c66dfde.tar.xz host.fuwn.me-18bb451f793677a5bbfdc2c14128bae33c66dfde.zip | |
feat: implement all-in-one file detail viewer, tag editor and album selection modal
Diffstat (limited to 'src/site/components')
| -rw-r--r-- | src/site/components/grid/Grid.vue | 71 | ||||
| -rw-r--r-- | src/site/components/grid/waterfall/Waterfall.vue | 3 | ||||
| -rw-r--r-- | src/site/components/image-modal/AlbumInfo.vue | 73 | ||||
| -rw-r--r-- | src/site/components/image-modal/ImageInfo.vue | 61 | ||||
| -rw-r--r-- | src/site/components/image-modal/TagInfo.vue | 103 |
5 files changed, 252 insertions, 59 deletions
diff --git a/src/site/components/grid/Grid.vue b/src/site/components/grid/Grid.vue index 1e427fc..d29160f 100644 --- a/src/site/components/grid/Grid.vue +++ b/src/site/components/grid/Grid.vue @@ -6,6 +6,7 @@ <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"> @@ -66,27 +67,22 @@ @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="icon-web-code" /> + <i class="mdi mdi-open-in-new" /> </a> </b-tooltip> - <b-tooltip label="Tags" position="is-top"> - <a class="btn" @click="false && manageTags(item)"> - <i class="icon-ecommerce-tag-c" /> - </a> - </b-tooltip> - <b-tooltip label="Albums" position="is-top"> + <b-tooltip label="Edit" position="is-top"> <a class="btn" @click="handleFileModal(item)"> - <i class="icon-interface-window" /> + <i class="mdi mdi-pencil" /> </a> </b-tooltip> <b-tooltip label="Delete" position="is-top"> <a class="btn" @click="deleteFile(item)"> - <i class="icon-editorial-trash-a-l" /> + <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="icon-interface-more" /> + <i class="mdi mdi-dots-horizontal" /> </nuxt-link> </b-tooltip> </div> @@ -120,19 +116,19 @@ </b-table-column> <b-table-column field="purge" centered> - <b-tooltip label="Albums" position="is-top"> + <b-tooltip label="Edit" position="is-top"> <a class="btn" @click="handleFileModal(props.row)"> - <i class="icon-interface-window" /> + <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="icon-editorial-trash-a-l" /> + <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="icon-interface-more" /> + <i class="mdi mdi-dots-horizontal" /> </nuxt-link> </b-tooltip> </b-table-column> @@ -159,33 +155,10 @@ Load more </button> </div> - <b-modal :active.sync="isAlbumsModalActive" scroll="keep"> - <ImageInfo :file="modalData.file" /> + + <b-modal class="imageinfo-modal" :active.sync="isAlbumsModalActive"> + <ImageInfo :file="modalData.file" :albums="modalData.albums" :tags="modalData.tags" /> </b-modal> - <!-- <b-modal :active.sync="isAlbumsModalActive" :width="640" scroll="keep"> - <div class="card albumsModal"> - <div class="card-content"> - <div class="content"> - <h3 class="subtitle"> - Select the albums this file should be a part of - </h3> - <hr> - - <div class="albums-container"> - <div v-for="album in albums" :key="album.id" class="album"> - <div class="field"> - <b-checkbox - :value="isAlbumSelected(album.id)" - @input="albumCheckboxClicked($event, album.id)"> - {{ album.name }} - </b-checkbox> - </div> - </div> - </div> - </div> - </div> - </div> - </b-modal> --> </div> </template> @@ -263,7 +236,9 @@ export default { }, }, created() { + // TODO: Create a middleware for this this.getAlbums(); + this.getTags(); }, methods: { async search() { @@ -348,6 +323,13 @@ export default { 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; @@ -504,4 +486,13 @@ 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; +} </style> diff --git a/src/site/components/grid/waterfall/Waterfall.vue b/src/site/components/grid/waterfall/Waterfall.vue index 79a330a..762cbbd 100644 --- a/src/site/components/grid/waterfall/Waterfall.vue +++ b/src/site/components/grid/waterfall/Waterfall.vue @@ -1,7 +1,7 @@ <template> <div ref="waterfall" class="waterfall"> <WaterfallItem - v-for="(item, index) in items" + v-for="item in items" :key="item.id" :style="{ width: `${itemWidth}px`, marginBottom: `${gutterHeight}px` }" :width="itemWidth"> @@ -13,6 +13,7 @@ import WaterfallItem from './WaterfallItem.vue'; const isBrowser = typeof window !== 'undefined'; +// eslint-disable-next-line global-require const Masonry = isBrowser ? window.Masonry || require('masonry-layout') : null; const imagesloaded = isBrowser ? require('imagesloaded') : null; diff --git a/src/site/components/image-modal/AlbumInfo.vue b/src/site/components/image-modal/AlbumInfo.vue new file mode 100644 index 0000000..9d57e55 --- /dev/null +++ b/src/site/components/image-modal/AlbumInfo.vue @@ -0,0 +1,73 @@ +<template> + <b-dropdown + v-model="selectedOptions" + multiple + expanded + scrollable + inline + aria-role="list" + max-height="500px"> + <button slot="trigger" class="button is-primary" type="button"> + <span>Albums ({{ selectedOptions.length }})</span> + <b-icon icon="menu-down" /> + </button> + + <b-dropdown-item + v-for="album in albums" + :key="album.id" + :value="album.id" + aria-role="listitem" + @click="handleClick(album.id)"> + <span>{{ album. name }}</span> + </b-dropdown-item> + </b-dropdown> +</template> + +<script> +import { mapState } from 'vuex'; + +export default { + name: 'Albuminfo', + props: { + imageId: { + type: Number, + default: 0, + }, + imageAlbums: { + type: Array, + default: () => [], + }, + }, + data() { + return { + selectedOptions: this.imageAlbums.map((e) => e.id), + }; + }, + computed: { + ...mapState({ + albums: (state) => state.albums.tinyDetails, + }), + }, + methods: { + isAlbumSelected(id) { + if (!this.showingModalForFile) return false; + const found = this.showingModalForFile.albums.find((el) => el.id === id); + return !!(found && found.id); + }, + async handleClick(id) { + // here the album should be already removed from the selected list + if (this.selectedOptions.indexOf(id) > -1) { + this.$handler.executeAction('images/addToAlbum', { + albumId: id, + fileId: this.imageId, + }); + } else { + this.$handler.executeAction('images/removeFromAlbum', { + albumId: id, + fileId: this.imageId, + }); + } + }, + }, +}; +</script> diff --git a/src/site/components/image-modal/ImageInfo.vue b/src/site/components/image-modal/ImageInfo.vue index c9dba1a..c3f0041 100644 --- a/src/site/components/image-modal/ImageInfo.vue +++ b/src/site/components/image-modal/ImageInfo.vue @@ -1,10 +1,13 @@ <template> <div class="container has-background-lolisafe"> <div class="columns is-marginless"> - <div class="column fucking-opl-shut-up"> - <img src="https://placehold.it/1024x10024"> + <div class="column image-col has-centered-items"> + <img v-if="!isVideo(file.type)" class="col-img" :src="file.url"> + <video v-else class="col-vid" controls> + <source :src="file.url" :type="file.type"> + </video> </div> - <div class="column is-one-third"> + <div class="column data-col is-one-third"> <div class="sticky"> <div class="divider is-lolisafe has-text-light"> File information @@ -90,21 +93,16 @@ <span class="fake-input"><timeago :since="file.createdAt" /></span> </div> </b-field> + <div class="divider is-lolisafe has-text-light"> - Albums + Tags </div> + <Taginfo :imageId="file.id" :imageTags="tags" /> <div class="divider is-lolisafe has-text-light"> - Tags + Albums </div> - <b-field label="Add some tags"> - <b-taginput - v-model="tags" - class="lolisafe" - ellipsis - icon="label" - placeholder="Add a tag" /> - </b-field> + <Albuminfo :imageId="file.id" :imageAlbums="albums" /> </div> </div> </div> @@ -114,17 +112,27 @@ <script> import { mapState } from 'vuex'; +import Albuminfo from './AlbumInfo.vue'; +import Taginfo from './TagInfo.vue'; + export default { + components: { + Taginfo, + Albuminfo, + }, props: { file: { type: Object, default: () => ({}), }, - }, - data() { - return { - tags: [], - }; + albums: { + type: Array, + default: () => ([]), + }, + tags: { + type: Array, + default: () => ([]), + }, }, computed: mapState(['images']), methods: { @@ -139,6 +147,9 @@ export default { return `${parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`; }, + isVideo(type) { + return type.startsWith('video'); + }, }, }; </script> @@ -176,4 +187,18 @@ export default { .divider:first-child { margin: 10px 0 25px; } + +.col-vid { + width: 100%; +} + +.image-col { + align-items: start; +} + +.data-col { + @media screen and (min-width: 769px) { + padding-right: 1.5rem; + } +} </style> diff --git a/src/site/components/image-modal/TagInfo.vue b/src/site/components/image-modal/TagInfo.vue new file mode 100644 index 0000000..2054e6a --- /dev/null +++ b/src/site/components/image-modal/TagInfo.vue @@ -0,0 +1,103 @@ +<template> + <b-field label="Add some tags"> + <b-taginput + :value="selectedTags" + :data="filteredTags" + class="lolisafe taginp" + ellipsis + icon="label" + placeholder="Add a tag" + autocomplete + allow-new + @typing="getFilteredTags" + @add="tagAdded" + @remove="tagRemoved" /> + </b-field> +</template> + +<script> +import { mapState } from 'vuex'; + +export default { + name: 'Taginfo', + props: { + imageId: { + type: Number, + default: 0, + }, + imageTags: { + type: Array, + default: () => [], + }, + }, + data() { + return { + filteredTags: [], + }; + }, + computed: { + ...mapState({ + tags: (state) => state.tags.tagsList, + }), + selectedTags() { return this.imageTags.map((e) => e.name); }, + lowercaseTags() { return this.imageTags.map((e) => e.name.toLowerCase()); }, + }, + methods: { + getFilteredTags(str) { + this.filteredTags = this.tags.map((e) => e.name).filter((e) => { + // check if the search string matches any of the tags + const sanitezedTag = e.toString().toLowerCase(); + const matches = sanitezedTag.indexOf(str.toLowerCase()) >= 0; + + // check if this tag is already added to our image, to avoid duplicates + if (matches) { + const foundIndex = this.lowercaseTags.indexOf(sanitezedTag); + if (foundIndex === -1) { + return true; + } + } + + return false; + }); + }, + async tagAdded(tag) { + if (!tag) return; + + // normalize into NFC form (diactirics and moonrunes) + // replace all whitespace with _ + // replace multiple __ with a single one + tag = tag.normalize('NFC').replace(/\s/g, '_').replace(/_+/g, '_'); + + const foundIndex = this.tags.findIndex(({ name }) => name === tag); + + if (foundIndex === -1) { + await this.$handler.executeAction('tags/createTag', tag); + } + + await this.$handler.executeAction('images/addTag', { fileId: this.imageId, tagName: tag }); + }, + tagRemoved(tag) { + this.$handler.executeAction('images/removeTag', { fileId: this.imageId, tagName: tag }); + }, + }, +}; +</script> + +<style lang="scss" scoped> +@import '~/assets/styles/_colors.scss'; + +.taginp { + ::v-deep .dropdown-content { + background-color: hsl(0, 0%, 100%); + + .dropdown-item { + color: hsl(0, 0%, 29%); + + &:hover { + color: hsl(0, 0%, 4%); + background-color: hsl(0, 0%, 90%); + } + } + } +} +</style> |