Toutes les app sur les cotés
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
.DEFAULT_GOAL := help
|
.DEFAULT_GOAL := help
|
||||||
|
include .env
|
||||||
PORT ?= 8000
|
PORT ?= 8000
|
||||||
|
|
||||||
.PHONY: help install config serve sync export deploy
|
.PHONY: help install config serve sync fetch export deploy
|
||||||
|
|
||||||
help:
|
help:
|
||||||
@echo "Cibles disponibles :"
|
@echo "Cibles disponibles :"
|
||||||
@@ -9,6 +10,7 @@ help:
|
|||||||
@echo " make config - cree .env et src/config.json si absents"
|
@echo " make config - cree .env et src/config.json si absents"
|
||||||
@echo " make serve - lance l'apercu + l'editeur en local (PORT=$(PORT))"
|
@echo " make serve - lance l'apercu + l'editeur en local (PORT=$(PORT))"
|
||||||
@echo " make sync - recupere dans src/config.json les apps YunoHost manquantes"
|
@echo " make sync - recupere dans src/config.json les apps YunoHost manquantes"
|
||||||
|
@echo " make fetch - recupere src/config.json depuis le serveur (config actuellement deployee)"
|
||||||
@echo " make export - copie src/ vers output/ (equivalent au bouton Exporter)"
|
@echo " make export - copie src/ vers output/ (equivalent au bouton Exporter)"
|
||||||
@echo " make deploy - envoie output/ sur le serveur YunoHost"
|
@echo " make deploy - envoie output/ sur le serveur YunoHost"
|
||||||
|
|
||||||
@@ -42,3 +44,7 @@ export:
|
|||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
scripts/deploy.sh
|
scripts/deploy.sh
|
||||||
|
|
||||||
|
fetch:
|
||||||
|
scp akbar:/var/www/$(REMOTE_USER)/www/config.json src/config.json
|
||||||
|
@echo "Config de la HomePage téléchargée depuis le serveur"
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ import shutil
|
|||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
|
from yunohost_sync import build_app_list, fetch_remote_apps, known_urls, load_env, sync_target
|
||||||
|
|
||||||
PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 8000
|
PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 8000
|
||||||
|
|
||||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||||
@@ -71,6 +74,10 @@ class ReloadingHandler(http.server.SimpleHTTPRequestHandler):
|
|||||||
self.wfile.write(body)
|
self.wfile.write(body)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if self.path == "/__catalog":
|
||||||
|
self._send_catalog()
|
||||||
|
return
|
||||||
|
|
||||||
if self.path in ("/", "/index.html"):
|
if self.path in ("/", "/index.html"):
|
||||||
try:
|
try:
|
||||||
html = (SRC_DIR / "index.html").read_bytes()
|
html = (SRC_DIR / "index.html").read_bytes()
|
||||||
@@ -114,6 +121,22 @@ class ReloadingHandler(http.server.SimpleHTTPRequestHandler):
|
|||||||
|
|
||||||
self.send_error(404)
|
self.send_error(404)
|
||||||
|
|
||||||
|
def _send_catalog(self):
|
||||||
|
env = load_env()
|
||||||
|
ssh_target, domain, self_app_id = sync_target(env)
|
||||||
|
if not ssh_target or not domain:
|
||||||
|
self._send_json({"error": "SYNC_SSH_HOST (ou REMOTE_HOST) et PORTAL_DOMAIN manquants dans .env"})
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
with open(SRC_DIR / "config.json", encoding="utf-8") as f:
|
||||||
|
config = json.load(f)
|
||||||
|
remote = fetch_remote_apps(ssh_target, domain)
|
||||||
|
apps = build_app_list(remote, domain, self_app_id, exclude_urls=known_urls(config))
|
||||||
|
except Exception as exc: # SSH/JSON/reseau : on remonte un message lisible
|
||||||
|
self._send_json({"error": str(exc)})
|
||||||
|
return
|
||||||
|
self._send_json({"apps": apps})
|
||||||
|
|
||||||
def _send_json(self, payload):
|
def _send_json(self, payload):
|
||||||
body = json.dumps(payload).encode()
|
body = json.dumps(payload).encode()
|
||||||
self.send_response(200)
|
self.send_response(200)
|
||||||
|
|||||||
+18
-116
@@ -10,44 +10,16 @@ PORTAL_DOMAIN, REMOTE_USER) ; les arguments en ligne de commande, s'ils sont
|
|||||||
fournis, ont priorite sur .env.
|
fournis, ont priorite sur .env.
|
||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
import subprocess
|
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
PROJECT_ROOT = SCRIPT_DIR.parent
|
from yunohost_sync import CONFIG_PATH, build_app_list, fetch_remote_apps, known_urls, load_env, sync_target
|
||||||
CONFIG_PATH = PROJECT_ROOT / "src" / "config.json"
|
|
||||||
ENV_PATH = PROJECT_ROOT / ".env"
|
|
||||||
|
|
||||||
|
|
||||||
def load_env():
|
|
||||||
env = {}
|
|
||||||
if not ENV_PATH.exists():
|
|
||||||
return env
|
|
||||||
for line in ENV_PATH.read_text(encoding="utf-8").splitlines():
|
|
||||||
line = line.strip()
|
|
||||||
if not line or line.startswith("#") or "=" not in line:
|
|
||||||
continue
|
|
||||||
key, value = line.split("=", 1)
|
|
||||||
env[key.strip()] = value.strip()
|
|
||||||
return env
|
|
||||||
|
|
||||||
|
|
||||||
ENV = load_env()
|
ENV = load_env()
|
||||||
|
arg_target = sys.argv[1] if len(sys.argv) > 1 else None
|
||||||
SYNC_SSH_USER = ENV.get("SYNC_SSH_USER", "root")
|
arg_domain = sys.argv[2] if len(sys.argv) > 2 else None
|
||||||
SYNC_SSH_HOST = ENV.get("SYNC_SSH_HOST") or ENV.get("REMOTE_HOST")
|
SSH_TARGET, DOMAIN, SELF_APP_ID = sync_target(ENV, arg_target, arg_domain)
|
||||||
PORTAL_DOMAIN = ENV.get("PORTAL_DOMAIN") or ENV.get("REMOTE_HOST")
|
|
||||||
SELF_APP_ID = ENV.get("REMOTE_USER", "") # cette page elle-meme, jamais ajoutee
|
|
||||||
|
|
||||||
if len(sys.argv) > 1:
|
|
||||||
SSH_TARGET = sys.argv[1]
|
|
||||||
elif SYNC_SSH_HOST:
|
|
||||||
SSH_TARGET = f"{SYNC_SSH_USER}@{SYNC_SSH_HOST}"
|
|
||||||
else:
|
|
||||||
SSH_TARGET = None
|
|
||||||
|
|
||||||
DOMAIN = sys.argv[2] if len(sys.argv) > 2 else PORTAL_DOMAIN
|
|
||||||
|
|
||||||
if not SSH_TARGET or not DOMAIN:
|
if not SSH_TARGET or not DOMAIN:
|
||||||
print(
|
print(
|
||||||
@@ -59,103 +31,33 @@ if not SSH_TARGET or not DOMAIN:
|
|||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
PALETTE = [
|
|
||||||
"#1db9c3", "#ff5a1f", "#2094f3", "#ffc230", "#00a651", "#e6721a",
|
|
||||||
"#b71c1c", "#f39c12", "#0d3b66", "#3b8a8c", "#568d34", "#609926",
|
|
||||||
"#d24939", "#0066cc", "#8e44ad", "#16a085", "#c0392b", "#2c3e50",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def color_of(name):
|
|
||||||
h = 0
|
|
||||||
for ch in name:
|
|
||||||
h = (h * 31 + ord(ch)) & 0xFFFFFFFF
|
|
||||||
return PALETTE[h % len(PALETTE)]
|
|
||||||
|
|
||||||
|
|
||||||
def initials_of(name):
|
|
||||||
words = name.strip().split()
|
|
||||||
if len(words) == 1:
|
|
||||||
return words[0][:2].upper()
|
|
||||||
return (words[0][0] + words[1][0]).upper()
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_remote_apps():
|
|
||||||
cmd = ["ssh", "-o", "BatchMode=yes", SSH_TARGET, f"cat /etc/yunohost/portal/{DOMAIN}.json"]
|
|
||||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
||||||
if result.returncode != 0:
|
|
||||||
print(f"Erreur SSH: {result.stderr}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
return json.loads(result.stdout)
|
|
||||||
|
|
||||||
|
|
||||||
def normalize_url(url):
|
|
||||||
return url.replace("https://", "").replace("http://", "").rstrip("/").lower()
|
|
||||||
|
|
||||||
|
|
||||||
def known_urls(config):
|
|
||||||
urls = set()
|
|
||||||
for app in config.get("pinned", []):
|
|
||||||
urls.add(normalize_url(app["url"]))
|
|
||||||
for group in config.get("groups", []):
|
|
||||||
for app in group["apps"]:
|
|
||||||
urls.add(normalize_url(app["url"]))
|
|
||||||
for app in config.get("ungrouped", []):
|
|
||||||
urls.add(normalize_url(app["url"]))
|
|
||||||
return urls
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
with open(CONFIG_PATH, encoding="utf-8") as f:
|
with open(CONFIG_PATH, encoding="utf-8") as f:
|
||||||
config = json.load(f)
|
config = json.load(f)
|
||||||
|
|
||||||
remote = fetch_remote_apps()
|
try:
|
||||||
remote_apps = remote.get("apps", {})
|
remote = fetch_remote_apps(SSH_TARGET, DOMAIN)
|
||||||
|
except (RuntimeError, json.JSONDecodeError) as exc:
|
||||||
|
print(f"Erreur SSH: {exc}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
existing_urls = known_urls(config)
|
existing_urls = known_urls(config)
|
||||||
|
to_add = build_app_list(remote, DOMAIN, SELF_APP_ID, exclude_urls=existing_urls)
|
||||||
|
|
||||||
added = []
|
if not to_add:
|
||||||
for perm_key, info in sorted(remote_apps.items()):
|
|
||||||
if not perm_key.endswith(".main"):
|
|
||||||
continue
|
|
||||||
app_id = perm_key[: -len(".main")]
|
|
||||||
if app_id == SELF_APP_ID:
|
|
||||||
continue
|
|
||||||
|
|
||||||
url = info["url"]
|
|
||||||
if not url.startswith("http"):
|
|
||||||
url = "https://" + url
|
|
||||||
if not url.endswith("/"):
|
|
||||||
url += "/"
|
|
||||||
|
|
||||||
if normalize_url(url) in existing_urls:
|
|
||||||
continue
|
|
||||||
|
|
||||||
name = info.get("label", app_id)
|
|
||||||
app = {
|
|
||||||
"id": app_id,
|
|
||||||
"name": name,
|
|
||||||
"url": url,
|
|
||||||
"color": color_of(name),
|
|
||||||
"initials": initials_of(name),
|
|
||||||
}
|
|
||||||
logo = info.get("logo")
|
|
||||||
if logo:
|
|
||||||
app["icon"] = f"https://{DOMAIN}{logo}"
|
|
||||||
|
|
||||||
config.setdefault("ungrouped", []).append(app)
|
|
||||||
added.append(name)
|
|
||||||
|
|
||||||
if not added:
|
|
||||||
print("Rien a ajouter, config.json est deja a jour.")
|
print("Rien a ajouter, config.json est deja a jour.")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
config.setdefault("ungrouped", []).extend(to_add)
|
||||||
|
|
||||||
with open(CONFIG_PATH, "w", encoding="utf-8") as f:
|
with open(CONFIG_PATH, "w", encoding="utf-8") as f:
|
||||||
json.dump(config, f, ensure_ascii=False, indent=2)
|
json.dump(config, f, ensure_ascii=False, indent=2)
|
||||||
f.write("\n")
|
f.write("\n")
|
||||||
|
|
||||||
print(f"{len(added)} application(s) ajoutee(s) dans \"ungrouped\" :")
|
print(f"{len(to_add)} application(s) ajoutee(s) dans \"ungrouped\" :")
|
||||||
for name in added:
|
for app in to_add:
|
||||||
print(f" - {name}")
|
print(f" - {app['name']}")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
+71
-1
@@ -79,11 +79,81 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#editor-root {
|
#editor-root {
|
||||||
max-width: 1100px;
|
max-width: 1500px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.editor-layout {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-center {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-column {
|
||||||
|
width: 200px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
position: sticky;
|
||||||
|
top: 88px;
|
||||||
|
max-height: calc(100vh - 112px);
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-column h2 {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--muted);
|
||||||
|
margin: 0 0 12px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-message {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-error {
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-list {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(88px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-tile {
|
||||||
|
cursor: grab;
|
||||||
|
background: var(--panel-bg);
|
||||||
|
border: 1px dashed var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-tile:hover {
|
||||||
|
border-color: var(--purple);
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-tile .tile-icon {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-tile .tile-name {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
.catalog-column {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.group-card {
|
.group-card {
|
||||||
background: var(--panel-bg);
|
background: var(--panel-bg);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
|
|||||||
+140
-11
@@ -1,6 +1,8 @@
|
|||||||
let state = null;
|
let state = null;
|
||||||
let dirty = false;
|
let dirty = false;
|
||||||
let dragSource = null; // { appId } or { groupId } currently being dragged
|
let dragSource = null; // { appId }, { groupId } or { catalogApp } currently being dragged
|
||||||
|
let catalog = []; // apps YunoHost pas encore dans le portail (voir /__catalog)
|
||||||
|
let catalogError = null;
|
||||||
|
|
||||||
const PALETTE = [
|
const PALETTE = [
|
||||||
"#1db9c3", "#ff5a1f", "#2094f3", "#ffc230", "#00a651", "#e6721a",
|
"#1db9c3", "#ff5a1f", "#2094f3", "#ffc230", "#00a651", "#e6721a",
|
||||||
@@ -147,7 +149,7 @@ function renderAddTile(onAdd) {
|
|||||||
|
|
||||||
function attachGridDropzone(gridEl, targetListGetter) {
|
function attachGridDropzone(gridEl, targetListGetter) {
|
||||||
gridEl.addEventListener("dragover", (e) => {
|
gridEl.addEventListener("dragover", (e) => {
|
||||||
if (!dragSource || !dragSource.appId) return;
|
if (!dragSource || (!dragSource.appId && !dragSource.catalogApp)) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
gridEl.classList.add("drag-over");
|
gridEl.classList.add("drag-over");
|
||||||
});
|
});
|
||||||
@@ -157,19 +159,112 @@ function attachGridDropzone(gridEl, targetListGetter) {
|
|||||||
gridEl.addEventListener("drop", (e) => {
|
gridEl.addEventListener("drop", (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
gridEl.classList.remove("drag-over");
|
gridEl.classList.remove("drag-over");
|
||||||
if (!dragSource || !dragSource.appId) return;
|
if (!dragSource) return;
|
||||||
const appId = dragSource.appId;
|
|
||||||
const loc = findAppLocation(appId);
|
|
||||||
if (!loc) return;
|
|
||||||
const [app] = loc.list.splice(loc.index, 1);
|
|
||||||
const targetList = targetListGetter();
|
const targetList = targetListGetter();
|
||||||
const dropIndex = computeDropIndexGrid(gridEl, e.clientX, e.clientY);
|
const dropIndex = computeDropIndexGrid(gridEl, e.clientX, e.clientY);
|
||||||
|
|
||||||
|
if (dragSource.catalogApp) {
|
||||||
|
const app = { ...dragSource.catalogApp };
|
||||||
|
targetList.splice(Math.min(dropIndex, targetList.length), 0, app);
|
||||||
|
catalog = catalog.filter((a) => a.id !== app.id);
|
||||||
|
markDirty();
|
||||||
|
render();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dragSource.appId) {
|
||||||
|
const loc = findAppLocation(dragSource.appId);
|
||||||
|
if (!loc) return;
|
||||||
|
const [app] = loc.list.splice(loc.index, 1);
|
||||||
targetList.splice(Math.min(dropIndex, targetList.length), 0, app);
|
targetList.splice(Math.min(dropIndex, targetList.length), 0, app);
|
||||||
markDirty();
|
markDirty();
|
||||||
render();
|
render();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addCatalogAppTo(app, targetList) {
|
||||||
|
targetList.push({ ...app });
|
||||||
|
catalog = catalog.filter((a) => a.id !== app.id);
|
||||||
|
markDirty();
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCatalogTile(app) {
|
||||||
|
const tile = document.createElement("div");
|
||||||
|
tile.className = "app-tile catalog-tile";
|
||||||
|
tile.draggable = true;
|
||||||
|
tile.title = "Glisser dans le portail, ou cliquer pour ajouter dans \"Sans groupe\"";
|
||||||
|
|
||||||
|
tile.addEventListener("dragstart", (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
dragSource = { catalogApp: app };
|
||||||
|
e.dataTransfer.effectAllowed = "copy";
|
||||||
|
e.dataTransfer.setData("text/plain", app.id);
|
||||||
|
tile.classList.add("dragging");
|
||||||
|
});
|
||||||
|
tile.addEventListener("dragend", (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
tile.classList.remove("dragging");
|
||||||
|
dragSource = null;
|
||||||
|
});
|
||||||
|
tile.addEventListener("click", () => {
|
||||||
|
state.ungrouped = state.ungrouped || [];
|
||||||
|
addCatalogAppTo(app, state.ungrouped);
|
||||||
|
});
|
||||||
|
|
||||||
|
const icon = document.createElement("div");
|
||||||
|
icon.className = "tile-icon";
|
||||||
|
if (app.icon) {
|
||||||
|
const img = document.createElement("img");
|
||||||
|
img.src = app.icon;
|
||||||
|
img.alt = "";
|
||||||
|
icon.appendChild(img);
|
||||||
|
} else {
|
||||||
|
icon.style.background = app.color || "#8e8e8e";
|
||||||
|
icon.textContent = app.initials || initialsOf(app.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = document.createElement("div");
|
||||||
|
name.className = "tile-name";
|
||||||
|
name.textContent = app.name;
|
||||||
|
|
||||||
|
tile.appendChild(icon);
|
||||||
|
tile.appendChild(name);
|
||||||
|
return tile;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCatalogColumn(title, apps) {
|
||||||
|
const column = document.createElement("aside");
|
||||||
|
column.className = "catalog-column";
|
||||||
|
|
||||||
|
const heading = document.createElement("h2");
|
||||||
|
heading.textContent = title;
|
||||||
|
column.appendChild(heading);
|
||||||
|
|
||||||
|
if (catalogError) {
|
||||||
|
const msg = document.createElement("p");
|
||||||
|
msg.className = "catalog-message catalog-error";
|
||||||
|
msg.textContent = catalogError;
|
||||||
|
column.appendChild(msg);
|
||||||
|
return column;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apps.length === 0) {
|
||||||
|
const msg = document.createElement("p");
|
||||||
|
msg.className = "catalog-message";
|
||||||
|
msg.textContent = "Aucune app à ajouter.";
|
||||||
|
column.appendChild(msg);
|
||||||
|
return column;
|
||||||
|
}
|
||||||
|
|
||||||
|
const list = document.createElement("div");
|
||||||
|
list.className = "catalog-list";
|
||||||
|
apps.forEach((app) => list.appendChild(renderCatalogTile(app)));
|
||||||
|
column.appendChild(list);
|
||||||
|
return column;
|
||||||
|
}
|
||||||
|
|
||||||
function openAppForm(onSubmit) {
|
function openAppForm(onSubmit) {
|
||||||
const backdrop = document.createElement("div");
|
const backdrop = document.createElement("div");
|
||||||
backdrop.className = "app-form-backdrop";
|
backdrop.className = "app-form-backdrop";
|
||||||
@@ -326,6 +421,21 @@ function render() {
|
|||||||
const root = document.getElementById("editor-root");
|
const root = document.getElementById("editor-root");
|
||||||
root.innerHTML = "";
|
root.innerHTML = "";
|
||||||
|
|
||||||
|
const layout = document.createElement("div");
|
||||||
|
layout.className = "editor-layout";
|
||||||
|
|
||||||
|
const half = Math.ceil(catalog.length / 2);
|
||||||
|
const leftCatalog = renderCatalogColumn("Catalogue YunoHost", catalog.slice(0, half));
|
||||||
|
const rightCatalog = renderCatalogColumn("Catalogue YunoHost", catalog.slice(half));
|
||||||
|
|
||||||
|
const center = document.createElement("div");
|
||||||
|
center.className = "editor-center";
|
||||||
|
|
||||||
|
layout.appendChild(leftCatalog);
|
||||||
|
layout.appendChild(center);
|
||||||
|
layout.appendChild(rightCatalog);
|
||||||
|
root.appendChild(layout);
|
||||||
|
|
||||||
const pinnedSection = document.createElement("section");
|
const pinnedSection = document.createElement("section");
|
||||||
pinnedSection.className = "pinned-section";
|
pinnedSection.className = "pinned-section";
|
||||||
const pinnedHeading = document.createElement("h2");
|
const pinnedHeading = document.createElement("h2");
|
||||||
@@ -345,13 +455,13 @@ function render() {
|
|||||||
}));
|
}));
|
||||||
attachGridDropzone(pinnedGrid, () => (state.pinned = state.pinned || []));
|
attachGridDropzone(pinnedGrid, () => (state.pinned = state.pinned || []));
|
||||||
pinnedSection.appendChild(pinnedGrid);
|
pinnedSection.appendChild(pinnedGrid);
|
||||||
root.appendChild(pinnedSection);
|
center.appendChild(pinnedSection);
|
||||||
|
|
||||||
const groupsList = document.createElement("div");
|
const groupsList = document.createElement("div");
|
||||||
groupsList.id = "groups-list";
|
groupsList.id = "groups-list";
|
||||||
state.groups.forEach((group, i) => groupsList.appendChild(renderGroup(group, i)));
|
state.groups.forEach((group, i) => groupsList.appendChild(renderGroup(group, i)));
|
||||||
attachGroupsDropzone(groupsList);
|
attachGroupsDropzone(groupsList);
|
||||||
root.appendChild(groupsList);
|
center.appendChild(groupsList);
|
||||||
|
|
||||||
const addGroupBtn = document.createElement("button");
|
const addGroupBtn = document.createElement("button");
|
||||||
addGroupBtn.className = "add-group-btn";
|
addGroupBtn.className = "add-group-btn";
|
||||||
@@ -362,7 +472,7 @@ function render() {
|
|||||||
markDirty();
|
markDirty();
|
||||||
render();
|
render();
|
||||||
});
|
});
|
||||||
root.appendChild(addGroupBtn);
|
center.appendChild(addGroupBtn);
|
||||||
|
|
||||||
const ungroupedSection = document.createElement("section");
|
const ungroupedSection = document.createElement("section");
|
||||||
ungroupedSection.className = "ungrouped-section";
|
ungroupedSection.className = "ungrouped-section";
|
||||||
@@ -383,12 +493,31 @@ function render() {
|
|||||||
attachGridDropzone(ungroupedGrid, () => state.ungrouped);
|
attachGridDropzone(ungroupedGrid, () => state.ungrouped);
|
||||||
ungroupedSection.appendChild(ungroupedGrid);
|
ungroupedSection.appendChild(ungroupedGrid);
|
||||||
|
|
||||||
root.appendChild(ungroupedSection);
|
center.appendChild(ungroupedSection);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadCatalog() {
|
||||||
|
try {
|
||||||
|
const res = await fetch("/__catalog", { cache: "no-store" });
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.error) {
|
||||||
|
catalog = [];
|
||||||
|
catalogError = data.error;
|
||||||
|
} else {
|
||||||
|
catalog = data.apps || [];
|
||||||
|
catalogError = null;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
catalog = [];
|
||||||
|
catalogError = "Catalogue indisponible (aperçu local requis).";
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
const res = await fetch("config.json", { cache: "no-store" });
|
const res = await fetch("config.json", { cache: "no-store" });
|
||||||
state = await res.json();
|
state = await res.json();
|
||||||
|
await loadCatalog();
|
||||||
render();
|
render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user