Reuse viewer windows and organize favorites
This commit is contained in:
parent
ef2a0a7915
commit
bb73c1abea
3 changed files with 587 additions and 163 deletions
|
|
@ -10,6 +10,7 @@ use serde::{Deserialize, Serialize};
|
|||
pub struct AppConfig {
|
||||
pub cache_quota_gb: u64,
|
||||
pub fast_image_loading: bool,
|
||||
pub viewer_mode: ViewerMode,
|
||||
pub locations: Vec<LocationProfile>,
|
||||
pub favorites: Vec<FavoriteEntry>,
|
||||
}
|
||||
|
|
@ -19,6 +20,7 @@ impl Default for AppConfig {
|
|||
Self {
|
||||
cache_quota_gb: 10,
|
||||
fast_image_loading: true,
|
||||
viewer_mode: ViewerMode::FitWidth,
|
||||
locations: Vec::new(),
|
||||
favorites: Vec::new(),
|
||||
}
|
||||
|
|
@ -55,6 +57,39 @@ pub enum LocationKind {
|
|||
Sftp,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ViewerMode {
|
||||
ActualSize,
|
||||
#[default]
|
||||
FitWidth,
|
||||
FitHeight,
|
||||
TwoPage,
|
||||
Continuous,
|
||||
}
|
||||
|
||||
impl ViewerMode {
|
||||
pub fn dropdown_index(self) -> u32 {
|
||||
match self {
|
||||
Self::ActualSize => 0,
|
||||
Self::FitWidth => 1,
|
||||
Self::FitHeight => 2,
|
||||
Self::TwoPage => 3,
|
||||
Self::Continuous => 4,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_dropdown_index(index: u32) -> Self {
|
||||
match index {
|
||||
0 => Self::ActualSize,
|
||||
2 => Self::FitHeight,
|
||||
3 => Self::TwoPage,
|
||||
4 => Self::Continuous,
|
||||
_ => Self::FitWidth,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AppConfig {
|
||||
pub fn load_or_create(path: &Path) -> Result<Self> {
|
||||
if path.exists() {
|
||||
|
|
@ -139,6 +174,7 @@ mod tests {
|
|||
let config = AppConfig {
|
||||
cache_quota_gb: 0,
|
||||
fast_image_loading: true,
|
||||
viewer_mode: ViewerMode::FitWidth,
|
||||
locations: vec![LocationProfile {
|
||||
name: "테스트".into(),
|
||||
kind: LocationKind::Sftp,
|
||||
|
|
@ -173,6 +209,21 @@ mod tests {
|
|||
fn fast_image_loading_is_enabled_for_existing_configs() {
|
||||
let decoded: AppConfig = toml::from_str("cache_quota_gb = 10\n").unwrap();
|
||||
assert!(decoded.fast_image_loading);
|
||||
assert_eq!(decoded.viewer_mode, ViewerMode::FitWidth);
|
||||
assert!(decoded.favorites.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn viewer_modes_round_trip_through_dropdown_indices() {
|
||||
for mode in [
|
||||
ViewerMode::ActualSize,
|
||||
ViewerMode::FitWidth,
|
||||
ViewerMode::FitHeight,
|
||||
ViewerMode::TwoPage,
|
||||
ViewerMode::Continuous,
|
||||
] {
|
||||
assert_eq!(ViewerMode::from_dropdown_index(mode.dropdown_index()), mode);
|
||||
}
|
||||
assert_eq!(ViewerMode::from_dropdown_index(99), ViewerMode::FitWidth);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
306
src/viewer.rs
306
src/viewer.rs
|
|
@ -24,11 +24,15 @@ pub struct ViewerWindow;
|
|||
pub struct SiblingNavigator {
|
||||
current: usize,
|
||||
count: usize,
|
||||
open: Rc<dyn Fn(usize) -> bool>,
|
||||
open: Rc<dyn Fn(usize, gtk::ApplicationWindow) -> bool>,
|
||||
}
|
||||
|
||||
impl SiblingNavigator {
|
||||
pub fn new(current: usize, count: usize, open: Rc<dyn Fn(usize) -> bool>) -> Self {
|
||||
pub fn new(
|
||||
current: usize,
|
||||
count: usize,
|
||||
open: Rc<dyn Fn(usize, gtk::ApplicationWindow) -> bool>,
|
||||
) -> Self {
|
||||
Self {
|
||||
current,
|
||||
count,
|
||||
|
|
@ -36,15 +40,15 @@ impl SiblingNavigator {
|
|||
}
|
||||
}
|
||||
|
||||
fn open_relative(&self, forward: bool) -> bool {
|
||||
fn open_relative(&self, forward: bool, window: gtk::ApplicationWindow) -> bool {
|
||||
let Some(index) = sibling_index(self.current, self.count, forward) else {
|
||||
return false;
|
||||
};
|
||||
(self.open)(index)
|
||||
(self.open)(index, window)
|
||||
}
|
||||
|
||||
fn open_index(&self, index: usize) -> bool {
|
||||
index < self.count && index != self.current && (self.open)(index)
|
||||
fn open_index(&self, index: usize, window: gtk::ApplicationWindow) -> bool {
|
||||
index < self.count && index != self.current && (self.open)(index, window)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -57,23 +61,43 @@ pub struct FileDownloadControl {
|
|||
pub download: Rc<dyn Fn(gtk::Window, gtk::Label)>,
|
||||
}
|
||||
|
||||
pub struct ViewModeControl {
|
||||
pub selected: u32,
|
||||
pub change: Rc<dyn Fn(u32)>,
|
||||
}
|
||||
|
||||
pub struct FileViewerControls {
|
||||
pub sibling_navigator: Option<SiblingNavigator>,
|
||||
pub favorite_control: FavoriteControl,
|
||||
pub download_control: FileDownloadControl,
|
||||
pub reuse_window: Option<gtk::ApplicationWindow>,
|
||||
}
|
||||
|
||||
impl ViewerWindow {
|
||||
pub fn open(
|
||||
application: >k::Application,
|
||||
source: ArchiveSource,
|
||||
database: Rc<Database>,
|
||||
fast_image_loading: bool,
|
||||
sibling_navigator: Option<SiblingNavigator>,
|
||||
favorite_control: FavoriteControl,
|
||||
download_control: FileDownloadControl,
|
||||
view_mode_control: ViewModeControl,
|
||||
controls: FileViewerControls,
|
||||
) {
|
||||
let FileViewerControls {
|
||||
sibling_navigator,
|
||||
favorite_control,
|
||||
download_control,
|
||||
reuse_window,
|
||||
} = controls;
|
||||
let title = source.display_name.clone();
|
||||
let window = gtk::ApplicationWindow::builder()
|
||||
let window = reuse_window.unwrap_or_else(|| {
|
||||
gtk::ApplicationWindow::builder()
|
||||
.application(application)
|
||||
.title(&title)
|
||||
.default_width(1100)
|
||||
.default_height(850)
|
||||
.build();
|
||||
.build()
|
||||
});
|
||||
window.set_title(Some(&title));
|
||||
let header = gtk::HeaderBar::new();
|
||||
header.set_title_widget(Some(>k::Label::new(Some(&title))));
|
||||
let favorite_button = gtk::Button::with_label(favorite_label(favorite_control.active));
|
||||
|
|
@ -109,7 +133,7 @@ impl ViewerWindow {
|
|||
"2페이지",
|
||||
"이어서 보기",
|
||||
]);
|
||||
mode_dropdown.set_selected(1);
|
||||
mode_dropdown.set_selected(view_mode_control.selected);
|
||||
toolbar.append(&previous_button);
|
||||
toolbar.append(&shift_back_button);
|
||||
toolbar.append(&shift_forward_button);
|
||||
|
|
@ -159,14 +183,12 @@ impl ViewerWindow {
|
|||
.margin_start(8)
|
||||
.margin_end(8)
|
||||
.build();
|
||||
let page_seek = gtk::Scale::with_range(gtk::Orientation::Horizontal, 0.0, 0.0, 1.0);
|
||||
page_seek.set_draw_value(false);
|
||||
let page_seek = navigation_scale();
|
||||
let page_seek_row = navigation_scale_row(&page_seek);
|
||||
page_seek.set_sensitive(false);
|
||||
page_seek.set_margin_start(8);
|
||||
page_seek.set_margin_end(8);
|
||||
root.append(&toolbar);
|
||||
root.append(&overlay);
|
||||
root.append(&page_seek);
|
||||
root.append(&page_seek_row);
|
||||
root.append(&status_label);
|
||||
window.set_child(Some(&root));
|
||||
|
||||
|
|
@ -183,6 +205,7 @@ impl ViewerWindow {
|
|||
spinner,
|
||||
page_label,
|
||||
page_seek,
|
||||
page_seek_row,
|
||||
status_label,
|
||||
previous_button,
|
||||
next_button,
|
||||
|
|
@ -196,6 +219,11 @@ impl ViewerWindow {
|
|||
favorite_toggle: favorite_control.toggle,
|
||||
download_button,
|
||||
file_download: download_control.download,
|
||||
view_mode_change: view_mode_control.change,
|
||||
seek_hide_timer: RefCell::new(None),
|
||||
last_pointer_position: Cell::new(None),
|
||||
key_controller: RefCell::new(None),
|
||||
motion_controller: RefCell::new(None),
|
||||
state: RefCell::new(ViewerState {
|
||||
pages: Vec::new(),
|
||||
anchor: 0,
|
||||
|
|
@ -263,6 +291,7 @@ struct ViewerController {
|
|||
spinner: gtk::Spinner,
|
||||
page_label: gtk::Label,
|
||||
page_seek: gtk::Scale,
|
||||
page_seek_row: gtk::Box,
|
||||
status_label: gtk::Label,
|
||||
previous_button: gtk::Button,
|
||||
next_button: gtk::Button,
|
||||
|
|
@ -276,6 +305,11 @@ struct ViewerController {
|
|||
favorite_toggle: Rc<dyn Fn() -> bool>,
|
||||
download_button: gtk::Button,
|
||||
file_download: Rc<dyn Fn(gtk::Window, gtk::Label)>,
|
||||
view_mode_change: Rc<dyn Fn(u32)>,
|
||||
seek_hide_timer: RefCell<Option<glib::SourceId>>,
|
||||
last_pointer_position: Cell<Option<(f64, f64)>>,
|
||||
key_controller: RefCell<Option<gtk::EventControllerKey>>,
|
||||
motion_controller: RefCell<Option<gtk::EventControllerMotion>>,
|
||||
state: RefCell<ViewerState>,
|
||||
}
|
||||
|
||||
|
|
@ -294,8 +328,10 @@ impl ViewerController {
|
|||
self.shift_forward_button
|
||||
.connect_clicked(move |_| controller.shift_pairing(1));
|
||||
let controller = Rc::clone(self);
|
||||
self.mode_dropdown
|
||||
.connect_selected_notify(move |_| controller.apply_view_mode());
|
||||
self.mode_dropdown.connect_selected_notify(move |dropdown| {
|
||||
(controller.view_mode_change)(dropdown.selected());
|
||||
controller.apply_view_mode();
|
||||
});
|
||||
let controller = Rc::clone(self);
|
||||
self.direction_dropdown
|
||||
.connect_selected_notify(move |_| controller.render());
|
||||
|
|
@ -313,9 +349,8 @@ impl ViewerController {
|
|||
});
|
||||
let controller = Rc::clone(self);
|
||||
self.page_seek.connect_value_changed(move |scale| {
|
||||
if let Some(index) =
|
||||
page_seek_index(scale.value(), controller.state.borrow().pages.len())
|
||||
{
|
||||
let page_count = controller.state.borrow().pages.len();
|
||||
if let Some(index) = page_seek_index(scale.value(), page_count) {
|
||||
controller.seek_page(index);
|
||||
}
|
||||
});
|
||||
|
|
@ -345,9 +380,18 @@ impl ViewerController {
|
|||
});
|
||||
self.stack.add_controller(scroll_controller);
|
||||
|
||||
let motion_controller = gtk::EventControllerMotion::new();
|
||||
motion_controller.set_propagation_phase(gtk::PropagationPhase::Capture);
|
||||
let controller = Rc::clone(self);
|
||||
motion_controller.connect_motion(move |_, x, y| controller.pointer_moved(x, y));
|
||||
self.window.add_controller(motion_controller.clone());
|
||||
self.motion_controller.replace(Some(motion_controller));
|
||||
|
||||
let key_controller = gtk::EventControllerKey::new();
|
||||
let controller = Rc::clone(self);
|
||||
key_controller.connect_key_pressed(move |_, key, _, _| match key {
|
||||
key_controller.connect_key_pressed(move |_, key, _, _| {
|
||||
controller.hide_seek();
|
||||
match key {
|
||||
gdk::Key::Left | gdk::Key::Page_Up => {
|
||||
controller.navigate(false);
|
||||
glib::Propagation::Stop
|
||||
|
|
@ -376,18 +420,75 @@ impl ViewerController {
|
|||
glib::Propagation::Stop
|
||||
}
|
||||
_ => glib::Propagation::Proceed,
|
||||
}
|
||||
});
|
||||
self.window.add_controller(key_controller);
|
||||
self.window.add_controller(key_controller.clone());
|
||||
self.key_controller.replace(Some(key_controller));
|
||||
}
|
||||
|
||||
fn open_sibling(&self, forward: bool) {
|
||||
if let Some(navigate) = &self.sibling_navigator
|
||||
&& navigate.open_relative(forward)
|
||||
&& navigate.open_relative(forward, self.window.clone())
|
||||
{
|
||||
self.window.close();
|
||||
self.detach_window_controllers();
|
||||
}
|
||||
}
|
||||
|
||||
fn detach_window_controllers(&self) {
|
||||
if let Some(controller) = self.key_controller.borrow_mut().take() {
|
||||
self.window.remove_controller(&controller);
|
||||
}
|
||||
if let Some(controller) = self.motion_controller.borrow_mut().take() {
|
||||
self.window.remove_controller(&controller);
|
||||
}
|
||||
}
|
||||
|
||||
fn show_seek_temporarily(self: &Rc<Self>) {
|
||||
if let Some(timer) = self.seek_hide_timer.borrow_mut().take() {
|
||||
timer.remove();
|
||||
}
|
||||
self.set_seek_visible(true);
|
||||
let controller = Rc::downgrade(self);
|
||||
let timer = glib::timeout_add_local_once(Duration::from_secs(5), move || {
|
||||
if let Some(controller) = controller.upgrade() {
|
||||
controller.seek_hide_timer.borrow_mut().take();
|
||||
controller.set_seek_visible(false);
|
||||
}
|
||||
});
|
||||
self.seek_hide_timer.replace(Some(timer));
|
||||
}
|
||||
|
||||
fn pointer_moved(self: &Rc<Self>, x: f64, y: f64) {
|
||||
if self.last_pointer_position.get() == Some((x, y)) {
|
||||
return;
|
||||
}
|
||||
self.last_pointer_position.set(Some((x, y)));
|
||||
self.show_seek_temporarily();
|
||||
}
|
||||
|
||||
fn hide_seek(self: &Rc<Self>) {
|
||||
if let Some(timer) = self.seek_hide_timer.borrow_mut().take() {
|
||||
timer.remove();
|
||||
}
|
||||
self.set_seek_visible(false);
|
||||
}
|
||||
|
||||
fn set_seek_visible(self: &Rc<Self>, visible: bool) {
|
||||
if self.page_seek_row.is_visible() == visible {
|
||||
return;
|
||||
}
|
||||
self.page_seek_row.set_visible(visible);
|
||||
self.window.queue_allocate();
|
||||
let controller = Rc::downgrade(self);
|
||||
glib::idle_add_local_once(move || {
|
||||
if let Some(controller) = controller.upgrade() {
|
||||
controller.stack.queue_allocate();
|
||||
controller.resize_continuous_rows();
|
||||
controller.render();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn toggle_fullscreen(&self) {
|
||||
if self.window.is_fullscreen() {
|
||||
self.window.unfullscreen();
|
||||
|
|
@ -438,7 +539,8 @@ impl ViewerController {
|
|||
state.pages = pages;
|
||||
state.anchor = saved;
|
||||
drop(state);
|
||||
self.page_seek.set_range(0.0, (page_count - 1) as f64);
|
||||
self.page_seek
|
||||
.set_range(0.0, navigation_scale_upper(page_count));
|
||||
self.page_seek.set_sensitive(page_count > 1);
|
||||
self.build_continuous_rows();
|
||||
self.render();
|
||||
|
|
@ -986,17 +1088,24 @@ impl MediaViewerWindow {
|
|||
application: >k::Application,
|
||||
source: ArchiveSource,
|
||||
kind: EntryKind,
|
||||
sibling_navigator: Option<SiblingNavigator>,
|
||||
favorite_control: FavoriteControl,
|
||||
download_control: FileDownloadControl,
|
||||
controls: FileViewerControls,
|
||||
) {
|
||||
let FileViewerControls {
|
||||
sibling_navigator,
|
||||
favorite_control,
|
||||
download_control,
|
||||
reuse_window,
|
||||
} = controls;
|
||||
let title = source.display_name.clone();
|
||||
let window = gtk::ApplicationWindow::builder()
|
||||
let window = reuse_window.unwrap_or_else(|| {
|
||||
gtk::ApplicationWindow::builder()
|
||||
.application(application)
|
||||
.title(&title)
|
||||
.default_width(1100)
|
||||
.default_height(850)
|
||||
.build();
|
||||
.build()
|
||||
});
|
||||
window.set_title(Some(&title));
|
||||
let header = gtk::HeaderBar::new();
|
||||
header.set_title_widget(Some(>k::Label::new(Some(&title))));
|
||||
let favorite_button = gtk::Button::with_label(favorite_label(favorite_control.active));
|
||||
|
|
@ -1028,19 +1137,17 @@ impl MediaViewerWindow {
|
|||
.margin_start(8)
|
||||
.margin_end(8)
|
||||
.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);
|
||||
let media_seek = navigation_scale();
|
||||
let media_seek_row = navigation_scale_row(&media_seek);
|
||||
if let Some(navigator) = &sibling_navigator {
|
||||
media_seek.set_range(0.0, navigator.count.saturating_sub(1) as f64);
|
||||
media_seek.set_range(0.0, navigation_scale_upper(navigator.count));
|
||||
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(&media_seek);
|
||||
root.append(&media_seek_row);
|
||||
root.append(&status_label);
|
||||
window.set_child(Some(&root));
|
||||
|
||||
|
|
@ -1052,6 +1159,7 @@ impl MediaViewerWindow {
|
|||
stack,
|
||||
status_label,
|
||||
media_seek,
|
||||
media_seek_row,
|
||||
sibling_navigator,
|
||||
favorite_button,
|
||||
favorite_toggle: favorite_control.toggle,
|
||||
|
|
@ -1059,6 +1167,10 @@ impl MediaViewerWindow {
|
|||
file_download: download_control.download,
|
||||
seek_generation: Cell::new(0),
|
||||
seek_opening: Cell::new(false),
|
||||
seek_hide_timer: RefCell::new(None),
|
||||
last_pointer_position: Cell::new(None),
|
||||
key_controller: RefCell::new(None),
|
||||
motion_controller: RefCell::new(None),
|
||||
});
|
||||
if let Some(stream) = controller.video.media_stream() {
|
||||
let status_label = controller.status_label.clone();
|
||||
|
|
@ -1091,6 +1203,7 @@ struct MediaViewerController {
|
|||
stack: gtk::Stack,
|
||||
status_label: gtk::Label,
|
||||
media_seek: gtk::Scale,
|
||||
media_seek_row: gtk::Box,
|
||||
sibling_navigator: Option<SiblingNavigator>,
|
||||
favorite_button: gtk::Button,
|
||||
favorite_toggle: Rc<dyn Fn() -> bool>,
|
||||
|
|
@ -1098,13 +1211,26 @@ struct MediaViewerController {
|
|||
file_download: Rc<dyn Fn(gtk::Window, gtk::Label)>,
|
||||
seek_generation: Cell<u64>,
|
||||
seek_opening: Cell<bool>,
|
||||
seek_hide_timer: RefCell<Option<glib::SourceId>>,
|
||||
last_pointer_position: Cell<Option<(f64, f64)>>,
|
||||
key_controller: RefCell<Option<gtk::EventControllerKey>>,
|
||||
motion_controller: RefCell<Option<gtk::EventControllerMotion>>,
|
||||
}
|
||||
|
||||
impl MediaViewerController {
|
||||
fn connect_signals(self: &Rc<Self>) {
|
||||
let motion_controller = gtk::EventControllerMotion::new();
|
||||
motion_controller.set_propagation_phase(gtk::PropagationPhase::Capture);
|
||||
let controller = Rc::clone(self);
|
||||
motion_controller.connect_motion(move |_, x, y| controller.pointer_moved(x, y));
|
||||
self.window.add_controller(motion_controller.clone());
|
||||
self.motion_controller.replace(Some(motion_controller));
|
||||
|
||||
let keys = gtk::EventControllerKey::new();
|
||||
let controller = Rc::clone(self);
|
||||
keys.connect_key_pressed(move |_, key, _, _| match key.to_unicode() {
|
||||
keys.connect_key_pressed(move |_, key, _, _| {
|
||||
controller.hide_seek();
|
||||
match key.to_unicode() {
|
||||
Some('[') => {
|
||||
controller.open_sibling(false);
|
||||
glib::Propagation::Stop
|
||||
|
|
@ -1122,8 +1248,10 @@ impl MediaViewerController {
|
|||
glib::Propagation::Stop
|
||||
}
|
||||
_ => glib::Propagation::Proceed,
|
||||
}
|
||||
});
|
||||
self.window.add_controller(keys);
|
||||
self.window.add_controller(keys.clone());
|
||||
self.key_controller.replace(Some(keys));
|
||||
let controller = Rc::clone(self);
|
||||
self.favorite_button.connect_clicked(move |_| {
|
||||
let active = (controller.favorite_toggle)();
|
||||
|
|
@ -1150,12 +1278,58 @@ impl MediaViewerController {
|
|||
|
||||
fn open_sibling(&self, forward: bool) {
|
||||
if let Some(navigate) = &self.sibling_navigator
|
||||
&& navigate.open_relative(forward)
|
||||
&& navigate.open_relative(forward, self.window.clone())
|
||||
{
|
||||
self.close_for_navigation();
|
||||
self.prepare_for_navigation();
|
||||
}
|
||||
}
|
||||
|
||||
fn show_seek_temporarily(self: &Rc<Self>) {
|
||||
if let Some(timer) = self.seek_hide_timer.borrow_mut().take() {
|
||||
timer.remove();
|
||||
}
|
||||
self.set_seek_visible(true);
|
||||
let controller = Rc::downgrade(self);
|
||||
let timer = glib::timeout_add_local_once(Duration::from_secs(5), move || {
|
||||
if let Some(controller) = controller.upgrade() {
|
||||
controller.seek_hide_timer.borrow_mut().take();
|
||||
controller.set_seek_visible(false);
|
||||
}
|
||||
});
|
||||
self.seek_hide_timer.replace(Some(timer));
|
||||
}
|
||||
|
||||
fn pointer_moved(self: &Rc<Self>, x: f64, y: f64) {
|
||||
if self.last_pointer_position.get() == Some((x, y)) {
|
||||
return;
|
||||
}
|
||||
self.last_pointer_position.set(Some((x, y)));
|
||||
self.show_seek_temporarily();
|
||||
}
|
||||
|
||||
fn hide_seek(self: &Rc<Self>) {
|
||||
if let Some(timer) = self.seek_hide_timer.borrow_mut().take() {
|
||||
timer.remove();
|
||||
}
|
||||
self.set_seek_visible(false);
|
||||
}
|
||||
|
||||
fn set_seek_visible(self: &Rc<Self>, visible: bool) {
|
||||
if self.media_seek_row.is_visible() == visible {
|
||||
return;
|
||||
}
|
||||
self.media_seek_row.set_visible(visible);
|
||||
self.window.queue_allocate();
|
||||
let controller = Rc::downgrade(self);
|
||||
glib::idle_add_local_once(move || {
|
||||
if let Some(controller) = controller.upgrade() {
|
||||
controller.stack.queue_allocate();
|
||||
controller.picture.queue_allocate();
|
||||
controller.video.queue_allocate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn schedule_seek(self: &Rc<Self>, index: usize) {
|
||||
let Some(navigator) = &self.sibling_navigator else {
|
||||
return;
|
||||
|
|
@ -1176,18 +1350,23 @@ impl MediaViewerController {
|
|||
let Some(navigator) = &controller.sibling_navigator else {
|
||||
return;
|
||||
};
|
||||
if navigator.open_index(index) {
|
||||
if navigator.open_index(index, controller.window.clone()) {
|
||||
controller.seek_opening.set(true);
|
||||
controller.close_for_navigation();
|
||||
controller.prepare_for_navigation();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn close_for_navigation(&self) {
|
||||
fn prepare_for_navigation(&self) {
|
||||
if let Some(stream) = self.video.media_stream() {
|
||||
stream.pause();
|
||||
}
|
||||
self.window.close();
|
||||
if let Some(controller) = self.key_controller.borrow_mut().take() {
|
||||
self.window.remove_controller(&controller);
|
||||
}
|
||||
if let Some(controller) = self.motion_controller.borrow_mut().take() {
|
||||
self.window.remove_controller(&controller);
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_fullscreen(&self) {
|
||||
|
|
@ -1259,6 +1438,33 @@ fn page_picture() -> gtk::Picture {
|
|||
picture
|
||||
}
|
||||
|
||||
fn navigation_scale() -> gtk::Scale {
|
||||
let scale = gtk::Scale::with_range(gtk::Orientation::Horizontal, 0.0, 1.0, 1.0);
|
||||
scale.set_draw_value(false);
|
||||
scale.set_hexpand(true);
|
||||
scale.set_height_request(32);
|
||||
scale.set_valign(gtk::Align::Center);
|
||||
scale.set_focusable(false);
|
||||
scale.set_margin_top(2);
|
||||
scale.set_margin_bottom(8);
|
||||
scale.set_margin_start(8);
|
||||
scale.set_margin_end(8);
|
||||
scale
|
||||
}
|
||||
|
||||
fn navigation_scale_row(scale: >k::Scale) -> gtk::Box {
|
||||
let row = gtk::Box::new(gtk::Orientation::Horizontal, 0);
|
||||
row.set_height_request(42);
|
||||
row.set_hexpand(true);
|
||||
row.append(scale);
|
||||
row.set_visible(false);
|
||||
row
|
||||
}
|
||||
|
||||
fn navigation_scale_upper(count: usize) -> f64 {
|
||||
count.saturating_sub(1).max(1) as f64
|
||||
}
|
||||
|
||||
fn spread_indices(anchor: usize, count: usize) -> Vec<usize> {
|
||||
if count == 0 {
|
||||
return Vec::new();
|
||||
|
|
@ -1370,6 +1576,14 @@ mod tests {
|
|||
assert_eq!(page_seek_index(0.0, 0), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn navigation_scale_always_has_a_valid_range() {
|
||||
assert_eq!(navigation_scale_upper(0), 1.0);
|
||||
assert_eq!(navigation_scale_upper(1), 1.0);
|
||||
assert_eq!(navigation_scale_upper(2), 1.0);
|
||||
assert_eq!(navigation_scale_upper(10), 9.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sibling_navigation_stops_at_boundaries() {
|
||||
assert_eq!(sibling_index(1, 3, false), Some(0));
|
||||
|
|
|
|||
283
src/window.rs
283
src/window.rs
|
|
@ -14,14 +14,15 @@ use gtk::prelude::*;
|
|||
use crate::browser::{
|
||||
BrowserEntry, EntryKind, SortDirection, SortField, classify_file, sort_entries,
|
||||
};
|
||||
use crate::config::{AppConfig, FavoriteEntry, LocationKind, LocationProfile};
|
||||
use crate::config::{AppConfig, FavoriteEntry, LocationKind, LocationProfile, ViewerMode};
|
||||
use crate::database::Database;
|
||||
use crate::source::{
|
||||
display_remote_uri, ensure_mounted, enumerate_directory, file_from_user_input, is_within_root,
|
||||
mount_operation, parent_within_root, root_file, validate_profile,
|
||||
};
|
||||
use crate::viewer::{
|
||||
FavoriteControl, FileDownloadControl, MediaViewerWindow, SiblingNavigator, ViewerWindow,
|
||||
FavoriteControl, FileDownloadControl, FileViewerControls, MediaViewerWindow, SiblingNavigator,
|
||||
ViewModeControl, ViewerWindow,
|
||||
};
|
||||
use crate::zip_cache::ZipCache;
|
||||
|
||||
|
|
@ -70,12 +71,6 @@ impl ComicWindow {
|
|||
locations.append(&edit_location_button);
|
||||
locations.append(&delete_location_button);
|
||||
|
||||
let favorites = gtk::Box::new(gtk::Orientation::Horizontal, 6);
|
||||
let favorite_dropdown = gtk::DropDown::from_strings(&["즐겨찾기 선택"]);
|
||||
favorite_dropdown.set_hexpand(true);
|
||||
favorites.append(>k::Label::new(Some("즐겨찾기")));
|
||||
favorites.append(&favorite_dropdown);
|
||||
|
||||
let navigation = gtk::Box::new(gtk::Orientation::Horizontal, 6);
|
||||
let up_button = gtk::Button::builder()
|
||||
.icon_name("go-up-symbolic")
|
||||
|
|
@ -119,7 +114,6 @@ impl ComicWindow {
|
|||
let status_label = gtk::Label::builder().xalign(0.0).build();
|
||||
|
||||
root.append(&locations);
|
||||
root.append(&favorites);
|
||||
root.append(&navigation);
|
||||
root.append(&controls);
|
||||
root.append(&scrolled);
|
||||
|
|
@ -129,7 +123,6 @@ impl ComicWindow {
|
|||
let controller = Rc::new(BrowserController {
|
||||
window,
|
||||
location_dropdown,
|
||||
favorite_dropdown,
|
||||
edit_location_button,
|
||||
delete_location_button,
|
||||
path_entry,
|
||||
|
|
@ -141,6 +134,7 @@ impl ComicWindow {
|
|||
state: RefCell::new(BrowserState {
|
||||
profile_index: 0,
|
||||
favorite_profile: None,
|
||||
browsing_favorites: false,
|
||||
root_uri: root_uri.clone(),
|
||||
current_uri: root_uri,
|
||||
entries: Vec::new(),
|
||||
|
|
@ -158,7 +152,6 @@ impl ComicWindow {
|
|||
});
|
||||
|
||||
controller.rebuild_profiles();
|
||||
controller.rebuild_favorites();
|
||||
controller.connect_signals(
|
||||
open_button,
|
||||
refresh_button,
|
||||
|
|
@ -178,6 +171,7 @@ impl ComicWindow {
|
|||
struct BrowserState {
|
||||
profile_index: usize,
|
||||
favorite_profile: Option<LocationProfile>,
|
||||
browsing_favorites: bool,
|
||||
root_uri: String,
|
||||
current_uri: String,
|
||||
entries: Vec<BrowserEntry>,
|
||||
|
|
@ -192,7 +186,6 @@ struct BrowserState {
|
|||
struct BrowserController {
|
||||
window: gtk::ApplicationWindow,
|
||||
location_dropdown: gtk::DropDown,
|
||||
favorite_dropdown: gtk::DropDown,
|
||||
edit_location_button: gtk::Button,
|
||||
delete_location_button: gtk::Button,
|
||||
path_entry: gtk::Entry,
|
||||
|
|
@ -244,9 +237,6 @@ impl BrowserController {
|
|||
self.location_dropdown
|
||||
.connect_selected_notify(move |_| controller.select_profile());
|
||||
let controller = Rc::clone(self);
|
||||
self.favorite_dropdown
|
||||
.connect_selected_notify(move |_| controller.select_favorite());
|
||||
let controller = Rc::clone(self);
|
||||
self.search_entry
|
||||
.connect_search_changed(move |_| controller.render());
|
||||
let controller = Rc::clone(self);
|
||||
|
|
@ -261,8 +251,8 @@ impl BrowserController {
|
|||
|
||||
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);
|
||||
context_click.connect_pressed(move |_, _, x, y| {
|
||||
controller.show_context_menu(x, y);
|
||||
});
|
||||
self.list_box.add_controller(context_click);
|
||||
|
||||
|
|
@ -296,38 +286,26 @@ impl BrowserController {
|
|||
|
||||
fn rebuild_profiles(&self) {
|
||||
let profiles = self.profiles();
|
||||
let names: Vec<_> = profiles
|
||||
let mut names: Vec<_> = profiles
|
||||
.iter()
|
||||
.map(|profile| profile.name.as_str())
|
||||
.map(|profile| profile.name.clone())
|
||||
.collect();
|
||||
names.push("즐겨찾기".into());
|
||||
let names: Vec<_> = names.iter().map(String::as_str).collect();
|
||||
self.location_dropdown
|
||||
.set_model(Some(>k::StringList::new(&names)));
|
||||
}
|
||||
|
||||
fn rebuild_favorites(&self) {
|
||||
let config = self.config.borrow();
|
||||
let mut names = Vec::with_capacity(config.favorites.len() + 1);
|
||||
names.push("즐겨찾기 선택".to_owned());
|
||||
names.extend(config.favorites.iter().map(|favorite| {
|
||||
let marker = if favorite.is_directory {
|
||||
"폴더"
|
||||
} else {
|
||||
"파일"
|
||||
};
|
||||
format!("[{marker}] {}", favorite.name)
|
||||
}));
|
||||
let names: Vec<_> = names.iter().map(String::as_str).collect();
|
||||
self.favorite_dropdown
|
||||
.set_model(Some(>k::StringList::new(&names)));
|
||||
self.favorite_dropdown.set_selected(0);
|
||||
}
|
||||
|
||||
fn select_profile(self: &Rc<Self>) {
|
||||
let profile_index = self.location_dropdown.selected() as usize;
|
||||
let editable = profile_index > 0 && profile_index <= self.config.borrow().locations.len();
|
||||
let profiles = self.profiles();
|
||||
if profile_index == profiles.len() {
|
||||
self.show_favorites();
|
||||
return;
|
||||
}
|
||||
let editable = profile_index > 0 && profile_index < profiles.len();
|
||||
self.edit_location_button.set_sensitive(editable);
|
||||
self.delete_location_button.set_sensitive(editable);
|
||||
let profiles = self.profiles();
|
||||
let Some(profile) = profiles.get(profile_index) else {
|
||||
return;
|
||||
};
|
||||
|
|
@ -337,9 +315,11 @@ impl BrowserController {
|
|||
let mut state = self.state.borrow_mut();
|
||||
state.profile_index = profile_index;
|
||||
state.favorite_profile = None;
|
||||
state.browsing_favorites = false;
|
||||
state.root_uri = root_uri.clone();
|
||||
state.current_uri = root_uri;
|
||||
drop(state);
|
||||
self.path_entry.set_editable(true);
|
||||
self.search_entry.set_text("");
|
||||
self.refresh();
|
||||
}
|
||||
|
|
@ -347,15 +327,28 @@ impl BrowserController {
|
|||
}
|
||||
}
|
||||
|
||||
fn select_favorite(self: &Rc<Self>) {
|
||||
let Some(index) = (self.favorite_dropdown.selected() as usize).checked_sub(1) else {
|
||||
return;
|
||||
};
|
||||
let Some(favorite) = self.config.borrow().favorites.get(index).cloned() else {
|
||||
self.favorite_dropdown.set_selected(0);
|
||||
return;
|
||||
};
|
||||
self.favorite_dropdown.set_selected(0);
|
||||
fn show_favorites(&self) {
|
||||
self.edit_location_button.set_sensitive(false);
|
||||
self.delete_location_button.set_sensitive(false);
|
||||
self.path_entry.set_text("즐겨찾기");
|
||||
self.path_entry.set_editable(false);
|
||||
self.search_entry.set_text("");
|
||||
let entries = self
|
||||
.config
|
||||
.borrow()
|
||||
.favorites
|
||||
.iter()
|
||||
.map(favorite_browser_entry)
|
||||
.collect();
|
||||
let mut state = self.state.borrow_mut();
|
||||
state.browsing_favorites = true;
|
||||
state.favorite_profile = None;
|
||||
state.entries = entries;
|
||||
drop(state);
|
||||
self.render();
|
||||
}
|
||||
|
||||
fn open_favorite(self: &Rc<Self>, favorite: FavoriteEntry) {
|
||||
let Ok(root) = root_file(&favorite.profile) else {
|
||||
self.status_label
|
||||
.set_text("즐겨찾기의 저장된 위치가 올바르지 않습니다.");
|
||||
|
|
@ -374,6 +367,7 @@ impl BrowserController {
|
|||
self.delete_location_button.set_sensitive(false);
|
||||
let mut state = self.state.borrow_mut();
|
||||
state.favorite_profile = Some(favorite.profile.clone());
|
||||
state.browsing_favorites = false;
|
||||
state.root_uri = root.uri().into();
|
||||
state.current_uri = favorite.uri;
|
||||
drop(state);
|
||||
|
|
@ -412,11 +406,15 @@ impl BrowserController {
|
|||
etag: None,
|
||||
};
|
||||
let siblings = Rc::new(vec![entry.clone()]);
|
||||
controller.open_viewable(entry, siblings, 0, favorite.profile);
|
||||
controller.open_viewable(entry, siblings, 0, favorite.profile, None);
|
||||
});
|
||||
}
|
||||
|
||||
fn open_entered_path(self: &Rc<Self>) {
|
||||
let browsing_favorites = self.state.borrow().browsing_favorites;
|
||||
if browsing_favorites {
|
||||
return;
|
||||
}
|
||||
let profile = self.current_profile();
|
||||
let entered = self.path_entry.text();
|
||||
let file = file_from_user_input(profile.kind, entered.as_str());
|
||||
|
|
@ -431,6 +429,10 @@ impl BrowserController {
|
|||
}
|
||||
|
||||
fn go_up(self: &Rc<Self>) {
|
||||
let browsing_favorites = self.state.borrow().browsing_favorites;
|
||||
if browsing_favorites {
|
||||
return;
|
||||
}
|
||||
let state = self.state.borrow();
|
||||
let current = gio::File::for_uri(&state.current_uri);
|
||||
let root = gio::File::for_uri(&state.root_uri);
|
||||
|
|
@ -442,6 +444,11 @@ impl BrowserController {
|
|||
}
|
||||
|
||||
fn refresh(self: &Rc<Self>) {
|
||||
let browsing_favorites = self.state.borrow().browsing_favorites;
|
||||
if browsing_favorites {
|
||||
self.show_favorites();
|
||||
return;
|
||||
}
|
||||
let (profile, uri, generation) = {
|
||||
let mut state = self.state.borrow_mut();
|
||||
state.generation = state.generation.wrapping_add(1);
|
||||
|
|
@ -602,6 +609,20 @@ impl BrowserController {
|
|||
else {
|
||||
return;
|
||||
};
|
||||
let browsing_favorites = self.state.borrow().browsing_favorites;
|
||||
if browsing_favorites {
|
||||
let favorite = self
|
||||
.config
|
||||
.borrow()
|
||||
.favorites
|
||||
.iter()
|
||||
.find(|favorite| favorite.uri == entry.uri)
|
||||
.cloned();
|
||||
if let Some(favorite) = favorite {
|
||||
self.open_favorite(favorite);
|
||||
}
|
||||
return;
|
||||
}
|
||||
match entry.kind {
|
||||
EntryKind::Directory => {
|
||||
self.state.borrow_mut().current_uri = entry.uri;
|
||||
|
|
@ -623,7 +644,7 @@ impl BrowserController {
|
|||
};
|
||||
let siblings = Rc::new(siblings);
|
||||
if let Some(current) = siblings.iter().position(|item| item.uri == entry.uri) {
|
||||
self.open_viewable(entry, siblings, current, self.current_profile());
|
||||
self.open_viewable(entry, siblings, current, self.current_profile(), None);
|
||||
}
|
||||
}
|
||||
EntryKind::OtherFile => {}
|
||||
|
|
@ -636,9 +657,12 @@ impl BrowserController {
|
|||
siblings: Rc<Vec<BrowserEntry>>,
|
||||
current: usize,
|
||||
profile: LocationProfile,
|
||||
reuse_window: Option<gtk::ApplicationWindow>,
|
||||
) {
|
||||
match entry.kind {
|
||||
EntryKind::ZipArchive => self.open_archive(entry, siblings, current, profile),
|
||||
EntryKind::ZipArchive => {
|
||||
self.open_archive(entry, siblings, current, profile, reuse_window)
|
||||
}
|
||||
EntryKind::Image | EntryKind::Video => {
|
||||
let media_siblings = Rc::new(
|
||||
siblings
|
||||
|
|
@ -651,7 +675,7 @@ impl BrowserController {
|
|||
.iter()
|
||||
.position(|sibling| sibling.uri == entry.uri)
|
||||
{
|
||||
self.open_media(entry, media_siblings, current, profile);
|
||||
self.open_media(entry, media_siblings, current, profile, reuse_window);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
|
|
@ -666,7 +690,7 @@ impl BrowserController {
|
|||
) -> SiblingNavigator {
|
||||
let controller = Rc::downgrade(self);
|
||||
let count = siblings.len();
|
||||
let open = Rc::new(move |next: usize| {
|
||||
let open = Rc::new(move |next: usize, reuse_window: gtk::ApplicationWindow| {
|
||||
let Some(controller) = controller.upgrade() else {
|
||||
return false;
|
||||
};
|
||||
|
|
@ -681,6 +705,7 @@ impl BrowserController {
|
|||
Rc::clone(&siblings),
|
||||
next,
|
||||
profile.clone(),
|
||||
Some(reuse_window),
|
||||
);
|
||||
true
|
||||
});
|
||||
|
|
@ -693,13 +718,19 @@ impl BrowserController {
|
|||
siblings: Rc<Vec<BrowserEntry>>,
|
||||
current: usize,
|
||||
profile: LocationProfile,
|
||||
reuse_window: Option<gtk::ApplicationWindow>,
|
||||
) {
|
||||
self.status_label.set_text("ZIP 파일 다운로드 준비 중...");
|
||||
let progress_label = self.status_label.clone();
|
||||
let progress_label = reuse_window.as_ref().map_or_else(
|
||||
|| self.status_label.clone(),
|
||||
|window| show_viewer_loading(window, &entry.name, "ZIP 파일을 준비하는 중..."),
|
||||
);
|
||||
let result_label = progress_label.clone();
|
||||
let controller = Rc::clone(self);
|
||||
let navigator = self.sibling_navigator(siblings, current, profile.clone());
|
||||
let favorite_control = self.favorite_control(&entry, &profile);
|
||||
let download_control = self.download_control(&entry);
|
||||
let view_mode_control = self.view_mode_control();
|
||||
self.zip_cache.prepare(
|
||||
entry,
|
||||
move |current, total| {
|
||||
|
|
@ -709,6 +740,7 @@ impl BrowserController {
|
|||
move |result| match result {
|
||||
Ok(source) => {
|
||||
controller.status_label.set_text("다운로드 완료");
|
||||
result_label.set_text("ZIP 파일을 여는 중...");
|
||||
let Some(application) = controller.window.application() else {
|
||||
return;
|
||||
};
|
||||
|
|
@ -717,12 +749,20 @@ impl BrowserController {
|
|||
source,
|
||||
Rc::clone(&controller.database),
|
||||
controller.config.borrow().fast_image_loading,
|
||||
Some(navigator),
|
||||
view_mode_control,
|
||||
FileViewerControls {
|
||||
sibling_navigator: Some(navigator),
|
||||
favorite_control,
|
||||
download_control,
|
||||
reuse_window,
|
||||
},
|
||||
);
|
||||
}
|
||||
Err(error) => controller.status_label.set_text(&format!("{error:#}")),
|
||||
Err(error) => {
|
||||
let message = format!("{error:#}");
|
||||
controller.status_label.set_text(&message);
|
||||
result_label.set_text(&message);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
@ -733,11 +773,16 @@ impl BrowserController {
|
|||
siblings: Rc<Vec<BrowserEntry>>,
|
||||
current: usize,
|
||||
profile: LocationProfile,
|
||||
reuse_window: Option<gtk::ApplicationWindow>,
|
||||
) {
|
||||
let kind = entry.kind;
|
||||
self.status_label
|
||||
.set_text("미디어 파일 다운로드 준비 중...");
|
||||
let progress_label = self.status_label.clone();
|
||||
let progress_label = reuse_window.as_ref().map_or_else(
|
||||
|| self.status_label.clone(),
|
||||
|window| show_viewer_loading(window, &entry.name, "미디어 파일을 준비하는 중..."),
|
||||
);
|
||||
let result_label = progress_label.clone();
|
||||
let controller = Rc::clone(self);
|
||||
let navigator = self.sibling_navigator(siblings, current, profile.clone());
|
||||
let favorite_control = self.favorite_control(&entry, &profile);
|
||||
|
|
@ -751,6 +796,7 @@ impl BrowserController {
|
|||
move |result| match result {
|
||||
Ok(source) => {
|
||||
controller.status_label.set_text("다운로드 완료");
|
||||
result_label.set_text("미디어 파일을 여는 중...");
|
||||
let Some(application) = controller.window.application() else {
|
||||
return;
|
||||
};
|
||||
|
|
@ -758,18 +804,37 @@ impl BrowserController {
|
|||
&application,
|
||||
source,
|
||||
kind,
|
||||
Some(navigator),
|
||||
FileViewerControls {
|
||||
sibling_navigator: Some(navigator),
|
||||
favorite_control,
|
||||
download_control,
|
||||
reuse_window,
|
||||
},
|
||||
);
|
||||
}
|
||||
Err(error) => controller.status_label.set_text(&format!("{error:#}")),
|
||||
Err(error) => {
|
||||
let message = format!("{error:#}");
|
||||
controller.status_label.set_text(&message);
|
||||
result_label.set_text(&message);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn show_context_menu(self: &Rc<Self>, y: i32) {
|
||||
let Some(row) = self.list_box.row_at_y(y) else {
|
||||
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;
|
||||
};
|
||||
let Some(entry) = self
|
||||
|
|
@ -869,6 +934,32 @@ impl BrowserController {
|
|||
}
|
||||
}
|
||||
|
||||
fn view_mode_control(self: &Rc<Self>) -> ViewModeControl {
|
||||
let selected = self.config.borrow().viewer_mode.dropdown_index();
|
||||
let controller = Rc::downgrade(self);
|
||||
ViewModeControl {
|
||||
selected,
|
||||
change: Rc::new(move |index| {
|
||||
let Some(controller) = controller.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let viewer_mode = ViewerMode::from_dropdown_index(index);
|
||||
if controller.config.borrow().viewer_mode == viewer_mode {
|
||||
return;
|
||||
}
|
||||
let mut updated = controller.config.borrow().clone();
|
||||
updated.viewer_mode = viewer_mode;
|
||||
if let Err(error) = updated.save(&controller.config_path) {
|
||||
controller
|
||||
.status_label
|
||||
.set_text(&format!("보기 모드를 저장할 수 없습니다: {error:#}"));
|
||||
return;
|
||||
}
|
||||
*controller.config.borrow_mut() = updated;
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_favorite(&self, entry: &BrowserEntry, profile: &LocationProfile) -> bool {
|
||||
let mut updated = self.config.borrow().clone();
|
||||
let was_active = self.is_favorite(&entry.uri);
|
||||
|
|
@ -879,7 +970,10 @@ impl BrowserController {
|
|||
return was_active;
|
||||
}
|
||||
*self.config.borrow_mut() = updated;
|
||||
self.rebuild_favorites();
|
||||
let browsing_favorites = self.state.borrow().browsing_favorites;
|
||||
if browsing_favorites {
|
||||
self.show_favorites();
|
||||
}
|
||||
self.status_label.set_text(if active {
|
||||
"즐겨찾기에 추가했습니다."
|
||||
} else {
|
||||
|
|
@ -1245,6 +1339,51 @@ fn navigable_sibling(current: EntryKind, candidate: EntryKind) -> bool {
|
|||
}
|
||||
}
|
||||
|
||||
fn favorite_browser_entry(favorite: &FavoriteEntry) -> BrowserEntry {
|
||||
BrowserEntry {
|
||||
uri: favorite.uri.clone(),
|
||||
name: favorite.name.clone(),
|
||||
kind: if favorite.is_directory {
|
||||
EntryKind::Directory
|
||||
} else {
|
||||
classify_file(Path::new(&favorite.name))
|
||||
},
|
||||
size: None,
|
||||
modified: None,
|
||||
created: None,
|
||||
etag: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn show_viewer_loading(
|
||||
window: >k::ApplicationWindow,
|
||||
file_name: &str,
|
||||
status: &str,
|
||||
) -> gtk::Label {
|
||||
window.set_title(Some(file_name));
|
||||
let header = gtk::HeaderBar::new();
|
||||
header.set_title_widget(Some(>k::Label::new(Some(file_name))));
|
||||
window.set_titlebar(Some(&header));
|
||||
|
||||
let content = gtk::Box::new(gtk::Orientation::Vertical, 12);
|
||||
content.set_halign(gtk::Align::Center);
|
||||
content.set_valign(gtk::Align::Center);
|
||||
content.set_hexpand(true);
|
||||
content.set_vexpand(true);
|
||||
let spinner = gtk::Spinner::new();
|
||||
spinner.set_spinning(true);
|
||||
let name_label = gtk::Label::new(Some(file_name));
|
||||
name_label.add_css_class("title-2");
|
||||
name_label.set_ellipsize(gtk::pango::EllipsizeMode::Middle);
|
||||
name_label.set_max_width_chars(60);
|
||||
let status_label = gtk::Label::new(Some(status));
|
||||
content.append(&spinner);
|
||||
content.append(&name_label);
|
||||
content.append(&status_label);
|
||||
window.set_child(Some(&content));
|
||||
status_label
|
||||
}
|
||||
|
||||
fn download_part_path(final_path: &std::path::Path) -> PathBuf {
|
||||
final_path.with_file_name(format!(
|
||||
"{}.part",
|
||||
|
|
@ -1315,6 +1454,26 @@ mod tests {
|
|||
assert!(!navigable_sibling(EntryKind::Video, EntryKind::OtherFile));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn favorites_are_rendered_as_browser_paths_and_files() {
|
||||
let profile = home_profile();
|
||||
let folder = favorite_browser_entry(&FavoriteEntry {
|
||||
name: "Comics".into(),
|
||||
uri: "file:///tmp/Comics".into(),
|
||||
is_directory: true,
|
||||
profile: profile.clone(),
|
||||
});
|
||||
let archive = favorite_browser_entry(&FavoriteEntry {
|
||||
name: "book.zip".into(),
|
||||
uri: "file:///tmp/book.zip".into(),
|
||||
is_directory: false,
|
||||
profile,
|
||||
});
|
||||
|
||||
assert_eq!(folder.kind, EntryKind::Directory);
|
||||
assert_eq!(archive.kind, EntryKind::ZipArchive);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn incomplete_download_uses_distinct_suffix() {
|
||||
assert_eq!(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue