initial commit , first one was bullshit
This commit is contained in:
10
.gitattributes
vendored
Normal file
10
.gitattributes
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
*.css linguist-vendored
|
||||
*.js linguist-vendored
|
||||
*.json linguist-vendored
|
||||
*.html linguist-vendored
|
||||
*.xml linguist-vendored
|
||||
*.md linguist-documentation
|
||||
*.txt linguist-documentation
|
||||
*.properties linguist-documentation
|
||||
*.yml linguist-documentation
|
||||
*.yaml linguist-documentation
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -24,3 +24,7 @@
|
||||
hs_err_pid*
|
||||
replay_pid*
|
||||
|
||||
target
|
||||
|
||||
*.env
|
||||
|
||||
|
||||
62
pom.xml
vendored
Normal file
62
pom.xml
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>com.filejet</groupId>
|
||||
<artifactId>filejet</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.github.cdimascio</groupId>
|
||||
<artifactId>dotenv-java</artifactId>
|
||||
<version>3.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.javalin</groupId>
|
||||
<artifactId>javalin</artifactId>
|
||||
<version>6.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
<version>2.17.2</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-simple</artifactId>
|
||||
<version>2.0.16</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.ben-manes.caffeine</groupId>
|
||||
<artifactId>caffeine</artifactId>
|
||||
<version>3.2.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
<properties>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
</properties>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>exec-maven-plugin</artifactId>
|
||||
<version>3.5.1</version>
|
||||
<configuration>
|
||||
<mainClass>com.filejet.API.Server</mainClass>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
29
src/main/java/com/filejet/API/Server.java
Normal file
29
src/main/java/com/filejet/API/Server.java
Normal file
@@ -0,0 +1,29 @@
|
||||
package com.filejet.API;
|
||||
// server logic
|
||||
|
||||
import com.filejet.API.routes.DownloadRoute;
|
||||
import com.filejet.API.util.EnvLoader;
|
||||
import io.javalin.Javalin;
|
||||
|
||||
|
||||
public class Server {
|
||||
public static void main(String[] args) {
|
||||
Javalin app = Javalin.create(config -> {
|
||||
config.staticFiles.add("/HTML/");
|
||||
}).start(EnvLoader.getPort());
|
||||
|
||||
app.get("/api/download", DownloadRoute::handle); // for ?path=... downloads
|
||||
app.get("/api/download/{file}", DownloadRoute::handle); // legacy/compat
|
||||
app.get("/api/list-isos", DownloadRoute::list);
|
||||
|
||||
app.before("/api/*", ctx -> {
|
||||
String ua = ctx.header("User-Agent");
|
||||
if (ua == null || ua.toLowerCase().contains("bot")
|
||||
|| ua.toLowerCase().contains("curl")
|
||||
) {
|
||||
ctx.status(403).result("Forbidden");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
150
src/main/java/com/filejet/API/routes/DownloadRoute.java
Normal file
150
src/main/java/com/filejet/API/routes/DownloadRoute.java
Normal file
@@ -0,0 +1,150 @@
|
||||
package com.filejet.API.routes;
|
||||
|
||||
import io.javalin.http.Context;
|
||||
import com.filejet.API.util.EnvLoader;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
|
||||
public class DownloadRoute {
|
||||
// Use resource path for ISO_DIR
|
||||
private static final String isoDir = EnvLoader.getISODir();
|
||||
private static final AtomicInteger activeDownloaders = new AtomicInteger(0);
|
||||
|
||||
// Caffeine cache: key = path + '|' + searchTerm, value = List<Map<String, Object>>
|
||||
private static final Cache<String, List<Map<String, Object>>> searchCache =
|
||||
Caffeine.newBuilder().maximumSize(100).expireAfterWrite(java.time.Duration.ofMinutes(10)).build();
|
||||
|
||||
public static void list(Context ctx) {
|
||||
// List files and folders in the given path (relative to ISO_DIR), or search recursively if 'search' param is present
|
||||
String relPath = ctx.queryParam("path");
|
||||
if (relPath == null || relPath.isEmpty()) relPath = "/";
|
||||
String searchTerm = ctx.queryParam("search");
|
||||
try {
|
||||
var classLoader = Thread.currentThread().getContextClassLoader();
|
||||
String fullPath = getFullPath(relPath);
|
||||
var resource = classLoader.getResource(fullPath);
|
||||
if (resource == null) {
|
||||
ctx.status(404).json(new String[]{"Directory not found"});
|
||||
return;
|
||||
// Helper: construct the full resource path from isoDir and relPath
|
||||
private static String getFullPath(String relPath) {
|
||||
String basePath = isoDir.endsWith("/") ? isoDir : isoDir + "/";
|
||||
return basePath + (relPath.startsWith("/") ? relPath.substring(1) : relPath);
|
||||
}
|
||||
}
|
||||
File dir = new File(resource.toURI());
|
||||
if (!dir.exists() || !dir.isDirectory()) {
|
||||
ctx.status(404).json(new String[]{"Not a directory"});
|
||||
return;
|
||||
}
|
||||
|
||||
List<Map<String, Object>> result;
|
||||
if (searchTerm != null && !searchTerm.isEmpty()) {
|
||||
String cacheKey = relPath + "|" + searchTerm.toLowerCase();
|
||||
result = searchCache.getIfPresent(cacheKey);
|
||||
if (result == null) {
|
||||
result = new ArrayList<>();
|
||||
recursiveSearch(dir, relPath, searchTerm.toLowerCase(), result);
|
||||
searchCache.put(cacheKey, result);
|
||||
}
|
||||
} else {
|
||||
File[] files = dir.listFiles();
|
||||
if (files == null) {
|
||||
ctx.json(new Object[0]);
|
||||
return;
|
||||
}
|
||||
result = new ArrayList<>();
|
||||
for (File f : files) {
|
||||
var entry = new HashMap<String, Object>();
|
||||
entry.put("name", f.getName());
|
||||
entry.put("isDir", f.isDirectory());
|
||||
result.add(entry);
|
||||
}
|
||||
}
|
||||
ctx.json(result);
|
||||
} catch (Exception e) {
|
||||
ctx.status(500).json(new String[]{"Error: " + e.getMessage()});
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: recursively search for files/folders containing searchTerm, add to result with relative path
|
||||
private static void recursiveSearch(File dir, String relPath, String searchTerm, List<Map<String, Object>> result) {
|
||||
File[] files = dir.listFiles();
|
||||
if (files == null) return;
|
||||
for (File f : files) {
|
||||
String rel = relPath.endsWith("/") ? relPath + f.getName() : relPath + "/" + f.getName();
|
||||
if (f.getName().toLowerCase().contains(searchTerm)) {
|
||||
var entry = new HashMap<String, Object>();
|
||||
entry.put("name", f.getName());
|
||||
entry.put("isDir", f.isDirectory());
|
||||
entry.put("path", rel);
|
||||
result.add(entry);
|
||||
}
|
||||
if (f.isDirectory()) {
|
||||
recursiveSearch(f, rel, searchTerm, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void handle(Context ctx) throws IOException {
|
||||
// Download a file from a given path (relative to ISO_DIR)
|
||||
String relPath = ctx.queryParam("path");
|
||||
if (relPath == null || relPath.isEmpty()) {
|
||||
ctx.status(400).result("Missing file path");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
var classLoader = Thread.currentThread().getContextClassLoader();
|
||||
String basePath = isoDir.endsWith("/") ? isoDir : isoDir + "/";
|
||||
String fullPath = basePath + (relPath.startsWith("/") ? relPath.substring(1) : relPath);
|
||||
var resource = classLoader.getResource(fullPath);
|
||||
if (resource == null) {
|
||||
ctx.status(404).result("Not Found!");
|
||||
return;
|
||||
}
|
||||
File file = new File(resource.toURI());
|
||||
if (!file.exists() || !file.isFile()) {
|
||||
ctx.status(404).result("Not Found!");
|
||||
return;
|
||||
}
|
||||
int totalMB = EnvLoader.getMaxBandwitdh();
|
||||
int active = activeDownloaders.incrementAndGet();
|
||||
int perClickKB = (totalMB * 1024) / active;
|
||||
try (InputStream fis = new FileInputStream(file);
|
||||
OutputStream os = ctx.res().getOutputStream()) {
|
||||
ctx.res().setHeader("Content-Disposition", "attachment; filename=\"" + file.getName() + "\"");
|
||||
ctx.res().setContentType("application/octet-stream");
|
||||
byte[] buf = new byte[8192];
|
||||
int read;
|
||||
long lastSent = System.nanoTime();
|
||||
long byteSent = 0;
|
||||
int throttleRate = perClickKB * 1024;
|
||||
while ((read = fis.read(buf)) != -1) {
|
||||
os.write(buf, 0, read);
|
||||
byteSent += read;
|
||||
long elapsedNs = System.nanoTime() - lastSent;
|
||||
double elapsedSec = elapsedNs / 1_000_000_000.0;
|
||||
double currentRate = byteSent / elapsedSec;
|
||||
if (currentRate > throttleRate) {
|
||||
long sleepTime = (long) ((byteSent / (double) throttleRate - elapsedSec) * 1000);
|
||||
if (sleepTime > 0) {
|
||||
try {
|
||||
Thread.sleep(sleepTime);
|
||||
} catch (InterruptedException ignored) {
|
||||
// do nothing :3
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
activeDownloaders.decrementAndGet();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
ctx.status(500).result("Error: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
29
src/main/java/com/filejet/API/util/EnvLoader.java
Normal file
29
src/main/java/com/filejet/API/util/EnvLoader.java
Normal file
@@ -0,0 +1,29 @@
|
||||
package com.filejet.API.util;
|
||||
|
||||
// we load the env file from resources here
|
||||
|
||||
import io.github.cdimascio.dotenv.Dotenv;
|
||||
|
||||
public class EnvLoader {
|
||||
//set dotenv up to use
|
||||
private static final Dotenv dotenv = Dotenv.configure()
|
||||
.directory("src/main/resources")
|
||||
.load();
|
||||
|
||||
public static String get(String key) {
|
||||
return dotenv.get(key);
|
||||
}
|
||||
|
||||
public static int getPort() {
|
||||
// return a plain int (ports aren't float)
|
||||
return Integer.parseInt(get("PORT"));
|
||||
}
|
||||
|
||||
public static String getISODir() {
|
||||
return get("ISO_DIR");
|
||||
}
|
||||
|
||||
public static int getMaxBandwitdh() {
|
||||
return Integer.parseInt(get("MAX_TOTAL_BANDWIDTH_MB"));
|
||||
}
|
||||
}
|
||||
9
src/main/resources/Content/info.md
Normal file
9
src/main/resources/Content/info.md
Normal file
@@ -0,0 +1,9 @@
|
||||
store your files here for the webserver to host:
|
||||
|
||||
.env should be in resources and look like this:
|
||||
|
||||
```ruby
|
||||
PORT=8080
|
||||
ISO_DIR=Content/
|
||||
MAX_TOTAL_BANDWIDTH_MB=10
|
||||
```
|
||||
45
src/main/resources/HTML/index.html
vendored
Normal file
45
src/main/resources/HTML/index.html
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<script src="script.js" defer></script>
|
||||
<title>FileJet</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="terminal">
|
||||
<div class="header">
|
||||
<div class="title-bar">-- FileJet --</div>
|
||||
|
||||
<div class="toolbar">
|
||||
<button id="back-btn" class="btn">◀ Back</button>
|
||||
<button id="forward-btn" class="btn">Forward ▶</button>
|
||||
<button id="up-btn" class="btn">↑ Up</button>
|
||||
<input type="text" id="address-input" class="address-bar" placeholder="Address..." readonly>
|
||||
<input type="text" id="search-input" class="search-bar" placeholder="Search...">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-content">
|
||||
<div class="file-header">
|
||||
<div></div>
|
||||
<div>NAME</div>
|
||||
<div>TYPE</div>
|
||||
<div>SIZE</div>
|
||||
<div>MODIFIED</div>
|
||||
<div>ACTION</div>
|
||||
</div>
|
||||
|
||||
<div id="file-list" class="file-list"></div>
|
||||
</div>
|
||||
|
||||
<div class="status-bar">
|
||||
<div id="status-text">Ready</div>
|
||||
<div class="help-text">
|
||||
↑↓: Navigate | ENTER: Open | ESC: Deselect | TAB: Focus controls
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
267
src/main/resources/HTML/script.js
vendored
Normal file
267
src/main/resources/HTML/script.js
vendored
Normal file
@@ -0,0 +1,267 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const fileList = document.getElementById('file-list');
|
||||
const backBtn = document.getElementById('back-btn');
|
||||
const forwardBtn = document.getElementById('forward-btn');
|
||||
const upBtn = document.getElementById('up-btn');
|
||||
const addressInput = document.getElementById('address-input');
|
||||
const searchInput = document.getElementById('search-input');
|
||||
const statusText = document.getElementById('status-text');
|
||||
|
||||
let history = ['/'];
|
||||
let historyIndex = 0;
|
||||
let forwardStack = [];
|
||||
let selectedIndex = -1;
|
||||
let currentItems = [];
|
||||
|
||||
function getCurrentPath() {
|
||||
return history[historyIndex];
|
||||
}
|
||||
|
||||
function setAddressBar() {
|
||||
let p = getCurrentPath();
|
||||
addressInput.value = p === '/' ? 'Computer' : p;
|
||||
}
|
||||
|
||||
function updateSelection() {
|
||||
const items = fileList.querySelectorAll('.file-item');
|
||||
items.forEach((item, index) => {
|
||||
item.classList.toggle('focused', index === selectedIndex);
|
||||
});
|
||||
}
|
||||
|
||||
function selectItem(index) {
|
||||
const items = fileList.querySelectorAll('.file-item');
|
||||
if (index >= 0 && index < items.length) {
|
||||
selectedIndex = index;
|
||||
updateSelection();
|
||||
// Scroll into view
|
||||
items[selectedIndex].scrollIntoView({ block: 'nearest' });
|
||||
|
||||
// Update status
|
||||
if (currentItems[selectedIndex]) {
|
||||
const item = currentItems[selectedIndex];
|
||||
statusText.textContent = `Selected: ${item.name} (${item.isDir ? 'Folder' : 'File'})`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function openSelectedItem() {
|
||||
if (selectedIndex >= 0 && selectedIndex < currentItems.length) {
|
||||
const item = currentItems[selectedIndex];
|
||||
if (item.isDir) {
|
||||
let cur = getCurrentPath();
|
||||
let next = cur.endsWith('/') ? cur + item.name : cur + '/' + item.name;
|
||||
history = history.slice(0, historyIndex + 1);
|
||||
history.push(next);
|
||||
historyIndex++;
|
||||
forwardStack = [];
|
||||
selectedIndex = -1;
|
||||
fetchFiles();
|
||||
} else {
|
||||
// Download file
|
||||
let cur = getCurrentPath();
|
||||
let filePath = cur.endsWith('/') ? cur + item.name : cur + '/' + item.name;
|
||||
window.location.href = `/api/download?path=${encodeURIComponent(filePath)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function fetchFiles(searchTerm = '') {
|
||||
setAddressBar();
|
||||
statusText.textContent = 'Loading...';
|
||||
let path = getCurrentPath();
|
||||
let url = `/api/list-isos?path=${encodeURIComponent(path)}`;
|
||||
if (searchTerm) {
|
||||
url += `&search=${encodeURIComponent(searchTerm)}`;
|
||||
}
|
||||
fetch(url)
|
||||
.then(res => {
|
||||
if (!res.ok) throw new Error('Failed to fetch file list');
|
||||
return res.json();
|
||||
})
|
||||
.then(items => {
|
||||
fileList.innerHTML = '';
|
||||
currentItems = [];
|
||||
selectedIndex = -1;
|
||||
if (!items.length) {
|
||||
fileList.innerHTML = '<div class="file-item" style="grid-column: 1 / -1; text-align: center; color: #888;">No files or folders found.</div>';
|
||||
statusText.textContent = 'No items found';
|
||||
return;
|
||||
}
|
||||
currentItems = items;
|
||||
items.forEach((item, index) => {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'file-item';
|
||||
div.dataset.index = index;
|
||||
|
||||
// Icon
|
||||
const icon = document.createElement('img');
|
||||
icon.className = 'icon';
|
||||
icon.src = item.isDir ? 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTIgM0MxLjQ0NzcyIDMgMSAzLjQ0NzcyIDEgNFYxMkMxIDEyLjU1MjMgMS40NDc3MiAxMyAyIDEzSDE0QzE0LjU1MjMgMTMgMTUgMTIuNTUyMyAxNSAxMlY2QzE1IDUuNDQ3NzIgMTQuNTUyMyA1IDE0IDVIOEw2IDNIMloiIGZpbGw9IiNGRkQ3MDAiLz4KPC9zdmc+' : 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTMgM0MyLjQ0NzcyIDMgMiAzLjQ0NzcyIDIgNFYxMkMyIDEyLjU1MjMgMi40NDc3MiAxMyAzIDEzSDEzQzEzLjU1MjMgMTMgMTQgMTIuNTUyMyAxNCAxMlY0QzE0IDMuNDQ3NzIgMTMuNTUyMyAzIDEzIDNIM1oiIGZpbGw9IiM4ODg4ODgiLz4KPC9zdmc+';
|
||||
icon.onerror = () => icon.style.display = 'none';
|
||||
div.appendChild(icon);
|
||||
|
||||
// Name (show relative path if searching)
|
||||
const nameSpan = document.createElement('span');
|
||||
nameSpan.className = 'file-name';
|
||||
if (searchTerm && item.path) {
|
||||
nameSpan.textContent = item.path;
|
||||
} else {
|
||||
nameSpan.textContent = item.name;
|
||||
}
|
||||
div.appendChild(nameSpan);
|
||||
|
||||
// Type
|
||||
const typeSpan = document.createElement('span');
|
||||
typeSpan.className = 'file-type';
|
||||
typeSpan.textContent = item.isDir ? 'Folder' : 'File';
|
||||
div.appendChild(typeSpan);
|
||||
|
||||
// Size
|
||||
const sizeSpan = document.createElement('span');
|
||||
sizeSpan.className = 'file-size';
|
||||
sizeSpan.textContent = item.isDir ? '' : '-';
|
||||
div.appendChild(sizeSpan);
|
||||
|
||||
// Date Modified
|
||||
const dateSpan = document.createElement('span');
|
||||
dateSpan.className = 'file-date';
|
||||
dateSpan.textContent = '';
|
||||
div.appendChild(dateSpan);
|
||||
|
||||
// Action
|
||||
const actionDiv = document.createElement('div');
|
||||
if (!item.isDir) {
|
||||
const dlBtn = document.createElement('button');
|
||||
dlBtn.className = 'download-btn';
|
||||
dlBtn.textContent = 'Download';
|
||||
dlBtn.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
let cur = getCurrentPath();
|
||||
let filePath = cur.endsWith('/') ? cur + item.name : cur + '/' + item.name;
|
||||
window.location.href = `/api/download?path=${encodeURIComponent(filePath)}`;
|
||||
};
|
||||
actionDiv.appendChild(dlBtn);
|
||||
}
|
||||
div.appendChild(actionDiv);
|
||||
|
||||
// Click handlers
|
||||
div.onclick = () => {
|
||||
selectItem(index);
|
||||
if (item.isDir) {
|
||||
setTimeout(() => openSelectedItem(), 200);
|
||||
}
|
||||
};
|
||||
|
||||
fileList.appendChild(div);
|
||||
});
|
||||
|
||||
statusText.textContent = `${items.length} item${items.length !== 1 ? 's' : ''}`;
|
||||
|
||||
// Update button states
|
||||
backBtn.disabled = historyIndex <= 0;
|
||||
forwardBtn.disabled = historyIndex >= history.length - 1;
|
||||
upBtn.disabled = getCurrentPath() === '/';
|
||||
})
|
||||
.catch(err => {
|
||||
fileList.innerHTML = `<div class="file-item" style="grid-column: 1 / -1; text-align: center; color: #d9534f;">${err.message}</div>`;
|
||||
statusText.textContent = 'Error loading files';
|
||||
});
|
||||
}
|
||||
|
||||
// Navigation event handlers
|
||||
backBtn.addEventListener('click', () => {
|
||||
if (historyIndex > 0) {
|
||||
historyIndex--;
|
||||
selectedIndex = -1;
|
||||
fetchFiles();
|
||||
}
|
||||
});
|
||||
|
||||
forwardBtn.addEventListener('click', () => {
|
||||
if (historyIndex < history.length - 1) {
|
||||
historyIndex++;
|
||||
selectedIndex = -1;
|
||||
fetchFiles();
|
||||
}
|
||||
});
|
||||
|
||||
upBtn.addEventListener('click', () => {
|
||||
let cur = getCurrentPath();
|
||||
if (cur === '/' || cur === '') return;
|
||||
let parts = cur.split('/').filter(Boolean);
|
||||
parts.pop();
|
||||
let upPath = '/' + parts.join('/');
|
||||
if (upPath === '') upPath = '/';
|
||||
history = history.slice(0, historyIndex + 1);
|
||||
history.push(upPath);
|
||||
historyIndex++;
|
||||
selectedIndex = -1;
|
||||
fetchFiles();
|
||||
});
|
||||
|
||||
addressInput.addEventListener('click', () => {
|
||||
addressInput.select();
|
||||
});
|
||||
|
||||
searchInput.addEventListener('input', (e) => {
|
||||
selectedIndex = -1;
|
||||
fetchFiles(e.target.value);
|
||||
});
|
||||
|
||||
// Keyboard navigation
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.target.tagName === 'INPUT') return;
|
||||
|
||||
switch(e.key) {
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
if (selectedIndex > 0) {
|
||||
selectItem(selectedIndex - 1);
|
||||
} else if (currentItems.length > 0) {
|
||||
selectItem(currentItems.length - 1);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
if (selectedIndex < currentItems.length - 1) {
|
||||
selectItem(selectedIndex + 1);
|
||||
} else if (currentItems.length > 0) {
|
||||
selectItem(0);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'Enter':
|
||||
e.preventDefault();
|
||||
openSelectedItem();
|
||||
break;
|
||||
|
||||
case 'Escape':
|
||||
e.preventDefault();
|
||||
selectedIndex = -1;
|
||||
updateSelection();
|
||||
statusText.textContent = `${currentItems.length} item${currentItems.length !== 1 ? 's' : ''}`;
|
||||
break;
|
||||
|
||||
case 'Backspace':
|
||||
e.preventDefault();
|
||||
upBtn.click();
|
||||
break;
|
||||
|
||||
case 'F5':
|
||||
e.preventDefault();
|
||||
selectedIndex = -1;
|
||||
fetchFiles(searchInput.value);
|
||||
break;
|
||||
|
||||
case '/':
|
||||
e.preventDefault();
|
||||
searchInput.focus();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// Initial load
|
||||
fetchFiles();
|
||||
});
|
||||
241
src/main/resources/HTML/styles.css
vendored
Normal file
241
src/main/resources/HTML/styles.css
vendored
Normal file
@@ -0,0 +1,241 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #000;
|
||||
color: #eee;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.terminal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background: #000;
|
||||
border: 2px solid #333;
|
||||
padding: 20px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.header {
|
||||
border-bottom: 1px solid #333;
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.title-bar {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background: #222;
|
||||
color: #fff;
|
||||
border: 1px solid #444;
|
||||
padding: 6px 12px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.btn:hover:not(:disabled) {
|
||||
background: #444;
|
||||
border-color: #666;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn:focus {
|
||||
outline: 1px solid #fff;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.address-bar,
|
||||
.search-bar {
|
||||
background: #111;
|
||||
color: #fff;
|
||||
border: 1px solid #333;
|
||||
padding: 6px 10px;
|
||||
font-size: 13px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.address-bar {
|
||||
flex: 1;
|
||||
min-width: 240px;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
width: 160px;
|
||||
}
|
||||
|
||||
.address-bar:focus,
|
||||
.search-bar:focus {
|
||||
outline: 1px solid #fff;
|
||||
background: #222;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.file-header {
|
||||
display: grid;
|
||||
grid-template-columns: 30px 1fr 80px 100px 120px 100px;
|
||||
gap: 12px;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #333;
|
||||
font-weight: bold;
|
||||
background: #111;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.file-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
background: #000;
|
||||
border: 1px solid #333;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
display: grid;
|
||||
grid-template-columns: 30px 1fr 80px 100px 120px 100px;
|
||||
gap: 12px;
|
||||
padding: 8px 0;
|
||||
background: #181818;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.file-item:hover {
|
||||
background: #111;
|
||||
}
|
||||
|
||||
.file-item.selected {
|
||||
background: #222;
|
||||
}
|
||||
|
||||
.file-item.focused {
|
||||
background: #333;
|
||||
outline: 1px solid #fff;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.file-type,
|
||||
.file-size,
|
||||
.file-date {
|
||||
font-size: 13px;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.download-btn {
|
||||
background: #222;
|
||||
color: #fff;
|
||||
border: 1px solid #555;
|
||||
padding: 3px 8px;
|
||||
font-size: 12px;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.download-btn:hover {
|
||||
background: #444;
|
||||
}
|
||||
|
||||
.status-bar {
|
||||
border-top: 1px solid #333;
|
||||
padding: 8px 0;
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.help-text {
|
||||
font-size: 11px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
/* Scrollbar styling */
|
||||
.file-list::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
.file-list::-webkit-scrollbar-track {
|
||||
background: #111;
|
||||
}
|
||||
|
||||
.file-list::-webkit-scrollbar-thumb {
|
||||
background: #333;
|
||||
border: 1px solid #555;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.file-list::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
/* Terminal cursor effect */
|
||||
@keyframes blink {
|
||||
0%, 50% { opacity: 1; }
|
||||
51%, 100% { opacity: 0; }
|
||||
}
|
||||
|
||||
.cursor::after {
|
||||
content: '█';
|
||||
color: #fff;
|
||||
animation: blink 1s infinite;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.file-header,
|
||||
.file-item {
|
||||
grid-template-columns: 24px 1fr 60px 80px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.file-header :nth-child(5),
|
||||
.file-header :nth-child(6),
|
||||
.file-item :nth-child(5),
|
||||
.file-item :nth-child(6) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user