Sync viewer navigation with browser sorting
This commit is contained in:
parent
bb73c1abea
commit
f0d5cf3496
5 changed files with 375 additions and 87 deletions
177
src/viewer.rs
177
src/viewer.rs
|
|
@ -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: >k::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),
|
||||
"더 이상 다음 파일이 없습니다."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue