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]]
|
[[package]]
|
||||||
name = "comicviewer"
|
name = "comicviewer"
|
||||||
version = "0.2.6"
|
version = "0.2.7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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" />
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
이미 원격에 공개한 태그는 이동하거나 덮어쓰지 않습니다. 수정이 필요하면 새
|
이미 원격에 공개한 태그는 이동하거나 덮어쓰지 않습니다. 수정이 필요하면 새
|
||||||
|
|
|
||||||
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 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: >k::ApplicationWindow) {
|
fn clear_viewer_controller(window: >k::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));
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue