mirror of
https://github.com/avinal/sciezka.git
synced 2026-07-04 07:40:09 +05:30
feat: project infrastructure and MV3 extension setup
TypeScript + esbuild build system, Manifest V3 with background script, content script for iframe overlay injection, and typed message contracts between extension contexts. Assisted-by: Claude Code Signed-off-by: Avinal Kumar <avinal.xlvii@gmail.com>
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
import type { Message, SearchRequest, ActionRequest, SearchItem } from "./types";
|
||||
|
||||
async function getOpenTabs(): Promise<SearchItem[]> {
|
||||
const tabs = await chrome.tabs.query({});
|
||||
return tabs.map((tab) => ({
|
||||
id: `tab-${tab.id}`,
|
||||
title: tab.title ?? "",
|
||||
url: tab.url ?? "",
|
||||
type: "tabs" as const,
|
||||
favIconUrl: tab.favIconUrl,
|
||||
}));
|
||||
}
|
||||
|
||||
async function getHistory(query: string): Promise<SearchItem[]> {
|
||||
const results = await chrome.history.search({
|
||||
text: query,
|
||||
maxResults: 50,
|
||||
startTime: 0,
|
||||
});
|
||||
return results.map((item) => ({
|
||||
id: `history-${item.id}`,
|
||||
title: item.title ?? "",
|
||||
url: item.url ?? "",
|
||||
type: "history" as const,
|
||||
}));
|
||||
}
|
||||
|
||||
async function getBookmarks(query: string): Promise<SearchItem[]> {
|
||||
const results = await chrome.bookmarks.search(query || " ");
|
||||
return results
|
||||
.filter((b) => b.url)
|
||||
.map((item) => ({
|
||||
id: `bookmark-${item.id}`,
|
||||
title: item.title ?? "",
|
||||
url: item.url!,
|
||||
type: "bookmarks" as const,
|
||||
}));
|
||||
}
|
||||
|
||||
async function getRecentlyClosed(): Promise<SearchItem[]> {
|
||||
const sessions = await chrome.sessions.getRecentlyClosed({ maxResults: 25 });
|
||||
return sessions
|
||||
.filter((s) => s.tab)
|
||||
.map((session) => ({
|
||||
id: `closed-${session.tab!.sessionId}`,
|
||||
title: session.tab!.title ?? "",
|
||||
url: session.tab!.url ?? "",
|
||||
type: "closed" as const,
|
||||
favIconUrl: session.tab!.favIconUrl,
|
||||
}));
|
||||
}
|
||||
|
||||
async function handleSearch(request: SearchRequest): Promise<SearchItem[]> {
|
||||
const { query, mode } = request;
|
||||
switch (mode) {
|
||||
case "tabs":
|
||||
return getOpenTabs();
|
||||
case "history":
|
||||
return getHistory(query);
|
||||
case "bookmarks":
|
||||
return getBookmarks(query);
|
||||
case "closed":
|
||||
return getRecentlyClosed();
|
||||
case "all": {
|
||||
const [tabs, history, bookmarks, closed] = await Promise.all([
|
||||
getOpenTabs(),
|
||||
getHistory(query),
|
||||
getBookmarks(query),
|
||||
getRecentlyClosed(),
|
||||
]);
|
||||
return [...tabs, ...history, ...bookmarks, ...closed];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAction(request: ActionRequest): Promise<void> {
|
||||
const rawId = request.id.replace(/^(tab|history|bookmark|closed)-/, "");
|
||||
|
||||
switch (request.action) {
|
||||
case "switch": {
|
||||
const tabId = parseInt(rawId, 10);
|
||||
await chrome.tabs.update(tabId, { active: true });
|
||||
const tab = await chrome.tabs.get(tabId);
|
||||
if (tab.windowId != null) {
|
||||
await chrome.windows.update(tab.windowId, { focused: true });
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "open": {
|
||||
if (request.newTab) {
|
||||
await chrome.tabs.create({ url: request.id });
|
||||
} else {
|
||||
const [activeTab] = await chrome.tabs.query({
|
||||
active: true,
|
||||
currentWindow: true,
|
||||
});
|
||||
if (activeTab?.id != null) {
|
||||
await chrome.tabs.update(activeTab.id, { url: request.id });
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "close": {
|
||||
const tabId = parseInt(rawId, 10);
|
||||
await chrome.tabs.remove(tabId);
|
||||
break;
|
||||
}
|
||||
case "restore": {
|
||||
await chrome.sessions.restore(rawId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
chrome.commands.onCommand.addListener(async (command) => {
|
||||
if (command === "toggle-sciezka") {
|
||||
const [tab] = await chrome.tabs.query({
|
||||
active: true,
|
||||
currentWindow: true,
|
||||
});
|
||||
if (tab?.id != null) {
|
||||
chrome.tabs.sendMessage(tab.id, { type: "toggle" } satisfies Message);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
chrome.runtime.onMessage.addListener(
|
||||
(msg: unknown, _sender, sendResponse) => {
|
||||
const message = msg as Message;
|
||||
if (message.type === "search") {
|
||||
handleSearch(message).then((items) => {
|
||||
sendResponse({ type: "searchResults", results: items });
|
||||
});
|
||||
return true;
|
||||
}
|
||||
if (message.type === "action") {
|
||||
handleAction(message).then(() => sendResponse({ ok: true }));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
);
|
||||
Vendored
+78
@@ -0,0 +1,78 @@
|
||||
declare namespace chrome {
|
||||
namespace tabs {
|
||||
interface Tab {
|
||||
id?: number;
|
||||
windowId?: number;
|
||||
title?: string;
|
||||
url?: string;
|
||||
favIconUrl?: string;
|
||||
active?: boolean;
|
||||
}
|
||||
function query(queryInfo: Record<string, unknown>): Promise<Tab[]>;
|
||||
function get(tabId: number): Promise<Tab>;
|
||||
function create(createProperties: { url?: string; active?: boolean }): Promise<Tab>;
|
||||
function update(tabId: number, updateProperties: { active?: boolean; url?: string }): Promise<Tab>;
|
||||
function remove(tabId: number | number[]): Promise<void>;
|
||||
function sendMessage(tabId: number, message: unknown): void;
|
||||
}
|
||||
|
||||
namespace windows {
|
||||
function update(windowId: number, updateInfo: { focused?: boolean }): Promise<void>;
|
||||
}
|
||||
|
||||
namespace history {
|
||||
interface HistoryItem {
|
||||
id: string;
|
||||
title?: string;
|
||||
url?: string;
|
||||
lastVisitTime?: number;
|
||||
}
|
||||
function search(query: { text: string; maxResults?: number; startTime?: number }): Promise<HistoryItem[]>;
|
||||
}
|
||||
|
||||
namespace bookmarks {
|
||||
interface BookmarkTreeNode {
|
||||
id: string;
|
||||
title: string;
|
||||
url?: string;
|
||||
children?: BookmarkTreeNode[];
|
||||
}
|
||||
function search(query: string): Promise<BookmarkTreeNode[]>;
|
||||
}
|
||||
|
||||
namespace sessions {
|
||||
interface Session {
|
||||
tab?: tabs.Tab & { sessionId?: string };
|
||||
window?: { sessionId?: string };
|
||||
}
|
||||
function getRecentlyClosed(filter?: { maxResults?: number }): Promise<Session[]>;
|
||||
function restore(sessionId: string): Promise<Session>;
|
||||
}
|
||||
|
||||
namespace commands {
|
||||
const onCommand: {
|
||||
addListener(callback: (command: string) => void): void;
|
||||
};
|
||||
}
|
||||
|
||||
namespace runtime {
|
||||
function getURL(path: string): string;
|
||||
const onMessage: {
|
||||
addListener(
|
||||
callback: (
|
||||
message: unknown,
|
||||
sender: { tab?: tabs.Tab; id?: string },
|
||||
sendResponse: (response?: unknown) => void
|
||||
) => boolean | void
|
||||
): void;
|
||||
};
|
||||
function sendMessage(message: unknown, responseCallback?: (response: unknown) => void): void;
|
||||
}
|
||||
|
||||
namespace storage {
|
||||
const sync: {
|
||||
get(keys: string | string[]): Promise<Record<string, unknown>>;
|
||||
set(items: Record<string, unknown>): Promise<void>;
|
||||
};
|
||||
}
|
||||
}
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
import type { Message } from "./types";
|
||||
|
||||
let overlay: HTMLDivElement | null = null;
|
||||
let iframe: HTMLIFrameElement | null = null;
|
||||
|
||||
function createOverlay(): void {
|
||||
overlay = document.createElement("div");
|
||||
overlay.id = "sciezka-overlay";
|
||||
overlay.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: rgba(0, 0, 0, 0);
|
||||
z-index: 2147483647;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
padding-top: 12vh;
|
||||
transition: background 0.15s ease;
|
||||
`;
|
||||
|
||||
iframe = document.createElement("iframe");
|
||||
iframe.src = chrome.runtime.getURL("sciezka.html");
|
||||
iframe.style.cssText = `
|
||||
width: 620px;
|
||||
height: 60px;
|
||||
border: none;
|
||||
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.3);
|
||||
background: transparent;
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
transition: opacity 0.15s ease, transform 0.15s ease, height 0.1s ease;
|
||||
`;
|
||||
|
||||
overlay.appendChild(iframe);
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
overlay!.style.background = "rgba(0, 0, 0, 0.4)";
|
||||
iframe!.style.opacity = "1";
|
||||
iframe!.style.transform = "translateY(0)";
|
||||
});
|
||||
|
||||
overlay.addEventListener("click", (e) => {
|
||||
if (e.target === overlay) {
|
||||
removeOverlay();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function removeOverlay(): void {
|
||||
if (overlay) {
|
||||
overlay.remove();
|
||||
overlay = null;
|
||||
iframe = null;
|
||||
}
|
||||
}
|
||||
|
||||
function toggle(): void {
|
||||
if (overlay) {
|
||||
removeOverlay();
|
||||
} else {
|
||||
createOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
chrome.runtime.onMessage.addListener((msg: unknown) => {
|
||||
const message = msg as Message;
|
||||
if (message.type === "toggle") {
|
||||
toggle();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape" && overlay) {
|
||||
removeOverlay();
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener("message", (event) => {
|
||||
if (event.source !== iframe?.contentWindow) return;
|
||||
|
||||
const data = event.data as Message;
|
||||
if (data.type === "closeSaka") {
|
||||
removeOverlay();
|
||||
return;
|
||||
}
|
||||
if (data.type === "resize") {
|
||||
if (iframe) {
|
||||
iframe.style.height = `${(data as { height: number }).height}px`;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (data.type === "search" || data.type === "action") {
|
||||
chrome.runtime.sendMessage(data, (response) => {
|
||||
iframe?.contentWindow?.postMessage(response, "*");
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
export type SearchMode = "tabs" | "history" | "bookmarks" | "closed" | "all";
|
||||
export type SearchMethod = "fuzzy" | "fulltext" | "prefix";
|
||||
|
||||
export interface SearchItem {
|
||||
id: string;
|
||||
title: string;
|
||||
url: string;
|
||||
type: SearchMode;
|
||||
favIconUrl?: string;
|
||||
}
|
||||
|
||||
export interface SearchResult {
|
||||
item: SearchItem;
|
||||
score: number;
|
||||
positions: number[];
|
||||
}
|
||||
|
||||
export interface SearchRequest {
|
||||
type: "search";
|
||||
query: string;
|
||||
mode: SearchMode;
|
||||
method: SearchMethod;
|
||||
}
|
||||
|
||||
export interface SearchResponse {
|
||||
type: "searchResults";
|
||||
results: SearchResult[];
|
||||
}
|
||||
|
||||
export interface ActionRequest {
|
||||
type: "action";
|
||||
action: "switch" | "open" | "close" | "restore";
|
||||
id: string;
|
||||
newTab?: boolean;
|
||||
}
|
||||
|
||||
export interface ToggleMessage {
|
||||
type: "toggle";
|
||||
}
|
||||
|
||||
export interface CloseMessage {
|
||||
type: "closeSaka";
|
||||
}
|
||||
|
||||
export interface ResizeMessage {
|
||||
type: "resize";
|
||||
height: number;
|
||||
}
|
||||
|
||||
export type Message =
|
||||
| SearchRequest
|
||||
| SearchResponse
|
||||
| ActionRequest
|
||||
| ToggleMessage
|
||||
| CloseMessage
|
||||
| ResizeMessage;
|
||||
Reference in New Issue
Block a user