mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-07-07 18:01:42 +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 './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
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">
|
<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 ? '' : '' }}
|
||||||
|
</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() {
|
||||||
|
|
|
||||||
|
|
@ -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"></span>
|
<span class="material-symbols text-2xl"></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"></span>
|
<span class="material-symbols text-2xl"></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"></span>
|
<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>
|
<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"></span>
|
<span class="material-symbols text-2xl"></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"></span>
|
<span class="material-symbols text-2xl"></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"></span>
|
<span class="material-symbols text-2xl"></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>
|
||||||
|
|
|
||||||
|
|
@ -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 -->
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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 }}. </p>
|
<p class="text-sm text-white w-8">{{ index + 1 }}. </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">
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
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