Add viewer navigation and file downloads
This commit is contained in:
parent
490c925f62
commit
ef2a0a7915
3 changed files with 221 additions and 43 deletions
148
src/viewer.rs
148
src/viewer.rs
|
|
@ -1,4 +1,4 @@
|
|||
use std::cell::RefCell;
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
use std::rc::Rc;
|
||||
use std::sync::mpsc::{self, TryRecvError};
|
||||
|
|
@ -21,13 +21,42 @@ const FAST_CACHE_LIMIT_PAGES: usize = 64;
|
|||
|
||||
pub struct ViewerWindow;
|
||||
|
||||
pub type SiblingNavigator = Rc<dyn Fn(bool) -> bool>;
|
||||
pub struct SiblingNavigator {
|
||||
current: usize,
|
||||
count: usize,
|
||||
open: Rc<dyn Fn(usize) -> bool>,
|
||||
}
|
||||
|
||||
impl SiblingNavigator {
|
||||
pub fn new(current: usize, count: usize, open: Rc<dyn Fn(usize) -> bool>) -> Self {
|
||||
Self {
|
||||
current,
|
||||
count,
|
||||
open,
|
||||
}
|
||||
}
|
||||
|
||||
fn open_relative(&self, forward: bool) -> bool {
|
||||
let Some(index) = sibling_index(self.current, self.count, forward) else {
|
||||
return false;
|
||||
};
|
||||
(self.open)(index)
|
||||
}
|
||||
|
||||
fn open_index(&self, index: usize) -> bool {
|
||||
index < self.count && index != self.current && (self.open)(index)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FavoriteControl {
|
||||
pub active: bool,
|
||||
pub toggle: Rc<dyn Fn() -> bool>,
|
||||
}
|
||||
|
||||
pub struct FileDownloadControl {
|
||||
pub download: Rc<dyn Fn(gtk::Window, gtk::Label)>,
|
||||
}
|
||||
|
||||
impl ViewerWindow {
|
||||
pub fn open(
|
||||
application: >k::Application,
|
||||
|
|
@ -36,6 +65,7 @@ impl ViewerWindow {
|
|||
fast_image_loading: bool,
|
||||
sibling_navigator: Option<SiblingNavigator>,
|
||||
favorite_control: FavoriteControl,
|
||||
download_control: FileDownloadControl,
|
||||
) {
|
||||
let title = source.display_name.clone();
|
||||
let window = gtk::ApplicationWindow::builder()
|
||||
|
|
@ -47,6 +77,8 @@ impl ViewerWindow {
|
|||
let header = gtk::HeaderBar::new();
|
||||
header.set_title_widget(Some(>k::Label::new(Some(&title))));
|
||||
let favorite_button = gtk::Button::with_label(favorite_label(favorite_control.active));
|
||||
let download_button = gtk::Button::with_label("파일 다운로드");
|
||||
header.pack_end(&download_button);
|
||||
header.pack_end(&favorite_button);
|
||||
window.set_titlebar(Some(&header));
|
||||
|
||||
|
|
@ -162,6 +194,8 @@ impl ViewerWindow {
|
|||
sibling_navigator,
|
||||
favorite_button,
|
||||
favorite_toggle: favorite_control.toggle,
|
||||
download_button,
|
||||
file_download: download_control.download,
|
||||
state: RefCell::new(ViewerState {
|
||||
pages: Vec::new(),
|
||||
anchor: 0,
|
||||
|
|
@ -240,6 +274,8 @@ struct ViewerController {
|
|||
sibling_navigator: Option<SiblingNavigator>,
|
||||
favorite_button: gtk::Button,
|
||||
favorite_toggle: Rc<dyn Fn() -> bool>,
|
||||
download_button: gtk::Button,
|
||||
file_download: Rc<dyn Fn(gtk::Window, gtk::Label)>,
|
||||
state: RefCell<ViewerState>,
|
||||
}
|
||||
|
||||
|
|
@ -269,6 +305,13 @@ impl ViewerController {
|
|||
controller.favorite_button.set_label(favorite_label(active));
|
||||
});
|
||||
let controller = Rc::clone(self);
|
||||
self.download_button.connect_clicked(move |_| {
|
||||
(controller.file_download)(
|
||||
controller.window.clone().upcast(),
|
||||
controller.status_label.clone(),
|
||||
);
|
||||
});
|
||||
let controller = Rc::clone(self);
|
||||
self.page_seek.connect_value_changed(move |scale| {
|
||||
if let Some(index) =
|
||||
page_seek_index(scale.value(), controller.state.borrow().pages.len())
|
||||
|
|
@ -339,7 +382,7 @@ impl ViewerController {
|
|||
|
||||
fn open_sibling(&self, forward: bool) {
|
||||
if let Some(navigate) = &self.sibling_navigator
|
||||
&& navigate(forward)
|
||||
&& navigate.open_relative(forward)
|
||||
{
|
||||
self.window.close();
|
||||
}
|
||||
|
|
@ -945,6 +988,7 @@ impl MediaViewerWindow {
|
|||
kind: EntryKind,
|
||||
sibling_navigator: Option<SiblingNavigator>,
|
||||
favorite_control: FavoriteControl,
|
||||
download_control: FileDownloadControl,
|
||||
) {
|
||||
let title = source.display_name.clone();
|
||||
let window = gtk::ApplicationWindow::builder()
|
||||
|
|
@ -956,6 +1000,8 @@ impl MediaViewerWindow {
|
|||
let header = gtk::HeaderBar::new();
|
||||
header.set_title_widget(Some(>k::Label::new(Some(&title))));
|
||||
let favorite_button = gtk::Button::with_label(favorite_label(favorite_control.active));
|
||||
let download_button = gtk::Button::with_label("파일 다운로드");
|
||||
header.pack_end(&download_button);
|
||||
header.pack_end(&favorite_button);
|
||||
window.set_titlebar(Some(&header));
|
||||
|
||||
|
|
@ -982,7 +1028,19 @@ impl MediaViewerWindow {
|
|||
.margin_start(8)
|
||||
.margin_end(8)
|
||||
.build();
|
||||
let media_seek = gtk::Scale::with_range(gtk::Orientation::Horizontal, 0.0, 0.0, 1.0);
|
||||
media_seek.set_draw_value(false);
|
||||
media_seek.set_margin_start(8);
|
||||
media_seek.set_margin_end(8);
|
||||
if let Some(navigator) = &sibling_navigator {
|
||||
media_seek.set_range(0.0, navigator.count.saturating_sub(1) as f64);
|
||||
media_seek.set_value(navigator.current as f64);
|
||||
media_seek.set_sensitive(navigator.count > 1);
|
||||
} else {
|
||||
media_seek.set_sensitive(false);
|
||||
}
|
||||
root.append(&stack);
|
||||
root.append(&media_seek);
|
||||
root.append(&status_label);
|
||||
window.set_child(Some(&root));
|
||||
|
||||
|
|
@ -993,9 +1051,14 @@ impl MediaViewerWindow {
|
|||
video,
|
||||
stack,
|
||||
status_label,
|
||||
media_seek,
|
||||
sibling_navigator,
|
||||
favorite_button,
|
||||
favorite_toggle: favorite_control.toggle,
|
||||
download_button,
|
||||
file_download: download_control.download,
|
||||
seek_generation: Cell::new(0),
|
||||
seek_opening: Cell::new(false),
|
||||
});
|
||||
if let Some(stream) = controller.video.media_stream() {
|
||||
let status_label = controller.status_label.clone();
|
||||
|
|
@ -1027,9 +1090,14 @@ struct MediaViewerController {
|
|||
video: gtk::Video,
|
||||
stack: gtk::Stack,
|
||||
status_label: gtk::Label,
|
||||
media_seek: gtk::Scale,
|
||||
sibling_navigator: Option<SiblingNavigator>,
|
||||
favorite_button: gtk::Button,
|
||||
favorite_toggle: Rc<dyn Fn() -> bool>,
|
||||
download_button: gtk::Button,
|
||||
file_download: Rc<dyn Fn(gtk::Window, gtk::Label)>,
|
||||
seek_generation: Cell<u64>,
|
||||
seek_opening: Cell<bool>,
|
||||
}
|
||||
|
||||
impl MediaViewerController {
|
||||
|
|
@ -1061,19 +1129,67 @@ impl MediaViewerController {
|
|||
let active = (controller.favorite_toggle)();
|
||||
controller.favorite_button.set_label(favorite_label(active));
|
||||
});
|
||||
let controller = Rc::clone(self);
|
||||
self.download_button.connect_clicked(move |_| {
|
||||
(controller.file_download)(
|
||||
controller.window.clone().upcast(),
|
||||
controller.status_label.clone(),
|
||||
);
|
||||
});
|
||||
let controller = Rc::clone(self);
|
||||
self.media_seek.connect_value_changed(move |scale| {
|
||||
let Some(navigator) = &controller.sibling_navigator else {
|
||||
return;
|
||||
};
|
||||
let Some(index) = page_seek_index(scale.value(), navigator.count) else {
|
||||
return;
|
||||
};
|
||||
controller.schedule_seek(index);
|
||||
});
|
||||
}
|
||||
|
||||
fn open_sibling(&self, forward: bool) {
|
||||
if let Some(navigate) = &self.sibling_navigator
|
||||
&& navigate(forward)
|
||||
&& navigate.open_relative(forward)
|
||||
{
|
||||
if let Some(stream) = self.video.media_stream() {
|
||||
stream.pause();
|
||||
}
|
||||
self.window.close();
|
||||
self.close_for_navigation();
|
||||
}
|
||||
}
|
||||
|
||||
fn schedule_seek(self: &Rc<Self>, index: usize) {
|
||||
let Some(navigator) = &self.sibling_navigator else {
|
||||
return;
|
||||
};
|
||||
if index == navigator.current || self.seek_opening.get() {
|
||||
return;
|
||||
}
|
||||
let generation = self.seek_generation.get().wrapping_add(1);
|
||||
self.seek_generation.set(generation);
|
||||
let controller = Rc::downgrade(self);
|
||||
glib::timeout_add_local_once(Duration::from_millis(180), move || {
|
||||
let Some(controller) = controller.upgrade() else {
|
||||
return;
|
||||
};
|
||||
if controller.seek_generation.get() != generation || controller.seek_opening.get() {
|
||||
return;
|
||||
}
|
||||
let Some(navigator) = &controller.sibling_navigator else {
|
||||
return;
|
||||
};
|
||||
if navigator.open_index(index) {
|
||||
controller.seek_opening.set(true);
|
||||
controller.close_for_navigation();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn close_for_navigation(&self) {
|
||||
if let Some(stream) = self.video.media_stream() {
|
||||
stream.pause();
|
||||
}
|
||||
self.window.close();
|
||||
}
|
||||
|
||||
fn toggle_fullscreen(&self) {
|
||||
if self.window.is_fullscreen() {
|
||||
self.window.unfullscreen();
|
||||
|
|
@ -1196,6 +1312,14 @@ fn page_seek_index(value: f64, count: usize) -> Option<usize> {
|
|||
(count > 0).then(|| (value.round().max(0.0) as usize).min(count - 1))
|
||||
}
|
||||
|
||||
fn sibling_index(current: usize, count: usize, forward: bool) -> Option<usize> {
|
||||
if forward {
|
||||
(current + 1 < count).then_some(current + 1)
|
||||
} else {
|
||||
current.checked_sub(1)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
@ -1245,4 +1369,12 @@ mod tests {
|
|||
assert_eq!(page_seek_index(20.0, 10), Some(9));
|
||||
assert_eq!(page_seek_index(0.0, 0), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sibling_navigation_stops_at_boundaries() {
|
||||
assert_eq!(sibling_index(1, 3, false), Some(0));
|
||||
assert_eq!(sibling_index(1, 3, true), Some(2));
|
||||
assert_eq!(sibling_index(0, 3, false), None);
|
||||
assert_eq!(sibling_index(2, 3, true), None);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue