Release v0.2.12
This commit is contained in:
parent
7d6202e0a2
commit
77d6e3b68b
7 changed files with 294 additions and 86 deletions
182
src/window.rs
182
src/window.rs
|
|
@ -125,12 +125,13 @@ impl ComicWindow {
|
|||
controls.append(>k::Label::new(Some("빠른 이미지 로딩")));
|
||||
controls.append(&fast_loading_switch);
|
||||
|
||||
let list_box = gtk::ListBox::new();
|
||||
list_box.set_selection_mode(gtk::SelectionMode::Single);
|
||||
list_box.set_activate_on_single_click(false);
|
||||
let list_selection = gtk::SingleSelection::new(None::<gio::ListModel>);
|
||||
let list_view =
|
||||
gtk::ListView::new(Some(list_selection.clone()), None::<gtk::ListItemFactory>);
|
||||
list_view.set_single_click_activate(false);
|
||||
let scrolled = gtk::ScrolledWindow::builder()
|
||||
.vexpand(true)
|
||||
.child(&list_box)
|
||||
.child(&list_view)
|
||||
.build();
|
||||
let status_label = gtk::Label::builder().xalign(0.0).build();
|
||||
|
||||
|
|
@ -152,7 +153,8 @@ impl ComicWindow {
|
|||
search_entry,
|
||||
sort_dropdown,
|
||||
direction_button,
|
||||
list_box,
|
||||
list_view,
|
||||
list_selection,
|
||||
status_label,
|
||||
state: RefCell::new(BrowserState {
|
||||
profile_index: 0,
|
||||
|
|
@ -227,7 +229,8 @@ struct BrowserController {
|
|||
search_entry: gtk::SearchEntry,
|
||||
sort_dropdown: gtk::DropDown,
|
||||
direction_button: gtk::ToggleButton,
|
||||
list_box: gtk::ListBox,
|
||||
list_view: gtk::ListView,
|
||||
list_selection: gtk::SingleSelection,
|
||||
status_label: gtk::Label,
|
||||
state: RefCell<BrowserState>,
|
||||
config: RefCell<AppConfig>,
|
||||
|
|
@ -328,16 +331,50 @@ impl BrowserController {
|
|||
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()));
|
||||
|
||||
let context_click = gtk::GestureClick::builder().button(3).build();
|
||||
let controller = Rc::clone(self);
|
||||
context_click.connect_pressed(move |_, _, x, y| {
|
||||
controller.show_context_menu(x, y);
|
||||
let factory = gtk::SignalListItemFactory::new();
|
||||
let controller = Rc::downgrade(self);
|
||||
factory.connect_setup(move |_, item| {
|
||||
let Some(item) = item.downcast_ref::<gtk::ListItem>() else {
|
||||
return;
|
||||
};
|
||||
let content = entry_content();
|
||||
item.set_child(Some(&content));
|
||||
let context_click = gtk::GestureClick::builder().button(3).build();
|
||||
let item = item.downgrade();
|
||||
let controller = controller.clone();
|
||||
let menu_parent = content.clone();
|
||||
context_click.connect_pressed(move |_, _, _, _| {
|
||||
let Some(item) = item.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let Some(controller) = controller.upgrade() else {
|
||||
return;
|
||||
};
|
||||
controller.show_context_menu(item.position(), &menu_parent);
|
||||
});
|
||||
content.add_controller(context_click);
|
||||
});
|
||||
self.list_box.add_controller(context_click);
|
||||
let controller = Rc::downgrade(self);
|
||||
factory.connect_bind(move |_, item| {
|
||||
let Some(item) = item.downcast_ref::<gtk::ListItem>() else {
|
||||
return;
|
||||
};
|
||||
let Some(content) = item.child().and_downcast::<gtk::Box>() else {
|
||||
return;
|
||||
};
|
||||
let Some(controller) = controller.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let state = controller.state.borrow();
|
||||
let Some(entry) = state.visible_entries.get(item.position() as usize) else {
|
||||
return;
|
||||
};
|
||||
bind_entry_content(&content, entry);
|
||||
});
|
||||
self.list_view.set_factory(Some(&factory));
|
||||
let controller = Rc::clone(self);
|
||||
self.list_view
|
||||
.connect_activate(move |_, position| controller.activate_row(position));
|
||||
|
||||
let key_controller = gtk::EventControllerKey::new();
|
||||
let controller = Rc::clone(self);
|
||||
|
|
@ -349,7 +386,7 @@ impl BrowserController {
|
|||
controller.typeahead(key, modifiers)
|
||||
}
|
||||
});
|
||||
self.list_box.add_controller(key_controller);
|
||||
self.list_view.add_controller(key_controller);
|
||||
|
||||
let mouse_back = gtk::GestureClick::builder()
|
||||
.button(MOUSE_BACK_BUTTON)
|
||||
|
|
@ -609,9 +646,7 @@ impl BrowserController {
|
|||
state.generation,
|
||||
)
|
||||
};
|
||||
while let Some(child) = self.list_box.first_child() {
|
||||
self.list_box.remove(&child);
|
||||
}
|
||||
self.list_selection.set_model(None::<&gio::ListModel>);
|
||||
self.path_entry.set_text(&display_uri(&uri));
|
||||
self.status_label
|
||||
.set_text("폴더를 불러오는 중... (0개의 파일/폴더를 읽음)");
|
||||
|
|
@ -657,9 +692,6 @@ impl BrowserController {
|
|||
}
|
||||
|
||||
fn render(&self) {
|
||||
while let Some(child) = self.list_box.first_child() {
|
||||
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();
|
||||
|
|
@ -675,11 +707,15 @@ impl BrowserController {
|
|||
state.sort_direction,
|
||||
case_insensitive,
|
||||
);
|
||||
for entry in &visible_entries {
|
||||
self.list_box.append(&entry_row(entry));
|
||||
}
|
||||
let count = visible_entries.len();
|
||||
let names = visible_entries
|
||||
.iter()
|
||||
.map(BrowserEntry::display_name)
|
||||
.collect::<Vec<_>>();
|
||||
let model = gtk::StringList::new(&names);
|
||||
state.visible_entries = visible_entries;
|
||||
drop(state);
|
||||
self.list_selection.set_model(Some(&model));
|
||||
self.status_label.set_text(&format!("{count}개 항목"));
|
||||
}
|
||||
|
||||
|
|
@ -712,7 +748,8 @@ impl BrowserController {
|
|||
return glib::Propagation::Proceed;
|
||||
};
|
||||
|
||||
let selected = self.list_box.selected_row().map(|row| row.index() as usize);
|
||||
let selected = (self.list_selection.selected() != gtk::INVALID_LIST_POSITION)
|
||||
.then(|| self.list_selection.selected() as usize);
|
||||
let now = Instant::now();
|
||||
let mut state = self.state.borrow_mut();
|
||||
if state
|
||||
|
|
@ -738,11 +775,9 @@ impl BrowserController {
|
|||
let found_name = found.map(|index| state.visible_entries[index].display_name().to_owned());
|
||||
drop(state);
|
||||
|
||||
if let Some(index) = found
|
||||
&& let Some(row) = self.list_box.row_at_index(index as i32)
|
||||
{
|
||||
self.list_box.select_row(Some(&row));
|
||||
let _ = row.grab_focus();
|
||||
if let Some(index) = found {
|
||||
self.list_selection.set_selected(index as u32);
|
||||
let _ = self.list_view.grab_focus();
|
||||
self.status_label.set_text(&format!(
|
||||
"'{query}'로 이동: {}",
|
||||
found_name.unwrap_or_default()
|
||||
|
|
@ -783,7 +818,7 @@ impl BrowserController {
|
|||
self.render();
|
||||
}
|
||||
|
||||
fn activate_row(self: &Rc<Self>, index: i32) {
|
||||
fn activate_row(self: &Rc<Self>, index: u32) {
|
||||
let Some(entry) = self
|
||||
.state
|
||||
.borrow()
|
||||
|
|
@ -1091,27 +1126,16 @@ impl BrowserController {
|
|||
);
|
||||
}
|
||||
|
||||
fn show_context_menu(self: &Rc<Self>, x: f64, y: f64) {
|
||||
let Some(widget) = self.list_box.pick(x, y, gtk::PickFlags::DEFAULT) else {
|
||||
return;
|
||||
};
|
||||
let Some(row) = widget
|
||||
.clone()
|
||||
.downcast::<gtk::ListBoxRow>()
|
||||
.ok()
|
||||
.or_else(|| {
|
||||
widget
|
||||
.ancestor(gtk::ListBoxRow::static_type())
|
||||
.and_then(|widget| widget.downcast::<gtk::ListBoxRow>().ok())
|
||||
})
|
||||
else {
|
||||
return;
|
||||
};
|
||||
fn show_context_menu(
|
||||
self: &Rc<Self>,
|
||||
position: u32,
|
||||
parent: &impl glib::object::IsA<gtk::Widget>,
|
||||
) {
|
||||
let Some(entry) = self
|
||||
.state
|
||||
.borrow()
|
||||
.visible_entries
|
||||
.get(row.index() as usize)
|
||||
.get(position as usize)
|
||||
.cloned()
|
||||
else {
|
||||
return;
|
||||
|
|
@ -1121,9 +1145,9 @@ impl BrowserController {
|
|||
if !can_favorite && !can_download {
|
||||
return;
|
||||
}
|
||||
self.list_box.select_row(Some(&row));
|
||||
self.list_selection.set_selected(position);
|
||||
let popover = gtk::Popover::new();
|
||||
popover.set_parent(&row);
|
||||
popover.set_parent(parent);
|
||||
let actions = gtk::Box::new(gtk::Orientation::Vertical, 4);
|
||||
actions.set_margin_top(6);
|
||||
actions.set_margin_bottom(6);
|
||||
|
|
@ -2154,37 +2178,53 @@ fn display_uri(uri: &str) -> String {
|
|||
)
|
||||
}
|
||||
|
||||
fn entry_row(entry: &BrowserEntry) -> gtk::ListBoxRow {
|
||||
let row = gtk::ListBoxRow::new();
|
||||
fn entry_content() -> gtk::Box {
|
||||
let content = gtk::Box::new(gtk::Orientation::Horizontal, 12);
|
||||
content.set_margin_top(8);
|
||||
content.set_margin_bottom(8);
|
||||
content.set_margin_start(10);
|
||||
content.set_margin_end(10);
|
||||
let icon = match entry.kind {
|
||||
content.append(>k::Image::new());
|
||||
content.append(
|
||||
>k::Label::builder()
|
||||
.hexpand(true)
|
||||
.xalign(0.0)
|
||||
.ellipsize(gtk::pango::EllipsizeMode::Middle)
|
||||
.build(),
|
||||
);
|
||||
for _ in 0..2 {
|
||||
let label = gtk::Label::new(None);
|
||||
label.set_width_chars(17);
|
||||
label.set_xalign(1.0);
|
||||
content.append(&label);
|
||||
}
|
||||
content
|
||||
}
|
||||
|
||||
fn bind_entry_content(content: >k::Box, entry: &BrowserEntry) {
|
||||
let icon_name = match entry.kind {
|
||||
EntryKind::Directory => "folder-symbolic",
|
||||
EntryKind::ZipArchive => "package-x-generic-symbolic",
|
||||
EntryKind::Image => "image-x-generic-symbolic",
|
||||
EntryKind::Video => "video-x-generic-symbolic",
|
||||
EntryKind::OtherFile => "text-x-generic-symbolic",
|
||||
};
|
||||
content.append(>k::Image::from_icon_name(icon));
|
||||
content.append(
|
||||
>k::Label::builder()
|
||||
.label(entry.display_name())
|
||||
.hexpand(true)
|
||||
.xalign(0.0)
|
||||
.ellipsize(gtk::pango::EllipsizeMode::Middle)
|
||||
.build(),
|
||||
);
|
||||
for time in [entry.modified, entry.created] {
|
||||
let label = gtk::Label::new(Some(&format_time(time)));
|
||||
label.set_width_chars(17);
|
||||
label.set_xalign(1.0);
|
||||
content.append(&label);
|
||||
}
|
||||
row.set_child(Some(&content));
|
||||
row
|
||||
let Some(icon) = content.first_child().and_downcast::<gtk::Image>() else {
|
||||
return;
|
||||
};
|
||||
icon.set_icon_name(Some(icon_name));
|
||||
let Some(name) = icon.next_sibling().and_downcast::<gtk::Label>() else {
|
||||
return;
|
||||
};
|
||||
name.set_text(entry.display_name());
|
||||
let Some(modified) = name.next_sibling().and_downcast::<gtk::Label>() else {
|
||||
return;
|
||||
};
|
||||
modified.set_text(&format_time(entry.modified));
|
||||
let Some(created) = modified.next_sibling().and_downcast::<gtk::Label>() else {
|
||||
return;
|
||||
};
|
||||
created.set_text(&format_time(entry.created));
|
||||
}
|
||||
|
||||
fn format_time(time: Option<std::time::SystemTime>) -> String {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue