feat(export): add JSON format support for export functions and enhance export options

This commit is contained in:
troyeguo committed 2026-09-12 17:43:54 +08:00
1 parent 6e9fc1bc0a
commit d8db403e7f
5 files changed
+193 -99

No files matched your search

+46 -41
View File
@@ -35,27 +35,51 @@ class MoreAction extends React.Component<MoreActionProps, MoreActionState> {
this.state = { exportSubmenu: "" };
}
renderFormatSubmenu(type: "notes" | "highlights") {
renderFormatSubmenu(type: "notes" | "highlights" | "words") {
const isVisible = this.state.exportSubmenu === type;
const isNotes = type === "notes";
const filterFn = isNotes
? (note: any) =>
note.notes && note.notes.length > 0 && note.notes !== "annotation"
: (note: any) => note.notes === "";
const exportFn = isNotes ? exportNotes : exportHighlights;
const isWords = type === "words";
const filterFn = isWords
? () => true
: isNotes
? (note: any) =>
note.notes && note.notes.length > 0 && note.notes !== "annotation"
: (note: any) => note.notes === "";
const exportFn = (
isWords
? exportDictionaryHistory
: isNotes
? exportNotes
: exportHighlights
) as (
records: any[],
books: any[],
format: "csv" | "md" | "txt" | "html" | "pdf" | "json"
) => Promise<"success" | "failed" | "cancel">;
const formats = isWords
? (["csv", "json"] as const)
: (["csv", "md", "txt", "html", "pdf", "json"] as const);
const formatLabels: Record<string, string> = {
csv: "CSV",
md: "Markdown",
txt: "TXT",
html: "HTML",
pdf: "PDF",
json: "JSON",
};
const handleExport = async (
format: "csv" | "md" | "txt" | "html" | "pdf"
format: "csv" | "md" | "txt" | "html" | "pdf" | "json"
) => {
let books = await DatabaseService.getAllRecords("books");
let notes = (
let records = (
await DatabaseService.getRecordsByBookKey(
this.props.currentBook.key,
"notes"
isWords ? "words" : "notes"
)
).filter(filterFn);
if (notes.length > 0) {
const result = await exportFn(notes, books, format);
if (records.length > 0) {
const result = await exportFn(records, books, format);
if (result === "success") {
toast.success(this.props.t("Export successful"), { id: "exporting" });
} else if (result === "failed") {
@@ -93,7 +117,7 @@ class MoreAction extends React.Component<MoreActionProps, MoreActionState> {
195,
mainMenuPos.top + noteOffset * itemHeight,
120,
estimateMenuHeight(5)
estimateMenuHeight(isWords ? 2 : 6)
);
return {
position: "fixed",
@@ -114,24 +138,14 @@ class MoreAction extends React.Component<MoreActionProps, MoreActionState> {
}}
>
<div className="action-dialog-actions-container">
{(["csv", "md", "txt", "html", "pdf"] as const).map((fmt) => (
{formats.map((fmt) => (
<div
key={fmt}
className="action-dialog-edit"
style={{ paddingLeft: "0px" }}
onClick={() => handleExport(fmt)}
>
<p className="action-name">
{fmt === "csv"
? "CSV"
: fmt === "md"
? "Markdown"
: fmt === "txt"
? "TXT"
: fmt === "html"
? "HTML"
: "PDF"}
</p>
<p className="action-name">{formatLabels[fmt]}</p>
</div>
))}
</div>
@@ -271,26 +285,16 @@ class MoreAction extends React.Component<MoreActionProps, MoreActionState> {
<div
className="action-dialog-edit"
style={{ paddingLeft: "0px" }}
onClick={async () => {
let dictHistory = await DatabaseService.getRecordsByBookKey(
this.props.currentBook.key,
"words"
);
let books = await DatabaseService.getAllRecords("books");
if (dictHistory.length > 0) {
const result = await exportDictionaryHistory(dictHistory, books);
if (result === "success") {
toast.success(this.props.t("Export successful"), { id: "exporting" });
} else if (result === "failed") {
toast.error(this.props.t("Failed to export"), { id: "exporting" });
}
} else {
toast(this.props.t("Nothing to export"));
}
onMouseEnter={() => {
this.setState({ exportSubmenu: "words" });
}}
onMouseLeave={() => {
this.setState({ exportSubmenu: "" });
}}
>
<p className="action-name">
<p className="action-name export-action-name">
<Trans>Export dictionary history</Trans>
<span className="icon-dropdown icon-export-all"></span>
</p>
</div>
<div
@@ -434,6 +438,7 @@ class MoreAction extends React.Component<MoreActionProps, MoreActionState> {
</div>
{this.renderFormatSubmenu("notes")}
{this.renderFormatSubmenu("highlights")}
{this.renderFormatSubmenu("words")}
</>
);
}
+94 -45
View File
@@ -213,7 +213,7 @@ class SelectBook extends React.Component<BookListProps, BookListState> {
this.setState({ exportSubmenu: "", isShowExport: false });
}}
>
{(["csv", "md", "txt", "html", "pdf"] as const).map(
{(["csv", "md", "txt", "html", "pdf", "json"] as const).map(
(fmt) => (
<span
key={fmt}
@@ -250,15 +250,16 @@ class SelectBook extends React.Component<BookListProps, BookListState> {
});
}}
>
{fmt === "csv"
? "CSV"
: fmt === "md"
? "Markdown"
: fmt === "txt"
? "TXT"
: fmt === "html"
? "HTML"
: "PDF"}
{
({
csv: "CSV",
md: "Markdown",
txt: "TXT",
html: "HTML",
pdf: "PDF",
json: "JSON",
} as Record<string, string>)[fmt]
}
</span>
)
)}
@@ -297,7 +298,7 @@ class SelectBook extends React.Component<BookListProps, BookListState> {
this.setState({ exportSubmenu: "", isShowExport: false });
}}
>
{(["csv", "md", "txt", "html", "pdf"] as const).map(
{(["csv", "md", "txt", "html", "pdf", "json"] as const).map(
(fmt) => (
<span
key={fmt}
@@ -330,46 +331,94 @@ class SelectBook extends React.Component<BookListProps, BookListState> {
});
}}
>
{fmt === "csv"
? "CSV"
: fmt === "md"
? "Markdown"
: fmt === "txt"
? "TXT"
: fmt === "html"
? "HTML"
: "PDF"}
{
({
csv: "CSV",
md: "Markdown",
txt: "TXT",
html: "HTML",
pdf: "PDF",
json: "JSON",
} as Record<string, string>)[fmt]
}
</span>
)
)}
</div>
</div>
<span
className="book-manage-title select-book-action"
onClick={async () => {
let selectedBooks = await DatabaseService.getRecordsByKeys(
this.props.selectedBooks,
"books"
);
let dictHistory =
await DatabaseService.getRecordsByBookKeys(
this.props.selectedBooks,
"words"
);
if (dictHistory.length > 0) {
const result = await exportDictionaryHistory(dictHistory, selectedBooks);
if (result === "success") {
toast.success(this.props.t("Export successful"), { id: "exporting" });
} else if (result === "failed") {
toast.error(this.props.t("Failed to export"), { id: "exporting" });
}
} else {
toast(this.props.t("Nothing to export"));
<div style={{ position: "relative" }}>
<span
className="book-manage-title select-book-action"
onMouseEnter={() => {
this.setState({
exportSubmenu: "words",
isShowExport: true,
});
}}
onMouseLeave={() => {
this.setState({ exportSubmenu: "" });
}}
>
<Trans>Export dictionary history</Trans>
<span className="icon-dropdown icon-export-all"></span>
</span>
<div
className="select-more-actions select-export-format-submenu"
style={
this.state.exportSubmenu === "words"
? { left: "160px", bottom: "auto", top: "0px" }
: { display: "none" }
}
}}
>
<Trans>Export dictionary history</Trans>
</span>
onMouseEnter={() => {
this.setState({
exportSubmenu: "words",
isShowExport: true,
});
}}
onMouseLeave={() => {
this.setState({ exportSubmenu: "", isShowExport: false });
}}
>
{(["csv", "json"] as const).map((fmt) => (
<span
key={fmt}
className="book-manage-title select-book-action"
onClick={async () => {
let selectedBooks =
await DatabaseService.getRecordsByKeys(
this.props.selectedBooks,
"books"
);
let dictHistory =
await DatabaseService.getRecordsByBookKeys(
this.props.selectedBooks,
"words"
);
if (dictHistory.length > 0) {
const result = await exportDictionaryHistory(
dictHistory,
selectedBooks,
fmt
);
if (result === "success") {
toast.success(this.props.t("Export successful"), { id: "exporting" });
} else if (result === "failed") {
toast.error(this.props.t("Failed to export"), { id: "exporting" });
}
} else {
toast(this.props.t("Nothing to export"));
}
this.setState({
exportSubmenu: "",
isShowExport: false,
});
}}
>
{fmt === "csv" ? "CSV" : "JSON"}
</span>
))}
</div>
</div>
<span
className="book-manage-title select-book-action"
onClick={async () => {
@@ -45,6 +45,7 @@ class DataSetting extends React.Component<SettingInfoProps, SettingInfoState> {
snapshotList: [],
exportNotesFormat: "",
exportHighlightsFormat: "",
exportWordsFormat: "",
isEnableDiscordRPC:
ConfigService.getReaderConfig("isEnableDiscordRPC") === "yes",
isEnableKoReaderSync:
@@ -721,6 +722,7 @@ class DataSetting extends React.Component<SettingInfoProps, SettingInfoState> {
| "txt"
| "html"
| "pdf"
| "json"
| "";
if (!fmt) return;
this.setState({ exportNotesFormat: "" });
@@ -762,6 +764,9 @@ class DataSetting extends React.Component<SettingInfoProps, SettingInfoState> {
<option value="pdf" className="lang-setting-option">
PDF
</option>
<option value="json" className="lang-setting-option">
JSON
</option>
</select>
</div>
<div className="setting-dialog-new-title">
@@ -776,6 +781,7 @@ class DataSetting extends React.Component<SettingInfoProps, SettingInfoState> {
| "txt"
| "html"
| "pdf"
| "json"
| "";
if (!fmt) return;
this.setState({ exportHighlightsFormat: "" });
@@ -812,17 +818,24 @@ class DataSetting extends React.Component<SettingInfoProps, SettingInfoState> {
<option value="pdf" className="lang-setting-option">
PDF
</option>
<option value="json" className="lang-setting-option">
JSON
</option>
</select>
</div>
<div className="setting-dialog-new-title">
<Trans>Export all dictionary history</Trans>
<span
className="change-location-button"
onClick={async () => {
<select
className="lang-setting-dropdown"
value={this.state.exportWordsFormat}
onChange={async (event) => {
const fmt = event.target.value as "csv" | "json" | "";
if (!fmt) return;
this.setState({ exportWordsFormat: "" });
let dictHistory = await DatabaseService.getAllRecords("words");
let books = await DatabaseService.getAllRecords("books");
if (dictHistory.length > 0) {
const result = await exportDictionaryHistory(dictHistory, books);
const result = await exportDictionaryHistory(dictHistory, books, fmt);
if (result === "success") {
toast.success(this.props.t("Export successful"), { id: "exporting" });
} else if (result === "failed") {
@@ -833,8 +846,16 @@ class DataSetting extends React.Component<SettingInfoProps, SettingInfoState> {
}
}}
>
<Trans>Export</Trans>
</span>
<option value="" className="lang-setting-option">
{this.props.t("Select format")}
</option>
<option value="csv" className="lang-setting-option">
CSV
</option>
<option value="json" className="lang-setting-option">
JSON
</option>
</select>
</div>
<div className="setting-dialog-new-title">
<Trans>Clear all data</Trans>
@@ -9,6 +9,7 @@ export interface SettingInfoState {
snapshotList: { file: string; time: number }[];
exportNotesFormat: string;
exportHighlightsFormat: string;
exportWordsFormat: string;
isEnableDiscordRPC: boolean;
isEnableKoReaderSync: boolean;
isEnableNotionSync: boolean;
+25 -7
View File
@@ -27,6 +27,8 @@ let year = new Date().getFullYear(),
export type ExportResult = "success" | "failed" | "cancel";
export type ExportFormat = "csv" | "md" | "txt" | "html" | "pdf" | "json";
export const exportBooks = async (
books: Book[]
): Promise<ExportResult> => {
@@ -135,6 +137,7 @@ const toBlob = (content: string, format: string): Blob => {
txt: "text/plain,charset=UTF-8",
html: "text/html,charset=UTF-8",
csv: "text/csv,charset=UTF-8",
json: "application/json,charset=UTF-8",
};
return new Blob([content], { type: mimeMap[format] || "text/plain" });
};
@@ -157,18 +160,19 @@ const sanitizeFileName = (name: string): string =>
// 根据 format 将 data 转为文本内容
const convertNotesData = (
data: any[],
format: "csv" | "md" | "txt" | "html"
format: "csv" | "md" | "txt" | "html" | "json"
): string => {
if (format === "md") return convertNotesToMarkdown(data);
if (format === "txt") return convertNotesToTxt(data);
if (format === "html") return convertNotesToHTML(data);
if (format === "json") return JSON.stringify(data, null, 2);
return convertArrayToCSV(data);
};
export const exportNotes = async (
notes: Note[],
books: Book[],
format: "csv" | "md" | "txt" | "html" | "pdf" = "csv"
format: ExportFormat = "csv"
): Promise<ExportResult> => {
let data = notes.map((item) => {
let book = books.filter((subitem) => subitem.key === item.bookKey)[0];
@@ -272,6 +276,11 @@ export const exportNotes = async (
convertNotesToHTML(data),
`KoodoReader-Note-${fileDate}.pdf`
);
} else if (format === "json") {
saveAs(
toBlob(JSON.stringify(data, null, 2), "json"),
`KoodoReader-Note-${fileDate}.json`
);
} else {
saveAs(
toBlob(convertArrayToCSV(data), "csv"),
@@ -288,18 +297,19 @@ export const exportNotes = async (
// 根据 format 将 data 转为文本内容
const convertHighlightsData = (
data: any[],
format: "csv" | "md" | "txt" | "html"
format: "csv" | "md" | "txt" | "html" | "json"
): string => {
if (format === "md") return convertHighlightsToMarkdown(data);
if (format === "txt") return convertHighlightsToTxt(data);
if (format === "html") return convertHighlightsToHTML(data);
if (format === "json") return JSON.stringify(data, null, 2);
return convertArrayToCSV(data);
};
export const exportHighlights = async (
highlights: Note[],
books: Book[],
format: "csv" | "md" | "txt" | "html" | "pdf" = "csv"
format: ExportFormat = "csv"
): Promise<ExportResult> => {
let data = highlights.map((item) => {
let book = books.filter((subitem) => subitem.key === item.bookKey)[0];
@@ -406,6 +416,11 @@ export const exportHighlights = async (
convertHighlightsToHTML(data),
`KoodoReader-Highlight-${fileDate}.pdf`
);
} else if (format === "json") {
saveAs(
toBlob(JSON.stringify(data, null, 2), "json"),
`KoodoReader-Highlight-${fileDate}.json`
);
} else {
saveAs(
toBlob(convertArrayToCSV(data), "csv"),
@@ -420,7 +435,8 @@ export const exportHighlights = async (
};
export const exportDictionaryHistory = async (
dictHistory: DictHistory[],
books: Book[]
books: Book[],
format: "csv" | "json" = "csv"
): Promise<ExportResult> => {
let data = dictHistory.map((item) => {
let book = books.filter((subitem) => subitem.key === item.bookKey)[0];
@@ -440,11 +456,13 @@ export const exportDictionaryHistory = async (
try {
saveAs(
new Blob([convertArrayToCSV(data)], { type: "text/csv,charset=UTF-8" }),
format === "json"
? toBlob(JSON.stringify(data, null, 2), "json")
: new Blob([convertArrayToCSV(data)], { type: "text/csv,charset=UTF-8" }),
"KoodoReader-Dictionary-History-" +
`${year}-${month <= 9 ? "0" + month : month}-${
day <= 9 ? "0" + day : day
}.csv`
}.${format}`
);
return "success";
} catch (error) {