Add Paul's Toolbox Contents

This commit is contained in:
奇趣保罗 2024-03-01 18:33:29 +08:00
parent 65b2b86b78
commit 3ef694a468
28 changed files with 1534 additions and 366 deletions

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"editor.tabSize": 2
}

55
assets/global.css Normal file
View File

@ -0,0 +1,55 @@
* {
box-sizing: border-box;
}
body {
margin: 0;
}
h1, h2, h3, h4, h5, h6, p {
margin: 0;
}
h2 {
font-size: 1.2em;
font-weight: inherit;
}
button {
padding: 0;
border: none;
cursor: pointer;
background-color: transparent;
}
img {
max-width: 100%;
}
img, svg {
vertical-align: middle;
}
select, input, textarea, form button {
display: block;
padding: .75em;
width: 100%;
border-radius: .5em;
border: 1px solid #eee;
}
button {
color: inherit;
}
button, input, textarea {
font: inherit;
}
form button {
background-color: #eee;
}
textarea {
resize: vertical;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

35
assets/icons.tsx Normal file
View File

@ -0,0 +1,35 @@
export function IconPaul() {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200"><path fill="currentColor" d="M22.8 42.4s14.4 2.4 20.8 2S60 39.6 60 39.6s.8-13.6 44.4-14c58-.4 71.6 61.6 71.6 61.6s4.4 19.2 12 27.6c5.2 3.6 11.6 2 11.6 2s-9.2 6-14.4 7.2c-5.2.8-12.8-1.6-12.8-1.6L166 148l-6-6.8-2 .8-4.4 9.2V142l-2 .8s-1.6 8-2.8 12-4.8 8.8-4.8 8.8l-3.6-4.8s-.4 2.4-1.6 3.6l-2.8 2.8-2-3.6s-12 10.8-37.2 10.8-40-12-40-12l-2.8 2.8-6-12.8s-5.2-3.6-7.2-7.6-.8-7.2-.8-7.2l-7.2-10-4.4 5.6s-3.2-6.4-2.8-9.2c.4-2.8.8-7.2.8-7.2S19.2 114 12 112c-6.8-2-12-9.2-12-9.2s6 2 11.6-2 20.8-28.4 20.8-28.4l12.8-16.8s-5.6-.4-10.8-2.8c-5.2-2.4-11.6-10.4-11.6-10.4zM55.2 90l-4.4 40.4s3.2 7.6 5.2 12c1.6 4.4 2 7.6 2 7.6s6 14 39.6 14.8c42-.8 53.2-32 53.2-32s-6-14.8-10.4-18.8c-4-4-27.2-16.4-27.2-16.4s3.2 8 4.4 15.6c1.2 7.6 1.6 13.6 1.6 13.6s-11.6-3.2-15.2-6.4c-3.6-3.2-8.8-10-8.8-10s0 3.6.8 7.2 2.8 5.6 2.8 5.6-10.4-5.2-15.6-14.8c-5.6-9.6-9.2-24-9.2-24l-6.4 36.8L55.2 90z"/></svg>
);
}
export function IconRead() {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M20 22H6.5C4.567 22 3 20.433 3 18.5V5C3 3.34315 4.34315 2 6 2H20C20.5523 2 21 2.44772 21 3V21C21 21.5523 20.5523 22 20 22ZM19 20V17H6.5C5.67157 17 5 17.6716 5 18.5C5 19.3284 5.67157 20 6.5 20H19Z"></path></svg>
);
}
export function IconBili() {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M18.223 3.08609C18.7112 3.57424 18.7112 4.3657 18.223 4.85385L17.08 5.99622L18.25 5.99662C20.3211 5.99662 22 7.67555 22 9.74662V17.2466C22 19.3177 20.3211 20.9966 18.25 20.9966H5.75C3.67893 20.9966 2 19.3177 2 17.2466V9.74662C2 7.67555 3.67893 5.99662 5.75 5.99662L6.91625 5.99622L5.77466 4.85481C5.28651 4.36665 5.28651 3.5752 5.77466 3.08704C6.26282 2.59889 7.05427 2.59889 7.54243 3.08704L10.1941 5.73869C10.2729 5.81753 10.339 5.90428 10.3924 5.99638L13.6046 5.99661C13.6581 5.90407 13.7244 5.81691 13.8036 5.73774L16.4553 3.08609C16.9434 2.59793 17.7349 2.59793 18.223 3.08609ZM18.25 8.50662H5.75C5.09102 8.50662 4.55115 9.01654 4.50343 9.66333L4.5 9.75662V17.2566C4.5 17.9156 5.00992 18.4555 5.65671 18.5032L5.75 18.5066H18.25C18.909 18.5066 19.4489 17.9967 19.4966 17.3499L19.5 17.2566V9.75662C19.5 9.06626 18.9404 8.50662 18.25 8.50662ZM8.25 11.0066C8.94036 11.0066 9.5 11.5663 9.5 12.2566V13.5066C9.5 14.197 8.94036 14.7566 8.25 14.7566C7.55964 14.7566 7 14.197 7 13.5066V12.2566C7 11.5663 7.55964 11.0066 8.25 11.0066ZM15.75 11.0066C16.4404 11.0066 17 11.5663 17 12.2566V13.5066C17 14.197 16.4404 14.7566 15.75 14.7566C15.0596 14.7566 14.5 14.197 14.5 13.5066V12.2566C14.5 11.5663 15.0596 11.0066 15.75 11.0066Z"></path></svg>
);
}
export function IconBack() {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M7.82843 10.9999H20V12.9999H7.82843L13.1924 18.3638L11.7782 19.778L4 11.9999L11.7782 4.22168L13.1924 5.63589L7.82843 10.9999Z"></path></svg>
);
}
export function IconSetting() {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M9.95401 2.2106C11.2876 1.93144 12.6807 1.92263 14.0449 2.20785C14.2219 3.3674 14.9048 4.43892 15.9997 5.07103C17.0945 5.70313 18.364 5.75884 19.4566 5.3323C20.3858 6.37118 21.0747 7.58203 21.4997 8.87652C20.5852 9.60958 19.9997 10.736 19.9997 11.9992C19.9997 13.2632 20.5859 14.3902 21.5013 15.1232C21.29 15.7636 21.0104 16.3922 20.6599 16.9992C20.3094 17.6063 19.9049 18.1627 19.4559 18.6659C18.3634 18.2396 17.0943 18.2955 15.9997 18.9274C14.9057 19.559 14.223 20.6294 14.0453 21.7879C12.7118 22.067 11.3187 22.0758 9.95443 21.7906C9.77748 20.6311 9.09451 19.5595 7.99967 18.9274C6.90484 18.2953 5.63539 18.2396 4.54272 18.6662C3.61357 17.6273 2.92466 16.4164 2.49964 15.1219C3.41412 14.3889 3.99968 13.2624 3.99968 11.9992C3.99968 10.7353 3.41344 9.60827 2.49805 8.87524C2.70933 8.23482 2.98894 7.60629 3.33942 6.99923C3.68991 6.39217 4.09443 5.83576 4.54341 5.33257C5.63593 5.75881 6.90507 5.703 7.99967 5.07103C9.09364 4.43942 9.7764 3.3691 9.95401 2.2106ZM11.9997 14.9992C13.6565 14.9992 14.9997 13.6561 14.9997 11.9992C14.9997 10.3424 13.6565 8.99923 11.9997 8.99923C10.3428 8.99923 8.99967 10.3424 8.99967 11.9992C8.99967 13.6561 10.3428 14.9992 11.9997 14.9992Z"></path></svg>
);
}
export function IconSad() {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C17.5228 2 22 6.47715 22 12C22 13.6169 21.6162 15.1442 20.9348 16.4958C20.8633 16.2175 20.7307 15.9523 20.5374 15.7206L20.4142 15.5858L19 14.1716L17.5858 15.5858L17.469 15.713C16.8069 16.4988 16.8458 17.6743 17.5858 18.4142C18.014 18.8424 18.588 19.0358 19.148 18.9946C17.3323 20.8487 14.8006 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2ZM12 15C10.6199 15 9.37036 15.5592 8.46564 16.4633L8.30009 16.6368L9.24506 17.4961C10.035 17.1825 10.982 17 12 17C12.9049 17 13.7537 17.1442 14.4859 17.3965L14.7549 17.4961L15.6999 16.6368C14.7853 15.6312 13.4664 15 12 15ZM8.5 10C7.67157 10 7 10.6716 7 11.5C7 12.3284 7.67157 13 8.5 13C9.32843 13 10 12.3284 10 11.5C10 10.6716 9.32843 10 8.5 10ZM15.5 10C14.6716 10 14 10.6716 14 11.5C14 12.3284 14.6716 13 15.5 13C16.3284 13 17 12.3284 17 11.5C17 10.6716 16.3284 10 15.5 10Z"></path></svg>
);
}

