Sync viewer navigation with browser sorting

This commit is contained in:
burnintuna 2026-07-22 03:01:12 +09:00
parent bb73c1abea
commit f0d5cf3496
5 changed files with 375 additions and 87 deletions

View file

@ -22,33 +22,43 @@ const FAST_CACHE_LIMIT_PAGES: usize = 64;
pub struct ViewerWindow;
pub struct SiblingNavigator {
current: usize,
count: usize,
open: Rc<dyn Fn(usize, gtk::ApplicationWindow) -> bool>,
position: Rc<dyn Fn() -> Option<(usize, usize)>>,
navigate: Rc<dyn Fn(SiblingTarget, gtk::ApplicationWindow) -> bool>,
}
impl SiblingNavigator {
pub fn new(
current: usize,
count: usize,
open: Rc<dyn Fn(usize, gtk::ApplicationWindow) -> bool>,
position: Rc<dyn Fn() -> Option<(usize, usize)>>,
navigate: Rc<dyn Fn(SiblingTarget, gtk::ApplicationWindow) -> bool>,
) -> Self {
Self {
current,
count,
open,
}
Self { position, navigate }
}
fn open_relative(&self, forward: bool, window: gtk::ApplicationWindow) -> bool {
let Some(index) = sibling_index(self.current, self.count, forward) else {
return false;
};
(self.open)(index, window)
(self.navigate)(SiblingTarget::Relative(forward), window)
}
fn open_index(&self, index: usize, window: gtk::ApplicationWindow) -> bool {
index < self.count && index != self.current && (self.open)(index, window)
(self.navigate)(SiblingTarget::Index(index), window)
}
fn position(&self) -> Option<(usize, usize)> {
(self.position)()
}
}
#[derive(Clone, Copy)]
pub enum SiblingTarget {
Relative(bool),
Index(usize),
}
impl SiblingTarget {
pub fn resolve(self, current: usize, count: usize) -> Option<usize> {
match self {
Self::Relative(forward) => sibling_index(current, count, forward),
Self::Index(index) => (index < count && index != current).then_some(index),
}
}
}
@ -79,6 +89,7 @@ impl ViewerWindow {
source: ArchiveSource,
database: Rc<Database>,
fast_image_loading: bool,
case_insensitive_sort: bool,
view_mode_control: ViewModeControl,
controls: FileViewerControls,
) {
@ -190,7 +201,11 @@ impl ViewerWindow {
root.append(&overlay);
root.append(&page_seek_row);
root.append(&status_label);
window.set_child(Some(&root));
let window_overlay = gtk::Overlay::new();
window_overlay.set_child(Some(&root));
let (navigation_message, navigation_message_label) = navigation_message();
window_overlay.add_overlay(&navigation_message);
window.set_child(Some(&window_overlay));
let controller = Rc::new(ViewerController {
window,
@ -214,6 +229,7 @@ impl ViewerWindow {
mode_dropdown,
direction_dropdown,
fast_image_loading,
case_insensitive_sort,
sibling_navigator,
favorite_button,
favorite_toggle: favorite_control.toggle,
@ -224,6 +240,9 @@ impl ViewerWindow {
last_pointer_position: Cell::new(None),
key_controller: RefCell::new(None),
motion_controller: RefCell::new(None),
navigation_message,
navigation_message_label,
navigation_message_generation: Rc::new(Cell::new(0)),
state: RefCell::new(ViewerState {
pages: Vec::new(),
anchor: 0,
@ -300,6 +319,7 @@ struct ViewerController {
mode_dropdown: gtk::DropDown,
direction_dropdown: gtk::DropDown,
fast_image_loading: bool,
case_insensitive_sort: bool,
sibling_navigator: Option<SiblingNavigator>,
favorite_button: gtk::Button,
favorite_toggle: Rc<dyn Fn() -> bool>,
@ -310,6 +330,9 @@ struct ViewerController {
last_pointer_position: Cell<Option<(f64, f64)>>,
key_controller: RefCell<Option<gtk::EventControllerKey>>,
motion_controller: RefCell<Option<gtk::EventControllerMotion>>,
navigation_message: gtk::Box,
navigation_message_label: gtk::Label,
navigation_message_generation: Rc<Cell<u64>>,
state: RefCell<ViewerState>,
}
@ -431,9 +454,26 @@ impl ViewerController {
&& navigate.open_relative(forward, self.window.clone())
{
self.detach_window_controllers();
} else {
self.show_navigation_error(forward);
}
}
fn show_navigation_error(&self, forward: bool) {
self.navigation_message_label
.set_text(navigation_error_message(forward));
self.navigation_message.set_visible(true);
let message = self.navigation_message.clone();
let generation = self.navigation_message_generation.get().wrapping_add(1);
self.navigation_message_generation.set(generation);
let current_generation = Rc::clone(&self.navigation_message_generation);
glib::timeout_add_local_once(Duration::from_secs(2), move || {
if current_generation.get() == generation {
message.set_visible(false);
}
});
}
fn detach_window_controllers(&self) {
if let Some(controller) = self.key_controller.borrow_mut().take() {
self.window.remove_controller(&controller);
@ -500,9 +540,12 @@ impl ViewerController {
fn load_archive(self: &Rc<Self>) {
self.spinner.start();
let path = self.source.local_path.clone();
let case_insensitive_sort = self.case_insensitive_sort;
let (sender, receiver) = mpsc::channel();
thread::spawn(move || {
let _ = sender.send(list_pages(&path).map_err(|error| format!("{error:#}")));
let _ = sender.send(
list_pages(&path, case_insensitive_sort).map_err(|error| format!("{error:#}")),
);
});
let controller = Rc::downgrade(self);
glib::timeout_add_local(Duration::from_millis(20), move || {
@ -1139,17 +1182,24 @@ impl MediaViewerWindow {
.build();
let media_seek = navigation_scale();
let media_seek_row = navigation_scale_row(&media_seek);
if let Some(navigator) = &sibling_navigator {
media_seek.set_range(0.0, navigation_scale_upper(navigator.count));
media_seek.set_value(navigator.current as f64);
media_seek.set_sensitive(navigator.count > 1);
if let Some((current, count)) = sibling_navigator
.as_ref()
.and_then(SiblingNavigator::position)
{
media_seek.set_range(0.0, navigation_scale_upper(count));
media_seek.set_value(current as f64);
media_seek.set_sensitive(count > 1);
} else {
media_seek.set_sensitive(false);
}
root.append(&stack);
root.append(&media_seek_row);
root.append(&status_label);
window.set_child(Some(&root));
let window_overlay = gtk::Overlay::new();
window_overlay.set_child(Some(&root));
let (navigation_message, navigation_message_label) = navigation_message();
window_overlay.add_overlay(&navigation_message);
window.set_child(Some(&window_overlay));
let controller = Rc::new(MediaViewerController {
window,
@ -1171,6 +1221,9 @@ impl MediaViewerWindow {
last_pointer_position: Cell::new(None),
key_controller: RefCell::new(None),
motion_controller: RefCell::new(None),
navigation_message,
navigation_message_label,
navigation_message_generation: Rc::new(Cell::new(0)),
});
if let Some(stream) = controller.video.media_stream() {
let status_label = controller.status_label.clone();
@ -1215,6 +1268,9 @@ struct MediaViewerController {
last_pointer_position: Cell<Option<(f64, f64)>>,
key_controller: RefCell<Option<gtk::EventControllerKey>>,
motion_controller: RefCell<Option<gtk::EventControllerMotion>>,
navigation_message: gtk::Box,
navigation_message_label: gtk::Label,
navigation_message_generation: Rc<Cell<u64>>,
}
impl MediaViewerController {
@ -1269,7 +1325,10 @@ impl MediaViewerController {
let Some(navigator) = &controller.sibling_navigator else {
return;
};
let Some(index) = page_seek_index(scale.value(), navigator.count) else {
let Some((_, count)) = navigator.position() else {
return;
};
let Some(index) = page_seek_index(scale.value(), count) else {
return;
};
controller.schedule_seek(index);
@ -1281,13 +1340,40 @@ impl MediaViewerController {
&& navigate.open_relative(forward, self.window.clone())
{
self.prepare_for_navigation();
} else {
self.show_navigation_error(forward);
}
}
fn show_navigation_error(&self, forward: bool) {
self.navigation_message_label
.set_text(navigation_error_message(forward));
self.navigation_message.set_visible(true);
let message = self.navigation_message.clone();
let generation = self.navigation_message_generation.get().wrapping_add(1);
self.navigation_message_generation.set(generation);
let current_generation = Rc::clone(&self.navigation_message_generation);
glib::timeout_add_local_once(Duration::from_secs(2), move || {
if current_generation.get() == generation {
message.set_visible(false);
}
});
}
fn show_seek_temporarily(self: &Rc<Self>) {
if let Some(timer) = self.seek_hide_timer.borrow_mut().take() {
timer.remove();
}
if let Some((current, count)) = self
.sibling_navigator
.as_ref()
.and_then(SiblingNavigator::position)
{
self.media_seek
.set_range(0.0, navigation_scale_upper(count));
self.media_seek.set_sensitive(count > 1);
self.media_seek.set_value(current as f64);
}
self.set_seek_visible(true);
let controller = Rc::downgrade(self);
let timer = glib::timeout_add_local_once(Duration::from_secs(5), move || {
@ -1334,7 +1420,11 @@ impl MediaViewerController {
let Some(navigator) = &self.sibling_navigator else {
return;
};
if index == navigator.current || self.seek_opening.get() {
if navigator
.position()
.is_none_or(|(current, _)| index == current)
|| self.seek_opening.get()
{
return;
}
let generation = self.seek_generation.get().wrapping_add(1);
@ -1452,6 +1542,31 @@ fn navigation_scale() -> gtk::Scale {
scale
}
fn navigation_message() -> (gtk::Box, gtk::Label) {
let message = gtk::Box::new(gtk::Orientation::Horizontal, 0);
message.add_css_class("osd");
message.set_halign(gtk::Align::Center);
message.set_valign(gtk::Align::Center);
message.set_can_target(false);
message.set_margin_top(12);
message.set_margin_bottom(12);
message.set_margin_start(18);
message.set_margin_end(18);
message.set_visible(false);
let label = gtk::Label::new(None);
label.add_css_class("title-2");
message.append(&label);
(message, label)
}
fn navigation_error_message(forward: bool) -> &'static str {
if forward {
"더 이상 다음 파일이 없습니다."
} else {
"더 이상 이전 파일이 없습니다."
}
}
fn navigation_scale_row(scale: &gtk::Scale) -> gtk::Box {
let row = gtk::Box::new(gtk::Orientation::Horizontal, 0);
row.set_height_request(42);
@ -1591,4 +1706,16 @@ mod tests {
assert_eq!(sibling_index(0, 3, false), None);
assert_eq!(sibling_index(2, 3, true), None);
}
#[test]
fn navigation_boundary_messages_match_direction() {
assert_eq!(
navigation_error_message(false),
"더 이상 이전 파일이 없습니다."
);
assert_eq!(
navigation_error_message(true),
"더 이상 다음 파일이 없습니다."
);
}
}