diff --git a/.devcontainer/dev.js b/.devcontainer/dev.js
index 0d113a3e..b5f6714e 100644
--- a/.devcontainer/dev.js
+++ b/.devcontainer/dev.js
@@ -5,5 +5,6 @@ module.exports.config = {
ConfigPath: Path.resolve('config'),
MetadataPath: Path.resolve('metadata'),
FFmpegPath: '/usr/bin/ffmpeg',
- FFProbePath: '/usr/bin/ffprobe'
+ FFProbePath: '/usr/bin/ffprobe',
+ SkipBinariesCheck: false
}
\ No newline at end of file
diff --git a/client/components/app/BookShelfToolbar.vue b/client/components/app/BookShelfToolbar.vue
index bd31768b..9064c914 100644
--- a/client/components/app/BookShelfToolbar.vue
+++ b/client/components/app/BookShelfToolbar.vue
@@ -98,6 +98,9 @@
{{ $strings.ButtonMatchAllAuthors }}
+
+
+
@@ -183,6 +186,30 @@ export default {
}
]
},
+ authorSortItems() {
+ return [
+ {
+ text: this.$strings.LabelAuthorFirstLast,
+ value: 'name'
+ },
+ {
+ text: this.$strings.LabelAuthorLastFirst,
+ value: 'lastFirst'
+ },
+ {
+ text: this.$strings.LabelNumberOfBooks,
+ value: 'numBooks'
+ },
+ {
+ text: this.$strings.LabelAddedAt,
+ value: 'addedAt'
+ },
+ {
+ text: this.$strings.LabelUpdatedAt,
+ value: 'updatedAt'
+ }
+ ]
+ },
userIsAdminOrUp() {
return this.$store.getters['user/getIsAdminOrUp']
},
@@ -455,6 +482,9 @@ export default {
updateCollapseBookSeries() {
this.saveSettings()
},
+ updateAuthorSort() {
+ this.saveSettings()
+ },
saveSettings() {
this.$store.dispatch('user/updateUserSettings', this.settings)
},
diff --git a/client/components/controls/LibraryFilterSelect.vue b/client/components/controls/LibraryFilterSelect.vue
index 5ed4400b..04dd11af 100644
--- a/client/components/controls/LibraryFilterSelect.vue
+++ b/client/components/controls/LibraryFilterSelect.vue
@@ -368,9 +368,17 @@ export default {
id: 'ebook',
name: this.$strings.LabelHasEbook
},
+ {
+ id: 'no-ebook',
+ name: this.$strings.LabelMissingEbook
+ },
{
id: 'supplementary',
name: this.$strings.LabelHasSupplementaryEbook
+ },
+ {
+ id: 'no-supplementary',
+ name: this.$strings.LabelMissingSupplementaryEbook
}
]
},
diff --git a/client/components/modals/item/tabs/Match.vue b/client/components/modals/item/tabs/Match.vue
index 7051a444..72aa7116 100644
--- a/client/components/modals/item/tabs/Match.vue
+++ b/client/components/modals/item/tabs/Match.vue
@@ -49,8 +49,8 @@
diff --git a/client/components/modals/libraries/EditModal.vue b/client/components/modals/libraries/EditModal.vue
index 2a68dd63..27e3ec6d 100644
--- a/client/components/modals/libraries/EditModal.vue
+++ b/client/components/modals/libraries/EditModal.vue
@@ -127,6 +127,7 @@ export default {
skipMatchingMediaWithIsbn: false,
autoScanCronExpression: null,
hideSingleBookSeries: false,
+ onlyShowLaterBooksInContinueSeries: false,
metadataPrecedence: ['folderStructure', 'audioMetatags', 'nfoFile', 'txtFiles', 'opfFile', 'absMetadata']
}
}
diff --git a/client/components/modals/libraries/LibrarySettings.vue b/client/components/modals/libraries/LibrarySettings.vue
index 4712d6a2..5a8a7a40 100644
--- a/client/components/modals/libraries/LibrarySettings.vue
+++ b/client/components/modals/libraries/LibrarySettings.vue
@@ -49,6 +49,17 @@
+
+
+
+
+
+ {{ $strings.LabelSettingsOnlyShowLaterBooksInContinueSeries }}
+ info_outlined
+
+
+
+
@@ -73,6 +84,7 @@ export default {
skipMatchingMediaWithIsbn: false,
audiobooksOnly: false,
hideSingleBookSeries: false,
+ onlyShowLaterBooksInContinueSeries: false,
podcastSearchRegion: 'us'
}
},
@@ -107,6 +119,7 @@ export default {
skipMatchingMediaWithIsbn: !!this.skipMatchingMediaWithIsbn,
audiobooksOnly: !!this.audiobooksOnly,
hideSingleBookSeries: !!this.hideSingleBookSeries,
+ onlyShowLaterBooksInContinueSeries: !!this.onlyShowLaterBooksInContinueSeries,
podcastSearchRegion: this.podcastSearchRegion
}
}
@@ -121,6 +134,7 @@ export default {
this.skipMatchingMediaWithIsbn = !!this.librarySettings.skipMatchingMediaWithIsbn
this.audiobooksOnly = !!this.librarySettings.audiobooksOnly
this.hideSingleBookSeries = !!this.librarySettings.hideSingleBookSeries
+ this.onlyShowLaterBooksInContinueSeries = !!this.librarySettings.onlyShowLaterBooksInContinueSeries
this.podcastSearchRegion = this.librarySettings.podcastSearchRegion || 'us'
}
},
diff --git a/client/components/player/PlayerTrackBar.vue b/client/components/player/PlayerTrackBar.vue
index 2f832785..3afc5d06 100644
--- a/client/components/player/PlayerTrackBar.vue
+++ b/client/components/player/PlayerTrackBar.vue
@@ -57,7 +57,6 @@ export default {
},
watch: {
duration: {
- immediate: true,
handler() {
this.setChapterTicks()
}
@@ -205,10 +204,14 @@ export default {
},
windowResize() {
this.setTrackWidth()
+ this.setChapterTicks()
+ this.updatePlayedTrackWidth()
+ this.updateBufferTrack()
}
},
mounted() {
this.setTrackWidth()
+ this.setChapterTicks()
window.addEventListener('resize', this.windowResize)
},
beforeDestroy() {
diff --git a/client/components/ui/MultiSelect.vue b/client/components/ui/MultiSelect.vue
index 2009b28d..516b062d 100644
--- a/client/components/ui/MultiSelect.vue
+++ b/client/components/ui/MultiSelect.vue
@@ -11,13 +11,13 @@
{{ item }}
-
+
- -
+
-
{{ item }}
@@ -54,7 +54,7 @@ export default {
menuDisabled: {
type: Boolean,
default: false
- },
+ }
},
data() {
return {
@@ -62,7 +62,9 @@ export default {
currentSearch: null,
typingTimeout: null,
isFocused: false,
- menu: null
+ menu: null,
+ filteredItems: null,
+ selectedMenuItemIndex: null
}
},
watch: {
@@ -91,24 +93,63 @@ export default {
return classes.join(' ')
},
itemsToShow() {
- if (!this.currentSearch || !this.textInput) {
+ if (!this.currentSearch || !this.textInput || !this.filteredItems) {
return this.items
}
- return this.items.filter((i) => {
- var iValue = String(i).toLowerCase()
- return iValue.includes(this.currentSearch.toLowerCase())
- })
+ return this.filteredItems
}
},
methods: {
editItem(item) {
this.$emit('edit', item)
},
- keydownInput() {
+ search() {
+ if (!this.textInput) {
+ this.filteredItems = null
+ return
+ }
+ this.currentSearch = this.textInput
+
+ const results = this.items.filter((i) => {
+ var iValue = String(i).toLowerCase()
+ return iValue.includes(this.currentSearch.toLowerCase())
+ })
+
+ this.filteredItems = results || []
+ },
+ keydownInput(event) {
+ let items = this.itemsToShow
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
+ event.preventDefault()
+ if (!items.length) return
+ if (event.key === 'ArrowDown') {
+ if (this.selectedMenuItemIndex === null) {
+ this.selectedMenuItemIndex = 0
+ } else {
+ this.selectedMenuItemIndex = Math.min(this.selectedMenuItemIndex + 1, items.length - 1)
+ }
+ } else if (event.key === 'ArrowUp') {
+ if (this.selectedMenuItemIndex === null) {
+ this.selectedMenuItemIndex = items.length - 1
+ } else {
+ this.selectedMenuItemIndex = Math.max(this.selectedMenuItemIndex - 1, 0)
+ }
+ }
+ this.recalcScroll()
+ return
+ } else if (event.key === 'Enter') {
+ if (this.selectedMenuItemIndex !== null) {
+ this.clickedOption(event, items[this.selectedMenuItemIndex])
+ } else {
+ this.submitForm()
+ }
+ return
+ }
+ this.selectedMenuItemIndex = null
clearTimeout(this.typingTimeout)
this.typingTimeout = setTimeout(() => {
- this.currentSearch = this.textInput
+ this.search()
}, 100)
this.setInputWidth()
},
@@ -120,6 +161,24 @@ export default {
this.recalcMenuPos()
}, 50)
},
+ recalcScroll() {
+ if (!this.menu) return
+ var menuItems = this.menu.querySelectorAll('li')
+ if (!menuItems.length) return
+ var selectedItem = menuItems[this.selectedMenuItemIndex]
+ if (!selectedItem) return
+ var menuHeight = this.menu.offsetHeight
+ var itemHeight = selectedItem.offsetHeight
+ var itemTop = selectedItem.offsetTop
+ var itemBottom = itemTop + itemHeight
+ if (itemBottom > this.menu.scrollTop + menuHeight) {
+ let menuPaddingBottom = parseFloat(window.getComputedStyle(this.menu).paddingBottom)
+ this.menu.scrollTop = itemBottom - menuHeight + menuPaddingBottom
+ } else if (itemTop < this.menu.scrollTop) {
+ let menuPaddingTop = parseFloat(window.getComputedStyle(this.menu).paddingTop)
+ this.menu.scrollTop = itemTop - menuPaddingTop
+ }
+ },
recalcMenuPos() {
if (!this.menu || !this.$refs.inputWrapper) return
var boundingBox = this.$refs.inputWrapper.getBoundingClientRect()
@@ -208,7 +267,10 @@ export default {
e.stopPropagation()
e.preventDefault()
}
- if (this.$refs.input) this.$refs.input.focus()
+ if (this.$refs.input) {
+ this.$refs.input.style.width = '24px'
+ this.$refs.input.focus()
+ }
var newSelected = null
if (this.selected.includes(itemValue)) {
@@ -219,6 +281,7 @@ export default {
}
this.textInput = null
this.currentSearch = null
+ this.selectedMenuItemIndex = null
this.$emit('input', newSelected)
this.$nextTick(() => {
this.recalcMenuPos()
@@ -245,6 +308,7 @@ export default {
this.$emit('newItem', item)
this.textInput = null
this.currentSearch = null
+ this.selectedMenuItemIndex = null
this.$nextTick(() => {
this.blur()
})
@@ -261,6 +325,7 @@ export default {
} else {
this.insertNewItem(this.textInput)
}
+ if (this.$refs.input) this.$refs.input.style.width = '24px'
},
scroll() {
this.recalcMenuPos()
diff --git a/client/components/ui/MultiSelectQueryInput.vue b/client/components/ui/MultiSelectQueryInput.vue
index c86d3228..6e9c0f10 100644
--- a/client/components/ui/MultiSelectQueryInput.vue
+++ b/client/components/ui/MultiSelectQueryInput.vue
@@ -14,13 +14,13 @@
add
-
+
- -
+
-
{{ item.name }}
@@ -63,7 +63,8 @@ export default {
typingTimeout: null,
isFocused: false,
menu: null,
- items: []
+ items: [],
+ selectedMenuItemIndex: null
}
},
watch: {
@@ -122,7 +123,35 @@ export default {
this.items = results || []
},
- keydownInput() {
+ keydownInput(event) {
+ let items = this.itemsToShow
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
+ event.preventDefault()
+ if (!items.length) return
+ if (event.key === 'ArrowDown') {
+ if (this.selectedMenuItemIndex === null) {
+ this.selectedMenuItemIndex = 0
+ } else {
+ this.selectedMenuItemIndex = Math.min(this.selectedMenuItemIndex + 1, items.length - 1)
+ }
+ } else if (event.key === 'ArrowUp') {
+ if (this.selectedMenuItemIndex === null) {
+ this.selectedMenuItemIndex = items.length - 1
+ } else {
+ this.selectedMenuItemIndex = Math.max(this.selectedMenuItemIndex - 1, 0)
+ }
+ }
+ this.recalcScroll()
+ return
+ } else if (event.key === 'Enter') {
+ if (this.selectedMenuItemIndex !== null) {
+ this.clickedOption(event, items[this.selectedMenuItemIndex])
+ } else {
+ this.submitForm()
+ }
+ return
+ }
+ this.selectedMenuItemIndex = null
clearTimeout(this.typingTimeout)
this.typingTimeout = setTimeout(() => {
this.search()
@@ -137,6 +166,24 @@ export default {
this.recalcMenuPos()
}, 50)
},
+ recalcScroll() {
+ if (!this.menu) return
+ var menuItems = this.menu.querySelectorAll('li')
+ if (!menuItems.length) return
+ var selectedItem = menuItems[this.selectedMenuItemIndex]
+ if (!selectedItem) return
+ var menuHeight = this.menu.offsetHeight
+ var itemHeight = selectedItem.offsetHeight
+ var itemTop = selectedItem.offsetTop
+ var itemBottom = itemTop + itemHeight
+ if (itemBottom > this.menu.scrollTop + menuHeight) {
+ let menuPaddingBottom = parseFloat(window.getComputedStyle(this.menu).paddingBottom)
+ this.menu.scrollTop = itemBottom - menuHeight + menuPaddingBottom
+ } else if (itemTop < this.menu.scrollTop) {
+ let menuPaddingTop = parseFloat(window.getComputedStyle(this.menu).paddingTop)
+ this.menu.scrollTop = itemTop - menuPaddingTop
+ }
+ },
recalcMenuPos() {
if (!this.menu || !this.$refs.inputWrapper) return
var boundingBox = this.$refs.inputWrapper.getBoundingClientRect()
@@ -228,7 +275,10 @@ export default {
e.stopPropagation()
e.preventDefault()
}
- if (this.$refs.input) this.$refs.input.focus()
+ if (this.$refs.input) {
+ this.$refs.input.style.width = '24px'
+ this.$refs.input.focus()
+ }
let newSelected = null
if (this.getIsSelected(item.id)) {
@@ -244,6 +294,7 @@ export default {
}
this.textInput = null
this.currentSearch = null
+ this.selectedMenuItemIndex = null
this.$emit('input', newSelected)
this.$nextTick(() => {
@@ -271,6 +322,7 @@ export default {
this.$emit('newItem', item)
this.textInput = null
this.currentSearch = null
+ this.selectedMenuItemIndex = null
this.$nextTick(() => {
this.blur()
})
@@ -291,6 +343,7 @@ export default {
name: this.textInput
})
}
+ if (this.$refs.input) this.$refs.input.style.width = '24px'
},
scroll() {
this.recalcMenuPos()
diff --git a/client/components/ui/TextInput.vue b/client/components/ui/TextInput.vue
index 5f871635..e06740ea 100644
--- a/client/components/ui/TextInput.vue
+++ b/client/components/ui/TextInput.vue
@@ -1,6 +1,6 @@
-
+
close
@@ -33,6 +33,7 @@ export default {
textCenter: Boolean,
clearable: Boolean,
inputId: String,
+ inputName: String,
step: [String, Number],
min: [String, Number]
},
@@ -117,4 +118,4 @@ input:read-only {
input::-webkit-calendar-picker-indicator {
filter: invert(1);
}
-
\ No newline at end of file
+
diff --git a/client/pages/batch/index.vue b/client/pages/batch/index.vue
index 15675fb8..e1687f0f 100644
--- a/client/pages/batch/index.vue
+++ b/client/pages/batch/index.vue
@@ -20,44 +20,44 @@
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
+
{{ $strings.ButtonApply }}
@@ -139,7 +153,8 @@ export default {
narrators: [],
publisher: null,
language: null,
- explicit: false
+ explicit: false,
+ abridged: false
},
selectedBatchUsage: {
subtitle: false,
@@ -151,7 +166,8 @@ export default {
narrators: false,
publisher: false,
language: false,
- explicit: false
+ explicit: false,
+ abridged: false
},
appendableKeys: ['authors', 'genres', 'tags', 'narrators', 'series'],
openMapOptions: false
diff --git a/client/pages/library/_library/authors/index.vue b/client/pages/library/_library/authors/index.vue
index 1f8e385b..3906f671 100644
--- a/client/pages/library/_library/authors/index.vue
+++ b/client/pages/library/_library/authors/index.vue
@@ -3,7 +3,7 @@
-
+
@@ -44,6 +44,22 @@ export default {
},
selectedAuthor() {
return this.$store.state.globals.selectedAuthor
+ },
+ authorSortBy() {
+ return this.$store.getters['user/getUserSetting']('authorSortBy') || 'name'
+ },
+ authorSortDesc() {
+ return !!this.$store.getters['user/getUserSetting']('authorSortDesc')
+ },
+ authorsSorted() {
+ const sortProp = this.authorSortBy
+ const bDesc = this.authorSortDesc ? -1 : 1
+ return this.authors.sort((a, b) => {
+ if (typeof a[sortProp] === 'number' && typeof b[sortProp] === 'number') {
+ return a[sortProp] > b[sortProp] ? bDesc : -bDesc
+ }
+ return a[sortProp].localeCompare(b[sortProp], undefined, { sensitivity: 'base' }) * bDesc
+ })
}
},
methods: {
diff --git a/client/pages/library/_library/podcast/latest.vue b/client/pages/library/_library/podcast/latest.vue
index 42f107c8..3fc47dfd 100644
--- a/client/pages/library/_library/podcast/latest.vue
+++ b/client/pages/library/_library/podcast/latest.vue
@@ -8,11 +8,11 @@
{{ $strings.MessageNoEpisodes }}
-
+
-
+
diff --git a/client/pages/login.vue b/client/pages/login.vue
index efdbba4d..897d5604 100644
--- a/client/pages/login.vue
+++ b/client/pages/login.vue
@@ -35,10 +35,10 @@