mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-06-02 00:40:39 +02:00
Compare commits
190 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 14ee17de47 | |||
| 034b8956a2 | |||
| 1a3f0e332e | |||
| fc36e86db7 | |||
| 60b4bc1a7e | |||
| 9fdc8df8bc | |||
| 212b97fa20 | |||
| 704fbaced8 | |||
| 575a162f8b | |||
| d2e0844493 | |||
| f2baf3fafd | |||
| 916fd039ca | |||
| e248b6d8d8 | |||
| 936de68622 | |||
| a99257e758 | |||
| c89d77dd06 | |||
| 3138865d69 | |||
| 4d29ebd647 | |||
| fd58df4729 | |||
| 5078818295 | |||
| 7181df0479 | |||
| 6c618d7760 | |||
| 17b8cf19b7 | |||
| e018f8341e | |||
| 59b5f8cbbe | |||
| d6108a0722 | |||
| 1af7e59d88 | |||
| 7b425e9a9d | |||
| 596a03900b | |||
| b283644d95 | |||
| 808690c137 | |||
| 136c347586 | |||
| e81238038e | |||
| fcf6964d7d | |||
| bd75ad4576 | |||
| f970d8e539 | |||
| c49010b4e1 | |||
| 146093d81e | |||
| 11ccbf1913 | |||
| a4a334a18a | |||
| 387a37e4da | |||
| ebad304aa9 | |||
| 8b557a0cb9 | |||
| 40b808e73d | |||
| a8b57a1ce9 | |||
| 35315843f2 | |||
| 27b9d3b94f | |||
| 0010ac5a40 | |||
| 884808f34e | |||
| f75ed07497 | |||
| b707d6f3c9 | |||
| a2d4a4a906 | |||
| 434d743d99 | |||
| 30f16b05fe | |||
| 92a88f4416 | |||
| 5c9c122af2 | |||
| 620d5ce578 | |||
| 363e1cee4b | |||
| 93f576772a | |||
| d4612bae92 | |||
| e01af27008 | |||
| 657fe0a650 | |||
| 9a6ec5548e | |||
| 0807509ea7 | |||
| d9d1c4e360 | |||
| 2135e5b066 | |||
| b69eb10ae0 | |||
| e1512b6f54 | |||
| 1b8e8215d6 | |||
| 9b44e36e7b | |||
| db1ca08c2e | |||
| 557d3243c3 | |||
| 785942b94f | |||
| 3df7caa838 | |||
| aef2c52630 | |||
| dccad3055b | |||
| c629923a80 | |||
| b4f1fd5b25 | |||
| 267897ce74 | |||
| 022bf9d0ef | |||
| 61c759e0c4 | |||
| cfb3ce0c60 | |||
| 72396c5a98 | |||
| 12f231b886 | |||
| 6aeed24296 | |||
| d8b6e09bc0 | |||
| d95975cade | |||
| c4208a4690 | |||
| 7c7a6df6e4 | |||
| 791c058ef8 | |||
| c847aea0a4 | |||
| e56164aa5a | |||
| cfb5e909a9 | |||
| 071444a9e7 | |||
| 34ac972130 | |||
| 97b5cf04f5 | |||
| 0d50d730d9 | |||
| 3a7fd0bcc9 | |||
| f0edea5d52 | |||
| 9c6b07df99 | |||
| caacf461ab | |||
| 5bdbc75522 | |||
| 0d3e6b1d0a | |||
| a122e25cba | |||
| d7b287bfed | |||
| ba4f585318 | |||
| 3f859723a6 | |||
| c820d0e62b | |||
| 7a47032a96 | |||
| 2db4dd6a40 | |||
| f58e2b6dce | |||
| 859a53e79a | |||
| ad0edc6329 | |||
| 002fb7a35e | |||
| cc62a20a5d | |||
| ec7e965dfa | |||
| 9c3f5406a9 | |||
| f4ec6948d2 | |||
| 9a51c3be0f | |||
| b1ee54522a | |||
| c14d13440f | |||
| 8c84640484 | |||
| 0d8917ced6 | |||
| a006eb489d | |||
| f2941e04d3 | |||
| 2728546660 | |||
| eeb7c80518 | |||
| c8c40360ad | |||
| 79ab656217 | |||
| 5c250da388 | |||
| 505e0eb3a2 | |||
| 388444e51f | |||
| 08d7a9aa14 | |||
| f650ae7f18 | |||
| 6d138ae905 | |||
| 956678c08c | |||
| 911c854365 | |||
| 3c5dc17e3c | |||
| e709cc4cb1 | |||
| da7825e3e3 | |||
| 4039dc7968 | |||
| e345c4cc9e | |||
| a08cfa436e | |||
| 7207efb4da | |||
| 481611ff33 | |||
| b67cd37a38 | |||
| 6e8547f0c8 | |||
| 96930d7ecc | |||
| 23f2c8a251 | |||
| c5372d1405 | |||
| dcfbed5f30 | |||
| 895ab8d18a | |||
| 8b5d05739f | |||
| a8f6202302 | |||
| 5d40fdf277 | |||
| f35c96e118 | |||
| 8f8d6f81ab | |||
| e195eec1c5 | |||
| 33846e46fa | |||
| 2ad03bcb9a | |||
| 371cd3b2e5 | |||
| b9307143bd | |||
| 36e44e902a | |||
| 4529fc0124 | |||
| ba07761de3 | |||
| 3b7ce69327 | |||
| cf927f61a0 | |||
| 61c32d99e7 | |||
| 19e39f6321 | |||
| f9e6655359 | |||
| debf0f495d | |||
| 3383ec2046 | |||
| b957e1a36b | |||
| c93f17051a | |||
| 5983f0262f | |||
| 96a8e74d38 | |||
| 6f3d488c3d | |||
| 74dcc4f9e4 | |||
| 8c4d3b93c8 | |||
| c411cf04cc | |||
| d1b25da408 | |||
| 08f765fa51 | |||
| 337cf90c4b | |||
| 17b930e13d | |||
| 639b600570 | |||
| 7a751b8f91 | |||
| 68621e0c07 | |||
| d2512d324a | |||
| 5f63d97e59 | |||
| bf2fe3faea |
@@ -0,0 +1,44 @@
|
|||||||
|
name: Integration Test
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches-ignore:
|
||||||
|
- 'dependabot/**' # Don't run dependabot branches, as they are already covered by pull requests
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: build and test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: setup nade
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 16
|
||||||
|
|
||||||
|
- name: install pkg
|
||||||
|
run: npm install -g pkg
|
||||||
|
|
||||||
|
- name: get client dependencies
|
||||||
|
working-directory: client
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: build client
|
||||||
|
working-directory: client
|
||||||
|
run: npm run generate
|
||||||
|
|
||||||
|
- name: get server dependencies
|
||||||
|
run: npm ci --only=production
|
||||||
|
|
||||||
|
- name: build binary
|
||||||
|
run: pkg -t node18-linux-x64 -o audiobookshelf .
|
||||||
|
|
||||||
|
- name: run audiobookshelf
|
||||||
|
run: |
|
||||||
|
./audiobookshelf &
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
- name: test if server is available
|
||||||
|
run: curl -sf http://127.0.0.1:3333 | grep Audiobookshelf
|
||||||
+1
-1
@@ -29,4 +29,4 @@ HEALTHCHECK \
|
|||||||
--timeout=3s \
|
--timeout=3s \
|
||||||
--start-period=10s \
|
--start-period=10s \
|
||||||
CMD curl -f http://127.0.0.1/healthcheck || exit 1
|
CMD curl -f http://127.0.0.1/healthcheck || exit 1
|
||||||
CMD ["npm", "start"]
|
CMD ["node", "index.js"]
|
||||||
|
|||||||
@@ -48,14 +48,6 @@
|
|||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Gentium Book Basic';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
font-display: swap;
|
|
||||||
src: url(~static/fonts/GentiumBookBasic.woff2) format('woff2');
|
|
||||||
}
|
|
||||||
|
|
||||||
/* cyrillic-ext */
|
/* cyrillic-ext */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Source Sans Pro';
|
font-family: 'Source Sans Pro';
|
||||||
|
|||||||
@@ -3,11 +3,11 @@
|
|||||||
<div id="appbar" class="absolute top-0 bottom-0 left-0 w-full h-full px-2 md:px-6 py-1 z-60">
|
<div id="appbar" class="absolute top-0 bottom-0 left-0 w-full h-full px-2 md:px-6 py-1 z-60">
|
||||||
<div class="flex h-full items-center">
|
<div class="flex h-full items-center">
|
||||||
<nuxt-link to="/">
|
<nuxt-link to="/">
|
||||||
<img src="~static/icon.svg" :alt="$strings.ButtonHome" class="w-8 min-w-8 h-8 mr-2 sm:w-12 sm:min-w-12 sm:h-12 sm:mr-4" />
|
<img src="~static/icon.svg" :alt="$strings.ButtonHome" class="w-8 min-w-8 h-8 mr-2 sm:w-10 sm:min-w-10 sm:h-10 sm:mr-4" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link to="/">
|
<nuxt-link to="/">
|
||||||
<h1 class="text-2xl font-book mr-6 hidden lg:block hover:underline">audiobookshelf <span v-if="showExperimentalFeatures" class="material-icons text-lg text-warning pr-1">logo_dev</span></h1>
|
<h1 class="text-xl mr-6 hidden lg:block hover:underline">audiobookshelf <span v-if="showExperimentalFeatures" class="material-icons text-lg text-warning pr-1">logo_dev</span></h1>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<ui-libraries-dropdown class="mr-2" />
|
<ui-libraries-dropdown class="mr-2" />
|
||||||
@@ -58,9 +58,6 @@
|
|||||||
<span class="material-icons text-2xl -ml-2 pr-1 text-white">play_arrow</span>
|
<span class="material-icons text-2xl -ml-2 pr-1 text-white">play_arrow</span>
|
||||||
{{ $strings.ButtonPlay }}
|
{{ $strings.ButtonPlay }}
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
<ui-tooltip v-if="userIsAdminOrUp && isBookLibrary" :text="$strings.ButtonQuickMatch" direction="bottom">
|
|
||||||
<ui-icon-btn :disabled="processingBatch" icon="auto_awesome" @click="batchAutoMatchClick" class="mx-1.5" />
|
|
||||||
</ui-tooltip>
|
|
||||||
<ui-tooltip v-if="isBookLibrary" :text="selectedIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="bottom">
|
<ui-tooltip v-if="isBookLibrary" :text="selectedIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="bottom">
|
||||||
<ui-read-icon-btn :disabled="processingBatch" :is-read="selectedIsFinished" @click="toggleBatchRead" class="mx-1.5" />
|
<ui-read-icon-btn :disabled="processingBatch" :is-read="selectedIsFinished" @click="toggleBatchRead" class="mx-1.5" />
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
@@ -75,8 +72,11 @@
|
|||||||
<ui-tooltip v-if="userCanDelete" :text="$strings.ButtonRemove" direction="bottom">
|
<ui-tooltip v-if="userCanDelete" :text="$strings.ButtonRemove" direction="bottom">
|
||||||
<ui-icon-btn :disabled="processingBatch" icon="delete" bg-color="error" class="mx-1.5" @click="batchDeleteClick" />
|
<ui-icon-btn :disabled="processingBatch" icon="delete" bg-color="error" class="mx-1.5" @click="batchDeleteClick" />
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
<ui-tooltip :text="$strings.LabelDeselectAll" direction="bottom">
|
|
||||||
<span class="material-icons text-4xl px-4 hover:text-gray-100 cursor-pointer" :class="processingBatch ? 'text-gray-400' : ''" @click="cancelSelectionMode">close</span>
|
<ui-context-menu-dropdown v-if="contextMenuItems.length && !processingBatch" :items="contextMenuItems" class="ml-1" @action="contextMenuAction" />
|
||||||
|
|
||||||
|
<ui-tooltip :text="$strings.LabelDeselectAll" direction="bottom" class="flex items-center">
|
||||||
|
<span class="material-icons text-3xl px-4 hover:text-gray-100 cursor-pointer" :class="processingBatch ? 'text-gray-400' : ''" @click="cancelSelectionMode">close</span>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -160,9 +160,59 @@ export default {
|
|||||||
},
|
},
|
||||||
isHttps() {
|
isHttps() {
|
||||||
return location.protocol === 'https:' || process.env.NODE_ENV === 'development'
|
return location.protocol === 'https:' || process.env.NODE_ENV === 'development'
|
||||||
|
},
|
||||||
|
contextMenuItems() {
|
||||||
|
if (!this.userIsAdminOrUp) return []
|
||||||
|
|
||||||
|
const options = [
|
||||||
|
{
|
||||||
|
text: this.$strings.ButtonQuickMatch,
|
||||||
|
action: 'quick-match'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
if (!this.isPodcastLibrary && this.selectedMediaItemsArePlayable) {
|
||||||
|
options.push({
|
||||||
|
text: 'Quick Embed Metadata',
|
||||||
|
action: 'quick-embed'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return options
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
requestBatchQuickEmbed() {
|
||||||
|
const payload = {
|
||||||
|
message: 'Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files. <br><br>Would you like to continue?',
|
||||||
|
callback: (confirmed) => {
|
||||||
|
if (confirmed) {
|
||||||
|
this.$axios
|
||||||
|
.$post(`/api/tools/batch/embed-metadata`, {
|
||||||
|
libraryItemIds: this.selectedMediaItems.map((i) => i.id)
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
console.log('Audio metadata embed started')
|
||||||
|
this.cancelSelectionMode()
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Audio metadata embed failed', error)
|
||||||
|
const errorMsg = error.response.data || 'Failed to embed metadata'
|
||||||
|
this.$toast.error(errorMsg)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'yesNo'
|
||||||
|
}
|
||||||
|
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||||
|
},
|
||||||
|
contextMenuAction(action) {
|
||||||
|
if (action === 'quick-embed') {
|
||||||
|
this.requestBatchQuickEmbed()
|
||||||
|
} else if (action === 'quick-match') {
|
||||||
|
this.batchAutoMatchClick()
|
||||||
|
}
|
||||||
|
},
|
||||||
async playSelectedItems() {
|
async playSelectedItems() {
|
||||||
this.$store.commit('setProcessingBatch', true)
|
this.$store.commit('setProcessingBatch', true)
|
||||||
|
|
||||||
|
|||||||
@@ -4,14 +4,14 @@
|
|||||||
<widgets-cover-size-widget class="fixed bottom-4 right-4 z-50" />
|
<widgets-cover-size-widget class="fixed bottom-4 right-4 z-50" />
|
||||||
|
|
||||||
<div v-if="loaded && !shelves.length && !search" class="w-full flex flex-col items-center justify-center py-12">
|
<div v-if="loaded && !shelves.length && !search" class="w-full flex flex-col items-center justify-center py-12">
|
||||||
<p class="text-center text-2xl font-book mb-4 py-4">{{ libraryName }} Library is empty!</p>
|
<p class="text-center text-2xl mb-4 py-4">{{ libraryName }} Library is empty!</p>
|
||||||
<div v-if="userIsAdminOrUp" class="flex">
|
<div v-if="userIsAdminOrUp" class="flex">
|
||||||
<ui-btn to="/config" color="primary" class="w-52 mr-2">Configure Scanner</ui-btn>
|
<ui-btn to="/config" color="primary" class="w-52 mr-2">Configure Scanner</ui-btn>
|
||||||
<ui-btn color="success" class="w-52" @click="scan">Scan Library</ui-btn>
|
<ui-btn color="success" class="w-52" @click="scan">Scan Library</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="loaded && !shelves.length && search" class="w-full h-40 flex items-center justify-center">
|
<div v-else-if="loaded && !shelves.length && search" class="w-full h-40 flex items-center justify-center">
|
||||||
<p class="text-center text-xl font-book py-4">No results for query</p>
|
<p class="text-center text-xl py-4">No results for query</p>
|
||||||
</div>
|
</div>
|
||||||
<!-- Alternate plain view -->
|
<!-- Alternate plain view -->
|
||||||
<div v-else-if="isAlternativeBookshelfView" class="w-full mb-24">
|
<div v-else-if="isAlternativeBookshelfView" class="w-full mb-24">
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="absolute text-center categoryPlacard font-book transform z-30 bottom-px left-4 md:left-8 w-44 rounded-md" style="height: 22px">
|
<div class="absolute text-center categoryPlacard transform z-30 bottom-px left-4 md:left-8 w-44 rounded-md" style="height: 22px">
|
||||||
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border">
|
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border">
|
||||||
<p class="transform text-sm">{{ $strings[shelf.labelStringKey] }}</p>
|
<p class="transform text-sm">{{ $strings[shelf.labelStringKey] }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
<div id="toolbar" class="absolute top-10 md:top-0 left-0 w-full h-10 md:h-full z-40 flex items-center justify-end md:justify-start px-2 md:px-8">
|
<div id="toolbar" class="absolute top-10 md:top-0 left-0 w-full h-10 md:h-full z-40 flex items-center justify-end md:justify-start px-2 md:px-8">
|
||||||
<!-- Series books page -->
|
<!-- Series books page -->
|
||||||
<template v-if="selectedSeries">
|
<template v-if="selectedSeries">
|
||||||
<p class="pl-2 font-book text-base md:text-lg">
|
<p class="pl-2 text-base md:text-lg">
|
||||||
{{ seriesName }}
|
{{ seriesName }}
|
||||||
</p>
|
</p>
|
||||||
<div class="w-6 h-6 rounded-full bg-black bg-opacity-30 flex items-center justify-center ml-3">
|
<div class="w-6 h-6 rounded-full bg-black bg-opacity-30 flex items-center justify-center ml-3">
|
||||||
@@ -60,16 +60,26 @@
|
|||||||
</template>
|
</template>
|
||||||
<!-- library & collections page -->
|
<!-- library & collections page -->
|
||||||
<template v-else-if="page !== 'search' && page !== 'podcast-search' && page !== 'recent-episodes' && !isHome">
|
<template v-else-if="page !== 'search' && page !== 'podcast-search' && page !== 'recent-episodes' && !isHome">
|
||||||
<p class="font-book hidden md:block">{{ numShowing }} {{ entityName }}</p>
|
<p class="hidden md:block">{{ numShowing }} {{ entityName }}</p>
|
||||||
|
|
||||||
<div class="flex-grow hidden sm:inline-block" />
|
<div class="flex-grow hidden sm:inline-block" />
|
||||||
|
|
||||||
|
<!-- collapse series checkbox -->
|
||||||
<ui-checkbox v-if="isLibraryPage && isBookLibrary && !isBatchSelecting" v-model="settings.collapseSeries" :label="$strings.LabelCollapseSeries" checkbox-bg="bg" check-color="white" small class="mr-2" @input="updateCollapseSeries" />
|
<ui-checkbox v-if="isLibraryPage && isBookLibrary && !isBatchSelecting" v-model="settings.collapseSeries" :label="$strings.LabelCollapseSeries" checkbox-bg="bg" check-color="white" small class="mr-2" @input="updateCollapseSeries" />
|
||||||
|
|
||||||
|
<!-- library filter select -->
|
||||||
<controls-library-filter-select v-if="isLibraryPage && !isBatchSelecting" v-model="settings.filterBy" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateFilter" />
|
<controls-library-filter-select v-if="isLibraryPage && !isBatchSelecting" v-model="settings.filterBy" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateFilter" />
|
||||||
|
|
||||||
|
<!-- library sort select -->
|
||||||
<controls-library-sort-select v-if="isLibraryPage && !isBatchSelecting" v-model="settings.orderBy" :descending.sync="settings.orderDesc" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateOrder" />
|
<controls-library-sort-select v-if="isLibraryPage && !isBatchSelecting" v-model="settings.orderBy" :descending.sync="settings.orderDesc" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateOrder" />
|
||||||
|
|
||||||
|
<!-- series filter select -->
|
||||||
<controls-library-filter-select v-if="isSeriesPage && !isBatchSelecting" v-model="settings.seriesFilterBy" is-series class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateSeriesFilter" />
|
<controls-library-filter-select v-if="isSeriesPage && !isBatchSelecting" v-model="settings.seriesFilterBy" is-series class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateSeriesFilter" />
|
||||||
|
|
||||||
|
<!-- series sort select -->
|
||||||
<controls-sort-select v-if="isSeriesPage && !isBatchSelecting" v-model="settings.seriesSortBy" :descending.sync="settings.seriesSortDesc" :items="seriesSortItems" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateSeriesSort" />
|
<controls-sort-select v-if="isSeriesPage && !isBatchSelecting" v-model="settings.seriesSortBy" :descending.sync="settings.seriesSortDesc" :items="seriesSortItems" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateSeriesSort" />
|
||||||
|
|
||||||
|
<!-- issues page remove all button -->
|
||||||
<ui-btn v-if="isIssuesFilter && userCanDelete && !isBatchSelecting" :loading="processingIssues" color="error" small class="ml-4" @click="removeAllIssues">{{ $strings.ButtonRemoveAll }} {{ numShowing }} {{ entityName }}</ui-btn>
|
<ui-btn v-if="isIssuesFilter && userCanDelete && !isBatchSelecting" :loading="processingIssues" color="error" small class="ml-4" @click="removeAllIssues">{{ $strings.ButtonRemoveAll }} {{ numShowing }} {{ entityName }}</ui-btn>
|
||||||
</template>
|
</template>
|
||||||
<!-- search page -->
|
<!-- search page -->
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-44 fixed left-0 top-16 h-full bg-bg bg-opacity-100 md:bg-opacity-70 shadow-lg border-r border-white border-opacity-5 py-3 transform transition-transform" :class="wrapperClass" v-click-outside="clickOutside">
|
<div>
|
||||||
<div v-show="isMobilePortrait" class="flex items-center justify-end pb-2 px-4 mb-1" @click="closeDrawer">
|
<div class="w-44 fixed left-0 top-16 bg-bg bg-opacity-100 md:bg-opacity-70 shadow-lg border-r border-white border-opacity-5 py-3 transform transition-transform mb-12 overflow-y-auto" :class="wrapperClass + ' ' + (streamLibraryItem ? 'h-[calc(100%-270px)]' : 'h-[calc(100%-110px)]')" v-click-outside="clickOutside">
|
||||||
<span class="material-icons text-2xl">arrow_back</span>
|
<div v-show="isMobilePortrait" class="flex items-center justify-end pb-2 px-4 mb-1" @click="closeDrawer">
|
||||||
|
<span class="material-icons text-2xl">arrow_back</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nuxt-link v-for="route in configRoutes" :key="route.id" :to="route.path" class="w-full px-3 h-12 border-b border-primary border-opacity-30 flex items-center cursor-pointer relative" :class="routeName === route.id ? 'bg-primary bg-opacity-70' : 'hover:bg-primary hover:bg-opacity-30'">
|
||||||
|
<p class="leading-4">{{ route.title }}</p>
|
||||||
|
<div v-show="routeName === route.iod" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
|
</nuxt-link>
|
||||||
|
|
||||||
|
<modals-changelog-view-modal v-model="showChangelogModal" :changelog="currentVersionChangelog" :currentVersion="$config.version" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nuxt-link v-for="route in configRoutes" :key="route.id" :to="route.path" class="w-full px-4 h-12 border-b border-primary border-opacity-30 flex items-center cursor-pointer relative" :class="routeName === route.id ? 'bg-primary bg-opacity-70' : 'hover:bg-primary hover:bg-opacity-30'">
|
<div class="w-44 h-12 px-4 border-t bg-bg border-black border-opacity-20 fixed left-0 flex flex-col justify-center" :class="wrapperClass" :style="{ bottom: streamLibraryItem ? '160px' : '0px' }">
|
||||||
<p>{{ route.title }}</p>
|
|
||||||
<div v-show="routeName === route.iod" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
|
||||||
</nuxt-link>
|
|
||||||
|
|
||||||
<div class="w-full h-12 px-4 border-t border-black border-opacity-20 absolute left-0 flex flex-col justify-center" :style="{ bottom: streamLibraryItem && isMobileLandscape ? '300px' : '65px' }">
|
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<p class="underline font-mono text-sm" @click="clickChangelog">v{{ $config.version }}</p>
|
<p class="underline font-mono text-sm" @click="clickChangelog">v{{ $config.version }}</p>
|
||||||
|
|
||||||
@@ -17,8 +21,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<a v-if="hasUpdate" :href="githubTagUrl" target="_blank" class="text-warning text-xs">Latest: {{ latestVersion }}</a>
|
<a v-if="hasUpdate" :href="githubTagUrl" target="_blank" class="text-warning text-xs">Latest: {{ latestVersion }}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<modals-changelog-view-modal v-model="showChangelogModal" :changelog="currentVersionChangelog" :currentVersion="$config.version" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div v-if="initialized && !totalShelves && !hasFilter && entityName === 'items'" class="w-full flex flex-col items-center justify-center py-12">
|
<div v-if="initialized && !totalShelves && !hasFilter && entityName === 'items'" class="w-full flex flex-col items-center justify-center py-12">
|
||||||
<p class="text-center text-2xl font-book mb-4 py-4">{{ $getString('MessageXLibraryIsEmpty', [libraryName]) }}</p>
|
<p class="text-center text-2xl mb-4 py-4">{{ $getString('MessageXLibraryIsEmpty', [libraryName]) }}</p>
|
||||||
<div v-if="userIsAdminOrUp" class="flex">
|
<div v-if="userIsAdminOrUp" class="flex">
|
||||||
<ui-btn to="/config" color="primary" class="w-52 mr-2">{{ $strings.ButtonConfigureScanner }}</ui-btn>
|
<ui-btn to="/config" color="primary" class="w-52 mr-2">{{ $strings.ButtonConfigureScanner }}</ui-btn>
|
||||||
<ui-btn color="success" class="w-52" @click="scan">{{ $strings.ButtonScanLibrary }}</ui-btn>
|
<ui-btn color="success" class="w-52" @click="scan">{{ $strings.ButtonScanLibrary }}</ui-btn>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<p class="font-book pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonHome }}</p>
|
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonHome }}</p>
|
||||||
|
|
||||||
<div v-show="homePage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
<div v-show="homePage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
<nuxt-link v-if="isPodcastLibrary" :to="`/library/${currentLibraryId}/podcast/latest`" class="w-full h-20 flex flex-col items-center justify-center text-white border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastLatestPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
<nuxt-link v-if="isPodcastLibrary" :to="`/library/${currentLibraryId}/podcast/latest`" class="w-full h-20 flex flex-col items-center justify-center text-white border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastLatestPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||||
<span class="material-icons text-2xl">format_list_bulleted</span>
|
<span class="material-icons text-2xl">format_list_bulleted</span>
|
||||||
|
|
||||||
<p class="font-book pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonLatest }}</p>
|
<p class="pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonLatest }}</p>
|
||||||
|
|
||||||
<div v-show="isPodcastLatestPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
<div v-show="isPodcastLatestPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<p class="font-book pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonLibrary }}</p>
|
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonLibrary }}</p>
|
||||||
|
|
||||||
<div v-show="showLibrary" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
<div v-show="showLibrary" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<p class="font-book pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonSeries }}</p>
|
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonSeries }}</p>
|
||||||
|
|
||||||
<div v-show="isSeriesPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
<div v-show="isSeriesPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === 'collections' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === 'collections' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||||
<span class="material-icons-outlined text-2xl">collections_bookmark</span>
|
<span class="material-icons-outlined text-2xl">collections_bookmark</span>
|
||||||
|
|
||||||
<p class="font-book pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonCollections }}</p>
|
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonCollections }}</p>
|
||||||
|
|
||||||
<div v-show="paramId === 'collections'" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
<div v-show="paramId === 'collections'" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<p class="font-book pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonAuthors }}</p>
|
<p class="pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonAuthors }}</p>
|
||||||
|
|
||||||
<div v-show="isAuthorsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
<div v-show="isAuthorsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/search`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastSearchPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/search`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastSearchPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||||
<span class="abs-icons icon-podcast text-xl"></span>
|
<span class="abs-icons icon-podcast text-xl"></span>
|
||||||
|
|
||||||
<p class="font-book pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonSearch }}</p>
|
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonSearch }}</p>
|
||||||
|
|
||||||
<div v-show="isPodcastSearchPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
<div v-show="isPodcastSearchPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
@@ -73,7 +73,7 @@
|
|||||||
<nuxt-link v-if="isMusicLibrary" :to="`/library/${currentLibraryId}/bookshelf/albums`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isMusicAlbumsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
<nuxt-link v-if="isMusicLibrary" :to="`/library/${currentLibraryId}/bookshelf/albums`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isMusicAlbumsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||||
<span class="material-icons-outlined text-xl">album</span>
|
<span class="material-icons-outlined text-xl">album</span>
|
||||||
|
|
||||||
<p class="font-book pt-1.5 text-center leading-4" style="font-size: 0.9rem">Albums</p>
|
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">Albums</p>
|
||||||
|
|
||||||
<div v-show="isMusicAlbumsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
<div v-show="isMusicAlbumsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
@@ -81,15 +81,23 @@
|
|||||||
<nuxt-link v-if="showPlaylists" :to="`/library/${currentLibraryId}/bookshelf/playlists`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPlaylistsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
<nuxt-link v-if="showPlaylists" :to="`/library/${currentLibraryId}/bookshelf/playlists`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPlaylistsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||||
<span class="material-icons text-2.5xl">queue_music</span>
|
<span class="material-icons text-2.5xl">queue_music</span>
|
||||||
|
|
||||||
<p class="font-book pt-0.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonPlaylists }}</p>
|
<p class="pt-0.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonPlaylists }}</p>
|
||||||
|
|
||||||
<div v-show="isPlaylistsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
<div v-show="isPlaylistsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
|
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/download-queue`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastDownloadQueuePage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||||
|
<span class="material-icons text-2xl">file_download</span>
|
||||||
|
|
||||||
|
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonDownloadQueue }}</p>
|
||||||
|
|
||||||
|
<div v-show="isPodcastDownloadQueuePage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link v-if="numIssues" :to="`/library/${currentLibraryId}/bookshelf?filter=issues`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-opacity-40 cursor-pointer relative" :class="showingIssues ? 'bg-error bg-opacity-40' : ' bg-error bg-opacity-20'">
|
<nuxt-link v-if="numIssues" :to="`/library/${currentLibraryId}/bookshelf?filter=issues`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-opacity-40 cursor-pointer relative" :class="showingIssues ? 'bg-error bg-opacity-40' : ' bg-error bg-opacity-20'">
|
||||||
<span class="material-icons text-2xl">warning</span>
|
<span class="material-icons text-2xl">warning</span>
|
||||||
|
|
||||||
<p class="font-book pt-1.5 text-center leading-4" style="font-size: 1rem">{{ $strings.ButtonIssues }}</p>
|
<p class="pt-1.5 text-center leading-4" style="font-size: 1rem">{{ $strings.ButtonIssues }}</p>
|
||||||
|
|
||||||
<div v-show="showingIssues" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
<div v-show="showingIssues" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
<div class="absolute top-1 right-1 w-4 h-4 rounded-full bg-white bg-opacity-30 flex items-center justify-center">
|
<div class="absolute top-1 right-1 w-4 h-4 rounded-full bg-white bg-opacity-30 flex items-center justify-center">
|
||||||
@@ -149,6 +157,9 @@ export default {
|
|||||||
isMusicLibrary() {
|
isMusicLibrary() {
|
||||||
return this.currentLibraryMediaType === 'music'
|
return this.currentLibraryMediaType === 'music'
|
||||||
},
|
},
|
||||||
|
isPodcastDownloadQueuePage() {
|
||||||
|
return this.$route.name === 'library-library-podcast-download-queue'
|
||||||
|
},
|
||||||
isPodcastSearchPage() {
|
isPodcastSearchPage() {
|
||||||
return this.$route.name === 'library-library-podcast-search'
|
return this.$route.name === 'library-library-podcast-search'
|
||||||
},
|
},
|
||||||
@@ -212,4 +223,4 @@ export default {
|
|||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,22 +1,25 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="streamLibraryItem" id="streamContainer" class="w-full fixed bottom-0 left-0 right-0 h-48 sm:h-44 md:h-40 z-50 bg-primary px-2 md:px-4 pb-1 md:pb-4 pt-2">
|
<div v-if="streamLibraryItem" id="streamContainer" class="w-full fixed bottom-0 left-0 right-0 h-48 md:h-40 z-50 bg-primary px-2 md:px-4 pb-1 md:pb-4 pt-2">
|
||||||
<div id="videoDock" />
|
<div id="videoDock" />
|
||||||
<nuxt-link v-if="!playerHandler.isVideo" :to="`/item/${streamLibraryItem.id}`" class="absolute left-2 top-2 md:left-4 cursor-pointer">
|
<nuxt-link v-if="!playerHandler.isVideo" :to="`/item/${streamLibraryItem.id}`" class="absolute left-2 top-2 md:left-4 cursor-pointer">
|
||||||
<covers-book-cover :library-item="streamLibraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="coverAspectRatio" />
|
<covers-book-cover :library-item="streamLibraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="coverAspectRatio" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<div class="flex items-start mb-6 md:mb-0" :class="playerHandler.isVideo ? 'ml-4 pl-96' : isSquareCover ? 'pl-18 sm:pl-24' : 'pl-12 sm:pl-16'">
|
<div class="flex items-start mb-6 md:mb-0" :class="playerHandler.isVideo ? 'ml-4 pl-96' : isSquareCover ? 'pl-18 sm:pl-24' : 'pl-12 sm:pl-16'">
|
||||||
<div>
|
<div class="min-w-0">
|
||||||
<nuxt-link :to="`/item/${streamLibraryItem.id}`" class="hover:underline cursor-pointer text-sm sm:text-lg">
|
<nuxt-link :to="`/item/${streamLibraryItem.id}`" class="hover:underline cursor-pointer text-sm sm:text-lg block truncate">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<div v-if="!playerHandler.isVideo" class="text-gray-400 flex items-center">
|
<div v-if="!playerHandler.isVideo" class="text-gray-400 flex items-center">
|
||||||
<span class="material-icons text-sm">person</span>
|
<span class="material-icons text-sm">person</span>
|
||||||
<p v-if="podcastAuthor" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ podcastAuthor }}</p>
|
<div class="flex items-center">
|
||||||
<p v-else-if="musicArtists" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ musicArtists }}</p>
|
<div v-if="podcastAuthor" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ podcastAuthor }}</div>
|
||||||
<p v-else-if="authors.length" class="pl-1 sm:pl-1.5 text-xs sm:text-base">
|
<div v-else-if="musicArtists" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ musicArtists }}</div>
|
||||||
<nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">, </span></nuxt-link>
|
<div v-else-if="authors.length" class="pl-1 sm:pl-1.5 text-xs sm:text-base">
|
||||||
</p>
|
<nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">, </span></nuxt-link>
|
||||||
<p v-else class="text-xs sm:text-base cursor-pointer pl-1 sm:pl-1.5">{{ $strings.LabelUnknown }}</p>
|
</div>
|
||||||
|
<div v-else class="text-xs sm:text-base cursor-pointer pl-1 sm:pl-1.5">{{ $strings.LabelUnknown }}</div>
|
||||||
|
<widgets-explicit-indicator :explicit="isExplicit"></widgets-explicit-indicator>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-gray-400 flex items-center">
|
<div class="text-gray-400 flex items-center">
|
||||||
@@ -129,6 +132,9 @@ export default {
|
|||||||
isMusic() {
|
isMusic() {
|
||||||
return this.streamLibraryItem ? this.streamLibraryItem.mediaType === 'music' : false
|
return this.streamLibraryItem ? this.streamLibraryItem.mediaType === 'music' : false
|
||||||
},
|
},
|
||||||
|
isExplicit() {
|
||||||
|
return this.mediaMetadata.explicit || false
|
||||||
|
},
|
||||||
mediaMetadata() {
|
mediaMetadata() {
|
||||||
return this.media.metadata || {}
|
return this.media.metadata || {}
|
||||||
},
|
},
|
||||||
@@ -474,4 +480,4 @@ export default {
|
|||||||
#streamContainer {
|
#streamContainer {
|
||||||
box-shadow: 0px -6px 8px #1111113f;
|
box-shadow: 0px -6px 8px #1111113f;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -28,7 +28,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="px-4 flex-grow">
|
<div v-else class="px-4 flex-grow">
|
||||||
<h1>{{ book.title }}</h1>
|
<h1>
|
||||||
|
<div class="flex items-center">
|
||||||
|
{{ book.title }}<widgets-explicit-indicator :explicit="book.explicit" />
|
||||||
|
</div>
|
||||||
|
</h1>
|
||||||
<p class="text-base text-gray-300 whitespace-nowrap truncate">by {{ book.author }}</p>
|
<p class="text-base text-gray-300 whitespace-nowrap truncate">by {{ book.author }}</p>
|
||||||
<p v-if="book.genres" class="text-xs text-gray-400 leading-5">{{ book.genres.join(', ') }}</p>
|
<p v-if="book.genres" class="text-xs text-gray-400 leading-5">{{ book.genres.join(', ') }}</p>
|
||||||
<p class="text-xs text-gray-400 leading-5">{{ book.trackCount }} Episodes</p>
|
<p class="text-xs text-gray-400 leading-5">{{ book.trackCount }} Episodes</p>
|
||||||
@@ -78,4 +82,4 @@ export default {
|
|||||||
this.selectedCover = this.bookCovers.length ? this.bookCovers[0] : this.book.cover || null
|
this.selectedCover = this.bookCovers.length ? this.bookCovers[0] : this.book.cover || null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<covers-group-cover ref="groupcover" :id="groupEncode" :name="groupName" :type="groupType" :book-items="bookItems" :width="width" :height="height" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
<covers-group-cover ref="groupcover" :id="groupEncode" :name="groupName" :type="groupType" :book-items="bookItems" :width="width" :height="height" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
|
|
||||||
<div v-if="hasValidCovers" class="bg-black bg-opacity-60 absolute top-0 left-0 w-full h-full flex items-center justify-center text-center transition-opacity z-30" :class="isHovering ? '' : 'opacity-0'" :style="{ padding: `${sizeMultiplier}rem` }">
|
<div v-if="hasValidCovers" class="bg-black bg-opacity-60 absolute top-0 left-0 w-full h-full flex items-center justify-center text-center transition-opacity z-30" :class="isHovering ? '' : 'opacity-0'" :style="{ padding: `${sizeMultiplier}rem` }">
|
||||||
<p class="font-book" :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">{{ groupName }}</p>
|
<p :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">{{ groupName }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="absolute z-10 top-1.5 right-1.5 rounded-md leading-3 text-sm p-1 font-semibold text-white flex items-center justify-center" style="background-color: #cd9d49dd">{{ bookItems.length }}</div>
|
<div class="absolute z-10 top-1.5 right-1.5 rounded-md leading-3 text-sm p-1 font-semibold text-white flex items-center justify-center" style="background-color: #cd9d49dd">{{ bookItems.length }}</div>
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex items-center h-full px-1 overflow-hidden">
|
||||||
|
<div class="h-5 w-5 min-w-5 text-lg mr-1.5 flex items-center justify-center">
|
||||||
|
<span v-if="isFinished" :class="taskIconStatus" class="material-icons text-base">{{actionIcon}}</span>
|
||||||
|
<widgets-loading-spinner v-else />
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow px-2 taskRunningCardContent">
|
||||||
|
<p class="truncate text-sm">{{ title }}</p>
|
||||||
|
|
||||||
|
<p class="truncate text-xs text-gray-300">{{ description }}</p>
|
||||||
|
|
||||||
|
<p v-if="isFailed && failedMessage" class="text-xs truncate text-red-500">{{ failedMessage }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
task: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
title() {
|
||||||
|
return this.task.title || 'No Title'
|
||||||
|
},
|
||||||
|
description() {
|
||||||
|
return this.task.description || ''
|
||||||
|
},
|
||||||
|
details() {
|
||||||
|
return this.task.details || 'Unknown'
|
||||||
|
},
|
||||||
|
isFinished() {
|
||||||
|
return this.task.isFinished || false
|
||||||
|
},
|
||||||
|
isFailed() {
|
||||||
|
return this.task.isFailed || false
|
||||||
|
},
|
||||||
|
failedMessage() {
|
||||||
|
return this.task.error || ''
|
||||||
|
},
|
||||||
|
action() {
|
||||||
|
return this.task.action || ''
|
||||||
|
},
|
||||||
|
actionIcon() {
|
||||||
|
switch (this.action) {
|
||||||
|
case 'download-podcast-episode':
|
||||||
|
return 'cloud_download'
|
||||||
|
case 'encode-m4b':
|
||||||
|
return 'sync'
|
||||||
|
default:
|
||||||
|
return 'settings'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
taskIconStatus() {
|
||||||
|
if (this.isFinished && this.isFailed) {
|
||||||
|
return 'text-red-500'
|
||||||
|
}
|
||||||
|
if (this.isFinished && !this.isFailed) {
|
||||||
|
return 'text-green-500'
|
||||||
|
}
|
||||||
|
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.taskRunningCardContent {
|
||||||
|
width: calc(100% - 80px);
|
||||||
|
height: 75px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
<covers-preview-cover ref="cover" :src="coverSrc" :width="width" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
<covers-preview-cover ref="cover" :src="coverSrc" :width="width" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md font-book text-center" :style="{ width: Math.min(200, width) + 'px' }">
|
<div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md text-center" :style="{ width: Math.min(200, width) + 'px' }">
|
||||||
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
|
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
|
||||||
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
|
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,9 +7,12 @@
|
|||||||
|
|
||||||
<!-- Alternative bookshelf title/author/sort -->
|
<!-- Alternative bookshelf title/author/sort -->
|
||||||
<div v-if="isAlternativeBookshelfView || isAuthorBookshelfView" class="absolute left-0 z-50 w-full" :style="{ bottom: `-${titleDisplayBottomOffset}rem` }">
|
<div v-if="isAlternativeBookshelfView || isAuthorBookshelfView" class="absolute left-0 z-50 w-full" :style="{ bottom: `-${titleDisplayBottomOffset}rem` }">
|
||||||
<p class="truncate" :style="{ fontSize: 0.9 * sizeMultiplier + 'rem' }">
|
<div :style="{ fontSize: 0.9 * sizeMultiplier + 'rem' }">
|
||||||
{{ displayTitle }}
|
<div class="flex items-center">
|
||||||
</p>
|
<span class="truncate">{{ displayTitle }}</span>
|
||||||
|
<widgets-explicit-indicator :explicit="isExplicit" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<p class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displayLineTwo || ' ' }}</p>
|
<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>
|
<p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -23,7 +26,7 @@
|
|||||||
|
|
||||||
<div class="w-full h-full absolute top-0 left-0 rounded overflow-hidden z-10">
|
<div class="w-full h-full absolute top-0 left-0 rounded overflow-hidden z-10">
|
||||||
<div v-show="libraryItem && !imageReady" class="absolute top-0 left-0 w-full h-full flex items-center justify-center" :style="{ padding: sizeMultiplier * 0.5 + 'rem' }">
|
<div v-show="libraryItem && !imageReady" class="absolute top-0 left-0 w-full h-full flex items-center justify-center" :style="{ padding: sizeMultiplier * 0.5 + 'rem' }">
|
||||||
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }" class="font-book text-gray-300 text-center">{{ title }}</p>
|
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }" class="text-gray-300 text-center">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Cover Image -->
|
<!-- Cover Image -->
|
||||||
@@ -32,13 +35,13 @@
|
|||||||
<!-- Placeholder Cover Title & Author -->
|
<!-- Placeholder Cover Title & Author -->
|
||||||
<div v-if="!hasCover" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'rem' }">
|
<div v-if="!hasCover" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'rem' }">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-center font-book" style="color: rgb(247 223 187)" :style="{ fontSize: titleFontSize + 'rem' }">
|
<p class="text-center" style="color: rgb(247 223 187)" :style="{ fontSize: titleFontSize + 'rem' }">
|
||||||
{{ titleCleaned }}
|
{{ titleCleaned }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!hasCover" class="absolute left-0 right-0 w-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'rem', bottom: authorBottom + 'rem' }">
|
<div v-if="!hasCover" class="absolute left-0 right-0 w-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'rem', bottom: authorBottom + 'rem' }">
|
||||||
<p class="text-center font-book" style="color: rgb(247 223 187); opacity: 0.75" :style="{ fontSize: authorFontSize + 'rem' }">{{ authorCleaned }}</p>
|
<p class="text-center" style="color: rgb(247 223 187); opacity: 0.75" :style="{ fontSize: authorFontSize + 'rem' }">{{ authorCleaned }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -102,8 +105,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Podcast Episode # -->
|
<!-- Podcast Episode # -->
|
||||||
<div v-if="recentEpisodeNumber && !isHovering && !isSelectionMode && !processing" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-10" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
|
<div v-if="recentEpisodeNumber !== null && !isHovering && !isSelectionMode && !processing" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-10" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
|
||||||
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">Episode #{{ recentEpisodeNumber }}</p>
|
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">
|
||||||
|
Episode<span v-if="recentEpisodeNumber"> #{{ recentEpisodeNumber }}</span>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Podcast Num Episodes -->
|
<!-- Podcast Num Episodes -->
|
||||||
@@ -193,6 +198,9 @@ export default {
|
|||||||
isMusic() {
|
isMusic() {
|
||||||
return this.mediaType === 'music'
|
return this.mediaType === 'music'
|
||||||
},
|
},
|
||||||
|
isExplicit() {
|
||||||
|
return this.mediaMetadata.explicit || false
|
||||||
|
},
|
||||||
placeholderUrl() {
|
placeholderUrl() {
|
||||||
const config = this.$config || this.$nuxt.$config
|
const config = this.$config || this.$nuxt.$config
|
||||||
return `${config.routerBasePath}/book_placeholder.jpg`
|
return `${config.routerBasePath}/book_placeholder.jpg`
|
||||||
@@ -236,7 +244,7 @@ export default {
|
|||||||
if (this.recentEpisode.episode) {
|
if (this.recentEpisode.episode) {
|
||||||
return this.recentEpisode.episode.replace(/^#/, '')
|
return this.recentEpisode.episode.replace(/^#/, '')
|
||||||
}
|
}
|
||||||
return this.recentEpisode.index
|
return ''
|
||||||
},
|
},
|
||||||
collapsedSeries() {
|
collapsedSeries() {
|
||||||
// Only added to item object when collapseSeries is enabled
|
// Only added to item object when collapseSeries is enabled
|
||||||
@@ -317,8 +325,13 @@ export default {
|
|||||||
if (this.episodeProgress) return this.episodeProgress
|
if (this.episodeProgress) return this.episodeProgress
|
||||||
return this.store.getters['user/getUserMediaProgress'](this.libraryItemId)
|
return this.store.getters['user/getUserMediaProgress'](this.libraryItemId)
|
||||||
},
|
},
|
||||||
|
useEBookProgress() {
|
||||||
|
if (!this.userProgress || this.userProgress.progress) return false
|
||||||
|
return this.userProgress.ebookProgress > 0
|
||||||
|
},
|
||||||
userProgressPercent() {
|
userProgressPercent() {
|
||||||
return this.userProgress ? this.userProgress.progress || 0 : 0
|
if (this.useEBookProgress) return Math.max(Math.min(1, this.userProgress.ebookProgress), 0)
|
||||||
|
return this.userProgress ? Math.max(Math.min(1, this.userProgress.progress), 0) || 0 : 0
|
||||||
},
|
},
|
||||||
itemIsFinished() {
|
itemIsFinished() {
|
||||||
return this.userProgress ? !!this.userProgress.isFinished : false
|
return this.userProgress ? !!this.userProgress.isFinished : false
|
||||||
@@ -662,7 +675,7 @@ export default {
|
|||||||
const axios = this.$axios || this.$nuxt.$axios
|
const axios = this.$axios || this.$nuxt.$axios
|
||||||
this.processing = true
|
this.processing = true
|
||||||
axios
|
axios
|
||||||
.$get(`/api/items/${this.libraryItemId}/scan`)
|
.$post(`/api/items/${this.libraryItemId}/scan`)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
var result = data.result
|
var result = data.result
|
||||||
if (!result) {
|
if (!result) {
|
||||||
@@ -734,7 +747,7 @@ export default {
|
|||||||
episodeId: this.recentEpisode.id,
|
episodeId: this.recentEpisode.id,
|
||||||
title: this.recentEpisode.title,
|
title: this.recentEpisode.title,
|
||||||
subtitle: this.mediaMetadata.title,
|
subtitle: this.mediaMetadata.title,
|
||||||
caption: this.recentEpisode.publishedAt ? `Published ${this.$formatDate(this.recentEpisode.publishedAt, 'MMM do, yyyy')}` : 'Unknown publish date',
|
caption: this.recentEpisode.publishedAt ? `Published ${this.$formatDate(this.recentEpisode.publishedAt, this.dateFormat)}` : 'Unknown publish date',
|
||||||
duration: this.recentEpisode.audioFile.duration || null,
|
duration: this.recentEpisode.audioFile.duration || null,
|
||||||
coverPath: this.media.coverPath || null
|
coverPath: this.media.coverPath || null
|
||||||
}
|
}
|
||||||
@@ -858,7 +871,7 @@ export default {
|
|||||||
episodeId: episode.id,
|
episodeId: episode.id,
|
||||||
title: episode.title,
|
title: episode.title,
|
||||||
subtitle: this.mediaMetadata.title,
|
subtitle: this.mediaMetadata.title,
|
||||||
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, 'MMM do, yyyy')}` : 'Unknown publish date',
|
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date',
|
||||||
duration: episode.audioFile.duration || null,
|
duration: episode.audioFile.duration || null,
|
||||||
coverPath: this.media.coverPath || null
|
coverPath: this.media.coverPath || null
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -12,13 +12,13 @@
|
|||||||
|
|
||||||
<span v-if="!isHovering && rssFeed" class="absolute z-10 material-icons text-success" :style="{ top: 0.5 * sizeMultiplier + 'rem', left: 0.5 * sizeMultiplier + 'rem', fontSize: 1.5 * sizeMultiplier + 'rem' }">rss_feed</span>
|
<span v-if="!isHovering && rssFeed" class="absolute z-10 material-icons text-success" :style="{ top: 0.5 * sizeMultiplier + 'rem', left: 0.5 * sizeMultiplier + 'rem', fontSize: 1.5 * sizeMultiplier + 'rem' }">rss_feed</span>
|
||||||
|
|
||||||
<div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md font-book text-center" :style="{ width: Math.min(200, width) + 'px' }">
|
<div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md text-center" :style="{ width: Math.min(200, width) + 'px' }">
|
||||||
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
|
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
|
||||||
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
|
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="absolute z-30 left-0 right-0 mx-auto -bottom-8 h-8 py-1 rounded-md text-center">
|
<div v-else class="absolute z-30 left-0 right-0 mx-auto -bottom-8 h-8 py-1 rounded-md text-center">
|
||||||
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
|
<p class="truncate" :style="{ fontSize: labelFontSize * sizeMultiplier + 'rem' }">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -9,13 +9,13 @@
|
|||||||
<span class="material-icons text-xl text-white text-opacity-75 hover:text-opacity-100">edit</span>
|
<span class="material-icons text-xl text-white text-opacity-75 hover:text-opacity-100">edit</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md font-book text-center" :style="{ width: Math.min(200, width) + 'px' }">
|
<div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md text-center" :style="{ width: Math.min(200, width) + 'px' }">
|
||||||
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
|
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
|
||||||
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
|
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="absolute z-30 left-0 right-0 mx-auto -bottom-8 h-8 py-1 rounded-md text-center">
|
<div v-else class="absolute z-30 left-0 right-0 mx-auto -bottom-8 h-8 py-1 rounded-md text-center">
|
||||||
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
|
<p class="truncate" :style="{ fontSize: labelFontSize * sizeMultiplier + 'rem' }">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -50,8 +50,8 @@ export default {
|
|||||||
return 0.875
|
return 0.875
|
||||||
},
|
},
|
||||||
sizeMultiplier() {
|
sizeMultiplier() {
|
||||||
if (this.bookCoverAspectRatio === 1) return this.width / (120 * 1.6 * 2)
|
if (this.bookCoverAspectRatio === 1) return this.width / (120 * 1.6)
|
||||||
return this.width / 240
|
return this.width / 120
|
||||||
},
|
},
|
||||||
title() {
|
title() {
|
||||||
return this.playlist ? this.playlist.name : ''
|
return this.playlist ? this.playlist.name : ''
|
||||||
|
|||||||
@@ -10,18 +10,18 @@
|
|||||||
<div v-if="isSeriesFinished" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b bg-success w-full" />
|
<div v-if="isSeriesFinished" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b bg-success w-full" />
|
||||||
|
|
||||||
<div v-if="hasValidCovers" class="bg-black bg-opacity-60 absolute top-0 left-0 w-full h-full flex items-center justify-center text-center transition-opacity" :class="isHovering ? '' : 'opacity-0'" :style="{ padding: `${sizeMultiplier}rem` }">
|
<div v-if="hasValidCovers" class="bg-black bg-opacity-60 absolute top-0 left-0 w-full h-full flex items-center justify-center text-center transition-opacity" :class="isHovering ? '' : 'opacity-0'" :style="{ padding: `${sizeMultiplier}rem` }">
|
||||||
<p class="font-book" :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">{{ displayTitle }}</p>
|
<p :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">{{ displayTitle }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span v-if="!isHovering && rssFeed" class="absolute z-10 material-icons text-success" :style="{ top: 0.5 * sizeMultiplier + 'rem', left: 0.5 * sizeMultiplier + 'rem', fontSize: 1.5 * sizeMultiplier + 'rem' }">rss_feed</span>
|
<span v-if="!isHovering && rssFeed" class="absolute z-10 material-icons text-success" :style="{ top: 0.5 * sizeMultiplier + 'rem', left: 0.5 * sizeMultiplier + 'rem', fontSize: 1.5 * sizeMultiplier + 'rem' }">rss_feed</span>
|
||||||
|
|
||||||
<div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-10 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md font-book text-center" :style="{ width: Math.min(200, width) + 'px' }">
|
<div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-10 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md text-center" :style="{ width: Math.min(200, width) + 'px' }">
|
||||||
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
|
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
|
||||||
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ displayTitle }}</p>
|
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ displayTitle }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="absolute z-30 left-0 right-0 mx-auto -bottom-8 h-8 py-1 rounded-md text-center">
|
<div v-else class="absolute z-30 left-0 right-0 mx-auto -bottom-8 h-8 py-1 rounded-md text-center">
|
||||||
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ displayTitle }}</p>
|
<p class="truncate" :style="{ fontSize: labelFontSize * sizeMultiplier + 'rem' }">{{ displayTitle }}</p>
|
||||||
<p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p>
|
<p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export default {
|
|||||||
},
|
},
|
||||||
folderPath() {
|
folderPath() {
|
||||||
if (!this.libraryFolderPath) return ''
|
if (!this.libraryFolderPath) return ''
|
||||||
return `${this.libraryFolderPath}\\${this.$sanitizeFilename(this.title)}`
|
return `${this.libraryFolderPath}/${this.$sanitizeFilename(this.title)}`
|
||||||
},
|
},
|
||||||
detailsWidth() {
|
detailsWidth() {
|
||||||
return this.width - 85
|
return this.width - 85
|
||||||
|
|||||||
@@ -185,6 +185,11 @@ export default {
|
|||||||
value: 'tracks',
|
value: 'tracks',
|
||||||
sublist: true
|
sublist: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelAbridged,
|
||||||
|
value: 'abridged',
|
||||||
|
sublist: false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: this.$strings.ButtonIssues,
|
text: this.$strings.ButtonIssues,
|
||||||
value: 'issues',
|
value: 'issues',
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="wrapper" class="relative ml-4 sm:ml-8" v-click-outside="clickOutside">
|
<div ref="wrapper" class="relative ml-4 sm:ml-8" v-click-outside="clickOutside">
|
||||||
<div class="flex items-center justify-center text-gray-300 cursor-pointer h-full" @mousedown.prevent @mouseup.prevent @click="setShowMenu(true)">
|
<div class="flex items-center justify-center text-gray-300 cursor-pointer h-full" @mousedown.prevent @mouseup.prevent @click="setShowMenu(true)">
|
||||||
<span class="font-mono uppercase text-gray-200 text-sm sm:text-base">{{ playbackRate.toFixed(1) }}<span class="text-base sm:text-lg">⨯</span></span>
|
<span class="font-mono uppercase text-gray-200 text-sm sm:text-base">{{ playbackRate.toFixed(1) }}<span class="text-base">x</span></span>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="showMenu" class="absolute -top-20 z-20 bg-bg border-black-200 border shadow-xl rounded-lg" :style="{ left: menuLeft + 'px' }">
|
<div v-show="showMenu" class="absolute -top-20 z-20 bg-bg border-black-200 border shadow-xl rounded-lg" :style="{ left: menuLeft + 'px' }">
|
||||||
<div class="absolute -bottom-1.5 right-0 w-full flex justify-center" :style="{ left: arrowLeft + 'px' }">
|
<div class="absolute -bottom-1.5 right-0 w-full flex justify-center" :style="{ left: arrowLeft + 'px' }">
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
<template v-for="rate in rates">
|
<template v-for="rate in rates">
|
||||||
<div :key="rate" class="h-full border-black-300 w-11 cursor-pointer border rounded-sm" :class="value === rate ? 'bg-black-100' : 'hover:bg-black hover:bg-opacity-10'" style="min-width: 44px; max-width: 44px" @click="set(rate)">
|
<div :key="rate" class="h-full border-black-300 w-11 cursor-pointer border rounded-sm" :class="value === rate ? 'bg-black-100' : 'hover:bg-black hover:bg-opacity-10'" style="min-width: 44px; max-width: 44px" @click="set(rate)">
|
||||||
<div class="w-full h-full flex justify-center items-center">
|
<div class="w-full h-full flex justify-center items-center">
|
||||||
<p class="text-xs text-center font-mono">{{ rate }}<span class="text-sm">⨯</span></p>
|
<p class="text-xs text-center font-mono">{{ rate }}<span class="text-sm">x</span></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
<div class="w-full py-1 px-4">
|
<div class="w-full py-1 px-4">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<ui-icon-btn :disabled="!canDecrement" icon="remove" @click="decrement" />
|
<ui-icon-btn :disabled="!canDecrement" icon="remove" @click="decrement" />
|
||||||
<p class="px-2 text-2xl sm:text-3xl">{{ playbackRate }}<span class="text-2xl">⨯</span></p>
|
<p class="px-2 text-2xl sm:text-3xl">{{ playbackRate }}<span class="text-2xl">x</span></p>
|
||||||
<ui-icon-btn :disabled="!canIncrement" icon="add" @click="increment" />
|
<ui-icon-btn :disabled="!canIncrement" icon="add" @click="increment" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
<img v-if="libraryItem" ref="cover" :src="fullCoverUrl" loading="lazy" @error="imageError" @load="imageLoaded" class="w-full h-full absolute top-0 left-0 z-10 duration-300 transition-opacity" :style="{ opacity: imageReady ? '1' : '0' }" :class="showCoverBg ? 'object-contain' : 'object-fill'" />
|
<img v-if="libraryItem" ref="cover" :src="fullCoverUrl" loading="lazy" @error="imageError" @load="imageLoaded" class="w-full h-full absolute top-0 left-0 z-10 duration-300 transition-opacity" :style="{ opacity: imageReady ? '1' : '0' }" :class="showCoverBg ? 'object-contain' : 'object-fill'" />
|
||||||
<div v-show="loading && libraryItem" class="absolute top-0 left-0 h-full w-full flex items-center justify-center">
|
<div v-show="loading && libraryItem" class="absolute top-0 left-0 h-full w-full flex items-center justify-center">
|
||||||
<p class="font-book text-center" :style="{ fontSize: 0.75 * sizeMultiplier + 'rem' }">{{ title }}</p>
|
<p class="text-center" :style="{ fontSize: 0.75 * sizeMultiplier + 'rem' }">{{ title }}</p>
|
||||||
<div class="absolute top-2 right-2">
|
<div class="absolute top-2 right-2">
|
||||||
<widgets-loading-spinner />
|
<widgets-loading-spinner />
|
||||||
</div>
|
</div>
|
||||||
@@ -17,17 +17,17 @@
|
|||||||
<div v-if="imageFailed" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full bg-red-100" :style="{ padding: placeholderCoverPadding + 'rem' }">
|
<div v-if="imageFailed" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full bg-red-100" :style="{ padding: placeholderCoverPadding + 'rem' }">
|
||||||
<div class="w-full h-full border-2 border-error flex flex-col items-center justify-center">
|
<div class="w-full h-full border-2 border-error flex flex-col items-center justify-center">
|
||||||
<img src="/Logo.png" loading="lazy" class="mb-2" :style="{ height: 64 * sizeMultiplier + 'px' }" />
|
<img src="/Logo.png" loading="lazy" class="mb-2" :style="{ height: 64 * sizeMultiplier + 'px' }" />
|
||||||
<p class="text-center font-book text-error" :style="{ fontSize: titleFontSize + 'rem' }">Invalid Cover</p>
|
<p class="text-center text-error" :style="{ fontSize: titleFontSize + 'rem' }">Invalid Cover</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!hasCover" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full flex items-center justify-center z-10" :style="{ padding: placeholderCoverPadding + 'rem' }">
|
<div v-if="!hasCover" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full flex items-center justify-center z-10" :style="{ padding: placeholderCoverPadding + 'rem' }">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-center font-book" style="color: rgb(247 223 187)" :style="{ fontSize: titleFontSize + 'rem' }">{{ titleCleaned }}</p>
|
<p class="text-center" style="color: rgb(247 223 187)" :style="{ fontSize: titleFontSize + 'rem' }">{{ titleCleaned }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!hasCover" class="absolute left-0 right-0 w-full flex items-center justify-center z-10" :style="{ padding: placeholderCoverPadding + 'rem', bottom: authorBottom + 'rem' }">
|
<div v-if="!hasCover" class="absolute left-0 right-0 w-full flex items-center justify-center z-10" :style="{ padding: placeholderCoverPadding + 'rem', bottom: authorBottom + 'rem' }">
|
||||||
<p class="text-center font-book" style="color: rgb(247 223 187); opacity: 0.75" :style="{ fontSize: authorFontSize + 'rem' }">{{ authorCleaned }}</p>
|
<p class="text-center" style="color: rgb(247 223 187); opacity: 0.75" :style="{ fontSize: authorFontSize + 'rem' }">{{ authorCleaned }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
<div v-else class="relative w-full h-full flex items-center justify-center p-2 bg-primary rounded-sm">
|
<div v-else class="relative w-full h-full flex items-center justify-center p-2 bg-primary rounded-sm">
|
||||||
<div class="absolute top-0 left-0 w-full h-full bg-gray-400 bg-opacity-5" />
|
<div class="absolute top-0 left-0 w-full h-full bg-gray-400 bg-opacity-5" />
|
||||||
|
|
||||||
<p class="font-book text-white text-opacity-60 text-center" :style="{ fontSize: Math.min(1, sizeMultiplier) + 'rem' }">Empty Collection</p>
|
<p class="text-white text-opacity-60 text-center" :style="{ fontSize: Math.min(1, sizeMultiplier) + 'rem' }">Empty Collection</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ export default {
|
|||||||
|
|
||||||
var innerP = document.createElement('p')
|
var innerP = document.createElement('p')
|
||||||
innerP.textContent = this.name
|
innerP.textContent = this.name
|
||||||
innerP.className = 'text-sm font-book text-white'
|
innerP.className = 'text-sm text-white'
|
||||||
imgdiv.appendChild(innerP)
|
imgdiv.appendChild(innerP)
|
||||||
|
|
||||||
return imgdiv
|
return imgdiv
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<div v-if="imageFailed" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full bg-red-100" :style="{ padding: placeholderCoverPadding + 'rem' }">
|
<div v-if="imageFailed" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full bg-red-100" :style="{ padding: placeholderCoverPadding + 'rem' }">
|
||||||
<div class="w-full h-full border-2 border-error flex flex-col items-center justify-center">
|
<div class="w-full h-full border-2 border-error flex flex-col items-center justify-center">
|
||||||
<img src="/Logo.png" class="mb-2" :style="{ height: 64 * sizeMultiplier + 'px' }" />
|
<img src="/Logo.png" class="mb-2" :style="{ height: 64 * sizeMultiplier + 'px' }" />
|
||||||
<p class="text-center font-book text-error" :style="{ fontSize: sizeMultiplier + 'rem' }">Invalid Cover</p>
|
<p class="text-center text-error" :style="{ fontSize: sizeMultiplier + 'rem' }">Invalid Cover</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<modals-modal ref="modal" v-model="show" name="account" :width="800" :height="'unset'" :processing="processing">
|
<modals-modal ref="modal" v-model="show" name="account" :width="800" :height="'unset'" :processing="processing">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||||
<p class="font-book text-3xl text-white truncate">{{ title }}</p>
|
<p class="text-3xl text-white truncate">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<form @submit.prevent="submitForm">
|
<form @submit.prevent="submitForm">
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<modals-modal v-model="show" name="backup-scheduler" :width="700" :height="'unset'" :processing="processing">
|
<modals-modal v-model="show" name="backup-scheduler" :width="700" :height="'unset'" :processing="processing">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||||
<p class="font-book text-3xl text-white truncate">{{ $strings.HeaderSetBackupSchedule }}</p>
|
<p class="text-3xl text-white truncate">{{ $strings.HeaderSetBackupSchedule }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div v-if="show && newCronExpression" class="p-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden" style="min-height: 400px; max-height: 80vh">
|
<div v-if="show && newCronExpression" class="p-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden" style="min-height: 400px; max-height: 80vh">
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<modals-modal v-model="show" name="batchQuickMatch" :processing="processing" :width="500" :height="'unset'">
|
<modals-modal v-model="show" name="batchQuickMatch" :processing="processing" :width="500" :height="'unset'">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||||
<p class="font-book text-3xl text-white truncate">{{ title }}</p>
|
<p class="text-3xl text-white truncate">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<modals-modal v-model="show" name="bookmarks" :width="500" :height="'unset'">
|
<modals-modal v-model="show" name="bookmarks" :width="500" :height="'unset'">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||||
<p class="font-book text-3xl text-white truncate">{{ $strings.LabelYourBookmarks }}</p>
|
<p class="text-3xl text-white truncate">{{ $strings.LabelYourBookmarks }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div ref="container" class="w-full rounded-lg bg-bg box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
|
<div ref="container" class="w-full rounded-lg bg-bg box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
|
||||||
@@ -73,6 +73,12 @@ export default {
|
|||||||
},
|
},
|
||||||
canCreateBookmark() {
|
canCreateBookmark() {
|
||||||
return !this.bookmarks.find((bm) => bm.time === this.currentTime)
|
return !this.bookmarks.find((bm) => bm.time === this.currentTime)
|
||||||
|
},
|
||||||
|
dateFormat() {
|
||||||
|
return this.$store.state.serverSettings.dateFormat
|
||||||
|
},
|
||||||
|
timeFormat() {
|
||||||
|
return this.$store.state.serverSettings.timeFormat
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -111,7 +117,7 @@ export default {
|
|||||||
},
|
},
|
||||||
submitCreateBookmark() {
|
submitCreateBookmark() {
|
||||||
if (!this.newBookmarkTitle) {
|
if (!this.newBookmarkTitle) {
|
||||||
this.newBookmarkTitle = this.$formatDate(Date.now(), 'MMM dd, yyyy HH:mm')
|
this.newBookmarkTitle = this.$formatDatetime(Date.now(), this.dateFormat, this.timeFormat)
|
||||||
}
|
}
|
||||||
var bookmark = {
|
var bookmark = {
|
||||||
title: this.newBookmarkTitle,
|
title: this.newBookmarkTitle,
|
||||||
@@ -134,4 +140,4 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<modals-modal v-model="show" name="listening-session-modal" :processing="processing" :width="700" :height="'unset'">
|
<modals-modal v-model="show" name="listening-session-modal" :processing="processing" :width="700" :height="'unset'">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||||
<p class="font-book text-3xl text-white truncate">{{ $strings.HeaderSession }} {{ _session.id }}</p>
|
<p class="text-3xl text-white truncate">{{ $strings.HeaderSession }} {{ _session.id }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div ref="container" class="w-full rounded-lg bg-bg box-shadow-md overflow-y-auto overflow-x-hidden p-6" style="max-height: 80vh">
|
<div ref="container" class="w-full rounded-lg bg-bg box-shadow-md overflow-y-auto overflow-x-hidden p-6" style="max-height: 80vh">
|
||||||
@@ -19,13 +19,13 @@
|
|||||||
<div class="flex items-center -mx-1 mb-1">
|
<div class="flex items-center -mx-1 mb-1">
|
||||||
<div class="w-40 px-1 text-gray-200">{{ $strings.LabelStartedAt }}</div>
|
<div class="w-40 px-1 text-gray-200">{{ $strings.LabelStartedAt }}</div>
|
||||||
<div class="px-1">
|
<div class="px-1">
|
||||||
{{ $formatDate(_session.startedAt, 'MMMM do, yyyy HH:mm') }}
|
{{ $formatDatetime(_session.startedAt, dateFormat, timeFormat) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center -mx-1 mb-1">
|
<div class="flex items-center -mx-1 mb-1">
|
||||||
<div class="w-40 px-1 text-gray-200">{{ $strings.LabelUpdatedAt }}</div>
|
<div class="w-40 px-1 text-gray-200">{{ $strings.LabelUpdatedAt }}</div>
|
||||||
<div class="px-1">
|
<div class="px-1">
|
||||||
{{ $formatDate(_session.updatedAt, 'MMMM do, yyyy HH:mm') }}
|
{{ $formatDatetime(_session.updatedAt, dateFormat, timeFormat) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center -mx-1 mb-1">
|
<div class="flex items-center -mx-1 mb-1">
|
||||||
@@ -151,6 +151,12 @@ export default {
|
|||||||
else if (playMethod === this.$constants.PlayMethod.DIRECTSTREAM) return 'Direct Stream'
|
else if (playMethod === this.$constants.PlayMethod.DIRECTSTREAM) return 'Direct Stream'
|
||||||
else if (playMethod === this.$constants.PlayMethod.LOCAL) return 'Local'
|
else if (playMethod === this.$constants.PlayMethod.LOCAL) return 'Local'
|
||||||
return 'Unknown'
|
return 'Unknown'
|
||||||
|
},
|
||||||
|
dateFormat() {
|
||||||
|
return this.$store.state.serverSettings.dateFormat
|
||||||
|
},
|
||||||
|
timeFormat() {
|
||||||
|
return this.$store.state.serverSettings.timeFormat
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -186,4 +192,4 @@ export default {
|
|||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<modals-modal v-model="show" name="sleep-timer" :width="350" :height="'unset'">
|
<modals-modal v-model="show" name="sleep-timer" :width="350" :height="'unset'">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden pointer-events-none">
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden pointer-events-none">
|
||||||
<p class="font-book text-3xl text-white truncate pointer-events-none">{{ $strings.HeaderSleepTimer }}</p>
|
<p class="text-3xl text-white truncate pointer-events-none">{{ $strings.HeaderSleepTimer }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<modals-modal v-model="show" name="edit-author" :width="800" :height="'unset'" :processing="processing">
|
<modals-modal v-model="show" name="edit-author" :width="800" :height="'unset'" :processing="processing">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||||
<p class="font-book text-3xl text-white truncate">{{ title }}</p>
|
<p class="text-3xl text-white truncate">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="p-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden" style="min-height: 400px; max-height: 80vh">
|
<div class="p-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden" style="min-height: 400px; max-height: 80vh">
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<modals-modal v-model="show" name="changelog" :width="800" :height="'unset'">
|
<modals-modal v-model="show" name="changelog" :width="800" :height="'unset'">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||||
<p class="font-book text-3xl text-white truncate">Changelog</p>
|
<p class="text-3xl text-white truncate">Changelog</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="px-8 py-6 w-full rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-y-scroll" style="max-height: 80vh">
|
<div class="px-8 py-6 w-full rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-y-scroll" style="max-height: 80vh">
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<modals-modal v-model="show" name="collections" :processing="processing" :width="500" :height="'unset'">
|
<modals-modal v-model="show" name="collections" :processing="processing" :width="500" :height="'unset'">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden pointer-events-none">
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden pointer-events-none">
|
||||||
<p class="font-book text-3xl text-white truncate">{{ title }}</p>
|
<p class="text-3xl text-white truncate">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<modals-modal v-model="show" name="edit-collection" :width="700" :height="'unset'" :processing="processing">
|
<modals-modal v-model="show" name="edit-collection" :width="700" :height="'unset'" :processing="processing">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||||
<p class="font-book text-3xl text-white truncate">{{ $strings.HeaderCollection }}</p>
|
<p class="text-3xl text-white truncate">{{ $strings.HeaderCollection }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="p-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden" style="min-height: 400px; max-height: 80vh">
|
<div class="p-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden" style="min-height: 400px; max-height: 80vh">
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
<modals-modal v-model="show" name="edit-book" :width="800" :height="height" :processing="processing" :content-margin-top="marginTop">
|
<modals-modal v-model="show" name="edit-book" :width="800" :height="height" :processing="processing" :content-margin-top="marginTop">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-4 landscape:px-4 landscape:py-2 md:portrait:p-5 lg:p-5 w-2/3 overflow-hidden pointer-events-none">
|
<div class="absolute top-0 left-0 p-4 landscape:px-4 landscape:py-2 md:portrait:p-5 lg:p-5 w-2/3 overflow-hidden pointer-events-none">
|
||||||
<p class="font-book text-xl md:portrait:text-3xl md:landscape:text-lg lg:text-3xl text-white truncate pointer-events-none">{{ title }}</p>
|
<p class="text-xl md:portrait:text-3xl md:landscape:text-lg lg:text-3xl text-white truncate pointer-events-none">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="absolute -top-10 left-0 z-10 w-full flex">
|
<div class="absolute -top-10 left-0 z-10 w-full flex">
|
||||||
<template v-for="tab in availableTabs">
|
<template v-for="tab in availableTabs">
|
||||||
<div :key="tab.id" class="w-28 rounded-t-lg flex items-center justify-center mr-0.5 sm:mr-1 cursor-pointer hover:bg-bg font-book border-t border-l border-r border-black-300 tab text-xs sm:text-base" :class="selectedTab === tab.id ? 'tab-selected bg-bg pb-px' : 'bg-primary text-gray-400'" @click="selectTab(tab.id)">{{ tab.title }}</div>
|
<div :key="tab.id" class="w-28 rounded-t-lg flex items-center justify-center mr-0.5 sm:mr-1 cursor-pointer hover:bg-bg border-t border-l border-r border-black-300 tab text-xs sm:text-base" :class="selectedTab === tab.id ? 'tab-selected bg-bg pb-px' : 'bg-primary text-gray-400'" @click="selectTab(tab.id)">{{ tab.title }}</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
<div class="w-full h-full overflow-hidden overflow-y-auto px-2 sm:px-4 py-6 relative">
|
<div class="w-full h-full overflow-hidden overflow-y-auto px-2 sm:px-4 py-6 relative">
|
||||||
<div class="flex flex-wrap">
|
<div class="flex flex-wrap">
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<covers-book-cover :library-item="libraryItem" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
<covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId, null, true)" :width="120" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
|
|
||||||
<!-- book cover overlay -->
|
<!-- book cover overlay -->
|
||||||
<div v-if="media.coverPath" class="absolute top-0 left-0 w-full h-full z-10 opacity-0 hover:opacity-100 transition-opacity duration-100">
|
<div v-if="media.coverPath" class="absolute top-0 left-0 w-full h-full z-10 opacity-0 hover:opacity-100 transition-opacity duration-100">
|
||||||
<div class="absolute top-0 left-0 w-full h-16 bg-gradient-to-b from-black-600 to-transparent" />
|
<div class="absolute top-0 left-0 w-full h-16 bg-gradient-to-b from-black-600 to-transparent" />
|
||||||
@@ -27,14 +28,14 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="localCovers.length" class="mb-4 mt-6 border-t border-b border-primary">
|
<div v-if="localCovers.length" class="mb-4 mt-6 border-t border-b border-white border-opacity-10">
|
||||||
<div class="flex items-center justify-center py-2">
|
<div class="flex items-center justify-center py-2">
|
||||||
<p>{{ localCovers.length }} local image{{ localCovers.length !== 1 ? 's' : '' }}</p>
|
<p>{{ localCovers.length }} local image{{ localCovers.length !== 1 ? 's' : '' }}</p>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<ui-btn small @click="showLocalCovers = !showLocalCovers">{{ showLocalCovers ? $strings.ButtonHide : $strings.ButtonShow }}</ui-btn>
|
<ui-btn small @click="showLocalCovers = !showLocalCovers">{{ showLocalCovers ? $strings.ButtonHide : $strings.ButtonShow }}</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="showLocalCovers" class="flex items-center justify-center">
|
<div v-if="showLocalCovers" class="flex items-center justify-center pb-2">
|
||||||
<template v-for="cover in localCovers">
|
<template v-for="cover in localCovers">
|
||||||
<div :key="cover.path" class="m-0.5 mb-5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover.metadata.path === coverPath ? 'border-yellow-300' : ''" @click="setCover(cover)">
|
<div :key="cover.path" class="m-0.5 mb-5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover.metadata.path === coverPath ? 'border-yellow-300' : ''" @click="setCover(cover)">
|
||||||
<div class="h-24 bg-primary" :style="{ width: 96 / bookCoverAspectRatio + 'px' }">
|
<div class="h-24 bg-primary" :style="{ width: 96 / bookCoverAspectRatio + 'px' }">
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ export default {
|
|||||||
rescan() {
|
rescan() {
|
||||||
this.rescanning = true
|
this.rescanning = true
|
||||||
this.$axios
|
this.$axios
|
||||||
.$get(`/api/items/${this.libraryItemId}/scan`)
|
.$post(`/api/items/${this.libraryItemId}/scan`)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
this.rescanning = false
|
this.rescanning = false
|
||||||
var result = data.result
|
var result = data.result
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="!episodes.length" class="flex my-4 text-center justify-center text-xl">{{ $strings.MessageNoEpisodes }}</div>
|
<div v-if="!episodes.length" class="flex my-4 text-center justify-center text-xl">{{ $strings.MessageNoEpisodes }}</div>
|
||||||
<table v-else class="text-sm tracksTable">
|
<table v-else class="text-sm tracksTable">
|
||||||
<tr class="font-book">
|
<tr>
|
||||||
<th class="text-left">Sort #</th>
|
<th class="text-left">Sort #</th>
|
||||||
<th class="text-left whitespace-nowrap">{{ $strings.LabelEpisode }}</th>
|
<th class="text-left whitespace-nowrap">{{ $strings.LabelEpisode }}</th>
|
||||||
<th class="text-left">{{ $strings.EpisodeTitle }}</th>
|
<th class="text-left">{{ $strings.EpisodeTitle }}</th>
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
<td class="text-left">
|
<td class="text-left">
|
||||||
<p class="px-4">{{ episode.episode }}</p>
|
<p class="px-4">{{ episode.episode }}</p>
|
||||||
</td>
|
</td>
|
||||||
<td class="font-book">
|
<td>
|
||||||
{{ episode.title }}
|
{{ episode.title }}
|
||||||
</td>
|
</td>
|
||||||
<td class="font-mono text-center">
|
<td class="font-mono text-center">
|
||||||
|
|||||||
@@ -34,13 +34,25 @@
|
|||||||
</div>
|
</div>
|
||||||
<ui-checkbox v-model="selectAll" checkbox-bg="bg" @input="selectAllToggled" />
|
<ui-checkbox v-model="selectAll" checkbox-bg="bg" @input="selectAllToggled" />
|
||||||
<form @submit.prevent="submitMatchUpdate">
|
<form @submit.prevent="submitMatchUpdate">
|
||||||
<div v-if="selectedMatchOrig.cover" class="flex items-center py-2">
|
<div v-if="selectedMatchOrig.cover" class="flex flex-wrap md:flex-nowrap items-center justify-center">
|
||||||
<ui-checkbox v-model="selectedMatchUsage.cover" checkbox-bg="bg" @input="checkboxToggled" />
|
<div class="flex flex-grow items-center py-2">
|
||||||
<ui-text-input-with-label v-model="selectedMatch.cover" :disabled="!selectedMatchUsage.cover" readonly :label="$strings.LabelCover" class="flex-grow mx-4" />
|
<ui-checkbox v-model="selectedMatchUsage.cover" checkbox-bg="bg" @input="checkboxToggled" />
|
||||||
<div class="min-w-12 max-w-12 md:min-w-16 md:max-w-16">
|
<ui-text-input-with-label v-model="selectedMatch.cover" :disabled="!selectedMatchUsage.cover" readonly :label="$strings.LabelCover" class="flex-grow mx-4" />
|
||||||
<a :href="selectedMatch.cover" target="_blank" class="w-full bg-primary">
|
</div>
|
||||||
<img :src="selectedMatch.cover" class="h-full w-full object-contain" />
|
|
||||||
</a>
|
<div class="flex py-2">
|
||||||
|
<div>
|
||||||
|
<p class="text-center text-gray-200">New</p>
|
||||||
|
<a :href="selectedMatch.cover" target="_blank" class="bg-primary">
|
||||||
|
<covers-preview-cover :src="selectedMatch.cover" :width="100" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div v-if="media.coverPath">
|
||||||
|
<p class="text-center text-gray-200">Current</p>
|
||||||
|
<a :href="$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId, null, true)" target="_blank" class="bg-primary">
|
||||||
|
<covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId, null, true)" :width="100" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="selectedMatchOrig.title" class="flex items-center py-2">
|
<div v-if="selectedMatchOrig.title" class="flex items-center py-2">
|
||||||
@@ -164,6 +176,20 @@
|
|||||||
<p v-if="mediaMetadata.releaseDate" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.releaseDate || '' }}</p>
|
<p v-if="mediaMetadata.releaseDate" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.releaseDate || '' }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="selectedMatchOrig.explicit != null" class="flex items-center pb-2" :class="{ 'pt-2': mediaMetadata.explicit == null }">
|
||||||
|
<ui-checkbox v-model="selectedMatchUsage.explicit" checkbox-bg="bg" @input="checkboxToggled" />
|
||||||
|
<div class="flex-grow ml-4" :class="{ 'pt-4': mediaMetadata.explicit != null }">
|
||||||
|
<ui-checkbox v-model="selectedMatch.explicit" :label="$strings.LabelExplicit" :disabled="!selectedMatchUsage.explicit" :checkbox-bg="!selectedMatchUsage.explicit ? 'bg' : 'primary'" border-color="gray-600" label-class="pl-2 text-base font-semibold" />
|
||||||
|
<p v-if="mediaMetadata.explicit != null" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.explicit ? 'Explicit (checked)' : 'Not Explicit (unchecked)' }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="selectedMatchOrig.abridged != null" class="flex items-center pb-2" :class="{ 'pt-2': mediaMetadata.abridged == null }">
|
||||||
|
<ui-checkbox v-model="selectedMatchUsage.abridged" checkbox-bg="bg" @input="checkboxToggled" />
|
||||||
|
<div class="flex-grow ml-4" :class="{ 'pt-4': mediaMetadata.abridged != null }">
|
||||||
|
<ui-checkbox v-model="selectedMatch.abridged" :label="$strings.LabelAbridged" :disabled="!selectedMatchUsage.abridged" :checkbox-bg="!selectedMatchUsage.abridged ? 'bg' : 'primary'" border-color="gray-600" label-class="pl-2 text-base font-semibold" />
|
||||||
|
<p v-if="mediaMetadata.abridged != null" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.abridged ? 'Abridged (checked)' : 'Unabridged (unchecked)' }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center justify-end py-2">
|
<div class="flex items-center justify-end py-2">
|
||||||
<ui-btn color="success" type="submit">{{ $strings.ButtonSubmit }}</ui-btn>
|
<ui-btn color="success" type="submit">{{ $strings.ButtonSubmit }}</ui-btn>
|
||||||
@@ -209,6 +235,7 @@ export default {
|
|||||||
explicit: true,
|
explicit: true,
|
||||||
asin: true,
|
asin: true,
|
||||||
isbn: true,
|
isbn: true,
|
||||||
|
abridged: true,
|
||||||
// Podcast specific
|
// Podcast specific
|
||||||
itunesPageUrl: true,
|
itunesPageUrl: true,
|
||||||
itunesId: true,
|
itunesId: true,
|
||||||
@@ -327,6 +354,7 @@ export default {
|
|||||||
res.itunesPageUrl = res.pageUrl || null
|
res.itunesPageUrl = res.pageUrl || null
|
||||||
res.itunesId = res.id || null
|
res.itunesId = res.id || null
|
||||||
res.author = res.artistName || null
|
res.author = res.artistName || null
|
||||||
|
res.explicit = res.explicit || false
|
||||||
return res
|
return res
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -352,6 +380,7 @@ export default {
|
|||||||
explicit: true,
|
explicit: true,
|
||||||
asin: true,
|
asin: true,
|
||||||
isbn: true,
|
isbn: true,
|
||||||
|
abridged: true,
|
||||||
// Podcast specific
|
// Podcast specific
|
||||||
itunesPageUrl: true,
|
itunesPageUrl: true,
|
||||||
itunesId: true,
|
itunesId: true,
|
||||||
@@ -468,7 +497,6 @@ export default {
|
|||||||
} else if (key === 'narrator') {
|
} else if (key === 'narrator') {
|
||||||
updatePayload.metadata.narrators = this.selectedMatch[key].split(',').map((v) => v.trim())
|
updatePayload.metadata.narrators = this.selectedMatch[key].split(',').map((v) => v.trim())
|
||||||
} else if (key === 'genres') {
|
} else if (key === 'genres') {
|
||||||
// updatePayload.metadata.genres = this.selectedMatch[key].split(',').map((v) => v.trim())
|
|
||||||
updatePayload.metadata.genres = [...this.selectedMatch[key]]
|
updatePayload.metadata.genres = [...this.selectedMatch[key]]
|
||||||
} else if (key === 'tags') {
|
} else if (key === 'tags') {
|
||||||
updatePayload.tags = this.selectedMatch[key].split(',').map((v) => v.trim())
|
updatePayload.tags = this.selectedMatch[key].split(',').map((v) => v.trim())
|
||||||
|
|||||||
@@ -59,6 +59,14 @@ export default {
|
|||||||
newMaxNewEpisodesToDownload: 0
|
newMaxNewEpisodesToDownload: 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
libraryItem: {
|
||||||
|
immediate: true,
|
||||||
|
handler(newVal) {
|
||||||
|
if (newVal) this.init()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
isProcessing: {
|
isProcessing: {
|
||||||
get() {
|
get() {
|
||||||
@@ -176,4 +184,4 @@ export default {
|
|||||||
height: calc(100% - 80px);
|
height: calc(100% - 80px);
|
||||||
max-height: calc(100% - 80px);
|
max-height: calc(100% - 80px);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -46,8 +46,20 @@
|
|||||||
>{{ $strings.ButtonOpenManager }}
|
>{{ $strings.ButtonOpenManager }}
|
||||||
<span class="material-icons text-lg ml-2">launch</span>
|
<span class="material-icons text-lg ml-2">launch</span>
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
|
|
||||||
|
<ui-btn v-if="!isMetadataEmbedQueued && !isEmbedTaskRunning" class="w-full mt-4" small @click.stop="quickEmbed">Quick Embed</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- queued alert -->
|
||||||
|
<widgets-alert v-if="isMetadataEmbedQueued" type="warning" class="mt-4">
|
||||||
|
<p class="text-lg">Queued for metadata embed ({{ queuedEmbedLIds.length }} in queue)</p>
|
||||||
|
</widgets-alert>
|
||||||
|
|
||||||
|
<!-- processing alert -->
|
||||||
|
<widgets-alert v-if="isEmbedTaskRunning" type="warning" class="mt-4">
|
||||||
|
<p class="text-lg">Currently embedding metadata</p>
|
||||||
|
</widgets-alert>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p v-if="!mediaTracks.length" class="text-lg text-center my-8">{{ $strings.MessageNoAudioTracks }}</p>
|
<p v-if="!mediaTracks.length" class="text-lg text-center my-8">{{ $strings.MessageNoAudioTracks }}</p>
|
||||||
@@ -71,10 +83,10 @@ export default {
|
|||||||
return this.$store.state.showExperimentalFeatures
|
return this.$store.state.showExperimentalFeatures
|
||||||
},
|
},
|
||||||
libraryItemId() {
|
libraryItemId() {
|
||||||
return this.libraryItem ? this.libraryItem.id : null
|
return this.libraryItem?.id || null
|
||||||
},
|
},
|
||||||
media() {
|
media() {
|
||||||
return this.libraryItem ? this.libraryItem.media || {} : {}
|
return this.libraryItem?.media || {}
|
||||||
},
|
},
|
||||||
mediaTracks() {
|
mediaTracks() {
|
||||||
return this.media.tracks || []
|
return this.media.tracks || []
|
||||||
@@ -92,9 +104,49 @@ export default {
|
|||||||
showMp3Split() {
|
showMp3Split() {
|
||||||
if (!this.mediaTracks.length) return false
|
if (!this.mediaTracks.length) return false
|
||||||
return this.isSingleM4b && this.chapters.length
|
return this.isSingleM4b && this.chapters.length
|
||||||
|
},
|
||||||
|
queuedEmbedLIds() {
|
||||||
|
return this.$store.state.tasks.queuedEmbedLIds || []
|
||||||
|
},
|
||||||
|
isMetadataEmbedQueued() {
|
||||||
|
return this.queuedEmbedLIds.some((lid) => lid === this.libraryItemId)
|
||||||
|
},
|
||||||
|
tasks() {
|
||||||
|
return this.$store.getters['tasks/getTasksByLibraryItemId'](this.libraryItemId)
|
||||||
|
},
|
||||||
|
embedTask() {
|
||||||
|
return this.tasks.find((t) => t.action === 'embed-metadata')
|
||||||
|
},
|
||||||
|
encodeTask() {
|
||||||
|
return this.tasks.find((t) => t.action === 'encode-m4b')
|
||||||
|
},
|
||||||
|
isEmbedTaskRunning() {
|
||||||
|
return this.embedTask && !this.embedTask?.isFinished
|
||||||
|
},
|
||||||
|
isEncodeTaskRunning() {
|
||||||
|
return this.encodeTask && !this.encodeTask?.isFinished
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {},
|
methods: {
|
||||||
mounted() {}
|
quickEmbed() {
|
||||||
|
const payload = {
|
||||||
|
message: 'Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files. <br><br>Would you like to continue?',
|
||||||
|
callback: (confirmed) => {
|
||||||
|
if (confirmed) {
|
||||||
|
this.$axios
|
||||||
|
.$post(`/api/tools/item/${this.libraryItemId}/embed-metadata`)
|
||||||
|
.then(() => {
|
||||||
|
console.log('Audio metadata encode started')
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Audio metadata encode failed', error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'yesNo'
|
||||||
|
}
|
||||||
|
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -2,12 +2,12 @@
|
|||||||
<modals-modal v-model="show" name="edit-library" :width="700" :height="'unset'" :processing="processing">
|
<modals-modal v-model="show" name="edit-library" :width="700" :height="'unset'" :processing="processing">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||||
<p class="font-book text-xl md:text-3xl text-white truncate">{{ title }}</p>
|
<p class="text-xl md:text-3xl text-white truncate">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="absolute -top-10 left-0 z-10 w-full flex">
|
<div class="absolute -top-10 left-0 z-10 w-full flex">
|
||||||
<template v-for="tab in tabs">
|
<template v-for="tab in tabs">
|
||||||
<div :key="tab.id" class="w-28 rounded-t-lg flex items-center justify-center mr-1 cursor-pointer hover:bg-bg font-book border-t border-l border-r border-black-300 tab text-xs sm:text-base" :class="selectedTab === tab.id ? 'tab-selected bg-bg pb-px' : 'bg-primary text-gray-400'" @click="selectTab(tab.id)">{{ tab.title }}</div>
|
<div :key="tab.id" class="w-28 rounded-t-lg flex items-center justify-center mr-1 cursor-pointer hover:bg-bg border-t border-l border-r border-black-300 tab text-xs sm:text-base" :class="selectedTab === tab.id ? 'tab-selected bg-bg pb-px' : 'bg-primary text-gray-400'" @click="selectTab(tab.id)">{{ tab.title }}</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<modals-modal ref="modal" v-model="show" name="notification-edit" :width="800" :height="'unset'" :processing="processing">
|
<modals-modal ref="modal" v-model="show" name="notification-edit" :width="800" :height="'unset'" :processing="processing">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||||
<p class="font-book text-3xl text-white truncate">{{ title }}</p>
|
<p class="text-3xl text-white truncate">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<form @submit.prevent="submitForm">
|
<form @submit.prevent="submitForm">
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<modals-modal v-model="show" name="queue-items" :width="800" :height="'unset'">
|
<modals-modal v-model="show" name="queue-items" :width="800" :height="'unset'">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||||
<p class="font-book text-3xl text-white truncate">{{ $strings.HeaderPlayerQueue }}</p>
|
<p class="text-3xl text-white truncate">{{ $strings.HeaderPlayerQueue }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div ref="container" class="w-full rounded-lg bg-bg box-shadow-md overflow-y-auto overflow-x-hidden py-4" style="max-height: 80vh">
|
<div ref="container" class="w-full rounded-lg bg-bg box-shadow-md overflow-y-auto overflow-x-hidden py-4" style="max-height: 80vh">
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<modals-modal v-model="show" name="playlists" :processing="processing" :width="500" :height="'unset'">
|
<modals-modal v-model="show" name="playlists" :processing="processing" :width="500" :height="'unset'">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden pointer-events-none">
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden pointer-events-none">
|
||||||
<p class="font-book text-3xl text-white truncate">{{ title }}</p>
|
<p class="text-3xl text-white truncate">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<modals-modal v-model="show" name="edit-playlist" :width="700" :height="'unset'" :processing="processing">
|
<modals-modal v-model="show" name="edit-playlist" :width="700" :height="'unset'" :processing="processing">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||||
<p class="font-book text-3xl text-white truncate">{{ $strings.HeaderPlaylist }}</p>
|
<p class="text-3xl text-white truncate">{{ $strings.HeaderPlaylist }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="p-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden" style="min-height: 400px; max-height: 80vh">
|
<div class="p-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden" style="min-height: 400px; max-height: 80vh">
|
||||||
|
|||||||
@@ -2,17 +2,24 @@
|
|||||||
<modals-modal v-model="show" name="podcast-episode-edit-modal" :width="800" :height="'unset'" :processing="processing">
|
<modals-modal v-model="show" name="podcast-episode-edit-modal" :width="800" :height="'unset'" :processing="processing">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||||
<p class="font-book text-3xl text-white truncate">{{ title }}</p>
|
<p class="text-3xl text-white truncate">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="absolute -top-10 left-0 z-10 w-full flex">
|
<div class="absolute -top-10 left-0 z-10 w-full flex">
|
||||||
<template v-for="tab in tabs">
|
<template v-for="tab in tabs">
|
||||||
<div :key="tab.id" class="w-28 rounded-t-lg flex items-center justify-center mr-0.5 sm:mr-1 cursor-pointer hover:bg-bg font-book border-t border-l border-r border-black-300 tab text-xs sm:text-base" :class="selectedTab === tab.id ? 'tab-selected bg-bg pb-px' : 'bg-primary text-gray-400'" @click="selectTab(tab.id)">{{ tab.title }}</div>
|
<div :key="tab.id" class="w-28 rounded-t-lg flex items-center justify-center mr-0.5 sm:mr-1 cursor-pointer hover:bg-bg border-t border-l border-r border-black-300 tab text-xs sm:text-base" :class="selectedTab === tab.id ? 'tab-selected bg-bg pb-px' : 'bg-primary text-gray-400'" @click="selectTab(tab.id)">{{ tab.title }}</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-show="canGoPrev" class="absolute -left-24 top-0 bottom-0 h-full pointer-events-none flex items-center px-6">
|
||||||
|
<div class="material-icons text-5xl text-white text-opacity-50 hover:text-opacity-90 cursor-pointer pointer-events-auto" @click.stop.prevent="goPrevEpisode" @mousedown.prevent>arrow_back_ios</div>
|
||||||
|
</div>
|
||||||
|
<div v-show="canGoNext" class="absolute -right-24 top-0 bottom-0 h-full pointer-events-none flex items-center px-6">
|
||||||
|
<div class="material-icons text-5xl text-white text-opacity-50 hover:text-opacity-90 cursor-pointer pointer-events-auto" @click.stop.prevent="goNextEpisode" @mousedown.prevent>arrow_forward_ios</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div ref="wrapper" class="p-4 w-full text-sm rounded-b-lg rounded-tr-lg bg-bg shadow-lg border border-black-300 relative overflow-y-auto" style="max-height: 80vh">
|
<div ref="wrapper" class="p-4 w-full text-sm rounded-b-lg rounded-tr-lg bg-bg shadow-lg border border-black-300 relative overflow-y-auto" style="max-height: 80vh">
|
||||||
<component v-if="libraryItem && show" :is="tabComponentName" :library-item="libraryItem" :episode="episode" :processing.sync="processing" @close="show = false" @selectTab="selectTab" />
|
<component v-if="libraryItem && show" :is="tabComponentName" :library-item="libraryItem" :episode="episodeItem" :processing.sync="processing" @close="show = false" @selectTab="selectTab" />
|
||||||
</div>
|
</div>
|
||||||
</modals-modal>
|
</modals-modal>
|
||||||
</template>
|
</template>
|
||||||
@@ -21,8 +28,8 @@
|
|||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
episodeItem: null,
|
||||||
processing: false,
|
processing: false,
|
||||||
selectedTab: 'details',
|
|
||||||
tabs: [
|
tabs: [
|
||||||
{
|
{
|
||||||
id: 'details',
|
id: 'details',
|
||||||
@@ -37,6 +44,29 @@ export default {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
show: {
|
||||||
|
handler(newVal) {
|
||||||
|
if (newVal) {
|
||||||
|
const availableTabIds = this.tabs.map((tab) => tab.id)
|
||||||
|
if (!availableTabIds.length) {
|
||||||
|
this.show = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!availableTabIds.includes(this.selectedTab)) {
|
||||||
|
this.selectedTab = availableTabIds[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
this.episodeItem = null
|
||||||
|
this.init()
|
||||||
|
this.registerListeners()
|
||||||
|
} else {
|
||||||
|
this.unregisterListeners()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
show: {
|
show: {
|
||||||
get() {
|
get() {
|
||||||
@@ -46,27 +76,118 @@ export default {
|
|||||||
this.$store.commit('globals/setShowEditPodcastEpisodeModal', val)
|
this.$store.commit('globals/setShowEditPodcastEpisodeModal', val)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
selectedTab: {
|
||||||
|
get() {
|
||||||
|
return this.$store.state.editPodcastModalTab
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$store.commit('setEditPodcastModalTab', val)
|
||||||
|
}
|
||||||
|
},
|
||||||
libraryItem() {
|
libraryItem() {
|
||||||
return this.$store.state.selectedLibraryItem
|
return this.$store.state.selectedLibraryItem
|
||||||
},
|
},
|
||||||
episode() {
|
episode() {
|
||||||
return this.$store.state.globals.selectedEpisode
|
return this.$store.state.globals.selectedEpisode
|
||||||
},
|
},
|
||||||
|
selectedEpisodeId() {
|
||||||
|
return this.episode.id
|
||||||
|
},
|
||||||
title() {
|
title() {
|
||||||
if (!this.libraryItem) return ''
|
return this.libraryItem?.media.metadata.title || 'Unknown'
|
||||||
return this.libraryItem.media.metadata.title || 'Unknown'
|
|
||||||
},
|
},
|
||||||
tabComponentName() {
|
tabComponentName() {
|
||||||
var _tab = this.tabs.find((t) => t.id === this.selectedTab)
|
const _tab = this.tabs.find((t) => t.id === this.selectedTab)
|
||||||
return _tab ? _tab.component : ''
|
return _tab ? _tab.component : ''
|
||||||
|
},
|
||||||
|
episodeTableEpisodeIds() {
|
||||||
|
return this.$store.state.episodeTableEpisodeIds || []
|
||||||
|
},
|
||||||
|
currentEpisodeIndex() {
|
||||||
|
if (!this.episodeTableEpisodeIds.length) return 0
|
||||||
|
return this.episodeTableEpisodeIds.findIndex((bid) => bid === this.selectedEpisodeId)
|
||||||
|
},
|
||||||
|
canGoPrev() {
|
||||||
|
return this.episodeTableEpisodeIds.length && this.currentEpisodeIndex > 0
|
||||||
|
},
|
||||||
|
canGoNext() {
|
||||||
|
return this.episodeTableEpisodeIds.length && this.currentEpisodeIndex < this.episodeTableEpisodeIds.length - 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
async goPrevEpisode() {
|
||||||
|
if (this.currentEpisodeIndex - 1 < 0) return
|
||||||
|
const prevEpisodeId = this.episodeTableEpisodeIds[this.currentEpisodeIndex - 1]
|
||||||
|
this.processing = true
|
||||||
|
const prevEpisode = await this.$axios.$get(`/api/podcasts/${this.libraryItem.id}/episode/${prevEpisodeId}`).catch((error) => {
|
||||||
|
const errorMsg = error.response && error.response.data ? error.response.data : 'Failed to fetch episode'
|
||||||
|
this.$toast.error(errorMsg)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
this.processing = false
|
||||||
|
if (prevEpisode) {
|
||||||
|
this.episodeItem = prevEpisode
|
||||||
|
this.$store.commit('globals/setSelectedEpisode', prevEpisode)
|
||||||
|
} else {
|
||||||
|
console.error('Episode not found', prevEpisodeId)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async goNextEpisode() {
|
||||||
|
if (this.currentEpisodeIndex >= this.episodeTableEpisodeIds.length - 1) return
|
||||||
|
this.processing = true
|
||||||
|
const nextEpisodeId = this.episodeTableEpisodeIds[this.currentEpisodeIndex + 1]
|
||||||
|
const nextEpisode = await this.$axios.$get(`/api/podcasts/${this.libraryItem.id}/episode/${nextEpisodeId}`).catch((error) => {
|
||||||
|
const errorMsg = error.response && error.response.data ? error.response.data : 'Failed to fetch book'
|
||||||
|
this.$toast.error(errorMsg)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
this.processing = false
|
||||||
|
if (nextEpisode) {
|
||||||
|
this.episodeItem = nextEpisode
|
||||||
|
this.$store.commit('globals/setSelectedEpisode', nextEpisode)
|
||||||
|
} else {
|
||||||
|
console.error('Episode not found', nextEpisodeId)
|
||||||
|
}
|
||||||
|
},
|
||||||
selectTab(tab) {
|
selectTab(tab) {
|
||||||
this.selectedTab = tab
|
if (this.selectedTab === tab) return
|
||||||
|
if (this.tabs.find((t) => t.id === tab)) {
|
||||||
|
this.selectedTab = tab
|
||||||
|
this.processing = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
init() {
|
||||||
|
this.fetchFull()
|
||||||
|
},
|
||||||
|
async fetchFull() {
|
||||||
|
try {
|
||||||
|
this.processing = true
|
||||||
|
this.episodeItem = await this.$axios.$get(`/api/podcasts/${this.libraryItem.id}/episode/${this.selectedEpisodeId}`)
|
||||||
|
this.processing = false
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch episode', this.selectedEpisodeId, error)
|
||||||
|
this.processing = false
|
||||||
|
this.show = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hotkey(action) {
|
||||||
|
if (action === this.$hotkeys.Modal.NEXT_PAGE) {
|
||||||
|
this.goNextEpisode()
|
||||||
|
} else if (action === this.$hotkeys.Modal.PREV_PAGE) {
|
||||||
|
this.goPrevEpisode()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
registerListeners() {
|
||||||
|
this.$eventBus.$on('modal-hotkey', this.hotkey)
|
||||||
|
},
|
||||||
|
unregisterListeners() {
|
||||||
|
this.$eventBus.$off('modal-hotkey', this.hotkey)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.unregisterListeners()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -77,4 +198,4 @@ export default {
|
|||||||
.tab.tab-selected {
|
.tab.tab-selected {
|
||||||
height: 41px;
|
height: 41px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,25 +2,37 @@
|
|||||||
<modals-modal v-model="show" name="podcast-episodes-modal" :width="1200" :height="'unset'" :processing="processing">
|
<modals-modal v-model="show" name="podcast-episodes-modal" :width="1200" :height="'unset'" :processing="processing">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||||
<p class="font-book text-3xl text-white truncate">{{ title }}</p>
|
<p class="text-3xl text-white truncate">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div ref="wrapper" id="podcast-wrapper" class="p-4 w-full text-sm py-2 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden">
|
<div ref="wrapper" id="podcast-wrapper" class="p-4 w-full text-sm py-2 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden">
|
||||||
|
<div v-if="episodes.length" class="w-full py-3 mx-auto flex">
|
||||||
|
<form @submit.prevent="submit" class="flex flex-grow">
|
||||||
|
<ui-text-input v-model="search" @input="inputUpdate" type="search" :placeholder="$strings.PlaceholderSearchEpisode" class="flex-grow mr-2 text-sm md:text-base" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
<div ref="episodeContainer" id="episodes-scroll" class="w-full overflow-x-hidden overflow-y-auto">
|
<div ref="episodeContainer" id="episodes-scroll" class="w-full overflow-x-hidden overflow-y-auto">
|
||||||
<div
|
<div
|
||||||
v-for="(episode, index) in episodes"
|
v-for="(episode, index) in episodesList"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="relative"
|
class="relative"
|
||||||
:class="itemEpisodeMap[episode.enclosure.url] ? 'bg-primary bg-opacity-40' : selectedEpisodes[String(index)] ? 'cursor-pointer bg-success bg-opacity-10' : index % 2 == 0 ? 'cursor-pointer bg-primary bg-opacity-25 hover:bg-opacity-40' : 'cursor-pointer bg-primary bg-opacity-5 hover:bg-opacity-25'"
|
:class="itemEpisodeMap[episode.enclosure.url?.split('?')[0]] ? 'bg-primary bg-opacity-40' : selectedEpisodes[String(index)] ? 'cursor-pointer bg-success bg-opacity-10' : index % 2 == 0 ? 'cursor-pointer bg-primary bg-opacity-25 hover:bg-opacity-40' : 'cursor-pointer bg-primary bg-opacity-5 hover:bg-opacity-25'"
|
||||||
@click="toggleSelectEpisode(index, episode)"
|
@click="toggleSelectEpisode(index, episode)"
|
||||||
>
|
>
|
||||||
<div class="absolute top-0 left-0 h-full flex items-center p-2">
|
<div class="absolute top-0 left-0 h-full flex items-center p-2">
|
||||||
<span v-if="itemEpisodeMap[episode.enclosure.url]" class="material-icons text-success text-xl">download_done</span>
|
<span v-if="itemEpisodeMap[episode.enclosure.url?.split('?')[0]]" class="material-icons text-success text-xl">download_done</span>
|
||||||
<ui-checkbox v-else v-model="selectedEpisodes[String(index)]" small checkbox-bg="primary" border-color="gray-600" />
|
<ui-checkbox v-else v-model="selectedEpisodes[String(index)]" small checkbox-bg="primary" border-color="gray-600" />
|
||||||
</div>
|
</div>
|
||||||
<div class="px-8 py-2">
|
<div class="px-8 py-2">
|
||||||
<p v-if="episode.episode" class="font-semibold text-gray-200">#{{ episode.episode }}</p>
|
<div class="flex items-center font-semibold text-gray-200">
|
||||||
<p class="break-words mb-1">{{ episode.title }}</p>
|
<div v-if="episode.season || episode.episode">#</div>
|
||||||
|
<div v-if="episode.season">{{ episode.season }}x</div>
|
||||||
|
<div v-if="episode.episode">{{ episode.episode }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center mb-1">
|
||||||
|
<div class="break-words">{{ episode.title }}</div>
|
||||||
|
<widgets-podcast-type-indicator :type="episode.episodeType" />
|
||||||
|
</div>
|
||||||
<p v-if="episode.subtitle" class="break-words mb-1 text-sm text-gray-300 episode-subtitle">{{ episode.subtitle }}</p>
|
<p v-if="episode.subtitle" class="break-words mb-1 text-sm text-gray-300 episode-subtitle">{{ episode.subtitle }}</p>
|
||||||
<p class="text-xs text-gray-300">Published {{ episode.publishedAt ? $dateDistanceFromNow(episode.publishedAt) : 'Unknown' }}</p>
|
<p class="text-xs text-gray-300">Published {{ episode.publishedAt ? $dateDistanceFromNow(episode.publishedAt) : 'Unknown' }}</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -52,7 +64,10 @@ export default {
|
|||||||
return {
|
return {
|
||||||
processing: false,
|
processing: false,
|
||||||
selectedEpisodes: {},
|
selectedEpisodes: {},
|
||||||
selectAll: false
|
selectAll: false,
|
||||||
|
search: null,
|
||||||
|
searchTimeout: null,
|
||||||
|
searchText: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -77,7 +92,7 @@ export default {
|
|||||||
return this.libraryItem.media.metadata.title || 'Unknown'
|
return this.libraryItem.media.metadata.title || 'Unknown'
|
||||||
},
|
},
|
||||||
allDownloaded() {
|
allDownloaded() {
|
||||||
return !this.episodes.some((episode) => !this.itemEpisodeMap[episode.enclosure.url])
|
return !this.episodes.some((episode) => !this.itemEpisodeMap[episode.enclosure.url?.split('?')[0]])
|
||||||
},
|
},
|
||||||
episodesSelected() {
|
episodesSelected() {
|
||||||
return Object.keys(this.selectedEpisodes).filter((key) => !!this.selectedEpisodes[key])
|
return Object.keys(this.selectedEpisodes).filter((key) => !!this.selectedEpisodes[key])
|
||||||
@@ -93,23 +108,39 @@ export default {
|
|||||||
itemEpisodeMap() {
|
itemEpisodeMap() {
|
||||||
var map = {}
|
var map = {}
|
||||||
this.itemEpisodes.forEach((item) => {
|
this.itemEpisodes.forEach((item) => {
|
||||||
if (item.enclosure) map[item.enclosure.url] = true
|
if (item.enclosure) map[item.enclosure.url.split('?')[0]] = true
|
||||||
})
|
})
|
||||||
return map
|
return map
|
||||||
|
},
|
||||||
|
episodesList() {
|
||||||
|
return this.episodes.filter((episode) => {
|
||||||
|
if (!this.searchText) return true
|
||||||
|
return (episode.title && episode.title.toLowerCase().includes(this.searchText)) || (episode.subtitle && episode.subtitle.toLowerCase().includes(this.searchText))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
inputUpdate() {
|
||||||
|
clearTimeout(this.searchTimeout)
|
||||||
|
this.searchTimeout = setTimeout(() => {
|
||||||
|
if (!this.search || !this.search.trim()) {
|
||||||
|
this.searchText = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.searchText = this.search.toLowerCase().trim()
|
||||||
|
}, 500)
|
||||||
|
},
|
||||||
toggleSelectAll(val) {
|
toggleSelectAll(val) {
|
||||||
for (let i = 0; i < this.episodes.length; i++) {
|
for (let i = 0; i < this.episodes.length; i++) {
|
||||||
const episode = this.episodes[i]
|
const episode = this.episodes[i]
|
||||||
if (this.itemEpisodeMap[episode.enclosure.url]) this.selectedEpisodes[String(i)] = false
|
if (this.itemEpisodeMap[episode.enclosure.url?.split('?')[0]]) this.selectedEpisodes[String(i)] = false
|
||||||
else this.$set(this.selectedEpisodes, String(i), val)
|
else this.$set(this.selectedEpisodes, String(i), val)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
checkSetIsSelectedAll() {
|
checkSetIsSelectedAll() {
|
||||||
for (let i = 0; i < this.episodes.length; i++) {
|
for (let i = 0; i < this.episodes.length; i++) {
|
||||||
const episode = this.episodes[i]
|
const episode = this.episodes[i]
|
||||||
if (!this.itemEpisodeMap[episode.enclosure.url] && !this.selectedEpisodes[String(i)]) {
|
if (!this.itemEpisodeMap[episode.enclosure.url?.split('?')[0]] && !this.selectedEpisodes[String(i)]) {
|
||||||
this.selectAll = false
|
this.selectAll = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -117,7 +148,7 @@ export default {
|
|||||||
this.selectAll = true
|
this.selectAll = true
|
||||||
},
|
},
|
||||||
toggleSelectEpisode(index, episode) {
|
toggleSelectEpisode(index, episode) {
|
||||||
if (this.itemEpisodeMap[episode.enclosure.url]) return
|
if (this.itemEpisodeMap[episode.enclosure.url?.split('?')[0]]) return
|
||||||
this.$set(this.selectedEpisodes, String(index), !this.selectedEpisodes[String(index)])
|
this.$set(this.selectedEpisodes, String(index), !this.selectedEpisodes[String(index)])
|
||||||
this.checkSetIsSelectedAll()
|
this.checkSetIsSelectedAll()
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<modals-modal v-model="show" name="new-podcast-modal" :width="1000" :height="'unset'" :processing="processing">
|
<modals-modal v-model="show" name="new-podcast-modal" :width="1000" :height="'unset'" :processing="processing">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-5 w-3/4 overflow-hidden">
|
<div class="absolute top-0 left-0 p-5 w-3/4 overflow-hidden">
|
||||||
<p class="font-book text-xl md:text-3xl text-white truncate">{{ title }}</p>
|
<p class="text-xl md:text-3xl text-white truncate">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div ref="wrapper" id="podcast-wrapper" class="p-2 md:p-8 w-full text-sm py-2 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-x-hidden overflow-y-auto" style="max-height: 80vh">
|
<div ref="wrapper" id="podcast-wrapper" class="p-2 md:p-8 w-full text-sm py-2 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-x-hidden overflow-y-auto" style="max-height: 80vh">
|
||||||
@@ -28,6 +28,17 @@
|
|||||||
<ui-multi-select v-model="podcast.genres" :items="podcast.genres" :label="$strings.LabelGenres" />
|
<ui-multi-select v-model="podcast.genres" :items="podcast.genres" :label="$strings.LabelGenres" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex flex-wrap">
|
||||||
|
<div class="md:w-1/4 p-2">
|
||||||
|
<ui-dropdown :label="$strings.LabelPodcastType" v-model="podcast.type" :items="podcastTypes" small />
|
||||||
|
</div>
|
||||||
|
<div class="md:w-1/4 p-2">
|
||||||
|
<ui-text-input-with-label v-model="podcast.language" :label="$strings.LabelLanguage" />
|
||||||
|
</div>
|
||||||
|
<div class="md:w-1/4 px-2 pt-7">
|
||||||
|
<ui-checkbox v-model="podcast.explicit" :label="$strings.LabelExplicit" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="p-2 w-full">
|
<div class="p-2 w-full">
|
||||||
<ui-textarea-with-label v-model="podcast.description" :label="$strings.LabelDescription" :rows="3" />
|
<ui-textarea-with-label v-model="podcast.description" :label="$strings.LabelDescription" :rows="3" />
|
||||||
</div>
|
</div>
|
||||||
@@ -82,7 +93,10 @@ export default {
|
|||||||
itunesPageUrl: '',
|
itunesPageUrl: '',
|
||||||
itunesId: '',
|
itunesId: '',
|
||||||
itunesArtistId: '',
|
itunesArtistId: '',
|
||||||
autoDownloadEpisodes: false
|
autoDownloadEpisodes: false,
|
||||||
|
language: '',
|
||||||
|
explicit: false,
|
||||||
|
type: ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -140,6 +154,9 @@ export default {
|
|||||||
selectedFolderPath() {
|
selectedFolderPath() {
|
||||||
if (!this.selectedFolder) return ''
|
if (!this.selectedFolder) return ''
|
||||||
return this.selectedFolder.fullPath
|
return this.selectedFolder.fullPath
|
||||||
|
},
|
||||||
|
podcastTypes() {
|
||||||
|
return this.$store.state.globals.podcastTypes || []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -170,7 +187,9 @@ export default {
|
|||||||
itunesPageUrl: this.podcast.itunesPageUrl,
|
itunesPageUrl: this.podcast.itunesPageUrl,
|
||||||
itunesId: this.podcast.itunesId,
|
itunesId: this.podcast.itunesId,
|
||||||
itunesArtistId: this.podcast.itunesArtistId,
|
itunesArtistId: this.podcast.itunesArtistId,
|
||||||
language: this.podcast.language
|
language: this.podcast.language,
|
||||||
|
explicit: this.podcast.explicit,
|
||||||
|
type: this.podcast.type
|
||||||
},
|
},
|
||||||
autoDownloadEpisodes: this.podcast.autoDownloadEpisodes
|
autoDownloadEpisodes: this.podcast.autoDownloadEpisodes
|
||||||
}
|
}
|
||||||
@@ -205,9 +224,11 @@ export default {
|
|||||||
this.podcast.itunesPageUrl = this._podcastData.pageUrl || ''
|
this.podcast.itunesPageUrl = this._podcastData.pageUrl || ''
|
||||||
this.podcast.itunesId = this._podcastData.id || ''
|
this.podcast.itunesId = this._podcastData.id || ''
|
||||||
this.podcast.itunesArtistId = this._podcastData.artistId || ''
|
this.podcast.itunesArtistId = this._podcastData.artistId || ''
|
||||||
this.podcast.language = this._podcastData.language || ''
|
this.podcast.language = this._podcastData.language || this.feedMetadata.language || ''
|
||||||
this.podcast.autoDownloadEpisodes = false
|
this.podcast.autoDownloadEpisodes = false
|
||||||
|
this.podcast.type = this._podcastData.type || this.feedMetadata.type || 'episodic'
|
||||||
|
|
||||||
|
this.podcast.explicit = this._podcastData.explicit || this.feedMetadata.explicit === 'yes' || this.feedMetadata.explicit == 'true'
|
||||||
if (this.folderItems[0]) {
|
if (this.folderItems[0]) {
|
||||||
this.selectedFolderId = this.folderItems[0].value
|
this.selectedFolderId = this.folderItems[0].value
|
||||||
this.folderUpdated()
|
this.folderUpdated()
|
||||||
@@ -226,4 +247,4 @@ export default {
|
|||||||
#episodes-scroll {
|
#episodes-scroll {
|
||||||
max-height: calc(80vh - 200px);
|
max-height: calc(80vh - 200px);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<modals-modal v-model="show" name="opml-feeds-modal" :width="1000" :height="'unset'" :processing="processing">
|
<modals-modal v-model="show" name="opml-feeds-modal" :width="1000" :height="'unset'" :processing="processing">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||||
<p class="font-book text-3xl text-white truncate">{{ title }}</p>
|
<p class="text-3xl text-white truncate">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div ref="wrapper" class="p-4 w-full text-sm py-2 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden">
|
<div ref="wrapper" class="p-4 w-full text-sm py-2 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden">
|
||||||
@@ -97,7 +97,7 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toFeedMetadata(feed) {
|
toFeedMetadata(feed) {
|
||||||
var metadata = feed.metadata
|
const metadata = feed.metadata
|
||||||
return {
|
return {
|
||||||
title: metadata.title,
|
title: metadata.title,
|
||||||
author: metadata.author,
|
author: metadata.author,
|
||||||
@@ -122,9 +122,9 @@ export default {
|
|||||||
},
|
},
|
||||||
async submit() {
|
async submit() {
|
||||||
this.processing = true
|
this.processing = true
|
||||||
var newFeedPayloads = this.feedMetadata.map((metadata) => {
|
const newFeedPayloads = this.feedMetadata.map((metadata) => {
|
||||||
return {
|
return {
|
||||||
path: `${this.selectedFolderPath}\\${this.$sanitizeFilename(metadata.title)}`,
|
path: `${this.selectedFolderPath}/${this.$sanitizeFilename(metadata.title)}`,
|
||||||
folderId: this.selectedFolderId,
|
folderId: this.selectedFolderId,
|
||||||
libraryId: this.currentLibrary.id,
|
libraryId: this.currentLibrary.id,
|
||||||
media: {
|
media: {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<modals-modal v-model="show" name="podcast-episode-remove-modal" :width="500" :height="'unset'" :processing="processing">
|
<modals-modal v-model="show" name="podcast-episode-remove-modal" :width="500" :height="'unset'" :processing="processing">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||||
<p class="font-book text-3xl text-white truncate">{{ title }}</p>
|
<p class="text-3xl text-white truncate">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div ref="wrapper" class="px-8 py-6 w-full text-sm rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden">
|
<div ref="wrapper" class="px-8 py-6 w-full text-sm rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden">
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<modals-modal v-model="show" name="podcast-episode-view-modal" :width="800" :height="'unset'" :processing="processing">
|
<modals-modal v-model="show" name="podcast-episode-view-modal" :width="800" :height="'unset'" :processing="processing">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||||
<p class="font-book text-3xl text-white truncate">{{ $strings.LabelEpisode }}</p>
|
<p class="text-3xl text-white truncate">{{ $strings.LabelEpisode }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div ref="wrapper" class="p-4 w-full text-sm rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-y-auto" style="max-height: 80vh">
|
<div ref="wrapper" class="p-4 w-full text-sm rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-y-auto" style="max-height: 80vh">
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<ui-text-input-with-label v-model="newEpisode.episode" :label="$strings.LabelEpisode" />
|
<ui-text-input-with-label v-model="newEpisode.episode" :label="$strings.LabelEpisode" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-1/5 p-1">
|
<div class="w-1/5 p-1">
|
||||||
<ui-text-input-with-label v-model="newEpisode.episodeType" :label="$strings.LabelEpisodeType" />
|
<ui-dropdown v-model="newEpisode.episodeType" :label="$strings.LabelEpisodeType" :items="episodeTypes" small />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-2/5 p-1">
|
<div class="w-2/5 p-1">
|
||||||
<ui-text-input-with-label v-model="pubDateInput" @input="updatePubDate" type="datetime-local" :label="$strings.LabelPubDate" />
|
<ui-text-input-with-label v-model="pubDateInput" @input="updatePubDate" type="datetime-local" :label="$strings.LabelPubDate" />
|
||||||
@@ -24,7 +24,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-end pt-4">
|
<div class="flex items-center justify-end pt-4">
|
||||||
<ui-btn @click="submit">{{ $strings.ButtonSubmit }}</ui-btn>
|
<!-- desktop -->
|
||||||
|
<ui-btn @click="submit" class="mx-2 hidden md:block">{{ $strings.ButtonSave }}</ui-btn>
|
||||||
|
<ui-btn @click="saveAndClose" class="mx-2 hidden md:block">{{ $strings.ButtonSaveAndClose }}</ui-btn>
|
||||||
|
|
||||||
|
<!-- mobile -->
|
||||||
|
<ui-btn @click="saveAndClose" class="mx-2 md:hidden">{{ $strings.ButtonSave }}</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="enclosureUrl" class="py-4">
|
<div v-if="enclosureUrl" class="py-4">
|
||||||
<p class="text-xs text-gray-300 font-semibold">Episode URL from RSS feed</p>
|
<p class="text-xs text-gray-300 font-semibold">Episode URL from RSS feed</p>
|
||||||
@@ -89,6 +94,9 @@ export default {
|
|||||||
},
|
},
|
||||||
enclosureUrl() {
|
enclosureUrl() {
|
||||||
return this.enclosure.url
|
return this.enclosure.url
|
||||||
|
},
|
||||||
|
episodeTypes() {
|
||||||
|
return this.$store.state.globals.episodeTypes || []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -122,28 +130,43 @@ export default {
|
|||||||
}
|
}
|
||||||
return updatePayload
|
return updatePayload
|
||||||
},
|
},
|
||||||
submit() {
|
async saveAndClose() {
|
||||||
const payload = this.getUpdatePayload()
|
const wasUpdated = await this.submit()
|
||||||
if (!Object.keys(payload).length) {
|
if (wasUpdated !== null) this.$emit('close')
|
||||||
return this.$toast.info('No updates were made')
|
},
|
||||||
|
async submit() {
|
||||||
|
if (this.isProcessing) {
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updatedDetails = this.getUpdatePayload()
|
||||||
|
if (!Object.keys(updatedDetails).length) {
|
||||||
|
this.$toast.info('No changes were made')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return this.updateDetails(updatedDetails)
|
||||||
|
},
|
||||||
|
async updateDetails(updatedDetails) {
|
||||||
this.isProcessing = true
|
this.isProcessing = true
|
||||||
this.$axios
|
const updateResult = await this.$axios.$patch(`/api/podcasts/${this.libraryItem.id}/episode/${this.episodeId}`, updatedDetails).catch((error) => {
|
||||||
.$patch(`/api/podcasts/${this.libraryItem.id}/episode/${this.episodeId}`, payload)
|
console.error('Failed update episode', error)
|
||||||
.then(() => {
|
this.isProcessing = false
|
||||||
this.isProcessing = false
|
this.$toast.error(error?.response?.data || 'Failed to update episode')
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
this.isProcessing = false
|
||||||
|
if (updateResult) {
|
||||||
|
if (updateResult) {
|
||||||
this.$toast.success('Podcast episode updated')
|
this.$toast.success('Podcast episode updated')
|
||||||
this.$emit('close')
|
return true
|
||||||
})
|
} else {
|
||||||
.catch((error) => {
|
this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
|
||||||
var errorMsg = error.response && error.response.data ? error.response.data : 'Failed to update episode'
|
}
|
||||||
console.error('Failed update episode', error)
|
}
|
||||||
this.isProcessing = false
|
return false
|
||||||
this.$toast.error(errorMsg)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<modals-modal v-model="show" name="rss-feed-modal" :width="600" :height="'unset'" :processing="processing">
|
<modals-modal v-model="show" name="rss-feed-modal" :width="600" :height="'unset'" :processing="processing">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||||
<p class="font-book text-3xl text-white truncate">{{ title }}</p>
|
<p class="text-3xl text-white truncate">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div ref="wrapper" class="px-8 py-6 w-full text-sm rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden">
|
<div ref="wrapper" class="px-8 py-6 w-full text-sm rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden">
|
||||||
@@ -14,6 +14,27 @@
|
|||||||
|
|
||||||
<span class="material-icons 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(currentFeed.feedUrl)">content_copy</span>
|
<span class="material-icons 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(currentFeed.feedUrl)">content_copy</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="currentFeed.meta" class="mt-5">
|
||||||
|
<div class="flex py-0.5">
|
||||||
|
<div class="w-48">
|
||||||
|
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelRSSFeedPreventIndexing }}</span>
|
||||||
|
</div>
|
||||||
|
<div>{{ currentFeed.meta.preventIndexing ? 'Yes' : 'No' }}</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="currentFeed.meta.ownerName" class="flex py-0.5">
|
||||||
|
<div class="w-48">
|
||||||
|
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelRSSFeedCustomOwnerName }}</span>
|
||||||
|
</div>
|
||||||
|
<div>{{ currentFeed.meta.ownerName }}</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="currentFeed.meta.ownerEmail" class="flex py-0.5">
|
||||||
|
<div class="w-48">
|
||||||
|
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelRSSFeedCustomOwnerEmail }}</span>
|
||||||
|
</div>
|
||||||
|
<div>{{ currentFeed.meta.ownerEmail }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="w-full">
|
<div v-else class="w-full">
|
||||||
<p class="text-lg font-semibold mb-4">{{ $strings.HeaderOpenRSSFeed }}</p>
|
<p class="text-lg font-semibold mb-4">{{ $strings.HeaderOpenRSSFeed }}</p>
|
||||||
@@ -22,6 +43,7 @@
|
|||||||
<ui-text-input-with-label v-model="newFeedSlug" :label="$strings.LabelRSSFeedSlug" />
|
<ui-text-input-with-label v-model="newFeedSlug" :label="$strings.LabelRSSFeedSlug" />
|
||||||
<p class="text-xs text-gray-400 py-0.5 px-1">{{ $getString('MessageFeedURLWillBe', [demoFeedUrl]) }}</p>
|
<p class="text-xs text-gray-400 py-0.5 px-1">{{ $getString('MessageFeedURLWillBe', [demoFeedUrl]) }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
<widgets-rss-feed-metadata-builder v-model="metadataDetails" />
|
||||||
|
|
||||||
<p v-if="isHttp" class="w-full pt-2 text-warning text-xs">{{ $strings.NoteRSSFeedPodcastAppsHttps }}</p>
|
<p v-if="isHttp" class="w-full pt-2 text-warning text-xs">{{ $strings.NoteRSSFeedPodcastAppsHttps }}</p>
|
||||||
<p v-if="hasEpisodesWithoutPubDate" class="w-full pt-2 text-warning text-xs">{{ $strings.NoteRSSFeedPodcastAppsPubDate }}</p>
|
<p v-if="hasEpisodesWithoutPubDate" class="w-full pt-2 text-warning text-xs">{{ $strings.NoteRSSFeedPodcastAppsPubDate }}</p>
|
||||||
@@ -41,7 +63,12 @@ export default {
|
|||||||
return {
|
return {
|
||||||
processing: false,
|
processing: false,
|
||||||
newFeedSlug: null,
|
newFeedSlug: null,
|
||||||
currentFeed: null
|
currentFeed: null,
|
||||||
|
metadataDetails: {
|
||||||
|
preventIndexing: true,
|
||||||
|
ownerName: '',
|
||||||
|
ownerEmail: ''
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -107,7 +134,8 @@ export default {
|
|||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
serverAddress: window.origin,
|
serverAddress: window.origin,
|
||||||
slug: this.newFeedSlug
|
slug: this.newFeedSlug,
|
||||||
|
metadataDetails: this.metadataDetails
|
||||||
}
|
}
|
||||||
if (this.$isDev) payload.serverAddress = `http://localhost:3333${this.$config.routerBasePath}`
|
if (this.$isDev) payload.serverAddress = `http://localhost:3333${this.$config.routerBasePath}`
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Hover timestamp -->
|
<!-- Hover timestamp -->
|
||||||
<div ref="hoverTimestamp" class="absolute -top-8 left-0 bg-white text-black rounded-full opacity-0 pointer-events-none">
|
<div ref="hoverTimestamp" class="absolute -top-8 left-0 bg-white text-black rounded-full opacity-0 pointer-events-none z-10">
|
||||||
<p ref="hoverTimestampText" class="text-xs font-mono text-center px-2 py-0.5 truncate whitespace-nowrap">00:00</p>
|
<p ref="hoverTimestampText" class="text-xs font-mono text-center px-2 py-0.5 truncate whitespace-nowrap">00:00</p>
|
||||||
</div>
|
</div>
|
||||||
<div ref="hoverTimestampArrow" class="absolute -top-3 left-0 bg-white text-black rounded-full opacity-0 pointer-events-none">
|
<div ref="hoverTimestampArrow" class="absolute -top-3 left-0 bg-white text-black rounded-full opacity-0 pointer-events-none">
|
||||||
@@ -83,9 +83,9 @@ export default {
|
|||||||
|
|
||||||
var offsetX = e.offsetX
|
var offsetX = e.offsetX
|
||||||
var perc = offsetX / this.trackWidth
|
var perc = offsetX / this.trackWidth
|
||||||
const baseTime = this.useChapterTrack ? this.currentChapterStart : 0;
|
const baseTime = this.useChapterTrack ? this.currentChapterStart : 0
|
||||||
const duration = this.useChapterTrack ? this.currentChapterDuration : this.duration;
|
const duration = this.useChapterTrack ? this.currentChapterDuration : this.duration
|
||||||
const time = baseTime + (perc * duration);
|
const time = baseTime + perc * duration
|
||||||
if (isNaN(time) || time === null) {
|
if (isNaN(time) || time === null) {
|
||||||
console.error('Invalid time', perc, time)
|
console.error('Invalid time', perc, time)
|
||||||
return
|
return
|
||||||
@@ -143,10 +143,10 @@ export default {
|
|||||||
mousemoveTrack(e) {
|
mousemoveTrack(e) {
|
||||||
var offsetX = e.offsetX
|
var offsetX = e.offsetX
|
||||||
|
|
||||||
const baseTime = this.useChapterTrack ? this.currentChapterStart : 0;
|
const baseTime = this.useChapterTrack ? this.currentChapterStart : 0
|
||||||
const duration = this.useChapterTrack ? this.currentChapterDuration : this.duration;
|
const duration = this.useChapterTrack ? this.currentChapterDuration : this.duration
|
||||||
const progressTime = (offsetX / this.trackWidth) * duration;
|
const progressTime = (offsetX / this.trackWidth) * duration
|
||||||
const totalTime = baseTime + progressTime;
|
const totalTime = baseTime + progressTime
|
||||||
|
|
||||||
if (this.$refs.hoverTimestamp) {
|
if (this.$refs.hoverTimestamp) {
|
||||||
var width = this.$refs.hoverTimestamp.clientWidth
|
var width = this.$refs.hoverTimestamp.clientWidth
|
||||||
|
|||||||
@@ -1,18 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-full w-full">
|
<div class="h-full w-full">
|
||||||
<div class="h-full flex items-center">
|
<div class="h-full flex items-center justify-center">
|
||||||
<div style="width: 100px; max-width: 100px" class="h-full flex items-center overflow-x-hidden justify-center">
|
<div style="width: 100px; max-width: 100px" class="h-full hidden sm:flex items-center overflow-x-hidden justify-center">
|
||||||
<span v-show="hasPrev" class="material-icons text-white text-opacity-50 hover:text-opacity-80 cursor-pointer text-6xl" @mousedown.prevent @click="prev">chevron_left</span>
|
<span v-if="hasPrev" class="material-icons text-white text-opacity-50 hover:text-opacity-80 cursor-pointer text-6xl" @mousedown.prevent @click="prev">chevron_left</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="frame" class="w-full" style="height: 650px">
|
<div id="frame" class="w-full" style="height: 80%">
|
||||||
<div id="viewer" class="border border-gray-100 bg-white shadow-md"></div>
|
<div id="viewer"></div>
|
||||||
|
|
||||||
<div class="py-4 flex justify-center" style="height: 50px">
|
|
||||||
<p>{{ progress }}%</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div style="width: 100px; max-width: 100px" class="h-full flex items-center justify-center overflow-x-hidden">
|
<div style="width: 100px; max-width: 100px" class="h-full hidden sm:flex items-center justify-center overflow-x-hidden">
|
||||||
<span v-show="hasNext" class="material-icons text-white text-opacity-50 hover:text-opacity-80 cursor-pointer text-6xl" @mousedown.prevent @click="next">chevron_right</span>
|
<span v-if="hasNext" class="material-icons text-white text-opacity-50 hover:text-opacity-80 cursor-pointer text-6xl" @mousedown.prevent @click="next">chevron_right</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -21,108 +17,252 @@
|
|||||||
<script>
|
<script>
|
||||||
import ePub from 'epubjs'
|
import ePub from 'epubjs'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {object} EpubReader
|
||||||
|
* @property {ePub.Book} book
|
||||||
|
* @property {ePub.Rendition} rendition
|
||||||
|
*/
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
url: String
|
url: String,
|
||||||
|
libraryItem: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
windowWidth: 0,
|
||||||
|
/** @type {ePub.Book} */
|
||||||
book: null,
|
book: null,
|
||||||
rendition: null,
|
/** @type {ePub.Rendition} */
|
||||||
chapters: [],
|
rendition: null
|
||||||
title: '',
|
|
||||||
author: '',
|
|
||||||
progress: 0,
|
|
||||||
hasNext: true,
|
|
||||||
hasPrev: false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {},
|
computed: {
|
||||||
methods: {
|
/** @returns {string} */
|
||||||
changedChapter() {
|
libraryItemId() {
|
||||||
if (this.rendition) {
|
return this.libraryItem?.id
|
||||||
this.rendition.display(this.selectedChapter)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
hasPrev() {
|
||||||
|
return !this.rendition?.location?.atStart
|
||||||
|
},
|
||||||
|
hasNext() {
|
||||||
|
return !this.rendition?.location?.atEnd
|
||||||
|
},
|
||||||
|
/** @returns {Array<ePub.NavItem>} */
|
||||||
|
chapters() {
|
||||||
|
return this.book ? this.book.navigation.toc : []
|
||||||
|
},
|
||||||
|
userMediaProgress() {
|
||||||
|
if (!this.libraryItemId) return
|
||||||
|
return this.$store.getters['user/getUserMediaProgress'](this.libraryItemId)
|
||||||
|
},
|
||||||
|
localStorageLocationsKey() {
|
||||||
|
return `ebookLocations-${this.libraryItemId}`
|
||||||
|
},
|
||||||
|
readerWidth() {
|
||||||
|
if (this.windowWidth < 640) return this.windowWidth
|
||||||
|
return this.windowWidth - 200
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
prev() {
|
prev() {
|
||||||
if (this.rendition) {
|
return this.rendition?.prev()
|
||||||
this.rendition.prev()
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
next() {
|
next() {
|
||||||
if (this.rendition) {
|
return this.rendition?.next()
|
||||||
this.rendition.next()
|
},
|
||||||
|
goToChapter(href) {
|
||||||
|
return this.rendition?.display(href)
|
||||||
|
},
|
||||||
|
keyUp(e) {
|
||||||
|
const rtl = this.book.package.metadata.direction === 'rtl'
|
||||||
|
if ((e.keyCode || e.which) == 37) {
|
||||||
|
return rtl ? this.next() : this.prev()
|
||||||
|
} else if ((e.keyCode || e.which) == 39) {
|
||||||
|
return rtl ? this.prev() : this.next()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
keyUp() {
|
/**
|
||||||
if ((e.keyCode || e.which) == 37) {
|
* @param {object} payload
|
||||||
this.prev()
|
* @param {string} payload.ebookLocation - CFI of the current location
|
||||||
} else if ((e.keyCode || e.which) == 39) {
|
* @param {string} payload.ebookProgress - eBook Progress Percentage
|
||||||
this.next()
|
*/
|
||||||
|
updateProgress(payload) {
|
||||||
|
this.$axios.$patch(`/api/me/progress/${this.libraryItemId}`, payload).catch((error) => {
|
||||||
|
console.error('EpubReader.updateProgress failed:', error)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
getAllEbookLocationData() {
|
||||||
|
const locations = []
|
||||||
|
let totalSize = 0 // Total in bytes
|
||||||
|
|
||||||
|
for (const key in localStorage) {
|
||||||
|
if (!localStorage.hasOwnProperty(key) || !key.startsWith('ebookLocations-')) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const ebookLocations = JSON.parse(localStorage[key])
|
||||||
|
if (!ebookLocations.locations) throw new Error('Invalid locations object')
|
||||||
|
|
||||||
|
ebookLocations.key = key
|
||||||
|
ebookLocations.size = (localStorage[key].length + key.length) * 2
|
||||||
|
locations.push(ebookLocations)
|
||||||
|
totalSize += ebookLocations.size
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to parse ebook locations', key, error)
|
||||||
|
localStorage.removeItem(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by oldest lastAccessed first
|
||||||
|
locations.sort((a, b) => a.lastAccessed - b.lastAccessed)
|
||||||
|
|
||||||
|
return {
|
||||||
|
locations,
|
||||||
|
totalSize
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/** @param {string} locationString */
|
||||||
|
checkSaveLocations(locationString) {
|
||||||
|
const maxSizeInBytes = 3000000 // Allow epub locations to take up to 3MB of space
|
||||||
|
const newLocationsSize = JSON.stringify({ lastAccessed: Date.now(), locations: locationString }).length * 2
|
||||||
|
|
||||||
|
// Too large overall
|
||||||
|
if (newLocationsSize > maxSizeInBytes) {
|
||||||
|
console.error('Epub locations are too large to store. Size =', newLocationsSize)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const ebookLocationsData = this.getAllEbookLocationData()
|
||||||
|
|
||||||
|
let availableSpace = maxSizeInBytes - ebookLocationsData.totalSize
|
||||||
|
|
||||||
|
// Remove epub locations until there is room for locations
|
||||||
|
while (availableSpace < newLocationsSize && ebookLocationsData.locations.length) {
|
||||||
|
const oldestLocation = ebookLocationsData.locations.shift()
|
||||||
|
console.log(`Removing cached locations for epub "${oldestLocation.key}" taking up ${oldestLocation.size} bytes`)
|
||||||
|
availableSpace += oldestLocation.size
|
||||||
|
localStorage.removeItem(oldestLocation.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Cacheing epub locations with key "${this.localStorageLocationsKey}" taking up ${newLocationsSize} bytes`)
|
||||||
|
this.saveLocations(locationString)
|
||||||
|
},
|
||||||
|
/** @param {string} locationString */
|
||||||
|
saveLocations(locationString) {
|
||||||
|
localStorage.setItem(
|
||||||
|
this.localStorageLocationsKey,
|
||||||
|
JSON.stringify({
|
||||||
|
lastAccessed: Date.now(),
|
||||||
|
locations: locationString
|
||||||
|
})
|
||||||
|
)
|
||||||
|
},
|
||||||
|
loadLocations() {
|
||||||
|
const locationsObjString = localStorage.getItem(this.localStorageLocationsKey)
|
||||||
|
if (!locationsObjString) return null
|
||||||
|
|
||||||
|
const locationsObject = JSON.parse(locationsObjString)
|
||||||
|
|
||||||
|
// Remove invalid location objects
|
||||||
|
if (!locationsObject.locations) {
|
||||||
|
console.error('Invalid epub locations stored', this.localStorageLocationsKey)
|
||||||
|
localStorage.removeItem(this.localStorageLocationsKey)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update lastAccessed
|
||||||
|
this.saveLocations(locationsObject.locations)
|
||||||
|
|
||||||
|
return locationsObject.locations
|
||||||
|
},
|
||||||
|
/** @param {string} location - CFI of the new location */
|
||||||
|
relocated(location) {
|
||||||
|
if (this.userMediaProgress?.ebookLocation === location.start.cfi) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (location.end.percentage) {
|
||||||
|
this.updateProgress({
|
||||||
|
ebookLocation: location.start.cfi,
|
||||||
|
ebookProgress: location.end.percentage
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.updateProgress({
|
||||||
|
ebookLocation: location.start.cfi
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
initEpub() {
|
initEpub() {
|
||||||
// var book = ePub(this.url, {
|
/** @type {EpubReader} */
|
||||||
// requestHeaders: {
|
const reader = this
|
||||||
// Authorization: `Bearer ${this.userToken}`
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
var book = ePub(this.url)
|
|
||||||
this.book = book
|
|
||||||
|
|
||||||
this.rendition = book.renderTo('viewer', {
|
/** @type {ePub.Book} */
|
||||||
width: window.innerWidth - 200,
|
reader.book = new ePub(reader.url, {
|
||||||
height: 600,
|
width: this.readerWidth,
|
||||||
ignoreClass: 'annotator-hl',
|
height: window.innerHeight - 50
|
||||||
manager: 'continuous',
|
|
||||||
spread: 'always'
|
|
||||||
})
|
})
|
||||||
var displayed = this.rendition.display()
|
|
||||||
|
|
||||||
book.ready
|
/** @type {ePub.Rendition} */
|
||||||
.then(() => {
|
reader.rendition = reader.book.renderTo('viewer', {
|
||||||
console.log('Book ready')
|
width: this.readerWidth,
|
||||||
return book.locations.generate(1600)
|
height: window.innerHeight * 0.8
|
||||||
|
})
|
||||||
|
|
||||||
|
// load saved progress
|
||||||
|
reader.rendition.display(this.userMediaProgress?.ebookLocation || reader.book.locations.start)
|
||||||
|
|
||||||
|
// load style
|
||||||
|
reader.rendition.themes.default({ '*': { color: '#fff!important' } })
|
||||||
|
|
||||||
|
reader.book.ready.then(() => {
|
||||||
|
// set up event listeners
|
||||||
|
reader.rendition.on('relocated', reader.relocated)
|
||||||
|
reader.rendition.on('keydown', reader.keyUp)
|
||||||
|
|
||||||
|
let touchStart = 0
|
||||||
|
let touchEnd = 0
|
||||||
|
reader.rendition.on('touchstart', (event) => {
|
||||||
|
touchStart = event.changedTouches[0].screenX
|
||||||
})
|
})
|
||||||
.then((locations) => {
|
|
||||||
// console.log('Loaded locations', locations)
|
reader.rendition.on('touchend', (event) => {
|
||||||
// Wait for book to be rendered to get current page
|
touchEnd = event.changedTouches[0].screenX
|
||||||
displayed.then(() => {
|
const touchDistanceX = Math.abs(touchEnd - touchStart)
|
||||||
// Get the current CFI
|
if (touchStart < touchEnd && touchDistanceX > 120) {
|
||||||
var currentLocation = this.rendition.currentLocation()
|
this.next()
|
||||||
if (!currentLocation.start) {
|
}
|
||||||
console.error('No Start', currentLocation)
|
if (touchStart > touchEnd && touchDistanceX > 120) {
|
||||||
} else {
|
this.prev()
|
||||||
var currentPage = book.locations.percentageFromCfi(currentLocation.start.cfi)
|
}
|
||||||
// console.log('current page', currentPage)
|
})
|
||||||
}
|
|
||||||
|
// load ebook cfi locations
|
||||||
|
const savedLocations = this.loadLocations()
|
||||||
|
if (savedLocations) {
|
||||||
|
reader.book.locations.load(savedLocations)
|
||||||
|
} else {
|
||||||
|
reader.book.locations.generate().then(() => {
|
||||||
|
this.checkSaveLocations(reader.book.locations.save())
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
|
|
||||||
book.loaded.navigation.then((toc) => {
|
|
||||||
var _chapters = []
|
|
||||||
toc.forEach((chapter) => {
|
|
||||||
_chapters.push(chapter)
|
|
||||||
})
|
|
||||||
this.chapters = _chapters
|
|
||||||
})
|
|
||||||
book.loaded.metadata.then((metadata) => {
|
|
||||||
this.author = metadata.creator
|
|
||||||
this.title = metadata.title
|
|
||||||
})
|
|
||||||
|
|
||||||
this.rendition.on('keyup', this.keyUp)
|
|
||||||
|
|
||||||
this.rendition.on('relocated', (location) => {
|
|
||||||
var percent = book.locations.percentageFromCfi(location.start.cfi)
|
|
||||||
this.progress = Math.floor(percent * 100)
|
|
||||||
|
|
||||||
this.hasNext = !location.atEnd
|
|
||||||
this.hasPrev = !location.atStart
|
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
resize() {
|
||||||
|
this.windowWidth = window.innerWidth
|
||||||
|
this.rendition?.resize(this.readerWidth, window.innerHeight * 0.8)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
window.removeEventListener('resize', this.resize)
|
||||||
|
this.book?.destroy()
|
||||||
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
this.windowWidth = window.innerWidth
|
||||||
|
window.addEventListener('resize', this.resize)
|
||||||
this.initEpub()
|
this.initEpub()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,88 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="h-full w-full">
|
|
||||||
<div id="viewer" class="border border-gray-100 bg-white text-black shadow-md h-screen overflow-y-auto p-4" v-html="pageHtml"></div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
url: String,
|
|
||||||
libraryItem: {
|
|
||||||
type: Object,
|
|
||||||
default: () => {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
bookInfo: {},
|
|
||||||
page: 0,
|
|
||||||
numPages: 0,
|
|
||||||
pageHtml: '',
|
|
||||||
progress: 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
libraryItemId() {
|
|
||||||
return this.libraryItem ? this.libraryItem.id : null
|
|
||||||
},
|
|
||||||
hasPrev() {
|
|
||||||
return this.page > 0
|
|
||||||
},
|
|
||||||
hasNext() {
|
|
||||||
return this.page < this.numPages - 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
prev() {
|
|
||||||
if (!this.hasPrev) return
|
|
||||||
this.page--
|
|
||||||
this.loadPage()
|
|
||||||
},
|
|
||||||
next() {
|
|
||||||
if (!this.hasNext) return
|
|
||||||
this.page++
|
|
||||||
this.loadPage()
|
|
||||||
},
|
|
||||||
keyUp() {
|
|
||||||
if ((e.keyCode || e.which) == 37) {
|
|
||||||
this.prev()
|
|
||||||
} else if ((e.keyCode || e.which) == 39) {
|
|
||||||
this.next()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
loadPage() {
|
|
||||||
this.$axios
|
|
||||||
.$get(`/api/ebooks/${this.libraryItemId}/page/${this.page}?dev=${this.$isDev ? 1 : 0}`)
|
|
||||||
.then((html) => {
|
|
||||||
this.pageHtml = html
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('Failed to load page', error)
|
|
||||||
this.$toast.error('Failed to load page')
|
|
||||||
})
|
|
||||||
},
|
|
||||||
loadInfo() {
|
|
||||||
this.$axios
|
|
||||||
.$get(`/api/ebooks/${this.libraryItemId}/info?dev=${this.$isDev ? 1 : 0}`)
|
|
||||||
.then((bookInfo) => {
|
|
||||||
this.bookInfo = bookInfo
|
|
||||||
this.numPages = bookInfo.pages
|
|
||||||
this.page = 0
|
|
||||||
this.loadPage()
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('Failed to load page', error)
|
|
||||||
this.$toast.error('Failed to load info')
|
|
||||||
})
|
|
||||||
},
|
|
||||||
initEpub() {
|
|
||||||
if (!this.libraryItemId) return
|
|
||||||
this.loadInfo()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.initEpub()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,24 +1,48 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="show" class="w-screen h-screen fixed top-0 left-0 z-60 bg-primary text-white">
|
<div v-if="show" class="w-screen h-screen fixed top-0 left-0 z-60 bg-primary text-white">
|
||||||
<div class="absolute top-4 right-4 z-20">
|
<div class="absolute top-4 left-4 z-20">
|
||||||
<span class="material-icons cursor-pointer text-4xl" @click="close">close</span>
|
<span v-if="hasToC && !tocOpen" ref="tocButton" class="material-icons cursor-pointer text-2xl" @click="toggleToC">menu</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="absolute top-4 left-4 font-book">
|
<div class="absolute top-4 left-1/2 transform -translate-x-1/2">
|
||||||
<h1 class="text-2xl mb-1">{{ abTitle }}</h1>
|
<h1 class="text-lg sm:text-xl md:text-2xl mb-1" style="line-height: 1.15; font-weight: 100">
|
||||||
<p v-if="abAuthor">by {{ abAuthor }}</p>
|
<span style="font-weight: 600">{{ abTitle }}</span>
|
||||||
|
<span v-if="abAuthor" style="display: inline"> – </span>
|
||||||
|
<span v-if="abAuthor">{{ abAuthor }}</span>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="absolute top-4 right-4 z-20">
|
||||||
|
<span v-if="hasSettings" class="material-icons cursor-pointer text-2xl" @click="openSettings">settings</span>
|
||||||
|
<span class="material-icons cursor-pointer text-2xl" @click="close">close</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<component v-if="componentName" ref="readerComponent" :is="componentName" :url="ebookUrl" :library-item="selectedLibraryItem" />
|
<component v-if="componentName" ref="readerComponent" :is="componentName" :url="ebookUrl" :library-item="selectedLibraryItem" />
|
||||||
|
|
||||||
<div class="absolute bottom-2 left-2">{{ ebookType }}</div>
|
<!-- TOC side nav -->
|
||||||
|
<div v-if="tocOpen" class="w-full h-full fixed inset-0 bg-black/20 z-20" @click.stop.prevent="toggleToC"></div>
|
||||||
|
<div v-if="hasToC" class="w-72 h-full max-h-full absolute top-0 left-0 bg-bg shadow-xl transition-transform z-30" :class="tocOpen ? 'translate-x-0' : '-translate-x-72'" @click.stop.prevent="toggleToC">
|
||||||
|
<div class="p-4 h-full overflow-hidden">
|
||||||
|
<p class="text-lg font-semibold mb-2">Table of Contents</p>
|
||||||
|
<div class="tocContent">
|
||||||
|
<ul>
|
||||||
|
<li v-for="chapter in chapters" :key="chapter.id" class="py-1">
|
||||||
|
<a :href="chapter.href" @click.prevent="$refs.readerComponent.goToChapter(chapter.href)">{{ chapter.label }}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {
|
||||||
|
chapters: [],
|
||||||
|
tocOpen: false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
show(newVal) {
|
show(newVal) {
|
||||||
@@ -37,13 +61,18 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
componentName() {
|
componentName() {
|
||||||
if (this.ebookType === 'epub' && this.$isDev) return 'readers-epub-reader2'
|
if (this.ebookType === 'epub') return 'readers-epub-reader'
|
||||||
else if (this.ebookType === 'epub') return 'readers-epub-reader'
|
|
||||||
else if (this.ebookType === 'mobi') return 'readers-mobi-reader'
|
else if (this.ebookType === 'mobi') return 'readers-mobi-reader'
|
||||||
else if (this.ebookType === 'pdf') return 'readers-pdf-reader'
|
else if (this.ebookType === 'pdf') return 'readers-pdf-reader'
|
||||||
else if (this.ebookType === 'comic') return 'readers-comic-reader'
|
else if (this.ebookType === 'comic') return 'readers-comic-reader'
|
||||||
return null
|
return null
|
||||||
},
|
},
|
||||||
|
hasToC() {
|
||||||
|
return this.isEpub
|
||||||
|
},
|
||||||
|
hasSettings() {
|
||||||
|
return false
|
||||||
|
},
|
||||||
abTitle() {
|
abTitle() {
|
||||||
return this.mediaMetadata.title
|
return this.mediaMetadata.title
|
||||||
},
|
},
|
||||||
@@ -111,18 +140,29 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
toggleToC() {
|
||||||
|
this.tocOpen = !this.tocOpen
|
||||||
|
this.chapters = this.$refs.readerComponent.chapters
|
||||||
|
},
|
||||||
|
openSettings() {},
|
||||||
hotkey(action) {
|
hotkey(action) {
|
||||||
console.log('Reader hotkey', action)
|
console.log('Reader hotkey', action)
|
||||||
if (!this.$refs.readerComponent) return
|
if (!this.$refs.readerComponent) return
|
||||||
|
|
||||||
if (action === this.$hotkeys.EReader.NEXT_PAGE) {
|
if (action === this.$hotkeys.EReader.NEXT_PAGE) {
|
||||||
if (this.$refs.readerComponent.next) this.$refs.readerComponent.next()
|
this.next()
|
||||||
} else if (action === this.$hotkeys.EReader.PREV_PAGE) {
|
} else if (action === this.$hotkeys.EReader.PREV_PAGE) {
|
||||||
if (this.$refs.readerComponent.prev) this.$refs.readerComponent.prev()
|
this.prev()
|
||||||
} else if (action === this.$hotkeys.EReader.CLOSE) {
|
} else if (action === this.$hotkeys.EReader.CLOSE) {
|
||||||
this.close()
|
this.close()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
next() {
|
||||||
|
if (this.$refs.readerComponent?.next) this.$refs.readerComponent.next()
|
||||||
|
},
|
||||||
|
prev() {
|
||||||
|
if (this.$refs.readerComponent?.prev) this.$refs.readerComponent.prev()
|
||||||
|
},
|
||||||
registerListeners() {
|
registerListeners() {
|
||||||
this.$eventBus.$on('reader-hotkey', this.hotkey)
|
this.$eventBus.$on('reader-hotkey', this.hotkey)
|
||||||
},
|
},
|
||||||
@@ -151,4 +191,8 @@ export default {
|
|||||||
.ebook-viewer {
|
.ebook-viewer {
|
||||||
height: calc(100% - 96px);
|
height: calc(100% - 96px);
|
||||||
}
|
}
|
||||||
|
.tocContent {
|
||||||
|
height: calc(100% - 36px);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-96 my-6 mx-auto">
|
<div class="w-96 my-6 mx-auto">
|
||||||
<h1 class="text-2xl mb-4 font-book">{{ $strings.HeaderStatsMinutesListeningChart }}</h1>
|
<h1 class="text-2xl mb-4">{{ $strings.HeaderStatsMinutesListeningChart }}</h1>
|
||||||
<div class="relative w-96 h-72">
|
<div class="relative w-96 h-72">
|
||||||
<div class="absolute top-0 left-0">
|
<div class="absolute top-0 left-0">
|
||||||
<template v-for="lbl in yAxisLabels">
|
<template v-for="lbl in yAxisLabels">
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
<div class="absolute -bottom-2 left-0 flex ml-6">
|
<div class="absolute -bottom-2 left-0 flex ml-6">
|
||||||
<template v-for="dayObj in last7Days">
|
<template v-for="dayObj in last7Days">
|
||||||
<div :key="dayObj.date" :style="{ width: daySpacing + daySpacing / 14 + 'px' }">
|
<div :key="dayObj.date" :style="{ width: daySpacing + daySpacing / 14 + 'px' }">
|
||||||
<p class="text-sm font-book">{{ dayObj.dayOfWeekAbbr }}</p>
|
<p class="text-sm">{{ dayObj.dayOfWeekAbbr }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
<div class="px-2">
|
<div class="px-2">
|
||||||
<p class="text-4xl md:text-5xl font-bold">{{ totalItems }}</p>
|
<p class="text-4xl md:text-5xl font-bold">{{ totalItems }}</p>
|
||||||
<p class="font-book text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsItemsInLibrary }}</p>
|
<p class="text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsItemsInLibrary }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
<span class="material-icons text-7xl">show_chart</span>
|
<span class="material-icons text-7xl">show_chart</span>
|
||||||
<div class="px-1">
|
<div class="px-1">
|
||||||
<p class="text-4xl md:text-5xl font-bold">{{ totalTime }}</p>
|
<p class="text-4xl md:text-5xl font-bold">{{ totalTime }}</p>
|
||||||
<p class="font-book text-xs md:text-sm text-white text-opacity-80">{{ useOverallHours ? $strings.LabelStatsOverallHours : $strings.LabelStatsOverallDays }}</p>
|
<p class="text-xs md:text-sm text-white text-opacity-80">{{ useOverallHours ? $strings.LabelStatsOverallHours : $strings.LabelStatsOverallDays }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
<div class="px-1">
|
<div class="px-1">
|
||||||
<p class="text-4xl md:text-5xl font-bold">{{ totalAuthors }}</p>
|
<p class="text-4xl md:text-5xl font-bold">{{ totalAuthors }}</p>
|
||||||
<p class="font-book text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsAuthors }}</p>
|
<p class="text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsAuthors }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
<span class="material-icons-outlined text-6xl pt-1">insert_drive_file</span>
|
<span class="material-icons-outlined text-6xl pt-1">insert_drive_file</span>
|
||||||
<div class="px-1">
|
<div class="px-1">
|
||||||
<p class="text-4xl md:text-5xl font-bold">{{ totalSizeNum }}</p>
|
<p class="text-4xl md:text-5xl font-bold">{{ totalSizeNum }}</p>
|
||||||
<p class="font-book text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelSize }} ({{ totalSizeMod }})</p>
|
<p class="text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelSize }} ({{ totalSizeMod }})</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
<span class="material-icons-outlined text-6xl pt-1">audio_file</span>
|
<span class="material-icons-outlined text-6xl pt-1">audio_file</span>
|
||||||
<div class="px-1">
|
<div class="px-1">
|
||||||
<p class="text-4xl md:text-5xl font-bold">{{ numAudioTracks }}</p>
|
<p class="text-4xl md:text-5xl font-bold">{{ numAudioTracks }}</p>
|
||||||
<p class="font-book text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsAudioTracks }}</p>
|
<p class="text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsAudioTracks }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<td>
|
<td>
|
||||||
<p class="truncate text-xs sm:text-sm md:text-base">/{{ backup.path.replace(/\\/g, '/') }}</p>
|
<p class="truncate text-xs sm:text-sm md:text-base">/{{ backup.path.replace(/\\/g, '/') }}</p>
|
||||||
</td>
|
</td>
|
||||||
<td class="hidden sm:table-cell font-sans text-sm">{{ backup.datePretty }}</td>
|
<td class="hidden sm:table-cell font-sans text-sm">{{ $formatDatetime(backup.createdAt, dateFormat, timeFormat) }}</td>
|
||||||
<td class="hidden sm:table-cell font-mono md:text-sm text-xs">{{ $bytesPretty(backup.fileSize) }}</td>
|
<td class="hidden sm:table-cell font-mono md:text-sm text-xs">{{ $bytesPretty(backup.fileSize) }}</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="w-full flex flex-row items-center justify-center">
|
<div class="w-full flex flex-row items-center justify-center">
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
<p class="text-error text-lg font-semibold">{{ $strings.MessageImportantNotice }}</p>
|
<p class="text-error text-lg font-semibold">{{ $strings.MessageImportantNotice }}</p>
|
||||||
<p class="text-base py-1" v-html="$strings.MessageRestoreBackupWarning" />
|
<p class="text-base py-1" v-html="$strings.MessageRestoreBackupWarning" />
|
||||||
|
|
||||||
<p class="text-lg text-center my-8">{{ $strings.MessageRestoreBackupConfirm }} {{ selectedBackup.datePretty }}?</p>
|
<p class="text-lg text-center my-8">{{ $strings.MessageRestoreBackupConfirm }} {{ $formatDatetime(selectedBackup.createdAt, dateFormat, timeFormat) }}?</p>
|
||||||
<div class="flex px-1 items-center">
|
<div class="flex px-1 items-center">
|
||||||
<ui-btn color="primary" @click="showConfirmApply = false">{{ $strings.ButtonNevermind }}</ui-btn>
|
<ui-btn color="primary" @click="showConfirmApply = false">{{ $strings.ButtonNevermind }}</ui-btn>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
@@ -71,6 +71,12 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
userToken() {
|
userToken() {
|
||||||
return this.$store.getters['user/getToken']
|
return this.$store.getters['user/getToken']
|
||||||
|
},
|
||||||
|
dateFormat() {
|
||||||
|
return this.$store.state.serverSettings.dateFormat
|
||||||
|
},
|
||||||
|
timeFormat() {
|
||||||
|
return this.$store.state.serverSettings.timeFormat
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -90,7 +96,7 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
deleteBackupClick(backup) {
|
deleteBackupClick(backup) {
|
||||||
if (confirm(this.$getString('MessageConfirmDeleteBackup', [backup.datePretty]))) {
|
if (confirm(this.$getString('MessageConfirmDeleteBackup', [this.$formatDatetime(backup.createdAt, this.dateFormat, this.timeFormat)]))) {
|
||||||
this.processing = true
|
this.processing = true
|
||||||
this.$axios
|
this.$axios
|
||||||
.$delete(`/api/backups/${backup.id}`)
|
.$delete(`/api/backups/${backup.id}`)
|
||||||
@@ -208,4 +214,4 @@ export default {
|
|||||||
padding-bottom: 5px;
|
padding-bottom: 5px;
|
||||||
background-color: #333;
|
background-color: #333;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<transition name="slide">
|
<transition name="slide">
|
||||||
<table class="text-sm tracksTable" v-show="expanded || keepOpen">
|
<table class="text-sm tracksTable" v-show="expanded || keepOpen">
|
||||||
<tr class="font-book">
|
<tr>
|
||||||
<th class="text-left w-16"><span class="px-4">Id</span></th>
|
<th class="text-left w-16"><span class="px-4">Id</span></th>
|
||||||
<th class="text-left">{{ $strings.LabelTitle }}</th>
|
<th class="text-left">{{ $strings.LabelTitle }}</th>
|
||||||
<th class="text-center">{{ $strings.LabelStart }}</th>
|
<th class="text-center">{{ $strings.LabelStart }}</th>
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
<td class="text-left">
|
<td class="text-left">
|
||||||
<p class="px-4">{{ chapter.id }}</p>
|
<p class="px-4">{{ chapter.id }}</p>
|
||||||
</td>
|
</td>
|
||||||
<td class="font-book">
|
<td>
|
||||||
{{ chapter.title }}
|
{{ chapter.title }}
|
||||||
</td>
|
</td>
|
||||||
<td class="font-mono text-center hover:underline cursor-pointer" @click.stop="goToTimestamp(chapter.start)">
|
<td class="font-mono text-center hover:underline cursor-pointer" @click.stop="goToTimestamp(chapter.start)">
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<transition name="slide">
|
<transition name="slide">
|
||||||
<div class="w-full" v-show="showFiles">
|
<div class="w-full" v-show="showFiles">
|
||||||
<table class="text-sm tracksTable">
|
<table class="text-sm tracksTable">
|
||||||
<tr class="font-book">
|
<tr>
|
||||||
<th class="text-left px-4">{{ $strings.LabelPath }}</th>
|
<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 w-24 min-w-24">{{ $strings.LabelSize }}</th>
|
||||||
<th class="text-left px-4 w-24">{{ $strings.LabelType }}</th>
|
<th class="text-left px-4 w-24">{{ $strings.LabelType }}</th>
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<template v-for="file in files">
|
<template v-for="file in files">
|
||||||
<tr :key="file.path">
|
<tr :key="file.path">
|
||||||
<td class="font-book px-4">
|
<td class="px-4">
|
||||||
{{ showFullPath ? file.metadata.path : file.metadata.relPath }}
|
{{ showFullPath ? file.metadata.path : file.metadata.relPath }}
|
||||||
</td>
|
</td>
|
||||||
<td class="font-mono">
|
<td class="font-mono">
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<transition name="slide">
|
<transition name="slide">
|
||||||
<div class="w-full" v-show="showTracks">
|
<div class="w-full" v-show="showTracks">
|
||||||
<table class="text-sm tracksTable">
|
<table class="text-sm tracksTable">
|
||||||
<tr class="font-book">
|
<tr>
|
||||||
<th class="w-10">#</th>
|
<th class="w-10">#</th>
|
||||||
<th class="text-left">{{ $strings.LabelFilename }}</th>
|
<th class="text-left">{{ $strings.LabelFilename }}</th>
|
||||||
<th class="text-left w-20">{{ $strings.LabelSize }}</th>
|
<th class="text-left w-20">{{ $strings.LabelSize }}</th>
|
||||||
|
|||||||
@@ -11,20 +11,20 @@
|
|||||||
<transition name="slide">
|
<transition name="slide">
|
||||||
<div class="w-full" v-show="expand">
|
<div class="w-full" v-show="expand">
|
||||||
<table class="text-sm tracksTable">
|
<table class="text-sm tracksTable">
|
||||||
<tr class="font-book">
|
<tr>
|
||||||
<th class="text-left">{{ $strings.LabelFilename }}</th>
|
<th class="text-left">{{ $strings.LabelFilename }}</th>
|
||||||
<th class="text-left">{{ $strings.LabelSize }}</th>
|
<th class="text-left">{{ $strings.LabelSize }}</th>
|
||||||
<th class="text-left">{{ $strings.LabelType }}</th>
|
<th class="text-left">{{ $strings.LabelType }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
<template v-for="file in files">
|
<template v-for="file in files">
|
||||||
<tr :key="file.path">
|
<tr :key="file.path">
|
||||||
<td class="font-book pl-2">
|
<td class="pl-2">
|
||||||
{{ file.name }}
|
{{ file.name }}
|
||||||
</td>
|
</td>
|
||||||
<td class="font-mono">
|
<td class="font-mono">
|
||||||
{{ $bytesPretty(file.size) }}
|
{{ $bytesPretty(file.size) }}
|
||||||
</td>
|
</td>
|
||||||
<td class="font-book">
|
<td>
|
||||||
{{ file.filetype }}
|
{{ file.filetype }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -25,13 +25,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-xs font-mono hidden sm:table-cell">
|
<td class="text-xs font-mono hidden sm:table-cell">
|
||||||
<ui-tooltip v-if="user.lastSeen" direction="top" :text="$formatDate(user.lastSeen, 'MMMM do, yyyy HH:mm')">
|
<ui-tooltip v-if="user.lastSeen" direction="top" :text="$formatDatetime(user.lastSeen, dateFormat, timeFormat)">
|
||||||
{{ $dateDistanceFromNow(user.lastSeen) }}
|
{{ $dateDistanceFromNow(user.lastSeen) }}
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-xs font-mono hidden sm:table-cell">
|
<td class="text-xs font-mono hidden sm:table-cell">
|
||||||
<ui-tooltip direction="top" :text="$formatDate(user.createdAt, 'MMMM do, yyyy HH:mm')">
|
<ui-tooltip direction="top" :text="$formatDatetime(user.createdAt, dateFormat, timeFormat)">
|
||||||
{{ $formatDate(user.createdAt, 'MMM d, yyyy') }}
|
{{ $formatDate(user.createdAt, dateFormat) }}
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</td>
|
</td>
|
||||||
<td class="py-0">
|
<td class="py-0">
|
||||||
@@ -74,6 +74,12 @@ export default {
|
|||||||
var usermap = {}
|
var usermap = {}
|
||||||
this.$store.state.users.usersOnline.forEach((u) => (usermap[u.id] = u))
|
this.$store.state.users.usersOnline.forEach((u) => (usermap[u.id] = u))
|
||||||
return usermap
|
return usermap
|
||||||
|
},
|
||||||
|
dateFormat() {
|
||||||
|
return this.$store.state.serverSettings.dateFormat
|
||||||
|
},
|
||||||
|
timeFormat() {
|
||||||
|
return this.$store.state.serverSettings.timeFormat
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -201,4 +207,4 @@ export default {
|
|||||||
padding-bottom: 5px;
|
padding-bottom: 5px;
|
||||||
background-color: #272727;
|
background-color: #272727;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<svg v-else viewBox="0 0 24 24" class="h-6 w-6 text-white text-opacity-50 animate-spin">
|
<svg v-else viewBox="0 0 24 24" class="h-6 w-6 text-white text-opacity-50 animate-spin">
|
||||||
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
|
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
|
||||||
</svg>
|
</svg>
|
||||||
<p class="text-base md:text-xl font-book pl-2 md:pl-4 hover:underline cursor-pointer" @click.stop="$emit('click', library)">{{ library.name }}</p>
|
<p class="text-base md:text-xl pl-2 md:pl-4 hover:underline cursor-pointer" @click.stop="$emit('click', library)">{{ library.name }}</p>
|
||||||
|
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
<template>
|
||||||
|
<div class="w-full my-2">
|
||||||
|
<div class="w-full bg-primary px-4 md:px-6 py-2 flex items-center">
|
||||||
|
<p class="pr-2 md:pr-4">{{ $strings.HeaderDownloadQueue }}</p>
|
||||||
|
<div class="h-5 md:h-7 w-5 md:w-7 rounded-full bg-white bg-opacity-10 flex items-center justify-center">
|
||||||
|
<span class="text-sm font-mono">{{ queue.length }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<transition name="slide">
|
||||||
|
<div class="w-full">
|
||||||
|
<table class="text-sm tracksTable">
|
||||||
|
<tr>
|
||||||
|
<th class="text-left px-4 min-w-48">{{ $strings.LabelPodcast }}</th>
|
||||||
|
<th class="text-left w-32 min-w-32">{{ $strings.LabelEpisode }}</th>
|
||||||
|
<th class="text-left px-4">{{ $strings.LabelEpisodeTitle }}</th>
|
||||||
|
<th class="text-left px-4 w-48">{{ $strings.LabelPubDate }}</th>
|
||||||
|
</tr>
|
||||||
|
<template v-for="downloadQueued in queue">
|
||||||
|
<tr :key="downloadQueued.id">
|
||||||
|
<td class="px-4">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<nuxt-link :to="`/item/${downloadQueued.libraryItemId}`" class="text-sm text-gray-200 hover:underline">{{ downloadQueued.podcastTitle }}</nuxt-link>
|
||||||
|
<widgets-explicit-indicator :explicit="downloadQueued.podcastExplicit" />
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div v-if="downloadQueued.season">{{ downloadQueued.season }}x</div>
|
||||||
|
<div v-if="downloadQueued.episode">{{ downloadQueued.episode }}</div>
|
||||||
|
<widgets-podcast-type-indicator :type="downloadQueued.episodeType" />
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-4">
|
||||||
|
{{ downloadQueued.episodeDisplayTitle }}
|
||||||
|
</td>
|
||||||
|
<td class="text-xs">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<p>{{ $dateDistanceFromNow(downloadQueued.publishedAt) }}</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
queue: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
libraryItemId: String
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
computed: {},
|
||||||
|
methods: {},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -2,16 +2,17 @@
|
|||||||
<div class="w-full px-2 py-3 overflow-hidden relative border-b border-white border-opacity-10" @mouseover="mouseover" @mouseleave="mouseleave">
|
<div class="w-full px-2 py-3 overflow-hidden relative border-b border-white border-opacity-10" @mouseover="mouseover" @mouseleave="mouseleave">
|
||||||
<div v-if="episode" class="flex items-center cursor-pointer" :class="{ 'opacity-70': isSelected || selectionMode }" @click="clickedEpisode">
|
<div v-if="episode" class="flex items-center cursor-pointer" :class="{ 'opacity-70': isSelected || selectionMode }" @click="clickedEpisode">
|
||||||
<div class="flex-grow px-2">
|
<div class="flex-grow px-2">
|
||||||
<p class="text-sm font-semibold">
|
<div class="flex items-center">
|
||||||
{{ title }}
|
<span class="text-sm font-semibold">{{ title }}</span>
|
||||||
</p>
|
<widgets-podcast-type-indicator :type="episode.episodeType" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<p class="text-sm text-gray-200 episode-subtitle mt-1.5 mb-0.5">{{ subtitle }}</p>
|
<p class="text-sm text-gray-200 episode-subtitle mt-1.5 mb-0.5">{{ subtitle }}</p>
|
||||||
|
|
||||||
<div class="flex justify-between pt-2 max-w-xl">
|
<div class="flex justify-between pt-2 max-w-xl">
|
||||||
<p v-if="episode.season" class="text-sm text-gray-300">Season #{{ episode.season }}</p>
|
<p v-if="episode.season" class="text-sm text-gray-300">Season #{{ episode.season }}</p>
|
||||||
<p v-if="episode.episode" class="text-sm text-gray-300">Episode #{{ episode.episode }}</p>
|
<p v-if="episode.episode" class="text-sm text-gray-300">Episode #{{ episode.episode }}</p>
|
||||||
<p v-if="publishedAt" class="text-sm text-gray-300">Published {{ $formatDate(publishedAt, 'MMM do, yyyy') }}</p>
|
<p v-if="publishedAt" class="text-sm text-gray-300">Published {{ $formatDate(publishedAt, dateFormat) }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center pt-2">
|
<div class="flex items-center pt-2">
|
||||||
@@ -128,6 +129,9 @@ export default {
|
|||||||
},
|
},
|
||||||
publishedAt() {
|
publishedAt() {
|
||||||
return this.episode.publishedAt
|
return this.episode.publishedAt
|
||||||
|
},
|
||||||
|
dateFormat() {
|
||||||
|
return this.$store.state.serverSettings.dateFormat
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -205,4 +209,4 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -19,7 +19,12 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="!episodes.length" class="py-4 text-center text-lg">{{ $strings.MessageNoEpisodes }}</p>
|
<p v-if="!episodes.length" class="py-4 text-center text-lg">{{ $strings.MessageNoEpisodes }}</p>
|
||||||
<template v-for="episode in episodesSorted">
|
<div v-if="episodes.length" class="w-full py-3 mx-auto flex">
|
||||||
|
<form @submit.prevent="submit" class="flex flex-grow">
|
||||||
|
<ui-text-input v-model="search" @input="inputUpdate" type="search" :placeholder="$strings.PlaceholderSearchEpisode" class="flex-grow mr-2 text-sm md:text-base" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<template v-for="episode in episodesList">
|
||||||
<tables-podcast-episode-table-row ref="episodeRow" :key="episode.id" :episode="episode" :library-item-id="libraryItem.id" :selection-mode="isSelectionMode" class="item" @play="playEpisode" @remove="removeEpisode" @edit="editEpisode" @view="viewEpisode" @selected="episodeSelected" @addToQueue="addEpisodeToQueue" @addToPlaylist="addToPlaylist" />
|
<tables-podcast-episode-table-row ref="episodeRow" :key="episode.id" :episode="episode" :library-item-id="libraryItem.id" :selection-mode="isSelectionMode" class="item" @play="playEpisode" @remove="removeEpisode" @edit="editEpisode" @view="viewEpisode" @selected="episodeSelected" @addToQueue="addEpisodeToQueue" @addToPlaylist="addToPlaylist" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -46,7 +51,10 @@ export default {
|
|||||||
selectedEpisodes: [],
|
selectedEpisodes: [],
|
||||||
episodesToRemove: [],
|
episodesToRemove: [],
|
||||||
processing: false,
|
processing: false,
|
||||||
quickMatchingEpisodes: false
|
quickMatchingEpisodes: false,
|
||||||
|
search: null,
|
||||||
|
searchTimeout: null,
|
||||||
|
searchText: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -137,15 +145,40 @@ export default {
|
|||||||
return String(a[this.sortKey]).localeCompare(String(b[this.sortKey]), undefined, { numeric: true, sensitivity: 'base' })
|
return String(a[this.sortKey]).localeCompare(String(b[this.sortKey]), undefined, { numeric: true, sensitivity: 'base' })
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
episodesList() {
|
||||||
|
return this.episodesSorted.filter((episode) => {
|
||||||
|
if (!this.searchText) return true
|
||||||
|
return (
|
||||||
|
(episode.title && episode.title.toLowerCase().includes(this.searchText)) ||
|
||||||
|
(episode.subtitle && episode.subtitle.toLowerCase().includes(this.searchText))
|
||||||
|
)
|
||||||
|
})
|
||||||
|
},
|
||||||
selectedIsFinished() {
|
selectedIsFinished() {
|
||||||
// Find an item that is not finished, if none then all items finished
|
// Find an item that is not finished, if none then all items finished
|
||||||
return !this.selectedEpisodes.find((episode) => {
|
return !this.selectedEpisodes.find((episode) => {
|
||||||
var itemProgress = this.$store.getters['user/getUserMediaProgress'](this.libraryItem.id, episode.id)
|
var itemProgress = this.$store.getters['user/getUserMediaProgress'](this.libraryItem.id, episode.id)
|
||||||
return !itemProgress || !itemProgress.isFinished
|
return !itemProgress || !itemProgress.isFinished
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
dateFormat() {
|
||||||
|
return this.$store.state.serverSettings.dateFormat
|
||||||
|
},
|
||||||
|
timeFormat() {
|
||||||
|
return this.$store.state.serverSettings.timeFormat
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
inputUpdate() {
|
||||||
|
clearTimeout(this.searchTimeout)
|
||||||
|
this.searchTimeout = setTimeout(() => {
|
||||||
|
if (!this.search || !this.search.trim()) {
|
||||||
|
this.searchText = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.searchText = this.search.toLowerCase().trim()
|
||||||
|
}, 500)
|
||||||
|
},
|
||||||
contextMenuAction(action) {
|
contextMenuAction(action) {
|
||||||
if (action === 'quick-match-episodes') {
|
if (action === 'quick-match-episodes') {
|
||||||
if (this.quickMatchingEpisodes) return
|
if (this.quickMatchingEpisodes) return
|
||||||
@@ -195,7 +228,7 @@ export default {
|
|||||||
episodeId: episode.id,
|
episodeId: episode.id,
|
||||||
title: episode.title,
|
title: episode.title,
|
||||||
subtitle: this.mediaMetadata.title,
|
subtitle: this.mediaMetadata.title,
|
||||||
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, 'MMM do, yyyy')}` : 'Unknown publish date',
|
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date',
|
||||||
duration: episode.audioFile.duration || null,
|
duration: episode.audioFile.duration || null,
|
||||||
coverPath: this.media.coverPath || null
|
coverPath: this.media.coverPath || null
|
||||||
}
|
}
|
||||||
@@ -263,7 +296,7 @@ export default {
|
|||||||
episodeId: episode.id,
|
episodeId: episode.id,
|
||||||
title: episode.title,
|
title: episode.title,
|
||||||
subtitle: this.mediaMetadata.title,
|
subtitle: this.mediaMetadata.title,
|
||||||
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, 'MMM do, yyyy')}` : 'Unknown publish date',
|
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date',
|
||||||
duration: episode.audioFile.duration || null,
|
duration: episode.audioFile.duration || null,
|
||||||
coverPath: this.media.coverPath || null
|
coverPath: this.media.coverPath || null
|
||||||
})
|
})
|
||||||
@@ -281,6 +314,8 @@ export default {
|
|||||||
this.showPodcastRemoveModal = true
|
this.showPodcastRemoveModal = true
|
||||||
},
|
},
|
||||||
editEpisode(episode) {
|
editEpisode(episode) {
|
||||||
|
const episodeIds = this.episodesSorted.map((e) => e.id)
|
||||||
|
this.$store.commit('setEpisodeTableEpisodeIds', episodeIds)
|
||||||
this.$store.commit('setSelectedLibraryItem', this.libraryItem)
|
this.$store.commit('setSelectedLibraryItem', this.libraryItem)
|
||||||
this.$store.commit('globals/setSelectedEpisode', episode)
|
this.$store.commit('globals/setSelectedEpisode', episode)
|
||||||
this.$store.commit('globals/setShowEditPodcastEpisodeModal', true)
|
this.$store.commit('globals/setShowEditPodcastEpisodeModal', true)
|
||||||
@@ -314,4 +349,4 @@ export default {
|
|||||||
.episode-leave-active {
|
.episode-leave-active {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<input v-model="selected" :disabled="disabled" type="checkbox" class="opacity-0 absolute" :class="!disabled ? 'cursor-pointer' : ''" />
|
<input v-model="selected" :disabled="disabled" type="checkbox" class="opacity-0 absolute" :class="!disabled ? 'cursor-pointer' : ''" />
|
||||||
<svg v-if="selected" class="fill-current pointer-events-none" :class="svgClass" viewBox="0 0 20 20"><path d="M0 11l2-2 5 5L18 3l2 2L7 18z" /></svg>
|
<svg v-if="selected" class="fill-current pointer-events-none" :class="svgClass" viewBox="0 0 20 20"><path d="M0 11l2-2 5 5L18 3l2 2L7 18z" /></svg>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="label" class="select-none text-gray-100" :class="labelClassname">{{ label }}</div>
|
<div v-if="label" class="select-none" :class="[labelClassname, disabled ? 'text-gray-400' : 'text-gray-100']">{{ label }}</div>
|
||||||
</label>
|
</label>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -68,8 +68,6 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {},
|
mounted() {},
|
||||||
beforeDestroy() {
|
beforeDestroy() {}
|
||||||
console.log('Before destroy')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -51,8 +51,8 @@ export default {
|
|||||||
tooltip.style.zIndex = 100
|
tooltip.style.zIndex = 100
|
||||||
tooltip.style.backgroundColor = 'rgba(0,0,0,0.85)'
|
tooltip.style.backgroundColor = 'rgba(0,0,0,0.85)'
|
||||||
tooltip.innerHTML = this.text
|
tooltip.innerHTML = this.text
|
||||||
tooltip.addEventListener('mouseover', this.cancelHide);
|
tooltip.addEventListener('mouseover', this.cancelHide)
|
||||||
tooltip.addEventListener('mouseleave', this.hideTooltip);
|
tooltip.addEventListener('mouseleave', this.hideTooltip)
|
||||||
|
|
||||||
this.setTooltipPosition(tooltip)
|
this.setTooltipPosition(tooltip)
|
||||||
|
|
||||||
@@ -107,7 +107,7 @@ export default {
|
|||||||
this.isShowing = false
|
this.isShowing = false
|
||||||
},
|
},
|
||||||
cancelHide() {
|
cancelHide() {
|
||||||
if (this.hideTimeout) clearTimeout(this.hideTimeout);
|
if (this.hideTimeout) clearTimeout(this.hideTimeout)
|
||||||
},
|
},
|
||||||
mouseover() {
|
mouseover() {
|
||||||
if (!this.isShowing) this.showTooltip()
|
if (!this.isShowing) this.showTooltip()
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<template>
|
||||||
|
<ui-tooltip v-if="alreadyInLibrary" :text="$strings.LabelAlreadyInYourLibrary" direction="top">
|
||||||
|
<span class="material-icons ml-1 text-success" style="font-size: 0.8rem">check_circle</span>
|
||||||
|
</ui-tooltip>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
alreadyInLibrary: Boolean
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
computed: {},
|
||||||
|
methods: {},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap mt-2 -mx-1">
|
<div class="flex flex-wrap mt-2 -mx-1">
|
||||||
<div class="w-full md:w-1/2 px-1">
|
<div class="w-full md:w-1/4 px-1">
|
||||||
<ui-text-input-with-label ref="publisherInput" v-model="details.publisher" :label="$strings.LabelPublisher" />
|
<ui-text-input-with-label ref="publisherInput" v-model="details.publisher" :label="$strings.LabelPublisher" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-1/2 md:w-1/4 px-1 mt-2 md:mt-0">
|
<div class="w-1/2 md:w-1/4 px-1 mt-2 md:mt-0">
|
||||||
@@ -61,6 +61,11 @@
|
|||||||
<ui-checkbox v-model="details.explicit" :label="$strings.LabelExplicit" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" />
|
<ui-checkbox v-model="details.explicit" :label="$strings.LabelExplicit" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex-grow px-1 pt-6 mt-2 md:mt-0">
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<ui-checkbox v-model="details.abridged" :label="$strings.LabelAbridged" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -89,7 +94,8 @@ export default {
|
|||||||
isbn: null,
|
isbn: null,
|
||||||
asin: null,
|
asin: null,
|
||||||
genres: [],
|
genres: [],
|
||||||
explicit: false
|
explicit: false,
|
||||||
|
abridged: false
|
||||||
},
|
},
|
||||||
newTags: []
|
newTags: []
|
||||||
}
|
}
|
||||||
@@ -271,6 +277,7 @@ export default {
|
|||||||
this.details.isbn = this.mediaMetadata.isbn || null
|
this.details.isbn = this.mediaMetadata.isbn || null
|
||||||
this.details.asin = this.mediaMetadata.asin || null
|
this.details.asin = this.mediaMetadata.asin || null
|
||||||
this.details.explicit = !!this.mediaMetadata.explicit
|
this.details.explicit = !!this.mediaMetadata.explicit
|
||||||
|
this.details.abridged = !!this.mediaMetadata.abridged
|
||||||
this.newTags = [...(this.media.tags || [])]
|
this.newTags = [...(this.media.tags || [])]
|
||||||
},
|
},
|
||||||
submitForm() {
|
submitForm() {
|
||||||
|
|||||||
@@ -36,6 +36,10 @@
|
|||||||
<p v-else class="text-success text-base md:text-lg text-center">{{ $strings.MessageValidCronExpression }}</p>
|
<p v-else class="text-success text-base md:text-lg text-center">{{ $strings.MessageValidCronExpression }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
<div v-if="cronExpression && isValid" class="flex items-center justify-center text-yellow-400 mt-2">
|
||||||
|
<span class="material-icons-outlined mr-2 text-xl">event</span>
|
||||||
|
<p>{{ $strings.LabelNextScheduledRun }}: {{ nextRun }}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -63,6 +67,14 @@ export default {
|
|||||||
isValid: true
|
isValid: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
value: {
|
||||||
|
immediate: true,
|
||||||
|
handler(newVal) {
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
minuteIsValid() {
|
minuteIsValid() {
|
||||||
return !(isNaN(this.selectedMinute) || this.selectedMinute === '' || this.selectedMinute < 0 || this.selectedMinute > 59)
|
return !(isNaN(this.selectedMinute) || this.selectedMinute === '' || this.selectedMinute < 0 || this.selectedMinute > 59)
|
||||||
@@ -70,6 +82,11 @@ export default {
|
|||||||
hourIsValid() {
|
hourIsValid() {
|
||||||
return !(isNaN(this.selectedHour) || this.selectedHour === '' || this.selectedHour < 0 || this.selectedHour > 23)
|
return !(isNaN(this.selectedHour) || this.selectedHour === '' || this.selectedHour < 0 || this.selectedHour > 23)
|
||||||
},
|
},
|
||||||
|
nextRun() {
|
||||||
|
if (!this.cronExpression) return ''
|
||||||
|
const parsed = this.$getNextScheduledDate(this.cronExpression)
|
||||||
|
return this.$formatJsDatetime(parsed, this.$store.state.serverSettings.dateFormat, this.$store.state.serverSettings.timeFormat) || ''
|
||||||
|
},
|
||||||
description() {
|
description() {
|
||||||
if ((this.selectedInterval !== 'custom' || !this.selectedWeekdays.length) && this.selectedInterval !== 'daily') return ''
|
if ((this.selectedInterval !== 'custom' || !this.selectedWeekdays.length) && this.selectedInterval !== 'daily') return ''
|
||||||
|
|
||||||
@@ -271,6 +288,11 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
init() {
|
init() {
|
||||||
|
this.selectedInterval = 'custom'
|
||||||
|
this.selectedHour = 0
|
||||||
|
this.selectedMinute = 0
|
||||||
|
this.selectedWeekdays = []
|
||||||
|
|
||||||
if (!this.value) return
|
if (!this.value) return
|
||||||
const pieces = this.value.split(' ')
|
const pieces = this.value.split(' ')
|
||||||
if (pieces.length !== 5) {
|
if (pieces.length !== 5) {
|
||||||
@@ -309,4 +331,4 @@ export default {
|
|||||||
this.init()
|
this.init()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<template>
|
||||||
|
<ui-tooltip v-if="explicit" :text="$strings.LabelExplicit" direction="top">
|
||||||
|
<span class="material-icons ml-1" style="font-size: 0.8rem">explicit</span>
|
||||||
|
</ui-tooltip>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
explicit: Boolean
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
computed: {},
|
||||||
|
methods: {},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,15 +1,51 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="tasksRunning" class="w-4 h-4 mx-3 relative">
|
<div v-if="tasksRunning" class="w-4 h-4 mx-3 relative" v-click-outside="clickOutsideObj">
|
||||||
<div class="flex h-full items-center justify-center">
|
<button type="button" :disabled="disabled" class="w-10 sm:w-full relative h-full cursor-pointer" aria-haspopup="listbox" :aria-expanded="showMenu" @click.stop.prevent="clickShowMenu">
|
||||||
<widgets-loading-spinner />
|
<div class="flex h-full items-center justify-center">
|
||||||
</div>
|
<ui-tooltip text="Tasks running" direction="bottom" class="flex items-center">
|
||||||
|
<widgets-loading-spinner />
|
||||||
|
</ui-tooltip>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<transition name="menu">
|
||||||
|
<div class="sm:w-80 w-full relative">
|
||||||
|
<div v-show="showMenu" class="absolute z-40 -mt-px w-40 sm:w-full bg-bg border border-black-200 shadow-lg rounded-md py-1 px-2 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm globalTaskRunningMenu">
|
||||||
|
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
||||||
|
<template v-if="tasksRunningOrFailed.length">
|
||||||
|
<p class="uppercase text-xs text-gray-400 my-1 px-1 font-semibold">{{ $strings.LabelTasks }}</p>
|
||||||
|
<template v-for="task in tasksRunningOrFailed">
|
||||||
|
<nuxt-link :key="task.id" v-if="actionLink(task)" :to="actionLink(task)">
|
||||||
|
<li class="text-gray-50 select-none relative hover:bg-black-400 py-1 cursor-pointer">
|
||||||
|
<cards-item-task-running-card :task="task" />
|
||||||
|
</li>
|
||||||
|
</nuxt-link>
|
||||||
|
<li v-else :key="task.id" class="text-gray-50 select-none relative hover:bg-black-400 py-1">
|
||||||
|
<cards-item-task-running-card :task="task" />
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
<li v-else class="py-2 px-2">
|
||||||
|
<p>{{ $strings.MessageNoTasksRunning }}</p>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {
|
||||||
|
clickOutsideObj: {
|
||||||
|
handler: this.clickedOutside,
|
||||||
|
events: ['mousedown'],
|
||||||
|
isActive: true
|
||||||
|
},
|
||||||
|
showMenu: false,
|
||||||
|
disabled: false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
tasks() {
|
tasks() {
|
||||||
@@ -17,9 +53,39 @@ export default {
|
|||||||
},
|
},
|
||||||
tasksRunning() {
|
tasksRunning() {
|
||||||
return this.tasks.some((t) => !t.isFinished)
|
return this.tasks.some((t) => !t.isFinished)
|
||||||
|
},
|
||||||
|
tasksRunningOrFailed() {
|
||||||
|
// return just the tasks that are running or failed in the last 1 minute
|
||||||
|
return this.tasks.filter((t) => !t.isFinished || (t.isFailed && t.finishedAt > new Date().getTime() - 1000 * 60)) || []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
clickShowMenu() {
|
||||||
|
if (this.disabled) return
|
||||||
|
this.showMenu = !this.showMenu
|
||||||
|
},
|
||||||
|
clickedOutside() {
|
||||||
|
this.showMenu = false
|
||||||
|
},
|
||||||
|
actionLink(task) {
|
||||||
|
switch (task.action) {
|
||||||
|
case 'download-podcast-episode':
|
||||||
|
return `/library/${task.data.libraryId}/podcast/download-queue`
|
||||||
|
case 'encode-m4b':
|
||||||
|
return `/audiobook/${task.data.libraryItemId}/manage?tool=m4b`
|
||||||
|
case 'embed-metadata':
|
||||||
|
return `/audiobook/${task.data.libraryItemId}/manage?tool=embed`
|
||||||
|
default:
|
||||||
|
return ''
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {},
|
|
||||||
mounted() {}
|
mounted() {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.globalTaskRunningMenu {
|
||||||
|
max-height: 80vh;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -39,6 +39,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex mt-2 -mx-1">
|
||||||
|
<div class="w-1/4 px-1">
|
||||||
|
<ui-dropdown :label="$strings.LabelPodcastType" v-model="details.type" :items="podcastTypes" small class="max-w-52" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -65,7 +70,8 @@ export default {
|
|||||||
itunesId: null,
|
itunesId: null,
|
||||||
itunesArtistId: null,
|
itunesArtistId: null,
|
||||||
explicit: false,
|
explicit: false,
|
||||||
language: null
|
language: null,
|
||||||
|
type: null
|
||||||
},
|
},
|
||||||
newTags: []
|
newTags: []
|
||||||
}
|
}
|
||||||
@@ -93,6 +99,9 @@ export default {
|
|||||||
},
|
},
|
||||||
filterData() {
|
filterData() {
|
||||||
return this.$store.state.libraries.filterData || {}
|
return this.$store.state.libraries.filterData || {}
|
||||||
|
},
|
||||||
|
podcastTypes() {
|
||||||
|
return this.$store.state.globals.podcastTypes || []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -219,6 +228,7 @@ export default {
|
|||||||
this.details.itunesArtistId = this.mediaMetadata.itunesArtistId || ''
|
this.details.itunesArtistId = this.mediaMetadata.itunesArtistId || ''
|
||||||
this.details.language = this.mediaMetadata.language || ''
|
this.details.language = this.mediaMetadata.language || ''
|
||||||
this.details.explicit = !!this.mediaMetadata.explicit
|
this.details.explicit = !!this.mediaMetadata.explicit
|
||||||
|
this.details.type = this.mediaMetadata.type || 'episodic'
|
||||||
|
|
||||||
this.newTags = [...(this.media.tags || [])]
|
this.newTags = [...(this.media.tags || [])]
|
||||||
},
|
},
|
||||||
@@ -228,4 +238,4 @@ export default {
|
|||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<template v-if="type == 'bonus'">
|
||||||
|
<ui-tooltip text="Bonus" direction="top">
|
||||||
|
<span class="material-icons ml-1" style="font-size: 0.8rem">local_play</span>
|
||||||
|
</ui-tooltip>
|
||||||
|
</template>
|
||||||
|
<template v-if="type == 'trailer'">
|
||||||
|
<ui-tooltip text="Trailer" direction="top">
|
||||||
|
<span class="material-icons ml-1" style="font-size: 0.8rem">local_movies</span>
|
||||||
|
</ui-tooltip>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: 'full'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
computed: {},
|
||||||
|
methods: {},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
<template>
|
||||||
|
<div class="w-full py-2">
|
||||||
|
<div class="flex -mb-px">
|
||||||
|
<div class="w-1/2 h-6 rounded-tl-md relative border border-black-200 flex items-center justify-center cursor-pointer" :class="!showAdvancedView ? 'text-white bg-bg hover:bg-opacity-60 border-b-bg' : 'text-gray-400 hover:text-gray-300 bg-primary bg-opacity-70 hover:bg-opacity-60'" @click="showAdvancedView = false">
|
||||||
|
<p class="text-sm">{{ $strings.HeaderRSSFeedGeneral }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="w-1/2 h-6 rounded-tr-md relative border border-black-200 flex items-center justify-center -ml-px cursor-pointer" :class="showAdvancedView ? 'text-white bg-bg hover:bg-opacity-60 border-b-bg' : 'text-gray-400 hover:text-gray-300 bg-primary bg-opacity-70 hover:bg-opacity-60'" @click="showAdvancedView = true">
|
||||||
|
<p class="text-sm">{{ $strings.HeaderAdvanced }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="px-2 py-4 md:p-4 border border-black-200 rounded-b-md mr-px" style="min-height: 200px">
|
||||||
|
<template v-if="!showAdvancedView">
|
||||||
|
<div class="flex-grow pt-2 mb-2">
|
||||||
|
<ui-checkbox v-model="preventIndexing" :label="$strings.LabelPreventIndexing" checkbox-bg="primary" border-color="gray-600" label-class="pl-2" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="flex-grow pt-2 mb-2">
|
||||||
|
<ui-checkbox v-model="preventIndexing" :label="$strings.LabelPreventIndexing" checkbox-bg="primary" border-color="gray-600" label-class="pl-2" />
|
||||||
|
</div>
|
||||||
|
<div class="w-full relative mb-1">
|
||||||
|
<ui-text-input-with-label v-model="ownerName" :label="$strings.LabelRSSFeedCustomOwnerName" />
|
||||||
|
</div>
|
||||||
|
<div class="w-full relative mb-1">
|
||||||
|
<ui-text-input-with-label v-model="ownerEmail" :label="$strings.LabelRSSFeedCustomOwnerEmail" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {
|
||||||
|
return {
|
||||||
|
preventIndexing: true,
|
||||||
|
ownerName: '',
|
||||||
|
ownerEmail: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showAdvancedView: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {},
|
||||||
|
computed: {
|
||||||
|
preventIndexing: {
|
||||||
|
get() {
|
||||||
|
return this.value.preventIndexing
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.$emit('input', {
|
||||||
|
...this.value,
|
||||||
|
preventIndexing: value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ownerName: {
|
||||||
|
get() {
|
||||||
|
return this.value.ownerName
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.$emit('input', {
|
||||||
|
...this.value,
|
||||||
|
ownerName: value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ownerEmail: {
|
||||||
|
get() {
|
||||||
|
return this.value.ownerEmail
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.$emit('input', {
|
||||||
|
...this.value,
|
||||||
|
ownerEmail: value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -278,6 +278,13 @@ export default {
|
|||||||
console.log('Task finished', task)
|
console.log('Task finished', task)
|
||||||
this.$store.commit('tasks/addUpdateTask', task)
|
this.$store.commit('tasks/addUpdateTask', task)
|
||||||
},
|
},
|
||||||
|
metadataEmbedQueueUpdate(data) {
|
||||||
|
if (data.queued) {
|
||||||
|
this.$store.commit('tasks/addQueuedEmbedLId', data.libraryItemId)
|
||||||
|
} else {
|
||||||
|
this.$store.commit('tasks/removeQueuedEmbedLId', data.libraryItemId)
|
||||||
|
}
|
||||||
|
},
|
||||||
userUpdated(user) {
|
userUpdated(user) {
|
||||||
if (this.$store.state.user.user.id === user.id) {
|
if (this.$store.state.user.user.id === user.id) {
|
||||||
this.$store.commit('user/setUser', user)
|
this.$store.commit('user/setUser', user)
|
||||||
@@ -418,6 +425,7 @@ export default {
|
|||||||
// Task Listeners
|
// Task Listeners
|
||||||
this.socket.on('task_started', this.taskStarted)
|
this.socket.on('task_started', this.taskStarted)
|
||||||
this.socket.on('task_finished', this.taskFinished)
|
this.socket.on('task_finished', this.taskFinished)
|
||||||
|
this.socket.on('metadata_embed_queue_update', this.metadataEmbedQueueUpdate)
|
||||||
|
|
||||||
this.socket.on('backup_applied', this.backupApplied)
|
this.socket.on('backup_applied', this.backupApplied)
|
||||||
|
|
||||||
@@ -531,12 +539,18 @@ export default {
|
|||||||
},
|
},
|
||||||
loadTasks() {
|
loadTasks() {
|
||||||
this.$axios
|
this.$axios
|
||||||
.$get('/api/tasks')
|
.$get('/api/tasks?include=queue')
|
||||||
.then((payload) => {
|
.then((payload) => {
|
||||||
console.log('Fetched tasks', payload)
|
console.log('Fetched tasks', payload)
|
||||||
if (payload.tasks) {
|
if (payload.tasks) {
|
||||||
this.$store.commit('tasks/setTasks', payload.tasks)
|
this.$store.commit('tasks/setTasks', payload.tasks)
|
||||||
}
|
}
|
||||||
|
if (payload.queuedTaskData?.embedMetadata?.length) {
|
||||||
|
this.$store.commit(
|
||||||
|
'tasks/setQueuedEmbedLIds',
|
||||||
|
payload.queuedTaskData.embedMetadata.map((td) => td.libraryItemId)
|
||||||
|
)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to load tasks', error)
|
console.error('Failed to load tasks', error)
|
||||||
|
|||||||
Generated
+35
-2
@@ -1,17 +1,18 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.2.14",
|
"version": "2.2.18",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.2.14",
|
"version": "2.2.18",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxtjs/axios": "^5.13.6",
|
"@nuxtjs/axios": "^5.13.6",
|
||||||
"@nuxtjs/proxy": "^2.1.0",
|
"@nuxtjs/proxy": "^2.1.0",
|
||||||
"core-js": "^3.16.0",
|
"core-js": "^3.16.0",
|
||||||
|
"cron-parser": "^4.7.1",
|
||||||
"date-fns": "^2.25.0",
|
"date-fns": "^2.25.0",
|
||||||
"epubjs": "^0.3.88",
|
"epubjs": "^0.3.88",
|
||||||
"hls.js": "^1.0.7",
|
"hls.js": "^1.0.7",
|
||||||
@@ -5464,6 +5465,17 @@
|
|||||||
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||||
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="
|
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/cron-parser": {
|
||||||
|
"version": "4.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.7.1.tgz",
|
||||||
|
"integrity": "sha512-WguFaoQ0hQ61SgsCZLHUcNbAvlK0lypKXu62ARguefYmjzaOXIVRNrAmyXzabTwUn4sQvQLkk6bjH+ipGfw8bA==",
|
||||||
|
"dependencies": {
|
||||||
|
"luxon": "^3.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.3",
|
"version": "7.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||||
@@ -9134,6 +9146,14 @@
|
|||||||
"yallist": "^3.0.2"
|
"yallist": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/luxon": {
|
||||||
|
"version": "3.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.2.1.tgz",
|
||||||
|
"integrity": "sha512-QrwPArQCNLAKGO/C+ZIilgIuDnEnKx5QYODdDtbFaxzsbZcc/a7WFq7MhsVYgRlwawLtvOUESTlfJ+hc/USqPg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/make-dir": {
|
"node_modules/make-dir": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
|
||||||
@@ -21582,6 +21602,14 @@
|
|||||||
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||||
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="
|
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="
|
||||||
},
|
},
|
||||||
|
"cron-parser": {
|
||||||
|
"version": "4.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.7.1.tgz",
|
||||||
|
"integrity": "sha512-WguFaoQ0hQ61SgsCZLHUcNbAvlK0lypKXu62ARguefYmjzaOXIVRNrAmyXzabTwUn4sQvQLkk6bjH+ipGfw8bA==",
|
||||||
|
"requires": {
|
||||||
|
"luxon": "^3.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"cross-spawn": {
|
"cross-spawn": {
|
||||||
"version": "7.0.3",
|
"version": "7.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||||
@@ -24397,6 +24425,11 @@
|
|||||||
"yallist": "^3.0.2"
|
"yallist": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"luxon": {
|
||||||
|
"version": "3.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.2.1.tgz",
|
||||||
|
"integrity": "sha512-QrwPArQCNLAKGO/C+ZIilgIuDnEnKx5QYODdDtbFaxzsbZcc/a7WFq7MhsVYgRlwawLtvOUESTlfJ+hc/USqPg=="
|
||||||
|
},
|
||||||
"make-dir": {
|
"make-dir": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.2.14",
|
"version": "2.2.18",
|
||||||
"description": "Self-hosted audiobook and podcast client",
|
"description": "Self-hosted audiobook and podcast client",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
"@nuxtjs/axios": "^5.13.6",
|
"@nuxtjs/axios": "^5.13.6",
|
||||||
"@nuxtjs/proxy": "^2.1.0",
|
"@nuxtjs/proxy": "^2.1.0",
|
||||||
"core-js": "^3.16.0",
|
"core-js": "^3.16.0",
|
||||||
|
"cron-parser": "^4.7.1",
|
||||||
"date-fns": "^2.25.0",
|
"date-fns": "^2.25.0",
|
||||||
"epubjs": "^0.3.88",
|
"epubjs": "^0.3.88",
|
||||||
"hls.js": "^1.0.7",
|
"hls.js": "^1.0.7",
|
||||||
|
|||||||
@@ -136,7 +136,7 @@
|
|||||||
<modals-modal v-model="showFindChaptersModal" name="edit-book" :width="500" :processing="findingChapters">
|
<modals-modal v-model="showFindChaptersModal" name="edit-book" :width="500" :processing="findingChapters">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden pointer-events-none">
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden pointer-events-none">
|
||||||
<p class="font-book text-3xl text-white truncate pointer-events-none">{{ $strings.HeaderFindChapters }}</p>
|
<p class="text-3xl text-white truncate pointer-events-none">{{ $strings.HeaderFindChapters }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="w-full h-full max-h-full text-sm rounded-lg bg-bg shadow-lg border border-black-300 relative">
|
<div class="w-full h-full max-h-full text-sm rounded-lg bg-bg shadow-lg border border-black-300 relative">
|
||||||
|
|||||||
@@ -37,23 +37,23 @@
|
|||||||
<draggable v-model="files" v-bind="dragOptions" class="list-group border border-gray-600" draggable=".item" tag="ul" @start="drag = true" @end="drag = false" @update="draggableUpdate">
|
<draggable v-model="files" v-bind="dragOptions" class="list-group border border-gray-600" draggable=".item" tag="ul" @start="drag = true" @end="drag = false" @update="draggableUpdate">
|
||||||
<transition-group type="transition" :name="!drag ? 'flip-list' : null">
|
<transition-group type="transition" :name="!drag ? 'flip-list' : null">
|
||||||
<li v-for="(audio, index) in files" :key="audio.ino" :class="audio.include ? 'item' : 'exclude'" class="w-full list-group-item flex items-center relative">
|
<li v-for="(audio, index) in files" :key="audio.ino" :class="audio.include ? 'item' : 'exclude'" class="w-full list-group-item flex items-center relative">
|
||||||
<div class="font-book text-center px-4 py-1 w-12 min-w-12">
|
<div class="text-center px-4 py-1 w-12 min-w-12">
|
||||||
{{ audio.include ? index - numExcluded + 1 : -1 }}
|
{{ audio.include ? index - numExcluded + 1 : -1 }}
|
||||||
</div>
|
</div>
|
||||||
<div class="font-book text-center px-4 w-24 min-w-24">{{ audio.index }}</div>
|
<div class="text-center px-4 w-24 min-w-24">{{ audio.index }}</div>
|
||||||
<div class="font-book text-center px-2 w-32 min-w-32">
|
<div class="text-center px-2 w-32 min-w-32">
|
||||||
{{ audio.trackNumFromFilename }}
|
{{ audio.trackNumFromFilename }}
|
||||||
</div>
|
</div>
|
||||||
<div class="font-book text-center w-32 min-w-32">
|
<div class="text-center w-32 min-w-32">
|
||||||
{{ audio.trackNumFromMeta }}
|
{{ audio.trackNumFromMeta }}
|
||||||
</div>
|
</div>
|
||||||
<div class="font-book truncate px-4 w-20 min-w-20">
|
<div class="truncate px-4 w-20 min-w-20">
|
||||||
{{ audio.discNumFromFilename }}
|
{{ audio.discNumFromFilename }}
|
||||||
</div>
|
</div>
|
||||||
<div class="font-book truncate px-4 w-20 min-w-20">
|
<div class="truncate px-4 w-20 min-w-20">
|
||||||
{{ audio.discNumFromMeta }}
|
{{ audio.discNumFromMeta }}
|
||||||
</div>
|
</div>
|
||||||
<div class="font-book truncate px-4 flex-grow">
|
<div class="truncate px-4 flex-grow">
|
||||||
{{ audio.metadata.filename }}
|
{{ audio.metadata.filename }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -62,14 +62,20 @@
|
|||||||
<div class="w-full h-px bg-white bg-opacity-10 my-8" />
|
<div class="w-full h-px bg-white bg-opacity-10 my-8" />
|
||||||
|
|
||||||
<div class="w-full max-w-4xl mx-auto">
|
<div class="w-full max-w-4xl mx-auto">
|
||||||
<div v-if="isEmbedTool" class="w-full flex justify-end items-center mb-4">
|
<!-- queued alert -->
|
||||||
<ui-checkbox v-if="!isFinished" v-model="shouldBackupAudioFiles" label="Backup audio files" medium checkbox-bg="bg" label-class="pl-2 text-base md:text-lg" @input="toggleBackupAudioFiles" />
|
<widgets-alert v-if="isMetadataEmbedQueued" type="warning" class="mb-4">
|
||||||
|
<p class="text-lg">Audiobook is queued for metadata embed ({{ queuedEmbedLIds.length }} in queue)</p>
|
||||||
|
</widgets-alert>
|
||||||
|
<!-- metadata embed action buttons -->
|
||||||
|
<div v-else-if="isEmbedTool" class="w-full flex justify-end items-center mb-4">
|
||||||
|
<ui-checkbox v-if="!isTaskFinished" v-model="shouldBackupAudioFiles" :disabled="processing" label="Backup audio files" medium checkbox-bg="bg" label-class="pl-2 text-base md:text-lg" @input="toggleBackupAudioFiles" />
|
||||||
|
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
|
|
||||||
<ui-btn v-if="!isFinished" color="primary" :loading="processing" @click.stop="embedClick">{{ $strings.ButtonStartMetadataEmbed }}</ui-btn>
|
<ui-btn v-if="!isTaskFinished" color="primary" :loading="processing" @click.stop="embedClick">{{ $strings.ButtonStartMetadataEmbed }}</ui-btn>
|
||||||
<p v-else class="text-success text-lg font-semibold">{{ $strings.MessageEmbedFinished }}</p>
|
<p v-else class="text-success text-lg font-semibold">{{ $strings.MessageEmbedFinished }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- m4b embed action buttons -->
|
||||||
<div v-else class="w-full flex items-center mb-4">
|
<div v-else class="w-full flex items-center mb-4">
|
||||||
<button :disabled="processing" class="text-sm uppercase text-gray-200 flex items-center pt-px pl-1 pr-2 hover:bg-white/5 rounded-md" @click="showEncodeOptions = !showEncodeOptions">
|
<button :disabled="processing" class="text-sm uppercase text-gray-200 flex items-center pt-px pl-1 pr-2 hover:bg-white/5 rounded-md" @click="showEncodeOptions = !showEncodeOptions">
|
||||||
<span class="material-icons text-xl">{{ showEncodeOptions ? 'check_box' : 'check_box_outline_blank' }}</span> <span class="pl-1">Use Advanced Options</span>
|
<span class="material-icons text-xl">{{ showEncodeOptions ? 'check_box' : 'check_box_outline_blank' }}</span> <span class="pl-1">Use Advanced Options</span>
|
||||||
@@ -83,6 +89,7 @@
|
|||||||
<p v-else class="text-success text-lg font-semibold">{{ $strings.MessageM4BFinished }}</p>
|
<p v-else class="text-success text-lg font-semibold">{{ $strings.MessageM4BFinished }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- advanced encoding options -->
|
||||||
<div v-if="isM4BTool" class="overflow-hidden">
|
<div v-if="isM4BTool" class="overflow-hidden">
|
||||||
<transition name="slide">
|
<transition name="slide">
|
||||||
<div v-if="showEncodeOptions" class="mb-4 pb-4 border-b border-white/10">
|
<div v-if="showEncodeOptions" class="mb-4 pb-4 border-b border-white/10">
|
||||||
@@ -191,6 +198,7 @@ export default {
|
|||||||
cnosole.error('No audio files')
|
cnosole.error('No audio files')
|
||||||
return redirect('/?error=no audio files')
|
return redirect('/?error=no audio files')
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
libraryItem
|
libraryItem
|
||||||
}
|
}
|
||||||
@@ -200,7 +208,6 @@ export default {
|
|||||||
processing: false,
|
processing: false,
|
||||||
audiofilesEncoding: {},
|
audiofilesEncoding: {},
|
||||||
audiofilesFinished: {},
|
audiofilesFinished: {},
|
||||||
isFinished: false,
|
|
||||||
toneObject: null,
|
toneObject: null,
|
||||||
selectedTool: 'embed',
|
selectedTool: 'embed',
|
||||||
isCancelingEncode: false,
|
isCancelingEncode: false,
|
||||||
@@ -272,11 +279,28 @@ export default {
|
|||||||
isTaskFinished() {
|
isTaskFinished() {
|
||||||
return this.task && this.task.isFinished
|
return this.task && this.task.isFinished
|
||||||
},
|
},
|
||||||
|
tasks() {
|
||||||
|
return this.$store.getters['tasks/getTasksByLibraryItemId'](this.libraryItemId)
|
||||||
|
},
|
||||||
|
embedTask() {
|
||||||
|
return this.tasks.find((t) => t.action === 'embed-metadata')
|
||||||
|
},
|
||||||
|
encodeTask() {
|
||||||
|
return this.tasks.find((t) => t.action === 'encode-m4b')
|
||||||
|
},
|
||||||
task() {
|
task() {
|
||||||
return this.$store.getters['tasks/getTaskByLibraryItemId'](this.libraryItemId)
|
if (this.isEmbedTool) return this.embedTask
|
||||||
|
else if (this.isM4BTool) return this.encodeTask
|
||||||
|
return null
|
||||||
},
|
},
|
||||||
taskRunning() {
|
taskRunning() {
|
||||||
return this.task && !this.task.isFinished
|
return this.task && !this.task.isFinished
|
||||||
|
},
|
||||||
|
queuedEmbedLIds() {
|
||||||
|
return this.$store.state.tasks.queuedEmbedLIds || []
|
||||||
|
},
|
||||||
|
isMetadataEmbedQueued() {
|
||||||
|
return this.queuedEmbedLIds.some((lid) => lid === this.libraryItemId)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -322,7 +346,7 @@ export default {
|
|||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
var errorMsg = error.response ? error.response.data || 'Unknown Error' : 'Unknown Error'
|
var errorMsg = error.response ? error.response.data || 'Unknown Error' : 'Unknown Error'
|
||||||
this.$toast.error(errorMsg)
|
this.$toast.error(errorMsg)
|
||||||
this.processing = true
|
this.processing = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
embedClick() {
|
embedClick() {
|
||||||
@@ -349,24 +373,6 @@ export default {
|
|||||||
this.processing = false
|
this.processing = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
audioMetadataStarted(data) {
|
|
||||||
console.log('audio metadata started', data)
|
|
||||||
if (data.libraryItemId !== this.libraryItemId) return
|
|
||||||
this.audiofilesFinished = {}
|
|
||||||
},
|
|
||||||
audioMetadataFinished(data) {
|
|
||||||
console.log('audio metadata finished', data)
|
|
||||||
if (data.libraryItemId !== this.libraryItemId) return
|
|
||||||
this.processing = false
|
|
||||||
this.audiofilesEncoding = {}
|
|
||||||
|
|
||||||
if (data.failed) {
|
|
||||||
this.$toast.error(data.error)
|
|
||||||
} else {
|
|
||||||
this.isFinished = true
|
|
||||||
this.$toast.success('Audio file metadata updated')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
audiofileMetadataStarted(data) {
|
audiofileMetadataStarted(data) {
|
||||||
if (data.libraryItemId !== this.libraryItemId) return
|
if (data.libraryItemId !== this.libraryItemId) return
|
||||||
this.$set(this.audiofilesEncoding, data.ino, true)
|
this.$set(this.audiofilesEncoding, data.ino, true)
|
||||||
@@ -412,14 +418,10 @@ export default {
|
|||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.init()
|
this.init()
|
||||||
this.$root.socket.on('audio_metadata_started', this.audioMetadataStarted)
|
|
||||||
this.$root.socket.on('audio_metadata_finished', this.audioMetadataFinished)
|
|
||||||
this.$root.socket.on('audiofile_metadata_started', this.audiofileMetadataStarted)
|
this.$root.socket.on('audiofile_metadata_started', this.audiofileMetadataStarted)
|
||||||
this.$root.socket.on('audiofile_metadata_finished', this.audiofileMetadataFinished)
|
this.$root.socket.on('audiofile_metadata_finished', this.audiofileMetadataFinished)
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.$root.socket.off('audio_metadata_started', this.audioMetadataStarted)
|
|
||||||
this.$root.socket.off('audio_metadata_finished', this.audioMetadataFinished)
|
|
||||||
this.$root.socket.off('audiofile_metadata_started', this.audiofileMetadataStarted)
|
this.$root.socket.off('audiofile_metadata_started', this.audiofileMetadataStarted)
|
||||||
this.$root.socket.off('audiofile_metadata_finished', this.audiofileMetadataFinished)
|
this.$root.socket.off('audiofile_metadata_finished', this.audiofileMetadataFinished)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,10 +9,17 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="enableBackups" class="mb-6">
|
<div v-if="enableBackups" class="mb-6">
|
||||||
<div class="flex items-center pl-6">
|
<div class="flex items-center pl-6 mb-2">
|
||||||
<span class="material-icons-outlined text-2xl text-black-50">schedule</span>
|
<span class="material-icons-outlined text-2xl text-black-50 mr-2">schedule</span>
|
||||||
<p class="text-gray-100 px-2">{{ scheduleDescription }}</p>
|
<div class="w-48"><span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.HeaderSchedule }}:</span></div>
|
||||||
<span class="material-icons text-lg text-black-50 hover:text-yellow-500 cursor-pointer" @click="showCronBuilder = !showCronBuilder">edit</span>
|
<div class="text-gray-100">{{ scheduleDescription }}</div>
|
||||||
|
<span class="material-icons text-lg text-black-50 hover:text-yellow-500 cursor-pointer ml-2" @click="showCronBuilder = !showCronBuilder">edit</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="nextBackupDate" class="flex items-center pl-6 py-0.5 px-2">
|
||||||
|
<span class="material-icons-outlined text-2xl text-black-50 mr-2">event</span>
|
||||||
|
<div class="w-48"><span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelNextBackupDate }}:</span></div>
|
||||||
|
<div class="text-gray-100">{{ nextBackupDate }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -64,10 +71,21 @@ export default {
|
|||||||
serverSettings() {
|
serverSettings() {
|
||||||
return this.$store.state.serverSettings
|
return this.$store.state.serverSettings
|
||||||
},
|
},
|
||||||
|
dateFormat() {
|
||||||
|
return this.serverSettings.dateFormat
|
||||||
|
},
|
||||||
|
timeFormat() {
|
||||||
|
return this.serverSettings.timeFormat
|
||||||
|
},
|
||||||
scheduleDescription() {
|
scheduleDescription() {
|
||||||
if (!this.cronExpression) return ''
|
if (!this.cronExpression) return ''
|
||||||
const parsed = this.$parseCronExpression(this.cronExpression)
|
const parsed = this.$parseCronExpression(this.cronExpression)
|
||||||
return parsed ? parsed.description : 'Custom cron expression ' + this.cronExpression
|
return parsed ? parsed.description : `${this.$strings.LabelCustomCronExpression} ${this.cronExpression}`
|
||||||
|
},
|
||||||
|
nextBackupDate() {
|
||||||
|
if (!this.cronExpression) return ''
|
||||||
|
const parsed = this.$getNextScheduledDate(this.cronExpression)
|
||||||
|
return this.$formatJsDatetime(parsed, this.dateFormat, this.timeFormat) || ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -90,15 +108,15 @@ export default {
|
|||||||
updateServerSettings(payload) {
|
updateServerSettings(payload) {
|
||||||
this.updatingServerSettings = true
|
this.updatingServerSettings = true
|
||||||
this.$store
|
this.$store
|
||||||
.dispatch('updateServerSettings', payload)
|
.dispatch('updateServerSettings', payload)
|
||||||
.then((success) => {
|
.then((success) => {
|
||||||
console.log('Updated Server Settings', success)
|
console.log('Updated Server Settings', success)
|
||||||
this.updatingServerSettings = false
|
this.updatingServerSettings = false
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to update server settings', error)
|
console.error('Failed to update server settings', error)
|
||||||
this.updatingServerSettings = false
|
this.updatingServerSettings = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
initServerSettings() {
|
initServerSettings() {
|
||||||
this.newServerSettings = this.serverSettings ? { ...this.serverSettings } : {}
|
this.newServerSettings = this.serverSettings ? { ...this.serverSettings } : {}
|
||||||
@@ -113,4 +131,4 @@ export default {
|
|||||||
this.initServerSettings()
|
this.initServerSettings()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<ui-toggle-switch labeledBy="settings-store-cover-with-items" v-model="newServerSettings.storeCoverWithItem" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('storeCoverWithItem', val)" />
|
<ui-toggle-switch labeledBy="settings-store-cover-with-items" v-model="newServerSettings.storeCoverWithItem" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('storeCoverWithItem', val)" />
|
||||||
<ui-tooltip :text="$strings.LabelSettingsStoreCoversWithItemHelp">
|
<ui-tooltip :text="$strings.LabelSettingsStoreCoversWithItemHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
<span id="settings-store-cover-with-items">{{ $strings.LabelSettingsStoreCoversWithItem }}</span>
|
<span id="settings-store-cover-with-items">{{ $strings.LabelSettingsStoreCoversWithItem }}</span>
|
||||||
<span class="material-icons icon-text">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
@@ -68,8 +68,14 @@
|
|||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="py-2">
|
<div class="flex-grow py-2">
|
||||||
<ui-dropdown :label="$strings.LabelSettingsDateFormat" v-model="newServerSettings.dateFormat" :items="dateFormats" small class="max-w-52" @input="(val) => updateSettingsKey('dateFormat', val)" />
|
<ui-dropdown :label="$strings.LabelSettingsDateFormat" v-model="newServerSettings.dateFormat" :items="dateFormats" small class="max-w-52" @input="(val) => updateSettingsKey('dateFormat', val)" />
|
||||||
|
<p class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelExample }}: {{ dateExample }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-grow py-2">
|
||||||
|
<ui-dropdown :label="$strings.LabelSettingsTimeFormat" v-model="newServerSettings.timeFormat" :items="timeFormats" small class="max-w-52" @input="(val) => updateSettingsKey('timeFormat', val)" />
|
||||||
|
<p class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelExample }}: {{ timeExample }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="py-2">
|
<div class="py-2">
|
||||||
@@ -206,7 +212,7 @@
|
|||||||
|
|
||||||
<div class="flex items-center py-4">
|
<div class="flex items-center py-4">
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<p class="pr-2 text-sm font-book text-yellow-400">
|
<p class="pr-2 text-sm text-yellow-400">
|
||||||
{{ $strings.MessageReportBugsAndContribute }}
|
{{ $strings.MessageReportBugsAndContribute }}
|
||||||
<a class="underline" href="https://github.com/advplyr/audiobookshelf" target="_blank">github</a>
|
<a class="underline" href="https://github.com/advplyr/audiobookshelf" target="_blank">github</a>
|
||||||
</p>
|
</p>
|
||||||
@@ -217,7 +223,7 @@
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
<p class="pl-4 pr-2 text-sm font-book text-yellow-400">
|
<p class="pl-4 pr-2 text-sm text-yellow-400">
|
||||||
{{ $strings.MessageJoinUsOn }}
|
{{ $strings.MessageJoinUsOn }}
|
||||||
<a class="underline" href="https://discord.gg/pJsjuNCKRq" target="_blank">discord</a>
|
<a class="underline" href="https://discord.gg/pJsjuNCKRq" target="_blank">discord</a>
|
||||||
</p>
|
</p>
|
||||||
@@ -293,6 +299,17 @@ export default {
|
|||||||
},
|
},
|
||||||
dateFormats() {
|
dateFormats() {
|
||||||
return this.$store.state.globals.dateFormats
|
return this.$store.state.globals.dateFormats
|
||||||
|
},
|
||||||
|
timeFormats() {
|
||||||
|
return this.$store.state.globals.timeFormats
|
||||||
|
},
|
||||||
|
dateExample() {
|
||||||
|
const date = new Date(2014, 2, 25)
|
||||||
|
return this.$formatJsDate(date, this.newServerSettings.dateFormat)
|
||||||
|
},
|
||||||
|
timeExample() {
|
||||||
|
const date = new Date(2014, 2, 25, 17, 30, 0)
|
||||||
|
return this.$formatJsTime(date, this.newServerSettings.timeFormat)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -420,4 +437,4 @@ export default {
|
|||||||
this.initServerSettings()
|
this.initServerSettings()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<app-settings-content :header-text="'Item Metadata Utils'">
|
<app-settings-content :header-text="$strings.HeaderItemMetadataUtils">
|
||||||
<nuxt-link to="/config/item-metadata-utils/tags" class="block w-full rounded bg-primary/40 hover:bg-primary/60 text-gray-300 hover:text-white p-4 mt-6 mb-2">
|
<nuxt-link to="/config/item-metadata-utils/tags" class="block w-full rounded bg-primary/40 hover:bg-primary/60 text-gray-300 hover:text-white p-4 mt-6 mb-2">
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<p>{{ $strings.HeaderManageTags }}</p>
|
<p>{{ $strings.HeaderManageTags }}</p>
|
||||||
|
|||||||
@@ -5,14 +5,14 @@
|
|||||||
|
|
||||||
<div class="flex lg:flex-row flex-wrap justify-between flex-col mt-8">
|
<div class="flex lg:flex-row flex-wrap justify-between flex-col mt-8">
|
||||||
<div class="w-80 my-6 mx-auto">
|
<div class="w-80 my-6 mx-auto">
|
||||||
<h1 class="text-2xl mb-4 font-book">{{ $strings.HeaderStatsTop5Genres }}</h1>
|
<h1 class="text-2xl mb-4">{{ $strings.HeaderStatsTop5Genres }}</h1>
|
||||||
<p v-if="!top5Genres.length">{{ $strings.MessageNoGenres }}</p>
|
<p v-if="!top5Genres.length">{{ $strings.MessageNoGenres }}</p>
|
||||||
<template v-for="genre in top5Genres">
|
<template v-for="genre in top5Genres">
|
||||||
<div :key="genre.genre" class="w-full py-2">
|
<div :key="genre.genre" class="w-full py-2">
|
||||||
<div class="flex items-end mb-1">
|
<div class="flex items-end mb-1">
|
||||||
<p class="text-2xl font-bold">{{ Math.round((100 * genre.count) / totalItems) }} %</p>
|
<p class="text-2xl font-bold">{{ Math.round((100 * genre.count) / totalItems) }} %</p>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=genres.${$encode(genre.genre)}`" class="text-base font-book text-white text-opacity-70 hover:underline">
|
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=genres.${$encode(genre.genre)}`" class="text-base text-white text-opacity-70 hover:underline">
|
||||||
{{ genre.genre }}
|
{{ genre.genre }}
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</div>
|
</div>
|
||||||
@@ -23,12 +23,12 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-80 my-6 mx-auto">
|
<div class="w-80 my-6 mx-auto">
|
||||||
<h1 class="text-2xl mb-4 font-book">{{ $strings.HeaderStatsTop10Authors }}</h1>
|
<h1 class="text-2xl mb-4">{{ $strings.HeaderStatsTop10Authors }}</h1>
|
||||||
<p v-if="!top10Authors.length">{{ $strings.MessageNoAuthors }}</p>
|
<p v-if="!top10Authors.length">{{ $strings.MessageNoAuthors }}</p>
|
||||||
<template v-for="(author, index) in top10Authors">
|
<template v-for="(author, index) in top10Authors">
|
||||||
<div :key="author.id" class="w-full py-2">
|
<div :key="author.id" class="w-full py-2">
|
||||||
<div class="flex items-center mb-1">
|
<div class="flex items-center mb-1">
|
||||||
<p class="text-sm font-book text-white text-opacity-70 w-36 pr-2 truncate">
|
<p class="text-sm text-white text-opacity-70 w-36 pr-2 truncate">
|
||||||
{{ index + 1 }}. <nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=authors.${$encode(author.id)}`" class="hover:underline">{{ author.name }}</nuxt-link>
|
{{ index + 1 }}. <nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=authors.${$encode(author.id)}`" class="hover:underline">{{ author.name }}</nuxt-link>
|
||||||
</p>
|
</p>
|
||||||
<div class="flex-grow rounded-full h-2.5 bg-primary bg-opacity-0 overflow-hidden">
|
<div class="flex-grow rounded-full h-2.5 bg-primary bg-opacity-0 overflow-hidden">
|
||||||
@@ -42,12 +42,12 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-80 my-6 mx-auto">
|
<div class="w-80 my-6 mx-auto">
|
||||||
<h1 class="text-2xl mb-4 font-book">{{ $strings.HeaderStatsLongestItems }}</h1>
|
<h1 class="text-2xl mb-4">{{ $strings.HeaderStatsLongestItems }}</h1>
|
||||||
<p v-if="!top10LongestItems.length">{{ $strings.MessageNoItems }}</p>
|
<p v-if="!top10LongestItems.length">{{ $strings.MessageNoItems }}</p>
|
||||||
<template v-for="(ab, index) in top10LongestItems">
|
<template v-for="(ab, index) in top10LongestItems">
|
||||||
<div :key="index" class="w-full py-2">
|
<div :key="index" class="w-full py-2">
|
||||||
<div class="flex items-center mb-1">
|
<div class="flex items-center mb-1">
|
||||||
<p class="text-sm font-book text-white text-opacity-70 w-44 pr-2 truncate">
|
<p class="text-sm text-white text-opacity-70 w-44 pr-2 truncate">
|
||||||
{{ index + 1 }}. <nuxt-link :to="`/item/${ab.id}`" class="hover:underline">{{ ab.title }}</nuxt-link>
|
{{ index + 1 }}. <nuxt-link :to="`/item/${ab.id}`" class="hover:underline">{{ ab.title }}</nuxt-link>
|
||||||
</p>
|
</p>
|
||||||
<div class="flex-grow rounded-full h-2.5 bg-primary bg-opacity-0 overflow-hidden">
|
<div class="flex-grow rounded-full h-2.5 bg-primary bg-opacity-0 overflow-hidden">
|
||||||
@@ -60,6 +60,25 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="w-80 my-6 mx-auto">
|
||||||
|
<h1 class="text-2xl mb-4">{{ $strings.HeaderStatsLargestItems }}</h1>
|
||||||
|
<p v-if="!top10LargestItems.length">{{ $strings.MessageNoItems }}</p>
|
||||||
|
<template v-for="(ab, index) in top10LargestItems">
|
||||||
|
<div :key="index" class="w-full py-2">
|
||||||
|
<div class="flex items-center mb-1">
|
||||||
|
<p class="text-sm text-white text-opacity-70 w-44 pr-2 truncate">
|
||||||
|
{{ index + 1 }}. <nuxt-link :to="`/item/${ab.id}`" class="hover:underline">{{ ab.title }}</nuxt-link>
|
||||||
|
</p>
|
||||||
|
<div class="flex-grow rounded-full h-2.5 bg-primary bg-opacity-0 overflow-hidden">
|
||||||
|
<div class="bg-yellow-400 h-full rounded-full" :style="{ width: Math.round((100 * ab.size) / largestItemSize) + '%' }" />
|
||||||
|
</div>
|
||||||
|
<div class="w-4 ml-3">
|
||||||
|
<p class="text-sm font-bold">{{ $bytesPretty(ab.size) }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</app-settings-content>
|
</app-settings-content>
|
||||||
</div>
|
</div>
|
||||||
@@ -105,6 +124,13 @@ export default {
|
|||||||
if (!this.top10LongestItems.length) return 0
|
if (!this.top10LongestItems.length) return 0
|
||||||
return this.top10LongestItems[0].duration
|
return this.top10LongestItems[0].duration
|
||||||
},
|
},
|
||||||
|
top10LargestItems() {
|
||||||
|
return this.libraryStats ? this.libraryStats.largestItems || [] : []
|
||||||
|
},
|
||||||
|
largestItemSize() {
|
||||||
|
if (!this.top10LargestItems.length) return 0
|
||||||
|
return this.top10LargestItems[0].size
|
||||||
|
},
|
||||||
authorsWithCount() {
|
authorsWithCount() {
|
||||||
return this.libraryStats ? this.libraryStats.authorsWithCount : []
|
return this.libraryStats ? this.libraryStats.authorsWithCount : []
|
||||||
},
|
},
|
||||||
@@ -135,4 +161,4 @@ export default {
|
|||||||
this.init()
|
this.init()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
<p class="text-xs font-mono">{{ $secondsToTimestamp(session.currentTime) }}</p>
|
<p class="text-xs font-mono">{{ $secondsToTimestamp(session.currentTime) }}</p>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center hidden sm:table-cell">
|
<td class="text-center hidden sm:table-cell">
|
||||||
<ui-tooltip v-if="session.updatedAt" direction="top" :text="$formatDate(session.updatedAt, 'MMMM do, yyyy HH:mm')">
|
<ui-tooltip v-if="session.updatedAt" direction="top" :text="$formatDatetime(session.updatedAt, dateFormat, timeFormat)">
|
||||||
<p class="text-xs text-gray-200">{{ $dateDistanceFromNow(session.updatedAt) }}</p>
|
<p class="text-xs text-gray-200">{{ $dateDistanceFromNow(session.updatedAt) }}</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</td>
|
</td>
|
||||||
@@ -105,6 +105,12 @@ export default {
|
|||||||
if (!this.userFilter) return null
|
if (!this.userFilter) return null
|
||||||
var user = this.users.find((u) => u.id === this.userFilter)
|
var user = this.users.find((u) => u.id === this.userFilter)
|
||||||
return user ? user.username : null
|
return user ? user.username : null
|
||||||
|
},
|
||||||
|
dateFormat() {
|
||||||
|
return this.$store.state.serverSettings.dateFormat
|
||||||
|
},
|
||||||
|
timeFormat() {
|
||||||
|
return this.$store.state.serverSettings.timeFormat
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -149,7 +155,7 @@ export default {
|
|||||||
episodeId: episode.id,
|
episodeId: episode.id,
|
||||||
title: episode.title,
|
title: episode.title,
|
||||||
subtitle: libraryItem.media.metadata.title,
|
subtitle: libraryItem.media.metadata.title,
|
||||||
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, 'MMM do, yyyy')}` : 'Unknown publish date',
|
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date',
|
||||||
duration: episode.audioFile.duration || null,
|
duration: episode.audioFile.duration || null,
|
||||||
coverPath: libraryItem.media.coverPath || null
|
coverPath: libraryItem.media.coverPath || null
|
||||||
}
|
}
|
||||||
@@ -266,4 +272,4 @@ export default {
|
|||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
<div class="px-3">
|
<div class="px-3">
|
||||||
<p class="text-4xl md:text-5xl font-bold">{{ userItemsFinished.length }}</p>
|
<p class="text-4xl md:text-5xl font-bold">{{ userItemsFinished.length }}</p>
|
||||||
<p class="font-book text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsItemsFinished }}</p>
|
<p class="text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsItemsFinished }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="px-1">
|
<div class="px-1">
|
||||||
<p class="text-4xl md:text-5xl font-bold">{{ totalDaysListened }}</p>
|
<p class="text-4xl md:text-5xl font-bold">{{ totalDaysListened }}</p>
|
||||||
<p class="font-book text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsDaysListened }}</p>
|
<p class="text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsDaysListened }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="px-1">
|
<div class="px-1">
|
||||||
<p class="text-4xl md:text-5xl font-bold">{{ totalMinutesListening }}</p>
|
<p class="text-4xl md:text-5xl font-bold">{{ totalMinutesListening }}</p>
|
||||||
<p class="font-book text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsMinutesListening }}</p>
|
<p class="text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsMinutesListening }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
<stats-daily-listening-chart :listening-stats="listeningStats" class="origin-top-left transform scale-75 lg:scale-100" />
|
<stats-daily-listening-chart :listening-stats="listeningStats" class="origin-top-left transform scale-75 lg:scale-100" />
|
||||||
<div class="w-80 my-6 mx-auto">
|
<div class="w-80 my-6 mx-auto">
|
||||||
<div class="flex mb-4 items-center">
|
<div class="flex mb-4 items-center">
|
||||||
<h1 class="text-2xl font-book">{{ $strings.HeaderStatsRecentSessions }}</h1>
|
<h1 class="text-2xl">{{ $strings.HeaderStatsRecentSessions }}</h1>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<ui-btn :to="`/config/users/${user.id}/sessions`" class="text-xs" :padding-x="1.5" :padding-y="1">{{ $strings.ButtonViewAll }}</ui-btn>
|
<ui-btn :to="`/config/users/${user.id}/sessions`" class="text-xs" :padding-x="1.5" :padding-y="1">{{ $strings.ButtonViewAll }}</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
@@ -47,9 +47,9 @@
|
|||||||
<template v-for="(item, index) in mostRecentListeningSessions">
|
<template v-for="(item, index) in mostRecentListeningSessions">
|
||||||
<div :key="item.id" class="w-full py-0.5">
|
<div :key="item.id" class="w-full py-0.5">
|
||||||
<div class="flex items-center mb-1">
|
<div class="flex items-center mb-1">
|
||||||
<p class="text-sm font-book text-white text-opacity-70 w-8">{{ index + 1 }}. </p>
|
<p class="text-sm text-white text-opacity-70 w-8">{{ index + 1 }}. </p>
|
||||||
<div class="w-56">
|
<div class="w-56">
|
||||||
<p class="text-sm font-book text-white text-opacity-80 truncate">{{ item.mediaMetadata ? item.mediaMetadata.title : '' }}</p>
|
<p class="text-sm text-white text-opacity-80 truncate">{{ item.mediaMetadata ? item.mediaMetadata.title : '' }}</p>
|
||||||
<p class="text-xs text-white text-opacity-50">{{ $dateDistanceFromNow(item.updatedAt) }}</p>
|
<p class="text-xs text-white text-opacity-50">{{ $dateDistanceFromNow(item.updatedAt) }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
|
|||||||
@@ -65,7 +65,7 @@
|
|||||||
<td>
|
<td>
|
||||||
<covers-book-cover :width="50" :library-item="item" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
<covers-book-cover :width="50" :library-item="item" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
</td>
|
</td>
|
||||||
<td class="font-book">
|
<td>
|
||||||
<template v-if="item.media && item.media.metadata && item.episode">
|
<template v-if="item.media && item.media.metadata && item.episode">
|
||||||
<p>{{ item.episode.title || 'Unknown' }}</p>
|
<p>{{ item.episode.title || 'Unknown' }}</p>
|
||||||
<p class="text-white text-opacity-50 text-sm font-sans">{{ item.media.metadata.title }}</p>
|
<p class="text-white text-opacity-50 text-sm font-sans">{{ item.media.metadata.title }}</p>
|
||||||
@@ -79,12 +79,12 @@
|
|||||||
<p class="text-sm">{{ Math.floor(item.progress * 100) }}%</p>
|
<p class="text-sm">{{ Math.floor(item.progress * 100) }}%</p>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center hidden sm:table-cell">
|
<td class="text-center hidden sm:table-cell">
|
||||||
<ui-tooltip v-if="item.startedAt" direction="top" :text="$formatDate(item.startedAt, 'MMMM do, yyyy HH:mm')">
|
<ui-tooltip v-if="item.startedAt" direction="top" :text="$formatDatetime(item.startedAt, dateFormat, timeFormat)">
|
||||||
<p class="text-sm">{{ $dateDistanceFromNow(item.startedAt) }}</p>
|
<p class="text-sm">{{ $dateDistanceFromNow(item.startedAt) }}</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center hidden sm:table-cell">
|
<td class="text-center hidden sm:table-cell">
|
||||||
<ui-tooltip v-if="item.lastUpdate" direction="top" :text="$formatDate(item.lastUpdate, 'MMMM do, yyyy HH:mm')">
|
<ui-tooltip v-if="item.lastUpdate" direction="top" :text="$formatDatetime(item.lastUpdate, dateFormat, timeFormat)">
|
||||||
<p class="text-sm">{{ $dateDistanceFromNow(item.lastUpdate) }}</p>
|
<p class="text-sm">{{ $dateDistanceFromNow(item.lastUpdate) }}</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</td>
|
</td>
|
||||||
@@ -149,6 +149,12 @@ export default {
|
|||||||
latestSession() {
|
latestSession() {
|
||||||
if (!this.listeningSessions.sessions || !this.listeningSessions.sessions.length) return null
|
if (!this.listeningSessions.sessions || !this.listeningSessions.sessions.length) return null
|
||||||
return this.listeningSessions.sessions[0]
|
return this.listeningSessions.sessions[0]
|
||||||
|
},
|
||||||
|
dateFormat() {
|
||||||
|
return this.$store.state.serverSettings.dateFormat
|
||||||
|
},
|
||||||
|
timeFormat() {
|
||||||
|
return this.$store.state.serverSettings.timeFormat
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
<p class="text-xs font-mono">{{ $secondsToTimestamp(session.currentTime) }}</p>
|
<p class="text-xs font-mono">{{ $secondsToTimestamp(session.currentTime) }}</p>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center hidden sm:table-cell">
|
<td class="text-center hidden sm:table-cell">
|
||||||
<ui-tooltip v-if="session.updatedAt" direction="top" :text="$formatDate(session.updatedAt, 'MMMM do, yyyy HH:mm')">
|
<ui-tooltip v-if="session.updatedAt" direction="top" :text="$formatDatetime(session.updatedAt, dateFormat, timeFormat)">
|
||||||
<p class="text-xs text-gray-200">{{ $dateDistanceFromNow(session.updatedAt) }}</p>
|
<p class="text-xs text-gray-200">{{ $dateDistanceFromNow(session.updatedAt) }}</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</td>
|
</td>
|
||||||
@@ -96,6 +96,12 @@ export default {
|
|||||||
},
|
},
|
||||||
userOnline() {
|
userOnline() {
|
||||||
return this.$store.getters['users/getIsUserOnline'](this.user.id)
|
return this.$store.getters['users/getIsUserOnline'](this.user.id)
|
||||||
|
},
|
||||||
|
dateFormat() {
|
||||||
|
return this.$store.state.serverSettings.dateFormat
|
||||||
|
},
|
||||||
|
timeFormat() {
|
||||||
|
return this.$store.state.serverSettings.timeFormat
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -140,7 +146,7 @@ export default {
|
|||||||
episodeId: episode.id,
|
episodeId: episode.id,
|
||||||
title: episode.title,
|
title: episode.title,
|
||||||
subtitle: libraryItem.media.metadata.title,
|
subtitle: libraryItem.media.metadata.title,
|
||||||
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, 'MMM do, yyyy')}` : 'Unknown publish date',
|
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date',
|
||||||
duration: episode.audioFile.duration || null,
|
duration: episode.audioFile.duration || null,
|
||||||
coverPath: libraryItem.media.coverPath || null
|
coverPath: libraryItem.media.coverPath || null
|
||||||
}
|
}
|
||||||
@@ -252,4 +258,4 @@ export default {
|
|||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -25,7 +25,10 @@
|
|||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h1 class="text-2xl md:text-3xl font-semibold">
|
<h1 class="text-2xl md:text-3xl font-semibold">
|
||||||
{{ title }}
|
<div class="flex items-center">
|
||||||
|
{{ title }}
|
||||||
|
<widgets-explicit-indicator :explicit="isExplicit" />
|
||||||
|
</div>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p v-if="bookSubtitle" class="text-gray-200 text-xl md:text-2xl">{{ bookSubtitle }}</p>
|
<p v-if="bookSubtitle" class="text-gray-200 text-xl md:text-2xl">{{ bookSubtitle }}</p>
|
||||||
@@ -121,6 +124,14 @@
|
|||||||
{{ sizePretty }}
|
{{ sizePretty }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="isBook" class="flex py-0.5">
|
||||||
|
<div class="w-32">
|
||||||
|
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelAbridged }}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{ isAbridged ? 'Yes' : 'No' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="hidden md:block flex-grow" />
|
<div class="hidden md:block flex-grow" />
|
||||||
</div>
|
</div>
|
||||||
@@ -153,7 +164,7 @@
|
|||||||
<div v-if="!isPodcast && progressPercent > 0" class="px-4 py-2 mt-4 bg-primary text-sm font-semibold rounded-md text-gray-100 relative max-w-max mx-auto md:mx-0" :class="resettingProgress ? 'opacity-25' : ''">
|
<div v-if="!isPodcast && progressPercent > 0" class="px-4 py-2 mt-4 bg-primary text-sm font-semibold rounded-md text-gray-100 relative max-w-max mx-auto md:mx-0" :class="resettingProgress ? 'opacity-25' : ''">
|
||||||
<p v-if="progressPercent < 1" class="leading-6">{{ $strings.LabelYourProgress }}: {{ Math.round(progressPercent * 100) }}%</p>
|
<p v-if="progressPercent < 1" class="leading-6">{{ $strings.LabelYourProgress }}: {{ Math.round(progressPercent * 100) }}%</p>
|
||||||
<p v-else class="text-xs">{{ $strings.LabelFinished }} {{ $formatDate(userProgressFinishedAt, dateFormat) }}</p>
|
<p v-else class="text-xs">{{ $strings.LabelFinished }} {{ $formatDate(userProgressFinishedAt, dateFormat) }}</p>
|
||||||
<p v-if="progressPercent < 1" class="text-gray-200 text-xs">{{ $getString('LabelTimeRemaining', [$elapsedPretty(userTimeRemaining)]) }}</p>
|
<p v-if="progressPercent < 1 && !useEBookProgress" class="text-gray-200 text-xs">{{ $getString('LabelTimeRemaining', [$elapsedPretty(userTimeRemaining)]) }}</p>
|
||||||
<p class="text-gray-400 text-xs pt-1">{{ $strings.LabelStarted }} {{ $formatDate(userProgressStartedAt, dateFormat) }}</p>
|
<p class="text-gray-400 text-xs pt-1">{{ $strings.LabelStarted }} {{ $formatDate(userProgressStartedAt, dateFormat) }}</p>
|
||||||
|
|
||||||
<div v-if="!resettingProgress" class="absolute -top-1.5 -right-1.5 p-1 w-5 h-5 rounded-full bg-bg hover:bg-error border border-primary flex items-center justify-center cursor-pointer" @click.stop="clearProgressClick">
|
<div v-if="!resettingProgress" class="absolute -top-1.5 -right-1.5 p-1 w-5 h-5 rounded-full bg-bg hover:bg-error border border-primary flex items-center justify-center cursor-pointer" @click.stop="clearProgressClick">
|
||||||
@@ -315,6 +326,12 @@ export default {
|
|||||||
isInvalid() {
|
isInvalid() {
|
||||||
return this.libraryItem.isInvalid
|
return this.libraryItem.isInvalid
|
||||||
},
|
},
|
||||||
|
isExplicit() {
|
||||||
|
return !!this.mediaMetadata.explicit
|
||||||
|
},
|
||||||
|
isAbridged() {
|
||||||
|
return !!this.mediaMetadata.abridged
|
||||||
|
},
|
||||||
invalidAudioFiles() {
|
invalidAudioFiles() {
|
||||||
if (!this.isBook) return []
|
if (!this.isBook) return []
|
||||||
return this.libraryItem.media.audioFiles.filter((af) => af.invalid)
|
return this.libraryItem.media.audioFiles.filter((af) => af.invalid)
|
||||||
@@ -465,7 +482,12 @@ export default {
|
|||||||
const duration = this.userMediaProgress.duration || this.duration
|
const duration = this.userMediaProgress.duration || this.duration
|
||||||
return duration - this.userMediaProgress.currentTime
|
return duration - this.userMediaProgress.currentTime
|
||||||
},
|
},
|
||||||
|
useEBookProgress() {
|
||||||
|
if (!this.userMediaProgress || this.userMediaProgress.progress) return false
|
||||||
|
return this.userMediaProgress.ebookProgress > 0
|
||||||
|
},
|
||||||
progressPercent() {
|
progressPercent() {
|
||||||
|
if (this.useEBookProgress) return Math.max(Math.min(1, this.userMediaProgress.ebookProgress), 0)
|
||||||
return this.userMediaProgress ? Math.max(Math.min(1, this.userMediaProgress.progress), 0) : 0
|
return this.userMediaProgress ? Math.max(Math.min(1, this.userMediaProgress.progress), 0) : 0
|
||||||
},
|
},
|
||||||
userProgressStartedAt() {
|
userProgressStartedAt() {
|
||||||
@@ -632,7 +654,7 @@ export default {
|
|||||||
episodeId: episode.id,
|
episodeId: episode.id,
|
||||||
title: episode.title,
|
title: episode.title,
|
||||||
subtitle: this.title,
|
subtitle: this.title,
|
||||||
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, 'MMM do, yyyy')}` : 'Unknown publish date',
|
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date',
|
||||||
duration: episode.audioFile.duration || null,
|
duration: episode.audioFile.duration || null,
|
||||||
coverPath: this.libraryItem.media.coverPath || null
|
coverPath: this.libraryItem.media.coverPath || null
|
||||||
})
|
})
|
||||||
@@ -753,9 +775,8 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
if (this.libraryItem.episodesDownloading) {
|
this.episodeDownloadsQueued = this.libraryItem.episodeDownloadsQueued || []
|
||||||
this.episodeDownloadsQueued = this.libraryItem.episodesDownloading || []
|
this.episodesDownloading = this.libraryItem.episodesDownloading || []
|
||||||
}
|
|
||||||
|
|
||||||
// use this items library id as the current
|
// use this items library id as the current
|
||||||
if (this.libraryId) {
|
if (this.libraryId) {
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page" :class="streamLibraryItem ? 'streaming' : ''">
|
||||||
|
<app-book-shelf-toolbar page="podcast-search" />
|
||||||
|
|
||||||
|
<div id="bookshelf" class="w-full overflow-y-auto px-2 py-6 sm:px-4 md:p-12 relative">
|
||||||
|
<div class="w-full max-w-5xl mx-auto py-4">
|
||||||
|
<p class="text-xl mb-2 font-semibold px-4 md:px-0">{{ $strings.HeaderCurrentDownloads }}</p>
|
||||||
|
<p v-if="!episodesDownloading.length" class="text-lg py-4">{{ $strings.MessageNoDownloadsInProgress }}</p>
|
||||||
|
<template v-for="episode in episodesDownloading">
|
||||||
|
<div :key="episode.id" class="flex py-5 relative">
|
||||||
|
<covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](episode.libraryItemId)" :width="96" :book-cover-aspect-ratio="bookCoverAspectRatio" :show-resolution="false" class="hidden md:block" />
|
||||||
|
<div class="flex-grow pl-4 max-w-2xl">
|
||||||
|
<!-- mobile -->
|
||||||
|
<div class="flex md:hidden mb-2">
|
||||||
|
<covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](episode.libraryItemId)" :width="48" :book-cover-aspect-ratio="bookCoverAspectRatio" :show-resolution="false" class="md:hidden" />
|
||||||
|
<div class="flex-grow px-2">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<nuxt-link :to="`/item/${episode.libraryItemId}`" class="text-sm text-gray-200 hover:underline">{{ episode.podcastTitle }}</nuxt-link>
|
||||||
|
<widgets-explicit-indicator :explicit="episode.podcastExplicit" />
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-gray-300 mb-1">{{ $dateDistanceFromNow(episode.publishedAt) }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- desktop -->
|
||||||
|
<div class="hidden md:block">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<nuxt-link :to="`/item/${episode.libraryItemId}`" class="text-sm text-gray-200 hover:underline">{{ episode.podcastTitle }}</nuxt-link>
|
||||||
|
<widgets-explicit-indicator :explicit="episode.podcastExplicit" />
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-gray-300 mb-1">{{ $dateDistanceFromNow(episode.publishedAt) }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center font-semibold text-gray-200">
|
||||||
|
<div v-if="episode.season || episode.episode">#</div>
|
||||||
|
<div v-if="episode.season">{{ episode.season }}x</div>
|
||||||
|
<div v-if="episode.episode">{{ episode.episode }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center mb-2">
|
||||||
|
<span class="font-semibold text-sm md:text-base">{{ episode.episodeDisplayTitle }}</span>
|
||||||
|
<widgets-podcast-type-indicator :type="episode.episodeType" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-sm text-gray-200 mb-4">{{ episode.subtitle }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<tables-podcast-download-queue-table v-if="episodeDownloadsQueued.length" :queue="episodeDownloadsQueued"></tables-podcast-download-queue-table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
async asyncData({ params, redirect }) {
|
||||||
|
if (!params.library) {
|
||||||
|
console.error('No library...', params.library)
|
||||||
|
return redirect('/')
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
libraryId: params.library
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
episodesDownloading: [],
|
||||||
|
episodeDownloadsQueued: [],
|
||||||
|
processing: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
bookCoverAspectRatio() {
|
||||||
|
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||||
|
},
|
||||||
|
streamLibraryItem() {
|
||||||
|
return this.$store.state.streamLibraryItem
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
episodeDownloadQueued(episodeDownload) {
|
||||||
|
if (episodeDownload.libraryId === this.libraryId) {
|
||||||
|
this.episodeDownloadsQueued.push(episodeDownload)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
episodeDownloadStarted(episodeDownload) {
|
||||||
|
if (episodeDownload.libraryId === this.libraryId) {
|
||||||
|
this.episodeDownloadsQueued = this.episodeDownloadsQueued.filter((d) => d.id !== episodeDownload.id)
|
||||||
|
this.episodesDownloading.push(episodeDownload)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
episodeDownloadFinished(episodeDownload) {
|
||||||
|
if (episodeDownload.libraryId === this.libraryId) {
|
||||||
|
this.episodeDownloadsQueued = this.episodeDownloadsQueued.filter((d) => d.id !== episodeDownload.id)
|
||||||
|
this.episodesDownloading = this.episodesDownloading.filter((d) => d.id !== episodeDownload.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
episodeDownloadQueueUpdated(downloadQueueDetails) {
|
||||||
|
this.episodeDownloadsQueued = downloadQueueDetails.queue.filter((q) => q.libraryId == this.libraryId)
|
||||||
|
},
|
||||||
|
async loadInitialDownloadQueue() {
|
||||||
|
this.processing = true
|
||||||
|
const queuePayload = await this.$axios.$get(`/api/libraries/${this.libraryId}/episode-downloads`).catch((error) => {
|
||||||
|
console.error('Failed to get download queue', error)
|
||||||
|
this.$toast.error('Failed to get download queue')
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
this.processing = false
|
||||||
|
this.episodeDownloadsQueued = queuePayload?.queue || []
|
||||||
|
|
||||||
|
if (queuePayload?.currentDownload) {
|
||||||
|
this.episodesDownloading.push(queuePayload.currentDownload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize listeners after load to prevent event race conditions
|
||||||
|
this.initListeners()
|
||||||
|
},
|
||||||
|
initListeners() {
|
||||||
|
this.$root.socket.on('episode_download_queued', this.episodeDownloadQueued)
|
||||||
|
this.$root.socket.on('episode_download_started', this.episodeDownloadStarted)
|
||||||
|
this.$root.socket.on('episode_download_finished', this.episodeDownloadFinished)
|
||||||
|
this.$root.socket.on('episode_download_queue_updated', this.episodeDownloadQueueUpdated)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (this.libraryId) {
|
||||||
|
this.$store.commit('libraries/setCurrentLibrary', this.libraryId)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loadInitialDownloadQueue()
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.$root.socket.off('episode_download_queued', this.episodeDownloadQueued)
|
||||||
|
this.$root.socket.off('episode_download_started', this.episodeDownloadStarted)
|
||||||
|
this.$root.socket.off('episode_download_finished', this.episodeDownloadFinished)
|
||||||
|
this.$root.socket.off('episode_download_queue_updated', this.episodeDownloadQueueUpdated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -14,19 +14,36 @@
|
|||||||
<div class="flex md:hidden mb-2">
|
<div class="flex md:hidden mb-2">
|
||||||
<covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](episode.libraryItemId)" :width="48" :book-cover-aspect-ratio="bookCoverAspectRatio" :show-resolution="false" class="md:hidden" />
|
<covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](episode.libraryItemId)" :width="48" :book-cover-aspect-ratio="bookCoverAspectRatio" :show-resolution="false" class="md:hidden" />
|
||||||
<div class="flex-grow px-2">
|
<div class="flex-grow px-2">
|
||||||
<nuxt-link :to="`/item/${episode.libraryItemId}`" class="text-sm text-gray-200 hover:underline">{{ episode.podcast.metadata.title }}</nuxt-link>
|
<div class="flex items-center">
|
||||||
|
<div class="flex" @click.stop>
|
||||||
|
<nuxt-link :to="`/item/${episode.libraryItemId}`" class="text-sm text-gray-200 hover:underline">{{ episode.podcast.metadata.title }}</nuxt-link>
|
||||||
|
</div>
|
||||||
|
<widgets-explicit-indicator :explicit="episode.podcast.metadata.explicit" />
|
||||||
|
</div>
|
||||||
<p class="text-xs text-gray-300 mb-1">{{ $dateDistanceFromNow(episode.publishedAt) }}</p>
|
<p class="text-xs text-gray-300 mb-1">{{ $dateDistanceFromNow(episode.publishedAt) }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- desktop -->
|
<!-- desktop -->
|
||||||
<div class="hidden md:block">
|
<div class="hidden md:block">
|
||||||
<nuxt-link :to="`/item/${episode.libraryItemId}`" class="text-sm text-gray-200 hover:underline">{{ episode.podcast.metadata.title }}</nuxt-link>
|
<div class="flex items-center">
|
||||||
|
<div class="flex" @click.stop>
|
||||||
|
<nuxt-link :to="`/item/${episode.libraryItemId}`" class="text-sm text-gray-200 hover:underline">{{ episode.podcast.metadata.title }}</nuxt-link>
|
||||||
|
</div>
|
||||||
|
<widgets-explicit-indicator :explicit="episode.podcast.metadata.explicit" />
|
||||||
|
</div>
|
||||||
<p class="text-xs text-gray-300 mb-1">{{ $dateDistanceFromNow(episode.publishedAt) }}</p>
|
<p class="text-xs text-gray-300 mb-1">{{ $dateDistanceFromNow(episode.publishedAt) }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="font-semibold mb-2 text-sm md:text-base">{{ episode.title }}</p>
|
<div class="flex items-center font-semibold text-gray-200">
|
||||||
|
<div v-if="episode.season || episode.episode">#</div>
|
||||||
|
<div v-if="episode.season">{{ episode.season }}x</div>
|
||||||
|
<div v-if="episode.episode">{{ episode.episode }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center mb-2">
|
||||||
|
<div class="font-semibold text-sm md:text-base">{{ episode.title }}</div>
|
||||||
|
<widgets-podcast-type-indicator :type="episode.episodeType" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<p class="text-sm text-gray-200 mb-4">{{ episode.subtitle }}</p>
|
<p class="text-sm text-gray-200 mb-4">{{ episode.subtitle }}</p>
|
||||||
|
|
||||||
@@ -113,6 +130,9 @@ export default {
|
|||||||
if (i.episodeId) episodeIds[i.episodeId] = true
|
if (i.episodeId) episodeIds[i.episodeId] = true
|
||||||
})
|
})
|
||||||
return episodeIds
|
return episodeIds
|
||||||
|
},
|
||||||
|
dateFormat() {
|
||||||
|
return this.$store.state.serverSettings.dateFormat
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -156,7 +176,7 @@ export default {
|
|||||||
episodeId: episode.id,
|
episodeId: episode.id,
|
||||||
title: episode.title,
|
title: episode.title,
|
||||||
subtitle: episode.podcast.metadata.title,
|
subtitle: episode.podcast.metadata.title,
|
||||||
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, 'MMM do, yyyy')}` : 'Unknown publish date',
|
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date',
|
||||||
duration: episode.duration || null,
|
duration: episode.duration || null,
|
||||||
coverPath: episode.podcast.coverPath || null
|
coverPath: episode.podcast.coverPath || null
|
||||||
})
|
})
|
||||||
@@ -194,7 +214,7 @@ export default {
|
|||||||
episodeId: episode.id,
|
episodeId: episode.id,
|
||||||
title: episode.title,
|
title: episode.title,
|
||||||
subtitle: episode.podcast.metadata.title,
|
subtitle: episode.podcast.metadata.title,
|
||||||
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, 'MMM do, yyyy')}` : 'Unknown publish date',
|
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date',
|
||||||
duration: episode.duration || null,
|
duration: episode.duration || null,
|
||||||
coverPath: episode.podcast.coverPath || null
|
coverPath: episode.podcast.coverPath || null
|
||||||
}
|
}
|
||||||
@@ -206,4 +226,4 @@ export default {
|
|||||||
this.loadRecentEpisodes()
|
this.loadRecentEpisodes()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user