import { fetchCurrentUser, fetchRepoBranches, fetchRepoContents, fetchRepositories } from "./gitea-api.js";
import { getActiveServer, getState, setSettings, updateSettings, addRecentRepo } from "./state.js";
import {
listLocalRepoBranches,
listLocalRepoTree,
readLocalRepoFile,
runGitBranch,
runGitClone,
runGitPull,
runGitPush,
runGitStatus,
testGiteaConnection,
} from "./tauri-api.js";
const appRoot = document.getElementById("app");
const mockRepos = [
{ id: 1, full_name: "alice/portfolio", private: false, clone_url: "https://example.com/portfolio.git", updated_at: "today", owner: { login: "alice", type: "User" } },
{ id: 2, full_name: "alice/dotfiles", private: true, clone_url: "https://example.com/dotfiles.git", updated_at: "yesterday", owner: { login: "alice", type: "User" } },
{ id: 3, full_name: "acme-corp/client-ui", private: false, clone_url: "https://example.com/client-ui.git", updated_at: "today", owner: { login: "acme-corp", type: "Organization" } },
{ id: 4, full_name: "acme-corp/api-server", private: true, clone_url: "https://example.com/api-server.git", updated_at: "3 days ago", owner: { login: "acme-corp", type: "Organization" } },
{ id: 5, full_name: "oss-collective/toolkit", private: false, clone_url: "https://example.com/toolkit.git", updated_at: "last week", owner: { login: "oss-collective", type: "Organization" } },
];
let repositories = [...mockRepos];
let currentUserLogin = ""; // authenticated user's login name
let serverTestResult = "";
let settingsNotice = "";
let gitOutput = "";
let activeRightTab = "clone"; // "clone" | "settings" | "servers"
let activeMainTab = "repos"; // "repos" | "local" | "viewer"
let repoOwnerFilter = "all"; // "all" | "personal" | "orgs"
const maxPreviewBytes = 256 * 1024;
const FOLDER_ICON = ` `;
const FILE_ICON = ` `;
function uid() {
return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
}
function escapeHtml(value = "") {
return value
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """);
}
function formatBytes(value) {
if (!Number.isFinite(value)) return "";
if (value < 1024) return `${value} B`;
if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KB`;
return `${(value / 1024 / 1024).toFixed(1)} MB`;
}
function parentPath(path = "") {
const parts = path.split("/").filter(Boolean);
parts.pop();
return parts.join("/");
}
function languageForPath(path = "") {
const extension = path.split(".").pop()?.toLowerCase();
const names = {
js: "JavaScript",
ts: "TypeScript",
tsx: "TypeScript",
jsx: "JavaScript",
rs: "Rust",
json: "JSON",
css: "CSS",
html: "HTML",
htm: "HTML",
md: "Markdown",
toml: "TOML",
yml: "YAML",
yaml: "YAML",
};
return names[extension] || "Text";
}
function highlightCode(content = "", language = "Text") {
let html = escapeHtml(content);
if (["JavaScript", "TypeScript", "Rust"].includes(language)) {
html = html.replace(
/\b(async|await|const|let|var|function|return|if|else|for|while|class|struct|enum|impl|fn|pub|use|mod|match|Ok|Err|true|false|null)\b/g,
'$1 '
);
} else if (language === "JSON") {
html = html.replace(/("[^&]*?")(\s*:)/g, '$1 $2');
} else if (language === "CSS") {
html = html.replace(/([.#]?[a-zA-Z0-9_-]+)(\s*\{)/g, '$1 $2');
}
return html;
}
function isMarkdownPath(path = "") {
return /\.(md|markdown)$/i.test(path);
}
function safeMarkdownHref(value = "") {
const href = value.trim();
const lowerHref = href.toLowerCase();
if (!href || lowerHref.startsWith("javascript:") || lowerHref.startsWith("data:")) {
return "";
}
return href;
}
function renderMarkdownInline(value = "") {
return escapeHtml(value)
.replace(/`([^`]+)`/g, "$1")
.replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^&]*")?\)/g, (_match, label, href) => {
const safeHref = safeMarkdownHref(href.replaceAll("&", "&"));
return safeHref ? `${label} ` : label;
})
.replace(/\*\*([^*]+)\*\*/g, "$1 ")
.replace(/__([^_]+)__/g, "$1 ")
.replace(/\*([^*]+)\*/g, "$1 ")
.replace(/_([^_]+)_/g, "$1 ");
}
function renderMarkdown(content = "") {
const lines = content.replace(/\r\n/g, "\n").split("\n");
const html = [];
let inFence = false;
let fenceLanguage = "";
let fenceLines = [];
let listType = "";
let listItems = [];
const flushList = () => {
if (!listType) return;
html.push(`<${listType}>${listItems.map((item) => `
${renderMarkdownInline(item)} `).join("")}${listType}>`);
listType = "";
listItems = [];
};
const flushFence = () => {
html.push(`${escapeHtml(fenceLines.join("\n"))} `);
inFence = false;
fenceLanguage = "";
fenceLines = [];
};
for (const line of lines) {
const fence = line.match(/^```(\w+)?\s*$/);
if (fence) {
if (inFence) {
flushFence();
} else {
flushList();
inFence = true;
fenceLanguage = fence[1] || "";
fenceLines = [];
}
continue;
}
if (inFence) {
fenceLines.push(line);
continue;
}
if (!line.trim()) {
flushList();
continue;
}
const heading = line.match(/^(#{1,6})\s+(.+)$/);
if (heading) {
flushList();
const level = heading[1].length;
html.push(`${renderMarkdownInline(heading[2])} `);
continue;
}
if (/^\s*[-*_]{3,}\s*$/.test(line)) {
flushList();
html.push(" ");
continue;
}
const quote = line.match(/^>\s?(.*)$/);
if (quote) {
flushList();
html.push(`${renderMarkdownInline(quote[1])} `);
continue;
}
const unordered = line.match(/^\s*[-*+]\s+(.+)$/);
if (unordered) {
if (listType && listType !== "ul") flushList();
listType = "ul";
listItems.push(unordered[1]);
continue;
}
const ordered = line.match(/^\s*\d+\.\s+(.+)$/);
if (ordered) {
if (listType && listType !== "ol") flushList();
listType = "ol";
listItems.push(ordered[1]);
continue;
}
flushList();
html.push(`${renderMarkdownInline(line)}
`);
}
if (inFence) flushFence();
flushList();
return html.join("");
}
function decodeBase64Content(content = "") {
const cleaned = content.replace(/\s/g, "");
const binary = atob(cleaned);
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
if (bytes.byteLength > maxPreviewBytes) {
return { content: "", size: bytes.byteLength, isBinary: false, tooLarge: true };
}
if (bytes.includes(0)) {
return { content: "", size: bytes.byteLength, isBinary: true, tooLarge: false };
}
try {
return {
content: new TextDecoder("utf-8", { fatal: true }).decode(bytes),
size: bytes.byteLength,
isBinary: false,
tooLarge: false,
};
} catch {
return { content: "", size: bytes.byteLength, isBinary: true, tooLarge: false };
}
}
function normaliseRemoteEntries(contents) {
const items = Array.isArray(contents) ? contents : [contents];
return items
.map((item) => ({
name: item.name,
path: item.path || item.name,
type: item.type === "dir" ? "dir" : "file",
size: item.size,
}))
.sort((a, b) => (a.type === b.type ? a.name.localeCompare(b.name) : a.type === "dir" ? -1 : 1));
}
function normaliseLocalEntries(entries) {
return entries.map((entry) => ({
name: entry.name,
path: entry.path,
type: entry.entryType === "tree" ? "dir" : "file",
size: entry.size,
}));
}
function serverFormTemplate(server = null) {
// Reused in first-launch setup and in Settings server management.
const defaults = {
id: "",
displayName: "",
serverUrl: "",
authMethod: "token",
token: "",
username: "",
password: "",
};
const config = { ...defaults, ...(server || {}) };
return `
`;
}
function welcomeView() {
appRoot.innerHTML = `
Desktop
Welcome to Gitpub Desktop
Connect your first Gitea backend. Gitpub Desktop works with any compatible Gitea server.
${serverFormTemplate(null)}
`;
bindServerFormEvents();
}
function isOrgRepo(repo) {
const ownerLogin = repo.owner?.login || repo.full_name.split("/")[0];
// When we know the authenticated user, compare against owner login.
// Fall back to owner.type for cases where user info wasn't fetched.
if (currentUserLogin) {
return ownerLogin.toLowerCase() !== currentUserLogin.toLowerCase();
}
return repo.owner?.type === "Organization" || repo.owner?.type === "organization";
}
function filteredRepositories() {
const searchTerm = getState().repoSearch.trim().toLowerCase();
let result = repositories;
if (repoOwnerFilter === "personal") {
result = result.filter((repo) => !isOrgRepo(repo));
} else if (repoOwnerFilter === "orgs") {
result = result.filter((repo) => isOrgRepo(repo));
}
if (searchTerm) {
result = result.filter((repo) => repo.full_name.toLowerCase().includes(searchTerm));
}
return result;
}
function groupedByOrg(repos) {
const groups = {};
for (const repo of repos) {
const org = repo.owner?.login || "Unknown";
if (!groups[org]) groups[org] = [];
groups[org].push(repo);
}
return groups;
}
function repoCardTemplate(repo) {
return `
${escapeHtml(repo.full_name)}
${repo.private ? "Private" : "Public"} · ${escapeHtml(repo.updated_at || "recently")}
View
Clone
Open
`;
}
function breadcrumbTemplate(repoName, path = "") {
const parts = path.split("/").filter(Boolean);
const crumbs = [`${escapeHtml(repoName)} `];
parts.forEach((part, index) => {
const crumbPath = parts.slice(0, index + 1).join("/");
crumbs.push(`/ ${escapeHtml(part)} `);
});
return crumbs.join("");
}
function filePreviewTemplate(file) {
const language = languageForPath(file.path);
const meta = [language, formatBytes(file.size)].filter(Boolean).join(" · ");
const header = `
`;
if (file.tooLarge) {
return header + `File too large to preview
Preview is limited to ${formatBytes(maxPreviewBytes)}.
`;
}
if (file.isBinary) {
return header + `Binary file
Binary content cannot be previewed.
`;
}
if (isMarkdownPath(file.path)) {
return header + `${renderMarkdown(file.content || "")}
`;
}
return header + `${highlightCode(file.content || "", language)} `;
}
function viewerTemplate() {
const { viewer } = getState();
if (!viewer.source) {
return `
Repository Viewer
No repository selected
Use View on a repository card, or open a local path and click View files.
`;
}
const rows = [];
if (viewer.path) {
rows.push(`
${FOLDER_ICON}
..
`);
}
viewer.entries.forEach((entry) => {
const active = viewer.selectedFile?.path === entry.path ? " active" : "";
rows.push(`
${entry.type === "dir" ? FOLDER_ICON : FILE_ICON}
${escapeHtml(entry.name)}
${entry.type === "file" ? formatBytes(entry.size) : ""}
`);
});
if (!rows.length && !viewer.loading) {
rows.push(`This folder is empty.
`);
}
const showReadme = viewer.readmeFile && !viewer.selectedFile;
return `
${breadcrumbTemplate(viewer.repoName || "repo", viewer.path)}
${viewer.source === "remote" ? "Remote" : "Local"}
${viewer.branches.map((b) => `${escapeHtml(b.name)}${b.current ? " ✓" : ""} `).join("")}
Refresh
${viewer.error ? `${escapeHtml(viewer.error)}
` : ""}
${viewer.loading ? `Loading…
` : ""}
${rows.join("")}
${viewer.selectedFile ? `${filePreviewTemplate(viewer.selectedFile)}
` : ""}
${showReadme ? `
${renderMarkdown(viewer.readmeFile.content || "")}
` : ""}
`;
}
function dashboardView() {
const state = getState();
const activeServer = getActiveServer();
const visibleRepos = filteredRepositories();
appRoot.innerHTML = `
Repositories
Local Repo
Viewer
${activeMainTab === "repos"
? ` `
: ""}
${activeMainTab === "repos" ? `
All
Personal
Organizations
${repoOwnerFilter === "orgs"
? (() => {
const groups = groupedByOrg(visibleRepos);
const orgNames = Object.keys(groups).sort();
if (!orgNames.length) {
return `No organisation repositories found
Try refreshing or check your server connection.
`;
}
return orgNames.map((org) => `
${groups[org].map((repo) => repoCardTemplate(repo)).join("")}
`).join("");
})()
: `
${visibleRepos.length
? visibleRepos.map((repo) => repoCardTemplate(repo)).join("")
: `
No repositories found
Try refreshing or check your server connection.
`}
`}
` : activeMainTab === "local" ? `
Local Repository
Open
${state.selectedRepoName
? `Active: ${escapeHtml(state.selectedRepoName)}
`
: ""}
Status
Branch
Pull
Push
View files
Commit message
Commit support coming in the next milestone.
${gitOutput
? `${escapeHtml(gitOutput)} `
: `Run a git command to see output here.
`}
` : viewerTemplate()}
`;
bindDashboardEvents();
}
function bindServerFormEvents(existingServer = null) {
const testButton = document.getElementById("test-server-btn");
const saveButton = document.getElementById("save-server-btn");
const formCard = document.getElementById("server-form-card");
if (!testButton || !saveButton || !formCard) return;
const collectPayload = () => {
const get = (name) => formCard.querySelector(`[name="${name}"]`)?.value?.trim() || "";
return {
id: get("id"),
displayName: get("displayName"),
serverUrl: get("serverUrl"),
authMethod: get("authMethod") || "token",
token: get("token"),
username: get("username"),
password: get("password"),
};
};
testButton.addEventListener("click", async () => {
const payload = collectPayload();
try {
const result = await testGiteaConnection(payload);
serverTestResult = `${result.ok ? "Connected" : "Failed"}: ${result.message} (${result.apiBaseUrl})`;
render();
if (existingServer) {
openServerForm(existingServer.id);
}
} catch (error) {
serverTestResult = `Connection test failed: ${error.message}`;
render();
if (existingServer) {
openServerForm(existingServer.id);
}
}
});
saveButton.addEventListener("click", () => {
const payload = collectPayload();
if (!payload.displayName || !payload.serverUrl) {
settingsNotice = "Display name and server URL are required.";
render();
if (existingServer) openServerForm(existingServer.id);
return;
}
const state = getState();
let nextServers = [...state.settings.servers];
const nextId = payload.id || uid();
const serverData = { ...payload, id: nextId };
const existingIndex = nextServers.findIndex((item) => item.id === nextId);
if (existingIndex >= 0) {
nextServers[existingIndex] = serverData;
settingsNotice = `Updated server ${payload.displayName}.`;
} else {
nextServers.push(serverData);
settingsNotice = `Added server ${payload.displayName}.`;
}
const activeServerId = state.settings.activeServerId || nextId;
setSettings({ ...state.settings, servers: nextServers, activeServerId });
serverTestResult = "";
render();
});
}
function openServerForm(serverId = null) {
const slot = document.getElementById("server-form-slot");
if (!slot) return;
const server = getState().settings.servers.find((item) => item.id === serverId) || null;
slot.innerHTML = serverFormTemplate(server);
bindServerFormEvents(server);
}
async function loadRepositories() {
const activeServer = getActiveServer();
if (!activeServer) {
repositories = [...mockRepos];
currentUserLogin = "alice"; // mock user matches mock personal repos
return;
}
try {
const [user, repos] = await Promise.all([
fetchCurrentUser(activeServer),
fetchRepositories(activeServer),
]);
currentUserLogin = user.login || "";
repositories = repos;
} catch (error) {
settingsNotice = `Using mock repositories. API fetch failed: ${error.message}`;
currentUserLogin = "alice";
repositories = [...mockRepos];
}
}
async function autoLoadReadme() {
const state = getState();
const viewer = state.viewer;
const entry = viewer.entries.find(
(e) => e.type === "file" && /^readme(\.md|\.txt|\.rst)?$/i.test(e.name)
);
viewer.readmeFile = null;
if (!entry) return;
try {
if (viewer.source === "remote") {
const activeServer = getActiveServer();
const file = await fetchRepoContents(activeServer, viewer.repoName, entry.path, viewer.branch);
const decoded = file.encoding === "base64"
? decodeBase64Content(file.content || "")
: { content: file.content || "", isBinary: false, tooLarge: false };
if (!decoded.isBinary && !decoded.tooLarge) {
viewer.readmeFile = { path: entry.path, content: decoded.content };
}
} else {
const file = await readLocalRepoFile(
viewer.repoPath, viewer.branch, entry.path, state.settings.gitExecutablePath
);
if (!file.isBinary && !file.tooLarge) {
viewer.readmeFile = { path: entry.path, content: file.content || "" };
}
}
render();
} catch {
// README load failure is non-fatal
}
}
async function loadViewerPath(path = "") {
const state = getState();
const viewer = state.viewer;
if (!viewer.source) return;
viewer.path = path;
viewer.loading = true;
viewer.error = "";
viewer.entries = [];
viewer.selectedFile = null;
viewer.readmeFile = null;
render();
try {
if (viewer.source === "remote") {
const activeServer = getActiveServer();
if (!activeServer) throw new Error("No active server is configured.");
const contents = await fetchRepoContents(activeServer, viewer.repoName, path, viewer.branch);
viewer.entries = normaliseRemoteEntries(contents).filter((entry) => entry.type === "dir" || entry.path !== path);
} else {
const entries = await listLocalRepoTree(
viewer.repoPath,
viewer.branch,
path,
state.settings.gitExecutablePath
);
viewer.entries = normaliseLocalEntries(entries);
}
viewer.loading = false;
render();
await autoLoadReadme();
} catch (error) {
viewer.error = `Unable to load repository contents: ${error.message}`;
viewer.loading = false;
render();
}
}
async function openViewerFile(path) {
const state = getState();
const viewer = state.viewer;
if (!viewer.source || !path) return;
viewer.loading = true;
viewer.error = "";
viewer.selectedFile = null;
render();
try {
if (viewer.source === "remote") {
const activeServer = getActiveServer();
if (!activeServer) throw new Error("No active server is configured.");
const file = await fetchRepoContents(activeServer, viewer.repoName, path, viewer.branch);
const decoded = file.encoding === "base64"
? decodeBase64Content(file.content || "")
: { content: file.content || "", size: file.size || 0, isBinary: false, tooLarge: false };
viewer.selectedFile = {
path: file.path || path,
size: file.size || decoded.size,
content: decoded.content,
isBinary: decoded.isBinary,
tooLarge: decoded.tooLarge,
};
} else {
const file = await readLocalRepoFile(
viewer.repoPath,
viewer.branch,
path,
state.settings.gitExecutablePath
);
viewer.selectedFile = {
path: file.path,
size: file.size,
content: file.content || "",
isBinary: file.isBinary,
tooLarge: file.tooLarge,
};
}
} catch (error) {
viewer.error = `Unable to preview file: ${error.message}`;
} finally {
viewer.loading = false;
render();
}
}
async function openRemoteViewer(repo) {
const state = getState();
const viewer = state.viewer;
Object.assign(viewer, {
source: "remote",
repoName: repo.full_name,
repoPath: "",
cloneUrl: repo.clone_url || "",
defaultBranch: repo.default_branch || "",
branch: "",
branches: [],
path: "",
entries: [],
selectedFile: null,
loading: true,
error: "",
});
activeMainTab = "viewer";
render();
try {
const activeServer = getActiveServer();
if (!activeServer) throw new Error("No active server is configured.");
const branches = await fetchRepoBranches(activeServer, repo.full_name);
const branchNames = branches.map((branch) => branch.name).filter(Boolean);
const selectedBranch = repo.default_branch || branchNames[0] || "";
viewer.branches = branchNames.map((name) => ({ name, current: name === selectedBranch }));
viewer.branch = selectedBranch;
await loadViewerPath("");
} catch (error) {
viewer.loading = false;
viewer.error = `Unable to open repository viewer: ${error.message}`;
render();
}
}
async function openLocalViewer() {
const state = getState();
const value = state.selectedRepoPath || state.localRepoPathInput;
if (!value) {
gitOutput = "Select or enter a local repository path first.";
activeMainTab = "local";
render();
return;
}
const viewer = state.viewer;
Object.assign(viewer, {
source: "local",
repoName: state.selectedRepoName || value.split(/[/\\]/).filter(Boolean).pop() || value,
repoPath: value,
cloneUrl: "",
defaultBranch: "",
branch: "",
branches: [],
path: "",
entries: [],
selectedFile: null,
loading: true,
error: "",
});
activeMainTab = "viewer";
render();
try {
const branches = await listLocalRepoBranches(value, state.settings.gitExecutablePath);
const selectedBranch = branches.find((branch) => branch.current)?.name || branches[0]?.name || "";
viewer.branches = branches;
viewer.branch = selectedBranch;
await loadViewerPath("");
} catch (error) {
viewer.loading = false;
viewer.error = `Unable to open local viewer: ${error.message}`;
render();
}
}
async function runRepoCommand(actionName, runner) {
const state = getState();
if (!state.selectedRepoPath) {
gitOutput = "Select or enter a local repository path first.";
render();
return;
}
try {
const result = await runner();
gitOutput = `${actionName}: ${result.command}\n\n${result.stdout || "(no stdout)"}\n${result.stderr ? `\n${result.stderr}` : ""}`;
} catch (error) {
gitOutput = `${actionName} failed: ${error.message}`;
}
render();
}
function bindDashboardEvents() {
const state = getState();
document.getElementById("repo-search-input")?.addEventListener("input", (event) => {
state.repoSearch = event.target.value;
render();
});
document.getElementById("refresh-repos-btn")?.addEventListener("click", async () => {
await loadRepositories();
render();
});
document.getElementById("open-settings-btn")?.addEventListener("click", () => {
activeRightTab = "servers";
render();
});
document.querySelectorAll("[data-right-tab]").forEach((btn) => {
btn.addEventListener("click", () => {
activeRightTab = btn.dataset.rightTab;
render();
});
});
document.querySelectorAll("[data-main-tab]").forEach((btn) => {
btn.addEventListener("click", () => {
activeMainTab = btn.dataset.mainTab;
render();
});
});
document.querySelectorAll("[data-owner-filter]").forEach((btn) => {
btn.addEventListener("click", () => {
repoOwnerFilter = btn.dataset.ownerFilter;
render();
});
});
document.querySelectorAll(".view-repo-btn").forEach((button) => {
button.addEventListener("click", () => {
const repo = repositories.find((item) => item.full_name === button.dataset.repoName);
if (repo) {
openRemoteViewer(repo);
}
});
});
document.querySelectorAll(".open-repo-btn").forEach((button) => {
button.addEventListener("click", () => {
state.selectedRepoName = button.dataset.repoName || "Repository";
activeMainTab = "local";
render();
});
});
document.querySelectorAll(".clone-repo-btn").forEach((button) => {
button.addEventListener("click", () => {
state.cloneUrlInput = button.dataset.cloneUrl || "";
activeRightTab = "clone";
render();
});
});
document.getElementById("open-local-repo-btn")?.addEventListener("click", () => {
// MVP local detection is user-driven path entry; folder picker can be added next.
const value = document.getElementById("local-repo-path-input")?.value?.trim() || "";
state.localRepoPathInput = value;
state.selectedRepoPath = value;
state.selectedRepoName = value.split(/[/\\]/).filter(Boolean).pop() || value;
addRecentRepo(value);
render();
});
document.getElementById("view-local-repo-btn")?.addEventListener("click", () => {
const value = document.getElementById("local-repo-path-input")?.value?.trim() || state.selectedRepoPath;
state.localRepoPathInput = value;
state.selectedRepoPath = value;
state.selectedRepoName = value.split(/[/\\]/).filter(Boolean).pop() || value;
if (value) addRecentRepo(value);
openLocalViewer();
});
document.getElementById("clone-btn")?.addEventListener("click", async () => {
state.cloneUrlInput = document.getElementById("clone-url-input")?.value?.trim() || "";
state.cloneDestinationInput = document.getElementById("clone-destination-input")?.value?.trim() || "";
if (!state.cloneUrlInput || !state.cloneDestinationInput) {
gitOutput = "Clone URL and destination path are required.";
render();
return;
}
try {
const result = await runGitClone(
state.cloneUrlInput,
state.cloneDestinationInput,
state.settings.gitExecutablePath
);
gitOutput = `${result.command}\n\n${result.stdout || "Clone completed."}\n${result.stderr || ""}`;
addRecentRepo(state.cloneDestinationInput);
} catch (error) {
gitOutput = `Clone failed: ${error.message}`;
}
render();
});
document.getElementById("git-status-btn")?.addEventListener("click", () => {
runRepoCommand("Status", () => runGitStatus(state.selectedRepoPath, state.settings.gitExecutablePath));
});
document.getElementById("git-branch-btn")?.addEventListener("click", () => {
runRepoCommand("Branch", () => runGitBranch(state.selectedRepoPath, state.settings.gitExecutablePath));
});
document.getElementById("git-pull-btn")?.addEventListener("click", () => {
runRepoCommand("Pull", () => runGitPull(state.selectedRepoPath, state.settings.gitExecutablePath));
});
document.getElementById("git-push-btn")?.addEventListener("click", () => {
runRepoCommand("Push", () => runGitPush(state.selectedRepoPath, state.settings.gitExecutablePath));
});
document.getElementById("save-basic-settings-btn")?.addEventListener("click", () => {
updateSettings({
theme: document.getElementById("theme-select")?.value || "dark",
gitExecutablePath: document.getElementById("git-path-input")?.value?.trim() || "",
defaultCloneDirectory: document.getElementById("default-clone-dir-input")?.value?.trim() || "",
});
settingsNotice = "Saved basic settings.";
render();
});
document.getElementById("add-server-btn")?.addEventListener("click", () => openServerForm());
document.querySelectorAll(".set-default-server-btn").forEach((button) => {
button.addEventListener("click", async () => {
updateSettings({ activeServerId: button.dataset.id });
settingsNotice = "Default server updated.";
await loadRepositories();
render();
});
});
document.querySelectorAll(".delete-server-btn").forEach((button) => {
button.addEventListener("click", () => {
const nextServers = getState().settings.servers.filter((server) => server.id !== button.dataset.id);
const nextActive =
getState().settings.activeServerId === button.dataset.id ? nextServers[0]?.id || null : getState().settings.activeServerId;
setSettings({ ...getState().settings, servers: nextServers, activeServerId: nextActive });
settingsNotice = "Server removed.";
render();
});
});
document.querySelectorAll(".edit-server-btn").forEach((button) => {
button.addEventListener("click", () => openServerForm(button.dataset.id));
});
document.getElementById("viewer-branch-select")?.addEventListener("change", (event) => {
state.viewer.branch = event.target.value;
loadViewerPath("");
});
document.getElementById("viewer-refresh-btn")?.addEventListener("click", () => {
loadViewerPath(state.viewer.path);
});
document.querySelectorAll(".viewer-crumb-btn").forEach((button) => {
button.addEventListener("click", () => {
loadViewerPath(button.dataset.viewerPath || "");
});
});
document.querySelectorAll(".viewer-row").forEach((button) => {
button.addEventListener("click", () => {
const path = button.dataset.entryPath || "";
if (button.dataset.entryType === "dir") {
loadViewerPath(path);
} else {
openViewerFile(path);
}
});
});
}
function render() {
if (!getState().settings.servers.length) {
// First launch always lands in setup until at least one backend is saved.
welcomeView();
return;
}
dashboardView();
}
window.addEventListener("DOMContentLoaded", async () => {
await loadRepositories();
render();
});