1936 lines
75 KiB
HTML
1936 lines
75 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ro">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<link rel="icon" type="image/svg+xml" href="/echo/favicon.svg">
|
|
<title>Echo · Files</title>
|
|
<link rel="stylesheet" href="/echo/common.css">
|
|
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>
|
|
<script src="/echo/swipe-nav.js"></script>
|
|
<style>
|
|
.main {
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: calc(100vh - 52px);
|
|
}
|
|
|
|
/* Toolbar */
|
|
.toolbar {
|
|
padding: var(--space-3) var(--space-5);
|
|
background: var(--bg-surface);
|
|
border-bottom: 1px solid var(--border);
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
gap: var(--space-4);
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.breadcrumb {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-1);
|
|
font-size: var(--text-sm);
|
|
color: var(--text-muted);
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.breadcrumb-item {
|
|
color: var(--text-muted);
|
|
text-decoration: none;
|
|
padding: var(--space-1) var(--space-2);
|
|
border-radius: var(--radius-sm);
|
|
transition: all var(--transition-fast);
|
|
cursor: pointer;
|
|
}
|
|
|
|
.breadcrumb-item:hover {
|
|
color: var(--text-primary);
|
|
background: var(--bg-surface-hover);
|
|
}
|
|
|
|
.breadcrumb-item.current {
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.breadcrumb-sep {
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.breadcrumb-sep svg {
|
|
width: 14px;
|
|
height: 14px;
|
|
}
|
|
|
|
.toolbar-actions {
|
|
display: flex;
|
|
gap: var(--space-2);
|
|
}
|
|
|
|
/* View modes */
|
|
.view-toggle {
|
|
display: flex;
|
|
gap: 2px;
|
|
background: var(--bg-base);
|
|
border-radius: var(--radius-md);
|
|
padding: 2px;
|
|
}
|
|
|
|
.view-btn {
|
|
padding: var(--space-2);
|
|
background: transparent;
|
|
border: none;
|
|
color: var(--text-muted);
|
|
cursor: pointer;
|
|
border-radius: var(--radius-sm);
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.view-btn:hover {
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.view-btn.active {
|
|
background: var(--accent);
|
|
color: white;
|
|
}
|
|
|
|
.view-btn svg {
|
|
width: 16px;
|
|
height: 16px;
|
|
}
|
|
|
|
.sort-select {
|
|
background: transparent;
|
|
border: none;
|
|
color: var(--text-secondary);
|
|
font-size: var(--text-sm);
|
|
padding: var(--space-1) var(--space-2);
|
|
cursor: pointer;
|
|
outline: none;
|
|
}
|
|
|
|
.sort-select option {
|
|
background: var(--bg-base);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
/* ========== LIST VIEW - Windows Explorer style ========== */
|
|
.file-grid.view-list {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
|
gap: var(--space-1);
|
|
}
|
|
|
|
.file-grid.view-list .file-item {
|
|
flex-direction: row;
|
|
padding: var(--space-1) var(--space-2);
|
|
gap: var(--space-2);
|
|
background: transparent;
|
|
border: none;
|
|
}
|
|
|
|
.file-grid.view-list .file-item:hover {
|
|
background: var(--bg-surface-hover);
|
|
}
|
|
|
|
.file-grid.view-list .file-icon {
|
|
width: 16px;
|
|
height: 16px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.file-grid.view-list .file-icon svg {
|
|
width: 16px;
|
|
height: 16px;
|
|
}
|
|
|
|
.file-grid.view-list .file-name {
|
|
font-size: var(--text-sm);
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.file-grid.view-list .file-meta {
|
|
display: none;
|
|
}
|
|
|
|
/* ========== DETAILS VIEW - Windows Explorer style with columns ========== */
|
|
.file-grid.view-details {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0;
|
|
}
|
|
|
|
.file-grid.view-details .file-header {
|
|
display: grid;
|
|
grid-template-columns: 24px 1fr 100px 80px 120px;
|
|
align-items: center;
|
|
padding: var(--space-2) var(--space-3);
|
|
gap: var(--space-3);
|
|
background: var(--bg-surface);
|
|
border-bottom: 2px solid var(--border);
|
|
font-size: var(--text-xs);
|
|
font-weight: 600;
|
|
color: var(--text-muted);
|
|
position: sticky;
|
|
top: 0;
|
|
}
|
|
|
|
.file-grid.view-details .file-header span {
|
|
cursor: pointer;
|
|
}
|
|
|
|
.file-grid.view-details .file-header span:hover {
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.file-grid.view-details .file-item {
|
|
display: grid;
|
|
grid-template-columns: 24px 1fr 100px 80px 120px;
|
|
align-items: center;
|
|
padding: var(--space-2) var(--space-3);
|
|
gap: var(--space-3);
|
|
background: transparent;
|
|
border: none;
|
|
border-bottom: 1px solid var(--border);
|
|
border-radius: 0;
|
|
}
|
|
|
|
.file-grid.view-details .file-item:hover {
|
|
background: var(--bg-surface-hover);
|
|
}
|
|
|
|
.file-grid.view-details .file-icon {
|
|
width: 18px;
|
|
height: 18px;
|
|
}
|
|
|
|
.file-grid.view-details .file-icon svg {
|
|
width: 18px;
|
|
height: 18px;
|
|
}
|
|
|
|
.file-grid.view-details .file-name {
|
|
font-size: var(--text-sm);
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
text-align: left;
|
|
}
|
|
|
|
.file-grid.view-details .file-meta {
|
|
display: contents;
|
|
}
|
|
|
|
.file-grid.view-details .file-type,
|
|
.file-grid.view-details .file-size,
|
|
.file-grid.view-details .file-date {
|
|
font-size: var(--text-xs);
|
|
color: var(--text-muted);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.file-grid.view-details .file-type {
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
/* ========== TILES VIEW - Original grid style ========== */
|
|
.file-grid.view-tiles {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
|
gap: var(--space-3);
|
|
}
|
|
|
|
/* Content area */
|
|
.content-area {
|
|
flex: 1;
|
|
display: flex;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* Browse panel */
|
|
.browse-panel {
|
|
flex: 1;
|
|
overflow: auto;
|
|
padding: var(--space-5);
|
|
}
|
|
|
|
.browse-panel.hidden {
|
|
display: none;
|
|
}
|
|
|
|
.file-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
|
gap: var(--space-3);
|
|
}
|
|
|
|
.file-item {
|
|
background: var(--bg-surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-md);
|
|
padding: var(--space-4);
|
|
cursor: pointer;
|
|
transition: all var(--transition-fast);
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
text-align: center;
|
|
gap: var(--space-2);
|
|
}
|
|
|
|
.file-item:hover {
|
|
border-color: var(--accent);
|
|
transform: translateY(-2px);
|
|
box-shadow: var(--shadow-sm);
|
|
}
|
|
|
|
.file-item.active {
|
|
border-color: var(--accent);
|
|
background: var(--accent-subtle);
|
|
}
|
|
|
|
.file-item.git-changed {
|
|
border-left: 3px solid var(--warning);
|
|
}
|
|
|
|
.git-badge {
|
|
display: inline-block;
|
|
font-size: 10px;
|
|
font-weight: 700;
|
|
padding: 1px 4px;
|
|
border-radius: 3px;
|
|
margin-right: 4px;
|
|
font-family: var(--font-mono);
|
|
}
|
|
|
|
.git-modified { background: #3b82f620; color: #3b82f6; }
|
|
.git-added { background: #22c55e20; color: #22c55e; }
|
|
.git-deleted { background: #ef444420; color: #ef4444; }
|
|
.git-untracked { background: #f59e0b20; color: #f59e0b; }
|
|
.git-renamed { background: #8b5cf620; color: #8b5cf6; }
|
|
|
|
.diff-btn {
|
|
background: var(--accent);
|
|
color: white;
|
|
border: none;
|
|
padding: 2px 8px;
|
|
border-radius: 4px;
|
|
font-size: 11px;
|
|
cursor: pointer;
|
|
font-family: var(--font-mono);
|
|
}
|
|
|
|
.diff-btn:hover {
|
|
opacity: 0.8;
|
|
}
|
|
|
|
.file-icon {
|
|
width: 40px;
|
|
height: 40px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.file-icon svg {
|
|
width: 32px;
|
|
height: 32px;
|
|
}
|
|
|
|
.file-icon.folder svg {
|
|
color: var(--accent);
|
|
}
|
|
|
|
.file-name {
|
|
font-size: var(--text-sm);
|
|
color: var(--text-primary);
|
|
word-break: break-word;
|
|
line-height: 1.3;
|
|
}
|
|
|
|
.file-size {
|
|
font-size: var(--text-xs);
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
/* Editor panel */
|
|
.editor-panel {
|
|
flex: 1;
|
|
display: none;
|
|
flex-direction: column;
|
|
background: var(--bg-base);
|
|
}
|
|
|
|
.editor-panel.active {
|
|
display: flex;
|
|
}
|
|
|
|
.editor-header {
|
|
padding: var(--space-3) var(--space-5);
|
|
background: var(--bg-surface);
|
|
border-bottom: 1px solid var(--border);
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.editor-title {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-2);
|
|
color: var(--text-primary);
|
|
font-size: var(--text-sm);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.editor-title svg {
|
|
width: 16px;
|
|
height: 16px;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.editor-actions {
|
|
display: flex;
|
|
gap: var(--space-2);
|
|
}
|
|
|
|
.editor-body {
|
|
flex: 1;
|
|
overflow: hidden;
|
|
}
|
|
|
|
#codeEditor {
|
|
width: 100%;
|
|
height: 100%;
|
|
background: transparent;
|
|
color: var(--text-secondary);
|
|
border: none;
|
|
padding: var(--space-5);
|
|
font-family: var(--font-mono);
|
|
font-size: 14px;
|
|
line-height: 1.6;
|
|
resize: none;
|
|
outline: none;
|
|
word-wrap: break-word;
|
|
overflow-wrap: break-word;
|
|
white-space: pre-wrap;
|
|
}
|
|
|
|
#markdownPreview {
|
|
display: none;
|
|
width: 100%;
|
|
height: 100%;
|
|
padding: var(--space-5);
|
|
overflow-y: auto;
|
|
overflow-x: auto;
|
|
color: var(--text-secondary);
|
|
line-height: 1.7;
|
|
word-wrap: break-word;
|
|
overflow-wrap: break-word;
|
|
}
|
|
|
|
#markdownPreview h1, #markdownPreview h2, #markdownPreview h3 {
|
|
color: var(--text-primary);
|
|
margin-top: 1.5em;
|
|
margin-bottom: 0.5em;
|
|
}
|
|
|
|
#markdownPreview h1 { font-size: 1.8em; border-bottom: 1px solid var(--border); padding-bottom: 0.3em; }
|
|
#markdownPreview h2 { font-size: 1.4em; }
|
|
#markdownPreview h3 { font-size: 1.2em; }
|
|
|
|
#markdownPreview p { margin-bottom: 1em; }
|
|
#markdownPreview ul, #markdownPreview ol { margin-bottom: 1em; padding-left: 2em; }
|
|
#markdownPreview li { margin-bottom: 0.3em; }
|
|
#markdownPreview strong { color: var(--text-primary); }
|
|
#markdownPreview hr { border: none; border-top: 1px solid var(--border); margin: 2em 0; }
|
|
#markdownPreview code { background: var(--bg-surface); padding: 2px 6px; border-radius: 4px; font-family: var(--font-mono); }
|
|
#markdownPreview pre { background: var(--bg-surface); padding: 1em; border-radius: 8px; overflow-x: auto; }
|
|
#markdownPreview blockquote { border-left: 3px solid var(--accent); padding-left: 1em; margin-left: 0; color: var(--text-muted); }
|
|
#markdownPreview a, #markdownPreview .file-link { color: var(--accent); text-decoration: none; }
|
|
#markdownPreview a:hover, #markdownPreview .file-link:hover { text-decoration: underline; }
|
|
|
|
.preview-active #codeEditor { display: none; }
|
|
.preview-active #markdownPreview { display: block; }
|
|
|
|
.btn-preview.active { background: var(--accent); color: white; }
|
|
.btn-diff.active { background: var(--warning); color: white; }
|
|
|
|
#gitFilterBtn.active { background: var(--warning); color: white; }
|
|
|
|
.editor-footer {
|
|
padding: var(--space-2) var(--space-5);
|
|
background: var(--bg-surface);
|
|
border-top: 1px solid var(--border);
|
|
display: flex;
|
|
justify-content: space-between;
|
|
font-size: var(--text-xs);
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.status-saved { color: var(--success); }
|
|
.status-modified { color: var(--warning); }
|
|
.status-error { color: var(--error); }
|
|
|
|
/* Empty state */
|
|
.empty-state {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
height: 100%;
|
|
color: var(--text-muted);
|
|
gap: var(--space-3);
|
|
}
|
|
|
|
.empty-state svg {
|
|
width: 48px;
|
|
height: 48px;
|
|
opacity: 0.5;
|
|
}
|
|
|
|
/* Hide view/sort controls in editor mode (both mobile and desktop) */
|
|
body.editor-mode #viewModeToggle,
|
|
body.editor-mode #sortBy,
|
|
body.editor-mode #sortDirBtn {
|
|
display: none !important;
|
|
}
|
|
|
|
/* Editor menu for mobile */
|
|
.editor-menu-mobile {
|
|
position: relative;
|
|
display: none; /* Hidden by default, shown on mobile via media query */
|
|
}
|
|
|
|
.editor-menu-dropdown {
|
|
position: absolute;
|
|
top: 100%;
|
|
right: 0;
|
|
background: var(--bg-surface);
|
|
backdrop-filter: blur(8px);
|
|
-webkit-backdrop-filter: blur(8px);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-md);
|
|
min-width: 180px;
|
|
z-index: 100;
|
|
box-shadow: var(--shadow-md);
|
|
margin-top: 4px;
|
|
}
|
|
|
|
/* Ensure opaque background on both themes */
|
|
[data-theme="dark"] .editor-menu-dropdown {
|
|
background: #1a1a1aee;
|
|
}
|
|
|
|
[data-theme="light"] .editor-menu-dropdown {
|
|
background: #ffffffee;
|
|
}
|
|
|
|
.menu-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-2);
|
|
width: 100%;
|
|
padding: var(--space-2) var(--space-3);
|
|
background: transparent;
|
|
border: none;
|
|
color: var(--text-secondary);
|
|
cursor: pointer;
|
|
font-size: var(--text-sm);
|
|
text-align: left;
|
|
transition: all var(--transition-fast);
|
|
}
|
|
|
|
.menu-item:hover {
|
|
background: var(--bg-surface-hover);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.menu-item svg {
|
|
width: 16px;
|
|
height: 16px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.menu-item:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.menu-item.hidden {
|
|
display: none;
|
|
}
|
|
|
|
@media (max-width: 1200px) {
|
|
/* Prevent horizontal overflow */
|
|
.main,
|
|
.content-area,
|
|
.browse-panel,
|
|
.editor-panel {
|
|
overflow-x: hidden;
|
|
}
|
|
|
|
.toolbar {
|
|
padding: var(--space-2) var(--space-3);
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
/* Compact breadcrumb on mobile */
|
|
.breadcrumb {
|
|
font-size: 12px;
|
|
max-width: 100%;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.breadcrumb-item {
|
|
padding: var(--space-1);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* Make toolbar actions wrap and stay visible */
|
|
.toolbar-actions {
|
|
flex-wrap: wrap;
|
|
gap: var(--space-1);
|
|
}
|
|
|
|
/* Collapse view/sort controls into dropdown on mobile */
|
|
.view-sort-group {
|
|
position: relative;
|
|
display: flex;
|
|
}
|
|
|
|
#viewModeToggle,
|
|
.sort-select,
|
|
#sortDirBtn {
|
|
display: none !important;
|
|
}
|
|
|
|
.view-sort-dropdown-toggle {
|
|
display: flex !important;
|
|
}
|
|
|
|
/* Hide when in editor mode */
|
|
body.editor-mode .view-sort-dropdown-toggle {
|
|
display: none !important;
|
|
}
|
|
|
|
.view-sort-dropdown {
|
|
position: fixed;
|
|
top: 60px;
|
|
left: var(--space-3);
|
|
right: var(--space-3);
|
|
max-width: 280px;
|
|
background: #1a1a1aee;
|
|
backdrop-filter: blur(8px);
|
|
-webkit-backdrop-filter: blur(8px);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-md);
|
|
z-index: 100;
|
|
box-shadow: var(--shadow-md);
|
|
padding: var(--space-2);
|
|
}
|
|
|
|
[data-theme="light"] .view-sort-dropdown {
|
|
background: #ffffffee;
|
|
}
|
|
|
|
.view-sort-section {
|
|
padding: var(--space-2) 0;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
.view-sort-section:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.view-sort-section-title {
|
|
font-size: var(--text-xs);
|
|
color: var(--text-muted);
|
|
font-weight: 600;
|
|
margin-bottom: var(--space-2);
|
|
padding: 0 var(--space-2);
|
|
}
|
|
|
|
.view-sort-options {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
}
|
|
|
|
.view-sort-option {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-2);
|
|
padding: var(--space-2) var(--space-3);
|
|
background: transparent;
|
|
border: none;
|
|
color: var(--text-secondary);
|
|
cursor: pointer;
|
|
font-size: var(--text-sm);
|
|
text-align: left;
|
|
border-radius: var(--radius-sm);
|
|
transition: all var(--transition-fast);
|
|
width: 100%;
|
|
}
|
|
|
|
.view-sort-option:hover {
|
|
background: var(--bg-surface-hover);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.view-sort-option.active {
|
|
background: var(--accent-subtle);
|
|
color: var(--accent);
|
|
}
|
|
|
|
.view-sort-option svg {
|
|
width: 16px;
|
|
height: 16px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
/* Simplify details view for mobile - use single column layout */
|
|
.file-grid.view-details .file-header {
|
|
display: none; /* Hide header on mobile */
|
|
}
|
|
|
|
.file-grid.view-details .file-item {
|
|
display: grid;
|
|
grid-template-columns: 24px 1fr auto;
|
|
grid-template-rows: auto auto;
|
|
gap: var(--space-2);
|
|
padding: var(--space-2) var(--space-3);
|
|
}
|
|
|
|
.file-grid.view-details .file-icon {
|
|
grid-row: 1 / 3;
|
|
}
|
|
|
|
.file-grid.view-details .file-name {
|
|
grid-column: 2;
|
|
grid-row: 1;
|
|
font-size: var(--text-sm);
|
|
}
|
|
|
|
.file-grid.view-details .file-meta {
|
|
grid-column: 2 / 4;
|
|
grid-row: 2;
|
|
display: flex;
|
|
gap: var(--space-2);
|
|
font-size: var(--text-xs);
|
|
}
|
|
|
|
.file-grid.view-details .file-type,
|
|
.file-grid.view-details .file-size,
|
|
.file-grid.view-details .file-date {
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* Reduce grid columns for tiles/list */
|
|
.file-grid.view-tiles {
|
|
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
|
gap: var(--space-2);
|
|
}
|
|
|
|
.file-grid.view-list {
|
|
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
|
}
|
|
|
|
/* Hide individual buttons on mobile - available in hamburger menu */
|
|
.editor-header #previewBtn,
|
|
.editor-header #downloadPdfBtn,
|
|
.editor-header #diffBtn,
|
|
.editor-header #reloadBtn {
|
|
display: none !important;
|
|
}
|
|
|
|
#saveBtn {
|
|
display: flex !important;
|
|
}
|
|
|
|
/* Hamburger menu ALWAYS visible on mobile */
|
|
.editor-menu-mobile {
|
|
display: flex !important;
|
|
position: relative;
|
|
}
|
|
|
|
.editor-actions {
|
|
gap: var(--space-1);
|
|
flex-wrap: nowrap; /* Keep buttons in one line */
|
|
}
|
|
|
|
/* Fix button sizes on mobile - prevent scaling */
|
|
.editor-actions .btn {
|
|
padding: var(--space-2) !important;
|
|
min-width: auto !important;
|
|
min-height: auto !important;
|
|
font-size: 14px !important;
|
|
}
|
|
|
|
.editor-actions .btn svg {
|
|
width: 16px !important;
|
|
height: 16px !important;
|
|
}
|
|
|
|
/* Compact editor header */
|
|
.editor-header {
|
|
padding: var(--space-2) var(--space-3);
|
|
}
|
|
|
|
.editor-title {
|
|
font-size: 13px;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
max-width: 200px;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
/* Override common.css mobile touch target sizes for editor buttons */
|
|
.editor-actions .btn {
|
|
min-height: auto !important;
|
|
padding: var(--space-2) !important;
|
|
}
|
|
|
|
.editor-header {
|
|
padding: var(--space-2) !important;
|
|
}
|
|
}
|
|
|
|
@media (min-width: 1201px) {
|
|
/* Hide hamburger menu on desktop */
|
|
.editor-menu-mobile {
|
|
display: none !important;
|
|
}
|
|
|
|
/* Hide view/sort dropdown toggle on desktop */
|
|
.view-sort-dropdown-toggle {
|
|
display: none !important;
|
|
}
|
|
|
|
/* Re-enable desktop buttons (override mobile hide) */
|
|
#previewBtn,
|
|
#downloadPdfBtn,
|
|
#diffBtn,
|
|
#reloadBtn {
|
|
display: none; /* Default hidden, JS will show when needed */
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<header class="header">
|
|
<a href="/echo/index.html" class="logo">
|
|
<i data-lucide="circle-dot"></i>
|
|
Echo
|
|
</a>
|
|
<nav class="nav">
|
|
<a href="/echo/index.html" class="nav-item">
|
|
<i data-lucide="layout-dashboard"></i>
|
|
<span>Dashboard</span>
|
|
</a>
|
|
<a href="/echo/workspace.html" class="nav-item">
|
|
<i data-lucide="code"></i>
|
|
<span>Workspace</span>
|
|
</a>
|
|
<a href="/echo/notes.html" class="nav-item">
|
|
<i data-lucide="file-text"></i>
|
|
<span>KB</span>
|
|
</a>
|
|
<a href="/echo/habits.html" class="nav-item">
|
|
<i data-lucide="dumbbell"></i>
|
|
<span>Habits</span>
|
|
</a>
|
|
<a href="/echo/files.html" class="nav-item active">
|
|
<i data-lucide="folder"></i>
|
|
<span>Files</span>
|
|
</a>
|
|
<a href="/echo/eco.html" class="nav-item">
|
|
<i data-lucide="cpu"></i>
|
|
<span>Eco</span>
|
|
</a>
|
|
<button class="theme-toggle" onclick="toggleTheme()" title="Schimbă tema">
|
|
<i data-lucide="sun" id="themeIcon"></i>
|
|
</button>
|
|
</nav>
|
|
</header>
|
|
|
|
<main class="main">
|
|
<div class="toolbar">
|
|
<div class="breadcrumb" id="breadcrumb">
|
|
<span class="breadcrumb-item current" onclick="loadPath('')">~/clawd</span>
|
|
</div>
|
|
<div class="toolbar-actions">
|
|
<!-- Git Filter Toggle -->
|
|
<div class="view-toggle">
|
|
<button class="view-btn" id="gitFilterBtn" onclick="toggleGitFilter()" title="Git Changes">
|
|
<i data-lucide="git-branch"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- View/Sort Controls Group -->
|
|
<div class="view-sort-group">
|
|
<!-- Mobile: Collapsed dropdown toggle -->
|
|
<div class="view-toggle">
|
|
<button class="view-btn view-sort-dropdown-toggle" onclick="toggleViewSortMenu()" title="View & Sort" style="display:none;">
|
|
<i data-lucide="settings-2"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Dropdown menu (mobile only) -->
|
|
<div class="view-sort-dropdown" id="viewSortDropdown" style="display:none;">
|
|
<div class="view-sort-section">
|
|
<div class="view-sort-section-title">Mod vizualizare</div>
|
|
<div class="view-sort-options">
|
|
<button class="view-sort-option" data-view="list" onclick="setViewMode('list'); toggleViewSortMenu()">
|
|
<i data-lucide="list"></i>
|
|
<span>Listă</span>
|
|
</button>
|
|
<button class="view-sort-option" data-view="details" onclick="setViewMode('details'); toggleViewSortMenu()">
|
|
<i data-lucide="layout-list"></i>
|
|
<span>Detalii</span>
|
|
</button>
|
|
<button class="view-sort-option active" data-view="tiles" onclick="setViewMode('tiles'); toggleViewSortMenu()">
|
|
<i data-lucide="layout-grid"></i>
|
|
<span>Tiles</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="view-sort-section">
|
|
<div class="view-sort-section-title">Sortare</div>
|
|
<div class="view-sort-options">
|
|
<button class="view-sort-option active" data-sort="name" onclick="setSortByMobile('name'); toggleViewSortMenu()">
|
|
<i data-lucide="text"></i>
|
|
<span>Nume</span>
|
|
</button>
|
|
<button class="view-sort-option" data-sort="type" onclick="setSortByMobile('type'); toggleViewSortMenu()">
|
|
<i data-lucide="file-type"></i>
|
|
<span>Tip</span>
|
|
</button>
|
|
<button class="view-sort-option" data-sort="size" onclick="setSortByMobile('size'); toggleViewSortMenu()">
|
|
<i data-lucide="hard-drive"></i>
|
|
<span>Mărime</span>
|
|
</button>
|
|
<button class="view-sort-option" data-sort="date" onclick="setSortByMobile('date'); toggleViewSortMenu()">
|
|
<i data-lucide="calendar"></i>
|
|
<span>Dată</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="view-sort-section">
|
|
<div class="view-sort-section-title">Ordine</div>
|
|
<div class="view-sort-options">
|
|
<button class="view-sort-option active" data-dir="asc" onclick="setSortDirMobile('asc'); toggleViewSortMenu()">
|
|
<i data-lucide="arrow-down-a-z"></i>
|
|
<span>Crescător</span>
|
|
</button>
|
|
<button class="view-sort-option" data-dir="desc" onclick="setSortDirMobile('desc'); toggleViewSortMenu()">
|
|
<i data-lucide="arrow-up-z-a"></i>
|
|
<span>Descrescător</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Desktop: Original controls -->
|
|
<div class="view-toggle" id="viewModeToggle">
|
|
<button class="view-btn" data-view="list" onclick="setViewMode('list')" title="Listă">
|
|
<i data-lucide="list"></i>
|
|
</button>
|
|
<button class="view-btn" data-view="details" onclick="setViewMode('details')" title="Detalii">
|
|
<i data-lucide="layout-list"></i>
|
|
</button>
|
|
<button class="view-btn active" data-view="tiles" onclick="setViewMode('tiles')" title="Tiles">
|
|
<i data-lucide="layout-grid"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Sort Toggle (desktop only) -->
|
|
<div class="view-toggle">
|
|
<select class="sort-select" id="sortBy" onchange="sortFiles()">
|
|
<option value="name">Nume</option>
|
|
<option value="type">Tip</option>
|
|
<option value="size">Mărime</option>
|
|
<option value="date">Dată</option>
|
|
</select>
|
|
<button class="view-btn" id="sortDirBtn" onclick="toggleSortDir()" title="Ordine">
|
|
<i data-lucide="arrow-down-a-z" id="sortDirIcon"></i>
|
|
</button>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<div class="content-area">
|
|
<div class="browse-panel" id="browsePanel">
|
|
<div class="file-grid" id="fileGrid">
|
|
<div class="empty-state">
|
|
<i data-lucide="loader"></i>
|
|
<p>Se încarcă...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="editor-panel" id="editorPanel">
|
|
<div class="editor-header">
|
|
<div class="editor-title">
|
|
<i data-lucide="file"></i>
|
|
<span id="editorFileName">Niciun fișier</span>
|
|
</div>
|
|
<div class="editor-actions">
|
|
<button class="btn btn-ghost" onclick="showBrowse()" title="Înapoi">
|
|
<i data-lucide="arrow-left"></i>
|
|
</button>
|
|
<button class="btn btn-ghost btn-preview" onclick="togglePreview()" id="previewBtn" style="display:none;" title="Preview Markdown">
|
|
<i data-lucide="eye"></i>
|
|
</button>
|
|
<button class="btn btn-ghost" onclick="downloadPDF()" id="downloadPdfBtn" style="display:none;" title="Download as PDF">
|
|
<i data-lucide="download"></i>
|
|
</button>
|
|
<button class="btn btn-ghost btn-diff" onclick="toggleDiff()" id="diffBtn" style="display:none;" title="Git Diff">
|
|
<i data-lucide="git-compare"></i>
|
|
</button>
|
|
<!-- Hamburger menu for mobile -->
|
|
<div class="editor-menu-mobile" id="editorMenuMobile">
|
|
<button class="btn btn-ghost" onclick="toggleEditorMenu()" title="More">
|
|
<i data-lucide="more-vertical"></i>
|
|
</button>
|
|
<div class="editor-menu-dropdown" id="editorMenuDropdown" style="display:none;">
|
|
<button onclick="togglePreview(); toggleEditorMenu()" class="menu-item" id="previewMenuItem">
|
|
<i data-lucide="eye"></i>
|
|
<span id="previewLabel">Preview</span>
|
|
</button>
|
|
<button onclick="downloadPDF(); toggleEditorMenu()" class="menu-item" id="downloadPdfMenuItem">
|
|
<i data-lucide="download"></i>
|
|
<span>Download PDF</span>
|
|
</button>
|
|
<button onclick="toggleDiff(); toggleEditorMenu()" class="menu-item" id="diffMenuItem">
|
|
<i data-lucide="git-compare"></i>
|
|
<span>Git Diff</span>
|
|
</button>
|
|
<button onclick="reloadFile(); toggleEditorMenu()" class="menu-item">
|
|
<i data-lucide="refresh-cw"></i>
|
|
<span>Reload</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<!-- Desktop buttons -->
|
|
<button class="btn btn-ghost" onclick="reloadFile()" id="reloadBtn" disabled title="Reload" style="display:flex;">
|
|
<i data-lucide="refresh-cw"></i>
|
|
</button>
|
|
<button class="btn btn-primary" onclick="saveFile()" id="saveBtn" disabled style="display:flex;" title="Save">
|
|
<i data-lucide="save"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="editor-body" id="editorBody">
|
|
<textarea id="codeEditor" placeholder="Selectează un fișier..."></textarea>
|
|
<div id="markdownPreview"></div>
|
|
</div>
|
|
<div class="editor-footer">
|
|
<span id="statusText">Ready</span>
|
|
<span id="fileInfo"></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
<script>
|
|
// Theme
|
|
function initTheme() {
|
|
const saved = localStorage.getItem('theme') || 'dark';
|
|
document.documentElement.setAttribute('data-theme', saved);
|
|
updateThemeIcon(saved);
|
|
}
|
|
|
|
function toggleTheme() {
|
|
const current = document.documentElement.getAttribute('data-theme') || 'dark';
|
|
const next = current === 'dark' ? 'light' : 'dark';
|
|
document.documentElement.setAttribute('data-theme', next);
|
|
localStorage.setItem('theme', next);
|
|
updateThemeIcon(next);
|
|
}
|
|
|
|
function updateThemeIcon(theme) {
|
|
const icon = document.getElementById('themeIcon');
|
|
if (icon) {
|
|
icon.setAttribute('data-lucide', theme === 'dark' ? 'sun' : 'moon');
|
|
lucide.createIcons();
|
|
}
|
|
}
|
|
|
|
initTheme();
|
|
lucide.createIcons();
|
|
|
|
const API_BASE = window.location.pathname.includes('/echo/') ? '/echo' : '';
|
|
let currentPath = '';
|
|
let currentFile = null;
|
|
let originalContent = '';
|
|
let isModified = false;
|
|
let currentViewMode = localStorage.getItem('filesViewMode') || 'tiles';
|
|
let currentSortBy = localStorage.getItem('filesSortBy') || 'name';
|
|
let currentSortDir = localStorage.getItem('filesSortDir') || 'asc';
|
|
let currentItems = [];
|
|
let gitStatus = {}; // Map of filepath -> status (M, A, D, ??)
|
|
let gitOnlyMode = false; // Show only git-changed files
|
|
|
|
// Initialize view mode
|
|
function initViewMode() {
|
|
setViewMode(currentViewMode, false);
|
|
document.getElementById('sortBy').value = currentSortBy;
|
|
updateSortIcon();
|
|
|
|
// Update mobile dropdown initial states
|
|
document.querySelectorAll('.view-sort-option[data-sort]').forEach(btn => {
|
|
btn.classList.toggle('active', btn.dataset.sort === currentSortBy);
|
|
});
|
|
document.querySelectorAll('.view-sort-option[data-dir]').forEach(btn => {
|
|
btn.classList.toggle('active', btn.dataset.dir === currentSortDir);
|
|
});
|
|
}
|
|
|
|
function setViewMode(mode, reload = true) {
|
|
currentViewMode = mode;
|
|
localStorage.setItem('filesViewMode', mode);
|
|
|
|
// Update desktop buttons
|
|
document.querySelectorAll('#viewModeToggle .view-btn').forEach(btn => {
|
|
btn.classList.toggle('active', btn.dataset.view === mode);
|
|
});
|
|
|
|
// Update mobile dropdown options
|
|
document.querySelectorAll('.view-sort-option[data-view]').forEach(btn => {
|
|
btn.classList.toggle('active', btn.dataset.view === mode);
|
|
});
|
|
|
|
// Update grid class
|
|
const grid = document.getElementById('fileGrid');
|
|
grid.classList.remove('view-list', 'view-details', 'view-tiles');
|
|
grid.classList.add('view-' + mode);
|
|
|
|
if (reload && currentItems.length > 0) {
|
|
renderFileGrid(currentItems);
|
|
}
|
|
}
|
|
|
|
function sortFiles() {
|
|
currentSortBy = document.getElementById('sortBy').value;
|
|
localStorage.setItem('filesSortBy', currentSortBy);
|
|
if (currentItems.length > 0) {
|
|
renderFileGrid(currentItems);
|
|
}
|
|
}
|
|
|
|
function setSortBy(field) {
|
|
if (currentSortBy === field) {
|
|
toggleSortDir();
|
|
} else {
|
|
currentSortBy = field;
|
|
document.getElementById('sortBy').value = field;
|
|
localStorage.setItem('filesSortBy', currentSortBy);
|
|
if (currentItems.length > 0) {
|
|
renderFileGrid(currentItems);
|
|
}
|
|
}
|
|
}
|
|
|
|
function toggleSortDir() {
|
|
currentSortDir = currentSortDir === 'asc' ? 'desc' : 'asc';
|
|
localStorage.setItem('filesSortDir', currentSortDir);
|
|
updateSortIcon();
|
|
if (currentItems.length > 0) {
|
|
renderFileGrid(currentItems);
|
|
}
|
|
}
|
|
|
|
function updateSortIcon() {
|
|
const icon = document.getElementById('sortDirIcon');
|
|
const iconName = currentSortDir === 'asc' ? 'arrow-down-a-z' : 'arrow-up-z-a';
|
|
icon.setAttribute('data-lucide', iconName);
|
|
lucide.createIcons();
|
|
}
|
|
|
|
function showBrowse() {
|
|
if (isModified && !confirm('Ai modificări nesalvate. Continui?')) return;
|
|
|
|
// Get parent directory of current file
|
|
let parentPath = '';
|
|
if (currentFile) {
|
|
const parts = currentFile.split('/');
|
|
parts.pop(); // Remove filename
|
|
parentPath = parts.join('/');
|
|
}
|
|
|
|
// Switch to browse mode
|
|
document.body.classList.remove('editor-mode');
|
|
document.getElementById('browsePanel').classList.remove('hidden');
|
|
document.getElementById('editorPanel').classList.remove('active');
|
|
|
|
// Show git filter in browse mode
|
|
document.getElementById('gitFilterBtn').style.display = 'flex';
|
|
|
|
// Reload directory listing
|
|
loadPath(parentPath);
|
|
}
|
|
|
|
function showEditor() {
|
|
document.body.classList.add('editor-mode');
|
|
document.getElementById('browsePanel').classList.add('hidden');
|
|
document.getElementById('editorPanel').classList.add('active');
|
|
document.getElementById('gitFilterBtn').style.display = 'none';
|
|
}
|
|
|
|
async function loadGitStatus() {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/api/git?` + Date.now());
|
|
const data = await response.json();
|
|
gitStatus = {};
|
|
if (data.uncommittedParsed) {
|
|
data.uncommittedParsed.forEach(item => {
|
|
// Normalize path: remove ./ prefix, forward slashes for consistency
|
|
const normalized = item.path.replace(/^\.\//, '').replace(/\\/g, '/');
|
|
gitStatus[normalized] = item.status;
|
|
});
|
|
}
|
|
console.log('📂 Git status loaded:', Object.keys(gitStatus).length, 'files');
|
|
} catch (e) {
|
|
console.error('Failed to load git status:', e);
|
|
}
|
|
}
|
|
|
|
function getGitStatusForPath(path) {
|
|
// Try exact match first
|
|
if (gitStatus[path]) return gitStatus[path];
|
|
|
|
// Normalize: remove ./ prefix, convert backslashes
|
|
const normalized = path.replace(/^\.\//, '').replace(/\\/g, '/');
|
|
if (gitStatus[normalized]) return gitStatus[normalized];
|
|
|
|
// Try without extension for edge cases
|
|
const withoutExt = path.replace(/\.[^.]+$/, '');
|
|
if (gitStatus[withoutExt]) return gitStatus[withoutExt];
|
|
|
|
// Try all keys that might match (case-insensitive)
|
|
const lowerPath = path.toLowerCase();
|
|
for (const [key, val] of Object.entries(gitStatus)) {
|
|
if (key.toLowerCase() === lowerPath) return val;
|
|
if (key.toLowerCase().endsWith('/' + lowerPath)) return val;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
async function showDiff(filepath, event) {
|
|
if (event) event.stopPropagation();
|
|
try {
|
|
const response = await fetch(`${API_BASE}/api/diff?path=${encodeURIComponent(filepath)}`);
|
|
const data = await response.json();
|
|
|
|
// Show in a modal or the editor
|
|
const diffHtml = data.diff
|
|
.split('\n')
|
|
.map(line => {
|
|
if (line.startsWith('+') && !line.startsWith('+++')) {
|
|
return `<span style="color:var(--success)">${escapeHtml(line)}</span>`;
|
|
} else if (line.startsWith('-') && !line.startsWith('---')) {
|
|
return `<span style="color:var(--error)">${escapeHtml(line)}</span>`;
|
|
} else if (line.startsWith('@@')) {
|
|
return `<span style="color:var(--accent)">${escapeHtml(line)}</span>`;
|
|
}
|
|
return escapeHtml(line);
|
|
})
|
|
.join('\n');
|
|
|
|
// Open file in editor with diff view
|
|
document.getElementById('editorFileName').textContent = `DIFF: ${filepath}`;
|
|
document.getElementById('codeEditor').value = data.diff;
|
|
document.getElementById('markdownPreview').innerHTML = `<pre style="font-family:var(--font-mono);font-size:13px;line-height:1.5">${diffHtml}</pre>`;
|
|
document.getElementById('editorBody').classList.add('preview-active');
|
|
document.getElementById('previewBtn').style.display = 'flex';
|
|
document.getElementById('previewBtn').classList.add('active');
|
|
document.getElementById('saveBtn').disabled = true;
|
|
document.getElementById('reloadBtn').disabled = true;
|
|
currentFile = null;
|
|
setStatus('Diff view', 'saved');
|
|
showEditor();
|
|
} catch (e) {
|
|
alert('Eroare la încărcare diff: ' + e.message);
|
|
}
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
}
|
|
|
|
async function loadPath(path = '') {
|
|
currentPath = path;
|
|
updateBreadcrumb();
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/api/files?path=${encodeURIComponent(path)}&action=list`);
|
|
const data = await response.json();
|
|
|
|
if (data.error) {
|
|
showError(data.error);
|
|
return;
|
|
}
|
|
|
|
if (data.type === 'dir') {
|
|
await loadGitStatus(); // Refresh git status
|
|
renderFileGrid(data.items);
|
|
updateURL(path);
|
|
|
|
// If we're in editor mode and loading a directory, switch to browse mode
|
|
const editorPanel = document.getElementById('editorPanel');
|
|
if (editorPanel.classList.contains('active')) {
|
|
document.body.classList.remove('editor-mode');
|
|
document.getElementById('browsePanel').classList.remove('hidden');
|
|
editorPanel.classList.remove('active');
|
|
document.getElementById('gitFilterBtn').style.display = 'flex';
|
|
}
|
|
} else if (data.type === 'file') {
|
|
await loadGitStatus(); // Load git status before opening file
|
|
openFile(path, data);
|
|
}
|
|
} catch (e) {
|
|
showError('Eroare: ' + e.message);
|
|
}
|
|
}
|
|
|
|
function updateBreadcrumb() {
|
|
const breadcrumb = document.getElementById('breadcrumb');
|
|
const parts = currentPath.split('/').filter(p => p);
|
|
|
|
let html = `<span class="breadcrumb-item ${parts.length === 0 ? 'current' : ''}" onclick="loadPath('')">~/clawd</span>`;
|
|
let buildPath = '';
|
|
|
|
for (let i = 0; i < parts.length; i++) {
|
|
const part = parts[i];
|
|
buildPath += (buildPath ? '/' : '') + part;
|
|
const p = buildPath;
|
|
const isCurrent = i === parts.length - 1;
|
|
html += `
|
|
<span class="breadcrumb-sep"><i data-lucide="chevron-right"></i></span>
|
|
<span class="breadcrumb-item ${isCurrent ? 'current' : ''}" onclick="loadPath('${p}')">${part}</span>
|
|
`;
|
|
}
|
|
|
|
breadcrumb.innerHTML = html;
|
|
lucide.createIcons();
|
|
}
|
|
|
|
function renderFileGrid(items) {
|
|
// Store items for re-rendering on view/sort change
|
|
currentItems = items;
|
|
|
|
const grid = document.getElementById('fileGrid');
|
|
grid.classList.remove('view-list', 'view-details', 'view-tiles');
|
|
grid.classList.add('view-' + currentViewMode);
|
|
|
|
if (items.length === 0) {
|
|
grid.innerHTML = `
|
|
<div class="empty-state">
|
|
<i data-lucide="folder-open"></i>
|
|
<p>Folder gol</p>
|
|
</div>
|
|
`;
|
|
lucide.createIcons();
|
|
return;
|
|
}
|
|
|
|
// Sort items
|
|
const getExt = (name) => name.includes('.') ? name.split('.').pop().toLowerCase() : '';
|
|
const sorted = [...items].sort((a, b) => {
|
|
// Directories always first
|
|
if (a.type !== b.type) return a.type === 'dir' ? -1 : 1;
|
|
|
|
let cmp = 0;
|
|
if (currentSortBy === 'name') {
|
|
cmp = a.name.localeCompare(b.name);
|
|
} else if (currentSortBy === 'date') {
|
|
cmp = (a.mtime || 0) - (b.mtime || 0);
|
|
} else if (currentSortBy === 'size') {
|
|
cmp = (a.size || 0) - (b.size || 0);
|
|
} else if (currentSortBy === 'type') {
|
|
cmp = getExt(a.name).localeCompare(getExt(b.name));
|
|
}
|
|
return currentSortDir === 'asc' ? cmp : -cmp;
|
|
});
|
|
|
|
// Add header for details view
|
|
let headerHtml = '';
|
|
if (currentViewMode === 'details') {
|
|
const arrow = (field) => currentSortBy === field ? (currentSortDir === 'asc' ? ' ▲' : ' ▼') : '';
|
|
headerHtml = `
|
|
<div class="file-header">
|
|
<span></span>
|
|
<span onclick="setSortBy('name')" style="cursor:pointer">Nume${arrow('name')}</span>
|
|
<span onclick="setSortBy('type')" style="cursor:pointer">Tip${arrow('type')}</span>
|
|
<span onclick="setSortBy('size')" style="cursor:pointer">Mărime${arrow('size')}</span>
|
|
<span onclick="setSortBy('date')" style="cursor:pointer">Dată${arrow('date')}</span>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
grid.innerHTML = headerHtml + sorted.map(item => {
|
|
const dateStr = item.mtime ? new Date(item.mtime * 1000).toLocaleString('ro-RO', {
|
|
day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit'
|
|
}) : '';
|
|
const fileType = item.type === 'dir' ? 'Folder' : getFileType(item.name);
|
|
const sizeStr = item.size !== undefined ? formatSize(item.size) : '-';
|
|
|
|
// Git status
|
|
const gStatus = getGitStatusForPath(item.path) || '';
|
|
const gitBadge = gStatus ? getGitBadge(gStatus) : '';
|
|
const hasGitChange = gStatus && gStatus !== '??'; // Only show for tracked changes
|
|
|
|
if (currentViewMode === 'details') {
|
|
return `
|
|
<div class="file-item ${currentFile === item.path ? 'active' : ''} ${hasGitChange ? 'git-changed' : ''}" onclick="handleClick('${item.path}', '${item.type}')">
|
|
<div class="file-icon ${item.type === 'dir' ? 'folder' : ''}">
|
|
<i data-lucide="${item.type === 'dir' ? 'folder' : getFileIcon(item.name)}"></i>
|
|
</div>
|
|
<div class="file-name">${gitBadge}${item.name}</div>
|
|
<div class="file-meta">
|
|
<span class="file-type">${fileType}</span>
|
|
<span class="file-size">${sizeStr}</span>
|
|
<span class="file-date">${hasGitChange ? `<button class="diff-btn" onclick="showDiff('${item.path}', event)" title="Vezi diff">diff</button>` : (dateStr || '-')}</span>
|
|
</div>
|
|
</div>
|
|
`;
|
|
} else if (currentViewMode === 'list') {
|
|
return `
|
|
<div class="file-item ${currentFile === item.path ? 'active' : ''} ${hasGitChange ? 'git-changed' : ''}" onclick="handleClick('${item.path}', '${item.type}')">
|
|
<div class="file-icon ${item.type === 'dir' ? 'folder' : ''}">
|
|
<i data-lucide="${item.type === 'dir' ? 'folder' : getFileIcon(item.name)}"></i>
|
|
</div>
|
|
<div class="file-name">${gitBadge}${item.name}</div>
|
|
</div>
|
|
`;
|
|
} else {
|
|
// Tiles view - original style
|
|
return `
|
|
<div class="file-item ${currentFile === item.path ? 'active' : ''} ${hasGitChange ? 'git-changed' : ''}" onclick="handleClick('${item.path}', '${item.type}')">
|
|
<div class="file-icon ${item.type === 'dir' ? 'folder' : ''}">
|
|
<i data-lucide="${item.type === 'dir' ? 'folder' : getFileIcon(item.name)}"></i>
|
|
</div>
|
|
<div class="file-name">${gitBadge}${item.name}</div>
|
|
</div>
|
|
`;
|
|
}
|
|
}).join('');
|
|
|
|
lucide.createIcons();
|
|
}
|
|
|
|
function getFileIcon(name) {
|
|
const ext = name.split('.').pop().toLowerCase();
|
|
const icons = {
|
|
'md': 'file-text',
|
|
'txt': 'file-text',
|
|
'json': 'file-json',
|
|
'js': 'file-code',
|
|
'py': 'file-code',
|
|
'html': 'file-code',
|
|
'css': 'file-code',
|
|
'sh': 'terminal',
|
|
'yml': 'file-cog',
|
|
'yaml': 'file-cog',
|
|
'log': 'file-text',
|
|
'xsd': 'file-code',
|
|
'pdf': 'file-text'
|
|
};
|
|
return icons[ext] || 'file';
|
|
}
|
|
|
|
function getFileType(name) {
|
|
const ext = name.split('.').pop().toLowerCase();
|
|
const types = {
|
|
'md': 'Markdown',
|
|
'txt': 'Text',
|
|
'json': 'JSON',
|
|
'js': 'JavaScript',
|
|
'py': 'Python',
|
|
'html': 'HTML',
|
|
'css': 'CSS',
|
|
'sh': 'Shell',
|
|
'yml': 'YAML',
|
|
'yaml': 'YAML',
|
|
'log': 'Log',
|
|
'xsd': 'XML Schema',
|
|
'pdf': 'PDF'
|
|
};
|
|
return types[ext] || ext.toUpperCase();
|
|
}
|
|
|
|
function formatSize(bytes) {
|
|
if (bytes < 1024) return bytes + ' B';
|
|
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
|
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
|
}
|
|
|
|
function getGitBadge(status) {
|
|
const badges = {
|
|
'M': '<span class="git-badge git-modified" title="Modificat">M</span>',
|
|
'A': '<span class="git-badge git-added" title="Adăugat">A</span>',
|
|
'D': '<span class="git-badge git-deleted" title="Șters">D</span>',
|
|
'??': '<span class="git-badge git-untracked" title="Nou (untracked)">+</span>',
|
|
'R': '<span class="git-badge git-renamed" title="Redenumit">R</span>',
|
|
};
|
|
return badges[status] || `<span class="git-badge">${status}</span>`;
|
|
}
|
|
|
|
function handleClick(path, type) {
|
|
if (type === 'dir') {
|
|
loadPath(path);
|
|
} else {
|
|
if (isModified && !confirm('Ai modificări nesalvate. Continui?')) return;
|
|
loadPath(path);
|
|
}
|
|
}
|
|
|
|
function openFile(path, data) {
|
|
currentFile = path;
|
|
originalContent = data.content;
|
|
updateURL(path);
|
|
|
|
// Switch to editor mode
|
|
document.body.classList.add('editor-mode');
|
|
document.getElementById('gitFilterBtn').style.display = 'none';
|
|
|
|
document.getElementById('editorFileName').textContent = data.name;
|
|
document.getElementById('codeEditor').value = data.content;
|
|
document.getElementById('saveBtn').disabled = false;
|
|
document.getElementById('reloadBtn').disabled = false;
|
|
document.getElementById('fileInfo').textContent = formatSize(data.size);
|
|
|
|
// Show preview button for markdown files
|
|
const isMarkdown = path.endsWith('.md');
|
|
const previewBtn = document.getElementById('previewBtn');
|
|
const downloadPdfBtn = document.getElementById('downloadPdfBtn');
|
|
const downloadPdfMenuItem = document.getElementById('downloadPdfMenuItem');
|
|
const previewMenuItem = document.getElementById('previewMenuItem');
|
|
|
|
previewBtn.style.display = isMarkdown ? 'flex' : 'none';
|
|
downloadPdfBtn.style.display = isMarkdown ? 'flex' : 'none';
|
|
downloadPdfMenuItem.classList.toggle('hidden', !isMarkdown);
|
|
previewMenuItem.classList.toggle('hidden', !isMarkdown);
|
|
|
|
// Show diff button only if file has git changes
|
|
const gitStatus_forFile = getGitStatusForPath(path);
|
|
const hasGitChanges = gitStatus_forFile && gitStatus_forFile !== '??'; // Only show for tracked changes (M, A, D, etc), not untracked (??)
|
|
|
|
const diffBtn = document.getElementById('diffBtn');
|
|
const diffMenuItem = document.getElementById('diffMenuItem');
|
|
|
|
// Desktop: show diff button only if git changes
|
|
diffBtn.style.display = hasGitChanges ? 'flex' : 'none';
|
|
|
|
// Mobile menu: ALWAYS show diff item, but disable if no changes
|
|
diffMenuItem.classList.remove('hidden');
|
|
diffMenuItem.disabled = !hasGitChanges;
|
|
if (!gitStatus_forFile) {
|
|
diffMenuItem.title = 'File not in git repo';
|
|
} else if (!hasGitChanges && gitStatus_forFile === '??') {
|
|
diffMenuItem.title = 'File is untracked (new)';
|
|
} else if (!hasGitChanges) {
|
|
diffMenuItem.title = 'No tracked changes';
|
|
} else {
|
|
diffMenuItem.title = 'Show git changes';
|
|
}
|
|
diffBtn.classList.remove('active');
|
|
|
|
// Auto-activate preview for markdown files (hides diff button automatically)
|
|
if (isMarkdown) {
|
|
const preview = document.getElementById('markdownPreview');
|
|
preview.innerHTML = marked.parse(data.content);
|
|
document.getElementById('editorBody').classList.add('preview-active');
|
|
previewBtn.classList.add('active');
|
|
// Hide desktop diff button in preview mode, but keep menu item visible
|
|
if (diffBtn.style.display !== 'none') {
|
|
diffBtn.style.display = 'none';
|
|
}
|
|
} else {
|
|
document.getElementById('editorBody').classList.remove('preview-active');
|
|
previewBtn.classList.remove('active');
|
|
}
|
|
|
|
if (data.truncated) {
|
|
setStatus('Fișier trunchiat', 'error');
|
|
} else {
|
|
setStatus('Loaded', 'saved');
|
|
}
|
|
|
|
isModified = false;
|
|
showEditor();
|
|
}
|
|
|
|
function togglePreview() {
|
|
const editorBody = document.getElementById('editorBody');
|
|
const previewBtn = document.getElementById('previewBtn');
|
|
const diffBtn = document.getElementById('diffBtn');
|
|
const diffMenuItem = document.getElementById('diffMenuItem');
|
|
const preview = document.getElementById('markdownPreview');
|
|
const content = document.getElementById('codeEditor').value;
|
|
|
|
if (editorBody.classList.contains('preview-active')) {
|
|
// Switch to edit mode
|
|
editorBody.classList.remove('preview-active');
|
|
previewBtn.classList.remove('active');
|
|
if (diffBtn) {
|
|
diffBtn.classList.remove('active');
|
|
const gitStat = getGitStatusForPath(currentFile);
|
|
const hasGitChanges = gitStat && gitStat !== '??';
|
|
diffBtn.style.display = hasGitChanges ? 'flex' : 'none';
|
|
diffMenuItem.disabled = !hasGitChanges;
|
|
}
|
|
setStatus('Edit mode', 'saved');
|
|
} else {
|
|
// Switch to preview mode
|
|
preview.innerHTML = marked.parse(content);
|
|
editorBody.classList.add('preview-active');
|
|
previewBtn.classList.add('active');
|
|
if (diffBtn) {
|
|
diffBtn.classList.remove('active');
|
|
diffBtn.style.display = 'none'; // Hide diff button in preview mode
|
|
// Keep diff menu item enabled/disabled based on git status
|
|
const gitStat = getGitStatusForPath(currentFile);
|
|
const hasGitChanges = gitStat && gitStat !== '??';
|
|
diffMenuItem.disabled = !hasGitChanges;
|
|
}
|
|
setStatus('Preview mode', 'saved');
|
|
}
|
|
}
|
|
|
|
async function toggleDiff() {
|
|
if (!currentFile) return;
|
|
|
|
// Check if file has git changes (only for tracked changes, not untracked)
|
|
const gitStat = getGitStatusForPath(currentFile);
|
|
const hasGitChanges = gitStat && gitStat !== '??';
|
|
if (!hasGitChanges) {
|
|
setStatus('Nicio modificare git pentru acest fișier', 'error');
|
|
return;
|
|
}
|
|
|
|
const editorBody = document.getElementById('editorBody');
|
|
const diffBtn = document.getElementById('diffBtn');
|
|
const diffMenuItem = document.getElementById('diffMenuItem');
|
|
const previewBtn = document.getElementById('previewBtn');
|
|
const preview = document.getElementById('markdownPreview');
|
|
|
|
// If already showing diff, switch back to edit
|
|
if (diffBtn.classList.contains('active')) {
|
|
editorBody.classList.remove('preview-active');
|
|
diffBtn.classList.remove('active');
|
|
setStatus('Edit mode', 'saved');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setStatus('Se încarcă diff...', 'modified');
|
|
const response = await fetch(`${API_BASE}/api/diff?path=${encodeURIComponent(currentFile)}`);
|
|
const data = await response.json();
|
|
|
|
if (data.error) {
|
|
setStatus('Eroare: ' + data.error, 'error');
|
|
return;
|
|
}
|
|
|
|
// Format diff with colors
|
|
const diffHtml = data.diff
|
|
.split('\n')
|
|
.map(line => {
|
|
if (line.startsWith('+') && !line.startsWith('+++')) {
|
|
return `<span style="color:var(--success)">${escapeHtml(line)}</span>`;
|
|
} else if (line.startsWith('-') && !line.startsWith('---')) {
|
|
return `<span style="color:var(--error)">${escapeHtml(line)}</span>`;
|
|
} else if (line.startsWith('@@')) {
|
|
return `<span style="color:var(--accent)">${escapeHtml(line)}</span>`;
|
|
}
|
|
return escapeHtml(line);
|
|
})
|
|
.join('\n');
|
|
|
|
preview.innerHTML = `<pre style="font-family:var(--font-mono);font-size:13px;line-height:1.5;white-space:pre-wrap">${diffHtml || 'Nicio modificare față de ultima versiune comisă.'}</pre>`;
|
|
editorBody.classList.add('preview-active');
|
|
diffBtn.classList.add('active');
|
|
if (previewBtn) previewBtn.classList.remove('active');
|
|
setStatus('Diff view', 'saved');
|
|
} catch (e) {
|
|
setStatus('Eroare: ' + e.message, 'error');
|
|
}
|
|
}
|
|
|
|
async function downloadPDF() {
|
|
if (!currentFile) {
|
|
setStatus('Niciun fișier deschis', 'error');
|
|
return;
|
|
}
|
|
|
|
if (!currentFile.endsWith('.md')) {
|
|
setStatus('PDF download disponibil doar pentru fișiere Markdown', 'error');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setStatus('Se generează PDF...', 'modified');
|
|
|
|
// Get markdown content from editor
|
|
const markdownContent = document.getElementById('codeEditor').value;
|
|
const filename = currentFile.split('/').pop().replace('.md', '.pdf');
|
|
|
|
// Send to backend for conversion
|
|
const response = await fetch(`${API_BASE}/api/pdf`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
markdown: markdownContent,
|
|
filename: filename
|
|
})
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
setStatus('Eroare: ' + (error.error || 'Unknown error'), 'error');
|
|
return;
|
|
}
|
|
|
|
// Download the PDF
|
|
const blob = await response.blob();
|
|
const url = window.URL.createObjectURL(blob);
|
|
const link = document.createElement('a');
|
|
link.href = url;
|
|
link.download = filename;
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
window.URL.revokeObjectURL(url);
|
|
|
|
setStatus('PDF descărcat: ' + filename, 'saved');
|
|
} catch (e) {
|
|
setStatus('Eroare la descărcare PDF: ' + e.message, 'error');
|
|
console.error('PDF generation error:', e);
|
|
}
|
|
}
|
|
|
|
async function saveFile() {
|
|
if (!currentFile) return;
|
|
|
|
const content = document.getElementById('codeEditor').value;
|
|
|
|
try {
|
|
document.getElementById('saveBtn').disabled = true;
|
|
setStatus('Se salvează...', 'modified');
|
|
|
|
const response = await fetch(`${API_BASE}/api/files`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ path: currentFile, content })
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.error) {
|
|
setStatus('Eroare: ' + data.error, 'error');
|
|
} else {
|
|
originalContent = content;
|
|
isModified = false;
|
|
setStatus('Salvat ✓', 'saved');
|
|
}
|
|
} catch (e) {
|
|
setStatus('Eroare: ' + e.message, 'error');
|
|
}
|
|
|
|
document.getElementById('saveBtn').disabled = false;
|
|
}
|
|
|
|
function reloadFile() {
|
|
if (currentFile && confirm('Renunți la modificări?')) {
|
|
loadPath(currentFile);
|
|
}
|
|
}
|
|
|
|
function setStatus(text, type) {
|
|
const status = document.getElementById('statusText');
|
|
status.textContent = text;
|
|
status.className = 'status-' + type;
|
|
}
|
|
|
|
function showError(msg) {
|
|
document.getElementById('fileGrid').innerHTML = `
|
|
<div class="empty-state">
|
|
<i data-lucide="alert-circle"></i>
|
|
<p style="color: var(--error)">${msg}</p>
|
|
</div>
|
|
`;
|
|
lucide.createIcons();
|
|
}
|
|
|
|
function updateURL(path) {
|
|
if (path) {
|
|
history.replaceState(null, '', `#${path}`);
|
|
} else {
|
|
history.replaceState(null, '', window.location.pathname);
|
|
}
|
|
}
|
|
|
|
function toggleEditorMenu() {
|
|
const dropdown = document.getElementById('editorMenuDropdown');
|
|
dropdown.style.display = dropdown.style.display === 'none' ? 'block' : 'none';
|
|
}
|
|
|
|
function toggleViewSortMenu() {
|
|
const dropdown = document.getElementById('viewSortDropdown');
|
|
dropdown.style.display = dropdown.style.display === 'none' ? 'block' : 'none';
|
|
}
|
|
|
|
function setSortByMobile(field) {
|
|
currentSortBy = field;
|
|
document.getElementById('sortBy').value = field;
|
|
localStorage.setItem('filesSortBy', currentSortBy);
|
|
|
|
// Update active state in dropdown
|
|
document.querySelectorAll('.view-sort-option[data-sort]').forEach(btn => {
|
|
btn.classList.toggle('active', btn.dataset.sort === field);
|
|
});
|
|
|
|
if (currentItems.length > 0) {
|
|
renderFileGrid(currentItems);
|
|
}
|
|
}
|
|
|
|
function setSortDirMobile(dir) {
|
|
currentSortDir = dir;
|
|
localStorage.setItem('filesSortDir', currentSortDir);
|
|
updateSortIcon();
|
|
|
|
// Update active state in dropdown
|
|
document.querySelectorAll('.view-sort-option[data-dir]').forEach(btn => {
|
|
btn.classList.toggle('active', btn.dataset.dir === dir);
|
|
});
|
|
|
|
if (currentItems.length > 0) {
|
|
renderFileGrid(currentItems);
|
|
}
|
|
}
|
|
|
|
// Close menus when clicking outside
|
|
document.addEventListener('click', (e) => {
|
|
const editorMenu = document.getElementById('editorMenuMobile');
|
|
const editorDropdown = document.getElementById('editorMenuDropdown');
|
|
if (editorMenu && !editorMenu.contains(e.target) && editorDropdown) {
|
|
editorDropdown.style.display = 'none';
|
|
}
|
|
|
|
const viewSortGroup = document.querySelector('.view-sort-group');
|
|
const viewSortDropdown = document.getElementById('viewSortDropdown');
|
|
if (viewSortGroup && !viewSortGroup.contains(e.target) && viewSortDropdown) {
|
|
viewSortDropdown.style.display = 'none';
|
|
}
|
|
});
|
|
|
|
function getPathFromURL() {
|
|
const hash = window.location.hash;
|
|
return hash ? decodeURIComponent(hash.slice(1)) : '';
|
|
}
|
|
|
|
window.addEventListener('hashchange', () => loadPath(getPathFromURL()));
|
|
|
|
document.getElementById('codeEditor').addEventListener('input', function() {
|
|
isModified = this.value !== originalContent;
|
|
setStatus(isModified ? 'Modified' : 'Ready', isModified ? 'modified' : 'saved');
|
|
});
|
|
|
|
document.addEventListener('keydown', e => {
|
|
if (e.ctrlKey && e.key === 's') {
|
|
e.preventDefault();
|
|
saveFile();
|
|
}
|
|
});
|
|
|
|
// Init
|
|
initViewMode();
|
|
|
|
// Show git filter initially (browse mode by default)
|
|
document.getElementById('gitFilterBtn').style.display = 'flex';
|
|
|
|
// Load git status on init
|
|
loadGitStatus();
|
|
|
|
// Check for git mode
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
if (urlParams.get('git') === '1') {
|
|
gitOnlyMode = true;
|
|
loadGitChangedFiles();
|
|
} else {
|
|
loadPath(getPathFromURL());
|
|
}
|
|
|
|
function toggleGitFilter() {
|
|
gitOnlyMode = !gitOnlyMode;
|
|
const btn = document.getElementById('gitFilterBtn');
|
|
btn.classList.toggle('active', gitOnlyMode);
|
|
|
|
if (gitOnlyMode) {
|
|
loadGitChangedFiles();
|
|
} else {
|
|
// Return to normal browse
|
|
window.history.replaceState(null, '', 'files.html');
|
|
loadPath('');
|
|
}
|
|
}
|
|
|
|
async function loadGitChangedFiles() {
|
|
await loadGitStatus();
|
|
const changedPaths = Object.keys(gitStatus).filter(p => p);
|
|
|
|
// Update button state
|
|
document.getElementById('gitFilterBtn').classList.add('active');
|
|
|
|
if (changedPaths.length === 0) {
|
|
document.getElementById('breadcrumb').innerHTML = `
|
|
<span class="breadcrumb-item" onclick="toggleGitFilter()">~/clawd</span>
|
|
<span class="breadcrumb-sep"><i data-lucide="chevron-right"></i></span>
|
|
<span class="breadcrumb-item current" style="color:var(--success)">✓ Git curat</span>
|
|
`;
|
|
lucide.createIcons();
|
|
showError('Nicio modificare git - totul e comis!');
|
|
return;
|
|
}
|
|
|
|
// Update breadcrumb
|
|
document.getElementById('breadcrumb').innerHTML = `
|
|
<span class="breadcrumb-item" onclick="toggleGitFilter()">~/clawd</span>
|
|
<span class="breadcrumb-sep"><i data-lucide="chevron-right"></i></span>
|
|
<span class="breadcrumb-item current" style="color:var(--warning)">🔸 Git Changes (${changedPaths.length})</span>
|
|
`;
|
|
lucide.createIcons();
|
|
|
|
// Create virtual items for changed files
|
|
const items = changedPaths.map(path => ({
|
|
name: path,
|
|
path: path,
|
|
type: 'file',
|
|
size: null,
|
|
mtime: null
|
|
}));
|
|
|
|
currentItems = items;
|
|
renderFileGrid(items);
|
|
}
|
|
</script>
|
|
<!-- v2.0.2 - Fixed media query threshold (992px→1200px) for mobile hamburger menu -->
|
|
</body>
|
|
</html>
|