Release v0.2.3
This commit is contained in:
parent
b31acd3f58
commit
86f3185c59
13 changed files with 1255 additions and 68 deletions
413
src/viewer.rs
413
src/viewer.rs
|
|
@ -3,7 +3,7 @@ use std::collections::{HashMap, HashSet, VecDeque};
|
|||
use std::rc::{Rc, Weak};
|
||||
use std::sync::mpsc::{self, TryRecvError};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use gtk::gdk;
|
||||
use gtk::glib;
|
||||
|
|
@ -26,12 +26,12 @@ const MEDIA_VIEWER_CONTROLLER_KEY: &str = "comicviewer-media-viewer-controller";
|
|||
fn clear_viewer_controller(window: >k::ApplicationWindow) {
|
||||
unsafe {
|
||||
if let Some(controller) = window.steal_data::<Rc<ViewerController>>(VIEWER_CONTROLLER_KEY) {
|
||||
controller.stop_animations();
|
||||
controller.detach_window_controllers();
|
||||
}
|
||||
if let Some(controller) =
|
||||
window.steal_data::<Rc<MediaViewerController>>(MEDIA_VIEWER_CONTROLLER_KEY)
|
||||
{
|
||||
controller.stop_playback();
|
||||
controller.prepare_for_navigation();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -47,7 +47,45 @@ fn ensure_viewer_close_handler(window: >k::ApplicationWindow) {
|
|||
});
|
||||
}
|
||||
|
||||
pub(crate) fn set_viewer_header(window: >k::ApplicationWindow, header: >k::HeaderBar) {
|
||||
pub(crate) fn set_viewer_loading(window: >k::ApplicationWindow, loading: bool) {
|
||||
if !window.has_css_class("viewer-loading-key-handler") {
|
||||
window.add_css_class("viewer-loading-key-handler");
|
||||
let keys = gtk::EventControllerKey::new();
|
||||
keys.set_propagation_phase(gtk::PropagationPhase::Capture);
|
||||
let weak_window = window.downgrade();
|
||||
keys.connect_key_pressed(move |_, key, _, _| {
|
||||
let Some(window) = weak_window.upgrade() else {
|
||||
return glib::Propagation::Proceed;
|
||||
};
|
||||
if !window.has_css_class("viewer-loading") || !window.is_fullscreen() {
|
||||
return glib::Propagation::Proceed;
|
||||
}
|
||||
if let Some(titlebar) = window.titlebar() {
|
||||
titlebar.set_visible(true);
|
||||
}
|
||||
if key == gdk::Key::Escape
|
||||
|| key
|
||||
.to_unicode()
|
||||
.is_some_and(|character| character.eq_ignore_ascii_case(&'f'))
|
||||
{
|
||||
window.unfullscreen();
|
||||
return glib::Propagation::Stop;
|
||||
}
|
||||
glib::Propagation::Proceed
|
||||
});
|
||||
window.add_controller(keys);
|
||||
}
|
||||
if loading {
|
||||
window.add_css_class("viewer-loading");
|
||||
} else {
|
||||
window.remove_css_class("viewer-loading");
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_viewer_header(
|
||||
window: >k::ApplicationWindow,
|
||||
header: >k::HeaderBar,
|
||||
) -> gtk::Box {
|
||||
let host = window
|
||||
.titlebar()
|
||||
.and_downcast::<gtk::Box>()
|
||||
|
|
@ -62,6 +100,8 @@ pub(crate) fn set_viewer_header(window: >k::ApplicationWindow, header: >k::H
|
|||
}
|
||||
header.set_hexpand(true);
|
||||
host.append(header);
|
||||
host.set_visible(!window.is_fullscreen());
|
||||
host
|
||||
}
|
||||
|
||||
pub struct ViewerWindow;
|
||||
|
|
@ -244,6 +284,7 @@ impl ViewerWindow {
|
|||
});
|
||||
install_file_drop_target(&window, file_drop_control);
|
||||
ensure_viewer_close_handler(&window);
|
||||
set_viewer_loading(&window, false);
|
||||
window.set_title(Some(&title));
|
||||
let header = gtk::HeaderBar::new();
|
||||
header.set_title_widget(Some(>k::Label::new(Some(&title))));
|
||||
|
|
@ -255,7 +296,7 @@ impl ViewerWindow {
|
|||
header.pack_end(&download_button);
|
||||
header.pack_end(&favorite_button);
|
||||
header.pack_end(&file_list_button);
|
||||
set_viewer_header(&window, &header);
|
||||
let titlebar_host = set_viewer_header(&window, &header);
|
||||
|
||||
let root = gtk::Box::new(gtk::Orientation::Vertical, 0);
|
||||
let toolbar = gtk::Box::new(gtk::Orientation::Horizontal, 6);
|
||||
|
|
@ -358,6 +399,7 @@ impl ViewerWindow {
|
|||
|
||||
let controller = Rc::new(ViewerController {
|
||||
window,
|
||||
titlebar_host,
|
||||
source,
|
||||
database,
|
||||
stack,
|
||||
|
|
@ -372,6 +414,7 @@ impl ViewerWindow {
|
|||
page_label,
|
||||
page_seek,
|
||||
page_seek_row,
|
||||
toolbar,
|
||||
status_label,
|
||||
previous_button,
|
||||
next_button,
|
||||
|
|
@ -395,6 +438,10 @@ impl ViewerWindow {
|
|||
last_pointer_position: Cell::new(None),
|
||||
key_controller: RefCell::new(None),
|
||||
motion_controller: RefCell::new(None),
|
||||
fullscreen_key_controller: RefCell::new(None),
|
||||
fullscreen_ui_hide_timer: RefCell::new(None),
|
||||
fullscreen_ui_visible: Cell::new(false),
|
||||
fullscreen_notify_handler: RefCell::new(None),
|
||||
navigation_message,
|
||||
navigation_message_label,
|
||||
navigation_message_generation: Rc::new(Cell::new(0)),
|
||||
|
|
@ -417,6 +464,7 @@ impl ViewerWindow {
|
|||
}),
|
||||
});
|
||||
controller.connect_signals();
|
||||
controller.fullscreen_changed();
|
||||
if controller.file_list_button.is_active() {
|
||||
controller.refresh_file_list();
|
||||
}
|
||||
|
|
@ -506,6 +554,7 @@ struct ViewerState {
|
|||
|
||||
struct ViewerController {
|
||||
window: gtk::ApplicationWindow,
|
||||
titlebar_host: gtk::Box,
|
||||
source: ArchiveSource,
|
||||
database: Rc<Database>,
|
||||
stack: gtk::Stack,
|
||||
|
|
@ -520,6 +569,7 @@ struct ViewerController {
|
|||
page_label: gtk::Label,
|
||||
page_seek: gtk::Scale,
|
||||
page_seek_row: gtk::Box,
|
||||
toolbar: gtk::Box,
|
||||
status_label: gtk::Label,
|
||||
previous_button: gtk::Button,
|
||||
next_button: gtk::Button,
|
||||
|
|
@ -543,6 +593,10 @@ struct ViewerController {
|
|||
last_pointer_position: Cell<Option<(f64, f64)>>,
|
||||
key_controller: RefCell<Option<gtk::EventControllerKey>>,
|
||||
motion_controller: RefCell<Option<gtk::EventControllerMotion>>,
|
||||
fullscreen_key_controller: RefCell<Option<gtk::EventControllerKey>>,
|
||||
fullscreen_ui_hide_timer: RefCell<Option<glib::SourceId>>,
|
||||
fullscreen_ui_visible: Cell<bool>,
|
||||
fullscreen_notify_handler: RefCell<Option<glib::SignalHandlerId>>,
|
||||
navigation_message: gtk::Box,
|
||||
navigation_message_label: gtk::Label,
|
||||
navigation_message_generation: Rc<Cell<u64>>,
|
||||
|
|
@ -612,7 +666,11 @@ impl ViewerController {
|
|||
let controller = Rc::downgrade(self);
|
||||
self.file_list_button.connect_toggled(move |button| {
|
||||
if let Some(controller) = controller.upgrade() {
|
||||
controller.file_list_panel.set_visible(button.is_active());
|
||||
controller.file_list_panel.set_visible(
|
||||
button.is_active()
|
||||
&& (!controller.window.is_fullscreen()
|
||||
|| controller.fullscreen_ui_visible.get()),
|
||||
);
|
||||
if button.is_active() {
|
||||
controller.window.add_css_class("viewer-file-list-visible");
|
||||
} else {
|
||||
|
|
@ -631,6 +689,22 @@ impl ViewerController {
|
|||
controller.open_file_list_row(row.index());
|
||||
}
|
||||
});
|
||||
let file_list_typeahead = Rc::new(RefCell::new(TypeaheadBuffer::default()));
|
||||
let file_list_keys = gtk::EventControllerKey::new();
|
||||
let controller = Rc::downgrade(self);
|
||||
file_list_keys.connect_key_pressed(move |_, key, _, modifiers| {
|
||||
let Some(controller) = controller.upgrade() else {
|
||||
return glib::Propagation::Proceed;
|
||||
};
|
||||
file_list_typeahead_select(
|
||||
&controller.file_list,
|
||||
&controller.file_list_files.borrow(),
|
||||
&mut file_list_typeahead.borrow_mut(),
|
||||
key,
|
||||
modifiers,
|
||||
)
|
||||
});
|
||||
self.file_list.add_controller(file_list_keys);
|
||||
let file_list_click = gtk::GestureClick::builder().button(3).build();
|
||||
let controller = Rc::downgrade(self);
|
||||
file_list_click.connect_pressed(move |_, _, x, y| {
|
||||
|
|
@ -717,13 +791,30 @@ impl ViewerController {
|
|||
self.window.add_controller(motion_controller.clone());
|
||||
self.motion_controller.replace(Some(motion_controller));
|
||||
|
||||
let fullscreen_keys = gtk::EventControllerKey::new();
|
||||
fullscreen_keys.set_propagation_phase(gtk::PropagationPhase::Capture);
|
||||
let controller = Rc::downgrade(self);
|
||||
fullscreen_keys.connect_key_pressed(move |_, _, _, _| {
|
||||
if let Some(controller) = controller.upgrade()
|
||||
&& controller.window.is_fullscreen()
|
||||
{
|
||||
controller.show_fullscreen_ui_temporarily();
|
||||
}
|
||||
glib::Propagation::Proceed
|
||||
});
|
||||
self.window.add_controller(fullscreen_keys.clone());
|
||||
self.fullscreen_key_controller
|
||||
.replace(Some(fullscreen_keys));
|
||||
|
||||
let key_controller = gtk::EventControllerKey::new();
|
||||
let controller = Rc::downgrade(self);
|
||||
key_controller.connect_key_pressed(move |_, key, _, _| {
|
||||
let Some(controller) = controller.upgrade() else {
|
||||
return glib::Propagation::Proceed;
|
||||
};
|
||||
controller.hide_seek();
|
||||
if !controller.window.is_fullscreen() {
|
||||
controller.hide_seek();
|
||||
}
|
||||
match key {
|
||||
gdk::Key::Home => {
|
||||
controller.seek_edge(false);
|
||||
|
|
@ -783,6 +874,15 @@ impl ViewerController {
|
|||
});
|
||||
self.window.add_controller(key_controller.clone());
|
||||
self.key_controller.replace(Some(key_controller));
|
||||
let controller = Rc::downgrade(self);
|
||||
let handler = self
|
||||
.window
|
||||
.connect_notify_local(Some("fullscreened"), move |_, _| {
|
||||
if let Some(controller) = controller.upgrade() {
|
||||
controller.fullscreen_changed();
|
||||
}
|
||||
});
|
||||
self.fullscreen_notify_handler.replace(Some(handler));
|
||||
}
|
||||
|
||||
fn open_sibling(&self, forward: bool) {
|
||||
|
|
@ -876,12 +976,22 @@ impl ViewerController {
|
|||
|
||||
fn detach_window_controllers(&self) {
|
||||
self.stop_animations();
|
||||
if let Some(timer) = self.seek_hide_timer.borrow_mut().take() {
|
||||
timer.remove();
|
||||
}
|
||||
self.cancel_fullscreen_ui_timer();
|
||||
if let Some(handler) = self.fullscreen_notify_handler.borrow_mut().take() {
|
||||
self.window.disconnect(handler);
|
||||
}
|
||||
if let Some(controller) = self.key_controller.borrow_mut().take() {
|
||||
self.window.remove_controller(&controller);
|
||||
}
|
||||
if let Some(controller) = self.motion_controller.borrow_mut().take() {
|
||||
self.window.remove_controller(&controller);
|
||||
}
|
||||
if let Some(controller) = self.fullscreen_key_controller.borrow_mut().take() {
|
||||
self.window.remove_controller(&controller);
|
||||
}
|
||||
}
|
||||
|
||||
fn stop_animations(&self) {
|
||||
|
|
@ -913,7 +1023,11 @@ impl ViewerController {
|
|||
return;
|
||||
}
|
||||
self.last_pointer_position.set(Some((x, y)));
|
||||
self.show_seek_temporarily();
|
||||
if self.window.is_fullscreen() {
|
||||
self.show_fullscreen_ui_temporarily();
|
||||
} else {
|
||||
self.show_seek_temporarily();
|
||||
}
|
||||
}
|
||||
|
||||
fn hide_seek(self: &Rc<Self>) {
|
||||
|
|
@ -947,6 +1061,62 @@ impl ViewerController {
|
|||
}
|
||||
}
|
||||
|
||||
fn fullscreen_changed(self: &Rc<Self>) {
|
||||
self.cancel_fullscreen_ui_timer();
|
||||
self.hide_seek();
|
||||
if self.window.is_fullscreen() {
|
||||
self.set_fullscreen_ui_visible(false);
|
||||
} else {
|
||||
self.fullscreen_ui_visible.set(true);
|
||||
self.titlebar_host.set_visible(true);
|
||||
self.toolbar.set_visible(true);
|
||||
self.status_label.set_visible(true);
|
||||
self.file_list_panel
|
||||
.set_visible(self.file_list_button.is_active());
|
||||
}
|
||||
}
|
||||
|
||||
fn show_fullscreen_ui_temporarily(self: &Rc<Self>) {
|
||||
if !self.window.is_fullscreen() {
|
||||
return;
|
||||
}
|
||||
self.cancel_fullscreen_ui_timer();
|
||||
self.set_fullscreen_ui_visible(true);
|
||||
let controller = Rc::downgrade(self);
|
||||
let timer = glib::timeout_add_local_once(Duration::from_secs(5), move || {
|
||||
if let Some(controller) = controller.upgrade() {
|
||||
controller.fullscreen_ui_hide_timer.borrow_mut().take();
|
||||
controller.set_fullscreen_ui_visible(false);
|
||||
}
|
||||
});
|
||||
self.fullscreen_ui_hide_timer.replace(Some(timer));
|
||||
}
|
||||
|
||||
fn cancel_fullscreen_ui_timer(&self) {
|
||||
if let Some(timer) = self.fullscreen_ui_hide_timer.borrow_mut().take() {
|
||||
timer.remove();
|
||||
}
|
||||
}
|
||||
|
||||
fn set_fullscreen_ui_visible(self: &Rc<Self>, visible: bool) {
|
||||
self.fullscreen_ui_visible.set(visible);
|
||||
self.titlebar_host.set_visible(visible);
|
||||
self.toolbar.set_visible(visible);
|
||||
self.page_seek_row.set_visible(visible);
|
||||
self.status_label.set_visible(visible);
|
||||
self.file_list_panel
|
||||
.set_visible(visible && self.file_list_button.is_active());
|
||||
self.window.queue_allocate();
|
||||
let controller = Rc::downgrade(self);
|
||||
glib::idle_add_local_once(move || {
|
||||
if let Some(controller) = controller.upgrade() {
|
||||
controller.stack.queue_allocate();
|
||||
controller.resize_continuous_rows();
|
||||
controller.render();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn load_archive(self: &Rc<Self>) {
|
||||
self.spinner.start();
|
||||
let path = self.source.local_path.clone();
|
||||
|
|
@ -1727,6 +1897,7 @@ impl MediaViewerWindow {
|
|||
});
|
||||
install_file_drop_target(&window, file_drop_control);
|
||||
ensure_viewer_close_handler(&window);
|
||||
set_viewer_loading(&window, false);
|
||||
window.set_title(Some(&title));
|
||||
let header = gtk::HeaderBar::new();
|
||||
header.set_title_widget(Some(>k::Label::new(Some(&title))));
|
||||
|
|
@ -1752,7 +1923,7 @@ impl MediaViewerWindow {
|
|||
header.pack_end(&download_button);
|
||||
header.pack_end(&favorite_button);
|
||||
header.pack_end(&file_list_button);
|
||||
set_viewer_header(&window, &header);
|
||||
let titlebar_host = set_viewer_header(&window, &header);
|
||||
|
||||
let root = gtk::Box::new(gtk::Orientation::Vertical, 0);
|
||||
let stack = gtk::Stack::new();
|
||||
|
|
@ -1816,6 +1987,7 @@ impl MediaViewerWindow {
|
|||
|
||||
let controller = Rc::new(MediaViewerController {
|
||||
window,
|
||||
titlebar_host,
|
||||
source,
|
||||
kind,
|
||||
picture,
|
||||
|
|
@ -1845,6 +2017,10 @@ impl MediaViewerWindow {
|
|||
last_pointer_position: Cell::new(None),
|
||||
key_controller: RefCell::new(None),
|
||||
motion_controller: RefCell::new(None),
|
||||
fullscreen_key_controller: RefCell::new(None),
|
||||
fullscreen_ui_hide_timer: RefCell::new(None),
|
||||
fullscreen_ui_visible: Cell::new(false),
|
||||
fullscreen_notify_handler: RefCell::new(None),
|
||||
navigation_message,
|
||||
navigation_message_label,
|
||||
navigation_message_generation: Rc::new(Cell::new(0)),
|
||||
|
|
@ -1858,6 +2034,7 @@ impl MediaViewerWindow {
|
|||
});
|
||||
}
|
||||
controller.connect_signals();
|
||||
controller.fullscreen_changed();
|
||||
if controller.file_list_button.is_active() {
|
||||
controller.refresh_file_list();
|
||||
}
|
||||
|
|
@ -1883,6 +2060,7 @@ impl MediaViewerWindow {
|
|||
|
||||
struct MediaViewerController {
|
||||
window: gtk::ApplicationWindow,
|
||||
titlebar_host: gtk::Box,
|
||||
source: ArchiveSource,
|
||||
kind: EntryKind,
|
||||
picture: gtk::Picture,
|
||||
|
|
@ -1912,6 +2090,10 @@ struct MediaViewerController {
|
|||
last_pointer_position: Cell<Option<(f64, f64)>>,
|
||||
key_controller: RefCell<Option<gtk::EventControllerKey>>,
|
||||
motion_controller: RefCell<Option<gtk::EventControllerMotion>>,
|
||||
fullscreen_key_controller: RefCell<Option<gtk::EventControllerKey>>,
|
||||
fullscreen_ui_hide_timer: RefCell<Option<glib::SourceId>>,
|
||||
fullscreen_ui_visible: Cell<bool>,
|
||||
fullscreen_notify_handler: RefCell<Option<glib::SignalHandlerId>>,
|
||||
navigation_message: gtk::Box,
|
||||
navigation_message_label: gtk::Label,
|
||||
navigation_message_generation: Rc<Cell<u64>>,
|
||||
|
|
@ -1930,6 +2112,21 @@ impl MediaViewerController {
|
|||
self.window.add_controller(motion_controller.clone());
|
||||
self.motion_controller.replace(Some(motion_controller));
|
||||
|
||||
let fullscreen_keys = gtk::EventControllerKey::new();
|
||||
fullscreen_keys.set_propagation_phase(gtk::PropagationPhase::Capture);
|
||||
let controller = Rc::downgrade(self);
|
||||
fullscreen_keys.connect_key_pressed(move |_, _, _, _| {
|
||||
if let Some(controller) = controller.upgrade()
|
||||
&& controller.window.is_fullscreen()
|
||||
{
|
||||
controller.show_fullscreen_ui_temporarily();
|
||||
}
|
||||
glib::Propagation::Proceed
|
||||
});
|
||||
self.window.add_controller(fullscreen_keys.clone());
|
||||
self.fullscreen_key_controller
|
||||
.replace(Some(fullscreen_keys));
|
||||
|
||||
let keys = gtk::EventControllerKey::new();
|
||||
let controller = Rc::downgrade(self);
|
||||
keys.connect_key_pressed(move |_, key, _, modifiers| {
|
||||
|
|
@ -1943,7 +2140,9 @@ impl MediaViewerController {
|
|||
) {
|
||||
return glib::Propagation::Proceed;
|
||||
}
|
||||
controller.hide_seek();
|
||||
if !controller.window.is_fullscreen() {
|
||||
controller.hide_seek();
|
||||
}
|
||||
match key {
|
||||
gdk::Key::Left if controller.kind == EntryKind::Video => {
|
||||
controller.seek_video(-VIDEO_SEEK_STEP_US);
|
||||
|
|
@ -2014,6 +2213,15 @@ impl MediaViewerController {
|
|||
self.window.add_controller(keys.clone());
|
||||
self.key_controller.replace(Some(keys));
|
||||
let controller = Rc::downgrade(self);
|
||||
let handler = self
|
||||
.window
|
||||
.connect_notify_local(Some("fullscreened"), move |_, _| {
|
||||
if let Some(controller) = controller.upgrade() {
|
||||
controller.fullscreen_changed();
|
||||
}
|
||||
});
|
||||
self.fullscreen_notify_handler.replace(Some(handler));
|
||||
let controller = Rc::downgrade(self);
|
||||
self.favorite_button.connect_clicked(move |_| {
|
||||
if let Some(controller) = controller.upgrade() {
|
||||
let active = (controller.favorite_toggle)();
|
||||
|
|
@ -2032,7 +2240,11 @@ impl MediaViewerController {
|
|||
let controller = Rc::downgrade(self);
|
||||
self.file_list_button.connect_toggled(move |button| {
|
||||
if let Some(controller) = controller.upgrade() {
|
||||
controller.file_list_panel.set_visible(button.is_active());
|
||||
controller.file_list_panel.set_visible(
|
||||
button.is_active()
|
||||
&& (!controller.window.is_fullscreen()
|
||||
|| controller.fullscreen_ui_visible.get()),
|
||||
);
|
||||
if button.is_active() {
|
||||
controller.window.add_css_class("viewer-file-list-visible");
|
||||
} else {
|
||||
|
|
@ -2051,6 +2263,22 @@ impl MediaViewerController {
|
|||
controller.open_file_list_row(row.index());
|
||||
}
|
||||
});
|
||||
let file_list_typeahead = Rc::new(RefCell::new(TypeaheadBuffer::default()));
|
||||
let file_list_keys = gtk::EventControllerKey::new();
|
||||
let controller = Rc::downgrade(self);
|
||||
file_list_keys.connect_key_pressed(move |_, key, _, modifiers| {
|
||||
let Some(controller) = controller.upgrade() else {
|
||||
return glib::Propagation::Proceed;
|
||||
};
|
||||
file_list_typeahead_select(
|
||||
&controller.file_list,
|
||||
&controller.file_list_files.borrow(),
|
||||
&mut file_list_typeahead.borrow_mut(),
|
||||
key,
|
||||
modifiers,
|
||||
)
|
||||
});
|
||||
self.file_list.add_controller(file_list_keys);
|
||||
let file_list_click = gtk::GestureClick::builder().button(3).build();
|
||||
let controller = Rc::downgrade(self);
|
||||
file_list_click.connect_pressed(move |_, _, x, y| {
|
||||
|
|
@ -2318,7 +2546,11 @@ impl MediaViewerController {
|
|||
return;
|
||||
}
|
||||
self.last_pointer_position.set(Some((x, y)));
|
||||
self.show_seek_temporarily();
|
||||
if self.window.is_fullscreen() {
|
||||
self.show_fullscreen_ui_temporarily();
|
||||
} else {
|
||||
self.show_seek_temporarily();
|
||||
}
|
||||
}
|
||||
|
||||
fn hide_seek(self: &Rc<Self>) {
|
||||
|
|
@ -2377,12 +2609,22 @@ impl MediaViewerController {
|
|||
|
||||
fn prepare_for_navigation(&self) {
|
||||
self.stop_playback();
|
||||
if let Some(timer) = self.seek_hide_timer.borrow_mut().take() {
|
||||
timer.remove();
|
||||
}
|
||||
self.cancel_fullscreen_ui_timer();
|
||||
if let Some(handler) = self.fullscreen_notify_handler.borrow_mut().take() {
|
||||
self.window.disconnect(handler);
|
||||
}
|
||||
if let Some(controller) = self.key_controller.borrow_mut().take() {
|
||||
self.window.remove_controller(&controller);
|
||||
}
|
||||
if let Some(controller) = self.motion_controller.borrow_mut().take() {
|
||||
self.window.remove_controller(&controller);
|
||||
}
|
||||
if let Some(controller) = self.fullscreen_key_controller.borrow_mut().take() {
|
||||
self.window.remove_controller(&controller);
|
||||
}
|
||||
}
|
||||
|
||||
fn stop_playback(&self) {
|
||||
|
|
@ -2400,6 +2642,61 @@ impl MediaViewerController {
|
|||
}
|
||||
}
|
||||
|
||||
fn fullscreen_changed(self: &Rc<Self>) {
|
||||
self.cancel_fullscreen_ui_timer();
|
||||
self.hide_seek();
|
||||
if self.window.is_fullscreen() {
|
||||
self.set_fullscreen_ui_visible(false);
|
||||
} else {
|
||||
self.fullscreen_ui_visible.set(true);
|
||||
self.titlebar_host.set_visible(true);
|
||||
self.status_label.set_visible(true);
|
||||
self.file_list_panel
|
||||
.set_visible(self.file_list_button.is_active());
|
||||
}
|
||||
}
|
||||
|
||||
fn show_fullscreen_ui_temporarily(self: &Rc<Self>) {
|
||||
if !self.window.is_fullscreen() {
|
||||
return;
|
||||
}
|
||||
self.cancel_fullscreen_ui_timer();
|
||||
self.set_fullscreen_ui_visible(true);
|
||||
let controller = Rc::downgrade(self);
|
||||
let timer = glib::timeout_add_local_once(Duration::from_secs(5), move || {
|
||||
if let Some(controller) = controller.upgrade() {
|
||||
controller.fullscreen_ui_hide_timer.borrow_mut().take();
|
||||
controller.set_fullscreen_ui_visible(false);
|
||||
}
|
||||
});
|
||||
self.fullscreen_ui_hide_timer.replace(Some(timer));
|
||||
}
|
||||
|
||||
fn cancel_fullscreen_ui_timer(&self) {
|
||||
if let Some(timer) = self.fullscreen_ui_hide_timer.borrow_mut().take() {
|
||||
timer.remove();
|
||||
}
|
||||
}
|
||||
|
||||
fn set_fullscreen_ui_visible(self: &Rc<Self>, visible: bool) {
|
||||
self.fullscreen_ui_visible.set(visible);
|
||||
self.titlebar_host.set_visible(visible);
|
||||
self.media_seek_row.set_visible(visible);
|
||||
self.status_label.set_visible(visible);
|
||||
self.file_list_panel
|
||||
.set_visible(visible && self.file_list_button.is_active());
|
||||
self.window.queue_allocate();
|
||||
let controller = Rc::downgrade(self);
|
||||
glib::idle_add_local_once(move || {
|
||||
if let Some(controller) = controller.upgrade() {
|
||||
controller.stack.queue_allocate();
|
||||
controller.picture.queue_allocate();
|
||||
controller.video.queue_allocate();
|
||||
controller.resize_image_picture();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn load_image(self: &Rc<Self>) {
|
||||
self.stack.set_visible_child_name("image");
|
||||
self.status_label.set_text("이미지를 읽는 중...");
|
||||
|
|
@ -2458,6 +2755,85 @@ struct ViewerFileList {
|
|||
list: gtk::ListBox,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct TypeaheadBuffer {
|
||||
query: String,
|
||||
last_input: Option<Instant>,
|
||||
}
|
||||
|
||||
fn file_list_typeahead_select(
|
||||
list: >k::ListBox,
|
||||
files: &[SiblingFile],
|
||||
state: &mut TypeaheadBuffer,
|
||||
key: gdk::Key,
|
||||
modifiers: gdk::ModifierType,
|
||||
) -> glib::Propagation {
|
||||
let blocked = gdk::ModifierType::CONTROL_MASK
|
||||
| gdk::ModifierType::ALT_MASK
|
||||
| gdk::ModifierType::SUPER_MASK;
|
||||
if modifiers.intersects(blocked) {
|
||||
return glib::Propagation::Proceed;
|
||||
}
|
||||
if key == gdk::Key::Escape {
|
||||
state.query.clear();
|
||||
state.last_input = None;
|
||||
return glib::Propagation::Proceed;
|
||||
}
|
||||
if matches!(key, gdk::Key::space)
|
||||
|| key.to_unicode().is_some_and(|character| {
|
||||
character.eq_ignore_ascii_case(&'f') || matches!(character, '[' | ']')
|
||||
})
|
||||
{
|
||||
return glib::Propagation::Proceed;
|
||||
}
|
||||
let now = Instant::now();
|
||||
if state
|
||||
.last_input
|
||||
.is_none_or(|last| now.duration_since(last) > Duration::from_millis(1200))
|
||||
{
|
||||
state.query.clear();
|
||||
}
|
||||
if key == gdk::Key::BackSpace {
|
||||
return if state.query.pop().is_some() {
|
||||
state.last_input = Some(now);
|
||||
glib::Propagation::Stop
|
||||
} else {
|
||||
glib::Propagation::Proceed
|
||||
};
|
||||
}
|
||||
let Some(character) = key.to_unicode().filter(|character| !character.is_control()) else {
|
||||
return glib::Propagation::Proceed;
|
||||
};
|
||||
let input = character.to_lowercase().collect::<String>();
|
||||
let repeated_single = state.query.chars().count() == 1 && state.query == input;
|
||||
if !repeated_single {
|
||||
state.query.push_str(&input);
|
||||
}
|
||||
state.last_input = Some(now);
|
||||
let selected = list.selected_row().map(|row| row.index() as usize);
|
||||
let start = if repeated_single {
|
||||
selected.map_or(0, |index| index + 1)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let found = find_sibling_prefix(files, &state.query, start);
|
||||
if let Some(row) = found.and_then(|index| list.row_at_index(index as i32)) {
|
||||
list.select_row(Some(&row));
|
||||
let _ = row.grab_focus();
|
||||
}
|
||||
glib::Propagation::Stop
|
||||
}
|
||||
|
||||
fn find_sibling_prefix(files: &[SiblingFile], query: &str, start: usize) -> Option<usize> {
|
||||
if files.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let query = query.to_lowercase();
|
||||
(0..files.len())
|
||||
.map(|offset| (start + offset) % files.len())
|
||||
.find(|index| files[*index].name.to_lowercase().starts_with(&query))
|
||||
}
|
||||
|
||||
fn viewer_file_list() -> ViewerFileList {
|
||||
let list = gtk::ListBox::new();
|
||||
list.set_selection_mode(gtk::SelectionMode::Single);
|
||||
|
|
@ -2943,6 +3319,19 @@ mod tests {
|
|||
assert_eq!(video_volume_target(0.5, 0.05), 0.55);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn viewer_file_typeahead_wraps_by_display_name() {
|
||||
let files = ["Alpha.zip", "beta.jpg", "Bravo.mp4"].map(|name| SiblingFile {
|
||||
uri: format!("file:///{name}"),
|
||||
name: name.into(),
|
||||
kind: EntryKind::Image,
|
||||
});
|
||||
|
||||
assert_eq!(find_sibling_prefix(&files, "br", 0), Some(2));
|
||||
assert_eq!(find_sibling_prefix(&files, "b", 2), Some(2));
|
||||
assert_eq!(find_sibling_prefix(&files, "b", 3), Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn navigation_boundary_messages_match_direction() {
|
||||
assert_eq!(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue