added tag editing to multi select menu

This commit is contained in:
zipben 2025-06-05 01:59:22 +00:00
parent 9baca4f16c
commit 76ea038f5a
3 changed files with 189 additions and 2 deletions

View file

@ -97,6 +97,9 @@
</ui-tooltip> </ui-tooltip>
</div> </div>
</div> </div>
<!-- Apply Tags Modal -->
<modals-batch-apply-tags-modal v-model="showApplyTagsModal" :selected-media-items="selectedMediaItems" @tagsApplied="tagsApplied" />
</div> </div>
</template> </template>
@ -105,7 +108,8 @@ export default {
data() { data() {
return { return {
totalEntities: 0, totalEntities: 0,
showProfileDropdown: false showProfileDropdown: false,
showApplyTagsModal: false
} }
}, },
computed: { computed: {
@ -198,6 +202,14 @@ export default {
action: 'rescan' action: 'rescan'
}) })
// Add Edit Tags option
if (this.userCanUpdate) {
options.push({
text: 'Edit Tags',
action: 'apply-tags'
})
}
// The limit of 50 is introduced because of the URL length. Each id has 36 chars, so 36 * 40 = 1440 // The limit of 50 is introduced because of the URL length. Each id has 36 chars, so 36 * 40 = 1440
// + 40 , separators = 1480 chars + base path 280 chars = 1760 chars. This keeps the URL under 2000 chars even with longer domains // + 40 , separators = 1480 chars + base path 280 chars = 1760 chars. This keeps the URL under 2000 chars even with longer domains
if (this.selectedMediaItems.length <= 40) { if (this.selectedMediaItems.length <= 40) {
@ -244,6 +256,8 @@ export default {
this.batchRescan() this.batchRescan()
} else if (action === 'download') { } else if (action === 'download') {
this.batchDownload() this.batchDownload()
} else if (action === 'apply-tags') {
this.batchApplyTagsClick()
} }
}, },
async batchRescan() { async batchRescan() {
@ -394,6 +408,16 @@ export default {
batchAutoMatchClick() { batchAutoMatchClick() {
this.$store.commit('globals/setShowBatchQuickMatchModal', true) this.$store.commit('globals/setShowBatchQuickMatchModal', true)
}, },
batchApplyTagsClick() {
this.showApplyTagsModal = true
},
tagsApplied() {
// Clear selection and refresh the bookshelf
this.$store.commit('globals/resetSelectedMediaItems', [])
this.$eventBus.$emit('bookshelf_clear_selection')
// Optionally refresh the current page to show updated tags
this.$eventBus.$emit('bookshelf_refresh')
},
logout() { logout() {
this.showProfileDropdown = false this.showProfileDropdown = false

View file

@ -0,0 +1,162 @@
<template>
<modals-modal v-model="show" name="batch-apply-tags" :processing="processing" :width="500" :height="'unset'">
<template #outer>
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden pointer-events-none">
<p class="text-3xl text-white truncate">Apply Tags</p>
</div>
</template>
<div ref="container" class="w-full rounded-lg bg-bg box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
<div v-if="show" class="w-full h-full">
<div class="py-4 px-4">
<h1 class="text-2xl">Apply Tags to {{ selectedItemsCount }} Items</h1>
<p class="text-sm text-gray-300 mt-2">Add or remove tags from {{ selectedItemsCount }} selected items</p>
</div>
<div class="px-4 pb-4">
<div class="mb-4">
<ui-multi-select ref="tagsMultiSelect" v-model="selectedTags" :items="allTags" :label="addMode ? 'Tags to add' : 'Tags to remove'" @newItem="newTagItem" @removedItem="removedTagItem" />
</div>
<div class="mb-4">
<div class="flex items-center mb-2">
<ui-toggle-switch v-model="addMode" />
<p class="text-sm ml-2">{{ addMode ? 'Add tags to items' : 'Remove tags from items' }}</p>
</div>
<p class="text-xs text-gray-400">
{{ addMode ? 'Selected tags will be added to all selected items' : 'Selected tags will be removed from all selected items' }}
</p>
</div>
<div class="flex justify-end space-x-2">
<ui-btn small @click="show = false">{{ $strings.ButtonCancel }}</ui-btn>
<ui-btn color="bg-success" small :disabled="!selectedTags.length" @click="applyTags">
{{ addMode ? 'Add Tags' : 'Remove Tags' }}
</ui-btn>
</div>
</div>
</div>
</div>
</modals-modal>
</template>
<script>
export default {
props: {
value: Boolean,
selectedMediaItems: {
type: Array,
default: () => []
}
},
data() {
return {
processing: false,
selectedTags: [],
allTags: [],
addMode: true,
localSelectedItems: [] // Local copy to avoid Vuex mutation issues
}
},
watch: {
show: {
handler(newVal) {
if (newVal) {
this.init()
}
}
},
selectedMediaItems: {
handler(newItems) {
// Create a local copy to avoid Vuex mutation issues
this.localSelectedItems = newItems ? [...newItems] : []
},
immediate: true
}
},
computed: {
show: {
get() {
return this.value
},
set(val) {
this.$emit('input', val)
}
},
selectedItemsCount() {
return this.localSelectedItems.length
},
filterData() {
return this.$store.state.libraries.filterData || {}
}
},
methods: {
init() {
this.selectedTags = []
this.loadTags()
// Ensure we have a local copy of selected items
this.localSelectedItems = this.selectedMediaItems ? [...this.selectedMediaItems] : []
},
loadTags() {
// Get tags from filter data (existing tags in the library)
this.allTags = [...(this.filterData.tags || [])]
},
newTagItem(tag) {
if (!this.allTags.includes(tag)) {
this.allTags = [...this.allTags, tag]
}
},
removedTagItem(tag) {
// Don't remove from allTags as user might want to re-add
},
async applyTags() {
if (!this.selectedTags.length || !this.localSelectedItems.length) return
this.processing = true
try {
// Get the current library items to see their existing tags
const libraryItemsResponse = await this.$axios.$post('/api/items/batch/get', {
libraryItemIds: this.localSelectedItems.map((item) => item.id)
})
const libraryItems = libraryItemsResponse.libraryItems || []
// Prepare update payloads for each item
const updatePayloads = libraryItems.map((item) => {
let currentTags = item.media?.tags || []
let newTags
if (this.addMode) {
// Add tags: merge current tags with selected tags (remove duplicates)
newTags = [...new Set([...currentTags, ...this.selectedTags])]
} else {
// Remove tags: filter out selected tags from current tags
newTags = currentTags.filter((tag) => !this.selectedTags.includes(tag))
}
return {
id: item.id,
mediaPayload: {
tags: newTags
}
}
})
await this.$axios.$post('/api/items/batch/update', updatePayloads)
this.$toast.success(this.addMode ? `Successfully added ${this.selectedTags.length} tag(s) to ${this.selectedItemsCount} item(s)` : `Successfully removed ${this.selectedTags.length} tag(s) from ${this.selectedItemsCount} item(s)`)
this.show = false
this.$emit('tagsApplied')
} catch (error) {
console.error('Failed to apply tags', error)
const errorMsg = error.response?.data || 'Failed to apply tags'
this.$toast.error(errorMsg)
} finally {
this.processing = false
}
}
}
}
</script>

View file

@ -138,7 +138,8 @@ export default {
}, },
setInputWidth() { setInputWidth() {
setTimeout(() => { setTimeout(() => {
var value = this.$refs.input.value if (!this.$refs.input) return
var value = this.$refs.input.value || ''
var len = value.length * 7 + 24 var len = value.length * 7 + 24
this.$refs.input.style.width = len + 'px' this.$refs.input.style.width = len + 'px'
this.recalcMenuPos() this.recalcMenuPos()