OneLauncher-Cracked/packages/oneclient_app/src/components/screenshot_viewer.rs
LynithDev 2f5a64b97d
Fix/linux skia crashes (#966)
* fix: run hooks before early returns in cluster and gallery views

* chore: upgrade freya to 0.5.0-beta.9

* chore: pin freya to 0.5.0-beta.10 branch for skia 0.103.2
2026-10-10 00:58:42 +02:00

341 lines
13 KiB
Rust

use std::path::PathBuf;
use freya::prelude::*;
use oneclient_core::ScreenshotInfo;
use crate::components::{
Button, ContextMenu, Icon, IconType, LocalImage, OVERLAY_BASE_LEVEL, OVERLAY_MAX_LEVEL,
OverlayPopup, overlay_is_topmost,
};
use crate::hooks::{
Actions, ScreenshotAction, UseScreenshotAction, use_dispatch, use_screenshot_action,
};
use crate::theme::colors;
use crate::ui::fmt_date;
use crate::utils::{format_res, format_size};
#[derive(PartialEq)]
pub struct ScreenshotViewer {
shots: Vec<ScreenshotInfo>,
start: usize,
on_close: EventHandler<()>,
}
impl ScreenshotViewer {
pub fn new(
shots: Vec<ScreenshotInfo>,
start: usize,
on_close: impl Into<EventHandler<()>>,
) -> Self {
Self {
shots,
start,
on_close: on_close.into(),
}
}
}
impl Component for ScreenshotViewer {
fn render(&self) -> impl IntoElement {
let shots = self.shots.clone();
let len = shots.len();
let mut index = use_state({
let start = self.start.min(len.saturating_sub(1));
move || start
});
let mut menu = use_state(|| None::<(f32, f32)>);
let action = use_screenshot_action();
let dispatch = use_dispatch();
if len == 0 {
return rect().into_element();
}
let idx = (*index.read()).min(len - 1);
let info = shots[idx].clone();
let close = self.on_close.clone();
let scrim_close = self.on_close.clone();
let delete_close = self.on_close.clone();
let menu_delete_close = self.on_close.clone();
let has_prev = idx > 0;
let has_next = idx + 1 < len;
let preview = LocalImage::new(info.path.clone(), 0, false).skeleton(true);
let open_path = info.path.clone();
let copy_path = info.path.clone();
let copy_dispatch = dispatch.clone();
let delete_path = info.path.clone();
let menu_overlay = (*menu.read()).map(|(x, y)| {
let menu_close = menu_delete_close.clone();
// Nested inside this viewer's popup, so it has to be raised above it
screenshot_context_menu(
x,
y,
info.path.clone(),
action,
dispatch.clone(),
move |()| menu_close.call(()),
None,
)
.overlay_level(OVERLAY_MAX_LEVEL)
.on_close(move |_| menu.set(None))
.into_element()
});
OverlayPopup::new()
.on_close(move |_| scrim_close.call(()))
.child(
rect()
.width(Size::window_percent(100.))
.height(Size::window_percent(100.))
.center()
.on_global_key_down(move |e: Event<KeyboardEventData>| {
if !overlay_is_topmost(OVERLAY_BASE_LEVEL) {
return;
}
match &e.key {
Key::Named(NamedKey::ArrowLeft) if idx > 0 => index.set(idx - 1),
Key::Named(NamedKey::ArrowRight) if idx + 1 < len => index.set(idx + 1),
_ => {}
}
})
.child(
rect()
.vertical()
.width(Size::window_percent(88.))
.height(Size::window_percent(90.))
.spacing(12.)
.padding(Gaps::new_all(16.))
.content(Content::Flex)
.child(header_row(&info, idx, len, move |_| close.call(())))
.child(
rect()
.width(Size::fill())
.height(Size::flex(1.0))
.horizontal()
.cross_align(Alignment::Center)
.content(Content::Flex)
.spacing(12.)
.overflow(Overflow::Clip)
.child(chevron_btn(IconType::ArrowLeft, has_prev, move |_| {
if idx > 0 {
index.set(idx - 1);
}
}))
.child(
rect()
.width(Size::flex(1.0))
.height(Size::fill())
.on_secondary_down(move |e: Event<PressEventData>| {
if let PressEventData::Mouse(m) = e.data() {
menu.set(Some((
m.global_location.x as f32,
m.global_location.y as f32,
)));
}
})
.child(preview),
)
.child(chevron_btn(
IconType::ArrowRight,
has_next,
move |_| {
if idx + 1 < len {
index.set(idx + 1);
}
},
)),
)
.child(
rect()
.horizontal()
.width(Size::fill())
.main_align(Alignment::End)
.spacing(8.)
.child(
Button::new()
.secondary()
.on_press(move |_| {
if let Some(dir) = open_path.parent() {
crate::platform::open_path(
&dir.to_string_lossy(),
);
}
})
.child(Icon::new(IconType::Folder).size(14.))
.text("Open in folder"),
)
.child(
Button::new()
.secondary()
.on_press(move |_| {
crate::platform::copy_image_to_clipboard(
copy_path.clone(),
);
copy_dispatch
.notify("Copied to clipboard")
.body("Screenshot copied to your clipboard.")
.info()
.icon(IconType::ClipboardCheck)
.toast_only()
.send();
})
.child(Icon::new(IconType::Copy01).size(14.))
.text("Copy"),
)
.child(
Button::new()
.danger()
.on_press(move |_| {
action.mutate(ScreenshotAction::Delete {
path: delete_path.clone(),
});
delete_close.call(());
})
.child(Icon::new(IconType::Trash01).size(14.))
.text("Delete"),
),
),
),
)
.maybe_child(menu_overlay)
.into_element()
}
}
/// The screenshot menu shown both by the grid and by this viewer, so the two
/// cannot drift apart
pub fn screenshot_context_menu(
x: f32,
y: f32,
path: PathBuf,
action: UseScreenshotAction,
dispatch: Actions,
on_deleted: impl Into<EventHandler<()>>,
on_select: Option<EventHandler<()>>,
) -> ContextMenu {
let open_path = path.clone();
let copy_path = path.clone();
let delete_path = path;
let on_deleted = on_deleted.into();
let context_menu = match on_select {
Some(on_select) => ContextMenu::new(x, y)
.action(IconType::Check, "Select", on_select)
.separator(),
None => ContextMenu::new(x, y),
};
context_menu
.action(IconType::Folder, "Open in folder", move |()| {
if let Some(dir) = open_path.parent() {
crate::platform::open_path(&dir.to_string_lossy());
}
})
.action(IconType::Copy01, "Copy", move |()| {
crate::platform::copy_image_to_clipboard(copy_path.clone());
dispatch
.notify("Copied to clipboard")
.body("Screenshot copied to your clipboard.")
.info()
.icon(IconType::ClipboardCheck)
.toast_only()
.send();
})
.separator()
.danger_action(IconType::Trash01, "Delete", move |()| {
action.mutate(ScreenshotAction::Delete {
path: delete_path.clone(),
});
on_deleted.call(());
})
}
fn chevron_btn(
icon: IconType,
enabled: bool,
on_press: impl Into<EventHandler<Event<PressEventData>>>,
) -> impl IntoElement {
let base = rect()
.width(Size::px(44.))
.height(Size::px(44.))
.center()
.corner_radius(CornerRadius::new_all(22.));
if !enabled {
return base
.background(Color::from_argb(70, 0, 0, 0))
.on_press(|_| {})
.child(
Icon::new(icon)
.size(26.)
.color(colors::fg_secondary().with_a(90)),
)
.into_element();
}
base.background(Color::from_argb(140, 0, 0, 0))
.cursor(CursorIcon::Pointer)
.on_press(on_press)
.child(Icon::new(icon).size(26.).color(colors::fg_primary()))
.into_element()
}
fn header_row(
info: &ScreenshotInfo,
idx: usize,
len: usize,
on_close: impl Into<EventHandler<Event<PressEventData>>>,
) -> impl IntoElement {
let mut meta = format!(
"{} • {}",
format_size(info.size_bytes),
fmt_date(info.created)
);
if let Some(res) = info.resolution {
meta = format!("{} • {}", format_res(res), meta);
}
if len > 1 {
meta = format!("{} of {} • {meta}", idx + 1, len);
}
rect()
.horizontal()
.width(Size::fill())
.content(Content::Flex)
.cross_align(Alignment::Center)
.child(
rect()
.vertical()
.width(Size::flex(1.0))
.spacing(2.)
.child(
label()
.text(info.name.clone())
.font_size(15.)
.font_weight(FontWeight::SEMI_BOLD)
.color(colors::fg_primary()),
)
.child(
label()
.text(meta)
.font_size(11.)
.color(colors::fg_secondary()),
),
)
.child(
Button::new()
.ghost()
.icon()
.tooltip("Close")
.on_press(on_close)
.child(Icon::new(IconType::XClose).size(18.)),
)
.into_element()
}