The final update

I've been working on this for a few months now. I will no longer be updating it personally. This is a full revamp with all the features I wanted to add.
This commit is contained in:
Ramaerel 2025-03-06 10:18:00 +10:00
parent 20f42e433f
commit 10185de76a
64 changed files with 6015 additions and 403 deletions

108
README.md
View File

@ -1,36 +1,106 @@
# Game Library[![Badge License]][License]
*A game library extension for* ***[EmulatorJS]***
# RetroHub [![Badge_License]][License]
*Formerly EJS Library*
*A game library and rom management tool using ***[EmulatorJS]***
<br>
This add-on site allows users of EmulatorJS to manage ROMs, including a built-in (albeit basic) data and image scraper.<br>
This site allows users to run and manage their ROMs, using EmulatorJS and the RetroAchievements website.
PSX has returned!
<br>
## Disclaimer
I will no longer be working on this project; this final update was purely me learning how to program better for my new project
## Installation
If you wish for functionality like this, but are unsure how to set it up yourself, check out ***[Temporus]***.
This is a drag and drop extension, with the <br>
exception that it requires something to host <br>
PHP files like XAMPP. After that, simply upload your roms.<br>
Bulk rom uploading has been added to make this easier.
## Features
I've taken out image scraping for now, I will try to add it in with more options etc with the rebuild.<br>
Instead, I've given cloud save options!
### Modern Game Library Interface
## BIOS setup
Grid and list views for your ROM collection
Search and filter by console type
Game cards with cover art and metadata
To add BIOs for the systems that require it, simply<br />
add the BIOs to a ZIP file and rename it to *console name*.zip.<br />
For example, the gba bios would be kept as /bios/gba.zip <br />
<!----------------------------------------------------------------------------->
### Powerful ROM Management
Bulk upload capability for ROMs
Automatic console detection based on file extension
Support for various ROM formats including ZIP files
### RetroAchievements Integration
Automatic game metadata and images from RetroAchievements
Game info including developer, publisher, release date
Cover art, screenshots, and title screens
### Profile System
Netflix-style profile switching for family sharing
Custom avatars for each profile
Independent save states per profile
### Advanced Save State System
Multiple save slots per game
Screenshot preview of each save state
Seamless saving/loading during gameplay
### BIOS Management
Upload and manage BIOS files for various systems
Visual indication of installed BIOS files
System-specific BIOS requirements reference
### Cloud Save Support
Server-side save state management
Persistent game progress across sessions
Backup protection for your progress
### Installation
Requires a PHP-enabled web server (XAMPP, WAMP, or similar)
Copy all files to your web server's document root
Ensure proper permissions for cache and save directories
Access the site via your web browser
### RetroAchievements Setup
(This is for direct access. If using a Proxy, I have one set up already and linked in :)
Go to Settings → RetroAchievements
Enable RetroAchievements integration
Choose between direct API access (requires your own API key) or proxy mode
If using direct access, enter your RetroAchievements API key from your account's control panel
Save settings and enjoy enhanced game metadata and images
### BIOS Requirements
Some systems require BIOS files to function correctly. Upload your BIOS files through the BIOS management page. Common requirements include:
PlayStation: SCPH5500.bin, SCPH5501.bin, SCPH5502.bin
Game Boy Advance: gba_bios.bin
Nintendo DS: bios7.bin, bios9.bin, firmware.bin
Sega CD: bios_CD_U.bin, bios_CD_J.bin, bios_CD_E.bin
### Support
This project is no longer actively maintained. For similar functionality with professional support, please visit Temporus.
License
RetroHub is released under the GPL license.
### Credits
EmulatorJS - Emulation core
RetroAchievements - Game metadata and images
[Badge License]: https://img.shields.io/badge/license-GPL-blue
[EmulatorJS]: https://github.com/EmulatorJS/emulatorjs
[Temporus]: https://temporus.one/
[License]: #

149
backend/raproxy.php Normal file
View File

@ -0,0 +1,149 @@
<?php
/**
* RetroAchievements API Proxy Server
*
* This script acts as a proxy for RetroAchievements API requests.
* It caches responses to reduce API calls and allows sharing a single
* API key among multiple RetroHub installations.
*
* Updated according to the latest API documentation.
*/
// CORS headers to allow access from different domains
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST');
header('Access-Control-Allow-Headers: Content-Type');
// Configuration
$config = [
'api_key' => '** YOUR API KEY **', // Replace with your RetroAchievements API key
'cache_dir' => 'cache/', // Directory to store cached responses
'cache_expiration' => 604800, // Cache expiration time in seconds (7 days)
'rate_limit' => 30, // Maximum requests per minute per IP
'rate_limit_window' => 60 // Time window for rate limiting in seconds
];
// Create cache directory if it doesn't exist
if (!is_dir($config['cache_dir'])) {
mkdir($config['cache_dir'], 0755, true);
}
// Basic rate limiting
$clientIP = $_SERVER['REMOTE_ADDR'];
$rateLimitFile = $config['cache_dir'] . 'rate_' . md5($clientIP) . '.json';
$rateData = [
'count' => 0,
'timestamp' => time()
];
if (file_exists($rateLimitFile)) {
$rateData = json_decode(file_get_contents($rateLimitFile), true);
// Reset counter if window has passed
if (time() - $rateData['timestamp'] > $config['rate_limit_window']) {
$rateData['count'] = 0;
$rateData['timestamp'] = time();
}
}
// Check if rate limit exceeded
if ($rateData['count'] >= $config['rate_limit']) {
header('HTTP/1.1 429 Too Many Requests');
echo json_encode([
'error' => 'Rate limit exceeded. Please try again later.'
]);
exit;
}
// Process only POST requests
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('HTTP/1.1 405 Method Not Allowed');
echo json_encode([
'error' => 'Only POST requests are allowed.'
]);
exit;
}
// Verify required parameters
if (!isset($_POST['endpoint']) || empty($_POST['endpoint'])) {
header('HTTP/1.1 400 Bad Request');
echo json_encode([
'error' => 'Missing required parameter: endpoint'
]);
exit;
}
$endpoint = $_POST['endpoint'];
$params = isset($_POST['params']) ? $_POST['params'] : [];
// Sanitize endpoint to prevent directory traversal
$endpoint = basename($endpoint);
// Generate cache key based on request
$cacheKey = md5($endpoint . serialize($params));
$cachePath = $config['cache_dir'] . $cacheKey . '.json';
// Check if cached response exists and is still valid
if (file_exists($cachePath) && (time() - filemtime($cachePath) < $config['cache_expiration'])) {
$cachedResponse = file_get_contents($cachePath);
if ($cachedResponse) {
header('Content-Type: application/json');
echo $cachedResponse;
exit;
}
}
// Increment rate limit counter and save
$rateData['count']++;
file_put_contents($rateLimitFile, json_encode($rateData));
// Build API URL - Using the correct format: API_EndpointName.php
$baseUrl = 'https://retroachievements.org/API/';
$url = $baseUrl . 'API_' . $endpoint . '.php';
// Add authentication to params
if (is_array($params)) {
$params['y'] = $config['api_key'];
} else {
$params = [
'y' => $config['api_key']
];
}
// Build full URL with parameters
$url .= '?' . http_build_query($params);
// Make request to RetroAchievements API
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_USERAGENT, 'RetroHub-Proxy/1.0');
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
// Return error if request failed
if ($httpCode !== 200 || empty($response)) {
header('HTTP/1.1 ' . ($httpCode ? $httpCode : 500) . ' Error');
echo json_encode([
'error' => 'Error fetching data from RetroAchievements API',
'http_code' => $httpCode,
'curl_error' => $error,
'url' => $url
]);
exit;
}
// Cache the response
file_put_contents($cachePath, $response);
// Return the response
header('Content-Type: application/json');
echo $response;

275
bios.php Normal file
View File

@ -0,0 +1,275 @@
<?php
session_start();
include 'functions.php';
// Get current profile or set default
if (!isset($_SESSION['current_profile'])) {
$profiles = getProfiles();
if (count($profiles) > 0) {
$_SESSION['current_profile'] = $profiles[0]['id'];
} else {
// Create default profile if none exists
$defaultProfileId = createProfile("Player 1", "avatar1.png");
$_SESSION['current_profile'] = $defaultProfileId;
}
}
// Process BIOS file uploads
$uploadMessage = '';
$uploadStatus = '';
if ($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_POST['upload_type'])) {
if ($_POST['upload_type'] == 'bios' && isset($_FILES['bios_file'])) {
if ($_FILES['bios_file']['error'] == UPLOAD_ERR_OK) {
// Create the bios directory if it doesn't exist
if (!is_dir('bios')) {
mkdir('bios', 0755, true);
}
$tmp_name = $_FILES['bios_file']['tmp_name'];
$name = basename($_FILES['bios_file']['name']);
$console = isset($_POST['console_type']) ? $_POST['console_type'] : pathinfo($name, PATHINFO_FILENAME);
// If console type is specified, rename the file
if (isset($_POST['console_type']) && !empty($_POST['console_type'])) {
$ext = pathinfo($name, PATHINFO_EXTENSION);
$name = $console . '.' . $ext;
}
// Move the uploaded file
if (move_uploaded_file($tmp_name, "bios/$name")) {
$uploadMessage = "BIOS file uploaded successfully!";
$uploadStatus = 'success';
} else {
$uploadMessage = "Error uploading BIOS file.";
$uploadStatus = 'error';
}
} else {
$uploadMessage = "Error uploading BIOS file: " . $_FILES['bios_file']['error'];
$uploadStatus = 'error';
}
}
}
// Get current profile data
$currentProfile = getProfileById($_SESSION['current_profile']);
$allProfiles = getProfiles();
// Get list of BIOS files
$biosFiles = getBiosFiles();
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RetroHub - BIOS Files</title>
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
</head>
<body>
<header>
<div class="container">
<nav>
<a href="index.php" class="logo">
<i class="fas fa-gamepad"></i>
<span>RetroHub</span>
</a>
<ul class="nav-menu">
<li><a href="index.php">Games</a></li>
<li><a href="upload.php">Upload ROMs</a></li>
<li><a href="bios.php" class="active">BIOS Files</a></li>
</ul>
<div class="profile-menu">
<div class="current-profile" id="profile-toggle">
<img src="img/avatars/<?php echo $currentProfile['avatar']; ?>" alt="Profile">
<span><?php echo $currentProfile['name']; ?></span>
<i class="fas fa-chevron-down"></i>
</div>
<div class="profile-dropdown" id="profile-dropdown">
<ul class="profile-list">
<?php foreach($allProfiles as $profile): ?>
<li class="profile-item <?php echo ($profile['id'] == $_SESSION['current_profile']) ? 'active' : ''; ?>"
data-profile-id="<?php echo $profile['id']; ?>">
<img src="img/avatars/<?php echo $profile['avatar']; ?>" alt="<?php echo $profile['name']; ?>">
<span class="profile-name"><?php echo $profile['name']; ?></span>
</li>
<?php endforeach; ?>
</ul>
<div class="profile-actions">
<button class="add-profile-btn" id="add-profile-btn">
<i class="fas fa-plus-circle"></i>
<span>Add New Profile</span>
</button>
</div>
</div>
</div>
</nav>
</div>
</header>
<main>
<div class="container">
<div class="page-header">
<h1 class="page-title">BIOS Files</h1>
</div>
<?php if($uploadMessage): ?>
<div class="alert alert-<?php echo $uploadStatus; ?>">
<i class="fas fa-info-circle"></i>
<span><?php echo $uploadMessage; ?></span>
</div>
<?php endif; ?>
<div class="upload-section">
<div class="upload-container">
<div>
<h2 class="upload-title">Upload BIOS Files</h2>
<p>Some systems require BIOS files to function correctly. Upload your BIOS files here.</p>
<div class="bios-upload-form">
<form action="bios.php" method="post" enctype="multipart/form-data">
<input type="hidden" name="upload_type" value="bios">
<div class="form-group">
<label for="console-type" class="form-label">Console Type</label>
<select id="console-type" name="console_type" class="form-input">
<option value="">Auto-detect from filename</option>
<option value="psx">PlayStation (PSX)</option>
<option value="gba">Game Boy Advance</option>
<option value="nds">Nintendo DS</option>
<option value="segaMD">Sega Mega Drive / Genesis</option>
<option value="segaCD">Sega CD</option>
<option value="segaSaturn">Sega Saturn</option>
<option value="pcengine">PC Engine / TurboGrafx-16</option>
</select>
</div>
<div class="form-group">
<label for="bios-file" class="form-label">BIOS File</label>
<div class="file-input-wrapper">
<input type="file" id="bios-file" name="bios_file" class="form-input file-input" required>
<div class="file-input-button">
<i class="fas fa-upload"></i>
<span>Select BIOS File</span>
</div>
<span class="selected-file-name">No file selected</span>
</div>
</div>
<button type="submit" class="btn">Upload BIOS</button>
</form>
</div>
<div class="bios-info">
<h3><i class="fas fa-info-circle"></i> About BIOS Files</h3>
<p>BIOS files are required for some systems to run correctly. They contain low-level system code that the emulator needs to properly emulate the original hardware.</p>
<p>For copyright reasons, BIOS files are not included with RetroHub and must be provided by you.</p>
<p>Once uploaded, BIOS files will be automatically detected and used by the emulator.</p>
</div>
</div>
<div>
<h2 class="upload-title">Installed BIOS Files</h2>
<div class="bios-list">
<div class="bios-list-header">
<span class="bios-list-console">Console</span>
<span class="bios-list-filename">Filename</span>
<span class="bios-list-size">Size</span>
<span class="bios-list-date">Uploaded</span>
<span class="bios-list-actions">Actions</span>
</div>
<?php if (count($biosFiles) > 0): ?>
<?php foreach ($biosFiles as $bios): ?>
<div class="bios-item">
<span class="bios-item-console"><?php echo $bios['friendly_name']; ?></span>
<span class="bios-item-filename"><?php echo $bios['filename']; ?></span>
<span class="bios-item-size"><?php echo $bios['formatted_size']; ?></span>
<span class="bios-item-date"><?php echo date('M j, Y', $bios['upload_date']); ?></span>
<div class="bios-item-actions">
<a href="delete_bios.php?file=<?php echo urlencode($bios['filename']); ?>" class="bios-delete-btn"
onclick="return confirm('Are you sure you want to delete this BIOS file?');">
<i class="fas fa-trash"></i>
</a>
</div>
</div>
<?php endforeach; ?>
<?php else: ?>
<div class="empty-bios">
<p>No BIOS files installed yet.</p>
</div>
<?php endif; ?>
</div>
<div class="bios-required">
<h3>Required BIOS Files by System</h3>
<div class="bios-required-list">
<div class="bios-required-item">
<div class="bios-required-console">PlayStation (PSX)</div>
<div class="bios-required-files">scph5500.bin, scph5501.bin, scph5502.bin</div>
</div>
<div class="bios-required-item">
<div class="bios-required-console">Game Boy Advance</div>
<div class="bios-required-files">gba_bios.bin</div>
</div>
<div class="bios-required-item">
<div class="bios-required-console">Nintendo DS</div>
<div class="bios-required-files">bios7.bin, bios9.bin, firmware.bin</div>
</div>
<div class="bios-required-item">
<div class="bios-required-console">Sega CD</div>
<div class="bios-required-files">bios_CD_U.bin, bios_CD_J.bin, bios_CD_E.bin</div>
</div>
<!-- Add more systems as needed -->
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="js/profiles.js"></script>
<script src="js/bios.js"></script>
</body>
</html></main>
<!-- Add Profile Modal (same as in index.php) -->
<div class="modal-overlay" id="profile-modal">
<div class="modal">
<div class="modal-header">
<h2 class="modal-title">Create New Profile</h2>
<button class="close-modal" id="close-profile-modal">&times;</button>
</div>
<div class="modal-body">
<form id="profile-form" action="profile_action.php" method="post">
<div class="form-group">
<label for="profile-name" class="form-label">Profile Name</label>
<input type="text" id="profile-name" name="name" class="form-input" required>
</div>
<div class="form-group">
<label class="form-label">Select Avatar</label>
<div class="avatar-selector">
<?php for($i = 1; $i <= 8; $i++): ?>
<img src="img/avatars/avatar<?php echo $i; ?>.png"
class="avatar-option <?php echo ($i == 1) ? 'selected' : ''; ?>"
data-avatar="avatar<?php echo $i; ?>.png">
<?php endfor; ?>
</div>
<input type="hidden" id="selected-avatar" name="avatar" value="avatar1.png">
</div>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" id="cancel-profile">Cancel</button>
<button class="btn" id="save-profile">Create Profile</button>
</div>
</div>

View File

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,8 @@
{
"enabled": true,
"override_local_images": false,
"mode": "direct",
"username": "YOUR USERNAME",
"api_key": "YOUR API KEY",
"proxy_url": "https:\/\/temporus.one\/backend\/raproxy.php"
}

489
debug_ra.php Normal file
View File

