Get rid of private API on macos

This commit is contained in:
chrox
2024-11-11 02:23:06 +01:00
parent a059396008
commit 4e7dd9051b
12 changed files with 558 additions and 61 deletions
+67 -10
View File
@@ -668,6 +668,22 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "cocoa"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c"
dependencies = [
"bitflags 1.3.2",
"block",
"cocoa-foundation 0.1.2",
"core-foundation 0.9.4",
"core-graphics 0.23.2",
"foreign-types",
"libc",
"objc",
]
[[package]]
name = "cocoa"
version = "0.26.0"
@@ -676,14 +692,28 @@ checksum = "f79398230a6e2c08f5c9760610eb6924b52aa9e7950a619602baba59dcbbdbb2"
dependencies = [
"bitflags 2.6.0",
"block",
"cocoa-foundation",
"cocoa-foundation 0.2.0",
"core-foundation 0.10.0",
"core-graphics",
"core-graphics 0.24.0",
"foreign-types",
"libc",
"objc",
]
[[package]]
name = "cocoa-foundation"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7"
dependencies = [
"bitflags 1.3.2",
"block",
"core-foundation 0.9.4",
"core-graphics-types 0.1.3",
"libc",
"objc",
]
[[package]]
name = "cocoa-foundation"
version = "0.2.0"
@@ -693,7 +723,7 @@ dependencies = [
"bitflags 2.6.0",
"block",
"core-foundation 0.10.0",
"core-graphics-types",
"core-graphics-types 0.2.0",
"libc",
"objc",
]
@@ -787,6 +817,19 @@ version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "core-graphics"
version = "0.23.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081"
dependencies = [
"bitflags 1.3.2",
"core-foundation 0.9.4",
"core-graphics-types 0.1.3",
"foreign-types",
"libc",
]
[[package]]
name = "core-graphics"
version = "0.24.0"
@@ -795,11 +838,22 @@ checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1"
dependencies = [
"bitflags 2.6.0",
"core-foundation 0.10.0",
"core-graphics-types",
"core-graphics-types 0.2.0",
"foreign-types",
"libc",
]
[[package]]
name = "core-graphics-types"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf"
dependencies = [
"bitflags 1.3.2",
"core-foundation 0.9.4",
"libc",
]
[[package]]
name = "core-graphics-types"
version = "0.2.0"
@@ -3511,7 +3565,10 @@ checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
name = "readest"
version = "0.1.0"
dependencies = [
"cocoa 0.25.0",
"log",
"objc",
"rand 0.8.5",
"serde",
"serde_json",
"tauri",
@@ -4153,7 +4210,7 @@ checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08"
dependencies = [
"bytemuck",
"cfg_aliases 0.2.1",
"core-graphics",
"core-graphics 0.24.0",
"foreign-types",
"js-sys",
"log",
@@ -4359,9 +4416,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63f1f6b2017cc33d7f6fc9c6186a2c0f5dfc985899a7b4fe9e64985c17533db3"
dependencies = [
"bitflags 2.6.0",
"cocoa",
"cocoa 0.26.0",
"core-foundation 0.10.0",
"core-graphics",
"core-graphics 0.24.0",
"crossbeam-channel",
"dispatch",
"dlopen2",
@@ -4568,7 +4625,7 @@ checksum = "8e5cd17faa36a826e5686bd0fda5bc3f4c903682263f00cd50f2f778fc4bb866"
dependencies = [
"async-stream",
"bytes",
"cocoa",
"cocoa 0.26.0",
"colored",
"devtools-core",
"futures",
@@ -4656,7 +4713,7 @@ checksum = "9a49f2c05d15e6375ab7f7e528b3049150ba4dfafdf61f85e5178d0aef18e3f5"
dependencies = [
"android_logger",
"byte-unit",
"cocoa",
"cocoa 0.26.0",
"fern",
"log",
"objc",
@@ -5228,7 +5285,7 @@ version = "0.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c92af36a182b46206723bdf8a7942e20838cde1cf062e5b97854d57eb01763b"
dependencies = [
"core-graphics",
"core-graphics 0.24.0",
"crossbeam-channel",
"dirs",
"libappindicator",
+6 -1
View File
@@ -22,7 +22,7 @@ serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
log = "0.4"
# FIXME: remove the devtools feature in production
tauri = { version = "2.0.2", features = [ "macos-private-api", "protocol-asset", "devtools"] }
tauri = { version = "2.0.2", features = [ "protocol-asset", "devtools"] }
tauri-plugin-log = "2.0.1"
tauri-plugin-fs = "2.0.1"
tauri-plugin-dialog = "2.0.1"
@@ -33,3 +33,8 @@ tauri-plugin-devtools = "2.0.0"
[patch.crates-io]
tauri = { path = "../../../packages/tauri/crates/tauri" }
wry = { path = "../../../packages/wry" }
[target."cfg(target_os = \"macos\")".dependencies]
cocoa = "0.25"
objc = "0.2.7"
rand = "0.8"
+25
View File
@@ -1,3 +1,16 @@
#[cfg(target_os = "macos")]
#[macro_use]
extern crate cocoa;
#[cfg(target_os = "macos")]
#[macro_use]
extern crate objc;
#[cfg(target_os = "macos")]
mod tauri_traffic_light_positioner_plugin;
use tauri::{TitleBarStyle, WebviewUrl, WebviewWindowBuilder};
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
@@ -5,6 +18,7 @@ pub fn run() {
.plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_traffic_light_positioner_plugin::init())
.setup(|app| {
if cfg!(debug_assertions) {
app.handle().plugin(
@@ -13,6 +27,17 @@ pub fn run() {
.build(),
)?;
}
let win_builder = WebviewWindowBuilder::new(app, "main", WebviewUrl::default())
.title("")
.inner_size(800.0, 600.0)
.resizable(true)
.maximized(true);
#[cfg(target_os = "macos")]
let win_builder = win_builder.title_bar_style(TitleBarStyle::Overlay);
let _ = win_builder.build().unwrap();
Ok(())
})
.run(tauri::generate_context!())
@@ -0,0 +1,364 @@
use objc::{msg_send, sel, sel_impl};
use rand::{distributions::Alphanumeric, Rng};
use tauri::Emitter;
use tauri::{
plugin::{Builder, TauriPlugin},
Runtime, Window,
}; // 0.8
const WINDOW_CONTROL_PAD_X: f64 = 10.0;
const WINDOW_CONTROL_PAD_Y: f64 = 21.0;
struct UnsafeWindowHandle(*mut std::ffi::c_void);
unsafe impl Send for UnsafeWindowHandle {}
unsafe impl Sync for UnsafeWindowHandle {}
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("traffic_light_positioner")
.on_window_ready(|window| {
#[cfg(target_os = "macos")]
setup_traffic_light_positioner(window.clone());
let window_clone = window.clone();
window.on_window_event(move |event| {
let window = window_clone.clone();
if let tauri::WindowEvent::ThemeChanged(_theme) = event {
setup_traffic_light_positioner(window);
}
});
return;
})
.build()
}
#[cfg(target_os = "macos")]
fn position_traffic_lights(ns_window_handle: UnsafeWindowHandle, 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;
unsafe {
let close = ns_window.standardWindowButton_(NSWindowButton::NSWindowCloseButton);
let miniaturize =
ns_window.standardWindowButton_(NSWindowButton::NSWindowMiniaturizeButton);
let zoom = ns_window.standardWindowButton_(NSWindowButton::NSWindowZoomButton);
let title_bar_container_view = close.superview().superview();
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_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;
let _: () = msg_send![title_bar_container_view, setFrame: title_bar_rect];
let window_buttons = vec![close, miniaturize, zoom];
let space_between = NSView::frame(miniaturize).origin.x - NSView::frame(close).origin.x;
for (i, button) in window_buttons.into_iter().enumerate() {
let mut rect: NSRect = NSView::frame(button);
rect.origin.x = x + (i as f64 * space_between);
button.setFrameOrigin(rect.origin);
}
}
}
#[cfg(target_os = "macos")]
#[derive(Debug)]
struct WindowState<R: Runtime> {
window: Window<R>,
}
#[cfg(target_os = "macos")]
pub fn setup_traffic_light_positioner<R: Runtime>(window: Window<R>) {
use cocoa::appkit::NSWindow;
use cocoa::base::{id, BOOL};
use cocoa::foundation::NSUInteger;
use objc::runtime::{Object, Sel};
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,
);
// Ensure they stay in place while resizing the window.
fn with_window_state<R: Runtime, F: FnOnce(&mut WindowState<R>) -> T, T>(
this: &Object,
func: F,
) {
let ptr = unsafe {
let x: *mut c_void = *this.get_ivar("app_box");
&mut *(x as *mut WindowState<R>)
};
func(ptr);
}
unsafe {
let ns_win = window
.ns_window()
.expect("NS Window should exist to mount traffic light delegate.")
as id;
let current_delegate: id = ns_win.delegate();
extern "C" fn on_window_should_close(this: &Object, _cmd: Sel, sender: id) -> BOOL {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
msg_send![super_del, windowShouldClose: sender]
}
}
extern "C" fn on_window_will_close(this: &Object, _cmd: Sel, notification: id) {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, windowWillClose: notification];
}
}
extern "C" fn on_window_did_resize<R: Runtime>(this: &Object, _cmd: Sel, notification: id) {
unsafe {
with_window_state(&*this, |state: &mut WindowState<R>| {
let id = state
.window
.ns_window()
.expect("NS window should exist on state to handle resize")
as id;
#[cfg(target_os = "macos")]
position_traffic_lights(
UnsafeWindowHandle(id as *mut std::ffi::c_void),
WINDOW_CONTROL_PAD_X,
WINDOW_CONTROL_PAD_Y,
);
});
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, windowDidResize: notification];
}
}
extern "C" fn on_window_did_move(this: &Object, _cmd: Sel, notification: id) {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, windowDidMove: notification];
}
}
extern "C" fn on_window_did_change_backing_properties(
this: &Object,
_cmd: Sel,
notification: id,
) {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, windowDidChangeBackingProperties: notification];
}
}
extern "C" fn on_window_did_become_key(this: &Object, _cmd: Sel, notification: id) {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, windowDidBecomeKey: notification];
}
}
extern "C" fn on_window_did_resign_key(this: &Object, _cmd: Sel, notification: id) {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, windowDidResignKey: notification];
}
}
extern "C" fn on_dragging_entered(this: &Object, _cmd: Sel, notification: id) -> BOOL {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
msg_send![super_del, draggingEntered: notification]
}
}
extern "C" fn on_prepare_for_drag_operation(
this: &Object,
_cmd: Sel,
notification: id,
) -> BOOL {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
msg_send![super_del, prepareForDragOperation: notification]
}
}
extern "C" fn on_perform_drag_operation(this: &Object, _cmd: Sel, sender: id) -> BOOL {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
msg_send![super_del, performDragOperation: sender]
}
}
extern "C" fn on_conclude_drag_operation(this: &Object, _cmd: Sel, notification: id) {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, concludeDragOperation: notification];
}
}
extern "C" fn on_dragging_exited(this: &Object, _cmd: Sel, notification: id) {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, draggingExited: notification];
}
}
extern "C" fn on_window_will_use_full_screen_presentation_options(
this: &Object,
_cmd: Sel,
window: id,
proposed_options: NSUInteger,
) -> NSUInteger {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
msg_send![super_del, window: window willUseFullScreenPresentationOptions: proposed_options]
}
}
extern "C" fn on_window_did_enter_full_screen<R: Runtime>(
this: &Object,
_cmd: Sel,
notification: id,
) {
unsafe {
with_window_state(&*this, |state: &mut WindowState<R>| {
state
.window
.emit("did-enter-fullscreen", ())
.expect("Failed to emit event");
});
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, windowDidEnterFullScreen: notification];
}
}
extern "C" fn on_window_will_enter_full_screen<R: Runtime>(
this: &Object,
_cmd: Sel,
notification: id,
) {
unsafe {
with_window_state(&*this, |state: &mut WindowState<R>| {
state
.window
.emit("will-enter-fullscreen", ())
.expect("Failed to emit event");
});
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, windowWillEnterFullScreen: notification];
}
}
extern "C" fn on_window_did_exit_full_screen<R: Runtime>(
this: &Object,
_cmd: Sel,
notification: id,
) {
unsafe {
with_window_state(&*this, |state: &mut WindowState<R>| {
state
.window
.emit("did-exit-fullscreen", ())
.expect("Failed to emit event");
let id = state.window.ns_window().expect("Failed to emit event") as id;
position_traffic_lights(
UnsafeWindowHandle(id as *mut std::ffi::c_void),
WINDOW_CONTROL_PAD_X,
WINDOW_CONTROL_PAD_Y,
);
});
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, windowDidExitFullScreen: notification];
}
}
extern "C" fn on_window_will_exit_full_screen<R: Runtime>(
this: &Object,
_cmd: Sel,
notification: id,
) {
unsafe {
with_window_state(&*this, |state: &mut WindowState<R>| {
state
.window
.emit("will-exit-fullscreen", ())
.expect("Failed to emit event");
});
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, windowWillExitFullScreen: notification];
}
}
extern "C" fn on_window_did_fail_to_enter_full_screen(
this: &Object,
_cmd: Sel,
window: id,
) {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, windowDidFailToEnterFullScreen: window];
}
}
extern "C" fn on_effective_appearance_did_change(
this: &Object,
_cmd: Sel,
notification: id,
) {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, effectiveAppearanceDidChange: notification];
}
}
extern "C" fn on_effective_appearance_did_changed_on_main_thread(
this: &Object,
_cmd: Sel,
notification: id,
) {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![
super_del,
effectiveAppearanceDidChangedOnMainThread: notification
];
}
}
// Are we deallocing this properly ? (I miss safe Rust :( )
let window_label = window.label().to_string();
let app_state = WindowState { window };
let app_box = Box::into_raw(Box::new(app_state)) as *mut c_void;
let random_str: String = rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(20)
.map(char::from)
.collect();
// We need to ensure we have a unique delegate name, otherwise we will panic while trying to create a duplicate
// delegate with the same name.
let delegate_name = format!("windowDelegate_{}_{}", window_label, random_str);
ns_win.setDelegate_(delegate!(&delegate_name, {
window: id = ns_win,
app_box: *mut c_void = app_box,
toolbar: id = cocoa::base::nil,
super_delegate: id = current_delegate,
(windowShouldClose:) => on_window_should_close as extern fn(&Object, Sel, id) -> BOOL,
(windowWillClose:) => on_window_will_close as extern fn(&Object, Sel, id),
(windowDidResize:) => on_window_did_resize::<R> as extern fn(&Object, Sel, id),
(windowDidMove:) => on_window_did_move as extern fn(&Object, Sel, id),
(windowDidChangeBackingProperties:) => on_window_did_change_backing_properties as extern fn(&Object, Sel, id),
(windowDidBecomeKey:) => on_window_did_become_key as extern fn(&Object, Sel, id),
(windowDidResignKey:) => on_window_did_resign_key as extern fn(&Object, Sel, id),
(draggingEntered:) => on_dragging_entered as extern fn(&Object, Sel, id) -> BOOL,
(prepareForDragOperation:) => on_prepare_for_drag_operation as extern fn(&Object, Sel, id) -> BOOL,
(performDragOperation:) => on_perform_drag_operation as extern fn(&Object, Sel, id) -> BOOL,
(concludeDragOperation:) => on_conclude_drag_operation as extern fn(&Object, Sel, id),
(draggingExited:) => on_dragging_exited as extern fn(&Object, Sel, id),
(window:willUseFullScreenPresentationOptions:) => on_window_will_use_full_screen_presentation_options as extern fn(&Object, Sel, id, NSUInteger) -> NSUInteger,
(windowDidEnterFullScreen:) => on_window_did_enter_full_screen::<R> as extern fn(&Object, Sel, id),
(windowWillEnterFullScreen:) => on_window_will_enter_full_screen::<R> as extern fn(&Object, Sel, id),
(windowDidExitFullScreen:) => on_window_did_exit_full_screen::<R> as extern fn(&Object, Sel, id),
(windowWillExitFullScreen:) => on_window_will_exit_full_screen::<R> as extern fn(&Object, Sel, id),
(windowDidFailToEnterFullScreen:) => on_window_did_fail_to_enter_full_screen as extern fn(&Object, Sel, id),
(effectiveAppearanceDidChange:) => on_effective_appearance_did_change as extern fn(&Object, Sel, id),
(effectiveAppearanceDidChangedOnMainThread:) => on_effective_appearance_did_changed_on_main_thread as extern fn(&Object, Sel, id)
}))
}
}
+1 -14
View File
@@ -10,20 +10,7 @@
"beforeBuildCommand": "pnpm build"
},
"app": {
"macOSPrivateApi": true,
"windows": [
{
"title": "Readest",
"width": 800,
"height": 600,
"resizable": true,
"fullscreen": false,
"maximized": true,
"decorations": false,
"transparent": true,
"shadow": false
}
],
"windows": [],
"security": {
"csp": {
"default-src": "'self' 'unsafe-inline' blob: customprotocol: asset: http://asset.localhost ipc: http://ipc.localhost https://*.sentry.io https://*.posthog.com https://fonts.gstatic.com",
@@ -1,8 +1,11 @@
import clsx from 'clsx';
import React, { useEffect, useRef } from 'react';
import { FaSearch } from 'react-icons/fa';
import { PiPlus } from 'react-icons/pi';
import { PiSelectionAllDuotone } from 'react-icons/pi';
import { useEnv } from '@/context/EnvContext';
import useFullScreen from '@/hooks/useFullScreen';
import WindowButtons from '@/components/WindowButtons';
interface LibraryHeaderProps {
@@ -16,7 +19,10 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
onImportBooks,
onToggleSelectMode,
}) => {
const { appService } = useEnv();
const { isFullScreen } = useFullScreen();
const headerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.ctrlKey || e.shiftKey) {
@@ -45,10 +51,16 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
};
return (
<div ref={headerRef} className='titlebar fixed top-2 z-10 w-full bg-gray-100 px-6 py-4'>
<div
ref={headerRef}
className={clsx(
'titlebar fixed z-10 w-full bg-gray-100 py-2 pr-6',
isFullScreen ? 'pl-2' : 'pl-16',
)}
>
<div className='flex items-center justify-between'>
<div className='sm:w relative flex w-full items-center'>
<span className='absolute left-4 text-gray-500'>
<div className='sm:w relative flex w-full items-center pl-4'>
<span className='absolute left-8 text-gray-500'>
<FaSearch className='h-4 w-4' />
</span>
<input
@@ -81,12 +93,14 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
</button>
</div>
</div>
<WindowButtons
headerRef={headerRef}
onMinimize={handleMinimize}
onToggleMaximize={handleToggleMaximize}
onClose={handleClose}
/>
{!appService?.isNativeWindow && (
<WindowButtons
headerRef={headerRef}
onMinimize={handleMinimize}
onToggleMaximize={handleToggleMaximize}
onClose={handleClose}
/>
)}
</div>
</div>
);
@@ -72,6 +72,10 @@ const LibraryPage = () => {
setIsSelectMode(!isSelectMode);
};
if (!appService) {
return <Spinner loading />;
}
return (
<div className='rounded-window min-h-screen select-none bg-gray-100'>
<LibraryHeader
@@ -1,8 +1,10 @@
import clsx from 'clsx';
import React from 'react';
import { GiBookshelf } from 'react-icons/gi';
import { FiSearch } from 'react-icons/fi';
import { MdOutlineMenu, MdOutlinePushPin, MdPushPin } from 'react-icons/md';
import useFullScreen from '@/hooks/useFullScreen';
import Dropdown from '@/components/Dropdown';
import BookMenu from './BookMenu';
@@ -11,32 +13,40 @@ const SidebarHeader: React.FC<{
onGoToLibrary: () => void;
onOpenSplitView: () => void;
handleTogglePin: () => void;
}> = ({ isPinned, onGoToLibrary, onOpenSplitView, handleTogglePin }) => (
<div className='sidebar-header flex h-11 items-center justify-between pl-1.5 pr-3'>
<div className='flex items-center'>
<button className='btn btn-ghost h-8 min-h-8 w-8 p-0' onClick={onGoToLibrary}>
<GiBookshelf size={20} className='fill-base-content' />
</button>
}> = ({ isPinned, onGoToLibrary, onOpenSplitView, handleTogglePin }) => {
const { isFullScreen } = useFullScreen();
return (
<div
className={clsx(
'sidebar-header flex h-11 items-center justify-between pr-3',
isFullScreen ? 'pl-1.5' : 'pl-20',
)}
>
<div className='flex items-center'>
<button className='btn btn-ghost h-8 min-h-8 w-8 p-0' onClick={onGoToLibrary}>
<GiBookshelf size={20} className='fill-base-content' />
</button>
</div>
<div className='flex size-[50%] min-w-20 items-center justify-between'>
<button className='btn btn-ghost left-0 h-8 min-h-8 w-8 p-0'>
<FiSearch size={18} />
</button>
<Dropdown
className='dropdown-bottom flex justify-center'
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
toggleButton={<MdOutlineMenu size={20} />}
>
<BookMenu openSplitView={onOpenSplitView} />
</Dropdown>
<button
onClick={handleTogglePin}
className={`${isPinned ? 'bg-gray-300' : 'bg-base-300'} btn btn-ghost btn-circle right-0 h-6 min-h-6 w-6`}
>
{isPinned ? <MdPushPin size={14} /> : <MdOutlinePushPin size={14} />}
</button>
</div>
</div>
<div className='flex size-[50%] min-w-20 items-center justify-between'>
<button className='btn btn-ghost left-0 h-8 min-h-8 w-8 p-0'>
<FiSearch size={18} />
</button>
<Dropdown
className='dropdown-bottom flex justify-center'
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
toggleButton={<MdOutlineMenu size={20} />}
>
<BookMenu openSplitView={onOpenSplitView} />
</Dropdown>
<button
onClick={handleTogglePin}
className={`${isPinned ? 'bg-gray-300' : 'bg-base-300'} btn btn-ghost btn-circle right-0 h-6 min-h-6 w-6`}
>
{isPinned ? <MdPushPin size={14} /> : <MdOutlinePushPin size={14} />}
</button>
</div>
</div>
);
);
};
export default SidebarHeader;
@@ -0,0 +1,27 @@
import { useEffect, useState } from 'react';
const useFullScreen = () => {
const [isFullScreen, setIsFullScreen] = useState(false);
const handleSwitchFullScreen = async () => {
const { getCurrentWindow } = await import('@tauri-apps/api/window');
const currentWindow = getCurrentWindow();
currentWindow.listen('will-enter-fullscreen', () => {
console.log('Window entered fullscreen');
setIsFullScreen(true);
});
currentWindow.listen('will-exit-fullscreen', () => {
console.log('Window exited fullscreen');
setIsFullScreen(false);
});
};
useEffect(() => {
handleSwitchFullScreen();
}, []);
return { isFullScreen };
};
export default useFullScreen;
@@ -26,6 +26,7 @@ import {
export abstract class BaseAppService implements AppService {
localBooksDir: string = '';
abstract isAppDataSandbox: boolean;
abstract isNativeWindow: boolean;
abstract fs: FileSystem;
abstract resolvePath(fp: string, base: BaseDir): { baseDir: number; base: BaseDir; fp: string };
@@ -115,6 +115,7 @@ export const nativeFileSystem: FileSystem = {
export class NativeAppService extends BaseAppService {
fs = nativeFileSystem;
isAppDataSandbox = isMobile;
isNativeWindow = osType() === 'macos';
override resolvePath(fp: string, base: BaseDir): { baseDir: number; base: BaseDir; fp: string } {
return resolvePath(fp, base);
+2
View File
@@ -18,6 +18,8 @@ export interface FileSystem {
export interface AppService {
fs: FileSystem;
isNativeWindow: boolean;
isAppDataSandbox: boolean;
selectDirectory(title: string): Promise<string>;
selectFiles(name: string, extensions: string[]): Promise<string[]>;