Implement PDF reader zoom controls and fix theme support

This commit is contained in:
Farhan ALVY 2025-09-30 02:30:03 +00:00
parent 32da0f1224
commit c4e59f61a4
24 changed files with 961 additions and 112 deletions

View file

@ -3,11 +3,12 @@
@import './draggable.css'; @import './draggable.css';
@import './defaultStyles.css'; @import './defaultStyles.css';
@import './absicons.css'; @import './absicons.css';
@import './theme.css';
/*
:root { :root {
--bookshelf-texture-img: url(~static/textures/wood_default.jpg); --bookshelf-texture-img: url(~static/textures/wood_default.jpg);
--bookshelf-divider-bg: linear-gradient(180deg, rgba(149, 119, 90, 1) 0%, rgba(103, 70, 37, 1) 17%, rgba(103, 70, 37, 1) 88%, rgba(71, 48, 25, 1) 100%); --bookshelf-divider-bg: linear-gradient(180deg, rgba(149, 119, 90, 1) 0%, rgba(103, 70, 37, 1) 17%, rgba(103, 70, 37, 1) 88%, rgba(71, 48, 25, 1) 100%);
} } */
.page { .page {
width: 100%; width: 100%;
@ -22,8 +23,7 @@
#bookshelf { #bookshelf {
height: calc(100% - 40px); height: calc(100% - 40px);
background-image: linear-gradient(to right bottom, #2e2e2e, #303030, #313131, #333333, #353535, #343434, #323232, #313131, #2c2c2c, #282828, #232323, #1f1f1f); background-image: var(--gradient-bookshelf) !important;
/* For Firefox */ /* For Firefox */
scrollbar-width: thin; scrollbar-width: thin;
scrollbar-color: #855620 rgba(0, 0, 0, 0); scrollbar-color: #855620 rgba(0, 0, 0, 0);
@ -44,7 +44,7 @@
} }
#page-wrapper { #page-wrapper {
background-image: linear-gradient(to right bottom, #2e2e2e, #303030, #313131, #333333, #353535, #343434, #323232, #313131, #2c2c2c, #282828, #232323, #1f1f1f); background-image: var(--gradient-bookshelf) !important;
} }
/* width */ /* width */

345
client/assets/theme.css Normal file
View file