@ -0,0 +1,489 @@
<?php
/**
* RetroAchievements Debug Tool
* This script tests the connection to RetroAchievements API and displays detailed results
*/
// Include necessary files
include 'includes/retroachievements.php';
// Set error reporting
error_reporting(E_ALL);
ini_set('display_errors', 1);
// Function to test proxy connection
function testProxyConnection($proxyUrl, $testEndpoint = 'API_GetGameList', $params = ['i' => 1, 'f' => 'mario']) {
echo "<h3>Testing Proxy Connection</h3>";
echo "<p>Proxy URL: " . htmlspecialchars($proxyUrl) . "</p>";
try {
// Build request data
$data = [
'endpoint' => $testEndpoint,
'params' => $params
];
// Make request to proxy
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $proxyUrl);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_POST, true);
curl_setopt($curl, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($curl, CURLOPT_USERAGENT, 'RetroHub/1.0');
curl_setopt($curl, CURLOPT_TIMEOUT, 30);
curl_setopt($curl, CURLOPT_VERBOSE, true);
// Create a stream for curl to write verbose information to
$verbose = fopen('php://temp', 'w+');
curl_setopt($curl, CURLOPT_STDERR, $verbose);
$response = curl_exec($curl);
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
$error = curl_error($curl);
// Get verbose information
rewind($verbose);
$verboseLog = stream_get_contents($verbose);
curl_close($curl);
// Display results
echo "<p>HTTP Status Code: " . $httpCode . "</p>";
if ($error) {
echo "<p>Error: " . htmlspecialchars($error) . "</p>";
}
echo "<h4>Curl Verbose Log:</h4>";
echo "<pre>" . htmlspecialchars($verboseLog) . "</pre>";
echo "<h4>Response:</h4>";
if ($response === false) {
echo "<p>No response received</p>";
return false;
} else {
// Attempt to parse JSON
$parsedResponse = json_decode($response, true);
if ($parsedResponse === null && json_last_error() !== JSON_ERROR_NONE) {
echo "<p>Invalid JSON response. JSON error: " . json_last_error_msg() . "</p>";
echo "<pre>" . htmlspecialchars(substr($response, 0, 1000)) . "...</pre>";
} else {
echo "<pre>" . htmlspecialchars(print_r($parsedResponse, true)) . "</pre>";
return $parsedResponse;
}
}
} catch (Exception $e) {
echo "<p>Exception: " . $e->getMessage() . "</p>";
}
return false;
}
// Function to test direct connection
function testDirectConnection($username, $apiKey, $testEndpoint = 'API_GetGameList', $params = ['i' => 1, 'f' => 'mario']) {
echo "<h3>Testing Direct Connection</h3>";
echo "<p>Username: " . htmlspecialchars($username) . "</p>";
echo "<p>API Key: " . (empty($apiKey) ? "Not provided" : "Provided (hidden)") . "</p>";
try {
// Add authentication to params
$params['z'] = $username;
$params['y'] = $apiKey;
// Build URL
$url = 'https://retroachievements.org/API/' . $testEndpoint . '?' . http_build_query($params);
echo "<p>Request URL: " . htmlspecialchars($url) . "</p>";
// Make request
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($curl, CURLOPT_USERAGENT, 'RetroHub/1.0');
curl_setopt($curl, CURLOPT_TIMEOUT, 30);
curl_setopt($curl, CURLOPT_VERBOSE, true);
// Create a stream for curl to write verbose information to
$verbose = fopen('php://temp', 'w+');
curl_setopt($curl, CURLOPT_STDERR, $verbose);
$response = curl_exec($curl);
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
$error = curl_error($curl);
// Get verbose information
rewind($verbose);
$verboseLog = stream_get_contents($verbose);
curl_close($curl);
// Display results
echo "<p>HTTP Status Code: " . $httpCode . "</p>";
if ($error) {
echo "<p>Error: " . htmlspecialchars($error) . "</p>";
}
echo "<h4>Curl Verbose Log:</h4>";
echo "<pre>" . htmlspecialchars($verboseLog) . "</pre>";
echo "<h4>Response:</h4>";
if ($response === false) {
echo "<p>No response received</p>";
return false;
} else {
// Attempt to parse JSON
$parsedResponse = json_decode($response, true);
if ($parsedResponse === null && json_last_error() !== JSON_ERROR_NONE) {
echo "<p>Invalid JSON response. JSON error: " . json_last_error_msg() . "</p>";
echo "<pre>" . htmlspecialchars(substr($response, 0, 1000)) . "...</pre>";
} else {
echo "<pre>" . htmlspecialchars(print_r($parsedResponse, true)) . "</pre>";
return $parsedResponse;
}
}
} catch (Exception $e) {
echo "<p>Exception: " . $e->getMessage() . "</p>";
}
return false;
}
// Function to check file permissions
function checkFilePermissions() {
echo "<h3>File Permission Check</h3>";
$directories = [
RA_CACHE_DIR,
RA_ICONS_CACHE_DIR,
RA_SCREENSHOTS_CACHE_DIR,
'config/'
];
echo "<table border='1' cellpadding='5' style='border-collapse: collapse;'>";
echo "<tr><th>Directory</th><th>Exists</th><th>Writable</th><th>Permissions</th></tr>";
foreach ($directories as $dir) {
$exists = is_dir($dir);
$writable = $exists && is_writable($dir);
$permissions = $exists ? substr(sprintf('%o', fileperms($dir)), -4) : 'N/A';
echo "<tr>";
echo "<td>" . htmlspecialchars($dir) . "</td>";
echo "<td style='color: " . ($exists ? "green" : "red") . ";'>" . ($exists ? "Yes" : "No") . "</td>";
echo "<td style='color: " . ($writable ? "green" : "red") . ";'>" . ($writable ? "Yes" : "No") . "</td>";
echo "<td>" . $permissions . "</td>";
echo "</tr>";
// Try to create directory if it doesn't exist
if (!$exists) {
$created = mkdir($dir, 0755, true);
echo "<tr><td colspan='4'>Attempted to create directory: " . ($created ? "Success" : "Failed") . "</td></tr>";
}
}
echo "</table>";
}
// Function to test image download
function testImageDownload($url, $destination) {
echo "<h3>Testing Image Download</h3>";
echo "<p>Source URL: " . htmlspecialchars($url) . "</p>";
echo "<p>Destination: " . htmlspecialchars($destination) . "</p>";
try {
// Handle full URLs or relative URLs
if (strpos($url, 'http') !== 0) {
$url = 'https://retroachievements.org' . $url;
echo "<p>Converted to full URL: " . htmlspecialchars($url) . "</p>";
}
// Download image
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($curl, CURLOPT_USERAGENT, 'RetroHub/1.0');
curl_setopt($curl, CURLOPT_TIMEOUT, 30);
$image = curl_exec($curl);
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
$contentType = curl_getinfo($curl, CURLINFO_CONTENT_TYPE);
$error = curl_error($curl);
curl_close($curl);
// Display results
echo "<p>HTTP Status Code: " . $httpCode . "</p>";
echo "<p>Content Type: " . htmlspecialchars($contentType) . "</p>";
echo "<p>Content Size: " . strlen($image) . " bytes</p>";
if ($error) {
echo "<p>Error: " . htmlspecialchars($error) . "</p>";
return false;
}
if ($httpCode !== 200) {
echo "<p>Failed to download image (HTTP " . $httpCode . ")</p>";
return false;
}
// Ensure directory exists
$dir = dirname($destination);
if (!is_dir($dir)) {
$created = mkdir($dir, 0755, true);
echo "<p>Created directory " . htmlspecialchars($dir) . ": " . ($created ? "Success" : "Failed") . "</p>";
if (!$created) {
echo "<p>Error: Could not create directory for image</p>";
return false;
}
}
// Save image
$saved = file_put_contents($destination, $image);
if ($saved === false) {
echo "<p>Error: Could not save image to " . htmlspecialchars($destination) . "</p>";
return false;
}
echo "<p>Image saved successfully (" . $saved . " bytes)</p>";
// Display the image
echo "<h4>Downloaded Image:</h4>";
$base64 = base64_encode($image);
echo "<img src='data:" . $contentType . ";base64," . $base64 . "' style='max-width: 300px; max-height: 300px;' />";
return true;
} catch (Exception $e) {
echo "<p>Exception: " . $e->getMessage() . "</p>";
}
return false;
}
// Get settings
$raSettings = getRetroAchievementsSettings();
// Output page structure
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RetroAchievements Debug Tool</title>
<style>
body {
font-family: Arial, sans-serif;
line-height: 1.6;
margin: 20px;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
h1, h2, h3, h4 {
margin-top: 20px;
}
pre {
background: #f4f4f4;
padding: 10px;
border-radius: 5px;
overflow-x: auto;
}
.section {
border: 1px solid #ddd;
padding: 15px;
margin-bottom: 20px;
border-radius: 5px;
}
.success {
color: green;
font-weight: bold;
}
.error {
color: red;
font-weight: bold;
}
table {
width: 100%;
}
</style>
</head>
<body>
<h1>RetroAchievements Debug Tool</h1>
<div class="section">
<h2>Current Configuration</h2>
<table border="1" cellpadding="5" style="border-collapse: collapse;">
<tr><th>Setting</th><th>Value</th></tr>
<tr><td>Enabled</td><td><?php echo $raSettings['enabled'] ? "Yes" : "No"; ?></td></tr>
<tr><td>Mode</td><td><?php echo $raSettings['mode']; ?></td></tr>
<tr><td>Username</td><td><?php echo htmlspecialchars($raSettings['username']); ?></td></tr>
<tr><td>API Key</td><td><?php echo empty($raSettings['api_key']) ? "Not provided" : "Provided (hidden)"; ?></td></tr>
<tr><td>Proxy URL</td><td><?php echo htmlspecialchars($raSettings['proxy_url']); ?></td></tr>
<tr><td>Override Local Images</td><td><?php echo isset($raSettings['override_local_images']) && $raSettings['override_local_images'] ? "Yes" : "No"; ?></td></tr>
</table>
</div>
<div class="section">
<h2>System Information</h2>
<p>PHP Version: <?php echo phpversion(); ?></p>
<p>cURL Enabled: <?php echo function_exists('curl_version') ? "Yes" : "No"; ?></p>
<?php
if (function_exists('curl_version')) {
$curlVersion = curl_version();
echo "<p>cURL Version: " . $curlVersion['version'] . "</p>";
echo "<p>SSL Version: " . $curlVersion['ssl_version'] . "</p>";
}
?>
<p>allow_url_fopen: <?php echo ini_get('allow_url_fopen') ? "Enabled" : "Disabled"; ?></p>
</div>
<div class="section">
<h2>File System Checks</h2>
<?php checkFilePermissions(); ?>
</div>
<div class="section">
<h2>API Connection Tests</h2>
<?php
if ($raSettings['mode'] === 'proxy') {
$proxyResult = testProxyConnection($raSettings['proxy_url']);
if ($proxyResult && !empty($proxyResult)) {
// Try downloading a sample image from the first game
if (isset($proxyResult[0]['ImageIcon'])) {
$testImageUrl = $proxyResult[0]['ImageIcon'];
$testImageDest = RA_ICONS_CACHE_DIR . 'test_icon.png';
testImageDownload($testImageUrl, $testImageDest);
}
}
} else {
$directResult = testDirectConnection($raSettings['username'], $raSettings['api_key']);
if ($directResult && !empty($directResult)) {
// Try downloading a sample image from the first game
if (isset($directResult[0]['ImageIcon'])) {
$testImageUrl = $directResult[0]['ImageIcon'];
$testImageDest = RA_ICONS_CACHE_DIR . 'test_icon.png';
testImageDownload($testImageUrl, $testImageDest);
}
}
}
?>
</div>
<div class="section">
<h2>Game Search Test</h2>
<form method="post">
<p>
<label for="game_title">Game Title:</label>
<input type="text" name="game_title" id="game_title" value="<?php echo isset($_POST['game_title']) ? htmlspecialchars($_POST['game_title']) : 'Super Mario World'; ?>">
</p>
<p>
<label for="console">Console:</label>
<select name="console" id="console">
<?php
global $RA_CONSOLE_IDS;
foreach ($RA_CONSOLE_IDS as $consoleKey => $consoleId) {
$selected = (isset($_POST['console']) && $_POST['console'] === $consoleKey) ? ' selected' : '';
echo "<option value=\"$consoleKey\"$selected>$consoleKey ($consoleId)</option>";
}
?>
</select>
</p>
<p>
<button type="submit" name="search_test">Test Game Search</button>
</p>
</form>
<?php
if (isset($_POST['search_test']) && isset($_POST['game_title']) && isset($_POST['console'])) {
echo "<h3>Searching for: " . htmlspecialchars($_POST['game_title']) . " on " . htmlspecialchars($_POST['console']) . "</h3>";
// Get game metadata
$gameMetadata = getGameMetadata($_POST['game_title'], $_POST['console']);
if ($gameMetadata) {
echo "<h4>Game Metadata Found:</h4>";
echo "<pre>" . htmlspecialchars(print_r($gameMetadata, true)) . "</pre>";
// Display images if available
echo "<h4>Images:</h4>";
echo "<div style='display: flex; flex-wrap: wrap; gap: 20px;'>";
if (isset($gameMetadata['icon']) && $gameMetadata['icon']) {
echo "<div>";
echo "<p>Icon:</p>";
echo "<img src='" . $gameMetadata['icon'] . "' alt='Game Icon' style='max-width: 200px; max-height: 200px;'>";
echo "</div>";
}
if (isset($gameMetadata['screenshot_title']) && $gameMetadata['screenshot_title']) {
echo "<div>";
echo "<p>Title Screenshot:</p>";
echo "<img src='" . $gameMetadata['screenshot_title'] . "' alt='Title Screenshot' style='max-width: 200px; max-height: 200px;'>";
echo "</div>";
}
if (isset($gameMetadata['screenshot_ingame']) && $gameMetadata['screenshot_ingame']) {
echo "<div>";
echo "<p>Ingame Screenshot:</p>";
echo "<img src='" . $gameMetadata['screenshot_ingame'] . "' alt='Ingame Screenshot' style='max-width: 200px; max-height: 200px;'>";
echo "</div>";
}
if (isset($gameMetadata['screenshot_boxart']) && $gameMetadata['screenshot_boxart']) {
echo "<div>";
echo "<p>Box Art:</p>";
echo "<img src='" . $gameMetadata['screenshot_boxart'] . "' alt='Box Art' style='max-width: 200px; max-height: 200px;'>";
echo "</div>";
}
echo "</div>";
} else {
echo "<p class='error'>No metadata found. Let's see if we can get the raw API response:</p>";
// Try to get the raw data
if ($raSettings['mode'] === 'proxy') {
$consoleId = $RA_CONSOLE_IDS[$_POST['console']];
testProxyConnection($raSettings['proxy_url'], 'API_GetGameList', ['i' => $consoleId, 'f' => $_POST['game_title']]);
} else {
$consoleId = $RA_CONSOLE_IDS[$_POST['console']];
testDirectConnection($raSettings['username'], $raSettings['api_key'], 'API_GetGameList', ['i' => $consoleId, 'f' => $_POST['game_title']]);
}
}
}
?>
</div>
<div class="section">
<h2>Clear Cache</h2>
<form method="post">
<button type="submit" name="clear_cache">Clear All RetroAchievements Cache</button>
</form>
<?php
if (isset($_POST['clear_cache'])) {
// Clear cache directories
$cacheFiles = glob(RA_CACHE_DIR . '*.*');
$iconFiles = glob(RA_ICONS_CACHE_DIR . '*.*');
$screenshotFiles = glob(RA_SCREENSHOTS_CACHE_DIR . '*.*');
$totalFiles = count($cacheFiles) + count($iconFiles) + count($screenshotFiles);
$deletedFiles = 0;
foreach (array_merge($cacheFiles, $iconFiles, $screenshotFiles) as $file) {
if (unlink($file)) {
$deletedFiles++;
}
}
echo "<p>Cleared $deletedFiles out of $totalFiles cache files.</p>";
}
?>
</div>
</body>
</html>

52
delete_bios.php Normal file
View File

@ -0,0 +1,52 @@
<?php
session_start();
include 'functions.php';
// Check if we have the required data
if ($_SERVER["REQUEST_METHOD"] !== "POST") {
http_response_code(405);
echo "Method not allowed";
exit;
}
if (!isset($_POST["profile_id"]) || !isset($_POST["game"]) || !isset($_POST["slot"])) {
http_response_code(400);
echo "Missing required data";
exit;
}
$profileId = $_POST["profile_id"];
$game = $_POST["game"];
$slot = (int)$_POST["slot"];
// Validate the profile exists
$profile = getProfileById($profileId);
if (!$profile) {
http_response_code(404);
echo "Profile not found";
exit;
}
// Construct file paths
$gameName = pathinfo($game, PATHINFO_FILENAME);
$saveStatePath = "saves/$profileId/{$gameName}_{$slot}.state";
$screenshotPath = "img/saves/$profileId/{$gameName}_{$slot}.png";
// Delete save state file
$stateDeleted = false;
if (file_exists($saveStatePath)) {
$stateDeleted = unlink($saveStatePath);
}
// Delete screenshot file
$screenshotDeleted = false;
if (file_exists($screenshotPath)) {
$screenshotDeleted = unlink($screenshotPath);
}
if ($stateDeleted || $screenshotDeleted) {
echo "Save state deleted successfully";
} else {
http_response_code(404);
echo "Save state not found or could not be deleted";
}

45
fnc.php
View File

@ -1,45 +0,0 @@
<?php
function write_ini_file($assoc_arr, $path, $has_sections=FALSE) {
$content = "";
if ($has_sections) {
foreach ($assoc_arr as $key=>$elem) {
$content .= "[".$key."]\n";
foreach ($elem as $key2=>$elem2) {
if(is_array($elem2))
{
for($i=0;$i<count($elem2);$i++)
{
$content .= $key2."[] = \"".$elem2[$i]."\"\n";
}
}
else if($elem2=="") $content .= $key2." = \n";
else $content .= $key2." = \"".$elem2."\"\n";
}
}
}
else {
foreach ($assoc_arr as $key=>$elem) {
if(is_array($elem))
{
for($i=0;$i<count($elem);$i++)
{
$content .= $key."[] = \"".$elem[$i]."\"\n";
}
}
else if($elem=="") $content .= $key." = \n";
else $content .= $key." = \"".$elem."\"\n";
}
}
if (!$handle = fopen($path, 'w')) {
return false;
}
$success = fwrite($handle, $content);
fclose($handle);
return $success;
}
?>

356
functions.php Normal file
View File

@ -0,0 +1,356 @@
<?php
/**
* Core functions for RetroHub
*/
// Make sure needed directories exist
function ensureDirectoriesExist() {
$directories = [
'roms', 'bios', 'img', 'img/avatars', 'saves', 'profiles'
];
foreach($directories as $dir) {
if(!is_dir($dir)) {
mkdir($dir, 0755, true);
}
}
}
// Initialize application on first run
function initializeApp() {
ensureDirectoriesExist();
// Copy default avatars if they don't exist
$avatarSource = 'default_assets/avatars/';
$avatarDest = 'img/avatars/';
if (is_dir($avatarSource)) {
$avatars = scandir($avatarSource);
foreach($avatars as $avatar) {
if(!in_array($avatar, array('.', '..')) && !file_exists($avatarDest . $avatar)) {
copy($avatarSource . $avatar, $avatarDest . $avatar);
}
}
}
// Copy placeholder images if they don't exist
$placeholderSource = 'default_assets/placeholders/';
$placeholderDest = 'img/';
if (is_dir($placeholderSource)) {
$placeholders = scandir($placeholderSource);
foreach($placeholders as $placeholder) {
if(!in_array($placeholder, array('.', '..')) && !file_exists($placeholderDest . $placeholder)) {
copy($placeholderSource . $placeholder, $placeholderDest . $placeholder);
}
}
}
}
// Initialize if needed
initializeApp();
/**
* Determine the console type based on file extension
*/
function getConsoleByExtension($ext) {
// Nintendo
$snes = ["smc", "sfc", "fig", "swc", "bs", "st"];
$gba = ["gba"];
$gb = ["gb", "gbc", "dmg"];
$nes = ["fds", "nes", "unif", "unf"];
$vb = ["vb", "vboy"];
$nds = ["nds"];
$n64 = ["n64", "z64", "v64", "u1", "ndd"];
// Sega
$sms = ["sms"];
$smd = ["smd", "md"];
$gg = ["gg"];
// Sony
$psx = ["pbp", "chd"];
// For zip files, try to peek inside
if ($ext == 'zip') {
$zip = @new ZipArchive;
if ($zip->open("roms/".$name) === TRUE) {
$names = $zip->getNameIndex(0);
$ext0 = explode(".", $names);
$ext = strtolower(end($ext0));
$zip->close();
}
}
if (in_array($ext, $nes)) return 'nes';
if (in_array($ext, $snes)) return 'snes';
if (in_array($ext, $n64)) return 'n64';
if (in_array($ext, $gb)) return 'gb';
if (in_array($ext, $vb)) return 'vb';
if (in_array($ext, $gba)) return 'gba';
if (in_array($ext, $nds)) return 'nds';
if (in_array($ext, $sms)) return 'segaMS';
if (in_array($ext, $smd)) return 'segaMD';
if (in_array($ext, $gg)) return 'segaGG';
if (in_array($ext, $psx)) return 'psx';
return 'unknown';
}
/**
* Get friendly name for console
*/
function getConsoleFriendlyName($console) {
$names = [
'nes' => 'Nintendo NES',
'snes' => 'Super Nintendo',
'n64' => 'Nintendo 64',
'gb' => 'Game Boy',
'gba' => 'Game Boy Advance',
'nds' => 'Nintendo DS',
'vb' => 'Virtual Boy',
'segaMS' => 'Sega Master System',
'segaMD' => 'Sega Mega Drive / Genesis',
'segaGG' => 'Sega Game Gear',
'psx' => 'PlayStation'
];
return isset($names[$console]) ? $names[$console] : 'Unknown System';
}
/**
* Write INI file
*/
function write_ini_file($assoc_arr, $path, $has_sections=FALSE) {
$content = "";
if ($has_sections) {
foreach ($assoc_arr as $key=>$elem) {
$content .= "[".$key."]\n";
foreach ($elem as $key2=>$elem2) {
if(is_array($elem2))
{
for($i=0;$i<count($elem2);$i++)
{
$content .= $key2."[] = \"".$elem2[$i]."\"\n";
}
}
else if($elem2=="") $content .= $key2." = \n";
else $content .= $key2." = \"".$elem2."\"\n";
}
}
}
else {
foreach ($assoc_arr as $key=>$elem) {
if(is_array($elem))
{
for($i=0;$i<count($elem);$i++)
{
$content .= $key."[] = \"".$elem[$i]."\"\n";
}
}
else if($elem=="") $content .= $key." = \n";
else $content .= $key." = \"".$elem."\"\n";
}
}
if (!$handle = fopen($path, 'w')) {
return false;
}
$success = fwrite($handle, $content);
fclose($handle);
return $success;
}
/**
* Profile Management Functions
*/
// Get all profiles
function getProfiles() {
if (!is_dir('profiles')) {
mkdir('profiles', 0755, true);
}
$profiles = [];
$files = glob('profiles/*.json');
foreach ($files as $file) {
$profileData = json_decode(file_get_contents($file), true);
if ($profileData) {
$profiles[] = $profileData;
}
}
return $profiles;
}
// Get profile by ID
function getProfileById($id) {
$profileFile = "profiles/$id.json";
if (file_exists($profileFile)) {
return json_decode(file_get_contents($profileFile), true);
}
return null;
}
// Create a new profile
function createProfile($name, $avatar) {
$id = uniqid();
$profile = [
'id' => $id,
'name' => $name,
'avatar' => $avatar,
'created' => date('Y-m-d H:i:s')
];
file_put_contents("profiles/$id.json", json_encode($profile));
return $id;
}
// Update profile
function updateProfile($id, $data) {
$profile = getProfileById($id);
if (!$profile) {
return false;
}
// Update profile with new data
$profile = array_merge($profile, $data);
file_put_contents("profiles/$id.json", json_encode($profile));
return true;
}
// Delete profile
function deleteProfile($id) {
$profileFile = "profiles/$id.json";
if (file_exists($profileFile)) {
return unlink($profileFile);
}
return false;
}
/**
* Save State Management
*/
// Get save states for a game and profile
function getSaveStates($gameFile, $profileId) {
$saveDir = "saves/$profileId";
if (!is_dir($saveDir)) {
return [];
}
$saveStates = [];
$game = pathinfo($gameFile, PATHINFO_FILENAME);
$files = glob("$saveDir/{$game}_*.state");
foreach ($files as $file) {
$filename = basename($file);
preg_match('/(.+)_(\d+)\.state$/', $filename, $matches);
if (count($matches) === 3) {
$slot = $matches[2];
$timestamp = filemtime($file);
$saveStates[] = [
'slot' => $slot,
'timestamp' => $timestamp,
'formatted_time' => date('Y-m-d H:i:s', $timestamp),
'screenshot' => file_exists("img/saves/{$profileId}/{$game}_{$slot}.png")
? "img/saves/{$profileId}/{$game}_{$slot}.png"
: null
];
}
}
// Sort by newest first
usort($saveStates, function($a, $b) {
return $b['timestamp'] - $a['timestamp'];
});
return $saveStates;
}
// Save a game state for a specific profile
function saveGameState($gameFile, $profileId, $slot, $stateData, $screenshotData) {
$saveDir = "saves/$profileId";
$screenshotDir = "img/saves/$profileId";
// Create directories if they don't exist
if (!is_dir($saveDir)) {
mkdir($saveDir, 0755, true);
}
if (!is_dir($screenshotDir)) {
mkdir($screenshotDir, 0755, true);
}
$game = pathinfo($gameFile, PATHINFO_FILENAME);
$stateFile = "$saveDir/{$game}_{$slot}.state";
$screenshotFile = "$screenshotDir/{$game}_{$slot}.png";
// Save state file
file_put_contents($stateFile, $stateData);
// Save screenshot
if ($screenshotData) {
file_put_contents($screenshotFile, $screenshotData);
}
return true;
}
/**
* BIOS Management
*/
// Get all installed BIOS files
function getBiosFiles() {
if (!is_dir('bios')) {
mkdir('bios', 0755, true);
}
$biosFiles = [];
$files = glob('bios/*.{zip,bin}', GLOB_BRACE);
foreach ($files as $file) {
$filename = basename($file);
$filesize = filesize($file);
$console = pathinfo($filename, PATHINFO_FILENAME);
$biosFiles[] = [
'filename' => $filename,
'console' => $console,
'friendly_name' => getConsoleFriendlyName($console),
'size' => $filesize,
'formatted_size' => formatFileSize($filesize),
'upload_date' => filemtime($file),
'formatted_date' => date('Y-m-d H:i:s', filemtime($file))
];
}
return $biosFiles;
}
// Helper to format file size
function formatFileSize($bytes) {
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$bytes = max($bytes, 0);
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1);
$bytes /= pow(1024, $pow);
return round($bytes, 2) . ' ' . $units[$pow];
}

