Skip to content

Commit 1e8d5a8

Browse files
authored
Merge pull request #24 from a23bc/fix/engine-update-prompt
fix: ipatool update prompt, engine settings UX, account list polish (v0.1.1)
2 parents 67d4d87 + a499159 commit 1e8d5a8

19 files changed

Lines changed: 295 additions & 93 deletions

File tree

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "ipatool-gui",
33
"productName": "IPATool GUI",
4-
"version": "0.1.0",
4+
"version": "0.1.1",
55
"engines": {
66
"node": ">=20.19"
77
},

‎src/main/accounts.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ export class AccountRegistry extends EventEmitter {
196196
async snapshot(): Promise<AccountsSnapshot> {
197197
const bridge = await slotBridge()
198198
const activeId = this.activeId
199-
const profiles = sortAccounts(this.list(), activeId)
199+
const profiles = sortAccounts(this.list())
200200
const shared = profiles.some((profile) => this.effectiveStore(profile) === 'os')
201201
return {
202202
// Unused slots are not listed: the switcher must not offer a nameless

‎src/main/engine.ts‎

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ import { gunzip } from 'node:zlib'
2626
import { promisify } from 'node:util'
2727
import { mkdir, rename, rm, writeFile, chmod } from 'node:fs/promises'
2828
import { app } from 'electron'
29-
import type { EngineDownloadProgress, EngineErrorCode, EngineRelease, EngineStatus } from '../shared/types'
29+
import type { EngineDownloadProgress, EngineErrorCode, EngineRelease, EngineStatus, EngineUpdateCheck } from '../shared/types'
30+
import { compareSemver } from '../shared/semver'
3031
import { parseSha256Sum, parseVersion } from '../shared/ipatool/parse'
3132
import { findTarFile, parseTar } from '../shared/tar'
3233
import {
@@ -287,6 +288,37 @@ export class EngineManager extends EventEmitter {
287288
}
288289
}
289290

291+
/**
292+
* Compares the installed ipatool with the newest published release.
293+
*
294+
* `checkAppUpdate` is about *this* GUI; engine updates are separate and only
295+
* look at majd/ipatool tags. Prereleases never count as "newer".
296+
*/
297+
async checkUpdate(): Promise<EngineUpdateCheck> {
298+
const current = (this.current.version ?? '').replace(/^v/, '')
299+
try {
300+
const list = await this.releases(15)
301+
const stable = list
302+
.filter((r) => !r.prerelease && r.hasAssetForThisPlatform)
303+
.map((r) => r.version.replace(/^v/, ''))
304+
const latest = stable.length > 0 ? (stable[0] ?? '') : ''
305+
// releases() is newest-first; pick the max in case GitHub order shifts.
306+
let newest = latest
307+
for (const v of stable) {
308+
if (compareSemver(v, newest) > 0) newest = v
309+
}
310+
const hasUpdate = current !== '' && newest !== '' && compareSemver(newest, current) > 0
311+
return { current, latest: newest === '' ? null : newest, hasUpdate, error: null }
312+
} catch (error) {
313+
return {
314+
current,
315+
latest: null,
316+
hasUpdate: false,
317+
error: error instanceof Error ? error.message : String(error)
318+
}
319+
}
320+
}
321+
290322
/**
291323
* Downloads, verifies and installs ipatool. Emits progress through the
292324
* `status` event so the UI can show a real percentage.

‎src/main/index.ts‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,21 @@ async function postStartup(): Promise<void> {
182182
/* surfaced through the engine status event */
183183
}
184184

185+
// Tell the UI when a newer ipatool is out - the version picker can already
186+
// see it, but nothing used to say "you are behind".
187+
if (engineManager.state.state === 'ready') {
188+
void engineManager
189+
.checkUpdate()
190+
.then((result) => {
191+
if (result.hasUpdate) {
192+
for (const win of BrowserWindow.getAllWindows()) {
193+
if (!win.isDestroyed()) win.webContents.send('engine:update', result)
194+
}
195+
}
196+
})
197+
.catch(() => undefined)
198+
}
199+
185200
// Only worth asking once the engine is actually usable. The probe confirms the
186201
// active account's session, learns its identity (or adopts a session the
187202
// terminal CLI created) and publishes the whole account list.

‎src/main/ipc.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ export function registerIpc(): void {
217217
ipcMain.handle(IPC.EngineReleases, () =>
218218
wrap(() => engineManager.releases(30))
219219
)
220+
ipcMain.handle(IPC.EngineCheckUpdate, () => engineManager.checkUpdate())
220221
ipcMain.handle(IPC.AppCheckUpdate, () => checkAppUpdate())
221222

222223
/* ---------------------------------------------------------------- *

‎src/main/session.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,9 @@ export async function switchTo(accountId: string): Promise<{ snapshot: AccountsS
118118
*/
119119
export async function refreshAll(onlyUnknown = false): Promise<AccountsSnapshot> {
120120
for (const profile of accounts.list()) {
121+
// Never-used slots are hidden from the UI and have nothing to probe -
122+
// running `auth info` on them only produces keyring errors in the log.
123+
if (!accounts.hasRecordedSession(profile) && profile.remark.trim() === '') continue
121124
if (onlyUnknown && accounts.hasRecordedSession(profile)) continue
122125
try {
123126
await verifySession(profile.id)

‎src/preload/index.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import type { QueueAction, SearchRequest, PurchasesRequest, EnqueueOptions } fro
5353
*/
5454
const EVENT_CHANNELS: ReadonlySet<string> = new Set<EventChannel>([
5555
'engine:status',
56+
'engine:update',
5657
'engine:progress',
5758
'accounts:changed',
5859
'settings:changed',
@@ -99,6 +100,7 @@ const api: RendererApi = {
99100
installEngine: (version?: string, force?: boolean): Promise<EngineStatus> =>
100101
ipcRenderer.invoke(IPC.EngineInstall, version, force),
101102
listEngineReleases: (): Promise<Operation<EngineRelease[]>> => ipcRenderer.invoke(IPC.EngineReleases),
103+
checkEngineUpdate: () => ipcRenderer.invoke(IPC.EngineCheckUpdate),
102104
uninstallEngine: (): Promise<EngineStatus> => ipcRenderer.invoke(IPC.EngineUninstall),
103105
checkAppUpdate: (): Promise<UpdateCheckResult> => ipcRenderer.invoke(IPC.AppCheckUpdate),
104106

‎src/renderer/src/components/AccountManager.tsx‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ export function AccountManager(): ReactNode {
172172
const updateAccount = useAppStore((state) => state.updateAccount)
173173
const removeAccount = useAppStore((state) => state.removeAccount)
174174
const getAccounts = useAppStore((state) => state.getAccounts)
175+
const refreshAccounts = useAppStore((state) => state.refreshAccounts)
175176

176177
const open = useUiStore((state) => state.accountsOpen)
177178
const setOpen = useUiStore((state) => state.setAccountsOpen)
@@ -249,8 +250,18 @@ export function AccountManager(): ReactNode {
249250
width={560}
250251
footer={
251252
<>
252-
<button type="button" className="btn" onClick={() => void getAccounts()}>
253-
<Icon name="refresh" size={13} />
253+
<button
254+
type="button"
255+
className="btn"
256+
disabled={busy}
257+
onClick={() => {
258+
void run(async () => {
259+
await refreshAccounts()
260+
toast({ kind: 'success', message: t('accounts.verified') })
261+
})
262+
}}
263+
>
264+
{busy ? <Spinner size={13} /> : <Icon name="refresh" size={13} />}
254265
{t('accounts.refreshAll')}
255266
</button>
256267
<button type="button" className="btn btn-primary" onClick={onAdd} disabled={busy}>

‎src/renderer/src/components/Header.tsx‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,17 @@ function useChromePadding(): { left: number; right: number } {
1919

2020
function EnginePill(): ReactNode {
2121
const engine = useAppStore((state) => state.engine)
22+
const engineUpdate = useAppStore((state) => state.engineUpdate)
2223
const t = useAppStore((state) => state.t)
2324
const setView = useUiStore((state) => state.setView)
2425

2526
let label: string
2627
let tone: 'ok' | 'warn' | 'bad'
2728

28-
if (engineReady(engine)) {
29+
if (engineReady(engine) && engineUpdate?.hasUpdate) {
30+
label = t('engine.pill.ready', { version: engine.version ?? '' })
31+
tone = 'warn'
32+
} else if (engineReady(engine)) {
2933
label = t('engine.pill.ready', { version: engine.version ?? '' })
3034
tone = 'ok'
3135
} else if (engine.state === 'downloading') {
@@ -55,7 +59,11 @@ function EnginePill(): ReactNode {
5559
onClick={() => setView('settings')}
5660
className="no-drag flex h-[26px] shrink-0 items-center gap-1.5 rounded-full px-2.5 text-[11.5px] font-semibold transition-transform active:scale-[0.97]"
5761
style={{ background: colors.bg, color: colors.fg }}
58-
title={engine.path ?? engine.message ?? label}
62+
title={
63+
engineUpdate?.hasUpdate
64+
? t('engine.updateAvailable', { version: engineUpdate.latest ?? '' })
65+
: (engine.path ?? engine.message ?? label)
66+
}
5967
>
6068
{engine.state === 'downloading' || engine.state === 'checking' ? (
6169
<Spinner size={11} />

‎src/renderer/src/components/SettingsControls.tsx‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,12 @@ export function Choice<T extends string | number>({
192192
onChange: (value: T) => void
193193
}): ReactNode {
194194
return (
195-
<div className="flex items-center gap-1 rounded-lg p-0.5" style={{ background: 'var(--panel-2)' }}>
195+
// `self-start` keeps the pill bar hug its labels when the Field stacks
196+
// children full-width (otherwise the track stretches across the card).
197+
<div
198+
className="flex w-fit items-center gap-1 rounded-lg p-0.5"
199+
style={{ background: 'var(--panel-2)', alignSelf: 'flex-start' }}
200+
>
196201
{options.map((option) => {
197202
const active = option.value === value
198203
return (

0 commit comments

Comments
 (0)