import SwiftUI import WidgetKit private struct ActivityEntry: TimelineEntry { let date: Date let serverName: String let rows: [ActivityRow] let message: String? } private struct PullsEntry: TimelineEntry { let date: Date let serverName: String let rows: [PullRow] let message: String? } private struct ActivityProvider: AppIntentTimelineProvider { func placeholder(in context: Context) -> ActivityEntry { ActivityEntry( date: .now, serverName: "My Server", rows: activityPlaceholderRows(for: context.family), message: nil ) } func snapshot(for configuration: ActivityWidgetIntent, in context: Context) async -> ActivityEntry { context.isPreview ? placeholder(in: context) : await load(configuration, family: context.family) } func timeline(for configuration: ActivityWidgetIntent, in context: Context) async -> Timeline { Timeline( entries: [await load(configuration, family: context.family)], policy: .after(Date.now.addingTimeInterval(15 * 60)) ) } private func load(_ configuration: ActivityWidgetIntent, family: WidgetFamily) async -> ActivityEntry { guard let selection = configuration.server, let server = WidgetEnvironment.server(for: selection) else { return ActivityEntry( date: .now, serverName: "Activity", rows: [], message: "Add a server in Gotcha, then configure this widget." ) } do { let limit = activityLimit(for: family) let page = try await WidgetEnvironment.core().widgetActivity(serverId: server.id, limit: limit) return ActivityEntry( date: .now, serverName: page.serverName, rows: page.rows, message: page.rows.isEmpty ? "No recent activity." : nil ) } catch { return ActivityEntry( date: .now, serverName: server.name, rows: [], message: error.localizedDescription ) } } } private struct PullsProvider: AppIntentTimelineProvider { func placeholder(in context: Context) -> PullsEntry { PullsEntry( date: .now, serverName: "My Server", rows: pullPlaceholderRows(for: context.family), message: nil ) } func snapshot(for configuration: PullsWidgetIntent, in context: Context) async -> PullsEntry { context.isPreview ? placeholder(in: context) : await load(configuration, family: context.family) } func timeline(for configuration: PullsWidgetIntent, in context: Context) async -> Timeline { Timeline( entries: [await load(configuration, family: context.family)], policy: .after(Date.now.addingTimeInterval(15 * 60)) ) } private func load(_ configuration: PullsWidgetIntent, family: WidgetFamily) async -> PullsEntry { guard let selection = configuration.server, let server = WidgetEnvironment.server(for: selection) else { return PullsEntry( date: .now, serverName: "Pull Requests", rows: [], message: "Add a server in Gotcha, then configure this widget." ) } do { let limit = pullLimit(for: family) let page = try await WidgetEnvironment.core().widgetPulls(serverId: server.id, limit: limit) return PullsEntry( date: .now, serverName: page.serverName, rows: page.rows, message: page.rows.isEmpty ? "No open pull requests." : nil ) } catch { return PullsEntry( date: .now, serverName: server.name, rows: [], message: error.localizedDescription ) } } } 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 { let title: String let symbol: String let server: String let updated: Date var body: some View { HStack(alignment: .firstTextBaseline) { Label(title, systemImage: symbol) .font(.headline) Spacer(minLength: 8) VStack(alignment: .trailing, spacing: 1) { Text(server) .font(.caption.weight(.semibold)) .lineLimit(1) Text(updated, style: .relative) .font(.caption2) .foregroundStyle(.secondary) } } } } private struct ActivityWidgetView: View { let entry: ActivityEntry var body: some View { VStack(alignment: .leading, spacing: 8) { WidgetHeader( title: "Activity", symbol: "waveform.path.ecg", server: entry.serverName, updated: entry.date ) Divider() if let message = entry.message { emptyState(message, symbol: "clock.arrow.circlepath") } else { VStack(alignment: .leading, spacing: 7) { ForEach(Array(entry.rows.enumerated()), id: \.offset) { _, row in HStack(alignment: .firstTextBaseline, spacing: 8) { Image(systemName: symbol(for: row.icon)) .foregroundStyle(.tint) .frame(width: 16) Text(row.title) .font(.caption) .lineLimit(1) Spacer(minLength: 4) Text(row.meta) .font(.caption2) .foregroundStyle(.secondary) .lineLimit(1) } .accessibilityElement(children: .combine) } } .privacySensitive() } } .containerBackground(.background, for: .widget) .widgetURL(URL(string: "gotcha://home")) } private func symbol(for icon: ActivityIcon) -> String { switch icon { case .repository: "shippingbox" case .issue: "exclamationmark.circle" case .pullRequest: "arrow.triangle.pull" case .branch: "arrow.triangle.branch" case .tag: "tag" case .push: "arrow.up.circle" case .release: "shippingbox.fill" } } } private struct PullsWidgetView: View { let entry: PullsEntry var body: some View { VStack(alignment: .leading, spacing: 8) { WidgetHeader( title: "Open Pull Requests", symbol: "arrow.triangle.pull", server: entry.serverName, updated: entry.date ) Divider() if let message = entry.message { emptyState(message, symbol: "arrow.triangle.pull") } else { VStack(alignment: .leading, spacing: 8) { ForEach(Array(entry.rows.enumerated()), id: \.offset) { _, row in VStack(alignment: .leading, spacing: 2) { Text(row.title) .font(.caption.weight(.semibold)) .lineLimit(2) Text(row.meta) .font(.caption2) .foregroundStyle(.secondary) .lineLimit(1) } .accessibilityElement(children: .combine) } } .privacySensitive() } } .containerBackground(.background, for: .widget) .widgetURL(URL(string: "gotcha://pulls")) } } private func emptyState(_ message: String, symbol: String) -> some View { VStack(spacing: 8) { Spacer() Image(systemName: symbol) .font(.title2) .foregroundStyle(.secondary) Text(message) .font(.caption) .foregroundStyle(.secondary) .multilineTextAlignment(.center) Spacer() } .frame(maxWidth: .infinity) } struct ActivityWidget: Widget { let kind = "GotchaActivityWidget" var body: some WidgetConfiguration { AppIntentConfiguration(kind: kind, intent: ActivityWidgetIntent.self, provider: ActivityProvider()) { ActivityWidgetView(entry: $0) } .configurationDisplayName("Recent Activity") .description("See the latest activity from a configured Gitea or Forgejo server.") .supportedFamilies([.systemMedium, .systemLarge]) } } struct PullsWidget: Widget { let kind = "GotchaPullsWidget" var body: some WidgetConfiguration { AppIntentConfiguration(kind: kind, intent: PullsWidgetIntent.self, provider: PullsProvider()) { PullsWidgetView(entry: $0) } .configurationDisplayName("Open Pull Requests") .description("See open pull requests from a configured Gitea or Forgejo server.") .supportedFamilies([.systemMedium, .systemLarge]) } } @main struct GotchaWidgetBundle: WidgetBundle { var body: some Widget { ActivityWidget() PullsWidget() } }