49
get_state.php Normal file
View File

@ -0,0 +1,49 @@
<?php
session_start();
include 'functions.php';
// Check if we have the required data
if (!isset($_GET["profile_id"]) || !isset($_GET["game"]) || !isset($_GET["slot"])) {
http_response_code(400);
echo "Missing required parameters";
exit;
}
$profileId = $_GET["profile_id"];
$game = $_GET["game"];
$slot = (int)$_GET["slot"];
// Validate the profile exists
$profile = getProfileById($profileId);
if (!$profile) {
http_response_code(404);
echo "Profile not found";
exit;
}
// Construct the save state file path
$gameName = pathinfo($game, PATHINFO_FILENAME);
$saveStatePath = "saves/$profileId/{$gameName}_{$slot}.state";
// Check if the save state exists
if (!file_exists($saveStatePath)) {
http_response_code(404);
echo "Save state not found";
exit;
}
// Read the save state data
$stateData = file_get_contents($saveStatePath);
if ($stateData === false) {
http_response_code(500);
echo "Failed to read save state";
exit;
}
// Set appropriate headers
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . basename($saveStatePath) . '"');
header('Content-Length: ' . filesize($saveStatePath));
// Output the save state data
echo $stateData;

BIN
img/avatars/avatar1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

BIN
img/avatars/avatar2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

BIN
img/avatars/avatar3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
img/avatars/avatar4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

BIN
img/avatars/avatar5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
img/avatars/avatar6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

BIN
img/avatars/avatar7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
img/avatars/avatar8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

View File

@ -0,0 +1,450 @@
<?php
/**
* RetroAchievements API integration
* Updated according to the latest API documentation
*/
// Base API URL - This is the correct official endpoint
define('RA_API_BASE_URL', 'https://retroachievements.org/API/');
// Cache directory for RetroAchievements data
define('RA_CACHE_DIR', 'cache/retroachievements/');
// Cache directory for game icons
define('RA_ICONS_CACHE_DIR', 'img/cache/icons/');
// Cache directory for game screenshots
define('RA_SCREENSHOTS_CACHE_DIR', 'img/cache/screenshots/');
// Cache expiration time (in seconds)
define('RA_CACHE_EXPIRATION', 7 * 24 * 60 * 60); // 1 week
// Console ID mapping for RetroAchievements
$RA_CONSOLE_IDS = [
'nes' => 7,
'snes' => 3,
'n64' => 2,
'gb' => 4,
'gbc' => 5,
'gba' => 6,
'nds' => 10,
'vb' => 28,
'segaMS' => 11,
'segaMD' => 1,
'segaGG' => 9,
'psx' => 27,
'pce' => 8, // TurboGrafx-16/PC Engine
'segaSaturn' => 22,
'segaCD' => 21,
'32x' => 23,
'atari2600' => 25,
'atari7800' => 51
];
/**
* Initialize RetroAchievements by creating necessary directories
*/
function initRetroAchievements() {
// Create cache directories if they don't exist
$directories = [
RA_CACHE_DIR,
RA_ICONS_CACHE_DIR,
RA_SCREENSHOTS_CACHE_DIR
];
foreach ($directories as $dir) {
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
}
}
/**
* Get RetroAchievements settings
*/
function getRetroAchievementsSettings() {
$settingsPath = 'config/retroachievements.json';
if (file_exists($settingsPath)) {
return json_decode(file_get_contents($settingsPath), true);
}
return [
'enabled' => false,
'mode' => 'direct', // 'direct' or 'proxy'
'username' => '',
'api_key' => '',
'proxy_url' => 'https://temporus.one/backend/raproxy.php',
'override_local_images' => true // Whether RA images should override local images
];
}
/**
* Save RetroAchievements settings
*/
function saveRetroAchievementsSettings($settings) {
$settingsPath = 'config/retroachievements.json';
// Create config directory if it doesn't exist
if (!is_dir('config')) {
mkdir('config', 0755, true);
}
return file_put_contents($settingsPath, json_encode($settings, JSON_PRETTY_PRINT));
}
/**
* Make a RetroAchievements API request (direct method)
* Updated according to the latest API documentation
*/
function raApiRequest($endpoint, $params = []) {
$settings = getRetroAchievementsSettings();
if (!$settings['enabled'] || $settings['mode'] !== 'direct' || empty($settings['api_key'])) {
return false;
}
// Add authentication to params - only need API key according to docs
$params['y'] = $settings['api_key'];
// Build URL - API_EndpointName.php format with query params
$url = RA_API_BASE_URL . 'API_' . $endpoint . '.php?' . http_build_query($params);
// Make request
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_USERAGENT, 'RetroHub/1.0');
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// Check for valid response
if ($httpCode !== 200 || empty($response)) {
return false;
}
return json_decode($response, true);
}
/**
* Make a RetroAchievements API request through proxy
* Updated according to the latest API documentation
*/
function raProxyRequest($endpoint, $params = []) {
$settings = getRetroAchievementsSettings();
if (!$settings['enabled'] || $settings['mode'] !== 'proxy' || empty($settings['proxy_url'])) {
return false;
}
// Build request data
$data = [
'endpoint' => $endpoint,
'params' => $params
];
// Make request to proxy
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $settings['proxy_url']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_USERAGENT, 'RetroHub/1.0');
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// Check for valid response
if ($httpCode !== 200 || empty($response)) {
return false;
}
return json_decode($response, true);
}
/**
* Clean a game title for better searching
*/
function cleanGameTitle($title) {
// Remove file extension
$title = preg_replace('/\.(zip|nes|smc|rom|md|gb|gba|n64)$/i', '', $title);
// Remove tags like (USA), [!], etc.
$title = preg_replace('/\([^\)]+\)|\[[^\]]+\]/', '', $title);
// Remove version numbers and other common patterns
$title = preg_replace('/ v[\d\.]+| rev[\d\.]+| \d+in\d+| hack/i', '', $title);
// Convert underscores to spaces
$title = str_replace('_', ' ', $title);
// Remove extra spaces
$title = preg_replace('/\s+/', ' ', $title);
return trim($title);
}
/**
* Get game data from RetroAchievements
*/
function getGameData($gameTitle, $console) {
global $RA_CONSOLE_IDS;
// Skip if RetroAchievements is disabled or console not supported
$settings = getRetroAchievementsSettings();
if (!$settings['enabled'] || !isset($RA_CONSOLE_IDS[$console])) {
return false;
}
// Clean the game title
$cleanTitle = cleanGameTitle($gameTitle);
// Generate cache key
$cacheKey = md5($cleanTitle . '_' . $console);
$cachePath = RA_CACHE_DIR . $cacheKey . '.json';
// Check cache
if (file_exists($cachePath) && (time() - filemtime($cachePath) < RA_CACHE_EXPIRATION)) {
return json_decode(file_get_contents($cachePath), true);
}
// Prepare API request
$consoleId = $RA_CONSOLE_IDS[$console];
// Search for games by console and title
$endpoint = 'GetGameList';
$params = [
'i' => $consoleId,
'f' => $cleanTitle
];
// Make API request
$response = ($settings['mode'] === 'direct')
? raApiRequest($endpoint, $params)
: raProxyRequest($endpoint, $params);
if (!$response || empty($response)) {
return false;
}
// Find the best match game
$bestMatch = null;
$bestScore = 0;
foreach ($response as $game) {
// Skip games that don't match the console or have no ID
if (!isset($game['ID']) || !isset($game['ConsoleID']) || (int)$game['ConsoleID'] !== $consoleId) {
continue;
}
// Calculate similarity
$gameTitle = isset($game['Title']) ? cleanGameTitle($game['Title']) : '';
$similarity = 0;
// Exact match
if (strtolower($gameTitle) === strtolower($cleanTitle)) {
$similarity = 1;
} else {
// Use similar_text for a better match score
similar_text(strtolower($gameTitle), strtolower($cleanTitle), $similarity);
$similarity = $similarity / 100; // Convert to 0-1 scale
}
if ($similarity > $bestScore) {
$bestScore = $similarity;
$bestMatch = $game;
}
}
// If no good match found, return the first result if available
if (!$bestMatch && !empty($response) && is_array($response) && count($response) > 0) {
$bestMatch = $response[0];
}
// If still no match, return false
if (!$bestMatch) {
return false;
}
// Get additional game details using the game ID
if (isset($bestMatch['ID'])) {
$gameId = $bestMatch['ID'];
// Use extended game info endpoint
$extendedEndpoint = 'GetGameExtended';
$extendedParams = ['i' => $gameId];
$additionalDetails = ($settings['mode'] === 'direct')
? raApiRequest($extendedEndpoint, $extendedParams)
: raProxyRequest($extendedEndpoint, $extendedParams);
if ($additionalDetails) {
$bestMatch = array_merge($bestMatch, $additionalDetails);
}
// Fix image URLs if they are relative paths
foreach (['ImageIcon', 'ImageTitle', 'ImageIngame', 'ImageBoxArt'] as $imageKey) {
if (isset($bestMatch[$imageKey]) && !empty($bestMatch[$imageKey])) {
if (strpos($bestMatch[$imageKey], 'http') !== 0) {
$bestMatch[$imageKey] = 'https://retroachievements.org' . $bestMatch[$imageKey];
}
}
}
}
// Save to cache
file_put_contents($cachePath, json_encode($bestMatch));
return $bestMatch;
}
/**
* Download an image from RetroAchievements
*/
function downloadRAImage($url, $cachePath) {
if (empty($url)) {
return false;
}
// Create directory if it doesn't exist
$dir = dirname($cachePath);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
// Ensure URL starts with http
if (strpos($url, 'http') !== 0) {
$url = 'https://retroachievements.org' . $url;
}
// Check if the file already exists in cache
if (file_exists($cachePath)) {
return $cachePath;
}
// Download image with curl
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_USERAGENT, 'RetroHub/1.0');
$image = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200 || empty($image)) {
return false;
}
// Save to cache
if (file_put_contents($cachePath, $image)) {
return $cachePath;
}
return false;
}
/**
* Get game icon from RetroAchievements
*/
function getGameIcon($gameTitle, $console) {
// Get game data
$gameData = getGameData($gameTitle, $console);
if (!$gameData || empty($gameData['ImageIcon'])) {
return false;
}
// Generate cache path
$cacheKey = md5(cleanGameTitle($gameTitle) . '_' . $console);
$cachePath = RA_ICONS_CACHE_DIR . $cacheKey . '.png';
// Download icon
return downloadRAImage($gameData['ImageIcon'], $cachePath);
}
/**
* Get game screenshots from RetroAchievements
*/
function getGameScreenshots($gameTitle, $console) {
// Get game data
$gameData = getGameData($gameTitle, $console);
if (!$gameData) {
return false;
}
$result = [];
// Generate cache paths
$cacheKey = md5(cleanGameTitle($gameTitle) . '_' . $console);
// Title screenshot
if (!empty($gameData['ImageTitle'])) {
$titlePath = RA_SCREENSHOTS_CACHE_DIR . $cacheKey . '_title.png';
$downloaded = downloadRAImage($gameData['ImageTitle'], $titlePath);
if ($downloaded) {
$result['title'] = $downloaded;
}
}
// Ingame screenshot
if (!empty($gameData['ImageIngame'])) {
$ingamePath = RA_SCREENSHOTS_CACHE_DIR . $cacheKey . '_ingame.png';
$downloaded = downloadRAImage($gameData['ImageIngame'], $ingamePath);
if ($downloaded) {
$result['ingame'] = $downloaded;
}
}
// Box art
if (!empty($gameData['ImageBoxArt'])) {
$boxPath = RA_SCREENSHOTS_CACHE_DIR . $cacheKey . '_boxart.png';
$downloaded = downloadRAImage($gameData['ImageBoxArt'], $boxPath);
if ($downloaded) {
$result['boxart'] = $downloaded;
}
}
return empty($result) ? false : $result;
}
/**
* Get all available RetroAchievements game metadata for a game
*/
function getGameMetadata($gameTitle, $console) {
// Get game data
$gameData = getGameData($gameTitle, $console);
if (!$gameData) {
return false;
}
// Get icon and screenshots
$icon = getGameIcon($gameTitle, $console);
$screenshots = getGameScreenshots($gameTitle, $console);
return [
'title' => $gameData['Title'] ?? null,
'developer' => $gameData['Developer'] ?? null,
'publisher' => $gameData['Publisher'] ?? null,
'genre' => $gameData['Genre'] ?? null,
'released' => $gameData['Released'] ?? null,
'icon' => $icon,
'screenshot_title' => $screenshots && isset($screenshots['title']) ? $screenshots['title'] : null,
'screenshot_ingame' => $screenshots && isset($screenshots['ingame']) ? $screenshots['ingame'] : null,
'screenshot_boxart' => $screenshots && isset($screenshots['boxart']) ? $screenshots['boxart'] : null
];
}
// Initialize RetroAchievements
initRetroAchievements();

371
index.php
View File

@ -1,39 +1,346 @@
<!DOCTYPE html>
<html>
<?php
session_start();
include 'functions.php';
include 'includes/retroachievements.php';
<head>
<title>EmulatorJS Library - Arcade</title>
<link rel="stylesheet" type="text/css" href="style.css">
</head>
// Get current profile or set default
if (!isset($_SESSION['current_profile'])) {
$profiles = getProfiles();
if (count($profiles) > 0) {
$_SESSION['current_profile'] = $profiles[0]['id'];
} else {
// Create default profile if none exists
$defaultProfileId = createProfile("Player 1", "avatar1.png");
$_SESSION['current_profile'] = $defaultProfileId;
}
}
<body>
// Get RetroAchievements settings
$raSettings = getRetroAchievementsSettings();
<!-- Navbar -->
<nav>
<ul>
<li><a href="#">Arcade</a></li>
<li><a href="upload.php">Upload</a></li>
</ul>
</nav>
// Get general settings
$generalSettingsPath = 'config/general.json';
$generalSettings = [];
<br />
<!-- Game Arcade -->
<div class="arcadelist">
<?php
$files = scandir("./roms/");
foreach($files as $file) {
if(!in_array($file, array('.', '..'))) {
$file_url = 'play.php?game=' . urlencode($file);
if (file_exists("./img/$file.png")) {
echo("<a href='".$file_url."'><img class='linkimg' src='img/".$file.".png '/></a>");
} else {
echo("<a href='$file_url' class='link'><p>$file</p></a>");
}
}
}
?>
if (file_exists($generalSettingsPath)) {
$generalSettings = json_decode(file_get_contents($generalSettingsPath), true);
}
$siteName = $generalSettings['site_name'] ?? 'RetroHub';
// Get list of all ROMs
$games = [];
$files = scandir("./roms/");
foreach($files as $file) {
if(!in_array($file, array('.', '..'))) {
$name = pathinfo($file, PATHINFO_FILENAME);
$ext = strtolower(pathinfo($file, PATHINFO_EXTENSION));
// Determine console type based on extension
$console = getConsoleByExtension($ext);
// Default thumbnail path
$thumbnail = null;
// First check for a local thumbnail
if (file_exists("./img/$file.png")) {
$thumbnail = "img/$file.png";
}
// Check RetroAchievements for thumbnail if enabled and no local thumbnail
$metadata = null;
if ($raSettings['enabled'] && (!$thumbnail || $raSettings['override_local_images'])) {
// Fetch game metadata (this will also cache the images)
$metadata = getGameMetadata($name, $console);
// Use screenshot as thumbnail if available
if ($metadata) {
if ($metadata['screenshot_title']) {
$thumbnail = $metadata['screenshot_title'];
} elseif ($metadata['screenshot_ingame']) {
$thumbnail = $metadata['screenshot_ingame'];
} elseif ($metadata['icon']) {
$thumbnail = $metadata['icon'];
}
}
}
// If still no thumbnail, use default placeholder
if (!$thumbnail) {
$thumbnail = "img/placeholder_" . $console . ".png";
// If console-specific placeholder doesn't exist, use generic one
if (!file_exists($thumbnail)) {
$thumbnail = "img/placeholder_game.png";
}
}
$games[] = [
'file' => $file,
'name' => $name,
'console' => $console,
'thumbnail' => $thumbnail,
'play_url' => 'play.php?game=' . urlencode($file),
'metadata' => $metadata
];
}
}
// Sort games alphabetically
usort($games, function($a, $b) {
return strcasecmp($a['name'], $b['name']);
});
// Get current profile data
$currentProfile = getProfileById($_SESSION['current_profile']);
$allProfiles = getProfiles();
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo htmlspecialchars($siteName); ?> - Game Library</title>
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
</head>
<body>
<header>
<div class="container">
<nav>
<a href="index.php" class="logo">
<i class="fas fa-gamepad"></i>
<span><?php echo htmlspecialchars($siteName); ?></span>
</a>
<ul class="nav-menu">
<li><a href="index.php" class="active">Games</a></li>
<li><a href="upload.php">Upload ROMs</a></li>
<li><a href="bios.php">BIOS Files</a></li>
<li><a href="settings.php">Settings</a></li>
</ul>
<div class="profile-menu">
<div class="current-profile" id="profile-toggle">
<img src="img/avatars/<?php echo $currentProfile['avatar']; ?>" alt="Profile">
<span><?php echo $currentProfile['name']; ?></span>
<i class="fas fa-chevron-down"></i>
</div>
<div class="profile-dropdown" id="profile-dropdown">
<ul class="profile-list">
<?php foreach($allProfiles as $profile): ?>
<li class="profile-item <?php echo ($profile['id'] == $_SESSION['current_profile']) ? 'active' : ''; ?>"
data-profile-id="<?php echo $profile['id']; ?>">
<img src="img/avatars/<?php echo $profile['avatar']; ?>" alt="<?php echo $profile['name']; ?>">
<span class="profile-name"><?php echo $profile['name']; ?></span>
</li>
<?php endforeach; ?>
</ul>
<div class="profile-actions">
<button class="add-profile-btn" id="add-profile-btn">
<i class="fas fa-plus-circle"></i>
<span>Add New Profile</span>
</button>
</div>
</div>
</div>
</nav>
</div>
</body>
</header>
<main>
<div class="container">
<div class="page-header">
<h1 class="page-title">Game Library</h1>
<div class="library-actions">
<div class="search-bar">
<input type="text" id="game-search" placeholder="Search games..." class="search-input">
<button class="search-btn"><i class="fas fa-search"></i></button>
</div>
<div class="view-options">
<button class="view-btn active" data-view="grid"><i class="fas fa-th"></i></button>
<button class="view-btn" data-view="list"><i class="fas fa-list"></i></button>
</div>
</div>
</div>
<!-- Console Filter Tabs -->
<div class="console-filters">
<button class="console-filter active" data-console="all">All</button>
<button class="console-filter" data-console="nes">NES</button>
<button class="console-filter" data-console="snes">SNES</button>
<button class="console-filter" data-console="n64">N64</button>
<button class="console-filter" data-console="gb">Game Boy</button>
<button class="console-filter" data-console="gba">GBA</button>
<button class="console-filter" data-console="segaMD">Genesis/MD</button>
<button class="console-filter" data-console="psx">PlayStation</button>
<button class="console-filter" data-console="other">Other</button>
</div>
<div class="gallery" id="game-gallery">
<?php foreach($games as $game): ?>
<a href="<?php echo $game['play_url']; ?>" class="game-card" data-console="<?php echo $game['console']; ?>" data-name="<?php echo strtolower($game['name']); ?>">
<div class="game-image-container">
<img src="<?php echo $game['thumbnail']; ?>" alt="<?php echo $game['name']; ?>" class="game-image">
<?php if($game['metadata'] && isset($game['metadata']['released'])): ?>
<span class="game-year"><?php echo substr($game['metadata']['released'], 0, 4); ?></span>
<?php endif; ?>
</div>
<div class="game-info">
<h3 class="game-title"><?php echo $game['name']; ?></h3>
<div class="game-details">
<span class="game-console"><?php echo getConsoleFriendlyName($game['console']); ?></span>
<?php if($game['metadata'] && isset($game['metadata']['developer'])): ?>
<span class="game-developer"><?php echo $game['metadata']['developer']; ?></span>
<?php endif; ?>
</div>
</div>
</a>
<?php endforeach; ?>
<?php if(count($games) == 0): ?>
<div class="empty-state">
<i class="fas fa-folder-open"></i>
<h3>No games found</h3>
<p>Upload some ROMs to get started!</p>
<a href="upload.php" class="btn">Upload ROMs</a>
</div>
<?php endif; ?>
</div>
</div>
</main>
<!-- Add Profile Modal -->
<div class="modal-overlay" id="profile-modal">
<div class="modal">
<div class="modal-header">
<h2 class="modal-title">Create New Profile</h2>
<button class="close-modal" id="close-profile-modal">&times;</button>
</div>
<div class="modal-body">
<form id="profile-form" action="profile_action.php" method="post">
<div class="form-group">
<label for="profile-name" class="form-label">Profile Name</label>
<input type="text" id="profile-name" name="name" class="form-input" required>
</div>
<div class="form-group">
<label class="form-label">Select Avatar</label>
<div class="avatar-selector">
<?php for($i = 1; $i <= 8; $i++): ?>
<img src="img/avatars/avatar<?php echo $i; ?>.png"
class="avatar-option <?php echo ($i == 1) ? 'selected' : ''; ?>"
data-avatar="avatar<?php echo $i; ?>.png">
<?php endfor; ?>
</div>
<input type="hidden" id="selected-avatar" name="avatar" value="avatar1.png">
</div>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" id="cancel-profile">Cancel</button>
<button class="btn" id="save-profile">Create Profile</button>
</div>
</div>
</div>
<script src="js/profiles.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Game search functionality
const searchInput = document.getElementById('game-search');
const gameGallery = document.getElementById('game-gallery');
const gameCards = document.querySelectorAll('.game-card');
searchInput.addEventListener('input', function() {
const searchTerm = this.value.toLowerCase().trim();
gameCards.forEach(card => {
const gameName = card.dataset.name;
if (gameName.includes(searchTerm)) {
card.style.display = '';
} else {
card.style.display = 'none';
}
});
// Check if no games are visible
checkEmptyState();
});
// Console filter functionality
const consoleFilters = document.querySelectorAll('.console-filter');
consoleFilters.forEach(filter => {
filter.addEventListener('click', function() {
// Update active filter
consoleFilters.forEach(f => f.classList.remove('active'));
this.classList.add('active');
const consoleType = this.dataset.console;
gameCards.forEach(card => {
if (consoleType === 'all' || card.dataset.console === consoleType ||
(consoleType === 'other' && !['nes', 'snes', 'n64', 'gb', 'gba', 'segaMD', 'psx'].includes(card.dataset.console))) {
card.style.display = '';
} else {
card.style.display = 'none';
}
});
// Check if no games are visible
checkEmptyState();
});
});
// View toggle functionality
const viewButtons = document.querySelectorAll('.view-btn');
viewButtons.forEach(button => {
button.addEventListener('click', function() {
viewButtons.forEach(btn => btn.classList.remove('active'));
this.classList.add('active');
const viewType = this.dataset.view;
gameGallery.className = viewType === 'grid' ? 'gallery' : 'gallery list-view';
});
});
// Function to check if no games are visible and show empty state
function checkEmptyState() {
let visibleGames = 0;
gameCards.forEach(card => {
if (card.style.display !== 'none') {
visibleGames++;
}
});
// Create or remove empty state message
let emptyState = gameGallery.querySelector('.empty-search-state');
if (visibleGames === 0 && gameCards.length > 0) {
if (!emptyState) {
emptyState = document.createElement('div');
emptyState.className = 'empty-search-state';
emptyState.innerHTML = `
<i class="fas fa-search"></i>
<h3>No games found</h3>
<p>Try a different search term or filter</p>
`;
gameGallery.appendChild(emptyState);
}
} else if (emptyState) {
emptyState.remove();
}
}
});
</script>
</body>
</html>

