Make Timeline text layout like current apps
This commit is contained in:
parent
ba7fcd70c9
commit
8f7b8160a1
|
@ -18,9 +18,13 @@ enum TimelineItemStatus {
|
|||
struct TimelineItem: Identifiable {
|
||||
|
||||
var article: Article
|
||||
var truncatedTitle: String
|
||||
var truncatedSummary: String
|
||||
|
||||
init(article: Article) {
|
||||
self.article = article
|
||||
self.truncatedTitle = ArticleStringFormatter.truncatedTitle(article)
|
||||
self.truncatedSummary = ArticleStringFormatter.truncatedSummary(article)
|
||||
updateStatus()
|
||||
}
|
||||
|
||||
|
@ -50,4 +54,45 @@ struct TimelineItem: Identifiable {
|
|||
}
|
||||
}
|
||||
|
||||
func numberOfTitleLines(width: CGFloat) -> Int {
|
||||
guard !truncatedTitle.isEmpty else { return 0 }
|
||||
|
||||
#if os(macOS)
|
||||
let descriptor = NSFont.preferredFont(forTextStyle: .body).fontDescriptor.withSymbolicTraits(.bold)
|
||||
guard let font = NSFont(descriptor: descriptor, size: 0) else { return 0 }
|
||||
#else
|
||||
guard let descriptor = UIFont.preferredFont(forTextStyle: .body).fontDescriptor.withSymbolicTraits(.traitBold) else { return 0 }
|
||||
let font = UIFont(descriptor: descriptor, size: 0)
|
||||
#endif
|
||||
|
||||
let lines = Int(AppDefaults.shared.timelineNumberOfLines)
|
||||
let sizeInfo = TimelineTextSizer.size(for: truncatedTitle, font: font, numberOfLines: lines, width: adjustedWidth(width))
|
||||
return sizeInfo.numberOfLinesUsed
|
||||
}
|
||||
|
||||
func numberOfSummaryLines(width: CGFloat, titleLines: Int) -> Int {
|
||||
guard !truncatedSummary.isEmpty else { return 0 }
|
||||
|
||||
let remainingLines = Int(AppDefaults.shared.timelineNumberOfLines) - titleLines
|
||||
guard remainingLines > 0 else { return 0 }
|
||||
|
||||
#if os(macOS)
|
||||
let font = NSFont.preferredFont(forTextStyle: .body)
|
||||
#else
|
||||
let font = UIFont.preferredFont(forTextStyle: .body)
|
||||
#endif
|
||||
|
||||
let sizeInfo = TimelineTextSizer.size(for: truncatedSummary, font: font, numberOfLines: remainingLines, width: adjustedWidth(width))
|
||||
return sizeInfo.numberOfLinesUsed
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private extension TimelineItem {
|
||||
|
||||
// This clearly isn't correct yet, but it gets us close enough for now. -Maurice
|
||||
func adjustedWidth(_ width: CGFloat) -> Int {
|
||||
return Int(width - CGFloat(AppDefaults.shared.timelineIconDimensions + 64))
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ struct TimelineItemView: View {
|
|||
@EnvironmentObject var defaults: AppDefaults
|
||||
@StateObject var articleIconImageLoader = ArticleIconImageLoader()
|
||||
|
||||
var width: CGFloat
|
||||
var timelineItem: TimelineItem
|
||||
|
||||
#if os(macOS)
|
||||
|
@ -23,33 +24,42 @@ struct TimelineItemView: View {
|
|||
#endif
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
HStack(alignment: .top) {
|
||||
TimelineItemStatusView(status: timelineItem.status)
|
||||
if let image = articleIconImageLoader.image {
|
||||
IconImageView(iconImage: image)
|
||||
.frame(width: CGFloat(defaults.timelineIconDimensions), height: CGFloat(defaults.timelineIconDimensions), alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/)
|
||||
}
|
||||
VStack {
|
||||
Text(verbatim: timelineItem.article.title ?? "N/A")
|
||||
HStack(alignment: .top) {
|
||||
TimelineItemStatusView(status: timelineItem.status)
|
||||
if let image = articleIconImageLoader.image {
|
||||
IconImageView(iconImage: image)
|
||||
.frame(width: CGFloat(defaults.timelineIconDimensions), height: CGFloat(defaults.timelineIconDimensions), alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/)
|
||||
}
|
||||
VStack {
|
||||
let titleLines = timelineItem.numberOfTitleLines(width: width)
|
||||
if titleLines > 0 {
|
||||
Text(verbatim: timelineItem.truncatedTitle)
|
||||
.fontWeight(.semibold)
|
||||
.lineLimit(Int(defaults.timelineNumberOfLines))
|
||||
.lineLimit(titleLines)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.trailing, 4)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
let summaryLines = timelineItem.numberOfSummaryLines(width: width, titleLines: titleLines)
|
||||
if summaryLines > 0 {
|
||||
Text(verbatim: timelineItem.truncatedSummary)
|
||||
.lineLimit(summaryLines)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.trailing, 4)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
HStack {
|
||||
Text(verbatim: timelineItem.byline)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
.font(.footnote)
|
||||
.foregroundColor(.secondary)
|
||||
Spacer()
|
||||
HStack {
|
||||
Text(verbatim: timelineItem.byline)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
.font(.footnote)
|
||||
.foregroundColor(.secondary)
|
||||
Spacer()
|
||||
Text(verbatim: timelineItem.dateTimeString)
|
||||
.lineLimit(1)
|
||||
.font(.footnote)
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.trailing, 4)
|
||||
}
|
||||
Text(verbatim: timelineItem.dateTimeString)
|
||||
.lineLimit(1)
|
||||
.font(.footnote)
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.trailing, 4)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -59,16 +69,3 @@ struct TimelineItemView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct TimelineItemView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
TimelineItemView(timelineItem: TimelineItem(article: PreviewArticles.basicRead))
|
||||
.frame(maxWidth: 250)
|
||||
TimelineItemView(timelineItem: TimelineItem(article: PreviewArticles.basicUnread))
|
||||
.frame(maxWidth: 250)
|
||||
TimelineItemView(timelineItem: TimelineItem(article: PreviewArticles.basicStarred))
|
||||
.frame(maxWidth: 250)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,197 @@
|
|||
//
|
||||
// MultilineUILabelSizer.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Maurice Parker on 7/16/20.
|
||||
// Copyright © 2020 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
#if os(macOS)
|
||||
import AppKit
|
||||
typealias RSFont = NSFont
|
||||
#else
|
||||
import UIKit
|
||||
typealias RSFont = UIFont
|
||||
#endif
|
||||
|
||||
// Get the height of an NSTextField given a string, font, and width.
|
||||
// Uses a cache. Avoids actually measuring text as much as possible.
|
||||
// Main thread only.
|
||||
|
||||
typealias WidthHeightCache = [Int: Int] // width: height
|
||||
|
||||
private struct TextSizerSpecifier: Hashable {
|
||||
let numberOfLines: Int
|
||||
let font: RSFont
|
||||
}
|
||||
|
||||
struct TextSizeInfo {
|
||||
|
||||
let size: CGSize // Integral size (ceiled)
|
||||
let numberOfLinesUsed: Int // A two-line text field may only use one line, for instance. This would equal 1, then.
|
||||
}
|
||||
|
||||
final class TimelineTextSizer {
|
||||
|
||||
private let numberOfLines: Int
|
||||
private let font: RSFont
|
||||
private let singleLineHeightEstimate: Int
|
||||
private let doubleLineHeightEstimate: Int
|
||||
private var cache = [String: WidthHeightCache]() // Each string has a cache.
|
||||
private static var sizers = [TextSizerSpecifier: TimelineTextSizer]()
|
||||
|
||||
private init(numberOfLines: Int, font: RSFont) {
|
||||
|
||||
self.numberOfLines = numberOfLines
|
||||
self.font = font
|
||||
|
||||
self.singleLineHeightEstimate = TimelineTextSizer.calculateHeight("AqLjJ0/y", 200, font)
|
||||
self.doubleLineHeightEstimate = TimelineTextSizer.calculateHeight("AqLjJ0/y\nAqLjJ0/y", 200, font)
|
||||
|
||||
}
|
||||
|
||||
static func size(for string: String, font: RSFont, numberOfLines: Int, width: Int) -> TextSizeInfo {
|
||||
return sizer(numberOfLines: numberOfLines, font: font).sizeInfo(for: string, width: width)
|
||||
}
|
||||
|
||||
static func emptyCache() {
|
||||
sizers = [TextSizerSpecifier: TimelineTextSizer]()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private extension TimelineTextSizer {
|
||||
|
||||
static func sizer(numberOfLines: Int, font: RSFont) -> TimelineTextSizer {
|
||||
|
||||
let specifier = TextSizerSpecifier(numberOfLines: numberOfLines, font: font)
|
||||
if let cachedSizer = sizers[specifier] {
|
||||
return cachedSizer
|
||||
}
|
||||
|
||||
let newSizer = TimelineTextSizer(numberOfLines: numberOfLines, font: font)
|
||||
sizers[specifier] = newSizer
|
||||
return newSizer
|
||||
|
||||
}
|
||||
|
||||
func sizeInfo(for string: String, width: Int) -> TextSizeInfo {
|
||||
|
||||
let textFieldHeight = height(for: string, width: width)
|
||||
let numberOfLinesUsed = numberOfLines(for: textFieldHeight)
|
||||
|
||||
let size = CGSize(width: width, height: textFieldHeight)
|
||||
let sizeInfo = TextSizeInfo(size: size, numberOfLinesUsed: numberOfLinesUsed)
|
||||
return sizeInfo
|
||||
|
||||
}
|
||||
|
||||
func height(for string: String, width: Int) -> Int {
|
||||
|
||||
if cache[string] == nil {
|
||||
cache[string] = WidthHeightCache()
|
||||
}
|
||||
|
||||
if let height = cache[string]![width] {
|
||||
return height
|
||||
}
|
||||
|
||||
if let height = heightConsideringNeighbors(cache[string]!, width) {
|
||||
return height
|
||||
}
|
||||
|
||||
var height = TimelineTextSizer.calculateHeight(string, width, font)
|
||||
|
||||
if numberOfLines != 0 {
|
||||
let maxHeight = singleLineHeightEstimate * numberOfLines
|
||||
if height > maxHeight {
|
||||
height = maxHeight
|
||||
}
|
||||
}
|
||||
|
||||
cache[string]![width] = height
|
||||
|
||||
return height
|
||||
}
|
||||
|
||||
static func calculateHeight(_ string: String, _ width: Int, _ font: RSFont) -> Int {
|
||||
let height = string.height(withConstrainedWidth: CGFloat(width), font: font)
|
||||
return Int(ceil(height))
|
||||
}
|
||||
|
||||
func numberOfLines(for height: Int) -> Int {
|
||||
|
||||
// We’ll have to see if this really works reliably.
|
||||
|
||||
let averageHeight = CGFloat(doubleLineHeightEstimate) / 2.0
|
||||
let lines = Int(round(CGFloat(height) / averageHeight))
|
||||
return lines
|
||||
|
||||
}
|
||||
|
||||
func heightIsProbablySingleLineHeight(_ height: Int) -> Bool {
|
||||
return heightIsProbablyEqualToEstimate(height, singleLineHeightEstimate)
|
||||
}
|
||||
|
||||
func heightIsProbablyDoubleLineHeight(_ height: Int) -> Bool {
|
||||
return heightIsProbablyEqualToEstimate(height, doubleLineHeightEstimate)
|
||||
}
|
||||
|
||||
func heightIsProbablyEqualToEstimate(_ height: Int, _ estimate: Int) -> Bool {
|
||||
|
||||
let slop = 4
|
||||
let minimum = estimate - slop
|
||||
let maximum = estimate + slop
|
||||
return height >= minimum && height <= maximum
|
||||
|
||||
}
|
||||
|
||||
func heightConsideringNeighbors(_ heightCache: WidthHeightCache, _ width: Int) -> Int? {
|
||||
|
||||
// Given width, if the height at width - something and width + something is equal,
|
||||
// then that height must be correct for the given width.
|
||||
// Also:
|
||||
// If a narrower neighbor’s height is single line height, then this wider width must also be single-line height.
|
||||
// If a wider neighbor’s height is double line height, and numberOfLines == 2, then this narrower width must able be double-line height.
|
||||
|
||||
var smallNeighbor = (width: 0, height: 0)
|
||||
var largeNeighbor = (width: 0, height: 0)
|
||||
|
||||
for (oneWidth, oneHeight) in heightCache {
|
||||
|
||||
if oneWidth < width && heightIsProbablySingleLineHeight(oneHeight) {
|
||||
return oneHeight
|
||||
}
|
||||
if numberOfLines == 2 && oneWidth > width && heightIsProbablyDoubleLineHeight(oneHeight) {
|
||||
return oneHeight
|
||||
}
|
||||
|
||||
if oneWidth < width && (oneWidth > smallNeighbor.width || smallNeighbor.width == 0) {
|
||||
smallNeighbor = (oneWidth, oneHeight)
|
||||
}
|
||||
else if oneWidth > width && (oneWidth < largeNeighbor.width || largeNeighbor.width == 0) {
|
||||
largeNeighbor = (oneWidth, oneHeight)
|
||||
}
|
||||
|
||||
if smallNeighbor.width != 0 && smallNeighbor.height == largeNeighbor.height {
|
||||
return smallNeighbor.height
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension String {
|
||||
|
||||
func height(withConstrainedWidth width: CGFloat, font: RSFont) -> CGFloat {
|
||||
let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude)
|
||||
let boundingBox = self.boundingRect(with: constraintRect, options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: [NSAttributedString.Key.font: font], context: nil)
|
||||
return ceil(boundingBox.height)
|
||||
}
|
||||
|
||||
}
|
|
@ -14,50 +14,52 @@ struct TimelineView: View {
|
|||
@State var navigate = true
|
||||
|
||||
@ViewBuilder var body: some View {
|
||||
#if os(macOS)
|
||||
VStack {
|
||||
HStack {
|
||||
TimelineSortOrderView()
|
||||
Spacer()
|
||||
Button (action: {
|
||||
withAnimation {
|
||||
timelineModel.toggleReadFilter()
|
||||
GeometryReader { proxy in
|
||||
#if os(macOS)
|
||||
VStack {
|
||||
HStack {
|
||||
TimelineSortOrderView()
|
||||
Spacer()
|
||||
Button (action: {
|
||||
withAnimation {
|
||||
timelineModel.toggleReadFilter()
|
||||
}
|
||||
}, label: {
|
||||
if timelineModel.isReadFiltered ?? false {
|
||||
AppAssets.filterActiveImage
|
||||
} else {
|
||||
AppAssets.filterInactiveImage
|
||||
}
|
||||
})
|
||||
.hidden(timelineModel.isReadFiltered == nil)
|
||||
.padding(.top, 8).padding(.trailing)
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
.help(timelineModel.isReadFiltered ?? false ? "Show Read Articles" : "Filter Read Articles")
|
||||
}
|
||||
ZStack {
|
||||
NavigationLink(destination: ArticleContainerView(), isActive: $navigate) {
|
||||
EmptyView()
|
||||
}.hidden()
|
||||
List(timelineModel.timelineItems, selection: $timelineModel.selectedArticleIDs) { timelineItem in
|
||||
TimelineItemView(width: proxy.size.width, timelineItem: timelineItem)
|
||||
}
|
||||
}, label: {
|
||||
if timelineModel.isReadFiltered ?? false {
|
||||
AppAssets.filterActiveImage
|
||||
} else {
|
||||
AppAssets.filterInactiveImage
|
||||
}
|
||||
})
|
||||
.hidden(timelineModel.isReadFiltered == nil)
|
||||
.padding(.top, 8).padding(.trailing)
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
.help(timelineModel.isReadFiltered ?? false ? "Show Read Articles" : "Filter Read Articles")
|
||||
}
|
||||
ZStack {
|
||||
NavigationLink(destination: ArticleContainerView(), isActive: $navigate) {
|
||||
EmptyView()
|
||||
}.hidden()
|
||||
List(timelineModel.timelineItems, selection: $timelineModel.selectedArticleIDs) { timelineItem in
|
||||
TimelineItemView(timelineItem: timelineItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle(Text(verbatim: timelineModel.nameForDisplay))
|
||||
#else
|
||||
List(timelineModel.timelineItems) { timelineItem in
|
||||
ZStack {
|
||||
TimelineItemView(timelineItem: timelineItem)
|
||||
NavigationLink(destination: ArticleContainerView(),
|
||||
tag: timelineItem.article.articleID,
|
||||
selection: $timelineModel.selectedArticleID) {
|
||||
EmptyView()
|
||||
}.buttonStyle(PlainButtonStyle())
|
||||
.navigationTitle(Text(verbatim: timelineModel.nameForDisplay))
|
||||
#else
|
||||
List(timelineModel.timelineItems) { timelineItem in
|
||||
ZStack {
|
||||
TimelineItemView(width: proxy.size.width, timelineItem: timelineItem)
|
||||
NavigationLink(destination: ArticleContainerView(),
|
||||
tag: timelineItem.article.articleID,
|
||||
selection: $timelineModel.selectedArticleID) {
|
||||
EmptyView()
|
||||
}.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
}
|
||||
.navigationBarTitle(Text(verbatim: timelineModel.nameForDisplay), displayMode: .inline)
|
||||
#endif
|
||||
}
|
||||
.navigationBarTitle(Text(verbatim: timelineModel.nameForDisplay), displayMode: .inline)
|
||||
#endif
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -369,6 +369,8 @@
|
|||
51B80F4224BE588200C6C32D /* SharingServicePickerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B80F4124BE588200C6C32D /* SharingServicePickerDelegate.swift */; };
|
||||
51B80F4424BE58BF00C6C32D /* SharingServiceDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B80F4324BE58BF00C6C32D /* SharingServiceDelegate.swift */; };
|
||||
51B80F4624BF76E700C6C32D /* Browser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B80F4524BF76E700C6C32D /* Browser.swift */; };
|
||||
51B8104524C0E6D200C6C32D /* TimelineTextSizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B8104424C0E6D200C6C32D /* TimelineTextSizer.swift */; };
|
||||
51B8104624C0E6D200C6C32D /* TimelineTextSizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B8104424C0E6D200C6C32D /* TimelineTextSizer.swift */; };
|
||||
51BB7C272335A8E5008E8144 /* ArticleActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BB7C262335A8E5008E8144 /* ArticleActivityItemSource.swift */; };
|
||||
51BB7C312335ACDE008E8144 /* page.html in Resources */ = {isa = PBXBuildFile; fileRef = 51BB7C302335ACDE008E8144 /* page.html */; };
|
||||
51BC4AFF247277E0000A6ED8 /* URL-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BC4ADD247277DF000A6ED8 /* URL-Extensions.swift */; };
|
||||
|
@ -2030,6 +2032,7 @@
|
|||
51B80F4124BE588200C6C32D /* SharingServicePickerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharingServicePickerDelegate.swift; sourceTree = "<group>"; };
|
||||
51B80F4324BE58BF00C6C32D /* SharingServiceDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharingServiceDelegate.swift; sourceTree = "<group>"; };
|
||||
51B80F4524BF76E700C6C32D /* Browser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Browser.swift; sourceTree = "<group>"; };
|
||||
51B8104424C0E6D200C6C32D /* TimelineTextSizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineTextSizer.swift; sourceTree = "<group>"; };
|
||||
51BB7C262335A8E5008E8144 /* ArticleActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleActivityItemSource.swift; sourceTree = "<group>"; };
|
||||
51BB7C302335ACDE008E8144 /* page.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = page.html; sourceTree = "<group>"; };
|
||||
51BC4ADD247277DF000A6ED8 /* URL-Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "URL-Extensions.swift"; sourceTree = "<group>"; };
|
||||
|
@ -2940,6 +2943,7 @@
|
|||
514E6BD924ACEA0400AC6F6E /* TimelineItemView.swift */,
|
||||
51919FF024AB864A00541E64 /* TimelineModel.swift */,
|
||||
5194737024BBCAF4001A2939 /* TimelineSortOrderView.swift */,
|
||||
51B8104424C0E6D200C6C32D /* TimelineTextSizer.swift */,
|
||||
5177470224B2657F00EB0F74 /* TimelineToolbarModifier.swift */,
|
||||
51919FF624AB8B7700541E64 /* TimelineView.swift */,
|
||||
);
|
||||
|
@ -5095,6 +5099,7 @@
|
|||
5177471C24B387AC00EB0F74 /* ImageScrollView.swift in Sources */,
|
||||
51E498F824A8085D00B667CB /* UnreadFeed.swift in Sources */,
|
||||
6591723124B5C35400B638E8 /* AccountHeaderImageView.swift in Sources */,
|
||||
51B8104524C0E6D200C6C32D /* TimelineTextSizer.swift in Sources */,
|
||||
FF64D0E724AF53EE0084080A /* RefreshProgressModel.swift in Sources */,
|
||||
51E4996A24A8762D00B667CB /* ExtractedArticle.swift in Sources */,
|
||||
51919FF124AB864A00541E64 /* TimelineModel.swift in Sources */,
|
||||
|
@ -5277,6 +5282,7 @@
|
|||
51E4991624A8090300B667CB /* ArticleUtilities.swift in Sources */,
|
||||
51919FF224AB864A00541E64 /* TimelineModel.swift in Sources */,
|
||||
51E4991A24A8090F00B667CB /* IconImage.swift in Sources */,
|
||||
51B8104624C0E6D200C6C32D /* TimelineTextSizer.swift in Sources */,
|
||||
51E4992724A80AAB00B667CB /* AppAssets.swift in Sources */,
|
||||
51E49A0124A91FC100B667CB /* SidebarContainerView.swift in Sources */,
|
||||
51E4995B24A875D500B667CB /* ArticlePasteboardWriter.swift in Sources */,
|
||||
|
|
Loading…
Reference in New Issue