This commit is contained in:
+286
-20
@@ -1,29 +1,222 @@
|
||||
import { createIcons, CirclePlus, FolderGit2, GitBranch, Link, List, RefreshCw } from "lucide";
|
||||
import "./styles.css";
|
||||
import { appStore } from "./store";
|
||||
import { appStore, type AppView } from "./store";
|
||||
import type { GiteaRepoSummary } from "./api";
|
||||
import type { KnownUrl, LocalWorkspace } from "./localContext";
|
||||
|
||||
const appWindow = document.querySelector<HTMLElement>("#app-window");
|
||||
const dragHandle = document.querySelector<HTMLElement>("#window-drag-handle");
|
||||
const configBadge = document.querySelector<HTMLSpanElement>("#config-badge");
|
||||
const baseUrl = document.querySelector<HTMLElement>("#base-url");
|
||||
const tokenState = document.querySelector<HTMLElement>("#token-state");
|
||||
const targetRepo = document.querySelector<HTMLElement>("#target-repo");
|
||||
const status = document.querySelector<HTMLElement>("#status");
|
||||
const testConnection = document.querySelector<HTMLButtonElement>("#test-connection");
|
||||
const refreshData = document.querySelector<HTMLButtonElement>("#refresh-data");
|
||||
const viewHeader = document.querySelector<HTMLElement>("#view-header");
|
||||
const viewContent = document.querySelector<HTMLElement>("#view-content");
|
||||
const viewButtons = Array.from(document.querySelectorAll<HTMLButtonElement>("[data-view]"));
|
||||
|
||||
function setText(element: HTMLElement | null, text: string): void {
|
||||
if (element) {
|
||||
element.textContent = text;
|
||||
createIcons({
|
||||
icons: {
|
||||
CirclePlus,
|
||||
FolderGit2,
|
||||
GitBranch,
|
||||
Link,
|
||||
List,
|
||||
RefreshCw
|
||||
}
|
||||
});
|
||||
|
||||
function escapeHtml(value: string): string {
|
||||
return value.replace(/[&<>"']/g, (char) => {
|
||||
const replacements: Record<string, string> = {
|
||||
"&": "&",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
"\"": """,
|
||||
"'": "'"
|
||||
};
|
||||
return replacements[char];
|
||||
});
|
||||
}
|
||||
|
||||
function render(): void {
|
||||
function formatDate(value: string): string {
|
||||
if (!value) {
|
||||
return "No commit date";
|
||||
}
|
||||
|
||||
return new Intl.DateTimeFormat(undefined, {
|
||||
dateStyle: "medium",
|
||||
timeStyle: "short"
|
||||
}).format(new Date(value));
|
||||
}
|
||||
|
||||
function formatSha(value: string): string {
|
||||
return value ? value.slice(0, 8) : "unknown";
|
||||
}
|
||||
|
||||
function getRepoWorkspaceLink(repo: GiteaRepoSummary): string {
|
||||
const context = appStore.getState().localContext;
|
||||
const worktree = context?.gitWorktrees.find((item) => item.giteaRepo?.fullName === repo.fullName);
|
||||
|
||||
if (!worktree) {
|
||||
return `<span class="muted">No workspace link</span>`;
|
||||
}
|
||||
|
||||
return `<span class="link-chip">Workspace link</span><span class="path-text">${escapeHtml(worktree.path)}</span>`;
|
||||
}
|
||||
|
||||
function renderRepos(repos: GiteaRepoSummary[]): string {
|
||||
if (!repos.length) {
|
||||
return `<div class="empty-state">No Gitea repos loaded yet.</div>`;
|
||||
}
|
||||
|
||||
return repos
|
||||
.map(
|
||||
(repo) => `
|
||||
<article class="item-card">
|
||||
<div class="item-heading">
|
||||
<div>
|
||||
<a class="item-title" href="${repo.webUrl}" target="_blank" rel="noreferrer">${escapeHtml(repo.fullName)}</a>
|
||||
<p class="item-subtitle">${escapeHtml(repo.description || "No description")}</p>
|
||||
</div>
|
||||
<span class="badge quiet">${repo.isPrivate ? "Private" : "Public"}</span>
|
||||
</div>
|
||||
<div class="meta-grid">
|
||||
<span>Branch: <strong>${escapeHtml(repo.defaultBranch)}</strong></span>
|
||||
<span>Last commit: <strong>${formatDate(repo.lastCommitAt)}</strong></span>
|
||||
<span>SHA: <strong>${formatSha(repo.lastCommitSha)}</strong></span>
|
||||
</div>
|
||||
<p class="commit-message">${escapeHtml(repo.lastCommitMessage)}</p>
|
||||
<div class="linked-row">${getRepoWorkspaceLink(repo)}</div>
|
||||
</article>
|
||||
`
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
|
||||
function renderWorkspaces(workspaces: LocalWorkspace[]): string {
|
||||
if (!workspaces.length) {
|
||||
return `<div class="empty-state">No workspaces found in workspace_Father.</div>`;
|
||||
}
|
||||
|
||||
return workspaces
|
||||
.map((workspace) => {
|
||||
const links = workspace.linkedRepos.length
|
||||
? workspace.linkedRepos
|
||||
.map(
|
||||
(repo) =>
|
||||
`<a class="repo-link" href="${repo.webUrl}" target="_blank" rel="noreferrer">${escapeHtml(repo.fullName)}</a>`
|
||||
)
|
||||
.join("")
|
||||
: `<span class="muted">No Gitea repo link established</span>`;
|
||||
|
||||
return `
|
||||
<article class="item-card">
|
||||
<div class="item-heading">
|
||||
<div>
|
||||
<h2 class="item-title">${escapeHtml(workspace.name)}</h2>
|
||||
<p class="path-text">${escapeHtml(workspace.path)}</p>
|
||||
</div>
|
||||
${workspace.linkedRepos.length ? `<span class="link-chip">Linked</span>` : ""}
|
||||
</div>
|
||||
<div class="repo-link-list">${links}</div>
|
||||
</article>
|
||||
`;
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
function renderUrls(urls: KnownUrl[]): string {
|
||||
if (!urls.length) {
|
||||
return `<div class="empty-state">No known URLs registered.</div>`;
|
||||
}
|
||||
|
||||
return urls
|
||||
.map(
|
||||
(knownUrl) => `
|
||||
<article class="item-card">
|
||||
<div class="item-heading">
|
||||
<div>
|
||||
<h2 class="item-title">${escapeHtml(knownUrl.label)}</h2>
|
||||
<a class="url-text" href="${knownUrl.url}" target="_blank" rel="noreferrer">${escapeHtml(knownUrl.url)}</a>
|
||||
</div>
|
||||
<button class="visit-button" type="button" data-open-url="${knownUrl.url}">Quick visit</button>
|
||||
</div>
|
||||
<div class="linked-row">
|
||||
<span class="link-chip">Workspace link</span>
|
||||
<span class="path-text">${escapeHtml(knownUrl.workspacePath)}</span>
|
||||
</div>
|
||||
<div class="linked-row">
|
||||
<span class="link-chip">Gitea link</span>
|
||||
${
|
||||
knownUrl.giteaRepo
|
||||
? `<a class="repo-link" href="${knownUrl.giteaRepo.webUrl}" target="_blank" rel="noreferrer">${escapeHtml(knownUrl.giteaRepo.fullName)}</a>`
|
||||
: `<span class="muted">No Gitea repo link established</span>`
|
||||
}
|
||||
</div>
|
||||
</article>
|
||||
`
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
|
||||
function openUrl(url: string): void {
|
||||
if (globalThis.chrome?.tabs?.create) {
|
||||
void globalThis.chrome.tabs.create({ url });
|
||||
return;
|
||||
}
|
||||
|
||||
window.open(url, "_blank", "noopener,noreferrer");
|
||||
}
|
||||
|
||||
function renderView(): void {
|
||||
if (!viewHeader || !viewContent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const state = appStore.getState();
|
||||
const context = state.localContext;
|
||||
const titles: Record<AppView, string> = {
|
||||
urls: "URLs",
|
||||
repos: "Gitea repos",
|
||||
workspaces: "Workspaces"
|
||||
};
|
||||
|
||||
const descriptions: Record<AppView, string> = {
|
||||
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.innerHTML = `
|
||||
<div>
|
||||
<h2>${titles[state.activeView]}</h2>
|
||||
<p>${escapeHtml(descriptions[state.activeView])}</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (state.activeView === "repos") {
|
||||
viewContent.innerHTML = renderRepos(state.repos);
|
||||
} else if (state.activeView === "workspaces") {
|
||||
viewContent.innerHTML = renderWorkspaces(context?.workspaces ?? []);
|
||||
} else {
|
||||
viewContent.innerHTML = renderUrls(context?.knownUrls ?? []);
|
||||
}
|
||||
|
||||
viewContent.querySelectorAll<HTMLButtonElement>("[data-open-url]").forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
const url = button.dataset.openUrl;
|
||||
if (url) {
|
||||
openUrl(url);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function renderChrome(): void {
|
||||
const state = appStore.getState();
|
||||
const config = state.config;
|
||||
|
||||
setText(baseUrl, config.baseUrl || "Not configured");
|
||||
setText(tokenState, config.hasToken ? "Configured" : "Not configured");
|
||||
setText(targetRepo, config.targetRepo || "Not configured");
|
||||
setText(configBadge, config.isConfigured ? "Configured" : "Missing env");
|
||||
|
||||
if (configBadge) {
|
||||
configBadge.textContent = config.isConfigured ? "Configured" : "Missing env";
|
||||
configBadge.dataset.tone = config.isConfigured ? "success" : "warning";
|
||||
}
|
||||
|
||||
@@ -32,14 +225,87 @@ function render(): void {
|
||||
status.dataset.tone = state.statusTone;
|
||||
}
|
||||
|
||||
if (testConnection) {
|
||||
testConnection.disabled = state.isTestingConnection;
|
||||
if (refreshData) {
|
||||
refreshData.disabled = state.isLoadingData;
|
||||
}
|
||||
|
||||
if (appWindow) {
|
||||
appWindow.style.transform = `translate(${state.windowPosition.x}px, ${state.windowPosition.y}px)`;
|
||||
}
|
||||
|
||||
viewButtons.forEach((button) => {
|
||||
const view = button.dataset.view;
|
||||
button.dataset.active = view === state.activeView ? "true" : "false";
|
||||
});
|
||||
}
|
||||
|
||||
function render(): void {
|
||||
renderChrome();
|
||||
renderView();
|
||||
}
|
||||
|
||||
function setupDragging(): void {
|
||||
if (!appWindow || !dragHandle) {
|
||||
return;
|
||||
}
|
||||
|
||||
let dragStart:
|
||||
| {
|
||||
pointerId: number;
|
||||
pointerX: number;
|
||||
pointerY: number;
|
||||
windowX: number;
|
||||
windowY: number;
|
||||
}
|
||||
| null = null;
|
||||
|
||||
dragHandle.addEventListener("pointerdown", (event) => {
|
||||
const state = appStore.getState();
|
||||
dragStart = {
|
||||
pointerId: event.pointerId,
|
||||
pointerX: event.clientX,
|
||||
pointerY: event.clientY,
|
||||
windowX: state.windowPosition.x,
|
||||
windowY: state.windowPosition.y
|
||||
};
|
||||
dragHandle.setPointerCapture(event.pointerId);
|
||||
});
|
||||
|
||||
dragHandle.addEventListener("pointermove", (event) => {
|
||||
if (!dragStart || event.pointerId !== dragStart.pointerId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = appWindow.getBoundingClientRect();
|
||||
const maxX = Math.max(0, window.innerWidth - rect.width);
|
||||
const maxY = Math.max(0, window.innerHeight - rect.height);
|
||||
const x = Math.min(Math.max(0, dragStart.windowX + event.clientX - dragStart.pointerX), maxX);
|
||||
const y = Math.min(Math.max(0, dragStart.windowY + event.clientY - dragStart.pointerY), maxY);
|
||||
appStore.getState().setWindowPosition({ x, y });
|
||||
});
|
||||
|
||||
dragHandle.addEventListener("pointerup", (event) => {
|
||||
if (dragStart?.pointerId === event.pointerId) {
|
||||
dragStart = null;
|
||||
dragHandle.releasePointerCapture(event.pointerId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
setupDragging();
|
||||
appStore.subscribe(render);
|
||||
render();
|
||||
|
||||
testConnection?.addEventListener("click", () => {
|
||||
void appStore.getState().testConnection();
|
||||
});
|
||||
void appStore.getState().loadData();
|
||||
|
||||
Reference in New Issue
Block a user