Skip to content

Commit 6f78e0f

Browse files
graycreateclaude
andauthored
fix: improve Toast and add InAppBrowser dark mode for V2EX (#134)
Toast improvements: - Reduce shadow opacity from 0.3 to 0.12 for softer appearance - Add version tracking to fix consecutive toast not clearing issue - Reset dismiss timer when new toast content arrives InAppBrowser improvements: - Add dark mode CSS injection for V2EX pages when app is in dark mode - Auto-detect app's appearance setting and apply matching theme - Comprehensive styling: boxes, cells, tags, links, code blocks, buttons, etc. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent d8b124b commit 6f78e0f

File tree

5 files changed

+143
-3
lines changed

5 files changed

+143
-3
lines changed

V2er/General/RootView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ struct RootHostView: View {
150150
ZStack {
151151
MainPage()
152152
.buttonStyle(.plain)
153-
.toast(isPresented: toast.isPresented) {
153+
.toast(isPresented: toast.isPresented, version: toast.version.raw) {
154154
DefaultToastView(title: toast.title.raw, icon: toast.icon.raw)
155155
}
156156
.sheet(isPresented: loginState.showLoginView) {

V2er/State/DataFlow/Actions/GlobalActions.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ func globalStateReducer(_ state: GlobalState, _ action: Action?) -> (GlobalState
5252
case let action as ShowToastAction:
5353
state.toast.title = action.title
5454
state.toast.icon = action.icon
55+
state.toast.version += 1
5556
state.toast.isPresented = true
5657
break
5758
case _ as LaunchFinishedAction:

V2er/View/InAppBrowserView.swift

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,136 @@ class WebViewHostController: UIViewController, WKNavigationDelegate, WKUIDelegat
310310
DispatchQueue.main.async {
311311
self.state.isLoading = false
312312
}
313+
// Inject dark mode CSS for V2EX pages if app is in dark mode
314+
injectDarkModeIfNeeded(for: webView)
315+
}
316+
317+
private func injectDarkModeIfNeeded(for webView: WKWebView) {
318+
guard let host = webView.url?.host,
319+
host.contains("v2ex.com") else {
320+
return
321+
}
322+
323+
// Check if app is in dark mode
324+
let isDarkMode: Bool
325+
if let rootStyle = V2erApp.rootViewController?.overrideUserInterfaceStyle {
326+
switch rootStyle {
327+
case .dark:
328+
isDarkMode = true
329+
case .light:
330+
isDarkMode = false
331+
case .unspecified:
332+
// Follow system
333+
isDarkMode = traitCollection.userInterfaceStyle == .dark
334+
@unknown default:
335+
isDarkMode = traitCollection.userInterfaceStyle == .dark
336+
}
337+
} else {
338+
isDarkMode = traitCollection.userInterfaceStyle == .dark
339+
}
340+
341+
guard isDarkMode else { return }
342+
343+
// Inject V2EX night mode CSS
344+
let darkModeCSS = """
345+
:root { color-scheme: dark; }
346+
body, html {
347+
background-color: #1a1a1a !important;
348+
color: #e0e0e0 !important;
349+
}
350+
#Wrapper, #Main, #Rightbar {
351+
background-color: #1a1a1a !important;
352+
}
353+
.box, .cell, .cell_ops {
354+
background-color: #262626 !important;
355+
border-color: #3a3a3a !important;
356+
}
357+
.header, .inner {
358+
background-color: #262626 !important;
359+
}
360+
/* Tags */
361+
.node, a.node, .tag, a.tag {
362+
background-color: #3a3a3a !important;
363+
color: #ccc !important;
364+
}
365+
.topic_info, .votes {
366+
background-color: transparent !important;
367+
}
368+
/* Topic content */
369+
.topic-link, .item_title a, h1 {
370+
color: #e0e0e0 !important;
371+
}
372+
.topic_content, .reply_content, .markdown_body {
373+
color: #e0e0e0 !important;
374+
}
375+
/* Links */
376+
a {
377+
color: #4a9eff !important;
378+
}
379+
a.node:hover, a.tag:hover {
380+
background-color: #4a4a4a !important;
381+
}
382+
/* Meta text */
383+
.gray, .fade, .small, .ago, .no {
384+
color: #888 !important;
385+
}
386+
.snow {
387+
background-color: #333 !important;
388+
}
389+
/* Code blocks */
390+
pre, code {
391+
background-color: #333 !important;
392+
color: #e0e0e0 !important;
393+
}
394+
/* Forms */
395+
input, textarea, select {
396+
background-color: #333 !important;
397+
color: #e0e0e0 !important;
398+
border-color: #555 !important;
399+
}
400+
/* Tabs */
401+
.tab, .tab_current {
402+
background-color: #333 !important;
403+
color: #ccc !important;
404+
}
405+
.tab:hover {
406+
background-color: #444 !important;
407+
}
408+
/* Buttons */
409+
.super.button, .normal.button, input[type=submit] {
410+
background-color: #444 !important;
411+
color: #e0e0e0 !important;
412+
border-color: #555 !important;
413+
}
414+
/* Subtle backgrounds */
415+
.subtle {
416+
background-color: #2a2a2a !important;
417+
}
418+
/* Thank area */
419+
.thank_area {
420+
background-color: transparent !important;
421+
}
422+
/* Member info */
423+
.member-info, .balance_area {
424+
background-color: #262626 !important;
425+
}
426+
/* Separator */
427+
hr, .sep {
428+
border-color: #3a3a3a !important;
429+
background-color: #3a3a3a !important;
430+
}
431+
/* Page title */
432+
#page-title {
433+
background-color: #1a1a1a !important;
434+
}
435+
/* Embedded */
436+
.embedded {
437+
background-color: #2a2a2a !important;
438+
border-color: #3a3a3a !important;
439+
}
440+
"""
441+
let js = "var style = document.createElement('style'); style.innerHTML = `\(darkModeCSS)`; document.head.appendChild(style);"
442+
webView.evaluateJavaScript(js, completionHandler: nil)
313443
}
314444

315445
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {

V2er/View/Login/LoginPage.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ struct LoginPage: StateView {
3232
dismiss()
3333
}
3434
}
35-
.toast(isPresented: toast.isPresented, paddingTop: 40) {
35+
.toast(isPresented: toast.isPresented, paddingTop: 40, version: toast.version.raw) {
3636
DefaultToastView(title: toast.title.raw, icon: toast.icon.raw)
3737
}
3838
.alert(isPresented: bindingState.showAlert) {

V2er/View/Widget/Toast.swift

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ final class Toast {
2121
var isPresented: Bool = false
2222
var title: String = ""
2323
var icon: String = ""
24+
var version: Int = 0 // Incremented on each new toast to trigger timer reset
2425

2526
static func show(_ title: String, icon: String = .empty, target: Reducer = .global) {
2627
guard title.notEmpty() || icon.notEmpty() else { return }
@@ -71,6 +72,7 @@ struct DefaultToastView: View {
7172
private struct ToastContainerView<Content: View>: View {
7273
@Binding var isPresented: Bool
7374
let paddingTop: CGFloat
75+
let version: Int
7476
let content: Content
7577

7678
@State private var dismissTask: Task<Void, Never>?
@@ -81,7 +83,7 @@ private struct ToastContainerView<Content: View>: View {
8183
content
8284
.background(Color.secondaryBackground.opacity(0.98))
8385
.cornerRadius(99)
84-
.shadow(color: Color.primaryText.opacity(0.3), radius: 3)
86+
.shadow(color: Color.primaryText.opacity(0.12), radius: 4, y: 2)
8587
.padding(.top, paddingTop)
8688
.transition(.move(edge: .top).combined(with: .opacity))
8789
.zIndex(1)
@@ -101,6 +103,11 @@ private struct ToastContainerView<Content: View>: View {
101103
scheduleDismiss()
102104
}
103105
}
106+
.onChange(of: version) { _ in
107+
// New toast content: reset timer
108+
toastId = UUID()
109+
scheduleDismiss()
110+
}
104111
}
105112

106113
private func scheduleDismiss() {
@@ -133,13 +140,15 @@ private struct ToastContainerView<Content: View>: View {
133140
extension View {
134141
func toast<Content: View>(isPresented: Binding<Bool>,
135142
paddingTop: CGFloat = 0,
143+
version: Int = 0,
136144
@ViewBuilder content: () -> Content?) -> some View {
137145
ZStack(alignment: .top) {
138146
self
139147
if isPresented.wrappedValue, let toastContent = content() {
140148
ToastContainerView(
141149
isPresented: isPresented,
142150
paddingTop: paddingTop,
151+
version: version,
143152
content: toastContent
144153
)
145154
}

0 commit comments

Comments
 (0)