Skip to content

Tags: AMDL-Web/ios

Tags

v3.4+30

Toggle v3.4+30's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Merge pull request #13 from AMDL-Web/dev

Release v3.4: 资料库同步设置页

v3.3+29

Toggle v3.3+29's commit message

Verified

This commit was signed with the committer’s verified signature.
LYJW131 Yangjunwei Liang
feat(config): sync MusicKit token on app activation

Co-authored-by: Codex <noreply@openai.com>
Signed-off-by: LYJW131 <lyjw2007@gmail.com>

v3.3+28

Toggle v3.3+28's commit message

Verified

This commit was signed with the committer’s verified signature.
LYJW131 Yangjunwei Liang
docs(auth): stop claiming the Apple token lasts ten minutes

It lasts about a day. Reading `exp` off a real token on 2026-07-30 gave ~23.4
hours; the app's own "会话 剩余 N 分" line, which parses that claim, showed 1400+
minutes.

The wrong number has a history worth keeping. This repo first said "about 24
hours", someone then "corrected" it to "about 10 minutes" and annotated the 24
hours as an observation error — and the correction was the error. It spread into
four files and, worse, into a label the user reads on screen: "Apple 的登录凭据
只有约十分钟有效期".

So the fix is not a better number. Nothing states a lifetime any more:

  - the on-screen line already shows real time remaining, parsed from `exp`
  - the warning text below it no longer names a duration
  - comments point at `exp` instead of asserting a value

The 10-minute fallback in GatewayCredential.init stays, now labelled for what it
is: a deliberately pessimistic floor for a token whose `exp` cannot be parsed,
not an estimate of the real lifetime.

This also right-sizes a product decision I had been describing wrongly.
Dropping amdl-portal costs a sign-in roughly once a day, not one every ten
minutes — still a regression against the portal's 60-day refresh, but nowhere
near the one the old wording implied.

No behaviour change: comments and one user-facing string.

Signed-off-by: LYJW131 <lyjw2007@gmail.com>

v3.3+27

Toggle v3.3+27's commit message

Verified

This commit was signed with the committer’s verified signature.
LYJW131 Yangjunwei Liang
feat(shazam): refine result actions for version 3.3

Signed-off-by: LYJW131 <lyjw2007@gmail.com>

v3.2+26

Toggle v3.2+26's commit message

Verified

This commit was signed with the committer’s verified signature.
LYJW131 Yangjunwei Liang
feat(auth): send Apple's identity token straight to the gateway

amdl-portal is gone, so the session it issued goes with it. PortalAuth.swift
becomes GatewayAuth.swift: the credential is now the Apple identity token
itself, sent as Authorization: Bearer to oauth2-proxy, which verifies it
against Apple's JWKS and checks an email allow-list.

Deleted along the way: the access/refresh pair, the exchange against
POST /api/gw/auth/apple/native, the refresh single-flight actor (its whole
reason for existing was that a concurrent refresh would rotate the token twice
and get the family revoked as a replay — there is nothing to rotate now), the
401-refresh-retry, GET /api/gw/me, and the pending-approval state that every
new account used to land in.

## The ten-minute token is back, and it is stated rather than hidden

An Apple identity token lives ~10 minutes with no silent renewal, which is
exactly what the portal was built to paper over. It is written down in
GatewayCredential, in AGENTS.md, and in the gateway's own compose file, because
the failure it produces — "I have to sign in again constantly" — reads as a bug
unless you know it is a trade.

Two consequences the code now has to be honest about:

- isSignedIn checks the credential is still usable, not just present. The old
  one only asked "have you signed in", because an expired access token was
  refreshable. Keeping that would show a signed-in UI while every request 401s.
- Expiry is parsed from the token's own `exp`, not received-time + 10 minutes.
  Ten minutes is measured, not promised.

## Cross-target contract

ShareViewController hand-copies the Keychain decoder (extensions cannot see the
main target). Its keys moved from accessToken to identityToken/expiresAt, and
its service from …amdl.portal to …amdl.gateway; a test pins both, including
that expiresAt encodes as a Double, since the extension decodes with a default
JSONDecoder.

Startup now purges two dead credentials: the plaintext identity token an old
build left in UserDefaults, and the portal's 60-day refresh token.

BackendEndpoint keeps its "portal" identifiers on purpose — the host name comes
from the build setting AMDL_PORTAL_HOST, and renaming that key would silently
empty the user's local Config/Portal.xcconfig. The file explains it.

Verified: xcodebuild build and test both succeed on iPhone 17 (iOS 26.4.1).
Signed-off-by: LYJW131 <lyjw2007@gmail.com>

v3.2+25

Toggle v3.2+25's commit message

Verified

This commit was signed with the committer’s verified signature.
LYJW131 Yangjunwei Liang
fix(push): open Emby once the app is active, not from the tap callback

The deep link was on the push and the app parsed it, but tapping the banner
just opened this app. `didReceive` runs before the root view exists on a
cold launch — the app is not active yet, and iOS ignores
`UIApplication.open` from there.

