mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-07-07 09:51:37 +00:00
Add tour guide for home page and library page.create enable feature in setting file.
This commit is contained in:
parent
ee3fa2f427
commit
7fc572a3a2
12 changed files with 4292 additions and 4842 deletions
|
|
@ -66,3 +66,20 @@
|
|||
margin-block-start: 0;
|
||||
}
|
||||
|
||||
.shepherd-progress-bar {
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
margin-top: 5px;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.shepherd-progress-bar span {
|
||||
display: block;
|
||||
height: 100%;
|
||||
background: #4cafef;
|
||||
/* Change to match theme */
|
||||
width: 0%;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
|
@ -42,14 +42,6 @@
|
|||
</ui-tooltip>
|
||||
</nuxt-link>
|
||||
|
||||
<!-- Tour Tooltip icon -->
|
||||
<ui-tooltip text="Start Tour" direction="bottom">
|
||||
<button @click="startTour" class="hover:text-gray-200 w-8 h-8 flex items-center justify-center mx-1">
|
||||
<span class="material-symbols text-2xl">info</span>
|
||||
</button>
|
||||
</ui-tooltip>
|
||||
<!-- Tour Tooltip icon -->
|
||||
|
||||
<nuxt-link to="/account" id="appbar-account-link" class="relative w-9 h-9 md:w-32 bg-fg border border-gray-500 rounded-sm shadow-xs ml-1.5 sm:ml-3 md:ml-5 md:pl-3 md:pr-10 py-2 text-left sm:text-sm cursor-pointer hover:bg-bg/40" aria-haspopup="listbox" aria-expanded="true">
|
||||
<span class="items-center hidden md:flex">
|
||||
<span class="block truncate">{{ username }}</span>
|
||||
|
|
@ -390,106 +382,8 @@ export default {
|
|||
batchAutoMatchClick() {
|
||||
this.$store.commit('globals/setShowBatchQuickMatchModal', true)
|
||||
},
|
||||
startTour() {
|
||||
const tour = new Shepherd.Tour({
|
||||
useModalOverlay: true,
|
||||
defaultStepOptions: {
|
||||
scrollTo: true,
|
||||
cancelIcon: { enabled: true },
|
||||
classes: 'shadow-md bg-primary text-white',
|
||||
highlightClass: 'bg-secondary'
|
||||
}
|
||||
})
|
||||
|
||||
tour.addStep({
|
||||
id: 'home-button',
|
||||
text: 'Click here to return to the homepage.',
|
||||
attachTo: {
|
||||
element: '#appbar-home-icon',
|
||||
on: 'bottom'
|
||||
},
|
||||
buttons: [{ text: 'Next', action: tour.next }]
|
||||
})
|
||||
|
||||
tour.addStep({
|
||||
id: 'library_icon',
|
||||
text: 'Click to select the library',
|
||||
attachTo: {
|
||||
element: '#appbar-library-icon',
|
||||
on: 'bottom'
|
||||
},
|
||||
buttons: [
|
||||
{ text: 'Back', action: tour.back },
|
||||
{ text: 'Next', action: tour.next }
|
||||
]
|
||||
})
|
||||
|
||||
tour.addStep({
|
||||
id: 'search-bar',
|
||||
text: 'Use this search bar to find any book.',
|
||||
attachTo: {
|
||||
element: '#appbar-search-bar',
|
||||
on: 'bottom'
|
||||
},
|
||||
buttons: [
|
||||
{ text: 'Back', action: tour.back },
|
||||
{ text: 'Next', action: tour.next }
|
||||
]
|
||||
})
|
||||
|
||||
tour.addStep({
|
||||
id: 'stats-icon',
|
||||
text: 'View your stats here.',
|
||||
attachTo: {
|
||||
element: '#appbar-stats',
|
||||
on: 'bottom'
|
||||
},
|
||||
buttons: [
|
||||
{ text: 'Back', action: tour.back },
|
||||
{ text: 'Next', action: tour.next }
|
||||
]
|
||||
})
|
||||
|
||||
tour.addStep({
|
||||
id: 'upload-button',
|
||||
text: 'Click here to upload a new audiobook.',
|
||||
attachTo: {
|
||||
element: '#appbar-upload-button',
|
||||
on: 'bottom'
|
||||
},
|
||||
buttons: [
|
||||
{ text: 'Back', action: tour.back },
|
||||
{ text: 'Next', action: tour.next }
|
||||
]
|
||||
})
|
||||
|
||||
tour.addStep({
|
||||
id: 'settings-icon',
|
||||
text: 'Access your app settings here.',
|
||||
attachTo: {
|
||||
element: '#appbar-settings-icon',
|
||||
on: 'bottom'
|
||||
},
|
||||
buttons: [
|
||||
{ text: 'Back', action: tour.back },
|
||||
{ text: 'Next', action: tour.next }
|
||||
]
|
||||
})
|
||||
|
||||
tour.addStep({
|
||||
id: 'account-link',
|
||||
text: 'Click here to access your account settings.',
|
||||
attachTo: {
|
||||
element: '#appbar-account-link',
|
||||
on: 'bottom'
|
||||
},
|
||||
buttons: [
|
||||
{ text: 'Back', action: tour.back },
|
||||
{ text: 'Done', action: tour.complete }
|
||||
]
|
||||
})
|
||||
|
||||
tour.start()
|
||||
startTour(tourId) {
|
||||
startTour(tourId)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
|
@ -503,6 +397,59 @@ export default {
|
|||
|
||||
<style>
|
||||
#appbar {
|
||||
box-shadow: 0px 5px 5px #11111155;
|
||||
box-shadow: 0px 5px 5px #bab4b4b6;
|
||||
}
|
||||
.shepherd-content {
|
||||
padding: 10px;
|
||||
background: #f36f3a;
|
||||
}
|
||||
.shepherd-header {
|
||||
background: #fbeedc;
|
||||
}
|
||||
.shepherd-cancel-icon {
|
||||
background: #4a2b1d;
|
||||
font-size: 30px;
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
line-height: 0;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.shepherd-extra-wrapper {
|
||||
background: #fbeedc;
|
||||
margin: 0 !important;
|
||||
padding: 10px;
|
||||
}
|
||||
.shepherd-text {
|
||||
color: #4a2b1d;
|
||||
background: #fbeedc;
|
||||
}
|
||||
.shepherd-footer {
|
||||
justify-content: center;
|
||||
background: #fbeedc;
|
||||
}
|
||||
.shepherd-button {
|
||||
background: #f36f3a;
|
||||
border-radius: 10px;
|
||||
color: #ffffff;
|
||||
}
|
||||
.shepherd-button:not(:disabled):hover {
|
||||
background: #e4572e;
|
||||
}
|
||||
.shepherd-progress-bar {
|
||||
width: 100%;
|
||||
height: 5px;
|
||||
background: #f36f3a;
|
||||
margin-top: 5px;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.shepherd-progress-bar span {
|
||||
display: block;
|
||||
height: 100%;
|
||||
background: #0ad93d;
|
||||
/* Change to match theme */
|
||||
width: 0%;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<div v-if="isShowingBookshelfToolbar" class="absolute top-0 -right-4 w-4 bg-bg h-10 pointer-events-none" />
|
||||
|
||||
<div id="siderail-buttons-container" role="navigation" aria-label="Library Navigation" :class="{ 'player-open': streamLibraryItem }" class="w-full overflow-y-auto overflow-x-hidden">
|
||||
<nuxt-link :to="`/library/${currentLibraryId}`" class="w-full h-20 flex flex-col items-center justify-center text-white border-b border-primary/70 hover:bg-primary cursor-pointer relative" :class="homePage ? 'bg-primary/80' : 'bg-bg/60'">
|
||||
<nuxt-link :to="`/library/${currentLibraryId}`" id="sidebar-home" class="w-full h-20 flex flex-col items-center justify-center text-white border-b border-primary/70 hover:bg-primary cursor-pointer relative" :class="homePage ? 'bg-primary/80' : 'bg-bg/60'">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<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>
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
<div v-show="isPodcastLatestPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf`" class="w-full h-20 flex flex-col items-center justify-center text-white border-b border-primary/70 hover:bg-primary cursor-pointer relative" :class="showLibrary ? 'bg-primary/80' : 'bg-bg/60'">
|
||||
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf`" id="sidebar-library" class="w-full h-20 flex flex-col items-center justify-center text-white border-b border-primary/70 hover:bg-primary cursor-pointer relative" :class="showLibrary ? 'bg-primary/80' : 'bg-bg/60'">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<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>
|
||||
|
|
@ -32,7 +32,7 @@
|
|||
<div v-show="showLibrary" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/series`" class="w-full h-20 flex flex-col items-center justify-center text-white/80 border-b border-primary/70 hover:bg-primary cursor-pointer relative" :class="isSeriesPage ? 'bg-primary/80' : 'bg-bg/60'">
|
||||
<nuxt-link v-if="isBookLibrary" id="sidebar-series" :to="`/library/${currentLibraryId}/bookshelf/series`" class="w-full h-20 flex flex-col items-center justify-center text-white/80 border-b border-primary/70 hover:bg-primary cursor-pointer relative" :class="isSeriesPage ? 'bg-primary/80' : 'bg-bg/60'">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<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>
|
||||
|
|
@ -42,7 +42,7 @@
|
|||
<div v-show="isSeriesPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="w-full h-20 flex flex-col items-center justify-center text-white/80 border-b border-primary/70 hover:bg-primary cursor-pointer relative" :class="paramId === 'collections' ? 'bg-primary/80' : 'bg-bg/60'">
|
||||
<nuxt-link v-if="isBookLibrary" id="sidebar-collection" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="w-full h-20 flex flex-col items-center justify-center text-white/80 border-b border-primary/70 hover:bg-primary cursor-pointer relative" :class="paramId === 'collections' ? 'bg-primary/80' : 'bg-bg/60'">
|
||||
<span class="material-symbols text-2xl"></span>
|
||||
|
||||
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonCollections }}</p>
|
||||
|
|
@ -50,7 +50,7 @@
|
|||
<div v-show="paramId === 'collections'" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link v-if="showPlaylists" :to="`/library/${currentLibraryId}/bookshelf/playlists`" class="w-full h-20 flex flex-col items-center justify-center text-white/80 border-b border-primary/70 hover:bg-primary cursor-pointer relative" :class="isPlaylistsPage ? 'bg-primary/80' : 'bg-bg/60'">
|
||||
<nuxt-link v-if="showPlaylists" id="sidebar-playlist" :to="`/library/${currentLibraryId}/bookshelf/playlists`" class="w-full h-20 flex flex-col items-center justify-center text-white/80 border-b border-primary/70 hover:bg-primary cursor-pointer relative" :class="isPlaylistsPage ? 'bg-primary/80' : 'bg-bg/60'">
|
||||
<span class="material-symbols text-2.5xl"></span>
|
||||
|
||||
<p class="pt-0.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonPlaylists }}</p>
|
||||
|
|
@ -58,7 +58,7 @@
|
|||
<div v-show="isPlaylistsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/authors`" class="w-full h-20 flex flex-col items-center justify-center text-white/80 border-b border-primary/70 hover:bg-primary cursor-pointer relative" :class="isAuthorsPage ? 'bg-primary/80' : 'bg-bg/60'">
|
||||
<nuxt-link v-if="isBookLibrary" id="sidebar-authors" :to="`/library/${currentLibraryId}/bookshelf/authors`" class="w-full h-20 flex flex-col items-center justify-center text-white/80 border-b border-primary/70 hover:bg-primary cursor-pointer relative" :class="isAuthorsPage ? 'bg-primary/80' : 'bg-bg/60'">
|
||||
<svg class="w-6 h-6" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
|
|
@ -71,7 +71,7 @@
|
|||
<div v-show="isAuthorsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/narrators`" class="w-full h-20 flex flex-col items-center justify-center text-white/80 border-b border-primary/70 hover:bg-primary cursor-pointer relative" :class="isNarratorsPage ? 'bg-primary/80' : 'bg-bg/60'">
|
||||
<nuxt-link v-if="isBookLibrary" id="sidebar-narrators" :to="`/library/${currentLibraryId}/narrators`" class="w-full h-20 flex flex-col items-center justify-center text-white/80 border-b border-primary/70 hover:bg-primary cursor-pointer relative" :class="isNarratorsPage ? 'bg-primary/80' : 'bg-bg/60'">
|
||||
<span class="material-symbols text-2xl"></span>
|
||||
|
||||
<p class="pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.LabelNarrators }}</p>
|
||||
|
|
@ -79,7 +79,7 @@
|
|||
<div v-show="isNarratorsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link v-if="isBookLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/stats`" class="w-full h-20 flex flex-col items-center justify-center text-white/80 border-b border-primary/70 hover:bg-primary cursor-pointer relative" :class="isStatsPage ? 'bg-primary/80' : 'bg-bg/60'">
|
||||
<nuxt-link v-if="isBookLibrary && userIsAdminOrUp" id="sidebar-stats" :to="`/library/${currentLibraryId}/stats`" class="w-full h-20 flex flex-col items-center justify-center text-white/80 border-b border-primary/70 hover:bg-primary cursor-pointer relative" :class="isStatsPage ? 'bg-primary/80' : 'bg-bg/60'">
|
||||
<span class="material-symbols text-2xl"></span>
|
||||
|
||||
<p class="pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonStats }}</p>
|
||||
|
|
@ -87,6 +87,38 @@
|
|||
<div v-show="isStatsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||
</nuxt-link>
|
||||
|
||||
<!-- cus -->
|
||||
<!-- Floating Action Button -->
|
||||
<div v-if="enableTourGuide">
|
||||
<button v-if="!showChild" @click="showChild = true" class="fixed bottom-32 right-4 w-12 h-12 rounded-full bg-tourGuide text-white flex items-center justify-center shadow-lg hover:bg-primary-dark z-50" title="Tour Guide">
|
||||
<span class="material-symbols">tour</span>
|
||||
</button>
|
||||
|
||||
<!-- Popup with Child Links -->
|
||||
<transition name="fade">
|
||||
<div v-if="showChild" class="fixed bottom-36 right-4 flex flex-col bg-bg/95 border border-primary rounded-xl shadow-lg z-50 w-40">
|
||||
<!-- Home Tour -->
|
||||
<div class="child-link flex items-center gap-2 px-4 py-2 border-b border-primary/40 cursor-pointer" :class="{ active: activeChild === 'home' }" @click="startAppBarTour">
|
||||
<span class="material-symbols">home</span>
|
||||
<p>Home Tour</p>
|
||||
</div>
|
||||
|
||||
<!-- Library Tour -->
|
||||
<div class="child-link flex items-center gap-2 px-4 py-2 border-b border-primary/40 cursor-pointer" :class="{ active: activeChild === 'library' }" @click="startLibraryTour">
|
||||
<span class="material-symbols">menu_book</span>
|
||||
<p>Library Tour</p>
|
||||
</div>
|
||||
|
||||
<!-- Close -->
|
||||
<button @click="showChild = false" class="px-4 py-2 text-sm text-white bg-red-600 hover:bg-red-700 rounded-b-xl">Close</button>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
<button @click="resetFirstVisit" class="fixed bottom-16 right-4 bg-red-500 text-white px-3 py-1 rounded shadow-md hover:bg-red-600 z-50">Reset Tour</button>
|
||||
|
||||
<!-- cus -->
|
||||
|
||||
<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/80 border-b border-primary/70 hover:bg-primary cursor-pointer relative" :class="isPodcastSearchPage ? 'bg-primary/80' : 'bg-bg/60'">
|
||||
<span class="abs-icons icon-podcast text-xl"></span>
|
||||
|
||||
|
|
@ -125,11 +157,19 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
import { startTour } from '~/plugins/tour'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
showChangelogModal: false
|
||||
showChangelogModal: false,
|
||||
showChild: false,
|
||||
activeParent: null,
|
||||
activeChild: null,
|
||||
isFirstVisit: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -221,14 +261,76 @@ export default {
|
|||
},
|
||||
showPlaylists() {
|
||||
return this.$store.state.libraries.numUserPlaylists > 0
|
||||
},
|
||||
enableTourGuide() {
|
||||
return this.$store.state.serverSettings?.enableTourGuide
|
||||
},
|
||||
activeParent() {
|
||||
// Parent is active if menu is open OR it's first visit
|
||||
return this.activeChild || this.showChild || this.isFirstVisit ? 'tour' : null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
clickChangelog() {
|
||||
this.showChangelogModal = true
|
||||
},
|
||||
toggleChild() {
|
||||
this.showChild = !this.showChild
|
||||
this.activeParent = this.showChild ? 'tour' : null
|
||||
this.activeChild = null
|
||||
this.isFirstVisit = false
|
||||
},
|
||||
startAppBarTour() {
|
||||
this.activeChild = 'home'
|
||||
startTour('appBar', {
|
||||
onComplete: () => {
|
||||
this.showChild = false
|
||||
this.activeParent = 'tour'
|
||||
}
|
||||
})
|
||||
},
|
||||
startLibraryTour() {
|
||||
this.activeChild = 'library'
|
||||
|
||||
// Use Vue 2 / Nuxt 2 $nextTick
|
||||
this.$nextTick(() => {
|
||||
startTour('libraryItem', {
|
||||
onComplete: () => {
|
||||
this.showChild = false
|
||||
this.activeParent = 'tour'
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
resetFirstVisit() {
|
||||
localStorage.removeItem('tourGuideVisited')
|
||||
this.isFirstVisit = true
|
||||
this.showChild = false
|
||||
this.activeParent = 'tour'
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
mounted() {
|
||||
// Listen to the event from the button in template
|
||||
this.$eventBus.$on('start-library-tour', () => {
|
||||
this.startLibraryTour()
|
||||
})
|
||||
|
||||
// Existing first visit logic
|
||||
const hasVisited = localStorage.getItem('tourGuideVisited')
|
||||
if (!hasVisited) {
|
||||
this.showChild = true
|
||||
this.activeParent = 'tour'
|
||||
this.isFirstVisit = true
|
||||
|
||||
this.startAppBarTour()
|
||||
|
||||
localStorage.setItem('tourGuideVisited', 'true')
|
||||
}
|
||||
},
|
||||
beforeUnmount() {
|
||||
// Clean up the event listener
|
||||
this.$eventBus.$off('start-library-tour')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
@ -239,4 +341,49 @@ export default {
|
|||
#siderail-buttons-container.player-open {
|
||||
max-height: calc(100vh - 64px - 48px - 160px);
|
||||
}
|
||||
.child-link {
|
||||
font-size: 12px;
|
||||
}
|
||||
.child-link:hover {
|
||||
background-color: #92622a;
|
||||
}
|
||||
.child-link.active {
|
||||
background-color: #92622a;
|
||||
}
|
||||
.bg-tourGuide {
|
||||
background-color: #92622a;
|
||||
}
|
||||
.bg-tourGuide .text-tg {
|
||||
font-size: 10px;
|
||||
}
|
||||
.parent-link.first-time {
|
||||
animation: pulse-highlight 1.5s ease-in-out 3;
|
||||
}
|
||||
|
||||
/* Pulse keyframes */
|
||||
@keyframes pulse-highlight {
|
||||
0%,
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(255, 165, 0, 0.5);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 10px 5px rgba(255, 165, 0, 0.7);
|
||||
}
|
||||
}
|
||||
|
||||
/* Transition for tour guide open/close */
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
height: 0;
|
||||
}
|
||||
.fade-enter-to,
|
||||
.fade-leave-from {
|
||||
opacity: 1;
|
||||
height: auto;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
5657
client/package-lock.json
generated
5657
client/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -130,6 +130,18 @@
|
|||
</ui-tooltip>
|
||||
</div>
|
||||
|
||||
<!-- Tour Guide -->
|
||||
<div class="flex items-center py-2">
|
||||
<ui-toggle-switch labeledBy="settings-enable-tour-guide" v-model="newServerSettings.enableTourGuide" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('enableTourGuide', val)" />
|
||||
<ui-tooltip :text="'Enable or disable the in-app tour guide feature'">
|
||||
<p class="pl-4">
|
||||
<span id="settings-enable-tour-guide">Enable Tour Guide</span>
|
||||
<span class="material-symbols icon-text">info</span>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
<!-- Tour Guide -->
|
||||
|
||||
<div class="grow py-2">
|
||||
<ui-dropdown :label="$strings.LabelSettingsDateFormat" v-model="newServerSettings.dateFormat" :items="dateFormats" small class="max-w-72" @input="(val) => updateSettingsKey('dateFormat', val)" />
|
||||
<p class="text-xs ml-1 text-white/60">{{ $strings.LabelExample }}: {{ dateExample }}</p>
|
||||
|
|
@ -369,6 +381,11 @@ export default {
|
|||
|
||||
this.homepageUseBookshelfView = this.newServerSettings.homeBookshelfView != this.$constants.BookshelfView.DETAIL
|
||||
this.useBookshelfView = this.newServerSettings.bookshelfView != this.$constants.BookshelfView.DETAIL
|
||||
|
||||
// Initialize Tour Guide toggle
|
||||
if (this.newServerSettings.enableTourGuide === undefined) {
|
||||
this.newServerSettings.enableTourGuide = false
|
||||
}
|
||||
},
|
||||
purgeCache() {
|
||||
this.showConfirmPurgeCache = true
|
||||
|
|
|
|||
|
|
@ -4,8 +4,9 @@
|
|||
<div class="flex flex-col lg:flex-row max-w-6xl mx-auto">
|
||||
<div class="w-full flex justify-center lg:block lg:w-52" style="min-width: 208px">
|
||||
<div class="relative group" style="height: fit-content">
|
||||
<covers-book-cover class="relative group-hover:brightness-75 transition cursor-pointer" expand-on-click :library-item="libraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
|
||||
<div id="covers-book-cover">
|
||||
<covers-book-cover class="relative group-hover:brightness-75 transition cursor-pointer" expand-on-click :library-item="libraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
</div>
|
||||
<!-- Item Progress Bar -->
|
||||
<div v-if="!isPodcast" class="absolute bottom-0 left-0 h-1.5 shadow-xs z-10" :class="userIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: 208 * progressPercent + 'px' }"></div>
|
||||
|
||||
|
|
@ -24,7 +25,7 @@
|
|||
<div class="grow px-2 py-6 lg:py-0 md:px-10">
|
||||
<div class="flex justify-center">
|
||||
<div class="mb-4">
|
||||
<h1 class="text-2xl md:text-3xl font-semibold">
|
||||
<h1 class="text-2xl md:text-3xl font-semibold" id="book-title">
|
||||
<div class="flex items-center">
|
||||
{{ title }}
|
||||
<widgets-explicit-indicator v-if="isExplicit" />
|
||||
|
|
@ -32,7 +33,7 @@
|
|||
</div>
|
||||
</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" id="book-sub-title">{{ bookSubtitle }}</p>
|
||||
|
||||
<template v-for="(_series, index) in seriesList">
|
||||
<nuxt-link :key="_series.id" :to="`/library/${libraryId}/series/${_series.id}`" class="hover:underline font-sans text-gray-300 text-lg leading-7">{{ _series.text }}</nuxt-link
|
||||
|
|
@ -43,7 +44,7 @@
|
|||
<p v-else-if="authors.length" class="mb-2 mt-0.5 text-gray-200 text-lg md:text-xl max-w-[calc(100vw-2rem)] overflow-hidden text-ellipsis">
|
||||
{{ $getString('LabelByAuthor', ['']) }}<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>
|
||||
<p v-else class="mb-2 mt-0.5 text-gray-200 text-xl">by Unknown</p>
|
||||
<p v-else class="mb-2 mt-0.5 text-gray-200 text-xl" id="item-author">by Unknown</p>
|
||||
|
||||
<content-library-item-details :library-item="libraryItem" />
|
||||
</div>
|
||||
|
|
@ -123,7 +124,7 @@
|
|||
</div>
|
||||
|
||||
<div class="my-4 w-full">
|
||||
<div ref="description" id="item-description" dir="auto" role="paragraph" class="default-style less-spacing text-base text-gray-100 whitespace-pre-line mb-1" :class="{ 'show-full': showFullDescription }" v-html="description" />
|
||||
<div ref="description" dir="auto" role="paragraph" class="default-style less-spacing text-base text-gray-100 whitespace-pre-line mb-1" :class="{ 'show-full': showFullDescription }" v-html="description" />
|
||||
|
||||
<button v-if="isDescriptionClamped" class="py-0.5 flex items-center text-slate-300 hover:text-white" @click="showFullDescription = !showFullDescription">{{ showFullDescription ? $strings.ButtonReadLess : $strings.ButtonReadMore }} <span class="material-symbols text-xl pl-1" v-html="showFullDescription ? 'expand_less' : ''" /></button>
|
||||
</div>
|
||||
|
|
@ -147,6 +148,8 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { startTour } from '~/plugins/tour'
|
||||
|
||||
export default {
|
||||
async asyncData({ store, params, app, redirect, route }) {
|
||||
if (!store.state.user.user) {
|
||||
|
|
@ -795,6 +798,22 @@ export default {
|
|||
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_cleared', this.episodeDownloadQueueCleared)
|
||||
// Existing mounted code...
|
||||
this.checkDescriptionClamped()
|
||||
|
||||
this.$eventBus.$on('start-library-tour', () => {
|
||||
if (this.libraryItem && this.libraryItem.id) {
|
||||
this.$nextTick(() => startTour('libraryItem'))
|
||||
} else {
|
||||
// Wait until libraryItem is loaded
|
||||
const unwatch = this.$watch('libraryItem', (newVal) => {
|
||||
if (newVal && newVal.id) {
|
||||
this.$nextTick(() => startTour('libraryItem'))
|
||||
unwatch()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$eventBus.$off(`${this.libraryItem.id}_updated`, this.libraryItemUpdated)
|
||||
|
|
@ -807,6 +826,7 @@ export default {
|
|||
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_cleared', this.episodeDownloadQueueCleared)
|
||||
this.$eventBus.$off('start-library-tour')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,24 +1,190 @@
|
|||
// client/plugins/tour.js
|
||||
import Shepherd from 'shepherd.js'
|
||||
import 'shepherd.js/dist/css/shepherd.css'
|
||||
|
||||
export default (context, inject) => {
|
||||
const defaultTourOptions = {
|
||||
defaultStepOptions: {
|
||||
cancelIcon: {
|
||||
enabled: true
|
||||
},
|
||||
scrollTo: { behavior: 'smooth', block: 'center' },
|
||||
classes: 'shadow-md bg-purple-dark text-white rounded p-3'
|
||||
let tour = null
|
||||
|
||||
const tours = {
|
||||
appBar: [
|
||||
{ id: 'home-button', text: 'Click here to return to the homepage.', attachTo: { element: '#appbar-home-icon', on: 'bottom' } },
|
||||
{ id: 'library-icon', text: 'Click to select the library.', attachTo: { element: '#appbar-library-icon', on: 'bottom' } },
|
||||
{ id: 'search-bar', text: 'Use this search bar to find any book.', attachTo: { element: '#appbar-search-bar', on: 'bottom' } },
|
||||
{ id: 'stats-icon', text: 'View your stats here.', attachTo: { element: '#appbar-stats', on: 'bottom' } },
|
||||
{ id: 'upload-button', text: 'Click here to upload a new audiobook.', attachTo: { element: '#appbar-upload-button', on: 'bottom' } },
|
||||
{ id: 'settings-icon', text: 'Access your app settings here.', attachTo: { element: '#appbar-settings-icon', on: 'bottom' } },
|
||||
{ id: 'account-link', text: 'Click here to access your account settings.', attachTo: { element: '#appbar-account-link', on: 'bottom' } },
|
||||
{ id: 'sidebar-home', text: 'This is your homepage where you can see your recent activity.', attachTo: { element: '#sidebar-home', on: 'right' } },
|
||||
{ id: 'sidebar-library', text: 'Access your entire audiobook library here.', attachTo: { element: '#sidebar-library', on: 'right' } },
|
||||
{ id: 'sidebar-series', text: 'Quick access to your series.', attachTo: { element: '#sidebar-series', on: 'right' } },
|
||||
{ id: 'sidebar-collection', text: 'View your books collection.', attachTo: { element: '#sidebar-collection', on: 'right' } },
|
||||
{ id: 'sidebar-playlists', text: 'Manage your playlists from this section.', attachTo: { element: '#sidebar-playlists', on: 'right' } },
|
||||
{ id: 'sidebar-authors', text: 'Manage your authors from this section.', attachTo: { element: '#sidebar-authors', on: 'right' } },
|
||||
{ id: 'sidebar-narrators', text: 'View narrators and their books from this section.', attachTo: { element: '#sidebar-narrators', on: 'right' } },
|
||||
{ id: 'sidebar-stats', text: 'This section will provide all the stats in audio book shelf.', attachTo: { element: '#sidebar-stats', on: 'right' } }
|
||||
],
|
||||
libraryItem: [
|
||||
{
|
||||
id: 'item-cover',
|
||||
text: 'This is the book cover. Click to expand or edit.',
|
||||
attachTo: { element: '#covers-book-cover', on: 'right' },
|
||||
canClickTarget: false // stops Shepherd from triggering the click
|
||||
},
|
||||
useModalOverlay: true
|
||||
}
|
||||
|
||||
const createTour = (steps = []) => {
|
||||
const tour = new Shepherd.Tour(defaultTourOptions)
|
||||
steps.forEach((step) => tour.addStep(step))
|
||||
return tour
|
||||
}
|
||||
|
||||
inject('createTour', createTour)
|
||||
{
|
||||
id: 'item-title',
|
||||
text: 'Here is the title and subtitle of the item.',
|
||||
attachTo: { element: '#book-title', on: 'right' }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export function startTour(tourId = 'appBar') {
|
||||
if (!tours[tourId]) return console.warn('Tour not found:', tourId)
|
||||
|
||||
tour = new Shepherd.Tour({
|
||||
useModalOverlay: true,
|
||||
defaultStepOptions: {
|
||||
scrollTo: true,
|
||||
cancelIcon: { enabled: true },
|
||||
popperOptions: { modifiers: [{ name: 'preventOverflow', options: { boundary: 'viewport' } }] },
|
||||
classes: 'shadow-md bg-primary text-white',
|
||||
highlightClass: 'bg-secondary',
|
||||
container: 'body',
|
||||
when: {
|
||||
show() {
|
||||
requestAnimationFrame(() => {
|
||||
const content = this.el.querySelector('.shepherd-content') || this.el.querySelector('.shepherd-body')
|
||||
if (!content) return
|
||||
|
||||
const existingWrapper = content.querySelector('.shepherd-extra-wrapper')
|
||||
if (existingWrapper) existingWrapper.remove()
|
||||
|
||||
const wrapper = document.createElement('div')
|
||||
wrapper.className = 'shepherd-extra-wrapper'
|
||||
wrapper.style.display = 'flex'
|
||||
wrapper.style.flexDirection = 'column'
|
||||
wrapper.style.alignItems = 'center'
|
||||
wrapper.style.margin = '10px 0'
|
||||
|
||||
// Progress bar
|
||||
const progress = document.createElement('div')
|
||||
progress.style.height = '6px'
|
||||
progress.style.width = '100%'
|
||||
progress.style.background = '#eee'
|
||||
progress.style.borderRadius = '3px'
|
||||
progress.style.marginBottom = '10px'
|
||||
|
||||
const innerBar = document.createElement('span')
|
||||
innerBar.style.display = 'block'
|
||||
innerBar.style.height = '100%'
|
||||
innerBar.style.background = '#E4572E'
|
||||
innerBar.style.borderRadius = '3px'
|
||||
const currentStepIndex = tour.steps.indexOf(this) + 1
|
||||
innerBar.style.width = `${(currentStepIndex / tour.steps.length) * 100}%`
|
||||
|
||||
progress.appendChild(innerBar)
|
||||
|
||||
// Image slider
|
||||
const slider = document.createElement('div')
|
||||
slider.className = 'tour-image-slider'
|
||||
slider.style.position = 'relative'
|
||||
slider.style.width = '200px'
|
||||
slider.style.height = '120px'
|
||||
slider.style.overflow = 'hidden'
|
||||
slider.style.marginTop = '10px'
|
||||
|
||||
// Correct public paths
|
||||
const images = ['/book_placeholder.jpg', '/images/step2.png', '/images/step3.png']
|
||||
let currentIndex = 0
|
||||
|
||||
const imgEl = document.createElement('img')
|
||||
imgEl.src = images[currentIndex]
|
||||
imgEl.alt = 'Tour Image'
|
||||
imgEl.style.width = '100%'
|
||||
imgEl.style.height = '100%'
|
||||
imgEl.style.objectFit = 'contain'
|
||||
slider.appendChild(imgEl)
|
||||
|
||||
// Prev button
|
||||
const prevBtn = document.createElement('button')
|
||||
prevBtn.textContent = '<'
|
||||
prevBtn.style.position = 'absolute'
|
||||
prevBtn.style.left = '5px'
|
||||
prevBtn.style.top = '50%'
|
||||
prevBtn.style.transform = 'translateY(-50%)'
|
||||
prevBtn.style.background = 'rgba(0,0,0,0.5)'
|
||||
prevBtn.style.color = '#fff'
|
||||
prevBtn.style.border = 'none'
|
||||
prevBtn.style.borderRadius = '50%'
|
||||
prevBtn.style.width = '25px'
|
||||
prevBtn.style.height = '25px'
|
||||
prevBtn.onclick = () => {
|
||||
currentIndex = (currentIndex - 1 + images.length) % images.length
|
||||
imgEl.src = images[currentIndex]
|
||||
}
|
||||
|
||||
// Next button
|
||||
const nextBtn = document.createElement('button')
|
||||
nextBtn.textContent = '>'
|
||||
nextBtn.style.position = 'absolute'
|
||||
nextBtn.style.right = '5px'
|
||||
nextBtn.style.top = '50%'
|
||||
nextBtn.style.transform = 'translateY(-50%)'
|
||||
nextBtn.style.background = 'rgba(0,0,0,0.5)'
|
||||
nextBtn.style.color = '#fff'
|
||||
nextBtn.style.border = 'none'
|
||||
nextBtn.style.borderRadius = '50%'
|
||||
nextBtn.style.width = '25px'
|
||||
nextBtn.style.height = '25px'
|
||||
nextBtn.onclick = () => {
|
||||
currentIndex = (currentIndex + 1) % images.length
|
||||
imgEl.src = images[currentIndex]
|
||||
}
|
||||
|
||||
slider.appendChild(prevBtn)
|
||||
slider.appendChild(nextBtn)
|
||||
|
||||
// Add to wrapper
|
||||
wrapper.appendChild(progress)
|
||||
wrapper.appendChild(slider)
|
||||
|
||||
const footer = content.querySelector('.shepherd-footer')
|
||||
if (footer) content.insertBefore(wrapper, footer)
|
||||
else content.appendChild(wrapper)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const addStepSafely = (step) => {
|
||||
const el = document.querySelector(step.attachTo?.element)
|
||||
if (!el) return false
|
||||
tour.addStep(step)
|
||||
return true
|
||||
}
|
||||
|
||||
const steps = tours[tourId].map((step, idx) => {
|
||||
const stepWithButtons = { ...step, buttons: [] }
|
||||
if (idx > 0) stepWithButtons.buttons.push({ text: 'Back', action: () => tour.back() })
|
||||
stepWithButtons.buttons.push({
|
||||
text: idx === tours[tourId].length - 1 ? 'Done' : 'Next',
|
||||
action: idx === tours[tourId].length - 1 ? () => tour.complete() : () => tour.next()
|
||||
})
|
||||
return stepWithButtons
|
||||
})
|
||||
|
||||
if (tourId === 'libraryItem') {
|
||||
// Wait for all elements to exist before adding steps and starting tour
|
||||
const interval = setInterval(() => {
|
||||
const allExist = steps.every((step) => document.querySelector(step.attachTo?.element))
|
||||
if (allExist) {
|
||||
steps.forEach((step) => tour.addStep(step))
|
||||
clearInterval(interval)
|
||||
tour.start()
|
||||
}
|
||||
}, 200)
|
||||
} else {
|
||||
// AppBar tour: add steps immediately
|
||||
steps.forEach((step) => tour.addStep(step))
|
||||
tour.start()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
BIN
client/public/images/audioBookShelf.png
Normal file
BIN
client/public/images/audioBookShelf.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1 MiB |
Binary file not shown.
Binary file not shown.
2866
package-lock.json
generated
2866
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -83,6 +83,9 @@ class ServerSettings {
|
|||
this.authOpenIDAdvancedPermsClaim = ''
|
||||
this.authOpenIDSubfolderForRedirectURLs = undefined
|
||||
|
||||
//Tour Guide - be initialized with default false
|
||||
this.enableTourGuide = settings.enableTourGuide !== undefined ? !!settings.enableTourGuide : true
|
||||
|
||||
if (settings) {
|
||||
this.construct(settings)
|
||||
}
|
||||
|
|
@ -147,6 +150,9 @@ class ServerSettings {
|
|||
this.authOpenIDAdvancedPermsClaim = settings.authOpenIDAdvancedPermsClaim || ''
|
||||
this.authOpenIDSubfolderForRedirectURLs = settings.authOpenIDSubfolderForRedirectURLs
|
||||
|
||||
//Tour Guide - update when saving
|
||||
this.enableTourGuide = !!settings.enableTourGuide
|
||||
|
||||
if (!Array.isArray(this.authActiveAuthMethods)) {
|
||||
this.authActiveAuthMethods = ['local']
|
||||
}
|
||||
|
|
@ -255,7 +261,8 @@ class ServerSettings {
|
|||
authOpenIDMobileRedirectURIs: this.authOpenIDMobileRedirectURIs, // Do not return to client
|
||||
authOpenIDGroupClaim: this.authOpenIDGroupClaim, // Do not return to client
|
||||
authOpenIDAdvancedPermsClaim: this.authOpenIDAdvancedPermsClaim, // Do not return to client
|
||||
authOpenIDSubfolderForRedirectURLs: this.authOpenIDSubfolderForRedirectURLs
|
||||
authOpenIDSubfolderForRedirectURLs: this.authOpenIDSubfolderForRedirectURLs,
|
||||
enableTourGuide: this.enableTourGuide // return to frontend via API
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue