Первая версия
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
export class AppView {
|
||||
constructor(reviewStore) {
|
||||
this.reviewStore = reviewStore;
|
||||
this.el = {
|
||||
layout: document.getElementById("layout-root"),
|
||||
splitterLeft: document.getElementById("splitter-left"),
|
||||
splitterRight: document.getElementById("splitter-right"),
|
||||
pickFallback: document.getElementById("pick-project-fallback"),
|
||||
projectName: document.getElementById("project-name"),
|
||||
indexStatus: document.getElementById("index-status"),
|
||||
treeInfo: document.getElementById("tree-info"),
|
||||
treeRoot: document.getElementById("tree-root"),
|
||||
fileTabs: document.getElementById("file-tabs"),
|
||||
newTextTabBtn: document.getElementById("new-text-tab"),
|
||||
mdToggleBtn: document.getElementById("md-toggle-mode"),
|
||||
fileEditor: document.getElementById("file-editor"),
|
||||
mdPreview: document.getElementById("md-preview"),
|
||||
editorInfo: document.getElementById("editor-info"),
|
||||
saveFileBtn: document.getElementById("save-file"),
|
||||
closeFileBtn: document.getElementById("close-file"),
|
||||
diffView: document.getElementById("diff-view"),
|
||||
changeList: document.getElementById("change-list"),
|
||||
toolbar: document.getElementById("review-toolbar"),
|
||||
applyAccepted: document.getElementById("apply-accepted"),
|
||||
newChatSessionBtn: document.getElementById("new-chat-session"),
|
||||
chatLog: document.getElementById("chat-log"),
|
||||
chatForm: document.getElementById("chat-form"),
|
||||
chatInput: document.getElementById("chat-input")
|
||||
};
|
||||
}
|
||||
|
||||
setIndexStatus(text) {
|
||||
if (this.el.indexStatus) this.el.indexStatus.textContent = text;
|
||||
}
|
||||
|
||||
setProjectName(name) {
|
||||
if (this.el.projectName) this.el.projectName.textContent = name;
|
||||
}
|
||||
|
||||
setTreeStats(totalFiles, totalBytes) {
|
||||
const kb = Math.round((totalBytes || 0) / 1024);
|
||||
this.el.treeInfo.textContent = `Файлов: ${totalFiles || 0} • ${kb} KB`;
|
||||
}
|
||||
|
||||
setApplyEnabled(enabled) {
|
||||
this.el.applyAccepted.disabled = !enabled;
|
||||
}
|
||||
|
||||
setEditorEnabled(enabled) {
|
||||
this.el.fileEditor.readOnly = !enabled;
|
||||
}
|
||||
|
||||
bindEditorInput(onInput) {
|
||||
this.el.fileEditor.oninput = () => onInput(this.el.fileEditor.value);
|
||||
}
|
||||
|
||||
bindMarkdownToggle(onToggle) {
|
||||
this.el.mdToggleBtn.onclick = onToggle;
|
||||
}
|
||||
|
||||
bindEditorActions(onSave, onClose) {
|
||||
this.el.saveFileBtn.onclick = onSave;
|
||||
this.el.closeFileBtn.onclick = onClose;
|
||||
}
|
||||
|
||||
bindNewTextTab(onCreate) {
|
||||
this.el.newTextTabBtn.onclick = onCreate;
|
||||
}
|
||||
|
||||
bindNewChatSession(onNewSession) {
|
||||
this.el.newChatSessionBtn.onclick = onNewSession;
|
||||
}
|
||||
|
||||
clearChat() {
|
||||
this.el.chatLog.innerHTML = "";
|
||||
}
|
||||
|
||||
setEditorActionsState({ hasFile, isDirty, infoText }) {
|
||||
this.el.saveFileBtn.disabled = !hasFile || !isDirty;
|
||||
this.el.closeFileBtn.disabled = !hasFile;
|
||||
this.el.editorInfo.textContent = infoText || "Файл не выбран";
|
||||
}
|
||||
|
||||
setMarkdownToggleVisible(visible) {
|
||||
this.el.mdToggleBtn.classList.toggle("hidden", !visible);
|
||||
if (!visible) {
|
||||
this.el.mdPreview.classList.add("hidden");
|
||||
this.el.fileEditor.classList.remove("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
setMarkdownMode(mode) {
|
||||
const isPreview = mode === "preview";
|
||||
this.el.mdToggleBtn.classList.toggle("active", isPreview);
|
||||
this.el.mdToggleBtn.textContent = isPreview ? "✏️" : "👁";
|
||||
this.el.mdToggleBtn.title = isPreview ? "Перейти в редактирование" : "Перейти в просмотр";
|
||||
this.el.mdPreview.classList.toggle("hidden", !isPreview);
|
||||
this.el.fileEditor.classList.toggle("hidden", isPreview);
|
||||
}
|
||||
|
||||
renderMarkdown(html) {
|
||||
this.el.mdPreview.innerHTML = html;
|
||||
}
|
||||
|
||||
appendChat(role, text) {
|
||||
if (!["user", "assistant"].includes(role)) return;
|
||||
const div = document.createElement("div");
|
||||
div.className = "chat-entry";
|
||||
div.textContent = `[${role}] ${text}`;
|
||||
this.el.chatLog.appendChild(div);
|
||||
this.el.chatLog.scrollTop = this.el.chatLog.scrollHeight;
|
||||
}
|
||||
|
||||
renderTree(rootNode, selectedPath, onSelect) {
|
||||
this.el.treeRoot.innerHTML = "";
|
||||
if (!rootNode) {
|
||||
this.el.treeRoot.textContent = "Директория не выбрана";
|
||||
return;
|
||||
}
|
||||
if (!rootNode.children?.length) {
|
||||
this.el.treeRoot.textContent = "В выбранной директории нет файлов";
|
||||
return;
|
||||
}
|
||||
|
||||
const renderNode = (node, depth) => {
|
||||
const line = document.createElement("div");
|
||||
line.className = "tree-item";
|
||||
line.style.paddingLeft = `${depth * 14}px`;
|
||||
const marker = node.type === "dir" ? "📁" : "📄";
|
||||
const suffix = node.type === "file" && node.supported === false ? " (skip)" : "";
|
||||
line.textContent = `${marker} ${node.name}${suffix}`;
|
||||
if (node.path === selectedPath) line.style.fontWeight = "700";
|
||||
if (node.type === "file" && node.supported !== false) line.onclick = () => onSelect(node.path);
|
||||
this.el.treeRoot.appendChild(line);
|
||||
if (node.children) node.children.forEach((child) => renderNode(child, depth + 1));
|
||||
};
|
||||
|
||||
renderNode(rootNode, 0);
|
||||
}
|
||||
|
||||
renderFileTabs(openPaths, activePath, dirtyPaths, onTabClick, onCloseTab, onRenameTab) {
|
||||
this.el.fileTabs.innerHTML = "";
|
||||
for (const path of openPaths) {
|
||||
const tab = document.createElement("div");
|
||||
tab.className = `tab-item ${path === activePath ? "active" : ""} ${dirtyPaths.has(path) ? "dirty" : ""}`;
|
||||
tab.title = path;
|
||||
|
||||
const openBtn = document.createElement("button");
|
||||
openBtn.className = "tab-main";
|
||||
openBtn.textContent = this.#formatTabLabel(path);
|
||||
openBtn.title = path;
|
||||
openBtn.onclick = () => onTabClick(path);
|
||||
openBtn.ondblclick = () => onRenameTab(path);
|
||||
|
||||
const closeBtn = document.createElement("button");
|
||||
closeBtn.className = "tab-close";
|
||||
closeBtn.type = "button";
|
||||
closeBtn.textContent = "x";
|
||||
closeBtn.onclick = (event) => {
|
||||
event.stopPropagation();
|
||||
onCloseTab(path);
|
||||
};
|
||||
|
||||
tab.append(openBtn, closeBtn);
|
||||
this.el.fileTabs.appendChild(tab);
|
||||
}
|
||||
}
|
||||
|
||||
#formatTabLabel(path) {
|
||||
const normalized = (path || "").replaceAll("\\", "/");
|
||||
const baseName = normalized.includes("/") ? normalized.split("/").pop() : normalized;
|
||||
if (baseName.length <= 32) return baseName;
|
||||
return `${baseName.slice(0, 29)}...`;
|
||||
}
|
||||
|
||||
renderFile(content) {
|
||||
this.el.fileEditor.value = content || "";
|
||||
}
|
||||
|
||||
renderChanges(changes, activePath, onPick) {
|
||||
this.el.changeList.innerHTML = "";
|
||||
if (!changes.length) {
|
||||
this.el.toolbar.classList.add("hidden");
|
||||
this.el.diffView.innerHTML = "";
|
||||
return;
|
||||
}
|
||||
|
||||
this.el.toolbar.classList.remove("hidden");
|
||||
for (const change of changes) {
|
||||
const review = this.reviewStore.get(change.path);
|
||||
const btn = document.createElement("button");
|
||||
btn.className = `change-btn ${change.path === activePath ? "active" : ""}`;
|
||||
btn.textContent = `${change.op} ${change.path} [${review?.status || "pending"}]`;
|
||||
btn.onclick = () => onPick(change.path);
|
||||
this.el.changeList.appendChild(btn);
|
||||
}
|
||||
}
|
||||
|
||||
renderDiff(change, onToggleLine) {
|
||||
this.el.diffView.innerHTML = "";
|
||||
if (!change) return;
|
||||
const review = this.reviewStore.get(change.path);
|
||||
|
||||
for (const op of change.diffOps) {
|
||||
const row = document.createElement("div");
|
||||
row.className = `diff-line ${op.kind}`;
|
||||
const marker = document.createElement("span");
|
||||
|
||||
if (op.kind === "equal") marker.textContent = " ";
|
||||
else {
|
||||
const cb = document.createElement("input");
|
||||
cb.type = "checkbox";
|
||||
cb.checked = review?.stagedSelection?.has(op.id) || false;
|
||||
cb.onchange = () => onToggleLine(change.path, op.id);
|
||||
marker.appendChild(cb);
|
||||
}
|
||||
|
||||
const text = document.createElement("span");
|
||||
if (op.kind === "add") text.textContent = `+ ${op.newLine}`;
|
||||
else if (op.kind === "remove") text.textContent = `- ${op.oldLine}`;
|
||||
else text.textContent = ` ${op.oldLine}`;
|
||||
|
||||
row.append(marker, text);
|
||||
this.el.diffView.appendChild(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user