mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-07-09 19:01:41 +00:00
added tag editing to multi select menu
This commit is contained in:
parent
9baca4f16c
commit
76ea038f5a
3 changed files with 189 additions and 2 deletions
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
162
client/components/modals/BatchApplyTagsModal.vue
Normal file
162
client/components/modals/BatchApplyTagsModal.vue
Normal 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>
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue