diff --git a/src/components/dialogs/moreAction/component.tsx b/src/components/dialogs/moreAction/component.tsx index 08a505c0..921b5fad 100644 --- a/src/components/dialogs/moreAction/component.tsx +++ b/src/components/dialogs/moreAction/component.tsx @@ -35,27 +35,51 @@ class MoreAction extends React.Component { 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 = { + 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 { 195, mainMenuPos.top + noteOffset * itemHeight, 120, - estimateMenuHeight(5) + estimateMenuHeight(isWords ? 2 : 6) ); return { position: "fixed", @@ -114,24 +138,14 @@ class MoreAction extends React.Component { }} >
- {(["csv", "md", "txt", "html", "pdf"] as const).map((fmt) => ( + {formats.map((fmt) => (
handleExport(fmt)} > -

- {fmt === "csv" - ? "CSV" - : fmt === "md" - ? "Markdown" - : fmt === "txt" - ? "TXT" - : fmt === "html" - ? "HTML" - : "PDF"} -

+

{formatLabels[fmt]}

))}
@@ -271,26 +285,16 @@ class MoreAction extends React.Component {
{ - 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: "" }); }} > -

+

Export dictionary history +

{
{this.renderFormatSubmenu("notes")} {this.renderFormatSubmenu("highlights")} + {this.renderFormatSubmenu("words")} ); } diff --git a/src/components/selectBook/component.tsx b/src/components/selectBook/component.tsx index c1a6fd79..85bcf71d 100644 --- a/src/components/selectBook/component.tsx +++ b/src/components/selectBook/component.tsx @@ -213,7 +213,7 @@ class SelectBook extends React.Component { this.setState({ exportSubmenu: "", isShowExport: false }); }} > - {(["csv", "md", "txt", "html", "pdf"] as const).map( + {(["csv", "md", "txt", "html", "pdf", "json"] as const).map( (fmt) => ( { }); }} > - {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)[fmt] + } ) )} @@ -297,7 +298,7 @@ class SelectBook extends React.Component { this.setState({ exportSubmenu: "", isShowExport: false }); }} > - {(["csv", "md", "txt", "html", "pdf"] as const).map( + {(["csv", "md", "txt", "html", "pdf", "json"] as const).map( (fmt) => ( { }); }} > - {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)[fmt] + } ) )} - { - 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")); +
+ { + this.setState({ + exportSubmenu: "words", + isShowExport: true, + }); + }} + onMouseLeave={() => { + this.setState({ exportSubmenu: "" }); + }} + > + Export dictionary history + + +
- Export dictionary history - + onMouseEnter={() => { + this.setState({ + exportSubmenu: "words", + isShowExport: true, + }); + }} + onMouseLeave={() => { + this.setState({ exportSubmenu: "", isShowExport: false }); + }} + > + {(["csv", "json"] as const).map((fmt) => ( + { + 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"} + + ))} +
+
{ diff --git a/src/containers/settings/dataSetting/component.tsx b/src/containers/settings/dataSetting/component.tsx index 55cf3a78..a352281c 100644 --- a/src/containers/settings/dataSetting/component.tsx +++ b/src/containers/settings/dataSetting/component.tsx @@ -45,6 +45,7 @@ class DataSetting extends React.Component { snapshotList: [], exportNotesFormat: "", exportHighlightsFormat: "", + exportWordsFormat: "", isEnableDiscordRPC: ConfigService.getReaderConfig("isEnableDiscordRPC") === "yes", isEnableKoReaderSync: @@ -721,6 +722,7 @@ class DataSetting extends React.Component { | "txt" | "html" | "pdf" + | "json" | ""; if (!fmt) return; this.setState({ exportNotesFormat: "" }); @@ -762,6 +764,9 @@ class DataSetting extends React.Component { +
@@ -776,6 +781,7 @@ class DataSetting extends React.Component { | "txt" | "html" | "pdf" + | "json" | ""; if (!fmt) return; this.setState({ exportHighlightsFormat: "" }); @@ -812,17 +818,24 @@ class DataSetting extends React.Component { +
Export all dictionary history - { +
Clear all data diff --git a/src/containers/settings/dataSetting/interface.tsx b/src/containers/settings/dataSetting/interface.tsx index e5f69dee..bf9617ab 100644 --- a/src/containers/settings/dataSetting/interface.tsx +++ b/src/containers/settings/dataSetting/interface.tsx @@ -9,6 +9,7 @@ export interface SettingInfoState { snapshotList: { file: string; time: number }[]; exportNotesFormat: string; exportHighlightsFormat: string; + exportWordsFormat: string; isEnableDiscordRPC: boolean; isEnableKoReaderSync: boolean; isEnableNotionSync: boolean; diff --git a/src/utils/file/export.ts b/src/utils/file/export.ts index 2a404665..31d0b72a 100644 --- a/src/utils/file/export.ts +++ b/src/utils/file/export.ts @@ -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 => { @@ -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 => { 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 => { 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 => { 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) {