@ -0,0 +1,345 @@
:root,
:root.dark {
/* Main backgrounds - from your existing design */
--color-bg: #373838; /* From tailwind.css --color-bg */
--color-primary: #232323; /* From tailwind.css --color-primary */
--color-fg: #2e2e2e; /* From your existing even rows */
/* Gradient backgrounds - from your bookshelf design */
--gradient-bookshelf: linear-gradient(to right bottom, #2e2e2e, #303030, #313131, #333333, #353535, #343434, #323232, #313131, #2c2c2c, #282828, #232323, #1f1f1f);
/* Text colors */
--color-text: #ffffff;
--color-text-muted: #bbbbbb; /* From --color-black-50 */
--color-text-dim: #666666; /* From --color-black-100 */
/* Border colors - from your existing table styles */
--color-border: #474747; /* From tracksTable border */
--color-border-light: #555555; /* From --color-black-200 */
/* Interactive colors */
--color-hover: #474747; /* From tracksTable hover */
--color-accent: #1ad691; /* From tailwind.css --color-accent */
--color-success: #4caf50; /* From tailwind.css --color-success */
--color-darkgreen: rgb(34, 127, 35); /* From tailwind.css --color-darkgreen */
/* Status colors */
--color-warning: #fb8c00; /* From tailwind.css --color-warning */
--color-error: #ff5252; /* From tailwind.css --color-error */
--color-info: #2196f3; /* From tailwind.css --color-info */
/* Scrollbar colors - from your existing design */
--scrollbar-thumb: #855620;
--scrollbar-thumb-hover: #704922;
/* Shadow colors */
--color-shadow-light: rgba(17, 17, 17, 0.4); /* #111111aa */
--color-shadow-medium: rgba(17, 17, 17, 0.6); /* #111111ee */
--color-shadow-heavy: rgba(17, 17, 17, 0.8);
/* Bookshelf specific - keep your existing design */
--bookshelf-texture-img: url(~static/textures/wood_default.jpg);
--bookshelf-divider-bg: linear-gradient(180deg, rgba(149, 119, 90, 1) 0%, rgba(103, 70, 37, 1) 17%, rgba(103, 70, 37, 1) 88%, rgba(71, 48, 25, 1) 100%);
/* Special effects */
--shimmer-bg: linear-gradient(315deg, #19191a 0%, rgb(15, 15, 15) 74%);
--gold-border: rgba(255, 244, 182, 0.6);
--gold-text: #fce3a6;
--table-border: #474747;
--table-header-bg: #272727;
--table-row-bg: #373838;
--table-row-alt-bg: #2f2f2f;
--table-row-hover-bg: #474747;
--table-text-color: #e0e0e0;
}
/* Light theme */
:root.light {
/* Main backgrounds - lighter equivalents */
--color-bg: #f8f9fa;
--color-primary: #ffffff;
--color-fg: #f1f3f4;
/* Gradient backgrounds - light version */
--gradient-bookshelf: linear-gradient(to right bottom, #f8f9fa, #f1f3f4, #e9ecef, #dee2e6, #ced4da, #adb5bd, #868e96, #6c757d, #495057, #343a40, #212529, #000000);
/* Text colors */
--color-text: #212529;
--color-text-muted: #495057;
--color-text-dim: #868e96;
/* Border colors */
--color-border: #dee2e6;
--color-border-light: #e9ecef;
/* Interactive colors */
--color-hover: #e9ecef;
--color-accent: #20c997; /* Slightly adjusted for light mode */
--color-success: #198754;
--color-darkgreen: rgb(25, 135, 84);
/* Status colors - adjusted for light mode */
--color-warning: #fd7e14;
--color-error: #dc3545;
--color-info: #0d6efd;
/* Scrollbar colors */
--scrollbar-thumb: #adb5bd;
--scrollbar-thumb-hover: #868e96;
/* Shadow colors */
--color-shadow-light: rgba(0, 0, 0, 0.1);
--color-shadow-medium: rgba(0, 0, 0, 0.2);
--color-shadow-heavy: rgba(0, 0, 0, 0.3);
/* Bookshelf specific - lighter wood texture */
--bookshelf-texture-img: url(~static/textures/wood_default.jpg);
--bookshelf-divider-bg: linear-gradient(180deg, rgba(200, 170, 140, 1) 0%, rgba(180, 150, 120, 1) 17%, rgba(180, 150, 120, 1) 88%, rgba(160, 130, 100, 1) 100%);
/* Special effects */
--shimmer-bg: linear-gradient(315deg, #f8f9fa 0%, #e9ecef 74%);
--gold-border: rgba(133, 86, 32, 0.6);
--gold-text: #855620;
--table-border: #ccc;
--table-header-bg: #f0f0f0;
--table-row-bg: #ffffff;
--table-row-alt-bg: #f9f9f9;
--table-row-hover-bg: #e6e6e6;
--table-text-color: #333;
}
/* Smooth transitions for theme changes */
* {
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease, fill 0.3s ease;
}
body {
color: var(--color-text);
}
.userSessionsTable {
color: var(--table-text-color);
}
/* Update common classes to use CSS variables */
.bg-bg {
background-color: var(--color-bg) !important;
}
.bg-primary {
background-color: var(--color-primary) !important;
}
.bg-fg {
background-color: var(--color-fg) !important;
}
.text-white,
.text-themed {
color: var(--color-text) !important;
}
.text-gray-100 {
color: var(--color-text-muted) !important;
}
.text-gray-200 {
color: var(--color-text-muted) !important;
}
.text-gray-300 {
color: var(--color-text-muted) !important;
}
.text-gray-400 {
color: var(--color-text-dim) !important;
}
.border-gray-500 {
border-color: var(--color-border) !important;
}
.shadow-xs {
box-shadow: 0 1px 2px var(--color-shadow) !important;
}
/* Ensure SVG icons also use theme colors */
svg {
color: var(--color-text);
fill: currentColor;
}
/* Material symbols and icons */
.material-symbols,
.material-icons {
color: var(--color-text) !important;
}
.tooltip-content,
.ui-tooltip-content {
background-color: var(--color-primary) !important;
color: var(--color-text) !important;
border: 1px solid var(--color-border) !important;
}
/* For Popper.js or similar tooltip libraries */
.popper,
.tippy-box {
background-color: var(--color-primary) !important;
color: var(--color-text) !important;
border: 1px solid var(--color-border) !important;
}
.tippy-arrow {
color: var(--color-primary) !important;
}
/* Ensure text is readable */
.ui-tooltip *,
.tooltip * {
color: var(--color-text) !important;
}
/* More specific overrides for common tooltip libraries */
[data-tippy-root] {
--tippy-color: var(--color-text) !important;
--tippy-bg: var(--color-primary) !important;
}
/* Vue/Nuxt specific tooltip classes */
.v-tooltip-content {
background: var(--color-primary) !important;
color: var(--color-text) !important;
border: 1px solid var(--color-border) !important;
}
/* Override any hardcoded dark/light theme classes */
.tooltip-dark,
.tooltip-light {
background-color: var(--color-primary) !important;
color: var(--color-text) !important;
border-color: var(--color-border) !important;
}
/* Force override for stubborn tooltips */
[class*="tooltip"],
[class*="popover"] {
background-color: var(--color-primary) !important;
color: var(--color-text) !important;
border-color: var(--color-border) !important;
}
/* READERS */
/* PDF Reader dark mode */
.pdf-reader-container {
background-color: var(--color-primary);
color: var(--color-text);
}
/* Ensure proper contrast for controls */
.pdf-reader-container .material-symbols {
color: var(--color-text);
}
/* Make sure the viewer area has proper background */
.pdf-reader-container .pdf-viewer {
background-color: var(--color-primary);
}
/* Modal specific overrides */
.modal-content {
background-color: var(--color-fg) !important;
color: var(--color-text) !important;
}
/* Input fields - general styling that works for both themes */
input,
textarea,
select {
background-color: var(--color-primary);
color: var(--color-text);
border-color: var(--color-border-light);
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
}
/* Input focus states */
input:focus,
textarea:focus,
select:focus {
border-color: var(--color-accent);
outline: none;
box-shadow: 0 0 0 2px rgba(var(--color-accent), 0.2);
}
/* Button styling - general that works for both themes */
.ui-btn,
button {
background-color: var(--color-primary);
color: var(--color-text);
border-color: var(--color-border-light);
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
}
/* Button hover states */
.ui-btn:hover,
button:hover {
background-color: var(--color-hover);
}
/* Tab styling */
.tab {
background-color: var(--color-primary);
color: var(--color-text);
border-color: var(--color-border);
transition: background-color 0.3s ease;
}
/* Tab hover states */
.tab:hover {
background-color: var(--color-hover);
}
/* Tab active states */
.tab.active,
.tab[aria-selected="true"] {
background-color: var(--color-fg);
border-color: var(--color-accent);
}
/* Dropdown menus */
.dropdown-menu,
select option {
background-color: var(--color-primary);
color: var(--color-text);
border-color: var(--color-border);
}
/* Form validation states */
input.error,
textarea.error {
border-color: var(--color-error);
}
input.success,
textarea.success {
border-color: var(--color-success);
}
/* Placeholder text */
input::placeholder,
textarea::placeholder {
color: var(--color-text-dim);
}
/* Disabled states */
input:disabled,
textarea:disabled,
select:disabled,
button:disabled,
.ui-btn:disabled {
background-color: var(--color-fg);
color: var(--color-text-dim);
border-color: var(--color-border);
opacity: 0.6;
cursor: not-allowed;
}

View file

@ -21,7 +21,13 @@
<div v-if="isChromecastInitialized" class="w-6 min-w-6 h-6 ml-2 mr-1 sm:mx-2 cursor-pointer"> <div v-if="isChromecastInitialized" class="w-6 min-w-6 h-6 ml-2 mr-1 sm:mx-2 cursor-pointer">
<google-cast-launcher></google-cast-launcher> <google-cast-launcher></google-cast-launcher>
</div> </div>
<ui-tooltip :text="isDarkMode ? 'Switch to Light Mode' : 'Switch to Dark Mode'" direction="bottom" class="flex items-center">
<button @click="toggleTheme" class="hover:text-gray-200 cursor-pointer w-8 h-8 flex items-center justify-center mx-1" :aria-label="isDarkMode ? 'Switch to Light Mode' : 'Switch to Dark Mode'">
<span class="material-symbols text-2xl">
{{ isDarkMode ? '&#xe518;' : '&#xe51c;' }}
</span>
</button>
</ui-tooltip>
<widgets-notification-widget class="hidden md:block" /> <widgets-notification-widget class="hidden md:block" />
<nuxt-link v-if="currentLibrary" to="/config/stats" class="hover:text-gray-200 cursor-pointer w-8 h-8 hidden sm:flex items-center justify-center mx-1"> <nuxt-link v-if="currentLibrary" to="/config/stats" class="hover:text-gray-200 cursor-pointer w-8 h-8 hidden sm:flex items-center justify-center mx-1">
@ -158,6 +164,9 @@ export default {
isHttps() { isHttps() {
return location.protocol === 'https:' || process.env.NODE_ENV === 'development' return location.protocol === 'https:' || process.env.NODE_ENV === 'development'
}, },
isDarkMode() {
return this.$store.getters['theme/isDarkMode']
},
contextMenuItems() { contextMenuItems() {
if (!this.userIsAdminOrUp) return [] if (!this.userIsAdminOrUp) return []
@ -375,6 +384,9 @@ export default {
}, },
batchAutoMatchClick() { batchAutoMatchClick() {
this.$store.commit('globals/setShowBatchQuickMatchModal', true) this.$store.commit('globals/setShowBatchQuickMatchModal', true)
},
toggleTheme() {
this.$store.dispatch('theme/toggleTheme')
} }
}, },
mounted() { mounted() {

View file

@ -14,7 +14,7 @@
<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>
<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/70 hover:bg-primary cursor-pointer relative" :class="isPodcastLatestPage ? 'bg-primary/80' : 'bg-bg/60'"> <nuxt-link v-if="isPodcastLibrary" :to="`/library/${currentLibraryId}/podcast/latest`" class="w-full h-20 flex flex-col items-center justify-center text-themed border-b border-primary/70 hover:bg-primary cursor-pointer relative" :class="isPodcastLatestPage ? 'bg-primary/80' : 'bg-bg/60'">
<span class="material-symbols text-2xl">&#xe241;</span> <span class="material-symbols text-2xl">&#xe241;</span>
<p class="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>
@ -22,7 +22,7 @@
<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>
<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`" class="w-full h-20 flex flex-col items-center justify-center text-themed 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"> <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" /> <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>
@ -32,7 +32,7 @@
<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>
<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" :to="`/library/${currentLibraryId}/bookshelf/series`" class="w-full h-20 flex flex-col items-center justify-center text-themed 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"> <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" /> <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>
@ -42,7 +42,7 @@
<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>
<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" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="w-full h-20 flex flex-col items-center justify-center text-themed 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">&#xe431;</span> <span class="material-symbols text-2xl">&#xe431;</span>
<p class="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>
@ -50,7 +50,7 @@
<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>
<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" :to="`/library/${currentLibraryId}/bookshelf/playlists`" class="w-full h-20 flex flex-col items-center justify-center text-themed 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">&#xe03d;</span> <span class="material-symbols text-2.5xl">&#xe03d;</span>
<p class="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>
@ -58,7 +58,7 @@
<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="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" :to="`/library/${currentLibraryId}/bookshelf/authors`" class="w-full h-20 flex flex-col items-center justify-center text-themed 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"> <svg class="w-6 h-6" viewBox="0 0 24 24">
<path <path
fill="currentColor" fill="currentColor"
@ -71,7 +71,7 @@
<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>
<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" :to="`/library/${currentLibraryId}/narrators`" class="w-full h-20 flex flex-col items-center justify-center text-themed 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">&#xe91f;</span> <span class="material-symbols text-2xl">&#xe91f;</span>
<p class="pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.LabelNarrators }}</p> <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" /> <div v-show="isNarratorsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</nuxt-link> </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" :to="`/library/${currentLibraryId}/stats`" class="w-full h-20 flex flex-col items-center justify-center text-themed 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">&#xf190;</span> <span class="material-symbols text-2xl">&#xf190;</span>
<p class="pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonStats }}</p> <p class="pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonStats }}</p>
@ -87,7 +87,7 @@
<div v-show="isStatsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> <div v-show="isStatsPage" 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/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'"> <nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/search`" class="w-full h-20 flex flex-col items-center justify-center text-themed 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> <span class="abs-icons icon-podcast text-xl"></span>
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonAdd }}</p> <p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonAdd }}</p>
@ -95,7 +95,7 @@
<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>
<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/80 border-b border-primary/70 hover:bg-primary cursor-pointer relative" :class="isPodcastDownloadQueuePage ? 'bg-primary/80' : 'bg-bg/60'"> <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-themed border-b border-primary/70 hover:bg-primary cursor-pointer relative" :class="isPodcastDownloadQueuePage ? 'bg-primary/80' : 'bg-bg/60'">
<span class="material-symbols text-2xl">&#xf090;</span> <span class="material-symbols text-2xl">&#xf090;</span>
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonDownloadQueue }}</p> <p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonDownloadQueue }}</p>
@ -103,7 +103,7 @@
<div v-show="isPodcastDownloadQueuePage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> <div v-show="isPodcastDownloadQueuePage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</nuxt-link> </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/80 border-b border-primary/70 hover:bg-error/40 cursor-pointer relative" :class="showingIssues ? 'bg-error/40' : 'bg-error/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-themed border-b border-primary/70 hover:bg-error/40 cursor-pointer relative" :class="showingIssues ? 'bg-error/40' : 'bg-error/20'">
<span class="material-symbols text-2xl">warning</span> <span class="material-symbols text-2xl">warning</span>
<p class="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>

