diff --git a/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/Extensions/DynamicTypeSize.swift b/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/Extensions/DynamicTypeSize.swift new file mode 100644 index 00000000..d2f3391c --- /dev/null +++ b/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/Extensions/DynamicTypeSize.swift @@ -0,0 +1,19 @@ +import SwiftUI + +public extension DynamicTypeSize { + var usesPrimaryCardLayout: Bool { + switch self { + case .xSmall, + .small, + .medium, + .large, + .xLarge, + .xxLarge, + .xxxLarge: + return true + + default: + return false + } + } +} diff --git a/MacMagazine/Features/PodcastLibrary/Sources/Podcast/Views/PodcastCardView.swift b/MacMagazine/Features/PodcastLibrary/Sources/Podcast/Views/PodcastCardView.swift index 24edceae..64ba8bed 100644 --- a/MacMagazine/Features/PodcastLibrary/Sources/Podcast/Views/PodcastCardView.swift +++ b/MacMagazine/Features/PodcastLibrary/Sources/Podcast/Views/PodcastCardView.swift @@ -1,10 +1,189 @@ import FeedLibrary +import MacMagazineLibrary import SwiftUI import UIComponentsLibrary +import UtilityLibrary +import YouTubeLibrary -struct PodcastCardView: View { +struct AdaptivePodcastCardView: View { + @Environment(\.dynamicTypeSize) private var dynamicTypeSize let podcast: PodcastDB let onPlay: () -> Void + + var body: some View { + Group { + if dynamicTypeSize.usesPrimaryCardLayout { + GlassPodcastCardView( + podcast: podcast, + onPlay: onPlay + ) + } else { + PodcastCardView( + podcast: podcast, + onPlay: onPlay + ) + } + } + } +} + +// MARK: - New Card + +private struct GlassPodcastCardView: View { + let podcast: PodcastDB + let onPlay: () -> Void + + @Environment(\.theme) private var theme + + @State private var cardWidth: CGFloat = 0 + @State private var thumbnailSize: CGSize = .zero + + private var density: CardDensity { .from(width: cardWidth) } + + var body: some View { + Button(action: onPlay) { + + ZStack(alignment: .topTrailing) { + cardBase + topButtons + .padding(10) + } + .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous)) + .background( + GeometryReader { geo in + Color.clear + .onAppear { cardWidth = geo.size.width } + .onChange(of: geo.size) { _, newValue in + cardWidth = newValue.width + } + } + ) + } + .buttonStyle(.plain) + } + + // MARK: - Card base (thumbnail + bottom content) + + private var cardBase: some View { + ZStack(alignment: .bottom) { + thumbnail + content + } + } + + var fallbackBackground: LinearGradient { + LinearGradient( + gradient: Gradient(stops: [ + .init(color: .black.opacity(0.40), location: 0.0), + .init(color: .black.opacity(0.85), location: 1.0) + ]), + startPoint: .top, + endPoint: .bottom + ) + } + + // MARK: - Thumbnail + + @ViewBuilder + var thumbnail: some View { + if let url = URL(string: podcast.artworkURL) { + CachedAsyncImage(image: url) + } + } + + // MARK: - Bottom content block + + private var content: some View { + VStack(alignment: .leading, spacing: 6) { + + Text(podcast.title) + .font(density.titleFont) + .multilineTextAlignment(.leading) + .lineLimit(density.titleLineLimit) + .foregroundStyle(.white) + .shadow(color: .white.opacity(0.6), radius: 2, x: 0, y: 1) + + HStack(alignment: .firstTextBaseline, spacing: 8) { + dateRow + .foregroundStyle(.white.opacity(0.9)) + .shadow(color: .white.opacity(0.6), radius: 2, x: 0, y: 1) + + Spacer(minLength: 4) + + duration + .lineLimit(1) + .minimumScaleFactor(0.8) + .layoutPriority(0) + } + .font(.caption2) + .shadow(color: .black.opacity(0.7), radius: 2, x: 0, y: 1) + } + .padding(.horizontal, 12) + .padding(.vertical, 10) + .padding(.top, density == .compact ? 20 : 30) + .frame(maxWidth: .infinity, alignment: .leading) + .background(gradientOverlay) + } + + var gradientOverlay: LinearGradient { + LinearGradient( + gradient: Gradient(stops: [ + .init(color: .black.opacity(0.0), location: 0.0), + .init(color: .black.opacity(0.60), location: 0.4), + .init(color: .black.opacity(0.95), location: 1.0) + ]), + startPoint: .top, + endPoint: .bottom + ) + } + + @ViewBuilder var topButtons: some View { + +// FavoriteShareContainer( +// favoriteView: PodcastFavoriteButton(content: data), +// shareView: ShareButton(content: data) +// ) + + if let urlPodCast = URL(string: podcast.podcastURL) { + UtilityLibrary.ShareButton(title: podcast.title, + url: urlPodCast) + .buttonStyle(.plain) + .tint(.primary) + .font(.system(size: 16)) + .padding(.horizontal, 14) + .padding(.vertical, 10) + .padding(.bottom, 2) + .glassEffect() + } else { + EmptyView() + } + } + + private var dateRow: some View { + HStack(spacing: 4) { + Image(systemName: "calendar") + Text(podcast.pubDate.format(using: .dateOnly)) + } + } + + private var duration: some View { + Text(podcast.duration) + .font(.caption) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .foregroundColor(.white) + .lineLimit(1) + .minimumScaleFactor(0.8) + .glassEffect(.clear, in: .rect(cornerRadius: 6)) + } +} + +// MARK: - Old Card + +private struct PodcastCardView: View { + let podcast: PodcastDB + let onPlay: () -> Void + @Environment(\.theme) private var theme var body: some View { @@ -16,33 +195,38 @@ struct PodcastCardView: View { } .cornerRadius(12) } -} -private extension PodcastCardView { @ViewBuilder var thumbnail: some View { if let url = URL(string: podcast.artworkURL) { CachedAsyncImage(image: url) .overlay { - VStack { - HStack { - Text(podcast.duration) - .font(.caption) - .padding(6) - .foregroundColor(.white) - .background(Color(red: 0.0, green: 0.0, blue: 0.0, opacity: 0.80)) - .cornerRadius(6) + ZStack { + VStack { + HStack { + Text(podcast.duration) + .font(.caption) + .padding(6) + .foregroundColor(.white) + .background( + Color( + red: 0.0, + green: 0.0, + blue: 0.0, + opacity: 0.80 + ) + ) + .cornerRadius(6) + Spacer() + } Spacer() } - Spacer() + .padding([.top, .leading, .trailing], 10) } - .padding([.top, .leading, .trailing], 10) } } } -} -private extension PodcastCardView { var metadata: some View { VStack(spacing: 0) { HStack(spacing: 0) { @@ -69,3 +253,45 @@ private extension PodcastCardView { .background(.background) } } + +#if DEBUG +#Preview { + @Previewable @State var playing = false + @Previewable @Environment(\.theme) var theme + + let mockPodcast = PodcastDB( + postId: "1", + title: "MacMagazine no Ar #123: Especial WWDC 2024", + subtitle: "Neste episódio especial, discutimos todas as novidades anunciadas na WWDC 2024", + pubDate: Date(), + artworkURL: "https://macmagazine.com.br/wp-content/uploads/2025/11/28-podcast-1260x709.jpg", + podcastURL: "https://traffic.libsyn.com/secure/macmagazine/MacMagazine_no_Ar_001.mp3", + podcastSize: 50_000_000, + duration: "45:30", + podcastFrame: "", + favorite: false, + playable: true + ) + + ZStack { + theme.main.background.color + .edgesIgnoringSafeArea(.all) + + VStack { + PodcastCardView( + podcast: mockPodcast, + onPlay: { playing.toggle() } + ) + .padding() + + GlassPodcastCardView( + podcast: mockPodcast, + onPlay: { playing.toggle() } + ) + .padding() + } + } + .environment(\.theme, ThemeColor()) + +} +#endif diff --git a/MacMagazine/Features/PodcastLibrary/Sources/Podcast/Views/PodcastView.swift b/MacMagazine/Features/PodcastLibrary/Sources/Podcast/Views/PodcastView.swift index b61293a0..5b9385b6 100644 --- a/MacMagazine/Features/PodcastLibrary/Sources/Podcast/Views/PodcastView.swift +++ b/MacMagazine/Features/PodcastLibrary/Sources/Podcast/Views/PodcastView.swift @@ -78,7 +78,7 @@ extension PodcastView { quantity: search.isEmpty ? podcasts.count : 0, content: { ForEach(podcasts) { podcast in - PodcastCardView(podcast: podcast) { + AdaptivePodcastCardView(podcast: podcast) { playerManager.loadPodcast(podcast) } } diff --git a/MacMagazine/Features/VideosLibrary/Sources/Videos/Card/GlassCardView.swift b/MacMagazine/Features/VideosLibrary/Sources/Videos/Card/GlassCardView.swift index 02c6478d..4a06cbb7 100644 --- a/MacMagazine/Features/VideosLibrary/Sources/Videos/Card/GlassCardView.swift +++ b/MacMagazine/Features/VideosLibrary/Sources/Videos/Card/GlassCardView.swift @@ -5,20 +5,27 @@ import UIComponentsLibrary import UtilityLibrary import YouTubeLibrary +// MARK: - GlassCard + +@MainActor +public struct GlassCard: VideoCard { + public var accessibilityLabels: [CardLabel]? + public var accessibilityButtons: [CardButton]? + + public func makeBody(data: VideoDB) -> some View { + GlassCardView(data: data) + } +} + @MainActor struct GlassCardView: View { @Namespace var namespace - @Environment(\.sizeCategory) private var sizeCategory let data: VideoDB @State private var cardWidth: CGFloat = .zero @State private var thumbnailSize: CGSize = .zero - private var isAccessibilityCategory: Bool { - sizeCategory.isAccessibilityCategory - } - private var density: CardDensity { .from(width: cardWidth) } // MARK: - Body