New stuff.
This commit is contained in:
		
							parent
							
								
									17c6124a57
								
							
						
					
					
						commit
						4c34d7eda5
					
				
							
								
								
									
										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