SF映画っぽいエクスプローラ「Exp3D 1.5」
ダウンロードされる方はこちら。↓
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Exp3D 1.5</title>
<style>
:root {
--cyan: #00ffff;
--magenta: #ff00ff;
--orange: #ffaa00;
--yellow: #ffee00;
--blue: #0088ff;
--bg: #010103;
--panel: rgba(5, 10, 15, 0.95);
}
/* Body background provides the base darkness */
body { margin: 0; overflow: hidden; background-color: var(--bg); font-family: 'Segoe UI', monospace; user-select: none; color: #fff; }
/* App Layout (Split View) */
#app { display: flex; width: 100vw; height: 100vh; position: relative; overflow: hidden; }
/* Panes - MUST BE TRANSPARENT to see the canvas underneath */
#pane-tree {
flex: 0 0 25%; position: relative; overflow: hidden;
border-right: 1px solid #333;
background: transparent; /* Changed from opaque to transparent */
z-index: 10; pointer-events: none; /* Allow events to pass through to canvas logic if needed, but we handle via window events */
}
#pane-file {
flex: 1; position: relative; overflow: hidden;
background: transparent;
z-index: 10; pointer-events: none;
}
/* Interactive elements inside panes must re-enable pointer-events */
.pane-label, .sys-btn, .stock-item, .stock-tab-item, #stock, #stock-tabs-container {
pointer-events: auto;
}
/* Splitter Handle */
#splitter {
width: 6px; cursor: col-resize; background: #111; border-left: 1px solid #444; border-right: 1px solid #444;
display: flex; flex-direction: column; justify-content: center; align-items: center; z-index: 100;
transition: background 0.2s; pointer-events: auto;
}
#splitter:hover, #splitter.active { background: var(--orange); }
.grip { width: 2px; height: 4px; background: #666; margin: 2px 0; }
/* Canvas Container - Sitting behind the panes */
#canvas-wrap {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
z-index: 0; /* Behind everything */
pointer-events: auto; /* Canvas needs to receive events? We use window listener actually. */
}
canvas { display: block; outline: none; }
/* HUD & UI Overlays */
#hud { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 50; }
#hud-canvas { width: 100%; height: 100%; display: block; }
#top-bar {
position: absolute; top: 0; left: 0; width: 100%; height: 40px;
background: linear-gradient(to bottom, rgba(0,20,40,0.9), transparent);
display: flex; align-items: center; justify-content: space-between; padding: 0 20px;
z-index: 60; pointer-events: none;
}
.sys-title { font-size: 16px; font-weight: bold; color: var(--cyan); letter-spacing: 2px; pointer-events: auto; }
.sys-btn {
background: rgba(0,0,0,0.5); border: 1px solid var(--cyan); color: var(--cyan);
padding: 4px 12px; cursor: pointer; font-family: inherit; font-weight: bold; font-size: 12px;
pointer-events: auto; transition: 0.3s;
}
.sys-btn:hover { background: var(--cyan); color: #000; box-shadow: 0 0 10px var(--cyan); }
.pane-label {
position: absolute; bottom: 10px; left: 10px; font-size: 12px;
color: rgba(255,255,255,0.3); pointer-events: none; z-index: 10;
}
#mode-info {
position: absolute; top: 50px; right: 20px; text-align: right; pointer-events: none;
color: var(--yellow); text-shadow: 0 0 5px var(--yellow); font-weight: bold; font-size: 18px; z-index: 20;
}
/* Async Loader */
#loader {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.85); display: flex; flex-direction: column;
justify-content: center; align-items: center; z-index: 200;
display: none; pointer-events: auto;
}
#loader.active { display: flex; }
.loader-text { color: var(--cyan); font-size: 24px; margin-bottom: 20px; animation: blink 1s infinite; letter-spacing: 2px; }
@keyframes blink { 50% { opacity: 0.5; } }
/* STOCK PANEL */
#stock {
position: absolute; bottom: 0; left: 0; right: 0; height: 180px;
background: linear-gradient(to top, rgba(20,10,0,0.95), transparent);
border-top: 2px solid var(--orange); display: flex; align-items: center;
padding: 0 50px; gap: 40px;
z-index: 40; transition: bottom 0.3s; overflow-x: hidden;
cursor: grab; pointer-events: auto;
}
#stock.active { cursor: grabbing; }
#stock.closed { bottom: -180px; }
#stock-tabs-container {
position: absolute; bottom: 180px; left: 50%; transform: translateX(-50%);
display: flex; gap: 5px; z-index: 41; transition: bottom 0.3s; pointer-events: auto;
}
#stock.closed + #stock-tabs-container { bottom: 0; }
.stock-tab-item {
background: rgba(50, 20, 0, 0.8); color: #aaa;
padding: 2px 20px; font-weight: bold; font-size: 12px;
cursor: pointer; clip-path: polygon(10% 0, 90% 0, 100% 100%, 0% 100%);
border-bottom: none; transition: 0.2s; min-width: 60px; text-align: center;
border-top: 2px solid #553300; pointer-events: auto;
}
.stock-tab-item:hover { color: #fff; background: rgba(100, 50, 0, 0.8); }
.stock-tab-item.active { background: var(--orange); color: #000; border-top: 2px solid #fff; }
.stock-item {
width: 100px; height: 130px;
background: rgba(0,0,0,0.3); border: 1px solid var(--orange);
display: flex; flex-direction: column; align-items: center;
cursor: grab; color: var(--orange); flex-shrink: 0; position: relative;
transition: 0.2s; user-select: none; pointer-events: auto;
}
.stock-item:hover { background: rgba(255,170,0,0.1); box-shadow: 0 0 10px rgba(255,170,0,0.2); }
.stock-thumb { width: 90px; height: 90px; margin-top: 5px; object-fit: contain; background: #000; display:flex; justify-content:center; align-items:center; overflow:hidden; }
.stock-label { font-size: 11px; margin-top: 5px; width: 90px; text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-weight: bold; }
/* Context Menu */
.ctx-menu {
position: fixed; display: none; flex-direction: column;
background: rgba(10, 15, 20, 0.98); border: 1px solid var(--cyan);
box-shadow: 0 0 20px rgba(0,255,255,0.3); min-width: 200px; z-index: 9999;
transform-origin: top; padding: 5px 0; overflow: hidden; pointer-events: auto;
}
.ctx-menu.opening { animation: menuOpen 0.2s ease-out forwards; }
@keyframes menuOpen { from { transform: scaleY(0); opacity: 0; } to { transform: scaleY(1); opacity: 1; } }
.ctx-item { padding: 10px 15px; cursor: pointer; color: #ccc; display: flex; justify-content: space-between; border-bottom: 1px solid #333; transition: background 0.1s; }
.ctx-item:hover { background: var(--cyan); color: #000; }
.ctx-menu.closing .ctx-item { animation: itemFold 0.2s forwards ease-in; }
@keyframes itemFold { 0% { transform: translateY(0); opacity: 1; height: 40px; } 100% { transform: translateY(-20px); opacity: 0; height: 0; padding: 0; border: none; } }
/* Modals */
.modal-overlay {
position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8);
display: flex; justify-content: center; align-items: center; z-index: 300;
opacity: 0; pointer-events: none; visibility: hidden; transition: opacity 0.3s;
perspective: 1500px;
}
.modal-overlay.active { opacity: 1; pointer-events: auto; visibility: visible; }
.modal-box {
border: 1px solid var(--cyan); background: #000;
display: flex; flex-direction: column; box-shadow: 0 0 50px rgba(0,255,255,0.2);
transform-style: preserve-3d; opacity: 0; transform-origin: center;
}
.modal-overlay.opening .modal-box { animation: animFlipOpen 0.6s cubic-bezier(0.19, 1, 0.22, 1) forwards; }
.modal-overlay.closing .modal-box { animation: animFlipClose 0.5s cubic-bezier(0.19, 1, 0.22, 1) forwards; }
@keyframes animFlipOpen { 0% { transform: translateZ(-500px) rotateX(180deg); opacity: 0; } 100% { transform: translateZ(0) rotateX(0deg); opacity: 1; } }
@keyframes animFlipClose { 0% { transform: translateZ(0) rotateX(0deg); opacity: 1; } 100% { transform: translateZ(-500px) rotateX(-180deg); opacity: 0; } }
.modal-head { padding: 10px; background: rgba(0,255,255,0.15); color: var(--cyan); display: flex; justify-content: space-between; align-items: center; font-weight: bold; }
.modal-body { flex: 1; padding: 0; overflow: hidden; position: relative; background: #050505; display: flex; justify-content: center; align-items: center; }
.p-btn { background: transparent; border: 1px solid var(--cyan); color: var(--cyan); padding: 5px 15px; margin-left: 10px; cursor: pointer; transition: 0.2s; }
.p-btn:hover { background: var(--cyan); color: #000; }
.p-btn-save { display: none; border-color: #0f0; color: #0f0; }
#p-box { width: 90%; height: 90%; }
.hex-view { width: 100%; height: 100%; padding: 15px; color: #0f0; background: #000; border: none; resize: none; font-family: 'Courier New', monospace; outline: none; }
#d-box { width: 400px; height: 200px; }
#d-body { flex-direction: column; padding: 20px; gap: 20px; }
.d-input { width: 100%; background: #111; border: 1px solid #333; color: #fff; padding: 10px; font-family: 'Segoe UI', monospace; font-size: 16px; outline: none; text-align: center; }
.d-input:focus { border-color: var(--cyan); }
.d-actions { display: flex; gap: 10px; width: 100%; justify-content: center; }
.scanlines {
position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none;
background: linear-gradient(to bottom, rgba(255,255,255,0) 50%, rgba(0,0,0,0.1) 50%);
background-size: 100% 3px; z-index: 9000; opacity: 0.15;
}
</style>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.160.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
}
}
</script>
</head>
<body>
<div id="app">
<div id="canvas-wrap"></div>
<div id="pane-tree">
<div class="pane-label">SYSTEM // TREE</div>
</div>
<div id="splitter">
<div class="grip"></div><div class="grip"></div><div class="grip"></div>
</div>
<div id="pane-file">
<div class="pane-label">SYSTEM // OBJECTS</div>
<div id="hud"><canvas id="hud-canvas"></canvas></div>
<div id="stock" class="closed">
<div id="stock-hint" style="color:var(--orange); opacity:0.6; pointer-events:none;">[ EMPTY STOCK ]</div>
</div>
<div id="stock-tabs-container">
<div id="stock-tab-placeholder" class="stock-tab-item" style="opacity:0.5; pointer-events:none;">STOCK</div>
</div>
</div>
<div id="top-bar">
<div class="sys-title">EXP3D // DUAL_PANE</div>
<div>
<button id="btn-mount" class="sys-btn">MOUNT DRIVE</button>
</div>
</div>
<div id="mode-info">MODE: F1 (VERTICAL)</div>
<div id="loader">
<div class="loader-text">READING DATA...</div>
<button id="btn-cancel-load" class="p-btn" style="border-color:#ff5555; color:#ff5555;">CLOSE</button>
</div>
<div id="ctx-obj" class="ctx-menu">
<div class="ctx-item" id="cm-open">Open</div>
<div class="ctx-item" id="cm-copy">Copy</div>
<div class="ctx-item" id="cm-rename">Rename</div>
<div class="ctx-item" id="cm-add-stock" style="color:var(--orange)">Add to Stock</div>
<div class="ctx-item" id="cm-delete" style="color:#ff5555">Delete</div>
</div>
<div id="ctx-bg" class="ctx-menu">
<div class="ctx-item" id="cm-new">New Folder</div>
<div class="ctx-item" id="cm-paste">Paste</div>
<div class="ctx-item" id="cm-up">Up Directory</div>
<div class="ctx-item" id="cm-refresh">Refresh</div>
</div>
<div id="ctx-tab" class="ctx-menu">
<div class="ctx-item" id="cm-tab-hide" style="color:var(--orange)">Hide</div>
<div class="ctx-item" id="cm-tab-cancel" style="color:#ff5555">Cancel</div>
</div>
</div>
<div id="preview" class="modal-overlay">
<div id="p-box" class="modal-box">
<div class="modal-head">
<span id="p-title">FILE</span>
<div>
<button id="p-save" class="p-btn p-btn-save">SAVE</button>
<button id="p-close" class="p-btn">CLOSE</button>
</div>
</div>
<div id="p-body" class="modal-body"></div>
</div>
</div>
<div id="dialog" class="modal-overlay">
<div id="d-box" class="modal-box">
<div class="modal-head">
<span id="d-title">INPUT</span>
</div>
<div id="d-body" class="modal-body">
<input type="text" id="d-input" class="d-input" spellcheck="false" autocomplete="off">
<div class="d-actions">
<button id="d-ok" class="p-btn" style="border-color:var(--cyan); color:var(--cyan); width:100px;">OK</button>
<button id="d-cancel" class="p-btn" style="border-color:#ff5555; color:#ff5555; width:100px;">CANCEL</button>
</div>
</div>
</div>
</div>
<div class="scanlines"></div>
<script type="module">
import * as THREE from 'three';
// --- Config & State ---
const CONFIG = {
colFolder: 0x00ffff, colFile: 0x0088ff, colActive: 0xffaa00,
treeCamOffset: { x:0, y:5, z:40 }
};
const state = {
// Layout
splitPercent: 25,
draggingSplit: false,
activePane: 'none', // 'tree' or 'file'
// Data
rootHandle: null, currentHandle: null,
treeNodes: [], // Array of TreeNode objects
fileObjects: [], // Meshes in right pane
activeNode: null, // Currently selected Directory in Tree
// Stock & Clipboard
stockList: [], activeStockIdx: -1,
clipboard: null,
// View Modes (Right Pane)
viewMode: 'F1',
targetRotX: 0, currentRotX: 0,
targetRotY: 0, currentRotY: 0,
targetPosX: 0, currentPosX: 0,
targetPosY: 0, currentPosY: 0,
targetPosZ: 0, currentPosZ: 0,
// Tree Nav
treeScrollZ: 0, targetTreeScrollZ: 0,
treeScrollX: 0, targetTreeScrollX: 0,
// Interaction
mouse: new THREE.Vector2(),
lastMouse: new THREE.Vector2(),
hovered: null, selected: null,
dragging: null, dragStart: new THREE.Vector2(),
contextTarget: null, tabContextTargetIdx: -1,
isLoading: false
};
// --- Sound System ---
const sfx = {
ctx: null,
init: () => { if (!sfx.ctx) sfx.ctx = new (window.AudioContext || window.webkitAudioContext)(); },
play: (type) => {
if (!sfx.ctx) return;
const t = sfx.ctx.currentTime;
const o = sfx.ctx.createOscillator(); const g = sfx.ctx.createGain();
o.connect(g); g.connect(sfx.ctx.destination);
if(type==='click'){o.frequency.setValueAtTime(600,t);g.gain.exponentialRampToValueAtTime(0.001,t+0.1);o.start(t);o.stop(t+0.1);}
else if(type==='hover'){o.frequency.setValueAtTime(300,t);g.gain.setValueAtTime(0.05,t);g.gain.exponentialRampToValueAtTime(0.001,t+0.05);o.start(t);o.stop(t+0.05);}
else if(type==='open'){o.type='triangle';o.frequency.setValueAtTime(200,t);o.frequency.linearRampToValueAtTime(600,t+0.2);g.gain.exponentialRampToValueAtTime(0.001,t+0.2);o.start(t);o.stop(t+0.2);}
else if(type==='back'){o.type='triangle';o.frequency.setValueAtTime(600,t);o.frequency.linearRampToValueAtTime(200,t+0.2);g.gain.exponentialRampToValueAtTime(0.001,t+0.2);o.start(t);o.stop(t+0.2);}
else if(type==='lock'){o.type='square';o.frequency.setValueAtTime(1000,t);g.gain.setValueAtTime(0.05,t);g.gain.exponentialRampToValueAtTime(0.001,t+0.1);o.start(t);o.stop(t+0.1);}
else if(type==='error'){o.type='sawtooth';o.frequency.setValueAtTime(100,t);g.gain.exponentialRampToValueAtTime(0.001,t+0.3);o.start(t);o.stop(t+0.3);}
}
};
// --- Three.js Setup (Dual Scene, Single Renderer) ---
const container = document.getElementById('canvas-wrap');
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setScissorTest(true);
container.appendChild(renderer.domElement);
// Scene 1: TREE (Left)
const sceneTree = new THREE.Scene();
sceneTree.fog = new THREE.FogExp2(0x020205, 0.008);
const camTree = new THREE.PerspectiveCamera(60, 1, 0.1, 2000);
const groupTree = new THREE.Group(); sceneTree.add(groupTree);
// Scene 2: FILES (Right)
const sceneFile = new THREE.Scene();
sceneFile.fog = new THREE.FogExp2(0x020205, 0.015);
const camFile = new THREE.PerspectiveCamera(60, 1, 0.1, 1000);
camFile.position.set(0, 0, 0.1); // Camera stays static relative to group
const groupFile = new THREE.Group(); sceneFile.add(groupFile);
const reticleGroup = new THREE.Group(); sceneFile.add(reticleGroup);
reticleGroup.visible = false;
// Lights & Environment
[sceneTree, sceneFile].forEach(s => {
s.add(new THREE.AmbientLight(0xffffff, 1.0));
const dl = new THREE.DirectionalLight(0xffffff, 2);
dl.position.set(10, 50, 20); s.add(dl);
});
// Background Elements
function createStars() {
const g = new THREE.Group();
const starsGeo = new THREE.BufferGeometry();
const starPos = new Float32Array(5000 * 3);
for(let i=0; i<15000; i++) starPos[i] = (Math.random()-0.5)*800;
starsGeo.setAttribute('position', new THREE.BufferAttribute(starPos, 3));
const starMat = new THREE.PointsMaterial({color:0xffffff, size:0.5, transparent:true, opacity:0.8});
g.add(new THREE.Points(starsGeo, starMat));
return g;
}
sceneFile.add(createStars());
const gridHelperTree = new THREE.GridHelper(500, 100, 0x004488, 0x001122);
gridHelperTree.position.y = -20; sceneTree.add(gridHelperTree);
const gridHelperFile = new THREE.GridHelper(500, 100, 0x00ffff, 0x111111);
gridHelperFile.position.y = -15; gridHelperFile.visible = false; sceneFile.add(gridHelperFile);
// Reticle
function createSegmentedRing(rIn, rOut, count, speed, dir) {
const g = new THREE.Group();
const angle = (Math.PI * 2) / count;
const gap = 0.2;
for(let i=0; i<count; i++) {
const s = i * angle + gap/2;
const l = angle - gap;
const geo = new THREE.RingGeometry(rIn, rOut, 32, 1, s, l);
const color = new THREE.Color(0xffff00); color.multiplyScalar(1.5);
const mat = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: 1.0, side: THREE.DoubleSide, depthTest: false });
const mesh = new THREE.Mesh(geo, mat);
g.add(mesh);
}
g.userData = { speed: speed, dir: dir };
return g;
}
const r1 = createSegmentedRing(1.4, 1.45, 3, 0.08, 1); reticleGroup.add(r1);
const r2 = createSegmentedRing(1.55, 1.6, 4, 0.05, -1); reticleGroup.add(r2);
const r3 = createSegmentedRing(1.7, 1.75, 6, 0.02, 1); reticleGroup.add(r3);
reticleGroup.userData = { layers: [r1, r2, r3] };
// HUD Canvas
const hudC = document.getElementById('hud-canvas');
const hudX = hudC.getContext('2d');
function resizeRenderer() {
const width = window.innerWidth;
const height = window.innerHeight;
renderer.setSize(width, height);
const dpr = window.devicePixelRatio || 1;
hudC.width = width * dpr;
hudC.height = height * dpr;
hudX.scale(dpr, dpr);
}
window.addEventListener('resize', resizeRenderer);
resizeRenderer();
// --- Logic: Tree View ---
class TreeNode {
constructor(handle, parent = null) {
this.handle = handle;
this.parent = parent;
this.children = [];
this.mesh = null;
this.depth = parent ? parent.depth + 1 : 0;
this.expanded = false;
}
}
function createTreeMesh(node) {
const grp = new THREE.Group();
const geo = new THREE.BoxGeometry(3, 3, 3);
const mat = new THREE.MeshPhongMaterial({
color: CONFIG.colActive, emissive: 0x332200, wireframe: false,
transparent: true, opacity: 0.9, flatShading: true
});
const mesh = new THREE.Mesh(geo, mat);
grp.add(mesh);
const edges = new THREE.LineSegments(new THREE.EdgesGeometry(geo), new THREE.LineBasicMaterial({ color: 0xffaa00, transparent:true, opacity:0.8 }));
mesh.add(edges);
const cvs = document.createElement('canvas'); cvs.width=256; cvs.height=64;
const ctx = cvs.getContext('2d');
ctx.font = 'bold 36px "Segoe UI"'; ctx.fillStyle='#fff'; ctx.textAlign='center';
ctx.shadowBlur=4; ctx.shadowColor='#00ffff';
ctx.fillText(node.handle.name, 128, 45);
const tex = new THREE.CanvasTexture(cvs);
const sprite = new THREE.Sprite(new THREE.SpriteMaterial({ map: tex }));
sprite.scale.set(6, 1.5, 1); sprite.position.y = -2.5;
grp.add(sprite);
grp.userData = { isNode: true, node: node };
return grp;
}
function updateTreeLayout() {
// Cleanup old connection lines
groupTree.children.filter(c => c.userData.isLine).forEach(c => groupTree.remove(c));
// Safety: Ensure root exists
if (!state.treeNodes[0] || !state.treeNodes[0].mesh) return;
const depthSpacing = 50;
const spreadX = 20;
const traverse = (node, levelIdx, levelCount) => {
if (!node.mesh) {
node.mesh = createTreeMesh(node);
groupTree.add(node.mesh);
state.treeNodes.push(node);
}
const z = -(node.depth * depthSpacing);
let x = 0;
if (node.parent) {
const offset = (levelIdx - (levelCount-1)/2) * spreadX;
x = node.parent.mesh.position.x + offset;
// Draw Line
const pts = [node.parent.mesh.position.clone(), new THREE.Vector3(x, 0, z)];
const line = new THREE.Line(new THREE.BufferGeometry().setFromPoints(pts), new THREE.LineBasicMaterial({ color: 0x004466 }));
line.userData = { isLine: true };
groupTree.add(line);
}
node.mesh.position.set(x, 0, z);
node.mesh.visible = true;
if (node.expanded && node.children.length > 0) {
node.children.forEach((c, i) => traverse(c, i, node.children.length));
} else {
const hide = (n) => {
if(n.mesh) n.mesh.visible = false;
n.children.forEach(hide);
};
node.children.forEach(hide);
}
};
traverse(state.treeNodes[0], 0, 1);
}
async function selectNode(node) {
if(!node) return;
state.activeNode = node;
state.currentHandle = node.handle;
// Expand logic
if(!node.expanded) {
const children = [];
try {
for await (const e of node.handle.values()) {
if (e.kind === 'directory') children.push(new TreeNode(e, node));
}
} catch(e){}
node.children = children;
node.expanded = true;
updateTreeLayout();
}
// Load Files for Right Pane
loadFilesForPane(node.handle);
// Move Tree Camera to focus on node
if(node.mesh) {
state.targetTreeScrollZ = node.mesh.position.z + CONFIG.treeCamOffset.z;
state.targetTreeScrollX = node.mesh.position.x;
}
sfx.play('click');
}
// --- Logic: File View (Monoliths) ---
let popupMesh = null; let infoMesh = null;
async function loadFilesForPane(handle) {
state.isLoading = true;
document.getElementById('loader').classList.add('active');
// Clear old
state.fileObjects.forEach(o => groupFile.remove(o));
state.fileObjects = [];
reticleGroup.visible = false; updatePopup(null); state.selected = null;
const entries = [];
try {
for await (const e of handle.values()) {
entries.push(e);
// Allow UI breathing
if(entries.length % 50 === 0) await new Promise(r=>setTimeout(r,0));
}
} catch(e){}
document.getElementById('loader').classList.remove('active');
state.isLoading = false;
// Reset transforms
setMode(state.viewMode, entries);
}
function createMonolith(handle, x, y, z, angle, index, mode) {
const isF = handle.kind === 'directory';
const col = isF ? CONFIG.colFolder : CONFIG.colFile;
const geo = new THREE.BoxGeometry(2.4, 3.2, 0.1);
const mat = new THREE.MeshPhysicalMaterial({ color: col, transparent: true, opacity: 0.3, side: THREE.DoubleSide });
const mesh = new THREE.Mesh(geo, mat);
mesh.position.set(x, y, z);
if(mode === 'F1' || mode === 'F3') mesh.lookAt(x*2, y, z*2);
else if (mode === 'F2') mesh.lookAt(x, 0, 0);
else if (mode === 'F4') mesh.rotation.set(0, 0, 0);
mesh.userData = { handle, isFolder:isF, isFileObj:true };
const edges = new THREE.EdgesGeometry(geo);
const line = new THREE.LineSegments(edges, new THREE.LineBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.8 }));
mesh.add(line);
// Extension Icon
const cvs = document.createElement('canvas'); cvs.width=256; cvs.height=256;
const ctx = cvs.getContext('2d');
const ext = handle.name.split('.').pop().toUpperCase().slice(0,4);
ctx.font = `bold 100px "Segoe UI"`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.lineWidth = 6; ctx.strokeStyle = '#000000'; ctx.strokeText(isF?'DIR':ext, 128, 128);
ctx.fillStyle = '#ffffff'; ctx.fillText(isF?'DIR':ext, 128, 128);
const defTex = new THREE.CanvasTexture(cvs);
const extSprite = new THREE.Sprite(new THREE.SpriteMaterial({ map: defTex }));
extSprite.scale.set(2, 2, 1); extSprite.position.z = -1.5;
mesh.add(extSprite);
// Name
const nCvs = document.createElement('canvas'); nCvs.width=512; nCvs.height=128;
const nCtx = nCvs.getContext('2d');
nCtx.fillStyle = '#ffffff'; nCtx.font = 'bold 50px "Segoe UI"';
nCtx.shadowColor = 'black'; nCtx.shadowBlur = 6; nCtx.textAlign = 'center'; nCtx.fillText(handle.name, 256, 50);
const nTex = new THREE.CanvasTexture(nCvs);
const nameSprite = new THREE.Sprite(new THREE.SpriteMaterial({ map: nTex, transparent: true }));
nameSprite.scale.set(4, 1, 1); nameSprite.position.set(0, -2.2, 0);
mesh.add(nameSprite);
// Thumbnail (Lazy)
if (!isF) {
const n = handle.name.toLowerCase();
const loadT = (t) => { mesh.material = new THREE.MeshBasicMaterial({map:t, color:0xffffff}); };
if(n.match(/\.(jpg|png|webp|gif)$/)) handle.getFile().then(f => new THREE.TextureLoader().load(URL.createObjectURL(f), loadT));
else if(n.match(/\.bmp$/)) handle.getFile().then(f => createImageBitmap(f)).then(bmp => loadT(new THREE.CanvasTexture(bmp)));
else if(n.match(/\.(mp4|webm)$/i)) handle.getFile().then(f => {
const v = document.createElement('video'); v.src=URL.createObjectURL(f);
v.muted=true; v.loop=true; v.play().then(()=>{ loadT(new THREE.VideoTexture(v)); }).catch(()=>{});
});
}
groupFile.add(mesh); state.fileObjects.push(mesh);
}
function setMode(mode, entries) {
state.viewMode = mode;
let label = "VERTICAL";
if(mode==='F2') label="TUNNEL";
if(mode==='F3') label="SPIRAL";
if(mode==='F4') label="GRID";
document.getElementById('mode-info').innerText = `MODE: ${mode} (${label})`;
state.targetRotX = 0; state.currentRotX = 0;
state.targetRotY = 0; state.currentRotY = 0;
state.targetPosX = 0; state.currentPosX = 0;
state.targetPosY = 0; state.currentPosY = 0;
state.targetPosZ = 0; state.currentPosZ = 0;
gridHelperFile.visible = false;
if(!entries) return; // Wait for load
// Layout
const count = entries.length; const radius = 22;
state.fileObjects.forEach(o => groupFile.remove(o));
state.fileObjects = [];
if (mode === 'F1') {
const spacing = 5.0; const rowHeight = 7.0;
const circumference = 2 * Math.PI * radius;
const cols = Math.max(1, Math.min(count, Math.floor(circumference/spacing)));
entries.forEach((e, i) => {
const col = i % cols; const row = Math.floor(i / cols);
const angle = col * ((Math.PI * 2) / Math.max(cols, 10)) + Math.PI;
const x = radius * Math.sin(angle); const z = radius * Math.cos(angle);
let y = -(row * rowHeight) + (Math.floor(count/cols)*rowHeight)/2;
if (col % 2 !== 0) y -= rowHeight * 0.5;
createMonolith(e, x, y, z, angle, i, 'F1');
});
} else if (mode === 'F2') {
const itemsPerRing = 12; const spacingX = 6.0; const totalRings = Math.ceil(count / itemsPerRing);
entries.forEach((e, i) => {
const ring = Math.floor(i / itemsPerRing); const ang = (i % itemsPerRing) * ((Math.PI*2)/itemsPerRing);
const x = (ring * spacingX) - (totalRings * spacingX) / 2;
createMonolith(e, x, radius * Math.sin(ang), radius * Math.cos(ang), ang, i, 'F2');
});
} else if (mode === 'F3') {
state.targetPosY = 20;
entries.forEach((e, i) => {
const ang = i * 0.3;
createMonolith(e, radius*Math.sin(ang), -(i*2.0), radius*Math.cos(ang), ang, i, 'F3');
});
} else if (mode === 'F4') {
gridHelperFile.visible = true;
const gridW = 8; const spX = 7.0; const spZ = 8.0;
const rows = Math.ceil(count / gridW);
state.targetRotX = -Math.PI / 4; state.currentRotX = -Math.PI / 4;
state.targetPosY = -(rows*spZ)/2; state.currentPosY = -(rows*spZ)/2;
entries.forEach((e, i) => {
const col = i % gridW; const row = Math.floor(i / gridW);
const x = (col - gridW/2 + 0.5) * spX; const z = -row * spZ;
createMonolith(e, x, 0, z, 0, i, 'F4');
});
}
}
function updatePopup(target) {
if(popupMesh) { groupFile.remove(popupMesh); popupMesh=null; }
if(infoMesh) { groupFile.remove(infoMesh); infoMesh=null; }
if(!target) return;
const geo = new THREE.PlaneGeometry(6, 8);
const mat = new THREE.MeshBasicMaterial({ color: target.material.map ? 0xffffff : 0x0088ff, transparent: true, opacity: 0.95, side: THREE.DoubleSide, depthTest: false });
if(target.material.map) mat.map = target.material.map;
popupMesh = new THREE.Mesh(geo, mat); popupMesh.renderOrder = 9999;
popupMesh.position.copy(target.position);
// Popout logic
if(state.viewMode === 'F4') { popupMesh.position.y += 3; popupMesh.position.z += 6; popupMesh.rotation.x = Math.PI/4; }
else {
const v = new THREE.Vector3().copy(target.position).normalize();
if(state.viewMode==='F2') v.set(0, v.y, v.z).normalize();
else v.set(v.x, 0, v.z).normalize();
popupMesh.position.add(v.multiplyScalar(5));
popupMesh.lookAt(target.position);
}
groupFile.add(popupMesh);
reticleGroup.scale.set(3,3,1);
}
// --- Interaction ---
const splitter = document.getElementById('splitter');
const paneTree = document.getElementById('pane-tree');
splitter.addEventListener('pointerdown', e => { state.draggingSplit = true; splitter.classList.add('active'); e.preventDefault(); });
window.addEventListener('pointerup', async e => {
if (state.draggingSplit) { state.draggingSplit = false; splitter.classList.remove('active'); return; }
if (state.dragging) {
// Drag Drop Logic (Add to Stock)
if(e.clientY > window.innerHeight - 180 && state.dragging.mesh) {
await moveToStock(state.dragging.mesh.userData.handle);
} else if (!state.dragging.isMove && state.dragging.mesh) {
// Click (Selection)
activateReticle(state.dragging.mesh); updatePopup(state.dragging.mesh); sfx.play('lock');
}
state.dragging = null;
}
document.body.style.cursor = 'default';
});
window.addEventListener('pointermove', e => {
// Splitter
if (state.draggingSplit) {
const pct = (e.clientX / window.innerWidth) * 100;
if (pct > 10 && pct < 90) {
state.splitPercent = pct;
paneTree.style.flex = `0 0 ${pct}%`;
resizeRenderer();
}
return;
}
// Detect Pane
const treeW = window.innerWidth * (state.splitPercent / 100);
const isTree = e.clientX < treeW;
state.activePane = isTree ? 'tree' : 'file';
// Normalize Mouse for specific camera
if (isTree) {
state.mouse.x = (e.clientX / treeW) * 2 - 1;
state.mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
} else {
const fileW = window.innerWidth - treeW;
state.mouse.x = ((e.clientX - treeW) / fileW) * 2 - 1;
state.mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
}
// Raycasting & Hover
const raycaster = new THREE.Raycaster();
if (isTree) {
raycaster.setFromCamera(state.mouse, camTree);
const hits = raycaster.intersectObjects(groupTree.children, true);
let hitNode = null;
if(hits.length>0) {
let obj = hits[0].object;
while(obj && !obj.userData.isNode) obj = obj.parent;
hitNode = obj;
}
if (hitNode && state.hovered !== hitNode) { sfx.play('hover'); state.hovered = hitNode; document.body.style.cursor='pointer'; }
else if (!hitNode) { state.hovered = null; document.body.style.cursor='default'; }
} else {
// File Pane Interaction
if(e.buttons === 1 && !state.dragging) {
// Rotate/Pan File View
const dx = e.clientX - state.lastMouse.x; const dy = e.clientY - state.lastMouse.y;
if(state.viewMode==='F1') { state.targetRotY -= dx*0.005; state.targetPosY -= dy*0.1; }
else if(state.viewMode==='F2') { state.targetPosX += dx*0.05; state.targetRotX -= dy*0.005; }
else if(state.viewMode==='F3') { state.targetRotY -= dx*0.005; state.targetPosY -= dy*0.2; }
else if(state.viewMode==='F4') { state.targetPosX += dx*0.1; state.targetPosZ += dy*0.1; }
} else if (state.dragging) {
if(state.dragStart.distanceTo(new THREE.Vector2(e.clientX, e.clientY)) > 5) state.dragging.isMove = true;
} else {
raycaster.setFromCamera(state.mouse, camFile);
const hits = raycaster.intersectObjects(state.fileObjects, true);
let hitObj = null;
if(hits.length>0) {
let obj = hits[0].object;
while(obj && !obj.userData.isFileObj) obj = obj.parent;
hitObj = obj;
}
if(hitObj && state.hovered !== hitObj) { sfx.play('hover'); state.hovered = hitObj; document.body.style.cursor='pointer'; }
else if (!hitObj) { state.hovered = null; document.body.style.cursor='default'; }
}
}
state.lastMouse.set(e.clientX, e.clientY);
});
window.addEventListener('pointerdown', e => {
sfx.init();
if(e.target.closest('.ctx-menu') || e.target.closest('.stock-item') || e.target.closest('.stock-tab-item')) return;
closeAllMenus();
if(state.activePane === 'file' && state.hovered) {
state.dragStart.set(e.clientX, e.clientY);
state.dragging = { mesh: state.hovered, isMove: false };
state.selected = state.hovered;
} else if (state.activePane === 'tree' && state.hovered) {
// Tree selection immediate
selectNode(state.hovered.userData.node);
} else {
state.selected = null; reticleGroup.visible = false; updatePopup(null);
}
});
window.addEventListener('wheel', e => {
if(state.activePane === 'tree') {
state.targetTreeScrollZ += e.deltaY * 0.2;
} else {
if(state.viewMode==='F1' || state.viewMode==='F3') state.targetRotY += e.deltaY * 0.001;
else if(state.viewMode==='F2') state.targetRotX += e.deltaY * 0.001;
else if(state.viewMode==='F4') state.targetPosZ -= e.deltaY * 0.1;
if(state.viewMode==='F3' || state.viewMode==='F1') state.targetPosY += e.deltaY * 0.05;
}
});
window.addEventListener('dblclick', e => {
if(state.selected && state.selected.userData.isFileObj) {
const h = state.selected.userData.handle;
if(h.kind==='directory') {
// Find equivalent node in active directory to expand tree
if(state.activeNode) {
const child = state.activeNode.children.find(c => c.handle.name === h.name);
if(child) selectNode(child);
else { /* Should probably reload tree children */ }
}
} else openPreview(h);
}
});
// Key Commands
window.addEventListener('keydown', e => {
if(e.key.startsWith('F')) {
const m = e.key;
if(['F1','F2','F3','F4'].includes(m)) {
e.preventDefault();
setMode(m, state.activeNode ? [] : null); // If active, re-layout happens in loop or manual trigger
// Re-trigger layout with current objects
if(state.activeNode) loadFilesForPane(state.activeNode.handle);
}
}
});
// --- Render Loop ---
function activateReticle(mesh) {
reticleGroup.visible = true;
reticleGroup.position.copy(mesh.position);
reticleGroup.rotation.copy(mesh.rotation);
}
function drawHUD() {
const dpr = window.devicePixelRatio||1;
hudX.clearRect(0,0,hudC.width, hudC.height);
// Only draw HUD for File items in Right Pane
const t = state.hovered || state.selected;
if(!t || !t.userData.isFileObj || state.dragging) return;
// Project position
const p = new THREE.Vector3(); t.getWorldPosition(p);
p.y += 2.0; p.project(camFile);
// Convert to screen coords (Right pane relative)
const width = window.innerWidth;
const treeW = width * (state.splitPercent / 100);
const fileW = width - treeW;
const x = (p.x * 0.5 + 0.5) * fileW + treeW;
const y = (-(p.y * 0.5) + 0.5) * window.innerHeight;
hudX.save();
hudX.scale(dpr, dpr);
hudX.shadowBlur = 10; hudX.shadowColor = CONFIG.colActive;
hudX.strokeStyle = '#ffee00'; hudX.lineWidth = 2;
hudX.beginPath(); hudX.moveTo(x,y); hudX.lineTo(x+40,y-40); hudX.lineTo(x+160,y-40); hudX.stroke();
hudX.font="bold 14px 'Segoe UI'"; hudX.fillStyle='#fff'; hudX.fillText(t.userData.handle.name, x+45, y-45);
hudX.restore();
}
function animate() {
requestAnimationFrame(animate);
// 1. Update Tree Logic
state.treeScrollZ += (state.targetTreeScrollZ - state.treeScrollZ) * 0.1;
state.treeScrollX += (state.targetTreeScrollX - state.treeScrollX) * 0.1;
camTree.position.z = state.treeScrollZ;
camTree.position.x = state.treeScrollX;
camTree.position.y = CONFIG.treeCamOffset.y;
camTree.lookAt(state.treeScrollX, 0, state.treeScrollZ - 100);
// 2. Update File Logic
state.currentRotY += (state.targetRotY - state.currentRotY) * 0.1;
state.currentPosY += (state.targetPosY - state.currentPosY) * 0.1;
state.currentRotX += (state.targetRotX - state.currentRotX) * 0.1;
state.currentPosX += (state.targetPosX - state.currentPosX) * 0.1;
state.currentPosZ += (state.targetPosZ - state.currentPosZ) * 0.1;
if(state.viewMode === 'F1' || state.viewMode === 'F3') {
groupFile.rotation.y = state.currentRotY; groupFile.position.y = state.currentPosY;
groupFile.rotation.x = 0; groupFile.position.x = 0; groupFile.position.z = 0;
} else if (state.viewMode === 'F2') {
groupFile.rotation.x = state.currentRotX; groupFile.position.x = state.currentPosX;
groupFile.rotation.y = 0; groupFile.position.y = 0; groupFile.position.z = 0;
} else if (state.viewMode === 'F4') {
groupFile.rotation.set(state.currentRotX, 0, 0);
groupFile.position.set(state.currentPosX, state.currentPosY, state.currentPosZ);
}
// Reticle Anim
if(reticleGroup.visible) {
const s = (state.selected && popupMesh) ? 3 : 1;
reticleGroup.scale.set(s,s,1);
reticleGroup.userData.layers.forEach(l => l.rotation.z += l.userData.speed * l.userData.dir);
// If dragging/rotating view, reticle must follow mesh world pos
if(state.selected) {
const wp = new THREE.Vector3(); state.selected.getWorldPosition(wp);
const wq = new THREE.Quaternion(); state.selected.getWorldQuaternion(wq);
reticleGroup.position.copy(wp); reticleGroup.quaternion.copy(wq);
reticleGroup.translateZ(0.2);
}
}
// 3. Render (Scissor)
const width = window.innerWidth; const height = window.innerHeight;
const treeW = width * (state.splitPercent / 100);
// Left Pane
const leftW = Math.floor(treeW);
renderer.setViewport(0, 0, leftW, height);
renderer.setScissor(0, 0, leftW, height);
renderer.render(sceneTree, camTree);
// Right Pane
const rightW = width - leftW;
renderer.setViewport(leftW, 0, rightW, height);
renderer.setScissor(leftW, 0, rightW, height);
renderer.render(sceneFile, camFile);
drawHUD();
}
animate();
// --- Menus, Stock, Dialogs (From 1.4.6) ---
const ctxObj = document.getElementById('ctx-obj');
const ctxBg = document.getElementById('ctx-bg');
const ctxTab = document.getElementById('ctx-tab');
function closeAllMenus() {
document.querySelectorAll('.ctx-menu').forEach(m => {
if(m.style.display !== 'none' && !m.classList.contains('closing')) {
m.classList.remove('opening'); m.classList.add('closing');
setTimeout(() => { m.style.display = 'none'; m.classList.remove('closing'); }, 400);
}
});
}
function showMenu(el, x, y) {
el.style.display='flex'; el.classList.remove('closing'); el.classList.add('opening');
if(x+200>window.innerWidth) x-=200; if(y+250>window.innerHeight) y-=250;
el.style.left=x+'px'; el.style.top=y+'px';
}
window.addEventListener('contextmenu', e => {
e.preventDefault();
if(e.target.closest('.stock-tab-item')) {
const idx = e.target.closest('.stock-tab-item').dataset.idx;
if (idx !== undefined) { state.tabContextTargetIdx = parseInt(idx); showMenu(ctxTab, e.clientX, e.clientY); }
return;
}
if(state.activePane === 'tree' && state.hovered) {
state.contextTarget = state.hovered.userData.node.handle;
showMenu(ctxObj, e.clientX, e.clientY);
return;
}
if(state.activePane === 'file' && state.hovered) {
state.contextTarget = state.hovered.userData.handle;
state.selected = state.hovered;
activateReticle(state.hovered); updatePopup(state.hovered);
showMenu(ctxObj, e.clientX, e.clientY);
} else {
state.contextTarget = null;
showMenu(ctxBg, e.clientX, e.clientY);
}
});
// Stock Logic
function renderStockTabs() {
const c = document.getElementById('stock-tabs-container'); c.innerHTML = '';
if(state.stockList.length === 0) { c.innerHTML = '<div class="stock-tab-item" style="opacity:0.5; pointer-events:none;">STOCK</div>'; return; }
state.stockList.forEach((item, i) => {
const div = document.createElement('div'); div.className = 'stock-tab-item';
if(i === state.activeStockIdx) div.classList.add('active');
div.innerText = item.handle.name.toUpperCase(); div.dataset.idx = i;
div.onclick = () => {
state.activeStockIdx = i; document.getElementById('stock').classList.remove('closed');
renderStockTabs(); refreshStock(); sfx.play('click');
};
c.appendChild(div);
});
}
async function refreshStock() {
const s = document.getElementById('stock'); s.innerHTML='';
if(state.activeStockIdx < 0 || !state.stockList[state.activeStockIdx]) { s.innerHTML = '<div style="color:var(--orange); opacity:0.6; pointer-events:none;">[ EMPTY STOCK ]</div>'; return; }
const currentStock = state.stockList[state.activeStockIdx];
currentStock.entries = [];
try {
for await (const entry of currentStock.handle.values()) currentStock.entries.push(entry);
if(currentStock.entries.length === 0) s.innerHTML = '<div style="color:var(--orange); opacity:0.6; pointer-events:none;">[ EMPTY FOLDER ]</div>';
else {
for (let i=0; i<currentStock.entries.length; i++) {
const h = currentStock.entries[i];
const d = document.createElement('div'); d.className = 'stock-item'; d.draggable = true;
const thumb = document.createElement('div'); thumb.className = 'stock-thumb';
if(h.kind === 'directory') thumb.innerHTML = '<span style="font-size:40px">📂</span>';
else {
const n = h.name.toLowerCase();
if(n.match(/\.(jpg|png|webp|gif)$/)) h.getFile().then(f => { const img=document.createElement('img'); img.src=URL.createObjectURL(f); img.style.maxWidth='100%'; img.style.maxHeight='100%'; thumb.appendChild(img); });
else thumb.innerHTML = '<span style="font-size:30px">📄</span>';
}
d.appendChild(thumb);
const lbl = document.createElement('div'); lbl.className = 'stock-label'; lbl.innerText = h.name; d.appendChild(lbl);
d.ondragstart = e => { e.dataTransfer.setData('stockIndex', i); e.stopPropagation(); };
s.appendChild(d);
}
}
} catch(e) { s.innerHTML = '<div style="color:red">ACCESS ERROR</div>'; }
}
async function moveToStock(handle) {
if(state.activeStockIdx < 0 || !state.stockList[state.activeStockIdx]) { alert("No Active Stock Tab!"); return; }
const dest = state.stockList[state.activeStockIdx].handle;
if(handle.move) {
try { await handle.move(dest); refreshStock(); loadFilesForPane(state.currentHandle); }
catch(e){ sfx.play('error'); alert(e.message); }
}
}
async function moveFromStock(idx) {
if(state.activeStockIdx<0) return;
const h = state.stockList[state.activeStockIdx].entries[idx];
if(h && h.move) {
try { await h.move(state.currentHandle); refreshStock(); loadFilesForPane(state.currentHandle); }
catch(e){ sfx.play('error'); alert(e.message); }
}
}
document.getElementById('stock').ondrop = async e => {
// Handled by pointerup on splitter logic or internal drag?
// Browser native drag needs this:
};
const stockDiv = document.getElementById('stock');
stockDiv.ondragover = e => e.preventDefault();
container.ondragover = e => e.preventDefault();
container.ondrop = async e => { e.preventDefault(); const idx = e.dataTransfer.getData('stockIndex'); if(idx) moveFromStock(parseInt(idx)); };
// Context Actions
document.getElementById('cm-open').onclick = () => {
const t = state.contextTarget;
if(t) { if(t.kind==='directory') selectNode(state.treeNodes.find(n=>n.userData.node.handle.name===t.name).userData.node); else openPreview(t); }
closeAllMenus();
};
document.getElementById('cm-add-stock').onclick = () => {
const t = state.contextTarget;
if(t && t.kind==='directory') {
if(state.stockList.find(s=>s.handle.name===t.name)) state.activeStockIdx = state.stockList.findIndex(s=>s.handle.name===t.name);
else { state.stockList.push({handle:t, entries:[]}); state.activeStockIdx=state.stockList.length-1; }
renderStockTabs(); refreshStock(); document.getElementById('stock').classList.remove('closed');
}
closeAllMenus();
};
document.getElementById('cm-tab-hide').onclick=()=>{ document.getElementById('stock').classList.add('closed'); closeAllMenus(); };
document.getElementById('cm-tab-cancel').onclick=()=>{
state.stockList.splice(state.tabContextTargetIdx, 1);
if(state.stockList.length===0) { state.activeStockIdx=-1; document.getElementById('stock').classList.add('closed'); }
else state.activeStockIdx = Math.max(0, state.stockList.length-1);
renderStockTabs(); refreshStock(); closeAllMenus();
};
document.getElementById('cm-new').onclick = async () => {
closeAllMenus(); const n = await customPrompt("NEW FOLDER", "New Folder");
if(n) { await state.currentHandle.getDirectoryHandle(n, {create:true}); loadFilesForPane(state.currentHandle); }
};
document.getElementById('cm-rename').onclick = async () => {
closeAllMenus(); const t = state.contextTarget;
if(t) { const n = await customPrompt("RENAME", t.name); if(n && n!==t.name && t.move) { await t.move(n); loadFilesForPane(state.currentHandle); } }
};
document.getElementById('cm-delete').onclick = async () => {
closeAllMenus(); const t = state.contextTarget;
if(t && confirm('Delete?')) { await state.currentHandle.removeEntry(t.name, {recursive:true}); loadFilesForPane(state.currentHandle); }
};
// Dialogs
const dModal=document.getElementById('dialog'), dInput=document.getElementById('d-input');
let dResolve=null;
function customPrompt(title, val) {
return new Promise(r => {
document.getElementById('d-title').innerText=title; dInput.value=val; dResolve=r;
dModal.classList.remove('closing'); dModal.classList.add('active','opening'); dInput.focus(); sfx.play('open');
});
}
function closeDialog(res){ dModal.classList.remove('opening'); dModal.classList.add('closing'); sfx.play('back'); setTimeout(()=>{dModal.classList.remove('active','closing'); if(dResolve) dResolve(res); dResolve=null;},500); }
document.getElementById('d-ok').onclick=()=>closeDialog(dInput.value);
document.getElementById('d-cancel').onclick=()=>closeDialog(null);
dInput.onkeydown=e=>{if(e.key==='Enter')closeDialog(dInput.value);if(e.key==='Escape')closeDialog(null);};
// Preview
const pModal=document.getElementById('preview'), pBody=document.getElementById('p-body');
let curPH=null;
async function openPreview(h) {
curPH=h; pModal.classList.remove('closing'); pModal.classList.add('active','opening'); sfx.play('open');
document.getElementById('p-title').innerText=h.name; pBody.innerHTML='Loading...';
try {
const f = await h.getFile(); const u = URL.createObjectURL(f);
if(f.name.match(/\.(png|jpg|webp|gif)$/i)) pBody.innerHTML=`<img src="${u}" style="max-width:100%;max-height:100%">`;
else if(f.name.match(/\.(mp4|webm)$/i)) pBody.innerHTML=`<video src="${u}" controls autoplay style="max-width:100%"></video>`;
else { const t = await f.text(); pBody.innerHTML=`<textarea id="editor-area" class="hex-view">${t}</textarea>`; document.getElementById('p-save').style.display='block'; }
} catch(e){ pBody.innerText="Error"; }
}
document.getElementById('p-close').onclick=()=>{ pModal.classList.remove('opening'); pModal.classList.add('closing'); sfx.play('back'); setTimeout(()=>{pModal.classList.remove('active','closing');},500); };
document.getElementById('p-save').onclick=async()=>{
const ta=document.getElementById('editor-area');
if(curPH && ta) { try { const w=await curPH.createWritable(); await w.write(ta.value); await w.close(); alert("Saved"); } catch(e){alert("Error");} }
};
// Init
document.getElementById('btn-mount').onclick = async () => {
sfx.init(); sfx.play('click');
try {
const h = await window.showDirectoryPicker();
state.rootHandle = h;
// Clear State
state.treeNodes = [];
state.activeNode = null;
// Reset Tree Group
groupTree.children.slice().forEach(c => {
if(c.userData.isNode || c.userData.isLine) groupTree.remove(c);
});
// Create & Register Root
const root = new TreeNode(h);
root.mesh = createTreeMesh(root);
groupTree.add(root.mesh);
state.treeNodes.push(root);
// Select
await selectNode(root);
// Initial Layout
updateTreeLayout();
} catch(e) { console.error(e); }
};
</script>
</body>
</html>

コメント