diff --git a/client/assets/app.css b/client/assets/app.css
index 36b3b4bad..163fce8b7 100644
--- a/client/assets/app.css
+++ b/client/assets/app.css
@@ -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 */
diff --git a/client/assets/theme.css b/client/assets/theme.css
new file mode 100644
index 000000000..362f042c6
--- /dev/null
+++ b/client/assets/theme.css
@@ -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;
+}
diff --git a/client/components/app/Appbar.vue b/client/components/app/Appbar.vue
index f74134041..dbd96c47b 100644
--- a/client/components/app/Appbar.vue
+++ b/client/components/app/Appbar.vue
@@ -21,7 +21,13 @@
-
+
+
+
@@ -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() {
diff --git a/client/components/app/SideRail.vue b/client/components/app/SideRail.vue
index 5f3642011..4b95d6394 100644
--- a/client/components/app/SideRail.vue
+++ b/client/components/app/SideRail.vue
@@ -14,7 +14,7 @@
-
+
{{ $strings.ButtonLatest }}
@@ -22,7 +22,7 @@
-
+
@@ -32,7 +32,7 @@
-
+
@@ -42,7 +42,7 @@
-
+
{{ $strings.ButtonCollections }}
@@ -50,7 +50,7 @@
-
+
{{ $strings.ButtonPlaylists }}
@@ -58,7 +58,7 @@
-
+
-
+
{{ $strings.LabelNarrators }}
@@ -79,7 +79,7 @@
-
+
{{ $strings.ButtonStats }}
@@ -87,7 +87,7 @@
-
+
{{ $strings.ButtonAdd }}
@@ -95,7 +95,7 @@
-
+
{{ $strings.ButtonDownloadQueue }}
@@ -103,7 +103,7 @@
-
+
warning
{{ $strings.ButtonIssues }}
diff --git a/client/components/cards/AuthorCard.vue b/client/components/cards/AuthorCard.vue
index 053473934..82bbd1094 100644
--- a/client/components/cards/AuthorCard.vue
+++ b/client/components/cards/AuthorCard.vue
@@ -9,7 +9,7 @@
{{ name }}
-
{{ numBooks }} {{ $strings.LabelBooks }}
+
{{ numBooks }} {{ numBooks === 1 ? $strings.LabelBook : $strings.LabelBooks }}
diff --git a/client/components/controls/PlaybackSpeedControl.vue b/client/components/controls/PlaybackSpeedControl.vue
index 5decea57b..b6beb1f33 100644
--- a/client/components/controls/PlaybackSpeedControl.vue
+++ b/client/components/controls/PlaybackSpeedControl.vue
@@ -1,15 +1,15 @@
- {{ playbackRateDisplay }}x
+ {{ playbackRateDisplay }}x
-
-
-
+
+
-
+
-
+
@@ -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 {
}
}
+
+
diff --git a/client/components/controls/VolumeControl.vue b/client/components/controls/VolumeControl.vue
index a17a3b357..f21cec780 100644
--- a/client/components/controls/VolumeControl.vue
+++ b/client/components/controls/VolumeControl.vue
@@ -4,10 +4,10 @@
{{ volumeIcon }}
-
-
-
-
+
@@ -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 {
}
}
+
+
diff --git a/client/components/readers/PdfReader.vue b/client/components/readers/PdfReader.vue
index d9459d768..2ea758461 100644
--- a/client/components/readers/PdfReader.vue
+++ b/client/components/readers/PdfReader.vue
@@ -1,6 +1,6 @@
-
+
arrow_back_ios
@@ -11,11 +11,22 @@
-
-
{{ page }} / {{ numPages }}
+
+
+
{{ page }} / {{ numPages }}
-
+
+
+
@@ -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)
}
}
diff --git a/client/components/stats/PreviewIcons.vue b/client/components/stats/PreviewIcons.vue
index 18cd1daba..fc14fd90a 100644
--- a/client/components/stats/PreviewIcons.vue
+++ b/client/components/stats/PreviewIcons.vue
@@ -6,7 +6,7 @@
{{ $formatNumber(totalItems) }}
-
{{ $strings.LabelStatsItemsInLibrary }}
+
{{ $strings.LabelStatsItemsInLibrary }}
@@ -14,7 +14,7 @@
show_chart
{{ $formatNumber(totalTime) }}
-
{{ useOverallHours ? $strings.LabelStatsOverallHours : $strings.LabelStatsOverallDays }}
+
{{ useOverallHours ? $strings.LabelStatsOverallHours : $strings.LabelStatsOverallDays }}
@@ -24,7 +24,7 @@
{{ $formatNumber(totalAuthors) }}
-
{{ $strings.LabelStatsAuthors }}
+
{{ $strings.LabelStatsAuthors }}
@@ -32,7 +32,7 @@
insert_drive_file
{{ $formatNumber(totalSizeNum) }}
-
{{ $strings.LabelSize }} ({{ totalSizeMod }})
+
{{ $strings.LabelSize }} ({{ totalSizeMod }})
@@ -40,7 +40,7 @@
audio_file
{{ $formatNumber(numAudioTracks) }}
-
{{ $strings.LabelStatsAudioTracks }}
+
{{ $strings.LabelStatsAudioTracks }}
diff --git a/client/components/tables/BackupsTable.vue b/client/components/tables/BackupsTable.vue
index f769abdb0..a1dcd4a0c 100644
--- a/client/components/tables/BackupsTable.vue
+++ b/client/components/tables/BackupsTable.vue
@@ -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);
}
diff --git a/client/components/tables/UsersTable.vue b/client/components/tables/UsersTable.vue
index c71710181..c66cba8e0 100644
--- a/client/components/tables/UsersTable.vue
+++ b/client/components/tables/UsersTable.vue
@@ -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);
}
diff --git a/client/components/widgets/BookDetailsEdit.vue b/client/components/widgets/BookDetailsEdit.vue
index db3b86ed6..f630abedc 100644
--- a/client/components/widgets/BookDetailsEdit.vue
+++ b/client/components/widgets/BookDetailsEdit.vue
@@ -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
diff --git a/client/layouts/default.vue b/client/layouts/default.vue
index 9f15af67e..119f51d08 100644
--- a/client/layouts/default.vue
+++ b/client/layouts/default.vue
@@ -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)
diff --git a/client/pages/config/rss-feeds.vue b/client/pages/config/rss-feeds.vue
index b5ba90a38..d607f9cec 100644
--- a/client/pages/config/rss-feeds.vue
+++ b/client/pages/config/rss-feeds.vue
@@ -57,7 +57,7 @@
-
+
|
@@ -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);
}
diff --git a/client/pages/config/sessions.vue b/client/pages/config/sessions.vue
index 86ec7eec6..7e4742c27 100644
--- a/client/pages/config/sessions.vue
+++ b/client/pages/config/sessions.vue
@@ -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;
diff --git a/client/pages/config/stats.vue b/client/pages/config/stats.vue
index 137ac5a8d..0d259d978 100644
--- a/client/pages/config/stats.vue
+++ b/client/pages/config/stats.vue
@@ -11,7 +11,7 @@
{{ $formatNumber(userItemsFinished.length) }}
-
{{ $strings.LabelStatsItemsFinished }}
+
{{ $strings.LabelStatsItemsFinished }}
@@ -21,7 +21,7 @@
{{ $formatNumber(totalDaysListened) }}
-
{{ $strings.LabelStatsDaysListened }}
+
{{ $strings.LabelStatsDaysListened }}
@@ -31,7 +31,7 @@
{{ $formatNumber(totalMinutesListening) }}
-
{{ $strings.LabelStatsMinutesListening }}
+
{{ $strings.LabelStatsMinutesListening }}
@@ -47,10 +47,10 @@
-
{{ index + 1 }}.
+
{{ index + 1 }}.
-
{{ item.mediaMetadata ? item.mediaMetadata.title : '' }}
-
{{ $dateDistanceFromNow(item.updatedAt) }}
+
{{ item.mediaMetadata ? item.mediaMetadata.title : '' }}
+
{{ $dateDistanceFromNow(item.updatedAt) }}
diff --git a/client/pages/config/users/_id/index.vue b/client/pages/config/users/_id/index.vue
index 34a0fc86c..a26fe5879 100644
--- a/client/pages/config/users/_id/index.vue
+++ b/client/pages/config/users/_id/index.vue
@@ -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);
}
diff --git a/client/pages/config/users/_id/sessions.vue b/client/pages/config/users/_id/sessions.vue
index 060f34be8..5b8b00f5c 100644
--- a/client/pages/config/users/_id/sessions.vue
+++ b/client/pages/config/users/_id/sessions.vue
@@ -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);
}
diff --git a/client/store/theme.js b/client/store/theme.js
new file mode 100644
index 000000000..4eb292c7c
--- /dev/null
+++ b/client/store/theme.js
@@ -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
+}
diff --git a/docs/Horror Tales 01 (v1 #7)/Horror Tales 01 (v1 #7).pdf b/docs/Horror Tales 01 (v1 #7)/Horror Tales 01 (v1 #7).pdf
new file mode 100644
index 000000000..5e352d638
Binary files /dev/null and b/docs/Horror Tales 01 (v1 #7)/Horror Tales 01 (v1 #7).pdf differ
diff --git a/docs/Horror Tales 01 (v1 #7)/metadata.json b/docs/Horror Tales 01 (v1 #7)/metadata.json
new file mode 100644
index 000000000..04638420f
--- /dev/null
+++ b/docs/Horror Tales 01 (v1 #7)/metadata.json
@@ -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
+}
\ No newline at end of file
diff --git a/docs/pg11-images-3/cover.jpg b/docs/pg11-images-3/cover.jpg
new file mode 100644
index 000000000..6049bb1c9
Binary files /dev/null and b/docs/pg11-images-3/cover.jpg differ
diff --git a/docs/pg11-images-3/metadata.json b/docs/pg11-images-3/metadata.json
new file mode 100644
index 000000000..14a3c707c
--- /dev/null
+++ b/docs/pg11-images-3/metadata.json
@@ -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
+}
\ No newline at end of file
diff --git a/docs/pg11-images-3/pg11-images-3.epub b/docs/pg11-images-3/pg11-images-3.epub
new file mode 100644
index 000000000..4395bcabb
Binary files /dev/null and b/docs/pg11-images-3/pg11-images-3.epub differ