Skip to content

Commit 38e18c3

Browse files
authored
Merge pull request #255 from rfcbf/feature/Apple_Watch
feat: Apple Watch
2 parents ab4397e + 8c9204d commit 38e18c3

File tree

12 files changed

+846
-37
lines changed

12 files changed

+846
-37
lines changed

MacMagazine/WatchApp/ContentView.swift

Lines changed: 0 additions & 27 deletions
This file was deleted.
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import FeedLibrary
2+
import Foundation
3+
4+
extension FeedDB {
5+
var linkURL: URL? {
6+
guard !link.isEmpty else { return nil }
7+
return URL(string: link)
8+
}
9+
10+
var dateText: String {
11+
let formatter = DateFormatter()
12+
formatter.locale = Locale(identifier: "pt_BR")
13+
formatter.dateFormat = "dd/MM/yyyy 'às' HH:mm"
14+
return formatter.string(from: pubDate)
15+
}
16+
17+
var displaySubtitle: String? {
18+
let subtitle = subtitle.trimmingCharacters(in: .whitespacesAndNewlines)
19+
return subtitle.isEmpty ? nil : subtitle
20+
}
21+
22+
var displayBody: String? {
23+
let full = fullContent.trimmingCharacters(in: .whitespacesAndNewlines)
24+
if !full.isEmpty { return full }
25+
26+
let excerpt = excerpt.trimmingCharacters(in: .whitespacesAndNewlines)
27+
return excerpt.isEmpty ? nil : excerpt
28+
}
29+
30+
var artworkRemoteURL: URL? {
31+
guard !artworkURL.isEmpty else { return nil }
32+
return URL(string: artworkURL)
33+
}
34+
}
35+
36+
#if DEBUG
37+
extension FeedDB {
38+
39+
static var previewItem: FeedDB {
40+
FeedDB(
41+
postId: UUID().uuidString,
42+
title: "Apple lança atualização do watchOS",
43+
subtitle: "Mudanças importantes para o Apple Watch",
44+
pubDate: Date().addingTimeInterval(-3600),
45+
artworkURL: "https://picsum.photos/400/400",
46+
link: "https://macmagazine.com.br",
47+
categories: ["watchos", "news", "teste1", "teste2", "teste 3"],
48+
excerpt: "Resumo curto para teste no relógio…",
49+
fullContent: "",
50+
favorite: false
51+
)
52+
}
53+
54+
static var previewItems: [FeedDB] {
55+
(1...10).map { index in
56+
FeedDB(
57+
postId: UUID().uuidString,
58+
title: "Notícia \(index): título de teste para o Watch",
59+
subtitle: "Subtítulo \(index)",
60+
pubDate: Date().addingTimeInterval(TimeInterval(-index * 900)),
61+
artworkURL: "https://picsum.photos/seed/\(index)/600/600",
62+
link: "https://macmagazine.com.br",
63+
categories: ["news", "teste1", "teste2", "teste 3"],
64+
excerpt: "Excerpt \(index) – texto curto para validar layout.",
65+
fullContent: "",
66+
favorite: index % 3 == 0
67+
)
68+
}
69+
}
70+
}
71+
#endif
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import SwiftUI
2+
3+
// MARK: - Dots Indicator
4+
5+
public struct FeedDotsIndicatorView: View {
6+
let count: Int
7+
let selectedIndex: Int
8+
9+
public var body: some View {
10+
VStack(spacing: 5) {
11+
dots
12+
}
13+
.padding(6)
14+
.glassEffect(.clear)
15+
.allowsHitTesting(false)
16+
}
17+
18+
private var dots: some View {
19+
ForEach(0..<count, id: \.self) { index in
20+
Circle()
21+
.fill(dotColor(for: index))
22+
.frame(width: dotSize(for: index), height: dotSize(for: index))
23+
.shadow(color: index == selectedIndex ? .white.opacity(0.5) : .clear, radius: 2)
24+
.animation(.easeInOut(duration: 0.2), value: selectedIndex)
25+
}
26+
}
27+
28+
private func dotColor(for index: Int) -> Color {
29+
index == selectedIndex ? .white : .white.opacity(0.4)
30+
}
31+
32+
private func dotSize(for index: Int) -> CGFloat {
33+
index == selectedIndex ? 8 : 5
34+
}
35+
}
36+
37+
#if DEBUG
38+
#Preview {
39+
ZStack {
40+
Color.gray
41+
FeedDotsIndicatorView(count: 10, selectedIndex: 3)
42+
}
43+
}
44+
#endif
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import SwiftUI
2+
3+
struct FlowTagsView<Tag: Hashable, Content: View>: View {
4+
5+
// MARK: - Properties
6+
7+
let tags: [Tag]
8+
let horizontalSpacing: CGFloat
9+
let verticalSpacing: CGFloat
10+
let content: (Tag) -> Content
11+
12+
@State private var measuredHeight: CGFloat = 0
13+
14+
// MARK: - Init
15+
16+
init(
17+
tags: [Tag],
18+
horizontalSpacing: CGFloat,
19+
verticalSpacing: CGFloat,
20+
@ViewBuilder content: @escaping (Tag) -> Content
21+
) {
22+
self.tags = tags
23+
self.horizontalSpacing = horizontalSpacing
24+
self.verticalSpacing = verticalSpacing
25+
self.content = content
26+
}
27+
28+
// MARK: - Body
29+
30+
var body: some View {
31+
GeometryReader { proxy in
32+
let rows = makeRows(availableWidth: proxy.size.width)
33+
34+
VStack(alignment: .leading, spacing: verticalSpacing) {
35+
ForEach(rows.indices, id: \.self) { rowIndex in
36+
HStack(spacing: horizontalSpacing) {
37+
ForEach(rows[rowIndex], id: \.self) { tag in
38+
content(tag)
39+
.fixedSize(horizontal: true, vertical: true)
40+
}
41+
}
42+
}
43+
}
44+
.background(
45+
GeometryReader { innerProxy in
46+
Color.clear
47+
.preference(key: HeightPreferenceKey.self, value: innerProxy.size.height)
48+
}
49+
)
50+
}
51+
.frame(height: measuredHeight)
52+
.onPreferenceChange(HeightPreferenceKey.self) { height in
53+
if height != measuredHeight {
54+
measuredHeight = height
55+
}
56+
}
57+
.frame(maxWidth: .infinity, alignment: .leading)
58+
.fixedSize(horizontal: false, vertical: true)
59+
}
60+
61+
// MARK: - Layout
62+
63+
private func makeRows(availableWidth: CGFloat) -> [[Tag]] {
64+
var rows: [[Tag]] = [[]]
65+
var currentRowWidth: CGFloat = 0
66+
67+
for tag in tags {
68+
let tagWidth = estimatedTagWidth(tag: tag)
69+
70+
if rows[rows.count - 1].isEmpty {
71+
rows[rows.count - 1].append(tag)
72+
currentRowWidth = tagWidth
73+
continue
74+
}
75+
76+
let nextWidth = currentRowWidth + horizontalSpacing + tagWidth
77+
78+
if nextWidth <= availableWidth {
79+
rows[rows.count - 1].append(tag)
80+
currentRowWidth = nextWidth
81+
} else {
82+
rows.append([tag])
83+
currentRowWidth = tagWidth
84+
}
85+
}
86+
87+
return rows
88+
}
89+
90+
private func estimatedTagWidth(tag: Tag) -> CGFloat {
91+
let text = String(describing: tag)
92+
let estimatedCharacterWidth: CGFloat = 6
93+
let horizontalPadding: CGFloat = 16
94+
return CGFloat(text.count) * estimatedCharacterWidth + horizontalPadding
95+
}
96+
}
97+
98+
// MARK: - Height Preference
99+
100+
private struct HeightPreferenceKey: PreferenceKey {
101+
static var defaultValue: CGFloat = 0
102+
103+
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
104+
value = max(value, nextValue())
105+
}
106+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import FeedLibrary
2+
import StorageLibrary
3+
import SwiftData
4+
import SwiftUI
5+
6+
@main
7+
struct WatchApp: App {
8+
9+
private let database = Database(models: [FeedDB.self], inMemory: true)
10+
11+
var body: some Scene {
12+
WindowGroup {
13+
FeedRootView(
14+
viewModel: FeedRootViewModel(
15+
feedViewModel: FeedViewModel(
16+
network: nil,
17+
storage: database
18+
)
19+
)
20+
)
21+
.modelContainer(database.sharedModelContainer)
22+
}
23+
}
24+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import FeedLibrary
2+
import SwiftUI
3+
4+
// MARK: - Navigation Payload
5+
6+
struct SelectedPost: Hashable, Identifiable {
7+
let id: String
8+
let post: FeedDB
9+
10+
init(post: FeedDB) {
11+
id = post.postId
12+
self.post = post
13+
}
14+
}

0 commit comments

Comments
 (0)