git repos / sago_web_git

commit b3dc7f09

Poul Sander · 2026-06-27 13:41
b3dc7f091b847fa69d7e1f25a269779f6936753b patch · browse files
parent 1ce3296d568bb28496fbf0c2b8ab5dd149d810f8

Allow us to browse as files instead of a tree. Previes images.

Changed files

M src/git_repo.cpp before
M src/git_repo.h before
M static/app.js before
M static/style.css before
M templates/skin.tmpl before
diff --git a/src/git_repo.cpp b/src/git_repo.cpp index 9e720e0..e7f898a 100644 --- a/src/git_repo.cpp +++ b/src/git_repo.cpp
@@ -358,6 +358,8 @@ std::vector<TreeEntry> GitRepo::list_tree(const std::string &commit_oid,
TreeEntry e = make_entry(git_tree_entry_byindex(tree, i), clean, repo_);
if (e.is_dir)
collapse_single_child_dirs(repo_, e);
+ e.is_image =
+ !e.is_dir && !e.is_submodule && util::image_mime_type(e.name).length() > 0;
entries.push_back(std::move(e));
}
diff --git a/src/git_repo.h b/src/git_repo.h index f364cf0..6c165a5 100644 --- a/src/git_repo.h +++ b/src/git_repo.h
@@ -22,6 +22,7 @@ struct TreeEntry {
bool is_dir = false; // tree
bool is_submodule = false;
uint64_t size = 0; // blob size in bytes (0 for non-blobs)
+ bool is_image = false; // blob whose name has an image extension
};
struct CommitInfo {
diff --git a/static/app.js b/static/app.js index eafa089..bd6b21f 100644 --- a/static/app.js +++ b/static/app.js
@@ -77,9 +77,34 @@
});
}
+ // Toggle between the file list table and a thumbnail grid. The choice is
+ // remembered in localStorage so it persists while browsing into subfolders.
+ function setupViewToggle() {
+ var btn = document.querySelector(".view-toggle");
+ if (!btn) return;
+ var table = document.querySelector("table.tree");
+ var grid = document.querySelector(".tree-grid");
+ if (!table || !grid) return;
+
+ function apply(grid_on) {
+ btn.setAttribute("aria-pressed", grid_on ? "true" : "false");
+ btn.textContent = "view: " + (grid_on ? "grid" : "list");
+ table.hidden = grid_on;
+ grid.hidden = !grid_on;
+ }
+
+ apply(localStorage.getItem("treeView") === "grid");
+ btn.addEventListener("click", function () {
+ var grid_on = btn.getAttribute("aria-pressed") !== "true";
+ localStorage.setItem("treeView", grid_on ? "grid" : "list");
+ apply(grid_on);
+ });
+ }
+
document.addEventListener("DOMContentLoaded", function () {
highlight();
renderMarkdown();
setupLogToggle();
+ setupViewToggle();
});
})();
diff --git a/static/style.css b/static/style.css index 23933bb..0ed38b1 100644 --- a/static/style.css +++ b/static/style.css
@@ -54,6 +54,17 @@ table.tree .size { text-align: right; }
.dir { font-weight: 600; }
.submodule { color: var(--muted); }
+.tree-controls { margin: 0 0 0.75rem; }
+.view-toggle { font-size: 0.85rem; color: var(--fg); background: var(--code-bg); border: 1px solid var(--border); border-radius: 4px; padding: 0.25rem 0.6rem; cursor: pointer; }
+.view-toggle[aria-pressed="true"] { border-color: var(--link); color: var(--link); }
+
+.tree-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 0.75rem; }
+.tree-grid[hidden] { display: none; }
+.tree-grid .tile { display: flex; flex-direction: column; align-items: center; justify-content: flex-end; gap: 0.4rem; padding: 0.6rem; border: 1px solid var(--border); border-radius: 6px; text-align: center; word-break: break-word; }
+.tree-grid .tile-icon { font-size: 1.8rem; line-height: 1; }
+.tree-grid .tile-name { font-size: 0.85rem; }
+.tree-grid img.thumb { max-width: 100%; max-height: 120px; object-fit: contain; }
+
pre.code {
background: var(--code-bg);
border: 1px solid var(--border);
diff --git a/templates/skin.tmpl b/templates/skin.tmpl index dd426ec..23eb34c 100644 --- a/templates/skin.tmpl +++ b/templates/skin.tmpl
@@ -100,6 +100,9 @@
<% if (content.tags.size() > 0) %>
<p class="tags">tags: <% foreach t in tags %><% item %><a href="/repos/<%= repo %>/tags/<%= t %>"><%= t %></a> <% end item %><% end foreach %></p>
<% end %>
+<% if (content.entries.size() > 0) %>
+<p class="tree-controls"><button type="button" class="view-toggle" aria-pressed="false">view: list</button></p>
+<% end %>
<table class="tree">
<% if (not content.is_root) %>
<tr><td class="name"><a href="/repos/<%= repo %>/tree/<%= parent_path %><%= query %>">..</a></td><td></td></tr>
@@ -123,6 +126,28 @@
<tr><td>empty tree</td></tr>
<% end %>
</table>
+<div class="tree-grid" hidden>
+<% if (not content.is_root) %>
+<a class="tile dir" href="/repos/<%= repo %>/tree/<%= parent_path %><%= query %>"><span class="tile-icon">&#128193;</span><span class="tile-name">..</span></a>
+<% end %>
+<% foreach e in entries %><% item %>
+<% if e.is_dir %>
+<a class="tile dir" href="/repos/<%= repo %>/tree/<%= e.path %><%= query %>"><span class="tile-icon">&#128193;</span><span class="tile-name"><%= e.name %>/</span></a>
+<% else %>
+<% if e.is_submodule %>
+<span class="tile submodule"><span class="tile-icon">&#128279;</span><span class="tile-name"><%= e.name %></span></span>
+<% else %>
+<a class="tile" href="/repos/<%= repo %>/tree/<%= e.path %><%= query %>">
+<% if e.is_image %>
+<img class="thumb" loading="lazy" src="/repos/<%= repo %>/raw/<%= e.path %><%= query %>" alt="<%= e.name %>">
+<% else %>
+<span class="tile-icon">&#128196;</span>
+<% end %>
+<span class="tile-name"><%= e.name %></span></a>
+<% end %>
+<% end %>
+<% end item %><% end %>
+</div>
<% end template %>
<% end view %>