Use widget space by family

This commit is contained in:
Georg Bauer
2026-08-03 22:44:04 +02:00
parent 1a43bf1ba8
commit 9d7ce83cb1
2 changed files with 79 additions and 59 deletions

View File

@@ -235,13 +235,15 @@ answer before uploading it to App Store Connect.
- [ ] Upgrade over a build with an existing server. Launch Gotcha once, then - [ ] Upgrade over a build with an existing server. Launch Gotcha once, then
add both **Recent Activity** and **Open Pull Requests** from the system add both **Recent Activity** and **Open Pull Requests** from the system
widget gallery; the saved server and Keychain token work without re-entry. widget gallery; the saved server and Keychain token work without re-entry.
- [ ] Both widgets are offered only in the large system size and use native - [ ] Both widgets are offered in medium and large system sizes and use native
widget margins, typography, tint, relative update time, and light/dark widget margins, typography, tint, relative update time, and light/dark
appearances without clipping at the largest accessibility text size. appearances without clipping at the largest accessibility text size.
- [ ] Recent Activity shows up to six latest activity rows for its configured - [ ] In medium size, Recent Activity shows up to three latest activity rows
server. Open Pull Requests shows up to five open pull requests. Loading, and Open Pull Requests shows up to two open pull requests. In large size,
empty, missing-server, and API-error states remain legible and do not they show up to ten and five rows respectively. Confirm both the widget
expose account data while the device is locked. gallery previews and live configured timelines use those family-specific
row counts. Loading, empty, missing-server, and API-error states remain
legible and do not expose account data while the device is locked.
- [ ] Add a second server profile, long-press each widget, choose **Edit - [ ] Add a second server profile, long-press each widget, choose **Edit
Widget**, and assign a different server to each. The displayed server Widget**, and assign a different server to each. The displayed server
name and rows update independently; deleting a selected server changes name and rows update independently; deleting a selected server changes

View File