View File

@ -0,0 +1,26 @@
import type { PlasmoMessaging } from "@plasmohq/messaging";
const handler: PlasmoMessaging.MessageHandler = async (req, res) => {
const { token, siteUrl } = await chrome.storage.local.get(["token", "siteUrl"]);
if (req.body.action === "submitAddForm") {
const { values } = req.body;
const formData = new FormData();
Object.keys(values).forEach((item) => {
formData.append(item, String(values[item]));
});
const addReq = await fetch(`${siteUrl}/api/read/add`, {
method: "POST",
body: formData,
headers: {
"paul-token-code": token,
},
}).then((res) => res.json());
res.send(addReq);
}
}
export default handler;

View File

@ -1,13 +0,0 @@
import type { PlasmoMessaging } from "@plasmohq/messaging"
const handler: PlasmoMessaging.MessageHandler = async (req, res) => {
const message = await fetch('https://paul.ren/api/note').then((res) => res.json());
console.log(message);
res.send({
message
})
}
export default handler

View File

@ -0,0 +1,13 @@
.form {
flex: 1;
gap: 1em;
display: flex;
flex-direction: column;
label {
opacity: .6;
display: block;
text-align: left;
margin-bottom: .5em;
}
}

View File

@ -0,0 +1,17 @@
import type { FormHTMLAttributes } from "react";
import { clsn } from "~utils";
import styles from "./form.module.less";
interface FormProps extends FormHTMLAttributes<HTMLFormElement> {
}
function Form({ children, className, ...props }: FormProps) {
return (
<form className={clsn(styles.form, className)} {...props}>
{children}
</form>
);
}
export default Form;

