mirror of
https://github.com/koodo-reader/koodo-reader.git
synced 2026-10-07 05:11:52 -04:00
feat: add annotation feature with customizable brush color and width
- Added new translation keys for "Annotation", "Brush color", and "Brush width" in English and Chinese locale files. - Implemented AnnotationDialog component for users to select brush color and width for annotations. - Integrated AnnotationDialog into the Reader component, allowing users to toggle its visibility. - Updated Redux store to manage the state of the annotation dialog and its visibility. - Created styles for the annotation dialog to enhance user experience.
This commit is contained in:
1 parent
dba683d03c
commit
36b8bf4fbc
14 files changed
+302
-10
No files matched your search
Vendored
+1
-1
File diff suppressed because one or more lines are too long.
@@ -1233,5 +1233,8 @@
|
||||
"Search highlight style": "Search highlight style",
|
||||
"Customize the highlight style when searching in books": "Customize the highlight style when searching in books",
|
||||
"Theme": "Theme",
|
||||
"You are clicking too fast, please try again later": "You are clicking too fast, please try again later"
|
||||
"You are clicking too fast, please try again later": "You are clicking too fast, please try again later",
|
||||
"Annotation": "Annotation",
|
||||
"Brush color": "Brush color",
|
||||
"Brush width": "Brush width"
|
||||
}
|
||||
@@ -712,6 +712,9 @@
|
||||
"GitHub repository": "GitHub 仓库",
|
||||
"Speak the text": "朗读文字",
|
||||
"Search on the Internet": "网上搜索",
|
||||
"Annotation": "批注",
|
||||
"Brush color": "笔画颜色",
|
||||
"Brush width": "笔画粗细",
|
||||
"Search in the Book": "全书搜索",
|
||||
"Change storage location": "更改存储位置",
|
||||
"Change location": "更改",
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
.annotation-dialog-container {
|
||||
padding: 12px 16px 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.annotation-dialog-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.annotation-dialog-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.annotation-dialog-close {
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
opacity: 0.6;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
.annotation-dialog-close:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
.annotation-dialog-section {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.annotation-dialog-section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.annotation-dialog-label {
|
||||
font-size: 13px;
|
||||
margin-bottom: 8px;
|
||||
opacity: 0.75;
|
||||
}
|
||||
.annotation-color-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
.annotation-color-item {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
border: 2px solid transparent;
|
||||
transition: border-color 0.15s, transform 0.15s;
|
||||
}
|
||||
.annotation-color-item:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
.active-annotation-color-item {
|
||||
border-color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
.annotation-width-list {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
.annotation-width-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
border: 2px solid transparent;
|
||||
transition: border-color 0.15s, background-color 0.15s;
|
||||
}
|
||||
.annotation-width-item:hover {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
.active-annotation-width-item {
|
||||
border-color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
.annotation-width-preview {
|
||||
display: block;
|
||||
width: 26px;
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import React from "react";
|
||||
import { AnnotationDialogProps, AnnotationDialogState } from "./interface";
|
||||
import "./annotationDialog.css";
|
||||
import { ConfigService } from "../../../assets/lib/kookit-extra-browser.min";
|
||||
|
||||
const BRUSH_COLORS = [
|
||||
"#E53131",
|
||||
"#F08C00",
|
||||
"#F5C400",
|
||||
"#1FA861",
|
||||
"#1B8CF0",
|
||||
"#6C5CE7",
|
||||
"#E54394",
|
||||
"#2C2C2C",
|
||||
];
|
||||
|
||||
const BRUSH_WIDTHS = [2, 4, 8, 14];
|
||||
|
||||
class AnnotationDialog extends React.Component<
|
||||
AnnotationDialogProps,
|
||||
AnnotationDialogState
|
||||
> {
|
||||
constructor(props: AnnotationDialogProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
brushColor: ConfigService.getReaderConfig("brushColor") || BRUSH_COLORS[0],
|
||||
brushWidth: parseFloat(
|
||||
ConfigService.getReaderConfig("brushWidth") || BRUSH_WIDTHS[1] + ""
|
||||
),
|
||||
};
|
||||
}
|
||||
handleSelectColor = (color: string) => {
|
||||
this.setState({ brushColor: color });
|
||||
ConfigService.setReaderConfig("brushColor", color);
|
||||
};
|
||||
handleSelectWidth = (width: number) => {
|
||||
this.setState({ brushWidth: width });
|
||||
ConfigService.setReaderConfig("brushWidth", width + "");
|
||||
};
|
||||
handleClose = () => {
|
||||
this.props.handleAnnotationDialog(false);
|
||||
};
|
||||
render() {
|
||||
const { brushColor, brushWidth } = this.state;
|
||||
return (
|
||||
<div
|
||||
className="sort-dialog-container annotation-dialog-container"
|
||||
style={{
|
||||
left: "auto",
|
||||
top: "auto",
|
||||
bottom: "60px",
|
||||
width: "300px",
|
||||
right: this.props.isSettingLocked ? 370 : 65,
|
||||
}}
|
||||
>
|
||||
<div className="annotation-dialog-header">
|
||||
<span className="annotation-dialog-title">
|
||||
{this.props.t("Annotation")}
|
||||
</span>
|
||||
<span
|
||||
className="icon-close annotation-dialog-close"
|
||||
onClick={this.handleClose}
|
||||
></span>
|
||||
</div>
|
||||
|
||||
<div className="annotation-dialog-section">
|
||||
<div className="annotation-dialog-label">
|
||||
{this.props.t("Brush color")}
|
||||
</div>
|
||||
<ul className="annotation-color-list">
|
||||
{BRUSH_COLORS.map((color) => (
|
||||
<li
|
||||
key={color}
|
||||
className={
|
||||
brushColor === color
|
||||
? "annotation-color-item active-annotation-color-item"
|
||||
: "annotation-color-item"
|
||||
}
|
||||
style={{ backgroundColor: color }}
|
||||
onClick={() => this.handleSelectColor(color)}
|
||||
></li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="annotation-dialog-section">
|
||||
<div className="annotation-dialog-label">
|
||||
{this.props.t("Brush width")}
|
||||
</div>
|
||||
<ul className="annotation-width-list">
|
||||
{BRUSH_WIDTHS.map((width) => (
|
||||
<li
|
||||
key={width}
|
||||
className={
|
||||
brushWidth === width
|
||||
? "annotation-width-item active-annotation-width-item"
|
||||
: "annotation-width-item"
|
||||
}
|
||||
onClick={() => this.handleSelectWidth(width)}
|
||||
>
|
||||
<span
|
||||
className="annotation-width-preview"
|
||||
style={{
|
||||
backgroundColor: brushColor,
|
||||
height: width + "px",
|
||||
borderRadius: (width / 2) + "px",
|
||||
}}
|
||||
></span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default AnnotationDialog;
|
||||
@@ -0,0 +1,19 @@
|
||||
import { connect } from "react-redux";
|
||||
import { handleAnnotationDialog } from "../../../store/actions";
|
||||
import { stateType } from "../../../store";
|
||||
import AnnotationDialog from "./component";
|
||||
import { withTranslation } from "react-i18next";
|
||||
|
||||
const mapStateToProps = (state: stateType) => {
|
||||
return {
|
||||
isAnnotationOpen: state.reader.isAnnotationOpen,
|
||||
isSettingLocked: state.reader.isSettingLocked,
|
||||
};
|
||||
};
|
||||
const actionCreator = {
|
||||
handleAnnotationDialog,
|
||||
};
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
actionCreator
|
||||
)(withTranslation()(AnnotationDialog as any) as any);
|
||||
@@ -0,0 +1,11 @@
|
||||
export interface AnnotationDialogProps {
|
||||
isAnnotationOpen: boolean;
|
||||
isSettingLocked: boolean;
|
||||
handleAnnotationDialog: (isAnnotationOpen: boolean) => void;
|
||||
t: (title: string) => string;
|
||||
}
|
||||
|
||||
export interface AnnotationDialogState {
|
||||
brushColor: string;
|
||||
brushWidth: number;
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import PdfCropDialog from "../../components/dialogs/pdfCropDialog";
|
||||
import { isElectron } from "react-device-detect";
|
||||
import SettingDialog from "../../components/dialogs/settingDialog";
|
||||
import SpeechDialog from "../../components/dialogs/speechDialog";
|
||||
import AnnotationDialog from "../../components/dialogs/annotationDialog";
|
||||
import PopupOptionDialog from "../../components/dialogs/popupOptionDialog";
|
||||
import {
|
||||
updateDiscordPresence,
|
||||
@@ -432,6 +433,24 @@ class Reader extends React.Component<ReaderProps, ReaderState> {
|
||||
AI
|
||||
</div>
|
||||
)}
|
||||
{!this.props.isHideAnnotationButton && (
|
||||
<div
|
||||
className="next-chapter-single-container"
|
||||
onClick={() => {
|
||||
this.props.handleAnnotationDialog(
|
||||
!this.props.isAnnotationOpen
|
||||
);
|
||||
}}
|
||||
style={{ position: "static", transform: "rotate(0deg)" }}
|
||||
>
|
||||
<span
|
||||
style={
|
||||
this.props.isAnnotationOpen ? { fontWeight: "bold" } : {}
|
||||
}
|
||||
className={`icon-${this.props.isAnnotationOpen ? "close" : "edit"} next-chapter-single`}
|
||||
></span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -812,6 +831,7 @@ class Reader extends React.Component<ReaderProps, ReaderState> {
|
||||
<SpeechDialog />
|
||||
</div>
|
||||
}
|
||||
{this.props.isAnnotationOpen && <AnnotationDialog />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -61,10 +61,10 @@
|
||||
}
|
||||
.left-panel {
|
||||
width: 40px;
|
||||
height: 60%;
|
||||
height: 40%;
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: calc(50vh - 30%);
|
||||
top: calc(50vh - 20%);
|
||||
background-color: rgba(75, 75, 75, 0.3);
|
||||
border-bottom-right-radius: 20px;
|
||||
border-top-right-radius: 20px;
|
||||
@@ -77,10 +77,10 @@
|
||||
}
|
||||
.right-panel {
|
||||
width: 40px;
|
||||
height: 60%;
|
||||
height: 40%;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top: calc(50vh - 30%);
|
||||
top: calc(50vh - 20%);
|
||||
background-color: rgba(75, 75, 75, 0.3);
|
||||
border-bottom-left-radius: 20px;
|
||||
border-top-left-radius: 20px;
|
||||
@@ -92,10 +92,10 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
.top-panel {
|
||||
width: 60%;
|
||||
width: 40%;
|
||||
height: 40px;
|
||||
position: absolute;
|
||||
left: calc(50vw - 30%);
|
||||
left: calc(50vw - 20%);
|
||||
top: 0px;
|
||||
background-color: rgba(75, 75, 75, 0.3);
|
||||
border-bottom-left-radius: 20px;
|
||||
@@ -108,10 +108,10 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
.bottom-panel {
|
||||
width: 60%;
|
||||
width: 40%;
|
||||
height: 40px;
|
||||
position: absolute;
|
||||
left: calc(50vw - 30%);
|
||||
left: calc(50vw - 20%);
|
||||
bottom: 0px;
|
||||
background-color: rgba(75, 75, 75, 0.3);
|
||||
border-top-left-radius: 20px;
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
handleConvertDialog,
|
||||
handlePdfCropDialog,
|
||||
handleSpeechDialog,
|
||||
handleAnnotationDialog,
|
||||
handleScale,
|
||||
handleFetchAuthed,
|
||||
handleFetchUserInfo,
|
||||
@@ -30,6 +31,7 @@ const mapStateToProps = (state: stateType) => {
|
||||
isConvertOpen: state.reader.isConvertOpen,
|
||||
isPdfCropOpen: state.reader.isPdfCropOpen,
|
||||
isSpeechOpen: state.reader.isSpeechOpen,
|
||||
isAnnotationOpen: state.reader.isAnnotationOpen,
|
||||
isOpenPopupOptionDialog: state.backupPage.isOpenPopupOptionDialog,
|
||||
isSettingLocked: state.reader.isSettingLocked,
|
||||
isAuthed: state.manager.isAuthed,
|
||||
@@ -40,6 +42,7 @@ const mapStateToProps = (state: stateType) => {
|
||||
isHidePageButton: state.reader.isHidePageButton,
|
||||
isHideMenuButton: state.reader.isHideMenuButton,
|
||||
isHideAudiobookButton: state.reader.isHideAudiobookButton,
|
||||
isHideAnnotationButton: state.reader.isHideAnnotationButton,
|
||||
isHideAIButton: state.reader.isHideAIButton,
|
||||
isHidePDFConvertButton: state.reader.isHidePDFConvertButton,
|
||||
isHideScaleButton: state.reader.isHideScaleButton,
|
||||
@@ -60,6 +63,7 @@ const actionCreator = {
|
||||
handleScale,
|
||||
handleFetchAuthed,
|
||||
handleSpeechDialog,
|
||||
handleAnnotationDialog,
|
||||
handleFetchUserInfo,
|
||||
};
|
||||
export default connect(
|
||||
|
||||
@@ -10,6 +10,7 @@ export interface ReaderProps {
|
||||
isConvertOpen: boolean;
|
||||
isPdfCropOpen: boolean;
|
||||
isSpeechOpen: boolean;
|
||||
isAnnotationOpen: boolean;
|
||||
isOpenPopupOptionDialog: boolean;
|
||||
isSearch: boolean;
|
||||
isAuthed: boolean;
|
||||
@@ -17,6 +18,7 @@ export interface ReaderProps {
|
||||
isSettingOpen: boolean;
|
||||
isHideMenuButton: boolean;
|
||||
isHideAudiobookButton: boolean;
|
||||
isHideAnnotationButton: boolean;
|
||||
isHideAIButton: boolean;
|
||||
isHidePDFConvertButton: boolean;
|
||||
isHideScaleButton: boolean;
|
||||
@@ -27,6 +29,7 @@ export interface ReaderProps {
|
||||
handleConvertDialog: (isConvertOpen: boolean) => void;
|
||||
handlePdfCropDialog: (isPdfCropOpen: boolean) => void;
|
||||
handleSpeechDialog: (isSpeechOpen: boolean) => void;
|
||||
handleAnnotationDialog: (isAnnotationOpen: boolean) => void;
|
||||
handleMenuMode: (menuMode: string) => void;
|
||||
handleOriginalText: (originalText: string) => void;
|
||||
handleFetchBooks: () => void;
|
||||
|
||||
@@ -35,6 +35,9 @@ export function handlePdfCropDialog(isPdfCropOpen: boolean) {
|
||||
export function handleSpeechDialog(isSpeechOpen: boolean) {
|
||||
return { type: "HANDLE_SPEECH_DIALOG", payload: isSpeechOpen };
|
||||
}
|
||||
export function handleAnnotationDialog(isAnnotationOpen: boolean) {
|
||||
return { type: "HANDLE_ANNOTATION_DIALOG", payload: isAnnotationOpen };
|
||||
}
|
||||
export function handleSpeechStartText(speechStartText: string) {
|
||||
return { type: "HANDLE_SPEECH_START_TEXT", payload: speechStartText };
|
||||
}
|
||||
@@ -107,6 +110,12 @@ export function handleHideAudiobookButton(isHideAudiobookButton: boolean) {
|
||||
payload: isHideAudiobookButton,
|
||||
};
|
||||
}
|
||||
export function handleHideAnnotationButton(isHideAnnotationButton: boolean) {
|
||||
return {
|
||||
type: "HANDLE_HIDE_ANNOTATION_BUTTON",
|
||||
payload: isHideAnnotationButton,
|
||||
};
|
||||
}
|
||||
export function handleHideAIButton(isHideAIButton: boolean) {
|
||||
return { type: "HANDLE_HIDE_AI_BUTTON", payload: isHideAIButton };
|
||||
}
|
||||
|
||||
@@ -107,12 +107,14 @@ export type stateType = {
|
||||
isConvertOpen: boolean;
|
||||
isPdfCropOpen: boolean;
|
||||
isSpeechOpen: boolean;
|
||||
isAnnotationOpen: boolean;
|
||||
isHideFooter: boolean;
|
||||
isHideHeader: boolean;
|
||||
isHideBackground: boolean;
|
||||
isHidePageButton: boolean;
|
||||
isHideMenuButton: boolean;
|
||||
isHideAudiobookButton: boolean;
|
||||
isHideAnnotationButton: boolean;
|
||||
isHideAIButton: boolean;
|
||||
isHideScaleButton: boolean;
|
||||
isHidePDFConvertButton: boolean;
|
||||
|
||||
@@ -30,6 +30,7 @@ const initState = {
|
||||
isConvertOpen: false,
|
||||
isPdfCropOpen: false,
|
||||
isSpeechOpen: false,
|
||||
isAnnotationOpen: false,
|
||||
speechStartText: "",
|
||||
isSpeechAutoStart: false,
|
||||
isNavLocked: ConfigService.getReaderConfig("isNavLocked") === "yes",
|
||||
@@ -41,6 +42,8 @@ const initState = {
|
||||
isHideMenuButton: ConfigService.getReaderConfig("isHideMenuButton") === "yes",
|
||||
isHideAudiobookButton:
|
||||
ConfigService.getReaderConfig("isHideAudiobookButton") === "yes",
|
||||
isHideAnnotationButton:
|
||||
ConfigService.getReaderConfig("isHideAnnotationButton") === "yes",
|
||||
isHideAIButton: ConfigService.getReaderConfig("isHideAIButton") === "yes",
|
||||
isHideScaleButton:
|
||||
ConfigService.getReaderConfig("isHideScaleButton") === "yes",
|
||||
@@ -93,6 +96,11 @@ export function reader(
|
||||
...state,
|
||||
isSpeechOpen: action.payload,
|
||||
};
|
||||
case "HANDLE_ANNOTATION_DIALOG":
|
||||
return {
|
||||
...state,
|
||||
isAnnotationOpen: action.payload,
|
||||
};
|
||||
case "HANDLE_SPEECH_START_TEXT":
|
||||
return {
|
||||
...state,
|
||||
@@ -163,6 +171,11 @@ export function reader(
|
||||
...state,
|
||||
isHideAudiobookButton: action.payload,
|
||||
};
|
||||
case "HANDLE_HIDE_ANNOTATION_BUTTON":
|
||||
return {
|
||||
...state,
|
||||
isHideAnnotationButton: action.payload,
|
||||
};
|
||||
case "HANDLE_HIDE_AI_BUTTON":
|
||||
return {
|
||||
...state,
|
||||
|
||||
Reference in new issue
Block a user