From 8a83389164bc364a220227a3df2d4bdd46ee1b66 Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Thu, 6 Mar 2025 14:14:23 +0800 Subject: [PATCH] fix: hide traffic lights when sidebar is invisible on macOS, closes #297 (#497) --- apps/readest-app/src-tauri/src/lib.rs | 4 ++ .../src-tauri/src/traffic_light.rs | 50 ++++++++++---- .../app/library/components/LibraryHeader.tsx | 23 ++++++- .../src/app/reader/components/HeaderBar.tsx | 24 ++++++- .../src/app/reader/components/HintInfo.tsx | 4 +- .../src/app/reader/components/SectionInfo.tsx | 4 +- .../app/reader/components/sidebar/Header.tsx | 5 +- apps/readest-app/src/hooks/useTrafficLight.ts | 47 ------------- .../src/store/trafficLightStore.ts | 69 +++++++++++++++++++ 9 files changed, 159 insertions(+), 71 deletions(-) delete mode 100644 apps/readest-app/src/hooks/useTrafficLight.ts create mode 100644 apps/readest-app/src/store/trafficLightStore.ts diff --git a/apps/readest-app/src-tauri/src/lib.rs b/apps/readest-app/src-tauri/src/lib.rs index bc1d5759..4db66df2 100644 --- a/apps/readest-app/src-tauri/src/lib.rs +++ b/apps/readest-app/src-tauri/src/lib.rs @@ -10,6 +10,8 @@ extern crate objc; mod menu; #[cfg(target_os = "macos")] mod traffic_light; +#[cfg(target_os = "macos")] +use traffic_light::set_traffic_lights; #[cfg(target_os = "macos")] use tauri::TitleBarStyle; @@ -98,6 +100,8 @@ pub fn run() { start_server, download_file, upload_file, + #[cfg(target_os = "macos")] + set_traffic_lights, #[cfg(desktop)] list_fonts ]) diff --git a/apps/readest-app/src-tauri/src/traffic_light.rs b/apps/readest-app/src-tauri/src/traffic_light.rs index 1a8ba28f..45355591 100644 --- a/apps/readest-app/src-tauri/src/traffic_light.rs +++ b/apps/readest-app/src-tauri/src/traffic_light.rs @@ -1,13 +1,14 @@ use objc::{msg_send, sel, sel_impl}; use rand::{distributions::Alphanumeric, Rng}; -use tauri::Emitter; use tauri::{ + command, plugin::{Builder, TauriPlugin}, - Runtime, Window, -}; // 0.8 + Emitter, Runtime, Window, +}; -const WINDOW_CONTROL_PAD_X: f64 = 10.0; -const WINDOW_CONTROL_PAD_Y: f64 = 22.0; +static mut WINDOW_CONTROL_PAD_X: f64 = 10.0; +static mut WINDOW_CONTROL_PAD_Y: f64 = 22.0; +static mut TRAFFIC_LIGHTS_VISIBLE: bool = true; struct UnsafeWindowHandle(*mut std::ffi::c_void); unsafe impl Send for UnsafeWindowHandle {} @@ -30,7 +31,24 @@ pub fn init() -> TauriPlugin { } #[cfg(target_os = "macos")] -fn position_traffic_lights(ns_window_handle: UnsafeWindowHandle, x: f64, y: f64) { +#[command] +pub fn set_traffic_lights(window: Window, visible: bool, x: f64, y: f64) { + unsafe { + TRAFFIC_LIGHTS_VISIBLE = visible; + WINDOW_CONTROL_PAD_X = x; + WINDOW_CONTROL_PAD_Y = y; + + position_traffic_lights( + UnsafeWindowHandle(window.ns_window().expect("Failed to create window handle")), + TRAFFIC_LIGHTS_VISIBLE, + WINDOW_CONTROL_PAD_X, + WINDOW_CONTROL_PAD_Y, + ); + } +} + +#[cfg(target_os = "macos")] +fn position_traffic_lights(ns_window_handle: UnsafeWindowHandle, visible: bool, x: f64, y: f64) { use cocoa::appkit::{NSView, NSWindow, NSWindowButton}; use cocoa::foundation::NSRect; let ns_window = ns_window_handle.0 as cocoa::base::id; @@ -45,7 +63,10 @@ fn position_traffic_lights(ns_window_handle: UnsafeWindowHandle, x: f64, y: f64) let close_rect: NSRect = msg_send![close, frame]; let button_height = close_rect.size.height; - let title_bar_frame_height = button_height + y; + let mut title_bar_frame_height = button_height + y; + if !visible { + title_bar_frame_height = 0.0; + } let mut title_bar_rect = NSView::frame(title_bar_container_view); title_bar_rect.size.height = title_bar_frame_height; title_bar_rect.origin.y = NSView::frame(ns_window).size.height - title_bar_frame_height; @@ -77,11 +98,14 @@ pub fn setup_traffic_light_positioner(window: Window) { use std::ffi::c_void; // Do the initial positioning - position_traffic_lights( - UnsafeWindowHandle(window.ns_window().expect("Failed to create window handle")), - WINDOW_CONTROL_PAD_X, - WINDOW_CONTROL_PAD_Y, - ); + unsafe { + position_traffic_lights( + UnsafeWindowHandle(window.ns_window().expect("Failed to create window handle")), + TRAFFIC_LIGHTS_VISIBLE, + WINDOW_CONTROL_PAD_X, + WINDOW_CONTROL_PAD_Y, + ); + } // Ensure they stay in place while resizing the window. fn with_window_state) -> T, T>( @@ -127,6 +151,7 @@ pub fn setup_traffic_light_positioner(window: Window) { #[cfg(target_os = "macos")] position_traffic_lights( UnsafeWindowHandle(id as *mut std::ffi::c_void), + TRAFFIC_LIGHTS_VISIBLE, WINDOW_CONTROL_PAD_X, WINDOW_CONTROL_PAD_Y, ); @@ -258,6 +283,7 @@ pub fn setup_traffic_light_positioner(window: Window) { let id = state.window.ns_window().expect("Failed to emit event") as id; position_traffic_lights( UnsafeWindowHandle(id as *mut std::ffi::c_void), + TRAFFIC_LIGHTS_VISIBLE, WINDOW_CONTROL_PAD_X, WINDOW_CONTROL_PAD_Y, ); diff --git a/apps/readest-app/src/app/library/components/LibraryHeader.tsx b/apps/readest-app/src/app/library/components/LibraryHeader.tsx index c8f5d220..803dcbed 100644 --- a/apps/readest-app/src/app/library/components/LibraryHeader.tsx +++ b/apps/readest-app/src/app/library/components/LibraryHeader.tsx @@ -1,5 +1,5 @@ import clsx from 'clsx'; -import React, { useRef } from 'react'; +import React, { useEffect, useRef } from 'react'; import { useRouter, useSearchParams } from 'next/navigation'; import { FaSearch } from 'react-icons/fa'; import { PiPlus } from 'react-icons/pi'; @@ -10,8 +10,8 @@ import { MdArrowBackIosNew } from 'react-icons/md'; import { useEnv } from '@/context/EnvContext'; import { useTranslation } from '@/hooks/useTranslation'; import { useResponsiveSize } from '@/hooks/useResponsiveSize'; +import { useTrafficLightStore } from '@/store/trafficLightStore'; import { navigateToLibrary } from '@/utils/nav'; -import useTrafficLight from '@/hooks/useTrafficLight'; import WindowButtons from '@/components/WindowButtons'; import Dropdown from '@/components/Dropdown'; import SettingsMenu from './SettingsMenu'; @@ -33,7 +33,13 @@ const LibraryHeader: React.FC = ({ const router = useRouter(); const searchParams = useSearchParams(); const { appService } = useEnv(); - const { isTrafficLightVisible } = useTrafficLight(); + const { + isTrafficLightVisible, + initializeTrafficLightStore, + initializeTrafficLightListeners, + cleanupTrafficLightListeners, + } = useTrafficLightStore(); + const headerRef = useRef(null); const iconSize16 = useResponsiveSize(16); const iconSize20 = useResponsiveSize(20); @@ -42,6 +48,17 @@ const LibraryHeader: React.FC = ({ onToggleSelectMode, }); + useEffect(() => { + if (!appService?.hasTrafficLight) return; + + initializeTrafficLightStore(appService); + initializeTrafficLightListeners(); + return () => { + cleanupTrafficLightListeners(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + const windowButtonVisible = appService?.hasWindowBar && !isTrafficLightVisible; const isInGroupView = !!searchParams?.get('group'); diff --git a/apps/readest-app/src/app/reader/components/HeaderBar.tsx b/apps/readest-app/src/app/reader/components/HeaderBar.tsx index a7c855ab..b701d677 100644 --- a/apps/readest-app/src/app/reader/components/HeaderBar.tsx +++ b/apps/readest-app/src/app/reader/components/HeaderBar.tsx @@ -1,12 +1,12 @@ import clsx from 'clsx'; -import React, { useRef, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { PiDotsThreeVerticalBold } from 'react-icons/pi'; import { useEnv } from '@/context/EnvContext'; import { useReaderStore } from '@/store/readerStore'; import { useSidebarStore } from '@/store/sidebarStore'; +import { useTrafficLightStore } from '@/store/trafficLightStore'; import { useResponsiveSize } from '@/hooks/useResponsiveSize'; -import useTrafficLight from '@/hooks/useTrafficLight'; import WindowButtons from '@/components/WindowButtons'; import Dropdown from '@/components/Dropdown'; import SidebarToggler from './SidebarToggler'; @@ -34,7 +34,13 @@ const HeaderBar: React.FC = ({ }) => { const { appService } = useEnv(); const headerRef = useRef(null); - const { isTrafficLightVisible } = useTrafficLight(); + const { + isTrafficLightVisible, + setTrafficLightVisibility, + initializeTrafficLightStore, + initializeTrafficLightListeners, + cleanupTrafficLightListeners, + } = useTrafficLightStore(); const [isDropdownOpen, setIsDropdownOpen] = useState(false); const { hoveredBookKey, setHoveredBookKey, bookKeys } = useReaderStore(); const { isSideBarVisible } = useSidebarStore(); @@ -45,6 +51,18 @@ const HeaderBar: React.FC = ({ if (!isOpen) setHoveredBookKey(''); }; + useEffect(() => { + if (!appService?.hasTrafficLight) return; + + initializeTrafficLightStore(appService); + initializeTrafficLightListeners(); + setTrafficLightVisibility(isSideBarVisible); + return () => { + cleanupTrafficLightListeners(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isSideBarVisible]); + return (
= ({ bookKey, gapRight }) => { const { isSideBarVisible } = useSidebarStore(); - const { isTrafficLightVisible } = useTrafficLight(); + const { isTrafficLightVisible } = useTrafficLightStore(); const [hintMessage, setHintMessage] = React.useState(null); const hintTimeout = useRef(2000); const dismissTimeout = useRef | null>(null); diff --git a/apps/readest-app/src/app/reader/components/SectionInfo.tsx b/apps/readest-app/src/app/reader/components/SectionInfo.tsx index b64c5739..3e8c0141 100644 --- a/apps/readest-app/src/app/reader/components/SectionInfo.tsx +++ b/apps/readest-app/src/app/reader/components/SectionInfo.tsx @@ -2,7 +2,7 @@ import clsx from 'clsx'; import React from 'react'; import { useSidebarStore } from '@/store/sidebarStore'; -import useTrafficLight from '@/hooks/useTrafficLight'; +import { useTrafficLightStore } from '@/store/trafficLightStore'; interface SectionInfoProps { section?: string; @@ -11,7 +11,7 @@ interface SectionInfoProps { const SectionInfo: React.FC = ({ section, gapLeft }) => { const { isSideBarVisible } = useSidebarStore(); - const { isTrafficLightVisible } = useTrafficLight(); + const { isTrafficLightVisible } = useTrafficLightStore(); return (
void; onToggleSearchBar: () => void; }> = ({ isPinned, isSearchBarVisible, onGoToLibrary, onClose, onTogglePin, onToggleSearchBar }) => { - const { isTrafficLightVisible } = useTrafficLight(); + const { isTrafficLightVisible } = useTrafficLightStore(); const iconSize14 = useResponsiveSize(14); const iconSize18 = useResponsiveSize(18); const iconSize22 = useResponsiveSize(22); + return (
{ - const { appService } = useEnv(); - const [isTrafficLightVisible, setVisible] = useState(appService?.hasTrafficLight ?? false); - let unlistenEnterFullScreen: () => void; - let unlistenExitFullScreen: () => void; - - const handleSwitchFullScreen = async () => { - const { getCurrentWindow } = await import('@tauri-apps/api/window'); - const currentWindow = getCurrentWindow(); - const isFullscreen = await currentWindow.isFullscreen(); - if (appService?.hasTrafficLight) setVisible(!isFullscreen); - - unlistenEnterFullScreen = await currentWindow.listen('will-enter-fullscreen', () => { - if (appService?.hasTrafficLight) setVisible(false); - }); - - unlistenExitFullScreen = await currentWindow.listen('will-exit-fullscreen', () => { - if (appService?.hasTrafficLight) setVisible(true); - }); - }; - - useEffect(() => { - if (!appService?.hasTrafficLight) return; - - handleSwitchFullScreen(); - - return () => { - unlistenEnterFullScreen?.(); - unlistenExitFullScreen?.(); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - return { isTrafficLightVisible }; -}; - -export default useTrafficLight; diff --git a/apps/readest-app/src/store/trafficLightStore.ts b/apps/readest-app/src/store/trafficLightStore.ts new file mode 100644 index 00000000..01bd6225 --- /dev/null +++ b/apps/readest-app/src/store/trafficLightStore.ts @@ -0,0 +1,69 @@ +import { create } from 'zustand'; +import { invoke } from '@tauri-apps/api/core'; +import { AppService } from '@/types/system'; + +const WINDOW_CONTROL_PAD_X = 10.0; +const WINDOW_CONTROL_PAD_Y = 22.0; + +interface TrafficLightState { + appService?: AppService; + isTrafficLightVisible: boolean; + shouldShowTrafficLight: boolean; + initializeTrafficLightStore: (appService: AppService) => void; + setTrafficLightVisibility: (visible: boolean) => void; + initializeTrafficLightListeners: () => Promise; + cleanupTrafficLightListeners: () => void; + unlistenEnterFullScreen?: () => void; + unlistenExitFullScreen?: () => void; +} + +export const useTrafficLightStore = create((set, get) => { + return { + appService: undefined, + isTrafficLightVisible: false, + shouldShowTrafficLight: false, + + initializeTrafficLightStore: (appService: AppService) => { + set({ + appService, + isTrafficLightVisible: appService.hasTrafficLight, + shouldShowTrafficLight: appService.hasTrafficLight, + }); + }, + + setTrafficLightVisibility: async (visible: boolean) => { + const { getCurrentWindow } = await import('@tauri-apps/api/window'); + const currentWindow = getCurrentWindow(); + const isFullscreen = await currentWindow.isFullscreen(); + set({ isTrafficLightVisible: !isFullscreen && visible, shouldShowTrafficLight: visible }); + invoke('set_traffic_lights', { + visible: visible, + x: WINDOW_CONTROL_PAD_X, + y: WINDOW_CONTROL_PAD_Y, + }); + }, + + initializeTrafficLightListeners: async () => { + const { getCurrentWindow } = await import('@tauri-apps/api/window'); + const currentWindow = getCurrentWindow(); + + const unlistenEnterFullScreen = await currentWindow.listen('will-enter-fullscreen', () => { + set({ isTrafficLightVisible: false }); + }); + + const unlistenExitFullScreen = await currentWindow.listen('will-exit-fullscreen', () => { + const { shouldShowTrafficLight } = get(); + set({ isTrafficLightVisible: shouldShowTrafficLight }); + }); + + set({ unlistenEnterFullScreen, unlistenExitFullScreen }); + }, + + cleanupTrafficLightListeners: () => { + const { unlistenEnterFullScreen, unlistenExitFullScreen } = get(); + if (unlistenEnterFullScreen) unlistenEnterFullScreen(); + if (unlistenExitFullScreen) unlistenExitFullScreen(); + set({ unlistenEnterFullScreen: undefined, unlistenExitFullScreen: undefined }); + }, + }; +});