All checks were successful
CI / build-and-deploy (push) Successful in 41s
- Save serverConfig to App Group on every app launch so ShareExtension can always find credentials without requiring a disconnect/reconnect flow - Add MarksWidget target with RandomBookmarkWidget (random saved bookmark, reloads hourly) using file-based App Group storage to avoid CFPreferences issues - Extract BookmarkListRow as shared component with editorial typography tweaks (17pt semibold title, 13pt domain, relative date, 32×32 favicon, italic AI summary, 20pt podcast icon, 16pt horizontal insets) - Add MarksWidget and ShareExtension Xcode schemes for direct run/debug Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
112 lines
4.2 KiB
Swift
112 lines
4.2 KiB
Swift
import WidgetKit
|
|
import SwiftUI
|
|
|
|
struct RecentPodcastsEntry: TimelineEntry {
|
|
let date: Date
|
|
let podcasts: [WidgetPodcast]
|
|
|
|
static let placeholder = RecentPodcastsEntry(date: .now, podcasts: [
|
|
WidgetPodcast(articleUrl: "https://example.com/a", title: "The Future of AI", createdAt: .now),
|
|
WidgetPodcast(articleUrl: "https://news.com/b", title: "Swift Concurrency Deep Dive", createdAt: .now.addingTimeInterval(-86400)),
|
|
WidgetPodcast(articleUrl: "https://blog.dev/c", title: "Building Great iOS Apps", createdAt: .now.addingTimeInterval(-172800)),
|
|
])
|
|
}
|
|
|
|
struct RecentPodcastsProvider: TimelineProvider {
|
|
func placeholder(in context: Context) -> RecentPodcastsEntry { .placeholder }
|
|
|
|
func getSnapshot(in context: Context, completion: @escaping (RecentPodcastsEntry) -> Void) {
|
|
let podcasts = WidgetDataStore.loadPodcasts()
|
|
completion(RecentPodcastsEntry(date: .now, podcasts: Array(podcasts.prefix(3))))
|
|
}
|
|
|
|
func getTimeline(in context: Context, completion: @escaping (Timeline<RecentPodcastsEntry>) -> Void) {
|
|
let podcasts = WidgetDataStore.loadPodcasts()
|
|
let entry = RecentPodcastsEntry(date: .now, podcasts: Array(podcasts.prefix(3)))
|
|
let next = Calendar.current.date(byAdding: .minute, value: 30, to: .now)!
|
|
completion(Timeline(entries: [entry], policy: .after(next)))
|
|
}
|
|
}
|
|
|
|
struct RecentPodcastsWidgetView: View {
|
|
let entry: RecentPodcastsEntry
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 0) {
|
|
HStack {
|
|
Text("Podcasts")
|
|
.font(.system(size: 12, weight: .semibold))
|
|
.foregroundStyle(.secondary)
|
|
Spacer()
|
|
Image(systemName: "headphones")
|
|
.font(.system(size: 11))
|
|
.foregroundStyle(.blue)
|
|
}
|
|
.padding(.bottom, 8)
|
|
|
|
if entry.podcasts.isEmpty {
|
|
Spacer()
|
|
Text("Generate podcasts from your bookmarks to see them here.")
|
|
.font(.system(size: 13))
|
|
.foregroundStyle(.secondary)
|
|
Spacer()
|
|
} else {
|
|
ForEach(Array(entry.podcasts.enumerated()), id: \.element.id) { idx, podcast in
|
|
if idx > 0 { Divider().padding(.vertical, 5) }
|
|
Link(destination: .marksPodcast(podcast.articleUrl)) {
|
|
WidgetPodcastRow(podcast: podcast)
|
|
}
|
|
}
|
|
Spacer(minLength: 0)
|
|
}
|
|
}
|
|
.padding(14)
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
|
.containerBackground(.fill.tertiary, for: .widget)
|
|
}
|
|
}
|
|
|
|
struct WidgetPodcastRow: View {
|
|
let podcast: WidgetPodcast
|
|
|
|
var body: some View {
|
|
HStack(spacing: 8) {
|
|
RoundedRectangle(cornerRadius: 5)
|
|
.fill(Color.blue.opacity(0.1))
|
|
.frame(width: 26, height: 26)
|
|
.overlay {
|
|
Image(systemName: "waveform")
|
|
.font(.system(size: 10, weight: .medium))
|
|
.foregroundStyle(.blue)
|
|
}
|
|
VStack(alignment: .leading, spacing: 1) {
|
|
Text(podcast.title)
|
|
.font(.system(size: 13, weight: .medium))
|
|
.foregroundStyle(.primary)
|
|
.lineLimit(1)
|
|
Text(podcast.createdAt, style: .relative)
|
|
.font(.system(size: 11))
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
Spacer(minLength: 0)
|
|
Image(systemName: "play.circle.fill")
|
|
.font(.system(size: 18))
|
|
.foregroundStyle(.blue.opacity(0.8))
|
|
}
|
|
}
|
|
}
|
|
|
|
struct RecentPodcastsWidget: Widget {
|
|
var body: some WidgetConfiguration {
|
|
StaticConfiguration(
|
|
kind: "com.magicive.marks.RecentPodcasts",
|
|
provider: RecentPodcastsProvider()
|
|
) { entry in
|
|
RecentPodcastsWidgetView(entry: entry)
|
|
}
|
|
.configurationDisplayName("Recent Podcasts")
|
|
.description("Tap to play recently generated podcasts.")
|
|
.supportedFamilies([.systemMedium])
|
|
}
|
|
}
|