feat: add speech dialog functionality and new earphone icon

- Introduced a new SpeechDialog component for text-to-speech features.
- Updated the Reader component to handle speech dialog state.
- Added new earphone icon to the icon font.
- Modified CSS for the speech dialog and updated styles for the next chapter button.
- Enhanced Redux actions and reducers to manage speech dialog state.
- Updated font-face URLs in style.css for better asset management.
This commit is contained in:
troyeguo committed 2026-03-13 16:05:13 +08:00
1 parent 943d12d3e0
commit 168ff3e0b7
16 files changed
+179 -41

No files matched your search

Binary file not shown.
+1
View File
@@ -106,4 +106,5 @@
<glyph unicode="&#xe960;" glyph-name="ai-assist" d="M512 862.476c228.913 0 414.476-185.563 414.476-414.476s-185.563-414.476-414.476-414.476-414.476 185.563-414.476 414.476 185.563 414.476 414.476 414.476zM499.81 691.81c-19.31 0-36.035-34.67-45.032-51.395l-23.942-57.198c-15.151-35.546-42.887-63.283-77.479-78.071l-0.954-0.363-57.198-23.942c-16.725-9.021-51.395-25.722-51.395-45.032s34.67-36.035 51.395-45.032l57.198-23.942c35.546-15.151 63.283-42.887 78.071-77.479l0.363-0.954 23.942-57.198c9.021-16.725 25.722-51.395 45.032-51.395s36.035 34.67 45.032 51.395l23.942 57.198c15.151 35.546 42.887 63.283 77.479 78.071l0.954 0.363 57.198 23.942c16.725 9.021 51.395 25.722 51.395 45.032s-34.67 36.035-51.395 45.032l-57.198 23.942c-35.546 15.151-63.283 42.887-78.071 77.479l-0.363 0.954-23.942 57.198c-9.021 16.725-25.722 51.395-45.032 51.395z" />
<glyph unicode="&#xe961;" glyph-name="cloud" d="M213.333 533.333c0 0.044 0 0.096 0 0.148 0 141.385 114.615 256 256 256 111.44 0 206.249-71.206 241.41-170.602l0.552-1.791c128.641-14.77 227.615-123.037 227.615-254.421 0-141.385-114.615-256-256-256-0.086 0-0.172 0-0.258 0h-383.986c-0.039 0-0.086 0-0.132 0-117.821 0-213.333 95.513-213.333 213.333 0 87.234 52.358 162.239 127.364 195.303l1.366 0.537c-0.378 5.229-0.595 11.334-0.597 17.488v0.005zM469.333 704c-0.010 0-0.021 0-0.033 0-94.257 0-170.667-76.41-170.667-170.667 0-13.295 1.52-26.235 4.396-38.655l-0.224 1.151c0.654-2.798 1.029-6.011 1.029-9.312 0-20.254-14.112-37.211-33.040-41.576l-0.287-0.056c-57.295-13.565-99.269-64.281-99.269-124.802 0-70.509 56.972-127.711 127.393-128.084h384.035c93.524 0.965 168.969 77.005 168.969 170.667 0 94.261-76.414 170.675-170.675 170.675-0.6 0-1.199-0.003-1.798-0.009l0.091 0.001c-0.256-0.006-0.558-0.009-0.86-0.009-20.547 0-37.701 14.523-41.758 33.864l-0.049 0.278c-16.468 78.451-85.077 136.516-167.251 136.533h-0.002z" />
<glyph unicode="&#xe962;" glyph-name="copy" horiz-adv-x="832" d="M614.432 748.8h192c7.072 0.032 12.768 5.728 12.8 12.8v0c-0.064 18.208-8.288 34.496-21.248 45.376l-0.096 0.064-164.48 136.96c-12.16 9.152-27.296 14.976-43.744 15.776h-0.192c0 0-0.032 0-0.032 0-8.704 0-15.744-7.040-15.744-15.744 0 0 0-0.032 0-0.032v0-154.688c0.192-22.368 18.336-40.448 40.736-40.512v0zM518.4 789.312v170.688h-279.488c-37.664-0.128-68.16-30.624-68.256-68.256v-716.704c0.096-37.664 30.624-68.192 68.256-68.288h512c37.664 0.128 68.16 30.624 68.288 68.256v518.304h-204.8c-52.992 0.064-95.936 43.008-96.032 96v0zM110.912 115.2v708.288h-42.656c-37.664-0.096-68.192-30.624-68.288-68.256v-750.976c0.128-37.664 30.624-68.16 68.256-68.288h546.144c37.664 0.128 68.16 30.624 68.288 68.256v42.688h-503.488c-37.664 0.128-68.16 30.624-68.288 68.256v0z" />
<glyph unicode="&#xe963;" glyph-name="earphone" d="M511.957 874.027c-235.648 0-426.667-191.019-426.667-426.667v-170.667c0-23.564 19.103-42.667 42.667-42.667h42.667v-42.667c0-70.692 57.308-128 128-128s128 57.308 128 128v0 170.667c0 70.692-57.308 128-128 128s-128-57.308-128-128v0 85.333c0 188.513 152.82 341.333 341.333 341.333s341.333-152.82 341.333-341.333v0-85.333c0 70.692-57.308 128-128 128s-128-57.308-128-128v0-170.667c0-70.692 57.308-128 128-128s128 57.308 128 128v0 42.667h42.667c23.564 0 42.667 19.103 42.667 42.667v0 170.667c0 235.648-191.019 426.667-426.667 426.667z" />
</font></defs></svg>

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.
Binary file not shown.
+8 -5
View File
@@ -1,10 +1,10 @@
@font-face {
font-family: 'icomoon';
src: url('fonts/icomoon.eot?mz9umd');
src: url('fonts/icomoon.eot?mz9umd#iefix') format('embedded-opentype'),
url('fonts/icomoon.ttf?mz9umd') format('truetype'),
url('fonts/icomoon.woff?mz9umd') format('woff'),
url('fonts/icomoon.svg?mz9umd#icomoon') format('svg');
src: url('fonts/icomoon.eot?qn1hwh');
src: url('fonts/icomoon.eot?qn1hwh#iefix') format('embedded-opentype'),
url('fonts/icomoon.ttf?qn1hwh') format('truetype'),
url('fonts/icomoon.woff?qn1hwh') format('woff'),
url('fonts/icomoon.svg?qn1hwh#icomoon') format('svg');
font-weight: normal;
font-style: normal;
font-display: block;
@@ -25,6 +25,9 @@
-moz-osx-font-smoothing: grayscale;
}
.icon-earphone:before {
content: "\e963";
}
.icon-copy:before {
content: "\e962";
}
@@ -0,0 +1,45 @@
import React from "react";
import { SpeechDialogProps, SpeechDialogState } from "./interface";
import "./speechDialog.css";
import { ConfigService } from "../../../assets/lib/kookit-extra-browser.min";
import TextToSpeech from "../../textToSpeech";
class SpeechDialog extends React.Component<
SpeechDialogProps,
SpeechDialogState
> {
constructor(props: SpeechDialogProps) {
super(props);
this.state = {
isShowExportAll: false,
isConvertPDF: ConfigService.getReaderConfig("isConvertPDF") === "yes",
};
}
componentDidMount(): void {
this.props.handleFetchPlugins();
}
render() {
return (
<>
<div
className="sort-dialog-container"
onMouseEnter={() => {
this.props.handleSpeechDialog(true);
}}
style={{
right: "65px",
left: "auto",
top: "auto",
bottom: "60px",
width: "300px",
height: "400px",
}}
>
{this.props.plugins && <TextToSpeech />}
</div>
</>
);
}
}
export default SpeechDialog;
@@ -0,0 +1,27 @@
import { connect } from "react-redux";
import {
handleSetting,
handleSpeechDialog,
handleFetchPlugins,
} from "../../../store/actions";
import { stateType } from "../../../store";
import SpeechDialog from "./component";
import { withTranslation } from "react-i18next";
const mapStateToProps = (state: stateType) => {
return {
isSpeechOpen: state.reader.isSpeechOpen,
plugins: state.manager.plugins,
isAuthed: state.manager.isAuthed,
currentBook: state.book.currentBook,
};
};
const actionCreator = {
handleSetting,
handleSpeechDialog,
handleFetchPlugins,
};
export default connect(
mapStateToProps,
actionCreator
)(withTranslation()(SpeechDialog as any) as any);
@@ -0,0 +1,18 @@
import BookModel from "../../../models/Book";
import Plugin from "../../../models/Plugin";
export interface SpeechDialogProps {
isSettingOpen: boolean;
isAboutOpen: boolean;
currentBook: BookModel;
handleSetting: (isSettingOpen: boolean) => void;
handleSpeechDialog: (isAboutOpen: boolean) => void;
handleFetchPlugins: () => void;
isAuthed: boolean;
t: (title: string) => string;
plugins: Plugin[];
}
export interface SpeechDialogState {
isShowExportAll: boolean;
isConvertPDF: boolean;
}
@@ -0,0 +1,5 @@
.icon-export-all {
line-height: 18px;
display: inline-block;
transform: rotate(-90deg) scale(0.7);
}
+55 -32
View File
@@ -14,6 +14,7 @@ import DatabaseService from "../../utils/storage/databaseService";
import ConvertDialog from "../../components/dialogs/convertDialog";
import { isElectron } from "react-device-detect";
import SettingDialog from "../../components/dialogs/settingDialog";
import SpeechDialog from "../../components/dialogs/speechDialog";
let lock = false; //prevent from clicking too fasts
let throttleTime =
@@ -200,43 +201,64 @@ class Reader extends React.Component<ReaderProps, ReaderState> {
<span className="icon-dropdown previous-chapter-single"></span>
</div>
<div
className="next-chapter-single-container"
onClick={async () => {
if (lock) return;
lock = true;
await this.props.htmlBook.rendition.next();
this.handleLocation();
setTimeout(() => (lock = false), throttleTime);
}}
style={{
position: "absolute",
bottom: 10,
right: this.props.isSettingLocked ? 315 : 15,
display: "flex",
flexDirection: "column-reverse",
alignItems: "center",
gap: "8px",
zIndex: 10,
}}
>
<span className="icon-dropdown next-chapter-single"></span>
<div
className="next-chapter-single-container"
onClick={async () => {
if (lock) return;
lock = true;
await this.props.htmlBook.rendition.next();
this.handleLocation();
setTimeout(() => (lock = false), throttleTime);
}}
style={{ position: "static" }}
>
<span className="icon-dropdown next-chapter-single"></span>
</div>
<div
className="next-chapter-single-container"
onClick={async () => {
this.props.handleSpeechDialog(!this.props.isSpeechOpen);
}}
style={{ position: "static", transform: "rotate(0deg)" }}
>
<span
className={`icon-${this.props.isSpeechOpen ? "close" : "earphone"} next-chapter-single`}
></span>
</div>
{this.props.isAuthed &&
!this.props.isHideAIButton &&
ConfigService.getReaderConfig("isDisableAI") !== "yes" && (
<div
className="next-chapter-single-container"
onClick={async () => {
this.props.handleMenuMode("assistant");
this.props.handleOriginalText(
await this.props.htmlBook.rendition.chapterText()
);
this.props.handleOpenMenu(true);
}}
style={{
position: "static",
transform: "rotate(0deg)",
fontWeight: "bold",
fontSize: "17px",
}}
>
AI
</div>
)}
</div>
{this.props.isAuthed &&
!this.props.isHideAIButton &&
ConfigService.getReaderConfig("isDisableAI") !== "yes" && (
<div
className="next-chapter-single-container"
onClick={async () => {
this.props.handleMenuMode("assistant");
this.props.handleOriginalText(
await this.props.htmlBook.rendition.chapterText()
);
this.props.handleOpenMenu(true);
}}
style={{
bottom: "55px",
transform: "rotate(0deg)",
fontWeight: "bold",
fontSize: "17px",
right: this.props.isSettingLocked ? 315 : 15,
}}
>
AI
</div>
)}
</>
)}
<div
@@ -643,6 +665,7 @@ class Reader extends React.Component<ReaderProps, ReaderState> {
{this.props.currentBook.key && <Viewer {...renditionProps} />}
{this.props.isConvertOpen && <ConvertDialog />}
{this.props.isSpeechOpen && <SpeechDialog />}
</div>
);
}
+5 -4
View File
@@ -191,16 +191,13 @@
width: 40px;
height: 40px;
border-radius: 50%;
position: absolute;
bottom: 10px;
right: 15px;
transform: rotate(-90deg);
cursor: pointer;
opacity: 0.2;
display: flex;
justify-content: center;
align-items: center;
z-index: 10;
flex-shrink: 0;
}
.next-chapter-single-container:hover {
opacity: 0.7;
@@ -228,6 +225,10 @@
align-items: center;
justify-content: flex-end;
}
.text-to-speech-icon {
font-size: 22px;
margin-right: 10px;
}
.reader-setting-icon-container,
.reader-zoom-in-icon-container {
width: 50px;
+3
View File
@@ -9,6 +9,7 @@ import {
handleOriginalText,
handleOpenMenu,
handleConvertDialog,
handleSpeechDialog,
handleScale,
handleFetchAuthed,
} from "../../store/actions";
@@ -25,6 +26,7 @@ const mapStateToProps = (state: stateType) => {
readerMode: state.reader.readerMode,
isNavLocked: state.reader.isNavLocked,
isConvertOpen: state.reader.isConvertOpen,
isSpeechOpen: state.reader.isSpeechOpen,
isSettingLocked: state.reader.isSettingLocked,
isAuthed: state.manager.isAuthed,
isSearch: state.manager.isSearch,
@@ -51,6 +53,7 @@ const actionCreator = {
handleConvertDialog,
handleScale,
handleFetchAuthed,
handleSpeechDialog,
};
export default connect(
mapStateToProps,
+2
View File
@@ -8,6 +8,7 @@ export interface ReaderProps {
isNavLocked: boolean;
isSettingLocked: boolean;
isConvertOpen: boolean;
isSpeechOpen: boolean;
isSearch: boolean;
isAuthed: boolean;
isHidePageButton: boolean;
@@ -21,6 +22,7 @@ export interface ReaderProps {
handleFetchNotes: () => void;
handleReaderMode: (readerMode: string) => void;
handleConvertDialog: (isConvertOpen: boolean) => void;
handleSpeechDialog: (isSpeechOpen: boolean) => void;
handleMenuMode: (menuMode: string) => void;
handleOriginalText: (originalText: string) => void;
handleFetchBooks: () => void;
+3
View File
@@ -22,6 +22,9 @@ export function handleColor(color: number) {
export function handleConvertDialog(isConvertOpen: boolean) {
return { type: "HANDLE_CONVERT_DIALOG", payload: isConvertOpen };
}
export function handleSpeechDialog(isSpeechOpen: boolean) {
return { type: "HANDLE_SPEECH_DIALOG", payload: isSpeechOpen };
}
export function handleBookmarks(bookmarks: BookmarkModel[]) {
return { type: "HANDLE_BOOKMARKS", payload: bookmarks };
}
+1
View File
@@ -99,6 +99,7 @@ export type stateType = {
isNavLocked: boolean;
isSettingLocked: boolean;
isConvertOpen: boolean;
isSpeechOpen: boolean;
isHideFooter: boolean;
isHideHeader: boolean;
isHideBackground: boolean;
+6
View File
@@ -31,6 +31,7 @@ const initState = {
section: null,
readerMode: "double",
isConvertOpen: false,
isSpeechOpen: false,
isNavLocked: ConfigService.getReaderConfig("isNavLocked") === "yes",
isSettingLocked: ConfigService.getReaderConfig("isSettingLocked") === "yes",
isHideFooter: ConfigService.getReaderConfig("isHideFooter") === "yes",
@@ -75,6 +76,11 @@ export function reader(
...state,
isConvertOpen: action.payload,
};
case "HANDLE_SPEECH_DIALOG":
return {
...state,
isSpeechOpen: action.payload,
};
case "HANDLE_CURRENT_CHAPTER_INDEX":
return {
...state,