Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
172 commits
Select commit Hold shift + click to select a range
eca709e
feat: external resource support for media-server items (#1523 rebased…
Hirobreak Aug 23, 2026
55ae384
test: add externalURL argument to PlayableChapter test factory
GianniCarlo Aug 23, 2026
c4fe7cd
fix: align external-resource host resolution with the shared Android …
GianniCarlo Aug 23, 2026
ba0d5b2
fix: consume discarded external progress pushes; keep them ungated by…
GianniCarlo Aug 23, 2026
249fbce
fix: address review feedback (round 1)
GianniCarlo Aug 23, 2026
ee98b33
fix: address review feedback (round 2)
GianniCarlo Aug 23, 2026
1deee31
fix: address review feedback (round 3)
GianniCarlo Aug 23, 2026
0982618
fix: address review feedback (round 4)
GianniCarlo Aug 23, 2026
b13cd81
fix: address review feedback (round 5)
GianniCarlo Aug 24, 2026
f4c5cfe
fix: address review feedback (round 6)
GianniCarlo Aug 24, 2026
8541b14
fix: address review feedback (round 7)
GianniCarlo Aug 24, 2026
a9f50b1
fix: persist hostId on external-update tasks (round 8)
GianniCarlo Aug 24, 2026
e5a4665
test: dedicated coverage for the concurrent-sync engine and host reso…
GianniCarlo Aug 24, 2026
6b20470
fix: address review feedback (round 9)
GianniCarlo Aug 24, 2026
d736db1
fix: address review feedback (round 10)
GianniCarlo Aug 24, 2026
07f20f3
fix: address review feedback (round 11)
GianniCarlo Aug 24, 2026
3f2eef0
fix: regenerate AutoMockable for the provider-scoped findResource
GianniCarlo Aug 24, 2026
0e20054
ci: raise the reviewer turn limit to 100
GianniCarlo Aug 24, 2026
8c50d43
ci: repair malformed reviewer JSON with a one-shot model call
GianniCarlo Aug 24, 2026
b8afc48
fix: address review feedback (round 12)
GianniCarlo Aug 24, 2026
0f3571c
fix: address review feedback (round 13)
GianniCarlo Aug 24, 2026
c68a2f4
fix: address review feedback (round 14)
GianniCarlo Aug 24, 2026
1bea495
fix: address review feedback (round 15)
GianniCarlo Aug 24, 2026
9a51708
chore: remove stray empty scratch file
GianniCarlo Aug 24, 2026
150e615
fix: address review feedback (round 16)
GianniCarlo Aug 24, 2026
ad72e28
fix: address review feedback (round 17)
GianniCarlo Aug 24, 2026
b36c05b
fix: repost uploadProgressUpdated so upload progress bars update again
GianniCarlo Aug 24, 2026
30d5f63
fix: address review feedback (round 18)
GianniCarlo Aug 24, 2026
fb9ff52
fix: case-insensitive Authorization dedup in external request headers…
GianniCarlo Aug 24, 2026
504895c
fix: guard the already-loaded dedup on real uuids; consistent access-…
GianniCarlo Aug 24, 2026
41c645c
fix: address review feedback (round 21)
GianniCarlo Aug 24, 2026
ae1f170
chore: delete callerless task-queue helpers (round 22)
GianniCarlo Aug 24, 2026
450005b
fix: address review feedback (round 23)
GianniCarlo Aug 24, 2026
ff6e454
fix: address review feedback (round 24)
GianniCarlo Aug 24, 2026
11aac26
fix: carry externalResources through SyncableItem.copy (round 25)
GianniCarlo Aug 24, 2026
5bc36b4
fix: address review feedback (round 26)
GianniCarlo Aug 24, 2026
3bfdebb
fix: address review feedback (round 27)
GianniCarlo Aug 24, 2026
6cfaad1
fix: address review feedback (round 28)
GianniCarlo Aug 24, 2026
2f33bbe
fix: address review feedback (round 29)
GianniCarlo Aug 24, 2026
013529c
fix: address review feedback (round 30)
GianniCarlo Aug 24, 2026
204ccfa
fix: address review feedback (round 31)
GianniCarlo Aug 24, 2026
0cdb71c
fix: address review feedback (round 32)
GianniCarlo Aug 24, 2026
d773d93
fix: address review feedback (round 33)
GianniCarlo Aug 24, 2026
38d506f
test: add token-leak and v11->v12 migration regression guards (round 34)
GianniCarlo Aug 24, 2026
4320c0e
fix: address review feedback (round 35)
GianniCarlo Aug 24, 2026
19b406f
fix: address review feedback (round 36)
GianniCarlo Aug 24, 2026
8d75e55
fix: address review feedback (round 37)
GianniCarlo Aug 24, 2026
a63875c
fix: address review feedback (round 38)
GianniCarlo Aug 24, 2026
b32ef2e
fix: address review feedback (round 39)
GianniCarlo Aug 24, 2026
b78afb6
fix: address review feedback (round 40)
GianniCarlo Aug 25, 2026
3a71f0c
fix: address review feedback (round 41)
GianniCarlo Aug 25, 2026
9b66128
fix: address review feedback (round 42)
GianniCarlo Aug 25, 2026
aaf99d7
fix: address review feedback (round 43)
GianniCarlo Aug 25, 2026
8d5cd6b
docs: bring CLAUDE.md in line with the PR's invariants (round 44)
GianniCarlo Aug 25, 2026
f698036
fix: drop spurious await on synchronous processExternalFiles (round 45)
GianniCarlo Aug 25, 2026
a703d32
fix: address review feedback (round 46)
GianniCarlo Aug 25, 2026
74b83a6
fix: address review feedback (round 47)
GianniCarlo Aug 25, 2026
3396ea1
Regenerate mapping model
GianniCarlo Aug 29, 2026
818a1b8
fix: close the cancel-before-task window + guard poison ticks
GianniCarlo Aug 29, 2026
ad0842c
fix: repair positional format specifiers in sync_invitation_description
GianniCarlo Aug 29, 2026
ee33049
feat: require real file extensions for virtual imports (no m4a guess)
GianniCarlo Aug 30, 2026
fb69a06
refactor: ImportManager is never nil — make it non-optional end to end
GianniCarlo Aug 31, 2026
7b63758
fix: address review feedback on the tightening push
GianniCarlo Aug 31, 2026
7543aba
refactor: honest-async imports, Jellyfin VM collapse, shared import p…
GianniCarlo Sep 1, 2026
a416f23
fix: five review polish items in one sweep
GianniCarlo Sep 1, 2026
d1b17f5
refactor: entitlement CTAs compute live on the VM, not construction s…
GianniCarlo Sep 2, 2026
a16bf52
refactor: subscribe navigation moves onto the details VMs (goToSubscr…
GianniCarlo Sep 2, 2026
658b560
fix: reconcile external resources by the (providerName, providerId) pair
GianniCarlo Sep 2, 2026
5bde750
fix: virtual re-imports reuse the existing row instead of creating twins
GianniCarlo Sep 2, 2026
3aee655
fix: details view models get @StateObject ownership (initModel pattern)
GianniCarlo Sep 3, 2026
f918790
fix: two review one-liners — folder-download filter, loud stale-path …
GianniCarlo Sep 3, 2026
5760ff3
refactor: integrations own virtual-import staging and confirmation en…
GianniCarlo Sep 3, 2026
c02460f
refactor: integrations depend on an import-confirmed closure, not Imp…
GianniCarlo Sep 4, 2026
dcbaeda
fix: decode ABS audio files by their real shape; strip the dot-prefix…
GianniCarlo Sep 4, 2026
b43ec75
refactor: external-import bus moves off ImportManager onto its own ob…
GianniCarlo Sep 4, 2026
bc2a452
fix: drop MainView's dead importManager; theme the provider-row separ…
GianniCarlo Sep 4, 2026
f68b7a6
refactor: ConcurrenceService owns its access-policy refresh (SyncServ…
GianniCarlo Sep 5, 2026
87bd032
docs: correct the @Entry-placeholder failure-mode claim in CLAUDE.md
GianniCarlo Sep 5, 2026
a8f3727
fix: HardcoverService's syncService is never nil — stop holding it op…
GianniCarlo Sep 5, 2026
b526ccb
fix: clear the open review backlog (17 items)
GianniCarlo Sep 5, 2026
086fa84
fix: reuse the existing title_button key for the ABS sort option
GianniCarlo Sep 5, 2026
3e31d71
refactor: collapse the provider switch to a hardcover-only filter
GianniCarlo Sep 5, 2026
40405ef
fix: repair device-B's metadata-empty Hardcover stub instead of rende…
GianniCarlo Sep 5, 2026
d969f33
fix: extract embedded artwork before falling back to the Hardcover cover
GianniCarlo Sep 6, 2026
32ed0f3
refactor: BookByIdData joins the Hardcover response-model files
GianniCarlo Sep 6, 2026
17253d7
chore: sweep whitespace left behind in ImportManager
GianniCarlo Sep 6, 2026
1213e12
chore: delete the rebase-resurrected zombie files
GianniCarlo Sep 6, 2026
fe62540
test: thread externalURL through develop's new chapter fixtures
GianniCarlo Sep 6, 2026
dd95972
refactor: extract a reusable import-confirmation shell into its own file
GianniCarlo Sep 6, 2026
a9d14cc
test: pin the confirmation-row mapping on the view model
GianniCarlo Sep 6, 2026
06bb846
refactor: fold the duplicated Jellyfin import flow into a protocol ex…
GianniCarlo Sep 6, 2026
4490351
refactor: external-resource details section moves to its own file
GianniCarlo Sep 7, 2026
c3a2b94
refactor: item details resolves external hosts in the view model, off…
GianniCarlo Sep 7, 2026
fca73e5
refactor: collapse the external-resources section gate into the caller
GianniCarlo Sep 7, 2026
961be92
refactor: collapse the host resolver into one switch with a generic f…
GianniCarlo Sep 7, 2026
59a05dc
fix: keep hardcover links out of the external-resources section
GianniCarlo Sep 7, 2026
2bcc7ee
fix: never render an empty host field
GianniCarlo Sep 7, 2026
f33e728
chore: drop the dead audiobookshelfService property from ItemListView
GianniCarlo Sep 7, 2026
bc76901
fix: offer Media Servers when a server is missing, not when sync is off
GianniCarlo Sep 7, 2026
52fc66b
refactor: one home for the media-server resource question
GianniCarlo Sep 7, 2026
69b6c53
refactor: item details loads from .task instead of init
GianniCarlo Sep 7, 2026
e46ffc0
refactor: the media-servers listener moves to MainView
GianniCarlo Sep 8, 2026
0781028
refactor: the library row subtitle is one Text, not a stack
GianniCarlo Sep 9, 2026
d9c23ad
fix: no Hardcover badge in the library row
GianniCarlo Sep 10, 2026
337bcd1
fix: Stream button uses the accent-filled button treatment
GianniCarlo Sep 10, 2026
1fecf35
feat: a service owns the media-server progress pull
GianniCarlo Sep 10, 2026
3cd50ec
refactor: the progress pull runs off .bookPlayed, not a delegate slot
GianniCarlo Sep 10, 2026
d758c13
fix: AudiobookShelf items refresh from their server too
GianniCarlo Sep 10, 2026
4967d5f
feat: CarPlay asks about a farther remote position
GianniCarlo Sep 10, 2026
7a39cc7
refactor: stream resolution moves behind an injected resolver
GianniCarlo Sep 10, 2026
8c2835a
refactor: the prompt rule lives on the positions it compares
GianniCarlo Sep 10, 2026
47c3e0f
fix: one arbiter decides which surface asks about a remote position
GianniCarlo Sep 11, 2026
3294ec5
fix: downloading an external item resolves its source the same way st…
GianniCarlo Sep 11, 2026
02ffd3d
refactor: the progress service holds its lock once and owns its notif…
GianniCarlo Sep 11, 2026
48a90f5
chore: drop a dead dependency and two loose ends from the extraction
GianniCarlo Sep 11, 2026
d8217de
refactor: ExternalImportBus becomes ExternalImportEvents, owned by Ma…
GianniCarlo Sep 12, 2026
a2733cd
refactor: the sync-queue engine owns every lane's count
GianniCarlo Sep 13, 2026
c8b8438
refactor: one Queued Tasks screen, a collapsible section per lane
GianniCarlo Sep 14, 2026
cb91ec8
refactor: rename the concurrent-sync family to SyncQueue
GianniCarlo Sep 14, 2026
dbff6d6
fix: set aside a task store no schema in the migration plan can open
GianniCarlo Sep 14, 2026
9ef0cac
feat: media-server progress pull requires the sync entitlement
GianniCarlo Sep 14, 2026
d85705e
refactor: the list refresh pulls media-server progress per level, res…
GianniCarlo Sep 14, 2026
26448f7
feat: split the media-server bulk action into explicit Stream and Dow…
GianniCarlo Sep 15, 2026
240062e
refactor: drop the streaming buffering overlay and its state chain
GianniCarlo Sep 15, 2026
b70b437
feat: virtual imports require a server-reported duration
GianniCarlo Sep 16, 2026
d1d722d
fix: give the two download failures their own message
GianniCarlo Sep 16, 2026
8c0ebf2
fix: populate the file extension the details screen imports with
GianniCarlo Sep 16, 2026
a358715
refactor: drop the playback-time duration probe for external chapters
GianniCarlo Sep 16, 2026
5b81e5d
feat: media-server chapters from the provider, at import and on the pull
GianniCarlo Sep 16, 2026
c8cc0fa
fix: media-server chapters have to cover the whole item
GianniCarlo Sep 16, 2026
2ea28e9
perf: ask a media server for chapters only where they're missing
GianniCarlo Sep 16, 2026
f098f91
revert: keep loadValues(forKeys:) for asset metadata
GianniCarlo Sep 16, 2026
4f3c81b
feat: media-server streaming for anyone who has ever paid
GianniCarlo Sep 16, 2026
3f60b94
revert: drop the subscription tier badge from the profile card
GianniCarlo Sep 17, 2026
20eb0b0
revert: let AVFoundation choose the streaming buffer depth
GianniCarlo Sep 17, 2026
ddfe34c
fix: playback failures reach the surface the user is actually on
GianniCarlo Sep 17, 2026
28900d7
chore: let SwiftUI literals localize themselves again
GianniCarlo Sep 17, 2026
254b7bc
refactor: name the tier at the call site, not through a parameter
GianniCarlo Sep 17, 2026
fcf1514
revert: drop the Info.plist churn
GianniCarlo Sep 17, 2026
ffe9101
fix: the media-server sign-up screen shows its errors again
GianniCarlo Sep 17, 2026
494387d
fix: the media-server paywall stops selling what is already free
GianniCarlo Sep 17, 2026
a289e49
fix: the media-server paywall stops disclaiming someone else's product
GianniCarlo Sep 17, 2026
ff39d69
fix: one glyph per idea across the media-server screens
GianniCarlo Sep 19, 2026
9e130c3
refactor: one primary button instead of four
GianniCarlo Sep 19, 2026
5c65c71
fix: the SwiftUI import sheet stops imitating a navigation bar
GianniCarlo Sep 19, 2026
b1b8043
fix: the login buttons are the inset, not an overlay
GianniCarlo Sep 20, 2026
76e86f2
fix: a tip no longer reopens streaming after sign-out
GianniCarlo Sep 20, 2026
d8a9226
fix: the provider mark is a glyph, not a picture beside the text
GianniCarlo Sep 20, 2026
6ddcd20
refactor: media-server-only code takes a type that cannot be Hardcover
GianniCarlo Sep 20, 2026
3279655
chore: drop the unreferenced Plus assets
GianniCarlo Sep 21, 2026
93fba54
fix: the corner badge says whose cloud the file is in
GianniCarlo Sep 21, 2026
44ffa13
fix(sync): send real uuids on GET requests
GianniCarlo Sep 23, 2026
0717831
fix(sync): show the file name on queued upload rows
GianniCarlo Sep 24, 2026
6509388
chore: raise the iOS minimum to 26
GianniCarlo Sep 24, 2026
6346357
ci: build with Xcode 27
GianniCarlo Sep 25, 2026
99915ae
chore: drop the contentMarginsDisabledIfAvailable wrapper
GianniCarlo Sep 25, 2026
66675e2
fix(sync): clean up upload temp links and cap matchUuid batches at 1,000
GianniCarlo Sep 27, 2026
614e04b
fix(sync): hold the server lanes while sync is off
GianniCarlo Sep 27, 2026
ffbbcf6
feat(sync): park tasks the server can never accept
GianniCarlo Sep 27, 2026
1028e10
feat(sync): show, retry and report parked tasks
GianniCarlo Sep 27, 2026
9092234
feat(upload): multipart upload routes and resumable task state
GianniCarlo Sep 27, 2026
d6ef1c7
feat(upload): upload book files as S3 multipart uploads
GianniCarlo Sep 27, 2026
19cd4b3
feat(upload): media-server files and lost books go through the upload…
GianniCarlo Sep 27, 2026
53487ac
feat(sync): dismiss too-large uploads; flag parked tasks on Profile's…
GianniCarlo Sep 27, 2026
b0cbf32
feat(upload): answer iOS's background session wakes after the work se…
GianniCarlo Sep 28, 2026
1935610
feat(upload): keep the upload queue at full speed in the background
GianniCarlo Sep 28, 2026
2dc5be8
feat(upload): "Continue in background" buttons, and Retry resumes at …
GianniCarlo Sep 28, 2026
3df42a6
fix(sync): never confirm a book as synced on a tier without S3
GianniCarlo Sep 28, 2026
734bb24
feat(sync): the missing-items pass, by uuid
GianniCarlo Sep 29, 2026
64d516b
fix(sync): a Retry registers a lost upload again
GianniCarlo Sep 30, 2026
017e5e3
fix(sync): don't report a too-large book's park to Sentry
GianniCarlo Oct 1, 2026
b52d79a
docs: CLAUDE.md matches CI on Xcode 27, the iOS 26 minimum and curren…
GianniCarlo Oct 1, 2026
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
38 changes: 37 additions & 1 deletion .github/claude/reviewer/review.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,36 @@ function extractJson(text) {
return JSON.parse(candidate.slice(start, end + 1));
}

/// One-shot, tool-less model call that fixes the reviewer's own malformed JSON (typically an
/// unescaped quote or raw newline inside a finding's comment). Content is preserved; only
/// syntax is repaired. Costs one cheap turn instead of discarding a completed review pass.
async function repairJsonWithModel(brokenText) {
let repaired = '';
const iterator = query({
prompt:
'The following text should be exactly one valid JSON object but fails to parse. ' +
'Reply with ONLY the corrected, valid JSON object — no code fences, no commentary. ' +
'Preserve every property and value; fix escaping/syntax only.\n\n' + brokenText,
options: {
model: MODEL,
allowedTools: [],
permissionMode: 'bypassPermissions',
maxTurns: 1,
},
});
for await (const msg of iterator) {
if (msg.type === 'assistant') {
const content = msg.message?.content;
if (Array.isArray(content)) {
for (const block of content) {
if (block.type === 'text' && block.text) repaired = block.text;
}
}
}
}
return extractJson(repaired);
}

async function runAgent() {
let finalText = '';
let turns = 0;
Expand Down Expand Up @@ -210,7 +240,13 @@ async function main() {
let parsed;
try {
if (!finalText) throw new Error('agent produced no text output');
parsed = extractJson(finalText);
try {
parsed = extractJson(finalText);
} catch (parseError) {
// A finished review with broken JSON is worth one repair attempt before discarding.
console.warn(`Agent JSON malformed (${parseError.message}); attempting model repair…`);
parsed = await repairJsonWithModel(finalText);
}
if (!parsed.verdict || !parsed.summary || !Array.isArray(parsed.findings)) {
throw new Error('JSON missing verdict/summary/findings');
}
Expand Down
10 changes: 5 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ on:
jobs:
build:
# https://github.com/actions/runner-images/tree/main/images/macos
runs-on: macos-26
runs-on: xcode-27
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: Select Xcode 26.0
- name: Select Xcode 27.0
run: |
ls -1 /Applications | grep -E '^Xcode_26\.' || true
sudo xcode-select -s "/Applications/Xcode_26.4.app"
ls -1 /Applications | grep -E '^Xcode_27' || true
sudo xcode-select -s "/Applications/Xcode_27.0.app"
xcodebuild -version
xcrun simctl list runtimes

Expand All @@ -29,7 +29,7 @@ jobs:
- name: Reset Simulator
run: |
set -x
export DEVELOPER_DIR="/Applications/Xcode_26.4.app"
export DEVELOPER_DIR="/Applications/Xcode_27.0.app"
xcrun simctl shutdown all || true
xcrun simctl erase all || true
launchctl kickstart -k gui/$UID/com.apple.CoreSimulator.CoreSimulatorService || true
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/claude-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,6 @@ jobs:
BASE_REF: ${{ github.event.pull_request.base.ref }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
REVIEW_MODEL: claude-opus-4-8
REVIEW_MAX_TURNS: '50' # agent only reads/reports (posting is done by the script), so turns go far
REVIEW_MAX_TURNS: '100' # agent only reads/reports (posting is done by the script), so turns go far
IS_SANDBOX: '1'
run: node .github/claude/reviewer/review.mjs
716 changes: 559 additions & 157 deletions BookPlayer.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

90 changes: 80 additions & 10 deletions BookPlayer/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import WatchConnectivity

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, BPLogger {
static weak var shared: AppDelegate?
static weak var shared: AppDelegate?

var window: UIWindow?

Expand All @@ -48,6 +48,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, BPLogger {
/// Database backup task identifier
private lazy var databaseBackupTaskIdentifier =
"\(Bundle.main.configurationString(for: .bundleIdentifier)).background.database.backup"
/// Answers iOS's relaunches and wakes for the background transfer sessions
private lazy var backgroundSessionWakes = BackgroundSessionWakeCoordinator()

func application(
_ application: UIApplication,
Expand All @@ -63,13 +65,24 @@ class AppDelegate: UIResponder, UIApplicationDelegate, BPLogger {
Constants.UserDefaults.videoPictureInPictureEnabled: false,
])

// Hardcover settings live in the shared (app group) suite. Register their defaults
// there so the service (`bool/object(forKey:)`) and the settings UI (`@AppStorage`)
// resolve to the same value before the user explicitly saves anything.
UserDefaults.sharedDefaults.register(defaults: [
Constants.UserDefaults.hardcoverAutoAddWantToRead: true,
Constants.UserDefaults.hardcoverReadingThreshold: 1.0,
])

NotificationCenter.default.addObserver(
self,
selector: #selector(self.messageReceived),
name: .messageReceived,
object: nil
)

// Before anything can wake a session: a session's "finished events" may arrive before
// iOS hands over its handler, and must not find no one listening
_ = backgroundSessionWakes
// register background refresh tasks
self.setupBackgroundRefreshTasks()
// register for remote events
Expand All @@ -81,9 +94,29 @@ class AppDelegate: UIResponder, UIApplicationDelegate, BPLogger {
// Setup core services
AppServices.shared.setupCoreServices()

// The extension-safe framework can't reach UIApplication.shared itself; the SSO flow's
// presentation anchor comes from here, the one process that owns windows.
WebAuthenticationSession.foregroundWindowProvider = {
let scenes = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }
guard let scene = scenes.first(where: { $0.activationState == .foregroundActive }) else {
return nil
}
return scene.keyWindow ?? scene.windows.first
}

return true
}

/// Stays on the app delegate with scenes (TN3187). The handler is called once the
/// session's events are handled — for uploads, once the next parts are queued.
func application(
_ application: UIApplication,
handleEventsForBackgroundURLSession identifier: String,
completionHandler: @escaping () -> Void
) {
backgroundSessionWakes.handleEvents(forSession: identifier, completionHandler: completionHandler)
}

func application(
_ application: UIApplication,
handle intent: INIntent,
Expand Down Expand Up @@ -412,6 +445,9 @@ extension AppDelegate {
self.handleAppRefresh(task: refreshTask)
}

// At launch, so a task iOS runs for an earlier submit finds its handler
UploadContinuationController.register { AppServices.shared.uploadContinuation }

BGTaskScheduler.shared.register(
forTaskWithIdentifier: databaseBackupTaskIdentifier,
using: nil
Expand All @@ -429,13 +465,45 @@ extension AppDelegate {
let request = BGAppRefreshTaskRequest(identifier: refreshTaskIdentifier)
request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)

try? BGTaskScheduler.shared.submit(request)
Self.submitBackgroundTask(request)
}

/// `submit(_:)` is deprecated from iOS 27 in favor of `submitTaskRequest`.
static func submitBackgroundTask(
_ request: BGTaskRequest,
completion: @escaping @Sendable ((any Error)?) -> Void = { _ in }
) {
if #available(iOS 27, *) {
// Its header: not from the main thread
DispatchQueue.global(qos: .utility).async {
BGTaskScheduler.shared.submitTaskRequest(request, completionHandler: completion)
}
} else {
do {
try BGTaskScheduler.shared.submit(request)
completion(nil)
} catch {
completion(error)
}
}
}

func handleAppRefresh(task: BGAppRefreshTask) {
guard let syncService = AppServices.shared.coreServices?.syncService else { return }
guard let syncQueueService = AppServices.shared.coreServices?.syncQueueService else { return }

let refreshOperation = RefreshTaskOperation(syncService: syncService)
// A gated sync lane (sync off) never drains, so waiting on it would only hold the
// window open until expiration
guard syncQueueService.serverLanesEnabled else {
task.setTaskCompleted(success: true)
return
}

// Sync lane only: metadata pushes are what the refresh window is for. Provider pushes
// retry forever against an unreachable home server, and S3 uploads already run on a
// background URLSession — neither should keep the process awake until expiration.
let refreshOperation = RefreshTaskOperation(
queueDrained: syncQueueService.observeQueueCounts().laneDrained(TaskQueueKey.sync)
)

refreshOperation.completionBlock = { [weak self] in
let success = !refreshOperation.isCancelled
Expand Down Expand Up @@ -486,11 +554,13 @@ extension AppDelegate {
request.earliestBeginDate = Date(timeIntervalSinceNow: 24 * 60 * 60)
}

do {
try BGTaskScheduler.shared.submit(request)
Self.logger.info("Database backup scheduled for: \(request.earliestBeginDate?.description ?? "unknown")")
} catch {
Self.logger.error("Failed to schedule database backup: \(error.localizedDescription)")
let scheduledFor = request.earliestBeginDate?.description ?? "unknown"
Self.submitBackgroundTask(request) { error in
if let error {
Self.logger.error("Failed to schedule database backup: \(error.localizedDescription)")
} else {
Self.logger.info("Database backup scheduled for: \(scheduledFor)")
}
}
}

Expand All @@ -515,4 +585,4 @@ extension AppDelegate {

queue.addOperation(backupOperation)
}
}
}
2 changes: 1 addition & 1 deletion BookPlayer/AppIntents/BPAppShortcuts.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import Foundation
import AppIntents

@available(iOS 16.4, macOS 14.0, watchOS 10.0, *)
@available(macOS 14.0, watchOS 10.0, *)
struct BPAppShortcuts: AppShortcutsProvider {
static var appShortcuts: [AppShortcut] {
return [
Expand Down
2 changes: 1 addition & 1 deletion BookPlayer/AppIntents/CancelSleepTimerIntent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import Foundation
import AppIntents

@available(iOS 16.4, macOS 14.0, watchOS 10.0, *)
@available(macOS 14.0, watchOS 10.0, *)
struct CancelSleepTimerIntent: AppIntent {
static var title: LocalizedStringResource = "intent_sleeptimer_cancel"

Expand Down
2 changes: 1 addition & 1 deletion BookPlayer/AppIntents/EndChapterSleepTimerIntent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import Foundation
import AppIntents

@available(iOS 16.4, macOS 14.0, watchOS 10.0, *)
@available(macOS 14.0, watchOS 10.0, *)
struct EndChapterSleepTimerIntent: AppIntent {
static var title: LocalizedStringResource = "intent_sleeptimer_eoc_title"

Expand Down

This file was deleted.

31 changes: 0 additions & 31 deletions BookPlayer/Assets.xcassets/audiobookshelf-icon.imageset/abs.svg

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"info" : {
"author" : "xcode",
"version" : 1
},
"symbols" : [
{
"filename" : "audiobookshelf.svg",
"idiom" : "universal"
}
]
}
Loading
Loading