diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2b371ef..4184f80 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 52db2f5..0e3c463 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/app/api.js b/app/api.js index 613aeee..981f19b 100644 --- a/app/api.js +++ b/app/api.js @@ -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. */ @@ -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; } @@ -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(() => {}); @@ -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); } @@ -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) { diff --git a/app/i18n.js b/app/i18n.js index 2b1e314..5b319d9 100644 --- a/app/i18n.js +++ b/app/i18n.js @@ -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}?`, @@ -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', @@ -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}?`, @@ -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', diff --git a/app/index.jsx b/app/index.jsx index 2f635af..37e5eda 100644 --- a/app/index.jsx +++ b/app/index.jsx @@ -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], @@ -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') { @@ -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); @@ -618,7 +619,7 @@ 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}); @@ -626,6 +627,23 @@ function App() { } 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)); + }; const askApp = layer => { if (changed(ref.current)) toast(T.tabs.saveFirst); else patch({layer}); @@ -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 ( @@ -724,6 +745,16 @@ function App() { onPress={() => changeApp(STORE, storeHidden ? 'show' : 'hide', T.apps.store)} />, changeApp(GALLERY, galleryHidden ? 'show' : 'hide', T.apps.gallery)} />, + welcome && (welcome.shown || welcome.tab !== 'none') ? ( + changeApp(WELCOME, welcome.shown ? 'hide' : 'show', T.apps.welcome)} /> + ) : null, + otherSystem.length ? {T.apps.systemLabel} : null, + ...otherSystem.map(tile => ( + changeApp(tile.titleId, 'hide', tile.name)} /> + )), , ...otherHidden.map(app => ( patch({layer: 'updates'})} />, , patch({layer: 'removePins'})} />, + askApp('removeTabShifter')} />, 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. @@ -816,6 +848,12 @@ function App() {