@@ -7,11 +7,13 @@ const pageTemplate = `<!doctype html> |
| 7 |
<meta name="viewport" content="width=device-width, initial-scale=1"> |
7 |
<meta name="viewport" content="width=device-width, initial-scale=1"> |
| 8 |
<meta name="color-scheme" content="light"> |
8 |
<meta name="color-scheme" content="light"> |
| 9 |
<title>{{.Title}}</title> |
9 |
<title>{{.Title}}</title> |
|
|
10 |
<link rel="icon" type="image/png" sizes="256x256" href="{{.BaseURL}}/.dumbforge/assets/gabriel-mark.png"> |
|
|
11 |
<link rel="icon" type="image/svg+xml" href="{{.BaseURL}}/.dumbforge/assets/gabriel-mark.svg"> |
| 10 |
<link rel="stylesheet" href="{{.BaseURL}}/.dumbforge/assets/site.css"> |
12 |
<link rel="stylesheet" href="{{.BaseURL}}/.dumbforge/assets/site.css"> |
| 11 |
</head> |
13 |
</head> |
| 12 |
<body data-base-url="{{.BaseURL}}" data-route-kind="{{.Kind}}" data-route-path="{{.Path}}" data-current-branch="{{.Branch}}" data-default-branch="{{.DefaultBranch}}" data-branches-url="{{.BranchesURL}}" data-files-url="{{.FilesURL}}"> |
14 |
<body data-base-url="{{.BaseURL}}" data-route-kind="{{.Kind}}" data-route-path="{{.Path}}" data-current-branch="{{.Branch}}" data-default-branch="{{.DefaultBranch}}" data-branches-url="{{.BranchesURL}}" data-files-url="{{.FilesURL}}"> |
| 13 |
<header class="topbar"> |
15 |
<header class="topbar"> |
| 14 |
<a class="brand" href="{{.BaseURL}}" aria-label="Dumbforge repository home"><span class="brand-mark">D</span></a> |
16 |
<a class="brand" href="{{.BaseURL}}" aria-label="Dumbforge repository home"><img class="brand-mark" src="{{.BaseURL}}/.dumbforge/assets/gabriel-mark.svg" alt=""></a> |
| 15 |
<nav class="header-crumbs" aria-label="Repository breadcrumb"> |
17 |
<nav class="header-crumbs" aria-label="Repository breadcrumb"> |
| 16 |
<a href="{{.BaseURL}}">{{.Owner}}</a><span>/</span><a class="header-repo" href="{{.BaseURL}}">{{.RepoName}}</a> |
18 |
<a href="{{.BaseURL}}">{{.Owner}}</a><span>/</span><a class="header-repo" href="{{.BaseURL}}">{{.RepoName}}</a> |
| 17 |
</nav> |
19 |
</nav> |
@@ -24,7 +26,7 @@ const pageTemplate = `<!doctype html> |
| 24 |
</section> |
26 |
</section> |
| 25 |
|
27 |
|
| 26 |
<main> |
28 |
<main> |
| 27 |
<div class="content-shell"> |
29 |
<div class="content-shell {{if eq .Kind "blob"}}blob-content-shell{{end}}"> |
| 28 |
{{if eq .Kind "repo"}} |
30 |
{{if eq .Kind "repo"}} |
| 29 |
<div class="repo-overview-head"> |
31 |
<div class="repo-overview-head"> |
| 30 |
<div class="repo-title"><span class="repo-icon">{{icon "repo"}}</span><a href="{{.BaseURL}}">{{.RepoName}}</a><span class="visibility">Public</span></div> |
32 |
<div class="repo-title"><span class="repo-icon">{{icon "repo"}}</span><a href="{{.BaseURL}}">{{.RepoName}}</a><span class="visibility">Public</span></div> |
@@ -113,24 +115,31 @@ const pageTemplate = `<!doctype html> |
| 113 |
</section>{{end}} |
115 |
</section>{{end}} |
| 114 |
</section> |
116 |
</section> |
| 115 |
{{else}} |
117 |
{{else}} |
| 116 |
<section class="primary full-width"> |
118 |
<div class="blob-layout"> |
| 117 |
<div class="toolbar toolbar-path"> |
119 |
<aside class="file-browser" aria-label="Repository files"> |
| 118 |
{{template "branch-menu" .}} |
120 |
<div class="file-browser-heading">{{icon "folder"}}<strong>Files</strong></div> |
| 119 |
<div class="breadcrumbs">{{range $i, $crumb := .Breadcrumbs}}{{if $i}}<span>/</span>{{end}}<a href="{{$crumb.URL}}">{{$crumb.Name}}</a>{{end}}</div> |
121 |
<div class="file-browser-branch">{{template "branch-menu" .}}</div> |
| 120 |
<span class="toolbar-spacer"></span> |
122 |
<label class="file-tree-search">{{icon "search"}}<input type="search" placeholder="Search files" autocomplete="off" data-file-tree-query></label> |
| 121 |
{{template "go-file-button" .}} |
123 |
<nav class="file-tree" data-file-tree><p class="menu-status">Loading files…</p></nav> |
| 122 |
{{template "clone-menu" .}} |
124 |
</aside> |
| 123 |
</div> |
125 |
<section class="primary blob-main"> |
| 124 |
<div class="blob-card"> |
126 |
<div class="toolbar toolbar-path blob-toolbar"> |
| 125 |
<div class="blob-heading"> |
127 |
<div class="breadcrumbs">{{range $i, $crumb := .Breadcrumbs}}{{if $i}}<span>/</span>{{end}}<a href="{{$crumb.URL}}">{{$crumb.Name}}</a>{{end}}</div> |
| 126 |
<div><strong>{{.Path}}</strong><span>{{humanSize .FileSize}} · {{.Language}}</span></div> |
128 |
<span class="toolbar-spacer"></span> |
| 127 |
<div class="blob-actions"><a href="{{.RawURL}}">Raw</a><button type="button" data-copy="{{.RawURL}}"><span data-copy-icon>{{icon "copy"}}</span><span data-copied-icon hidden>{{icon "check"}}</span><span data-copy-label>Copy link</span></button></div> |
129 |
{{template "go-file-button" .}} |
|
|
130 |
{{template "clone-menu" .}} |
| 128 |
</div> |
131 |
</div> |
| 129 |
{{if .IsImage}}<div class="image-preview"><img src="{{.RawURL}}" alt="{{.Path}}"></div> |
132 |
<div class="blob-card"> |
| 130 |
{{else if .Code}}<div class="code-view">{{.Code}}</div> |
133 |
<div class="blob-heading"> |
| 131 |
{{else}}<div class="binary-notice"><span>{{icon "file"}}</span><strong>Preview unavailable</strong><p>This file is binary or too large to render safely.</p><a href="{{.RawURL}}">Download raw file</a></div>{{end}} |
134 |
<div><strong>{{.Path}}</strong><span>{{humanSize .FileSize}} · {{.Language}}</span></div> |
| 132 |
</div> |
135 |
<div class="blob-actions"><a href="{{.RawURL}}">Raw</a><button type="button" data-copy="{{.RawURL}}"><span data-copy-icon>{{icon "copy"}}</span><span data-copied-icon hidden>{{icon "check"}}</span><span data-copy-label>Copy link</span></button></div> |
| 133 |
</section> |
136 |
</div> |
|
|
137 |
{{if .IsImage}}<div class="image-preview"><img src="{{.RawURL}}" alt="{{.Path}}"></div> |
|
|
138 |
{{else if .Code}}<div class="code-view">{{.Code}}</div> |
|
|
139 |
{{else}}<div class="binary-notice"><span>{{icon "file"}}</span><strong>Preview unavailable</strong><p>This file is binary or too large to render safely.</p><a href="{{.RawURL}}">Download raw file</a></div>{{end}} |
|
|
140 |
</div> |
|
|
141 |
</section> |
|
|
142 |
</div> |
| 134 |
{{end}} |
143 |
{{end}} |
| 135 |
</div> |
144 |
</div> |
| 136 |
</main> |
145 |
</main> |
@@ -142,6 +151,8 @@ const pageTemplate = `<!doctype html> |
| 142 |
<div class="file-results" data-file-results><p class="menu-status">Loading files…</p></div> |
151 |
<div class="file-results" data-file-results><p class="menu-status">Loading files…</p></div> |
| 143 |
</section> |
152 |
</section> |
| 144 |
</div> |
153 |
</div> |
|
|
154 |
<template data-tree-folder-icon>{{icon "folder"}}</template> |
|
|
155 |
<template data-tree-file-icon>{{icon "file"}}</template> |
| 145 |
|
156 |
|
| 146 |
<script> |
157 |
<script> |
| 147 |
(function () { |
158 |
(function () { |
@@ -262,6 +273,23 @@ const pageTemplate = `<!doctype html> |
| 262 |
var fileQuery = one('[data-file-query]'); |
273 |
var fileQuery = one('[data-file-query]'); |
| 263 |
var fileResults = one('[data-file-results]'); |
274 |
var fileResults = one('[data-file-results]'); |
| 264 |
var files = null; |
275 |
var files = null; |
|
|
276 |
var filesPromise = null; |
|
|
277 |
function loadFiles() { |
|
|
278 |
if (files) return Promise.resolve(files); |
|
|
279 |
if (!filesPromise) { |
|
|
280 |
filesPromise = fetch(body.dataset.filesUrl, {cache: 'no-cache'}).then(function (response) { |
|
|
281 |
if (!response.ok) throw new Error('HTTP ' + response.status); |
|
|
282 |
return response.json(); |
|
|
283 |
}).then(function (manifest) { |
|
|
284 |
files = manifest.files || []; |
|
|
285 |
return files; |
|
|
286 |
}).catch(function (error) { |
|
|
287 |
filesPromise = null; |
|
|
288 |
throw error; |
|
|
289 |
}); |
|
|
290 |
} |
|
|
291 |
return filesPromise; |
|
|
292 |
} |
| 265 |
function renderFiles() { |
293 |
function renderFiles() { |
| 266 |
if (!files) return; |
294 |
if (!files) return; |
| 267 |
var query = fileQuery.value.trim().toLowerCase(); |
295 |
var query = fileQuery.value.trim().toLowerCase(); |
@@ -293,10 +321,7 @@ const pageTemplate = `<!doctype html> |
| 293 |
fileQuery.value = ''; |
321 |
fileQuery.value = ''; |
| 294 |
fileQuery.focus(); |
322 |
fileQuery.focus(); |
| 295 |
if (files) { renderFiles(); return; } |
323 |
if (files) { renderFiles(); return; } |
| 296 |
fetch(body.dataset.filesUrl, {cache: 'no-cache'}).then(function (response) { |
324 |
loadFiles().then(renderFiles) |
| 297 |
if (!response.ok) throw new Error('HTTP ' + response.status); |
|
|
| 298 |
return response.json(); |
|
|
| 299 |
}).then(function (manifest) { files = manifest.files || []; renderFiles(); }) |
|
|
| 300 |
.catch(function () { fileResults.innerHTML = '<p class="menu-status">Could not load the file list.</p>'; }); |
325 |
.catch(function () { fileResults.innerHTML = '<p class="menu-status">Could not load the file list.</p>'; }); |
| 301 |
} |
326 |
} |
| 302 |
function closeFiles() { |
327 |
function closeFiles() { |
@@ -310,6 +335,95 @@ const pageTemplate = `<!doctype html> |
| 310 |
fileQuery.addEventListener('keydown', function (event) { |
335 |
fileQuery.addEventListener('keydown', function (event) { |
| 311 |
if (event.key === 'Enter') { var first = one('.file-result', fileResults); if (first) window.location.href = first.href; } |
336 |
if (event.key === 'Enter') { var first = one('.file-result', fileResults); if (first) window.location.href = first.href; } |
| 312 |
}); |
337 |
}); |
|
|
338 |
|
|
|
339 |
var fileTree = one('[data-file-tree]'); |
|
|
340 |
var fileTreeQuery = one('[data-file-tree-query]'); |
|
|
341 |
function addTreeIcon(target, selector) { |
|
|
342 |
var source = one(selector); |
|
|
343 |
if (source) target.appendChild(source.content.cloneNode(true)); |
|
|
344 |
} |
|
|
345 |
function fileTreeLink(file, label, compact) { |
|
|
346 |
var link = document.createElement('a'); |
|
|
347 |
link.className = 'file-tree-file'; |
|
|
348 |
link.href = file.url; |
|
|
349 |
addTreeIcon(link, '[data-tree-file-icon]'); |
|
|
350 |
var text = document.createElement('span'); |
|
|
351 |
text.textContent = label; |
|
|
352 |
if (compact) text.title = file.path; |
|
|
353 |
link.appendChild(text); |
|
|
354 |
if (file.path === body.dataset.routePath) { |
|
|
355 |
link.classList.add('selected'); |
|
|
356 |
link.setAttribute('aria-current', 'page'); |
|
|
357 |
} |
|
|
358 |
return link; |
|
|
359 |
} |
|
|
360 |
function renderTreeNode(node, container, prefix) { |
|
|
361 |
Object.keys(node.folders).sort().forEach(function (name) { |
|
|
362 |
var child = node.folders[name]; |
|
|
363 |
var childPath = prefix ? prefix + '/' + name : name; |
|
|
364 |
var wrapper = document.createElement('div'); |
|
|
365 |
wrapper.className = 'file-tree-node'; |
|
|
366 |
var toggle = document.createElement('button'); |
|
|
367 |
toggle.className = 'file-tree-folder'; |
|
|
368 |
toggle.type = 'button'; |
|
|
369 |
var caret = document.createElement('span'); |
|
|
370 |
caret.className = 'file-tree-caret'; |
|
|
371 |
caret.textContent = '›'; |
|
|
372 |
toggle.appendChild(caret); |
|
|
373 |
addTreeIcon(toggle, '[data-tree-folder-icon]'); |
|
|
374 |
var label = document.createElement('span'); |
|
|
375 |
label.textContent = name; |
|
|
376 |
toggle.appendChild(label); |
|
|
377 |
var children = document.createElement('div'); |
|
|
378 |
children.className = 'file-tree-children'; |
|
|
379 |
var open = body.dataset.routePath === childPath || body.dataset.routePath.indexOf(childPath + '/') === 0; |
|
|
380 |
children.hidden = !open; |
|
|
381 |
caret.classList.toggle('open', open); |
|
|
382 |
toggle.setAttribute('aria-expanded', open ? 'true' : 'false'); |
|
|
383 |
toggle.addEventListener('click', function () { |
|
|
384 |
var opening = children.hidden; |
|
|
385 |
children.hidden = !opening; |
|
|
386 |
caret.classList.toggle('open', opening); |
|
|
387 |
toggle.setAttribute('aria-expanded', opening ? 'true' : 'false'); |
|
|
388 |
}); |
|
|
389 |
wrapper.appendChild(toggle); |
|
|
390 |
wrapper.appendChild(children); |
|
|
391 |
container.appendChild(wrapper); |
|
|
392 |
renderTreeNode(child, children, childPath); |
|
|
393 |
}); |
|
|
394 |
node.files.sort(function (a, b) { return a.name.localeCompare(b.name); }).forEach(function (entry) { |
|
|
395 |
container.appendChild(fileTreeLink(entry.file, entry.name, false)); |
|
|
396 |
}); |
|
|
397 |
} |
|
|
398 |
function renderFileTree() { |
|
|
399 |
if (!fileTree || !files) return; |
|
|
400 |
var query = fileTreeQuery.value.trim().toLowerCase(); |
|
|
401 |
fileTree.textContent = ''; |
|
|
402 |
if (query) { |
|
|
403 |
var matches = files.filter(function (file) { return file.path.toLowerCase().indexOf(query) !== -1; }); |
|
|
404 |
matches.slice(0, 200).forEach(function (file) { fileTree.appendChild(fileTreeLink(file, file.path, true)); }); |
|
|
405 |
if (!matches.length) fileTree.innerHTML = '<p class="menu-status">No matching files.</p>'; |
|
|
406 |
return; |
|
|
407 |
} |
|
|
408 |
var root = {folders: {}, files: []}; |
|
|
409 |
files.forEach(function (file) { |
|
|
410 |
var parts = file.path.split('/'); |
|
|
411 |
var node = root; |
|
|
412 |
parts.slice(0, -1).forEach(function (part) { |
|
|
413 |
if (!node.folders[part]) node.folders[part] = {folders: {}, files: []}; |
|
|
414 |
node = node.folders[part]; |
|
|
415 |
}); |
|
|
416 |
node.files.push({name: parts[parts.length - 1], file: file}); |
|
|
417 |
}); |
|
|
418 |
renderTreeNode(root, fileTree, ''); |
|
|
419 |
var selected = one('.file-tree-file.selected', fileTree); |
|
|
420 |
if (selected) selected.scrollIntoView({block: 'center'}); |
|
|
421 |
} |
|
|
422 |
if (fileTree) { |
|
|
423 |
loadFiles().then(renderFileTree).catch(function () { fileTree.innerHTML = '<p class="menu-status">Could not load files.</p>'; }); |
|
|
424 |
fileTreeQuery.addEventListener('input', renderFileTree); |
|
|
425 |
} |
|
|
426 |
|
| 313 |
document.addEventListener('keydown', function (event) { |
427 |
document.addEventListener('keydown', function (event) { |
| 314 |
if (event.key !== 'Escape') return; |
428 |
if (event.key !== 'Escape') return; |
| 315 |
if (!fileDialog.hidden) { closeFiles(); return; } |
429 |
if (!fileDialog.hidden) { closeFiles(); return; } |
@@ -357,13 +471,13 @@ const pageTemplate = `<!doctype html> |
| 357 |
<a class="commit-count-link" href="{{.CommitsURL}}">{{icon "history"}}<strong>{{.CommitCount}}</strong> commits</a> |
471 |
<a class="commit-count-link" href="{{.CommitsURL}}">{{icon "history"}}<strong>{{.CommitCount}}</strong> commits</a> |
| 358 |
</div> |
472 |
</div> |
| 359 |
<div class="tree-rows"> |
473 |
<div class="tree-rows"> |
| 360 |
{{if .ParentURL}}<a class="tree-row parent-row" href="{{.ParentURL}}"><span class="file-icon">{{icon "back"}}</span><span class="file-name">..</span><span></span><span></span></a>{{end}} |
474 |
{{if .ParentURL}}<div class="tree-row parent-row"><span class="file-icon">{{icon "back"}}</span><a class="file-name" href="{{.ParentURL}}">..</a><span></span><span></span></div>{{end}} |
| 361 |
{{range .Entries}}<a class="tree-row" href="{{.URL}}"> |
475 |
{{range .Entries}}<div class="tree-row"> |
| 362 |
{{if eq .Type "tree"}}<span class="file-icon folder">{{icon "folder"}}</span>{{else}}<span class="file-icon">{{icon "file"}}</span>{{end}} |
476 |
{{if eq .Type "tree"}}<span class="file-icon folder">{{icon "folder"}}</span>{{else}}<span class="file-icon">{{icon "file"}}</span>{{end}} |
| 363 |
<span class="file-name">{{.Name}}</span> |
477 |
<a class="file-name" href="{{.URL}}">{{.Name}}</a> |
| 364 |
<span class="row-message">{{$.Commit.Message}}</span> |
478 |
<span class="row-message">{{$.Commit.Message}}</span> |
| 365 |
<span class="file-size">{{humanSize .Size}}</span> |
479 |
<span class="file-size">{{humanSize .Size}}</span> |
| 366 |
</a>{{end}} |
480 |
</div>{{end}} |
| 367 |
</div> |
481 |
</div> |
| 368 |
</div> |
482 |
</div> |
| 369 |
{{end}}` |
483 |
{{end}}` |