Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
325 changes: 309 additions & 16 deletions BookPlayer.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

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

17 changes: 13 additions & 4 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 pendingURLActions = [Action]()

var window: UIWindow?
Expand Down Expand Up @@ -522,10 +522,12 @@ extension AppDelegate {
return coreServices
} else {
let dataManager = DataManager(coreDataStack: stack)
MigrationPlan.injectedCoreDataContext = stack.backgroundContext
let accountService = makeAccountService(dataManager: dataManager)
let audioMetadataService = makeAudioMetadataService()
let libraryService = makeLibraryService(dataManager: dataManager, audioMetadataService: audioMetadataService)
let syncService = makeSyncService(accountService: accountService, libraryService: libraryService)
let syncService = makeSyncService(accountService: accountService, libraryService: libraryService, dataManager: dataManager)
let concurrenceSerivce = makeConcurrenceService(libraryService: libraryService)
let playbackService = makePlaybackService(libraryService: libraryService)
let playerManager = PlayerManager(
libraryService: libraryService,
Expand Down Expand Up @@ -557,6 +559,7 @@ extension AppDelegate {
playerLoaderService: playerLoaderService,
playerManager: playerManager,
syncService: syncService,
concurrenceService: concurrenceSerivce,
watchService: watchService
)

Expand Down Expand Up @@ -585,9 +588,15 @@ extension AppDelegate {
return service
}

private func makeSyncService(accountService: AccountService, libraryService: LibraryService) -> SyncService {
private func makeSyncService(accountService: AccountService, libraryService: LibraryService, dataManager: DataManager) -> SyncService {
let service = SyncService()
service.setup(isActive: accountService.hasSyncEnabled(), libraryService: libraryService)
service.setup(isActive: accountService.hasSyncEnabled(), libraryService: libraryService, dataManager: dataManager)
return service
}

private func makeConcurrenceService(libraryService: LibraryService) -> ConcurrenceService {
let service = ConcurrenceService()
service.setup(libraryService: libraryService)
return service
}

Expand Down
4 changes: 3 additions & 1 deletion BookPlayer/AppIntents/CreateBookmarkIntent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ struct CreateBookmarkIntent: AppIntent {
if let bookmark = libraryService.createBookmark(
at: floor(currentTime),
relativePath: currentItem.relativePath,
uuid: currentItem.uuid,
type: .user
) {

Expand All @@ -68,7 +69,8 @@ struct CreateBookmarkIntent: AppIntent {
playerLoaderService.syncService.scheduleSetBookmark(
relativePath: currentItem.relativePath,
time: floor(currentTime),
note: note
note: note,
uuid: currentItem.uuid
)

let formattedTime = TimeParser.formatTime(bookmark.time)
Expand Down
3 changes: 3 additions & 0 deletions BookPlayer/Coordinators/MainCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class MainCoordinator: NSObject {
let watchConnectivityService: PhoneWatchConnectivityService
let jellyfinConnectionService: JellyfinConnectionService
let audiobookshelfConnectionService: AudiobookShelfConnectionService
let concurrenceService: ConcurrenceService
let hardcoverService: HardcoverService

let playerState = PlayerState()
Expand Down Expand Up @@ -55,6 +56,7 @@ class MainCoordinator: NSObject {
syncService: syncService,
playerLoaderService: coreServices.playerLoaderService
)
self.concurrenceService = coreServices.concurrenceService
self.singleFileDownloadService = SingleFileDownloadService(networkClient: NetworkClient())
self.watchConnectivityService = coreServices.watchService
let jellyfinService = JellyfinConnectionService()
Expand Down Expand Up @@ -103,6 +105,7 @@ class MainCoordinator: NSObject {
.environment(\.playerState, playerState)
.environment(\.playerLoaderService, playerLoaderService)
.environment(\.playbackService, playbackService)
.environment(\.concurrenceService, concurrenceService)
)
vc.modalPresentationStyle = .fullScreen
vc.modalTransitionStyle = .crossDissolve
Expand Down
122 changes: 122 additions & 0 deletions BookPlayer/Import/ExternalSource/ExternalImportView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
//
// ExternalImportView.swift
// BookPlayer
//
// Created by Pedro Iñiguez on 17/3/26.
// Copyright © 2026 BookPlayer LLC. All rights reserved.
//
import SwiftUI
import BookPlayerKit

struct ExternalImportView<Model: ExternalViewModelProtocol>: View {
@ObservedObject var viewModel: Model
@Environment(\.dismiss) var dismiss
@EnvironmentObject private var theme: ThemeViewModel

var body: some View {
ZStack {
theme.systemBackgroundColor
.ignoresSafeArea()

VStack(alignment: .leading, spacing: 20) {

HStack {
Button {
dismiss()
} label: {
Image(systemName: "xmark")
.font(.system(size: 16, weight: .semibold))
.foregroundColor(.white)
.frame(width: 44, height: 44)
.background(
Circle().stroke(Color.white.opacity(0.3), lineWidth: 1)
)
}

Spacer()

Button {
Task {
dismiss()
await viewModel.handleImportResources()
}
} label: {
Image(systemName: "checkmark")
.font(.system(size: 16, weight: .semibold))
.foregroundColor(.white)
.frame(width: 44, height: 44)
.background(
Circle().stroke(Color.white.opacity(0.3), lineWidth: 1)
)
}
}
.padding(.top, 10)

// Headers
Text("Import")
.font(.system(size: 34, weight: .bold))
.foregroundColor(.white)

Text("import_warning_description".localized)
.font(.subheadline)
.foregroundColor(Color.white.opacity(0.6))
.lineSpacing(4)

Text("\(viewModel.resources.count) File\(viewModel.resources.count == 1 ? "" : "s")")
.font(.headline)
.foregroundColor(Color.white.opacity(0.6))
.padding(.top, 10)

ScrollView {
VStack(spacing: 0) {
ForEach(viewModel.resources) { resource in
HStack(spacing: 16) {
Button {
withAnimation {
viewModel.removeResource(withId: resource.providerId)
}
} label: {
Image(systemName: "minus.circle.fill")
.foregroundColor(.red)
.font(.system(size: 20))
}

// Waveform Icon
Image(systemName: "waveform")
.foregroundColor(.pink)

// File Name
Text(resource.libraryItem?.title ?? "Unknown Item")
.foregroundColor(.white)
.font(.system(size: 14))
.lineLimit(1)

Spacer()
}
.padding(.vertical, 14)

// Separator
Divider()
.background(Color.white.opacity(0.2))
}
}
}

Spacer()
}
.padding(.horizontal, 24)
}
}
}

struct ExternalImportView_Previews: PreviewProvider {
static var previews: some View {
ExternalImportView(
viewModel: ExternalImportViewModel(
importManager: ImportManager(
libraryService: LibraryService()
)
)
)
}
}
44 changes: 44 additions & 0 deletions BookPlayer/Import/ExternalSource/ExternalImportViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//
// ExternalImportViewModel.swift
// BookPlayer
//
// Created by Pedro Iñiguez on 17/3/26.
// Copyright © 2026 BookPlayer LLC. All rights reserved.
//
import Foundation
import SwiftUI
import BookPlayerKit

protocol ExternalViewModelProtocol: ObservableObject {
var resources: [SimpleExternalResource] { get set }
func removeResource(withId id: String)
func handleImportResources() async
}

class ExternalImportViewModel: ExternalViewModelProtocol {
let importManager: ImportManager

var resources: [SimpleExternalResource] {
get {
print(importManager.externalFiles.description)
return importManager.externalFiles
}
set {
importManager.externalFiles = newValue
}
}

init(
importManager: ImportManager
) {
self.importManager = importManager
}

func removeResource(withId id: String) {
importManager.externalFiles.removeAll { $0.providerId == id }
}

func handleImportResources() async {
await importManager.processExternalFiles()
}
}
31 changes: 30 additions & 1 deletion BookPlayer/Import/ImportManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,12 @@ final class ImportManager: ObservableObject {
private var subscription: AnyCancellable?
private var timer: Timer?
private var files = CurrentValueSubject<Set<URL>, Never>(Set())

public var operationPublisher = PassthroughSubject<ImportOperation, Never>()
public var externalOperationPublisher = PassthroughSubject<[SimpleExternalResource], Never>()

@Published var externalFiles: [SimpleExternalResource] = []
@Published var isShowingExternalImportView: Bool = false

init(libraryService: LibraryServiceProtocol) {
self.libraryService = libraryService
Expand Down Expand Up @@ -150,4 +154,29 @@ final class ImportManager: ObservableObject {
}
}
}

private func hasExistingBook(_ externalResource: SimpleExternalResource) -> Bool {
guard let simpleItem = externalResource.libraryItem else { return false }
let documentsFolder = DataManager.getDocumentsFolderURL()
let destinationURL = documentsFolder.appendingPathComponent(simpleItem.relativePath)
print(destinationURL)
if self.libraryService.findBooks(containing: destinationURL)?.first != nil {
return true
}

if self.libraryService.findResource(for: externalResource.providerId) != nil {
return true
}

return false
}

func processExternalFiles() {
guard self.externalFiles.count > 0 else {
return
}
let myExternalFiles = self.externalFiles.filter { !hasExistingBook($0) }
self.externalFiles = []
self.externalOperationPublisher.send(myExternalFiles)
}
}
Loading