import { createIcons, CirclePlus, FolderGit2, GitBranch, Link, List, RefreshCw } from "lucide"; import "./styles.css"; import { appStore, CHROME_PROFILE_NAME, findKnownUrlForBase, type AddUrlMode, type AppView, type ScreenshotAttachment } from "./store"; import type { GiteaIssueSummary, GiteaRepoSummary } from "./api"; import type { KnownUrl, LocalContext, LocalWorkspace } from "./localContext"; const status = document.querySelector("#status"); const refreshData = document.querySelector("#refresh-data"); const addUrlButton = document.querySelector("#add-url"); const addIssueButton = document.querySelector("#add-issue"); const viewHeader = document.querySelector("#view-header"); const viewContent = document.querySelector("#view-content"); const viewButtons = Array.from(document.querySelectorAll("[data-view]")); createIcons({ icons: { CirclePlus, FolderGit2, GitBranch, Link, List, RefreshCw } }); function escapeHtml(value: string): string { return value.replace(/[&<>"']/g, (char) => { const replacements: Record = { "&": "&", "<": "<", ">": ">", "\"": """, "'": "'" }; return replacements[char]; }); } function formatDate(value: string, emptyText = "No date"): string { if (!value) { return emptyText; } return new Intl.DateTimeFormat(undefined, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" }).format(new Date(value)); } function formatSha(value: string): string { return value ? value.slice(0, 8) : "unknown"; } function allUrls(): KnownUrl[] { const state = appStore.getState(); const deletedBases = new Set(state.deletedUrlBases); const urls = [...(state.localContext?.knownUrls ?? []), ...state.userUrls].filter( (knownUrl) => !deletedBases.has(baseUrlFor(knownUrl.url)) ); return urls.filter( (knownUrl, index) => urls.findIndex((item) => baseUrlFor(item.url) === baseUrlFor(knownUrl.url)) === index ); } function baseUrlFor(value: string): string { try { const url = new URL(value); if (url.protocol !== "http:" && url.protocol !== "https:") { return ""; } return `${url.origin}/`; } catch { return ""; } } function isActiveBaseUrlKnown(): boolean { return Boolean(findKnownUrlForBase(appStore.getState())); } function getActiveKnownUrl(): KnownUrl | null { return findKnownUrlForBase(appStore.getState()); } function canAddIssue(): boolean { return Boolean(getActiveKnownUrl()?.giteaRepo); } function getRepoWorkspaceLink(repo: GiteaRepoSummary): string { const context = appStore.getState().localContext; const worktree = context?.gitWorktrees.find((item) => item.giteaRepo?.fullName === repo.fullName); if (!worktree) { return `No workspace link`; } return `./${escapeHtml(worktree.relativePath)}`; } function renderRepos(repos: GiteaRepoSummary[]): string { if (!repos.length) { return `
No Gitea repos loaded yet.
`; } return `
${repos .map( (repo) => `
${escapeHtml(repo.fullName)} ${escapeHtml(repo.lastCommitMessage)}
${formatDate(repo.lastCommitAt, "No commit date")} ${formatSha(repo.lastCommitSha)} ${getRepoWorkspaceLink(repo)}
` ) .join("")}
`; } function renderWorkspaces(workspaces: LocalWorkspace[]): string { if (!workspaces.length) { return `
No workspaces found in workspace_Father.
`; } return `
${workspaces .map((workspace) => { const links = workspace.linkedRepos.length ? workspace.linkedRepos .map( (repo) => `${escapeHtml(repo.fullName)}` ) .join("") : `No Gitea repo link established`; return `

${escapeHtml(workspace.name)}

${escapeHtml(workspace.path)}

`; }) .join("")}
`; } function renderUrls(urls: KnownUrl[]): string { if (!urls.length) { return `
No known URLs registered.
`; } return `
${urls .map( (knownUrl) => ` ` ) .join("")}
`; } function selectedAttribute(value: string, selectedValue: string): string { return value === selectedValue ? " selected" : ""; } function workspaceOptions(context: LocalContext, selectedValue: string): string { return context.workspaces .map( (workspace) => `` ) .join(""); } function repoOptions(context: LocalContext, selectedValue: string): string { const repos = context.gitWorktrees .map((worktree) => worktree.giteaRepo) .filter((repo): repo is NonNullable => Boolean(repo)) .filter((repo, index, repos) => repos.findIndex((item) => item.fullName === repo.fullName) === index) .sort((a, b) => a.fullName.localeCompare(b.fullName)); return repos .map( (repo) => `` ) .join(""); } function renderAddUrl(): string { const state = appStore.getState(); const context = state.localContext; if (!state.activeBaseUrl) { return `
Open an http or https tab before adding a URL link.
`; } if (isActiveBaseUrlKnown()) { return `
${escapeHtml(state.activeBaseUrl)} is already linked.
`; } if (!context) { return `
Workspace context has not loaded yet.
`; } const mode = state.addUrlDraft.mode; const selectedValue = state.addUrlDraft.selectedValue; return `
Link by
`; } function renderAttachmentList(items: string[], emptyText: string): string { if (!items.length) { return `${escapeHtml(emptyText)}`; } return `
    ${items.map((item) => `
  • ${escapeHtml(item)}
  • `).join("")}
`; } function renderScreenshotList(screenshots: ScreenshotAttachment[], deleteAction: "issue" | "comment"): string { if (!screenshots.length) { return `No screenshots captured.`; } const attribute = deleteAction === "issue" ? "data-delete-screenshot" : "data-delete-comment-screenshot"; return `
${screenshots .map( (screenshot) => `
${escapeHtml(screenshot.name)}
${escapeHtml(screenshot.name)}
` ) .join("")}
`; } function renderIssueRows(issues: GiteaIssueSummary[], selectedIssueNumber: number | null): string { const state = appStore.getState(); if (!issues.length) { return `
No issues found for this repo.
`; } return `
${issues .map( (issue) => `
` ) .join("")}
`; } function renderIssueCommentForm(issue: GiteaIssueSummary): string { const state = appStore.getState(); const draft = state.issueCommentDraft; const canSubmit = Boolean(draft.content.trim() || draft.screenshots.length || draft.files.length) && !state.isCreatingIssueComment; return `
#${issue.number} ${escapeHtml(issue.title)} ${escapeHtml(issue.state)}
Screenshots
${renderScreenshotList(draft.screenshots, "comment")}
${renderAttachmentList( draft.files.map((file) => file.name), "No files selected." )}
`; } function renderTargetIssues(): string { const state = appStore.getState(); const selectedIssue = state.targetIssues.find((issue) => issue.number === state.selectedIssueNumber) ?? null; return `
Issues
${state.isLoadingTargetIssues && !state.targetIssues.length ? `
Loading issues...
` : renderIssueRows(state.targetIssues, state.selectedIssueNumber)} ${selectedIssue ? renderIssueCommentForm(selectedIssue) : ""}
`; } function renderAddIssue(): string { const state = appStore.getState(); const knownUrl = getActiveKnownUrl(); const draft = state.issueDraft; if (!knownUrl) { return `
Open a known URL before creating an issue.
`; } if (!knownUrl.giteaRepo) { return `
${escapeHtml(knownUrl.label)} is linked to a workspace but not a Gitea repo.
`; } return `
Target repo ${escapeHtml(knownUrl.giteaRepo.fullName)} ${escapeHtml(knownUrl.workspacePath)} ${escapeHtml(knownUrl.url)} Chrome profile: ${escapeHtml(CHROME_PROFILE_NAME)}
Screenshots
${renderScreenshotList(draft.screenshots, "issue")}
${renderAttachmentList( draft.files.map((file) => file.name), "No files selected." )}
${renderTargetIssues()} `; } function openUrl(url: string): void { if (globalThis.chrome?.tabs?.create) { void globalThis.chrome.tabs.create({ url }); return; } window.open(url, "_blank", "noopener,noreferrer"); } function bindViewActions(): void { viewContent?.querySelectorAll("[data-open-url]").forEach((button) => { button.addEventListener("click", () => { const url = button.dataset.openUrl; if (url) { openUrl(url); } }); }); viewContent?.querySelectorAll("[data-delete-url]").forEach((button) => { button.addEventListener("click", () => { const url = button.dataset.deleteUrl; if (url) { void appStore.getState().deleteUrlLink(url); } }); }); viewContent?.querySelectorAll("input[name='add-url-mode']").forEach((input) => { input.addEventListener("change", () => { appStore.getState().setAddUrlDraft({ mode: input.value as AddUrlMode, selectedValue: "" }); }); }); viewContent?.querySelector("#add-url-selection")?.addEventListener("change", (event) => { appStore.getState().setAddUrlDraft({ selectedValue: (event.target as HTMLSelectElement).value }); }); viewContent?.querySelector("#save-url-link")?.addEventListener("click", () => { void appStore.getState().saveCurrentUrlLink(); }); viewContent?.querySelector("#issue-subject")?.addEventListener("input", (event) => { appStore.getState().setIssueDraft({ subject: (event.target as HTMLInputElement).value }); }); viewContent?.querySelector("#issue-content")?.addEventListener("input", (event) => { appStore.getState().setIssueDraft({ content: (event.target as HTMLTextAreaElement).value }); }); viewContent?.querySelector("#issue-ready")?.addEventListener("change", (event) => { appStore.getState().setIssueDraft({ ready: (event.target as HTMLInputElement).checked }); }); viewContent?.querySelector("#issue-files")?.addEventListener("change", (event) => { appStore.getState().setIssueFiles(Array.from((event.target as HTMLInputElement).files ?? [])); }); viewContent?.querySelector("#capture-screenshot")?.addEventListener("click", () => { void appStore.getState().captureScreenshot(); }); viewContent?.querySelectorAll("[data-delete-screenshot]").forEach((button) => { button.addEventListener("click", () => { const id = button.dataset.deleteScreenshot; if (id) { appStore.getState().deleteIssueScreenshot(id); } }); }); viewContent?.querySelectorAll("[data-select-issue]").forEach((button) => { button.addEventListener("click", () => { const issueNumber = Number(button.dataset.selectIssue); if (Number.isFinite(issueNumber)) { appStore.getState().selectIssue(issueNumber); } }); }); viewContent?.querySelectorAll("[data-ready-issue]").forEach((button) => { button.addEventListener("click", () => { const issueNumber = Number(button.dataset.readyIssue); if (Number.isFinite(issueNumber)) { void appStore.getState().markIssueReady(issueNumber); } }); }); viewContent?.querySelectorAll("[data-delete-issue]").forEach((button) => { button.addEventListener("click", () => { const issueNumber = Number(button.dataset.deleteIssue); if (Number.isFinite(issueNumber) && window.confirm(`Delete issue #${issueNumber}?`)) { void appStore.getState().deleteIssue(issueNumber); } }); }); viewContent?.querySelector("#refresh-target-issues")?.addEventListener("click", () => { void appStore.getState().loadTargetIssues(true); }); viewContent?.querySelector("#issue-comment-content")?.addEventListener("input", (event) => { appStore.getState().setIssueCommentDraft({ content: (event.target as HTMLTextAreaElement).value }); }); viewContent?.querySelector("#issue-comment-files")?.addEventListener("change", (event) => { appStore.getState().setIssueCommentFiles(Array.from((event.target as HTMLInputElement).files ?? [])); }); viewContent?.querySelector("#capture-comment-screenshot")?.addEventListener("click", () => { void appStore.getState().captureIssueCommentScreenshot(); }); viewContent?.querySelectorAll("[data-delete-comment-screenshot]").forEach((button) => { button.addEventListener("click", () => { const id = button.dataset.deleteCommentScreenshot; if (id) { appStore.getState().deleteIssueCommentScreenshot(id); } }); }); viewContent?.querySelector("#submit-issue-comment")?.addEventListener("click", () => { void appStore.getState().createIssueComment(); }); viewContent?.querySelector("#create-issue")?.addEventListener("click", () => { void appStore.getState().createIssue(); }); } function renderView(): void { if (!viewHeader || !viewContent) { return; } const state = appStore.getState(); const context = state.localContext; const titles: Record = { "add-issue": "Add issue", "add-url": "Add URL", urls: "URLs", repos: "Gitea repos", workspaces: "Workspaces" }; const descriptions: Record = { "add-issue": "Create a Gitea issue for the repo linked to the active tab.", "add-url": "Link the active tab base URL to a workspace or Gitea repo.", urls: "Known browser targets and their workspace/repo links.", repos: "Repositories ordered by latest default-branch commit.", workspaces: `Authoritative workspaces from ${context?.workspaceRoot || "workspace_Father"}.` }; viewHeader.hidden = state.activeView === "add-issue"; viewHeader.innerHTML = state.activeView === "add-issue" ? "" : `

${titles[state.activeView]}

${escapeHtml(descriptions[state.activeView])}

`; if (state.activeView === "repos") { viewContent.innerHTML = renderRepos(state.repos); } else if (state.activeView === "workspaces") { viewContent.innerHTML = renderWorkspaces(context?.workspaces ?? []); } else if (state.activeView === "add-issue") { viewContent.innerHTML = renderAddIssue(); } else if (state.activeView === "add-url") { viewContent.innerHTML = renderAddUrl(); } else { viewContent.innerHTML = renderUrls(allUrls()); } bindViewActions(); } function renderChrome(): void { const state = appStore.getState(); if (status) { status.textContent = state.statusMessage; status.dataset.tone = state.statusTone; } if (refreshData) { refreshData.disabled = state.isLoadingData; } if (addUrlButton) { addUrlButton.disabled = !state.activeBaseUrl || isActiveBaseUrlKnown(); } if (addIssueButton) { addIssueButton.disabled = !canAddIssue(); } viewButtons.forEach((button) => { const view = button.dataset.view; button.dataset.active = view === state.activeView ? "true" : "false"; }); } function render(): void { renderChrome(); renderView(); ensureTargetIssuesLoaded(); } function ensureTargetIssuesLoaded(): void { const state = appStore.getState(); const knownUrl = getActiveKnownUrl(); const fullName = knownUrl?.giteaRepo?.fullName ?? ""; if ( state.activeView === "add-issue" && fullName && state.issueListRepoFullName !== fullName && !state.isLoadingTargetIssues ) { void appStore.getState().loadTargetIssues(); } } viewButtons.forEach((button) => { button.addEventListener("click", () => { const view = button.dataset.view as AppView | undefined; if (view) { appStore.getState().setActiveView(view); } }); }); refreshData?.addEventListener("click", () => { void appStore.getState().loadData(); }); function setupActiveTabTracking(): void { let refreshTimer: number | undefined; const scheduleRefresh = (): void => { if (refreshTimer !== undefined) { window.clearTimeout(refreshTimer); } refreshTimer = window.setTimeout(() => { refreshTimer = undefined; void appStore.getState().refreshActiveTab(); }, 80); }; globalThis.chrome?.tabs?.onActivated?.addListener(scheduleRefresh); globalThis.chrome?.tabs?.onUpdated?.addListener((_tabId, changeInfo) => { if (changeInfo.url || changeInfo.status === "complete") { scheduleRefresh(); } }); globalThis.chrome?.windows?.onFocusChanged?.addListener((windowId) => { if (windowId !== chrome.windows.WINDOW_ID_NONE) { scheduleRefresh(); } }); document.addEventListener("visibilitychange", () => { if (!document.hidden) { scheduleRefresh(); } }); window.addEventListener("focus", scheduleRefresh); } setupActiveTabTracking(); appStore.subscribe(render); render(); void appStore.getState().loadData();