Mercurial
comparison hg-web/src/index.html @ 104:2301aeb7503b
[Hg Web] Super simple mercurial server.
| author | June Park <parkjune1995@gmail.com> |
|---|---|
| date | Sat, 03 Jan 2026 10:20:45 -0800 |
| parents | |
| children | ffb764d2fcc5 |
comparison
equal
deleted
inserted
replaced
| 103:f6d2f2eaaf84 | 104:2301aeb7503b |
|---|---|
| 1 <!DOCTYPE html> | |
| 2 <html lang="en"> | |
| 3 <head> | |
| 4 <meta charset="UTF-8"> | |
| 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| 6 <title>Zenbu Repository</title> | |
| 7 <link rel="stylesheet" href="/base.css"> | |
| 8 <link rel="stylesheet" href="/index.css"> | |
| 9 </head> | |
| 10 <body> | |
| 11 <main> | |
| 12 <div class="header"> | |
| 13 <h1>Zenbu Repository</h1> | |
| 14 <p class="description">Browse and clone this mercurial repository</p> | |
| 15 </div> | |
| 16 | |
| 17 <div class="clone-info"> | |
| 18 <strong>Clone this repository:</strong><br> | |
| 19 <code>hg clone http://zenbu.babocoder.com</code> | |
| 20 </div> | |
| 21 | |
| 22 <div class="breadcrumb" id="breadcrumb"></div> | |
| 23 | |
| 24 <div class="file-list" id="fileList"></div> | |
| 25 | |
| 26 <div class="readme-section" id="readmeSection" style="display: none;"> | |
| 27 <h2>README</h2> | |
| 28 <div class="readme-content" id="readmeContent"></div> | |
| 29 </div> | |
| 30 | |
| 31 <div class="empty-state" id="emptyState" style="display: none;"> | |
| 32 <p>No files found in this directory</p> | |
| 33 </div> | |
| 34 </main> | |
| 35 | |
| 36 <script src="/markdown_to_html.js"></script> | |
| 37 <script> | |
| 38 const API_BASE = '/api/repo'; | |
| 39 | |
| 40 // Get current path from URL | |
| 41 function getCurrentPath() { | |
| 42 const params = new URLSearchParams(window.location.search); | |
| 43 return params.get('path') || ''; | |
| 44 } | |
| 45 | |
| 46 // Update URL without reloading | |
| 47 function updateURL(path) { | |
| 48 const url = path ? `?path=${encodeURIComponent(path)}` : '/'; | |
| 49 window.history.pushState({path}, '', url); | |
| 50 } | |
| 51 | |
| 52 // Render breadcrumb | |
| 53 function renderBreadcrumb(path) { | |
| 54 const breadcrumb = document.getElementById('breadcrumb'); | |
| 55 if (!path) { | |
| 56 breadcrumb.innerHTML = '<a href="/">Root</a>'; | |
| 57 return; | |
| 58 } | |
| 59 | |
| 60 const parts = path.split('/').filter(p => p); | |
| 61 let currentPath = ''; | |
| 62 let html = '<a href="/">Root</a>'; | |
| 63 | |
| 64 parts.forEach((part, index) => { | |
| 65 currentPath += (currentPath ? '/' : '') + part; | |
| 66 html += ` <span>/</span> `; | |
| 67 if (index === parts.length - 1) { | |
| 68 html += `<span>${part}</span>`; | |
| 69 } else { | |
| 70 html += `<a href="?path=${encodeURIComponent(currentPath)}">${part}</a>`; | |
| 71 } | |
| 72 }); | |
| 73 | |
| 74 breadcrumb.innerHTML = html; | |
| 75 } | |
| 76 | |
| 77 // Render file list | |
| 78 function renderFiles(files) { | |
| 79 const fileList = document.getElementById('fileList'); | |
| 80 const emptyState = document.getElementById('emptyState'); | |
| 81 | |
| 82 if (!files || files.length === 0) { | |
| 83 fileList.style.display = 'none'; | |
| 84 emptyState.style.display = 'block'; | |
| 85 return; | |
| 86 } | |
| 87 | |
| 88 emptyState.style.display = 'none'; | |
| 89 fileList.style.display = 'block'; | |
| 90 | |
| 91 // Sort: directories first, then files, alphabetically | |
| 92 files.sort((a, b) => { | |
| 93 if (a.type !== b.type) { | |
| 94 return a.type === 'directory' ? -1 : 1; | |
| 95 } | |
| 96 return a.name.localeCompare(b.name); | |
| 97 }); | |
| 98 | |
| 99 let html = ''; | |
| 100 files.forEach(file => { | |
| 101 const icon = file.type === 'directory' ? '📁' : '📄'; | |
| 102 const className = file.type; | |
| 103 const href = file.type === 'directory' | |
| 104 ? `?path=${encodeURIComponent(file.path)}` | |
| 105 : `/api/repo/file?path=${encodeURIComponent(file.path)}`; | |
| 106 const target = file.type === 'directory' ? '' : 'target="_blank"'; | |
| 107 | |
| 108 html += ` | |
| 109 <div class="file-item ${className}"> | |
| 110 <span class="icon">${icon}</span> | |
| 111 <span class="name"> | |
| 112 <a href="${href}" ${target}>${file.name}</a> | |
| 113 </span> | |
| 114 </div> | |
| 115 `; | |
| 116 }); | |
| 117 | |
| 118 fileList.innerHTML = html; | |
| 119 } | |
| 120 | |
| 121 // Load and render README | |
| 122 async function loadReadme(path) { | |
| 123 const readmeSection = document.getElementById('readmeSection'); | |
| 124 const readmeContent = document.getElementById('readmeContent'); | |
| 125 | |
| 126 try { | |
| 127 const readmePath = path ? `${path}/README.md` : 'README.md'; | |
| 128 const response = await fetch(`/api/repo/readme?path=${encodeURIComponent(readmePath)}`); | |
| 129 | |
| 130 if (response.ok) { | |
| 131 const markdown = await response.text(); | |
| 132 readmeSection.style.display = 'block'; | |
| 133 renderMarkdown(readmeContent, markdown); | |
| 134 } else { | |
| 135 readmeSection.style.display = 'none'; | |
| 136 } | |
| 137 } catch (error) { | |
| 138 readmeSection.style.display = 'none'; | |
| 139 } | |
| 140 } | |
| 141 | |
| 142 // Load directory contents | |
| 143 async function loadDirectory(path) { | |
| 144 try { | |
| 145 const url = path ? `${API_BASE}/list?path=${encodeURIComponent(path)}` : `${API_BASE}/list`; | |
| 146 const response = await fetch(url); | |
| 147 const data = await response.json(); | |
| 148 | |
| 149 if (data.error) { | |
| 150 throw new Error(data.error); | |
| 151 } | |
| 152 | |
| 153 renderBreadcrumb(path); | |
| 154 renderFiles(data.files); | |
| 155 loadReadme(path); | |
| 156 } catch (error) { | |
| 157 console.error('Error loading directory:', error); | |
| 158 document.getElementById('fileList').innerHTML = ` | |
| 159 <div class="error-message">Error loading directory: ${error.message}</div> | |
| 160 `; | |
| 161 } | |
| 162 } | |
| 163 | |
| 164 // Handle browser back/forward | |
| 165 window.addEventListener('popstate', (event) => { | |
| 166 const path = event.state?.path || ''; | |
| 167 loadDirectory(path); | |
| 168 }); | |
| 169 | |
| 170 // Initial load | |
| 171 const currentPath = getCurrentPath(); | |
| 172 loadDirectory(currentPath); | |
| 173 </script> | |
| 174 </body> | |
| 175 </html> |