View File

@ -0,0 +1,44 @@
import { useEffect, useState } from "react";
import { createPortal } from "react-dom";
import { type MessageItem, addFn, removeFn } from "./utils";
import styles from "./message.module.less";
function Message() {
const [message, setMessage] = useState<MessageItem>();
useEffect(() => {
const fn = (nextMessage: MessageItem) => {
const key = `${Math.ceil(performance.now())}-${Math.round(Math.random() * 100)}`;
setMessage({ ...nextMessage, key });
setTimeout(() => {
onClose();
}, nextMessage.duration || 5000);
};
addFn(fn);
return () => {
removeFn(fn);
};
}, []);
const onClose = () => {
setMessage(undefined);
}
return createPortal((
<div className={styles.message}>
{message && (
<div key={message.key} className={styles.item}>
<span className={styles.content}>{message.content}</span>
<button className={styles.close} onClick={() => onClose()}>×</button>
</div>
)}
</div>
), document.body);
}
export default Message;

View File

@ -0,0 +1,34 @@
.message {
left: 0;
right: 0;
bottom: 5em;
z-index: 2;
margin: 0 1em;
display: flex;
position: fixed;
align-items: flex-end;
flex-direction: column;
}
.item {
color: #fff;
padding: 1em;
margin: 0 auto;
max-width: 50em;
overflow: hidden;
position: relative;
border-radius: .5em;
background-color: rgba(0, 0, 0, .6);
box-shadow: 0 0 5em rgba(0, 0, 0, .1);
.content, .close {
vertical-align: middle;
}
.close {
line-height: 1;
cursor: pointer;
font-size: 1.5em;
margin-left: 1em;
}
}