44
js/bios.js Normal file
View File

@ -0,0 +1,44 @@
/**
* BIOS management functionality
*/
document.addEventListener('DOMContentLoaded', function() {
// File input styling
const biosFileInput = document.getElementById('bios-file');
const selectedFileName = document.querySelector('.selected-file-name');
if (biosFileInput && selectedFileName) {
biosFileInput.addEventListener('change', function() {
if (biosFileInput.files.length > 0) {
selectedFileName.textContent = biosFileInput.files[0].name;
} else {
selectedFileName.textContent = 'No file selected';
}
});
}
// Console type dropdown
const consoleTypeSelect = document.getElementById('console-type');
if (consoleTypeSelect) {
consoleTypeSelect.addEventListener('change', function() {
// Update any UI based on console selection if needed
const selectedConsole = consoleTypeSelect.value;
// You can add functionality here to show specific information
// based on the selected console type if desired
});
}
// BIOS file deletion confirmation
const biosDeleteButtons = document.querySelectorAll('.bios-delete-btn');
if (biosDeleteButtons.length > 0) {
biosDeleteButtons.forEach(button => {
button.addEventListener('click', function(e) {
if (!confirm('Are you sure you want to delete this BIOS file?')) {
e.preventDefault();
}
});
});
}
});

88
js/play.js Normal file
View File

