Fix ShareExtension "Open Marks first" + add widget + BookmarkListRow polish
All checks were successful
CI / build-and-deploy (push) Successful in 41s
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>
This commit is contained in:
111
MarksWidget/RecentPodcastsWidget.swift
Normal file
111
MarksWidget/RecentPodcastsWidget.swift
Normal file
@@ -0,0 +1,111 @@
|
||||
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])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user