fix(appearance): 完善深色模式和阅读页面对比度

- 区分按钮填充与图标颜色,统一书架、书源、搜索和阅读设置的选中态
- 调整编辑器语法高亮、键盘工具栏及统计卡片的浅深色显示
- 适配 RSS 正文和登录网页背景,保留书源自定义样式和页面状态
- 补充外观切换、页面对比度和管理页面的回归测试
This commit is contained in:
Aminark
2026-10-22 10:53:19 +08:00
parent bf72ebc894
commit 45cf5b5482
21 changed files with 611 additions and 46 deletions
+16
View File
@@ -340,6 +340,22 @@ extension View {
}
}
@ViewBuilder
func readingNavigationBarBackground() -> some View {
if #available(iOS 16.0, *), !ReadingCompatibility.usesLegacyUI {
toolbarBackground(ReadingTheme.canvas, for: .navigationBar)
.toolbarBackground(.visible, for: .navigationBar)
} else {
background(ReadingUIKitConfiguration { controller in
let appearance = UINavigationBarAppearance()
appearance.configureWithOpaqueBackground()
appearance.backgroundColor = UIColor(ReadingTheme.canvas)
controller.navigationController?.navigationBar.standardAppearance = appearance
controller.navigationController?.navigationBar.scrollEdgeAppearance = appearance
})
}
}
@ViewBuilder
func readingTabBarBackground() -> some View {
if #available(iOS 16.0, *), !ReadingCompatibility.usesLegacyUI {
+29 -2
View File
@@ -77,6 +77,29 @@ private struct ReadingUnavailableLabelStyle: LabelStyle {
}
extension View {
/// Keep filled controls dark in both appearances, independently of icon tint.
func readingProminentButtonStyle() -> some View {
buttonStyle(.borderedProminent)
.tint(ReadingTheme.accentFill)
.foregroundStyle(ReadingTheme.onAccent)
}
func readingSecondaryButtonStyle() -> some View {
buttonStyle(.bordered)
.tint(ReadingTheme.accentFill)
.foregroundStyle(ReadingTheme.accent)
}
@ViewBuilder
func readingChoiceButtonStyle(isSelected: Bool) -> some View {
if isSelected {
readingProminentButtonStyle()
.accessibilityAddTraits(.isSelected)
} else {
readingSecondaryButtonStyle()
}
}
func readingSearchable(text: Binding<String>, isPresented: Binding<Bool>, prompt: String) -> some View {
modifier(ReadingSearchModifier(text: text, isPresented: isPresented, prompt: prompt))
}
@@ -144,7 +167,11 @@ private struct ReadingSearchModifier: ViewModifier {
}
enum ReadingTheme {
static let accent = adaptive(0x171717, dark: 0xF2F2F2)
static let accent = adaptive(0x171717, dark: 0xDADAE0)
static let accentFill = adaptive(0x171717, dark: 0x414148)
static let emphasisSurface = adaptive(0x171717, dark: 0x29292E)
static let selectionBorder = adaptive(0x171717, dark: 0x777780)
static let toggleTint = Color(uiColor: .systemGreen)
static let accentSoft = adaptive(0xEEEEEF, dark: 0x29292C)
static let coral = Color(red: 0.76, green: 0.48, blue: 0.27)
static let paper = Color(red: 0.965, green: 0.945, blue: 0.895)
@@ -153,7 +180,7 @@ enum ReadingTheme {
static let groupedCanvas = adaptive(0xF3F3F4, dark: 0x1C1C1F)
static let surface = adaptive(0xFFFFFF, dark: 0x232326)
static let separator = adaptive(0xE6E6E8, dark: 0x363639)
static let onAccent = adaptive(0xFFFFFF, dark: 0x171717)
static let onAccent = Color.white
private static func adaptive(_ light: UInt32, dark: UInt32) -> Color {
Color(uiColor: UIColor { traits in
@@ -47,7 +47,9 @@ struct ReadingStatisticsView: View {
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(24)
.background(ReadingTheme.accent, in: RoundedRectangle(cornerRadius: 20))
.background(ReadingTheme.emphasisSurface, in: RoundedRectangle(cornerRadius: 20))
.accessibilityElement(children: .contain)
.accessibilityIdentifier("statistics.todayCard")
}
private var overviewGrid: some View {
+2 -2
View File
@@ -194,7 +194,7 @@ struct BookDetailView: View {
.font(.body.weight(.medium))
.frame(maxWidth: .infinity, minHeight: 28)
}
.buttonStyle(.borderedProminent)
.readingProminentButtonStyle()
.disabled(model.chapters.isEmpty)
Button {
if library.contains(model.book) {
@@ -210,7 +210,7 @@ struct BookDetailView: View {
.font(.subheadline)
.frame(maxWidth: dynamicTypeSize.isAccessibilitySize ? .infinity : nil, minHeight: 28)
}
.buttonStyle(.bordered)
.readingSecondaryButtonStyle()
}
@ViewBuilder
+1 -1
View File
@@ -155,7 +155,7 @@ struct ExploreView: View {
Text("添加带有发现分类的书源,即可在这里浏览书籍。")
} actions: {
NavigationLink("管理书源") { SourceManagementView() }
.buttonStyle(.borderedProminent)
.readingProminentButtonStyle()
}
} else {
LazyVStack(spacing: 0) {
+9 -4
View File
@@ -142,7 +142,7 @@ private struct RSSArticleListView: View {
ScrollView(.horizontal, showsIndicators: false) {
HStack { ForEach(Array(sorts.enumerated()), id: \.element.id) { index, sort in
Button(sort.name.isEmpty ? "默认" : sort.name) { selectedSort = index; Task { await refresh() } }
.buttonStyle(.bordered).tint(selectedSort == index ? ReadingTheme.accent : .secondary)
.readingChoiceButtonStyle(isSelected: selectedSort == index)
}}.padding(.horizontal)
}.padding(.vertical, 6)
}
@@ -435,7 +435,7 @@ private struct RSSDownloadView: View {
Image(systemName: "doc.badge.arrow.up").font(.system(size: 48)).foregroundStyle(ReadingTheme.accent)
Text(fileName).font(.headline)
Text(ByteCountFormatter.string(fromByteCount: Int64(data.count), countStyle: .file)).foregroundStyle(.secondary)
Button("选择保存位置", systemImage: "folder") { showsExporter = true }.buttonStyle(.borderedProminent)
Button("选择保存位置", systemImage: "folder") { showsExporter = true }.readingProminentButtonStyle()
}
}.padding().navigationTitle("文件下载").toolbar { ToolbarItem(placement: .cancellationAction) { Button("关闭") { dismiss() } } }
.task(id: url) { await download() }
@@ -464,7 +464,7 @@ private struct RSSBinaryDocument: FileDocument {
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { .init(regularFileWithContents: data) }
}
private struct RSSPhotoView: View {
struct RSSPhotoView: View {
@Environment(\.dismiss) private var dismiss
let value: String
let source: RssSource
@@ -480,9 +480,13 @@ private struct RSSPhotoView: View {
else if loading { ProgressView() }
else { ReadingUnavailableView("图片加载失败", systemImage: "photo.badge.exclamationmark") }
}.frame(minWidth: proxy.size.width, minHeight: proxy.size.height)
}.background(.black)
}
.background(.black)
.environment(\.colorScheme, .dark)
.tint(.white)
}
.navigationTitle(source.sourceName).navigationBarTitleDisplayMode(.inline)
.readingNavigationBarBackground()
.toolbar {
ToolbarItem(placement: .primaryAction) { Button("保存到照片", systemImage: "square.and.arrow.down") { save() }.disabled(image == nil) }
ToolbarItem(placement: .confirmationAction) { Button("完成") { dismiss() } }
@@ -490,6 +494,7 @@ private struct RSSPhotoView: View {
.task(id: value) { await load() }
.alert("图片", isPresented: Binding(get: { message != nil }, set: { if !$0 { message = nil } })) { Button("好") { message = nil } } message: { Text(message ?? "") }
}
.readingTabBarHidden()
}
private func load() async {
+3
View File
@@ -41,6 +41,9 @@ struct RSSWebReaderView: UIViewRepresentable {
controller.addUserScript(.init(source: inject, injectionTime: .atDocumentEnd, forMainFrameOnly: false))
}
let webView = WKWebView(frame: .zero, configuration: configuration)
webView.isOpaque = false
webView.backgroundColor = UIColor(ReadingTheme.canvas)
webView.scrollView.backgroundColor = UIColor(ReadingTheme.canvas)
webView.navigationDelegate = context.coordinator
webView.uiDelegate = context.coordinator
webView.allowsBackForwardNavigationGestures = true
+16 -7
View File
@@ -492,7 +492,7 @@ struct ReaderView: View {
.accessibilityIdentifier("reader.failureChangeSource")
}
}
.buttonStyle(.bordered)
.readingSecondaryButtonStyle()
}
private func openSourceSwitch() {
@@ -1118,7 +1118,7 @@ struct ReaderView: View {
Image(systemName: "textformat.size.smaller")
.frame(width: 38, height: 32)
}
.buttonStyle(.bordered)
.readingSecondaryButtonStyle()
VStack(spacing: 2) {
Text("字号")
@@ -1135,7 +1135,7 @@ struct ReaderView: View {
Image(systemName: "textformat.size.larger")
.frame(width: 38, height: 32)
}
.buttonStyle(.bordered)
.readingSecondaryButtonStyle()
}
HStack(spacing: 8) {
@@ -1285,14 +1285,19 @@ struct ReaderView: View {
Text(choice.title)
.font(.caption2)
}
.foregroundStyle(pageAnimation == choice.value ? Color.white : Color.primary)
.foregroundStyle(pageAnimation == choice.value ? ReadingTheme.onAccent : Color.primary)
.frame(width: 58, height: 54)
.background(
pageAnimation == choice.value ? ReadingTheme.accent : Color.secondary.opacity(0.1),
pageAnimation == choice.value ? ReadingTheme.accentFill : Color.secondary.opacity(0.1),
in: RoundedRectangle(cornerRadius: 12, style: .continuous)
)
.overlay {
RoundedRectangle(cornerRadius: 12, style: .continuous)
.strokeBorder(pageAnimation == choice.value ? ReadingTheme.selectionBorder : .clear, lineWidth: 1)
}
}
.buttonStyle(.plain)
.accessibilityAddTraits(pageAnimation == choice.value ? .isSelected : [])
}
}
}
@@ -1392,12 +1397,16 @@ struct ReaderView: View {
Button(action: action) {
Text(title)
.font(.caption.weight(.medium))
.foregroundStyle(selected ? Color.white : Color.primary)
.foregroundStyle(selected ? ReadingTheme.onAccent : Color.primary)
.padding(.horizontal, 12)
.padding(.vertical, 7)
.background(selected ? ReadingTheme.accent : Color.secondary.opacity(0.1), in: Capsule())
.background(selected ? ReadingTheme.accentFill : Color.secondary.opacity(0.1), in: Capsule())
.overlay {
Capsule().strokeBorder(selected ? ReadingTheme.selectionBorder : .clear, lineWidth: 1)
}
}
.buttonStyle(.plain)
.accessibilityAddTraits(selected ? .isSelected : [])
}
private func valueSlider(
@@ -39,7 +39,7 @@ struct QRCodeImportView: View {
} label: {
Label("从照片选择二维码", systemImage: "photo")
.frame(maxWidth: .infinity)
}.buttonStyle(.borderedProminent).disabled(isReading)
}.readingProminentButtonStyle().disabled(isReading)
Text("支持 HTTP(S) 链接、Legado 导入深链和 JSON 内容。识别后进入导入流程。")
.font(.footnote).foregroundStyle(.secondary)
if values.count > 1 {
@@ -3,6 +3,26 @@ import UIKit
struct SyntaxCodeEditor: UIViewRepresentable {
enum Language { case json, rule, javascript, regex }
// Syntax colors are text, so they need more contrast than system icon tints.
private enum Palette {
static let keyword = adaptive(0x7A2BA6, dark: 0xD5A5F5)
static let number = adaptive(0x895900, dark: 0xE6B66A)
static let operation = adaptive(0xAD285F, dark: 0xF49AB8)
static let punctuation = adaptive(0x14666E, dark: 0x83CED8)
static let string = adaptive(0x2D6B39, dark: 0x8CCF96)
static let key = adaptive(0x1E5EB2, dark: 0x93BFFF)
static let comment = adaptive(0x64646B, dark: 0xA4A4AE)
private static func adaptive(_ light: UInt32, dark: UInt32) -> UIColor {
UIColor { traits in
let hex = traits.userInterfaceStyle == .dark ? dark : light
return UIColor(red: CGFloat((hex >> 16) & 0xff) / 255,
green: CGFloat((hex >> 8) & 0xff) / 255,
blue: CGFloat(hex & 0xff) / 255, alpha: 1)
}
}
}
@Binding var text: String
var language: Language = .rule
@@ -13,6 +33,7 @@ struct SyntaxCodeEditor: UIViewRepresentable {
view.delegate = context.coordinator
view.font = .monospacedSystemFont(ofSize: 14, weight: .regular)
view.backgroundColor = .secondarySystemGroupedBackground
view.tintColor = UIColor(ReadingTheme.accent)
view.textContainerInset = UIEdgeInsets(top: 14, left: 12, bottom: 20, right: 12)
view.autocorrectionType = .no
view.autocapitalizationType = .none
@@ -24,6 +45,7 @@ struct SyntaxCodeEditor: UIViewRepresentable {
view.accessibilityIdentifier = "syntaxCodeEditor"
if #available(iOS 16.0, *) { view.isFindInteractionEnabled = true }
let bar = UIToolbar()
bar.tintColor = UIColor(ReadingTheme.accent)
bar.sizeToFit()
bar.items = [
UIBarButtonItem(image: UIImage(systemName: "magnifyingglass"), primaryAction: UIAction(title: "", subtitle: nil) { [weak view, coordinator = context.coordinator] _ in
@@ -126,12 +148,12 @@ struct SyntaxCodeEditor: UIViewRepresentable {
storage.addAttributes([.font: UIFont.monospacedSystemFont(ofSize: 14, weight: .regular), .foregroundColor: UIColor.label], range: all)
if storage.length <= 500_000 {
let tokens: [(String, UIColor)] = [
(#"\b(?:true|false|null|undefined|return|var|let|const|if|else|function|new|for|while|try|catch|throw|await|async)\b"#, .systemPurple),
(#"\b\d+(?:\.\d+)?\b"#, .systemOrange),
(#"@(?:css|xpath|json|js|put|get):?|\$\.|&&|\|\||%%|##|\{\{|\}\}"#, .systemPink),
(#"[{}\[\]()]"#, .systemTeal),
(#""(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`"#, .systemGreen),
(#""(?:\\.|[^"\\])*"(?=\s*:)"#, .systemBlue),
(#"\b(?:true|false|null|undefined|return|var|let|const|if|else|function|new|for|while|try|catch|throw|await|async)\b"#, Palette.keyword),
(#"\b\d+(?:\.\d+)?\b"#, Palette.number),
(#"@(?:css|xpath|json|js|put|get):?|\$\.|&&|\|\||%%|##|\{\{|\}\}"#, Palette.operation),
(#"[{}\[\]()]"#, Palette.punctuation),
(#""(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`"#, Palette.string),
(#""(?:\\.|[^"\\])*"(?=\s*:)"#, Palette.key),
]
for (pattern, color) in tokens {
guard let regex = try? NSRegularExpression(pattern: pattern) else { continue }
@@ -143,7 +165,7 @@ struct SyntaxCodeEditor: UIViewRepresentable {
for pattern in [#"[.*+?^$|]"#, #"\\[dDsSwWbB0-9]|\{[0-9,]+\}"#] {
let regex = try? NSRegularExpression(pattern: pattern)
regex?.enumerateMatches(in: view.text, range: all) { result, _, _ in
if let result { storage.addAttribute(.foregroundColor, value: UIColor.systemPink, range: result.range) }
if let result { storage.addAttribute(.foregroundColor, value: Palette.operation, range: result.range) }
}
}
}
@@ -151,7 +173,7 @@ struct SyntaxCodeEditor: UIViewRepresentable {
let pattern = #"(?m)^\s*//.*$|/\*[\s\S]*?\*/"#
let regex = try? NSRegularExpression(pattern: pattern)
regex?.enumerateMatches(in: view.text, range: all) { result, _, _ in
if let result { storage.addAttribute(.foregroundColor, value: UIColor.secondaryLabel, range: result.range) }
if let result { storage.addAttribute(.foregroundColor, value: Palette.comment, range: result.range) }
}
}
}
+1 -2
View File
@@ -81,8 +81,7 @@ struct SourceDebugView: View {
Label(model.isRunning ? "正在调试…" : "运行完整链路", systemImage: "play.fill")
.font(.headline).frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
.tint(ReadingTheme.accent)
.readingProminentButtonStyle()
.disabled(model.isRunning || model.keyword.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
}
}
+3 -4
View File
@@ -44,11 +44,10 @@ struct SourceEditorView: View {
HStack {
ForEach(BookSourceEditorSchema.sections) { section in
Button(section.title) { sectionID = section.id }
.buttonStyle(.bordered)
.tint(sectionID == section.id ? ReadingTheme.accent : .secondary)
.readingChoiceButtonStyle(isSelected: sectionID == section.id)
}
Button("选项") { sectionID = "options" }.buttonStyle(.bordered)
.tint(sectionID == "options" ? ReadingTheme.accent : .secondary)
Button("选项") { sectionID = "options" }
.readingChoiceButtonStyle(isSelected: sectionID == "options")
}.padding(.horizontal)
}.padding(.vertical, 6)
Form {
@@ -1,5 +1,6 @@
@preconcurrency import WebKit
import SwiftUI
import UIKit
struct SourceLoginView: View {
let source: BookSource
@@ -74,6 +75,9 @@ private struct SourceLoginWebView: UIViewRepresentable {
let configuration = WKWebViewConfiguration()
configuration.websiteDataStore = .default()
let webView = WKWebView(frame: .zero, configuration: configuration)
webView.isOpaque = false
webView.backgroundColor = UIColor(ReadingTheme.canvas)
webView.scrollView.backgroundColor = UIColor(ReadingTheme.canvas)
webView.navigationDelegate = context.coordinator
context.coordinator.installExistingCookies(in: webView) { webView.load(request) }
return webView
@@ -61,7 +61,7 @@ struct SourceManagementView: View {
Text("新建书源,或从文件、链接、JSON、二维码导入。")
} actions: {
Button("导入书源", action: presentFileImporter)
.buttonStyle(.borderedProminent)
.readingProminentButtonStyle()
}
} else {
List {
+14 -9
View File
@@ -50,6 +50,10 @@ struct RootTabView: View {
SearchView(initialKeyword: automationSearchKeyword)
} else if requestedDemoPage == "sources" {
SourceManagementView()
} else if requestedDemoPage == "rss" {
RSSHomeView()
} else if requestedDemoPage == "rss-photo" {
RSSPhotoView(value: "data:image/png;base64,", source: RssSource(sourceUrl: "https://appearance.invalid", sourceName: "图片外观检查"))
} else {
BookshelfView()
}
@@ -99,7 +103,6 @@ struct RootTabView: View {
.tabItem { Label("我的", systemImage: "person.crop.circle") }
.tag(Tab.my)
}
.tint(ReadingTheme.accent)
.readingTabBarBackground()
.preferredColorScheme(themeMode == "2" ? .dark : (themeMode == "1" || themeMode == "3" ? .light : nil))
.transaction { transaction in
@@ -123,6 +126,8 @@ struct RootTabView: View {
dismissButton: .default(Text("好")) { sourceLibrary.notice = nil }
)
}
.tint(ReadingTheme.accent)
.toggleStyle(SwitchToggleStyle(tint: ReadingTheme.toggleTint))
}
private var requestedDemoPage: String? {
@@ -487,7 +492,7 @@ private struct ContinueReadingCard: View {
.foregroundStyle(ReadingTheme.onAccent)
.padding(.horizontal, 18)
.frame(minHeight: 40)
.background(ReadingTheme.accent, in: Capsule())
.background(ReadingTheme.accentFill, in: Capsule())
.padding(.top, 20)
}
.frame(maxWidth: .infinity, alignment: .leading)
@@ -719,7 +724,7 @@ private struct EmptyBookshelfHero: View {
}
VStack(spacing: 12) {
Button("导入本地书", action: onImport)
.buttonStyle(.borderedProminent)
.readingProminentButtonStyle()
.controlSize(.large)
NavigationLink("添加书源") { SourceManagementView() }
.font(.subheadline)
@@ -986,7 +991,7 @@ struct SearchView: View {
Spacer()
if model.isPaused {
Button("继续", systemImage: "play.fill") { model.resume() }
.buttonStyle(.bordered)
.readingSecondaryButtonStyle()
.controlSize(.small)
} else if model.state == .searching {
StatusPill(text: "\(model.completedSourceCount)/\(model.totalSourceCount)", systemImage: "magnifyingglass")
@@ -1037,10 +1042,10 @@ struct SearchView: View {
) {
if model.isPaused {
Button("继续搜索", systemImage: "play.fill") { model.resume() }
.buttonStyle(.borderedProminent).tint(ReadingTheme.accent)
.readingProminentButtonStyle()
}
Button("修改关键词", systemImage: "text.cursor") { searchIsPresented = true }
.buttonStyle(.bordered)
.readingSecondaryButtonStyle()
}
}
@@ -1051,13 +1056,13 @@ struct SearchView: View {
systemImage: "network.slash"
) {
Button("调整范围", systemImage: "scope") { showsScope = true }
.buttonStyle(.borderedProminent).tint(ReadingTheme.accent)
.readingProminentButtonStyle()
NavigationLink {
SourceManagementView()
} label: {
Label("管理书源", systemImage: "network")
}
.buttonStyle(.bordered)
.readingSecondaryButtonStyle()
}
}
@@ -1281,7 +1286,7 @@ private struct SettingsView: View {
.clipped()
.overlay {
if colorScheme == .dark {
Color.black.opacity(0.62)
Color.black.opacity(0.78)
}
}
.overlay(alignment: .bottom) {