diff --git a/src/css/_controls.scss b/src/css/_controls.scss
index f152b9431..afa50c139 100644
--- a/src/css/_controls.scss
+++ b/src/css/_controls.scss
@@ -30,29 +30,37 @@
}
select.playback-speed, select.playback-voices {
- padding-top: 18px;
+ height: 24px;
+ padding: 0 6px 0 26px;
-moz-appearance: none;
-webkit-appearance: none;
appearance: none;
- font-size: 10px;
- text-align-last: center;
+ font-size: 12px;
color: $controlsText;
border: none;
+ border-radius: 4px;
cursor: pointer;
+ background: transparent no-repeat 4px 50% / 18px;
+ &:hover {
+ background-color: rgba($white, 0.1);
+ }
option, optgroup {
background: $controlsBG;
}
}
select.playback-speed {
- width: 30px;
- background: transparent url("icons/playback-speed.svg") 50% 0 no-repeat;
+ background-image: url("icons/playback-speed.svg");
+ // the gauge only fills the top half of the icon's canvas
+ background-position-y: 6px;
}
select.playback-voices {
- width: 30px;
- background: transparent url("icons/voice.svg") 50% 0 no-repeat;
- background-size: 18px;
+ max-width: 200px;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ background-image: url("icons/voice.svg");
}
.active {
@@ -99,16 +107,24 @@
.read-aloud {
display: none;
position: absolute;
- bottom: 100%;
+ bottom: calc(100% + 12px);
+ // left/right 0 + auto margins centers the max-content width without a
+ // transform, which the slideUp animation already owns
right: 0;
left: 0;
z-index: 1;
- padding: 5px 0;
- margin: 0;
- align-items: center;
- justify-content: center;
- list-style: none;
- background: $controlsBG;
+ width: max-content;
+ max-width: min(420px, calc(100% - 24px));
+ padding: 6px 8px;
+ margin: 0 auto;
+ flex-direction: column;
+ align-items: stretch;
+ background: rgba($controlsBG, 0.75);
+ backdrop-filter: blur(10px);
+ -webkit-backdrop-filter: blur(10px);
+ border: 1px solid rgba($white, 0.12);
+ border-radius: 8px;
+ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.35);
&.visible {
display: flex;
@@ -119,8 +135,243 @@
}
}
+ // Specific enough to beat `.visible button`
+ button.read-aloud-size-button {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 28px;
+ height: 28px;
+ padding: 0;
+ color: $controlsText;
+ border: none;
+ border-radius: 50%;
+ background: transparent;
+ cursor: pointer;
+ opacity: 0.7;
+
+ &:hover, &:focus-visible {
+ opacity: 1;
+ background: rgba($white, 0.1);
+ }
+
+ svg {
+ width: 16px;
+ height: 16px;
+ fill: none;
+ stroke: currentColor;
+ stroke-width: 1.75;
+ stroke-linecap: round;
+ stroke-linejoin: round;
+ }
+
+ .icon-expand {
+ display: none;
+ }
+ }
+
+ // Far right of the playback row; see the .read-aloud-controls grid
+ li.read-aloud-size {
+ grid-column: 5;
+ justify-self: end;
+ padding: 0;
+ }
+
+ &.minimized {
+ .read-aloud-chapter, .read-aloud-settings {
+ display: none;
+ }
+
+ // Floats just outside the bar's right edge instead
+ li.read-aloud-size {
+ position: absolute;
+ top: 50%;
+ left: 100%;
+ margin-left: 6px;
+ padding: 0;
+ transform: translateY(-50%);
+
+ button.read-aloud-size-button {
+ width: 32px;
+ height: 32px;
+ opacity: 1;
+ background: rgba($controlsBG, 0.75);
+ backdrop-filter: blur(10px);
+ -webkit-backdrop-filter: blur(10px);
+ border: 1px solid rgba($white, 0.12);
+ border-radius: 50%;
+ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.35);
+
+ &:hover, &:focus-visible {
+ background: rgba($controlsBG, 0.9);
+ }
+
+ .icon-minimize {
+ display: none;
+ }
+
+ .icon-expand {
+ display: block;
+ }
+ }
+ }
+
+ // Without the toggle there's nothing to balance
+ .read-aloud-controls {
+ grid-template-columns: repeat(3, auto);
+
+ > :first-child {
+ grid-column-start: auto;
+ }
+ }
+ }
+
+ &.maximized {
+ top: auto;
+ bottom: 100%;
+ z-index: 3;
+ width: auto;
+ max-width: none;
+ box-sizing: border-box;
+ height: var(--read-aloud-maximized-height, 100vh);
+ padding: 32px 0 24px;
+ margin: 0;
+ // controls sit at the bottom; the cover takes up the space above them
+ justify-content: flex-end;
+ align-items: center;
+ background: $controlsBG;
+ border: none;
+ border-radius: 0;
+ box-shadow: none;
+
+ > * {
+ box-sizing: border-box;
+ width: min(480px, calc(100% - 48px));
+ }
+
+
+ .read-aloud-chapter {
+ padding: 0 0 16px;
+ }
+
+ button.read-aloud-chapter-name {
+ width: 100%;
+ margin: 0 0 12px;
+ padding: 4px 8px;
+ border-radius: 4px;
+ }
+
+ .read-aloud-chapter-text {
+ white-space: normal;
+ }
+
+
+ .read-aloud-settings {
+ margin-top: 16px;
+ padding-top: 16px;
+ }
+
+ .read-aloud-cover:not(.failed) {
+ display: block;
+ flex: 1 1 auto;
+ min-height: 0;
+ margin-bottom: 24px;
+ object-fit: contain;
+ }
+
+ }
+
+ .read-aloud-cover {
+ display: none;
+ }
+
+ .read-aloud-chapter {
+ min-width: 0;
+ padding: 2px 8px 4px;
+
+ &.empty {
+ display: none;
+ }
+ }
+
+ // Specific enough to beat `.visible button`
+ button.read-aloud-chapter-name {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+ // Bleed over the toolbar's and row's padding so the whole top strip of
+ // the toolbar is clickable
+ width: calc(100% + 32px);
+ margin: -8px -16px 0;
+ padding: 8px 16px 4px;
+ font-size: 12px;
+ line-height: 1.3;
+ color: $controlsText;
+ background: transparent;
+ border: none;
+ border-radius: 8px 8px 0 0;
+ cursor: pointer;
+
+ &:hover, &:focus-visible {
+ background: rgba($white, 0.1);
+ }
+
+ ia-icon-toc {
+ flex-shrink: 0;
+ width: 14px;
+ height: 14px;
+ --iconFillColor: #{$controlsText};
+ }
+ }
+
+ .read-aloud-chapter-text {
+ min-width: 0;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+
+
+ br-tts-chapter-progress {
+ margin-top: 4px;
+ }
+
+ .read-aloud-chapter-label {
+ opacity: 0.65;
+ &:not(:empty) + .read-aloud-chapter-title:not(:empty)::before {
+ content: "· ";
+ opacity: 0.65;
+ }
+ }
+
+ .read-aloud-controls, .read-aloud-settings {
+ display: flex;
+ padding: 0;
+ margin: 0;
+ align-items: center;
+ justify-content: center;
+ list-style: none;
+ }
+
+ // The empty 1fr column mirrors the size toggle's, keeping play centered
+ .read-aloud-controls {
+ display: grid;
+ grid-template-columns: 1fr repeat(3, auto) 1fr;
+
+ > :first-child {
+ grid-column-start: 2;
+ }
+ }
+
+ .read-aloud-settings {
+ margin-top: 6px;
+ padding-top: 6px;
+ border-top: 1px solid rgba($white, 0.12);
+ }
+
li {
- padding: 0 10px;
+ padding: 0 8px;
}
[name="play"] {
diff --git a/src/ia-bookreader/ia-bookreader.js b/src/ia-bookreader/ia-bookreader.js
index 233572eeb..0201da223 100644
--- a/src/ia-bookreader/ia-bookreader.js
+++ b/src/ia-bookreader/ia-bookreader.js
@@ -365,6 +365,17 @@ export class IaBookReader extends LitElement {
return this._brWidth >= 640;
}
+ /**
+ * Panels fire `closeMenu` after the user picks something (e.g. a search
+ * result or chapter). Only close on narrow screens, where the menu covers
+ * the book.
+ */
+ closeMenuIfNarrow() {
+ if (!this.isWideEnoughToOpenMenu) {
+ this.itemNav?.closeMenu();
+ }
+ }
+
/**
* Open side menu
* @param {string} menuId
@@ -585,6 +596,7 @@ export class IaBookReader extends LitElement {
.menuShortcuts=${this.menuShortcuts}
.menuContents=${this.menuContents}
.openMenu=${this.openMenuName}
+ @closeMenu=${this.closeMenuIfNarrow}
>
diff --git a/src/plugins/plugin.chapters.js b/src/plugins/plugin.chapters.js
index e43ebb971..d1f545080 100644
--- a/src/plugins/plugin.chapters.js
+++ b/src/plugins/plugin.chapters.js
@@ -120,10 +120,7 @@ export class ChaptersPlugin extends BookReaderPlugin {
label: 'Table of Contents',
component: html`
{
this._chaptersPanel = e.target;
this._updateCurrent();
@@ -174,10 +171,7 @@ export class ChaptersPlugin extends BookReaderPlugin {
$(event.target).addClass('front');
})
.on("mouseleave", event => $(event.target).removeClass('front'))
- .on('click', () => {
- this._updateCurrent(tocEntry.pageIndex);
- this.br.jumpToIndex(tocEntry.pageIndex, { ariaLive: true });
- });
+ .on('click', () => this._jumpToChapter(tocEntry.pageIndex));
this.br.$('.BRchapter, .BRsearch').each((i, el) => {
const $el = $(el);
@@ -187,11 +181,35 @@ export class ChaptersPlugin extends BookReaderPlugin {
});
}
+ /**
+ * Fires `chapterSelected` so e.g. read aloud can follow along
+ * @param {PageIndex} pageIndex
+ */
+ _jumpToChapter(pageIndex) {
+ if (pageIndex == undefined) return;
+ this._updateCurrent(pageIndex);
+ this.br.jumpToIndex(pageIndex, { ariaLive: true });
+ this.br.trigger('chapterSelected', { pageIndex });
+ }
+
+ /** @type {Promise