Erster Commit
This commit is contained in:
+450
@@ -0,0 +1,450 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Admin Panel</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
background: #111;
|
||||
color: #eee;
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 20px;
|
||||
}
|
||||
h1 { margin-top: 0; }
|
||||
h2 { border-bottom: 2px solid #444; padding-bottom: 6px; margin-top: 40px; }
|
||||
|
||||
.panel {
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 10px;
|
||||
}
|
||||
th, td {
|
||||
text-align: left;
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid #333;
|
||||
font-size: 14px;
|
||||
}
|
||||
th { color: #aaa; font-weight: normal; }
|
||||
|
||||
input, select {
|
||||
background: #222;
|
||||
border: 1px solid #444;
|
||||
color: #eee;
|
||||
padding: 6px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
button {
|
||||
background: #2c7a3d;
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
button.danger { background: #a83232; }
|
||||
button:hover { opacity: 0.85; }
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.shop-card {
|
||||
background: #202020;
|
||||
border: 1px solid #333;
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.shop-card h3 { margin: 0 0 6px 0; }
|
||||
.shop-meta { color: #888; font-size: 13px; margin-bottom: 10px; }
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
background: #333;
|
||||
border-radius: 4px;
|
||||
padding: 2px 6px;
|
||||
font-size: 12px;
|
||||
color: #ccc;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.msg {
|
||||
font-size: 13px;
|
||||
color: #6fbf73;
|
||||
min-height: 18px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<a href="/index.html" style="color:#6fbf73; text-decoration:none; font-size:14px;">← zurück zur Startseite</a>
|
||||
<h1>🎮 Admin Panel</h1>
|
||||
|
||||
<!-- ITEMS -->
|
||||
<div class="panel">
|
||||
<h2>Items</h2>
|
||||
<div class="msg" id="itemMsg"></div>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th><th>Name</th><th>Typ</th><th>Restore</th><th>Model</th><th>Schaden</th><th>Reichweite</th><th>Gewicht</th><th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="itemsTableBody"></tbody>
|
||||
</table>
|
||||
|
||||
<h3>Item hinzufügen / bearbeiten</h3>
|
||||
<div class="form-row">
|
||||
<input id="itemId" placeholder="ID (z.B. burger)">
|
||||
<input id="itemName" placeholder="Name (z.B. Burger)">
|
||||
<select id="itemType">
|
||||
<option value="item">item</option>
|
||||
<option value="food">food</option>
|
||||
<option value="drink">drink</option>
|
||||
<option value="heal">heal</option>
|
||||
<option value="fuel">fuel (Benzinkanister - Restore = Liter)</option>
|
||||
<option value="car">car</option>
|
||||
<option value="weapon_melee">weapon_melee (Nahkampf)</option>
|
||||
<option value="weapon_ranged">weapon_ranged (Fernkampf)</option>
|
||||
</select>
|
||||
<input id="itemRestore" type="number" placeholder="Restore (optional)" style="width:150px;">
|
||||
<input id="itemModel" placeholder="Model (nur bei car, z.B. sedan)">
|
||||
<input id="itemDamage" type="number" placeholder="Schaden (nur bei Waffen)" style="width:170px;">
|
||||
<input id="itemWeaponRange" type="number" placeholder="Reichweite px (nur ranged)" style="width:190px;">
|
||||
<input id="itemWeight" type="number" step="0.1" placeholder="Gewicht (kg)" style="width:130px;">
|
||||
<button onclick="saveItem()">Speichern</button>
|
||||
</div>
|
||||
<div style="color:#888; font-size:12px; margin-top:6px;">
|
||||
Nahkampf-Reichweite ist fest ~45px, "Reichweite" gilt nur für weapon_ranged.
|
||||
Gewicht: Standard 1.0kg, max. Traglast pro Spieler ist eine Server-Einstellung.
|
||||
Tipp: gleiche ID erneut speichern = Item wird aktualisiert (Preis wird pro Shop separat gesetzt, nicht hier).
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SHOPS -->
|
||||
<div class="panel">
|
||||
<h2>Shops</h2>
|
||||
<div class="msg" id="shopMsg"></div>
|
||||
|
||||
<h3>Neuen Shop anlegen</h3>
|
||||
<div class="form-row">
|
||||
<input id="newShopName" placeholder="Shop-Name">
|
||||
<input id="newShopWorld" placeholder="Welt (z.B. stadt)">
|
||||
<input id="newShopX" type="number" placeholder="X (Tile)" style="width:100px;">
|
||||
<input id="newShopY" type="number" placeholder="Y (Tile)" style="width:100px;">
|
||||
<button onclick="createShop()">Shop anlegen</button>
|
||||
</div>
|
||||
|
||||
<div id="shopsContainer"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// -------------------------------------------------------------
|
||||
// ZUGRIFFSSCHUTZ: nur eingeloggte Admins dürfen diese Seite nutzen
|
||||
// -------------------------------------------------------------
|
||||
const token = localStorage.getItem("token");
|
||||
// Voll-Admin ODER passende Gruppen-Berechtigung reicht jetzt aus - der
|
||||
// Server prüft das bei jedem Aufruf ohnehin final ab (siehe requireAdmin);
|
||||
// hier reicht ein simpler Login-Check, authFetch() fängt fehlende Rechte
|
||||
// beim ersten echten API-Aufruf sauber ab (Meldung + Weiterleitung)
|
||||
let isAdmin = localStorage.getItem("isAdmin") === "true";
|
||||
|
||||
if (!token) {
|
||||
alert("Bitte zuerst einloggen.");
|
||||
location.href = "/index.html";
|
||||
}
|
||||
|
||||
async function authFetch(url, options = {}) {
|
||||
options.headers = { ...(options.headers || {}), "Authorization": "Bearer " + token };
|
||||
const res = await fetch(url, options);
|
||||
if (res.status === 401 || res.status === 403) {
|
||||
alert("Sitzung abgelaufen oder kein Admin-Zugriff. Bitte erneut einloggen.");
|
||||
location.href = "/index.html";
|
||||
throw new Error("Nicht autorisiert");
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
let allItems = [];
|
||||
let allShops = [];
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// ITEMS
|
||||
// -------------------------------------------------------------
|
||||
async function loadItems() {
|
||||
const res = await authFetch("/api/admin/items");
|
||||
const data = await res.json();
|
||||
allItems = data.items || [];
|
||||
renderItemsTable();
|
||||
renderShopItemDropdowns();
|
||||
}
|
||||
|
||||
function renderItemsTable() {
|
||||
const body = document.getElementById("itemsTableBody");
|
||||
body.innerHTML = "";
|
||||
|
||||
allItems.forEach(item => {
|
||||
const tr = document.createElement("tr");
|
||||
tr.innerHTML = `
|
||||
<td>${item.id}</td>
|
||||
<td>${item.name}</td>
|
||||
<td>${item.type}</td>
|
||||
<td>${item.restore ?? 0}</td>
|
||||
<td>${item.model ?? "-"}</td>
|
||||
<td>${item.damage ? item.damage : "-"}</td>
|
||||
<td>${item.weapon_range ? item.weapon_range : "-"}</td>
|
||||
<td>${item.weight ?? 1}kg</td>
|
||||
<td>
|
||||
<button onclick="editItem('${item.id}')">Bearbeiten</button>
|
||||
<button class="danger" onclick="deleteItem('${item.id}')">Löschen</button>
|
||||
</td>
|
||||
`;
|
||||
body.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
function editItem(id) {
|
||||
const item = allItems.find(i => i.id === id);
|
||||
if (!item) return;
|
||||
|
||||
document.getElementById("itemId").value = item.id;
|
||||
document.getElementById("itemName").value = item.name;
|
||||
document.getElementById("itemType").value = item.type;
|
||||
document.getElementById("itemRestore").value = item.restore ?? "";
|
||||
document.getElementById("itemModel").value = item.model ?? "";
|
||||
document.getElementById("itemDamage").value = item.damage ?? "";
|
||||
document.getElementById("itemWeaponRange").value = item.weapon_range ?? "";
|
||||
document.getElementById("itemWeight").value = item.weight ?? "";
|
||||
}
|
||||
|
||||
async function saveItem() {
|
||||
const id = document.getElementById("itemId").value.trim();
|
||||
const name = document.getElementById("itemName").value.trim();
|
||||
const type = document.getElementById("itemType").value;
|
||||
const restore = Number(document.getElementById("itemRestore").value) || 0;
|
||||
const model = document.getElementById("itemModel").value.trim() || null;
|
||||
const damage = Number(document.getElementById("itemDamage").value) || 0;
|
||||
const weaponRange = Number(document.getElementById("itemWeaponRange").value) || 0;
|
||||
const weight = Number(document.getElementById("itemWeight").value) || 1;
|
||||
|
||||
if (!id || !name) {
|
||||
showMsg("itemMsg", "ID und Name sind Pflicht.", true);
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await authFetch("/api/admin/items", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ id, name, type, restore, model, damage, weaponRange, weight })
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
showMsg("itemMsg", "Item gespeichert.");
|
||||
document.getElementById("itemId").value = "";
|
||||
document.getElementById("itemName").value = "";
|
||||
document.getElementById("itemRestore").value = "";
|
||||
document.getElementById("itemModel").value = "";
|
||||
document.getElementById("itemDamage").value = "";
|
||||
document.getElementById("itemWeaponRange").value = "";
|
||||
document.getElementById("itemWeight").value = "";
|
||||
await loadItems();
|
||||
} else {
|
||||
showMsg("itemMsg", "Fehler: " + (data.error || "unbekannt"), true);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteItem(id) {
|
||||
if (!confirm(`Item "${id}" wirklich löschen?`)) return;
|
||||
|
||||
const res = await authFetch("/api/admin/items/" + encodeURIComponent(id), { method: "DELETE" });
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
showMsg("itemMsg", "Item gelöscht.");
|
||||
await loadItems();
|
||||
await loadShops();
|
||||
} else {
|
||||
showMsg("itemMsg", "Fehler beim Löschen (evtl. noch in einem Shop verknüpft?).", true);
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// SHOPS
|
||||
// -------------------------------------------------------------
|
||||
async function loadShops() {
|
||||
const res = await authFetch("/api/admin/shops");
|
||||
const data = await res.json();
|
||||
allShops = data.shops || [];
|
||||
renderShops();
|
||||
}
|
||||
|
||||
function renderShops() {
|
||||
const container = document.getElementById("shopsContainer");
|
||||
container.innerHTML = "";
|
||||
|
||||
allShops.forEach(shop => {
|
||||
const card = document.createElement("div");
|
||||
card.className = "shop-card";
|
||||
|
||||
const itemsRows = shop.items.map(si => `
|
||||
<tr>
|
||||
<td>${si.name}</td>
|
||||
<td>${si.price}$</td>
|
||||
<td><button class="danger" onclick="removeShopItem(${si.id})">Entfernen</button></td>
|
||||
</tr>
|
||||
`).join("");
|
||||
|
||||
card.innerHTML = `
|
||||
<h3>${shop.name} <span class="badge">#${shop.id}</span></h3>
|
||||
<div class="shop-meta">Welt: ${shop.world} — Position: (${shop.x}, ${shop.y})
|
||||
<button class="danger" onclick="deleteShop(${shop.id})" style="margin-left:10px;">Shop löschen</button>
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<thead><tr><th>Item</th><th>Preis</th><th></th></tr></thead>
|
||||
<tbody>${itemsRows || "<tr><td colspan=3 style='color:#666;'>Noch keine Items</td></tr>"}</tbody>
|
||||
</table>
|
||||
|
||||
<div class="form-row">
|
||||
<select id="itemSelect_${shop.id}"></select>
|
||||
<input id="priceInput_${shop.id}" type="number" placeholder="Preis" style="width:100px;">
|
||||
<button onclick="addShopItem(${shop.id})">Item hinzufügen</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.appendChild(card);
|
||||
});
|
||||
|
||||
renderShopItemDropdowns();
|
||||
}
|
||||
|
||||
function renderShopItemDropdowns() {
|
||||
allShops.forEach(shop => {
|
||||
const select = document.getElementById("itemSelect_" + shop.id);
|
||||
if (!select) return;
|
||||
|
||||
select.innerHTML = allItems.map(i =>
|
||||
`<option value="${i.id}">${i.name} (${i.type})</option>`
|
||||
).join("");
|
||||
});
|
||||
}
|
||||
|
||||
async function createShop() {
|
||||
const name = document.getElementById("newShopName").value.trim();
|
||||
const world = document.getElementById("newShopWorld").value.trim();
|
||||
const x = Number(document.getElementById("newShopX").value);
|
||||
const y = Number(document.getElementById("newShopY").value);
|
||||
|
||||
if (!name || !world || isNaN(x) || isNaN(y)) {
|
||||
showMsg("shopMsg", "Bitte alle Felder ausfüllen.", true);
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await authFetch("/api/admin/shops", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name, world, x, y })
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
showMsg("shopMsg", "Shop angelegt.");
|
||||
document.getElementById("newShopName").value = "";
|
||||
document.getElementById("newShopWorld").value = "";
|
||||
document.getElementById("newShopX").value = "";
|
||||
document.getElementById("newShopY").value = "";
|
||||
await loadShops();
|
||||
} else {
|
||||
showMsg("shopMsg", "Fehler: " + (data.error || "unbekannt"), true);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteShop(id) {
|
||||
if (!confirm("Diesen Shop wirklich komplett löschen?")) return;
|
||||
|
||||
const res = await authFetch("/api/admin/shops/" + id, { method: "DELETE" });
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
showMsg("shopMsg", "Shop gelöscht.");
|
||||
await loadShops();
|
||||
} else {
|
||||
showMsg("shopMsg", "Fehler beim Löschen.", true);
|
||||
}
|
||||
}
|
||||
|
||||
async function addShopItem(shopId) {
|
||||
const itemId = document.getElementById("itemSelect_" + shopId).value;
|
||||
const price = Number(document.getElementById("priceInput_" + shopId).value);
|
||||
|
||||
if (!itemId || !price || price <= 0) {
|
||||
showMsg("shopMsg", "Bitte Item und gültigen Preis wählen.", true);
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await authFetch(`/api/admin/shops/${shopId}/items`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ itemId, price })
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
showMsg("shopMsg", "Item zum Shop hinzugefügt.");
|
||||
await loadShops();
|
||||
} else {
|
||||
showMsg("shopMsg", "Fehler: " + (data.error || "unbekannt"), true);
|
||||
}
|
||||
}
|
||||
|
||||
async function removeShopItem(shopItemId) {
|
||||
const res = await authFetch("/api/admin/shop_items/" + shopItemId, { method: "DELETE" });
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
showMsg("shopMsg", "Item entfernt.");
|
||||
await loadShops();
|
||||
} else {
|
||||
showMsg("shopMsg", "Fehler beim Entfernen.", true);
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// HELPERS
|
||||
// -------------------------------------------------------------
|
||||
function showMsg(elId, text, isError) {
|
||||
const el = document.getElementById(elId);
|
||||
el.style.color = isError ? "#e06c6c" : "#6fbf73";
|
||||
el.textContent = text;
|
||||
setTimeout(() => { el.textContent = ""; }, 4000);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// INIT
|
||||
// -------------------------------------------------------------
|
||||
(async () => {
|
||||
await loadItems();
|
||||
await loadShops();
|
||||
})();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
+616
@@ -0,0 +1,616 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Auto-Verwaltung</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
background: #111;
|
||||
color: #eee;
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 20px;
|
||||
}
|
||||
h1 { margin-top: 0; }
|
||||
a.back { color: #6fbf73; text-decoration: none; font-size: 14px; }
|
||||
a.back:hover { text-decoration: underline; }
|
||||
|
||||
.panel {
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 10px;
|
||||
}
|
||||
th, td {
|
||||
text-align: left;
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid #333;
|
||||
font-size: 14px;
|
||||
}
|
||||
th { color: #aaa; font-weight: normal; }
|
||||
|
||||
input {
|
||||
background: #222;
|
||||
border: 1px solid #444;
|
||||
color: #eee;
|
||||
padding: 6px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
button {
|
||||
background: #2c7a3d;
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
button.danger { background: #a83232; }
|
||||
button:hover { opacity: 0.85; }
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 10px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.form-grid label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.form-grid input { width: 100%; box-sizing: border-box; }
|
||||
|
||||
.color-swatch {
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid #555;
|
||||
vertical-align: middle;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.msg {
|
||||
font-size: 13px;
|
||||
color: #6fbf73;
|
||||
min-height: 18px;
|
||||
}
|
||||
|
||||
.car-preview {
|
||||
display: inline-block;
|
||||
border: 1px solid #444;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<a class="back" href="/index.html">← zurück zur Startseite</a> · <a class="back" href="/admin.html">Item/Shop-Verwaltung</a>
|
||||
<h1>🚗 Auto-Verwaltung</h1>
|
||||
|
||||
<div class="panel">
|
||||
<div class="msg" id="carMsg"></div>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Model</th>
|
||||
<th>Größe</th>
|
||||
<th>Farbe</th>
|
||||
<th>Max. Speed</th>
|
||||
<th>Accel</th>
|
||||
<th>Brake</th>
|
||||
<th>Friction</th>
|
||||
<th>Turn Speed</th>
|
||||
<th>Tank</th>
|
||||
<th>Verbrauch</th>
|
||||
<th>Anhänger</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="carsTableBody"></tbody>
|
||||
</table>
|
||||
|
||||
<h3>Modell hinzufügen / bearbeiten</h3>
|
||||
<div class="form-grid">
|
||||
<div>
|
||||
<label>Model-Name</label>
|
||||
<input id="cfgModel" placeholder="z.B. sedan">
|
||||
</div>
|
||||
<div>
|
||||
<label>Breite</label>
|
||||
<input id="cfgWidth" type="number" value="40">
|
||||
</div>
|
||||
<div>
|
||||
<label>Höhe</label>
|
||||
<input id="cfgHeight" type="number" value="22">
|
||||
</div>
|
||||
<div>
|
||||
<label>Farbe</label>
|
||||
<input id="cfgColor" type="color" value="#c0392b">
|
||||
</div>
|
||||
<div>
|
||||
<label>Max. Speed</label>
|
||||
<input id="cfgMaxSpeed" type="number" step="0.1" value="6">
|
||||
</div>
|
||||
<div>
|
||||
<label>Beschleunigung</label>
|
||||
<input id="cfgAccel" type="number" step="0.01" value="0.15">
|
||||
</div>
|
||||
<div>
|
||||
<label>Bremskraft</label>
|
||||
<input id="cfgBrake" type="number" step="0.01" value="0.3">
|
||||
</div>
|
||||
<div>
|
||||
<label>Reibung</label>
|
||||
<input id="cfgFriction" type="number" step="0.01" value="0.05">
|
||||
</div>
|
||||
<div>
|
||||
<label>Lenkgeschwindigkeit</label>
|
||||
<input id="cfgTurnSpeed" type="number" step="0.001" value="0.045">
|
||||
</div>
|
||||
<div>
|
||||
<label>Tankgröße (Liter)</label>
|
||||
<input id="cfgTankSize" type="number" step="1" value="50">
|
||||
</div>
|
||||
<div>
|
||||
<label>Verbrauch (L/Tick)</label>
|
||||
<input id="cfgConsumption" type="number" step="0.001" value="0.02">
|
||||
</div>
|
||||
<div>
|
||||
<label style="display:flex; align-items:center; gap:6px; cursor:pointer;">
|
||||
<input id="cfgIsTrailerModel" type="checkbox" style="width:auto;">
|
||||
Als Anhänger im Shop verkaufen
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label>Anhänger-Preis (falls oben angehakt)</label>
|
||||
<input id="cfgTrailerPrice" type="number" step="1" value="800">
|
||||
</div>
|
||||
<div style="display:flex; align-items:flex-end;">
|
||||
<button onclick="saveCarConfig()">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="color:#888; font-size:12px; margin-top:8px;">
|
||||
Tipp: gleichen Model-Namen erneut speichern = Modell wird aktualisiert (wirkt sich live auf alle
|
||||
Autos dieses Modells im Spiel aus).
|
||||
</div>
|
||||
|
||||
<h3>Karosserie bemalen (optional)</h3>
|
||||
<p style="color:#888; font-size:12px; margin-top:0;">
|
||||
Statt der reinen Flächenfarbe kannst du hier ein Pixel-Muster malen (z.B. Streifen, Muster, Logo).
|
||||
Das Raster passt sich automatisch an die oben eingestellte Breite/Höhe an. "Vorne" ist rechts
|
||||
(Pfeilspitze-Seite). Ohne gemalte Textur wird einfach die Flächenfarbe genutzt.
|
||||
</p>
|
||||
<div style="display:flex; gap:20px; align-items:flex-start; flex-wrap:wrap;">
|
||||
<div>
|
||||
<canvas id="carPixelCanvas" width="400" height="300" style="border:1px solid #444; cursor:crosshair; image-rendering:pixelated; background:#c0392b;"></canvas>
|
||||
</div>
|
||||
<div style="display:flex; flex-direction:column; gap:8px; min-width:180px;">
|
||||
<label style="font-size:12px; color:#999;">Malfarbe</label>
|
||||
<input id="carPixelColor" type="color" value="#ffffff" style="width:60px; height:36px; padding:2px; cursor:pointer;">
|
||||
|
||||
<div style="display:flex; gap:4px; flex-wrap:wrap; margin-top:4px;">
|
||||
<button onclick="carPresetStripe()" style="font-size:11px;">Streifen mittig</button>
|
||||
<button onclick="carPresetBorder()" style="font-size:11px;">Rand</button>
|
||||
</div>
|
||||
|
||||
<button onclick="rebuildCarPixelGrid()" style="background:#555; margin-top:8px;">Raster an Breite/Höhe anpassen</button>
|
||||
<button onclick="clearCarPixelCanvas()" style="background:#a83232;">Löschen (Grundfarbe)</button>
|
||||
<button onclick="saveCarTexture()" style="background:#2c7a3d;">Textur speichern</button>
|
||||
<button onclick="removeCarTexture()" style="background:#555;">Textur entfernen</button>
|
||||
<div class="msg" id="carTextureMsg"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// -------------------------------------------------------------
|
||||
// ZUGRIFFSSCHUTZ: nur eingeloggte Admins dürfen diese Seite nutzen
|
||||
// -------------------------------------------------------------
|
||||
const token = localStorage.getItem("token");
|
||||
// Voll-Admin ODER passende Gruppen-Berechtigung reicht jetzt aus - der
|
||||
// Server prüft das bei jedem Aufruf ohnehin final ab (siehe requireAdmin);
|
||||
// hier reicht ein simpler Login-Check, authFetch() fängt fehlende Rechte
|
||||
// beim ersten echten API-Aufruf sauber ab (Meldung + Weiterleitung)
|
||||
let isAdmin = localStorage.getItem("isAdmin") === "true";
|
||||
|
||||
if (!token) {
|
||||
alert("Bitte zuerst einloggen.");
|
||||
location.href = "/index.html";
|
||||
}
|
||||
|
||||
async function authFetch(url, options = {}) {
|
||||
options.headers = { ...(options.headers || {}), "Authorization": "Bearer " + token };
|
||||
const res = await fetch(url, options);
|
||||
if (res.status === 401 || res.status === 403) {
|
||||
alert("Sitzung abgelaufen oder kein Admin-Zugriff. Bitte erneut einloggen.");
|
||||
location.href = "/index.html";
|
||||
throw new Error("Nicht autorisiert");
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
let allCarConfigs = [];
|
||||
|
||||
async function loadCarConfigs() {
|
||||
const res = await authFetch("/api/admin/car_configs");
|
||||
const data = await res.json();
|
||||
allCarConfigs = data.configs || [];
|
||||
renderCarsTable();
|
||||
}
|
||||
|
||||
function renderCarsTable() {
|
||||
const body = document.getElementById("carsTableBody");
|
||||
body.innerHTML = "";
|
||||
|
||||
allCarConfigs.forEach(cfg => {
|
||||
const tr = document.createElement("tr");
|
||||
|
||||
const previewWidth = Math.min(cfg.width, 60);
|
||||
const previewHeight = Math.min(cfg.height, 40);
|
||||
|
||||
tr.innerHTML = `
|
||||
<td>
|
||||
<div class="car-preview" style="width:${previewWidth}px; height:${previewHeight}px; background:${cfg.color};"></div>
|
||||
</td>
|
||||
<td>${cfg.model}</td>
|
||||
<td>${cfg.width} x ${cfg.height}</td>
|
||||
<td><span class="color-swatch" style="background:${cfg.color};"></span>${cfg.color}</td>
|
||||
<td>${cfg.max_speed}</td>
|
||||
<td>${cfg.accel}</td>
|
||||
<td>${cfg.brake}</td>
|
||||
<td>${cfg.friction}</td>
|
||||
<td>${cfg.turn_speed}</td>
|
||||
<td>${cfg.tank_size ?? 50}L</td>
|
||||
<td>${cfg.consumption ?? 0.02}</td>
|
||||
<td>${cfg.is_trailer_model ? `🚛 ${cfg.trailer_price}$` : "-"}</td>
|
||||
<td>
|
||||
<button onclick="editCarConfig('${cfg.model}')">Bearbeiten</button>
|
||||
<button class="danger" onclick="deleteCarConfig('${cfg.model}')">Löschen</button>
|
||||
</td>
|
||||
`;
|
||||
body.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
function editCarConfig(model) {
|
||||
const cfg = allCarConfigs.find(c => c.model === model);
|
||||
if (!cfg) return;
|
||||
|
||||
document.getElementById("cfgModel").value = cfg.model;
|
||||
document.getElementById("cfgWidth").value = cfg.width;
|
||||
document.getElementById("cfgHeight").value = cfg.height;
|
||||
document.getElementById("cfgColor").value = cfg.color;
|
||||
document.getElementById("cfgMaxSpeed").value = cfg.max_speed;
|
||||
document.getElementById("cfgAccel").value = cfg.accel;
|
||||
document.getElementById("cfgBrake").value = cfg.brake;
|
||||
document.getElementById("cfgFriction").value = cfg.friction;
|
||||
document.getElementById("cfgTurnSpeed").value = cfg.turn_speed;
|
||||
document.getElementById("cfgTankSize").value = cfg.tank_size ?? 50;
|
||||
document.getElementById("cfgConsumption").value = cfg.consumption ?? 0.02;
|
||||
document.getElementById("cfgIsTrailerModel").checked = !!cfg.is_trailer_model;
|
||||
document.getElementById("cfgTrailerPrice").value = cfg.trailer_price ?? 800;
|
||||
|
||||
rebuildCarPixelGrid();
|
||||
if (cfg.image_data) {
|
||||
loadCarTextureIntoGrid(cfg.image_data);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveCarConfig() {
|
||||
const model = document.getElementById("cfgModel").value.trim();
|
||||
if (!model) {
|
||||
showMsg("Model-Name ist Pflicht.", true);
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
model,
|
||||
width: Number(document.getElementById("cfgWidth").value) || 40,
|
||||
height: Number(document.getElementById("cfgHeight").value) || 22,
|
||||
color: document.getElementById("cfgColor").value,
|
||||
maxSpeed: Number(document.getElementById("cfgMaxSpeed").value) || 6,
|
||||
accel: Number(document.getElementById("cfgAccel").value) || 0.15,
|
||||
brake: Number(document.getElementById("cfgBrake").value) || 0.3,
|
||||
friction: Number(document.getElementById("cfgFriction").value) || 0.05,
|
||||
turnSpeed: Number(document.getElementById("cfgTurnSpeed").value) || 0.045,
|
||||
tankSize: Number(document.getElementById("cfgTankSize").value) || 50,
|
||||
consumption: Number(document.getElementById("cfgConsumption").value) || 0.02,
|
||||
isTrailerModel: document.getElementById("cfgIsTrailerModel").checked,
|
||||
trailerPrice: Number(document.getElementById("cfgTrailerPrice").value) || null,
|
||||
imageData: exportCarGridAsDataUrl()
|
||||
};
|
||||
|
||||
const res = await authFetch("/api/admin/car_configs", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
showMsg("Modell gespeichert.");
|
||||
resetForm();
|
||||
await loadCarConfigs();
|
||||
} else {
|
||||
showMsg("Fehler: " + (data.error || "unbekannt"), true);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteCarConfig(model) {
|
||||
if (!confirm(`Modell "${model}" wirklich löschen?`)) return;
|
||||
|
||||
const res = await authFetch("/api/admin/car_configs/" + encodeURIComponent(model), { method: "DELETE" });
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
showMsg("Modell gelöscht.");
|
||||
await loadCarConfigs();
|
||||
} else {
|
||||
showMsg("Fehler beim Löschen (evtl. noch Autos dieses Modells im Umlauf).", true);
|
||||
}
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
document.getElementById("cfgModel").value = "";
|
||||
document.getElementById("cfgWidth").value = 40;
|
||||
document.getElementById("cfgHeight").value = 22;
|
||||
document.getElementById("cfgColor").value = "#c0392b";
|
||||
document.getElementById("cfgMaxSpeed").value = 6;
|
||||
document.getElementById("cfgAccel").value = 0.15;
|
||||
document.getElementById("cfgBrake").value = 0.3;
|
||||
document.getElementById("cfgFriction").value = 0.05;
|
||||
document.getElementById("cfgTurnSpeed").value = 0.045;
|
||||
document.getElementById("cfgTankSize").value = 50;
|
||||
document.getElementById("cfgConsumption").value = 0.02;
|
||||
}
|
||||
|
||||
function showMsg(text, isError) {
|
||||
const el = document.getElementById("carMsg");
|
||||
el.style.color = isError ? "#e06c6c" : "#6fbf73";
|
||||
el.textContent = text;
|
||||
setTimeout(() => { el.textContent = ""; }, 4000);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// PIXEL-EDITOR FÜR AUTO-TEXTUREN
|
||||
// -------------------------------------------------------------
|
||||
const carCanvas = document.getElementById("carPixelCanvas");
|
||||
const carCtx = carCanvas.getContext("2d");
|
||||
const CAR_DISPLAY_MAX = 380; // maximale Anzeigegröße im Editor
|
||||
|
||||
let carGridW = 40;
|
||||
let carGridH = 22;
|
||||
let carPixelGrid = [];
|
||||
let carCellPxX = 8;
|
||||
let carCellPxY = 8;
|
||||
let carIsPainting = false;
|
||||
|
||||
function fillCarGridWithColor(color) {
|
||||
carPixelGrid = [];
|
||||
for (let y = 0; y < carGridH; y++) {
|
||||
const row = [];
|
||||
for (let x = 0; x < carGridW; x++) row.push(color);
|
||||
carPixelGrid.push(row);
|
||||
}
|
||||
}
|
||||
|
||||
function rebuildCarPixelGrid() {
|
||||
carGridW = Math.max(4, Math.min(200, parseInt(document.getElementById("cfgWidth").value) || 40));
|
||||
carGridH = Math.max(4, Math.min(200, parseInt(document.getElementById("cfgHeight").value) || 22));
|
||||
|
||||
// Anzeige so skalieren, dass sie ins Canvas passt, Seitenverhältnis bleibt 1 Pixel = 1 "Auto-Pixel"
|
||||
const scale = Math.min(CAR_DISPLAY_MAX / carGridW, CAR_DISPLAY_MAX / carGridH, 12);
|
||||
carCellPxX = scale;
|
||||
carCellPxY = scale;
|
||||
|
||||
carCanvas.width = carGridW * carCellPxX;
|
||||
carCanvas.height = carGridH * carCellPxY;
|
||||
|
||||
fillCarGridWithColor(document.getElementById("cfgColor").value);
|
||||
redrawCarPixelCanvas();
|
||||
}
|
||||
rebuildCarPixelGrid();
|
||||
|
||||
function redrawCarPixelCanvas() {
|
||||
for (let y = 0; y < carGridH; y++) {
|
||||
for (let x = 0; x < carGridW; x++) {
|
||||
carCtx.fillStyle = carPixelGrid[y][x];
|
||||
carCtx.fillRect(x * carCellPxX, y * carCellPxY, carCellPxX, carCellPxY);
|
||||
}
|
||||
}
|
||||
// Pfeil zur Erinnerung, wo "vorne" ist (rechts)
|
||||
carCtx.fillStyle = "rgba(255,255,255,0.5)";
|
||||
carCtx.beginPath();
|
||||
carCtx.moveTo(carCanvas.width - 2, carCanvas.height / 2 - 8);
|
||||
carCtx.lineTo(carCanvas.width + 10, carCanvas.height / 2);
|
||||
carCtx.lineTo(carCanvas.width - 2, carCanvas.height / 2 + 8);
|
||||
carCtx.closePath();
|
||||
carCtx.fill();
|
||||
}
|
||||
|
||||
function loadCarTextureIntoGrid(dataUrl) {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
const off = document.createElement("canvas");
|
||||
off.width = carGridW;
|
||||
off.height = carGridH;
|
||||
const offCtx = off.getContext("2d");
|
||||
offCtx.drawImage(img, 0, 0, carGridW, carGridH);
|
||||
const data = offCtx.getImageData(0, 0, carGridW, carGridH).data;
|
||||
|
||||
for (let y = 0; y < carGridH; y++) {
|
||||
for (let x = 0; x < carGridW; x++) {
|
||||
const i = (y * carGridW + x) * 4;
|
||||
carPixelGrid[y][x] = `rgb(${data[i]},${data[i + 1]},${data[i + 2]})`;
|
||||
}
|
||||
}
|
||||
redrawCarPixelCanvas();
|
||||
};
|
||||
img.src = dataUrl;
|
||||
}
|
||||
|
||||
function carPaintAt(clientX, clientY) {
|
||||
const rect = carCanvas.getBoundingClientRect();
|
||||
const x = Math.floor((clientX - rect.left) / (rect.width / carGridW));
|
||||
const y = Math.floor((clientY - rect.top) / (rect.height / carGridH));
|
||||
if (x < 0 || y < 0 || x >= carGridW || y >= carGridH) return;
|
||||
|
||||
carPixelGrid[y][x] = document.getElementById("carPixelColor").value;
|
||||
redrawCarPixelCanvas();
|
||||
}
|
||||
|
||||
carCanvas.addEventListener("mousedown", e => { carIsPainting = true; carPaintAt(e.clientX, e.clientY); });
|
||||
carCanvas.addEventListener("mousemove", e => { if (carIsPainting) carPaintAt(e.clientX, e.clientY); });
|
||||
window.addEventListener("mouseup", () => { carIsPainting = false; });
|
||||
|
||||
function clearCarPixelCanvas() {
|
||||
fillCarGridWithColor(document.getElementById("cfgColor").value);
|
||||
redrawCarPixelCanvas();
|
||||
}
|
||||
|
||||
document.getElementById("cfgColor").addEventListener("input", e => {
|
||||
carCanvas.style.background = e.target.value;
|
||||
});
|
||||
|
||||
document.getElementById("cfgWidth").addEventListener("change", rebuildCarPixelGrid);
|
||||
document.getElementById("cfgHeight").addEventListener("change", rebuildCarPixelGrid);
|
||||
|
||||
function carPresetStripe() {
|
||||
const color = document.getElementById("carPixelColor").value;
|
||||
const midStart = Math.floor(carGridH / 2) - 1;
|
||||
for (let x = 0; x < carGridW; x++) {
|
||||
carPixelGrid[midStart][x] = color;
|
||||
if (midStart + 1 < carGridH) carPixelGrid[midStart + 1][x] = color;
|
||||
}
|
||||
redrawCarPixelCanvas();
|
||||
}
|
||||
|
||||
function carPresetBorder() {
|
||||
const color = document.getElementById("carPixelColor").value;
|
||||
for (let x = 0; x < carGridW; x++) {
|
||||
carPixelGrid[0][x] = color;
|
||||
carPixelGrid[carGridH - 1][x] = color;
|
||||
}
|
||||
for (let y = 0; y < carGridH; y++) {
|
||||
carPixelGrid[y][0] = color;
|
||||
carPixelGrid[y][carGridW - 1] = color;
|
||||
}
|
||||
redrawCarPixelCanvas();
|
||||
}
|
||||
|
||||
function exportCarGridAsDataUrl() {
|
||||
const off = document.createElement("canvas");
|
||||
off.width = carGridW;
|
||||
off.height = carGridH;
|
||||
const offCtx = off.getContext("2d");
|
||||
for (let y = 0; y < carGridH; y++) {
|
||||
for (let x = 0; x < carGridW; x++) {
|
||||
offCtx.fillStyle = carPixelGrid[y][x];
|
||||
offCtx.fillRect(x, y, 1, 1);
|
||||
}
|
||||
}
|
||||
return off.toDataURL("image/png");
|
||||
}
|
||||
|
||||
async function saveCarTexture() {
|
||||
const model = document.getElementById("cfgModel").value.trim();
|
||||
if (!model) {
|
||||
showCarTextureMsg("Erst oben einen Model-Namen eintragen.", true);
|
||||
return;
|
||||
}
|
||||
|
||||
// restliche Fahrzeugwerte mitschicken, damit "Textur speichern" alleine funktioniert
|
||||
const payload = {
|
||||
model,
|
||||
width: Number(document.getElementById("cfgWidth").value),
|
||||
height: Number(document.getElementById("cfgHeight").value),
|
||||
color: document.getElementById("cfgColor").value,
|
||||
maxSpeed: Number(document.getElementById("cfgMaxSpeed").value),
|
||||
accel: Number(document.getElementById("cfgAccel").value),
|
||||
brake: Number(document.getElementById("cfgBrake").value),
|
||||
friction: Number(document.getElementById("cfgFriction").value),
|
||||
turnSpeed: Number(document.getElementById("cfgTurnSpeed").value),
|
||||
tankSize: Number(document.getElementById("cfgTankSize").value),
|
||||
consumption: Number(document.getElementById("cfgConsumption").value),
|
||||
imageData: exportCarGridAsDataUrl()
|
||||
};
|
||||
|
||||
const res = await authFetch("/api/admin/car_configs", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
showCarTextureMsg("Textur gespeichert.");
|
||||
await loadCarConfigs();
|
||||
} else {
|
||||
showCarTextureMsg("Fehler: " + (data.error || "unbekannt"), true);
|
||||
}
|
||||
}
|
||||
|
||||
async function removeCarTexture() {
|
||||
const model = document.getElementById("cfgModel").value.trim();
|
||||
if (!model) {
|
||||
showCarTextureMsg("Erst oben einen Model-Namen eintragen.", true);
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
model,
|
||||
width: Number(document.getElementById("cfgWidth").value),
|
||||
height: Number(document.getElementById("cfgHeight").value),
|
||||
color: document.getElementById("cfgColor").value,
|
||||
maxSpeed: Number(document.getElementById("cfgMaxSpeed").value),
|
||||
accel: Number(document.getElementById("cfgAccel").value),
|
||||
brake: Number(document.getElementById("cfgBrake").value),
|
||||
friction: Number(document.getElementById("cfgFriction").value),
|
||||
turnSpeed: Number(document.getElementById("cfgTurnSpeed").value),
|
||||
tankSize: Number(document.getElementById("cfgTankSize").value),
|
||||
consumption: Number(document.getElementById("cfgConsumption").value),
|
||||
imageData: null
|
||||
};
|
||||
|
||||
const res = await authFetch("/api/admin/car_configs", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
showCarTextureMsg("Textur entfernt - nutzt jetzt wieder die Flächenfarbe.");
|
||||
clearCarPixelCanvas();
|
||||
await loadCarConfigs();
|
||||
} else {
|
||||
showCarTextureMsg("Fehler: " + (data.error || "unbekannt"), true);
|
||||
}
|
||||
}
|
||||
|
||||
function showCarTextureMsg(text, isError) {
|
||||
const el = document.getElementById("carTextureMsg");
|
||||
el.style.color = isError ? "#e06c6c" : "#6fbf73";
|
||||
el.textContent = text;
|
||||
setTimeout(() => { el.textContent = ""; }, 4000);
|
||||
}
|
||||
|
||||
loadCarConfigs();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,447 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Kleidungs-Editor</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
background: #111;
|
||||
color: #eee;
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 20px;
|
||||
}
|
||||
h1 { margin-top: 0; }
|
||||
a.back { color: #6fbf73; text-decoration: none; font-size: 14px; }
|
||||
a.back:hover { text-decoration: underline; }
|
||||
|
||||
.panel {
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
table { width: 100%; border-collapse: collapse; margin-top: 10px; }
|
||||
th, td { text-align: left; padding: 8px; border-bottom: 1px solid #333; font-size: 14px; }
|
||||
th { color: #aaa; font-weight: normal; }
|
||||
|
||||
input, select {
|
||||
background: #222; border: 1px solid #444; color: #eee;
|
||||
padding: 6px 8px; border-radius: 4px; font-size: 14px;
|
||||
}
|
||||
button {
|
||||
background: #2c7a3d; border: none; color: white;
|
||||
padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 14px;
|
||||
}
|
||||
button.danger { background: #a83232; }
|
||||
button.secondary { background: #555; }
|
||||
button:hover { opacity: 0.85; }
|
||||
|
||||
.form-grid { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; margin-top: 10px; }
|
||||
.swatch { display: inline-block; width: 20px; height: 20px; border-radius: 3px; vertical-align: middle; border: 1px solid #555; }
|
||||
.msg { margin-top: 10px; font-size: 13px; min-height: 18px; }
|
||||
|
||||
.slot-tabs { display: flex; gap: 6px; margin-bottom: 12px; }
|
||||
.slot-tab-btn {
|
||||
background: #222; border: 1px solid #444; color: #ccc;
|
||||
padding: 8px 16px; border-radius: 4px; cursor: pointer; font-size: 13px;
|
||||
}
|
||||
.slot-tab-btn.active { background: #2c7a3d; border-color: #2c7a3d; color: white; }
|
||||
|
||||
#pixelCanvas { border: 1px solid #444; cursor: crosshair; image-rendering: pixelated; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<a class="back" href="/admin.html">← Zurück zum Admin-Menü</a>
|
||||
<h1>👕 Kleidungs-Editor</h1>
|
||||
<p style="color:#888;">Male Kleidungsstücke, die Spieler im Kleidungsladen kaufen können. Jeder Slot hat eine
|
||||
feste Größe passend zur In-Game-Darstellung (Oberteil 20×9, Hose 20×7, Schuhe 20×4 Pixel).</p>
|
||||
|
||||
<div class="panel">
|
||||
<h3 style="margin-top:0;">Vorhandene Kleidungsstücke</h3>
|
||||
<div class="slot-tabs">
|
||||
<button class="slot-tab-btn active" data-filter-slot="shirt">👕 Oberteile</button>
|
||||
<button class="slot-tab-btn" data-filter-slot="pants">👖 Hosen</button>
|
||||
<button class="slot-tab-btn" data-filter-slot="shoes">👟 Schuhe</button>
|
||||
<button class="slot-tab-btn" data-filter-slot="helmet">⛑️ Helme</button>
|
||||
</div>
|
||||
<table>
|
||||
<thead><tr><th>Vorschau</th><th>Name</th><th>Preis</th><th>Aktionen</th></tr></thead>
|
||||
<tbody id="itemsTableBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h3 id="formTitle" style="margin-top:0;">Neues Kleidungsstück</h3>
|
||||
|
||||
<div class="form-grid">
|
||||
<label>Slot:
|
||||
<select id="itemSlot">
|
||||
<option value="shirt">Oberteil</option>
|
||||
<option value="pants">Hose</option>
|
||||
<option value="shoes">Schuhe</option>
|
||||
<option value="helmet">Helm</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>Name: <input type="text" id="itemName" placeholder="z.B. Rotes T-Shirt"></label>
|
||||
<label>Preis: <input type="number" id="itemPrice" value="150" min="0" style="width:80px;"> $</label>
|
||||
<label>Grundfarbe: <input type="color" id="itemColor" value="#3498db"></label>
|
||||
</div>
|
||||
|
||||
<div style="display:flex; gap:20px; align-items:flex-start; flex-wrap:wrap; margin-top:16px;">
|
||||
<div>
|
||||
<canvas id="pixelCanvas" width="400" height="180"></canvas>
|
||||
</div>
|
||||
<div style="display:flex; flex-direction:column; gap:8px; min-width:180px;">
|
||||
<label style="font-size:12px; color:#999;">Malfarbe</label>
|
||||
<input id="pixelColor" type="color" value="#ffffff" style="width:60px; height:36px; padding:2px; cursor:pointer;">
|
||||
<button id="eraserBtn" class="secondary" onclick="toggleEraser()" style="font-size:11px;">🧹 Radiergummi (transparent)</button>
|
||||
|
||||
<div style="display:flex; gap:4px; flex-wrap:wrap; margin-top:4px;">
|
||||
<button class="secondary" onclick="presetStripes()" style="font-size:11px;">Streifen</button>
|
||||
<button class="secondary" onclick="presetBorder()" style="font-size:11px;">Rand</button>
|
||||
</div>
|
||||
|
||||
<button class="secondary" onclick="clearPixelCanvas()" style="margin-top:8px;">Löschen (Grundfarbe)</button>
|
||||
<button onclick="saveClothingItem()" style="background:#2c7a3d;">Speichern</button>
|
||||
<button class="secondary" onclick="resetForm()">Neues Kleidungsstück</button>
|
||||
<div class="msg" id="formMsg"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// -------------------------------------------------------------
|
||||
// ZUGRIFFSSCHUTZ
|
||||
// -------------------------------------------------------------
|
||||
const token = localStorage.getItem("token");
|
||||
// Voll-Admin ODER passende Gruppen-Berechtigung reicht jetzt aus - der
|
||||
// Server prüft das bei jedem Aufruf ohnehin final ab (siehe requireAdmin);
|
||||
// hier reicht ein simpler Login-Check, authFetch() fängt fehlende Rechte
|
||||
// beim ersten echten API-Aufruf sauber ab (Meldung + Weiterleitung)
|
||||
let isAdmin = localStorage.getItem("isAdmin") === "true";
|
||||
|
||||
if (!token) {
|
||||
alert("Bitte zuerst einloggen.");
|
||||
location.href = "/index.html";
|
||||
}
|
||||
|
||||
async function authFetch(url, options = {}) {
|
||||
options.headers = { ...(options.headers || {}), "Authorization": "Bearer " + token };
|
||||
const res = await fetch(url, options);
|
||||
if (res.status === 401 || res.status === 403) {
|
||||
alert("Sitzung abgelaufen oder kein Admin-Zugriff. Bitte erneut einloggen.");
|
||||
location.href = "/index.html";
|
||||
throw new Error("Nicht autorisiert");
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// SLOT-GRÖSSEN (müssen zur Darstellung in game.js passen)
|
||||
// -------------------------------------------------------------
|
||||
const SLOT_SIZES = {
|
||||
shirt: { w: 20, h: 9 },
|
||||
pants: { w: 20, h: 7 },
|
||||
shoes: { w: 20, h: 4 },
|
||||
helmet: { w: 20, h: 20 }
|
||||
};
|
||||
|
||||
let allItems = [];
|
||||
let editingId = null;
|
||||
let activeFilterSlot = "shirt";
|
||||
|
||||
async function loadClothingItems() {
|
||||
const res = await authFetch("/api/admin/clothing_items");
|
||||
const data = await res.json();
|
||||
allItems = data.items || [];
|
||||
renderItemsTable();
|
||||
}
|
||||
|
||||
function renderItemsTable() {
|
||||
const tbody = document.getElementById("itemsTableBody");
|
||||
tbody.innerHTML = "";
|
||||
allItems.filter(i => i.slot === activeFilterSlot).forEach(item => {
|
||||
const tr = document.createElement("tr");
|
||||
tr.innerHTML = `
|
||||
<td><span class="swatch" style="background:${item.color};"></span>${item.image_data ? " 🎨" : ""}</td>
|
||||
<td>${escapeHtml(item.name)}</td>
|
||||
<td>${item.price}$</td>
|
||||
<td>
|
||||
<button onclick="editItem(${item.id})">Bearbeiten</button>
|
||||
<button class="danger" onclick="deleteItem(${item.id})">Löschen</button>
|
||||
</td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement("div");
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
document.querySelectorAll(".slot-tab-btn").forEach(btn => {
|
||||
btn.addEventListener("click", () => {
|
||||
document.querySelectorAll(".slot-tab-btn").forEach(b => b.classList.remove("active"));
|
||||
btn.classList.add("active");
|
||||
activeFilterSlot = btn.dataset.filterSlot;
|
||||
renderItemsTable();
|
||||
});
|
||||
});
|
||||
|
||||
function editItem(id) {
|
||||
const item = allItems.find(i => i.id === id);
|
||||
if (!item) return;
|
||||
|
||||
editingId = id;
|
||||
document.getElementById("formTitle").textContent = "Kleidungsstück bearbeiten: " + item.name;
|
||||
document.getElementById("itemSlot").value = item.slot;
|
||||
document.getElementById("itemName").value = item.name;
|
||||
document.getElementById("itemColor").value = item.color;
|
||||
document.getElementById("itemPrice").value = item.price;
|
||||
|
||||
rebuildPixelGrid();
|
||||
if (item.image_data) loadTextureIntoGrid(item.image_data);
|
||||
|
||||
window.scrollTo({ top: document.body.scrollHeight, behavior: "smooth" });
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
editingId = null;
|
||||
document.getElementById("formTitle").textContent = "Neues Kleidungsstück";
|
||||
document.getElementById("itemName").value = "";
|
||||
document.getElementById("itemColor").value = "#3498db";
|
||||
document.getElementById("itemPrice").value = "150";
|
||||
rebuildPixelGrid();
|
||||
}
|
||||
|
||||
async function deleteItem(id) {
|
||||
if (!confirm("Dieses Kleidungsstück wirklich löschen?")) return;
|
||||
const res = await authFetch("/api/admin/clothing_items/" + id, { method: "DELETE" });
|
||||
const data = await res.json();
|
||||
if (data.ok) {
|
||||
showMsg("Gelöscht.");
|
||||
await loadClothingItems();
|
||||
if (editingId === id) resetForm();
|
||||
} else {
|
||||
showMsg("Löschen fehlgeschlagen.", true);
|
||||
}
|
||||
}
|
||||
|
||||
function showMsg(text, isError) {
|
||||
const el = document.getElementById("formMsg");
|
||||
el.style.color = isError ? "#e06c6c" : "#6fbf73";
|
||||
el.textContent = text;
|
||||
setTimeout(() => { el.textContent = ""; }, 4000);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// PIXEL-EDITOR
|
||||
// -------------------------------------------------------------
|
||||
const pixelCanvas = document.getElementById("pixelCanvas");
|
||||
const pctx = pixelCanvas.getContext("2d");
|
||||
const DISPLAY_MAX_W = 400;
|
||||
const DISPLAY_MAX_H = 220;
|
||||
|
||||
let gridW = 20, gridH = 9;
|
||||
let pixelGrid = [];
|
||||
let cellPx = 8;
|
||||
let isPainting = false;
|
||||
|
||||
function fillGridWithColor(color) {
|
||||
pixelGrid = [];
|
||||
for (let y = 0; y < gridH; y++) {
|
||||
const row = [];
|
||||
for (let x = 0; x < gridW; x++) row.push(color);
|
||||
pixelGrid.push(row);
|
||||
}
|
||||
}
|
||||
|
||||
function fillGridTransparent() {
|
||||
pixelGrid = [];
|
||||
for (let y = 0; y < gridH; y++) {
|
||||
const row = [];
|
||||
for (let x = 0; x < gridW; x++) row.push(null);
|
||||
pixelGrid.push(row);
|
||||
}
|
||||
}
|
||||
|
||||
function rebuildPixelGrid() {
|
||||
const slot = document.getElementById("itemSlot").value;
|
||||
const size = SLOT_SIZES[slot];
|
||||
gridW = size.w;
|
||||
gridH = size.h;
|
||||
|
||||
cellPx = Math.min(DISPLAY_MAX_W / gridW, DISPLAY_MAX_H / gridH, 30);
|
||||
|
||||
pixelCanvas.width = gridW * cellPx;
|
||||
pixelCanvas.height = gridH * cellPx;
|
||||
|
||||
// Helme starten komplett transparent (sitzen nur über dem Kopf, nicht der
|
||||
// ganzen Figur) - alle anderen Slots wie bisher komplett mit Grundfarbe gefüllt
|
||||
if (slot === "helmet") fillGridTransparent();
|
||||
else fillGridWithColor(document.getElementById("itemColor").value);
|
||||
redrawPixelCanvas();
|
||||
}
|
||||
rebuildPixelGrid();
|
||||
|
||||
function redrawPixelCanvas() {
|
||||
pctx.clearRect(0, 0, pixelCanvas.width, pixelCanvas.height);
|
||||
for (let y = 0; y < gridH; y++) {
|
||||
for (let x = 0; x < gridW; x++) {
|
||||
const cell = pixelGrid[y][x];
|
||||
if (cell === null) {
|
||||
// Transparente Zelle: Schachbrett-Muster zur Orientierung, statt einfach schwarz
|
||||
pctx.fillStyle = ((x + y) % 2 === 0) ? "#3a3a3a" : "#2a2a2a";
|
||||
pctx.fillRect(x * cellPx, y * cellPx, cellPx, cellPx);
|
||||
} else {
|
||||
pctx.fillStyle = cell;
|
||||
pctx.fillRect(x * cellPx, y * cellPx, cellPx, cellPx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function loadTextureIntoGrid(dataUrl) {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
const off = document.createElement("canvas");
|
||||
off.width = gridW;
|
||||
off.height = gridH;
|
||||
const offCtx = off.getContext("2d");
|
||||
offCtx.clearRect(0, 0, gridW, gridH);
|
||||
offCtx.drawImage(img, 0, 0, gridW, gridH);
|
||||
const data = offCtx.getImageData(0, 0, gridW, gridH).data;
|
||||
|
||||
for (let y = 0; y < gridH; y++) {
|
||||
for (let x = 0; x < gridW; x++) {
|
||||
const i = (y * gridW + x) * 4;
|
||||
const alpha = data[i + 3];
|
||||
pixelGrid[y][x] = alpha < 10 ? null : `rgb(${data[i]},${data[i + 1]},${data[i + 2]})`;
|
||||
}
|
||||
}
|
||||
redrawPixelCanvas();
|
||||
};
|
||||
img.src = dataUrl;
|
||||
}
|
||||
|
||||
let eraserActive = false;
|
||||
|
||||
function paintAt(clientX, clientY) {
|
||||
const rect = pixelCanvas.getBoundingClientRect();
|
||||
const x = Math.floor((clientX - rect.left) / (rect.width / gridW));
|
||||
const y = Math.floor((clientY - rect.top) / (rect.height / gridH));
|
||||
if (x < 0 || y < 0 || x >= gridW || y >= gridH) return;
|
||||
|
||||
pixelGrid[y][x] = eraserActive ? null : document.getElementById("pixelColor").value;
|
||||
redrawPixelCanvas();
|
||||
}
|
||||
|
||||
pixelCanvas.addEventListener("mousedown", e => { isPainting = true; paintAt(e.clientX, e.clientY); });
|
||||
pixelCanvas.addEventListener("mousemove", e => { if (isPainting) paintAt(e.clientX, e.clientY); });
|
||||
window.addEventListener("mouseup", () => { isPainting = false; });
|
||||
|
||||
function toggleEraser() {
|
||||
eraserActive = !eraserActive;
|
||||
document.getElementById("eraserBtn").style.background = eraserActive ? "#a83232" : "#555";
|
||||
}
|
||||
|
||||
function clearPixelCanvas() {
|
||||
const slot = document.getElementById("itemSlot").value;
|
||||
if (slot === "helmet") fillGridTransparent();
|
||||
else fillGridWithColor(document.getElementById("itemColor").value);
|
||||
redrawPixelCanvas();
|
||||
}
|
||||
|
||||
document.getElementById("itemSlot").addEventListener("change", () => {
|
||||
if (editingId === null) rebuildPixelGrid();
|
||||
else alert("Der Slot eines bestehenden Kleidungsstücks kann nicht geändert werden - bitte ein neues anlegen.");
|
||||
});
|
||||
document.getElementById("itemColor").addEventListener("input", () => {
|
||||
// Grundfarbe ändert nur die Vorschau im Formular, nicht rückwirkend das Raster
|
||||
});
|
||||
|
||||
function presetStripes() {
|
||||
const color = document.getElementById("pixelColor").value;
|
||||
for (let y = 0; y < gridH; y += 2) {
|
||||
for (let x = 0; x < gridW; x++) pixelGrid[y][x] = color;
|
||||
}
|
||||
redrawPixelCanvas();
|
||||
}
|
||||
function presetBorder() {
|
||||
const color = document.getElementById("pixelColor").value;
|
||||
for (let x = 0; x < gridW; x++) {
|
||||
pixelGrid[0][x] = color;
|
||||
pixelGrid[gridH - 1][x] = color;
|
||||
}
|
||||
for (let y = 0; y < gridH; y++) {
|
||||
pixelGrid[y][0] = color;
|
||||
pixelGrid[y][gridW - 1] = color;
|
||||
}
|
||||
redrawPixelCanvas();
|
||||
}
|
||||
|
||||
function exportGridAsDataUrl() {
|
||||
const off = document.createElement("canvas");
|
||||
off.width = gridW;
|
||||
off.height = gridH;
|
||||
const offCtx = off.getContext("2d");
|
||||
offCtx.clearRect(0, 0, gridW, gridH);
|
||||
for (let y = 0; y < gridH; y++) {
|
||||
for (let x = 0; x < gridW; x++) {
|
||||
const cell = pixelGrid[y][x];
|
||||
if (cell === null) continue; // transparent bleibt transparent
|
||||
offCtx.fillStyle = cell;
|
||||
offCtx.fillRect(x, y, 1, 1);
|
||||
}
|
||||
}
|
||||
return off.toDataURL("image/png");
|
||||
}
|
||||
|
||||
async function saveClothingItem() {
|
||||
const slot = document.getElementById("itemSlot").value;
|
||||
const name = document.getElementById("itemName").value.trim();
|
||||
const color = document.getElementById("itemColor").value;
|
||||
const price = Number(document.getElementById("itemPrice").value) || 0;
|
||||
|
||||
if (!name) {
|
||||
showMsg("Bitte einen Namen eingeben.", true);
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
slot, name, color, price,
|
||||
imageData: exportGridAsDataUrl()
|
||||
};
|
||||
if (editingId) payload.id = editingId;
|
||||
|
||||
const res = await authFetch("/api/admin/clothing_items", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
showMsg(editingId ? "Aktualisiert." : "Gespeichert.");
|
||||
resetForm();
|
||||
activeFilterSlot = slot;
|
||||
document.querySelectorAll(".slot-tab-btn").forEach(b => b.classList.toggle("active", b.dataset.filterSlot === slot));
|
||||
await loadClothingItems();
|
||||
} else {
|
||||
showMsg("Fehler: " + (data.error || "unbekannt"), true);
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// INIT
|
||||
// -------------------------------------------------------------
|
||||
loadClothingItems();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,251 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Drogensorten</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
background: #111;
|
||||
color: #eee;
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 20px;
|
||||
}
|
||||
h1 { margin-top: 0; }
|
||||
a.back { color: #6fbf73; text-decoration: none; font-size: 14px; }
|
||||
a.back:hover { text-decoration: underline; }
|
||||
|
||||
.panel {
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-top: 16px;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
table { width: 100%; border-collapse: collapse; margin-top: 10px; }
|
||||
th, td {
|
||||
text-align: left;
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid #333;
|
||||
font-size: 14px;
|
||||
}
|
||||
th { color: #aaa; font-weight: normal; }
|
||||
|
||||
input {
|
||||
background: #222;
|
||||
border: 1px solid #444;
|
||||
color: #eee;
|
||||
padding: 6px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
button {
|
||||
background: #2c7a3d;
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
button.danger { background: #a83232; }
|
||||
button:hover { opacity: 0.85; }
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 10px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.form-grid label { display: block; font-size: 12px; color: #999; margin-bottom: 4px; }
|
||||
.form-grid input { width: 100%; }
|
||||
|
||||
.msg { font-size: 13px; color: #6fbf73; min-height: 18px; margin-top: 8px; }
|
||||
.empty-hint { color: #666; padding: 10px 0; }
|
||||
.hidden { display: none !important; }
|
||||
|
||||
.hint-box {
|
||||
background: #1a2a1a;
|
||||
border: 1px solid #2c4a2c;
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
font-size: 13px;
|
||||
color: #a8d8a8;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<a class="back" href="/index.html">← zurück zur Startseite</a>
|
||||
<h1>🌿 Drogensorten</h1>
|
||||
|
||||
<div class="hint-box">
|
||||
<strong>Ablauf:</strong> 1) In <a href="/admin.html" style="color:#6fbf73;">admin.html</a> zwei Items anlegen
|
||||
(Rohware + fertiges Produkt, z.B. "cannabis_raw" und "cannabis"). 2) Hier eine Sorte anlegen, die beide
|
||||
Items miteinander verknüpft. 3) Im Map-Editor Anbaustellen, Labore und Verkaufsorte für diese Sorte platzieren.
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h2 style="margin-top:0;">Vorhandene Sorten</h2>
|
||||
<table>
|
||||
<thead><tr><th>ID</th><th>Name</th><th>Rohware</th><th>Produkt</th><th>Basispreis</th><th></th></tr></thead>
|
||||
<tbody id="drugsTableBody"></tbody>
|
||||
</table>
|
||||
<div class="empty-hint hidden" id="drugsEmpty">Noch keine Drogensorten angelegt.</div>
|
||||
|
||||
<h3>Sorte anlegen</h3>
|
||||
<div class="form-grid">
|
||||
<div>
|
||||
<label>ID (z.B. cannabis)</label>
|
||||
<input id="drugId" placeholder="cannabis">
|
||||
</div>
|
||||
<div>
|
||||
<label>Name</label>
|
||||
<input id="drugName" placeholder="Cannabis">
|
||||
</div>
|
||||
<div>
|
||||
<label>Rohware-Item-ID</label>
|
||||
<input id="drugRawItem" placeholder="cannabis_raw">
|
||||
</div>
|
||||
<div>
|
||||
<label>Produkt-Item-ID</label>
|
||||
<input id="drugProductItem" placeholder="cannabis">
|
||||
</div>
|
||||
<div>
|
||||
<label>Basispreis ($/Stück)</label>
|
||||
<input id="drugBasePrice" type="number" value="50">
|
||||
</div>
|
||||
<div>
|
||||
<button onclick="saveDrugType()">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="msg" id="drugMsg"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const token = localStorage.getItem("token");
|
||||
// Voll-Admin ODER passende Gruppen-Berechtigung reicht jetzt aus - der
|
||||
// Server prüft das bei jedem Aufruf ohnehin final ab (siehe requireAdmin);
|
||||
// hier reicht ein simpler Login-Check, authFetch() fängt fehlende Rechte
|
||||
// beim ersten echten API-Aufruf sauber ab (Meldung + Weiterleitung)
|
||||
let isAdmin = localStorage.getItem("isAdmin") === "true";
|
||||
|
||||
if (!token) {
|
||||
alert("Bitte zuerst einloggen.");
|
||||
location.href = "/index.html";
|
||||
}
|
||||
|
||||
async function authFetch(url, options = {}) {
|
||||
options.headers = { ...(options.headers || {}), "Authorization": "Bearer " + token };
|
||||
const res = await fetch(url, options);
|
||||
if (res.status === 401 || res.status === 403) {
|
||||
alert("Sitzung abgelaufen oder kein Admin-Zugriff. Bitte erneut einloggen.");
|
||||
location.href = "/index.html";
|
||||
throw new Error("Nicht autorisiert");
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
const div = document.createElement("div");
|
||||
div.textContent = str;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
let allDrugTypes = [];
|
||||
|
||||
async function loadDrugTypes() {
|
||||
const res = await authFetch("/api/admin/drug_types");
|
||||
const data = await res.json();
|
||||
allDrugTypes = data.drugTypes || [];
|
||||
renderDrugTypes();
|
||||
}
|
||||
|
||||
function renderDrugTypes() {
|
||||
const body = document.getElementById("drugsTableBody");
|
||||
const empty = document.getElementById("drugsEmpty");
|
||||
body.innerHTML = "";
|
||||
|
||||
if (allDrugTypes.length === 0) {
|
||||
empty.classList.remove("hidden");
|
||||
return;
|
||||
}
|
||||
empty.classList.add("hidden");
|
||||
|
||||
allDrugTypes.forEach(d => {
|
||||
const tr = document.createElement("tr");
|
||||
tr.innerHTML = `
|
||||
<td>${escapeHtml(d.id)}</td>
|
||||
<td>${escapeHtml(d.name)}</td>
|
||||
<td>${escapeHtml(d.raw_item_id)}</td>
|
||||
<td>${escapeHtml(d.product_item_id)}</td>
|
||||
<td>${d.base_price}$</td>
|
||||
<td><button class="danger" onclick="deleteDrugType('${d.id}')">Löschen</button></td>
|
||||
`;
|
||||
body.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
async function saveDrugType() {
|
||||
const id = document.getElementById("drugId").value.trim();
|
||||
const name = document.getElementById("drugName").value.trim();
|
||||
const rawItemId = document.getElementById("drugRawItem").value.trim();
|
||||
const productItemId = document.getElementById("drugProductItem").value.trim();
|
||||
const basePrice = Number(document.getElementById("drugBasePrice").value) || 50;
|
||||
|
||||
if (!id || !name || !rawItemId || !productItemId) {
|
||||
showMsg("ID, Name, Rohware- und Produkt-Item sind Pflicht.", true);
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await authFetch("/api/admin/drug_types", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ id, name, rawItemId, productItemId, basePrice })
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
showMsg("Sorte gespeichert.");
|
||||
document.getElementById("drugId").value = "";
|
||||
document.getElementById("drugName").value = "";
|
||||
document.getElementById("drugRawItem").value = "";
|
||||
document.getElementById("drugProductItem").value = "";
|
||||
document.getElementById("drugBasePrice").value = "50";
|
||||
await loadDrugTypes();
|
||||
} else {
|
||||
showMsg("Fehler: " + (data.error || "unbekannt"), true);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteDrugType(id) {
|
||||
if (!confirm(`Sorte "${id}" wirklich löschen? Alle zugehörigen Anbau-/Labor-/Verkaufsstellen werden mitgelöscht.`)) return;
|
||||
|
||||
const res = await authFetch("/api/admin/drug_types/" + encodeURIComponent(id), { method: "DELETE" });
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
showMsg("Sorte gelöscht.");
|
||||
await loadDrugTypes();
|
||||
} else {
|
||||
showMsg("Löschen fehlgeschlagen.", true);
|
||||
}
|
||||
}
|
||||
|
||||
function showMsg(text, isError) {
|
||||
const el = document.getElementById("drugMsg");
|
||||
el.style.color = isError ? "#e06c6c" : "#6fbf73";
|
||||
el.textContent = text;
|
||||
setTimeout(() => { el.textContent = ""; }, 4000);
|
||||
}
|
||||
|
||||
loadDrugTypes();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,237 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Server-Events</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
background: #111;
|
||||
color: #eee;
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 20px;
|
||||
}
|
||||
h1 { margin-top: 0; }
|
||||
a.back { color: #6fbf73; text-decoration: none; font-size: 14px; }
|
||||
a.back:hover { text-decoration: underline; }
|
||||
|
||||
.panel {
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-top: 16px;
|
||||
max-width: 500px;
|
||||
}
|
||||
.panel h2 { margin-top: 0; }
|
||||
|
||||
.status-line {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid #333;
|
||||
font-size: 14px;
|
||||
}
|
||||
.status-line:last-child { border-bottom: none; }
|
||||
.status-badge {
|
||||
padding: 3px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.status-on { background: #1a4d2a; color: #6fbf73; }
|
||||
.status-off { background: #444; color: #999; }
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||
gap: 10px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.form-grid label { display: block; font-size: 12px; color: #999; margin-bottom: 4px; }
|
||||
input {
|
||||
background: #222;
|
||||
border: 1px solid #444;
|
||||
color: #eee;
|
||||
padding: 6px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
button {
|
||||
background: #2c7a3d;
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 8px 14px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
margin-top: 12px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
button.danger { background: #a83232; }
|
||||
button:hover { opacity: 0.85; }
|
||||
|
||||
.msg { font-size: 13px; color: #6fbf73; min-height: 18px; margin-top: 8px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<a class="back" href="/index.html">← zurück zur Startseite</a>
|
||||
<h1>🎉 Server-Events</h1>
|
||||
|
||||
<div class="panel">
|
||||
<h2>Status</h2>
|
||||
<div class="status-line">
|
||||
<span>⭐ Doppel-XP</span>
|
||||
<span id="statusDoubleXp" class="status-badge status-off">Aus</span>
|
||||
</div>
|
||||
<div class="status-line">
|
||||
<span>💰 Rabatt-Aktion</span>
|
||||
<span id="statusDiscount" class="status-badge status-off">Aus</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h2>⭐ Doppel-XP starten</h2>
|
||||
<div class="form-grid">
|
||||
<div>
|
||||
<label>Dauer (Minuten)</label>
|
||||
<input type="number" id="xpDuration" value="120" min="1">
|
||||
</div>
|
||||
</div>
|
||||
<button onclick="startEvent('doubleXp')">Starten</button>
|
||||
<button class="danger" onclick="stopEvent('doubleXp')">Vorzeitig beenden</button>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h2>💰 Rabatt-Aktion starten</h2>
|
||||
<div class="form-grid">
|
||||
<div>
|
||||
<label>Dauer (Minuten)</label>
|
||||
<input type="number" id="discountDuration" value="120" min="1">
|
||||
</div>
|
||||
<div>
|
||||
<label>Rabatt (%)</label>
|
||||
<input type="number" id="discountPercent" value="20" min="1" max="90">
|
||||
</div>
|
||||
</div>
|
||||
<button onclick="startEvent('discount')">Starten</button>
|
||||
<button class="danger" onclick="stopEvent('discount')">Vorzeitig beenden</button>
|
||||
</div>
|
||||
|
||||
<div class="msg" id="eventMsg"></div>
|
||||
|
||||
<script>
|
||||
const token = localStorage.getItem("token");
|
||||
// Voll-Admin ODER passende Gruppen-Berechtigung reicht jetzt aus - der
|
||||
// Server prüft das bei jedem Aufruf ohnehin final ab (siehe requireAdmin);
|
||||
// hier reicht ein simpler Login-Check, authFetch() fängt fehlende Rechte
|
||||
// beim ersten echten API-Aufruf sauber ab (Meldung + Weiterleitung)
|
||||
let isAdmin = localStorage.getItem("isAdmin") === "true";
|
||||
|
||||
if (!token) {
|
||||
alert("Bitte zuerst einloggen.");
|
||||
location.href = "/index.html";
|
||||
}
|
||||
|
||||
async function authFetch(url, options = {}) {
|
||||
options.headers = { ...(options.headers || {}), "Authorization": "Bearer " + token };
|
||||
const res = await fetch(url, options);
|
||||
if (res.status === 401 || res.status === 403) {
|
||||
alert("Sitzung abgelaufen oder kein Admin-Zugriff. Bitte erneut einloggen.");
|
||||
location.href = "/index.html";
|
||||
throw new Error("Nicht autorisiert");
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function timeLeftLabel(endsAt) {
|
||||
const diff = endsAt - Date.now();
|
||||
if (diff <= 0) return "";
|
||||
const h = Math.floor(diff / 3600000);
|
||||
const m = Math.floor((diff % 3600000) / 60000);
|
||||
return h > 0 ? `noch ${h}h ${m}min` : `noch ${m}min`;
|
||||
}
|
||||
|
||||
async function loadStatus() {
|
||||
const res = await authFetch("/api/admin/events/status");
|
||||
const data = await res.json();
|
||||
renderStatus(data.events);
|
||||
}
|
||||
|
||||
function renderStatus(events) {
|
||||
const xpEl = document.getElementById("statusDoubleXp");
|
||||
if (events.doubleXp.active) {
|
||||
xpEl.textContent = "An - " + timeLeftLabel(events.doubleXp.endsAt);
|
||||
xpEl.className = "status-badge status-on";
|
||||
} else {
|
||||
xpEl.textContent = "Aus";
|
||||
xpEl.className = "status-badge status-off";
|
||||
}
|
||||
|
||||
const discEl = document.getElementById("statusDiscount");
|
||||
if (events.discount.active) {
|
||||
discEl.textContent = `An (${events.discount.percent}%) - ` + timeLeftLabel(events.discount.endsAt);
|
||||
discEl.className = "status-badge status-on";
|
||||
} else {
|
||||
discEl.textContent = "Aus";
|
||||
discEl.className = "status-badge status-off";
|
||||
}
|
||||
}
|
||||
|
||||
async function startEvent(type) {
|
||||
const durationMinutes = type === "doubleXp"
|
||||
? Number(document.getElementById("xpDuration").value) || 60
|
||||
: Number(document.getElementById("discountDuration").value) || 60;
|
||||
const percent = type === "discount" ? Number(document.getElementById("discountPercent").value) || 10 : undefined;
|
||||
|
||||
const res = await authFetch("/api/admin/events/start", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ type, durationMinutes, percent })
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
showMsg("Event gestartet und an alle Online-Spieler angekündigt.");
|
||||
renderStatus(data.events);
|
||||
} else {
|
||||
showMsg("Fehler: " + (data.error || "unbekannt"), true);
|
||||
}
|
||||
}
|
||||
|
||||
async function stopEvent(type) {
|
||||
if (!confirm("Dieses Event wirklich vorzeitig beenden?")) return;
|
||||
|
||||
const res = await authFetch("/api/admin/events/stop", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ type })
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
showMsg("Event beendet.");
|
||||
renderStatus(data.events);
|
||||
} else {
|
||||
showMsg("Fehler: " + (data.error || "unbekannt"), true);
|
||||
}
|
||||
}
|
||||
|
||||
function showMsg(text, isError) {
|
||||
const el = document.getElementById("eventMsg");
|
||||
el.style.color = isError ? "#e06c6c" : "#6fbf73";
|
||||
el.textContent = text;
|
||||
setTimeout(() => { el.textContent = ""; }, 4000);
|
||||
}
|
||||
|
||||
loadStatus();
|
||||
setInterval(loadStatus, 30000);
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
+474
@@ -0,0 +1,474 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Job-Verwaltung</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
background: #111;
|
||||
color: #eee;
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 20px;
|
||||
}
|
||||
h1 { margin-top: 0; }
|
||||
a.back { color: #6fbf73; text-decoration: none; font-size: 14px; }
|
||||
a.back:hover { text-decoration: underline; }
|
||||
|
||||
.panel {
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
input, select {
|
||||
background: #222;
|
||||
border: 1px solid #444;
|
||||
color: #eee;
|
||||
padding: 6px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
button {
|
||||
background: #2c7a3d;
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
button.danger { background: #a83232; }
|
||||
button.secondary { background: #333; }
|
||||
button:hover { opacity: 0.85; }
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 10px;
|
||||
}
|
||||
th, td {
|
||||
text-align: left;
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid #333;
|
||||
font-size: 14px;
|
||||
}
|
||||
th { color: #aaa; font-weight: normal; }
|
||||
td input { width: 100%; }
|
||||
|
||||
.job-card {
|
||||
background: #202020;
|
||||
border: 1px solid #333;
|
||||
border-radius: 6px;
|
||||
padding: 14px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
.job-card h3 { margin: 0 0 8px 0; display:flex; justify-content:space-between; align-items:center; }
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.msg { font-size: 13px; color: #6fbf73; min-height: 18px; margin-top: 8px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<a class="back" href="/index.html">← zurück zur Startseite</a>
|
||||
<h1>💼 Job-Verwaltung</h1>
|
||||
|
||||
<div class="panel">
|
||||
<h3 style="margin-top:0;">Neuen Job anlegen</h3>
|
||||
<div class="form-row">
|
||||
<input id="newJobName" placeholder="Job-Name (z.B. Trucker)">
|
||||
<select id="newJobType">
|
||||
<option value="generic">Normal (Job-Punkte: Abholen/Abliefern)</option>
|
||||
<option value="taxi">Taxi (Spieler fahren)</option>
|
||||
<option value="police">Polizei (Verhaften, Alarm-Empfang)</option>
|
||||
<option value="medic">Sanitäter (Notruf-Empfang)</option>
|
||||
<option value="tow">Abschlepper (Fahrzeuge abschleppen)</option>
|
||||
<option value="fire">Feuerwehr (Brände löschen)</option>
|
||||
<option value="mechanic">Handwerker (Fahrzeuge vor Ort reparieren)</option>
|
||||
</select>
|
||||
<label style="display:flex; align-items:center; gap:4px; font-size:13px; color:#ccc;">
|
||||
<input type="checkbox" id="newJobProtected"> Geschützt (nur per Einladung)
|
||||
</label>
|
||||
<button onclick="createJob()">Job anlegen</button>
|
||||
</div>
|
||||
<div class="msg" id="jobMsg"></div>
|
||||
</div>
|
||||
|
||||
<div id="jobsContainer"></div>
|
||||
|
||||
<div class="panel">
|
||||
<h3 style="margin-top:0;">🚓 Fuhrpark (Job-Fahrzeuge)</h3>
|
||||
<p style="color:#888; font-size:13px; margin-top:0;">
|
||||
Fahrzeuge, die keinem Spieler gehören, sondern einem Job zugeordnet sind.
|
||||
Spieler mit passendem Job können sie an der zugehörigen Job-Garage auschecken.
|
||||
</p>
|
||||
|
||||
<table>
|
||||
<thead><tr><th>ID</th><th>Job</th><th>Modell</th><th>Status</th><th></th></tr></thead>
|
||||
<tbody id="fleetTableBody"></tbody>
|
||||
</table>
|
||||
|
||||
<div class="form-row">
|
||||
<select id="fleetJobSelect"></select>
|
||||
<select id="fleetModelSelect"></select>
|
||||
<button onclick="addFleetVehicle()">Fahrzeug hinzufügen</button>
|
||||
</div>
|
||||
<div class="msg" id="fleetMsg"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const token = localStorage.getItem("token");
|
||||
// Voll-Admin ODER passende Gruppen-Berechtigung reicht jetzt aus - der
|
||||
// Server prüft das bei jedem Aufruf ohnehin final ab (siehe requireAdmin);
|
||||
// hier reicht ein simpler Login-Check, authFetch() fängt fehlende Rechte
|
||||
// beim ersten echten API-Aufruf sauber ab (Meldung + Weiterleitung)
|
||||
let isAdmin = localStorage.getItem("isAdmin") === "true";
|
||||
|
||||
if (!token) {
|
||||
alert("Bitte zuerst einloggen.");
|
||||
location.href = "/index.html";
|
||||
}
|
||||
|
||||
async function authFetch(url, options = {}) {
|
||||
options.headers = { ...(options.headers || {}), "Authorization": "Bearer " + token };
|
||||
const res = await fetch(url, options);
|
||||
if (res.status === 401 || res.status === 403) {
|
||||
alert("Sitzung abgelaufen oder kein Admin-Zugriff. Bitte erneut einloggen.");
|
||||
location.href = "/index.html";
|
||||
throw new Error("Nicht autorisiert");
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
let allJobs = [];
|
||||
let clothingCatalog = { shirt: [], pants: [], shoes: [], helmet: [] };
|
||||
|
||||
async function loadClothingCatalogForUniforms() {
|
||||
const res = await authFetch("/api/admin/clothing_items");
|
||||
const data = await res.json();
|
||||
clothingCatalog = { shirt: [], pants: [], shoes: [], helmet: [] };
|
||||
(data.items || []).forEach(item => {
|
||||
if (clothingCatalog[item.slot]) clothingCatalog[item.slot].push(item);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadJobs() {
|
||||
const res = await authFetch("/api/admin/jobs");
|
||||
const data = await res.json();
|
||||
allJobs = data.jobs || [];
|
||||
await loadClothingCatalogForUniforms();
|
||||
renderJobs();
|
||||
fillFleetJobSelect();
|
||||
}
|
||||
|
||||
async function saveUniform(jobId) {
|
||||
const shirtId = document.getElementById(`uniformShirt_${jobId}`).value || null;
|
||||
const pantsId = document.getElementById(`uniformPants_${jobId}`).value || null;
|
||||
const shoesId = document.getElementById(`uniformShoes_${jobId}`).value || null;
|
||||
const helmetId = document.getElementById(`uniformHelmet_${jobId}`).value || null;
|
||||
|
||||
const res = await authFetch(`/api/admin/jobs/${jobId}/uniform`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ shirtId, pantsId, shoesId, helmetId })
|
||||
});
|
||||
const result = await res.json();
|
||||
|
||||
if (result.ok) {
|
||||
await loadJobs();
|
||||
} else {
|
||||
alert("Fehler: " + (result.error || "unbekannt"));
|
||||
}
|
||||
}
|
||||
|
||||
function renderJobs() {
|
||||
const container = document.getElementById("jobsContainer");
|
||||
container.innerHTML = "";
|
||||
|
||||
allJobs.forEach(job => {
|
||||
const card = document.createElement("div");
|
||||
card.className = "job-card";
|
||||
|
||||
const ranksSorted = [...job.ranks].sort((a, b) => a.level - b.level);
|
||||
|
||||
const rows = ranksSorted.map(r => `
|
||||
<tr>
|
||||
<td>${r.level}</td>
|
||||
<td>${r.title}</td>
|
||||
<td>${r.salary}$ / Min</td>
|
||||
<td>
|
||||
<button class="secondary" onclick="editRank(${job.id}, ${r.id}, ${r.level}, '${escapeAttr(r.title)}', ${r.salary})">Bearbeiten</button>
|
||||
<button class="danger" onclick="deleteRank(${r.id})">Löschen</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join("");
|
||||
|
||||
card.innerHTML = `
|
||||
<h3>
|
||||
${job.name} <span style="font-size:12px; color:#888;">#${job.id}</span>${job.type === "taxi" ? ' <span style="font-size:11px; background:#f5d90a; color:#111; border-radius:4px; padding:2px 6px;">🚕 Taxi</span>' : ''}${job.type === "police" ? ' <span style="font-size:11px; background:#3498db; color:#111; border-radius:4px; padding:2px 6px;">🚓 Polizei</span>' : ''}${job.type === "medic" ? ' <span style="font-size:11px; background:#2ecc71; color:#111; border-radius:4px; padding:2px 6px;">⛑️ Sanitäter</span>' : ''}${job.type === "tow" ? ' <span style="font-size:11px; background:#b8860b; color:#111; border-radius:4px; padding:2px 6px;">🚛 Abschlepper</span>' : ''}${job.type === "fire" ? ' <span style="font-size:11px; background:#c0392b; color:white; border-radius:4px; padding:2px 6px;">🚒 Feuerwehr</span>' : ''}${job.protected ? ' <span style="font-size:11px; background:#a83232; color:white; border-radius:4px; padding:2px 6px;">🔒 Geschützt</span>' : ''}
|
||||
<button class="danger" onclick="deleteJob(${job.id})">Job löschen</button>
|
||||
</h3>
|
||||
|
||||
<table>
|
||||
<thead><tr><th>Level</th><th>Titel</th><th>Gehalt</th><th></th></tr></thead>
|
||||
<tbody>${rows || "<tr><td colspan=4 style='color:#666;'>Noch keine Ränge</td></tr>"}</tbody>
|
||||
</table>
|
||||
|
||||
<div class="form-row">
|
||||
<input id="rankLevel_${job.id}" type="number" placeholder="Level" style="width:80px;" value="${ranksSorted.length + 1}">
|
||||
<input id="rankTitle_${job.id}" placeholder="Rang-Titel (z.B. Fahranfänger)">
|
||||
<input id="rankSalary_${job.id}" type="number" placeholder="Gehalt/Min" style="width:120px;">
|
||||
<input id="rankId_${job.id}" type="hidden" value="">
|
||||
<button onclick="saveRank(${job.id})">Rang speichern</button>
|
||||
<button class="secondary" onclick="resetRankForm(${job.id})">Neu</button>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:14px; padding-top:12px; border-top:1px solid #333;">
|
||||
<h4 style="margin:0 0 8px 0; color:#f5d90a; font-size:13px;">👕 Uniform (am Dienst-Punkt angezogen)</h4>
|
||||
<div class="form-row">
|
||||
<select id="uniformShirt_${job.id}">
|
||||
<option value="">Kein Oberteil</option>
|
||||
${clothingCatalog.shirt.map(i => `<option value="${i.id}" ${job.uniformShirtId === i.id ? "selected" : ""}>${i.name}</option>`).join("")}
|
||||
</select>
|
||||
<select id="uniformPants_${job.id}">
|
||||
<option value="">Keine Hose</option>
|
||||
${clothingCatalog.pants.map(i => `<option value="${i.id}" ${job.uniformPantsId === i.id ? "selected" : ""}>${i.name}</option>`).join("")}
|
||||
</select>
|
||||
<select id="uniformShoes_${job.id}">
|
||||
<option value="">Keine Schuhe</option>
|
||||
${clothingCatalog.shoes.map(i => `<option value="${i.id}" ${job.uniformShoesId === i.id ? "selected" : ""}>${i.name}</option>`).join("")}
|
||||
</select>
|
||||
<select id="uniformHelmet_${job.id}">
|
||||
<option value="">Kein Helm</option>
|
||||
${clothingCatalog.helmet.map(i => `<option value="${i.id}" ${job.uniformHelmetId === i.id ? "selected" : ""}>${i.name}</option>`).join("")}
|
||||
</select>
|
||||
<button onclick="saveUniform(${job.id})">Uniform speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
function escapeAttr(str) {
|
||||
return String(str).replace(/'/g, "\\'");
|
||||
}
|
||||
|
||||
async function createJob() {
|
||||
const name = document.getElementById("newJobName").value.trim();
|
||||
const type = document.getElementById("newJobType").value;
|
||||
const isProtected = document.getElementById("newJobProtected").checked;
|
||||
if (!name) {
|
||||
showMsg("Job-Name ist Pflicht.", true);
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await authFetch("/api/admin/jobs", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name, type, protected: isProtected })
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
showMsg("Job angelegt.");
|
||||
document.getElementById("newJobName").value = "";
|
||||
await loadJobs();
|
||||
} else {
|
||||
showMsg("Fehler: " + (data.error || "unbekannt"), true);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteJob(id) {
|
||||
if (!confirm("Diesen Job (inkl. aller Ränge) wirklich löschen?")) return;
|
||||
|
||||
const res = await authFetch("/api/admin/jobs/" + id, { method: "DELETE" });
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
showMsg("Job gelöscht.");
|
||||
await loadJobs();
|
||||
} else {
|
||||
showMsg("Fehler beim Löschen.", true);
|
||||
}
|
||||
}
|
||||
|
||||
function editRank(jobId, rankId, level, title, salary) {
|
||||
document.getElementById(`rankId_${jobId}`).value = rankId;
|
||||
document.getElementById(`rankLevel_${jobId}`).value = level;
|
||||
document.getElementById(`rankTitle_${jobId}`).value = title;
|
||||
document.getElementById(`rankSalary_${jobId}`).value = salary;
|
||||
}
|
||||
|
||||
function resetRankForm(jobId) {
|
||||
document.getElementById(`rankId_${jobId}`).value = "";
|
||||
document.getElementById(`rankTitle_${jobId}`).value = "";
|
||||
document.getElementById(`rankSalary_${jobId}`).value = "";
|
||||
}
|
||||
|
||||
async function saveRank(jobId) {
|
||||
const id = document.getElementById(`rankId_${jobId}`).value || null;
|
||||
const level = Number(document.getElementById(`rankLevel_${jobId}`).value) || 1;
|
||||
const title = document.getElementById(`rankTitle_${jobId}`).value.trim();
|
||||
const salary = Number(document.getElementById(`rankSalary_${jobId}`).value) || 0;
|
||||
|
||||
if (!title) {
|
||||
showMsg("Rang-Titel ist Pflicht.", true);
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await authFetch("/api/admin/job_ranks", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ id, jobId, level, title, salary })
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
showMsg("Rang gespeichert.");
|
||||
await loadJobs();
|
||||
} else {
|
||||
showMsg("Fehler: " + (data.error || "unbekannt"), true);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteRank(id) {
|
||||
if (!confirm("Diesen Rang wirklich löschen?")) return;
|
||||
|
||||
const res = await authFetch("/api/admin/job_ranks/" + id, { method: "DELETE" });
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
showMsg("Rang gelöscht.");
|
||||
await loadJobs();
|
||||
} else {
|
||||
showMsg("Fehler beim Löschen.", true);
|
||||
}
|
||||
}
|
||||
|
||||
function showMsg(text, isError) {
|
||||
const el = document.getElementById("jobMsg");
|
||||
el.style.color = isError ? "#e06c6c" : "#6fbf73";
|
||||
el.textContent = text;
|
||||
setTimeout(() => { el.textContent = ""; }, 4000);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// FUHRPARK (Job-Fahrzeuge)
|
||||
// -------------------------------------------------------------
|
||||
let allFleetVehicles = [];
|
||||
let allCarModels = [];
|
||||
|
||||
async function loadFleet() {
|
||||
const res = await authFetch("/api/admin/job_vehicles");
|
||||
const data = await res.json();
|
||||
allFleetVehicles = data.vehicles || [];
|
||||
renderFleetTable();
|
||||
}
|
||||
|
||||
async function loadCarModelsForFleet() {
|
||||
const res = await authFetch("/api/admin/car_configs");
|
||||
const data = await res.json();
|
||||
allCarModels = data.configs || [];
|
||||
|
||||
const select = document.getElementById("fleetModelSelect");
|
||||
select.innerHTML = allCarModels.map(c => `<option value="${c.model}">${c.model}</option>`).join("");
|
||||
}
|
||||
|
||||
function fillFleetJobSelect() {
|
||||
const select = document.getElementById("fleetJobSelect");
|
||||
select.innerHTML = allJobs.map(j => `<option value="${j.id}">${j.name}</option>`).join("");
|
||||
}
|
||||
|
||||
function jobNameById(id) {
|
||||
const job = allJobs.find(j => j.id === id);
|
||||
return job ? job.name : "Job #" + id;
|
||||
}
|
||||
|
||||
function renderFleetTable() {
|
||||
const body = document.getElementById("fleetTableBody");
|
||||
body.innerHTML = "";
|
||||
|
||||
if (allFleetVehicles.length === 0) {
|
||||
body.innerHTML = `<tr><td colspan="5" style="color:#666;">Noch keine Job-Fahrzeuge</td></tr>`;
|
||||
return;
|
||||
}
|
||||
|
||||
allFleetVehicles.forEach(v => {
|
||||
const tr = document.createElement("tr");
|
||||
tr.innerHTML = `
|
||||
<td>#${v.id}</td>
|
||||
<td>${jobNameById(v.job_id)}</td>
|
||||
<td>${v.model}</td>
|
||||
<td>${v.is_stored ? "in Garage" : "unterwegs"}</td>
|
||||
<td><button class="danger" onclick="deleteFleetVehicle(${v.id})">Löschen</button></td>
|
||||
`;
|
||||
body.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
async function addFleetVehicle() {
|
||||
const jobId = Number(document.getElementById("fleetJobSelect").value);
|
||||
const model = document.getElementById("fleetModelSelect").value;
|
||||
|
||||
if (!jobId || !model) {
|
||||
showFleetMsg("Job und Modell auswählen.", true);
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await authFetch("/api/admin/job_vehicles", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ jobId, model })
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
showFleetMsg("Fahrzeug hinzugefügt.");
|
||||
await loadFleet();
|
||||
} else {
|
||||
showFleetMsg("Fehler: " + (data.error || "unbekannt"), true);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteFleetVehicle(id) {
|
||||
if (!confirm("Dieses Job-Fahrzeug wirklich löschen?")) return;
|
||||
|
||||
const res = await authFetch("/api/admin/job_vehicles/" + id, { method: "DELETE" });
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
showFleetMsg("Fahrzeug gelöscht.");
|
||||
await loadFleet();
|
||||
} else {
|
||||
showFleetMsg("Löschen fehlgeschlagen.", true);
|
||||
}
|
||||
}
|
||||
|
||||
function showFleetMsg(text, isError) {
|
||||
const el = document.getElementById("fleetMsg");
|
||||
el.style.color = isError ? "#e06c6c" : "#6fbf73";
|
||||
el.textContent = text;
|
||||
setTimeout(() => { el.textContent = ""; }, 4000);
|
||||
}
|
||||
|
||||
loadJobs();
|
||||
loadCarModelsForFleet();
|
||||
loadFleet();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Logs</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
background: #111;
|
||||
color: #eee;
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 20px;
|
||||
}
|
||||
h1 { margin-top: 0; }
|
||||
a.back { color: #6fbf73; text-decoration: none; font-size: 14px; }
|
||||
a.back:hover { text-decoration: underline; }
|
||||
|
||||
.panel {
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-top: 16px;
|
||||
max-width: 900px;
|
||||
}
|
||||
|
||||
.filter-row { display: flex; gap: 8px; margin-bottom: 14px; align-items: center; }
|
||||
select, button {
|
||||
background: #222;
|
||||
border: 1px solid #444;
|
||||
color: #eee;
|
||||
padding: 6px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
}
|
||||
button { cursor: pointer; }
|
||||
button:hover { opacity: 0.85; }
|
||||
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th, td {
|
||||
text-align: left;
|
||||
padding: 6px 8px;
|
||||
border-bottom: 1px solid #2a2a2a;
|
||||
font-size: 13px;
|
||||
vertical-align: top;
|
||||
}
|
||||
th { color: #888; font-weight: normal; }
|
||||
|
||||
.cat-badge {
|
||||
display: inline-block;
|
||||
border-radius: 4px;
|
||||
padding: 2px 8px;
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.cat-ban { background: #5a1a1a; color: #ff8080; }
|
||||
.cat-mute { background: #5a4a1a; color: #ffd580; }
|
||||
.cat-admin_money, .cat-admin_lock, .cat-admin_duty { background: #2c6a7a; color: #cdeffb; }
|
||||
.cat-crime { background: #5a2a2a; color: #e08a8a; }
|
||||
.cat-arrest { background: #444; color: #ccc; }
|
||||
.cat-gang { background: #4a1a5a; color: #d8a8ff; }
|
||||
.cat-job_invite { background: #1a4d2a; color: #6fbf73; }
|
||||
.cat-server { background: #333; color: #aaa; }
|
||||
|
||||
.empty-hint { color: #666; padding: 10px 0; }
|
||||
.hidden { display: none !important; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<a class="back" href="/index.html">← zurück zur Startseite</a>
|
||||
<h1>📋 Logs</h1>
|
||||
|
||||
<div class="panel">
|
||||
<div class="filter-row">
|
||||
<select id="categoryFilter">
|
||||
<option value="">Alle Kategorien</option>
|
||||
<option value="ban">Bans</option>
|
||||
<option value="mute">Mutes</option>
|
||||
<option value="admin_money">Admin: Bargeld</option>
|
||||
<option value="admin_lock">Admin: Sperren</option>
|
||||
<option value="admin_duty">Admin: Dienstmodus</option>
|
||||
<option value="crime">Verbrechen</option>
|
||||
<option value="arrest">Verhaftungen</option>
|
||||
<option value="gang">Banden</option>
|
||||
<option value="job_invite">Job-Einladungen</option>
|
||||
<option value="server">Server</option>
|
||||
</select>
|
||||
<button onclick="loadLogs()">Aktualisieren</button>
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<thead><tr><th>Zeit</th><th>Kategorie</th><th>Ausgeführt von</th><th>Meldung</th></tr></thead>
|
||||
<tbody id="logsTableBody"></tbody>
|
||||
</table>
|
||||
<div class="empty-hint hidden" id="logsEmpty">Keine Log-Einträge vorhanden.</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const token = localStorage.getItem("token");
|
||||
const isAdmin = localStorage.getItem("isAdmin") === "true";
|
||||
|
||||
if (!token || !isAdmin) {
|
||||
alert("Kein Admin-Zugriff. Bitte auf der Startseite als Admin einloggen.");
|
||||
location.href = "/index.html";
|
||||
}
|
||||
|
||||
async function authFetch(url, options = {}) {
|
||||
options.headers = { ...(options.headers || {}), "Authorization": "Bearer " + token };
|
||||
const res = await fetch(url, options);
|
||||
if (res.status === 401 || res.status === 403) {
|
||||
alert("Sitzung abgelaufen oder kein Admin-Zugriff. Bitte erneut einloggen.");
|
||||
location.href = "/index.html";
|
||||
throw new Error("Nicht autorisiert");
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
const div = document.createElement("div");
|
||||
div.textContent = str;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
async function loadLogs() {
|
||||
const category = document.getElementById("categoryFilter").value;
|
||||
const url = "/api/admin/logs" + (category ? "?category=" + encodeURIComponent(category) : "");
|
||||
const res = await authFetch(url);
|
||||
const data = await res.json();
|
||||
renderLogs(data.logs || []);
|
||||
}
|
||||
|
||||
function renderLogs(logs) {
|
||||
const body = document.getElementById("logsTableBody");
|
||||
const empty = document.getElementById("logsEmpty");
|
||||
body.innerHTML = "";
|
||||
|
||||
if (logs.length === 0) {
|
||||
empty.classList.remove("hidden");
|
||||
return;
|
||||
}
|
||||
empty.classList.add("hidden");
|
||||
|
||||
logs.forEach(l => {
|
||||
const tr = document.createElement("tr");
|
||||
const date = new Date(l.created_at).toLocaleString("de-DE");
|
||||
tr.innerHTML = `
|
||||
<td>${date}</td>
|
||||
<td><span class="cat-badge cat-${l.category}">${l.category}</span></td>
|
||||
<td>${l.actor ? escapeHtml(l.actor) : "-"}</td>
|
||||
<td>${escapeHtml(l.message)}</td>
|
||||
`;
|
||||
body.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById("categoryFilter").addEventListener("change", loadLogs);
|
||||
|
||||
loadLogs();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,197 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Server-Metriken</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
background: #111;
|
||||
color: #eee;
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 20px;
|
||||
}
|
||||
h1 { margin-top: 0; }
|
||||
a.back { color: #6fbf73; text-decoration: none; font-size: 14px; }
|
||||
a.back:hover { text-decoration: underline; }
|
||||
|
||||
.stat-row { display: flex; gap: 14px; flex-wrap: wrap; margin: 16px 0; }
|
||||
.stat-card {
|
||||
background: #1a1a1a; border: 1px solid #333; border-radius: 8px;
|
||||
padding: 14px 18px; min-width: 140px;
|
||||
}
|
||||
.stat-card .label { color: #888; font-size: 12px; margin-bottom: 4px; }
|
||||
.stat-card .value { font-size: 24px; font-weight: bold; }
|
||||
.stat-card .value.warn { color: #e0a83a; }
|
||||
.stat-card .value.bad { color: #e06c6c; }
|
||||
.stat-card .value.good { color: #6fbf73; }
|
||||
|
||||
.panel {
|
||||
background: #1a1a1a; border: 1px solid #333; border-radius: 8px;
|
||||
padding: 16px; margin-top: 16px; max-width: 900px;
|
||||
}
|
||||
.panel h3 { margin-top: 0; color: #ccc; font-size: 15px; }
|
||||
canvas { display: block; background: #141414; border-radius: 4px; }
|
||||
|
||||
button { background: #2c7a3d; border: none; color: white; padding: 8px 14px; border-radius: 4px; cursor: pointer; font-size: 13px; }
|
||||
button:hover { opacity: 0.85; }
|
||||
.refresh-row { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
|
||||
#lastUpdate { color: #666; font-size: 12px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<a class="back" href="/uebersicht.html">← zurück zur Übersicht</a>
|
||||
<h1>📊 Server-Metriken</h1>
|
||||
|
||||
<div class="refresh-row">
|
||||
<button onclick="loadMetrics()">Jetzt aktualisieren</button>
|
||||
<span id="lastUpdate"></span>
|
||||
</div>
|
||||
|
||||
<div class="stat-row">
|
||||
<div class="stat-card">
|
||||
<div class="label">Spieler online</div>
|
||||
<div class="value" id="statPlayers">-</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="label">Fahrzeuge geladen</div>
|
||||
<div class="value" id="statCars">-</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="label">Physik-Tick</div>
|
||||
<div class="value" id="statTick">-</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="label">Server-Laufzeit</div>
|
||||
<div class="value" id="statUptime">-</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h3>Spieler online (letzte 24h)</h3>
|
||||
<canvas id="chartPlayers" width="860" height="180"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h3>Physik-Tick-Dauer in ms (letzte 24h) - sollte deutlich unter 50ms bleiben</h3>
|
||||
<canvas id="chartTick" width="860" height="180"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h3>Arbeitsspeicher in MB (letzte 24h)</h3>
|
||||
<canvas id="chartRam" width="860" height="180"></canvas>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const token = localStorage.getItem("token");
|
||||
if (!token) {
|
||||
alert("Bitte zuerst einloggen.");
|
||||
location.href = "/index.html";
|
||||
}
|
||||
|
||||
async function authFetch(url, options = {}) {
|
||||
options.headers = { ...(options.headers || {}), "Authorization": "Bearer " + token };
|
||||
const res = await fetch(url, options);
|
||||
if (res.status === 401 || res.status === 403) {
|
||||
alert("Sitzung abgelaufen oder keine Berechtigung. Bitte erneut einloggen.");
|
||||
location.href = "/index.html";
|
||||
throw new Error("Nicht autorisiert");
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function formatUptime(seconds) {
|
||||
const h = Math.floor(seconds / 3600);
|
||||
const m = Math.floor((seconds % 3600) / 60);
|
||||
return `${h}h ${m}m`;
|
||||
}
|
||||
|
||||
function drawLineChart(canvasId, points, options = {}) {
|
||||
const canvas = document.getElementById(canvasId);
|
||||
const ctx = canvas.getContext("2d");
|
||||
const w = canvas.width;
|
||||
const h = canvas.height;
|
||||
const padding = { top: 10, right: 10, bottom: 20, left: 40 };
|
||||
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
|
||||
if (points.length < 2) {
|
||||
ctx.fillStyle = "#666";
|
||||
ctx.font = "13px Arial";
|
||||
ctx.fillText("Noch nicht genug Daten (Punkte sammeln sich alle 60s)...", 12, h / 2);
|
||||
return;
|
||||
}
|
||||
|
||||
const values = points.map(p => p.v);
|
||||
const maxV = options.maxOverride || Math.max(...values, 1) * 1.15;
|
||||
const minV = 0;
|
||||
const plotW = w - padding.left - padding.right;
|
||||
const plotH = h - padding.top - padding.bottom;
|
||||
|
||||
// Gitterlinien + Achsenbeschriftung
|
||||
ctx.strokeStyle = "#2a2a2a";
|
||||
ctx.fillStyle = "#666";
|
||||
ctx.font = "10px Arial";
|
||||
for (let i = 0; i <= 4; i++) {
|
||||
const y = padding.top + plotH - (i / 4) * plotH;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(padding.left, y);
|
||||
ctx.lineTo(w - padding.right, y);
|
||||
ctx.stroke();
|
||||
ctx.fillText(Math.round(maxV * i / 4), 4, y + 3);
|
||||
}
|
||||
|
||||
// Linie zeichnen
|
||||
ctx.strokeStyle = options.color || "#5b8def";
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
points.forEach((p, i) => {
|
||||
const x = padding.left + (i / (points.length - 1)) * plotW;
|
||||
const y = padding.top + plotH - (Math.min(p.v, maxV) / maxV) * plotH;
|
||||
if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
|
||||
});
|
||||
ctx.stroke();
|
||||
|
||||
// Fläche unter der Linie leicht einfärben
|
||||
ctx.lineTo(padding.left + plotW, padding.top + plotH);
|
||||
ctx.lineTo(padding.left, padding.top + plotH);
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = (options.color || "#5b8def") + "22";
|
||||
ctx.fill();
|
||||
|
||||
// Zeitachse: erste/letzte/mittlere Uhrzeit anzeigen
|
||||
ctx.fillStyle = "#666";
|
||||
const first = new Date(points[0].t).toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" });
|
||||
const last = new Date(points[points.length - 1].t).toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" });
|
||||
ctx.fillText(first, padding.left, h - 4);
|
||||
ctx.fillText(last, w - padding.right - 30, h - 4);
|
||||
}
|
||||
|
||||
async function loadMetrics() {
|
||||
const res = await authFetch("/api/admin/metrics");
|
||||
const data = await res.json();
|
||||
if (!data.ok) return;
|
||||
|
||||
document.getElementById("statPlayers").textContent = data.current.players;
|
||||
document.getElementById("statCars").textContent = data.current.cars;
|
||||
|
||||
const tickEl = document.getElementById("statTick");
|
||||
tickEl.textContent = data.current.tickMs + " ms";
|
||||
tickEl.className = "value " + (data.current.tickMs > 40 ? "bad" : data.current.tickMs > 20 ? "warn" : "good");
|
||||
|
||||
document.getElementById("statUptime").textContent = formatUptime(data.current.uptimeSeconds);
|
||||
|
||||
const history = data.history || [];
|
||||
drawLineChart("chartPlayers", history.map(p => ({ t: p.t, v: p.players })), { color: "#5b8def" });
|
||||
drawLineChart("chartTick", history.map(p => ({ t: p.t, v: p.tickMs })), { color: "#e0a83a", maxOverride: 50 });
|
||||
drawLineChart("chartRam", history.map(p => ({ t: p.t, v: p.ramMb })), { color: "#6fbf73" });
|
||||
|
||||
document.getElementById("lastUpdate").textContent = "Zuletzt aktualisiert: " + new Date().toLocaleTimeString("de-DE");
|
||||
}
|
||||
|
||||
loadMetrics();
|
||||
setInterval(loadMetrics, 30000); // alle 30s automatisch neu laden
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,329 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Berechtigungen & Team</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
background: #111;
|
||||
color: #eee;
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 24px;
|
||||
max-width: 900px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
a.back { color: #6fbf73; text-decoration: none; font-size: 14px; }
|
||||
a.back:hover { text-decoration: underline; }
|
||||
h1 { margin-top: 10px; }
|
||||
|
||||
.box { background: #1a1a1a; border: 1px solid #333; border-radius: 8px; padding: 18px; margin-bottom: 20px; }
|
||||
.box h2 { margin-top: 0; font-size: 16px; border-bottom: 1px solid #333; padding-bottom: 8px; }
|
||||
|
||||
table { width: 100%; border-collapse: collapse; margin-top: 8px; }
|
||||
th, td { text-align: left; padding: 8px 6px; border-bottom: 1px solid #2a2a2a; font-size: 13px; }
|
||||
th { color: #999; font-weight: normal; }
|
||||
.group-color-dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-right: 6px; }
|
||||
|
||||
input, select {
|
||||
background: #222; border: 1px solid #444; color: #eee;
|
||||
padding: 7px 9px; border-radius: 4px; font-size: 13px;
|
||||
}
|
||||
button {
|
||||
background: #2c7a3d; border: none; color: white; padding: 7px 14px;
|
||||
border-radius: 4px; cursor: pointer; font-size: 13px;
|
||||
}
|
||||
button.secondary { background: #555; }
|
||||
button.danger { background: #a83232; }
|
||||
button:hover { opacity: 0.85; }
|
||||
|
||||
.form-row { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; margin-bottom: 12px; }
|
||||
|
||||
.perm-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 6px 16px; margin: 12px 0; }
|
||||
.perm-item { display: flex; align-items: center; gap: 8px; font-size: 13px; }
|
||||
.perm-item input { width: auto; }
|
||||
|
||||
.msg { font-size: 13px; min-height: 18px; margin-top: 8px; }
|
||||
.hidden { display: none !important; }
|
||||
|
||||
#accessDeniedBox { text-align: center; padding: 60px 20px; color: #e06c6c; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<a class="back" href="/uebersicht.html">← Übersicht</a>
|
||||
<h1>🔑 Berechtigungen & Team</h1>
|
||||
|
||||
<div id="accessDeniedBox" class="hidden">
|
||||
<h2>Kein Zugriff</h2>
|
||||
<p>Du hast keine Berechtigung, Berechtigungsgruppen zu verwalten.</p>
|
||||
</div>
|
||||
|
||||
<div id="mainContent" class="hidden">
|
||||
<div class="box">
|
||||
<h2>Vorhandene Gruppen</h2>
|
||||
<table>
|
||||
<thead><tr><th>Gruppe</th><th>Rechte</th><th>Rang</th><th></th></tr></thead>
|
||||
<tbody id="groupsTableBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h2 id="groupFormTitle">Neue Gruppe anlegen</h2>
|
||||
<div class="form-row">
|
||||
<label>Name <input type="text" id="groupName" placeholder="z.B. Moderator"></label>
|
||||
<label>Farbe <input type="color" id="groupColor" value="#6fbf73"></label>
|
||||
<label>Rang (höher = wichtiger, nur zur Sortierung) <input type="number" id="groupRank" value="0" style="width:80px;"></label>
|
||||
</div>
|
||||
|
||||
<div id="permCheckboxes" class="perm-grid"></div>
|
||||
|
||||
<div class="form-row">
|
||||
<button onclick="saveGroup()">Speichern</button>
|
||||
<button class="secondary" onclick="resetGroupForm()">Neue Gruppe (Formular leeren)</button>
|
||||
</div>
|
||||
<div class="msg" id="groupMsg"></div>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h2>Spieler einer Gruppe zuweisen</h2>
|
||||
<div class="form-row">
|
||||
<select id="playerSelect" style="min-width:220px;"></select>
|
||||
<select id="assignGroupSelect">
|
||||
<option value="">- keine Gruppe -</option>
|
||||
</select>
|
||||
<button onclick="assignSelectedPlayerGroup()">Zuweisen</button>
|
||||
</div>
|
||||
<p id="playerCurrentGroupHint" style="color:#888; font-size:12px; margin:6px 0 0;"></p>
|
||||
<div class="msg" id="assignMsg"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const token = localStorage.getItem("token");
|
||||
if (!token) {
|
||||
location.href = "/index.html";
|
||||
}
|
||||
|
||||
async function authFetch(url, options = {}) {
|
||||
options.headers = { ...(options.headers || {}), "Authorization": "Bearer " + token };
|
||||
const res = await fetch(url, options);
|
||||
return res;
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement("div");
|
||||
div.textContent = text == null ? "" : String(text);
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
let permissionCatalog = [];
|
||||
let allGroups = [];
|
||||
let editingGroupId = null;
|
||||
|
||||
async function init() {
|
||||
const res = await authFetch("/api/admin/permission_catalog");
|
||||
if (res.status === 401 || res.status === 403) {
|
||||
document.getElementById("accessDeniedBox").classList.remove("hidden");
|
||||
return;
|
||||
}
|
||||
const data = await res.json();
|
||||
permissionCatalog = data.catalog || [];
|
||||
renderPermCheckboxes();
|
||||
|
||||
document.getElementById("mainContent").classList.remove("hidden");
|
||||
await loadGroups();
|
||||
await loadAllPlayers();
|
||||
}
|
||||
|
||||
function renderPermCheckboxes() {
|
||||
const container = document.getElementById("permCheckboxes");
|
||||
container.innerHTML = permissionCatalog.map(p => `
|
||||
<label class="perm-item">
|
||||
<input type="checkbox" class="permCheckbox" value="${p.key}">
|
||||
${escapeHtml(p.label)}
|
||||
</label>
|
||||
`).join("");
|
||||
}
|
||||
|
||||
async function loadGroups() {
|
||||
const res = await authFetch("/api/admin/permission_groups");
|
||||
const data = await res.json();
|
||||
allGroups = data.groups || [];
|
||||
|
||||
const tbody = document.getElementById("groupsTableBody");
|
||||
tbody.innerHTML = "";
|
||||
allGroups.forEach(g => {
|
||||
const tr = document.createElement("tr");
|
||||
const permLabels = g.permissions.map(key => {
|
||||
const found = permissionCatalog.find(p => p.key === key);
|
||||
return found ? found.label : key;
|
||||
});
|
||||
tr.innerHTML = `
|
||||
<td><span class="group-color-dot" style="background:${g.color};"></span>${escapeHtml(g.name)}</td>
|
||||
<td style="color:#999; font-size:12px;">${permLabels.length ? escapeHtml(permLabels.join(", ")) : "- keine -"}</td>
|
||||
<td>${g.rank}</td>
|
||||
<td>
|
||||
<button class="secondary" onclick="editGroup(${g.id})">Bearbeiten</button>
|
||||
<button class="danger" onclick="deleteGroup(${g.id})">Löschen</button>
|
||||
</td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
|
||||
renderPlayerSelect();
|
||||
}
|
||||
|
||||
function editGroup(id) {
|
||||
const group = allGroups.find(g => g.id === id);
|
||||
if (!group) return;
|
||||
|
||||
editingGroupId = id;
|
||||
document.getElementById("groupFormTitle").textContent = "Gruppe bearbeiten: " + group.name;
|
||||
document.getElementById("groupName").value = group.name;
|
||||
document.getElementById("groupColor").value = group.color;
|
||||
document.getElementById("groupRank").value = group.rank;
|
||||
|
||||
document.querySelectorAll(".permCheckbox").forEach(cb => {
|
||||
cb.checked = group.permissions.includes(cb.value);
|
||||
});
|
||||
|
||||
window.scrollTo({ top: document.getElementById("groupFormTitle").offsetTop, behavior: "smooth" });
|
||||
}
|
||||
|
||||
function resetGroupForm() {
|
||||
editingGroupId = null;
|
||||
document.getElementById("groupFormTitle").textContent = "Neue Gruppe anlegen";
|
||||
document.getElementById("groupName").value = "";
|
||||
document.getElementById("groupColor").value = "#6fbf73";
|
||||
document.getElementById("groupRank").value = "0";
|
||||
document.querySelectorAll(".permCheckbox").forEach(cb => { cb.checked = false; });
|
||||
}
|
||||
|
||||
async function saveGroup() {
|
||||
const name = document.getElementById("groupName").value.trim();
|
||||
if (!name) {
|
||||
showMsg("groupMsg", "Name ist Pflicht.", true);
|
||||
return;
|
||||
}
|
||||
|
||||
const permissions = [...document.querySelectorAll(".permCheckbox:checked")].map(cb => cb.value);
|
||||
const payload = {
|
||||
id: editingGroupId,
|
||||
name,
|
||||
color: document.getElementById("groupColor").value,
|
||||
rank: Number(document.getElementById("groupRank").value) || 0,
|
||||
permissions
|
||||
};
|
||||
|
||||
const res = await authFetch("/api/admin/permission_groups", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
showMsg("groupMsg", "Gespeichert.");
|
||||
resetGroupForm();
|
||||
await loadGroups();
|
||||
} else {
|
||||
showMsg("groupMsg", "Fehler: " + (data.error || "unbekannt"), true);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteGroup(id) {
|
||||
if (!confirm("Diese Gruppe wirklich löschen? Zugewiesene Spieler verlieren dann diese Rechte.")) return;
|
||||
const res = await authFetch("/api/admin/permission_groups/" + id, { method: "DELETE" });
|
||||
const data = await res.json();
|
||||
if (data.ok) {
|
||||
await loadGroups();
|
||||
} else {
|
||||
showMsg("groupMsg", "Löschen fehlgeschlagen.", true);
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// SPIELER-ZUWEISUNG
|
||||
// -------------------------------------------------------------
|
||||
let allPlayers = [];
|
||||
|
||||
async function loadAllPlayers() {
|
||||
const res = await authFetch("/api/admin/player_group_search"); // ohne q -> alle Spieler
|
||||
const data = await res.json();
|
||||
allPlayers = data.players || [];
|
||||
renderPlayerSelect();
|
||||
}
|
||||
|
||||
function renderPlayerSelect() {
|
||||
const select = document.getElementById("playerSelect");
|
||||
const previousValue = select.value;
|
||||
|
||||
select.innerHTML = allPlayers.map(pl => {
|
||||
const currentGroup = allGroups.find(g => g.id === pl.permission_group_id);
|
||||
const suffix = pl.is_admin ? " 🛡️ Super-Admin" : (currentGroup ? ` (${currentGroup.name})` : " (keine Gruppe)");
|
||||
return `<option value="${pl.id}">${escapeHtml(pl.username)}${escapeHtml(suffix)}</option>`;
|
||||
}).join("");
|
||||
|
||||
if (previousValue) select.value = previousValue;
|
||||
|
||||
const groupSelect = document.getElementById("assignGroupSelect");
|
||||
groupSelect.innerHTML = `<option value="">- keine Gruppe -</option>` +
|
||||
allGroups.map(g => `<option value="${g.id}">${escapeHtml(g.name)}</option>`).join("");
|
||||
|
||||
updatePlayerCurrentGroupHint();
|
||||
}
|
||||
|
||||
function updatePlayerCurrentGroupHint() {
|
||||
const playerId = Number(document.getElementById("playerSelect").value);
|
||||
const pl = allPlayers.find(p => p.id === playerId);
|
||||
const hint = document.getElementById("playerCurrentGroupHint");
|
||||
if (!pl) { hint.textContent = ""; return; }
|
||||
|
||||
if (pl.is_admin) {
|
||||
hint.textContent = "Dieser Spieler ist Super-Admin und hat ohnehin immer vollen Zugriff, unabhängig von der Gruppe.";
|
||||
} else {
|
||||
const currentGroup = allGroups.find(g => g.id === pl.permission_group_id);
|
||||
hint.textContent = "Aktuelle Gruppe: " + (currentGroup ? currentGroup.name : "keine");
|
||||
}
|
||||
|
||||
document.getElementById("assignGroupSelect").value = pl.permission_group_id || "";
|
||||
}
|
||||
document.getElementById("playerSelect").addEventListener("change", updatePlayerCurrentGroupHint);
|
||||
|
||||
async function assignSelectedPlayerGroup() {
|
||||
const playerId = Number(document.getElementById("playerSelect").value);
|
||||
if (!playerId) {
|
||||
showMsg("assignMsg", "Bitte einen Spieler auswählen.", true);
|
||||
return;
|
||||
}
|
||||
const groupId = document.getElementById("assignGroupSelect").value || null;
|
||||
|
||||
const res = await authFetch("/api/admin/assign_player_group", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ playerId, groupId })
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
showMsg("assignMsg", "Gruppe zugewiesen.");
|
||||
await loadAllPlayers();
|
||||
} else {
|
||||
showMsg("assignMsg", "Fehler: " + (data.error || "unbekannt"), true);
|
||||
}
|
||||
}
|
||||
|
||||
function showMsg(elId, text, isError) {
|
||||
const el = document.getElementById(elId);
|
||||
el.style.color = isError ? "#e06c6c" : "#6fbf73";
|
||||
el.textContent = text;
|
||||
setTimeout(() => { el.textContent = ""; }, 4000);
|
||||
}
|
||||
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,418 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Spieler-Inventare</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
background: #111;
|
||||
color: #eee;
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 20px;
|
||||
}
|
||||
h1 { margin-top: 0; }
|
||||
a.back { color: #6fbf73; text-decoration: none; font-size: 14px; }
|
||||
a.back:hover { text-decoration: underline; }
|
||||
|
||||
.layout {
|
||||
display: grid;
|
||||
grid-template-columns: 280px 1fr;
|
||||
gap: 20px;
|
||||
margin-top: 16px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
input, select {
|
||||
background: #222;
|
||||
border: 1px solid #444;
|
||||
color: #eee;
|
||||
padding: 6px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
button {
|
||||
background: #2c7a3d;
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
button.danger { background: #a83232; }
|
||||
button.secondary { background: #333; }
|
||||
button:hover { opacity: 0.85; }
|
||||
|
||||
#playerSearch { width: 100%; margin-bottom: 10px; }
|
||||
|
||||
.player-item {
|
||||
padding: 8px 10px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.player-item:hover { background: #242424; }
|
||||
.player-item.active { background: #2c7a3d33; border: 1px solid #2c7a3d; }
|
||||
.player-item .sub { color: #888; font-size: 12px; }
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 10px;
|
||||
}
|
||||
th, td {
|
||||
text-align: left;
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid #333;
|
||||
font-size: 14px;
|
||||
}
|
||||
th { color: #aaa; font-weight: normal; }
|
||||
td input[type="number"] { width: 80px; }
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 14px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.msg { font-size: 13px; color: #6fbf73; min-height: 18px; margin-top: 8px; }
|
||||
.hidden { display: none !important; }
|
||||
.empty-hint { color: #666; padding: 20px 0; }
|
||||
|
||||
.player-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.player-header .money { color: #999; font-size: 13px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<a class="back" href="/index.html">← zurück zur Startseite</a> · <a class="back" href="/admin.html">Items/Shops</a>
|
||||
<h1>🎒 Spieler-Inventare</h1>
|
||||
|
||||
<div class="layout">
|
||||
<div class="panel">
|
||||
<input id="playerSearch" placeholder="Spieler suchen...">
|
||||
<div id="playerList"></div>
|
||||
</div>
|
||||
|
||||
<div class="panel" id="inventoryPanel">
|
||||
<div class="empty-hint" id="emptyHint">Wähle links einen Spieler aus.</div>
|
||||
|
||||
<div id="inventoryContent" class="hidden">
|
||||
<div class="player-header">
|
||||
<h2 id="playerName" style="margin:0;"></h2>
|
||||
<span class="money" id="playerMoney"></span>
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Item-ID</th><th>Name</th><th>Menge</th><th></th></tr>
|
||||
</thead>
|
||||
<tbody id="invTableBody"></tbody>
|
||||
</table>
|
||||
|
||||
<h3>Item hinzufügen</h3>
|
||||
<div class="form-row">
|
||||
<select id="addItemSelect"></select>
|
||||
<input id="addItemAmount" type="number" placeholder="Menge" value="1" style="width:100px;">
|
||||
<button onclick="addInventoryItem()">Hinzufügen</button>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<button onclick="saveInventory()">💾 Speichern</button>
|
||||
<button class="secondary" onclick="loadInventory(currentPlayerId)">Änderungen verwerfen</button>
|
||||
</div>
|
||||
|
||||
<div class="msg" id="invMsg"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel hidden" id="jobPanel">
|
||||
<h2 style="margin-top:0;">Job & Rang</h2>
|
||||
<div class="form-row">
|
||||
<select id="jobRankSelect" style="min-width:280px;"></select>
|
||||
<button onclick="saveJobRank()">Übernehmen</button>
|
||||
<button class="danger" onclick="clearJobRank()">Job entfernen</button>
|
||||
</div>
|
||||
<div class="msg" id="jobMsg"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// -------------------------------------------------------------
|
||||
// ZUGRIFFSSCHUTZ: nur eingeloggte Admins dürfen diese Seite nutzen
|
||||
// -------------------------------------------------------------
|
||||
const token = localStorage.getItem("token");
|
||||
// Voll-Admin ODER passende Gruppen-Berechtigung reicht jetzt aus - der
|
||||
// Server prüft das bei jedem Aufruf ohnehin final ab (siehe requireAdmin);
|
||||
// hier reicht ein simpler Login-Check, authFetch() fängt fehlende Rechte
|
||||
// beim ersten echten API-Aufruf sauber ab (Meldung + Weiterleitung)
|
||||
let isAdmin = localStorage.getItem("isAdmin") === "true";
|
||||
|
||||
if (!token) {
|
||||
alert("Bitte zuerst einloggen.");
|
||||
location.href = "/index.html";
|
||||
}
|
||||
|
||||
async function authFetch(url, options = {}) {
|
||||
options.headers = { ...(options.headers || {}), "Authorization": "Bearer " + token };
|
||||
const res = await fetch(url, options);
|
||||
if (res.status === 401 || res.status === 403) {
|
||||
alert("Sitzung abgelaufen oder kein Admin-Zugriff. Bitte erneut einloggen.");
|
||||
location.href = "/index.html";
|
||||
throw new Error("Nicht autorisiert");
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
let allPlayers = [];
|
||||
let allItems = [];
|
||||
let currentPlayerId = null;
|
||||
let currentInventory = [];
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// SPIELERLISTE
|
||||
// -------------------------------------------------------------
|
||||
async function loadPlayers() {
|
||||
const res = await authFetch("/api/admin/players");
|
||||
const data = await res.json();
|
||||
allPlayers = data.players || [];
|
||||
renderPlayerList();
|
||||
}
|
||||
|
||||
function renderPlayerList() {
|
||||
const list = document.getElementById("playerList");
|
||||
const filter = document.getElementById("playerSearch").value.toLowerCase();
|
||||
list.innerHTML = "";
|
||||
|
||||
allPlayers
|
||||
.filter(p => p.username.toLowerCase().includes(filter))
|
||||
.forEach(p => {
|
||||
const div = document.createElement("div");
|
||||
div.className = "player-item" + (p.id === currentPlayerId ? " active" : "");
|
||||
div.innerHTML = `
|
||||
${p.username}
|
||||
<div class="sub">ID ${p.id} — ${p.money}$ Bargeld</div>
|
||||
`;
|
||||
div.onclick = () => loadInventory(p.id);
|
||||
list.appendChild(div);
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById("playerSearch").addEventListener("input", renderPlayerList);
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// ITEMS (für die Auswahl beim Hinzufügen)
|
||||
// -------------------------------------------------------------
|
||||
async function loadItems() {
|
||||
const res = await authFetch("/api/admin/items");
|
||||
const data = await res.json();
|
||||
allItems = data.items || [];
|
||||
|
||||
const select = document.getElementById("addItemSelect");
|
||||
select.innerHTML = allItems.map(i =>
|
||||
`<option value="${i.id}">${i.name} (${i.id})</option>`
|
||||
).join("");
|
||||
}
|
||||
|
||||
function itemName(id) {
|
||||
const item = allItems.find(i => i.id === id);
|
||||
return item ? item.name : "(unbekanntes Item)";
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// INVENTAR LADEN / ANZEIGEN
|
||||
// -------------------------------------------------------------
|
||||
async function loadInventory(playerId) {
|
||||
currentPlayerId = playerId;
|
||||
renderPlayerList();
|
||||
|
||||
const res = await authFetch(`/api/admin/players/${playerId}/inventory`);
|
||||
const data = await res.json();
|
||||
|
||||
if (!data.ok) {
|
||||
showMsg("Fehler: " + (data.error || "unbekannt"), true);
|
||||
return;
|
||||
}
|
||||
|
||||
currentInventory = data.inventory || [];
|
||||
|
||||
document.getElementById("emptyHint").classList.add("hidden");
|
||||
document.getElementById("inventoryContent").classList.remove("hidden");
|
||||
document.getElementById("playerName").textContent = data.username;
|
||||
|
||||
const player = allPlayers.find(p => p.id === playerId);
|
||||
document.getElementById("playerMoney").textContent = player
|
||||
? `${player.money}$ Bargeld — ${player.bank}$ Bank`
|
||||
: "";
|
||||
|
||||
renderInventoryTable();
|
||||
|
||||
document.getElementById("jobPanel").classList.remove("hidden");
|
||||
await loadJobRankOptions(player ? player.job_rank_id : null);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// JOB & RANG (Beförderung)
|
||||
// -------------------------------------------------------------
|
||||
let allJobsWithRanks = [];
|
||||
|
||||
async function loadJobRankOptions(currentRankId) {
|
||||
if (allJobsWithRanks.length === 0) {
|
||||
const res = await authFetch("/api/admin/jobs");
|
||||
const data = await res.json();
|
||||
allJobsWithRanks = data.jobs || [];
|
||||
}
|
||||
|
||||
const select = document.getElementById("jobRankSelect");
|
||||
select.innerHTML = `<option value="">Kein Job</option>`;
|
||||
|
||||
allJobsWithRanks.forEach(job => {
|
||||
[...job.ranks].sort((a, b) => a.level - b.level).forEach(r => {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = r.id;
|
||||
opt.textContent = `${job.name} - Rang ${r.level}: ${r.title} (${r.salary}$/Min)`;
|
||||
if (r.id === currentRankId) opt.selected = true;
|
||||
select.appendChild(opt);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function saveJobRank() {
|
||||
if (currentPlayerId === null) return;
|
||||
|
||||
const rankId = document.getElementById("jobRankSelect").value || null;
|
||||
|
||||
const res = await authFetch(`/api/admin/players/${currentPlayerId}/job`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ rankId })
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
showJobMsg("Job/Rang übernommen.");
|
||||
await loadPlayers();
|
||||
} else {
|
||||
showJobMsg("Fehler beim Speichern.", true);
|
||||
}
|
||||
}
|
||||
|
||||
function clearJobRank() {
|
||||
document.getElementById("jobRankSelect").value = "";
|
||||
saveJobRank();
|
||||
}
|
||||
|
||||
function showJobMsg(text, isError) {
|
||||
const el = document.getElementById("jobMsg");
|
||||
el.style.color = isError ? "#e06c6c" : "#6fbf73";
|
||||
el.textContent = text;
|
||||
setTimeout(() => { el.textContent = ""; }, 4000);
|
||||
}
|
||||
|
||||
function renderInventoryTable() {
|
||||
const body = document.getElementById("invTableBody");
|
||||
body.innerHTML = "";
|
||||
|
||||
if (currentInventory.length === 0) {
|
||||
body.innerHTML = `<tr><td colspan="4" style="color:#666;">Inventar ist leer</td></tr>`;
|
||||
return;
|
||||
}
|
||||
|
||||
currentInventory.forEach((item, index) => {
|
||||
const tr = document.createElement("tr");
|
||||
tr.innerHTML = `
|
||||
<td>${item.id}</td>
|
||||
<td>${itemName(item.id)}</td>
|
||||
<td><input type="number" min="0" value="${item.amount}" data-index="${index}" class="amountInput"></td>
|
||||
<td><button class="danger" data-index="${index}">Entfernen</button></td>
|
||||
`;
|
||||
body.appendChild(tr);
|
||||
});
|
||||
|
||||
body.querySelectorAll(".amountInput").forEach(input => {
|
||||
input.addEventListener("change", () => {
|
||||
const idx = Number(input.dataset.index);
|
||||
currentInventory[idx].amount = Math.max(0, Number(input.value) || 0);
|
||||
});
|
||||
});
|
||||
|
||||
body.querySelectorAll("button.danger").forEach(btn => {
|
||||
btn.addEventListener("click", () => {
|
||||
const idx = Number(btn.dataset.index);
|
||||
currentInventory.splice(idx, 1);
|
||||
renderInventoryTable();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function addInventoryItem() {
|
||||
const itemId = document.getElementById("addItemSelect").value;
|
||||
const amount = Math.max(1, Number(document.getElementById("addItemAmount").value) || 1);
|
||||
|
||||
if (!itemId) return;
|
||||
|
||||
const existing = currentInventory.find(i => i.id === itemId);
|
||||
if (existing) {
|
||||
existing.amount += amount;
|
||||
} else {
|
||||
currentInventory.push({ id: itemId, amount });
|
||||
}
|
||||
|
||||
renderInventoryTable();
|
||||
}
|
||||
|
||||
async function saveInventory() {
|
||||
if (currentPlayerId === null) return;
|
||||
|
||||
const res = await authFetch(`/api/admin/players/${currentPlayerId}/inventory`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ inventory: currentInventory })
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
showMsg("Inventar gespeichert.");
|
||||
currentInventory = data.inventory;
|
||||
renderInventoryTable();
|
||||
} else {
|
||||
showMsg("Fehler: " + (data.error || "unbekannt"), true);
|
||||
}
|
||||
}
|
||||
|
||||
function showMsg(text, isError) {
|
||||
const el = document.getElementById("invMsg");
|
||||
el.style.color = isError ? "#e06c6c" : "#6fbf73";
|
||||
el.textContent = text;
|
||||
setTimeout(() => { el.textContent = ""; }, 4000);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// INIT
|
||||
// -------------------------------------------------------------
|
||||
(async () => {
|
||||
await loadItems();
|
||||
await loadPlayers();
|
||||
})();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,777 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Orte & Inhalte verwalten</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
background: #111;
|
||||
color: #eee;
|
||||
font-family: Arial, sans-serif;
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
}
|
||||
a.back { color: #6fbf73; text-decoration: none; font-size: 14px; }
|
||||
a.back:hover { text-decoration: underline; }
|
||||
|
||||
#sidebar {
|
||||
width: 240px;
|
||||
flex-shrink: 0;
|
||||
background: #161616;
|
||||
border-right: 1px solid #333;
|
||||
padding: 16px 0;
|
||||
height: 100vh;
|
||||
overflow-y: auto;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
#sidebar .backLink { padding: 0 16px 12px; display: block; }
|
||||
.sidebar-group-label {
|
||||
padding: 10px 16px 4px; font-size: 11px; color: #666;
|
||||
text-transform: uppercase; letter-spacing: 0.5px;
|
||||
}
|
||||
.sidebar-btn {
|
||||
display: block; width: 100%; text-align: left; background: none; border: none;
|
||||
color: #ccc; padding: 8px 16px; cursor: pointer; font-size: 13px;
|
||||
}
|
||||
.sidebar-btn:hover { background: #1f1f1f; }
|
||||
.sidebar-btn.active { background: #2c7a3d; color: white; }
|
||||
|
||||
#main { flex: 1; padding: 24px; max-width: 900px; }
|
||||
h1 { margin-top: 0; font-size: 20px; }
|
||||
|
||||
.panel { display: none; }
|
||||
.panel.active { display: block; }
|
||||
|
||||
.box { background: #1a1a1a; border: 1px solid #333; border-radius: 8px; padding: 16px; margin-bottom: 16px; }
|
||||
table { width: 100%; border-collapse: collapse; margin-top: 6px; }
|
||||
th, td { text-align: left; padding: 7px 6px; border-bottom: 1px solid #2a2a2a; font-size: 13px; }
|
||||
th { color: #999; font-weight: normal; }
|
||||
|
||||
input, select, textarea {
|
||||
background: #222; border: 1px solid #444; color: #eee;
|
||||
padding: 6px 8px; border-radius: 4px; font-size: 13px; font-family: inherit;
|
||||
}
|
||||
textarea { width: 100%; resize: vertical; }
|
||||
button {
|
||||
background: #2c7a3d; border: none; color: white; padding: 6px 12px;
|
||||
border-radius: 4px; cursor: pointer; font-size: 13px;
|
||||
}
|
||||
button.danger { background: #a83232; }
|
||||
button.secondary { background: #555; }
|
||||
button:hover { opacity: 0.85; }
|
||||
|
||||
.form-grid { display: flex; gap: 8px; flex-wrap: wrap; align-items: flex-end; margin-top: 10px; }
|
||||
.form-grid label { display: flex; flex-direction: column; gap: 3px; font-size: 11px; color: #999; }
|
||||
.msg { margin-top: 10px; font-size: 13px; min-height: 18px; }
|
||||
|
||||
.news-columns { display: flex; gap: 16px; }
|
||||
.news-columns > div { flex: 1; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="sidebar">
|
||||
<a class="back backLink" href="/admin.html">← Admin-Menü</a>
|
||||
|
||||
<div class="sidebar-group-label">Orte</div>
|
||||
<div id="poiSidebarButtons"></div>
|
||||
<button class="sidebar-btn" data-panel="highway_links">🛣️ Autobahn-Verbindungen</button>
|
||||
|
||||
<div class="sidebar-group-label">Inhalte</div>
|
||||
<button class="sidebar-btn" data-panel="news">📰 News</button>
|
||||
<button class="sidebar-btn" data-panel="changelog">📝 Changelog</button>
|
||||
|
||||
<div class="sidebar-group-label">Integrationen</div>
|
||||
<button class="sidebar-btn" data-panel="discord">🔗 Discord-Webhooks</button>
|
||||
|
||||
<div class="sidebar-group-label">Admin-Dienst</div>
|
||||
<button class="sidebar-btn" data-panel="adminduty">🛡️ Admin-Look</button>
|
||||
</div>
|
||||
|
||||
<div id="main">
|
||||
<div id="poiPanels"></div>
|
||||
|
||||
<div class="panel" id="panel-highway_links">
|
||||
<h1>🛣️ Autobahn-Verbindungen verwalten</h1>
|
||||
<p style="color:#888; font-size:13px; margin-top:-8px;">
|
||||
Verbindet zwei Punkte (auch auf unterschiedlichen Karten) miteinander. Spieler werden beim
|
||||
Herannahen automatisch dorthin teleportiert - zu Fuß oder mit Auto (das Auto wird mitgenommen).
|
||||
Kein Tastendruck nötig.
|
||||
</p>
|
||||
<div class="box">
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Name</th><th>Seite A (Welt/X/Y)</th><th>Seite B (Welt/X/Y)</th><th></th></tr>
|
||||
</thead>
|
||||
<tbody id="highwayLinksTable"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="box">
|
||||
<h3 style="margin-top:0;">Neue Verbindung anlegen</h3>
|
||||
<div class="form-grid">
|
||||
<label>Name <input type="text" id="hlName" placeholder="z.B. Autobahn Nord"></label>
|
||||
</div>
|
||||
<div class="form-grid" style="margin-top:10px;">
|
||||
<label>Welt A <input type="text" id="hlWorldA" placeholder="z.B. stadt"></label>
|
||||
<label>X A (Tile) <input type="number" id="hlXA" style="width:80px;"></label>
|
||||
<label>Y A (Tile) <input type="number" id="hlYA" style="width:80px;"></label>
|
||||
</div>
|
||||
<div class="form-grid" style="margin-top:10px;">
|
||||
<label>Welt B <input type="text" id="hlWorldB" placeholder="z.B. neustadt"></label>
|
||||
<label>X B (Tile) <input type="number" id="hlXB" style="width:80px;"></label>
|
||||
<label>Y B (Tile) <input type="number" id="hlYB" style="width:80px;"></label>
|
||||
</div>
|
||||
<button onclick="addHighwayLink()" style="margin-top:12px;">Anlegen</button>
|
||||
<div class="msg" id="hlMsg"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel" id="panel-news">
|
||||
<h1>📰 News verwalten</h1>
|
||||
<div class="box">
|
||||
<table>
|
||||
<thead><tr><th>Titel</th><th>Autor</th><th>Datum</th><th></th></tr></thead>
|
||||
<tbody id="newsTableBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="box">
|
||||
<h3 style="margin-top:0;">Neue News</h3>
|
||||
<div class="form-grid" style="flex-direction:column; align-items:stretch;">
|
||||
<label>Titel <input type="text" id="newsTitle"></label>
|
||||
<label>Inhalt <textarea id="newsContent" rows="4"></textarea></label>
|
||||
<button onclick="addNews()" style="align-self:flex-start;">Veröffentlichen</button>
|
||||
</div>
|
||||
<div class="msg" id="newsMsg"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel" id="panel-changelog">
|
||||
<h1>📝 Changelog verwalten</h1>
|
||||
<div class="box">
|
||||
<table>
|
||||
<thead><tr><th>Version</th><th>Titel</th><th>Datum</th><th></th></tr></thead>
|
||||
<tbody id="changelogTableBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="box">
|
||||
<h3 style="margin-top:0;">Neuer Eintrag</h3>
|
||||
<div class="form-grid" style="flex-direction:column; align-items:stretch;">
|
||||
<label>Version <input type="text" id="changelogVersion" placeholder="z.B. 1.4.0" style="max-width:150px;"></label>
|
||||
<label>Titel <input type="text" id="changelogTitle"></label>
|
||||
<label>Inhalt <textarea id="changelogContent" rows="4"></textarea></label>
|
||||
<button onclick="addChangelog()" style="align-self:flex-start;">Veröffentlichen</button>
|
||||
</div>
|
||||
<div class="msg" id="changelogMsg"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel" id="panel-discord">
|
||||
<h1>🔗 Discord-Webhooks</h1>
|
||||
<p style="color:#888; font-size:13px;">
|
||||
Für jeden Bereich kannst du separat festlegen, in welchen Discord-Kanal die Nachrichten gehen sollen.
|
||||
Webhook-URL bekommst du in Discord unter Servereinstellungen → Integrationen → Webhooks → "Neuer Webhook".
|
||||
Feld leer lassen und speichern entfernt den Webhook wieder.
|
||||
</p>
|
||||
|
||||
<div class="box">
|
||||
<h3 style="margin-top:0;">📰 News-Kanal</h3>
|
||||
<div class="form-grid" style="flex-direction:column; align-items:stretch;">
|
||||
<input type="text" id="discordUrl_news" placeholder="https://discord.com/api/webhooks/...">
|
||||
<button onclick="saveDiscordWebhook('news')" style="align-self:flex-start;">Speichern</button>
|
||||
</div>
|
||||
<div class="msg" id="discordMsg_news"></div>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h3 style="margin-top:0;">📝 Changelog-Kanal</h3>
|
||||
<div class="form-grid" style="flex-direction:column; align-items:stretch;">
|
||||
<input type="text" id="discordUrl_changelog" placeholder="https://discord.com/api/webhooks/...">
|
||||
<button onclick="saveDiscordWebhook('changelog')" style="align-self:flex-start;">Speichern</button>
|
||||
</div>
|
||||
<div class="msg" id="discordMsg_changelog"></div>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h3 style="margin-top:0;">⚠️ Neustart-Ankündigungen-Kanal</h3>
|
||||
<div class="form-grid" style="flex-direction:column; align-items:stretch;">
|
||||
<input type="text" id="discordUrl_restart" placeholder="https://discord.com/api/webhooks/...">
|
||||
<button onclick="saveDiscordWebhook('restart')" style="align-self:flex-start;">Speichern</button>
|
||||
</div>
|
||||
<div class="msg" id="discordMsg_restart"></div>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h3 style="margin-top:0;">🔧 Wartungsmodus-Kanal</h3>
|
||||
<div class="form-grid" style="flex-direction:column; align-items:stretch;">
|
||||
<input type="text" id="discordUrl_maintenance" placeholder="https://discord.com/api/webhooks/...">
|
||||
<button onclick="saveDiscordWebhook('maintenance')" style="align-self:flex-start;">Speichern</button>
|
||||
</div>
|
||||
<div class="msg" id="discordMsg_maintenance"></div>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h3 style="margin-top:0;">🟢 Online-Status-Kanal</h3>
|
||||
<p style="color:#888; font-size:12px; margin-top:-4px;">
|
||||
Postet eine sich selbst aktualisierende Nachricht mit der aktuellen Spielerzahl (bearbeitet
|
||||
immer dieselbe Nachricht statt zu spammen) - aktualisiert bei Login/Logout und alle 5 Minuten.
|
||||
</p>
|
||||
<div class="form-grid" style="flex-direction:column; align-items:stretch;">
|
||||
<input type="text" id="discordUrl_online_status" placeholder="https://discord.com/api/webhooks/...">
|
||||
<button onclick="saveDiscordWebhook('online_status')" style="align-self:flex-start;">Speichern</button>
|
||||
</div>
|
||||
<div class="msg" id="discordMsg_online_status"></div>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h3 style="margin-top:0;">🎫 Support-Tickets</h3>
|
||||
<p style="color:#888; font-size:12px; margin-top:-4px;">Meldet jedes neue Support-Ticket.</p>
|
||||
<div class="form-grid" style="flex-direction:column; align-items:stretch;">
|
||||
<input type="text" id="discordUrl_tickets" placeholder="https://discord.com/api/webhooks/...">
|
||||
<button onclick="saveDiscordWebhook('tickets')" style="align-self:flex-start;">Speichern</button>
|
||||
</div>
|
||||
<div class="msg" id="discordMsg_tickets"></div>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h3 style="margin-top:0;">💡 Wünsche</h3>
|
||||
<p style="color:#888; font-size:12px; margin-top:-4px;">Meldet jeden neuen Spieler-Wunsch.</p>
|
||||
<div class="form-grid" style="flex-direction:column; align-items:stretch;">
|
||||
<input type="text" id="discordUrl_wishes" placeholder="https://discord.com/api/webhooks/...">
|
||||
<button onclick="saveDiscordWebhook('wishes')" style="align-self:flex-start;">Speichern</button>
|
||||
</div>
|
||||
<div class="msg" id="discordMsg_wishes"></div>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h3 style="margin-top:0;">📊 Umfragen</h3>
|
||||
<p style="color:#888; font-size:12px; margin-top:-4px;">Meldet jede neu erstellte Umfrage.</p>
|
||||
<div class="form-grid" style="flex-direction:column; align-items:stretch;">
|
||||
<input type="text" id="discordUrl_surveys" placeholder="https://discord.com/api/webhooks/...">
|
||||
<button onclick="saveDiscordWebhook('surveys')" style="align-self:flex-start;">Speichern</button>
|
||||
</div>
|
||||
<div class="msg" id="discordMsg_surveys"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel" id="panel-adminduty">
|
||||
<h1>🛡️ Admin-Dienst: Aussehen</h1>
|
||||
<p style="color:#888; font-size:13px;">
|
||||
Wenn ein Admin den Admin-Dienstmodus aktiviert, zieht er automatisch die hier festgelegte
|
||||
Kleidung + den Skin an (bildet eine erkennbare "Held"-Figur) und darf Admin-Befehle nutzen.
|
||||
Beim Beenden des Dienstes kommt die vorherige Kleidung automatisch zurück.
|
||||
Leer lassen = kein automatischer Wechsel für dieses Feld.
|
||||
</p>
|
||||
<div class="box">
|
||||
<div class="form-grid">
|
||||
<label>Oberteil
|
||||
<select id="adminShirtSelect"><option value="">- keins -</option></select>
|
||||
</label>
|
||||
<label>Hose
|
||||
<select id="adminPantsSelect"><option value="">- keine -</option></select>
|
||||
</label>
|
||||
<label>Schuhe
|
||||
<select id="adminShoesSelect"><option value="">- keine -</option></select>
|
||||
</label>
|
||||
<label>Helm
|
||||
<select id="adminHelmetSelect"><option value="">- keiner -</option></select>
|
||||
</label>
|
||||
<label>Skin (Figur)
|
||||
<select id="adminSkinSelect"><option value="">- keiner -</option></select>
|
||||
</label>
|
||||
<button onclick="saveAdminLook()">Speichern</button>
|
||||
</div>
|
||||
<div class="msg" id="adminLookMsg"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// -------------------------------------------------------------
|
||||
// ZUGRIFFSSCHUTZ
|
||||
// -------------------------------------------------------------
|
||||
const token = localStorage.getItem("token");
|
||||
// Voll-Admin ODER passende Gruppen-Berechtigung reicht - der Server prüft
|
||||
// das pro Bereich final ab, authFetch() fängt fehlende Rechte sauber ab
|
||||
let isAdmin = localStorage.getItem("isAdmin") === "true";
|
||||
if (!token) {
|
||||
alert("Bitte zuerst einloggen.");
|
||||
location.href = "/index.html";
|
||||
}
|
||||
async function authFetch(url, options = {}) {
|
||||
options.headers = { ...(options.headers || {}), "Authorization": "Bearer " + token };
|
||||
const res = await fetch(url, options);
|
||||
if (res.status === 401 || res.status === 403) {
|
||||
alert("Sitzung abgelaufen oder kein Admin-Zugriff. Bitte erneut einloggen.");
|
||||
location.href = "/index.html";
|
||||
throw new Error("Nicht autorisiert");
|
||||
}
|
||||
return res;
|
||||
}
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement("div");
|
||||
div.textContent = text == null ? "" : String(text);
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// KONFIGURATION ALLER ORTS-TYPEN (config-gesteuert statt 12x Copy-Paste)
|
||||
// -------------------------------------------------------------
|
||||
const POI_CONFIGS = [
|
||||
{ key: "gas_stations", label: "Tankstellen", icon: "⛽", endpoint: "/api/admin/gas_stations", listKey: "gasStations",
|
||||
priceField: "price", priceLabel: "Preis/Liter", priceDefault: 2 },
|
||||
{ key: "repair_shops", label: "Werkstätten", icon: "🔧", endpoint: "/api/admin/repair_shops", listKey: "repairShops",
|
||||
priceField: "pricePerPoint", priceLabel: "Preis/Reparatur-Punkt", priceDefault: 5 },
|
||||
{ key: "garages", label: "Garagen", icon: "🚗", endpoint: "/api/admin/garages", listKey: "garages",
|
||||
extraFields: [{ name: "jobId", label: "Job-ID (optional)", type: "number" }] },
|
||||
{ key: "taxi_stands", label: "Taxistände", icon: "🚕", endpoint: "/api/admin/taxi_stands", listKey: "taxiStands" },
|
||||
{ key: "hospitals", label: "Krankenhäuser", icon: "🏥", endpoint: "/api/admin/hospitals", listKey: "hospitals" },
|
||||
{ key: "prisons", label: "Gefängnisse", icon: "🔒", endpoint: "/api/admin/prisons", listKey: "prisons" },
|
||||
{ key: "fire_stations", label: "Feuerwachen", icon: "🚒", endpoint: "/api/admin/fire_stations", listKey: "stations" },
|
||||
{ key: "impound_lots", label: "Abschlepphöfe", icon: "🚛", endpoint: "/api/admin/impound_lots", listKey: "lots" },
|
||||
{ key: "jobcenters", label: "Jobcenter", icon: "💼", endpoint: "/api/admin/jobcenters", listKey: "jobcenters" },
|
||||
{ key: "insurance_offices", label: "Versicherungsbüros", icon: "🛡️", endpoint: "/api/admin/insurance_offices", listKey: "offices" },
|
||||
{ key: "plate_offices", label: "Zulassungsstellen", icon: "🔖", endpoint: "/api/admin/plate_offices", listKey: "offices" },
|
||||
{ key: "clothing_shops", label: "Kleidungsläden", icon: "👕", endpoint: "/api/admin/clothing_shops", listKey: "shops" },
|
||||
{ key: "trailer_shops", label: "Anhänger-Shops", icon: "🚛", endpoint: "/api/admin/trailer_shops", listKey: "shops" },
|
||||
{ key: "black_market_spots", label: "Hehler (Schwarzmarkt)", icon: "🕶️", endpoint: "/api/admin/black_market_spots", listKey: "shops" }
|
||||
];
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// SIDEBAR + PANELS AUFBAUEN
|
||||
// -------------------------------------------------------------
|
||||
const sidebarButtonsEl = document.getElementById("poiSidebarButtons");
|
||||
const poiPanelsEl = document.getElementById("poiPanels");
|
||||
|
||||
POI_CONFIGS.forEach(cfg => {
|
||||
const btn = document.createElement("button");
|
||||
btn.className = "sidebar-btn";
|
||||
btn.dataset.panel = "poi_" + cfg.key;
|
||||
btn.textContent = `${cfg.icon} ${cfg.label}`;
|
||||
sidebarButtonsEl.appendChild(btn);
|
||||
|
||||
const panel = document.createElement("div");
|
||||
panel.className = "panel";
|
||||
panel.id = "panel-poi_" + cfg.key;
|
||||
panel.innerHTML = `
|
||||
<h1>${cfg.icon} ${cfg.label} verwalten</h1>
|
||||
<div class="box">
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Name</th><th>Welt</th><th>X</th><th>Y</th>${cfg.priceField ? `<th>${cfg.priceLabel}</th>` : ""}<th></th></tr>
|
||||
</thead>
|
||||
<tbody id="poiTable_${cfg.key}"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="box">
|
||||
<h3 style="margin-top:0;">Neuen Eintrag anlegen</h3>
|
||||
<p style="color:#888; font-size:12px; margin-top:-4px;">
|
||||
Schneller geht's meist direkt im Map-Editor per Klick - hier für schnelle Textmengen-Erfassung.
|
||||
</p>
|
||||
<div class="form-grid">
|
||||
<label>Name <input type="text" id="poiName_${cfg.key}"></label>
|
||||
<label>Welt <input type="text" id="poiWorld_${cfg.key}" placeholder="z.B. stadt"></label>
|
||||
<label>X (Tile) <input type="number" id="poiX_${cfg.key}" style="width:80px;"></label>
|
||||
<label>Y (Tile) <input type="number" id="poiY_${cfg.key}" style="width:80px;"></label>
|
||||
${cfg.priceField ? `<label>${cfg.priceLabel} <input type="number" step="0.01" id="poiPrice_${cfg.key}" value="${cfg.priceDefault}" style="width:100px;"></label>` : ""}
|
||||
${(cfg.extraFields || []).map(f => `<label>${f.label} <input type="${f.type}" id="poiExtra_${cfg.key}_${f.name}" style="width:120px;"></label>`).join("")}
|
||||
<button onclick="addPoi('${cfg.key}')">Anlegen</button>
|
||||
</div>
|
||||
<div class="msg" id="poiMsg_${cfg.key}"></div>
|
||||
</div>
|
||||
`;
|
||||
poiPanelsEl.appendChild(panel);
|
||||
});
|
||||
|
||||
function poiConfig(key) {
|
||||
return POI_CONFIGS.find(c => c.key === key);
|
||||
}
|
||||
|
||||
async function loadPoi(key) {
|
||||
const cfg = poiConfig(key);
|
||||
const res = await authFetch(cfg.endpoint);
|
||||
const data = await res.json();
|
||||
const items = data[cfg.listKey] || [];
|
||||
|
||||
const tbody = document.getElementById("poiTable_" + key);
|
||||
tbody.innerHTML = "";
|
||||
items.forEach(item => {
|
||||
const tr = document.createElement("tr");
|
||||
let priceCell = "";
|
||||
if (cfg.priceField) {
|
||||
const currentPrice = item.price !== undefined ? item.price : item.price_per_point;
|
||||
priceCell = `<td>
|
||||
<input type="number" step="0.01" value="${currentPrice}" id="poiPriceEdit_${key}_${item.id}" style="width:70px;">
|
||||
<button class="secondary" onclick="updatePoiPrice('${key}', ${item.id})" style="padding:4px 6px; font-size:11px;">✓</button>
|
||||
</td>`;
|
||||
}
|
||||
tr.innerHTML = `
|
||||
<td>#${item.id} ${escapeHtml(item.name)}</td>
|
||||
<td>${escapeHtml(item.world)}</td>
|
||||
<td>${item.x}</td>
|
||||
<td>${item.y}</td>
|
||||
${priceCell}
|
||||
<td><button class="danger" onclick="deletePoi('${key}', ${item.id})">Löschen</button></td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
async function addPoi(key) {
|
||||
const cfg = poiConfig(key);
|
||||
const name = document.getElementById(`poiName_${key}`).value.trim();
|
||||
const world = document.getElementById(`poiWorld_${key}`).value.trim();
|
||||
const x = Number(document.getElementById(`poiX_${key}`).value);
|
||||
const y = Number(document.getElementById(`poiY_${key}`).value);
|
||||
|
||||
if (!name || !world || isNaN(x) || isNaN(y)) {
|
||||
showPoiMsg(key, "Name, Welt, X und Y sind Pflicht.", true);
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = { name, world, x, y };
|
||||
if (cfg.priceField) {
|
||||
payload[cfg.priceField] = Number(document.getElementById(`poiPrice_${key}`).value) || cfg.priceDefault;
|
||||
}
|
||||
(cfg.extraFields || []).forEach(f => {
|
||||
const val = document.getElementById(`poiExtra_${key}_${f.name}`).value;
|
||||
if (val !== "") payload[f.name] = f.type === "number" ? Number(val) : val;
|
||||
});
|
||||
|
||||
const res = await authFetch(cfg.endpoint, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
showPoiMsg(key, "Angelegt.");
|
||||
document.getElementById(`poiName_${key}`).value = "";
|
||||
document.getElementById(`poiX_${key}`).value = "";
|
||||
document.getElementById(`poiY_${key}`).value = "";
|
||||
await loadPoi(key);
|
||||
} else {
|
||||
showPoiMsg(key, "Fehler: " + (data.error || "unbekannt"), true);
|
||||
}
|
||||
}
|
||||
|
||||
async function updatePoiPrice(key, id) {
|
||||
const cfg = poiConfig(key);
|
||||
const value = Number(document.getElementById(`poiPriceEdit_${key}_${id}`).value);
|
||||
const payload = { id };
|
||||
payload[cfg.priceField] = value;
|
||||
|
||||
const res = await authFetch(cfg.endpoint, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const data = await res.json();
|
||||
showPoiMsg(key, data.ok ? "Preis aktualisiert." : "Fehler beim Aktualisieren.", !data.ok);
|
||||
if (data.ok) await loadPoi(key);
|
||||
}
|
||||
|
||||
async function deletePoi(key, id) {
|
||||
if (!confirm("Diesen Eintrag wirklich löschen?")) return;
|
||||
const cfg = poiConfig(key);
|
||||
const res = await authFetch(`${cfg.endpoint}/${id}`, { method: "DELETE" });
|
||||
const data = await res.json();
|
||||
showPoiMsg(key, data.ok ? "Gelöscht." : "Löschen fehlgeschlagen.", !data.ok);
|
||||
if (data.ok) await loadPoi(key);
|
||||
}
|
||||
|
||||
function showPoiMsg(key, text, isError) {
|
||||
const el = document.getElementById("poiMsg_" + key);
|
||||
el.style.color = isError ? "#e06c6c" : "#6fbf73";
|
||||
el.textContent = text;
|
||||
setTimeout(() => { el.textContent = ""; }, 4000);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// NEWS
|
||||
// -------------------------------------------------------------
|
||||
async function loadNews() {
|
||||
const res = await fetch("/api/news");
|
||||
const data = await res.json();
|
||||
const tbody = document.getElementById("newsTableBody");
|
||||
tbody.innerHTML = "";
|
||||
(data.news || []).forEach(n => {
|
||||
const tr = document.createElement("tr");
|
||||
tr.innerHTML = `
|
||||
<td>${escapeHtml(n.title)}</td>
|
||||
<td>${escapeHtml(n.author)}</td>
|
||||
<td>${new Date(n.created_at).toLocaleString("de-DE")}</td>
|
||||
<td><button class="danger" onclick="deleteNews(${n.id})">Löschen</button></td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
async function addNews() {
|
||||
const title = document.getElementById("newsTitle").value.trim();
|
||||
const content = document.getElementById("newsContent").value.trim();
|
||||
if (!title || !content) {
|
||||
showMsg("newsMsg", "Titel und Inhalt erforderlich.", true);
|
||||
return;
|
||||
}
|
||||
const res = await authFetch("/api/admin/news", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ title, content })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.ok) {
|
||||
showMsg("newsMsg", "Veröffentlicht.");
|
||||
document.getElementById("newsTitle").value = "";
|
||||
document.getElementById("newsContent").value = "";
|
||||
await loadNews();
|
||||
} else {
|
||||
showMsg("newsMsg", "Fehler: " + (data.error || "unbekannt"), true);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteNews(id) {
|
||||
if (!confirm("Diese News wirklich löschen?")) return;
|
||||
const res = await authFetch("/api/admin/news/" + id, { method: "DELETE" });
|
||||
const data = await res.json();
|
||||
showMsg("newsMsg", data.ok ? "Gelöscht." : "Löschen fehlgeschlagen.", !data.ok);
|
||||
if (data.ok) await loadNews();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// CHANGELOG
|
||||
// -------------------------------------------------------------
|
||||
async function loadChangelog() {
|
||||
const res = await fetch("/api/changelog");
|
||||
const data = await res.json();
|
||||
const tbody = document.getElementById("changelogTableBody");
|
||||
tbody.innerHTML = "";
|
||||
(data.changelog || []).forEach(c => {
|
||||
const tr = document.createElement("tr");
|
||||
tr.innerHTML = `
|
||||
<td>${escapeHtml(c.version)}</td>
|
||||
<td>${escapeHtml(c.title)}</td>
|
||||
<td>${new Date(c.created_at).toLocaleString("de-DE")}</td>
|
||||
<td><button class="danger" onclick="deleteChangelog(${c.id})">Löschen</button></td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
async function addChangelog() {
|
||||
const version = document.getElementById("changelogVersion").value.trim();
|
||||
const title = document.getElementById("changelogTitle").value.trim();
|
||||
const content = document.getElementById("changelogContent").value.trim();
|
||||
if (!version || !title || !content) {
|
||||
showMsg("changelogMsg", "Version, Titel und Inhalt erforderlich.", true);
|
||||
return;
|
||||
}
|
||||
const res = await authFetch("/api/admin/changelog", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ version, title, content })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.ok) {
|
||||
showMsg("changelogMsg", "Veröffentlicht.");
|
||||
document.getElementById("changelogVersion").value = "";
|
||||
document.getElementById("changelogTitle").value = "";
|
||||
document.getElementById("changelogContent").value = "";
|
||||
await loadChangelog();
|
||||
} else {
|
||||
showMsg("changelogMsg", "Fehler: " + (data.error || "unbekannt"), true);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteChangelog(id) {
|
||||
if (!confirm("Diesen Changelog-Eintrag wirklich löschen?")) return;
|
||||
const res = await authFetch("/api/admin/changelog/" + id, { method: "DELETE" });
|
||||
const data = await res.json();
|
||||
showMsg("changelogMsg", data.ok ? "Gelöscht." : "Löschen fehlgeschlagen.", !data.ok);
|
||||
if (data.ok) await loadChangelog();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// DISCORD-WEBHOOKS
|
||||
// -------------------------------------------------------------
|
||||
async function loadDiscordWebhooks() {
|
||||
const res = await authFetch("/api/admin/discord_webhooks");
|
||||
const data = await res.json();
|
||||
const webhooks = data.webhooks || {};
|
||||
|
||||
["news", "changelog", "restart", "maintenance", "online_status", "tickets", "wishes", "surveys"].forEach(key => {
|
||||
const input = document.getElementById("discordUrl_" + key);
|
||||
if (input) input.value = webhooks[key] || "";
|
||||
});
|
||||
}
|
||||
|
||||
async function saveDiscordWebhook(key) {
|
||||
const url = document.getElementById("discordUrl_" + key).value.trim();
|
||||
|
||||
const res = await authFetch("/api/admin/discord_webhooks", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ key, url })
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
showMsg("discordMsg_" + key, url ? "Gespeichert." : "Webhook entfernt.");
|
||||
} else {
|
||||
showMsg("discordMsg_" + key, "Fehler: " + (data.error || "unbekannt"), true);
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// ADMIN-DIENST: AUSSEHEN
|
||||
// -------------------------------------------------------------
|
||||
async function loadAdminLookPanel() {
|
||||
const [clothingRes, skinRes, lookRes] = await Promise.all([
|
||||
authFetch("/api/admin/clothing_items"),
|
||||
authFetch("/api/skin_catalog"),
|
||||
authFetch("/api/admin/admin_look")
|
||||
]);
|
||||
const clothingData = await clothingRes.json();
|
||||
const skinData = await skinRes.json();
|
||||
const look = await lookRes.json();
|
||||
|
||||
const bySlot = { shirt: [], pants: [], shoes: [], helmet: [] };
|
||||
(clothingData.items || []).forEach(item => {
|
||||
if (bySlot[item.slot]) bySlot[item.slot].push(item);
|
||||
});
|
||||
|
||||
fillSelect("adminShirtSelect", bySlot.shirt, look.shirtId);
|
||||
fillSelect("adminPantsSelect", bySlot.pants, look.pantsId);
|
||||
fillSelect("adminShoesSelect", bySlot.shoes, look.shoesId);
|
||||
fillSelect("adminHelmetSelect", bySlot.helmet, look.helmetId);
|
||||
fillSelect("adminSkinSelect", skinData.items || [], look.skinId);
|
||||
}
|
||||
|
||||
function fillSelect(selectId, items, currentValue) {
|
||||
const select = document.getElementById(selectId);
|
||||
const placeholder = select.options[0];
|
||||
select.innerHTML = "";
|
||||
select.appendChild(placeholder);
|
||||
items.forEach(item => {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = item.id;
|
||||
opt.textContent = item.name;
|
||||
if (String(item.id) === String(currentValue)) opt.selected = true;
|
||||
select.appendChild(opt);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadHighwayLinksTable() {
|
||||
const res = await authFetch("/api/admin/highway_links");
|
||||
const data = await res.json();
|
||||
const links = data.links || [];
|
||||
|
||||
const tbody = document.getElementById("highwayLinksTable");
|
||||
tbody.innerHTML = "";
|
||||
links.forEach(l => {
|
||||
const tr = document.createElement("tr");
|
||||
tr.innerHTML = `
|
||||
<td>${escapeHtml(l.name)}</td>
|
||||
<td>${escapeHtml(l.world_a)} / ${l.x_a} / ${l.y_a}</td>
|
||||
<td>${escapeHtml(l.world_b)} / ${l.x_b} / ${l.y_b}</td>
|
||||
<td><button class="secondary" onclick="deleteHighwayLink(${l.id})">Löschen</button></td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
async function addHighwayLink() {
|
||||
const name = document.getElementById("hlName").value.trim();
|
||||
const worldA = document.getElementById("hlWorldA").value.trim();
|
||||
const xA = Number(document.getElementById("hlXA").value);
|
||||
const yA = Number(document.getElementById("hlYA").value);
|
||||
const worldB = document.getElementById("hlWorldB").value.trim();
|
||||
const xB = Number(document.getElementById("hlXB").value);
|
||||
const yB = Number(document.getElementById("hlYB").value);
|
||||
|
||||
if (!worldA || !worldB || isNaN(xA) || isNaN(yA) || isNaN(xB) || isNaN(yB)) {
|
||||
showMsg("hlMsg", "Beide Welten und alle Koordinaten sind Pflicht.", true);
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await authFetch("/api/admin/highway_links", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name, worldA, xA, yA, worldB, xB, yB })
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
showMsg("hlMsg", "Verbindung angelegt.");
|
||||
await loadHighwayLinksTable();
|
||||
} else {
|
||||
showMsg("hlMsg", "Fehler: " + (data.error || "unbekannt"), true);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteHighwayLink(id) {
|
||||
if (!confirm("Diese Autobahn-Verbindung wirklich löschen?")) return;
|
||||
await authFetch(`/api/admin/highway_links/${id}`, { method: "DELETE" });
|
||||
await loadHighwayLinksTable();
|
||||
}
|
||||
|
||||
async function saveAdminLook() {
|
||||
const payload = {
|
||||
shirtId: document.getElementById("adminShirtSelect").value,
|
||||
pantsId: document.getElementById("adminPantsSelect").value,
|
||||
shoesId: document.getElementById("adminShoesSelect").value,
|
||||
helmetId: document.getElementById("adminHelmetSelect").value,
|
||||
skinId: document.getElementById("adminSkinSelect").value
|
||||
};
|
||||
|
||||
const res = await authFetch("/api/admin/admin_look", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const data = await res.json();
|
||||
showMsg("adminLookMsg", data.ok ? "Gespeichert." : "Fehler beim Speichern.", !data.ok);
|
||||
}
|
||||
|
||||
function showMsg(elId, text, isError) {
|
||||
const el = document.getElementById(elId);
|
||||
el.style.color = isError ? "#e06c6c" : "#6fbf73";
|
||||
el.textContent = text;
|
||||
setTimeout(() => { el.textContent = ""; }, 4000);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// TAB-NAVIGATION
|
||||
// -------------------------------------------------------------
|
||||
document.querySelectorAll(".sidebar-btn").forEach(btn => {
|
||||
btn.addEventListener("click", () => {
|
||||
document.querySelectorAll(".sidebar-btn").forEach(b => b.classList.remove("active"));
|
||||
document.querySelectorAll(".panel").forEach(p => p.classList.remove("active"));
|
||||
btn.classList.add("active");
|
||||
const panel = document.getElementById("panel-" + btn.dataset.panel);
|
||||
if (panel) panel.classList.add("active");
|
||||
|
||||
if (btn.dataset.panel.startsWith("poi_")) {
|
||||
loadPoi(btn.dataset.panel.replace("poi_", ""));
|
||||
} else if (btn.dataset.panel === "news") {
|
||||
loadNews();
|
||||
} else if (btn.dataset.panel === "changelog") {
|
||||
loadChangelog();
|
||||
} else if (btn.dataset.panel === "discord") {
|
||||
loadDiscordWebhooks();
|
||||
} else if (btn.dataset.panel === "highway_links") {
|
||||
loadHighwayLinksTable();
|
||||
} else if (btn.dataset.panel === "adminduty") {
|
||||
loadAdminLookPanel();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Ersten Tab (erste Orts-Kategorie) initial anzeigen
|
||||
document.querySelector(".sidebar-btn").click();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,205 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Radiosender</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
background: #111;
|
||||
color: #eee;
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 20px;
|
||||
}
|
||||
h1 { margin-top: 0; }
|
||||
a.back { color: #6fbf73; text-decoration: none; font-size: 14px; }
|
||||
a.back:hover { text-decoration: underline; }
|
||||
|
||||
.panel {
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-top: 16px;
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
table { width: 100%; border-collapse: collapse; margin-top: 10px; }
|
||||
th, td {
|
||||
text-align: left;
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid #333;
|
||||
font-size: 14px;
|
||||
}
|
||||
th { color: #aaa; font-weight: normal; }
|
||||
|
||||
input {
|
||||
background: #222;
|
||||
border: 1px solid #444;
|
||||
color: #eee;
|
||||
padding: 6px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
button {
|
||||
background: #2c7a3d;
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
button.danger { background: #a83232; }
|
||||
button:hover { opacity: 0.85; }
|
||||
|
||||
.form-row { display: flex; gap: 8px; margin-top: 12px; }
|
||||
.form-row input:first-child { width: 200px; }
|
||||
.form-row input:last-of-type { flex: 1; }
|
||||
|
||||
.msg { font-size: 13px; color: #6fbf73; min-height: 18px; margin-top: 8px; }
|
||||
.empty-hint { color: #666; padding: 10px 0; }
|
||||
.hidden { display: none !important; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<a class="back" href="/index.html">← zurück zur Startseite</a>
|
||||
<h1>📻 Radiosender</h1>
|
||||
|
||||
<div class="panel">
|
||||
<p style="color:#888; font-size:13px; margin-top:0;">
|
||||
Trag hier Internet-Radio-Streams ein (direkte MP3/AAC-Stream-URLs, z.B. von öffentlichen Shoutcast/Icecast-Streams).
|
||||
Spieler wechseln im Auto mit der Taste <strong>R</strong> durch, "Aus" ist immer als erste Option mit dabei.
|
||||
</p>
|
||||
|
||||
<table>
|
||||
<thead><tr><th>Name</th><th>Stream-URL</th><th></th></tr></thead>
|
||||
<tbody id="stationsTableBody"></tbody>
|
||||
</table>
|
||||
<div class="empty-hint hidden" id="stationsEmpty">Noch keine Sender eingetragen.</div>
|
||||
|
||||
<h3>Sender hinzufügen</h3>
|
||||
<div class="form-row">
|
||||
<input id="stationName" placeholder="Name (z.B. Radio XY)">
|
||||
<input id="stationUrl" placeholder="https://stream.beispiel.de/live.mp3">
|
||||
<button onclick="addStation()">Hinzufügen</button>
|
||||
</div>
|
||||
<div class="msg" id="stationMsg"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const token = localStorage.getItem("token");
|
||||
// Voll-Admin ODER passende Gruppen-Berechtigung reicht jetzt aus - der
|
||||
// Server prüft das bei jedem Aufruf ohnehin final ab (siehe requireAdmin);
|
||||
// hier reicht ein simpler Login-Check, authFetch() fängt fehlende Rechte
|
||||
// beim ersten echten API-Aufruf sauber ab (Meldung + Weiterleitung)
|
||||
let isAdmin = localStorage.getItem("isAdmin") === "true";
|
||||
|
||||
if (!token) {
|
||||
alert("Bitte zuerst einloggen.");
|
||||
location.href = "/index.html";
|
||||
}
|
||||
|
||||
async function authFetch(url, options = {}) {
|
||||
options.headers = { ...(options.headers || {}), "Authorization": "Bearer " + token };
|
||||
const res = await fetch(url, options);
|
||||
if (res.status === 401 || res.status === 403) {
|
||||
alert("Sitzung abgelaufen oder kein Admin-Zugriff. Bitte erneut einloggen.");
|
||||
location.href = "/index.html";
|
||||
throw new Error("Nicht autorisiert");
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
const div = document.createElement("div");
|
||||
div.textContent = str;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
let allStations = [];
|
||||
|
||||
async function loadStations() {
|
||||
const res = await authFetch("/api/admin/radio_stations");
|
||||
const data = await res.json();
|
||||
allStations = data.stations || [];
|
||||
renderStations();
|
||||
}
|
||||
|
||||
function renderStations() {
|
||||
const body = document.getElementById("stationsTableBody");
|
||||
const empty = document.getElementById("stationsEmpty");
|
||||
body.innerHTML = "";
|
||||
|
||||
if (allStations.length === 0) {
|
||||
empty.classList.remove("hidden");
|
||||
return;
|
||||
}
|
||||
empty.classList.add("hidden");
|
||||
|
||||
allStations.forEach(s => {
|
||||
const tr = document.createElement("tr");
|
||||
tr.innerHTML = `
|
||||
<td>${escapeHtml(s.name)}</td>
|
||||
<td style="word-break:break-all; color:#999; font-size:12px;">${escapeHtml(s.url)}</td>
|
||||
<td><button class="danger" onclick="deleteStation(${s.id})">Löschen</button></td>
|
||||
`;
|
||||
body.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
async function addStation() {
|
||||
const name = document.getElementById("stationName").value.trim();
|
||||
const url = document.getElementById("stationUrl").value.trim();
|
||||
|
||||
if (!name || !url) {
|
||||
showMsg("Name und Stream-URL sind Pflicht.", true);
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await authFetch("/api/admin/radio_stations", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name, url })
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
showMsg("Sender hinzugefügt.");
|
||||
document.getElementById("stationName").value = "";
|
||||
document.getElementById("stationUrl").value = "";
|
||||
await loadStations();
|
||||
} else {
|
||||
showMsg("Fehler: " + (data.error || "unbekannt"), true);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteStation(id) {
|
||||
if (!confirm("Diesen Sender wirklich löschen?")) return;
|
||||
|
||||
const res = await authFetch("/api/admin/radio_stations/" + id, { method: "DELETE" });
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
showMsg("Sender gelöscht.");
|
||||
await loadStations();
|
||||
} else {
|
||||
showMsg("Löschen fehlgeschlagen.", true);
|
||||
}
|
||||
}
|
||||
|
||||
function showMsg(text, isError) {
|
||||
const el = document.getElementById("stationMsg");
|
||||
el.style.color = isError ? "#e06c6c" : "#6fbf73";
|
||||
el.textContent = text;
|
||||
setTimeout(() => { el.textContent = ""; }, 4000);
|
||||
}
|
||||
|
||||
loadStations();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,211 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Einstellungen</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
background: #111;
|
||||
color: #eee;
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 20px;
|
||||
}
|
||||
h1 { margin-top: 0; }
|
||||
a.back { color: #6fbf73; text-decoration: none; font-size: 14px; }
|
||||
a.back:hover { text-decoration: underline; }
|
||||
|
||||
.panel {
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-top: 16px;
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th, td {
|
||||
text-align: left;
|
||||
padding: 10px 8px;
|
||||
border-bottom: 1px solid #333;
|
||||
font-size: 14px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
th { color: #aaa; font-weight: normal; }
|
||||
.setting-desc { color: #888; font-size: 12px; margin-top: 2px; }
|
||||
|
||||
input {
|
||||
background: #222;
|
||||
border: 1px solid #444;
|
||||
color: #eee;
|
||||
padding: 6px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
width: 100px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
button {
|
||||
background: #2c7a3d;
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
}
|
||||
button:hover { opacity: 0.85; }
|
||||
|
||||
.default-hint { color: #666; font-size: 11px; }
|
||||
.msg { font-size: 13px; color: #6fbf73; min-height: 18px; margin-top: 8px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<a class="back" href="/index.html">← zurück zur Startseite</a>
|
||||
<h1>⚙️ Einstellungen</h1>
|
||||
|
||||
<div class="panel">
|
||||
<p style="color:#888; font-size:13px; margin-top:0;">
|
||||
Änderungen wirken sofort auf dem laufenden Server, kein Neustart nötig.
|
||||
</p>
|
||||
|
||||
<table>
|
||||
<thead><tr><th>Einstellung</th><th>Wert</th><th></th></tr></thead>
|
||||
<tbody id="settingsTableBody"></tbody>
|
||||
</table>
|
||||
<div class="msg" id="settingsMsg"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const token = localStorage.getItem("token");
|
||||
// Voll-Admin ODER passende Gruppen-Berechtigung reicht jetzt aus - der
|
||||
// Server prüft das bei jedem Aufruf ohnehin final ab (siehe requireAdmin);
|
||||
// hier reicht ein simpler Login-Check, authFetch() fängt fehlende Rechte
|
||||
// beim ersten echten API-Aufruf sauber ab (Meldung + Weiterleitung)
|
||||
let isAdmin = localStorage.getItem("isAdmin") === "true";
|
||||
|
||||
if (!token) {
|
||||
alert("Bitte zuerst einloggen.");
|
||||
location.href = "/index.html";
|
||||
}
|
||||
|
||||
async function authFetch(url, options = {}) {
|
||||
options.headers = { ...(options.headers || {}), "Authorization": "Bearer " + token };
|
||||
const res = await fetch(url, options);
|
||||
if (res.status === 401 || res.status === 403) {
|
||||
alert("Sitzung abgelaufen oder kein Admin-Zugriff. Bitte erneut einloggen.");
|
||||
location.href = "/index.html";
|
||||
throw new Error("Nicht autorisiert");
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
const div = document.createElement("div");
|
||||
div.textContent = str;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
const SETTING_LABELS = {
|
||||
salary_interval_minutes: { name: "Gehalts-Intervall", desc: "Alle wie viele Minuten wird Gehalt ausgezahlt", unit: "Min." },
|
||||
hunger_decay_per_tick: { name: "Hunger-Abnahme", desc: "Abnahme pro Sekunde (kleiner = langsamer hungrig)", unit: "" },
|
||||
thirst_decay_per_tick: { name: "Durst-Abnahme", desc: "Abnahme pro Sekunde (kleiner = langsamer durstig)", unit: "" },
|
||||
health_regen_per_tick: { name: "Leben-Regeneration", desc: "Zunahme pro Sekunde, wenn Hunger UND Durst über 50 sind", unit: "" },
|
||||
health_decay_per_tick: { name: "Leben-Verfall", desc: "Abnahme pro Sekunde, wenn Hunger ODER Durst unter 50 sind", unit: "" },
|
||||
shop_owner_cut_percent: { name: "Shop-Besitzer-Anteil", desc: "Prozent-Anteil je Verkauf für den Shop-Besitzer", unit: "%" },
|
||||
gas_owner_cut_percent: { name: "Tankstellen-Besitzer-Anteil", desc: "Prozent-Anteil je Betankung für den Besitzer", unit: "%" },
|
||||
tow_fee: { name: "Abschlepp-Belohnung", desc: "Betrag pro abgeliefertem Fahrzeug am Abschlepphof", unit: "$" },
|
||||
car_collision_damage_factor: { name: "Auto-Kollisionsschaden", desc: "Schaden pro Geschwindigkeitseinheit bei Auto-gegen-Auto-Kollision", unit: "" },
|
||||
pedestrian_damage_factor: { name: "Fußgänger-Kollisionsschaden", desc: "Schaden pro Geschwindigkeitseinheit, wenn ein Auto einen Fußgänger anfährt", unit: "" },
|
||||
insurance_fee_interval_minutes: { name: "Versicherung: Intervall", desc: "Alle wie viele Minuten wird die Versicherungsgebühr abgebucht", unit: "Min." },
|
||||
insurance_fee_amount: { name: "Versicherung: Gebühr", desc: "Betrag pro Abbuchung und versichertem Auto", unit: "$" },
|
||||
insurance_repair_discount_percent: { name: "Versicherung: Rabatt bei Totalschaden", desc: "Prozent-Rabatt auf die Reparatur, wenn das Auto bei 0 Zustand repariert wird", unit: "%" },
|
||||
tax_interval_minutes: { name: "Steuer: Intervall", desc: "Alle wie viele Minuten wird Steuer abgezogen", unit: "Min." },
|
||||
tax_percent: { name: "Steuer: Satz", desc: "Prozent vom Bank-Guthaben, der bei jedem Intervall abgezogen wird", unit: "%" },
|
||||
tuning_cost_per_level: { name: "Tuning: Kosten pro Stufe", desc: "Basiskosten, steigt mit jeder weiteren Stufe (Stufe 2 kostet doppelt usw.)", unit: "$" },
|
||||
tuning_bonus_per_level_percent: { name: "Tuning: Bonus pro Stufe", desc: "Wie viel Prozent mehr Leistung pro Tuning-Stufe", unit: "%" },
|
||||
tuning_max_level: { name: "Tuning: Maximalstufe", desc: "Wie viele Stufen pro Kategorie (Motor/Beschleunigung/Bremsen) möglich sind", unit: "" },
|
||||
tuning_paint_cost: { name: "Tuning: Lackierungs-Preis", desc: "Kosten für eine neue Lackierung", unit: "$" },
|
||||
npc_traffic_enabled: { name: "NPC-Verkehr", desc: "NPC-Autos serverweit an/aus - wirkt sofort, kein Neustart nötig", unit: "", boolean: true },
|
||||
cargo_drop_enabled: { name: "Zufalls-Funde", desc: "LKW-Ladungs-Zufallsfunde auf Straßen an/aus", unit: "", boolean: true },
|
||||
cargo_drop_interval_minutes: { name: "Zufalls-Funde: Intervall", desc: "Alle wie viele Minuten wird die Chance auf einen Fund geprüft", unit: "Min." },
|
||||
cargo_drop_chance_percent: { name: "Zufalls-Funde: Chance", desc: "Wahrscheinlichkeit pro Intervall, dass wirklich einer spawnt", unit: "%" },
|
||||
cargo_drop_expiry_minutes: { name: "Zufalls-Funde: Ablaufzeit", desc: "Nach wie vielen Minuten ein nicht abgeholter Fund verschwindet", unit: "Min." }
|
||||
};
|
||||
|
||||
async function loadSettings() {
|
||||
const res = await authFetch("/api/admin/settings");
|
||||
const data = await res.json();
|
||||
|
||||
const currentValues = {};
|
||||
(data.settings || []).forEach(s => { currentValues[s.setting_key] = s.setting_value; });
|
||||
|
||||
const allKeys = Object.keys(data.defaults || {});
|
||||
const body = document.getElementById("settingsTableBody");
|
||||
body.innerHTML = "";
|
||||
|
||||
allKeys.forEach(key => {
|
||||
const label = SETTING_LABELS[key] || { name: key, desc: "", unit: "" };
|
||||
const value = currentValues[key] !== undefined ? currentValues[key] : data.defaults[key];
|
||||
|
||||
const inputHtml = label.boolean
|
||||
? `<input type="checkbox" class="settingInput" data-key="${key}" data-boolean="true" ${Number(value) ? "checked" : ""} style="width:20px; height:20px;">`
|
||||
: `<input type="text" class="settingInput" data-key="${key}" value="${escapeHtml(String(value))}">
|
||||
${label.unit ? escapeHtml(label.unit) : ""}`;
|
||||
|
||||
const tr = document.createElement("tr");
|
||||
tr.innerHTML = `
|
||||
<td>
|
||||
<strong>${escapeHtml(label.name)}</strong>
|
||||
<div class="setting-desc">${escapeHtml(label.desc)}</div>
|
||||
<div class="default-hint">Standard: ${label.boolean ? (Number(data.defaults[key]) ? "an" : "aus") : data.defaults[key]}</div>
|
||||
</td>
|
||||
<td>${inputHtml}</td>
|
||||
<td><button class="saveBtn" data-key="${key}">Speichern</button></td>
|
||||
`;
|
||||
body.appendChild(tr);
|
||||
});
|
||||
|
||||
body.querySelectorAll(".saveBtn").forEach(btn => {
|
||||
btn.onclick = () => saveSetting(btn.dataset.key);
|
||||
});
|
||||
}
|
||||
|
||||
async function saveSetting(key) {
|
||||
const input = document.querySelector(`.settingInput[data-key="${key}"]`);
|
||||
const isBoolean = input.dataset.boolean === "true";
|
||||
const value = isBoolean ? (input.checked ? "1" : "0") : input.value.trim();
|
||||
|
||||
if (!isBoolean && (value === "" || isNaN(Number(value)))) {
|
||||
showMsg("Bitte eine gültige Zahl eingeben.", true);
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await authFetch("/api/admin/settings", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ key, value })
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
showMsg("Gespeichert - wirkt sofort.");
|
||||
} else {
|
||||
showMsg("Fehler: " + (data.error || "unbekannt"), true);
|
||||
}
|
||||
}
|
||||
|
||||
function showMsg(text, isError) {
|
||||
const el = document.getElementById("settingsMsg");
|
||||
el.style.color = isError ? "#e06c6c" : "#6fbf73";
|
||||
el.textContent = text;
|
||||
setTimeout(() => { el.textContent = ""; }, 4000);
|
||||
}
|
||||
|
||||
loadSettings();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,346 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Skin-Editor</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
background: #111;
|
||||
color: #eee;
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 20px;
|
||||
}
|
||||
h1 { margin-top: 0; }
|
||||
a.back { color: #6fbf73; text-decoration: none; font-size: 14px; }
|
||||
a.back:hover { text-decoration: underline; }
|
||||
|
||||
.panel {
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-top: 16px;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
table { width: 100%; border-collapse: collapse; margin-top: 10px; }
|
||||
th, td { text-align: left; padding: 8px; border-bottom: 1px solid #333; font-size: 14px; }
|
||||
th { color: #aaa; font-weight: normal; }
|
||||
.swatch { display: inline-block; width: 32px; height: 32px; border-radius: 4px; vertical-align: middle; margin-right: 6px; border: 1px solid #555; background-size: cover; image-rendering: pixelated; }
|
||||
|
||||
input { background: #222; border: 1px solid #444; color: #eee; padding: 6px 8px; border-radius: 4px; font-size: 14px; box-sizing: border-box; }
|
||||
button { background: #2c7a3d; border: none; color: white; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 13px; }
|
||||
button.danger { background: #a83232; }
|
||||
button.secondary { background: #555; }
|
||||
button:hover { opacity: 0.85; }
|
||||
|
||||
.form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 10px; margin-top: 12px; }
|
||||
.form-grid label { display: block; font-size: 12px; color: #999; margin-bottom: 4px; }
|
||||
|
||||
#editorArea { display: flex; gap: 24px; align-items: flex-start; flex-wrap: wrap; margin-top: 16px; }
|
||||
#pixelCanvas { border: 1px solid #444; cursor: crosshair; image-rendering: pixelated; background: #444; }
|
||||
.msg { font-size: 13px; color: #6fbf73; min-height: 18px; margin-top: 8px; }
|
||||
.hint { color: #888; font-size: 12px; margin-top: 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<a class="back" href="/index.html">← zurück zur Startseite</a>
|
||||
<h1>🎨 Skin-Editor</h1>
|
||||
<p class="hint">Skins werden mit exakt 20×20 Pixeln gemalt (dieselbe Größe wie der Körper-Kasten im Spiel) und darüber gezeichnet, statt nur ein Emoji zu sein.</p>
|
||||
|
||||
<div class="panel">
|
||||
<table>
|
||||
<thead><tr><th></th><th>Name</th><th></th></tr></thead>
|
||||
<tbody id="itemsTableBody"></tbody>
|
||||
</table>
|
||||
<div class="hint" id="itemsEmpty" style="display:none;">Noch keine Skins angelegt.</div>
|
||||
|
||||
<h3 style="margin-top:24px;">Skin anlegen / bearbeiten</h3>
|
||||
<div class="form-grid">
|
||||
<div>
|
||||
<label>Name</label>
|
||||
<input id="itemName" placeholder="z.B. Punk-Frisur">
|
||||
</div>
|
||||
<div>
|
||||
<button onclick="newItemForm()" class="secondary">Neu (Formular leeren)</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="editorArea">
|
||||
<div>
|
||||
<canvas id="pixelCanvas" width="320" height="320"></canvas>
|
||||
</div>
|
||||
<div style="display:flex; flex-direction:column; gap:8px; min-width:180px;">
|
||||
<label style="font-size:12px; color:#999;">Malfarbe</label>
|
||||
<input id="pixelColor" type="color" value="#f1c27d" style="width:60px; height:36px; padding:2px; cursor:pointer;">
|
||||
|
||||
<div style="display:flex; gap:4px; flex-wrap:wrap; margin-top:4px;">
|
||||
<button onclick="presetFace()" style="font-size:11px;">Gesicht-Grundform</button>
|
||||
</div>
|
||||
|
||||
<button onclick="clearPixelCanvas()" class="danger" style="margin-top:8px;">Löschen (transparent)</button>
|
||||
<button onclick="saveItem()">Speichern</button>
|
||||
<button onclick="deleteCurrentItem()" class="danger" id="deleteBtn" style="display:none;">Löschen</button>
|
||||
<div class="msg" id="itemMsg"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const token = localStorage.getItem("token");
|
||||
// Voll-Admin ODER passende Gruppen-Berechtigung reicht jetzt aus - der
|
||||
// Server prüft das bei jedem Aufruf ohnehin final ab (siehe requireAdmin);
|
||||
// hier reicht ein simpler Login-Check, authFetch() fängt fehlende Rechte
|
||||
// beim ersten echten API-Aufruf sauber ab (Meldung + Weiterleitung)
|
||||
let isAdmin = localStorage.getItem("isAdmin") === "true";
|
||||
|
||||
if (!token) {
|
||||
alert("Bitte zuerst einloggen.");
|
||||
location.href = "/index.html";
|
||||
}
|
||||
|
||||
async function authFetch(url, options = {}) {
|
||||
options.headers = { ...(options.headers || {}), "Authorization": "Bearer " + token };
|
||||
const res = await fetch(url, options);
|
||||
if (res.status === 401 || res.status === 403) {
|
||||
alert("Sitzung abgelaufen oder kein Admin-Zugriff. Bitte erneut einloggen.");
|
||||
location.href = "/index.html";
|
||||
throw new Error("Nicht autorisiert");
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
const div = document.createElement("div");
|
||||
div.textContent = str;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
const GRID = 20; // exakt Körper-Kasten-Größe im Spiel
|
||||
const SCALE = 16;
|
||||
|
||||
let allItems = [];
|
||||
let currentEditId = null;
|
||||
let pixelGrid = [];
|
||||
|
||||
const canvas = document.getElementById("pixelCanvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
canvas.width = GRID * SCALE;
|
||||
canvas.height = GRID * SCALE;
|
||||
|
||||
function fillGridTransparent() {
|
||||
pixelGrid = [];
|
||||
for (let y = 0; y < GRID; y++) {
|
||||
const row = [];
|
||||
for (let x = 0; x < GRID; x++) row.push(null); // null = transparent
|
||||
pixelGrid.push(row);
|
||||
}
|
||||
}
|
||||
|
||||
function redrawCanvas() {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
// Schachbrett-Muster als Transparenz-Hintergrund
|
||||
for (let y = 0; y < GRID; y++) {
|
||||
for (let x = 0; x < GRID; x++) {
|
||||
if (pixelGrid[y][x]) {
|
||||
ctx.fillStyle = pixelGrid[y][x];
|
||||
ctx.fillRect(x * SCALE, y * SCALE, SCALE, SCALE);
|
||||
} else {
|
||||
ctx.fillStyle = (x + y) % 2 === 0 ? "#3a3a3a" : "#2a2a2a";
|
||||
ctx.fillRect(x * SCALE, y * SCALE, SCALE, SCALE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function loadTextureIntoGrid(dataUrl) {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
const off = document.createElement("canvas");
|
||||
off.width = GRID;
|
||||
off.height = GRID;
|
||||
const offCtx = off.getContext("2d");
|
||||
offCtx.drawImage(img, 0, 0, GRID, GRID);
|
||||
const data = offCtx.getImageData(0, 0, GRID, GRID).data;
|
||||
|
||||
for (let y = 0; y < GRID; y++) {
|
||||
for (let x = 0; x < GRID; x++) {
|
||||
const i = (y * GRID + x) * 4;
|
||||
const alpha = data[i + 3];
|
||||
pixelGrid[y][x] = alpha > 10 ? `rgb(${data[i]},${data[i + 1]},${data[i + 2]})` : null;
|
||||
}
|
||||
}
|
||||
redrawCanvas();
|
||||
};
|
||||
img.src = dataUrl;
|
||||
}
|
||||
|
||||
function exportGridAsDataUrl() {
|
||||
const off = document.createElement("canvas");
|
||||
off.width = GRID;
|
||||
off.height = GRID;
|
||||
const offCtx = off.getContext("2d");
|
||||
for (let y = 0; y < GRID; y++) {
|
||||
for (let x = 0; x < GRID; x++) {
|
||||
if (pixelGrid[y][x]) {
|
||||
offCtx.fillStyle = pixelGrid[y][x];
|
||||
offCtx.fillRect(x, y, 1, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
return off.toDataURL("image/png");
|
||||
}
|
||||
|
||||
let isPainting = false;
|
||||
let isErasing = false;
|
||||
function paintAt(clientX, clientY, erase) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const x = Math.floor((clientX - rect.left) / (rect.width / GRID));
|
||||
const y = Math.floor((clientY - rect.top) / (rect.height / GRID));
|
||||
if (x < 0 || y < 0 || x >= GRID || y >= GRID) return;
|
||||
|
||||
pixelGrid[y][x] = erase ? null : document.getElementById("pixelColor").value;
|
||||
redrawCanvas();
|
||||
}
|
||||
canvas.addEventListener("mousedown", e => {
|
||||
isPainting = true;
|
||||
isErasing = e.button === 2;
|
||||
paintAt(e.clientX, e.clientY, isErasing);
|
||||
});
|
||||
canvas.addEventListener("mousemove", e => { if (isPainting) paintAt(e.clientX, e.clientY, isErasing); });
|
||||
canvas.addEventListener("contextmenu", e => e.preventDefault());
|
||||
window.addEventListener("mouseup", () => { isPainting = false; });
|
||||
|
||||
function clearPixelCanvas() {
|
||||
fillGridTransparent();
|
||||
redrawCanvas();
|
||||
}
|
||||
|
||||
function presetFace() {
|
||||
// Einfache Kopf-Grundform als Startpunkt (Kreis-artig, Rest bleibt transparent)
|
||||
const skin = document.getElementById("pixelColor").value;
|
||||
for (let y = 3; y < 15; y++) {
|
||||
for (let x = 4; x < 16; x++) {
|
||||
pixelGrid[y][x] = skin;
|
||||
}
|
||||
}
|
||||
redrawCanvas();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// LADEN / ANZEIGEN
|
||||
// -------------------------------------------------------------
|
||||
async function loadItems() {
|
||||
const res = await authFetch("/api/admin/skin_items");
|
||||
const data = await res.json();
|
||||
allItems = data.items || [];
|
||||
renderItemsTable();
|
||||
}
|
||||
|
||||
function renderItemsTable() {
|
||||
const body = document.getElementById("itemsTableBody");
|
||||
const empty = document.getElementById("itemsEmpty");
|
||||
body.innerHTML = "";
|
||||
empty.style.display = allItems.length === 0 ? "block" : "none";
|
||||
|
||||
allItems.forEach(item => {
|
||||
const tr = document.createElement("tr");
|
||||
tr.innerHTML = `
|
||||
<td><span class="swatch"></span></td>
|
||||
<td>${escapeHtml(item.name)}</td>
|
||||
<td>
|
||||
<button onclick="editItem(${item.id})">Bearbeiten</button>
|
||||
<button class="danger" onclick="quickDelete(${item.id})">Löschen</button>
|
||||
</td>
|
||||
`;
|
||||
body.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
function newItemForm() {
|
||||
currentEditId = null;
|
||||
document.getElementById("itemName").value = "";
|
||||
document.getElementById("deleteBtn").style.display = "none";
|
||||
fillGridTransparent();
|
||||
redrawCanvas();
|
||||
}
|
||||
|
||||
async function editItem(id) {
|
||||
const item = allItems.find(i => i.id === id);
|
||||
if (!item) return;
|
||||
|
||||
const res = await authFetch("/api/admin/skin_items/" + id);
|
||||
const data = await res.json();
|
||||
if (!data.ok) return;
|
||||
|
||||
currentEditId = id;
|
||||
document.getElementById("itemName").value = data.item.name;
|
||||
document.getElementById("deleteBtn").style.display = "inline-block";
|
||||
|
||||
fillGridTransparent();
|
||||
redrawCanvas();
|
||||
if (data.item.image_data) loadTextureIntoGrid(data.item.image_data);
|
||||
}
|
||||
|
||||
async function saveItem() {
|
||||
const name = document.getElementById("itemName").value.trim();
|
||||
if (!name) {
|
||||
showMsg("Name ist Pflicht.", true);
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = { id: currentEditId, name, imageData: exportGridAsDataUrl() };
|
||||
|
||||
const res = await authFetch("/api/admin/skin_items", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
showMsg("Gespeichert.");
|
||||
await loadItems();
|
||||
newItemForm();
|
||||
} else {
|
||||
showMsg("Fehler: " + (data.error || "unbekannt"), true);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteCurrentItem() {
|
||||
if (currentEditId === null) return;
|
||||
await quickDelete(currentEditId);
|
||||
newItemForm();
|
||||
}
|
||||
|
||||
async function quickDelete(id) {
|
||||
if (!confirm("Diesen Skin wirklich löschen?")) return;
|
||||
const res = await authFetch("/api/admin/skin_items/" + id, { method: "DELETE" });
|
||||
const data = await res.json();
|
||||
if (data.ok) {
|
||||
showMsg("Gelöscht.");
|
||||
await loadItems();
|
||||
} else {
|
||||
showMsg("Löschen fehlgeschlagen.", true);
|
||||
}
|
||||
}
|
||||
|
||||
function showMsg(text, isError) {
|
||||
const el = document.getElementById("itemMsg");
|
||||
el.style.color = isError ? "#e06c6c" : "#6fbf73";
|
||||
el.textContent = text;
|
||||
setTimeout(() => { el.textContent = ""; }, 4000);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// INIT
|
||||
// -------------------------------------------------------------
|
||||
fillGridTransparent();
|
||||
redrawCanvas();
|
||||
loadItems();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,266 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Support-Tickets</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
background: #111;
|
||||
color: #eee;
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 20px;
|
||||
}
|
||||
h1 { margin-top: 0; }
|
||||
a.back { color: #6fbf73; text-decoration: none; font-size: 14px; }
|
||||
a.back:hover { text-decoration: underline; }
|
||||
|
||||
.panel {
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-top: 16px;
|
||||
max-width: 900px;
|
||||
}
|
||||
|
||||
.filter-row { display: flex; gap: 8px; margin-bottom: 14px; align-items: center; }
|
||||
select, button, input, textarea {
|
||||
background: #222;
|
||||
border: 1px solid #444;
|
||||
color: #eee;
|
||||
padding: 6px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
font-family: inherit;
|
||||
}
|
||||
button { cursor: pointer; background: #6a5acd; }
|
||||
button.secondary { background: #444; }
|
||||
button.danger { background: #a83232; }
|
||||
button:hover { opacity: 0.85; }
|
||||
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th, td {
|
||||
text-align: left;
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid #2a2a2a;
|
||||
font-size: 13px;
|
||||
vertical-align: top;
|
||||
}
|
||||
th { color: #888; font-weight: normal; }
|
||||
tr.ticket-list-row { cursor: pointer; }
|
||||
tr.ticket-list-row:hover { background: #202020; }
|
||||
|
||||
.ticket-status { padding: 2px 8px; border-radius: 10px; font-size: 11px; white-space: nowrap; }
|
||||
.ticket-status.open { background: #2c5a3d; color: #9be6b0; }
|
||||
.ticket-status.in_progress { background: #5a4a2c; color: #e6c89b; }
|
||||
.ticket-status.closed { background: #444; color: #aaa; }
|
||||
|
||||
.empty-hint { color: #666; padding: 10px 0; }
|
||||
.hidden { display: none !important; }
|
||||
|
||||
/* Detail-Overlay */
|
||||
#detailOverlay {
|
||||
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
||||
background: rgba(0,0,0,0.7); z-index: 1000;
|
||||
align-items: center; justify-content: center;
|
||||
}
|
||||
#detailBox {
|
||||
background: #1a1a1a; border: 2px solid #6a5acd; border-radius: 8px;
|
||||
padding: 20px; width: 500px; max-height: 80vh; display: flex; flex-direction: column;
|
||||
}
|
||||
#detailMessages { flex: 1; overflow-y: auto; margin: 12px 0; display: flex; flex-direction: column; gap: 8px; max-height: 400px; }
|
||||
.ticket-msg { padding: 8px 10px; border-radius: 8px; font-size: 13px; max-width: 85%; }
|
||||
.ticket-msg.staff { background: #3a2f6b; align-self: flex-end; }
|
||||
.ticket-msg.player { background: #262633; align-self: flex-start; }
|
||||
.ticket-msg .ticket-msg-meta { font-size: 10px; color: #999; margin-bottom: 3px; }
|
||||
#detailReply { width: 100%; box-sizing: border-box; min-height: 60px; margin-bottom: 8px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<a class="back" href="/uebersicht.html">← zurück zur Übersicht</a>
|
||||
<h1>🎫 Support-Tickets</h1>
|
||||
|
||||
<div class="panel">
|
||||
<div class="filter-row">
|
||||
<select id="statusFilter">
|
||||
<option value="all">Alle Status</option>
|
||||
<option value="open" selected>Offen</option>
|
||||
<option value="in_progress">In Bearbeitung</option>
|
||||
<option value="closed">Geschlossen</option>
|
||||
</select>
|
||||
<button onclick="loadTickets()">Aktualisieren</button>
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<thead><tr><th>#</th><th>Betreff</th><th>Von</th><th>Kategorie</th><th>Status</th><th>Aktualisiert</th></tr></thead>
|
||||
<tbody id="ticketsTableBody"></tbody>
|
||||
</table>
|
||||
<div class="empty-hint hidden" id="ticketsEmpty">Keine Tickets in dieser Ansicht.</div>
|
||||
</div>
|
||||
|
||||
<div id="detailOverlay" class="hidden">
|
||||
<div id="detailBox">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center;">
|
||||
<h3 id="detailTitle" style="margin:0;"></h3>
|
||||
<button class="secondary" onclick="closeDetail()">✕</button>
|
||||
</div>
|
||||
<div id="detailMeta" style="color:#888; font-size:12px; margin-top:4px;"></div>
|
||||
|
||||
<div id="detailMessages"></div>
|
||||
|
||||
<textarea id="detailReply" placeholder="Antworten..."></textarea>
|
||||
<div style="display:flex; gap:8px; flex-wrap:wrap;">
|
||||
<button onclick="sendDetailReply()">Antworten</button>
|
||||
<button class="secondary" onclick="setTicketStatus('in_progress')">In Bearbeitung</button>
|
||||
<button class="secondary" onclick="setTicketStatus('closed')">Schließen</button>
|
||||
<button class="secondary" onclick="setTicketStatus('open')">Wieder öffnen</button>
|
||||
</div>
|
||||
<div class="msg" id="detailMsg" style="font-size:12px; margin-top:6px; min-height:16px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const token = localStorage.getItem("token");
|
||||
if (!token) {
|
||||
alert("Bitte zuerst einloggen.");
|
||||
location.href = "/index.html";
|
||||
}
|
||||
|
||||
async function authFetch(url, options = {}) {
|
||||
options.headers = { ...(options.headers || {}), "Authorization": "Bearer " + token };
|
||||
const res = await fetch(url, options);
|
||||
if (res.status === 401 || res.status === 403) {
|
||||
alert("Sitzung abgelaufen oder keine Berechtigung. Bitte erneut einloggen.");
|
||||
location.href = "/index.html";
|
||||
throw new Error("Nicht autorisiert");
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement("div");
|
||||
div.textContent = text == null ? "" : String(text);
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
const statusLabels = { open: "Offen", in_progress: "In Bearbeitung", closed: "Geschlossen" };
|
||||
let allTickets = [];
|
||||
let currentDetailId = null;
|
||||
|
||||
async function loadTickets() {
|
||||
const status = document.getElementById("statusFilter").value;
|
||||
const res = await authFetch("/api/admin/tickets?status=" + encodeURIComponent(status));
|
||||
const data = await res.json();
|
||||
allTickets = data.tickets || [];
|
||||
renderTicketsTable();
|
||||
}
|
||||
|
||||
function renderTicketsTable() {
|
||||
const tbody = document.getElementById("ticketsTableBody");
|
||||
const empty = document.getElementById("ticketsEmpty");
|
||||
|
||||
if (allTickets.length === 0) {
|
||||
tbody.innerHTML = "";
|
||||
empty.classList.remove("hidden");
|
||||
return;
|
||||
}
|
||||
empty.classList.add("hidden");
|
||||
|
||||
tbody.innerHTML = allTickets.map(t => `
|
||||
<tr class="ticket-list-row" onclick="openDetail(${t.id})">
|
||||
<td>#${t.id}</td>
|
||||
<td>${escapeHtml(t.subject)} <span style="color:#666;">(${t.message_count} Nachrichten)</span></td>
|
||||
<td>${escapeHtml(t.username)}</td>
|
||||
<td>${escapeHtml(t.category)}</td>
|
||||
<td><span class="ticket-status ${t.status}">${statusLabels[t.status] || t.status}</span></td>
|
||||
<td>${new Date(t.updated_at).toLocaleString("de-DE")}</td>
|
||||
</tr>
|
||||
`).join("");
|
||||
}
|
||||
|
||||
async function openDetail(ticketId) {
|
||||
currentDetailId = ticketId;
|
||||
const res = await authFetch("/api/tickets/" + ticketId);
|
||||
const data = await res.json();
|
||||
if (!data.ok) {
|
||||
alert("Fehler: " + (data.error || "unbekannt"));
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById("detailTitle").textContent = `#${data.ticket.id} ${data.ticket.subject}`;
|
||||
document.getElementById("detailMeta").innerHTML =
|
||||
`Von ${escapeHtml(data.ticket.username || "")} · Kategorie: ${escapeHtml(data.ticket.category)} · Status: <span class="ticket-status ${data.ticket.status}">${statusLabels[data.ticket.status] || data.ticket.status}</span>`;
|
||||
|
||||
const container = document.getElementById("detailMessages");
|
||||
container.innerHTML = data.messages.map(m => `
|
||||
<div class="ticket-msg ${m.is_admin_reply ? "staff" : "player"}">
|
||||
<div class="ticket-msg-meta">${escapeHtml(m.username)}${m.is_admin_reply ? " (Team)" : ""} · ${new Date(m.created_at).toLocaleString("de-DE")}</div>
|
||||
${escapeHtml(m.message)}
|
||||
</div>
|
||||
`).join("");
|
||||
container.scrollTop = container.scrollHeight;
|
||||
|
||||
document.getElementById("detailReply").value = "";
|
||||
document.getElementById("detailOverlay").classList.remove("hidden");
|
||||
document.getElementById("detailOverlay").style.display = "flex";
|
||||
}
|
||||
|
||||
function closeDetail() {
|
||||
document.getElementById("detailOverlay").classList.add("hidden");
|
||||
document.getElementById("detailOverlay").style.display = "none";
|
||||
currentDetailId = null;
|
||||
loadTickets();
|
||||
}
|
||||
|
||||
async function sendDetailReply() {
|
||||
if (!currentDetailId) return;
|
||||
const message = document.getElementById("detailReply").value.trim();
|
||||
if (!message) return;
|
||||
|
||||
const res = await authFetch(`/api/tickets/${currentDetailId}/reply`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ message })
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
await openDetail(currentDetailId);
|
||||
} else {
|
||||
showDetailMsg("Fehler: " + (data.error || "unbekannt"), true);
|
||||
}
|
||||
}
|
||||
|
||||
async function setTicketStatus(status) {
|
||||
if (!currentDetailId) return;
|
||||
const res = await authFetch(`/api/admin/tickets/${currentDetailId}/status`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ status })
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
showDetailMsg("Status aktualisiert.");
|
||||
await openDetail(currentDetailId);
|
||||
} else {
|
||||
showDetailMsg("Fehler beim Ändern des Status.", true);
|
||||
}
|
||||
}
|
||||
|
||||
function showDetailMsg(text, isError) {
|
||||
const el = document.getElementById("detailMsg");
|
||||
el.style.color = isError ? "#e06c6c" : "#6fbf73";
|
||||
el.textContent = text;
|
||||
setTimeout(() => { el.textContent = ""; }, 4000);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// INIT
|
||||
// -------------------------------------------------------------
|
||||
loadTickets();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,902 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Tiles & Objekte</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
background: #111;
|
||||
color: #eee;
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 20px;
|
||||
}
|
||||
h1 { margin-top: 0; }
|
||||
a.back { color: #6fbf73; text-decoration: none; font-size: 14px; }
|
||||
a.back:hover { text-decoration: underline; }
|
||||
|
||||
.panel {
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 10px;
|
||||
}
|
||||
th, td {
|
||||
text-align: left;
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid #333;
|
||||
font-size: 14px;
|
||||
}
|
||||
th { color: #aaa; font-weight: normal; }
|
||||
|
||||
input, select {
|
||||
background: #222;
|
||||
border: 1px solid #444;
|
||||
color: #eee;
|
||||
padding: 6px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
input[type="checkbox"] { width: auto; }
|
||||
|
||||
button {
|
||||
background: #2c7a3d;
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
button.danger { background: #a83232; }
|
||||
button:hover { opacity: 0.85; }
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
|
||||
gap: 10px;
|
||||
margin-top: 12px;
|
||||
align-items: end;
|
||||
}
|
||||
.form-grid label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.form-grid input, .form-grid select { width: 100%; }
|
||||
.checkbox-row { display: flex; align-items: center; gap: 6px; }
|
||||
|
||||
.swatch {
|
||||
display: inline-block;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid #555;
|
||||
vertical-align: middle;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.msg { font-size: 13px; color: #6fbf73; min-height: 18px; margin-top: 8px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<a class="back" href="/index.html">← zurück zur Startseite</a>
|
||||
<h1>🧱 Tiles & Objekte</h1>
|
||||
|
||||
<!-- TILES -->
|
||||
<div class="panel">
|
||||
<h2 style="margin-top:0;">Tiles</h2>
|
||||
<div class="msg" id="tileMsg"></div>
|
||||
|
||||
<table>
|
||||
<thead><tr><th></th><th>ID</th><th>Name</th><th>Kollision</th><th>PvP-sicher</th><th>Straße</th><th></th></tr></thead>
|
||||
<tbody id="tilesTableBody"></tbody>
|
||||
</table>
|
||||
|
||||
<h3>Tile hinzufügen / bearbeiten</h3>
|
||||
<div class="form-grid">
|
||||
<div>
|
||||
<label>ID (Zahl)</label>
|
||||
<input id="tileId" type="number">
|
||||
</div>
|
||||
<div>
|
||||
<label>Name</label>
|
||||
<input id="tileName" placeholder="z.B. Gras">
|
||||
</div>
|
||||
<div>
|
||||
<label>Grundfarbe</label>
|
||||
<input id="tileColor" type="color" value="#3a9d3a">
|
||||
</div>
|
||||
<div class="checkbox-row">
|
||||
<input id="tileCollision" type="checkbox">
|
||||
<label for="tileCollision" style="margin:0;">Kollision</label>
|
||||
</div>
|
||||
<div class="checkbox-row">
|
||||
<input id="tilePvpSafe" type="checkbox">
|
||||
<label for="tilePvpSafe" style="margin:0;">🛡️ PvP-sichere Zone (kein Angreifen möglich)</label>
|
||||
</div>
|
||||
<div class="checkbox-row">
|
||||
<input id="tileIsRoad" type="checkbox">
|
||||
<label for="tileIsRoad" style="margin:0;">🛣️ Straße (NPC-Verkehr fährt nur hier)</label>
|
||||
</div>
|
||||
<div>
|
||||
<button onclick="saveTile()">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Textur malen (optional)</h3>
|
||||
<p style="color:#888; font-size:12px; margin-top:0;">
|
||||
Statt einer reinen Flächenfarbe kannst du hier ein kleines 16×16-Pixel-Muster malen
|
||||
(z.B. eine Mittellinie für eine Straße). Ohne gemalte Textur wird einfach die Grundfarbe genutzt.
|
||||
</p>
|
||||
<div style="display:flex; gap:20px; align-items:flex-start; flex-wrap:wrap;">
|
||||
<div>
|
||||
<canvas id="pixelCanvas" width="480" height="480" style="border:1px solid #444; cursor:crosshair; image-rendering:pixelated; background:#3a9d3a;"></canvas>
|
||||
</div>
|
||||
<div style="display:flex; flex-direction:column; gap:8px; min-width:180px;">
|
||||
<label style="font-size:12px; color:#999;">Malfarbe</label>
|
||||
<input id="pixelColor" type="color" value="#ffffff" style="width:60px; height:36px; padding:2px; cursor:pointer;">
|
||||
|
||||
<div style="display:flex; gap:4px; flex-wrap:wrap; margin-top:4px;">
|
||||
<button onclick="presetStripeMiddle()" style="font-size:11px;">Strich Mitte</button>
|
||||
<button onclick="presetBorder()" style="font-size:11px;">Rand</button>
|
||||
<button onclick="presetDot()" style="font-size:11px;">Punkt Mitte</button>
|
||||
</div>
|
||||
|
||||
<button onclick="clearPixelCanvas()" style="background:#a83232; margin-top:8px;">Löschen (Grundfarbe)</button>
|
||||
<button onclick="saveTileTexture()" style="background:#2c7a3d;">Textur speichern</button>
|
||||
<button onclick="removeTileTexture()" style="background:#555;">Textur entfernen</button>
|
||||
<div class="msg" id="textureMsg"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- OBJECTS -->
|
||||
<div class="panel">
|
||||
<h2 style="margin-top:0;">Objekte</h2>
|
||||
<div class="msg" id="objMsg"></div>
|
||||
|
||||
<table>
|
||||
<thead><tr><th></th><th>Typ</th><th>Name</th><th>Größe</th><th>Kollision</th><th>Interaktiv</th><th>Aktion</th><th>Nachts</th><th></th></tr></thead>
|
||||
<tbody id="objectsTableBody"></tbody>
|
||||
</table>
|
||||
|
||||
<h3>Objekt hinzufügen / bearbeiten</h3>
|
||||
<div class="form-grid">
|
||||
<div>
|
||||
<label>Typ (ID)</label>
|
||||
<input id="objType" placeholder="z.B. crate">
|
||||
</div>
|
||||
<div>
|
||||
<label>Name</label>
|
||||
<input id="objName" placeholder="z.B. Kiste">
|
||||
</div>
|
||||
<div>
|
||||
<label>Farbe</label>
|
||||
<input id="objColor" type="color" value="#8b5a2b">
|
||||
</div>
|
||||
<div>
|
||||
<label>Breite</label>
|
||||
<input id="objWidth" type="number" value="32">
|
||||
</div>
|
||||
<div>
|
||||
<label>Höhe</label>
|
||||
<input id="objHeight" type="number" value="32">
|
||||
</div>
|
||||
<div class="checkbox-row">
|
||||
<input id="objCollision" type="checkbox">
|
||||
<label for="objCollision" style="margin:0;">Kollision</label>
|
||||
</div>
|
||||
<div class="checkbox-row">
|
||||
<input id="objInteractive" type="checkbox">
|
||||
<label for="objInteractive" style="margin:0;">Interaktiv</label>
|
||||
</div>
|
||||
<div class="checkbox-row">
|
||||
<input id="objGlowsAtNight" type="checkbox">
|
||||
<label for="objGlowsAtNight" style="margin:0;">Leuchtet nachts</label>
|
||||
</div>
|
||||
<div>
|
||||
<label>Aktion (bei interaktiv)</label>
|
||||
<input id="objAction" placeholder="z.B. house_storage">
|
||||
</div>
|
||||
<div>
|
||||
<button onclick="saveObject()">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="color:#888; font-size:12px; margin-top:8px;">
|
||||
Bekannte Aktionen: <code>loot</code> (einfache Meldung), <code>house_storage</code> (Kiste im Haus),
|
||||
<code>house_exit</code> (Ausgang aus dem Haus).
|
||||
</div>
|
||||
|
||||
<h3>Objekt bemalen (optional)</h3>
|
||||
<p style="color:#888; font-size:12px; margin-top:0;">
|
||||
Statt der reinen Flächenfarbe kannst du hier ein Pixel-Muster malen. Das Raster passt sich
|
||||
automatisch an die oben eingestellte Breite/Höhe an. Ohne gemalte Textur wird einfach die
|
||||
Flächenfarbe genutzt.
|
||||
</p>
|
||||
<div style="display:flex; gap:20px; align-items:flex-start; flex-wrap:wrap;">
|
||||
<div>
|
||||
<canvas id="objPixelCanvas" width="320" height="320" style="border:1px solid #444; cursor:crosshair; image-rendering:pixelated; background:#8b5a2b;"></canvas>
|
||||
</div>
|
||||
<div style="display:flex; flex-direction:column; gap:8px; min-width:180px;">
|
||||
<label style="font-size:12px; color:#999;">Malfarbe</label>
|
||||
<input id="objPixelColor" type="color" value="#ffffff" style="width:60px; height:36px; padding:2px; cursor:pointer;">
|
||||
|
||||
<div style="display:flex; gap:4px; flex-wrap:wrap; margin-top:4px;">
|
||||
<button onclick="objPresetBorder()" style="font-size:11px;">Rand</button>
|
||||
<button onclick="objPresetCross()" style="font-size:11px;">Kreuz</button>
|
||||
</div>
|
||||
|
||||
<button onclick="rebuildObjPixelGrid()" style="background:#555; margin-top:8px;">Raster an Breite/Höhe anpassen</button>
|
||||
<button onclick="clearObjPixelCanvas()" style="background:#a83232;">Löschen (Grundfarbe)</button>
|
||||
<button onclick="saveObjTexture()" style="background:#2c7a3d;">Textur speichern</button>
|
||||
<button onclick="removeObjTexture()" style="background:#555;">Textur entfernen</button>
|
||||
<div class="msg" id="objTextureMsg"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const token = localStorage.getItem("token");
|
||||
// Voll-Admin ODER passende Gruppen-Berechtigung reicht jetzt aus - der
|
||||
// Server prüft das bei jedem Aufruf ohnehin final ab (siehe requireAdmin);
|
||||
// hier reicht ein simpler Login-Check, authFetch() fängt fehlende Rechte
|
||||
// beim ersten echten API-Aufruf sauber ab (Meldung + Weiterleitung)
|
||||
let isAdmin = localStorage.getItem("isAdmin") === "true";
|
||||
|
||||
if (!token) {
|
||||
alert("Bitte zuerst einloggen.");
|
||||
location.href = "/index.html";
|
||||
}
|
||||
|
||||
async function authFetch(url, options = {}) {
|
||||
options.headers = { ...(options.headers || {}), "Authorization": "Bearer " + token };
|
||||
const res = await fetch(url, options);
|
||||
if (res.status === 401 || res.status === 403) {
|
||||
alert("Sitzung abgelaufen oder kein Admin-Zugriff. Bitte erneut einloggen.");
|
||||
location.href = "/index.html";
|
||||
throw new Error("Nicht autorisiert");
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// TILES
|
||||
// -------------------------------------------------------------
|
||||
let allTiles = [];
|
||||
|
||||
async function loadTiles() {
|
||||
const res = await authFetch("/api/admin/tile_config");
|
||||
const data = await res.json();
|
||||
allTiles = data.tiles || [];
|
||||
renderTilesTable();
|
||||
}
|
||||
|
||||
function renderTilesTable() {
|
||||
const body = document.getElementById("tilesTableBody");
|
||||
body.innerHTML = "";
|
||||
|
||||
allTiles.forEach(t => {
|
||||
const tr = document.createElement("tr");
|
||||
tr.innerHTML = `
|
||||
<td><span class="swatch" style="background:${t.color};"></span>${t.image_data ? " 🎨" : ""}</td>
|
||||
<td>${t.id}</td>
|
||||
<td>${t.name}</td>
|
||||
<td>${t.collision ? "Ja" : "Nein"}</td>
|
||||
<td>${t.pvp_safe ? "🛡️ Ja" : "Nein"}</td>
|
||||
<td>${t.is_road ? "🛣️ Ja" : "Nein"}</td>
|
||||
<td>
|
||||
<button onclick="editTile(${t.id})">Bearbeiten</button>
|
||||
<button class="danger" onclick="deleteTile(${t.id})">Löschen</button>
|
||||
</td>
|
||||
`;
|
||||
body.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
function editTile(id) {
|
||||
const t = allTiles.find(t => t.id === id);
|
||||
if (!t) return;
|
||||
document.getElementById("tileId").value = t.id;
|
||||
document.getElementById("tileName").value = t.name;
|
||||
document.getElementById("tileColor").value = t.color;
|
||||
document.getElementById("tileCollision").checked = !!t.collision;
|
||||
document.getElementById("tilePvpSafe").checked = !!t.pvp_safe;
|
||||
document.getElementById("tileIsRoad").checked = !!t.is_road;
|
||||
|
||||
if (t.image_data) {
|
||||
loadTextureIntoGrid(t.image_data);
|
||||
} else {
|
||||
fillGridWithColor(t.color);
|
||||
}
|
||||
redrawPixelCanvas();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// PIXEL-EDITOR FÜR TILE-TEXTUREN
|
||||
// -------------------------------------------------------------
|
||||
const GRID_SIZE = 32;
|
||||
const CELL_PX = 480 / GRID_SIZE; // 15px pro Zelle in der Editor-Anzeige
|
||||
let pixelGrid = [];
|
||||
const pixelCanvas = document.getElementById("pixelCanvas");
|
||||
const pixelCtx = pixelCanvas.getContext("2d");
|
||||
let isPainting = false;
|
||||
|
||||
function fillGridWithColor(color) {
|
||||
pixelGrid = [];
|
||||
for (let y = 0; y < GRID_SIZE; y++) {
|
||||
const row = [];
|
||||
for (let x = 0; x < GRID_SIZE; x++) row.push(color);
|
||||
pixelGrid.push(row);
|
||||
}
|
||||
}
|
||||
fillGridWithColor(document.getElementById("tileColor").value);
|
||||
|
||||
function redrawPixelCanvas() {
|
||||
for (let y = 0; y < GRID_SIZE; y++) {
|
||||
for (let x = 0; x < GRID_SIZE; x++) {
|
||||
pixelCtx.fillStyle = pixelGrid[y][x];
|
||||
pixelCtx.fillRect(x * CELL_PX, y * CELL_PX, CELL_PX, CELL_PX);
|
||||
}
|
||||
}
|
||||
}
|
||||
redrawPixelCanvas();
|
||||
|
||||
function loadTextureIntoGrid(dataUrl) {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
const off = document.createElement("canvas");
|
||||
off.width = GRID_SIZE;
|
||||
off.height = GRID_SIZE;
|
||||
const offCtx = off.getContext("2d");
|
||||
offCtx.drawImage(img, 0, 0, GRID_SIZE, GRID_SIZE);
|
||||
const data = offCtx.getImageData(0, 0, GRID_SIZE, GRID_SIZE).data;
|
||||
|
||||
for (let y = 0; y < GRID_SIZE; y++) {
|
||||
for (let x = 0; x < GRID_SIZE; x++) {
|
||||
const i = (y * GRID_SIZE + x) * 4;
|
||||
const r = data[i], g = data[i + 1], b = data[i + 2];
|
||||
pixelGrid[y][x] = `rgb(${r},${g},${b})`;
|
||||
}
|
||||
}
|
||||
redrawPixelCanvas();
|
||||
};
|
||||
img.src = dataUrl;
|
||||
}
|
||||
|
||||
function paintAt(clientX, clientY) {
|
||||
const rect = pixelCanvas.getBoundingClientRect();
|
||||
const x = Math.floor((clientX - rect.left) / (rect.width / GRID_SIZE));
|
||||
const y = Math.floor((clientY - rect.top) / (rect.height / GRID_SIZE));
|
||||
if (x < 0 || y < 0 || x >= GRID_SIZE || y >= GRID_SIZE) return;
|
||||
|
||||
pixelGrid[y][x] = document.getElementById("pixelColor").value;
|
||||
redrawPixelCanvas();
|
||||
}
|
||||
|
||||
pixelCanvas.addEventListener("mousedown", e => { isPainting = true; paintAt(e.clientX, e.clientY); });
|
||||
pixelCanvas.addEventListener("mousemove", e => { if (isPainting) paintAt(e.clientX, e.clientY); });
|
||||
window.addEventListener("mouseup", () => { isPainting = false; });
|
||||
|
||||
function clearPixelCanvas() {
|
||||
fillGridWithColor(document.getElementById("tileColor").value);
|
||||
redrawPixelCanvas();
|
||||
}
|
||||
|
||||
document.getElementById("tileColor").addEventListener("input", e => {
|
||||
pixelCanvas.style.background = e.target.value;
|
||||
});
|
||||
|
||||
function presetStripeMiddle() {
|
||||
const color = document.getElementById("pixelColor").value;
|
||||
const mid = Math.floor(GRID_SIZE / 2);
|
||||
for (let y = 0; y < GRID_SIZE; y++) {
|
||||
pixelGrid[y][mid - 1] = color;
|
||||
pixelGrid[y][mid] = color;
|
||||
}
|
||||
redrawPixelCanvas();
|
||||
}
|
||||
|
||||
function presetBorder() {
|
||||
const color = document.getElementById("pixelColor").value;
|
||||
for (let i = 0; i < GRID_SIZE; i++) {
|
||||
pixelGrid[0][i] = color;
|
||||
pixelGrid[GRID_SIZE - 1][i] = color;
|
||||
pixelGrid[i][0] = color;
|
||||
pixelGrid[i][GRID_SIZE - 1] = color;
|
||||
}
|
||||
redrawPixelCanvas();
|
||||
}
|
||||
|
||||
function presetDot() {
|
||||
const color = document.getElementById("pixelColor").value;
|
||||
const mid = Math.floor(GRID_SIZE / 2);
|
||||
for (let y = mid - 1; y <= mid; y++) {
|
||||
for (let x = mid - 1; x <= mid; x++) {
|
||||
pixelGrid[y][x] = color;
|
||||
}
|
||||
}
|
||||
redrawPixelCanvas();
|
||||
}
|
||||
|
||||
function exportGridAsDataUrl() {
|
||||
const off = document.createElement("canvas");
|
||||
off.width = GRID_SIZE;
|
||||
off.height = GRID_SIZE;
|
||||
const offCtx = off.getContext("2d");
|
||||
for (let y = 0; y < GRID_SIZE; y++) {
|
||||
for (let x = 0; x < GRID_SIZE; x++) {
|
||||
offCtx.fillStyle = pixelGrid[y][x];
|
||||
offCtx.fillRect(x, y, 1, 1);
|
||||
}
|
||||
}
|
||||
return off.toDataURL("image/png");
|
||||
}
|
||||
|
||||
async function saveTileTexture() {
|
||||
const id = Number(document.getElementById("tileId").value);
|
||||
const name = document.getElementById("tileName").value.trim();
|
||||
const color = document.getElementById("tileColor").value;
|
||||
const collision = document.getElementById("tileCollision").checked;
|
||||
const pvpSafe = document.getElementById("tilePvpSafe").checked;
|
||||
const isRoad = document.getElementById("tileIsRoad").checked;
|
||||
|
||||
if (isNaN(id) || !name) {
|
||||
showTextureMsg("Erst oben ID und Name ausfüllen.", true);
|
||||
return;
|
||||
}
|
||||
|
||||
const imageData = exportGridAsDataUrl();
|
||||
|
||||
const res = await authFetch("/api/admin/tile_config", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ id, name, color, collision, pvpSafe, isRoad, imageData })
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
showTextureMsg("Textur gespeichert.");
|
||||
await loadTiles();
|
||||
} else {
|
||||
showTextureMsg("Fehler: " + (data.error || "unbekannt"), true);
|
||||
}
|
||||
}
|
||||
|
||||
async function removeTileTexture() {
|
||||
const id = Number(document.getElementById("tileId").value);
|
||||
const name = document.getElementById("tileName").value.trim();
|
||||
const color = document.getElementById("tileColor").value;
|
||||
const collision = document.getElementById("tileCollision").checked;
|
||||
const pvpSafe = document.getElementById("tilePvpSafe").checked;
|
||||
const isRoad = document.getElementById("tileIsRoad").checked;
|
||||
|
||||
if (isNaN(id) || !name) {
|
||||
showTextureMsg("Erst oben ID und Name ausfüllen.", true);
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await authFetch("/api/admin/tile_config", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ id, name, color, collision, pvpSafe, isRoad, imageData: null })
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
showTextureMsg("Textur entfernt - nutzt jetzt wieder die Grundfarbe.");
|
||||
clearPixelCanvas();
|
||||
await loadTiles();
|
||||
} else {
|
||||
showTextureMsg("Fehler: " + (data.error || "unbekannt"), true);
|
||||
}
|
||||
}
|
||||
|
||||
function showTextureMsg(text, isError) {
|
||||
const el = document.getElementById("textureMsg");
|
||||
el.style.color = isError ? "#e06c6c" : "#6fbf73";
|
||||
el.textContent = text;
|
||||
setTimeout(() => { el.textContent = ""; }, 4000);
|
||||
}
|
||||
|
||||
async function saveTile() {
|
||||
const id = Number(document.getElementById("tileId").value);
|
||||
const name = document.getElementById("tileName").value.trim();
|
||||
const color = document.getElementById("tileColor").value;
|
||||
const collision = document.getElementById("tileCollision").checked;
|
||||
const pvpSafe = document.getElementById("tilePvpSafe").checked;
|
||||
const isRoad = document.getElementById("tileIsRoad").checked;
|
||||
|
||||
if (isNaN(id) || !name) {
|
||||
showTileMsg("ID und Name sind Pflicht.", true);
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await authFetch("/api/admin/tile_config", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ id, name, color, collision, pvpSafe, isRoad })
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
showTileMsg("Tile gespeichert.");
|
||||
document.getElementById("tileId").value = "";
|
||||
document.getElementById("tileName").value = "";
|
||||
document.getElementById("tileCollision").checked = false;
|
||||
document.getElementById("tilePvpSafe").checked = false;
|
||||
document.getElementById("tileIsRoad").checked = false;
|
||||
await loadTiles();
|
||||
} else {
|
||||
showTileMsg("Fehler: " + (data.error || "unbekannt"), true);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteTile(id) {
|
||||
if (!confirm(`Tile #${id} wirklich löschen?`)) return;
|
||||
|
||||
const res = await authFetch("/api/admin/tile_config/" + id, { method: "DELETE" });
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
showTileMsg("Tile gelöscht.");
|
||||
await loadTiles();
|
||||
} else {
|
||||
showTileMsg("Löschen fehlgeschlagen.", true);
|
||||
}
|
||||
}
|
||||
|
||||
function showTileMsg(text, isError) {
|
||||
const el = document.getElementById("tileMsg");
|
||||
el.style.color = isError ? "#e06c6c" : "#6fbf73";
|
||||
el.textContent = text;
|
||||
setTimeout(() => { el.textContent = ""; }, 4000);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// OBJECTS
|
||||
// -------------------------------------------------------------
|
||||
let allObjects = [];
|
||||
|
||||
async function loadObjects() {
|
||||
const res = await authFetch("/api/admin/object_config");
|
||||
const data = await res.json();
|
||||
allObjects = data.objects || [];
|
||||
renderObjectsTable();
|
||||
}
|
||||
|
||||
function renderObjectsTable() {
|
||||
const body = document.getElementById("objectsTableBody");
|
||||
body.innerHTML = "";
|
||||
|
||||
allObjects.forEach(o => {
|
||||
const tr = document.createElement("tr");
|
||||
tr.innerHTML = `
|
||||
<td><span class="swatch" style="background:${o.color};"></span>${o.image_data ? " 🎨" : ""}</td>
|
||||
<td>${o.type}</td>
|
||||
<td>${o.name}</td>
|
||||
<td>${o.width} x ${o.height}</td>
|
||||
<td>${o.collision ? "Ja" : "Nein"}</td>
|
||||
<td>${o.interactive ? "Ja" : "Nein"}</td>
|
||||
<td>${o.action || "-"}</td>
|
||||
<td>${o.glows_at_night ? "🔆 Ja" : "Nein"}</td>
|
||||
<td>
|
||||
<button onclick="editObject('${o.type}')">Bearbeiten</button>
|
||||
<button class="danger" onclick="deleteObject('${o.type}')">Löschen</button>
|
||||
</td>
|
||||
`;
|
||||
body.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
function editObject(type) {
|
||||
const o = allObjects.find(o => o.type === type);
|
||||
if (!o) return;
|
||||
document.getElementById("objType").value = o.type;
|
||||
document.getElementById("objName").value = o.name;
|
||||
document.getElementById("objColor").value = o.color;
|
||||
document.getElementById("objWidth").value = o.width;
|
||||
document.getElementById("objHeight").value = o.height;
|
||||
document.getElementById("objCollision").checked = !!o.collision;
|
||||
document.getElementById("objInteractive").checked = !!o.interactive;
|
||||
document.getElementById("objAction").value = o.action || "";
|
||||
document.getElementById("objGlowsAtNight").checked = !!o.glows_at_night;
|
||||
|
||||
rebuildObjPixelGrid();
|
||||
if (o.image_data) {
|
||||
loadObjTextureIntoGrid(o.image_data);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveObject() {
|
||||
const type = document.getElementById("objType").value.trim();
|
||||
const name = document.getElementById("objName").value.trim();
|
||||
const color = document.getElementById("objColor").value;
|
||||
const width = Number(document.getElementById("objWidth").value) || 32;
|
||||
const height = Number(document.getElementById("objHeight").value) || 32;
|
||||
const collision = document.getElementById("objCollision").checked;
|
||||
const interactive = document.getElementById("objInteractive").checked;
|
||||
const action = document.getElementById("objAction").value.trim();
|
||||
const glowsAtNight = document.getElementById("objGlowsAtNight").checked;
|
||||
|
||||
if (!type || !name) {
|
||||
showObjMsg("Typ und Name sind Pflicht.", true);
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await authFetch("/api/admin/object_config", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ type, name, color, width, height, collision, interactive, action, glowsAtNight, imageData: exportObjGridAsDataUrl() })
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
showObjMsg("Objekt gespeichert.");
|
||||
document.getElementById("objType").value = "";
|
||||
document.getElementById("objName").value = "";
|
||||
document.getElementById("objAction").value = "";
|
||||
document.getElementById("objCollision").checked = false;
|
||||
document.getElementById("objInteractive").checked = false;
|
||||
document.getElementById("objGlowsAtNight").checked = false;
|
||||
clearObjPixelCanvas();
|
||||
await loadObjects();
|
||||
} else {
|
||||
showObjMsg("Fehler: " + (data.error || "unbekannt"), true);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteObject(type) {
|
||||
if (!confirm(`Objekt-Typ "${type}" wirklich löschen?`)) return;
|
||||
|
||||
const res = await authFetch("/api/admin/object_config/" + encodeURIComponent(type), { method: "DELETE" });
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
showObjMsg("Objekt gelöscht.");
|
||||
await loadObjects();
|
||||
} else {
|
||||
showObjMsg("Löschen fehlgeschlagen.", true);
|
||||
}
|
||||
}
|
||||
|
||||
function showObjMsg(text, isError) {
|
||||
const el = document.getElementById("objMsg");
|
||||
el.style.color = isError ? "#e06c6c" : "#6fbf73";
|
||||
el.textContent = text;
|
||||
setTimeout(() => { el.textContent = ""; }, 4000);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// PIXEL-EDITOR FÜR OBJEKT-TEXTUREN
|
||||
// -------------------------------------------------------------
|
||||
const objCanvas = document.getElementById("objPixelCanvas");
|
||||
const objCtx = objCanvas.getContext("2d");
|
||||
const OBJ_DISPLAY_MAX = 300; // maximale Anzeigegröße im Editor
|
||||
|
||||
let objGridW = 32;
|
||||
let objGridH = 32;
|
||||
let objPixelGrid = [];
|
||||
let objCellPxX = 8;
|
||||
let objCellPxY = 8;
|
||||
let objIsPainting = false;
|
||||
|
||||
function fillObjGridWithColor(color) {
|
||||
objPixelGrid = [];
|
||||
for (let y = 0; y < objGridH; y++) {
|
||||
const row = [];
|
||||
for (let x = 0; x < objGridW; x++) row.push(color);
|
||||
objPixelGrid.push(row);
|
||||
}
|
||||
}
|
||||
|
||||
function rebuildObjPixelGrid() {
|
||||
objGridW = Math.max(4, Math.min(200, parseInt(document.getElementById("objWidth").value) || 32));
|
||||
objGridH = Math.max(4, Math.min(200, parseInt(document.getElementById("objHeight").value) || 32));
|
||||
|
||||
const scale = Math.min(OBJ_DISPLAY_MAX / objGridW, OBJ_DISPLAY_MAX / objGridH, 16);
|
||||
objCellPxX = scale;
|
||||
objCellPxY = scale;
|
||||
|
||||
objCanvas.width = objGridW * objCellPxX;
|
||||
objCanvas.height = objGridH * objCellPxY;
|
||||
|
||||
fillObjGridWithColor(document.getElementById("objColor").value);
|
||||
redrawObjPixelCanvas();
|
||||
}
|
||||
rebuildObjPixelGrid();
|
||||
|
||||
function redrawObjPixelCanvas() {
|
||||
for (let y = 0; y < objGridH; y++) {
|
||||
for (let x = 0; x < objGridW; x++) {
|
||||
objCtx.fillStyle = objPixelGrid[y][x];
|
||||
objCtx.fillRect(x * objCellPxX, y * objCellPxY, objCellPxX, objCellPxY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function loadObjTextureIntoGrid(dataUrl) {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
const off = document.createElement("canvas");
|
||||
off.width = objGridW;
|
||||
off.height = objGridH;
|
||||
const offCtx = off.getContext("2d");
|
||||
offCtx.drawImage(img, 0, 0, objGridW, objGridH);
|
||||
const data = offCtx.getImageData(0, 0, objGridW, objGridH).data;
|
||||
|
||||
for (let y = 0; y < objGridH; y++) {
|
||||
for (let x = 0; x < objGridW; x++) {
|
||||
const i = (y * objGridW + x) * 4;
|
||||
objPixelGrid[y][x] = `rgb(${data[i]},${data[i + 1]},${data[i + 2]})`;
|
||||
}
|
||||
}
|
||||
redrawObjPixelCanvas();
|
||||
};
|
||||
img.src = dataUrl;
|
||||
}
|
||||
|
||||
function objPaintAt(clientX, clientY) {
|
||||
const rect = objCanvas.getBoundingClientRect();
|
||||
const x = Math.floor((clientX - rect.left) / (rect.width / objGridW));
|
||||
const y = Math.floor((clientY - rect.top) / (rect.height / objGridH));
|
||||
if (x < 0 || y < 0 || x >= objGridW || y >= objGridH) return;
|
||||
|
||||
objPixelGrid[y][x] = document.getElementById("objPixelColor").value;
|
||||
redrawObjPixelCanvas();
|
||||
}
|
||||
|
||||
objCanvas.addEventListener("mousedown", e => { objIsPainting = true; objPaintAt(e.clientX, e.clientY); });
|
||||
objCanvas.addEventListener("mousemove", e => { if (objIsPainting) objPaintAt(e.clientX, e.clientY); });
|
||||
window.addEventListener("mouseup", () => { objIsPainting = false; });
|
||||
|
||||
function clearObjPixelCanvas() {
|
||||
fillObjGridWithColor(document.getElementById("objColor").value);
|
||||
redrawObjPixelCanvas();
|
||||
}
|
||||
|
||||
document.getElementById("objColor").addEventListener("input", e => {
|
||||
objCanvas.style.background = e.target.value;
|
||||
});
|
||||
|
||||
document.getElementById("objWidth").addEventListener("change", rebuildObjPixelGrid);
|
||||
document.getElementById("objHeight").addEventListener("change", rebuildObjPixelGrid);
|
||||
|
||||
function objPresetBorder() {
|
||||
const color = document.getElementById("objPixelColor").value;
|
||||
for (let x = 0; x < objGridW; x++) {
|
||||
objPixelGrid[0][x] = color;
|
||||
objPixelGrid[objGridH - 1][x] = color;
|
||||
}
|
||||
for (let y = 0; y < objGridH; y++) {
|
||||
objPixelGrid[y][0] = color;
|
||||
objPixelGrid[y][objGridW - 1] = color;
|
||||
}
|
||||
redrawObjPixelCanvas();
|
||||
}
|
||||
|
||||
function objPresetCross() {
|
||||
const color = document.getElementById("objPixelColor").value;
|
||||
const midX = Math.floor(objGridW / 2);
|
||||
const midY = Math.floor(objGridH / 2);
|
||||
for (let x = 0; x < objGridW; x++) objPixelGrid[midY][x] = color;
|
||||
for (let y = 0; y < objGridH; y++) objPixelGrid[y][midX] = color;
|
||||
redrawObjPixelCanvas();
|
||||
}
|
||||
|
||||
function exportObjGridAsDataUrl() {
|
||||
const off = document.createElement("canvas");
|
||||
off.width = objGridW;
|
||||
off.height = objGridH;
|
||||
const offCtx = off.getContext("2d");
|
||||
for (let y = 0; y < objGridH; y++) {
|
||||
for (let x = 0; x < objGridW; x++) {
|
||||
offCtx.fillStyle = objPixelGrid[y][x];
|
||||
offCtx.fillRect(x, y, 1, 1);
|
||||
}
|
||||
}
|
||||
return off.toDataURL("image/png");
|
||||
}
|
||||
|
||||
async function saveObjTexture() {
|
||||
const type = document.getElementById("objType").value.trim();
|
||||
if (!type) {
|
||||
showObjTextureMsg("Erst oben einen Typ (ID) eintragen.", true);
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
type,
|
||||
name: document.getElementById("objName").value.trim(),
|
||||
color: document.getElementById("objColor").value,
|
||||
width: Number(document.getElementById("objWidth").value) || 32,
|
||||
height: Number(document.getElementById("objHeight").value) || 32,
|
||||
collision: document.getElementById("objCollision").checked,
|
||||
interactive: document.getElementById("objInteractive").checked,
|
||||
action: document.getElementById("objAction").value.trim(),
|
||||
glowsAtNight: document.getElementById("objGlowsAtNight").checked,
|
||||
imageData: exportObjGridAsDataUrl()
|
||||
};
|
||||
|
||||
if (!payload.name) {
|
||||
showObjTextureMsg("Erst oben einen Namen eintragen.", true);
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await authFetch("/api/admin/object_config", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
showObjTextureMsg("Textur gespeichert.");
|
||||
await loadObjects();
|
||||
} else {
|
||||
showObjTextureMsg("Fehler: " + (data.error || "unbekannt"), true);
|
||||
}
|
||||
}
|
||||
|
||||
async function removeObjTexture() {
|
||||
const type = document.getElementById("objType").value.trim();
|
||||
if (!type) {
|
||||
showObjTextureMsg("Erst oben einen Typ (ID) eintragen.", true);
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
type,
|
||||
name: document.getElementById("objName").value.trim(),
|
||||
color: document.getElementById("objColor").value,
|
||||
width: Number(document.getElementById("objWidth").value) || 32,
|
||||
height: Number(document.getElementById("objHeight").value) || 32,
|
||||
collision: document.getElementById("objCollision").checked,
|
||||
interactive: document.getElementById("objInteractive").checked,
|
||||
action: document.getElementById("objAction").value.trim(),
|
||||
glowsAtNight: document.getElementById("objGlowsAtNight").checked,
|
||||
imageData: null
|
||||
};
|
||||
|
||||
const res = await authFetch("/api/admin/object_config", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
showObjTextureMsg("Textur entfernt - nutzt jetzt wieder die Flächenfarbe.");
|
||||
clearObjPixelCanvas();
|
||||
await loadObjects();
|
||||
} else {
|
||||
showObjTextureMsg("Fehler: " + (data.error || "unbekannt"), true);
|
||||
}
|
||||
}
|
||||
|
||||
function showObjTextureMsg(text, isError) {
|
||||
const el = document.getElementById("objTextureMsg");
|
||||
el.style.color = isError ? "#e06c6c" : "#6fbf73";
|
||||
el.textContent = text;
|
||||
setTimeout(() => { el.textContent = ""; }, 4000);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// INIT
|
||||
// -------------------------------------------------------------
|
||||
loadTiles();
|
||||
loadObjects();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,279 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Benutzerverwaltung</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
background: #111;
|
||||
color: #eee;
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 20px;
|
||||
}
|
||||
h1 { margin-top: 0; }
|
||||
a.back { color: #6fbf73; text-decoration: none; font-size: 14px; }
|
||||
a.back:hover { text-decoration: underline; }
|
||||
|
||||
.panel {
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 10px;
|
||||
}
|
||||
th, td {
|
||||
text-align: left;
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid #333;
|
||||
font-size: 14px;
|
||||
}
|
||||
th { color: #aaa; font-weight: normal; }
|
||||
|
||||
button {
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 5px 10px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
button.approve { background: #2c7a3d; }
|
||||
button.revoke { background: #a83232; }
|
||||
button.admin-on { background: #4942ae; }
|
||||
button.admin-off { background: #333; }
|
||||
button:hover { opacity: 0.85; }
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
border-radius: 4px;
|
||||
padding: 2px 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.badge.pending { background: #6b5b1a; color: #ffd76b; }
|
||||
.badge.approved { background: #1a4d2a; color: #6fbf73; }
|
||||
.badge.admin { background: #2f2a5e; color: #b3a6ff; margin-left: 4px; }
|
||||
|
||||
.msg { font-size: 13px; color: #6fbf73; min-height: 18px; margin-top: 8px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<a class="back" href="/index.html">← zurück zur Startseite</a>
|
||||
<h1>👥 Benutzerverwaltung</h1>
|
||||
|
||||
<div class="panel">
|
||||
<div class="msg" id="userMsg"></div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>Status</th>
|
||||
<th>Bargeld</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="usersTableBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const token = localStorage.getItem("token");
|
||||
// Voll-Admin ODER passende Gruppen-Berechtigung reicht jetzt aus - der
|
||||
// Server prüft das bei jedem Aufruf ohnehin final ab (siehe requireAdmin);
|
||||
// hier reicht ein simpler Login-Check, authFetch() fängt fehlende Rechte
|
||||
// beim ersten echten API-Aufruf sauber ab (Meldung + Weiterleitung)
|
||||
let isAdmin = localStorage.getItem("isAdmin") === "true";
|
||||
|
||||
if (!token) {
|
||||
alert("Bitte zuerst einloggen.");
|
||||
location.href = "/index.html";
|
||||
}
|
||||
|
||||
async function authFetch(url, options = {}) {
|
||||
options.headers = { ...(options.headers || {}), "Authorization": "Bearer " + token };
|
||||
const res = await fetch(url, options);
|
||||
if (res.status === 401 || res.status === 403) {
|
||||
alert("Sitzung abgelaufen oder kein Admin-Zugriff. Bitte erneut einloggen.");
|
||||
location.href = "/index.html";
|
||||
throw new Error("Nicht autorisiert");
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
let allUsers = [];
|
||||
|
||||
async function loadUsers() {
|
||||
const res = await authFetch("/api/admin/players");
|
||||
const data = await res.json();
|
||||
allUsers = data.players || [];
|
||||
renderUsers();
|
||||
}
|
||||
|
||||
function renderUsers() {
|
||||
const body = document.getElementById("usersTableBody");
|
||||
body.innerHTML = "";
|
||||
|
||||
allUsers.forEach(u => {
|
||||
const tr = document.createElement("tr");
|
||||
|
||||
const statusBadge = u.approved
|
||||
? `<span class="badge approved">Freigeschaltet</span>`
|
||||
: `<span class="badge pending">Wartet auf Freischaltung</span>`;
|
||||
const adminBadge = u.is_admin ? `<span class="badge admin">Admin</span>` : "";
|
||||
const betaBadge = u.is_beta_tester ? `<span class="badge" style="background:#3a1a5a; color:#c9a8ff;">Beta-Tester</span>` : "";
|
||||
const bannedBadge = u.banned ? `<span class="badge" style="background:#5a1a1a; color:#ff8080;">Gesperrt</span>` : "";
|
||||
const isMuted = u.muted_until && new Date(u.muted_until).getTime() > Date.now();
|
||||
const mutedBadge = isMuted ? `<span class="badge" style="background:#5a4a1a; color:#ffd580;">Stumm</span>` : "";
|
||||
|
||||
tr.innerHTML = `
|
||||
<td>${u.username}</td>
|
||||
<td>
|
||||
${statusBadge}${adminBadge}${betaBadge}${bannedBadge}${mutedBadge}
|
||||
${!u.approved && u.application_text ? `<div style="color:#999; font-size:12px; margin-top:6px; max-width:320px; white-space:pre-wrap;">"${escapeHtml(u.application_text)}"</div>` : ""}
|
||||
</td>
|
||||
<td>${u.money}$</td>
|
||||
<td>
|
||||
${u.approved
|
||||
? `<button class="revoke" onclick="revokeUser(${u.id})">Sperren</button>`
|
||||
: `<button class="approve" onclick="approveUser(${u.id})">Freischalten</button>
|
||||
<button class="admin-off" onclick="rejectUser(${u.id})">Ablehnen</button>`}
|
||||
<button class="${u.is_admin ? 'admin-off' : 'admin-on'}" onclick="toggleAdmin(${u.id}, ${!u.is_admin})">
|
||||
${u.is_admin ? "Admin entziehen" : "Zum Admin machen"}
|
||||
</button>
|
||||
<button class="${u.is_beta_tester ? 'admin-off' : 'admin-on'}" onclick="toggleBetaTester(${u.id}, ${!u.is_beta_tester})">
|
||||
${u.is_beta_tester ? "Beta-Zugang entziehen" : "Beta-Zugang geben"}
|
||||
</button>
|
||||
<button class="${u.banned ? 'admin-on' : 'admin-off'}" onclick="toggleBan(${u.id}, ${!u.banned})">
|
||||
${u.banned ? "Entsperren" : "Bannen"}
|
||||
</button>
|
||||
${isMuted
|
||||
? `<button class="admin-on" onclick="muteUser(${u.id}, 0)">Entstummen</button>`
|
||||
: `<button class="admin-off" onclick="muteUser(${u.id}, null)">Stummschalten</button>`}
|
||||
</td>
|
||||
`;
|
||||
body.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
async function toggleBan(id, banned) {
|
||||
const res = await authFetch(`/api/admin/players/${id}/ban`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ banned })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.ok) loadUsers();
|
||||
else alert("Fehler beim Ändern des Bann-Status.");
|
||||
}
|
||||
|
||||
async function muteUser(id, minutes) {
|
||||
if (minutes === null) {
|
||||
const input = prompt("Für wie viele Minuten stummschalten?", "30");
|
||||
if (!input) return;
|
||||
minutes = Number(input);
|
||||
if (!minutes || minutes <= 0) return;
|
||||
}
|
||||
|
||||
const res = await authFetch(`/api/admin/players/${id}/mute`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ minutes })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.ok) loadUsers();
|
||||
else alert("Fehler beim Stummschalten.");
|
||||
}
|
||||
|
||||
async function approveUser(id) {
|
||||
const res = await authFetch(`/api/admin/players/${id}/approve`, { method: "POST" });
|
||||
const data = await res.json();
|
||||
if (data.ok) {
|
||||
showMsg("Benutzer freigeschaltet.");
|
||||
loadUsers();
|
||||
} else {
|
||||
showMsg("Fehler beim Freischalten.", true);
|
||||
}
|
||||
}
|
||||
|
||||
async function rejectUser(id) {
|
||||
if (!confirm("Diese Bewerbung wirklich ablehnen? Der Account wird komplett gelöscht.")) return;
|
||||
|
||||
const res = await authFetch(`/api/admin/players/${id}/reject`, { method: "POST" });
|
||||
const data = await res.json();
|
||||
if (data.ok) {
|
||||
showMsg("Bewerbung abgelehnt.");
|
||||
loadUsers();
|
||||
} else {
|
||||
showMsg(data.error || "Fehler beim Ablehnen.", true);
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement("div");
|
||||
div.textContent = text == null ? "" : String(text);
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
async function revokeUser(id) {
|
||||
if (!confirm("Diesen Benutzer wirklich sperren? Eine aktive Verbindung wird getrennt.")) return;
|
||||
|
||||
const res = await authFetch(`/api/admin/players/${id}/revoke`, { method: "POST" });
|
||||
const data = await res.json();
|
||||
if (data.ok) {
|
||||
showMsg("Benutzer gesperrt.");
|
||||
loadUsers();
|
||||
} else {
|
||||
showMsg("Fehler beim Sperren.", true);
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleAdmin(id, makeAdmin) {
|
||||
const res = await authFetch(`/api/admin/players/${id}/set_admin`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ isAdmin: makeAdmin })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.ok) {
|
||||
showMsg(makeAdmin ? "Admin-Rechte vergeben." : "Admin-Rechte entzogen.");
|
||||
loadUsers();
|
||||
} else {
|
||||
showMsg("Fehler.", true);
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleBetaTester(id, makeBetaTester) {
|
||||
const res = await authFetch(`/api/admin/players/${id}/set_beta_tester`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ isBetaTester: makeBetaTester })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.ok) {
|
||||
showMsg(makeBetaTester ? "Beta-Zugang vergeben." : "Beta-Zugang entzogen.");
|
||||
loadUsers();
|
||||
} else {
|
||||
showMsg("Fehler.", true);
|
||||
}
|
||||
}
|
||||
|
||||
function showMsg(text, isError) {
|
||||
const el = document.getElementById("userMsg");
|
||||
el.style.color = isError ? "#e06c6c" : "#6fbf73";
|
||||
el.textContent = text;
|
||||
setTimeout(() => { el.textContent = ""; }, 4000);
|
||||
}
|
||||
|
||||
loadUsers();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,200 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Anmerkungen</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
background: #111;
|
||||
color: #eee;
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 20px;
|
||||
}
|
||||
h1 { margin-top: 0; }
|
||||
a.back { color: #6fbf73; text-decoration: none; font-size: 14px; }
|
||||
a.back:hover { text-decoration: underline; }
|
||||
|
||||
main { max-width: 700px; margin: 20px auto 0; }
|
||||
|
||||
.panel {
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.panel h2 { margin-top: 0; }
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
min-height: 100px;
|
||||
background: #222;
|
||||
border: 1px solid #444;
|
||||
color: #eee;
|
||||
padding: 8px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
font-family: inherit;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
button {
|
||||
background: #2c7a3d;
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 8px 14px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
button.danger { background: #a83232; padding: 4px 8px; font-size: 12px; margin-top: 0; }
|
||||
button:hover { opacity: 0.85; }
|
||||
|
||||
.remark-item {
|
||||
border-bottom: 1px solid #2a2a2a;
|
||||
padding: 12px 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
.remark-item:last-child { border-bottom: none; }
|
||||
.remark-meta { color: #777; font-size: 12px; margin-bottom: 4px; }
|
||||
.remark-content { white-space: pre-wrap; line-height: 1.5; }
|
||||
|
||||
.msg { font-size: 13px; color: #6fbf73; min-height: 18px; margin-top: 8px; }
|
||||
.empty-hint { color: #666; padding: 10px 0; }
|
||||
.hidden { display: none !important; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<a class="back" href="/index.html">← zurück zur Startseite</a>
|
||||
<h1>📝 Anmerkungen</h1>
|
||||
|
||||
<main>
|
||||
<div class="panel">
|
||||
<h2>Anmerkung schreiben</h2>
|
||||
<p style="color:#888; font-size:13px; margin-top:0;">
|
||||
Geht direkt und privat an die Admins - z.B. Bug-Meldungen, Kritik, Lob.
|
||||
</p>
|
||||
<textarea id="remarkContent" placeholder="Deine Anmerkung..."></textarea>
|
||||
<button onclick="submitRemark()">Absenden</button>
|
||||
<div class="msg" id="remarkMsg"></div>
|
||||
</div>
|
||||
|
||||
<div class="panel hidden" id="adminPanel">
|
||||
<h2>Eingegangene Anmerkungen (nur Admins)</h2>
|
||||
<div id="remarkList"></div>
|
||||
<div class="empty-hint hidden" id="remarkEmpty">Noch keine Anmerkungen vorhanden.</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
const token = localStorage.getItem("token");
|
||||
const isAdmin = localStorage.getItem("isAdmin") === "true";
|
||||
|
||||
if (!token) {
|
||||
alert("Bitte zuerst auf der Startseite einloggen.");
|
||||
location.href = "/index.html";
|
||||
}
|
||||
|
||||
async function authFetch(url, options = {}) {
|
||||
options.headers = { ...(options.headers || {}), "Authorization": "Bearer " + token };
|
||||
const res = await fetch(url, options);
|
||||
if (res.status === 401) {
|
||||
alert("Sitzung abgelaufen. Bitte erneut einloggen.");
|
||||
location.href = "/index.html";
|
||||
throw new Error("Nicht autorisiert");
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
const div = document.createElement("div");
|
||||
div.textContent = str;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
async function submitRemark() {
|
||||
const content = document.getElementById("remarkContent").value.trim();
|
||||
if (!content) {
|
||||
showMsg("Bitte einen Text eingeben.", true);
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await authFetch("/api/remarks", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ content })
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
showMsg("Danke, deine Anmerkung wurde übermittelt!");
|
||||
document.getElementById("remarkContent").value = "";
|
||||
if (isAdmin) loadRemarks();
|
||||
} else {
|
||||
showMsg("Fehler: " + (data.error || "unbekannt"), true);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRemarks() {
|
||||
if (!isAdmin) return;
|
||||
document.getElementById("adminPanel").classList.remove("hidden");
|
||||
|
||||
const res = await authFetch("/api/admin/remarks");
|
||||
const data = await res.json();
|
||||
renderRemarks(data.remarks || []);
|
||||
}
|
||||
|
||||
function renderRemarks(items) {
|
||||
const list = document.getElementById("remarkList");
|
||||
const empty = document.getElementById("remarkEmpty");
|
||||
list.innerHTML = "";
|
||||
|
||||
if (items.length === 0) {
|
||||
empty.classList.remove("hidden");
|
||||
return;
|
||||
}
|
||||
empty.classList.add("hidden");
|
||||
|
||||
items.forEach(r => {
|
||||
const date = new Date(r.created_at).toLocaleString("de-DE");
|
||||
const div = document.createElement("div");
|
||||
div.className = "remark-item";
|
||||
div.innerHTML = `
|
||||
<div>
|
||||
<div class="remark-meta">${date} — ${escapeHtml(r.username)}</div>
|
||||
<div class="remark-content">${escapeHtml(r.content)}</div>
|
||||
</div>
|
||||
<button class="danger deleteBtn" data-id="${r.id}">Löschen</button>
|
||||
`;
|
||||
list.appendChild(div);
|
||||
});
|
||||
|
||||
list.querySelectorAll(".deleteBtn").forEach(btn => {
|
||||
btn.onclick = () => deleteRemark(btn.dataset.id);
|
||||
});
|
||||
}
|
||||
|
||||
async function deleteRemark(id) {
|
||||
if (!confirm("Diese Anmerkung wirklich löschen?")) return;
|
||||
await authFetch("/api/admin/remarks/" + id, { method: "DELETE" });
|
||||
loadRemarks();
|
||||
}
|
||||
|
||||
function showMsg(text, isError) {
|
||||
const el = document.getElementById("remarkMsg");
|
||||
el.style.color = isError ? "#e06c6c" : "#6fbf73";
|
||||
el.textContent = text;
|
||||
setTimeout(() => { el.textContent = ""; }, 4000);
|
||||
}
|
||||
|
||||
loadRemarks();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,443 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Auktionshaus</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
background: #111;
|
||||
color: #eee;
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 20px;
|
||||
}
|
||||
h1 { margin-top: 0; }
|
||||
a.back { color: #6fbf73; text-decoration: none; font-size: 14px; }
|
||||
a.back:hover { text-decoration: underline; }
|
||||
|
||||
main { max-width: 900px; margin: 20px auto 0; }
|
||||
|
||||
.tabs { display: flex; gap: 8px; margin-bottom: 16px; }
|
||||
.tab-btn {
|
||||
background: #222;
|
||||
border: 1px solid #444;
|
||||
color: #ccc;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
.tab-btn.active { background: #2c7a3d; border-color: #2c7a3d; color: white; }
|
||||
|
||||
.tab-panel { display: none; }
|
||||
.tab-panel.active { display: block; }
|
||||
|
||||
.panel {
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.panel h2 { margin-top: 0; }
|
||||
|
||||
.auction-item {
|
||||
border-bottom: 1px solid #2a2a2a;
|
||||
padding: 14px 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.auction-item:last-child { border-bottom: none; }
|
||||
.auction-name { font-weight: bold; font-size: 15px; }
|
||||
.auction-meta { color: #888; font-size: 12px; margin-top: 2px; }
|
||||
.auction-price { color: #f5d90a; font-weight: bold; font-size: 15px; }
|
||||
.auction-actions { display: flex; gap: 6px; align-items: center; }
|
||||
.auction-actions input {
|
||||
width: 80px;
|
||||
background: #222;
|
||||
border: 1px solid #444;
|
||||
color: #eee;
|
||||
padding: 6px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
input, select {
|
||||
background: #222;
|
||||
border: 1px solid #444;
|
||||
color: #eee;
|
||||
padding: 8px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
button {
|
||||
background: #2c7a3d;
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 8px 14px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
button.secondary { background: #444; }
|
||||
button.danger { background: #a83232; }
|
||||
button:hover { opacity: 0.85; }
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
gap: 10px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.form-grid label { display: block; font-size: 12px; color: #999; margin-bottom: 4px; }
|
||||
.form-grid input, .form-grid select { width: 100%; }
|
||||
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
border-radius: 4px;
|
||||
padding: 2px 8px;
|
||||
font-size: 11px;
|
||||
margin-left: 6px;
|
||||
}
|
||||
.status-sold { background: #1a4d2a; color: #6fbf73; }
|
||||
.status-expired { background: #444; color: #ccc; }
|
||||
.status-cancelled { background: #5a2a2a; color: #e08a8a; }
|
||||
|
||||
.msg { font-size: 13px; color: #6fbf73; min-height: 18px; margin-top: 8px; }
|
||||
.empty-hint { color: #666; padding: 10px 0; }
|
||||
.hidden { display: none !important; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<a class="back" href="/index.html">← zurück zur Startseite</a>
|
||||
<h1>🏛️ Auktionshaus</h1>
|
||||
|
||||
<main>
|
||||
<div class="tabs">
|
||||
<button class="tab-btn active" data-tab="browse">Angebote durchstöbern</button>
|
||||
<button class="tab-btn" data-tab="create">Neues Angebot</button>
|
||||
<button class="tab-btn" data-tab="mine">Meine Angebote</button>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel active" id="tab-browse">
|
||||
<div class="panel">
|
||||
<h2>Aktive Angebote</h2>
|
||||
<div id="auctionList"></div>
|
||||
<div class="empty-hint hidden" id="auctionEmpty">Aktuell keine aktiven Angebote.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel" id="tab-create">
|
||||
<div class="panel">
|
||||
<h2>Item verkaufen</h2>
|
||||
<div class="form-grid">
|
||||
<div>
|
||||
<label>Item aus deinem Inventar</label>
|
||||
<select id="newItemSelect"></select>
|
||||
</div>
|
||||
<div>
|
||||
<label>Menge</label>
|
||||
<input type="number" id="newAmount" value="1" min="1">
|
||||
</div>
|
||||
<div>
|
||||
<label>Startpreis ($)</label>
|
||||
<input type="number" id="newStartPrice" value="100" min="1">
|
||||
</div>
|
||||
<div>
|
||||
<label>Sofortkauf-Preis (optional)</label>
|
||||
<input type="number" id="newBuyout" placeholder="leer = kein Sofortkauf">
|
||||
</div>
|
||||
<div>
|
||||
<label>Laufzeit (Stunden)</label>
|
||||
<input type="number" id="newHours" value="24" min="1" max="72">
|
||||
</div>
|
||||
</div>
|
||||
<button style="margin-top:14px;" onclick="createAuction()">Angebot einstellen</button>
|
||||
<div class="msg" id="createMsg"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel" id="tab-mine">
|
||||
<div class="panel">
|
||||
<h2>Meine Angebote</h2>
|
||||
<div id="mineList"></div>
|
||||
<div class="empty-hint hidden" id="mineEmpty">Du hast noch keine Angebote eingestellt.</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
const token = localStorage.getItem("token");
|
||||
|
||||
if (!token) {
|
||||
alert("Bitte zuerst auf der Startseite einloggen.");
|
||||
location.href = "/index.html";
|
||||
}
|
||||
|
||||
async function authFetch(url, options = {}) {
|
||||
options.headers = { ...(options.headers || {}), "Authorization": "Bearer " + token };
|
||||
const res = await fetch(url, options);
|
||||
if (res.status === 401) {
|
||||
alert("Sitzung abgelaufen. Bitte erneut einloggen.");
|
||||
location.href = "/index.html";
|
||||
throw new Error("Nicht autorisiert");
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
const div = document.createElement("div");
|
||||
div.textContent = str;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
let itemNames = {};
|
||||
|
||||
async function loadItemNames() {
|
||||
const res = await fetch("/api/items");
|
||||
const data = await res.json();
|
||||
(data.items || []).forEach(i => { itemNames[i.id] = i.name; });
|
||||
}
|
||||
|
||||
function itemLabel(id) {
|
||||
return itemNames[id] || id;
|
||||
}
|
||||
|
||||
function timeLeftLabel(endsAt) {
|
||||
const diff = new Date(endsAt).getTime() - Date.now();
|
||||
if (diff <= 0) return "läuft gleich ab";
|
||||
const hours = Math.floor(diff / 3600000);
|
||||
const mins = Math.floor((diff % 3600000) / 60000);
|
||||
if (hours > 0) return `noch ${hours}h ${mins}min`;
|
||||
return `noch ${mins}min`;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// TABS
|
||||
// -------------------------------------------------------------
|
||||
document.querySelectorAll(".tab-btn").forEach(btn => {
|
||||
btn.addEventListener("click", () => {
|
||||
document.querySelectorAll(".tab-btn").forEach(b => b.classList.remove("active"));
|
||||
document.querySelectorAll(".tab-panel").forEach(p => p.classList.remove("active"));
|
||||
btn.classList.add("active");
|
||||
document.getElementById("tab-" + btn.dataset.tab).classList.add("active");
|
||||
|
||||
if (btn.dataset.tab === "create") loadInventoryForSelect();
|
||||
if (btn.dataset.tab === "mine") loadMyAuctions();
|
||||
});
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// ANGEBOTE DURCHSTÖBERN
|
||||
// -------------------------------------------------------------
|
||||
async function loadAuctions() {
|
||||
const res = await authFetch("/api/auctions");
|
||||
const data = await res.json();
|
||||
renderAuctions(data.auctions || []);
|
||||
}
|
||||
|
||||
function renderAuctions(auctions) {
|
||||
const list = document.getElementById("auctionList");
|
||||
const empty = document.getElementById("auctionEmpty");
|
||||
list.innerHTML = "";
|
||||
|
||||
if (auctions.length === 0) {
|
||||
empty.classList.remove("hidden");
|
||||
return;
|
||||
}
|
||||
empty.classList.add("hidden");
|
||||
|
||||
auctions.forEach(a => {
|
||||
const minBid = a.current_bid ? a.current_bid + 1 : a.starting_price;
|
||||
const div = document.createElement("div");
|
||||
div.className = "auction-item";
|
||||
|
||||
div.innerHTML = `
|
||||
<div>
|
||||
<div class="auction-name">${itemLabel(a.item_id)} ${a.amount > 1 ? "x" + a.amount : ""}</div>
|
||||
<div class="auction-meta">
|
||||
Verkäufer: ${escapeHtml(a.seller_name)} — ${timeLeftLabel(a.ends_at)}
|
||||
${a.bidder_name ? ` — höchstes Gebot von ${escapeHtml(a.bidder_name)}` : ""}
|
||||
</div>
|
||||
</div>
|
||||
<div class="auction-price">${a.current_bid ? a.current_bid + "$" : a.starting_price + "$ (Start)"}</div>
|
||||
<div class="auction-actions">
|
||||
<input type="number" class="bidInput" data-id="${a.id}" placeholder="min. ${minBid}$" value="${minBid}">
|
||||
<button class="bidBtn" data-id="${a.id}">Bieten</button>
|
||||
${a.buyout_price ? `<button class="secondary buyoutBtn" data-id="${a.id}">Sofort für ${a.buyout_price}$</button>` : ""}
|
||||
</div>
|
||||
`;
|
||||
list.appendChild(div);
|
||||
});
|
||||
|
||||
list.querySelectorAll(".bidBtn").forEach(btn => {
|
||||
btn.onclick = () => placeBid(btn.dataset.id);
|
||||
});
|
||||
list.querySelectorAll(".buyoutBtn").forEach(btn => {
|
||||
btn.onclick = () => buyout(btn.dataset.id);
|
||||
});
|
||||
}
|
||||
|
||||
async function placeBid(id) {
|
||||
const input = document.querySelector(`.bidInput[data-id="${id}"]`);
|
||||
const amount = Number(input.value);
|
||||
if (!amount || amount <= 0) return;
|
||||
|
||||
const res = await authFetch(`/api/auctions/${id}/bid`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ amount })
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
loadAuctions();
|
||||
} else {
|
||||
alert(data.error || "Gebot fehlgeschlagen.");
|
||||
}
|
||||
}
|
||||
|
||||
async function buyout(id) {
|
||||
if (!confirm("Dieses Angebot jetzt sofort kaufen?")) return;
|
||||
|
||||
const res = await authFetch(`/api/auctions/${id}/buyout`, { method: "POST" });
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
alert("Gekauft! Das Item ist jetzt in deinem Inventar.");
|
||||
loadAuctions();
|
||||
} else {
|
||||
alert(data.error || "Kauf fehlgeschlagen.");
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// NEUES ANGEBOT ERSTELLEN
|
||||
// -------------------------------------------------------------
|
||||
async function loadInventoryForSelect() {
|
||||
const res = await authFetch("/api/me/inventory");
|
||||
const data = await res.json();
|
||||
const select = document.getElementById("newItemSelect");
|
||||
|
||||
const inv = (data.inventory || []).filter(i => i.amount > 0);
|
||||
if (inv.length === 0) {
|
||||
select.innerHTML = `<option value="">Kein Item im Inventar</option>`;
|
||||
return;
|
||||
}
|
||||
|
||||
select.innerHTML = inv.map(i =>
|
||||
`<option value="${i.id}" data-max="${i.amount}">${itemLabel(i.id)} (${i.amount}x vorhanden)</option>`
|
||||
).join("");
|
||||
}
|
||||
|
||||
async function createAuction() {
|
||||
const select = document.getElementById("newItemSelect");
|
||||
const itemId = select.value;
|
||||
if (!itemId) {
|
||||
showMsg("createMsg", "Kein Item ausgewählt.", true);
|
||||
return;
|
||||
}
|
||||
|
||||
const amount = Number(document.getElementById("newAmount").value) || 1;
|
||||
const startingPrice = Number(document.getElementById("newStartPrice").value) || 1;
|
||||
const buyoutVal = document.getElementById("newBuyout").value.trim();
|
||||
const buyoutPrice = buyoutVal ? Number(buyoutVal) : null;
|
||||
const hours = Number(document.getElementById("newHours").value) || 24;
|
||||
|
||||
const res = await authFetch("/api/auctions", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ itemId, amount, startingPrice, buyoutPrice, hours })
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
showMsg("createMsg", "Angebot eingestellt!");
|
||||
loadInventoryForSelect();
|
||||
} else {
|
||||
showMsg("createMsg", "Fehler: " + (data.error || "unbekannt"), true);
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// MEINE ANGEBOTE
|
||||
// -------------------------------------------------------------
|
||||
async function loadMyAuctions() {
|
||||
const res = await authFetch("/api/auctions/mine");
|
||||
const data = await res.json();
|
||||
renderMyAuctions(data.auctions || []);
|
||||
}
|
||||
|
||||
function renderMyAuctions(auctions) {
|
||||
const list = document.getElementById("mineList");
|
||||
const empty = document.getElementById("mineEmpty");
|
||||
list.innerHTML = "";
|
||||
|
||||
if (auctions.length === 0) {
|
||||
empty.classList.remove("hidden");
|
||||
return;
|
||||
}
|
||||
empty.classList.add("hidden");
|
||||
|
||||
const statusLabels = { active: "Aktiv", sold: "Verkauft", expired: "Abgelaufen (zurückgegeben)", cancelled: "Storniert" };
|
||||
|
||||
auctions.forEach(a => {
|
||||
const div = document.createElement("div");
|
||||
div.className = "auction-item";
|
||||
|
||||
div.innerHTML = `
|
||||
<div>
|
||||
<div class="auction-name">
|
||||
${itemLabel(a.item_id)} ${a.amount > 1 ? "x" + a.amount : ""}
|
||||
<span class="status-badge status-${a.status}">${statusLabels[a.status] || a.status}</span>
|
||||
</div>
|
||||
<div class="auction-meta">${timeLeftLabel(a.ends_at)} - Startpreis ${a.starting_price}$${a.current_bid ? `, aktuelles Gebot ${a.current_bid}$` : ""}</div>
|
||||
</div>
|
||||
${a.status === "active" && !a.current_bid ? `<button class="danger cancelBtn" data-id="${a.id}">Stornieren</button>` : ""}
|
||||
`;
|
||||
list.appendChild(div);
|
||||
});
|
||||
|
||||
list.querySelectorAll(".cancelBtn").forEach(btn => {
|
||||
btn.onclick = () => cancelAuction(btn.dataset.id);
|
||||
});
|
||||
}
|
||||
|
||||
async function cancelAuction(id) {
|
||||
if (!confirm("Dieses Angebot wirklich stornieren? Du bekommst das Item zurück.")) return;
|
||||
|
||||
const res = await authFetch(`/api/auctions/${id}/cancel`, { method: "POST" });
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
loadMyAuctions();
|
||||
} else {
|
||||
alert(data.error || "Stornieren fehlgeschlagen.");
|
||||
}
|
||||
}
|
||||
|
||||
function showMsg(elId, text, isError) {
|
||||
const el = document.getElementById(elId);
|
||||
el.style.color = isError ? "#e06c6c" : "#6fbf73";
|
||||
el.textContent = text;
|
||||
setTimeout(() => { el.textContent = ""; }, 4000);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// INIT
|
||||
// -------------------------------------------------------------
|
||||
async function init() {
|
||||
await loadItemNames();
|
||||
await loadAuctions();
|
||||
setInterval(loadAuctions, 30000);
|
||||
}
|
||||
init();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,826 @@
|
||||
const canvas = document.getElementById("gameCanvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
canvas.width = 1280;
|
||||
canvas.height = 720;
|
||||
|
||||
const loginBox = document.getElementById("loginBox");
|
||||
const registerBox = document.getElementById("registerBox");
|
||||
|
||||
const loginUser = document.getElementById("loginUser");
|
||||
const loginPass = document.getElementById("loginPass");
|
||||
const loginBtn = document.getElementById("loginBtn");
|
||||
|
||||
const regUser = document.getElementById("regUser");
|
||||
const regPass = document.getElementById("regPass");
|
||||
const regBtn = document.getElementById("regBtn");
|
||||
|
||||
const switchToRegister = document.getElementById("switchToRegister");
|
||||
const switchToLogin = document.getElementById("switchToLogin");
|
||||
|
||||
const shopWindow = document.getElementById("shopWindow");
|
||||
const shopList = document.getElementById("shopList");
|
||||
|
||||
let ws = null;
|
||||
let maps = {};
|
||||
let tileConfig = {};
|
||||
let lastServerUpdate = 0;
|
||||
|
||||
let player = {
|
||||
id: null,
|
||||
x: 100,
|
||||
y: 100,
|
||||
world: "stadt"
|
||||
};
|
||||
player.health = 100;
|
||||
player.hunger = 100;
|
||||
player.thirst = 100;
|
||||
|
||||
player.inventory = [];
|
||||
player.money = 1000;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
let loggedIn = false;
|
||||
let otherPlayers = [];
|
||||
let keys = {};
|
||||
let shops = [];
|
||||
let atms = [];
|
||||
|
||||
let cars = [];
|
||||
let drivingCarId = null;
|
||||
|
||||
|
||||
|
||||
let objectConfig = {};
|
||||
|
||||
fetch("/objectConfig.json")
|
||||
.then(res => res.json())
|
||||
.then(data => objectConfig = data);
|
||||
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// Tiles laden
|
||||
// -------------------------------------------------------------
|
||||
fetch("/tiles.json")
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
tileConfig = data;
|
||||
console.log("TileConfig geladen:", tileConfig);
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// REGISTER
|
||||
// -------------------------------------------------------------
|
||||
regBtn.onclick = async () => {
|
||||
const username = regUser.value;
|
||||
const password = regPass.value;
|
||||
|
||||
const res = await fetch("/api/register", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ username, password })
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (!data.ok) {
|
||||
alert(data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
alert("Registrierung erfolgreich!");
|
||||
registerBox.style.display = "none";
|
||||
loginBox.style.display = "block";
|
||||
};
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// LOGIN
|
||||
// -------------------------------------------------------------
|
||||
loginBtn.onclick = async () => {
|
||||
const username = loginUser.value;
|
||||
const password = loginPass.value;
|
||||
|
||||
const res = await fetch("/api/login", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ username, password })
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (!data.ok) {
|
||||
alert(data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
loginBox.style.display = "none";
|
||||
canvas.style.display = "block";
|
||||
|
||||
startMultiplayer(data.token);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
// Hilfsfunktion: Spieler holen
|
||||
function getPlayer(playerId) {
|
||||
return playersOnline.get(playerId);
|
||||
}
|
||||
|
||||
// BANK: PIN prüfen
|
||||
async function handleBankPinCheck(playerId, data, ws) {
|
||||
const p = getPlayer(playerId);
|
||||
if (!p) return;
|
||||
|
||||
const pin = Number(data.pin) || 0;
|
||||
|
||||
const [rows] = await db.query("SELECT pin FROM players WHERE id=?", [playerId]);
|
||||
if (!rows.length) return;
|
||||
|
||||
if (rows[0].pin === pin) {
|
||||
ws.send(JSON.stringify({ type: "bank_pin_ok" }));
|
||||
} else {
|
||||
ws.send(JSON.stringify({ type: "bank_pin_fail" }));
|
||||
}
|
||||
}
|
||||
|
||||
// BANK: Fenster öffnen (Kontostand + Bargeld)
|
||||
async function handleBankOpen(playerId, ws) {
|
||||
const p = getPlayer(playerId);
|
||||
if (!p) return;
|
||||
|
||||
ws.send(JSON.stringify({
|
||||
type: "bank_open",
|
||||
money: p.state.money,
|
||||
bank: p.state.bank
|
||||
}));
|
||||
}
|
||||
|
||||
// BANK: Einzahlen
|
||||
async function handleBankDeposit(playerId, data, ws) {
|
||||
const p = getPlayer(playerId);
|
||||
if (!p) return;
|
||||
|
||||
const amount = Number(data.amount) || 0;
|
||||
if (amount <= 0) return;
|
||||
if (p.state.money < amount) return;
|
||||
|
||||
p.state.money -= amount;
|
||||
p.state.bank += amount;
|
||||
|
||||
await db.query(
|
||||
"UPDATE players SET money=?, bank=? WHERE id=?",
|
||||
[p.state.money, p.state.bank, playerId]
|
||||
);
|
||||
|
||||
ws.send(JSON.stringify({
|
||||
type: "bank_update",
|
||||
money: p.state.money,
|
||||
bank: p.state.bank
|
||||
}));
|
||||
}
|
||||
|
||||
// BANK: Auszahlen
|
||||
async function handleBankWithdraw(playerId, data, ws) {
|
||||
const p = getPlayer(playerId);
|
||||
if (!p) return;
|
||||
|
||||
const amount = Number(data.amount) || 0;
|
||||
if (amount <= 0) return;
|
||||
if (p.state.bank < amount) return;
|
||||
|
||||
p.state.bank -= amount;
|
||||
p.state.money += amount;
|
||||
|
||||
await db.query(
|
||||
"UPDATE players SET money=?, bank=? WHERE id=?",
|
||||
[p.state.money, p.state.bank, playerId]
|
||||
);
|
||||
|
||||
ws.send(JSON.stringify({
|
||||
type: "bank_update",
|
||||
money: p.state.money,
|
||||
bank: p.state.bank
|
||||
}));
|
||||
}
|
||||
|
||||
// BANK: Geld an anderen Spieler senden (Bank → Bank)
|
||||
async function handleBankTransfer(playerId, data, ws) {
|
||||
const p = getPlayer(playerId);
|
||||
if (!p) return;
|
||||
|
||||
const targetId = Number(data.targetId);
|
||||
const amount = Number(data.amount) || 0;
|
||||
if (amount <= 0) return;
|
||||
|
||||
const t = getPlayer(targetId);
|
||||
if (!t) return;
|
||||
if (p.state.bank < amount) return;
|
||||
|
||||
p.state.bank -= amount;
|
||||
t.state.bank += amount;
|
||||
|
||||
await db.query("UPDATE players SET bank=? WHERE id=?", [p.state.bank, playerId]);
|
||||
await db.query("UPDATE players SET bank=? WHERE id=?", [t.state.bank, targetId]);
|
||||
|
||||
ws.send(JSON.stringify({
|
||||
type: "bank_update",
|
||||
money: p.state.money,
|
||||
bank: p.state.bank
|
||||
}));
|
||||
|
||||
t.ws.send(JSON.stringify({
|
||||
type: "bank_update",
|
||||
money: t.state.money,
|
||||
bank: t.state.bank
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// UI SWITCH
|
||||
// -------------------------------------------------------------
|
||||
switchToRegister.onclick = () => {
|
||||
loginBox.style.display = "none";
|
||||
registerBox.style.display = "block";
|
||||
canvas.style.display = "none";
|
||||
};
|
||||
|
||||
switchToLogin.onclick = () => {
|
||||
registerBox.style.display = "none";
|
||||
loginBox.style.display = "block";
|
||||
canvas.style.display = "none";
|
||||
};
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// MULTIPLAYER STARTEN
|
||||
// -------------------------------------------------------------
|
||||
function startMultiplayer(token) {
|
||||
ws = new WebSocket("ws://45.81.233.187:5555");
|
||||
|
||||
ws.onopen = () => {
|
||||
ws.send(JSON.stringify({ type: "auth", token }));
|
||||
};
|
||||
|
||||
ws.onmessage = ev => {
|
||||
const msg = JSON.parse(ev.data);
|
||||
|
||||
if (msg.type === "auth_ok") {
|
||||
player.id = msg.id;
|
||||
maps = msg.maps;
|
||||
document.getElementById("topMenu").style.display = "flex";
|
||||
document.getElementById("statusWindow").style.display = "block";
|
||||
document.getElementById("inventoryWindow").style.display = "block";
|
||||
|
||||
player.inventory = msg.inventory || [];
|
||||
document.getElementById("inventoryWindow").style.display = "block";
|
||||
renderInventoryWindow();
|
||||
canvas.style.display = "block";
|
||||
|
||||
console.log("MAPS GELADEN:", maps);
|
||||
|
||||
const map = maps[player.world];
|
||||
if (map && map.spawn) {
|
||||
player.x = map.spawn.x;
|
||||
player.y = map.spawn.y;
|
||||
}
|
||||
//shops=map.shops;
|
||||
loggedIn = true;
|
||||
|
||||
return;
|
||||
}
|
||||
if (msg.type === "state") {
|
||||
lastServerUpdate = Date.now();
|
||||
|
||||
const me = msg.players.find(p => p.id === player.id);
|
||||
if (me) {
|
||||
// nur setzen, wenn wir NICHT gerade bewegen
|
||||
if (!keys["w"] && !keys["a"] && !keys["s"] && !keys["d"]) {
|
||||
player.x = me.x;
|
||||
player.y = me.y;
|
||||
}
|
||||
player.world = me.world;
|
||||
player.money = me.money;
|
||||
player.bank = me.bank;
|
||||
player.health = me.health;
|
||||
player.hunger = me.hunger;
|
||||
player.thirst = me.thirst;
|
||||
player.inventory = me.inventory;
|
||||
//console.log("geld:", player.money);
|
||||
}
|
||||
|
||||
otherPlayers = msg.players.filter(p => p.id !== player.id);
|
||||
renderInventoryWindow();
|
||||
updateStatusUI();
|
||||
}
|
||||
|
||||
if (msg.type === "npc_dialog") {
|
||||
alert("NPC sagt: " + msg.text);
|
||||
}
|
||||
if (msg.type === "shop_open") {
|
||||
const shopWindow = document.getElementById("shopWindow");
|
||||
const shopList = document.getElementById("shopList");
|
||||
|
||||
shopWindow.style.display = "block";
|
||||
shopList.innerHTML = "";
|
||||
|
||||
msg.items.forEach(item => {
|
||||
const div = document.createElement("div");
|
||||
div.style.padding = "5px";
|
||||
div.style.borderBottom = "1px solid #333";
|
||||
div.innerHTML = `
|
||||
${item.name} - ${item.price}$
|
||||
<button onclick="buyItem('${item.id}')">Kaufen</button>
|
||||
`;
|
||||
shopList.appendChild(div);
|
||||
});
|
||||
}
|
||||
|
||||
if (msg.type === "shop_error") {
|
||||
alert(msg.msg);
|
||||
}
|
||||
if (msg.type === "item_used") {
|
||||
console.log(msg.hunger)
|
||||
console.log(msg.thirst)
|
||||
player.hunger = msg.hunger;
|
||||
player.thirst = msg.thirst;
|
||||
player.health = msg.health;
|
||||
player.inventory = msg.inventory;
|
||||
|
||||
updateStatusWindow();
|
||||
renderInventoryWindow();
|
||||
}
|
||||
if (msg.type === "atm_open") {
|
||||
const pin = prompt("PIN eingeben");
|
||||
ws.send(JSON.stringify({ type: "bank_pin_check", pin }));
|
||||
}
|
||||
if (msg.type === "bank_pin_ok") {
|
||||
console.log("bank_pin_ok KOMMT AN");
|
||||
ws.send(JSON.stringify({ type: "bank_open" }));
|
||||
}
|
||||
if (msg.type === "bank_pin_fail") {
|
||||
alert("Falsche PIN!");
|
||||
}
|
||||
if (msg.type === "bank_open") {
|
||||
console.log("BANK_OPEN KOMMT AN");
|
||||
openATMWindow(msg.money, msg.bank);
|
||||
}
|
||||
if (msg.type === "bank_update") {
|
||||
document.getElementById("atmMoney").innerText = msg.money;
|
||||
document.getElementById("atmBank").innerText = msg.bank;
|
||||
}
|
||||
// im ws.onmessage ergänzen:
|
||||
if (msg.type === "cars") {
|
||||
cars = msg.cars;
|
||||
const mine = cars.find(c => c.driverId === player.id);
|
||||
drivingCarId = mine ? mine.id : null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (msg.type === "object_interact") {
|
||||
if (msg.action === "loot") {
|
||||
alert("Du hast die Kiste geöffnet!");
|
||||
}
|
||||
}
|
||||
|
||||
// Map-Daten
|
||||
if (msg.type === "map_data") {
|
||||
tiles = msg.tiles;
|
||||
doors = msg.doors || [];
|
||||
objects = msg.objects || [];
|
||||
shops = msg.shops || []; // <-- WICHTIG
|
||||
atms = msg.atms || [];
|
||||
spawn = msg.spawn;
|
||||
|
||||
console.log("shops:", shops);
|
||||
console.log("atms:", atms);
|
||||
return;
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
function openATMWindow(money, bank) {
|
||||
document.getElementById("atmMoney").innerText = money;
|
||||
document.getElementById("atmBank").innerText = bank;
|
||||
document.getElementById("atmBox").style.display = "block";
|
||||
}
|
||||
|
||||
function closeATM() {
|
||||
document.getElementById("atmBox").style.display = "none";
|
||||
}
|
||||
|
||||
function atmDeposit() {
|
||||
const amount = Number(document.getElementById("atmAmount").value);
|
||||
ws.send(JSON.stringify({ type: "bank_deposit", amount }));
|
||||
}
|
||||
|
||||
function atmWithdraw() {
|
||||
const amount = Number(document.getElementById("atmAmount").value);
|
||||
ws.send(JSON.stringify({ type: "bank_withdraw", amount }));
|
||||
}
|
||||
|
||||
function atmTransfer() {
|
||||
const amount = Number(document.getElementById("atmAmount").value);
|
||||
const targetId = Number(document.getElementById("atmTarget").value);
|
||||
ws.send(JSON.stringify({ type: "bank_transfer", amount, targetId }));
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
function updateStatusUI() {
|
||||
const hungerBar = document.getElementById("hungerValue");
|
||||
const thirstBar = document.getElementById("thirstValue");
|
||||
const moneyLabel = document.getElementById("moneyValue");
|
||||
|
||||
if (hungerBar) hungerBar.textContent = player.hunger;
|
||||
if (thirstBar) thirstBar.textContent = player.thirst;
|
||||
if (moneyLabel) moneyLabel.textContent = player.money;
|
||||
}
|
||||
|
||||
function openATM(atm) {
|
||||
ws.send(JSON.stringify({
|
||||
type: "bank_pin_check",
|
||||
pin: prompt("PIN eingeben")
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
function useItem(itemId) {
|
||||
console.log("useItem wurde ausgeführt mit:", itemId);
|
||||
ws.send(JSON.stringify({
|
||||
type: "use_item",
|
||||
itemId
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
function buyItem(itemId) {
|
||||
ws.send(JSON.stringify({
|
||||
type: "shop_buy",
|
||||
itemId
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// CAMERA (mit Zentrierung bei kleinen Maps)
|
||||
// -------------------------------------------------------------
|
||||
function getCamera() {
|
||||
const map = maps[player.world];
|
||||
if (!map || !map.tiles || map.tiles.length === 0) {
|
||||
return { x: 0, y: 0 };
|
||||
}
|
||||
|
||||
const mapWidth = map.tiles[0].length * 32;
|
||||
const mapHeight = map.tiles.length * 32;
|
||||
|
||||
let camX = player.x - canvas.width / 2;
|
||||
let camY = player.y - canvas.height / 2;
|
||||
|
||||
if (mapWidth < canvas.width) {
|
||||
camX = -(canvas.width / 2 - mapWidth / 2);
|
||||
} else {
|
||||
if (camX < 0) camX = 0;
|
||||
if (camX > mapWidth - canvas.width)
|
||||
camX = mapWidth - canvas.width;
|
||||
}
|
||||
|
||||
if (mapHeight < canvas.height) {
|
||||
camY = -(canvas.height / 2 - mapHeight / 2);
|
||||
} else {
|
||||
if (camY < 0) camY = 0;
|
||||
if (camY > mapHeight - canvas.height)
|
||||
camY = mapHeight - canvas.height;
|
||||
}
|
||||
|
||||
return { x: camX, y: camY };
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// MOVEMENT + Collision
|
||||
// -------------------------------------------------------------
|
||||
document.addEventListener("keydown", e => {
|
||||
keys[e.key] = true;
|
||||
|
||||
if (!ws) return;
|
||||
|
||||
if (e.key.toLowerCase() === "e") {
|
||||
ws.send(JSON.stringify({
|
||||
type: "interact",
|
||||
x: player.x,
|
||||
y: player.y,
|
||||
world: player.world
|
||||
}));
|
||||
}
|
||||
});
|
||||
// Shop schließen (ESC)
|
||||
document.addEventListener("keydown", e => {
|
||||
if (e.key === "Escape") {
|
||||
shopWindow.style.display = "none";
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("keyup", e => {
|
||||
keys[e.key] = false;
|
||||
});
|
||||
|
||||
function canMoveTo(nx, ny) {
|
||||
const map = maps[player.world];
|
||||
if (!map || !map.tiles) return true;
|
||||
|
||||
const tileX = Math.floor(nx / 32);
|
||||
const tileY = Math.floor(ny / 32);
|
||||
|
||||
// TILE COLLISION
|
||||
const tileId = map.tiles[tileY]?.[tileX];
|
||||
const tile = tileConfig[tileId];
|
||||
if (tile && tile.collision) return false;
|
||||
|
||||
// OBJECT COLLISION
|
||||
if (map.objects) {
|
||||
for (const obj of map.objects) {
|
||||
const cfg = objectConfig[obj.type];
|
||||
if (!cfg || !cfg.collision) continue;
|
||||
|
||||
const ox = obj.x;
|
||||
const oy = obj.y - (cfg.height - 32); // Objekt höher als Tile
|
||||
const ow = cfg.width;
|
||||
const oh = cfg.height;
|
||||
|
||||
// Spieler-Hitbox (30x30)
|
||||
const px = nx;
|
||||
const py = ny;
|
||||
const pw = 30;
|
||||
const ph = 30;
|
||||
|
||||
const hit =
|
||||
px < ox + ow &&
|
||||
px + pw > ox &&
|
||||
py < oy + oh &&
|
||||
py + ph > oy;
|
||||
|
||||
if (hit) return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
function updateNeeds() {
|
||||
player.hunger -= 0.002;
|
||||
player.thirst -= 0.004;
|
||||
|
||||
if (player.hunger < 0) player.hunger = 0;
|
||||
if (player.thirst < 0) player.thirst = 0;
|
||||
|
||||
if (player.hunger === 0 || player.thirst === 0) {
|
||||
player.health -= 0.01;
|
||||
if (player.health < 0) player.health = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function updateStatusWindow() {
|
||||
const status = document.getElementById("statusWindow");
|
||||
|
||||
status.innerHTML = `
|
||||
Leben: ${Math.round(player.health)}<br>
|
||||
Hunger: ${Math.round(player.hunger)}<br>
|
||||
Durst: ${Math.round(player.thirst)}<br>
|
||||
Geld: ${player.money}$
|
||||
`;
|
||||
}
|
||||
|
||||
function updateMovement() {
|
||||
let moved = false;
|
||||
|
||||
let nx = player.x;
|
||||
let ny = player.y;
|
||||
|
||||
/*if (keys["w"]) ny -= 5;
|
||||
if (keys["s"]) ny += 5;
|
||||
if (keys["a"]) nx -= 5;
|
||||
if (keys["d"]) nx += 5;*/
|
||||
|
||||
if (keys["w"]) { ny -= 5; moved = true; }
|
||||
if (keys["s"]) { ny += 5; moved = true; }
|
||||
if (keys["a"]) { nx -= 5; moved = true; }
|
||||
if (keys["d"]) { nx += 5; moved = true; }
|
||||
|
||||
if (canMoveTo(nx, ny)) {
|
||||
player.x = nx;
|
||||
player.y = ny;
|
||||
|
||||
// FIX: nur senden, wenn WS existiert und verbunden ist
|
||||
if (ws && moved && ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({
|
||||
type: "move",
|
||||
x: nx,
|
||||
y: ny,
|
||||
//health: player.health,
|
||||
//hunger: player.hunger,
|
||||
//thirst: player.thirst
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (ws && player.id && moved) {
|
||||
ws.send(JSON.stringify({
|
||||
type: "move",
|
||||
x: player.x,
|
||||
y: player.y
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
function renderInventoryWindow() {
|
||||
// console.log("renderInventoryWindow wurde ausgeführt");
|
||||
const list = document.getElementById("inventoryList");
|
||||
if (!list) return;
|
||||
|
||||
list.innerHTML = "";
|
||||
|
||||
(player.inventory || []).forEach(item => {
|
||||
const div = document.createElement("div");
|
||||
div.style.display = "flex";
|
||||
div.style.justifyContent = "space-between";
|
||||
div.style.padding = "5px";
|
||||
div.style.borderBottom = "1px solid #333";
|
||||
|
||||
div.innerHTML = `
|
||||
<span>${item.name || item.id} x${item.amount}</span>
|
||||
<button class="useItemBtn" data-id="${item.id}">Benutzen</button>
|
||||
`;
|
||||
|
||||
list.appendChild(div);
|
||||
});
|
||||
|
||||
const buttons = document.querySelectorAll(".useItemBtn");
|
||||
//console.log("Buttons gefunden:", buttons.length);
|
||||
//console.log("Buttons gefunden:", document.querySelectorAll(".useItemBtn").length);
|
||||
|
||||
document.querySelectorAll(".useItemBtn").forEach(btn => {
|
||||
btn.onclick = () => {
|
||||
//console.log("Button wurde geklickt:", btn.dataset.id);
|
||||
//console.log("Buttons gefunden:", document.querySelectorAll(".useItemBtn").length);
|
||||
|
||||
useItem(btn.dataset.id);
|
||||
};
|
||||
});
|
||||
}
|
||||
// -------------------------------------------------------------
|
||||
// RENDERING
|
||||
// -------------------------------------------------------------
|
||||
function renderMap() {
|
||||
const map = maps[player.world];
|
||||
if (!map || !map.tiles) return;
|
||||
|
||||
const cam = getCamera();
|
||||
|
||||
for (let y = 0; y < map.tiles.length; y++) {
|
||||
for (let x = 0; x < map.tiles[y].length; x++) {
|
||||
const tileId = map.tiles[y][x];
|
||||
const tile = tileConfig[tileId];
|
||||
|
||||
if (!tile) continue;
|
||||
|
||||
ctx.fillStyle = tile.color;
|
||||
|
||||
ctx.fillRect(
|
||||
x * 32 - cam.x,
|
||||
y * 32 - cam.y,
|
||||
32,
|
||||
32
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderDoors() {
|
||||
const map = maps[player.world];
|
||||
if (!map || !map.doors) return;
|
||||
|
||||
const cam = getCamera();
|
||||
|
||||
map.doors.forEach(d => {
|
||||
ctx.fillStyle = "orange";
|
||||
ctx.fillRect(
|
||||
d.x - cam.x,
|
||||
d.y - cam.y,
|
||||
30,
|
||||
30
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function renderPlayers() {
|
||||
const cam = getCamera();
|
||||
|
||||
ctx.fillStyle = "yellow";
|
||||
ctx.fillRect(
|
||||
player.x - cam.x,
|
||||
player.y - cam.y,
|
||||
20,
|
||||
20
|
||||
);
|
||||
|
||||
otherPlayers.forEach(p => {
|
||||
if (p.world === player.world) {
|
||||
ctx.fillStyle = "cyan";
|
||||
ctx.fillRect(
|
||||
p.x - cam.x,
|
||||
p.y - cam.y,
|
||||
20,
|
||||
20
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderObjects() {
|
||||
const map = maps[player.world];
|
||||
if (!map || !map.objects) return;
|
||||
|
||||
const cam = getCamera();
|
||||
|
||||
map.objects.forEach(o => {
|
||||
const cfg = objectConfig[o.type];
|
||||
if (!cfg) return;
|
||||
|
||||
ctx.fillStyle = cfg.color;
|
||||
ctx.fillRect(
|
||||
o.x - cam.x,
|
||||
o.y - (cfg.height - 32) - cam.y,
|
||||
cfg.width,
|
||||
cfg.height
|
||||
);
|
||||
});
|
||||
}
|
||||
function renderShops() {
|
||||
const cam = getCamera();
|
||||
|
||||
shops.forEach(s => {
|
||||
const px = s.x * 32 - cam.x;
|
||||
const py = s.y * 32 - cam.y;
|
||||
|
||||
ctx.fillStyle = "yellow";
|
||||
ctx.fillRect(px, py, 32, 32);
|
||||
|
||||
ctx.strokeStyle = "black";
|
||||
ctx.strokeRect(px, py, 32, 32);
|
||||
});
|
||||
}
|
||||
function renderATMs() {
|
||||
const cam = getCamera();
|
||||
atms.forEach(a => {
|
||||
const px = a.x * 32 - cam.x; // klein!
|
||||
const py = a.y * 32 - cam.y; // klein!
|
||||
|
||||
ctx.fillStyle = "blue";
|
||||
ctx.fillRect(px, py, 32, 32);
|
||||
ctx.strokeStyle = "black"; // optional, wie bei Shops
|
||||
ctx.strokeRect(px, py, 32, 32);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function render() {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
renderMap();
|
||||
renderObjects();
|
||||
renderShops();
|
||||
renderATMs();
|
||||
renderDoors();
|
||||
renderPlayers();
|
||||
|
||||
requestAnimationFrame(render);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// GAME LOOP
|
||||
// -------------------------------------------------------------
|
||||
function gameLoop() {
|
||||
updateMovement();
|
||||
setTimeout(gameLoop, 16);
|
||||
updateStatusWindow();
|
||||
//updateNeeds();
|
||||
}
|
||||
|
||||
|
||||
render();
|
||||
gameLoop();
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
export const db = await mysql.createPool({
|
||||
host: "156.67.28.205",
|
||||
user: "game",
|
||||
password: "tito13101",
|
||||
database: "gamegta",
|
||||
port:"3406",
|
||||
connectionLimit: 10
|
||||
});
|
||||
|
||||
|
||||
ws = new WebSocket("ws://45.81.233.187:5555");
|
||||
@@ -0,0 +1,654 @@
|
||||
|
||||
const canvas = document.getElementById("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
const mapNameInput = document.getElementById("mapName");
|
||||
const mapSelect = document.getElementById("mapSelect");
|
||||
const newMapBtn = document.getElementById("newMapBtn");
|
||||
const saveMapBtn = document.getElementById("saveMapBtn");
|
||||
const modeSelect = document.getElementById("modeSelect");
|
||||
|
||||
const doorTargetMap = document.getElementById("doorTargetMap");
|
||||
const doorTargetX = document.getElementById("doorTargetX");
|
||||
const doorTargetY = document.getElementById("doorTargetY");
|
||||
|
||||
const mapWidthInput = document.getElementById("mapWidth");
|
||||
const mapHeightInput = document.getElementById("mapHeight");
|
||||
const resizeMapBtn = document.getElementById("resizeMapBtn");
|
||||
|
||||
const tilePalette = document.getElementById("tilePalette");
|
||||
const objectPalette = document.getElementById("objectPalette");
|
||||
|
||||
let objectConfig = {};
|
||||
let selectedObjectType = null;
|
||||
let objects = [];
|
||||
|
||||
let tileConfig = {};
|
||||
let selectedTile = 1;
|
||||
|
||||
let tileSize = 32;
|
||||
let tiles = [];
|
||||
let doors = [];
|
||||
let spawn = { x: 32, y: 32 };
|
||||
|
||||
let cols = 20;
|
||||
let rows = 20;
|
||||
|
||||
let shops = []; // SHOP SYSTEM
|
||||
let atms = [];
|
||||
|
||||
let currentShop = null;
|
||||
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// Tiles laden
|
||||
// -------------------------------------------------------------
|
||||
fetch("/tiles.json")
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
tileConfig = data;
|
||||
buildTilePalette();
|
||||
loadMapList();
|
||||
newMap();
|
||||
});
|
||||
|
||||
fetch("/objectConfig.json")
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
objectConfig = data;
|
||||
buildObjectPalette();
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// Tile-Palette bauen
|
||||
// -------------------------------------------------------------
|
||||
function buildTilePalette() {
|
||||
tilePalette.innerHTML = "";
|
||||
|
||||
Object.entries(tileConfig).forEach(([id, tile]) => {
|
||||
const div = document.createElement("div");
|
||||
div.style.background = tile.color;
|
||||
div.title = `${id}: ${tile.name} (collision: ${tile.collision})`;
|
||||
div.dataset.id = id;
|
||||
|
||||
div.onclick = () => {
|
||||
selectedTile = parseInt(id);
|
||||
};
|
||||
|
||||
tilePalette.appendChild(div);
|
||||
});
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// Object-Palette bauen
|
||||
// -------------------------------------------------------------
|
||||
function buildObjectPalette() {
|
||||
objectPalette.innerHTML = "";
|
||||
|
||||
Object.entries(objectConfig).forEach(([type, obj]) => {
|
||||
const div = document.createElement("div");
|
||||
div.style.width = "32px";
|
||||
div.style.height = "32px";
|
||||
div.style.background = obj.color;
|
||||
div.style.border = "2px solid #000";
|
||||
div.style.display = "inline-block";
|
||||
div.style.margin = "4px";
|
||||
div.style.cursor = "pointer";
|
||||
div.title = `${type}: ${obj.name}`;
|
||||
|
||||
div.onclick = () => {
|
||||
selectedObjectType = type;
|
||||
};
|
||||
|
||||
objectPalette.appendChild(div);
|
||||
});
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// Neue Map
|
||||
// -------------------------------------------------------------
|
||||
function newMap() {
|
||||
tiles = [];
|
||||
for (let y = 0; y < rows; y++) {
|
||||
tiles[y] = [];
|
||||
for (let x = 0; x < cols; x++) {
|
||||
tiles[y][x] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
doors = [];
|
||||
objects = [];
|
||||
shops = [];
|
||||
spawn = { x: 32, y: 32 };
|
||||
|
||||
render();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// Map-Liste laden
|
||||
// -------------------------------------------------------------
|
||||
function loadMapList() {
|
||||
fetch("/api/get_maps")
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
mapSelect.innerHTML = `<option value="">Map laden...</option>`;
|
||||
Object.keys(data.maps).forEach(name => {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = name;
|
||||
opt.textContent = name;
|
||||
mapSelect.appendChild(opt);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// Map laden
|
||||
// -------------------------------------------------------------
|
||||
mapSelect.onchange = () => {
|
||||
const name = mapSelect.value;
|
||||
if (!name) return;
|
||||
|
||||
fetch("/api/get_maps")
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
const map = data.maps[name];
|
||||
|
||||
mapNameInput.value = map.name;
|
||||
tiles = map.tiles;
|
||||
doors = map.doors || [];
|
||||
objects = map.objects || [];
|
||||
shops = map.shops || [];
|
||||
atms = map.atms || [];
|
||||
atms = atms || [];
|
||||
|
||||
spawn = map.spawn;
|
||||
|
||||
rows = tiles.length;
|
||||
cols = tiles[0].length;
|
||||
|
||||
render();
|
||||
});
|
||||
};
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// Map speichern
|
||||
// -------------------------------------------------------------
|
||||
saveMapBtn.onclick = () => {
|
||||
const name = mapNameInput.value.trim();
|
||||
if (!name) return alert("Map-Name fehlt!");
|
||||
|
||||
const mapData = {
|
||||
name,
|
||||
spawn,
|
||||
tiles,
|
||||
doors,
|
||||
objects,
|
||||
shops,
|
||||
atms
|
||||
};
|
||||
|
||||
fetch("/api/save_map", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name, data: mapData })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.ok) {
|
||||
alert("Map gespeichert!");
|
||||
loadMapList();
|
||||
} else {
|
||||
alert("Fehler: " + data.error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
function saveMap() {
|
||||
const name = mapNameInput.value.trim();
|
||||
if (!name) return alert("Map-Name fehlt!");
|
||||
|
||||
const mapData = {
|
||||
name,
|
||||
spawn,
|
||||
tiles,
|
||||
doors,
|
||||
objects,
|
||||
shops,
|
||||
atms
|
||||
};
|
||||
|
||||
fetch("/api/save_map", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name, data: mapData })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.ok) {
|
||||
alert("Map gespeichert!");
|
||||
loadMapList();
|
||||
} else {
|
||||
alert("Fehler: " + data.error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// Map-Größe ändern
|
||||
// -------------------------------------------------------------
|
||||
resizeMapBtn.onclick = () => {
|
||||
const newW = parseInt(mapWidthInput.value);
|
||||
const newH = parseInt(mapHeightInput.value);
|
||||
|
||||
if (!newW || !newH) return;
|
||||
|
||||
const newTiles = [];
|
||||
|
||||
for (let y = 0; y < newH; y++) {
|
||||
newTiles[y] = [];
|
||||
for (let x = 0; x < newW; x++) {
|
||||
newTiles[y][x] = tiles[y]?.[x] ?? 1;
|
||||
}
|
||||
}
|
||||
|
||||
tiles = newTiles;
|
||||
cols = newW;
|
||||
rows = newH;
|
||||
|
||||
render();
|
||||
};
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// Canvas Click
|
||||
// -------------------------------------------------------------
|
||||
canvas.addEventListener("contextmenu", e => e.preventDefault());
|
||||
|
||||
/*
|
||||
canvas.addEventListener("mousedown", e => {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const mx = e.clientX - rect.left;
|
||||
const my = e.clientY - rect.top;
|
||||
|
||||
const x = Math.floor(mx / tileSize);
|
||||
const y = Math.floor(my / tileSize);
|
||||
|
||||
if (x < 0 || y < 0 || x >= cols || y >= rows) return;
|
||||
|
||||
const mode = modeSelect.value;
|
||||
|
||||
// TILES
|
||||
if (mode === "tile") {
|
||||
if (e.button === 0) tiles[y][x] = selectedTile;
|
||||
if (e.button === 2) tiles[y][x] = 0;
|
||||
}
|
||||
if (mode === "shop") {
|
||||
map.objects.push({ type: "shop", x: tx, y: ty });
|
||||
saveMap();
|
||||
}
|
||||
|
||||
|
||||
// SPAWN
|
||||
if (mode === "spawn") {
|
||||
spawn.x = x * tileSize;
|
||||
spawn.y = y * tileSize;
|
||||
}
|
||||
|
||||
// DOORS
|
||||
if (mode === "door") {
|
||||
if (e.button === 0) {
|
||||
doors.push({
|
||||
x: x * tileSize,
|
||||
y: y * tileSize,
|
||||
targetMap: doorTargetMap.value,
|
||||
targetX: parseInt(doorTargetX.value),
|
||||
targetY: parseInt(doorTargetY.value)
|
||||
});
|
||||
}
|
||||
if (e.button === 2) {
|
||||
doors = doors.filter(d => !(d.x === x * tileSize && d.y === y * tileSize));
|
||||
}
|
||||
}
|
||||
|
||||
// OBJECTS
|
||||
if (mode === "object") {
|
||||
const ox = x * tileSize;
|
||||
const oy = y * tileSize;
|
||||
|
||||
if (e.button === 0) {
|
||||
if (!selectedObjectType) return;
|
||||
objects.push({
|
||||
type: selectedObjectType,
|
||||
x: ox,
|
||||
y: oy
|
||||
});
|
||||
}
|
||||
|
||||
if (e.button === 2) {
|
||||
objects = objects.filter(o => !(o.x === ox && o.y === oy));
|
||||
}
|
||||
}
|
||||
|
||||
// SHOPS
|
||||
if (mode === "shop") {
|
||||
const tileX = Math.floor(mx / tileSize);
|
||||
const tileY = Math.floor(my / tileSize);
|
||||
|
||||
// Wenn Linksklick auf existierenden Shop → Editor öffnen
|
||||
if (e.button === 0) {
|
||||
const existing = shops.find(s => s.x === tileX && s.y === tileY);
|
||||
if (existing) {
|
||||
openShopEditor(existing);
|
||||
return;
|
||||
}
|
||||
|
||||
// neuen Shop setzen
|
||||
shops.push({
|
||||
id: "shop_" + Date.now(),
|
||||
x: tileX,
|
||||
y: tileY,
|
||||
items: []
|
||||
});
|
||||
}
|
||||
|
||||
// Shop löschen
|
||||
if (e.button === 2) {
|
||||
shops = shops.filter(s => !(s.x === tileX && s.y === tileY));
|
||||
|
||||
if (currentShop && currentShop.x === tileX && currentShop.y === tileY) {
|
||||
currentShop = null;
|
||||
document.getElementById("shopEditor").style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (mode === "atm") {
|
||||
map.atms.push({
|
||||
id: "atm_" + Date.now(),
|
||||
x: tileX,
|
||||
y: tileY
|
||||
});
|
||||
saveMap();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
render();
|
||||
});
|
||||
*/
|
||||
|
||||
canvas.addEventListener("mousedown", e => {
|
||||
e.preventDefault();
|
||||
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const mx = e.clientX - rect.left;
|
||||
const my = e.clientY - rect.top;
|
||||
|
||||
const tileX = Math.floor(mx / tileSize);
|
||||
const tileY = Math.floor(my / tileSize);
|
||||
|
||||
if (tileX < 0 || tileY < 0 || tileX >= cols || tileY >= rows) return;
|
||||
|
||||
const mode = modeSelect.value;
|
||||
|
||||
// TILES
|
||||
if (mode === "tile") {
|
||||
if (e.button === 0) tiles[tileY][tileX] = selectedTile;
|
||||
if (e.button === 2) tiles[tileY][tileX] = 0;
|
||||
render();
|
||||
return;
|
||||
}
|
||||
|
||||
// SPAWN
|
||||
if (mode === "spawn") {
|
||||
if (e.button === 0) {
|
||||
spawn.x = tileX * tileSize;
|
||||
spawn.y = tileY * tileSize;
|
||||
//saveMap();
|
||||
render();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// DOORS
|
||||
if (mode === "door") {
|
||||
if (e.button === 0) {
|
||||
doors.push({
|
||||
x: tileX * tileSize,
|
||||
y: tileY * tileSize,
|
||||
targetMap: doorTargetMap.value,
|
||||
targetX: parseInt(doorTargetX.value),
|
||||
targetY: parseInt(doorTargetY.value)
|
||||
});
|
||||
//saveMap();
|
||||
render();
|
||||
}
|
||||
if (e.button === 2) {
|
||||
doors = doors.filter(d => !(d.x === tileX * tileSize && d.y === tileY * tileSize));
|
||||
//saveMap();
|
||||
render();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// OBJECTS
|
||||
if (mode === "object") {
|
||||
const ox = tileX * tileSize;
|
||||
const oy = tileY * tileSize;
|
||||
|
||||
if (e.button === 0) {
|
||||
if (!selectedObjectType) return;
|
||||
objects.push({ type: selectedObjectType, x: ox, y: oy });
|
||||
//saveMap();
|
||||
render();
|
||||
}
|
||||
|
||||
if (e.button === 2) {
|
||||
objects = objects.filter(o => !(o.x === ox && o.y === oy));
|
||||
//saveMap();
|
||||
render();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// SHOPS
|
||||
if (mode === "shop") {
|
||||
if (e.button === 0) {
|
||||
const name = prompt("Shop-Name:");
|
||||
if (!name) return;
|
||||
|
||||
fetch("/api/admin/shops", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name, world: mapNameInput.value, x: tileX, y: tileY })
|
||||
}).then(() => alert("Shop angelegt – Items im Admin-Panel zuweisen"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
/*if (mode === "shop") {
|
||||
if (e.button === 0) {
|
||||
const existing = shops.find(s => s.x === tileX && s.y === tileY);
|
||||
if (existing) {
|
||||
openShopEditor(existing);
|
||||
return;
|
||||
}
|
||||
shops.push({
|
||||
id: "shop_" + Date.now(),
|
||||
x: tileX,
|
||||
y: tileY,
|
||||
items: []
|
||||
});
|
||||
//saveMap();
|
||||
render();
|
||||
}
|
||||
|
||||
if (e.button === 2) {
|
||||
shops = shops.filter(s => !(s.x === tileX && s.y === tileY));
|
||||
//saveMap();
|
||||
render();
|
||||
}
|
||||
return;
|
||||
}*/
|
||||
|
||||
// ATMS (wie Shops)
|
||||
if (mode === "atm") {
|
||||
if (e.button === 0) {
|
||||
const existing = atms.find(a => a.x === tileX && a.y === tileY);
|
||||
if (existing) return;
|
||||
|
||||
atms.push({
|
||||
id: "atm_" + Date.now(),
|
||||
x: tileX,
|
||||
y: tileY
|
||||
});
|
||||
|
||||
//saveMap();
|
||||
render();
|
||||
}
|
||||
|
||||
if (e.button === 2) {
|
||||
atms = atms.filter(a => !(a.x === tileX && a.y === tileY));
|
||||
//saveMap();
|
||||
render();
|
||||
}
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// Render Shops
|
||||
// -------------------------------------------------------------
|
||||
function renderShops() {
|
||||
shops.forEach(s => {
|
||||
ctx.fillStyle = "yellow";
|
||||
ctx.fillRect(s.x * tileSize, s.y * tileSize, tileSize, tileSize);
|
||||
ctx.strokeStyle = "black";
|
||||
ctx.strokeRect(s.x * tileSize, s.y * tileSize, tileSize, tileSize);
|
||||
});
|
||||
}
|
||||
|
||||
function renderATMs() {
|
||||
atms.forEach(a => {
|
||||
const px = a.x * tileSize;
|
||||
const py = a.y * tileSize;
|
||||
|
||||
ctx.fillStyle = "blue";
|
||||
ctx.fillRect(px, py, tileSize, tileSize);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// shop
|
||||
// -------------------------------------------------------------
|
||||
function openShopEditor(shop) {
|
||||
currentShop = shop;
|
||||
|
||||
const editor = document.getElementById("shopEditor");
|
||||
const info = document.getElementById("shopInfo");
|
||||
|
||||
editor.style.display = "block";
|
||||
info.innerHTML = `ID: ${shop.id}<br>Position: (${shop.x}, ${shop.y})`;
|
||||
|
||||
renderShopItemList();
|
||||
}
|
||||
|
||||
function renderShopItemList() {
|
||||
const list = document.getElementById("shopItemList");
|
||||
list.innerHTML = "";
|
||||
|
||||
if (!currentShop) return;
|
||||
|
||||
currentShop.items.forEach(item => {
|
||||
const div = document.createElement("div");
|
||||
div.innerHTML = `
|
||||
${item.name} (${item.price}$)
|
||||
<button onclick="removeShopItem('${item.id}')">X</button>
|
||||
`;
|
||||
list.appendChild(div);
|
||||
});
|
||||
}
|
||||
|
||||
function addShopItem() {
|
||||
if (!currentShop) return;
|
||||
|
||||
const id = prompt("Item ID:");
|
||||
if (!id) return;
|
||||
|
||||
const name = prompt("Item Name:");
|
||||
if (!name) return;
|
||||
|
||||
const priceStr = prompt("Preis:");
|
||||
const price = parseInt(priceStr, 10);
|
||||
if (isNaN(price)) return;
|
||||
|
||||
currentShop.items.push({ id, name, price });
|
||||
renderShopItemList();
|
||||
}
|
||||
|
||||
function removeShopItem(id) {
|
||||
if (!currentShop) return;
|
||||
|
||||
currentShop.items = currentShop.items.filter(i => i.id !== id);
|
||||
renderShopItemList();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// Render
|
||||
// -------------------------------------------------------------
|
||||
function render() {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Tiles
|
||||
for (let y = 0; y < rows; y++) {
|
||||
for (let x = 0; x < cols; x++) {
|
||||
const id = tiles[y][x];
|
||||
const tile = tileConfig[id];
|
||||
|
||||
ctx.fillStyle = tile ? tile.color : "#000";
|
||||
ctx.fillRect(x * tileSize, y * tileSize, tileSize, tileSize);
|
||||
|
||||
ctx.strokeStyle = "#333";
|
||||
ctx.strokeRect(x * tileSize, y * tileSize, tileSize, tileSize);
|
||||
}
|
||||
}
|
||||
|
||||
// Spawn
|
||||
ctx.fillStyle = "yellow";
|
||||
ctx.fillRect(spawn.x, spawn.y, tileSize, tileSize);
|
||||
|
||||
// Doors
|
||||
ctx.fillStyle = "orange";
|
||||
doors.forEach(d => {
|
||||
ctx.fillRect(d.x, d.y, tileSize, tileSize);
|
||||
});
|
||||
|
||||
// Objects
|
||||
objects.forEach(o => {
|
||||
const cfg = objectConfig[o.type];
|
||||
if (!cfg) return;
|
||||
|
||||
ctx.fillStyle = cfg.color;
|
||||
ctx.fillRect(
|
||||
o.x,
|
||||
o.y - (cfg.height - tileSize),
|
||||
cfg.width,
|
||||
cfg.height
|
||||
);
|
||||
});
|
||||
|
||||
// Shops
|
||||
renderShops();
|
||||
renderATMs();
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,15 @@
|
||||
mail.borderville.de {
|
||||
reverse_proxy 127.0.0.1:8080
|
||||
}
|
||||
|
||||
borderville.de {
|
||||
reverse_proxy 127.0.0.1:3000
|
||||
}
|
||||
|
||||
game.borderville.de {
|
||||
reverse_proxy 127.0.0.1:3000
|
||||
}
|
||||
|
||||
panel.borderville.de {
|
||||
reverse_proxy 127.0.0.1:3001
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,275 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Bestenliste</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
background: #111;
|
||||
color: #eee;
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 20px;
|
||||
}
|
||||
h1 { margin-top: 0; }
|
||||
a.back { color: #6fbf73; text-decoration: none; font-size: 14px; }
|
||||
a.back:hover { text-decoration: underline; }
|
||||
|
||||
main { max-width: 900px; margin: 20px auto 0; }
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
}
|
||||
.panel h2 { margin-top: 0; font-size: 16px; display: flex; align-items: center; gap: 6px; }
|
||||
|
||||
ol { margin: 0; padding-left: 22px; }
|
||||
li {
|
||||
padding: 6px 0;
|
||||
border-bottom: 1px solid #2a2a2a;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 14px;
|
||||
}
|
||||
li:last-child { border-bottom: none; }
|
||||
li .value { color: #f5d90a; font-weight: bold; }
|
||||
li:first-child { color: #f5d90a; }
|
||||
|
||||
.empty-hint { color: #666; font-size: 13px; padding: 6px 0; }
|
||||
|
||||
.ach-search { display: flex; gap: 8px; margin-bottom: 12px; }
|
||||
.ach-search input {
|
||||
flex: 1;
|
||||
background: #222;
|
||||
border: 1px solid #444;
|
||||
color: #eee;
|
||||
padding: 8px 10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.ach-search button {
|
||||
background: #2c7a3d;
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 8px 14px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ach-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #2a2a2a;
|
||||
font-size: 14px;
|
||||
}
|
||||
.ach-item:last-child { border-bottom: none; }
|
||||
.ach-item.locked { opacity: 0.4; }
|
||||
.ach-name { font-weight: bold; }
|
||||
.ach-desc { color: #999; font-size: 12px; }
|
||||
.ach-xp { color: #f5d90a; font-size: 12px; white-space: nowrap; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<a class="back" href="/index.html">← zurück zur Startseite</a>
|
||||
<h1>🏆 Bestenliste</h1>
|
||||
|
||||
<main>
|
||||
<div class="grid">
|
||||
<div class="panel">
|
||||
<h2>💰 Reichste Spieler</h2>
|
||||
<ol id="listRichest"></ol>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<h2>⭐ Höchstes Level</h2>
|
||||
<ol id="listLevels"></ol>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<h2>📦 Meiste Lieferungen</h2>
|
||||
<ol id="listDeliveries"></ol>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<h2>🚨 Meiste Raubüberfälle</h2>
|
||||
<ol id="listRobberies"></ol>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<h2>⏱️ Meiste Spielzeit</h2>
|
||||
<ol id="listPlaytime"></ol>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h2>🏅 Achievements nachschauen</h2>
|
||||
<div class="ach-search">
|
||||
<input type="text" id="achUsername" placeholder="Benutzername eingeben...">
|
||||
<button onclick="loadAchievements()">Anzeigen</button>
|
||||
</div>
|
||||
<div id="achList"></div>
|
||||
</div>
|
||||
|
||||
<div class="panel" id="myTitlesPanel" style="display:none;">
|
||||
<h2>🎖️ Mein Titel</h2>
|
||||
<p style="color:#999; font-size:13px; margin-top:0;">Wird neben deinem Namen im Spiel angezeigt.</p>
|
||||
<div id="myTitlesList"></div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
const token = localStorage.getItem("token");
|
||||
|
||||
async function authFetch(url, options = {}) {
|
||||
if (token) options.headers = { ...(options.headers || {}), "Authorization": "Bearer " + token };
|
||||
return fetch(url, options);
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
const div = document.createElement("div");
|
||||
div.textContent = str;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
async function loadLeaderboard() {
|
||||
const res = await authFetch("/api/leaderboard");
|
||||
const data = await res.json();
|
||||
|
||||
renderList("listRichest", data.richest || [], p => `${p.total}$`);
|
||||
renderList("listLevels", data.levels || [], p => `Lvl ${p.level}`);
|
||||
renderList("listDeliveries", data.deliveries || [], p => `${p.delivery_count}`);
|
||||
renderList("listRobberies", data.robberies || [], p => `${p.robbery_count}`);
|
||||
renderList("listPlaytime", data.playtime || [], p => formatPlaytime(p.playtime_seconds));
|
||||
}
|
||||
|
||||
function formatPlaytime(seconds) {
|
||||
const h = Math.floor(seconds / 3600);
|
||||
const m = Math.floor((seconds % 3600) / 60);
|
||||
return `${h}h ${m}min`;
|
||||
}
|
||||
|
||||
function renderList(elId, items, valueFn) {
|
||||
const el = document.getElementById(elId);
|
||||
el.innerHTML = "";
|
||||
|
||||
if (items.length === 0) {
|
||||
el.innerHTML = `<div class="empty-hint">Noch keine Daten.</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
items.forEach(item => {
|
||||
const li = document.createElement("li");
|
||||
li.innerHTML = `<span>${escapeHtml(item.username)}</span><span class="value">${valueFn(item)}</span>`;
|
||||
el.appendChild(li);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadAchievements() {
|
||||
const username = document.getElementById("achUsername").value.trim();
|
||||
if (!username) return;
|
||||
|
||||
const res = await authFetch("/api/achievements/" + encodeURIComponent(username));
|
||||
const data = await res.json();
|
||||
const list = document.getElementById("achList");
|
||||
|
||||
if (!data.ok) {
|
||||
list.innerHTML = `<div class="empty-hint">${escapeHtml(data.error || "Fehler")}</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
const unlockedKeys = new Set(data.unlocked.map(a => a.key_name));
|
||||
list.innerHTML = "";
|
||||
|
||||
data.all.forEach(ach => {
|
||||
const unlocked = unlockedKeys.has(ach.key_name);
|
||||
const div = document.createElement("div");
|
||||
div.className = "ach-item" + (unlocked ? "" : " locked");
|
||||
div.innerHTML = `
|
||||
<div>
|
||||
<div class="ach-name">${unlocked ? "🏆" : "🔒"} ${escapeHtml(ach.name)}</div>
|
||||
<div class="ach-desc">${escapeHtml(ach.description)}</div>
|
||||
</div>
|
||||
<div class="ach-xp">+${ach.xp_reward} XP</div>
|
||||
`;
|
||||
list.appendChild(div);
|
||||
});
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// MEIN TITEL
|
||||
// -------------------------------------------------------------
|
||||
async function loadMyTitles() {
|
||||
try {
|
||||
const res = await authFetch("/api/me/titles");
|
||||
const data = await res.json();
|
||||
if (!data.ok) return;
|
||||
|
||||
document.getElementById("myTitlesPanel").style.display = "block";
|
||||
renderMyTitles(data.titles || [], data.active);
|
||||
} catch {
|
||||
// nicht eingeloggt - Panel bleibt versteckt
|
||||
}
|
||||
}
|
||||
|
||||
function renderMyTitles(titles, activeId) {
|
||||
const list = document.getElementById("myTitlesList");
|
||||
list.innerHTML = "";
|
||||
|
||||
const noneRow = document.createElement("div");
|
||||
noneRow.className = "ach-item";
|
||||
noneRow.innerHTML = `
|
||||
<div><div class="ach-name">Kein Titel</div></div>
|
||||
<button onclick="setMyTitle(null)" style="${activeId ? "" : "background:#555;"}">${activeId ? "Wählen" : "Aktiv"}</button>
|
||||
`;
|
||||
list.appendChild(noneRow);
|
||||
|
||||
if (titles.length === 0) {
|
||||
const hint = document.createElement("div");
|
||||
hint.className = "empty-hint";
|
||||
hint.textContent = "Noch keine Titel freigeschaltet - erledige Achievements, um welche zu bekommen.";
|
||||
list.appendChild(hint);
|
||||
return;
|
||||
}
|
||||
|
||||
titles.forEach(t => {
|
||||
const isActive = t.id === activeId;
|
||||
const div = document.createElement("div");
|
||||
div.className = "ach-item";
|
||||
div.innerHTML = `
|
||||
<div>
|
||||
<div class="ach-name">${escapeHtml(t.title_text)}</div>
|
||||
<div class="ach-desc">von: ${escapeHtml(t.name)}</div>
|
||||
</div>
|
||||
<button onclick="setMyTitle(${t.id})" style="${isActive ? "background:#555;" : ""}">${isActive ? "Aktiv" : "Wählen"}</button>
|
||||
`;
|
||||
list.appendChild(div);
|
||||
});
|
||||
}
|
||||
|
||||
async function setMyTitle(achievementId) {
|
||||
const res = await authFetch("/api/me/title", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ achievementId })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.ok) {
|
||||
loadMyTitles();
|
||||
} else {
|
||||
alert(data.error || "Fehler beim Setzen des Titels.");
|
||||
}
|
||||
}
|
||||
|
||||
loadLeaderboard();
|
||||
loadMyTitles();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"sedan": {
|
||||
"width": 40,
|
||||
"height": 22,
|
||||
"color": "#c0392b",
|
||||
"maxSpeed": 6,
|
||||
"accel": 0.15,
|
||||
"brake": 0.3,
|
||||
"friction": 0.05,
|
||||
"turnSpeed": 0.045
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Datenschutz</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
background: #111;
|
||||
color: #eee;
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 20px;
|
||||
}
|
||||
main { max-width: 700px; margin: 0 auto; }
|
||||
a.back { color: #6fbf73; text-decoration: none; font-size: 14px; }
|
||||
a.back:hover { text-decoration: underline; }
|
||||
h1 { margin-top: 20px; }
|
||||
h2 { border-bottom: 2px solid #333; padding-bottom: 6px; margin-top: 30px; }
|
||||
.placeholder { color: #e0a02c; }
|
||||
p, li { line-height: 1.6; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<a class="back" href="/index.html">← zurück zur Startseite</a>
|
||||
|
||||
<main>
|
||||
<h1>Datenschutzerklärung</h1>
|
||||
|
||||
<h2>Verantwortlicher</h2>
|
||||
<p>
|
||||
<span class="placeholder">[Vor- und Nachname / Firmenname]</span><br>
|
||||
<span class="placeholder">[Straße und Hausnummer]</span><br>
|
||||
<span class="placeholder">[PLZ und Ort]</span><br>
|
||||
E-Mail: <span class="placeholder">[deine E-Mail-Adresse]</span>
|
||||
</p>
|
||||
|
||||
<h2>Welche Daten wir verarbeiten</h2>
|
||||
<ul>
|
||||
<li>Account-Daten: Benutzername, Passwort (verschlüsselt gespeichert)</li>
|
||||
<li>Spielstand-Daten: Position, Inventar, Geld, Fahrzeuge, Häuser und ähnliche spielrelevante Daten</li>
|
||||
<li>Chat-Nachrichten, die du im Spiel schreibst</li>
|
||||
<li>Freiwillig eingereichte Inhalte: Wünsche, Anmerkungen</li>
|
||||
<li>Technische Daten: IP-Adresse beim Verbindungsaufbau (serverseitig, für Betrieb/Sicherheit)</li>
|
||||
</ul>
|
||||
|
||||
<h2>Zweck der Verarbeitung</h2>
|
||||
<p>
|
||||
Die genannten Daten werden ausschließlich zum Betrieb dieses Multiplayer-Spiels verarbeitet -
|
||||
insbesondere zur Bereitstellung deines Accounts, deines Spielstands und der Kommunikation
|
||||
zwischen Spielern (Chat).
|
||||
</p>
|
||||
|
||||
<h2>Speicherdauer</h2>
|
||||
<p>
|
||||
Deine Daten werden gespeichert, solange dein Account besteht. Bei Löschung deines Accounts
|
||||
<span class="placeholder">[hier beschreiben, was tatsächlich passiert - werden Daten gelöscht/anonymisiert?]</span>.
|
||||
</p>
|
||||
|
||||
<h2>Weitergabe an Dritte</h2>
|
||||
<p>
|
||||
Es findet <span class="placeholder">[grundsätzlich keine / im Fall von ... eine]</span> Weitergabe
|
||||
deiner Daten an Dritte statt.
|
||||
</p>
|
||||
|
||||
<h2>Deine Rechte</h2>
|
||||
<p>
|
||||
Du hast das Recht auf Auskunft, Berichtigung, Löschung und Einschränkung der Verarbeitung
|
||||
deiner personenbezogenen Daten. Wende dich dazu an: <span class="placeholder">[deine E-Mail-Adresse]</span>.
|
||||
</p>
|
||||
|
||||
<h2>Cookies / Local Storage</h2>
|
||||
<p>
|
||||
Zur Anmeldung wird ein Login-Token im <em>localStorage</em> deines Browsers gespeichert.
|
||||
Dieser verbleibt lokal auf deinem Gerät und wird nicht an Dritte übermittelt.
|
||||
</p>
|
||||
|
||||
<p style="color:#888; font-size:13px; margin-top:40px;">
|
||||
⚠️ Diese Seite ist eine Vorlage und ersetzt keine Rechtsberatung. Bitte alle
|
||||
<span class="placeholder">[orange markierten]</span> Platzhalter durch deine echten Angaben ersetzen
|
||||
und bei Bedarf rechtlich prüfen lassen, bevor die Seite produktiv genutzt wird - insbesondere wenn
|
||||
du personenbezogene Daten von Nutzern innerhalb der EU verarbeitest (DSGVO).
|
||||
</p>
|
||||
</main>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,10 @@
|
||||
// ecosystem.config.js
|
||||
module.exports = {
|
||||
apps: [{
|
||||
name: "gtagame",
|
||||
script: "server.js",
|
||||
env: {
|
||||
JWT_SECRET: "db134e59078109441389a6af0a10ab942a625c409ba7486610e7da29c97e2468213d74b22651b3d471714addb504382d"
|
||||
}
|
||||
}]
|
||||
};
|
||||
+2297
File diff suppressed because it is too large
Load Diff
+4442
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,66 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Impressum</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
background: #111;
|
||||
color: #eee;
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 20px;
|
||||
}
|
||||
main { max-width: 700px; margin: 0 auto; }
|
||||
a.back { color: #6fbf73; text-decoration: none; font-size: 14px; }
|
||||
a.back:hover { text-decoration: underline; }
|
||||
h1 { margin-top: 20px; }
|
||||
h2 { border-bottom: 2px solid #333; padding-bottom: 6px; margin-top: 30px; }
|
||||
.placeholder { color: #e0a02c; }
|
||||
p { line-height: 1.6; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<a class="back" href="/index.html">← zurück zur Startseite</a>
|
||||
|
||||
<main>
|
||||
<h1>Impressum</h1>
|
||||
|
||||
<h2>Angaben gemäß § 5 TMG</h2>
|
||||
<p>
|
||||
<span class="placeholder">[Vor- und Nachname / Firmenname]</span><br>
|
||||
<span class="placeholder">[Straße und Hausnummer]</span><br>
|
||||
<span class="placeholder">[PLZ und Ort]</span><br>
|
||||
<span class="placeholder">[Land]</span>
|
||||
</p>
|
||||
|
||||
<h2>Kontakt</h2>
|
||||
<p>
|
||||
E-Mail: <span class="placeholder">[deine E-Mail-Adresse]</span>
|
||||
</p>
|
||||
|
||||
<h2>Verantwortlich für den Inhalt nach § 55 Abs. 2 RStV</h2>
|
||||
<p>
|
||||
<span class="placeholder">[Vor- und Nachname]</span><br>
|
||||
<span class="placeholder">[Anschrift wie oben]</span>
|
||||
</p>
|
||||
|
||||
<h2>Haftungsausschluss</h2>
|
||||
<p>
|
||||
Die Inhalte dieses Projekts wurden mit größtmöglicher Sorgfalt erstellt. Für die Richtigkeit,
|
||||
Vollständigkeit und Aktualität der Inhalte kann jedoch keine Gewähr übernommen werden.
|
||||
Dies ist ein privates/nicht-kommerzielles Hobbyprojekt <span class="placeholder">[ggf. anpassen, falls kommerziell]</span>.
|
||||
</p>
|
||||
|
||||
<p style="color:#888; font-size:13px; margin-top:40px;">
|
||||
⚠️ Diese Seite ist eine Vorlage. Bitte alle <span class="placeholder">[orange markierten]</span> Platzhalter
|
||||
durch deine echten Angaben ersetzen, bevor die Seite für andere Spieler sichtbar/produktiv genutzt wird.
|
||||
Falls du Fragen hast, was ins Impressum gehört (z.B. abhängig davon ob es kommerziell/privat ist,
|
||||
in welchem Land du bist), lohnt sich eine kurze Recherche oder Rechtsberatung - ich kann dir dazu
|
||||
keine rechtsverbindliche Auskunft geben.
|
||||
</p>
|
||||
</main>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
+589
@@ -0,0 +1,589 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Startseite</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
background: #111;
|
||||
color: #eee;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
|
||||
header {
|
||||
background: #1a1a1a;
|
||||
border-bottom: 2px solid #333;
|
||||
padding: 16px 24px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
header h1 { margin: 0; font-size: 22px; }
|
||||
|
||||
#topNav {
|
||||
background: #161616;
|
||||
border-bottom: 1px solid #2a2a2a;
|
||||
padding: 10px 24px;
|
||||
display: flex;
|
||||
gap: 18px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
#topNav a {
|
||||
color: #6fbf73;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
}
|
||||
#topNav a:hover { text-decoration: underline; }
|
||||
|
||||
#loginArea { display: flex; gap: 8px; align-items: center; }
|
||||
#loggedInArea { display: none; align-items: center; gap: 12px; }
|
||||
|
||||
input {
|
||||
background: #222;
|
||||
border: 1px solid #444;
|
||||
color: #eee;
|
||||
padding: 8px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
button {
|
||||
background: #2c7a3d;
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 8px 14px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
button.secondary { background: #333; }
|
||||
button.danger { background: #a83232; }
|
||||
button:hover { opacity: 0.85; }
|
||||
|
||||
main {
|
||||
max-width: 900px;
|
||||
margin: 30px auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.play-banner {
|
||||
background: linear-gradient(135deg, #1f3d2b, #142218);
|
||||
border: 1px solid #2c7a3d;
|
||||
border-radius: 10px;
|
||||
padding: 24px;
|
||||
margin-bottom: 30px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.play-banner h2 { margin: 0 0 6px 0; }
|
||||
.play-banner p { margin: 0; color: #aaa; }
|
||||
|
||||
section.panel {
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
section.panel h2 {
|
||||
margin-top: 0;
|
||||
border-bottom: 2px solid #333;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.news-columns { display: flex; gap: 24px; align-items: flex-start; }
|
||||
.news-columns section.panel { flex: 1; min-width: 0; }
|
||||
@media (max-width: 800px) {
|
||||
.news-columns { flex-direction: column; }
|
||||
}
|
||||
|
||||
.news-item {
|
||||
border-bottom: 1px solid #2a2a2a;
|
||||
padding: 14px 0;
|
||||
}
|
||||
.news-item:last-child { border-bottom: none; }
|
||||
.news-item h3 { margin: 0 0 4px 0; }
|
||||
.news-meta { color: #777; font-size: 12px; margin-bottom: 8px; }
|
||||
.news-content { white-space: pre-wrap; line-height: 1.5; }
|
||||
.badge {
|
||||
display: inline-block;
|
||||
background: #2c7a3d;
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
padding: 2px 8px;
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
margin-left: 6px;
|
||||
}
|
||||
.news-item .danger { margin-top: 8px; font-size: 12px; padding: 4px 10px; }
|
||||
|
||||
#newsEmpty { color: #666; }
|
||||
|
||||
.admin-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
.admin-card {
|
||||
background: #202020;
|
||||
border: 1px solid #333;
|
||||
border-radius: 8px;
|
||||
padding: 18px;
|
||||
text-decoration: none;
|
||||
color: #eee;
|
||||
display: block;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
.admin-card:hover { border-color: #2c7a3d; }
|
||||
.admin-card .icon { font-size: 26px; margin-bottom: 8px; }
|
||||
.admin-card h3 { margin: 0 0 4px 0; font-size: 16px; }
|
||||
.admin-card p { margin: 0; color: #999; font-size: 13px; }
|
||||
|
||||
.form-row { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 10px; }
|
||||
textarea {
|
||||
width: 100%;
|
||||
min-height: 100px;
|
||||
background: #222;
|
||||
border: 1px solid #444;
|
||||
color: #eee;
|
||||
padding: 8px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
font-family: inherit;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.msg { font-size: 13px; color: #6fbf73; min-height: 18px; margin-top: 6px; }
|
||||
.hidden { display: none !important; }
|
||||
|
||||
#registerOverlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0,0,0,0.7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 20000;
|
||||
}
|
||||
#registerBox {
|
||||
background: #1a1a1a;
|
||||
border: 2px solid #444;
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
width: 320px;
|
||||
}
|
||||
#registerBox input {
|
||||
width: 100%;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
#registerBox .form-row {
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header>
|
||||
<h1>🎮 <span id="siteTitleText">Mein Multiplayer-Spiel</span> <span id="onlineCount" style="font-size:14px; color:#6fbf73; font-weight:normal; margin-left:12px;"></span></h1>
|
||||
|
||||
<div id="loginArea">
|
||||
<input type="text" id="loginUser" placeholder="Benutzername">
|
||||
<input type="password" id="loginPass" placeholder="Passwort">
|
||||
<button onclick="doLogin()">Login</button>
|
||||
<button class="secondary" onclick="showRegister()">Registrieren</button>
|
||||
</div>
|
||||
|
||||
<div id="loggedInArea">
|
||||
<span id="welcomeText"></span>
|
||||
<a href="/profile.html" style="color:#6fbf73; text-decoration:none; font-size:14px;">👤 Profil</a>
|
||||
<button class="secondary" onclick="doLogout()">Logout</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<nav id="topNav">
|
||||
<a href="/uebersicht.html">📂 Übersicht</a>
|
||||
<a href="/wuensche.html">Wünsche</a>
|
||||
<a href="/umfragen.html">Umfragen</a>
|
||||
<a href="/bestenliste.html">Bestenliste</a>
|
||||
<a href="/auktionshaus.html">Auktionshaus</a>
|
||||
<a href="/anmerkungen.html">Anmerkungen</a>
|
||||
</nav>
|
||||
|
||||
<div id="registerOverlay" class="hidden">
|
||||
<div id="registerBox">
|
||||
<h2 style="margin-top:0;">Registrieren</h2>
|
||||
<input type="text" id="regUser" placeholder="Benutzername">
|
||||
<input type="password" id="regPass" placeholder="Passwort">
|
||||
<input type="password" id="regPassConfirm" placeholder="Passwort wiederholen">
|
||||
<p style="color:#888; font-size:12px; margin-bottom:4px;">
|
||||
Warum möchtest du bei uns mitspielen? (mind. 20 Zeichen - ein Admin schaut sich das vor der Freischaltung an)
|
||||
</p>
|
||||
<textarea id="regApplicationText" placeholder="Kurz erzählen, wer du bist und warum du mitspielen möchtest..." style="width:100%; box-sizing:border-box; min-height:80px; resize:vertical;"></textarea>
|
||||
<div class="form-row">
|
||||
<button onclick="doRegister()">Account erstellen</button>
|
||||
<button class="secondary" onclick="hideRegister()">Abbrechen</button>
|
||||
</div>
|
||||
<div class="msg" id="registerMsg"></div>
|
||||
<p style="color:#888; font-size:13px; margin-bottom:0;">
|
||||
Nach der Registrierung muss ein Admin deinen Account erst freischalten, bevor du dich einloggen kannst.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main>
|
||||
|
||||
<div class="play-banner hidden" id="playBanner">
|
||||
<div>
|
||||
<h2>Bereit zum Spielen?</h2>
|
||||
<p>Log dich ein und tauch direkt in die Welt ein.</p>
|
||||
</div>
|
||||
<button onclick="location.href='/game3d.html'">▶ Spiel starten</button>
|
||||
</div>
|
||||
|
||||
<!-- NEWS + CHANGELOG nebeneinander (für alle sichtbar) -->
|
||||
<div class="news-columns">
|
||||
<section class="panel">
|
||||
<h2>📰 News</h2>
|
||||
<div id="newsList"></div>
|
||||
<div id="newsEmpty" class="hidden">Noch keine News vorhanden.</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<h2>🛠️ Changelog</h2>
|
||||
<div id="changelogList"></div>
|
||||
<div id="changelogEmpty" class="hidden">Noch keine Einträge vorhanden.</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<footer style="max-width:900px; margin:0 auto 30px; padding:0 20px; color:#666; font-size:13px; display:flex; justify-content:space-between; flex-wrap:wrap; gap:10px;">
|
||||
<span>Version <span id="footerVersion">-</span></span>
|
||||
<span>
|
||||
<a href="/impressum.html" style="color:#888; text-decoration:none;">Impressum</a> ·
|
||||
<a href="/datenschutz.html" style="color:#888; text-decoration:none;">Datenschutz</a>
|
||||
</span>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
let token = localStorage.getItem("token") || null;
|
||||
let isAdmin = localStorage.getItem("isAdmin") === "true";
|
||||
let username = localStorage.getItem("username") || "";
|
||||
|
||||
function updateAuthUI() {
|
||||
const loginArea = document.getElementById("loginArea");
|
||||
const loggedInArea = document.getElementById("loggedInArea");
|
||||
const playBanner = document.getElementById("playBanner");
|
||||
|
||||
if (token) {
|
||||
loginArea.style.display = "none";
|
||||
loggedInArea.style.display = "flex";
|
||||
document.getElementById("welcomeText").textContent = "Angemeldet als " + username;
|
||||
playBanner.classList.remove("hidden");
|
||||
} else {
|
||||
loginArea.style.display = "flex";
|
||||
loggedInArea.style.display = "none";
|
||||
playBanner.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// REGISTRIERUNG
|
||||
// -------------------------------------------------------------
|
||||
function showRegister() {
|
||||
document.getElementById("registerOverlay").classList.remove("hidden");
|
||||
}
|
||||
|
||||
function hideRegister() {
|
||||
document.getElementById("registerOverlay").classList.add("hidden");
|
||||
document.getElementById("regUser").value = "";
|
||||
document.getElementById("regPass").value = "";
|
||||
document.getElementById("regPassConfirm").value = "";
|
||||
document.getElementById("regApplicationText").value = "";
|
||||
document.getElementById("registerMsg").textContent = "";
|
||||
}
|
||||
|
||||
async function doRegister() {
|
||||
const username = document.getElementById("regUser").value.trim();
|
||||
const password = document.getElementById("regPass").value;
|
||||
const passwordConfirm = document.getElementById("regPassConfirm").value;
|
||||
const applicationText = document.getElementById("regApplicationText").value.trim();
|
||||
|
||||
if (!username || !password) {
|
||||
showRegisterMsg("Benutzername und Passwort sind Pflicht.", true);
|
||||
return;
|
||||
}
|
||||
if (password !== passwordConfirm) {
|
||||
showRegisterMsg("Passwörter stimmen nicht überein.", true);
|
||||
return;
|
||||
}
|
||||
if (password.length < 4) {
|
||||
showRegisterMsg("Passwort muss mind. 4 Zeichen haben.", true);
|
||||
return;
|
||||
}
|
||||
if (applicationText.length < 20) {
|
||||
showRegisterMsg("Bitte kurz begründen, warum du mitspielen möchtest (mind. 20 Zeichen).", true);
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await fetch("/api/register", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ username, password, applicationText })
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (!data.ok) {
|
||||
showRegisterMsg(data.error || "Registrierung fehlgeschlagen.", true);
|
||||
return;
|
||||
}
|
||||
|
||||
showRegisterMsg("Account erstellt! Ein Admin muss dich noch freischalten.");
|
||||
setTimeout(hideRegister, 2500);
|
||||
}
|
||||
|
||||
function showRegisterMsg(text, isError) {
|
||||
const el = document.getElementById("registerMsg");
|
||||
el.style.color = isError ? "#e06c6c" : "#6fbf73";
|
||||
el.textContent = text;
|
||||
}
|
||||
|
||||
async function doLogin() {
|
||||
const user = document.getElementById("loginUser").value;
|
||||
const pass = document.getElementById("loginPass").value;
|
||||
|
||||
if (!user || !pass) return;
|
||||
|
||||
const res = await fetch("/api/login", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ username: user, password: pass })
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (!data.ok) {
|
||||
alert(data.error || "Login fehlgeschlagen");
|
||||
return;
|
||||
}
|
||||
|
||||
token = data.token;
|
||||
isAdmin = !!data.isAdmin;
|
||||
username = data.username;
|
||||
|
||||
localStorage.setItem("token", token);
|
||||
localStorage.setItem("isAdmin", isAdmin ? "true" : "false");
|
||||
localStorage.setItem("username", username);
|
||||
|
||||
updateAuthUI();
|
||||
}
|
||||
|
||||
function doLogout() {
|
||||
token = null;
|
||||
isAdmin = false;
|
||||
username = "";
|
||||
localStorage.removeItem("token");
|
||||
localStorage.removeItem("isAdmin");
|
||||
localStorage.removeItem("username");
|
||||
updateAuthUI();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// NEWS
|
||||
// -------------------------------------------------------------
|
||||
async function loadNews() {
|
||||
const res = await fetch("/api/news");
|
||||
const data = await res.json();
|
||||
renderNews(data.news || []);
|
||||
}
|
||||
|
||||
async function loadChangelog() {
|
||||
const res = await fetch("/api/changelog");
|
||||
const data = await res.json();
|
||||
renderChangelog(data.changelog || []);
|
||||
}
|
||||
|
||||
function renderChangelog(items) {
|
||||
const list = document.getElementById("changelogList");
|
||||
const empty = document.getElementById("changelogEmpty");
|
||||
list.innerHTML = "";
|
||||
|
||||
if (items.length === 0) {
|
||||
empty.classList.remove("hidden");
|
||||
return;
|
||||
}
|
||||
empty.classList.add("hidden");
|
||||
|
||||
items.forEach(c => {
|
||||
const div = document.createElement("div");
|
||||
div.className = "news-item";
|
||||
|
||||
const date = new Date(c.created_at).toLocaleString("de-DE");
|
||||
|
||||
div.innerHTML = `
|
||||
<h3>${escapeHtml(c.title)} <span class="badge">v${escapeHtml(c.version)}</span></h3>
|
||||
<div class="news-meta">${date}</div>
|
||||
<div class="news-content">${escapeHtml(c.content)}</div>
|
||||
`;
|
||||
|
||||
if (isAdmin) {
|
||||
const delBtn = document.createElement("button");
|
||||
delBtn.className = "danger";
|
||||
delBtn.textContent = "Löschen";
|
||||
delBtn.onclick = () => deleteChangelog(c.id);
|
||||
div.appendChild(delBtn);
|
||||
}
|
||||
|
||||
list.appendChild(div);
|
||||
});
|
||||
}
|
||||
|
||||
async function deleteChangelog(id) {
|
||||
if (!confirm("Diesen Changelog-Eintrag wirklich löschen?")) return;
|
||||
|
||||
const res = await fetch("/api/admin/changelog/" + id, {
|
||||
method: "DELETE",
|
||||
headers: { "Authorization": "Bearer " + token }
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) loadChangelog();
|
||||
else alert("Löschen fehlgeschlagen.");
|
||||
}
|
||||
|
||||
async function loadVersion() {
|
||||
try {
|
||||
const res = await fetch("/api/status");
|
||||
const data = await res.json();
|
||||
const el = document.getElementById("footerVersion");
|
||||
if (el && data.version) el.textContent = data.version;
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function renderNews(items) {
|
||||
const list = document.getElementById("newsList");
|
||||
const empty = document.getElementById("newsEmpty");
|
||||
list.innerHTML = "";
|
||||
|
||||
if (items.length === 0) {
|
||||
empty.classList.remove("hidden");
|
||||
return;
|
||||
}
|
||||
empty.classList.add("hidden");
|
||||
|
||||
items.forEach(n => {
|
||||
const div = document.createElement("div");
|
||||
div.className = "news-item";
|
||||
|
||||
const date = new Date(n.created_at).toLocaleString("de-DE");
|
||||
|
||||
div.innerHTML = `
|
||||
<h3>${escapeHtml(n.title)}</h3>
|
||||
<div class="news-meta">${date}${n.author ? " — " + escapeHtml(n.author) : ""}</div>
|
||||
<div class="news-content">${escapeHtml(n.content)}</div>
|
||||
`;
|
||||
|
||||
if (isAdmin) {
|
||||
const delBtn = document.createElement("button");
|
||||
delBtn.className = "danger";
|
||||
delBtn.textContent = "Löschen";
|
||||
delBtn.onclick = () => deleteNews(n.id);
|
||||
div.appendChild(delBtn);
|
||||
}
|
||||
|
||||
list.appendChild(div);
|
||||
});
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
const div = document.createElement("div");
|
||||
div.textContent = str;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
async function deleteNews(id) {
|
||||
if (!confirm("Diese News wirklich löschen?")) return;
|
||||
|
||||
const res = await fetch("/api/admin/news/" + id, {
|
||||
method: "DELETE",
|
||||
headers: { "Authorization": "Bearer " + token }
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
loadNews();
|
||||
} else {
|
||||
alert("Löschen fehlgeschlagen.");
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// ONLINE-ZÄHLER
|
||||
// -------------------------------------------------------------
|
||||
async function loadOnlineCount() {
|
||||
try {
|
||||
const res = await fetch("/api/status");
|
||||
const data = await res.json();
|
||||
const el = document.getElementById("onlineCount");
|
||||
if (el && data.ok) el.textContent = `👥 ${data.online} online`;
|
||||
} catch {
|
||||
// Server evtl. gerade nicht erreichbar - still bleiben, kein Fehler-Popup nötig
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// SEITEN-TITEL (einstellbar über /admin_settings.html o.ä.)
|
||||
// -------------------------------------------------------------
|
||||
async function loadSiteTitle() {
|
||||
try {
|
||||
const res = await fetch("/api/site_config");
|
||||
const data = await res.json();
|
||||
if (data.ok && data.title) {
|
||||
document.title = data.title;
|
||||
const el = document.getElementById("siteTitleText");
|
||||
if (el) el.textContent = data.title;
|
||||
const input = document.getElementById("siteTitleInput");
|
||||
if (input && !input.value) input.value = data.title;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Seiten-Titel konnte nicht geladen werden:", err);
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// INIT
|
||||
// -------------------------------------------------------------
|
||||
updateAuthUI();
|
||||
loadSiteTitle();
|
||||
loadNews();
|
||||
loadChangelog();
|
||||
loadVersion();
|
||||
loadOnlineCount();
|
||||
setInterval(loadOnlineCount, 10000);
|
||||
|
||||
// Hinweis anzeigen, falls man wegen Verbindungsverlust aus dem Spiel hierher geschickt wurde
|
||||
if (new URLSearchParams(location.search).get("disconnected") === "1") {
|
||||
alert("Die Verbindung zum Server wurde unterbrochen. Du wurdest zur Startseite zurückgebracht - logg dich einfach erneut ein.");
|
||||
history.replaceState(null, "", location.pathname);
|
||||
}
|
||||
|
||||
// Hinweis anzeigen, falls ein geplanter Server-Neustart die Verbindung beendet hat
|
||||
if (new URLSearchParams(location.search).get("restarted") === "1") {
|
||||
alert("Der Server wurde für einen Neustart neu gestartet. Warte einen Moment und logg dich dann erneut ein.");
|
||||
history.replaceState(null, "", location.pathname);
|
||||
}
|
||||
|
||||
// Hinweis anzeigen, falls der Zugang zum 3D-Spiel verweigert wurde (kein Beta-Tester)
|
||||
if (new URLSearchParams(location.search).get("betaonly") === "1") {
|
||||
alert("Das 3D-Spiel ist aktuell nur für Beta-Tester zugänglich. Wende dich an einen Admin, falls du Zugang haben möchtest.");
|
||||
history.replaceState(null, "", location.pathname);
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
+318
@@ -0,0 +1,318 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Map Editor</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin: 0; height: 100%; background: #0a0a0a; color: #eee; font-family: Arial, sans-serif; overflow: hidden; }
|
||||
|
||||
#appLayout { display: flex; height: 100vh; width: 100vw; }
|
||||
|
||||
/* ---------- Linke Seite: Kopfzeile + Karte ---------- */
|
||||
#mainArea { flex: 1; display: flex; flex-direction: column; min-width: 0; }
|
||||
|
||||
#topBar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
background: #1a1a1a;
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
a.back { color: #6fbf73; text-decoration: none; font-size: 14px; margin-right: 6px; }
|
||||
a.back:hover { text-decoration: underline; }
|
||||
#topBar input, #topBar select, #topBar button {
|
||||
background: #222; border: 1px solid #444; color: #eee;
|
||||
padding: 6px 8px; border-radius: 4px; font-size: 13px;
|
||||
}
|
||||
#topBar button { background: #2c7a3d; cursor: pointer; border-color: #2c7a3d; }
|
||||
#topBar button:hover { opacity: 0.85; }
|
||||
#camLabel { color: #888; font-size: 12px; margin-left: auto; white-space: nowrap; }
|
||||
|
||||
#canvasWrap { flex: 1; position: relative; background: #000; min-height: 0; }
|
||||
#canvas { display: block; position: absolute; top: 0; left: 0; }
|
||||
|
||||
#hintBar {
|
||||
padding: 4px 12px;
|
||||
background: #161616;
|
||||
color: #777;
|
||||
font-size: 11px;
|
||||
border-top: 1px solid #2a2a2a;
|
||||
}
|
||||
|
||||
/* ---------- Rechte Seite: Sidebar mit Tabs ---------- */
|
||||
#sidebar {
|
||||
width: 360px;
|
||||
flex-shrink: 0;
|
||||
background: #161616;
|
||||
border-left: 1px solid #333;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
#sidebarTabs {
|
||||
display: flex;
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
.sidebar-tab-btn {
|
||||
flex: 1;
|
||||
background: #1a1a1a;
|
||||
border: none;
|
||||
border-right: 1px solid #333;
|
||||
color: #999;
|
||||
padding: 10px 4px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.sidebar-tab-btn:last-child { border-right: none; }
|
||||
.sidebar-tab-btn:hover { background: #222; }
|
||||
.sidebar-tab-btn.active { background: #222; color: #f5d90a; border-bottom: 2px solid #f5d90a; }
|
||||
|
||||
#sidebarBody { flex: 1; overflow-y: auto; padding: 12px; }
|
||||
.sidebar-panel { display: none; }
|
||||
.sidebar-panel.active { display: block; }
|
||||
|
||||
.toolbar-group { margin-bottom: 14px; }
|
||||
.toolbar-group-label {
|
||||
font-size: 10px; color: #777; text-transform: uppercase;
|
||||
letter-spacing: 0.5px; display: block; margin-bottom: 6px;
|
||||
}
|
||||
.toolbar-group-buttons { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
.mode-btn {
|
||||
width: 42px; height: 42px; font-size: 19px;
|
||||
background: #2a2a2a; border: 2px solid #444; border-radius: 6px;
|
||||
cursor: pointer; display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.mode-btn:hover { border-color: #666; }
|
||||
.mode-btn.active { border-color: #f5d90a; background: #3a3520; box-shadow: 0 0 6px rgba(245,217,10,0.5); }
|
||||
|
||||
#layerToggles { display: flex; flex-direction: column; gap: 8px; font-size: 13px; color: #ccc; }
|
||||
#layerToggles label { display: flex; align-items: center; gap: 8px; cursor: pointer; }
|
||||
|
||||
#tilePalette, #objectPalette { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
.tile-swatch {
|
||||
width: 56px; border: 2px solid #000; border-radius: 4px; cursor: pointer;
|
||||
display: flex; flex-direction: column; align-items: center; overflow: hidden;
|
||||
background: #1a1a1a; font-family: Arial, sans-serif;
|
||||
}
|
||||
.tile-swatch .color-box { width: 100%; height: 32px; position: relative; }
|
||||
.tile-swatch .collision-icon { position: absolute; top: 1px; right: 1px; font-size: 9px; text-shadow: 0 0 2px black; }
|
||||
.tile-swatch .tile-label {
|
||||
font-size: 9px; color: #ccc; padding: 2px; text-align: center;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; width: 100%;
|
||||
}
|
||||
.tile-swatch.selected { border-color: #f5d90a; box-shadow: 0 0 6px #f5d90a; }
|
||||
|
||||
.sidebar-section-title { color: #f5d90a; font-size: 13px; margin: 0 0 8px 0; }
|
||||
.sidebar-hint { color: #777; font-size: 11px; margin-bottom: 10px; }
|
||||
|
||||
#shopEditor {
|
||||
position: absolute; right: 10px; top: 60px; width: 200px;
|
||||
background: rgba(20,20,20,0.9); color: white; padding: 10px;
|
||||
border: 2px solid #444; border-radius: 6px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="appLayout">
|
||||
<div id="mainArea">
|
||||
<div id="topBar">
|
||||
<a class="back" href="/index.html">← Startseite</a>
|
||||
<input type="text" id="mapName" placeholder="Map-Name (z.B. stadt)">
|
||||
<select id="mapSelect"><option value="">Map laden...</option></select>
|
||||
<button id="newMapBtn">Neue Map</button>
|
||||
<button id="saveMapBtn">Speichern</button>
|
||||
|
||||
<input type="text" id="doorTargetMap" placeholder="Ziel-Map" style="width:110px;">
|
||||
<input type="number" id="doorTargetX" placeholder="Ziel X" style="width:70px;">
|
||||
<input type="number" id="doorTargetY" placeholder="Ziel Y" style="width:70px;">
|
||||
|
||||
<input type="number" id="mapWidth" placeholder="Breite" style="width:70px;">
|
||||
<input type="number" id="mapHeight" placeholder="Höhe" style="width:70px;">
|
||||
<button id="resizeMapBtn">Größe ändern</button>
|
||||
|
||||
<span id="camLabel"></span>
|
||||
</div>
|
||||
|
||||
<div id="canvasWrap">
|
||||
<canvas id="canvas" width="1500" height="800"></canvas>
|
||||
</div>
|
||||
|
||||
<div id="hintBar">
|
||||
Pfeiltasten oder mittlere Maustaste (gedrückt halten + ziehen) zum Scrollen. Linksklick = setzen, Rechtsklick = löschen (außer Tile-Modus: da malt Rechtsklick "leer"). Werkzeuge und Paletten jetzt rechts →
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="sidebar">
|
||||
<div id="sidebarTabs">
|
||||
<button class="sidebar-tab-btn active" data-sidebar-tab="modes">🛠️ Werkzeuge</button>
|
||||
<button class="sidebar-tab-btn" data-sidebar-tab="layers">👁️ Ebenen</button>
|
||||
<button class="sidebar-tab-btn" data-sidebar-tab="palette">🧱📦 Tiles & Objekte</button>
|
||||
</div>
|
||||
|
||||
<div id="sidebarBody">
|
||||
<!-- WERKZEUGE -->
|
||||
<div class="sidebar-panel active" id="panel-modes">
|
||||
<select id="modeSelect" style="display:none;">
|
||||
<option value="tile">Tiles setzen</option>
|
||||
<option value="rotate">Tile drehen</option>
|
||||
<option value="spawn">Spawn-Punkt setzen</option>
|
||||
<option value="door">Tür/Portal setzen</option>
|
||||
<option value="object">Objekte setzen</option>
|
||||
<option value="shop">Shop setzen</option>
|
||||
<option value="atm">ATM setzen</option>
|
||||
<option value="garage">Garage setzen</option>
|
||||
<option value="jobcenter">Jobcenter setzen</option>
|
||||
<option value="clothingshop">Kleidungsladen setzen</option>
|
||||
<option value="insuranceoffice">Versicherungsbüro setzen</option>
|
||||
<option value="plateoffice">Kfz-Zulassungsstelle setzen</option>
|
||||
<option value="trailershop">Anhänger-Shop setzen</option>
|
||||
<option value="gasstation">Tankstelle setzen</option>
|
||||
<option value="repairshop">Werkstatt setzen</option>
|
||||
<option value="house">Haus setzen</option>
|
||||
<option value="jobpoint">Job-Punkt setzen</option>
|
||||
<option value="taxistand">Taxi-Stand setzen</option>
|
||||
<option value="hospital">Krankenhaus setzen</option>
|
||||
<option value="prison">Gefängnis setzen</option>
|
||||
<option value="zone">Territoriums-Zone setzen</option>
|
||||
<option value="impound">Abschlepphof setzen</option>
|
||||
<option value="firestation">Feuerwache setzen</option>
|
||||
<option value="drug_harvest">Anbaustelle setzen</option>
|
||||
<option value="drug_process">Labor setzen</option>
|
||||
<option value="drug_dealer">Verkaufsort (Pool) setzen</option>
|
||||
</select>
|
||||
|
||||
<div class="toolbar-group">
|
||||
<span class="toolbar-group-label">Terrain</span>
|
||||
<div class="toolbar-group-buttons">
|
||||
<button class="mode-btn" data-mode="tile" title="Tiles setzen">🧱</button>
|
||||
<button class="mode-btn" data-mode="rotate" title="Tile drehen (Linksklick=+90°, Rechtsklick=-90°)">🔄</button>
|
||||
<button class="mode-btn" data-mode="object" title="Objekte setzen">📦</button>
|
||||
<button class="mode-btn" data-mode="spawn" title="Spawn-Punkt setzen">🚩</button>
|
||||
<button class="mode-btn" data-mode="door" title="Tür/Portal setzen">🚪</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="toolbar-group">
|
||||
<span class="toolbar-group-label">Shops & Gebäude</span>
|
||||
<div class="toolbar-group-buttons">
|
||||
<button class="mode-btn" data-mode="shop" title="Shop setzen">🛒</button>
|
||||
<button class="mode-btn" data-mode="atm" title="ATM setzen">🏧</button>
|
||||
<button class="mode-btn" data-mode="garage" title="Garage setzen">🚗</button>
|
||||
<button class="mode-btn" data-mode="gasstation" title="Tankstelle setzen">⛽</button>
|
||||
<button class="mode-btn" data-mode="repairshop" title="Werkstatt setzen">🔧</button>
|
||||
<button class="mode-btn" data-mode="house" title="Haus setzen">🏠</button>
|
||||
<button class="mode-btn" data-mode="jobcenter" title="Jobcenter setzen">💼</button>
|
||||
<button class="mode-btn" data-mode="clothingshop" title="Kleidungsladen setzen">👕</button>
|
||||
<button class="mode-btn" data-mode="insuranceoffice" title="Versicherungsbüro setzen">🛡️</button>
|
||||
<button class="mode-btn" data-mode="plateoffice" title="Kfz-Zulassungsstelle setzen">🔖</button>
|
||||
<button class="mode-btn" data-mode="trailershop" title="Anhänger-Shop setzen">🚛</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="toolbar-group">
|
||||
<span class="toolbar-group-label">Verkehr</span>
|
||||
<div class="toolbar-group-buttons">
|
||||
<button class="mode-btn" data-mode="taxistand" title="Taxi-Stand setzen">🚕</button>
|
||||
<button class="mode-btn" data-mode="jobpoint" title="Job-Punkt setzen">📍</button>
|
||||
<button class="mode-btn" data-mode="impound" title="Abschlepphof setzen">🚛</button>
|
||||
<button class="mode-btn" data-mode="firestation" title="Feuerwache setzen">🚒</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="toolbar-group">
|
||||
<span class="toolbar-group-label">Sicherheit</span>
|
||||
<div class="toolbar-group-buttons">
|
||||
<button class="mode-btn" data-mode="hospital" title="Krankenhaus setzen">🏥</button>
|
||||
<button class="mode-btn" data-mode="prison" title="Gefängnis setzen">🔒</button>
|
||||
<button class="mode-btn" data-mode="zone" title="Territoriums-Zone setzen">🚩</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="toolbar-group">
|
||||
<span class="toolbar-group-label">Drogen</span>
|
||||
<div class="toolbar-group-buttons">
|
||||
<button class="mode-btn" data-mode="drug_harvest" title="Anbaustelle setzen">🌿</button>
|
||||
<button class="mode-btn" data-mode="drug_process" title="Labor setzen">⚗️</button>
|
||||
<button class="mode-btn" data-mode="drug_dealer" title="Verkaufsort (Pool) setzen">💰</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- EBENEN -->
|
||||
<div class="sidebar-panel" id="panel-layers">
|
||||
<h3 class="sidebar-section-title">Sichtbarkeit</h3>
|
||||
<div id="layerToggles">
|
||||
<label><input type="checkbox" class="layer-toggle" data-layer="object" checked> 📦 Objekte</label>
|
||||
<label><input type="checkbox" class="layer-toggle" data-layer="shop" checked> 🛒 Shops</label>
|
||||
<label><input type="checkbox" class="layer-toggle" data-layer="atm" checked> 🏧 ATMs</label>
|
||||
<label><input type="checkbox" class="layer-toggle" data-layer="garage" checked> 🚗 Garagen</label>
|
||||
<label><input type="checkbox" class="layer-toggle" data-layer="jobcenter" checked> 💼 Jobcenter</label>
|
||||
<label><input type="checkbox" class="layer-toggle" data-layer="clothingshop" checked> 👕 Kleidungsläden</label>
|
||||
<label><input type="checkbox" class="layer-toggle" data-layer="insuranceoffice" checked> 🛡️ Versicherungsbüros</label>
|
||||
<label><input type="checkbox" class="layer-toggle" data-layer="plateoffice" checked> 🔖 Zulassungsstellen</label>
|
||||
<label><input type="checkbox" class="layer-toggle" data-layer="trailershop" checked> 🚛 Anhänger-Shops</label>
|
||||
<label><input type="checkbox" class="layer-toggle" data-layer="gasstation" checked> ⛽ Tankstellen</label>
|
||||
<label><input type="checkbox" class="layer-toggle" data-layer="repairshop" checked> 🔧 Werkstätten</label>
|
||||
<label><input type="checkbox" class="layer-toggle" data-layer="house" checked> 🏠 Häuser</label>
|
||||
<label><input type="checkbox" class="layer-toggle" data-layer="jobpoint" checked> 📍 Job-Punkte</label>
|
||||
<label><input type="checkbox" class="layer-toggle" data-layer="taxistand" checked> 🚕 Taxi-Stände</label>
|
||||
<label><input type="checkbox" class="layer-toggle" data-layer="hospital" checked> 🏥 Krankenhäuser</label>
|
||||
<label><input type="checkbox" class="layer-toggle" data-layer="prison" checked> 🔒 Gefängnisse</label>
|
||||
<label><input type="checkbox" class="layer-toggle" data-layer="zone" checked> 🚩 Zonen</label>
|
||||
<label><input type="checkbox" class="layer-toggle" data-layer="impound" checked> 🚛 Abschlepphöfe</label>
|
||||
<label><input type="checkbox" class="layer-toggle" data-layer="firestation" checked> 🚒 Feuerwachen</label>
|
||||
<label><input type="checkbox" class="layer-toggle" data-layer="drug_harvest" checked> 🌿 Anbaustellen</label>
|
||||
<label><input type="checkbox" class="layer-toggle" data-layer="drug_process" checked> ⚗️ Labore</label>
|
||||
<label><input type="checkbox" class="layer-toggle" data-layer="drug_dealer" checked> 💰 Verkaufsorte</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TILES & OBJEKTE -->
|
||||
<div class="sidebar-panel" id="panel-palette">
|
||||
<h3 class="sidebar-section-title">🧱 Tile-Palette</h3>
|
||||
<div style="margin-bottom:12px;">
|
||||
<label for="brushSize" style="color:#ccc; font-size:13px; display:block; margin-bottom:4px;">Pinselgröße:</label>
|
||||
<select id="brushSize">
|
||||
<option value="1">1×1</option>
|
||||
<option value="2">2×2</option>
|
||||
<option value="4">4×4</option>
|
||||
<option value="6">6×6</option>
|
||||
<option value="8">8×8</option>
|
||||
</select>
|
||||
<div class="sidebar-hint">Klicken und ziehen malt durchgehend. Drehen-Werkzeug (🔄) dreht Tile ODER Objekt an der angeklickten Stelle.</div>
|
||||
</div>
|
||||
<div id="tilePalette"></div>
|
||||
|
||||
<h3 class="sidebar-section-title" style="margin-top:20px;">📦 Objekt-Palette</h3>
|
||||
<div id="objectPalette"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="map_editor.js?v=26"></script>
|
||||
<script>
|
||||
// Sidebar-Tabs umschalten
|
||||
document.querySelectorAll(".sidebar-tab-btn").forEach(btn => {
|
||||
btn.addEventListener("click", () => {
|
||||
document.querySelectorAll(".sidebar-tab-btn").forEach(b => b.classList.remove("active"));
|
||||
document.querySelectorAll(".sidebar-panel").forEach(p => p.classList.remove("active"));
|
||||
btn.classList.add("active");
|
||||
document.getElementById("panel-" + btn.dataset.sidebarTab).classList.add("active");
|
||||
});
|
||||
});
|
||||
|
||||
// Canvas füllt den verfügbaren Platz aus, statt fest 1500x800 zu sein
|
||||
function resizeEditorCanvas() {
|
||||
const wrap = document.getElementById("canvasWrap");
|
||||
const canvas = document.getElementById("canvas");
|
||||
canvas.width = wrap.clientWidth;
|
||||
canvas.height = wrap.clientHeight;
|
||||
if (typeof render === "function") render();
|
||||
}
|
||||
window.addEventListener("resize", resizeEditorCanvas);
|
||||
window.addEventListener("load", resizeEditorCanvas);
|
||||
// Direkt beim Einbinden schon einmal versuchen (falls load-Event schon vorbei ist)
|
||||
setTimeout(resizeEditorCanvas, 50);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+2552
File diff suppressed because it is too large
Load Diff
+2625
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "haus",
|
||||
"spawn": { "x": 50, "y": 50 },
|
||||
"tiles": [
|
||||
[1,1,1,1],
|
||||
[1,0,0,1],
|
||||
[1,0,0,1],
|
||||
[1,1,1,1]
|
||||
],
|
||||
"doors": [
|
||||
{
|
||||
"x": 60,
|
||||
"y": 50,
|
||||
"targetMap": "interior_haus",
|
||||
"targetX": 40,
|
||||
"targetY": 40
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "interior_haus",
|
||||
"spawn": { "x": 40, "y": 40 },
|
||||
"tiles": [],
|
||||
"doors": [
|
||||
{
|
||||
"x": 40,
|
||||
"y": 80,
|
||||
"targetMap": "stadt",
|
||||
"targetX": 200,
|
||||
"targetY": 120
|
||||
}
|
||||
]
|
||||
}
|
||||
+251049
File diff suppressed because it is too large
Load Diff
+348
@@ -0,0 +1,348 @@
|
||||
{
|
||||
"name": "test",
|
||||
"spawn": {
|
||||
"x": 100,
|
||||
"y": 100
|
||||
},
|
||||
"tiles": [
|
||||
[
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2
|
||||
],
|
||||
[
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2
|
||||
]
|
||||
],
|
||||
"doors": [
|
||||
{
|
||||
"x": 176,
|
||||
"y": 144,
|
||||
"targetMap": "bunker",
|
||||
"targetX": 4,
|
||||
"targetY": 4
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
// -------------------------------------------------------------
|
||||
// EINMALIGES MIGRATIONSSKRIPT
|
||||
// Überträgt tiles.json und objectConfig.json in die neuen
|
||||
// "tile_config"- und "object_config"-Tabellen der Datenbank.
|
||||
//
|
||||
// Ausführen mit: node migrate_configs.js
|
||||
// (im selben Ordner wie server.js, NACHDEM die beiden Tabellen
|
||||
// per SQL angelegt wurden)
|
||||
// -------------------------------------------------------------
|
||||
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import mysql from "mysql2/promise";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const db = await mysql.createPool({
|
||||
host: "156.67.28.205",
|
||||
user: "game",
|
||||
password: "tito13101",
|
||||
database: "gamegta",
|
||||
port: "3406",
|
||||
connectionLimit: 5
|
||||
});
|
||||
|
||||
async function migrate() {
|
||||
// --- TILES ---
|
||||
const tilesPath = path.join(__dirname, "tiles.json");
|
||||
if (fs.existsSync(tilesPath)) {
|
||||
const tiles = JSON.parse(fs.readFileSync(tilesPath, "utf8"));
|
||||
let count = 0;
|
||||
|
||||
for (const [id, tile] of Object.entries(tiles)) {
|
||||
await db.query(
|
||||
`INSERT INTO tile_config (id, name, color, collision) VALUES (?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE name=?, color=?, collision=?`,
|
||||
[
|
||||
Number(id), tile.name || ("Tile " + id), tile.color || "#888888", tile.collision ? 1 : 0,
|
||||
tile.name || ("Tile " + id), tile.color || "#888888", tile.collision ? 1 : 0
|
||||
]
|
||||
);
|
||||
count++;
|
||||
}
|
||||
console.log(`✅ ${count} Tile(s) migriert.`);
|
||||
} else {
|
||||
console.log("Keine tiles.json gefunden - übersprungen.");
|
||||
}
|
||||
|
||||
// --- OBJECTS ---
|
||||
const objectsPath = path.join(__dirname, "objectConfig.json");
|
||||
if (fs.existsSync(objectsPath)) {
|
||||
const objects = JSON.parse(fs.readFileSync(objectsPath, "utf8"));
|
||||
let count = 0;
|
||||
|
||||
for (const [type, obj] of Object.entries(objects)) {
|
||||
await db.query(
|
||||
`INSERT INTO object_config (type, name, color, width, height, collision, interactive, action)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE name=?, color=?, width=?, height=?, collision=?, interactive=?, action=?`,
|
||||
[
|
||||
type, obj.name || type, obj.color || "#888888", obj.width || 32, obj.height || 32,
|
||||
obj.collision ? 1 : 0, obj.interactive ? 1 : 0, obj.action || null,
|
||||
obj.name || type, obj.color || "#888888", obj.width || 32, obj.height || 32,
|
||||
obj.collision ? 1 : 0, obj.interactive ? 1 : 0, obj.action || null
|
||||
]
|
||||
);
|
||||
count++;
|
||||
}
|
||||
console.log(`✅ ${count} Objekt-Typ(en) migriert.`);
|
||||
} else {
|
||||
console.log("Keine objectConfig.json gefunden - übersprungen.");
|
||||
}
|
||||
|
||||
console.log("Migration abgeschlossen.");
|
||||
console.log("Du kannst tiles.json/objectConfig.json jetzt umbenennen/archivieren, sobald du geprüft hast, dass alles im Spiel korrekt lädt.");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
migrate().catch(err => {
|
||||
console.error("Migration fehlgeschlagen:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
// -------------------------------------------------------------
|
||||
// EINMALIGES MIGRATIONSSKRIPT
|
||||
// Überträgt alle vorhandenen maps/*.json Dateien in die neue
|
||||
// "maps"-Tabelle der Datenbank.
|
||||
//
|
||||
// Ausführen mit: node migrate_maps.js
|
||||
// (im selben Ordner wie server.js, NACHDEM die "maps"-Tabelle
|
||||
// per SQL angelegt wurde)
|
||||
// -------------------------------------------------------------
|
||||
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import mysql from "mysql2/promise";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const db = await mysql.createPool({
|
||||
host: "156.67.28.205",
|
||||
user: "game",
|
||||
password: "tito13101",
|
||||
database: "gamegta",
|
||||
port: "3406",
|
||||
connectionLimit: 5
|
||||
});
|
||||
|
||||
async function migrate() {
|
||||
const dir = path.join(__dirname, "maps");
|
||||
|
||||
if (!fs.existsSync(dir)) {
|
||||
console.log("Kein maps/-Ordner gefunden - nichts zu migrieren.");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const files = fs.readdirSync(dir).filter(f => f.endsWith(".json"));
|
||||
|
||||
if (files.length === 0) {
|
||||
console.log("Keine .json-Dateien im maps/-Ordner gefunden.");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log(`Gefunden: ${files.length} Map-Datei(en). Starte Migration...`);
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(dir, file);
|
||||
const raw = fs.readFileSync(filePath, "utf8");
|
||||
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(raw);
|
||||
} catch (err) {
|
||||
console.error(`❌ ${file}: ungültiges JSON, übersprungen`, err.message);
|
||||
continue;
|
||||
}
|
||||
|
||||
const name = data.name || path.basename(file, ".json");
|
||||
|
||||
await db.query(
|
||||
"INSERT INTO maps (name, data) VALUES (?, ?) ON DUPLICATE KEY UPDATE data=?",
|
||||
[name, JSON.stringify(data), JSON.stringify(data)]
|
||||
);
|
||||
|
||||
console.log(`✅ "${name}" migriert (${(raw.length / 1024).toFixed(1)} KB)`);
|
||||
}
|
||||
|
||||
console.log("Migration abgeschlossen.");
|
||||
console.log("Du kannst den maps/-Ordner jetzt umbenennen/archivieren (z.B. maps_backup/), sobald du geprüft hast, dass alles im Spiel korrekt lädt.");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
migrate().catch(err => {
|
||||
console.error("Migration fehlgeschlagen:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
+1
@@ -0,0 +1 @@
|
||||
../node-gyp-build/bin.js
|
||||
+1
@@ -0,0 +1 @@
|
||||
../node-gyp-build/optional.js
|
||||
+1
@@ -0,0 +1 @@
|
||||
../node-gyp-build/build-test.js
|
||||
+1
@@ -0,0 +1 @@
|
||||
../semver/bin/semver.js
|
||||
+1203
File diff suppressed because it is too large
Load Diff
+43
@@ -0,0 +1,43 @@
|
||||
# @bufbuild/protobuf
|
||||
|
||||
This package provides the runtime library for the code generator plugin
|
||||
[protoc-gen-es](https://www.npmjs.com/package/@bufbuild/protoc-gen-es).
|
||||
|
||||
## Protocol Buffers for ECMAScript
|
||||
|
||||
A complete implementation of [Protocol Buffers](https://developers.google.com/protocol-buffers) in TypeScript,
|
||||
suitable for web browsers and Node.js.
|
||||
|
||||
**Protobuf-ES** is intended to be a solid, modern alternative to existing Protobuf implementations for the JavaScript ecosystem. It is the first project in this space to provide a comprehensive plugin framework and decouple the base types from RPC functionality.
|
||||
|
||||
Some additional features that set it apart from the others:
|
||||
|
||||
- ECMAScript module support
|
||||
- First-class TypeScript support
|
||||
- Generation of idiomatic JavaScript and TypeScript code.
|
||||
- Generation of [much smaller bundles](https://github.com/bufbuild/protobuf-es/blob/main/packages/bundle-size)
|
||||
- Implementation of all proto3 features, including the [canonical JSON format](https://developers.google.com/protocol-buffers/docs/proto3#json).
|
||||
- Implementation of all proto2 features, except for extensions and the text format.
|
||||
- Usage of standard JavaScript APIs instead of the [Closure Library](http://googlecode.blogspot.com/2009/11/introducing-closure-tools.html)
|
||||
- Compatibility is covered by the protocol buffers [conformance tests](https://github.com/bufbuild/protobuf-es/blob/main/packages/protobuf-conformance).
|
||||
- Descriptor and reflection support
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @bufbuild/protobuf
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
To learn how to work with `@bufbuild/protobuf` check out the docs for the [Runtime API](https://github.com/bufbuild/protobuf-es/blob/main/docs/runtime_api.md)
|
||||
and the [generated code](https://github.com/bufbuild/protobuf-es/blob/main/docs/generated_code.md).
|
||||
|
||||
Official documentation for the Protobuf-ES project can be found at [github.com/bufbuild/protobuf-es](https://github.com/bufbuild/protobuf-es).
|
||||
|
||||
For more information on Buf, check out the official [Buf documentation](https://docs.buf.build/introduction).
|
||||
|
||||
## Examples
|
||||
|
||||
A complete code example can be found in the **Protobuf-ES** repo [here](https://github.com/bufbuild/protobuf-es/tree/main/packages/protobuf-example).
|
||||
|
||||
+422
@@ -0,0 +1,422 @@
|
||||
/**
|
||||
* Protobuf binary format wire types.
|
||||
*
|
||||
* A wire type provides just enough information to find the length of the
|
||||
* following value.
|
||||
*
|
||||
* See https://developers.google.com/protocol-buffers/docs/encoding#structure
|
||||
*/
|
||||
export declare enum WireType {
|
||||
/**
|
||||
* Used for int32, int64, uint32, uint64, sint32, sint64, bool, enum
|
||||
*/
|
||||
Varint = 0,
|
||||
/**
|
||||
* Used for fixed64, sfixed64, double.
|
||||
* Always 8 bytes with little-endian byte order.
|
||||
*/
|
||||
Bit64 = 1,
|
||||
/**
|
||||
* Used for string, bytes, embedded messages, packed repeated fields
|
||||
*
|
||||
* Only repeated numeric types (types which use the varint, 32-bit,
|
||||
* or 64-bit wire types) can be packed. In proto3, such fields are
|
||||
* packed by default.
|
||||
*/
|
||||
LengthDelimited = 2,
|
||||
/**
|
||||
* Start of a tag-delimited aggregate, such as a proto2 group, or a message
|
||||
* in editions with message_encoding = DELIMITED.
|
||||
*/
|
||||
StartGroup = 3,
|
||||
/**
|
||||
* End of a tag-delimited aggregate.
|
||||
*/
|
||||
EndGroup = 4,
|
||||
/**
|
||||
* Used for fixed32, sfixed32, float.
|
||||
* Always 4 bytes with little-endian byte order.
|
||||
*/
|
||||
Bit32 = 5
|
||||
}
|
||||
type TextEncoderLike = {
|
||||
encode(input?: string): Uint8Array;
|
||||
};
|
||||
type TextDecoderLike = {
|
||||
decode(input?: Uint8Array): string;
|
||||
};
|
||||
export interface IBinaryReader {
|
||||
/**
|
||||
* Current position.
|
||||
*/
|
||||
readonly pos: number;
|
||||
/**
|
||||
* Number of bytes available in this reader.
|
||||
*/
|
||||
readonly len: number;
|
||||
/**
|
||||
* Reads a tag - field number and wire type.
|
||||
*/
|
||||
tag(): [number, WireType];
|
||||
/**
|
||||
* Skip one element on the wire and return the skipped data.
|
||||
*/
|
||||
skip(wireType: WireType, fieldNo?: number): Uint8Array;
|
||||
/**
|
||||
* Read a `uint32` field, an unsigned 32 bit varint.
|
||||
*/
|
||||
uint32(): number;
|
||||
/**
|
||||
* Read a `int32` field, a signed 32 bit varint.
|
||||
*/
|
||||
int32(): number;
|
||||
/**
|
||||
* Read a `sint32` field, a signed, zigzag-encoded 32-bit varint.
|
||||
*/
|
||||
sint32(): number;
|
||||
/**
|
||||
* Read a `int64` field, a signed 64-bit varint.
|
||||
*/
|
||||
int64(): bigint | string;
|
||||
/**
|
||||
* Read a `sint64` field, a signed, zig-zag-encoded 64-bit varint.
|
||||
*/
|
||||
sint64(): bigint | string;
|
||||
/**
|
||||
* Read a `fixed64` field, a signed, fixed-length 64-bit integer.
|
||||
*/
|
||||
sfixed64(): bigint | string;
|
||||
/**
|
||||
* Read a `uint64` field, an unsigned 64-bit varint.
|
||||
*/
|
||||
uint64(): bigint | string;
|
||||
/**
|
||||
* Read a `fixed64` field, an unsigned, fixed-length 64 bit integer.
|
||||
*/
|
||||
fixed64(): bigint | string;
|
||||
/**
|
||||
* Read a `bool` field, a variant.
|
||||
*/
|
||||
bool(): boolean;
|
||||
/**
|
||||
* Read a `fixed32` field, an unsigned, fixed-length 32-bit integer.
|
||||
*/
|
||||
fixed32(): number;
|
||||
/**
|
||||
* Read a `sfixed32` field, a signed, fixed-length 32-bit integer.
|
||||
*/
|
||||
sfixed32(): number;
|
||||
/**
|
||||
* Read a `float` field, 32-bit floating point number.
|
||||
*/
|
||||
float(): number;
|
||||
/**
|
||||
* Read a `double` field, a 64-bit floating point number.
|
||||
*/
|
||||
double(): number;
|
||||
/**
|
||||
* Read a `bytes` field, length-delimited arbitrary data.
|
||||
*/
|
||||
bytes(): Uint8Array;
|
||||
/**
|
||||
* Read a `string` field, length-delimited data converted to UTF-8 text.
|
||||
*/
|
||||
string(): string;
|
||||
}
|
||||
export interface IBinaryWriter {
|
||||
/**
|
||||
* Return all bytes written and reset this writer.
|
||||
*/
|
||||
finish(): Uint8Array;
|
||||
/**
|
||||
* Start a new fork for length-delimited data like a message
|
||||
* or a packed repeated field.
|
||||
*
|
||||
* Must be joined later with `join()`.
|
||||
*/
|
||||
fork(): IBinaryWriter;
|
||||
/**
|
||||
* Join the last fork. Write its length and bytes, then
|
||||
* return to the previous state.
|
||||
*/
|
||||
join(): IBinaryWriter;
|
||||
/**
|
||||
* Writes a tag (field number and wire type).
|
||||
*
|
||||
* Equivalent to `uint32( (fieldNo << 3 | type) >>> 0 )`
|
||||
*
|
||||
* Generated code should compute the tag ahead of time and call `uint32()`.
|
||||
*/
|
||||
tag(fieldNo: number, type: WireType): IBinaryWriter;
|
||||
/**
|
||||
* Write a chunk of raw bytes.
|
||||
*/
|
||||
raw(chunk: Uint8Array): IBinaryWriter;
|
||||
/**
|
||||
* Write a `uint32` value, an unsigned 32 bit varint.
|
||||
*/
|
||||
uint32(value: number): IBinaryWriter;
|
||||
/**
|
||||
* Write a `int32` value, a signed 32 bit varint.
|
||||
*/
|
||||
int32(value: number): IBinaryWriter;
|
||||
/**
|
||||
* Write a `sint32` value, a signed, zigzag-encoded 32-bit varint.
|
||||
*/
|
||||
sint32(value: number): IBinaryWriter;
|
||||
/**
|
||||
* Write a `int64` value, a signed 64-bit varint.
|
||||
*/
|
||||
int64(value: string | number | bigint): IBinaryWriter;
|
||||
/**
|
||||
* Write a `uint64` value, an unsigned 64-bit varint.
|
||||
*/
|
||||
uint64(value: string | number | bigint): IBinaryWriter;
|
||||
/**
|
||||
* Write a `sint64` value, a signed, zig-zag-encoded 64-bit varint.
|
||||
*/
|
||||
sint64(value: string | number | bigint): IBinaryWriter;
|
||||
/**
|
||||
* Write a `fixed64` value, an unsigned, fixed-length 64 bit integer.
|
||||
*/
|
||||
fixed64(value: string | number | bigint): IBinaryWriter;
|
||||
/**
|
||||
* Write a `fixed64` value, a signed, fixed-length 64-bit integer.
|
||||
*/
|
||||
sfixed64(value: string | number | bigint): IBinaryWriter;
|
||||
/**
|
||||
* Write a `bool` value, a variant.
|
||||
*/
|
||||
bool(value: boolean): IBinaryWriter;
|
||||
/**
|
||||
* Write a `fixed32` value, an unsigned, fixed-length 32-bit integer.
|
||||
*/
|
||||
fixed32(value: number): IBinaryWriter;
|
||||
/**
|
||||
* Write a `sfixed32` value, a signed, fixed-length 32-bit integer.
|
||||
*/
|
||||
sfixed32(value: number): IBinaryWriter;
|
||||
/**
|
||||
* Write a `float` value, 32-bit floating point number.
|
||||
*/
|
||||
float(value: number): IBinaryWriter;
|
||||
/**
|
||||
* Write a `double` value, a 64-bit floating point number.
|
||||
*/
|
||||
double(value: number): IBinaryWriter;
|
||||
/**
|
||||
* Write a `bytes` value, length-delimited arbitrary data.
|
||||
*/
|
||||
bytes(value: Uint8Array): IBinaryWriter;
|
||||
/**
|
||||
* Write a `string` value, length-delimited data converted to UTF-8 text.
|
||||
*/
|
||||
string(value: string): IBinaryWriter;
|
||||
}
|
||||
export declare class BinaryWriter implements IBinaryWriter {
|
||||
/**
|
||||
* We cannot allocate a buffer for the entire output
|
||||
* because we don't know it's size.
|
||||
*
|
||||
* So we collect smaller chunks of known size and
|
||||
* concat them later.
|
||||
*
|
||||
* Use `raw()` to push data to this array. It will flush
|
||||
* `buf` first.
|
||||
*/
|
||||
private chunks;
|
||||
/**
|
||||
* A growing buffer for byte values. If you don't know
|
||||
* the size of the data you are writing, push to this
|
||||
* array.
|
||||
*/
|
||||
protected buf: number[];
|
||||
/**
|
||||
* Previous fork states.
|
||||
*/
|
||||
private stack;
|
||||
/**
|
||||
* Text encoder instance to convert UTF-8 to bytes.
|
||||
*/
|
||||
private readonly textEncoder;
|
||||
constructor(textEncoder?: TextEncoderLike);
|
||||
/**
|
||||
* Return all bytes written and reset this writer.
|
||||
*/
|
||||
finish(): Uint8Array;
|
||||
/**
|
||||
* Start a new fork for length-delimited data like a message
|
||||
* or a packed repeated field.
|
||||
*
|
||||
* Must be joined later with `join()`.
|
||||
*/
|
||||
fork(): IBinaryWriter;
|
||||
/**
|
||||
* Join the last fork. Write its length and bytes, then
|
||||
* return to the previous state.
|
||||
*/
|
||||
join(): IBinaryWriter;
|
||||
/**
|
||||
* Writes a tag (field number and wire type).
|
||||
*
|
||||
* Equivalent to `uint32( (fieldNo << 3 | type) >>> 0 )`.
|
||||
*
|
||||
* Generated code should compute the tag ahead of time and call `uint32()`.
|
||||
*/
|
||||
tag(fieldNo: number, type: WireType): IBinaryWriter;
|
||||
/**
|
||||
* Write a chunk of raw bytes.
|
||||
*/
|
||||
raw(chunk: Uint8Array): IBinaryWriter;
|
||||
/**
|
||||
* Write a `uint32` value, an unsigned 32 bit varint.
|
||||
*/
|
||||
uint32(value: number): IBinaryWriter;
|
||||
/**
|
||||
* Write a `int32` value, a signed 32 bit varint.
|
||||
*/
|
||||
int32(value: number): IBinaryWriter;
|
||||
/**
|
||||
* Write a `bool` value, a variant.
|
||||
*/
|
||||
bool(value: boolean): IBinaryWriter;
|
||||
/**
|
||||
* Write a `bytes` value, length-delimited arbitrary data.
|
||||
*/
|
||||
bytes(value: Uint8Array): IBinaryWriter;
|
||||
/**
|
||||
* Write a `string` value, length-delimited data converted to UTF-8 text.
|
||||
*/
|
||||
string(value: string): IBinaryWriter;
|
||||
/**
|
||||
* Write a `float` value, 32-bit floating point number.
|
||||
*/
|
||||
float(value: number): IBinaryWriter;
|
||||
/**
|
||||
* Write a `double` value, a 64-bit floating point number.
|
||||
*/
|
||||
double(value: number): IBinaryWriter;
|
||||
/**
|
||||
* Write a `fixed32` value, an unsigned, fixed-length 32-bit integer.
|
||||
*/
|
||||
fixed32(value: number): IBinaryWriter;
|
||||
/**
|
||||
* Write a `sfixed32` value, a signed, fixed-length 32-bit integer.
|
||||
*/
|
||||
sfixed32(value: number): IBinaryWriter;
|
||||
/**
|
||||
* Write a `sint32` value, a signed, zigzag-encoded 32-bit varint.
|
||||
*/
|
||||
sint32(value: number): IBinaryWriter;
|
||||
/**
|
||||
* Write a `fixed64` value, a signed, fixed-length 64-bit integer.
|
||||
*/
|
||||
sfixed64(value: string | number | bigint): IBinaryWriter;
|
||||
/**
|
||||
* Write a `fixed64` value, an unsigned, fixed-length 64 bit integer.
|
||||
*/
|
||||
fixed64(value: string | number | bigint): IBinaryWriter;
|
||||
/**
|
||||
* Write a `int64` value, a signed 64-bit varint.
|
||||
*/
|
||||
int64(value: string | number | bigint): IBinaryWriter;
|
||||
/**
|
||||
* Write a `sint64` value, a signed, zig-zag-encoded 64-bit varint.
|
||||
*/
|
||||
sint64(value: string | number | bigint): IBinaryWriter;
|
||||
/**
|
||||
* Write a `uint64` value, an unsigned 64-bit varint.
|
||||
*/
|
||||
uint64(value: string | number | bigint): IBinaryWriter;
|
||||
}
|
||||
export declare class BinaryReader implements IBinaryReader {
|
||||
/**
|
||||
* Current position.
|
||||
*/
|
||||
pos: number;
|
||||
/**
|
||||
* Number of bytes available in this reader.
|
||||
*/
|
||||
readonly len: number;
|
||||
private readonly buf;
|
||||
private readonly view;
|
||||
private readonly textDecoder;
|
||||
constructor(buf: Uint8Array, textDecoder?: TextDecoderLike);
|
||||
/**
|
||||
* Reads a tag - field number and wire type.
|
||||
*/
|
||||
tag(): [number, WireType];
|
||||
/**
|
||||
* Skip one element and return the skipped data.
|
||||
*
|
||||
* When skipping StartGroup, provide the tags field number to check for
|
||||
* matching field number in the EndGroup tag.
|
||||
*/
|
||||
skip(wireType: WireType, fieldNo?: number): Uint8Array;
|
||||
protected varint64: () => [number, number];
|
||||
/**
|
||||
* Throws error if position in byte array is out of range.
|
||||
*/
|
||||
protected assertBounds(): void;
|
||||
/**
|
||||
* Read a `uint32` field, an unsigned 32 bit varint.
|
||||
*/
|
||||
uint32: () => number;
|
||||
/**
|
||||
* Read a `int32` field, a signed 32 bit varint.
|
||||
*/
|
||||
int32(): number;
|
||||
/**
|
||||
* Read a `sint32` field, a signed, zigzag-encoded 32-bit varint.
|
||||
*/
|
||||
sint32(): number;
|
||||
/**
|
||||
* Read a `int64` field, a signed 64-bit varint.
|
||||
*/
|
||||
int64(): bigint | string;
|
||||
/**
|
||||
* Read a `uint64` field, an unsigned 64-bit varint.
|
||||
*/
|
||||
uint64(): bigint | string;
|
||||
/**
|
||||
* Read a `sint64` field, a signed, zig-zag-encoded 64-bit varint.
|
||||
*/
|
||||
sint64(): bigint | string;
|
||||
/**
|
||||
* Read a `bool` field, a variant.
|
||||
*/
|
||||
bool(): boolean;
|
||||
/**
|
||||
* Read a `fixed32` field, an unsigned, fixed-length 32-bit integer.
|
||||
*/
|
||||
fixed32(): number;
|
||||
/**
|
||||
* Read a `sfixed32` field, a signed, fixed-length 32-bit integer.
|
||||
*/
|
||||
sfixed32(): number;
|
||||
/**
|
||||
* Read a `fixed64` field, an unsigned, fixed-length 64 bit integer.
|
||||
*/
|
||||
fixed64(): bigint | string;
|
||||
/**
|
||||
* Read a `fixed64` field, a signed, fixed-length 64-bit integer.
|
||||
*/
|
||||
sfixed64(): bigint | string;
|
||||
/**
|
||||
* Read a `float` field, 32-bit floating point number.
|
||||
*/
|
||||
float(): number;
|
||||
/**
|
||||
* Read a `double` field, a 64-bit floating point number.
|
||||
*/
|
||||
double(): number;
|
||||
/**
|
||||
* Read a `bytes` field, length-delimited arbitrary data.
|
||||
*/
|
||||
bytes(): Uint8Array;
|
||||
/**
|
||||
* Read a `string` field, length-delimited data converted to UTF-8 text.
|
||||
*/
|
||||
string(): string;
|
||||
}
|
||||
export {};
|
||||
+444
@@ -0,0 +1,444 @@
|
||||
"use strict";
|
||||
// Copyright 2021-2024 Buf Technologies, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.BinaryReader = exports.BinaryWriter = exports.WireType = void 0;
|
||||
const varint_js_1 = require("./google/varint.js");
|
||||
const assert_js_1 = require("./private/assert.js");
|
||||
const proto_int64_js_1 = require("./proto-int64.js");
|
||||
/* eslint-disable prefer-const,no-case-declarations,@typescript-eslint/restrict-plus-operands */
|
||||
/**
|
||||
* Protobuf binary format wire types.
|
||||
*
|
||||
* A wire type provides just enough information to find the length of the
|
||||
* following value.
|
||||
*
|
||||
* See https://developers.google.com/protocol-buffers/docs/encoding#structure
|
||||
*/
|
||||
var WireType;
|
||||
(function (WireType) {
|
||||
/**
|
||||
* Used for int32, int64, uint32, uint64, sint32, sint64, bool, enum
|
||||
*/
|
||||
WireType[WireType["Varint"] = 0] = "Varint";
|
||||
/**
|
||||
* Used for fixed64, sfixed64, double.
|
||||
* Always 8 bytes with little-endian byte order.
|
||||
*/
|
||||
WireType[WireType["Bit64"] = 1] = "Bit64";
|
||||
/**
|
||||
* Used for string, bytes, embedded messages, packed repeated fields
|
||||
*
|
||||
* Only repeated numeric types (types which use the varint, 32-bit,
|
||||
* or 64-bit wire types) can be packed. In proto3, such fields are
|
||||
* packed by default.
|
||||
*/
|
||||
WireType[WireType["LengthDelimited"] = 2] = "LengthDelimited";
|
||||
/**
|
||||
* Start of a tag-delimited aggregate, such as a proto2 group, or a message
|
||||
* in editions with message_encoding = DELIMITED.
|
||||
*/
|
||||
WireType[WireType["StartGroup"] = 3] = "StartGroup";
|
||||
/**
|
||||
* End of a tag-delimited aggregate.
|
||||
*/
|
||||
WireType[WireType["EndGroup"] = 4] = "EndGroup";
|
||||
/**
|
||||
* Used for fixed32, sfixed32, float.
|
||||
* Always 4 bytes with little-endian byte order.
|
||||
*/
|
||||
WireType[WireType["Bit32"] = 5] = "Bit32";
|
||||
})(WireType || (exports.WireType = WireType = {}));
|
||||
class BinaryWriter {
|
||||
constructor(textEncoder) {
|
||||
/**
|
||||
* Previous fork states.
|
||||
*/
|
||||
this.stack = [];
|
||||
this.textEncoder = textEncoder !== null && textEncoder !== void 0 ? textEncoder : new TextEncoder();
|
||||
this.chunks = [];
|
||||
this.buf = [];
|
||||
}
|
||||
/**
|
||||
* Return all bytes written and reset this writer.
|
||||
*/
|
||||
finish() {
|
||||
this.chunks.push(new Uint8Array(this.buf)); // flush the buffer
|
||||
let len = 0;
|
||||
for (let i = 0; i < this.chunks.length; i++)
|
||||
len += this.chunks[i].length;
|
||||
let bytes = new Uint8Array(len);
|
||||
let offset = 0;
|
||||
for (let i = 0; i < this.chunks.length; i++) {
|
||||
bytes.set(this.chunks[i], offset);
|
||||
offset += this.chunks[i].length;
|
||||
}
|
||||
this.chunks = [];
|
||||
return bytes;
|
||||
}
|
||||
/**
|
||||
* Start a new fork for length-delimited data like a message
|
||||
* or a packed repeated field.
|
||||
*
|
||||
* Must be joined later with `join()`.
|
||||
*/
|
||||
fork() {
|
||||
this.stack.push({ chunks: this.chunks, buf: this.buf });
|
||||
this.chunks = [];
|
||||
this.buf = [];
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Join the last fork. Write its length and bytes, then
|
||||
* return to the previous state.
|
||||
*/
|
||||
join() {
|
||||
// get chunk of fork
|
||||
let chunk = this.finish();
|
||||
// restore previous state
|
||||
let prev = this.stack.pop();
|
||||
if (!prev)
|
||||
throw new Error("invalid state, fork stack empty");
|
||||
this.chunks = prev.chunks;
|
||||
this.buf = prev.buf;
|
||||
// write length of chunk as varint
|
||||
this.uint32(chunk.byteLength);
|
||||
return this.raw(chunk);
|
||||
}
|
||||
/**
|
||||
* Writes a tag (field number and wire type).
|
||||
*
|
||||
* Equivalent to `uint32( (fieldNo << 3 | type) >>> 0 )`.
|
||||
*
|
||||
* Generated code should compute the tag ahead of time and call `uint32()`.
|
||||
*/
|
||||
tag(fieldNo, type) {
|
||||
return this.uint32(((fieldNo << 3) | type) >>> 0);
|
||||
}
|
||||
/**
|
||||
* Write a chunk of raw bytes.
|
||||
*/
|
||||
raw(chunk) {
|
||||
if (this.buf.length) {
|
||||
this.chunks.push(new Uint8Array(this.buf));
|
||||
this.buf = [];
|
||||
}
|
||||
this.chunks.push(chunk);
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Write a `uint32` value, an unsigned 32 bit varint.
|
||||
*/
|
||||
uint32(value) {
|
||||
(0, assert_js_1.assertUInt32)(value);
|
||||
// write value as varint 32, inlined for speed
|
||||
while (value > 0x7f) {
|
||||
this.buf.push((value & 0x7f) | 0x80);
|
||||
value = value >>> 7;
|
||||
}
|
||||
this.buf.push(value);
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Write a `int32` value, a signed 32 bit varint.
|
||||
*/
|
||||
int32(value) {
|
||||
(0, assert_js_1.assertInt32)(value);
|
||||
(0, varint_js_1.varint32write)(value, this.buf);
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Write a `bool` value, a variant.
|
||||
*/
|
||||
bool(value) {
|
||||
this.buf.push(value ? 1 : 0);
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Write a `bytes` value, length-delimited arbitrary data.
|
||||
*/
|
||||
bytes(value) {
|
||||
this.uint32(value.byteLength); // write length of chunk as varint
|
||||
return this.raw(value);
|
||||
}
|
||||
/**
|
||||
* Write a `string` value, length-delimited data converted to UTF-8 text.
|
||||
*/
|
||||
string(value) {
|
||||
let chunk = this.textEncoder.encode(value);
|
||||
this.uint32(chunk.byteLength); // write length of chunk as varint
|
||||
return this.raw(chunk);
|
||||
}
|
||||
/**
|
||||
* Write a `float` value, 32-bit floating point number.
|
||||
*/
|
||||
float(value) {
|
||||
(0, assert_js_1.assertFloat32)(value);
|
||||
let chunk = new Uint8Array(4);
|
||||
new DataView(chunk.buffer).setFloat32(0, value, true);
|
||||
return this.raw(chunk);
|
||||
}
|
||||
/**
|
||||
* Write a `double` value, a 64-bit floating point number.
|
||||
*/
|
||||
double(value) {
|
||||
let chunk = new Uint8Array(8);
|
||||
new DataView(chunk.buffer).setFloat64(0, value, true);
|
||||
return this.raw(chunk);
|
||||
}
|
||||
/**
|
||||
* Write a `fixed32` value, an unsigned, fixed-length 32-bit integer.
|
||||
*/
|
||||
fixed32(value) {
|
||||
(0, assert_js_1.assertUInt32)(value);
|
||||
let chunk = new Uint8Array(4);
|
||||
new DataView(chunk.buffer).setUint32(0, value, true);
|
||||
return this.raw(chunk);
|
||||
}
|
||||
/**
|
||||
* Write a `sfixed32` value, a signed, fixed-length 32-bit integer.
|
||||
*/
|
||||
sfixed32(value) {
|
||||
(0, assert_js_1.assertInt32)(value);
|
||||
let chunk = new Uint8Array(4);
|
||||
new DataView(chunk.buffer).setInt32(0, value, true);
|
||||
return this.raw(chunk);
|
||||
}
|
||||
/**
|
||||
* Write a `sint32` value, a signed, zigzag-encoded 32-bit varint.
|
||||
*/
|
||||
sint32(value) {
|
||||
(0, assert_js_1.assertInt32)(value);
|
||||
// zigzag encode
|
||||
value = ((value << 1) ^ (value >> 31)) >>> 0;
|
||||
(0, varint_js_1.varint32write)(value, this.buf);
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Write a `fixed64` value, a signed, fixed-length 64-bit integer.
|
||||
*/
|
||||
sfixed64(value) {
|
||||
let chunk = new Uint8Array(8), view = new DataView(chunk.buffer), tc = proto_int64_js_1.protoInt64.enc(value);
|
||||
view.setInt32(0, tc.lo, true);
|
||||
view.setInt32(4, tc.hi, true);
|
||||
return this.raw(chunk);
|
||||
}
|
||||
/**
|
||||
* Write a `fixed64` value, an unsigned, fixed-length 64 bit integer.
|
||||
*/
|
||||
fixed64(value) {
|
||||
let chunk = new Uint8Array(8), view = new DataView(chunk.buffer), tc = proto_int64_js_1.protoInt64.uEnc(value);
|
||||
view.setInt32(0, tc.lo, true);
|
||||
view.setInt32(4, tc.hi, true);
|
||||
return this.raw(chunk);
|
||||
}
|
||||
/**
|
||||
* Write a `int64` value, a signed 64-bit varint.
|
||||
*/
|
||||
int64(value) {
|
||||
let tc = proto_int64_js_1.protoInt64.enc(value);
|
||||
(0, varint_js_1.varint64write)(tc.lo, tc.hi, this.buf);
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Write a `sint64` value, a signed, zig-zag-encoded 64-bit varint.
|
||||
*/
|
||||
sint64(value) {
|
||||
let tc = proto_int64_js_1.protoInt64.enc(value),
|
||||
// zigzag encode
|
||||
sign = tc.hi >> 31, lo = (tc.lo << 1) ^ sign, hi = ((tc.hi << 1) | (tc.lo >>> 31)) ^ sign;
|
||||
(0, varint_js_1.varint64write)(lo, hi, this.buf);
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Write a `uint64` value, an unsigned 64-bit varint.
|
||||
*/
|
||||
uint64(value) {
|
||||
let tc = proto_int64_js_1.protoInt64.uEnc(value);
|
||||
(0, varint_js_1.varint64write)(tc.lo, tc.hi, this.buf);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
exports.BinaryWriter = BinaryWriter;
|
||||
class BinaryReader {
|
||||
constructor(buf, textDecoder) {
|
||||
this.varint64 = varint_js_1.varint64read; // dirty cast for `this`
|
||||
/**
|
||||
* Read a `uint32` field, an unsigned 32 bit varint.
|
||||
*/
|
||||
this.uint32 = varint_js_1.varint32read; // dirty cast for `this` and access to protected `buf`
|
||||
this.buf = buf;
|
||||
this.len = buf.length;
|
||||
this.pos = 0;
|
||||
this.view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
|
||||
this.textDecoder = textDecoder !== null && textDecoder !== void 0 ? textDecoder : new TextDecoder();
|
||||
}
|
||||
/**
|
||||
* Reads a tag - field number and wire type.
|
||||
*/
|
||||
tag() {
|
||||
let tag = this.uint32(), fieldNo = tag >>> 3, wireType = tag & 7;
|
||||
if (fieldNo <= 0 || wireType < 0 || wireType > 5)
|
||||
throw new Error("illegal tag: field no " + fieldNo + " wire type " + wireType);
|
||||
return [fieldNo, wireType];
|
||||
}
|
||||
/**
|
||||
* Skip one element and return the skipped data.
|
||||
*
|
||||
* When skipping StartGroup, provide the tags field number to check for
|
||||
* matching field number in the EndGroup tag.
|
||||
*/
|
||||
skip(wireType, fieldNo) {
|
||||
let start = this.pos;
|
||||
switch (wireType) {
|
||||
case WireType.Varint:
|
||||
while (this.buf[this.pos++] & 0x80) {
|
||||
// ignore
|
||||
}
|
||||
break;
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore TS7029: Fallthrough case in switch
|
||||
case WireType.Bit64:
|
||||
this.pos += 4;
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore TS7029: Fallthrough case in switch
|
||||
case WireType.Bit32:
|
||||
this.pos += 4;
|
||||
break;
|
||||
case WireType.LengthDelimited:
|
||||
let len = this.uint32();
|
||||
this.pos += len;
|
||||
break;
|
||||
case WireType.StartGroup:
|
||||
for (;;) {
|
||||
const [fn, wt] = this.tag();
|
||||
if (wt === WireType.EndGroup) {
|
||||
if (fieldNo !== undefined && fn !== fieldNo) {
|
||||
throw new Error("invalid end group tag");
|
||||
}
|
||||
break;
|
||||
}
|
||||
this.skip(wt, fn);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error("cant skip wire type " + wireType);
|
||||
}
|
||||
this.assertBounds();
|
||||
return this.buf.subarray(start, this.pos);
|
||||
}
|
||||
/**
|
||||
* Throws error if position in byte array is out of range.
|
||||
*/
|
||||
assertBounds() {
|
||||
if (this.pos > this.len)
|
||||
throw new RangeError("premature EOF");
|
||||
}
|
||||
/**
|
||||
* Read a `int32` field, a signed 32 bit varint.
|
||||
*/
|
||||
int32() {
|
||||
return this.uint32() | 0;
|
||||
}
|
||||
/**
|
||||
* Read a `sint32` field, a signed, zigzag-encoded 32-bit varint.
|
||||
*/
|
||||
sint32() {
|
||||
let zze = this.uint32();
|
||||
// decode zigzag
|
||||
return (zze >>> 1) ^ -(zze & 1);
|
||||
}
|
||||
/**
|
||||
* Read a `int64` field, a signed 64-bit varint.
|
||||
*/
|
||||
int64() {
|
||||
return proto_int64_js_1.protoInt64.dec(...this.varint64());
|
||||
}
|
||||
/**
|
||||
* Read a `uint64` field, an unsigned 64-bit varint.
|
||||
*/
|
||||
uint64() {
|
||||
return proto_int64_js_1.protoInt64.uDec(...this.varint64());
|
||||
}
|
||||
/**
|
||||
* Read a `sint64` field, a signed, zig-zag-encoded 64-bit varint.
|
||||
*/
|
||||
sint64() {
|
||||
let [lo, hi] = this.varint64();
|
||||
// decode zig zag
|
||||
let s = -(lo & 1);
|
||||
lo = ((lo >>> 1) | ((hi & 1) << 31)) ^ s;
|
||||
hi = (hi >>> 1) ^ s;
|
||||
return proto_int64_js_1.protoInt64.dec(lo, hi);
|
||||
}
|
||||
/**
|
||||
* Read a `bool` field, a variant.
|
||||
*/
|
||||
bool() {
|
||||
let [lo, hi] = this.varint64();
|
||||
return lo !== 0 || hi !== 0;
|
||||
}
|
||||
/**
|
||||
* Read a `fixed32` field, an unsigned, fixed-length 32-bit integer.
|
||||
*/
|
||||
fixed32() {
|
||||
return this.view.getUint32((this.pos += 4) - 4, true);
|
||||
}
|
||||
/**
|
||||
* Read a `sfixed32` field, a signed, fixed-length 32-bit integer.
|
||||
*/
|
||||
sfixed32() {
|
||||
return this.view.getInt32((this.pos += 4) - 4, true);
|
||||
}
|
||||
/**
|
||||
* Read a `fixed64` field, an unsigned, fixed-length 64 bit integer.
|
||||
*/
|
||||
fixed64() {
|
||||
return proto_int64_js_1.protoInt64.uDec(this.sfixed32(), this.sfixed32());
|
||||
}
|
||||
/**
|
||||
* Read a `fixed64` field, a signed, fixed-length 64-bit integer.
|
||||
*/
|
||||
sfixed64() {
|
||||
return proto_int64_js_1.protoInt64.dec(this.sfixed32(), this.sfixed32());
|
||||
}
|
||||
/**
|
||||
* Read a `float` field, 32-bit floating point number.
|
||||
*/
|
||||
float() {
|
||||
return this.view.getFloat32((this.pos += 4) - 4, true);
|
||||
}
|
||||
/**
|
||||
* Read a `double` field, a 64-bit floating point number.
|
||||
*/
|
||||
double() {
|
||||
return this.view.getFloat64((this.pos += 8) - 8, true);
|
||||
}
|
||||
/**
|
||||
* Read a `bytes` field, length-delimited arbitrary data.
|
||||
*/
|
||||
bytes() {
|
||||
let len = this.uint32(), start = this.pos;
|
||||
this.pos += len;
|
||||
this.assertBounds();
|
||||
return this.buf.subarray(start, start + len);
|
||||
}
|
||||
/**
|
||||
* Read a `string` field, length-delimited data converted to UTF-8 text.
|
||||
*/
|
||||
string() {
|
||||
return this.textDecoder.decode(this.bytes());
|
||||
}
|
||||
}
|
||||
exports.BinaryReader = BinaryReader;
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
import type { Message } from "./message.js";
|
||||
import type { IBinaryReader, IBinaryWriter, WireType } from "./binary-encoding.js";
|
||||
import type { FieldInfo } from "./field.js";
|
||||
/**
|
||||
* BinaryFormat is the contract for serializing messages to and from binary
|
||||
* data. Implementations may be specific to a proto syntax, and can be
|
||||
* reflection based, or delegate to speed optimized generated code.
|
||||
*/
|
||||
export interface BinaryFormat {
|
||||
/**
|
||||
* Provide options for parsing binary data.
|
||||
*/
|
||||
makeReadOptions(options?: Partial<BinaryReadOptions>): Readonly<BinaryReadOptions>;
|
||||
/**
|
||||
* Provide options for serializing binary data.
|
||||
*/
|
||||
makeWriteOptions(options?: Partial<BinaryWriteOptions>): Readonly<BinaryWriteOptions>;
|
||||
/**
|
||||
* Parse a message from binary data, merging fields.
|
||||
*
|
||||
* Supports two message encodings:
|
||||
* - length-prefixed: delimitedMessageEncoding is false or omitted, and
|
||||
* lengthOrEndTagFieldNo is the expected length of the message in the reader.
|
||||
* - delimited: delimitedMessageEncoding is true, and lengthOrEndTagFieldNo is
|
||||
* the field number in a tag with wire type end-group signalling the end of
|
||||
* the message in the reader.
|
||||
*
|
||||
* delimitedMessageEncoding is optional for backwards compatibility.
|
||||
*/
|
||||
readMessage(message: Message, reader: IBinaryReader, lengthOrEndTagFieldNo: number, options: BinaryReadOptions, delimitedMessageEncoding?: boolean): void;
|
||||
/**
|
||||
* Parse a field from binary data, and store it in the given target.
|
||||
*
|
||||
* The target must be an initialized message object, with oneof groups,
|
||||
* repeated fields and maps already present.
|
||||
*/
|
||||
readField(target: Record<string, any>, // eslint-disable-line @typescript-eslint/no-explicit-any -- `any` is the best choice for dynamic access
|
||||
reader: IBinaryReader, field: FieldInfo, wireType: WireType, options: BinaryReadOptions): void;
|
||||
/**
|
||||
* Serialize a message to binary data.
|
||||
*/
|
||||
writeMessage(message: Message, writer: IBinaryWriter, options: BinaryWriteOptions): void;
|
||||
/**
|
||||
* Serialize a field value to binary data.
|
||||
*
|
||||
* The value must be an array for repeated fields, a record object for map
|
||||
* fields. Only selected oneof fields should be passed to this method.
|
||||
*/
|
||||
writeField(field: FieldInfo, value: any, // eslint-disable-line @typescript-eslint/no-explicit-any -- `any` is the best choice for dynamic access
|
||||
writer: IBinaryWriter, options: BinaryWriteOptions): void;
|
||||
/**
|
||||
* Retrieve the unknown fields for the given message.
|
||||
*
|
||||
* Unknown fields are well-formed protocol buffer serialized data for
|
||||
* fields that the parserdoes not recognize.
|
||||
*
|
||||
* For more details see https://developers.google.com/protocol-buffers/docs/proto3#unknowns
|
||||
*/
|
||||
listUnknownFields(message: Message): ReadonlyArray<{
|
||||
no: number;
|
||||
wireType: WireType;
|
||||
data: Uint8Array;
|
||||
}>;
|
||||
/**
|
||||
* Discard unknown fields for the given message.
|
||||
*/
|
||||
discardUnknownFields(message: Message): void;
|
||||
/**
|
||||
* Retrieve the unknown fields for the given message and write them to
|
||||
* the given writer. This method is called when a message is serialized,
|
||||
* so the fields that are unknown to the parser persist through a round
|
||||
* trip.
|
||||
*/
|
||||
writeUnknownFields(message: Message, writer: IBinaryWriter): void;
|
||||
/**
|
||||
* Store an unknown field for the given message. The parser will use this
|
||||
* method if it does not recognize a field, unless the option
|
||||
* `readUnknownFields` has been disabled.
|
||||
*/
|
||||
onUnknownField(message: Message, no: number, wireType: WireType, data: Uint8Array): void;
|
||||
}
|
||||
/**
|
||||
* Options for parsing binary data.
|
||||
*/
|
||||
export interface BinaryReadOptions {
|
||||
/**
|
||||
* Retain unknown fields during parsing? The default behavior is to retain
|
||||
* unknown fields and include them in the serialized output.
|
||||
*
|
||||
* For more details see https://developers.google.com/protocol-buffers/docs/proto3#unknowns
|
||||
*/
|
||||
readUnknownFields: boolean;
|
||||
/**
|
||||
* Allows to use a custom implementation to decode binary data.
|
||||
*/
|
||||
readerFactory: (bytes: Uint8Array) => IBinaryReader;
|
||||
}
|
||||
/**
|
||||
* Options for serializing to binary data.
|
||||
*/
|
||||
export interface BinaryWriteOptions {
|
||||
/**
|
||||
* Include unknown fields in the serialized output? The default behavior
|
||||
* is to retain unknown fields and include them in the serialized output.
|
||||
*
|
||||
* For more details see https://developers.google.com/protocol-buffers/docs/proto3#unknowns
|
||||
*/
|
||||
writeUnknownFields: boolean;
|
||||
/**
|
||||
* Allows to use a custom implementation to encode binary data.
|
||||
*/
|
||||
writerFactory: () => IBinaryWriter;
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
"use strict";
|
||||
// Copyright 2021-2024 Buf Technologies, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import { reifyWkt } from "./private/reify-wkt.js";
|
||||
import type { DescEnum, DescEnumValue, DescField, DescExtension, DescMessage, DescMethod, DescOneof, DescService } from "./descriptor-set.js";
|
||||
import type { ScalarValue } from "./scalar.js";
|
||||
import { LongType, ScalarType } from "./scalar.js";
|
||||
interface CodegenInfo {
|
||||
/**
|
||||
* Name of the runtime library NPM package.
|
||||
*/
|
||||
readonly packageName: string;
|
||||
readonly localName: (desc: DescEnum | DescEnumValue | DescMessage | DescExtension | DescOneof | DescField | DescService | DescMethod) => string;
|
||||
readonly symbols: Record<RuntimeSymbolName, RuntimeSymbolInfo>;
|
||||
readonly getUnwrappedFieldType: (field: DescField | DescExtension) => ScalarType | undefined;
|
||||
readonly wktSourceFiles: readonly string[];
|
||||
/**
|
||||
* @deprecated please use scalarZeroValue instead
|
||||
*/
|
||||
readonly scalarDefaultValue: (type: ScalarType, longType: LongType) => any;
|
||||
readonly scalarZeroValue: <T extends ScalarType, L extends LongType>(type: T, longType: L) => ScalarValue<T, L>;
|
||||
/**
|
||||
* @deprecated please use reifyWkt from @bufbuild/protoplugin/ecmascript instead
|
||||
*/
|
||||
readonly reifyWkt: typeof reifyWkt;
|
||||
readonly safeIdentifier: (name: string) => string;
|
||||
readonly safeObjectProperty: (name: string) => string;
|
||||
}
|
||||
type RuntimeSymbolName = "proto2" | "proto3" | "Message" | "PartialMessage" | "PlainMessage" | "FieldList" | "MessageType" | "Extension" | "BinaryReadOptions" | "BinaryWriteOptions" | "JsonReadOptions" | "JsonWriteOptions" | "JsonValue" | "JsonObject" | "protoDouble" | "protoInt64" | "ScalarType" | "LongType" | "MethodKind" | "MethodIdempotency" | "IMessageTypeRegistry";
|
||||
type RuntimeSymbolInfo = {
|
||||
typeOnly: boolean;
|
||||
publicImportPath: string;
|
||||
privateImportPath: string;
|
||||
};
|
||||
export declare const codegenInfo: CodegenInfo;
|
||||
export {};
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
"use strict";
|
||||
// Copyright 2021-2024 Buf Technologies, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.codegenInfo = void 0;
|
||||
const names_js_1 = require("./private/names.js");
|
||||
const field_wrapper_js_1 = require("./private/field-wrapper.js");
|
||||
const scalars_js_1 = require("./private/scalars.js");
|
||||
const reify_wkt_js_1 = require("./private/reify-wkt.js");
|
||||
const packageName = "@bufbuild/protobuf";
|
||||
exports.codegenInfo = {
|
||||
packageName: "@bufbuild/protobuf",
|
||||
localName: names_js_1.localName,
|
||||
reifyWkt: reify_wkt_js_1.reifyWkt,
|
||||
getUnwrappedFieldType: field_wrapper_js_1.getUnwrappedFieldType,
|
||||
scalarDefaultValue: scalars_js_1.scalarZeroValue,
|
||||
scalarZeroValue: scalars_js_1.scalarZeroValue,
|
||||
safeIdentifier: names_js_1.safeIdentifier,
|
||||
safeObjectProperty: names_js_1.safeObjectProperty,
|
||||
// prettier-ignore
|
||||
symbols: {
|
||||
proto2: { typeOnly: false, privateImportPath: "./proto2.js", publicImportPath: packageName },
|
||||
proto3: { typeOnly: false, privateImportPath: "./proto3.js", publicImportPath: packageName },
|
||||
Message: { typeOnly: false, privateImportPath: "./message.js", publicImportPath: packageName },
|
||||
PartialMessage: { typeOnly: true, privateImportPath: "./message.js", publicImportPath: packageName },
|
||||
PlainMessage: { typeOnly: true, privateImportPath: "./message.js", publicImportPath: packageName },
|
||||
FieldList: { typeOnly: true, privateImportPath: "./field-list.js", publicImportPath: packageName },
|
||||
MessageType: { typeOnly: true, privateImportPath: "./message-type.js", publicImportPath: packageName },
|
||||
Extension: { typeOnly: true, privateImportPath: "./extension.js", publicImportPath: packageName },
|
||||
BinaryReadOptions: { typeOnly: true, privateImportPath: "./binary-format.js", publicImportPath: packageName },
|
||||
BinaryWriteOptions: { typeOnly: true, privateImportPath: "./binary-format.js", publicImportPath: packageName },
|
||||
JsonReadOptions: { typeOnly: true, privateImportPath: "./json-format.js", publicImportPath: packageName },
|
||||
JsonWriteOptions: { typeOnly: true, privateImportPath: "./json-format.js", publicImportPath: packageName },
|
||||
JsonValue: { typeOnly: true, privateImportPath: "./json-format.js", publicImportPath: packageName },
|
||||
JsonObject: { typeOnly: true, privateImportPath: "./json-format.js", publicImportPath: packageName },
|
||||
protoDouble: { typeOnly: false, privateImportPath: "./proto-double.js", publicImportPath: packageName },
|
||||
protoInt64: { typeOnly: false, privateImportPath: "./proto-int64.js", publicImportPath: packageName },
|
||||
ScalarType: { typeOnly: false, privateImportPath: "./scalar.js", publicImportPath: packageName },
|
||||
LongType: { typeOnly: false, privateImportPath: "./scalar.js", publicImportPath: packageName },
|
||||
MethodKind: { typeOnly: false, privateImportPath: "./service-type.js", publicImportPath: packageName },
|
||||
MethodIdempotency: { typeOnly: false, privateImportPath: "./service-type.js", publicImportPath: packageName },
|
||||
IMessageTypeRegistry: { typeOnly: true, privateImportPath: "./type-registry.js", publicImportPath: packageName },
|
||||
},
|
||||
wktSourceFiles: [
|
||||
"google/protobuf/compiler/plugin.proto",
|
||||
"google/protobuf/any.proto",
|
||||
"google/protobuf/api.proto",
|
||||
"google/protobuf/descriptor.proto",
|
||||
"google/protobuf/duration.proto",
|
||||
"google/protobuf/empty.proto",
|
||||
"google/protobuf/field_mask.proto",
|
||||
"google/protobuf/source_context.proto",
|
||||
"google/protobuf/struct.proto",
|
||||
"google/protobuf/timestamp.proto",
|
||||
"google/protobuf/type.proto",
|
||||
"google/protobuf/wrappers.proto",
|
||||
],
|
||||
};
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import { FeatureSetDefaults, FileDescriptorProto, FileDescriptorSet } from "./google/protobuf/descriptor_pb.js";
|
||||
import type { DescriptorSet } from "./descriptor-set.js";
|
||||
import type { BinaryReadOptions, BinaryWriteOptions } from "./binary-format.js";
|
||||
/**
|
||||
* Create a DescriptorSet, a convenient interface for working with a set of
|
||||
* google.protobuf.FileDescriptorProto.
|
||||
*
|
||||
* Note that files must be given in topological order, so each file appears
|
||||
* before any file that imports it. Protocol buffer compilers always produce
|
||||
* files in topological order.
|
||||
*/
|
||||
export declare function createDescriptorSet(input: FileDescriptorProto[] | FileDescriptorSet | Uint8Array, options?: CreateDescriptorSetOptions): DescriptorSet;
|
||||
/**
|
||||
* Options to createDescriptorSet()
|
||||
*/
|
||||
interface CreateDescriptorSetOptions {
|
||||
/**
|
||||
* Editions support language-specific features with extensions to
|
||||
* google.protobuf.FeatureSet. They can define defaults, and specify on
|
||||
* which targets the features can be set.
|
||||
*
|
||||
* To create a DescriptorSet that provides your language-specific features,
|
||||
* you have to provide a google.protobuf.FeatureSetDefaults message in this
|
||||
* option. It can also specify the minimum and maximum supported edition.
|
||||
*
|
||||
* The defaults can be generated with `protoc` - see the flag
|
||||
* `--experimental_edition_defaults_out`.
|
||||
*/
|
||||
featureSetDefaults?: FeatureSetDefaults;
|
||||
/**
|
||||
* Internally, data is serialized when features are resolved. The
|
||||
* serialization options given here will be used for feature resolution.
|
||||
*/
|
||||
serializationOptions?: Partial<BinaryReadOptions & BinaryWriteOptions>;
|
||||
}
|
||||
export {};
|
||||
+910
@@ -0,0 +1,910 @@
|
||||
"use strict";
|
||||
// Copyright 2021-2024 Buf Technologies, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createDescriptorSet = void 0;
|
||||
const descriptor_pb_js_1 = require("./google/protobuf/descriptor_pb.js");
|
||||
const assert_js_1 = require("./private/assert.js");
|
||||
const service_type_js_1 = require("./service-type.js");
|
||||
const names_js_1 = require("./private/names.js");
|
||||
const text_format_js_1 = require("./private/text-format.js");
|
||||
const feature_set_js_1 = require("./private/feature-set.js");
|
||||
const scalar_js_1 = require("./scalar.js");
|
||||
const is_message_js_1 = require("./is-message.js");
|
||||
/**
|
||||
* Create a DescriptorSet, a convenient interface for working with a set of
|
||||
* google.protobuf.FileDescriptorProto.
|
||||
*
|
||||
* Note that files must be given in topological order, so each file appears
|
||||
* before any file that imports it. Protocol buffer compilers always produce
|
||||
* files in topological order.
|
||||
*/
|
||||
function createDescriptorSet(input, options) {
|
||||
var _a;
|
||||
const cart = {
|
||||
files: [],
|
||||
enums: new Map(),
|
||||
messages: new Map(),
|
||||
services: new Map(),
|
||||
extensions: new Map(),
|
||||
mapEntries: new Map(),
|
||||
};
|
||||
const fileDescriptors = (0, is_message_js_1.isMessage)(input, descriptor_pb_js_1.FileDescriptorSet)
|
||||
? input.file
|
||||
: input instanceof Uint8Array
|
||||
? descriptor_pb_js_1.FileDescriptorSet.fromBinary(input).file
|
||||
: input;
|
||||
const resolverByEdition = new Map();
|
||||
for (const proto of fileDescriptors) {
|
||||
const edition = (_a = proto.edition) !== null && _a !== void 0 ? _a : parseFileSyntax(proto.syntax, proto.edition).edition;
|
||||
let resolveFeatures = resolverByEdition.get(edition);
|
||||
if (resolveFeatures === undefined) {
|
||||
resolveFeatures = (0, feature_set_js_1.createFeatureResolver)(edition, options === null || options === void 0 ? void 0 : options.featureSetDefaults, options === null || options === void 0 ? void 0 : options.serializationOptions);
|
||||
resolverByEdition.set(edition, resolveFeatures);
|
||||
}
|
||||
addFile(proto, cart, resolveFeatures);
|
||||
}
|
||||
return cart;
|
||||
}
|
||||
exports.createDescriptorSet = createDescriptorSet;
|
||||
/**
|
||||
* Create a descriptor for a file.
|
||||
*/
|
||||
function addFile(proto, cart, resolveFeatures) {
|
||||
var _a, _b;
|
||||
(0, assert_js_1.assert)(proto.name, `invalid FileDescriptorProto: missing name`);
|
||||
const file = Object.assign(Object.assign({ kind: "file", proto, deprecated: (_b = (_a = proto.options) === null || _a === void 0 ? void 0 : _a.deprecated) !== null && _b !== void 0 ? _b : false }, parseFileSyntax(proto.syntax, proto.edition)), { name: proto.name.replace(/\.proto/, ""), dependencies: findFileDependencies(proto, cart), enums: [], messages: [], extensions: [], services: [], toString() {
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions -- we asserted above
|
||||
return `file ${this.proto.name}`;
|
||||
},
|
||||
getSyntaxComments() {
|
||||
return findComments(this.proto.sourceCodeInfo, [
|
||||
FieldNumber.FileDescriptorProto_Syntax,
|
||||
]);
|
||||
},
|
||||
getPackageComments() {
|
||||
return findComments(this.proto.sourceCodeInfo, [
|
||||
FieldNumber.FileDescriptorProto_Package,
|
||||
]);
|
||||
},
|
||||
getFeatures() {
|
||||
var _a;
|
||||
return resolveFeatures((_a = proto.options) === null || _a === void 0 ? void 0 : _a.features);
|
||||
} });
|
||||
cart.mapEntries.clear(); // map entries are local to the file, we can safely discard
|
||||
for (const enumProto of proto.enumType) {
|
||||
addEnum(enumProto, file, undefined, cart, resolveFeatures);
|
||||
}
|
||||
for (const messageProto of proto.messageType) {
|
||||
addMessage(messageProto, file, undefined, cart, resolveFeatures);
|
||||
}
|
||||
for (const serviceProto of proto.service) {
|
||||
addService(serviceProto, file, cart, resolveFeatures);
|
||||
}
|
||||
addExtensions(file, cart, resolveFeatures);
|
||||
for (const mapEntry of cart.mapEntries.values()) {
|
||||
addFields(mapEntry, cart, resolveFeatures);
|
||||
}
|
||||
for (const message of file.messages) {
|
||||
addFields(message, cart, resolveFeatures);
|
||||
addExtensions(message, cart, resolveFeatures);
|
||||
}
|
||||
cart.mapEntries.clear(); // map entries are local to the file, we can safely discard
|
||||
cart.files.push(file);
|
||||
}
|
||||
/**
|
||||
* Create descriptors for extensions, and add them to the message / file,
|
||||
* and to our cart.
|
||||
* Recurses into nested types.
|
||||
*/
|
||||
function addExtensions(desc, cart, resolveFeatures) {
|
||||
switch (desc.kind) {
|
||||
case "file":
|
||||
for (const proto of desc.proto.extension) {
|
||||
const ext = newExtension(proto, desc, undefined, cart, resolveFeatures);
|
||||
desc.extensions.push(ext);
|
||||
cart.extensions.set(ext.typeName, ext);
|
||||
}
|
||||
break;
|
||||
case "message":
|
||||
for (const proto of desc.proto.extension) {
|
||||
const ext = newExtension(proto, desc.file, desc, cart, resolveFeatures);
|
||||
desc.nestedExtensions.push(ext);
|
||||
cart.extensions.set(ext.typeName, ext);
|
||||
}
|
||||
for (const message of desc.nestedMessages) {
|
||||
addExtensions(message, cart, resolveFeatures);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Create descriptors for fields and oneof groups, and add them to the message.
|
||||
* Recurses into nested types.
|
||||
*/
|
||||
function addFields(message, cart, resolveFeatures) {
|
||||
const allOneofs = message.proto.oneofDecl.map((proto) => newOneof(proto, message, resolveFeatures));
|
||||
const oneofsSeen = new Set();
|
||||
for (const proto of message.proto.field) {
|
||||
const oneof = findOneof(proto, allOneofs);
|
||||
const field = newField(proto, message.file, message, oneof, cart, resolveFeatures);
|
||||
message.fields.push(field);
|
||||
if (oneof === undefined) {
|
||||
message.members.push(field);
|
||||
}
|
||||
else {
|
||||
oneof.fields.push(field);
|
||||
if (!oneofsSeen.has(oneof)) {
|
||||
oneofsSeen.add(oneof);
|
||||
message.members.push(oneof);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const oneof of allOneofs.filter((o) => oneofsSeen.has(o))) {
|
||||
message.oneofs.push(oneof);
|
||||
}
|
||||
for (const child of message.nestedMessages) {
|
||||
addFields(child, cart, resolveFeatures);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Create a descriptor for an enumeration, and add it our cart and to the
|
||||
* parent type, if any.
|
||||
*/
|
||||
function addEnum(proto, file, parent, cart, resolveFeatures) {
|
||||
var _a, _b, _c;
|
||||
(0, assert_js_1.assert)(proto.name, `invalid EnumDescriptorProto: missing name`);
|
||||
const desc = {
|
||||
kind: "enum",
|
||||
proto,
|
||||
deprecated: (_b = (_a = proto.options) === null || _a === void 0 ? void 0 : _a.deprecated) !== null && _b !== void 0 ? _b : false,
|
||||
file,
|
||||
parent,
|
||||
name: proto.name,
|
||||
typeName: makeTypeName(proto, parent, file),
|
||||
values: [],
|
||||
sharedPrefix: (0, names_js_1.findEnumSharedPrefix)(proto.name, proto.value.map((v) => { var _a; return (_a = v.name) !== null && _a !== void 0 ? _a : ""; })),
|
||||
toString() {
|
||||
return `enum ${this.typeName}`;
|
||||
},
|
||||
getComments() {
|
||||
const path = this.parent
|
||||
? [
|
||||
...this.parent.getComments().sourcePath,
|
||||
FieldNumber.DescriptorProto_EnumType,
|
||||
this.parent.proto.enumType.indexOf(this.proto),
|
||||
]
|
||||
: [
|
||||
FieldNumber.FileDescriptorProto_EnumType,
|
||||
this.file.proto.enumType.indexOf(this.proto),
|
||||
];
|
||||
return findComments(file.proto.sourceCodeInfo, path);
|
||||
},
|
||||
getFeatures() {
|
||||
var _a, _b;
|
||||
return resolveFeatures((_a = parent === null || parent === void 0 ? void 0 : parent.getFeatures()) !== null && _a !== void 0 ? _a : file.getFeatures(), (_b = proto.options) === null || _b === void 0 ? void 0 : _b.features);
|
||||
},
|
||||
};
|
||||
cart.enums.set(desc.typeName, desc);
|
||||
proto.value.forEach((proto) => {
|
||||
var _a, _b;
|
||||
(0, assert_js_1.assert)(proto.name, `invalid EnumValueDescriptorProto: missing name`);
|
||||
(0, assert_js_1.assert)(proto.number !== undefined, `invalid EnumValueDescriptorProto: missing number`);
|
||||
desc.values.push({
|
||||
kind: "enum_value",
|
||||
proto,
|
||||
deprecated: (_b = (_a = proto.options) === null || _a === void 0 ? void 0 : _a.deprecated) !== null && _b !== void 0 ? _b : false,
|
||||
parent: desc,
|
||||
name: proto.name,
|
||||
number: proto.number,
|
||||
toString() {
|
||||
return `enum value ${desc.typeName}.${this.name}`;
|
||||
},
|
||||
declarationString() {
|
||||
var _a;
|
||||
let str = `${this.name} = ${this.number}`;
|
||||
if (((_a = this.proto.options) === null || _a === void 0 ? void 0 : _a.deprecated) === true) {
|
||||
str += " [deprecated = true]";
|
||||
}
|
||||
return str;
|
||||
},
|
||||
getComments() {
|
||||
const path = [
|
||||
...this.parent.getComments().sourcePath,
|
||||
FieldNumber.EnumDescriptorProto_Value,
|
||||
this.parent.proto.value.indexOf(this.proto),
|
||||
];
|
||||
return findComments(file.proto.sourceCodeInfo, path);
|
||||
},
|
||||
getFeatures() {
|
||||
var _a;
|
||||
return resolveFeatures(desc.getFeatures(), (_a = proto.options) === null || _a === void 0 ? void 0 : _a.features);
|
||||
},
|
||||
});
|
||||
});
|
||||
((_c = parent === null || parent === void 0 ? void 0 : parent.nestedEnums) !== null && _c !== void 0 ? _c : file.enums).push(desc);
|
||||
}
|
||||
/**
|
||||
* Create a descriptor for a message, including nested types, and add it to our
|
||||
* cart. Note that this does not create descriptors fields.
|
||||
*/
|
||||
function addMessage(proto, file, parent, cart, resolveFeatures) {
|
||||
var _a, _b, _c, _d;
|
||||
(0, assert_js_1.assert)(proto.name, `invalid DescriptorProto: missing name`);
|
||||
const desc = {
|
||||
kind: "message",
|
||||
proto,
|
||||
deprecated: (_b = (_a = proto.options) === null || _a === void 0 ? void 0 : _a.deprecated) !== null && _b !== void 0 ? _b : false,
|
||||
file,
|
||||
parent,
|
||||
name: proto.name,
|
||||
typeName: makeTypeName(proto, parent, file),
|
||||
fields: [],
|
||||
oneofs: [],
|
||||
members: [],
|
||||
nestedEnums: [],
|
||||
nestedMessages: [],
|
||||
nestedExtensions: [],
|
||||
toString() {
|
||||
return `message ${this.typeName}`;
|
||||
},
|
||||
getComments() {
|
||||
const path = this.parent
|
||||
? [
|
||||
...this.parent.getComments().sourcePath,
|
||||
FieldNumber.DescriptorProto_NestedType,
|
||||
this.parent.proto.nestedType.indexOf(this.proto),
|
||||
]
|
||||
: [
|
||||
FieldNumber.FileDescriptorProto_MessageType,
|
||||
this.file.proto.messageType.indexOf(this.proto),
|
||||
];
|
||||
return findComments(file.proto.sourceCodeInfo, path);
|
||||
},
|
||||
getFeatures() {
|
||||
var _a, _b;
|
||||
return resolveFeatures((_a = parent === null || parent === void 0 ? void 0 : parent.getFeatures()) !== null && _a !== void 0 ? _a : file.getFeatures(), (_b = proto.options) === null || _b === void 0 ? void 0 : _b.features);
|
||||
},
|
||||
};
|
||||
if (((_c = proto.options) === null || _c === void 0 ? void 0 : _c.mapEntry) === true) {
|
||||
cart.mapEntries.set(desc.typeName, desc);
|
||||
}
|
||||
else {
|
||||
((_d = parent === null || parent === void 0 ? void 0 : parent.nestedMessages) !== null && _d !== void 0 ? _d : file.messages).push(desc);
|
||||
cart.messages.set(desc.typeName, desc);
|
||||
}
|
||||
for (const enumProto of proto.enumType) {
|
||||
addEnum(enumProto, file, desc, cart, resolveFeatures);
|
||||
}
|
||||
for (const messageProto of proto.nestedType) {
|
||||
addMessage(messageProto, file, desc, cart, resolveFeatures);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Create a descriptor for a service, including methods, and add it to our
|
||||
* cart.
|
||||
*/
|
||||
function addService(proto, file, cart, resolveFeatures) {
|
||||
var _a, _b;
|
||||
(0, assert_js_1.assert)(proto.name, `invalid ServiceDescriptorProto: missing name`);
|
||||
const desc = {
|
||||
kind: "service",
|
||||
proto,
|
||||
deprecated: (_b = (_a = proto.options) === null || _a === void 0 ? void 0 : _a.deprecated) !== null && _b !== void 0 ? _b : false,
|
||||
file,
|
||||
name: proto.name,
|
||||
typeName: makeTypeName(proto, undefined, file),
|
||||
methods: [],
|
||||
toString() {
|
||||
return `service ${this.typeName}`;
|
||||
},
|
||||
getComments() {
|
||||
const path = [
|
||||
FieldNumber.FileDescriptorProto_Service,
|
||||
this.file.proto.service.indexOf(this.proto),
|
||||
];
|
||||
return findComments(file.proto.sourceCodeInfo, path);
|
||||
},
|
||||
getFeatures() {
|
||||
var _a;
|
||||
return resolveFeatures(file.getFeatures(), (_a = proto.options) === null || _a === void 0 ? void 0 : _a.features);
|
||||
},
|
||||
};
|
||||
file.services.push(desc);
|
||||
cart.services.set(desc.typeName, desc);
|
||||
for (const methodProto of proto.method) {
|
||||
desc.methods.push(newMethod(methodProto, desc, cart, resolveFeatures));
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Create a descriptor for a method.
|
||||
*/
|
||||
function newMethod(proto, parent, cart, resolveFeatures) {
|
||||
var _a, _b, _c;
|
||||
(0, assert_js_1.assert)(proto.name, `invalid MethodDescriptorProto: missing name`);
|
||||
(0, assert_js_1.assert)(proto.inputType, `invalid MethodDescriptorProto: missing input_type`);
|
||||
(0, assert_js_1.assert)(proto.outputType, `invalid MethodDescriptorProto: missing output_type`);
|
||||
let methodKind;
|
||||
if (proto.clientStreaming === true && proto.serverStreaming === true) {
|
||||
methodKind = service_type_js_1.MethodKind.BiDiStreaming;
|
||||
}
|
||||
else if (proto.clientStreaming === true) {
|
||||
methodKind = service_type_js_1.MethodKind.ClientStreaming;
|
||||
}
|
||||
else if (proto.serverStreaming === true) {
|
||||
methodKind = service_type_js_1.MethodKind.ServerStreaming;
|
||||
}
|
||||
else {
|
||||
methodKind = service_type_js_1.MethodKind.Unary;
|
||||
}
|
||||
let idempotency;
|
||||
switch ((_a = proto.options) === null || _a === void 0 ? void 0 : _a.idempotencyLevel) {
|
||||
case descriptor_pb_js_1.MethodOptions_IdempotencyLevel.IDEMPOTENT:
|
||||
idempotency = service_type_js_1.MethodIdempotency.Idempotent;
|
||||
break;
|
||||
case descriptor_pb_js_1.MethodOptions_IdempotencyLevel.NO_SIDE_EFFECTS:
|
||||
idempotency = service_type_js_1.MethodIdempotency.NoSideEffects;
|
||||
break;
|
||||
case descriptor_pb_js_1.MethodOptions_IdempotencyLevel.IDEMPOTENCY_UNKNOWN:
|
||||
case undefined:
|
||||
idempotency = undefined;
|
||||
break;
|
||||
}
|
||||
const input = cart.messages.get(trimLeadingDot(proto.inputType));
|
||||
const output = cart.messages.get(trimLeadingDot(proto.outputType));
|
||||
(0, assert_js_1.assert)(input, `invalid MethodDescriptorProto: input_type ${proto.inputType} not found`);
|
||||
(0, assert_js_1.assert)(output, `invalid MethodDescriptorProto: output_type ${proto.inputType} not found`);
|
||||
const name = proto.name;
|
||||
return {
|
||||
kind: "rpc",
|
||||
proto,
|
||||
deprecated: (_c = (_b = proto.options) === null || _b === void 0 ? void 0 : _b.deprecated) !== null && _c !== void 0 ? _c : false,
|
||||
parent,
|
||||
name,
|
||||
methodKind,
|
||||
input,
|
||||
output,
|
||||
idempotency,
|
||||
toString() {
|
||||
return `rpc ${parent.typeName}.${name}`;
|
||||
},
|
||||
getComments() {
|
||||
const path = [
|
||||
...this.parent.getComments().sourcePath,
|
||||
FieldNumber.ServiceDescriptorProto_Method,
|
||||
this.parent.proto.method.indexOf(this.proto),
|
||||
];
|
||||
return findComments(parent.file.proto.sourceCodeInfo, path);
|
||||
},
|
||||
getFeatures() {
|
||||
var _a;
|
||||
return resolveFeatures(parent.getFeatures(), (_a = proto.options) === null || _a === void 0 ? void 0 : _a.features);
|
||||
},
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Create a descriptor for a oneof group.
|
||||
*/
|
||||
function newOneof(proto, parent, resolveFeatures) {
|
||||
(0, assert_js_1.assert)(proto.name, `invalid OneofDescriptorProto: missing name`);
|
||||
return {
|
||||
kind: "oneof",
|
||||
proto,
|
||||
deprecated: false,
|
||||
parent,
|
||||
fields: [],
|
||||
name: proto.name,
|
||||
toString() {
|
||||
return `oneof ${parent.typeName}.${this.name}`;
|
||||
},
|
||||
getComments() {
|
||||
const path = [
|
||||
...this.parent.getComments().sourcePath,
|
||||
FieldNumber.DescriptorProto_OneofDecl,
|
||||
this.parent.proto.oneofDecl.indexOf(this.proto),
|
||||
];
|
||||
return findComments(parent.file.proto.sourceCodeInfo, path);
|
||||
},
|
||||
getFeatures() {
|
||||
var _a;
|
||||
return resolveFeatures(parent.getFeatures(), (_a = proto.options) === null || _a === void 0 ? void 0 : _a.features);
|
||||
},
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Create a descriptor for a field.
|
||||
*/
|
||||
function newField(proto, file, parent, oneof, cart, resolveFeatures) {
|
||||
var _a, _b, _c;
|
||||
(0, assert_js_1.assert)(proto.name, `invalid FieldDescriptorProto: missing name`);
|
||||
(0, assert_js_1.assert)(proto.number, `invalid FieldDescriptorProto: missing number`);
|
||||
(0, assert_js_1.assert)(proto.type, `invalid FieldDescriptorProto: missing type`);
|
||||
const common = {
|
||||
proto,
|
||||
deprecated: (_b = (_a = proto.options) === null || _a === void 0 ? void 0 : _a.deprecated) !== null && _b !== void 0 ? _b : false,
|
||||
name: proto.name,
|
||||
number: proto.number,
|
||||
parent,
|
||||
oneof,
|
||||
optional: isOptionalField(proto, file.syntax),
|
||||
packedByDefault: isPackedFieldByDefault(proto, resolveFeatures),
|
||||
packed: isPackedField(file, parent, proto, resolveFeatures),
|
||||
jsonName: proto.jsonName === (0, names_js_1.fieldJsonName)(proto.name) ? undefined : proto.jsonName,
|
||||
scalar: undefined,
|
||||
longType: undefined,
|
||||
message: undefined,
|
||||
enum: undefined,
|
||||
mapKey: undefined,
|
||||
mapValue: undefined,
|
||||
declarationString,
|
||||
// toString, getComments, getFeatures are overridden in newExtension
|
||||
toString() {
|
||||
return `field ${this.parent.typeName}.${this.name}`;
|
||||
},
|
||||
getComments() {
|
||||
const path = [
|
||||
...this.parent.getComments().sourcePath,
|
||||
FieldNumber.DescriptorProto_Field,
|
||||
this.parent.proto.field.indexOf(this.proto),
|
||||
];
|
||||
return findComments(file.proto.sourceCodeInfo, path);
|
||||
},
|
||||
getFeatures() {
|
||||
var _a;
|
||||
return resolveFeatures(parent.getFeatures(), (_a = proto.options) === null || _a === void 0 ? void 0 : _a.features);
|
||||
},
|
||||
};
|
||||
const repeated = proto.label === descriptor_pb_js_1.FieldDescriptorProto_Label.REPEATED;
|
||||
switch (proto.type) {
|
||||
case descriptor_pb_js_1.FieldDescriptorProto_Type.MESSAGE:
|
||||
case descriptor_pb_js_1.FieldDescriptorProto_Type.GROUP: {
|
||||
(0, assert_js_1.assert)(proto.typeName, `invalid FieldDescriptorProto: missing type_name`);
|
||||
const mapEntry = cart.mapEntries.get(trimLeadingDot(proto.typeName));
|
||||
if (mapEntry !== undefined) {
|
||||
(0, assert_js_1.assert)(repeated, `invalid FieldDescriptorProto: expected map entry to be repeated`);
|
||||
return Object.assign(Object.assign(Object.assign({}, common), { kind: "field", fieldKind: "map", repeated: false }), getMapFieldTypes(mapEntry));
|
||||
}
|
||||
const message = cart.messages.get(trimLeadingDot(proto.typeName));
|
||||
(0, assert_js_1.assert)(message !== undefined, `invalid FieldDescriptorProto: type_name ${proto.typeName} not found`);
|
||||
return Object.assign(Object.assign({}, common), { kind: "field", fieldKind: "message", repeated,
|
||||
message });
|
||||
}
|
||||
case descriptor_pb_js_1.FieldDescriptorProto_Type.ENUM: {
|
||||
(0, assert_js_1.assert)(proto.typeName, `invalid FieldDescriptorProto: missing type_name`);
|
||||
const e = cart.enums.get(trimLeadingDot(proto.typeName));
|
||||
(0, assert_js_1.assert)(e !== undefined, `invalid FieldDescriptorProto: type_name ${proto.typeName} not found`);
|
||||
return Object.assign(Object.assign({}, common), { kind: "field", fieldKind: "enum", getDefaultValue,
|
||||
repeated, enum: e });
|
||||
}
|
||||
default: {
|
||||
const scalar = fieldTypeToScalarType[proto.type];
|
||||
(0, assert_js_1.assert)(scalar, `invalid FieldDescriptorProto: unknown type ${proto.type}`);
|
||||
return Object.assign(Object.assign({}, common), { kind: "field", fieldKind: "scalar", getDefaultValue,
|
||||
repeated,
|
||||
scalar, longType: ((_c = proto.options) === null || _c === void 0 ? void 0 : _c.jstype) == descriptor_pb_js_1.FieldOptions_JSType.JS_STRING
|
||||
? scalar_js_1.LongType.STRING
|
||||
: scalar_js_1.LongType.BIGINT });
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Create a descriptor for an extension field.
|
||||
*/
|
||||
function newExtension(proto, file, parent, cart, resolveFeatures) {
|
||||
(0, assert_js_1.assert)(proto.extendee, `invalid FieldDescriptorProto: missing extendee`);
|
||||
const field = newField(proto, file, null, // to safe us many lines of duplicated code, we trick the type system
|
||||
undefined, cart, resolveFeatures);
|
||||
const extendee = cart.messages.get(trimLeadingDot(proto.extendee));
|
||||
(0, assert_js_1.assert)(extendee, `invalid FieldDescriptorProto: extendee ${proto.extendee} not found`);
|
||||
return Object.assign(Object.assign({}, field), { kind: "extension", typeName: makeTypeName(proto, parent, file), parent,
|
||||
file,
|
||||
extendee,
|
||||
// Must override toString, getComments, getFeatures from newField, because we
|
||||
// call newField with parent undefined.
|
||||
toString() {
|
||||
return `extension ${this.typeName}`;
|
||||
},
|
||||
getComments() {
|
||||
const path = this.parent
|
||||
? [
|
||||
...this.parent.getComments().sourcePath,
|
||||
FieldNumber.DescriptorProto_Extension,
|
||||
this.parent.proto.extension.indexOf(proto),
|
||||
]
|
||||
: [
|
||||
FieldNumber.FileDescriptorProto_Extension,
|
||||
this.file.proto.extension.indexOf(proto),
|
||||
];
|
||||
return findComments(file.proto.sourceCodeInfo, path);
|
||||
},
|
||||
getFeatures() {
|
||||
var _a;
|
||||
return resolveFeatures((parent !== null && parent !== void 0 ? parent : file).getFeatures(), (_a = proto.options) === null || _a === void 0 ? void 0 : _a.features);
|
||||
} });
|
||||
}
|
||||
/**
|
||||
* Parse the "syntax" and "edition" fields, stripping test editions.
|
||||
*/
|
||||
function parseFileSyntax(syntax, edition) {
|
||||
let e;
|
||||
let s;
|
||||
switch (syntax) {
|
||||
case undefined:
|
||||
case "proto2":
|
||||
s = "proto2";
|
||||
e = descriptor_pb_js_1.Edition.EDITION_PROTO2;
|
||||
break;
|
||||
case "proto3":
|
||||
s = "proto3";
|
||||
e = descriptor_pb_js_1.Edition.EDITION_PROTO3;
|
||||
break;
|
||||
case "editions":
|
||||
s = "editions";
|
||||
switch (edition) {
|
||||
case undefined:
|
||||
case descriptor_pb_js_1.Edition.EDITION_1_TEST_ONLY:
|
||||
case descriptor_pb_js_1.Edition.EDITION_2_TEST_ONLY:
|
||||
case descriptor_pb_js_1.Edition.EDITION_99997_TEST_ONLY:
|
||||
case descriptor_pb_js_1.Edition.EDITION_99998_TEST_ONLY:
|
||||
case descriptor_pb_js_1.Edition.EDITION_99999_TEST_ONLY:
|
||||
case descriptor_pb_js_1.Edition.EDITION_UNKNOWN:
|
||||
e = descriptor_pb_js_1.Edition.EDITION_UNKNOWN;
|
||||
break;
|
||||
default:
|
||||
e = edition;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error(`invalid FileDescriptorProto: unsupported syntax: ${syntax}`);
|
||||
}
|
||||
if (syntax === "editions" && edition === descriptor_pb_js_1.Edition.EDITION_UNKNOWN) {
|
||||
throw new Error(`invalid FileDescriptorProto: syntax ${syntax} cannot have edition ${String(edition)}`);
|
||||
}
|
||||
return {
|
||||
syntax: s,
|
||||
edition: e,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Resolve dependencies of FileDescriptorProto to DescFile.
|
||||
*/
|
||||
function findFileDependencies(proto, cart) {
|
||||
return proto.dependency.map((wantName) => {
|
||||
const dep = cart.files.find((f) => f.proto.name === wantName);
|
||||
(0, assert_js_1.assert)(dep);
|
||||
return dep;
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Create a fully qualified name for a protobuf type or extension field.
|
||||
*
|
||||
* The fully qualified name for messages, enumerations, and services is
|
||||
* constructed by concatenating the package name (if present), parent
|
||||
* message names (for nested types), and the type name. We omit the leading
|
||||
* dot added by protobuf compilers. Examples:
|
||||
* - mypackage.MyMessage
|
||||
* - mypackage.MyMessage.NestedMessage
|
||||
*
|
||||
* The fully qualified name for extension fields is constructed by
|
||||
* concatenating the package name (if present), parent message names (for
|
||||
* extensions declared within a message), and the field name. Examples:
|
||||
* - mypackage.extfield
|
||||
* - mypackage.MyMessage.extfield
|
||||
*/
|
||||
function makeTypeName(proto, parent, file) {
|
||||
(0, assert_js_1.assert)(proto.name, `invalid ${proto.getType().typeName}: missing name`);
|
||||
let typeName;
|
||||
if (parent) {
|
||||
typeName = `${parent.typeName}.${proto.name}`;
|
||||
}
|
||||
else if (file.proto.package !== undefined) {
|
||||
typeName = `${file.proto.package}.${proto.name}`;
|
||||
}
|
||||
else {
|
||||
typeName = `${proto.name}`;
|
||||
}
|
||||
return typeName;
|
||||
}
|
||||
/**
|
||||
* Remove the leading dot from a fully qualified type name.
|
||||
*/
|
||||
function trimLeadingDot(typeName) {
|
||||
return typeName.startsWith(".") ? typeName.substring(1) : typeName;
|
||||
}
|
||||
function getMapFieldTypes(mapEntry) {
|
||||
var _a, _b;
|
||||
(0, assert_js_1.assert)((_a = mapEntry.proto.options) === null || _a === void 0 ? void 0 : _a.mapEntry, `invalid DescriptorProto: expected ${mapEntry.toString()} to be a map entry`);
|
||||
(0, assert_js_1.assert)(mapEntry.fields.length === 2, `invalid DescriptorProto: map entry ${mapEntry.toString()} has ${mapEntry.fields.length} fields`);
|
||||
const keyField = mapEntry.fields.find((f) => f.proto.number === 1);
|
||||
(0, assert_js_1.assert)(keyField, `invalid DescriptorProto: map entry ${mapEntry.toString()} is missing key field`);
|
||||
const mapKey = keyField.scalar;
|
||||
(0, assert_js_1.assert)(mapKey !== undefined &&
|
||||
mapKey !== scalar_js_1.ScalarType.BYTES &&
|
||||
mapKey !== scalar_js_1.ScalarType.FLOAT &&
|
||||
mapKey !== scalar_js_1.ScalarType.DOUBLE, `invalid DescriptorProto: map entry ${mapEntry.toString()} has unexpected key type ${(_b = keyField.proto.type) !== null && _b !== void 0 ? _b : -1}`);
|
||||
const valueField = mapEntry.fields.find((f) => f.proto.number === 2);
|
||||
(0, assert_js_1.assert)(valueField, `invalid DescriptorProto: map entry ${mapEntry.toString()} is missing value field`);
|
||||
switch (valueField.fieldKind) {
|
||||
case "scalar":
|
||||
return {
|
||||
mapKey,
|
||||
mapValue: Object.assign(Object.assign({}, valueField), { kind: "scalar" }),
|
||||
};
|
||||
case "message":
|
||||
return {
|
||||
mapKey,
|
||||
mapValue: Object.assign(Object.assign({}, valueField), { kind: "message" }),
|
||||
};
|
||||
case "enum":
|
||||
return {
|
||||
mapKey,
|
||||
mapValue: Object.assign(Object.assign({}, valueField), { kind: "enum" }),
|
||||
};
|
||||
default:
|
||||
throw new Error("invalid DescriptorProto: unsupported map entry value field");
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Did the user put the field in a oneof group?
|
||||
* This handles proto3 optionals.
|
||||
*/
|
||||
function findOneof(proto, allOneofs) {
|
||||
var _a;
|
||||
const oneofIndex = proto.oneofIndex;
|
||||
if (oneofIndex === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
let oneof;
|
||||
if (proto.proto3Optional !== true) {
|
||||
oneof = allOneofs[oneofIndex];
|
||||
(0, assert_js_1.assert)(oneof, `invalid FieldDescriptorProto: oneof #${oneofIndex} for field #${(_a = proto.number) !== null && _a !== void 0 ? _a : -1} not found`);
|
||||
}
|
||||
return oneof;
|
||||
}
|
||||
/**
|
||||
* Did the user use the `optional` keyword?
|
||||
* This handles proto3 optionals.
|
||||
*/
|
||||
function isOptionalField(proto, syntax) {
|
||||
switch (syntax) {
|
||||
case "proto2":
|
||||
return (proto.oneofIndex === undefined &&
|
||||
proto.label === descriptor_pb_js_1.FieldDescriptorProto_Label.OPTIONAL);
|
||||
case "proto3":
|
||||
return proto.proto3Optional === true;
|
||||
case "editions":
|
||||
return false;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Is this field packed by default? Only valid for repeated enum fields, and
|
||||
* for repeated scalar fields except BYTES and STRING.
|
||||
*
|
||||
* In proto3 syntax, fields are packed by default. In proto2 syntax, fields
|
||||
* are unpacked by default. With editions, the default is whatever the edition
|
||||
* specifies as a default. In edition 2023, fields are packed by default.
|
||||
*/
|
||||
function isPackedFieldByDefault(proto, resolveFeatures) {
|
||||
const { repeatedFieldEncoding } = resolveFeatures();
|
||||
if (repeatedFieldEncoding != descriptor_pb_js_1.FeatureSet_RepeatedFieldEncoding.PACKED) {
|
||||
return false;
|
||||
}
|
||||
// From the proto3 language guide:
|
||||
// > In proto3, repeated fields of scalar numeric types are packed by default.
|
||||
// This information is incomplete - according to the conformance tests, BOOL
|
||||
// and ENUM are packed by default as well. This means only STRING and BYTES
|
||||
// are not packed by default, which makes sense because they are length-delimited.
|
||||
switch (proto.type) {
|
||||
case descriptor_pb_js_1.FieldDescriptorProto_Type.STRING:
|
||||
case descriptor_pb_js_1.FieldDescriptorProto_Type.BYTES:
|
||||
case descriptor_pb_js_1.FieldDescriptorProto_Type.GROUP:
|
||||
case descriptor_pb_js_1.FieldDescriptorProto_Type.MESSAGE:
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Pack this repeated field?
|
||||
*
|
||||
* Respects field type, proto2/proto3 defaults and the `packed` option, or
|
||||
* edition defaults and the edition features.repeated_field_encoding options.
|
||||
*/
|
||||
function isPackedField(file, parent, proto, resolveFeatures) {
|
||||
var _a, _b, _c, _d, _e, _f;
|
||||
switch (proto.type) {
|
||||
case descriptor_pb_js_1.FieldDescriptorProto_Type.STRING:
|
||||
case descriptor_pb_js_1.FieldDescriptorProto_Type.BYTES:
|
||||
case descriptor_pb_js_1.FieldDescriptorProto_Type.GROUP:
|
||||
case descriptor_pb_js_1.FieldDescriptorProto_Type.MESSAGE:
|
||||
// length-delimited types cannot be packed
|
||||
return false;
|
||||
default:
|
||||
switch (file.edition) {
|
||||
case descriptor_pb_js_1.Edition.EDITION_PROTO2:
|
||||
return (_b = (_a = proto.options) === null || _a === void 0 ? void 0 : _a.packed) !== null && _b !== void 0 ? _b : false;
|
||||
case descriptor_pb_js_1.Edition.EDITION_PROTO3:
|
||||
return (_d = (_c = proto.options) === null || _c === void 0 ? void 0 : _c.packed) !== null && _d !== void 0 ? _d : true;
|
||||
default: {
|
||||
const { repeatedFieldEncoding } = resolveFeatures((_e = parent === null || parent === void 0 ? void 0 : parent.getFeatures()) !== null && _e !== void 0 ? _e : file.getFeatures(), (_f = proto.options) === null || _f === void 0 ? void 0 : _f.features);
|
||||
return (repeatedFieldEncoding == descriptor_pb_js_1.FeatureSet_RepeatedFieldEncoding.PACKED);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Map from a compiler-generated field type to our ScalarType, which is a
|
||||
* subset of field types declared by protobuf enum google.protobuf.FieldDescriptorProto.
|
||||
*/
|
||||
const fieldTypeToScalarType = {
|
||||
[descriptor_pb_js_1.FieldDescriptorProto_Type.DOUBLE]: scalar_js_1.ScalarType.DOUBLE,
|
||||
[descriptor_pb_js_1.FieldDescriptorProto_Type.FLOAT]: scalar_js_1.ScalarType.FLOAT,
|
||||
[descriptor_pb_js_1.FieldDescriptorProto_Type.INT64]: scalar_js_1.ScalarType.INT64,
|
||||
[descriptor_pb_js_1.FieldDescriptorProto_Type.UINT64]: scalar_js_1.ScalarType.UINT64,
|
||||
[descriptor_pb_js_1.FieldDescriptorProto_Type.INT32]: scalar_js_1.ScalarType.INT32,
|
||||
[descriptor_pb_js_1.FieldDescriptorProto_Type.FIXED64]: scalar_js_1.ScalarType.FIXED64,
|
||||
[descriptor_pb_js_1.FieldDescriptorProto_Type.FIXED32]: scalar_js_1.ScalarType.FIXED32,
|
||||
[descriptor_pb_js_1.FieldDescriptorProto_Type.BOOL]: scalar_js_1.ScalarType.BOOL,
|
||||
[descriptor_pb_js_1.FieldDescriptorProto_Type.STRING]: scalar_js_1.ScalarType.STRING,
|
||||
[descriptor_pb_js_1.FieldDescriptorProto_Type.GROUP]: undefined,
|
||||
[descriptor_pb_js_1.FieldDescriptorProto_Type.MESSAGE]: undefined,
|
||||
[descriptor_pb_js_1.FieldDescriptorProto_Type.BYTES]: scalar_js_1.ScalarType.BYTES,
|
||||
[descriptor_pb_js_1.FieldDescriptorProto_Type.UINT32]: scalar_js_1.ScalarType.UINT32,
|
||||
[descriptor_pb_js_1.FieldDescriptorProto_Type.ENUM]: undefined,
|
||||
[descriptor_pb_js_1.FieldDescriptorProto_Type.SFIXED32]: scalar_js_1.ScalarType.SFIXED32,
|
||||
[descriptor_pb_js_1.FieldDescriptorProto_Type.SFIXED64]: scalar_js_1.ScalarType.SFIXED64,
|
||||
[descriptor_pb_js_1.FieldDescriptorProto_Type.SINT32]: scalar_js_1.ScalarType.SINT32,
|
||||
[descriptor_pb_js_1.FieldDescriptorProto_Type.SINT64]: scalar_js_1.ScalarType.SINT64,
|
||||
};
|
||||
/**
|
||||
* Find comments.
|
||||
*/
|
||||
function findComments(sourceCodeInfo, sourcePath) {
|
||||
if (!sourceCodeInfo) {
|
||||
return {
|
||||
leadingDetached: [],
|
||||
sourcePath,
|
||||
};
|
||||
}
|
||||
for (const location of sourceCodeInfo.location) {
|
||||
if (location.path.length !== sourcePath.length) {
|
||||
continue;
|
||||
}
|
||||
if (location.path.some((value, index) => sourcePath[index] !== value)) {
|
||||
continue;
|
||||
}
|
||||
return {
|
||||
leadingDetached: location.leadingDetachedComments,
|
||||
leading: location.leadingComments,
|
||||
trailing: location.trailingComments,
|
||||
sourcePath,
|
||||
};
|
||||
}
|
||||
return {
|
||||
leadingDetached: [],
|
||||
sourcePath,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* The following field numbers are used to find comments in
|
||||
* google.protobuf.SourceCodeInfo.
|
||||
*/
|
||||
var FieldNumber;
|
||||
(function (FieldNumber) {
|
||||
FieldNumber[FieldNumber["FileDescriptorProto_Package"] = 2] = "FileDescriptorProto_Package";
|
||||
FieldNumber[FieldNumber["FileDescriptorProto_MessageType"] = 4] = "FileDescriptorProto_MessageType";
|
||||
FieldNumber[FieldNumber["FileDescriptorProto_EnumType"] = 5] = "FileDescriptorProto_EnumType";
|
||||
FieldNumber[FieldNumber["FileDescriptorProto_Service"] = 6] = "FileDescriptorProto_Service";
|
||||
FieldNumber[FieldNumber["FileDescriptorProto_Extension"] = 7] = "FileDescriptorProto_Extension";
|
||||
FieldNumber[FieldNumber["FileDescriptorProto_Syntax"] = 12] = "FileDescriptorProto_Syntax";
|
||||
FieldNumber[FieldNumber["DescriptorProto_Field"] = 2] = "DescriptorProto_Field";
|
||||
FieldNumber[FieldNumber["DescriptorProto_NestedType"] = 3] = "DescriptorProto_NestedType";
|
||||
FieldNumber[FieldNumber["DescriptorProto_EnumType"] = 4] = "DescriptorProto_EnumType";
|
||||
FieldNumber[FieldNumber["DescriptorProto_Extension"] = 6] = "DescriptorProto_Extension";
|
||||
FieldNumber[FieldNumber["DescriptorProto_OneofDecl"] = 8] = "DescriptorProto_OneofDecl";
|
||||
FieldNumber[FieldNumber["EnumDescriptorProto_Value"] = 2] = "EnumDescriptorProto_Value";
|
||||
FieldNumber[FieldNumber["ServiceDescriptorProto_Method"] = 2] = "ServiceDescriptorProto_Method";
|
||||
})(FieldNumber || (FieldNumber = {}));
|
||||
/**
|
||||
* Return a string that matches the definition of a field in the protobuf
|
||||
* source. Does not take custom options into account.
|
||||
*/
|
||||
function declarationString() {
|
||||
var _a, _b, _c;
|
||||
const parts = [];
|
||||
if (this.repeated) {
|
||||
parts.push("repeated");
|
||||
}
|
||||
if (this.optional) {
|
||||
parts.push("optional");
|
||||
}
|
||||
const file = this.kind === "extension" ? this.file : this.parent.file;
|
||||
if (file.syntax == "proto2" &&
|
||||
this.proto.label === descriptor_pb_js_1.FieldDescriptorProto_Label.REQUIRED) {
|
||||
parts.push("required");
|
||||
}
|
||||
let type;
|
||||
switch (this.fieldKind) {
|
||||
case "scalar":
|
||||
type = scalar_js_1.ScalarType[this.scalar].toLowerCase();
|
||||
break;
|
||||
case "enum":
|
||||
type = this.enum.typeName;
|
||||
break;
|
||||
case "message":
|
||||
type = this.message.typeName;
|
||||
break;
|
||||
case "map": {
|
||||
const k = scalar_js_1.ScalarType[this.mapKey].toLowerCase();
|
||||
let v;
|
||||
switch (this.mapValue.kind) {
|
||||
case "scalar":
|
||||
v = scalar_js_1.ScalarType[this.mapValue.scalar].toLowerCase();
|
||||
break;
|
||||
case "enum":
|
||||
v = this.mapValue.enum.typeName;
|
||||
break;
|
||||
case "message":
|
||||
v = this.mapValue.message.typeName;
|
||||
break;
|
||||
}
|
||||
type = `map<${k}, ${v}>`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
parts.push(`${type} ${this.name} = ${this.number}`);
|
||||
const options = [];
|
||||
if (((_a = this.proto.options) === null || _a === void 0 ? void 0 : _a.packed) !== undefined) {
|
||||
options.push(`packed = ${this.proto.options.packed.toString()}`);
|
||||
}
|
||||
let defaultValue = this.proto.defaultValue;
|
||||
if (defaultValue !== undefined) {
|
||||
if (this.proto.type == descriptor_pb_js_1.FieldDescriptorProto_Type.BYTES ||
|
||||
this.proto.type == descriptor_pb_js_1.FieldDescriptorProto_Type.STRING) {
|
||||
defaultValue = '"' + defaultValue.replace('"', '\\"') + '"';
|
||||
}
|
||||
options.push(`default = ${defaultValue}`);
|
||||
}
|
||||
if (this.jsonName !== undefined) {
|
||||
options.push(`json_name = "${this.jsonName}"`);
|
||||
}
|
||||
if (((_b = this.proto.options) === null || _b === void 0 ? void 0 : _b.jstype) !== undefined) {
|
||||
options.push(`jstype = ${descriptor_pb_js_1.FieldOptions_JSType[this.proto.options.jstype]}`);
|
||||
}
|
||||
if (((_c = this.proto.options) === null || _c === void 0 ? void 0 : _c.deprecated) === true) {
|
||||
options.push(`deprecated = true`);
|
||||
}
|
||||
if (options.length > 0) {
|
||||
parts.push("[" + options.join(", ") + "]");
|
||||
}
|
||||
return parts.join(" ");
|
||||
}
|
||||
/**
|
||||
* Parses a text-encoded default value (proto2) of a scalar or enum field.
|
||||
*/
|
||||
function getDefaultValue() {
|
||||
const d = this.proto.defaultValue;
|
||||
if (d === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
switch (this.fieldKind) {
|
||||
case "enum":
|
||||
return (0, text_format_js_1.parseTextFormatEnumValue)(this.enum, d);
|
||||
case "scalar":
|
||||
return (0, text_format_js_1.parseTextFormatScalarValue)(this.scalar, d);
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import type { IEnumTypeRegistry, IExtensionRegistry, IMessageTypeRegistry, IServiceTypeRegistry } from "./type-registry.js";
|
||||
import { FileDescriptorSet } from "./google/protobuf/descriptor_pb.js";
|
||||
import type { DescriptorSet } from "./descriptor-set.js";
|
||||
/**
|
||||
* Create a registry from a set of descriptors. The types returned by this
|
||||
* registry behave exactly like types from generated code.
|
||||
*
|
||||
* This function accepts google.protobuf.FileDescriptorSet in serialized or
|
||||
* deserialized form. Alternatively, it also accepts a DescriptorSet (see
|
||||
* createDescriptorSet()).
|
||||
*
|
||||
* By default, all well-known types with a specialized JSON representation
|
||||
* are replaced with their generated counterpart in this package.
|
||||
*/
|
||||
export declare function createRegistryFromDescriptors(input: DescriptorSet | FileDescriptorSet | Uint8Array, replaceWkt?: boolean): IMessageTypeRegistry & IEnumTypeRegistry & IExtensionRegistry & IServiceTypeRegistry;
|
||||
+264
@@ -0,0 +1,264 @@
|
||||
"use strict";
|
||||
// Copyright 2021-2024 Buf Technologies, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createRegistryFromDescriptors = void 0;
|
||||
const assert_js_1 = require("./private/assert.js");
|
||||
const proto3_js_1 = require("./proto3.js");
|
||||
const proto2_js_1 = require("./proto2.js");
|
||||
const names_js_1 = require("./private/names.js");
|
||||
const timestamp_pb_js_1 = require("./google/protobuf/timestamp_pb.js");
|
||||
const duration_pb_js_1 = require("./google/protobuf/duration_pb.js");
|
||||
const any_pb_js_1 = require("./google/protobuf/any_pb.js");
|
||||
const empty_pb_js_1 = require("./google/protobuf/empty_pb.js");
|
||||
const field_mask_pb_js_1 = require("./google/protobuf/field_mask_pb.js");
|
||||
const struct_pb_js_1 = require("./google/protobuf/struct_pb.js");
|
||||
const enum_js_1 = require("./private/enum.js");
|
||||
const wrappers_pb_js_1 = require("./google/protobuf/wrappers_pb.js");
|
||||
const descriptor_pb_js_1 = require("./google/protobuf/descriptor_pb.js");
|
||||
const create_descriptor_set_js_1 = require("./create-descriptor-set.js");
|
||||
const is_message_js_1 = require("./is-message.js");
|
||||
// well-known message types with specialized JSON representation
|
||||
const wkMessages = [
|
||||
any_pb_js_1.Any,
|
||||
duration_pb_js_1.Duration,
|
||||
empty_pb_js_1.Empty,
|
||||
field_mask_pb_js_1.FieldMask,
|
||||
struct_pb_js_1.Struct,
|
||||
struct_pb_js_1.Value,
|
||||
struct_pb_js_1.ListValue,
|
||||
timestamp_pb_js_1.Timestamp,
|
||||
duration_pb_js_1.Duration,
|
||||
wrappers_pb_js_1.DoubleValue,
|
||||
wrappers_pb_js_1.FloatValue,
|
||||
wrappers_pb_js_1.Int64Value,
|
||||
wrappers_pb_js_1.Int32Value,
|
||||
wrappers_pb_js_1.UInt32Value,
|
||||
wrappers_pb_js_1.UInt64Value,
|
||||
wrappers_pb_js_1.BoolValue,
|
||||
wrappers_pb_js_1.StringValue,
|
||||
wrappers_pb_js_1.BytesValue,
|
||||
];
|
||||
// well-known enum types with specialized JSON representation
|
||||
const wkEnums = [(0, enum_js_1.getEnumType)(struct_pb_js_1.NullValue)];
|
||||
/**
|
||||
* Create a registry from a set of descriptors. The types returned by this
|
||||
* registry behave exactly like types from generated code.
|
||||
*
|
||||
* This function accepts google.protobuf.FileDescriptorSet in serialized or
|
||||
* deserialized form. Alternatively, it also accepts a DescriptorSet (see
|
||||
* createDescriptorSet()).
|
||||
*
|
||||
* By default, all well-known types with a specialized JSON representation
|
||||
* are replaced with their generated counterpart in this package.
|
||||
*/
|
||||
function createRegistryFromDescriptors(input, replaceWkt = true) {
|
||||
const set = input instanceof Uint8Array || (0, is_message_js_1.isMessage)(input, descriptor_pb_js_1.FileDescriptorSet)
|
||||
? (0, create_descriptor_set_js_1.createDescriptorSet)(input)
|
||||
: input;
|
||||
const enums = new Map();
|
||||
const messages = new Map();
|
||||
const extensions = new Map();
|
||||
const extensionsByExtendee = new Map();
|
||||
const services = {};
|
||||
if (replaceWkt) {
|
||||
for (const mt of wkMessages) {
|
||||
messages.set(mt.typeName, mt);
|
||||
}
|
||||
for (const et of wkEnums) {
|
||||
enums.set(et.typeName, et);
|
||||
}
|
||||
}
|
||||
return {
|
||||
/**
|
||||
* May raise an error on invalid descriptors.
|
||||
*/
|
||||
findEnum(typeName) {
|
||||
const existing = enums.get(typeName);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
const desc = set.enums.get(typeName);
|
||||
if (!desc) {
|
||||
return undefined;
|
||||
}
|
||||
const runtime = desc.file.syntax == "proto3" ? proto3_js_1.proto3 : proto2_js_1.proto2;
|
||||
const type = runtime.makeEnumType(typeName, desc.values.map((u) => ({
|
||||
no: u.number,
|
||||
name: u.name,
|
||||
localName: (0, names_js_1.localName)(u),
|
||||
})), {});
|
||||
enums.set(typeName, type);
|
||||
return type;
|
||||
},
|
||||
/**
|
||||
* May raise an error on invalid descriptors.
|
||||
*/
|
||||
findMessage(typeName) {
|
||||
const existing = messages.get(typeName);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
const desc = set.messages.get(typeName);
|
||||
if (!desc) {
|
||||
return undefined;
|
||||
}
|
||||
const runtime = desc.file.syntax == "proto3" ? proto3_js_1.proto3 : proto2_js_1.proto2;
|
||||
const fields = [];
|
||||
const type = runtime.makeMessageType(typeName, () => fields, {
|
||||
localName: (0, names_js_1.localName)(desc),
|
||||
});
|
||||
messages.set(typeName, type);
|
||||
for (const field of desc.fields) {
|
||||
fields.push(makeFieldInfo(field, this));
|
||||
}
|
||||
return type;
|
||||
},
|
||||
/**
|
||||
* May raise an error on invalid descriptors.
|
||||
*/
|
||||
findService(typeName) {
|
||||
const existing = services[typeName];
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
const desc = set.services.get(typeName);
|
||||
if (!desc) {
|
||||
return undefined;
|
||||
}
|
||||
const methods = {};
|
||||
for (const method of desc.methods) {
|
||||
const I = resolve(method.input, this, method);
|
||||
const O = resolve(method.output, this, method);
|
||||
methods[(0, names_js_1.localName)(method)] = {
|
||||
name: method.name,
|
||||
I,
|
||||
O,
|
||||
kind: method.methodKind,
|
||||
idempotency: method.idempotency,
|
||||
// We do not surface options at this time
|
||||
// options: {},
|
||||
};
|
||||
}
|
||||
return (services[typeName] = {
|
||||
typeName: desc.typeName,
|
||||
methods,
|
||||
});
|
||||
},
|
||||
/**
|
||||
* May raise an error on invalid descriptors.
|
||||
*/
|
||||
findExtensionFor(typeName, no) {
|
||||
var _a;
|
||||
if (!set.messages.has(typeName)) {
|
||||
return undefined;
|
||||
}
|
||||
let extensionsByNo = extensionsByExtendee.get(typeName);
|
||||
if (!extensionsByNo) {
|
||||
// maintain a lookup for extension desc by number
|
||||
extensionsByNo = new Map();
|
||||
extensionsByExtendee.set(typeName, extensionsByNo);
|
||||
for (const desc of set.extensions.values()) {
|
||||
if (desc.extendee.typeName == typeName) {
|
||||
extensionsByNo.set(desc.number, desc);
|
||||
}
|
||||
}
|
||||
}
|
||||
const desc = (_a = extensionsByExtendee.get(typeName)) === null || _a === void 0 ? void 0 : _a.get(no);
|
||||
return desc ? this.findExtension(desc.typeName) : undefined;
|
||||
},
|
||||
/**
|
||||
* May raise an error on invalid descriptors.
|
||||
*/
|
||||
findExtension(typeName) {
|
||||
const existing = extensions.get(typeName);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
const desc = set.extensions.get(typeName);
|
||||
if (!desc) {
|
||||
return undefined;
|
||||
}
|
||||
const extendee = resolve(desc.extendee, this, desc);
|
||||
const runtime = desc.file.syntax == "proto3" ? proto3_js_1.proto3 : proto2_js_1.proto2;
|
||||
const ext = runtime.makeExtension(typeName, extendee, makeFieldInfo(desc, this));
|
||||
extensions.set(typeName, ext);
|
||||
return ext;
|
||||
},
|
||||
};
|
||||
}
|
||||
exports.createRegistryFromDescriptors = createRegistryFromDescriptors;
|
||||
function makeFieldInfo(desc, registry) {
|
||||
var _a;
|
||||
const f = {
|
||||
kind: desc.fieldKind,
|
||||
no: desc.number,
|
||||
name: desc.name,
|
||||
jsonName: desc.jsonName,
|
||||
delimited: desc.proto.type == descriptor_pb_js_1.FieldDescriptorProto_Type.GROUP,
|
||||
repeated: desc.repeated,
|
||||
packed: desc.packed,
|
||||
oneof: (_a = desc.oneof) === null || _a === void 0 ? void 0 : _a.name,
|
||||
opt: desc.optional,
|
||||
req: desc.proto.label === descriptor_pb_js_1.FieldDescriptorProto_Label.REQUIRED,
|
||||
};
|
||||
switch (desc.fieldKind) {
|
||||
case "map": {
|
||||
(0, assert_js_1.assert)(desc.kind == "field"); // maps are not allowed for extensions
|
||||
let T;
|
||||
switch (desc.mapValue.kind) {
|
||||
case "scalar":
|
||||
T = desc.mapValue.scalar;
|
||||
break;
|
||||
case "enum": {
|
||||
T = resolve(desc.mapValue.enum, registry, desc);
|
||||
break;
|
||||
}
|
||||
case "message": {
|
||||
T = resolve(desc.mapValue.message, registry, desc);
|
||||
break;
|
||||
}
|
||||
}
|
||||
f.K = desc.mapKey;
|
||||
f.V = {
|
||||
kind: desc.mapValue.kind,
|
||||
T,
|
||||
};
|
||||
break;
|
||||
}
|
||||
case "message": {
|
||||
f.T = resolve(desc.message, registry, desc);
|
||||
break;
|
||||
}
|
||||
case "enum": {
|
||||
f.T = resolve(desc.enum, registry, desc);
|
||||
f.default = desc.getDefaultValue();
|
||||
break;
|
||||
}
|
||||
case "scalar": {
|
||||
f.L = desc.longType;
|
||||
f.T = desc.scalar;
|
||||
f.default = desc.getDefaultValue();
|
||||
break;
|
||||
}
|
||||
}
|
||||
return f;
|
||||
}
|
||||
function resolve(desc, registry, context) {
|
||||
const type = desc.kind == "message"
|
||||
? registry.findMessage(desc.typeName)
|
||||
: registry.findEnum(desc.typeName);
|
||||
(0, assert_js_1.assert)(type, `${desc.toString()}" for ${context.toString()} not found`);
|
||||
return type;
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import type { MessageType } from "./message-type.js";
|
||||
import type { EnumType } from "./enum.js";
|
||||
import type { ServiceType } from "./service-type.js";
|
||||
import type { IEnumTypeRegistry, IExtensionRegistry, IMessageTypeRegistry, IMutableRegistry, IServiceTypeRegistry } from "./type-registry.js";
|
||||
import type { Extension } from "./extension.js";
|
||||
/**
|
||||
* Create a new registry from the given types.
|
||||
*/
|
||||
export declare function createRegistry(...types: Array<MessageType | EnumType | ServiceType | Extension>): IMessageTypeRegistry & IEnumTypeRegistry & IExtensionRegistry & IServiceTypeRegistry;
|
||||
/**
|
||||
* Create a mutable registry from the given types.
|
||||
*/
|
||||
export declare function createMutableRegistry(...types: Array<MessageType | EnumType | ServiceType | Extension>): IMutableRegistry;
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
"use strict";
|
||||
// Copyright 2021-2024 Buf Technologies, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createMutableRegistry = exports.createRegistry = void 0;
|
||||
/**
|
||||
* Create a new registry from the given types.
|
||||
*/
|
||||
function createRegistry(...types) {
|
||||
const mutable = createMutableRegistry(...types);
|
||||
delete mutable.add;
|
||||
return mutable;
|
||||
}
|
||||
exports.createRegistry = createRegistry;
|
||||
/**
|
||||
* Create a mutable registry from the given types.
|
||||
*/
|
||||
function createMutableRegistry(...types) {
|
||||
const messages = {};
|
||||
const enums = {};
|
||||
const services = {};
|
||||
const extensionsByName = new Map();
|
||||
const extensionsByExtendee = new Map();
|
||||
const registry = {
|
||||
findMessage(typeName) {
|
||||
return messages[typeName];
|
||||
},
|
||||
findEnum(typeName) {
|
||||
return enums[typeName];
|
||||
},
|
||||
findService(typeName) {
|
||||
return services[typeName];
|
||||
},
|
||||
findExtensionFor(typeName, no) {
|
||||
var _a, _b;
|
||||
return (_b = (_a = extensionsByExtendee.get(typeName)) === null || _a === void 0 ? void 0 : _a.get(no)) !== null && _b !== void 0 ? _b : undefined;
|
||||
},
|
||||
findExtension(typeName) {
|
||||
var _a;
|
||||
return (_a = extensionsByName.get(typeName)) !== null && _a !== void 0 ? _a : undefined;
|
||||
},
|
||||
add(type) {
|
||||
var _a;
|
||||
if ("fields" in type) {
|
||||
if (!this.findMessage(type.typeName)) {
|
||||
messages[type.typeName] = type;
|
||||
type.fields.list().forEach(addField);
|
||||
}
|
||||
}
|
||||
else if ("methods" in type) {
|
||||
if (!this.findService(type.typeName)) {
|
||||
services[type.typeName] = type;
|
||||
for (const method of Object.values(type.methods)) {
|
||||
this.add(method.I);
|
||||
this.add(method.O);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if ("extendee" in type) {
|
||||
if (!extensionsByName.has(type.typeName)) {
|
||||
extensionsByName.set(type.typeName, type);
|
||||
const extendeeName = type.extendee.typeName;
|
||||
if (!extensionsByExtendee.has(extendeeName)) {
|
||||
extensionsByExtendee.set(extendeeName, new Map());
|
||||
}
|
||||
(_a = extensionsByExtendee.get(extendeeName)) === null || _a === void 0 ? void 0 : _a.set(type.field.no, type);
|
||||
this.add(type.extendee);
|
||||
addField(type.field);
|
||||
}
|
||||
}
|
||||
else {
|
||||
enums[type.typeName] = type;
|
||||
}
|
||||
},
|
||||
};
|
||||
function addField(field) {
|
||||
if (field.kind == "message") {
|
||||
registry.add(field.T);
|
||||
}
|
||||
else if (field.kind == "map" && field.V.kind == "message") {
|
||||
registry.add(field.V.T);
|
||||
}
|
||||
else if (field.kind == "enum") {
|
||||
registry.add(field.T);
|
||||
}
|
||||
}
|
||||
for (const type of types) {
|
||||
registry.add(type);
|
||||
}
|
||||
return registry;
|
||||
}
|
||||
exports.createMutableRegistry = createMutableRegistry;
|
||||
+672
@@ -0,0 +1,672 @@
|
||||
import type { DescriptorProto, Edition, EnumDescriptorProto, EnumValueDescriptorProto, FieldDescriptorProto, FileDescriptorProto, MethodDescriptorProto, OneofDescriptorProto, ServiceDescriptorProto } from "./google/protobuf/descriptor_pb.js";
|
||||
import { LongType, ScalarType } from "./scalar.js";
|
||||
import type { MethodIdempotency, MethodKind } from "./service-type.js";
|
||||
import type { MergedFeatureSet } from "./private/feature-set.js";
|
||||
/**
|
||||
* DescriptorSet provides a convenient interface for working with a set
|
||||
* of google.protobuf.FileDescriptorProto.
|
||||
*
|
||||
* When protobuf sources are compiled, each file is parsed into a
|
||||
* google.protobuf.FileDescriptorProto. Those messages describe all parts
|
||||
* of the source file that are required to generate code for them.
|
||||
*
|
||||
* DescriptorSet resolves references between the descriptors, hides
|
||||
* implementation details like synthetic map entry messages, and provides
|
||||
* simple access to comments.
|
||||
*/
|
||||
export interface DescriptorSet {
|
||||
/**
|
||||
* All files, in the order they were added to the set.
|
||||
*/
|
||||
readonly files: DescFile[];
|
||||
/**
|
||||
* All enumerations, indexed by their fully qualified type name.
|
||||
* (We omit the leading dot.)
|
||||
*/
|
||||
readonly enums: ReadonlyMap<string, DescEnum>;
|
||||
/**
|
||||
* All messages, indexed by their fully qualified type name.
|
||||
* (We omit the leading dot.)
|
||||
*/
|
||||
readonly messages: ReadonlyMap<string, DescMessage>;
|
||||
/**
|
||||
* All services, indexed by their fully qualified type name.
|
||||
* (We omit the leading dot.)
|
||||
*/
|
||||
readonly services: ReadonlyMap<string, DescService>;
|
||||
/**
|
||||
* All extensions, indexed by their fully qualified type name.
|
||||
*/
|
||||
readonly extensions: ReadonlyMap<string, DescExtension>;
|
||||
}
|
||||
/**
|
||||
* A union of all descriptors, discriminated by a `kind` property.
|
||||
*/
|
||||
export type AnyDesc = DescFile | DescEnum | DescEnumValue | DescMessage | DescField | DescExtension | DescOneof | DescService | DescMethod;
|
||||
/**
|
||||
* Describes a protobuf source file.
|
||||
*/
|
||||
export interface DescFile {
|
||||
readonly kind: "file";
|
||||
/**
|
||||
* The syntax specified in the protobuf source.
|
||||
*/
|
||||
readonly syntax: "proto3" | "proto2" | "editions";
|
||||
/**
|
||||
* The edition of the protobuf file. Will be EDITION_PROTO2 for syntax="proto2",
|
||||
* EDITION_PROTO3 for syntax="proto3";
|
||||
*/
|
||||
readonly edition: Exclude<Edition, Edition.EDITION_1_TEST_ONLY | Edition.EDITION_2_TEST_ONLY | Edition.EDITION_99997_TEST_ONLY | Edition.EDITION_99998_TEST_ONLY | Edition.EDITION_99999_TEST_ONLY>;
|
||||
/**
|
||||
* The name of the file, excluding the .proto suffix.
|
||||
* For a protobuf file `foo/bar.proto`, this is `foo/bar`.
|
||||
*/
|
||||
readonly name: string;
|
||||
/**
|
||||
* Files imported by this file.
|
||||
*/
|
||||
readonly dependencies: DescFile[];
|
||||
/**
|
||||
* Top-level enumerations declared in this file.
|
||||
* Note that more enumerations might be declared within message declarations.
|
||||
*/
|
||||
readonly enums: DescEnum[];
|
||||
/**
|
||||
* Top-level messages declared in this file.
|
||||
* Note that more messages might be declared within message declarations.
|
||||
*/
|
||||
readonly messages: DescMessage[];
|
||||
/**
|
||||
* Top-level extensions declared in this file.
|
||||
* Note that more extensions might be declared within message declarations.
|
||||
*/
|
||||
readonly extensions: DescExtension[];
|
||||
/**
|
||||
* Services declared in this file.
|
||||
*/
|
||||
readonly services: DescService[];
|
||||
/**
|
||||
* Marked as deprecated in the protobuf source.
|
||||
*/
|
||||
readonly deprecated: boolean;
|
||||
/**
|
||||
* The compiler-generated descriptor.
|
||||
*/
|
||||
readonly proto: FileDescriptorProto;
|
||||
/**
|
||||
* Get comments on the syntax element in the protobuf source.
|
||||
*/
|
||||
getSyntaxComments(): DescComments;
|
||||
/**
|
||||
* Get comments on the package element in the protobuf source.
|
||||
*/
|
||||
getPackageComments(): DescComments;
|
||||
/**
|
||||
* Get the edition features for this protobuf element.
|
||||
*/
|
||||
getFeatures(): MergedFeatureSet;
|
||||
toString(): string;
|
||||
}
|
||||
/**
|
||||
* Describes an enumeration in a protobuf source file.
|
||||
*/
|
||||
export interface DescEnum {
|
||||
readonly kind: "enum";
|
||||
/**
|
||||
* The fully qualified name of the enumeration. (We omit the leading dot.)
|
||||
*/
|
||||
readonly typeName: string;
|
||||
/**
|
||||
* The name of the enumeration, as declared in the protobuf source.
|
||||
*/
|
||||
readonly name: string;
|
||||
/**
|
||||
* The file this enumeration was declared in.
|
||||
*/
|
||||
readonly file: DescFile;
|
||||
/**
|
||||
* The parent message, if this enumeration was declared inside a message declaration.
|
||||
*/
|
||||
readonly parent: DescMessage | undefined;
|
||||
/**
|
||||
* Values declared for this enumeration.
|
||||
*/
|
||||
readonly values: DescEnumValue[];
|
||||
/**
|
||||
* A prefix shared by all enum values.
|
||||
* For example, `MY_ENUM_` for `enum MyEnum {MY_ENUM_A=0; MY_ENUM_B=1;}`
|
||||
*/
|
||||
readonly sharedPrefix?: string;
|
||||
/**
|
||||
* Marked as deprecated in the protobuf source.
|
||||
*/
|
||||
readonly deprecated: boolean;
|
||||
/**
|
||||
* The compiler-generated descriptor.
|
||||
*/
|
||||
readonly proto: EnumDescriptorProto;
|
||||
/**
|
||||
* Get comments on the element in the protobuf source.
|
||||
*/
|
||||
getComments(): DescComments;
|
||||
/**
|
||||
* Get the edition features for this protobuf element.
|
||||
*/
|
||||
getFeatures(): MergedFeatureSet;
|
||||
toString(): string;
|
||||
}
|
||||
/**
|
||||
* Describes an individual value of an enumeration in a protobuf source file.
|
||||
*/
|
||||
export interface DescEnumValue {
|
||||
kind: "enum_value";
|
||||
/**
|
||||
* The name of the enumeration value, as specified in the protobuf source.
|
||||
*/
|
||||
readonly name: string;
|
||||
/**
|
||||
* The enumeration this value belongs to.
|
||||
*/
|
||||
readonly parent: DescEnum;
|
||||
/**
|
||||
* The numeric enumeration value, as specified in the protobuf source.
|
||||
*/
|
||||
readonly number: number;
|
||||
/**
|
||||
* Marked as deprecated in the protobuf source.
|
||||
*/
|
||||
readonly deprecated: boolean;
|
||||
/**
|
||||
* The compiler-generated descriptor.
|
||||
*/
|
||||
readonly proto: EnumValueDescriptorProto;
|
||||
/**
|
||||
* Return a string that (closely) matches the definition of the enumeration
|
||||
* value in the protobuf source.
|
||||
*/
|
||||
declarationString(): string;
|
||||
/**
|
||||
* Get comments on the element in the protobuf source.
|
||||
*/
|
||||
getComments(): DescComments;
|
||||
/**
|
||||
* Get the edition features for this protobuf element.
|
||||
*/
|
||||
getFeatures(): MergedFeatureSet;
|
||||
toString(): string;
|
||||
}
|
||||
/**
|
||||
* Describes a message declaration in a protobuf source file.
|
||||
*/
|
||||
export interface DescMessage {
|
||||
readonly kind: "message";
|
||||
/**
|
||||
* The fully qualified name of the message. (We omit the leading dot.)
|
||||
*/
|
||||
readonly typeName: string;
|
||||
/**
|
||||
* The name of the message, as specified in the protobuf source.
|
||||
*/
|
||||
readonly name: string;
|
||||
/**
|
||||
* The file this message was declared in.
|
||||
*/
|
||||
readonly file: DescFile;
|
||||
/**
|
||||
* The parent message, if this message was declared inside a message declaration.
|
||||
*/
|
||||
readonly parent: DescMessage | undefined;
|
||||
/**
|
||||
* Fields declared for this message, including fields declared in a oneof
|
||||
* group.
|
||||
*/
|
||||
readonly fields: DescField[];
|
||||
/**
|
||||
* Oneof groups declared for this message.
|
||||
* This does not include synthetic oneofs for proto3 optionals.
|
||||
*/
|
||||
readonly oneofs: DescOneof[];
|
||||
/**
|
||||
* Fields and oneof groups for this message, ordered by their appearance in the
|
||||
* protobuf source.
|
||||
*/
|
||||
readonly members: (DescField | DescOneof)[];
|
||||
/**
|
||||
* Enumerations declared within the message, if any.
|
||||
*/
|
||||
readonly nestedEnums: DescEnum[];
|
||||
/**
|
||||
* Messages declared within the message, if any.
|
||||
* This does not include synthetic messages like map entries.
|
||||
*/
|
||||
readonly nestedMessages: DescMessage[];
|
||||
/**
|
||||
* Extensions declared within the message, if any.
|
||||
*/
|
||||
readonly nestedExtensions: DescExtension[];
|
||||
/**
|
||||
* Marked as deprecated in the protobuf source.
|
||||
*/
|
||||
readonly deprecated: boolean;
|
||||
/**
|
||||
* The compiler-generated descriptor.
|
||||
*/
|
||||
readonly proto: DescriptorProto;
|
||||
/**
|
||||
* Get comments on the element in the protobuf source.
|
||||
*/
|
||||
getComments(): DescComments;
|
||||
/**
|
||||
* Get the edition features for this protobuf element.
|
||||
*/
|
||||
getFeatures(): MergedFeatureSet;
|
||||
toString(): string;
|
||||
}
|
||||
/**
|
||||
* Describes a field declaration in a protobuf source file.
|
||||
*/
|
||||
export type DescField = DescFieldCommon & (DescFieldScalar | DescFieldMessage | DescFieldEnum | DescFieldMap) & {
|
||||
readonly kind: "field";
|
||||
/**
|
||||
* The message this field is declared on.
|
||||
*/
|
||||
readonly parent: DescMessage;
|
||||
};
|
||||
/**
|
||||
* Describes an extension in a protobuf source file.
|
||||
*/
|
||||
export type DescExtension = DescFieldCommon & (DescFieldScalar | DescFieldMessage | DescFieldEnum | DescFieldMap) & {
|
||||
readonly kind: "extension";
|
||||
/**
|
||||
* The fully qualified name of the extension.
|
||||
*/
|
||||
readonly typeName: string;
|
||||
/**
|
||||
* The file this extension was declared in.
|
||||
*/
|
||||
readonly file: DescFile;
|
||||
/**
|
||||
* The parent message, if this extension was declared inside a message declaration.
|
||||
*/
|
||||
readonly parent: DescMessage | undefined;
|
||||
/**
|
||||
* The message that this extension extends.
|
||||
*/
|
||||
readonly extendee: DescMessage;
|
||||
};
|
||||
interface DescFieldCommon {
|
||||
/**
|
||||
* The field name, as specified in the protobuf source
|
||||
*/
|
||||
readonly name: string;
|
||||
/**
|
||||
* The field number, as specified in the protobuf source.
|
||||
*/
|
||||
readonly number: number;
|
||||
/**
|
||||
* The `oneof` group this field belongs to, if any.
|
||||
*/
|
||||
readonly oneof: DescOneof | undefined;
|
||||
/**
|
||||
* Whether this field was declared with `optional` in the protobuf source.
|
||||
*/
|
||||
readonly optional: boolean;
|
||||
/**
|
||||
* Pack this repeated field?
|
||||
*/
|
||||
readonly packed: boolean;
|
||||
/**
|
||||
* Is this field packed by default? Only valid for repeated enum fields, and
|
||||
* for repeated scalar fields except BYTES and STRING.
|
||||
*
|
||||
* In proto3 syntax, fields are packed by default. In proto2 syntax, fields
|
||||
* are unpacked by default.
|
||||
*
|
||||
* With editions, the default is whatever the edition specifies as a default.
|
||||
* In edition 2023, fields are packed by default.
|
||||
*/
|
||||
readonly packedByDefault: boolean;
|
||||
/**
|
||||
* A user-defined name for the JSON format, set with the field option
|
||||
* [json_name="foo"].
|
||||
*/
|
||||
readonly jsonName: string | undefined;
|
||||
/**
|
||||
* Marked as deprecated in the protobuf source.
|
||||
*/
|
||||
readonly deprecated: boolean;
|
||||
/**
|
||||
* The compiler-generated descriptor.
|
||||
*/
|
||||
readonly proto: FieldDescriptorProto;
|
||||
/**
|
||||
* Get comments on the element in the protobuf source.
|
||||
*/
|
||||
getComments(): DescComments;
|
||||
/**
|
||||
* Return a string that (closely) matches the definition of the field in the
|
||||
* protobuf source.
|
||||
*/
|
||||
declarationString(): string;
|
||||
/**
|
||||
* Get the edition features for this protobuf element.
|
||||
*/
|
||||
getFeatures(): MergedFeatureSet;
|
||||
toString(): string;
|
||||
}
|
||||
interface DescFieldScalar {
|
||||
readonly fieldKind: "scalar";
|
||||
/**
|
||||
* Is the field repeated?
|
||||
*/
|
||||
readonly repeated: boolean;
|
||||
/**
|
||||
* Scalar type, if it is a scalar field.
|
||||
*/
|
||||
readonly scalar: ScalarType;
|
||||
/**
|
||||
* JavaScript type for 64 bit integral types (int64, uint64,
|
||||
* sint64, fixed64, sfixed64).
|
||||
*/
|
||||
readonly longType: LongType;
|
||||
/**
|
||||
* The message type, if it is a message field.
|
||||
*/
|
||||
readonly message: undefined;
|
||||
/**
|
||||
* The enum type, if it is an enum field.
|
||||
*/
|
||||
readonly enum: undefined;
|
||||
/**
|
||||
* The map key type, if this is a map field.
|
||||
*/
|
||||
readonly mapKey: undefined;
|
||||
/**
|
||||
* The map value type, if this is a map field.
|
||||
*/
|
||||
readonly mapValue: undefined;
|
||||
/**
|
||||
* Return the default value specified in the protobuf source.
|
||||
* Only valid for proto2 syntax.
|
||||
*/
|
||||
getDefaultValue(): number | boolean | string | bigint | Uint8Array | undefined;
|
||||
}
|
||||
interface DescFieldMessage {
|
||||
readonly fieldKind: "message";
|
||||
/**
|
||||
* Is the field repeated?
|
||||
*/
|
||||
readonly repeated: boolean;
|
||||
/**
|
||||
* Scalar type, if it is a scalar field.
|
||||
*/
|
||||
readonly scalar: undefined;
|
||||
/**
|
||||
* JavaScript type for 64 bit integral types (int64, uint64,
|
||||
* sint64, fixed64, sfixed64).
|
||||
*/
|
||||
readonly longType: undefined;
|
||||
/**
|
||||
* The message type, if it is a message field.
|
||||
*/
|
||||
readonly message: DescMessage;
|
||||
/**
|
||||
* The enum type, if it is an enum field.
|
||||
*/
|
||||
readonly enum: undefined;
|
||||
/**
|
||||
* The map key type, if this is a map field.
|
||||
*/
|
||||
readonly mapKey: undefined;
|
||||
/**
|
||||
* The map value type, if this is a map field.
|
||||
*/
|
||||
readonly mapValue: undefined;
|
||||
}
|
||||
interface DescFieldEnum {
|
||||
readonly fieldKind: "enum";
|
||||
/**
|
||||
* Is the field repeated?
|
||||
*/
|
||||
readonly repeated: boolean;
|
||||
/**
|
||||
* Scalar type, if it is a scalar field.
|
||||
*/
|
||||
readonly scalar: undefined;
|
||||
/**
|
||||
* JavaScript type for 64 bit integral types (int64, uint64,
|
||||
* sint64, fixed64, sfixed64).
|
||||
*/
|
||||
readonly longType: undefined;
|
||||
/**
|
||||
* The message type, if it is a message field.
|
||||
*/
|
||||
readonly message: undefined;
|
||||
/**
|
||||
* The enum type, if it is an enum field.
|
||||
*/
|
||||
readonly enum: DescEnum;
|
||||
/**
|
||||
* The map key type, if this is a map field.
|
||||
*/
|
||||
readonly mapKey: undefined;
|
||||
/**
|
||||
* The map value type, if this is a map field.
|
||||
*/
|
||||
readonly mapValue: undefined;
|
||||
/**
|
||||
* Return the default value specified in the protobuf source.
|
||||
* Only valid for proto2 syntax.
|
||||
*/
|
||||
getDefaultValue(): number | boolean | string | bigint | Uint8Array | undefined;
|
||||
}
|
||||
interface DescFieldMap {
|
||||
readonly fieldKind: "map";
|
||||
/**
|
||||
* Is the field repeated?
|
||||
*/
|
||||
readonly repeated: false;
|
||||
/**
|
||||
* Scalar type, if it is a scalar field.
|
||||
*/
|
||||
readonly scalar: undefined;
|
||||
/**
|
||||
* JavaScript type for 64 bit integral types (int64, uint64,
|
||||
* sint64, fixed64, sfixed64).
|
||||
*/
|
||||
readonly longType: undefined;
|
||||
/**
|
||||
* The message type, if it is a message field.
|
||||
*/
|
||||
readonly message: undefined;
|
||||
/**
|
||||
* The enum type, if it is an enum field.
|
||||
*/
|
||||
readonly enum: undefined;
|
||||
/**
|
||||
* The map key type, if this is a map field.
|
||||
*/
|
||||
readonly mapKey: Exclude<ScalarType, ScalarType.FLOAT | ScalarType.DOUBLE | ScalarType.BYTES>;
|
||||
/**
|
||||
* The map value type, if this is a map field.
|
||||
*/
|
||||
readonly mapValue: DescFieldMapValueEnum | DescFieldMapValueMessage | DescFieldMapValueScalar;
|
||||
}
|
||||
interface DescFieldMapValueEnum {
|
||||
readonly kind: "enum";
|
||||
/**
|
||||
* The enum type, if this is a map field with enum values.
|
||||
*/
|
||||
readonly enum: DescEnum;
|
||||
/**
|
||||
* The message this message field uses.
|
||||
*/
|
||||
readonly message: undefined;
|
||||
/**
|
||||
* Scalar type, if this is a map field with scalar values.
|
||||
*/
|
||||
readonly scalar: undefined;
|
||||
}
|
||||
interface DescFieldMapValueMessage {
|
||||
readonly kind: "message";
|
||||
/**
|
||||
* The enum type, if this is a map field with enum values.
|
||||
*/
|
||||
readonly enum: undefined;
|
||||
/**
|
||||
* The message type, if this is a map field with message values.
|
||||
*/
|
||||
readonly message: DescMessage;
|
||||
/**
|
||||
* Scalar type, if this is a map field with scalar values.
|
||||
*/
|
||||
readonly scalar: undefined;
|
||||
}
|
||||
interface DescFieldMapValueScalar {
|
||||
readonly kind: "scalar";
|
||||
/**
|
||||
* The enum type, if this is a map field with enum values.
|
||||
*/
|
||||
readonly enum: undefined;
|
||||
/**
|
||||
* The message type, if this is a map field with message values.
|
||||
*/
|
||||
readonly message: undefined;
|
||||
/**
|
||||
* Scalar type, if this is a map field with scalar values.
|
||||
*/
|
||||
readonly scalar: ScalarType;
|
||||
}
|
||||
/**
|
||||
* Describes a oneof group in a protobuf source file.
|
||||
*/
|
||||
export interface DescOneof {
|
||||
readonly kind: "oneof";
|
||||
/**
|
||||
* The name of the oneof group, as specified in the protobuf source.
|
||||
*/
|
||||
readonly name: string;
|
||||
/**
|
||||
* The message this oneof group was declared in.
|
||||
*/
|
||||
readonly parent: DescMessage;
|
||||
/**
|
||||
* The fields declared in this oneof group.
|
||||
*/
|
||||
readonly fields: DescField[];
|
||||
/**
|
||||
* Marked as deprecated in the protobuf source.
|
||||
* Note that oneof groups cannot be marked as deprecated, this property
|
||||
* only exists for consistency and will always be false.
|
||||
*/
|
||||
readonly deprecated: boolean;
|
||||
/**
|
||||
* The compiler-generated descriptor.
|
||||
*/
|
||||
readonly proto: OneofDescriptorProto;
|
||||
/**
|
||||
* Get comments on the element in the protobuf source.
|
||||
*/
|
||||
getComments(): DescComments;
|
||||
/**
|
||||
* Get the edition features for this protobuf element.
|
||||
*/
|
||||
getFeatures(): MergedFeatureSet;
|
||||
toString(): string;
|
||||
}
|
||||
/**
|
||||
* Describes a service declaration in a protobuf source file.
|
||||
*/
|
||||
export interface DescService {
|
||||
readonly kind: "service";
|
||||
/**
|
||||
* The fully qualified name of the service. (We omit the leading dot.)
|
||||
*/
|
||||
readonly typeName: string;
|
||||
/**
|
||||
* The name of the service, as specified in the protobuf source.
|
||||
*/
|
||||
readonly name: string;
|
||||
/**
|
||||
* The file this service was declared in.
|
||||
*/
|
||||
readonly file: DescFile;
|
||||
/**
|
||||
* The RPCs this service declares.
|
||||
*/
|
||||
readonly methods: DescMethod[];
|
||||
/**
|
||||
* Marked as deprecated in the protobuf source.
|
||||
*/
|
||||
readonly deprecated: boolean;
|
||||
/**
|
||||
* The compiler-generated descriptor.
|
||||
*/
|
||||
readonly proto: ServiceDescriptorProto;
|
||||
/**
|
||||
* Get comments on the element in the protobuf source.
|
||||
*/
|
||||
getComments(): DescComments;
|
||||
/**
|
||||
* Get the edition features for this protobuf element.
|
||||
*/
|
||||
getFeatures(): MergedFeatureSet;
|
||||
toString(): string;
|
||||
}
|
||||
/**
|
||||
* Describes an RPC declaration in a protobuf source file.
|
||||
*/
|
||||
export interface DescMethod {
|
||||
readonly kind: "rpc";
|
||||
/**
|
||||
* The name of the RPC, as specified in the protobuf source.
|
||||
*/
|
||||
readonly name: string;
|
||||
/**
|
||||
* The parent service.
|
||||
*/
|
||||
readonly parent: DescService;
|
||||
/**
|
||||
* One of the four available method types.
|
||||
*/
|
||||
readonly methodKind: MethodKind;
|
||||
/**
|
||||
* The message type for requests.
|
||||
*/
|
||||
readonly input: DescMessage;
|
||||
/**
|
||||
* The message type for responses.
|
||||
*/
|
||||
readonly output: DescMessage;
|
||||
/**
|
||||
* The idempotency level declared in the protobuf source, if any.
|
||||
*/
|
||||
readonly idempotency?: MethodIdempotency;
|
||||
/**
|
||||
* Marked as deprecated in the protobuf source.
|
||||
*/
|
||||
readonly deprecated: boolean;
|
||||
/**
|
||||
* The compiler-generated descriptor.
|
||||
*/
|
||||
readonly proto: MethodDescriptorProto;
|
||||
/**
|
||||
* Get comments on the element in the protobuf source.
|
||||
*/
|
||||
getComments(): DescComments;
|
||||
/**
|
||||
* Get the edition features for this protobuf element.
|
||||
*/
|
||||
getFeatures(): MergedFeatureSet;
|
||||
toString(): string;
|
||||
}
|
||||
/**
|
||||
* Comments on an element in a protobuf source file.
|
||||
*/
|
||||
export interface DescComments {
|
||||
readonly leadingDetached: readonly string[];
|
||||
readonly leading?: string;
|
||||
readonly trailing?: string;
|
||||
readonly sourcePath: readonly number[];
|
||||
}
|
||||
export {};
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
"use strict";
|
||||
// Copyright 2021-2024 Buf Technologies, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Reflection information for a protobuf enumeration.
|
||||
*/
|
||||
export interface EnumType {
|
||||
/**
|
||||
* The fully qualified name of the enumeration.
|
||||
*/
|
||||
readonly typeName: string;
|
||||
readonly values: readonly EnumValueInfo[];
|
||||
/**
|
||||
* Find an enum value by its (protobuf) name.
|
||||
*/
|
||||
findName(name: string): EnumValueInfo | undefined;
|
||||
/**
|
||||
* Find an enum value by its number.
|
||||
*/
|
||||
findNumber(no: number): EnumValueInfo | undefined;
|
||||
}
|
||||
/**
|
||||
* Reflection information for a protobuf enumeration value.
|
||||
*/
|
||||
export interface EnumValueInfo {
|
||||
/**
|
||||
* The numeric enumeration value, as specified in the protobuf source.
|
||||
*/
|
||||
readonly no: number;
|
||||
/**
|
||||
* The name of the enumeration value, as specified in the protobuf source.
|
||||
*/
|
||||
readonly name: string;
|
||||
/**
|
||||
* The name of the enumeration value in generated code.
|
||||
*/
|
||||
readonly localName: string;
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
"use strict";
|
||||
// Copyright 2021-2024 Buf Technologies, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
import type { Message } from "./message.js";
|
||||
import type { BinaryReadOptions, BinaryWriteOptions } from "./binary-format.js";
|
||||
import type { Extension } from "./extension.js";
|
||||
/**
|
||||
* Retrieve an extension value from a message.
|
||||
*
|
||||
* The function never returns undefined. Use hasExtension() to check whether an
|
||||
* extension is set. If the extension is not set, this function returns the
|
||||
* default value (if one was specified in the protobuf source), or the zero value
|
||||
* (for example `0` for numeric types, `[]` for repeated extension fields, and
|
||||
* an empty message instance for message fields).
|
||||
*
|
||||
* Extensions are stored as unknown fields on a message. To mutate an extension
|
||||
* value, make sure to store the new value with setExtension() after mutating.
|
||||
*
|
||||
* If the extension does not extend the given message, an error is raised.
|
||||
*/
|
||||
export declare function getExtension<E extends Message<E>, V>(message: E, extension: Extension<E, V>, options?: Partial<BinaryReadOptions>): V;
|
||||
/**
|
||||
* Set an extension value on a message. If the message already has a value for
|
||||
* this extension, the value is replaced.
|
||||
*
|
||||
* If the extension does not extend the given message, an error is raised.
|
||||
*/
|
||||
export declare function setExtension<E extends Message<E>, V>(message: E, extension: Extension<E, V>, value: V, options?: Partial<BinaryReadOptions & BinaryWriteOptions>): void;
|
||||
/**
|
||||
* Remove an extension value from a message.
|
||||
*
|
||||
* If the extension does not extend the given message, an error is raised.
|
||||
*/
|
||||
export declare function clearExtension<E extends Message<E>, V>(message: E, extension: Extension<E, V>): void;
|
||||
/**
|
||||
* Check whether an extension is set on a message.
|
||||
*/
|
||||
export declare function hasExtension<E extends Message<E>, V>(message: E, extension: Extension<E, V>): boolean;
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
"use strict";
|
||||
// Copyright 2021-2024 Buf Technologies, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.hasExtension = exports.clearExtension = exports.setExtension = exports.getExtension = void 0;
|
||||
const assert_js_1 = require("./private/assert.js");
|
||||
const extensions_js_1 = require("./private/extensions.js");
|
||||
/**
|
||||
* Retrieve an extension value from a message.
|
||||
*
|
||||
* The function never returns undefined. Use hasExtension() to check whether an
|
||||
* extension is set. If the extension is not set, this function returns the
|
||||
* default value (if one was specified in the protobuf source), or the zero value
|
||||
* (for example `0` for numeric types, `[]` for repeated extension fields, and
|
||||
* an empty message instance for message fields).
|
||||
*
|
||||
* Extensions are stored as unknown fields on a message. To mutate an extension
|
||||
* value, make sure to store the new value with setExtension() after mutating.
|
||||
*
|
||||
* If the extension does not extend the given message, an error is raised.
|
||||
*/
|
||||
function getExtension(message, extension, options) {
|
||||
assertExtendee(extension, message);
|
||||
const opt = extension.runtime.bin.makeReadOptions(options);
|
||||
const ufs = (0, extensions_js_1.filterUnknownFields)(message.getType().runtime.bin.listUnknownFields(message), extension.field);
|
||||
const [container, get] = (0, extensions_js_1.createExtensionContainer)(extension);
|
||||
for (const uf of ufs) {
|
||||
extension.runtime.bin.readField(container, opt.readerFactory(uf.data), extension.field, uf.wireType, opt);
|
||||
}
|
||||
return get();
|
||||
}
|
||||
exports.getExtension = getExtension;
|
||||
/**
|
||||
* Set an extension value on a message. If the message already has a value for
|
||||
* this extension, the value is replaced.
|
||||
*
|
||||
* If the extension does not extend the given message, an error is raised.
|
||||
*/
|
||||
function setExtension(message, extension, value, options) {
|
||||
assertExtendee(extension, message);
|
||||
const readOpt = extension.runtime.bin.makeReadOptions(options);
|
||||
const writeOpt = extension.runtime.bin.makeWriteOptions(options);
|
||||
if (hasExtension(message, extension)) {
|
||||
const ufs = message
|
||||
.getType()
|
||||
.runtime.bin.listUnknownFields(message)
|
||||
.filter((uf) => uf.no != extension.field.no);
|
||||
message.getType().runtime.bin.discardUnknownFields(message);
|
||||
for (const uf of ufs) {
|
||||
message
|
||||
.getType()
|
||||
.runtime.bin.onUnknownField(message, uf.no, uf.wireType, uf.data);
|
||||
}
|
||||
}
|
||||
const writer = writeOpt.writerFactory();
|
||||
let f = extension.field;
|
||||
// Implicit presence does not apply to extensions, see https://github.com/protocolbuffers/protobuf/issues/8234
|
||||
// We patch the field info to use explicit presence:
|
||||
if (!f.opt && !f.repeated && (f.kind == "enum" || f.kind == "scalar")) {
|
||||
f = Object.assign(Object.assign({}, extension.field), { opt: true });
|
||||
}
|
||||
extension.runtime.bin.writeField(f, value, writer, writeOpt);
|
||||
const reader = readOpt.readerFactory(writer.finish());
|
||||
while (reader.pos < reader.len) {
|
||||
const [no, wireType] = reader.tag();
|
||||
const data = reader.skip(wireType, no);
|
||||
message.getType().runtime.bin.onUnknownField(message, no, wireType, data);
|
||||
}
|
||||
}
|
||||
exports.setExtension = setExtension;
|
||||
/**
|
||||
* Remove an extension value from a message.
|
||||
*
|
||||
* If the extension does not extend the given message, an error is raised.
|
||||
*/
|
||||
function clearExtension(message, extension) {
|
||||
assertExtendee(extension, message);
|
||||
if (hasExtension(message, extension)) {
|
||||
const bin = message.getType().runtime.bin;
|
||||
const ufs = bin
|
||||
.listUnknownFields(message)
|
||||
.filter((uf) => uf.no != extension.field.no);
|
||||
bin.discardUnknownFields(message);
|
||||
for (const uf of ufs) {
|
||||
bin.onUnknownField(message, uf.no, uf.wireType, uf.data);
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.clearExtension = clearExtension;
|
||||
/**
|
||||
* Check whether an extension is set on a message.
|
||||
*/
|
||||
function hasExtension(message, extension) {
|
||||
const messageType = message.getType();
|
||||
return (extension.extendee.typeName === messageType.typeName &&
|
||||
!!messageType.runtime.bin
|
||||
.listUnknownFields(message)
|
||||
.find((uf) => uf.no == extension.field.no));
|
||||
}
|
||||
exports.hasExtension = hasExtension;
|
||||
function assertExtendee(extension, message) {
|
||||
(0, assert_js_1.assert)(extension.extendee.typeName == message.getType().typeName, `extension ${extension.typeName} can only be applied to message ${extension.extendee.typeName}`);
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import type { FieldInfo } from "./field.js";
|
||||
import type { AnyMessage, Message } from "./message.js";
|
||||
import type { MessageType } from "./message-type.js";
|
||||
import type { ProtoRuntime } from "./private/proto-runtime.js";
|
||||
export interface Extension<E extends Message<E> = AnyMessage, V = unknown> {
|
||||
/**
|
||||
* The fully qualified name of the extension.
|
||||
*/
|
||||
readonly typeName: string;
|
||||
/**
|
||||
* The message extended by this extension.
|
||||
*/
|
||||
readonly extendee: MessageType<E>;
|
||||
/**
|
||||
* Field information for this extension. Note that required fields, maps,
|
||||
* oneof are not allowed in extensions. Behavior of "localName" property is
|
||||
* undefined and must not be relied upon.
|
||||
*/
|
||||
readonly field: FieldInfo;
|
||||
/**
|
||||
* Provides serialization and other functionality.
|
||||
*/
|
||||
readonly runtime: ProtoRuntime;
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
"use strict";
|
||||
// Copyright 2021-2024 Buf Technologies, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import type { FieldInfo, OneofInfo } from "./field.js";
|
||||
/**
|
||||
* Provides convenient access to field information of a message type.
|
||||
*/
|
||||
export interface FieldList {
|
||||
/**
|
||||
* Find field information by field name or json_name.
|
||||
*/
|
||||
findJsonName(jsonName: string): FieldInfo | undefined;
|
||||
/**
|
||||
* Find field information by proto field number.
|
||||
*/
|
||||
find(fieldNo: number): FieldInfo | undefined;
|
||||
/**
|
||||
* Return field information in the order they appear in the source.
|
||||
*/
|
||||
list(): readonly FieldInfo[];
|
||||
/**
|
||||
* Return field information ordered by field number ascending.
|
||||
*/
|
||||
byNumber(): readonly FieldInfo[];
|
||||
/**
|
||||
* In order of appearance in the source, list fields and
|
||||
* oneof groups.
|
||||
*/
|
||||
byMember(): readonly (FieldInfo | OneofInfo)[];
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
"use strict";
|
||||
// Copyright 2021-2024 Buf Technologies, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
+314
@@ -0,0 +1,314 @@
|
||||
import type { EnumType } from "./enum.js";
|
||||
import type { MessageType } from "./message-type.js";
|
||||
import type { LongType, ScalarType } from "./scalar.js";
|
||||
/**
|
||||
* FieldInfo describes a field of a protobuf message for runtime reflection. We
|
||||
* distinguish between the following kinds of fields:
|
||||
*
|
||||
* - "scalar": string, bool, float, int32, etc. The scalar type is "T".
|
||||
* - "enum": The field was declared with an enum type. The enum type is "T".
|
||||
* - "message": The field was declared with a message type. The message type is "T".
|
||||
* - "map": The field was declared with map<K,V>. The key type is "K", the value type is "V".
|
||||
*
|
||||
* Every field always has the following properties:
|
||||
*
|
||||
* - "no": The field number of the protobuf field.
|
||||
* - "name": The original name of the protobuf field.
|
||||
* - "localName": The name of the field as used in generated code.
|
||||
* - "jsonName": The name for JSON serialization / deserialization.
|
||||
* - "opt": Whether the field is optional.
|
||||
* - "req": Whether the field is required (a legacy proto2 feature).
|
||||
* - "repeated": Whether the field is repeated.
|
||||
* - "packed": Whether the repeated field is packed.
|
||||
*
|
||||
* Additionally, fields may have the following properties:
|
||||
*
|
||||
* - "oneof": If the field is member of a oneof group.
|
||||
* - "default": Only proto2: An explicit default value.
|
||||
* - "delimited": Only proto2: Use the tag-delimited group encoding.
|
||||
*/
|
||||
export type FieldInfo = fiRules<fiScalar> | fiRules<fiEnum> | fiRules<fiMessage> | fiRules<fiMap>;
|
||||
/**
|
||||
* Version of `FieldInfo` that allows the following properties
|
||||
* to be omitted:
|
||||
*
|
||||
* - "localName", "jsonName": can be omitted if equal to lowerCamelCase(name)
|
||||
* - "opt": Can be omitted if false.
|
||||
* - "repeated": Can be omitted if false.
|
||||
* - "packed": Can be omitted if equal to the standard packing of the field.
|
||||
*/
|
||||
export type PartialFieldInfo = fiPartialRules<fiScalar> | fiPartialRules<fiEnum> | fiPartialRules<fiMessage> | fiPartialRules<fiMap>;
|
||||
/**
|
||||
* Provides convenient access to field information of a oneof.
|
||||
*/
|
||||
export interface OneofInfo {
|
||||
readonly kind: "oneof";
|
||||
readonly name: string;
|
||||
readonly localName: string;
|
||||
readonly repeated: false;
|
||||
readonly packed: false;
|
||||
readonly opt: false;
|
||||
readonly req: false;
|
||||
readonly default: undefined;
|
||||
readonly delimited?: undefined;
|
||||
readonly fields: readonly FieldInfo[];
|
||||
/**
|
||||
* Return field information by local name.
|
||||
*/
|
||||
findField(localName: string): FieldInfo | undefined;
|
||||
}
|
||||
interface fiShared {
|
||||
/**
|
||||
* The field number of the .proto field.
|
||||
*/
|
||||
readonly no: number;
|
||||
/**
|
||||
* The original name of the .proto field.
|
||||
*/
|
||||
readonly name: string;
|
||||
/**
|
||||
* The name of the field as used in generated code.
|
||||
*/
|
||||
readonly localName: string;
|
||||
/**
|
||||
* The name for JSON serialization / deserialization.
|
||||
*/
|
||||
readonly jsonName: string;
|
||||
/**
|
||||
* The `oneof` group, if this field belongs to one.
|
||||
*/
|
||||
readonly oneof?: OneofInfo | undefined;
|
||||
}
|
||||
interface fiScalar extends fiShared {
|
||||
readonly kind: "scalar";
|
||||
/**
|
||||
* Scalar type of the field.
|
||||
*/
|
||||
readonly T: ScalarType;
|
||||
/**
|
||||
* JavaScript representation of 64 bit integral types (int64, uint64,
|
||||
* sint64, fixed64, sfixed64).
|
||||
*
|
||||
* By default, this is LongType.BIGINT. Generated code will use the BigInt
|
||||
* primitive.
|
||||
*
|
||||
* With LongType.STRING, generated code will use the String primitive instead.
|
||||
* This can be specified per field with the option `[jstype = JS_STRING]`:
|
||||
*
|
||||
* ```protobuf
|
||||
* uint64 field_a = 1; // BigInt
|
||||
* uint64 field_b = 2 [jstype = JS_NORMAL]; // BigInt
|
||||
* uint64 field_b = 2 [jstype = JS_NUMBER]; // BigInt
|
||||
* uint64 field_b = 2 [jstype = JS_STRING]; // String
|
||||
* ```
|
||||
*
|
||||
* This property is ignored for other scalar types.
|
||||
*/
|
||||
readonly L: LongType;
|
||||
/**
|
||||
* Is the field repeated?
|
||||
*/
|
||||
readonly repeated: boolean;
|
||||
/**
|
||||
* Is this repeated field packed?
|
||||
* BYTES and STRING can never be packed, since they are length-delimited.
|
||||
* Other types can be packed with the field option "packed".
|
||||
* For proto3, fields are packed by default.
|
||||
*/
|
||||
readonly packed: boolean;
|
||||
/**
|
||||
* Is the field optional?
|
||||
*/
|
||||
readonly opt: boolean;
|
||||
/**
|
||||
* Is the field required? A legacy proto2 feature.
|
||||
*/
|
||||
readonly req: boolean;
|
||||
/**
|
||||
* Only proto2: An explicit default value.
|
||||
*/
|
||||
readonly default: number | boolean | string | bigint | Uint8Array | undefined;
|
||||
/**
|
||||
* Serialize this message with the delimited format, also known as group
|
||||
* encoding, as opposed to the standard length prefix.
|
||||
*
|
||||
* Only valid for message fields.
|
||||
*/
|
||||
readonly delimited?: undefined;
|
||||
}
|
||||
interface fiMessage extends fiShared {
|
||||
readonly kind: "message";
|
||||
/**
|
||||
* Message handler for the field.
|
||||
*/
|
||||
readonly T: MessageType;
|
||||
/**
|
||||
* Is the field repeated?
|
||||
*/
|
||||
readonly repeated: boolean;
|
||||
/**
|
||||
* Is this repeated field packed? Never true for messages.
|
||||
*/
|
||||
readonly packed: false;
|
||||
/**
|
||||
* Is the field required? A legacy proto2 feature.
|
||||
*/
|
||||
readonly req: boolean;
|
||||
/**
|
||||
* An explicit default value (only proto2). Never set for messages.
|
||||
*/
|
||||
readonly default: undefined;
|
||||
/**
|
||||
* Serialize this message with the delimited format, also known as group
|
||||
* encoding, as opposed to the standard length prefix.
|
||||
*
|
||||
* Only valid for message fields.
|
||||
*/
|
||||
readonly delimited?: boolean;
|
||||
}
|
||||
interface fiEnum extends fiShared {
|
||||
readonly kind: "enum";
|
||||
/**
|
||||
* Enum type information for the field.
|
||||
*/
|
||||
readonly T: EnumType;
|
||||
/**
|
||||
* Is the field repeated?
|
||||
*/
|
||||
readonly repeated: boolean;
|
||||
/**
|
||||
* Is this repeated field packed?
|
||||
* Repeated enums can be packed with the field option "packed".
|
||||
* For proto3, they are packed by default.
|
||||
*/
|
||||
readonly packed: boolean;
|
||||
/**
|
||||
* Is the field optional?
|
||||
*/
|
||||
readonly opt: boolean;
|
||||
/**
|
||||
* Is the field required? A legacy proto2 feature.
|
||||
*/
|
||||
readonly req: boolean;
|
||||
/**
|
||||
* Only proto2: An explicit default value.
|
||||
*/
|
||||
readonly default: number | undefined;
|
||||
/**
|
||||
* Serialize this message with the delimited format, also known as group
|
||||
* encoding, as opposed to the standard length prefix.
|
||||
*
|
||||
* Only valid for message fields.
|
||||
*/
|
||||
readonly delimited?: undefined;
|
||||
}
|
||||
interface fiMap extends fiShared {
|
||||
readonly kind: "map";
|
||||
/**
|
||||
* Map key type.
|
||||
*
|
||||
* The key_type can be any integral or string type
|
||||
* (so, any scalar type except for floating point
|
||||
* types and bytes)
|
||||
*/
|
||||
readonly K: Exclude<ScalarType, ScalarType.FLOAT | ScalarType.DOUBLE | ScalarType.BYTES>;
|
||||
/**
|
||||
* Map value type. Can be scalar, enum, or message.
|
||||
*/
|
||||
readonly V: {
|
||||
readonly kind: "scalar";
|
||||
readonly T: ScalarType;
|
||||
} | {
|
||||
readonly kind: "enum";
|
||||
readonly T: EnumType;
|
||||
} | {
|
||||
readonly kind: "message";
|
||||
readonly T: MessageType;
|
||||
};
|
||||
/**
|
||||
* Is the field repeated? Never true for maps.
|
||||
*/
|
||||
readonly repeated: false;
|
||||
/**
|
||||
* Is this repeated field packed? Never true for maps.
|
||||
*/
|
||||
readonly packed: false;
|
||||
/**
|
||||
* An explicit default value (only proto2). Never set for maps.
|
||||
*/
|
||||
readonly default: undefined;
|
||||
/**
|
||||
* Serialize this message with the delimited format, also known as group
|
||||
* encoding, as opposed to the standard length prefix.
|
||||
*
|
||||
* Only valid for message fields.
|
||||
*/
|
||||
readonly delimited?: undefined;
|
||||
}
|
||||
type fiRules<T> = Omit<T, "oneof" | "repeat" | "repeated" | "packed" | "opt" | "req"> & ({
|
||||
readonly repeated: false;
|
||||
readonly packed: false;
|
||||
readonly opt: false;
|
||||
readonly req: boolean;
|
||||
readonly oneof: undefined;
|
||||
} | {
|
||||
readonly repeated: false;
|
||||
readonly packed: false;
|
||||
readonly opt: true;
|
||||
readonly req: false;
|
||||
readonly oneof: undefined;
|
||||
} | {
|
||||
readonly repeated: boolean;
|
||||
readonly packed: boolean;
|
||||
readonly opt: false;
|
||||
readonly req: boolean;
|
||||
readonly oneof: undefined;
|
||||
} | {
|
||||
readonly repeated: false;
|
||||
readonly packed: false;
|
||||
readonly opt: false;
|
||||
readonly req: false;
|
||||
readonly oneof: OneofInfo;
|
||||
});
|
||||
type fiPartialRules<T extends fiScalar | fiMap | fiEnum | fiMessage> = Omit<T, "jsonName" | "localName" | "oneof" | "repeat" | "repeated" | "packed" | "opt" | "req" | "default" | "L" | "delimited"> & ({
|
||||
readonly jsonName?: string;
|
||||
readonly repeated?: false;
|
||||
readonly packed?: false;
|
||||
readonly opt?: false;
|
||||
readonly req?: boolean;
|
||||
readonly oneof?: undefined;
|
||||
default?: T["default"];
|
||||
L?: LongType;
|
||||
delimited?: boolean;
|
||||
} | {
|
||||
readonly jsonName?: string;
|
||||
readonly repeated?: false;
|
||||
readonly packed?: false;
|
||||
readonly opt: true;
|
||||
readonly req?: false;
|
||||
readonly oneof?: undefined;
|
||||
default?: T["default"];
|
||||
L?: LongType;
|
||||
delimited?: boolean;
|
||||
} | {
|
||||
readonly jsonName?: string;
|
||||
readonly repeated?: boolean;
|
||||
readonly packed?: boolean;
|
||||
readonly opt?: false;
|
||||
readonly req?: boolean;
|
||||
readonly oneof?: undefined;
|
||||
default?: T["default"];
|
||||
L?: LongType;
|
||||
delimited?: boolean;
|
||||
} | {
|
||||
readonly jsonName?: string;
|
||||
readonly repeated?: false;
|
||||
readonly packed?: false;
|
||||
readonly opt?: false;
|
||||
readonly req?: false;
|
||||
readonly oneof: string;
|
||||
default?: T["default"];
|
||||
L?: LongType;
|
||||
delimited?: boolean;
|
||||
});
|
||||
export {};
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
"use strict";
|
||||
// Copyright 2021-2024 Buf Technologies, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
import type { PartialMessage, PlainMessage } from "../../message.js";
|
||||
import { Message } from "../../message.js";
|
||||
import { proto3 } from "../../proto3.js";
|
||||
import type { JsonReadOptions, JsonValue, JsonWriteOptions } from "../../json-format.js";
|
||||
import type { IMessageTypeRegistry } from "../../type-registry.js";
|
||||
import type { MessageType } from "../../message-type.js";
|
||||
import type { FieldList } from "../../field-list.js";
|
||||
import type { BinaryReadOptions } from "../../binary-format.js";
|
||||
/**
|
||||
* `Any` contains an arbitrary serialized protocol buffer message along with a
|
||||
* URL that describes the type of the serialized message.
|
||||
*
|
||||
* Protobuf library provides support to pack/unpack Any values in the form
|
||||
* of utility functions or additional generated methods of the Any type.
|
||||
*
|
||||
* Example 1: Pack and unpack a message in C++.
|
||||
*
|
||||
* Foo foo = ...;
|
||||
* Any any;
|
||||
* any.PackFrom(foo);
|
||||
* ...
|
||||
* if (any.UnpackTo(&foo)) {
|
||||
* ...
|
||||
* }
|
||||
*
|
||||
* Example 2: Pack and unpack a message in Java.
|
||||
*
|
||||
* Foo foo = ...;
|
||||
* Any any = Any.pack(foo);
|
||||
* ...
|
||||
* if (any.is(Foo.class)) {
|
||||
* foo = any.unpack(Foo.class);
|
||||
* }
|
||||
* // or ...
|
||||
* if (any.isSameTypeAs(Foo.getDefaultInstance())) {
|
||||
* foo = any.unpack(Foo.getDefaultInstance());
|
||||
* }
|
||||
*
|
||||
* Example 3: Pack and unpack a message in Python.
|
||||
*
|
||||
* foo = Foo(...)
|
||||
* any = Any()
|
||||
* any.Pack(foo)
|
||||
* ...
|
||||
* if any.Is(Foo.DESCRIPTOR):
|
||||
* any.Unpack(foo)
|
||||
* ...
|
||||
*
|
||||
* Example 4: Pack and unpack a message in Go
|
||||
*
|
||||
* foo := &pb.Foo{...}
|
||||
* any, err := anypb.New(foo)
|
||||
* if err != nil {
|
||||
* ...
|
||||
* }
|
||||
* ...
|
||||
* foo := &pb.Foo{}
|
||||
* if err := any.UnmarshalTo(foo); err != nil {
|
||||
* ...
|
||||
* }
|
||||
*
|
||||
* The pack methods provided by protobuf library will by default use
|
||||
* 'type.googleapis.com/full.type.name' as the type URL and the unpack
|
||||
* methods only use the fully qualified type name after the last '/'
|
||||
* in the type URL, for example "foo.bar.com/x/y.z" will yield type
|
||||
* name "y.z".
|
||||
*
|
||||
* JSON
|
||||
* ====
|
||||
* The JSON representation of an `Any` value uses the regular
|
||||
* representation of the deserialized, embedded message, with an
|
||||
* additional field `@type` which contains the type URL. Example:
|
||||
*
|
||||
* package google.profile;
|
||||
* message Person {
|
||||
* string first_name = 1;
|
||||
* string last_name = 2;
|
||||
* }
|
||||
*
|
||||
* {
|
||||
* "@type": "type.googleapis.com/google.profile.Person",
|
||||
* "firstName": <string>,
|
||||
* "lastName": <string>
|
||||
* }
|
||||
*
|
||||
* If the embedded message type is well-known and has a custom JSON
|
||||
* representation, that representation will be embedded adding a field
|
||||
* `value` which holds the custom JSON in addition to the `@type`
|
||||
* field. Example (for message [google.protobuf.Duration][]):
|
||||
*
|
||||
* {
|
||||
* "@type": "type.googleapis.com/google.protobuf.Duration",
|
||||
* "value": "1.212s"
|
||||
* }
|
||||
*
|
||||
*
|
||||
* @generated from message google.protobuf.Any
|
||||
*/
|
||||
export declare class Any extends Message<Any> {
|
||||
/**
|
||||
* A URL/resource name that uniquely identifies the type of the serialized
|
||||
* protocol buffer message. This string must contain at least
|
||||
* one "/" character. The last segment of the URL's path must represent
|
||||
* the fully qualified name of the type (as in
|
||||
* `path/google.protobuf.Duration`). The name should be in a canonical form
|
||||
* (e.g., leading "." is not accepted).
|
||||
*
|
||||
* In practice, teams usually precompile into the binary all types that they
|
||||
* expect it to use in the context of Any. However, for URLs which use the
|
||||
* scheme `http`, `https`, or no scheme, one can optionally set up a type
|
||||
* server that maps type URLs to message definitions as follows:
|
||||
*
|
||||
* * If no scheme is provided, `https` is assumed.
|
||||
* * An HTTP GET on the URL must yield a [google.protobuf.Type][]
|
||||
* value in binary format, or produce an error.
|
||||
* * Applications are allowed to cache lookup results based on the
|
||||
* URL, or have them precompiled into a binary to avoid any
|
||||
* lookup. Therefore, binary compatibility needs to be preserved
|
||||
* on changes to types. (Use versioned type names to manage
|
||||
* breaking changes.)
|
||||
*
|
||||
* Note: this functionality is not currently available in the official
|
||||
* protobuf release, and it is not used for type URLs beginning with
|
||||
* type.googleapis.com. As of May 2023, there are no widely used type server
|
||||
* implementations and no plans to implement one.
|
||||
*
|
||||
* Schemes other than `http`, `https` (or the empty scheme) might be
|
||||
* used with implementation specific semantics.
|
||||
*
|
||||
*
|
||||
* @generated from field: string type_url = 1;
|
||||
*/
|
||||
typeUrl: string;
|
||||
/**
|
||||
* Must be a valid serialized protocol buffer of the above specified type.
|
||||
*
|
||||
* @generated from field: bytes value = 2;
|
||||
*/
|
||||
value: Uint8Array;
|
||||
constructor(data?: PartialMessage<Any>);
|
||||
toJson(options?: Partial<JsonWriteOptions>): JsonValue;
|
||||
fromJson(json: JsonValue, options?: Partial<JsonReadOptions>): this;
|
||||
packFrom(message: Message): void;
|
||||
unpackTo(target: Message): boolean;
|
||||
unpack(registry: IMessageTypeRegistry): Message | undefined;
|
||||
is(type: MessageType | string): boolean;
|
||||
private typeNameToUrl;
|
||||
private typeUrlToName;
|
||||
static readonly runtime: typeof proto3;
|
||||
static readonly typeName = "google.protobuf.Any";
|
||||
static readonly fields: FieldList;
|
||||
static pack(message: Message): Any;
|
||||
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): Any;
|
||||
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): Any;
|
||||
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): Any;
|
||||
static equals(a: Any | PlainMessage<Any> | undefined, b: Any | PlainMessage<Any> | undefined): boolean;
|
||||
}
|
||||
+273
@@ -0,0 +1,273 @@
|
||||
"use strict";
|
||||
// Copyright 2021-2024 Buf Technologies, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.Any = void 0;
|
||||
const message_js_1 = require("../../message.js");
|
||||
const proto3_js_1 = require("../../proto3.js");
|
||||
/**
|
||||
* `Any` contains an arbitrary serialized protocol buffer message along with a
|
||||
* URL that describes the type of the serialized message.
|
||||
*
|
||||
* Protobuf library provides support to pack/unpack Any values in the form
|
||||
* of utility functions or additional generated methods of the Any type.
|
||||
*
|
||||
* Example 1: Pack and unpack a message in C++.
|
||||
*
|
||||
* Foo foo = ...;
|
||||
* Any any;
|
||||
* any.PackFrom(foo);
|
||||
* ...
|
||||
* if (any.UnpackTo(&foo)) {
|
||||
* ...
|
||||
* }
|
||||
*
|
||||
* Example 2: Pack and unpack a message in Java.
|
||||
*
|
||||
* Foo foo = ...;
|
||||
* Any any = Any.pack(foo);
|
||||
* ...
|
||||
* if (any.is(Foo.class)) {
|
||||
* foo = any.unpack(Foo.class);
|
||||
* }
|
||||
* // or ...
|
||||
* if (any.isSameTypeAs(Foo.getDefaultInstance())) {
|
||||
* foo = any.unpack(Foo.getDefaultInstance());
|
||||
* }
|
||||
*
|
||||
* Example 3: Pack and unpack a message in Python.
|
||||
*
|
||||
* foo = Foo(...)
|
||||
* any = Any()
|
||||
* any.Pack(foo)
|
||||
* ...
|
||||
* if any.Is(Foo.DESCRIPTOR):
|
||||
* any.Unpack(foo)
|
||||
* ...
|
||||
*
|
||||
* Example 4: Pack and unpack a message in Go
|
||||
*
|
||||
* foo := &pb.Foo{...}
|
||||
* any, err := anypb.New(foo)
|
||||
* if err != nil {
|
||||
* ...
|
||||
* }
|
||||
* ...
|
||||
* foo := &pb.Foo{}
|
||||
* if err := any.UnmarshalTo(foo); err != nil {
|
||||
* ...
|
||||
* }
|
||||
*
|
||||
* The pack methods provided by protobuf library will by default use
|
||||
* 'type.googleapis.com/full.type.name' as the type URL and the unpack
|
||||
* methods only use the fully qualified type name after the last '/'
|
||||
* in the type URL, for example "foo.bar.com/x/y.z" will yield type
|
||||
* name "y.z".
|
||||
*
|
||||
* JSON
|
||||
* ====
|
||||
* The JSON representation of an `Any` value uses the regular
|
||||
* representation of the deserialized, embedded message, with an
|
||||
* additional field `@type` which contains the type URL. Example:
|
||||
*
|
||||
* package google.profile;
|
||||
* message Person {
|
||||
* string first_name = 1;
|
||||
* string last_name = 2;
|
||||
* }
|
||||
*
|
||||
* {
|
||||
* "@type": "type.googleapis.com/google.profile.Person",
|
||||
* "firstName": <string>,
|
||||
* "lastName": <string>
|
||||
* }
|
||||
*
|
||||
* If the embedded message type is well-known and has a custom JSON
|
||||
* representation, that representation will be embedded adding a field
|
||||
* `value` which holds the custom JSON in addition to the `@type`
|
||||
* field. Example (for message [google.protobuf.Duration][]):
|
||||
*
|
||||
* {
|
||||
* "@type": "type.googleapis.com/google.protobuf.Duration",
|
||||
* "value": "1.212s"
|
||||
* }
|
||||
*
|
||||
*
|
||||
* @generated from message google.protobuf.Any
|
||||
*/
|
||||
class Any extends message_js_1.Message {
|
||||
constructor(data) {
|
||||
super();
|
||||
/**
|
||||
* A URL/resource name that uniquely identifies the type of the serialized
|
||||
* protocol buffer message. This string must contain at least
|
||||
* one "/" character. The last segment of the URL's path must represent
|
||||
* the fully qualified name of the type (as in
|
||||
* `path/google.protobuf.Duration`). The name should be in a canonical form
|
||||
* (e.g., leading "." is not accepted).
|
||||
*
|
||||
* In practice, teams usually precompile into the binary all types that they
|
||||
* expect it to use in the context of Any. However, for URLs which use the
|
||||
* scheme `http`, `https`, or no scheme, one can optionally set up a type
|
||||
* server that maps type URLs to message definitions as follows:
|
||||
*
|
||||
* * If no scheme is provided, `https` is assumed.
|
||||
* * An HTTP GET on the URL must yield a [google.protobuf.Type][]
|
||||
* value in binary format, or produce an error.
|
||||
* * Applications are allowed to cache lookup results based on the
|
||||
* URL, or have them precompiled into a binary to avoid any
|
||||
* lookup. Therefore, binary compatibility needs to be preserved
|
||||
* on changes to types. (Use versioned type names to manage
|
||||
* breaking changes.)
|
||||
*
|
||||
* Note: this functionality is not currently available in the official
|
||||
* protobuf release, and it is not used for type URLs beginning with
|
||||
* type.googleapis.com. As of May 2023, there are no widely used type server
|
||||
* implementations and no plans to implement one.
|
||||
*
|
||||
* Schemes other than `http`, `https` (or the empty scheme) might be
|
||||
* used with implementation specific semantics.
|
||||
*
|
||||
*
|
||||
* @generated from field: string type_url = 1;
|
||||
*/
|
||||
this.typeUrl = "";
|
||||
/**
|
||||
* Must be a valid serialized protocol buffer of the above specified type.
|
||||
*
|
||||
* @generated from field: bytes value = 2;
|
||||
*/
|
||||
this.value = new Uint8Array(0);
|
||||
proto3_js_1.proto3.util.initPartial(data, this);
|
||||
}
|
||||
toJson(options) {
|
||||
var _a;
|
||||
if (this.typeUrl === "") {
|
||||
return {};
|
||||
}
|
||||
const typeName = this.typeUrlToName(this.typeUrl);
|
||||
const messageType = (_a = options === null || options === void 0 ? void 0 : options.typeRegistry) === null || _a === void 0 ? void 0 : _a.findMessage(typeName);
|
||||
if (!messageType) {
|
||||
throw new Error(`cannot encode message google.protobuf.Any to JSON: "${this.typeUrl}" is not in the type registry`);
|
||||
}
|
||||
const message = messageType.fromBinary(this.value);
|
||||
let json = message.toJson(options);
|
||||
if (typeName.startsWith("google.protobuf.") || (json === null || Array.isArray(json) || typeof json !== "object")) {
|
||||
json = { value: json };
|
||||
}
|
||||
json["@type"] = this.typeUrl;
|
||||
return json;
|
||||
}
|
||||
fromJson(json, options) {
|
||||
var _a;
|
||||
if (json === null || Array.isArray(json) || typeof json != "object") {
|
||||
throw new Error(`cannot decode message google.protobuf.Any from JSON: expected object but got ${json === null ? "null" : Array.isArray(json) ? "array" : typeof json}`);
|
||||
}
|
||||
if (Object.keys(json).length == 0) {
|
||||
return this;
|
||||
}
|
||||
const typeUrl = json["@type"];
|
||||
if (typeof typeUrl != "string" || typeUrl == "") {
|
||||
throw new Error(`cannot decode message google.protobuf.Any from JSON: "@type" is empty`);
|
||||
}
|
||||
const typeName = this.typeUrlToName(typeUrl), messageType = (_a = options === null || options === void 0 ? void 0 : options.typeRegistry) === null || _a === void 0 ? void 0 : _a.findMessage(typeName);
|
||||
if (!messageType) {
|
||||
throw new Error(`cannot decode message google.protobuf.Any from JSON: ${typeUrl} is not in the type registry`);
|
||||
}
|
||||
let message;
|
||||
if (typeName.startsWith("google.protobuf.") && Object.prototype.hasOwnProperty.call(json, "value")) {
|
||||
message = messageType.fromJson(json["value"], options);
|
||||
}
|
||||
else {
|
||||
const copy = Object.assign({}, json);
|
||||
delete copy["@type"];
|
||||
message = messageType.fromJson(copy, options);
|
||||
}
|
||||
this.packFrom(message);
|
||||
return this;
|
||||
}
|
||||
packFrom(message) {
|
||||
this.value = message.toBinary();
|
||||
this.typeUrl = this.typeNameToUrl(message.getType().typeName);
|
||||
}
|
||||
unpackTo(target) {
|
||||
if (!this.is(target.getType())) {
|
||||
return false;
|
||||
}
|
||||
target.fromBinary(this.value);
|
||||
return true;
|
||||
}
|
||||
unpack(registry) {
|
||||
if (this.typeUrl === "") {
|
||||
return undefined;
|
||||
}
|
||||
const messageType = registry.findMessage(this.typeUrlToName(this.typeUrl));
|
||||
if (!messageType) {
|
||||
return undefined;
|
||||
}
|
||||
return messageType.fromBinary(this.value);
|
||||
}
|
||||
is(type) {
|
||||
if (this.typeUrl === '') {
|
||||
return false;
|
||||
}
|
||||
const name = this.typeUrlToName(this.typeUrl);
|
||||
let typeName = '';
|
||||
if (typeof type === 'string') {
|
||||
typeName = type;
|
||||
}
|
||||
else {
|
||||
typeName = type.typeName;
|
||||
}
|
||||
return name === typeName;
|
||||
}
|
||||
typeNameToUrl(name) {
|
||||
return `type.googleapis.com/${name}`;
|
||||
}
|
||||
typeUrlToName(url) {
|
||||
if (!url.length) {
|
||||
throw new Error(`invalid type url: ${url}`);
|
||||
}
|
||||
const slash = url.lastIndexOf("/");
|
||||
const name = slash >= 0 ? url.substring(slash + 1) : url;
|
||||
if (!name.length) {
|
||||
throw new Error(`invalid type url: ${url}`);
|
||||
}
|
||||
return name;
|
||||
}
|
||||
static pack(message) {
|
||||
const any = new Any();
|
||||
any.packFrom(message);
|
||||
return any;
|
||||
}
|
||||
static fromBinary(bytes, options) {
|
||||
return new Any().fromBinary(bytes, options);
|
||||
}
|
||||
static fromJson(jsonValue, options) {
|
||||
return new Any().fromJson(jsonValue, options);
|
||||
}
|
||||
static fromJsonString(jsonString, options) {
|
||||
return new Any().fromJsonString(jsonString, options);
|
||||
}
|
||||
static equals(a, b) {
|
||||
return proto3_js_1.proto3.util.equals(Any, a, b);
|
||||
}
|
||||
}
|
||||
exports.Any = Any;
|
||||
Any.runtime = proto3_js_1.proto3;
|
||||
Any.typeName = "google.protobuf.Any";
|
||||
Any.fields = proto3_js_1.proto3.util.newFieldList(() => [
|
||||
{ no: 1, name: "type_url", kind: "scalar", T: 9 /* ScalarType.STRING */ },
|
||||
{ no: 2, name: "value", kind: "scalar", T: 12 /* ScalarType.BYTES */ },
|
||||
]);
|
||||
+256
@@ -0,0 +1,256 @@
|
||||
import type { PartialMessage, PlainMessage } from "../../message.js";
|
||||
import { Message } from "../../message.js";
|
||||
import { Option, Syntax } from "./type_pb.js";
|
||||
import { SourceContext } from "./source_context_pb.js";
|
||||
import { proto3 } from "../../proto3.js";
|
||||
import type { FieldList } from "../../field-list.js";
|
||||
import type { BinaryReadOptions } from "../../binary-format.js";
|
||||
import type { JsonReadOptions, JsonValue } from "../../json-format.js";
|
||||
/**
|
||||
* Api is a light-weight descriptor for an API Interface.
|
||||
*
|
||||
* Interfaces are also described as "protocol buffer services" in some contexts,
|
||||
* such as by the "service" keyword in a .proto file, but they are different
|
||||
* from API Services, which represent a concrete implementation of an interface
|
||||
* as opposed to simply a description of methods and bindings. They are also
|
||||
* sometimes simply referred to as "APIs" in other contexts, such as the name of
|
||||
* this message itself. See https://cloud.google.com/apis/design/glossary for
|
||||
* detailed terminology.
|
||||
*
|
||||
* @generated from message google.protobuf.Api
|
||||
*/
|
||||
export declare class Api extends Message<Api> {
|
||||
/**
|
||||
* The fully qualified name of this interface, including package name
|
||||
* followed by the interface's simple name.
|
||||
*
|
||||
* @generated from field: string name = 1;
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* The methods of this interface, in unspecified order.
|
||||
*
|
||||
* @generated from field: repeated google.protobuf.Method methods = 2;
|
||||
*/
|
||||
methods: Method[];
|
||||
/**
|
||||
* Any metadata attached to the interface.
|
||||
*
|
||||
* @generated from field: repeated google.protobuf.Option options = 3;
|
||||
*/
|
||||
options: Option[];
|
||||
/**
|
||||
* A version string for this interface. If specified, must have the form
|
||||
* `major-version.minor-version`, as in `1.10`. If the minor version is
|
||||
* omitted, it defaults to zero. If the entire version field is empty, the
|
||||
* major version is derived from the package name, as outlined below. If the
|
||||
* field is not empty, the version in the package name will be verified to be
|
||||
* consistent with what is provided here.
|
||||
*
|
||||
* The versioning schema uses [semantic
|
||||
* versioning](http://semver.org) where the major version number
|
||||
* indicates a breaking change and the minor version an additive,
|
||||
* non-breaking change. Both version numbers are signals to users
|
||||
* what to expect from different versions, and should be carefully
|
||||
* chosen based on the product plan.
|
||||
*
|
||||
* The major version is also reflected in the package name of the
|
||||
* interface, which must end in `v<major-version>`, as in
|
||||
* `google.feature.v1`. For major versions 0 and 1, the suffix can
|
||||
* be omitted. Zero major versions must only be used for
|
||||
* experimental, non-GA interfaces.
|
||||
*
|
||||
*
|
||||
* @generated from field: string version = 4;
|
||||
*/
|
||||
version: string;
|
||||
/**
|
||||
* Source context for the protocol buffer service represented by this
|
||||
* message.
|
||||
*
|
||||
* @generated from field: google.protobuf.SourceContext source_context = 5;
|
||||
*/
|
||||
sourceContext?: SourceContext;
|
||||
/**
|
||||
* Included interfaces. See [Mixin][].
|
||||
*
|
||||
* @generated from field: repeated google.protobuf.Mixin mixins = 6;
|
||||
*/
|
||||
mixins: Mixin[];
|
||||
/**
|
||||
* The source syntax of the service.
|
||||
*
|
||||
* @generated from field: google.protobuf.Syntax syntax = 7;
|
||||
*/
|
||||
syntax: Syntax;
|
||||
constructor(data?: PartialMessage<Api>);
|
||||
static readonly runtime: typeof proto3;
|
||||
static readonly typeName = "google.protobuf.Api";
|
||||
static readonly fields: FieldList;
|
||||
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): Api;
|
||||
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): Api;
|
||||
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): Api;
|
||||
static equals(a: Api | PlainMessage<Api> | undefined, b: Api | PlainMessage<Api> | undefined): boolean;
|
||||
}
|
||||
/**
|
||||
* Method represents a method of an API interface.
|
||||
*
|
||||
* @generated from message google.protobuf.Method
|
||||
*/
|
||||
export declare class Method extends Message<Method> {
|
||||
/**
|
||||
* The simple name of this method.
|
||||
*
|
||||
* @generated from field: string name = 1;
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* A URL of the input message type.
|
||||
*
|
||||
* @generated from field: string request_type_url = 2;
|
||||
*/
|
||||
requestTypeUrl: string;
|
||||
/**
|
||||
* If true, the request is streamed.
|
||||
*
|
||||
* @generated from field: bool request_streaming = 3;
|
||||
*/
|
||||
requestStreaming: boolean;
|
||||
/**
|
||||
* The URL of the output message type.
|
||||
*
|
||||
* @generated from field: string response_type_url = 4;
|
||||
*/
|
||||
responseTypeUrl: string;
|
||||
/**
|
||||
* If true, the response is streamed.
|
||||
*
|
||||
* @generated from field: bool response_streaming = 5;
|
||||
*/
|
||||
responseStreaming: boolean;
|
||||
/**
|
||||
* Any metadata attached to the method.
|
||||
*
|
||||
* @generated from field: repeated google.protobuf.Option options = 6;
|
||||
*/
|
||||
options: Option[];
|
||||
/**
|
||||
* The source syntax of this method.
|
||||
*
|
||||
* @generated from field: google.protobuf.Syntax syntax = 7;
|
||||
*/
|
||||
syntax: Syntax;
|
||||
constructor(data?: PartialMessage<Method>);
|
||||
static readonly runtime: typeof proto3;
|
||||
static readonly typeName = "google.protobuf.Method";
|
||||
static readonly fields: FieldList;
|
||||
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): Method;
|
||||
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): Method;
|
||||
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): Method;
|
||||
static equals(a: Method | PlainMessage<Method> | undefined, b: Method | PlainMessage<Method> | undefined): boolean;
|
||||
}
|
||||
/**
|
||||
* Declares an API Interface to be included in this interface. The including
|
||||
* interface must redeclare all the methods from the included interface, but
|
||||
* documentation and options are inherited as follows:
|
||||
*
|
||||
* - If after comment and whitespace stripping, the documentation
|
||||
* string of the redeclared method is empty, it will be inherited
|
||||
* from the original method.
|
||||
*
|
||||
* - Each annotation belonging to the service config (http,
|
||||
* visibility) which is not set in the redeclared method will be
|
||||
* inherited.
|
||||
*
|
||||
* - If an http annotation is inherited, the path pattern will be
|
||||
* modified as follows. Any version prefix will be replaced by the
|
||||
* version of the including interface plus the [root][] path if
|
||||
* specified.
|
||||
*
|
||||
* Example of a simple mixin:
|
||||
*
|
||||
* package google.acl.v1;
|
||||
* service AccessControl {
|
||||
* // Get the underlying ACL object.
|
||||
* rpc GetAcl(GetAclRequest) returns (Acl) {
|
||||
* option (google.api.http).get = "/v1/{resource=**}:getAcl";
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* package google.storage.v2;
|
||||
* service Storage {
|
||||
* rpc GetAcl(GetAclRequest) returns (Acl);
|
||||
*
|
||||
* // Get a data record.
|
||||
* rpc GetData(GetDataRequest) returns (Data) {
|
||||
* option (google.api.http).get = "/v2/{resource=**}";
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* Example of a mixin configuration:
|
||||
*
|
||||
* apis:
|
||||
* - name: google.storage.v2.Storage
|
||||
* mixins:
|
||||
* - name: google.acl.v1.AccessControl
|
||||
*
|
||||
* The mixin construct implies that all methods in `AccessControl` are
|
||||
* also declared with same name and request/response types in
|
||||
* `Storage`. A documentation generator or annotation processor will
|
||||
* see the effective `Storage.GetAcl` method after inherting
|
||||
* documentation and annotations as follows:
|
||||
*
|
||||
* service Storage {
|
||||
* // Get the underlying ACL object.
|
||||
* rpc GetAcl(GetAclRequest) returns (Acl) {
|
||||
* option (google.api.http).get = "/v2/{resource=**}:getAcl";
|
||||
* }
|
||||
* ...
|
||||
* }
|
||||
*
|
||||
* Note how the version in the path pattern changed from `v1` to `v2`.
|
||||
*
|
||||
* If the `root` field in the mixin is specified, it should be a
|
||||
* relative path under which inherited HTTP paths are placed. Example:
|
||||
*
|
||||
* apis:
|
||||
* - name: google.storage.v2.Storage
|
||||
* mixins:
|
||||
* - name: google.acl.v1.AccessControl
|
||||
* root: acls
|
||||
*
|
||||
* This implies the following inherited HTTP annotation:
|
||||
*
|
||||
* service Storage {
|
||||
* // Get the underlying ACL object.
|
||||
* rpc GetAcl(GetAclRequest) returns (Acl) {
|
||||
* option (google.api.http).get = "/v2/acls/{resource=**}:getAcl";
|
||||
* }
|
||||
* ...
|
||||
* }
|
||||
*
|
||||
* @generated from message google.protobuf.Mixin
|
||||
*/
|
||||
export declare class Mixin extends Message<Mixin> {
|
||||
/**
|
||||
* The fully qualified name of the interface which is included.
|
||||
*
|
||||
* @generated from field: string name = 1;
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* If non-empty specifies a path under which inherited HTTP paths
|
||||
* are rooted.
|
||||
*
|
||||
* @generated from field: string root = 2;
|
||||
*/
|
||||
root: string;
|
||||
constructor(data?: PartialMessage<Mixin>);
|
||||
static readonly runtime: typeof proto3;
|
||||
static readonly typeName = "google.protobuf.Mixin";
|
||||
static readonly fields: FieldList;
|
||||
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): Mixin;
|
||||
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): Mixin;
|
||||
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): Mixin;
|
||||
static equals(a: Mixin | PlainMessage<Mixin> | undefined, b: Mixin | PlainMessage<Mixin> | undefined): boolean;
|
||||
}
|
||||
+316
@@ -0,0 +1,316 @@
|
||||
"use strict";
|
||||
// Copyright 2021-2024 Buf Technologies, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.Mixin = exports.Method = exports.Api = void 0;
|
||||
const message_js_1 = require("../../message.js");
|
||||
const type_pb_js_1 = require("./type_pb.js");
|
||||
const source_context_pb_js_1 = require("./source_context_pb.js");
|
||||
const proto3_js_1 = require("../../proto3.js");
|
||||
/**
|
||||
* Api is a light-weight descriptor for an API Interface.
|
||||
*
|
||||
* Interfaces are also described as "protocol buffer services" in some contexts,
|
||||
* such as by the "service" keyword in a .proto file, but they are different
|
||||
* from API Services, which represent a concrete implementation of an interface
|
||||
* as opposed to simply a description of methods and bindings. They are also
|
||||
* sometimes simply referred to as "APIs" in other contexts, such as the name of
|
||||
* this message itself. See https://cloud.google.com/apis/design/glossary for
|
||||
* detailed terminology.
|
||||
*
|
||||
* @generated from message google.protobuf.Api
|
||||
*/
|
||||
class Api extends message_js_1.Message {
|
||||
constructor(data) {
|
||||
super();
|
||||
/**
|
||||
* The fully qualified name of this interface, including package name
|
||||
* followed by the interface's simple name.
|
||||
*
|
||||
* @generated from field: string name = 1;
|
||||
*/
|
||||
this.name = "";
|
||||
/**
|
||||
* The methods of this interface, in unspecified order.
|
||||
*
|
||||
* @generated from field: repeated google.protobuf.Method methods = 2;
|
||||
*/
|
||||
this.methods = [];
|
||||
/**
|
||||
* Any metadata attached to the interface.
|
||||
*
|
||||
* @generated from field: repeated google.protobuf.Option options = 3;
|
||||
*/
|
||||
this.options = [];
|
||||
/**
|
||||
* A version string for this interface. If specified, must have the form
|
||||
* `major-version.minor-version`, as in `1.10`. If the minor version is
|
||||
* omitted, it defaults to zero. If the entire version field is empty, the
|
||||
* major version is derived from the package name, as outlined below. If the
|
||||
* field is not empty, the version in the package name will be verified to be
|
||||
* consistent with what is provided here.
|
||||
*
|
||||
* The versioning schema uses [semantic
|
||||
* versioning](http://semver.org) where the major version number
|
||||
* indicates a breaking change and the minor version an additive,
|
||||
* non-breaking change. Both version numbers are signals to users
|
||||
* what to expect from different versions, and should be carefully
|
||||
* chosen based on the product plan.
|
||||
*
|
||||
* The major version is also reflected in the package name of the
|
||||
* interface, which must end in `v<major-version>`, as in
|
||||
* `google.feature.v1`. For major versions 0 and 1, the suffix can
|
||||
* be omitted. Zero major versions must only be used for
|
||||
* experimental, non-GA interfaces.
|
||||
*
|
||||
*
|
||||
* @generated from field: string version = 4;
|
||||
*/
|
||||
this.version = "";
|
||||
/**
|
||||
* Included interfaces. See [Mixin][].
|
||||
*
|
||||
* @generated from field: repeated google.protobuf.Mixin mixins = 6;
|
||||
*/
|
||||
this.mixins = [];
|
||||
/**
|
||||
* The source syntax of the service.
|
||||
*
|
||||
* @generated from field: google.protobuf.Syntax syntax = 7;
|
||||
*/
|
||||
this.syntax = type_pb_js_1.Syntax.PROTO2;
|
||||
proto3_js_1.proto3.util.initPartial(data, this);
|
||||
}
|
||||
static fromBinary(bytes, options) {
|
||||
return new Api().fromBinary(bytes, options);
|
||||
}
|
||||
static fromJson(jsonValue, options) {
|
||||
return new Api().fromJson(jsonValue, options);
|
||||
}
|
||||
static fromJsonString(jsonString, options) {
|
||||
return new Api().fromJsonString(jsonString, options);
|
||||
}
|
||||
static equals(a, b) {
|
||||
return proto3_js_1.proto3.util.equals(Api, a, b);
|
||||
}
|
||||
}
|
||||
exports.Api = Api;
|
||||
Api.runtime = proto3_js_1.proto3;
|
||||
Api.typeName = "google.protobuf.Api";
|
||||
Api.fields = proto3_js_1.proto3.util.newFieldList(() => [
|
||||
{ no: 1, name: "name", kind: "scalar", T: 9 /* ScalarType.STRING */ },
|
||||
{ no: 2, name: "methods", kind: "message", T: Method, repeated: true },
|
||||
{ no: 3, name: "options", kind: "message", T: type_pb_js_1.Option, repeated: true },
|
||||
{ no: 4, name: "version", kind: "scalar", T: 9 /* ScalarType.STRING */ },
|
||||
{ no: 5, name: "source_context", kind: "message", T: source_context_pb_js_1.SourceContext },
|
||||
{ no: 6, name: "mixins", kind: "message", T: Mixin, repeated: true },
|
||||
{ no: 7, name: "syntax", kind: "enum", T: proto3_js_1.proto3.getEnumType(type_pb_js_1.Syntax) },
|
||||
]);
|
||||
/**
|
||||
* Method represents a method of an API interface.
|
||||
*
|
||||
* @generated from message google.protobuf.Method
|
||||
*/
|
||||
class Method extends message_js_1.Message {
|
||||
constructor(data) {
|
||||
super();
|
||||
/**
|
||||
* The simple name of this method.
|
||||
*
|
||||
* @generated from field: string name = 1;
|
||||
*/
|
||||
this.name = "";
|
||||
/**
|
||||
* A URL of the input message type.
|
||||
*
|
||||
* @generated from field: string request_type_url = 2;
|
||||
*/
|
||||
this.requestTypeUrl = "";
|
||||
/**
|
||||
* If true, the request is streamed.
|
||||
*
|
||||
* @generated from field: bool request_streaming = 3;
|
||||
*/
|
||||
this.requestStreaming = false;
|
||||
/**
|
||||
* The URL of the output message type.
|
||||
*
|
||||
* @generated from field: string response_type_url = 4;
|
||||
*/
|
||||
this.responseTypeUrl = "";
|
||||
/**
|
||||
* If true, the response is streamed.
|
||||
*
|
||||
* @generated from field: bool response_streaming = 5;
|
||||
*/
|
||||
this.responseStreaming = false;
|
||||
/**
|
||||
* Any metadata attached to the method.
|
||||
*
|
||||
* @generated from field: repeated google.protobuf.Option options = 6;
|
||||
*/
|
||||
this.options = [];
|
||||
/**
|
||||
* The source syntax of this method.
|
||||
*
|
||||
* @generated from field: google.protobuf.Syntax syntax = 7;
|
||||
*/
|
||||
this.syntax = type_pb_js_1.Syntax.PROTO2;
|
||||
proto3_js_1.proto3.util.initPartial(data, this);
|
||||
}
|
||||
static fromBinary(bytes, options) {
|
||||
return new Method().fromBinary(bytes, options);
|
||||
}
|
||||
static fromJson(jsonValue, options) {
|
||||
return new Method().fromJson(jsonValue, options);
|
||||
}
|
||||
static fromJsonString(jsonString, options) {
|
||||
return new Method().fromJsonString(jsonString, options);
|
||||
}
|
||||
static equals(a, b) {
|
||||
return proto3_js_1.proto3.util.equals(Method, a, b);
|
||||
}
|
||||
}
|
||||
exports.Method = Method;
|
||||
Method.runtime = proto3_js_1.proto3;
|
||||
Method.typeName = "google.protobuf.Method";
|
||||
Method.fields = proto3_js_1.proto3.util.newFieldList(() => [
|
||||
{ no: 1, name: "name", kind: "scalar", T: 9 /* ScalarType.STRING */ },
|
||||
{ no: 2, name: "request_type_url", kind: "scalar", T: 9 /* ScalarType.STRING */ },
|
||||
{ no: 3, name: "request_streaming", kind: "scalar", T: 8 /* ScalarType.BOOL */ },
|
||||
{ no: 4, name: "response_type_url", kind: "scalar", T: 9 /* ScalarType.STRING */ },
|
||||
{ no: 5, name: "response_streaming", kind: "scalar", T: 8 /* ScalarType.BOOL */ },
|
||||
{ no: 6, name: "options", kind: "message", T: type_pb_js_1.Option, repeated: true },
|
||||
{ no: 7, name: "syntax", kind: "enum", T: proto3_js_1.proto3.getEnumType(type_pb_js_1.Syntax) },
|
||||
]);
|
||||
/**
|
||||
* Declares an API Interface to be included in this interface. The including
|
||||
* interface must redeclare all the methods from the included interface, but
|
||||
* documentation and options are inherited as follows:
|
||||
*
|
||||
* - If after comment and whitespace stripping, the documentation
|
||||
* string of the redeclared method is empty, it will be inherited
|
||||
* from the original method.
|
||||
*
|
||||
* - Each annotation belonging to the service config (http,
|
||||
* visibility) which is not set in the redeclared method will be
|
||||
* inherited.
|
||||
*
|
||||
* - If an http annotation is inherited, the path pattern will be
|
||||
* modified as follows. Any version prefix will be replaced by the
|
||||
* version of the including interface plus the [root][] path if
|
||||
* specified.
|
||||
*
|
||||
* Example of a simple mixin:
|
||||
*
|
||||
* package google.acl.v1;
|
||||
* service AccessControl {
|
||||
* // Get the underlying ACL object.
|
||||
* rpc GetAcl(GetAclRequest) returns (Acl) {
|
||||
* option (google.api.http).get = "/v1/{resource=**}:getAcl";
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* package google.storage.v2;
|
||||
* service Storage {
|
||||
* rpc GetAcl(GetAclRequest) returns (Acl);
|
||||
*
|
||||
* // Get a data record.
|
||||
* rpc GetData(GetDataRequest) returns (Data) {
|
||||
* option (google.api.http).get = "/v2/{resource=**}";
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* Example of a mixin configuration:
|
||||
*
|
||||
* apis:
|
||||
* - name: google.storage.v2.Storage
|
||||
* mixins:
|
||||
* - name: google.acl.v1.AccessControl
|
||||
*
|
||||
* The mixin construct implies that all methods in `AccessControl` are
|
||||
* also declared with same name and request/response types in
|
||||
* `Storage`. A documentation generator or annotation processor will
|
||||
* see the effective `Storage.GetAcl` method after inherting
|
||||
* documentation and annotations as follows:
|
||||
*
|
||||
* service Storage {
|
||||
* // Get the underlying ACL object.
|
||||
* rpc GetAcl(GetAclRequest) returns (Acl) {
|
||||
* option (google.api.http).get = "/v2/{resource=**}:getAcl";
|
||||
* }
|
||||
* ...
|
||||
* }
|
||||
*
|
||||
* Note how the version in the path pattern changed from `v1` to `v2`.
|
||||
*
|
||||
* If the `root` field in the mixin is specified, it should be a
|
||||
* relative path under which inherited HTTP paths are placed. Example:
|
||||
*
|
||||
* apis:
|
||||
* - name: google.storage.v2.Storage
|
||||
* mixins:
|
||||
* - name: google.acl.v1.AccessControl
|
||||
* root: acls
|
||||
*
|
||||
* This implies the following inherited HTTP annotation:
|
||||
*
|
||||
* service Storage {
|
||||
* // Get the underlying ACL object.
|
||||
* rpc GetAcl(GetAclRequest) returns (Acl) {
|
||||
* option (google.api.http).get = "/v2/acls/{resource=**}:getAcl";
|
||||
* }
|
||||
* ...
|
||||
* }
|
||||
*
|
||||
* @generated from message google.protobuf.Mixin
|
||||
*/
|
||||
class Mixin extends message_js_1.Message {
|
||||
constructor(data) {
|
||||
super();
|
||||
/**
|
||||
* The fully qualified name of the interface which is included.
|
||||
*
|
||||
* @generated from field: string name = 1;
|
||||
*/
|
||||
this.name = "";
|
||||
/**
|
||||
* If non-empty specifies a path under which inherited HTTP paths
|
||||
* are rooted.
|
||||
*
|
||||
* @generated from field: string root = 2;
|
||||
*/
|
||||
this.root = "";
|
||||
proto3_js_1.proto3.util.initPartial(data, this);
|
||||
}
|
||||
static fromBinary(bytes, options) {
|
||||
return new Mixin().fromBinary(bytes, options);
|
||||
}
|
||||
static fromJson(jsonValue, options) {
|
||||
return new Mixin().fromJson(jsonValue, options);
|
||||
}
|
||||
static fromJsonString(jsonString, options) {
|
||||
return new Mixin().fromJsonString(jsonString, options);
|
||||
}
|
||||
static equals(a, b) {
|
||||
return proto3_js_1.proto3.util.equals(Mixin, a, b);
|
||||
}
|
||||
}
|
||||
exports.Mixin = Mixin;
|
||||
Mixin.runtime = proto3_js_1.proto3;
|
||||
Mixin.typeName = "google.protobuf.Mixin";
|
||||
Mixin.fields = proto3_js_1.proto3.util.newFieldList(() => [
|
||||
{ no: 1, name: "name", kind: "scalar", T: 9 /* ScalarType.STRING */ },
|
||||
{ no: 2, name: "root", kind: "scalar", T: 9 /* ScalarType.STRING */ },
|
||||
]);
|
||||
+271
@@ -0,0 +1,271 @@
|
||||
import type { PartialMessage, PlainMessage } from "../../../message.js";
|
||||
import { Message } from "../../../message.js";
|
||||
import { proto2 } from "../../../proto2.js";
|
||||
import type { FieldList } from "../../../field-list.js";
|
||||
import type { BinaryReadOptions } from "../../../binary-format.js";
|
||||
import type { JsonReadOptions, JsonValue } from "../../../json-format.js";
|
||||
import { FileDescriptorProto, GeneratedCodeInfo } from "../descriptor_pb.js";
|
||||
/**
|
||||
* The version number of protocol compiler.
|
||||
*
|
||||
* @generated from message google.protobuf.compiler.Version
|
||||
*/
|
||||
export declare class Version extends Message<Version> {
|
||||
/**
|
||||
* @generated from field: optional int32 major = 1;
|
||||
*/
|
||||
major?: number;
|
||||
/**
|
||||
* @generated from field: optional int32 minor = 2;
|
||||
*/
|
||||
minor?: number;
|
||||
/**
|
||||
* @generated from field: optional int32 patch = 3;
|
||||
*/
|
||||
patch?: number;
|
||||
/**
|
||||
* A suffix for alpha, beta or rc release, e.g., "alpha-1", "rc2". It should
|
||||
* be empty for mainline stable releases.
|
||||
*
|
||||
* @generated from field: optional string suffix = 4;
|
||||
*/
|
||||
suffix?: string;
|
||||
constructor(data?: PartialMessage<Version>);
|
||||
static readonly runtime: typeof proto2;
|
||||
static readonly typeName = "google.protobuf.compiler.Version";
|
||||
static readonly fields: FieldList;
|
||||
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): Version;
|
||||
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): Version;
|
||||
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): Version;
|
||||
static equals(a: Version | PlainMessage<Version> | undefined, b: Version | PlainMessage<Version> | undefined): boolean;
|
||||
}
|
||||
/**
|
||||
* An encoded CodeGeneratorRequest is written to the plugin's stdin.
|
||||
*
|
||||
* @generated from message google.protobuf.compiler.CodeGeneratorRequest
|
||||
*/
|
||||
export declare class CodeGeneratorRequest extends Message<CodeGeneratorRequest> {
|
||||
/**
|
||||
* The .proto files that were explicitly listed on the command-line. The
|
||||
* code generator should generate code only for these files. Each file's
|
||||
* descriptor will be included in proto_file, below.
|
||||
*
|
||||
* @generated from field: repeated string file_to_generate = 1;
|
||||
*/
|
||||
fileToGenerate: string[];
|
||||
/**
|
||||
* The generator parameter passed on the command-line.
|
||||
*
|
||||
* @generated from field: optional string parameter = 2;
|
||||
*/
|
||||
parameter?: string;
|
||||
/**
|
||||
* FileDescriptorProtos for all files in files_to_generate and everything
|
||||
* they import. The files will appear in topological order, so each file
|
||||
* appears before any file that imports it.
|
||||
*
|
||||
* Note: the files listed in files_to_generate will include runtime-retention
|
||||
* options only, but all other files will include source-retention options.
|
||||
* The source_file_descriptors field below is available in case you need
|
||||
* source-retention options for files_to_generate.
|
||||
*
|
||||
* protoc guarantees that all proto_files will be written after
|
||||
* the fields above, even though this is not technically guaranteed by the
|
||||
* protobuf wire format. This theoretically could allow a plugin to stream
|
||||
* in the FileDescriptorProtos and handle them one by one rather than read
|
||||
* the entire set into memory at once. However, as of this writing, this
|
||||
* is not similarly optimized on protoc's end -- it will store all fields in
|
||||
* memory at once before sending them to the plugin.
|
||||
*
|
||||
* Type names of fields and extensions in the FileDescriptorProto are always
|
||||
* fully qualified.
|
||||
*
|
||||
* @generated from field: repeated google.protobuf.FileDescriptorProto proto_file = 15;
|
||||
*/
|
||||
protoFile: FileDescriptorProto[];
|
||||
/**
|
||||
* File descriptors with all options, including source-retention options.
|
||||
* These descriptors are only provided for the files listed in
|
||||
* files_to_generate.
|
||||
*
|
||||
* @generated from field: repeated google.protobuf.FileDescriptorProto source_file_descriptors = 17;
|
||||
*/
|
||||
sourceFileDescriptors: FileDescriptorProto[];
|
||||
/**
|
||||
* The version number of protocol compiler.
|
||||
*
|
||||
* @generated from field: optional google.protobuf.compiler.Version compiler_version = 3;
|
||||
*/
|
||||
compilerVersion?: Version;
|
||||
constructor(data?: PartialMessage<CodeGeneratorRequest>);
|
||||
static readonly runtime: typeof proto2;
|
||||
static readonly typeName = "google.protobuf.compiler.CodeGeneratorRequest";
|
||||
static readonly fields: FieldList;
|
||||
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): CodeGeneratorRequest;
|
||||
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): CodeGeneratorRequest;
|
||||
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): CodeGeneratorRequest;
|
||||
static equals(a: CodeGeneratorRequest | PlainMessage<CodeGeneratorRequest> | undefined, b: CodeGeneratorRequest | PlainMessage<CodeGeneratorRequest> | undefined): boolean;
|
||||
}
|
||||
/**
|
||||
* The plugin writes an encoded CodeGeneratorResponse to stdout.
|
||||
*
|
||||
* @generated from message google.protobuf.compiler.CodeGeneratorResponse
|
||||
*/
|
||||
export declare class CodeGeneratorResponse extends Message<CodeGeneratorResponse> {
|
||||
/**
|
||||
* Error message. If non-empty, code generation failed. The plugin process
|
||||
* should exit with status code zero even if it reports an error in this way.
|
||||
*
|
||||
* This should be used to indicate errors in .proto files which prevent the
|
||||
* code generator from generating correct code. Errors which indicate a
|
||||
* problem in protoc itself -- such as the input CodeGeneratorRequest being
|
||||
* unparseable -- should be reported by writing a message to stderr and
|
||||
* exiting with a non-zero status code.
|
||||
*
|
||||
* @generated from field: optional string error = 1;
|
||||
*/
|
||||
error?: string;
|
||||
/**
|
||||
* A bitmask of supported features that the code generator supports.
|
||||
* This is a bitwise "or" of values from the Feature enum.
|
||||
*
|
||||
* @generated from field: optional uint64 supported_features = 2;
|
||||
*/
|
||||
supportedFeatures?: bigint;
|
||||
/**
|
||||
* The minimum edition this plugin supports. This will be treated as an
|
||||
* Edition enum, but we want to allow unknown values. It should be specified
|
||||
* according the edition enum value, *not* the edition number. Only takes
|
||||
* effect for plugins that have FEATURE_SUPPORTS_EDITIONS set.
|
||||
*
|
||||
* @generated from field: optional int32 minimum_edition = 3;
|
||||
*/
|
||||
minimumEdition?: number;
|
||||
/**
|
||||
* The maximum edition this plugin supports. This will be treated as an
|
||||
* Edition enum, but we want to allow unknown values. It should be specified
|
||||
* according the edition enum value, *not* the edition number. Only takes
|
||||
* effect for plugins that have FEATURE_SUPPORTS_EDITIONS set.
|
||||
*
|
||||
* @generated from field: optional int32 maximum_edition = 4;
|
||||
*/
|
||||
maximumEdition?: number;
|
||||
/**
|
||||
* @generated from field: repeated google.protobuf.compiler.CodeGeneratorResponse.File file = 15;
|
||||
*/
|
||||
file: CodeGeneratorResponse_File[];
|
||||
constructor(data?: PartialMessage<CodeGeneratorResponse>);
|
||||
static readonly runtime: typeof proto2;
|
||||
static readonly typeName = "google.protobuf.compiler.CodeGeneratorResponse";
|
||||
static readonly fields: FieldList;
|
||||
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): CodeGeneratorResponse;
|
||||
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): CodeGeneratorResponse;
|
||||
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): CodeGeneratorResponse;
|
||||
static equals(a: CodeGeneratorResponse | PlainMessage<CodeGeneratorResponse> | undefined, b: CodeGeneratorResponse | PlainMessage<CodeGeneratorResponse> | undefined): boolean;
|
||||
}
|
||||
/**
|
||||
* Sync with code_generator.h.
|
||||
*
|
||||
* @generated from enum google.protobuf.compiler.CodeGeneratorResponse.Feature
|
||||
*/
|
||||
export declare enum CodeGeneratorResponse_Feature {
|
||||
/**
|
||||
* @generated from enum value: FEATURE_NONE = 0;
|
||||
*/
|
||||
NONE = 0,
|
||||
/**
|
||||
* @generated from enum value: FEATURE_PROTO3_OPTIONAL = 1;
|
||||
*/
|
||||
PROTO3_OPTIONAL = 1,
|
||||
/**
|
||||
* @generated from enum value: FEATURE_SUPPORTS_EDITIONS = 2;
|
||||
*/
|
||||
SUPPORTS_EDITIONS = 2
|
||||
}
|
||||
/**
|
||||
* Represents a single generated file.
|
||||
*
|
||||
* @generated from message google.protobuf.compiler.CodeGeneratorResponse.File
|
||||
*/
|
||||
export declare class CodeGeneratorResponse_File extends Message<CodeGeneratorResponse_File> {
|
||||
/**
|
||||
* The file name, relative to the output directory. The name must not
|
||||
* contain "." or ".." components and must be relative, not be absolute (so,
|
||||
* the file cannot lie outside the output directory). "/" must be used as
|
||||
* the path separator, not "\".
|
||||
*
|
||||
* If the name is omitted, the content will be appended to the previous
|
||||
* file. This allows the generator to break large files into small chunks,
|
||||
* and allows the generated text to be streamed back to protoc so that large
|
||||
* files need not reside completely in memory at one time. Note that as of
|
||||
* this writing protoc does not optimize for this -- it will read the entire
|
||||
* CodeGeneratorResponse before writing files to disk.
|
||||
*
|
||||
* @generated from field: optional string name = 1;
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* If non-empty, indicates that the named file should already exist, and the
|
||||
* content here is to be inserted into that file at a defined insertion
|
||||
* point. This feature allows a code generator to extend the output
|
||||
* produced by another code generator. The original generator may provide
|
||||
* insertion points by placing special annotations in the file that look
|
||||
* like:
|
||||
* @@protoc_insertion_point(NAME)
|
||||
* The annotation can have arbitrary text before and after it on the line,
|
||||
* which allows it to be placed in a comment. NAME should be replaced with
|
||||
* an identifier naming the point -- this is what other generators will use
|
||||
* as the insertion_point. Code inserted at this point will be placed
|
||||
* immediately above the line containing the insertion point (thus multiple
|
||||
* insertions to the same point will come out in the order they were added).
|
||||
* The double-@ is intended to make it unlikely that the generated code
|
||||
* could contain things that look like insertion points by accident.
|
||||
*
|
||||
* For example, the C++ code generator places the following line in the
|
||||
* .pb.h files that it generates:
|
||||
* // @@protoc_insertion_point(namespace_scope)
|
||||
* This line appears within the scope of the file's package namespace, but
|
||||
* outside of any particular class. Another plugin can then specify the
|
||||
* insertion_point "namespace_scope" to generate additional classes or
|
||||
* other declarations that should be placed in this scope.
|
||||
*
|
||||
* Note that if the line containing the insertion point begins with
|
||||
* whitespace, the same whitespace will be added to every line of the
|
||||
* inserted text. This is useful for languages like Python, where
|
||||
* indentation matters. In these languages, the insertion point comment
|
||||
* should be indented the same amount as any inserted code will need to be
|
||||
* in order to work correctly in that context.
|
||||
*
|
||||
* The code generator that generates the initial file and the one which
|
||||
* inserts into it must both run as part of a single invocation of protoc.
|
||||
* Code generators are executed in the order in which they appear on the
|
||||
* command line.
|
||||
*
|
||||
* If |insertion_point| is present, |name| must also be present.
|
||||
*
|
||||
* @generated from field: optional string insertion_point = 2;
|
||||
*/
|
||||
insertionPoint?: string;
|
||||
/**
|
||||
* The file contents.
|
||||
*
|
||||
* @generated from field: optional string content = 15;
|
||||
*/
|
||||
content?: string;
|
||||
/**
|
||||
* Information describing the file content being inserted. If an insertion
|
||||
* point is used, this information will be appropriately offset and inserted
|
||||
* into the code generation metadata for the generated files.
|
||||
*
|
||||
* @generated from field: optional google.protobuf.GeneratedCodeInfo generated_code_info = 16;
|
||||
*/
|
||||
generatedCodeInfo?: GeneratedCodeInfo;
|
||||
constructor(data?: PartialMessage<CodeGeneratorResponse_File>);
|
||||
static readonly runtime: typeof proto2;
|
||||
static readonly typeName = "google.protobuf.compiler.CodeGeneratorResponse.File";
|
||||
static readonly fields: FieldList;
|
||||
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): CodeGeneratorResponse_File;
|
||||
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): CodeGeneratorResponse_File;
|
||||
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): CodeGeneratorResponse_File;
|
||||
static equals(a: CodeGeneratorResponse_File | PlainMessage<CodeGeneratorResponse_File> | undefined, b: CodeGeneratorResponse_File | PlainMessage<CodeGeneratorResponse_File> | undefined): boolean;
|
||||
}
|
||||
+219
@@ -0,0 +1,219 @@
|
||||
"use strict";
|
||||
// Copyright 2021-2024 Buf Technologies, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.CodeGeneratorResponse_File = exports.CodeGeneratorResponse_Feature = exports.CodeGeneratorResponse = exports.CodeGeneratorRequest = exports.Version = void 0;
|
||||
const message_js_1 = require("../../../message.js");
|
||||
const proto2_js_1 = require("../../../proto2.js");
|
||||
const descriptor_pb_js_1 = require("../descriptor_pb.js");
|
||||
/**
|
||||
* The version number of protocol compiler.
|
||||
*
|
||||
* @generated from message google.protobuf.compiler.Version
|
||||
*/
|
||||
class Version extends message_js_1.Message {
|
||||
constructor(data) {
|
||||
super();
|
||||
proto2_js_1.proto2.util.initPartial(data, this);
|
||||
}
|
||||
static fromBinary(bytes, options) {
|
||||
return new Version().fromBinary(bytes, options);
|
||||
}
|
||||
static fromJson(jsonValue, options) {
|
||||
return new Version().fromJson(jsonValue, options);
|
||||
}
|
||||
static fromJsonString(jsonString, options) {
|
||||
return new Version().fromJsonString(jsonString, options);
|
||||
}
|
||||
static equals(a, b) {
|
||||
return proto2_js_1.proto2.util.equals(Version, a, b);
|
||||
}
|
||||
}
|
||||
exports.Version = Version;
|
||||
Version.runtime = proto2_js_1.proto2;
|
||||
Version.typeName = "google.protobuf.compiler.Version";
|
||||
Version.fields = proto2_js_1.proto2.util.newFieldList(() => [
|
||||
{ no: 1, name: "major", kind: "scalar", T: 5 /* ScalarType.INT32 */, opt: true },
|
||||
{ no: 2, name: "minor", kind: "scalar", T: 5 /* ScalarType.INT32 */, opt: true },
|
||||
{ no: 3, name: "patch", kind: "scalar", T: 5 /* ScalarType.INT32 */, opt: true },
|
||||
{ no: 4, name: "suffix", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true },
|
||||
]);
|
||||
/**
|
||||
* An encoded CodeGeneratorRequest is written to the plugin's stdin.
|
||||
*
|
||||
* @generated from message google.protobuf.compiler.CodeGeneratorRequest
|
||||
*/
|
||||
class CodeGeneratorRequest extends message_js_1.Message {
|
||||
constructor(data) {
|
||||
super();
|
||||
/**
|
||||
* The .proto files that were explicitly listed on the command-line. The
|
||||
* code generator should generate code only for these files. Each file's
|
||||
* descriptor will be included in proto_file, below.
|
||||
*
|
||||
* @generated from field: repeated string file_to_generate = 1;
|
||||
*/
|
||||
this.fileToGenerate = [];
|
||||
/**
|
||||
* FileDescriptorProtos for all files in files_to_generate and everything
|
||||
* they import. The files will appear in topological order, so each file
|
||||
* appears before any file that imports it.
|
||||
*
|
||||
* Note: the files listed in files_to_generate will include runtime-retention
|
||||
* options only, but all other files will include source-retention options.
|
||||
* The source_file_descriptors field below is available in case you need
|
||||
* source-retention options for files_to_generate.
|
||||
*
|
||||
* protoc guarantees that all proto_files will be written after
|
||||
* the fields above, even though this is not technically guaranteed by the
|
||||
* protobuf wire format. This theoretically could allow a plugin to stream
|
||||
* in the FileDescriptorProtos and handle them one by one rather than read
|
||||
* the entire set into memory at once. However, as of this writing, this
|
||||
* is not similarly optimized on protoc's end -- it will store all fields in
|
||||
* memory at once before sending them to the plugin.
|
||||
*
|
||||
* Type names of fields and extensions in the FileDescriptorProto are always
|
||||
* fully qualified.
|
||||
*
|
||||
* @generated from field: repeated google.protobuf.FileDescriptorProto proto_file = 15;
|
||||
*/
|
||||
this.protoFile = [];
|
||||
/**
|
||||
* File descriptors with all options, including source-retention options.
|
||||
* These descriptors are only provided for the files listed in
|
||||
* files_to_generate.
|
||||
*
|
||||
* @generated from field: repeated google.protobuf.FileDescriptorProto source_file_descriptors = 17;
|
||||
*/
|
||||
this.sourceFileDescriptors = [];
|
||||
proto2_js_1.proto2.util.initPartial(data, this);
|
||||
}
|
||||
static fromBinary(bytes, options) {
|
||||
return new CodeGeneratorRequest().fromBinary(bytes, options);
|
||||
}
|
||||
static fromJson(jsonValue, options) {
|
||||
return new CodeGeneratorRequest().fromJson(jsonValue, options);
|
||||
}
|
||||
static fromJsonString(jsonString, options) {
|
||||
return new CodeGeneratorRequest().fromJsonString(jsonString, options);
|
||||
}
|
||||
static equals(a, b) {
|
||||
return proto2_js_1.proto2.util.equals(CodeGeneratorRequest, a, b);
|
||||
}
|
||||
}
|
||||
exports.CodeGeneratorRequest = CodeGeneratorRequest;
|
||||
CodeGeneratorRequest.runtime = proto2_js_1.proto2;
|
||||
CodeGeneratorRequest.typeName = "google.protobuf.compiler.CodeGeneratorRequest";
|
||||
CodeGeneratorRequest.fields = proto2_js_1.proto2.util.newFieldList(() => [
|
||||
{ no: 1, name: "file_to_generate", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true },
|
||||
{ no: 2, name: "parameter", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true },
|
||||
{ no: 15, name: "proto_file", kind: "message", T: descriptor_pb_js_1.FileDescriptorProto, repeated: true },
|
||||
{ no: 17, name: "source_file_descriptors", kind: "message", T: descriptor_pb_js_1.FileDescriptorProto, repeated: true },
|
||||
{ no: 3, name: "compiler_version", kind: "message", T: Version, opt: true },
|
||||
]);
|
||||
/**
|
||||
* The plugin writes an encoded CodeGeneratorResponse to stdout.
|
||||
*
|
||||
* @generated from message google.protobuf.compiler.CodeGeneratorResponse
|
||||
*/
|
||||
class CodeGeneratorResponse extends message_js_1.Message {
|
||||
constructor(data) {
|
||||
super();
|
||||
/**
|
||||
* @generated from field: repeated google.protobuf.compiler.CodeGeneratorResponse.File file = 15;
|
||||
*/
|
||||
this.file = [];
|
||||
proto2_js_1.proto2.util.initPartial(data, this);
|
||||
}
|
||||
static fromBinary(bytes, options) {
|
||||
return new CodeGeneratorResponse().fromBinary(bytes, options);
|
||||
}
|
||||
static fromJson(jsonValue, options) {
|
||||
return new CodeGeneratorResponse().fromJson(jsonValue, options);
|
||||
}
|
||||
static fromJsonString(jsonString, options) {
|
||||
return new CodeGeneratorResponse().fromJsonString(jsonString, options);
|
||||
}
|
||||
static equals(a, b) {
|
||||
return proto2_js_1.proto2.util.equals(CodeGeneratorResponse, a, b);
|
||||
}
|
||||
}
|
||||
exports.CodeGeneratorResponse = CodeGeneratorResponse;
|
||||
CodeGeneratorResponse.runtime = proto2_js_1.proto2;
|
||||
CodeGeneratorResponse.typeName = "google.protobuf.compiler.CodeGeneratorResponse";
|
||||
CodeGeneratorResponse.fields = proto2_js_1.proto2.util.newFieldList(() => [
|
||||
{ no: 1, name: "error", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true },
|
||||
{ no: 2, name: "supported_features", kind: "scalar", T: 4 /* ScalarType.UINT64 */, opt: true },
|
||||
{ no: 3, name: "minimum_edition", kind: "scalar", T: 5 /* ScalarType.INT32 */, opt: true },
|
||||
{ no: 4, name: "maximum_edition", kind: "scalar", T: 5 /* ScalarType.INT32 */, opt: true },
|
||||
{ no: 15, name: "file", kind: "message", T: CodeGeneratorResponse_File, repeated: true },
|
||||
]);
|
||||
/**
|
||||
* Sync with code_generator.h.
|
||||
*
|
||||
* @generated from enum google.protobuf.compiler.CodeGeneratorResponse.Feature
|
||||
*/
|
||||
var CodeGeneratorResponse_Feature;
|
||||
(function (CodeGeneratorResponse_Feature) {
|
||||
/**
|
||||
* @generated from enum value: FEATURE_NONE = 0;
|
||||
*/
|
||||
CodeGeneratorResponse_Feature[CodeGeneratorResponse_Feature["NONE"] = 0] = "NONE";
|
||||
/**
|
||||
* @generated from enum value: FEATURE_PROTO3_OPTIONAL = 1;
|
||||
*/
|
||||
CodeGeneratorResponse_Feature[CodeGeneratorResponse_Feature["PROTO3_OPTIONAL"] = 1] = "PROTO3_OPTIONAL";
|
||||
/**
|
||||
* @generated from enum value: FEATURE_SUPPORTS_EDITIONS = 2;
|
||||
*/
|
||||
CodeGeneratorResponse_Feature[CodeGeneratorResponse_Feature["SUPPORTS_EDITIONS"] = 2] = "SUPPORTS_EDITIONS";
|
||||
})(CodeGeneratorResponse_Feature || (exports.CodeGeneratorResponse_Feature = CodeGeneratorResponse_Feature = {}));
|
||||
// Retrieve enum metadata with: proto2.getEnumType(CodeGeneratorResponse_Feature)
|
||||
proto2_js_1.proto2.util.setEnumType(CodeGeneratorResponse_Feature, "google.protobuf.compiler.CodeGeneratorResponse.Feature", [
|
||||
{ no: 0, name: "FEATURE_NONE" },
|
||||
{ no: 1, name: "FEATURE_PROTO3_OPTIONAL" },
|
||||
{ no: 2, name: "FEATURE_SUPPORTS_EDITIONS" },
|
||||
]);
|
||||
/**
|
||||
* Represents a single generated file.
|
||||
*
|
||||
* @generated from message google.protobuf.compiler.CodeGeneratorResponse.File
|
||||
*/
|
||||
class CodeGeneratorResponse_File extends message_js_1.Message {
|
||||
constructor(data) {
|
||||
super();
|
||||
proto2_js_1.proto2.util.initPartial(data, this);
|
||||
}
|
||||
static fromBinary(bytes, options) {
|
||||
return new CodeGeneratorResponse_File().fromBinary(bytes, options);
|
||||
}
|
||||
static fromJson(jsonValue, options) {
|
||||
return new CodeGeneratorResponse_File().fromJson(jsonValue, options);
|
||||
}
|
||||
static fromJsonString(jsonString, options) {
|
||||
return new CodeGeneratorResponse_File().fromJsonString(jsonString, options);
|
||||
}
|
||||
static equals(a, b) {
|
||||
return proto2_js_1.proto2.util.equals(CodeGeneratorResponse_File, a, b);
|
||||
}
|
||||
}
|
||||
exports.CodeGeneratorResponse_File = CodeGeneratorResponse_File;
|
||||
CodeGeneratorResponse_File.runtime = proto2_js_1.proto2;
|
||||
CodeGeneratorResponse_File.typeName = "google.protobuf.compiler.CodeGeneratorResponse.File";
|
||||
CodeGeneratorResponse_File.fields = proto2_js_1.proto2.util.newFieldList(() => [
|
||||
{ no: 1, name: "name", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true },
|
||||
{ no: 2, name: "insertion_point", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true },
|
||||
{ no: 15, name: "content", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true },
|
||||
{ no: 16, name: "generated_code_info", kind: "message", T: descriptor_pb_js_1.GeneratedCodeInfo, opt: true },
|
||||
]);
|
||||
+2277
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user