Add:Ebook files table and supplementary ereader

This commit is contained in:
advplyr 2023-06-11 13:36:19 -05:00
parent 543ac209e4
commit d8bc26f5f8
16 changed files with 1283 additions and 638 deletions

View file

@ -143,4 +143,27 @@ Bookshelf Label
display: -webkit-box; display: -webkit-box;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
-webkit-line-clamp: 4; -webkit-line-clamp: 4;
}
.tracksTable {
border-collapse: collapse;
width: 100%;
border: 1px solid #474747;
}
.tracksTable tr:nth-child(even) {
background-color: #2e2e2e;
}
.tracksTable tr {
background-color: #373838;
}
.tracksTable td {
padding: 8px 8px;
}
.tracksTable th {
padding: 4px 8px;
font-size: 0.75rem;
} }

View file

@ -7,7 +7,7 @@
</template> </template>
<div class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center" @click="show = false"> <div class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center" @click="show = false">
<div ref="container" class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-white border-opacity-20 p-2" style="max-height: 75%" @click.stop> <div ref="container" class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-white/20 p-2" style="max-height: 75%" @click.stop>
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label"> <ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
<template v-for="item in itemsToShow"> <template v-for="item in itemsToShow">
<slot :name="item.value" :item="item" :selected="item.value === selected"> <slot :name="item.value" :item="item" :selected="item.value === selected">

View file

