Compare commits
103 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9ab81a6e4 | ||
|
|
a33b92df89 | ||
|
|
ed1e9f1cf7 | ||
|
|
a0704add61 | ||
|
|
eef55334ab | ||
|
|
80ac0a2778 | ||
|
|
1559d3317a | ||
|
|
4481afcf3b | ||
|
|
2ab10ea667 | ||
|
|
2ff919c8ca | ||
|
|
5494ad61b0 | ||
|
|
43b56b119d | ||
|
|
4eeb272e13 | ||
|
|
ffdc72f6bc | ||
|
|
82c78a0ccf | ||
|
|
7371dd6a32 | ||
|
|
e9ca6f8563 | ||
|
|
a3d1310fef | ||
|
|
d34f805c43 | ||
|
|
e1612aa1d7 | ||
|
|
7bf4ee401b | ||
|
|
cfd86d563b | ||
|
|
1e444db50f | ||
|
|
e6a6201429 | ||
|
|
7e3044a0cf | ||
|
|
93a51282b6 | ||
|
|
ea9e20931a | ||
|
|
7c0ea49fb1 | ||
|
|
476dd008cd | ||
|
|
f37890e59e | ||
|
|
de2ab8f506 | ||
|
|
274245f8ff | ||
|
|
7dd3afddff | ||
|
|
15138866c8 | ||
|
|
828803e618 | ||
|
|
e0c6c0e02a | ||
|
|
b40ac410a2 | ||
|
|
0fcaaafa81 | ||
|
|
98fabed547 | ||
|
|
e738d6a90a | ||
|
|
b55e67e8f2 | ||
|
|
cba1689206 | ||
|
|
59a83bdb12 | ||
|
|
35e0646f7f | ||
|
|
f1fc11c81d | ||
|
|
001b22f9e6 | ||
|
|
daf3a10ff6 | ||
|
|
de3e1fe8b0 | ||
|
|
3e4ea02b87 | ||
|
|
fe5e1b0bcd | ||
|
|
45390b82e5 | ||
|
|
0378be64ba | ||
|
|
f252c91c69 | ||
|
|
110da4c67f | ||
|
|
fb9f9ffa4c | ||
|
|
d833c6ada7 | ||
|
|
cfa98eee91 | ||
|
|
c105e2e7e3 | ||
|
|
9aad0bca35 | ||
|
|
8ea071edd5 | ||
|
|
5f2005420e | ||
|
|
9732e682c0 | ||
|
|
0eeb0e2fe0 | ||
|
|
33db14c17c | ||
|
|
c388582d72 | ||
|
|
278c768d24 | ||
|
|
dd4892cbe6 | ||
|
|
9a728ab537 | ||
|
|
1ec433e1b1 | ||
|
|
794376a653 | ||
|
|
30e958764d | ||
|
|
cde6f34286 | ||
|
|
6d87fbc0a4 | ||
|
|
db1f5d22b8 | ||
|
|
486d5244d8 | ||
|
|
2818fae6af | ||
|
|
cb3a6ceb2c | ||
|
|
96c6948c6a | ||
|
|
b1a5f7773b | ||
|
|
5c7838b4ee | ||
|
|
747ed2dbb7 | ||
|
|
a9dc863e97 | ||
|
|
7028723eb1 | ||
|
|
d8f0257d6b | ||
|
|
205d9e8983 | ||
|
|
eb521a9274 | ||
|
|
790d227d67 | ||
|
|
e616321382 | ||
|
|
14132c260a | ||
|
|
3a6da84e88 | ||
|
|
17febed302 | ||
|
|
018b8a273f | ||
|
|
3c27f5f7c8 | ||
|
|
14109a966c | ||
|
|
f575e0f104 | ||
|
|
6773171b9e | ||
|
|
31acbf5e25 | ||
|
|
77377578ad | ||
|
|
1af5aca88c | ||
|
|
83b76175b4 | ||
|
|
509bf41ccd | ||
|
|
4efd43b329 | ||
|
|
1ac71be2ce |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -13,7 +13,7 @@ dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
# .vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
@@ -22,3 +22,5 @@ dist-ssr
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# **/buckup
|
||||
|
||||
8
.prettierrc
Normal file
8
.prettierrc
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"printWidth": 100,
|
||||
"trailingComma": "all",
|
||||
"quoteProps": "preserve",
|
||||
"jsxSingleQuote": false,
|
||||
"singleQuote": false,
|
||||
"singleAttributePerLine": false
|
||||
}
|
||||
5
.vscode/settings.json
vendored
Normal file
5
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"editor.formatOnSaveMode": "modificationsIfAvailable",
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"typescript.tsdk": "./node_modules/typescript/lib"
|
||||
}
|
||||
30
README.md
30
README.md
@@ -1,28 +1,12 @@
|
||||
**userscript**
|
||||
|
||||
vite-plugin-monkey + preact
|
||||
|
||||
```sh
|
||||
pnpm i
|
||||
pnpm dev
|
||||
|
||||
pnpm build
|
||||
pnpm preview
|
||||
```
|
||||
|
||||
### todo
|
||||
|
||||
- 删减列表
|
||||
|
||||
- 演员头像,偷 bus
|
||||
```html
|
||||
<img src="/pics/actress/sl1_a.jpg" title="河北彩花">
|
||||
```
|
||||
|
||||
- 移动端 样式改动(panel 的阴影、圆角等)
|
||||
|
||||
- 更多域名
|
||||
|
||||
- lib 弹窗 zindex
|
||||
|
||||
- hook buttons
|
||||
```js
|
||||
const rbu = document.querySelector(`a[href="#magnet-links"]`) as HTMLElement;
|
||||
console.log(rbu);
|
||||
const rbuRef = useRef<HTMLElement>(rbu);
|
||||
rbuRef.current.click();
|
||||
```
|
||||
|
||||
|
||||
1681
dist/jop.user.js
vendored
1681
dist/jop.user.js
vendored
File diff suppressed because it is too large
Load Diff
13
index.html
13
index.html
@@ -1,13 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0"
|
||||
/>
|
||||
<title>jop</title>
|
||||
<!-- /index.html 必须存在, 否则 vite 无法注入 hmr 代码, plugin-monkey 以及其他插件 也无法注入相关辅助代码 -->
|
||||
<!-- /index.html must exist, if not, vite will not inject hmr code, plugin-monkey and others will not inject theirs related auxiliary code -->
|
||||
</head>
|
||||
</html>
|
||||
12
package.json
12
package.json
@@ -5,15 +5,17 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"preview": "vite preview",
|
||||
"build": "tsc && vite build"
|
||||
},
|
||||
"dependencies": {
|
||||
"preact": "10.11.0"
|
||||
"@types/node": "^22.14.1",
|
||||
"preact": "10.25.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@preact/preset-vite": "^2.4.0",
|
||||
"typescript": "^4.8.4",
|
||||
"vite": "^3.1.4",
|
||||
"vite-plugin-monkey": "^2.5.1"
|
||||
"@preact/preset-vite": "^2.10.1",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^6.3.2",
|
||||
"vite-plugin-monkey": "^5.0.8"
|
||||
}
|
||||
}
|
||||
|
||||
2091
pnpm-lock.yaml
generated
2091
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
63
script/urlConfig.ts
Normal file
63
script/urlConfig.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { siteList } from "../src/utils/siteList";
|
||||
|
||||
const cleanUrl = (url: string): string => {
|
||||
const domainMatch = url.match(/^(?:https?:\/\/)?(?:www\.)?([^\/]+)/i);
|
||||
if (!domainMatch) return "";
|
||||
const domain = domainMatch[1];
|
||||
return `*://*.${domain}/*`;
|
||||
};
|
||||
|
||||
const getLibMirror = async () => {
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
const user = "javlibcom";
|
||||
const res = await fetch(`https://api.github.com/users/${user}`);
|
||||
const data = await res.json();
|
||||
return cleanUrl(data.blog ?? "");
|
||||
} catch (error) {
|
||||
console.warn("Error fetching lib mirror:", error);
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
const getDbMirrors = async () => {
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
const res = await fetch(`https://javdb.com/`);
|
||||
const htmlText = await res.text();
|
||||
|
||||
const navMatch = htmlText.match(
|
||||
/<nav[^>]*class="[^"]*sub-header[^"]*"[^>]*>([\s\S]*?)<\/nav>/i,
|
||||
);
|
||||
if (!navMatch) return [];
|
||||
|
||||
const hrefRegex = /href="([^"]+)"/g;
|
||||
return [...navMatch[1].matchAll(hrefRegex)].map((match) => cleanUrl(match[1])).filter(Boolean);
|
||||
} catch (error) {
|
||||
console.warn("Error fetching db mirror:", error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const libMirrorUrl = await getLibMirror();
|
||||
const dbMirrorUrls = await getDbMirrors();
|
||||
|
||||
const connectList = siteList.map((site) => site.hostname);
|
||||
|
||||
const includeList = [
|
||||
/^https?:\/\/(\w*\.)?javdb(\d)*\.com\/v.*$/,
|
||||
/^https?:\/\/(\w*\.)?(javbus|seejav|javsee)*\.(com|cc|me|life|bid).*$/,
|
||||
/^https?:\/\/(\w*\.)?javlibrary\.com.*$/,
|
||||
/^http.*\/cn\/\?v=jav.*$/,
|
||||
];
|
||||
|
||||
const mirrorList = [...dbMirrorUrls, libMirrorUrl].filter(Boolean);
|
||||
export default {
|
||||
match: [...mirrorList],
|
||||
connect: connectList,
|
||||
include: includeList,
|
||||
};
|
||||
@@ -1,31 +0,0 @@
|
||||
import { createButtonNode, createPanel } from "./utils/createNode";
|
||||
import { matchList } from "./utils/matchList";
|
||||
import { siteList } from "./utils/siteList";
|
||||
import { xhr } from "./utils/xhr";
|
||||
|
||||
import type { Cms } from "./utils/matchList";
|
||||
|
||||
import "./style.css";
|
||||
|
||||
export async function main() {
|
||||
/** 当前 macth 站点对象 */
|
||||
const cms = matchList.find((item) => item.hostname === window.location.hostname) as Cms;
|
||||
const CODE = getCode(cms.name);
|
||||
if (CODE === undefined) return;
|
||||
|
||||
const panel = createPanel(cms);
|
||||
|
||||
/** 禁用部分 */
|
||||
const envSiteList = siteList.filter((item) => item.disable !== cms.name);
|
||||
envSiteList.forEach(async (siteItem) => {
|
||||
const siteUrl = siteItem.url.replace("{{code}}", CODE);
|
||||
|
||||
const { setButtonStatus } = createButtonNode(panel, siteItem.name, siteUrl);
|
||||
|
||||
const { isSuccess, hasLeakage, hasSubtitle, targetLink } = await xhr(siteItem, siteUrl, CODE);
|
||||
|
||||
setButtonStatus(targetLink, isSuccess ? "green" : "red", hasLeakage, hasSubtitle);
|
||||
});
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -1,81 +1,58 @@
|
||||
import { memo } from "preact/compat";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import xhr from "../utils/xhr";
|
||||
import { Cms } from "../utils/matchList";
|
||||
import { siteList } from "../utils/siteList";
|
||||
import Info, { Infos } from "./Info";
|
||||
import SiteButton from "./SiteButton";
|
||||
import { GM_getValue } from "vite-plugin-monkey/dist/client";
|
||||
import Top from "./Top";
|
||||
|
||||
export type RenderSiteItem = {
|
||||
name: string;
|
||||
targetLink: string;
|
||||
status: {
|
||||
isSuccess: "pedding" | "rejected" | "fulfilled";
|
||||
hasSubtitle: boolean;
|
||||
hasLeakage: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
const App = memo(function ({ cms, infos, CODE }: { cms: Cms; infos: Infos; CODE: string }) {
|
||||
// !todo hook buttons
|
||||
// const rbu = document.querySelector(`a[href="#magnet-links"]`) as HTMLElement;
|
||||
// console.log(rbu);
|
||||
// const rbuRef = useRef<HTMLElement>(rbu);
|
||||
// rbuRef.current.click();
|
||||
|
||||
const gmShowPanel = GM_getValue("setting", { gmShowPanel: true }).gmShowPanel;
|
||||
|
||||
const [showPanel, setShowPanel] = useState(gmShowPanel);
|
||||
|
||||
/** 禁用 disable */
|
||||
const initSiteList = siteList.filter((item) => item.disable !== cms.name);
|
||||
|
||||
const [renderSiteList, setRenderSiteList] = useState<RenderSiteItem[]>(
|
||||
initSiteList.map((item) => ({
|
||||
name: item.name,
|
||||
targetLink: item.url.replace("{{code}}", CODE),
|
||||
status: { isSuccess: "pedding", hasSubtitle: false, hasLeakage: false },
|
||||
})),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
initSiteList.forEach(async (siteItem, index) => {
|
||||
const targetLink = siteItem.url.replace("{{code}}", CODE);
|
||||
const result = await xhr(siteItem, targetLink, CODE);
|
||||
renderSiteList[index] = {
|
||||
name: siteItem.name,
|
||||
targetLink,
|
||||
status: {
|
||||
isSuccess: result.isSuccess ? "fulfilled" : "rejected",
|
||||
hasLeakage: result.hasLeakage,
|
||||
hasSubtitle: result.hasSubtitle,
|
||||
},
|
||||
};
|
||||
setRenderSiteList([...renderSiteList]);
|
||||
});
|
||||
}, [xhr, setRenderSiteList]);
|
||||
|
||||
// }, []);
|
||||
return (
|
||||
<>
|
||||
{showPanel && (
|
||||
<div className="jop-panel">
|
||||
<Info infos={infos} />
|
||||
<div className="jop-list">
|
||||
{renderSiteList.map((item) => {
|
||||
return <SiteButton itemData={item} />;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Top
|
||||
showPanel={showPanel}
|
||||
setShowPanel={setShowPanel}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default App;
|
||||
import { memo, useState } from "preact/compat";
|
||||
import Setting from "./Setting";
|
||||
import SiteBtn from "./SiteBtn";
|
||||
import { GM_getValue, GM_setValue } from "$";
|
||||
import { SiteItem, siteList } from "@/utils/siteList";
|
||||
import type { LibItem } from "@/utils/libSites";
|
||||
|
||||
const App = memo(function ({ libItem, CODE }: { libItem: LibItem; CODE: string }) {
|
||||
// 默认不显示
|
||||
const DEF_DIS = [
|
||||
...["AvJoy", "baihuse", "GGJAV", "AV01", "18sex", "highporn", "evojav", "HAYAV"],
|
||||
...["JavBus", "JavDB", "JAVLib", "MISSAV_", "123av", "javhub", "javgo", "JAVMENU"],
|
||||
];
|
||||
const [disables, setDisables] = useState(GM_getValue<SiteItem["name"][]>("disable", DEF_DIS));
|
||||
const [multipleNavi, setMultipleNavi] = useState(GM_getValue<boolean>("multipleNavi", true));
|
||||
const [hiddenError, setHiddenError] = useState(GM_getValue<boolean>("hiddenError", false));
|
||||
|
||||
const list = siteList.filter(
|
||||
(siteItem) => !disables.includes(siteItem.name) && !siteItem.hostname.includes(libItem.name),
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div class="jop-list">
|
||||
{list.map((siteItem) => (
|
||||
<SiteBtn
|
||||
siteItem={siteItem}
|
||||
CODE={CODE}
|
||||
key={siteItem.name}
|
||||
multipleNavi={multipleNavi}
|
||||
hiddenError={hiddenError}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Setting
|
||||
siteList={siteList}
|
||||
setDisables={(disable) => {
|
||||
setDisables(disable);
|
||||
GM_setValue("disable", disable);
|
||||
}}
|
||||
multipleNavi={multipleNavi}
|
||||
setMultipleNavi={(multipleNavi) => {
|
||||
setMultipleNavi(multipleNavi);
|
||||
GM_setValue("multipleNavi", multipleNavi);
|
||||
}}
|
||||
disables={disables}
|
||||
hiddenError={hiddenError}
|
||||
setHiddenError={(v) => {
|
||||
setHiddenError(v);
|
||||
GM_setValue("hiddenError", v);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default App;
|
||||
|
||||
31
src/components/Checkbox.tsx
Normal file
31
src/components/Checkbox.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import Tooltip from "./Tooltip";
|
||||
|
||||
interface CheckboxProps {
|
||||
label: string;
|
||||
value: boolean;
|
||||
tip?: string;
|
||||
onChange: (checked: boolean) => void;
|
||||
}
|
||||
|
||||
const Checkbox = ({ label, value, tip, onChange }: CheckboxProps): JSX.Element => {
|
||||
const handleChange = (event: JSX.TargetedEvent<HTMLInputElement>) => {
|
||||
onChange(event.currentTarget.checked);
|
||||
};
|
||||
|
||||
return (
|
||||
<label className="jop-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="jop-checkbox-input"
|
||||
checked={value}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<span className="jop-checkbox-custom"></span>
|
||||
<Tooltip content={tip || ""}>
|
||||
<span className="jop-checkbox-label">{label}</span>
|
||||
</Tooltip>
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
export default Checkbox;
|
||||
@@ -1,2 +0,0 @@
|
||||
// !todo
|
||||
export {};
|
||||
@@ -1,40 +0,0 @@
|
||||
import { memo } from "preact/compat";
|
||||
|
||||
/** 从原 info panel 抄一点精简的信息 */
|
||||
export type Infos = {
|
||||
codeText: string;
|
||||
actorList: {
|
||||
text: string;
|
||||
link: string;
|
||||
// avatart:string
|
||||
}[];
|
||||
};
|
||||
|
||||
const Info = memo(({ infos }: { infos: Infos }) => {
|
||||
return (
|
||||
<div className="jop-info">
|
||||
<span
|
||||
className="jop-info-code"
|
||||
title="点击复制"
|
||||
onClick={() => navigator.clipboard.writeText(infos.codeText)}
|
||||
>
|
||||
{infos.codeText}
|
||||
</span>
|
||||
|
||||
<div class="jop-info-actor">
|
||||
<a
|
||||
class="jop-info-actor-item"
|
||||
target="_blank"
|
||||
href={infos.actorList[0].link}
|
||||
>
|
||||
{infos.actorList[0].text}
|
||||
</a>
|
||||
<span> 等</span>
|
||||
</div>
|
||||
|
||||
{/* {info.actorList.map((item, index) => { const length = info.actorList.length; return ( <a href={item.link} style={{ paddingRight: length !== 1 && index !== length - 1 ? 16 : 0 }} > {item.text} </a> ); })} */}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default Info;
|
||||
106
src/components/Setting.tsx
Normal file
106
src/components/Setting.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import { Dispatch, StateUpdater, useState } from "preact/hooks";
|
||||
import { GM_setValue } from "$";
|
||||
import { SiteItem } from "@/utils/siteList";
|
||||
import Checkbox from "./Checkbox";
|
||||
|
||||
type Props = {
|
||||
siteList: SiteItem[];
|
||||
setDisables: Dispatch<StateUpdater<string[]>>;
|
||||
disables: SiteItem["name"][];
|
||||
multipleNavi: boolean;
|
||||
setMultipleNavi: Dispatch<StateUpdater<boolean>>;
|
||||
hiddenError: boolean;
|
||||
setHiddenError: Dispatch<StateUpdater<boolean>>;
|
||||
};
|
||||
|
||||
const Setting = ({
|
||||
siteList,
|
||||
setDisables,
|
||||
disables,
|
||||
multipleNavi,
|
||||
setMultipleNavi,
|
||||
hiddenError,
|
||||
setHiddenError,
|
||||
}: Props) => {
|
||||
const [showSetting, setShowSetting] = useState(false);
|
||||
|
||||
const hanleListChange = (item: SiteItem, isHidden: boolean) => {
|
||||
if (isHidden) {
|
||||
setDisables(disables.filter((disItem) => disItem !== item.name));
|
||||
} else {
|
||||
setDisables([...disables, item.name]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleNaviChange = (checked: boolean) => {
|
||||
setMultipleNavi(checked);
|
||||
GM_setValue("multipleNavi", checked);
|
||||
};
|
||||
|
||||
const handlehiddenErrorChange = (checked: boolean) => {
|
||||
setHiddenError(checked);
|
||||
GM_setValue("hiddenError", checked);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{!showSetting && (
|
||||
<div className="jop-button_def" onClick={() => setShowSetting(!showSetting)}>
|
||||
设置
|
||||
</div>
|
||||
)}
|
||||
{showSetting && (
|
||||
<>
|
||||
<div className="jop-setting">
|
||||
<Group title="勾选默认展示">
|
||||
{siteList.map((item) => {
|
||||
const isHidden = disables.includes(item.name);
|
||||
|
||||
return (
|
||||
<Checkbox
|
||||
label={item.name}
|
||||
value={!isHidden}
|
||||
onChange={(checked) => hanleListChange(item, checked)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Group>
|
||||
|
||||
<Group title="其他设置">
|
||||
<Checkbox
|
||||
label="展示多个搜索结果"
|
||||
value={multipleNavi}
|
||||
tip="一个站点内出现多条匹配结果时,打开后跳转搜索结果页"
|
||||
onChange={handleNaviChange}
|
||||
/>
|
||||
<Checkbox
|
||||
label="隐藏失败结果"
|
||||
value={hiddenError}
|
||||
onChange={handlehiddenErrorChange}
|
||||
/>
|
||||
</Group>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="jop-button_def"
|
||||
onClick={() => {
|
||||
setShowSetting(!showSetting);
|
||||
}}
|
||||
>
|
||||
收起设置
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Group = ({ title, children }: { title: string; children: React.ReactNode }) => {
|
||||
return (
|
||||
<>
|
||||
<h4 className="jop-setting-title">{title}</h4>
|
||||
<div className="jop-setting-list">{children}</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Setting;
|
||||
52
src/components/SiteBtn.tsx
Normal file
52
src/components/SiteBtn.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { fetcher, FetchResult } from "@/utils/xhr";
|
||||
import { SiteItem } from "@/utils/siteList";
|
||||
import { useEffect, useState } from "preact/compat";
|
||||
|
||||
type Props = {
|
||||
siteItem: SiteItem;
|
||||
CODE: string;
|
||||
multipleNavi?: boolean;
|
||||
hiddenError?: boolean;
|
||||
};
|
||||
|
||||
const SiteBtn = ({ siteItem, CODE, multipleNavi, hiddenError }: Props) => {
|
||||
const { name, codeFormater } = siteItem;
|
||||
/** 格式化 CODE */
|
||||
const formatCode = codeFormater ? codeFormater(CODE) : CODE;
|
||||
const originLink = siteItem.url.replace("{{code}}", formatCode);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [fetchRes, setFetchRes] = useState<FetchResult>();
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
fetcher({ siteItem, targetLink: originLink, CODE: formatCode }).then((res) => {
|
||||
setFetchRes(res);
|
||||
setLoading(false);
|
||||
});
|
||||
}, [fetcher, siteItem, CODE, originLink]);
|
||||
|
||||
const multipleFlag = multipleNavi && fetchRes?.multipleRes;
|
||||
const tag = multipleFlag ? "多结果" : fetchRes?.tag;
|
||||
const resultLink = multipleFlag ? originLink : fetchRes?.resultLink;
|
||||
const colorClass = fetchRes?.isSuccess ? "jop-button_green " : "jop-button_red ";
|
||||
|
||||
if (hiddenError && !fetchRes?.isSuccess) {
|
||||
return <></>;
|
||||
}
|
||||
return (
|
||||
<a
|
||||
className={"jop-button " + (loading ? " " : colorClass)}
|
||||
target="_blank"
|
||||
href={!resultLink ? originLink : resultLink}
|
||||
>
|
||||
{tag && <div className="jop-button_label">{tag}</div>}
|
||||
|
||||
{/* 加载动画 */}
|
||||
{/* {isSuccess === "pedding" && <span className="jop-loading"> </span>} */}
|
||||
<span>{name}</span>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
export default SiteBtn;
|
||||
@@ -1,35 +0,0 @@
|
||||
import { memo } from "preact/compat";
|
||||
import { RenderSiteItem } from "./App";
|
||||
|
||||
const SiteButton = memo(({ itemData }: { itemData: RenderSiteItem }) => {
|
||||
const { name, targetLink, status } = itemData;
|
||||
const { isSuccess, hasSubtitle, hasLeakage } = status;
|
||||
// console.log("sitebutton render");
|
||||
const colorClass =
|
||||
isSuccess === "pedding"
|
||||
? " "
|
||||
: isSuccess === "fulfilled"
|
||||
? "jop-button_green "
|
||||
: "jop-button_red ";
|
||||
|
||||
return (
|
||||
<a
|
||||
className={"jop-button " + colorClass}
|
||||
target="_blank"
|
||||
href={targetLink}
|
||||
>
|
||||
{(hasSubtitle || hasLeakage) && (
|
||||
<div className="jop-button_label">
|
||||
{hasSubtitle && <span>字幕 </span>}
|
||||
{hasLeakage && <span> 无码</span>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 加载动画 */}
|
||||
{/* {isSuccess === "pedding" && <span className="jop-loading"> </span>} */}
|
||||
<span>{name}</span>
|
||||
</a>
|
||||
);
|
||||
});
|
||||
|
||||
export default SiteButton;
|
||||
23
src/components/Tooltip.tsx
Normal file
23
src/components/Tooltip.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { useState } from "preact/hooks";
|
||||
|
||||
interface TooltipProps {
|
||||
content: string;
|
||||
children: preact.ComponentChildren;
|
||||
}
|
||||
|
||||
const Tooltip = ({ content, children }: TooltipProps) => {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="jop-tooltip-container"
|
||||
onMouseEnter={() => setIsVisible(true)}
|
||||
onMouseLeave={() => setIsVisible(false)}
|
||||
>
|
||||
{children}
|
||||
{isVisible && content && <div className="jop-tooltip">{content}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tooltip;
|
||||
@@ -1,197 +0,0 @@
|
||||
import { GM_getValue, GM_setValue } from "$";
|
||||
import { useState } from "preact/hooks";
|
||||
|
||||
// const Setting = () => {
|
||||
|
||||
// };
|
||||
// const CloseBtn = ({
|
||||
// showPanel,
|
||||
// setShowPanel,
|
||||
// }: {
|
||||
// showPanel: boolean;
|
||||
// setShowPanel: (showPanel: boolean) => void;
|
||||
// }) => {
|
||||
// return
|
||||
// };
|
||||
|
||||
const Top = ({
|
||||
showPanel,
|
||||
setShowPanel,
|
||||
}: {
|
||||
showPanel: boolean;
|
||||
setShowPanel: (showPanel: boolean) => void;
|
||||
}) => {
|
||||
const [showSettingPanel, setShowSettingPanel] = useState(false);
|
||||
const gmShowPanel = GM_getValue("setting", { gmShowPanel: true }).gmShowPanel;
|
||||
|
||||
return (
|
||||
<div className="jop-top">
|
||||
{showPanel && (
|
||||
<div
|
||||
className="jop-top-setting jop-top-item"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShowSettingPanel(!showSettingPanel);
|
||||
}}
|
||||
>
|
||||
<div className="jop-top-setting-svg jop-top-svgicon">
|
||||
<svg
|
||||
width="25"
|
||||
height="25"
|
||||
viewBox="0 0 48 48"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M41.5 10H35.5"
|
||||
stroke="#333"
|
||||
stroke-width="3"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M27.5 6V14"
|
||||
stroke="#333"
|
||||
stroke-width="3"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M27.5 10L5.5 10"
|
||||
stroke="#333"
|
||||
stroke-width="3"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M13.5 24H5.5"
|
||||
stroke="#333"
|
||||
stroke-width="3"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M21.5 20V28"
|
||||
stroke="#333"
|
||||
stroke-width="3"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M43.5 24H21.5"
|
||||
stroke="#333"
|
||||
stroke-width="3"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M41.5 38H35.5"
|
||||
stroke="#333"
|
||||
stroke-width="3"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M27.5 34V42"
|
||||
stroke="#333"
|
||||
stroke-width="3"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M27.5 38H5.5"
|
||||
stroke="#333"
|
||||
stroke-width="3"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{showSettingPanel && showPanel && (
|
||||
<div className="jop-top-settingPanel">
|
||||
<h4 className="jop-top-setting-title">脚本设置</h4>
|
||||
<div className="jop-top-settingPanel-item">
|
||||
默认显示脚本界面
|
||||
<input
|
||||
type="checkbox"
|
||||
className="jop-top-checkbox"
|
||||
checked={gmShowPanel}
|
||||
onChange={(e: any) => {
|
||||
const checked: boolean = e.target.checked;
|
||||
GM_setValue("setting", { gmShowPanel: checked });
|
||||
setShowPanel(checked);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className="jop-top-close jop-top-item jop-top-svgicon"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShowPanel(!showPanel);
|
||||
}}
|
||||
>
|
||||
{showPanel ? (
|
||||
<svg
|
||||
width="25"
|
||||
height="25"
|
||||
viewBox="0 0 48 48"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M9.85786 18C6.23858 21 4 24 4 24C4 24 12.9543 36 24 36C25.3699 36 26.7076 35.8154 28 35.4921M20.0318 12.5C21.3144 12.1816 22.6414 12 24 12C35.0457 12 44 24 44 24C44 24 41.7614 27 38.1421 30"
|
||||
stroke="#333"
|
||||
stroke-width="3"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M20.3142 20.6211C19.4981 21.5109 19 22.6972 19 23.9998C19 26.7612 21.2386 28.9998 24 28.9998C25.3627 28.9998 26.5981 28.4546 27.5 27.5705"
|
||||
stroke="#333"
|
||||
stroke-width="3"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M42 42L6 6"
|
||||
stroke="#333"
|
||||
stroke-width="3"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
) : (
|
||||
<svg
|
||||
width="25"
|
||||
height="25"
|
||||
viewBox="0 0 48 48"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M24 36C35.0457 36 44 24 44 24C44 24 35.0457 12 24 12C12.9543 12 4 24 4 24C4 24 12.9543 36 24 36Z"
|
||||
fill="none"
|
||||
stroke="#333"
|
||||
stroke-width="3"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M24 29C26.7614 29 29 26.7614 29 24C29 21.2386 26.7614 19 24 19C21.2386 19 19 21.2386 19 24C19 26.7614 21.2386 29 24 29Z"
|
||||
fill="none"
|
||||
stroke="#333"
|
||||
stroke-width="3"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Top;
|
||||
72
src/main.tsx
72
src/main.tsx
@@ -1,33 +1,39 @@
|
||||
import React from "preact/compat";
|
||||
import { render } from "preact";
|
||||
import { Cms, matchList } from "./utils/matchList";
|
||||
import { getInfos } from "./utils/getInfos";
|
||||
import "./style.css";
|
||||
|
||||
import App from "./components/App";
|
||||
|
||||
// !debugger 图片关
|
||||
// document.querySelectorAll("img").forEach((item) => (item.style.display = "none"));
|
||||
|
||||
function main() {
|
||||
/** 当前 macth 站点对象 */
|
||||
const cms = matchList.find((item) => item.href.test(window.location.href)) as Cms;
|
||||
const infos = getInfos(cms);
|
||||
const CODE = infos.codeText;
|
||||
if (CODE === undefined) return;
|
||||
cms.method();
|
||||
|
||||
const panelParent = document.querySelector(cms.panelParentQueryStr) as Element;
|
||||
panelParent?.classList.add("jop-panelParent");
|
||||
|
||||
render(
|
||||
<App
|
||||
cms={cms}
|
||||
CODE={CODE}
|
||||
infos={infos}
|
||||
/>,
|
||||
panelParent,
|
||||
);
|
||||
}
|
||||
|
||||
main();
|
||||
import { render } from "preact";
|
||||
import { libSites } from "@/utils/libSites";
|
||||
import { getCode } from "@/utils";
|
||||
|
||||
import "@/style.css";
|
||||
import App from "./components/App";
|
||||
|
||||
if (!import.meta.env.PROD) {
|
||||
// document.querySelectorAll("img").forEach((item) => (item.style.visibility = "hidden"));
|
||||
}
|
||||
|
||||
function main() {
|
||||
/** 当前匹配的图书馆站点对象 */
|
||||
const libItem = libSites.find((item) => document.querySelector(item.identifier));
|
||||
if (!libItem) {
|
||||
console.error("||jop 匹配站点失败");
|
||||
return;
|
||||
}
|
||||
const CODE = getCode(libItem);
|
||||
|
||||
// 执行对于当前图书馆站的特殊适配,如单独的样式改动
|
||||
libItem.method();
|
||||
|
||||
const panel = document.querySelector<HTMLElement>(libItem.querys.panelQueryStr);
|
||||
if (!panel) {
|
||||
console.error("||jop 插入界面失败");
|
||||
return;
|
||||
}
|
||||
|
||||
const app = document.createElement("div");
|
||||
app.classList.add("jop-app");
|
||||
// app.classList.add(a.toString());
|
||||
panel.append(app);
|
||||
|
||||
render(<App libItem={libItem} CODE={CODE} />, app);
|
||||
console.log("||脚本挂载成功", CODE);
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
406
src/style.css
406
src/style.css
@@ -1,180 +1,226 @@
|
||||
.jop-panelParent {
|
||||
position: relative;
|
||||
}
|
||||
.jop-panel {
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: -3px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
padding: 17.5px;
|
||||
/* border-radius: 15px 0 0 15px; */
|
||||
background-color: white;
|
||||
/* box-shadow: rgb(0 0 0 / 26%) -3px 0px 8px; */
|
||||
transition: right 200ms ease-in-out;
|
||||
}
|
||||
|
||||
/* */
|
||||
|
||||
.jop-top {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
.jop-top-item {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.jop-top-close {
|
||||
right: 10px;
|
||||
}
|
||||
.jop-top-setting {
|
||||
position: absolute;
|
||||
right: 50px;
|
||||
}
|
||||
.jop-top-settingPanel {
|
||||
position: absolute;
|
||||
top: 60px;
|
||||
right: 20px;
|
||||
width: 230px;
|
||||
height: 150px;
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
box-shadow: rgb(0 0 0 / 26%) 0px -3px 8px;
|
||||
background: white;
|
||||
}
|
||||
.jop-top-settingPanel-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.jop-top-setting-title {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.jop-top-checkbox {
|
||||
margin-left: 20px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
/* */
|
||||
.jop-info {
|
||||
color: black;
|
||||
/* text-align: right; */
|
||||
height: 30px;
|
||||
}
|
||||
.jop-info-code {
|
||||
margin-bottom: 20px;
|
||||
color: #3272dc;
|
||||
font-size: 30px;
|
||||
cursor: pointer;
|
||||
font-family: system-ui, -apple-system;
|
||||
font-weight: bolder;
|
||||
font-style: italic;
|
||||
}
|
||||
.jop-info-actor {
|
||||
margin-top: 6px;
|
||||
}
|
||||
.jop-info-actor-item {
|
||||
padding: 3px 6px;
|
||||
color: #409eff;
|
||||
background: #ecf5ff;
|
||||
border: 1px solid #d9ecff;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* */
|
||||
|
||||
.jop-list {
|
||||
position: absolute;
|
||||
top: 115px;
|
||||
width: 80%;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.jop-button {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
/* min-width: 50px; */
|
||||
padding: 3px 10px;
|
||||
border-radius: 4px;
|
||||
font-family: Roboto, Helvetica, Arial, sans-serif;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
border: 1px solid #dcdfe6;
|
||||
color: #606266;
|
||||
}
|
||||
.jop-button:visited {
|
||||
color: #606266;
|
||||
}
|
||||
.jop-button:hover {
|
||||
text-decoration: none;
|
||||
color: #409eff;
|
||||
border: 1px solid #c6e2ff;
|
||||
background-color: #ecf5ff;
|
||||
}
|
||||
|
||||
.jop-button_label {
|
||||
position: absolute;
|
||||
font-size: 10px;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
top: -13px;
|
||||
right: -10px;
|
||||
line-height: 0.75;
|
||||
color: #67c23a;
|
||||
border: 1px solid #e1f3d8;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.jop-button_green {
|
||||
color: white !important;
|
||||
background-color: #67c23a;
|
||||
/* border: 1px solid #b3e19d; */
|
||||
}
|
||||
.jop-button_green:hover {
|
||||
color: white !important;
|
||||
background-color: #95d475;
|
||||
}
|
||||
|
||||
.jop-button_red {
|
||||
color: white !important;
|
||||
background-color: #f56c6c;
|
||||
/* border: 1px solid #fab6b6; */
|
||||
}
|
||||
.jop-button_red:hover {
|
||||
color: white !important;
|
||||
background-color: #f89898;
|
||||
}
|
||||
|
||||
.jop-loading {
|
||||
display: inline-block;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin-right: 10px;
|
||||
border: 2px dashed #dcdfe6;
|
||||
border-top-color: transparent;
|
||||
border-radius: 100%;
|
||||
animation: btnLoading infinite 1s linear;
|
||||
}
|
||||
@keyframes btnLoading {
|
||||
0% {
|
||||
transform: rotate(0);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
/* */
|
||||
.jop-list {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
transition: right 200ms ease-in-out;
|
||||
color: black;
|
||||
}
|
||||
|
||||
/* */
|
||||
.jop-button,
|
||||
.jop-button_def {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
padding: 3px 10px;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
border: 1px solid #dcdfe6;
|
||||
color: #606266;
|
||||
cursor: pointer;
|
||||
}
|
||||
.jop-button_def {
|
||||
margin: 10px 0;
|
||||
width: 100px;
|
||||
}
|
||||
.jop-button:visited {
|
||||
color: #606266;
|
||||
}
|
||||
.jop-button:hover {
|
||||
text-decoration: none;
|
||||
color: #409eff;
|
||||
border: 1px solid #c6e2ff;
|
||||
background-color: #ecf5ff;
|
||||
}
|
||||
|
||||
.jop-button_label {
|
||||
position: absolute;
|
||||
font-size: 10px;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
top: -13px;
|
||||
right: -10px;
|
||||
line-height: 0.75;
|
||||
color: #67c23a;
|
||||
border: 1px solid #e1f3d8;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.jop-button_green {
|
||||
color: white !important;
|
||||
background-color: #67c23a;
|
||||
/* border: 1px solid #b3e19d; */
|
||||
}
|
||||
.jop-button_green:hover {
|
||||
color: white !important;
|
||||
background-color: #95d475;
|
||||
}
|
||||
|
||||
.jop-button_red {
|
||||
color: white !important;
|
||||
background-color: #f56c6c;
|
||||
/* border: 1px solid #fab6b6; */
|
||||
}
|
||||
.jop-button_red:hover {
|
||||
color: white !important;
|
||||
background-color: #f89898;
|
||||
}
|
||||
|
||||
.jop-loading {
|
||||
display: inline-block;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin-right: 10px;
|
||||
border: 2px dashed #dcdfe6;
|
||||
border-top-color: transparent;
|
||||
border-radius: 100%;
|
||||
animation: btnLoading infinite 1s linear;
|
||||
}
|
||||
@keyframes btnLoading {
|
||||
0% {
|
||||
transform: rotate(0);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* */
|
||||
.jop-tag {
|
||||
padding: 3px 6px;
|
||||
color: #409eff !important;
|
||||
background: #ecf5ff;
|
||||
border: 1px solid #d9ecff;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* */
|
||||
.jop-setting{
|
||||
margin-top:20px;
|
||||
}
|
||||
.jop-setting-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.jop-setting-title {
|
||||
margin: 10px 0 5px 0;
|
||||
font-weight: 700;
|
||||
}
|
||||
.jop-setting-item {
|
||||
display: flex;
|
||||
height: 20px;
|
||||
align-items: center;
|
||||
margin-right: 15px;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
/* */
|
||||
|
||||
/* db */
|
||||
.db-panel .movie-panel-info div.panel-block {
|
||||
padding: 5.5px 12px;
|
||||
}
|
||||
.db-panel .jop-app {
|
||||
padding: 15px 12px;
|
||||
}
|
||||
/* */
|
||||
|
||||
/* lib */
|
||||
.lib-panel .jop-app {
|
||||
padding: 20px 30px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
/* */
|
||||
|
||||
/* */
|
||||
input[type="checkbox"],
|
||||
input[type="radio"] {
|
||||
margin: 0 0 0 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
/* */
|
||||
|
||||
.jop-tooltip-container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.jop-tooltip {
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background-color: #333;
|
||||
color: #fff;
|
||||
padding: 5px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.jop-setting-label {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.jop-checkbox {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
margin-right: 15px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.jop-checkbox-input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.jop-checkbox-custom {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 2px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.jop-checkbox-input:checked + .jop-checkbox-custom {
|
||||
background-color: #409eff;
|
||||
border-color: #409eff;
|
||||
}
|
||||
|
||||
.jop-checkbox-input:checked + .jop-checkbox-custom::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
left: 4px;
|
||||
width: 5px;
|
||||
height: 10px;
|
||||
border: solid white;
|
||||
border-width: 0 2px 2px 0;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.jop-checkbox-label {
|
||||
margin-left: 3px;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.jop-checkbox:hover .jop-checkbox-custom {
|
||||
border-color: #409eff;
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import { Infos } from "../components/Info";
|
||||
import { Cms } from "./matchList";
|
||||
|
||||
export function getInfos(cms: Cms): Infos {
|
||||
const codeNode = document.querySelector<HTMLElement>(cms.codeQueryStr) as HTMLElement;
|
||||
const actorNodeList = document.querySelectorAll<HTMLAnchorElement>(cms.actorQueryStr);
|
||||
const actorList = [...actorNodeList].map((item) => {
|
||||
return { text: item.innerHTML, link: item.href };
|
||||
});
|
||||
return {
|
||||
codeText:
|
||||
cms.name === "javdb"
|
||||
? (codeNode?.dataset.clipboardText as string)
|
||||
: codeNode.innerText.replace("复制", ""),
|
||||
actorList,
|
||||
};
|
||||
}
|
||||
88
src/utils/index.ts
Normal file
88
src/utils/index.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { GM_xmlhttpRequest } from "$";
|
||||
import { type LibItem } from "./libSites";
|
||||
import { SP_PREFIX } from "./siteList";
|
||||
interface TResponse {
|
||||
readonly responseHeaders: string;
|
||||
readonly readyState: 0 | 1 | 2 | 3 | 4;
|
||||
readonly response: any;
|
||||
readonly responseText: string;
|
||||
readonly responseXML: Document | null;
|
||||
readonly status: number;
|
||||
readonly statusText: string;
|
||||
readonly finalUrl: string;
|
||||
}
|
||||
|
||||
export const gmGet = ({ url }: { url: string }): Promise<TResponse> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
GM_xmlhttpRequest({
|
||||
method: "GET",
|
||||
url,
|
||||
onload: (response) => resolve(response),
|
||||
onerror: (error) => reject(error),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const gmPost = ({
|
||||
url,
|
||||
data,
|
||||
}: {
|
||||
url: string;
|
||||
data?: Record<string, any>;
|
||||
}): Promise<TResponse> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
GM_xmlhttpRequest({
|
||||
method: "POST",
|
||||
data: new URLSearchParams(data).toString(),
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
url,
|
||||
onload: (response) => resolve(response),
|
||||
onerror: (error) => reject(error),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const isCaseInsensitiveEqual = (str1?: any, str2?: any) => {
|
||||
if (!str1 || !str2) return false;
|
||||
return str1.toLowerCase() === str2.toLowerCase();
|
||||
};
|
||||
|
||||
export const isErrorCode = (resCode: number) => {
|
||||
return [404, 403].includes(resCode);
|
||||
};
|
||||
|
||||
export const getCode = (libItem: LibItem): string => {
|
||||
const { codeQueryStr } = libItem.querys;
|
||||
const codeNode = document.querySelector<HTMLElement>(codeQueryStr);
|
||||
if (!codeNode) return "";
|
||||
|
||||
const codeText =
|
||||
libItem.name === "javdb"
|
||||
? (codeNode.dataset.clipboardText as string)
|
||||
: codeNode.innerText.replace("复制", "");
|
||||
|
||||
if (codeText.includes("FC2")) return codeText.split("-")[1];
|
||||
if (codeText.startsWith(SP_PREFIX)) return codeText.substring(3);
|
||||
|
||||
return codeText;
|
||||
};
|
||||
|
||||
export const regEnum = {
|
||||
subtitle: /(中文|字幕|subtitle)/,
|
||||
leakage: /(无码|無碼|泄漏|泄露|Uncensored)/,
|
||||
};
|
||||
|
||||
export const tagsQuery = ({
|
||||
leakageText,
|
||||
subtitleText,
|
||||
}: {
|
||||
leakageText: string;
|
||||
subtitleText: string;
|
||||
}) => {
|
||||
const hasLeakage = regEnum.leakage.test(leakageText);
|
||||
const hasSubtitle = regEnum.subtitle.test(subtitleText);
|
||||
const tags = [];
|
||||
if (hasLeakage) tags.push("无码");
|
||||
if (hasSubtitle) tags.push("字幕");
|
||||
return tags.join(" ");
|
||||
};
|
||||
58
src/utils/libSites.ts
Normal file
58
src/utils/libSites.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
/** 当前 macth 站点对象 */
|
||||
export type LibItem = {
|
||||
name: "javdb" | "javbus" | "javlib";
|
||||
enable: boolean;
|
||||
identifier: string;
|
||||
querys: {
|
||||
panelQueryStr: string;
|
||||
codeQueryStr: string;
|
||||
};
|
||||
method: () => void;
|
||||
};
|
||||
|
||||
/** 需要匹配的图书馆站点列表 */
|
||||
export const libSites: LibItem[] = [
|
||||
{
|
||||
name: "javdb",
|
||||
enable: true,
|
||||
identifier: "a[href*='javdb']",
|
||||
querys: {
|
||||
panelQueryStr: ".video-meta-panel>.columns.is-desktop .panel.movie-panel-info",
|
||||
codeQueryStr: `[data-clipboard-text]`,
|
||||
},
|
||||
method() {
|
||||
// 一些样式调整
|
||||
const columnVideoCover = document.querySelector<HTMLElement>(".column-video-cover");
|
||||
if (columnVideoCover) {
|
||||
columnVideoCover.style.width = "60%";
|
||||
}
|
||||
const panel = document.querySelector<HTMLElement>(
|
||||
".video-meta-panel>.columns.is-desktop>.column:not(.column-video-cover)",
|
||||
);
|
||||
panel?.classList.add("db-panel");
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "javbus",
|
||||
enable: true,
|
||||
identifier: "a[href*='javbus']",
|
||||
querys: {
|
||||
panelQueryStr: ".movie>div.info",
|
||||
codeQueryStr: `span[style="color:#CC0000;"]`,
|
||||
},
|
||||
method() {},
|
||||
},
|
||||
{
|
||||
name: "javlib",
|
||||
enable: true,
|
||||
identifier: "img[src*='logo-top']",
|
||||
querys: {
|
||||
panelQueryStr: "#video_jacket_info #video_info",
|
||||
codeQueryStr: `#video_id td.text`,
|
||||
},
|
||||
method() {
|
||||
const panel = document.querySelector<HTMLElement>("#video_info");
|
||||
panel?.classList.add("lib-panel");
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,57 +0,0 @@
|
||||
/** 当前 macth 站点对象 */
|
||||
export type Cms = {
|
||||
name: "javdb" | "javbus" | "javlib";
|
||||
enable: boolean;
|
||||
href: RegExp;
|
||||
panelParentQueryStr: string;
|
||||
codeQueryStr: string;
|
||||
actorQueryStr: string;
|
||||
method: () => void;
|
||||
};
|
||||
export const matchList: Cms[] = [
|
||||
{
|
||||
name: "javdb",
|
||||
enable: true,
|
||||
href: /^https:\/\/(\w*\.)?javdb(\d)*\.com.*$/,
|
||||
panelParentQueryStr: ".video-meta-panel>.columns.is-desktop>.column:not(.column-video-cover)",
|
||||
codeQueryStr: `[data-clipboard-text]`,
|
||||
actorQueryStr: `span.value>a[href^="/actors"]`,
|
||||
method() {},
|
||||
},
|
||||
{
|
||||
name: "javbus",
|
||||
enable: true,
|
||||
// hostname: [
|
||||
// "www.javbus.com",
|
||||
// "www.seejav.one",
|
||||
// "www.seejav.cc",
|
||||
// "www.javsee.me",
|
||||
// "www.javsee.in",
|
||||
// ],
|
||||
href: /^https?:\/\/(\w*\.)?(javbus|seejav|javsee)*\.(com|cc|me|life).*$/,
|
||||
panelParentQueryStr: ".movie>div.info",
|
||||
codeQueryStr: `span[style="color:#CC0000;"]`,
|
||||
actorQueryStr: `.genre>a`,
|
||||
method() {
|
||||
// panel 加宽
|
||||
const colmd8 = document.querySelector(".movie>.col-md-9.screencap");
|
||||
colmd8?.classList.remove("col-md-9");
|
||||
colmd8?.classList.add("col-md-8");
|
||||
const colmd4 = document.querySelector(".movie>.col-md-3.info");
|
||||
colmd4?.classList.remove("col-md-3");
|
||||
colmd4?.classList.add("col-md-4");
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "javlib",
|
||||
enable: true,
|
||||
// hostname: ["www.javlibrary.com", "www.javlib.com"],
|
||||
href: /^https?:\/\/(\w*\.)?(javlib|javlibrary)*\.com.*$/,
|
||||
panelParentQueryStr: "#video_jacket_info #video_info",
|
||||
codeQueryStr: `#video_id td.text`,
|
||||
actorQueryStr: `.cast>.star>a`,
|
||||
method() {
|
||||
// const infoPanel = document.querySelectorAll( `#video_jacket_info td[style="vertical-align: top;"]`, )[1]; infoPanel?.classList.add("JOPAPP");
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1 +0,0 @@
|
||||
export {};
|
||||
@@ -1,207 +1,327 @@
|
||||
export interface DomQuery_parser {
|
||||
/** 部分网站搜索页结果第一个是广告,所以要加一个 index */
|
||||
listIndex?: number;
|
||||
/** code 是用空格分割的 */
|
||||
spaceCode?: boolean;
|
||||
linkQuery: string;
|
||||
titleQuery: string;
|
||||
/** 确定返回颜色,暂未启用 */
|
||||
certainColor?: string;
|
||||
}
|
||||
|
||||
export interface DomQuery_get {
|
||||
/** 收录视频,但是未提供在线播放资源 */
|
||||
videoQuery?: string;
|
||||
subQuery?: string;
|
||||
leakQuery?: string;
|
||||
}
|
||||
|
||||
interface SiteItemBase {
|
||||
name: string;
|
||||
/** 针对 matchList 的 hostname */
|
||||
disable?: string;
|
||||
hostname: string;
|
||||
url: string;
|
||||
codeFormater?: (arg0: string) => string;
|
||||
method?: Function;
|
||||
}
|
||||
|
||||
export interface SiteItem_get extends SiteItemBase {
|
||||
fetcher: "get";
|
||||
domQuery: DomQuery_get;
|
||||
}
|
||||
|
||||
export interface SiteItem_parser extends SiteItemBase {
|
||||
fetcher: "parser";
|
||||
domQuery: DomQuery_parser;
|
||||
}
|
||||
|
||||
export type SiteItem = SiteItem_get | SiteItem_parser;
|
||||
|
||||
const print = (name: string) => {
|
||||
console.log(name);
|
||||
};
|
||||
|
||||
/** 网站列表 */
|
||||
export const siteList: SiteItem[] = [
|
||||
{
|
||||
name: "Jable",
|
||||
hostname: "jable.tv",
|
||||
url: "https://jable.tv/videos/{{code}}/",
|
||||
fetcher: "get",
|
||||
domQuery: { subQuery: ".header-right>h6" },
|
||||
method: print,
|
||||
},
|
||||
{
|
||||
name: "MISSAV",
|
||||
hostname: "missav.com",
|
||||
url: "https://missav.com/{{code}}/",
|
||||
fetcher: "get",
|
||||
domQuery: {
|
||||
/** 标签区的第一个一般是字幕标签 */
|
||||
subQuery: '.space-y-2 a.text-nord13[href="https://missav.com/chinese-subtitle"]',
|
||||
/** videoPage 有个跳转按钮 */
|
||||
leakQuery: ".order-first div.rounded-md a[href]:last-child",
|
||||
},
|
||||
method: print,
|
||||
},
|
||||
{
|
||||
name: "NETFLAV",
|
||||
hostname: "netflav.com",
|
||||
url: "https://netflav.com/search?type=title&keyword={{code}}",
|
||||
fetcher: "parser",
|
||||
domQuery: { linkQuery: ".grid_cell>a", titleQuery: ".grid_cell>a>.grid_title" },
|
||||
method: print,
|
||||
},
|
||||
{
|
||||
name: "Avgle",
|
||||
hostname: "avgle.com",
|
||||
url: "https://avgle.com/search/videos?search_query={{code}}&search_type=videos",
|
||||
fetcher: "parser",
|
||||
domQuery: {
|
||||
linkQuery: ".container>.row .row .well>a[href]",
|
||||
titleQuery: ".container>.row .row .well .video-title",
|
||||
},
|
||||
method: print,
|
||||
},
|
||||
{
|
||||
name: "JAVHHH",
|
||||
hostname: "javhhh.com",
|
||||
url: "https://javhhh.com/v/?wd={{code}}",
|
||||
fetcher: "parser",
|
||||
domQuery: {
|
||||
linkQuery: ".typelist>.i-container>a[href]",
|
||||
titleQuery: ".typelist>.i-container>a[href]",
|
||||
},
|
||||
method: print,
|
||||
},
|
||||
{
|
||||
name: "BestJP",
|
||||
hostname: "bestjavporn.com",
|
||||
url: "https://www3.bestjavporn.com/search/{{code}}",
|
||||
fetcher: "parser",
|
||||
domQuery: { linkQuery: "article.thumb-block>a", titleQuery: "article.thumb-block>a" },
|
||||
method: print,
|
||||
},
|
||||
{
|
||||
name: "JAVMENU",
|
||||
hostname: "javmenu.com",
|
||||
url: "https://javmenu.com/{{code}}",
|
||||
fetcher: "get",
|
||||
domQuery: {
|
||||
videoQuery: "a.nav-link[aria-controls='pills-0']",
|
||||
},
|
||||
method: print,
|
||||
},
|
||||
{
|
||||
name: "Jav.Guru",
|
||||
hostname: "jav.guru",
|
||||
url: "https://jav.guru/?s={{code}}",
|
||||
fetcher: "parser",
|
||||
domQuery: { linkQuery: ".imgg>a[href]", titleQuery: ".inside-article>.grid1 a[title]" },
|
||||
method: print,
|
||||
},
|
||||
{
|
||||
name: "JAVMOST",
|
||||
hostname: "javmost.cx",
|
||||
url: "https://javmost.cx/search/{{code}}/",
|
||||
fetcher: "parser",
|
||||
domQuery: {
|
||||
linkQuery: "#content .card a#MyImage",
|
||||
titleQuery: "#content .card-block .card-title",
|
||||
},
|
||||
method: print,
|
||||
},
|
||||
{
|
||||
name: "HAYAV",
|
||||
hostname: "hayav.com",
|
||||
url: "https://hayav.com/video/{{code}}/",
|
||||
fetcher: "get",
|
||||
domQuery: {},
|
||||
method: print,
|
||||
},
|
||||
|
||||
{
|
||||
name: "JAVFC2",
|
||||
hostname: "javfc2.net",
|
||||
url: "https://javfc2.net/?s={{code}}",
|
||||
fetcher: "parser",
|
||||
domQuery: {
|
||||
linkQuery: "article.loop-video>a[href]",
|
||||
titleQuery: "article.loop-video .entry-header",
|
||||
},
|
||||
method: print,
|
||||
},
|
||||
{
|
||||
name: "baihuse",
|
||||
hostname: "paipancon.com",
|
||||
url: "https://paipancon.com/search/{{code}}",
|
||||
fetcher: "parser",
|
||||
domQuery: {
|
||||
linkQuery: "div.col>div.card>a[href]",
|
||||
// 然而这个不是 title,是图片,这个站居然 title 里不包含 code,反而图片包含
|
||||
titleQuery: "div.card img.card-img-top",
|
||||
},
|
||||
method: print,
|
||||
},
|
||||
{
|
||||
name: "GGJAV",
|
||||
hostname: "ggjav.com",
|
||||
url: "https://ggjav.com/main/search?string={{code}}",
|
||||
fetcher: "parser",
|
||||
domQuery: {
|
||||
listIndex: 1,
|
||||
spaceCode: true,
|
||||
titleQuery: "div.columns.large-3.medium-6.small-12.item.float-left>div.item_title>a.gray_a",
|
||||
linkQuery: "div.columns.large-3.medium-6.small-12.item.float-left>div.item_title>a.gray_a",
|
||||
},
|
||||
method: print,
|
||||
},
|
||||
|
||||
{
|
||||
name: "AV01",
|
||||
hostname: "av01.tv",
|
||||
url: "https://www.av01.tv/search/videos?search_query={{code}}",
|
||||
fetcher: "parser",
|
||||
domQuery: { linkQuery: "div[id].well-sm>a", titleQuery: ".video-views>.pull-left" },
|
||||
method: print,
|
||||
},
|
||||
{
|
||||
name: "JavBus",
|
||||
disable: "javbus",
|
||||
hostname: "javbus.com",
|
||||
url: "https://javbus.com/{{code}}",
|
||||
fetcher: "get",
|
||||
domQuery: {},
|
||||
method: print,
|
||||
},
|
||||
// {
|
||||
// name: "JavDB",
|
||||
// disable:"javdb",
|
||||
// hostname: "javbus.com",
|
||||
// url: "https://javbus.com/{{code}}",
|
||||
// fetcher: "get",
|
||||
// domQuery: {},
|
||||
// method: print,
|
||||
// },
|
||||
];
|
||||
export interface DomQuery_parser {
|
||||
/** 部分网站搜索页结果第一个是广告,加入一个 index,来指定到固定的位置。
|
||||
* 点名:GGJAV
|
||||
*/
|
||||
listIndex?: number;
|
||||
|
||||
/** 大部分 code 格式是 `xxx-000`,还有一部分用空格 `xxx 000`。
|
||||
* 原来是点名 GGJAV 的
|
||||
*/
|
||||
spaceCode?: boolean;
|
||||
|
||||
/** a 标签 href 的 query */
|
||||
linkQuery: string;
|
||||
/** 在 title 里检测是否包含「字幕」等文本 */
|
||||
titleQuery: string;
|
||||
}
|
||||
|
||||
export interface DomQuery_get {
|
||||
/** 检测是否提供播放
|
||||
* JAVMENU 收录视频,但是未提供在线播放资源
|
||||
*/
|
||||
videoQuery?: string;
|
||||
|
||||
subQuery?: string;
|
||||
leakQuery?: string;
|
||||
}
|
||||
|
||||
interface SiteItemBase {
|
||||
name: string;
|
||||
|
||||
/** [废弃] 用户定义的 disable */
|
||||
// disable: boolean;
|
||||
|
||||
/** 在指定 LibItem.name 下不显示 */
|
||||
|
||||
hostname: string;
|
||||
url: string;
|
||||
/** 部分站 CODE 格式不一样 */
|
||||
codeFormater?: (preCode: string) => string;
|
||||
}
|
||||
|
||||
export interface SiteItem_get extends SiteItemBase {
|
||||
fetchType: "get";
|
||||
domQuery: DomQuery_get;
|
||||
}
|
||||
|
||||
export interface SiteItem_parser extends SiteItemBase {
|
||||
fetchType: "parser";
|
||||
/** 严格匹配,会检查搜索结果的 code */
|
||||
strictParser?: true;
|
||||
domQuery: DomQuery_parser;
|
||||
}
|
||||
// export interface SiteItem_post extends SiteItemBase {
|
||||
// fetchType: "post";
|
||||
// postParams: Record<string, any>;
|
||||
// domQuery: DomQuery_parser;
|
||||
// }
|
||||
export interface SiteItem_false extends SiteItemBase {
|
||||
fetchType: "false";
|
||||
}
|
||||
|
||||
export type SiteItem = SiteItem_get | SiteItem_parser | SiteItem_false;
|
||||
|
||||
/** 在线网站列表 */
|
||||
export const siteList: SiteItem[] = [
|
||||
{
|
||||
name: "FANZA 動画",
|
||||
hostname: "dmm.co.jp",
|
||||
url: "https://www.dmm.co.jp/digital/videoa/-/detail/=/cid={{code}}/",
|
||||
// url: "https://video.dmm.co.jp/av/list/?key={{code}}",
|
||||
fetchType: "get",
|
||||
codeFormater: (preCode) => {
|
||||
const [pre, num] = preCode.split("-");
|
||||
const padNum = num.padStart(5, "0");
|
||||
if (pre.toLowerCase().startsWith("start")) {
|
||||
return `1${pre.toLowerCase()}${padNum}`;
|
||||
}
|
||||
return `${pre}${padNum}`;
|
||||
},
|
||||
domQuery: {},
|
||||
},
|
||||
{
|
||||
name: "Jable",
|
||||
hostname: "jable.tv",
|
||||
url: "https://jable.tv/videos/{{code}}/",
|
||||
fetchType: "get",
|
||||
domQuery: {
|
||||
subQuery: ".info-header",
|
||||
leakQuery: ".info-header",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "MISSAV",
|
||||
hostname: "missav.ws",
|
||||
url: "https://missav.ws/{{code}}/",
|
||||
fetchType: "get",
|
||||
domQuery: {
|
||||
// 标签区的第一个一般是字幕标签
|
||||
subQuery: '.space-y-2 a.text-nord13[href="https://missav.ws/chinese-subtitle"]',
|
||||
// 有个「切換無碼」按钮,藏在分享按钮旁边……
|
||||
leakQuery: ".order-first div.rounded-md a[href]:last-child",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "123av",
|
||||
hostname: "123av.com",
|
||||
url: "https://123av.com/zh/search?keyword={{code}}",
|
||||
fetchType: "parser",
|
||||
strictParser: true,
|
||||
domQuery: {
|
||||
linkQuery: `.detail>a[href*='v/']`,
|
||||
titleQuery: `.detail>a[href*='v/']`,
|
||||
},
|
||||
},
|
||||
{
|
||||
// 有可能搜出仨:leakage subtitle 4k
|
||||
name: "Supjav",
|
||||
hostname: "supjav.com",
|
||||
url: "https://supjav.com/zh/?s={{code}}",
|
||||
fetchType: "parser",
|
||||
domQuery: {
|
||||
linkQuery: `.posts.clearfix>.post>a.img[title]`,
|
||||
titleQuery: `h3>a[rel="bookmark"][itemprop="url"]`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NETFLAV",
|
||||
hostname: "netflav5.com",
|
||||
url: "https://netflav5.com/search?type=title&keyword={{code}}",
|
||||
fetchType: "parser",
|
||||
domQuery: {
|
||||
linkQuery: ".grid_0_cell>a[href^='/video?']",
|
||||
titleQuery: ".grid_0_cell>a[href^='/video?'] .grid_0_title",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Avgle",
|
||||
hostname: "avgle.com",
|
||||
url: "https://avgle.com/search/videos?search_query={{code}}&search_type=videos",
|
||||
fetchType: "parser",
|
||||
domQuery: {
|
||||
linkQuery: ".container>.row .row .well>a[href]",
|
||||
titleQuery: ".container>.row .row .well .video-title",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "JAVHHH",
|
||||
hostname: "javhhh.com",
|
||||
url: "https://javhhh.com/v/?wd={{code}}",
|
||||
fetchType: "parser",
|
||||
domQuery: {
|
||||
linkQuery: ".typelist>.i-container>a[href]",
|
||||
titleQuery: ".typelist>.i-container>a[href]",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "BestJP",
|
||||
hostname: "bestjavporn.com",
|
||||
url: "https://www3.bestjavporn.com/search/{{code}}",
|
||||
fetchType: "parser",
|
||||
domQuery: { linkQuery: "article.thumb-block>a", titleQuery: "article.thumb-block>a" },
|
||||
},
|
||||
{
|
||||
name: "JAVMENU",
|
||||
hostname: "javmenu.com",
|
||||
url: "https://javmenu.com/{{code}}",
|
||||
fetchType: "get",
|
||||
domQuery: {
|
||||
videoQuery: "a.nav-link[aria-controls='pills-0']",
|
||||
},
|
||||
// codeFormater: (preCode) => preCode.replace("-", ""),
|
||||
},
|
||||
{
|
||||
name: "Jav.Guru",
|
||||
hostname: "jav.guru",
|
||||
url: "https://jav.guru/?s={{code}}",
|
||||
fetchType: "parser",
|
||||
domQuery: { linkQuery: ".imgg>a[href]", titleQuery: ".inside-article>.grid1 a[title]" },
|
||||
},
|
||||
{
|
||||
name: "JAVMOST",
|
||||
hostname: "javmost.cx",
|
||||
url: "https://javmost.cx/search/{{code}}/",
|
||||
fetchType: "parser",
|
||||
domQuery: {
|
||||
linkQuery: ".card #myButton",
|
||||
titleQuery: ".card-block h4.card-title",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "HAYAV",
|
||||
hostname: "hayav.com",
|
||||
url: "https://hayav.com/video/{{code}}/",
|
||||
fetchType: "get",
|
||||
domQuery: {
|
||||
// subQuery: `.site__col>.entry-header>h1.entry-title`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "AvJoy",
|
||||
hostname: "avjoy.me",
|
||||
url: "https://avjoy.me/search/videos/{{code}}",
|
||||
fetchType: "parser",
|
||||
domQuery: {
|
||||
titleQuery: `#wrapper .row .content-info span.content-title`,
|
||||
linkQuery: `#wrapper .row a[href^="/video/"]`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "JAVFC2",
|
||||
hostname: "javfc2.net",
|
||||
url: "https://javfc2.net/?s={{code}}",
|
||||
fetchType: "parser",
|
||||
domQuery: {
|
||||
linkQuery: "article.loop-video>a[href]",
|
||||
titleQuery: "article.loop-video .entry-header",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "baihuse",
|
||||
hostname: "paipancon.com",
|
||||
url: "https://paipancon.com/search/{{code}}",
|
||||
fetchType: "parser",
|
||||
domQuery: {
|
||||
linkQuery: "div.col>div.card>a[href]",
|
||||
// 然而这个不是 title,是图片,这个站居然 title 里不包含 code,反而图片包含
|
||||
titleQuery: "div.card img.card-img-top",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "GGJAV",
|
||||
hostname: "ggjav.com",
|
||||
url: "https://ggjav.com/main/search?string={{code}}",
|
||||
fetchType: "parser",
|
||||
domQuery: {
|
||||
listIndex: 1,
|
||||
// spaceCode: true,
|
||||
titleQuery: "div.columns.large-3.medium-6.small-12.item.float-left>div.item_title>a.gray_a",
|
||||
linkQuery: "div.columns.large-3.medium-6.small-12.item.float-left>div.item_title>a.gray_a",
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "AV01",
|
||||
hostname: "www.av01.tv",
|
||||
url: "https://www.av01.tv/search/videos?search_query={{code}}",
|
||||
fetchType: "parser",
|
||||
domQuery: {
|
||||
linkQuery: "div.well>a[href^='/video/']",
|
||||
titleQuery: "div.well>a[href^='/video/']",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "18sex",
|
||||
hostname: "18sex.org",
|
||||
url: "https://www.18sex.org/cn/search/{{code}}/",
|
||||
fetchType: "parser",
|
||||
domQuery: { linkQuery: ".white_link[href]", titleQuery: ".white_link>.card-title" },
|
||||
},
|
||||
{
|
||||
name: "highporn",
|
||||
hostname: "highporn.net",
|
||||
url: "https://highporn.net/search/videos?search_query={{code}}",
|
||||
fetchType: "parser",
|
||||
domQuery: { linkQuery: ".well>a[href]", titleQuery: ".well>a[href]>span.video-title" },
|
||||
},
|
||||
{
|
||||
// 套了个 cf_clearance 的 cookie,不好搞
|
||||
name: "evojav",
|
||||
hostname: "evojav.pro",
|
||||
url: "https://evojav.pro/video/{{code}}/",
|
||||
fetchType: "get",
|
||||
domQuery: {},
|
||||
},
|
||||
{
|
||||
name: "18av",
|
||||
hostname: "18av.mm-cg.com",
|
||||
url: "https://18av.mm-cg.com/zh/fc_search/all/{{code}}/1.html",
|
||||
fetchType: "parser",
|
||||
domQuery: { linkQuery: ".posts h3>a[href]", titleQuery: ".posts h3>a[href]" },
|
||||
},
|
||||
{
|
||||
name: "javgo",
|
||||
hostname: "javgo.to",
|
||||
url: "https://javgo.to/zh/v/{{code}}",
|
||||
fetchType: "get",
|
||||
domQuery: {},
|
||||
},
|
||||
{
|
||||
name: "javhub",
|
||||
hostname: "javhub.net",
|
||||
url: "https://javhub.net/search/{{code}}",
|
||||
fetchType: "parser",
|
||||
domQuery: { linkQuery: "a.card-text[href*='play']", titleQuery: "a.card-text[href*='play']" },
|
||||
},
|
||||
{
|
||||
name: "JavBus",
|
||||
hostname: "javbus.com",
|
||||
url: "https://javbus.com/{{code}}",
|
||||
fetchType: "get",
|
||||
domQuery: {},
|
||||
codeFormater: (preCode) => (preCode.startsWith("MIUM") ? `${SP_PREFIX}${preCode}` : preCode),
|
||||
},
|
||||
{
|
||||
name: "JavDB",
|
||||
hostname: "javdb.com",
|
||||
url: "https://javdb.com/search?q={{code}}",
|
||||
fetchType: "parser",
|
||||
domQuery: {
|
||||
linkQuery: ".movie-list>.item:first-child>a",
|
||||
titleQuery: ".video-title",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "JAVLib",
|
||||
hostname: "javlibrary.com",
|
||||
url: "https://www.javlibrary.com/cn/vl_searchbyid.php?keyword={{code}}",
|
||||
fetchType: "false",
|
||||
// domQuery: {
|
||||
// linkQuery: ".videothumblist .video[id]:first-child>a",
|
||||
// titleQuery: ".videothumblist .video[id]:first-child>a>div.id",
|
||||
// },
|
||||
},
|
||||
];
|
||||
|
||||
/** bus 里有些以 '300MIUM' 开头,要处理掉这个 300 */
|
||||
export const SP_PREFIX = "300" as const;
|
||||
|
||||
264
src/utils/xhr.ts
264
src/utils/xhr.ts
@@ -1,127 +1,137 @@
|
||||
import { GM_xmlhttpRequest } from "$";
|
||||
import { RenderSiteItem } from "../components/App";
|
||||
import type { DomQuery_get, DomQuery_parser, SiteItem } from "./siteList";
|
||||
|
||||
export type xhrResult = {
|
||||
isSuccess: boolean;
|
||||
targetLink: string;
|
||||
hasSubtitle: boolean;
|
||||
hasLeakage: boolean;
|
||||
msg: string;
|
||||
};
|
||||
|
||||
/** 针对视频播放页进行解析,寻找字幕等信息 */
|
||||
function videoPageParser(responseText: string, { subQuery, leakQuery, videoQuery }: DomQuery_get) {
|
||||
const doc = new DOMParser().parseFromString(responseText, "text/html");
|
||||
|
||||
const subNode = subQuery ? doc.querySelector<HTMLElement>(subQuery) : "";
|
||||
const subNodeText = subNode ? subNode.innerHTML : "";
|
||||
const leakNode = leakQuery ? doc.querySelector<HTMLElement>(leakQuery) : null;
|
||||
// 部分网站收录视频,但是未提供播放资源,所以需要使用 videoQuery 进一步检测是否存在在线播放
|
||||
/** videoQuery 为 undefine 时,不需要查找 video */
|
||||
const videoNode = videoQuery ? doc.querySelector<HTMLElement>(videoQuery) : true;
|
||||
|
||||
return {
|
||||
isSuccess: !!videoNode,
|
||||
hasSubtitle: subNodeText.includes("字幕") || subNodeText.includes("subtitle"),
|
||||
hasLeakage: !!leakNode,
|
||||
};
|
||||
}
|
||||
|
||||
/** 针对 fetcher==="parser" 时的搜索结果页进行解析,寻找是否存在视频资源。
|
||||
* linkQuery、titleQuery 都是必须,
|
||||
* linkQuery 有结果且 titleQuery 结果包含 code,返回 isSuccess。
|
||||
* 再检查下 title 中是否含有字幕信息等
|
||||
*/
|
||||
function serachPageParser(
|
||||
responseText: string,
|
||||
{ linkQuery, titleQuery, listIndex = 0, spaceCode = false }: DomQuery_parser,
|
||||
siteHostName: string,
|
||||
CODE: string,
|
||||
) {
|
||||
const doc = new DOMParser().parseFromString(responseText, "text/html");
|
||||
|
||||
const linkNode = linkQuery ? doc.querySelectorAll<HTMLAnchorElement>(linkQuery)[listIndex] : null;
|
||||
const titleNode = titleQuery ? doc.querySelectorAll(titleQuery)[listIndex] : null;
|
||||
const titleNodeText = titleNode ? titleNode?.outerHTML : "";
|
||||
|
||||
function query() {
|
||||
/** 空格版本的 code */
|
||||
const envCodeWithSpace = spaceCode ? CODE.replace("-", " ") : CODE;
|
||||
const condition =
|
||||
linkNode &&
|
||||
titleNode &&
|
||||
(titleNodeText.includes(envCodeWithSpace) || titleNodeText.includes(CODE));
|
||||
|
||||
if (condition) {
|
||||
return {
|
||||
isSuccess: true,
|
||||
targetLink: linkNode.href.replace(linkNode.hostname, siteHostName),
|
||||
hasLeakage: titleNodeText.includes("无码") || titleNodeText.includes("Uncensored"),
|
||||
hasSubtitle: titleNodeText.includes("字幕") || titleNodeText.includes("subtitle"),
|
||||
};
|
||||
} else {
|
||||
return { targetLink: "", isSuccess: false, hasSubtitle: false, hasLeakage: false };
|
||||
}
|
||||
}
|
||||
return query();
|
||||
}
|
||||
|
||||
async function xhr(siteItem: SiteItem, targetLink: string, CODE: string) {
|
||||
const xhrPromise: Promise<xhrResult> = new Promise((resolve) => {
|
||||
GM_xmlhttpRequest({
|
||||
method: "GET",
|
||||
url: targetLink,
|
||||
onload: (response) => {
|
||||
if (siteItem.fetcher === "get") {
|
||||
// 直接 get 网页,且 get 结果为 404,大概是对应网站没有资源
|
||||
if (response.status === 404) {
|
||||
resolve({
|
||||
isSuccess: false,
|
||||
targetLink,
|
||||
hasSubtitle: false,
|
||||
hasLeakage: false,
|
||||
msg: "应该是没有资源",
|
||||
});
|
||||
}
|
||||
// 直接 get 网页,成功,需要进一步解析 videoPage,获取字幕等信息
|
||||
else {
|
||||
const { hasSubtitle, hasLeakage, isSuccess } = videoPageParser(
|
||||
response.responseText,
|
||||
siteItem.domQuery,
|
||||
);
|
||||
resolve({ isSuccess, targetLink, hasSubtitle, hasLeakage, msg: "[get],存在资源" });
|
||||
}
|
||||
}
|
||||
// 需要解析 searchPage
|
||||
else if (siteItem.fetcher === "parser") {
|
||||
const { targetLink, isSuccess, hasLeakage, hasSubtitle } = serachPageParser(
|
||||
response.responseText,
|
||||
siteItem.domQuery,
|
||||
siteItem.hostname,
|
||||
CODE,
|
||||
);
|
||||
resolve({
|
||||
isSuccess,
|
||||
targetLink: isSuccess ? targetLink : targetLink,
|
||||
hasSubtitle,
|
||||
hasLeakage,
|
||||
msg: "[parser]存在资源",
|
||||
});
|
||||
}
|
||||
},
|
||||
onerror: (error) => {
|
||||
resolve({
|
||||
isSuccess: false,
|
||||
targetLink: targetLink,
|
||||
hasSubtitle: false,
|
||||
hasLeakage: false,
|
||||
msg: error.error,
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
return xhrPromise;
|
||||
}
|
||||
|
||||
export default xhr;
|
||||
import { gmGet, isCaseInsensitiveEqual, isErrorCode, tagsQuery } from "./";
|
||||
import type { DomQuery_get, SiteItem, SiteItem_get, SiteItem_parser } from "./siteList";
|
||||
|
||||
export type FetchResult = {
|
||||
isSuccess: boolean;
|
||||
resultLink?: string;
|
||||
tag?: string;
|
||||
multipResLink?: string;
|
||||
multipleRes?: boolean;
|
||||
};
|
||||
|
||||
/** 针对视频播放页进行解析,寻找字幕等信息 */
|
||||
function videoPageParser(responseText: string, { subQuery, leakQuery, videoQuery }: DomQuery_get) {
|
||||
const doc = new DOMParser().parseFromString(responseText, "text/html");
|
||||
|
||||
const subNode = subQuery ? doc.querySelector<HTMLElement>(subQuery) : "";
|
||||
const subNodeText = subNode ? subNode.innerHTML : "";
|
||||
const leakNode = leakQuery ? doc.querySelector<HTMLElement>(leakQuery) : null;
|
||||
const leakNodeText = leakNode ? leakNode.innerHTML : "";
|
||||
/** 部分网站收录视频,但是未提供播放资源,所以需要使用 videoQuery 进一步检测是否存在在线播放。
|
||||
* videoQuery 为 undefine 时,不需要查找 video
|
||||
*/
|
||||
const videoNode = videoQuery ? doc.querySelector<HTMLElement>(videoQuery) : true;
|
||||
return {
|
||||
isSuccess: !!videoNode,
|
||||
tag: tagsQuery({ leakageText: leakNodeText, subtitleText: subNodeText }),
|
||||
};
|
||||
}
|
||||
|
||||
function searchPageCodeCheck(
|
||||
titleNodes: NodeListOf<Element> | never[],
|
||||
siteItem: SiteItem_parser,
|
||||
CODE: string,
|
||||
) {
|
||||
if (!titleNodes || titleNodes.length === 0) return { isSuccess: false, titleNodeText: "" };
|
||||
const codeRegex = /[a-zA-Z]{3,5}-\d{3,5}/;
|
||||
if (siteItem.strictParser) {
|
||||
const nodes = Array.from(titleNodes);
|
||||
const passNodes = nodes.filter((node) => {
|
||||
const nodeCode = node.outerHTML.match(codeRegex);
|
||||
return isCaseInsensitiveEqual(nodeCode?.[0], CODE);
|
||||
});
|
||||
const titleNodeText = passNodes.map((node) => node.outerHTML).join(" ");
|
||||
return {
|
||||
titleNodeText,
|
||||
isSuccess: passNodes.length > 0,
|
||||
multipleRes: passNodes.length > 1,
|
||||
};
|
||||
} else {
|
||||
const titleNode = titleNodes[siteItem.domQuery.listIndex ?? 0];
|
||||
const titleNodeText = titleNode ? titleNode?.outerHTML : "";
|
||||
const matchCode = titleNodeText.match(codeRegex);
|
||||
const isSuccess = isCaseInsensitiveEqual(matchCode?.[0], CODE);
|
||||
return { titleNodeText, isSuccess, multipleRes: titleNodes.length > 1 };
|
||||
}
|
||||
}
|
||||
|
||||
/** 针对 fetcher==="parser" 时的搜索结果页进行解析,寻找是否存在视频资源。
|
||||
* linkQuery & titleQuery 都是必须,
|
||||
* linkQuery 有结果且 titleQuery 有结果包含 code,返回 isSuccess。
|
||||
* 再检查下 title 中是否含有字幕信息等
|
||||
*/
|
||||
function serachPageParser(responseText: string, siteItem: SiteItem_parser, CODE: string) {
|
||||
const { linkQuery, titleQuery } = siteItem.domQuery;
|
||||
const doc = new DOMParser().parseFromString(responseText, "text/html");
|
||||
|
||||
const titleNodes = titleQuery ? doc.querySelectorAll(titleQuery) : [];
|
||||
const { isSuccess, titleNodeText, multipleRes } = searchPageCodeCheck(titleNodes, siteItem, CODE);
|
||||
|
||||
const linkNodes = linkQuery ? doc.querySelectorAll<HTMLAnchorElement>(linkQuery) : [];
|
||||
const linkNode = linkNodes[siteItem.domQuery.listIndex ?? 0];
|
||||
|
||||
if (!isSuccess) {
|
||||
return { isSuccess: false };
|
||||
}
|
||||
const resultLinkText = linkNode.href.replace(linkNode.hostname, siteItem.hostname);
|
||||
return {
|
||||
isSuccess: true,
|
||||
resultLink: resultLinkText,
|
||||
multipleRes,
|
||||
tag: tagsQuery({ leakageText: titleNodeText, subtitleText: titleNodeText }),
|
||||
};
|
||||
}
|
||||
|
||||
type Args = {
|
||||
siteItem: SiteItem;
|
||||
targetLink: string;
|
||||
CODE: string;
|
||||
};
|
||||
|
||||
const baseFetcher = async ({ siteItem, targetLink, CODE }: Args): Promise<FetchResult> => {
|
||||
if (siteItem.fetchType === "false") {
|
||||
return Promise.resolve({
|
||||
isSuccess: true,
|
||||
resultLink: targetLink,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await gmGet({ url: targetLink });
|
||||
if (isErrorCode(response.status)) {
|
||||
// 请求 404,大概是对应网站没有资源
|
||||
throw Error(String(response.status));
|
||||
}
|
||||
if (siteItem.fetchType === "get") {
|
||||
// 直接 get 网页,成功,需要进一步解析 videoPage,获取字幕等信息
|
||||
return {
|
||||
resultLink: targetLink,
|
||||
...videoPageParser(response.responseText, siteItem.domQuery),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...serachPageParser(response.responseText, siteItem, CODE),
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
isSuccess: false,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/** jable 有些域名是带 -c */
|
||||
const javbleFetcher = async (args: Args): Promise<FetchResult> => {
|
||||
const res = await baseFetcher(args);
|
||||
if (res.isSuccess) return res;
|
||||
const newLink = args.targetLink.slice(0, -1) + "-c/";
|
||||
return await baseFetcher({ ...args, targetLink: newLink });
|
||||
};
|
||||
|
||||
export const fetcher = (args: Args) => {
|
||||
if (args.siteItem.name === "Jable") {
|
||||
return javbleFetcher(args);
|
||||
}
|
||||
|
||||
return baseFetcher(args);
|
||||
};
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "preserve",
|
||||
"jsxFactory": "h",
|
||||
"jsxFragmentFactory": "Fragment"
|
||||
},
|
||||
"include": ["./src/**/*.ts"],
|
||||
"exclude": ["src/buckup"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "preserve",
|
||||
"baseUrl": "./",
|
||||
"paths": { "@/*": ["./src/*"] }
|
||||
},
|
||||
"include": ["./src/**/*.ts", "./src/**/*.tsx"],
|
||||
"exclude": ["**/backup"]
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts", "src/utils/siteList.ts"]
|
||||
}
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
import { defineConfig } from "vite";
|
||||
import monkey, { cdn } from "vite-plugin-monkey";
|
||||
import monkey, { cdn, MonkeyUserScript } from "vite-plugin-monkey";
|
||||
import preact from "@preact/preset-vite";
|
||||
import type { MonkeyUserScript } from "vite-plugin-monkey";
|
||||
import urlConfig from "./script/urlConfig";
|
||||
|
||||
import { siteList } from "./src/utils/siteList";
|
||||
const connectList = siteList.map((site) => site.hostname);
|
||||
// const PARALLEL =
|
||||
// "https://raw.githubusercontent.com/Tampermonkey/utils/refs/heads/main/requires/gh_2215_make_GM_xhr_more_parallel_again.js";
|
||||
const PARALLEL = "https://update.greasyfork.org/scripts/522123/1511104/tampermonkey%20parallel.js";
|
||||
|
||||
const UserscriptConfig: MonkeyUserScript = {
|
||||
author: "mission522",
|
||||
version: "1.0.4",
|
||||
version: "1.2.11",
|
||||
license: "MIT",
|
||||
name: "JAV 添加跳转在线观看 三合一",
|
||||
match: [
|
||||
"*://*/cn/?v=jav*",
|
||||
// "*://*.javdb.com/*", "*://*.javbus.com/*", "*://*.seejav.com/*", "*://*.seejav.cc/*", "*://*.javsee.com/*", "*://*.javlib.com/*", "*://*.javlibrary.com/*",
|
||||
],
|
||||
include: [
|
||||
/^https?:\/\/(\w*\.)?javdb(\d)*\.com.*$/,
|
||||
/^https?:\/\/(\w*\.)?(javbus|seejav|javsee)*\.(com|cc|me|life).*$/,
|
||||
/^https?:\/\/(\w*\.)?(javlib|javlibrary)*\.com.*$/,
|
||||
],
|
||||
name: "JAV 添加跳转在线观看",
|
||||
icon: "https://javdb.com/favicon-32x32.png",
|
||||
namespace: "https://greasyfork.org/zh-CN/scripts/429173",
|
||||
description:
|
||||
"在 JavDB、JavBus、JavLibrart 网站的影片详情页添加跳转在线播放按钮,并在按钮上标注是否支持在线播放、包含无码或包含字幕",
|
||||
connect: connectList,
|
||||
description: "为 JavDB、JavBus、JavLibrary 这三个站点添加跳转在线观看的链接",
|
||||
...urlConfig,
|
||||
require: [PARALLEL],
|
||||
};
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
minify: false,
|
||||
rollupOptions: {},
|
||||
// target:''
|
||||
},
|
||||
|
||||
resolve: { alias: { "@": "/src/" } },
|
||||
esbuild: { charset: "utf8" },
|
||||
plugins: [
|
||||
preact(),
|
||||
monkey({
|
||||
@@ -38,7 +38,6 @@ export default defineConfig({
|
||||
preact: cdn.jsdelivr("preact", "dist/preact.min.js"),
|
||||
},
|
||||
},
|
||||
|
||||
userscript: UserscriptConfig,
|
||||
}),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user