mirror of
https://github.com/nkcmr/HyperTab.git
synced 2026-04-29 12:17:40 +02:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6dc2074870 | ||
|
|
0ce204a948 | ||
|
|
4f2db92534 | ||
|
|
3463d25fd0 |
@@ -147,41 +147,11 @@ const HighlightMatches: FunctionComponent<{
|
||||
);
|
||||
};
|
||||
|
||||
// // <big_sigh> ...
|
||||
// function faviconsWork(tabURL: string, size: number): Promise<boolean> {
|
||||
// return new Promise((resolve) => {
|
||||
// const hiddenDiv = document.createElement("div", {});
|
||||
// hiddenDiv.setAttribute("style", "display:none;");
|
||||
// const testImg = document.createElement("img");
|
||||
// testImg.src = faviconURL({ url: tabURL } as chrome.tabs.Tab, 32)!;
|
||||
// testImg.onerror = () => {
|
||||
// document.body.removeChild(hiddenDiv);
|
||||
// resolve(false);
|
||||
// };
|
||||
// testImg.onload = () => {
|
||||
// document.body.removeChild(hiddenDiv);
|
||||
// resolve(true);
|
||||
// };
|
||||
// hiddenDiv.appendChild(testImg);
|
||||
// document.body.appendChild(hiddenDiv);
|
||||
// });
|
||||
// }
|
||||
|
||||
function faviconURL(t: chrome.tabs.Tab, size: number): string | undefined {
|
||||
function faviconURL(t: chrome.tabs.Tab): string | undefined {
|
||||
return (
|
||||
t.favIconUrl ??
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="
|
||||
);
|
||||
// if (t.favIconUrl) {
|
||||
// return t.favIconUrl;
|
||||
// }
|
||||
// if (!t.url) {
|
||||
// return;
|
||||
// }
|
||||
// const url = new URL(browser.runtime.getURL("/_favicon/"));
|
||||
// url.searchParams.set("pageUrl", t.url);
|
||||
// url.searchParams.set("size", `${size}`);
|
||||
// return url.toString();
|
||||
}
|
||||
|
||||
const prefersDarkMode = (): boolean => {
|
||||
@@ -249,11 +219,11 @@ const TabItem = styled.div<{ $selected: boolean; $dark: boolean }>`
|
||||
gap: ${TAB_ITEM_FLEX_GAP}px;
|
||||
align-items: center;
|
||||
&:hover {
|
||||
background-color: #efefef;
|
||||
background-color: ${(props) => (props.$dark ? "#4e4e4e" : "#efefef")};
|
||||
}
|
||||
`;
|
||||
const TabItemFavicon = styled.div`
|
||||
background-color: #d9d9d9;
|
||||
const TabItemFavicon = styled.div<{ $dark: boolean }>`
|
||||
background-color: ${(props) => (props.$dark ? "#6e6e6e" : "#d9d9d9")};
|
||||
border-radius: 5px;
|
||||
padding: 0.5em;
|
||||
width: ${TAB_ITEM_FAVICON_SIZE}px;
|
||||
@@ -261,6 +231,9 @@ const TabItemFavicon = styled.div`
|
||||
padding: 7px;
|
||||
flex-shrink: 0;
|
||||
`;
|
||||
const TabItemCloseBox = styled.div`
|
||||
cursor: pointer;
|
||||
`;
|
||||
const TabItemMain = styled.div`
|
||||
width: ${TAB_ITEM_MAIN_WIDTH}px;
|
||||
`;
|
||||
@@ -271,8 +244,8 @@ const TabItemMainTitle = styled.div<{ $dark: boolean }>`
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
`;
|
||||
const TabItemMainHostname = styled.div`
|
||||
color: #6e6e6e;
|
||||
const TabItemMainHostname = styled.div<{ $dark: boolean }>`
|
||||
color: ${(props) => (props.$dark ? "#a9a9a9" : "#6e6e6e")};
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -298,24 +271,79 @@ const SearchInput = styled.input`
|
||||
font-size: 1.2em;
|
||||
`;
|
||||
|
||||
type searchField = {
|
||||
aliases?: string[];
|
||||
filterValues: (value: string | null) => boolean;
|
||||
evaluate: (t: chrome.tabs.Tab, value: string | null) => boolean;
|
||||
};
|
||||
|
||||
const searchFields: Record<string, searchField> = {
|
||||
sys: {
|
||||
filterValues(value) {
|
||||
return ["true", "false"].includes(value?.toLowerCase() ?? "");
|
||||
},
|
||||
evaluate(t, value) {
|
||||
if (!t.url) {
|
||||
return value === "true";
|
||||
}
|
||||
const ishttp = !!t.url.match(/^https?:\/\//i);
|
||||
return ishttp !== (value === "true");
|
||||
},
|
||||
},
|
||||
pinned: {
|
||||
filterValues(value) {
|
||||
return ["true", "false"].includes(value?.toLowerCase() ?? "");
|
||||
},
|
||||
evaluate(t, value) {
|
||||
return t.pinned === (value === "true");
|
||||
},
|
||||
},
|
||||
hostname: {
|
||||
aliases: ["domain"],
|
||||
filterValues(value) {
|
||||
return true;
|
||||
},
|
||||
evaluate(t, value) {
|
||||
if (!t.url) {
|
||||
return false;
|
||||
}
|
||||
return hostname(t.url).includes(value ?? "");
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
function getSearchField(key: string): searchField | null {
|
||||
return key in searchFields
|
||||
? searchFields[key]
|
||||
: Object.values(searchFields).find((sf) => {
|
||||
return sf.aliases?.includes(key) ?? false;
|
||||
}) ?? null;
|
||||
}
|
||||
|
||||
function structuredQuery(query: string): Map<string, string | null> {
|
||||
return new Map<string, string | null>(
|
||||
query
|
||||
.split(/\s+/g)
|
||||
.map((pair): [string, string | null] => {
|
||||
const [key, value] = pair.split(":", 2);
|
||||
return [key, value ?? null];
|
||||
})
|
||||
.filter(([key, value]) => {
|
||||
const sf = getSearchField(key);
|
||||
if (!sf) {
|
||||
return false;
|
||||
}
|
||||
return sf.filterValues(value);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const Popup: FunctionComponent = () => {
|
||||
const darkMode = useDarkMode();
|
||||
const [tabSelector, setTabSelector] = useState(0);
|
||||
const [tabs, setTabs] = useState<chrome.tabs.Tab[]>([]);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
|
||||
// const FAVICON_NOT_SUPPORTED = 0;
|
||||
// const FAVICON_SUPPORTED_VIA_EXT_URL = 1;
|
||||
// const FAVICON_SUPPORTED_VIA_TAB_DATA = 2;
|
||||
// const [enableFavicons, setEnabledFavicons] = useState(FAVICON_NOT_SUPPORTED);
|
||||
// useEffect(() => {
|
||||
// faviconsWork("https://www.google.com", 32).then((ok) => {
|
||||
// if (enableFavicons === 0) {
|
||||
// setEnabledFavicons(FAVICON_SUPPORTED_VIA_EXT_URL);
|
||||
// }
|
||||
// });
|
||||
// }, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (tabs.length === 0) {
|
||||
return;
|
||||
@@ -327,7 +355,15 @@ const Popup: FunctionComponent = () => {
|
||||
}, [setTabSelector, searchQuery]);
|
||||
const searchIndex = useMemo(() => {
|
||||
const result = new Fuse(tabs, {
|
||||
keys: ["title", "url"],
|
||||
keys: [
|
||||
"title",
|
||||
{
|
||||
name: "hostname",
|
||||
getFn(obj) {
|
||||
return obj.url ? hostname(obj.url) : "";
|
||||
},
|
||||
},
|
||||
],
|
||||
includeMatches: true,
|
||||
});
|
||||
return result;
|
||||
@@ -341,6 +377,24 @@ const Popup: FunctionComponent = () => {
|
||||
})
|
||||
);
|
||||
}
|
||||
const sq = structuredQuery(searchQuery);
|
||||
if (sq.size > 0) {
|
||||
return tabs
|
||||
.filter((tab) => {
|
||||
for (let [key, value] of sq.entries()) {
|
||||
if (!getSearchField(key)!.evaluate(tab, value)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map(
|
||||
(t, i): FuseResult<chrome.tabs.Tab> => ({
|
||||
item: t,
|
||||
refIndex: i,
|
||||
})
|
||||
);
|
||||
}
|
||||
return searchIndex.search(searchQuery);
|
||||
}, [tabs, searchIndex, searchQuery]);
|
||||
|
||||
@@ -409,7 +463,14 @@ const Popup: FunctionComponent = () => {
|
||||
}, [selectedTabEle.current]);
|
||||
|
||||
const [tabHover, setTabHover] = useState<number | null>(null);
|
||||
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
useEffect(() => {
|
||||
requestIdleCallback(() => {
|
||||
// this is more reliable than autoFocus attribute. sometimes autoFocus
|
||||
// would let the popup open and then input would not be focused.
|
||||
inputRef.current?.focus();
|
||||
});
|
||||
}, [inputRef.current]);
|
||||
return (
|
||||
<div>
|
||||
<SearchContainer>
|
||||
@@ -428,8 +489,8 @@ const Popup: FunctionComponent = () => {
|
||||
</SearchIconLeftContainer>
|
||||
<SearchInputRightContainer>
|
||||
<SearchInput
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
autoFocus
|
||||
placeholder={t("ui_search_tabs")}
|
||||
value={searchQuery}
|
||||
onKeyDown={(e) => {
|
||||
@@ -458,7 +519,7 @@ const Popup: FunctionComponent = () => {
|
||||
<TabListEmpty>No Results Found</TabListEmpty>
|
||||
) : null}
|
||||
{searchResults.map((t, i) => {
|
||||
const favicURL = faviconURL(t.item, 32);
|
||||
const favicURL = faviconURL(t.item);
|
||||
const showCloseAction = tabHover === t.item.id! && !t.item.pinned;
|
||||
return (
|
||||
<TabItem
|
||||
@@ -471,6 +532,7 @@ const Popup: FunctionComponent = () => {
|
||||
ref={i === tabSelector ? selectedTabEle : undefined}
|
||||
>
|
||||
<TabItemFavicon
|
||||
$dark={darkMode}
|
||||
onMouseEnter={() => {
|
||||
setTabHover(t.item.id!);
|
||||
}}
|
||||
@@ -479,7 +541,7 @@ const Popup: FunctionComponent = () => {
|
||||
}}
|
||||
>
|
||||
{showCloseAction ? (
|
||||
<div
|
||||
<TabItemCloseBox
|
||||
title="Close Tab"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
@@ -499,7 +561,7 @@ const Popup: FunctionComponent = () => {
|
||||
>
|
||||
<path d="M12 0c-6.627 0-12 5.373-12 12s5.373 12 12 12 12-5.373 12-12-5.373-12-12-12zm4.151 17.943l-4.143-4.102-4.117 4.159-1.833-1.833 4.104-4.157-4.162-4.119 1.833-1.833 4.155 4.102 4.106-4.16 1.849 1.849-4.1 4.141 4.157 4.104-1.849 1.849z" />
|
||||
</svg>
|
||||
</div>
|
||||
</TabItemCloseBox>
|
||||
) : (
|
||||
<img width={16} height={16} src={favicURL} />
|
||||
)}
|
||||
@@ -511,7 +573,7 @@ const Popup: FunctionComponent = () => {
|
||||
match={t.matches?.find((m) => m.key === "title")}
|
||||
/>
|
||||
</TabItemMainTitle>
|
||||
<TabItemMainHostname>
|
||||
<TabItemMainHostname $dark={darkMode}>
|
||||
{t.item.url ? hostname(t.item.url) : ""}
|
||||
</TabItemMainHostname>
|
||||
</TabItemMain>
|
||||
|
||||
Reference in New Issue
Block a user