View File

@ -0,0 +1,27 @@
export interface MessageItem {
key?: string;
content: React.ReactNode;
duration?: number;
}
type MessageFn = (Message: MessageItem) => void;
const addMessageFn: MessageFn[] = [];
export const add = (Message: MessageItem) => {
addMessageFn.forEach((item) => {
item(Message);
});
}
export const addFn = (fn: MessageFn) => {
return addMessageFn.push(fn) - 1;
}
export const removeFn = (fn: MessageFn) => {
const index = addMessageFn.indexOf(fn);
if (index > -1) {
addMessageFn.splice(index, 1);
}
}

View File

@ -0,0 +1,28 @@
import { IconSad } from "~assets/icons";
import type { PropsWithChildren } from "react";
import styles from "./placeholder.module.less";
interface PlaceholderProps extends PropsWithChildren {
className?: string;
show?: boolean;
value: string;
}
function Placeholder({ className, show, value, children }: PlaceholderProps) {
if (show === undefined || show) {
return (
<div className={styles.placeholder}>
<IconSad />
<p>{value}</p>
</div>
);
}
return (
<div className={className}>
{children}
</div>
);
}
export default Placeholder;

View File

@ -0,0 +1,13 @@
.placeholder {
margin: 2em 0;
text-align: center;
svg {
width: 4em;
margin-bottom: 1em;
}
p {
opacity: .6;
}
}

44
contents/bili.ts Normal file
View File

@ -0,0 +1,44 @@
import type { PlasmoCSConfig } from "plasmo";
export const config: PlasmoCSConfig = {
matches: ["https://mall.bilibili.com/*"]
};
chrome.runtime.onMessage.addListener((req, sender, send) => {
if (req.type === "toolbox:getBiliToy") {
const item = document.querySelectorAll<HTMLDivElement>(".silde-item");
if (item.length) {
const project = document.querySelector<HTMLDivElement>(".tagC-ip .tagC-ip-con").innerText;
const made = document.querySelector<HTMLDivElement>(".tagC-ip .tagC-ip-con-brand").innerText;
const projectExp = new RegExp(`\s?${project}\s?`);
const madeExp = new RegExp(`\s?${made}\s?`);
const name = document.querySelector<HTMLDivElement>(".title-text-wrap")
.innerText.replace(/\[\S+\]/, "")
.replace(projectExp, "")
.replace(madeExp, "")
.replaceAll(/\s?Q版手办\s?|\s?粘土人\s?/g, "")
.trim();
const images = [];
item.forEach((item) => {
images.push(
item.style.backgroundImage.replace("url(\"", "").replace("\")", "").replace("//", "https://")
);
});
send({
name,
project,
made: document.querySelector<HTMLDivElement>(".tagC-ip .tagC-ip-con-brand").innerText,
sale: document.querySelector<HTMLDivElement>(".item-complex:nth-child(4) .item-complex-value").innerText.replace("-", "/"),
images,
});
}
else {
send(false);
}
}
});

91
contents/read.ts Normal file
View File

