Release v0.2.7
This commit is contained in:
parent
be02626912
commit
38ad4989b9
6 changed files with 273 additions and 65 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -182,7 +182,7 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
|||
|
||||
[[package]]
|
||||
name = "comicviewer"
|
||||
version = "0.2.6"
|
||||
version = "0.2.7"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "comicviewer"
|
||||
version = "0.2.6"
|
||||
version = "0.2.7"
|
||||
edition = "2024"
|
||||
description = "A comic archive viewer for local and remote locations"
|
||||
license = "GPL-3.0-or-later"
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@
|
|||
</provides>
|
||||
<content_rating type="oars-1.1" />
|
||||
<releases>
|
||||
<release version="0.2.7" date="2026-07-24" />
|
||||
<release version="0.2.6" date="2026-07-23" />
|
||||
<release version="0.2.5" date="2026-07-23" />
|
||||
<release version="0.2.4" date="2026-07-23" />
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ AppImage 빌드가 끝나면 다음 스크립트로 체크섬과 통합 `tar.gz`
|
|||
|
||||
스크립트는 `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.sha256`
|
||||
|
||||
|
|
@ -78,10 +78,10 @@ sha256sum -c Comicviewer-latest-x86_64.tar.gz.sha256
|
|||
|
||||
```sh
|
||||
git add Cargo.toml Cargo.lock data packaging README.md docs
|
||||
git commit -m "Release v0.2.6"
|
||||
git tag -a v0.2.6 -m "Comicviewer v0.2.6"
|
||||
git commit -m "Release v0.2.7"
|
||||
git tag -a v0.2.7 -m "Comicviewer v0.2.7"
|
||||
git push origin main
|
||||
git push origin v0.2.6
|
||||
git push origin v0.2.7
|
||||
```
|
||||
|
||||
이미 원격에 공개한 태그는 이동하거나 덮어쓰지 않습니다. 수정이 필요하면 새
|
||||
|
|
|
|||
289
src/viewer.rs
289
src/viewer.rs
|
|
@ -25,6 +25,7 @@ const IMAGE_ZOOM_MIN: f64 = 0.1;
|
|||
const IMAGE_ZOOM_MAX: f64 = 4.0;
|
||||
const VIEWER_CONTROLLER_KEY: &str = "comicviewer-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: >k::ApplicationWindow) {
|
||||
unsafe {
|
||||
|
|
@ -110,10 +111,10 @@ pub(crate) fn set_viewer_header(
|
|||
pub struct ViewerWindow;
|
||||
|
||||
pub struct SiblingNavigator {
|
||||
position: Rc<dyn Fn() -> Option<(usize, usize)>>,
|
||||
navigate: Rc<dyn Fn(SiblingTarget, gtk::ApplicationWindow) -> bool>,
|
||||
files: Rc<dyn Fn() -> Vec<SiblingFile>>,
|
||||
actions: Rc<SiblingActionResolver>,
|
||||
recursive: bool,
|
||||
}
|
||||
|
||||
type SiblingActionResolver = dyn Fn(&str) -> Option<SiblingFileActions>;
|
||||
|
|
@ -133,16 +134,16 @@ pub struct SiblingFileActions {
|
|||
|
||||
impl SiblingNavigator {
|
||||
pub fn new(
|
||||
position: Rc<dyn Fn() -> Option<(usize, usize)>>,
|
||||
navigate: Rc<dyn Fn(SiblingTarget, gtk::ApplicationWindow) -> bool>,
|
||||
files: Rc<dyn Fn() -> Vec<SiblingFile>>,
|
||||
actions: Rc<SiblingActionResolver>,
|
||||
recursive: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
position,
|
||||
navigate,
|
||||
files,
|
||||
actions,
|
||||
recursive,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -165,14 +166,14 @@ impl SiblingNavigator {
|
|||
)
|
||||
}
|
||||
|
||||
fn position(&self) -> Option<(usize, usize)> {
|
||||
(self.position)()
|
||||
}
|
||||
|
||||
fn files(&self) -> Vec<SiblingFile> {
|
||||
(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 {
|
||||
let files = self.files();
|
||||
let Some(index) = files.iter().position(|file| file.uri == uri) else {
|
||||
|
|
@ -445,6 +446,7 @@ impl ViewerWindow {
|
|||
file_download: download_control.download,
|
||||
view_mode_change: view_mode_control.change,
|
||||
seek_hide_timer: RefCell::new(None),
|
||||
seek_interacting: Cell::new(false),
|
||||
last_pointer_position: Cell::new(None),
|
||||
key_controller: RefCell::new(None),
|
||||
motion_controller: RefCell::new(None),
|
||||
|
|
@ -477,6 +479,14 @@ impl ViewerWindow {
|
|||
});
|
||||
controller.connect_signals();
|
||||
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() {
|
||||
controller.refresh_file_list();
|
||||
}
|
||||
|
|
@ -604,6 +614,7 @@ struct ViewerController {
|
|||
file_download: Rc<dyn Fn(gtk::Window, gtk::Label)>,
|
||||
view_mode_change: Rc<dyn Fn(u32)>,
|
||||
seek_hide_timer: RefCell<Option<glib::SourceId>>,
|
||||
seek_interacting: Cell<bool>,
|
||||
last_pointer_position: Cell<Option<(f64, f64)>>,
|
||||
key_controller: RefCell<Option<gtk::EventControllerKey>>,
|
||||
motion_controller: RefCell<Option<gtk::EventControllerMotion>>,
|
||||
|
|
@ -629,13 +640,13 @@ impl ViewerController {
|
|||
let controller = Rc::downgrade(self);
|
||||
self.previous_button.connect_clicked(move |_| {
|
||||
if let Some(controller) = controller.upgrade() {
|
||||
controller.navigate(false);
|
||||
controller.navigate_from_button(false);
|
||||
}
|
||||
});
|
||||
let controller = Rc::downgrade(self);
|
||||
self.next_button.connect_clicked(move |_| {
|
||||
if let Some(controller) = controller.upgrade() {
|
||||
controller.navigate(true);
|
||||
controller.navigate_from_button(true);
|
||||
}
|
||||
});
|
||||
let controller = Rc::downgrade(self);
|
||||
|
|
@ -740,6 +751,29 @@ impl ViewerController {
|
|||
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 controller = Rc::downgrade(self);
|
||||
|
|
@ -1103,12 +1137,36 @@ impl ViewerController {
|
|||
let timer = glib::timeout_add_local_once(Duration::from_secs(5), move || {
|
||||
if let Some(controller) = controller.upgrade() {
|
||||
controller.seek_hide_timer.borrow_mut().take();
|
||||
if !controller.seek_interacting.get() {
|
||||
controller.set_seek_visible(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
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) {
|
||||
if self.last_pointer_position.get() == Some((x, y)) {
|
||||
return;
|
||||
|
|
@ -1128,8 +1186,10 @@ impl ViewerController {
|
|||
if let Some(timer) = self.seek_hide_timer.borrow_mut().take() {
|
||||
timer.remove();
|
||||
}
|
||||
if !self.seek_interacting.get() {
|
||||
self.set_seek_visible(false);
|
||||
}
|
||||
}
|
||||
|
||||
fn set_seek_visible(self: &Rc<Self>, visible: bool) {
|
||||
if self.page_seek_row.is_visible() == visible {
|
||||
|
|
@ -1187,7 +1247,7 @@ impl ViewerController {
|
|||
}
|
||||
|
||||
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.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 {
|
||||
2
|
||||
} else {
|
||||
|
|
@ -1450,7 +1516,7 @@ impl ViewerController {
|
|||
};
|
||||
let mut state = self.state.borrow_mut();
|
||||
if state.pages.is_empty() {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
let next = if forward {
|
||||
state.anchor.saturating_add(step).min(state.pages.len() - 1)
|
||||
|
|
@ -1458,7 +1524,7 @@ impl ViewerController {
|
|||
state.anchor.saturating_sub(step)
|
||||
};
|
||||
if next == state.anchor {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
state.anchor = next;
|
||||
drop(state);
|
||||
|
|
@ -1467,6 +1533,7 @@ impl ViewerController {
|
|||
if self.view_mode() == ViewMode::Continuous {
|
||||
self.schedule_scroll_to_anchor();
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn seek_edge(self: &Rc<Self>, end: bool) {
|
||||
|
|
@ -2175,10 +2242,12 @@ impl MediaViewerWindow {
|
|||
.build();
|
||||
let media_seek = navigation_scale();
|
||||
let media_seek_row = navigation_scale_row(&media_seek);
|
||||
if let Some((current, count)) = sibling_navigator
|
||||
let seek_files = sibling_navigator
|
||||
.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_value(current as f64);
|
||||
media_seek.set_sensitive(count > 1);
|
||||
|
|
@ -2219,6 +2288,7 @@ impl MediaViewerWindow {
|
|||
status_label,
|
||||
media_seek,
|
||||
media_seek_row,
|
||||
seek_files: RefCell::new(seek_files),
|
||||
sibling_navigator,
|
||||
file_list_button,
|
||||
file_list_panel: file_list.panel,
|
||||
|
|
@ -2231,6 +2301,8 @@ impl MediaViewerWindow {
|
|||
seek_generation: Cell::new(0),
|
||||
seek_opening: Cell::new(false),
|
||||
seek_hide_timer: RefCell::new(None),
|
||||
seek_interacting: Cell::new(false),
|
||||
pending_seek_index: Cell::new(None),
|
||||
last_pointer_position: Cell::new(None),
|
||||
key_controller: RefCell::new(None),
|
||||
motion_controller: RefCell::new(None),
|
||||
|
|
@ -2253,6 +2325,14 @@ impl MediaViewerWindow {
|
|||
}
|
||||
controller.connect_signals();
|
||||
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() {
|
||||
controller.refresh_file_list();
|
||||
}
|
||||
|
|
@ -2294,6 +2374,7 @@ struct MediaViewerController {
|
|||
status_label: gtk::Label,
|
||||
media_seek: gtk::Scale,
|
||||
media_seek_row: gtk::Box,
|
||||
seek_files: RefCell<Vec<SiblingFile>>,
|
||||
sibling_navigator: Option<SiblingNavigator>,
|
||||
file_list_button: gtk::ToggleButton,
|
||||
file_list_panel: gtk::ScrolledWindow,
|
||||
|
|
@ -2306,6 +2387,8 @@ struct MediaViewerController {
|
|||
seek_generation: Cell<u64>,
|
||||
seek_opening: Cell<bool>,
|
||||
seek_hide_timer: RefCell<Option<glib::SourceId>>,
|
||||
seek_interacting: Cell<bool>,
|
||||
pending_seek_index: Cell<Option<usize>>,
|
||||
last_pointer_position: Cell<Option<(f64, f64)>>,
|
||||
key_controller: RefCell<Option<gtk::EventControllerKey>>,
|
||||
motion_controller: RefCell<Option<gtk::EventControllerMotion>>,
|
||||
|
|
@ -2589,17 +2672,39 @@ impl MediaViewerController {
|
|||
let Some(controller) = controller.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let Some(navigator) = &controller.sibling_navigator else {
|
||||
return;
|
||||
};
|
||||
let Some((_, count)) = navigator.position() else {
|
||||
return;
|
||||
};
|
||||
let count = controller.seek_files.borrow().len();
|
||||
let Some(index) = page_seek_index(scale.value(), count) else {
|
||||
return;
|
||||
};
|
||||
if controller.seek_interacting.get() {
|
||||
controller.pending_seek_index.set(Some(index));
|
||||
} else {
|
||||
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) {
|
||||
|
|
@ -2849,11 +2954,9 @@ impl MediaViewerController {
|
|||
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)
|
||||
{
|
||||
let seek_files = self.seek_files.borrow();
|
||||
if let Some(current) = seek_position(&seek_files, &self.source.source_uri) {
|
||||
let count = seek_files.len();
|
||||
self.media_seek
|
||||
.set_range(0.0, navigation_scale_upper(count));
|
||||
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 || {
|
||||
if let Some(controller) = controller.upgrade() {
|
||||
controller.seek_hide_timer.borrow_mut().take();
|
||||
if !controller.seek_interacting.get() {
|
||||
controller.set_seek_visible(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
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) {
|
||||
if self.last_pointer_position.get() == Some((x, y)) {
|
||||
return;
|
||||
|
|
@ -2889,8 +3035,10 @@ impl MediaViewerController {
|
|||
if let Some(timer) = self.seek_hide_timer.borrow_mut().take() {
|
||||
timer.remove();
|
||||
}
|
||||
if !self.seek_interacting.get() {
|
||||
self.set_seek_visible(false);
|
||||
}
|
||||
}
|
||||
|
||||
fn set_seek_visible(self: &Rc<Self>, visible: bool) {
|
||||
if self.media_seek_row.is_visible() == visible {
|
||||
|
|
@ -2909,14 +3057,13 @@ impl MediaViewerController {
|
|||
}
|
||||
|
||||
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;
|
||||
};
|
||||
if navigator
|
||||
.position()
|
||||
.is_none_or(|(current, _)| index == current)
|
||||
|| self.seek_opening.get()
|
||||
{
|
||||
if file.uri == self.source.source_uri || self.seek_opening.get() {
|
||||
return;
|
||||
}
|
||||
let generation = self.seek_generation.get().wrapping_add(1);
|
||||
|
|
@ -2932,14 +3079,28 @@ impl MediaViewerController {
|
|||
let Some(navigator) = &controller.sibling_navigator else {
|
||||
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.window.add_css_class(PRESERVE_SEEK_CLASS);
|
||||
if navigator.open_uri(&uri, controller.window.clone()) {
|
||||
controller.prepare_for_navigation();
|
||||
} else {
|
||||
controller.seek_opening.set(false);
|
||||
controller.window.remove_css_class(PRESERVE_SEEK_CLASS);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn prepare_for_navigation(&self) {
|
||||
self.seek_generation
|
||||
.set(self.seek_generation.get().wrapping_add(1));
|
||||
self.stop_playback();
|
||||
if let Some(timer) = self.seek_hide_timer.borrow_mut().take() {
|
||||
timer.remove();
|
||||
|
|
@ -3008,7 +3169,7 @@ impl MediaViewerController {
|
|||
}
|
||||
|
||||
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.set_fullscreen_ui_visible(false);
|
||||
}
|
||||
|
|
@ -3404,9 +3565,12 @@ fn single_picture_request(
|
|||
}
|
||||
|
||||
fn next_image_zoom(current: f64, zoom_in: bool) -> f64 {
|
||||
let direction = if zoom_in { 1.0 } else { -1.0 };
|
||||
((((current + direction * IMAGE_ZOOM_STEP) * 10.0).round()) / 10.0)
|
||||
.clamp(IMAGE_ZOOM_MIN, IMAGE_ZOOM_MAX)
|
||||
let multiplier = if zoom_in {
|
||||
1.0 + IMAGE_ZOOM_STEP
|
||||
} 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 {
|
||||
|
|
@ -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 {
|
||||
let target = timestamp.saturating_add(delta).max(0);
|
||||
if duration > 0 {
|
||||
|
|
@ -3764,10 +3943,44 @@ mod tests {
|
|||
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, 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_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]
|
||||
fn file_list_shortcut_rejects_command_modifiers() {
|
||||
assert!(!shortcut_modifiers_blocked(gdk::ModifierType::SHIFT_MASK));
|
||||
|
|
|
|||
|
|
@ -1024,21 +1024,9 @@ impl BrowserController {
|
|||
siblings: Rc<Vec<BrowserEntry>>,
|
||||
profile: LocationProfile,
|
||||
) -> SiblingNavigator {
|
||||
let recursive = siblings.iter().any(|entry| entry.list_name.is_some());
|
||||
let current_uri = entry.uri.clone();
|
||||
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 navigate_siblings = Rc::clone(&siblings);
|
||||
let navigate_current_uri = current_uri.clone();
|
||||
|
|
@ -1052,6 +1040,7 @@ impl BrowserController {
|
|||
&navigate_current_uri,
|
||||
current_kind,
|
||||
&navigate_siblings,
|
||||
recursive,
|
||||
);
|
||||
let Some(current) = siblings
|
||||
.iter()
|
||||
|
|
@ -1079,7 +1068,7 @@ impl BrowserController {
|
|||
return Vec::new();
|
||||
};
|
||||
controller
|
||||
.navigation_siblings(&files_current_uri, current_kind, &files_fallback)
|
||||
.navigation_siblings(&files_current_uri, current_kind, &files_fallback, recursive)
|
||||
.iter()
|
||||
.map(|entry| SiblingFile {
|
||||
uri: entry.uri.clone(),
|
||||
|
|
@ -1098,6 +1087,7 @@ impl BrowserController {
|
|||
&actions_current_uri,
|
||||
current_kind,
|
||||
&actions_fallback,
|
||||
recursive,
|
||||
);
|
||||
let entry = entries.iter().find(|entry| entry.uri == uri)?;
|
||||
Some(SiblingFileActions {
|
||||
|
|
@ -1105,7 +1095,7 @@ impl BrowserController {
|
|||
download: controller.download_control(entry),
|
||||
})
|
||||
});
|
||||
SiblingNavigator::new(position, navigate, files, actions)
|
||||
SiblingNavigator::new(navigate, files, actions, recursive)
|
||||
}
|
||||
|
||||
fn navigation_siblings(
|
||||
|
|
@ -1113,7 +1103,11 @@ impl BrowserController {
|
|||
current_uri: &str,
|
||||
current_kind: EntryKind,
|
||||
fallback: &Rc<Vec<BrowserEntry>>,
|
||||
fixed: bool,
|
||||
) -> Rc<Vec<BrowserEntry>> {
|
||||
if fixed {
|
||||
return Rc::clone(fallback);
|
||||
}
|
||||
let case_insensitive = self.config.borrow().case_insensitive_sort;
|
||||
let state = self.state.borrow();
|
||||
let entries = sorted_navigable_entries(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue