Add viewer navigation and file downloads
This commit is contained in:
parent
490c925f62
commit
ef2a0a7915
3 changed files with 221 additions and 43 deletions
|
|
@ -15,6 +15,10 @@ impl EntryKind {
|
||||||
pub fn is_viewable(self) -> bool {
|
pub fn is_viewable(self) -> bool {
|
||||||
matches!(self, Self::ZipArchive | Self::Image | Self::Video)
|
matches!(self, Self::ZipArchive | Self::Image | Self::Video)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_media(self) -> bool {
|
||||||
|
matches!(self, Self::Image | Self::Video)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
@ -194,6 +198,10 @@ mod tests {
|
||||||
assert_eq!(classify_file(Path::new("movie.MP4")), EntryKind::Video);
|
assert_eq!(classify_file(Path::new("movie.MP4")), EntryKind::Video);
|
||||||
assert_eq!(classify_file(Path::new("movie.mkv")), EntryKind::Video);
|
assert_eq!(classify_file(Path::new("movie.mkv")), EntryKind::Video);
|
||||||
assert_eq!(classify_file(Path::new("notes.txt")), EntryKind::OtherFile);
|
assert_eq!(classify_file(Path::new("notes.txt")), EntryKind::OtherFile);
|
||||||
|
assert!(EntryKind::Image.is_media());
|
||||||
|
assert!(EntryKind::Video.is_media());
|
||||||
|
assert!(!EntryKind::ZipArchive.is_media());
|
||||||
|
assert!(!EntryKind::OtherFile.is_media());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
148
src/viewer.rs
148
src/viewer.rs
|
|
@ -1,4 +1,4 @@
|
||||||
use std::cell::RefCell;
|
use std::cell::{Cell, RefCell};
|
||||||
use std::collections::{HashMap, HashSet, VecDeque};
|
use std::collections::{HashMap, HashSet, VecDeque};
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::sync::mpsc::{self, TryRecvError};
|
use std::sync::mpsc::{self, TryRecvError};
|
||||||
|
|
@ -21,13 +21,42 @@ const FAST_CACHE_LIMIT_PAGES: usize = 64;
|
||||||
|
|
||||||
pub struct ViewerWindow;
|
pub struct ViewerWindow;
|
||||||
|
|
||||||
pub type SiblingNavigator = Rc<dyn Fn(bool) -> bool>;
|
pub struct SiblingNavigator {
|
||||||
|
current: usize,
|
||||||
|
count: usize,
|
||||||
|
open: Rc<dyn Fn(usize) -> bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SiblingNavigator {
|
||||||
|
pub fn new(current: usize, count: usize, open: Rc<dyn Fn(usize) -> bool>) -> Self {
|
||||||
|
Self {
|
||||||
|
current,
|
||||||
|
count,
|
||||||
|
open,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_relative(&self, forward: bool) -> bool {
|
||||||
|
let Some(index) = sibling_index(self.current, self.count, forward) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
(self.open)(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_index(&self, index: usize) -> bool {
|
||||||
|
index < self.count && index != self.current && (self.open)(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct FavoriteControl {
|
pub struct FavoriteControl {
|
||||||
pub active: bool,
|
pub active: bool,
|
||||||
pub toggle: Rc<dyn Fn() -> bool>,
|
pub toggle: Rc<dyn Fn() -> bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct FileDownloadControl {
|
||||||
|
pub download: Rc<dyn Fn(gtk::Window, gtk::Label)>,
|
||||||
|
}
|
||||||
|
|
||||||
impl ViewerWindow {
|
impl ViewerWindow {
|
||||||
pub fn open(
|
pub fn open(
|
||||||
application: >k::Application,
|
application: >k::Application,
|
||||||
|
|
@ -36,6 +65,7 @@ impl ViewerWindow {
|
||||||
fast_image_loading: bool,
|
fast_image_loading: bool,
|
||||||
sibling_navigator: Option<SiblingNavigator>,
|
sibling_navigator: Option<SiblingNavigator>,
|
||||||
favorite_control: FavoriteControl,
|
favorite_control: FavoriteControl,
|
||||||
|
download_control: FileDownloadControl,
|
||||||
) {
|
) {
|
||||||
let title = source.display_name.clone();
|
let title = source.display_name.clone();
|
||||||
let window = gtk::ApplicationWindow::builder()
|
let window = gtk::ApplicationWindow::builder()
|
||||||
|
|
@ -47,6 +77,8 @@ impl ViewerWindow {
|
||||||
let header = gtk::HeaderBar::new();
|
let header = gtk::HeaderBar::new();
|
||||||
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("파일 다운로드");
|
||||||
|
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));
|
||||||
|
|
||||||
|
|
@ -162,6 +194,8 @@ impl ViewerWindow {
|
||||||
sibling_navigator,
|
sibling_navigator,
|
||||||
favorite_button,
|
favorite_button,
|
||||||
favorite_toggle: favorite_control.toggle,
|
favorite_toggle: favorite_control.toggle,
|
||||||
|
download_button,
|
||||||
|
file_download: download_control.download,
|
||||||
state: RefCell::new(ViewerState {
|
state: RefCell::new(ViewerState {
|
||||||
pages: Vec::new(),
|
pages: Vec::new(),
|
||||||
anchor: 0,
|
anchor: 0,
|
||||||
|
|
@ -240,6 +274,8 @@ struct ViewerController {
|
||||||
sibling_navigator: Option<SiblingNavigator>,
|
sibling_navigator: Option<SiblingNavigator>,
|
||||||
favorite_button: gtk::Button,
|
favorite_button: gtk::Button,
|
||||||
favorite_toggle: Rc<dyn Fn() -> bool>,
|
favorite_toggle: Rc<dyn Fn() -> bool>,
|
||||||
|
download_button: gtk::Button,
|
||||||
|
file_download: Rc<dyn Fn(gtk::Window, gtk::Label)>,
|
||||||
state: RefCell<ViewerState>,
|
state: RefCell<ViewerState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -269,6 +305,13 @@ impl ViewerController {
|
||||||
controller.favorite_button.set_label(favorite_label(active));
|
controller.favorite_button.set_label(favorite_label(active));
|
||||||
});
|
});
|
||||||
let controller = Rc::clone(self);
|
let controller = Rc::clone(self);
|
||||||
|
self.download_button.connect_clicked(move |_| {
|
||||||
|
(controller.file_download)(
|
||||||
|
controller.window.clone().upcast(),
|
||||||
|
controller.status_label.clone(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
let controller = Rc::clone(self);
|
||||||
self.page_seek.connect_value_changed(move |scale| {
|
self.page_seek.connect_value_changed(move |scale| {
|
||||||
if let Some(index) =
|
if let Some(index) =
|
||||||
page_seek_index(scale.value(), controller.state.borrow().pages.len())
|
page_seek_index(scale.value(), controller.state.borrow().pages.len())
|
||||||
|
|
@ -339,7 +382,7 @@ impl ViewerController {
|
||||||
|
|
||||||
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(forward)
|
&& navigate.open_relative(forward)
|
||||||
{
|
{
|
||||||
self.window.close();
|
self.window.close();
|
||||||
}
|
}
|
||||||
|
|
@ -945,6 +988,7 @@ impl MediaViewerWindow {
|
||||||
kind: EntryKind,
|
kind: EntryKind,
|
||||||
sibling_navigator: Option<SiblingNavigator>,
|
sibling_navigator: Option<SiblingNavigator>,
|
||||||
favorite_control: FavoriteControl,
|
favorite_control: FavoriteControl,
|
||||||
|
download_control: FileDownloadControl,
|
||||||
) {
|
) {
|
||||||
let title = source.display_name.clone();
|
let title = source.display_name.clone();
|
||||||
let window = gtk::ApplicationWindow::builder()
|
let window = gtk::ApplicationWindow::builder()
|
||||||
|
|
@ -956,6 +1000,8 @@ impl MediaViewerWindow {
|
||||||
let header = gtk::HeaderBar::new();
|
let header = gtk::HeaderBar::new();
|
||||||
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("파일 다운로드");
|
||||||
|
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));
|
||||||
|
|
||||||
|
|
@ -982,7 +1028,19 @@ impl MediaViewerWindow {
|
||||||
.margin_start(8)
|
.margin_start(8)
|
||||||
.margin_end(8)
|
.margin_end(8)
|
||||||
.build();
|
.build();
|
||||||
|
let media_seek = gtk::Scale::with_range(gtk::Orientation::Horizontal, 0.0, 0.0, 1.0);
|
||||||
|
media_seek.set_draw_value(false);
|
||||||
|
media_seek.set_margin_start(8);
|
||||||
|
media_seek.set_margin_end(8);
|
||||||
|
if let Some(navigator) = &sibling_navigator {
|
||||||
|
media_seek.set_range(0.0, navigator.count.saturating_sub(1) as f64);
|
||||||
|
media_seek.set_value(navigator.current as f64);
|
||||||
|
media_seek.set_sensitive(navigator.count > 1);
|
||||||
|
} else {
|
||||||
|
media_seek.set_sensitive(false);
|
||||||
|
}
|
||||||
root.append(&stack);
|
root.append(&stack);
|
||||||
|
root.append(&media_seek);
|
||||||
root.append(&status_label);
|
root.append(&status_label);
|
||||||
window.set_child(Some(&root));
|
window.set_child(Some(&root));
|
||||||
|
|
||||||
|
|
@ -993,9 +1051,14 @@ impl MediaViewerWindow {
|
||||||
video,
|
video,
|
||||||
stack,
|
stack,
|
||||||
status_label,
|
status_label,
|
||||||
|
media_seek,
|
||||||
sibling_navigator,
|
sibling_navigator,
|
||||||
favorite_button,
|
favorite_button,
|
||||||
favorite_toggle: favorite_control.toggle,
|
favorite_toggle: favorite_control.toggle,
|
||||||
|
download_button,
|
||||||
|
file_download: download_control.download,
|
||||||
|
seek_generation: Cell::new(0),
|
||||||
|
seek_opening: Cell::new(false),
|
||||||
});
|
});
|
||||||
if let Some(stream) = controller.video.media_stream() {
|
if let Some(stream) = controller.video.media_stream() {
|
||||||
let status_label = controller.status_label.clone();
|
let status_label = controller.status_label.clone();
|
||||||
|
|
@ -1027,9 +1090,14 @@ struct MediaViewerController {
|
||||||
video: gtk::Video,
|
video: gtk::Video,
|
||||||
stack: gtk::Stack,
|
stack: gtk::Stack,
|
||||||
status_label: gtk::Label,
|
status_label: gtk::Label,
|
||||||
|
media_seek: gtk::Scale,
|
||||||
sibling_navigator: Option<SiblingNavigator>,
|
sibling_navigator: Option<SiblingNavigator>,
|
||||||
favorite_button: gtk::Button,
|
favorite_button: gtk::Button,
|
||||||
favorite_toggle: Rc<dyn Fn() -> bool>,
|
favorite_toggle: Rc<dyn Fn() -> bool>,
|
||||||
|
download_button: gtk::Button,
|
||||||
|
file_download: Rc<dyn Fn(gtk::Window, gtk::Label)>,
|
||||||
|
seek_generation: Cell<u64>,
|
||||||
|
seek_opening: Cell<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MediaViewerController {
|
impl MediaViewerController {
|
||||||
|
|
@ -1061,19 +1129,67 @@ impl MediaViewerController {
|
||||||
let active = (controller.favorite_toggle)();
|
let active = (controller.favorite_toggle)();
|
||||||
controller.favorite_button.set_label(favorite_label(active));
|
controller.favorite_button.set_label(favorite_label(active));
|
||||||
});
|
});
|
||||||
|
let controller = Rc::clone(self);
|
||||||
|
self.download_button.connect_clicked(move |_| {
|
||||||
|
(controller.file_download)(
|
||||||
|
controller.window.clone().upcast(),
|
||||||
|
controller.status_label.clone(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
let controller = Rc::clone(self);
|
||||||
|
self.media_seek.connect_value_changed(move |scale| {
|
||||||
|
let Some(navigator) = &controller.sibling_navigator else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(index) = page_seek_index(scale.value(), navigator.count) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
controller.schedule_seek(index);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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(forward)
|
&& navigate.open_relative(forward)
|
||||||
{
|
{
|
||||||
if let Some(stream) = self.video.media_stream() {
|
self.close_for_navigation();
|
||||||
stream.pause();
|
|
||||||
}
|
|
||||||
self.window.close();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn schedule_seek(self: &Rc<Self>, index: usize) {
|
||||||
|
let Some(navigator) = &self.sibling_navigator else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if index == navigator.current || self.seek_opening.get() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let generation = self.seek_generation.get().wrapping_add(1);
|
||||||
|
self.seek_generation.set(generation);
|
||||||
|
let controller = Rc::downgrade(self);
|
||||||
|
glib::timeout_add_local_once(Duration::from_millis(180), move || {
|
||||||
|
let Some(controller) = controller.upgrade() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if controller.seek_generation.get() != generation || controller.seek_opening.get() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Some(navigator) = &controller.sibling_navigator else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if navigator.open_index(index) {
|
||||||
|
controller.seek_opening.set(true);
|
||||||
|
controller.close_for_navigation();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn close_for_navigation(&self) {
|
||||||
|
if let Some(stream) = self.video.media_stream() {
|
||||||
|
stream.pause();
|
||||||
|
}
|
||||||
|
self.window.close();
|
||||||
|
}
|
||||||
|
|
||||||
fn toggle_fullscreen(&self) {
|
fn toggle_fullscreen(&self) {
|
||||||
if self.window.is_fullscreen() {
|
if self.window.is_fullscreen() {
|
||||||
self.window.unfullscreen();
|
self.window.unfullscreen();
|
||||||
|
|
@ -1196,6 +1312,14 @@ fn page_seek_index(value: f64, count: usize) -> Option<usize> {
|
||||||
(count > 0).then(|| (value.round().max(0.0) as usize).min(count - 1))
|
(count > 0).then(|| (value.round().max(0.0) as usize).min(count - 1))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn sibling_index(current: usize, count: usize, forward: bool) -> Option<usize> {
|
||||||
|
if forward {
|
||||||
|
(current + 1 < count).then_some(current + 1)
|
||||||
|
} else {
|
||||||
|
current.checked_sub(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
@ -1245,4 +1369,12 @@ mod tests {
|
||||||
assert_eq!(page_seek_index(20.0, 10), Some(9));
|
assert_eq!(page_seek_index(20.0, 10), Some(9));
|
||||||
assert_eq!(page_seek_index(0.0, 0), None);
|
assert_eq!(page_seek_index(0.0, 0), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sibling_navigation_stops_at_boundaries() {
|
||||||
|
assert_eq!(sibling_index(1, 3, false), Some(0));
|
||||||
|
assert_eq!(sibling_index(1, 3, true), Some(2));
|
||||||
|
assert_eq!(sibling_index(0, 3, false), None);
|
||||||
|
assert_eq!(sibling_index(2, 3, true), None);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
108
src/window.rs
108
src/window.rs
|
|
@ -20,7 +20,9 @@ use crate::source::{
|
||||||
display_remote_uri, ensure_mounted, enumerate_directory, file_from_user_input, is_within_root,
|
display_remote_uri, ensure_mounted, enumerate_directory, file_from_user_input, is_within_root,
|
||||||
mount_operation, parent_within_root, root_file, validate_profile,
|
mount_operation, parent_within_root, root_file, validate_profile,
|
||||||
};
|
};
|
||||||
use crate::viewer::{FavoriteControl, MediaViewerWindow, SiblingNavigator, ViewerWindow};
|
use crate::viewer::{
|
||||||
|
FavoriteControl, FileDownloadControl, MediaViewerWindow, SiblingNavigator, ViewerWindow,
|
||||||
|
};
|
||||||
use crate::zip_cache::ZipCache;
|
use crate::zip_cache::ZipCache;
|
||||||
|
|
||||||
pub struct ComicWindow {
|
pub struct ComicWindow {
|
||||||
|
|
@ -607,12 +609,13 @@ impl BrowserController {
|
||||||
self.refresh();
|
self.refresh();
|
||||||
}
|
}
|
||||||
EntryKind::ZipArchive | EntryKind::Image | EntryKind::Video => {
|
EntryKind::ZipArchive | EntryKind::Image | EntryKind::Video => {
|
||||||
|
let current_kind = entry.kind;
|
||||||
let siblings = {
|
let siblings = {
|
||||||
let state = self.state.borrow();
|
let state = self.state.borrow();
|
||||||
let mut entries = state
|
let mut entries = state
|
||||||
.entries
|
.entries
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|entry| entry.kind.is_viewable())
|
.filter(|entry| navigable_sibling(current_kind, entry.kind))
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
sort_entries(&mut entries, state.sort_field, state.sort_direction);
|
sort_entries(&mut entries, state.sort_field, state.sort_direction);
|
||||||
|
|
@ -637,7 +640,19 @@ impl BrowserController {
|
||||||
match entry.kind {
|
match entry.kind {
|
||||||
EntryKind::ZipArchive => self.open_archive(entry, siblings, current, profile),
|
EntryKind::ZipArchive => self.open_archive(entry, siblings, current, profile),
|
||||||
EntryKind::Image | EntryKind::Video => {
|
EntryKind::Image | EntryKind::Video => {
|
||||||
self.open_media(entry, siblings, current, profile)
|
let media_siblings = Rc::new(
|
||||||
|
siblings
|
||||||
|
.iter()
|
||||||
|
.filter(|sibling| sibling.kind.is_media())
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
);
|
||||||
|
if let Some(current) = media_siblings
|
||||||
|
.iter()
|
||||||
|
.position(|sibling| sibling.uri == entry.uri)
|
||||||
|
{
|
||||||
|
self.open_media(entry, media_siblings, current, profile);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
@ -650,16 +665,17 @@ impl BrowserController {
|
||||||
profile: LocationProfile,
|
profile: LocationProfile,
|
||||||
) -> SiblingNavigator {
|
) -> SiblingNavigator {
|
||||||
let controller = Rc::downgrade(self);
|
let controller = Rc::downgrade(self);
|
||||||
Rc::new(move |forward| {
|
let count = siblings.len();
|
||||||
|
let open = Rc::new(move |next: usize| {
|
||||||
let Some(controller) = controller.upgrade() else {
|
let Some(controller) = controller.upgrade() else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
let Some(next) = sibling_index(current, siblings.len(), forward) else {
|
if next >= siblings.len() {
|
||||||
controller
|
controller
|
||||||
.status_label
|
.status_label
|
||||||
.set_text("더 이상 이동할 파일이 없습니다.");
|
.set_text("더 이상 이동할 파일이 없습니다.");
|
||||||
return false;
|
return false;
|
||||||
};
|
}
|
||||||
controller.open_viewable(
|
controller.open_viewable(
|
||||||
siblings[next].clone(),
|
siblings[next].clone(),
|
||||||
Rc::clone(&siblings),
|
Rc::clone(&siblings),
|
||||||
|
|
@ -667,7 +683,8 @@ impl BrowserController {
|
||||||
profile.clone(),
|
profile.clone(),
|
||||||
);
|
);
|
||||||
true
|
true
|
||||||
})
|
});
|
||||||
|
SiblingNavigator::new(current, count, open)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn open_archive(
|
fn open_archive(
|
||||||
|
|
@ -682,6 +699,7 @@ impl BrowserController {
|
||||||
let controller = Rc::clone(self);
|
let controller = Rc::clone(self);
|
||||||
let navigator = self.sibling_navigator(siblings, current, profile.clone());
|
let navigator = self.sibling_navigator(siblings, current, 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);
|
||||||
self.zip_cache.prepare(
|
self.zip_cache.prepare(
|
||||||
entry,
|
entry,
|
||||||
move |current, total| {
|
move |current, total| {
|
||||||
|
|
@ -701,6 +719,7 @@ impl BrowserController {
|
||||||
controller.config.borrow().fast_image_loading,
|
controller.config.borrow().fast_image_loading,
|
||||||
Some(navigator),
|
Some(navigator),
|
||||||
favorite_control,
|
favorite_control,
|
||||||
|
download_control,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Err(error) => controller.status_label.set_text(&format!("{error:#}")),
|
Err(error) => controller.status_label.set_text(&format!("{error:#}")),
|
||||||
|
|
@ -722,6 +741,7 @@ impl BrowserController {
|
||||||
let controller = Rc::clone(self);
|
let controller = Rc::clone(self);
|
||||||
let navigator = self.sibling_navigator(siblings, current, profile.clone());
|
let navigator = self.sibling_navigator(siblings, current, 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);
|
||||||
self.zip_cache.prepare_media(
|
self.zip_cache.prepare_media(
|
||||||
entry,
|
entry,
|
||||||
move |current, total| {
|
move |current, total| {
|
||||||
|
|
@ -740,6 +760,7 @@ impl BrowserController {
|
||||||
kind,
|
kind,
|
||||||
Some(navigator),
|
Some(navigator),
|
||||||
favorite_control,
|
favorite_control,
|
||||||
|
download_control,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Err(error) => controller.status_label.set_text(&format!("{error:#}")),
|
Err(error) => controller.status_label.set_text(&format!("{error:#}")),
|
||||||
|
|
@ -761,8 +782,7 @@ impl BrowserController {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let can_favorite = entry.kind == EntryKind::Directory || entry.kind.is_viewable();
|
let can_favorite = entry.kind == EntryKind::Directory || entry.kind.is_viewable();
|
||||||
let can_download =
|
let can_download = entry.kind != EntryKind::Directory;
|
||||||
entry.kind != EntryKind::Directory && !gio::File::for_uri(&entry.uri).is_native();
|
|
||||||
if !can_favorite && !can_download {
|
if !can_favorite && !can_download {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -792,13 +812,17 @@ impl BrowserController {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if can_download {
|
if can_download {
|
||||||
let download_button = gtk::Button::with_label("다운로드...");
|
let download_button = gtk::Button::with_label("파일 다운로드...");
|
||||||
actions.append(&download_button);
|
actions.append(&download_button);
|
||||||
let controller = Rc::clone(self);
|
let controller = Rc::clone(self);
|
||||||
let popover_to_close = popover.clone();
|
let popover_to_close = popover.clone();
|
||||||
download_button.connect_clicked(move |_| {
|
download_button.connect_clicked(move |_| {
|
||||||
popover_to_close.popdown();
|
popover_to_close.popdown();
|
||||||
controller.choose_download_destination(entry.clone());
|
controller.choose_download_destination(
|
||||||
|
entry.clone(),
|
||||||
|
controller.window.clone().upcast(),
|
||||||
|
controller.status_label.clone(),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
popover.set_child(Some(&actions));
|
popover.set_child(Some(&actions));
|
||||||
|
|
@ -832,6 +856,19 @@ impl BrowserController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn download_control(self: &Rc<Self>, entry: &BrowserEntry) -> FileDownloadControl {
|
||||||
|
let entry = entry.clone();
|
||||||
|
let controller = Rc::downgrade(self);
|
||||||
|
FileDownloadControl {
|
||||||
|
download: Rc::new(move |parent, status_label| {
|
||||||
|
let Some(controller) = controller.upgrade() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
controller.choose_download_destination(entry.clone(), parent, status_label);
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn toggle_favorite(&self, entry: &BrowserEntry, profile: &LocationProfile) -> bool {
|
fn toggle_favorite(&self, entry: &BrowserEntry, profile: &LocationProfile) -> bool {
|
||||||
let mut updated = self.config.borrow().clone();
|
let mut updated = self.config.borrow().clone();
|
||||||
let was_active = self.is_favorite(&entry.uri);
|
let was_active = self.is_favorite(&entry.uri);
|
||||||
|
|
@ -851,13 +888,17 @@ impl BrowserController {
|
||||||
active
|
active
|
||||||
}
|
}
|
||||||
|
|
||||||
fn choose_download_destination(self: &Rc<Self>, entry: BrowserEntry) {
|
fn choose_download_destination(
|
||||||
|
self: &Rc<Self>,
|
||||||
|
entry: BrowserEntry,
|
||||||
|
parent: gtk::Window,
|
||||||
|
status_label: gtk::Label,
|
||||||
|
) {
|
||||||
let dialog = gtk::FileDialog::builder()
|
let dialog = gtk::FileDialog::builder()
|
||||||
.title("다운로드 위치 선택")
|
.title("다운로드 위치 선택")
|
||||||
.initial_name(&entry.name)
|
.initial_name(&entry.name)
|
||||||
.build();
|
.build();
|
||||||
let controller = Rc::downgrade(self);
|
let controller = Rc::downgrade(self);
|
||||||
let parent = self.window.clone();
|
|
||||||
glib::MainContext::default().spawn_local(async move {
|
glib::MainContext::default().spawn_local(async move {
|
||||||
let result = dialog.save_future(Some(&parent)).await;
|
let result = dialog.save_future(Some(&parent)).await;
|
||||||
let Some(controller) = controller.upgrade() else {
|
let Some(controller) = controller.upgrade() else {
|
||||||
|
|
@ -865,30 +906,27 @@ impl BrowserController {
|
||||||
};
|
};
|
||||||
match result {
|
match result {
|
||||||
Ok(destination) if destination.is_native() => {
|
Ok(destination) if destination.is_native() => {
|
||||||
controller.download_remote_file(entry, destination);
|
controller.download_file(entry, destination, status_label);
|
||||||
}
|
}
|
||||||
Ok(_) => controller
|
Ok(_) => status_label.set_text("로컬 파일 시스템 위치를 선택해 주세요."),
|
||||||
.status_label
|
|
||||||
.set_text("로컬 파일 시스템 위치를 선택해 주세요."),
|
|
||||||
Err(error) if error.matches(gio::IOErrorEnum::Cancelled) => {}
|
Err(error) if error.matches(gio::IOErrorEnum::Cancelled) => {}
|
||||||
Err(error) => controller
|
Err(error) => {
|
||||||
.status_label
|
status_label.set_text(&format!("저장 위치를 선택할 수 없습니다: {error}"));
|
||||||
.set_text(&format!("저장 위치를 선택할 수 없습니다: {error}")),
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn download_remote_file(&self, entry: BrowserEntry, destination: gio::File) {
|
fn download_file(&self, entry: BrowserEntry, destination: gio::File, status_label: gtk::Label) {
|
||||||
let Some(final_path) = destination.path() else {
|
let Some(final_path) = destination.path() else {
|
||||||
self.status_label
|
status_label.set_text("로컬 파일 시스템 위치를 선택해 주세요.");
|
||||||
.set_text("로컬 파일 시스템 위치를 선택해 주세요.");
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let part_path = download_part_path(&final_path);
|
let part_path = download_part_path(&final_path);
|
||||||
let source = gio::File::for_uri(&entry.uri);
|
let source = gio::File::for_uri(&entry.uri);
|
||||||
let part_file = gio::File::for_path(&part_path);
|
let part_file = gio::File::for_path(&part_path);
|
||||||
let progress_label = self.status_label.clone();
|
let progress_label = status_label.clone();
|
||||||
let result_label = self.status_label.clone();
|
let result_label = status_label;
|
||||||
let expected_size = entry.size;
|
let expected_size = entry.size;
|
||||||
source.copy_async(
|
source.copy_async(
|
||||||
&part_file,
|
&part_file,
|
||||||
|
|
@ -1199,11 +1237,11 @@ fn find_prefix_index(entries: &[BrowserEntry], query: &str, start: usize) -> Opt
|
||||||
.find(|index| entries[*index].name.to_lowercase().starts_with(&query))
|
.find(|index| entries[*index].name.to_lowercase().starts_with(&query))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sibling_index(current: usize, count: usize, forward: bool) -> Option<usize> {
|
fn navigable_sibling(current: EntryKind, candidate: EntryKind) -> bool {
|
||||||
if forward {
|
match current {
|
||||||
(current + 1 < count).then_some(current + 1)
|
EntryKind::ZipArchive => candidate.is_viewable(),
|
||||||
} else {
|
EntryKind::Image | EntryKind::Video => candidate.is_media(),
|
||||||
current.checked_sub(1)
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1270,11 +1308,11 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn sibling_navigation_stops_at_boundaries() {
|
fn media_navigation_excludes_zip_archives() {
|
||||||
assert_eq!(sibling_index(1, 3, false), Some(0));
|
assert!(navigable_sibling(EntryKind::Image, EntryKind::Video));
|
||||||
assert_eq!(sibling_index(1, 3, true), Some(2));
|
assert!(navigable_sibling(EntryKind::Video, EntryKind::Image));
|
||||||
assert_eq!(sibling_index(0, 3, false), None);
|
assert!(!navigable_sibling(EntryKind::Image, EntryKind::ZipArchive));
|
||||||
assert_eq!(sibling_index(2, 3, true), None);
|
assert!(!navigable_sibling(EntryKind::Video, EntryKind::OtherFile));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue