frenzy-ios/Widgets/Recents.swift

197 lines
6.5 KiB
Swift

//
// Recents.swift
// Reader
//
// Created by Shadowfacts on 6/19/22.
//
import WidgetKit
import SwiftUI
import Persistence
struct RecentsProvider: IntentTimelineProvider {
func placeholder(in context: Context) -> RecentsEntry {
RecentsEntry(items: [], configuration: ConfigurationIntent())
}
func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (RecentsEntry) -> ()) {
let entry = RecentsEntry(items: getItems(), configuration: configuration)
completion(entry)
}
func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<RecentsEntry>) -> ()) {
// TODO: get account from configuration intent
let entry = RecentsEntry(items: getItems(), configuration: configuration)
let refreshDate = Calendar.current.date(byAdding: .hour, value: 1, to: Date())!
let timeline = Timeline(entries: [entry], policy: .after(refreshDate))
completion(timeline)
}
private func getItems() -> [WidgetData.Item] {
guard let account = LocalData.mostRecentAccount() else {
return []
}
return WidgetData.load(account: account).recentItems
}
}
struct RecentsEntry: TimelineEntry {
let date = Date()
let items: [WidgetData.Item]
let configuration: ConfigurationIntent
}
struct RecentsEntryView: View {
let entry: RecentsEntry
@Environment(\.widgetFamily) var family
var body: some View {
if entry.items.isEmpty {
} else {
switch family {
case .systemSmall:
SquareItemView(item: entry.items[0])
.padding()
case .systemMedium, .systemLarge:
VStack {
ForEach(Array(entry.items.prefix(family.maxItemCount).enumerated()), id: \.element.id) { (index, item) in
if index != 0 {
Divider()
}
ItemListEntryView(item: item)
Spacer(minLength: 4)
}
}
.padding()
case .systemExtraLarge:
if #available(iOS 16.0, *) {
VStack {
ForEach(Array(stride(from: 0, to: min(entry.items.count, family.maxItemCount), by: 2)), id: \.self) { idx in
HStack {
ItemListEntryView(item: entry.items[idx])
if idx + 1 < entry.items.count {
Divider()
ItemListEntryView(item: entry.items[idx + 1])
}
}
if idx + 2 < entry.items.count {
Spacer()
Divider()
}
}
}
.padding()
} else {
Text("Requires iOS 16")
}
default:
fatalError("unreachable")
}
}
}
}
private extension WidgetFamily {
var maxItemCount: Int {
switch self {
case .systemSmall:
return 1
case .systemMedium:
return 2
case .systemLarge:
return 4
case .systemExtraLarge:
return 8
default:
return 0
}
}
}
private var feedFont = Font.subheadline.weight(.medium).italic()
private var titleUIFont: UIFont {
// TODO: this should use the compressed SF Pro variant, but there's no API to get at it
let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .headline).withSymbolicTraits(.traitCondensed)!
return UIFont(descriptor: descriptor, size: 0)
}
private var titleFont = Font(titleUIFont).leading(.tight)
struct SquareItemView: View {
let item: WidgetData.Item
var body: some View {
VStack(alignment: .leading) {
HStack {
Text(verbatim: item.feedTitle ?? "")
.font(feedFont)
.foregroundColor(.red)
// force the vstack to be as wide as possible
Spacer(minLength: 0)
}
Text(verbatim: item.title ?? "")
.font(titleFont)
if let published = item.published {
Text(published, format: .relative(presentation: .numeric, unitsStyle: .narrow))
.font(.caption)
}
Spacer(minLength: 0)
}
.widgetURL(item.launchURL)
}
}
struct ItemListEntryView: View {
let item: WidgetData.Item
var body: some View {
Link(destination: item.launchURL) {
VStack(alignment: .leading) {
HStack {
Text(verbatim: item.feedTitle ?? "")
.font(feedFont)
.foregroundColor(.red)
Spacer()
if let published = item.published {
Text(published, format: .relative(presentation: .numeric, unitsStyle: .narrow))
.font(.caption)
}
}
Text(verbatim: item.title ?? "")
.font(titleFont)
.lineLimit(3)
Spacer(minLength: 0)
}
}
}
}
struct Recents_Previews: PreviewProvider {
static let item1 = WidgetData.Item(
id: "1",
feedTitle: "Daring Fireball",
title: "There's a Privacy Angle on Apple's Decision to Finance Apple Pay Layer On Its Own",
published: Calendar.current.date(byAdding: .hour, value: -1, to: Date())!
)
static let item2 = WidgetData.Item(
id: "2",
feedTitle: "Ars Technica",
title: "Senate bill would ban data brokers from selling location and health data",
published: Calendar.current.date(byAdding: .hour, value: -2, to: Date())!
)
static var previews: some View {
// RecentsEntryView(entry: RecentsEntry(items: [item1], configuration: ConfigurationIntent()))
// .previewContext(WidgetPreviewContext(family: .systemSmall))
RecentsEntryView(entry: RecentsEntry(items: [item1, item2], configuration: ConfigurationIntent()))
.previewContext(WidgetPreviewContext(family: .systemMedium))
}
}