aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/site/assets/styles/_colors.scss12
-rw-r--r--src/site/assets/styles/style.scss6
-rw-r--r--src/site/components/grid/Grid.vue71
-rw-r--r--src/site/components/grid/waterfall/Waterfall.vue3
-rw-r--r--src/site/components/image-modal/AlbumInfo.vue73
-rw-r--r--src/site/components/image-modal/ImageInfo.vue61
-rw-r--r--src/site/components/image-modal/TagInfo.vue103
-rw-r--r--src/site/store/images.js27
-rw-r--r--src/site/store/tags.js40
9 files changed, 336 insertions, 60 deletions
diff --git a/src/site/assets/styles/_colors.scss b/src/site/assets/styles/_colors.scss
index 0bc7c5e..e8b17c4 100644
--- a/src/site/assets/styles/_colors.scss
+++ b/src/site/assets/styles/_colors.scss
@@ -69,3 +69,15 @@ $sidebar-box-shadow: none;
$menu-item-color: $textColor;
$menu-item-hover-color: $textColorHighlight;
$menu-item-active-background-color: $backgroundAccent;
+
+// dropdown
+$dropdown-content-background-color: $background;
+$dropdown-item-hover-background-color: $backgroundAccentLighter;
+$dropdown-item-color: $textColor;
+$dropdown-item-hover-color: $textColorHighlight;
+$dropdown-item-active-color: $textColorHighlight;
+$dropdown-item-active-background-color: hsl(171, 100%, 41%); // $primary
+
+// tags
+$tag-background-color: $base-2;
+$tag-color: $textColor;
diff --git a/src/site/assets/styles/style.scss b/src/site/assets/styles/style.scss
index 026f277..4ad0471 100644
--- a/src/site/assets/styles/style.scss
+++ b/src/site/assets/styles/style.scss
@@ -360,7 +360,7 @@ table.table {
margin: 0 !important;
}
-.fucking-opl-shut-up {
+.has-centered-items {
display: flex;
justify-content: center;
align-items: center;
@@ -399,3 +399,7 @@ table.table {
}
}
}
+
+.dropdown-content a {
+ text-decoration: none;
+}
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>
diff --git a/src/site/store/images.js b/src/site/store/images.js
index 0d5e82a..be04c8a 100644
--- a/src/site/store/images.js
+++ b/src/site/store/images.js
@@ -93,6 +93,20 @@ export const actions = {
return response;
},
+ async addTag({ commit }, { fileId, tagName }) {
+ const response = await this.$axios.$post('file/tag/add', { fileId, tagName });
+
+ commit('addTagToFile', response.data);
+
+ return response;
+ },
+ async removeTag({ commit }, { fileId, tagName }) {
+ const response = await this.$axios.$post('file/tag/del', { fileId, tagName });
+
+ commit('removeTagFromFile', response.data);
+
+ return response;
+ },
};
export const mutations = {
@@ -138,6 +152,19 @@ export const mutations = {
state.fileAlbumsMap[fileId].splice(foundIndex, 1);
}
},
+ addTagToFile(state, { fileId, tag }) {
+ if (!state.fileTagsMap[fileId]) return;
+
+ state.fileTagsMap[fileId].push(tag);
+ },
+ removeTagFromFile(state, { fileId, tag }) {
+ if (!state.fileTagsMap[fileId]) return;
+
+ const foundIndex = state.fileTagsMap[fileId].findIndex(({ id }) => id === tag.id);
+ if (foundIndex > -1) {
+ state.fileTagsMap[fileId].splice(foundIndex, 1);
+ }
+ },
resetState(state) {
Object.assign(state, getDefaultState());
},
diff --git a/src/site/store/tags.js b/src/site/store/tags.js
new file mode 100644
index 0000000..c06b741
--- /dev/null
+++ b/src/site/store/tags.js
@@ -0,0 +1,40 @@
+export const state = () => ({
+ tagsList: [],
+});
+
+export const actions = {
+ async fetch({ commit }) {
+ const response = await this.$axios.$get('tags');
+
+ commit('setTags', response.tags);
+
+ return response;
+ },
+ async createTag({ commit }, name) {
+ const response = await this.$axios.$post('tag/new', { name });
+
+ commit('addTag', response.data);
+
+ return response;
+ },
+ async deleteTag({ commit }, tagId) {
+ const response = await this.$axios.$delete(`tag/${tagId}`);
+
+ commit('deleteTag', response.data);
+
+ return response;
+ },
+};
+
+export const mutations = {
+ setTags(state, tags) {
+ state.tagsList = tags;
+ },
+ addTag(state, tag) {
+ state.tagsList.unshift(tag);
+ },
+ deleteTag(state, { id: tagId }) {
+ const foundIndex = state.tagsList.findIndex(({ id }) => id === tagId);
+ state.tagsList.splice(foundIndex, 1);
+ },
+};