Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
b588922
new external source table
Hirobreak Mar 17, 2026
54c85a5
working single import from jellyfin
Hirobreak Mar 21, 2026
d908693
working post updates to jellyfin in concurrent queue
Hirobreak Mar 27, 2026
a2ec67a
working queues
Hirobreak Mar 28, 2026
b147212
working concurrence queue tasks
Hirobreak Apr 7, 2026
aa888eb
new subscription
Hirobreak Apr 11, 2026
9f4fcdc
adding validations and more
Hirobreak Apr 15, 2026
a134d59
deleting new tasks on logout, improved ui buttons, update external re…
Hirobreak Apr 17, 2026
02d5993
visual improvements, download from jellyfin back to our server and more
Hirobreak Apr 21, 2026
74e7f5d
cherrypick fixes
Hirobreak Apr 22, 2026
85290d3
rebase fixes and update local external resource
Hirobreak Apr 24, 2026
e733262
handling missing external resource source and loading display
Hirobreak May 4, 2026
4d3f2ff
audiobookshelf
Hirobreak May 5, 2026
c6016a7
rebase fixes
Hirobreak May 5, 2026
3af6fff
uploading and retrieving progress from audiobookshelf
Hirobreak May 6, 2026
6bbcee8
review fixes
Hirobreak May 12, 2026
22272eb
test fixes
Hirobreak May 13, 2026
30dc07b
localizations
Hirobreak May 13, 2026
2931a83
missing translations
Hirobreak Jun 2, 2026
c39ff44
rebase fixes
Hirobreak Jun 11, 2026
00189ed
supporting external resource hosts
Hirobreak Jun 12, 2026
d27081d
working consume stream based on server
Hirobreak Jun 13, 2026
f7baada
cleaning external import sheet
Hirobreak Jun 15, 2026
7e7ecab
fixing details view refresh
Hirobreak Jun 15, 2026
aab03bd
delete external resource, hardcover as an er
Hirobreak Jun 26, 2026
1a30ee4
Creating sync tasks for ahrdcover ids
Hirobreak Jun 26, 2026
948be81
use hardcover artwork
Hirobreak Jul 7, 2026
e73803d
schema v3 fix and Unified Queue Tasks
Hirobreak Jul 9, 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
426 changes: 346 additions & 80 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.

13 changes: 11 additions & 2 deletions BookPlayer/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ import WatchConnectivity

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, BPLogger {
static weak var shared: AppDelegate?
static weak var shared: AppDelegate?
var pendingURLActions = [Action]()

var window: UIWindow?

Expand Down Expand Up @@ -61,6 +62,14 @@ class AppDelegate: UIResponder, UIApplicationDelegate, BPLogger {
Constants.UserDefaults.systemThemeVariantEnabled: true
])

// 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),
Expand Down Expand Up @@ -467,4 +476,4 @@ extension AppDelegate {

queue.addOperation(backupOperation)
}
}
}
38 changes: 34 additions & 4 deletions BookPlayer/AudiobookShelf/AudiobookShelfRootView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
//

import SwiftUI
import BookPlayerKit

