Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## 1.3.0

- Pinned games: □ pins the focused game at its place when you save. Opening other games no longer
pushes it out; the rest of the row still follows the system's rule (the last game opened takes the
first free place). A pin badge marks the game, a block marks it on the ruler, and the position reads
"pinned at 3rd".
- Options: Pin the whole row, Unpin all, and Remove pinned games from every profile.
- If the system rebuilds its database and the pins come loose, Lineup offers to pin the games again.
- The change count includes pins.

## 1.2.0

- Profile pictures: in the header, under Options, and next to each name in Switch profile. A profile
Expand Down
13 changes: 9 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ out on the TV. Its local service then writes the new order where the home screen
Options menu.
- **Home screen edge.** The home screen shows only the first 12 games. A divider in the row and a mark
on the ruler show where that edge falls, and games past it read "not on the home screen".
- **Sort A to Z**, discard changes, and a count of the games that moved.
- **Pinned games.** □ pins the focused game where it stands when you save. Opening other games no longer
pushes it out of its place; the rest of the row still follows the system's rule. Options can pin the
whole row, unpin all, or remove pinned games from every profile. Works from 1.3.0 on.
- **Sort A to Z**, discard changes, and a count of the games that changed.
- **Applies on exit.** Saving closes the app, and the home screen comes back with the new order. No
restart needed.
- **Updates in the app.** Lineup checks for a new release once a day (Options → Updates turns that off),
Expand Down Expand Up @@ -56,12 +59,13 @@ Use one or the other. Both register title ID `PPSA99077`.
| --- | --- | --- |
| ✕ | Lift the focused game | Drop it here |
| D-pad | Move focus | Carry the game |
| □ | Pin or unpin the focused game | Pin or unpin it |
| △ | Save | |
| Options | Options menu | |
| ◯ | Quit (asks to save unsaved changes) | Put the game back |

Lineup changes only the order of the home row. Opening a game afterwards still moves it to the front,
as it always does.
Opening a game afterwards still moves it to the front, as it always does, unless a pinned game holds
that place.

## Safety

