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
7 changes: 7 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ jobs:
with:
path: ps5-react/.deps
key: deps-${{ hashFiles('ps5-react/dependencies.lock.json') }}
- name: Stamp the build
# The app replaces a lineupd of another build; test builds share a version, so the commit tells them apart.
run: |
B="$(git rev-parse --short HEAD)"
sed -i "s/^export const BUILD = 'local';/export const BUILD = '$B';/" app/api.js
sed -i "s/^#define LINEUPD_BUILD \"local\"/#define LINEUPD_BUILD \"$B\"/" app/payloads/lineupd.c
grep -q "BUILD = '$B'" app/api.js && grep -q "LINEUPD_BUILD \"$B\"" app/payloads/lineupd.c
- run: scripts/test.sh
- run: scripts/build.sh
- name: Package
Expand Down
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
# Changelog

## Unreleased

- The Media Gallery moves between the Games and Media tabs (focus it on the row, Options, Move), and stays there
after a restart or rest mode. The PS Store only hides. Game Library and App Library never move or hide.
- The Media Gallery is on its tab's row: it can be pinned and moved from the row like an app.
- Welcome can be shown, on every profile, and hidden again (Options, Hidden apps).
- Every other system tile that is showing can be hidden, and stays hidden.
- Moving an app now moves its concept too, which 1.8.0 left on the old tab. Apps moved before are fixed when
Lineup starts.
- More options has Remove TabShifter leftovers: TabShifter's "TS" tile goes from both databases.
- Backups: the first time this version runs, it keeps both databases for good in /data/lineup/backups
(original-app.db, original-appinfo.db), before its own checks write. After an upgrade they already hold what
earlier versions of Lineup changed, so they are not a copy from before Lineup. Two backups in the same second no
longer overwrite each other.
- Triggers and writes are made only for the columns the firmware has.
- The build downloads the OpenGL SDK from a second mirror when the first is gone.
- When Lineup opens, it replaces a running lineupd whose version or build differs: /version answers a build too,
the commit for a CI build. Builds made locally all answer local, so two of one version still look alike.

## 1.8.0

- A moved app stays on its new tab when it is installed again. Game Compressor's payload installs its shortcut
Expand Down
30 changes: 25 additions & 5 deletions app/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import {Archives, Downloads, FileSystem, Http, Payloads} from '@ps5-react/core';
import {T} from './i18n.js';

