Improve viewer controls and navigation
This commit is contained in:
parent
b139395067
commit
ffa8bc93ba
2 changed files with 477 additions and 16 deletions
412
src/viewer.rs
412
src/viewer.rs
|
|
@ -42,6 +42,17 @@ impl SiblingNavigator {
|
||||||
(self.navigate)(SiblingTarget::Index(index), window)
|
(self.navigate)(SiblingTarget::Index(index), window)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn open_edge(&self, end: bool, window: gtk::ApplicationWindow) -> bool {
|
||||||
|
(self.navigate)(
|
||||||
|
if end {
|
||||||
|
SiblingTarget::Last
|
||||||
|
} else {
|
||||||
|
SiblingTarget::First
|
||||||
|
},
|
||||||
|
window,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn position(&self) -> Option<(usize, usize)> {
|
fn position(&self) -> Option<(usize, usize)> {
|
||||||
(self.position)()
|
(self.position)()
|
||||||
}
|
}
|
||||||
|
|
@ -51,6 +62,8 @@ impl SiblingNavigator {
|
||||||
pub enum SiblingTarget {
|
pub enum SiblingTarget {
|
||||||
Relative(bool),
|
Relative(bool),
|
||||||
Index(usize),
|
Index(usize),
|
||||||
|
First,
|
||||||
|
Last,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SiblingTarget {
|
impl SiblingTarget {
|
||||||
|
|
@ -58,6 +71,8 @@ impl SiblingTarget {
|
||||||
match self {
|
match self {
|
||||||
Self::Relative(forward) => sibling_index(current, count, forward),
|
Self::Relative(forward) => sibling_index(current, count, forward),
|
||||||
Self::Index(index) => (index < count && index != current).then_some(index),
|
Self::Index(index) => (index < count && index != current).then_some(index),
|
||||||
|
Self::First => (count > 0 && current > 0).then_some(0),
|
||||||
|
Self::Last => count.checked_sub(1).filter(|last| current < *last),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -81,6 +96,7 @@ pub struct FileViewerControls {
|
||||||
pub favorite_control: FavoriteControl,
|
pub favorite_control: FavoriteControl,
|
||||||
pub download_control: FileDownloadControl,
|
pub download_control: FileDownloadControl,
|
||||||
pub reuse_window: Option<gtk::ApplicationWindow>,
|
pub reuse_window: Option<gtk::ApplicationWindow>,
|
||||||
|
pub view_mode_control: Option<ViewModeControl>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ViewerWindow {
|
impl ViewerWindow {
|
||||||
|
|
@ -98,6 +114,7 @@ impl ViewerWindow {
|
||||||
favorite_control,
|
favorite_control,
|
||||||
download_control,
|
download_control,
|
||||||
reuse_window,
|
reuse_window,
|
||||||
|
view_mode_control: _,
|
||||||
} = controls;
|
} = controls;
|
||||||
let title = source.display_name.clone();
|
let title = source.display_name.clone();
|
||||||
let window = reuse_window.unwrap_or_else(|| {
|
let window = reuse_window.unwrap_or_else(|| {
|
||||||
|
|
@ -154,10 +171,12 @@ impl ViewerWindow {
|
||||||
toolbar.append(&mode_dropdown);
|
toolbar.append(&mode_dropdown);
|
||||||
|
|
||||||
let single_picture = page_picture();
|
let single_picture = page_picture();
|
||||||
|
let single_box = gtk::Box::new(gtk::Orientation::Horizontal, 0);
|
||||||
|
single_box.append(&single_picture);
|
||||||
let single_scrolled = gtk::ScrolledWindow::builder()
|
let single_scrolled = gtk::ScrolledWindow::builder()
|
||||||
.hexpand(true)
|
.hexpand(true)
|
||||||
.vexpand(true)
|
.vexpand(true)
|
||||||
.child(&single_picture)
|
.child(&single_box)
|
||||||
.build();
|
.build();
|
||||||
let left_picture = page_picture();
|
let left_picture = page_picture();
|
||||||
let right_picture = page_picture();
|
let right_picture = page_picture();
|
||||||
|
|
@ -212,6 +231,8 @@ impl ViewerWindow {
|
||||||
source,
|
source,
|
||||||
database,
|
database,
|
||||||
stack,
|
stack,
|
||||||
|
single_scrolled,
|
||||||
|
single_box,
|
||||||
single_picture,
|
single_picture,
|
||||||
left_picture,
|
left_picture,
|
||||||
right_picture,
|
right_picture,
|
||||||
|
|
@ -244,6 +265,7 @@ impl ViewerWindow {
|
||||||
navigation_message_label,
|
navigation_message_label,
|
||||||
navigation_message_generation: Rc::new(Cell::new(0)),
|
navigation_message_generation: Rc::new(Cell::new(0)),
|
||||||
continuous_programmatic_scroll: Cell::new(false),
|
continuous_programmatic_scroll: Cell::new(false),
|
||||||
|
view_layout_generation: Cell::new(0),
|
||||||
state: RefCell::new(ViewerState {
|
state: RefCell::new(ViewerState {
|
||||||
pages: Vec::new(),
|
pages: Vec::new(),
|
||||||
anchor: 0,
|
anchor: 0,
|
||||||
|
|
@ -303,6 +325,8 @@ struct ViewerController {
|
||||||
source: ArchiveSource,
|
source: ArchiveSource,
|
||||||
database: Rc<Database>,
|
database: Rc<Database>,
|
||||||
stack: gtk::Stack,
|
stack: gtk::Stack,
|
||||||
|
single_scrolled: gtk::ScrolledWindow,
|
||||||
|
single_box: gtk::Box,
|
||||||
single_picture: gtk::Picture,
|
single_picture: gtk::Picture,
|
||||||
left_picture: gtk::Picture,
|
left_picture: gtk::Picture,
|
||||||
right_picture: gtk::Picture,
|
right_picture: gtk::Picture,
|
||||||
|
|
@ -335,6 +359,7 @@ struct ViewerController {
|
||||||
navigation_message_label: gtk::Label,
|
navigation_message_label: gtk::Label,
|
||||||
navigation_message_generation: Rc<Cell<u64>>,
|
navigation_message_generation: Rc<Cell<u64>>,
|
||||||
continuous_programmatic_scroll: Cell<bool>,
|
continuous_programmatic_scroll: Cell<bool>,
|
||||||
|
view_layout_generation: Cell<u64>,
|
||||||
state: RefCell<ViewerState>,
|
state: RefCell<ViewerState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -390,6 +415,16 @@ impl ViewerController {
|
||||||
.connect_notify_local(Some("width"), move |_, _| {
|
.connect_notify_local(Some("width"), move |_, _| {
|
||||||
controller.resize_continuous_rows();
|
controller.resize_continuous_rows();
|
||||||
});
|
});
|
||||||
|
let controller = Rc::clone(self);
|
||||||
|
self.single_scrolled
|
||||||
|
.connect_notify_local(Some("width"), move |_, _| {
|
||||||
|
controller.resize_single_picture();
|
||||||
|
});
|
||||||
|
let controller = Rc::clone(self);
|
||||||
|
self.single_picture
|
||||||
|
.connect_notify_local(Some("width"), move |_, _| {
|
||||||
|
controller.resize_single_picture();
|
||||||
|
});
|
||||||
|
|
||||||
let scroll_controller = gtk::EventControllerScroll::new(
|
let scroll_controller = gtk::EventControllerScroll::new(
|
||||||
gtk::EventControllerScrollFlags::VERTICAL | gtk::EventControllerScrollFlags::DISCRETE,
|
gtk::EventControllerScrollFlags::VERTICAL | gtk::EventControllerScrollFlags::DISCRETE,
|
||||||
|
|
@ -397,7 +432,11 @@ impl ViewerController {
|
||||||
scroll_controller.set_propagation_phase(gtk::PropagationPhase::Capture);
|
scroll_controller.set_propagation_phase(gtk::PropagationPhase::Capture);
|
||||||
let controller = Rc::clone(self);
|
let controller = Rc::clone(self);
|
||||||
scroll_controller.connect_scroll(move |_, _, delta_y| {
|
scroll_controller.connect_scroll(move |_, _, delta_y| {
|
||||||
if controller.view_mode() == ViewMode::Continuous || delta_y == 0.0 {
|
if matches!(
|
||||||
|
controller.view_mode(),
|
||||||
|
ViewMode::ActualSize | ViewMode::FitWidth | ViewMode::Continuous
|
||||||
|
) || delta_y == 0.0
|
||||||
|
{
|
||||||
return glib::Propagation::Proceed;
|
return glib::Propagation::Proceed;
|
||||||
}
|
}
|
||||||
controller.navigate(delta_y > 0.0);
|
controller.navigate(delta_y > 0.0);
|
||||||
|
|
@ -417,6 +456,32 @@ impl ViewerController {
|
||||||
key_controller.connect_key_pressed(move |_, key, _, _| {
|
key_controller.connect_key_pressed(move |_, key, _, _| {
|
||||||
controller.hide_seek();
|
controller.hide_seek();
|
||||||
match key {
|
match key {
|
||||||
|
gdk::Key::Home => {
|
||||||
|
controller.seek_edge(false);
|
||||||
|
glib::Propagation::Stop
|
||||||
|
}
|
||||||
|
gdk::Key::End => {
|
||||||
|
controller.seek_edge(true);
|
||||||
|
glib::Propagation::Stop
|
||||||
|
}
|
||||||
|
gdk::Key::Up
|
||||||
|
if matches!(
|
||||||
|
controller.view_mode(),
|
||||||
|
ViewMode::ActualSize | ViewMode::FitWidth | ViewMode::Continuous
|
||||||
|
) =>
|
||||||
|
{
|
||||||
|
controller.scroll_vertically(false);
|
||||||
|
glib::Propagation::Stop
|
||||||
|
}
|
||||||
|
gdk::Key::Down
|
||||||
|
if matches!(
|
||||||
|
controller.view_mode(),
|
||||||
|
ViewMode::ActualSize | ViewMode::FitWidth | ViewMode::Continuous
|
||||||
|
) =>
|
||||||
|
{
|
||||||
|
controller.scroll_vertically(true);
|
||||||
|
glib::Propagation::Stop
|
||||||
|
}
|
||||||
gdk::Key::Left | gdk::Key::Page_Up => {
|
gdk::Key::Left | gdk::Key::Page_Up => {
|
||||||
controller.navigate(false);
|
controller.navigate(false);
|
||||||
glib::Propagation::Stop
|
glib::Propagation::Stop
|
||||||
|
|
@ -655,15 +720,73 @@ impl ViewerController {
|
||||||
self.single_picture.set_paintable(gdk::Paintable::NONE);
|
self.single_picture.set_paintable(gdk::Paintable::NONE);
|
||||||
self.left_picture.set_paintable(gdk::Paintable::NONE);
|
self.left_picture.set_paintable(gdk::Paintable::NONE);
|
||||||
self.right_picture.set_paintable(gdk::Paintable::NONE);
|
self.right_picture.set_paintable(gdk::Paintable::NONE);
|
||||||
self.schedule_scroll_to_anchor();
|
|
||||||
} else {
|
} else {
|
||||||
self.clear_continuous_paintables();
|
self.clear_continuous_paintables();
|
||||||
}
|
}
|
||||||
let actual = mode == ViewMode::ActualSize;
|
let actual = mode == ViewMode::ActualSize;
|
||||||
|
self.single_box.set_hexpand(!actual);
|
||||||
|
self.single_box.set_vexpand(mode == ViewMode::FitHeight);
|
||||||
|
self.single_box.set_halign(match mode {
|
||||||
|
ViewMode::ActualSize => gtk::Align::Start,
|
||||||
|
ViewMode::FitHeight => gtk::Align::Center,
|
||||||
|
_ => gtk::Align::Fill,
|
||||||
|
});
|
||||||
|
self.single_box.set_valign(if actual {
|
||||||
|
gtk::Align::Start
|
||||||
|
} else {
|
||||||
|
gtk::Align::Fill
|
||||||
|
});
|
||||||
self.single_picture.set_can_shrink(!actual);
|
self.single_picture.set_can_shrink(!actual);
|
||||||
self.single_picture.set_hexpand(mode == ViewMode::FitWidth);
|
self.single_picture.set_hexpand(mode == ViewMode::FitWidth);
|
||||||
self.single_picture.set_vexpand(mode == ViewMode::FitHeight);
|
self.single_picture.set_vexpand(mode == ViewMode::FitHeight);
|
||||||
|
if matches!(mode, ViewMode::ActualSize | ViewMode::FitWidth) {
|
||||||
|
self.single_picture.set_content_fit(gtk::ContentFit::Fill);
|
||||||
|
self.single_picture.set_valign(gtk::Align::Start);
|
||||||
|
self.single_picture
|
||||||
|
.set_halign(if mode == ViewMode::FitWidth {
|
||||||
|
gtk::Align::Fill
|
||||||
|
} else {
|
||||||
|
gtk::Align::Start
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
self.single_picture
|
||||||
|
.set_content_fit(gtk::ContentFit::Contain);
|
||||||
|
self.single_picture.set_valign(gtk::Align::Fill);
|
||||||
|
self.single_picture
|
||||||
|
.set_halign(if mode == ViewMode::FitHeight {
|
||||||
|
gtk::Align::Center
|
||||||
|
} else {
|
||||||
|
gtk::Align::Fill
|
||||||
|
});
|
||||||
|
self.single_picture.set_size_request(-1, -1);
|
||||||
|
}
|
||||||
self.render();
|
self.render();
|
||||||
|
self.schedule_view_mode_refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn schedule_view_mode_refresh(self: &Rc<Self>) {
|
||||||
|
let generation = self.view_layout_generation.get().wrapping_add(1);
|
||||||
|
self.view_layout_generation.set(generation);
|
||||||
|
self.stack.queue_allocate();
|
||||||
|
self.window.queue_allocate();
|
||||||
|
let controller = Rc::downgrade(self);
|
||||||
|
glib::idle_add_local_once(move || {
|
||||||
|
let Some(controller) = controller.upgrade() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if controller.view_layout_generation.get() != generation {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
match controller.view_mode() {
|
||||||
|
ViewMode::FitWidth => controller.resize_single_picture(),
|
||||||
|
ViewMode::Continuous => controller.resize_continuous_rows(),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
controller.render();
|
||||||
|
if controller.view_mode() == ViewMode::Continuous {
|
||||||
|
controller.schedule_scroll_to_anchor();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn navigate(self: &Rc<Self>, forward: bool) {
|
fn navigate(self: &Rc<Self>, forward: bool) {
|
||||||
|
|
@ -686,12 +809,33 @@ impl ViewerController {
|
||||||
}
|
}
|
||||||
state.anchor = next;
|
state.anchor = next;
|
||||||
drop(state);
|
drop(state);
|
||||||
|
self.reset_single_scroll();
|
||||||
self.render();
|
self.render();
|
||||||
if self.view_mode() == ViewMode::Continuous {
|
if self.view_mode() == ViewMode::Continuous {
|
||||||
self.schedule_scroll_to_anchor();
|
self.schedule_scroll_to_anchor();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn seek_edge(self: &Rc<Self>, end: bool) {
|
||||||
|
let count = self.state.borrow().pages.len();
|
||||||
|
if count == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.seek_page(if end { count - 1 } else { 0 });
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scroll_vertically(&self, down: bool) {
|
||||||
|
let adjustment = if self.view_mode() == ViewMode::Continuous {
|
||||||
|
self.continuous_scrolled.vadjustment()
|
||||||
|
} else {
|
||||||
|
self.single_scrolled.vadjustment()
|
||||||
|
};
|
||||||
|
let step = adjustment.step_increment().max(64.0);
|
||||||
|
let delta = if down { step } else { -step };
|
||||||
|
let maximum = (adjustment.upper() - adjustment.page_size()).max(adjustment.lower());
|
||||||
|
adjustment.set_value((adjustment.value() + delta).clamp(adjustment.lower(), maximum));
|
||||||
|
}
|
||||||
|
|
||||||
fn shift_pairing(self: &Rc<Self>, offset: isize) {
|
fn shift_pairing(self: &Rc<Self>, offset: isize) {
|
||||||
let mut state = self.state.borrow_mut();
|
let mut state = self.state.borrow_mut();
|
||||||
if state.pages.is_empty() {
|
if state.pages.is_empty() {
|
||||||
|
|
@ -716,6 +860,7 @@ impl ViewerController {
|
||||||
}
|
}
|
||||||
state.anchor = index;
|
state.anchor = index;
|
||||||
drop(state);
|
drop(state);
|
||||||
|
self.reset_single_scroll();
|
||||||
self.render();
|
self.render();
|
||||||
if self.view_mode() == ViewMode::Continuous {
|
if self.view_mode() == ViewMode::Continuous {
|
||||||
self.schedule_scroll_to_anchor();
|
self.schedule_scroll_to_anchor();
|
||||||
|
|
@ -893,6 +1038,35 @@ impl ViewerController {
|
||||||
self.single_picture
|
self.single_picture
|
||||||
.set_paintable(state.cache.get(&index).map(|page| &page.texture));
|
.set_paintable(state.cache.get(&index).map(|page| &page.texture));
|
||||||
self.status_label.set_text(&state.pages[index].name);
|
self.status_label.set_text(&state.pages[index].name);
|
||||||
|
drop(state);
|
||||||
|
self.resize_single_picture();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resize_single_picture(&self) {
|
||||||
|
let dimensions = {
|
||||||
|
let state = self.state.borrow();
|
||||||
|
state
|
||||||
|
.cache
|
||||||
|
.get(&state.anchor)
|
||||||
|
.map(|page| (page.texture.width(), page.texture.height()))
|
||||||
|
};
|
||||||
|
if let Some((texture_width, texture_height)) = dimensions
|
||||||
|
&& let Some((width, height)) = single_picture_request(
|
||||||
|
self.view_mode(),
|
||||||
|
self.stack.allocated_width().max(1),
|
||||||
|
texture_width,
|
||||||
|
texture_height,
|
||||||
|
)
|
||||||
|
{
|
||||||
|
self.single_picture.set_size_request(width, height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset_single_scroll(&self) {
|
||||||
|
if matches!(self.view_mode(), ViewMode::ActualSize | ViewMode::FitWidth) {
|
||||||
|
let adjustment = self.single_scrolled.vadjustment();
|
||||||
|
adjustment.set_value(adjustment.lower());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_spread(&self, indices: &[usize]) {
|
fn render_spread(&self, indices: &[usize]) {
|
||||||
|
|
@ -1071,7 +1245,7 @@ impl ViewerController {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn continuous_content_width(&self) -> i32 {
|
fn continuous_content_width(&self) -> i32 {
|
||||||
(self.continuous_scrolled.allocated_width() - 24).max(320)
|
continuous_content_width(self.stack.allocated_width())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn evict_cache(&self) {
|
fn evict_cache(&self) {
|
||||||
|
|
@ -1155,6 +1329,7 @@ impl MediaViewerWindow {
|
||||||
favorite_control,
|
favorite_control,
|
||||||
download_control,
|
download_control,
|
||||||
reuse_window,
|
reuse_window,
|
||||||
|
view_mode_control,
|
||||||
} = controls;
|
} = controls;
|
||||||
let title = source.display_name.clone();
|
let title = source.display_name.clone();
|
||||||
let window = reuse_window.unwrap_or_else(|| {
|
let window = reuse_window.unwrap_or_else(|| {
|
||||||
|
|
@ -1170,6 +1345,20 @@ impl MediaViewerWindow {
|
||||||
header.set_title_widget(Some(>k::Label::new(Some(&title))));
|
header.set_title_widget(Some(>k::Label::new(Some(&title))));
|
||||||
let favorite_button = gtk::Button::with_label(favorite_label(favorite_control.active));
|
let favorite_button = gtk::Button::with_label(favorite_label(favorite_control.active));
|
||||||
let download_button = gtk::Button::with_label("파일 다운로드");
|
let download_button = gtk::Button::with_label("파일 다운로드");
|
||||||
|
let loop_check = gtk::CheckButton::with_label("반복 재생");
|
||||||
|
loop_check.set_visible(kind == EntryKind::Video);
|
||||||
|
let image_mode_dropdown = gtk::DropDown::from_strings(&["100%", "폭 맞춤", "높이 맞춤"]);
|
||||||
|
let (image_mode_selected, image_mode_change) =
|
||||||
|
view_mode_control.map_or((1, None), |control| {
|
||||||
|
(
|
||||||
|
standalone_mode_index(control.selected),
|
||||||
|
Some(control.change),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
image_mode_dropdown.set_selected(image_mode_selected);
|
||||||
|
image_mode_dropdown.set_visible(kind == EntryKind::Image);
|
||||||
|
header.pack_start(&image_mode_dropdown);
|
||||||
|
header.pack_end(&loop_check);
|
||||||
header.pack_end(&download_button);
|
header.pack_end(&download_button);
|
||||||
header.pack_end(&favorite_button);
|
header.pack_end(&favorite_button);
|
||||||
window.set_titlebar(Some(&header));
|
window.set_titlebar(Some(&header));
|
||||||
|
|
@ -1179,7 +1368,14 @@ impl MediaViewerWindow {
|
||||||
stack.set_hexpand(true);
|
stack.set_hexpand(true);
|
||||||
stack.set_vexpand(true);
|
stack.set_vexpand(true);
|
||||||
let picture = page_picture();
|
let picture = page_picture();
|
||||||
stack.add_named(&picture, Some("image"));
|
let image_box = gtk::Box::new(gtk::Orientation::Horizontal, 0);
|
||||||
|
image_box.append(&picture);
|
||||||
|
let image_scrolled = gtk::ScrolledWindow::builder()
|
||||||
|
.hexpand(true)
|
||||||
|
.vexpand(true)
|
||||||
|
.child(&image_box)
|
||||||
|
.build();
|
||||||
|
stack.add_named(&image_scrolled, Some("image"));
|
||||||
let video = if kind == EntryKind::Video {
|
let video = if kind == EntryKind::Video {
|
||||||
gtk::Video::for_filename(Some(&source.local_path))
|
gtk::Video::for_filename(Some(&source.local_path))
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1188,6 +1384,8 @@ impl MediaViewerWindow {
|
||||||
video.set_autoplay(true);
|
video.set_autoplay(true);
|
||||||
video.set_hexpand(true);
|
video.set_hexpand(true);
|
||||||
video.set_vexpand(true);
|
video.set_vexpand(true);
|
||||||
|
let video_for_loop = video.clone();
|
||||||
|
loop_check.connect_toggled(move |check| video_for_loop.set_loop(check.is_active()));
|
||||||
stack.add_named(&video, Some("video"));
|
stack.add_named(&video, Some("video"));
|
||||||
let status_label = gtk::Label::builder()
|
let status_label = gtk::Label::builder()
|
||||||
.label(&title)
|
.label(&title)
|
||||||
|
|
@ -1221,7 +1419,13 @@ impl MediaViewerWindow {
|
||||||
let controller = Rc::new(MediaViewerController {
|
let controller = Rc::new(MediaViewerController {
|
||||||
window,
|
window,
|
||||||
source,
|
source,
|
||||||
|
kind,
|
||||||
picture,
|
picture,
|
||||||
|
image_box,
|
||||||
|
image_scrolled,
|
||||||
|
image_mode_dropdown,
|
||||||
|
image_mode_change,
|
||||||
|
image_dimensions: Cell::new(None),
|
||||||
video,
|
video,
|
||||||
stack,
|
stack,
|
||||||
status_label,
|
status_label,
|
||||||
|
|
@ -1251,6 +1455,7 @@ impl MediaViewerWindow {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
controller.connect_signals();
|
controller.connect_signals();
|
||||||
|
controller.apply_image_view_mode();
|
||||||
controller.window.present();
|
controller.window.present();
|
||||||
match kind {
|
match kind {
|
||||||
EntryKind::Image => controller.load_image(),
|
EntryKind::Image => controller.load_image(),
|
||||||
|
|
@ -1268,7 +1473,13 @@ impl MediaViewerWindow {
|
||||||
struct MediaViewerController {
|
struct MediaViewerController {
|
||||||
window: gtk::ApplicationWindow,
|
window: gtk::ApplicationWindow,
|
||||||
source: ArchiveSource,
|
source: ArchiveSource,
|
||||||
|
kind: EntryKind,
|
||||||
picture: gtk::Picture,
|
picture: gtk::Picture,
|
||||||
|
image_box: gtk::Box,
|
||||||
|
image_scrolled: gtk::ScrolledWindow,
|
||||||
|
image_mode_dropdown: gtk::DropDown,
|
||||||
|
image_mode_change: Option<Rc<dyn Fn(u32)>>,
|
||||||
|
image_dimensions: Cell<Option<(i32, i32)>>,
|
||||||
video: gtk::Video,
|
video: gtk::Video,
|
||||||
stack: gtk::Stack,
|
stack: gtk::Stack,
|
||||||
status_label: gtk::Label,
|
status_label: gtk::Label,
|
||||||
|
|
@ -1303,20 +1514,51 @@ impl MediaViewerController {
|
||||||
let controller = Rc::clone(self);
|
let controller = Rc::clone(self);
|
||||||
keys.connect_key_pressed(move |_, key, _, _| {
|
keys.connect_key_pressed(move |_, key, _, _| {
|
||||||
controller.hide_seek();
|
controller.hide_seek();
|
||||||
match key.to_unicode() {
|
match key {
|
||||||
Some('[') => {
|
gdk::Key::Home => {
|
||||||
|
controller.open_edge(false);
|
||||||
|
glib::Propagation::Stop
|
||||||
|
}
|
||||||
|
gdk::Key::End => {
|
||||||
|
controller.open_edge(true);
|
||||||
|
glib::Propagation::Stop
|
||||||
|
}
|
||||||
|
gdk::Key::Up
|
||||||
|
if controller.kind == EntryKind::Image
|
||||||
|
&& matches!(
|
||||||
|
controller.image_view_mode(),
|
||||||
|
ViewMode::ActualSize | ViewMode::FitWidth
|
||||||
|
) =>
|
||||||
|
{
|
||||||
|
controller.scroll_image(false);
|
||||||
|
glib::Propagation::Stop
|
||||||
|
}
|
||||||
|
gdk::Key::Down
|
||||||
|
if controller.kind == EntryKind::Image
|
||||||
|
&& matches!(
|
||||||
|
controller.image_view_mode(),
|
||||||
|
ViewMode::ActualSize | ViewMode::FitWidth
|
||||||
|
) =>
|
||||||
|
{
|
||||||
|
controller.scroll_image(true);
|
||||||
|
glib::Propagation::Stop
|
||||||
|
}
|
||||||
|
_ if key.to_unicode() == Some('[') => {
|
||||||
controller.open_sibling(false);
|
controller.open_sibling(false);
|
||||||
glib::Propagation::Stop
|
glib::Propagation::Stop
|
||||||
}
|
}
|
||||||
Some(']') => {
|
_ if key.to_unicode() == Some(']') => {
|
||||||
controller.open_sibling(true);
|
controller.open_sibling(true);
|
||||||
glib::Propagation::Stop
|
glib::Propagation::Stop
|
||||||
}
|
}
|
||||||
Some(character) if character.eq_ignore_ascii_case(&'f') => {
|
_ if key
|
||||||
|
.to_unicode()
|
||||||
|
.is_some_and(|character| character.eq_ignore_ascii_case(&'f')) =>
|
||||||
|
{
|
||||||
controller.toggle_fullscreen();
|
controller.toggle_fullscreen();
|
||||||
glib::Propagation::Stop
|
glib::Propagation::Stop
|
||||||
}
|
}
|
||||||
_ if key == gdk::Key::Escape && controller.window.is_fullscreen() => {
|
gdk::Key::Escape if controller.window.is_fullscreen() => {
|
||||||
controller.window.unfullscreen();
|
controller.window.unfullscreen();
|
||||||
glib::Propagation::Stop
|
glib::Propagation::Stop
|
||||||
}
|
}
|
||||||
|
|
@ -1337,6 +1579,19 @@ impl MediaViewerController {
|
||||||
controller.status_label.clone(),
|
controller.status_label.clone(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
if let Some(change) = self.image_mode_change.clone() {
|
||||||
|
let controller = Rc::clone(self);
|
||||||
|
self.image_mode_dropdown
|
||||||
|
.connect_selected_notify(move |dropdown| {
|
||||||
|
change(dropdown.selected());
|
||||||
|
controller.apply_image_view_mode();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let controller = Rc::clone(self);
|
||||||
|
self.image_scrolled
|
||||||
|
.connect_notify_local(Some("width"), move |_, _| {
|
||||||
|
controller.resize_image_picture();
|
||||||
|
});
|
||||||
let controller = Rc::clone(self);
|
let controller = Rc::clone(self);
|
||||||
self.media_seek.connect_value_changed(move |scale| {
|
self.media_seek.connect_value_changed(move |scale| {
|
||||||
let Some(navigator) = &controller.sibling_navigator else {
|
let Some(navigator) = &controller.sibling_navigator else {
|
||||||
|
|
@ -1352,6 +1607,79 @@ impl MediaViewerController {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn image_view_mode(&self) -> ViewMode {
|
||||||
|
match self.image_mode_dropdown.selected() {
|
||||||
|
0 => ViewMode::ActualSize,
|
||||||
|
2 => ViewMode::FitHeight,
|
||||||
|
_ => ViewMode::FitWidth,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_image_view_mode(&self) {
|
||||||
|
if self.kind != EntryKind::Image {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mode = self.image_view_mode();
|
||||||
|
let actual = mode == ViewMode::ActualSize;
|
||||||
|
self.image_box.set_hexpand(!actual);
|
||||||
|
self.image_box.set_vexpand(mode == ViewMode::FitHeight);
|
||||||
|
self.image_box.set_halign(match mode {
|
||||||
|
ViewMode::ActualSize => gtk::Align::Start,
|
||||||
|
ViewMode::FitHeight => gtk::Align::Center,
|
||||||
|
_ => gtk::Align::Fill,
|
||||||
|
});
|
||||||
|
self.image_box.set_valign(if actual {
|
||||||
|
gtk::Align::Start
|
||||||
|
} else {
|
||||||
|
gtk::Align::Fill
|
||||||
|
});
|
||||||
|
self.picture.set_can_shrink(!actual);
|
||||||
|
self.picture.set_hexpand(mode == ViewMode::FitWidth);
|
||||||
|
self.picture.set_vexpand(mode == ViewMode::FitHeight);
|
||||||
|
if matches!(mode, ViewMode::ActualSize | ViewMode::FitWidth) {
|
||||||
|
self.picture.set_content_fit(gtk::ContentFit::Fill);
|
||||||
|
self.picture.set_halign(if actual {
|
||||||
|
gtk::Align::Start
|
||||||
|
} else {
|
||||||
|
gtk::Align::Fill
|
||||||
|
});
|
||||||
|
self.picture.set_valign(gtk::Align::Start);
|
||||||
|
} else {
|
||||||
|
self.picture.set_content_fit(gtk::ContentFit::Contain);
|
||||||
|
self.picture.set_halign(if mode == ViewMode::FitHeight {
|
||||||
|
gtk::Align::Center
|
||||||
|
} else {
|
||||||
|
gtk::Align::Fill
|
||||||
|
});
|
||||||
|
self.picture.set_valign(gtk::Align::Fill);
|
||||||
|
self.picture.set_size_request(-1, -1);
|
||||||
|
}
|
||||||
|
self.resize_image_picture();
|
||||||
|
self.stack.queue_allocate();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resize_image_picture(&self) {
|
||||||
|
let Some((texture_width, texture_height)) = self.image_dimensions.get() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if let Some((width, height)) = single_picture_request(
|
||||||
|
self.image_view_mode(),
|
||||||
|
self.stack.allocated_width().max(1),
|
||||||
|
texture_width,
|
||||||
|
texture_height,
|
||||||
|
) {
|
||||||
|
self.picture.set_size_request(width, height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scroll_image(&self, down: bool) {
|
||||||
|
let adjustment = self.image_scrolled.vadjustment();
|
||||||
|
let step = adjustment.step_increment().max(64.0);
|
||||||
|
let delta = if down { step } else { -step };
|
||||||
|
let maximum = (adjustment.upper() - adjustment.page_size()).max(adjustment.lower());
|
||||||
|
adjustment.set_value((adjustment.value() + delta).clamp(adjustment.lower(), maximum));
|
||||||
|
}
|
||||||
|
|
||||||
fn open_sibling(&self, forward: bool) {
|
fn open_sibling(&self, forward: bool) {
|
||||||
if let Some(navigate) = &self.sibling_navigator
|
if let Some(navigate) = &self.sibling_navigator
|
||||||
&& navigate.open_relative(forward, self.window.clone())
|
&& navigate.open_relative(forward, self.window.clone())
|
||||||
|
|
@ -1362,6 +1690,16 @@ impl MediaViewerController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn open_edge(&self, end: bool) {
|
||||||
|
if let Some(navigate) = &self.sibling_navigator
|
||||||
|
&& navigate.open_edge(end, self.window.clone())
|
||||||
|
{
|
||||||
|
self.prepare_for_navigation();
|
||||||
|
} else {
|
||||||
|
self.show_navigation_error(end);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn show_navigation_error(&self, forward: bool) {
|
fn show_navigation_error(&self, forward: bool) {
|
||||||
self.navigation_message_label
|
self.navigation_message_label
|
||||||
.set_text(navigation_error_message(forward));
|
.set_text(navigation_error_message(forward));
|
||||||
|
|
@ -1513,6 +1851,8 @@ impl MediaViewerController {
|
||||||
fn finish_image_load(&self, result: Result<DecodedPage, String>) {
|
fn finish_image_load(&self, result: Result<DecodedPage, String>) {
|
||||||
match result {
|
match result {
|
||||||
Ok(page) => {
|
Ok(page) => {
|
||||||
|
self.image_dimensions
|
||||||
|
.set(Some((page.width as i32, page.height as i32)));
|
||||||
let bytes = glib::Bytes::from_owned(page.rgba);
|
let bytes = glib::Bytes::from_owned(page.rgba);
|
||||||
let texture = gdk::MemoryTexture::new(
|
let texture = gdk::MemoryTexture::new(
|
||||||
page.width as i32,
|
page.width as i32,
|
||||||
|
|
@ -1522,6 +1862,7 @@ impl MediaViewerController {
|
||||||
page.width as usize * 4,
|
page.width as usize * 4,
|
||||||
);
|
);
|
||||||
self.picture.set_paintable(Some(&texture));
|
self.picture.set_paintable(Some(&texture));
|
||||||
|
self.apply_image_view_mode();
|
||||||
self.status_label.set_text(&self.source.display_name);
|
self.status_label.set_text(&self.source.display_name);
|
||||||
}
|
}
|
||||||
Err(error) => self.status_label.set_text(&error),
|
Err(error) => self.status_label.set_text(&error),
|
||||||
|
|
@ -1537,6 +1878,13 @@ fn favorite_label(active: bool) -> &'static str {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn standalone_mode_index(global_index: u32) -> u32 {
|
||||||
|
match global_index {
|
||||||
|
0 | 2 => global_index,
|
||||||
|
_ => 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn page_picture() -> gtk::Picture {
|
fn page_picture() -> gtk::Picture {
|
||||||
let picture = gtk::Picture::new();
|
let picture = gtk::Picture::new();
|
||||||
picture.set_content_fit(gtk::ContentFit::Contain);
|
picture.set_content_fit(gtk::ContentFit::Contain);
|
||||||
|
|
@ -1613,6 +1961,26 @@ fn row_height(width: i32, ratio: f32) -> i32 {
|
||||||
((width as f32 / ratio).round() as i32).clamp(1, 30_000)
|
((width as f32 / ratio).round() as i32).clamp(1, 30_000)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn single_picture_request(
|
||||||
|
mode: ViewMode,
|
||||||
|
viewport_width: i32,
|
||||||
|
texture_width: i32,
|
||||||
|
texture_height: i32,
|
||||||
|
) -> Option<(i32, i32)> {
|
||||||
|
match mode {
|
||||||
|
ViewMode::ActualSize => Some((texture_width, texture_height)),
|
||||||
|
ViewMode::FitWidth => Some((
|
||||||
|
-1,
|
||||||
|
row_height(viewport_width, texture_width as f32 / texture_height as f32),
|
||||||
|
)),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn continuous_content_width(stack_width: i32) -> i32 {
|
||||||
|
(stack_width - 24).max(320)
|
||||||
|
}
|
||||||
|
|
||||||
fn prefetch_window(visible: &[usize], count: usize, fast: bool) -> Vec<usize> {
|
fn prefetch_window(visible: &[usize], count: usize, fast: bool) -> Vec<usize> {
|
||||||
if visible.is_empty() || count == 0 {
|
if visible.is_empty() || count == 0 {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
|
|
@ -1687,6 +2055,25 @@ mod tests {
|
||||||
fn continuous_row_height_preserves_image_ratio() {
|
fn continuous_row_height_preserves_image_ratio() {
|
||||||
assert_eq!(row_height(1200, 2.0 / 3.0), 1800);
|
assert_eq!(row_height(1200, 2.0 / 3.0), 1800);
|
||||||
assert_eq!(row_height(1200, 1.5), 800);
|
assert_eq!(row_height(1200, 1.5), 800);
|
||||||
|
assert_eq!(continuous_content_width(1200), 1176);
|
||||||
|
assert_eq!(continuous_content_width(0), 320);
|
||||||
|
assert_eq!(
|
||||||
|
single_picture_request(ViewMode::ActualSize, 1200, 800, 1600),
|
||||||
|
Some((800, 1600))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
single_picture_request(ViewMode::FitWidth, 1200, 800, 1600),
|
||||||
|
Some((-1, 2400))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
single_picture_request(ViewMode::FitHeight, 1200, 800, 1600),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
assert_eq!(standalone_mode_index(0), 0);
|
||||||
|
assert_eq!(standalone_mode_index(1), 1);
|
||||||
|
assert_eq!(standalone_mode_index(2), 2);
|
||||||
|
assert_eq!(standalone_mode_index(3), 1);
|
||||||
|
assert_eq!(standalone_mode_index(4), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -1745,6 +2132,11 @@ mod tests {
|
||||||
assert_eq!(sibling_index(1, 3, true), Some(2));
|
assert_eq!(sibling_index(1, 3, true), Some(2));
|
||||||
assert_eq!(sibling_index(0, 3, false), None);
|
assert_eq!(sibling_index(0, 3, false), None);
|
||||||
assert_eq!(sibling_index(2, 3, true), None);
|
assert_eq!(sibling_index(2, 3, true), None);
|
||||||
|
assert_eq!(SiblingTarget::First.resolve(2, 5), Some(0));
|
||||||
|
assert_eq!(SiblingTarget::Last.resolve(1, 5), Some(4));
|
||||||
|
assert_eq!(SiblingTarget::First.resolve(0, 5), None);
|
||||||
|
assert_eq!(SiblingTarget::Last.resolve(4, 5), None);
|
||||||
|
assert_eq!(SiblingTarget::Last.resolve(0, 0), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -248,6 +248,44 @@ impl BrowserController {
|
||||||
let controller = Rc::clone(self);
|
let controller = Rc::clone(self);
|
||||||
self.location_dropdown
|
self.location_dropdown
|
||||||
.connect_selected_notify(move |_| controller.select_profile());
|
.connect_selected_notify(move |_| controller.select_profile());
|
||||||
|
let location_factory = gtk::SignalListItemFactory::new();
|
||||||
|
let controller = Rc::downgrade(self);
|
||||||
|
location_factory.connect_setup(move |_, item| {
|
||||||
|
let Some(item) = item.downcast_ref::<gtk::ListItem>() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let label = gtk::Label::builder().xalign(0.0).build();
|
||||||
|
let click = gtk::GestureClick::builder().button(1).build();
|
||||||
|
item.set_child(Some(&label));
|
||||||
|
let item = item.downgrade();
|
||||||
|
let controller = controller.clone();
|
||||||
|
click.connect_released(move |_, _, _, _| {
|
||||||
|
let Some(item) = item.upgrade() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(controller) = controller.upgrade() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if is_favorites_selection(item.position(), controller.profiles().len()) {
|
||||||
|
controller.show_favorites();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
label.add_controller(click);
|
||||||
|
});
|
||||||
|
location_factory.connect_bind(|_, item| {
|
||||||
|
let Some(item) = item.downcast_ref::<gtk::ListItem>() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(label) = item.child().and_downcast::<gtk::Label>() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(value) = item.item().and_downcast::<gtk::StringObject>() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
label.set_text(value.string().as_str());
|
||||||
|
});
|
||||||
|
self.location_dropdown
|
||||||
|
.set_list_factory(Some(&location_factory));
|
||||||
let controller = Rc::clone(self);
|
let controller = Rc::clone(self);
|
||||||
self.search_entry
|
self.search_entry
|
||||||
.connect_search_changed(move |_| controller.render());
|
.connect_search_changed(move |_| controller.render());
|
||||||
|
|
@ -274,8 +312,14 @@ impl BrowserController {
|
||||||
|
|
||||||
let key_controller = gtk::EventControllerKey::new();
|
let key_controller = gtk::EventControllerKey::new();
|
||||||
let controller = Rc::clone(self);
|
let controller = Rc::clone(self);
|
||||||
key_controller
|
key_controller.connect_key_pressed(move |_, key, _, modifiers| {
|
||||||
.connect_key_pressed(move |_, key, _, modifiers| controller.typeahead(key, modifiers));
|
if key == gtk::gdk::Key::BackSpace {
|
||||||
|
controller.go_up();
|
||||||
|
glib::Propagation::Stop
|
||||||
|
} else {
|
||||||
|
controller.typeahead(key, modifiers)
|
||||||
|
}
|
||||||
|
});
|
||||||
self.list_box.add_controller(key_controller);
|
self.list_box.add_controller(key_controller);
|
||||||
|
|
||||||
let controller = Rc::clone(self);
|
let controller = Rc::clone(self);
|
||||||
|
|
@ -315,7 +359,7 @@ impl BrowserController {
|
||||||
fn select_profile(self: &Rc<Self>) {
|
fn select_profile(self: &Rc<Self>) {
|
||||||
let profile_index = self.location_dropdown.selected() as usize;
|
let profile_index = self.location_dropdown.selected() as usize;
|
||||||
let profiles = self.profiles();
|
let profiles = self.profiles();
|
||||||
if profile_index == profiles.len() {
|
if is_favorites_selection(self.location_dropdown.selected(), profiles.len()) {
|
||||||
self.show_favorites();
|
self.show_favorites();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -348,7 +392,6 @@ impl BrowserController {
|
||||||
self.delete_location_button.set_sensitive(false);
|
self.delete_location_button.set_sensitive(false);
|
||||||
self.path_entry.set_text("즐겨찾기");
|
self.path_entry.set_text("즐겨찾기");
|
||||||
self.path_entry.set_editable(false);
|
self.path_entry.set_editable(false);
|
||||||
self.search_entry.set_text("");
|
|
||||||
let entries = self
|
let entries = self
|
||||||
.config
|
.config
|
||||||
.borrow()
|
.borrow()
|
||||||
|
|
@ -357,10 +400,14 @@ impl BrowserController {
|
||||||
.map(favorite_browser_entry)
|
.map(favorite_browser_entry)
|
||||||
.collect();
|
.collect();
|
||||||
let mut state = self.state.borrow_mut();
|
let mut state = self.state.borrow_mut();
|
||||||
|
state.generation = state.generation.wrapping_add(1);
|
||||||
|
state.typeahead_query.clear();
|
||||||
|
state.typeahead_last_input = None;
|
||||||
state.browsing_favorites = true;
|
state.browsing_favorites = true;
|
||||||
state.favorite_profile = None;
|
state.favorite_profile = None;
|
||||||
state.entries = entries;
|
state.entries = entries;
|
||||||
drop(state);
|
drop(state);
|
||||||
|
self.search_entry.set_text("");
|
||||||
self.render();
|
self.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -500,7 +547,10 @@ impl BrowserController {
|
||||||
let Some(controller) = controller.upgrade() else {
|
let Some(controller) = controller.upgrade() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if controller.state.borrow().generation != generation {
|
let state = controller.state.borrow();
|
||||||
|
let stale = state.generation != generation || state.browsing_favorites;
|
||||||
|
drop(state);
|
||||||
|
if stale {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
match result {
|
match result {
|
||||||
|
|
@ -815,6 +865,7 @@ impl BrowserController {
|
||||||
favorite_control,
|
favorite_control,
|
||||||
download_control,
|
download_control,
|
||||||
reuse_window,
|
reuse_window,
|
||||||
|
view_mode_control: None,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -846,6 +897,7 @@ impl BrowserController {
|
||||||
let navigator = self.sibling_navigator(&entry, siblings, profile.clone());
|
let navigator = self.sibling_navigator(&entry, siblings, profile.clone());
|
||||||
let favorite_control = self.favorite_control(&entry, &profile);
|
let favorite_control = self.favorite_control(&entry, &profile);
|
||||||
let download_control = self.download_control(&entry);
|
let download_control = self.download_control(&entry);
|
||||||
|
let view_mode_control = (kind == EntryKind::Image).then(|| self.view_mode_control());
|
||||||
self.zip_cache.prepare_media(
|
self.zip_cache.prepare_media(
|
||||||
entry,
|
entry,
|
||||||
move |current, total| {
|
move |current, total| {
|
||||||
|
|
@ -868,6 +920,7 @@ impl BrowserController {
|
||||||
favorite_control,
|
favorite_control,
|
||||||
download_control,
|
download_control,
|
||||||
reuse_window,
|
reuse_window,
|
||||||
|
view_mode_control,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -1166,7 +1219,7 @@ impl BrowserController {
|
||||||
.transient_for(&self.window)
|
.transient_for(&self.window)
|
||||||
.modal(true)
|
.modal(true)
|
||||||
.resizable(false)
|
.resizable(false)
|
||||||
.default_width(360)
|
.default_width(520)
|
||||||
.build();
|
.build();
|
||||||
let content = gtk::Box::new(gtk::Orientation::Vertical, 14);
|
let content = gtk::Box::new(gtk::Orientation::Vertical, 14);
|
||||||
content.set_margin_top(24);
|
content.set_margin_top(24);
|
||||||
|
|
@ -1179,12 +1232,21 @@ impl BrowserController {
|
||||||
)));
|
)));
|
||||||
name.add_css_class("title-2");
|
name.add_css_class("title-2");
|
||||||
let contact = gtk::Label::new(Some("개발자 연락처 : burningtuna@gmail.com"));
|
let contact = gtk::Label::new(Some("개발자 연락처 : burningtuna@gmail.com"));
|
||||||
|
let libraries = gtk::Label::new(Some(
|
||||||
|
"사용한 라이브러리\n\
|
||||||
|
GTK4 / GIO · GStreamer · image · zip · rusqlite / SQLite\n\
|
||||||
|
serde · toml · anyhow · chrono · filetime · percent-encoding\n\
|
||||||
|
sha2 · tempfile",
|
||||||
|
));
|
||||||
|
libraries.set_xalign(0.0);
|
||||||
|
libraries.set_wrap(true);
|
||||||
let close_button = gtk::Button::with_label("닫기");
|
let close_button = gtk::Button::with_label("닫기");
|
||||||
close_button.set_halign(gtk::Align::End);
|
close_button.set_halign(gtk::Align::End);
|
||||||
let dialog_to_close = dialog.clone();
|
let dialog_to_close = dialog.clone();
|
||||||
close_button.connect_clicked(move |_| dialog_to_close.close());
|
close_button.connect_clicked(move |_| dialog_to_close.close());
|
||||||
content.append(&name);
|
content.append(&name);
|
||||||
content.append(&contact);
|
content.append(&contact);
|
||||||
|
content.append(&libraries);
|
||||||
content.append(&close_button);
|
content.append(&close_button);
|
||||||
dialog.set_child(Some(&content));
|
dialog.set_child(Some(&content));
|
||||||
dialog.present();
|
dialog.present();
|
||||||
|
|
@ -1449,6 +1511,10 @@ fn navigable_sibling(current: EntryKind, candidate: EntryKind) -> bool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_favorites_selection(selected: u32, profile_count: usize) -> bool {
|
||||||
|
selected != gtk::INVALID_LIST_POSITION && selected as usize == profile_count
|
||||||
|
}
|
||||||
|
|
||||||
fn sorted_navigable_entries(
|
fn sorted_navigable_entries(
|
||||||
entries: &[BrowserEntry],
|
entries: &[BrowserEntry],
|
||||||
current_kind: EntryKind,
|
current_kind: EntryKind,
|
||||||
|
|
@ -1634,6 +1700,9 @@ mod tests {
|
||||||
|
|
||||||
assert_eq!(folder.kind, EntryKind::Directory);
|
assert_eq!(folder.kind, EntryKind::Directory);
|
||||||
assert_eq!(archive.kind, EntryKind::ZipArchive);
|
assert_eq!(archive.kind, EntryKind::ZipArchive);
|
||||||
|
assert!(is_favorites_selection(3, 3));
|
||||||
|
assert!(!is_favorites_selection(2, 3));
|
||||||
|
assert!(!is_favorites_selection(gtk::INVALID_LIST_POSITION, 3));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue