add upload button for fav words

This commit is contained in:
Gabe Yuan
2023-10-26 23:55:05 +08:00
parent b785cfe854
commit 3e24568df9
7 changed files with 224 additions and 223 deletions

View File

@@ -5,6 +5,7 @@ import { getWordsWithDefault, setWords } from "../libs/storage";
import { useSyncMeta } from "./Sync"; import { useSyncMeta } from "./Sync";
export function useFavWords() { export function useFavWords() {
const [loading, setLoading] = useState(false);
const [favWords, setFavWords] = useState({}); const [favWords, setFavWords] = useState({});
const { updateSyncMeta } = useSyncMeta(); const { updateSyncMeta } = useSyncMeta();
@@ -24,17 +25,43 @@ export function useFavWords() {
[updateSyncMeta, favWords] [updateSyncMeta, favWords]
); );
const mergeWords = useCallback(
async (newWords) => {
const favs = { ...favWords };
newWords.forEach((word) => {
if (!favs[word]) {
favs[word] = { createdAt: Date.now() };
}
});
await setWords(favs);
await updateSyncMeta(KV_WORDS_KEY);
await trySyncWords();
setFavWords(favs);
},
[updateSyncMeta, favWords]
);
const clearWords = useCallback(async () => {
await setWords({});
await updateSyncMeta(KV_WORDS_KEY);
await trySyncWords();
setFavWords({});
}, [updateSyncMeta]);
useEffect(() => { useEffect(() => {
(async () => { (async () => {
try { try {
setLoading(true);
await trySyncWords(); await trySyncWords();
const favWords = await getWordsWithDefault(); const favWords = await getWordsWithDefault();
setFavWords(favWords); setFavWords(favWords);
} catch (err) { } catch (err) {
console.log("[query fav]", err); console.log("[query fav]", err);
} finally {
setLoading(false);
} }
})(); })();
}, []); }, []);
return { favWords, toggleFav }; return { loading, favWords, toggleFav, mergeWords, clearWords };
} }

View File

@@ -0,0 +1,27 @@
import FileDownloadIcon from "@mui/icons-material/FileDownload";
import Button from "@mui/material/Button";
export default function DownloadButton({ data, text, fileName }) {
const handleClick = (e) => {
e.preventDefault();
if (data) {
const url = window.URL.createObjectURL(new Blob([data]));
const link = document.createElement("a");
link.href = url;
link.setAttribute("download", fileName || `${Date.now()}.json`);
document.body.appendChild(link);
link.click();
link.remove();
}
};
return (
<Button
size="small"
variant="outlined"
onClick={handleClick}
startIcon={<FileDownloadIcon />}
>
{text}
</Button>
);
}

View File