View file

@ -9,7 +9,7 @@
<!-- Author name & num books overlay --> <!-- Author name & num books overlay -->
<div cy-id="textInline" v-show="!searching && !nameBelow" class="absolute bottom-0 left-0 w-full py-1e bg-black/60 px-2e"> <div cy-id="textInline" v-show="!searching && !nameBelow" class="absolute bottom-0 left-0 w-full py-1e bg-black/60 px-2e">
<p class="text-center font-semibold truncate" :style="{ fontSize: 0.75 + 'em' }">{{ name }}</p> <p class="text-center font-semibold truncate" :style="{ fontSize: 0.75 + 'em' }">{{ name }}</p>
<p class="text-center text-gray-200" :style="{ fontSize: 0.65 + 'em' }">{{ numBooks }} {{ $strings.LabelBooks }}</p> <p class="text-center text-gray-200" :style="{ fontSize: 0.65 + 'em' }">{{ numBooks }} {{ numBooks === 1 ? $strings.LabelBook : $strings.LabelBooks }}</p>
</div> </div>
<!-- Search icon btn --> <!-- Search icon btn -->

View file

@ -1,15 +1,15 @@
<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="text-gray-200 text-sm sm:text-base">{{ playbackRateDisplay }}<span class="text-base">x</span></span> <span class="text-themed text-sm sm:text-base">{{ playbackRateDisplay }}<span class="text-base">x</span></span>
</div> </div>
<div v-show="showMenu" class="absolute -top-[5.5rem] z-20 bg-bg border-black-200 border shadow-xl rounded-lg" :style="{ left: menuLeft + 'px' }"> <div v-show="showMenu" class="absolute z-20 bg-bg border-black-200 border shadow-xl rounded-lg" :style="{ left: menuLeft + 'px', top: menuTop + 'px' }">
<div class="absolute -bottom-1.5 right-0 w-full flex justify-center" :style="{ left: arrowLeft + 'px' }"> <div class="absolute w-full flex justify-center" :style="{ left: arrowLeft + 'px', [arrowPosition]: '-6px' }">
<div class="arrow-down" /> <div :class="arrowPosition === 'top' ? 'arrow-up' : 'arrow-down'" />
</div> </div>
<div class="flex items-center h-9 relative overflow-hidden rounded-lg" style="width: 220px"> <div class="flex items-center relative overflow-hidden rounded-lg" :style="{ width: '220px', height: menuHeight + 'px' }">
<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-xs" :class="value === rate ? 'bg-black-100' : 'hover:bg-black/10'" style="min-width: 44px; max-width: 44px" @click="set(rate)"> <div :key="rate" class="border-black-300 w-11 cursor-pointer border rounded-xs" :class="value === rate ? 'bg-black-100' : 'hover:bg-black/10'" :style="{ minWidth: '44px', maxWidth: '44px', height: menuHeight + 'px' }" @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">{{ rate }}<span class="text-sm">x</span></p> <p class="text-xs text-center">{{ rate }}<span class="text-sm">x</span></p>
</div> </div>
@ -46,7 +46,9 @@ export default {
MIN_SPEED: 0.5, MIN_SPEED: 0.5,
MAX_SPEED: 10, MAX_SPEED: 10,
menuLeft: -96, menuLeft: -96,
arrowLeft: 0 arrowLeft: 0,
menuTop: -88, // Default top position
arrowPosition: 'bottom' // 'top' or 'bottom'
} }
}, },
computed: { computed: {
@ -73,6 +75,19 @@ export default {
const numDecimals = String(this.playbackRate).split('.')[1]?.length || 0 const numDecimals = String(this.playbackRate).split('.')[1]?.length || 0
if (numDecimals <= 1) return this.playbackRate.toFixed(1) if (numDecimals <= 1) return this.playbackRate.toFixed(1)
return this.playbackRate.toFixed(2) return this.playbackRate.toFixed(2)
},
showEReader() {
return this.$store.state.showEReader
},
menuHeight() {
return this.showEReader ? 18 : 36 // Half height when eReader is shown
}
},
watch: {
showEReader() {
if (this.showMenu) {
this.updateMenuPositions()
}
} }
}, },
methods: { methods: {
@ -97,14 +112,25 @@ export default {
if (!this.$refs.wrapper) return if (!this.$refs.wrapper) return
const boundingBox = this.$refs.wrapper.getBoundingClientRect() const boundingBox = this.$refs.wrapper.getBoundingClientRect()
// Calculate horizontal position
if (boundingBox.left + 110 > window.innerWidth - 10) { if (boundingBox.left + 110 > window.innerWidth - 10) {
this.menuLeft = window.innerWidth - 230 - boundingBox.left this.menuLeft = window.innerWidth - 230 - boundingBox.left
this.arrowLeft = Math.abs(this.menuLeft) - 96 this.arrowLeft = Math.abs(this.menuLeft) - 96
} else { } else {
this.menuLeft = -96 this.menuLeft = -96
this.arrowLeft = 0 this.arrowLeft = 0
} }
// Calculate vertical position based on eReader state
if (this.showEReader) {
// When eReader is shown, position menu with margin bottom zero (at the bottom of the trigger)
this.menuTop = 0
this.arrowPosition = 'top'
} else {
// Default position (above the trigger)
this.menuTop = -88
this.arrowPosition = 'bottom'
}
}, },
setShowMenu(val) { setShowMenu(val) {
if (val) { if (val) {
@ -121,3 +147,21 @@ export default {
} }
} }
</script> </script>
<style scoped>
.arrow-down {
width: 0;
height: 0;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 6px solid var(--color-bg);
}
.arrow-up {
width: 0;
height: 0;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-bottom: 6px solid var(--color-bg);
}
</style>

View file

@ -4,10 +4,10 @@
<span class="material-symbols text-2xl sm:text-3xl">{{ volumeIcon }}</span> <span class="material-symbols text-2xl sm:text-3xl">{{ volumeIcon }}</span>
</button> </button>
<transition name="menux"> <transition name="menux">
<div v-show="isOpen" class="volumeMenu h-28 absolute bottom-2 w-6 py-2 bg-bg shadow-xs rounded-lg" style="top: -116px"> <div v-show="isOpen" class="volumeMenu absolute bg-bg shadow-xs rounded-lg" :class="isHorizontal ? 'horizontal-menu' : 'vertical-menu'" :style="menuStyle">
<div ref="volumeTrack" class="w-1 h-full bg-gray-500 mx-2.5 relative cursor-pointer rounded-full" @mousedown="mousedownTrack" @click="clickVolumeTrack"> <div ref="volumeTrack" class="bg-gray-500 relative cursor-pointer rounded-full" :class="isHorizontal ? 'horizontal-track' : 'vertical-track'" @mousedown="mousedownTrack" @click="clickVolumeTrack">
<div class="bg-gray-100 w-full absolute left-0 bottom-0 pointer-events-none rounded-full" :style="{ height: volume * trackHeight + 'px' }" /> <div class="bg-gray-100 absolute pointer-events-none rounded-full" :class="isHorizontal ? 'horizontal-fill' : 'vertical-fill'" :style="fillStyle" />
<div class="w-2.5 h-2.5 bg-white shadow-xs rounded-full absolute pointer-events-none" :class="isDragging ? 'transform scale-125 origin-center' : ''" :style="{ bottom: cursorBottom + 'px', left: '-3px' }" /> <div class="w-2.5 h-2.5 bg-white shadow-xs rounded-full absolute pointer-events-none" :class="isDragging ? 'transform scale-125 origin-center' : ''" :style="cursorStyle" />
</div> </div>
</div> </div>
</transition> </transition>
@ -25,13 +25,22 @@ export default {
isDragging: false, isDragging: false,
isHovering: false, isHovering: false,
posY: 0, posY: 0,
posX: 0,
lastValue: 0.5, lastValue: 0.5,
isMute: false, isMute: false,
trackHeight: 112 - 20, verticalTrackHeight: 112 - 20,
horizontalTrackWidth: 112 - 20,
openTimeout: null openTimeout: null
} }
}, },
computed: { computed: {
isHorizontal() {
// Check if eReader is shown to determine orientation
return this.$store && this.$store.state && this.$store.state.showEReader
},
trackSize() {
return this.isHorizontal ? this.horizontalTrackWidth : this.verticalTrackHeight
},
volume: { volume: {
get() { get() {
return this.value return this.value
@ -40,14 +49,58 @@ export default {
try { try {
localStorage.setItem('volume', val) localStorage.setItem('volume', val)
} catch (error) { } catch (error) {
console.error('Failed to store volume', err) console.error('Failed to store volume', error)
} }
this.$emit('input', val) this.$emit('input', val)
} }
}, },
cursorBottom() { menuStyle() {
var bottom = this.trackHeight * this.volume if (this.isHorizontal) {
return bottom - 3 return {
bottom: '100%',
left: '50%',
transform: 'translateX(-50%)',
marginBottom: '8px',
zIndex: 100
}
} else {
return {
top: '-116px',
zIndex: 100
}
}
},
fillStyle() {
if (this.isHorizontal) {
return {
width: this.volume * this.trackSize + 'px',
height: '100%',
left: '0',
top: '0'
}
} else {
return {
height: this.volume * this.trackSize + 'px',
width: '100%',
left: '0',
bottom: '0'
}
}
},
cursorStyle() {
if (this.isHorizontal) {
const left = this.volume * this.trackSize - 3
return {
left: left + 'px',
top: '-3px'
}
} else {
const bottom = this.volume * this.trackSize - 3
return {
bottom: bottom + 'px',
left: '-3px'
}
}
}, },
volumeIcon() { volumeIcon() {
if (this.volume <= 0) return 'volume_mute' if (this.volume <= 0) return 'volume_mute'
@ -89,12 +142,21 @@ export default {
}, 600) }, 600)
}, },
mousemove(e) { mousemove(e) {
var diff = this.posY - e.y if (this.isHorizontal) {
this.posY = e.y // Horizontal movement
var volShift = diff / this.trackHeight const diff = e.x - this.posX
var newVol = this.volume + volShift this.posX = e.x
newVol = Math.min(Math.max(0, newVol), 1) const volShift = diff / this.trackSize
this.volume = newVol const newVol = Math.min(Math.max(0, this.volume + volShift), 1)
this.volume = newVol
} else {
// Vertical movement (original logic)
const diff = this.posY - e.y
this.posY = e.y
const volShift = diff / this.trackSize
const newVol = Math.min(Math.max(0, this.volume + volShift), 1)
this.volume = newVol
}
e.preventDefault() e.preventDefault()
}, },
mouseup(e) { mouseup(e) {
@ -106,10 +168,17 @@ export default {
}, },
mousedownTrack(e) { mousedownTrack(e) {
this.isDragging = true this.isDragging = true
this.posY = e.y
var vol = 1 - e.offsetY / this.trackHeight if (this.isHorizontal) {
vol = Math.min(Math.max(vol, 0), 1) this.posX = e.x
this.volume = vol const vol = e.offsetX / this.trackSize
this.volume = Math.min(Math.max(vol, 0), 1)
} else {
this.posY = e.y
const vol = 1 - e.offsetY / this.trackSize
this.volume = Math.min(Math.max(vol, 0), 1)
}
document.body.addEventListener('mousemove', this.mousemove) document.body.addEventListener('mousemove', this.mousemove)
document.body.addEventListener('mouseup', this.mouseup) document.body.addEventListener('mouseup', this.mouseup)
e.preventDefault() e.preventDefault()
@ -130,9 +199,13 @@ export default {
this.clickVolumeIcon() this.clickVolumeIcon()
}, },
clickVolumeTrack(e) { clickVolumeTrack(e) {
var vol = 1 - e.offsetY / this.trackHeight if (this.isHorizontal) {
vol = Math.min(Math.max(vol, 0), 1) const vol = e.offsetX / this.trackSize
this.volume = vol this.volume = Math.min(Math.max(vol, 0), 1)
} else {
const vol = 1 - e.offsetY / this.trackSize
this.volume = Math.min(Math.max(vol, 0), 1)
}
} }
}, },
mounted() { mounted() {
@ -151,3 +224,52 @@ export default {
} }
} }
</script> </script>
<style scoped>
.vertical-menu {
height: 112px;
width: 24px;
padding: 8px 0;
}
.horizontal-menu {
width: 112px;
height: 24px;
padding: 0 8px;
}
.vertical-track {
width: 4px;
height: 100%;
margin: 0 10px;
}
.horizontal-track {
height: 4px;
width: 100%;
margin: 10px 0;
}
.vertical-fill {
width: 100%;
}
.horizontal-fill {
height: 100%;
}
/* Smooth transitions for orientation changes */
.volumeMenu {
transition: all 0.3s ease;
}
.menux-enter-active,
.menux-leave-active {
transition: opacity 0.2s;
}
.menux-enter,
.menux-leave-to {
opacity: 0;
}
</style>