@ -264,16 +264,25 @@ export default {
flow: 'paginated' flow: 'paginated'
}) })
// load saved progress
reader.rendition.display(this.savedEbookLocation || reader.book.locations.start)
// load style // load style
reader.rendition.themes.default({ '*': { color: '#fff!important', 'background-color': 'rgb(35 35 35)!important' }, a: { color: '#fff!important' } }) reader.rendition.themes.default({ '*': { color: '#fff!important', 'background-color': 'rgb(35 35 35)!important' }, a: { color: '#fff!important' } })
reader.book.ready.then(() => { reader.book.ready.then(() => {
// load saved progress
// when not checking spine first uncaught exception is thrown
if (this.savedEbookLocation && reader.book.spine.get(this.savedEbookLocation)) {
reader.rendition.display(this.savedEbookLocation)
} else {
reader.rendition.display(reader.book.locations.start)
}
// set up event listeners // set up event listeners
reader.rendition.on('relocated', reader.relocated) reader.rendition.on('relocated', reader.relocated)
reader.rendition.on('displayError', (err) => {
console.log('Display error', err)
})
// load ebook cfi locations // load ebook cfi locations
const savedLocations = this.loadLocations() const savedLocations = this.loadLocations()
if (savedLocations) { if (savedLocations) {

View file

@ -70,28 +70,3 @@ export default {
mounted() {} mounted() {}
} }
</script> </script>
<style scoped>
.tracksTable {
border-collapse: collapse;
width: 100%;
border: 1px solid #474747;
}
.tracksTable tr:nth-child(even) {
background-color: #2e2e2e;
}
.tracksTable tr {
background-color: #373838;
}
.tracksTable td {
padding: 8px 8px;
}
.tracksTable th {
padding: 4px 8px;
font-size: 0.75rem;
}
</style>

View file

@ -58,28 +58,3 @@ export default {
mounted() {} mounted() {}
} }
</script> </script>
<style scoped>
.tracksTable {
border-collapse: collapse;
width: 100%;
border: 1px solid #474747;
}
.tracksTable tr:nth-child(even) {
background-color: #2e2e2e;
}
.tracksTable tr {
background-color: #373838;
}
.tracksTable td {
padding: 8px 8px;
}
.tracksTable th {
padding: 4px 8px;
font-size: 0.75rem;
}
</style>

View file

@ -0,0 +1,102 @@
<template>
<div class="w-full my-2">
<div class="w-full bg-primary px-4 py-2 flex items-center" :class="showFiles ? 'rounded-t-md' : 'rounded-md'" @click.stop="clickBar">
<p class="pr-2">Ebook Files</p>
<div class="h-6 w-6 rounded-full bg-white bg-opacity-10 flex items-center justify-center">
<span class="text-xs font-mono">{{ ebookFiles.length }}</span>
</div>
<div class="flex-grow" />
<div class="h-10 w-10 rounded-full flex justify-center items-center duration-500" :class="showFiles ? 'transform rotate-180' : ''">
<span class="material-icons text-3xl">expand_more</span>
</div>
</div>
<transition name="slide">
<div class="w-full" v-show="showFiles">
<table class="text-sm tracksTable">
<tr>
<th class="text-left px-4">Filename</th>
<th class="text-left px-4 w-16">Read</th>
<th v-if="userCanUpdate && !libraryIsAudiobooksOnly" class="text-center w-16"></th>
</tr>
<template v-for="file in ebookFiles">
<tables-ebook-files-table-row :key="file.path" :libraryItemId="libraryItemId" :file="file" @read="readEbook" @more="showMore" />
</template>
</table>
</div>
</transition>
<modals-dialog v-model="showMoreMenu" :items="moreMenuItems" @action="moreMenuAction" />
</div>
</template>
<script>
export default {
props: {
libraryItem: {
type: Object,
default: () => {}
}
},
data() {
return {
processing: false,
showFiles: false,
showMoreMenu: false,
moreMenuItems: [],
selectedFile: null
}
},
computed: {
libraryItemId() {
return this.libraryItem.id
},
userToken() {
return this.$store.getters['user/getToken']
},
ebookFiles() {
return (this.libraryItem.libraryFiles || []).filter((lf) => lf.fileType === 'ebook')
},
userCanUpdate() {
return this.$store.getters['user/getUserCanUpdate']
},
libraryIsAudiobooksOnly() {
return this.$store.getters['libraries/getLibraryIsAudiobooksOnly']
}
},
methods: {
moreMenuAction(action) {
this.showMoreMenu = false
if (action === 'updateStatus') {
this.updateEbookStatus()
}
},
showMore({ file, items }) {
this.showMoreMenu = true
this.selectedFile = file
this.moreMenuItems = items
},
readEbook(fileIno) {
this.$store.commit('showReader', { libraryItem: this.libraryItem, keepProgress: false, fileId: fileIno })
},
clickBar() {
this.showFiles = !this.showFiles
},
updateEbookStatus() {
this.processing = true
this.$axios
.$patch(`/api/items/${this.libraryItemId}/ebook/${this.selectedFile.ino}/status`)
.then(() => {
this.$toast.success('Ebook updated')
})
.catch((error) => {
console.error('Failed to update ebook', error)
this.$toast.error('Failed to update ebook')
})
.finally(() => {
this.processing = false
})
}
},
mounted() {}
}
</script>

View file

@ -0,0 +1,63 @@
<template>
<tr>
<td class="px-4">{{ file.metadata.filename }} <span v-if="isPrimary" class="material-icons-outlined text-success align-text-bottom text-base">check_circle</span></td>
<td class="text-xs w-16">
<ui-icon-btn icon="auto_stories" outlined borderless icon-font-size="1.125rem" :size="8" @click="readEbook" />
</td>
<td v-if="contextMenuItems.length" class="text-center">
<ui-icon-btn icon="more_vert" borderless @click="clickMore" />
</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']
},
userCanUpdate() {
return this.$store.getters['user/getUserCanUpdate']
},
isPrimary() {
return !this.file.isSupplementary
},
libraryIsAudiobooksOnly() {
return this.$store.getters['libraries/getLibraryIsAudiobooksOnly']
},
contextMenuItems() {
const items = []
if (this.userCanUpdate && !this.libraryIsAudiobooksOnly) {
items.push({
text: this.isPrimary ? 'Set as supplementary' : 'Set as primary',
value: 'updateStatus'
})
}
return items
}
},
methods: {
clickMore() {
this.$emit('more', {
file: this.file,
items: this.contextMenuItems
})
},
readEbook() {
this.$emit('read', this.file.ino)
}
},
mounted() {}
}
</script>

92
components/ui/Tooltip.vue Normal file
View file

@ -0,0 +1,92 @@
<template>
<div ref="box" class="inline-block" @click.stop="click">
<slot />
</div>
</template>
<script>
export default {
props: {
text: {
type: [String, Number],
required: true
},
direction: {
type: String,
default: 'right'
},
disabled: Boolean
},
data() {
return {
tooltip: null,
tooltipTextEl: null,
tooltipId: null,
isShowing: false,
hideTimeout: null
}
},
watch: {
text() {
this.updateText()
},
disabled(newVal) {
if (newVal && this.isShowing) {
this.hideTooltip()
}
}
},
methods: {
updateText() {
if (this.tooltipTextEl) {
this.tooltipTextEl.innerHTML = this.text
}
},
createTooltip() {
if (!this.$refs.box) return
const tooltip = document.createElement('div')
this.tooltipId = String(Math.floor(Math.random() * 10000))
tooltip.id = this.tooltipId
tooltip.className = 'fixed inset-0 w-screen h-screen bg-black/25 text-xs flex items-center justify-center p-2'
tooltip.style.zIndex = 100
tooltip.style.backgroundColor = 'rgba(0,0,0,0.85)'
tooltip.addEventListener('click', this.hideTooltip)
const innerDiv = document.createElement('div')
innerDiv.className = 'w-full p-2 border border-white/20 pointer-events-none text-white bg-primary'
innerDiv.innerHTML = this.text
tooltip.appendChild(innerDiv)
this.tooltipTextEl = innerDiv
this.tooltip = tooltip
},
showTooltip() {
if (this.disabled) return
if (!this.tooltip) {
this.createTooltip()
if (!this.tooltip) return
}
if (!this.$refs.box) return // Ensure element is not destroyed
try {
document.body.appendChild(this.tooltip)
} catch (error) {
console.error(error)
}
this.isShowing = true
},
hideTooltip() {
if (!this.tooltip) return
this.tooltip.remove()
this.isShowing = false
},
click() {
if (!this.isShowing) this.showTooltip()
}
},
beforeDestroy() {
this.hideTooltip()
}
}
</script>

1513
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -39,7 +39,8 @@
"@babel/core": "7.13.15", "@babel/core": "7.13.15",
"@babel/preset-env": "7.13.15", "@babel/preset-env": "7.13.15",
"@capacitor/cli": "^4.0.0", "@capacitor/cli": "^4.0.0",
"@nuxtjs/tailwindcss": "^4.2.0", "@nuxtjs/tailwindcss": "^4.2.1",
"postcss": "^8.3.5" "postcss": "^8.3.5",
"tailwindcss": "^3.3.2"
} }
} }

