diff --git a/src/config/i18n.js b/src/config/i18n.js index 3ade455..07ae872 100644 --- a/src/config/i18n.js +++ b/src/config/i18n.js @@ -375,6 +375,14 @@ export const I18N = { zh: `选择器`, en: `Selector`, }, + rootSelector: { + zh: `根选择器`, + en: `Root Selector`, + }, + fixerFunction: { + zh: `修复函数`, + en: `Fixer Function`, + }, import: { zh: `导入`, en: `Import`, diff --git a/src/config/index.js b/src/config/index.js index 00083aa..79da443 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -23,6 +23,7 @@ export const STOKEY_MSAUTH = `${APP_NAME}_msauth`; export const STOKEY_BDAUTH = `${APP_NAME}_bdauth`; export const STOKEY_SETTING = `${APP_NAME}_setting`; export const STOKEY_RULES = `${APP_NAME}_rules`; +export const STOKEY_WFRULES = `${APP_NAME}_webfix_rules`; export const STOKEY_WORDS = `${APP_NAME}_words`; export const STOKEY_SYNC = `${APP_NAME}_sync`; export const STOKEY_FAB = `${APP_NAME}_fab`; @@ -41,6 +42,7 @@ export const CLIENT_USERSCRIPT = "userscript"; export const CLIENT_EXTS = [CLIENT_CHROME, CLIENT_EDGE, CLIENT_FIREFOX]; export const KV_RULES_KEY = "kiss-rules.json"; +export const KV_WFRULES_KEY = "kiss-webfix.json"; export const KV_WORDS_KEY = "kiss-words.json"; export const KV_RULES_SHARE_KEY = "kiss-rules-share.json"; export const KV_SETTING_KEY = "kiss-setting.json"; diff --git a/src/hooks/Storage.js b/src/hooks/Storage.js index b84da0b..07e6524 100644 --- a/src/hooks/Storage.js +++ b/src/hooks/Storage.js @@ -1,6 +1,12 @@ import { useCallback, useEffect, useState } from "react"; import { storage } from "../libs/storage"; +/** + * + * @param {*} key + * @param {*} defaultVal 需为调用hook外的常量 + * @returns + */ export function useStorage(key, defaultVal) { const [loading, setLoading] = useState(false); const [data, setData] = useState(null); diff --git a/src/hooks/WebfixRules.js b/src/hooks/WebfixRules.js new file mode 100644 index 0000000..cd91784 --- /dev/null +++ b/src/hooks/WebfixRules.js @@ -0,0 +1,58 @@ +import { STOKEY_WFRULES, KV_WFRULES_KEY } from "../config"; +import { useStorage } from "./Storage"; +import { trySyncWebfixRules } from "../libs/sync"; +import { useCallback } from "react"; +import { useSyncMeta } from "./Sync"; + +const DEFAULT_WFRULES = []; + +/** + * 修复规则 hook + * @returns + */ +export function useWebfixRules() { + const { data: list, save } = useStorage(STOKEY_WFRULES, DEFAULT_WFRULES); + const { updateSyncMeta } = useSyncMeta(); + + const updateRules = useCallback( + async (rules) => { + await save(rules); + await updateSyncMeta(KV_WFRULES_KEY); + trySyncWebfixRules(); + }, + [save, updateSyncMeta] + ); + + const add = useCallback( + async (rule) => { + const rules = [...list]; + if (rules.map((item) => item.pattern).includes(rule.pattern)) { + return; + } + rules.unshift(rule); + await updateRules(rules); + }, + [list, updateRules] + ); + + const del = useCallback( + async (pattern) => { + let rules = [...list]; + rules = rules.filter((item) => item.pattern !== pattern); + await updateRules(rules); + }, + [list, updateRules] + ); + + const put = useCallback( + async (pattern, obj) => { + const rules = [...list]; + const rule = rules.find((r) => r.pattern === pattern); + rule && Object.assign(rule, obj); + await updateRules(rules); + }, + [list, updateRules] + ); + + return { list, add, del, put }; +} diff --git a/src/libs/storage.js b/src/libs/storage.js index 6802ae3..3e19c6b 100644 --- a/src/libs/storage.js +++ b/src/libs/storage.js @@ -1,6 +1,7 @@ import { STOKEY_SETTING, STOKEY_RULES, + STOKEY_WFRULES, STOKEY_WORDS, STOKEY_FAB, STOKEY_SYNC, @@ -98,6 +99,14 @@ export const getRulesWithDefault = async () => (await getRules()) || DEFAULT_RULES; export const setRules = (val) => setObj(STOKEY_RULES, val); +/** + * 修复规则列表 + */ +export const getWebfixRules = () => getObj(STOKEY_WFRULES); +export const getWebfixRulesWithDefault = async () => + (await getWebfixRules()) || []; +export const setWebfixRules = (val) => setObj(STOKEY_WFRULES, val); + /** * 词汇列表 */ diff --git a/src/libs/sync.js b/src/libs/sync.js index 7d9743a..3e7b34a 100644 --- a/src/libs/sync.js +++ b/src/libs/sync.js @@ -2,6 +2,7 @@ import { APP_LCNAME, KV_SETTING_KEY, KV_RULES_KEY, + KV_WFRULES_KEY, KV_WORDS_KEY, KV_RULES_SHARE_KEY, KV_SALT_SHARE, @@ -13,8 +14,10 @@ import { getSettingWithDefault, getRulesWithDefault, getWordsWithDefault, + getWebfixRulesWithDefault, setSetting, setRules, + setWebfixRules, setWords, } from "./storage"; import { apiSyncData } from "../apis"; @@ -138,6 +141,25 @@ export const trySyncRules = async () => { } }; +/** + * 同步修复规则 + * @returns + */ +const syncWebfixRules = async () => { + const res = await syncData(KV_WFRULES_KEY, getWebfixRulesWithDefault); + if (res?.isNew) { + await setWebfixRules(res.value); + } +}; + +export const trySyncWebfixRules = async () => { + try { + await syncWebfixRules(); + } catch (err) { + console.log("[sync user webfix rules]", err); + } +}; + /** * 同步词汇 * @returns @@ -185,11 +207,13 @@ export const syncShareRules = async ({ rules, syncUrl, syncKey }) => { export const syncSettingAndRules = async () => { await syncSetting(); await syncRules(); + await syncWebfixRules(); await syncWords(); }; export const trySyncSettingAndRules = async () => { await trySyncSetting(); await trySyncRules(); + await trySyncWebfixRules(); await trySyncWords(); }; diff --git a/src/libs/webfix.js b/src/libs/webfix.js index 2ab3a01..c9a7c85 100644 --- a/src/libs/webfix.js +++ b/src/libs/webfix.js @@ -5,9 +5,10 @@ import { apiFetch } from "../apis"; /** * 修复程序类型 */ -const FIXER_BR = "br"; +export const FIXER_BR = "br"; const FIXER_BN = "bn"; const FIXER_FONTSIZE = "fontSize"; +export const FIXER_ALL = [FIXER_BR, FIXER_BN, FIXER_FONTSIZE]; /** * 需要修复的站点列表 diff --git a/src/views/Options/Webfix.js b/src/views/Options/Webfix.js index b5486a5..a6ca014 100644 --- a/src/views/Options/Webfix.js +++ b/src/views/Options/Webfix.js @@ -13,43 +13,209 @@ import FormControlLabel from "@mui/material/FormControlLabel"; import Switch from "@mui/material/Switch"; import { useSetting } from "../../hooks/Setting"; import CircularProgress from "@mui/material/CircularProgress"; -import { syncWebfix, loadOrFetchWebfix } from "../../libs/webfix"; +import { + syncWebfix, + loadOrFetchWebfix, + FIXER_BR, + FIXER_ALL, +} from "../../libs/webfix"; import Button from "@mui/material/Button"; import SyncIcon from "@mui/icons-material/Sync"; import { useAlert } from "../../hooks/Alert"; import HelpButton from "./HelpButton"; import { URL_KISS_RULES_NEW_ISSUE } from "../../config"; +import MenuItem from "@mui/material/MenuItem"; +import { useWebfixRules } from "../../hooks/WebfixRules"; + +function WebfixFields({ rule, webfix, setShow }) { + const editMode = !!rule; + const initFormValues = rule || { + pattern: "", + selector: "", + rootSelector: "", + fixer: FIXER_BR, + }; + const i18n = useI18n(); + const [disabled, setDisabled] = useState(editMode); + const [errors, setErrors] = useState({}); + const [formValues, setFormValues] = useState(initFormValues); + + const { pattern, selector, rootSelector, fixer } = formValues; + + const hasSamePattern = (str) => { + for (const item of webfix.list || []) { + if (item.pattern === str && rule?.pattern !== str) { + return true; + } + } + return false; + }; + + const handleFocus = (e) => { + e.preventDefault(); + const { name } = e.target; + setErrors((pre) => ({ ...pre, [name]: "" })); + }; + + const handleChange = (e) => { + e.preventDefault(); + const { name, value } = e.target; + setFormValues((pre) => ({ ...pre, [name]: value })); + }; + + const handleCancel = (e) => { + e.preventDefault(); + if (editMode) { + setDisabled(true); + } else { + setShow(false); + } + setFormValues(initFormValues); + }; + + const handleSubmit = (e) => { + e.preventDefault(); + const errors = {}; + if (!pattern.trim()) { + errors.pattern = i18n("error_cant_be_blank"); + } + if (hasSamePattern(pattern)) { + errors.pattern = i18n("error_duplicate_values"); + } + if (!selector.trim()) { + errors.selector = i18n("error_cant_be_blank"); + } + if (Object.keys(errors).length > 0) { + setErrors(errors); + return; + } + + if (editMode) { + // 编辑 + setDisabled(true); + webfix.put(rule.pattern, formValues); + } else { + // 添加 + webfix.add(formValues); + setShow(false); + setFormValues(initFormValues); + } + }; -function ApiFields({ site }) { - const { selector, rootSelector, fixer } = site; return ( - - - - - +
+ + + + + + {FIXER_ALL.map((item) => ( + + {item} + + ))} + + + {webfix && + (editMode ? ( + // 编辑 + + {disabled ? ( + <> + + + + ) : ( + <> + + + + )} + + ) : ( + // 添加 + + + + + ))} + +
); } -function ApiAccordion({ site }) { +function WebfixAccordion({ rule, webfix }) { const [expanded, setExpanded] = useState(false); const handleChange = (e) => { @@ -59,10 +225,17 @@ function ApiAccordion({ site }) { return ( }> - {site.pattern} + + {rule.pattern} + - {expanded && } + {expanded && } ); @@ -74,6 +247,8 @@ export default function Webfix() { const i18n = useI18n(); const alert = useAlert(); const { setting, updateSetting } = useSetting(); + const [showAdd, setShowAdd] = useState(false); + const webfix = useWebfixRules(); const loadSites = useCallback(async () => { const sites = await loadOrFetchWebfix(process.env.REACT_APP_WEBFIXURL); @@ -120,6 +295,18 @@ export default function Webfix() { useFlexGap flexWrap="wrap" > + +