View file

@ -1,6 +1,6 @@
<template> <template>
<div class="w-full h-full pt-20 relative"> <div class="w-full h-full pt-20 relative">
<div v-show="canGoPrev" class="absolute top-0 left-0 h-full w-1/2 hover:opacity-100 opacity-0 z-10 cursor-pointer" @click.stop.prevent="prev" @mousedown.prevent> <div v-show="canGoPrev" class="absolute top-0 left-0 h-full w-1/2 h }, computed: { userToken() {00 opacity-0 z-0 cursor-pointer" @click.stop.prevent="prev" @mousedown.prevent>
<div class="flex items-center justify-center h-full w-1/2"> <div class="flex items-center justify-center h-full w-1/2">
<span class="material-symbols text-5xl text-white/30 cursor-pointer hover:text-white/90">arrow_back_ios</span> <span class="material-symbols text-5xl text-white/30 cursor-pointer hover:text-white/90">arrow_back_ios</span>
</div> </div>
@ -11,11 +11,22 @@
</div> </div>
</div> </div>
<div class="absolute top-0 right-20 bg-bg text-gray-100 border-b border-l border-r border-gray-400 z-20 rounded-b-md px-2 h-9 hidden md:flex items-center text-center"> <div class="absolute top-0 right-12 bg-bg text-gray-100 border-b border-l border-r border-gray-400 z-20 rounded-b-md px-2 h-9 hidden md:flex items-center text-center">
<p class="font-mono">{{ page }} / {{ numPages }}</p> <input v-if="editingPage" v-model="pageInput" @keyup.enter="goToPage" @keyup.esc="cancelPageEdit" @blur="goToPage" ref="pageInputField" class="bg-transparent text-center font-mono text-sm border-none outline-none" :style="{ width: String(numPages).length * 8 + 16 + 'px' }" />
<p v-else @click="startPageEdit" class="font-mono cursor-pointer hover:bg-white/10 px-1 rounded" title="Click to jump to page">{{ page }} / {{ numPages }}</p>
</div> </div>
<div class="absolute top-0 right-40 bg-bg text-gray-100 border-b border-l border-r border-gray-400 z-20 rounded-b-md px-2 h-9 hidden md:flex items-center text-center">
<div class="absolute top-0 right-40 bg-bg text-gray-100 border-b border-l border-r border-gray-400 z-20 rounded-b-md px-1 h-9 hidden md:flex items-center text-center">
<ui-icon-btn icon="zoom_out" :size="8" :disabled="!canScaleDown" borderless class="mr-px" @click="zoomOut" /> <ui-icon-btn icon="zoom_out" :size="8" :disabled="!canScaleDown" borderless class="mr-px" @click="zoomOut" />
<select v-model="zoomLevel" @change="setZoomLevel" class="bg-bg text-xs mx-1 min-w-16 text-gray-100 border border-gray-400 rounded">
<option value="fit-width">Fit Width</option>
<option value="fit-page">Fit Page</option>
<option value="1">100%</option>
<option value="1.25">125%</option>
<option value="1.5">150%</option>
<option value="1.75">175%</option>
<option value="2">200%</option>
</select>
<ui-icon-btn icon="zoom_in" :size="8" :disabled="!canScaleUp" borderless class="ml-px" @click="zoomIn" /> <ui-icon-btn icon="zoom_in" :size="8" :disabled="!canScaleUp" borderless class="ml-px" @click="zoomIn" />
</div> </div>
@ -56,7 +67,12 @@ export default {
page: 1, page: 1,
numPages: 0, numPages: 0,
pdfDocInitParams: null, pdfDocInitParams: null,
isRefreshing: false isRefreshing: false,
zoomLevel: '1',
editingPage: false,
pageInput: 1,
viewMode: 'single', // 'single' or 'continuous'
scrollPosition: 0
} }
}, },
computed: { computed: {
@ -76,6 +92,15 @@ export default {
if (this.windowHeight < 400 || !this.playerOpen) return this.windowHeight - 120 if (this.windowHeight < 400 || !this.playerOpen) return this.windowHeight - 120
return this.windowHeight - 284 return this.windowHeight - 284
}, },
fitToWidth() {
return (this.windowWidth - 40) / this.fitToPageWidth
},
fitToPage() {
return Math.min(this.fitToWidth, (this.pdfHeight - 40) / (this.fitToPageWidth * 1.4))
},
showContinuousView() {
return this.viewMode === 'continuous'
},
maxScale() { maxScale() {
return Math.floor((this.windowWidth * 10) / this.fitToPageWidth) / 10 return Math.floor((this.windowWidth * 10) / this.fitToPageWidth) / 10
}, },
@ -86,7 +111,7 @@ export default {
return this.page > 1 return this.page > 1
}, },
canScaleUp() { canScaleUp() {
return this.scale < this.maxScale return this.scale < 2
}, },
canScaleDown() { canScaleDown() {
return this.scale > 1 return this.scale > 1
@ -109,12 +134,47 @@ export default {
return `/api/items/${this.libraryItemId}/ebook` return `/api/items/${this.libraryItemId}/ebook`
} }
}, },
watch: {
page(newVal) {
this.pageInput = newVal
},
scale(newVal) {
// Update zoom level dropdown when scale changes
if (newVal >= 1 && newVal <= 2) {
this.zoomLevel = newVal.toString()
}
}
},
methods: { methods: {
zoomIn() { zoomIn() {
this.scale += 0.1 if (this.scale < 2) {
this.scale = Math.min(2, this.scale + 0.25)
this.updateZoomLevel()
}
},
setZoomLevel() {
switch (this.zoomLevel) {
case 'fit-width':
this.scale = this.fitToWidth
break
case 'fit-page':
this.scale = this.fitToPage
break
default:
this.scale = parseFloat(this.zoomLevel)
}
}, },
zoomOut() { zoomOut() {
this.scale -= 0.1 if (this.scale > 1) {
this.scale = Math.max(1, this.scale - 0.25)
this.updateZoomLevel()
}
},
updateZoomLevel() {
// Update the dropdown to show current zoom percentage
if (this.scale >= 1 && this.scale <= 2) {
this.zoomLevel = this.scale.toString()
}
}, },
updateProgress() { updateProgress() {
if (!this.keepProgress) return if (!this.keepProgress) return
@ -146,11 +206,13 @@ export default {
prev() { prev() {
if (this.page <= 1) return if (this.page <= 1) return
this.page-- this.page--
this.pageInput = this.page
this.updateProgress() this.updateProgress()
}, },
next() { next() {
if (this.page >= this.numPages) return if (this.page >= this.numPages) return
this.page++ this.page++
this.pageInput = this.page
this.updateProgress() this.updateProgress()
}, },
async refreshToken() { async refreshToken() {
@ -187,6 +249,91 @@ export default {
this.windowWidth = window.innerWidth this.windowWidth = window.innerWidth
this.windowHeight = window.innerHeight this.windowHeight = window.innerHeight
}, },
toggleViewMode() {
this.viewMode = this.viewMode === 'single' ? 'continuous' : 'single'
},
handleScroll(e) {
if (this.viewMode === 'continuous') {
const container = e.target
const scrollPercent = container.scrollTop / (container.scrollHeight - container.clientHeight)
const newPage = Math.ceil(scrollPercent * this.numPages) || 1
if (newPage !== this.page) {
this.page = newPage
this.updateProgress()
}
}
},
startPageEdit() {
this.editingPage = true
this.pageInput = this.page
this.$nextTick(() => {
this.$refs.pageInputField?.focus()
this.$refs.pageInputField?.select()
})
},
goToPage() {
const targetPage = parseInt(this.pageInput)
if (targetPage >= 1 && targetPage <= this.numPages) {
this.page = targetPage
this.updateProgress()
} else {
this.pageInput = this.page
}
this.editingPage = false
},
cancelPageEdit() {
this.pageInput = this.page
this.editingPage = false
},
keyboardHandler(e) {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return
switch (e.key) {
case 'ArrowLeft':
case 'ArrowUp':
case 'PageUp':
e.preventDefault()
e.stopPropagation()
this.prev()
break
case 'ArrowRight':
case 'ArrowDown':
case 'PageDown':
case ' ':
e.preventDefault()
e.stopPropagation()
this.next()
break
case 'Home':
e.preventDefault()
this.page = 1
this.pageInput = 1
this.updateProgress()
break
case 'End':
e.preventDefault()
this.page = this.numPages
this.pageInput = this.numPages
this.updateProgress()
break
case '+':
case '=':
e.preventDefault()
if (this.canScaleUp) this.zoomIn()
break
case '-':
e.preventDefault()
if (this.canScaleDown) this.zoomOut()
break
case '0':
e.preventDefault()
this.scale = 1
this.updateZoomLevel()
break
}
},
init() { init() {
this.pdfDocInitParams = { this.pdfDocInitParams = {
url: this.ebookUrl, url: this.ebookUrl,
@ -194,17 +341,22 @@ export default {
Authorization: `Bearer ${this.userToken}` Authorization: `Bearer ${this.userToken}`
} }
} }
// Initialize zoom level to match default scale
this.zoomLevel = this.scale.toString()
} }
}, },
mounted() { mounted() {
this.windowWidth = window.innerWidth this.windowWidth = window.innerWidth
this.windowHeight = window.innerHeight this.windowHeight = window.innerHeight
window.addEventListener('resize', this.resize) window.addEventListener('resize', this.resize)
document.removeEventListener('keydown', this.keyboardHandler)
document.addEventListener('keydown', this.keyboardHandler)
this.init() this.init()
}, },
beforeDestroy() { beforeDestroy() {
window.removeEventListener('resize', this.resize) window.removeEventListener('resize', this.resize)
document.removeEventListener('keydown', this.keyboardHandler)
} }
} }
</script> </script>

View file

@ -6,7 +6,7 @@
</svg> </svg>
<div class="px-1"> <div class="px-1">
<p class="text-4.5xl leading-none font-bold">{{ $formatNumber(totalItems) }}</p> <p class="text-4.5xl leading-none font-bold">{{ $formatNumber(totalItems) }}</p>
<p class="text-xs md:text-sm text-white/80">{{ $strings.LabelStatsItemsInLibrary }}</p> <p class="text-xs md:text-sm text-white">{{ $strings.LabelStatsItemsInLibrary }}</p>
</div> </div>
</div> </div>
@ -14,7 +14,7 @@
<span class="material-symbols text-5xl py-1">show_chart</span> <span class="material-symbols text-5xl py-1">show_chart</span>
<div class="px-1"> <div class="px-1">
<p class="text-4.5xl leading-none font-bold">{{ $formatNumber(totalTime) }}</p> <p class="text-4.5xl leading-none font-bold">{{ $formatNumber(totalTime) }}</p>
<p class="text-xs md:text-sm text-white/80">{{ useOverallHours ? $strings.LabelStatsOverallHours : $strings.LabelStatsOverallDays }}</p> <p class="text-xs md:text-sm text-white">{{ 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-4.5xl leading-none font-bold">{{ $formatNumber(totalAuthors) }}</p> <p class="text-4.5xl leading-none font-bold">{{ $formatNumber(totalAuthors) }}</p>
<p class="text-xs md:text-sm text-white/80">{{ $strings.LabelStatsAuthors }}</p> <p class="text-xs md:text-sm text-white">{{ $strings.LabelStatsAuthors }}</p>
</div> </div>
</div> </div>
@ -32,7 +32,7 @@
<span class="material-symbols text-5xl pt-1">insert_drive_file</span> <span class="material-symbols text-5xl pt-1">insert_drive_file</span>
<div class="px-1"> <div class="px-1">
<p class="text-4.5xl leading-none font-bold">{{ $formatNumber(totalSizeNum) }}</p> <p class="text-4.5xl leading-none font-bold">{{ $formatNumber(totalSizeNum) }}</p>
<p class="text-xs md:text-sm text-white/80">{{ $strings.LabelSize }} ({{ totalSizeMod }})</p> <p class="text-xs md:text-sm text-white">{{ $strings.LabelSize }} ({{ totalSizeMod }})</p>
</div> </div>
</div> </div>
@ -40,7 +40,7 @@
<span class="material-symbols text-5xl pt-1">audio_file</span> <span class="material-symbols text-5xl pt-1">audio_file</span>
<div class="px-1"> <div class="px-1">
<p class="text-4.5xl leading-none font-bold">{{ $formatNumber(numAudioTracks) }}</p> <p class="text-4.5xl leading-none font-bold">{{ $formatNumber(numAudioTracks) }}</p>
<p class="text-xs md:text-sm text-white/80">{{ $strings.LabelStatsAudioTracks }}</p> <p class="text-xs md:text-sm text-white">{{ $strings.LabelStatsAudioTracks }}</p>
</div> </div>
</div> </div>
</div> </div>

View file

@ -208,11 +208,12 @@ export default {
table-layout: fixed; table-layout: fixed;
border-collapse: collapse; border-collapse: collapse;
width: 100%; width: 100%;
color: var(--table-text-color);
} }
#backups td, #backups td,
#backups th { #backups th {
border: 1px solid #2e2e2e; border: 1px solid var(--table-border);
padding: 8px 8px; padding: 8px 8px;
text-align: left; text-align: left;
} }
@ -222,11 +223,15 @@ export default {
} }
#backups tr:nth-child(even):not(.bg-error) { #backups tr:nth-child(even):not(.bg-error) {
background-color: #3a3a3a; background-color: var(--table-row-alt-bg);
}
#backups tr:nth-child(odd):not(.bg-error):not(.staticrow) {
background-color: var(--table-row-bg);
} }
#backups tr:not(.staticrow):not(.bg-error):hover { #backups tr:not(.staticrow):not(.bg-error):hover {
background-color: #444; background-color: var(--table-row-hover-bg);
} }
#backups th { #backups th {
@ -234,6 +239,7 @@ export default {
font-weight: 600; font-weight: 600;
padding-top: 5px; padding-top: 5px;
padding-bottom: 5px; padding-bottom: 5px;
background-color: #333; background-color: var(--table-header-bg);
color: var(--table-text-color);
} }
</style> </style>

