325 lines
11 KiB
Swift
325 lines
11 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: [
|
|
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
|
|
)
|
|
}
|
|
|
|
func snapshot(for configuration: ActivityWidgetIntent, in context: Context) async -> ActivityEntry {
|
|
context.isPreview ? placeholder(in: context) : await load(configuration)
|
|
}
|
|
|
|
func timeline(for configuration: ActivityWidgetIntent, in context: Context) async -> Timeline<ActivityEntry> {
|
|
Timeline(
|
|
entries: [await load(configuration)],
|
|
policy: .after(Date.now.addingTimeInterval(15 * 60))
|
|
)
|
|
}
|
|
|
|
private func load(_ configuration: ActivityWidgetIntent) 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 page = try await WidgetEnvironment.core().widgetActivity(serverId: server.id, limit: 6)
|
|
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: [
|
|
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
|
|
)
|
|
}
|
|
|
|
func snapshot(for configuration: PullsWidgetIntent, in context: Context) async -> PullsEntry {
|
|
context.isPreview ? placeholder(in: context) : await load(configuration)
|
|
}
|
|
|
|
func timeline(for configuration: PullsWidgetIntent, in context: Context) async -> Timeline<PullsEntry> {
|
|
Timeline(
|
|
entries: [await load(configuration)],
|
|
policy: .after(Date.now.addingTimeInterval(15 * 60))
|
|
)
|
|
}
|
|
|
|
private func load(_ configuration: PullsWidgetIntent) 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 page = try await WidgetEnvironment.core().widgetPulls(serverId: server.id, limit: 5)
|
|
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 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([.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([.systemLarge])
|
|
}
|
|
}
|
|
|
|
@main
|
|
struct GotchaWidgetBundle: WidgetBundle {
|
|
var body: some Widget {
|
|
ActivityWidget()
|
|
PullsWidget()
|
|
}
|
|
}
|