struct AudiobookShelfRootView: View {
let connectionService: AudiobookShelfConnectionService
Expand All @@ -22,11 +23,13 @@ struct AudiobookShelfRootView: View {
}

@EnvironmentObject private var singleFileDownloadService: SingleFileDownloadService
@EnvironmentObject private var importManager: ImportManager
@EnvironmentObject private var theme: ThemeViewModel

@Environment(\.dismiss) var dismiss
@Environment(\.listState) private var listState

@Environment(\.accountService) private var accountService

init(connectionService: AudiobookShelfConnectionService) {
self.connectionService = connectionService
self._connectionViewModel = .init(
Expand Down Expand Up @@ -55,6 +58,8 @@ struct AudiobookShelfRootView: View {
libraryTitle: resolvedLibrary?.title ?? "",
connectionService: connectionService,
singleFileDownloadService: singleFileDownloadService,
accountService: accountService,
importManager: importManager,
onDismiss: { listState.activeIntegrationSheet = nil },
onSwitchLibrary: switchLibraryAction,
dismissAll: dismiss
Expand All @@ -69,6 +74,8 @@ struct AudiobookShelfRootView: View {
libraryTitle: resolvedLibrary?.title ?? "",
connectionService: connectionService,
singleFileDownloadService: singleFileDownloadService,
accountService: accountService,
importManager: importManager,
onDismiss: { listState.activeIntegrationSheet = nil },
onSwitchLibrary: switchLibraryAction,
dismissAll: dismiss
Expand All @@ -83,6 +90,8 @@ struct AudiobookShelfRootView: View {
libraryTitle: resolvedLibrary?.title ?? "",
connectionService: connectionService,
singleFileDownloadService: singleFileDownloadService,
accountService: accountService,
importManager: importManager,
onDismiss: { listState.activeIntegrationSheet = nil },
onSwitchLibrary: switchLibraryAction,
dismissAll: dismiss
Expand All @@ -97,6 +106,8 @@ struct AudiobookShelfRootView: View {
libraryTitle: resolvedLibrary?.title ?? "",
connectionService: connectionService,
singleFileDownloadService: singleFileDownloadService,
accountService: accountService,
importManager: importManager,
onDismiss: { listState.activeIntegrationSheet = nil },
onSwitchLibrary: switchLibraryAction,
dismissAll: dismiss
Expand All @@ -111,6 +122,8 @@ struct AudiobookShelfRootView: View {
libraryTitle: resolvedLibrary?.title ?? "",
connectionService: connectionService,
singleFileDownloadService: singleFileDownloadService,
accountService: accountService,
importManager: importManager,
onDismiss: { listState.activeIntegrationSheet = nil },
onSwitchLibrary: switchLibraryAction,
dismissAll: dismiss
Expand Down Expand Up @@ -292,14 +305,15 @@ struct AudiobookShelfRootView: View {
}
}


// MARK: - Per-Tab NavigationStack

/// Each tab owns its own NavigationStack and BPNavigation.
/// This matches the MainView pattern where each tab has independent navigation.
private struct AudiobookShelfTabRoot: View {
let connectionService: AudiobookShelfConnectionService
let singleFileDownloadService: SingleFileDownloadService
let accountService: AccountService
let importManager: ImportManager?
let onDismiss: () -> Void
var onSwitchLibrary: (() -> Void)?
var dismissAll: DismissAction?
Expand All @@ -316,12 +330,16 @@ private struct AudiobookShelfTabRoot: View {
libraryTitle: String,
connectionService: AudiobookShelfConnectionService,
singleFileDownloadService: SingleFileDownloadService,
accountService: AccountService,
importManager: ImportManager,
onDismiss: @escaping () -> Void,
onSwitchLibrary: (() -> Void)? = nil,
dismissAll: DismissAction? = nil
) {
self.connectionService = connectionService
self.singleFileDownloadService = singleFileDownloadService
self.accountService = accountService
self.importManager = importManager
self.dismissAll = dismissAll
self.onDismiss = onDismiss
self.onSwitchLibrary = onSwitchLibrary
Expand All @@ -333,6 +351,8 @@ private struct AudiobookShelfTabRoot: View {
source: source,
connectionService: connectionService,
singleFileDownloadService: singleFileDownloadService,
accountService: accountService,
importManager: importManager,
navigation: navigation,
navigationTitle: libraryTitle
)
Expand All @@ -351,6 +371,8 @@ private struct AudiobookShelfTabRoot: View {
source: source,
connectionService: connectionService,
singleFileDownloadService: singleFileDownloadService,
accountService: accountService,
importManager: importManager,
navigation: navigation,
navigationTitle: title
)
Expand All @@ -360,11 +382,19 @@ private struct AudiobookShelfTabRoot: View {
viewModel: AudiobookShelfAudiobookDetailsViewModel(
item: item,
connectionService: connectionService,
singleFileDownloadService: singleFileDownloadService
)
singleFileDownloadService: singleFileDownloadService,
accountService: accountService,
importManager: importManager
),
showSubscribeButton: !accountService.hasSyncEnabled(),
allowStream: accountService.hasLiteEnabled(),
) {
onDismiss()
} onStreamTap: {
navigation.path.append(AudiobookShelfLibraryLevelData.subscribe)
}
case .subscribe:
ExternalSyncIntroView()
}
}
.toolbar {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
//

import SwiftUI
import BookPlayerKit

/// Thin wrapper providing AudiobookShelf-specific cell, row, sort picker, and environment
/// to the shared `IntegrationLibraryView`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ enum AudiobookShelfLayout {

@MainActor
final class AudiobookShelfLibraryViewModel: IntegrationLibraryViewModelProtocol, BPLogger {
var importManager: ImportManager?
var accountService: AccountService

enum Routes {
case done
}
Expand All @@ -44,7 +47,9 @@ final class AudiobookShelfLibraryViewModel: IntegrationLibraryViewModelProtocol,

@Published var editMode: EditMode = .inactive
@Published var selectedItems: Set<AudiobookShelfLibraryItem.ID> = []

@Published var useSelectedItems: Bool = false
@Published var showingDownloadConfirmation = false

var onTransition: BPTransition<Routes>?

let connectionService: AudiobookShelfConnectionService
Expand Down Expand Up @@ -122,12 +127,16 @@ final class AudiobookShelfLibraryViewModel: IntegrationLibraryViewModelProtocol,
source: AudiobookShelfLibraryViewSource,
connectionService: AudiobookShelfConnectionService,
singleFileDownloadService: SingleFileDownloadService,
accountService: AccountService,
importManager: ImportManager?,
navigation: BPNavigation,
navigationTitle: String
) {
self.source = source
self.connectionService = connectionService
self.singleFileDownloadService = singleFileDownloadService
self.accountService = accountService
self.importManager = importManager
self.navigation = navigation
self.navigationTitle = navigationTitle

Expand Down Expand Up @@ -222,6 +231,89 @@ final class AudiobookShelfLibraryViewModel: IntegrationLibraryViewModelProtocol,
selectedItems.removeAll()
}
}

func handleImportItems(useSelectedItems: Bool) {
if accountService.hasLiteEnabled() {
virtualImportFolderAudiobooks(useSelectedItems: useSelectedItems)
} else {
if useSelectedItems {
onDownloadTapped()
} else {
confirmDownloadFolder()
}
}
}

@MainActor
func virtualImportFolderAudiobooks(useSelectedItems: Bool) {
let audiobooks = useSelectedItems
? selectedItems.compactMap({ id in
self.items.first(where: { $0.id == id })
})
: self.items.filter { $0.kind == .audiobook }

let libraryItems: [SimpleExternalResource] = audiobooks.map { item in
let fileExt = item.fileExtension ?? "m4a"
let libraryItem = SimpleLibraryItem(
title: item.title,
details: item.authorName ?? "voiceover_unknown_author".localized,
speed: 1,
currentTime: Double(item.currentTime ?? 0),
duration: Double(item.duration ?? 0),
percentCompleted: (item.progress ?? 0 > 0 && item.duration ?? 0 > 0)
? Double(item.progress!) / Double(item.duration!) : 0,
isFinished: item.isFinished ?? false,
relativePath: "",
remoteURL: nil,
artworkURL: item.coverPath != nil ? URL(string: item.coverPath!) : nil,
orderRank: 0,
parentFolder: nil,
originalFileName: "\(item.title).\(fileExt)",
lastPlayDate: nil,
type: .book,
uuid: UUID().uuidString
)

let externalItem = SimpleExternalResource(
id: Int(Date.timeIntervalBetween1970AndReferenceDate),
providerName: ExternalResource.ProviderName.audiobookshelf.rawValue,
providerId: item.id,
syncStatus: ExternalResource.SyncStatus.stream.rawValue,
lastSyncedAt: nil,
hostId: connectionService.connection?.serverId ?? connectionService.connection?.url.absoluteString,
libraryItem: libraryItem
)

return externalItem
}

navigation.dismiss?()
importManager?.externalFiles.append(contentsOf: libraryItems)
importManager?.isShowingExternalImportView = true
}

@MainActor
func onDownloadFolderTapped() {
useSelectedItems = false
showingDownloadConfirmation = true
}

@MainActor
func confirmDownloadFolder() {
var requests = [URLRequest]()
for item in self.items {
do {
let request = try connectionService.createItemDownloadRequest(item)
requests.append(request)
} catch {
self.error = error
}
}

guard !requests.isEmpty else { return }
singleFileDownloadService.handleDownload(requests)
navigation.dismiss?()
}

@MainActor
func onDownloadTapped() {
Expand All @@ -243,6 +335,11 @@ final class AudiobookShelfLibraryViewModel: IntegrationLibraryViewModelProtocol,
singleFileDownloadService.handleDownload(requests)
navigation.dismiss?()
}

@MainActor
func goToSubscribe() {
self.navigation.path.append(AudiobookShelfLibraryLevelData.subscribe)
}

private func handleSortChanged() {
guard !items.isEmpty else { return }
Expand Down
Loading
Loading