diff --git a/Cargo.lock b/Cargo.lock
index 4e9026f..bd47889 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -182,7 +182,7 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]]
name = "comicviewer"
-version = "0.2.11"
+version = "0.2.12"
dependencies = [
"anyhow",
"chrono",
diff --git a/Cargo.toml b/Cargo.toml
index 962f340..58af1a5 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "comicviewer"
-version = "0.2.11"
+version = "0.2.12"
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 4d02a91..999b213 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 905fc7a..ac64840 100644
--- a/docs/RELEASING.ko.md
+++ b/docs/RELEASING.ko.md
@@ -55,7 +55,7 @@ AppImage 빌드가 끝나면 다음 스크립트로 체크섬과 통합 `tar.gz`
스크립트는 `Cargo.toml`에서 버전을 읽고 다음 파일을 생성합니다.
-- `dist/Comicviewer-0.2.11-x86_64.AppImage.sha256`
+- `dist/Comicviewer-0.2.12-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.11"
-git tag -a v0.2.11 -m "Comicviewer v0.2.11"
+git commit -m "Release v0.2.12"
+git tag -a v0.2.12 -m "Comicviewer v0.2.12"
git push origin main
-git push origin v0.2.11
+git push origin v0.2.12
```
이미 원격에 공개한 태그는 이동하거나 덮어쓰지 않습니다. 수정이 필요하면 새
diff --git a/src/source.rs b/src/source.rs
index fc9b2c2..bf6b8e2 100644
--- a/src/source.rs
+++ b/src/source.rs
@@ -16,6 +16,32 @@ const RECURSIVE_ATTRIBUTES: &str = "standard::display-name,standard::name,standa
const MAX_RECURSIVE_DIRECTORIES: usize = 10_000;
const MAX_RECURSIVE_ENTRIES: usize = 100_000;
+#[derive(Default)]
+struct EnumerationProgress {
+ seen: HashSet,
+ new_in_batch: usize,
+}
+
+impl EnumerationProgress {
+ fn begin_batch(&mut self) {
+ self.new_in_batch = 0;
+ }
+
+ fn record(&mut self, uri: String) -> bool {
+ let inserted = self.seen.insert(uri);
+ self.new_in_batch += usize::from(inserted);
+ inserted
+ }
+
+ fn repeated_batch(&self) -> bool {
+ self.new_in_batch == 0
+ }
+
+ fn len(&self) -> usize {
+ self.seen.len()
+ }
+}
+
#[derive(Clone, Copy, Default)]
pub struct RecursiveProgress {
pub directories: usize,
@@ -112,7 +138,7 @@ where
}
};
let mut entries = Vec::new();
- let mut scanned = 0;
+ let mut enumeration = EnumerationProgress::default();
loop {
let batch = enumerator
@@ -121,19 +147,21 @@ where
if batch.is_empty() {
break;
}
- scanned += batch.len();
- progress(scanned);
+ enumeration.begin_batch();
for info in batch {
+ let child = enumerator.child(&info);
+ let uri: String = info
+ .attribute_string("standard::target-uri")
+ .map_or_else(|| child.uri().into(), Into::into);
+ if !enumeration.record(uri.clone()) {
+ continue;
+ }
let kind = match info.file_type() {
gio::FileType::Directory => EntryKind::Directory,
gio::FileType::Mountable | gio::FileType::Shortcut => EntryKind::Directory,
gio::FileType::Regular => classify_file(Path::new(info.display_name().as_str())),
_ => continue,
};
- let child = enumerator.child(&info);
- let uri = info
- .attribute_string("standard::target-uri")
- .map_or_else(|| child.uri().into(), Into::into);
entries.push(BrowserEntry {
uri,
name: info.display_name().into(),
@@ -153,6 +181,10 @@ where
.map(Into::into),
});
}
+ if enumeration.repeated_batch() {
+ break;
+ }
+ progress(enumeration.len());
}
Ok(entries)
}
@@ -449,6 +481,26 @@ mod tests {
assert!(encoded_input.equal(&encoded));
}
+ #[test]
+ fn enumeration_allows_large_unique_directories_and_stops_repeated_batches() {
+ let mut enumeration = EnumerationProgress::default();
+ for batch_start in (0..330_001).step_by(128) {
+ enumeration.begin_batch();
+ for index in batch_start..(batch_start + 128).min(330_001) {
+ assert!(enumeration.record(format!("smb://server/share/file-{index}")));
+ }
+ assert!(!enumeration.repeated_batch());
+ }
+ assert_eq!(enumeration.len(), 330_001);
+
+ enumeration.begin_batch();
+ for index in 0..128 {
+ assert!(!enumeration.record(format!("smb://server/share/file-{index}")));
+ }
+ assert!(enumeration.repeated_batch());
+ assert_eq!(enumeration.len(), 330_001);
+ }
+
#[test]
fn recursively_lists_viewable_files_without_following_symlinks() {
let directory = tempfile::tempdir().unwrap();
diff --git a/src/viewer.rs b/src/viewer.rs
index b4b6a94..861094e 100644
--- a/src/viewer.rs
+++ b/src/viewer.rs
@@ -26,6 +26,7 @@ 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 VIEWER_FILE_SEARCH_KEY: &str = "comicviewer-file-search";
+const VIEWER_FILE_SCROLL_KEY: &str = "comicviewer-file-scroll";
const PRESERVE_SEEK_CLASS: &str = "viewer-preserve-seek";
fn clear_viewer_controller(window: >k::ApplicationWindow) {
@@ -289,6 +290,10 @@ impl ViewerWindow {
.as_ref()
.map(viewer_file_search_query)
.unwrap_or_default();
+ let file_list_scroll = reuse_window
+ .as_ref()
+ .map(viewer_file_scroll_position)
+ .unwrap_or_default();
if let Some(window) = reuse_window.as_ref() {
clear_viewer_controller(window);
}
@@ -305,7 +310,7 @@ impl ViewerWindow {
set_viewer_loading(&window, false);
window.set_title(Some(&title));
let header = gtk::HeaderBar::new();
- header.set_title_widget(Some(>k::Label::new(Some(&title))));
+ header.set_title_widget(Some(&viewer_title_label(&title)));
let favorite_button = gtk::Button::with_label(favorite_label(favorite_control.active));
let download_button = gtk::Button::with_label("파일 다운로드");
let file_list_button = gtk::ToggleButton::with_label("파일 목록");
@@ -454,8 +459,10 @@ impl ViewerWindow {
file_list_button,
file_list_panel: file_list.panel,
file_list_search: file_list.search,
+ file_list_scrolled: file_list.scrolled,
file_list: file_list.list,
file_list_files: RefCell::new(Vec::new()),
+ file_list_scroll_restore: Cell::new(Some(file_list_scroll)),
favorite_button,
favorite_toggle: favorite_control.toggle,
download_button,
@@ -625,8 +632,10 @@ struct ViewerController {
file_list_button: gtk::ToggleButton,
file_list_panel: gtk::Box,
file_list_search: gtk::SearchEntry,
+ file_list_scrolled: gtk::ScrolledWindow,
file_list: gtk::ListBox,
file_list_files: RefCell>,
+ file_list_scroll_restore: Cell