init
This commit is contained in:
585
index.html
Normal file
585
index.html
Normal file
@@ -0,0 +1,585 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Everest Linux - Package Repository</title>
|
||||
<link rel="icon" type="image/x-icon" href="logo.svg"/>
|
||||
<style>
|
||||
/* === Palette from k2.css === */
|
||||
:root {
|
||||
--bg: #1E1E1E;
|
||||
--bg-dark: #0F0F0F;
|
||||
--bg-darker: #131313;
|
||||
--bg-mid: #2C2C2C;
|
||||
--text: #bfbfbf;
|
||||
--text-bright: #e6e6e6;
|
||||
--blue: #4a86e8;
|
||||
--green: MediumSeaGreen;
|
||||
--purple: SlateBlue;
|
||||
--yellow: #ffcc00;
|
||||
--red: #e60000;
|
||||
}
|
||||
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
background-color: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
/* === Sidebar (verbatim k2.css) === */
|
||||
.sidenav {
|
||||
height: 100%;
|
||||
width: 200px;
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: var(--bg-darker);
|
||||
overflow-x: hidden;
|
||||
padding-top: 20px;
|
||||
}
|
||||
.sidenav img {
|
||||
display: block;
|
||||
width: 120px;
|
||||
margin: 0 auto 4px;
|
||||
padding: 8px;
|
||||
}
|
||||
.sidenav a {
|
||||
padding: 6px 8px 6px 16px;
|
||||
text-decoration: none;
|
||||
font-size: 20px;
|
||||
color: var(--text);
|
||||
display: block;
|
||||
}
|
||||
.sidenav a:hover, .sidenav a.active { color: var(--blue); }
|
||||
|
||||
/* === Hero (verbatim k2.css) === */
|
||||
.hero-image {
|
||||
background-image: linear-gradient(rgba(0,0,0,0.5), rgba(0,0,0,0.5)), url("banner.jpg");
|
||||
height: 55%;
|
||||
background-position: 50% 30%;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
position: relative;
|
||||
padding-top: 0;
|
||||
padding-right: 0;
|
||||
margin-right: -8px;
|
||||
margin-top: -8px;
|
||||
}
|
||||
.hero-text {
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: white;
|
||||
}
|
||||
.hero-text h1 { font-size: 32px; font-weight: 700; }
|
||||
.hero-text p { font-size: 15px; color: #ccc; margin-top: 6px; font-family: monospace; }
|
||||
|
||||
/* === Main === */
|
||||
.main {
|
||||
margin-left: 200px;
|
||||
padding: 0 10px;
|
||||
background-color: var(--bg);
|
||||
}
|
||||
|
||||
/* === h2 (verbatim k2.css) === */
|
||||
h2 {
|
||||
border-left: 6px solid var(--blue);
|
||||
padding-left: 4px;
|
||||
border-radius: 5px;
|
||||
color: var(--text);
|
||||
font-size: 20px;
|
||||
margin: 20px 0 8px;
|
||||
}
|
||||
h3 { color: var(--text-bright); font-size: 15px; margin: 12px 0 6px; }
|
||||
p { margin: 6px 0; }
|
||||
|
||||
/* === Callout divs (k2.css) === */
|
||||
.note { background-color: #1f2a2a; border-left: 4px solid var(--blue); border-radius: 2px; padding: 4px; width: 50%; color: var(--text); margin: 4px 0; }
|
||||
.caution { background-color: #332b00; border-left: 4px solid var(--yellow); border-radius: 2px; padding: 4px; width: 50%; color: var(--yellow); margin: 4px 0; }
|
||||
.warning { background-color: #330000; border-left: 4px solid var(--red); border-radius: 2px; padding: 4px; width: 50%; color: var(--red); margin: 4px 0; }
|
||||
.news { background-color: var(--bg-darker); border-left: 4px solid var(--blue); border-radius: 2px; padding: 4px; width: 50%; color: var(--text); margin: 4px 0; }
|
||||
.file { font-family: monospace; background-color: var(--bg-dark); border-left: 4px solid MediumSeaGreen; border-radius: 2px; padding: 4px; width: 50%; color: var(--text); margin: 4px 0; }
|
||||
.kernel { font-family: monospace; background-color: var(--bg-dark); border-left: 4px solid SlateBlue; border-radius: 2px; padding: 4px; width: 50%; color: var(--text); margin: 4px 0; }
|
||||
.bigcode { font-family: monospace; background-color: var(--bg-dark); border-left: 4px solid var(--text); border-radius: 2px; padding: 6px; width: 50%; margin: 4px 0; white-space: pre; overflow-x: auto; }
|
||||
|
||||
/* === Custom elements (k2.css) === */
|
||||
notehead { display: block; background-color: #1f2a2a; border-left: 4px solid var(--blue); border-radius: 2px; padding: 6px; width: 50%; color: var(--blue); font-weight: 600; font-size: 14px; }
|
||||
fhead { display: block; background-color: var(--bg-dark); border-left: 4px solid MediumSeaGreen; border-radius: 2px; padding: 6px; width: 50%; color: MediumSeaGreen; font-weight: 600; font-size: 14px; }
|
||||
khead { display: block; background-color: var(--bg-dark); border-left: 4px solid SlateBlue; border-radius: 2px; padding: 6px; width: 50%; color: SlateBlue; font-weight: 600; font-size: 14px; }
|
||||
cautionhead { display: block; background-color: #332b00; border-left: 4px solid var(--yellow); border-radius: 2px; padding: 6px; width: 50%; color: var(--yellow); font-weight: 600; font-size: 14px; }
|
||||
warnhead { display: block; background-color: #330000; border-left: 4px solid var(--red); border-radius: 2px; padding: 6px; width: 50%; color: var(--red); font-weight: 600; font-size: 14px; }
|
||||
bigcodehead { display: block; background-color: var(--bg-dark); border-left: 4px solid var(--text); border-radius: 2px; padding: 4px; width: 50%; color: var(--text); font-weight: 600; font-size: 14px; }
|
||||
wikihead { display: block; border-left: 4px solid Tomato; border-radius: 2px; padding: 6px; width: 50%; color: Tomato; margin: 4px 0; }
|
||||
wiki { display: block; border-left: 4px solid Tomato; border-radius: 2px; padding: 6px; width: 50%; margin: 4px 0; }
|
||||
cil { font-family: monospace; background-color: var(--bg-dark); padding: 2px; }
|
||||
|
||||
/* === Inline code (k2.css) === */
|
||||
code { font-family: monospace; background-color: var(--bg-dark); border-left: 4px solid var(--blue); border-radius: 2px; padding: 2px; color: var(--text); display: inline-block; margin: 2px 0; }
|
||||
|
||||
/* === Tables (k2.css) === */
|
||||
table { border-collapse: collapse; }
|
||||
table, td { border: 2px solid var(--text); padding: 4px; background-color: var(--bg-dark); color: var(--text); }
|
||||
th { border: 2px solid var(--text); background-color: var(--bg-mid); color: var(--text-bright); padding: 4px; text-align: left; }
|
||||
|
||||
/* === Buttons (k2.css) === */
|
||||
button {
|
||||
font-size: 16px;
|
||||
border: none;
|
||||
border-left: 8px solid var(--blue);
|
||||
padding: 8px 20px;
|
||||
border-radius: 5px;
|
||||
background-color: var(--bg-mid);
|
||||
color: var(--text-bright);
|
||||
cursor: pointer;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
button:hover { background-color: #252525; }
|
||||
|
||||
/* === HR (k2.css) === */
|
||||
hr { color: var(--blue); width: 50%; border-width: 4px; border-radius: 5px; margin-left: 0; margin: 16px 0; }
|
||||
|
||||
/* === Search (k2.css #myInput) === */
|
||||
#searchInput {
|
||||
width: 378px;
|
||||
font-size: 16px;
|
||||
padding: 10px;
|
||||
border: 2px solid var(--blue);
|
||||
border-radius: 2px;
|
||||
margin-bottom: 12px;
|
||||
background-color: var(--bg-mid);
|
||||
color: var(--text-bright);
|
||||
font-family: monospace;
|
||||
outline: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* === Tabs === */
|
||||
.tabs { display: flex; border-bottom: 2px solid var(--blue); margin-top: 14px; }
|
||||
.tab-btn {
|
||||
padding: 7px 18px;
|
||||
background: var(--bg-darker);
|
||||
color: var(--text);
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
font-size: 15px;
|
||||
cursor: pointer;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
.tab-btn.active { background: var(--bg-mid); color: var(--text-bright); border-bottom: 2px solid var(--blue); margin-bottom: -2px; }
|
||||
.tab-btn:hover { background: var(--bg-mid); }
|
||||
|
||||
.tab-panel { display: none; }
|
||||
.tab-panel.active { display: block; }
|
||||
|
||||
/* === Stats bar === */
|
||||
.stats-bar { display: flex; gap: 16px; flex-wrap: wrap; margin: 12px 0 4px; }
|
||||
.stat { background: var(--bg-darker); border-left: 4px solid var(--blue); border-radius: 2px; padding: 8px 16px; }
|
||||
.stat .stat-val { font-family: monospace; font-size: 22px; color: var(--blue); }
|
||||
.stat .stat-label { font-size: 12px; color: #777; }
|
||||
|
||||
/* === Package cards === */
|
||||
#pkgList { display: flex; flex-direction: column; gap: 6px; margin-top: 8px; }
|
||||
.pkg-card {
|
||||
font-family: monospace;
|
||||
background-color: var(--bg-dark);
|
||||
border-left: 4px solid MediumSeaGreen;
|
||||
border-radius: 2px;
|
||||
padding: 6px 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
flex-wrap: wrap;
|
||||
width: 70%;
|
||||
}
|
||||
.pkg-name { font-size: 15px; color: MediumSeaGreen; min-width: 150px; }
|
||||
.pkg-ver { font-size: 13px; color: SlateBlue; min-width: 80px; }
|
||||
.pkg-arch { font-size: 12px; color: #777; min-width: 70px; }
|
||||
.pkg-desc { flex: 1; font-size: 13px; color: var(--text); font-family: sans-serif; }
|
||||
.pkg-size { font-size: 12px; color: #666; min-width: 70px; text-align: right; }
|
||||
.pkg-dl a {
|
||||
font-size: 13px;
|
||||
padding: 2px 10px;
|
||||
border-left: 4px solid var(--blue);
|
||||
background-color: var(--bg-mid);
|
||||
color: var(--text-bright);
|
||||
text-decoration: none;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.pkg-dl a:hover { background-color: #252525; }
|
||||
|
||||
/* === Upload form === */
|
||||
.upload-form { display: flex; flex-direction: column; gap: 10px; max-width: 460px; }
|
||||
.upload-form label { font-size: 13px; color: #888; margin-bottom: 2px; display: block; }
|
||||
.upload-form input[type=text],
|
||||
.upload-form input[type=file],
|
||||
.upload-form textarea,
|
||||
.upload-form select {
|
||||
width: 100%;
|
||||
padding: 7px 10px;
|
||||
background: var(--bg-mid);
|
||||
border: 2px solid #333;
|
||||
border-radius: 2px;
|
||||
color: var(--text-bright);
|
||||
font-family: monospace;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
}
|
||||
.upload-form input:focus,
|
||||
.upload-form textarea:focus,
|
||||
.upload-form select:focus { border-color: var(--blue); }
|
||||
.upload-form textarea { resize: vertical; min-height: 60px; }
|
||||
#statusMsg { margin-top: 6px; font-family: monospace; font-size: 14px; }
|
||||
|
||||
/* === Footer === */
|
||||
footer { text-align: center; font-size: 10px; padding: 20px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Navbar -->
|
||||
<div class="sidenav">
|
||||
<img src="logo.svg" alt="Everest Linux">
|
||||
<a href="index.html" class="active">Packages</a>
|
||||
<a href="https://everestlinux.org">Home</a>
|
||||
<a href="https://everestlinux.org/about.html">About</a>
|
||||
<a href="https://everestlinux.org/install.html">Install</a>
|
||||
<a href="https://everestlinux.org/download.html">Downloads</a>
|
||||
<a href="https://everestlinux.org/docs/home.html">Documentation</a>
|
||||
<a href="https://git.everestlinux.org">Git ↗</a>
|
||||
</div>
|
||||
|
||||
<!-- Hero -->
|
||||
<div class="hero-image">
|
||||
<div class="hero-text">
|
||||
<h1>Package Repository</h1>
|
||||
<p>glacier package host</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main content -->
|
||||
<div class="main">
|
||||
|
||||
<div class="stats-bar" id="statsBar">
|
||||
<div class="stat"><div class="stat-val" id="statTotal">-</div><div class="stat-label">Total Packages</div></div>
|
||||
<div class="stat"><div class="stat-val" id="statSections">-</div><div class="stat-label">Sections</div></div>
|
||||
<div class="stat"><div class="stat-val" id="statSize">-</div><div class="stat-label">Total Size</div></div>
|
||||
</div>
|
||||
|
||||
<div class="tabs">
|
||||
<button class="tab-btn active" id="tab-browse" onclick="showTab('browse')">Browse Packages</button>
|
||||
<button class="tab-btn" id="tab-upload" onclick="showTab('upload')">Upload</button>
|
||||
<button class="tab-btn" id="tab-api" onclick="showTab('api')">API Docs</button>
|
||||
<button class="tab-btn" id="tab-about" onclick="showTab('about')">About</button>
|
||||
</div>
|
||||
|
||||
<!-- ===== BROWSE ===== -->
|
||||
<div class="tab-panel active" id="panel-browse">
|
||||
<h2>Browse Packages</h2>
|
||||
<input type="text" id="searchInput" placeholder="Search packages..." oninput="filterPackages()">
|
||||
<div id="pkgList">
|
||||
<div class="note"><p>Loading packages...</p></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== UPLOAD ===== -->
|
||||
<div class="tab-panel" id="panel-upload">
|
||||
<h2>Upload a Package</h2>
|
||||
<p>Upload a compiled package archive to the repository.</p>
|
||||
<notehead><strong>NOTE:</strong></notehead>
|
||||
<div class="note">
|
||||
<p>Supported formats: <code>.tar.gz</code>, <code>.tar.xz</code>, <code>.epkg</code></p>
|
||||
<p>Uploading a package with an existing name will overwrite the previous entry.</p>
|
||||
</div>
|
||||
<p></p>
|
||||
<div class="upload-form">
|
||||
<div>
|
||||
<label>Package Name *</label>
|
||||
<input type="text" id="upName" placeholder="e.g. bash">
|
||||
</div>
|
||||
<div>
|
||||
<label>Version *</label>
|
||||
<input type="text" id="upVersion" placeholder="e.g. 5.2.0">
|
||||
</div>
|
||||
<div>
|
||||
<label>Description</label>
|
||||
<textarea id="upDesc" placeholder="Short description of the package"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label>Architecture</label>
|
||||
<select id="upArch">
|
||||
<option value="x86_64">x86_64</option>
|
||||
<option value="aarch64">aarch64</option>
|
||||
<option value="any">any</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label>Section</label>
|
||||
<select id="upSection">
|
||||
<option value="base">base</option>
|
||||
<option value="core">core</option>
|
||||
<option value="extra">extra</option>
|
||||
<option value="community">community</option>
|
||||
<option value="misc">misc</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label>Package File *</label>
|
||||
<input type="file" id="upFile">
|
||||
</div>
|
||||
<div>
|
||||
<button onclick="uploadPackage()">Upload Package</button>
|
||||
</div>
|
||||
<div id="statusMsg"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== API DOCS ===== -->
|
||||
<div class="tab-panel" id="panel-api">
|
||||
<h2>API Reference</h2>
|
||||
<p>The repository exposes a REST API for use with the <code>glacier</code> package manager or any HTTP client.</p>
|
||||
|
||||
<h2>Endpoints</h2>
|
||||
|
||||
<notehead><strong>GET</strong> /api/packages</notehead>
|
||||
<div class="note">
|
||||
<p>Returns a JSON array of all packages in the repository.</p>
|
||||
</div>
|
||||
<p></p>
|
||||
|
||||
<notehead><strong>GET</strong> /api/packages/search?q=term</notehead>
|
||||
<div class="note">
|
||||
<p>Search packages by name or description. Returns a filtered JSON array.</p>
|
||||
</div>
|
||||
<p></p>
|
||||
|
||||
<notehead><strong>GET</strong> /api/packages/:name</notehead>
|
||||
<div class="note">
|
||||
<p>Returns metadata for a single package by name.</p>
|
||||
</div>
|
||||
<p></p>
|
||||
|
||||
<fhead><strong>POST</strong> /api/packages</fhead>
|
||||
<div class="file">
|
||||
<p>Upload a new package. Send request body as <cil>multipart/form-data</cil>.</p>
|
||||
<p>Required fields: <cil>name</cil>, <cil>version</cil>, <cil>file</cil></p>
|
||||
<p>Optional fields: <cil>description</cil>, <cil>arch</cil>, <cil>section</cil></p>
|
||||
</div>
|
||||
<p></p>
|
||||
|
||||
<khead><strong>DELETE</strong> /api/packages/:name</khead>
|
||||
<div class="kernel">
|
||||
<p>Delete a package by name. Removes both the archive file and its metadata entry.</p>
|
||||
</div>
|
||||
<p></p>
|
||||
|
||||
<notehead><strong>GET</strong> /packages/:filename</notehead>
|
||||
<div class="note">
|
||||
<p>Direct download URL for a package archive. Served as static content.</p>
|
||||
</div>
|
||||
|
||||
<h2>Usage</h2>
|
||||
|
||||
<p>Sync the package database and install a package with glacier:</p>
|
||||
<code>(root)# glacier sync</code>
|
||||
<p></p>
|
||||
<code>(root)# glacier install <package-name></code>
|
||||
|
||||
<p>Upload a package with curl:</p>
|
||||
<bigcodehead><strong>COMMAND:</strong> Upload via curl</bigcodehead>
|
||||
<div class="bigcode">curl -X POST https://your-repo.vercel.app/api/packages \
|
||||
-F "name=bash" \
|
||||
-F "version=5.2.0" \
|
||||
-F "description=The GNU Bourne Again shell" \
|
||||
-F "arch=x86_64" \
|
||||
-F "section=base" \
|
||||
-F "file=@bash-5.2.0.tar.gz"</div>
|
||||
|
||||
<p>Example JSON response for a package entry:</p>
|
||||
<bigcodehead><strong>RESPONSE:</strong> Package metadata</bigcodehead>
|
||||
<div class="bigcode">{
|
||||
"name": "bash",
|
||||
"version": "5.2.0",
|
||||
"description": "The GNU Bourne Again shell",
|
||||
"arch": "x86_64",
|
||||
"section": "base",
|
||||
"filename": "bash-5.2.0.tar.gz",
|
||||
"size": 4194304,
|
||||
"uploadedAt": "2025-01-01T00:00:00.000Z",
|
||||
"downloadUrl": "/packages/bash-5.2.0.tar.gz"
|
||||
}</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== ABOUT ===== -->
|
||||
<div class="tab-panel" id="panel-about">
|
||||
<h2>About This Repository</h2>
|
||||
<p>This is the official EverestLinux package repository, hosting compiled packages for the <code>glacier</code> package manager.</p>
|
||||
<p></p>
|
||||
<notehead><strong>NOTE:</strong></notehead>
|
||||
<div class="note">
|
||||
<p>This repository is self-hostable. Fork the project, deploy to Vercel, and point <code>glacier</code> at your instance URL.</p>
|
||||
</div>
|
||||
|
||||
<h2>Repository Sections</h2>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Section</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
<tr><td><cil>base</cil></td><td>Essential system packages required for a minimal installation</td></tr>
|
||||
<tr><td><cil>core</cil></td><td>Core utilities and libraries</td></tr>
|
||||
<tr><td><cil>extra</cil></td><td>Additional packages not included in core</td></tr>
|
||||
<tr><td><cil>community</cil></td><td>Community-maintained packages</td></tr>
|
||||
<tr><td><cil>misc</cil></td><td>Miscellaneous packages</td></tr>
|
||||
</table>
|
||||
|
||||
<h2>Configuring glacier</h2>
|
||||
<p>Point glacier at this repository by editing the repos config file:</p>
|
||||
<fhead><strong>FILE:</strong> /etc/glacier/repos.conf</fhead>
|
||||
<div class="file">
|
||||
<p>[everest]</p>
|
||||
<p>url = https://your-repo.vercel.app</p>
|
||||
<p>enabled = yes</p>
|
||||
</div>
|
||||
<p>Then run:</p>
|
||||
<code>(root)# glacier sync</code>
|
||||
|
||||
<h2>Persistent Storage</h2>
|
||||
<cautionhead><strong>CAUTION:</strong></cautionhead>
|
||||
<div class="caution">
|
||||
<p>Vercel's serverless filesystem is ephemeral. Uploaded packages will not survive redeployments.</p>
|
||||
<p>For production use, replace the file storage backend with an S3-compatible service such as Cloudflare R2 or AWS S3, and the metadata store with a hosted database.</p>
|
||||
</div>
|
||||
<p></p>
|
||||
<wikihead><strong>WIKI</strong>: <a href="https://git.everestlinux.org/EverestLinux/wiki/wiki/Introduction-to-Glacier">Introduction to Glacier</a></wikihead>
|
||||
<p></p>
|
||||
<hr>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<p>Copyright (C) 2021-2025 Everest Linux</p>
|
||||
<p>Linux (R) is a registered trademark of Linus Torvalds.</p>
|
||||
<p>Everest Linux is provided AS IS, WITHOUT WARRANTY.</p>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
function showTab(name) {
|
||||
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
|
||||
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
||||
document.getElementById('panel-' + name).classList.add('active');
|
||||
document.getElementById('tab-' + name).classList.add('active');
|
||||
}
|
||||
|
||||
let allPackages = [];
|
||||
|
||||
async function loadPackages() {
|
||||
try {
|
||||
const res = await fetch('/api/packages');
|
||||
allPackages = await res.json();
|
||||
renderPackages(allPackages);
|
||||
updateStats(allPackages);
|
||||
} catch (e) {
|
||||
document.getElementById('pkgList').innerHTML =
|
||||
'<div class="warning"><p>Could not connect to package API. Is the server running?</p></div>';
|
||||
}
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (!bytes) return '?';
|
||||
if (bytes < 1024) return bytes + ' B';
|
||||
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
|
||||
return (bytes / 1048576).toFixed(1) + ' MB';
|
||||
}
|
||||
|
||||
function renderPackages(pkgs) {
|
||||
const list = document.getElementById('pkgList');
|
||||
if (!pkgs.length) {
|
||||
list.innerHTML = '<div class="note"><p>No packages found.</p></div>';
|
||||
return;
|
||||
}
|
||||
list.innerHTML = pkgs.map(p => `
|
||||
<div class="pkg-card">
|
||||
<span class="pkg-name">${esc(p.name)}</span>
|
||||
<span class="pkg-ver">${esc(p.version)}</span>
|
||||
<span class="pkg-arch">[${esc(p.arch)}]</span>
|
||||
<span class="pkg-desc">${esc(p.description || '-')}</span>
|
||||
<span class="pkg-size">${formatBytes(p.size)}</span>
|
||||
<span class="pkg-dl"><a href="${esc(p.downloadUrl)}" download>Download</a></span>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function updateStats(pkgs) {
|
||||
const sections = new Set(pkgs.map(p => p.section)).size;
|
||||
const totalSize = pkgs.reduce((a, p) => a + (p.size || 0), 0);
|
||||
document.getElementById('statTotal').textContent = pkgs.length;
|
||||
document.getElementById('statSections').textContent = sections || '-';
|
||||
document.getElementById('statSize').textContent = formatBytes(totalSize);
|
||||
}
|
||||
|
||||
function filterPackages() {
|
||||
const q = document.getElementById('searchInput').value.toLowerCase();
|
||||
renderPackages(allPackages.filter(p =>
|
||||
p.name.toLowerCase().includes(q) ||
|
||||
(p.description || '').toLowerCase().includes(q) ||
|
||||
(p.section || '').toLowerCase().includes(q)
|
||||
));
|
||||
}
|
||||
|
||||
function esc(s) {
|
||||
return String(s)
|
||||
.replace(/&/g,'&').replace(/</g,'<')
|
||||
.replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
async function uploadPackage() {
|
||||
const name = document.getElementById('upName').value.trim();
|
||||
const version = document.getElementById('upVersion').value.trim();
|
||||
const desc = document.getElementById('upDesc').value.trim();
|
||||
const arch = document.getElementById('upArch').value;
|
||||
const section = document.getElementById('upSection').value;
|
||||
const file = document.getElementById('upFile').files[0];
|
||||
const status = document.getElementById('statusMsg');
|
||||
|
||||
if (!name || !version || !file) {
|
||||
status.innerHTML = '<span style="color:var(--red);">[x] Name, version, and file are required.</span>';
|
||||
return;
|
||||
}
|
||||
status.innerHTML = '<span style="color:#888;">Uploading...</span>';
|
||||
|
||||
const fd = new FormData();
|
||||
fd.append('name', name);
|
||||
fd.append('version', version);
|
||||
fd.append('description', desc);
|
||||
fd.append('arch', arch);
|
||||
fd.append('section', section);
|
||||
fd.append('file', file);
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/packages', { method: 'POST', body: fd });
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
status.innerHTML = `<span style="color:MediumSeaGreen;">[+] Package '${esc(data.name)}-${esc(data.version)}' uploaded successfully.</span>`;
|
||||
loadPackages();
|
||||
['upName','upVersion','upDesc'].forEach(id => document.getElementById(id).value = '');
|
||||
document.getElementById('upFile').value = '';
|
||||
} else {
|
||||
status.innerHTML = `<span style="color:var(--red);">[x] Error: ${esc(data.error)}</span>`;
|
||||
}
|
||||
} catch (e) {
|
||||
status.innerHTML = `<span style="color:var(--red);">[x] Network error: ${esc(e.message)}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
loadPackages();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user