mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2024-12-20 19:06:06 +01:00
Add:Delete library files, condense item options in more menu #1439
This commit is contained in:
parent
24ef105732
commit
5a21e63d0b
@ -18,25 +18,10 @@
|
||||
<th class="text-left px-4">{{ $strings.LabelPath }}</th>
|
||||
<th class="text-left w-24 min-w-24">{{ $strings.LabelSize }}</th>
|
||||
<th class="text-left px-4 w-24">{{ $strings.LabelType }}</th>
|
||||
<th v-if="userCanDownload && !isMissing" class="text-center w-20">{{ $strings.LabelDownload }}</th>
|
||||
<th v-if="userCanDelete || userCanDownload" class="text-center w-16"></th>
|
||||
</tr>
|
||||
<template v-for="file in files">
|
||||
<tr :key="file.path">
|
||||
<td class="px-4">
|
||||
{{ showFullPath ? file.metadata.path : file.metadata.relPath }}
|
||||
</td>
|
||||
<td class="font-mono">
|
||||
{{ $bytesPretty(file.metadata.size) }}
|
||||
</td>
|
||||
<td class="text-xs">
|
||||
<div class="flex items-center">
|
||||
<p>{{ file.fileType }}</p>
|
||||
</div>
|
||||
</td>
|
||||
<td v-if="userCanDownload && !isMissing" class="text-center">
|
||||
<a :href="`${$config.routerBasePath}/s/item/${libraryItemId}/${$encodeUriPath(file.metadata.relPath).replace(/^\//, '')}?token=${userToken}`" download><span class="material-icons icon-text">download</span></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tables-library-files-table-row :key="file.path" :libraryItemId="libraryItemId" :showFullPath="showFullPath" :file="file" />
|
||||
</template>
|
||||
</table>
|
||||
</div>
|
||||
@ -68,6 +53,9 @@ export default {
|
||||
userCanDownload() {
|
||||
return this.$store.getters['user/getUserCanDownload']
|
||||
},
|
||||
userCanDelete() {
|
||||
return this.$store.getters['user/getUserCanDelete']
|
||||
},
|
||||
userIsAdmin() {
|
||||
return this.$store.getters['user/getIsAdminOrUp']
|
||||
}
|
||||
|
105
client/components/tables/LibraryFilesTableRow.vue
Normal file
105
client/components/tables/LibraryFilesTableRow.vue
Normal file
@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<tr>
|
||||
<td class="px-4">
|
||||
{{ showFullPath ? file.metadata.path : file.metadata.relPath }}
|
||||
</td>
|
||||
<td class="font-mono">
|
||||
{{ $bytesPretty(file.metadata.size) }}
|
||||
</td>
|
||||
<td class="text-xs">
|
||||
<div class="flex items-center">
|
||||
<p>{{ file.fileType }}</p>
|
||||
</div>
|
||||
</td>
|
||||
<td v-if="contextMenuItems.length" class="text-center">
|
||||
<ui-context-menu-dropdown :items="contextMenuItems" menu-width="110px" @action="contextMenuAction" />
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
libraryItemId: String,
|
||||
showFullPath: Boolean,
|
||||
file: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
userToken() {
|
||||
return this.$store.getters['user/getToken']
|
||||
},
|
||||
userCanDownload() {
|
||||
return this.$store.getters['user/getUserCanDownload']
|
||||
},
|
||||
userCanDelete() {
|
||||
return this.$store.getters['user/getUserCanDelete']
|
||||
},
|
||||
downloadUrl() {
|
||||
return `${process.env.serverUrl}/s/item/${this.libraryItemId}/${this.$encodeUriPath(this.file.metadata.relPath).replace(/^\//, '')}?token=${this.userToken}`
|
||||
},
|
||||
contextMenuItems() {
|
||||
const items = []
|
||||
if (this.userCanDownload) {
|
||||
items.push({
|
||||
text: this.$strings.LabelDownload,
|
||||
action: 'download'
|
||||
})
|
||||
}
|
||||
if (this.userCanDelete) {
|
||||
items.push({
|
||||
text: this.$strings.ButtonDelete,
|
||||
action: 'delete'
|
||||
})
|
||||
}
|
||||
return items
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
contextMenuAction(action) {
|
||||
if (action === 'delete') {
|
||||
this.deleteLibraryFile()
|
||||
} else if (action === 'download') {
|
||||
this.downloadLibraryFile()
|
||||
}
|
||||
},
|
||||
deleteLibraryFile() {
|
||||
const payload = {
|
||||
message: 'This will delete the file from your file system. Are you sure?',
|
||||
callback: (confirmed) => {
|
||||
if (confirmed) {
|
||||
this.$axios
|
||||
.$delete(`/api/items/${this.libraryItemId}/file/${this.file.ino}`)
|
||||
.then(() => {
|
||||
this.$toast.success('File deleted')
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to delete file', error)
|
||||
this.$toast.error('Failed to delete file')
|
||||
})
|
||||
}
|
||||
},
|
||||
type: 'yesNo'
|
||||
}
|
||||
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||
},
|
||||
downloadLibraryFile() {
|
||||
const a = document.createElement('a')
|
||||
a.style.display = 'none'
|
||||
a.href = this.downloadUrl
|
||||
a.download = this.file.metadata.filename
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
setTimeout(() => {
|
||||
a.remove()
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
@ -1,11 +1,13 @@
|
||||
<template>
|
||||
<div class="relative h-9 w-9" v-click-outside="clickOutsideObj">
|
||||
<button type="button" :disabled="disabled" class="relative h-full w-full flex items-center justify-center shadow-sm pl-3 pr-3 text-left focus:outline-none cursor-pointer text-gray-100 hover:text-gray-200 rounded-full hover:bg-white/5" aria-haspopup="listbox" :aria-expanded="showMenu" @click.stop.prevent="clickShowMenu">
|
||||
<span class="material-icons" :class="iconClass">more_vert</span>
|
||||
</button>
|
||||
<slot :disabled="disabled" :showMenu="showMenu" :clickShowMenu="clickShowMenu">
|
||||
<button type="button" :disabled="disabled" class="relative h-full w-full flex items-center justify-center shadow-sm pl-3 pr-3 text-left focus:outline-none cursor-pointer text-gray-100 hover:text-gray-200 rounded-full hover:bg-white/5" aria-haspopup="listbox" :aria-expanded="showMenu" @click.stop.prevent="clickShowMenu">
|
||||
<span class="material-icons" :class="iconClass">more_vert</span>
|
||||
</button>
|
||||
</slot>
|
||||
|
||||
<transition name="menu">
|
||||
<div v-show="showMenu" class="absolute right-0 mt-1 z-10 bg-bg border border-black-200 shadow-lg max-h-56 w-48 rounded-md py-1 overflow-auto focus:outline-none sm:text-sm">
|
||||
<div v-show="showMenu" class="absolute right-0 mt-1 z-10 bg-bg border border-black-200 shadow-lg max-h-56 rounded-md py-1 overflow-auto focus:outline-none sm:text-sm" :style="{ width: menuWidth }">
|
||||
<template v-for="(item, index) in items">
|
||||
<div :key="index" class="flex items-center px-2 py-1.5 hover:bg-white hover:bg-opacity-5 text-white text-xs cursor-pointer" @click.stop="clickAction(item.action)">
|
||||
<p>{{ item.text }}</p>
|
||||
@ -27,6 +29,10 @@ export default {
|
||||
iconClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
menuWidth: {
|
||||
type: String,
|
||||
default: '192px'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
|
@ -201,34 +201,18 @@
|
||||
<ui-read-icon-btn :disabled="isProcessingReadUpdate" :is-read="userIsFinished" class="mx-0.5" @click="toggleFinished" />
|
||||
</ui-tooltip>
|
||||
|
||||
<ui-tooltip v-if="showCollectionsButton" :text="$strings.LabelCollections" direction="top">
|
||||
<ui-icon-btn icon="collections_bookmark" class="mx-0.5" outlined @click="collectionsClick" />
|
||||
</ui-tooltip>
|
||||
|
||||
<ui-tooltip v-if="!isPodcast && tracks.length" :text="$strings.LabelYourPlaylists" direction="top">
|
||||
<ui-icon-btn icon="playlist_add" class="mx-0.5" outlined @click="playlistsClick" />
|
||||
</ui-tooltip>
|
||||
|
||||
<!-- Only admin or root user can download new episodes -->
|
||||
<ui-tooltip v-if="isPodcast && userIsAdminOrUp" :text="$strings.LabelFindEpisodes" direction="top">
|
||||
<ui-icon-btn icon="search" class="mx-0.5" :loading="fetchingRSSFeed" outlined @click="findEpisodesClick" />
|
||||
</ui-tooltip>
|
||||
|
||||
<ui-tooltip v-if="bookmarks.length" :text="$strings.LabelYourBookmarks" direction="top">
|
||||
<ui-icon-btn :icon="bookmarks.length ? 'bookmarks' : 'bookmark_border'" class="mx-0.5" @click="clickBookmarksBtn" />
|
||||
</ui-tooltip>
|
||||
|
||||
<!-- RSS feed -->
|
||||
<ui-tooltip v-if="showRssFeedBtn" :text="$strings.LabelOpenRSSFeed" direction="top">
|
||||
<ui-icon-btn icon="rss_feed" class="mx-0.5" :bg-color="rssFeed ? 'success' : 'primary'" outlined @click="clickRSSFeed" />
|
||||
</ui-tooltip>
|
||||
|
||||
<!-- Download -->
|
||||
<ui-tooltip v-if="userCanDownload" :text="$strings.LabelDownload" direction="top">
|
||||
<a :href="downloadPath" class="h-9 w-9 mx-0.5 rounded-md flex items-center justify-center relative bg-primary border border-gray-600">
|
||||
<span class="material-icons-outlined" :style="{ fontSize: '1.4rem' }">download</span>
|
||||
</a>
|
||||
</ui-tooltip>
|
||||
<ui-context-menu-dropdown v-if="contextMenuItems.length" :items="contextMenuItems" menu-width="148px" @action="contextMenuAction">
|
||||
<template #default="{ showMenu, clickShowMenu, disabled }">
|
||||
<button type="button" :disabled="disabled" class="mx-0.5 icon-btn bg-primary border border-gray-600 w-9 h-9 rounded-md flex items-center justify-center relative" aria-haspopup="listbox" :aria-expanded="showMenu" @click.stop.prevent="clickShowMenu">
|
||||
<span class="material-icons">more_vert</span>
|
||||
</button>
|
||||
</template>
|
||||
</ui-context-menu-dropdown>
|
||||
</div>
|
||||
|
||||
<div class="my-4 max-w-2xl">
|
||||
@ -294,7 +278,7 @@ export default {
|
||||
userToken() {
|
||||
return this.$store.getters['user/getToken']
|
||||
},
|
||||
downloadPath() {
|
||||
downloadUrl() {
|
||||
return `${process.env.serverUrl}/api/items/${this.libraryItemId}/download?token=${this.userToken}`
|
||||
},
|
||||
dateFormat() {
|
||||
@ -309,9 +293,6 @@ export default {
|
||||
userIsAdminOrUp() {
|
||||
return this.$store.getters['user/getIsAdminOrUp']
|
||||
},
|
||||
isFile() {
|
||||
return this.libraryItem.isFile
|
||||
},
|
||||
bookCoverAspectRatio() {
|
||||
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||
},
|
||||
@ -321,6 +302,9 @@ export default {
|
||||
isDeveloperMode() {
|
||||
return this.$store.state.developerMode
|
||||
},
|
||||
isFile() {
|
||||
return this.libraryItem.isFile
|
||||
},
|
||||
isBook() {
|
||||
return this.libraryItem.mediaType === 'book'
|
||||
},
|
||||
@ -539,12 +523,56 @@ export default {
|
||||
},
|
||||
showCollectionsButton() {
|
||||
return this.isBook && this.userCanUpdate
|
||||
},
|
||||
contextMenuItems() {
|
||||
const items = []
|
||||
|
||||
if (this.showCollectionsButton) {
|
||||
items.push({
|
||||
text: this.$strings.LabelCollections,
|
||||
action: 'collections'
|
||||
})
|
||||
}
|
||||
|
||||
if (!this.isPodcast && this.tracks.length) {
|
||||
items.push({
|
||||
text: this.$strings.LabelYourPlaylists,
|
||||
action: 'playlists'
|
||||
})
|
||||
}
|
||||
|
||||
if (this.bookmarks.length) {
|
||||
items.push({
|
||||
text: this.$strings.LabelYourBookmarks,
|
||||
action: 'bookmarks'
|
||||
})
|
||||
}
|
||||
|
||||
if (this.showRssFeedBtn) {
|
||||
items.push({
|
||||
text: this.$strings.LabelOpenRSSFeed,
|
||||
action: 'rss-feeds'
|
||||
})
|
||||
}
|
||||
|
||||
if (this.userCanDownload) {
|
||||
items.push({
|
||||
text: this.$strings.LabelDownload,
|
||||
action: 'download'
|
||||
})
|
||||
}
|
||||
|
||||
// if (this.userCanDelete) {
|
||||
// items.push({
|
||||
// text: this.$strings.ButtonDelete,
|
||||
// action: 'delete'
|
||||
// })
|
||||
// }
|
||||
|
||||
return items
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
clickBookmarksBtn() {
|
||||
this.showBookmarksModal = true
|
||||
},
|
||||
selectBookmark(bookmark) {
|
||||
if (!bookmark) return
|
||||
if (this.isStreaming) {
|
||||
@ -720,14 +748,6 @@ export default {
|
||||
})
|
||||
}
|
||||
},
|
||||
collectionsClick() {
|
||||
this.$store.commit('setSelectedLibraryItem', this.libraryItem)
|
||||
this.$store.commit('globals/setShowCollectionsModal', true)
|
||||
},
|
||||
playlistsClick() {
|
||||
this.$store.commit('globals/setSelectedPlaylistItems', [{ libraryItem: this.libraryItem }])
|
||||
this.$store.commit('globals/setShowPlaylistsModal', true)
|
||||
},
|
||||
clickRSSFeed() {
|
||||
this.$store.commit('globals/setRSSFeedOpenCloseModal', {
|
||||
id: this.libraryItemId,
|
||||
@ -785,6 +805,54 @@ export default {
|
||||
}
|
||||
this.$store.commit('addItemToQueue', queueItem)
|
||||
}
|
||||
},
|
||||
downloadLibraryItem() {
|
||||
const a = document.createElement('a')
|
||||
a.style.display = 'none'
|
||||
a.href = this.downloadUrl
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
setTimeout(() => {
|
||||
a.remove()
|
||||
})
|
||||
},
|
||||
deleteLibraryItem() {
|
||||
const payload = {
|
||||
message: 'This will delete the library item files from your file system. Are you sure?',
|
||||
callback: (confirmed) => {
|
||||
if (confirmed) {
|
||||
this.$axios
|
||||
.$delete(`/api/items/${this.libraryItemId}?hard=1`)
|
||||
.then(() => {
|
||||
this.$toast.success('Item deleted')
|
||||
this.$router.replace(`/library/${this.libraryId}/bookshelf`)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to delete item', error)
|
||||
this.$toast.error('Failed to delete item')
|
||||
})
|
||||
}
|
||||
},
|
||||
type: 'yesNo'
|
||||
}
|
||||
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||
},
|
||||
contextMenuAction(action) {
|
||||
if (action === 'collections') {
|
||||
this.$store.commit('setSelectedLibraryItem', this.libraryItem)
|
||||
this.$store.commit('globals/setShowCollectionsModal', true)
|
||||
} else if (action === 'playlists') {
|
||||
this.$store.commit('globals/setSelectedPlaylistItems', [{ libraryItem: this.libraryItem }])
|
||||
this.$store.commit('globals/setShowPlaylistsModal', true)
|
||||
} else if (action === 'bookmarks') {
|
||||
this.showBookmarksModal = true
|
||||
} else if (action === 'rss-feeds') {
|
||||
this.clickRSSFeed()
|
||||
} else if (action === 'download') {
|
||||
this.downloadLibraryItem()
|
||||
} else if (action === 'delete') {
|
||||
this.deleteLibraryItem()
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
@ -482,6 +482,28 @@ class LibraryItemController {
|
||||
res.json(toneData)
|
||||
}
|
||||
|
||||
async deleteLibraryFile(req, res) {
|
||||
const libraryFile = req.libraryItem.libraryFiles.find(lf => lf.ino === req.params.ino)
|
||||
if (!libraryFile) {
|
||||
Logger.error(`[LibraryItemController] Unable to delete library file. Not found. "${req.params.ino}"`)
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
|
||||
await fs.remove(libraryFile.metadata.path)
|
||||
req.libraryItem.removeLibraryFile(req.params.ino)
|
||||
|
||||
if (req.libraryItem.media.removeFileWithInode(req.params.ino)) {
|
||||
// If book has no more media files then mark it as missing
|
||||
if (req.libraryItem.mediaType === 'book' && !req.libraryItem.media.hasMediaEntities) {
|
||||
req.libraryItem.setMissing()
|
||||
}
|
||||
}
|
||||
req.libraryItem.updatedAt = Date.now()
|
||||
await this.db.updateLibraryItem(req.libraryItem)
|
||||
SocketAuthority.emitter('item_updated', req.libraryItem.toJSONExpanded())
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
middleware(req, res, next) {
|
||||
const item = this.db.libraryItems.find(li => li.id === req.params.id)
|
||||
if (!item || !item.media) return res.sendStatus(404)
|
||||
|
@ -166,7 +166,11 @@ class Podcast {
|
||||
}
|
||||
|
||||
removeFileWithInode(inode) {
|
||||
this.episodes = this.episodes.filter(ep => ep.audioFile.ino !== inode)
|
||||
const hasEpisode = this.episodes.some(ep => ep.audioFile.ino === inode)
|
||||
if (hasEpisode) {
|
||||
this.episodes = this.episodes.filter(ep => ep.audioFile.ino !== inode)
|
||||
}
|
||||
return hasEpisode
|
||||
}
|
||||
|
||||
findFileWithInode(inode) {
|
||||
|
@ -112,6 +112,7 @@ class ApiRouter {
|
||||
this.router.get('/items/:id/tone-object', LibraryItemController.middleware.bind(this), LibraryItemController.getToneMetadataObject.bind(this))
|
||||
this.router.post('/items/:id/chapters', LibraryItemController.middleware.bind(this), LibraryItemController.updateMediaChapters.bind(this))
|
||||
this.router.post('/items/:id/tone-scan/:index?', LibraryItemController.middleware.bind(this), LibraryItemController.toneScan.bind(this))
|
||||
this.router.delete('/items/:id/file/:ino', LibraryItemController.middleware.bind(this), LibraryItemController.deleteLibraryFile.bind(this))
|
||||
|
||||
this.router.post('/items/batch/delete', LibraryItemController.batchDelete.bind(this))
|
||||
this.router.post('/items/batch/update', LibraryItemController.batchUpdate.bind(this))
|
||||
|
Loading…
Reference in New Issue
Block a user