~ knotless Search/
↑↓ move↵ open⇧↵ all results

grain.social/grain-ios

1 collaborator
0
1
main
120 lines · 3.9 KB Raw History
import NukeUI
import SwiftUI

/// One entry in a browse index — a camera, a place.
///
/// A square with a mosaic of up to four photos behind the name and a count.
/// The mosaic is a sample of what is behind the tile rather than a feed in
/// its own right: the tile is for choosing, and the screen it opens is for
/// looking. Fewer than four photos stretch to fill rather than leaving holes,
/// so a place with one gallery doesn't read as a broken 2x2.
struct BrowseTile: View {
    let title: String
    let meta: String?
    let thumbs: [String]
    let action: () -> Void

    var body: some View {
        Button(action: action) {
            Color(.secondarySystemBackground)
                .aspectRatio(1, contentMode: .fit)
                .overlay { Mosaic(thumbs: Array(thumbs.prefix(4))) }
                .overlay(alignment: .bottomLeading) { label }
                .clipped()
                .contentShape(Rectangle())
        }
        .buttonStyle(.plain)
        .accessibilityLabel(meta.map { "\(title), \($0)" } ?? title)
    }

    /// The name has to survive whatever photo is behind it, so the scrim is
    /// part of the tile. Deep and tall on purpose: a lot of what gets posted
    /// here is high-key film, and a shallow scrim leaves white on white.
    private var label: some View {
        VStack(alignment: .leading, spacing: 2) {
            Text(title)
                .font(.subheadline.weight(.bold))
                .lineLimit(1)
            if let meta {
                Text(meta)
                    .font(.caption)
                    .foregroundStyle(.white.opacity(0.72))
                    .lineLimit(1)
            }
        }
        .foregroundStyle(.white)
        .frame(maxWidth: .infinity, alignment: .leading)
        .padding(.top, 34)
        .padding(.horizontal, 12)
        .padding(.bottom, 11)
        .background(
            LinearGradient(
                stops: [
                    .init(color: .clear, location: 0),
                    .init(color: .black.opacity(0.55), location: 0.55),
                    .init(color: .black.opacity(0.85), location: 1),
                ],
                startPoint: .top,
                endPoint: .bottom
            )
        )
    }
}

/// Up to four photos in a 2x2, with no gutter between them: the grid between
/// tiles has one, and matching it here made two adjacent tiles read as one
/// strip of four columns.
///
/// Two splits across, not down — a pair of full-width bands still reads as
/// one tile, where two tall slivers read as two. Three puts the first down the
/// left side and stacks the other two beside it.
private struct Mosaic: View {
    let thumbs: [String]

    var body: some View {
        switch thumbs.count {
        case 0:
            Color.clear
        case 1:
            photo(thumbs[0])
        case 2:
            VStack(spacing: 0) {
                photo(thumbs[0])
                photo(thumbs[1])
            }
        case 3:
            HStack(spacing: 0) {
                photo(thumbs[0])
                VStack(spacing: 0) {
                    photo(thumbs[1])
                    photo(thumbs[2])
                }
            }
        default:
            VStack(spacing: 0) {
                HStack(spacing: 0) {
                    photo(thumbs[0])
                    photo(thumbs[1])
                }
                HStack(spacing: 0) {
                    photo(thumbs[2])
                    photo(thumbs[3])
                }
            }
        }
    }

    private func photo(_ url: String) -> some View {
        Color.clear
            .overlay {
                LazyImage(url: URL(string: url)) { state in
                    if let image = state.image {
                        image.resizable().scaledToFill()
                    } else {
                        Color(.secondarySystemBackground)
                    }
                }
            }
            .clipped()
    }
}