Expand All @@ -77,7 +81,8 @@ as it always does.
The app is written in React and runs natively on the console through
[PS5 React](#ps5-react). A small C service, `lineupd`, ships inside the app folder. The app starts it
through the ELF loader, and it answers on `127.0.0.1:9077`. It reads the per-profile tables in
`/system_data/priv/mms/app.db` and writes the new order back.
`/system_data/priv/mms/app.db` and writes the new order back. Pinned games are kept in place by SQLite
triggers it adds to that database; `POST /pins/off` and `POST /pins/uninstall` take them out again.

See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the components, the service's HTTP API and what
was learned about how the home screen stores its row.
Expand Down
13 changes: 9 additions & 4 deletions README.pt-BR.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ ordem na TV. Depois, o serviço local dele grava a nova ordem onde a tela inicia
- **Limite da tela inicial.** A tela inicial mostra só os 12 primeiros jogos. Um traço na fileira e uma
marca na régua mostram onde fica esse limite, e os jogos depois dele aparecem como "fora da tela
inicial".
- **Ordenar de A a Z**, descartar mudanças e contagem de jogos que mudaram de lugar.
- **Jogos fixos.** □ fixa o jogo em foco na posição em que ele estiver ao salvar. Abrir outros jogos não
tira ele do lugar; o resto da fileira segue a regra do sistema. Em Opções dá para fixar a fileira
toda, desafixar todos ou remover os jogos fixos de todos os perfis. Vale a partir da 1.3.0.
- **Ordenar de A a Z**, descartar mudanças e contagem de jogos que mudaram.
- **Aplica ao sair.** Salvar fecha o app, e a tela inicial volta com a nova ordem. Não precisa
reiniciar.
- **Atualização no app.** O Lineup procura uma versão nova uma vez por dia (Opções → Atualizações
Expand Down Expand Up @@ -57,12 +60,13 @@ Use um ou outro. Os dois registram o title ID `PPSA99077`.
| --- | --- | --- |
| ✕ | Levanta o jogo em foco | Solta aqui |
| Direcional | Move o foco | Leva o jogo |
| □ | Fixa ou desafixa o jogo em foco | Fixa ou desafixa o jogo |
| △ | Salva | |
| Options | Menu Opções | |
| ◯ | Sai (pergunta se quer salvar as mudanças) | Devolve o jogo |

O Lineup muda só a ordem da fileira. Abrir um jogo depois continua trazendo ele para a frente, como
sempre.
Abrir um jogo depois continua trazendo ele para a frente, como sempre, a não ser que um jogo fixo ocupe
esse lugar.

## Segurança

Expand All @@ -78,7 +82,8 @@ sempre.
O app é escrito em React e roda nativo no console pelo [PS5 React](#ps5-react). Um pequeno serviço em
C, o `lineupd`, vai dentro da pasta do app. O app inicia o serviço pelo ELF loader, e ele responde em
`127.0.0.1:9077`. Ele lê as tabelas de cada perfil em `/system_data/priv/mms/app.db` e grava a nova
ordem.
ordem. Os jogos fixos ficam no lugar graças a triggers do SQLite que ele cria nesse banco;
`POST /pins/off` e `POST /pins/uninstall` tiram tudo de novo.

Veja [docs/pt-BR/ARQUITETURA.md](docs/pt-BR/ARQUITETURA.md) para os componentes, a API HTTP do serviço
e o que se descobriu sobre como a tela inicial guarda a fileira.
Expand Down
27 changes: 21 additions & 6 deletions app/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import {Archives, Downloads, FileSystem, Http, Payloads} from '@ps5-react/core';
import {T} from './i18n.js';

export const VERSION = '1.2.0';
export const VERSION = '1.3.0';
const BASE = 'http://127.0.0.1:9077';

/** An error the app shows as is: a short title and what to do about it. */
Expand Down Expand Up @@ -97,16 +97,21 @@ async function read(path, what) {
}

export const profiles = async () => (await read('/profiles', T.service.profiles)).profiles ?? [];
export const row = async user => (await read(`/row?user=${user}`, T.service.row)).games ?? [];
/** {games, pins}: each game's `pinned` is its slot or null; `pins` is 'on', 'missing' (triggers lost) or 'off'. */
export async function row(user) {
const data = await read(`/row?user=${user}`, T.service.row);
return {games: data.games ?? [], pins: data.pins ?? 'off'};
}
export const iconUrl = game => `${BASE}/icon/${game.titleId}?m=${game.icon}`;
export const avatarUrl = profile => `${BASE}/avatar/${profile.userId}?m=${profile.avatar}`;

/**
* Saves `order` (titleIds) for `user`, sending each game's index as it was read. Resolves to
* {ok: true} or {ok: false, stale, message}; `stale` means the row changed since it was read.
* Saves `order` (titleIds) for `user`, sending each game's index as it was read and pinning the games in `pins`
* where they stand. Resolves to {ok: true} or {ok: false, stale, message}; `stale` means the row changed since it
* was read.
*/
export async function save(user, order, games) {
const body = `LIN1\n${order.map(id => `${id} ${games[id].index}\n`).join('')}`;
export async function save(user, order, games, pins) {
const body = `LIN1\n${order.map(id => `${id} ${games[id].index}${pins[id] ? ' pin' : ''}\n`).join('')}`;
let reply;
try {
reply = await call(`/row?user=${user}`, {method: 'POST', body});
Expand All @@ -119,6 +124,16 @@ export async function save(user, order, games) {
return {ok: false, stale: false, message: T.service.saveStatus(reply.status) + detail};
}

/** Removes every profile's pinned games and all Lineup added to the home screen database. Resolves to an error message or ''. */
export async function unpinAll() {
try {
const reply = await call('/pins/uninstall', {method: 'POST', timeoutMs: 15000});
return reply.ok ? '' : T.pins.removeFailed(reply.data.error ?? T.service.status(reply.status));
} catch {
return T.pins.removeFailed(T.pins.noAnswer);
}
}

// ---- Updates ----

const UPDATE_DIR = '/data/lineup/update';
Expand Down
2 changes: 1 addition & 1 deletion app/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"titleId": "PPSA99077",
"contentId": "UP9000-PPSA99077_00-LINEUP0000000000",
"name": "Lineup",
"version": "01.002.000",
"version": "01.003.000",
"render": {
"width": 1920,
"height": 1080
Expand Down
38 changes: 34 additions & 4 deletions app/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,22 @@ const en = {
of: n => `of ${n}`,
hints: {
lift: 'Lift', save: 'Save', options: 'Options', quit: 'Quit', move: 'Move', drop: 'Drop',
cancel: 'Cancel', choose: 'Choose', back: 'Back', close: 'Close',
cancel: 'Cancel', choose: 'Choose', back: 'Back', close: 'Close', pin: 'Pin', unpin: 'Unpin',
},
pins: {
pinnedAt: n => `pinned at ${englishOrdinal(n)}`,
pinAll: 'Pin the whole row', unpinAll: 'Unpin all', removeAll: 'Remove pinned games from every profile',
removeTitle: 'Remove pinned games from every profile?',
removeBody: "Every profile goes back to the system's rule: the last game opened comes first. "
+ 'The current order stays as it is.',
removeConfirm: 'Remove', cancel: 'Cancel',
removed: 'Pinned games removed from every profile.',
removeFailed: detail => `Could not remove the pinned games (${detail}).`,
noAnswer: 'the service did not answer',
missingTitle: 'The pinned games came loose',
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',
},
saved: ['Order saved', 'Closing the app. The home screen comes back with the new order.'],
closeNow: 'Close now',
Expand Down Expand Up @@ -86,7 +101,7 @@ const en = {
profilesSubtitle: 'Each profile has its own row',
exitDialog: {
title: 'Save the new order before leaving?',
body: n => (n === 1 ? 'One game moved.' : `${n} games moved.`),
body: n => (n === 1 ? 'One game changed.' : `${n} games changed.`),
save: 'Save and quit', discard: 'Quit without saving', back: 'Back',
},
switchDialog: {
Expand Down Expand Up @@ -124,7 +139,22 @@ const pt = {
of: n => `de ${n}`,
hints: {
lift: 'Levantar', save: 'Salvar', options: 'Opções', quit: 'Sair', move: 'Mover', drop: 'Soltar',
cancel: 'Cancelar', choose: 'Escolher', back: 'Voltar', close: 'Fechar',
cancel: 'Cancelar', choose: 'Escolher', back: 'Voltar', close: 'Fechar', pin: 'Fixar', unpin: 'Desafixar',
},
pins: {
pinnedAt: n => `fixo no ${n}º`,
pinAll: 'Fixar toda a fileira', unpinAll: 'Desafixar todos', removeAll: 'Remover jogos fixos de todos os perfis',
removeTitle: 'Remover os jogos fixos de todos os perfis?',
removeBody: 'Todos os perfis voltam à regra do sistema: o último jogo aberto vai para a frente. '
+ 'A ordem atual continua como está.',
removeConfirm: 'Remover', cancel: 'Cancelar',
removed: 'Jogos fixos removidos de todos os perfis.',
removeFailed: detail => `Não consegui remover os jogos fixos (${detail}).`,
noAnswer: 'o serviço não respondeu',
missingTitle: 'Os jogos fixos se soltaram',
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',
},
saved: ['Ordem salva', 'Fechando o app. A tela inicial já volta com a nova ordem.'],
closeNow: 'Fechar agora',
Expand Down Expand Up @@ -173,7 +203,7 @@ const pt = {
profilesSubtitle: 'Cada perfil tem a sua fileira',
exitDialog: {
title: 'Salvar a nova ordem antes de sair?',
body: n => (n === 1 ? 'Um jogo mudou de lugar.' : `${n} jogos mudaram de lugar.`),
body: n => (n === 1 ? 'Um jogo mudou.' : `${n} jogos mudaram.`),
save: 'Salvar e sair', discard: 'Sair sem salvar', back: 'Voltar',
},
switchDialog: {
Expand Down
Loading
Loading