mirror of
https://github.com/aykhans/slash-e.git
synced 2025-04-18 21:19:44 +00:00
feat: implement extension's popup and options
This commit is contained in:
parent
b638d9cdf4
commit
f886bd7eb8
@ -0,0 +1 @@
|
|||||||
|
# Slash Browser Extension
|
@ -17,15 +17,19 @@
|
|||||||
"@mui/joy": "5.0.0-beta.0",
|
"@mui/joy": "5.0.0-beta.0",
|
||||||
"@plasmohq/storage": "^1.7.2",
|
"@plasmohq/storage": "^1.7.2",
|
||||||
"axios": "^1.4.0",
|
"axios": "^1.4.0",
|
||||||
|
"classnames": "^2.3.2",
|
||||||
|
"lodash-es": "^4.17.21",
|
||||||
"lucide-react": "^0.264.0",
|
"lucide-react": "^0.264.0",
|
||||||
"plasmo": "0.82.0",
|
"plasmo": "0.82.0",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-hot-toast": "^2.4.1"
|
"react-hot-toast": "^2.4.1",
|
||||||
|
"zustand": "^4.4.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@trivago/prettier-plugin-sort-imports": "4.1.0",
|
"@trivago/prettier-plugin-sort-imports": "4.1.0",
|
||||||
"@types/chrome": "0.0.241",
|
"@types/chrome": "0.0.241",
|
||||||
|
"@types/lodash-es": "^4.17.8",
|
||||||
"@types/node": "20.4.2",
|
"@types/node": "20.4.2",
|
||||||
"@types/react": "18.2.15",
|
"@types/react": "18.2.15",
|
||||||
"@types/react-dom": "18.2.7",
|
"@types/react-dom": "18.2.7",
|
||||||
|
58
extension/pnpm-lock.yaml
generated
58
extension/pnpm-lock.yaml
generated
@ -20,6 +20,12 @@ dependencies:
|
|||||||
axios:
|
axios:
|
||||||
specifier: ^1.4.0
|
specifier: ^1.4.0
|
||||||
version: 1.4.0
|
version: 1.4.0
|
||||||
|
classnames:
|
||||||
|
specifier: ^2.3.2
|
||||||
|
version: 2.3.2
|
||||||
|
lodash-es:
|
||||||
|
specifier: ^4.17.21
|
||||||
|
version: 4.17.21
|
||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: ^0.264.0
|
specifier: ^0.264.0
|
||||||
version: 0.264.0(react@18.2.0)
|
version: 0.264.0(react@18.2.0)
|
||||||
@ -35,6 +41,9 @@ dependencies:
|
|||||||
react-hot-toast:
|
react-hot-toast:
|
||||||
specifier: ^2.4.1
|
specifier: ^2.4.1
|
||||||
version: 2.4.1(csstype@3.1.2)(react-dom@18.2.0)(react@18.2.0)
|
version: 2.4.1(csstype@3.1.2)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
zustand:
|
||||||
|
specifier: ^4.4.1
|
||||||
|
version: 4.4.1(@types/react@18.2.15)(react@18.2.0)
|
||||||
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@trivago/prettier-plugin-sort-imports':
|
'@trivago/prettier-plugin-sort-imports':
|
||||||
@ -43,6 +52,9 @@ devDependencies:
|
|||||||
'@types/chrome':
|
'@types/chrome':
|
||||||
specifier: 0.0.241
|
specifier: 0.0.241
|
||||||
version: 0.0.241
|
version: 0.0.241
|
||||||
|
'@types/lodash-es':
|
||||||
|
specifier: ^4.17.8
|
||||||
|
version: 4.17.8
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: 20.4.2
|
specifier: 20.4.2
|
||||||
version: 20.4.2
|
version: 20.4.2
|
||||||
@ -2849,6 +2861,16 @@ packages:
|
|||||||
/@types/json-schema@7.0.12:
|
/@types/json-schema@7.0.12:
|
||||||
resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==}
|
resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==}
|
||||||
|
|
||||||
|
/@types/lodash-es@4.17.8:
|
||||||
|
resolution: {integrity: sha512-euY3XQcZmIzSy7YH5+Unb3b2X12Wtk54YWINBvvGQ5SmMvwb11JQskGsfkH/5HXK77Kr8GF0wkVDIxzAisWtog==}
|
||||||
|
dependencies:
|
||||||
|
'@types/lodash': 4.14.196
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@types/lodash@4.14.196:
|
||||||
|
resolution: {integrity: sha512-22y3o88f4a94mKljsZcanlNWPzO0uBsBdzLAngf2tp533LzZcQzb6+eZPJ+vCTt+bqF2XnvT9gejTLsAcJAJyQ==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/node@20.4.2:
|
/@types/node@20.4.2:
|
||||||
resolution: {integrity: sha512-Dd0BYtWgnWJKwO1jkmTrzofjK2QXXcai0dmtzvIBhcA+RsG5h8R3xlyta0kGOZRNfL9GuRtb1knmPEhQrePCEw==}
|
resolution: {integrity: sha512-Dd0BYtWgnWJKwO1jkmTrzofjK2QXXcai0dmtzvIBhcA+RsG5h8R3xlyta0kGOZRNfL9GuRtb1knmPEhQrePCEw==}
|
||||||
dev: true
|
dev: true
|
||||||
@ -3497,6 +3519,10 @@ packages:
|
|||||||
engines: {node: '>=6.0'}
|
engines: {node: '>=6.0'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/classnames@2.3.2:
|
||||||
|
resolution: {integrity: sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/cli-cursor@3.1.0:
|
/cli-cursor@3.1.0:
|
||||||
resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==}
|
resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@ -5259,6 +5285,10 @@ packages:
|
|||||||
p-locate: 5.0.0
|
p-locate: 5.0.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/lodash-es@4.17.21:
|
||||||
|
resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/lodash.merge@4.6.2:
|
/lodash.merge@4.6.2:
|
||||||
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
||||||
dev: true
|
dev: true
|
||||||
@ -6922,6 +6952,14 @@ packages:
|
|||||||
punycode: 2.3.0
|
punycode: 2.3.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/use-sync-external-store@1.2.0(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||||
|
dependencies:
|
||||||
|
react: 18.2.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/util-deprecate@1.0.2:
|
/util-deprecate@1.0.2:
|
||||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||||
|
|
||||||
@ -7026,3 +7064,23 @@ packages:
|
|||||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/zustand@4.4.1(@types/react@18.2.15)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-QCPfstAS4EBiTQzlaGP1gmorkh/UL1Leaj2tdj+zZCZ/9bm0WS7sI2wnfD5lpOszFqWJ1DcPnGoY8RDL61uokw==}
|
||||||
|
engines: {node: '>=12.7.0'}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '>=16.8'
|
||||||
|
immer: '>=9.0'
|
||||||
|
react: '>=16.8'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
immer:
|
||||||
|
optional: true
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@types/react': 18.2.15
|
||||||
|
react: 18.2.0
|
||||||
|
use-sync-external-store: 1.2.0(react@18.2.0)
|
||||||
|
dev: false
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import type { Shortcut } from "./types/proto/api/v2/shortcut_service_pb";
|
import type { Shortcut } from "@/types/proto/api/v2/shortcut_service_pb";
|
||||||
import { Storage } from "@plasmohq/storage";
|
import { Storage } from "@plasmohq/storage";
|
||||||
|
|
||||||
const storage = new Storage();
|
const storage = new Storage();
|
||||||
@ -9,11 +9,7 @@ chrome.tabs.onUpdated.addListener(async (tabId, _, tab) => {
|
|||||||
const matchResult = urlRegex.exec(tab.url);
|
const matchResult = urlRegex.exec(tab.url);
|
||||||
const sname = Array.isArray(matchResult) ? matchResult[1] : null;
|
const sname = Array.isArray(matchResult) ? matchResult[1] : null;
|
||||||
if (sname) {
|
if (sname) {
|
||||||
const shortcuts = (await storage.getItem("shortcuts")) as Shortcut[] | null;
|
const shortcuts = (await storage.getItem<Shortcut[]>("shortcuts")) || [];
|
||||||
if (!Array.isArray(shortcuts)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const shortcut = shortcuts.find((shortcut) => shortcut.name === sname);
|
const shortcut = shortcuts.find((shortcut) => shortcut.name === sname);
|
||||||
if (!shortcut) {
|
if (!shortcut) {
|
||||||
return;
|
return;
|
||||||
|
@ -4,13 +4,13 @@ import axios from "axios";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
import { Shortcut } from "@/types/proto/api/v2/shortcut_service_pb";
|
import { Shortcut } from "@/types/proto/api/v2/shortcut_service_pb";
|
||||||
|
import "../style.css";
|
||||||
import Icon from "./Icon";
|
import Icon from "./Icon";
|
||||||
import "./style.css";
|
|
||||||
|
|
||||||
function PullShortcutsButton() {
|
function PullShortcutsButton() {
|
||||||
const [domain] = useStorage("domain");
|
const [domain] = useStorage("domain");
|
||||||
const [accessToken] = useStorage("access_token");
|
const [accessToken] = useStorage("access_token");
|
||||||
const [shortcuts, setShortcuts] = useStorage("shortcuts");
|
const [, setShortcuts] = useStorage("shortcuts");
|
||||||
const [isPulling, setIsPulling] = useState(false);
|
const [isPulling, setIsPulling] = useState(false);
|
||||||
|
|
||||||
const handlePullShortcuts = async () => {
|
const handlePullShortcuts = async () => {
|
||||||
@ -30,13 +30,9 @@ function PullShortcutsButton() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full mt-4">
|
<Button loading={isPulling} color="neutral" variant="plain" size="sm" onClick={handlePullShortcuts}>
|
||||||
<Button loading={isPulling} className="w-full" onClick={handlePullShortcuts}>
|
<Icon.RefreshCcw className="w-4 h-auto" />
|
||||||
<Icon.RefreshCcw className="w-5 h-auto mr-1" />
|
</Button>
|
||||||
<span>Pull</span>
|
|
||||||
<span className="opacity-70 ml-1">{Array.isArray(shortcuts) ? `(${shortcuts.length})` : ""}</span>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,46 +0,0 @@
|
|||||||
import { Input } from "@mui/joy";
|
|
||||||
import { useStorage } from "@plasmohq/storage/hook";
|
|
||||||
import "../style.css";
|
|
||||||
import Icon from "./Icon";
|
|
||||||
|
|
||||||
const Setting = () => {
|
|
||||||
const [domain, setDomain] = useStorage("domain");
|
|
||||||
const [accessToken, setAccessToken] = useStorage("access_token");
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h2 className="flex flex-row justify-start items-center mb-2">
|
|
||||||
<Icon.Settings className="w-5 h-auto mr-1" />
|
|
||||||
<span className="text-lg">Setting</span>
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div className="w-full flex flex-col justify-start items-start mb-2">
|
|
||||||
<span className="mb-2 text-base">Domain</span>
|
|
||||||
<div className="relative w-full">
|
|
||||||
<Input
|
|
||||||
className="w-full"
|
|
||||||
type="text"
|
|
||||||
placeholder="The domain of your Slash instance"
|
|
||||||
value={domain}
|
|
||||||
onChange={(e) => setDomain(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="w-full flex flex-col justify-start items-start">
|
|
||||||
<span className="mb-2 text-base">Access Token</span>
|
|
||||||
<div className="relative w-full">
|
|
||||||
<Input
|
|
||||||
className="w-full"
|
|
||||||
type="text"
|
|
||||||
placeholder="The access token of your Slash instance"
|
|
||||||
value={accessToken}
|
|
||||||
onChange={(e) => setAccessToken(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Setting;
|
|
77
extension/src/components/ShortcutView.tsx
Normal file
77
extension/src/components/ShortcutView.tsx
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import type { Shortcut } from "@/types/proto/api/v2/shortcut_service_pb";
|
||||||
|
import { useStorage } from "@plasmohq/storage/hook";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import useFaviconStore from "../stores/favicon";
|
||||||
|
import Icon from "./Icon";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
shortcut: Shortcut;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ShortcutView = (props: Props) => {
|
||||||
|
const { shortcut } = props;
|
||||||
|
const faviconStore = useFaviconStore();
|
||||||
|
const [domain] = useStorage<string>("domain", "");
|
||||||
|
const [favicon, setFavicon] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
faviconStore.getOrFetchUrlFavicon(shortcut.link).then((url) => {
|
||||||
|
if (url) {
|
||||||
|
setFavicon(url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [shortcut.link]);
|
||||||
|
|
||||||
|
const handleShortcutLinkClick = () => {
|
||||||
|
const shortcutLink = `${domain}/s/${shortcut.name}`;
|
||||||
|
chrome.tabs.create({ url: shortcutLink });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
"group w-full px-3 py-2 flex flex-col justify-start items-start border rounded-lg hover:bg-gray-100 hover:shadow"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="w-full flex flex-row justify-start items-center">
|
||||||
|
<span className={classNames("w-5 h-5 flex justify-center items-center overflow-clip shrink-0")}>
|
||||||
|
{favicon ? (
|
||||||
|
<img className="w-full h-auto rounded-full" src={favicon} decoding="async" loading="lazy" />
|
||||||
|
) : (
|
||||||
|
<Icon.CircleSlash className="w-full h-auto text-gray-400" />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<div className="ml-1 w-[calc(100%-20px)] flex flex-col justify-start items-start">
|
||||||
|
<div className="w-full flex flex-row justify-start items-center">
|
||||||
|
<button
|
||||||
|
className={classNames(
|
||||||
|
"max-w-full flex flex-row px-1 mr-1 justify-start items-center cursor-pointer rounded-md hover:underline"
|
||||||
|
)}
|
||||||
|
onClick={handleShortcutLinkClick}
|
||||||
|
>
|
||||||
|
<div className="truncate">
|
||||||
|
<span>{shortcut.title}</span>
|
||||||
|
{shortcut.title ? (
|
||||||
|
<span className="text-gray-400">(s/{shortcut.name})</span>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span className="text-gray-400">s/</span>
|
||||||
|
<span className="truncate">{shortcut.name}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span className="hidden group-hover:block ml-1 cursor-pointer shrink-0">
|
||||||
|
<Icon.ExternalLink className="w-4 h-auto text-gray-600" />
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ShortcutView;
|
18
extension/src/components/ShortcutsContainer.tsx
Normal file
18
extension/src/components/ShortcutsContainer.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import type { Shortcut } from "@/types/proto/api/v2/shortcut_service_pb";
|
||||||
|
import { useStorage } from "@plasmohq/storage/hook";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import ShortcutView from "./ShortcutView";
|
||||||
|
|
||||||
|
const ShortcutsContainer = () => {
|
||||||
|
const [shortcuts] = useStorage<Shortcut[]>("shortcuts", (v) => (v ? v : []));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classNames("w-full grid grid-cols-2 gap-2")}>
|
||||||
|
{shortcuts.map((shortcut) => {
|
||||||
|
return <ShortcutView key={shortcut.id} shortcut={shortcut} />;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ShortcutsContainer;
|
15
extension/src/helpers/api.ts
Normal file
15
extension/src/helpers/api.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import type { Shortcut } from "@/types/proto/api/v2/shortcut_service_pb";
|
||||||
|
import { Storage } from "@plasmohq/storage";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
const storage = new Storage();
|
||||||
|
|
||||||
|
export const getShortcutList = () => {
|
||||||
|
const queryList = [];
|
||||||
|
return axios.get<Shortcut[]>(`/api/v1/shortcut?${queryList.join("&")}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getUrlFavicon = async (url: string) => {
|
||||||
|
const domain = await storage.getItem("domain");
|
||||||
|
return axios.get<string>(`${domain}/api/v1/url/favicon?url=${url}`);
|
||||||
|
};
|
5
extension/src/helpers/utils.ts
Normal file
5
extension/src/helpers/utils.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { isNull, isUndefined } from "lodash-es";
|
||||||
|
|
||||||
|
export const isNullorUndefined = (value: any) => {
|
||||||
|
return isNull(value) || isUndefined(value);
|
||||||
|
};
|
@ -1,10 +1,93 @@
|
|||||||
|
import { Button, Input } from "@mui/joy";
|
||||||
|
import { useStorage } from "@plasmohq/storage/hook";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Toaster, toast } from "react-hot-toast";
|
||||||
|
import Icon from "./components/Icon";
|
||||||
import "./style.css";
|
import "./style.css";
|
||||||
|
|
||||||
|
interface SettingState {
|
||||||
|
domain: string;
|
||||||
|
accessToken: string;
|
||||||
|
}
|
||||||
|
|
||||||
function IndexOptions() {
|
function IndexOptions() {
|
||||||
|
const [domain, setDomain] = useStorage<string>("domain", (v) => (v ? v : ""));
|
||||||
|
const [accessToken, setAccessToken] = useStorage<string>("access_token", (v) => (v ? v : ""));
|
||||||
|
const [settingState, setSettingState] = useState<SettingState>({
|
||||||
|
domain,
|
||||||
|
accessToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSettingState({
|
||||||
|
domain,
|
||||||
|
accessToken,
|
||||||
|
});
|
||||||
|
}, [domain, accessToken]);
|
||||||
|
|
||||||
|
const setPartialSettingState = (partialSettingState: Partial<SettingState>) => {
|
||||||
|
setSettingState((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
...partialSettingState,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveSetting = () => {
|
||||||
|
if (!settingState.domain || !settingState.accessToken) {
|
||||||
|
toast.error("Domain and access token are required");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDomain(settingState.domain);
|
||||||
|
setAccessToken(settingState.accessToken);
|
||||||
|
toast.success("Setting saved");
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
<h1>TBC</h1>
|
<div className="w-full">
|
||||||
</div>
|
<div className="w-full max-w-lg mx-auto flex flex-col justify-start items-start mt-12">
|
||||||
|
<h2 className="flex flex-row justify-start items-center mb-6 font-mono">
|
||||||
|
<Icon.CircleSlash className="w-8 h-auto mr-2 text-gray-500" />
|
||||||
|
<span className="text-lg">Slash</span>
|
||||||
|
<span className="mx-2 text-gray-400">/</span>
|
||||||
|
<span className="text-lg">Setting</span>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="w-full flex flex-col justify-start items-start mb-4">
|
||||||
|
<span className="mb-2 text-base">Domain</span>
|
||||||
|
<div className="relative w-full">
|
||||||
|
<Input
|
||||||
|
className="w-full"
|
||||||
|
type="text"
|
||||||
|
placeholder="The domain of your Slash instance"
|
||||||
|
value={settingState.domain}
|
||||||
|
onChange={(e) => setPartialSettingState({ domain: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full flex flex-col justify-start items-start">
|
||||||
|
<span className="mb-2 text-base">Access Token</span>
|
||||||
|
<div className="relative w-full">
|
||||||
|
<Input
|
||||||
|
className="w-full"
|
||||||
|
type="text"
|
||||||
|
placeholder="The access token of your Slash instance"
|
||||||
|
value={settingState.accessToken}
|
||||||
|
onChange={(e) => setPartialSettingState({ accessToken: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full mt-6">
|
||||||
|
<Button onClick={handleSaveSetting}>Save</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Toaster position="top-center" />
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,46 +1,51 @@
|
|||||||
import { Button } from "@mui/joy";
|
import { Button } from "@mui/joy";
|
||||||
import { useStorage } from "@plasmohq/storage/hook";
|
import { useStorage } from "@plasmohq/storage/hook";
|
||||||
import axios from "axios";
|
import { Toaster } from "react-hot-toast";
|
||||||
import { Toaster, toast } from "react-hot-toast";
|
|
||||||
import Setting from "@/components/Setting";
|
|
||||||
import { Shortcut } from "@/types/proto/api/v2/shortcut_service_pb";
|
import { Shortcut } from "@/types/proto/api/v2/shortcut_service_pb";
|
||||||
import Icon from "./components/Icon";
|
import Icon from "./components/Icon";
|
||||||
|
import PullShortcutsButton from "./components/PullShortcutsButton";
|
||||||
|
import ShortcutsContainer from "./components/ShortcutsContainer";
|
||||||
import "./style.css";
|
import "./style.css";
|
||||||
|
|
||||||
function IndexPopup() {
|
function IndexPopup() {
|
||||||
const [domain] = useStorage("domain");
|
const [domain] = useStorage<string>("domain", "");
|
||||||
const [accessToken] = useStorage("access_token");
|
const [accessToken] = useStorage<string>("access_token", "");
|
||||||
const [shortcuts, setShortcuts] = useStorage("shortcuts");
|
const [shortcuts] = useStorage<Shortcut[]>("shortcuts", []);
|
||||||
|
const isInitialized = domain && accessToken;
|
||||||
|
|
||||||
const handlePullShortcuts = async () => {
|
const handleSettingButtonClick = () => {
|
||||||
try {
|
chrome.runtime.openOptionsPage();
|
||||||
const { data } = await axios.get<Shortcut[]>(`${domain}/api/v1/shortcut`, {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${accessToken}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
setShortcuts(data);
|
|
||||||
toast.success("Shortcuts pulled");
|
|
||||||
} catch (error) {
|
|
||||||
toast.error("Failed to pull shortcuts, error: " + error.message);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="w-full min-w-[16rem] p-4">
|
<div className="w-full min-w-[480px] p-4">
|
||||||
<Setting />
|
<div className="w-full flex flex-row justify-between items-center text-sm">
|
||||||
|
<div className="flex flex-row justify-start items-center font-mono">
|
||||||
|
<Icon.CircleSlash className="w-5 h-auto mr-1 text-gray-500 -mt-0.5" />
|
||||||
|
<span className="font-mono">Slash</span>
|
||||||
|
{isInitialized && (
|
||||||
|
<>
|
||||||
|
<span className="mx-1 text-gray-400">/</span>
|
||||||
|
<span>Shortcuts</span>
|
||||||
|
<span className="mr-1 text-gray-500">({shortcuts.length})</span>
|
||||||
|
<PullShortcutsButton />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button size="sm" variant="plain" color="neutral" onClick={handleSettingButtonClick}>
|
||||||
|
<Icon.Settings className="w-5 h-auto" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="w-full mt-4">
|
<div className="w-full mt-4">
|
||||||
<Button className="w-full" onClick={handlePullShortcuts}>
|
<ShortcutsContainer />
|
||||||
<Icon.RefreshCcw className="w-5 h-auto mr-1" />
|
|
||||||
<span>Pull</span>
|
|
||||||
<span className="opacity-70 ml-1">{Array.isArray(shortcuts) ? `(${shortcuts.length})` : ""}</span>
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Toaster position="top-center" />
|
<Toaster position="top-right" />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
41
extension/src/stores/favicon.ts
Normal file
41
extension/src/stores/favicon.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { create } from "zustand";
|
||||||
|
import { persist } from "zustand/middleware";
|
||||||
|
import { getUrlFavicon } from "../helpers/api";
|
||||||
|
|
||||||
|
interface FaviconState {
|
||||||
|
cache: {
|
||||||
|
[key: string]: string;
|
||||||
|
};
|
||||||
|
getOrFetchUrlFavicon: (url: string) => Promise<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useFaviconStore = create<FaviconState>()(
|
||||||
|
persist(
|
||||||
|
(set, get) => ({
|
||||||
|
cache: {},
|
||||||
|
getOrFetchUrlFavicon: async (url: string) => {
|
||||||
|
const cache = get().cache;
|
||||||
|
if (cache[url]) {
|
||||||
|
return cache[url];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data: favicon } = await getUrlFavicon(url);
|
||||||
|
if (favicon) {
|
||||||
|
cache[url] = favicon;
|
||||||
|
set(cache);
|
||||||
|
return favicon;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: "favicon_cache",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
export default useFaviconStore;
|
Loading…
x
Reference in New Issue
Block a user