Uklonjen autofocus sa modala koji prikazuje formu bez poziva
All checks were successful
Tests / unit-tests (push) Successful in 25s
All checks were successful
Tests / unit-tests (push) Successful in 25s
- autofocus na inputu u hidden modalu izazivao prikaz forme na nekim browserima pri učitavanju stranice - Fokus se sada daje programski tek kad se modal otvori - Isto rešenje primenjeno i na projects.html i chat.html Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
6c0ca3a96f
commit
e60e574287
@ -221,17 +221,18 @@ func NewPTYSessionManager() *PTYSessionManager {
|
||||
}
|
||||
|
||||
// GetOrCreate returns an existing session or creates a new one.
|
||||
func (m *PTYSessionManager) GetOrCreate(project, projectDir string) (*PTYSession, bool, error) {
|
||||
// sessionKey is a free-form string (e.g. "project:tabId").
|
||||
func (m *PTYSessionManager) GetOrCreate(sessionKey, projectDir string) (*PTYSession, bool, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if sess, ok := m.sessions[project]; ok {
|
||||
if sess, ok := m.sessions[sessionKey]; ok {
|
||||
// Check if process is still alive
|
||||
select {
|
||||
case <-sess.Done():
|
||||
// Process exited, remove and create new
|
||||
sess.Close()
|
||||
delete(m.sessions, project)
|
||||
delete(m.sessions, sessionKey)
|
||||
default:
|
||||
sess.mu.Lock()
|
||||
sess.lastActive = time.Now()
|
||||
@ -245,7 +246,7 @@ func (m *PTYSessionManager) GetOrCreate(project, projectDir string) (*PTYSession
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
m.sessions[project] = sess
|
||||
m.sessions[sessionKey] = sess
|
||||
return sess, true, nil
|
||||
}
|
||||
|
||||
|
||||
@ -16,6 +16,7 @@
|
||||
--text-muted: #6c6c80;
|
||||
--accent: #e94560;
|
||||
--accent-hover: #ff6b81;
|
||||
--error: #f44336;
|
||||
}
|
||||
body {
|
||||
background: var(--bg-primary);
|
||||
@ -141,7 +142,99 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
.terminal-header button:hover { border-color: var(--accent); color: var(--text-primary); }
|
||||
#terminal-container { flex: 1; overflow: hidden; }
|
||||
|
||||
/* Tab bar */
|
||||
.tab-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 0 0.25rem;
|
||||
flex-shrink: 0;
|
||||
overflow-x: auto;
|
||||
gap: 2px;
|
||||
min-height: 30px;
|
||||
}
|
||||
.tab-bar::-webkit-scrollbar { height: 0; }
|
||||
.tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.2rem 0.5rem;
|
||||
font-size: 0.7rem;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
border-bottom: none;
|
||||
border-radius: 4px 4px 0 0;
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
transition: color 0.15s, background 0.15s;
|
||||
}
|
||||
.tab:hover { color: var(--text-secondary); background: rgba(255,255,255,0.03); }
|
||||
.tab.active { color: var(--text-primary); background: var(--bg-primary); border-color: var(--border); }
|
||||
.tab-close {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
font-size: 0.65rem;
|
||||
border-radius: 3px;
|
||||
color: var(--text-muted);
|
||||
transition: background 0.1s, color 0.1s;
|
||||
}
|
||||
.tab-close:hover { background: rgba(244,67,54,0.3); color: var(--error); }
|
||||
.tab-add-wrap { position: relative; flex-shrink: 0; }
|
||||
.tab-add {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
background: none;
|
||||
font-family: inherit;
|
||||
transition: color 0.15s, background 0.15s;
|
||||
}
|
||||
.tab-add:hover { color: var(--text-primary); background: rgba(255,255,255,0.05); }
|
||||
.tab-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
min-width: 180px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
z-index: 100;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
|
||||
}
|
||||
.tab-dropdown.hidden { display: none; }
|
||||
.tab-dropdown-item {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 0.4rem 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
font-family: inherit;
|
||||
color: var(--text-secondary);
|
||||
background: none;
|
||||
border: none;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: background 0.1s, color 0.1s;
|
||||
}
|
||||
.tab-dropdown-item:hover { background: rgba(233,69,96,0.15); color: var(--text-primary); }
|
||||
|
||||
/* Terminal containers */
|
||||
#terminals { flex: 1; overflow: hidden; position: relative; }
|
||||
.term-pane { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: none; }
|
||||
.term-pane.active { display: block; }
|
||||
.xterm { height: 100%; }
|
||||
|
||||
/* File viewer overlay */
|
||||
@ -201,6 +294,47 @@
|
||||
.file-viewer-content th { background: var(--bg-secondary); color: var(--accent); font-weight: 600; }
|
||||
.file-viewer-content tr:nth-child(even) { background: rgba(22,27,34,0.5); }
|
||||
|
||||
/* Sidebar project list */
|
||||
.sidebar-projects-list { max-height: 180px; overflow-y: auto; }
|
||||
.sidebar-projects-list .file-item.active {
|
||||
color: var(--accent);
|
||||
background: rgba(233,69,96,0.1);
|
||||
font-weight: 600;
|
||||
}
|
||||
.sidebar-add-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
border-radius: 3px;
|
||||
transition: color 0.15s, background 0.15s;
|
||||
line-height: 1;
|
||||
}
|
||||
.sidebar-add-btn:hover { color: var(--accent); background: rgba(233,69,96,0.15); }
|
||||
|
||||
/* Create project modal */
|
||||
.create-project-modal {
|
||||
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
||||
background: rgba(0,0,0,0.5); display: flex; align-items: center;
|
||||
justify-content: center; z-index: 300;
|
||||
}
|
||||
.create-project-box {
|
||||
background: var(--bg-secondary); border: 1px solid var(--border);
|
||||
border-radius: 8px; padding: 1rem; width: 260px;
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,0.4);
|
||||
}
|
||||
.create-project-box h3 { color: var(--accent); font-size: 0.85rem; margin-bottom: 0.6rem; }
|
||||
.create-project-box input {
|
||||
width: 100%; padding: 0.35rem 0.5rem; background: var(--bg-primary);
|
||||
border: 1px solid var(--border); border-radius: 4px; color: var(--text-primary);
|
||||
font-size: 0.75rem; font-family: inherit; outline: none;
|
||||
}
|
||||
.create-project-box input:focus { border-color: var(--accent); }
|
||||
|
||||
/* Scrollbar */
|
||||
::-webkit-scrollbar { width: 5px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
@ -215,6 +349,20 @@
|
||||
<h3>{{.Project}}</h3>
|
||||
<button onclick="toggleSidebar()" title="Sakrij sidebar">←</button>
|
||||
</div>
|
||||
|
||||
<!-- Projekti -->
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-section-title" style="display:flex;justify-content:space-between;align-items:center;">
|
||||
Projekti
|
||||
<span class="sidebar-add-btn" onclick="showCreateProjectModal()" title="Novi projekat">+</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sidebar-files sidebar-projects-list">
|
||||
{{range .Projects}}
|
||||
<a class="file-item{{if eq .Name $.Project}} active{{end}}" href="/chat/{{.Name}}">{{.Name}}</a>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
{{if .Files}}
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-section-title">Dokumentacija</div>
|
||||
@ -224,18 +372,29 @@
|
||||
<a class="file-item" href="#" onclick="loadFile('{{.RelPath}}'); return false;">{{.Name}}</a>
|
||||
{{end}}
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="sidebar-files" style="padding: 0.75rem; color: var(--text-muted); font-size: 0.75rem;">
|
||||
Nema .md fajlova
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="sidebar-footer">
|
||||
<a href="/projects">Svi projekti</a>
|
||||
<a href="/change-password">Promeni lozinku</a>
|
||||
<a href="/logout">Odjavi se</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal za kreiranje projekta -->
|
||||
<div id="createProjectModal" class="create-project-modal hidden" onclick="if(event.target===this)this.classList.add('hidden')">
|
||||
<div class="create-project-box">
|
||||
<h3>Novi projekat</h3>
|
||||
<form id="createProjectForm" onsubmit="return createProject(event)">
|
||||
<input type="text" id="newProjectName" placeholder="ime-projekta" required>
|
||||
<p style="color:var(--text-muted);font-size:0.65rem;margin:0.3rem 0;">Slova, brojevi, - i _</p>
|
||||
<div style="display:flex;gap:0.4rem;justify-content:flex-end;">
|
||||
<button type="button" onclick="document.getElementById('createProjectModal').classList.add('hidden')" style="background:var(--bg-primary);color:var(--text-secondary);border:1px solid var(--border);border-radius:4px;padding:0.3rem 0.6rem;font-size:0.7rem;cursor:pointer;font-family:inherit;">Otkaži</button>
|
||||
<button type="submit" style="background:var(--accent);color:#fff;border:none;border-radius:4px;padding:0.3rem 0.6rem;font-size:0.7rem;cursor:pointer;font-family:inherit;">Kreiraj</button>
|
||||
</div>
|
||||
</form>
|
||||
<div id="createProjectError" style="color:var(--error);font-size:0.65rem;margin-top:0.3rem;display:none;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main terminal area -->
|
||||
<div class="main">
|
||||
<div class="terminal-header">
|
||||
@ -255,10 +414,16 @@
|
||||
<option value="tokyo-night">Tokyo Night</option>
|
||||
<option value="catppuccin">Catppuccin</option>
|
||||
</select>
|
||||
<a href="/projects" style="color:var(--text-muted);text-decoration:none;font-size:0.7rem;">Projekti</a>
|
||||
</div>
|
||||
</div>
|
||||
<div id="terminal-container"></div>
|
||||
<div class="tab-bar" id="tab-bar">
|
||||
<!-- Tabs rendered by JS -->
|
||||
<div class="tab-add-wrap">
|
||||
<button class="tab-add" id="tab-add-btn" title="Novi tab">+</button>
|
||||
<div class="tab-dropdown hidden" id="tab-dropdown"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="terminals"></div>
|
||||
</div>
|
||||
|
||||
<!-- File viewer overlay -->
|
||||
@ -274,8 +439,12 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-web-links@0.11.0/lib/addon-web-links.min.js"></script>
|
||||
<script>
|
||||
// ── Config from server ──────────────────────────────
|
||||
var PAGE_PROJECT = '{{.Project}}';
|
||||
var PAGE_PROJECT_DIR = '{{.ProjectDir}}';
|
||||
|
||||
// ── Themes ──────────────────────────────────────────
|
||||
const THEMES = {
|
||||
var THEMES = {
|
||||
dark: {
|
||||
background: '#0d1117', foreground: '#e0e0e0', cursor: '#e94560', cursorAccent: '#0d1117',
|
||||
selectionBackground: 'rgba(233,69,96,0.3)',
|
||||
@ -342,111 +511,301 @@
|
||||
}
|
||||
};
|
||||
|
||||
// ── Terminal ────────────────────────────────────────
|
||||
const savedTheme = localStorage.getItem('terminal-theme') || 'dark';
|
||||
// ── Theme ───────────────────────────────────────────
|
||||
var savedTheme = localStorage.getItem('terminal-theme') || 'dark';
|
||||
document.getElementById('theme-select').value = savedTheme;
|
||||
|
||||
const statusEl = document.getElementById('ws-status');
|
||||
const container = document.getElementById('terminal-container');
|
||||
|
||||
const term = new Terminal({
|
||||
cursorBlink: true,
|
||||
cursorStyle: 'block',
|
||||
cursorInactiveStyle: 'outline',
|
||||
fontSize: 14,
|
||||
fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', Consolas, monospace",
|
||||
theme: THEMES[savedTheme] || THEMES.dark,
|
||||
allowProposedApi: true,
|
||||
scrollback: 10000,
|
||||
convertEol: false,
|
||||
drawBoldTextInBrightColors: true,
|
||||
});
|
||||
|
||||
const fitAddon = new FitAddon.FitAddon();
|
||||
term.loadAddon(fitAddon);
|
||||
term.loadAddon(new WebLinksAddon.WebLinksAddon());
|
||||
term.open(container);
|
||||
fitAddon.fit();
|
||||
|
||||
function applyTheme(name) {
|
||||
const theme = THEMES[name];
|
||||
var theme = THEMES[name];
|
||||
if (!theme) return;
|
||||
term.options.theme = theme;
|
||||
savedTheme = name;
|
||||
document.body.style.background = theme.background;
|
||||
document.querySelector('.main').style.background = theme.background;
|
||||
localStorage.setItem('terminal-theme', name);
|
||||
// Apply to all open tabs
|
||||
tabs.forEach(function(tab) {
|
||||
tab.term.options.theme = theme;
|
||||
});
|
||||
}
|
||||
// Apply saved theme to body
|
||||
applyTheme(savedTheme);
|
||||
|
||||
// ── WebSocket ───────────────────────────────────────
|
||||
let ws;
|
||||
let reconnectTimer;
|
||||
// ── Tab system ──────────────────────────────────────
|
||||
var tabs = [];
|
||||
var activeTabId = null;
|
||||
var nextTabId = 1;
|
||||
var statusEl = document.getElementById('ws-status');
|
||||
var tabBarEl = document.getElementById('tab-bar');
|
||||
var terminalsEl = document.getElementById('terminals');
|
||||
var tabAddBtn = document.getElementById('tab-add-btn');
|
||||
var tabDropdown = document.getElementById('tab-dropdown');
|
||||
var projectsCache = null;
|
||||
|
||||
function connect() {
|
||||
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
ws = new WebSocket(proto + '//' + location.host + '/ws?project={{.Project}}&project_dir={{.ProjectDir}}');
|
||||
function createTerminal() {
|
||||
var theme = THEMES[savedTheme] || THEMES.dark;
|
||||
var term = new Terminal({
|
||||
cursorBlink: true,
|
||||
cursorStyle: 'block',
|
||||
cursorInactiveStyle: 'outline',
|
||||
fontSize: 14,
|
||||
fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', Consolas, monospace",
|
||||
theme: theme,
|
||||
allowProposedApi: true,
|
||||
scrollback: 10000,
|
||||
convertEol: false,
|
||||
drawBoldTextInBrightColors: true,
|
||||
});
|
||||
var fitAddon = new FitAddon.FitAddon();
|
||||
term.loadAddon(fitAddon);
|
||||
term.loadAddon(new WebLinksAddon.WebLinksAddon());
|
||||
return { term: term, fitAddon: fitAddon };
|
||||
}
|
||||
|
||||
function connectTab(tab) {
|
||||
var proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
var url = proto + '//' + location.host + '/ws?project=' + encodeURIComponent(tab.project) +
|
||||
'&project_dir=' + encodeURIComponent(tab.projectDir) +
|
||||
'&tab=' + encodeURIComponent(tab.id);
|
||||
var ws = new WebSocket(url);
|
||||
ws.binaryType = 'arraybuffer';
|
||||
tab.ws = ws;
|
||||
|
||||
ws.onopen = function() {
|
||||
statusEl.textContent = 'Povezan';
|
||||
statusEl.className = 'status connected';
|
||||
ws.send(JSON.stringify({type: 'resize', cols: term.cols, rows: term.rows}));
|
||||
term.focus();
|
||||
if (activeTabId === tab.id) {
|
||||
statusEl.textContent = 'Povezan';
|
||||
statusEl.className = 'status connected';
|
||||
}
|
||||
ws.send(JSON.stringify({ type: 'resize', cols: tab.term.cols, rows: tab.term.rows }));
|
||||
if (activeTabId === tab.id) tab.term.focus();
|
||||
};
|
||||
|
||||
ws.onmessage = function(event) {
|
||||
if (event.data instanceof ArrayBuffer) {
|
||||
term.write(new Uint8Array(event.data));
|
||||
tab.term.write(new Uint8Array(event.data));
|
||||
} else {
|
||||
term.write(event.data);
|
||||
tab.term.write(event.data);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = function() {
|
||||
statusEl.textContent = 'Nepovezan';
|
||||
statusEl.className = 'status';
|
||||
clearTimeout(reconnectTimer);
|
||||
reconnectTimer = setTimeout(connect, 2000);
|
||||
if (activeTabId === tab.id) {
|
||||
statusEl.textContent = 'Nepovezan';
|
||||
statusEl.className = 'status';
|
||||
}
|
||||
// Reconnect only if tab still exists
|
||||
if (tabs.find(function(t) { return t.id === tab.id; })) {
|
||||
tab.reconnectTimer = setTimeout(function() { connectTab(tab); }, 2000);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = function() {
|
||||
statusEl.textContent = 'Greška';
|
||||
statusEl.className = 'status';
|
||||
if (activeTabId === tab.id) {
|
||||
statusEl.textContent = 'Greška';
|
||||
statusEl.className = 'status';
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
term.onData(function(data) {
|
||||
if (ws && ws.readyState === WebSocket.OPEN) ws.send(data);
|
||||
});
|
||||
function addTab(project, projectDir) {
|
||||
var tabId = String(nextTabId++);
|
||||
var t = createTerminal();
|
||||
|
||||
term.onResize(function(size) {
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({type: 'resize', cols: size.cols, rows: size.rows}));
|
||||
// Create pane
|
||||
var pane = document.createElement('div');
|
||||
pane.className = 'term-pane';
|
||||
pane.id = 'pane-' + tabId;
|
||||
terminalsEl.appendChild(pane);
|
||||
t.term.open(pane);
|
||||
|
||||
var tab = {
|
||||
id: tabId,
|
||||
project: project,
|
||||
projectDir: projectDir,
|
||||
term: t.term,
|
||||
fitAddon: t.fitAddon,
|
||||
ws: null,
|
||||
reconnectTimer: null,
|
||||
container: pane
|
||||
};
|
||||
|
||||
// Wire input
|
||||
t.term.onData(function(data) {
|
||||
if (tab.ws && tab.ws.readyState === WebSocket.OPEN) tab.ws.send(data);
|
||||
});
|
||||
t.term.onResize(function(size) {
|
||||
if (tab.ws && tab.ws.readyState === WebSocket.OPEN) {
|
||||
tab.ws.send(JSON.stringify({ type: 'resize', cols: size.cols, rows: size.rows }));
|
||||
}
|
||||
});
|
||||
|
||||
// Click on pane gives focus
|
||||
pane.addEventListener('click', function() { tab.term.focus(); });
|
||||
|
||||
tabs.push(tab);
|
||||
renderTabs();
|
||||
switchTab(tabId);
|
||||
connectTab(tab);
|
||||
|
||||
return tab;
|
||||
}
|
||||
|
||||
function switchTab(tabId) {
|
||||
activeTabId = tabId;
|
||||
tabs.forEach(function(tab) {
|
||||
if (tab.id === tabId) {
|
||||
tab.container.classList.add('active');
|
||||
} else {
|
||||
tab.container.classList.remove('active');
|
||||
}
|
||||
});
|
||||
renderTabs();
|
||||
var active = tabs.find(function(t) { return t.id === tabId; });
|
||||
if (active) {
|
||||
setTimeout(function() {
|
||||
active.fitAddon.fit();
|
||||
active.term.focus();
|
||||
}, 50);
|
||||
// Update status
|
||||
if (active.ws && active.ws.readyState === WebSocket.OPEN) {
|
||||
statusEl.textContent = 'Povezan';
|
||||
statusEl.className = 'status connected';
|
||||
} else {
|
||||
statusEl.textContent = 'Nepovezan';
|
||||
statusEl.className = 'status';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function closeTab(tabId) {
|
||||
if (tabs.length <= 1) return; // Don't close last tab
|
||||
var idx = tabs.findIndex(function(t) { return t.id === tabId; });
|
||||
if (idx === -1) return;
|
||||
var tab = tabs[idx];
|
||||
|
||||
// Clean up
|
||||
clearTimeout(tab.reconnectTimer);
|
||||
if (tab.ws) {
|
||||
tab.ws.onclose = null; // Prevent reconnect
|
||||
tab.ws.close();
|
||||
}
|
||||
tab.term.dispose();
|
||||
tab.container.remove();
|
||||
tabs.splice(idx, 1);
|
||||
|
||||
// Switch to neighbor if active was closed
|
||||
if (activeTabId === tabId) {
|
||||
var newIdx = Math.min(idx, tabs.length - 1);
|
||||
switchTab(tabs[newIdx].id);
|
||||
} else {
|
||||
renderTabs();
|
||||
}
|
||||
}
|
||||
|
||||
function renderTabs() {
|
||||
// Remove old tab elements (keep the add button wrapper)
|
||||
var addWrap = document.querySelector('.tab-add-wrap');
|
||||
var oldTabs = tabBarEl.querySelectorAll('.tab');
|
||||
oldTabs.forEach(function(el) { el.remove(); });
|
||||
|
||||
tabs.forEach(function(tab) {
|
||||
var el = document.createElement('div');
|
||||
el.className = 'tab' + (tab.id === activeTabId ? ' active' : '');
|
||||
|
||||
var label = document.createElement('span');
|
||||
label.textContent = tab.project;
|
||||
label.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
switchTab(tab.id);
|
||||
});
|
||||
el.appendChild(label);
|
||||
|
||||
if (tabs.length > 1) {
|
||||
var closeBtn = document.createElement('span');
|
||||
closeBtn.className = 'tab-close';
|
||||
closeBtn.textContent = '\u00d7';
|
||||
closeBtn.title = 'Zatvori tab';
|
||||
closeBtn.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
closeTab(tab.id);
|
||||
});
|
||||
el.appendChild(closeBtn);
|
||||
}
|
||||
|
||||
tabBarEl.insertBefore(el, addWrap);
|
||||
});
|
||||
}
|
||||
|
||||
// ── Add tab dropdown ────────────────────────────────
|
||||
tabAddBtn.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
if (!tabDropdown.classList.contains('hidden')) {
|
||||
tabDropdown.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
// Fetch projects and show dropdown
|
||||
if (projectsCache) {
|
||||
showDropdown(projectsCache);
|
||||
} else {
|
||||
fetch('/api/projects')
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
projectsCache = data;
|
||||
showDropdown(data);
|
||||
})
|
||||
.catch(function() {
|
||||
// Fallback: just add tab for current project
|
||||
addTab(PAGE_PROJECT, PAGE_PROJECT_DIR);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('resize', function() { fitAddon.fit(); });
|
||||
function showDropdown(projects) {
|
||||
tabDropdown.innerHTML = '';
|
||||
projects.forEach(function(p) {
|
||||
var btn = document.createElement('button');
|
||||
btn.className = 'tab-dropdown-item';
|
||||
btn.textContent = p.name;
|
||||
btn.addEventListener('click', function() {
|
||||
tabDropdown.classList.add('hidden');
|
||||
addTab(p.name, p.path);
|
||||
});
|
||||
tabDropdown.appendChild(btn);
|
||||
});
|
||||
tabDropdown.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Click on terminal area gives focus
|
||||
container.addEventListener('click', function() { term.focus(); });
|
||||
// Close dropdown when clicking outside
|
||||
document.addEventListener('click', function() {
|
||||
tabDropdown.classList.add('hidden');
|
||||
});
|
||||
|
||||
connect();
|
||||
// ── Window resize ───────────────────────────────────
|
||||
window.addEventListener('resize', function() {
|
||||
var active = tabs.find(function(t) { return t.id === activeTabId; });
|
||||
if (active) active.fitAddon.fit();
|
||||
});
|
||||
|
||||
// ── Apply initial theme ─────────────────────────────
|
||||
applyTheme(savedTheme);
|
||||
|
||||
// ── Create first tab ────────────────────────────────
|
||||
addTab(PAGE_PROJECT, PAGE_PROJECT_DIR);
|
||||
|
||||
// ── Sidebar ─────────────────────────────────────────
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const sidebarState = localStorage.getItem('sidebar-visible');
|
||||
var sidebar = document.getElementById('sidebar');
|
||||
var sidebarState = localStorage.getItem('sidebar-visible');
|
||||
if (sidebarState === 'false') sidebar.classList.add('collapsed');
|
||||
|
||||
function toggleSidebar() {
|
||||
sidebar.classList.toggle('collapsed');
|
||||
localStorage.setItem('sidebar-visible', !sidebar.classList.contains('collapsed'));
|
||||
setTimeout(function() { fitAddon.fit(); }, 200);
|
||||
setTimeout(function() {
|
||||
var active = tabs.find(function(t) { return t.id === activeTabId; });
|
||||
if (active) active.fitAddon.fit();
|
||||
}, 200);
|
||||
}
|
||||
|
||||
// ── File viewer ─────────────────────────────────────
|
||||
function loadFile(relPath) {
|
||||
fetch('/api/file?project={{.Project}}&path=' + encodeURIComponent(relPath))
|
||||
fetch('/api/file?project=' + encodeURIComponent(PAGE_PROJECT) + '&path=' + encodeURIComponent(relPath))
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
document.getElementById('file-viewer-title').textContent = data.name;
|
||||
@ -458,12 +817,56 @@
|
||||
|
||||
function closeFileViewer() {
|
||||
document.getElementById('file-viewer').classList.add('hidden');
|
||||
term.focus();
|
||||
var active = tabs.find(function(t) { return t.id === activeTabId; });
|
||||
if (active) active.term.focus();
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') closeFileViewer();
|
||||
if (e.key === 'Escape') {
|
||||
closeFileViewer();
|
||||
document.getElementById('createProjectModal').classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
// ── Create project ───────────────────────────────────
|
||||
function showCreateProjectModal() {
|
||||
document.getElementById('createProjectModal').classList.remove('hidden');
|
||||
setTimeout(function() {
|
||||
document.getElementById('newProjectName').focus();
|
||||
}, 50);
|
||||
}
|
||||
|
||||
function createProject(e) {
|
||||
e.preventDefault();
|
||||
var name = document.getElementById('newProjectName').value.trim();
|
||||
var errEl = document.getElementById('createProjectError');
|
||||
if (!name) return false;
|
||||
|
||||
var form = new FormData();
|
||||
form.append('name', name);
|
||||
|
||||
fetch('/projects/create', {
|
||||
method: 'POST',
|
||||
body: form,
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||
})
|
||||
.then(function(r) {
|
||||
return r.json().then(function(data) {
|
||||
if (r.ok) {
|
||||
projectsCache = null; // Invalidate cache
|
||||
window.location.href = '/chat/' + encodeURIComponent(name);
|
||||
} else {
|
||||
errEl.textContent = data.error || 'Greška pri kreiranju';
|
||||
errEl.style.display = 'block';
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(function() {
|
||||
errEl.textContent = 'Greška pri kreiranju';
|
||||
errEl.style.display = 'block';
|
||||
});
|
||||
return false;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
<div class="projects-header">
|
||||
<h1>Projekti</h1>
|
||||
<div>
|
||||
<button class="btn btn-secondary" onclick="document.getElementById('createModal').classList.remove('hidden')">Novi projekat</button>
|
||||
<button class="btn btn-secondary" onclick="showCreateModal()">Novi projekat</button>
|
||||
<a href="/change-password" class="btn btn-secondary">Promeni lozinku</a>
|
||||
<a href="/logout" class="btn">Odjavi se</a>
|
||||
</div>
|
||||
@ -27,7 +27,7 @@
|
||||
<form method="POST" action="/projects/create">
|
||||
<div class="form-group">
|
||||
<label for="projectName">Ime projekta</label>
|
||||
<input type="text" id="projectName" name="name" placeholder="moj-projekat" required autofocus>
|
||||
<input type="text" id="projectName" name="name" placeholder="moj-projekat" required>
|
||||
</div>
|
||||
<p class="modal-hint">Dozvoljeni karakteri: slova, brojevi, - i _</p>
|
||||
<div class="modal-actions">
|
||||
@ -54,5 +54,15 @@
|
||||
<p style="color: var(--text-secondary);">Nema projekata u {{.ProjectsPath}}</p>
|
||||
{{end}}
|
||||
</div>
|
||||
<script>
|
||||
function showCreateModal() {
|
||||
var modal = document.getElementById('createModal');
|
||||
modal.classList.remove('hidden');
|
||||
// Focus input after modal is visible
|
||||
setTimeout(function() {
|
||||
document.getElementById('projectName').focus();
|
||||
}, 50);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user