mirror of
https://github.com/koodo-reader/koodo-reader.git
synced 2026-10-07 13:22:17 -04:00
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:
1 parent
943d12d3e0
commit
168ff3e0b7
16 files changed
+179
-41
No files matched your search
Binary file not shown.
@@ -106,4 +106,5 @@
|
||||
<glyph unicode="" 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="" 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="" 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="" 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.
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -99,6 +99,7 @@ export type stateType = {
|
||||
isNavLocked: boolean;
|
||||
isSettingLocked: boolean;
|
||||
isConvertOpen: boolean;
|
||||
isSpeechOpen: boolean;
|
||||
isHideFooter: boolean;
|
||||
isHideHeader: boolean;
|
||||
isHideBackground: boolean;
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user