Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53c32f2bd8 | ||
|
|
0b9fe65833 | ||
|
|
bd45947d68 | ||
|
|
5d2e767e74 | ||
|
|
30af4c11d0 |
2
.env
2
.env
@@ -2,7 +2,7 @@ GENERATE_SOURCEMAP=false
|
|||||||
|
|
||||||
REACT_APP_NAME=KISS Translator
|
REACT_APP_NAME=KISS Translator
|
||||||
REACT_APP_NAME_CN=简约翻译
|
REACT_APP_NAME_CN=简约翻译
|
||||||
REACT_APP_VERSION=1.4.4
|
REACT_APP_VERSION=1.4.5
|
||||||
REACT_APP_HOMEPAGE=https://github.com/fishjar/kiss-translator
|
REACT_APP_HOMEPAGE=https://github.com/fishjar/kiss-translator
|
||||||
REACT_APP_OPTIONSPAGE=https://kiss-translator.rayjar.com/options
|
REACT_APP_OPTIONSPAGE=https://kiss-translator.rayjar.com/options
|
||||||
REACT_APP_OPTIONSPAGE2=https://fishjar.github.io/kiss-translator/options.html
|
REACT_APP_OPTIONSPAGE2=https://fishjar.github.io/kiss-translator/options.html
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "kiss-translator",
|
"name": "kiss-translator",
|
||||||
"description": "A minimalist bilingual translation Extension & Greasemonkey Script",
|
"description": "A minimalist bilingual translation Extension & Greasemonkey Script",
|
||||||
"version": "1.4.4",
|
"version": "1.4.5",
|
||||||
"author": "Gabe<yugang2002@gmail.com>",
|
"author": "Gabe<yugang2002@gmail.com>",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"name": "__MSG_app_name__",
|
"name": "__MSG_app_name__",
|
||||||
"description": "__MSG_app_description__",
|
"description": "__MSG_app_description__",
|
||||||
"version": "1.4.4",
|
"version": "1.4.5",
|
||||||
"default_locale": "en",
|
"default_locale": "en",
|
||||||
"author": "Gabe<yugang2002@gmail.com>",
|
"author": "Gabe<yugang2002@gmail.com>",
|
||||||
"homepage_url": "https://github.com/fishjar/kiss-translator",
|
"homepage_url": "https://github.com/fishjar/kiss-translator",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "__MSG_app_name__",
|
"name": "__MSG_app_name__",
|
||||||
"description": "__MSG_app_description__",
|
"description": "__MSG_app_description__",
|
||||||
"version": "1.4.4",
|
"version": "1.4.5",
|
||||||
"default_locale": "en",
|
"default_locale": "en",
|
||||||
"author": "Gabe<yugang2002@gmail.com>",
|
"author": "Gabe<yugang2002@gmail.com>",
|
||||||
"homepage_url": "https://github.com/fishjar/kiss-translator",
|
"homepage_url": "https://github.com/fishjar/kiss-translator",
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { fetchData, fetchPool } from "./libs/fetch";
|
|||||||
* 插件安装
|
* 插件安装
|
||||||
*/
|
*/
|
||||||
browser.runtime.onInstalled.addListener(() => {
|
browser.runtime.onInstalled.addListener(() => {
|
||||||
console.log("onInstalled");
|
console.log("KISS Translator onInstalled");
|
||||||
storage.trySetObj(STOKEY_SETTING, DEFAULT_SETTING);
|
storage.trySetObj(STOKEY_SETTING, DEFAULT_SETTING);
|
||||||
storage.trySetObj(STOKEY_RULES, DEFAULT_RULES);
|
storage.trySetObj(STOKEY_RULES, DEFAULT_RULES);
|
||||||
storage.trySetObj(STOKEY_SYNC, DEFAULT_SYNC);
|
storage.trySetObj(STOKEY_SYNC, DEFAULT_SYNC);
|
||||||
@@ -30,7 +30,7 @@ browser.runtime.onInstalled.addListener(() => {
|
|||||||
* 浏览器启动
|
* 浏览器启动
|
||||||
*/
|
*/
|
||||||
browser.runtime.onStartup.addListener(async () => {
|
browser.runtime.onStartup.addListener(async () => {
|
||||||
console.log("onStartup");
|
console.log("browser onStartup");
|
||||||
|
|
||||||
// 同步数据
|
// 同步数据
|
||||||
await syncAll();
|
await syncAll();
|
||||||
|
|||||||
@@ -104,6 +104,10 @@ export const I18N = {
|
|||||||
zh: `添加`,
|
zh: `添加`,
|
||||||
en: `Add`,
|
en: `Add`,
|
||||||
},
|
},
|
||||||
|
inject_rules: {
|
||||||
|
zh: `注入内置规则`,
|
||||||
|
en: `Inject Built-in Rules`,
|
||||||
|
},
|
||||||
sync_warn: {
|
sync_warn: {
|
||||||
zh: `如果服务器存在其他客户端同步的数据,第一次同步将直接覆盖本地配置,后面则根据修改时间,新的覆盖旧的。`,
|
zh: `如果服务器存在其他客户端同步的数据,第一次同步将直接覆盖本地配置,后面则根据修改时间,新的覆盖旧的。`,
|
||||||
en: `If the server has data synchronized by other clients, the first synchronization will directly overwrite the local configuration, and later, according to the modification time, the new one will overwrite the old one.`,
|
en: `If the server has data synchronized by other clients, the first synchronization will directly overwrite the local configuration, and later, according to the modification time, the new one will overwrite the old one.`,
|
||||||
@@ -153,8 +157,8 @@ export const I18N = {
|
|||||||
en: `Multiple URLs can be separated by English commas ","`,
|
en: `Multiple URLs can be separated by English commas ","`,
|
||||||
},
|
},
|
||||||
selector_helper: {
|
selector_helper: {
|
||||||
zh: `1、遵循CSS选择器规则,但不同浏览器,支持写法不尽相同。2、留空表示采用全局设置。`,
|
zh: `1、遵循CSS选择器规则。2、留空表示采用全局设置。`,
|
||||||
en: `1. Follow CSS selector rules, but different browsers support different writing methods. 2. Leave blank to adopt the global setting.`,
|
en: `1. Follow CSS selector rules. 2. Leave blank to adopt the global setting.`,
|
||||||
},
|
},
|
||||||
translate_switch: {
|
translate_switch: {
|
||||||
zh: `开启翻译`,
|
zh: `开启翻译`,
|
||||||
|
|||||||
@@ -161,6 +161,7 @@ export const DEFAULT_SETTING = {
|
|||||||
fetchLimit: DEFAULT_FETCH_LIMIT, // 最大任务数量
|
fetchLimit: DEFAULT_FETCH_LIMIT, // 最大任务数量
|
||||||
fetchInterval: DEFAULT_FETCH_INTERVAL, // 任务间隔时间
|
fetchInterval: DEFAULT_FETCH_INTERVAL, // 任务间隔时间
|
||||||
clearCache: false, // 是否在浏览器下次启动时清除缓存
|
clearCache: false, // 是否在浏览器下次启动时清除缓存
|
||||||
|
injectRules: true, // 是否注入内置规则
|
||||||
googleUrl: "https://translate.googleapis.com/translate_a/single", // 谷歌翻译接口
|
googleUrl: "https://translate.googleapis.com/translate_a/single", // 谷歌翻译接口
|
||||||
openaiUrl: "https://api.openai.com/v1/chat/completions",
|
openaiUrl: "https://api.openai.com/v1/chat/completions",
|
||||||
openaiKey: "",
|
openaiKey: "",
|
||||||
@@ -169,14 +170,20 @@ export const DEFAULT_SETTING = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const DEFAULT_RULES = [
|
export const DEFAULT_RULES = [
|
||||||
...RULES.map((item) => ({
|
{
|
||||||
...DEFAULT_RULE,
|
...DEFAULT_RULE,
|
||||||
...item,
|
...RULES[0],
|
||||||
transOpen: "true",
|
transOpen: "true",
|
||||||
})),
|
},
|
||||||
GLOBLA_RULE,
|
GLOBLA_RULE,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const BUILTIN_RULES = RULES.map((item) => ({
|
||||||
|
...DEFAULT_RULE,
|
||||||
|
...item,
|
||||||
|
transOpen: "true",
|
||||||
|
}));
|
||||||
|
|
||||||
export const TRANS_MIN_LENGTH = 5; // 最短翻译长度
|
export const TRANS_MIN_LENGTH = 5; // 最短翻译长度
|
||||||
export const TRANS_MAX_LENGTH = 5000; // 最长翻译长度
|
export const TRANS_MAX_LENGTH = 5000; // 最长翻译长度
|
||||||
|
|
||||||
|
|||||||
@@ -4,13 +4,17 @@ export const DEFAULT_SELECTOR = `:is(${els})`;
|
|||||||
|
|
||||||
export const RULES = [
|
export const RULES = [
|
||||||
{
|
{
|
||||||
pattern: `bearblog.dev, www.theverge.com, www.tampermonkey.net/documentation.php`,
|
pattern: `www.google.com/search`,
|
||||||
selector: DEFAULT_SELECTOR,
|
selector: `h3, .IsZvec, .VwiC3b`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
pattern: `https://news.google.com/`,
|
pattern: `https://news.google.com/`,
|
||||||
selector: `h4`,
|
selector: `h4`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
pattern: `bearblog.dev, www.theverge.com, www.tampermonkey.net/documentation.php`,
|
||||||
|
selector: DEFAULT_SELECTOR,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
pattern: `themessenger.com`,
|
pattern: `themessenger.com`,
|
||||||
selector: `.leading-tight, .leading-tighter, .my-2 p, .font-body p, article ${DEFAULT_SELECTOR}`,
|
selector: `.leading-tight, .leading-tighter, .my-2 p, .font-body p, article ${DEFAULT_SELECTOR}`,
|
||||||
@@ -117,7 +121,7 @@ export const RULES = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
pattern: `https://github.com/`,
|
pattern: `https://github.com/`,
|
||||||
selector: `.markdown-body ${DEFAULT_SELECTOR}, .repo-description p, .Layout-sidebar .f4, .container-lg .py-4 .f5, .container-lg .my-4 .f5, .Box-row .pr-4, .Box-row article .mt-1, [itemprop='description']`,
|
selector: `.markdown-body ${DEFAULT_SELECTOR}, .repo-description p, .Layout-sidebar .f4, .container-lg .py-4 .f5, .container-lg .my-4 .f5, .Box-row .pr-4, .Box-row article .mt-1, [itemprop='description'], .markdown-title, bdi`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
pattern: `twitter.com`,
|
pattern: `twitter.com`,
|
||||||
@@ -127,8 +131,4 @@ export const RULES = [
|
|||||||
pattern: `youtube.com`,
|
pattern: `youtube.com`,
|
||||||
selector: `h1, #video-title, #content-text, #title, yt-attributed-string>span>span`,
|
selector: `h1, #video-title, #content-text, #title, yt-attributed-string>span>span`,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
pattern: `www.google.com/search`,
|
|
||||||
selector: `h3, .IsZvec, .VwiC3b`,
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { Translator } from "./libs/translator";
|
|||||||
(async () => {
|
(async () => {
|
||||||
const setting = await getSetting();
|
const setting = await getSetting();
|
||||||
const rules = await getRules();
|
const rules = await getRules();
|
||||||
const rule = matchRule(rules, document.location.href);
|
const rule = matchRule(rules, document.location.href, setting);
|
||||||
const translator = new Translator(rule, setting);
|
const translator = new Translator(rule, setting);
|
||||||
|
|
||||||
// 监听消息
|
// 监听消息
|
||||||
|
|||||||
@@ -25,11 +25,7 @@ export function useRules() {
|
|||||||
const updateAt = sync.opt?.rulesUpdateAt ? Date.now() : 0;
|
const updateAt = sync.opt?.rulesUpdateAt ? Date.now() : 0;
|
||||||
await storage.setObj(STOKEY_RULES, rules);
|
await storage.setObj(STOKEY_RULES, rules);
|
||||||
await sync.update({ rulesUpdateAt: updateAt });
|
await sync.update({ rulesUpdateAt: updateAt });
|
||||||
try {
|
syncRules();
|
||||||
await syncRules();
|
|
||||||
} catch (err) {
|
|
||||||
console.log("[sync rules]", err);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const add = async (rule) => {
|
const add = async (rule) => {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { STOKEY_SETTING } from "../config";
|
|||||||
import storage from "../libs/storage";
|
import storage from "../libs/storage";
|
||||||
import { useStorages } from "./Storage";
|
import { useStorages } from "./Storage";
|
||||||
import { useSync } from "./Sync";
|
import { useSync } from "./Sync";
|
||||||
|
import { syncSetting } from "../libs/sync";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置hook
|
* 设置hook
|
||||||
@@ -22,5 +23,6 @@ export function useSettingUpdate() {
|
|||||||
const updateAt = sync.opt?.settingUpdateAt ? Date.now() : 0;
|
const updateAt = sync.opt?.settingUpdateAt ? Date.now() : 0;
|
||||||
await storage.putObj(STOKEY_SETTING, obj);
|
await storage.putObj(STOKEY_SETTING, obj);
|
||||||
await sync.update({ settingUpdateAt: updateAt });
|
await sync.update({ settingUpdateAt: updateAt });
|
||||||
|
syncSetting();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import CircularProgress from "@mui/material/CircularProgress";
|
import CircularProgress from "@mui/material/CircularProgress";
|
||||||
|
import Divider from "@mui/material/Divider";
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import Paper from "@mui/material/Paper";
|
import Paper from "@mui/material/Paper";
|
||||||
import { useFetch } from "./hooks/Fetch";
|
import { useFetch } from "./hooks/Fetch";
|
||||||
@@ -12,6 +13,7 @@ function App() {
|
|||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<Paper sx={{ padding: 2, margin: 2 }}>
|
<Paper sx={{ padding: 2, margin: 2 }}>
|
||||||
|
<Divider>{`KISS Translator v${process.env.REACT_APP_VERSION}`}</Divider>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<center>
|
<center>
|
||||||
<CircularProgress />
|
<CircularProgress />
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
STOKEY_FAB,
|
STOKEY_FAB,
|
||||||
GLOBLA_RULE,
|
GLOBLA_RULE,
|
||||||
GLOBAL_KEY,
|
GLOBAL_KEY,
|
||||||
|
BUILTIN_RULES,
|
||||||
} from "../config";
|
} from "../config";
|
||||||
import { browser } from "./browser";
|
import { browser } from "./browser";
|
||||||
|
|
||||||
@@ -52,7 +53,11 @@ export const setFab = async (obj) => await storage.setObj(STOKEY_FAB, obj);
|
|||||||
* @param {string} href
|
* @param {string} href
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const matchRule = (rules, href) => {
|
export const matchRule = (rules, href, { injectRules }) => {
|
||||||
|
if (injectRules) {
|
||||||
|
rules.splice(-1, 0, ...BUILTIN_RULES);
|
||||||
|
}
|
||||||
|
|
||||||
const rule = rules.find((rule) =>
|
const rule = rules.find((rule) =>
|
||||||
rule.pattern.split(",").some((p) => href.includes(p.trim()))
|
rule.pattern.split(",").some((p) => href.includes(p.trim()))
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,62 +13,66 @@ import { apiSyncData } from "../apis";
|
|||||||
const loadOpt = async () => (await storage.getObj(STOKEY_SYNC)) || DEFAULT_SYNC;
|
const loadOpt = async () => (await storage.getObj(STOKEY_SYNC)) || DEFAULT_SYNC;
|
||||||
|
|
||||||
export const syncSetting = async () => {
|
export const syncSetting = async () => {
|
||||||
const { syncUrl, syncKey, settingUpdateAt } = await loadOpt();
|
try {
|
||||||
if (!syncUrl || !syncKey) {
|
const { syncUrl, syncKey, settingUpdateAt } = await loadOpt();
|
||||||
return;
|
if (!syncUrl || !syncKey) {
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const setting = await getSetting();
|
const setting = await getSetting();
|
||||||
const res = await apiSyncData(syncUrl, syncKey, {
|
const res = await apiSyncData(syncUrl, syncKey, {
|
||||||
key: KV_SETTING_KEY,
|
key: KV_SETTING_KEY,
|
||||||
value: setting,
|
value: setting,
|
||||||
updateAt: settingUpdateAt,
|
updateAt: settingUpdateAt,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res && res.updateAt > settingUpdateAt) {
|
if (res && res.updateAt > settingUpdateAt) {
|
||||||
await storage.putObj(STOKEY_SYNC, {
|
await storage.putObj(STOKEY_SYNC, {
|
||||||
settingUpdateAt: res.updateAt,
|
settingUpdateAt: res.updateAt,
|
||||||
settingSyncAt: res.updateAt,
|
settingSyncAt: res.updateAt,
|
||||||
});
|
});
|
||||||
await storage.setObj(STOKEY_SETTING, res.value);
|
await storage.setObj(STOKEY_SETTING, res.value);
|
||||||
} else {
|
} else {
|
||||||
await storage.putObj(STOKEY_SYNC, {
|
await storage.putObj(STOKEY_SYNC, {
|
||||||
settingSyncAt: res.updateAt,
|
settingSyncAt: res.updateAt,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log("[sync setting]", err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const syncRules = async () => {
|
export const syncRules = async () => {
|
||||||
const { syncUrl, syncKey, rulesUpdateAt } = await loadOpt();
|
try {
|
||||||
if (!syncUrl || !syncKey) {
|
const { syncUrl, syncKey, rulesUpdateAt } = await loadOpt();
|
||||||
return;
|
if (!syncUrl || !syncKey) {
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const rules = await getRules();
|
const rules = await getRules();
|
||||||
const res = await apiSyncData(syncUrl, syncKey, {
|
const res = await apiSyncData(syncUrl, syncKey, {
|
||||||
key: KV_RULES_KEY,
|
key: KV_RULES_KEY,
|
||||||
value: rules,
|
value: rules,
|
||||||
updateAt: rulesUpdateAt,
|
updateAt: rulesUpdateAt,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res && res.updateAt > rulesUpdateAt) {
|
if (res && res.updateAt > rulesUpdateAt) {
|
||||||
await storage.putObj(STOKEY_SYNC, {
|
await storage.putObj(STOKEY_SYNC, {
|
||||||
rulesUpdateAt: res.updateAt,
|
rulesUpdateAt: res.updateAt,
|
||||||
rulesSyncAt: res.updateAt,
|
rulesSyncAt: res.updateAt,
|
||||||
});
|
});
|
||||||
await storage.setObj(STOKEY_RULES, res.value);
|
await storage.setObj(STOKEY_RULES, res.value);
|
||||||
} else {
|
} else {
|
||||||
await storage.putObj(STOKEY_SYNC, {
|
await storage.putObj(STOKEY_SYNC, {
|
||||||
rulesSyncAt: res.updateAt,
|
rulesSyncAt: res.updateAt,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log("[sync rules]", err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const syncAll = async () => {
|
export const syncAll = async () => {
|
||||||
try {
|
await syncSetting();
|
||||||
await syncSetting();
|
await syncRules();
|
||||||
await syncRules();
|
|
||||||
} catch (err) {
|
|
||||||
console.log("[sync all]", err);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import { Translator } from "./libs/translator";
|
|||||||
// 翻译页面
|
// 翻译页面
|
||||||
const setting = await getSetting();
|
const setting = await getSetting();
|
||||||
const rules = await getRules();
|
const rules = await getRules();
|
||||||
const rule = matchRule(rules, document.location.href);
|
const rule = matchRule(rules, document.location.href, setting);
|
||||||
const translator = new Translator(rule, setting);
|
const translator = new Translator(rule, setting);
|
||||||
|
|
||||||
// 浮球按钮
|
// 浮球按钮
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ const getEdgePosition = (
|
|||||||
edge = "top";
|
edge = "top";
|
||||||
top = 0;
|
top = 0;
|
||||||
}
|
}
|
||||||
|
left = limitNumber(left, 0, windowWidth - width);
|
||||||
|
top = limitNumber(top, 0, windowHeight - height);
|
||||||
return { x: left, y: top, edge, hide: false };
|
return { x: left, y: top, edge, hide: false };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import Box from "@mui/material/Box";
|
|||||||
import Navigator from "./Navigator";
|
import Navigator from "./Navigator";
|
||||||
import Header from "./Header";
|
import Header from "./Header";
|
||||||
import { useTheme } from "@mui/material/styles";
|
import { useTheme } from "@mui/material/styles";
|
||||||
import { syncAll } from "../../libs/sync";
|
|
||||||
|
|
||||||
export default function Layout() {
|
export default function Layout() {
|
||||||
const navWidth = 256;
|
const navWidth = 256;
|
||||||
@@ -21,7 +20,6 @@ export default function Layout() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
syncAll();
|
|
||||||
}, [location]);
|
}, [location]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
OPT_LANGS_TO,
|
OPT_LANGS_TO,
|
||||||
OPT_TRANS_ALL,
|
OPT_TRANS_ALL,
|
||||||
OPT_STYLE_ALL,
|
OPT_STYLE_ALL,
|
||||||
|
BUILTIN_RULES,
|
||||||
} from "../../config";
|
} from "../../config";
|
||||||
import { useState, useRef } from "react";
|
import { useState, useRef } from "react";
|
||||||
import { useI18n } from "../../hooks/I18n";
|
import { useI18n } from "../../hooks/I18n";
|
||||||
@@ -22,6 +23,9 @@ 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 FileDownloadIcon from "@mui/icons-material/FileDownload";
|
||||||
import FileUploadIcon from "@mui/icons-material/FileUpload";
|
import FileUploadIcon from "@mui/icons-material/FileUpload";
|
||||||
|
import { useSetting, useSettingUpdate } from "../../hooks/Setting";
|
||||||
|
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||||
|
import Switch from "@mui/material/Switch";
|
||||||
|
|
||||||
function RuleFields({ rule, rules, setShow }) {
|
function RuleFields({ rule, rules, setShow }) {
|
||||||
const initFormValues = rule || { ...DEFAULT_RULE, transOpen: "true" };
|
const initFormValues = rule || { ...DEFAULT_RULE, transOpen: "true" };
|
||||||
@@ -122,6 +126,7 @@ function RuleFields({ rule, rules, setShow }) {
|
|||||||
disabled={rule?.pattern === "*" || disabled}
|
disabled={rule?.pattern === "*" || disabled}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
|
multiline
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
size="small"
|
size="small"
|
||||||
@@ -134,8 +139,6 @@ function RuleFields({ rule, rules, setShow }) {
|
|||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
multiline
|
multiline
|
||||||
minRows={2}
|
|
||||||
maxRows={10}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
@@ -246,61 +249,91 @@ function RuleFields({ rule, rules, setShow }) {
|
|||||||
</Grid>
|
</Grid>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{editMode ? (
|
{rules &&
|
||||||
// 编辑
|
(editMode ? (
|
||||||
<Stack direction="row" spacing={2}>
|
// 编辑
|
||||||
{disabled ? (
|
<Stack direction="row" spacing={2}>
|
||||||
<>
|
{disabled ? (
|
||||||
<Button
|
<>
|
||||||
size="small"
|
<Button
|
||||||
variant="contained"
|
size="small"
|
||||||
onClick={(e) => {
|
variant="contained"
|
||||||
e.preventDefault();
|
onClick={(e) => {
|
||||||
setDisabled(false);
|
e.preventDefault();
|
||||||
}}
|
setDisabled(false);
|
||||||
>
|
}}
|
||||||
{i18n("edit")}
|
>
|
||||||
</Button>
|
{i18n("edit")}
|
||||||
{rule?.pattern !== "*" && (
|
</Button>
|
||||||
|
{rule?.pattern !== "*" && (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
rules.del(rule.pattern);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{i18n("delete")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Button size="small" variant="contained" type="submit">
|
||||||
|
{i18n("save")}
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onClick={(e) => {
|
onClick={handleCancel}
|
||||||
e.preventDefault();
|
|
||||||
rules.del(rule.pattern);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{i18n("delete")}
|
{i18n("cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
</>
|
||||||
</>
|
)}
|
||||||
) : (
|
</Stack>
|
||||||
<>
|
) : (
|
||||||
<Button size="small" variant="contained" type="submit">
|
// 添加
|
||||||
{i18n("save")}
|
<Stack direction="row" spacing={2}>
|
||||||
</Button>
|
<Button size="small" variant="contained" type="submit">
|
||||||
<Button size="small" variant="outlined" onClick={handleCancel}>
|
{i18n("save")}
|
||||||
{i18n("cancel")}
|
</Button>
|
||||||
</Button>
|
<Button size="small" variant="outlined" onClick={handleCancel}>
|
||||||
</>
|
{i18n("cancel")}
|
||||||
)}
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
) : (
|
))}
|
||||||
// 添加
|
|
||||||
<Stack direction="row" spacing={2}>
|
|
||||||
<Button size="small" variant="contained" type="submit">
|
|
||||||
{i18n("save")}
|
|
||||||
</Button>
|
|
||||||
<Button size="small" variant="outlined" onClick={handleCancel}>
|
|
||||||
{i18n("cancel")}
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function RuleAccordion({ rule, rules }) {
|
||||||
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
|
||||||
|
const handleChange = (e) => {
|
||||||
|
setExpanded((pre) => !pre);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Accordion expanded={expanded} onChange={handleChange}>
|
||||||
|
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||||
|
<Typography
|
||||||
|
style={{
|
||||||
|
opacity: rules ? 1 : 0.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{rule.pattern}
|
||||||
|
</Typography>
|
||||||
|
</AccordionSummary>
|
||||||
|
<AccordionDetails>
|
||||||
|
{expanded && <RuleFields rule={rule} rules={rules} />}
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function DownloadButton({ data, text, fileName }) {
|
function DownloadButton({ data, text, fileName }) {
|
||||||
const handleClick = (e) => {
|
const handleClick = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -355,6 +388,9 @@ export default function Rules() {
|
|||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const rules = useRules();
|
const rules = useRules();
|
||||||
const [showAdd, setShowAdd] = useState(false);
|
const [showAdd, setShowAdd] = useState(false);
|
||||||
|
const setting = useSetting();
|
||||||
|
const updateSetting = useSettingUpdate();
|
||||||
|
const injectRules = !!setting?.injectRules;
|
||||||
|
|
||||||
const handleImport = (e) => {
|
const handleImport = (e) => {
|
||||||
const file = e.target.files[0];
|
const file = e.target.files[0];
|
||||||
@@ -378,6 +414,12 @@ export default function Rules() {
|
|||||||
reader.readAsText(file);
|
reader.readAsText(file);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleInject = () => {
|
||||||
|
updateSetting({
|
||||||
|
injectRules: !injectRules,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Stack spacing={3}>
|
<Stack spacing={3}>
|
||||||
@@ -399,22 +441,34 @@ export default function Rules() {
|
|||||||
data={JSON.stringify([...rules.list].reverse(), null, "\t")}
|
data={JSON.stringify([...rules.list].reverse(), null, "\t")}
|
||||||
text={i18n("export")}
|
text={i18n("export")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Switch
|
||||||
|
size="small"
|
||||||
|
checked={injectRules}
|
||||||
|
onChange={handleInject}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={i18n("inject_rules")}
|
||||||
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
{showAdd && <RuleFields rules={rules} setShow={setShowAdd} />}
|
{showAdd && <RuleFields rules={rules} setShow={setShowAdd} />}
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
{rules.list.map((rule) => (
|
{rules.list.map((rule) => (
|
||||||
<Accordion key={rule.pattern}>
|
<RuleAccordion key={rule.pattern} rule={rule} rules={rules} />
|
||||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
|
||||||
<Typography>{rule.pattern}</Typography>
|
|
||||||
</AccordionSummary>
|
|
||||||
<AccordionDetails>
|
|
||||||
<RuleFields rule={rule} rules={rules} />
|
|
||||||
</AccordionDetails>
|
|
||||||
</Accordion>
|
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{injectRules && (
|
||||||
|
<Box>
|
||||||
|
{BUILTIN_RULES.map((rule) => (
|
||||||
|
<RuleAccordion key={rule.pattern} rule={rule} />
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,15 +6,37 @@ import MenuItem from "@mui/material/MenuItem";
|
|||||||
import FormControl from "@mui/material/FormControl";
|
import FormControl from "@mui/material/FormControl";
|
||||||
import Select from "@mui/material/Select";
|
import Select from "@mui/material/Select";
|
||||||
import { useSetting, useSettingUpdate } from "../../hooks/Setting";
|
import { useSetting, useSettingUpdate } from "../../hooks/Setting";
|
||||||
import { limitNumber } from "../../libs/utils";
|
import { limitNumber, debounce } from "../../libs/utils";
|
||||||
import { useI18n } from "../../hooks/I18n";
|
import { useI18n } from "../../hooks/I18n";
|
||||||
import { UI_LANGS } from "../../config";
|
import { UI_LANGS } from "../../config";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
export default function Settings() {
|
export default function Settings() {
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const setting = useSetting();
|
const setting = useSetting();
|
||||||
const updateSetting = useSettingUpdate();
|
const updateSetting = useSettingUpdate();
|
||||||
|
|
||||||
|
const handleChange = useMemo(
|
||||||
|
() =>
|
||||||
|
debounce((e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
let { name, value } = e.target;
|
||||||
|
switch (name) {
|
||||||
|
case "fetchLimit":
|
||||||
|
value = limitNumber(value, 1, 100);
|
||||||
|
break;
|
||||||
|
case "fetchInterval":
|
||||||
|
value = limitNumber(value, 0, 5000);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
updateSetting({
|
||||||
|
[name]: value,
|
||||||
|
});
|
||||||
|
}, 500),
|
||||||
|
[updateSetting]
|
||||||
|
);
|
||||||
|
|
||||||
if (!setting) {
|
if (!setting) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -37,13 +59,10 @@ export default function Settings() {
|
|||||||
<FormControl size="small">
|
<FormControl size="small">
|
||||||
<InputLabel>{i18n("ui_lang")}</InputLabel>
|
<InputLabel>{i18n("ui_lang")}</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
|
name="uiLang"
|
||||||
value={uiLang}
|
value={uiLang}
|
||||||
label={i18n("ui_lang")}
|
label={i18n("ui_lang")}
|
||||||
onChange={(e) => {
|
onChange={handleChange}
|
||||||
updateSetting({
|
|
||||||
uiLang: e.target.value,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{UI_LANGS.map(([lang, name]) => (
|
{UI_LANGS.map(([lang, name]) => (
|
||||||
<MenuItem key={lang} value={lang}>
|
<MenuItem key={lang} value={lang}>
|
||||||
@@ -57,36 +76,27 @@ export default function Settings() {
|
|||||||
size="small"
|
size="small"
|
||||||
label={i18n("fetch_limit")}
|
label={i18n("fetch_limit")}
|
||||||
type="number"
|
type="number"
|
||||||
|
name="fetchLimit"
|
||||||
defaultValue={fetchLimit}
|
defaultValue={fetchLimit}
|
||||||
onChange={(e) => {
|
onChange={handleChange}
|
||||||
updateSetting({
|
|
||||||
fetchLimit: limitNumber(e.target.value, 1, 100),
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
size="small"
|
size="small"
|
||||||
label={i18n("fetch_interval")}
|
label={i18n("fetch_interval")}
|
||||||
type="number"
|
type="number"
|
||||||
|
name="fetchInterval"
|
||||||
defaultValue={fetchInterval}
|
defaultValue={fetchInterval}
|
||||||
onChange={(e) => {
|
onChange={handleChange}
|
||||||
updateSetting({
|
|
||||||
fetchInterval: limitNumber(e.target.value, 0, 5000),
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormControl size="small">
|
<FormControl size="small">
|
||||||
<InputLabel>{i18n("clear_cache")}</InputLabel>
|
<InputLabel>{i18n("clear_cache")}</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
|
name="clearCache"
|
||||||
value={clearCache}
|
value={clearCache}
|
||||||
label={i18n("clear_cache")}
|
label={i18n("clear_cache")}
|
||||||
onChange={(e) => {
|
onChange={handleChange}
|
||||||
updateSetting({
|
|
||||||
clearCache: e.target.value,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<MenuItem value={false}>{i18n("clear_cache_never")}</MenuItem>
|
<MenuItem value={false}>{i18n("clear_cache_never")}</MenuItem>
|
||||||
<MenuItem value={true}>{i18n("clear_cache_restart")}</MenuItem>
|
<MenuItem value={true}>{i18n("clear_cache_restart")}</MenuItem>
|
||||||
@@ -96,60 +106,43 @@ export default function Settings() {
|
|||||||
<TextField
|
<TextField
|
||||||
size="small"
|
size="small"
|
||||||
label={i18n("google_api")}
|
label={i18n("google_api")}
|
||||||
|
name="googleUrl"
|
||||||
defaultValue={googleUrl}
|
defaultValue={googleUrl}
|
||||||
onChange={(e) => {
|
onChange={handleChange}
|
||||||
updateSetting({
|
|
||||||
googleUrl: e.target.value,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
size="small"
|
size="small"
|
||||||
label={i18n("openai_api")}
|
label={i18n("openai_api")}
|
||||||
|
name="openaiUrl"
|
||||||
defaultValue={openaiUrl}
|
defaultValue={openaiUrl}
|
||||||
onChange={(e) => {
|
onChange={handleChange}
|
||||||
updateSetting({
|
|
||||||
openaiUrl: e.target.value,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
size="small"
|
size="small"
|
||||||
type="password"
|
type="password"
|
||||||
label={i18n("openai_key")}
|
label={i18n("openai_key")}
|
||||||
|
name="openaiKey"
|
||||||
defaultValue={openaiKey}
|
defaultValue={openaiKey}
|
||||||
onChange={(e) => {
|
onChange={handleChange}
|
||||||
updateSetting({
|
|
||||||
openaiKey: e.target.value,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
size="small"
|
size="small"
|
||||||
label={i18n("openai_model")}
|
label={i18n("openai_model")}
|
||||||
|
name="openaiModel"
|
||||||
defaultValue={openaiModel}
|
defaultValue={openaiModel}
|
||||||
onChange={(e) => {
|
onChange={handleChange}
|
||||||
updateSetting({
|
|
||||||
openaiModel: e.target.value,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
size="small"
|
size="small"
|
||||||
label={i18n("openai_prompt")}
|
label={i18n("openai_prompt")}
|
||||||
|
name="openaiPrompt"
|
||||||
defaultValue={openaiPrompt}
|
defaultValue={openaiPrompt}
|
||||||
onChange={(e) => {
|
onChange={handleChange}
|
||||||
updateSetting({
|
|
||||||
openaiPrompt: e.target.value,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
multiline
|
multiline
|
||||||
minRows={2}
|
|
||||||
maxRows={10}
|
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -3,23 +3,33 @@ import Stack from "@mui/material/Stack";
|
|||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
import { useI18n } from "../../hooks/I18n";
|
import { useI18n } from "../../hooks/I18n";
|
||||||
import { useSync } from "../../hooks/Sync";
|
import { useSync } from "../../hooks/Sync";
|
||||||
import { syncAll } from "../../libs/sync";
|
|
||||||
import Alert from "@mui/material/Alert";
|
import Alert from "@mui/material/Alert";
|
||||||
import Link from "@mui/material/Link";
|
import Link from "@mui/material/Link";
|
||||||
import { URL_KISS_WORKER } from "../../config";
|
import { URL_KISS_WORKER } from "../../config";
|
||||||
|
import { debounce } from "../../libs/utils";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
export default function SyncSetting() {
|
export default function SyncSetting() {
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const sync = useSync();
|
const sync = useSync();
|
||||||
|
|
||||||
|
const handleChange = useMemo(
|
||||||
|
() =>
|
||||||
|
debounce((e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const { name, value } = e.target;
|
||||||
|
sync.update({
|
||||||
|
[name]: value,
|
||||||
|
});
|
||||||
|
}, 500),
|
||||||
|
[sync]
|
||||||
|
);
|
||||||
|
|
||||||
if (!sync.opt) {
|
if (!sync.opt) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { syncUrl, syncKey } = sync.opt;
|
const { syncUrl, syncKey } = sync.opt;
|
||||||
const handleSyncBlur = () => {
|
|
||||||
syncAll();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
@@ -29,13 +39,9 @@ export default function SyncSetting() {
|
|||||||
<TextField
|
<TextField
|
||||||
size="small"
|
size="small"
|
||||||
label={i18n("data_sync_url")}
|
label={i18n("data_sync_url")}
|
||||||
|
name="syncUrl"
|
||||||
defaultValue={syncUrl}
|
defaultValue={syncUrl}
|
||||||
onChange={(e) => {
|
onChange={handleChange}
|
||||||
sync.update({
|
|
||||||
syncUrl: e.target.value,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
onBlur={handleSyncBlur}
|
|
||||||
helperText={
|
helperText={
|
||||||
<Link href={URL_KISS_WORKER}>{i18n("about_sync_api")}</Link>
|
<Link href={URL_KISS_WORKER}>{i18n("about_sync_api")}</Link>
|
||||||
}
|
}
|
||||||
@@ -45,13 +51,9 @@ export default function SyncSetting() {
|
|||||||
size="small"
|
size="small"
|
||||||
type="password"
|
type="password"
|
||||||
label={i18n("data_sync_key")}
|
label={i18n("data_sync_key")}
|
||||||
|
name="syncKey"
|
||||||
defaultValue={syncKey}
|
defaultValue={syncKey}
|
||||||
onChange={(e) => {
|
onChange={handleChange}
|
||||||
sync.update({
|
|
||||||
syncKey: e.target.value,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
onBlur={handleSyncBlur}
|
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -10,31 +10,34 @@ import { useEffect, useState } from "react";
|
|||||||
import { isGm } from "../../libs/browser";
|
import { isGm } from "../../libs/browser";
|
||||||
import { sleep } from "../../libs/utils";
|
import { sleep } from "../../libs/utils";
|
||||||
import CircularProgress from "@mui/material/CircularProgress";
|
import CircularProgress from "@mui/material/CircularProgress";
|
||||||
|
import { syncAll } from "../../libs/sync";
|
||||||
|
|
||||||
export default function Options() {
|
export default function Options() {
|
||||||
const [error, setError] = useState(false);
|
const [error, setError] = useState(false);
|
||||||
const [ready, setReady] = useState(false);
|
const [ready, setReady] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isGm) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
let i = 0;
|
if (isGm) {
|
||||||
for (;;) {
|
// 等待GM注入
|
||||||
if (window.APP_NAME === process.env.REACT_APP_NAME) {
|
let i = 0;
|
||||||
setReady(true);
|
for (;;) {
|
||||||
break;
|
if (window.APP_NAME === process.env.REACT_APP_NAME) {
|
||||||
}
|
setReady(true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if (++i > 8) {
|
if (++i > 8) {
|
||||||
setError(true);
|
setError(true);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
await sleep(1000);
|
await sleep(1000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 同步数据
|
||||||
|
syncAll();
|
||||||
})();
|
})();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user