@@ -1,21 +1,23 @@
import Stack from "@mui/material/Stack"; import Stack from "@mui/material/Stack";
import { OPT_TRANS_BAIDU } from "../../config"; import { OPT_TRANS_BAIDU } from "../../config";
import { useEffect, useState, useRef } from "react"; import { useEffect, useState } from "react";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import Accordion from "@mui/material/Accordion"; import Accordion from "@mui/material/Accordion";
import AccordionSummary from "@mui/material/AccordionSummary"; import AccordionSummary from "@mui/material/AccordionSummary";
import AccordionDetails from "@mui/material/AccordionDetails"; import AccordionDetails from "@mui/material/AccordionDetails";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import CircularProgress from "@mui/material/CircularProgress"; import CircularProgress from "@mui/material/CircularProgress";
import FileDownloadIcon from "@mui/icons-material/FileDownload";
import FileUploadIcon from "@mui/icons-material/FileUpload";
import { useI18n } from "../../hooks/I18n"; import { useI18n } from "../../hooks/I18n";
import Alert from "@mui/material/Alert"; import Alert from "@mui/material/Alert";
import { apiTranslate } from "../../apis"; import { apiTranslate } from "../../apis";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import { useFavWords } from "../../hooks/FavWords"; import { useFavWords } from "../../hooks/FavWords";
import { DictCont } from "../Selection/TranCont"; import DictCont from "../Selection/DictCont";
import DownloadButton from "./DownloadButton";
import UploadButton from "./UploadButton";
import Button from "@mui/material/Button";
import ClearAllIcon from "@mui/icons-material/ClearAll";
import { isValidWord } from "../../libs/utils";
function DictField({ word }) { function DictField({ word }) {
const [dictResult, setDictResult] = useState(null); const [dictResult, setDictResult] = useState(null);
@@ -53,7 +55,7 @@ function DictField({ word }) {
return <DictCont dictResult={dictResult} />; return <DictCont dictResult={dictResult} />;
} }
function FavAccordion({ word }) { function FavAccordion({ word, index }) {
const [expanded, setExpanded] = useState(false); const [expanded, setExpanded] = useState(false);
const handleChange = (e) => { const handleChange = (e) => {
@@ -66,7 +68,7 @@ function FavAccordion({ word }) {
{/* <Typography>{`[${new Date( {/* <Typography>{`[${new Date(
createdAt createdAt
).toLocaleString()}] ${word}`}</Typography> */} ).toLocaleString()}] ${word}`}</Typography> */}
<Typography>{word}</Typography> <Typography>{`${index + 1}. ${word}`}</Typography>
</AccordionSummary> </AccordionSummary>
<AccordionDetails> <AccordionDetails>
{expanded && <DictField word={word} />} {expanded && <DictField word={word} />}
@@ -75,80 +77,9 @@ function FavAccordion({ word }) {
); );
} }
function DownloadButton({ data, text, fileName }) {
const handleClick = (e) => {
e.preventDefault();
if (data) {
const url = window.URL.createObjectURL(new Blob([data]));
const link = document.createElement("a");
link.href = url;
link.setAttribute(
"download",
fileName || `kiss-words_${Date.now()}.json`
);
document.body.appendChild(link);
link.click();
link.remove();
}
};
return (
<Button
size="small"
variant="outlined"
onClick={handleClick}
startIcon={<FileDownloadIcon />}
>
{text}
</Button>
);
}
function UploadButton({ handleImport, text }) {
const i18n = useI18n();
const inputRef = useRef(null);
const handleClick = () => {
inputRef.current && inputRef.current.click();
};
const onChange = (e) => {
const file = e.target.files[0];
if (!file) {
return;
}
if (!file.type.includes("json")) {
alert(i18n("error_wrong_file_type"));
return;
}
const reader = new FileReader();
reader.onload = async (e) => {
handleImport(e.target.result);
};
reader.readAsText(file);
};
return (
<Button
size="small"
variant="outlined"
onClick={handleClick}
startIcon={<FileUploadIcon />}
>
{text}
<input
type="file"
accept=".json"
ref={inputRef}
onChange={onChange}
hidden
/>
</Button>
);
}
export default function FavWords() { export default function FavWords() {
const i18n = useI18n(); const i18n = useI18n();
const { favWords } = useFavWords(); const { loading, favWords, mergeWords, clearWords } = useFavWords();
const favList = Object.entries(favWords).sort((a, b) => const favList = Object.entries(favWords).sort((a, b) =>
a[0].localeCompare(b[0]) a[0].localeCompare(b[0])
); );
@@ -156,8 +87,11 @@ export default function FavWords() {
const handleImport = async (data) => { const handleImport = async (data) => {
try { try {
console.log("data", data); const newWords = data
// await rules.merge(JSON.parse(data)); .split("\n")
.map((line) => line.split(",")[0].trim())
.filter(isValidWord);
await mergeWords(newWords);
} catch (err) { } catch (err) {
console.log("[import rules]", err); console.log("[import rules]", err);
} }
@@ -173,17 +107,42 @@ export default function FavWords() {
useFlexGap useFlexGap
flexWrap="wrap" flexWrap="wrap"
> >
<UploadButton text={i18n("import")} handleImport={handleImport} /> <UploadButton
<DownloadButton text={i18n("import")}
data={JSON.stringify(downloadList, null, 2)} handleImport={handleImport}
text={i18n("export")} fileType="text"
fileExts={[".txt", ".csv"]}
/> />
<DownloadButton
data={downloadList.join("\n")}
text={i18n("export")}
fileName={`kiss-words_${Date.now()}.txt`}
/>
<Button
size="small"
variant="outlined"
onClick={() => {
clearWords();
}}
startIcon={<ClearAllIcon />}
>
{i18n("clear_all")}
</Button>
</Stack> </Stack>
<Box> <Box>
{favList.map(([word, { createdAt }]) => ( {loading ? (
<FavAccordion key={word} word={word} createdAt={createdAt} /> <CircularProgress size={24} />
))} ) : (
favList.map(([word, { createdAt }], index) => (
<FavAccordion
key={word}
index={index}
word={word}
createdAt={createdAt}
/>
))
)}
</Box> </Box>
</Stack> </Stack>
</Box> </Box>

View File

@@ -16,7 +16,7 @@ import {
URL_KISS_RULES_NEW_ISSUE, URL_KISS_RULES_NEW_ISSUE,
OPT_SYNCTYPE_WORKER, OPT_SYNCTYPE_WORKER,
} from "../../config"; } from "../../config";
import { useState, useRef, useEffect, useMemo } from "react"; import { useState, useEffect, useMemo } from "react";
import { useI18n } from "../../hooks/I18n"; import { useI18n } from "../../hooks/I18n";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import Accordion from "@mui/material/Accordion"; import Accordion from "@mui/material/Accordion";
@@ -26,8 +26,6 @@ import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import { useRules } from "../../hooks/Rules"; import { useRules } from "../../hooks/Rules";
import MenuItem from "@mui/material/MenuItem"; import MenuItem from "@mui/material/MenuItem";
import Grid from "@mui/material/Grid"; import Grid from "@mui/material/Grid";
import FileDownloadIcon from "@mui/icons-material/FileDownload";
import FileUploadIcon from "@mui/icons-material/FileUpload";
import { useSetting } from "../../hooks/Setting"; import { useSetting } from "../../hooks/Setting";
import FormControlLabel from "@mui/material/FormControlLabel"; import FormControlLabel from "@mui/material/FormControlLabel";
import Switch from "@mui/material/Switch"; import Switch from "@mui/material/Switch";
@@ -50,6 +48,8 @@ import OwSubRule from "./OwSubRule";
import ClearAllIcon from "@mui/icons-material/ClearAll"; import ClearAllIcon from "@mui/icons-material/ClearAll";
import HelpButton from "./HelpButton"; import HelpButton from "./HelpButton";
import { useSyncCaches } from "../../hooks/Sync"; import { useSyncCaches } from "../../hooks/Sync";
import DownloadButton from "./DownloadButton";
import UploadButton from "./UploadButton";
function RuleFields({ rule, rules, setShow, setKeyword }) { function RuleFields({ rule, rules, setShow, setKeyword }) {
const initFormValues = rule || { const initFormValues = rule || {
@@ -392,77 +392,6 @@ function RuleAccordion({ rule, rules }) {
); );
} }
function DownloadButton({ data, text, fileName }) {
const handleClick = (e) => {
e.preventDefault();
if (data) {
const url = window.URL.createObjectURL(new Blob([data]));
const link = document.createElement("a");
link.href = url;
link.setAttribute(
"download",
fileName || `kiss-rules_${Date.now()}.json`
);
document.body.appendChild(link);
link.click();
link.remove();
}
};
return (
<Button
size="small"
variant="outlined"
onClick={handleClick}
startIcon={<FileDownloadIcon />}
>
{text}
</Button>
);
}
function UploadButton({ handleImport, text }) {
const i18n = useI18n();
const inputRef = useRef(null);
const handleClick = () => {
inputRef.current && inputRef.current.click();
};
const onChange = (e) => {
const file = e.target.files[0];
if (!file) {
return;
}
if (!file.type.includes("json")) {
alert(i18n("error_wrong_file_type"));
return;
}
const reader = new FileReader();
reader.onload = async (e) => {
handleImport(e.target.result);
};
reader.readAsText(file);
};
return (
<Button
size="small"
variant="outlined"
onClick={handleClick}
startIcon={<FileUploadIcon />}
>
{text}
<input
type="file"
accept=".json"
ref={inputRef}
onChange={onChange}
hidden
/>
</Button>
);
}
function ShareButton({ rules, injectRules, selectedUrl }) { function ShareButton({ rules, injectRules, selectedUrl }) {
const alert = useAlert(); const alert = useAlert();
const i18n = useI18n(); const i18n = useI18n();
@@ -564,6 +493,7 @@ function UserRules({ subRules }) {
<DownloadButton <DownloadButton
data={JSON.stringify([...rules.list].reverse(), null, 2)} data={JSON.stringify([...rules.list].reverse(), null, 2)}
text={i18n("export")} text={i18n("export")}
fileName={`kiss-rules_${Date.now()}.json`}
/> />
<ShareButton <ShareButton

View File

@@ -0,0 +1,55 @@
import { useRef } from "react";
import FileUploadIcon from "@mui/icons-material/FileUpload";
import { useI18n } from "../../hooks/I18n";
import Button from "@mui/material/Button";
export default function UploadButton({
handleImport,
text,
fileType = "json",
fileExts = [".json"],
}) {
const i18n = useI18n();
const inputRef = useRef(null);
const handleClick = () => {
if (inputRef.current) {
inputRef.current.click();
inputRef.current.value = null;
}
};
const onChange = (e) => {
const file = e.target.files[0];
if (!file) {
return;
}
if (!file.type.includes(fileType)) {
alert(i18n("error_wrong_file_type"));
return;
}
const reader = new FileReader();
reader.onload = async (e) => {
handleImport(e.target.result);
};
reader.readAsText(file);
};
return (
<Button
size="small"
variant="outlined"
onClick={handleClick}
startIcon={<FileUploadIcon />}
>
{text}
<input
type="file"
accept={fileExts.join(", ")}
ref={inputRef}
onChange={onChange}
hidden
/>
</Button>
);
}

View File

@@ -0,0 +1,62 @@
import Box from "@mui/material/Box";
import Chip from "@mui/material/Chip";
import Stack from "@mui/material/Stack";
import FavBtn from "./FavBtn";
const exchangeMap = {
word_third: "第三人称单数",
word_ing: "现在分词",
word_done: "过去式",
word_past: "过去分词",
word_pl: "复数",
word_proto: "原词",
};
export default function DictCont({ dictResult }) {
if (!dictResult) {
return;
}
return (
<Box>
<Stack
direction="row"
justifyContent="space-between"
alignItems="flex-start"
>
<div style={{ fontWeight: "bold" }}>
{dictResult.simple_means?.word_name}
</div>
<FavBtn word={dictResult.simple_means?.word_name} />
</Stack>
{dictResult.simple_means?.symbols?.map(({ ph_en, ph_am, parts }, idx) => (
<div key={idx}>
<div>{`英[${ph_en}] 美[${ph_am}]`}</div>
<ul style={{ margin: "0.5em 0" }}>
{parts.map(({ part, means }, idx) => (
<li key={idx}>
{part ? `[${part}] ${means.join("; ")}` : means.join("; ")}
</li>
))}
</ul>
</div>
))}
<div>
{Object.entries(dictResult.simple_means?.exchange || {})
.map(([key, val]) => `${exchangeMap[key] || key}: ${val.join(", ")}`)
.join("; ")}
</div>
<Stack direction="row" spacing={1}>
{Object.values(dictResult.simple_means?.tags || {})
.flat()
.filter((item) => item)
.map((item) => (
<Chip label={item} size="small" />
))}
</Stack>
</Box>
);
}

View File

@@ -2,7 +2,6 @@ import TextField from "@mui/material/TextField";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import Alert from "@mui/material/Alert"; import Alert from "@mui/material/Alert";
import CircularProgress from "@mui/material/CircularProgress"; import CircularProgress from "@mui/material/CircularProgress";
import Chip from "@mui/material/Chip";
import Stack from "@mui/material/Stack"; import Stack from "@mui/material/Stack";
import { useI18n } from "../../hooks/I18n"; import { useI18n } from "../../hooks/I18n";
import { DEFAULT_TRANS_APIS, OPT_TRANS_BAIDU } from "../../config"; import { DEFAULT_TRANS_APIS, OPT_TRANS_BAIDU } from "../../config";
@@ -10,65 +9,7 @@ import { useEffect, useState } from "react";
import { apiTranslate } from "../../apis"; import { apiTranslate } from "../../apis";
import { isValidWord } from "../../libs/utils"; import { isValidWord } from "../../libs/utils";
import CopyBtn from "./CopyBtn"; import CopyBtn from "./CopyBtn";
import FavBtn from "./FavBtn"; import DictCont from "./DictCont";
const exchangeMap = {
word_third: "第三人称单数",
word_ing: "现在分词",
word_done: "过去式",
word_past: "过去分词",
word_pl: "复数",
word_proto: "原词",
};
export function DictCont({ dictResult }) {
if (!dictResult) {
return;
}
return (
<Box>
<Stack
direction="row"
justifyContent="space-between"
alignItems="flex-start"
>
<div style={{ fontWeight: "bold" }}>
{dictResult.simple_means?.word_name}
</div>
<FavBtn word={dictResult.simple_means?.word_name} />
</Stack>
{dictResult.simple_means?.symbols?.map(({ ph_en, ph_am, parts }, idx) => (
<div key={idx}>
<div>{`英[${ph_en}] 美[${ph_am}]`}</div>
<ul style={{ margin: "0.5em 0" }}>
{parts.map(({ part, means }, idx) => (
<li key={idx}>
{part ? `[${part}] ${means.join("; ")}` : means.join("; ")}
</li>
))}
</ul>
</div>
))}
<div>
{Object.entries(dictResult.simple_means?.exchange || {})
.map(([key, val]) => `${exchangeMap[key] || key}: ${val.join(", ")}`)
.join("; ")}
</div>
<Stack direction="row" spacing={1}>
{Object.values(dictResult.simple_means?.tags || {})
.flat()
.filter((item) => item)
.map((item) => (
<Chip label={item} size="small" />
))}
</Stack>
</Box>
);
}
export default function TranCont({ export default function TranCont({
text, text,