mirror of
https://github.com/koodo-reader/koodo-reader.git
synced 2026-09-14 15:00:50 -04:00
feat(export): add JSON format support for export functions and enhance export options
This commit is contained in:
1 parent
6e9fc1bc0a
commit
d8db403e7f
5 files changed
+193
-99
No files matched your search
@@ -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")}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user