diff --git a/V2er/General/RootView.swift b/V2er/General/RootView.swift index a0b0e8c..9db3251 100644 --- a/V2er/General/RootView.swift +++ b/V2er/General/RootView.swift @@ -150,7 +150,7 @@ struct RootHostView: View { ZStack { MainPage() .buttonStyle(.plain) - .toast(isPresented: toast.isPresented) { + .toast(isPresented: toast.isPresented, version: toast.version.raw) { DefaultToastView(title: toast.title.raw, icon: toast.icon.raw) } .sheet(isPresented: loginState.showLoginView) { diff --git a/V2er/State/DataFlow/Actions/GlobalActions.swift b/V2er/State/DataFlow/Actions/GlobalActions.swift index 557f343..87458fd 100644 --- a/V2er/State/DataFlow/Actions/GlobalActions.swift +++ b/V2er/State/DataFlow/Actions/GlobalActions.swift @@ -52,6 +52,7 @@ func globalStateReducer(_ state: GlobalState, _ action: Action?) -> (GlobalState case let action as ShowToastAction: state.toast.title = action.title state.toast.icon = action.icon + state.toast.version += 1 state.toast.isPresented = true break case _ as LaunchFinishedAction: diff --git a/V2er/View/InAppBrowserView.swift b/V2er/View/InAppBrowserView.swift index fda4fdb..1ee5320 100644 --- a/V2er/View/InAppBrowserView.swift +++ b/V2er/View/InAppBrowserView.swift @@ -310,6 +310,136 @@ class WebViewHostController: UIViewController, WKNavigationDelegate, WKUIDelegat DispatchQueue.main.async { self.state.isLoading = false } + // Inject dark mode CSS for V2EX pages if app is in dark mode + injectDarkModeIfNeeded(for: webView) + } + + private func injectDarkModeIfNeeded(for webView: WKWebView) { + guard let host = webView.url?.host, + host.contains("v2ex.com") else { + return + } + + // Check if app is in dark mode + let isDarkMode: Bool + if let rootStyle = V2erApp.rootViewController?.overrideUserInterfaceStyle { + switch rootStyle { + case .dark: + isDarkMode = true + case .light: + isDarkMode = false + case .unspecified: + // Follow system + isDarkMode = traitCollection.userInterfaceStyle == .dark + @unknown default: + isDarkMode = traitCollection.userInterfaceStyle == .dark + } + } else { + isDarkMode = traitCollection.userInterfaceStyle == .dark + } + + guard isDarkMode else { return } + + // Inject V2EX night mode CSS + let darkModeCSS = """ + :root { color-scheme: dark; } + body, html { + background-color: #1a1a1a !important; + color: #e0e0e0 !important; + } + #Wrapper, #Main, #Rightbar { + background-color: #1a1a1a !important; + } + .box, .cell, .cell_ops { + background-color: #262626 !important; + border-color: #3a3a3a !important; + } + .header, .inner { + background-color: #262626 !important; + } + /* Tags */ + .node, a.node, .tag, a.tag { + background-color: #3a3a3a !important; + color: #ccc !important; + } + .topic_info, .votes { + background-color: transparent !important; + } + /* Topic content */ + .topic-link, .item_title a, h1 { + color: #e0e0e0 !important; + } + .topic_content, .reply_content, .markdown_body { + color: #e0e0e0 !important; + } + /* Links */ + a { + color: #4a9eff !important; + } + a.node:hover, a.tag:hover { + background-color: #4a4a4a !important; + } + /* Meta text */ + .gray, .fade, .small, .ago, .no { + color: #888 !important; + } + .snow { + background-color: #333 !important; + } + /* Code blocks */ + pre, code { + background-color: #333 !important; + color: #e0e0e0 !important; + } + /* Forms */ + input, textarea, select { + background-color: #333 !important; + color: #e0e0e0 !important; + border-color: #555 !important; + } + /* Tabs */ + .tab, .tab_current { + background-color: #333 !important; + color: #ccc !important; + } + .tab:hover { + background-color: #444 !important; + } + /* Buttons */ + .super.button, .normal.button, input[type=submit] { + background-color: #444 !important; + color: #e0e0e0 !important; + border-color: #555 !important; + } + /* Subtle backgrounds */ + .subtle { + background-color: #2a2a2a !important; + } + /* Thank area */ + .thank_area { + background-color: transparent !important; + } + /* Member info */ + .member-info, .balance_area { + background-color: #262626 !important; + } + /* Separator */ + hr, .sep { + border-color: #3a3a3a !important; + background-color: #3a3a3a !important; + } + /* Page title */ + #page-title { + background-color: #1a1a1a !important; + } + /* Embedded */ + .embedded { + background-color: #2a2a2a !important; + border-color: #3a3a3a !important; + } + """ + let js = "var style = document.createElement('style'); style.innerHTML = `\(darkModeCSS)`; document.head.appendChild(style);" + webView.evaluateJavaScript(js, completionHandler: nil) } func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { diff --git a/V2er/View/Login/LoginPage.swift b/V2er/View/Login/LoginPage.swift index 5fc0108..d53612e 100644 --- a/V2er/View/Login/LoginPage.swift +++ b/V2er/View/Login/LoginPage.swift @@ -32,7 +32,7 @@ struct LoginPage: StateView { dismiss() } } - .toast(isPresented: toast.isPresented, paddingTop: 40) { + .toast(isPresented: toast.isPresented, paddingTop: 40, version: toast.version.raw) { DefaultToastView(title: toast.title.raw, icon: toast.icon.raw) } .alert(isPresented: bindingState.showAlert) { diff --git a/V2er/View/Widget/Toast.swift b/V2er/View/Widget/Toast.swift index 8a2565f..ae68c00 100644 --- a/V2er/View/Widget/Toast.swift +++ b/V2er/View/Widget/Toast.swift @@ -21,6 +21,7 @@ final class Toast { var isPresented: Bool = false var title: String = "" var icon: String = "" + var version: Int = 0 // Incremented on each new toast to trigger timer reset static func show(_ title: String, icon: String = .empty, target: Reducer = .global) { guard title.notEmpty() || icon.notEmpty() else { return } @@ -71,6 +72,7 @@ struct DefaultToastView: View { private struct ToastContainerView: View { @Binding var isPresented: Bool let paddingTop: CGFloat + let version: Int let content: Content @State private var dismissTask: Task? @@ -81,7 +83,7 @@ private struct ToastContainerView: View { content .background(Color.secondaryBackground.opacity(0.98)) .cornerRadius(99) - .shadow(color: Color.primaryText.opacity(0.3), radius: 3) + .shadow(color: Color.primaryText.opacity(0.12), radius: 4, y: 2) .padding(.top, paddingTop) .transition(.move(edge: .top).combined(with: .opacity)) .zIndex(1) @@ -101,6 +103,11 @@ private struct ToastContainerView: View { scheduleDismiss() } } + .onChange(of: version) { _ in + // New toast content: reset timer + toastId = UUID() + scheduleDismiss() + } } private func scheduleDismiss() { @@ -133,6 +140,7 @@ private struct ToastContainerView: View { extension View { func toast(isPresented: Binding, paddingTop: CGFloat = 0, + version: Int = 0, @ViewBuilder content: () -> Content?) -> some View { ZStack(alignment: .top) { self @@ -140,6 +148,7 @@ extension View { ToastContainerView( isPresented: isPresented, paddingTop: paddingTop, + version: version, content: toastContent ) }