Head navigation supports adaptive themes
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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
75
src-tauri/src/inject/auth.js
vendored
Normal 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;
|
||||
73
src-tauri/src/inject/event.js
vendored
73
src-tauri/src/inject/event.js
vendored
@@ -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
113
src-tauri/src/inject/theme_refresh.js
vendored
Normal 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
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user