@ -0,0 +1,88 @@
/**
* Game player and save state functionality
*/
document.addEventListener('DOMContentLoaded', function() {
// Save state modal
const saveStateBtn = document.getElementById('save-state-btn');
const saveStateModal = document.getElementById('save-state-modal');
const closeSaveModal = document.getElementById('close-save-modal');
const cancelSave = document.getElementById('cancel-save');
const saveGameState = document.getElementById('save-game-state');
if (saveStateBtn && saveStateModal) {
// Open modal
saveStateBtn.addEventListener('click', function() {
saveStateModal.classList.add('active');
});
// Close modal
function closeSaveStateModal() {
saveStateModal.classList.remove('active');
}
if (closeSaveModal) closeSaveModal.addEventListener('click', closeSaveStateModal);
if (cancelSave) cancelSave.addEventListener('click', closeSaveStateModal);
// Save game state
if (saveGameState) {
saveGameState.addEventListener('click', function() {
// The actual saving is handled by EJS_onSaveState
// This just triggers the emulator's save state function
if (typeof EJS_emulator !== 'undefined') {
try {
EJS_emulator.gameManager.saveState();
} catch (e) {
alert('Error triggering save state: ' + e.message);
}
} else {
alert('Emulator not ready. Please wait and try again.');
}
});
}
}
// Load save state
const loadStateButtons = document.querySelectorAll('.load-state');
if (loadStateButtons.length > 0) {
loadStateButtons.forEach(button => {
button.addEventListener('click', function() {
const slot = this.dataset.slot;
if (typeof loadState === 'function') {
loadState(slot);
} else {
alert('Load state function not available');
}
});
});
}
// Delete save state
const deleteStateButtons = document.querySelectorAll('.delete-state');
if (deleteStateButtons.length > 0) {
deleteStateButtons.forEach(button => {
button.addEventListener('click', function() {
const slot = this.dataset.slot;
const profileId = document.querySelector('.current-profile').dataset.profileId;
const gameName = document.querySelector('.page-title').textContent;
if (confirm('Are you sure you want to delete this save state?')) {
const xhr = new XMLHttpRequest();
xhr.open('POST', 'delete_state.php', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onload = function() {
if (xhr.status === 200) {
// Reload the page to update the save states list
window.location.reload();
} else {
alert('Error deleting save state: ' + xhr.responseText);
}
};
xhr.send(`profile_id=${profileId}&game=${gameName}&slot=${slot}`);
}
});
});
}
});

224
js/profiles.js Normal file
View File

@ -0,0 +1,224 @@
/**
* Profile management functionality
*/
document.addEventListener('DOMContentLoaded', function() {
// Profile dropdown toggle
const profileToggle = document.getElementById('profile-toggle');
const profileDropdown = document.getElementById('profile-dropdown');
if (profileToggle && profileDropdown) {
profileToggle.addEventListener('click', function(e) {
e.stopPropagation();
profileDropdown.classList.toggle('active');
});
// Close dropdown when clicking outside
document.addEventListener('click', function(e) {
if (!profileDropdown.contains(e.target) && e.target !== profileToggle) {
profileDropdown.classList.remove('active');
}
});
// Profile selection
const profileItems = document.querySelectorAll('.profile-item');
profileItems.forEach(item => {
item.addEventListener('click', function() {
const profileId = this.dataset.profileId;
// Make AJAX request to switch profile
const xhr = new XMLHttpRequest();
xhr.open('POST', 'profile_action.php', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onload = function() {
if (xhr.status === 200) {
// Reload the page to reflect the new profile
window.location.reload();
} else {
console.error('Error switching profile');
}
};
xhr.send(`action=switch&profile_id=${profileId}`);
});
});
}
// Profile modal handling
const addProfileBtn = document.getElementById('add-profile-btn');
const profileModal = document.getElementById('profile-modal');
const closeProfileModal = document.getElementById('close-profile-modal');
const cancelProfile = document.getElementById('cancel-profile');
const saveProfile = document.getElementById('save-profile');
const profileForm = document.getElementById('profile-form');
const avatarOptions = document.querySelectorAll('.avatar-option');
const selectedAvatarInput = document.getElementById('selected-avatar');
if (addProfileBtn && profileModal) {
// Open modal
addProfileBtn.addEventListener('click', function() {
profileModal.classList.add('active');
profileDropdown.classList.remove('active');
});
// Close modal
function closeModal() {
profileModal.classList.remove('active');
profileForm.reset();
// Reset selected avatar
avatarOptions.forEach(option => {
option.classList.remove('selected');
});
avatarOptions[0].classList.add('selected');
selectedAvatarInput.value = avatarOptions[0].dataset.avatar;
}
if (closeProfileModal) closeProfileModal.addEventListener('click', closeModal);
if (cancelProfile) cancelProfile.addEventListener('click', closeModal);
// Avatar selection
avatarOptions.forEach(option => {
option.addEventListener('click', function() {
avatarOptions.forEach(opt => opt.classList.remove('selected'));
this.classList.add('selected');
selectedAvatarInput.value = this.dataset.avatar;
});
});
// Save new profile
if (saveProfile) {
saveProfile.addEventListener('click', function() {
const profileName = document.getElementById('profile-name');
if (!profileName.value.trim()) {
alert('Please enter a profile name');
profileName.focus();
return;
}
// Submit the form to create a new profile
const formData = new FormData(profileForm);
formData.append('action', 'create');
const xhr = new XMLHttpRequest();
xhr.open('POST', 'profile_action.php', true);
xhr.onload = function() {
if (xhr.status === 200) {
// Reload the page to show the new profile
window.location.reload();
} else {
alert('Error creating profile: ' + xhr.responseText);
}
};
xhr.send(formData);
});
}
}
});
// Add to the bottom of js/profiles.js or create a new file
/**
* Profile editing functionality
*/
document.addEventListener('DOMContentLoaded', function() {
// Profile edit functionality
const editProfileBtns = document.querySelectorAll('.edit-profile-btn');
const editProfileModal = document.getElementById('edit-profile-modal');
const closeEditProfileModal = document.getElementById('close-edit-profile-modal');
const cancelEditProfile = document.getElementById('cancel-edit-profile');
const updateProfile = document.getElementById('update-profile');
const editProfileForm = document.getElementById('edit-profile-form');
const editProfileId = document.getElementById('edit-profile-id');
const editProfileName = document.getElementById('edit-profile-name');
const editAvatarSelector = document.getElementById('edit-avatar-selector');
const editSelectedAvatar = document.getElementById('edit-selected-avatar');
if (editProfileBtns.length > 0 && editProfileModal) {
// Open modal when Edit button is clicked
editProfileBtns.forEach(btn => {
btn.addEventListener('click', function() {
const profileId = this.dataset.profileId;
// Set profile ID in the form
if (editProfileId) {
editProfileId.value = profileId;
}
// Manually fetch profile data since we already have it in the DOM
const profileItem = document.querySelector(`.profile-item[data-profile-id="${profileId}"]`);
if (profileItem) {
const profileName = profileItem.querySelector('.profile-name').textContent;
const profileAvatar = profileItem.querySelector('img').src.split('/').pop();
// Set profile name
if (editProfileName) {
editProfileName.value = profileName;
}
// Set selected avatar
if (editAvatarSelector) {
const avatarOptions = editAvatarSelector.querySelectorAll('.avatar-option');
avatarOptions.forEach(option => {
option.classList.remove('selected');
const optionAvatar = option.dataset.avatar;
if (profileAvatar.includes(optionAvatar)) {
option.classList.add('selected');
if (editSelectedAvatar) {
editSelectedAvatar.value = optionAvatar;
}
}
});
}
// Show the modal
editProfileModal.classList.add('active');
}
});
});
// Close modal
function closeEditProfileModalFn() {
if (editProfileModal) {
editProfileModal.classList.remove('active');
}
}
if (closeEditProfileModal) {
closeEditProfileModal.addEventListener('click', closeEditProfileModalFn);
}
if (cancelEditProfile) {
cancelEditProfile.addEventListener('click', closeEditProfileModalFn);
}
// Handle avatar selection in edit modal
if (editAvatarSelector) {
const avatarOptions = editAvatarSelector.querySelectorAll('.avatar-option');
avatarOptions.forEach(option => {
option.addEventListener('click', function() {
avatarOptions.forEach(opt => opt.classList.remove('selected'));
this.classList.add('selected');
if (editSelectedAvatar) {
editSelectedAvatar.value = this.dataset.avatar;
}
});
});
}
// Handle form submission
if (updateProfile && editProfileForm) {
updateProfile.addEventListener('click', function() {
// Manual validation
if (editProfileName && !editProfileName.value.trim()) {
alert('Please enter a profile name');
editProfileName.focus();
return;
}
// Submit the form
editProfileForm.submit();
});
}
}
});

77
js/upload.js Normal file
View File

@ -0,0 +1,77 @@
/**
* Handles ROM and BIOS file upload functionality
*/
document.addEventListener('DOMContentLoaded', function() {
// ROM upload functionality
const romUploadBox = document.getElementById('rom-upload-box');
const romFiles = document.getElementById('rom-files');
const romUploadForm = document.getElementById('rom-upload-form');
if (romUploadBox && romFiles) {
// Trigger file input when clicking on the upload box
romUploadBox.addEventListener('click', function() {
romFiles.click();
});
// Prevent default drag behaviors
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
romUploadBox.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
// Highlight drop area when item is dragged over
['dragenter', 'dragover'].forEach(eventName => {
romUploadBox.addEventListener(eventName, highlight, false);
});
['dragleave', 'drop'].forEach(eventName => {
romUploadBox.addEventListener(eventName, unhighlight, false);
});
function highlight() {
romUploadBox.classList.add('highlight');
}
function unhighlight() {
romUploadBox.classList.remove('highlight');
}
// Handle dropped files
romUploadBox.addEventListener('drop', handleDrop, false);
function handleDrop(e) {
const dt = e.dataTransfer;
const files = dt.files;
romFiles.files = files;
// Submit the form to upload the files
romUploadForm.submit();
}
// Auto-submit when files are selected
romFiles.addEventListener('change', function() {
if (romFiles.files.length > 0) {
romUploadForm.submit();
}
});
}
// BIOS file input styling
const biosFileInput = document.getElementById('bios-file');
const selectedFileName = document.querySelector('.selected-file-name');
if (biosFileInput && selectedFileName) {
biosFileInput.addEventListener('change', function() {
if (biosFileInput.files.length > 0) {
selectedFileName.textContent = biosFileInput.files[0].name;
} else {
selectedFileName.textContent = 'No file selected';
}
});
}
});

945
play.php
View File

@ -1,167 +1,826 @@
<!DOCTYPE html>
<html>
<head>
<?php
<?php
session_start();
include 'functions.php';
include 'includes/retroachievements.php';
$settings = parse_ini_file("./settings.ini");
// Ensure a game has been specified
if (!isset($_GET['game'])) {
header('Location: index.php');
exit;
}
//Write system extension arrays
// Nintendo
$snes = ["smc", "sfc", "fig", "swc", "bs", "st"];
$gba = ["gba"];
$gb = ["gb", "gbc", "dmg"];
$nes = ["fds", "nes", "unif", "unf"];
$vb = ["vb", "vboy"];
$nds = ["nds"];
$n64 = ["n64", "z64", "v64", "u1", "ndd"];
// Sega
$sms = ["sms"];
$smd = ["smd", "md"];
$gg = ["gg"];
// Other
$psx = ["pbp", "chd"];
// Get current profile or set default
if (!isset($_SESSION['current_profile'])) {
$profiles = getProfiles();
if (count($profiles) > 0) {
$_SESSION['current_profile'] = $profiles[0]['id'];
} else {
// Create default profile if none exists
$defaultProfileId = createProfile("Player 1", "avatar1.png");
$_SESSION['current_profile'] = $defaultProfileId;
}
}
// Get RetroAchievements settings
$raSettings = getRetroAchievementsSettings();
//Find console
$name = basename($_GET['game']);
$ext = explode(".", $name);
$ext = strtolower(end($ext));
// Get general settings
$generalSettingsPath = 'config/general.json';
$generalSettings = [];
//for zipfile
if ($ext=='zip'){
$zip = new ZipArchive;
if ($zip->open("roms/".$name))
{
$names =$zip->getNameIndex(0);
$ext0 = explode(".", $names);
$ext = strtolower(end($ext0));
}
}
if (file_exists($generalSettingsPath)) {
$generalSettings = json_decode(file_get_contents($generalSettingsPath), true);
}
if (in_array($ext, $nes)) { $console = 'nes'; }
else if (in_array($ext, $snes)) { $console = 'snes'; }
else if (in_array($ext, $n64)) { $console = 'n64'; }
else if (in_array($ext, $gb)) { $console = 'gb'; }
else if (in_array($ext, $vb)) { $console = 'vb'; }
else if (in_array($ext, $gba)) { $console = 'gba'; }
else if (in_array($ext, $nds)) { $console = 'nds'; }
else if (in_array($ext, $sms)) { $console = 'segaMS'; }
else if (in_array($ext, $smd)) { $console = 'segaMD'; }
else if (in_array($ext, $gg)) { $console = 'segaGG'; }
$siteName = $generalSettings['site_name'] ?? 'RetroHub';
else if (in_array($ext, $psx)) { $console = 'psx';};
?>
<title><?php echo($name); ?></title>
<style>
body {
background-color: #333;
}
// Get profile and game information
$currentProfile = getProfileById($_SESSION['current_profile']);
$allProfiles = getProfiles();
$gameFile = $_GET['game'];
nav {
background-color: #5f5f5f;
border-radius: 5px;
}
nav ul {
margin: 0;
padding: 0;
list-style: none;
display: flex;
justify-content: center;
// Find console based on file extension
$name = basename($gameFile);
$ext = explode(".", $name);
$ext = strtolower(end($ext));
}
nav a {
display: block;
padding: 1rem;
text-decoration: none;
color: #c5c5c5;
border-radius: 5px;
}
nav a:hover {
background-color: #868686;
color: #fff;
}
// For zipfile
if ($ext == 'zip') {
$zip = new ZipArchive;
if ($zip->open("roms/".$name)) {
$names = $zip->getNameIndex(0);
$ext0 = explode(".", $names);
$ext = strtolower(end($ext0));
}
}
</style>
</head>
// Determine console type
$console = getConsoleByExtension($ext);
$consoleName = getConsoleFriendlyName($console);
<body>
// Get save states for this game and profile
$saveStates = getSaveStates($gameFile, $_SESSION['current_profile']);
<nav>
<ul>
<li><a href="index.php">Arcade</a></li>
<li><a href="upload.php">Upload</a></li>
<li style="width:10%;"></li>
<li><p>Playing: <?php echo($name); ?></p></li>
</ul>
</nav>
// Get game metadata from RetroAchievements if enabled
$gameMetadata = null;
$gameScreenshots = [];
<div style='width:640px;height:480px;max-width:100%;margin: auto auto;'>
<div id='game'></div>
</div>
<script type='text/javascript'>
EJS_player = '#game';
<?php
if (file_exists("./bios/$console.zip")) {
$bios = "EJS_biosUrl = './bios/$console.zip';";
} else {
$bios = "";
if ($raSettings['enabled']) {
$gameName = pathinfo($gameFile, PATHINFO_FILENAME);
$gameMetadata = getGameMetadata($gameName, $console);
if ($gameMetadata) {
if ($gameMetadata['screenshot_title']) {
$gameScreenshots[] = $gameMetadata['screenshot_title'];
}
if ($gameMetadata['screenshot_ingame']) {
$gameScreenshots[] = $gameMetadata['screenshot_ingame'];
}
}
}
echo("
EJS_core = '$console';
$bios
EJS_gameUrl = './roms/".$_GET['game']."';
EJS_pathtodata = 'https://cdn.emulatorjs.org/stable/data/';");
?>
// Parse settings
$settings = parse_ini_file("./settings.ini");
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo htmlspecialchars($name); ?> - <?php echo htmlspecialchars($siteName); ?></title>
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
/* Additional styles specific to the game player */
.game-container {
position: relative;
overflow: hidden;
background-color: #000;
border-radius: var(--border-radius);
}
#game {
width: 100%;
height: 100%;
aspect-ratio: 16/9;
}
.game-details-section {
margin-top: 2rem;
display: grid;
grid-template-columns: 1fr 300px;
gap: 2rem;
}
@media (max-width: 991px) {
.game-details-section {
grid-template-columns: 1fr;
}
}
.game-info-panel {
background-color: var(--secondary-bg);
border-radius: var(--border-radius);
padding: 1.5rem;
}
.game-info-header {
display: flex;
align-items: center;
margin-bottom: 1rem;
}
.game-info-icon {
width: 48px;
height: 48px;
margin-right: 1rem;
border-radius: 8px;
}
.game-info-title {
flex: 1;
}
.game-info-title h2 {
margin: 0 0 0.3rem 0;
}
.game-info-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 1.5rem;
}
.game-info-table td {
padding: 0.5rem 0;
border-bottom: 1px solid var(--nav-bg);
}
.game-info-table td:first-child {
font-weight: 500;
width: 120px;
color: var(--accent-color);
}
.game-screenshots {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-top: 1.5rem;
}
.game-screenshot {
border-radius: var(--border-radius);
overflow: hidden;
cursor: pointer;
}
.game-screenshot img {
width: 100%;
height: auto;
transition: transform 0.3s ease;
}
.game-screenshot:hover img {
transform: scale(1.05);
}
.save-states {
background-color: var(--secondary-bg);
border-radius: var(--border-radius);
padding: 1.5rem;
height: fit-content;
}
.save-states-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.save-states-title {
font-size: 1.2rem;
color: var(--text-primary);
}
.save-slots {
display: grid;
gap: 1rem;
}
.save-slot {
background-color: var(--card-bg);
border-radius: var(--border-radius);
overflow: hidden;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.save-slot:hover {
transform: translateY(-3px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.save-slot-preview {
position: relative;
width: 100%;
aspect-ratio: 4/3;
background-color: #000;
overflow: hidden;
}
.save-slot-preview img {
width: 100%;
height: 100%;
object-fit: cover;
}
.save-slot-empty {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
background-color: var(--primary-bg);
}
.save-slot-empty i {
font-size: 2rem;
color: var(--nav-bg);
margin-bottom: 0.5rem;
}
.save-slot-info {
padding: 0.8rem;
}
.save-slot-title {
font-size: 0.9rem;
margin-bottom: 0.3rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.save-slot-time {
font-size: 0.8rem;
color: var(--accent-color);
}
.save-slot-actions {
display: flex;
justify-content: space-between;
padding: 0.5rem 0.8rem;
border-top: 1px solid var(--primary-bg);
}
.save-slot-btn {
background: none;
border: none;
color: var(--text-secondary);
cursor: pointer;
padding: 0.3rem;
border-radius: 4px;
transition: background-color 0.2s ease, color 0.2s ease;
}
.save-slot-btn:hover {
background-color: var(--nav-bg);
color: var(--accent-color);
}
.empty-states {
padding: 2rem;
text-align: center;
background-color: var(--card-bg);
border-radius: var(--border-radius);
}
.empty-states i {
font-size: 3rem;
color: var(--nav-bg);
margin-bottom: 1rem;
}
.empty-states h3 {
margin-bottom: 0.5rem;
}
.empty-states p {
color: var(--text-secondary);
margin-bottom: 1.5rem;
}
/* Screenshot modal */
.screenshot-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.9);
display: flex;
justify-content: center;
align-items: center;
z-index: 2000;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s ease, visibility 0.3s ease;
}
.screenshot-modal.active {
opacity: 1;
visibility: visible;
}
.screenshot-modal-close {
position: absolute;
top: 20px;
right: 20px;
background: none;
border: none;
color: white;
font-size: 2rem;
cursor: pointer;
}
.screenshot-modal-image {
max-width: 90%;
max-height: 90%;
border-radius: 5px;
}
</style>
</head>
<body>
<header>
<div class="container">
<nav>
<a href="index.php" class="logo">
<i class="fas fa-gamepad"></i>
<span><?php echo htmlspecialchars($siteName); ?></span>
</a>
<ul class="nav-menu">
<li><a href="index.php">Games</a></li>
<li><a href="upload.php">Upload ROMs</a></li>
<li><a href="bios.php">BIOS Files</a></li>
<li><a href="settings.php">Settings</a></li>
</ul>
<div class="profile-menu">
<div class="current-profile" id="profile-toggle">
<img src="img/avatars/<?php echo $currentProfile['avatar']; ?>" alt="Profile">
<span><?php echo $currentProfile['name']; ?></span>
<i class="fas fa-chevron-down"></i>
</div>
<div class="profile-dropdown" id="profile-dropdown">
<ul class="profile-list">
<?php foreach($allProfiles as $profile): ?>
<li class="profile-item <?php echo ($profile['id'] == $_SESSION['current_profile']) ? 'active' : ''; ?>"
data-profile-id="<?php echo $profile['id']; ?>">
<img src="img/avatars/<?php echo $profile['avatar']; ?>" alt="<?php echo $profile['name']; ?>">
<span class="profile-name"><?php echo $profile['name']; ?></span>
</li>
<?php endforeach; ?>
</ul>
<div class="profile-actions">
<button class="add-profile-btn" id="add-profile-btn">
<i class="fas fa-plus-circle"></i>
<span>Add New Profile</span>
</button>
</div>
</div>
</div>
</nav>
</div>
</header>
<main>
<div class="container">
<div class="page-header">
<div>
<h1 class="page-title">
<?php if ($gameMetadata && isset($gameMetadata['title'])): ?>
<?php echo htmlspecialchars($gameMetadata['title']); ?>
<?php else: ?>
<?php echo htmlspecialchars($name); ?>
<?php endif; ?>
</h1>
<span class="game-console-badge"><?php echo $consoleName; ?></span>
</div>
<a href="index.php" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i>
Back to Library
</a>
</div>
<div class="game-container">
<div id="game"></div>
</div>
<div class="game-details-section">
<div class="game-info-panel">
<?php if ($gameMetadata && $gameMetadata['icon']): ?>
<div class="game-info-header">
<img src="<?php echo $gameMetadata['icon']; ?>" alt="Game Icon" class="game-info-icon">
<div class="game-info-title">
<h2><?php echo htmlspecialchars($gameMetadata['title'] ?? $name); ?></h2>
<span class="game-console"><?php echo $consoleName; ?></span>
</div>
</div>
<?php else: ?>
<div class="game-info-header">
<div class="game-info-title">
<h2><?php echo htmlspecialchars($name); ?></h2>
<span class="game-console"><?php echo $consoleName; ?></span>
</div>
</div>
<?php endif; ?>
<?php if ($gameMetadata): ?>
<table class="game-info-table">
<?php if (isset($gameMetadata['developer'])): ?>
<tr>
<td>Developer</td>
<td><?php echo htmlspecialchars($gameMetadata['developer']); ?></td>
</tr>
<?php endif; ?>
<?php if (isset($gameMetadata['publisher'])): ?>
<tr>
<td>Publisher</td>
<td><?php echo htmlspecialchars($gameMetadata['publisher']); ?></td>
</tr>
<?php endif; ?>
<?php if (isset($gameMetadata['genre'])): ?>
<tr>
<td>Genre</td>
<td><?php echo htmlspecialchars($gameMetadata['genre']); ?></td>
</tr>
<?php endif; ?>
<?php if (isset($gameMetadata['released'])): ?>
<tr>
<td>Released</td>
<td><?php echo htmlspecialchars($gameMetadata['released']); ?></td>
</tr>
<?php endif; ?>
</table>
<?php endif; ?>
<div class="game-player-controls">
<button class="btn" id="save-state-btn">
<i class="fas fa-save"></i>
Save State
</button>
<button class="btn btn-secondary" id="fullscreen-btn">
<i class="fas fa-expand"></i>
Fullscreen
</button>
</div>
<?php if (!empty($gameScreenshots)): ?>
<div class="game-screenshots-section">
<h3>Screenshots</h3>
<div class="game-screenshots">
<?php foreach($gameScreenshots as $index => $screenshot): ?>
<div class="game-screenshot" data-index="<?php echo $index; ?>">
<img src="<?php echo $screenshot; ?>" alt="Game Screenshot">
</div>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
</div>
<div class="save-states">
<div class="save-states-header">
<h2 class="save-states-title">Save States</h2>
</div>
<?php if(count($saveStates) > 0): ?>
<div class="save-slots">
<?php foreach($saveStates as $saveState): ?>
<div class="save-slot" data-slot="<?php echo $saveState['slot']; ?>">
<div class="save-slot-preview">
<?php if($saveState['screenshot']): ?>
<img src="<?php echo $saveState['screenshot']; ?>" alt="Save State <?php echo $saveState['slot']; ?>">
<?php else: ?>
<div class="save-slot-empty">
<i class="fas fa-save"></i>
<span>No Preview</span>
</div>
<?php endif; ?>
</div>
<div class="save-slot-info">
<h3 class="save-slot-title">Save Slot <?php echo $saveState['slot']; ?></h3>
<span class="save-slot-time"><?php echo date('M j, Y g:i A', $saveState['timestamp']); ?></span>
</div>
<div class="save-slot-actions">
<button class="save-slot-btn load-state" data-slot="<?php echo $saveState['slot']; ?>">
<i class="fas fa-play"></i>
Load
</button>
<button class="save-slot-btn delete-state" data-slot="<?php echo $saveState['slot']; ?>">
<i class="fas fa-trash"></i>
Delete
</button>
</div>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<div class="empty-states">
<i class="fas fa-save"></i>
<h3>No Save States Yet</h3>
<p>Use the Save State button to save your progress.</p>
</div>
<?php endif; ?>
</div>
</div>
</div>
</main>
<!-- Save State Modal -->
<div class="modal-overlay" id="save-state-modal">
<div class="modal">
<div class="modal-header">
<h2 class="modal-title">Save Game State</h2>
<button class="close-modal" id="close-save-modal">&times;</button>
</div>
<div class="modal-body">
<form id="save-state-form">
<div class="form-group">
<label for="save-slot" class="form-label">Select Save Slot</label>
<select id="save-slot" name="slot" class="form-input">
<?php for($i = 1; $i <= 10; $i++): ?>
<option value="<?php echo $i; ?>">Slot <?php echo $i; ?></option>
<?php endfor; ?>
</select>
</div>
<div class="form-group">
<div class="save-slot-warning">
<i class="fas fa-exclamation-triangle"></i>
<p>If a save already exists in this slot, it will be overwritten.</p>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" id="cancel-save">Cancel</button>
<button class="btn" id="save-game-state">Save Game</button>
</div>
</div>
</div>
<!-- Add Profile Modal (same as in index.php) -->
<div class="modal-overlay" id="profile-modal">
<div class="modal">
<div class="modal-header">
<h2 class="modal-title">Create New Profile</h2>
<button class="close-modal" id="close-profile-modal">&times;</button>
</div>
<div class="modal-body">
<form id="profile-form" action="profile_action.php" method="post">
<div class="form-group">
<label for="profile-name" class="form-label">Profile Name</label>
<input type="text" id="profile-name" name="name" class="form-input" required>
</div>
<div class="form-group">
<label class="form-label">Select Avatar</label>
<div class="avatar-selector">
<?php for($i = 1; $i <= 8; $i++): ?>
<img src="img/avatars/avatar<?php echo $i; ?>.png"
class="avatar-option <?php echo ($i == 1) ? 'selected' : ''; ?>"
data-avatar="avatar<?php echo $i; ?>.png">
<?php endfor; ?>
</div>
<input type="hidden" id="selected-avatar" name="avatar" value="avatar1.png">
</div>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" id="cancel-profile">Cancel</button>
<button class="btn" id="save-profile">Create Profile</button>
</div>
</div>
</div>
<!-- Screenshot Modal -->
<div class="screenshot-modal" id="screenshot-modal">
<button class="screenshot-modal-close" id="close-screenshot-modal">×</button>
<img src="" alt="Game Screenshot" class="screenshot-modal-image" id="screenshot-modal-image">
</div>
<script type="text/javascript">
// EmulatorJS setup
EJS_player = '#game';
EJS_core = '<?php echo $console; ?>';
<?php
if (file_exists("./bios/$console.zip")) {
echo "EJS_biosUrl = './bios/$console.zip';";
}
?>
EJS_gameUrl = './roms/<?php echo $gameFile; ?>';
EJS_pathtodata = 'https://cdn.emulatorjs.org/stable/data/';
// Profile ID for save states
const profileId = '<?php echo $_SESSION['current_profile']; ?>';
const gameName = '<?php echo $gameFile; ?>';
// Save state handling
EJS_onSaveState = function(data) {
const stateBlob = new Blob([data.state], { type: "application/octet-stream" });
const screenshotBlob = new Blob([data.screenshot], { type: "image/png" });
const gameName = "<?php echo($_GET['game']); ?>";
const slotNumber = document.getElementById('save-slot').value;
const formData = new FormData();
formData.append("gameName", gameName); // Add gameName to the form data
formData.append("state", stateBlob, `${gameName}.state`); // Construct filename
formData.append("screenshot", screenshotBlob, `${gameName}.img`); // Construct filename
formData.append("profile_id", profileId);
formData.append("game_name", gameName);
formData.append("slot", slotNumber);
formData.append("state", stateBlob, `${gameName}_${slotNumber}.state`);
formData.append("screenshot", screenshotBlob, `${gameName}_${slotNumber}.png`);
const xhr = new XMLHttpRequest();
xhr.open("POST", "saveState.php", true);
xhr.send(formData);
}
EJS_onLoadState = function() {
const xhr = new XMLHttpRequest();
xhr.open("GET", <?php echo('"./saves/' . $_GET["game"]); ?>.state", true);
xhr.responseType = "arraybuffer"; // Set the response type to arraybuffer
xhr.onload = function() {
if (xhr.status === 200) {
const loadedState = new Uint8Array(xhr.response); // Convert arraybuffer to Uint8Array
EJS_emulator.gameManager.loadState(loadedState);
} else {
console.error("Error loading state");
}
};
xhr.onerror = function() {
console.error("Request failed");
};
xhr.onerror = function() {
console.error("Request failed");
xhr.open("POST", "save_state.php", true);
xhr.onload = function() {
if (xhr.status === 200) {
// Close modal and refresh page to show new save
document.getElementById('save-state-modal').classList.remove('active');
window.location.reload();
} else {
alert("Error saving game state: " + xhr.responseText);
}
};
xhr.send();
xhr.send(formData);
};
// Load state handling (will be wired up in the UI)
function loadState(slot) {
const xhr = new XMLHttpRequest();
xhr.open("GET", `get_state.php?profile_id=${profileId}&game=${gameName}&slot=${slot}`, true);
xhr.responseType = "arraybuffer";
xhr.onload = function() {
if (xhr.status === 200) {
const loadedState = new Uint8Array(xhr.response);
EJS_emulator.gameManager.loadState(loadedState);
} else {
alert("Error loading save state");
}
};
xhr.onerror = function() {
alert("Request failed");
};
xhr.send();
}
</script>
<script src='https://cdn.emulatorjs.org/stable/data/loader.js'></script>
</body>
</html>
<script src="https://cdn.emulatorjs.org/stable/data/loader.js"></script>
<script src="js/profiles.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Save state modal
const saveStateBtn = document.getElementById('save-state-btn');
const saveStateModal = document.getElementById('save-state-modal');
const closeSaveModal = document.getElementById('close-save-modal');
const cancelSave = document.getElementById('cancel-save');
const saveGameState = document.getElementById('save-game-state');
if (saveStateBtn && saveStateModal) {
// Open modal
saveStateBtn.addEventListener('click', function() {
saveStateModal.classList.add('active');
});
// Close modal
function closeSaveStateModal() {
saveStateModal.classList.remove('active');
}
if (closeSaveModal) closeSaveModal.addEventListener('click', closeSaveStateModal);
if (cancelSave) cancelSave.addEventListener('click', closeSaveStateModal);
// Save game state
if (saveGameState) {
saveGameState.addEventListener('click', function() {
// The actual saving is handled by EJS_onSaveState
// This just triggers the emulator's save state function
if (typeof EJS_emulator !== 'undefined') {
try {
EJS_emulator.gameManager.saveState();
} catch (e) {
alert('Error triggering save state: ' + e.message);
}
} else {
alert('Emulator not ready. Please wait and try again.');
}
});
}
}
// Load save state
const loadStateButtons = document.querySelectorAll('.load-state');
if (loadStateButtons.length > 0) {
loadStateButtons.forEach(button => {
button.addEventListener('click', function() {
const slot = this.dataset.slot;
if (typeof loadState === 'function') {
loadState(slot);
} else {
alert('Load state function not available');
}
});
});
}
// Delete save state
const deleteStateButtons = document.querySelectorAll('.delete-state');
if (deleteStateButtons.length > 0) {
deleteStateButtons.forEach(button => {
button.addEventListener('click', function() {
const slot = this.dataset.slot;
if (confirm('Are you sure you want to delete this save state?')) {
const xhr = new XMLHttpRequest();
xhr.open('POST', 'delete_state.php', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onload = function() {
if (xhr.status === 200) {
// Reload the page to update the save states list
window.location.reload();
} else {
alert('Error deleting save state: ' + xhr.responseText);
}
};
xhr.send(`profile_id=${profileId}&game=${gameName}&slot=${slot}`);
}
});
});
}
// Fullscreen button
const fullscreenBtn = document.getElementById('fullscreen-btn');
if (fullscreenBtn) {
fullscreenBtn.addEventListener('click', function() {
if (typeof EJS_emulator !== 'undefined') {
try {
EJS_emulator.setFullscreen(true);
} catch (e) {
alert('Error activating fullscreen: ' + e.message);
}
} else {
alert('Emulator not ready. Please wait and try again.');
}
});
}
// Screenshots modal
const screenshotItems = document.querySelectorAll('.game-screenshot');
const screenshotModal = document.getElementById('screenshot-modal');
const closeScreenshotModal = document.getElementById('close-screenshot-modal');
const screenshotModalImage = document.getElementById('screenshot-modal-image');
if (screenshotItems.length > 0 && screenshotModal) {
screenshotItems.forEach(item => {
item.addEventListener('click', function() {
const imgSrc = this.querySelector('img').src;
screenshotModalImage.src = imgSrc;
screenshotModal.classList.add('active');
});
});
// Close screenshot modal
function closeScreenshotModalFn() {
screenshotModal.classList.remove('active');
}
if (closeScreenshotModal) {
closeScreenshotModal.addEventListener('click', closeScreenshotModalFn);
}
// Close modal when clicking outside the image
screenshotModal.addEventListener('click', function(e) {
if (e.target === screenshotModal) {
closeScreenshotModalFn();
}
});
}
});
</script>
</body>
</html>

149
profile_action.php Normal file
View File

@ -0,0 +1,149 @@
<?php
session_start();
include 'functions.php';
// Ensure we have an action
if (!isset($_POST['action']) && !isset($_GET['action'])) {
header('Location: index.php');
exit;
}
$action = isset($_POST['action']) ? $_POST['action'] : $_GET['action'];
switch ($action) {
case 'create':
// Create a new profile
if (!isset($_POST['name']) || empty($_POST['name'])) {
$_SESSION['profile_error'] = "Profile name is required";
header('Location: settings.php#profiles-panel');
exit;
}
$name = trim($_POST['name']);
$avatar = isset($_POST['avatar']) ? $_POST['avatar'] : 'avatar1.png';
// Validate avatar file exists
if (!file_exists("img/avatars/$avatar")) {
$avatar = 'avatar1.png'; // Default to first avatar if missing
}
// Create the profile
$profileId = createProfile($name, $avatar);
if ($profileId) {
// Set as current profile
$_SESSION['current_profile'] = $profileId;
$_SESSION['profile_success'] = "Profile created successfully";
} else {
$_SESSION['profile_error'] = "Failed to create profile";
}
// Redirect back to previous page or settings
$redirect = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : 'settings.php#profiles-panel';
header("Location: $redirect");
break;
case 'switch':
// Switch to a different profile
if (!isset($_POST['profile_id']) || empty($_POST['profile_id'])) {
$_SESSION['profile_error'] = "Profile ID is required";
header('Location: index.php');
exit;
}
$profileId = $_POST['profile_id'];
// Validate the profile exists
$profile = getProfileById($profileId);
if ($profile) {
$_SESSION['current_profile'] = $profileId;
echo "Profile switched successfully";
} else {
http_response_code(404);
echo "Profile not found";
}
break;
case 'update':
// Update profile information
if (!isset($_POST['profile_id']) || empty($_POST['profile_id'])) {
$_SESSION['profile_error'] = "Profile ID is required";
header('Location: settings.php#profiles-panel');
exit;
}
$profileId = $_POST['profile_id'];
// Gather the update data
$updateData = [];
if (isset($_POST['name']) && !empty($_POST['name'])) {
$updateData['name'] = trim($_POST['name']);
}
if (isset($_POST['avatar']) && !empty($_POST['avatar'])) {
// Validate avatar file exists
if (file_exists("img/avatars/{$_POST['avatar']}")) {
$updateData['avatar'] = $_POST['avatar'];
}
}
if (empty($updateData)) {
$_SESSION['profile_error'] = "No update data provided";
header('Location: settings.php#profiles-panel');
exit;
}
// Update the profile
$success = updateProfile($profileId, $updateData);
if ($success) {
$_SESSION['profile_success'] = "Profile updated successfully";
} else {
$_SESSION['profile_error'] = "Failed to update profile";
}
// Redirect back to settings
header('Location: settings.php#profiles-panel');
break;
case 'delete':
// Delete a profile
if (!isset($_POST['profile_id']) || empty($_POST['profile_id'])) {
$_SESSION['profile_error'] = "Profile ID is required";
header('Location: settings.php#profiles-panel');
exit;
}
$profileId = $_POST['profile_id'];
// Don't delete the currently active profile
if ($_SESSION['current_profile'] === $profileId) {
$_SESSION['profile_error'] = "Cannot delete the currently active profile";
header('Location: settings.php#profiles-panel');
exit;
}
// Delete the profile
$success = deleteProfile($profileId);
if ($success) {
$_SESSION['profile_success'] = "Profile deleted successfully";
} else {
$_SESSION['profile_error'] = "Failed to delete profile";
}
// Redirect back to settings
header('Location: settings.php#profiles-panel');
break;
case 'get':
// Get profile data - handled by profile_get.php
header('Location: profile_get.php?action=get&profile_id=' . $_GET['profile_id']);
break;
default:
header('Location: index.php');
break;
}

30
profile_get.php Normal file
View File

@ -0,0 +1,30 @@
<?php
/**
* Profile data endpoint
* This file handles getting profile data for editing
*/
session_start();
include 'functions.php';
// Check if we're getting a profile
if (isset($_GET['action']) && $_GET['action'] == 'get' && isset($_GET['profile_id'])) {
$profileId = $_GET['profile_id'];
// Get the profile data
$profile = getProfileById($profileId);
// Return JSON response
header('Content-Type: application/json');
if ($profile) {
echo json_encode($profile);
} else {
http_response_code(404);
echo json_encode(['error' => 'Profile not found']);
}
exit;
}
// Redirect to index if accessed directly
header('Location: index.php');

View File

@ -0,0 +1 @@
{"id":"67bfaf6ae3257","name":"Test Profile","avatar":"avatar3.png","created":"2025-02-27 01:18:50"}

View File

View File

@ -1,33 +0,0 @@
<?php
if ($_SERVER["REQUEST_METHOD"] === "POST") {
if (isset($_FILES["state"]) && isset($_FILES["screenshot"]) && isset($_POST["gameName"])) {
$gameName = $_POST["gameName"]; // Retrieve the gameName
$stateFile = $_FILES["state"]["tmp_name"];
$screenshotFile = $_FILES["screenshot"]["tmp_name"];
// Read state data using fread
$stateData = "";
$stateHandle = fopen($stateFile, "rb");
while (!feof($stateHandle)) {
$stateData .= fread($stateHandle, 8192); // Read in chunks of 8KB
}
fclose($stateHandle);
// Read screenshot data using fread
$screenshotData = "";
$screenshotHandle = fopen($screenshotFile, "rb");
while (!feof($screenshotHandle)) {
$screenshotData .= fread($screenshotHandle, 8192);
}
fclose($screenshotHandle);
// Save the data to files with dynamic filenames
file_put_contents("./saves/{$gameName}.state", $stateData, LOCK_EX);
file_put_contents("./img/{$gameName}.png", $screenshotData, LOCK_EX);
echo "Data saved successfully!";
} else {
echo "Invalid data received.";
}
}
?>

62
save_state.php Normal file
View File

@ -0,0 +1,62 @@
<?php
session_start();
include 'functions.php';
// Check if we have the required data
if ($_SERVER["REQUEST_METHOD"] !== "POST") {
http_response_code(405);
echo "Method not allowed";
exit;
}
if (!isset($_POST["profile_id"]) || !isset($_POST["game_name"]) || !isset($_POST["slot"]) ||
!isset($_FILES["state"]) || !isset($_FILES["screenshot"])) {
http_response_code(400);
echo "Missing required data";
exit;
}
$profileId = $_POST["profile_id"];
$gameName = $_POST["game_name"];
$slot = (int)$_POST["slot"];
// Validate the profile exists
$profile = getProfileById($profileId);
if (!$profile) {
http_response_code(404);
echo "Profile not found";
exit;
}
// Check for upload errors
if ($_FILES["state"]["error"] !== UPLOAD_ERR_OK || $_FILES["screenshot"]["error"] !== UPLOAD_ERR_OK) {
http_response_code(400);
echo "File upload error";
exit;
}
// Read state data
$stateData = file_get_contents($_FILES["state"]["tmp_name"]);
if ($stateData === false) {
http_response_code(500);
echo "Failed to read state data";
exit;
}
// Read screenshot data
$screenshotData = file_get_contents($_FILES["screenshot"]["tmp_name"]);
if ($screenshotData === false) {
http_response_code(500);
echo "Failed to read screenshot data";
exit;
}
// Save the state and screenshot
$success = saveGameState($gameName, $profileId, $slot, $stateData, $screenshotData);
if ($success) {
echo "Save state created successfully";
} else {
http_response_code(500);
echo "Failed to save game state";
}

Binary file not shown.

View File

813
settings.php Normal file
View File

@ -0,0 +1,813 @@
<?php
session_start();
include 'functions.php';
include 'includes/retroachievements.php';
// Get current profile or set default
if (!isset($_SESSION['current_profile'])) {
$profiles = getProfiles();
if (count($profiles) > 0) {
$_SESSION['current_profile'] = $profiles[0]['id'];
} else {
// Create default profile if none exists
$defaultProfileId = createProfile("Player 1", "avatar1.png");
$_SESSION['current_profile'] = $defaultProfileId;
}
}
// Handle form submission
$message = '';
$messageType = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_POST['ra_settings'])) {
// Update RetroAchievements settings
$raSettings = [
'enabled' => isset($_POST['ra_enabled']) ? true : false,
'override_local_images' => isset($_POST['ra_override_local']) ? true : false,
'mode' => $_POST['ra_mode'],
'username' => trim($_POST['ra_username']),
'api_key' => trim($_POST['ra_api_key']),
'proxy_url' => trim($_POST['ra_proxy_url'])
];
if (saveRetroAchievementsSettings($raSettings)) {
$message = 'RetroAchievements settings saved successfully.';
$messageType = 'success';
} else {
$message = 'Failed to save RetroAchievements settings.';
$messageType = 'error';
}
} elseif (isset($_POST['general_settings'])) {
// Update general settings
$generalSettings = [
'site_name' => trim($_POST['site_name']),
'theme' => $_POST['theme'],
// Add more general settings as needed
];
// Save general settings to a file
if (file_put_contents('config/general.json', json_encode($generalSettings, JSON_PRETTY_PRINT))) {
$message = 'General settings saved successfully.';
$messageType = 'success';
} else {
$message = 'Failed to save general settings.';
$messageType = 'error';
}
} elseif (isset($_POST['clear_cache'])) {
// Clear RetroAchievements cache
$cacheTypes = $_POST['cache_type'] ?? [];
$cleared = false;
if (in_array('all', $cacheTypes) || in_array('metadata', $cacheTypes)) {
array_map('unlink', glob(RA_CACHE_DIR . '*.json'));
$cleared = true;
}
if (in_array('all', $cacheTypes) || in_array('icons', $cacheTypes)) {
array_map('unlink', glob(RA_ICONS_CACHE_DIR . '*.png'));
$cleared = true;
}
if (in_array('all', $cacheTypes) || in_array('screenshots', $cacheTypes)) {
array_map('unlink', glob(RA_SCREENSHOTS_CACHE_DIR . '*.png'));
$cleared = true;
}
if ($cleared) {
$message = 'Cache cleared successfully.';
$messageType = 'success';
} else {
$message = 'No cache files were selected to clear.';
$messageType = 'warning';
}
}
}
// Get current settings
$raSettings = getRetroAchievementsSettings();
// General settings
$generalSettingsPath = 'config/general.json';
$generalSettings = [];
if (file_exists($generalSettingsPath)) {
$generalSettings = json_decode(file_get_contents($generalSettingsPath), true);
}
// Default values
$generalSettings['site_name'] = $generalSettings['site_name'] ?? 'RetroHub';
$generalSettings['theme'] = $generalSettings['theme'] ?? 'dark';
// Get current profile data
$currentProfile = getProfileById($_SESSION['current_profile']);
$allProfiles = getProfiles();
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Settings - <?php echo htmlspecialchars($generalSettings['site_name']); ?></title>
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
.settings-container {
display: grid;
grid-template-columns: 250px 1fr;
gap: 2rem;
}
.settings-nav {
background-color: var(--card-bg);
border-radius: var(--border-radius);
overflow: hidden;
}
.settings-nav-item {
padding: 1rem;
cursor: pointer;
transition: background-color 0.2s ease, color 0.2s ease;
border-left: 3px solid transparent;
}
.settings-nav-item:hover {
background-color: var(--nav-bg);
}
.settings-nav-item.active {
background-color: var(--nav-bg);
border-left-color: var(--accent-color);
color: var(--accent-color);
}
.settings-nav-item i {
width: 24px;
text-align: center;
margin-right: 0.5rem;
}
.settings-panel {
background-color: var(--card-bg);
border-radius: var(--border-radius);
padding: 1.5rem;
display: none;
}
.settings-panel.active {
display: block;
}
.settings-panel h2 {
margin-top: 0;
margin-bottom: 1.5rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--nav-bg);
}
.form-row {
margin-bottom: 1.5rem;
}
.form-row.inline {
display: flex;
align-items: center;
}
.form-row.inline label {
margin-bottom: 0;
margin-right: 1rem;
}
.checkbox-label {
display: flex;
align-items: center;
cursor: pointer;
}
.checkbox-label input {
margin-right: 0.5rem;
}
.setting-description {
font-size: 0.9rem;
color: var(--text-secondary);
margin-top: 0.3rem;
}
.radio-options {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-top: 0.5rem;
}
.radio-option {
display: flex;
align-items: center;
}
.radio-option input {
margin-right: 0.5rem;
}
.conditional-section {
margin-top: 1rem;
padding: 1rem;
background-color: var(--primary-bg);
border-radius: var(--border-radius);
border-left: 3px solid var(--accent-color);
}
.cache-stats {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 1.5rem;
}
.cache-stat-item {
background-color: var(--primary-bg);
padding: 1rem;
border-radius: var(--border-radius);
text-align: center;
}
.cache-stat-value {
font-size: 1.5rem;
font-weight: bold;
margin: 0.5rem 0;
color: var(--accent-color);
}
.cache-stat-label {
font-size: 0.9rem;
color: var(--text-secondary);
}
.cache-options {
display: flex;
flex-wrap: wrap;
gap: 1rem;
margin-bottom: 1.5rem;
}
.cache-option {
display: flex;
align-items: center;
}
.cache-option input {
margin-right: 0.5rem;
}
</style>
</head>
<body>
<header>
<div class="container">
<nav>
<a href="index.php" class="logo">
<i class="fas fa-gamepad"></i>
<span><?php echo htmlspecialchars($generalSettings['site_name']); ?></span>
</a>
<ul class="nav-menu">
<li><a href="index.php">Games</a></li>
<li><a href="upload.php">Upload ROMs</a></li>
<li><a href="bios.php">BIOS Files</a></li>
<li><a href="settings.php" class="active">Settings</a></li>
</ul>
<div class="profile-menu">
<div class="current-profile" id="profile-toggle">
<img src="img/avatars/<?php echo $currentProfile['avatar']; ?>" alt="Profile">
<span><?php echo $currentProfile['name']; ?></span>
<i class="fas fa-chevron-down"></i>
</div>
<div class="profile-dropdown" id="profile-dropdown">
<ul class="profile-list">
<?php foreach($allProfiles as $profile): ?>
<li class="profile-item <?php echo ($profile['id'] == $_SESSION['current_profile']) ? 'active' : ''; ?>"
data-profile-id="<?php echo $profile['id']; ?>">
<img src="img/avatars/<?php echo $profile['avatar']; ?>" alt="<?php echo $profile['name']; ?>">
<span class="profile-name"><?php echo $profile['name']; ?></span>
</li>
<?php endforeach; ?>
</ul>
<div class="profile-actions">
<button class="add-profile-btn" id="add-profile-btn">
<i class="fas fa-plus-circle"></i>
<span>Add New Profile</span>
</button>
</div>
</div>
</div>
</nav>
</div>
</header>
<main>
<div class="container">
<div class="page-header">
<h1 class="page-title">Settings</h1>
</div>
<?php if ($message): ?>
<div class="alert alert-<?php echo $messageType; ?>">
<i class="fas fa-info-circle"></i>
<span><?php echo $message; ?></span>
</div>
<?php endif; ?>
<div class="settings-container">
<div class="settings-nav">
<div class="settings-nav-item active" data-target="general">
<i class="fas fa-cog"></i> General
</div>
<div class="settings-nav-item" data-target="retroachievements">
<i class="fas fa-trophy"></i> RetroAchievements
</div>
<div class="settings-nav-item" data-target="cache">
<i class="fas fa-database"></i> Cache Management
</div>
<div class="settings-nav-item" data-target="profiles">
<i class="fas fa-users"></i> Profiles
</div>
<div class="settings-nav-item" data-target="about">
<i class="fas fa-info-circle"></i> About
</div>
</div>
<div class="settings-content">
<!-- General Settings -->
<div class="settings-panel active" id="general-panel">
<h2>General Settings</h2>
<form method="post">
<div class="form-row">
<label for="site-name" class="form-label">Site Name</label>
<input type="text" id="site-name" name="site_name" class="form-input"
value="<?php echo htmlspecialchars($generalSettings['site_name']); ?>">
<div class="setting-description">Name displayed in the header and browser title.</div>
</div>
<div class="form-row">
<label class="form-label">Theme</label>
<div class="radio-options">
<label class="radio-option">
<input type="radio" name="theme" value="dark" <?php echo $generalSettings['theme'] === 'dark' ? 'checked' : ''; ?>>
Dark (Default)
</label>
<label class="radio-option">
<input type="radio" name="theme" value="light" <?php echo $generalSettings['theme'] === 'light' ? 'checked' : ''; ?>>
Light
</label>
</div>
</div>
<button type="submit" name="general_settings" class="btn">Save Settings</button>
</form>
</div>
<!-- RetroAchievements Settings -->
<div class="settings-panel" id="retroachievements-panel">
<h2>RetroAchievements Integration</h2>
<form method="post">
<div class="form-row inline">
<label class="checkbox-label">
<input type="checkbox" name="ra_enabled" id="ra-enabled" <?php echo $raSettings['enabled'] ? 'checked' : ''; ?>>
Enable RetroAchievements Integration
</label>
</div>
<div class="setting-description">
RetroAchievements integration allows automatic fetching of game icons and screenshots to enhance your game library.
<a href="https://retroachievements.org" target="_blank">Learn more about RetroAchievements</a>
</div>
<div class="form-row">
<label class="form-label">Integration Mode</label>
<div class="radio-options">
<label class="radio-option">
<input type="radio" name="ra_mode" value="direct" id="ra-mode-direct" <?php echo $raSettings['mode'] === 'direct' ? 'checked' : ''; ?>>
Direct API Access (requires your own RetroAchievements account)
</label>
<label class="radio-option">
<input type="radio" name="ra_mode" value="proxy" id="ra-mode-proxy" <?php echo $raSettings['mode'] === 'proxy' ? 'checked' : ''; ?>>
Proxy Server (uses a shared server for API requests)
</label>
</div>
</div>
<div class="conditional-section" id="direct-settings" style="display: <?php echo $raSettings['mode'] === 'direct' ? 'block' : 'none'; ?>">
<div class="form-row">
<label for="ra-username" class="form-label">RetroAchievements Username</label>
<input type="text" id="ra-username" name="ra_username" class="form-input" value="<?php echo htmlspecialchars($raSettings['username']); ?>">
</div>
<div class="form-row">
<label for="ra-api-key" class="form-label">RetroAchievements API Key</label>
<input type="password" id="ra-api-key" name="ra_api_key" class="form-input" value="<?php echo htmlspecialchars($raSettings['api_key']); ?>">
<div class="setting-description">
Your API key can be found in your <a href="https://retroachievements.org/controlpanel.php" target="_blank">RetroAchievements control panel</a>.
This is kept private and only used for API requests.
</div>
</div>
</div>
<div class="conditional-section" id="proxy-settings" style="display: <?php echo $raSettings['mode'] === 'proxy' ? 'block' : 'none'; ?>">
<div class="form-row">
<label for="ra-proxy-url" class="form-label">Proxy Server URL</label>
<input type="text" id="ra-proxy-url" name="ra_proxy_url" class="form-input" value="<?php echo htmlspecialchars($raSettings['proxy_url']); ?>">
<div class="setting-description">
The URL of your proxy server that handles RetroAchievements API requests.
Leave as default if using the shared server.
</div>
</div>
</div>
<button type="submit" name="ra_settings" class="btn">Save RetroAchievements Settings</button>
</form>
</div>
<!-- Cache Management -->
<div class="settings-panel" id="cache-panel">
<h2>Cache Management</h2>
<div class="cache-stats">
<div class="cache-stat-item">
<div class="cache-stat-value">
<?php echo count(glob(RA_CACHE_DIR . '*.json')); ?>
</div>
<div class="cache-stat-label">Game Metadata Files</div>
</div>
<div class="cache-stat-item">
<div class="cache-stat-value">
<?php echo count(glob(RA_ICONS_CACHE_DIR . '*.png')); ?>
</div>
<div class="cache-stat-label">Game Icons</div>
</div>
<div class="cache-stat-item">
<div class="cache-stat-value">
<?php echo count(glob(RA_SCREENSHOTS_CACHE_DIR . '*.png')); ?>
</div>
<div class="cache-stat-label">Game Screenshots</div>
</div>
<div class="cache-stat-item">
<div class="cache-stat-value">
<?php
$totalSize = 0;
foreach (glob(RA_CACHE_DIR . '*.json') as $file) {
$totalSize += filesize($file);
}
foreach (glob(RA_ICONS_CACHE_DIR . '*.png') as $file) {
$totalSize += filesize($file);
}
foreach (glob(RA_SCREENSHOTS_CACHE_DIR . '*.png') as $file) {
$totalSize += filesize($file);
}
echo formatFileSize($totalSize);
?>
</div>
<div class="cache-stat-label">Total Cache Size</div>
</div>
</div>
<form method="post">
<div class="form-row">
<label class="form-label">Clear Cache</label>
<div class="cache-options">
<label class="cache-option">
<input type="checkbox" name="cache_type[]" value="all">
All Cache
</label>
<label class="cache-option">
<input type="checkbox" name="cache_type[]" value="metadata">
Game Metadata
</label>
<label class="cache-option">
<input type="checkbox" name="cache_type[]" value="icons">
Game Icons
</label>
<label class="cache-option">
<input type="checkbox" name="cache_type[]" value="screenshots">
Game Screenshots
</label>
</div>
<div class="setting-description">
Warning: Clearing the cache will remove all downloaded game data. It will be re-downloaded as needed.
</div>
</div>
<button type="submit" name="clear_cache" class="btn btn-secondary">Clear Selected Cache</button>
</form>
</div>
<!-- Profiles -->
<div class="settings-panel" id="profiles-panel">
<h2>Profile Management</h2>
<?php if (isset($_SESSION['profile_success'])): ?>
<div class="alert alert-success">
<i class="fas fa-check-circle"></i>
<span><?php echo $_SESSION['profile_success']; ?></span>
</div>
<?php unset($_SESSION['profile_success']); ?>
<?php endif; ?>
<?php if (isset($_SESSION['profile_error'])): ?>
<div class="alert alert-error">
<i class="fas fa-exclamation-circle"></i>
<span><?php echo $_SESSION['profile_error']; ?></span>
</div>
<?php unset($_SESSION['profile_error']); ?>
<?php endif; ?>
<div class="profile-list-settings">
<?php foreach($allProfiles as $profile): ?>
<div class="profile-card">
<div class="profile-card-header">
<img src="img/avatars/<?php echo $profile['avatar']; ?>" alt="<?php echo $profile['name']; ?>" class="profile-avatar">
<div class="profile-info">
<h3 class="profile-name"><?php echo $profile['name']; ?></h3>
<div class="profile-created">Created: <?php echo date('M j, Y', strtotime($profile['created'])); ?></div>
</div>
</div>
<div class="profile-actions">
<button class="btn btn-secondary edit-profile-btn" data-profile-id="<?php echo $profile['id']; ?>">
<i class="fas fa-edit"></i> Edit
</button>
<?php if($profile['id'] !== $_SESSION['current_profile']): ?>
<button class="btn btn-secondary delete-profile-btn" data-profile-id="<?php echo $profile['id']; ?>">
<i class="fas fa-trash"></i> Delete
</button>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</div>
<div class="add-profile-section">
<button class="btn" id="settings-add-profile-btn">
<i class="fas fa-plus-circle"></i> Add New Profile
</button>
</div>
</div>
<!-- About -->
<div class="settings-panel" id="about-panel">
<h2>About RetroHub</h2>
<div class="about-content">
<div class="about-section">
<h3>RetroHub</h3>
<p>An enhanced game library extension for EmulatorJS with multi-profile support, BIOS management, and a modern user interface.</p>
<p>Version: 2.0</p>
</div>
<div class="about-section">
<h3>Credits</h3>
<ul>
<li>EmulatorJS: <a href="https://github.com/EmulatorJS/emulatorjs" target="_blank">https://github.com/EmulatorJS/emulatorjs</a></li>
<li>RetroAchievements: <a href="https://retroachievements.org" target="_blank">https://retroachievements.org</a></li>
</ul>
</div>
<div class="about-section">
<h3>License</h3>
<p>RetroHub is released under the GPL license, the same as the original EmulatorJS Game Library extension.</p>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<!-- Add Profile Modal (same as in index.php) -->
<div class="modal-overlay" id="profile-modal">
<div class="modal">
<div class="modal-header">
<h2 class="modal-title">Create New Profile</h2>
<button class="close-modal" id="close-profile-modal">&times;</button>
</div>
<div class="modal-body">
<form id="profile-form" action="profile_action.php" method="post">
<div class="form-group">
<label for="profile-name" class="form-label">Profile Name</label>
<input type="text" id="profile-name" name="name" class="form-input" required>
</div>
<div class="form-group">
<label class="form-label">Select Avatar</label>
<div class="avatar-selector">
<?php for($i = 1; $i <= 8; $i++): ?>
<img src="img/avatars/avatar<?php echo $i; ?>.png"
class="avatar-option <?php echo ($i == 1) ? 'selected' : ''; ?>"
data-avatar="avatar<?php echo $i; ?>.png">
<?php endfor; ?>
</div>
<input type="hidden" id="selected-avatar" name="avatar" value="avatar1.png">
</div>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" id="cancel-profile">Cancel</button>
<button class="btn" id="save-profile">Create Profile</button>
</div>
</div>
</div>
<!-- Edit Profile Modal -->
<div class="modal-overlay" id="edit-profile-modal">
<div class="modal">
<div class="modal-header">
<h2 class="modal-title">Edit Profile</h2>
<button class="close-modal" id="close-edit-profile-modal">&times;</button>
</div>
<div class="modal-body">
<form id="edit-profile-form" action="profile_action.php" method="post">
<input type="hidden" id="edit-profile-id" name="profile_id">
<input type="hidden" name="action" value="update">
<div class="form-group">
<label for="edit-profile-name" class="form-label">Profile Name</label>
<input type="text" id="edit-profile-name" name="name" class="form-input" required>
</div>
<div class="form-group">
<label class="form-label">Select Avatar</label>
<div class="avatar-selector" id="edit-avatar-selector">
<?php for($i = 1; $i <= 8; $i++): ?>
<img src="img/avatars/avatar<?php echo $i; ?>.png"
class="avatar-option"
data-avatar="avatar<?php echo $i; ?>.png">
<?php endfor; ?>
</div>
<input type="hidden" id="edit-selected-avatar" name="avatar">
</div>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" id="cancel-edit-profile">Cancel</button>
<button class="btn" id="update-profile">Update Profile</button>
</div>
</div>
</div>
<script src="js/profiles.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Settings navigation
const navItems = document.querySelectorAll('.settings-nav-item');
const panels = document.querySelectorAll('.settings-panel');
navItems.forEach(item => {
item.addEventListener('click', function() {
const target = this.dataset.target;
// Update active nav item
navItems.forEach(navItem => navItem.classList.remove('active'));
this.classList.add('active');
// Show target panel
panels.forEach(panel => panel.classList.remove('active'));
document.getElementById(target + '-panel').classList.add('active');
});
});
// RetroAchievements mode toggle
const directModeRadio = document.getElementById('ra-mode-direct');
const proxyModeRadio = document.getElementById('ra-mode-proxy');
const directSettings = document.getElementById('direct-settings');
const proxySettings = document.getElementById('proxy-settings');
directModeRadio.addEventListener('change', function() {
directSettings.style.display = this.checked ? 'block' : 'none';
proxySettings.style.display = this.checked ? 'none' : 'block';
});
proxyModeRadio.addEventListener('change', function() {
directSettings.style.display = this.checked ? 'none' : 'block';
proxySettings.style.display = this.checked ? 'block' : 'none';
});
// Edit profile functionality
const editProfileBtns = document.querySelectorAll('.edit-profile-btn');
const editProfileModal = document.getElementById('edit-profile-modal');
const closeEditProfileModal = document.getElementById('close-edit-profile-modal');
const cancelEditProfile = document.getElementById('cancel-edit-profile');
const updateProfile = document.getElementById('update-profile');
const editProfileForm = document.getElementById('edit-profile-form');
const editProfileId = document.getElementById('edit-profile-id');
const editProfileName = document.getElementById('edit-profile-name');
const editAvatarSelector = document.getElementById('edit-avatar-selector');
const editSelectedAvatar = document.getElementById('edit-selected-avatar');
editProfileBtns.forEach(btn => {
btn.addEventListener('click', function() {
const profileId = this.dataset.profileId;
// Fetch profile data
fetch('profile_action.php?action=get&profile_id=' + profileId)
.then(response => response.json())
.then(profile => {
editProfileId.value = profile.id;
editProfileName.value = profile.name;
// Set selected avatar
const avatarOptions = editAvatarSelector.querySelectorAll('.avatar-option');
avatarOptions.forEach(option => {
option.classList.remove('selected');
if (option.dataset.avatar === profile.avatar) {
option.classList.add('selected');
editSelectedAvatar.value = profile.avatar;
}
});
// Show modal
editProfileModal.classList.add('active');
})
.catch(error => {
console.error('Error fetching profile:', error);
});
});
});
// Edit profile avatar selection
const editAvatarOptions = editAvatarSelector.querySelectorAll('.avatar-option');
editAvatarOptions.forEach(option => {
option.addEventListener('click', function() {
editAvatarOptions.forEach(opt => opt.classList.remove('selected'));
this.classList.add('selected');
editSelectedAvatar.value = this.dataset.avatar;
});
});
// Close edit profile modal
function closeEditProfileModalFn() {
editProfileModal.classList.remove('active');
}
if (closeEditProfileModal) closeEditProfileModal.addEventListener('click', closeEditProfileModalFn);
if (cancelEditProfile) cancelEditProfile.addEventListener('click', closeEditProfileModalFn);
// Update profile
if (updateProfile) {
updateProfile.addEventListener('click', function() {
editProfileForm.submit();
});
}
// Delete profile functionality
const deleteProfileBtns = document.querySelectorAll('.delete-profile-btn');
deleteProfileBtns.forEach(btn => {
btn.addEventListener('click', function() {
const profileId = this.dataset.profileId;
if (confirm('Are you sure you want to delete this profile? All save states will be lost.')) {
const formData = new FormData();
formData.append('action', 'delete');
formData.append('profile_id', profileId);
fetch('profile_action.php', {
method: 'POST',
body: formData
})
.then(response => {
if (response.ok) {
// Reload the page to reflect changes
window.location.reload();
} else {
alert('Failed to delete profile.');
}
})
.catch(error => {
console.error('Error deleting profile:', error);
});
}
});
});
// Settings Add Profile button
const settingsAddProfileBtn = document.getElementById('settings-add-profile-btn');
if (settingsAddProfileBtn) {
settingsAddProfileBtn.addEventListener('click', function() {
document.getElementById('profile-modal').classList.add('active');
});
}
});
</script>
</body>
</html>

