Skip to content

Commit 9b59db3

Browse files
authored
Merge pull request #538 from synonymdev/feat/os-widgets
feat: OS widgets foundation + price widget
2 parents ed1a8ff + d2ea796 commit 9b59db3

120 files changed

Lines changed: 5802 additions & 1640 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,4 @@ buildServer.json
2525
.ai/
2626
.codex/
2727
.claude/*.local*
28+
.claude/scheduled_tasks.lock

Bitkit.xcodeproj/project.pbxproj

Lines changed: 244 additions & 2 deletions
Large diffs are not rendered by default.

Bitkit/AppScene.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ struct AppScene: View {
2727
@StateObject private var transferTracking: TransferTrackingManager
2828
@StateObject private var channelDetails = ChannelDetailsViewModel.shared
2929
@StateObject private var migrations = MigrationsService.shared
30+
@StateObject private var languageManager = LanguageManager.shared
3031
@StateObject private var pubkyProfile = PubkyProfileManager()
3132
@StateObject private var contactsManager = ContactsManager()
3233
@State private var keyboardManager = KeyboardManager()
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"images" : [
3+
{
4+
"filename" : "bitcoin-symbol.pdf",
5+
"idiom" : "universal"
6+
}
7+
],
8+
"info" : {
9+
"author" : "xcode",
10+
"version" : 1
11+
},
12+
"properties" : {
13+
"template-rendering-intent" : "template"
14+
}
15+
}
Binary file not shown.

Bitkit/Components/Widgets/BaseWidget.swift

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,6 @@ struct BaseWidget<Content: View>: View {
8585
@EnvironmentObject private var navigation: NavigationViewModel
8686
@EnvironmentObject private var widgets: WidgetsViewModel
8787
@EnvironmentObject private var currency: CurrencyViewModel
88-
@EnvironmentObject private var settings: SettingsViewModel
8988

9089
/// Widget metadata computed from type
9190
private var metadata: WidgetMetadata {
@@ -127,7 +126,7 @@ struct BaseWidget<Content: View>: View {
127126
var body: some View {
128127
Button {} label: {
129128
VStack(spacing: 0) {
130-
if type == .suggestions ? isEditing : (settings.showWidgetTitles || isEditing) {
129+
if isEditing {
131130
HStack {
132131
HStack(spacing: 16) {
133132
Image(metadata.icon)
@@ -185,12 +184,6 @@ struct BaseWidget<Content: View>: View {
185184
}
186185
}
187186
}
188-
189-
// Add spacer only when showing title and not editing
190-
if settings.showWidgetTitles && !isEditing {
191-
Spacer()
192-
.frame(height: 16)
193-
}
194187
}
195188

196189
// Widget content (only shown when not editing)

Bitkit/Components/Widgets/BlocksWidget.swift

Lines changed: 14 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,16 @@
11
import SwiftUI
22

3-
/// Options for configuring the BlocksWidget
4-
struct BlocksWidgetOptions: Codable, Equatable {
5-
var height: Bool = true
6-
var time: Bool = true
7-
var date: Bool = true
8-
var transactionCount: Bool = false
9-
var size: Bool = false
10-
var weight: Bool = false
11-
var difficulty: Bool = false
12-
var hash: Bool = false
13-
var merkleRoot: Bool = false
14-
var showSource: Bool = false
15-
}
3+
// MARK: - Widget
164

17-
/// A widget that displays Bitcoin block information
5+
/// In-app Bitcoin Blocks widget (v61). Renders the wide layout — used inside the home feed
6+
/// and the wide carousel page on the preview screen.
187
struct BlocksWidget: View {
19-
/// Configuration options for the widget
208
var options: BlocksWidgetOptions = .init()
21-
22-
/// Flag indicating if the widget is in editing mode
239
var isEditing: Bool = false
24-
25-
/// Callback to signal when editing should end
2610
var onEditingEnd: (() -> Void)?
2711

28-
/// View model for handling block data
2912
@StateObject private var viewModel = BlocksViewModel.shared
3013

31-
/// Initialize the widget
3214
init(
3315
options: BlocksWidgetOptions = BlocksWidgetOptions(),
3416
isEditing: Bool = false,
@@ -39,96 +21,29 @@ struct BlocksWidget: View {
3921
self.onEditingEnd = onEditingEnd
4022
}
4123

42-
/// Mapping of block data keys to display labels
43-
private let blocksMapping: [String: String] = [
44-
"height": "Block",
45-
"time": "Time",
46-
"date": "Date",
47-
"transactionCount": "Transactions",
48-
"size": "Size",
49-
"weight": "Weight",
50-
"difficulty": "Difficulty",
51-
"hash": "Hash",
52-
"merkleRoot": "Merkle Root",
53-
]
54-
5524
var body: some View {
5625
BaseWidget(
5726
type: .blocks,
5827
isEditing: isEditing,
5928
onEditingEnd: onEditingEnd
6029
) {
61-
VStack(spacing: 0) {
62-
if viewModel.isLoading {
63-
WidgetContentBuilder.loadingView()
64-
} else if viewModel.error != nil {
65-
WidgetContentBuilder.errorView(t("widgets__blocks__error"))
66-
} else if let data = viewModel.blockData {
67-
VStack(spacing: 0) {
68-
// Display block data rows based on options
69-
ForEach(getDisplayableData(data), id: \.key) { item in
70-
HStack(spacing: 0) {
71-
HStack {
72-
BodySSBText(item.label, textColor: .textSecondary)
73-
.lineLimit(1)
74-
}
75-
.frame(maxWidth: .infinity, alignment: .leading)
76-
77-
HStack {
78-
BodyMSBText(item.value)
79-
.lineLimit(1)
80-
.truncationMode(.middle)
81-
}
82-
.frame(maxWidth: .infinity, alignment: .trailing)
83-
}
84-
.frame(minHeight: 28)
85-
}
86-
87-
if options.showSource {
88-
WidgetContentBuilder.sourceRow(source: "mempool.space")
89-
}
90-
}
91-
}
92-
}
30+
content
9331
}
94-
.onAppear {
32+
.task {
9533
viewModel.startUpdates()
9634
}
9735
}
9836

99-
/// Get displayable data based on current options
100-
private func getDisplayableData(_ data: BlockData) -> [(key: String, label: String, value: String)] {
101-
var items: [(key: String, label: String, value: String)] = []
102-
103-
if options.height {
104-
items.append((key: "height", label: blocksMapping["height"]!, value: data.height))
105-
}
106-
if options.time {
107-
items.append((key: "time", label: blocksMapping["time"]!, value: data.time))
37+
@ViewBuilder
38+
private var content: some View {
39+
if viewModel.isLoading && viewModel.blockData == nil {
40+
WidgetContentBuilder.loadingView()
41+
} else if viewModel.error != nil && viewModel.blockData == nil {
42+
WidgetContentBuilder.errorView(t("widgets__blocks__error"))
43+
} else if let data = viewModel.blockData {
44+
BlocksWidgetWideContent(data: data, options: options)
45+
.frame(height: BlocksWidgetWideContent.inAppContentHeight)
10846
}
109-
if options.date {
110-
items.append((key: "date", label: blocksMapping["date"]!, value: data.date))
111-
}
112-
if options.transactionCount {
113-
items.append((key: "transactionCount", label: blocksMapping["transactionCount"]!, value: data.transactionCount))
114-
}
115-
if options.size {
116-
items.append((key: "size", label: blocksMapping["size"]!, value: data.size))
117-
}
118-
if options.weight {
119-
items.append((key: "weight", label: blocksMapping["weight"]!, value: data.weight))
120-
}
121-
if options.difficulty {
122-
items.append((key: "difficulty", label: blocksMapping["difficulty"]!, value: data.difficulty))
123-
}
124-
if options.hash {
125-
items.append((key: "hash", label: blocksMapping["hash"]!, value: data.hash))
126-
}
127-
if options.merkleRoot {
128-
items.append((key: "merkleRoot", label: blocksMapping["merkleRoot"]!, value: data.merkleRoot))
129-
}
130-
131-
return items
13247
}
13348
}
13449

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import SwiftUI
2+
import WidgetKit
3+
4+
// Shared Bitcoin Blocks widget content, reused by the in-app feed, the carousel preview, and the
5+
// home-screen WidgetKit extension. Colors adapt to `widgetRenderingMode` via ``WidgetPalette``.
6+
7+
// MARK: - Wide layout (in-app + 343-wide carousel page + .systemMedium / .systemLarge OS widget)
8+
9+
struct BlocksWidgetWideContent: View {
10+
static let inAppContentHeight: CGFloat = 124
11+
12+
let data: CachedBlock
13+
let options: BlocksWidgetOptions
14+
15+
@Environment(\.widgetRenderingMode) private var renderingMode
16+
17+
var body: some View {
18+
let palette = WidgetPalette(renderingMode: renderingMode)
19+
VStack(alignment: .leading, spacing: 0) {
20+
ForEach(Array(options.enabledFields.enumerated()), id: \.element) { index, field in
21+
if index > 0 {
22+
Spacer(minLength: 8)
23+
}
24+
BlocksWidgetWideRow(field: field, value: field.value(from: data), palette: palette)
25+
}
26+
}
27+
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
28+
}
29+
}
30+
31+
private struct BlocksWidgetWideRow: View {
32+
let field: BlocksWidgetField
33+
let value: String
34+
let palette: WidgetPalette
35+
36+
var body: some View {
37+
HStack(alignment: .center, spacing: 8) {
38+
BlocksWidgetIcon(field: field, palette: palette)
39+
40+
BodyMText(field.label, textColor: palette.label)
41+
.lineLimit(1)
42+
.frame(maxWidth: .infinity, alignment: .leading)
43+
44+
BodyMSBText(value, textColor: palette.title)
45+
.lineLimit(1)
46+
.truncationMode(.middle)
47+
.widgetAccentable()
48+
}
49+
}
50+
}
51+
52+
// MARK: - Compact layout (small carousel preview + .systemSmall OS widget)
53+
54+
struct BlocksWidgetCompactContent: View {
55+
let data: CachedBlock
56+
let options: BlocksWidgetOptions
57+
58+
@Environment(\.widgetRenderingMode) private var renderingMode
59+
60+
var body: some View {
61+
let palette = WidgetPalette(renderingMode: renderingMode)
62+
VStack(alignment: .leading, spacing: 0) {
63+
ForEach(Array(options.enabledFields.enumerated()), id: \.element) { index, field in
64+
if index > 0 {
65+
Spacer(minLength: 8)
66+
}
67+
HStack(alignment: .center, spacing: 8) {
68+
BlocksWidgetIcon(field: field, palette: palette)
69+
70+
BodySSBText(field.value(from: data), textColor: palette.title)
71+
.lineLimit(1)
72+
.truncationMode(.middle)
73+
.widgetAccentable()
74+
}
75+
}
76+
}
77+
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
78+
}
79+
}
80+
81+
// MARK: - Shared row icon
82+
83+
private struct BlocksWidgetIcon: View {
84+
let field: BlocksWidgetField
85+
let palette: WidgetPalette
86+
87+
var body: some View {
88+
Image(field.iconName)
89+
.resizable()
90+
.renderingMode(.template)
91+
.foregroundColor(palette.accent)
92+
.frame(width: 20, height: 20)
93+
.widgetAccentable()
94+
}
95+
}

Bitkit/Components/Widgets/FactsWidget.swift

Lines changed: 2 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,34 @@
11
import SwiftUI
22

3-
/// Options for configuring the FactsWidget
4-
struct FactsWidgetOptions: Codable, Equatable {
5-
var showSource: Bool = true
6-
}
7-
83
struct FactsWidget: View {
9-
/// Configuration options for the widget
10-
var options: FactsWidgetOptions = .init()
11-
12-
/// Flag indicating if the widget is in editing mode
134
var isEditing: Bool = false
14-
15-
/// Callback to signal when editing should end
165
var onEditingEnd: (() -> Void)?
176

18-
/// View model for handling facts data
197
@StateObject private var viewModel = FactsViewModel.shared
208

21-
/// Initialize the widget
229
init(
23-
options: FactsWidgetOptions = FactsWidgetOptions(),
2410
isEditing: Bool = false,
2511
onEditingEnd: (() -> Void)? = nil
2612
) {
27-
self.options = options
2813
self.isEditing = isEditing
2914
self.onEditingEnd = onEditingEnd
3015
}
3116

32-
/// Initialize with a custom view model (for previews)
33-
init(
34-
viewModel: FactsViewModel,
35-
options: FactsWidgetOptions = FactsWidgetOptions(),
36-
isEditing: Bool = false,
37-
onEditingEnd: (() -> Void)? = nil
38-
) {
39-
self.options = options
40-
self.isEditing = isEditing
41-
self.onEditingEnd = onEditingEnd
42-
_viewModel = StateObject(wrappedValue: viewModel)
43-
}
44-
4517
var body: some View {
4618
BaseWidget(
4719
type: .facts,
4820
isEditing: isEditing,
4921
onEditingEnd: onEditingEnd
5022
) {
51-
VStack(spacing: 0) {
52-
TitleText(viewModel.fact)
53-
.lineLimit(2)
54-
.frame(maxWidth: .infinity, alignment: .leading)
55-
56-
if options.showSource {
57-
WidgetContentBuilder.sourceRow(source: "synonym.to")
58-
}
59-
}
23+
FactsWidgetWideContent(fact: viewModel.fact)
6024
}
6125
}
6226
}
6327

6428
#Preview {
6529
VStack(spacing: 16) {
6630
FactsWidget()
67-
68-
FactsWidget(
69-
options: FactsWidgetOptions(showSource: false)
70-
)
71-
72-
FactsWidget(
73-
isEditing: true
74-
)
31+
FactsWidget(isEditing: true)
7532
}
7633
.padding()
7734
.background(Color.black)

0 commit comments

Comments
 (0)