mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-08-15 08:02:08 +02:00
Follow the OS appearance when set to System without a new window (#540)
This commit is contained in:
@@ -1,9 +1,15 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import type { Appearance } from "@yaakapp-internal/theme";
|
||||
import { getCSSAppearance, subscribeToPreferredAppearance } from "@yaakapp-internal/theme";
|
||||
import {
|
||||
getCSSAppearance,
|
||||
getSystemAppearance,
|
||||
subscribeToPreferredAppearance,
|
||||
} from "@yaakapp-internal/theme";
|
||||
|
||||
export function usePreferredAppearance() {
|
||||
const [preferredAppearance, setPreferredAppearance] = useState<Appearance>(getCSSAppearance());
|
||||
const [preferredAppearance, setPreferredAppearance] = useState<Appearance>(
|
||||
getSystemAppearance() ?? getCSSAppearance(),
|
||||
);
|
||||
useEffect(() => subscribeToPreferredAppearance(setPreferredAppearance), []);
|
||||
return preferredAppearance;
|
||||
}
|
||||
|
||||
+17
-28
@@ -5,30 +5,34 @@ import type { Appearance } from "@yaakapp-internal/theme";
|
||||
import {
|
||||
applyThemeToDocument,
|
||||
getCSSAppearance,
|
||||
getSystemAppearance,
|
||||
getWindowAppearance,
|
||||
subscribeToPreferredAppearanceChange,
|
||||
subscribeToSystemAppearanceChange,
|
||||
} from "@yaakapp-internal/theme";
|
||||
import { getSettings } from "./lib/settings";
|
||||
import { getResolvedTheme } from "./lib/themes";
|
||||
|
||||
// NOTE: CSS appearance isn't as accurate as getting it async from the window (next step), but we want
|
||||
// a good appearance guess so we're not waiting too long
|
||||
let preferredAppearance: Appearance = getInitialAppearance();
|
||||
let linuxSystemAppearanceAvailable =
|
||||
platform.osType() === "linux" && window.__YAAK_INITIAL_APPEARANCE_SOURCE__ === "linux-system";
|
||||
// NOTE: The appearance the OS prefers (never the one the settings force). The backend
|
||||
// injects it on macOS and Linux; the CSS guess is only a fallback until the async
|
||||
// window value arrives below.
|
||||
let preferredAppearance: Appearance = getSystemAppearance() ?? getCSSAppearance();
|
||||
let configureThemeGeneration = 0;
|
||||
let windowShown = false;
|
||||
|
||||
configureThemeAndShow().catch((err) => console.log("Failed to configure theme", err));
|
||||
|
||||
subscribeToPreferredAppearanceChange(async (a) => {
|
||||
if (linuxSystemAppearanceAvailable) return;
|
||||
preferredAppearance = a;
|
||||
await configureThemeAndShow();
|
||||
});
|
||||
if (getSystemAppearance() == null) {
|
||||
// The initial appearance is only a guess, so confirm it with the window once it's available
|
||||
getWindowAppearance()
|
||||
.then(async (a) => {
|
||||
if (a === preferredAppearance) return;
|
||||
preferredAppearance = a;
|
||||
await configureThemeAndShow();
|
||||
})
|
||||
.catch((err) => console.log("Failed to get window appearance", err));
|
||||
}
|
||||
|
||||
subscribeToSystemAppearanceChange(async (a) => {
|
||||
linuxSystemAppearanceAvailable = true;
|
||||
subscribeToPreferredAppearanceChange(async (a) => {
|
||||
preferredAppearance = a;
|
||||
await configureThemeAndShow();
|
||||
});
|
||||
@@ -75,18 +79,3 @@ async function configureTheme(): Promise<boolean> {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function getInitialAppearance(): Appearance {
|
||||
const initialAppearance = window.__YAAK_INITIAL_APPEARANCE__;
|
||||
if (initialAppearance === "dark" || initialAppearance === "light") {
|
||||
return initialAppearance;
|
||||
}
|
||||
return getCSSAppearance();
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__YAAK_INITIAL_APPEARANCE__?: Appearance;
|
||||
__YAAK_INITIAL_APPEARANCE_SOURCE__?: "settings" | "linux-system";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,19 +160,14 @@ fn setup_window_menu<R: Runtime>(win: &WebviewWindow<R>) -> Result<()> {
|
||||
}
|
||||
|
||||
fn initial_appearance_script<R: Runtime>(app_handle: &AppHandle<R>) -> Option<String> {
|
||||
use yaak_system_appearance::{Appearance, InitialAppearanceSource};
|
||||
|
||||
let settings = app_handle.db().get_settings();
|
||||
let (appearance, source) = match settings.appearance.as_str() {
|
||||
"dark" => (Appearance::Dark, InitialAppearanceSource::Settings),
|
||||
"light" => (Appearance::Light, InitialAppearanceSource::Settings),
|
||||
_ => (
|
||||
yaak_system_appearance::system_appearance()?,
|
||||
InitialAppearanceSource::LinuxSystem,
|
||||
),
|
||||
};
|
||||
|
||||
Some(yaak_system_appearance::initialization_script(appearance, source))
|
||||
// Only report the appearance the OS prefers. The frontend needs it to resolve the
|
||||
// "automatic" setting, so the configured appearance is never a substitute for it.
|
||||
//
|
||||
// NOTE: The value comes from the watcher state, not a fresh detection, so the frontend
|
||||
// only ever sees a snapshot when the change events that keep it fresh are also flowing.
|
||||
let state = app_handle.try_state::<yaak_system_appearance::SystemAppearanceState>()?;
|
||||
let appearance = state.last_appearance()?;
|
||||
Some(yaak_system_appearance::initialization_script(appearance))
|
||||
}
|
||||
|
||||
/// Extension trait for easily creating a PluginContext from a WebviewWindow
|
||||
@@ -1783,7 +1778,7 @@ pub fn run() {
|
||||
app.state::<yaak_models::query_manager::QueryManager>().inner().clone();
|
||||
let app_id = app.config().identifier.to_string();
|
||||
app.manage(yaak_crypto::manager::EncryptionManager::new(query_manager, app_id));
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
if let Some(state) = yaak_system_appearance::watch(app.app_handle().clone()) {
|
||||
app.manage(state);
|
||||
}
|
||||
@@ -2002,7 +1997,7 @@ pub fn run() {
|
||||
});
|
||||
}
|
||||
RunEvent::WindowEvent { event: WindowEvent::Focused(true), label, .. } => {
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
if let Some(state) =
|
||||
app_handle.try_state::<yaak_system_appearance::SystemAppearanceState>()
|
||||
{
|
||||
|
||||
@@ -4,7 +4,7 @@ version = "0.1.0"
|
||||
edition = "2024"
|
||||
publish = false
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
[target.'cfg(any(target_os = "linux", target_os = "macos"))'.dependencies]
|
||||
dark-light = "2.0.0"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
use std::time::Duration;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
use log::{debug, warn};
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
use tauri::Emitter;
|
||||
use tauri::{AppHandle, Runtime};
|
||||
|
||||
pub const INITIAL_APPEARANCE_GLOBAL: &str = "__YAAK_INITIAL_APPEARANCE__";
|
||||
pub const INITIAL_APPEARANCE_SOURCE_GLOBAL: &str = "__YAAK_INITIAL_APPEARANCE_SOURCE__";
|
||||
pub const SYSTEM_APPEARANCE_CHANGE_EVENT: &str = "system_appearance_change";
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
const SYSTEM_APPEARANCE_POLL_INTERVAL: Duration = Duration::from_secs(1);
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
@@ -30,64 +29,53 @@ impl Appearance {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum InitialAppearanceSource {
|
||||
Settings,
|
||||
LinuxSystem,
|
||||
}
|
||||
|
||||
impl InitialAppearanceSource {
|
||||
fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Settings => "settings",
|
||||
Self::LinuxSystem => "linux-system",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SystemAppearanceState {
|
||||
// Only read by the Linux polling thread
|
||||
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
|
||||
last_appearance: Arc<Mutex<Option<Appearance>>>,
|
||||
}
|
||||
|
||||
pub fn initialization_script(appearance: Appearance, source: InitialAppearanceSource) -> String {
|
||||
let appearance = appearance.as_str();
|
||||
let source = source.as_str();
|
||||
format!(
|
||||
"window.{INITIAL_APPEARANCE_GLOBAL} = {appearance:?};\
|
||||
window.{INITIAL_APPEARANCE_SOURCE_GLOBAL} = {source:?};"
|
||||
)
|
||||
impl SystemAppearanceState {
|
||||
pub fn last_appearance(&self) -> Option<Appearance> {
|
||||
*self.last_appearance.lock().expect("system appearance lock poisoned")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn initialization_script(appearance: Appearance) -> String {
|
||||
let appearance = appearance.as_str();
|
||||
format!("window.{INITIAL_APPEARANCE_GLOBAL} = {appearance:?};")
|
||||
}
|
||||
|
||||
/// Detect the appearance the OS prefers, independent of any appearance that has
|
||||
/// been forced onto app windows (which is what the webview itself reports).
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
pub fn system_appearance() -> Option<Appearance> {
|
||||
#[cfg(target_os = "linux")]
|
||||
if let Some(appearance) = gsettings_system_appearance() {
|
||||
return Some(appearance);
|
||||
}
|
||||
|
||||
// On macOS this reads AppleInterfaceStyle from the global user defaults
|
||||
match dark_light::detect() {
|
||||
Ok(dark_light::Mode::Dark) => Some(Appearance::Dark),
|
||||
Ok(dark_light::Mode::Light) => Some(Appearance::Light),
|
||||
Ok(dark_light::Mode::Unspecified) => None,
|
||||
Err(err) => {
|
||||
debug!("Failed to detect Linux system appearance: {err:?}");
|
||||
debug!("Failed to detect system appearance: {err:?}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
|
||||
pub fn system_appearance() -> Option<Appearance> {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
pub fn watch<R: Runtime>(app_handle: AppHandle<R>) -> Option<SystemAppearanceState> {
|
||||
let last_appearance = system_appearance();
|
||||
if last_appearance.is_none() {
|
||||
debug!("Linux system appearance detection unavailable");
|
||||
debug!("System appearance detection unavailable");
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -103,12 +91,12 @@ pub fn watch<R: Runtime>(app_handle: AppHandle<R>) -> Option<SystemAppearanceSta
|
||||
Some(state)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
|
||||
pub fn watch<R: Runtime>(_app_handle: AppHandle<R>) -> Option<SystemAppearanceState> {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
pub fn emit_change<R: Runtime>(app_handle: &AppHandle<R>, state: &SystemAppearanceState) {
|
||||
let appearance = system_appearance();
|
||||
let mut last_appearance =
|
||||
|
||||
@@ -4,6 +4,21 @@ export type Appearance = "light" | "dark";
|
||||
|
||||
const SYSTEM_APPEARANCE_CHANGE_EVENT = "system_appearance_change";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__YAAK_INITIAL_APPEARANCE__?: Appearance;
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: On macOS and Linux the backend detects the appearance the OS prefers and injects
|
||||
// it here, later emitting change events. This is required because applying a theme forces
|
||||
// the window appearance, which poisons what the webview reports (CSS media queries and
|
||||
// window theme events follow the override).
|
||||
export function getSystemAppearance(): Appearance | null {
|
||||
const appearance = window.__YAAK_INITIAL_APPEARANCE__;
|
||||
return appearance === "dark" || appearance === "light" ? appearance : null;
|
||||
}
|
||||
|
||||
export function getCSSAppearance(): Appearance {
|
||||
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||||
}
|
||||
@@ -41,12 +56,24 @@ export function resolveAppearance(
|
||||
}
|
||||
|
||||
export function subscribeToPreferredAppearance(cb: (appearance: Appearance) => void) {
|
||||
const systemAppearance = getSystemAppearance();
|
||||
if (systemAppearance != null) {
|
||||
cb(systemAppearance);
|
||||
return subscribeToSystemAppearanceChange(cb);
|
||||
}
|
||||
|
||||
cb(getCSSAppearance());
|
||||
void getWindowAppearance().then(cb);
|
||||
return subscribeToPreferredAppearanceChange(cb);
|
||||
}
|
||||
|
||||
export function subscribeToPreferredAppearanceChange(cb: (appearance: Appearance) => void) {
|
||||
if (getSystemAppearance() != null) {
|
||||
// The CSS and window sources are poisoned by forced window appearances, so only
|
||||
// listen to the backend's system appearance detection when it's available
|
||||
return subscribeToSystemAppearanceChange(cb);
|
||||
}
|
||||
|
||||
const unsubscribeCSS = subscribeToCSSAppearanceChange(cb);
|
||||
const unsubscribeWindow = subscribeToWindowAppearanceChange(cb);
|
||||
const unsubscribeSystem = subscribeToSystemAppearanceChange(cb);
|
||||
|
||||
@@ -2,6 +2,7 @@ export type { Appearance } from "./appearance";
|
||||
export {
|
||||
subscribeToCSSAppearanceChange,
|
||||
getCSSAppearance,
|
||||
getSystemAppearance,
|
||||
getWindowAppearance,
|
||||
resolveAppearance,
|
||||
subscribeToPreferredAppearance,
|
||||
|
||||
Reference in New Issue
Block a user