NewsBlur/clients/ios/Classes/TrainerView.swift
David Sinclair cffe4262d3 #1247 (Mac Catalyst edition)
- Fixed trainer not working in Everything folder.
- Fixed crash when loading images during a reload.
- Fixed story blanking out after 15 minutes (due to a WebKit crash; now auto-recovers).
2024-06-27 20:06:25 -04:00

218 lines
8.1 KiB
Swift

//
// TrainerView.swift
// NewsBlur
//
// Created by David Sinclair on 2024-04-02.
// Copyright © 2024 NewsBlur. All rights reserved.
//
import SwiftUI
/// A protocol of interaction between the trainer view and the enclosing view controller.
protocol TrainerInteraction {
var isStoryTrainer: Bool { get set }
}
struct TrainerView: View {
var interaction: TrainerInteraction
@ObservedObject var cache: StoryCache
let columns = [GridItem(.adaptive(minimum: 50))]
var body: some View {
VStack(alignment: .leading) {
Text("What do you 👍 \(Text("like").colored(.green)) and 👎 \(Text("dislike").colored(.red)) about this \(feedOrStoryLowercase)?")
.font(font(named: "WhitneySSm-Medium", size: 16))
.padding()
List {
Section(content: {
VStack(alignment: .leading) {
if interaction.isStoryTrainer {
Text("Choose one or more words from the title:")
.font(font(named: "WhitneySSm-Medium", size: 12))
.padding([.top], 10)
WrappingHStack(models: titleWords, horizontalSpacing: 1) { word in
Button(action: {
if addingTitle.isEmpty {
addingTitle = word
} else {
addingTitle.append(" \(word)")
}
}, label: {
TrainerWord(word: word)
})
.buttonStyle(BorderlessButtonStyle())
.padding([.top, .bottom], 5)
}
if !addingTitle.isEmpty {
HStack {
Button(action: {
cache.appDelegate.toggleTitleClassifier(addingTitle, feedId: feed?.id, score: 0)
addingTitle = ""
}, label: {
TrainerCapsule(score: .none, header: "Title", value: addingTitle)
})
.buttonStyle(BorderlessButtonStyle())
.padding([.top, .bottom], 5)
Button {
addingTitle = ""
} label: {
Image(systemName: "xmark.circle.fill")
.imageScale(.large)
.foregroundColor(.gray)
}
}
}
}
WrappingHStack(models: titles) { title in
Button(action: {
cache.appDelegate.toggleTitleClassifier(title.name, feedId: feed?.id, score: 0)
}, label: {
TrainerCapsule(score: title.score, header: "Title", value: title.name, count: title.count)
})
.buttonStyle(BorderlessButtonStyle())
.padding([.top, .bottom], 5)
}
}
}, header: {
header(story: "Story Title", feed: "Titles & Phrases")
})
Section(content: {
WrappingHStack(models: authors) { author in
Button(action: {
cache.appDelegate.toggleAuthorClassifier(author.name, feedId: feed?.id)
}, label: {
TrainerCapsule(score: author.score, header: "Author", value: author.name, count: author.count)
})
.buttonStyle(BorderlessButtonStyle())
.padding([.top, .bottom], 5)
}
}, header: {
header(story: "Story Author", feed: "Authors")
})
Section(content: {
WrappingHStack(models: tags) { tag in
Button(action: {
cache.appDelegate.toggleTagClassifier(tag.name, feedId: feed?.id)
}, label: {
TrainerCapsule(score: tag.score, header: "Tag", value: tag.name, count: tag.count)
})
.buttonStyle(BorderlessButtonStyle())
.padding([.top, .bottom], 5)
}
}, header: {
header(story: "Story Categories & Tags", feed: "Categories & Tags")
})
Section(content: {
HStack {
if let feed = feed {
Button(action: {
cache.appDelegate.toggleFeedClassifier(feed.id)
}, label: {
TrainerCapsule(score: score(key: "feeds", value: feed.id), header: "Site", image: feed.image, value: feed.name)
})
.buttonStyle(BorderlessButtonStyle())
.padding([.top, .bottom], 5)
}
}
}, header: {
header(feed: "Everything by This Publisher")
})
}
.font(font(named: "WhitneySSm-Medium", size: 12))
}
.onAppear {
addingTitle = ""
}
}
func font(named: String, size: CGFloat) -> Font {
return Font.custom(named, size: size + cache.settings.fontSize.offset, relativeTo: .caption)
}
func reload() {
cache.reload()
addingTitle = ""
}
var feedOrStoryLowercase: String {
return interaction.isStoryTrainer ? "story" : "site"
}
@ViewBuilder
func header(story: String? = nil, feed: String) -> some View {
if let story {
Text(interaction.isStoryTrainer ? story : feed)
.font(font(named: "WhitneySSm-Medium", size: 16))
} else {
Text(feed)
.font(font(named: "WhitneySSm-Medium", size: 16))
}
}
func score(key: String, value: String) -> Feed.Score {
guard let classifiers = feed?.classifiers(for: key),
let score = classifiers[value] as? Int else {
return .none
}
if score > 0 {
return .like
} else if score < 0 {
return .dislike
} else {
return .none
}
}
var titleWords: [String] {
if interaction.isStoryTrainer, let story = cache.selected {
return story.title.components(separatedBy: .whitespaces)
} else {
return []
}
}
@State private var addingTitle = ""
var feed: Feed? {
return cache.currentFeed ?? cache.selected?.feed
}
var titles: [Feed.Training] {
if interaction.isStoryTrainer {
return cache.selected?.titles ?? []
} else {
return feed?.titles ?? []
}
}
var authors: [Feed.Training] {
if interaction.isStoryTrainer {
return cache.selected?.authors ?? []
} else {
return feed?.authors ?? []
}
}
var tags: [Feed.Training] {
if interaction.isStoryTrainer {
return cache.selected?.tags ?? []
} else {
return feed?.tags ?? []
}
}
}
//#Preview {
// TrainerViewController()
//}