Files
yattee/Yattee/Views/Components/BookmarkRowView.swift
Arkadiusz Fal 3c04b8540f Fix 5 TestFlight crash types from builds 250-254
- Fix BGTaskScheduler assertion crash on Mac Catalyst by guarding all
  iOS background task APIs with isMacCatalystApp check
- Fix iPad popover crash in UIPopoverPresentationController by adding
  .presentationCompactAdaptation(.sheet) to all 27 confirmationDialogs
- Fix SwiftData assertion crash when accessing deleted Bookmark model
  properties during SwiftUI hit testing in BookmarkRowView
- Fix UICollectionView invalid item count crash on queue swipe-to-delete
  by using ID-based removal with withAnimation instead of stale index
- Fix Range crash in storyboard download when storyboardCount is zero
2026-03-27 17:52:08 +01:00

140 lines
3.8 KiB
Swift

//
// BookmarkRowView.swift
// Yattee
//
// Row view for displaying bookmarked videos with tags and notes.
//
import SwiftData
import SwiftUI
/// Row view for displaying bookmarked videos.
/// Uses VideoRowView for consistent presentation with additional tags/notes display.
/// Automatically handles DeArrow integration.
/// Supports optional queue context for auto-play functionality.
struct BookmarkRowView: View {
@Environment(\.appEnvironment) private var appEnvironment
let bookmark: Bookmark
var style: VideoRowStyle = .regular
var watchProgress: Double? = nil
let onRemove: () -> Void
// Queue context (optional, enables auto-play when provided)
var queueSource: QueueSource? = nil
var sourceLabel: String? = nil
var videoList: [Video]? = nil
var videoIndex: Int? = nil
var loadMoreVideos: LoadMoreVideosCallback? = nil
private var accentColor: Color {
appEnvironment?.settingsManager.accentColor.color ?? .accentColor
}
private var isBookmarkValid: Bool {
bookmark.modelContext != nil && !bookmark.isDeleted
}
private var video: Video {
bookmark.toVideo()
}
private var hasTags: Bool {
guard isBookmarkValid else { return false }
return !bookmark.tags.isEmpty
}
private var hasNote: Bool {
guard isBookmarkValid else { return false }
if let note = bookmark.note, !note.isEmpty {
return true
}
return false
}
private var hasMetadata: Bool {
hasTags || hasNote
}
/// Leading padding to align tags/notes with the text content (past thumbnail)
private var metadataLeadingPadding: CGFloat {
let hstackSpacing: CGFloat = 12
return style.thumbnailWidth + hstackSpacing
}
var body: some View {
if isBookmarkValid {
content
}
}
private var content: some View {
VStack(alignment: .leading, spacing: 0) {
// Video row content
videoRowContent
// Tags and notes (text-aligned, past thumbnail) - one line
if hasMetadata && style != .compact {
bookmarkMetadataLine
.padding(.leading, metadataLeadingPadding)
.padding(.top, 4)
}
}
.tappableVideo(
video,
queueSource: queueSource,
sourceLabel: sourceLabel,
videoList: videoList,
videoIndex: videoIndex,
loadMoreVideos: loadMoreVideos
)
.videoContextMenu(
video: video,
customActions: [
VideoContextAction(
String(localized: "home.bookmarks.remove"),
systemImage: "trash",
role: .destructive,
action: onRemove
)
],
context: .bookmarks
)
}
@ViewBuilder
private var videoRowContent: some View {
VideoRowView(
video: video,
style: style,
watchProgress: watchProgress
)
}
/// Single line with tags and note separated by dot
@ViewBuilder
private var bookmarkMetadataLine: some View {
HStack(spacing: 4) {
if hasTags {
BookmarkTagsView(
tags: bookmark.tags,
maxVisible: style == .large ? 4 : 3
)
}
if hasTags && hasNote {
Text(verbatim: "·")
.font(.caption2)
.foregroundStyle(.secondary)
}
if hasNote {
Text(bookmark.note!)
.font(.caption2)
.foregroundStyle(.secondary)
.lineLimit(1)
}
}
}
}