`PendingDownloadRoute` already existed for exactly this reason: the same
callback cannot set navigation state either, so it registers the target and
the root view acts on it. The Emby URL now rides along and is opened from
the same place, falling back to the in-app detail page when open() reports
it could not (no Emby installed).

Signed-off-by: LYJW131 <lyjw2007@gmail.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: LYJW131 <lyjw2007@gmail.com>

v3.2+24

Toggle v3.2+24's commit message

Verified

This commit was signed with the committer’s verified signature.
LYJW131 Yangjunwei Liang
feat(push): open Emby from a finished album's notification

The gateway now puts `emby_deep_link` on a finished album's banner, after
it has refreshed Emby and confirmed the album is actually there. Tapping
the banner goes straight to that album.

Every other case keeps the existing behaviour and they are not rare — a
non-album job, no Emby configured, a scan that had not caught up, a name
that did not match, or Emby not installed at all. `UIApplication.open`
reports whether it could open the URL, so the last one falls back rather
than dropping the tap on the floor, and `job_id` still rides along for it.

Only the `emby` scheme is accepted. The value arrives inside a push
payload, so taking it at face value would let anything that can push to
this device name a URL for the app to open.

Signed-off-by: LYJW131 <lyjw2007@gmail.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: LYJW131 <lyjw2007@gmail.com>

v3.2+23

Toggle v3.2+23's commit message

Verified

This commit was signed with the committer’s verified signature.
LYJW131 Yangjunwei Liang
fix(artwork): let the overview row borrow the detail page's cover

A job added from the share extension opens on `amdl://download/<id>`, which
pushes the detail page straight onto the navigation stack — the overview
list never appears, so nothing ever requested its 256px cover. Backing out
of the detail page therefore landed on a row holding the type placeholder
until that fetch finished, even though the full-size cover was already
decoded and in the cache.

The two pages cache under different keys because they request different
sizes, and `fallbackCacheKey` only ever resolved one way: detail borrowing
the overview's small image. It is now symmetric — whichever size is missing
borrows the other. The row shows the hero image on its first frame (the
memory-cache lookup in `CachedAsyncImage.init` is synchronous), then swaps
in the 256 without a fade, because the fade keys on nil-to-non-nil and the
slot was never empty.

Nothing new is downloaded: this reuses an image already in the cache rather
than prefetching a second one ahead of time.

`fallbackCacheKey` moved onto `JobArtworkLoader` so the direction it picks
is testable, and the literal 256 became `overviewPixelSize` — it is part of
a cache key, not just a request size.

Private playlists are unaffected: both sizes share one unsized key, the two
lookups return the same string, and the fallback stays nil rather than
having a view borrow from itself.

Signed-off-by: LYJW131 <lyjw2007@gmail.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: LYJW131 <lyjw2007@gmail.com>

v3.2+22

Toggle v3.2+22's commit message

Verified

This commit was signed with the committer’s verified signature.
LYJW131 Yangjunwei Liang
fix(detail): hold job progress at 99% until every track is done

With enough tracks the last one's shortfall averages away: 199 of 200 done
is 0.995, and printed to whole percent that is 100% — while the last track
may not have moved a byte. The job then sits at "100%" for as long as the
final track takes.

`DownloadDetail.progress` now refuses to report full while any item is
still in a live state, capping at 0.99. Terminal-only item sets are left
alone, so a genuinely finished job still reads 1 and a job that ended with
failures still reads whatever its average actually is.

Capped on the value rather than at each display site because all five
readers share this one number: the detail percentage, the segmented bar's
translucent segment, and the Live Activity's lock screen and both Dynamic
Island layouts (LiveActivityGateway assigns detail.progress straight
through).

Signed-off-by: LYJW131 <lyjw2007@gmail.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: LYJW131 <lyjw2007@gmail.com>

v3.2+21

Toggle v3.2+21's commit message

Verified

This commit was signed with the committer’s verified signature.
LYJW131 Yangjunwei Liang
feat(detail): segment the job progress bar by track

The detail bar showed one fraction and the track list showed another; the
bar could not say how many tracks were actually finished. It now carries
both at once, like the web detail page's `.jd-bar`:

  - a solid segment for tracks that are done — (done + failed) / total, so
    it only moves a whole notch at a time and always lands on a tick;
  - a translucent segment at `DownloadDetail.progress`, which is the very
    number printed beside the bar, so the two cannot disagree. What it
    shows past the solid segment is the part-finished work of whatever is
    in flight;
  - per-track ticks, drawn in the page background so they cut the bar
    rather than draw a line on it. Skipped above 24 tracks, where they
    stop being legible — the same ceiling the web page uses.

Failed tracks count toward the solid segment on purpose: they will never
progress again, and leaving them outside it parks the bar short of the end
where it reads as stuck.

The two fractions are computed by the caller, not the view, because the two
clients do not agree on what "progress" means and should not: iOS averages
per-track fractions (`ItemProgress.fraction`, weighted from measurement),
the web page uses finished-over-total. Nothing about the existing number,
the bar's height, or the surrounding layout changed.

Signed-off-by: LYJW131 <lyjw2007@gmail.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: LYJW131 <lyjw2007@gmail.com>