View file

@ -123,6 +123,8 @@
<tables-tracks-table v-if="numTracks" :tracks="tracks" :library-item-id="libraryItemId" /> <tables-tracks-table v-if="numTracks" :tracks="tracks" :library-item-id="libraryItemId" />
<tables-ebook-files-table v-if="ebookFiles.length" :library-item="libraryItem" />
<!-- modals --> <!-- modals -->
<modals-select-local-folder-modal v-model="showSelectLocalFolder" :media-type="mediaType" @select="selectedLocalFolder" /> <modals-select-local-folder-modal v-model="showSelectLocalFolder" :media-type="mediaType" @select="selectedLocalFolder" />
@ -359,6 +361,12 @@ export default {
if (this.isPodcast || this.hasLocal) return false if (this.isPodcast || this.hasLocal) return false
return this.user && this.userCanDownload && (this.showPlay || (this.showRead && !this.isIos)) return this.user && this.userCanDownload && (this.showPlay || (this.showRead && !this.isIos))
}, },
libraryFiles() {
return this.libraryItem.libraryFiles || []
},
ebookFiles() {
return this.libraryFiles.filter((lf) => lf.fileType === 'ebook')
},
ebookFile() { ebookFile() {
return this.media.ebookFile return this.media.ebookFile
}, },

View file

