mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-02-25 11:26:49 -05:00
Compare commits
4 Commits
v2.18.1
...
batch-edit
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
abb5bd3a2d | ||
|
|
79acc41d16 | ||
|
|
598a93d224 | ||
|
|
c3c846f82d |
@@ -90,8 +90,8 @@
|
||||
<div class="relative">
|
||||
<ui-textarea-with-label :value="prettyFfprobeData" readonly :rows="30" class="text-xs" />
|
||||
|
||||
<button class="absolute top-4 right-4" :class="copiedToClipboard ? 'text-success' : 'text-white/50 hover:text-white/80'" @click.stop="copyFfprobeData">
|
||||
<span class="material-symbols">{{ copiedToClipboard ? 'check' : 'content_copy' }}</span>
|
||||
<button class="absolute top-4 right-4" :class="hasCopied ? 'text-success' : 'text-gray-400 hover:text-white'" @click.stop="copyToClipboard">
|
||||
<span class="material-symbols">{{ hasCopied ? 'done' : 'content_copy' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -113,14 +113,13 @@ export default {
|
||||
return {
|
||||
probingFile: false,
|
||||
ffprobeData: null,
|
||||
copiedToClipboard: false
|
||||
hasCopied: null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
show(newVal) {
|
||||
if (newVal) {
|
||||
this.ffprobeData = null
|
||||
this.copiedToClipboard = false
|
||||
this.probingFile = false
|
||||
}
|
||||
}
|
||||
@@ -165,8 +164,13 @@ export default {
|
||||
this.probingFile = false
|
||||
})
|
||||
},
|
||||
async copyFfprobeData() {
|
||||
this.copiedToClipboard = await this.$copyToClipboard(this.prettyFfprobeData)
|
||||
copyToClipboard() {
|
||||
clearTimeout(this.hasCopied)
|
||||
this.$copyToClipboard(this.prettyFfprobeData).then((success) => {
|
||||
this.hasCopied = setTimeout(() => {
|
||||
this.hasCopied = null
|
||||
}, 2000)
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<template v-if="currentShare">
|
||||
<div class="w-full py-2">
|
||||
<label class="px-1 text-sm font-semibold block">{{ $strings.LabelShareURL }}</label>
|
||||
<ui-text-input v-model="currentShareUrl" show-copy readonly class="text-base h-10" />
|
||||
<ui-text-input v-model="currentShareUrl" show-copy readonly />
|
||||
</div>
|
||||
<div class="w-full py-2 px-1">
|
||||
<p v-if="currentShare.isDownloadable" class="text-sm mb-2">{{ $strings.LabelDownloadable }}</p>
|
||||
|
||||
@@ -10,9 +10,7 @@
|
||||
<p class="text-lg font-semibold mb-4">{{ $strings.HeaderRSSFeedIsOpen }}</p>
|
||||
|
||||
<div class="w-full relative">
|
||||
<ui-text-input :value="feedUrl" readonly />
|
||||
|
||||
<span class="material-symbols absolute right-2 bottom-2 p-0.5 text-base transition-transform duration-100 text-gray-300 hover:text-white transform hover:scale-125 cursor-pointer" @click="copyToClipboard(feedUrl)">content_copy</span>
|
||||
<ui-text-input :value="feedUrl" readonly show-copy />
|
||||
</div>
|
||||
|
||||
<div v-if="currentFeed.meta" class="mt-5">
|
||||
@@ -160,9 +158,6 @@ export default {
|
||||
this.processing = false
|
||||
})
|
||||
},
|
||||
copyToClipboard(str) {
|
||||
this.$copyToClipboard(str, this)
|
||||
},
|
||||
closeFeed() {
|
||||
this.processing = true
|
||||
this.$axios
|
||||
|
||||
@@ -5,8 +5,7 @@
|
||||
<p class="text-lg font-semibold mb-4">{{ $strings.HeaderRSSFeedGeneral }}</p>
|
||||
|
||||
<div class="w-full relative">
|
||||
<ui-text-input :value="feedUrl" readonly />
|
||||
<span class="material-symbols absolute right-2 bottom-2 p-0.5 text-base transition-transform duration-100 text-gray-300 hover:text-white transform hover:scale-125 cursor-pointer" @click="copyToClipboard(feedUrl)">content_copy</span>
|
||||
<ui-text-input :value="feedUrl" readonly show-copy />
|
||||
</div>
|
||||
|
||||
<div v-if="feed.meta" class="mt-5">
|
||||
@@ -74,13 +73,7 @@ export default {
|
||||
feedUrl() {
|
||||
return this.feed ? `${window.origin}${this.$config.routerBasePath}${this.feed.feedUrl}` : ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
copyToClipboard(str) {
|
||||
this.$copyToClipboard(str, this)
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
<div v-if="type === 'password' && isHovering" class="absolute top-0 right-0 h-full px-4 flex items-center justify-center">
|
||||
<span class="material-symbols text-gray-400 cursor-pointer text-lg" @click.stop.prevent="showPassword = !showPassword">{{ !showPassword ? 'visibility' : 'visibility_off' }}</span>
|
||||
</div>
|
||||
<div v-else-if="showCopy" class="absolute top-0 right-0 h-full px-4 flex items-center justify-center">
|
||||
<span class="material-symbols text-gray-400 cursor-pointer text-lg" @click.stop.prevent="copyToClipboard">{{ !hasCopied ? 'content_copy' : 'done' }}</span>
|
||||
<div v-else-if="showCopy" class="absolute top-0 right-0 h-full px-2 flex items-center justify-center">
|
||||
<span class="material-symbols cursor-pointer text-lg" :class="hasCopied ? 'text-success' : 'text-gray-400 hover:text-white'" @click.stop.prevent="copyToClipboard">{{ !hasCopied ? 'content_copy' : 'done' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -47,7 +47,7 @@ export default {
|
||||
showPassword: false,
|
||||
isHovering: false,
|
||||
isFocused: false,
|
||||
hasCopied: false,
|
||||
hasCopied: null,
|
||||
isInvalidDate: false
|
||||
}
|
||||
},
|
||||
@@ -62,7 +62,12 @@ export default {
|
||||
},
|
||||
classList() {
|
||||
var _list = []
|
||||
_list.push(`px-${this.paddingX}`)
|
||||
if (this.showCopy) {
|
||||
_list.push('pl-3', 'pr-8')
|
||||
} else {
|
||||
_list.push(`px-${this.paddingX}`)
|
||||
}
|
||||
|
||||
_list.push(`py-${this.paddingY}`)
|
||||
if (this.noSpinner) _list.push('no-spinner')
|
||||
if (this.textCenter) _list.push('text-center')
|
||||
@@ -80,11 +85,10 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
copyToClipboard() {
|
||||
if (this.hasCopied) return
|
||||
clearTimeout(this.hasCopied)
|
||||
this.$copyToClipboard(this.inputValue).then((success) => {
|
||||
this.hasCopied = success
|
||||
setTimeout(() => {
|
||||
this.hasCopied = false
|
||||
this.hasCopied = setTimeout(() => {
|
||||
this.hasCopied = null
|
||||
}, 2000)
|
||||
})
|
||||
},
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<em v-if="note" class="font-normal text-xs pl-2">{{ note }}</em>
|
||||
</label>
|
||||
</slot>
|
||||
<ui-text-input :placeholder="placeholder || label" :inputId="identifier" ref="input" v-model="inputValue" :disabled="disabled" :readonly="readonly" :type="type" class="w-full" :class="inputClass" @blur="inputBlurred" />
|
||||
<ui-text-input :placeholder="placeholder || label" :inputId="identifier" ref="input" v-model="inputValue" :disabled="disabled" :readonly="readonly" :type="type" :show-copy="showCopy" class="w-full" :class="inputClass" @blur="inputBlurred" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -23,7 +23,8 @@ export default {
|
||||
},
|
||||
readonly: Boolean,
|
||||
disabled: Boolean,
|
||||
inputClass: String
|
||||
inputClass: String,
|
||||
showCopy: Boolean
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
|
||||
@@ -86,7 +86,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full flex items-center justify-end p-4">
|
||||
<div class="w-full flex items-center p-4 space-x-2">
|
||||
<ui-btn small @click.stop="resetMapDetails">{{ $strings.ButtonReset }}</ui-btn>
|
||||
<ui-tooltip direction="bottom" :text="$strings.MessageBatchEditPopulateMapDetailsAllHelp">
|
||||
<ui-btn small :disabled="!hasSelectedBatchUsage" @click.stop="populateFromExisting()">{{ $strings.ButtonBatchEditPopulateFromExisting }}</ui-btn>
|
||||
</ui-tooltip>
|
||||
<div class="flex-grow" />
|
||||
<ui-btn color="success" :disabled="!hasSelectedBatchUsage" :padding-x="8" small class="text-base" :loading="isProcessing" @click="mapBatchDetails">{{ $strings.ButtonApply }}</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
@@ -97,6 +102,11 @@
|
||||
<div class="flex justify-center flex-wrap">
|
||||
<template v-for="libraryItem in libraryItemCopies">
|
||||
<div :key="libraryItem.id" class="w-full max-w-3xl border border-black-300 p-6 -ml-px -mt-px">
|
||||
<div class="flex items-center justify-end">
|
||||
<ui-tooltip direction="bottom" :text="$strings.MessageBatchEditPopulateMapDetailsItemHelp">
|
||||
<ui-btn small :disabled="!hasSelectedBatchUsage" @click="populateFromExisting(libraryItem.id)">{{ $strings.ButtonBatchEditPopulateMapDetails }}</ui-btn>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
<widgets-book-details-edit v-if="libraryItem.mediaType === 'book'" :ref="`itemForm-${libraryItem.id}`" :library-item="libraryItem" @change="handleItemChange" />
|
||||
<widgets-podcast-details-edit v-else :ref="`itemForm-${libraryItem.id}`" :library-item="libraryItem" @change="handleItemChange" />
|
||||
</div>
|
||||
@@ -228,6 +238,88 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
resetMapDetails() {
|
||||
this.blurBatchForm()
|
||||
this.batchDetails = {
|
||||
subtitle: null,
|
||||
authors: null,
|
||||
publishedYear: null,
|
||||
series: [],
|
||||
genres: [],
|
||||
tags: [],
|
||||
narrators: [],
|
||||
publisher: null,
|
||||
language: null,
|
||||
explicit: false,
|
||||
abridged: false
|
||||
}
|
||||
this.selectedBatchUsage = {
|
||||
subtitle: false,
|
||||
authors: false,
|
||||
publishedYear: false,
|
||||
series: false,
|
||||
genres: false,
|
||||
tags: false,
|
||||
narrators: false,
|
||||
publisher: false,
|
||||
language: false,
|
||||
explicit: false,
|
||||
abridged: false
|
||||
}
|
||||
},
|
||||
populateFromExisting(libraryItemId) {
|
||||
this.blurBatchForm()
|
||||
|
||||
let libraryItemsToMap = this.libraryItemCopies
|
||||
if (libraryItemId) {
|
||||
libraryItemsToMap = this.libraryItemCopies.filter((li) => li.id === libraryItemId)
|
||||
}
|
||||
|
||||
for (const key in this.selectedBatchUsage) {
|
||||
if (!this.selectedBatchUsage[key]) continue
|
||||
if (this.isMapAppend && !this.appendableKeys.includes(key)) continue
|
||||
|
||||
let existingValues = undefined
|
||||
libraryItemsToMap.forEach((li) => {
|
||||
if (key === 'tags') {
|
||||
if (!existingValues) existingValues = []
|
||||
li.media.tags.forEach((tag) => {
|
||||
if (!existingValues.includes(tag)) {
|
||||
existingValues.push(tag)
|
||||
}
|
||||
})
|
||||
} else if (key === 'authors') {
|
||||
if (!existingValues) existingValues = []
|
||||
li.media.metadata[key].forEach((entity) => {
|
||||
if (!existingValues.some((au) => au.id === entity.id)) {
|
||||
existingValues.push({
|
||||
id: entity.id,
|
||||
name: entity.name
|
||||
})
|
||||
}
|
||||
})
|
||||
} else if (key === 'series') {
|
||||
if (!existingValues) existingValues = []
|
||||
li.media.metadata[key].forEach((entity) => {
|
||||
if (!existingValues.includes(entity.name)) {
|
||||
existingValues.push(entity.name)
|
||||
}
|
||||
})
|
||||
} else if (key === 'genres' || key === 'narrators') {
|
||||
if (!existingValues) existingValues = []
|
||||
li.media.metadata[key].forEach((item) => {
|
||||
if (!existingValues.includes(item)) {
|
||||
existingValues.push(item)
|
||||
}
|
||||
})
|
||||
} else if (existingValues === undefined) {
|
||||
existingValues = li.media.metadata[key]
|
||||
}
|
||||
})
|
||||
|
||||
this.batchDetails[key] = existingValues
|
||||
}
|
||||
},
|
||||
handleItemChange(itemChange) {
|
||||
if (!itemChange.hasChanges) {
|
||||
this.itemsWithChanges = this.itemsWithChanges.filter((id) => id !== itemChange.libraryItemId)
|
||||
|
||||
@@ -14,11 +14,7 @@
|
||||
<h1 class="text-xl pl-2">{{ username }}</h1>
|
||||
</div>
|
||||
<div v-if="userToken" class="flex text-xs mt-4">
|
||||
<ui-text-input-with-label :label="$strings.LabelApiToken" :value="userToken" readonly />
|
||||
|
||||
<div class="px-1 mt-8 cursor-pointer" @click="copyToClipboard(userToken)">
|
||||
<span class="material-symbols pl-2 text-base">content_copy</span>
|
||||
</div>
|
||||
<ui-text-input-with-label :label="$strings.LabelApiToken" :value="userToken" readonly show-copy />
|
||||
</div>
|
||||
<div class="w-full h-px bg-white bg-opacity-10 my-2" />
|
||||
<div class="py-2">
|
||||
@@ -140,9 +136,6 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
copyToClipboard(str) {
|
||||
this.$copyToClipboard(str, this)
|
||||
},
|
||||
async init() {
|
||||
this.listeningSessions = await this.$axios
|
||||
.$get(`/api/users/${this.user.id}/listening-sessions?page=0&itemsPerPage=10`)
|
||||
|
||||
@@ -128,12 +128,11 @@ Vue.prototype.$sanitizeSlug = (str) => {
|
||||
return str
|
||||
}
|
||||
|
||||
Vue.prototype.$copyToClipboard = (str, ctx) => {
|
||||
Vue.prototype.$copyToClipboard = (str) => {
|
||||
return new Promise((resolve) => {
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(str).then(
|
||||
() => {
|
||||
if (ctx) ctx.$toast.success('Copied to clipboard')
|
||||
resolve(true)
|
||||
},
|
||||
(err) => {
|
||||
@@ -152,7 +151,6 @@ Vue.prototype.$copyToClipboard = (str, ctx) => {
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(el)
|
||||
|
||||
if (ctx) ctx.$toast.success('Copied to clipboard')
|
||||
resolve(true)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
"ButtonApplyChapters": "Apply Chapters",
|
||||
"ButtonAuthors": "Authors",
|
||||
"ButtonBack": "Back",
|
||||
"ButtonBatchEditPopulateFromExisting": "Populate from existing",
|
||||
"ButtonBatchEditPopulateMapDetails": "Populate map details",
|
||||
"ButtonBrowseForFolder": "Browse for Folder",
|
||||
"ButtonCancel": "Cancel",
|
||||
"ButtonCancelEncode": "Cancel Encode",
|
||||
@@ -704,6 +706,8 @@
|
||||
"MessageBackupsLocationEditNote": "Note: Updating the backup location will not move or modify existing backups",
|
||||
"MessageBackupsLocationNoEditNote": "Note: The backup location is set through an environment variable and cannot be changed here.",
|
||||
"MessageBackupsLocationPathEmpty": "Backup location path cannot be empty",
|
||||
"MessageBatchEditPopulateMapDetailsAllHelp": "Populate enabled fields with data from all items. Fields with multiple values will be merged",
|
||||
"MessageBatchEditPopulateMapDetailsItemHelp": "Populate enabled map details fields with data from this item",
|
||||
"MessageBatchQuickMatchDescription": "Quick Match will attempt to add missing covers and metadata for the selected items. Enable the options below to allow Quick Match to overwrite existing covers and/or metadata.",
|
||||
"MessageBookshelfNoCollections": "You haven't made any collections yet",
|
||||
"MessageBookshelfNoRSSFeeds": "No RSS feeds are open",
|
||||
|
||||
Reference in New Issue
Block a user