mirror of
https://github.com/Aminark/legado-ios.git
synced 2026-10-24 12:51:22 +08:00
fix(appearance): 完善深色模式和阅读页面对比度
- 区分按钮填充与图标颜色,统一书架、书源、搜索和阅读设置的选中态 - 调整编辑器语法高亮、键盘工具栏及统计卡片的浅深色显示 - 适配 RSS 正文和登录网页背景,保留书源自定义样式和页面状态 - 补充外观切换、页面对比度和管理页面的回归测试
This commit is contained 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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -155,7 +155,7 @@ struct ExploreView: View {
|
||||
Text("添加带有发现分类的书源,即可在这里浏览书籍。")
|
||||
} actions: {
|
||||
NavigationLink("管理书源") { SourceManagementView() }
|
||||
.buttonStyle(.borderedProminent)
|
||||
.readingProminentButtonStyle()
|
||||
}
|
||||
} else {
|
||||
LazyVStack(spacing: 0) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user