197 lines
6.5 KiB
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))
|
|
}
|
|
}
|