use crate::route::{Route, releases};
use std::path::{Path, PathBuf};
use crate::act::Step;
use crate::api::browse::{Asset, Release};
use crate::api::model::RepoId;
use crate::data::DataKey;
use crate::forms::Answer;
use crate::keymap::Action;
use crate::picker::{Choice, entry};
use crate::state::{Cmd, Screen, State};
use crate::ui::page::Link;
use crate::work::{self, Cx, Failure, Job};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Of {
pub repo: RepoId,
pub tag: String,
pub id: u64,
pub name: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Save {
pub of: Of,
pub path: PathBuf,
pub replace: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Upload {
pub of: Of,
pub path: PathBuf,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Delete(pub Of);
impl Job for Save {
type Out = u64;
const WRITES: bool = false;
async fn run(&self, cx: &Cx) -> Result<u64, Failure> {
let Save { of, path, replace } = self;
let saved = cx
.gh
.download_asset(&of.repo, of.id, &of.name, path, *replace);
Ok(saved.await?)
}
fn done(self, state: &mut State, result: Result<u64, Failure>) -> Vec<Cmd> {
match result {
Ok(size) => {
let size = crate::ui::text::size(size);
let (name, path) = (self.of.name, self.path.display());
state.info(format!("Saved {name} to {path} ({size})"));
}
Err(why) => state.error(format!("Couldn't save {}: {why}", self.of.name)),
}
Vec::new()
}
}
impl Job for Upload {
type Out = ();
async fn run(&self, cx: &Cx) -> Result<(), Failure> {
let of = &self.of;
Ok(cx
.gh
.upload_asset(&of.repo, of.id, &self.path, &of.name)
.await?)
}
fn done(self, state: &mut State, result: Result<(), Failure>) -> Vec<Cmd> {
if let Err(why) = result {
state.error(format!("GitHub didn't take {}: {why}", self.of.name));
return Vec::new();
}
state.info(format!("Uploaded {}", self.of.name));
follow(state, self.of, true)
}
}
impl Job for Delete {
type Out = ();
async fn run(&self, cx: &Cx) -> Result<(), Failure> {
let of = &self.0;
Ok(cx.gh.delete_asset(&of.repo, of.id, &of.name).await?)
}
fn done(self, state: &mut State, result: Result<(), Failure>) -> Vec<Cmd> {
let of = self.0;
if let Err(why) = result {
return crate::forms::say(state, format!("GitHub didn't delete {}: {why}", of.name));
}
let route = Route::Releases(releases::Release {
repo: of.repo.clone(),
tag: of.tag.clone(),
});
crate::forms::close_sender(state, &route);
state.info(format!("Deleted {}", of.name));
follow(state, of, false)
}
}
fn asset<'a>(r: &'a Release, name: &str) -> Option<&'a Asset> {
r.assets.iter().find(|a| a.name == name)
}
fn of(state: &State, name: &str) -> Option<Of> {
let (repo, tag, r) = super::shown(state)?;
Some(Of {
repo,
tag,
id: r.id?,
name: name.to_owned(),
})
}
pub fn selected(state: &State) -> Option<String> {
let Screen::Page(p) = state.screen() else {
return None;
};
match p.selected_link()? {
Link::Asset(name) => Some(name.clone()),
_ => None,
}
}
#[must_use]
pub fn open(state: &mut State, name: &str) -> Vec<Cmd> {
let Some(of) = of(state, name) else {
return state.just_say("The release hasn't loaded yet");
};
state.open_picker(crate::picker::Kind::SaveAsset(of))
}
#[must_use]
pub fn open_upload(state: &mut State) -> Vec<Cmd> {
if !matches!(
state.route(),
Some(Route::Releases(releases::Release { .. }))
) {
return state.just_say("Assets are uploaded to a release: open its page");
}
if let Some(why) = super::unchangeable(state) {
return state.just_say(why);
}
let Some(of) = of(state, "") else {
return Vec::new();
};
state.open_picker(crate::picker::Kind::Upload(of))
}
pub fn save_rows(of: &Of, q: &str) -> crate::picker::Rows {
crate::picker::save_rows(q, &of.name, "", |path, replace| {
let of = of.clone();
Save { of, path, replace }.into()
})
}
const LISTED: usize = 50;
pub fn upload_rows(state: &State, of: &Of, q: &str) -> crate::picker::Rows {
let typed = q.trim();
let release = (super::shown(state)).map(|(_, _, r)| r);
let row = |path: &Path| {
let name = path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_default();
let size = std::fs::metadata(path).map_or(0, |m| m.len());
let shown = path.display().to_string();
if release.is_some_and(|r| asset(r, &name).is_some()) {
let delete = state.first_key(Action::DeleteComment);
return entry(
format!("{name} is on the release already"),
format!("{delete} on it deletes it"),
None,
);
}
let up = Of { name, ..of.clone() };
entry(
format!("Upload {shown}"),
crate::ui::text::size(size),
Some(Choice::Step(Step::Start(
format!("Uploading {shown}…"),
Upload {
of: up,
path: path.to_owned(),
}
.into(),
))),
)
};
let path = PathBuf::from(if typed.is_empty() { "." } else { typed });
if path.is_file() {
return vec![row(&path)];
}
let (dir, start) = if typed.is_empty() || typed.ends_with('/') || path.is_dir() {
(path, String::new())
} else {
let dir = path
.parent()
.filter(|d| !d.as_os_str().is_empty())
.map_or_else(|| PathBuf::from("."), Path::to_owned);
let start = path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_default();
(dir, start)
};
let Ok(entries) = std::fs::read_dir(&dir) else {
return vec![entry(format!("No file or folder {typed}"), "", None)];
};
let mut files: Vec<PathBuf> = entries
.filter_map(Result::ok)
.map(|e| e.path())
.filter(|p| p.is_file())
.filter(|p| {
p.file_name()
.is_some_and(|n| n.to_string_lossy().starts_with(&start))
})
.collect();
files.sort();
let more = files.len().saturating_sub(LISTED);
let mut rows: Vec<_> = files.iter().take(LISTED).map(|p| row(p)).collect();
if rows.is_empty() {
rows.push(entry(
format!("No files in {} start with “{start}”", dir.display()),
"",
None,
));
}
if more > 0 {
rows.push(entry(
format!("{more} more: type more of the name"),
"",
None,
));
}
rows
}
#[must_use]
pub fn ask_delete(state: &mut State, name: &str) -> Vec<Cmd> {
let Some((repo, tag, r)) = super::shown(state) else {
return Vec::new();
};
let Some(a) = asset(r, name) else {
return Vec::new();
};
let facts = vec![(
format!(
"{}, downloaded {}: it can't be brought back",
crate::ui::text::size(a.size),
if a.downloads == 1 {
"once".to_owned()
} else {
format!("{} times", a.downloads)
}
),
true,
)];
crate::forms::ask(
state,
Route::Releases(releases::Release { repo, tag }),
format!("Delete {name}?"),
facts,
vec![("Delete asset".into(), Answer::DeleteAsset(name.to_owned()))],
);
Vec::new()
}
#[must_use]
pub fn delete(state: &mut State, name: &str) -> Vec<Cmd> {
let Some(of) = of(state, name) else {
return Vec::new();
};
crate::forms::sending(state);
work::start(Delete(of))
}
fn follow(state: &mut State, of: Of, there: bool) -> Vec<Cmd> {
let route = Route::Releases(releases::Release {
repo: of.repo.clone(),
tag: of.tag.clone(),
});
if state.route() != Some(&route) {
return Vec::new();
}
let what = super::Expected::Asset {
repo: of.repo,
tag: of.tag,
name: of.name,
there,
};
crate::follow::expect(state, route.clone(), what);
state.ensure_route(&route, true)
}
pub fn shows(state: &State, repo: &RepoId, tag: &str, name: &str, there: bool) -> bool {
let key = DataKey::Release(repo.clone(), tag.to_owned());
state
.picked::<Release>(&key)
.is_some_and(|r| asset(r, name).is_some() == there)
}