mirror of
https://github.com/viq/NewsBlur.git
synced 2025-08-31 22:20:12 +00:00
#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.
This commit is contained in:
parent
2b10b82c75
commit
aa88601905
5 changed files with 168 additions and 50 deletions
|
@ -17,10 +17,9 @@ struct CardView: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack(alignment: .leading) {
|
ZStack(alignment: .leading) {
|
||||||
if story.isSelected || cache.isGrid {
|
if story.isSelected || cache.isGrid {
|
||||||
RoundedRectangle(cornerRadius: 10).foregroundColor(.init(white: 0.9))
|
RoundedRectangle(cornerRadius: 10).foregroundColor(highlightColor)
|
||||||
|
|
||||||
CardFeedBarView(cache: cache, story: story)
|
CardFeedBarView(cache: cache, story: story)
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
|
||||||
.padding(.leading, 2)
|
.padding(.leading, 2)
|
||||||
} else {
|
} else {
|
||||||
CardFeedBarView(cache: cache, story: story)
|
CardFeedBarView(cache: cache, story: story)
|
||||||
|
@ -39,15 +38,16 @@ struct CardView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
if !cache.isGrid, cache.settings.listPreview.isLeft, let previewImage {
|
if !cache.isGrid, cache.settings.preview.isLeft, let previewImage {
|
||||||
listPreview(image: previewImage)
|
listPreview(image: previewImage)
|
||||||
}
|
}
|
||||||
|
|
||||||
CardContentView(cache: cache, story: story)
|
CardContentView(cache: cache, story: story)
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
|
||||||
.padding(15)
|
.padding([.leading, .trailing], 15)
|
||||||
|
.padding([.top, .bottom], cache.settings.spacing == .compact ? 10 : 15)
|
||||||
|
|
||||||
if !cache.isGrid, !cache.settings.listPreview.isLeft, let previewImage {
|
if !cache.isGrid, !cache.settings.preview.isLeft, let previewImage {
|
||||||
listPreview(image: previewImage)
|
listPreview(image: previewImage)
|
||||||
|
|
||||||
// Image(uiImage: previewImage)
|
// Image(uiImage: previewImage)
|
||||||
|
@ -60,6 +60,9 @@ struct CardView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.if(cache.isGrid || story.isSelected) { view in
|
||||||
|
view.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||||
|
}
|
||||||
.if(story.isSelected) { view in
|
.if(story.isSelected) { view in
|
||||||
view.padding(10)
|
view.padding(10)
|
||||||
}
|
}
|
||||||
|
@ -130,8 +133,16 @@ struct CardView: View {
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
var highlightColor: Color {
|
||||||
|
if cache.isGrid {
|
||||||
|
return Color.themed([0xFDFCFA, 0xFFFDEF, 0x4F4F4F, 0x292B2C])
|
||||||
|
} else {
|
||||||
|
return Color.themed([0xFFFDEF, 0xEEECCD, 0x303A40, 0x303030])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var previewImage: UIImage? {
|
var previewImage: UIImage? {
|
||||||
guard cache.settings.listPreview != .none, let image = cache.appDelegate.cachedImage(forStoryHash: story.hash), image.isKind(of: UIImage.self) else {
|
guard cache.settings.preview != .none, let image = cache.appDelegate.cachedImage(forStoryHash: story.hash), image.isKind(of: UIImage.self) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -151,9 +162,9 @@ struct CardView: View {
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
func listPreview(image: UIImage) -> some View {
|
func listPreview(image: UIImage) -> some View {
|
||||||
let isLeft = cache.settings.listPreview.isLeft
|
let isLeft = cache.settings.preview.isLeft
|
||||||
|
|
||||||
if cache.settings.listPreview.isSmall {
|
if cache.settings.preview.isSmall {
|
||||||
Image(uiImage: image)
|
Image(uiImage: image)
|
||||||
.resizable()
|
.resizable()
|
||||||
.scaledToFill()
|
.scaledToFill()
|
||||||
|
@ -167,7 +178,7 @@ struct CardView: View {
|
||||||
.resizable()
|
.resizable()
|
||||||
.scaledToFill()
|
.scaledToFill()
|
||||||
.frame(width: 90)
|
.frame(width: 90)
|
||||||
.cornerRadius(10, corners: isLeft || !story.isSelected ? [] : [.topRight, .bottomRight])
|
.clipped()
|
||||||
.padding(.leading, isLeft ? 8 : -10)
|
.padding(.leading, isLeft ? 8 : -10)
|
||||||
.padding(.trailing, isLeft ? -10 : 0)
|
.padding(.trailing, isLeft ? -10 : 0)
|
||||||
}
|
}
|
||||||
|
@ -208,8 +219,9 @@ struct CardContentView: View {
|
||||||
.padding(.leading, 24)
|
.padding(.leading, 24)
|
||||||
|
|
||||||
Text(story.feedName)
|
Text(story.feedName)
|
||||||
.font(.custom("WhitneySSm-Medium", size: 10, relativeTo: .caption))
|
.font(font(named: "WhitneySSm-Medium", size: 10))
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
|
.foregroundColor(feedColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -238,14 +250,28 @@ struct CardContentView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(story.title)
|
Text(story.title)
|
||||||
.font(.custom("WhitneySSm-Medium", size: 18, relativeTo: .caption).bold())
|
.font(font(named: "WhitneySSm-Medium", size: 18).bold())
|
||||||
|
.foregroundColor(titleColor)
|
||||||
|
.lineLimit(cache.isGrid ? StorySettings.Content.titleLimit : cache.settings.content.limit)
|
||||||
|
.truncationMode(.tail)
|
||||||
}
|
}
|
||||||
Text(story.content.prefix(400))
|
.padding(.bottom, cache.settings.spacing == .compact ? -5 : 0)
|
||||||
.font(.custom("WhitneySSm-Book", size: 13, relativeTo: .caption))
|
|
||||||
.padding(.top, 5)
|
if cache.isGrid || cache.settings.content != .title {
|
||||||
|
Text(story.content)
|
||||||
|
.font(font(named: "WhitneySSm-Book", size: 13))
|
||||||
|
.foregroundColor(contentColor)
|
||||||
|
.lineLimit(cache.isGrid ? StorySettings.Content.contentLimit : cache.settings.content.limit)
|
||||||
|
.truncationMode(.tail)
|
||||||
|
.padding(.top, 5)
|
||||||
|
.padding(.bottom, cache.settings.spacing == .compact ? -5 : 0)
|
||||||
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
Text(story.dateAndAuthor)
|
Text(story.dateAndAuthor)
|
||||||
.font(.custom("WhitneySSm-Medium", size: 10, relativeTo: .caption))
|
.font(font(named: "WhitneySSm-Medium", size: 10))
|
||||||
|
.foregroundColor(dateAndAuthorColor)
|
||||||
.padding(.top, 5)
|
.padding(.top, 5)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -270,6 +296,40 @@ struct CardContentView: View {
|
||||||
return UIImage(named: "indicator-unread")
|
return UIImage(named: "indicator-unread")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func font(named: String, size: CGFloat) -> Font {
|
||||||
|
return Font.custom(named, size: size + cache.settings.fontSize.offset, relativeTo: .caption)
|
||||||
|
}
|
||||||
|
|
||||||
|
var feedColor: Color {
|
||||||
|
return contentColor
|
||||||
|
}
|
||||||
|
|
||||||
|
var titleColor: Color {
|
||||||
|
if story.isSelected {
|
||||||
|
return Color.themed([0x686868, 0xA0A0A0])
|
||||||
|
} else if story.isRead {
|
||||||
|
return Color.themed([0x585858, 0x585858, 0x989898, 0x888888])
|
||||||
|
} else {
|
||||||
|
return Color.themed([0x111111, 0x333333, 0xD0D0D0, 0xCCCCCC])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var contentColor: Color {
|
||||||
|
if story.isSelected, story.isRead {
|
||||||
|
return Color.themed([0xB8B8B8, 0xB8B8B8, 0xA0A0A0, 0x707070])
|
||||||
|
} else if story.isSelected {
|
||||||
|
return Color.themed([0x888785, 0x686868, 0xA9A9A9, 0x989898])
|
||||||
|
} else if story.isRead {
|
||||||
|
return Color.themed([0xB8B8B8, 0xB8B8B8, 0xA0A0A0, 0x707070])
|
||||||
|
} else {
|
||||||
|
return Color.themed([0x404040, 0x404040, 0xC0C0C0, 0xB0B0B0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var dateAndAuthorColor: Color {
|
||||||
|
return contentColor
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct CardFeedBarView: View {
|
struct CardFeedBarView: View {
|
||||||
|
|
|
@ -71,6 +71,7 @@ struct FeedDetailGridView: View {
|
||||||
view.padding()
|
view.padding()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.background(Color.themed([0xF4F4F4, 0xFFFDEF, 0x4F4F4F, 0x101010]))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
|
|
|
@ -19,7 +19,7 @@ struct StoryView: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
ZStack {
|
ZStack {
|
||||||
Color(white: 0.9)
|
Color.themed([0xFFFDEF, 0xEEECCD, 0x303A40, 0x303030])
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
Text(story.title)
|
Text(story.title)
|
||||||
|
@ -36,7 +36,7 @@ struct StoryView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.font(.custom("WhitneySSm-Medium", size: 14, relativeTo: .body))
|
.font(.custom("WhitneySSm-Medium", size: 14, relativeTo: .body))
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(Color.themed([0x686868, 0xA0A0A0]))
|
||||||
.frame(height: 50)
|
.frame(height: 50)
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
|
@ -48,7 +48,7 @@ struct StoryView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
var previewImage: UIImage? {
|
var previewImage: UIImage? {
|
||||||
guard cache.settings.listPreview != .none, let image = cache.appDelegate.cachedImage(forStoryHash: story.hash), image.isKind(of: UIImage.self) else {
|
guard cache.settings.preview != .none, let image = cache.appDelegate.cachedImage(forStoryHash: story.hash), image.isKind(of: UIImage.self) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@ class Story: Identifiable {
|
||||||
var content = ""
|
var content = ""
|
||||||
var dateString = ""
|
var dateString = ""
|
||||||
var timestamp = 0
|
var timestamp = 0
|
||||||
|
var isRead = false
|
||||||
var isSaved = false
|
var isSaved = false
|
||||||
var isShared = false
|
var isShared = false
|
||||||
var score = 0
|
var score = 0
|
||||||
|
@ -99,6 +100,7 @@ class Story: Identifiable {
|
||||||
score = Int(NewsBlurAppDelegate.computeStoryScore(intelligence))
|
score = Int(NewsBlurAppDelegate.computeStoryScore(intelligence))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isRead = score < 0
|
||||||
isRiverOrSocial = storiesCollection.isRiverOrSocial
|
isRiverOrSocial = storiesCollection.isRiverOrSocial
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,9 +109,9 @@ class Story: Identifiable {
|
||||||
let scanner = Scanner(string: hex)
|
let scanner = Scanner(string: hex)
|
||||||
var color: Int64 = 0
|
var color: Int64 = 0
|
||||||
scanner.scanHexInt64(&color)
|
scanner.scanHexInt64(&color)
|
||||||
let array = [NSNumber(value: color)]
|
let value = Int(color)
|
||||||
|
|
||||||
return ThemeManager.color(fromRGB: array)
|
return ThemeManager.shared.fixedColor(fromRGB: value) ?? UIColor.gray
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,19 +151,44 @@ class StoryCache: ObservableObject {
|
||||||
class StorySettings {
|
class StorySettings {
|
||||||
let defaults = UserDefaults.standard
|
let defaults = UserDefaults.standard
|
||||||
|
|
||||||
//TODO: 🚧
|
enum Content: String, RawRepresentable {
|
||||||
// var listContent: Int {
|
case title
|
||||||
// NSString *preferenceKey = @"story_list_preview_text_size";
|
case short
|
||||||
// NSArray *titles = @[@"Title", @"content_preview_small.png", @"content_preview_medium.png", @"content_preview_large.png"];
|
case medium
|
||||||
// NSArray *values = @[@"title", @"short", @"medium", @"long"];
|
case long
|
||||||
// }
|
|
||||||
|
|
||||||
enum ListPreview {
|
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 none
|
||||||
case smallLeft
|
case smallLeft = "small_left"
|
||||||
case largeLeft
|
case largeLeft = "large_left"
|
||||||
case largeRight
|
case largeRight = "large_right"
|
||||||
case smallRight
|
case smallRight = "small_right"
|
||||||
|
|
||||||
var isLeft: Bool {
|
var isLeft: Bool {
|
||||||
return [.smallLeft, .largeLeft].contains(self)
|
return [.smallLeft, .largeLeft].contains(self)
|
||||||
|
@ -172,31 +199,57 @@ class StorySettings {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var listPreview: ListPreview {
|
var preview: Preview {
|
||||||
switch defaults.string(forKey: "story_list_preview_images_size") {
|
if let string = defaults.string(forKey: "story_list_preview_images_size"), let value = Preview(rawValue: string) {
|
||||||
case "none":
|
return value
|
||||||
return .none
|
} else {
|
||||||
case "small_left":
|
|
||||||
return .smallLeft
|
|
||||||
case "large_left":
|
|
||||||
return .largeLeft
|
|
||||||
case "large_right":
|
|
||||||
return .largeRight
|
|
||||||
default:
|
|
||||||
return .smallRight
|
return .smallRight
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: 🚧
|
enum FontSize: String, RawRepresentable {
|
||||||
// NSString *preferenceKey = @"feed_list_font_size";
|
case xs
|
||||||
// NSArray *titles = @[@"XS", @"S", @"M", @"L", @"XL"];
|
case small
|
||||||
// NSArray *values = @[@"xs", @"small", @"medium", @"large", @"xl"];
|
case medium
|
||||||
|
case large
|
||||||
|
case xl
|
||||||
|
|
||||||
//TODO: 🚧
|
var offset: CGFloat {
|
||||||
// preferenceKey = @"feed_list_spacing";
|
switch self {
|
||||||
// titles = @[@"Compact", @"Comfortable"];
|
case .xs:
|
||||||
// values = @[@"compact", @"comfortable"];
|
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 {
|
// enum GridColumns: String {
|
||||||
// case auto = "auto"
|
// case auto = "auto"
|
||||||
|
|
|
@ -53,4 +53,8 @@ extension Color {
|
||||||
return Color(red: .random(in: 0...1), green: .random(in: 0...1), blue: .random(in: 0...1)
|
return Color(red: .random(in: 0...1), green: .random(in: 0...1), blue: .random(in: 0...1)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func themed(_ hex: [NSNumber]) -> Color {
|
||||||
|
return Color(ThemeManager.color(fromRGB: hex))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue