Release v0.2.7

This commit is contained in:
burnintuna 2026-07-24 02:36:05 +09:00
parent be02626912
commit 38ad4989b9
6 changed files with 273 additions and 65 deletions

2
Cargo.lock generated
View file

@ -182,7 +182,7 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]] [[package]]
name = "comicviewer" name = "comicviewer"
version = "0.2.6" version = "0.2.7"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",

View file

@ -1,6 +1,6 @@
[package] [package]
name = "comicviewer" name = "comicviewer"
version = "0.2.6" version = "0.2.7"
edition = "2024" edition = "2024"
description = "A comic archive viewer for local and remote locations" description = "A comic archive viewer for local and remote locations"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"

View file

@ -26,6 +26,7 @@
</provides> </provides>
<content_rating type="oars-1.1" /> <content_rating type="oars-1.1" />
<releases> <releases>
<release version="0.2.7" date="2026-07-24" />
<release version="0.2.6" date="2026-07-23" /> <release version="0.2.6" date="2026-07-23" />
<release version="0.2.5" date="2026-07-23" /> <release version="0.2.5" date="2026-07-23" />
<release version="0.2.4" date="2026-07-23" /> <release version="0.2.4" date="2026-07-23" />

View file

@ -55,7 +55,7 @@ AppImage 빌드가 끝나면 다음 스크립트로 체크섬과 통합 `tar.gz`
스크립트는 `Cargo.toml`에서 버전을 읽고 다음 파일을 생성합니다. 스크립트는 `Cargo.toml`에서 버전을 읽고 다음 파일을 생성합니다.
- `dist/Comicviewer-0.2.6-x86_64.AppImage.sha256` - `dist/Comicviewer-0.2.7-x86_64.AppImage.sha256`
- `dist/Comicviewer-latest-x86_64.tar.gz` - `dist/Comicviewer-latest-x86_64.tar.gz`
- `dist/Comicviewer-latest-x86_64.tar.gz.sha256` - `dist/Comicviewer-latest-x86_64.tar.gz.sha256`
@ -78,10 +78,10 @@ sha256sum -c Comicviewer-latest-x86_64.tar.gz.sha256
```sh ```sh
git add Cargo.toml Cargo.lock data packaging README.md docs git add Cargo.toml Cargo.lock data packaging README.md docs
git commit -m "Release v0.2.6" git commit -m "Release v0.2.7"
git tag -a v0.2.6 -m "Comicviewer v0.2.6" git tag -a v0.2.7 -m "Comicviewer v0.2.7"
git push origin main git push origin main
git push origin v0.2.6 git push origin v0.2.7
``` ```
이미 원격에 공개한 태그는 이동하거나 덮어쓰지 않습니다. 수정이 필요하면 새 이미 원격에 공개한 태그는 이동하거나 덮어쓰지 않습니다. 수정이 필요하면 새

View file

