a1279a65ce
Builds the URL-clipping path of the "Send to Readest" feature: paste a
link, the renderer ingests the rendered page, and a self-contained EPUB
lands in the library. No server proxy, no external CDN refs left in the
EPUB once it's saved.
Architecture
- New Rust `clip_url` command spawns a hidden Tauri WebviewWindow at the
target URL with a real Chrome UA + WebKit fingerprint mask, so TLS-
fingerprint and JS-challenge walls (Cloudflare, Medium, X, WeChat MP)
resolve naturally instead of bouncing the server proxy.
- Capture transport is URL-payload navigation to a one-shot
127.0.0.1:RANDOM_PORT/clip/{token}?d={url-safe-base64} listener.
Top-level navigation isn't governed by CSP connect-src / form-action /
WebKit Private Network Access — the four earlier transports
(fetch, <form>, custom URI scheme, window.name) were each blocked by
one of those.
- Page-to-EPUB bundler (`assetBundler`) walks <img>/<picture> with
src → data-src → data-original → data-srcset → srcset fallback so lazy-
loading sites don't ship a 60px LQIP; fetches assets in parallel with a
per-asset timeout + per-asset/total caps; failed images degrade to alt-
text placeholders. A per-site rules table (seeded with WeChat MP) + a
selector fallback catches articles Readability misextracts. Builder
prepends the article <h1> + byline so the EPUB has a proper opening.
- Nested EPUB TOC built from h1–h6.
UI surfaces
- "From Web URL" entry in the library Import menu, gated to Tauri; web
build hides the URL field and points at the browser extension.
- `ImportFromUrlDialog` with auto-height (overrides Dialog's `sm:h-[65%]`
default) and a dim placeholder for the URL field.
- Clip webview window styled to match Readest's main window — macOS
decorations + overlay title bar; other desktops decorationless with a
drop shadow; native background + in-page loading overlay pick up the
caller's `themeCode.bg`/`fg` so light/dark/eink/custom themes all
render correctly. Title localised, all five overlay/title strings
translated across 33 locales.
Notes
- Gates the macOS traffic-light positioner to main/reader-* windows so
the decorationless clip window no longer null-derefs in
`position_traffic_lights`.
- Stricter validation across the path: schemes restricted to http/https,
hex-color parsing rejects malformed values, server endpoint returns
400 on missing/invalid base64.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
402 lines
16 KiB
Rust
402 lines
16 KiB
Rust
use objc::{msg_send, sel, sel_impl};
|
|
use rand::{distributions::Alphanumeric, Rng};
|
|
use tauri::{
|
|
command,
|
|
plugin::{Builder, TauriPlugin},
|
|
Emitter, Runtime, Window,
|
|
};
|
|
|
|
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 {}
|
|
unsafe impl Sync for UnsafeWindowHandle {}
|
|
|
|
pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
|
Builder::new("traffic_light")
|
|
.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);
|
|
}
|
|
});
|
|
})
|
|
.build()
|
|
}
|
|
|
|
#[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,
|
|
);
|
|
}
|
|
}
|
|
|
|
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;
|
|
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 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;
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct WindowState<R: Runtime> {
|
|
window: Window<R>,
|
|
}
|
|
|
|
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;
|
|
|
|
// The on_window_did_resize handler below already gates positioning to
|
|
// the main/reader windows by label — extending the same gate to the
|
|
// initial positioning. Other windows (the clip-* webview, future
|
|
// decorationless utility windows) have no standard NSWindow buttons,
|
|
// and `close.superview().superview()` in position_traffic_lights
|
|
// would null-deref on them.
|
|
let label = window.label().to_string();
|
|
if label != "main" && !label.starts_with("reader") {
|
|
return;
|
|
}
|
|
|
|
// Do the initial positioning
|
|
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<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;
|
|
|
|
if state.window.label() == "main" || state.window.label().starts_with("reader")
|
|
{
|
|
position_traffic_lights(
|
|
UnsafeWindowHandle(id as *mut std::ffi::c_void),
|
|
TRAFFIC_LIGHTS_VISIBLE,
|
|
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;
|
|
if state.window.label() == "main" || state.window.label().starts_with("reader")
|
|
{
|
|
position_traffic_lights(
|
|
UnsafeWindowHandle(id as *mut std::ffi::c_void),
|
|
TRAFFIC_LIGHTS_VISIBLE,
|
|
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 "C" fn(&Object, Sel, id) -> BOOL,
|
|
(windowWillClose:) => on_window_will_close as extern "C" fn(&Object, Sel, id),
|
|
(windowDidResize:) => on_window_did_resize::<R> as extern "C" fn(&Object, Sel, id),
|
|
(windowDidMove:) => on_window_did_move as extern "C" fn(&Object, Sel, id),
|
|
(windowDidChangeBackingProperties:) => on_window_did_change_backing_properties as extern "C" fn(&Object, Sel, id),
|
|
(windowDidBecomeKey:) => on_window_did_become_key as extern "C" fn(&Object, Sel, id),
|
|
(windowDidResignKey:) => on_window_did_resign_key as extern "C" fn(&Object, Sel, id),
|
|
(draggingEntered:) => on_dragging_entered as extern "C" fn(&Object, Sel, id) -> BOOL,
|
|
(prepareForDragOperation:) => on_prepare_for_drag_operation as extern "C" fn(&Object, Sel, id) -> BOOL,
|
|
(performDragOperation:) => on_perform_drag_operation as extern "C" fn(&Object, Sel, id) -> BOOL,
|
|
(concludeDragOperation:) => on_conclude_drag_operation as extern "C" fn(&Object, Sel, id),
|
|
(draggingExited:) => on_dragging_exited as extern "C" fn(&Object, Sel, id),
|
|
(window:willUseFullScreenPresentationOptions:) => on_window_will_use_full_screen_presentation_options as extern "C" fn(&Object, Sel, id, NSUInteger) -> NSUInteger,
|
|
(windowDidEnterFullScreen:) => on_window_did_enter_full_screen::<R> as extern "C" fn(&Object, Sel, id),
|
|
(windowWillEnterFullScreen:) => on_window_will_enter_full_screen::<R> as extern "C" fn(&Object, Sel, id),
|
|
(windowDidExitFullScreen:) => on_window_did_exit_full_screen::<R> as extern "C" fn(&Object, Sel, id),
|
|
(windowWillExitFullScreen:) => on_window_will_exit_full_screen::<R> as extern "C" fn(&Object, Sel, id),
|
|
(windowDidFailToEnterFullScreen:) => on_window_did_fail_to_enter_full_screen as extern "C" fn(&Object, Sel, id),
|
|
(effectiveAppearanceDidChange:) => on_effective_appearance_did_change as extern "C" fn(&Object, Sel, id),
|
|
(effectiveAppearanceDidChangedOnMainThread:) => on_effective_appearance_did_changed_on_main_thread as extern "C" fn(&Object, Sel, id)
|
|
}))
|
|
}
|
|
}
|