Add standalone media and remote downloads
This commit is contained in:
parent
8a213462e0
commit
5f9b5d9796
14 changed files with 683 additions and 86 deletions
240
src/window.rs
240
src/window.rs
|
|
@ -1,4 +1,5 @@
|
|||
use std::cell::RefCell;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
|
@ -17,7 +18,7 @@ use crate::source::{
|
|||
display_remote_uri, enumerate_directory, file_from_user_input, is_within_root, mount_operation,
|
||||
parent_within_root, root_file, validate_profile,
|
||||
};
|
||||
use crate::viewer::ViewerWindow;
|
||||
use crate::viewer::{MediaViewerWindow, SiblingNavigator, ViewerWindow};
|
||||
use crate::zip_cache::ZipCache;
|
||||
|
||||
pub struct ComicWindow {
|
||||
|
|
@ -239,6 +240,13 @@ impl BrowserController {
|
|||
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 |_, _, _, y| {
|
||||
controller.show_context_menu(y as i32);
|
||||
});
|
||||
self.list_box.add_controller(context_click);
|
||||
|
||||
let key_controller = gtk::EventControllerKey::new();
|
||||
let controller = Rc::clone(self);
|
||||
key_controller
|
||||
|
|
@ -480,14 +488,71 @@ impl BrowserController {
|
|||
self.search_entry.set_text("");
|
||||
self.refresh();
|
||||
}
|
||||
EntryKind::ZipArchive => self.open_archive(entry),
|
||||
EntryKind::ZipArchive | EntryKind::Image | EntryKind::Video => {
|
||||
let siblings = {
|
||||
let state = self.state.borrow();
|
||||
let mut entries = state
|
||||
.entries
|
||||
.iter()
|
||||
.filter(|entry| entry.kind.is_viewable())
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
sort_entries(&mut entries, state.sort_field, state.sort_direction);
|
||||
entries
|
||||
};
|
||||
let siblings = Rc::new(siblings);
|
||||
if let Some(current) = siblings.iter().position(|item| item.uri == entry.uri) {
|
||||
self.open_viewable(entry, siblings, current);
|
||||
}
|
||||
}
|
||||
EntryKind::OtherFile => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn open_archive(self: &Rc<Self>, entry: BrowserEntry) {
|
||||
fn open_viewable(
|
||||
self: &Rc<Self>,
|
||||
entry: BrowserEntry,
|
||||
siblings: Rc<Vec<BrowserEntry>>,
|
||||
current: usize,
|
||||
) {
|
||||
match entry.kind {
|
||||
EntryKind::ZipArchive => self.open_archive(entry, siblings, current),
|
||||
EntryKind::Image | EntryKind::Video => self.open_media(entry, siblings, current),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn sibling_navigator(
|
||||
self: &Rc<Self>,
|
||||
siblings: Rc<Vec<BrowserEntry>>,
|
||||
current: usize,
|
||||
) -> SiblingNavigator {
|
||||
let controller = Rc::downgrade(self);
|
||||
Rc::new(move |forward| {
|
||||
let Some(controller) = controller.upgrade() else {
|
||||
return false;
|
||||
};
|
||||
let Some(next) = sibling_index(current, siblings.len(), forward) else {
|
||||
controller
|
||||
.status_label
|
||||
.set_text("더 이상 이동할 파일이 없습니다.");
|
||||
return false;
|
||||
};
|
||||
controller.open_viewable(siblings[next].clone(), Rc::clone(&siblings), next);
|
||||
true
|
||||
})
|
||||
}
|
||||
|
||||
fn open_archive(
|
||||
self: &Rc<Self>,
|
||||
entry: BrowserEntry,
|
||||
siblings: Rc<Vec<BrowserEntry>>,
|
||||
current: usize,
|
||||
) {
|
||||
self.status_label.set_text("ZIP 파일 다운로드 준비 중...");
|
||||
let progress_label = self.status_label.clone();
|
||||
let controller = Rc::clone(self);
|
||||
let navigator = self.sibling_navigator(siblings, current);
|
||||
self.zip_cache.prepare(
|
||||
entry,
|
||||
move |current, total| {
|
||||
|
|
@ -505,6 +570,7 @@ impl BrowserController {
|
|||
source,
|
||||
Rc::clone(&controller.database),
|
||||
controller.config.borrow().fast_image_loading,
|
||||
Some(navigator),
|
||||
);
|
||||
}
|
||||
Err(error) => controller.status_label.set_text(&format!("{error:#}")),
|
||||
|
|
@ -512,6 +578,138 @@ impl BrowserController {
|
|||
);
|
||||
}
|
||||
|
||||
fn open_media(
|
||||
self: &Rc<Self>,
|
||||
entry: BrowserEntry,
|
||||
siblings: Rc<Vec<BrowserEntry>>,
|
||||
current: usize,
|
||||
) {
|
||||
let kind = entry.kind;
|
||||
self.status_label
|
||||
.set_text("미디어 파일 다운로드 준비 중...");
|
||||
let progress_label = self.status_label.clone();
|
||||
let controller = Rc::clone(self);
|
||||
let navigator = self.sibling_navigator(siblings, current);
|
||||
self.zip_cache.prepare_media(
|
||||
entry,
|
||||
move |current, total| {
|
||||
let percent = if total > 0 { current * 100 / total } else { 0 };
|
||||
progress_label.set_text(&format!("미디어 다운로드 중... {percent}%"));
|
||||
},
|
||||
move |result| match result {
|
||||
Ok(source) => {
|
||||
controller.status_label.set_text("다운로드 완료");
|
||||
let Some(application) = controller.window.application() else {
|
||||
return;
|
||||
};
|
||||
MediaViewerWindow::open(&application, source, kind, Some(navigator));
|
||||
}
|
||||
Err(error) => controller.status_label.set_text(&format!("{error:#}")),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn show_context_menu(self: &Rc<Self>, y: i32) {
|
||||
let Some(row) = self.list_box.row_at_y(y) else {
|
||||
return;
|
||||
};
|
||||
let Some(entry) = self
|
||||
.state
|
||||
.borrow()
|
||||
.visible_entries
|
||||
.get(row.index() as usize)
|
||||
.cloned()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if entry.kind == EntryKind::Directory || gio::File::for_uri(&entry.uri).is_native() {
|
||||
return;
|
||||
}
|
||||
self.list_box.select_row(Some(&row));
|
||||
let popover = gtk::Popover::new();
|
||||
popover.set_parent(&row);
|
||||
let download_button = gtk::Button::with_label("다운로드...");
|
||||
download_button.set_margin_top(6);
|
||||
download_button.set_margin_bottom(6);
|
||||
download_button.set_margin_start(6);
|
||||
download_button.set_margin_end(6);
|
||||
popover.set_child(Some(&download_button));
|
||||
let controller = Rc::clone(self);
|
||||
let popover_to_close = popover.clone();
|
||||
download_button.connect_clicked(move |_| {
|
||||
popover_to_close.popdown();
|
||||
controller.choose_download_destination(entry.clone());
|
||||
});
|
||||
popover.popup();
|
||||
}
|
||||
|
||||
fn choose_download_destination(self: &Rc<Self>, entry: BrowserEntry) {
|
||||
let dialog = gtk::FileDialog::builder()
|
||||
.title("다운로드 위치 선택")
|
||||
.initial_name(&entry.name)
|
||||
.build();
|
||||
let controller = Rc::downgrade(self);
|
||||
let parent = self.window.clone();
|
||||
glib::MainContext::default().spawn_local(async move {
|
||||
let result = dialog.save_future(Some(&parent)).await;
|
||||
let Some(controller) = controller.upgrade() else {
|
||||
return;
|
||||
};
|
||||
match result {
|
||||
Ok(destination) if destination.is_native() => {
|
||||
controller.download_remote_file(entry, destination);
|
||||
}
|
||||
Ok(_) => controller
|
||||
.status_label
|
||||
.set_text("로컬 파일 시스템 위치를 선택해 주세요."),
|
||||
Err(error) if error.matches(gio::IOErrorEnum::Cancelled) => {}
|
||||
Err(error) => controller
|
||||
.status_label
|
||||
.set_text(&format!("저장 위치를 선택할 수 없습니다: {error}")),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn download_remote_file(&self, entry: BrowserEntry, destination: gio::File) {
|
||||
let Some(final_path) = destination.path() else {
|
||||
self.status_label
|
||||
.set_text("로컬 파일 시스템 위치를 선택해 주세요.");
|
||||
return;
|
||||
};
|
||||
let part_path = download_part_path(&final_path);
|
||||
let source = gio::File::for_uri(&entry.uri);
|
||||
let part_file = gio::File::for_path(&part_path);
|
||||
let progress_label = self.status_label.clone();
|
||||
let result_label = self.status_label.clone();
|
||||
let expected_size = entry.size;
|
||||
source.copy_async(
|
||||
&part_file,
|
||||
gio::FileCopyFlags::OVERWRITE,
|
||||
glib::Priority::DEFAULT,
|
||||
gio::Cancellable::NONE,
|
||||
Some(Box::new(move |current, total| {
|
||||
let percent = if total > 0 { current * 100 / total } else { 0 };
|
||||
progress_label.set_text(&format!("{} 다운로드 중... {percent}%", entry.name));
|
||||
})),
|
||||
move |result| {
|
||||
let completed = result.map_err(anyhow::Error::from).and_then(|()| {
|
||||
let actual_size = fs::metadata(&part_path)?.len();
|
||||
if expected_size.is_some_and(|expected| expected != actual_size) {
|
||||
anyhow::bail!("다운로드 크기가 원본과 다릅니다.");
|
||||
}
|
||||
fs::rename(&part_path, &final_path)?;
|
||||
Ok(())
|
||||
});
|
||||
if let Err(error) = completed {
|
||||
let _ = fs::remove_file(&part_path);
|
||||
result_label.set_text(&format!("다운로드 실패: {error:#}"));
|
||||
} else {
|
||||
result_label.set_text(&format!("다운로드 완료: {}", final_path.display()));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn set_fast_image_loading(&self, switch: >k::Switch) {
|
||||
let active = switch.is_active();
|
||||
let previous = self.config.borrow().fast_image_loading;
|
||||
|
|
@ -752,6 +950,9 @@ fn entry_row(entry: &BrowserEntry) -> gtk::ListBoxRow {
|
|||
let icon = 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(
|
||||
|
|
@ -790,6 +991,23 @@ fn find_prefix_index(entries: &[BrowserEntry], query: &str, start: usize) -> Opt
|
|||
.find(|index| entries[*index].name.to_lowercase().starts_with(&query))
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
fn download_part_path(final_path: &std::path::Path) -> PathBuf {
|
||||
final_path.with_file_name(format!(
|
||||
"{}.part",
|
||||
final_path
|
||||
.file_name()
|
||||
.map_or_else(|| "download".into(), |name| name.to_string_lossy())
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
@ -819,4 +1037,20 @@ mod tests {
|
|||
assert_eq!(find_prefix_index(&entries, "b", 2), Some(2));
|
||||
assert_eq!(find_prefix_index(&entries, "b", 3), Some(1));
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn incomplete_download_uses_distinct_suffix() {
|
||||
assert_eq!(
|
||||
download_part_path(std::path::Path::new("/tmp/만화.zip")),
|
||||
PathBuf::from("/tmp/만화.zip.part")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue