- Replace gsub(/[\[\]]/) with two sub() calls for section parsing - Use explicit pattern matching for each NFO field - Single-pass awk extraction for all 7 fields (category, desc, keywords, caps, audience, icon, version) - Remove NFO parser library dependency (now uses direct awk) - Simplify capability tracking with tr instead of for loop Tested: 110 NFO entries now correctly extracted from 239 total items Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
856 lines
39 KiB
Bash
Executable File
856 lines
39 KiB
Bash
Executable File
#!/bin/sh
|
|
# SecuBox Hub Generator v7 - Full NFO Integration + Capability Filters
|
|
# Enhanced with MetaBlog NFO, version display, capability filtering, audience tabs
|
|
|
|
OUTPUT="/www/gk2-hub/index.html"
|
|
TEMP="/tmp/hub_gen_$$.html"
|
|
DATE=$(date "+%Y-%m-%d %H:%M")
|
|
PEERTUBE_API="http://192.168.255.1:9001/api/v1"
|
|
PEERTUBE_URL="https://tube.gk2.secubox.in"
|
|
STREAMLIT_APPS_DIR="/srv/streamlit/apps"
|
|
METABLOG_SITES_DIR="/srv/metablogizer/sites"
|
|
|
|
# Fast NFO field extraction using awk (no eval, no subshell parsing)
|
|
# Usage: get_nfo_info <app_dir> <section> <field> <default>
|
|
get_nfo_info() {
|
|
local app_dir="$1"
|
|
local section="$2"
|
|
local field="$3"
|
|
local default="$4"
|
|
|
|
local nfo_file="$app_dir/README.nfo"
|
|
[ ! -f "$nfo_file" ] && { echo "$default"; return; }
|
|
|
|
# Direct awk extraction - fast and reliable
|
|
local val=$(awk -v section="$section" -v field="$field" '
|
|
BEGIN { in_section = 0 }
|
|
/^\[/ {
|
|
if ($0 ~ "\\[" section "\\]") { in_section = 1 }
|
|
else { in_section = 0 }
|
|
}
|
|
in_section && /^[a-zA-Z_-]+=/ {
|
|
# Extract key and value
|
|
key = $0
|
|
sub(/=.*/, "", key)
|
|
gsub(/^[ \t]+|[ \t]+$/, "", key)
|
|
gsub(/-/, "_", key)
|
|
if (key == field) {
|
|
val = $0
|
|
sub(/^[^=]+=[ \t]*/, "", val)
|
|
gsub(/^["'"'"']|["'"'"']$/, "", val)
|
|
print val
|
|
exit
|
|
}
|
|
}
|
|
' "$nfo_file" 2>/dev/null)
|
|
|
|
[ -n "$val" ] && echo "$val" || echo "$default"
|
|
}
|
|
|
|
# Get all NFO metadata for an app in single awk pass
|
|
# Returns: category|description|keywords|capabilities|audience|icon|version
|
|
get_nfo_full() {
|
|
local app_dir="$1"
|
|
local nfo_file="$app_dir/README.nfo"
|
|
|
|
[ ! -f "$nfo_file" ] && return 1
|
|
|
|
# Single-pass extraction of all needed fields
|
|
awk '
|
|
BEGIN { section = "" }
|
|
/^\[/ {
|
|
section = $0
|
|
sub(/^\[/, "", section)
|
|
sub(/\]$/, "", section)
|
|
}
|
|
section == "tags" && /^category=/ { val=$0; sub(/^category=/, "", val); category=val }
|
|
section == "tags" && /^keywords=/ { val=$0; sub(/^keywords=/, "", val); keywords=val }
|
|
section == "tags" && /^audience=/ { val=$0; sub(/^audience=/, "", val); audience=val }
|
|
section == "description" && /^short=/ { val=$0; sub(/^short=/, "", val); desc=val }
|
|
section == "dynamics" && /^capabilities=/ { val=$0; sub(/^capabilities=/, "", val); caps=val }
|
|
section == "media" && /^icon=/ { val=$0; sub(/^icon=/, "", val); icon=val }
|
|
section == "identity" && /^version=/ { val=$0; sub(/^version=/, "", val); version=val }
|
|
END {
|
|
printf "%s|%s|%s|%s|%s|%s|%s\n", category, desc, keywords, caps, audience, icon, version
|
|
}
|
|
' "$nfo_file" 2>/dev/null
|
|
}
|
|
|
|
categorize_site() {
|
|
local name=$(echo "$1" | tr '[:upper:]' '[:lower:]')
|
|
case "$name" in
|
|
*intel*|*dgse*|*osint*|*threat*|*secu*|*raid*|*confid*|*mku*|*bdgse*|*camus*) echo "Intelligence" ;;
|
|
*game*|*play*|*comic*|*virus*|*survie*) echo "Divertissement" ;;
|
|
*dev*|*code*|*git*|*sdlc*|*crt*|*fabric*|*hermes*) echo "Développement" ;;
|
|
*doc*|*manual*|*guide*|*how*|*fm*|*bgp*|*lrh*|*bcf*) echo "Documentation" ;;
|
|
*media*|*video*|*tube*|*stream*|*radio*|*lyrion*|*jellyfin*) echo "Média" ;;
|
|
*blog*|*news*|*press*|*zine*|*flash*|*pub*) echo "Actualités" ;;
|
|
*cloud*|*file*|*nextcloud*|*photo*) echo "Cloud" ;;
|
|
*admin*|*control*|*status*|*hub*|*glances*|*holo*|*console*|*evolution*) echo "Administration" ;;
|
|
*money*|*coin*|*crypto*|*cgv*|*cpi*|*apr*) echo "Finance" ;;
|
|
*geo*|*map*|*gondwana*|*earth*) echo "Géographie" ;;
|
|
*psy*|*oracle*|*yijing*|*bazi*|*equa*|*lunaquar*|*clock*|*wuyun*|*yling*|*pix*|*tam*) echo "Ésotérique" ;;
|
|
*metabol*|*osint*|*generix*|*swg*|*ftvm*|*cpf*) echo "Outils" ;;
|
|
*) echo "Projets" ;;
|
|
esac
|
|
}
|
|
|
|
get_emoji() {
|
|
case "$1" in
|
|
"Intelligence") echo "🔍" ;;
|
|
"Divertissement") echo "🎮" ;;
|
|
"Développement") echo "💻" ;;
|
|
"Documentation") echo "📚" ;;
|
|
"Média") echo "🎬" ;;
|
|
"Actualités") echo "📰" ;;
|
|
"Cloud") echo "☁️" ;;
|
|
"Administration") echo "⚙️" ;;
|
|
"Finance") echo "💰" ;;
|
|
"Géographie") echo "🌍" ;;
|
|
"Ésotérique") echo "🔮" ;;
|
|
"Outils") echo "🛠️" ;;
|
|
"Streamlit") echo "📊" ;;
|
|
"PeerTube") echo "🎥" ;;
|
|
"Communication") echo "💬" ;;
|
|
"Social") echo "👥" ;;
|
|
"Security") echo "🛡️" ;;
|
|
"service") echo "🔌" ;;
|
|
*) echo "📄" ;;
|
|
esac
|
|
}
|
|
|
|
format_duration() {
|
|
local sec=$1
|
|
local min=$((sec / 60))
|
|
local s=$((sec % 60))
|
|
if [ $min -ge 60 ]; then
|
|
local h=$((min / 60))
|
|
min=$((min % 60))
|
|
printf "%d:%02d:%02d" $h $min $s
|
|
else
|
|
printf "%d:%02d" $min $s
|
|
fi
|
|
}
|
|
|
|
# HTML Header
|
|
cat > "$TEMP" << 'HTMLHEAD'
|
|
<!DOCTYPE html>
|
|
<html lang="fr">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>GK² Hub — Portal SecuBox</title>
|
|
<meta http-equiv="refresh" content="300">
|
|
<style>
|
|
:root{--bg:#0a0a0f;--surface:#12121a;--surface2:#1a1a24;--border:#2a2a3e;--accent:#00d4ff;--accent2:#7c3aed;--accent3:#10b981;--accent4:#f97316;--text:#e0e0e0;--muted:#888}
|
|
*{box-sizing:border-box;margin:0;padding:0}
|
|
body{background:var(--bg);color:var(--text);font-family:'Segoe UI',system-ui,sans-serif;min-height:100vh}
|
|
.bg-grid{position:fixed;inset:0;background-image:linear-gradient(rgba(0,212,255,0.03) 1px,transparent 1px),linear-gradient(90deg,rgba(0,212,255,0.03) 1px,transparent 1px);background-size:40px 40px;pointer-events:none;z-index:0}
|
|
.container{max-width:1600px;margin:0 auto;padding:20px;position:relative;z-index:1}
|
|
.header{display:flex;justify-content:space-between;align-items:center;padding:20px 0;border-bottom:1px solid var(--border);margin-bottom:20px;flex-wrap:wrap;gap:15px}
|
|
.logo{display:flex;align-items:center;gap:12px}
|
|
.logo-icon{width:48px;height:48px;background:linear-gradient(135deg,var(--accent),var(--accent2));border-radius:12px;display:flex;align-items:center;justify-content:center;font-weight:900;font-size:1.1rem;color:#fff}
|
|
.logo-text{font-size:1.4rem;font-weight:700}
|
|
.logo-text span{color:var(--accent)}
|
|
.meta-info{font-size:0.8rem;color:var(--muted)}
|
|
.controls{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
|
|
.view-btn{padding:8px 16px;background:var(--surface);border:1px solid var(--border);border-radius:8px;color:var(--text);cursor:pointer;transition:all 0.2s;font-size:0.85rem}
|
|
.view-btn:hover,.view-btn.active{background:var(--accent);color:#000;border-color:var(--accent)}
|
|
.search-box{padding:8px 16px;background:var(--surface);border:1px solid var(--border);border-radius:8px;color:var(--text);width:200px}
|
|
.search-box:focus{outline:none;border-color:var(--accent)}
|
|
.tag-cloud{display:none;flex-wrap:wrap;gap:8px;padding:20px;background:var(--surface);border-radius:12px;margin-bottom:20px;justify-content:center}
|
|
.tag-cloud.visible{display:flex}
|
|
.tag{padding:6px 14px;background:var(--surface2);border:1px solid var(--border);border-radius:20px;font-size:0.8rem;cursor:pointer;transition:all 0.2s}
|
|
.tag:hover,.tag.active{background:var(--accent);color:#000;border-color:var(--accent)}
|
|
.tag .count{font-size:0.7rem;opacity:0.7;margin-left:4px}
|
|
.stats-bar{display:flex;gap:20px;padding:15px 20px;background:var(--surface);border-radius:12px;margin-bottom:20px;flex-wrap:wrap}
|
|
.stat{display:flex;align-items:center;gap:8px}
|
|
.stat-value{color:var(--accent);font-weight:700;font-size:1.2rem}
|
|
.stat-label{color:var(--muted);font-size:0.8rem}
|
|
.category-tabs{display:flex;gap:5px;margin-bottom:20px;overflow-x:auto;padding-bottom:10px}
|
|
.cat-tab{padding:10px 20px;background:var(--surface);border:1px solid var(--border);border-radius:8px;color:var(--text);cursor:pointer;white-space:nowrap;transition:all 0.2s;font-size:0.85rem}
|
|
.cat-tab:hover,.cat-tab.active{background:linear-gradient(135deg,var(--accent),var(--accent2));color:#fff;border-color:transparent}
|
|
.cat-tab .count{background:rgba(255,255,255,0.2);padding:2px 8px;border-radius:10px;font-size:0.75rem;margin-left:8px}
|
|
.sites-grid{display:grid;gap:16px;grid-template-columns:repeat(auto-fill,minmax(260px,1fr))}
|
|
.view-list .sites-grid{grid-template-columns:1fr}
|
|
.view-compact .sites-grid{grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:8px}
|
|
.site-card{background:var(--surface);border:1px solid var(--border);border-radius:12px;overflow:hidden;text-decoration:none;color:inherit;display:flex;flex-direction:column;transition:all 0.3s}
|
|
.site-card:hover{border-color:var(--accent);transform:translateY(-3px);box-shadow:0 10px 40px rgba(0,212,255,0.15)}
|
|
.site-card.streamlit{border-color:var(--accent3)}
|
|
.site-card.streamlit:hover{border-color:var(--accent3);box-shadow:0 10px 40px rgba(16,185,129,0.15)}
|
|
.site-card.video{border-color:var(--accent4)}
|
|
.site-card.video:hover{border-color:var(--accent4);box-shadow:0 10px 40px rgba(249,115,22,0.15)}
|
|
.site-card.service{border-color:#8b5cf6}
|
|
.site-card.service:hover{border-color:#8b5cf6;box-shadow:0 10px 40px rgba(139,92,246,0.15)}
|
|
.view-list .site-card{flex-direction:row}
|
|
.card-preview{width:100%;height:120px;background:var(--surface2);position:relative;overflow:hidden}
|
|
.view-list .card-preview{width:160px;flex-shrink:0}
|
|
.view-compact .card-preview{height:70px}
|
|
.card-preview iframe{width:200%;height:200%;transform:scale(0.5);transform-origin:0 0;border:none;pointer-events:none}
|
|
.card-preview img{width:100%;height:100%;object-fit:cover}
|
|
.card-preview .duration{position:absolute;bottom:4px;right:4px;background:rgba(0,0,0,0.8);color:#fff;padding:2px 6px;border-radius:4px;font-size:0.7rem;font-family:monospace}
|
|
.card-preview .play-icon{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:48px;height:48px;background:rgba(0,0,0,0.7);border-radius:50%;display:flex;align-items:center;justify-content:center;opacity:0;transition:opacity 0.2s}
|
|
.site-card.video:hover .play-icon{opacity:1}
|
|
.play-icon:after{content:'';border-style:solid;border-width:10px 0 10px 16px;border-color:transparent transparent transparent #fff;margin-left:4px}
|
|
.card-content{padding:12px;flex:1}
|
|
.view-compact .card-content{padding:8px}
|
|
.card-title{font-weight:600;font-size:0.9rem;margin-bottom:3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
.card-domain{font-size:0.7rem;color:var(--accent);font-family:monospace;margin-bottom:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
.card-cat{display:inline-block;padding:2px 6px;border-radius:4px;font-size:0.6rem;color:#fff;font-weight:600;margin-right:4px}
|
|
.card-cat.meta{background:linear-gradient(135deg,var(--accent),var(--accent2))}
|
|
.card-cat.streamlit{background:linear-gradient(135deg,var(--accent3),#059669)}
|
|
.card-cat.video{background:linear-gradient(135deg,var(--accent4),#ea580c)}
|
|
.card-cat.service{background:linear-gradient(135deg,#8b5cf6,#6d28d9)}
|
|
.card-desc{font-size:0.75rem;color:var(--muted);margin:4px 0;line-height:1.3;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}
|
|
.card-caps{display:flex;flex-wrap:wrap;gap:3px;margin-top:6px}
|
|
.cap-badge{font-size:0.6rem;padding:2px 6px;background:rgba(124,58,237,0.2);color:var(--accent2);border-radius:10px;border:1px solid rgba(124,58,237,0.3)}
|
|
.card-keywords{font-size:0.65rem;color:var(--muted);margin-top:4px;font-style:italic}
|
|
.card-header{display:flex;align-items:center;justify-content:space-between;gap:8px}
|
|
.card-version{font-size:0.65rem;padding:2px 6px;background:rgba(0,212,255,0.15);color:var(--accent);border-radius:4px;font-family:monospace;white-space:nowrap}
|
|
.cap-cloud{display:flex;flex-wrap:wrap;gap:6px;padding:12px 20px;background:var(--surface);border-radius:8px;margin-bottom:12px;align-items:center}
|
|
.cap-title,.audience-title{color:var(--muted);font-size:0.8rem;margin-right:8px}
|
|
.cap-filter{padding:4px 10px;background:rgba(124,58,237,0.15);border:1px solid rgba(124,58,237,0.3);color:var(--accent2);border-radius:15px;font-size:0.75rem;cursor:pointer;transition:all 0.2s}
|
|
.cap-filter:hover,.cap-filter.active{background:var(--accent2);color:#fff;border-color:var(--accent2)}
|
|
.cap-filter .count{font-size:0.65rem;opacity:0.7;margin-left:3px}
|
|
.audience-bar{display:flex;flex-wrap:wrap;gap:8px;padding:10px 20px;background:var(--surface);border-radius:8px;margin-bottom:12px;align-items:center}
|
|
.audience-btn{padding:5px 12px;background:var(--surface2);border:1px solid var(--border);border-radius:6px;font-size:0.8rem;cursor:pointer;transition:all 0.2s}
|
|
.audience-btn:hover,.audience-btn.active{background:var(--accent3);color:#fff;border-color:var(--accent3)}
|
|
.audience-btn .count{font-size:0.7rem;opacity:0.7;margin-left:4px}
|
|
.lock-badge{position:absolute;top:8px;right:8px;background:rgba(0,0,0,0.7);padding:4px 8px;border-radius:4px;font-size:0.8rem}
|
|
.site-card[data-protected]{display:none}
|
|
.site-card[data-protected].unlocked{display:flex}
|
|
.login-banner{background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:12px 20px;margin-bottom:20px;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:10px}
|
|
.login-banner.hidden{display:none}
|
|
.login-banner a{color:var(--accent);text-decoration:none;padding:6px 16px;border:1px solid var(--accent);border-radius:6px}
|
|
.login-banner a:hover{background:var(--accent);color:#000}
|
|
.footer{margin-top:40px;padding:20px 0;border-top:1px solid var(--border);text-align:center;color:var(--muted);font-size:0.8rem}
|
|
.footer a{color:var(--accent);text-decoration:none}
|
|
.preview-btn{position:absolute;top:8px;left:8px;width:32px;height:32px;background:rgba(0,0,0,0.8);border:1px solid var(--accent);border-radius:6px;color:var(--accent);cursor:pointer;font-size:1rem;opacity:0;transition:all 0.2s;display:flex;align-items:center;justify-content:center}
|
|
.site-card:hover .preview-btn{opacity:1}
|
|
.preview-btn:hover{background:var(--accent);color:#000}
|
|
.preview-modal{position:fixed;inset:0;background:rgba(0,0,0,0.9);z-index:1000;display:none;align-items:center;justify-content:center;padding:20px}
|
|
.preview-modal.active{display:flex}
|
|
.preview-content{width:95%;max-width:1400px;height:90vh;background:var(--surface);border-radius:12px;overflow:hidden;display:flex;flex-direction:column;border:1px solid var(--border)}
|
|
.preview-close{position:absolute;top:15px;right:20px;width:36px;height:36px;background:var(--surface2);border:1px solid var(--border);border-radius:8px;color:var(--text);cursor:pointer;font-size:1.5rem;z-index:10}
|
|
.preview-close:hover{background:var(--accent);color:#000;border-color:var(--accent)}
|
|
#previewFrame{flex:1;border:none;width:100%;background:#fff}
|
|
.preview-info{padding:12px 20px;background:var(--surface2);display:flex;justify-content:space-between;align-items:center;border-top:1px solid var(--border)}
|
|
#previewTitle{font-weight:600;font-size:1rem}
|
|
#previewLink{color:var(--accent);text-decoration:none;padding:6px 12px;border:1px solid var(--accent);border-radius:6px;font-size:0.85rem}
|
|
#previewLink:hover{background:var(--accent);color:#000}
|
|
@media(max-width:768px){.header{flex-direction:column;align-items:flex-start}.view-list .site-card{flex-direction:column}.view-list .card-preview{width:100%}.preview-content{width:100%;height:100vh;border-radius:0}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="bg-grid"></div>
|
|
<div class="container">
|
|
HTMLHEAD
|
|
|
|
# Header
|
|
cat >> "$TEMP" << EOF
|
|
<div class="header">
|
|
<div class="logo">
|
|
<div class="logo-icon">GK²</div>
|
|
<div>
|
|
<div class="logo-text">Secu<span>Box</span> Hub</div>
|
|
<div class="meta-info">$DATE — Auto-refresh 5min</div>
|
|
</div>
|
|
</div>
|
|
<div class="controls">
|
|
<input type="text" class="search-box" placeholder="🔍 Rechercher..." id="searchBox">
|
|
<button class="view-btn active" data-view="grid">▦ Grille</button>
|
|
<button class="view-btn" data-view="list">☰ Liste</button>
|
|
<button class="view-btn" data-view="compact">▪ Compact</button>
|
|
<button class="view-btn" id="toggleCloud">☁ Tags</button>
|
|
</div>
|
|
</div>
|
|
<div class="login-banner" id="loginBanner">
|
|
<span>🔒 Certains contenus nécessitent une authentification</span>
|
|
<a href="/login.html">Se connecter</a>
|
|
</div>
|
|
EOF
|
|
|
|
# Collect all sites
|
|
SITES_FILE="/tmp/hub_sites_$$.txt"
|
|
CAT_FILE="/tmp/hub_cats_$$.txt"
|
|
VIDEOS_FILE="/tmp/hub_videos_$$.txt"
|
|
> "$SITES_FILE"
|
|
> "$CAT_FILE"
|
|
> "$VIDEOS_FILE"
|
|
|
|
# MetaBlogizer sites - now with full NFO support
|
|
uci show metablogizer 2>/dev/null | grep "=site$" | sed "s/metablogizer\.\(.*\)=site/\1/" | while read site; do
|
|
name=$(uci -q get "metablogizer.$site.name")
|
|
domain=$(uci -q get "metablogizer.$site.domain")
|
|
enabled=$(uci -q get "metablogizer.$site.enabled")
|
|
auth_required=$(uci -q get "metablogizer.$site.auth_required")
|
|
[ "$enabled" != "1" ] && continue
|
|
[ -z "$domain" ] && continue
|
|
|
|
# Get full NFO metadata for MetaBlog sites
|
|
site_dir="$METABLOG_SITES_DIR/$name"
|
|
nfo_data=$(get_nfo_full "$site_dir" 2>/dev/null)
|
|
|
|
if [ -n "$nfo_data" ]; then
|
|
nfo_cat=$(echo "$nfo_data" | cut -d'|' -f1)
|
|
nfo_desc=$(echo "$nfo_data" | cut -d'|' -f2)
|
|
nfo_keywords=$(echo "$nfo_data" | cut -d'|' -f3)
|
|
nfo_caps=$(echo "$nfo_data" | cut -d'|' -f4)
|
|
nfo_audience=$(echo "$nfo_data" | cut -d'|' -f5)
|
|
nfo_version=$(echo "$nfo_data" | cut -d'|' -f7)
|
|
else
|
|
nfo_cat=""
|
|
nfo_desc=""
|
|
nfo_keywords=""
|
|
nfo_caps=""
|
|
nfo_audience=""
|
|
nfo_version=""
|
|
fi
|
|
|
|
# Use NFO category or fallback to name-based categorization
|
|
[ -n "$nfo_cat" ] && cat="$nfo_cat" || cat=$(categorize_site "$name")
|
|
emoji=$(get_emoji "$cat")
|
|
echo "$cat" >> "$CAT_FILE"
|
|
|
|
# Track audiences and capabilities for filters
|
|
[ -n "$nfo_audience" ] && echo "$nfo_audience" >> "/tmp/hub_audiences_$$.txt"
|
|
[ -n "$nfo_caps" ] && echo "$nfo_caps" | tr ',' '\n' >> "/tmp/hub_caps_$$.txt"
|
|
|
|
protected="-"
|
|
[ "$auth_required" = "1" ] && protected="protected"
|
|
|
|
# Format: domain name cat emoji type thumb protected desc keywords caps version audience
|
|
printf '%s\t%s\t%s\t%s\tmeta\t-\t%s\t%s\t%s\t%s\t%s\t%s\n' \
|
|
"$domain" "$name" "$cat" "$emoji" "$protected" \
|
|
"${nfo_desc:--}" "${nfo_keywords:--}" "${nfo_caps:--}" "${nfo_version:--}" "${nfo_audience:--}" >> "$SITES_FILE"
|
|
done
|
|
|
|
# Streamlit instances
|
|
uci show streamlit 2>/dev/null | grep "=instance$" | sed "s/streamlit\.\(.*\)=instance/\1/" | while read app; do
|
|
name=$(uci -q get "streamlit.$app.name")
|
|
domain=$(uci -q get "streamlit.$app.domain")
|
|
enabled=$(uci -q get "streamlit.$app.enabled")
|
|
[ "$enabled" != "1" ] && continue
|
|
[ -z "$domain" ] && continue
|
|
|
|
# Get full NFO metadata
|
|
app_dir="$STREAMLIT_APPS_DIR/$name"
|
|
nfo_data=$(get_nfo_full "$app_dir" 2>/dev/null)
|
|
|
|
if [ -n "$nfo_data" ]; then
|
|
nfo_cat=$(echo "$nfo_data" | cut -d'|' -f1)
|
|
nfo_desc=$(echo "$nfo_data" | cut -d'|' -f2)
|
|
nfo_keywords=$(echo "$nfo_data" | cut -d'|' -f3)
|
|
nfo_caps=$(echo "$nfo_data" | cut -d'|' -f4)
|
|
nfo_audience=$(echo "$nfo_data" | cut -d'|' -f5)
|
|
nfo_version=$(echo "$nfo_data" | cut -d'|' -f7)
|
|
else
|
|
nfo_cat=""
|
|
nfo_desc=""
|
|
nfo_keywords=""
|
|
nfo_caps=""
|
|
nfo_audience=""
|
|
nfo_version=""
|
|
fi
|
|
|
|
[ -n "$nfo_cat" ] && cat="$nfo_cat" || cat=$(categorize_site "$name")
|
|
emoji=$(get_emoji "$cat")
|
|
echo "$cat" >> "$CAT_FILE"
|
|
|
|
# Track audiences and capabilities for filters
|
|
[ -n "$nfo_audience" ] && echo "$nfo_audience" >> "/tmp/hub_audiences_$$.txt"
|
|
[ -n "$nfo_caps" ] && echo "$nfo_caps" | tr ',' '\n' >> "/tmp/hub_caps_$$.txt"
|
|
|
|
# Format: domain name cat emoji type thumb protected desc keywords caps version audience
|
|
printf '%s\t%s\t%s\t%s\tstreamlit\t-\t-\t%s\t%s\t%s\t%s\t%s\n' \
|
|
"$domain" "$name" "$cat" "$emoji" \
|
|
"${nfo_desc:--}" "${nfo_keywords:--}" "${nfo_caps:--}" "${nfo_version:--}" "${nfo_audience:--}" >> "$SITES_FILE"
|
|
done
|
|
|
|
# Also check streamlit-forge config
|
|
uci show streamlit-forge 2>/dev/null | grep "=app$" | sed "s/streamlit-forge\.\(.*\)=app/\1/" | while read app; do
|
|
name=$(uci -q get "streamlit-forge.$app.name")
|
|
domain=$(uci -q get "streamlit-forge.$app.domain")
|
|
enabled=$(uci -q get "streamlit-forge.$app.enabled")
|
|
[ "$enabled" != "1" ] && continue
|
|
[ -z "$domain" ] && continue
|
|
|
|
# Check if already added from streamlit config
|
|
grep -q " $name " "$SITES_FILE" 2>/dev/null && continue
|
|
|
|
# Get full NFO metadata
|
|
app_dir="$STREAMLIT_APPS_DIR/$name"
|
|
nfo_data=$(get_nfo_full "$app_dir" 2>/dev/null)
|
|
|
|
if [ -n "$nfo_data" ]; then
|
|
nfo_cat=$(echo "$nfo_data" | cut -d'|' -f1)
|
|
nfo_desc=$(echo "$nfo_data" | cut -d'|' -f2)
|
|
nfo_keywords=$(echo "$nfo_data" | cut -d'|' -f3)
|
|
nfo_caps=$(echo "$nfo_data" | cut -d'|' -f4)
|
|
nfo_audience=$(echo "$nfo_data" | cut -d'|' -f5)
|
|
nfo_version=$(echo "$nfo_data" | cut -d'|' -f7)
|
|
else
|
|
nfo_cat=""
|
|
nfo_desc=""
|
|
nfo_keywords=""
|
|
nfo_caps=""
|
|
nfo_audience=""
|
|
nfo_version=""
|
|
fi
|
|
|
|
[ -n "$nfo_cat" ] && cat="$nfo_cat" || cat=$(categorize_site "$name")
|
|
emoji=$(get_emoji "$cat")
|
|
echo "$cat" >> "$CAT_FILE"
|
|
|
|
# Track audiences and capabilities
|
|
[ -n "$nfo_audience" ] && echo "$nfo_audience" >> "/tmp/hub_audiences_$$.txt"
|
|
[ -n "$nfo_caps" ] && echo "$nfo_caps" | tr ',' '\n' >> "/tmp/hub_caps_$$.txt"
|
|
|
|
printf '%s\t%s\t%s\t%s\tstreamlit\t-\t-\t%s\t%s\t%s\t%s\t%s\n' \
|
|
"$domain" "$name" "$cat" "$emoji" \
|
|
"${nfo_desc:--}" "${nfo_keywords:--}" "${nfo_caps:--}" "${nfo_version:--}" "${nfo_audience:--}" >> "$SITES_FILE"
|
|
done
|
|
|
|
# HAProxy vhosts - scan ALL exposed services
|
|
uci show haproxy 2>/dev/null | grep "=vhost$" | sed "s/haproxy\.\(.*\)=vhost/\1/" | while read vhost; do
|
|
domain=$(uci -q get "haproxy.$vhost.domain")
|
|
enabled=$(uci -q get "haproxy.$vhost.enabled")
|
|
[ "$enabled" = "0" ] && continue
|
|
[ -z "$domain" ] && continue
|
|
|
|
# Skip if already added from metablogizer/streamlit configs
|
|
grep -q "^$domain " "$SITES_FILE" 2>/dev/null && continue
|
|
|
|
# Get backend to determine service type
|
|
backend=$(uci -q get "haproxy.$vhost.backend")
|
|
|
|
# Determine type and name from domain/backend
|
|
name=$(echo "$domain" | cut -d'.' -f1)
|
|
|
|
# Categorize based on backend or domain patterns
|
|
case "$backend" in
|
|
*streamlit*) type="streamlit"; cat=$(categorize_site "$name") ;;
|
|
*metablog*|*uhttpd*) type="meta"; cat=$(categorize_site "$name") ;;
|
|
*jellyfin*) type="service"; cat="Média" ;;
|
|
*peertube*|*tube*) type="service"; cat="Média" ;;
|
|
*nextcloud*|*cloud*) type="service"; cat="Cloud" ;;
|
|
*gitea*|*git*) type="service"; cat="Développement" ;;
|
|
*lyrion*|*music*) type="service"; cat="Média" ;;
|
|
*glances*) type="service"; cat="Administration" ;;
|
|
*jitsi*|*meet*) type="service"; cat="Communication" ;;
|
|
*photoprism*|*photo*) type="service"; cat="Cloud" ;;
|
|
*social*|*gotosocial*) type="service"; cat="Social" ;;
|
|
*admin*|*luci*) type="service"; cat="Administration" ;;
|
|
*) type="service"; cat=$(categorize_site "$name") ;;
|
|
esac
|
|
|
|
emoji=$(get_emoji "$cat")
|
|
echo "$cat" >> "$CAT_FILE"
|
|
|
|
# Format: domain name cat emoji type thumb protected desc keywords caps version audience
|
|
printf '%s\t%s\t%s\t%s\t%s\t-\t-\t-\t-\t-\t-\t-\n' \
|
|
"$domain" "$name" "$cat" "$emoji" "$type" >> "$SITES_FILE"
|
|
done
|
|
|
|
# PeerTube videos
|
|
VIDEOS_JSON=$(curl -s "${PEERTUBE_API}/videos?count=50" 2>/dev/null)
|
|
TOTAL_VIDEOS=0
|
|
if [ -n "$VIDEOS_JSON" ]; then
|
|
# Parse videos using jsonfilter
|
|
echo "$VIDEOS_JSON" | jsonfilter -e '@.data[*]' 2>/dev/null | while read -r video_line; do
|
|
# Skip if empty
|
|
[ -z "$video_line" ] && continue
|
|
|
|
uuid=$(echo "$VIDEOS_JSON" | jsonfilter -e "@.data[$TOTAL_VIDEOS].uuid" 2>/dev/null)
|
|
name=$(echo "$VIDEOS_JSON" | jsonfilter -e "@.data[$TOTAL_VIDEOS].name" 2>/dev/null)
|
|
thumb=$(echo "$VIDEOS_JSON" | jsonfilter -e "@.data[$TOTAL_VIDEOS].thumbnailPath" 2>/dev/null)
|
|
duration=$(echo "$VIDEOS_JSON" | jsonfilter -e "@.data[$TOTAL_VIDEOS].duration" 2>/dev/null)
|
|
|
|
[ -z "$uuid" ] && break
|
|
[ -z "$name" ] && continue
|
|
|
|
# Format duration
|
|
dur_fmt=$(format_duration "$duration")
|
|
|
|
# Video URL and thumbnail
|
|
video_url="${PEERTUBE_URL}/w/${uuid}"
|
|
thumb_url="${PEERTUBE_URL}${thumb}"
|
|
|
|
echo "Média" >> "$CAT_FILE"
|
|
printf '%s\t%s\tMédia\t🎥\tvideo\t%s\t%s\n' "$video_url" "$name" "$thumb_url" "$dur_fmt" >> "$VIDEOS_FILE"
|
|
|
|
TOTAL_VIDEOS=$((TOTAL_VIDEOS + 1))
|
|
done
|
|
|
|
# Alternative parsing if jsonfilter fails
|
|
if [ ! -s "$VIDEOS_FILE" ]; then
|
|
# Use simple sed/awk parsing
|
|
echo "$VIDEOS_JSON" | sed 's/},/}\n/g' | while read -r line; do
|
|
uuid=$(echo "$line" | sed -n 's/.*"uuid":"\([^"]*\)".*/\1/p')
|
|
name=$(echo "$line" | sed -n 's/.*"name":"\([^"]*\)".*/\1/p' | head -1)
|
|
thumb=$(echo "$line" | sed -n 's/.*"thumbnailPath":"\([^"]*\)".*/\1/p')
|
|
duration=$(echo "$line" | sed -n 's/.*"duration":\([0-9]*\).*/\1/p')
|
|
|
|
[ -z "$uuid" ] && continue
|
|
[ -z "$name" ] && continue
|
|
|
|
dur_fmt=$(format_duration "${duration:-0}")
|
|
video_url="${PEERTUBE_URL}/w/${uuid}"
|
|
thumb_url="${PEERTUBE_URL}${thumb}"
|
|
|
|
echo "Média" >> "$CAT_FILE"
|
|
printf '%s\t%s\tMédia\t🎥\tvideo\t%s\t%s\n' "$video_url" "$name" "$thumb_url" "$dur_fmt" >> "$VIDEOS_FILE"
|
|
done
|
|
fi
|
|
fi
|
|
|
|
# Merge videos into sites file
|
|
cat "$VIDEOS_FILE" >> "$SITES_FILE" 2>/dev/null
|
|
|
|
# Stats
|
|
TOTAL=$(wc -l < "$SITES_FILE" | tr -d ' ')
|
|
TOTAL_META=$(grep " meta " "$SITES_FILE" | wc -l | tr -d ' ')
|
|
TOTAL_STREAMLIT=$(grep " streamlit " "$SITES_FILE" | wc -l | tr -d ' ')
|
|
TOTAL_VIDEOS=$(grep " video " "$SITES_FILE" | wc -l | tr -d ' ')
|
|
TOTAL_SERVICES=$(grep " service " "$SITES_FILE" | wc -l | tr -d ' ')
|
|
CAT_COUNTS=$(grep -v "^$" "$CAT_FILE" 2>/dev/null | sort | uniq -c | sort -rn)
|
|
|
|
# Capability and audience counts
|
|
CAPS_FILE="/tmp/hub_caps_$$.txt"
|
|
AUDIENCES_FILE="/tmp/hub_audiences_$$.txt"
|
|
[ -f "$CAPS_FILE" ] && CAP_COUNTS=$(sort "$CAPS_FILE" | uniq -c | sort -rn | head -10) || CAP_COUNTS=""
|
|
[ -f "$AUDIENCES_FILE" ] && AUDIENCE_COUNTS=$(sort "$AUDIENCES_FILE" | uniq -c | sort -rn) || AUDIENCE_COUNTS=""
|
|
TOTAL_WITH_NFO=$(grep -v " - - - - -$" "$SITES_FILE" 2>/dev/null | wc -l | tr -d ' ')
|
|
|
|
# Stats bar
|
|
cat >> "$TEMP" << EOF
|
|
<div class="stats-bar">
|
|
<div class="stat"><span class="stat-value">$TOTAL</span><span class="stat-label">Total</span></div>
|
|
<div class="stat"><span class="stat-value">$TOTAL_META</span><span class="stat-label">Sites</span></div>
|
|
<div class="stat"><span class="stat-value">$TOTAL_STREAMLIT</span><span class="stat-label">Streamlit</span></div>
|
|
<div class="stat"><span class="stat-value">$TOTAL_SERVICES</span><span class="stat-label">Services</span></div>
|
|
<div class="stat"><span class="stat-value">$TOTAL_VIDEOS</span><span class="stat-label">Vidéos</span></div>
|
|
<div class="stat"><span class="stat-value">$TOTAL_WITH_NFO</span><span class="stat-label">NFO</span></div>
|
|
EOF
|
|
echo "$CAT_COUNTS" | head -3 | while read count cat; do
|
|
[ -n "$cat" ] && printf ' <div class="stat"><span class="stat-value">%s</span><span class="stat-label">%s</span></div>\n' "$count" "$cat" >> "$TEMP"
|
|
done
|
|
echo "</div>" >> "$TEMP"
|
|
|
|
# Tag cloud - categories
|
|
echo '<div class="tag-cloud" id="tagCloud">' >> "$TEMP"
|
|
echo '<span class="tag active" data-cat="all">Tous</span>' >> "$TEMP"
|
|
echo '<span class="tag" data-cat="meta">📝 Sites</span>' >> "$TEMP"
|
|
echo '<span class="tag" data-cat="streamlit">📊 Streamlit</span>' >> "$TEMP"
|
|
echo '<span class="tag" data-cat="service">🔌 Services</span>' >> "$TEMP"
|
|
echo '<span class="tag" data-cat="video">🎥 Vidéos</span>' >> "$TEMP"
|
|
echo "$CAT_COUNTS" | while read count cat; do
|
|
[ -n "$cat" ] && printf '<span class="tag" data-cat="%s">%s<span class="count">%s</span></span>\n' "$cat" "$cat" "$count" >> "$TEMP"
|
|
done
|
|
echo '</div>' >> "$TEMP"
|
|
|
|
# Capability filter cloud
|
|
if [ -n "$CAP_COUNTS" ]; then
|
|
echo '<div class="cap-cloud" id="capCloud">' >> "$TEMP"
|
|
echo '<span class="cap-title">🔧 Capabilities:</span>' >> "$TEMP"
|
|
echo "$CAP_COUNTS" | while read count cap; do
|
|
[ -n "$cap" ] && printf '<span class="cap-filter" data-cap="%s">%s<span class="count">%s</span></span>\n' "$cap" "$cap" "$count" >> "$TEMP"
|
|
done
|
|
echo '</div>' >> "$TEMP"
|
|
fi
|
|
|
|
# Audience filter
|
|
if [ -n "$AUDIENCE_COUNTS" ]; then
|
|
echo '<div class="audience-bar" id="audienceBar">' >> "$TEMP"
|
|
echo '<span class="audience-title">👥 Audience:</span>' >> "$TEMP"
|
|
echo '<span class="audience-btn active" data-audience="all">Tous</span>' >> "$TEMP"
|
|
echo "$AUDIENCE_COUNTS" | while read count audience; do
|
|
[ -n "$audience" ] && printf '<span class="audience-btn" data-audience="%s">%s<span class="count">%s</span></span>\n' "$audience" "$audience" "$count" >> "$TEMP"
|
|
done
|
|
echo '</div>' >> "$TEMP"
|
|
fi
|
|
|
|
# Category tabs
|
|
echo '<div class="category-tabs">' >> "$TEMP"
|
|
printf '<div class="cat-tab active" data-cat="all">📁 Tous<span class="count">%s</span></div>\n' "$TOTAL" >> "$TEMP"
|
|
printf '<div class="cat-tab" data-cat="meta">📝 Sites<span class="count">%s</span></div>\n' "$TOTAL_META" >> "$TEMP"
|
|
printf '<div class="cat-tab" data-cat="streamlit">📊 Streamlit<span class="count">%s</span></div>\n' "$TOTAL_STREAMLIT" >> "$TEMP"
|
|
printf '<div class="cat-tab" data-cat="service">🔌 Services<span class="count">%s</span></div>\n' "$TOTAL_SERVICES" >> "$TEMP"
|
|
printf '<div class="cat-tab" data-cat="video">🎥 Vidéos<span class="count">%s</span></div>\n' "$TOTAL_VIDEOS" >> "$TEMP"
|
|
echo "$CAT_COUNTS" | head -6 | while read count cat; do
|
|
emoji=$(get_emoji "$cat")
|
|
[ -n "$cat" ] && printf '<div class="cat-tab" data-cat="%s">%s %s<span class="count">%s</span></div>\n' "$cat" "$emoji" "$cat" "$count" >> "$TEMP"
|
|
done
|
|
echo '</div>' >> "$TEMP"
|
|
|
|
# Sites grid
|
|
echo '<div class="view-grid" id="viewContainer"><div class="sites-grid" id="sitesGrid">' >> "$TEMP"
|
|
|
|
while IFS=' ' read -r url name cat emoji type thumb protected nfo_desc nfo_keywords nfo_caps nfo_version nfo_audience; do
|
|
[ -z "$url" ] && continue
|
|
|
|
# Handle placeholder values (- means empty, used for BusyBox read compatibility)
|
|
[ "$thumb" = "-" ] && thumb=""
|
|
[ "$protected" = "-" ] && protected=""
|
|
[ "$nfo_desc" = "-" ] && nfo_desc=""
|
|
[ "$nfo_keywords" = "-" ] && nfo_keywords=""
|
|
[ "$nfo_caps" = "-" ] && nfo_caps=""
|
|
[ "$nfo_version" = "-" ] && nfo_version=""
|
|
[ "$nfo_audience" = "-" ] && nfo_audience=""
|
|
|
|
# For videos, 'thumb' is thumbnail URL and 'protected' is duration
|
|
duration=""
|
|
if [ "$type" = "video" ]; then
|
|
duration="$protected"
|
|
protected=""
|
|
fi
|
|
|
|
protected_attr=""
|
|
protected_badge=""
|
|
if [ "$protected" = "protected" ]; then
|
|
protected_attr="data-protected=\"1\""
|
|
protected_badge="<span class=\"lock-badge\">🔒</span>"
|
|
fi
|
|
|
|
if [ "$type" = "streamlit" ]; then
|
|
card_class="site-card streamlit"
|
|
cat_class="card-cat streamlit"
|
|
preview_html="<iframe src=\"https://$url/\" loading=\"lazy\" sandbox></iframe>"
|
|
elif [ "$type" = "video" ]; then
|
|
card_class="site-card video"
|
|
cat_class="card-cat video"
|
|
preview_html="<img src=\"$thumb\" alt=\"$name\" loading=\"lazy\"><div class=\"play-icon\"></div><span class=\"duration\">$duration</span>"
|
|
elif [ "$type" = "service" ]; then
|
|
card_class="site-card service"
|
|
cat_class="card-cat service"
|
|
preview_html="<iframe src=\"https://$url/\" loading=\"lazy\" sandbox></iframe>"
|
|
else
|
|
card_class="site-card"
|
|
cat_class="card-cat meta"
|
|
preview_html="<iframe src=\"https://$url/\" loading=\"lazy\" sandbox></iframe>"
|
|
fi
|
|
|
|
# URL handling - videos already have full URL
|
|
if [ "$type" = "video" ]; then
|
|
link_url="$url"
|
|
domain_display="tube.gk2.secubox.in"
|
|
else
|
|
link_url="https://$url/"
|
|
domain_display="$url"
|
|
fi
|
|
|
|
# Build version badge
|
|
version_html=""
|
|
if [ -n "$nfo_version" ]; then
|
|
version_html="<span class=\"card-version\">v$nfo_version</span>"
|
|
fi
|
|
|
|
# Build description HTML
|
|
desc_html=""
|
|
if [ -n "$nfo_desc" ]; then
|
|
desc_html="<div class=\"card-desc\">$nfo_desc</div>"
|
|
fi
|
|
|
|
# Build capabilities badges HTML
|
|
caps_html=""
|
|
if [ -n "$nfo_caps" ]; then
|
|
caps_html="<div class=\"card-caps\">"
|
|
for cap in $(echo "$nfo_caps" | tr ',' ' '); do
|
|
[ -n "$cap" ] && caps_html="$caps_html<span class=\"cap-badge\" data-cap=\"$cap\">$cap</span>"
|
|
done
|
|
caps_html="$caps_html</div>"
|
|
fi
|
|
|
|
# Include all metadata in search data
|
|
search_data="$url $name $cat $type $nfo_keywords $nfo_caps $nfo_audience $nfo_desc"
|
|
|
|
# Data attributes for filtering
|
|
data_caps=""
|
|
[ -n "$nfo_caps" ] && data_caps="data-caps=\"$nfo_caps\""
|
|
data_audience=""
|
|
[ -n "$nfo_audience" ] && data_audience="data-audience=\"$nfo_audience\""
|
|
|
|
cat >> "$TEMP" << CARD
|
|
<a href="$link_url" class="$card_class" target="_blank" data-category="$cat" data-type="$type" data-search="$search_data" $data_caps $data_audience $protected_attr>
|
|
<div class="card-preview">
|
|
$preview_html
|
|
$protected_badge
|
|
</div>
|
|
<div class="card-content">
|
|
<div class="card-header">
|
|
<div class="card-title">$name</div>
|
|
$version_html
|
|
</div>
|
|
<div class="card-domain">$domain_display</div>
|
|
$desc_html
|
|
<span class="$cat_class">$emoji $cat</span>
|
|
$caps_html
|
|
</div>
|
|
</a>
|
|
CARD
|
|
done < "$SITES_FILE"
|
|
|
|
echo '</div></div>' >> "$TEMP"
|
|
|
|
# Footer and JS
|
|
cat >> "$TEMP" << 'FOOTER'
|
|
<div class="footer">SecuBox Hub — <a href="https://cybermind.fr">CyberMind.FR</a></div>
|
|
</div>
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded',function(){
|
|
const container=document.getElementById('viewContainer');
|
|
const cards=document.querySelectorAll('.site-card');
|
|
const search=document.getElementById('searchBox');
|
|
const cloud=document.getElementById('tagCloud');
|
|
const loginBanner=document.getElementById('loginBanner');
|
|
const protectedCards=document.querySelectorAll('.site-card[data-protected]');
|
|
|
|
// Check authentication
|
|
const isLoggedIn=sessionStorage.getItem('secubox_token')!==null;
|
|
|
|
if(isLoggedIn){
|
|
protectedCards.forEach(c=>c.classList.add('unlocked'));
|
|
loginBanner.classList.add('hidden');
|
|
}else if(protectedCards.length===0){
|
|
loginBanner.classList.add('hidden');
|
|
}
|
|
|
|
// Current filters state
|
|
let currentCat='all';
|
|
let currentCap='';
|
|
let currentAudience='all';
|
|
|
|
function applyFilters(){
|
|
cards.forEach(c=>{
|
|
let show=true;
|
|
// Category/type filter
|
|
if(currentCat!=='all'){
|
|
show=c.dataset.category===currentCat||c.dataset.type===currentCat;
|
|
}
|
|
// Capability filter
|
|
if(show&¤tCap){
|
|
const caps=c.dataset.caps||'';
|
|
show=caps.toLowerCase().includes(currentCap.toLowerCase());
|
|
}
|
|
// Audience filter
|
|
if(show&¤tAudience!=='all'){
|
|
const aud=c.dataset.audience||'';
|
|
show=aud.toLowerCase()===currentAudience.toLowerCase();
|
|
}
|
|
// Protected filter
|
|
if(show&&c.dataset.protected&&!isLoggedIn)show=false;
|
|
c.style.display=show?'':'none';
|
|
});
|
|
}
|
|
|
|
// View toggle
|
|
document.querySelectorAll('.view-btn[data-view]').forEach(b=>b.onclick=function(){
|
|
document.querySelectorAll('.view-btn[data-view]').forEach(x=>x.classList.remove('active'));
|
|
this.classList.add('active');
|
|
container.className='view-'+this.dataset.view;
|
|
});
|
|
|
|
// Tag cloud toggle
|
|
document.getElementById('toggleCloud').onclick=function(){
|
|
cloud.classList.toggle('visible');
|
|
this.classList.toggle('active');
|
|
};
|
|
|
|
// Category tabs
|
|
document.querySelectorAll('.cat-tab,.tag').forEach(t=>t.onclick=function(){
|
|
document.querySelectorAll('.cat-tab,.tag').forEach(x=>x.classList.remove('active'));
|
|
this.classList.add('active');
|
|
currentCat=this.dataset.cat;
|
|
applyFilters();
|
|
});
|
|
|
|
// Capability filter
|
|
document.querySelectorAll('.cap-filter').forEach(c=>c.onclick=function(){
|
|
document.querySelectorAll('.cap-filter').forEach(x=>x.classList.remove('active'));
|
|
if(currentCap===this.dataset.cap){
|
|
currentCap='';
|
|
}else{
|
|
this.classList.add('active');
|
|
currentCap=this.dataset.cap;
|
|
}
|
|
applyFilters();
|
|
});
|
|
|
|
// Audience filter
|
|
document.querySelectorAll('.audience-btn').forEach(a=>a.onclick=function(){
|
|
document.querySelectorAll('.audience-btn').forEach(x=>x.classList.remove('active'));
|
|
this.classList.add('active');
|
|
currentAudience=this.dataset.audience;
|
|
applyFilters();
|
|
});
|
|
|
|
// Search
|
|
search.oninput=function(){
|
|
const q=this.value.toLowerCase();
|
|
if(q===''){
|
|
applyFilters();
|
|
return;
|
|
}
|
|
cards.forEach(c=>{
|
|
let show=c.dataset.search.toLowerCase().includes(q);
|
|
if(c.dataset.protected&&!isLoggedIn)show=false;
|
|
c.style.display=show?'':'none';
|
|
});
|
|
};
|
|
|
|
// Dynamic preview modal
|
|
const previewModal=document.createElement('div');
|
|
previewModal.id='previewModal';
|
|
previewModal.className='preview-modal';
|
|
previewModal.innerHTML='<div class="preview-content"><button class="preview-close">×</button><iframe id="previewFrame"></iframe><div class="preview-info"><span id="previewTitle"></span><a id="previewLink" target="_blank">Open in new tab ↗</a></div></div>';
|
|
document.body.appendChild(previewModal);
|
|
|
|
previewModal.querySelector('.preview-close').onclick=function(){
|
|
previewModal.classList.remove('active');
|
|
document.getElementById('previewFrame').src='about:blank';
|
|
};
|
|
previewModal.onclick=function(e){
|
|
if(e.target===previewModal){
|
|
previewModal.classList.remove('active');
|
|
document.getElementById('previewFrame').src='about:blank';
|
|
}
|
|
};
|
|
|
|
// Add preview button to each card
|
|
cards.forEach(card=>{
|
|
if(card.classList.contains('video'))return; // Skip videos
|
|
const previewBtn=document.createElement('button');
|
|
previewBtn.className='preview-btn';
|
|
previewBtn.innerHTML='👁';
|
|
previewBtn.title='Preview';
|
|
previewBtn.onclick=function(e){
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
const url=card.href;
|
|
const name=card.querySelector('.card-title').textContent;
|
|
document.getElementById('previewFrame').src=url;
|
|
document.getElementById('previewTitle').textContent=name;
|
|
document.getElementById('previewLink').href=url;
|
|
previewModal.classList.add('active');
|
|
};
|
|
card.querySelector('.card-preview').appendChild(previewBtn);
|
|
});
|
|
|
|
// Keyboard shortcut
|
|
document.onkeydown=function(e){
|
|
if(e.key==='Escape'){
|
|
previewModal.classList.remove('active');
|
|
document.getElementById('previewFrame').src='about:blank';
|
|
}
|
|
};
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|
|
FOOTER
|
|
|
|
rm -f "$SITES_FILE" "$CAT_FILE" "$VIDEOS_FILE" "$CAPS_FILE" "$AUDIENCES_FILE"
|
|
mv "$TEMP" "$OUTPUT"
|
|
chmod 644 "$OUTPUT"
|
|
logger -t hub-generator "Hub v7: $TOTAL items ($TOTAL_META sites + $TOTAL_STREAMLIT streamlit + $TOTAL_VIDEOS videos, $TOTAL_WITH_NFO with NFO)"
|