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 {
|
struct TimelineItem: Identifiable {
|
||||||
|
|
||||||
var article: Article
|
var article: Article
|
||||||
|
var truncatedTitle: String
|
||||||
|
var truncatedSummary: String
|
||||||
|
|
||||||
init(article: Article) {
|
init(article: Article) {
|
||||||
self.article = article
|
self.article = article
|
||||||
|
self.truncatedTitle = ArticleStringFormatter.truncatedTitle(article)
|
||||||
|
self.truncatedSummary = ArticleStringFormatter.truncatedSummary(article)
|
||||||
updateStatus()
|
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
|
@EnvironmentObject var defaults: AppDefaults
|
||||||
@StateObject var articleIconImageLoader = ArticleIconImageLoader()
|
@StateObject var articleIconImageLoader = ArticleIconImageLoader()
|
||||||
|
|
||||||
|
var width: CGFloat
|
||||||
var timelineItem: TimelineItem
|
var timelineItem: TimelineItem
|
||||||
|
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
@ -23,33 +24,42 @@ struct TimelineItemView: View {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
HStack(alignment: .top) {
|
||||||
HStack(alignment: .top) {
|
TimelineItemStatusView(status: timelineItem.status)
|
||||||
TimelineItemStatusView(status: timelineItem.status)
|
if let image = articleIconImageLoader.image {
|
||||||
if let image = articleIconImageLoader.image {
|
IconImageView(iconImage: image)
|
||||||
IconImageView(iconImage: image)
|
.frame(width: CGFloat(defaults.timelineIconDimensions), height: CGFloat(defaults.timelineIconDimensions), alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/)
|
||||||
.frame(width: CGFloat(defaults.timelineIconDimensions), height: CGFloat(defaults.timelineIconDimensions), alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/)
|
}
|
||||||
}
|
VStack {
|
||||||
VStack {
|
let titleLines = timelineItem.numberOfTitleLines(width: width)
|
||||||
Text(verbatim: timelineItem.article.title ?? "N/A")
|
if titleLines > 0 {
|
||||||
|
Text(verbatim: timelineItem.truncatedTitle)
|
||||||
.fontWeight(.semibold)
|
.fontWeight(.semibold)
|
||||||
.lineLimit(Int(defaults.timelineNumberOfLines))
|
.lineLimit(titleLines)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.padding(.trailing, 4)
|
.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()
|
Spacer()
|
||||||
HStack {
|
Text(verbatim: timelineItem.dateTimeString)
|
||||||
Text(verbatim: timelineItem.byline)
|
.lineLimit(1)
|
||||||
.lineLimit(1)
|
.font(.footnote)
|
||||||
.truncationMode(.tail)
|
.foregroundColor(.secondary)
|
||||||
.font(.footnote)
|
.padding(.trailing, 4)
|
||||||
.foregroundColor(.secondary)
|
|
||||||
Spacer()
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
197
Multiplatform/Shared/Timeline/TimelineTextSizer.swift
Normal file
197
Multiplatform/Shared/Timeline/TimelineTextSizer.swift
Normal file
@ -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
|
@State var navigate = true
|
||||||
|
|
||||||
@ViewBuilder var body: some View {
|
@ViewBuilder var body: some View {
|
||||||
#if os(macOS)
|
GeometryReader { proxy in
|
||||||
VStack {
|
#if os(macOS)
|
||||||
HStack {
|
VStack {
|
||||||
TimelineSortOrderView()
|
HStack {
|
||||||
Spacer()
|
TimelineSortOrderView()
|
||||||
Button (action: {
|
Spacer()
|
||||||
withAnimation {
|
Button (action: {
|
||||||
timelineModel.toggleReadFilter()
|
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))
|
||||||
.navigationTitle(Text(verbatim: timelineModel.nameForDisplay))
|
#else
|
||||||
#else
|
List(timelineModel.timelineItems) { timelineItem in
|
||||||
List(timelineModel.timelineItems) { timelineItem in
|
ZStack {
|
||||||
ZStack {
|
TimelineItemView(width: proxy.size.width, timelineItem: timelineItem)
|
||||||
TimelineItemView(timelineItem: timelineItem)
|
NavigationLink(destination: ArticleContainerView(),
|
||||||
NavigationLink(destination: ArticleContainerView(),
|
tag: timelineItem.article.articleID,
|
||||||
tag: timelineItem.article.articleID,
|
selection: $timelineModel.selectedArticleID) {
|
||||||
selection: $timelineModel.selectedArticleID) {
|
EmptyView()
|
||||||
EmptyView()
|
}.buttonStyle(PlainButtonStyle())
|
||||||
}.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 */; };
|
51B80F4224BE588200C6C32D /* SharingServicePickerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B80F4124BE588200C6C32D /* SharingServicePickerDelegate.swift */; };
|
||||||
51B80F4424BE58BF00C6C32D /* SharingServiceDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B80F4324BE58BF00C6C32D /* SharingServiceDelegate.swift */; };
|
51B80F4424BE58BF00C6C32D /* SharingServiceDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B80F4324BE58BF00C6C32D /* SharingServiceDelegate.swift */; };
|
||||||
51B80F4624BF76E700C6C32D /* Browser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B80F4524BF76E700C6C32D /* Browser.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 */; };
|
51BB7C272335A8E5008E8144 /* ArticleActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BB7C262335A8E5008E8144 /* ArticleActivityItemSource.swift */; };
|
||||||
51BB7C312335ACDE008E8144 /* page.html in Resources */ = {isa = PBXBuildFile; fileRef = 51BB7C302335ACDE008E8144 /* page.html */; };
|
51BB7C312335ACDE008E8144 /* page.html in Resources */ = {isa = PBXBuildFile; fileRef = 51BB7C302335ACDE008E8144 /* page.html */; };
|
||||||
51BC4AFF247277E0000A6ED8 /* URL-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BC4ADD247277DF000A6ED8 /* URL-Extensions.swift */; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
51BC4ADD247277DF000A6ED8 /* URL-Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "URL-Extensions.swift"; sourceTree = "<group>"; };
|
||||||
@ -2940,6 +2943,7 @@
|
|||||||
514E6BD924ACEA0400AC6F6E /* TimelineItemView.swift */,
|
514E6BD924ACEA0400AC6F6E /* TimelineItemView.swift */,
|
||||||
51919FF024AB864A00541E64 /* TimelineModel.swift */,
|
51919FF024AB864A00541E64 /* TimelineModel.swift */,
|
||||||
5194737024BBCAF4001A2939 /* TimelineSortOrderView.swift */,
|
5194737024BBCAF4001A2939 /* TimelineSortOrderView.swift */,
|
||||||
|
51B8104424C0E6D200C6C32D /* TimelineTextSizer.swift */,
|
||||||
5177470224B2657F00EB0F74 /* TimelineToolbarModifier.swift */,
|
5177470224B2657F00EB0F74 /* TimelineToolbarModifier.swift */,
|
||||||
51919FF624AB8B7700541E64 /* TimelineView.swift */,
|
51919FF624AB8B7700541E64 /* TimelineView.swift */,
|
||||||
);
|
);
|
||||||
@ -5095,6 +5099,7 @@
|
|||||||
5177471C24B387AC00EB0F74 /* ImageScrollView.swift in Sources */,
|
5177471C24B387AC00EB0F74 /* ImageScrollView.swift in Sources */,
|
||||||
51E498F824A8085D00B667CB /* UnreadFeed.swift in Sources */,
|
51E498F824A8085D00B667CB /* UnreadFeed.swift in Sources */,
|
||||||
6591723124B5C35400B638E8 /* AccountHeaderImageView.swift in Sources */,
|
6591723124B5C35400B638E8 /* AccountHeaderImageView.swift in Sources */,
|
||||||
|
51B8104524C0E6D200C6C32D /* TimelineTextSizer.swift in Sources */,
|
||||||
FF64D0E724AF53EE0084080A /* RefreshProgressModel.swift in Sources */,
|
FF64D0E724AF53EE0084080A /* RefreshProgressModel.swift in Sources */,
|
||||||
51E4996A24A8762D00B667CB /* ExtractedArticle.swift in Sources */,
|
51E4996A24A8762D00B667CB /* ExtractedArticle.swift in Sources */,
|
||||||
51919FF124AB864A00541E64 /* TimelineModel.swift in Sources */,
|
51919FF124AB864A00541E64 /* TimelineModel.swift in Sources */,
|
||||||
@ -5277,6 +5282,7 @@
|
|||||||
51E4991624A8090300B667CB /* ArticleUtilities.swift in Sources */,
|
51E4991624A8090300B667CB /* ArticleUtilities.swift in Sources */,
|
||||||
51919FF224AB864A00541E64 /* TimelineModel.swift in Sources */,
|
51919FF224AB864A00541E64 /* TimelineModel.swift in Sources */,
|
||||||
51E4991A24A8090F00B667CB /* IconImage.swift in Sources */,
|
51E4991A24A8090F00B667CB /* IconImage.swift in Sources */,
|
||||||
|
51B8104624C0E6D200C6C32D /* TimelineTextSizer.swift in Sources */,
|
||||||
51E4992724A80AAB00B667CB /* AppAssets.swift in Sources */,
|
51E4992724A80AAB00B667CB /* AppAssets.swift in Sources */,
|
||||||
51E49A0124A91FC100B667CB /* SidebarContainerView.swift in Sources */,
|
51E49A0124A91FC100B667CB /* SidebarContainerView.swift in Sources */,
|
||||||
51E4995B24A875D500B667CB /* ArticlePasteboardWriter.swift in Sources */,
|
51E4995B24A875D500B667CB /* ArticlePasteboardWriter.swift in Sources */,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user