View file

@ -183,38 +183,44 @@ export default {
#accounts { #accounts {
table-layout: fixed; table-layout: fixed;
border-collapse: collapse; border-collapse: collapse;
border: 1px solid #474747; border: 1px solid var(--color-border);
width: 100%; width: 100%;
background-color: var(--color-primary);
} }
#accounts td, #accounts td,
#accounts th { #accounts th {
/* border: 1px solid #2e2e2e; */
padding: 8px 8px; padding: 8px 8px;
text-align: left; text-align: left;
border-bottom: 1px solid var(--color-border-light);
color: var(--color-text);
} }
#accounts td.py-0 { #accounts td.py-0 {
padding: 0px 8px; padding: 0px 8px;
} }
/* Row striping */
#accounts tr:nth-child(even) { #accounts tr:nth-child(even) {
background-color: #373838; background-color: var(--color-fg);
} }
#accounts tr:nth-child(odd) { #accounts tr:nth-child(odd) {
background-color: #2f2f2f; background-color: var(--color-primary);
} }
/* Hover state */
#accounts tr:hover { #accounts tr:hover {
background-color: #444; background-color: var(--color-hover);
} }
/* Table header */
#accounts th { #accounts th {
font-size: 0.8rem; font-size: 0.8rem;
font-weight: 600; font-weight: 600;
padding-top: 5px; padding-top: 5px;
padding-bottom: 5px; padding-bottom: 5px;
background-color: #272727; background-color: var(--color-bg);
color: var(--color-text);
border-bottom: 2px solid var(--color-border);
} }
</style> </style>

