Head navigation supports adaptive themes

This commit is contained in:
Tw93
2025-12-08 13:42:35 +08:00
parent 9d7ce96f0c
commit ba21525af3
6 changed files with 211 additions and 72 deletions

View File

@@ -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));
}
}

View File

@@ -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")]

75
src-tauri/src/inject/auth.js vendored Normal file
View File

@@ -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;

View File

@@ -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);
}

113
src-tauri/src/inject/theme_refresh.js vendored Normal file
View File

@@ -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
});
});

View File

@@ -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);