@ -0,0 +1,91 @@
const getAuthor = () => {
const metaAuthor = document.querySelector<HTMLMetaElement>(`meta[name="author"]`);
const metaOgAuthor = document.querySelector<HTMLMetaElement>(`meta[property="og:article:author"]`);
if (metaAuthor) {
return metaAuthor.getAttribute("content");
}
if (metaOgAuthor) {
return metaOgAuthor.getAttribute("content");
}
return "";
}
const getImage = () => {
const metaOgImage = document.querySelector<HTMLMetaElement>(`meta[property="og:image"]`);
if (metaOgImage) {
return metaOgImage.getAttribute("content");
}
return "";
}
const getDesc = () => {
const articleFirstParagraph = document.querySelector<HTMLParagraphElement>("article p") || document.querySelector<HTMLParagraphElement>("p");
const metaDescription = document.querySelector<HTMLMetaElement>(`meta[name="description"]`);
const metaOgDescription = document.querySelector<HTMLMetaElement>(`meta[property="og:description"]`);
if (metaOgDescription) {
return metaOgDescription.getAttribute("content");
}
if (metaDescription) {
return metaDescription.getAttribute("content");
}
if (articleFirstParagraph) {
return articleFirstParagraph.innerText;
}
return "";
}
const getSiteName = () => {
const title = document.title;
const metaSiteName = document.querySelector<HTMLMetaElement>(`meta[property="og:site_name"]`);
if (metaSiteName) {
return metaSiteName.getAttribute("content");
}
if (title.includes(" - ")) {
const start = title.lastIndexOf(" - ") + 3;
return title.substring(start);
}
if (title.includes(" | ")) {
const start = title.lastIndexOf(" | ") + 3;
return title.substring(start);
}
return "";
}
const getFrom = () => {
if (location.host === "mp.weixin.qq.com") {
return "wechat";
}
return "web";
}
chrome.runtime.onMessage.addListener((req, sender, send) => {
console.log(req);
if (req.type === "toolbox:getInfo") {
send({
title: document.title,
link: `${location.origin}${location.pathname}`,
desc: getDesc(),
from: getFrom(),
author: getAuthor(),
image: getImage(),
sitename: getSiteName(),
});
}
});

95
hooks/useForm.ts Normal file
View File

@ -0,0 +1,95 @@
import { useEffect, useRef, type FormEvent } from "react";
type Values = Record<string, string | number>;
interface Props {
initialValues?: Values;
onSubmit?: (values: Values) => void;
}
type a = (values: Values) => void;
interface UseFormReturns {
getValue: () => Values;
setValue: (name: string, value: string | number) => void;
setValues: (values: Values) => void;
bindInput: (name: string) => any;
onSubmit: (a: a) => (ev: FormEvent) => void;
}
function useForm({ initialValues }: Props) {
const formRef = useRef<HTMLFormElement>();
const inputs = useRef({});
const values = useRef({ ...initialValues });
const inst = useRef<UseFormReturns>();
if (!inst.current) {
const getValue = () => {
return values.current;
}
const setValue = (name, value) => {
console.log(inputs.current[name]);
if (inputs.current[name]) {
values.current[name] = value;
inputs.current[name].value = value;
}
}
const setValues = (values: Values) => {
Object.keys(values).forEach((key) => {
setValue(key, values[key]);
});
}
const bindInput = (name: string) => {
console.log('bindinput')
return {
name,
id: name,
ref: (target) => {
console.log('ref', target);
if (target) {
inputs.current[name] = target;
}
return undefined;
},
defaultValue: initialValues[name],
onChange: (ev) => {
values.current[ev.target.name] = ev.target.value;
},
}
}
const onSubmit = (fn) => (ev: SubmitEvent) => {
ev.preventDefault();
fn(getValue());
}
inst.current = {
getValue,
setValue,
setValues,
bindInput,
onSubmit,
};
}
useEffect(() => {
console.log('form', formRef.current);
// formRef.current.onsubmit = (ev: SubmitEvent) => {
// ev.preventDefault();
// console.log(t.current?.getValue());
// console.log(inputs);
// }
}, []);
return inst.current;
}
export default useForm;

72
options/index.tsx Normal file
View File