891
style.css
View File

@ -1,73 +1,836 @@
body {
background-color: #3c3c3c;
height: 100%;
text-align:center;
}
nav {
background-color: #5f5f5f;
border-radius: 5px;
}
:root {
--primary-bg: #212529;
--secondary-bg: #343a40;
--nav-bg: #495057;
--accent-color: #5cbcfc;
--text-primary: #f8f9fa;
--text-secondary: #e9ecef;
--card-bg: #343a40;
--card-hover: #495057;
--border-radius: 10px;
--transition-speed: 0.3s;
}
nav ul {
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
background-color: var(--primary-bg);
color: var(--text-primary);
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* Header and Navigation */
header {
background-color: var(--secondary-bg);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
position: sticky;
top: 0;
z-index: 100;
}
.container {
width: 90%;
max-width: 1400px;
margin: 0 auto;
padding: 0 20px;
}
nav {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 0;
}
.logo {
display: flex;
align-items: center;
gap: 10px;
text-decoration: none;
color: var(--accent-color);
font-size: 1.5rem;
font-weight: bold;
}
.logo i {
font-size: 1.8rem;
}
.nav-menu {
display: flex;
list-style: none;
gap: 1.5rem;
}
.nav-menu a {
text-decoration: none;
color: var(--text-secondary);
font-weight: 500;
padding: 0.5rem 1rem;
border-radius: var(--border-radius);
transition: all var(--transition-speed) ease;
}
.nav-menu a:hover, .nav-menu a.active {
background-color: var(--nav-bg);
color: var(--accent-color);
}
.profile-menu {
position: relative;
display: flex;
align-items: center;
}
.current-profile {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
border-radius: var(--border-radius);
background-color: var(--nav-bg);
cursor: pointer;
transition: all var(--transition-speed) ease;
}
.current-profile:hover {
background-color: var(--card-hover);
}
.current-profile img {
width: 32px;
height: 32px;
border-radius: 50%;
object-fit: cover;
}
.profile-dropdown {
position: absolute;
top: 100%;
right: 0;
width: 220px;
background-color: var(--secondary-bg);
border-radius: var(--border-radius);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
padding: 1rem;
margin-top: 8px;
z-index: 10;
display: none;
}
.profile-dropdown.active {
display: block;
}
.profile-list {
list-style: none;
margin-bottom: 1rem;
}
.profile-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 10px;
border-radius: var(--border-radius);
cursor: pointer;
transition: background-color var(--transition-speed) ease;
}
.profile-item:hover, .profile-item.active {
background-color: var(--nav-bg);
}
.profile-item img {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
}
.profile-name {
font-weight: 500;
}
.profile-actions {
padding-top: 10px;
border-top: 1px solid var(--nav-bg);
}
.add-profile-btn {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
padding: 8px 10px;
background: none;
border: none;
color: var(--accent-color);
cursor: pointer;
border-radius: var(--border-radius);
transition: background-color var(--transition-speed) ease;
}
.add-profile-btn:hover {
background-color: var(--nav-bg);
}
/* Main Content */
main {
flex: 1;
padding: 2rem 0;
}
.page-header {
margin-bottom: 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.page-title {
font-size: 1.8rem;
color: var(--text-primary);
}
/* Game Gallery */
.gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1.5rem;
}
.gallery.list-view {
grid-template-columns: 1fr;
gap: 1rem;
}
.game-card {
background-color: var(--card-bg);
border-radius: var(--border-radius);
overflow: hidden;
transition: transform var(--transition-speed) ease, box-shadow var(--transition-speed) ease;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
text-decoration: none;
color: var(--text-primary);
position: relative;
}
.game-card:hover {
transform: translateY(-5px);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
}
.game-image-container {
width: 100%;
position: relative;
background-color: var(--secondary-bg);
overflow: hidden;
}
.game-image {
width: 100%;
aspect-ratio: 4/3;
object-fit: cover;
display: block;
}
.game-year {
position: absolute;
top: 8px;
right: 8px;
background-color: rgba(0, 0, 0, 0.7);
color: var(--text-primary);
padding: 2px 6px;
border-radius: 3px;
font-size: 0.8rem;
font-weight: 500;
}
.game-info {
padding: 1rem;
}
.game-title {
font-size: 1rem;
font-weight: 600;
margin: 0 0 0.5rem 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.game-details {
font-size: 0.8rem;
color: var(--accent-color);
display: flex;
flex-direction: column;
}
.game-console {
text-transform: uppercase;
font-weight: 500;
margin-bottom: 0.2rem;
}
.game-developer {
font-style: italic;
color: var(--text-secondary);
}
/* List view modifications */
.gallery.list-view .game-card {
display: flex;
align-items: center;
}
.gallery.list-view .game-image-container {
width: 120px;
min-width: 120px;
height: 90px;
}
.gallery.list-view .game-image {
height: 100%;
aspect-ratio: auto;
}
.gallery.list-view .game-info {
flex: 1;
display: flex;
justify-content: space-between;
align-items: center;
}
.gallery.list-view .game-title {
font-size: 1.1rem;
margin: 0;
}
.gallery.list-view .game-details {
flex-direction: row;
align-items: center;
gap: 1rem;
}
/* Empty states */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: var(--card-bg);
border-radius: var(--border-radius);
padding: 3rem;
text-align: center;
grid-column: 1 / -1;
}
.empty-state i {
font-size: 3rem;
color: var(--accent-color);
margin-bottom: 1rem;
}
.empty-state h3 {
margin: 0 0 0.5rem 0;
font-size: 1.5rem;
}
.empty-state p {
color: var(--text-secondary);
margin-bottom: 1.5rem;
}
.empty-search-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: var(--card-bg);
border-radius: var(--border-radius);
padding: 3rem;
text-align: center;
grid-column: 1 / -1;
}
.empty-search-state i {
font-size: 3rem;
color: var(--nav-bg);
margin-bottom: 1rem;
}
.empty-search-state h3 {
margin: 0 0 0.5rem 0;
font-size: 1.5rem;
}
.empty-search-state p {
color: var(--text-secondary);
}
/* Search bar and filters */
.library-actions {
display: flex;
gap: 1rem;
align-items: center;
}
.search-bar {
position: relative;
flex: 1;
max-width: 400px;
}
.search-input {
width: 100%;
padding: 0.7rem 1rem;
padding-right: 2.5rem;
border: none;
border-radius: var(--border-radius);
background-color: var(--card-bg);
color: var(--text-primary);
}
.search-input:focus {
outline: none;
box-shadow: 0 0 0 2px var(--accent-color);
}
.search-btn {
position: absolute;
right: 0;
top: 0;
height: 100%;
width: 2.5rem;
border: none;
background: none;
color: var(--text-secondary);
cursor: pointer;
}
.search-btn:hover {
color: var(--accent-color);
}
.view-options {
display: flex;
gap: 0.5rem;
}
.view-btn {
width: 40px;
height: 40px;
border: none;
border-radius: var(--border-radius);
background-color: var(--card-bg);
color: var(--text-secondary);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background-color var(--transition-speed) ease, color var(--transition-speed) ease;
}
.view-btn:hover, .view-btn.active {
background-color: var(--accent-color);
color: var(--primary-bg);
}
.console-filters {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 1.5rem;
}
.console-filter {
padding: 0.5rem 1rem;
border: none;
border-radius: var(--border-radius);
background-color: var(--card-bg);
color: var(--text-secondary);
cursor: pointer;
transition: background-color var(--transition-speed) ease, color var(--transition-speed) ease;
}
.console-filter:hover, .console-filter.active {
background-color: var(--accent-color);
color: var(--primary-bg);
}
/* Responsive adjustments */
@media (max-width: 768px) {
.library-actions {
flex-direction: column;
align-items: stretch;
}
.search-bar {
max-width: none;
}
.view-options {
align-self: flex-end;
}
.gallery.list-view .game-card {
flex-direction: column;
align-items: stretch;
}
.gallery.list-view .game-image-container {
width: 100%;
height: auto;
}
.gallery.list-view .game-info {
flex-direction: column;
}
.gallery.list-view .game-details {
flex-direction: column;
align-items: flex-start;
gap: 0.3rem;
margin-top: 0.5rem;
}
}
/* Upload Section */
.upload-section {
background-color: var(--secondary-bg);
padding: 2rem;
border-radius: var(--border-radius);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
}
.upload-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
}
@media (max-width: 768px) {
.upload-container {
grid-template-columns: 1fr;
}
}
.upload-title {
font-size: 1.2rem;
margin-bottom: 1rem;
color: var(--accent-color);
}
.upload-box {
border: 2px dashed var(--nav-bg);
border-radius: var(--border-radius);
padding: 2rem;
text-align: center;
background-color: var(--card-bg);
transition: border-color var(--transition-speed) ease;
cursor: pointer;
position: relative;
}
.upload-box:hover {
border-color: var(--accent-color);
}
.upload-box i {
font-size: 2.5rem;
color: var(--accent-color);
margin-bottom: 1rem;
display: block;
}
.upload-input {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
opacity: 0;
cursor: pointer;
}
.upload-list {
margin-top: 1.5rem;
}
.upload-item {
display: flex;
align-items: center;
background-color: var(--card-bg);
padding: 0.8rem 1rem;
border-radius: var(--border-radius);
margin-bottom: 0.5rem;
}
.upload-item-icon {
margin-right: 1rem;
color: var(--accent-color);
}
.upload-item-name {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.upload-status {
width: 20px;
height: 20px;
}
/* Buttons */
.btn {
display: inline-block;
padding: 0.8rem 1.5rem;
background-color: var(--accent-color);
color: var(--primary-bg);
border: none;
border-radius: var(--border-radius);
font-weight: 500;
cursor: pointer;
transition: all var(--transition-speed) ease;
text-align: center;
text-decoration: none;
}
.btn:hover {
background-color: #4da8e2;
transform: translateY(-2px);
}
.btn-secondary {
background-color: var(--nav-bg);
color: var(--text-primary);
}
.btn-secondary:hover {
background-color: var(--card-hover);
}
/* Modal */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
}
align-items: center;
z-index: 1000;
opacity: 0;
visibility: hidden;
transition: all var(--transition-speed) ease;
}
nav a {
.modal-overlay.active {
opacity: 1;
visibility: visible;
}
.modal {
background-color: var(--secondary-bg);
border-radius: var(--border-radius);
max-width: 500px;
width: 90%;
padding: 2rem;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
transform: translateY(-20px);
transition: transform var(--transition-speed) ease;
}
.modal-overlay.active .modal {
transform: translateY(0);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.modal-title {
font-size: 1.3rem;
font-weight: bold;
}
.close-modal {
background: none;
border: none;
color: var(--text-secondary);
font-size: 1.5rem;
cursor: pointer;
transition: color var(--transition-speed) ease;
}
.close-modal:hover {
color: var(--accent-color);
}
.modal-body {
margin-bottom: 1.5rem;
}
.form-group {
margin-bottom: 1rem;
}
.form-label {
display: block;
padding: 1rem;
text-decoration: none;
color: #c5c5c5;
border-radius: 5px;
}
margin-bottom: 0.5rem;
font-weight: 500;
}
nav a:hover {
background-color: #868686;
color: #fff;
}
.linkimg {
border-radius: 10px;
border-width: 5px;
border-style: solid;
border-color:#CCCCCC;
width: 160px;
height: 120px;
margin: 20px 20px;
}
.linkimg:hover {
width: 180px;
height: 140px;
border-color:#FFFFFF;
margin-left: 0px;
margin-top: 0px;
margin-bottom: 0px;
}
a.link {
display: inline-block;
padding: 10px 20px;
border-radius: 20px;
background-color: #747474;
color: #d3d3d3;
text-decoration: none;
margin: 5px;
}
a.link:hover {
background-color: #7e7e7e;
}
.arcadelist {
border-radius: 10px;
background-color: #5c5c5c;
width: 75%;
height: 100%;
margin: auto auto;
}
.form-input {
width: 100%;
padding: 0.8rem;
border: 1px solid var(--nav-bg);
border-radius: var(--border-radius);
background-color: var(--card-bg);
color: var(--text-primary);
transition: border-color var(--transition-speed) ease;
}
.form-input:focus {
outline: none;
border-color: var(--accent-color);
}
.avatar-selector {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 0.5rem;
}
.avatar-option {
width: 50px;
height: 50px;
border-radius: 50%;
cursor: pointer;
border: 3px solid transparent;
transition: border-color var(--transition-speed) ease;
}
.avatar-option.selected {
border-color: var(--accent-color);
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 1rem;
}
/* Game Player */
.game-player {
max-width: 960px;
margin: 0 auto;
background-color: var(--secondary-bg);
border-radius: var(--border-radius);
overflow: hidden;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
}
.player-info {
padding: 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.player-title {
font-size: 1.3rem;
color: var(--text-primary);
}
.player-console {
color: var(--accent-color);
text-transform: uppercase;
font-size: 0.9rem;
}
.player-actions {
display: flex;
gap: 0.8rem;
}
.action-btn {
background-color: var(--nav-bg);
border: none;
color: var(--text-secondary);
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
transition: all var(--transition-speed) ease;
}
.action-btn:hover {
background-color: var(--card-hover);
color: var(--accent-color);
}
.game-embed {
width: 100%;
aspect-ratio: 16/9;
background-color: #000;
}
/* Responsive Design */
@media (max-width: 991px) {
.gallery {
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
}
}
@media (max-width: 768px) {
.container {
width: 95%;
padding: 0 10px;
}
.nav-menu {
gap: 0.8rem;
}
.nav-menu a {
padding: 0.4rem 0.8rem;
}
.page-header {
flex-direction: column;
align-items: flex-start;
gap: 1rem;
}
}
@media (max-width: 480px) {
.gallery {
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 1rem;
}
.nav-menu a {
padding: 0.3rem 0.6rem;
font-size: 0.9rem;
}
}

398
test_ra.php Normal file
View File

@ -0,0 +1,398 @@
<?php
/**
* Simple RetroAchievements Test Script
* Test the RetroAchievements integration with the official API
*/
// Include the RetroAchievements integration
include 'includes/retroachievements.php';
// Enable error reporting
error_reporting(E_ALL);
ini_set('display_errors', 1);
// Function to prettify JSON for display
function prettyJson($json) {
if (is_string($json)) {
$json = json_decode($json, true);
}
return '<pre>' . htmlspecialchars(json_encode($json, JSON_PRETTY_PRINT)) . '</pre>';
}
// Get current settings
$settings = getRetroAchievementsSettings();
// Output page
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RetroAchievements API Test</title>
<style>
body {
font-family: Arial, sans-serif;
line-height: 1.6;
margin: 0;
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
h2 {
border-bottom: 1px solid #ccc;
padding-bottom: 10px;
}
.test-section {
margin-bottom: 30px;
padding: 15px;
border: 1px solid #ddd;
border-radius: 5px;
}
pre {
background: #f4f4f4;
padding: 10px;
border-radius: 5px;
overflow-x: auto;
}
img {
max-width: 300px;
border: 1px solid #ddd;
margin: 10px 0;
}
.success { color: green; font-weight: bold; }
.error { color: red; font-weight: bold; }
.note { background-color: #ffffcc; padding: 10px; border-radius: 5px; }
</style>
</head>
<body>
<h1>RetroAchievements API Test</h1>
<div class="note">
<p><strong>Note:</strong> This test script uses the official RetroAchievements API at <code>api.retroachievements.org</code> which should bypass Cloudflare protection.</p>
</div>
<div class="test-section">
<h2>Current Configuration</h2>
<p>Enabled: <?php echo $settings['enabled'] ? 'Yes' : 'No'; ?></p>
<p>Mode: <?php echo $settings['mode']; ?></p>
<p>Username: <?php echo htmlspecialchars($settings['username']); ?></p>
<p>API Key: <?php echo empty($settings['api_key']) ? 'Not set' : 'Set (hidden)'; ?></p>
<p>Proxy URL: <?php echo htmlspecialchars($settings['proxy_url']); ?></p>
<form method="post">
<h3>Update Settings</h3>
<p>
<label>
<input type="checkbox" name="ra_enabled" <?php echo $settings['enabled'] ? 'checked' : ''; ?>>
Enable RetroAchievements
</label>
</p>
<p>
<label>
<input type="radio" name="ra_mode" value="direct" <?php echo $settings['mode'] === 'direct' ? 'checked' : ''; ?>>
Direct API Access
</label>
<br>
<label>
<input type="radio" name="ra_mode" value="proxy" <?php echo $settings['mode'] === 'proxy' ? 'checked' : ''; ?>>
Proxy Server
</label>
</p>
<p>
<label>Username: <input type="text" name="ra_username" value="<?php echo htmlspecialchars($settings['username']); ?>"></label>
</p>
<p>
<label>API Key: <input type="password" name="ra_api_key" value="<?php echo htmlspecialchars($settings['api_key']); ?>"></label>
</p>
<p>
<label>Proxy URL: <input type="text" name="ra_proxy_url" value="<?php echo htmlspecialchars($settings['proxy_url']); ?>" size="40"></label>
</p>
<p>
<button type="submit" name="save_settings">Save Settings</button>
</p>
</form>
<?php
// Handle settings update
if (isset($_POST['save_settings'])) {
$newSettings = [
'enabled' => isset($_POST['ra_enabled']),
'mode' => $_POST['ra_mode'],
'username' => $_POST['ra_username'],
'api_key' => $_POST['ra_api_key'],
'proxy_url' => $_POST['ra_proxy_url'],
'override_local_images' => $settings['override_local_images'] ?? true
];
if (saveRetroAchievementsSettings($newSettings)) {
echo '<p class="success">Settings saved successfully! Refresh the page to continue testing.</p>';
} else {
echo '<p class="error">Failed to save settings. Check file permissions.</p>';
}
}
?>
</div>
<div class="test-section">
<h2>Test API Connection</h2>
<form method="post">
<button type="submit" name="test_api">Test API Connection</button>
</form>
<?php
if (isset($_POST['test_api'])) {
if (!$settings['enabled']) {
echo '<p class="error">RetroAchievements integration is not enabled. Enable it in the settings above.</p>';
} else {
echo '<h3>Testing API Connection...</h3>';
// Test with a simple console list API call
$endpoint = 'GetConsoleIDs'; // Official endpoint for console list
$params = [];
$result = ($settings['mode'] === 'direct')
? raApiRequest($endpoint, $params)
: raProxyRequest($endpoint, $params);
if ($result) {
echo '<p class="success">API Connection successful!</p>';
echo '<p>Retrieved console data:</p>';
echo prettyJson($result);
} else {
echo '<p class="error">API Connection failed. Check your settings and try again.</p>';
// Try to show detailed error information
if ($settings['mode'] === 'direct') {
$url = RA_API_BASE_URL . $endpoint . '.php?' . http_build_query([
'z' => $settings['username'],
'y' => $settings['api_key']
]);
echo '<p>Attempted URL: ' . htmlspecialchars($url) . '</p>';
// Try a manual curl request for debugging
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($curl, CURLOPT_USERAGENT, 'RetroHub/1.0');
curl_setopt($curl, CURLOPT_VERBOSE, true);
$verboseLog = fopen('php://temp', 'w+');
curl_setopt($curl, CURLOPT_STDERR, $verboseLog);
$response = curl_exec($curl);
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
$error = curl_error($curl);
rewind($verboseLog);
$verboseOutput = stream_get_contents($verboseLog);
curl_close($curl);
echo '<p>HTTP Status Code: ' . $httpCode . '</p>';
if ($error) {
echo '<p>Error: ' . htmlspecialchars($error) . '</p>';
}
echo '<h4>Verbose Log:</h4>';
echo '<pre>' . htmlspecialchars($verboseOutput) . '</pre>';
if (!empty($response)) {
echo '<h4>Response:</h4>';
echo '<pre>' . htmlspecialchars($response) . '</pre>';
}
} else {
// For proxy mode, show the proxy URL
echo '<p>Proxy URL: ' . htmlspecialchars($settings['proxy_url']) . '</p>';
echo '<p>Check that your proxy server is correctly configured.</p>';
}
}
}
}
?>
</div>
<div class="test-section">
<h2>Search for Game Metadata</h2>
<form method="post">
<p>
<label>Game Title:
<input type="text" name="game_title" value="<?php echo isset($_POST['game_title']) ? htmlspecialchars($_POST['game_title']) : 'Super Mario World'; ?>" size="40">
</label>
</p>
<p>
<label>Console:
<select name="console">
<?php foreach ($RA_CONSOLE_IDS as $consoleKey => $consoleId): ?>
<option value="<?php echo $consoleKey; ?>" <?php echo (isset($_POST['console']) && $_POST['console'] === $consoleKey) ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($consoleKey); ?> (ID: <?php echo $consoleId; ?>)
</option>
<?php endforeach; ?>
</select>
</label>
</p>
<p>
<button type="submit" name="search_game">Search Game</button>
</p>
</form>
<?php
if (isset($_POST['search_game'])) {
if (!$settings['enabled']) {
echo '<p class="error">RetroAchievements integration is not enabled. Enable it in the settings above.</p>';
} else {
$gameTitle = $_POST['game_title'];
$console = $_POST['console'];
echo '<h3>Searching for: ' . htmlspecialchars($gameTitle) . ' (' . htmlspecialchars($console) . ')</h3>';
// Get game metadata
$metadata = getGameMetadata($gameTitle, $console);
if ($metadata) {
echo '<p class="success">Game metadata found!</p>';
echo '<h4>Metadata:</h4>';
echo '<ul>';
if (isset($metadata['title'])) echo '<li>Title: ' . htmlspecialchars($metadata['title']) . '</li>';
if (isset($metadata['developer'])) echo '<li>Developer: ' . htmlspecialchars($metadata['developer']) . '</li>';
if (isset($metadata['publisher'])) echo '<li>Publisher: ' . htmlspecialchars($metadata['publisher']) . '</li>';
if (isset($metadata['genre'])) echo '<li>Genre: ' . htmlspecialchars($metadata['genre']) . '</li>';
if (isset($metadata['released'])) echo '<li>Released: ' . htmlspecialchars($metadata['released']) . '</li>';
echo '</ul>';
echo '<h4>Images:</h4>';
echo '<div style="display: flex; flex-wrap: wrap; gap: 20px;">';
if (isset($metadata['icon']) && $metadata['icon']) {
echo '<div>';
echo '<p>Icon:</p>';
echo '<img src="' . $metadata['icon'] . '" alt="Game Icon">';
echo '</div>';
}
if (isset($metadata['screenshot_title']) && $metadata['screenshot_title']) {
echo '<div>';
echo '<p>Title Screenshot:</p>';
echo '<img src="' . $metadata['screenshot_title'] . '" alt="Title Screenshot">';
echo '</div>';
}
if (isset($metadata['screenshot_ingame']) && $metadata['screenshot_ingame']) {
echo '<div>';
echo '<p>Ingame Screenshot:</p>';
echo '<img src="' . $metadata['screenshot_ingame'] . '" alt="Ingame Screenshot">';
echo '</div>';
}
if (isset($metadata['screenshot_boxart']) && $metadata['screenshot_boxart']) {
echo '<div>';
echo '<p>Box Art:</p>';
echo '<img src="' . $metadata['screenshot_boxart'] . '" alt="Box Art">';
echo '</div>';
}
echo '</div>';
echo '<h4>Raw Data:</h4>';
$gameData = getGameData($gameTitle, $console);
echo prettyJson($gameData);
} else {
echo '<p class="error">No metadata found for this game.</p>';
// Try to display more debugging information
echo '<h4>Debug Information:</h4>';
echo '<p>Game title being searched: ' . htmlspecialchars($gameTitle) . '</p>';
echo '<p>Cleaned title: ' . htmlspecialchars(cleanGameTitle($gameTitle)) . '</p>';
// Try a direct API call to show available games
$consoleId = $RA_CONSOLE_IDS[$console];
echo '<p>Attempting direct search using ConsoleID: ' . $consoleId . '</p>';
if ($settings['mode'] === 'direct') {
$endpoint = 'GetGameList';
$params = [
'i' => $consoleId,
'f' => cleanGameTitle($gameTitle),
'z' => $settings['username'],
'y' => $settings['api_key']
];
$url = RA_API_BASE_URL . $endpoint . '.php?' . http_build_query($params);
echo '<p>URL being requested: ' . htmlspecialchars($url) . '</p>';
$response = @file_get_contents($url);
if ($response) {
echo '<p class="success">Direct API search returned data:</p>';
echo prettyJson($response);
} else {
echo '<p class="error">Direct API search failed. Error: ' . error_get_last()['message'] . '</p>';
// Try curl as a fallback
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($curl, CURLOPT_USERAGENT, 'RetroHub/1.0');
$response = curl_exec($curl);
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
curl_close($curl);
if ($response) {
echo '<p class="success">Direct API search with curl returned data:</p>';
echo prettyJson($response);
} else {
echo '<p class="error">Direct API search with curl failed. HTTP Status: ' . $httpCode . '</p>';
}
}
} else {
echo '<p>Using proxy mode. Cannot show direct API call.</p>';
}
}
}
}
?>
</div>
<div class="test-section">
<h2>Clear Cache</h2>
<form method="post">
<button type="submit" name="clear_cache">Clear RetroAchievements Cache</button>
</form>
<?php
if (isset($_POST['clear_cache'])) {
$cacheDirs = [
RA_CACHE_DIR,
RA_ICONS_CACHE_DIR,
RA_SCREENSHOTS_CACHE_DIR
];
$totalFiles = 0;
$deletedFiles = 0;
foreach ($cacheDirs as $dir) {
if (is_dir($dir)) {
$files = glob($dir . '*');
$totalFiles += count($files);
foreach ($files as $file) {
if (is_file($file) && unlink($file)) {
$deletedFiles++;
}
}
}
}
echo '<p>Deleted ' . $deletedFiles . ' out of ' . $totalFiles . ' cache files.</p>';
echo '<p>Cache has been cleared. Try searching for a game again.</p>';
}
?>
</div>
</body>
</html>

View File

@ -1,74 +1,246 @@
<!DOCTYPE html>
<html>
<?php
session_start();
include 'functions.php';
<head>
<title>EmulatorJS Library - Upload</title>
<link rel="stylesheet" type="text/css" href="style.css">
</head>
// Get current profile or set default
if (!isset($_SESSION['current_profile'])) {
$profiles = getProfiles();
if (count($profiles) > 0) {
$_SESSION['current_profile'] = $profiles[0]['id'];
} else {
// Create default profile if none exists
$defaultProfileId = createProfile("Player 1", "avatar1.png");
$_SESSION['current_profile'] = $defaultProfileId;
}
}
<body>
// Process ROM file uploads
$uploadMessage = '';
$uploadStatus = '';
<!-- Navbar -->
<nav>
<ul>
<li><a href="index.php">Arcade</a></li>
<li><a href="#">Upload</a></li>
</ul>
</nav>
<br />
<!-- Game Arcade -->
<div class="arcadelist">
<?php
include 'fnc.php';
$settings = parse_ini_file("./settings.ini");
//Write system extension arrays
// Nintendo
$snes = ["smc", "sfc", "fig", "swc", "bs", "st"];
$gba = ["gba"];
$gb = ["gb", "gbc", "dmg"];
$nes = ["fds", "nes", "unif", "unf"];
$vb = ["vb", "vboy"];
$nds = ["nds"];
$n64 = ["n64", "z64", "v64", "u1", "ndd"];
// Sega
$sms = ["sms"];
$smd = ["smd", "md"];
$gg = ["gg"];
//Upload functionality
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
if($_FILES['rom-files']['error'][0] == UPLOAD_ERR_OK) {
foreach ($_FILES['rom-files']['tmp_name'] as $key => $tmp_name) {
$name = basename($_FILES['rom-files']['name'][$key]);
$ext = explode('.', $name);
$ext = end($ext);
if(!is_dir("roms")) {
mkdir("roms");
}
//Move File
move_uploaded_file($tmp_name, "roms/$name");
print("<p>File successfully uploaded. Scraping failed due to no key error.</p>");
}
}
if ($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_POST['upload_type'])) {
if ($_POST['upload_type'] == 'rom' && isset($_FILES['rom_files'])) {
$uploadCount = 0;
$errorCount = 0;
// Create the roms directory if it doesn't exist
if (!is_dir('roms')) {
mkdir('roms', 0755, true);
}
foreach ($_FILES['rom_files']['tmp_name'] as $key => $tmp_name) {
if ($_FILES['rom_files']['error'][$key] == UPLOAD_ERR_OK) {
$name = basename($_FILES['rom_files']['name'][$key]);
// Move the uploaded file
if (move_uploaded_file($tmp_name, "roms/$name")) {
$uploadCount++;
} else {
$errorCount++;
}
?>
} else {
$errorCount++;
}
}
if ($uploadCount > 0) {
$uploadMessage = "$uploadCount ROM file" . ($uploadCount != 1 ? "s" : "") . " uploaded successfully!";
$uploadStatus = 'success';
}
if ($errorCount > 0) {
$uploadMessage .= ($uploadMessage ? " However, " : "") . "$errorCount file" . ($errorCount != 1 ? "s" : "") . " failed to upload.";
$uploadStatus = $uploadCount > 0 ? 'warning' : 'error';
}
}
}
<form action="upload.php" method="post" enctype="multipart/form-data">
<label for="rom-files">Select a ROM file (Max 20):</label>
<br />
<input type="file" id="rom-files" name="rom-files[]" multiple>
<br />
<br />
<input type="submit" value="Upload">
</form>
// Get current profile data
$currentProfile = getProfileById($_SESSION['current_profile']);
$allProfiles = getProfiles();
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RetroHub - Upload ROMs</title>
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
</head>
<body>
<header>
<div class="container">
<nav>
<a href="index.php" class="logo">
<i class="fas fa-gamepad"></i>
<span>RetroHub</span>
</a>
<ul class="nav-menu">
<li><a href="index.php">Games</a></li>
<li><a href="upload.php" class="active">Upload ROMs</a></li>
<li><a href="bios.php">BIOS Files</a></li>
</ul>
<div class="profile-menu">
<div class="current-profile" id="profile-toggle">
<img src="img/avatars/<?php echo $currentProfile['avatar']; ?>" alt="Profile">
<span><?php echo $currentProfile['name']; ?></span>
<i class="fas fa-chevron-down"></i>
</div>
<div class="profile-dropdown" id="profile-dropdown">
<ul class="profile-list">
<?php foreach($allProfiles as $profile): ?>
<li class="profile-item <?php echo ($profile['id'] == $_SESSION['current_profile']) ? 'active' : ''; ?>"
data-profile-id="<?php echo $profile['id']; ?>">
<img src="img/avatars/<?php echo $profile['avatar']; ?>" alt="<?php echo $profile['name']; ?>">
<span class="profile-name"><?php echo $profile['name']; ?></span>
</li>
<?php endforeach; ?>
</ul>
<div class="profile-actions">
<button class="add-profile-btn" id="add-profile-btn">
<i class="fas fa-plus-circle"></i>
<span>Add New Profile</span>
</button>
</div>
</div>
</div>
</nav>
</div>
</body>
</html>
</header>
<main>
<div class="container">
<div class="page-header">
<h1 class="page-title">Upload ROMs</h1>
</div>
<?php if($uploadMessage): ?>
<div class="alert alert-<?php echo $uploadStatus; ?>">
<i class="fas fa-info-circle"></i>
<span><?php echo $uploadMessage; ?></span>
</div>
<?php endif; ?>
<div class="upload-section">
<div class="upload-container">
<div>
<h2 class="upload-title">ROM Files</h2>
<p>Upload ROM files for various game consoles. Supported formats include .nes, .smc, .gba, .n64, and more.</p>
<div class="upload-box" id="rom-upload-box">
<i class="fas fa-upload"></i>
<h3>Drag & Drop ROM Files Here</h3>
<p>or click to browse your files</p>
<form action="upload.php" method="post" enctype="multipart/form-data" id="rom-upload-form">
<input type="hidden" name="upload_type" value="rom">
<input type="file" name="rom_files[]" id="rom-files" class="upload-input" multiple accept=".nes,.smc,.sfc,.n64,.z64,.gb,.gbc,.gba,.psx,.md,.smd,.zip">
</form>
</div>
<div class="supported-formats">
<h4>Supported Systems:</h4>
<div class="format-list">
<span class="format-tag">NES</span>
<span class="format-tag">SNES</span>
<span class="format-tag">N64</span>
<span class="format-tag">GameBoy</span>
<span class="format-tag">GBA</span>
<span class="format-tag">Genesis/MD</span>
<span class="format-tag">PSX</span>
<span class="format-tag">and more!</span>
</div>
</div>
</div>
<div>
<h2 class="upload-title">Recently Uploaded</h2>
<div class="upload-list" id="recent-uploads">
<?php
$recentUploads = [];
$files = glob('roms/*');
usort($files, function($a, $b) {
return filemtime($b) - filemtime($a);
});
$files = array_slice($files, 0, 5);
foreach ($files as $file) {
$filename = basename($file);
$ext = pathinfo($filename, PATHINFO_EXTENSION);
$console = getConsoleByExtension($ext);
$uploadDate = filemtime($file);
echo '<div class="upload-item">';
echo '<i class="fas fa-gamepad upload-item-icon"></i>';
echo '<div class="upload-item-info">';
echo '<span class="upload-item-name">' . $filename . '</span>';
echo '<span class="upload-item-meta">' . getConsoleFriendlyName($console) . ' • ' . date('M j, Y g:i A', $uploadDate) . '</span>';
echo '</div>';
echo '</div>';
}
if (count($files) == 0) {
echo '<div class="empty-uploads">No ROMs uploaded yet</div>';
}
?>
</div>
<div class="upload-tips">
<h4><i class="fas fa-lightbulb"></i> Upload Tips</h4>
<ul>
<li>You can upload multiple files at once</li>
<li>ZIP files are supported and will be automatically detected</li>
<li>Maximum upload size: <?php echo ini_get('upload_max_filesize'); ?></li>
<li>After uploading, your games will appear in the Game Library</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</main>
<!-- Add Profile Modal (same as in index.php) -->
<div class="modal-overlay" id="profile-modal">
<div class="modal">
<div class="modal-header">
<h2 class="modal-title">Create New Profile</h2>
<button class="close-modal" id="close-profile-modal">&times;</button>
</div>
<div class="modal-body">
<form id="profile-form" action="profile_action.php" method="post">
<div class="form-group">
<label for="profile-name" class="form-label">Profile Name</label>
<input type="text" id="profile-name" name="name" class="form-input" required>
</div>
<div class="form-group">
<label class="form-label">Select Avatar</label>
<div class="avatar-selector">
<?php for($i = 1; $i <= 8; $i++): ?>
<img src="img/avatars/avatar<?php echo $i; ?>.png"
class="avatar-option <?php echo ($i == 1) ? 'selected' : ''; ?>"
data-avatar="avatar<?php echo $i; ?>.png">
<?php endfor; ?>
</div>
<input type="hidden" id="selected-avatar" name="avatar" value="avatar1.png">
</div>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" id="cancel-profile">Cancel</button>
<button class="btn" id="save-profile">Create Profile</button>
</div>
</div>
</div>
<script src="js/profiles.js"></script>
<script src="js/upload.js"></script>
</body>
</html>