share rules

This commit is contained in:
Gabe Yuan
2023-08-20 23:30:08 +08:00
parent 7ec43a1d3f
commit 232e9a47a2
9 changed files with 178 additions and 21 deletions

View File

@@ -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),

View File

@@ -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.",
},
};

View File

@@ -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
View 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);
}

View File

@@ -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();

View File

@@ -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("");
};

View File

@@ -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

View File

@@ -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]
);

View File

@@ -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>
);