Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>feat/music-streaming
parent
8f4e330c92
commit
a0b95681e2
@ -0,0 +1,60 @@ |
||||
import SwiftUI |
||||
|
||||
struct FlowLayout: Layout { |
||||
var spacing: CGFloat = 6 |
||||
|
||||
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { |
||||
let rows = computeRows(proposal: proposal, subviews: subviews) |
||||
let height = rows.enumerated().reduce(CGFloat.zero) { total, entry in |
||||
let (i, row) = entry |
||||
let rowHeight = row.map { $0.size.height }.max() ?? 0 |
||||
return total + rowHeight + (i > 0 ? spacing : 0) |
||||
} |
||||
return CGSize(width: proposal.width ?? 0, height: height) |
||||
} |
||||
|
||||
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) { |
||||
let rows = computeRows(proposal: proposal, subviews: subviews) |
||||
var y = bounds.minY |
||||
|
||||
for row in rows { |
||||
let rowHeight = row.map { $0.size.height }.max() ?? 0 |
||||
var x = bounds.minX |
||||
|
||||
for item in row { |
||||
item.subview.place( |
||||
at: CGPoint(x: x, y: y), |
||||
proposal: ProposedViewSize(item.size) |
||||
) |
||||
x += item.size.width + spacing |
||||
} |
||||
y += rowHeight + spacing |
||||
} |
||||
} |
||||
|
||||
private struct LayoutItem { |
||||
let subview: LayoutSubview |
||||
let size: CGSize |
||||
} |
||||
|
||||
private func computeRows(proposal: ProposedViewSize, subviews: Subviews) -> [[LayoutItem]] { |
||||
let maxWidth = proposal.width ?? .infinity |
||||
var rows: [[LayoutItem]] = [[]] |
||||
var currentRowWidth: CGFloat = 0 |
||||
|
||||
for subview in subviews { |
||||
let size = subview.sizeThatFits(.unspecified) |
||||
let widthWithSpacing = currentRowWidth > 0 ? size.width + spacing : size.width |
||||
|
||||
if currentRowWidth + widthWithSpacing > maxWidth, !rows[rows.count - 1].isEmpty { |
||||
rows.append([]) |
||||
currentRowWidth = 0 |
||||
} |
||||
|
||||
rows[rows.count - 1].append(LayoutItem(subview: subview, size: size)) |
||||
currentRowWidth += currentRowWidth > 0 ? size.width + spacing : size.width |
||||
} |
||||
|
||||
return rows |
||||
} |
||||
} |
||||
@ -0,0 +1,59 @@ |
||||
import SwiftUI |
||||
|
||||
struct PlaylistBarView: View { |
||||
var playlists: [Playlist] |
||||
var selectedPlaylist: Playlist? |
||||
var onSelect: (Playlist) -> Void |
||||
var onDeselect: () -> Void |
||||
var onRename: (Playlist) -> Void |
||||
var onDelete: (Playlist) -> Void |
||||
|
||||
var body: some View { |
||||
if !playlists.isEmpty { |
||||
FlowLayout(spacing: 6) { |
||||
ForEach(playlists) { playlist in |
||||
PlaylistButton( |
||||
name: playlist.name, |
||||
isSelected: selectedPlaylist?.id == playlist.id, |
||||
action: { |
||||
if selectedPlaylist?.id == playlist.id { |
||||
onDeselect() |
||||
} else { |
||||
onSelect(playlist) |
||||
} |
||||
} |
||||
) |
||||
.contextMenu { |
||||
Button("Rename...") { onRename(playlist) } |
||||
Button("Delete") { onDelete(playlist) } |
||||
} |
||||
} |
||||
} |
||||
.padding(.horizontal, 12) |
||||
.padding(.vertical, 6) |
||||
} |
||||
} |
||||
} |
||||
|
||||
private struct PlaylistButton: View { |
||||
let name: String |
||||
let isSelected: Bool |
||||
let action: () -> Void |
||||
|
||||
var body: some View { |
||||
Button(action: action) { |
||||
Text(name) |
||||
.font(.system(size: 11)) |
||||
.padding(.horizontal, 10) |
||||
.padding(.vertical, 5) |
||||
.background(isSelected ? Color.accentColor.opacity(0.2) : Color.secondary.opacity(0.1)) |
||||
.foregroundStyle(isSelected ? Color.accentColor : .secondary) |
||||
.overlay( |
||||
RoundedRectangle(cornerRadius: 4) |
||||
.stroke(isSelected ? Color.accentColor : Color.secondary.opacity(0.3), lineWidth: 1) |
||||
) |
||||
.cornerRadius(4) |
||||
} |
||||
.buttonStyle(.plain) |
||||
} |
||||
} |
||||
Loading…
Reference in new issue