@@ -20,46 +20,23 @@ private struct ActivityProvider: AppIntentTimelineProvider {
ActivityEntry( ActivityEntry(
date: .now, date: .now,
serverName: "My Server", serverName: "My Server",
rows: [ rows: activityPlaceholderRows(for: context.family),
ActivityRow(
icon: .push,
title: "Pushed to main in owner/project",
detail: "",
meta: "Just now",
target: .commit,
owner: "owner",
repository: "project",
number: 0,
sha: ""
),
ActivityRow(
icon: .issue,
title: "Opened an issue in owner/project",
detail: "",
meta: "5m ago",
target: .issue,
owner: "owner",
repository: "project",
number: 1,
sha: ""
),
],
message: nil message: nil
) )
} }
func snapshot(for configuration: ActivityWidgetIntent, in context: Context) async -> ActivityEntry { func snapshot(for configuration: ActivityWidgetIntent, in context: Context) async -> ActivityEntry {
context.isPreview ? placeholder(in: context) : await load(configuration) context.isPreview ? placeholder(in: context) : await load(configuration, family: context.family)
} }
func timeline(for configuration: ActivityWidgetIntent, in context: Context) async -> Timeline<ActivityEntry> { func timeline(for configuration: ActivityWidgetIntent, in context: Context) async -> Timeline<ActivityEntry> {
Timeline( Timeline(
entries: [await load(configuration)], entries: [await load(configuration, family: context.family)],
policy: .after(Date.now.addingTimeInterval(15 * 60)) policy: .after(Date.now.addingTimeInterval(15 * 60))
) )
} }
private func load(_ configuration: ActivityWidgetIntent) async -> ActivityEntry { private func load(_ configuration: ActivityWidgetIntent, family: WidgetFamily) async -> ActivityEntry {
guard guard
let selection = configuration.server, let selection = configuration.server,
let server = WidgetEnvironment.server(for: selection) let server = WidgetEnvironment.server(for: selection)
@@ -72,7 +49,8 @@ private struct ActivityProvider: AppIntentTimelineProvider {
) )
} }
do { do {
let page = try await WidgetEnvironment.core().widgetActivity(serverId: server.id, limit: 6) let limit = activityLimit(for: family)
let page = try await WidgetEnvironment.core().widgetActivity(serverId: server.id, limit: limit)
return ActivityEntry( return ActivityEntry(
date: .now, date: .now,
serverName: page.serverName, serverName: page.serverName,
@@ -95,42 +73,23 @@ private struct PullsProvider: AppIntentTimelineProvider {
PullsEntry( PullsEntry(
date: .now, date: .now,
serverName: "My Server", serverName: "My Server",
rows: [ rows: pullPlaceholderRows(for: context.family),
PullRow(
number: 42,
owner: "owner",
repository: "project",
state: .open,
title: "project #42\nImprove the activity screen",
summary: "",
meta: "Open · developer · Just now"
),
PullRow(
number: 41,
owner: "owner",
repository: "project",
state: .open,
title: "project #41\nFix repository navigation",
summary: "",
meta: "Open · developer · 10m ago"
),
],
message: nil message: nil
) )
} }
func snapshot(for configuration: PullsWidgetIntent, in context: Context) async -> PullsEntry { func snapshot(for configuration: PullsWidgetIntent, in context: Context) async -> PullsEntry {
context.isPreview ? placeholder(in: context) : await load(configuration) context.isPreview ? placeholder(in: context) : await load(configuration, family: context.family)
} }
func timeline(for configuration: PullsWidgetIntent, in context: Context) async -> Timeline<PullsEntry> { func timeline(for configuration: PullsWidgetIntent, in context: Context) async -> Timeline<PullsEntry> {
Timeline( Timeline(
entries: [await load(configuration)], entries: [await load(configuration, family: context.family)],
policy: .after(Date.now.addingTimeInterval(15 * 60)) policy: .after(Date.now.addingTimeInterval(15 * 60))
) )
} }
private func load(_ configuration: PullsWidgetIntent) async -> PullsEntry { private func load(_ configuration: PullsWidgetIntent, family: WidgetFamily) async -> PullsEntry {
guard guard
let selection = configuration.server, let selection = configuration.server,
let server = WidgetEnvironment.server(for: selection) let server = WidgetEnvironment.server(for: selection)
@@ -143,7 +102,8 @@ private struct PullsProvider: AppIntentTimelineProvider {
) )
} }
do { do {
let page = try await WidgetEnvironment.core().widgetPulls(serverId: server.id, limit: 5) let limit = pullLimit(for: family)
let page = try await WidgetEnvironment.core().widgetPulls(serverId: server.id, limit: limit)
return PullsEntry( return PullsEntry(
date: .now, date: .now,
serverName: page.serverName, serverName: page.serverName,
@@ -161,6 +121,64 @@ private struct PullsProvider: AppIntentTimelineProvider {
} }
} }
private func activityLimit(for family: WidgetFamily) -> UInt32 {
family == .systemMedium ? 3 : 10
}
private func pullLimit(for family: WidgetFamily) -> UInt32 {
family == .systemMedium ? 2 : 5
}
private func activityPlaceholderRows(for family: WidgetFamily) -> [ActivityRow] {
let samples: [(ActivityIcon, String, String)] = [
(.push, "Pushed to main in owner/project", "Just now"),
(.issue, "Opened an issue in owner/project", "5m ago"),
(.pullRequest, "Merged a pull request in owner/project", "12m ago"),
(.branch, "Created branch feature/widget", "25m ago"),
(.tag, "Published tag v1.0.0", "40m ago"),
(.release, "Published a release in owner/project", "1h ago"),
(.repository, "Created repository owner/new-project", "2h ago"),
(.push, "Pushed to release in owner/project", "3h ago"),
(.issue, "Closed an issue in owner/project", "4h ago"),
(.pullRequest, "Opened a pull request in owner/project", "5h ago"),
]
return samples.prefix(Int(activityLimit(for: family))).enumerated().map { index, sample in
ActivityRow(
icon: sample.0,
title: sample.1,
detail: "",
meta: sample.2,
target: .commit,
owner: "owner",
repository: "project",
number: Int64(index),
sha: ""
)
}
}
private func pullPlaceholderRows(for family: WidgetFamily) -> [PullRow] {
let titles = [
"Improve the activity screen",
"Fix repository navigation",
"Add compact widget layouts",
"Refresh server configuration",
"Polish issue filters",
]
return titles.prefix(Int(pullLimit(for: family))).enumerated().map { index, title in
let number = Int64(42 - index)
return PullRow(
number: number,
owner: "owner",
repository: "project",
state: .open,
title: "project #\(number)\n\(title)",
summary: "",
meta: "Open · developer · \(index * 10)m ago"
)
}
}
private struct WidgetHeader: View { private struct WidgetHeader: View {
let title: String let title: String
let symbol: String let symbol: String
@@ -298,7 +316,7 @@ struct ActivityWidget: Widget {
} }
.configurationDisplayName("Recent Activity") .configurationDisplayName("Recent Activity")
.description("See the latest activity from a configured Gitea or Forgejo server.") .description("See the latest activity from a configured Gitea or Forgejo server.")
.supportedFamilies([.systemLarge]) .supportedFamilies([.systemMedium, .systemLarge])
} }
} }
@@ -311,7 +329,7 @@ struct PullsWidget: Widget {
} }
.configurationDisplayName("Open Pull Requests") .configurationDisplayName("Open Pull Requests")
.description("See open pull requests from a configured Gitea or Forgejo server.") .description("See open pull requests from a configured Gitea or Forgejo server.")
.supportedFamilies([.systemLarge]) .supportedFamilies([.systemMedium, .systemLarge])
} }
} }