667 lines
26 KiB
JavaScript
667 lines
26 KiB
JavaScript
/**
|
|
* CANNIBAL GIRLS - ARCHIVE TERMINAL
|
|
* Main Application Logic
|
|
*/
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
// ========================================
|
|
// CONFIGURATION
|
|
// ========================================
|
|
const CONFIG = {
|
|
loadDelayMin: 4000,
|
|
loadDelayMax: 9000,
|
|
audioVolume: 0.15,
|
|
audioPath: 'audio/',
|
|
terminalLines: [
|
|
{ text: 'Mounting storage volumes...', type: 'normal', delay: 0 },
|
|
{ text: 'Checking filesystem integrity...', type: 'normal', delay: 300 },
|
|
{ text: 'Scanning /archive/repository...', type: 'normal', delay: 600 },
|
|
{ text: 'Found 247 indexed files', type: 'success', delay: 1200 },
|
|
{ text: 'Verifying checksums...', type: 'normal', delay: 1500 },
|
|
{ text: 'Warning: 3 files flagged for review', type: 'warning', delay: 2200 },
|
|
{ text: 'Rebuilding file index...', type: 'normal', delay: 2500 },
|
|
{ text: 'Optimizing cache...', type: 'normal', delay: 3200 },
|
|
{ text: 'Archive mount successful', type: 'success', delay: 4000 },
|
|
{ text: 'Ready for access', type: 'success', delay: 4500 }
|
|
]
|
|
};
|
|
|
|
// ========================================
|
|
// DEMO FILE DATA
|
|
// ========================================
|
|
const DEMO_FILES = [
|
|
{ id: 1, name: 'project_venus_raw.mp4', type: 'video', size: 2847563210, date: '2024-01-15T14:23:00', checksum: 'a3f7c9d2e1b4f5a6c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1' },
|
|
{ id: 2, name: 'archive_segment_07.mkv', type: 'video', size: 1456789012, date: '2024-01-14T09:15:00', checksum: 'b4c8d0e3f2a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c2' },
|
|
{ id: 3, name: 'nocturne_stills_set.zip', type: 'archive', size: 456782345, date: '2024-01-13T16:45:00', checksum: 'c5d9e1f4a3b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d3' },
|
|
{ id: 4, name: 'soundscape_01.flac', type: 'audio', size: 124567890, date: '2024-01-12T11:30:00', checksum: 'd6e0f2a5b4c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e4' },
|
|
{ id: 5, name: 'behind_the_scenes_03.jpg', type: 'image', size: 8945678, date: '2024-01-11T08:20:00', checksum: 'e7f1a3b6c5d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f5' },
|
|
{ id: 6, name: 'production_notes_v2.pdf', type: 'document', size: 2345678, date: '2024-01-10T19:00:00', checksum: 'f8a2b4c7d6e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a6' },
|
|
{ id: 7, name: 'raw_footage_reel_12.mp4', type: 'video', size: 5678901234, date: '2024-01-09T13:10:00', checksum: 'a9b3c5d7e8f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7' },
|
|
{ id: 8, name: 'color_grade_lut.cube', type: 'document', size: 45678, date: '2024-01-08T10:45:00', checksum: 'b0c4d6e8f9a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8' },
|
|
{ id: 9, name: 'ambient_collection.zip', type: 'archive', size: 345678901, date: '2024-01-07T15:30:00', checksum: 'c1d5e7f9a0b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9' },
|
|
{ id: 10, name: 'title_sequence_v3.mov', type: 'video', size: 1234567890, date: '2024-01-06T07:00:00', checksum: 'd2e6f8a0b1c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0' },
|
|
{ id: 11, name: 'location_scout_photos.zip', type: 'archive', size: 156789012, date: '2024-01-05T12:15:00', checksum: 'e3f7a9b1c2d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1' },
|
|
{ id: 12, name: 'score_master_24bit.wav', type: 'audio', size: 890123456, date: '2024-01-04T18:30:00', checksum: 'f4a8b0c2d3e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2' },
|
|
{ id: 13, name: 'character_studies_01.png', type: 'image', size: 45678901, date: '2024-01-03T09:45:00', checksum: 'a5b9c1d3e4f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3' },
|
|
{ id: 14, name: 'edit_timeline_v4.prproj', type: 'document', size: 12345678, date: '2024-01-02T14:00:00', checksum: 'b6c0d2e4f5a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4' },
|
|
{ id: 15, name: 'promo_cut_30sec.mp4', type: 'video', size: 678901234, date: '2024-01-01T11:20:00', checksum: 'c7d1e3f5a6b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5' }
|
|
];
|
|
|
|
// ========================================
|
|
// STATE
|
|
// ========================================
|
|
let state = {
|
|
files: [],
|
|
filteredFiles: [],
|
|
selectedFile: null,
|
|
currentView: 'list',
|
|
currentFilter: 'all',
|
|
currentSort: 'date-desc',
|
|
searchQuery: '',
|
|
uptime: 0,
|
|
audioContext: null
|
|
};
|
|
|
|
// ========================================
|
|
// DOM REFERENCES
|
|
// ========================================
|
|
const els = {};
|
|
|
|
function cacheElements() {
|
|
els.loadingScreen = document.getElementById('loading-screen');
|
|
els.loadingTitle = document.getElementById('loading-title');
|
|
els.loadingDatetime = document.getElementById('loading-datetime');
|
|
els.loadBtn = document.getElementById('load-files-btn');
|
|
els.loadingPhase = document.getElementById('loading-phase');
|
|
els.terminalOutput = document.getElementById('terminal-output');
|
|
els.progressText = document.getElementById('progress-text');
|
|
els.progressPercent = document.getElementById('progress-percent');
|
|
els.progressFill = document.getElementById('progress-fill');
|
|
els.archiveBrowser = document.getElementById('archive-browser');
|
|
els.fileList = document.getElementById('file-list');
|
|
els.fileListHeader = document.getElementById('file-list-header');
|
|
els.sidebarMenu = document.getElementById('sidebar-menu');
|
|
els.sortSelect = document.getElementById('sort-select');
|
|
els.searchInput = document.getElementById('search-input');
|
|
els.previewPane = document.getElementById('preview-pane');
|
|
els.previewContent = document.getElementById('preview-content');
|
|
els.previewClose = document.getElementById('preview-close');
|
|
els.previewMeta = document.getElementById('preview-meta');
|
|
els.headerTime = document.getElementById('header-time');
|
|
els.uptime = document.getElementById('uptime');
|
|
els.fileCount = document.getElementById('file-count');
|
|
els.totalSize = document.getElementById('total-size');
|
|
els.footerStatus = document.getElementById('footer-status');
|
|
els.bgAudio = document.getElementById('bg-audio');
|
|
els.actionDownload = document.getElementById('action-download');
|
|
els.actionCopy = document.getElementById('action-copy');
|
|
}
|
|
|
|
// ========================================
|
|
// UTILITIES
|
|
// ========================================
|
|
function formatBytes(bytes) {
|
|
if (bytes === 0) return '0 B';
|
|
const k = 1024;
|
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
}
|
|
|
|
function formatDate(dateString) {
|
|
const date = new Date(dateString);
|
|
return date.toISOString().slice(0, 19).replace('T', ' ');
|
|
}
|
|
|
|
function formatDateShort(dateString) {
|
|
const date = new Date(dateString);
|
|
return date.toISOString().slice(0, 10);
|
|
}
|
|
|
|
function getFileIcon(type) {
|
|
const icons = {
|
|
video: '▶',
|
|
audio: '♬',
|
|
image: '■',
|
|
document: '☰',
|
|
archive: '■'
|
|
};
|
|
return icons[type] || '■';
|
|
}
|
|
|
|
function getRandomDelay() {
|
|
return Math.floor(Math.random() * (CONFIG.loadDelayMax - CONFIG.loadDelayMin + 1)) + CONFIG.loadDelayMin;
|
|
}
|
|
|
|
function generateChecksum() {
|
|
const chars = 'abcdef0123456789';
|
|
let result = '';
|
|
for (let i = 0; i < 64; i++) {
|
|
result += chars[Math.floor(Math.random() * chars.length)];
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// ========================================
|
|
// TEXT SCRAMBLE EFFECT
|
|
// ========================================
|
|
class TextScramble {
|
|
constructor(el) {
|
|
this.el = el;
|
|
this.chars = '!<>-_\\/[]{}--=+*^?#________';
|
|
this.update = this.update.bind(this);
|
|
}
|
|
|
|
setText(newText) {
|
|
const oldText = this.el.innerText;
|
|
const length = Math.max(oldText.length, newText.length);
|
|
const promise = new Promise(resolve => this.resolve = resolve);
|
|
this.queue = [];
|
|
for (let i = 0; i < length; i++) {
|
|
const from = oldText[i] || '';
|
|
const to = newText[i] || '';
|
|
const start = Math.floor(Math.random() * 40);
|
|
const end = start + Math.floor(Math.random() * 40);
|
|
this.queue.push({ from, to, start, end });
|
|
}
|
|
cancelAnimationFrame(this.frameRequest);
|
|
this.frame = 0;
|
|
this.update();
|
|
return promise;
|
|
}
|
|
|
|
update() {
|
|
let output = '';
|
|
let complete = 0;
|
|
for (let i = 0, n = this.queue.length; i < n; i++) {
|
|
let { from, to, start, end, char } = this.queue[i];
|
|
if (this.frame >= end) {
|
|
complete++;
|
|
output += to;
|
|
} else if (this.frame >= start) {
|
|
if (!char || Math.random() < 0.28) {
|
|
char = this.randomChar();
|
|
this.queue[i].char = char;
|
|
}
|
|
output += `<span class="text-dim">${char}</span>`;
|
|
} else {
|
|
output += from;
|
|
}
|
|
}
|
|
this.el.innerHTML = output;
|
|
if (complete === this.queue.length) {
|
|
this.resolve();
|
|
} else {
|
|
this.frameRequest = requestAnimationFrame(this.update);
|
|
this.frame++;
|
|
}
|
|
}
|
|
|
|
randomChar() {
|
|
return this.chars[Math.floor(Math.random() * this.chars.length)];
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// AUDIO MANAGEMENT
|
|
// ========================================
|
|
async function getAudioFiles() {
|
|
// In a real scenario, this would fetch from the server
|
|
// For now, we'll try common audio filenames or return empty
|
|
const commonNames = ['ambient.mp3', 'noise.mp3', 'static.mp3', 'drone.mp3', 'hum.mp3'];
|
|
return commonNames.map(name => CONFIG.audioPath + name);
|
|
}
|
|
|
|
async function playRandomAudio() {
|
|
try {
|
|
const audioFiles = await getAudioFiles();
|
|
if (audioFiles.length === 0) {
|
|
console.log('No audio files found in', CONFIG.audioPath);
|
|
return;
|
|
}
|
|
|
|
const randomFile = audioFiles[Math.floor(Math.random() * audioFiles.length)];
|
|
els.bgAudio.src = randomFile;
|
|
els.bgAudio.volume = CONFIG.audioVolume;
|
|
|
|
const playPromise = els.bgAudio.play();
|
|
if (playPromise !== undefined) {
|
|
playPromise.catch(err => {
|
|
console.log('Audio playback failed:', err.message);
|
|
});
|
|
}
|
|
} catch (err) {
|
|
console.log('Audio error:', err);
|
|
}
|
|
}
|
|
|
|
function stopAudio() {
|
|
els.bgAudio.pause();
|
|
els.bgAudio.currentTime = 0;
|
|
}
|
|
|
|
// ========================================
|
|
// TERMINAL OUTPUT
|
|
// ========================================
|
|
function addTerminalLine(text, type = 'normal') {
|
|
const line = document.createElement('div');
|
|
line.className = 'terminal-line';
|
|
|
|
const prompt = document.createElement('span');
|
|
prompt.className = 'line-prompt';
|
|
prompt.textContent = '>';
|
|
|
|
const content = document.createElement('span');
|
|
content.className = `line-content ${type}`;
|
|
content.textContent = text;
|
|
|
|
line.appendChild(prompt);
|
|
line.appendChild(content);
|
|
els.terminalOutput.appendChild(line);
|
|
|
|
// Auto-scroll
|
|
els.terminalOutput.scrollTop = els.terminalOutput.scrollHeight;
|
|
}
|
|
|
|
// ========================================
|
|
// LOADING SEQUENCE
|
|
// ========================================
|
|
async function startLoadingSequence() {
|
|
const delay = getRandomDelay();
|
|
|
|
// Hide loading screen, show loading phase
|
|
els.loadBtn.disabled = true;
|
|
els.loadBtn.innerHTML = '<span class="spinner"></span>';
|
|
|
|
setTimeout(() => {
|
|
els.loadingScreen.classList.add('hidden');
|
|
els.loadingPhase.classList.remove('hidden');
|
|
|
|
// Start audio
|
|
playRandomAudio();
|
|
|
|
// Start terminal output
|
|
let currentTime = 0;
|
|
CONFIG.terminalLines.forEach(line => {
|
|
setTimeout(() => {
|
|
addTerminalLine(line.text, line.type);
|
|
}, line.delay);
|
|
});
|
|
|
|
// Progress bar
|
|
const startTime = Date.now();
|
|
const updateProgress = () => {
|
|
const elapsed = Date.now() - startTime;
|
|
const progress = Math.min((elapsed / delay) * 100, 100);
|
|
|
|
els.progressFill.style.width = progress + '%';
|
|
els.progressPercent.textContent = Math.floor(progress) + '%';
|
|
|
|
// Update progress text based on percentage
|
|
if (progress < 20) els.progressText.textContent = 'MOUNTING VOLUMES...';
|
|
else if (progress < 40) els.progressText.textContent = 'VERIFYING CHECKSUMS...';
|
|
else if (progress < 60) els.progressText.textContent = 'INDEXING FILES...';
|
|
else if (progress < 80) els.progressText.textContent = 'OPTIMIZING CACHE...';
|
|
else if (progress < 100) els.progressText.textContent = 'FINALIZING...';
|
|
else els.progressText.textContent = 'COMPLETE';
|
|
|
|
if (progress < 100) {
|
|
requestAnimationFrame(updateProgress);
|
|
} else {
|
|
setTimeout(finishLoading, 500);
|
|
}
|
|
};
|
|
|
|
requestAnimationFrame(updateProgress);
|
|
}, 300);
|
|
}
|
|
|
|
function finishLoading() {
|
|
stopAudio();
|
|
els.loadingPhase.classList.add('hidden');
|
|
els.archiveBrowser.classList.remove('hidden');
|
|
els.archiveBrowser.classList.add('active');
|
|
|
|
// Initialize archive
|
|
initArchive();
|
|
}
|
|
|
|
// ========================================
|
|
// ARCHIVE INITIALIZATION
|
|
// ========================================
|
|
function initArchive() {
|
|
state.files = DEMO_FILES.map(f => ({
|
|
...f,
|
|
checksum: f.checksum || generateChecksum()
|
|
}));
|
|
|
|
updateCounts();
|
|
filterAndSortFiles();
|
|
renderFiles();
|
|
updateStats();
|
|
|
|
// Start uptime counter
|
|
setInterval(() => {
|
|
state.uptime++;
|
|
updateUptime();
|
|
}, 1000);
|
|
}
|
|
|
|
function updateCounts() {
|
|
const counts = {
|
|
all: state.files.length,
|
|
video: state.files.filter(f => f.type === 'video').length,
|
|
image: state.files.filter(f => f.type === 'image').length,
|
|
audio: state.files.filter(f => f.type === 'audio').length,
|
|
document: state.files.filter(f => f.type === 'document').length,
|
|
archive: state.files.filter(f => f.type === 'archive').length
|
|
};
|
|
|
|
Object.keys(counts).forEach(key => {
|
|
const el = document.getElementById(`count-${key}`);
|
|
if (el) el.textContent = counts[key];
|
|
});
|
|
}
|
|
|
|
function updateStats() {
|
|
els.fileCount.textContent = state.filteredFiles.length;
|
|
const totalBytes = state.filteredFiles.reduce((sum, f) => sum + f.size, 0);
|
|
els.totalSize.textContent = formatBytes(totalBytes);
|
|
}
|
|
|
|
function updateUptime() {
|
|
const hours = Math.floor(state.uptime / 3600).toString().padStart(2, '0');
|
|
const minutes = Math.floor((state.uptime % 3600) / 60).toString().padStart(2, '0');
|
|
const seconds = (state.uptime % 60).toString().padStart(2, '0');
|
|
els.uptime.textContent = `${hours}:${minutes}:${seconds}`;
|
|
}
|
|
|
|
// ========================================
|
|
// FILE FILTERING & SORTING
|
|
// ========================================
|
|
function filterAndSortFiles() {
|
|
let files = [...state.files];
|
|
|
|
// Apply filter
|
|
if (state.currentFilter !== 'all') {
|
|
files = files.filter(f => f.type === state.currentFilter);
|
|
}
|
|
|
|
// Apply search
|
|
if (state.searchQuery) {
|
|
const query = state.searchQuery.toLowerCase();
|
|
files = files.filter(f => f.name.toLowerCase().includes(query));
|
|
}
|
|
|
|
// Apply sort
|
|
files.sort((a, b) => {
|
|
switch (state.currentSort) {
|
|
case 'name-asc':
|
|
return a.name.localeCompare(b.name);
|
|
case 'name-desc':
|
|
return b.name.localeCompare(a.name);
|
|
case 'date-desc':
|
|
return new Date(b.date) - new Date(a.date);
|
|
case 'date-asc':
|
|
return new Date(a.date) - new Date(b.date);
|
|
case 'size-desc':
|
|
return b.size - a.size;
|
|
case 'size-asc':
|
|
return a.size - b.size;
|
|
case 'type':
|
|
return a.type.localeCompare(b.type) || a.name.localeCompare(b.name);
|
|
default:
|
|
return 0;
|
|
}
|
|
});
|
|
|
|
state.filteredFiles = files;
|
|
updateStats();
|
|
}
|
|
|
|
// ========================================
|
|
// FILE RENDERING
|
|
// ========================================
|
|
function renderFiles() {
|
|
els.fileList.innerHTML = '';
|
|
|
|
if (state.filteredFiles.length === 0) {
|
|
const empty = document.createElement('div');
|
|
empty.className = 'file-item';
|
|
empty.innerHTML = '<span style="color: #5A4545; grid-column: 1 / -1; text-align: center; padding: 2rem;">NO FILES FOUND</span>';
|
|
els.fileList.appendChild(empty);
|
|
return;
|
|
}
|
|
|
|
state.filteredFiles.forEach((file, index) => {
|
|
const item = document.createElement('div');
|
|
item.className = 'file-item';
|
|
if (state.selectedFile && state.selectedFile.id === file.id) {
|
|
item.classList.add('selected');
|
|
}
|
|
|
|
// Add staggered animation
|
|
item.style.animation = `fadeInUp 0.3s ease-out ${index * 0.03}s both`;
|
|
|
|
item.innerHTML = `
|
|
<div class="file-name-cell">
|
|
<span class="file-icon">${getFileIcon(file.type)}</span>
|
|
<span class="file-name" title="${file.name}">${file.name}</span>
|
|
</div>
|
|
<span class="file-type">${file.type}</span>
|
|
<span class="file-size">${formatBytes(file.size)}</span>
|
|
<span class="file-date">${formatDateShort(file.date)}</span>
|
|
`;
|
|
|
|
item.addEventListener('click', () => selectFile(file));
|
|
els.fileList.appendChild(item);
|
|
});
|
|
}
|
|
|
|
// ========================================
|
|
// FILE SELECTION & PREVIEW
|
|
// ========================================
|
|
function selectFile(file) {
|
|
state.selectedFile = file;
|
|
|
|
// Update selection in list
|
|
document.querySelectorAll('.file-item').forEach(item => {
|
|
item.classList.remove('selected');
|
|
});
|
|
|
|
const selectedEl = Array.from(els.fileList.children).find(el =>
|
|
el.querySelector('.file-name')?.textContent === file.name
|
|
);
|
|
if (selectedEl) selectedEl.classList.add('selected');
|
|
|
|
// Update preview
|
|
updatePreview(file);
|
|
|
|
// Update footer status
|
|
els.footerStatus.textContent = `SELECTED: ${file.name}`;
|
|
}
|
|
|
|
function updatePreview(file) {
|
|
// Update preview content
|
|
els.previewContent.innerHTML = `
|
|
<div class="preview-placeholder">
|
|
<span class="preview-file-icon">${getFileIcon(file.type)}</span>
|
|
<span class="placeholder-text">${file.type.toUpperCase()} FILE</span>
|
|
</div>
|
|
`;
|
|
|
|
// Update meta
|
|
document.getElementById('meta-filename').textContent = file.name;
|
|
document.getElementById('meta-type').textContent = file.type.toUpperCase();
|
|
document.getElementById('meta-size').textContent = formatBytes(file.size);
|
|
document.getElementById('meta-created').textContent = formatDate(file.date);
|
|
document.getElementById('meta-modified').textContent = formatDate(file.date);
|
|
document.getElementById('meta-checksum').textContent = file.checksum;
|
|
}
|
|
|
|
function closePreview() {
|
|
state.selectedFile = null;
|
|
document.querySelectorAll('.file-item').forEach(item => {
|
|
item.classList.remove('selected');
|
|
});
|
|
els.previewContent.innerHTML = `
|
|
<div class="preview-placeholder">
|
|
<span class="placeholder-icon">■</span>
|
|
<span class="placeholder-text">SELECT A FILE TO PREVIEW</span>
|
|
</div>
|
|
`;
|
|
document.getElementById('meta-filename').textContent = '---';
|
|
document.getElementById('meta-type').textContent = '---';
|
|
document.getElementById('meta-size').textContent = '---';
|
|
document.getElementById('meta-created').textContent = '---';
|
|
document.getElementById('meta-modified').textContent = '---';
|
|
document.getElementById('meta-checksum').textContent = '---';
|
|
els.footerStatus.textContent = 'READY';
|
|
}
|
|
|
|
// ========================================
|
|
// VIEW SWITCHING
|
|
// ========================================
|
|
function switchView(view) {
|
|
state.currentView = view;
|
|
|
|
// Update buttons
|
|
document.querySelectorAll('.view-btn').forEach(btn => {
|
|
btn.classList.toggle('active', btn.dataset.view === view);
|
|
});
|
|
|
|
// Update list class
|
|
if (view === 'grid') {
|
|
els.fileList.classList.add('grid-view');
|
|
els.fileListHeader.style.display = 'none';
|
|
} else {
|
|
els.fileList.classList.remove('grid-view');
|
|
els.fileListHeader.style.display = 'grid';
|
|
}
|
|
|
|
renderFiles();
|
|
}
|
|
|
|
// ========================================
|
|
// EVENT LISTENERS
|
|
// ========================================
|
|
function bindEvents() {
|
|
// Load button
|
|
els.loadBtn.addEventListener('click', startLoadingSequence);
|
|
|
|
// Sidebar filters
|
|
els.sidebarMenu.addEventListener('click', (e) => {
|
|
const item = e.target.closest('.sidebar-item');
|
|
if (!item) return;
|
|
|
|
document.querySelectorAll('.sidebar-item').forEach(i => i.classList.remove('active'));
|
|
item.classList.add('active');
|
|
|
|
state.currentFilter = item.dataset.filter;
|
|
filterAndSortFiles();
|
|
renderFiles();
|
|
});
|
|
|
|
// View switcher
|
|
document.querySelectorAll('.view-btn').forEach(btn => {
|
|
btn.addEventListener('click', () => switchView(btn.dataset.view));
|
|
});
|
|
|
|
// Sort
|
|
els.sortSelect.addEventListener('change', (e) => {
|
|
state.currentSort = e.target.value;
|
|
filterAndSortFiles();
|
|
renderFiles();
|
|
});
|
|
|
|
// Search
|
|
els.searchInput.addEventListener('input', (e) => {
|
|
state.searchQuery = e.target.value;
|
|
filterAndSortFiles();
|
|
renderFiles();
|
|
});
|
|
|
|
// Preview close
|
|
els.previewClose.addEventListener('click', closePreview);
|
|
|
|
// Download button
|
|
els.actionDownload.addEventListener('click', () => {
|
|
if (!state.selectedFile) return;
|
|
|
|
const blob = new Blob([`File: ${state.selectedFile.name}\nSize: ${formatBytes(state.selectedFile.size)}\nChecksum: ${state.selectedFile.checksum}`], { type: 'text/plain' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = state.selectedFile.name + '.info.txt';
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
|
|
els.footerStatus.textContent = 'DOWNLOADED INFO FILE';
|
|
setTimeout(() => {
|
|
els.footerStatus.textContent = 'READY';
|
|
}, 2000);
|
|
});
|
|
|
|
// Copy hash button
|
|
els.actionCopy.addEventListener('click', () => {
|
|
if (!state.selectedFile) return;
|
|
|
|
navigator.clipboard.writeText(state.selectedFile.checksum).then(() => {
|
|
els.footerStatus.textContent = 'CHECKSUM COPIED TO CLIPBOARD';
|
|
setTimeout(() => {
|
|
els.footerStatus.textContent = 'READY';
|
|
}, 2000);
|
|
}).catch(() => {
|
|
els.footerStatus.textContent = 'COPY FAILED';
|
|
});
|
|
});
|
|
}
|
|
|
|
// ========================================
|
|
// CLOCK UPDATES
|
|
// ========================================
|
|
function updateClock() {
|
|
const now = new Date();
|
|
const dateStr = now.toISOString().slice(0, 19).replace('T', ' ');
|
|
|
|
if (els.loadingDatetime) {
|
|
els.loadingDatetime.textContent = dateStr;
|
|
}
|
|
if (els.headerTime) {
|
|
els.headerTime.textContent = dateStr;
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// INITIALIZATION
|
|
// ========================================
|
|
function init() {
|
|
cacheElements();
|
|
|
|
// Title scramble effect
|
|
const scrambler = new TextScramble(els.loadingTitle);
|
|
setTimeout(() => {
|
|
scrambler.setText('CANNIBAL GIRLS');
|
|
}, 500);
|
|
|
|
// Clock
|
|
updateClock();
|
|
setInterval(updateClock, 1000);
|
|
|
|
// Bind events
|
|
bindEvents();
|
|
|
|
console.log('%c CANNIBAL GIRLS // ARCHIVE TERMINAL v2.4.1 ', 'background: #0F0D0D; color: #E85D75; font-family: monospace; padding: 4px 8px;');
|
|
console.log('%c SYSTEM ONLINE ', 'background: #181414; color: #4A7C59; font-family: monospace; padding: 2px 6px;');
|
|
}
|
|
|
|
// Start when DOM is ready
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
} else {
|
|
init();
|
|
}
|
|
})();
|