mirror of
https://github.com/infinition/Bjorn.git
synced 2025-12-13 16:14:57 +00:00
650 lines
27 KiB
HTML
650 lines
27 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
|
<title>Bjorn Cyberviking - Loot</title>
|
|
<link rel="icon" href="web/images/favicon.ico" type="image/x-icon" />
|
|
<link rel="stylesheet" href="web/css/global.css" />
|
|
<link rel="stylesheet" href="web/css/all.min.css" />
|
|
<link rel="manifest" href="manifest.json" />
|
|
<link rel="apple-touch-icon" sizes="192x192" href="web/images/icon-192x192.png" />
|
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
<meta name="mobile-web-app-capable" content="yes" />
|
|
<meta name="theme-color" content="#0a0a0a" />
|
|
<script src="web/js/global.js" defer></script>
|
|
|
|
<style>
|
|
/* ====== Respect global.css tokens (fallbacks only) ====== */
|
|
:root{
|
|
--_bg: var(--bg, #0b0c0f);
|
|
--_panel: var(--c-panel-2, rgba(16,22,22,.55));
|
|
--_border: var(--c-border, rgba(255,255,255,.08));
|
|
--_ink: var(--ink, #e6fff7);
|
|
--_muted: var(--muted, #8affc1cc);
|
|
--_acid: var(--acid, #00ff9a);
|
|
--_acid2: var(--acid-2, #18f0ff);
|
|
--_shadow: var(--shadow, 0 10px 26px rgba(0,0,0,.35));
|
|
}
|
|
|
|
*{ margin:0; padding:0; box-sizing:border-box; }
|
|
|
|
body{
|
|
background: var(--_bg);
|
|
color: var(--_ink);
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Inter', system-ui, sans-serif;
|
|
min-height:100vh; overflow-x:hidden;
|
|
}
|
|
|
|
.main{ padding:16px; }
|
|
|
|
/* gentle ambient glow (kept subtle) */
|
|
body::before{
|
|
content:'';
|
|
position:fixed; inset:0;
|
|
background:
|
|
radial-gradient(60rem 60rem at 20% 50%, color-mix(in oklab, var(--_acid) 10%, transparent), transparent 60%),
|
|
radial-gradient(60rem 60rem at 80% 80%, color-mix(in oklab, var(--_acid2) 10%, transparent), transparent 60%);
|
|
pointer-events:none; z-index:1; animation:breathe 20s ease-in-out infinite;
|
|
}
|
|
@keyframes breathe{ 0%,100%{opacity:1;} 50%{opacity:.6;} }
|
|
|
|
.loot-container{
|
|
position:relative; z-index:2;
|
|
padding:16px; margin-top:5px;
|
|
min-height:calc(100vh - 60px);
|
|
display:flex; flex-direction:column; gap:16px;
|
|
animation:fadeInUp .6s ease-out;
|
|
}
|
|
@keyframes fadeInUp{ from{opacity:0; transform:translateY(30px);} to{opacity:1; transform:translateY(0);} }
|
|
|
|
/* ===== Stats bar ===== */
|
|
.stats-bar{
|
|
display:flex; gap:12px; flex-wrap:wrap;
|
|
padding:12px;
|
|
background: color-mix(in oklab, var(--_panel) 88%, transparent);
|
|
border:1px solid var(--_border);
|
|
border-radius:12px;
|
|
box-shadow: var(--_shadow);
|
|
backdrop-filter: blur(16px);
|
|
}
|
|
.stat-item{
|
|
display:flex; align-items:center; gap:8px;
|
|
padding:8px 16px;
|
|
background: color-mix(in oklab, var(--_panel) 65%, transparent);
|
|
border:1px solid var(--_border);
|
|
border-radius:10px;
|
|
transition: .2s;
|
|
}
|
|
.stat-item:hover{ background: color-mix(in oklab, var(--_panel) 78%, transparent); transform: translateY(-2px); }
|
|
.stat-icon{ font-size:1.2rem; opacity:.95; }
|
|
.stat-value{
|
|
font-size:1.05rem; font-weight:800;
|
|
background: linear-gradient(135deg, var(--_acid), var(--_acid2));
|
|
-webkit-background-clip:text; background-clip:text; -webkit-text-fill-color:transparent;
|
|
}
|
|
.stat-label{ color: var(--_muted); font-size:.75rem; margin-left:4px; }
|
|
|
|
/* ===== Controls ===== */
|
|
.controls-bar{ display:flex; gap:12px; align-items:center; flex-wrap:wrap; }
|
|
|
|
.search-container{ flex:1; min-width:200px; position:relative; }
|
|
.search-input{
|
|
width:100%; padding:12px 16px 12px 44px;
|
|
background: color-mix(in oklab, var(--_panel) 90%, transparent);
|
|
border:1px solid var(--_border);
|
|
border-radius:12px;
|
|
color: var(--_ink); font-size:.95rem;
|
|
backdrop-filter: blur(10px);
|
|
transition:.2s;
|
|
}
|
|
.search-input:focus{
|
|
outline:none;
|
|
border-color: color-mix(in oklab, var(--_acid2) 40%, var(--_border));
|
|
box-shadow: 0 0 0 3px color-mix(in oklab, var(--_acid2) 18%, transparent);
|
|
background: color-mix(in oklab, var(--_panel) 96%, transparent);
|
|
}
|
|
.search-icon{
|
|
position:absolute; left:16px; top:50%; transform:translateY(-50%);
|
|
color: var(--_muted); pointer-events:none;
|
|
}
|
|
.clear-search{
|
|
position:absolute; right:12px; top:50%; transform:translateY(-50%);
|
|
color: var(--_muted); cursor:pointer; font-size:1rem; display:none;
|
|
}
|
|
.search-input:not(:placeholder-shown) ~ .clear-search{ display:block; }
|
|
|
|
.view-controls{ display:flex; gap:8px; align-items:center; }
|
|
.view-btn, .sort-btn{
|
|
padding:10px;
|
|
background: color-mix(in oklab, var(--_panel) 90%, transparent);
|
|
border:1px solid var(--_border);
|
|
border-radius:10px;
|
|
color: var(--_muted); cursor:pointer;
|
|
transition:.2s; backdrop-filter: blur(10px); font-size:1.1rem;
|
|
}
|
|
.view-btn:hover, .sort-btn:hover{ background: color-mix(in oklab, var(--_panel) 96%, transparent); color: var(--_ink); transform: translateY(-2px); }
|
|
.view-btn.active{
|
|
background: linear-gradient(135deg, color-mix(in oklab, var(--_acid) 20%, transparent), color-mix(in oklab, var(--_acid2) 12%, transparent));
|
|
color: var(--_ink);
|
|
border-color: color-mix(in oklab, var(--_acid2) 35%, var(--_border));
|
|
}
|
|
|
|
.sort-dropdown{ position:relative; }
|
|
.sort-menu{
|
|
position:absolute; top:calc(100% + 8px); right:0;
|
|
background: color-mix(in oklab, var(--_panel) 98%, transparent);
|
|
border:1px solid var(--_border); border-radius:12px;
|
|
padding:8px; min-width:150px;
|
|
backdrop-filter: blur(20px);
|
|
box-shadow: var(--_shadow);
|
|
opacity:0; pointer-events:none; transform: translateY(-10px);
|
|
transition:.2s; z-index:10;
|
|
}
|
|
.sort-dropdown.active .sort-menu{ opacity:1; pointer-events:auto; transform: translateY(0); }
|
|
.sort-option{
|
|
padding:10px 12px; border-radius:8px; cursor:pointer; transition:.2s; font-size:.9rem; color: var(--_ink);
|
|
}
|
|
.sort-option:hover{ background: rgba(255,255,255,.05); }
|
|
.sort-option.active{ color: var(--_ink); background: color-mix(in oklab, var(--_acid2) 14%, transparent); }
|
|
|
|
/* ===== Tabs ===== */
|
|
.tabs-container{
|
|
display:flex; gap:8px; padding:4px;
|
|
background: color-mix(in oklab, var(--_panel) 88%, transparent);
|
|
border-radius:12px; border:1px solid var(--_border);
|
|
backdrop-filter: blur(10px);
|
|
overflow-x:auto; scrollbar-width:none;
|
|
}
|
|
.tabs-container::-webkit-scrollbar{ display:none; }
|
|
.tab{
|
|
padding:10px 20px; border-radius:8px; cursor:pointer; transition:.2s;
|
|
white-space:nowrap; font-size:.9rem; font-weight:700; position:relative;
|
|
color: var(--_muted); border:1px solid transparent;
|
|
}
|
|
.tab:hover{ background: rgba(255,255,255,.05); color: var(--_ink); }
|
|
.tab.active{
|
|
background: linear-gradient(135deg, color-mix(in oklab, var(--_acid) 16%, transparent), color-mix(in oklab, var(--_acid2) 10%, transparent));
|
|
color: var(--_ink);
|
|
border-color: color-mix(in oklab, var(--_acid2) 28%, var(--_border));
|
|
}
|
|
.tab.active::after{
|
|
content:''; position:absolute; bottom:0; left:10%; right:10%; height:2px;
|
|
background: linear-gradient(90deg, var(--_acid), var(--_acid2)); border-radius:2px;
|
|
}
|
|
.tab-badge{
|
|
display:inline-block; padding:2px 6px; margin-left:6px;
|
|
background: rgba(255,255,255,.08); border:1px solid var(--_border);
|
|
border-radius:10px; font-size:.75rem; font-weight:700; color: var(--_ink);
|
|
}
|
|
|
|
/* ===== Explorer ===== */
|
|
.explorer{
|
|
background: color-mix(in oklab, var(--_panel) 88%, transparent);
|
|
border-radius:20px; border:1px solid var(--_border);
|
|
backdrop-filter: blur(20px);
|
|
box-shadow: var(--_shadow);
|
|
overflow:hidden; flex:1; display:flex; flex-direction:column;
|
|
animation:slideIn .6s ease-out;
|
|
}
|
|
@keyframes slideIn{ from{opacity:0; transform:translateX(-16px);} to{opacity:1; transform:translateX(0);} }
|
|
|
|
.explorer-content{ padding:20px; overflow-y:auto; flex:1; max-height:calc(100vh - 280px); }
|
|
|
|
.tree-view{ display:none; }
|
|
.tree-view.active{ display:block; }
|
|
.list-view{ display:none; }
|
|
.list-view.active{ display:grid; gap:8px; }
|
|
|
|
.tree-item{ margin-bottom:4px; animation:itemSlide .3s ease-out backwards; }
|
|
@keyframes itemSlide{ from{opacity:0; transform:translateX(-10px);} to{opacity:1; transform:translateX(0);} }
|
|
|
|
.tree-header{
|
|
display:flex; align-items:center; padding:12px; cursor:pointer;
|
|
border-radius:10px; transition:.2s; position:relative; overflow:hidden;
|
|
}
|
|
.tree-header::before{
|
|
content:''; position:absolute; inset:0;
|
|
background: linear-gradient(90deg, transparent, rgba(255,255,255,.05), transparent);
|
|
transform: translateX(-100%); transition: transform .6s;
|
|
}
|
|
.tree-header:hover::before{ transform: translateX(100%); }
|
|
.tree-header:hover{ background: rgba(255,255,255,.04); }
|
|
|
|
.tree-icon{
|
|
width:32px; height:32px; display:flex; align-items:center; justify-content:center;
|
|
border-radius:8px; margin-right:12px; font-size:1.1rem; flex-shrink:0;
|
|
background: color-mix(in oklab, var(--_acid) 12%, transparent); color: var(--_ink);
|
|
}
|
|
.folder-icon{ background: color-mix(in oklab, var(--_acid) 10%, transparent); color: var(--_ink); }
|
|
|
|
.tree-name{ flex:1; font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
|
.tree-chevron{ width:20px; height:20px; display:flex; align-items:center; justify-content:center; color: var(--_muted); transition: transform .3s cubic-bezier(.4,0,.2,1); margin-left:8px; }
|
|
.tree-item.expanded .tree-chevron{ transform: rotate(90deg); }
|
|
|
|
.tree-children{
|
|
max-height:0; overflow:hidden;
|
|
transition:max-height .3s cubic-bezier(.4,0,.2,1);
|
|
margin-left:20px; padding-left:20px;
|
|
border-left:1px solid var(--_border);
|
|
}
|
|
.tree-item.expanded .tree-children{ max-height:5000px; }
|
|
|
|
.file-item{
|
|
display:flex; align-items:center; padding:10px 12px; border-radius:10px;
|
|
cursor:pointer; transition:.2s; margin-bottom:4px;
|
|
}
|
|
.file-item:hover{ background: rgba(255,255,255,.04); transform: translateX(4px); }
|
|
.file-item:active{ transform: translateX(2px) scale(.98); }
|
|
|
|
.file-icon{
|
|
width:28px; height:28px; display:flex; align-items:center; justify-content:center;
|
|
border-radius:6px; margin-right:12px; font-size:.9rem; flex-shrink:0; color: var(--_ink);
|
|
background: color-mix(in oklab, var(--_panel) 75%, transparent);
|
|
}
|
|
.file-icon.ssh{ background: color-mix(in oklab, var(--_acid) 12%, transparent); }
|
|
.file-icon.sql{ background: color-mix(in oklab, var(--_acid2) 12%, transparent); }
|
|
.file-icon.smb{ background: color-mix(in oklab, var(--_acid2) 16%, transparent); }
|
|
.file-icon.other{ background: color-mix(in oklab, var(--_panel) 75%, transparent); }
|
|
|
|
.file-name{ flex:1; font-size:.9rem; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; color: var(--_ink); }
|
|
.file-type{
|
|
padding:3px 8px; border-radius:6px; font-size:.7rem; font-weight:800;
|
|
text-transform:uppercase; letter-spacing:.05em; margin-left:8px;
|
|
border:1px solid var(--_border); color: var(--_ink);
|
|
background: color-mix(in oklab, var(--_panel) 80%, transparent);
|
|
}
|
|
.file-type.ssh{ background: color-mix(in oklab, var(--_acid) 12%, transparent); }
|
|
.file-type.sql{ background: color-mix(in oklab, var(--_acid2) 12%, transparent); }
|
|
.file-type.smb{ background: color-mix(in oklab, var(--_acid2) 16%, transparent); }
|
|
|
|
.no-results{ text-align:center; color: var(--_muted); padding:40px; font-size:.95rem; }
|
|
.no-results-icon{ font-size:3rem; margin-bottom:16px; opacity:.5; }
|
|
|
|
.loading{ display:flex; justify-content:center; align-items:center; height:200px; }
|
|
.loading-spinner{
|
|
width:40px; height:40px; border:3px solid var(--_border);
|
|
border-top-color: var(--_acid2); border-radius:50%; animation: spin 1s linear infinite;
|
|
}
|
|
@keyframes spin{ to{ transform: rotate(360deg); } }
|
|
|
|
/* ===== Responsive ===== */
|
|
@media (max-width:768px){
|
|
.loot-container{ padding:12px; gap:12px; }
|
|
.controls-bar{ flex-direction:column; align-items:stretch; }
|
|
.search-container{ width:100%; }
|
|
.view-controls{ justify-content:center; }
|
|
.tabs-container{ padding:2px; }
|
|
.tab{ padding:8px 14px; font-size:.85rem; }
|
|
.explorer-content{ padding:12px; max-height:calc(100vh - 320px); }
|
|
.tree-children{ margin-left:12px; padding-left:12px; }
|
|
.stat-item{ padding:6px 10px; }
|
|
.stat-value{ font-size:.95rem; }
|
|
}
|
|
@media (hover:none){ .tree-header:active{ background: rgba(255,255,255,.06); } }
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<div class="main" id="main">
|
|
<div class="loot-container">
|
|
<!-- Stats bar -->
|
|
<div class="stats-bar">
|
|
<div class="stat-item"><span class="stat-icon">👥</span><span class="stat-value" id="stat-victims">0</span><span class="stat-label">victims</span></div>
|
|
<div class="stat-item"><span class="stat-icon">📄</span><span class="stat-value" id="stat-files">0</span><span class="stat-label">files</span></div>
|
|
<div class="stat-item"><span class="stat-icon">📁</span><span class="stat-value" id="stat-folders">0</span><span class="stat-label">folders</span></div>
|
|
</div>
|
|
|
|
<!-- Controls -->
|
|
<div class="controls-bar">
|
|
<div class="search-container">
|
|
<span class="search-icon">🔍</span>
|
|
<input type="text" class="search-input" id="searchInput" placeholder="Search in all categories..." />
|
|
<span class="clear-search" id="clearSearch">❌</span>
|
|
</div>
|
|
<div class="view-controls">
|
|
<button class="view-btn active" id="treeViewBtn" title="Tree View">🌳</button>
|
|
<button class="view-btn" id="listViewBtn" title="List View">📋</button>
|
|
<div class="sort-dropdown" id="sortDropdown">
|
|
<button class="sort-btn" id="sortBtn">⬇️</button>
|
|
<div class="sort-menu">
|
|
<div class="sort-option active" data-sort="name">Name</div>
|
|
<div class="sort-option" data-sort="type">Type</div>
|
|
<div class="sort-option" data-sort="date">Date</div>
|
|
<div class="sort-option" data-sort="asc">Asc</div>
|
|
<div class="sort-option" data-sort="desc">Desc</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tabs-container" id="tabsContainer"></div>
|
|
|
|
<div class="explorer">
|
|
<div class="explorer-content" id="explorerContent">
|
|
<div class="loading"><div class="loading-spinner"></div></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ===== JS: unchanged logic ===== -->
|
|
<script>
|
|
let fileData = [];
|
|
let allFiles = [];
|
|
let currentView = 'tree';
|
|
let currentCategory = 'all';
|
|
let currentSort = 'name';
|
|
let sortDirection = 'asc';
|
|
let searchTerm = '';
|
|
let globalStats = {};
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
loadFiles();
|
|
setupEventListeners();
|
|
});
|
|
|
|
function setupEventListeners(){
|
|
const searchInput = document.getElementById('searchInput');
|
|
const clearBtn = document.getElementById('clearSearch');
|
|
let searchTimeout;
|
|
|
|
searchInput.addEventListener('input', e => {
|
|
clearTimeout(searchTimeout);
|
|
searchTimeout = setTimeout(() => {
|
|
searchTerm = e.target.value.toLowerCase();
|
|
renderContent(true);
|
|
}, 300);
|
|
});
|
|
|
|
clearBtn.addEventListener("click", () => {
|
|
searchInput.value = "";
|
|
searchTerm = "";
|
|
renderContent();
|
|
});
|
|
|
|
document.getElementById('treeViewBtn').addEventListener('click', () => setView('tree'));
|
|
document.getElementById('listViewBtn').addEventListener('click', () => setView('list'));
|
|
|
|
const sortDropdown = document.getElementById('sortDropdown');
|
|
const sortBtn = document.getElementById('sortBtn');
|
|
sortBtn.addEventListener('click', () => { sortDropdown.classList.toggle('active'); });
|
|
|
|
document.querySelectorAll('.sort-option').forEach(option => {
|
|
option.addEventListener('click', () => {
|
|
document.querySelectorAll('.sort-option').forEach(opt => opt.classList.remove('active'));
|
|
option.classList.add('active');
|
|
if (option.dataset.sort === 'asc' || option.dataset.sort === 'desc') {
|
|
sortDirection = option.dataset.sort;
|
|
} else {
|
|
currentSort = option.dataset.sort;
|
|
}
|
|
sortDropdown.classList.remove('active');
|
|
renderContent();
|
|
});
|
|
});
|
|
|
|
document.addEventListener('click', e => {
|
|
const sortDropdownEl = document.getElementById('sortDropdown');
|
|
if (!sortDropdownEl.contains(e.target)) sortDropdownEl.classList.remove('active');
|
|
});
|
|
}
|
|
|
|
function setView(view){
|
|
currentView = view;
|
|
document.querySelectorAll('.view-btn').forEach(btn => btn.classList.remove('active'));
|
|
document.getElementById(view === 'tree' ? 'treeViewBtn' : 'listViewBtn').classList.add('active');
|
|
renderContent();
|
|
}
|
|
|
|
function loadFiles(){
|
|
fetch('/loot_directories')
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
if (data.status === 'success') {
|
|
fileData = data.data;
|
|
processFiles();
|
|
updateStats();
|
|
renderContent();
|
|
}
|
|
})
|
|
.catch(err => {
|
|
console.error('Error loading files:', err);
|
|
document.getElementById('explorerContent').innerHTML =
|
|
'<div class="no-results"><div class="no-results-icon">⚠️</div>Failed to load files</div>';
|
|
});
|
|
}
|
|
|
|
function renderTabs(categories){
|
|
const tabs = document.getElementById('tabsContainer');
|
|
tabs.innerHTML = '';
|
|
tabs.innerHTML += `<div class="tab active" data-category="all">All <span class="tab-badge" id="badge-all">0</span></div>`;
|
|
categories.forEach(cat => {
|
|
tabs.innerHTML += `<div class="tab" data-category="${cat}">${cat.toUpperCase()}<span class="tab-badge" id="badge-${cat}">0</span></div>`;
|
|
});
|
|
tabs.querySelectorAll('.tab').forEach(tab => {
|
|
tab.addEventListener('click', () => {
|
|
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
tab.classList.add('active');
|
|
currentCategory = tab.dataset.category;
|
|
renderContent();
|
|
});
|
|
});
|
|
}
|
|
|
|
function processFiles(){
|
|
allFiles = [];
|
|
let stats = {};
|
|
function extractFiles(items, path = ''){
|
|
items.forEach(item => {
|
|
if (item.type === 'directory' && item.children) {
|
|
extractFiles(item.children, path + item.name + '/');
|
|
} else {
|
|
const category = getFileCategory(item.name, path);
|
|
allFiles.push({ ...item, category, fullPath: path + item.name });
|
|
stats[category] = (stats[category] || 0) + 1;
|
|
}
|
|
});
|
|
}
|
|
extractFiles(fileData);
|
|
globalStats = stats;
|
|
renderTabs(Object.keys(stats));
|
|
document.getElementById('badge-all').textContent = allFiles.length;
|
|
for (const cat in stats) {
|
|
const badge = document.getElementById(`badge-${cat}`);
|
|
if (badge) badge.textContent = stats[cat];
|
|
}
|
|
}
|
|
|
|
function getFileCategory(filename, path){
|
|
const lowerName = filename.toLowerCase();
|
|
const lowerPath = path.toLowerCase();
|
|
if (lowerPath.includes('ssh') || lowerName.includes('ssh') || lowerName.includes('key')) return 'ssh';
|
|
else if (lowerPath.includes('sql') || lowerName.includes('sql') || lowerName.includes('database')) return 'sql';
|
|
else if (lowerPath.includes('smb') || lowerName.includes('smb') || lowerName.includes('share')) return 'smb';
|
|
return 'other';
|
|
}
|
|
function getDirCategory(path){
|
|
const lowerPath = path.toLowerCase();
|
|
if (lowerPath.includes('ssh')) return 'ssh';
|
|
if (lowerPath.includes('sql')) return 'sql';
|
|
if (lowerPath.includes('smb')) return 'smb';
|
|
return 'other';
|
|
}
|
|
|
|
function updateStats(){
|
|
let victims = new Set(); let totalFiles = 0; let totalFolders = 0;
|
|
function scan(items){
|
|
items.forEach(item => {
|
|
if (item.type === 'directory') {
|
|
totalFolders++;
|
|
if (/^[0-9a-f:]{17}_\d+\.\d+\.\d+\.\d+$/i.test(item.name)) { victims.add(item.name); }
|
|
if (item.children) scan(item.children);
|
|
} else { totalFiles++; }
|
|
});
|
|
}
|
|
scan(fileData);
|
|
document.getElementById('stat-victims').textContent = victims.size;
|
|
document.getElementById('stat-files').textContent = totalFiles;
|
|
document.getElementById('stat-folders').textContent = totalFolders;
|
|
}
|
|
|
|
/* ========== Search & badges (logic unchanged) ========== */
|
|
function fileMatchesSearch(f){
|
|
if (!searchTerm) return true;
|
|
const n = f.name?.toLowerCase() || "";
|
|
const p = f.fullPath?.toLowerCase() || "";
|
|
return n.includes(searchTerm) || p.includes(searchTerm);
|
|
}
|
|
function computeSearchFilteredFiles(){
|
|
return allFiles.filter(fileMatchesSearch);
|
|
}
|
|
function updateBadgesFromFiltered(){
|
|
const filtered = computeSearchFilteredFiles();
|
|
const allBadge = document.getElementById("badge-all");
|
|
if (allBadge) allBadge.textContent = filtered.length;
|
|
|
|
const byCat = filtered.reduce((acc, f) => {
|
|
acc[f.category] = (acc[f.category] || 0) + 1;
|
|
return acc;
|
|
}, {});
|
|
document.querySelectorAll(".tab").forEach(tab => {
|
|
const cat = tab.dataset.category;
|
|
if (cat !== "all") {
|
|
const badge = document.getElementById(`badge-${cat}`);
|
|
if (badge) badge.textContent = byCat[cat] || 0;
|
|
}
|
|
});
|
|
}
|
|
|
|
function renderContent(autoExpand = false){
|
|
const container = document.getElementById('explorerContent');
|
|
if (currentView === 'tree') {
|
|
renderTreeView(container);
|
|
} else {
|
|
renderListView(container);
|
|
}
|
|
}
|
|
|
|
function renderTreeView(container){
|
|
const filteredData = filterDataForTree();
|
|
updateBadgesFromFiltered();
|
|
|
|
if (filteredData.length === 0) {
|
|
container.innerHTML = '<div class="no-results"><div class="no-results-icon">🔍</div>No results</div>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = `<div class="tree-view active">${renderTreeItems(filteredData, 0)}</div>`;
|
|
container.querySelectorAll('.tree-header').forEach(header => {
|
|
header.addEventListener('click', e => {
|
|
e.stopPropagation();
|
|
const treeItem = header.closest('.tree-item');
|
|
if (treeItem) { treeItem.classList.toggle('expanded'); }
|
|
});
|
|
});
|
|
container.querySelectorAll('.file-item').forEach(fileItem => {
|
|
fileItem.addEventListener('click', e => {
|
|
e.stopPropagation();
|
|
const path = fileItem.dataset.path;
|
|
if (path) downloadFile(path);
|
|
});
|
|
});
|
|
}
|
|
|
|
function filterDataForTree(){
|
|
function filterItems(items, path = '', isRoot = false){
|
|
return items.map(item => {
|
|
if (item.type === 'directory') {
|
|
const dirPath = path + item.name + '/';
|
|
const dirCategory = getDirCategory(dirPath);
|
|
const filteredChildren = item.children ? filterItems(item.children, dirPath, false) : [];
|
|
const nameMatch = item.name.toLowerCase().includes(searchTerm);
|
|
|
|
if (isRoot) {
|
|
if (currentCategory !== 'all' && dirCategory !== currentCategory) return null;
|
|
if (!searchTerm) return { ...item, children: filteredChildren };
|
|
if (filteredChildren.length > 0) return { ...item, children: filteredChildren };
|
|
return null;
|
|
}
|
|
|
|
if (nameMatch || filteredChildren.length > 0) return { ...item, children: filteredChildren };
|
|
return null;
|
|
} else {
|
|
const category = getFileCategory(item.name, path);
|
|
const temp = { ...item, category, fullPath: path + item.name };
|
|
const matchesSearch = fileMatchesSearch(temp);
|
|
const matchesCategory = (currentCategory === 'all' || category === currentCategory);
|
|
return (matchesSearch && matchesCategory) ? temp : null;
|
|
}
|
|
}).filter(Boolean);
|
|
}
|
|
return filterItems(fileData, '', true);
|
|
}
|
|
|
|
function renderTreeItems(items, level, path = ''){
|
|
return items.map((item, index) => {
|
|
if (item.type === 'directory') {
|
|
const hasChildren = item.children && item.children.length > 0;
|
|
const expandedClass = searchTerm ? " expanded" : "";
|
|
return `<div class="tree-item${expandedClass}" style="animation-delay:${index*0.05}s">
|
|
<div class="tree-header">
|
|
<div class="tree-icon folder-icon">📁</div>
|
|
<div class="tree-name">${item.name}</div>
|
|
${hasChildren ? '<div class="tree-chevron">▶</div>' : ''}
|
|
</div>
|
|
${hasChildren ? `<div class="tree-children">${renderTreeItems(item.children, level + 1, path + item.name + '/')}</div>` : ''}
|
|
</div>`;
|
|
} else {
|
|
const category = getFileCategory(item.name, path);
|
|
return renderFileItem({ ...item, category, fullPath: path + item.name }, category, index, false);
|
|
}
|
|
}).join('');
|
|
}
|
|
|
|
function renderListView(container){
|
|
let filtered = allFiles.filter(f => fileMatchesSearch(f) && (currentCategory === 'all' || f.category === currentCategory));
|
|
updateBadgesFromFiltered();
|
|
|
|
filtered.sort((a, b) => {
|
|
let res = 0;
|
|
switch (currentSort) {
|
|
case 'type': res = a.category.localeCompare(b.category) || a.name.localeCompare(b.name); break;
|
|
case 'path': res = a.fullPath.localeCompare(b.fullPath); break;
|
|
case 'date': res = a.name.localeCompare(b.name); break;
|
|
case 'name':
|
|
default: res = a.name.localeCompare(b.name);
|
|
}
|
|
return sortDirection === 'desc' ? -res : res;
|
|
});
|
|
|
|
if (filtered.length === 0) {
|
|
container.innerHTML = '<div class="no-results"><div class="no-results-icon">🔍</div>No files</div>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = `<div class="list-view active">${filtered.map((f,i)=>renderFileItem(f, f.category, i, true)).join('')}</div>`;
|
|
container.querySelectorAll('.file-item').forEach(fi => {
|
|
fi.addEventListener('click', () => {
|
|
const path = fi.dataset.path;
|
|
if (path) downloadFile(path);
|
|
});
|
|
});
|
|
}
|
|
|
|
function renderFileItem(file, category, index = 0, showPath = false){
|
|
const icons = { ssh: '🔐', sql: '🗄️', smb: '🌐', other: '📄' };
|
|
return `<div class="file-item" data-path="${file.path}" style="animation-delay:${index*0.02}s">
|
|
<div class="file-icon ${category}">${icons[category]}</div>
|
|
<div class="file-name">${file.name}${showPath ? ` <span style="color:var(--_muted);font-size:0.75rem">— ${file.fullPath}</span>` : ''}</div>
|
|
<span class="file-type ${category}">${category}</span>
|
|
</div>`;
|
|
}
|
|
|
|
function downloadFile(path){
|
|
window.location.href = `/loot_download?path=${encodeURIComponent(path)}`;
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|