Skip to content
Merged
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
3 changes: 3 additions & 0 deletions BookPlayer/AudiobookShelf/AudiobookShelfRootView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -403,5 +403,8 @@ private struct AudiobookShelfTabRoot: View {
.tint(theme.linkColor)
.environmentObject(theme)
}
.task {
navigation.dismiss = onDismiss
}
}
}
12 changes: 8 additions & 4 deletions BookPlayer/Import/Models/ImportOperation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ public class ImportOperation: Operation {

try FileManager.default.copyItem(at: fileURL, to: existingFileURL)

if DataManager.isAppOwnFolder(fileURL) {
if DataManager.isAppManagedSource(fileURL) {
fileURL.disableFileProtection()
try? FileManager.default.removeItem(at: fileURL)
}
Comment on lines +190 to 193
Expand Down Expand Up @@ -216,13 +216,17 @@ public class ImportOperation: Operation {
private func detectFolderOrganization() {
guard files.count > 1 else { return }

let documentsURL = DataManager.getDocumentsFolderURL()
let topLevelImportPaths: Set<String> = [
DataManager.getDocumentsFolderURL().resolvingSymlinksInPath().path,
DataManager.getSharedFilesFolderURL().resolvingSymlinksInPath().path,
DataManager.getInboxFolderURL().resolvingSymlinksInPath().path,
]
var parentFolders = Set<String>()

for file in files {
let parentURL = file.deletingLastPathComponent()

guard parentURL != documentsURL else { continue }
guard !topLevelImportPaths.contains(parentURL.resolvingSymlinksInPath().path) else { continue }

parentFolders.insert(parentURL.lastPathComponent)
}
Expand Down Expand Up @@ -260,7 +264,7 @@ public class ImportOperation: Operation {
do {
try FileManager.default.copyItem(at: currentFile, to: destinationURL)

if DataManager.isAppOwnFolder(currentFile) {
if DataManager.isAppManagedSource(currentFile) {
currentFile.disableFileProtection()
try FileManager.default.removeItem(at: currentFile)
}
Expand Down
6 changes: 6 additions & 0 deletions BookPlayer/Jellyfin/JellyfinRootView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,9 @@ private struct JellyfinTabRoot: View {
.sheet(isPresented: $showConnectionDetails) {
connectionDetailsSheet
}
.task {
navigation.dismiss = onDismiss
}
}

@ViewBuilder
Expand Down Expand Up @@ -483,6 +486,9 @@ where ViewModel.Item == JellyfinLibraryItem {
dismissAll: dismissAll
)
}
.task {
navigation.dismiss = onDismiss
}
}
}

Expand Down
5 changes: 3 additions & 2 deletions BookPlayer/Library/ItemList/ItemListView+Alerts.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ extension ItemListView {
@ViewBuilder
func importCompletionAlert(for alertParameters: ImportOperationState.AlertParameters) -> some View {
let hasParentFolder = model.libraryNode.folderRelativePath != nil
let suggestedFolderName = alertParameters.suggestedFolderName ?? ""
let suggestedFolderName = ((alertParameters.suggestedFolderName ?? "") as NSString).deletingPathExtension
let canCreateBound = alertParameters.hasOnlyBooks || alertParameters.singleFolder != nil

if hasParentFolder {
Expand All @@ -60,6 +60,7 @@ extension ItemListView {
}

Button("new_playlist_button") {
folderInput.prepareForFolder(title: suggestedFolderName, placeholder: suggestedFolderName)
model.selectedSetItems = Set(alertParameters.itemIdentifiers)
activeAlert = nil
Task { @MainActor in
Expand Down Expand Up @@ -113,7 +114,7 @@ extension ItemListView {
.disabled(availableFolders.isEmpty)

Button("bound_books_create_button") {
let suggestedFolderName = model.selectedItems.first?.title ?? ""
let suggestedFolderName = ((model.selectedItems.first?.title ?? "") as NSString).deletingPathExtension
folderInput.prepareForBound(title: suggestedFolderName, placeholder: suggestedFolderName)
activeAlert = nil
Task { @MainActor in
Expand Down
2 changes: 1 addition & 1 deletion BookPlayer/Library/ItemList/ItemListViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -512,7 +512,7 @@ extension ItemListViewModel {
let gotAccess = url.startAccessingSecurityScopedResource()
guard gotAccess else { continue }

if DataManager.isAppOwnFolder(url) {
if DataManager.isInDocumentsFolder(url) {
skippedOwnFiles += 1
url.stopAccessingSecurityScopedResource()
continue
Expand Down
5 changes: 5 additions & 0 deletions BookPlayer/Library/ItemList/LibraryRootView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,11 @@ struct LibraryRootView: View {
UserDefaults.standard.removeObject(forKey: Constants.UserDefaults.showPlayer)
playerState.showPlayer = true
}
} catch BPPlayerError.fileMissing {
// Silent preload: if the last-played file is missing on disk,
// swallow the error. The user will see the proper alert if/when
// they explicitly try to play this book. Surfacing it here would
// race with other cold-launch presentations (e.g. the import sheet).
} catch {
loadingState.error = error
}
Expand Down
6 changes: 3 additions & 3 deletions BookPlayer/Library/ItemList/Models/FolderCreationInput.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ struct FolderCreationInput {
type = .bound
}

mutating func prepareForFolder(placeholder suggestedPlaceholder: String = "") {
name = ""
placeholder = suggestedPlaceholder
mutating func prepareForFolder(title: String? = nil, placeholder suggestedPlaceholder: String = "") {
name = title ?? ""
placeholder = title ?? suggestedPlaceholder
type = .folder
}
}
2 changes: 1 addition & 1 deletion BookPlayer/MediaServerIntegration/BPNavigation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import SwiftUI

@MainActor
final class BPNavigation: ObservableObject {
var dismiss: DismissAction?
var dismiss: (() -> Void)?

@Published var path = NavigationPath()

Expand Down
101 changes: 58 additions & 43 deletions BookPlayer/Settings/Sections/DebugFileTransferable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,64 +9,79 @@
import BookPlayerKit
import RevenueCat
import SwiftUI
import UniformTypeIdentifiers

struct DebugFileDocument: FileDocument {
static var readableContentTypes: [UTType] { [.plainText] }
let data: Data

init(data: Data) {
self.data = data
}

init(configuration: ReadConfiguration) throws {
self.data = configuration.file.regularFileContents ?? Data()
}

func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
FileWrapper(regularFileWithContents: data)
}
}

struct DebugFileTransferable: Transferable {
static var transferRepresentation: some TransferRepresentation {
DataRepresentation(exportedContentType: .text) { file in
let syncService = file.syncService
let libraryService = file.libraryService

var remoteIdentifiers: [String]?
var syncJobsInformation: String?
var syncError: String?

// Always get sync state information regardless of isActive
syncJobsInformation = await file.getSyncOperationsInformation()

if syncService.isActive {
do {
remoteIdentifiers = try await syncService.fetchSyncedIdentifiers()
} catch {
syncError = "Error fetching remote identifiers: \(error.localizedDescription)"
}
await file.generateDebugData()
}
.suggestedFileName { _ in
return "bookplayer_debug_information.txt"
}
}

let libraryService: LibraryService
let accountService: AccountService
let syncService: SyncService

func generateDebugData() async -> Data {
var remoteIdentifiers: [String]?
var syncError: String?

let syncJobsInformation = await getSyncOperationsInformation()

if syncService.isActive {
do {
remoteIdentifiers = try await syncService.fetchSyncedIdentifiers()
} catch {
syncError = "Error fetching remote identifiers: \(error.localizedDescription)"
}
}

let localidentifiers = libraryService.fetchIdentifiers()
let localidentifiers = libraryService.fetchIdentifiers()

var libraryRepresentation = file.getLibraryRepresentation(
var libraryRepresentation = getLibraryRepresentation(
localidentifiers: localidentifiers,
remoteIdentifiers: remoteIdentifiers
)

if let remoteIdentifiers,
let remoteOnlyInfo = getRemoteOnlyInformation(
localidentifiers: localidentifiers,
remoteIdentifiers: remoteIdentifiers
)
{
libraryRepresentation += remoteOnlyInfo
}

if let remoteIdentifiers,
let remoteOnlyInfo = file.getRemoteOnlyInformation(
localidentifiers: localidentifiers,
remoteIdentifiers: remoteIdentifiers
)
{
libraryRepresentation += remoteOnlyInfo
}

libraryRepresentation += file.getStorageBreakdown()

if let syncJobsInformation {
libraryRepresentation += syncJobsInformation
}
libraryRepresentation += getStorageBreakdown()

if let syncError {
libraryRepresentation += "\n\n⚠️ Sync Error:\n\(syncError)\n"
}
libraryRepresentation += syncJobsInformation

return libraryRepresentation.data(using: .utf8)!
if let syncError {
libraryRepresentation += "\n\n⚠️ Sync Error:\n\(syncError)\n"
}
.suggestedFileName { _ in
return "bookplayer_debug_information.txt"
}
}

let libraryService: LibraryService
let accountService: AccountService
let syncService: SyncService
return libraryRepresentation.data(using: .utf8)!
}

/// Get a representation of the library like with the `tree` command
/// Note: For the first status, '✓' means the backing file exists, and '𐄂' that it's missing locally,
Expand Down
70 changes: 55 additions & 15 deletions BookPlayer/Settings/Sections/SettingsSupportSectionView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,20 @@ import BookPlayerKit
import MessageUI
import RevenueCat
import SwiftUI
import UniformTypeIdentifiers

struct SettingsSupportSectionView: View {
var accessLevel: AccessLevel
@EnvironmentObject var theme: ThemeViewModel
@Environment(\.libraryService) private var libraryService
@Environment(\.accountService) private var accountService
@Environment(\.syncService) private var syncService
@Environment(\.loadingState) private var loadingState
@Environment(\.openURL) private var openURL

@State private var isExportingDebugFile = false
@State private var debugDocument: DebugFileDocument?

let supportEmail = "support@bookplayer.app"

var sendEmail: () -> Void
Expand All @@ -43,21 +48,7 @@ struct SettingsSupportSectionView: View {
}
.buttonStyle(.borderless)

ShareLink(
item: DebugFileTransferable(
libraryService: libraryService,
accountService: accountService,
syncService: syncService
),
preview: SharePreview(
"bookplayer_debug_information.txt",
image: Image(systemName: "text.page")
)
) {
Text("settings_share_debug_information")
.bpFont(.body)
}
.foregroundStyle(theme.primaryColor)
debugInfoButton
Button {
let url = URL(string: "https://github.com/TortugaPower/BookPlayer")!
openURL(url)
Expand Down Expand Up @@ -85,6 +76,55 @@ struct SettingsSupportSectionView: View {
}
}

@ViewBuilder
private var debugInfoButton: some View {
let file = DebugFileTransferable(
libraryService: libraryService,
accountService: accountService,
syncService: syncService
)
// On iOS-app-on-Mac, `ShareLink` + `Transferable` crashes in
// `SHKSaveToFilesSharingService` when routing to "Save to Files" because
// `NSItemProvider.suggestedName` isn't propagated, and `NSSavePanel`
// rejects the nil filename. Use `.fileExporter` with an explicit
// `defaultFilename` to bypass that path.
if ProcessInfo.processInfo.isiOSAppOnMac {
Button {
Task { @MainActor in
loadingState.show = true
let data = await file.generateDebugData()
loadingState.show = false
debugDocument = DebugFileDocument(data: data)
isExportingDebugFile = true
}
} label: {
Text("settings_share_debug_information")
.bpFont(.body)
}
.foregroundStyle(theme.primaryColor)
.fileExporter(
isPresented: $isExportingDebugFile,
document: debugDocument,
contentType: .plainText,
defaultFilename: "bookplayer_debug_information"
) { _ in
debugDocument = nil
}
Comment on lines +105 to +112
} else {
ShareLink(
item: file,
preview: SharePreview(
"bookplayer_debug_information.txt",
image: Image(systemName: "text.page")
)
) {
Text("settings_share_debug_information")
.bpFont(.body)
}
.foregroundStyle(theme.primaryColor)
}
}

private var appVersion: String {
let version =
Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
Expand Down
1 change: 1 addition & 0 deletions BookPlayer/Settings/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ struct SettingsView: View {
SettingsCreditsSectionView()
}
.environment(\.loadingState, loadingState)
.loadingOverlay(loadingState.show)
.navigationTitle("settings_title")
.navigationBarTitleDisplayMode(.inline)
.applyListStyle(with: theme, background: theme.systemBackgroundColor)
Expand Down
Loading
Loading