NewsBlur/clients/ios/Classes/Story.swift
David Sinclair 893c57d2aa #1720 (Grid view)
- Cards now include the feed bar, favicon, and name.
- Cards now include unread, saved, and shared indicators.
- Previews now support the none, left/right, and small/large options.
- Several layout tweaks.
- The Grid layout now supports the columns and length settings.
- The Grid layout now includes a bar above the story to close it.
- Tidied the code a bit.
2023-02-02 21:41:10 -06:00

256 lines
7.4 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 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))
}
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 array = [NSNumber(value: color)]
return ThemeManager.color(fromRGB: array)
}
}
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
//TODO: 🚧
// var listContent: Int {
// NSString *preferenceKey = @"story_list_preview_text_size";
// NSArray *titles = @[@"Title", @"content_preview_small.png", @"content_preview_medium.png", @"content_preview_large.png"];
// NSArray *values = @[@"title", @"short", @"medium", @"long"];
// }
enum ListPreview {
case none
case smallLeft
case largeLeft
case largeRight
case smallRight
var isLeft: Bool {
return [.smallLeft, .largeLeft].contains(self)
}
var isSmall: Bool {
return [.smallLeft, .smallRight].contains(self)
}
}
var listPreview: ListPreview {
switch defaults.string(forKey: "story_list_preview_images_size") {
case "none":
return .none
case "small_left":
return .smallLeft
case "large_left":
return .largeLeft
case "large_right":
return .largeRight
default:
return .smallRight
}
}
//TODO: 🚧
// NSString *preferenceKey = @"feed_list_font_size";
// NSArray *titles = @[@"XS", @"S", @"M", @"L", @"XL"];
// NSArray *values = @[@"xs", @"small", @"medium", @"large", @"xl"];
//TODO: 🚧
// preferenceKey = @"feed_list_spacing";
// titles = @[@"Compact", @"Comfortable"];
// values = @[@"compact", @"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
}
}
}