@ -15,20 +15,20 @@ export const getters = {
return state.libraries.find(lib => lib.id === state.currentLibraryId) return state.libraries.find(lib => lib.id === state.currentLibraryId)
}, },
getCurrentLibraryName: (state, getters) => { getCurrentLibraryName: (state, getters) => {
const currLib = getters.getCurrentLibrary return getters.getCurrentLibrary?.name || null
return currLib ? currLib.name : null
}, },
getCurrentLibraryMediaType: (state, getters) => { getCurrentLibraryMediaType: (state, getters) => {
const currLib = getters.getCurrentLibrary return getters.getCurrentLibrary?.mediaType || null
return currLib ? currLib.mediaType : null
}, },
getCurrentLibrarySettings: (state, getters) => { getCurrentLibrarySettings: (state, getters) => {
if (!getters.getCurrentLibrary) return null return getters.getCurrentLibrary?.settings || null
return getters.getCurrentLibrary.settings
}, },
getBookCoverAspectRatio: (state, getters) => { getBookCoverAspectRatio: (state, getters) => {
if (!getters.getCurrentLibrarySettings || isNaN(getters.getCurrentLibrarySettings.coverAspectRatio)) return 1 if (isNaN(getters.getCurrentLibrarySettings?.coverAspectRatio)) return 1
return getters.getCurrentLibrarySettings.coverAspectRatio === BookCoverAspectRatio.STANDARD ? 1.6 : 1 return getters.getCurrentLibrarySettings.coverAspectRatio === BookCoverAspectRatio.STANDARD ? 1.6 : 1
},
getLibraryIsAudiobooksOnly: (state, getters) => {
return !!getters.getCurrentLibrarySettings?.audiobooksOnly
} }
} }

View file

@ -15,22 +15,22 @@ export const getters = {
getIsRoot: (state) => state.user && state.user.type === 'root', getIsRoot: (state) => state.user && state.user.type === 'root',
getIsAdminOrUp: (state) => state.user && (state.user.type === 'admin' || state.user.type === 'root'), getIsAdminOrUp: (state) => state.user && (state.user.type === 'admin' || state.user.type === 'root'),
getToken: (state) => { getToken: (state) => {
return state.user ? state.user.token : null return state.user?.token || null
}, },
getServerConnectionConfigId: (state) => { getServerConnectionConfigId: (state) => {
return state.serverConnectionConfig ? state.serverConnectionConfig.id : null return state.serverConnectionConfig?.id || null
}, },
getServerAddress: (state) => { getServerAddress: (state) => {
return state.serverConnectionConfig ? state.serverConnectionConfig.address : null return state.serverConnectionConfig?.address || null
}, },
getServerConfigName: (state) => { getServerConfigName: (state) => {
return state.serverConnectionConfig ? state.serverConnectionConfig.name : null return state.serverConnectionConfig?.name || null
}, },
getCustomHeaders: (state) => { getCustomHeaders: (state) => {
return state.serverConnectionConfig ? state.serverConnectionConfig.customHeaders : null return state.serverConnectionConfig?.customHeaders || null
}, },
getUserMediaProgress: (state) => (libraryItemId, episodeId = null) => { getUserMediaProgress: (state) => (libraryItemId, episodeId = null) => {
if (!state.user || !state.user.mediaProgress) return null if (!state.user?.mediaProgress) return null
return state.user.mediaProgress.find(li => { return state.user.mediaProgress.find(li => {
if (episodeId && li.episodeId !== episodeId) return false if (episodeId && li.episodeId !== episodeId) return false
return li.libraryItemId == libraryItemId return li.libraryItemId == libraryItemId
@ -41,10 +41,16 @@ export const getters = {
return state.user.bookmarks.filter(bm => bm.libraryItemId === libraryItemId) return state.user.bookmarks.filter(bm => bm.libraryItemId === libraryItemId)
}, },
getUserSetting: (state) => (key) => { getUserSetting: (state) => (key) => {
return state.settings ? state.settings[key] || null : null return state.settings?.[key] || null
},
getUserCanUpdate: (state) => {
return !!state.user?.permissions?.update
},
getUserCanDelete: (state) => {
return !!state.user?.permissions?.delete
}, },
getUserCanDownload: (state) => { getUserCanDownload: (state) => {
return state.user && state.user.permissions ? !!state.user.permissions.download : false return !!state.user?.permissions?.download
} }
} }

View file

@ -2,13 +2,11 @@ const defaultTheme = require('tailwindcss/defaultTheme')
module.exports = { module.exports = {
purge: { purge: {
options: { safelist: [
safelist: [ 'bg-success',
'bg-success', 'bg-info',
'bg-info', 'text-info'
'text-info' ]
]
}
}, },
darkMode: false, darkMode: false,
theme: { theme: {