Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b7705fc110 | ||
|
|
4f634edcee | ||
|
|
ca13f15e48 | ||
|
|
ccea007e2a | ||
|
|
aeb5d36771 | ||
|
|
178c9cb550 |
No files matched your search
@@ -0,0 +1 @@
|
||||
ko_fi: ripalda
|
||||
@@ -3,7 +3,21 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="../../releases/latest"><b>Download PSGFX for Windows</b></a>
|
||||
<a href="https://github.com/elripalda/psgfx/releases/latest"><img alt="Latest release" src="https://img.shields.io/github/v/release/elripalda/psgfx?label=release&color=3b8cff"></a>
|
||||
<img alt="Platform: Windows" src="https://img.shields.io/badge/platform-Windows-1f5fd6">
|
||||
<img alt="For jailbroken PS5" src="https://img.shields.io/badge/for-jailbroken%20PS5-5cd3ff">
|
||||
<a href="LICENSE"><img alt="License: GPL-3.0-or-later" src="https://img.shields.io/badge/license-GPL--3.0--or--later-a9a6ff"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/elripalda/psgfx/releases/latest"><b>Download PSGFX 2.0</b></a> ·
|
||||
<a href="#how-to-use">How to use</a> ·
|
||||
<a href="#home-layout">Home layout</a> ·
|
||||
<a href="#community-icons">Community icons</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://ko-fi.com/ripalda"><img alt="Support me on Ko-fi" src="https://img.shields.io/badge/Support%20me%20on-Ko--fi-ff5e5b?style=for-the-badge&logo=ko-fi&logoColor=white"></a>
|
||||
</p>
|
||||
|
||||
PSGFX gives your PS5 homebrew and games new home-screen art, and lets you arrange the home screen itself: hide tiles like the PlayStation Store, rename anything, move apps between Games and Media, and lock your games in the order you want. Pick an app, drop in any image, and click **Apply**. PSGFX resizes and converts it to the exact format the console expects, uploads it to the right folder, and keeps a backup of the original so you can undo it with one click.
|
||||
@@ -14,6 +28,8 @@ It works through [PS5 Upload](https://github.com/phantomptr/ps5upload): keep the
|
||||
|
||||
## Features
|
||||
|
||||
- **New look (2.0).** A redesigned, larger glass interface with smooth animations, and a home-screen preview that slides between tiles like the console does.
|
||||
- **Community icons (new in 2.0).** Browse icons other PSGFX users made for the game you picked and use one in a click, or share yours. Shared icons are stored in a public GitHub repository and moderated.
|
||||
- **Custom icons.** Use any PNG or JPEG. PSGFX crops it to a square and saves it at the console's native size.
|
||||
- **Custom backgrounds.** Any 16:9 image becomes the backdrop behind the app on the home screen. Homebrew gets full 4K backgrounds.
|
||||
- **Homebrew and installed games.** Homebrew apps registered with PS5 Upload show up with their icons, and so do your installed PS4 and PS5 games, with their real names.
|
||||
@@ -38,7 +54,7 @@ It works through [PS5 Upload](https://github.com/phantomptr/ps5upload): keep the
|
||||
|
||||
1. Download `PSGFX.exe` from the [latest release](../../releases/latest) and double-click it. PSGFX opens in your browser.
|
||||
2. Open PS5 Upload and make sure it's connected to your PS5.
|
||||
3. Enter your PS5's IP address (the same one PS5 Upload uses) and click **Connect**.
|
||||
3. PSGFX asks for your PS5's IP address (the same one PS5 Upload uses). Enter it and click **Connect**. Next time it connects by itself.
|
||||
4. Choose a homebrew app or an installed game from the list.
|
||||
5. Drop an image onto the preview's tile (**icon**) or background (**background**), or use the **Choose image** buttons.
|
||||
6. Close that app or game on the PS5, then click **Apply to PS5**.
|
||||
@@ -49,12 +65,20 @@ To undo, select the app and click **Restore original**.
|
||||
### Home layout
|
||||
|
||||
1. Connect, then click **Home layout** at the top.
|
||||
2. Pick a tile in the strip under the preview. Choose **Games**, **Media** or **Hidden**, or type a new name.
|
||||
2. Pick a tile in the strip under the preview. Choose **Games**, **Media** or **Hidden**, or type a new name. When a tile leaves the tab you're on, the next one is selected so you can keep going, and **Undo** brings it back.
|
||||
3. Drag games into any order, or use **A–Z** / **Z–A**. Tick **Lock order** to keep it that way after you play other games.
|
||||
4. Click **Apply to PS5**, optionally saving the layout as a preset, then restart the PS5.
|
||||
|
||||

|
||||
|
||||
PSGFX edits the console's home-screen database (`/system_data/priv/mms/app.db`) through PS5 Upload. It uploads the new database to `/data/psgfx` and has PS5 Upload's console shell copy it into place, then reads it back to check it. Backups and presets are kept in the **PSGFX Layout** folder next to `PSGFX.exe`. If the home screen ever looks wrong, restore a backup, or use Safe Mode → **Rebuild Database**. App Library always comes back on its own; the system adds it regardless of the database.
|
||||
|
||||
### Community icons
|
||||
|
||||
Pick an app or game: the **Icon** tab shows icons other people shared for it. Click one to use it, then **Apply to PS5**. To share yours, choose or drop an icon, click **Share your icon**, and accept the guidelines. It goes live right away. Use the ⚑ on an icon to report it.
|
||||
|
||||
The library is run by `community-server/` (a few PHP files on the PSGFX website that save icons to a public GitHub repository). See [community-server/SETUP.md](community-server/SETUP.md).
|
||||
|
||||

|
||||
|
||||
## Tips
|
||||
@@ -105,3 +129,7 @@ If PS5 Upload's engine runs somewhere else (a NAS or Docker), add `"engine": "ho
|
||||
PSGFX is free software under the [GNU General Public License v3.0 or later](LICENSE). Versions up to 1.2 were released under the MIT License; 1.3 moved to the GPL because it includes the ATRAC9 encoder from [PS5 AT9 Converter](https://github.com/blackbearreloaded/ps5-at9-converter), which is GPL-3.0-or-later.
|
||||
|
||||
PSGFX is an independent fan project. It is not affiliated with, endorsed by, or sponsored by Sony Interactive Entertainment. "PlayStation" and "PS5" are trademarks of Sony Interactive Entertainment Inc. Only use PSGFX with games you own.
|
||||
|
||||
## Support
|
||||
|
||||
PSGFX is free. If it made your PS5 look the way you want, you can buy me a coffee on [Ko-fi](https://ko-fi.com/ripalda). Thank you!
|
||||
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 462 KiB |
|
Before Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 66 KiB |
@@ -9,7 +9,7 @@ mkdir -p build
|
||||
|
||||
BLAKE3_FLAGS="-O2 -DBLAKE3_NO_SSE2 -DBLAKE3_NO_SSE41 -DBLAKE3_NO_AVX2 -DBLAKE3_NO_AVX512"
|
||||
SQLITE_FLAGS="-O2 -DSQLITE_THREADSAFE=1 -DSQLITE_OMIT_LOAD_EXTENSION -DSQLITE_DEFAULT_MEMSTATUS=0"
|
||||
SOURCES="src/main.cpp src/ui.cpp src/assets_embed.cpp src/lib/bc7enc.cpp src/lib/bc7decomp.cpp src/presets_embed.cpp src/lib/at9/at9_encoder.cpp src/lib/at9/at9_snd0.cpp src/lib/at9/at9_decode.cpp src/lib/at9/at9_tables.cpp"
|
||||
SOURCES="src/main.cpp src/ui.cpp src/assets_embed.cpp src/lib/bc7enc.cpp src/lib/bc7decomp.cpp src/lib/at9/at9_encoder.cpp src/lib/at9/at9_snd0.cpp src/lib/at9/at9_decode.cpp src/lib/at9/at9_tables.cpp"
|
||||
|
||||
if [[ "${1:-}" == "linux" ]]; then
|
||||
for f in src/lib/blake3.c src/lib/blake3_dispatch.c src/lib/blake3_portable.c; do
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
config.php
|
||||
data/*.json
|
||||
files/
|
||||
@@ -0,0 +1,5 @@
|
||||
# Only the four pages are public; the settings and helper files are not.
|
||||
<FilesMatch "^(config|config\.sample|lib)\.php$">
|
||||
Require all denied
|
||||
</FilesMatch>
|
||||
Options -Indexes
|
||||
@@ -0,0 +1,73 @@
|
||||
# Community icons: setting up the server
|
||||
|
||||
PSGFX 2.0 lets people share icons with each other. Uploads go to a small PHP script on your own
|
||||
website, which checks each image and saves it to a public GitHub repository. Your GitHub key stays
|
||||
on your website, so it never ships inside PSGFX.
|
||||
|
||||
**Cost:** nothing. It runs on the web hosting plan you already have (any plan with PHP 8,
|
||||
including Hostinger's) and a free GitHub repository.
|
||||
|
||||
## 1. Make the icons repository on GitHub
|
||||
|
||||
1. Go to <https://github.com/new>.
|
||||
2. Name it `psgfx-community`, set it to **Public**, and tick **Add a README file**.
|
||||
3. Click **Create repository**.
|
||||
|
||||
## 2. Make a key that can only touch that repository
|
||||
|
||||
1. Go to <https://github.com/settings/personal-access-tokens/new>.
|
||||
2. **Token name:** `psgfx-community upload`. **Expiration:** the longest it offers. Put a reminder in
|
||||
your calendar to make a new one before it runs out (uploads stop working when it expires).
|
||||
3. **Repository access:** *Only select repositories* → `psgfx-community`.
|
||||
4. **Permissions** → **Repository permissions** → **Contents** → **Read and write**.
|
||||
5. Click **Generate token** and copy it (it starts with `github_pat_`). You only see it once.
|
||||
|
||||
## 3. Put the script on your website (Hostinger)
|
||||
|
||||
1. In hPanel open **Websites** → your site → **File Manager**.
|
||||
2. Open `public_html` and create a folder called `psgfx`.
|
||||
3. Open `psgfx` and upload everything from `psgfx-community-server.zip`, then extract it there
|
||||
(right-click the zip → **Extract**, into the current folder). Delete the zip afterwards.
|
||||
4. Rename `config.sample.php` to `config.php`, open it, and fill in:
|
||||
- `GITHUB_TOKEN`: the key from step 2.
|
||||
- `ADMIN_PASSWORD`: a long password for the moderation page.
|
||||
- `GITHUB_REPO` only if you used a different name than `elripalda/psgfx-community`.
|
||||
5. Save.
|
||||
|
||||
## 4. Check it
|
||||
|
||||
Open `https://YOUR-DOMAIN/psgfx/list.php?title=PPSA99764` in your browser. You should see:
|
||||
|
||||
```
|
||||
{"ok":true,"icons":[]}
|
||||
```
|
||||
|
||||
Then send the address (`https://YOUR-DOMAIN/psgfx/`; PSGFX 2.0 is built for `https://ps5gfx.elripalda.com/`) so it can be built into PSGFX. Until then you can
|
||||
try it yourself by adding `"community": "https://YOUR-DOMAIN/psgfx/"` to `psgfx.json` next to PSGFX.exe.
|
||||
|
||||
## Moderating
|
||||
|
||||
Open `https://YOUR-DOMAIN/psgfx/mod.php` and log in with `ADMIN_PASSWORD`.
|
||||
|
||||
- Every shared icon is listed, newest first. Reported ones come first.
|
||||
- An icon is hidden automatically after 3 different people report it, until you **Keep** or **Delete** it.
|
||||
- **Delete** removes it from GitHub and your site, so it disappears from PSGFX within a minute or two.
|
||||
- **Delete + block** also stops whoever uploaded it from uploading again.
|
||||
- You can also delete files straight on GitHub; PSGFX follows whatever is in the repository.
|
||||
|
||||
Limits per person are 10 uploads an hour and 30 a day. Change them in `config.php`.
|
||||
|
||||
## What's where
|
||||
|
||||
| File | What it does |
|
||||
| --- | --- |
|
||||
| `upload.php` | Receives an icon from PSGFX, checks it, makes a clean 512 × 512 PNG and a 128 × 128 thumbnail, saves both to GitHub, and keeps a copy in `files/` on your site. |
|
||||
| `list.php?title=ID` | The icons for one game, newest first. |
|
||||
| `index.php` | Every game and its icons in one file, for apps that show the whole library. |
|
||||
| `report.php` | Takes a report about an icon. |
|
||||
| `mod.php` | Your moderation page. |
|
||||
| `data/` | Upload limits, reports and blocks. Not reachable from the web. |
|
||||
| `files/` | Copies of every icon and thumbnail, served by your site. |
|
||||
|
||||
In the GitHub repository icons are saved as `icons/<title ID>/<time>_<hash>_<name>.png`, with the
|
||||
thumbnail at the same name under `thumbs/`.
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
// PSGFX community icons - settings. Copy this file to config.php and fill it in.
|
||||
// config.php holds your GitHub key: never share it or put it on GitHub.
|
||||
|
||||
// The GitHub repository the icons are saved to, as "owner/name". It must be public.
|
||||
const GITHUB_REPO = 'elripalda/psgfx-community';
|
||||
const GITHUB_BRANCH = 'main';
|
||||
|
||||
// A fine-grained personal access token with access to ONLY that repository,
|
||||
// permission "Contents: Read and write". See SETUP.md.
|
||||
const GITHUB_TOKEN = 'github_pat_PASTE_YOURS_HERE';
|
||||
|
||||
// Password for mod.php, where you review and delete icons. Pick a long one.
|
||||
const ADMIN_PASSWORD = 'change-me-to-something-long';
|
||||
|
||||
// Limits per person (by IP address).
|
||||
const UPLOADS_PER_HOUR = 10;
|
||||
const UPLOADS_PER_DAY = 30;
|
||||
// An icon is hidden after this many different people report it, until you look at it in mod.php.
|
||||
const REPORTS_TO_HIDE = 3;
|
||||
// Largest file accepted, in bytes.
|
||||
const MAX_UPLOAD_BYTES = 6 * 1024 * 1024;
|
||||
@@ -0,0 +1 @@
|
||||
Require all denied
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
// GET index.php -> every game that has community icons, and its icons, in one file.
|
||||
// Made for apps that show the whole library (for example on the PS5), so they don't have to ask per game.
|
||||
// { "ok": true, "updated": <unix time>, "titles": { "PPSA12345": [ {id, url, thumb, mirror?, mirror_thumb?, by, time}, ... ] } }
|
||||
require __DIR__ . '/lib.php';
|
||||
cors();
|
||||
$reports = data_read('reports');
|
||||
$titles = [];
|
||||
foreach (all_icons() as $i) {
|
||||
if (is_hidden($reports, $i['id'])) continue;
|
||||
$titles[$i['title']][] = icon_out($i);
|
||||
}
|
||||
ksort($titles);
|
||||
out(['ok' => true, 'updated' => time(), 'titles' => (object)$titles]);
|
||||
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
// Shared helpers for the PSGFX community icon server.
|
||||
declare(strict_types=1);
|
||||
if (!is_file(__DIR__ . '/config.php')) { http_response_code(500); exit('{"ok":false,"error":"The server is not set up yet (config.php is missing)."}'); }
|
||||
require __DIR__ . '/config.php';
|
||||
if (!defined('GITHUB_API')) define('GITHUB_API', 'https://api.github.com');
|
||||
const DATA_DIR = __DIR__ . '/data';
|
||||
const MIRROR_DIR = __DIR__ . '/files'; // copies of every icon and thumbnail, served by this site
|
||||
const TREE_TTL = 90; // seconds the icon list is cached before asking GitHub again
|
||||
|
||||
// PSGFX runs on http://127.0.0.1 in the user's browser, so every answer allows any origin.
|
||||
// Nothing here relies on cookies, so that is safe.
|
||||
function cors(): void {
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type');
|
||||
header('Access-Control-Max-Age: 86400');
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? '') === 'OPTIONS') { http_response_code(204); exit; }
|
||||
}
|
||||
function out(array $j, int $code = 200): void {
|
||||
http_response_code($code);
|
||||
header('Content-Type: application/json');
|
||||
header('Cache-Control: no-store');
|
||||
echo json_encode($j, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
function fail(string $msg, int $code = 400): void { out(['ok' => false, 'error' => $msg], $code); }
|
||||
|
||||
// Who is asking, as a salted hash: enough to rate-limit and block, without storing addresses.
|
||||
function who(): string {
|
||||
$ip = $_SERVER['HTTP_CF_CONNECTING_IP'] ?? $_SERVER['REMOTE_ADDR'] ?? '';
|
||||
return substr(hash('sha256', $ip . '|' . GITHUB_TOKEN), 0, 20);
|
||||
}
|
||||
|
||||
// Small JSON files in data/, updated under a lock so two requests can't overwrite each other.
|
||||
function data_path(string $name): string {
|
||||
if (!is_dir(DATA_DIR)) mkdir(DATA_DIR, 0750, true);
|
||||
return DATA_DIR . '/' . $name . '.json';
|
||||
}
|
||||
function data_read(string $name): array {
|
||||
$f = data_path($name);
|
||||
if (!is_file($f)) return [];
|
||||
$j = json_decode((string)file_get_contents($f), true);
|
||||
return is_array($j) ? $j : [];
|
||||
}
|
||||
function data_update(string $name, callable $fn) {
|
||||
$h = fopen(data_path($name), 'c+');
|
||||
flock($h, LOCK_EX);
|
||||
$j = json_decode((string)stream_get_contents($h), true);
|
||||
if (!is_array($j)) $j = [];
|
||||
$ret = $fn($j);
|
||||
ftruncate($h, 0); rewind($h);
|
||||
fwrite($h, json_encode($j, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
|
||||
fflush($h); flock($h, LOCK_UN); fclose($h);
|
||||
return $ret;
|
||||
}
|
||||
|
||||
function gh(string $method, string $path, ?array $body = null): array {
|
||||
$ch = curl_init(GITHUB_API . $path);
|
||||
$hdr = ['Accept: application/vnd.github+json', 'User-Agent: psgfx-community', 'X-GitHub-Api-Version: 2022-11-28',
|
||||
'Authorization: Bearer ' . GITHUB_TOKEN];
|
||||
if ($body !== null) { $hdr[] = 'Content-Type: application/json'; curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body)); }
|
||||
curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $hdr, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30]);
|
||||
$res = curl_exec($ch);
|
||||
$code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
$j = is_string($res) ? json_decode($res, true) : null;
|
||||
return [$code, is_array($j) ? $j : []];
|
||||
}
|
||||
|
||||
function valid_title(string $t): bool { return (bool)preg_match('/^[A-Z]{4}\d{5}$/', $t); }
|
||||
function raw_url(string $path): string { return 'https://raw.githubusercontent.com/' . GITHUB_REPO . '/' . GITHUB_BRANCH . '/' . $path; }
|
||||
|
||||
// Every icon in the repository: icons/<TITLE ID>/<time>_<hash>_<name>.png. GitHub is the source of truth,
|
||||
// so an icon you delete on GitHub disappears from PSGFX too. Cached for TREE_TTL seconds.
|
||||
function all_icons(bool $fresh = false): array {
|
||||
$cache = data_path('tree-cache');
|
||||
if (!$fresh && is_file($cache) && time() - filemtime($cache) < TREE_TTL) {
|
||||
$j = json_decode((string)file_get_contents($cache), true);
|
||||
if (is_array($j)) return $j;
|
||||
}
|
||||
[$code, $j] = gh('GET', '/repos/' . GITHUB_REPO . '/git/trees/' . GITHUB_BRANCH . '?recursive=1');
|
||||
if ($code === 404 || $code === 409) $j = ['tree' => []]; // empty repository: no icons yet
|
||||
elseif ($code !== 200) {
|
||||
if (is_file($cache)) return json_decode((string)file_get_contents($cache), true) ?: [];
|
||||
fail('GitHub is not answering right now. Try again in a minute.', 502);
|
||||
}
|
||||
$icons = [];
|
||||
foreach ($j['tree'] ?? [] as $e) {
|
||||
if (($e['type'] ?? '') !== 'blob') continue;
|
||||
if (!preg_match('#^icons/([A-Z]{4}\d{5})/((\d+)_([0-9a-f]{8})_?([^/]*))\.png$#', $e['path'], $m)) continue;
|
||||
$icons[] = ['id' => $e['path'], 'title' => $m[1], 'time' => (int)$m[3], 'by' => str_replace('-', ' ', $m[5]), 'sha' => $e['sha']];
|
||||
}
|
||||
usort($icons, fn($a, $b) => $b['time'] <=> $a['time']);
|
||||
file_put_contents($cache, json_encode($icons), LOCK_EX);
|
||||
return $icons;
|
||||
}
|
||||
function forget_icons(): void { @unlink(data_path('tree-cache')); }
|
||||
|
||||
// Reports: { "<icon path>": {"by": [who...], "ok": bool} }. "ok" means you reviewed it and kept it.
|
||||
function is_hidden(array $reports, string $id): bool {
|
||||
$r = $reports[$id] ?? null;
|
||||
return $r && empty($r['ok']) && count($r['by'] ?? []) >= REPORTS_TO_HIDE;
|
||||
}
|
||||
|
||||
// This site's own address, for links into files/. Uses whichever of http/https the request came in on.
|
||||
function site_base(): string {
|
||||
$https = ($_SERVER['HTTPS'] ?? '') !== '' && $_SERVER['HTTPS'] !== 'off' || ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https';
|
||||
$dir = rtrim(str_replace('\\', '/', dirname($_SERVER['SCRIPT_NAME'] ?? '/')), '/');
|
||||
return ($https ? 'https' : 'http') . '://' . ($_SERVER['HTTP_HOST'] ?? 'localhost') . $dir . '/';
|
||||
}
|
||||
function mirror_put(string $path, string $bytes): void {
|
||||
$f = MIRROR_DIR . '/' . $path;
|
||||
if (!is_dir(dirname($f))) mkdir(dirname($f), 0755, true);
|
||||
file_put_contents($f, $bytes);
|
||||
}
|
||||
function mirror_delete(string $path): void {
|
||||
@unlink(MIRROR_DIR . '/' . $path);
|
||||
@unlink(MIRROR_DIR . '/thumbs/' . substr($path, 6));
|
||||
}
|
||||
// What PSGFX and other apps get for one icon. "url"/"thumb" are on GitHub; "mirror"/"mirror_thumb" are on this site.
|
||||
function icon_out(array $i): array {
|
||||
$t = 'thumbs/' . substr($i['id'], 6);
|
||||
$o = ['id' => $i['id'], 'url' => raw_url($i['id']), 'thumb' => raw_url($t), 'by' => $i['by'], 'time' => $i['time']];
|
||||
if (is_file(MIRROR_DIR . '/' . $i['id'])) { $o['mirror'] = site_base() . 'files/' . $i['id']; $o['mirror_thumb'] = site_base() . 'files/' . $t; }
|
||||
return $o;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
// GET list.php?title=PPSA12345 -> the community icons for that game, newest first.
|
||||
require __DIR__ . '/lib.php';
|
||||
cors();
|
||||
$title = strtoupper((string)($_GET['title'] ?? ''));
|
||||
if (!valid_title($title)) fail('Unknown title ID.');
|
||||
$reports = data_read('reports');
|
||||
$list = [];
|
||||
foreach (all_icons() as $i) {
|
||||
if ($i['title'] !== $title || is_hidden($reports, $i['id'])) continue;
|
||||
$list[] = icon_out($i);
|
||||
}
|
||||
out(['ok' => true, 'icons' => $list]);
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
// mod.php - your moderation page. Log in with ADMIN_PASSWORD from config.php.
|
||||
// Reported icons come first. Delete removes an icon from GitHub (and this site), so it's gone from PSGFX.
|
||||
require __DIR__ . '/lib.php';
|
||||
session_start();
|
||||
header('Cache-Control: no-store');
|
||||
header('X-Frame-Options: DENY');
|
||||
$h = fn($s) => htmlspecialchars((string)$s, ENT_QUOTES);
|
||||
|
||||
if (isset($_POST['password'])) {
|
||||
if (hash_equals(ADMIN_PASSWORD, (string)$_POST['password']) && ADMIN_PASSWORD !== 'change-me-to-something-long') {
|
||||
session_regenerate_id(true); $_SESSION['mod'] = true; $_SESSION['csrf'] = bin2hex(random_bytes(16));
|
||||
} else { sleep(2); $err = 'Wrong password.'; }
|
||||
}
|
||||
if (isset($_GET['logout'])) { $_SESSION = []; session_destroy(); header('Location: mod.php'); exit; }
|
||||
|
||||
$msg = '';
|
||||
if (!empty($_SESSION['mod']) && isset($_POST['act']) && hash_equals($_SESSION['csrf'], (string)($_POST['csrf'] ?? ''))) {
|
||||
$id = (string)($_POST['id'] ?? '');
|
||||
if (preg_match('#^icons/[A-Z]{4}\d{5}/[^/]+\.png$#', $id)) {
|
||||
if ($_POST['act'] === 'keep') {
|
||||
data_update('reports', function (array &$r) use ($id) { $r[$id] = ['by' => [], 'ok' => true]; });
|
||||
$msg = 'Kept. It is visible again and reports were cleared.';
|
||||
} else {
|
||||
$deleted = 0;
|
||||
foreach ([$id, 'thumbs/' . substr($id, 6)] as $p) {
|
||||
[$c, $j] = gh('GET', '/repos/' . GITHUB_REPO . '/contents/' . $p . '?ref=' . GITHUB_BRANCH);
|
||||
if ($c !== 200 || empty($j['sha'])) continue;
|
||||
[$c2] = gh('DELETE', '/repos/' . GITHUB_REPO . '/contents/' . $p, ['message' => "Remove $p", 'sha' => $j['sha'], 'branch' => GITHUB_BRANCH]);
|
||||
if ($c2 === 200) $deleted++;
|
||||
}
|
||||
mirror_delete($id);
|
||||
data_update('reports', function (array &$r) use ($id) { unset($r[$id]); });
|
||||
if ($_POST['act'] === 'block') {
|
||||
$owner = data_read('owners')[$id] ?? null;
|
||||
if ($owner) data_update('blocked', function (array &$b) use ($owner) { if (!in_array($owner, $b, true)) $b[] = $owner; });
|
||||
$msg = $owner ? 'Deleted, and that person can no longer upload.' : 'Deleted. (Who uploaded it is unknown, so nobody was blocked.)';
|
||||
} else $msg = 'Deleted.';
|
||||
forget_icons();
|
||||
}
|
||||
}
|
||||
}
|
||||
?><!doctype html>
|
||||
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>PSGFX community - moderation</title>
|
||||
<style>
|
||||
body{margin:0;font:15px/1.5 system-ui,"Segoe UI",sans-serif;background:#070b16;color:#e8eefb}
|
||||
.wrap{max-width:1100px;margin:0 auto;padding:24px 16px}
|
||||
h1{font-size:22px;margin:0 0 4px}.m{color:#8c98ae}
|
||||
form.login{max-width:340px;margin:12vh auto;display:grid;gap:10px}
|
||||
input[type=password]{padding:10px 12px;border-radius:10px;border:1px solid #2a3552;background:#0f1628;color:inherit;font:inherit}
|
||||
button{padding:7px 12px;border-radius:9px;border:1px solid #2a3552;background:#16203a;color:inherit;font:inherit;cursor:pointer}
|
||||
button.p{background:#3d7bff;border-color:transparent}button.d{background:#5a1d26;border-color:#7a2a35}
|
||||
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:14px;margin-top:18px}
|
||||
.c{background:#0f1628;border:1px solid #1f2a45;border-radius:14px;padding:10px}
|
||||
.c.rep{border-color:#a0553a}.c img{width:100%;aspect-ratio:1;border-radius:10px;background:#1a2440;display:block}
|
||||
.c .t{font-weight:600;margin-top:8px}.c .s{color:#8c98ae;font-size:13px}.c .r{color:#ffb547;font-size:13px}
|
||||
.c form{display:inline}.acts{display:flex;gap:6px;flex-wrap:wrap;margin-top:8px}
|
||||
.msg{background:#13304a;border:1px solid #2f6bff;padding:8px 12px;border-radius:10px;margin-top:12px}
|
||||
</style></head><body><div class="wrap">
|
||||
<?php if (empty($_SESSION['mod'])): ?>
|
||||
<form class="login" method="post"><h1>PSGFX moderation</h1>
|
||||
<?php if (ADMIN_PASSWORD === 'change-me-to-something-long'): ?><p class="m">Set ADMIN_PASSWORD in config.php first.</p><?php endif; ?>
|
||||
<?php if (!empty($err)): ?><p class="r"><?= $h($err) ?></p><?php endif; ?>
|
||||
<input type="password" name="password" placeholder="Password" autofocus><button class="p">Log in</button></form>
|
||||
<?php else:
|
||||
$icons = all_icons(true); $reports = data_read('reports');
|
||||
$n = fn($i) => count($reports[$i['id']]['by'] ?? []);
|
||||
usort($icons, fn($a, $b) => ($n($b) <=> $n($a)) ?: ($b['time'] <=> $a['time']));
|
||||
$csrf = $_SESSION['csrf'];
|
||||
?>
|
||||
<h1>Community icons</h1>
|
||||
<p class="m"><?= count($icons) ?> icons. Reported ones come first; at <?= REPORTS_TO_HIDE ?> reports an icon is hidden until you keep or delete it. <a href="?logout" style="color:#5b90ff">Log out</a></p>
|
||||
<?php if ($msg): ?><div class="msg"><?= $h($msg) ?></div><?php endif; ?>
|
||||
<div class="grid">
|
||||
<?php foreach (array_slice($icons, 0, 300) as $i): $r = $n($i); $hidden = is_hidden($reports, $i['id']);
|
||||
$src = is_file(MIRROR_DIR . '/' . $i['id']) ? 'files/' . $i['id'] : raw_url($i['id']); ?>
|
||||
<div class="c<?= $r ? ' rep' : '' ?>"><img src="<?= $h($src) ?>" alt="" loading="lazy">
|
||||
<div class="t"><?= $h($i['title']) ?></div>
|
||||
<div class="s"><?= $h($i['by'] !== '' ? $i['by'] : 'Anonymous') ?> · <?= $h(date('Y-m-d H:i', $i['time'])) ?></div>
|
||||
<?php if ($r): ?><div class="r"><?= $r ?> report<?= $r > 1 ? 's' : '' ?><?= $hidden ? ' · hidden' : '' ?></div><?php endif; ?>
|
||||
<div class="acts">
|
||||
<?php foreach (($r ? ['keep' => 'Keep'] : []) + ['delete' => 'Delete', 'block' => 'Delete + block'] as $act => $label): ?>
|
||||
<form method="post" <?= $act !== 'keep' ? 'onsubmit="return confirm(\'Delete this icon?\')"' : '' ?>><input type="hidden" name="csrf" value="<?= $h($csrf) ?>"><input type="hidden" name="id" value="<?= $h($i['id']) ?>"><button name="act" value="<?= $act ?>" class="<?= $act === 'keep' ? 'p' : 'd' ?>"><?= $label ?></button></form>
|
||||
<?php endforeach; ?>
|
||||
</div></div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div></body></html>
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
// POST report.php id=<icon path> -> one report per person per icon.
|
||||
require __DIR__ . '/lib.php';
|
||||
cors();
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') fail('Use POST.', 405);
|
||||
$id = (string)($_POST['id'] ?? '');
|
||||
if (!preg_match('#^icons/[A-Z]{4}\d{5}/[^/]+\.png$#', $id)) fail('Unknown icon.');
|
||||
$me = who();
|
||||
data_update('reports', function (array &$r) use ($id, $me) {
|
||||
$e = $r[$id] ?? ['by' => [], 'ok' => false];
|
||||
if (!in_array($me, $e['by'], true)) $e['by'][] = $me;
|
||||
$r[$id] = $e;
|
||||
});
|
||||
out(['ok' => true]);
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
// POST upload.php (multipart): title, name (optional), agree=1, image (PNG or JPEG file).
|
||||
// Checks the image, turns it into a clean 512 x 512 PNG, and saves it to GitHub at
|
||||
// icons/<title>/<time>_<hash>_<name>.png, plus a 128 x 128 copy under thumbs/, and keeps a copy of both
|
||||
// on this site (files/) for consoles that can only use plain http. It shows up in PSGFX straight away.
|
||||
require __DIR__ . '/lib.php';
|
||||
cors();
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') fail('Use POST.', 405);
|
||||
if (($_POST['agree'] ?? '') !== '1') fail('Please accept the community guidelines first.');
|
||||
$title = strtoupper((string)($_POST['title'] ?? ''));
|
||||
if (!valid_title($title)) fail('Unknown title ID.');
|
||||
$name = preg_replace('/[^A-Za-z0-9 ._]/', '', (string)($_POST['name'] ?? ''));
|
||||
$name = trim(substr(preg_replace('/\s+/', ' ', $name), 0, 24));
|
||||
$f = $_FILES['image'] ?? null;
|
||||
if (!$f || $f['error'] !== UPLOAD_ERR_OK) fail('No image was received.');
|
||||
if ($f['size'] > MAX_UPLOAD_BYTES) fail('That image is too large.');
|
||||
|
||||
$me = who();
|
||||
if (in_array($me, data_read('blocked'), true)) fail('Uploads from you are turned off.', 403);
|
||||
|
||||
// Rate limit: drop timestamps older than a day, then count.
|
||||
$now = time();
|
||||
$allowed = data_update('uploads', function (array &$u) use ($me, $now) {
|
||||
foreach ($u as $k => $times) { $u[$k] = array_values(array_filter($times, fn($t) => $t > $now - 86400)); if (!$u[$k]) unset($u[$k]); }
|
||||
$mine = $u[$me] ?? [];
|
||||
$hour = count(array_filter($mine, fn($t) => $t > $now - 3600));
|
||||
if ($hour >= UPLOADS_PER_HOUR || count($mine) >= UPLOADS_PER_DAY) return false;
|
||||
$u[$me][] = $now;
|
||||
return true;
|
||||
});
|
||||
if (!$allowed) fail("You've shared a lot of icons recently. Try again later.", 429);
|
||||
|
||||
// Decode and re-encode, so only real pixels reach GitHub (nothing hidden inside the file).
|
||||
$info = @getimagesize($f['tmp_name']);
|
||||
if (!$info || !in_array($info[2], [IMAGETYPE_PNG, IMAGETYPE_JPEG], true)) fail("That file isn't a PNG or JPEG image.");
|
||||
if ($info[0] < 128 || $info[1] < 128) fail('That image is too small. Use one at least 256 x 256.');
|
||||
if ($info[0] * $info[1] > 40000000) fail('That image is too large.');
|
||||
$src = $info[2] === IMAGETYPE_PNG ? @imagecreatefrompng($f['tmp_name']) : @imagecreatefromjpeg($f['tmp_name']);
|
||||
if (!$src) fail("That image couldn't be read.");
|
||||
$w = imagesx($src); $h = imagesy($src); $side = min($w, $h);
|
||||
$dst = imagecreatetruecolor(512, 512);
|
||||
imagealphablending($dst, false); imagesavealpha($dst, true);
|
||||
imagefill($dst, 0, 0, imagecolorallocatealpha($dst, 0, 0, 0, 127));
|
||||
imagecopyresampled($dst, $src, 0, 0, intdiv($w - $side, 2), intdiv($h - $side, 2), 512, 512, $side, $side);
|
||||
ob_start(); imagepng($dst, null, 9); $png = (string)ob_get_clean();
|
||||
$th = imagecreatetruecolor(128, 128);
|
||||
imagealphablending($th, false); imagesavealpha($th, true);
|
||||
imagecopyresampled($th, $dst, 0, 0, 0, 0, 128, 128, 512, 512);
|
||||
ob_start(); imagepng($th, null, 9); $thumb = (string)ob_get_clean();
|
||||
imagedestroy($src); imagedestroy($dst); imagedestroy($th);
|
||||
|
||||
$hash = substr(sha1($png), 0, 8);
|
||||
foreach (all_icons() as $i) if ($i['title'] === $title && str_contains($i['id'], '_' . $hash)) fail('That icon is already shared for this game.');
|
||||
|
||||
$slug = str_replace(' ', '-', $name);
|
||||
$path = "icons/$title/{$now}_{$hash}" . ($slug !== '' ? "_$slug" : '') . '.png';
|
||||
[$code, $j] = gh('PUT', '/repos/' . GITHUB_REPO . '/contents/' . $path, [
|
||||
'message' => "Add icon for $title" . ($name !== '' ? " by $name" : ''),
|
||||
'content' => base64_encode($png),
|
||||
'branch' => GITHUB_BRANCH,
|
||||
]);
|
||||
if ($code !== 201 && $code !== 200) fail("Couldn't save the icon to GitHub (error $code). Try again later.", 502);
|
||||
// The small version for PS5 apps and quick lists. If this one fails, the mirror still has it.
|
||||
$tpath = 'thumbs/' . substr($path, 6);
|
||||
gh('PUT', '/repos/' . GITHUB_REPO . '/contents/' . $tpath, ['message' => "Thumbnail for $path", 'content' => base64_encode($thumb), 'branch' => GITHUB_BRANCH]);
|
||||
mirror_put($path, $png); mirror_put($tpath, $thumb);
|
||||
data_update('owners', function (array &$o) use ($path, $me) { $o[$path] = $me; });
|
||||
forget_icons();
|
||||
out(['ok' => true, 'icon' => icon_out(['id' => $path, 'title' => $title, 'by' => $name, 'time' => $now])]);
|
||||
|
Before Width: | Height: | Size: 366 KiB After Width: | Height: | Size: 600 KiB |
|
Before Width: | Height: | Size: 323 KiB After Width: | Height: | Size: 547 KiB |
|
After Width: | Height: | Size: 458 KiB |
|
Before Width: | Height: | Size: 355 KiB After Width: | Height: | Size: 561 KiB |
@@ -1,4 +1,4 @@
|
||||
PSGFX 1.3
|
||||
PSGFX 2.0
|
||||
Custom icons and backgrounds for your PS5 homebrew and games, and your home-screen layout.
|
||||
|
||||
BEFORE YOU START
|
||||
@@ -11,13 +11,19 @@ STEPS
|
||||
1. Double-click PSGFX.exe. PSGFX opens in your browser.
|
||||
(If Windows SmartScreen warns, click "More info" > "Run anyway".
|
||||
If the firewall asks, allow it on private networks.)
|
||||
2. Type your PS5's IP address and click Connect.
|
||||
2. PSGFX asks for your PS5's IP address. Type it and click Connect.
|
||||
(Next time it connects by itself. Click the connection button at the top to change it.)
|
||||
3. Pick a homebrew app or an installed game from the list.
|
||||
4. Drop an image on the preview's tile (icon) or background (background),
|
||||
or use the "Choose image" buttons. Any PNG or JPEG works.
|
||||
5. Close that app or game on the PS5, then click "Apply to PS5".
|
||||
6. Restart the PS5 to see the new art.
|
||||
|
||||
COMMUNITY ICONS (new in 2.0)
|
||||
- Pick an app or game: the Icon tab shows icons other people shared for it.
|
||||
Click one to use it, then Apply to PS5.
|
||||
- To share yours, choose an icon, click "Share your icon" and accept the guidelines.
|
||||
|
||||
MUSIC (new in 1.3)
|
||||
- Drop an MP3, WAV or FLAC on the preview, or use "Choose song".
|
||||
- Set where it starts and how long it runs, listen to it, then Apply.
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
#include "ps5client.h"
|
||||
#include "layout.h"
|
||||
#include "lib/at9/at9.h"
|
||||
#include "presets.h"
|
||||
#include "lib/json.hpp"
|
||||
|
||||
#include <atomic>
|
||||
@@ -29,7 +28,11 @@
|
||||
#include <shellapi.h>
|
||||
#include <tlhelp32.h>
|
||||
#endif
|
||||
#define STUDIO_VERSION "1.3"
|
||||
#define STUDIO_VERSION "2.0"
|
||||
// Community icons server (community-server/ in this repo). psgfx.json "community" overrides it; empty hides the feature.
|
||||
#ifndef PSGFX_COMMUNITY
|
||||
#define PSGFX_COMMUNITY "https://ps5gfx.elripalda.com/"
|
||||
#endif
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
@@ -51,6 +54,7 @@ struct Staged { // the user's image, decoded; converted per targ
|
||||
static std::mutex g_mu;
|
||||
static std::string g_ip;
|
||||
static std::string g_engine; // PS5 Upload's engine, "host:port"; empty = 127.0.0.1:19113
|
||||
static std::string g_community = PSGFX_COMMUNITY, g_community_set; // the server, and the psgfx.json override if any
|
||||
static std::map<std::string, Staged> g_staged; // "icon" | "background"
|
||||
static std::map<std::string, std::vector<uint8_t>> g_icon_cache;
|
||||
static std::string g_config_path = "psgfx.json";
|
||||
@@ -61,12 +65,17 @@ static void load_config() {
|
||||
std::ifstream f(g_config_path);
|
||||
if (!f && !g_legacy_config_path.empty()) f.open(g_legacy_config_path);
|
||||
if (!f) return;
|
||||
try { json j; f >> j; g_ip = j.value("ip", ""); g_engine = j.value("engine", ""); } catch (...) {}
|
||||
try {
|
||||
json j; f >> j; g_ip = j.value("ip", ""); g_engine = j.value("engine", "");
|
||||
g_community_set = j.value("community", "");
|
||||
if (!g_community_set.empty()) g_community = g_community_set;
|
||||
} catch (...) {}
|
||||
}
|
||||
static void save_config() {
|
||||
std::ofstream f(g_config_path);
|
||||
json j = {{"ip", g_ip}};
|
||||
if (!g_engine.empty()) j["engine"] = g_engine;
|
||||
if (!g_community_set.empty()) j["community"] = g_community_set;
|
||||
f << j.dump(2);
|
||||
}
|
||||
|
||||
@@ -978,7 +987,7 @@ static void handle(sock_t s, Req &r) {
|
||||
else if (r.path == "/f/sora700.woff2") { d = sora700; n = sora700_len; }
|
||||
if (d) { respond(s, 200, "font/woff2", d, n); return; }
|
||||
}
|
||||
if (r.path == "/api/config") { std::lock_guard<std::mutex> l(g_mu); respond_json(s, {{"ip", g_ip}}); return; }
|
||||
if (r.path == "/api/config") { std::lock_guard<std::mutex> l(g_mu); respond_json(s, {{"ip", g_ip}, {"community", g_community}, {"version", STUDIO_VERSION}}); return; }
|
||||
if (r.path == "/api/connect" && r.method == "POST") {
|
||||
json j = json::parse(std::string(r.body.begin(), r.body.end()));
|
||||
{ std::lock_guard<std::mutex> l(g_mu); g_ip = j.value("ip", ""); g_icon_cache.clear(); }
|
||||
@@ -1000,18 +1009,6 @@ static void handle(sock_t s, Req &r) {
|
||||
}
|
||||
if (r.path.rfind("/api/stage/", 0) == 0 && r.method == "POST") { respond_json(s, stage(r.path.substr(11), r.body)); return; }
|
||||
if (r.path.rfind("/api/unstage/", 0) == 0) { std::lock_guard<std::mutex> l(g_mu); g_staged.erase(r.path.substr(13)); respond_json(s, {{"ok", true}}); return; }
|
||||
if (r.path.rfind("/api/presets/", 0) == 0) { // /api/presets/<title id> -> the built-in icons for it
|
||||
std::string id = r.path.substr(13);
|
||||
json list = json::array();
|
||||
for (size_t i = 0; i < PRESETS_COUNT; i++) if (id == PRESETS[i].title_id) list.push_back({{"i", i}, {"app", PRESETS[i].app}});
|
||||
respond_json(s, {{"ok", true}, {"presets", list}}); return;
|
||||
}
|
||||
if (r.path.rfind("/api/preset/", 0) == 0) { // /api/preset/<index>: the image; POST stages it as the icon
|
||||
size_t i = (size_t)strtoull(r.path.c_str() + 12, nullptr, 10);
|
||||
if (i >= PRESETS_COUNT) { respond(s, 404, "text/plain", "none", 4); return; }
|
||||
if (r.method == "POST") { respond_json(s, stage("icon", std::vector<uint8_t>(PRESETS[i].data, PRESETS[i].data + PRESETS[i].len))); return; }
|
||||
respond(s, 200, "image/png", PRESETS[i].data, PRESETS[i].len); return;
|
||||
}
|
||||
if (r.path == "/api/music/clip" && r.method == "POST") { respond_json(s, music_clip(json::parse(std::string(r.body.begin(), r.body.end())))); return; }
|
||||
if (r.path == "/api/staged/music") {
|
||||
std::vector<uint8_t> b;
|
||||
@@ -1019,6 +1016,12 @@ static void handle(sock_t s, Req &r) {
|
||||
if (b.empty()) { respond(s, 404, "text/plain", "none", 4); return; }
|
||||
respond(s, 200, "audio/wav", b.data(), b.size()); return;
|
||||
}
|
||||
if (r.path == "/api/share/icon") { // the staged icon at full size, for sharing with the community
|
||||
std::vector<uint8_t> b;
|
||||
{ std::lock_guard<std::mutex> l(g_mu); auto it = g_staged.find("icon"); if (it != g_staged.end()) b = img::to_png(img::cover(it->second.src, 512, 512)); }
|
||||
if (b.empty()) { respond(s, 404, "text/plain", "none", 4); return; }
|
||||
respond(s, 200, "image/png", b.data(), b.size()); return;
|
||||
}
|
||||
if (r.path.rfind("/api/staged/", 0) == 0) {
|
||||
std::vector<uint8_t> b;
|
||||
{ std::lock_guard<std::mutex> l(g_mu); auto it = g_staged.find(r.path.substr(12)); if (it != g_staged.end()) b = it->second.preview_png; }
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
// Preset icons built into PSGFX, by title ID (assets/presets, embedded by tools/embed_presets.py).
|
||||
#pragma once
|
||||
#include <cstddef>
|
||||
|
||||
struct Preset { const char *app; const char *title_id; const unsigned char *data; size_t len; };
|
||||
extern const Preset PRESETS[];
|
||||
extern const size_t PRESETS_COUNT;
|
||||
@@ -1,30 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Regenerate src/presets_embed.cpp from assets/presets/<App name>/<TITLE ID>/<n>.png (preset icons).
|
||||
Run from the repository root: python3 tools/embed_presets.py"""
|
||||
import os, re
|
||||
|
||||
root = "assets/presets"
|
||||
items = []
|
||||
for app in sorted(os.listdir(root)):
|
||||
for tid in sorted(os.listdir(os.path.join(root, app))):
|
||||
d = os.path.join(root, app, tid)
|
||||
if not os.path.isdir(d) or not re.fullmatch(r"[A-Z]{4}\d{5}", tid): continue
|
||||
names = sorted((n for n in os.listdir(d) if n.lower().endswith(".png")),
|
||||
key=lambda n: (int(m.group()) if (m := re.match(r"\d+", n)) else 1 << 30, n))
|
||||
for n in names: items.append((app, tid, os.path.join(d, n)))
|
||||
|
||||
out = ["// Generated by tools/embed_presets.py - do not edit.",
|
||||
"#include <cstddef>", '#include "presets.h"']
|
||||
for i, (_, _, path) in enumerate(items):
|
||||
data = open(path, "rb").read()
|
||||
out.append(f"static const unsigned char p{i}[] = {{")
|
||||
for j in range(0, len(data), 24):
|
||||
out.append(" " + ",".join(str(b) for b in data[j:j + 24]) + ",")
|
||||
out.append("};")
|
||||
out.append("extern const Preset PRESETS[] = {")
|
||||
for i, (app, tid, _) in enumerate(items):
|
||||
out.append(f' {{"{app}", "{tid}", p{i}, sizeof p{i}}},')
|
||||
out.append("};")
|
||||
out.append(f"extern const size_t PRESETS_COUNT = {len(items)};")
|
||||
open("src/presets_embed.cpp", "w").write("\n".join(out) + "\n")
|
||||
print(f"wrote src/presets_embed.cpp ({len(items)} icons)")
|
||||