Release v0.2.10

This commit is contained in:
burnintuna 2026-07-24 23:44:49 +09:00
parent ae1f0c7c4a
commit f7c67c1a79
9 changed files with 87 additions and 15 deletions

View file

@ -42,7 +42,8 @@ standalone images, and videos.
entries when the search text is cleared. entries when the search text is cleared.
- Provide 100%, fit-width, fit-height, two-page, and continuous vertical viewing. - Provide 100%, fit-width, fit-height, two-page, and continuous vertical viewing.
- Support both right-to-left and left-to-right two-page layouts, plus controls - Support both right-to-left and left-to-right two-page layouts, plus controls
that advance by one page to adjust page pairing. that advance by one page to adjust page pairing. Persist the last selected
two-page direction and restore it in newly opened archive viewers.
- Reuse viewer windows when moving between ZIP, image, and video files. Preserve - Reuse viewer windows when moving between ZIP, image, and video files. Preserve
fullscreen state and keep the registered GTK titlebar widget stable while the fullscreen state and keep the registered GTK titlebar widget stable while the
displayed header changes. displayed header changes.

2
Cargo.lock generated
View file

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

View file

@ -1,6 +1,6 @@
[package] [package]
name = "comicviewer" name = "comicviewer"
version = "0.2.9" version = "0.2.10"
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.10" date="2026-07-24" />
<release version="0.2.9" date="2026-07-24" /> <release version="0.2.9" date="2026-07-24" />
<release version="0.2.8" date="2026-07-24" /> <release version="0.2.8" date="2026-07-24" />
<release version="0.2.7" date="2026-07-24" /> <release version="0.2.7" date="2026-07-24" />

View file

@ -96,6 +96,7 @@ Comicviewer는 XDG 표준 경로를 사용합니다.
- About 창의 `업데이트`: Cafe24에서 최신 번들을 내려받아 검증 후 사용자 영역에 설치 - About 창의 `업데이트`: Cafe24에서 최신 번들을 내려받아 검증 후 사용자 영역에 설치
- 이미지에서 `+`와 `-`: 현재 화면의 이미지를 임시로 10%씩 확대 또는 축소 - 이미지에서 `+`와 `-`: 현재 화면의 이미지를 임시로 10%씩 확대 또는 축소
- 이미지에서 `Ctrl` + 마우스 휠: 현재 화면의 이미지를 임시로 10%씩 확대 또는 축소 - 이미지에서 `Ctrl` + 마우스 휠: 현재 화면의 이미지를 임시로 10%씩 확대 또는 축소
- 2페이지의 `좌→우`/`우→좌` 방향은 마지막 선택을 저장해 다음 뷰어에서 복원
- 전체 화면에서 마우스 클릭: 숨겨진 컨트롤을 5초 동안 표시 - 전체 화면에서 마우스 클릭: 숨겨진 컨트롤을 5초 동안 표시
- 전체 화면에서 `←`, `→`, `↑`, `↓`: 컨트롤을 즉시 숨기고 해당 키 동작 실행 - 전체 화면에서 `←`, `→`, `↑`, `↓`: 컨트롤을 즉시 숨기고 해당 키 동작 실행
- 파일 목록의 더블클릭 또는 선택 후 `Enter`: 해당 파일 열기 - 파일 목록의 더블클릭 또는 선택 후 `Enter`: 해당 파일 열기

View file

