diff --git a/Cargo.lock b/Cargo.lock index 8065da6..9f8a707 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -182,7 +182,7 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] name = "comicviewer" -version = "0.2.17" +version = "0.2.18" dependencies = [ "anyhow", "chrono", diff --git a/Cargo.toml b/Cargo.toml index f8171e4..24c75e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "comicviewer" -version = "0.2.17" +version = "0.2.18" edition = "2024" description = "A comic archive viewer for local and remote locations" license = "GPL-3.0-or-later" diff --git a/data/io.github.comicviewer.Comicviewer.metainfo.xml b/data/io.github.comicviewer.Comicviewer.metainfo.xml index 9f7a740..b977e04 100644 --- a/data/io.github.comicviewer.Comicviewer.metainfo.xml +++ b/data/io.github.comicviewer.Comicviewer.metainfo.xml @@ -26,6 +26,7 @@ + diff --git a/docs/RELEASING.ko.md b/docs/RELEASING.ko.md index 8e60347..6ee436d 100644 --- a/docs/RELEASING.ko.md +++ b/docs/RELEASING.ko.md @@ -55,7 +55,7 @@ AppImage 빌드가 끝나면 다음 스크립트로 체크섬과 통합 `tar.gz` 스크립트는 `Cargo.toml`에서 버전을 읽고 다음 파일을 생성합니다. -- `dist/Comicviewer-0.2.17-x86_64.AppImage.sha256` +- `dist/Comicviewer-0.2.18-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.17" -git tag -a v0.2.17 -m "Comicviewer v0.2.17" +git commit -m "Release v0.2.18" +git tag -a v0.2.18 -m "Comicviewer v0.2.18" git push origin main -git push origin v0.2.17 +git push origin v0.2.18 ``` 이미 원격에 공개한 태그는 이동하거나 덮어쓰지 않습니다. 수정이 필요하면 새 diff --git a/src/viewer.rs b/src/viewer.rs index d0c5fbc..f7af34b 100644 --- a/src/viewer.rs +++ b/src/viewer.rs @@ -28,6 +28,7 @@ const VIEWER_CONTROLLER_KEY: &str = "comicviewer-viewer-controller"; const MEDIA_VIEWER_CONTROLLER_KEY: &str = "comicviewer-media-viewer-controller"; const VIEWER_FILE_SEARCH_KEY: &str = "comicviewer-file-search"; const VIEWER_FILE_SCROLL_KEY: &str = "comicviewer-file-scroll"; +const VIEWER_VIDEO_LOOP_KEY: &str = "comicviewer-video-loop"; const PRESERVE_SEEK_CLASS: &str = "viewer-preserve-seek"; fn clear_viewer_controller(window: >k::ApplicationWindow) { @@ -2277,6 +2278,7 @@ impl MediaViewerWindow { .as_ref() .map(viewer_file_scroll_position) .unwrap_or_default(); + let video_loop_active = reuse_window.as_ref().is_some_and(viewer_video_loop_enabled); if let Some(window) = reuse_window.as_ref() { clear_viewer_controller(window); } @@ -2300,6 +2302,7 @@ impl MediaViewerWindow { file_list_button.set_sensitive(sibling_navigator.is_some()); file_list_button.set_active(window.has_css_class("viewer-file-list-visible")); let loop_check = gtk::CheckButton::with_label("반복 재생"); + loop_check.set_active(video_loop_active); loop_check.set_visible(kind == EntryKind::Video); let image_mode_dropdown = gtk::DropDown::from_strings(&["100%", "폭 맞춤", "높이 맞춤"]); let (image_mode_selected, image_mode_change) = @@ -2453,7 +2456,9 @@ impl MediaViewerWindow { schedule_video_loop_restart(stream, &check, &restart_pending_for_timestamp); }); let stream_for_loop_check = stream.clone(); + let window_for_loop_check = controller.window.clone(); loop_check.connect_toggled(move |check| { + store_viewer_video_loop(&window_for_loop_check, check.is_active()); schedule_video_loop_restart(&stream_for_loop_check, check, &restart_pending); }); } @@ -3626,6 +3631,20 @@ fn store_viewer_file_scroll_position(window: >k::ApplicationWindow, position: } } +fn viewer_video_loop_enabled(window: >k::ApplicationWindow) -> bool { + unsafe { + window + .data::(VIEWER_VIDEO_LOOP_KEY) + .is_some_and(|active| *active.as_ref()) + } +} + +fn store_viewer_video_loop(window: >k::ApplicationWindow, active: bool) { + unsafe { + window.set_data(VIEWER_VIDEO_LOOP_KEY, active); + } +} + fn restore_viewer_file_scroll(scrolled: >k::ScrolledWindow, position: f64) { let scrolled = scrolled.clone(); glib::idle_add_local_once(move || { @@ -4093,16 +4112,13 @@ fn schedule_video_loop_restart( check: >k::CheckButton, pending: &Rc>, ) { - if !video_loop_restart_needed( + let restart_needed = video_loop_restart_needed( stream.is_ended(), stream.timestamp(), stream.duration(), check.is_active(), - ) { - pending.set(false); - return; - } - if pending.replace(true) { + ); + if !begin_video_loop_restart(pending, restart_needed) { return; } let stream = stream.clone(); @@ -4110,6 +4126,7 @@ fn schedule_video_loop_restart( let pending = Rc::clone(pending); glib::timeout_add_local_once(Duration::from_millis(50), move || { let Some(check) = check.upgrade() else { + pending.set(false); return; }; if video_loop_restart_needed( @@ -4121,12 +4138,19 @@ fn schedule_video_loop_restart( stream.pause(); stream.seek(0); stream.play(); - } else { - pending.set(false); } + pending.set(false); }); } +fn begin_video_loop_restart(pending: &Cell, restart_needed: bool) -> bool { + if !restart_needed { + pending.set(false); + return false; + } + !pending.replace(true) +} + fn initialize_video_volume(stream: >k::MediaStream) { let volume = stream.volume().clamp(0.0, 1.0); // Force the realized playback backend and GTK controls to receive the same initial value. @@ -4292,6 +4316,16 @@ mod tests { assert!(!video_loop_restart_needed(true, 10, 10, false)); } + #[test] + fn video_loop_can_restart_more_than_once() { + let pending = Cell::new(false); + assert!(begin_video_loop_restart(&pending, true)); + assert!(!begin_video_loop_restart(&pending, true)); + pending.set(false); + assert!(begin_video_loop_restart(&pending, true)); + assert!(!begin_video_loop_restart(&pending, false)); + } + #[test] fn image_pan_moves_at_most_100_pixels_and_consumes_clipped_axis() { assert_eq!(