@ -25,6 +25,7 @@ const IMAGE_ZOOM_MIN: f64 = 0.1;
const IMAGE_ZOOM_MAX: f64 = 4.0; const IMAGE_ZOOM_MAX: f64 = 4.0;
const VIEWER_CONTROLLER_KEY: &str = "comicviewer-viewer-controller"; const VIEWER_CONTROLLER_KEY: &str = "comicviewer-viewer-controller";
const MEDIA_VIEWER_CONTROLLER_KEY: &str = "comicviewer-media-viewer-controller"; const MEDIA_VIEWER_CONTROLLER_KEY: &str = "comicviewer-media-viewer-controller";
const PRESERVE_SEEK_CLASS: &str = "viewer-preserve-seek";
fn clear_viewer_controller(window: &gtk::ApplicationWindow) { fn clear_viewer_controller(window: &gtk::ApplicationWindow) {
unsafe { unsafe {
@ -110,10 +111,10 @@ pub(crate) fn set_viewer_header(
pub struct ViewerWindow; pub struct ViewerWindow;
pub struct SiblingNavigator { pub struct SiblingNavigator {
position: Rc<dyn Fn() -> Option<(usize, usize)>>,
navigate: Rc<dyn Fn(SiblingTarget, gtk::ApplicationWindow) -> bool>, navigate: Rc<dyn Fn(SiblingTarget, gtk::ApplicationWindow) -> bool>,
files: Rc<dyn Fn() -> Vec<SiblingFile>>, files: Rc<dyn Fn() -> Vec<SiblingFile>>,
actions: Rc<SiblingActionResolver>, actions: Rc<SiblingActionResolver>,
recursive: bool,
} }
type SiblingActionResolver = dyn Fn(&str) -> Option<SiblingFileActions>; type SiblingActionResolver = dyn Fn(&str) -> Option<SiblingFileActions>;
@ -133,16 +134,16 @@ pub struct SiblingFileActions {
impl SiblingNavigator { impl SiblingNavigator {
pub fn new( pub fn new(
position: Rc<dyn Fn() -> Option<(usize, usize)>>,
navigate: Rc<dyn Fn(SiblingTarget, gtk::ApplicationWindow) -> bool>, navigate: Rc<dyn Fn(SiblingTarget, gtk::ApplicationWindow) -> bool>,
files: Rc<dyn Fn() -> Vec<SiblingFile>>, files: Rc<dyn Fn() -> Vec<SiblingFile>>,
actions: Rc<SiblingActionResolver>, actions: Rc<SiblingActionResolver>,
recursive: bool,
) -> Self { ) -> Self {
Self { Self {
position,
navigate, navigate,
files, files,
actions, actions,
recursive,
} }
} }
@ -165,14 +166,14 @@ impl SiblingNavigator {
) )
} }
fn position(&self) -> Option<(usize, usize)> {
(self.position)()
}
fn files(&self) -> Vec<SiblingFile> { fn files(&self) -> Vec<SiblingFile> {
(self.files)() (self.files)()
} }
fn seek_files(&self, current_kind: EntryKind) -> Vec<SiblingFile> {
seek_sibling_files(self.files(), current_kind, self.recursive)
}
fn open_uri(&self, uri: &str, window: gtk::ApplicationWindow) -> bool { fn open_uri(&self, uri: &str, window: gtk::ApplicationWindow) -> bool {
let files = self.files(); let files = self.files();
let Some(index) = files.iter().position(|file| file.uri == uri) else { let Some(index) = files.iter().position(|file| file.uri == uri) else {
@ -445,6 +446,7 @@ impl ViewerWindow {
file_download: download_control.download, file_download: download_control.download,
view_mode_change: view_mode_control.change, view_mode_change: view_mode_control.change,
seek_hide_timer: RefCell::new(None), seek_hide_timer: RefCell::new(None),
seek_interacting: Cell::new(false),
last_pointer_position: Cell::new(None), last_pointer_position: Cell::new(None),
key_controller: RefCell::new(None), key_controller: RefCell::new(None),
motion_controller: RefCell::new(None), motion_controller: RefCell::new(None),
@ -477,6 +479,14 @@ impl ViewerWindow {
}); });
controller.connect_signals(); controller.connect_signals();
controller.fullscreen_changed(); controller.fullscreen_changed();
if controller.window.has_css_class(PRESERVE_SEEK_CLASS) {
controller.window.remove_css_class(PRESERVE_SEEK_CLASS);
if controller.window.is_fullscreen() {
controller.show_fullscreen_ui_temporarily();
} else {
controller.show_seek_temporarily();
}
}
if controller.file_list_button.is_active() { if controller.file_list_button.is_active() {
controller.refresh_file_list(); controller.refresh_file_list();
} }
@ -604,6 +614,7 @@ struct ViewerController {
file_download: Rc<dyn Fn(gtk::Window, gtk::Label)>, file_download: Rc<dyn Fn(gtk::Window, gtk::Label)>,
view_mode_change: Rc<dyn Fn(u32)>, view_mode_change: Rc<dyn Fn(u32)>,
seek_hide_timer: RefCell<Option<glib::SourceId>>, seek_hide_timer: RefCell<Option<glib::SourceId>>,
seek_interacting: Cell<bool>,
last_pointer_position: Cell<Option<(f64, f64)>>, last_pointer_position: Cell<Option<(f64, f64)>>,
key_controller: RefCell<Option<gtk::EventControllerKey>>, key_controller: RefCell<Option<gtk::EventControllerKey>>,
motion_controller: RefCell<Option<gtk::EventControllerMotion>>, motion_controller: RefCell<Option<gtk::EventControllerMotion>>,
@ -629,13 +640,13 @@ impl ViewerController {
let controller = Rc::downgrade(self); let controller = Rc::downgrade(self);
self.previous_button.connect_clicked(move |_| { self.previous_button.connect_clicked(move |_| {
if let Some(controller) = controller.upgrade() { if let Some(controller) = controller.upgrade() {
controller.navigate(false); controller.navigate_from_button(false);
} }
}); });
let controller = Rc::downgrade(self); let controller = Rc::downgrade(self);
self.next_button.connect_clicked(move |_| { self.next_button.connect_clicked(move |_| {
if let Some(controller) = controller.upgrade() { if let Some(controller) = controller.upgrade() {
controller.navigate(true); controller.navigate_from_button(true);
} }
}); });
let controller = Rc::downgrade(self); let controller = Rc::downgrade(self);
@ -740,6 +751,29 @@ impl ViewerController {
controller.seek_page(index); controller.seek_page(index);
} }
}); });
let seek_click = gtk::GestureClick::builder()
.button(gdk::BUTTON_PRIMARY)
.propagation_phase(gtk::PropagationPhase::Capture)
.build();
let controller = Rc::downgrade(self);
seek_click.connect_pressed(move |_, _, _, _| {
if let Some(controller) = controller.upgrade() {
controller.begin_seek_interaction();
}
});
let controller = Rc::downgrade(self);
seek_click.connect_released(move |_, _, _, _| {
if let Some(controller) = controller.upgrade() {
controller.end_seek_interaction();
}
});
let controller = Rc::downgrade(self);
seek_click.connect_cancel(move |_, _| {
if let Some(controller) = controller.upgrade() {
controller.end_seek_interaction();
}
});
self.page_seek.add_controller(seek_click);
let adjustment = self.continuous_scrolled.vadjustment(); let adjustment = self.continuous_scrolled.vadjustment();
let controller = Rc::downgrade(self); let controller = Rc::downgrade(self);
@ -1103,12 +1137,36 @@ impl ViewerController {
let timer = glib::timeout_add_local_once(Duration::from_secs(5), move || { let timer = glib::timeout_add_local_once(Duration::from_secs(5), move || {
if let Some(controller) = controller.upgrade() { if let Some(controller) = controller.upgrade() {
controller.seek_hide_timer.borrow_mut().take(); controller.seek_hide_timer.borrow_mut().take();
if !controller.seek_interacting.get() {
controller.set_seek_visible(false); controller.set_seek_visible(false);
} }
}
}); });
self.seek_hide_timer.replace(Some(timer)); self.seek_hide_timer.replace(Some(timer));
} }
fn begin_seek_interaction(self: &Rc<Self>) {
self.seek_interacting.set(true);
if let Some(timer) = self.seek_hide_timer.borrow_mut().take() {
timer.remove();
}
self.cancel_fullscreen_ui_timer();
if self.window.is_fullscreen() {
self.set_fullscreen_ui_visible(true);
} else {
self.set_seek_visible(true);
}
}
fn end_seek_interaction(self: &Rc<Self>) {
self.seek_interacting.set(false);
if self.window.is_fullscreen() {
self.show_fullscreen_ui_temporarily();
} else {
self.show_seek_temporarily();
}
}
fn pointer_moved(self: &Rc<Self>, x: f64, y: f64) { fn pointer_moved(self: &Rc<Self>, x: f64, y: f64) {
if self.last_pointer_position.get() == Some((x, y)) { if self.last_pointer_position.get() == Some((x, y)) {
return; return;
@ -1128,8 +1186,10 @@ impl ViewerController {
if let Some(timer) = self.seek_hide_timer.borrow_mut().take() { if let Some(timer) = self.seek_hide_timer.borrow_mut().take() {
timer.remove(); timer.remove();
} }
if !self.seek_interacting.get() {
self.set_seek_visible(false); self.set_seek_visible(false);
} }
}
fn set_seek_visible(self: &Rc<Self>, visible: bool) { fn set_seek_visible(self: &Rc<Self>, visible: bool) {
if self.page_seek_row.is_visible() == visible { if self.page_seek_row.is_visible() == visible {
@ -1187,7 +1247,7 @@ impl ViewerController {
} }
fn hide_fullscreen_ui_now(self: &Rc<Self>) { fn hide_fullscreen_ui_now(self: &Rc<Self>) {
if self.window.is_fullscreen() { if self.window.is_fullscreen() && !self.seek_interacting.get() {
self.cancel_fullscreen_ui_timer(); self.cancel_fullscreen_ui_timer();
self.set_fullscreen_ui_visible(false); self.set_fullscreen_ui_visible(false);
} }
@ -1442,7 +1502,13 @@ impl ViewerController {
}); });
} }
fn navigate(self: &Rc<Self>, forward: bool) { fn navigate_from_button(self: &Rc<Self>, forward: bool) {
if self.navigate(forward) && (self.image_zoom.replace(1.0) - 1.0).abs() >= f64::EPSILON {
self.apply_view_mode();
}
}
fn navigate(self: &Rc<Self>, forward: bool) -> bool {
let step = if self.view_mode() == ViewMode::TwoPage { let step = if self.view_mode() == ViewMode::TwoPage {
2 2
} else { } else {
@ -1450,7 +1516,7 @@ impl ViewerController {
}; };
let mut state = self.state.borrow_mut(); let mut state = self.state.borrow_mut();
if state.pages.is_empty() { if state.pages.is_empty() {
return; return false;
} }
let next = if forward { let next = if forward {
state.anchor.saturating_add(step).min(state.pages.len() - 1) state.anchor.saturating_add(step).min(state.pages.len() - 1)
@ -1458,7 +1524,7 @@ impl ViewerController {
state.anchor.saturating_sub(step) state.anchor.saturating_sub(step)
}; };
if next == state.anchor { if next == state.anchor {
return; return false;
} }
state.anchor = next; state.anchor = next;
drop(state); drop(state);
@ -1467,6 +1533,7 @@ impl ViewerController {
if self.view_mode() == ViewMode::Continuous { if self.view_mode() == ViewMode::Continuous {
self.schedule_scroll_to_anchor(); self.schedule_scroll_to_anchor();
} }
true
} }
fn seek_edge(self: &Rc<Self>, end: bool) { fn seek_edge(self: &Rc<Self>, end: bool) {
@ -2175,10 +2242,12 @@ impl MediaViewerWindow {
.build(); .build();
let media_seek = navigation_scale(); let media_seek = navigation_scale();
let media_seek_row = navigation_scale_row(&media_seek); let media_seek_row = navigation_scale_row(&media_seek);
if let Some((current, count)) = sibling_navigator let seek_files = sibling_navigator
.as_ref() .as_ref()
.and_then(SiblingNavigator::position) .map(|navigator| navigator.seek_files(kind))
{ .unwrap_or_default();
if let Some(current) = seek_position(&seek_files, &source.source_uri) {
let count = seek_files.len();
media_seek.set_range(0.0, navigation_scale_upper(count)); media_seek.set_range(0.0, navigation_scale_upper(count));
media_seek.set_value(current as f64); media_seek.set_value(current as f64);
media_seek.set_sensitive(count > 1); media_seek.set_sensitive(count > 1);
@ -2219,6 +2288,7 @@ impl MediaViewerWindow {
status_label, status_label,
media_seek, media_seek,
media_seek_row, media_seek_row,
seek_files: RefCell::new(seek_files),
sibling_navigator, sibling_navigator,
file_list_button, file_list_button,
file_list_panel: file_list.panel, file_list_panel: file_list.panel,
@ -2231,6 +2301,8 @@ impl MediaViewerWindow {
seek_generation: Cell::new(0), seek_generation: Cell::new(0),
seek_opening: Cell::new(false), seek_opening: Cell::new(false),
seek_hide_timer: RefCell::new(None), seek_hide_timer: RefCell::new(None),
seek_interacting: Cell::new(false),
pending_seek_index: Cell::new(None),
last_pointer_position: Cell::new(None), last_pointer_position: Cell::new(None),
key_controller: RefCell::new(None), key_controller: RefCell::new(None),
motion_controller: RefCell::new(None), motion_controller: RefCell::new(None),
@ -2253,6 +2325,14 @@ impl MediaViewerWindow {
} }
controller.connect_signals(); controller.connect_signals();
controller.fullscreen_changed(); controller.fullscreen_changed();
if controller.window.has_css_class(PRESERVE_SEEK_CLASS) {
controller.window.remove_css_class(PRESERVE_SEEK_CLASS);
if controller.window.is_fullscreen() {
controller.show_fullscreen_ui_temporarily();
} else {
controller.show_seek_temporarily();
}
}
if controller.file_list_button.is_active() { if controller.file_list_button.is_active() {
controller.refresh_file_list(); controller.refresh_file_list();
} }
@ -2294,6 +2374,7 @@ struct MediaViewerController {
status_label: gtk::Label, status_label: gtk::Label,
media_seek: gtk::Scale, media_seek: gtk::Scale,
media_seek_row: gtk::Box, media_seek_row: gtk::Box,
seek_files: RefCell<Vec<SiblingFile>>,
sibling_navigator: Option<SiblingNavigator>, sibling_navigator: Option<SiblingNavigator>,
file_list_button: gtk::ToggleButton, file_list_button: gtk::ToggleButton,
file_list_panel: gtk::ScrolledWindow, file_list_panel: gtk::ScrolledWindow,
@ -2306,6 +2387,8 @@ struct MediaViewerController {
seek_generation: Cell<u64>, seek_generation: Cell<u64>,
seek_opening: Cell<bool>, seek_opening: Cell<bool>,
seek_hide_timer: RefCell<Option<glib::SourceId>>, seek_hide_timer: RefCell<Option<glib::SourceId>>,
seek_interacting: Cell<bool>,
pending_seek_index: Cell<Option<usize>>,
last_pointer_position: Cell<Option<(f64, f64)>>, last_pointer_position: Cell<Option<(f64, f64)>>,
key_controller: RefCell<Option<gtk::EventControllerKey>>, key_controller: RefCell<Option<gtk::EventControllerKey>>,
motion_controller: RefCell<Option<gtk::EventControllerMotion>>, motion_controller: RefCell<Option<gtk::EventControllerMotion>>,
@ -2589,17 +2672,39 @@ impl MediaViewerController {
let Some(controller) = controller.upgrade() else { let Some(controller) = controller.upgrade() else {
return; return;
}; };
let Some(navigator) = &controller.sibling_navigator else { let count = controller.seek_files.borrow().len();
return;
};
let Some((_, count)) = navigator.position() else {
return;
};
let Some(index) = page_seek_index(scale.value(), count) else { let Some(index) = page_seek_index(scale.value(), count) else {
return; return;
}; };
if controller.seek_interacting.get() {
controller.pending_seek_index.set(Some(index));
} else {
controller.schedule_seek(index); controller.schedule_seek(index);
}
}); });
let seek_click = gtk::GestureClick::builder()
.button(gdk::BUTTON_PRIMARY)
.propagation_phase(gtk::PropagationPhase::Capture)
.build();
let controller = Rc::downgrade(self);
seek_click.connect_pressed(move |_, _, _, _| {
if let Some(controller) = controller.upgrade() {
controller.begin_seek_interaction();
}
});
let controller = Rc::downgrade(self);
seek_click.connect_released(move |_, _, _, _| {
if let Some(controller) = controller.upgrade() {
controller.end_seek_interaction();
}
});
let controller = Rc::downgrade(self);
seek_click.connect_cancel(move |_, _| {
if let Some(controller) = controller.upgrade() {
controller.cancel_seek_interaction();
}
});
self.media_seek.add_controller(seek_click);
} }
fn refresh_file_list(&self) { fn refresh_file_list(&self) {
@ -2849,11 +2954,9 @@ impl MediaViewerController {
if let Some(timer) = self.seek_hide_timer.borrow_mut().take() { if let Some(timer) = self.seek_hide_timer.borrow_mut().take() {
timer.remove(); timer.remove();
} }
if let Some((current, count)) = self let seek_files = self.seek_files.borrow();
.sibling_navigator if let Some(current) = seek_position(&seek_files, &self.source.source_uri) {
.as_ref() let count = seek_files.len();
.and_then(SiblingNavigator::position)
{
self.media_seek self.media_seek
.set_range(0.0, navigation_scale_upper(count)); .set_range(0.0, navigation_scale_upper(count));
self.media_seek.set_sensitive(count > 1); self.media_seek.set_sensitive(count > 1);
@ -2864,12 +2967,55 @@ impl MediaViewerController {
let timer = glib::timeout_add_local_once(Duration::from_secs(5), move || { let timer = glib::timeout_add_local_once(Duration::from_secs(5), move || {
if let Some(controller) = controller.upgrade() { if let Some(controller) = controller.upgrade() {
controller.seek_hide_timer.borrow_mut().take(); controller.seek_hide_timer.borrow_mut().take();
if !controller.seek_interacting.get() {
controller.set_seek_visible(false); controller.set_seek_visible(false);
} }
}
}); });
self.seek_hide_timer.replace(Some(timer)); self.seek_hide_timer.replace(Some(timer));
} }
fn begin_seek_interaction(self: &Rc<Self>) {
self.seek_interacting.set(true);
self.pending_seek_index.set(None);
self.seek_generation
.set(self.seek_generation.get().wrapping_add(1));
if let Some(timer) = self.seek_hide_timer.borrow_mut().take() {
timer.remove();
}
self.cancel_fullscreen_ui_timer();
if self.window.is_fullscreen() {
self.set_fullscreen_ui_visible(true);
} else {
self.set_seek_visible(true);
}
}
fn end_seek_interaction(self: &Rc<Self>) {
if !self.seek_interacting.replace(false) {
return;
}
if self.window.is_fullscreen() {
self.show_fullscreen_ui_temporarily();
} else {
self.show_seek_temporarily();
}
if let Some(index) = self.pending_seek_index.take() {
self.schedule_seek(index);
}
}
fn cancel_seek_interaction(self: &Rc<Self>) {
self.pending_seek_index.set(None);
if self.seek_interacting.replace(false) {
if self.window.is_fullscreen() {
self.show_fullscreen_ui_temporarily();
} else {
self.show_seek_temporarily();
}
}
}
fn pointer_moved(self: &Rc<Self>, x: f64, y: f64) { fn pointer_moved(self: &Rc<Self>, x: f64, y: f64) {
if self.last_pointer_position.get() == Some((x, y)) { if self.last_pointer_position.get() == Some((x, y)) {
return; return;
@ -2889,8 +3035,10 @@ impl MediaViewerController {
if let Some(timer) = self.seek_hide_timer.borrow_mut().take() { if let Some(timer) = self.seek_hide_timer.borrow_mut().take() {
timer.remove(); timer.remove();
} }
if !self.seek_interacting.get() {
self.set_seek_visible(false); self.set_seek_visible(false);
} }
}
fn set_seek_visible(self: &Rc<Self>, visible: bool) { fn set_seek_visible(self: &Rc<Self>, visible: bool) {
if self.media_seek_row.is_visible() == visible { if self.media_seek_row.is_visible() == visible {
@ -2909,14 +3057,13 @@ impl MediaViewerController {
} }
fn schedule_seek(self: &Rc<Self>, index: usize) { fn schedule_seek(self: &Rc<Self>, index: usize) {
let Some(navigator) = &self.sibling_navigator else { if self.sibling_navigator.is_none() {
return;
}
let Some(file) = self.seek_files.borrow().get(index).cloned() else {
return; return;
}; };
if navigator if file.uri == self.source.source_uri || self.seek_opening.get() {
.position()
.is_none_or(|(current, _)| index == current)
|| self.seek_opening.get()
{
return; return;
} }
let generation = self.seek_generation.get().wrapping_add(1); let generation = self.seek_generation.get().wrapping_add(1);
@ -2932,14 +3079,28 @@ impl MediaViewerController {
let Some(navigator) = &controller.sibling_navigator else { let Some(navigator) = &controller.sibling_navigator else {
return; return;
}; };
if navigator.open_index(index, controller.window.clone()) { let Some(uri) = controller
.seek_files
.borrow()
.get(index)
.map(|file| file.uri.clone())
else {
return;
};
controller.seek_opening.set(true); controller.seek_opening.set(true);
controller.window.add_css_class(PRESERVE_SEEK_CLASS);
if navigator.open_uri(&uri, controller.window.clone()) {
controller.prepare_for_navigation(); controller.prepare_for_navigation();
} else {
controller.seek_opening.set(false);
controller.window.remove_css_class(PRESERVE_SEEK_CLASS);
} }
}); });
} }
fn prepare_for_navigation(&self) { fn prepare_for_navigation(&self) {
self.seek_generation
.set(self.seek_generation.get().wrapping_add(1));
self.stop_playback(); self.stop_playback();
if let Some(timer) = self.seek_hide_timer.borrow_mut().take() { if let Some(timer) = self.seek_hide_timer.borrow_mut().take() {
timer.remove(); timer.remove();
@ -3008,7 +3169,7 @@ impl MediaViewerController {
} }
fn hide_fullscreen_ui_now(self: &Rc<Self>) { fn hide_fullscreen_ui_now(self: &Rc<Self>) {
if self.window.is_fullscreen() { if self.window.is_fullscreen() && !self.seek_interacting.get() {
self.cancel_fullscreen_ui_timer(); self.cancel_fullscreen_ui_timer();
self.set_fullscreen_ui_visible(false); self.set_fullscreen_ui_visible(false);
} }
@ -3404,9 +3565,12 @@ fn single_picture_request(
} }
fn next_image_zoom(current: f64, zoom_in: bool) -> f64 { fn next_image_zoom(current: f64, zoom_in: bool) -> f64 {
let direction = if zoom_in { 1.0 } else { -1.0 }; let multiplier = if zoom_in {
((((current + direction * IMAGE_ZOOM_STEP) * 10.0).round()) / 10.0) 1.0 + IMAGE_ZOOM_STEP
.clamp(IMAGE_ZOOM_MIN, IMAGE_ZOOM_MAX) } else {
1.0 - IMAGE_ZOOM_STEP
};
((current * multiplier * 10_000.0).round() / 10_000.0).clamp(IMAGE_ZOOM_MIN, IMAGE_ZOOM_MAX)
} }
fn scaled_dimension(value: i32, zoom: f64) -> i32 { fn scaled_dimension(value: i32, zoom: f64) -> i32 {
@ -3599,6 +3763,21 @@ fn sibling_index(current: usize, count: usize, forward: bool) -> Option<usize> {
} }
} }
fn seek_sibling_files(
mut files: Vec<SiblingFile>,
current_kind: EntryKind,
recursive: bool,
) -> Vec<SiblingFile> {
if current_kind == EntryKind::Image && !recursive {
files.retain(|file| file.kind != EntryKind::ZipArchive);
}
files
}
fn seek_position(files: &[SiblingFile], current_uri: &str) -> Option<usize> {
files.iter().position(|file| file.uri == current_uri)
}
fn video_seek_target(timestamp: i64, duration: i64, delta: i64) -> i64 { fn video_seek_target(timestamp: i64, duration: i64, delta: i64) -> i64 {
let target = timestamp.saturating_add(delta).max(0); let target = timestamp.saturating_add(delta).max(0);
if duration > 0 { if duration > 0 {
@ -3764,10 +3943,44 @@ mod tests {
fn image_zoom_changes_by_ten_percent_and_clamps() { fn image_zoom_changes_by_ten_percent_and_clamps() {
assert_eq!(next_image_zoom(1.0, true), 1.1); assert_eq!(next_image_zoom(1.0, true), 1.1);
assert_eq!(next_image_zoom(1.0, false), 0.9); assert_eq!(next_image_zoom(1.0, false), 0.9);
assert_eq!(next_image_zoom(1.1, true), 1.21);
assert_eq!(next_image_zoom(1.1, false), 0.99);
assert_eq!(next_image_zoom(IMAGE_ZOOM_MAX, true), IMAGE_ZOOM_MAX); assert_eq!(next_image_zoom(IMAGE_ZOOM_MAX, true), IMAGE_ZOOM_MAX);
assert_eq!(next_image_zoom(IMAGE_ZOOM_MIN, false), IMAGE_ZOOM_MIN); assert_eq!(next_image_zoom(IMAGE_ZOOM_MIN, false), IMAGE_ZOOM_MIN);
} }
#[test]
fn ordinary_image_seek_excludes_zip_but_recursive_seek_keeps_it() {
let files = vec![
SiblingFile {
uri: "file:///book.zip".into(),
name: "book.zip".into(),
kind: EntryKind::ZipArchive,
},
SiblingFile {
uri: "file:///page.jpg".into(),
name: "page.jpg".into(),
kind: EntryKind::Image,
},
SiblingFile {
uri: "file:///clip.mp4".into(),
name: "clip.mp4".into(),
kind: EntryKind::Video,
},
];
let ordinary = seek_sibling_files(files.clone(), EntryKind::Image, false);
assert_eq!(ordinary.len(), 2);
assert!(
!ordinary
.iter()
.any(|file| file.kind == EntryKind::ZipArchive)
);
assert_eq!(seek_position(&ordinary, "file:///page.jpg"), Some(0));
assert_eq!(seek_position(&ordinary, "file:///clip.mp4"), Some(1));
assert_eq!(seek_sibling_files(files, EntryKind::Image, true).len(), 3);
}
#[test] #[test]
fn file_list_shortcut_rejects_command_modifiers() { fn file_list_shortcut_rejects_command_modifiers() {
assert!(!shortcut_modifiers_blocked(gdk::ModifierType::SHIFT_MASK)); assert!(!shortcut_modifiers_blocked(gdk::ModifierType::SHIFT_MASK));

View file

@ -1024,21 +1024,9 @@ impl BrowserController {
siblings: Rc<Vec<BrowserEntry>>, siblings: Rc<Vec<BrowserEntry>>,
profile: LocationProfile, profile: LocationProfile,
) -> SiblingNavigator { ) -> SiblingNavigator {
let recursive = siblings.iter().any(|entry| entry.list_name.is_some());
let current_uri = entry.uri.clone(); let current_uri = entry.uri.clone();
let current_kind = entry.kind; let current_kind = entry.kind;
let position_controller = Rc::downgrade(self);
let position_uri = current_uri.clone();
let position_fallback = Rc::clone(&siblings);
let position = Rc::new(move || {
let controller = position_controller.upgrade()?;
let siblings =
controller.navigation_siblings(&position_uri, current_kind, &position_fallback);
siblings
.iter()
.position(|entry| entry.uri == position_uri)
.map(|current| (current, siblings.len()))
});
let controller = Rc::downgrade(self); let controller = Rc::downgrade(self);
let navigate_siblings = Rc::clone(&siblings); let navigate_siblings = Rc::clone(&siblings);
let navigate_current_uri = current_uri.clone(); let navigate_current_uri = current_uri.clone();
@ -1052,6 +1040,7 @@ impl BrowserController {
&navigate_current_uri, &navigate_current_uri,
current_kind, current_kind,
&navigate_siblings, &navigate_siblings,
recursive,
); );
let Some(current) = siblings let Some(current) = siblings
.iter() .iter()
@ -1079,7 +1068,7 @@ impl BrowserController {
return Vec::new(); return Vec::new();
}; };
controller controller
.navigation_siblings(&files_current_uri, current_kind, &files_fallback) .navigation_siblings(&files_current_uri, current_kind, &files_fallback, recursive)
.iter() .iter()
.map(|entry| SiblingFile { .map(|entry| SiblingFile {
uri: entry.uri.clone(), uri: entry.uri.clone(),
@ -1098,6 +1087,7 @@ impl BrowserController {
&actions_current_uri, &actions_current_uri,
current_kind, current_kind,
&actions_fallback, &actions_fallback,
recursive,
); );
let entry = entries.iter().find(|entry| entry.uri == uri)?; let entry = entries.iter().find(|entry| entry.uri == uri)?;
Some(SiblingFileActions { Some(SiblingFileActions {
@ -1105,7 +1095,7 @@ impl BrowserController {
download: controller.download_control(entry), download: controller.download_control(entry),
}) })
}); });
SiblingNavigator::new(position, navigate, files, actions) SiblingNavigator::new(navigate, files, actions, recursive)
} }
fn navigation_siblings( fn navigation_siblings(
@ -1113,7 +1103,11 @@ impl BrowserController {
current_uri: &str, current_uri: &str,
current_kind: EntryKind, current_kind: EntryKind,
fallback: &Rc<Vec<BrowserEntry>>, fallback: &Rc<Vec<BrowserEntry>>,
fixed: bool,
) -> Rc<Vec<BrowserEntry>> { ) -> Rc<Vec<BrowserEntry>> {
if fixed {
return Rc::clone(fallback);
}
let case_insensitive = self.config.borrow().case_insensitive_sort; let case_insensitive = self.config.borrow().case_insensitive_sort;
let state = self.state.borrow(); let state = self.state.borrow();
let entries = sorted_navigable_entries( let entries = sorted_navigable_entries(