Sync viewer navigation with browser sorting
This commit is contained in:
parent
bb73c1abea
commit
f0d5cf3496
5 changed files with 375 additions and 87 deletions
217
src/window.rs
217
src/window.rs
|
|
@ -22,7 +22,7 @@ use crate::source::{
|
|||
};
|
||||
use crate::viewer::{
|
||||
FavoriteControl, FileDownloadControl, FileViewerControls, MediaViewerWindow, SiblingNavigator,
|
||||
ViewModeControl, ViewerWindow,
|
||||
SiblingTarget, ViewModeControl, ViewerWindow,
|
||||
};
|
||||
use crate::zip_cache::ZipCache;
|
||||
|
||||
|
|
@ -94,6 +94,8 @@ impl ComicWindow {
|
|||
.build();
|
||||
let sort_dropdown = gtk::DropDown::from_strings(&["이름", "수정 시간", "생성 시간"]);
|
||||
let direction_button = gtk::ToggleButton::with_label("오름차순");
|
||||
let case_insensitive_check = gtk::CheckButton::with_label("대소문자 구분 안 함");
|
||||
case_insensitive_check.set_active(config.case_insensitive_sort);
|
||||
let fast_loading_switch = gtk::Switch::builder()
|
||||
.active(config.fast_image_loading)
|
||||
.tooltip_text("더 많은 페이지를 미리 디코딩하여 메모리에 유지")
|
||||
|
|
@ -101,6 +103,7 @@ impl ComicWindow {
|
|||
controls.append(&search_entry);
|
||||
controls.append(&sort_dropdown);
|
||||
controls.append(&direction_button);
|
||||
controls.append(&case_insensitive_check);
|
||||
controls.append(>k::Label::new(Some("빠른 이미지 로딩")));
|
||||
controls.append(&fast_loading_switch);
|
||||
|
||||
|
|
@ -157,6 +160,7 @@ impl ComicWindow {
|
|||
refresh_button,
|
||||
up_button,
|
||||
add_location_button,
|
||||
case_insensitive_check,
|
||||
fast_loading_switch,
|
||||
);
|
||||
controller.refresh();
|
||||
|
|
@ -208,6 +212,7 @@ impl BrowserController {
|
|||
refresh_button: gtk::Button,
|
||||
up_button: gtk::Button,
|
||||
add_location_button: gtk::Button,
|
||||
case_insensitive_check: gtk::CheckButton,
|
||||
fast_loading_switch: gtk::Switch,
|
||||
) {
|
||||
let controller = Rc::clone(self);
|
||||
|
|
@ -246,6 +251,10 @@ impl BrowserController {
|
|||
self.direction_button
|
||||
.connect_toggled(move |_| controller.change_sort_direction());
|
||||
let controller = Rc::clone(self);
|
||||
case_insensitive_check.connect_toggled(move |check| {
|
||||
controller.set_case_insensitive_sort(check);
|
||||
});
|
||||
let controller = Rc::clone(self);
|
||||
self.list_box
|
||||
.connect_row_activated(move |_, row| controller.activate_row(row.index()));
|
||||
|
||||
|
|
@ -406,7 +415,7 @@ impl BrowserController {
|
|||
etag: None,
|
||||
};
|
||||
let siblings = Rc::new(vec![entry.clone()]);
|
||||
controller.open_viewable(entry, siblings, 0, favorite.profile, None);
|
||||
controller.open_viewable(entry, siblings, favorite.profile, None);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -502,6 +511,7 @@ impl BrowserController {
|
|||
self.list_box.remove(&child);
|
||||
}
|
||||
let query = self.search_entry.text().to_lowercase();
|
||||
let case_insensitive = self.config.borrow().case_insensitive_sort;
|
||||
let mut state = self.state.borrow_mut();
|
||||
let mut visible_entries: Vec<_> = state
|
||||
.entries
|
||||
|
|
@ -509,7 +519,12 @@ impl BrowserController {
|
|||
.filter(|entry| entry.name.to_lowercase().contains(query.as_str()))
|
||||
.cloned()
|
||||
.collect();
|
||||
sort_entries(&mut visible_entries, state.sort_field, state.sort_direction);
|
||||
sort_entries(
|
||||
&mut visible_entries,
|
||||
state.sort_field,
|
||||
state.sort_direction,
|
||||
case_insensitive,
|
||||
);
|
||||
for entry in &visible_entries {
|
||||
self.list_box.append(&entry_row(entry));
|
||||
}
|
||||
|
|
@ -631,20 +646,20 @@ impl BrowserController {
|
|||
}
|
||||
EntryKind::ZipArchive | EntryKind::Image | EntryKind::Video => {
|
||||
let current_kind = entry.kind;
|
||||
let case_insensitive = self.config.borrow().case_insensitive_sort;
|
||||
let siblings = {
|
||||
let state = self.state.borrow();
|
||||
let mut entries = state
|
||||
.entries
|
||||
.iter()
|
||||
.filter(|entry| navigable_sibling(current_kind, entry.kind))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
sort_entries(&mut entries, state.sort_field, state.sort_direction);
|
||||
entries
|
||||
sorted_navigable_entries(
|
||||
&state.entries,
|
||||
current_kind,
|
||||
state.sort_field,
|
||||
state.sort_direction,
|
||||
case_insensitive,
|
||||
)
|
||||
};
|
||||
let siblings = Rc::new(siblings);
|
||||
if let Some(current) = siblings.iter().position(|item| item.uri == entry.uri) {
|
||||
self.open_viewable(entry, siblings, current, self.current_profile(), None);
|
||||
if siblings.iter().any(|item| item.uri == entry.uri) {
|
||||
self.open_viewable(entry, siblings, self.current_profile(), None);
|
||||
}
|
||||
}
|
||||
EntryKind::OtherFile => {}
|
||||
|
|
@ -655,14 +670,11 @@ impl BrowserController {
|
|||
self: &Rc<Self>,
|
||||
entry: BrowserEntry,
|
||||
siblings: Rc<Vec<BrowserEntry>>,
|
||||
current: usize,
|
||||
profile: LocationProfile,
|
||||
reuse_window: Option<gtk::ApplicationWindow>,
|
||||
) {
|
||||
match entry.kind {
|
||||
EntryKind::ZipArchive => {
|
||||
self.open_archive(entry, siblings, current, profile, reuse_window)
|
||||
}
|
||||
EntryKind::ZipArchive => self.open_archive(entry, siblings, profile, reuse_window),
|
||||
EntryKind::Image | EntryKind::Video => {
|
||||
let media_siblings = Rc::new(
|
||||
siblings
|
||||
|
|
@ -671,11 +683,11 @@ impl BrowserController {
|
|||
.cloned()
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
if let Some(current) = media_siblings
|
||||
if media_siblings
|
||||
.iter()
|
||||
.position(|sibling| sibling.uri == entry.uri)
|
||||
.any(|sibling| sibling.uri == entry.uri)
|
||||
{
|
||||
self.open_media(entry, media_siblings, current, profile, reuse_window);
|
||||
self.open_media(entry, media_siblings, profile, reuse_window);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
|
|
@ -684,39 +696,79 @@ impl BrowserController {
|
|||
|
||||
fn sibling_navigator(
|
||||
self: &Rc<Self>,
|
||||
entry: &BrowserEntry,
|
||||
siblings: Rc<Vec<BrowserEntry>>,
|
||||
current: usize,
|
||||
profile: LocationProfile,
|
||||
) -> SiblingNavigator {
|
||||
let controller = Rc::downgrade(self);
|
||||
let count = siblings.len();
|
||||
let open = Rc::new(move |next: usize, reuse_window: gtk::ApplicationWindow| {
|
||||
let Some(controller) = controller.upgrade() else {
|
||||
return false;
|
||||
};
|
||||
if next >= siblings.len() {
|
||||
controller
|
||||
.status_label
|
||||
.set_text("더 이상 이동할 파일이 없습니다.");
|
||||
return false;
|
||||
}
|
||||
controller.open_viewable(
|
||||
siblings[next].clone(),
|
||||
Rc::clone(&siblings),
|
||||
next,
|
||||
profile.clone(),
|
||||
Some(reuse_window),
|
||||
);
|
||||
true
|
||||
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()))
|
||||
});
|
||||
SiblingNavigator::new(current, count, open)
|
||||
|
||||
let controller = Rc::downgrade(self);
|
||||
let navigate = Rc::new(
|
||||
move |target: SiblingTarget, reuse_window: gtk::ApplicationWindow| {
|
||||
let Some(controller) = controller.upgrade() else {
|
||||
return false;
|
||||
};
|
||||
let siblings =
|
||||
controller.navigation_siblings(¤t_uri, current_kind, &siblings);
|
||||
let Some(current) = siblings.iter().position(|entry| entry.uri == current_uri)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let Some(next) = target.resolve(current, siblings.len()) else {
|
||||
return false;
|
||||
};
|
||||
controller.open_viewable(
|
||||
siblings[next].clone(),
|
||||
Rc::clone(&siblings),
|
||||
profile.clone(),
|
||||
Some(reuse_window),
|
||||
);
|
||||
true
|
||||
},
|
||||
);
|
||||
SiblingNavigator::new(position, navigate)
|
||||
}
|
||||
|
||||
fn navigation_siblings(
|
||||
&self,
|
||||
current_uri: &str,
|
||||
current_kind: EntryKind,
|
||||
fallback: &Rc<Vec<BrowserEntry>>,
|
||||
) -> Rc<Vec<BrowserEntry>> {
|
||||
let case_insensitive = self.config.borrow().case_insensitive_sort;
|
||||
let state = self.state.borrow();
|
||||
let entries = sorted_navigable_entries(
|
||||
&state.entries,
|
||||
current_kind,
|
||||
state.sort_field,
|
||||
state.sort_direction,
|
||||
case_insensitive,
|
||||
);
|
||||
drop(state);
|
||||
if entries.iter().any(|entry| entry.uri == current_uri) {
|
||||
Rc::new(entries)
|
||||
} else {
|
||||
Rc::clone(fallback)
|
||||
}
|
||||
}
|
||||
|
||||
fn open_archive(
|
||||
self: &Rc<Self>,
|
||||
entry: BrowserEntry,
|
||||
siblings: Rc<Vec<BrowserEntry>>,
|
||||
current: usize,
|
||||
profile: LocationProfile,
|
||||
reuse_window: Option<gtk::ApplicationWindow>,
|
||||
) {
|
||||
|
|
@ -727,7 +779,7 @@ impl BrowserController {
|
|||
);
|
||||
let result_label = progress_label.clone();
|
||||
let controller = Rc::clone(self);
|
||||
let navigator = self.sibling_navigator(siblings, current, profile.clone());
|
||||
let navigator = self.sibling_navigator(&entry, siblings, profile.clone());
|
||||
let favorite_control = self.favorite_control(&entry, &profile);
|
||||
let download_control = self.download_control(&entry);
|
||||
let view_mode_control = self.view_mode_control();
|
||||
|
|
@ -749,6 +801,7 @@ impl BrowserController {
|
|||
source,
|
||||
Rc::clone(&controller.database),
|
||||
controller.config.borrow().fast_image_loading,
|
||||
controller.config.borrow().case_insensitive_sort,
|
||||
view_mode_control,
|
||||
FileViewerControls {
|
||||
sibling_navigator: Some(navigator),
|
||||
|
|
@ -771,7 +824,6 @@ impl BrowserController {
|
|||
self: &Rc<Self>,
|
||||
entry: BrowserEntry,
|
||||
siblings: Rc<Vec<BrowserEntry>>,
|
||||
current: usize,
|
||||
profile: LocationProfile,
|
||||
reuse_window: Option<gtk::ApplicationWindow>,
|
||||
) {
|
||||
|
|
@ -784,7 +836,7 @@ impl BrowserController {
|
|||
);
|
||||
let result_label = progress_label.clone();
|
||||
let controller = Rc::clone(self);
|
||||
let navigator = self.sibling_navigator(siblings, current, profile.clone());
|
||||
let navigator = self.sibling_navigator(&entry, siblings, profile.clone());
|
||||
let favorite_control = self.favorite_control(&entry, &profile);
|
||||
let download_control = self.download_control(&entry);
|
||||
self.zip_cache.prepare_media(
|
||||
|
|
@ -1050,6 +1102,27 @@ impl BrowserController {
|
|||
);
|
||||
}
|
||||
|
||||
fn set_case_insensitive_sort(&self, check: >k::CheckButton) {
|
||||
let active = check.is_active();
|
||||
let previous = self.config.borrow().case_insensitive_sort;
|
||||
if active == previous {
|
||||
return;
|
||||
}
|
||||
let mut updated = self.config.borrow().clone();
|
||||
updated.case_insensitive_sort = active;
|
||||
match updated.save(&self.config_path) {
|
||||
Ok(()) => {
|
||||
*self.config.borrow_mut() = updated;
|
||||
self.render();
|
||||
}
|
||||
Err(error) => {
|
||||
self.status_label
|
||||
.set_text(&format!("정렬 설정을 저장할 수 없습니다: {error:#}"));
|
||||
check.set_active(previous);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_fast_image_loading(&self, switch: >k::Switch) {
|
||||
let active = switch.is_active();
|
||||
let previous = self.config.borrow().fast_image_loading;
|
||||
|
|
@ -1339,6 +1412,22 @@ fn navigable_sibling(current: EntryKind, candidate: EntryKind) -> bool {
|
|||
}
|
||||
}
|
||||
|
||||
fn sorted_navigable_entries(
|
||||
entries: &[BrowserEntry],
|
||||
current_kind: EntryKind,
|
||||
sort_field: SortField,
|
||||
sort_direction: SortDirection,
|
||||
case_insensitive: bool,
|
||||
) -> Vec<BrowserEntry> {
|
||||
let mut siblings = entries
|
||||
.iter()
|
||||
.filter(|entry| navigable_sibling(current_kind, entry.kind))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
sort_entries(&mut siblings, sort_field, sort_direction, case_insensitive);
|
||||
siblings
|
||||
}
|
||||
|
||||
fn favorite_browser_entry(favorite: &FavoriteEntry) -> BrowserEntry {
|
||||
BrowserEntry {
|
||||
uri: favorite.uri.clone(),
|
||||
|
|
@ -1432,6 +1521,12 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
fn media_entry(name: &str) -> BrowserEntry {
|
||||
let mut entry = entry(name);
|
||||
entry.kind = EntryKind::Image;
|
||||
entry
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typeahead_matches_case_insensitively() {
|
||||
let entries = [entry("Alpha"), entry("beta"), entry("Bravo")];
|
||||
|
|
@ -1454,6 +1549,36 @@ mod tests {
|
|||
assert!(!navigable_sibling(EntryKind::Video, EntryKind::OtherFile));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn media_navigation_recalculates_position_for_case_sorting() {
|
||||
let entries = [media_entry("a2.jpg"), media_entry("A10.jpg")];
|
||||
let insensitive = sorted_navigable_entries(
|
||||
&entries,
|
||||
EntryKind::Image,
|
||||
SortField::Name,
|
||||
SortDirection::Ascending,
|
||||
true,
|
||||
);
|
||||
let sensitive = sorted_navigable_entries(
|
||||
&entries,
|
||||
EntryKind::Image,
|
||||
SortField::Name,
|
||||
SortDirection::Ascending,
|
||||
false,
|
||||
);
|
||||
|
||||
assert_eq!(insensitive[0].name, "a2.jpg");
|
||||
assert_eq!(sensitive[0].name, "A10.jpg");
|
||||
assert_eq!(
|
||||
insensitive.iter().position(|entry| entry.name == "a2.jpg"),
|
||||
Some(0)
|
||||
);
|
||||
assert_eq!(
|
||||
sensitive.iter().position(|entry| entry.name == "a2.jpg"),
|
||||
Some(1)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn favorites_are_rendered_as_browser_paths_and_files() {
|
||||
let profile = home_profile();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue