aboutsummaryrefslogtreecommitdiff
path: root/src/site/components/image-modal
diff options
context:
space:
mode:
Diffstat (limited to 'src/site/components/image-modal')
-rw-r--r--src/site/components/image-modal/AlbumInfo.vue92
-rw-r--r--src/site/components/image-modal/ImageInfo.vue210
-rw-r--r--src/site/components/image-modal/TagInfo.vue95
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>