Release v0.2.2
This commit is contained in:
parent
048df6355e
commit
b31acd3f58
8 changed files with 309 additions and 16 deletions
230
src/window.rs
230
src/window.rs
|
|
@ -4,7 +4,7 @@ use std::path::{Path, PathBuf};
|
|||
use std::rc::Rc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{Context, Result, bail};
|
||||
use chrono::{DateTime, Local};
|
||||
use gtk::gio;
|
||||
use gtk::gio::prelude::*;
|
||||
|
|
@ -21,9 +21,9 @@ use crate::source::{
|
|||
mount_operation, parent_within_root, root_file, validate_profile,
|
||||
};
|
||||
use crate::viewer::{
|
||||
FavoriteControl, FileDownloadControl, FileViewerControls, MediaViewerWindow, SiblingFile,
|
||||
SiblingFileActions, SiblingNavigator, SiblingTarget, ViewModeControl, ViewerWindow,
|
||||
set_viewer_header,
|
||||
FavoriteControl, FileDownloadControl, FileDropControl, FileViewerControls, MediaViewerWindow,
|
||||
SiblingFile, SiblingFileActions, SiblingNavigator, SiblingTarget, ViewModeControl,
|
||||
ViewerWindow, install_file_drop_target, set_viewer_header,
|
||||
};
|
||||
use crate::zip_cache::ZipCache;
|
||||
|
||||
|
|
@ -171,6 +171,7 @@ impl ComicWindow {
|
|||
case_insensitive_check,
|
||||
fast_loading_switch,
|
||||
);
|
||||
install_file_drop_target(&controller.window, controller.file_drop_control());
|
||||
controller.refresh();
|
||||
Ok(Self { controller })
|
||||
}
|
||||
|
|
@ -956,6 +957,7 @@ impl BrowserController {
|
|||
sibling_navigator: Some(navigator),
|
||||
favorite_control,
|
||||
download_control,
|
||||
file_drop_control: controller.file_drop_control(),
|
||||
reuse_window,
|
||||
view_mode_control: None,
|
||||
},
|
||||
|
|
@ -1019,6 +1021,7 @@ impl BrowserController {
|
|||
sibling_navigator: Some(navigator),
|
||||
favorite_control,
|
||||
download_control,
|
||||
file_drop_control: controller.file_drop_control(),
|
||||
reuse_window,
|
||||
view_mode_control,
|
||||
},
|
||||
|
|
@ -1147,6 +1150,140 @@ impl BrowserController {
|
|||
}
|
||||
}
|
||||
|
||||
fn file_drop_control(self: &Rc<Self>) -> FileDropControl {
|
||||
let controller = Rc::downgrade(self);
|
||||
FileDropControl {
|
||||
handle: Rc::new(move |files, parent| {
|
||||
if let Some(controller) = controller.upgrade() {
|
||||
controller.open_dropped_files(files, parent);
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn open_dropped_files(self: &Rc<Self>, files: Vec<gio::File>, parent: gtk::Window) {
|
||||
let [file] = files.as_slice() else {
|
||||
show_drop_error(&parent, "한 번에 하나의 파일 또는 폴더만 열 수 있습니다.");
|
||||
return;
|
||||
};
|
||||
if !file.is_native() {
|
||||
show_drop_error(&parent, "로컬 파일 또는 폴더만 드래그해서 열 수 있습니다.");
|
||||
return;
|
||||
}
|
||||
let file = file.clone();
|
||||
self.cancel_listing();
|
||||
let generation = {
|
||||
let mut state = self.state.borrow_mut();
|
||||
state.generation = state.generation.wrapping_add(1);
|
||||
state.generation
|
||||
};
|
||||
self.status_label.set_text("드롭한 항목을 확인하는 중...");
|
||||
let controller = Rc::downgrade(self);
|
||||
let task = glib::MainContext::default().spawn_local(async move {
|
||||
let result: Result<_> = async {
|
||||
let info = file
|
||||
.query_info_future(
|
||||
"standard::type,standard::display-name",
|
||||
gio::FileQueryInfoFlags::NONE,
|
||||
glib::Priority::DEFAULT,
|
||||
)
|
||||
.await
|
||||
.context("드롭한 항목을 확인할 수 없습니다")?;
|
||||
let selected = match info.file_type() {
|
||||
gio::FileType::Directory => None,
|
||||
gio::FileType::Regular => {
|
||||
if !classify_file(Path::new(info.display_name().as_str())).is_viewable() {
|
||||
bail!("지원하지 않는 파일 형식입니다: {}", info.display_name());
|
||||
}
|
||||
Some(file.clone())
|
||||
}
|
||||
_ => bail!("일반 파일 또는 폴더만 열 수 있습니다"),
|
||||
};
|
||||
let directory = selected
|
||||
.as_ref()
|
||||
.and_then(gio::File::parent)
|
||||
.unwrap_or_else(|| file.clone());
|
||||
let profile = local_profile_for_directory(&directory)?;
|
||||
let operation = mount_operation(&profile, &parent);
|
||||
let entries =
|
||||
enumerate_directory(&profile, directory.uri().as_str(), &operation, |_| {})
|
||||
.await?;
|
||||
Ok((directory, profile, entries, selected))
|
||||
}
|
||||
.await;
|
||||
|
||||
let Some(controller) = controller.upgrade() else {
|
||||
return;
|
||||
};
|
||||
if controller.state.borrow().generation != generation || !parent.is_visible() {
|
||||
return;
|
||||
}
|
||||
let (directory, profile, entries, selected) = match result {
|
||||
Ok(result) => result,
|
||||
Err(error) => {
|
||||
let message = format!("드롭한 항목을 열 수 없습니다: {error:#}");
|
||||
controller.status_label.set_text(&message);
|
||||
show_drop_error(&parent, &message);
|
||||
return;
|
||||
}
|
||||
};
|
||||
controller.show_dropped_directory(&directory, profile.clone(), entries.clone());
|
||||
let Some(selected) = selected else {
|
||||
return;
|
||||
};
|
||||
let Some(entry) = entries
|
||||
.iter()
|
||||
.find(|entry| gio::File::for_uri(&entry.uri).equal(&selected))
|
||||
.cloned()
|
||||
else {
|
||||
show_drop_error(&parent, "드롭한 파일을 부모 폴더에서 찾을 수 없습니다.");
|
||||
return;
|
||||
};
|
||||
let state = controller.state.borrow();
|
||||
let Some((siblings, _selected_index)) = sorted_navigable_selection(
|
||||
&entries,
|
||||
&entry.uri,
|
||||
state.sort_field,
|
||||
state.sort_direction,
|
||||
controller.config.borrow().case_insensitive_sort,
|
||||
) else {
|
||||
drop(state);
|
||||
show_drop_error(&parent, "드롭한 파일의 목록 위치를 확인할 수 없습니다.");
|
||||
return;
|
||||
};
|
||||
drop(state);
|
||||
controller.open_viewable(entry, Rc::new(siblings), profile, None);
|
||||
});
|
||||
self.listing_task.replace(Some(task));
|
||||
}
|
||||
|
||||
fn show_dropped_directory(
|
||||
&self,
|
||||
directory: &gio::File,
|
||||
profile: LocationProfile,
|
||||
entries: Vec<BrowserEntry>,
|
||||
) {
|
||||
self.location_dropdown
|
||||
.set_selected(gtk::INVALID_LIST_POSITION);
|
||||
self.edit_location_button.set_sensitive(false);
|
||||
self.delete_location_button.set_sensitive(false);
|
||||
self.path_entry.set_editable(true);
|
||||
let uri: String = directory.uri().into();
|
||||
let mut state = self.state.borrow_mut();
|
||||
state.favorite_profile = Some(profile);
|
||||
state.browsing_favorites = false;
|
||||
state.root_uri = uri.clone();
|
||||
state.current_uri = uri.clone();
|
||||
state.entries = entries;
|
||||
state.typeahead_query.clear();
|
||||
state.typeahead_last_input = None;
|
||||
drop(state);
|
||||
self.path_entry.set_text(&display_uri(&uri));
|
||||
self.search_entry.set_text("");
|
||||
self.render();
|
||||
self.window.present();
|
||||
}
|
||||
|
||||
fn view_mode_control(self: &Rc<Self>) -> ViewModeControl {
|
||||
let selected = self.config.borrow().viewer_mode.dropdown_index();
|
||||
let controller = Rc::downgrade(self);
|
||||
|
|
@ -1599,6 +1736,37 @@ fn home_profile() -> LocationProfile {
|
|||
}
|
||||
}
|
||||
|
||||
fn local_profile_for_directory(directory: &gio::File) -> Result<LocationProfile> {
|
||||
let path = directory
|
||||
.path()
|
||||
.context("로컬 폴더 경로를 확인할 수 없습니다")?;
|
||||
let address = path
|
||||
.to_str()
|
||||
.context("UTF-8로 표현할 수 없는 로컬 경로는 열 수 없습니다")?;
|
||||
let name = path
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.filter(|name| !name.is_empty())
|
||||
.unwrap_or(address)
|
||||
.to_owned();
|
||||
Ok(LocationProfile {
|
||||
name,
|
||||
kind: LocationKind::Local,
|
||||
address: address.to_owned(),
|
||||
username: String::new(),
|
||||
password: String::new(),
|
||||
domain: String::new(),
|
||||
})
|
||||
}
|
||||
|
||||
fn show_drop_error(parent: >k::Window, message: &str) {
|
||||
gtk::AlertDialog::builder()
|
||||
.modal(true)
|
||||
.message(message)
|
||||
.build()
|
||||
.show(Some(parent));
|
||||
}
|
||||
|
||||
fn display_uri(uri: &str) -> String {
|
||||
let file = gio::File::for_uri(uri);
|
||||
file.path().map_or_else(
|
||||
|
|
@ -1682,6 +1850,24 @@ fn sorted_navigable_entries(
|
|||
siblings
|
||||
}
|
||||
|
||||
fn sorted_navigable_selection(
|
||||
entries: &[BrowserEntry],
|
||||
selected_uri: &str,
|
||||
sort_field: SortField,
|
||||
sort_direction: SortDirection,
|
||||
case_insensitive: bool,
|
||||
) -> Option<(Vec<BrowserEntry>, usize)> {
|
||||
let entries = sorted_navigable_entries(
|
||||
entries,
|
||||
EntryKind::Image,
|
||||
sort_field,
|
||||
sort_direction,
|
||||
case_insensitive,
|
||||
);
|
||||
let selected = entries.iter().position(|entry| entry.uri == selected_uri)?;
|
||||
Some((entries, selected))
|
||||
}
|
||||
|
||||
fn favorite_browser_entry(favorite: &FavoriteEntry) -> BrowserEntry {
|
||||
BrowserEntry {
|
||||
uri: favorite.uri.clone(),
|
||||
|
|
@ -1835,6 +2021,42 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dropped_file_selection_uses_sorted_parent_index() {
|
||||
let entries = [
|
||||
media_entry("page10.jpg"),
|
||||
entry("folder"),
|
||||
media_entry("page2.jpg"),
|
||||
];
|
||||
let selected_uri = entries[0].uri.clone();
|
||||
|
||||
let (siblings, selected) = sorted_navigable_selection(
|
||||
&entries,
|
||||
&selected_uri,
|
||||
SortField::Name,
|
||||
SortDirection::Ascending,
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(siblings.len(), 2);
|
||||
assert_eq!(siblings[0].name, "page2.jpg");
|
||||
assert_eq!(siblings[selected].name, "page10.jpg");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dropped_directory_creates_local_root_profile() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let file = gio::File::for_path(directory.path());
|
||||
|
||||
let profile = local_profile_for_directory(&file).unwrap();
|
||||
|
||||
assert_eq!(profile.kind, LocationKind::Local);
|
||||
assert_eq!(Path::new(&profile.address), directory.path());
|
||||
assert!(profile.username.is_empty());
|
||||
assert!(profile.password.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn favorites_are_rendered_as_browser_paths_and_files() {
|
||||
let profile = home_profile();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue