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::{command, AppHandle, Manager, Url, WebviewWindow};
|
||||||
use tauri_plugin_http::reqwest::{ClientBuilder, Request};
|
use tauri_plugin_http::reqwest::{ClientBuilder, Request};
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
use tauri::Theme;
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
pub struct DownloadFileParams {
|
pub struct DownloadFileParams {
|
||||||
url: String,
|
url: String,
|
||||||
@@ -111,3 +114,17 @@ pub fn send_notification(app: AppHandle, params: NotificationParams) -> Result<(
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
Ok(())
|
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/component.js"))
|
||||||
.initialization_script(include_str!("../inject/event.js"))
|
.initialization_script(include_str!("../inject/event.js"))
|
||||||
.initialization_script(include_str!("../inject/style.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"));
|
.initialization_script(include_str!("../inject/custom.js"));
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[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);
|
let filename = anchorElement.download || getFilenameFromUrl(absoluteUrl);
|
||||||
|
|
||||||
// Early check: Allow OAuth/authentication links to navigate naturally
|
// Early check: Allow OAuth/authentication links to navigate naturally
|
||||||
if (isAuthLink(absoluteUrl)) {
|
if (window.isAuthLink(absoluteUrl)) {
|
||||||
console.log("[Pake] Allowing OAuth navigation to:", absoluteUrl);
|
console.log("[Pake] Allowing OAuth navigation to:", absoluteUrl);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -480,80 +480,11 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
collectUrlToBlobs();
|
collectUrlToBlobs();
|
||||||
detectDownloadByCreateAnchor();
|
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.
|
// Rewrite the window.open function.
|
||||||
const originalWindowOpen = window.open;
|
const originalWindowOpen = window.open;
|
||||||
window.open = function (url, name, specs) {
|
window.open = function (url, name, specs) {
|
||||||
// Allow authentication popups to open normally
|
// Allow authentication popups to open normally
|
||||||
if (isAuthPopup(url, name)) {
|
if (window.isAuthPopup(url, name)) {
|
||||||
return originalWindowOpen.call(window, url, name, specs);
|
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 std::time::Duration;
|
||||||
|
|
||||||
use app::{
|
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},
|
setup::{set_global_shortcut, set_system_tray},
|
||||||
window::set_window,
|
window::set_window,
|
||||||
};
|
};
|
||||||
@@ -60,6 +60,7 @@ pub fn run_app() {
|
|||||||
download_file,
|
download_file,
|
||||||
download_file_by_binary,
|
download_file_by_binary,
|
||||||
send_notification,
|
send_notification,
|
||||||
|
update_theme_mode,
|
||||||
])
|
])
|
||||||
.setup(move |app| {
|
.setup(move |app| {
|
||||||
let window = set_window(app, &pake_config, &tauri_config);
|
let window = set_window(app, &pake_config, &tauri_config);
|
||||||
|
|||||||
Reference in New Issue
Block a user