Add iOS digest views

This commit is contained in:
Jackson Harper
2024-04-15 17:20:22 +08:00
committed by Hongbo Wu
parent e30fa13a2e
commit 4e61b360d7
26 changed files with 650 additions and 17 deletions

View File

@ -0,0 +1,267 @@
import SwiftUI
import Models
import Services
struct DigestItem {
let id: String
let site: String
let siteIcon: URL?
let author: String
let title: String
let summaryText: String
let keyPointsText: String
let highlightsText: String
}
@available(iOS 17.0, *)
@MainActor
struct LibraryDigestView: View {
let dataService: DataService
// @State private var currentIndex = 0
@State private var items: [DigestItem]
// @State private var preloadedItems: [Int: String] = [:]
@Environment(\.dismiss) private var dismiss
// let itemCount = 10 // Number of items to initially load
// let prefetchCount = 2 // Number of items to prefetch
public init(dataService: DataService) {
self.dataService = dataService
self.items = [
DigestItem(
id: "1468AFAA-88sdfsdfC-4546-BE02-EACF385288FC",
site: "CNBC.com",
siteIcon: URL(string: "https://www.cnbc.com/favicon.ico"),
author: "Kif Leswing",
title: "Apple shares just had their best day since last May",
summaryText: "In a significant political turn, the SOTU response faces unexpected collapse, marking a stark contrast to Trump's latest" +
" downturn, alongside an unprecedented surge in Biden's fundraising efforts as of 3/11/24, according to the TDPS Podcast. " +
"The analysis provides insights into the shifting dynamics of political support and the potential implications for future " +
"electoral strategies. ",
keyPointsText: "Key points from the article:",
highlightsText: "Highlights from the article:"
),
DigestItem(
id: "1468AFAA-8sdfsdffsdf-4546-BE02-EACF385288FC",
site: "CNBC.com",
siteIcon: URL(string: "https://www.cnbc.com/favicon.ico"),
author: "Kif Leswing",
title: "Apple shares just had their best day since last May",
summaryText: "In a significant political turn, the SOTU response faces unexpected collapse, marking a stark contrast to Trump's latest" +
" downturn, alongside an unprecedented surge in Biden's fundraising efforts as of 3/11/24, according to the TDPS Podcast. " +
"The analysis provides insights into the shifting dynamics of political support and the potential implications for future " +
"electoral strategies. ",
keyPointsText: "Key points from the article:",
highlightsText: "Highlights from the article:"
),
DigestItem(
id: "1468AFAA-882C-asdadfsa85288FC",
site: "CNBC.com",
siteIcon: URL(string: "https://www.cnbc.com/favicon.ico"),
author: "Kif Leswing",
title: "Apple shares just had their best day since last May",
summaryText: "In a significant political turn, the SOTU response faces unexpected collapse, marking a stark contrast to Trump's latest" +
" downturn, alongside an unprecedented surge in Biden's fundraising efforts as of 3/11/24, according to the TDPS Podcast. " +
"The analysis provides insights into the shifting dynamics of political support and the potential implications for future " +
"electoral strategies. ",
keyPointsText: "Key points from the article:",
highlightsText: "Highlights from the article:"
)
]
// currentIndex = 0
// _preloadedItems = [Int:String]
}
var body: some View {
itemBody
}
@available(iOS 17.0, *)
var itemBody: some View {
ScrollView(.vertical) {
LazyVStack(spacing: 0) {
ForEach(Array(self.items.enumerated()), id: \.1.id) { idx, item in
PreviewItemView(
viewModel: PreviewItemViewModel(dataService: dataService, item: item, showSwipeHint: idx == 0)
)
.containerRelativeFrame([.horizontal, .vertical])
}
}
.scrollTargetLayout()
}
.scrollTargetBehavior(.paging)
.ignoresSafeArea()
// ScrollView(.horizontal, showsIndicators: false) {
// HStack(spacing: 0) {
// ForEach(items.indices, id: \.self) { index in
// if let item = preloadedItems[index] {
// ItemView(content: item)
// .onAppear {
// if index == items.count - prefetchCount {
// fetchItems()
// }
// }
// }
// }
// }
// }
// .frame(maxWidth: .infinity, maxHeight: .infinity)
.onAppear {
// fetchItems()
}
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification)) { _ in
// Pause any background tasks if necessary
}
}
// private func fetchItems() {
// // Simulate fetching items from an API
// for idx in currentIndex..<currentIndex + itemCount {
// fetchItem(index: idx)
// }
// currentIndex += itemCount
// }
//
// private func fetchItem(index: Int) {
// // Simulate fetching item content from an API
// DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
// items.append("Item \(index + 1)")
// preloadNextItemIfNeeded(index: index)
// }
// }
//
// private func preloadNextItemIfNeeded(index: Int) {
// let nextIndex = index + 1
// if nextIndex < currentIndex + prefetchCount {
// fetchItem(index: nextIndex)
// }
// }
}
@MainActor
public class PreviewItemViewModel: ObservableObject {
let dataService: DataService
@Published var item: DigestItem
let showSwipeHint: Bool
@Published var isLoading = false
@Published var resultText: String?
@Published var promptDisplayText: String?
init(dataService: DataService, item: DigestItem, showSwipeHint: Bool) {
self.dataService = dataService
self.item = item
self.showSwipeHint = showSwipeHint
}
func loadResult() async {
// isLoading = true
// let taskId = try? await dataService.createAITask(
// extraText: extraText,
// libraryItemId: item?.id ?? "",
// promptName: "summarize-001"
// )
//
// if let taskId = taskId {
// do {
// let fetchedText = try await dataService.pollAITask(jobId: taskId, timeoutInterval: 30)
// resultText = fetchedText
// } catch {
// print("ERROR WITH RESULT TEXT: ", error)
// }
// } else {
// print("NO TASK ID: ", taskId)
// }
// isLoading = false
}
}
@MainActor
struct PreviewItemView: View {
@StateObject var viewModel: PreviewItemViewModel
var body: some View {
VStack(spacing: 10) {
HStack {
AsyncImage(url: viewModel.item.siteIcon) { phase in
if let image = phase.image {
image
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 20, height: 20, alignment: .center)
} else {
Color.appButtonBackground
.frame(width: 20, height: 20, alignment: .center)
}
}
Text(viewModel.item.site)
.font(Font.system(size: 14))
.frame(maxWidth: .infinity, alignment: .topLeading)
}
.padding(.top, 10)
Text(viewModel.item.title)
// .font(.body)
// .fontWeight(.semibold)
.font(Font.system(size: 18, weight: .semibold))
.frame(maxWidth: .infinity, alignment: .topLeading)
Text(viewModel.item.author)
.font(Font.system(size: 14))
.foregroundColor(Color(hex: "898989"))
.frame(maxWidth: .infinity, alignment: .topLeading)
Color(hex: "2A2A2A")
.frame(height: 1)
.frame(maxWidth: .infinity, alignment: .center)
.padding(.vertical, 20)
if viewModel.isLoading {
ProgressView()
.task {
await viewModel.loadResult()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else {
Text(viewModel.item.summaryText)
.font(Font.system(size: 16))
// .font(.body)
.lineSpacing(12.0)
.frame(maxWidth: .infinity, alignment: .topLeading)
HStack {
Button(action: {}, label: {
HStack(alignment: .center) {
Text("Start listening")
.font(Font.system(size: 14))
.frame(height: 42, alignment: .center)
Image(systemName: "play.fill")
.resizable()
.frame(width: 10, height: 10)
}
.padding(.horizontal, 15)
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(18)
})
Spacer()
}
.padding(.top, 20)
}
Spacer()
if viewModel.showSwipeHint {
VStack {
Image.doubleChevronUp
Text("Swipe up for next article")
.foregroundColor(Color(hex: "898989"))
}
.padding(.bottom, 50)
}
}.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding(.top, 100)
.padding(.horizontal, 15)
}
}

View File

@ -8,10 +8,19 @@ struct CustomTabBar: View {
var body: some View {
HStack(spacing: 0) {
if !hideFollowingTab {
TabBarButton(key: "following", image: Image.tabFollowing, selectedTab: $selectedTab)
TabBarButton(key: "following",
image: Image.tabFollowing,
selectedTab: $selectedTab,
selectionColor: Color(hex: "EE8232"))
}
TabBarButton(key: "inbox", image: Image.tabLibrary, selectedTab: $selectedTab)
TabBarButton(key: "profile", image: Image.tabProfile, selectedTab: $selectedTab)
TabBarButton(key: "digest",
image: Image.tabDigest,
selectedTab: $selectedTab,
selectedImage: Image.tabDigestSelected)
TabBarButton(key: "inbox",
image: Image.tabLibrary,
selectedTab: $selectedTab)
// TabBarButton(key: "profile", image: Image.tabProfile, selectedTab: $selectedTab)
}
.padding(.top, 10)
.padding(.bottom, 10)
@ -23,6 +32,8 @@ struct TabBarButton: View {
let key: String
let image: Image
@Binding var selectedTab: String
var selectedImage: Image?
var selectionColor: Color?
var body: some View {
Button(action: {
@ -31,13 +42,20 @@ struct TabBarButton: View {
}
selectedTab = key
}, label: {
image
.resizable()
.renderingMode(.template)
.aspectRatio(contentMode: .fit)
tabImage
.frame(width: 28, height: 28)
.foregroundColor(selectedTab == key ? Color.blue : Color.themeTabButtonColor)
.frame(maxWidth: .infinity)
}).buttonStyle(.plain)
}
var tabImage: some View {
if let selectedImage = selectedImage, selectedTab == key {
return AnyView(selectedImage
.resizable()
.aspectRatio(contentMode: .fit))
} else {
return AnyView(image
.foregroundColor(selectedTab == key ? selectionColor ?? Color.blue : Color.themeTabButtonColor))
}
}
}

View File

@ -191,6 +191,7 @@ struct AnimatingCellHeight: AnimatableModifier {
@State var isListScrolled = false
@State var listTitle = ""
@State var showExpandedAudioPlayer = false
@State var showLibraryDigest = false
@Binding var isEditMode: EditMode
@ -382,6 +383,15 @@ struct AnimatingCellHeight: AnimatableModifier {
if isEditMode == .active {
Button(action: { isEditMode = .inactive }, label: { Text("Cancel") })
} else {
// if #available(iOS 17.0, *) {
// Button(
// action: { showLibraryDigest = true },
// label: { Image(systemName: "sparkles") }
// )
// .buttonStyle(.plain)
// .padding(.trailing, 4)
// }
if prefersListLayout {
Button(
action: { isEditMode = isEditMode == .active ? .inactive : .active },

View File

@ -116,11 +116,19 @@ struct LibraryTabView: View {
}.tag("following")
}
NavigationView {
HomeFeedContainerView(viewModel: inboxViewModel, isEditMode: $isEditMode)
.navigationBarTitleDisplayMode(.inline)
.navigationViewStyle(.stack)
}.tag("inbox")
if #available(iOS 17.0, *) {
NavigationView {
LibraryDigestView(dataService: dataService)
.navigationBarTitleDisplayMode(.inline)
.navigationViewStyle(.stack)
}.tag("digest")
} else {
NavigationView {
HomeFeedContainerView(viewModel: inboxViewModel, isEditMode: $isEditMode)
.navigationBarTitleDisplayMode(.inline)
.navigationViewStyle(.stack)
}.tag("inbox")
}
NavigationView {
ProfileView()

View File

@ -11,6 +11,7 @@ struct WebReader: PlatformViewRepresentable {
let articleContent: ArticleContent
let openLinkAction: (URL) -> Void
let tapHandler: () -> Void
let explainHandler: ((String) -> Void)?
let scrollPercentHandler: (Int) -> Void
let webViewActionHandler: (WKScriptMessage, WKScriptMessageReplyHandler?) -> Void
let navBarVisibilityUpdater: (Bool) -> Void
@ -51,6 +52,7 @@ struct WebReader: PlatformViewRepresentable {
let contentController = WKUserContentController()
webView.tapHandler = tapHandler
webView.explainHandler = explainHandler
webView.navigationDelegate = context.coordinator
webView.configuration.userContentController = contentController
webView.configuration.userContentController.removeAllScriptMessageHandlers()

View File

@ -41,6 +41,8 @@ struct WebReaderContainerView: View {
@State var displayLinkSheet = false
@State var linkToOpen: URL?
@State var showExplainSheet = false
@EnvironmentObject var dataService: DataService
@EnvironmentObject var audioController: AudioController
@Environment(\.openURL) var openURL
@ -92,6 +94,11 @@ struct WebReaderContainerView: View {
}
}
private func explainHandler(text: String) {
viewModel.explainText = String(text)
showExplainSheet = true
}
private func handleHighlightAction(message: WKScriptMessage) {
guard let messageBody = message.body as? [String: String] else { return }
guard let actionID = messageBody["actionID"] else { return }
@ -271,6 +278,13 @@ struct WebReaderContainerView: View {
Spacer()
#endif
// Button(
// action: { showExplainSheet = true },
// label: { Image(systemName: "sparkles") }
// )
// .buttonStyle(.plain)
// .padding(.trailing, 4)
Button(
action: { showLabelsModal = true },
label: {
@ -378,6 +392,7 @@ struct WebReaderContainerView: View {
#endif
},
tapHandler: tapHandler,
explainHandler: explainHandler,
scrollPercentHandler: scrollPercentHandler,
webViewActionHandler: webViewActionHandler,
navBarVisibilityUpdater: { visible in
@ -485,6 +500,20 @@ struct WebReaderContainerView: View {
.formSheet(isPresented: $showOpenArchiveSheet) {
OpenArchiveTodayView(item: item)
}
.formSheet(isPresented: $showExplainSheet) {
ExplainView(
viewModel: ExplainViewModel(
dataService: dataService,
item: self.item,
promptName: "explain-text-001",
extraText: viewModel.explainText
),
dismissAction: {
viewModel.explainText = nil
showExplainSheet = false
}
)
}
#endif
.sheet(isPresented: $showHighlightAnnotationModal) {
NavigationView {
@ -633,7 +662,6 @@ struct WebReaderContainerView: View {
try? WebViewManager.shared().dispatchEvent(.saveReadPosition)
}
.onDisappear {
// WebViewManager.shared().loadHTMLString("<html></html>", baseURL: nil)
WebViewManager.shared().loadHTMLString(WebReaderContent.emptyContent(isDark: Color.isDarkMode), baseURL: nil)
}
.onReceive(NotificationCenter.default.publisher(for: Notification.Name("PopToRoot"))) { _ in
@ -674,7 +702,7 @@ struct WebReaderContainerView: View {
shareActionID = UUID()
}
func print() {
func printReader() {
shareActionID = UUID()
}

View File

@ -21,6 +21,8 @@ struct SafariWebLink: Identifiable {
@Published var showOperationToast: Bool = false
@Published var operationStatus: OperationStatus = .none
@Published var explainText: String?
func hasOriginalUrl(_ item: Models.LibraryItem) -> Bool {
if let pageURLString = item.pageURLString, let host = URL(string: pageURLString)?.host {
if host == "omnivore.app" {

View File

@ -75,7 +75,7 @@ extension LoginError {
return .unauthorized
case .unknown:
return .unknown
case .pendingEmailVerification:
case .pendingEmailVerification, .stillProcessing:
return .pendingEmailVerification
}
}

View File

@ -10,6 +10,7 @@ public enum ServerError: String, Error {
case unauthenticated
case timeout
case unknown
case stillProcessing
case pendingEmailVerification
}
@ -22,6 +23,8 @@ extension ServerError {
case 401?, 403?:
self = .unauthenticated
return
case 202:
self = .stillProcessing
default:
break
}

View File

@ -23,6 +23,7 @@ public final class OmnivoreWebView: WKWebView {
#endif
public var tapHandler: (() -> Void)?
public var explainHandler: ((String) -> Void)?
private var currentMenu: ContextMenu = .defaultMenu
@ -298,6 +299,7 @@ public final class OmnivoreWebView: WKWebView {
case #selector(removeSelection): return true
case #selector(copy(_:)): return true
case #selector(setLabels(_:)): return true
case #selector(explainSelection): return true
case Selector(("_lookup:")): return (currentMenu == .defaultMenu)
case Selector(("_define:")): return (currentMenu == .defaultMenu)
@ -334,6 +336,18 @@ public final class OmnivoreWebView: WKWebView {
hideMenu()
}
@objc private func explainSelection() {
Task {
let selection = try? await self.evaluateJavaScript("window.getSelection().toString()")
if let selection = selection as? String, let explainHandler = explainHandler {
print("Explaining \(selection)")
explainHandler(selection)
} else {
showInReaderSnackbar("Error getting text to explain")
}
}
}
@objc private func shareSelection() {
do {
try dispatchEvent(.share)
@ -386,7 +400,8 @@ public final class OmnivoreWebView: WKWebView {
return
}
let highlight = UICommand(title: LocalText.genericHighlight, action: #selector(highlightSelection))
items = [highlight, annotate]
// let explain = UICommand(title: "Explain", action: #selector(explainSelection))
items = [highlight, /* explain, */ annotate]
} else {
let remove = UICommand(title: "Remove", action: #selector(removeSelection))
let setLabels = UICommand(title: LocalText.labelsGeneric, action: #selector(setLabels))

View File

@ -9,6 +9,9 @@ public extension Image {
static var tabFollowing: Image { Image("_tab_following", bundle: .module).renderingMode(.template) }
static var tabLibrary: Image { Image("_tab_library", bundle: .module).renderingMode(.template) }
static var tabDigest: Image { Image("_tab_digest", bundle: .module).renderingMode(.template) }
static var tabDigestSelected: Image { Image("_tab_digest_selected", bundle: .module) }
static var tabSearch: Image { Image("_tab_search", bundle: .module).renderingMode(.template) }
static var tabHighlights: Image { Image("_tab_highlights", bundle: .module).renderingMode(.template) }
static var tabProfile: Image { Image("_tab_profile", bundle: .module).renderingMode(.template) }
@ -49,4 +52,7 @@ public extension Image {
static var flairNewsletter: Image { Image("flair-newsletter", bundle: .module) }
static var flairPinned: Image { Image("flair-pinned", bundle: .module) }
static var flairRecommended: Image { Image("flair-recommended", bundle: .module) }
static var doubleChevronUp: Image { Image("double_chevron_up", bundle: .module) }
}

View File

@ -0,0 +1,24 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "_tab_digest.svg",
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

View File

@ -0,0 +1,18 @@
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_10231_7167)">
<path d="M12.5 3.5V5.5" stroke="#8E8E93" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.5 19.5V21.5" stroke="#8E8E93" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.5 8.5V16.5" stroke="#8E8E93" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.5 16.5V19.5" stroke="#8E8E93" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.5 10.5V15.5" stroke="#8E8E93" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M20.5 10.5V16.5" stroke="#8E8E93" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.5 6.5V13.5" stroke="#8E8E93" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M16.5 9.5V6.5" stroke="#8E8E93" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M16.5 19.5V12.5" stroke="#8E8E93" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_10231_7167">
<rect width="24" height="24" fill="white" transform="translate(0.5 0.5)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "_tab_digest_selected.svg",
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,18 @@
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_10210_6950)">
<path d="M12.5 3.5V5.5" stroke="#767AF8" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.5 19.5V21.5" stroke="#DE76F8" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.5 8.5V16.5" stroke="#767AF8" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.5 16.5V19.5" stroke="#76AAF8" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.5 10.5V15.5" stroke="#76F8D1" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M20.5 10.5V16.5" stroke="#76F8D1" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.5 6.5V13.5" stroke="#CF76F8" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M16.5 9.5V6.5" stroke="#CF76F8" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M16.5 19.5V12.5" stroke="#76AAF8" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_10210_6950">
<rect width="24" height="24" fill="white" transform="translate(0.5 0.5)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,24 @@
{
"images" : [
{
"filename" : "digest-archive-button.svg",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

View File

@ -0,0 +1,13 @@
<svg width="35" height="35" viewBox="0 0 35 35" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.5" y="0.5" width="34" height="34" rx="17" stroke="#3D3D3D"/>
<g clip-path="url(#clip0_9916_1773)">
<path d="M10 12.5002C10 12.0581 10.1756 11.6342 10.4882 11.3217C10.8007 11.0091 11.2246 10.8335 11.6667 10.8335H23.3333C23.7754 10.8335 24.1993 11.0091 24.5118 11.3217C24.8244 11.6342 25 12.0581 25 12.5002C25 12.9422 24.8244 13.3661 24.5118 13.6787C24.1993 13.9912 23.7754 14.1668 23.3333 14.1668H11.6667C11.2246 14.1668 10.8007 13.9912 10.4882 13.6787C10.1756 13.3661 10 12.9422 10 12.5002Z" stroke="#EDEDED" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.6667 14.1665V22.4998C11.6667 22.9419 11.8423 23.3658 12.1549 23.6783C12.4675 23.9909 12.8914 24.1665 13.3334 24.1665H21.6667C22.1088 24.1665 22.5327 23.9909 22.8453 23.6783C23.1578 23.3658 23.3334 22.9419 23.3334 22.4998V14.1665" stroke="#EDEDED" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M15.8333 17.5H19.1666" stroke="#EDEDED" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_9916_1773">
<rect width="20" height="20" fill="white" transform="translate(7.5 7.5)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,24 @@
{
"images" : [
{
"filename" : "digest-dots-button.svg",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

View File

@ -0,0 +1,13 @@
<svg width="35" height="35" viewBox="0 0 35 35" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.5" y="0.5" width="34" height="34" rx="17" stroke="#3D3D3D"/>
<g clip-path="url(#clip0_9916_1805)">
<path d="M10.8333 17.4998C10.8333 17.7209 10.921 17.9328 11.0773 18.0891C11.2336 18.2454 11.4456 18.3332 11.6666 18.3332C11.8876 18.3332 12.0996 18.2454 12.2558 18.0891C12.4121 17.9328 12.4999 17.7209 12.4999 17.4998C12.4999 17.2788 12.4121 17.0669 12.2558 16.9106C12.0996 16.7543 11.8876 16.6665 11.6666 16.6665C11.4456 16.6665 11.2336 16.7543 11.0773 16.9106C10.921 17.0669 10.8333 17.2788 10.8333 17.4998Z" fill="#D9D9D9" stroke="#D9D9D9" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M16.6667 17.4998C16.6667 17.7209 16.7545 17.9328 16.9108 18.0891C17.0671 18.2454 17.2791 18.3332 17.5001 18.3332C17.7211 18.3332 17.9331 18.2454 18.0893 18.0891C18.2456 17.9328 18.3334 17.7209 18.3334 17.4998C18.3334 17.2788 18.2456 17.0669 18.0893 16.9106C17.9331 16.7543 17.7211 16.6665 17.5001 16.6665C17.2791 16.6665 17.0671 16.7543 16.9108 16.9106C16.7545 17.0669 16.6667 17.2788 16.6667 17.4998Z" fill="#D9D9D9" stroke="#D9D9D9" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M22.5 17.4998C22.5 17.7209 22.5878 17.9328 22.7441 18.0891C22.9004 18.2454 23.1123 18.3332 23.3333 18.3332C23.5543 18.3332 23.7663 18.2454 23.9226 18.0891C24.0789 17.9328 24.1667 17.7209 24.1667 17.4998C24.1667 17.2788 24.0789 17.0669 23.9226 16.9106C23.7663 16.7543 23.5543 16.6665 23.3333 16.6665C23.1123 16.6665 22.9004 16.7543 22.7441 16.9106C22.5878 17.0669 22.5 17.2788 22.5 17.4998Z" fill="#D9D9D9" stroke="#D9D9D9" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_9916_1805">
<rect width="20" height="20" fill="white" transform="translate(7.5 7.5)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,24 @@
{
"images" : [
{
"filename" : "digest-play-button.svg",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

View File

@ -0,0 +1,11 @@
<svg width="35" height="35" viewBox="0 0 35 35" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.5" y="0.5" width="34" height="34" rx="17" stroke="#3D3D3D"/>
<g clip-path="url(#clip0_9916_1819)">
<path d="M13.75 12.5002V22.5002C13.75 22.6114 13.7796 22.7206 13.8359 22.8165C13.8921 22.9124 13.9729 22.9916 14.07 23.0459C14.1671 23.1002 14.2769 23.1275 14.3881 23.1252C14.4992 23.1229 14.6078 23.0909 14.7025 23.0327L22.8275 18.0327C22.9185 17.9768 22.9937 17.8985 23.0458 17.8052C23.0979 17.712 23.1253 17.607 23.1253 17.5002C23.1253 17.3934 23.0979 17.2883 23.0458 17.1951C22.9937 17.1019 22.9185 17.0236 22.8275 16.9677L14.7025 11.9677C14.6078 11.9094 14.4992 11.8775 14.3881 11.8751C14.2769 11.8728 14.1671 11.9002 14.07 11.9545C13.9729 12.0087 13.8921 12.0879 13.8359 12.1838C13.7796 12.2798 13.75 12.389 13.75 12.5002Z" fill="#EDEDED"/>
</g>
<defs>
<clipPath id="clip0_9916_1819">
<rect width="15" height="15" fill="white" transform="translate(10 10)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 996 B

View File

@ -0,0 +1,24 @@
{
"images" : [
{
"filename" : "digest-trash-button.svg",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

View File

@ -0,0 +1,15 @@
<svg width="35" height="35" viewBox="0 0 35 35" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.5" y="0.5" width="34" height="34" rx="17" stroke="#3D3D3D"/>
<g clip-path="url(#clip0_9916_1792)">
<path d="M10.8333 13.3335H24.1666" stroke="#D9D9D9" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M15.8333 16.6665V21.6665" stroke="#D9D9D9" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M19.1667 16.6665V21.6665" stroke="#D9D9D9" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.6667 13.3335L12.5001 23.3335C12.5001 23.7755 12.6757 24.1994 12.9882 24.512C13.3008 24.8246 13.7247 25.0002 14.1667 25.0002H20.8334C21.2754 25.0002 21.6994 24.8246 22.0119 24.512C22.3245 24.1994 22.5001 23.7755 22.5001 23.3335L23.3334 13.3335" stroke="#D9D9D9" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M15 13.3333V10.8333C15 10.6123 15.0878 10.4004 15.2441 10.2441C15.4004 10.0878 15.6123 10 15.8333 10H19.1667C19.3877 10 19.5996 10.0878 19.7559 10.2441C19.9122 10.4004 20 10.6123 20 10.8333V13.3333" stroke="#D9D9D9" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_9916_1792">
<rect width="20" height="20" fill="white" transform="translate(7.5 7.5)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "double_chevron_up.svg",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,4 @@
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.4614 17L12.4614 12L7.46143 17" stroke="#898989" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M17.4614 11L12.4614 6L7.46143 11" stroke="#898989" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 360 B

View File

@ -31,6 +31,22 @@ public enum Theme: String, CaseIterable {
}
}
public var fgColor: Color {
let prefersHighContrastText = UserDefaults.standard.bool(forKey: UserDefaultKey.prefersHighContrastWebFont.rawValue)
switch self {
case .system:
return Color.isDarkMode ? .white : .black
case .light:
return .black
case .dark:
return Color.white
case .sepia:
return prefersHighContrastText ? Color.black : (Color(hex: "#5F4B32") ?? Color.black)
case .apollo:
return prefersHighContrastText ? Color.white : (Color(hex: "#F3F3F3") ?? Color.white)
}
}
public var toolbarColor: Color {
ThemeManager.currentTheme.isDark ? Color.themeDarkWhiteGray : Color.themeMiddleGray
}
@ -98,6 +114,10 @@ public enum ThemeManager {
currentTheme.bgColor
}
public static var currentFgColor: Color {
currentTheme.fgColor
}
public static var currentHighlightColor: Color {
currentTheme.highlightColor
}