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.
108
README.md
@ -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
@ -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
@ -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">×</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>
|
||||
1
cache/retroachievements/63bd4c10c895070621df80e7aaf48e7e.json
vendored
Normal file
1
cache/retroachievements/a945c9ea703250c42b01b3c1c8263323.json
vendored
Normal file
1
cache/retroachievements/b097c3655d6a0b2530311ad5ef15e8dc.json
vendored
Normal file
1
cache/retroachievements/dd1818f8c085ff272162d2398845ddeb.json
vendored
Normal file
1
cache/retroachievements/f6a91b8e7816be0562017961488ab9a9.json
vendored
Normal file
8
config/retroachievements.json
Normal 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
@ -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
@ -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
@ -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
@ -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
@ -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
|
After Width: | Height: | Size: 4.6 KiB |
BIN
img/avatars/avatar2.png
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
BIN
img/avatars/avatar3.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
img/avatars/avatar4.png
Normal file
|
After Width: | Height: | Size: 119 KiB |
BIN
img/avatars/avatar5.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
img/avatars/avatar6.png
Normal file
|
After Width: | Height: | Size: 99 KiB |
BIN
img/avatars/avatar7.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
img/avatars/avatar8.png
Normal file
|
After Width: | Height: | Size: 95 KiB |
BIN
img/cache/icons/63bd4c10c895070621df80e7aaf48e7e.png
vendored
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
img/cache/icons/a945c9ea703250c42b01b3c1c8263323.png
vendored
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
img/cache/icons/b097c3655d6a0b2530311ad5ef15e8dc.png
vendored
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
img/cache/icons/dd1818f8c085ff272162d2398845ddeb.png
vendored
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
img/cache/icons/f6a91b8e7816be0562017961488ab9a9.png
vendored
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
img/cache/screenshots/63bd4c10c895070621df80e7aaf48e7e_boxart.png
vendored
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
img/cache/screenshots/63bd4c10c895070621df80e7aaf48e7e_ingame.png
vendored
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
img/cache/screenshots/63bd4c10c895070621df80e7aaf48e7e_title.png
vendored
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
img/cache/screenshots/a945c9ea703250c42b01b3c1c8263323_boxart.png
vendored
Normal file
|
After Width: | Height: | Size: 107 KiB |
BIN
img/cache/screenshots/a945c9ea703250c42b01b3c1c8263323_ingame.png
vendored
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
img/cache/screenshots/a945c9ea703250c42b01b3c1c8263323_title.png
vendored
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
img/cache/screenshots/b097c3655d6a0b2530311ad5ef15e8dc_boxart.png
vendored
Normal file
|
After Width: | Height: | Size: 106 KiB |
BIN
img/cache/screenshots/b097c3655d6a0b2530311ad5ef15e8dc_ingame.png
vendored
Normal file
|
After Width: | Height: | Size: 133 KiB |
BIN
img/cache/screenshots/b097c3655d6a0b2530311ad5ef15e8dc_title.png
vendored
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
img/cache/screenshots/dd1818f8c085ff272162d2398845ddeb_boxart.png
vendored
Normal file
|
After Width: | Height: | Size: 123 KiB |
BIN
img/cache/screenshots/dd1818f8c085ff272162d2398845ddeb_ingame.png
vendored
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
img/cache/screenshots/dd1818f8c085ff272162d2398845ddeb_title.png
vendored
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
img/cache/screenshots/f6a91b8e7816be0562017961488ab9a9_boxart.png
vendored
Normal file
|
After Width: | Height: | Size: 97 KiB |
BIN
img/cache/screenshots/f6a91b8e7816be0562017961488ab9a9_ingame.png
vendored
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
img/cache/screenshots/f6a91b8e7816be0562017961488ab9a9_title.png
vendored
Normal file
|
After Width: | Height: | Size: 81 KiB |
450
includes/retroachievements.php
Normal 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
@ -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">×</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
@ -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
@ -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
@ -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
@ -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
@ -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">×</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">×</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
@ -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
@ -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');
|
||||
1
profiles/67bfaf6ae3257.json
Normal file
@ -0,0 +1 @@
|
||||
{"id":"67bfaf6ae3257","name":"Test Profile","avatar":"avatar3.png","created":"2025-02-27 01:18:50"}
|
||||
@ -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
@ -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";
|
||||
}
|
||||
BIN
saves/67bfaf6ae3257/Kirby Super Star_1.state
Normal file
813
settings.php
Normal 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">×</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">×</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
@ -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
@ -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>
|
||||
306
upload.php
@ -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">×</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>
|
||||