@ -0,0 +1,72 @@
import { useState, type ChangeEvent, type FormEvent, useEffect } from "react";
import Form from "~components/ui/form";
import { IconPaul } from "~assets/icons";
import Message from "~components/ui/message";
import { add } from "~components/ui/message/utils";
import "~assets/global.css";
import styles from "./options.module.less";
function Options() {
const [state, setState] = useState({
token: "",
siteUrl: "",
});
useEffect(() => {
chrome.storage.local.get(["token", "siteUrl"]).then((res) => {
setState((prevState) => ({ ...prevState, ...(res as any)}));
});
}, []);
const onChange = (ev: ChangeEvent<HTMLInputElement>) => {
const { name, value } = ev.target;
if (!name) {
return;
}
setState((prevState) => ({
...prevState,
[name]: value,
}));
}
const onSubmit = (ev: FormEvent) => {
ev.preventDefault();
chrome.storage.local.set(state);
add({
content: "保存成功",
});
}
return (
<>
<div className={styles.options} onSubmit={onSubmit}>
<h1 className={styles.title}>
<IconPaul />
</h1>
<Form className={styles.form}>
<div>
<label htmlFor="token"> Token</label>
<input id="token" name="token" value={state.token} onChange={onChange} />
</div>
<div>
<label htmlFor="siteUrl"> API </label>
<input id="siteUrl" name="siteUrl"
placeholder="不包含 /api"
value={state.siteUrl} onChange={onChange}
/>
</div>
<button type="submit"></button>
</Form>
</div>
<Message />
</>
);
}
export default Options;

View File

@ -0,0 +1,17 @@
.options {
padding: 0 1em;
max-width: 40em;
margin: 3em auto;
.title {
display: flex;
align-items: center;
margin-bottom: 1.5em;
svg {
width: 2em;
color: #28b9be;
margin-right: .5em;
}
}
}

View File