export const VERSION = '1.8.0';
// "local", or the commit of a CI build, as LINEUPD_BUILD in lineupd.c: a service of another build is replaced.
export const BUILD = 'local';
const SERVICE = `${VERSION}+${BUILD}`;
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 @@ -55,7 +58,7 @@ async function call(path, {method = 'GET', body, timeoutMs} = {}) {
async function version() {
try {
const {ok, data} = await call('/version', {timeoutMs: 1500});
return ok ? data.version ?? '' : '';
return ok ? `${data.version ?? ''}+${data.build ?? ''}` : '';
} catch {
return null;
}
Expand All @@ -64,7 +67,7 @@ async function version() {
/** Makes sure this app's version of the service answers, starting (or replacing) it when needed. */
export async function ensureService(onStep) {
let running = await version();
if (running === VERSION) return;
if (running === SERVICE) return;
if (running !== null) {
onStep(T.service.replacing);
await call('/quit', {method: 'POST', timeoutMs: 1500}).catch(() => {});
Expand All @@ -77,7 +80,7 @@ export async function ensureService(onStep) {
for (let waited = 0; waited < 5000; waited += 150) {
await sleep(150);
running = await version();
if (running === VERSION) return;
if (running === SERVICE) return;
}
throw problem(T.service.noAnswer);
}
Expand Down Expand Up @@ -135,8 +138,25 @@ export async function unpinAll() {
}
}

/** What Lineup hid on `user`, as [{titleId, name}]. */
export const hidden = async user => (await read(`/hidden?user=${user}`, T.service.hidden)).hidden ?? [];
/**
* What Lineup hid on `user`, as hidden: [{titleId, name}], and the tab of each system tile that can move, as
* system: [{titleId, tab}] where tab is 'games', 'media' or 'none'.
*/
export async function hidden(user) {
const data = await read(`/hidden?user=${user}`, T.service.hidden);
return {hidden: data.hidden ?? [], system: data.system ?? []};
}

/** Removes TabShifter's tile from the home screen databases. Resolves to {removed} or {error}. */
export async function removeTabShifter() {
try {
const reply = await call('/tabshifter/remove', {method: 'POST', timeoutMs: 15000});
return reply.ok ? {removed: reply.data.removed ?? 0}
: {error: T.cleanup.failed(reply.data.error ?? T.service.status(reply.status))};
} catch {
return {error: T.cleanup.failed(T.pins.noAnswer)};
}
}

/** Hides, shows or moves an app: `action` is 'hide', 'show', 'games' or 'media'. Resolves to an error message or ''. */
export async function changeApp(user, title, action) {
Expand Down
24 changes: 24 additions & 0 deletions app/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ const en = {
moveTo: tab => `Move to ${tab}`, hide: 'Hide from the home screen',
store: 'PS Store', storeHidden: 'Hidden', storeShown: 'Showing',
gallery: 'Media Gallery', galleryHidden: 'Hidden', galleryShown: 'Showing',
welcome: 'Welcome, on every profile', welcomeShown: 'Showing', welcomeHidden: 'Hidden',
systemLabel: 'Other system tiles', hideTile: 'Hide',
show: 'Bring back',
none: 'Nothing else is hidden. To hide an app, focus it on the row and open Options.',
hideTitle: name => `Hide ${name}?`,
Expand All @@ -86,6 +88,16 @@ const en = {
},
failed: detail => `Nothing changed (${detail}).`,
},
cleanup: {
item: 'Remove TabShifter leftovers',
title: "Remove TabShifter's tile?",
body: 'Lineup backs up both home screen databases first, then takes TabShifter\'s "TS" tile out of them and '
+ 'deletes its folders. Do this only after deleting TabShifter.',
confirm: 'Remove', cancel: 'Cancel',
done: removed => (removed ? "TabShifter's tile was removed. The home screen updates when Lineup closes."
: 'No TabShifter tile was found.'),
failed: detail => `Nothing was removed (${detail}).`,
},
report: {
save: 'Save a bug report',
title: 'Report saved',
Expand Down Expand Up @@ -222,6 +234,8 @@ const pt = {
moveTo: tab => `Mover para ${tab}`, hide: 'Esconder da tela inicial',
store: 'PS Store', storeHidden: 'Escondida', storeShown: 'Aparecendo',
gallery: 'Galeria de mídia', galleryHidden: 'Escondida', galleryShown: 'Aparecendo',
welcome: 'Boas-vindas, em todos os perfis', welcomeShown: 'Aparecendo', welcomeHidden: 'Escondida',
systemLabel: 'Outros blocos do sistema', hideTile: 'Esconder',
show: 'Trazer de volta',
none: 'Nada mais está escondido. Para esconder um app, deixe ele em foco na fileira e abra Opções.',
hideTitle: name => `Esconder ${name}?`,
Expand All @@ -237,6 +251,16 @@ const pt = {
},
failed: detail => `Nada mudou (${detail}).`,
},
cleanup: {
item: 'Remover restos do TabShifter',
title: 'Remover o bloco do TabShifter?',
body: 'O Lineup faz backup dos dois bancos da tela inicial antes, tira deles o bloco "TS" do TabShifter e apaga '
+ 'as pastas dele. Faça isso só depois de apagar o TabShifter.',
confirm: 'Remover', cancel: 'Cancelar',
done: removed => (removed ? 'O bloco do TabShifter foi removido. A tela inicial atualiza quando o Lineup fechar.'
: 'Nenhum bloco do TabShifter foi encontrado.'),
failed: detail => `Nada foi removido (${detail}).`,
},
report: {
save: 'Salvar relatório de bug',
title: 'Relatório salvo',
Expand Down
44 changes: 41 additions & 3 deletions app/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ const PICK_HINTS = [['cross', H.choose], ['circle', H.back]];

const STORE = 'NPXS40047';
const GALLERY = 'NPXS40075';
const WELCOME = 'NPXS40063';

// The side sheet's pages: title and subtitle. The menu and Updates work out their own subtitle.
const SHEETS = {menu: [T.menu.title], profiles: [T.profilesTitle, T.profilesSubtitle], updates: [T.updates.title],
Expand Down Expand Up @@ -542,7 +543,7 @@ function App() {
light(false);
patch({lifted: false, layer: 'menu'});
// For the count beside Hidden apps; the page reads it again.
api.hidden(ref.current.user).then(hidden => patch({hidden}), () => {});
api.hidden(ref.current.user).then(patch, () => {});
return true;
}
if (action === 'l1' || action === 'r1') {
Expand Down Expand Up @@ -600,7 +601,7 @@ function App() {
const {user} = ref.current;
patch({layer: 'busy'});
try {
patch({hidden: await api.hidden(user), layer: 'hidden'});
patch({...await api.hidden(user), layer: 'hidden'});
} catch (error) {
close();
toast(error.detail ?? error.message);
Expand All @@ -618,14 +619,31 @@ function App() {
patch({layer: 'busy'});
const error = await api.changeApp(p.user, title, action);
try {
if (keep.layer) patch({hidden: await api.hidden(p.user)});
if (keep.layer) patch(await api.hidden(p.user));
await loadRow(p.user, p.profiles, p.tab, keep).catch(() => loadRow(p.user, p.profiles, 'games', keep));
} catch (problem) {
patch({phase: 'error', error: problem});
return;
}
toast(error || T.apps.done[action](name));
};
// The tile may be on the row: after a removal the tab reloads, as after changeApp.
const removeTabShifter = async () => {
const p = ref.current;
patch({layer: 'busy'});
const {removed, error} = await api.removeTabShifter();
if (!error && removed) {
const keep = {focus: p.focus};
try {
await loadRow(p.user, p.profiles, p.tab, keep).catch(() => loadRow(p.user, p.profiles, 'games', keep));
} catch (problem) {
patch({phase: 'error', error: problem});
return;
}
}
close();
toast(error || T.cleanup.done(removed));
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
};
const askApp = layer => {
if (changed(ref.current)) toast(T.tabs.saveFirst);
else patch({layer});
Expand Down Expand Up @@ -653,6 +671,9 @@ function App() {
const galleryHidden = hiddenApps.some(app => app.titleId === GALLERY);
const otherHidden = hiddenApps.filter(app => app.titleId !== STORE && app.titleId !== GALLERY);
const otherTab = s.tab === 'games' ? 'media' : 'games';
// Welcome shows on every profile; the other system tiles showing can hide on this one.
const welcome = (s.system ?? []).find(tile => tile.titleId === WELCOME);
const otherSystem = (s.system ?? []).filter(tile => tile.visible && ![STORE, GALLERY, WELCOME].includes(tile.titleId));

if (s.phase === 'loading') {
return (
Expand Down Expand Up @@ -724,6 +745,16 @@ function App() {
onPress={() => changeApp(STORE, storeHidden ? 'show' : 'hide', T.apps.store)} />,
<SheetItem key="gallery" label={T.apps.gallery} detail={galleryHidden ? T.apps.galleryHidden : T.apps.galleryShown}
onPress={() => changeApp(GALLERY, galleryHidden ? 'show' : 'hide', T.apps.gallery)} />,
welcome && (welcome.shown || welcome.tab !== 'none') ? (
<SheetItem key="welcome" label={T.apps.welcome}
detail={welcome.shown ? T.apps.welcomeShown : T.apps.welcomeHidden}
onPress={() => changeApp(WELCOME, welcome.shown ? 'hide' : 'show', T.apps.welcome)} />
Comment thread
SuperLeiyan marked this conversation as resolved.
Resolved
) : null,
otherSystem.length ? <SheetLabel key="system">{T.apps.systemLabel}</SheetLabel> : null,
...otherSystem.map(tile => (
<SheetItem key={`sys-${tile.titleId}`} label={tile.name} detail={T.apps.hideTile}
onPress={() => changeApp(tile.titleId, 'hide', tile.name)} />
)),
<SheetRule key="rule" />,
...otherHidden.map(app => (
<SheetItem key={app.titleId} label={app.name} detail={T.apps.show}
Expand All @@ -738,6 +769,7 @@ function App() {
<SheetItem key="updates" label={T.menu.updates} onPress={() => patch({layer: 'updates'})} />,
<SheetItem key="report" label={T.report.save} onPress={saveReport} />,
<SheetItem key="remove-pins" label={T.pins.removeAll} onPress={() => patch({layer: 'removePins'})} />,
<SheetItem key="tabshifter" label={T.cleanup.item} onPress={() => askApp('removeTabShifter')} />,
<SheetItem key="star" label={T.star.item} onPress={() => patch({layer: 'star'})} />,
] : s.layer === 'star' ? [
// The repo's address as a code the phone's camera reads: the audience is at the TV, with no mouse.
Expand Down Expand Up @@ -816,6 +848,12 @@ function App() {
<Button label={T.pins.cancel} onPress={() => patch({layer: 'more'})} />
</Dialog>
) : null}
{s.layer === 'removeTabShifter' ? (
<Dialog title={T.cleanup.title} body={T.cleanup.body} onBack={() => patch({layer: 'more'})}>
<Button label={T.cleanup.confirm} onPress={removeTabShifter} />
<Button label={T.cleanup.cancel} onPress={() => patch({layer: 'more'})} />
</Dialog>
) : null}
{s.layer === 'hideApp' ? (
<Dialog title={T.apps.hideTitle(focused?.name)} body={T.apps.hideBody} onBack={() => patch({layer: 'menu'})}>
<Button label={T.apps.confirmHide} onPress={() => changeApp(focused?.titleId, 'hide', focused?.name)} />
Expand Down
Loading