mirror of
https://github.com/samuelclay/NewsBlur.git
synced 2025-08-31 21:41:33 +00:00

- 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).
218 lines
8.1 KiB
Swift
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()
|
|
//}
|