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