From 168ff3e0b73ccd4465b628ae00751cd41bf52577 Mon Sep 17 00:00:00 2001 From: troyeguo <13820674+troyeguo@users.noreply.github.com> Date: Fri, 13 Mar 2026 16:05:13 +0800 Subject: [PATCH] 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. --- src/assets/styles/fonts/icomoon.eot | Bin 28168 -> 28368 bytes src/assets/styles/fonts/icomoon.svg | 1 + src/assets/styles/fonts/icomoon.ttf | Bin 28004 -> 28204 bytes src/assets/styles/fonts/icomoon.woff | Bin 28080 -> 28280 bytes src/assets/styles/style.css | 13 ++- .../dialogs/speechDialog/component.tsx | 45 +++++++++ src/components/dialogs/speechDialog/index.tsx | 27 ++++++ .../dialogs/speechDialog/interface.tsx | 18 ++++ .../dialogs/speechDialog/speechDialog.css | 5 + src/pages/reader/component.tsx | 87 +++++++++++------- src/pages/reader/index.css | 9 +- src/pages/reader/index.tsx | 3 + src/pages/reader/interface.tsx | 2 + src/store/actions/reader.tsx | 3 + src/store/index.tsx | 1 + src/store/reducers/reader.tsx | 6 ++ 16 files changed, 179 insertions(+), 41 deletions(-) create mode 100644 src/components/dialogs/speechDialog/component.tsx create mode 100644 src/components/dialogs/speechDialog/index.tsx create mode 100644 src/components/dialogs/speechDialog/interface.tsx create mode 100644 src/components/dialogs/speechDialog/speechDialog.css diff --git a/src/assets/styles/fonts/icomoon.eot b/src/assets/styles/fonts/icomoon.eot index 6a5f547fabe73576280f727fbe7a72c21cf311af..533e216be09cd33250944af4a99a2cd46937b235 100644 GIT binary patch delta 454 zcmeCU!+7B?Bin^M1_qtHiEL&p+!8iXlLK=^CWaIhBJ-a&D;OBE zCjbR6_uF+Y0iFOBk3z`tx`|@(j!@jDnL> z80{I8Cof_2W=!7vfw3pO{{I2y)yxMNyntGi*!dXQ*<=|-#H<-bMZ}Da%uEeUP1KZ? z*hGP}xgDdqnX!?$9HY3HsG%rS9Ig(kT3wG(T}_#ZA;QnEL{UjnNZZ@g)Ek7Qq+{*u z+}-W$Vx^>EV%kEIiAstketr?mSs>asa+Q#zHc0(y5D^MeEe!-9ouP1HZAqb3k-i|~ zfo=x+42Yv_B)H@GZN4&avw%Fz@bqThO&C3SPNq3y#^x)Ta~PRe7$)asUlK9l14@Fl XW-u;b5CAD;V4D0fS9o)B&QeAI!2xg$ delta 236 zcmca`m$BmxBO6B^14BygL^d;)`#Fc>Cpxq7|b$&0=$R0)EO8ud4PPCjNFonS&x7$hD-yXo;f-B$%*IRy;;G)kaY$q zcp*2jqJW`-aS;PU_8cHzAulmEb*2~RZylLesKu{Gf01~3rL=UnT1hkatfn8 zW76a$jNXh%n?Er2q$@BmGBCs+&bc4YZ}XLbn+2qv;i=W9Cop<)OqMxg`sOKFa~LO^ R + \ No newline at end of file diff --git a/src/assets/styles/fonts/icomoon.ttf b/src/assets/styles/fonts/icomoon.ttf index 16878248d119386fe3862baf16aa75e7991b0110..6954fb8c6d87a01cd6604e0352d958ffddf77935 100644 GIT binary patch delta 456 zcmaEIi*d~z#tF*xuNW8@Wf&M3Ledk93xKo$kPo6c(sL@)d?FHyfcy#uh76gE)Wj5% zOG;4;44G4a@@5%80p7!08bJOTAYUaTx1?g;BOr?*^8--Nf}H&1MCLzlRxmJRPXG#D z%1x{&V5nqV#K4e~0^}>?CFZ8i^pfNJ4HRerYG^CSFD_wV1{sjY15&`i%py4PfIVaK zBt~z>eJ?ZuT4=}H0KEU7wRIkL&$H>km%P1md%_u4&W^80;YG`VrrmVy!3Z%{L z7|qR$jl|^`#l=JoMWNzwbx_sndW`C7%1jIqetsp2N|Hj_-lnGBAS@*vYiH-~Zf6%O zB@GkP7LrUQ{q^P>^bAAOPtMg$rv-3ayIt1sM-?GtgT= z9AzWH9nWv`m4TZDz6m1@bEx7}8@hQWH~* zr)mM^GOhsS%`$)jyob5e85lBofP9sV+>(k}kAN(OOaq{vIXU^siRa(FS;4@Nbp|MS zAvdw2fT4nM5d%Z^93WpIFEKZDrk5P&Z=k>)poX@B{NfS@W{?57E+7RA%q&6^57;v% zO=9$BOxomrUJipCX25uIRVuq(ypPstCfO78fuuFzx{gaX_&^dQN2;Q0xc; zLxv0p`$QxbWuzviFfe330ID$qVUtTrQ5ir%28JvNAYTQ9c@J}GWaO4q0L5&8`WJxk zyhjWSIr+&zb=h}-YA%5=^Pe{>auX|n_FCm^017C8aV6uTyu{p8pz;eqjcp)2(~I+W zL4I)w(A5!eb<98(GmGG49!7h{b1H;psc{gG7WS2~H#*EE1 cnR6JKSQsXYWnbbl;R8y7v}SC6mwkv404w=$S^xk5 delta 314 zcmexyhjGJgMzL~#H#Y`G1|Z1Yz`zZrQy3U0C+3Mw)KRa0nVwi&z`($`2PnY-#RBO$ zm1#h+BMc1bF(AzJU=wRbYGMilL#6^yjTs0VPt_{O015)dB7l4q5avD1rJj*nQUMgJ z0qUOv!m}PRFy!PX1Jz|q0M%Ro;q&j_tjJBQ0NQJn{Q@YU0LB%Ji}Dh4Q-R8X4q=XH1&x!|2VJw0Q<&Pr3rgm+^;l?#J`nd}ZKf z0V-o)cxv_O35=f1l4Z`AzS$;g4&&rI*%t)Bp_0zHfI$GPlnEFn!kaJWEM)`$TgOfl diff --git a/src/assets/styles/style.css b/src/assets/styles/style.css index 74a75489..4e85f129 100644 --- a/src/assets/styles/style.css +++ b/src/assets/styles/style.css @@ -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"; } diff --git a/src/components/dialogs/speechDialog/component.tsx b/src/components/dialogs/speechDialog/component.tsx new file mode 100644 index 00000000..5646b3ae --- /dev/null +++ b/src/components/dialogs/speechDialog/component.tsx @@ -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 ( + <> +
{ + this.props.handleSpeechDialog(true); + }} + style={{ + right: "65px", + left: "auto", + top: "auto", + bottom: "60px", + width: "300px", + height: "400px", + }} + > + {this.props.plugins && } +
+ + ); + } +} + +export default SpeechDialog; diff --git a/src/components/dialogs/speechDialog/index.tsx b/src/components/dialogs/speechDialog/index.tsx new file mode 100644 index 00000000..82483c7b --- /dev/null +++ b/src/components/dialogs/speechDialog/index.tsx @@ -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); diff --git a/src/components/dialogs/speechDialog/interface.tsx b/src/components/dialogs/speechDialog/interface.tsx new file mode 100644 index 00000000..807dee15 --- /dev/null +++ b/src/components/dialogs/speechDialog/interface.tsx @@ -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; +} diff --git a/src/components/dialogs/speechDialog/speechDialog.css b/src/components/dialogs/speechDialog/speechDialog.css new file mode 100644 index 00000000..061fa596 --- /dev/null +++ b/src/components/dialogs/speechDialog/speechDialog.css @@ -0,0 +1,5 @@ +.icon-export-all { + line-height: 18px; + display: inline-block; + transform: rotate(-90deg) scale(0.7); +} diff --git a/src/pages/reader/component.tsx b/src/pages/reader/component.tsx index 0687a992..4c6713c8 100644 --- a/src/pages/reader/component.tsx +++ b/src/pages/reader/component.tsx @@ -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 {
{ - 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, }} > - +
{ + if (lock) return; + lock = true; + await this.props.htmlBook.rendition.next(); + this.handleLocation(); + setTimeout(() => (lock = false), throttleTime); + }} + style={{ position: "static" }} + > + +
+
{ + this.props.handleSpeechDialog(!this.props.isSpeechOpen); + }} + style={{ position: "static", transform: "rotate(0deg)" }} + > + +
+ {this.props.isAuthed && + !this.props.isHideAIButton && + ConfigService.getReaderConfig("isDisableAI") !== "yes" && ( +
{ + 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 +
+ )}
- {this.props.isAuthed && - !this.props.isHideAIButton && - ConfigService.getReaderConfig("isDisableAI") !== "yes" && ( -
{ - 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 -
- )} )}
{ {this.props.currentBook.key && } {this.props.isConvertOpen && } + {this.props.isSpeechOpen && }
); } diff --git a/src/pages/reader/index.css b/src/pages/reader/index.css index a628acd6..b5978434 100644 --- a/src/pages/reader/index.css +++ b/src/pages/reader/index.css @@ -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; diff --git a/src/pages/reader/index.tsx b/src/pages/reader/index.tsx index 199083b9..adcea663 100644 --- a/src/pages/reader/index.tsx +++ b/src/pages/reader/index.tsx @@ -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, diff --git a/src/pages/reader/interface.tsx b/src/pages/reader/interface.tsx index d741ac86..f803f26e 100644 --- a/src/pages/reader/interface.tsx +++ b/src/pages/reader/interface.tsx @@ -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; diff --git a/src/store/actions/reader.tsx b/src/store/actions/reader.tsx index 52495392..7c7e0c77 100644 --- a/src/store/actions/reader.tsx +++ b/src/store/actions/reader.tsx @@ -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 }; } diff --git a/src/store/index.tsx b/src/store/index.tsx index 504e7710..39847886 100644 --- a/src/store/index.tsx +++ b/src/store/index.tsx @@ -99,6 +99,7 @@ export type stateType = { isNavLocked: boolean; isSettingLocked: boolean; isConvertOpen: boolean; + isSpeechOpen: boolean; isHideFooter: boolean; isHideHeader: boolean; isHideBackground: boolean; diff --git a/src/store/reducers/reader.tsx b/src/store/reducers/reader.tsx index d0f1e164..b1252d2d 100644 --- a/src/store/reducers/reader.tsx +++ b/src/store/reducers/reader.tsx @@ -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,