share rules
This commit is contained in:
@@ -8,9 +8,10 @@ import {
|
||||
OPT_LANGS_SPECIAL,
|
||||
PROMPT_PLACE_FROM,
|
||||
PROMPT_PLACE_TO,
|
||||
KV_HEADER_KEY,
|
||||
KV_SALT_SYNC,
|
||||
} from "../config";
|
||||
import { getSetting, detectLang } from "../libs";
|
||||
import { sha256 } from "../libs/utils";
|
||||
|
||||
/**
|
||||
* 同步数据
|
||||
@@ -25,7 +26,7 @@ export const apiSyncData = async (url, key, data) =>
|
||||
{
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
[KV_HEADER_KEY]: key,
|
||||
Authorization: `Bearer ${await sha256(key, KV_SALT_SYNC)}`,
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
|
||||
@@ -248,4 +248,12 @@ export const I18N = {
|
||||
zh: `数据同步密钥`,
|
||||
en: `Data Sync Key`,
|
||||
},
|
||||
error_got_some_wrong: {
|
||||
zh: "抱歉,出错了!",
|
||||
en: "Sorry, something went wrong!",
|
||||
},
|
||||
error_sync_setting: {
|
||||
zh: "您的同步设置未填写,无法在线分享。",
|
||||
en: "Your sync settings are missing and cannot be shared online.",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -25,9 +25,11 @@ export const CLIENT_FIREFOX = "firefox";
|
||||
export const CLIENT_USERSCRIPT = "userscript";
|
||||
export const CLIENT_EXTS = [CLIENT_CHROME, CLIENT_EDGE, CLIENT_FIREFOX];
|
||||
|
||||
export const KV_HEADER_KEY = "X-KISS-PSK";
|
||||
export const KV_RULES_KEY = "KT_RULES";
|
||||
export const KV_RULES_SHARE_KEY = "KT_RULES_SHARE";
|
||||
export const KV_SETTING_KEY = "KT_SETTING";
|
||||
export const KV_SALT_SYNC = "KISS-Translator-SYNC";
|
||||
export const KV_SALT_SHARE = "KISS-Translator-SHARE";
|
||||
|
||||
export const CACHE_NAME = `${APP_NAME}_cache`;
|
||||
|
||||
|
||||
60
src/hooks/Alert.js
Normal file
60
src/hooks/Alert.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import { createContext, useContext, useState, forwardRef } from "react";
|
||||
import Snackbar from "@mui/material/Snackbar";
|
||||
import MuiAlert from "@mui/material/Alert";
|
||||
|
||||
const Alert = forwardRef(function Alert(props, ref) {
|
||||
return <MuiAlert elevation={6} ref={ref} variant="filled" {...props} />;
|
||||
});
|
||||
|
||||
const AlertContext = createContext(null);
|
||||
|
||||
/**
|
||||
* 左下角提示,注入context后,方便全局调用
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
export function AlertProvider({ children }) {
|
||||
const vertical = "top";
|
||||
const horizontal = "center";
|
||||
const [open, setOpen] = useState(false);
|
||||
const [severity, setSeverity] = useState("info");
|
||||
const [message, setMessage] = useState("");
|
||||
|
||||
const error = (msg) => showAlert(msg, "error");
|
||||
const warning = (msg) => showAlert(msg, "warning");
|
||||
const info = (msg) => showAlert(msg, "info");
|
||||
const success = (msg) => showAlert(msg, "success");
|
||||
|
||||
const showAlert = (msg, type) => {
|
||||
setOpen(true);
|
||||
setMessage(msg);
|
||||
setSeverity(type);
|
||||
};
|
||||
|
||||
const handleClose = (_, reason) => {
|
||||
if (reason === "clickaway") {
|
||||
return;
|
||||
}
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<AlertContext.Provider value={{ error, warning, info, success }}>
|
||||
{children}
|
||||
<Snackbar
|
||||
open={open}
|
||||
autoHideDuration={3000}
|
||||
onClose={handleClose}
|
||||
anchorOrigin={{ vertical, horizontal }}
|
||||
>
|
||||
<Alert onClose={handleClose} severity={severity} sx={{ width: "100%" }}>
|
||||
{message}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
</AlertContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useAlert() {
|
||||
return useContext(AlertContext);
|
||||
}
|
||||
@@ -3,18 +3,22 @@ import {
|
||||
DEFAULT_SYNC,
|
||||
KV_SETTING_KEY,
|
||||
KV_RULES_KEY,
|
||||
KV_RULES_SHARE_KEY,
|
||||
STOKEY_SETTING,
|
||||
STOKEY_RULES,
|
||||
KV_SALT_SHARE,
|
||||
} from "../config";
|
||||
import storage from "../libs/storage";
|
||||
import { getSetting, getRules } from ".";
|
||||
import { apiSyncData } from "../apis";
|
||||
import { sha256 } from "./utils";
|
||||
|
||||
const loadOpt = async () => (await storage.getObj(STOKEY_SYNC)) || DEFAULT_SYNC;
|
||||
export const loadSyncOpt = async () =>
|
||||
(await storage.getObj(STOKEY_SYNC)) || DEFAULT_SYNC;
|
||||
|
||||
export const syncSetting = async () => {
|
||||
try {
|
||||
const { syncUrl, syncKey, settingUpdateAt } = await loadOpt();
|
||||
const { syncUrl, syncKey, settingUpdateAt } = await loadSyncOpt();
|
||||
if (!syncUrl || !syncKey) {
|
||||
return;
|
||||
}
|
||||
@@ -44,7 +48,7 @@ export const syncSetting = async () => {
|
||||
|
||||
export const syncRules = async () => {
|
||||
try {
|
||||
const { syncUrl, syncKey, rulesUpdateAt } = await loadOpt();
|
||||
const { syncUrl, syncKey, rulesUpdateAt } = await loadSyncOpt();
|
||||
if (!syncUrl || !syncKey) {
|
||||
return;
|
||||
}
|
||||
@@ -72,6 +76,17 @@ export const syncRules = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
export const syncShareRules = async ({ rules, syncUrl, syncKey }) => {
|
||||
await apiSyncData(syncUrl, syncKey, {
|
||||
key: KV_RULES_SHARE_KEY,
|
||||
value: rules,
|
||||
updateAt: Date.now(),
|
||||
});
|
||||
const psk = await sha256(syncKey, KV_SALT_SHARE);
|
||||
const shareUrl = `${syncUrl}?psk=${psk}`;
|
||||
return shareUrl;
|
||||
};
|
||||
|
||||
export const syncAll = async () => {
|
||||
await syncSetting();
|
||||
await syncRules();
|
||||
|
||||
@@ -98,3 +98,16 @@ export const type = (o) => {
|
||||
const s = Object.prototype.toString.call(o);
|
||||
return s.match(/\[object (.*?)\]/)[1].toLowerCase();
|
||||
};
|
||||
|
||||
/**
|
||||
* sha256
|
||||
* @param {*} text
|
||||
* @returns
|
||||
*/
|
||||
export const sha256 = async (text, salt) => {
|
||||
const data = new TextEncoder().encode(text + salt);
|
||||
const digest = await crypto.subtle.digest({ name: "SHA-256" }, data);
|
||||
return [...new Uint8Array(digest)]
|
||||
.map((b) => b.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
};
|
||||
|
||||
@@ -32,9 +32,12 @@ import Radio from "@mui/material/Radio";
|
||||
import RadioGroup from "@mui/material/RadioGroup";
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import ShareIcon from "@mui/icons-material/Share";
|
||||
import SyncIcon from "@mui/icons-material/Sync";
|
||||
import { useSubrules } from "../../hooks/Rules";
|
||||
import { rulesCache, tryLoadRules } from "../../libs/rules";
|
||||
import { useAlert } from "../../hooks/Alert";
|
||||
import { loadSyncOpt, syncShareRules } from "../../libs/sync";
|
||||
|
||||
function RuleFields({ rule, rules, setShow }) {
|
||||
const initFormValues = rule || { ...DEFAULT_RULE, transOpen: "true" };
|
||||
@@ -393,12 +396,56 @@ function UploadButton({ onChange, text }) {
|
||||
);
|
||||
}
|
||||
|
||||
function ShareButton({ rules, injectRules, selectedSub }) {
|
||||
const alert = useAlert();
|
||||
const i18n = useI18n();
|
||||
const handleClick = async () => {
|
||||
try {
|
||||
const { syncUrl, syncKey } = await loadSyncOpt();
|
||||
if (!syncUrl || !syncKey) {
|
||||
alert.warning(i18n("error_sync_setting"));
|
||||
return;
|
||||
}
|
||||
|
||||
const shareRules = [...rules.list];
|
||||
if (injectRules) {
|
||||
const subRules = await tryLoadRules(selectedSub?.url);
|
||||
shareRules.splice(-1, 0, ...subRules);
|
||||
}
|
||||
|
||||
const url = await syncShareRules({
|
||||
rules: shareRules,
|
||||
syncUrl,
|
||||
syncKey,
|
||||
});
|
||||
|
||||
window.open(url, "_blank");
|
||||
} catch (err) {
|
||||
alert.warning(i18n("error_got_some_wrong"));
|
||||
console.log("[share rules]", err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
onClick={handleClick}
|
||||
startIcon={<ShareIcon />}
|
||||
>
|
||||
{"分享"}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function UserRules() {
|
||||
const i18n = useI18n();
|
||||
const rules = useRules();
|
||||
const [showAdd, setShowAdd] = useState(false);
|
||||
const setting = useSetting();
|
||||
const updateSetting = useSettingUpdate();
|
||||
const subrules = useSubrules();
|
||||
const selectedSub = subrules.list.find((item) => item.selected);
|
||||
|
||||
const injectRules = !!setting?.injectRules;
|
||||
|
||||
@@ -451,6 +498,12 @@ function UserRules() {
|
||||
text={i18n("export")}
|
||||
/>
|
||||
|
||||
<ShareButton
|
||||
rules={rules}
|
||||
injectRules={injectRules}
|
||||
selectedSub={selectedSub}
|
||||
/>
|
||||
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
|
||||
@@ -8,6 +8,7 @@ import Link from "@mui/material/Link";
|
||||
import { URL_KISS_WORKER } from "../../config";
|
||||
import { debounce } from "../../libs/utils";
|
||||
import { useMemo } from "react";
|
||||
import { syncAll } from "../../libs/sync";
|
||||
|
||||
export default function SyncSetting() {
|
||||
const i18n = useI18n();
|
||||
@@ -15,13 +16,14 @@ export default function SyncSetting() {
|
||||
|
||||
const handleChange = useMemo(
|
||||
() =>
|
||||
debounce((e) => {
|
||||
debounce(async (e) => {
|
||||
e.preventDefault();
|
||||
const { name, value } = e.target;
|
||||
sync.update({
|
||||
await sync.update({
|
||||
[name]: value,
|
||||
});
|
||||
}, 500),
|
||||
await syncAll();
|
||||
}, 1000),
|
||||
[sync]
|
||||
);
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import { isGm } from "../../libs/browser";
|
||||
import { sleep } from "../../libs/utils";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
import { syncAll } from "../../libs/sync";
|
||||
import { AlertProvider } from "../../hooks/Alert";
|
||||
|
||||
export default function Options() {
|
||||
const [error, setError] = useState(false);
|
||||
@@ -69,6 +70,7 @@ export default function Options() {
|
||||
return (
|
||||
<StoragesProvider>
|
||||
<ThemeProvider>
|
||||
<AlertProvider>
|
||||
<HashRouter>
|
||||
<Routes>
|
||||
<Route path="/" element={<Layout />}>
|
||||
@@ -79,6 +81,7 @@ export default function Options() {
|
||||
</Route>
|
||||
</Routes>
|
||||
</HashRouter>
|
||||
</AlertProvider>
|
||||
</ThemeProvider>
|
||||
</StoragesProvider>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user