diff options
-rw-r--r-- | viewer/src/@types/tag.d.ts | 5 | ||||
-rw-r--r-- | viewer/src/assets/scss/global.scss | 4 | ||||
-rw-r--r-- | viewer/src/assets/scss/theme.scss | 4 | ||||
-rw-r--r-- | viewer/src/components/LdProposition.vue | 47 | ||||
-rw-r--r-- | viewer/src/locales/en.json | 5 | ||||
-rw-r--r-- | viewer/src/services/indexfactory.ts | 19 | ||||
-rw-r--r-- | viewer/src/store/galleryStore.ts | 15 | ||||
-rw-r--r-- | viewer/src/views/PanelLeft.vue | 24 |
8 files changed, 96 insertions, 27 deletions
diff --git a/viewer/src/@types/tag.d.ts b/viewer/src/@types/tag.d.ts index a390c80..425a995 100644 --- a/viewer/src/@types/tag.d.ts +++ b/viewer/src/@types/tag.d.ts | |||
@@ -31,4 +31,9 @@ declare namespace Tag { | |||
31 | } | 31 | } |
32 | type SearchByOperation = { [index: string]: Tag.Search[] }; | 32 | type SearchByOperation = { [index: string]: Tag.Search[] }; |
33 | type Index = { [index: string]: Node }; | 33 | type Index = { [index: string]: Node }; |
34 | |||
35 | interface Category { | ||
36 | tag: string; | ||
37 | index: Index; | ||
38 | } | ||
34 | } | 39 | } |
diff --git a/viewer/src/assets/scss/global.scss b/viewer/src/assets/scss/global.scss index bd7a7e7..a8a42a1 100644 --- a/viewer/src/assets/scss/global.scss +++ b/viewer/src/assets/scss/global.scss | |||
@@ -85,8 +85,8 @@ button svg + span { | |||
85 | overflow: auto; | 85 | overflow: auto; |
86 | } | 86 | } |
87 | .scrollbar::-webkit-scrollbar { | 87 | .scrollbar::-webkit-scrollbar { |
88 | width: 10px; | 88 | width: $scrollbar_width; |
89 | height: 10px; | 89 | height: $scrollbar_width; |
90 | } | 90 | } |
91 | .scrollbar::-webkit-scrollbar-corner { | 91 | .scrollbar::-webkit-scrollbar-corner { |
92 | background-color: transparent; | 92 | background-color: transparent; |
diff --git a/viewer/src/assets/scss/theme.scss b/viewer/src/assets/scss/theme.scss index 26cb355..35983a7 100644 --- a/viewer/src/assets/scss/theme.scss +++ b/viewer/src/assets/scss/theme.scss | |||
@@ -42,6 +42,8 @@ $radius: 0; | |||
42 | $loading-background: $palette-800; | 42 | $loading-background: $palette-800; |
43 | $title-color: $palette-200; | 43 | $title-color: $palette-200; |
44 | $title-size: $size-5; | 44 | $title-size: $size-5; |
45 | $subtitle-color: $palette-200; | ||
46 | $subtitle-size: $size-5; | ||
45 | $tag-background-color: $palette-800; | 47 | $tag-background-color: $palette-800; |
46 | $button-color: $palette-100; | 48 | $button-color: $palette-100; |
47 | $button-active-color: $palette-100; | 49 | $button-active-color: $palette-100; |
@@ -61,11 +63,13 @@ $panel-left-txtcolor: $primary; | |||
61 | $command-buttons-bgcolor: $palette-700; | 63 | $command-buttons-bgcolor: $palette-700; |
62 | $content-bgcolor: $palette-900; | 64 | $content-bgcolor: $palette-900; |
63 | $scrollbar-color: $palette-300; | 65 | $scrollbar-color: $palette-300; |
66 | $scrollbar_width: 10px; | ||
64 | $loader-color: $palette-800; | 67 | $loader-color: $palette-800; |
65 | $input-tag-delete-background-color: $palette-700; | 68 | $input-tag-delete-background-color: $palette-700; |
66 | $breadcrumb-margins: 12px; | 69 | $breadcrumb-margins: 12px; |
67 | $breadcrumb-overflow-mask-size: $breadcrumb-margins + 60px; | 70 | $breadcrumb-overflow-mask-size: $breadcrumb-margins + 60px; |
68 | $thumbnail-other-size: 120px; | 71 | $thumbnail-other-size: 120px; |
72 | $proposed-category-bgcolor: $palette-700; | ||
69 | 73 | ||
70 | // Layout | 74 | // Layout |
71 | 75 | ||
diff --git a/viewer/src/components/LdProposition.vue b/viewer/src/components/LdProposition.vue index 3357777..1a1d387 100644 --- a/viewer/src/components/LdProposition.vue +++ b/viewer/src/components/LdProposition.vue | |||
@@ -19,8 +19,12 @@ | |||
19 | --> | 19 | --> |
20 | 20 | ||
21 | <template> | 21 | <template> |
22 | <div> | 22 | <div class="proposition"> |
23 | <div v-for="proposed in proposedTags" :key="proposed.rawTag" class="proposition"> | 23 | <h2 |
24 | v-if="showTitle && proposedTags.length" | ||
25 | class="subtitle category" | ||
26 | >{{title || $t('panelLeft.propositions.other')}}</h2> | ||
27 | <div v-for="proposed in proposedTags" :key="proposed.rawTag"> | ||
24 | <a | 28 | <a |
25 | class="operation-btns link" | 29 | class="operation-btns link" |
26 | :title="$t('tag-propositions.substraction')" | 30 | :title="$t('tag-propositions.substraction')" |
@@ -54,6 +58,8 @@ import { Operation } from "@/@types/Operation"; | |||
54 | 58 | ||
55 | @Component | 59 | @Component |
56 | export default class LdProposition extends Vue { | 60 | export default class LdProposition extends Vue { |
61 | @Prop({ type: String, required: true }) readonly title!: string; | ||
62 | @Prop({ type: Boolean, required: true }) readonly showTitle!: boolean; | ||
57 | @Prop({ type: Array, required: true }) readonly currentTags!: string[]; | 63 | @Prop({ type: Array, required: true }) readonly currentTags!: string[]; |
58 | @Prop({ required: true }) readonly tagsIndex!: Tag.Index; | 64 | @Prop({ required: true }) readonly tagsIndex!: Tag.Index; |
59 | @PropSync("searchFilters", { type: Array, required: true }) model!: Tag.Search[]; | 65 | @PropSync("searchFilters", { type: Array, required: true }) model!: Tag.Search[]; |
@@ -69,13 +75,14 @@ export default class LdProposition extends Vue { | |||
69 | this.extractDistinctItems(this.model) | 75 | this.extractDistinctItems(this.model) |
70 | .flatMap(item => item.tags) | 76 | .flatMap(item => item.tags) |
71 | .map(this.rightmost) | 77 | .map(this.rightmost) |
72 | .filter(rawTag => !this.model.find(search => search.tag === rawTag)) | 78 | .filter(rawTag => this.tagsIndex[rawTag] && !this.model.find(search => search.tag === rawTag)) |
73 | .forEach(rawTag => (propositions[rawTag] = (propositions[rawTag] ?? 0) + 1)); | 79 | .forEach(rawTag => (propositions[rawTag] = (propositions[rawTag] ?? 0) + 1)); |
74 | } else { | 80 | } else { |
75 | // Tags count from the current directory | 81 | // Tags count from the current directory |
76 | this.currentTags | 82 | this.currentTags |
77 | .flatMap(tag => tag.split(":")) | 83 | .flatMap(tag => tag.split(":")) |
78 | .map(tag => this.tagsIndex[tag]) | 84 | .map(tag => this.tagsIndex[tag]) |
85 | .filter(Boolean) | ||
79 | .forEach(tagindex => (propositions[tagindex.tag] = tagindex.items.length)); | 86 | .forEach(tagindex => (propositions[tagindex.tag] = tagindex.items.length)); |
80 | } | 87 | } |
81 | 88 | ||
@@ -105,19 +112,29 @@ export default class LdProposition extends Vue { | |||
105 | @import "@/assets/scss/theme.scss"; | 112 | @import "@/assets/scss/theme.scss"; |
106 | 113 | ||
107 | .proposition { | 114 | .proposition { |
108 | display: flex; | 115 | .subtitle { |
109 | align-items: center; | 116 | background-color: $proposed-category-bgcolor; |
110 | padding-right: 7px; | 117 | width: 100%; |
111 | .operation-tag { | 118 | padding: 0 0 6px 0; |
112 | text-overflow: ellipsis; | 119 | margin: 0; |
113 | white-space: nowrap; | 120 | text-align: center; |
114 | overflow: hidden; | 121 | font-variant: small-caps; |
115 | flex-grow: 1; | ||
116 | cursor: pointer; | ||
117 | } | 122 | } |
118 | .operation-btns { | 123 | > div { |
119 | padding: 2px 7px; | 124 | display: flex; |
120 | cursor: pointer; | 125 | align-items: center; |
126 | padding-right: 7px; | ||
127 | .operation-tag { | ||
128 | text-overflow: ellipsis; | ||
129 | white-space: nowrap; | ||
130 | overflow: hidden; | ||
131 | flex-grow: 1; | ||
132 | cursor: pointer; | ||
133 | } | ||
134 | .operation-btns { | ||
135 | padding: 2px 7px; | ||
136 | cursor: pointer; | ||
137 | } | ||
121 | } | 138 | } |
122 | } | 139 | } |
123 | </style> | 140 | </style> |
diff --git a/viewer/src/locales/en.json b/viewer/src/locales/en.json index 58adcd2..4209a67 100644 --- a/viewer/src/locales/en.json +++ b/viewer/src/locales/en.json | |||
@@ -14,5 +14,6 @@ | |||
14 | "command.search.search": "Search", | 14 | "command.search.search": "Search", |
15 | "command.back": "Go back", | 15 | "command.back": "Go back", |
16 | "command.parent": "Go to parent directory", | 16 | "command.parent": "Go to parent directory", |
17 | "directory.no-results": "Empty directory" | 17 | "directory.no-results": "Empty directory", |
18 | } | 18 | "panelLeft.propositions.other": "other filters" |
19 | } \ No newline at end of file | ||
diff --git a/viewer/src/services/indexfactory.ts b/viewer/src/services/indexfactory.ts index a31f3ef..466b509 100644 --- a/viewer/src/services/indexfactory.ts +++ b/viewer/src/services/indexfactory.ts | |||
@@ -112,4 +112,23 @@ export default class IndexFactory { | |||
112 | if (strict) return node.tagfiltered === filter; | 112 | if (strict) return node.tagfiltered === filter; |
113 | return node.tagfiltered.includes(filter) | 113 | return node.tagfiltered.includes(filter) |
114 | } | 114 | } |
115 | |||
116 | // --- | ||
117 | |||
118 | public static generateCategories(tagsIndex: Tag.Index, tags?: Gallery.RawTag[]): Tag.Category[] { | ||
119 | if (!tags?.length) return [{ tag: "", index: tagsIndex }]; | ||
120 | |||
121 | const tagsCategories: Tag.Category[] = []; | ||
122 | const tagsRemaining = new Map(Object.entries(tagsIndex)); | ||
123 | tags | ||
124 | .map(tag => ({ tag, index: tagsIndex[tag]?.children })) | ||
125 | .filter(category => category.index && Object.keys(category.index).length) | ||
126 | .forEach(category => { | ||
127 | tagsCategories.push(category); | ||
128 | tagsRemaining.delete(category.tag); | ||
129 | Object.values(category.index).map(node => node.tag).forEach(tag => tagsRemaining.delete(tag)); | ||
130 | }); | ||
131 | tagsCategories.push({ tag: "", index: Object.fromEntries(tagsRemaining) }); | ||
132 | return tagsCategories; | ||
133 | } | ||
115 | } | 134 | } |
diff --git a/viewer/src/store/galleryStore.ts b/viewer/src/store/galleryStore.ts index 4a5de0f..352a266 100644 --- a/viewer/src/store/galleryStore.ts +++ b/viewer/src/store/galleryStore.ts | |||
@@ -31,6 +31,7 @@ export default class GalleryStore extends VuexModule { | |||
31 | config: Gallery.Config | null = null; | 31 | config: Gallery.Config | null = null; |
32 | galleryIndex: Gallery.Index | null = null; | 32 | galleryIndex: Gallery.Index | null = null; |
33 | tagsIndex: Tag.Index = {}; | 33 | tagsIndex: Tag.Index = {}; |
34 | tagsCategories: Tag.Category[] = []; | ||
34 | currentPath: string = "/"; | 35 | currentPath: string = "/"; |
35 | currentSearch: Tag.Search[] = []; | 36 | currentSearch: Tag.Search[] = []; |
36 | 37 | ||
@@ -48,6 +49,10 @@ export default class GalleryStore extends VuexModule { | |||
48 | this.tagsIndex = Object.freeze(tagsIndex); | 49 | this.tagsIndex = Object.freeze(tagsIndex); |
49 | } | 50 | } |
50 | 51 | ||
52 | @mutation private setTagsCategories(tagsCategories: Tag.Category[]) { | ||
53 | this.tagsCategories = tagsCategories; | ||
54 | } | ||
55 | |||
51 | @mutation setCurrentPath(currentPath: string) { | 56 | @mutation setCurrentPath(currentPath: string) { |
52 | this.currentPath = currentPath; | 57 | this.currentPath = currentPath; |
53 | } | 58 | } |
@@ -89,7 +94,8 @@ export default class GalleryStore extends VuexModule { | |||
89 | return fetch(`${process.env.VUE_APP_DATA_URL}${root}index.json`, { cache: "no-cache" }) | 94 | return fetch(`${process.env.VUE_APP_DATA_URL}${root}index.json`, { cache: "no-cache" }) |
90 | .then(response => response.json()) | 95 | .then(response => response.json()) |
91 | .then(this.setGalleryIndex) | 96 | .then(this.setGalleryIndex) |