mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-08-03 17:54:54 +02:00
Merge branch 'advplyr:master' into dewyer/add-custom-metadata-provider
This commit is contained in:
commit
6ef4944d89
31 changed files with 764 additions and 309 deletions
|
@ -8,10 +8,10 @@
|
|||
<!-- Alternative bookshelf title/author/sort -->
|
||||
<div v-if="isAlternativeBookshelfView || isAuthorBookshelfView" class="absolute left-0 z-50 w-full" :style="{ bottom: `-${titleDisplayBottomOffset}rem` }">
|
||||
<div :style="{ fontSize: 0.9 * sizeMultiplier + 'rem' }">
|
||||
<div class="flex items-center">
|
||||
<span class="truncate">{{ displayTitle }}</span>
|
||||
<ui-tooltip :text="displayTitle" :disabled="!displayTitleTruncated" direction="bottom" :delayOnShow="500" class="flex items-center">
|
||||
<p ref="displayTitle" class="truncate">{{ displayTitle }}</p>
|
||||
<widgets-explicit-indicator :explicit="isExplicit" />
|
||||
</div>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
<p class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displayLineTwo || ' ' }}</p>
|
||||
<p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p>
|
||||
|
@ -164,6 +164,7 @@ export default {
|
|||
imageReady: false,
|
||||
selected: false,
|
||||
isSelectionMode: false,
|
||||
displayTitleTruncated: false,
|
||||
showCoverBg: false
|
||||
}
|
||||
},
|
||||
|
@ -642,6 +643,12 @@ export default {
|
|||
}
|
||||
|
||||
this.libraryItem = libraryItem
|
||||
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.displayTitle) {
|
||||
this.displayTitleTruncated = this.$refs.displayTitle.scrollWidth > this.$refs.displayTitle.clientWidth
|
||||
}
|
||||
})
|
||||
},
|
||||
clickCard(e) {
|
||||
if (this.processing) return
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
<ui-btn class="w-full mt-2" color="primary" @click="browseForFolder">{{ $strings.ButtonBrowseForFolder }}</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
<modals-libraries-folder-chooser v-else :paths="folderPaths" @back="showDirectoryPicker = false" @select="selectFolder" />
|
||||
<modals-libraries-lazy-folder-chooser v-else :paths="folderPaths" @back="showDirectoryPicker = false" @select="selectFolder" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -4,29 +4,32 @@
|
|||
<span class="material-icons text-3xl cursor-pointer hover:text-gray-300" @click="$emit('back')">arrow_back</span>
|
||||
<p class="px-4 text-xl">{{ $strings.HeaderChooseAFolder }}</p>
|
||||
</div>
|
||||
<div v-if="allFolders.length" class="w-full bg-primary bg-opacity-70 py-1 px-4 mb-2">
|
||||
<p class="font-mono truncate">{{ selectedPath || '\\' }}</p>
|
||||
<div v-if="rootDirs.length" class="w-full bg-primary bg-opacity-70 py-1 px-4 mb-2">
|
||||
<p class="font-mono truncate">{{ selectedPath || '/' }}</p>
|
||||
</div>
|
||||
<div v-if="allFolders.length" class="flex bg-primary bg-opacity-50 p-4 folder-container">
|
||||
<div v-if="rootDirs.length" class="relative flex bg-primary bg-opacity-50 p-4 folder-container">
|
||||
<div class="w-1/2 border-r border-bg h-full overflow-y-auto">
|
||||
<div v-if="level > 0" class="w-full p-1 cursor-pointer flex items-center" @click="goBack">
|
||||
<div v-if="level > 0" class="w-full p-1 cursor-pointer flex items-center hover:bg-white/10" @click="goBack">
|
||||
<span class="material-icons bg-opacity-50 text-yellow-200" style="font-size: 1.2rem">folder</span>
|
||||
<p class="text-base font-mono px-2">..</p>
|
||||
</div>
|
||||
<div v-for="dir in _directories" :key="dir.path" class="dir-item w-full p-1 cursor-pointer flex items-center hover:text-white text-gray-200" :class="dir.className" @click="selectDir(dir)">
|
||||
<div v-for="dir in _directories" :key="dir.path" class="dir-item w-full p-1 cursor-pointer flex items-center hover:text-white text-gray-200 hover:bg-white/10" :class="dir.className" @click="selectDir(dir)">
|
||||
<span class="material-icons bg-opacity-50 text-yellow-200" style="font-size: 1.2rem">folder</span>
|
||||
<p class="text-base font-mono px-2 truncate">{{ dir.dirname }}</p>
|
||||
<span v-if="dir.dirs && dir.dirs.length && dir.path === selectedPath" class="material-icons" style="font-size: 1.1rem">arrow_right</span>
|
||||
<span v-if="dir.path === selectedPath" class="material-icons" style="font-size: 1.1rem">arrow_right</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-1/2 h-full overflow-y-auto">
|
||||
<div v-for="dir in _subdirs" :key="dir.path" :class="dir.className" class="dir-item w-full p-1 cursor-pointer flex items-center hover:text-white text-gray-200" @click="selectSubDir(dir)">
|
||||
<div v-for="dir in _subdirs" :key="dir.path" :class="dir.className" class="dir-item w-full p-1 cursor-pointer flex items-center hover:text-white text-gray-200 hover:bg-white/10" @click="selectSubDir(dir)">
|
||||
<span class="material-icons bg-opacity-50 text-yellow-200" style="font-size: 1.2rem">folder</span>
|
||||
<p class="text-base font-mono px-2 truncate">{{ dir.dirname }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="loadingDirs" class="absolute inset-0 w-full h-full flex items-center justify-center bg-black/10">
|
||||
<ui-loading-indicator />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="loadingFolders" class="py-12 text-center">
|
||||
<div v-else-if="initialLoad" class="py-12 text-center">
|
||||
<p>{{ $strings.MessageLoadingFolders }}</p>
|
||||
</div>
|
||||
<div v-else class="py-12 text-center max-w-sm mx-auto">
|
||||
|
@ -51,11 +54,12 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
loadingFolders: false,
|
||||
allFolders: [],
|
||||
initialLoad: false,
|
||||
loadingDirs: false,
|
||||
isPosix: true,
|
||||
rootDirs: [],
|
||||
directories: [],
|
||||
selectedPath: '',
|
||||
selectedFullPath: '',
|
||||
subdirs: [],
|
||||
level: 0,
|
||||
currentDir: null,
|
||||
|
@ -98,59 +102,88 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
goBack() {
|
||||
var splitPaths = this.selectedPath.split('\\').slice(1)
|
||||
var prev = splitPaths.slice(0, -1).join('\\')
|
||||
async goBack() {
|
||||
let selPath = this.selectedPath.replace(/^\//, '')
|
||||
var splitPaths = selPath.split('/')
|
||||
|
||||
var currDirs = this.allFolders
|
||||
for (let i = 0; i < splitPaths.length; i++) {
|
||||
var _dir = currDirs.find((dir) => dir.dirname === splitPaths[i])
|
||||
if (_dir && _dir.path.slice(1) === prev) {
|
||||
this.directories = currDirs
|
||||
this.selectDir(_dir)
|
||||
return
|
||||
} else if (_dir) {
|
||||
currDirs = _dir.dirs
|
||||
}
|
||||
let previousPath = ''
|
||||
let lookupPath = ''
|
||||
|
||||
if (splitPaths.length > 2) {
|
||||
lookupPath = splitPaths.slice(0, -2).join('/')
|
||||
}
|
||||
previousPath = splitPaths.slice(0, -1).join('/')
|
||||
|
||||
if (!this.isPosix) {
|
||||
// For windows drives add a trailing slash. e.g. C:/
|
||||
if (!this.isPosix && lookupPath.endsWith(':')) {
|
||||
lookupPath += '/'
|
||||
}
|
||||
if (!this.isPosix && previousPath.endsWith(':')) {
|
||||
previousPath += '/'
|
||||
}
|
||||
} else {
|
||||
// Add leading slash
|
||||
if (previousPath) previousPath = '/' + previousPath
|
||||
if (lookupPath) lookupPath = '/' + lookupPath
|
||||
}
|
||||
|
||||
this.level--
|
||||
this.subdirs = this.directories
|
||||
this.selectedPath = previousPath
|
||||
this.directories = await this.fetchDirs(lookupPath, this.level)
|
||||
},
|
||||
selectDir(dir) {
|
||||
async selectDir(dir) {
|
||||
if (dir.isUsed) return
|
||||
this.selectedPath = dir.path
|
||||
this.selectedFullPath = dir.fullPath
|
||||
this.level = dir.level
|
||||
this.subdirs = dir.dirs
|
||||
this.subdirs = await this.fetchDirs(dir.path, dir.level + 1)
|
||||
},
|
||||
selectSubDir(dir) {
|
||||
async selectSubDir(dir) {
|
||||
if (dir.isUsed) return
|
||||
this.selectedPath = dir.path
|
||||
this.selectedFullPath = dir.fullPath
|
||||
this.level = dir.level
|
||||
this.directories = this.subdirs
|
||||
this.subdirs = dir.dirs
|
||||
this.subdirs = await this.fetchDirs(dir.path, dir.level + 1)
|
||||
},
|
||||
selectFolder() {
|
||||
if (!this.selectedPath) {
|
||||
console.error('No Selected path')
|
||||
return
|
||||
}
|
||||
if (this.paths.find((p) => p.startsWith(this.selectedFullPath))) {
|
||||
if (this.paths.find((p) => p.startsWith(this.selectedPath))) {
|
||||
this.$toast.error(`Oops, you cannot add a parent directory of a folder already added`)
|
||||
return
|
||||
}
|
||||
this.$emit('select', this.selectedFullPath)
|
||||
this.$emit('select', this.selectedPath)
|
||||
this.selectedPath = ''
|
||||
this.selectedFullPath = ''
|
||||
},
|
||||
fetchDirs(path, level) {
|
||||
this.loadingDirs = true
|
||||
return this.$axios
|
||||
.$get(`/api/filesystem?path=${path}&level=${level}`)
|
||||
.then((data) => {
|
||||
console.log('Fetched directories', data.directories)
|
||||
this.isPosix = !!data.posix
|
||||
return data.directories
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to get filesystem paths', error)
|
||||
this.$toast.error('Failed to get filesystem paths')
|
||||
return []
|
||||
})
|
||||
.finally(() => {
|
||||
this.loadingDirs = false
|
||||
})
|
||||
},
|
||||
async init() {
|
||||
this.loadingFolders = true
|
||||
this.allFolders = await this.$store.dispatch('libraries/loadFolders')
|
||||
this.loadingFolders = false
|
||||
this.initialLoad = true
|
||||
this.rootDirs = await this.fetchDirs('', 0)
|
||||
this.initialLoad = false
|
||||
|
||||
this.directories = this.allFolders
|
||||
this.directories = this.rootDirs
|
||||
this.subdirs = []
|
||||
this.selectedPath = ''
|
||||
this.selectedFullPath = ''
|
||||
}
|
||||
},
|
||||
mounted() {
|
|
@ -63,7 +63,7 @@ export default {
|
|||
},
|
||||
audioMetatags: {
|
||||
id: 'audioMetatags',
|
||||
name: 'Audio file meta tags',
|
||||
name: 'Audio file meta tags OR ebook metadata',
|
||||
include: true
|
||||
},
|
||||
nfoFile: {
|
||||
|
|
|
@ -68,7 +68,9 @@ export default {
|
|||
selectAll: false,
|
||||
search: null,
|
||||
searchTimeout: null,
|
||||
searchText: null
|
||||
searchText: null,
|
||||
downloadedEpisodeGuidMap: {},
|
||||
downloadedEpisodeUrlMap: {}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
@ -122,11 +124,13 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
getIsEpisodeDownloaded(episode) {
|
||||
return this.itemEpisodes.some((downloadedEpisode) => {
|
||||
if (episode.guid && downloadedEpisode.guid === episode.guid) return true
|
||||
if (!downloadedEpisode.enclosure?.url) return false
|
||||
return this.getCleanEpisodeUrl(downloadedEpisode.enclosure.url) === episode.cleanUrl
|
||||
})
|
||||
if (episode.guid && !!this.downloadedEpisodeGuidMap[episode.guid]) {
|
||||
return true
|
||||
}
|
||||
if (this.downloadedEpisodeUrlMap[episode.cleanUrl]) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
/**
|
||||
* UPDATE: As of v2.4.5 guid is used for matching existing downloaded episodes if it is found on the RSS feed.
|
||||
|
@ -219,6 +223,14 @@ export default {
|
|||
})
|
||||
},
|
||||
init() {
|
||||
this.downloadedEpisodeGuidMap = {}
|
||||
this.downloadedEpisodeUrlMap = {}
|
||||
|
||||
this.itemEpisodes.forEach((episode) => {
|
||||
if (episode.guid) this.downloadedEpisodeGuidMap[episode.guid] = episode.id
|
||||
if (episode.enclosure?.url) this.downloadedEpisodeUrlMap[this.getCleanEpisodeUrl(episode.enclosure.url)] = episode.id
|
||||
})
|
||||
|
||||
this.episodesCleaned = this.episodes
|
||||
.filter((ep) => ep.enclosure?.url)
|
||||
.map((_ep) => {
|
||||
|
|
|
@ -87,7 +87,7 @@ export default {
|
|||
watch: {
|
||||
libraryItem: {
|
||||
handler() {
|
||||
this.init()
|
||||
this.refresh()
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -515,6 +515,10 @@ export default {
|
|||
filterSortChanged() {
|
||||
this.init()
|
||||
},
|
||||
refresh() {
|
||||
this.episodesCopy = this.episodes.map((ep) => ({ ...ep }))
|
||||
this.init()
|
||||
},
|
||||
init() {
|
||||
this.destroyEpisodeComponents()
|
||||
this.totalEpisodes = this.episodesList.length
|
||||
|
|
|
@ -15,6 +15,13 @@ export default {
|
|||
type: String,
|
||||
default: 'right'
|
||||
},
|
||||
/**
|
||||
* Delay showing the tooltip after X milliseconds of hovering
|
||||
*/
|
||||
delayOnShow: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
disabled: Boolean
|
||||
},
|
||||
data() {
|
||||
|
@ -22,7 +29,8 @@ export default {
|
|||
tooltip: null,
|
||||
tooltipId: null,
|
||||
isShowing: false,
|
||||
hideTimeout: null
|
||||
hideTimeout: null,
|
||||
delayOnShowTimeout: null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
@ -59,29 +67,44 @@ export default {
|
|||
this.tooltip = tooltip
|
||||
},
|
||||
setTooltipPosition(tooltip) {
|
||||
var boxChow = this.$refs.box.getBoundingClientRect()
|
||||
const boxRect = this.$refs.box.getBoundingClientRect()
|
||||
|
||||
const shouldMount = !tooltip.isConnected
|
||||
|
||||
var shouldMount = !tooltip.isConnected
|
||||
// Calculate size of tooltip
|
||||
if (shouldMount) document.body.appendChild(tooltip)
|
||||
var { width, height } = tooltip.getBoundingClientRect()
|
||||
const tooltipRect = tooltip.getBoundingClientRect()
|
||||
if (shouldMount) tooltip.remove()
|
||||
|
||||
var top = 0
|
||||
var left = 0
|
||||
// Subtracting scrollbar size
|
||||
const windowHeight = window.innerHeight - 8
|
||||
const windowWidth = window.innerWidth - 8
|
||||
|
||||
let top = 0
|
||||
let left = 0
|
||||
if (this.direction === 'right') {
|
||||
top = boxChow.top - height / 2 + boxChow.height / 2
|
||||
left = boxChow.left + boxChow.width + 4
|
||||
top = Math.max(0, boxRect.top - tooltipRect.height / 2 + boxRect.height / 2)
|
||||
left = Math.max(0, boxRect.left + boxRect.width + 4)
|
||||
} else if (this.direction === 'bottom') {
|
||||
top = boxChow.top + boxChow.height + 4
|
||||
left = boxChow.left - width / 2 + boxChow.width / 2
|
||||
top = Math.max(0, boxRect.top + boxRect.height + 4)
|
||||
left = Math.max(0, boxRect.left - tooltipRect.width / 2 + boxRect.width / 2)
|
||||
} else if (this.direction === 'top') {
|
||||
top = boxChow.top - height - 4
|
||||
left = boxChow.left - width / 2 + boxChow.width / 2
|
||||
top = Math.max(0, boxRect.top - tooltipRect.height - 4)
|
||||
left = Math.max(0, boxRect.left - tooltipRect.width / 2 + boxRect.width / 2)
|
||||
} else if (this.direction === 'left') {
|
||||
top = boxChow.top - height / 2 + boxChow.height / 2
|
||||
left = boxChow.left - width - 4
|
||||
top = Math.max(0, boxRect.top - tooltipRect.height / 2 + boxRect.height / 2)
|
||||
left = Math.max(0, boxRect.left - tooltipRect.width - 4)
|
||||
}
|
||||
|
||||
// Shift left if tooltip would overflow the window on the right
|
||||
if (left + tooltipRect.width > windowWidth) {
|
||||
left -= left + tooltipRect.width - windowWidth
|
||||
}
|
||||
// Shift up if tooltip would overflow the window on the bottom
|
||||
if (top + tooltipRect.height > windowHeight) {
|
||||
top -= top + tooltipRect.height - windowHeight
|
||||
}
|
||||
|
||||
tooltip.style.top = top + 'px'
|
||||
tooltip.style.left = left + 'px'
|
||||
},
|
||||
|
@ -107,15 +130,33 @@ export default {
|
|||
this.isShowing = false
|
||||
},
|
||||
cancelHide() {
|
||||
if (this.hideTimeout) clearTimeout(this.hideTimeout)
|
||||
clearTimeout(this.hideTimeout)
|
||||
},
|
||||
mouseover() {
|
||||
if (!this.isShowing) this.showTooltip()
|
||||
if (this.isShowing || this.disabled) return
|
||||
|
||||
if (this.delayOnShow) {
|
||||
if (this.delayOnShowTimeout) {
|
||||
// Delay already running
|
||||
return
|
||||
}
|
||||
|
||||
this.delayOnShowTimeout = setTimeout(() => {
|
||||
this.showTooltip()
|
||||
this.delayOnShowTimeout = null
|
||||
}, this.delayOnShow)
|
||||
} else {
|
||||
this.showTooltip()
|
||||
}
|
||||
},
|
||||
mouseleave() {
|
||||
if (this.isShowing) {
|
||||
this.hideTimeout = setTimeout(this.hideTooltip, 100)
|
||||
if (!this.isShowing) {
|
||||
clearTimeout(this.delayOnShowTimeout)
|
||||
this.delayOnShowTimeout = null
|
||||
return
|
||||
}
|
||||
|
||||
this.hideTimeout = setTimeout(this.hideTooltip, 100)
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue