mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-07-07 09:51:37 +00:00
Implement PDF reader zoom controls and fix theme support
This commit is contained in:
parent
32da0f1224
commit
c4e59f61a4
24 changed files with 961 additions and 112 deletions
|
|
@ -3,11 +3,12 @@
|
|||
@import './draggable.css';
|
||||
@import './defaultStyles.css';
|
||||
@import './absicons.css';
|
||||
|
||||
@import './theme.css';
|
||||
/*
|
||||
:root {
|
||||
--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%);
|
||||
}
|
||||
} */
|
||||
|
||||
.page {
|
||||
width: 100%;
|
||||
|
|
@ -22,8 +23,7 @@
|
|||
|
||||
#bookshelf {
|
||||
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 */
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #855620 rgba(0, 0, 0, 0);
|
||||
|
|
@ -44,7 +44,7 @@
|
|||
}
|
||||
|
||||
#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 */
|
||||
|
|
|
|||
345
client/assets/theme.css
Normal file
345
client/assets/theme.css
Normal 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;
|
||||
}
|
||||
|
|
@ -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">
|
||||
<google-cast-launcher></google-cast-launcher>
|
||||
</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 ? '' : '' }}
|
||||
</span>
|
||||
</button>
|
||||
</ui-tooltip>
|
||||
<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">
|
||||
|
|
@ -158,6 +164,9 @@ export default {
|
|||
isHttps() {
|
||||
return location.protocol === 'https:' || process.env.NODE_ENV === 'development'
|
||||
},
|
||||
isDarkMode() {
|
||||
return this.$store.getters['theme/isDarkMode']
|
||||
},
|
||||
contextMenuItems() {
|
||||
if (!this.userIsAdminOrUp) return []
|
||||
|
||||
|
|
@ -375,6 +384,9 @@ export default {
|
|||
},
|
||||
batchAutoMatchClick() {
|
||||
this.$store.commit('globals/setShowBatchQuickMatchModal', true)
|
||||
},
|
||||
toggleTheme() {
|
||||
this.$store.dispatch('theme/toggleTheme')
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
<div v-show="homePage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||
</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"></span>
|
||||
|
||||
<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" />
|
||||
</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">
|
||||
<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" :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">
|
||||
<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" :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"></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" :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"></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" :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">
|
||||
<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" :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"></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" :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"></span>
|
||||
|
||||
<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" />
|
||||
</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>
|
||||
|
||||
<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" />
|
||||
</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"></span>
|
||||
|
||||
<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" />
|
||||
</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>
|
||||
|
||||
<p class="pt-1.5 text-center leading-4" style="font-size: 1rem">{{ $strings.ButtonIssues }}</p>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
<!-- 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">
|
||||
<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>
|
||||
|
||||
<!-- Search icon btn -->
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
<template>
|
||||
<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)">
|
||||
<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 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 class="absolute -bottom-1.5 right-0 w-full flex justify-center" :style="{ left: arrowLeft + 'px' }">
|
||||
<div class="arrow-down" />
|
||||
<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 w-full flex justify-center" :style="{ left: arrowLeft + 'px', [arrowPosition]: '-6px' }">
|
||||
<div :class="arrowPosition === 'top' ? 'arrow-up' : 'arrow-down'" />
|
||||
</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">
|
||||
<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">
|
||||
<p class="text-xs text-center">{{ rate }}<span class="text-sm">x</span></p>
|
||||
</div>
|
||||
|
|
@ -46,7 +46,9 @@ export default {
|
|||
MIN_SPEED: 0.5,
|
||||
MAX_SPEED: 10,
|
||||
menuLeft: -96,
|
||||
arrowLeft: 0
|
||||
arrowLeft: 0,
|
||||
menuTop: -88, // Default top position
|
||||
arrowPosition: 'bottom' // 'top' or 'bottom'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -73,6 +75,19 @@ export default {
|
|||
const numDecimals = String(this.playbackRate).split('.')[1]?.length || 0
|
||||
if (numDecimals <= 1) return this.playbackRate.toFixed(1)
|
||||
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: {
|
||||
|
|
@ -97,14 +112,25 @@ export default {
|
|||
if (!this.$refs.wrapper) return
|
||||
const boundingBox = this.$refs.wrapper.getBoundingClientRect()
|
||||
|
||||
// Calculate horizontal position
|
||||
if (boundingBox.left + 110 > window.innerWidth - 10) {
|
||||
this.menuLeft = window.innerWidth - 230 - boundingBox.left
|
||||
|
||||
this.arrowLeft = Math.abs(this.menuLeft) - 96
|
||||
} else {
|
||||
this.menuLeft = -96
|
||||
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) {
|
||||
if (val) {
|
||||
|
|
@ -121,3 +147,21 @@ export default {
|
|||
}
|
||||
}
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@
|
|||
<span class="material-symbols text-2xl sm:text-3xl">{{ volumeIcon }}</span>
|
||||
</button>
|
||||
<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 ref="volumeTrack" class="w-1 h-full bg-gray-500 mx-2.5 relative cursor-pointer rounded-full" @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="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 v-show="isOpen" class="volumeMenu absolute bg-bg shadow-xs rounded-lg" :class="isHorizontal ? 'horizontal-menu' : 'vertical-menu'" :style="menuStyle">
|
||||
<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 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="cursorStyle" />
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
|
|
@ -25,13 +25,22 @@ export default {
|
|||
isDragging: false,
|
||||
isHovering: false,
|
||||
posY: 0,
|
||||
posX: 0,
|
||||
lastValue: 0.5,
|
||||
isMute: false,
|
||||
trackHeight: 112 - 20,
|
||||
verticalTrackHeight: 112 - 20,
|
||||
horizontalTrackWidth: 112 - 20,
|
||||
openTimeout: null
|
||||
}
|
||||
},
|
||||
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: {
|
||||
get() {
|
||||
return this.value
|
||||
|
|
@ -40,14 +49,58 @@ export default {
|
|||
try {
|
||||
localStorage.setItem('volume', val)
|
||||
} catch (error) {
|
||||
console.error('Failed to store volume', err)
|
||||
console.error('Failed to store volume', error)
|
||||
}
|
||||
this.$emit('input', val)
|
||||
}
|
||||
},
|
||||
cursorBottom() {
|
||||
var bottom = this.trackHeight * this.volume
|
||||
return bottom - 3
|
||||
menuStyle() {
|
||||
if (this.isHorizontal) {
|
||||
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() {
|
||||
if (this.volume <= 0) return 'volume_mute'
|
||||
|
|
@ -89,12 +142,21 @@ export default {
|
|||
}, 600)
|
||||
},
|
||||
mousemove(e) {
|
||||
var diff = this.posY - e.y
|
||||
this.posY = e.y
|
||||
var volShift = diff / this.trackHeight
|
||||
var newVol = this.volume + volShift
|
||||
newVol = Math.min(Math.max(0, newVol), 1)
|
||||
this.volume = newVol
|
||||
if (this.isHorizontal) {
|
||||
// Horizontal movement
|
||||
const diff = e.x - this.posX
|
||||
this.posX = e.x
|
||||
const volShift = diff / this.trackSize
|
||||
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()
|
||||
},
|
||||
mouseup(e) {
|
||||
|
|
@ -106,10 +168,17 @@ export default {
|
|||
},
|
||||
mousedownTrack(e) {
|
||||
this.isDragging = true
|
||||
this.posY = e.y
|
||||
var vol = 1 - e.offsetY / this.trackHeight
|
||||
vol = Math.min(Math.max(vol, 0), 1)
|
||||
this.volume = vol
|
||||
|
||||
if (this.isHorizontal) {
|
||||
this.posX = e.x
|
||||
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('mouseup', this.mouseup)
|
||||
e.preventDefault()
|
||||
|
|
@ -130,9 +199,13 @@ export default {
|
|||
this.clickVolumeIcon()
|
||||
},
|
||||
clickVolumeTrack(e) {
|
||||
var vol = 1 - e.offsetY / this.trackHeight
|
||||
vol = Math.min(Math.max(vol, 0), 1)
|
||||
this.volume = vol
|
||||
if (this.isHorizontal) {
|
||||
const vol = e.offsetX / this.trackSize
|
||||
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() {
|
||||
|
|
@ -151,3 +224,52 @@ export default {
|
|||
}
|
||||
}
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<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">
|
||||
<span class="material-symbols text-5xl text-white/30 cursor-pointer hover:text-white/90">arrow_back_ios</span>
|
||||
</div>
|
||||
|
|
@ -11,11 +11,22 @@
|
|||
</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">
|
||||
<p class="font-mono">{{ page }} / {{ numPages }}</p>
|
||||
<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">
|
||||
<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 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" />
|
||||
<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" />
|
||||
</div>
|
||||
|
||||
|
|
@ -56,7 +67,12 @@ export default {
|
|||
page: 1,
|
||||
numPages: 0,
|
||||
pdfDocInitParams: null,
|
||||
isRefreshing: false
|
||||
isRefreshing: false,
|
||||
zoomLevel: '1',
|
||||
editingPage: false,
|
||||
pageInput: 1,
|
||||
viewMode: 'single', // 'single' or 'continuous'
|
||||
scrollPosition: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -76,6 +92,15 @@ export default {
|
|||
if (this.windowHeight < 400 || !this.playerOpen) return this.windowHeight - 120
|
||||
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() {
|
||||
return Math.floor((this.windowWidth * 10) / this.fitToPageWidth) / 10
|
||||
},
|
||||
|
|
@ -86,7 +111,7 @@ export default {
|
|||
return this.page > 1
|
||||
},
|
||||
canScaleUp() {
|
||||
return this.scale < this.maxScale
|
||||
return this.scale < 2
|
||||
},
|
||||
canScaleDown() {
|
||||
return this.scale > 1
|
||||
|
|
@ -109,12 +134,47 @@ export default {
|
|||
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: {
|
||||
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() {
|
||||
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() {
|
||||
if (!this.keepProgress) return
|
||||
|
|
@ -146,11 +206,13 @@ export default {
|
|||
prev() {
|
||||
if (this.page <= 1) return
|
||||
this.page--
|
||||
this.pageInput = this.page
|
||||
this.updateProgress()
|
||||
},
|
||||
next() {
|
||||
if (this.page >= this.numPages) return
|
||||
this.page++
|
||||
this.pageInput = this.page
|
||||
this.updateProgress()
|
||||
},
|
||||
async refreshToken() {
|
||||
|
|
@ -187,6 +249,91 @@ export default {
|
|||
this.windowWidth = window.innerWidth
|
||||
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() {
|
||||
this.pdfDocInitParams = {
|
||||
url: this.ebookUrl,
|
||||
|
|
@ -194,17 +341,22 @@ export default {
|
|||
Authorization: `Bearer ${this.userToken}`
|
||||
}
|
||||
}
|
||||
// Initialize zoom level to match default scale
|
||||
this.zoomLevel = this.scale.toString()
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.windowWidth = window.innerWidth
|
||||
this.windowHeight = window.innerHeight
|
||||
window.addEventListener('resize', this.resize)
|
||||
document.removeEventListener('keydown', this.keyboardHandler)
|
||||
document.addEventListener('keydown', this.keyboardHandler)
|
||||
|
||||
this.init()
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.resize)
|
||||
document.removeEventListener('keydown', this.keyboardHandler)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
</svg>
|
||||
<div class="px-1">
|
||||
<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>
|
||||
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
<span class="material-symbols text-5xl py-1">show_chart</span>
|
||||
<div class="px-1">
|
||||
<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>
|
||||
|
||||
|
|
@ -24,7 +24,7 @@
|
|||
</svg>
|
||||
<div class="px-1">
|
||||
<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>
|
||||
|
||||
|
|
@ -32,7 +32,7 @@
|
|||
<span class="material-symbols text-5xl pt-1">insert_drive_file</span>
|
||||
<div class="px-1">
|
||||
<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>
|
||||
|
||||
|
|
@ -40,7 +40,7 @@
|
|||
<span class="material-symbols text-5xl pt-1">audio_file</span>
|
||||
<div class="px-1">
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -208,11 +208,12 @@ export default {
|
|||
table-layout: fixed;
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
color: var(--table-text-color);
|
||||
}
|
||||
|
||||
#backups td,
|
||||
#backups th {
|
||||
border: 1px solid #2e2e2e;
|
||||
border: 1px solid var(--table-border);
|
||||
padding: 8px 8px;
|
||||
text-align: left;
|
||||
}
|
||||
|
|
@ -222,11 +223,15 @@ export default {
|
|||
}
|
||||
|
||||
#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 {
|
||||
background-color: #444;
|
||||
background-color: var(--table-row-hover-bg);
|
||||
}
|
||||
|
||||
#backups th {
|
||||
|
|
@ -234,6 +239,7 @@ export default {
|
|||
font-weight: 600;
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
background-color: #333;
|
||||
background-color: var(--table-header-bg);
|
||||
color: var(--table-text-color);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -183,38 +183,44 @@ export default {
|
|||
#accounts {
|
||||
table-layout: fixed;
|
||||
border-collapse: collapse;
|
||||
border: 1px solid #474747;
|
||||
border: 1px solid var(--color-border);
|
||||
width: 100%;
|
||||
background-color: var(--color-primary);
|
||||
}
|
||||
|
||||
#accounts td,
|
||||
#accounts th {
|
||||
/* border: 1px solid #2e2e2e; */
|
||||
padding: 8px 8px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--color-border-light);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
#accounts td.py-0 {
|
||||
padding: 0px 8px;
|
||||
}
|
||||
|
||||
/* Row striping */
|
||||
#accounts tr:nth-child(even) {
|
||||
background-color: #373838;
|
||||
background-color: var(--color-fg);
|
||||
}
|
||||
|
||||
#accounts tr:nth-child(odd) {
|
||||
background-color: #2f2f2f;
|
||||
background-color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* Hover state */
|
||||
#accounts tr:hover {
|
||||
background-color: #444;
|
||||
background-color: var(--color-hover);
|
||||
}
|
||||
|
||||
/* Table header */
|
||||
#accounts th {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
background-color: #272727;
|
||||
background-color: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
border-bottom: 2px solid var(--color-border);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -278,7 +278,17 @@ export default {
|
|||
this.details.narrators = [...(this.mediaMetadata.narrators || [])]
|
||||
this.details.genres = [...(this.mediaMetadata.genres || [])]
|
||||
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.language = this.mediaMetadata.language || null
|
||||
this.details.isbn = this.mediaMetadata.isbn || null
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ export default {
|
|||
disconnect() {
|
||||
console.log('[SOCKET] Disconnected')
|
||||
this.isSocketConnected = false
|
||||
this.updateSocketConnectionToast(this.$strings.ToastSocketDisconnected, 'error', null)
|
||||
// this.updateSocketConnectionToast(this.$strings.ToastSocketDisconnected, 'error', null)
|
||||
},
|
||||
reconnect() {
|
||||
console.log('[SOCKET] reconnected')
|
||||
|
|
@ -377,6 +377,9 @@ export default {
|
|||
if (!provider?.id) return
|
||||
this.$store.commit('scanners/removeCustomMetadataProvider', provider)
|
||||
},
|
||||
initializeTheme() {
|
||||
this.$store.dispatch('theme/initializeTheme')
|
||||
},
|
||||
initializeSocket() {
|
||||
if (this.$root.socket) {
|
||||
// Can happen in dev due to hot reload
|
||||
|
|
@ -605,6 +608,7 @@ export default {
|
|||
mounted() {
|
||||
this.updateBodyClass()
|
||||
this.resize()
|
||||
this.initializeTheme()
|
||||
this.$eventBus.$on('change-lang', this.changeLanguage)
|
||||
this.$eventBus.$on('token_refreshed', this.tokenRefreshed)
|
||||
window.addEventListener('resize', this.resize)
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@
|
|||
</td>
|
||||
<!-- -->
|
||||
<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>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
@ -163,31 +163,38 @@ export default {
|
|||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
border: 1px solid #474747;
|
||||
border: 1px solid var(--table-border);
|
||||
}
|
||||
|
||||
/* Header row */
|
||||
.rssFeedsTable tr:first-child {
|
||||
background-color: #272727;
|
||||
background-color: var(--table-header-bg);
|
||||
}
|
||||
|
||||
/* Normal rows */
|
||||
.rssFeedsTable tr:not(:first-child) {
|
||||
background-color: #373838;
|
||||
background-color: var(--table-row-bg);
|
||||
}
|
||||
|
||||
/* Zebra striping */
|
||||
.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) {
|
||||
background-color: #474747;
|
||||
background-color: var(--table-row-hover-bg);
|
||||
}
|
||||
|
||||
/* Cells */
|
||||
.rssFeedsTable td {
|
||||
padding: 4px 8px;
|
||||
color: var(--table-text);
|
||||
}
|
||||
|
||||
.rssFeedsTable th {
|
||||
padding: 4px 8px;
|
||||
font-size: 0.75rem;
|
||||
color: var(--table-text-header);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -515,26 +515,35 @@ export default {
|
|||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
border: 1px solid #474747;
|
||||
border: 1px solid var(--table-border);
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.userSessionsTable tr:first-child {
|
||||
background-color: #272727;
|
||||
background-color: var(--table-header-bg);
|
||||
}
|
||||
|
||||
/* Normal rows */
|
||||
.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) {
|
||||
background-color: #2f2f2f;
|
||||
background-color: var(--table-row-alt-bg);
|
||||
}
|
||||
|
||||
/* Hover */
|
||||
.userSessionsTable tr:hover:not(:first-child) {
|
||||
background-color: #474747;
|
||||
background-color: var(--table-row-hover-bg);
|
||||
}
|
||||
|
||||
/* Selected */
|
||||
.userSessionsTable tr.selected {
|
||||
background-color: #474747;
|
||||
}
|
||||
.userSessionsTable td {
|
||||
padding: 4px 8px;
|
||||
background-color: var(--table-row-hover-bg);
|
||||
}
|
||||
|
||||
.userSessionsTable td,
|
||||
.userSessionsTable th {
|
||||
padding: 4px 8px;
|
||||
font-size: 0.75rem;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
</div>
|
||||
<div class="px-3">
|
||||
<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>
|
||||
|
||||
|
|
@ -21,7 +21,7 @@
|
|||
</div>
|
||||
<div class="px-1">
|
||||
<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>
|
||||
|
||||
|
|
@ -31,7 +31,7 @@
|
|||
</div>
|
||||
<div class="px-1">
|
||||
<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>
|
||||
|
|
@ -47,10 +47,10 @@
|
|||
<template v-for="(item, index) in mostRecentListeningSessions">
|
||||
<div :key="item.id" class="w-full py-0.5">
|
||||
<div class="flex items-center mb-1">
|
||||
<p class="text-sm text-white/70 w-8">{{ index + 1 }}. </p>
|
||||
<p class="text-sm text-white w-8">{{ index + 1 }}. </p>
|
||||
<div class="w-56">
|
||||
<p class="text-sm text-white/80 truncate">{{ item.mediaMetadata ? item.mediaMetadata.title : '' }}</p>
|
||||
<p class="text-xs text-white/50">{{ $dateDistanceFromNow(item.updatedAt) }}</p>
|
||||
<p class="text-sm text-white truncate">{{ item.mediaMetadata ? item.mediaMetadata.title : '' }}</p>
|
||||
<p class="text-xs text-white">{{ $dateDistanceFromNow(item.updatedAt) }}</p>
|
||||
</div>
|
||||
<div class="grow" />
|
||||
<div class="w-18 text-right">
|
||||
|
|
|
|||
|
|
@ -168,25 +168,44 @@ export default {
|
|||
.userAudiobooksTable {
|
||||
border-collapse: collapse;
|
||||
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) {
|
||||
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) {
|
||||
background-color: #474747;
|
||||
background-color: var(--color-hover);
|
||||
}
|
||||
|
||||
/* Finished rows (green hint, readable in both modes) */
|
||||
.userAudiobooksTable tr.isFinished {
|
||||
background-color: rgba(76, 175, 80, 0.1);
|
||||
}
|
||||
.userAudiobooksTable td {
|
||||
padding: 4px 8px;
|
||||
background-color: rgba(76, 175, 80, 0.12);
|
||||
}
|
||||
|
||||
/* Table cells */
|
||||
.userAudiobooksTable td,
|
||||
.userAudiobooksTable th {
|
||||
padding: 4px 8px;
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text);
|
||||
border-bottom: 1px solid var(--color-border-light);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -241,25 +241,37 @@ export default {
|
|||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
border: 1px solid #474747;
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
/* Header row */
|
||||
.userSessionsTable tr:first-child {
|
||||
background-color: #272727;
|
||||
background-color: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
/* Body rows */
|
||||
.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) {
|
||||
background-color: #2f2f2f;
|
||||
background-color: var(--color-fg);
|
||||
}
|
||||
|
||||
/* Hover state */
|
||||
.userSessionsTable tr:hover:not(:first-child) {
|
||||
background-color: #474747;
|
||||
}
|
||||
.userSessionsTable td {
|
||||
padding: 4px 8px;
|
||||
background-color: var(--color-hover);
|
||||
}
|
||||
|
||||
/* Table cells */
|
||||
.userSessionsTable td,
|
||||
.userSessionsTable th {
|
||||
padding: 4px 8px;
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text);
|
||||
border-bottom: 1px solid var(--color-border-light);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
56
client/store/theme.js
Normal file
56
client/store/theme.js
Normal 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
|
||||
}
|
||||
BIN
docs/Horror Tales 01 (v1 #7)/Horror Tales 01 (v1 #7).pdf
Normal file
BIN
docs/Horror Tales 01 (v1 #7)/Horror Tales 01 (v1 #7).pdf
Normal file
Binary file not shown.
19
docs/Horror Tales 01 (v1 #7)/metadata.json
Normal file
19
docs/Horror Tales 01 (v1 #7)/metadata.json
Normal 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
|
||||
}
|
||||
BIN
docs/pg11-images-3/cover.jpg
Normal file
BIN
docs/pg11-images-3/cover.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 102 KiB |
26
docs/pg11-images-3/metadata.json
Normal file
26
docs/pg11-images-3/metadata.json
Normal 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
|
||||
}
|
||||
BIN
docs/pg11-images-3/pg11-images-3.epub
Normal file
BIN
docs/pg11-images-3/pg11-images-3.epub
Normal file
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue