diff --git a/CHANGELOG.md b/CHANGELOG.md
index 16fa7a7..78a749b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
# Changelog
+## 1.4.2
+
+- One place on the home screen always stays unpinned. With every place pinned, the last game opened (the
+ jailbreak's entry, say) had nowhere to go and ended up in the game library. Pin the whole row now leaves the
+ first place free, and Lineup will not save a home screen pinned in full.
+- A row pinned in full by an older version opens with an offer to free its first place.
+
## 1.4.1
1.4.0 was not released; its changes ship here.
diff --git a/app/api.js b/app/api.js
index 78ed7f6..c1566e6 100644
--- a/app/api.js
+++ b/app/api.js
@@ -3,7 +3,7 @@
import {Archives, Downloads, FileSystem, Http, Payloads} from '@ps5-react/core';
import {T} from './i18n.js';
-export const VERSION = '1.4.1';
+export const VERSION = '1.4.2';
const BASE = 'http://127.0.0.1:9077';
/** An error the app shows as is: a short title and what to do about it. */
diff --git a/app/app.json b/app/app.json
index 5ecf898..01c36cd 100644
--- a/app/app.json
+++ b/app/app.json
@@ -3,7 +3,7 @@
"titleId": "PPSA99077",
"contentId": "UP9000-PPSA99077_00-LINEUP0000000000",
"name": "Lineup",
- "version": "01.004.001",
+ "version": "01.004.002",
"render": {
"width": 1920,
"height": 1080
diff --git a/app/i18n.js b/app/i18n.js
index e601f07..0645b1c 100644
--- a/app/i18n.js
+++ b/app/i18n.js
@@ -53,6 +53,11 @@ const en = {
missingBody: 'The home screen database was rebuilt or updated, so the games no longer hold their places. '
+ 'Pin them again?',
reapply: 'Pin again', later: 'Not now',
+ keepFree: 'Leave one place on the home screen unpinned, for the last game opened.',
+ fullTitle: 'Every place on the home screen is pinned',
+ fullBody: 'The last game opened has no place left there and ends up in the game library. '
+ + 'Free the first place for it?',
+ freeFirst: 'Free the first place',
},
saved: ['Order saved', 'Closing the app. The home screen comes back with the new order.'],
closeNow: 'Close now',
@@ -158,6 +163,11 @@ const pt = {
missingBody: 'O banco de dados da tela inicial foi reconstruído ou atualizado, e os jogos não seguram mais o '
+ 'lugar. Fixar de novo?',
reapply: 'Fixar de novo', later: 'Agora não',
+ keepFree: 'Deixe um lugar da tela inicial sem fixar, para o último jogo aberto.',
+ fullTitle: 'Todos os lugares da tela inicial estão fixos',
+ fullBody: 'O último jogo aberto fica sem lugar ali e vai parar na biblioteca de jogos. '
+ + 'Liberar o primeiro lugar para ele?',
+ freeFirst: 'Liberar o primeiro lugar',
},
saved: ['Ordem salva', 'Fechando o app. A tela inicial já volta com a nova ordem.'],
closeNow: 'Fechar agora',
diff --git a/app/index.jsx b/app/index.jsx
index 0b5a93a..4f6fb30 100644
--- a/app/index.jsx
+++ b/app/index.jsx
@@ -39,6 +39,14 @@ const sortKey = name => name.normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCas
// Games moved or pinned/unpinned since the row was read. `pinned` maps the games read pinned to their slots.
const changed = s => s.order.reduce((count, id, i) => count + (s.home[id] !== i || !s.pins[id] !== !(id in s.pinned)), 0);
const allPinned = pinned => Object.fromEntries(Object.keys(pinned).map(id => [id, true]));
+// Every home place pinned: the last game opened (the jailbreak's entry, say) would have no place there and end up in
+// the game library, so one stays free. lineupd refuses such a save too.
+const homeFull = (order, pins) => order.slice(0, HOME_LIMIT).every(id => pins[id]);
+const without = (pins, id) => {
+ const rest = {...pins};
+ delete rest[id];
+ return rest;
+};
// The order the pins ask for, as lineupd's triggers work it out: pinned games at their slots (packed at the end of
// a row that got shorter), the others in their order in the free places.
@@ -295,10 +303,11 @@ function App() {
if (game.pinned !== null && game.pinned !== undefined) pinned[game.titleId] = game.pinned;
});
const base = list.map(game => game.titleId);
- const missing = pins === 'missing';
+ // A row pinned in full by 1.4.1 or older.
+ const layer = pins === 'missing' ? 'pinsMissing' : pins === 'on' && homeFull(base, allPinned(pinned)) ? 'pinsFull' : null;
light(false);
patch({phase: 'row', profiles, user, games, home, base, order: base, pins: allPinned(pinned), pinned, focus: 0,
- dir: 1, lifted: false, layer: missing ? 'pinsMissing' : null, target: null, pending: missing ? null : {index: 0}});
+ dir: 1, lifted: false, layer, target: null, pending: layer ? null : {index: 0}});
};
const boot = async () => {
@@ -418,6 +427,11 @@ function App() {
toast(T.alreadySaved);
return;
}
+ if (homeFull(now.order, now.pins)) {
+ close();
+ toast(T.pins.keepFree);
+ return;
+ }
light(false);
patch({lifted: false, layer: 'busy'});
const result = await api.save(now.user, now.order, now.games, now.pins);
@@ -480,13 +494,11 @@ function App() {
return true;
}
if (action === 'square') {
- patch(p => {
- const id = p.order[p.focus];
- const pins = {...p.pins};
- if (pins[id]) delete pins[id];
- else pins[id] = true;
- return {pins};
- });
+ const p = ref.current;
+ const id = p.order[p.focus];
+ const pins = p.pins[id] ? without(p.pins, id) : {...p.pins, [id]: true};
+ if (!p.pins[id] && homeFull(p.order, pins)) toast(T.pins.keepFree);
+ else patch({pins});
return true;
}
return false;
@@ -496,14 +508,17 @@ function App() {
patch(p => ({order: p.base, pins: allPinned(p.pinned), layer: null, pending: {index: 0}}));
toast(T.discarded);
};
- const pinAll = on => patch(p => ({pins: on ? Object.fromEntries(p.order.map(id => [id, true])) : {}, layer: null,
- pending: {index: p.focus}}));
- // Puts the games back at their slots and saves, which creates the triggers again.
+ // The whole row but its first place, which stays free.
+ const pinAll = on => patch(p => ({pins: on ? Object.fromEntries(p.order.slice(1).map(id => [id, true])) : {},
+ layer: null, pending: {index: p.focus}}));
+ // Puts the games back at their slots and saves, which creates the triggers again. A row pinned in full gets its
+ // first place freed.
const reapply = () => {
const now = ref.current;
const order = pinnedOrder(now.order, now.pinned);
- patch({order});
- save(null, {...now, order}, true);
+ const pins = homeFull(order, now.pins) ? without(now.pins, order[0]) : now.pins;
+ patch({order, pins});
+ save(null, {...now, order, pins}, true);
};
const unpinEverywhere = async () => {
patch({layer: 'busy'});
@@ -664,6 +679,12 @@ function App() {
) : null}
+ {s.layer === 'pinsFull' ? (
+
+ ) : null}
{s.layer === 'busy' ? (
true} onAction={() => true}>
diff --git a/app/payloads/lineupd.c b/app/payloads/lineupd.c
index e84ac3d..3772344 100644
--- a/app/payloads/lineupd.c
+++ b/app/payloads/lineupd.c
@@ -60,7 +60,7 @@
#define LOG(fmt, ...) klog_printf("[lineupd] " fmt "\n", ##__VA_ARGS__)
#endif
-#define LINEUPD_VERSION "1.4.1"
+#define LINEUPD_VERSION "1.4.2"
#define TITLE_ID "PPSA99077"
#ifdef LINEUPD_DIAG
// Phase 0 diagnostic build (docs/pt-BR/PLANO-1.2.md): its own port, open to the LAN, so it runs beside the
@@ -73,6 +73,8 @@
#define MAX_HEADER (16 * 1024)
#define MAX_BODY (64 * 1024)
#define MAX_GAMES 512
+// The home screen shows this many games of the row, as HOME_LIMIT in app/index.jsx.
+#define HOME_LIMIT 12
#define KEEP_BACKUPS 5
static const char* db_path = "/system_data/priv/mms/app.db";
@@ -495,6 +497,14 @@ static int unpin(const char* user, char* backup, size_t backup_size, char* messa
// Saves `posted` as the new order. Returns the HTTP status; `message` explains a failure.
static int save_order(const char* user, const Row* posted, int posted_count, char* backup, size_t backup_size,
char* integrity, size_t integrity_size, char* message, size_t message_size) {
+ // With every home place pinned, the last game opened (the jailbreak's entry, say) gets no place there and ends up
+ // in the game library: one stays free.
+ int home = posted_count < HOME_LIMIT ? posted_count : HOME_LIMIT, free_places = 0;
+ for (int i = 0; i < home; ++i) free_places += !posted[i].pinned;
+ if (home && !free_places) {
+ snprintf(message, message_size, "Leave one of the first %d places unpinned. Nothing was saved.", home);
+ return 400;
+ }
sqlite3* db = open_db(1);
if (!db) { snprintf(message, message_size, "Could not open the home screen database."); return 500; }
int status = 500;
@@ -1732,6 +1742,17 @@ static void test_pins(void) {
const char* one = "0000000001";
int slots[MAX_GAMES];
given = scalar("SELECT MAX(lastAccessIndex) FROM tbl_iconinfo_0000000001");
+ // A row pinned in full is refused: one home place stays free.
+ {
+ static Row full[MAX_GAMES];
+ sqlite3* db = open_db(0);
+ int count = read_row(db, one, full, NULL, MAX_GAMES);
+ sqlite3_close(db);
+ for (int i = 0; i < count; ++i) full[i].pinned = 1;
+ char backup[256], integrity[256], message[300];
+ assert(save_order(one, full, count, backup, sizeof backup, integrity, sizeof integrity, message, sizeof message) == 400);
+ assert(!strcmp(pins_of(one, slots), "off"));
+ }
save_digits(one, "31254", "35");
expect_row(one, "31254");
assert(!strcmp(pins_of(one, slots), "on") && slots[0] == 0 && slots[1] < 0 && slots[3] == 3);
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
index 6e6be57..eebfba4 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -81,7 +81,7 @@ carry `X-Lineup: 1`, so a web page cannot trigger one with a plain form.
Errors are `{"error": ""}`:
-- 400: malformed request.
+- 400: malformed request, or every home place pinned (see Pinned games).
- 403: the `X-Lineup` header is missing.
- 404: no such profile or icon.
- 409: the row changed since it was read.
@@ -104,6 +104,11 @@ Saving drops the profile's triggers, writes the order and the pins, and creates
are pins, all in one transaction. When the last pin of every profile is gone, the tables go too. Rebuilding the
database can drop the triggers; `/row` then says `"missing"` and the app offers to pin the games again.
+One of the first `HOME_LIMIT` (12) places always stays unpinned: with all of them pinned, the last game opened,
+such as the jailbreak's entry, has no place on the home screen and ends up in the game library. The app keeps it
+free (Pin the whole row leaves the first place) and offers to free the first place of a row pinned in full by
+1.4.1 or older; `save_order` refuses such a row with 400.
+
## Updates
The app reads `https://github.com/bps2414/lineup/releases/latest/download/update.json` (written by
diff --git a/docs/pt-BR/ARQUITETURA.md b/docs/pt-BR/ARQUITETURA.md
index 9589f01..2174ed1 100644
--- a/docs/pt-BR/ARQUITETURA.md
+++ b/docs/pt-BR/ARQUITETURA.md
@@ -81,7 +81,7 @@ formulário simples.
Os erros vêm como `{"error": ""}`:
-- 400: pedido malformado.
+- 400: pedido malformado, ou todos os lugares da tela inicial fixos (veja Jogos fixos).
- 403: falta o cabeçalho `X-Lineup`.
- 404: o perfil ou o ícone não existe.
- 409: a fileira mudou desde a leitura.
@@ -104,6 +104,11 @@ Ao salvar, o lineupd apaga os triggers do perfil, grava a ordem e os fixos e rec
tudo numa transação só. Quando nenhum perfil tem mais jogos fixos, as tabelas também saem. Reconstruir o banco de
dados pode apagar os triggers; aí o `/row` responde `"missing"` e o app oferece fixar de novo.
+Um dos primeiros `HOME_LIMIT` (12) lugares sempre fica sem fixar: com todos fixos, o último jogo aberto, como a
+entrada do jailbreak, fica sem lugar na tela inicial e vai parar na biblioteca de jogos. O app mantém esse lugar
+livre ("Fixar toda a fileira" deixa o primeiro) e oferece liberar o primeiro lugar de uma fileira toda fixa pela
+1.4.1 ou anterior; o `save_order` recusa essa fileira com 400.
+
## Atualização
O app lê `https://github.com/bps2414/lineup/releases/latest/download/update.json` (gerado pelo
diff --git a/docs/pt-BR/NOVIDADES.md b/docs/pt-BR/NOVIDADES.md
index d284842..49a1645 100644
--- a/docs/pt-BR/NOVIDADES.md
+++ b/docs/pt-BR/NOVIDADES.md
@@ -2,6 +2,13 @@
As notas que o app mostra ao atualizar. Cada versão aqui acompanha a mesma seção do `CHANGELOG.md`.
+## 1.4.2
+
+- Um lugar da tela inicial sempre fica sem fixar. Com todos fixos, o último jogo aberto (a entrada do
+ jailbreak, por exemplo) ficava sem lugar e ia parar na biblioteca de jogos. "Fixar toda a fileira" agora deixa
+ o primeiro lugar livre, e o Lineup não salva uma tela inicial toda fixa.
+- Uma fileira toda fixa por uma versão anterior abre oferecendo liberar o primeiro lugar.
+
## 1.4.1
- O jogo levantado fica por cima: ao levar ele para a esquerda, o jogo que ele ultrapassa não passa mais por
diff --git a/ps5-react/dependencies.lock.json b/ps5-react/dependencies.lock.json
index 06fd4bd..c5191a9 100644
--- a/ps5-react/dependencies.lock.json
+++ b/ps5-react/dependencies.lock.json
@@ -45,7 +45,7 @@
]
},
"platform": {
- "url": "https://github.com/blackbearreloaded/ps5-homebrew-ui.git",
+ "url": "https://github.com/Phononenom/ps5-homebrew-ui.git",
"revision": "4bd942579dd981b3df9c740438489ca6a614ddc0",
"patches": [
"patches/platform-heap-size.patch",
@@ -85,7 +85,7 @@
"sha256": "ce2669c51272b70bc0aafe9a4e6223e5c7720edaebb40201db191634aa503f5e"
},
"filesystemHelperClient": {
- "url": "https://github.com/blackbearreloaded/ProsperoStore.git",
+ "url": "https://github.com/JVK3R/ProsperoStore.git",
"revision": "335d8c43f916020205f445b76b66106ee50d2eeb"
},
"filesystemHelper": {
diff --git a/ps5-react/tools/build_ps5.py b/ps5-react/tools/build_ps5.py
index eaa970d..8e798ca 100644
--- a/ps5-react/tools/build_ps5.py
+++ b/ps5-react/tools/build_ps5.py
@@ -65,7 +65,8 @@ def main():
PS5_CLANG=str(llvm / "clang"), PS5_PAYLOAD_SDK=str(sdk), USE_CCACHE="0")
archive = args.opengl_archive or DEPS / "ps5-opengl-sdk-1.0.0.tar.gz"
- fetch("https://github.com/blackbearreloaded/ps5-opengl/releases/download/v1.0.0/ps5-opengl-sdk-1.0.0.tar.gz",
+ # A copy of the upstream release (blackbearreloaded left GitHub); GL_HASH checks it is the same file.
+ fetch("https://github.com/MyLuxy/overdrive-deps/releases/download/ps5-opengl-v1.0.0/ps5-opengl-sdk-1.0.0.tar.gz",
archive, GL_HASH)
gl = DEPS / "ps5-opengl-sdk-1.0.0/sdk"
if not gl.exists():