feat: implement extension's popup and options

This commit is contained in:
Steven
2023-08-08 20:16:14 +08:00
parent b638d9cdf4
commit f886bd7eb8
13 changed files with 344 additions and 91 deletions

View File

@@ -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";
const storage = new Storage();
@@ -9,11 +9,7 @@ chrome.tabs.onUpdated.addListener(async (tabId, _, tab) => {
const matchResult = urlRegex.exec(tab.url);
const sname = Array.isArray(matchResult) ? matchResult[1] : null;
if (sname) {
const shortcuts = (await storage.getItem("shortcuts")) as Shortcut[] | null;
if (!Array.isArray(shortcuts)) {
return;
}
const shortcuts = (await storage.getItem<Shortcut[]>("shortcuts")) || [];
const shortcut = shortcuts.find((shortcut) => shortcut.name === sname);
if (!shortcut) {
return;

View File

@@ -4,13 +4,13 @@ import axios from "axios";
import { useState } from "react";
import { toast } from "react-hot-toast";
import { Shortcut } from "@/types/proto/api/v2/shortcut_service_pb";
import "../style.css";
import Icon from "./Icon";
import "./style.css";
function PullShortcutsButton() {
const [domain] = useStorage("domain");
const [accessToken] = useStorage("access_token");
const [shortcuts, setShortcuts] = useStorage("shortcuts");
const [, setShortcuts] = useStorage("shortcuts");
const [isPulling, setIsPulling] = useState(false);
const handlePullShortcuts = async () => {
@@ -30,13 +30,9 @@ function PullShortcutsButton() {
};
return (
<div className="w-full mt-4">
<Button loading={isPulling} className="w-full" onClick={handlePullShortcuts}>
<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>
<Button loading={isPulling} color="neutral" variant="plain" size="sm" onClick={handlePullShortcuts}>
<Icon.RefreshCcw className="w-4 h-auto" />
</Button>
);
}

View File

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

View 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;

View 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;

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

View File

@@ -0,0 +1,5 @@
import { isNull, isUndefined } from "lodash-es";
export const isNullorUndefined = (value: any) => {
return isNull(value) || isUndefined(value);
};

View File

@@ -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";
interface SettingState {
domain: string;
accessToken: string;
}
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 (
<div>
<h1>TBC</h1>
</div>
<>
<div className="w-full">
<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" />
</>
);
}

View File

@@ -1,46 +1,51 @@
import { Button } from "@mui/joy";
import { useStorage } from "@plasmohq/storage/hook";
import axios from "axios";
import { Toaster, toast } from "react-hot-toast";
import Setting from "@/components/Setting";
import { Toaster } from "react-hot-toast";
import { Shortcut } from "@/types/proto/api/v2/shortcut_service_pb";
import Icon from "./components/Icon";
import PullShortcutsButton from "./components/PullShortcutsButton";
import ShortcutsContainer from "./components/ShortcutsContainer";
import "./style.css";
function IndexPopup() {
const [domain] = useStorage("domain");
const [accessToken] = useStorage("access_token");
const [shortcuts, setShortcuts] = useStorage("shortcuts");
const [domain] = useStorage<string>("domain", "");
const [accessToken] = useStorage<string>("access_token", "");
const [shortcuts] = useStorage<Shortcut[]>("shortcuts", []);
const isInitialized = domain && accessToken;
const handlePullShortcuts = async () => {
try {
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);
}
const handleSettingButtonClick = () => {
chrome.runtime.openOptionsPage();
};
return (
<>
<div className="w-full min-w-[16rem] p-4">
<Setting />
<div className="w-full min-w-[480px] p-4">
<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">
<Button className="w-full" onClick={handlePullShortcuts}>
<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>
<ShortcutsContainer />
</div>
</div>
<Toaster position="top-center" />
<Toaster position="top-right" />
</>
);
}

View 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;