343 lines
12 KiB
Swift
343 lines
12 KiB
Swift
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<ActivityEntry> {
|
|
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<PullsEntry> {
|
|
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()
|
|
}
|
|
}
|