@ -55,7 +55,7 @@ AppImage 빌드가 끝나면 다음 스크립트로 체크섬과 통합 `tar.gz`
스크립트는 `Cargo.toml`에서 버전을 읽고 다음 파일을 생성합니다. 스크립트는 `Cargo.toml`에서 버전을 읽고 다음 파일을 생성합니다.
- `dist/Comicviewer-0.2.9-x86_64.AppImage.sha256` - `dist/Comicviewer-0.2.10-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.9" git commit -m "Release v0.2.10"
git tag -a v0.2.9 -m "Comicviewer v0.2.9" git tag -a v0.2.10 -m "Comicviewer v0.2.10"
git push origin main git push origin main
git push origin v0.2.9 git push origin v0.2.10
``` ```
이미 원격에 공개한 태그는 이동하거나 덮어쓰지 않습니다. 수정이 필요하면 새 이미 원격에 공개한 태그는 이동하거나 덮어쓰지 않습니다. 수정이 필요하면 새

View file

@ -12,6 +12,7 @@ pub struct AppConfig {
pub fast_image_loading: bool, pub fast_image_loading: bool,
pub case_insensitive_sort: bool, pub case_insensitive_sort: bool,
pub viewer_mode: ViewerMode, pub viewer_mode: ViewerMode,
pub two_page_right_to_left: bool,
pub locations: Vec<LocationProfile>, pub locations: Vec<LocationProfile>,
pub favorites: Vec<FavoriteEntry>, pub favorites: Vec<FavoriteEntry>,
} }
@ -23,6 +24,7 @@ impl Default for AppConfig {
fast_image_loading: true, fast_image_loading: true,
case_insensitive_sort: true, case_insensitive_sort: true,
viewer_mode: ViewerMode::FitWidth, viewer_mode: ViewerMode::FitWidth,
two_page_right_to_left: true,
locations: Vec::new(), locations: Vec::new(),
favorites: Vec::new(), favorites: Vec::new(),
} }
@ -179,6 +181,7 @@ mod tests {
fast_image_loading: true, fast_image_loading: true,
case_insensitive_sort: true, case_insensitive_sort: true,
viewer_mode: ViewerMode::FitWidth, viewer_mode: ViewerMode::FitWidth,
two_page_right_to_left: true,
locations: vec![LocationProfile { locations: vec![LocationProfile {
name: "테스트".into(), name: "테스트".into(),
kind: LocationKind::Sftp, kind: LocationKind::Sftp,
@ -215,6 +218,7 @@ mod tests {
assert!(decoded.fast_image_loading); assert!(decoded.fast_image_loading);
assert!(decoded.case_insensitive_sort); assert!(decoded.case_insensitive_sort);
assert_eq!(decoded.viewer_mode, ViewerMode::FitWidth); assert_eq!(decoded.viewer_mode, ViewerMode::FitWidth);
assert!(decoded.two_page_right_to_left);
assert!(decoded.favorites.is_empty()); assert!(decoded.favorites.is_empty());
} }
@ -231,4 +235,17 @@ mod tests {
} }
assert_eq!(ViewerMode::from_dropdown_index(99), ViewerMode::FitWidth); assert_eq!(ViewerMode::from_dropdown_index(99), ViewerMode::FitWidth);
} }
#[test]
fn two_page_direction_defaults_to_rtl_and_round_trips_ltr() {
let existing: AppConfig = toml::from_str("cache_quota_gb = 10\n").unwrap();
assert!(existing.two_page_right_to_left);
let config = AppConfig {
two_page_right_to_left: false,
..AppConfig::default()
};
let decoded: AppConfig = toml::from_str(&toml::to_string(&config).unwrap()).unwrap();
assert!(!decoded.two_page_right_to_left);
}
} }

View file

@ -223,6 +223,11 @@ pub struct ViewModeControl {
pub change: Rc<dyn Fn(u32)>, pub change: Rc<dyn Fn(u32)>,
} }
pub struct TwoPageDirectionControl {
pub selected: u32,
pub change: Rc<dyn Fn(u32) -> bool>,
}
pub struct FileViewerControls { pub struct FileViewerControls {
pub sibling_navigator: Option<SiblingNavigator>, pub sibling_navigator: Option<SiblingNavigator>,
pub favorite_control: FavoriteControl, pub favorite_control: FavoriteControl,
@ -230,6 +235,7 @@ pub struct FileViewerControls {
pub file_drop_control: FileDropControl, pub file_drop_control: FileDropControl,
pub reuse_window: Option<gtk::ApplicationWindow>, pub reuse_window: Option<gtk::ApplicationWindow>,
pub view_mode_control: Option<ViewModeControl>, pub view_mode_control: Option<ViewModeControl>,
pub two_page_direction_control: Option<TwoPageDirectionControl>,
} }
#[derive(Clone)] #[derive(Clone)]
@ -274,7 +280,10 @@ impl ViewerWindow {
file_drop_control, file_drop_control,
reuse_window, reuse_window,
view_mode_control: _, view_mode_control: _,
two_page_direction_control,
} = controls; } = controls;
let direction_control = two_page_direction_control
.expect("archive viewer requires a two-page direction control");
let title = source.display_name.clone(); let title = source.display_name.clone();
let file_list_query = reuse_window let file_list_query = reuse_window
.as_ref() .as_ref()
@ -326,7 +335,8 @@ impl ViewerWindow {
let page_label = gtk::Label::new(Some("- / -")); let page_label = gtk::Label::new(Some("- / -"));
page_label.set_hexpand(true); page_label.set_hexpand(true);
let direction_dropdown = gtk::DropDown::from_strings(&["좌→우", "우→좌"]); let direction_dropdown = gtk::DropDown::from_strings(&["좌→우", "우→좌"]);
direction_dropdown.set_selected(1); let direction_selected = direction_control.selected.min(1);
direction_dropdown.set_selected(direction_selected);
let mode_dropdown = gtk::DropDown::from_strings(&[ let mode_dropdown = gtk::DropDown::from_strings(&[
"100%", "100%",
"폭 맞춤", "폭 맞춤",
@ -451,6 +461,8 @@ impl ViewerWindow {
download_button, download_button,
file_download: download_control.download, file_download: download_control.download,
view_mode_change: view_mode_control.change, view_mode_change: view_mode_control.change,
direction_change: direction_control.change,
direction_selected: Cell::new(direction_selected),
seek_hide_timer: RefCell::new(None), seek_hide_timer: RefCell::new(None),
seek_interacting: Cell::new(false), seek_interacting: Cell::new(false),
last_pointer_position: Cell::new(None), last_pointer_position: Cell::new(None),
@ -620,6 +632,8 @@ struct ViewerController {
download_button: gtk::Button, download_button: gtk::Button,
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)>,
direction_change: Rc<dyn Fn(u32) -> bool>,
direction_selected: Cell<u32>,
seek_hide_timer: RefCell<Option<glib::SourceId>>, seek_hide_timer: RefCell<Option<glib::SourceId>>,
seek_interacting: Cell<bool>, seek_interacting: Cell<bool>,
last_pointer_position: Cell<Option<(f64, f64)>>, last_pointer_position: Cell<Option<(f64, f64)>>,
@ -677,11 +691,18 @@ impl ViewerController {
} }
}); });
let controller = Rc::downgrade(self); let controller = Rc::downgrade(self);
self.direction_dropdown.connect_selected_notify(move |_| { self.direction_dropdown
if let Some(controller) = controller.upgrade() { .connect_selected_notify(move |dropdown| {
controller.render(); if let Some(controller) = controller.upgrade() {
} let selected = dropdown.selected().min(1);
}); if (controller.direction_change)(selected) {
controller.direction_selected.set(selected);
controller.render();
} else {
dropdown.set_selected(controller.direction_selected.get());
}
}
});
let controller = Rc::downgrade(self); let controller = Rc::downgrade(self);
self.favorite_button.connect_clicked(move |_| { self.favorite_button.connect_clicked(move |_| {
if let Some(controller) = controller.upgrade() { if let Some(controller) = controller.upgrade() {
@ -2184,6 +2205,7 @@ impl MediaViewerWindow {
file_drop_control, file_drop_control,
reuse_window, reuse_window,
view_mode_control, view_mode_control,
two_page_direction_control: _,
} = controls; } = controls;
let title = source.display_name.clone(); let title = source.display_name.clone();
let file_list_query = reuse_window let file_list_query = reuse_window

View file

@ -30,8 +30,8 @@ use crate::source::{
}; };
use crate::viewer::{ use crate::viewer::{
FavoriteControl, FileDownloadControl, FileDropControl, FileViewerControls, MediaViewerWindow, FavoriteControl, FileDownloadControl, FileDropControl, FileViewerControls, MediaViewerWindow,
SiblingFile, SiblingFileActions, SiblingNavigator, SiblingTarget, ViewModeControl, SiblingFile, SiblingFileActions, SiblingNavigator, SiblingTarget, TwoPageDirectionControl,
ViewerWindow, install_file_drop_target, set_viewer_header, set_viewer_loading, ViewModeControl, ViewerWindow, install_file_drop_target, set_viewer_header, set_viewer_loading,
}; };
use crate::zip_cache::ZipCache; use crate::zip_cache::ZipCache;
@ -997,6 +997,7 @@ impl BrowserController {
let Some(application) = controller.window.application() else { let Some(application) = controller.window.application() else {
return; return;
}; };
let direction_control = controller.two_page_direction_control();
ViewerWindow::open( ViewerWindow::open(
&application, &application,
source, source,
@ -1011,6 +1012,7 @@ impl BrowserController {
file_drop_control: controller.file_drop_control(), file_drop_control: controller.file_drop_control(),
reuse_window, reuse_window,
view_mode_control: None, view_mode_control: None,
two_page_direction_control: Some(direction_control),
}, },
); );
} }
@ -1075,6 +1077,7 @@ impl BrowserController {
file_drop_control: controller.file_drop_control(), file_drop_control: controller.file_drop_control(),
reuse_window, reuse_window,
view_mode_control, view_mode_control,
two_page_direction_control: None,
}, },
); );
} }
@ -1461,6 +1464,33 @@ impl BrowserController {
} }
} }
fn two_page_direction_control(self: &Rc<Self>) -> TwoPageDirectionControl {
let selected = u32::from(self.config.borrow().two_page_right_to_left);
let controller = Rc::downgrade(self);
TwoPageDirectionControl {
selected,
change: Rc::new(move |index| {
let Some(controller) = controller.upgrade() else {
return false;
};
let right_to_left = index == 1;
if controller.config.borrow().two_page_right_to_left == right_to_left {
return true;
}
let mut updated = controller.config.borrow().clone();
updated.two_page_right_to_left = right_to_left;
if let Err(error) = updated.save(&controller.config_path) {
controller
.status_label
.set_text(&format!("2페이지 방향을 저장할 수 없습니다: {error:#}"));
return false;
}
*controller.config.borrow_mut() = updated;
true
}),
}
}
fn toggle_favorite(&self, entry: &BrowserEntry, profile: &LocationProfile) -> bool { fn toggle_favorite(&self, entry: &BrowserEntry, profile: &LocationProfile) -> bool {
let mut updated = self.config.borrow().clone(); let mut updated = self.config.borrow().clone();
let was_active = self.is_favorite(&entry.uri); let was_active = self.is_favorite(&entry.uri);