New stuff.
This commit is contained in:
parent
17c6124a57
commit
4c34d7eda5
src/snek
41
src/snek/static/file-manager.css
Normal file
41
src/snek/static/file-manager.css
Normal file
@ -0,0 +1,41 @@
|
||||
.file-manager {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
gap: 16px;
|
||||
padding: 20px;
|
||||
background: #111;
|
||||
color: #ddd;
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.file-tile {
|
||||
background-color: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.file-tile:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
.file-icon {
|
||||
font-size: 40px;
|
||||
margin-bottom: 10px;
|
||||
color: #888;
|
||||
}
|
||||
.file-name {
|
||||
font-size: 14px;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
.file-tile img {
|
||||
max-width: 80%;
|
||||
height: auto;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
|
100
src/snek/static/file-manager.js
Normal file
100
src/snek/static/file-manager.js
Normal file
@ -0,0 +1,100 @@
|
||||
/* A <file-browser> custom element that talks to /api/files */
|
||||
class FileBrowser extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: "open" });
|
||||
this.path = ""; // current virtual path ("" = ROOT)
|
||||
this.offset = 0; // pagination offset
|
||||
this.limit = 40; // items per request
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.renderShell();
|
||||
this.load();
|
||||
}
|
||||
|
||||
// ---------- UI scaffolding -------------------------------------------
|
||||
renderShell() {
|
||||
this.shadowRoot.innerHTML = `
|
||||
<style>
|
||||
:host { display:block; font-family: system-ui, sans-serif; box-sizing: border-box; }
|
||||
nav { display:flex; flex-wrap:wrap; gap:.5rem; margin:.5rem 0; align-items:center; }
|
||||
button { padding:.35rem .65rem; border:none; border-radius:4px; background:#0074d9; color:#fff; cursor:pointer; font:inherit; }
|
||||
button:disabled { background:#999; cursor:not-allowed; }
|
||||
.crumb { font-weight:600; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||
.grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(120px,1fr)); gap:1rem; }
|
||||
.tile { border:1px solid #ddd; border-radius:8px; padding:.5rem; background:#fafafa; text-align:center; cursor:pointer; transition:box-shadow .2s ease; }
|
||||
.tile:hover { box-shadow:0 2px 8px rgba(0,0,0,.1); }
|
||||
img.thumb { width:100%; height:90px; object-fit:cover; border-radius:6px; }
|
||||
.icon { font-size:48px; line-height:90px; }
|
||||
</style>
|
||||
|
||||
<nav>
|
||||
<button id="up">⬅️ Up</button>
|
||||
<span class="crumb" id="crumb"></span>
|
||||
</nav>
|
||||
<div class="grid" id="grid"></div>
|
||||
<nav>
|
||||
<button id="prev">Prev</button>
|
||||
<button id="next">Next</button>
|
||||
</nav>
|
||||
`;
|
||||
this.shadowRoot.getElementById("up").addEventListener("click", () => this.goUp());
|
||||
this.shadowRoot.getElementById("prev").addEventListener("click", () => {
|
||||
if (this.offset > 0) { this.offset -= this.limit; this.load(); }
|
||||
});
|
||||
this.shadowRoot.getElementById("next").addEventListener("click", () => {
|
||||
this.offset += this.limit; this.load();
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- Networking ----------------------------------------------
|
||||
async load() {
|
||||
const r = await fetch(`/drive.json?path=${encodeURIComponent(this.path)}&offset=${this.offset}&limit=${this.limit}`);
|
||||
if (!r.ok) { console.error(await r.text()); return; }
|
||||
const data = await r.json();
|
||||
this.renderTiles(data.items);
|
||||
this.updateNav(data.pagination);
|
||||
}
|
||||
|
||||
// ---------- Rendering -------------------------------------------------
|
||||
renderTiles(items) {
|
||||
const grid = this.shadowRoot.getElementById("grid");
|
||||
grid.innerHTML = "";
|
||||
items.forEach(item => {
|
||||
const tile = document.createElement("div");
|
||||
tile.className = "tile";
|
||||
|
||||
if (item.type === "directory") {
|
||||
tile.innerHTML = `<div class="icon">📂</div><div>${item.name}</div>`;
|
||||
tile.addEventListener("click", () => { this.path = item.path; this.offset = 0; this.load(); });
|
||||
} else {
|
||||
if (item.mimetype?.startsWith("image/")) {
|
||||
tile.innerHTML = `<img class="thumb" src="${item.url}" alt="${item.name}"><div>${item.name}</div>`;
|
||||
} else {
|
||||
tile.innerHTML = `<div class="icon">📄</div><div>${item.name}</div>`;
|
||||
}
|
||||
tile.addEventListener("click", () => window.open(item.url, "_blank"));
|
||||
}
|
||||
|
||||
grid.appendChild(tile);
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- Navigation + pagination ----------------------------------
|
||||
updateNav({ offset, limit, total }) {
|
||||
this.shadowRoot.getElementById("crumb").textContent = `/${this.path}`;
|
||||
this.shadowRoot.getElementById("prev").disabled = offset === 0;
|
||||
this.shadowRoot.getElementById("next").disabled = offset + limit >= total;
|
||||
this.shadowRoot.getElementById("up").disabled = this.path === "";
|
||||
}
|
||||
|
||||
goUp() {
|
||||
if (!this.path) return;
|
||||
this.path = this.path.split("/").slice(0, -1).join("/");
|
||||
this.offset = 0;
|
||||
this.load();
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("file-manager", FileBrowser);
|
82
src/snek/templates/repository.html
Normal file
82
src/snek/templates/repository.html
Normal file
@ -0,0 +1,82 @@
|
||||
{% extends "app.html" %}
|
||||
{% block header_text %}{{rel_path}}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
|
||||
<style>
|
||||
|
||||
.file-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #1c1c1c;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid #333;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.file-item:hover {
|
||||
background: #2a2a2a;
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
flex: 0 0 30px;
|
||||
text-align: center;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
flex: 1;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.file-type, .file-size {
|
||||
flex: 0 0 auto;
|
||||
margin-left: 1rem;
|
||||
font-size: 0.9rem;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.file-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.file-type, .file-size {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<div class="container">
|
||||
<div class="file-list">
|
||||
{% for file in files %}
|
||||
<a href="/repository/{{username}}/{{repo_name}}/{{ file.path }}" class="file-item">
|
||||
<div class="file-icon">
|
||||
{% if file.type == 'tree' %}
|
||||
<i class="fa fa-folder"></i>
|
||||
{% else %}
|
||||
<i class="fa fa-file"></i>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="file-name">{{ file.name }}</div>
|
||||
<div class="file-type">{{ file.type }}</div>
|
||||
<div class="file-size">{{ file.size }} B</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
15
src/snek/view/repository.py
Normal file
15
src/snek/view/repository.py
Normal file
@ -0,0 +1,15 @@
|
||||
from snek.system.view import BaseView
|
||||
from aiohttp import web
|
||||
class RepositoryView(BaseView):
|
||||
async def get(A):
|
||||
G='type';H='name';I='.git';J='username';B=A.request.match_info[J];K=A.request.match_info['repo_name'];C=A.request.match_info.get('rel_path','')
|
||||
if not B.count('-')==4:E=await A.services.user.get_by_username(B)
|
||||
else:E=await A.services.user.get(B)
|
||||
if not E:return web.HTTPNotFound()
|
||||
B=E[J];M=await A.services.user.get_repository_path(E['uid'])
|
||||
if C.endswith(I):C=C[:-4]
|
||||
L=M.joinpath(K+I)
|
||||
if not L.exists():return web.HTTPNotFound()
|
||||
import os;from git import Repo;N=Repo(L.joinpath(C));F=[];O=[];P=N.head.commit
|
||||
for D in P.tree.traverse():F.append({H:D.name,'mode':D.mode,G:D.type,'path':D.path,'size':D.size})
|
||||
sorted(F,key=lambda x:x[H]);sorted(F,key=lambda x:x[G],reverse=True);Q=f"{B}/{C}"[:-4];return await A.render_template('repository.html',dict(username=B,repo_name=K,rel_path=C,full_path=Q,files=F,directories=O))
|
Loading…
Reference in New Issue
Block a user