528 lines
17 KiB
Rust
528 lines
17 KiB
Rust
use std::path::{Path, PathBuf};
|
|
|
|
use freya::prelude::*;
|
|
use freya::router::RouterContext;
|
|
use notify::{Event, EventKind};
|
|
use oneclient_content::modpacks::BlockedFile;
|
|
|
|
use crate::components::{Button, Icon, IconType, OverlayPopup};
|
|
use crate::hooks::{use_dispatch, use_folder_watch, use_notifications_snapshot};
|
|
use crate::notifications::BlockedDownloads;
|
|
use crate::routes::Route;
|
|
use crate::theme::colors;
|
|
use crate::ui::border_all_color;
|
|
|
|
const CARD_BG: Color = Color::from_rgb(26, 34, 41);
|
|
const DIALOG_W: f32 = 560.;
|
|
const DIALOG_PAD: f32 = 22.;
|
|
const LIST_MAX_H: f32 = 300.;
|
|
const ROW_H: f32 = 52.;
|
|
|
|
#[derive(PartialEq)]
|
|
pub struct BlockedDownloadsPopup;
|
|
|
|
impl Component for BlockedDownloadsPopup {
|
|
fn render(&self) -> impl IntoElement {
|
|
let snapshot = use_notifications_snapshot();
|
|
let dispatch = use_dispatch();
|
|
let router = RouterContext::get();
|
|
|
|
use_side_effect_with_deps(&snapshot.open_cluster, move |requested| {
|
|
if requested.is_none() {
|
|
return;
|
|
}
|
|
if let Some(cluster_id) = dispatch.take_open_cluster() {
|
|
let _ = router.push(Route::ClusterOverview { cluster_id });
|
|
}
|
|
});
|
|
|
|
match snapshot.blocked_downloads {
|
|
Some(blocked) => BlockedDownloadsDialog { blocked }.into_element(),
|
|
None => rect().into_element(),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn downloads_dir() -> Option<PathBuf> {
|
|
directories::UserDirs::new().and_then(|dirs| dirs.download_dir().map(Path::to_path_buf))
|
|
}
|
|
|
|
fn touches_file(_root: &Path, event: &Event) -> bool {
|
|
matches!(event.kind, EventKind::Create(_) | EventKind::Modify(_))
|
|
}
|
|
|
|
type ExtraFolder = State<Option<(i64, PathBuf)>>;
|
|
|
|
fn bump(mut tick: State<u64>) {
|
|
let next = *tick.peek() + 1;
|
|
tick.set(next);
|
|
}
|
|
|
|
#[derive(PartialEq)]
|
|
struct ExtraFolderWatch {
|
|
folder: PathBuf,
|
|
tick: State<u64>,
|
|
key: DiffKey,
|
|
}
|
|
|
|
impl KeyExt for ExtraFolderWatch {
|
|
fn write_key(&mut self) -> &mut DiffKey {
|
|
&mut self.key
|
|
}
|
|
}
|
|
|
|
impl Component for ExtraFolderWatch {
|
|
fn render(&self) -> impl IntoElement {
|
|
let tick = self.tick;
|
|
use_folder_watch(Some(self.folder.clone()), false, touches_file, move || {
|
|
bump(tick)
|
|
});
|
|
rect()
|
|
}
|
|
|
|
fn render_key(&self) -> DiffKey {
|
|
self.key.clone().or(self.default_key())
|
|
}
|
|
}
|
|
|
|
#[derive(PartialEq)]
|
|
struct BlockedDownloadsDialog {
|
|
blocked: BlockedDownloads,
|
|
}
|
|
|
|
impl Component for BlockedDownloadsDialog {
|
|
fn render(&self) -> impl IntoElement {
|
|
let dispatch = use_dispatch();
|
|
let downloads = use_hook(downloads_dir);
|
|
let tick = use_state(|| 0u64);
|
|
let extra: ExtraFolder = use_state(|| None);
|
|
|
|
use_folder_watch(downloads.clone(), false, touches_file, move || bump(tick));
|
|
|
|
let cluster_id = self.blocked.cluster_id;
|
|
let chosen = extra
|
|
.read()
|
|
.as_ref()
|
|
.filter(|(owner, _)| *owner == cluster_id)
|
|
.map(|(_, folder)| folder.clone());
|
|
|
|
let deps = (
|
|
*tick.read(),
|
|
cluster_id,
|
|
self.blocked.remaining(),
|
|
chosen.clone(),
|
|
);
|
|
{
|
|
let dispatch = dispatch.clone();
|
|
let downloads = downloads.clone();
|
|
use_side_effect_with_deps(&deps, move |(_, cluster_id, files, chosen)| {
|
|
if files.is_empty() {
|
|
return;
|
|
}
|
|
let locations: Vec<PathBuf> =
|
|
downloads.iter().chain(chosen.iter()).cloned().collect();
|
|
if !locations.is_empty() {
|
|
dispatch.scan_blocked_downloads(*cluster_id, locations, files.clone());
|
|
}
|
|
});
|
|
}
|
|
|
|
let close = dispatch.clone();
|
|
OverlayPopup::new()
|
|
.on_close(move |_| close.close_blocked_downloads())
|
|
.child(
|
|
rect()
|
|
.width(Size::window_percent(100.))
|
|
.height(Size::window_percent(100.))
|
|
.center()
|
|
.child(dialog(
|
|
&self.blocked,
|
|
downloads.as_deref(),
|
|
chosen.as_deref(),
|
|
extra,
|
|
dispatch,
|
|
)),
|
|
)
|
|
.maybe_child(chosen.map(|folder| {
|
|
let key = folder.to_string_lossy().into_owned();
|
|
ExtraFolderWatch {
|
|
folder,
|
|
tick,
|
|
key: DiffKey::None,
|
|
}
|
|
.key(key)
|
|
.into_element()
|
|
}))
|
|
.into_element()
|
|
}
|
|
}
|
|
|
|
fn folder_label(path: &Path) -> String {
|
|
path.file_name()
|
|
.map(|name| name.to_string_lossy().into_owned())
|
|
.unwrap_or_else(|| path.display().to_string())
|
|
}
|
|
|
|
fn watched_text(downloads: Option<&Path>, chosen: Option<&Path>) -> String {
|
|
match (downloads, chosen) {
|
|
(Some(_), Some(chosen)) => format!("Downloads and {}", chosen.display()),
|
|
(Some(downloads), None) => downloads.display().to_string(),
|
|
(None, Some(chosen)) => chosen.display().to_string(),
|
|
(None, None) => "Nothing yet. Add a folder or use Browse.".to_string(),
|
|
}
|
|
}
|
|
|
|
fn pick_extra_folder(cluster_id: i64, start: Option<PathBuf>, mut extra: ExtraFolder) {
|
|
spawn(async move {
|
|
let mut picker = rfd::AsyncFileDialog::new().set_title("Pick the folder you download into");
|
|
if let Some(start) = start {
|
|
picker = picker.set_directory(start);
|
|
}
|
|
if let Some(handle) = picker.pick_folder().await {
|
|
extra.set(Some((cluster_id, handle.path().to_path_buf())));
|
|
}
|
|
});
|
|
}
|
|
|
|
fn folder_row(
|
|
cluster_id: i64,
|
|
downloads: Option<&Path>,
|
|
chosen: Option<&Path>,
|
|
extra: ExtraFolder,
|
|
) -> impl IntoElement {
|
|
let start = chosen
|
|
.map(Path::to_path_buf)
|
|
.or_else(|| downloads.map(Path::to_path_buf));
|
|
let has_extra = chosen.is_some();
|
|
|
|
rect()
|
|
.vertical()
|
|
.width(Size::fill())
|
|
.spacing(6.)
|
|
.child(
|
|
rect()
|
|
.horizontal()
|
|
.width(Size::fill())
|
|
.content(Content::Flex)
|
|
.cross_align(Alignment::Center)
|
|
.padding(Gaps::new_symmetric(8., 12.))
|
|
.spacing(10.)
|
|
.corner_radius(CornerRadius::new_all(10.))
|
|
.background(colors::component_bg())
|
|
.border(border_all_color(1., colors::component_border()))
|
|
.child(
|
|
Icon::new(IconType::FolderCheck)
|
|
.size(16.)
|
|
.color(colors::fg_secondary()),
|
|
)
|
|
.child(
|
|
rect()
|
|
.vertical()
|
|
.width(Size::flex(1.))
|
|
.spacing(2.)
|
|
.child(
|
|
label()
|
|
.text("Watching")
|
|
.font_size(11.)
|
|
.color(colors::fg_secondary()),
|
|
)
|
|
.child(
|
|
label()
|
|
.text(watched_text(downloads, chosen))
|
|
.font_size(12.5)
|
|
.max_lines(2)
|
|
.color(colors::fg_primary()),
|
|
),
|
|
)
|
|
.child(
|
|
Button::new()
|
|
.secondary()
|
|
.small()
|
|
.on_press(move |_| pick_extra_folder(cluster_id, start.clone(), extra))
|
|
.text(if has_extra {
|
|
"Change folder"
|
|
} else {
|
|
"Add folder"
|
|
}),
|
|
)
|
|
.maybe_child(has_extra.then(|| {
|
|
let mut extra = extra;
|
|
Button::new()
|
|
.ghost()
|
|
.small()
|
|
.on_press(move |_| extra.set(None))
|
|
.text("Remove")
|
|
.into_element()
|
|
})),
|
|
)
|
|
.child(
|
|
label()
|
|
.text(
|
|
"Your browser saves to its own download folder. To save somewhere else, \
|
|
set it to ask where to save each file.",
|
|
)
|
|
.font_size(11.)
|
|
.max_lines(2)
|
|
.color(colors::fg_secondary()),
|
|
)
|
|
}
|
|
|
|
fn dialog(
|
|
blocked: &BlockedDownloads,
|
|
downloads: Option<&Path>,
|
|
chosen: Option<&Path>,
|
|
extra: ExtraFolder,
|
|
dispatch: crate::Actions,
|
|
) -> impl IntoElement {
|
|
let count = blocked.files.len();
|
|
let added = blocked.added.len().min(count);
|
|
let done = added == count;
|
|
let plural = crate::utils::plural(count as i64);
|
|
let where_to = match (downloads, chosen) {
|
|
(_, Some(chosen)) => format!(
|
|
"Download each one from its page and it will be picked up from Downloads or {} automatically.",
|
|
folder_label(chosen)
|
|
),
|
|
(Some(_), None) => {
|
|
"Download each one from its page and it will be picked up from your Downloads folder automatically."
|
|
.to_string()
|
|
}
|
|
(None, None) => {
|
|
"Download each one from its page, then add the folder you saved them to or use Browse."
|
|
.to_string()
|
|
}
|
|
};
|
|
let subtitle = if done {
|
|
format!(
|
|
"All {count} file{plural} are in {}. It is ready to play.",
|
|
blocked.cluster_name
|
|
)
|
|
} else {
|
|
format!(
|
|
"CurseForge does not let launchers download {count} file{plural} that {} needs. {where_to}",
|
|
blocked.cluster_name
|
|
)
|
|
};
|
|
let title = if done {
|
|
"Manual downloads added".to_string()
|
|
} else if added > 0 {
|
|
format!("Some files need a manual download ({added} of {count} added)")
|
|
} else {
|
|
"Some files need a manual download".to_string()
|
|
};
|
|
|
|
let cluster_id = blocked.cluster_id;
|
|
let files = blocked.remaining();
|
|
let browse = dispatch.clone();
|
|
let close = dispatch.clone();
|
|
let browse_start = chosen
|
|
.map(Path::to_path_buf)
|
|
.or_else(|| downloads.map(Path::to_path_buf));
|
|
|
|
rect()
|
|
.vertical()
|
|
.width(Size::px(DIALOG_W))
|
|
.height(Size::Inner)
|
|
.max_width(Size::window_percent(95.))
|
|
.max_height(Size::window_percent(85.))
|
|
.overflow(Overflow::Clip)
|
|
.corner_radius(CornerRadius::new_all(16.))
|
|
.background(CARD_BG)
|
|
.border(border_all_color(1., colors::component_border()))
|
|
.shadow(Shadow::from((
|
|
0.,
|
|
18.,
|
|
52.,
|
|
0.,
|
|
Color::from_argb(150, 0, 0, 0),
|
|
)))
|
|
.child(
|
|
rect()
|
|
.vertical()
|
|
.width(Size::fill())
|
|
.height(Size::Inner)
|
|
.padding(Gaps::new_all(DIALOG_PAD))
|
|
.spacing(14.)
|
|
.child(
|
|
rect()
|
|
.vertical()
|
|
.width(Size::fill())
|
|
.spacing(3.)
|
|
.child(
|
|
label()
|
|
.text(title)
|
|
.font_size(17.)
|
|
.font_weight(FontWeight::SEMI_BOLD)
|
|
.color(colors::fg_primary()),
|
|
)
|
|
.child(
|
|
label()
|
|
.text(subtitle)
|
|
.font_size(12.5)
|
|
.max_lines(4)
|
|
.color(colors::fg_secondary()),
|
|
),
|
|
)
|
|
.maybe_child(
|
|
(!done)
|
|
.then(|| folder_row(cluster_id, downloads, chosen, extra).into_element()),
|
|
)
|
|
.child(file_list(blocked))
|
|
.child(if done {
|
|
let close = close.clone();
|
|
rect()
|
|
.horizontal()
|
|
.width(Size::fill())
|
|
.main_align(Alignment::End)
|
|
.child(
|
|
Button::new()
|
|
.primary()
|
|
.on_press(move |_| close.close_blocked_downloads())
|
|
.child(Icon::new(IconType::Check).size(15.))
|
|
.text("Done"),
|
|
)
|
|
.into_element()
|
|
} else {
|
|
footer(cluster_id, files, browse_start, browse, close).into_element()
|
|
}),
|
|
)
|
|
}
|
|
|
|
fn footer(
|
|
cluster_id: i64,
|
|
files: Vec<BlockedFile>,
|
|
browse_start: Option<PathBuf>,
|
|
browse: crate::Actions,
|
|
close: crate::Actions,
|
|
) -> impl IntoElement {
|
|
rect()
|
|
.horizontal()
|
|
.width(Size::fill())
|
|
.cross_align(Alignment::Center)
|
|
.main_align(Alignment::End)
|
|
.spacing(8.)
|
|
.child(
|
|
Button::new()
|
|
.ghost()
|
|
.on_press(move |_| close.close_blocked_downloads())
|
|
.text("Skip for now"),
|
|
)
|
|
.child(
|
|
Button::new()
|
|
.primary()
|
|
.on_press(move |_| {
|
|
let dispatch = browse.clone();
|
|
let files = files.clone();
|
|
let start = browse_start.clone();
|
|
spawn(async move {
|
|
let mut picker =
|
|
rfd::AsyncFileDialog::new().set_title("Pick the downloaded files");
|
|
if let Some(start) = start {
|
|
picker = picker.set_directory(start);
|
|
}
|
|
if let Some(handles) = picker.pick_files().await {
|
|
let picked = handles
|
|
.iter()
|
|
.map(|handle| handle.path().to_path_buf())
|
|
.collect();
|
|
dispatch.scan_blocked_downloads(cluster_id, picked, files);
|
|
}
|
|
});
|
|
})
|
|
.child(Icon::new(IconType::Folder).size(15.))
|
|
.text("Browse"),
|
|
)
|
|
}
|
|
|
|
fn file_list(blocked: &BlockedDownloads) -> impl IntoElement {
|
|
ScrollView::new()
|
|
.width(Size::fill())
|
|
.height(Size::Inner)
|
|
.max_height(Size::px(LIST_MAX_H))
|
|
.child(
|
|
rect().vertical().width(Size::fill()).spacing(6.).children(
|
|
blocked
|
|
.files
|
|
.iter()
|
|
.map(|file| file_row(file, blocked.is_added(file)).into_element()),
|
|
),
|
|
)
|
|
}
|
|
|
|
fn file_row(file: &BlockedFile, added: bool) -> impl IntoElement {
|
|
let page = file.page_url.clone();
|
|
let (background, border) = if added {
|
|
(colors::success().with_a(28), colors::success().with_a(120))
|
|
} else {
|
|
(colors::component_bg(), colors::component_border())
|
|
};
|
|
|
|
let trailing = if added {
|
|
rect()
|
|
.horizontal()
|
|
.cross_align(Alignment::Center)
|
|
.spacing(6.)
|
|
.padding(Gaps::new_symmetric(0., 4.))
|
|
.child(
|
|
Icon::new(IconType::CheckCircle)
|
|
.size(14.)
|
|
.color(colors::success()),
|
|
)
|
|
.child(
|
|
label()
|
|
.text("Added")
|
|
.font_size(12.)
|
|
.font_weight(FontWeight::SEMI_BOLD)
|
|
.color(colors::success()),
|
|
)
|
|
.into_element()
|
|
} else {
|
|
Button::new()
|
|
.secondary()
|
|
.small()
|
|
.enabled(page.is_some())
|
|
.on_press(move |_| {
|
|
if let Some(url) = &page {
|
|
crate::platform::open_url(url);
|
|
}
|
|
})
|
|
.child(Icon::new(IconType::LinkExternal01).size(13.))
|
|
.text("Open page")
|
|
.into_element()
|
|
};
|
|
|
|
rect()
|
|
.key(file.sha1.clone())
|
|
.horizontal()
|
|
.width(Size::fill())
|
|
.height(Size::px(ROW_H))
|
|
.content(Content::Flex)
|
|
.cross_align(Alignment::Center)
|
|
.padding(Gaps::new_symmetric(0., 12.))
|
|
.spacing(10.)
|
|
.corner_radius(CornerRadius::new_all(10.))
|
|
.background(background)
|
|
.border(border_all_color(1., border))
|
|
.child(
|
|
rect()
|
|
.vertical()
|
|
.width(Size::flex(1.))
|
|
.spacing(2.)
|
|
.child(
|
|
label()
|
|
.text(file.project_name.clone())
|
|
.font_size(13.)
|
|
.font_weight(FontWeight::SEMI_BOLD)
|
|
.max_lines(1)
|
|
.color(colors::fg_primary()),
|
|
)
|
|
.child(
|
|
label()
|
|
.text(file.file_name.clone())
|
|
.font_size(11.)
|
|
.max_lines(1)
|
|
.color(colors::fg_secondary()),
|
|
),
|
|
)
|
|
.child(trailing)
|
|
}
|