From 84a98ed0a6631923c21697f2f8155d9fadabbd30 Mon Sep 17 00:00:00 2001 From: "Renato Ferraz C. B. Ferreira" Date: Thu, 4 Dec 2025 17:50:13 -0300 Subject: [PATCH 1/4] Replaces video card with glass card style Updates the video list to use a glass card design, offering a modern and visually appealing aesthetic. The new glass card includes: - Enhanced visual elements and styling - Adjusted layout for improved content presentation - Uses new extension methods to correctly format the data Also adds accessibility duration helpers for the new card style. --- .gitignore | 2 + .../AccessibilityUtils.swift | 58 ++++ .../MacMagazineLibrary/CardDensity.swift | 37 +++ .../Extensions/LinearGradient.swift | 26 ++ .../Extensions/VideoDB.swift | 16 + .../MacMagazineLibrary/Extensions/View.swift | 19 ++ .../Sources/MacMagazineLibrary/Utils.swift | 2 + .../Accessories/GlassFavoriteButton.swift | 22 ++ .../Videos/Accessories/GlassShareButton.swift | 22 ++ .../Sources/Videos/Card/GlassCard.swift | 16 + .../Sources/Videos/Card/GlassCardView.swift | 294 ++++++++++++++++++ .../Sources/Videos/VideosView.swift | 10 +- 12 files changed, 521 insertions(+), 3 deletions(-) create mode 100644 MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/AccessibilityUtils.swift create mode 100644 MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/CardDensity.swift create mode 100644 MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/Extensions/LinearGradient.swift create mode 100644 MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/Extensions/VideoDB.swift create mode 100644 MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/Extensions/View.swift create mode 100644 MacMagazine/Features/VideosLibrary/Sources/Videos/Accessories/GlassFavoriteButton.swift create mode 100644 MacMagazine/Features/VideosLibrary/Sources/Videos/Accessories/GlassShareButton.swift create mode 100644 MacMagazine/Features/VideosLibrary/Sources/Videos/Card/GlassCard.swift create mode 100644 MacMagazine/Features/VideosLibrary/Sources/Videos/Card/GlassCardView.swift diff --git a/.gitignore b/.gitignore index 6bd7619d..932bcf3a 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,5 @@ build Package.resolved !*.xcodeproj/**/Package.resolved MacMagazine/MacMagazine.xcodeproj/project.xcworkspace/xcuserdata + +MacMagazine/MacMagazine.xcodeproj/xcuserdata/renato_ferreira.xcuserdatad/xcdebugger/ diff --git a/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/AccessibilityUtils.swift b/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/AccessibilityUtils.swift new file mode 100644 index 00000000..df820667 --- /dev/null +++ b/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/AccessibilityUtils.swift @@ -0,0 +1,58 @@ +// +// MMAccessibilityUtils.swift +// MacMagazineLibrary +// +// Created by Renato Ferraz Castelo Branco Ferreira on 04/12/25. +// + + +public enum AccessibilityUtils { + public static func spokenDuration(from formatted: String) -> String { + let parts = formatted.split(separator: ":").map { String($0) } + + var hours = 0 + var minutes = 0 + var seconds = 0 + + if parts.count == 3 { + hours = Int(parts[0]) ?? 0 + minutes = Int(parts[1]) ?? 0 + seconds = Int(parts[2]) ?? 0 + } else if parts.count == 2 { + minutes = Int(parts[0]) ?? 0 + seconds = Int(parts[1]) ?? 0 + } else if parts.count == 1 { + seconds = Int(parts[0]) ?? 0 + } + + var chunks: [String] = [] + if hours > 0 { + chunks.append("\(hours) " + (hours == 1 ? "hora" : "horas")) + } + if minutes > 0 { + chunks.append("\(minutes) " + (minutes == 1 ? "minuto" : "minutos")) + } + if seconds > 0 { + chunks.append("\(seconds) " + (seconds == 1 ? "segundo" : "segundos")) + } + + if chunks.isEmpty { + return "0 segundos" + } + + if chunks.count == 1 { + return chunks[0] + } else if chunks.count == 2 { + return chunks.joined(separator: " e ") + } else { + return chunks[0] + ", " + chunks[1] + " e " + chunks[2] + } + } + + public static func join(_ parts: [String?]) -> String { + parts + .compactMap { $0?.trimmingCharacters(in: .whitespacesAndNewlines) } + .filter { !$0.isEmpty } + .joined(separator: ", ") + } +} diff --git a/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/CardDensity.swift b/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/CardDensity.swift new file mode 100644 index 00000000..d53b6c9f --- /dev/null +++ b/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/CardDensity.swift @@ -0,0 +1,37 @@ +import SwiftUI + +public enum CardDensity { + case compact + case regular + case spacious + + public static func from(width: CGFloat) -> CardDensity { + switch width { + case ..<260: return .compact + case ..<340: return .regular + default: return .spacious + } + } + + public var titleFont: Font { + switch self { + case .compact: + return .subheadline + case .regular, .spacious: + return .headline + } + } + + public var isVisible: Bool { + self != .compact + } + + public var titleLineLimit: Int { + switch self { + case .compact, .regular: + return 1 + case .spacious: + return 2 + } + } +} diff --git a/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/Extensions/LinearGradient.swift b/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/Extensions/LinearGradient.swift new file mode 100644 index 00000000..3714d1de --- /dev/null +++ b/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/Extensions/LinearGradient.swift @@ -0,0 +1,26 @@ +import SwiftUI + +public extension LinearGradient { + static 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 + ) + } + + static 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 + ) + } +} diff --git a/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/Extensions/VideoDB.swift b/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/Extensions/VideoDB.swift new file mode 100644 index 00000000..a1409580 --- /dev/null +++ b/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/Extensions/VideoDB.swift @@ -0,0 +1,16 @@ +import YouTubeLibrary +import SwiftUI + +public extension VideoDB { + var formattedDateShort: String { + pubDate.formattedDate(using: "dd/MM/yy") + } + + var viewsText: String { + "\(views.formattedBigNumber) visualizações" + } + + var likesText: String { + "\(likes.formattedBigNumber) curtidas" + } +} diff --git a/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/Extensions/View.swift b/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/Extensions/View.swift new file mode 100644 index 00000000..4ace4126 --- /dev/null +++ b/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/Extensions/View.swift @@ -0,0 +1,19 @@ +// +// View+VideoLibrary.swift +// VideosLibrary +// +// Created by Renato Ferraz Castelo Branco Ferreira on 03/12/25. +// + +import SwiftUI + +public extension View { + @ViewBuilder + func applyTint(_ tint: Color?) -> some View { + if let tint { + self.tint(tint) + } else { + self + } + } +} diff --git a/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/Utils.swift b/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/Utils.swift index ed5289ff..c36b5fef 100644 --- a/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/Utils.swift +++ b/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/Utils.swift @@ -3,8 +3,10 @@ import Foundation import SafariServices import UIKit +import SwiftUI public class Utils { + @MainActor static public func openInSafari(_ url: URL) { if url.scheme?.lowercased().contains("http") ?? false { diff --git a/MacMagazine/Features/VideosLibrary/Sources/Videos/Accessories/GlassFavoriteButton.swift b/MacMagazine/Features/VideosLibrary/Sources/Videos/Accessories/GlassFavoriteButton.swift new file mode 100644 index 00000000..94b2e6e5 --- /dev/null +++ b/MacMagazine/Features/VideosLibrary/Sources/Videos/Accessories/GlassFavoriteButton.swift @@ -0,0 +1,22 @@ +import SwiftUI +import YouTubeLibrary +import StorageLibrary + +public struct GlassFavoriteButton: View { + let content: VideoDB + let tint: Color? + + public init(content: VideoDB, tint: Color? = nil) { + self.content = content + self.tint = tint + } + + public var body: some View { + FavoriteButton(content: content) + .buttonStyle(.plain) + .font(.system(size: 16)) + .frame(width: 35, height: 35) + .glassEffect() + .applyTint(tint) + } +} diff --git a/MacMagazine/Features/VideosLibrary/Sources/Videos/Accessories/GlassShareButton.swift b/MacMagazine/Features/VideosLibrary/Sources/Videos/Accessories/GlassShareButton.swift new file mode 100644 index 00000000..e66a34ca --- /dev/null +++ b/MacMagazine/Features/VideosLibrary/Sources/Videos/Accessories/GlassShareButton.swift @@ -0,0 +1,22 @@ +import SwiftUI +import StorageLibrary +import YouTubeLibrary + +public struct GlassShareButton: View { + let content: VideoDB + let tint: Color? + + public init(content: VideoDB, tint: Color? = nil) { + self.content = content + self.tint = tint + } + + public var body: some View { + ShareButton(content: content) + .buttonStyle(.plain) + .font(.system(size: 16)) + .frame(width: 35, height: 35) + .glassEffect() + .applyTint(tint) + } +} diff --git a/MacMagazine/Features/VideosLibrary/Sources/Videos/Card/GlassCard.swift b/MacMagazine/Features/VideosLibrary/Sources/Videos/Card/GlassCard.swift new file mode 100644 index 00000000..2d8f6076 --- /dev/null +++ b/MacMagazine/Features/VideosLibrary/Sources/Videos/Card/GlassCard.swift @@ -0,0 +1,16 @@ +import SwiftUI +import StorageLibrary +import YouTubeLibrary + +@MainActor +public struct GlassCard: VideoCard { + public let buttonColor: Color? + + public init(buttonColor: Color? = nil) { + self.buttonColor = buttonColor + } + + public func makeBody(data: VideoDB) -> some View { + GlassCardView(data: data, buttonColor: buttonColor) + } +} diff --git a/MacMagazine/Features/VideosLibrary/Sources/Videos/Card/GlassCardView.swift b/MacMagazine/Features/VideosLibrary/Sources/Videos/Card/GlassCardView.swift new file mode 100644 index 00000000..d609acd1 --- /dev/null +++ b/MacMagazine/Features/VideosLibrary/Sources/Videos/Card/GlassCardView.swift @@ -0,0 +1,294 @@ +import SwiftUI +import StorageLibrary +import UtilityLibrary +import YouTubeLibrary +import UIKit +import MacMagazineLibrary + +@MainActor +struct GlassCardView: View { + @Namespace var namespace + @Environment(\.sizeCategory) private var sizeCategory + + let data: VideoDB + let buttonColor: Color? + + @State private var cardWidth: CGFloat = 0 + @State private var thumbnailSize: CGSize = .zero + + + var isAccessibilityCategory: Bool { + sizeCategory.isAccessibilityCategory + } + + private var density: CardDensity { .from(width: cardWidth) } + + + // MARK: - Body + + var body: some View { + ZStack(alignment: .topTrailing) { + cardBase + .accessibilityElement(children: .ignore) + .accessibilityLabel(Text(accessibilityLabelText)) + .accessibilityAddTraits(.isButton) + + topButtons + } + .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 + } + } + ) + } + + + // MARK: - Card base (thumbnail + conteúdo inferior) + + private var cardBase: some View { + Group { + if let imageUrl = data.url { + ZStack(alignment: .bottom) { + thumbnail(imageUrl) + content + } + } else { + content + .background(fallbackBackground) + } + } + } + + + // MARK: - Thumbnail + + private func thumbnail(_ imageUrl: URL) -> some View { + Thumbnail( + imageUrl: imageUrl, + duration: "", + position: .none, + corners: [.allCorners] + ) + .background( + GeometryReader { g in + Color.clear + .onAppear { thumbnailSize = g.size } + .onChange(of: g.size) { _, newSize in + thumbnailSize = newSize + } + } + ) + } + + + // MARK: - Botões de topo + + private var topButtons: some View { + VStack { + GlassEffectContainer { + HStack(spacing: 10) { + GlassFavoriteButton(content: data, tint: .primary) + .accessibilityLabel( + Text(data.favorite + ? "Remover \(data.title) dos favoritos" + : "Adicionar \(data.title) aos favoritos") + ) + .accessibilityAddTraits(.isButton) + + GlassShareButton(content: data, tint: .primary) + .accessibilityLabel(Text("Compartilhar vídeo \(data.title)")) + .accessibilityAddTraits(.isButton) + } + .glassEffectUnion(id: 1, namespace: namespace) + } + } + .padding(10) + // Importante: NÃO agrupar isso com o card base + .accessibilityElement(children: .contain) + } + + + // MARK: - Fundo fallback (sem imagem) + + private var fallbackBackground: LinearGradient { .fallbackBackground } + + + // MARK: - Bloco de conteúdo inferior + + private var content: some View { + VStack(alignment: .leading, spacing: 6) { + + if density != .spacious { + dateRow + .font(.caption2) + .dynamicTypeSize(.xSmall ... .xxLarge) + .foregroundStyle(.white.opacity(0.9)) + .lineLimit(1) + .minimumScaleFactor(0.8) + .shadow(color: .white.opacity(0.6), radius: 2, x: 0, y: 1) + } + + Text(data.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) { + + if density == .spacious { + dateRow + .foregroundStyle(.white.opacity(0.9)) + .shadow(color: .white.opacity(0.6), radius: 2, x: 0, y: 1) + } + + Group { + if isAccessibilityCategory { + VStack(alignment: .leading, spacing: 4) { + statsRowViews + statsRowLikes + } + } else { + HStack(spacing: 8) { + statsRowViews + statsRowLikes + } + } + } + .foregroundStyle(.white.opacity(0.9)) + .shadow(color: .white.opacity(0.6), radius: 2, x: 0, y: 1) + + Spacer(minLength: 8) + + durationBadge + } + .font(.caption2) + .dynamicTypeSize(.xSmall ... .xxLarge) + .lineLimit(1) + .minimumScaleFactor(0.8) + .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) + } + + private var gradientOverlay: LinearGradient { .gradientOverlay } + + private var dateRow: some View { + HStack(spacing: 4) { + Image(systemName: "calendar") + Text(data.pubDate.formattedDate(using: "dd/MM/yy")) + } + } + + private var statsRowViews: some View { + HStack(spacing: 4) { + Image(systemName: "chart.bar") + Text(data.views.formattedBigNumber) + } + } + + private var statsRowLikes: some View { + HStack(spacing: 4) { + Image(systemName: "hand.thumbsup") + Text(data.likes.formattedBigNumber) + } + } + + private var durationBadge: some View { + Text(data.duration.formattedYTDuration) + .font(.caption) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .foregroundColor(.white) + .dynamicTypeSize(.xSmall ... .xxLarge) + .lineLimit(1) + .minimumScaleFactor(0.8) + .glassEffect(.clear, in: .rect(cornerRadius: 6)) + } +} + + +// MARK: - Acessibilidade (texto falado do card) + +private extension GlassCardView { + var formattedDate: String { data.formattedDateShort } + + var viewsText: String { data.viewsText } + var likesText: String { data.likesText } + + var durationText: String { + "Duração de \(AccessibilityUtils.spokenDuration(from: data.duration.formattedYTDuration))" + } + + var progressText: String? { + guard data.current > 0 else { return nil } + return "Vídeo em andamento" + } + + var favoriteText: String? { + guard data.favorite else { return nil } + return "Marcado como favorito" + } + + var accessibilityLabelText: String { + AccessibilityUtils.join([ + data.title, + "Publicado em \(formattedDate)", + viewsText, + likesText, + durationText, + progressText, + favoriteText + ]) + } +} + + +#if DEBUG +@MainActor +struct MMVideoDBPreview { + static let sample = VideoDB( + artworkURL: "https://i.ytimg.com/vi/bq02LMjcCns/maxresdefault.jpg", + current: 42.0, + duration: "PT4M46S".formattedYTDuration, + favorite: true, + likes: "782", + pubDate: "2021-02-17T20:45:21Z", + title: "Como Usar WhatsApp No iPad", + videoId: "VVVBel9Fc3prM1lqcVZMdzZvWGJTS1FBLmJxMDJMTWpjQ25z", + views: "5663" + ) +} + +#Preview { + ZStack { + Color.brown.ignoresSafeArea() + VStack(spacing: 30) { + GlassCardView( + data: MMVideoDBPreview.sample, + buttonColor: nil + ) + .frame(width: 320) + .padding() + + GlassCardView( + data: MMVideoDBPreview.sample, + buttonColor: nil + ) + .frame(width: 240) + .padding() + } + } +} +#endif + diff --git a/MacMagazine/Features/VideosLibrary/Sources/Videos/VideosView.swift b/MacMagazine/Features/VideosLibrary/Sources/Videos/VideosView.swift index 5d9f6bc2..3a21b697 100644 --- a/MacMagazine/Features/VideosLibrary/Sources/Videos/VideosView.swift +++ b/MacMagazine/Features/VideosLibrary/Sources/Videos/VideosView.swift @@ -23,7 +23,7 @@ public struct VideosView: View { public var body: some View { Videos( - card: ClassicCard(buttonColor: theme.button.primary.color ?? .blue), + card: GlassCard(buttonColor: .white), api: viewModel.youtube, scrollPosition: $scrollPosition, favorite: favorite, @@ -34,6 +34,10 @@ public struct VideosView: View { #Preview { let storage = Database(models: [VideoDB.self], inMemory: true) - VideosView(storage: storage, favorite: .constant(false), scrollPosition: .constant(.init())) - .environment(\.theme, ThemeColor()) + VideosView( + storage: storage, + favorite: .constant(false), + scrollPosition: .constant(.init()) + ) + .environment(\.theme, ThemeColor()) } From cf750d1bcad443a9f1cc7e489e5cb9d34c81766f Mon Sep 17 00:00:00 2001 From: "Renato Ferraz C. B. Ferreira" Date: Fri, 5 Dec 2025 09:55:01 -0300 Subject: [PATCH 2/4] Refactors GlassCard to improve structure Consolidates GlassCard logic into GlassCardView for better organization and reusability. Moves extension properties and functions into GlassCardView from separate extensions, improving code locality. Removes redundant GlassFavoriteButton and GlassShareButton views, using generic components for greater flexibility. --- .../AccessibilityUtils.swift | 58 --------- .../Extensions/LinearGradient.swift | 26 ---- .../Extensions/VideoDB.swift | 16 --- .../MacMagazineLibrary/Extensions/View.swift | 19 --- .../Accessories/GlassFavoriteButton.swift | 22 ---- .../Videos/Accessories/GlassShareButton.swift | 22 ---- .../Sources/Videos/Card/GlassCard.swift | 16 --- .../Sources/Videos/Card/GlassCardView.swift | 119 ++++++++---------- 8 files changed, 55 insertions(+), 243 deletions(-) delete mode 100644 MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/AccessibilityUtils.swift delete mode 100644 MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/Extensions/LinearGradient.swift delete mode 100644 MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/Extensions/VideoDB.swift delete mode 100644 MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/Extensions/View.swift delete mode 100644 MacMagazine/Features/VideosLibrary/Sources/Videos/Accessories/GlassFavoriteButton.swift delete mode 100644 MacMagazine/Features/VideosLibrary/Sources/Videos/Accessories/GlassShareButton.swift delete mode 100644 MacMagazine/Features/VideosLibrary/Sources/Videos/Card/GlassCard.swift diff --git a/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/AccessibilityUtils.swift b/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/AccessibilityUtils.swift deleted file mode 100644 index df820667..00000000 --- a/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/AccessibilityUtils.swift +++ /dev/null @@ -1,58 +0,0 @@ -// -// MMAccessibilityUtils.swift -// MacMagazineLibrary -// -// Created by Renato Ferraz Castelo Branco Ferreira on 04/12/25. -// - - -public enum AccessibilityUtils { - public static func spokenDuration(from formatted: String) -> String { - let parts = formatted.split(separator: ":").map { String($0) } - - var hours = 0 - var minutes = 0 - var seconds = 0 - - if parts.count == 3 { - hours = Int(parts[0]) ?? 0 - minutes = Int(parts[1]) ?? 0 - seconds = Int(parts[2]) ?? 0 - } else if parts.count == 2 { - minutes = Int(parts[0]) ?? 0 - seconds = Int(parts[1]) ?? 0 - } else if parts.count == 1 { - seconds = Int(parts[0]) ?? 0 - } - - var chunks: [String] = [] - if hours > 0 { - chunks.append("\(hours) " + (hours == 1 ? "hora" : "horas")) - } - if minutes > 0 { - chunks.append("\(minutes) " + (minutes == 1 ? "minuto" : "minutos")) - } - if seconds > 0 { - chunks.append("\(seconds) " + (seconds == 1 ? "segundo" : "segundos")) - } - - if chunks.isEmpty { - return "0 segundos" - } - - if chunks.count == 1 { - return chunks[0] - } else if chunks.count == 2 { - return chunks.joined(separator: " e ") - } else { - return chunks[0] + ", " + chunks[1] + " e " + chunks[2] - } - } - - public static func join(_ parts: [String?]) -> String { - parts - .compactMap { $0?.trimmingCharacters(in: .whitespacesAndNewlines) } - .filter { !$0.isEmpty } - .joined(separator: ", ") - } -} diff --git a/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/Extensions/LinearGradient.swift b/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/Extensions/LinearGradient.swift deleted file mode 100644 index 3714d1de..00000000 --- a/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/Extensions/LinearGradient.swift +++ /dev/null @@ -1,26 +0,0 @@ -import SwiftUI - -public extension LinearGradient { - static 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 - ) - } - - static 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 - ) - } -} diff --git a/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/Extensions/VideoDB.swift b/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/Extensions/VideoDB.swift deleted file mode 100644 index a1409580..00000000 --- a/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/Extensions/VideoDB.swift +++ /dev/null @@ -1,16 +0,0 @@ -import YouTubeLibrary -import SwiftUI - -public extension VideoDB { - var formattedDateShort: String { - pubDate.formattedDate(using: "dd/MM/yy") - } - - var viewsText: String { - "\(views.formattedBigNumber) visualizações" - } - - var likesText: String { - "\(likes.formattedBigNumber) curtidas" - } -} diff --git a/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/Extensions/View.swift b/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/Extensions/View.swift deleted file mode 100644 index 4ace4126..00000000 --- a/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/Extensions/View.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// View+VideoLibrary.swift -// VideosLibrary -// -// Created by Renato Ferraz Castelo Branco Ferreira on 03/12/25. -// - -import SwiftUI - -public extension View { - @ViewBuilder - func applyTint(_ tint: Color?) -> some View { - if let tint { - self.tint(tint) - } else { - self - } - } -} diff --git a/MacMagazine/Features/VideosLibrary/Sources/Videos/Accessories/GlassFavoriteButton.swift b/MacMagazine/Features/VideosLibrary/Sources/Videos/Accessories/GlassFavoriteButton.swift deleted file mode 100644 index 94b2e6e5..00000000 --- a/MacMagazine/Features/VideosLibrary/Sources/Videos/Accessories/GlassFavoriteButton.swift +++ /dev/null @@ -1,22 +0,0 @@ -import SwiftUI -import YouTubeLibrary -import StorageLibrary - -public struct GlassFavoriteButton: View { - let content: VideoDB - let tint: Color? - - public init(content: VideoDB, tint: Color? = nil) { - self.content = content - self.tint = tint - } - - public var body: some View { - FavoriteButton(content: content) - .buttonStyle(.plain) - .font(.system(size: 16)) - .frame(width: 35, height: 35) - .glassEffect() - .applyTint(tint) - } -} diff --git a/MacMagazine/Features/VideosLibrary/Sources/Videos/Accessories/GlassShareButton.swift b/MacMagazine/Features/VideosLibrary/Sources/Videos/Accessories/GlassShareButton.swift deleted file mode 100644 index e66a34ca..00000000 --- a/MacMagazine/Features/VideosLibrary/Sources/Videos/Accessories/GlassShareButton.swift +++ /dev/null @@ -1,22 +0,0 @@ -import SwiftUI -import StorageLibrary -import YouTubeLibrary - -public struct GlassShareButton: View { - let content: VideoDB - let tint: Color? - - public init(content: VideoDB, tint: Color? = nil) { - self.content = content - self.tint = tint - } - - public var body: some View { - ShareButton(content: content) - .buttonStyle(.plain) - .font(.system(size: 16)) - .frame(width: 35, height: 35) - .glassEffect() - .applyTint(tint) - } -} diff --git a/MacMagazine/Features/VideosLibrary/Sources/Videos/Card/GlassCard.swift b/MacMagazine/Features/VideosLibrary/Sources/Videos/Card/GlassCard.swift deleted file mode 100644 index 2d8f6076..00000000 --- a/MacMagazine/Features/VideosLibrary/Sources/Videos/Card/GlassCard.swift +++ /dev/null @@ -1,16 +0,0 @@ -import SwiftUI -import StorageLibrary -import YouTubeLibrary - -@MainActor -public struct GlassCard: VideoCard { - public let buttonColor: Color? - - public init(buttonColor: Color? = nil) { - self.buttonColor = buttonColor - } - - public func makeBody(data: VideoDB) -> some View { - GlassCardView(data: data, buttonColor: buttonColor) - } -} diff --git a/MacMagazine/Features/VideosLibrary/Sources/Videos/Card/GlassCardView.swift b/MacMagazine/Features/VideosLibrary/Sources/Videos/Card/GlassCardView.swift index d609acd1..2815df6e 100644 --- a/MacMagazine/Features/VideosLibrary/Sources/Videos/Card/GlassCardView.swift +++ b/MacMagazine/Features/VideosLibrary/Sources/Videos/Card/GlassCardView.swift @@ -5,6 +5,21 @@ import YouTubeLibrary import UIKit import MacMagazineLibrary + +@MainActor +public struct GlassCard: VideoCard { + public let buttonColor: Color? + + public init(buttonColor: Color? = nil) { + self.buttonColor = buttonColor + } + + public func makeBody(data: VideoDB) -> some View { + GlassCardView(data: data, buttonColor: buttonColor) + } +} + + @MainActor struct GlassCardView: View { @Namespace var namespace @@ -16,7 +31,6 @@ struct GlassCardView: View { @State private var cardWidth: CGFloat = 0 @State private var thumbnailSize: CGSize = .zero - var isAccessibilityCategory: Bool { sizeCategory.isAccessibilityCategory } @@ -29,10 +43,6 @@ struct GlassCardView: View { var body: some View { ZStack(alignment: .topTrailing) { cardBase - .accessibilityElement(children: .ignore) - .accessibilityLabel(Text(accessibilityLabelText)) - .accessibilityAddTraits(.isButton) - topButtons } .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous)) @@ -48,7 +58,7 @@ struct GlassCardView: View { } - // MARK: - Card base (thumbnail + conteúdo inferior) + // MARK: - Card base (thumbnail + bottom content) private var cardBase: some View { Group { @@ -64,6 +74,17 @@ struct GlassCardView: View { } } + 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 @@ -86,39 +107,35 @@ struct GlassCardView: View { } - // MARK: - Botões de topo + // MARK: - Top buttons private var topButtons: some View { VStack { GlassEffectContainer { HStack(spacing: 10) { - GlassFavoriteButton(content: data, tint: .primary) - .accessibilityLabel( - Text(data.favorite - ? "Remover \(data.title) dos favoritos" - : "Adicionar \(data.title) aos favoritos") - ) - .accessibilityAddTraits(.isButton) + FavoriteButton(content: data) + .buttonStyle(.plain) + .font(.system(size: 16)) + .frame(width: 35, height: 35) + .glassEffect() + .tint(.primary) + + ShareButton(content: data) + .buttonStyle(.plain) + .font(.system(size: 16)) + .frame(width: 35, height: 35) + .glassEffect() + .tint(.primary) - GlassShareButton(content: data, tint: .primary) - .accessibilityLabel(Text("Compartilhar vídeo \(data.title)")) - .accessibilityAddTraits(.isButton) } .glassEffectUnion(id: 1, namespace: namespace) } } .padding(10) - // Importante: NÃO agrupar isso com o card base - .accessibilityElement(children: .contain) } - // MARK: - Fundo fallback (sem imagem) - - private var fallbackBackground: LinearGradient { .fallbackBackground } - - - // MARK: - Bloco de conteúdo inferior + // MARK: - Bottom content block private var content: some View { VStack(alignment: .leading, spacing: 6) { @@ -166,7 +183,7 @@ struct GlassCardView: View { Spacer(minLength: 8) - durationBadge + duration } .font(.caption2) .dynamicTypeSize(.xSmall ... .xxLarge) @@ -181,7 +198,17 @@ struct GlassCardView: View { .background(gradientOverlay) } - private var gradientOverlay: LinearGradient { .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 + ) + } private var dateRow: some View { HStack(spacing: 4) { @@ -204,7 +231,7 @@ struct GlassCardView: View { } } - private var durationBadge: some View { + private var duration: some View { Text(data.duration.formattedYTDuration) .font(.caption) .padding(.horizontal, 8) @@ -218,42 +245,6 @@ struct GlassCardView: View { } -// MARK: - Acessibilidade (texto falado do card) - -private extension GlassCardView { - var formattedDate: String { data.formattedDateShort } - - var viewsText: String { data.viewsText } - var likesText: String { data.likesText } - - var durationText: String { - "Duração de \(AccessibilityUtils.spokenDuration(from: data.duration.formattedYTDuration))" - } - - var progressText: String? { - guard data.current > 0 else { return nil } - return "Vídeo em andamento" - } - - var favoriteText: String? { - guard data.favorite else { return nil } - return "Marcado como favorito" - } - - var accessibilityLabelText: String { - AccessibilityUtils.join([ - data.title, - "Publicado em \(formattedDate)", - viewsText, - likesText, - durationText, - progressText, - favoriteText - ]) - } -} - - #if DEBUG @MainActor struct MMVideoDBPreview { From 9eb344835d131b269d2f3d9ee6b54e4e30367446 Mon Sep 17 00:00:00 2001 From: "Renato Ferraz C. B. Ferreira" Date: Fri, 5 Dec 2025 09:56:06 -0300 Subject: [PATCH 3/4] Removes unused import Removes unused SwiftUI import from Utils.swift. This reduces unnecessary dependencies and improves code cleanliness. --- .../MacMagazineLibrary/Sources/MacMagazineLibrary/Utils.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/Utils.swift b/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/Utils.swift index c36b5fef..ed5289ff 100644 --- a/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/Utils.swift +++ b/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/Utils.swift @@ -3,10 +3,8 @@ import Foundation import SafariServices import UIKit -import SwiftUI public class Utils { - @MainActor static public func openInSafari(_ url: URL) { if url.scheme?.lowercased().contains("http") ?? false { From 2e39a76b9e62471d042114c5ab42627ba3dae638 Mon Sep 17 00:00:00 2001 From: "Renato Ferraz C. B. Ferreira" Date: Fri, 5 Dec 2025 12:35:26 -0300 Subject: [PATCH 4/4] Updates project configuration and UI Adds a comprehensive .gitignore file, improving project hygiene and preventing unnecessary files from being tracked in version control. Updates the GlassCardView to enhance UI by using the MacMagazineLibrary and improves the code structure. --- .gitignore | 112 +++++++++++++++--- .../Sources/Videos/Card/GlassCardView.swift | 67 +++++------ .../xcshareddata/swiftpm/Package.resolved | 2 +- 3 files changed, 125 insertions(+), 56 deletions(-) diff --git a/.gitignore b/.gitignore index 932bcf3a..90982b9a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,24 +1,100 @@ -Carthage/ -Docs/ +## --------------------------------------------- +## Xcode / Swift / Apple Platforms – Recommended +## --------------------------------------------- + +# Build output +build/ +DerivedData/ +DerivedSources/ + +# SwiftPM +.swiftpm/ +.build/ +Package.resolved + +# Xcode workspace settings (local-only) +xcuserdata/ +*.xccheckout +*.xcuserstate +*.moved-aside + +# User-specific settings +*.xcconfig.dynamic +*.xcsettings +*.swp +*.swo + +# Logs and debugging +*.log +*.diag +*.crash +*.dsym +*.dSYM/ +*.dSYM.zip + +# Index / metadata +.index/ +*.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +*.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings + +# iOS device support (from debugging) +DeviceSupport/ +DeviceLogs/ + +# Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager artifacts +.swiftpm/xcode/package.xcworkspace +.swiftpm/indices +.swiftpm/manifest-db + +# Bundler / Ruby artifacts (Fastlane, CocoaPods, etc.) +.bundle/ +vendor/ + +# General macOS files .DS_Store -.build -.swiftpm +.AppleDouble +.LSOverride +Docs/ +#*.xcscmblueprint +#*.hmap +#*.ipa + +# macOS resource forks +._* *.resolved -*.zip -build .claude +.TemporaryItems +.Trashes +.VolumeIcon.icns -## Xcode Patch -*.xcodeproj/* -!*.xcodeproj/project.pbxproj -!*.xcodeproj/xcshareddata/ -!*.xcworkspace/contents.xcworkspacedata -!*.xcodeproj/project.xcworkspace/contents.xcworkspacedata -!*.xcodeproj/project.xcworkspace/xcshareddata -!*.xcodeproj/project.xcworkspace/xcuserdata +# macOS temporary files +*.swp +*.tmp +*.temp +*~.nib -Package.resolved -!*.xcodeproj/**/Package.resolved -MacMagazine/MacMagazine.xcodeproj/project.xcworkspace/xcuserdata +# Archives +*.xcarchive +*.zip + +# Code coverage +*.profdata +*.gcda +*.gcno + +# Swift testing artifacts +*.xctestrun +.xcresult/ + +# Firebase / Google services (dependendo do fluxo) +# Descomente se o GoogleService-Info.plist é gerado via script CI: +# GoogleService-Info.plist + +# Env files (se você usa) +.env +.env.* -MacMagazine/MacMagazine.xcodeproj/xcuserdata/renato_ferreira.xcuserdatad/xcdebugger/ diff --git a/MacMagazine/Features/VideosLibrary/Sources/Videos/Card/GlassCardView.swift b/MacMagazine/Features/VideosLibrary/Sources/Videos/Card/GlassCardView.swift index 2815df6e..6a7893b8 100644 --- a/MacMagazine/Features/VideosLibrary/Sources/Videos/Card/GlassCardView.swift +++ b/MacMagazine/Features/VideosLibrary/Sources/Videos/Card/GlassCardView.swift @@ -1,25 +1,25 @@ -import SwiftUI +import MacMagazineLibrary import StorageLibrary +import SwiftUI +import UIKit import UtilityLibrary import YouTubeLibrary -import UIKit -import MacMagazineLibrary - @MainActor public struct GlassCard: VideoCard { + public var accessibilityLabels: [CardLabel]? + public var accessibilityButtons: [CardButton]? public let buttonColor: Color? - + public init(buttonColor: Color? = nil) { self.buttonColor = buttonColor } - + public func makeBody(data: VideoDB) -> some View { GlassCardView(data: data, buttonColor: buttonColor) } } - @MainActor struct GlassCardView: View { @Namespace var namespace @@ -27,7 +27,7 @@ struct GlassCardView: View { let data: VideoDB let buttonColor: Color? - + @State private var cardWidth: CGFloat = 0 @State private var thumbnailSize: CGSize = .zero @@ -36,10 +36,9 @@ struct GlassCardView: View { } private var density: CardDensity { .from(width: cardWidth) } - - + // MARK: - Body - + var body: some View { ZStack(alignment: .topTrailing) { cardBase @@ -56,10 +55,9 @@ struct GlassCardView: View { } ) } - - + // MARK: - Card base (thumbnail + bottom content) - + private var cardBase: some View { Group { if let imageUrl = data.url { @@ -73,7 +71,7 @@ struct GlassCardView: View { } } } - + var fallbackBackground: LinearGradient { LinearGradient( gradient: Gradient(stops: [ @@ -84,10 +82,9 @@ struct GlassCardView: View { endPoint: .bottom ) } - - + // MARK: - Thumbnail - + private func thumbnail(_ imageUrl: URL) -> some View { Thumbnail( imageUrl: imageUrl, @@ -96,19 +93,18 @@ struct GlassCardView: View { corners: [.allCorners] ) .background( - GeometryReader { g in + GeometryReader { geo in Color.clear - .onAppear { thumbnailSize = g.size } - .onChange(of: g.size) { _, newSize in + .onAppear { thumbnailSize = geo.size } + .onChange(of: geo.size) { _, newSize in thumbnailSize = newSize } } ) } - - + // MARK: - Top buttons - + private var topButtons: some View { VStack { GlassEffectContainer { @@ -119,7 +115,7 @@ struct GlassCardView: View { .frame(width: 35, height: 35) .glassEffect() .tint(.primary) - + ShareButton(content: data) .buttonStyle(.plain) .font(.system(size: 16)) @@ -133,13 +129,12 @@ struct GlassCardView: View { } .padding(10) } - - + // MARK: - Bottom content block - + private var content: some View { VStack(alignment: .leading, spacing: 6) { - + if density != .spacious { dateRow .font(.caption2) @@ -149,7 +144,7 @@ struct GlassCardView: View { .minimumScaleFactor(0.8) .shadow(color: .white.opacity(0.6), radius: 2, x: 0, y: 1) } - + Text(data.title) .font(density.titleFont) .multilineTextAlignment(.leading) @@ -197,7 +192,7 @@ struct GlassCardView: View { .frame(maxWidth: .infinity, alignment: .leading) .background(gradientOverlay) } - + var gradientOverlay: LinearGradient { LinearGradient( gradient: Gradient(stops: [ @@ -209,14 +204,14 @@ struct GlassCardView: View { endPoint: .bottom ) } - + private var dateRow: some View { HStack(spacing: 4) { Image(systemName: "calendar") Text(data.pubDate.formattedDate(using: "dd/MM/yy")) } } - + private var statsRowViews: some View { HStack(spacing: 4) { Image(systemName: "chart.bar") @@ -230,7 +225,7 @@ struct GlassCardView: View { Text(data.likes.formattedBigNumber) } } - + private var duration: some View { Text(data.duration.formattedYTDuration) .font(.caption) @@ -244,7 +239,6 @@ struct GlassCardView: View { } } - #if DEBUG @MainActor struct MMVideoDBPreview { @@ -271,7 +265,7 @@ struct MMVideoDBPreview { ) .frame(width: 320) .padding() - + GlassCardView( data: MMVideoDBPreview.sample, buttonColor: nil @@ -282,4 +276,3 @@ struct MMVideoDBPreview { } } #endif - diff --git a/MacMagazine/MacMagazine.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/MacMagazine/MacMagazine.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index d3bc622e..2d28f6c0 100644 --- a/MacMagazine/MacMagazine.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/MacMagazine/MacMagazine.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -106,7 +106,7 @@ "location" : "https://github.com/cassio-rossi/Libraries.git", "state" : { "branch" : "main", - "revision" : "175b3166984a7b114847f8a57a89cb638fe9e377" + "revision" : "34148feae97e1fa18af60b34c91a954b64669758" } }, {