Skip to content

Commit 443ac60

Browse files
toshskicrwxaj
andauthored
feat: Filters (Cast, Studio, Tags,...) can now be switched to "must have" and "must not have" states (#1008)
* Must Include & Exclude optionsfor scene filtering * Add Tag Tooltips Remove Db Debug used in testing * Updated filters UI Co-authored-by: crwxaj <[email protected]>
1 parent 8306d34 commit 443ac60

File tree

2 files changed

+207
-10
lines changed

2 files changed

+207
-10
lines changed

pkg/models/model_scene.go

Lines changed: 91 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"os"
77
"path/filepath"
88
"regexp"
9+
"strconv"
910
"strings"
1011
"time"
1112

@@ -542,44 +543,128 @@ func QueryScenes(r RequestSceneList, enablePreload bool) ResponseSceneList {
542543
}
543544

544545
var sites []string
546+
var excludedSites []string
545547
for _, i := range r.Sites {
546-
sites = append(sites, i.OrElse(""))
548+
switch firstchar := string(i.OrElse(" ")[0]); firstchar {
549+
case "!":
550+
exSite, _ := i.Get()
551+
excludedSites = append(excludedSites, exSite[1:])
552+
default:
553+
sites = append(sites, i.OrElse(""))
554+
}
547555
}
556+
548557
if len(sites) > 0 {
549558
tx = tx.Where("site IN (?)", sites)
550559
}
560+
for _, exclude := range excludedSites {
561+
tx = tx.Where("site NOT IN (?)", exclude)
562+
}
551563

552564
var tags []string
565+
var excludedTags []string
566+
var mustHaveTags []string
553567
for _, i := range r.Tags {
554-
tags = append(tags, i.OrElse(""))
568+
switch firstchar := string(i.OrElse(" ")[0]); firstchar {
569+
case "&":
570+
inclTag, _ := i.Get()
571+
mustHaveTags = append(mustHaveTags, inclTag[1:])
572+
case "!":
573+
exTag, _ := i.Get()
574+
excludedTags = append(excludedTags, exTag[1:])
575+
default:
576+
tags = append(tags, i.OrElse(""))
577+
}
555578
}
556579
if len(tags) > 0 {
557580
tx = tx.
558581
Joins("left join scene_tags on scene_tags.scene_id=scenes.id").
559582
Joins("left join tags on tags.id=scene_tags.tag_id").
560583
Where("tags.name IN (?)", tags)
561584
}
585+
for idx, musthave := range mustHaveTags {
586+
stAlias := "st_i" + strconv.Itoa(idx)
587+
tagAlias := "t_i" + strconv.Itoa(idx)
588+
tx = tx.
589+
Joins("join scene_tags "+stAlias+" on "+stAlias+".scene_id=scenes.id").
590+
Joins("join tags "+tagAlias+" on "+tagAlias+".id="+stAlias+".tag_id and "+tagAlias+".name=?", musthave)
591+
}
592+
for idx, exclude := range excludedTags {
593+
stAlias := "st_e" + strconv.Itoa(idx)
594+
tagAlias := "t_e" + strconv.Itoa(idx)
595+
tx = tx.Where("scenes.id not in (select "+stAlias+".scene_id from tags "+tagAlias+" join scene_tags "+stAlias+" on "+stAlias+".scene_id =scenes.id and "+tagAlias+".id ="+stAlias+".tag_id where "+tagAlias+".name =?)", exclude)
596+
}
562597

563598
var cast []string
599+
var mustHaveCast []string
600+
var excludedCast []string
564601
for _, i := range r.Cast {
565-
cast = append(cast, i.OrElse(""))
602+
switch firstchar := string(i.OrElse(" ")[0]); firstchar {
603+
case "&":
604+
inclCast, _ := i.Get()
605+
mustHaveCast = append(mustHaveCast, inclCast[1:])
606+
case "!":
607+
exCast, _ := i.Get()
608+
excludedCast = append(excludedCast, exCast[1:])
609+
default:
610+
cast = append(cast, i.OrElse(""))
611+
}
566612
}
567613
if len(cast) > 0 {
568614
tx = tx.
569615
Joins("left join scene_cast on scene_cast.scene_id=scenes.id").
570616
Joins("left join actors on actors.id=scene_cast.actor_id").
571617
Where("actors.name IN (?)", cast)
572618
}
619+
for idx, musthave := range mustHaveCast {
620+
scAlias := "sc_i" + strconv.Itoa(idx)
621+
actorAlias := "a_i" + strconv.Itoa(idx)
622+
tx = tx.
623+
Joins("join scene_cast "+scAlias+" on "+scAlias+".scene_id=scenes.id").
624+
Joins("join actors "+actorAlias+" on "+actorAlias+".id="+scAlias+".actor_id and "+actorAlias+".name=?", musthave)
625+
}
626+
for idx, exclude := range excludedCast {
627+
scAlias := "sc_e" + strconv.Itoa(idx)
628+
actorAlias := "a_e" + strconv.Itoa(idx)
629+
tx = tx.Where("scenes.id not in (select "+scAlias+".scene_id from actors "+actorAlias+" join scene_cast "+scAlias+" on "+scAlias+".scene_id =scenes.id and "+actorAlias+".id ="+scAlias+".actor_id where "+actorAlias+".name =?)", exclude)
630+
}
573631

574632
var cuepoint []string
633+
var mustHaveCuepoint []string
634+
var excludedCuepoint []string
575635
for _, i := range r.Cuepoint {
576-
cuepoint = append(cuepoint, i.OrElse(""))
636+
switch firstchar := string(i.OrElse(" ")[0]); firstchar {
637+
case "&":
638+
inclCp, _ := i.Get()
639+
mustHaveCuepoint = append(mustHaveCuepoint, inclCp[1:])
640+
case "!":
641+
exCp, _ := i.Get()
642+
excludedCuepoint = append(excludedCuepoint, exCp[1:])
643+
default:
644+
cuepoint = append(cuepoint, i.OrElse(""))
645+
}
577646
}
647+
578648
if len(cuepoint) > 0 {
579649
tx = tx.Joins("left join scene_cuepoints on scene_cuepoints.scene_id=scenes.id")
580-
for _, i := range cuepoint {
581-
tx = tx.Where("scene_cuepoints.name LIKE ?", "%"+i+"%")
650+
var where string
651+
for idx, i := range cuepoint {
652+
if idx == 0 {
653+
where = "scene_cuepoints.name LIKE '%" + i + "%'"
654+
} else {
655+
where = where + " or scene_cuepoints.name LIKE '%" + i + "%'"
656+
}
582657
}
658+
tx = tx.Where(where)
659+
}
660+
for idx, musthave := range mustHaveCuepoint {
661+
scpAlias := "scp_i" + strconv.Itoa(idx)
662+
tx = tx.
663+
Joins("join scene_cuepoints "+scpAlias+" on "+scpAlias+".scene_id=scenes.id and "+scpAlias+".name like ?", "%"+musthave+"%")
664+
}
665+
for idx, exclude := range excludedCuepoint {
666+
scpAlias := "scp_e" + strconv.Itoa(idx)
667+
tx = tx.Where("scenes.id not in (select "+scpAlias+".scene_id from scene_cuepoints "+scpAlias+" where "+scpAlias+".scene_id =scenes.id and "+scpAlias+".name like ?)", "%"+exclude+"%")
583668
}
584669

585670
if r.Released.Present() {

ui/src/views/scenes/Filters.vue

Lines changed: 116 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,27 +104,74 @@
104104
<b-taginput v-model="cast" autocomplete :data="filteredCast" @typing="getFilteredCast">
105105
<template slot-scope="props">{{ props.option }}</template>
106106
<template slot="empty">No matching cast</template>
107+
<template #selected="props">
108+
<b-tag v-for="(tag, index) in props.tags"
109+
:type="tag.charAt(0)=='!' ? 'is-danger': (tag.charAt(0)=='&' ? 'is-success' : '')"
110+
:key="tag+index" :tabstop="false" closable @close="cast=cast.filter(e => e !== tag)" @click="toggle3way(tag,index,'cast')">
111+
<b-tooltip position="is-right" :delay="200"
112+
:label="tag.charAt(0)=='!' ? 'Exclude ' + removeConditionPrefix(tag) : tag.charAt(0)=='&' ? 'Must Have ' + removeConditionPrefix(tag) : 'Include ' + removeConditionPrefix(tag)">
113+
<b-icon pack="mdi" v-if="tag.charAt(0)=='!'" icon="minus-circle-outline" size="is-small" class="tagicon"></b-icon>
114+
<b-icon pack="mdi" v-if="tag.charAt(0)=='&'" icon="plus-circle-outline" size="is-small" class="tagicon"></b-icon>
115+
{{removeConditionPrefix(tag)}}
116+
</b-tooltip>
117+
</b-tag>
118+
</template>
107119
</b-taginput>
108120
</b-field>
109121

110122
<b-field label="Site" label-position="on-border" class="field-extra">
111123
<b-taginput v-model="sites" autocomplete :data="filteredSites" @typing="getFilteredSites">
112124
<template slot-scope="props">{{ props.option }}</template>
113125
<template slot="empty">No matching sites</template>
126+
<template #selected="props">
127+
<b-tag v-for="(tag, index) in props.tags"
128+
:type="tag.charAt(0)=='!' ? 'is-danger': (tag.charAt(0)=='&' ? 'is-success' : '')"
129+
:key="tag+index" :tabstop="false" closable @close="sites=sites.filter(e => e !== tag)" @click="toggle2Way(tag,index,'sites')">
130+
<b-tooltip position="is-right" :delay="200"
131+
:label="tag.charAt(0)=='!' ? 'Exclude ' + removeConditionPrefix(tag) : 'Include ' + removeConditionPrefix(tag)">
132+
<b-icon pack="mdi" v-if="tag.charAt(0)=='!'" icon="minus-circle-outline" size="is-small" class="tagicon"></b-icon>
133+
{{removeConditionPrefix(tag)}}
134+
</b-tooltip>
135+
</b-tag>
136+
</template>
114137
</b-taginput>
115138
</b-field>
116139

117140
<b-field label="Tags" label-position="on-border" class="field-extra">
118141
<b-taginput v-model="tags" autocomplete :data="filteredTags" @typing="getFilteredTags">
119142
<template slot-scope="props">{{ props.option }}</template>
120143
<template slot="empty">No matching tags</template>
144+
<template #selected="props">
145+
<b-tag v-for="(tag, index) in props.tags"
146+
:type="tag.charAt(0)=='!' ? 'is-danger': (tag.charAt(0)=='&' ? 'is-success' : '')"
147+
:key="tag+index" :tabstop="false" closable @close="tags=tags.filter(e => e !== tag)" @click="toggle3way(tag,index,'tags')">
148+
<b-tooltip position="is-right" :delay="200"
149+
:label="tag.charAt(0)=='!' ? 'Exclude ' + removeConditionPrefix(tag) : tag.charAt(0)=='&' ? 'Must Have ' + removeConditionPrefix(tag) : 'Include ' + removeConditionPrefix(tag)">
150+
<b-icon pack="mdi" v-if="tag.charAt(0)=='!'" icon="minus-circle-outline" size="is-small" class="tagicon"></b-icon>
151+
<b-icon pack="mdi" v-if="tag.charAt(0)=='&'" icon="plus-circle-outline" size="is-small" class="tagicon"></b-icon>
152+
{{removeConditionPrefix(tag)}}
153+
</b-tooltip>
154+
</b-tag>
155+
</template>
121156
</b-taginput>
122157
</b-field>
123158

124159
<b-field label="Cuepoint" label-position="on-border" class="field-extra">
125160
<b-taginput v-model="cuepoint" allow-new>
126161
<template slot-scope="props">{{ props.option }}</template>
127162
<template slot="empty">No matching cuepoints</template>
163+
<template #selected="props">
164+
<b-tag v-for="(tag, index) in props.tags"
165+
:type="tag.charAt(0)=='!' ? 'is-danger': (tag.charAt(0)=='&' ? 'is-success' : '')"
166+
:key="tag+index" :tabstop="false" closable @close="cuepoint=cuepoint.filter(e => e !== tag)" @click="toggle3way(tag,index,'cuepoints')">
167+
<b-tooltip position="is-right" :delay="200"
168+
:label="tag.charAt(0)=='!' ? 'Exclude ' + removeConditionPrefix(tag) : tag.charAt(0)=='&' ? 'Must Have ' + removeConditionPrefix(tag) : 'Include ' + removeConditionPrefix(tag)">
169+
<b-icon pack="mdi" v-if="tag.charAt(0)=='!'" icon="minus-circle-outline" size="is-small" class="tagicon"></b-icon>
170+
<b-icon pack="mdi" v-if="tag.charAt(0)=='&'" icon="plus-circle-outline" size="is-small" class="tagicon"></b-icon>
171+
{{removeConditionPrefix(tag)}}
172+
</b-tooltip>
173+
</b-tag>
174+
</template>
128175
</b-taginput>
129176
</b-field>
130177

@@ -185,19 +232,19 @@ export default {
185232
getFilteredCast (text) {
186233
this.filteredCast = this.filters.cast.filter(option => (
187234
option.toString().toLowerCase().indexOf(text.toLowerCase()) >= 0 &&
188-
!this.cast.some(entry => entry.toString() === option.toString())
235+
!this.cast.some(entry => this.removeConditionPrefix(entry.toString()) === option.toString())
189236
))
190237
},
191238
getFilteredSites (text) {
192239
this.filteredSites = this.filters.sites.filter(option => (
193240
option.toString().toLowerCase().indexOf(text.toLowerCase()) >= 0 &&
194-
!this.sites.some(entry => entry.toString() === option.toString())
241+
!this.sites.some(entry => this.removeConditionPrefix(entry.toString()) === option.toString())
195242
))
196243
},
197244
getFilteredTags (text) {
198245
this.filteredTags = this.filters.tags.filter(option => (
199246
option.toString().toLowerCase().indexOf(text.toLowerCase()) >= 0 &&
200-
!this.tags.some(entry => entry.toString() === option.toString())
247+
!this.tags.some(entry => this.removeConditionPrefix(entry.toString()) === option.toString())
201248
))
202249
},
203250
clearReleaseMonth () {
@@ -259,6 +306,66 @@ export default {
259306
this.$store.state.sceneList.isLoading = false
260307
})
261308
},
309+
toggle3way (text, idx, list) {
310+
let tags = []
311+
switch (list) {
312+
case 'cast':
313+
tags=this.cast
314+
break
315+
case 'tags':
316+
tags=this.tags
317+
break
318+
case 'cuepoints':
319+
tags=this.cuepoint
320+
break
321+
}
322+
switch(tags[idx].charAt(0)) {
323+
case '!':
324+
tags[idx]=this.removeConditionPrefix(tags[idx])
325+
break
326+
case '&':
327+
tags[idx]='!' + this.removeConditionPrefix(tags[idx])
328+
break
329+
default:
330+
tags[idx]='&'+text
331+
}
332+
switch (list) {
333+
case 'cast':
334+
this.cast=tags
335+
break
336+
case 'tags':
337+
this.tags=tags
338+
break
339+
case 'cuepoints':
340+
this.cuepoint=tags
341+
break
342+
}
343+
},
344+
toggle2Way (text, idx, list) {
345+
let tags = []
346+
switch (list) {
347+
case 'sites':
348+
tags=this.sites
349+
}
350+
switch(tags[idx].charAt(0)) {
351+
case '!':
352+
tags[idx]=this.removeConditionPrefix(tags[idx])
353+
break
354+
default:
355+
tags[idx]='!'+text
356+
}
357+
switch (list) {
358+
case 'sites':
359+
this.sites=tags
360+
break
361+
}
362+
},
363+
removeConditionPrefix(txt) {
364+
if (txt.charAt(0)=='!' || txt.charAt(0)=='&') {
365+
return txt.substring(1)
366+
}
367+
return txt
368+
}
262369
},
263370
computed: {
264371
filters () {
@@ -316,6 +423,7 @@ export default {
316423
set (value) {
317424
this.$store.state.sceneList.filters.tags = value
318425
this.reloadList()
426+
console.log('reloaded',value)
319427
}
320428
},
321429
cuepoint: {
@@ -404,7 +512,7 @@ export default {
404512
// you can remove from a group if you select one group and one or more actors
405513
return akaCastCnt == 1 && actorCnt > 0 ? false : true
406514
407-
}
515+
},
408516
}
409517
}
410518
</script>
@@ -423,4 +531,8 @@ export default {
423531
.field-extra {
424532
margin-bottom: 1.1em !important;
425533
}
534+
535+
.tagicon {
536+
margin-right: -0.2em !important;
537+
}
426538
</style>

0 commit comments

Comments
 (0)