From c739b8529e990c7dfe84170a202b3d6d718316f8 Mon Sep 17 00:00:00 2001 From: austin-smith Date: Sun, 8 Mar 2026 18:41:06 -0700 Subject: [PATCH 1/2] keep macOS inspector pieces section mounted during loading --- BitDream/Views/macOS/macOSTorrentDetail.swift | 161 +++++++++++++++--- 1 file changed, 139 insertions(+), 22 deletions(-) diff --git a/BitDream/Views/macOS/macOSTorrentDetail.swift b/BitDream/Views/macOS/macOSTorrentDetail.swift index 2059ff2..57bf166 100644 --- a/BitDream/Views/macOS/macOSTorrentDetail.swift +++ b/BitDream/Views/macOS/macOSTorrentDetail.swift @@ -25,11 +25,17 @@ struct macOSTorrentDetail: View { var body: some View { let details = formatTorrentDetails(torrent: torrent) + let piecesSectionState = MacOSTorrentPiecesSectionState.resolve( + status: supplementalStore.status, + payload: supplementalPayload, + shouldDisplayPayload: shouldDisplaySupplementalPayload + ) MacOSTorrentDetailContent( torrent: torrent, details: details, supplementalPayload: supplementalPayload, + piecesSectionState: piecesSectionState, onShowFiles: { isShowingFilesSheet = true }, onShowPeers: { isShowingPeersSheet = true }, onDelete: { showingDeleteConfirmation = true } @@ -182,10 +188,37 @@ struct macOSTorrentDetail: View { } } +internal enum MacOSTorrentPiecesSectionState: Equatable { + case loading + case content(TorrentDetailSupplementalPayload) + case empty + case failed + + static func resolve( + status: TorrentDetailSupplementalLoadStatus, + payload: TorrentDetailSupplementalPayload, + shouldDisplayPayload: Bool + ) -> Self { + guard shouldDisplayPayload else { + return status == .failed ? .failed : .loading + } + + switch status { + case .failed: + return .failed + case .loaded: + return payload.hasRenderablePieceData ? .content(payload) : .empty + case .idle, .loading: + return payload.hasRenderablePieceData ? .content(payload) : .loading + } + } +} + private struct MacOSTorrentDetailContent: View { let torrent: Torrent let details: TorrentDetailsDisplay let supplementalPayload: TorrentDetailSupplementalPayload + let piecesSectionState: MacOSTorrentPiecesSectionState let onShowFiles: () -> Void let onShowPeers: () -> Void let onDelete: () -> Void @@ -250,29 +283,8 @@ private struct MacOSTorrentDetailContent: View { } .padding(.bottom, 8) - if supplementalPayload.pieceCount > 0 && !supplementalPayload.piecesHaveSet.isEmpty { - GroupBox { - VStack(alignment: .leading, spacing: 10) { - macOSSectionHeader("Pieces", icon: "square.grid.2x2") - - VStack(alignment: .leading, spacing: 8) { - PiecesGridView( - piecesHaveSet: supplementalPayload.piecesHaveSet - ) - .frame(maxWidth: .infinity) - - Text( - "\(supplementalPayload.piecesHaveCount) of \(supplementalPayload.pieceCount) pieces • \(formatByteCount(supplementalPayload.pieceSize)) each" - ) - .font(.caption) - .foregroundColor(.secondary) - } - } - .padding(.vertical, 16) - .padding(.horizontal, 20) - } + MacOSTorrentPiecesSection(state: piecesSectionState) .padding(.bottom, 8) - } GroupBox { VStack(alignment: .leading, spacing: 10) { @@ -316,6 +328,105 @@ private struct MacOSTorrentDetailContent: View { } } +private struct MacOSTorrentPiecesSection: View { + private static let contentMinHeight: CGFloat = 96 + + let state: MacOSTorrentPiecesSectionState + + var body: some View { + GroupBox { + VStack(alignment: .leading, spacing: 10) { + macOSSectionHeader("Pieces", icon: "square.grid.2x2") + + sectionContent + .frame(maxWidth: .infinity, alignment: .leading) + .frame(minHeight: Self.contentMinHeight, alignment: .top) + } + .padding(.vertical, 16) + .padding(.horizontal, 20) + } + } + + @ViewBuilder + private var sectionContent: some View { + switch state { + case .loading: + MacOSTorrentPiecesLoadingView() + case .content(let payload): + VStack(alignment: .leading, spacing: 8) { + PiecesGridView( + piecesHaveSet: payload.piecesHaveSet + ) + .frame(maxWidth: .infinity) + + Text( + "\(payload.piecesHaveCount) of \(payload.pieceCount) pieces • \(formatByteCount(payload.pieceSize)) each" + ) + .font(.caption) + .foregroundColor(.secondary) + } + case .empty: + MacOSTorrentPiecesMessageView( + title: "No Piece Data", + message: "Piece availability is not available for this torrent." + ) + case .failed: + MacOSTorrentPiecesMessageView( + title: "Pieces Unavailable", + message: "BitDream couldn't load piece availability for this torrent." + ) + } + } +} + +private struct MacOSTorrentPiecesLoadingView: View { + var body: some View { + VStack(alignment: .leading, spacing: 8) { + RoundedRectangle(cornerRadius: 6) + .fill(Color.secondary.opacity(0.12)) + .frame(maxWidth: .infinity) + .frame(height: 80) + .overlay(alignment: .topLeading) { + VStack(alignment: .leading, spacing: 6) { + RoundedRectangle(cornerRadius: 4) + .frame(width: 180, height: 8) + RoundedRectangle(cornerRadius: 4) + .frame(maxWidth: .infinity) + .frame(height: 8) + RoundedRectangle(cornerRadius: 4) + .frame(width: 240, height: 8) + } + .padding(12) + .foregroundStyle(.secondary.opacity(0.2)) + } + + RoundedRectangle(cornerRadius: 4) + .fill(Color.secondary.opacity(0.12)) + .frame(width: 220, height: 10) + } + .accessibilityElement(children: .ignore) + .accessibilityLabel("Loading pieces") + } +} + +private struct MacOSTorrentPiecesMessageView: View { + let title: String + let message: String + + var body: some View { + VStack(alignment: .leading, spacing: 6) { + Text(title) + .font(.subheadline) + .fontWeight(.semibold) + Text(message) + .font(.caption) + .foregroundColor(.secondary) + .fixedSize(horizontal: false, vertical: true) + } + .frame(maxWidth: .infinity, alignment: .leading) + } +} + // Helper view for consistent detail rows struct DetailRow: View { var label: String @@ -345,6 +456,12 @@ struct DetailRow: View { } } +private extension TorrentDetailSupplementalPayload { + var hasRenderablePieceData: Bool { + pieceCount > 0 && !piecesHaveSet.isEmpty + } +} + // Native macOS-style section header component struct macOSSectionHeader: View { let title: String From bc694a6afdf900692a94e2925e73a7b7599761b0 Mon Sep 17 00:00:00 2001 From: austin-smith Date: Sun, 8 Mar 2026 18:51:13 -0700 Subject: [PATCH 2/2] preserve cached pieces on macOS refresh failure --- BitDream/Views/macOS/macOSTorrentDetail.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BitDream/Views/macOS/macOSTorrentDetail.swift b/BitDream/Views/macOS/macOSTorrentDetail.swift index 57bf166..f645f22 100644 --- a/BitDream/Views/macOS/macOSTorrentDetail.swift +++ b/BitDream/Views/macOS/macOSTorrentDetail.swift @@ -205,7 +205,7 @@ internal enum MacOSTorrentPiecesSectionState: Equatable { switch status { case .failed: - return .failed + return payload.hasRenderablePieceData ? .content(payload) : .failed case .loaded: return payload.hasRenderablePieceData ? .content(payload) : .empty case .idle, .loading: