aboutsummaryrefslogtreecommitdiff
path: root/viewer/src/views/layout/left/LayoutProposition.vue
diff options
context:
space:
mode:
Diffstat (limited to 'viewer/src/views/layout/left/LayoutProposition.vue')
-rw-r--r--viewer/src/views/layout/left/LayoutProposition.vue208
1 files changed, 208 insertions, 0 deletions
diff --git a/viewer/src/views/layout/left/LayoutProposition.vue b/viewer/src/views/layout/left/LayoutProposition.vue
new file mode 100644
index 0000000..97dc3a6
--- /dev/null
+++ b/viewer/src/views/layout/left/LayoutProposition.vue
@@ -0,0 +1,208 @@
1<!-- ldgallery - A static generator which turns a collection of tagged
2-- pictures into a searchable web gallery.
3--
4-- Copyright (C) 2019-2022 Guillaume FOUET
5-- 2020 Pacien TRAN-GIRARD
6--
7-- This program is free software: you can redistribute it and/or modify
8-- it under the terms of the GNU Affero General Public License as
9-- published by the Free Software Foundation, either version 3 of the
10-- License, or (at your option) any later version.
11--
12-- This program is distributed in the hope that it will be useful,
13-- but WITHOUT ANY WARRANTY; without even the implied warranty of
14-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15-- GNU Affero General Public License for more details.
16--
17-- You should have received a copy of the GNU Affero General Public License
18-- along with this program. If not, see <https://www.gnu.org/licenses/>.
19-->
20
21<template>
22 <div :class="$style.proposition">
23 <h2
24 v-if="showCategory && Object.keys(propositions).length"
25 :class="[$style.subtitle, $style.category]"
26 >
27 {{ title }}
28 </h2>
29 <div
30 v-for="proposed in proposedTags"
31 :key="proposed.rawTag"
32 >
33 <LdLink
34 :class="$style.operationBtns"
35 :title="t('tag-propositions.substraction')"
36 @click="add(Operation.SUBSTRACTION, proposed.rawTag)"
37 >
38 <fa-icon
39 :icon="faMinus"
40 alt="[-]"
41 />
42 </LdLink>
43
44 <LdLink
45 :class="$style.operationBtns"
46 :title="t('tag-propositions.addition')"
47 @click="add(Operation.ADDITION, proposed.rawTag)"
48 >
49 <fa-icon
50 :icon="faPlus"
51 alt="[+]"
52 />
53 </LdLink>
54
55 <LdLink
56 :class="$style.operationTag"
57 :title="t('tag-propositions.intersection')"
58 @click="add(Operation.INTERSECTION, proposed.rawTag)"
59 >
60 {{ proposed.rawTag }}
61 </LdLink>
62
63 <div
64 class="disabled"
65 :title="t('tag-propositions.item-count')"
66 >
67 {{ proposed.count }}
68 </div>
69 </div>
70 <div
71 v-if="showMoreCount > 0"
72 :class="$style.showmore"
73 @click="limit += showMoreCount"
74 >
75 {{ t("tag-propositions.showmore", [showMoreCount]) }}<fa-icon :icon="faAngleDoubleDown" />
76 </div>
77 </div>
78</template>
79
80<script setup lang="ts">
81import { Item, RawTag } from '@/@types/gallery';
82import { Operation } from '@/@types/operation';
83import { TagIndex, TagNode, TagSearch } from '@/@types/tag';
84import LdLink from '@/components/LdLink.vue';
85import { useGalleryStore } from '@/store/galleryStore';
86import { faAngleDoubleDown, faMinus, faPlus } from '@fortawesome/free-solid-svg-icons';
87import { useVModel } from '@vueuse/core';
88import { computed, PropType, ref, watch } from 'vue';
89import { useI18n } from 'vue-i18n';
90import { useRoute } from 'vue-router';
91
92const props = defineProps({
93 searchFilters: { type: Array<TagSearch>, required: true },
94 currentTags: { type: Array<string>, required: true },
95 tagsIndex: { type: Object as PropType<TagIndex>, required: true },
96 category: { type: Object as PropType<TagNode>, default: null },
97 showCategory: Boolean,
98});
99const emit = defineEmits(['update:searchFilters']);
100const model = useVModel(props, 'searchFilters', emit);
101
102const { t } = useI18n();
103const route = useRoute();
104const galleryStore = useGalleryStore();
105
106const initialTagDisplayLimit = computed(() => {
107 const limit = galleryStore.config?.initialTagDisplayLimit ?? 10;
108 return limit >= 0 ? limit : 1000;
109});
110
111const limit = ref(initialTagDisplayLimit.value);
112
113watch(() => route, () => (limit.value = initialTagDisplayLimit.value));
114
115const propositions = computed<Record<string, number>>(() => {
116 const propositions: Record<string, number> = {};
117 const searchFilters = model.value;
118 if (searchFilters.length > 0) {
119 // Tags count from current search
120 extractDistinctItems(searchFilters)
121 .flatMap(item => item.tags)
122 .map(rightmost)
123 .filter(rawTag => props.tagsIndex[rawTag] && !searchFilters.find(search => search.tag === rawTag))
124 .forEach(rawTag => (propositions[rawTag] = (propositions[rawTag] ?? 0) + 1));
125 } else {
126 // Tags count from the current directory
127 props.currentTags
128 .flatMap(tag => tag.split(':'))
129 .map(tag => props.tagsIndex[tag])
130 .filter(Boolean)
131 .forEach(tagindex => (propositions[tagindex.tag] = tagindex.items.length));
132 }
133 return propositions;
134});
135
136const proposedTags = computed(() => {
137 return Object.entries(propositions.value)
138 .sort((a, b) => b[1] - a[1])
139 .slice(0, limit.value)
140 .map(entry => ({ rawTag: entry[0], count: entry[1] }));
141});
142
143const showMoreCount = computed(() => {
144 return Object.keys(propositions.value).length - Object.keys(proposedTags.value).length;
145});
146
147const title = computed(() => {
148 return props.category?.tag ?? t('panelLeft.propositions.other');
149});
150
151function extractDistinctItems(currentTags: TagSearch[]): Item[] {
152 return [...new Set(currentTags.flatMap(tag => tag.items))];
153}
154
155function rightmost(tag: RawTag): RawTag {
156 const dot = tag.lastIndexOf(':');
157 return dot <= 0 ? tag : tag.substring(dot + 1);
158}
159
160function add(operation: Operation, rawTag: RawTag) {
161 const node = props.tagsIndex[rawTag];
162 const display = props.category ? `${operation}${props.category.tag}:${node.tag}` : `${operation}${node.tag}`;
163 model.value.push({ ...node, parent: props.category, operation, display });
164}
165</script>
166
167<style lang="scss" module>
168@import "~@/assets/scss/theme";
169
170.proposition {
171 .subtitle {
172 background-color: $proposed-category-bgcolor;
173 width: 100%;
174 padding: 0 0 6px 0;
175 margin: 0;
176 text-align: center;
177 font-variant: small-caps;
178 }
179 > div {
180 display: flex;
181 align-items: center;
182 padding-right: 7px;
183 .operationTag {
184 text-overflow: ellipsis;
185 white-space: nowrap;
186 overflow: hidden;
187 flex-grow: 1;
188 cursor: pointer;
189 }
190 .operationBtns {
191 padding: 2px 7px;
192 cursor: pointer;
193 }
194 }
195 .showmore {
196 display: block;
197 text-align: right;
198 color: $palette-300;
199 cursor: pointer;
200 > svg {
201 margin-left: 10px;
202 }
203 &:hover {
204 color: $link-hover;
205 }
206 }
207}
208</style>