initial
This commit is contained in:
commit
bbf28141e2
61 changed files with 2576 additions and 0 deletions
25
src/main/java/org/adrianvictor/livingroom/Logger.java
Normal file
25
src/main/java/org/adrianvictor/livingroom/Logger.java
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
package org.adrianvictor.livingroom;
|
||||
|
||||
public class Logger {
|
||||
public static final String ANSI_RESET = "\u001B[0m";
|
||||
public static final String ANSI_BLACK = "\u001B[30m";
|
||||
public static final String ANSI_RED = "\u001B[31m";
|
||||
public static final String ANSI_GREEN = "\u001B[32m";
|
||||
public static final String ANSI_YELLOW = "\u001B[33m";
|
||||
public static final String ANSI_BLUE = "\u001B[34m";
|
||||
public static final String ANSI_PURPLE = "\u001B[35m";
|
||||
public static final String ANSI_CYAN = "\u001B[36m";
|
||||
public static final String ANSI_WHITE = "\u001B[37m";
|
||||
|
||||
public static void info(String t) {
|
||||
System.out.println(ANSI_BLUE + "[Info] " + ANSI_RESET + t);
|
||||
}
|
||||
|
||||
public static void warning(String t) {
|
||||
System.out.println(ANSI_YELLOW + "[Warning] " + ANSI_RESET + t);
|
||||
}
|
||||
|
||||
public static void error(String t) {
|
||||
System.out.println(ANSI_RED + "[Error] " + ANSI_RESET + t);
|
||||
}
|
||||
}
|
||||
49
src/main/java/org/adrianvictor/livingroom/Main.java
Normal file
49
src/main/java/org/adrianvictor/livingroom/Main.java
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
package org.adrianvictor.livingroom;
|
||||
|
||||
import org.adrianvictor.livingroom.config.AppConfig;
|
||||
import org.adrianvictor.livingroom.config.ConfigLoader;
|
||||
import org.adrianvictor.livingroom.http.Handler;
|
||||
import org.adrianvictor.livingroom.http.Handlers;
|
||||
import org.adrianvictor.livingroom.http.Server;
|
||||
import org.adrianvictor.livingroom.data.Indexer;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Map;
|
||||
|
||||
public class Main {
|
||||
private static String directoryPath;
|
||||
private static final Indexer indexer = Indexer.getInstance();
|
||||
private static AppConfig config;
|
||||
private static String configPath;
|
||||
|
||||
public static void main(String[] args) {
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
if ("--config".equals(args[i]) && i + 1 < args.length) {
|
||||
configPath = args[i + 1];
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
config = ConfigLoader.load("config.json");
|
||||
config.hashPlaintextPasswords();
|
||||
} catch (Exception e) {
|
||||
Logger.error("Error loading config: " + e.getMessage());
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
directoryPath = config.getLibrary();
|
||||
Server server = Server.getInstance();
|
||||
indexer.scanAsync(new File(directoryPath));
|
||||
indexer.collectGarbage();
|
||||
|
||||
for (Map.Entry<Handler, String> e : Handlers.getAll().entrySet()) {
|
||||
server.registerHandler(e.getKey(), e.getValue());
|
||||
}
|
||||
|
||||
server.start();
|
||||
}
|
||||
|
||||
public static AppConfig getConfig() {
|
||||
return config;
|
||||
}
|
||||
}
|
||||
6
src/main/java/org/adrianvictor/livingroom/auth/Role.java
Normal file
6
src/main/java/org/adrianvictor/livingroom/auth/Role.java
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
package org.adrianvictor.livingroom.auth;
|
||||
|
||||
public enum Role {
|
||||
USER,
|
||||
ADMIN
|
||||
}
|
||||
34
src/main/java/org/adrianvictor/livingroom/auth/Session.java
Normal file
34
src/main/java/org/adrianvictor/livingroom/auth/Session.java
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
package org.adrianvictor.livingroom.auth;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class Session {
|
||||
private final String sessionId;
|
||||
private final String username;
|
||||
private final long createdAt;
|
||||
private long lastAccessedAt;
|
||||
private static final long SESSION_TIMEOUT = 86400000; // 24 hours in milliseconds
|
||||
|
||||
public Session(String username) {
|
||||
this.sessionId = UUID.randomUUID().toString();
|
||||
this.username = username;
|
||||
this.createdAt = System.currentTimeMillis();
|
||||
this.lastAccessedAt = createdAt;
|
||||
}
|
||||
|
||||
public String getSessionId() {
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void updateLastAccessed() {
|
||||
this.lastAccessedAt = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public boolean isExpired() {
|
||||
return System.currentTimeMillis() - lastAccessedAt > SESSION_TIMEOUT;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package org.adrianvictor.livingroom.auth;
|
||||
|
||||
import org.adrianvictor.livingroom.Main;
|
||||
import org.adrianvictor.livingroom.services.UserService;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class SessionManager {
|
||||
private static final SessionManager instance = new SessionManager();
|
||||
private final Map<String, Session> sessions = new HashMap<>();
|
||||
|
||||
private SessionManager() {}
|
||||
|
||||
public static SessionManager getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
public Session createSession(String username) {
|
||||
Session session = new Session(username);
|
||||
sessions.put(session.getSessionId(), session);
|
||||
return session;
|
||||
}
|
||||
|
||||
public Session getSession(String sessionId) {
|
||||
Session session = sessions.get(sessionId);
|
||||
if (session != null && !session.isExpired()) {
|
||||
session.updateLastAccessed();
|
||||
return session;
|
||||
}
|
||||
if (session != null) {
|
||||
sessions.remove(sessionId);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void destroySession(String sessionId) {
|
||||
sessions.remove(sessionId);
|
||||
}
|
||||
}
|
||||
9
src/main/java/org/adrianvictor/livingroom/auth/User.java
Normal file
9
src/main/java/org/adrianvictor/livingroom/auth/User.java
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
package org.adrianvictor.livingroom.auth;
|
||||
|
||||
import org.adrianvictor.livingroom.utils.PasswordUtils;
|
||||
|
||||
public record User(String username, String hashedPassword, String salt, Role role) {
|
||||
public boolean auth(String password) {
|
||||
return PasswordUtils.verifyPassword(password, salt, hashedPassword);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
package org.adrianvictor.livingroom.config;
|
||||
|
||||
import org.adrianvictor.livingroom.Logger;
|
||||
import org.adrianvictor.livingroom.utils.PasswordUtils;
|
||||
import org.json.simple.JSONObject;
|
||||
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class AppConfig {
|
||||
private String library;
|
||||
private int port;
|
||||
private Map<String, UserConfig> users;
|
||||
private final String path;
|
||||
|
||||
public AppConfig(String path) {
|
||||
this.path = path;
|
||||
this.users = new HashMap<>();
|
||||
}
|
||||
|
||||
public String getLibrary() { return library; }
|
||||
public void setLibrary(String library) { this.library = library; }
|
||||
|
||||
public int getPort() { return port; }
|
||||
public void setPort(int port) { this.port = port; }
|
||||
|
||||
public Map<String, UserConfig> getUsers() { return users; }
|
||||
public void setUsers(Map<String, UserConfig> users) {
|
||||
this.users = (users != null) ? users : new HashMap<>();
|
||||
}
|
||||
|
||||
public void hashPlaintextPasswords() {
|
||||
if (users == null) return; // safety check
|
||||
for (Map.Entry<String, UserConfig> entry : users.entrySet()) {
|
||||
Logger.info("Hashing user password for %s.".formatted(entry.getKey()));
|
||||
UserConfig user = entry.getValue();
|
||||
if (user.getHash() == null && user.getPassword() != null) {
|
||||
String salt = PasswordUtils.generateSalt();
|
||||
String hash = PasswordUtils.hashPassword(user.getPassword(), salt);
|
||||
|
||||
user.setSalt(salt);
|
||||
user.setHash(hash);
|
||||
user.setPassword(null); // clear plaintext
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public void saveConfig() throws IOException {
|
||||
JSONObject json = new JSONObject();
|
||||
json.put("library", getLibrary());
|
||||
json.put("port", getPort());
|
||||
|
||||
JSONObject usersJson = new JSONObject();
|
||||
if (users != null) {
|
||||
for (Map.Entry<String, UserConfig> entry : users.entrySet()) {
|
||||
String username = entry.getKey();
|
||||
UserConfig user = entry.getValue();
|
||||
|
||||
JSONObject userJson = new JSONObject();
|
||||
userJson.put("role", user.getRole());
|
||||
userJson.put("salt", user.getSalt());
|
||||
userJson.put("hash", user.getHash());
|
||||
usersJson.put(username, userJson);
|
||||
}
|
||||
}
|
||||
json.put("users", usersJson);
|
||||
|
||||
try (FileWriter writer = new FileWriter(path)) {
|
||||
writer.write(json.toJSONString());
|
||||
}
|
||||
}
|
||||
|
||||
public static class UserConfig {
|
||||
private String role;
|
||||
private String password;
|
||||
private String salt;
|
||||
private String hash;
|
||||
|
||||
public String getPassword() { return password; }
|
||||
public void setPassword(String password) { this.password = password; }
|
||||
|
||||
public String getRole() { return role; }
|
||||
public void setRole(String role) { this.role = role; }
|
||||
|
||||
public String getSalt() { return salt; }
|
||||
public void setSalt(String salt) { this.salt = salt; }
|
||||
|
||||
public String getHash() { return hash; }
|
||||
public void setHash(String hash) { this.hash = hash; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
package org.adrianvictor.livingroom.config;
|
||||
|
||||
import org.json.simple.JSONObject;
|
||||
import org.json.simple.parser.JSONParser;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.io.FileWriter;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class ConfigLoader {
|
||||
|
||||
private static AppConfig config;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static AppConfig load(String path) throws Exception {
|
||||
if (config != null) return config; // singleton
|
||||
|
||||
File file = new File(path);
|
||||
AppConfig appConfig;
|
||||
|
||||
if (!file.exists()) {
|
||||
appConfig = createDefaultConfig(path);
|
||||
|
||||
saveConfig(appConfig, path);
|
||||
} else {
|
||||
JSONParser parser = new JSONParser();
|
||||
JSONObject obj = (JSONObject) parser.parse(new FileReader(path));
|
||||
|
||||
appConfig = new AppConfig(path);
|
||||
appConfig.setLibrary((String) obj.get("library"));
|
||||
|
||||
// port may be a long in org.json.simple
|
||||
Object portObj = obj.get("port");
|
||||
if (portObj instanceof Long) {
|
||||
appConfig.setPort(((Long) portObj).intValue());
|
||||
} else if (portObj instanceof String) {
|
||||
appConfig.setPort(Integer.parseInt((String) portObj));
|
||||
} else {
|
||||
throw new IllegalStateException("Invalid port value in config");
|
||||
}
|
||||
|
||||
// users
|
||||
JSONObject usersObj = (JSONObject) obj.get("users");
|
||||
Map<String, AppConfig.UserConfig> users = new HashMap<>();
|
||||
for (Object key : usersObj.keySet()) {
|
||||
String username = (String) key;
|
||||
JSONObject userJson = (JSONObject) usersObj.get(username);
|
||||
|
||||
AppConfig.UserConfig user = new AppConfig.UserConfig();
|
||||
user.setRole((String) userJson.get("role"));
|
||||
user.setPassword((String) userJson.get("password"));
|
||||
|
||||
users.put(username, user);
|
||||
}
|
||||
appConfig.setUsers(users);
|
||||
}
|
||||
|
||||
validate(appConfig);
|
||||
|
||||
config = appConfig;
|
||||
return config;
|
||||
}
|
||||
|
||||
private static AppConfig createDefaultConfig(String path) {
|
||||
AppConfig defaultConfig = new AppConfig(path);
|
||||
defaultConfig.setLibrary("/srv/games");
|
||||
defaultConfig.setPort(8080);
|
||||
|
||||
Map<String, AppConfig.UserConfig> users = new HashMap<>();
|
||||
AppConfig.UserConfig admin = new AppConfig.UserConfig();
|
||||
admin.setRole("admin");
|
||||
admin.setPassword("admin");
|
||||
users.put("admin", admin);
|
||||
|
||||
defaultConfig.setUsers(users);
|
||||
return defaultConfig;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static void saveConfig(AppConfig appConfig, String path) throws Exception {
|
||||
JSONObject obj = new JSONObject();
|
||||
obj.put("library", appConfig.getLibrary());
|
||||
obj.put("port", appConfig.getPort());
|
||||
|
||||
JSONObject usersObj = new JSONObject();
|
||||
for (Map.Entry<String, AppConfig.UserConfig> entry : appConfig.getUsers().entrySet()) {
|
||||
JSONObject userJson = new JSONObject();
|
||||
userJson.put("role", entry.getValue().getRole());
|
||||
userJson.put("password", entry.getValue().getPassword());
|
||||
usersObj.put(entry.getKey(), userJson);
|
||||
}
|
||||
obj.put("users", usersObj);
|
||||
|
||||
try (FileWriter writer = new FileWriter(path)) {
|
||||
writer.write(obj.toJSONString());
|
||||
}
|
||||
}
|
||||
|
||||
private static void validate(AppConfig appConfig) {
|
||||
if (appConfig.getLibrary() == null || appConfig.getLibrary().isEmpty())
|
||||
throw new IllegalStateException("Library path must be set");
|
||||
if (appConfig.getPort() <= 0)
|
||||
throw new IllegalStateException("Port must be > 0");
|
||||
if (appConfig.getUsers() == null || appConfig.getUsers().isEmpty())
|
||||
throw new IllegalStateException("At least one user must be defined");
|
||||
}
|
||||
|
||||
public static AppConfig get() {
|
||||
if (config == null) throw new IllegalStateException("Config not loaded yet");
|
||||
return config;
|
||||
}
|
||||
}
|
||||
195
src/main/java/org/adrianvictor/livingroom/data/Database.java
Normal file
195
src/main/java/org/adrianvictor/livingroom/data/Database.java
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
package org.adrianvictor.livingroom.data;
|
||||
|
||||
import org.adrianvictor.livingroom.Logger;
|
||||
import org.adrianvictor.livingroom.Main;
|
||||
import org.adrianvictor.livingroom.config.AppConfig;
|
||||
import org.adrianvictor.livingroom.data.catalog.Item;
|
||||
import org.adrianvictor.livingroom.data.catalog.Property;
|
||||
import org.json.simple.JSONObject;
|
||||
import org.json.simple.parser.JSONParser;
|
||||
import org.json.simple.parser.ParseException;
|
||||
|
||||
import java.io.File;
|
||||
import java.sql.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class Database {
|
||||
static String url;
|
||||
static Database instance;
|
||||
|
||||
private Database() {
|
||||
String library = Main.getConfig().getLibrary();
|
||||
File dbFile = new File(library, "livingroom.db");
|
||||
url = "jdbc:sqlite:" + dbFile.getAbsolutePath();
|
||||
Logger.info("Loading DB at %s.".formatted(url));
|
||||
setup();
|
||||
}
|
||||
|
||||
public static Database getInstance() {
|
||||
if (instance == null) {
|
||||
instance = new Database();
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
public void setup() {
|
||||
var sql = """
|
||||
CREATE TABLE IF NOT EXISTS games (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
location TEXT NOT NULL,
|
||||
properties TEXT NOT NULL
|
||||
)
|
||||
""";
|
||||
|
||||
try (var conn = DriverManager.getConnection(url); var stmt = conn.createStatement()) {
|
||||
stmt.execute(sql);
|
||||
} catch (SQLException e) {
|
||||
Logger.error(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void add(Item item) {
|
||||
String sql = "INSERT INTO games(name, location, properties) VALUES(?,?,?)";
|
||||
|
||||
try (var conn = DriverManager.getConnection(url);
|
||||
var pstmt = conn.prepareStatement(sql)) {
|
||||
pstmt.setString(1, item.getPropertiesString().get("name"));
|
||||
pstmt.setString(2, item.location().toString());
|
||||
pstmt.setString(3, item.getPropertiesJSON().toJSONString());
|
||||
pstmt.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
Logger.error(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public void remove(String gameID) {
|
||||
String sql = "DELETE FROM games WHERE id=?";
|
||||
|
||||
try (var conn = DriverManager.getConnection(url);
|
||||
var pstmt = conn.prepareStatement(sql)) {
|
||||
pstmt.setString(1, gameID);
|
||||
pstmt.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
Logger.error(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public void nuke() {
|
||||
String sql = "DROP TABLE IF EXISTS games;";
|
||||
|
||||
try (var conn = DriverManager.getConnection(url);
|
||||
var stmt = conn.createStatement()) {
|
||||
stmt.execute(sql);
|
||||
setup();
|
||||
} catch (SQLException e) {
|
||||
Logger.error(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public boolean has(String name) {
|
||||
String sql = "SELECT id FROM games WHERE name=?";
|
||||
|
||||
try (Connection conn = DriverManager.getConnection(url);
|
||||
PreparedStatement stmt = conn.prepareStatement(sql);) {
|
||||
stmt.setString(1, name);
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
return rs.next();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
Logger.error(e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public Item getGame(int id) {
|
||||
String sql = "SELECT * FROM games WHERE id=?";
|
||||
|
||||
try (Connection conn = DriverManager.getConnection(url);
|
||||
PreparedStatement stmt = conn.prepareStatement(sql);) {
|
||||
stmt.setString(1, String.valueOf(id));
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
List<Item> processed = processResultSet(rs);
|
||||
if (processed.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return processed.getFirst();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
Logger.error(e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public List<Item> getAllGames() {
|
||||
String sql = "SELECT * FROM games";
|
||||
JSONParser parser = new JSONParser();
|
||||
List<Item> result = new ArrayList<>();
|
||||
|
||||
try (Connection conn = DriverManager.getConnection(url);
|
||||
PreparedStatement stmt = conn.prepareStatement(sql);
|
||||
ResultSet rs = stmt.executeQuery()) {
|
||||
result.addAll(processResultSet(rs));
|
||||
} catch (SQLException e) {
|
||||
Logger.error(e.getMessage());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<Item> processResultSet(ResultSet rs) throws SQLException {
|
||||
List<Item> result = new ArrayList<>();
|
||||
JSONParser parser = new JSONParser();
|
||||
while (rs.next()) {
|
||||
long id = rs.getLong("id");
|
||||
String name = rs.getString("name");
|
||||
String location = rs.getString("location");
|
||||
String json = rs.getString("properties");
|
||||
|
||||
JSONObject props;
|
||||
try {
|
||||
props = (JSONObject) parser.parse(json);
|
||||
} catch (ParseException pe) {
|
||||
Logger.error("Failed to parse JSON for game id " + id + ": " + pe.getMessage());
|
||||
continue;
|
||||
}
|
||||
|
||||
props.put("name", name);
|
||||
props.put("id", id);
|
||||
|
||||
File file;
|
||||
|
||||
try {
|
||||
file = new File(location);
|
||||
} catch (Exception e) {
|
||||
Logger.error("Cannot find game path for id " + id + ": " + e.getMessage());
|
||||
continue;
|
||||
}
|
||||
|
||||
HashMap<Property, String> properties = new HashMap<>();
|
||||
|
||||
for (Object propEntry : props.entrySet()) {
|
||||
Map.Entry<?, ?> entry = (Map.Entry<?, ?>) propEntry;
|
||||
|
||||
String value = String.valueOf(entry.getValue());
|
||||
|
||||
try {
|
||||
Property prop = Property.valueOf(entry.getKey().toString().toUpperCase());
|
||||
properties.put(prop, value);
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
Logger.warning("Unknown property: %s".formatted(entry.getKey()));
|
||||
}
|
||||
}
|
||||
|
||||
Item item = new Item(file, properties);
|
||||
result.add(item);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
106
src/main/java/org/adrianvictor/livingroom/data/Indexer.java
Normal file
106
src/main/java/org/adrianvictor/livingroom/data/Indexer.java
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
package org.adrianvictor.livingroom.data;
|
||||
|
||||
import org.adrianvictor.livingroom.Logger;
|
||||
import org.adrianvictor.livingroom.data.catalog.Item;
|
||||
import org.adrianvictor.livingroom.data.catalog.Property;
|
||||
import org.json.simple.JSONArray;
|
||||
import org.json.simple.JSONObject;
|
||||
import org.json.simple.parser.JSONParser;
|
||||
import org.json.simple.parser.ParseException;
|
||||
|
||||
import javax.xml.crypto.Data;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class Indexer {
|
||||
private static Indexer instance;
|
||||
private Indexer() {};
|
||||
|
||||
public static synchronized Indexer getInstance() {
|
||||
if (instance == null) {
|
||||
instance = new Indexer();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
public void scan(File directory) {;
|
||||
File[] files = directory.listFiles();
|
||||
|
||||
for (File f : files) {
|
||||
if (f.isDirectory()) {
|
||||
File[] dir = f.listFiles();
|
||||
for (File file : dir) {
|
||||
if (file.isFile() && file.toPath().toString().endsWith(".json")) {
|
||||
Logger.info("Found json: %s".formatted(file.toPath().getFileName()));
|
||||
try (FileReader fr = new FileReader(file)) {
|
||||
Object obj = new JSONParser().parse(fr);
|
||||
|
||||
if (obj instanceof JSONObject) {
|
||||
processObject((JSONObject) obj, file);
|
||||
} else if (obj instanceof JSONArray) {
|
||||
for (Object element : (JSONArray) obj) {
|
||||
if (element instanceof JSONObject) {
|
||||
processObject((JSONObject) element, file);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
Logger.error("File not found: %s".formatted(file.getPath()) + e);
|
||||
} catch (IOException e) {
|
||||
Logger.error("IO Error reading file: %s".formatted(file.getPath()) + e);
|
||||
} catch (ParseException e) {
|
||||
Logger.error("JSON Parsing error in file: %s".formatted(file.getPath()) + e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void processObject(JSONObject jobj, File file) {
|
||||
HashMap<Property, String> properties = new HashMap<>();
|
||||
Logger.info("Adding %s to catalog.".formatted(file.getPath()));
|
||||
|
||||
for (Object propEntry : jobj.entrySet()) {
|
||||
Map.Entry<?, ?> entry = (Map.Entry<?, ?>) propEntry;
|
||||
|
||||
String value = String.valueOf(entry.getValue());
|
||||
|
||||
try {
|
||||
Property prop = Property.valueOf(entry.getKey().toString().toUpperCase());
|
||||
properties.put(prop, value);
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
Logger.warning("Unknown property: %s".formatted(entry.getKey()));
|
||||
}
|
||||
}
|
||||
|
||||
if (!Database.getInstance().has(properties.get(Property.NAME))) {
|
||||
Database.getInstance().add(new Item(file, properties));
|
||||
}
|
||||
}
|
||||
|
||||
public void collectGarbage() {
|
||||
List<Item> games = Database.getInstance().getAllGames();
|
||||
for (Item game : games) {
|
||||
if (!game.location().exists()) {
|
||||
Logger.warning("Game %s was caught by garbage collection because it's file does not exist anymore at %s."
|
||||
.formatted(game.properties().get(Property.NAME), game.location().getPath())
|
||||
);
|
||||
Database.getInstance().remove(game.properties().get(Property.ID));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void scanAsync(File directory) {
|
||||
Thread asyncThread = new Thread(() -> {
|
||||
scan(directory);
|
||||
});
|
||||
|
||||
asyncThread.start();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
package org.adrianvictor.livingroom.data.catalog;
|
||||
|
||||
import org.adrianvictor.livingroom.Logger;
|
||||
import org.json.simple.JSONObject;
|
||||
import org.json.simple.parser.JSONParser;
|
||||
import org.json.simple.parser.ParseException;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
public record Item(File location, HashMap<Property, String> properties) {
|
||||
public HashMap<String, String> getPropertiesString() {
|
||||
HashMap<String, String> result = new HashMap<>();
|
||||
properties.forEach((key, value) -> {
|
||||
result.put(key.name().toLowerCase(), value);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
public JSONObject getPropertiesJSON() {
|
||||
return new JSONObject(getPropertiesString());
|
||||
}
|
||||
|
||||
public List<String> getVersions() {
|
||||
List<String> result = new ArrayList<>();
|
||||
|
||||
if (getPropertiesString().get("files") == null) {
|
||||
return result;
|
||||
}
|
||||
|
||||
try {
|
||||
JSONParser parser = new JSONParser();
|
||||
JSONObject jsonObject = (JSONObject) parser.parse(getPropertiesString().get("files"));
|
||||
|
||||
for (Object o : jsonObject.keySet()) {
|
||||
if (o instanceof String s) {
|
||||
result.add(s);
|
||||
}
|
||||
}
|
||||
} catch (ParseException e) {
|
||||
Logger.error(e.getMessage());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public String getVersionFile(String version) {
|
||||
if (getPropertiesString().get("files") != null) {
|
||||
try {
|
||||
JSONParser parser = new JSONParser();
|
||||
JSONObject jsonObject = (JSONObject) parser.parse(getPropertiesString().get("files"));
|
||||
JSONObject versionObject = (JSONObject) jsonObject.get(version);
|
||||
return (String) versionObject.get("file");
|
||||
} catch (ParseException e) {
|
||||
Logger.error(e.getMessage());
|
||||
} catch (ClassCastException ignored) {}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package org.adrianvictor.livingroom.data.catalog;
|
||||
|
||||
public enum Property {
|
||||
ID,
|
||||
NAME,
|
||||
PUBLISHER,
|
||||
AUTHOR,
|
||||
YEAR,
|
||||
DATE,
|
||||
DESCRIPTION,
|
||||
MEDIUM,
|
||||
ARTWORK,
|
||||
FILES,
|
||||
OPERATING_SYSTEM;
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package org.adrianvictor.livingroom.http;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
|
||||
public interface Handler {
|
||||
HttpResponse result(String baseAddress, String path, HttpExchange exchange);
|
||||
}
|
||||
21
src/main/java/org/adrianvictor/livingroom/http/Handlers.java
Normal file
21
src/main/java/org/adrianvictor/livingroom/http/Handlers.java
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
package org.adrianvictor.livingroom.http;
|
||||
|
||||
import org.adrianvictor.livingroom.http.handlers.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
public class Handlers {
|
||||
private static HashMap<Handler, String> map = new HashMap<>();
|
||||
|
||||
static {
|
||||
map.put(new CatalogHandler(), "/catalog");
|
||||
map.put(new ImageHandler(), "/pic");
|
||||
map.put(new FileHandler(), "/download");
|
||||
map.put(new WebHandler(), "/web");
|
||||
map.put(new StaticWebHandler(), "/static");
|
||||
}
|
||||
|
||||
public static HashMap<Handler, String> getAll() {
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package org.adrianvictor.livingroom.http;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public record HttpResponse(
|
||||
int status,
|
||||
byte[] body,
|
||||
Map<String, String> headers
|
||||
) {
|
||||
public static HttpResponse ok(byte[] body, String contentType, Map<String, String> headers) {
|
||||
return new HttpResponse(
|
||||
200,
|
||||
body,
|
||||
Map.of("Content-Type", contentType)
|
||||
);
|
||||
}
|
||||
|
||||
public static HttpResponse ok(byte[] body, String contentType) {
|
||||
return ok(body, contentType, new HashMap<>());
|
||||
}
|
||||
|
||||
public static HttpResponse redirect(String location) {
|
||||
return new HttpResponse(
|
||||
302,
|
||||
new byte[0],
|
||||
Map.of("Location", location)
|
||||
);
|
||||
}
|
||||
}
|
||||
114
src/main/java/org/adrianvictor/livingroom/http/Server.java
Normal file
114
src/main/java/org/adrianvictor/livingroom/http/Server.java
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
package org.adrianvictor.livingroom.http;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpHandler;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import org.adrianvictor.livingroom.Logger;
|
||||
import org.adrianvictor.livingroom.Main;
|
||||
import org.adrianvictor.livingroom.http.handlers.FileHandler;
|
||||
import org.adrianvictor.livingroom.http.handlers.ImageHandler;
|
||||
import org.adrianvictor.livingroom.http.handlers.StaticWebHandler;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.Map;
|
||||
|
||||
public class Server {
|
||||
static Server instance;
|
||||
HttpServer httpServer;
|
||||
|
||||
private Server() {
|
||||
try {
|
||||
httpServer = HttpServer.create(new InetSocketAddress(Main.getConfig().getPort()), 0);
|
||||
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static Server getInstance() {
|
||||
if (instance == null) {
|
||||
instance = new Server();
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
public void start() {
|
||||
httpServer.start();
|
||||
}
|
||||
|
||||
public void registerHandler(Handler handler, String path) {
|
||||
httpServer.createContext(path, new HttpHandler() {
|
||||
@Override
|
||||
public void handle(HttpExchange exchange) throws IOException {
|
||||
try {
|
||||
String fullPath = exchange.getRequestURI().getPath();
|
||||
|
||||
String afterBase = "";
|
||||
if (fullPath.length() > path.length()) {
|
||||
afterBase = fullPath.substring(path.length() + 1);
|
||||
}
|
||||
String decoded = java.net.URLDecoder.decode(afterBase, java.nio.charset.StandardCharsets.UTF_8);
|
||||
|
||||
int thirdSlash = nthIndexOf(fullPath, '/', 3);
|
||||
String baseAddress;
|
||||
if (thirdSlash == -1) {
|
||||
baseAddress = fullPath;
|
||||
} else {
|
||||
baseAddress = fullPath.substring(0, thirdSlash);
|
||||
}
|
||||
|
||||
exchange.getResponseHeaders().add("X-Content-Type-Options", "nosniff");
|
||||
exchange.getResponseHeaders().add("X-Frame-Option", "DENY");
|
||||
exchange.getResponseHeaders().add("Referrer-Policy", "no-referrer");
|
||||
exchange.getResponseHeaders().add("Content-Security-Policy", "");
|
||||
exchange.getResponseHeaders().add("Access-Control-Allow-Origin", "*");
|
||||
exchange.getResponseHeaders().add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
|
||||
exchange.getResponseHeaders().add("Access-Control-Allow-Headers", "Content-Type");
|
||||
|
||||
if (handler instanceof FileHandler) {
|
||||
((FileHandler) handler).handleStream(exchange, decoded);
|
||||
} else if (handler instanceof ImageHandler) {
|
||||
((ImageHandler) handler).handleStream(exchange, decoded);
|
||||
} else if (handler instanceof StaticWebHandler) {
|
||||
((StaticWebHandler) handler).handleStream(exchange, decoded);
|
||||
} else {
|
||||
HttpResponse result = handler.result(baseAddress, decoded, exchange);
|
||||
|
||||
for (Map.Entry<String, String> header : result.headers().entrySet()) {
|
||||
exchange.getResponseHeaders().set(header.getKey(), header.getValue());
|
||||
}
|
||||
|
||||
exchange.sendResponseHeaders(result.status(), result.body().length);
|
||||
|
||||
OutputStream os = exchange.getResponseBody();
|
||||
os.write(result.body());
|
||||
os.close();
|
||||
}
|
||||
} catch (IOException ignored) {
|
||||
} catch (Exception e) {
|
||||
Logger.error("Handler error: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
try {
|
||||
exchange.sendResponseHeaders(500, 0);
|
||||
exchange.close();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
} finally {
|
||||
exchange.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static int nthIndexOf(String str, char c, int n) {
|
||||
int pos = -1;
|
||||
for (int i = 0; i < n; i++) {
|
||||
pos = str.indexOf(c, pos + 1);
|
||||
if (pos == -1) break;
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package org.adrianvictor.livingroom.http.handlers;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import org.adrianvictor.livingroom.http.Handler;
|
||||
import org.adrianvictor.livingroom.http.HttpResponse;
|
||||
import org.adrianvictor.livingroom.data.Database;
|
||||
import org.adrianvictor.livingroom.data.catalog.Item;
|
||||
import org.json.simple.JSONArray;
|
||||
import org.json.simple.JSONObject;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record CatalogHandler() implements Handler {
|
||||
@Override
|
||||
public HttpResponse result(String baseAddress, String path, HttpExchange exchange) {
|
||||
List<Item> rawCatalog = Database.getInstance().getAllGames();
|
||||
JSONArray jsonArray = new JSONArray();
|
||||
for (Item item : rawCatalog) {
|
||||
jsonArray.add(new JSONObject(item.getPropertiesString()));
|
||||
}
|
||||
String response = jsonArray.toJSONString();
|
||||
|
||||
return HttpResponse.ok(response.getBytes(), "application/json");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
package org.adrianvictor.livingroom.http.handlers;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import org.adrianvictor.livingroom.http.Handler;
|
||||
import org.adrianvictor.livingroom.http.HttpResponse;
|
||||
import org.adrianvictor.livingroom.Logger;
|
||||
import org.adrianvictor.livingroom.data.Database;
|
||||
import org.adrianvictor.livingroom.data.catalog.Item;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
|
||||
public class FileHandler implements Handler {
|
||||
|
||||
public void handleStream(HttpExchange exchange, String path) {
|
||||
String[] parts = path.split("/");
|
||||
|
||||
if (parts.length < 2) {
|
||||
send404(exchange, "Usage: .../game_id/DownloadName");
|
||||
return;
|
||||
}
|
||||
|
||||
int id;
|
||||
try {
|
||||
id = Integer.parseInt(parts[0]);
|
||||
} catch (NumberFormatException e) {
|
||||
send404(exchange, "The provided game ID %s is not a valid number.".formatted(parts[0]));
|
||||
return;
|
||||
}
|
||||
|
||||
String platform = URLDecoder.decode(parts[1], StandardCharsets.UTF_8);
|
||||
Item game = Database.getInstance().getGame(id);
|
||||
if (game == null) {
|
||||
send404(exchange, "Game not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
List<String> files = game.getVersions();
|
||||
if (files.isEmpty() || !files.contains(platform)) {
|
||||
send404(exchange, "This game has no such download: %s.".formatted(platform));
|
||||
return;
|
||||
}
|
||||
|
||||
File gameFile = new File(game.location().getParent() + '/' + game.getVersionFile(platform));
|
||||
|
||||
if (!gameFile.exists()) {
|
||||
Logger.info(gameFile.getPath());
|
||||
send404(exchange, "Could not find the file you're looking for. Sorry.");
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.info("Providing game file: " + gameFile.getPath());
|
||||
|
||||
exchange.getResponseHeaders().add("Content-Type", contentType());
|
||||
exchange.getResponseHeaders().add(
|
||||
"Content-Disposition",
|
||||
"attachment; filename=\"" + gameFile.getName() + "\""
|
||||
);
|
||||
|
||||
try (FileInputStream fis = new FileInputStream(gameFile);
|
||||
OutputStream os = exchange.getResponseBody()) {
|
||||
exchange.sendResponseHeaders(200, gameFile.length());
|
||||
|
||||
byte[] buffer = new byte[8192]; // 8 KB chunks
|
||||
int bytesRead;
|
||||
while ((bytesRead = fis.read(buffer)) != -1) {
|
||||
os.write(buffer, 0, bytesRead);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
send404(exchange);
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private void send404(HttpExchange exchange, String message) {
|
||||
byte[] notFound = message.getBytes();
|
||||
try {
|
||||
exchange.sendResponseHeaders(404, notFound.length);
|
||||
exchange.getResponseBody().write(notFound);
|
||||
exchange.getResponseBody().close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private void send404(HttpExchange exchange) {
|
||||
send404(exchange, "Not found.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpResponse result(String baseAddress, String path, HttpExchange exchange) {
|
||||
return HttpResponse.ok(new byte[0], "");
|
||||
}
|
||||
|
||||
public String contentType() {
|
||||
return "application/octet-stream";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package org.adrianvictor.livingroom.http.handlers;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import org.adrianvictor.livingroom.http.Handler;
|
||||
import org.adrianvictor.livingroom.http.HttpResponse;
|
||||
import org.adrianvictor.livingroom.data.Database;
|
||||
import org.adrianvictor.livingroom.data.catalog.Item;
|
||||
import org.adrianvictor.livingroom.data.catalog.Property;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
public class ImageHandler implements Handler {
|
||||
public void handleStream(HttpExchange exchange, String path) throws IOException {
|
||||
int id = Integer.parseInt(path);
|
||||
Item game = Database.getInstance().getGame(id);
|
||||
|
||||
if (game.properties().get(Property.ARTWORK) == null || game.properties().get(Property.ARTWORK).isEmpty()) {
|
||||
byte[] notFound = "Not found".getBytes();
|
||||
exchange.sendResponseHeaders(404, notFound.length);
|
||||
exchange.getResponseBody().write(notFound);
|
||||
exchange.getResponseBody().close();
|
||||
return;
|
||||
}
|
||||
|
||||
File image = new File(game.location().getParent() + '/' + game.properties().get(Property.ARTWORK));
|
||||
|
||||
exchange.getResponseHeaders().add("Content-Type", "image/png");
|
||||
exchange.sendResponseHeaders(200, image.length());
|
||||
|
||||
try (FileInputStream fis = new FileInputStream(image)) {
|
||||
byte[] buffer = new byte[8192]; // 8 KB chunks
|
||||
int bytesRead;
|
||||
while ((bytesRead = fis.read(buffer)) != -1) {
|
||||
exchange.getResponseBody().write(buffer, 0, bytesRead);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
exchange.getResponseBody().close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpResponse result(String baseAddress, String path, HttpExchange exchange) {
|
||||
return HttpResponse.ok(new byte[0], "");
|
||||
}
|
||||
|
||||
public String contentType() {
|
||||
return "image/png";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
package org.adrianvictor.livingroom.http.handlers;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import org.adrianvictor.livingroom.http.Handler;
|
||||
import org.adrianvictor.livingroom.http.HttpResponse;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class StaticWebHandler implements Handler {
|
||||
|
||||
private static final String STATIC_ROOT = "static";
|
||||
|
||||
public void handleStream(HttpExchange exchange, String path) throws IOException {
|
||||
if (path.contains("..")) {
|
||||
exchange.sendResponseHeaders(403, -1);
|
||||
return;
|
||||
}
|
||||
|
||||
String resourcePath = "static/" + path.replace("\\", "/");
|
||||
InputStream resourceStream = getClass().getClassLoader().getResourceAsStream(resourcePath);
|
||||
|
||||
if (resourceStream == null) {
|
||||
exchange.sendResponseHeaders(404, -1);
|
||||
return;
|
||||
}
|
||||
|
||||
String contentType;
|
||||
if (path.endsWith(".css")) contentType = "text/css";
|
||||
else if (path.endsWith(".js")) contentType = "application/javascript";
|
||||
else if (path.endsWith(".png")) contentType = "image/png";
|
||||
else if (path.endsWith(".jpg") || path.endsWith(".jpeg")) contentType = "image/jpeg";
|
||||
else contentType = "application/octet-stream";
|
||||
|
||||
exchange.getResponseHeaders().add("Content-Type", contentType);
|
||||
exchange.sendResponseHeaders(200, 0);
|
||||
|
||||
try (resourceStream; OutputStream os = exchange.getResponseBody()) {
|
||||
resourceStream.transferTo(os);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpResponse result(String baseAddress, String path, HttpExchange exchange) {
|
||||
return HttpResponse.ok(new byte[0], "");
|
||||
}
|
||||
|
||||
public String contentType() {
|
||||
return "application/octet-stream";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
package org.adrianvictor.livingroom.http.handlers;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import freemarker.template.Configuration;
|
||||
import org.adrianvictor.livingroom.auth.User;
|
||||
import org.adrianvictor.livingroom.http.Handler;
|
||||
import org.adrianvictor.livingroom.http.HttpResponse;
|
||||
import org.adrianvictor.livingroom.Logger;
|
||||
import org.adrianvictor.livingroom.Main;
|
||||
import org.adrianvictor.livingroom.services.UserService;
|
||||
import org.adrianvictor.livingroom.utils.net.Cookie;
|
||||
import org.adrianvictor.livingroom.web.Page;
|
||||
import org.adrianvictor.livingroom.web.Pages;
|
||||
import org.adrianvictor.livingroom.auth.Session;
|
||||
import org.adrianvictor.livingroom.auth.SessionManager;
|
||||
import org.adrianvictor.livingroom.web.pages.Login;
|
||||
import org.adrianvictor.livingroom.web.pages.NotFound;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class WebHandler implements Handler {
|
||||
|
||||
private final Configuration cfg;
|
||||
|
||||
public WebHandler() {
|
||||
cfg = new Configuration(Configuration.VERSION_2_3_32);
|
||||
cfg.setClassForTemplateLoading(
|
||||
Main.class,
|
||||
"/templates"
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpResponse result(String baseAddress, String path, HttpExchange exchange) {
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
String firstPath = path.split("/")[0];
|
||||
String remainingPath = path.substring(firstPath.length());
|
||||
if (remainingPath.startsWith("/")) {
|
||||
remainingPath = remainingPath.substring(1);
|
||||
}
|
||||
|
||||
data.put("webpref", "web");
|
||||
|
||||
String sessionId = Cookie.getSessionIdFromCookies(exchange);
|
||||
Session session = SessionManager.getInstance().getSession(sessionId);
|
||||
|
||||
if (firstPath.equals("login")) {
|
||||
if ("POST".equalsIgnoreCase(exchange.getRequestMethod())) {
|
||||
return handleLoginPage(baseAddress, remainingPath, exchange);
|
||||
}
|
||||
|
||||
Page page = Pages.get(firstPath);
|
||||
if (page == null) {
|
||||
page = new Login();
|
||||
}
|
||||
return HttpResponse.ok(page.result(cfg, baseAddress, remainingPath, data, exchange).getBytes(), "text/html");
|
||||
}
|
||||
|
||||
if (session == null) {
|
||||
return HttpResponse.ok(new Login().result(cfg, baseAddress, "", data, exchange).getBytes(), "text/html");
|
||||
}
|
||||
|
||||
Page page = Pages.get(firstPath);
|
||||
|
||||
User user = UserService.getInstance().getUser(session.getUsername());
|
||||
|
||||
data.put("username", session.getUsername());
|
||||
data.put("userRole", user.role().toString().toLowerCase());
|
||||
|
||||
if (page == null) {
|
||||
return HttpResponse.ok(new NotFound().result(cfg, baseAddress, path, data, exchange).getBytes(), "text/html");
|
||||
}
|
||||
|
||||
return HttpResponse.ok(page.result(cfg, baseAddress, remainingPath, data, exchange).getBytes(), "text/html");
|
||||
}
|
||||
|
||||
private HttpResponse handleLoginPage(String baseAddress, String path, HttpExchange exchange) {
|
||||
String method = exchange.getRequestMethod();
|
||||
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("webpref", "web");
|
||||
|
||||
if ("POST".equals(method)) {
|
||||
try {
|
||||
// Read POST body
|
||||
String body = new String(exchange.getRequestBody().readAllBytes());
|
||||
String[] params = body.split("&");
|
||||
String username = null;
|
||||
String password = null;
|
||||
|
||||
|
||||
for (String param : params) {
|
||||
String[] kv = param.split("=");
|
||||
if (kv.length == 2) {
|
||||
if ("username".equals(kv[0])) {
|
||||
username = java.net.URLDecoder.decode(kv[1], java.nio.charset.StandardCharsets.UTF_8);
|
||||
} else if ("password".equals(kv[0])) {
|
||||
password = java.net.URLDecoder.decode(kv[1], java.nio.charset.StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (UserService.getInstance().getUser(username).auth(password)) {
|
||||
Session session = SessionManager.getInstance().createSession(username);
|
||||
Cookie.setSessionCookie(exchange, session.getSessionId());
|
||||
|
||||
return HttpResponse.redirect("/web/");
|
||||
} else {
|
||||
return HttpResponse.ok(new Login().result(cfg, baseAddress, "", data, exchange).getBytes(), "text/html");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Logger.error("Login error: " + e.getMessage());
|
||||
return HttpResponse.ok(new NotFound().result(cfg, baseAddress, path, data, exchange).getBytes(), "text/html");
|
||||
}
|
||||
}
|
||||
|
||||
return HttpResponse.ok(new NotFound().result(cfg, baseAddress, path, data, exchange).getBytes(), "text/html");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
package org.adrianvictor.livingroom.services;
|
||||
|
||||
import org.adrianvictor.livingroom.Main;
|
||||
import org.adrianvictor.livingroom.auth.Role;
|
||||
import org.adrianvictor.livingroom.auth.User;
|
||||
import org.adrianvictor.livingroom.config.AppConfig;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class UserService {
|
||||
private static UserService instance;
|
||||
private static Map<String, AppConfig.UserConfig> users;
|
||||
|
||||
private UserService() {
|
||||
users = Main.getConfig().getUsers();
|
||||
}
|
||||
|
||||
public static UserService getInstance() {
|
||||
if (instance == null) {
|
||||
instance = new UserService();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
public User getUser(String user) {
|
||||
AppConfig.UserConfig raw = users.get(user);
|
||||
|
||||
if (user == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Role role;
|
||||
|
||||
try {
|
||||
role = Role.valueOf(raw.getRole());
|
||||
} catch (IllegalArgumentException e) {
|
||||
role = Role.USER;
|
||||
}
|
||||
|
||||
return new User(
|
||||
user,
|
||||
raw.getHash(),
|
||||
raw.getSalt(),
|
||||
role
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package org.adrianvictor.livingroom.utils;
|
||||
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Base64;
|
||||
import javax.crypto.SecretKeyFactory;
|
||||
import javax.crypto.spec.PBEKeySpec;
|
||||
|
||||
public class PasswordUtils {
|
||||
private static final int ITERATIONS = 65536;
|
||||
private static final int KEY_LENGTH = 256;
|
||||
|
||||
public static String generateSalt() {
|
||||
byte[] salt = new byte[16];
|
||||
new SecureRandom().nextBytes(salt);
|
||||
return Base64.getEncoder().encodeToString(salt);
|
||||
}
|
||||
|
||||
public static String hashPassword(String password, String salt) {
|
||||
try {
|
||||
PBEKeySpec spec = new PBEKeySpec(
|
||||
password.toCharArray(),
|
||||
Base64.getDecoder().decode(salt),
|
||||
ITERATIONS,
|
||||
KEY_LENGTH
|
||||
);
|
||||
SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
|
||||
byte[] hash = skf.generateSecret(spec).getEncoded();
|
||||
return Base64.getEncoder().encodeToString(hash);
|
||||
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
|
||||
throw new RuntimeException("Error hashing password", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean verifyPassword(String password, String salt, String expectedHash) {
|
||||
String hash = hashPassword(password, salt);
|
||||
return hash.equals(expectedHash);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package org.adrianvictor.livingroom.utils.image;
|
||||
|
||||
import org.adrianvictor.livingroom.data.catalog.Item;
|
||||
import org.adrianvictor.livingroom.data.catalog.Property;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class ColorExtractor {
|
||||
public static String getDominantColor(File imageFile) throws Exception {
|
||||
BufferedImage image = ImageIO.read(imageFile);
|
||||
|
||||
// Downsample
|
||||
BufferedImage resized = new BufferedImage(150, 150, BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D g2d = resized.createGraphics();
|
||||
g2d.drawImage(image, 0, 0, 150, 150, null);
|
||||
g2d.dispose();
|
||||
|
||||
// Score by saturation, skip neutrals
|
||||
Map<Integer, Double> colorScore = new HashMap<>();
|
||||
for (int y = 0; y < resized.getHeight(); y++) {
|
||||
for (int x = 0; x < resized.getWidth(); x++) {
|
||||
int rgb = resized.getRGB(x, y);
|
||||
|
||||
// if (isNeutralColor(rgb)) {
|
||||
// continue;
|
||||
// }
|
||||
|
||||
float[] hsb = Color.RGBtoHSB((rgb >> 16) & 0xFF, (rgb >> 8) & 0xFF, rgb & 0xFF, null);
|
||||
double score = colorScore.getOrDefault(rgb, 0.0) + hsb[1];
|
||||
colorScore.put(rgb, score);
|
||||
}
|
||||
}
|
||||
|
||||
int dominantColor = colorScore.entrySet().stream()
|
||||
.max(Map.Entry.comparingByValue())
|
||||
.map(Map.Entry::getKey)
|
||||
.orElse(0);
|
||||
|
||||
return String.format("#%06X", dominantColor & 0xFFFFFF);
|
||||
}
|
||||
|
||||
public static String getDominantColor(Item game) throws Exception {
|
||||
String image = game.properties().get(Property.ARTWORK);
|
||||
File imageFile = new File(game.location().getParent() + '/' + image);
|
||||
return getDominantColor(imageFile);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package org.adrianvictor.livingroom.utils.net;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
|
||||
public class Cookie {
|
||||
private static final String SESSION_COOKIE_NAME = "SESSIONID";
|
||||
|
||||
public static String getSessionIdFromCookies(HttpExchange exchange) {
|
||||
String cookieHeader = exchange.getRequestHeaders().getFirst("Cookie");
|
||||
if (cookieHeader == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String[] cookies = cookieHeader.split(";");
|
||||
for (String cookie : cookies) {
|
||||
cookie = cookie.trim();
|
||||
if (cookie.startsWith(SESSION_COOKIE_NAME + "=")) {
|
||||
return cookie.substring((SESSION_COOKIE_NAME + "=").length());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void setSessionCookie(HttpExchange exchange, String sessionId) {
|
||||
String cookie = SESSION_COOKIE_NAME + "=" + sessionId + "; Path=/; HttpOnly; SameSite=Strict";
|
||||
exchange.getResponseHeaders().add("Set-Cookie", cookie);
|
||||
}
|
||||
|
||||
public static void clearSessionCookie(HttpExchange exchange) {
|
||||
String cookie = SESSION_COOKIE_NAME + "=; Path=/; Max-Age=0; HttpOnly";
|
||||
exchange.getResponseHeaders().add("Set-Cookie", cookie);
|
||||
}
|
||||
}
|
||||
10
src/main/java/org/adrianvictor/livingroom/web/Page.java
Normal file
10
src/main/java/org/adrianvictor/livingroom/web/Page.java
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
package org.adrianvictor.livingroom.web;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import freemarker.template.Configuration;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public interface Page {
|
||||
String result(Configuration cfg, String baseAddress, String path, Map<String, Object> data, HttpExchange exchange);
|
||||
}
|
||||
22
src/main/java/org/adrianvictor/livingroom/web/Pages.java
Normal file
22
src/main/java/org/adrianvictor/livingroom/web/Pages.java
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
package org.adrianvictor.livingroom.web;
|
||||
|
||||
import org.adrianvictor.livingroom.web.pages.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
public class Pages {
|
||||
private static HashMap<String, Page> map = new HashMap<>();
|
||||
|
||||
static {
|
||||
Index index = new Index();
|
||||
map.put("", index);
|
||||
map.put("game", new Game());
|
||||
map.put("logout", new Logout());
|
||||
map.put("scan", new Scan());
|
||||
}
|
||||
|
||||
public static HashMap<String, Page> getAll() {
|
||||
return map;
|
||||
}
|
||||
public static Page get(String path) { return map.get(path); }
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package org.adrianvictor.livingroom.web;
|
||||
|
||||
public class QuickResponses {
|
||||
public static String notFound() {
|
||||
return "Not Found";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
package org.adrianvictor.livingroom.web.pages;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import freemarker.template.Configuration;
|
||||
import freemarker.template.Template;
|
||||
import org.adrianvictor.livingroom.Logger;
|
||||
import org.adrianvictor.livingroom.data.Database;
|
||||
import org.adrianvictor.livingroom.data.catalog.Item;
|
||||
import org.adrianvictor.livingroom.data.catalog.Property;
|
||||
import org.adrianvictor.livingroom.utils.image.ColorExtractor;
|
||||
import org.adrianvictor.livingroom.web.Page;
|
||||
import org.adrianvictor.livingroom.web.QuickResponses;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.StringWriter;
|
||||
import java.util.Map;
|
||||
|
||||
public class Game implements Page {
|
||||
@Override
|
||||
public String result(Configuration cfg, String baseAddress, String path, Map<String, Object> data, HttpExchange exchange) {
|
||||
try {
|
||||
Template template = cfg.getTemplate("game.ftl");
|
||||
|
||||
String[] args = path.split("/");
|
||||
|
||||
int id;
|
||||
try {
|
||||
id = Integer.parseInt(args[0]);
|
||||
} catch (NumberFormatException e) {
|
||||
return QuickResponses.notFound();
|
||||
// send404(exchange, "The provided game ID %s is not a valid number.".formatted(args[0]));
|
||||
}
|
||||
|
||||
Item game = Database.getInstance().getGame(id);
|
||||
|
||||
Map<String, String> gameMap = game.getPropertiesString();
|
||||
|
||||
try {
|
||||
String accentHEX = ColorExtractor.getDominantColor(game);
|
||||
data.put("accentColor", accentHEX);
|
||||
} catch (Exception e) {
|
||||
data.put("accentColor", "#FFFFFF");
|
||||
}
|
||||
|
||||
data.put("game", gameMap);
|
||||
data.put("versions", game.getVersions());
|
||||
|
||||
StringWriter writer = new StringWriter();
|
||||
template.process(data, writer);
|
||||
|
||||
return writer.toString();
|
||||
} catch (Exception e) {
|
||||
Logger.error(e.getMessage());
|
||||
return "<h1>Error</h1>";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
package org.adrianvictor.livingroom.web.pages;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import freemarker.template.Configuration;
|
||||
import freemarker.template.Template;
|
||||
import org.adrianvictor.livingroom.Logger;
|
||||
import org.adrianvictor.livingroom.data.Database;
|
||||
import org.adrianvictor.livingroom.data.catalog.Item;
|
||||
import org.adrianvictor.livingroom.data.catalog.Property;
|
||||
import org.adrianvictor.livingroom.utils.image.ColorExtractor;
|
||||
import org.adrianvictor.livingroom.web.Page;
|
||||
|
||||
import java.io.StringWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class Index implements Page {
|
||||
@Override
|
||||
public String result(Configuration cfg, String baseAddress, String path, Map<String, Object> data, HttpExchange exchange) {
|
||||
try {
|
||||
Template template = cfg.getTemplate("games.ftl");
|
||||
|
||||
List<Map<String, Object>> gamesList = new ArrayList<>();
|
||||
|
||||
for (Item item : Database.getInstance().getAllGames()) {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
for (Map.Entry<Property, String> e : item.properties().entrySet()) {
|
||||
map.put(e.getKey().name(), e.getValue());
|
||||
}
|
||||
map.put("versions", item.getVersions());
|
||||
String accent = "#FFFFFF";
|
||||
try {
|
||||
accent = ColorExtractor.getDominantColor(item);
|
||||
} catch (Exception ignored) {}
|
||||
map.put("accentColor", accent);
|
||||
gamesList.add(map);
|
||||
}
|
||||
|
||||
data.put("games", gamesList);
|
||||
|
||||
StringWriter writer = new StringWriter();
|
||||
template.process(data, writer);
|
||||
|
||||
return writer.toString();
|
||||
} catch (Exception e) {
|
||||
Logger.error(e.getMessage());
|
||||
return "<h1>Error</h1>";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package org.adrianvictor.livingroom.web.pages;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import freemarker.template.Configuration;
|
||||
import org.adrianvictor.livingroom.Logger;
|
||||
import org.adrianvictor.livingroom.web.Page;
|
||||
|
||||
import java.io.StringWriter;
|
||||
import java.util.Map;
|
||||
|
||||
public class Login implements Page {
|
||||
@Override
|
||||
public String result(Configuration cfg, String baseAddress, String path, Map<String, Object> data, HttpExchange exchange) {
|
||||
try {
|
||||
// Check if this is a POST request
|
||||
// For now, just show the login form
|
||||
|
||||
freemarker.template.Template template = cfg.getTemplate("login.ftl");
|
||||
|
||||
StringWriter writer = new StringWriter();
|
||||
template.process(data, writer);
|
||||
|
||||
return writer.toString();
|
||||
} catch (Exception e) {
|
||||
Logger.error(e.getMessage());
|
||||
return "<h1>Error</h1>";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package org.adrianvictor.livingroom.web.pages;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import freemarker.template.Configuration;
|
||||
import org.adrianvictor.livingroom.auth.SessionManager;
|
||||
import org.adrianvictor.livingroom.utils.net.Cookie;
|
||||
import org.adrianvictor.livingroom.web.Page;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class Logout implements Page {
|
||||
|
||||
@Override
|
||||
public String result(Configuration cfg, String baseAddress, String path, Map<String, Object> data, HttpExchange exchange) {
|
||||
String sessionId = Cookie.getSessionIdFromCookies(exchange);
|
||||
SessionManager sm = SessionManager.getInstance();
|
||||
sm.destroySession(sessionId);
|
||||
data.remove("username");
|
||||
return new Login().result(cfg, baseAddress, path, data, exchange);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package org.adrianvictor.livingroom.web.pages;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import freemarker.template.Configuration;
|
||||
import org.adrianvictor.livingroom.web.Page;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class NotFound implements Page {
|
||||
@Override
|
||||
public String result(Configuration cfg, String baseAddress, String path, Map<String, Object> data, HttpExchange exchange) {
|
||||
return "Not found";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package org.adrianvictor.livingroom.web.pages;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import freemarker.template.Configuration;
|
||||
import org.adrianvictor.livingroom.Main;
|
||||
import org.adrianvictor.livingroom.data.Database;
|
||||
import org.adrianvictor.livingroom.data.Indexer;
|
||||
import org.adrianvictor.livingroom.web.Page;
|
||||
import org.adrianvictor.livingroom.web.QuickResponses;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Map;
|
||||
|
||||
public class Remove implements Page {
|
||||
|
||||
@Override
|
||||
public String result(Configuration cfg, String baseAddress, String path, Map<String, Object> data, HttpExchange exchange) {
|
||||
String arg = path.split("/")[0];
|
||||
if (arg == null) {
|
||||
return QuickResponses.notFound();
|
||||
}
|
||||
int id;
|
||||
try {
|
||||
id = Integer.parseInt(arg);
|
||||
} catch (NumberFormatException e) {
|
||||
return QuickResponses.notFound();
|
||||
}
|
||||
Database.getInstance().remove(String.valueOf(id));
|
||||
return "Success";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package org.adrianvictor.livingroom.web.pages;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import freemarker.template.Configuration;
|
||||
import org.adrianvictor.livingroom.Main;
|
||||
import org.adrianvictor.livingroom.data.Indexer;
|
||||
import org.adrianvictor.livingroom.web.Page;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Map;
|
||||
|
||||
public class Scan implements Page {
|
||||
|
||||
@Override
|
||||
public String result(Configuration cfg, String baseAddress, String path, Map<String, Object> data, HttpExchange exchange) {
|
||||
Indexer.getInstance().scanAsync(new File(Main.getConfig().getLibrary()));
|
||||
return "";
|
||||
}
|
||||
}
|
||||
9
src/main/resources/config.json
Normal file
9
src/main/resources/config.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"library": "/home/adrian/Desktop/testing/livingroom",
|
||||
"users": {
|
||||
"admin": {
|
||||
"role": "admin",
|
||||
"password": "test"
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
src/main/resources/static/images/dvd.png
Normal file
BIN
src/main/resources/static/images/dvd.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 858 KiB |
275
src/main/resources/static/styles.css
Normal file
275
src/main/resources/static/styles.css
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
:root {
|
||||
--accent-color-darker: rgba(255, 255, 255, 0.6);
|
||||
--accent-color: white;
|
||||
--default-box-shadow: 2px 7px 5px rgba(0,0,0,0.3), 0px -4px 10px rgba(0,0,0,0.3);
|
||||
--alternative-bg: rgb(20, 20, 20);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: black;
|
||||
color: white;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
user-select: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
header {
|
||||
padding: 1em;
|
||||
border-bottom: thick solid var(--accent-color);
|
||||
background-color: var(--alternative-bg);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
header::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: var(--accent-color);
|
||||
filter: drop-shadow(0 0 50vh var(--accent-color));
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
h1, h2 {
|
||||
margin-bottom: .4em;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
a {
|
||||
transition: .2s;
|
||||
color: gainsboro;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: gray;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: .4em;
|
||||
}
|
||||
|
||||
input {
|
||||
border: none;
|
||||
border-bottom: medium solid var(--accent-color-darker);
|
||||
background-color: transparent;
|
||||
color: white;
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
border-bottom-color: var(--accent-color);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
button, .fakeButton {
|
||||
font-size: .9em;
|
||||
border: medium solid var(--accent-color);
|
||||
padding: .6rem;
|
||||
background-color: black;
|
||||
color: white;
|
||||
transition: .2s;
|
||||
box-shadow: var(--default-box-shadow);
|
||||
height: 100%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.fakeButton {
|
||||
/* border-color: var(--accent-color-darker); */
|
||||
border-style: dotted;
|
||||
}
|
||||
|
||||
button:hover, button:focus {
|
||||
color: black;
|
||||
background-color: var(--accent-color);
|
||||
}
|
||||
|
||||
#games {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1em;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.gameCard {
|
||||
width: 8em;
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
.gameCard:hover {
|
||||
opacity: .6;
|
||||
}
|
||||
|
||||
.gameCard .gameImage {
|
||||
border: thin solid var(--accent-color);
|
||||
width: 100%;
|
||||
box-shadow: var(--default-box-shadow);
|
||||
}
|
||||
|
||||
.gameCard .gameTitle {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.gameCard .versionsList {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
/* Game page */
|
||||
|
||||
body#gamePage {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body#gamePage header {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
body#gamePage .gameImage {
|
||||
border: medium solid var(--accent-color);
|
||||
}
|
||||
|
||||
body#gamePage main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body#gamePage h2 {
|
||||
flex-shrink: 0;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
body#gamePage .game {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
body#gamePage .gameImageContainer {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
body#gamePage .gameImage {
|
||||
height: 100%;
|
||||
width: auto;
|
||||
object-fit: contain;
|
||||
z-index: 1;
|
||||
box-shadow: var(--default-box-shadow);
|
||||
}
|
||||
|
||||
body#gamePage .gameDisk {
|
||||
position: absolute;
|
||||
height: 60%;
|
||||
width: auto;
|
||||
object-fit: contain;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
transition: .4s;
|
||||
pointer-events: none;
|
||||
filter: drop-shadow(2px 7px 5px rgba(0,0,0,0.3));
|
||||
}
|
||||
|
||||
body#gamePage .gameInfo {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0 1em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1em;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
body#gamePage .gameDownloads {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.9);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
overflow: auto;
|
||||
text-wrap: wrap;
|
||||
transition: opacity 0.3s, visibility 0.3s;
|
||||
}
|
||||
|
||||
body#gamePage .gameImageContainer.showDownloads:hover .gameDisk {
|
||||
transform: translateX(60%);
|
||||
}
|
||||
|
||||
body#gamePage .gameImageContainer.showDownloads .gameDownloads {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
body#gamePage .gameInfo .gameTitle .gameAuthorText {
|
||||
font-weight: lighter;
|
||||
}
|
||||
|
||||
body#gamePage .gameInfo .gameDescription {
|
||||
padding: .4em;
|
||||
background-color: rgb(40, 40, 40);
|
||||
box-shadow: var(--default-box-shadow);
|
||||
border: medium solid var(--accent-color);
|
||||
}
|
||||
|
||||
body#gamePage .gameInfo .gameDescription p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
body#gamePage .gameInfo .gameTitle h2 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Login Page */
|
||||
.loginForm {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .6em;
|
||||
}
|
||||
|
||||
.loginForm input {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.loginForm .formGroup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .4em;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.loginForm .formSubmit {
|
||||
margin-top: 1em;
|
||||
}
|
||||
8
src/main/resources/static/svg/disc.svg
Normal file
8
src/main/resources/static/svg/disc.svg
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<path fill="#444" d="M8 0c-4.4 0-8 3.6-8 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zM15 8c0 1.1-0.2 2.1-0.7 3l-2.7-1.2c0.2-0.6 0.4-1.2 0.4-1.8 0-2.2-1.8-4-4-4-0.5 0-0.9 0.1-1.4 0.3l-1.2-2.8c0.6-0.2 1.2-0.4 1.8-0.5l0.3 3h0.5v-3c3.9 0 7 3.1 7 7zM8 5c1.7 0 3 1.3 3 3s-1.3 3-3 3-3-1.3-3-3 1.3-3 3-3zM1 8c0-1.1 0.2-2.1 0.7-3l2.7 1.2c-0.2 0.6-0.4 1.2-0.4 1.8 0 2.2 1.8 4 4 4 0.5 0 0.9-0.1 1.4-0.3l1.2 2.8c-0.6 0.2-1.2 0.4-1.8 0.5l-0.3-3h-0.5v3c-3.9 0-7-3.1-7-7z"></path>
|
||||
<path fill="#444" d="M10 8c0 1.105-0.895 2-2 2s-2-0.895-2-2c0-1.105 0.895-2 2-2s2 0.895 2 2z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 929 B |
14
src/main/resources/templates/base.ftl
Normal file
14
src/main/resources/templates/base.ftl
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="/static/styles.css">
|
||||
<title><#if pageTitle??>${pageTitle}<#else>LivingRoom</#if></title>
|
||||
</head>
|
||||
<body <#if bodyID??>id="${bodyID}"</#if><#if accentColor??> style="--accent-color: ${accentColor};"</#if>>
|
||||
<#include "header.ftl">
|
||||
<main>
|
||||
<#if pageContent??>${pageContent}</#if>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
54
src/main/resources/templates/game.ftl
Normal file
54
src/main/resources/templates/game.ftl
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
<#assign pageTitle = game["name"]>
|
||||
<#assign bodyID = "gamePage">
|
||||
<#assign pageContent>
|
||||
<div class="game">
|
||||
<div class="gameImageContainer">
|
||||
<img class="gameImage" src="/pic/${game["id"]}">
|
||||
<img class="gameDisk" src="/static/images/dvd.png">
|
||||
<div class="gameDownloads">
|
||||
<#if versions?has_content>
|
||||
<div class="downloads">
|
||||
<h3>Available Downloads</h3>
|
||||
<ul>
|
||||
<#list versions as version>
|
||||
<li><a href="/download/${game["id"]}/${version}">${version}</a></li>
|
||||
</#list>
|
||||
</ul>
|
||||
</div>
|
||||
</#if>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gameInfo">
|
||||
<div class="gameTitle">
|
||||
<h2>${game["name"]}<#if game["author"]?has_content><span class="gameAuthorText"> by ${game["author"]}</span></#if></h2>
|
||||
<#if game["year"]?has_content>
|
||||
<p>${game["year"]}</p>
|
||||
</#if>
|
||||
</div>
|
||||
|
||||
<div class="downloadsButton">
|
||||
<button onclick="document.querySelector('.gameImageContainer').classList.toggle('showDownloads');">Downloads</button>
|
||||
</div>
|
||||
|
||||
<div class="gameProperties">
|
||||
<#if game["publisher"]?has_content>
|
||||
<p><b>Published by</b> ${game["publisher"]}</p>
|
||||
</#if>
|
||||
<#if game["date"]?has_content>
|
||||
<p><b>Publishing date:</b> ${game["date"]}</p>
|
||||
</#if>
|
||||
<#if game["operating_system"]?has_content>
|
||||
<p><b>Made for</b> ${game["operating_system"]}</p>
|
||||
</#if>
|
||||
<#if game["description"]?has_content>
|
||||
<p><b>Description: </b></p>
|
||||
<div class="gameDescription">
|
||||
<p>${game["description"]}</p>
|
||||
</div>
|
||||
</#if>
|
||||
<#if userRole == "admin">Admin actions: <a href="/${webpref}/remove/${game["id"]}">deindex</a>, <a href="/${webpref}/scan">trigger new scan</a></#if>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</#assign>
|
||||
<#include "base.ftl">
|
||||
15
src/main/resources/templates/games.ftl
Normal file
15
src/main/resources/templates/games.ftl
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<#assign pageTitle = "Games">
|
||||
<#assign pageContent>
|
||||
<h2>Games</h2>
|
||||
<div id="games">
|
||||
<#list games as game>
|
||||
<div class="gameCard" style="--accent-color: ${game["accentColor"]};">
|
||||
<a href="/${webpref}/game/${game["ID"]}">
|
||||
<img class="gameImage" src="/pic/${game["ID"]}">
|
||||
<p>${game["NAME"]}</p>
|
||||
</a>
|
||||
</div>
|
||||
</#list>
|
||||
</div>
|
||||
</#assign>
|
||||
<#include "base.ftl">
|
||||
4
src/main/resources/templates/header.ftl
Normal file
4
src/main/resources/templates/header.ftl
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<header>
|
||||
<h1><a href="/${webpref}/">LivingRoom</a></h1>
|
||||
<p><#if username??>${username} (<a href="/${webpref}/logout">logout</a>)</#if></p>
|
||||
</header>
|
||||
20
src/main/resources/templates/login.ftl
Normal file
20
src/main/resources/templates/login.ftl
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<#assign pageTitle = "Login">
|
||||
<#assign pageContent>
|
||||
<div class="loginContainer">
|
||||
<h2>Login</h2>
|
||||
<form class="loginForm" method="POST" action="/${webpref}/login">
|
||||
<div class="formGroup username">
|
||||
<label for="username">Username:</label>
|
||||
<input type="text" id="username" name="username" required>
|
||||
</div>
|
||||
<div class="formGroup password">
|
||||
<label for="password">Password:</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" class="formSubmit">Login</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</#assign>
|
||||
<#include "base.ftl">
|
||||
Loading…
Add table
Add a link
Reference in a new issue