mirror of
https://github.com/infinition/Bjorn.git
synced 2025-12-12 15:44:58 +00:00
759 lines
33 KiB
HTML
759 lines
33 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 - Files Explorer</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="#333" />
|
|
<script src="web/js/global.js"></script>
|
|
|
|
<!-- Integrated CSS (theming only; no logic changes) -->
|
|
<style>
|
|
/* ========= bridge variables to global.css tokens ========= */
|
|
:root{
|
|
--_bg: var(--bg, #0b0c0f);
|
|
--_panel: var(--c-panel-2, rgba(16,22,22,.6));
|
|
--_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));
|
|
}
|
|
|
|
body{ background:var(--_bg); color:var(--_ink); }
|
|
.main{ padding:0 !important; }
|
|
|
|
/* ===== Layout ===== */
|
|
.loot-container{
|
|
display:flex; flex-direction:column;
|
|
height:calc(100vh - 120px);
|
|
padding:12px;
|
|
gap:12px;
|
|
}
|
|
|
|
.file-explorer{
|
|
flex:1; display:flex; flex-direction:column; overflow:hidden; padding:10px;
|
|
color:var(--_ink);
|
|
background: color-mix(in oklab, var(--_panel) 92%, transparent);
|
|
border:1px solid var(--_border);
|
|
border-radius:14px;
|
|
backdrop-filter: blur(18px);
|
|
box-shadow: var(--_shadow);
|
|
}
|
|
|
|
/* ===== View containers ===== */
|
|
.files-grid{
|
|
overflow-y:auto;
|
|
display:grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
|
|
gap:8px; padding:8px; border-radius:8px;
|
|
}
|
|
.files-list{ overflow-y:auto; padding:4px; }
|
|
|
|
/* ===== Upload section ===== */
|
|
.upload-container{
|
|
padding:10px; margin-bottom:10px;
|
|
display:flex; justify-content:center; align-items:center;
|
|
}
|
|
.drop-zone{
|
|
width:100%; max-width:800px;
|
|
padding:16px;
|
|
border:2px dashed var(--_border);
|
|
border-radius:12px; text-align:center; font-size:14px;
|
|
color: var(--_muted);
|
|
cursor:pointer; transition:.25s ease;
|
|
background: color-mix(in oklab, var(--_panel) 88%, transparent);
|
|
backdrop-filter: blur(8px);
|
|
}
|
|
.drop-zone:hover{ background: color-mix(in oklab, var(--_panel) 96%, transparent); }
|
|
.drop-zone.dragover{
|
|
border-color: color-mix(in oklab, var(--_acid) 50%, var(--_border));
|
|
background: color-mix(in oklab, var(--_acid) 12%, var(--_panel));
|
|
color: var(--_ink);
|
|
}
|
|
|
|
/* ===== Items (grid & list) ===== */
|
|
.grid-item, .list-item{
|
|
border-radius:10px; padding:8px; cursor:pointer; transition:.15s ease;
|
|
display:flex; align-items:center; position:relative;
|
|
border:1px solid transparent;
|
|
background: color-mix(in oklab, var(--_panel) 86%, transparent);
|
|
}
|
|
.grid-item{ flex-direction:column; text-align:center; }
|
|
.list-item{ flex-direction:row; gap:12px; }
|
|
|
|
.grid-item:hover, .list-item:hover{
|
|
transform: translateY(-2px);
|
|
border-color: color-mix(in oklab, var(--_acid2) 35%, var(--_border));
|
|
box-shadow: 0 4px 14px rgba(0,0,0,.25);
|
|
background: color-mix(in oklab, var(--_panel) 96%, transparent);
|
|
}
|
|
|
|
.grid-item img, .list-item img{ width:28px; height:28px; margin-bottom:4px; }
|
|
.list-item img{ margin-bottom:0; }
|
|
|
|
.item-name{
|
|
color:var(--_ink); font-size:14px; line-height:1.3;
|
|
word-break: break-word; pointer-events:none;
|
|
}
|
|
.folder .item-name{ color:var(--_ink); font-weight:700; }
|
|
|
|
.item-meta{
|
|
font-size:11px; color:var(--_muted); margin-top:4px; pointer-events:none;
|
|
}
|
|
|
|
/* selected state */
|
|
.multi-select-mode{ background: color-mix(in oklab, var(--_acid) 6%, transparent); }
|
|
.item-selected{
|
|
background: color-mix(in oklab, var(--_acid) 18%, var(--_panel)) !important;
|
|
border:2px solid color-mix(in oklab, var(--_acid) 55%, var(--_border)) !important;
|
|
}
|
|
|
|
/* ===== Context menu ===== */
|
|
.context-menu{
|
|
position:absolute; z-index:1000;
|
|
background: color-mix(in oklab, var(--_panel) 98%, transparent);
|
|
border:1px solid var(--_border);
|
|
border-radius:10px; padding:6px 8px; min-width:160px;
|
|
color:var(--_ink); box-shadow: var(--_shadow);
|
|
}
|
|
.context-menu > div{
|
|
padding:8px 10px; border-radius:8px; cursor:pointer;
|
|
}
|
|
.context-menu > div:hover{
|
|
background: color-mix(in oklab, var(--_acid2) 12%, transparent);
|
|
}
|
|
|
|
/* ===== Search ===== */
|
|
.search-container{
|
|
position:relative; margin-bottom:10px; display:flex; align-items:center;
|
|
}
|
|
.search-input{
|
|
width:100%; padding:10px 40px 10px 12px; font-size:14px;
|
|
border-radius:10px; border:1px solid var(--_border);
|
|
background: color-mix(in oklab, var(--_panel) 90%, transparent);
|
|
color:var(--_ink); box-sizing:border-box; transition:.2s;
|
|
}
|
|
.search-input:focus{
|
|
outline:none;
|
|
border-color: color-mix(in oklab, var(--_acid2) 35%, 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-input::placeholder{ color: color-mix(in oklab, var(--_muted) 70%, transparent); }
|
|
.clear-button{
|
|
position:absolute; right:12px; background:none; border:none;
|
|
color: color-mix(in oklab, var(--_acid) 55%, var(--_ink));
|
|
font-size:16px; cursor:pointer; display:none;
|
|
}
|
|
.clear-button.show{ display:block; }
|
|
|
|
/* ===== Toolbar ===== */
|
|
.toolbar-buttons{ display:flex; gap:8px; margin-bottom:10px; flex-wrap:wrap; }
|
|
.action-button{
|
|
background: color-mix(in oklab, var(--_panel) 90%, transparent);
|
|
border:1px solid var(--_border);
|
|
color: var(--_muted);
|
|
padding:8px 10px; border-radius:10px; cursor:pointer; font-size:14px; font-weight:700;
|
|
display:flex; align-items:center; gap:6px; transition:.2s; backdrop-filter: blur(10px);
|
|
}
|
|
.action-button:hover{
|
|
background: color-mix(in oklab, var(--_panel) 96%, transparent);
|
|
color: var(--_ink); transform: translateY(-2px);
|
|
}
|
|
.action-button.active{
|
|
background: linear-gradient(135deg, color-mix(in oklab, var(--_acid) 18%, transparent), color-mix(in oklab, var(--_acid2) 10%, transparent));
|
|
color: var(--_ink);
|
|
border-color: color-mix(in oklab, var(--_acid2) 28%, var(--_border));
|
|
}
|
|
.action-button.delete{
|
|
background: color-mix(in oklab, var(--_acid) 14%, var(--_panel));
|
|
color: var(--_ink); display:none; border-color: color-mix(in oklab, var(--_acid) 40%, var(--_border));
|
|
}
|
|
.action-button.delete.show{ display:flex; }
|
|
|
|
/* ===== Modal ===== */
|
|
.modal{
|
|
display:block; position:fixed; inset:0; z-index:1000;
|
|
background: rgba(0,0,0,.5);
|
|
}
|
|
.modal-content{
|
|
background: color-mix(in oklab, var(--_panel) 98%, transparent);
|
|
color: var(--_ink);
|
|
margin: 12vh auto; padding:20px; width: min(500px, 92vw);
|
|
border:1px solid var(--_border); border-radius:14px; box-shadow: var(--_shadow);
|
|
}
|
|
.modal-buttons{ margin-top:18px; text-align:right; display:flex; gap:8px; justify-content:flex-end; }
|
|
.modal-buttons button{
|
|
margin-left:0; padding:8px 14px; border-radius:10px; border:1px solid var(--_border);
|
|
cursor:pointer; background: color-mix(in oklab, var(--_panel) 92%, transparent); color:var(--_ink);
|
|
}
|
|
.modal-buttons button:hover{ background: color-mix(in oklab, var(--_panel) 98%, transparent); }
|
|
.modal-buttons .primary{
|
|
background: linear-gradient(135deg, color-mix(in oklab, var(--_acid) 18%, transparent), color-mix(in oklab, var(--_acid2) 10%, transparent));
|
|
border-color: color-mix(in oklab, var(--_acid2) 35%, var(--_border));
|
|
color: var(--_ink);
|
|
}
|
|
|
|
#folder-tree{
|
|
border:1px solid var(--_border);
|
|
border-radius:10px; padding:8px; margin:10px 0; max-height:320px; overflow-y:auto;
|
|
background: color-mix(in oklab, var(--_panel) 92%, transparent);
|
|
}
|
|
.folder-item{
|
|
padding:8px 10px; cursor:pointer; display:flex; align-items:center; gap:8px; border-radius:8px;
|
|
}
|
|
.folder-item:hover{ background: color-mix(in oklab, var(--_panel) 98%, transparent); }
|
|
.folder-item.selected{
|
|
background: color-mix(in oklab, var(--_acid2) 16%, transparent);
|
|
outline: 1px solid color-mix(in oklab, var(--_acid2) 35%, var(--_border));
|
|
}
|
|
.folder-item i{ color: var(--_muted); }
|
|
|
|
/* ===== Path navigator ===== */
|
|
.path-navigator{
|
|
padding:8px; margin-bottom:8px; border-radius:10px;
|
|
display:flex; align-items:center; gap:8px;
|
|
background: color-mix(in oklab, var(--_panel) 90%, transparent);
|
|
border:1px solid var(--_border);
|
|
}
|
|
.nav-buttons{ display:flex; gap:8px; }
|
|
.back-button{
|
|
background: color-mix(in oklab, var(--_panel) 92%, transparent);
|
|
border:1px solid var(--_border);
|
|
color: var(--_muted);
|
|
padding:8px 12px; border-radius:10px; cursor:pointer; font-weight:700;
|
|
display:flex; align-items:center; gap:6px; min-width:40px; min-height:40px; justify-content:center;
|
|
transition:.2s;
|
|
}
|
|
.back-button:hover{ background: color-mix(in oklab, var(--_panel) 98%, transparent); color: var(--_ink); }
|
|
|
|
.current-path{ display:flex; align-items:center; gap:6px; overflow:hidden; flex-wrap:wrap; }
|
|
.path-segment{
|
|
background: linear-gradient(135deg, color-mix(in oklab, var(--_acid) 16%, transparent), color-mix(in oklab, var(--_acid2) 10%, transparent));
|
|
color: var(--_ink); padding:6px 10px; border-radius:10px; cursor:pointer; transition:.2s;
|
|
border:1px solid color-mix(in oklab, var(--_acid2) 28%, var(--_border));
|
|
}
|
|
.path-segment:hover{ filter: brightness(1.08); }
|
|
|
|
/* ===== Responsive ===== */
|
|
@media (max-width:420px){
|
|
.loot-container{ height:80vh; }
|
|
.file-explorer{ max-height:40vh; }
|
|
.files-grid{ max-height:40vh; }
|
|
.drop-zone{ padding:18px; font-size:15px; }
|
|
.toolbar-buttons{ padding:4px; gap:6px; }
|
|
.search-container, .path-navigator{ padding:4px; }
|
|
.grid-item{ min-height:74px; font-size:12px; }
|
|
.item-name{ font-size:13px; margin-top:2px; }
|
|
.item-meta{ font-size:10px; margin-top:2px; }
|
|
.grid-item img, .list-item img{ width:28px; height:28px; }
|
|
}
|
|
@media (max-width:768px){
|
|
.files-grid{ grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); gap:8px; }
|
|
#file-list{ max-height:fit-content; overflow-y:auto; }
|
|
.toolbar-buttons{ flex-direction:row; flex-wrap:wrap; gap:8px; }
|
|
.files-list{ padding:8px; max-height:50vh; overflow-y:auto; }
|
|
.grid-item{ padding:8px; }
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<div class="main" id="main">
|
|
<div class="loot-container">
|
|
<!-- File Explorer Section -->
|
|
<div class="file-explorer">
|
|
<div class="toolbar-buttons">
|
|
<button class="action-button" onclick="toggleView()">
|
|
<i class="fas fa-th-list"></i>
|
|
</button>
|
|
<button class="action-button" id="multiSelectBtn" onclick="toggleMultiSelect()">
|
|
<i class="fas fa-object-group"></i>Select
|
|
</button>
|
|
<button class="action-button" id="newFolderBtn" onclick="createNewFolder()">
|
|
<i class="fas fa-folder-plus"></i> New Folder
|
|
</button>
|
|
<button class="action-button" onclick="renameSelected()" id="renameBtn" style="display: none;">
|
|
<i class="fas fa-edit"></i> Rename
|
|
</button>
|
|
<button class="action-button" onclick="moveSelected()" id="moveBtn" disabled>
|
|
<i class="fas fa-arrows-alt"></i> Move
|
|
</button>
|
|
<button class="action-button delete" id="deleteBtn" onclick="deleteSelectedItems()" disabled>
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="search-container">
|
|
<input type="text" class="search-input" id="search-input" placeholder="Search files..." oninput="filterFiles()" />
|
|
<button class="clear-button" id="clear-button" onclick="clearSearch()">✖</button>
|
|
</div>
|
|
|
|
<div class="path-navigator">
|
|
<div class="nav-buttons">
|
|
<button class="back-button" onclick="navigateUp()" title="Go to parent directory">
|
|
← Back
|
|
</button>
|
|
</div>
|
|
<div class="current-path" id="currentPath"></div>
|
|
</div>
|
|
|
|
<div class="files-grid" id="file-list"></div>
|
|
</div>
|
|
|
|
<!-- Upload Container Fixed at Bottom -->
|
|
<div class="upload-container">
|
|
<input id="file-upload" type="file" multiple style="display: none;" onchange="handle_file_upload(event)" />
|
|
<div id="drop-zone" class="drop-zone" onclick="document.getElementById('file-upload').click()">
|
|
Drag files or folders here or click to upload
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Embedded JavaScript (UNCHANGED) -->
|
|
<script>
|
|
let currentPath = [];
|
|
let fontSize = 14;
|
|
let allFiles = [];
|
|
let isGridView = true; // Grid view by default
|
|
|
|
function formatBytes(bytes, decimals = 1) {
|
|
if (!bytes) return '0 Bytes';
|
|
const k = 1024; const dm = decimals < 0 ? 0 : decimals;
|
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
|
}
|
|
function toggleView() { isGridView = !isGridView; loadCurrentFolder(); }
|
|
function navigateUp() { if (currentPath.length > 0) { currentPath.pop(); loadCurrentFolder(); } }
|
|
function navigateToFolder(folderName) { currentPath.push(folderName); loadCurrentFolder(); }
|
|
|
|
function loadAllFiles() {
|
|
fetch('/list_files')
|
|
.then(r => r.json())
|
|
.then(data => { allFiles = data; loadCurrentFolder(); })
|
|
.catch(err => console.error('Error fetching files:', err));
|
|
}
|
|
function loadCurrentFolder() {
|
|
const currentContent = findFolderContents(allFiles, currentPath);
|
|
displayFiles(currentContent);
|
|
updateCurrentPathDisplay();
|
|
const v = document.getElementById('search-input').value.toLowerCase();
|
|
if (v) { filterFiles(); }
|
|
}
|
|
function findFolderContents(data, path) {
|
|
if (path.length === 0) return data;
|
|
let current = data;
|
|
for (let folder of path) {
|
|
const found = current.find(item => item.is_directory && item.name === folder);
|
|
if (!found || !found.children) return [];
|
|
current = found.children;
|
|
}
|
|
return current;
|
|
}
|
|
function displayFiles(currentContent) {
|
|
const container = document.getElementById('file-list');
|
|
container.innerHTML = '';
|
|
container.className = isGridView ? 'files-grid' : 'files-list';
|
|
|
|
const sorted = [...currentContent].sort((a,b)=>{
|
|
if (a.is_directory && !b.is_directory) return -1;
|
|
if (!a.is_directory && b.is_directory) return 1;
|
|
return a.name.localeCompare(b.name);
|
|
});
|
|
|
|
sorted.forEach(item => {
|
|
const el = document.createElement('div');
|
|
el.className = (isGridView ? 'grid-item ' : 'list-item ') + (item.is_directory ? 'folder' : 'file');
|
|
|
|
item.path = buildCompletePath(item.name);
|
|
el.dataset.path = item.path;
|
|
if (selectedItems.has(item.path)) el.classList.add('item-selected');
|
|
|
|
el.innerHTML = `
|
|
<img src="/web/images/${item.is_directory ? 'mainfolder' : 'file'}.png" alt="${item.is_directory ? 'Folder' : 'File'}">
|
|
<div>
|
|
<div class="item-name">${item.name}</div>
|
|
<div class="item-meta">${item.is_directory ? 'Folder' : formatBytes(item.size || 0)}</div>
|
|
</div>
|
|
`;
|
|
|
|
el.addEventListener('click', (e)=>{
|
|
e.preventDefault();
|
|
if (isMultiSelectMode) {
|
|
toggleItemSelection(el, item);
|
|
} else {
|
|
if (item.is_directory) navigateToFolder(item.name);
|
|
else window.location.href = `/download_file?path=${encodeURIComponent(item.path)}`;
|
|
}
|
|
});
|
|
|
|
el.addEventListener('contextmenu', (e)=>{ e.preventDefault(); showContextMenu(e, item); });
|
|
container.appendChild(el);
|
|
});
|
|
|
|
updateSelectionCount();
|
|
updateButtonStates();
|
|
}
|
|
function navigateToRoot(){ currentPath = []; loadCurrentFolder(); }
|
|
|
|
function showMultiContextMenu(event){
|
|
const existing = document.querySelector('.context-menu'); if (existing) existing.remove();
|
|
const menu = document.createElement('div');
|
|
menu.className = 'context-menu';
|
|
menu.style.top = `${event.clientY}px`; menu.style.left = `${event.clientX}px`;
|
|
const del = document.createElement('div');
|
|
del.textContent = `Delete (${selectedItems.size} items)`;
|
|
del.onclick = ()=>{ deleteMultipleItems(Array.from(selectedItems)); menu.remove(); };
|
|
menu.appendChild(del); document.body.appendChild(menu);
|
|
document.addEventListener('click', ()=>{ if (menu.parentElement) menu.remove(); }, { once:true });
|
|
}
|
|
|
|
async function deleteSelectedItems(){
|
|
if (selectedItems.size === 0) return;
|
|
if (confirm(`Are you sure you want to delete ${selectedItems.size} items?`)){
|
|
const items = Array.from(selectedItems.keys());
|
|
for (const path of items) {
|
|
try{
|
|
const r = await fetch('/delete_file', {
|
|
method:'POST', headers:{'Content-Type':'application/json'},
|
|
body: JSON.stringify({ file_path: path })
|
|
});
|
|
if (!r.ok) console.error(`Failed to delete ${path}`);
|
|
} catch(err){ console.error(`Error deleting ${path}:`, err); }
|
|
}
|
|
await loadAllFiles(); clearSelection(); toggleMultiSelect();
|
|
}
|
|
}
|
|
|
|
function updateCurrentPathDisplay(){
|
|
const wrap = document.getElementById('currentPath'); wrap.innerHTML='';
|
|
const root = document.createElement('span'); root.className='path-segment'; root.textContent='/'; root.onclick = ()=>navigateToRoot(); wrap.appendChild(root);
|
|
currentPath.forEach((folder, idx)=>{
|
|
const seg = document.createElement('span'); seg.className='path-segment'; seg.textContent=folder;
|
|
seg.onclick = ()=>{ currentPath = currentPath.slice(0, idx+1); loadCurrentFolder(); };
|
|
wrap.appendChild(seg);
|
|
});
|
|
}
|
|
|
|
function buildCompletePath(fileName){
|
|
const basePath = '/home/bjorn/';
|
|
return basePath + currentPath.join('/') + '/' + fileName;
|
|
}
|
|
|
|
function deleteFileOrFolder(item){
|
|
if (!item || !item.path){ console.error('Invalid item or missing path:', item); alert('Error: Invalid file information'); return; }
|
|
if (confirm(`Are you sure you want to delete "${item.name}"?`)){
|
|
fetch('/delete_file', {
|
|
method:'POST', headers:{'Content-Type':'application/json'},
|
|
body: JSON.stringify({ file_path: item.path })
|
|
})
|
|
.then(r=>{ if (!r.ok) return r.json().then(d=>{ throw new Error(d.message || 'Failed to delete the item.');}); return r.json(); })
|
|
.then(d=>{ if (d.status==='success') loadAllFiles(); else throw new Error(d.message || 'Unknown error occurred'); })
|
|
.catch(err=>{ console.error('Error in delete operation:', err); alert(`An error occurred: ${err.message}`); });
|
|
}
|
|
}
|
|
|
|
function showContextMenu(event, item){
|
|
event.preventDefault();
|
|
const existing = document.querySelector('.context-menu'); if (existing) existing.remove();
|
|
|
|
const menu = document.createElement('div');
|
|
menu.className = 'context-menu';
|
|
menu.style.top = `${event.clientY}px`; menu.style.left = `${event.clientX}px`;
|
|
|
|
const rename = document.createElement('div'); rename.textContent='Rename';
|
|
rename.onclick = ()=>{ renameItem(item); menu.remove(); };
|
|
const dup = document.createElement('div'); dup.textContent='Duplicate';
|
|
dup.onclick = ()=>{ duplicateItem(item); menu.remove(); };
|
|
const move = document.createElement('div'); move.textContent='Move to...';
|
|
move.onclick = ()=>{ showMoveToDialog(item); menu.remove(); };
|
|
const del = document.createElement('div'); del.textContent='Delete';
|
|
del.onclick = ()=>{ deleteFileOrFolder({ name:item.name, path:buildCompletePath(item.name), is_directory:item.is_directory }); menu.remove(); };
|
|
|
|
[rename, dup, move, del].forEach(x=>menu.appendChild(x));
|
|
document.body.appendChild(menu);
|
|
document.addEventListener('click', ()=>{ if (menu.parentElement) menu.remove(); }, { once:true });
|
|
}
|
|
|
|
function filterFiles(){
|
|
const v = document.getElementById('search-input').value.toLowerCase();
|
|
const clear = document.getElementById('clear-button');
|
|
if (v.length > 0){
|
|
clear.classList.add('show');
|
|
displayFiles( filterAllFiles(allFiles, v) );
|
|
} else {
|
|
clear.classList.remove('show');
|
|
loadCurrentFolder();
|
|
}
|
|
}
|
|
function filterAllFiles(files, v){
|
|
let res = [];
|
|
files.forEach(it=>{
|
|
if (it.name.toLowerCase().includes(v)) res.push(it);
|
|
if (it.is_directory && it.children) res = res.concat(filterAllFiles(it.children, v));
|
|
});
|
|
return res;
|
|
}
|
|
function clearSearch(){ const i = document.getElementById('search-input'); i.value=''; filterFiles(); }
|
|
|
|
document.addEventListener('DOMContentLoaded', ()=>{
|
|
const fi = document.getElementById('file-upload');
|
|
fi.removeAttribute('webkitdirectory'); fi.removeAttribute('directory'); fi.removeAttribute('mozdirectory'); fi.setAttribute('multiple','');
|
|
loadAllFiles();
|
|
if (/Mobi|Android/i.test(navigator.userAgent)){ fontSize = 12; adjustFontSize(0); }
|
|
const filesGrid = document.querySelector('.files-grid'); if (filesGrid){ filesGrid.addEventListener('contextmenu', showEmptySpaceContextMenu); }
|
|
});
|
|
|
|
const dropZone = document.getElementById('drop-zone');
|
|
dropZone.addEventListener('dragover', (e)=>{ e.preventDefault(); dropZone.classList.add('dragover'); });
|
|
dropZone.addEventListener('dragleave', ()=>{ dropZone.classList.remove('dragover'); });
|
|
dropZone.addEventListener('drop', (e)=>{
|
|
e.preventDefault(); dropZone.classList.remove('dragover');
|
|
const items = e.dataTransfer.items;
|
|
if (items){ handleDirectoryUpload(items); } else { handleFiles(e.dataTransfer.files); }
|
|
});
|
|
|
|
function handle_file_upload(e){ handleFiles(e.target.files); }
|
|
function handleFiles(files){
|
|
const formData = new FormData();
|
|
Array.from(files).forEach(file=>{
|
|
const rel = file.webkitRelativePath || file.name;
|
|
formData.append('files[]', file, rel);
|
|
});
|
|
formData.append('currentPath', JSON.stringify(currentPath));
|
|
fetch('/upload_files', { method:'POST', body: formData })
|
|
.then(r=>r.json())
|
|
.then(()=>{ loadAllFiles(); })
|
|
.catch(err=>{ alert('Error uploading files: ' + err.message); });
|
|
}
|
|
|
|
let isMultiSelectMode = false;
|
|
const selectedItems = new Map();
|
|
function toggleMultiSelect(){
|
|
isMultiSelectMode = !isMultiSelectMode;
|
|
const container = document.querySelector('.file-explorer');
|
|
const btn = document.querySelector('#multiSelectBtn');
|
|
if (isMultiSelectMode){ container.classList.add('multi-select-mode'); btn.classList.add('active'); }
|
|
else { container.classList.remove('multi-select-mode'); btn.classList.remove('active'); clearSelection(); }
|
|
updateButtonStates();
|
|
}
|
|
function updateSelectionCount(){
|
|
const del = document.getElementById('deleteBtn');
|
|
const n = selectedItems.size;
|
|
del.innerHTML = n>0 ? `<i class="fas fa-trash"></i> ${n}` : `<i class="fas fa-trash"></i>`;
|
|
}
|
|
function clearSelection(){
|
|
selectedItems.clear();
|
|
document.querySelectorAll('.grid-item, .list-item').forEach(i=>i.classList.remove('item-selected'));
|
|
updateButtonStates();
|
|
}
|
|
function toggleItemSelection(el, item){
|
|
if (!isMultiSelectMode) return;
|
|
const p = item.path;
|
|
if (selectedItems.has(p)){ selectedItems.delete(p); el.classList.remove('item-selected'); }
|
|
else { selectedItems.set(p, item); el.classList.add('item-selected'); }
|
|
updateButtonStates();
|
|
}
|
|
|
|
async function handleDirectoryUpload(items){
|
|
const files = []; const entries = Array.from(items).map(i=>i.webkitGetAsEntry());
|
|
async function traverseEntry(entry, path=''){
|
|
if (entry.isFile){
|
|
const file = await new Promise(res=>entry.file(res));
|
|
Object.defineProperty(file, 'webkitRelativePath', { value: path + entry.name });
|
|
files.push(file);
|
|
} else if (entry.isDirectory){
|
|
const reader = entry.createReader();
|
|
const kids = await new Promise(res=>{
|
|
const acc=[]; (function read(){ reader.readEntries((ents)=>{ if(ents.length){ acc.push(...ents); read(); } else { res(acc); } }); })();
|
|
});
|
|
const next = path + entry.name + '/';
|
|
for (const k of kids){ await traverseEntry(k, next); }
|
|
}
|
|
}
|
|
await Promise.all(entries.map(e=>traverseEntry(e)));
|
|
handleFiles(files);
|
|
}
|
|
|
|
function showEmptySpaceContextMenu(event){
|
|
if (event.target.classList.contains('files-grid')){
|
|
event.preventDefault();
|
|
const existing = document.querySelector('.context-menu'); if (existing) existing.remove();
|
|
const menu = document.createElement('div'); menu.className='context-menu';
|
|
menu.style.top = `${event.clientY}px`; menu.style.left = `${event.clientX}px`;
|
|
const nf = document.createElement('div'); nf.textContent='New Folder'; nf.onclick = ()=>{ createNewFolder(); menu.remove(); };
|
|
menu.appendChild(nf); document.body.appendChild(menu);
|
|
document.addEventListener('click', ()=>{ if (menu.parentElement) menu.remove(); }, { once:true });
|
|
}
|
|
}
|
|
|
|
async function renameItem(item){
|
|
const newName = prompt(`Rename ${item.is_directory ? 'folder' : 'file'} "${item.name}" to:`, item.name);
|
|
if (newName && newName !== item.name){
|
|
const oldPath = buildCompletePath(item.name);
|
|
const newPath = buildCompletePath(newName);
|
|
try{
|
|
const r = await fetch('/rename_file', {
|
|
method:'POST', headers:{'Content-Type':'application/json'},
|
|
body: JSON.stringify({ old_path: oldPath, new_path: newPath })
|
|
});
|
|
if (!r.ok) throw new Error(await r.text());
|
|
loadAllFiles();
|
|
} catch(err){ alert(`Error renaming item: ${err.message}`); }
|
|
}
|
|
}
|
|
|
|
async function duplicateItem(item){
|
|
const base = item.name;
|
|
const ext = item.is_directory ? '' : (base.includes('.') ? '.' + base.split('.').pop() : '');
|
|
const nameNoExt = item.is_directory ? base : base.split('.')[0];
|
|
const newName = `${nameNoExt} (copy)${ext}`;
|
|
try{
|
|
const sourcePath = buildCompletePath(item.name);
|
|
const targetPath = buildCompletePath(newName);
|
|
const r = await fetch('/duplicate_file', {
|
|
method:'POST', headers:{'Content-Type':'application/json'},
|
|
body: JSON.stringify({ source_path: sourcePath, target_path: targetPath })
|
|
});
|
|
if (!r.ok) throw new Error(await r.text());
|
|
loadAllFiles();
|
|
} catch(err){ alert(`Error duplicating item: ${err.message}`); }
|
|
}
|
|
|
|
async function createNewFolder(){
|
|
const folderName = prompt('Enter new folder name:', 'New Folder');
|
|
if (folderName){
|
|
try{
|
|
const folderPath = buildCompletePath(folderName);
|
|
const r = await fetch('/create_folder', {
|
|
method:'POST', headers:{'Content-Type':'application/json'},
|
|
body: JSON.stringify({ folder_path: folderPath })
|
|
});
|
|
if (!r.ok) throw new Error(await r.text());
|
|
loadAllFiles();
|
|
} catch(err){ alert(`Error creating folder: ${err.message}`); }
|
|
}
|
|
}
|
|
|
|
async function showMoveToDialog(items){
|
|
const itemsArr = Array.isArray(items) ? items : [items];
|
|
const modal = document.createElement('div');
|
|
modal.className='modal';
|
|
modal.innerHTML = `
|
|
<div class="modal-content">
|
|
<h2>Move ${itemsArr.length} ${itemsArr.length>1 ? 'items' : 'item'} to...</h2>
|
|
<div id="folder-tree"></div>
|
|
<div class="modal-buttons">
|
|
<button class="action-button" id="cancelButton"><i class="fas fa-times"></i> Cancel</button>
|
|
<button class="action-button primary" id="moveConfirmButton"><i class="fas fa-check"></i> Move</button>
|
|
</div>
|
|
</div>`;
|
|
document.body.appendChild(modal);
|
|
document.getElementById('cancelButton').addEventListener('click', closeModal);
|
|
document.getElementById('moveConfirmButton').addEventListener('click', ()=>{ processMove(itemsArr); });
|
|
modal.addEventListener('click', (e)=>{ if (e.target === modal) closeModal(); });
|
|
await loadFolderTree();
|
|
}
|
|
|
|
async function loadFolderTree(){
|
|
try{
|
|
const r = await fetch('/list_directories'); if (!r.ok) throw new Error('Failed to load directory structure');
|
|
const dirs = await r.json();
|
|
const tree = document.getElementById('folder-tree');
|
|
tree.innerHTML = buildFolderTreeHTML(dirs);
|
|
addFolderTreeListeners();
|
|
} catch(err){ alert('Error loading folder structure: ' + err.message); }
|
|
}
|
|
|
|
function buildFolderTreeHTML(directories, level=0){
|
|
let html=''; const pad = level * 20;
|
|
directories.forEach(dir=>{
|
|
if (dir.is_directory){
|
|
html += `<div class="folder-item" data-path="${dir.path}" style="padding-left:${pad}px"><i class="fas fa-folder"></i><span>${dir.name}</span></div>`;
|
|
if (dir.children && dir.children.length>0) html += buildFolderTreeHTML(dir.children, level+1);
|
|
}
|
|
});
|
|
return html;
|
|
}
|
|
|
|
let selectedTargetPath = null;
|
|
function addFolderTreeListeners(){
|
|
document.querySelectorAll('.folder-item').forEach(it=>{
|
|
it.addEventListener('click', (e)=>{
|
|
e.stopPropagation();
|
|
document.querySelectorAll('.folder-item.selected').forEach(x=>x.classList.remove('selected'));
|
|
it.classList.add('selected'); selectedTargetPath = it.dataset.path;
|
|
});
|
|
});
|
|
}
|
|
function closeModal(){ const m = document.querySelector('.modal'); if (m) m.remove(); selectedTargetPath = null; }
|
|
|
|
async function processMove(items){
|
|
if (!selectedTargetPath){ alert('Please select a destination folder'); return; }
|
|
const errors = []; const arr = Array.isArray(items)?items:[items];
|
|
for (const item of arr){
|
|
try{
|
|
const r = await fetch('/move_file', {
|
|
method:'POST', headers:{'Content-Type':'application/json'},
|
|
body: JSON.stringify({ source_path:item.path, target_path:`${selectedTargetPath}/${item.name}` })
|
|
});
|
|
if (!r.ok){ const e = await r.text(); errors.push(`Failed to move ${item.name}: ${e}`); }
|
|
} catch(err){ errors.push(`Error moving ${item.name}: ${err.message}`); }
|
|
}
|
|
if (errors.length>0) alert('Some errors occurred:\n' + errors.join('\n'));
|
|
closeModal(); loadAllFiles();
|
|
}
|
|
|
|
function renameSelected(){
|
|
const el = document.querySelector('.item-selected'); if (!el){ alert('Please select an item to rename'); return; }
|
|
const name = el.querySelector('.item-name').textContent; const path = el.dataset.path; const isDir = el.classList.contains('folder');
|
|
renameItem({ name, path, is_directory:isDir });
|
|
}
|
|
|
|
function moveSelected(){
|
|
const els = document.querySelectorAll('.item-selected');
|
|
if (els.length===0){ alert('Please select items to move'); return; }
|
|
const items = Array.from(els).map(el=>({ name: el.querySelector('.item-name').textContent, path: el.dataset.path, is_directory: el.classList.contains('folder') }));
|
|
showMoveToDialog(items);
|
|
}
|
|
|
|
function updateButtonStates(){
|
|
const n = selectedItems.size;
|
|
const renameBtn = document.getElementById('renameBtn');
|
|
const moveBtn = document.getElementById('moveBtn');
|
|
const deleteBtn = document.getElementById('deleteBtn');
|
|
const newFolderBtn = document.getElementById('newFolderBtn');
|
|
|
|
if (renameBtn){
|
|
if (isMultiSelectMode && n===1){ renameBtn.style.display='inline-block'; renameBtn.disabled=false; }
|
|
else { renameBtn.style.display='none'; renameBtn.disabled=true; }
|
|
}
|
|
if (moveBtn){
|
|
if (isMultiSelectMode && n>0){ moveBtn.style.display='inline-block'; moveBtn.disabled=false; }
|
|
else { moveBtn.style.display='none'; moveBtn.disabled=true; }
|
|
}
|
|
if (deleteBtn){
|
|
deleteBtn.style.display = isMultiSelectMode ? 'inline-block' : 'none';
|
|
deleteBtn.disabled = n===0;
|
|
deleteBtn.innerHTML = n>0 ? `<i class="fas fa-trash"></i> ${n}` : `<i class="fas fa-trash"></i>`;
|
|
}
|
|
if (newFolderBtn){ newFolderBtn.style.display = isMultiSelectMode ? 'none' : 'inline-block'; }
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|