View file

@ -278,7 +278,17 @@ export default {
this.details.narrators = [...(this.mediaMetadata.narrators || [])] this.details.narrators = [...(this.mediaMetadata.narrators || [])]
this.details.genres = [...(this.mediaMetadata.genres || [])] this.details.genres = [...(this.mediaMetadata.genres || [])]
this.details.series = (this.mediaMetadata.series || []).map((se) => ({ ...se })) this.details.series = (this.mediaMetadata.series || []).map((se) => ({ ...se }))
this.details.publishedYear = this.mediaMetadata.publishedYear // Handle publishedYear - extract just the year if it's a full date string
let publishedYear = this.mediaMetadata.publishedYear
if (publishedYear) {
// Check if it's a date string (contains '-')
if (publishedYear.includes('-')) {
publishedYear = publishedYear.split('-')[0]
}
// Check if it's a valid number
publishedYear = isNaN(Number(publishedYear)) ? null : Number(publishedYear)
}
this.details.publishedYear = publishedYear
this.details.publisher = this.mediaMetadata.publisher || null this.details.publisher = this.mediaMetadata.publisher || null
this.details.language = this.mediaMetadata.language || null this.details.language = this.mediaMetadata.language || null
this.details.isbn = this.mediaMetadata.isbn || null this.details.isbn = this.mediaMetadata.isbn || null

View file

@ -126,7 +126,7 @@ export default {
disconnect() { disconnect() {
console.log('[SOCKET] Disconnected') console.log('[SOCKET] Disconnected')
this.isSocketConnected = false this.isSocketConnected = false
this.updateSocketConnectionToast(this.$strings.ToastSocketDisconnected, 'error', null) // this.updateSocketConnectionToast(this.$strings.ToastSocketDisconnected, 'error', null)
}, },
reconnect() { reconnect() {
console.log('[SOCKET] reconnected') console.log('[SOCKET] reconnected')
@ -377,6 +377,9 @@ export default {
if (!provider?.id) return if (!provider?.id) return
this.$store.commit('scanners/removeCustomMetadataProvider', provider) this.$store.commit('scanners/removeCustomMetadataProvider', provider)
}, },
initializeTheme() {
this.$store.dispatch('theme/initializeTheme')
},
initializeSocket() { initializeSocket() {
if (this.$root.socket) { if (this.$root.socket) {
// Can happen in dev due to hot reload // Can happen in dev due to hot reload
@ -605,6 +608,7 @@ export default {
mounted() { mounted() {
this.updateBodyClass() this.updateBodyClass()
this.resize() this.resize()
this.initializeTheme()
this.$eventBus.$on('change-lang', this.changeLanguage) this.$eventBus.$on('change-lang', this.changeLanguage)
this.$eventBus.$on('token_refreshed', this.tokenRefreshed) this.$eventBus.$on('token_refreshed', this.tokenRefreshed)
window.addEventListener('resize', this.resize) window.addEventListener('resize', this.resize)

View file

@ -57,7 +57,7 @@
</td> </td>
<!-- --> <!-- -->
<td class="text-center"> <td class="text-center">
<ui-icon-btn icon="delete" class="mx-0.5 text-white/70" borderless :size="7" iconFontSize="1.25rem" outlined @click.stop="deleteFeedClick(feed)" /> <ui-icon-btn icon="delete" class="mx-0.5 text-white" borderless :size="7" iconFontSize="1.25rem" outlined @click.stop="deleteFeedClick(feed)" />
</td> </td>
</tr> </tr>
</table> </table>
@ -163,31 +163,38 @@ export default {
border-collapse: collapse; border-collapse: collapse;
width: 100%; width: 100%;
max-width: 100%; max-width: 100%;
border: 1px solid #474747; border: 1px solid var(--table-border);
} }
/* Header row */
.rssFeedsTable tr:first-child { .rssFeedsTable tr:first-child {
background-color: #272727; background-color: var(--table-header-bg);
} }
/* Normal rows */
.rssFeedsTable tr:not(:first-child) { .rssFeedsTable tr:not(:first-child) {
background-color: #373838; background-color: var(--table-row-bg);
} }
/* Zebra striping */
.rssFeedsTable tr:not(:first-child):nth-child(odd) { .rssFeedsTable tr:not(:first-child):nth-child(odd) {
background-color: #2f2f2f; background-color: var(--table-row-alt-bg);
} }
/* Hover state */
.rssFeedsTable tr:hover:not(:first-child) { .rssFeedsTable tr:hover:not(:first-child) {
background-color: #474747; background-color: var(--table-row-hover-bg);
} }
/* Cells */
.rssFeedsTable td { .rssFeedsTable td {
padding: 4px 8px; padding: 4px 8px;
color: var(--table-text);
} }
.rssFeedsTable th { .rssFeedsTable th {
padding: 4px 8px; padding: 4px 8px;
font-size: 0.75rem; font-size: 0.75rem;
color: var(--table-text-header);
} }
</style> </style>

View file

@ -515,26 +515,35 @@ export default {
border-collapse: collapse; border-collapse: collapse;
width: 100%; width: 100%;
max-width: 100%; max-width: 100%;
border: 1px solid #474747; border: 1px solid var(--table-border);
} }
/* Header */
.userSessionsTable tr:first-child { .userSessionsTable tr:first-child {
background-color: #272727; background-color: var(--table-header-bg);
} }
/* Normal rows */
.userSessionsTable tr:not(:first-child):not(.selected) { .userSessionsTable tr:not(:first-child):not(.selected) {
background-color: #373838; background-color: var(--table-row-bg);
} }
/* Alternate rows */
.userSessionsTable tr:not(:first-child):nth-child(odd):not(.selected):not(:hover) { .userSessionsTable tr:not(:first-child):nth-child(odd):not(.selected):not(:hover) {
background-color: #2f2f2f; background-color: var(--table-row-alt-bg);
} }
/* Hover */
.userSessionsTable tr:hover:not(:first-child) { .userSessionsTable tr:hover:not(:first-child) {
background-color: #474747; background-color: var(--table-row-hover-bg);
} }
/* Selected */
.userSessionsTable tr.selected { .userSessionsTable tr.selected {
background-color: #474747; background-color: var(--table-row-hover-bg);
}
.userSessionsTable td {
padding: 4px 8px;
} }
.userSessionsTable td,
.userSessionsTable th { .userSessionsTable th {
padding: 4px 8px; padding: 4px 8px;
font-size: 0.75rem; font-size: 0.75rem;

View file

@ -11,7 +11,7 @@
</div> </div>
<div class="px-3"> <div class="px-3">
<p class="text-4xl md:text-5xl font-bold">{{ $formatNumber(userItemsFinished.length) }}</p> <p class="text-4xl md:text-5xl font-bold">{{ $formatNumber(userItemsFinished.length) }}</p>
<p class="text-xs md:text-sm text-white/80">{{ $strings.LabelStatsItemsFinished }}</p> <p class="text-xs md:text-sm text-white">{{ $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">{{ $formatNumber(totalDaysListened) }}</p> <p class="text-4xl md:text-5xl font-bold">{{ $formatNumber(totalDaysListened) }}</p>
<p class="text-xs md:text-sm text-white/80">{{ $strings.LabelStatsDaysListened }}</p> <p class="text-xs md:text-sm text-white">{{ $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">{{ $formatNumber(totalMinutesListening) }}</p> <p class="text-4xl md:text-5xl font-bold">{{ $formatNumber(totalMinutesListening) }}</p>
<p class="text-xs md:text-sm text-white/80">{{ $strings.LabelStatsMinutesListening }}</p> <p class="text-xs md:text-sm text-white">{{ $strings.LabelStatsMinutesListening }}</p>
</div> </div>
</div> </div>
</div> </div>
@ -47,10 +47,10 @@
<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 text-white/70 w-8">{{ index + 1 }}.&nbsp;</p> <p class="text-sm text-white w-8">{{ index + 1 }}.&nbsp;</p>
<div class="w-56"> <div class="w-56">
<p class="text-sm text-white/80 truncate">{{ item.mediaMetadata ? item.mediaMetadata.title : '' }}</p> <p class="text-sm text-white truncate">{{ item.mediaMetadata ? item.mediaMetadata.title : '' }}</p>
<p class="text-xs text-white/50">{{ $dateDistanceFromNow(item.updatedAt) }}</p> <p class="text-xs text-white">{{ $dateDistanceFromNow(item.updatedAt) }}</p>
</div> </div>
<div class="grow" /> <div class="grow" />
<div class="w-18 text-right"> <div class="w-18 text-right">

View file

@ -168,25 +168,44 @@ export default {
.userAudiobooksTable { .userAudiobooksTable {
border-collapse: collapse; border-collapse: collapse;
width: 100%; width: 100%;
border: 1px solid #474747; border: 1px solid var(--color-border);
background-color: var(--color-primary);
} }
.userAudiobooksTable tr:nth-child(even) {
background-color: #2e2e2e; /* Header row */
.userAudiobooksTable tr:first-child {
background-color: var(--color-bg);
color: var(--color-text);
font-weight: 600;
} }
/* Body rows */
.userAudiobooksTable tr:not(:first-child) { .userAudiobooksTable tr:not(:first-child) {
background-color: #373838; background-color: var(--color-primary);
color: var(--color-text);
} }
/* Alternating rows */
.userAudiobooksTable tr:nth-child(even):not(:first-child) {
background-color: var(--color-fg);
}
/* Hover */
.userAudiobooksTable tr:hover:not(:first-child) { .userAudiobooksTable tr:hover:not(:first-child) {
background-color: #474747; background-color: var(--color-hover);
} }
/* Finished rows (green hint, readable in both modes) */
.userAudiobooksTable tr.isFinished { .userAudiobooksTable tr.isFinished {
background-color: rgba(76, 175, 80, 0.1); background-color: rgba(76, 175, 80, 0.12);
}
.userAudiobooksTable td {
padding: 4px 8px;
} }
/* Table cells */
.userAudiobooksTable td,
.userAudiobooksTable th { .userAudiobooksTable th {
padding: 4px 8px; padding: 4px 8px;
font-size: 0.75rem; font-size: 0.75rem;
color: var(--color-text);
border-bottom: 1px solid var(--color-border-light);
} }
</style> </style>

View file

@ -241,25 +241,37 @@ export default {
border-collapse: collapse; border-collapse: collapse;
width: 100%; width: 100%;
max-width: 100%; max-width: 100%;
border: 1px solid #474747; border: 1px solid var(--color-border);
} }
/* Header row */
.userSessionsTable tr:first-child { .userSessionsTable tr:first-child {
background-color: #272727; background-color: var(--color-bg);
color: var(--color-text);
} }
/* Body rows */
.userSessionsTable tr:not(:first-child) { .userSessionsTable tr:not(:first-child) {
background-color: #373838; background-color: var(--color-primary);
color: var(--color-text);
} }
/* Alternating striping */
.userSessionsTable tr:not(:first-child):nth-child(odd) { .userSessionsTable tr:not(:first-child):nth-child(odd) {
background-color: #2f2f2f; background-color: var(--color-fg);
} }
/* Hover state */
.userSessionsTable tr:hover:not(:first-child) { .userSessionsTable tr:hover:not(:first-child) {
background-color: #474747; background-color: var(--color-hover);
}
.userSessionsTable td {
padding: 4px 8px;
} }
/* Table cells */
.userSessionsTable td,
.userSessionsTable th { .userSessionsTable th {
padding: 4px 8px; padding: 4px 8px;
font-size: 0.75rem; font-size: 0.75rem;
color: var(--color-text);
border-bottom: 1px solid var(--color-border-light);
} }
</style> </style>

56
client/store/theme.js Normal file
View file

@ -0,0 +1,56 @@
export const state = () => ({
isDarkMode: true
})
export const mutations = {
setDarkMode(state, isDark) {
state.isDarkMode = isDark
},
toggleTheme(state) {
console.log('Is Dark Mode State', state.isDarkMode)
state.isDarkMode = !state.isDarkMode
}
}
export const actions = {
initializeTheme({ commit, dispatch }) {
// Check localStorage for saved theme preference
const savedTheme = localStorage.getItem('audiobookshelf_theme')
if (savedTheme) {
const isDark = savedTheme === 'dark'
commit('setDarkMode', isDark)
dispatch('applyTheme', isDark)
} else {
// Default to dark mode (matches your current app design)
// or check system preference: const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
commit('setDarkMode', true)
dispatch('applyTheme', true)
}
},
toggleTheme({ commit, state, dispatch }) {
const newTheme = !state.isDarkMode
commit('setDarkMode', newTheme)
localStorage.setItem('audiobookshelf_theme', newTheme ? 'dark' : 'light')
dispatch('applyTheme', newTheme)
},
applyTheme({}, isDark) {
// Apply theme class to document
if (isDark) {
document.documentElement.classList.add('dark')
document.documentElement.classList.remove('light')
} else {
document.documentElement.classList.add('light')
document.documentElement.classList.remove('dark')
}
// Update meta theme-color for mobile browsers
const metaThemeColor = document.querySelector('meta[name=theme-color]')
if (metaThemeColor) {
metaThemeColor.setAttribute('content', isDark ? '#232323' : '#ffffff')
}
}
}
export const getters = {
isDarkMode: (state) => state.isDarkMode
}

View file

@ -0,0 +1,19 @@
{
"tags": [],
"chapters": [],
"title": "Horror Tales 01 (v1 #7)",
"subtitle": null,
"authors": [],
"narrators": [],
"series": [],
"genres": [],
"publishedYear": null,
"publishedDate": null,
"publisher": null,
"description": null,
"isbn": null,
"asin": null,
"language": null,
"explicit": false,
"abridged": false
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

View file

@ -0,0 +1,26 @@
{
"tags": [],
"chapters": [],
"title": "Alice's Adventures in Wonderland",
"subtitle": null,
"authors": [
"Lewis Carroll"
],
"narrators": [],
"series": [],
"genres": [
"Fantasy fiction",
"Children's stories",
"Imaginary places -- Juvenile fiction",
"Alice (Fictitious character from Carroll) -- Juvenile fiction"
],
"publishedYear": "2008",
"publishedDate": null,
"publisher": null,
"description": null,
"isbn": null,
"asin": null,
"language": "en",
"explicit": false,
"abridged": false
}

Binary file not shown.