Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
5aa3e7b
new external source table
Hirobreak Mar 17, 2026
4eb6981
working single import from jellyfin
Hirobreak Mar 21, 2026
ecbc669
working post updates to jellyfin in concurrent queue
Hirobreak Mar 27, 2026
d8ae379
working queues
Hirobreak Mar 28, 2026
016cddc
working concurrence queue tasks
Hirobreak Apr 7, 2026
4d0a714
new subscription
Hirobreak Apr 11, 2026
05e8a32
adding validations and more
Hirobreak Apr 15, 2026
c5b2640
deleting new tasks on logout, improved ui buttons, update external re…
Hirobreak Apr 17, 2026
b5b361e
visual improvements, download from jellyfin back to our server and more
Hirobreak Apr 21, 2026
b34d560
cherrypick fixes
Hirobreak Apr 22, 2026
3913a44
rebase fixes and update local external resource
Hirobreak Apr 24, 2026
9725cd6
handling missing external resource source and loading display
Hirobreak May 4, 2026
14d37f5
audiobookshelf
Hirobreak May 5, 2026
e437a0f
rebase fixes
Hirobreak May 5, 2026
74d206f
uploading and retrieving progress from audiobookshelf
Hirobreak May 6, 2026
77a6c08
review fixes
Hirobreak May 12, 2026
5aa1b24
test fixes
Hirobreak May 13, 2026
a577fe9
localizations
Hirobreak May 13, 2026
a7e646f
missing translations
Hirobreak Jun 2, 2026
1086953
rebase fixes
Hirobreak Jun 11, 2026
5c7affe
supporting external resource hosts
Hirobreak Jun 12, 2026
a2001b7
working consume stream based on server
Hirobreak Jun 13, 2026
d55789a
cleaning external import sheet
Hirobreak Jun 15, 2026
b3e7cf2
fixing details view refresh
Hirobreak Jun 15, 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
464 changes: 374 additions & 90 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.

5 changes: 3 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 @@ -467,4 +468,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