diff options
| author | Kana <[email protected]> | 2020-12-24 21:41:24 +0900 |
|---|---|---|
| committer | GitHub <[email protected]> | 2020-12-24 21:41:24 +0900 |
| commit | 2412a60bd4cb2364a477a3af79a8c6dcb6b0ddab (patch) | |
| tree | dbf2b2cad342f31849a62089dedd40165758af86 /src/site/components/image-modal | |
| parent | Enable deleting files with the API key (diff) | |
| parent | bug: fix showlist resetting itself every time the page is changed (diff) | |
| download | host.fuwn.me-2412a60bd4cb2364a477a3af79a8c6dcb6b0ddab.tar.xz host.fuwn.me-2412a60bd4cb2364a477a3af79a8c6dcb6b0ddab.zip | |
Merge pull request #228 from Zephyrrus/begone_trailing_commas
Merge own dev branch into main dev branch
Diffstat (limited to 'src/site/components/image-modal')
| -rw-r--r-- | src/site/components/image-modal/AlbumInfo.vue | 92 | ||||
| -rw-r--r-- | src/site/components/image-modal/ImageInfo.vue | 210 | ||||
| -rw-r--r-- | src/site/components/image-modal/TagInfo.vue | 95 |
3 files changed, 397 insertions, 0 deletions
diff --git a/src/site/components/image-modal/AlbumInfo.vue b/src/site/components/image-modal/AlbumInfo.vue new file mode 100644 index 0000000..17c375a --- /dev/null +++ b/src/site/components/image-modal/AlbumInfo.vue @@ -0,0 +1,92 @@ +<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 orderedAlbums" + :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: () => [] + }, + albums: { + type: Array, + default: () => [] + } + }, + data() { + return { + selectedOptions: [], + orderedAlbums: [] + }; + }, + created() { + this.orderedAlbums = this.getOrderedAlbums(); + // we're sorting here instead of computed because we want sort on creation + // then the array's values should be frozen + this.selectedOptions = this.imageAlbums.map((e) => e.id); + }, + methods: { + getOrderedAlbums() { + return [...this.albums].sort( + (a, b) => { + const selectedA = this.imageAlbums.findIndex(({ name }) => name === a.name) !== -1; + const selectedB = this.imageAlbums.findIndex(({ name }) => name === b.name) !== -1; + + if (selectedA !== selectedB) { + return selectedA ? -1 : 1; + } + return a.name.localeCompare(b.name); + } + ); + }, + 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 new file mode 100644 index 0000000..73b6339 --- /dev/null +++ b/src/site/components/image-modal/ImageInfo.vue @@ -0,0 +1,210 @@ +<template> + <div class="container has-background-lolisafe"> + <div class="columns is-marginless"> + <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 data-col is-one-third"> + <div class="sticky"> + <div class="divider is-lolisafe has-text-light"> + File information + </div> + <b-field + label="ID" + label-position="on-border" + type="is-lolisafe" + class="lolisafe-on-border"> + <div class="control"> + <span class="fake-input">{{ file.id }}</span> + </div> + </b-field> + <b-field + label="Name" + label-position="on-border" + type="is-lolisafe" + class="lolisafe-on-border"> + <div class="control"> + <span class="fake-input">{{ file.name }}</span> + </div> + </b-field> + + <b-field + label="Original Name" + label-position="on-border" + type="is-lolisafe" + class="lolisafe-on-border"> + <div class="control"> + <span class="fake-input">{{ file.original }}</span> + </div> + </b-field> + + <b-field + label="IP" + label-position="on-border" + type="is-lolisafe" + class="lolisafe-on-border"> + <div class="control"> + <span class="fake-input">{{ file.ip }}</span> + </div> + </b-field> + + <b-field + label="Link" + label-position="on-border" + type="is-lolisafe" + class="lolisafe-on-border"> + <div class="control"> + <a + class="fake-input" + :href="file.url" + target="_blank">{{ file.url }}</a> + </div> + </b-field> + + <b-field + label="Size" + label-position="on-border" + type="is-lolisafe" + class="lolisafe-on-border"> + <div class="control"> + <span class="fake-input">{{ formatBytes(file.size) }}</span> + </div> + </b-field> + + <b-field + label="Hash" + label-position="on-border" + type="is-lolisafe" + class="lolisafe-on-border"> + <div class="control"> + <span class="fake-input">{{ file.hash }}</span> + </div> + </b-field> + + <b-field + label="Uploaded" + label-position="on-border" + type="is-lolisafe" + class="lolisafe-on-border"> + <div class="control"> + <span class="fake-input"><timeago :since="file.createdAt" /></span> + </div> + </b-field> + + <div class="divider is-lolisafe has-text-light"> + Tags + </div> + <Taginfo :imageId="file.id" :imageTags="tags" /> + + <div class="divider is-lolisafe has-text-light"> + Albums + </div> + <Albuminfo :imageId="file.id" :imageAlbums="albums" :albums="tinyDetails" /> + </div> + </div> + </div> + </div> +</template> + +<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: () => ({}) + }, + albums: { + type: Array, + default: () => ([]) + }, + tags: { + type: Array, + default: () => ([]) + } + }, + computed: mapState({ + images: (state) => state.images, + tinyDetails: (state) => state.albums.tinyDetails + }), + methods: { + formatBytes(bytes, decimals = 2) { + if (bytes === 0) return '0 Bytes'; + + const k = 1024; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return `${parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`; + }, + isVideo(type) { + return type.startsWith('video'); + } + } +}; +</script> + +<style lang="scss" scoped> +@import '~/assets/styles/_colors.scss'; +.modal-content, .modal-card { + max-height: 100%; +} + +.fake-input { + font-size: 1rem !important; + height: 2.5rem; + border-color: #323846; /* $lolisafe */ + max-width: 100%; + width: 100%; + border-radius: 4px; + display: inline-block; + font-size: 1rem; + justify-content: flex-start; + line-height: 1.5; + padding-bottom: calc(0.375em - 1px); + padding-left: calc(0.625em - 1px); + padding-right: calc(0.625em - 1px); + padding-top: calc(0.375em - 1px); + background-color: #21252d; + border: 2px solid #21252d; + border-radius: 0.3em !important; + + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +} + +.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; + } + @media screen and (max-width: 769px) { + padding-bottom: 3rem; + } +} +</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..59d01f5 --- /dev/null +++ b/src/site/components/image-modal/TagInfo.vue @@ -0,0 +1,95 @@ +<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: #323846; + box-shadow: 0 14px 28px rgba(0,0,0,0.25), 0 10px 10px rgba(0,0,0,0.22); + } +} +</style> |