NewsBlur/clients/ios/Classes/Story.swift
David Sinclair aa88601905 #1720 (Grid view)
- Implemented theme colors.
- Implemented the content length options.
- Implemented the font size options.
- Implemented the compact / comfortable options.
- Improved the logic for the rounded corners on cards.
2023-03-02 15:31:21 -07:00

309 lines
8.6 KiB
Swift

//
// Story.swift
// NewsBlur
//
// Created by David Sinclair on 2023-02-01.
// Copyright © 2023 NewsBlur. All rights reserved.
//
import Foundation
/// A story, wrapping the dictionary representation.
class Story: Identifiable {
let id = UUID()
let index: Int
// lazy var title: String = {
// return "lazy story #\(index)"
// }()
var dictionary = [String : Any]()
var feedID = ""
var feedName = ""
var title = ""
var content = ""
var dateString = ""
var timestamp = 0
var isRead = false
var isSaved = false
var isShared = false
var score = 0
var hash = ""
var author = ""
var dateAndAuthor: String {
let date = Utilities.formatShortDate(fromTimestamp: timestamp) ?? ""
return author.isEmpty ? date : "\(date) · \(author)"
}
var isRiverOrSocial = true
var feedColorBarLeft: UIColor?
var feedColorBarRight: UIColor?
var isSelected: Bool {
return index == NewsBlurAppDelegate.shared!.storiesCollection.indexOfActiveStory()
}
var isLoaded: Bool {
return !dictionary.isEmpty
}
init(index: Int) {
self.index = index
}
private func string(for key: String) -> String {
return dictionary[key] as? String ?? ""
}
func load() {
guard let appDelegate = NewsBlurAppDelegate.shared, let storiesCollection = appDelegate.storiesCollection,
index < storiesCollection.activeFeedStoryLocations.count,
let row = storiesCollection.activeFeedStoryLocations[index] as? Int,
let story = storiesCollection.activeFeedStories[row] as? [String : Any] else {
return
}
dictionary = story
if let id = dictionary["story_feed_id"] {
feedID = appDelegate.feedIdWithoutSearchQuery("\(id)")
}
var feed: [String : Any]?
if storiesCollection.isRiverOrSocial {
feed = appDelegate.dictActiveFeeds[feedID] as? [String : Any]
}
if feed == nil {
feed = appDelegate.dictFeeds[feedID] as? [String : Any]
}
if let feed {
feedName = feed["feed_title"] as? String ?? ""
feedColorBarLeft = color(for: "favicon_fade", from: feed, default: "707070")
feedColorBarRight = color(for: "favicon_color", from: feed, default: "505050")
}
title = (string(for: "story_title") as NSString).decodingHTMLEntities()
content = String(string(for: "story_content").convertHTML().decodingXMLEntities().decodingHTMLEntities().replacingOccurrences(of: "\n", with: " ").prefix(500))
author = string(for: "story_authors").replacingOccurrences(of: "\"", with: "")
dateString = string(for: "short_parsed_date")
timestamp = dictionary["story_timestamp"] as? Int ?? 0
isSaved = dictionary["starred"] as? Bool ?? false
isShared = dictionary["shared"] as? Bool ?? false
hash = string(for: "story_hash")
if let intelligence = dictionary["intelligence"] as? [String : Any] {
score = Int(NewsBlurAppDelegate.computeStoryScore(intelligence))
}
isRead = score < 0
isRiverOrSocial = storiesCollection.isRiverOrSocial
}
func color(for key: String, from feed: [String : Any], default defaultHex: String) -> UIColor {
let hex = feed[key] as? String ?? defaultHex
let scanner = Scanner(string: hex)
var color: Int64 = 0
scanner.scanHexInt64(&color)
let value = Int(color)
return ThemeManager.shared.fixedColor(fromRGB: value) ?? UIColor.gray
}
}
extension Story: Equatable {
static func == (lhs: Story, rhs: Story) -> Bool {
return lhs.id == rhs.id
}
}
extension Story: CustomDebugStringConvertible {
var debugDescription: String {
return "Story \"\(title)\" in \(feedName)"
}
}
/// A cache of stories for the feed detail grid view.
class StoryCache: ObservableObject {
let appDelegate = NewsBlurAppDelegate.shared!
let settings = StorySettings()
var isGrid: Bool {
return appDelegate.detailViewController.layout == .grid
}
@Published var before = [Story]()
@Published var selected: Story?
@Published var after = [Story]()
func appendStories(beforeSelection: [Int], selectedIndex: Int, afterSelection: [Int]) {
before = beforeSelection.map { Story(index: $0) }
selected = selectedIndex >= 0 ? Story(index: selectedIndex) : nil
after = afterSelection.map { Story(index: $0) }
}
}
class StorySettings {
let defaults = UserDefaults.standard
enum Content: String, RawRepresentable {
case title
case short
case medium
case long
static let titleLimit = 6
static let contentLimit = 10
var limit: Int {
switch self {
case .title:
return 6
case .short:
return 2
case .medium:
return 4
case .long:
return 6
}
}
}
var content: Content {
if let string = defaults.string(forKey: "story_list_preview_text_size"), let value = Content(rawValue: string) {
return value
} else {
return .short
}
}
enum Preview: String, RawRepresentable {
case none
case smallLeft = "small_left"
case largeLeft = "large_left"
case largeRight = "large_right"
case smallRight = "small_right"
var isLeft: Bool {
return [.smallLeft, .largeLeft].contains(self)
}
var isSmall: Bool {
return [.smallLeft, .smallRight].contains(self)
}
}
var preview: Preview {
if let string = defaults.string(forKey: "story_list_preview_images_size"), let value = Preview(rawValue: string) {
return value
} else {
return .smallRight
}
}
enum FontSize: String, RawRepresentable {
case xs
case small
case medium
case large
case xl
var offset: CGFloat {
switch self {
case .xs:
return -2
case .small:
return -1
case .medium:
return 0
case .large:
return 1
case .xl:
return 2
}
}
}
var fontSize: FontSize {
if let string = defaults.string(forKey: "feed_list_font_size"), let value = FontSize(rawValue: string) {
return value
} else {
return .medium
}
}
enum Spacing: String, RawRepresentable {
case compact
case comfortable
}
var spacing: Spacing {
if let string = defaults.string(forKey: "feed_list_spacing"), let value = Spacing(rawValue: string) {
return value
} else {
return .comfortable
}
}
// enum GridColumns: String {
// case auto = "auto"
// case two = "2"
// case three = "3"
// case four = "4"
//
// var number: Int {
// switch self {
// case .two:
// return 2
// case .three:
// return 3
// case .four:
// return 4
// default:
// if NewsBlurAppDelegate.shared.isCompactWidth {
// return 1
// } else {
// return 4
// }
// }
// }
// }
var gridColumns: Int {
if NewsBlurAppDelegate.shared.isCompactWidth {
return 1
}
guard let pref = UserDefaults.standard.string(forKey: "grid_columns"), let columns = Int(pref) else {
//TODO: 🚧 could have extra logic to determine the ideal number of columns
return 4
}
return columns
}
var gridHeight: CGFloat {
guard let pref = UserDefaults.standard.string(forKey: "grid_height") else {
return 400
}
switch pref {
case "xs":
return 250
case "short":
return 300
case "tall":
return 500
case "xl":
return 600
default:
return 400
}
}
}