@ -1,9 +1,9 @@
{
"name": "give-blue-newt",
"displayName": "Give blue newt",
"name": "home-toolbox",
"displayName": "小窝工具箱",
"version": "0.0.1",
"description": "A basic Plasmo extension.",
"author": "Plasmo Corp. <foss@plasmo.com>",
"description": "一个借助浏览器插件特性与小窝后端快速交互的方式",
"author": "Dreamer-Paul. <dreamer_paul@126.com>",
"scripts": {
"dev": "plasmo dev",
"build": "plasmo build",
@ -11,7 +11,7 @@
},
"dependencies": {
"@plasmohq/messaging": "^0.5.0",
"plasmo": "0.82.1",
"plasmo": "0.84.1",
"react": "18.2.0",
"react-dom": "18.2.0"
},
@ -21,10 +21,14 @@
"@types/node": "20.5.0",
"@types/react": "18.2.20",
"@types/react-dom": "18.2.7",
"less": "^4.2.0",
"prettier": "3.0.2",
"typescript": "5.1.6"
},
"manifest": {
"permissions": [
"tabs", "storage"
],
"host_permissions": [
"https://*/*"
]

File diff suppressed because it is too large Load Diff

View File

@ -1,43 +0,0 @@
import { useEffect, useState } from "react"
import { sendToBackground } from "@plasmohq/messaging"
function IndexPopup() {
const [data, setData] = useState("")
useEffect(() => {
const resp = sendToBackground({
name: "test",
body: {
id: 123
}
}).then((res) => {
console.log(res);
})
console.log(resp)
}, []);
return (
<div
style={{
display: "flex",
flexDirection: "column",
padding: 16
}}>
<h2>
Welcome to your test
<a href="https://www.plasmo.com" target="_blank">
{" "}
Plasmo
</a>{" "}
Extension!
</h2>
<input onChange={(e) => setData(e.target.value)} value={data} />
<a href="https://docs.plasmo.com" target="_blank">
View Docs
</a>
</div>
)
}
export default IndexPopup

22
popup/bili.module.less Normal file
View File

@ -0,0 +1,22 @@
.bili {
gap: 1em;
display: flex;
.image {
flex: 0 0 40%;
}
.selector {
gap: 1em;
display: flex;
flex-wrap: wrap;
margin-top: 1em;
img {
width: 3em;
height: 3em;
cursor: pointer;
border: 1px solid #eee;
}
}
}

126
popup/bili.tsx Normal file
View File

@ -0,0 +1,126 @@
import { useEffect, useState, type FormEvent } from "react";
import Placeholder from "~components/ui/placeholder";
import Form from "~components/ui/form";
import { IconBack } from "~assets/icons";
import styles from "./popup.module.less";
import stylesB from "./bili.module.less";
import useForm from "~hooks/useForm";
interface ReadProps {
onBack: () => void;
}
interface FormValue {
name: string;
ename: string;
project: string;
made: string;
sale: string;
type: number;
desc: string;
}
const getInfo = async () => {
let tab: chrome.tabs.Tab;
[tab] = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
if (tab && !tab.url.includes("chrome")) {
const a = await chrome.tabs.sendMessage(tab.id, { type: 'toolbox:getBiliToy' });
console.log('get toy:', a);
return a;
}
}
function Toy({ onBack }: ReadProps) {
const { bindInput, setValues, onSubmit } = useForm<FormValue>({
initialValues: {
type: 3,
},
onSubmit: (values) => {
console.log(values);
},
});
const [imgs, setImgs] = useState([]);
const [currentImg, setCurrentImg] = useState(0);
const [failed, setFailed] = useState(false);
useEffect(() => {
getInfo().then((res) => {
if (!res) {
setFailed(true);
return;
}
const { image, ...othors } = res;
setValues(othors);
setImgs(res.images);
setCurrentImg(0);
})
}, []);
return (
<div className={styles.tab} style={{ width: 600 }}>
<header className={styles.header}>
<button className={styles.back} onClick={onBack}>
<IconBack />
</button>
<h2></h2>
</header>
<main className={styles.body}>
<Placeholder className={stylesB.bili} show={failed} value="非 B 站会员购页面">
<div className={stylesB.image}>
<img src={imgs[currentImg]} alt="" />
<div className={stylesB.selector}>
{imgs.map((item, index) => (
<img src={item} alt="" onClick={() => setCurrentImg(index)} />
))}
</div>
</div>
<Form className={stylesB.form} onSubmit={onSubmit((data) => console.log(data))}>
<div>
<label htmlFor="name"></label>
<input {...bindInput("name")} required placeholder="中文名" />
</div>
<div>
<label htmlFor="ename"></label>
<input {...bindInput("ename")} required placeholder="英文名" />
</div>
<div>
<label htmlFor="project"></label>
<input {...bindInput("project")} required placeholder="作品" />
</div>
<div>
<label htmlFor="made"></label>
<input {...bindInput("made")} required placeholder="制作" />
</div>
<div>
<label htmlFor="sale"></label>
<input {...bindInput("sale")} required placeholder="发售日期" />
</div>
<div>
<label htmlFor="type"></label>
<select {...bindInput("type")}>
<option value={1}></option>
<option value={2}></option>
<option value={3}></option>
</select>
</div>
<div>
<label htmlFor="desc"></label>
<textarea {...bindInput("desc")} rows={5} required placeholder="描述"></textarea>
</div>
<button type="submit"></button>
</Form>
</Placeholder>
</main>
</div>
);
}
export default Toy;

49
popup/index.tsx Normal file
View File

@ -0,0 +1,49 @@
import { useState } from "react";
import Menu from "./menu";
import Read from "./read";
import Toy from "./bili";
import Message from "~components/ui/message";
import { clsn } from "~utils";
import "assets/global.css";
import styles from "./popup.module.less";
function IndexPopup() {
const [data, setData] = useState("")
const [tab, setTab] = useState<string>();
const onBack = () => {
setTab(undefined);
}
const onClickMenu = (value: string) => {
if (value === "options") {
chrome.runtime.openOptionsPage();
return;
}
setTab(value);
}
const renderBody = () => {
if (tab === "read") {
return <Read onBack={onBack} />;
}
if (tab === "toy") {
return <Toy onBack={onBack} />;
}
return <Menu onClick={onClickMenu} />;
}
return (
<>
<div className={clsn(styles.root, tab && styles.hasTab)}>
{renderBody()}
</div>
<Message />
</>
);
}
export default IndexPopup;

39
popup/menu.tsx Normal file
View File

@ -0,0 +1,39 @@
import { IconBili, IconRead, IconSetting } from "~assets/icons";
import styles from "./popup.module.less";
const items = [
{
name: "在看",
icon: <IconRead />,
value: "read",
},
{
name: "增加手办",
icon: <IconBili />,
value: "toy",
},
{
name: "插件设置",
icon: <IconSetting />,
value: "options",
}
];
interface MenuProps {
onClick: (value: string) => void;
}
function Menu({ onClick }: MenuProps) {
return (
<div className={styles.menu}>
{items.map((item) => (
<div key={item.value} className={styles.item} onClick={() => onClick(item.value)}>
{item.icon && item.icon}
{item.name}
</div>
))}
</div>
);
}
export default Menu;

60
popup/popup.module.less Normal file
View File

@ -0,0 +1,60 @@
.root {
color: #333;
font-size: 14px;
min-width: 200px;
&.hasTab {
min-width: 300px;
}
}
.menu {
.item {
display: flex;
cursor: pointer;
padding: .75em 1em;
border-bottom: 1px solid #eee;
transition: background-color .3s;
svg {
width: 1em;
color: #28b9be;
margin-right: .5em;
}
}
.item:hover {
background-color: #eee;
}
}
.tab {
.header {
padding: .75em;
position: relative;
text-align: center;
border-bottom: 1px solid #eee;
.back {
top: 0;
left: 0;
bottom: 0;
width: 2em;
font-size: 1.3em;
position: absolute;
transition: background-color .3s;
&:hover {
background-color: #eee;
}
svg {
width: 1em;
}
}
}
.body {
padding: 1em;
}
}

115
popup/read.tsx Normal file
View File

@ -0,0 +1,115 @@
import { sendToBackground } from "@plasmohq/messaging";
import { useEffect, useState } from "react";
import Form from "~components/ui/form";
import { IconBack } from "~assets/icons";
import styles from "./popup.module.less";
import useForm from "~hooks/useForm";
import { add } from "~components/ui/message/utils";
interface ReadProps {
onBack: () => void;
}
interface FormValue {
title: string;
link: string;
desc: string;
tips: string;
}
const getInfo = async () => {
const [tab] = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
if (tab) {
return await chrome.tabs.sendMessage(tab.id, { type: 'toolbox:getInfo' });
}
}
const submitForm = (body: FormValue) => {
sendToBackground({
name: "read",
body: {
action: "submitAddForm",
values: body,
},
}).then((res) => {
if (res.status === "Success") {
add({
content: "提交成功",
});
}
else {
add({
content: res.msg,
});
}
});
}
function Read({ onBack }: ReadProps) {
const [count, setCount] = useState(0);
const { bindInput, setValues, onSubmit } = useForm<FormValue>({
initialValues: {
test: 'test',
},
onSubmit: (values) => {
submitForm(values);
},
});
useEffect(() => {
getInfo().then((res) => {
if (!res) {
return;
}
setValues(res);
})
}, []);
return (
<div className={styles.tab}>
<header className={styles.header}>
<button className={styles.back} onClick={onBack}>
<IconBack />
</button>
<h2></h2>
</header>
<main className={styles.body}>
<Form onSubmit={onSubmit(submitForm)}>
<input {...bindInput("title")} required placeholder="标题" />
<input {...bindInput("link")} required placeholder="链接" />
<div>
<label htmlFor="desc"></label>
<textarea {...bindInput("desc")} rows={5} required placeholder="概括"></textarea>
</div>
<div>
<label htmlFor="tips"> / </label>
<textarea {...bindInput("tips")} rows={5} placeholder="推荐理由"></textarea>
</div>
<div>
<label htmlFor="author"></label>
<input {...bindInput("author")} placeholder="作者" />
</div>
<div>
<label htmlFor="image"></label>
<input {...bindInput("image")} placeholder="插图" />
</div>
<div>
<label htmlFor="sitename"></label>
<input {...bindInput("sitename")} placeholder="站点名称" />
</div>
<div>
<label htmlFor="tags"></label>
<input {...bindInput("tags")} placeholder="标签" />
</div>
<button type="submit"></button>
</Form>
{count}
<button onClick={() => setCount(prevCount => prevCount + 1)}>add</button>
</main>
</div>
);
}
export default Read;

4
utils/index.ts Normal file
View File

@ -0,0 +1,4 @@
// Classnames
export const clsn = (...clsn: (string | undefined | null | false)[]) => {
return clsn.filter(item => item).join(" ");
}