diff --git a/src-tauri/src/app/invoke.rs b/src-tauri/src/app/invoke.rs index b0815c0..0ce25de 100644 --- a/src-tauri/src/app/invoke.rs +++ b/src-tauri/src/app/invoke.rs @@ -6,6 +6,9 @@ use tauri::http::Method; use tauri::{command, AppHandle, Manager, Url, WebviewWindow}; use tauri_plugin_http::reqwest::{ClientBuilder, Request}; +#[cfg(target_os = "macos")] +use tauri::Theme; + #[derive(serde::Deserialize)] pub struct DownloadFileParams { url: String, @@ -111,3 +114,17 @@ pub fn send_notification(app: AppHandle, params: NotificationParams) -> Result<( .unwrap(); Ok(()) } + +#[command] +pub fn update_theme_mode(app: AppHandle, mode: String) { + let window = app.get_webview_window("pake").unwrap(); + #[cfg(target_os = "macos")] + { + let theme = if mode == "dark" { + Theme::Dark + } else { + Theme::Light + }; + let _ = window.set_theme(Some(theme)); + } +} \ No newline at end of file diff --git a/src-tauri/src/app/window.rs b/src-tauri/src/app/window.rs index b621d84..8fadfb2 100644 --- a/src-tauri/src/app/window.rs +++ b/src-tauri/src/app/window.rs @@ -88,6 +88,8 @@ pub fn set_window(app: &mut App, config: &PakeConfig, tauri_config: &Config) -> .initialization_script(include_str!("../inject/component.js")) .initialization_script(include_str!("../inject/event.js")) .initialization_script(include_str!("../inject/style.js")) + .initialization_script(include_str!("../inject/theme_refresh.js")) + .initialization_script(include_str!("../inject/auth.js")) .initialization_script(include_str!("../inject/custom.js")); #[cfg(target_os = "windows")] diff --git a/src-tauri/src/inject/auth.js b/src-tauri/src/inject/auth.js new file mode 100644 index 0000000..f48caa2 --- /dev/null +++ b/src-tauri/src/inject/auth.js @@ -0,0 +1,75 @@ +// OAuth and Authentication Logic + +// Check if URL matches OAuth/authentication patterns +function matchesAuthUrl(url, baseUrl = window.location.href) { + try { + const urlObj = new URL(url, baseUrl); + const hostname = urlObj.hostname.toLowerCase(); + const pathname = urlObj.pathname.toLowerCase(); + const fullUrl = urlObj.href.toLowerCase(); + + // Common OAuth providers and paths + const oauthPatterns = [ + /accounts\.google\.com/, + /accounts\.google\.[a-z]+/, + /login\.microsoftonline\.com/, + /github\.com\/login/, + /facebook\.com\/.*\/dialog/, + /twitter\.com\/oauth/, + /appleid\.apple\.com/, + /\/oauth\//, + /\/auth\//, + /\/authorize/, + /\/login\/oauth/, + /\/signin/, + /\/login/, + /servicelogin/, + /\/o\/oauth2/, + ]; + + const isMatch = oauthPatterns.some( + (pattern) => + pattern.test(hostname) || + pattern.test(pathname) || + pattern.test(fullUrl), + ); + + if (isMatch) { + console.log("[Pake] OAuth URL detected:", url); + } + + return isMatch; + } catch (e) { + return false; + } +} + +// Check if URL is an OAuth/authentication link +function isAuthLink(url) { + return matchesAuthUrl(url); +} + +// Check if this is an OAuth/authentication popup +function isAuthPopup(url, name) { + // Check for known authentication window names + const authWindowNames = [ + "AppleAuthentication", + "oauth2", + "oauth", + "google-auth", + "auth-popup", + "signin", + "login", + ]; + + if (authWindowNames.includes(name)) { + return true; + } + + return matchesAuthUrl(url); +} + +// Export functions to global scope +window.matchesAuthUrl = matchesAuthUrl; +window.isAuthLink = isAuthLink; +window.isAuthPopup = isAuthPopup; diff --git a/src-tauri/src/inject/event.js b/src-tauri/src/inject/event.js index ca9a198..2da0012 100644 --- a/src-tauri/src/inject/event.js +++ b/src-tauri/src/inject/event.js @@ -395,7 +395,7 @@ document.addEventListener("DOMContentLoaded", () => { let filename = anchorElement.download || getFilenameFromUrl(absoluteUrl); // Early check: Allow OAuth/authentication links to navigate naturally - if (isAuthLink(absoluteUrl)) { + if (window.isAuthLink(absoluteUrl)) { console.log("[Pake] Allowing OAuth navigation to:", absoluteUrl); return; } @@ -480,80 +480,11 @@ document.addEventListener("DOMContentLoaded", () => { collectUrlToBlobs(); detectDownloadByCreateAnchor(); - // Check if URL matches OAuth/authentication patterns - function matchesAuthUrl(url, baseUrl = window.location.href) { - try { - const urlObj = new URL(url, baseUrl); - const hostname = urlObj.hostname.toLowerCase(); - const pathname = urlObj.pathname.toLowerCase(); - const fullUrl = urlObj.href.toLowerCase(); - - // Common OAuth providers and paths - const oauthPatterns = [ - /accounts\.google\.com/, - /accounts\.google\.[a-z]+/, - /login\.microsoftonline\.com/, - /github\.com\/login/, - /facebook\.com\/.*\/dialog/, - /twitter\.com\/oauth/, - /appleid\.apple\.com/, - /\/oauth\//, - /\/auth\//, - /\/authorize/, - /\/login\/oauth/, - /\/signin/, - /\/login/, - /servicelogin/, - /\/o\/oauth2/, - ]; - - const isMatch = oauthPatterns.some( - (pattern) => - pattern.test(hostname) || - pattern.test(pathname) || - pattern.test(fullUrl), - ); - - if (isMatch) { - console.log("[Pake] OAuth URL detected:", url); - } - - return isMatch; - } catch (e) { - return false; - } - } - - // Check if URL is an OAuth/authentication link - function isAuthLink(url) { - return matchesAuthUrl(url); - } - - // Check if this is an OAuth/authentication popup - function isAuthPopup(url, name) { - // Check for known authentication window names - const authWindowNames = [ - "AppleAuthentication", - "oauth2", - "oauth", - "google-auth", - "auth-popup", - "signin", - "login", - ]; - - if (authWindowNames.includes(name)) { - return true; - } - - return matchesAuthUrl(url); - } - // Rewrite the window.open function. const originalWindowOpen = window.open; window.open = function (url, name, specs) { // Allow authentication popups to open normally - if (isAuthPopup(url, name)) { + if (window.isAuthPopup(url, name)) { return originalWindowOpen.call(window, url, name, specs); } diff --git a/src-tauri/src/inject/theme_refresh.js b/src-tauri/src/inject/theme_refresh.js new file mode 100644 index 0000000..041ad28 --- /dev/null +++ b/src-tauri/src/inject/theme_refresh.js @@ -0,0 +1,113 @@ +document.addEventListener("DOMContentLoaded", () => { + // Helper: Calculate brightness from RGB color + const isDarkColor = (color) => { + if (!color) return false; + const match = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); + if (!match) return false; + const r = parseInt(match[1]); + const g = parseInt(match[2]); + const b = parseInt(match[3]); + // Standard luminance formula + const luminance = 0.299 * r + 0.587 * g + 0.114 * b; + return luminance < 128; + }; + + // Debounce helper + const debounce = (func, wait) => { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; + }; + + // Function to detect and send theme to Rust + const updateTheme = () => { + let mode = "light"; + let detected = false; + + // Strategy 1: Explicit DOM Class/Attribute (High Priority) + // Many apps use specific classes for hard-coded themes + const doc = document.documentElement; + const body = document.body; + + const isExplicitDark = + doc.classList.contains("dark") || + body.classList.contains("dark") || + doc.getAttribute("data-theme") === "dark" || + body.getAttribute("data-theme") === "dark" || + doc.style.colorScheme === "dark"; + + const isExplicitLight = + doc.classList.contains("light") || + body.classList.contains("light") || + doc.getAttribute("data-theme") === "light" || + body.getAttribute("data-theme") === "light" || + doc.style.colorScheme === "light"; + + if (isExplicitDark) { + mode = "dark"; + detected = true; + } else if (isExplicitLight) { + mode = "light"; + detected = true; + } + + // Strategy 2: Computed Background Color (Fallback & Verification) + // If no explicit class is found, or to double-check, look at the actual background color. + // This is useful when the site relies purely on CSS media queries without classes. + if (!detected) { + const bodyBg = window.getComputedStyle(document.body).backgroundColor; + const htmlBg = window.getComputedStyle(document.documentElement).backgroundColor; + + // Check body first, then html + if (bodyBg && bodyBg !== "rgba(0, 0, 0, 0)" && bodyBg !== "transparent") { + mode = isDarkColor(bodyBg) ? "dark" : "light"; + } else if (htmlBg && htmlBg !== "rgba(0, 0, 0, 0)" && htmlBg !== "transparent") { + mode = isDarkColor(htmlBg) ? "dark" : "light"; + } else { + // Strategy 3: System Preference (Last Resort) + if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) { + mode = "dark"; + } + } + } + + // Send to Rust + if (window.__TAURI__ && window.__TAURI__.core) { + window.__TAURI__.core.invoke("update_theme_mode", { mode }); + } + }; + + // Debounced version of updateTheme + const debouncedUpdateTheme = debounce(updateTheme, 200); + + // Initial check + // Delay slightly to ensure styles are applied + setTimeout(updateTheme, 100); + + // Watch for system theme changes + window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", updateTheme); + + // Watch for DOM changes + // We observe attributes for class changes, and also style changes just in case + const observer = new MutationObserver((mutations) => { + debouncedUpdateTheme(); + }); + + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ["class", "data-theme", "style"], + subtree: false + }); + + observer.observe(document.body, { + attributes: true, + attributeFilter: ["class", "data-theme", "style"], + subtree: false + }); +}); diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 9895142..1eb1a78 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -10,7 +10,7 @@ use tauri_plugin_window_state::StateFlags; use std::time::Duration; use app::{ - invoke::{download_file, download_file_by_binary, send_notification}, + invoke::{download_file, download_file_by_binary, send_notification, update_theme_mode}, setup::{set_global_shortcut, set_system_tray}, window::set_window, }; @@ -60,6 +60,7 @@ pub fn run_app() { download_file, download_file_by_binary, send_notification, + update_theme_mode, ]) .setup(move |app| { let window = set_window(app, &pake_config, &tauri_config);