375 lines
12 KiB
Rust
375 lines
12 KiB
Rust
mod changelog;
|
|
|
|
use crate::changelog::Changelog;
|
|
|
|
use iced::font;
|
|
use iced::widget::{
|
|
button, center, column, container, markdown, pick_list, progress_bar,
|
|
rich_text, row, scrollable, span, stack, text, text_input,
|
|
};
|
|
use iced::{Center, Element, Fill, FillPortion, Font, Task, Theme};
|
|
|
|
pub fn main() -> iced::Result {
|
|
tracing_subscriber::fmt::init();
|
|
|
|
iced::application("Changelog Generator", Generator::update, Generator::view)
|
|
.theme(Generator::theme)
|
|
.run_with(Generator::new)
|
|
}
|
|
|
|
enum Generator {
|
|
Loading,
|
|
Reviewing {
|
|
changelog: Changelog,
|
|
pending: Vec<changelog::Contribution>,
|
|
state: State,
|
|
preview: Vec<markdown::Item>,
|
|
},
|
|
Done,
|
|
}
|
|
|
|
enum State {
|
|
Loading(changelog::Contribution),
|
|
Loaded {
|
|
pull_request: changelog::PullRequest,
|
|
description: Vec<markdown::Item>,
|
|
title: String,
|
|
category: changelog::Category,
|
|
},
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
enum Message {
|
|
ChangelogListed(
|
|
Result<(Changelog, Vec<changelog::Contribution>), changelog::Error>,
|
|
),
|
|
PullRequestFetched(Result<changelog::PullRequest, changelog::Error>),
|
|
UrlClicked(markdown::Url),
|
|
TitleChanged(String),
|
|
CategorySelected(changelog::Category),
|
|
Next,
|
|
OpenPullRequest(u64),
|
|
ChangelogSaved(Result<(), changelog::Error>),
|
|
Quit,
|
|
}
|
|
|
|
impl Generator {
|
|
fn new() -> (Self, Task<Message>) {
|
|
(
|
|
Self::Loading,
|
|
Task::perform(Changelog::list(), Message::ChangelogListed),
|
|
)
|
|
}
|
|
|
|
fn update(&mut self, message: Message) -> Task<Message> {
|
|
match message {
|
|
Message::ChangelogListed(Ok((changelog, mut pending))) => {
|
|
if let Some(contribution) = pending.pop() {
|
|
let preview =
|
|
markdown::parse(&changelog.to_string()).collect();
|
|
|
|
*self = Self::Reviewing {
|
|
changelog,
|
|
pending,
|
|
state: State::Loading(contribution.clone()),
|
|
preview,
|
|
};
|
|
|
|
Task::perform(
|
|
changelog::PullRequest::fetch(contribution),
|
|
Message::PullRequestFetched,
|
|
)
|
|
} else {
|
|
*self = Self::Done;
|
|
|
|
Task::none()
|
|
}
|
|
}
|
|
Message::PullRequestFetched(Ok(pull_request)) => {
|
|
let Self::Reviewing { state, .. } = self else {
|
|
return Task::none();
|
|
};
|
|
|
|
let description = markdown::parse(
|
|
pull_request
|
|
.description
|
|
.as_deref()
|
|
.unwrap_or("*No description provided*"),
|
|
)
|
|
.collect();
|
|
|
|
*state = State::Loaded {
|
|
title: pull_request.title.clone(),
|
|
category: pull_request
|
|
.labels
|
|
.iter()
|
|
.map(String::as_str)
|
|
.filter_map(changelog::Category::guess)
|
|
.next()
|
|
.unwrap_or(changelog::Category::Added),
|
|
pull_request,
|
|
description,
|
|
};
|
|
|
|
Task::none()
|
|
}
|
|
Message::UrlClicked(url) => {
|
|
let _ = webbrowser::open(url.as_str());
|
|
|
|
Task::none()
|
|
}
|
|
Message::TitleChanged(new_title) => {
|
|
let Self::Reviewing { state, .. } = self else {
|
|
return Task::none();
|
|
};
|
|
|
|
let State::Loaded { title, .. } = state else {
|
|
return Task::none();
|
|
};
|
|
|
|
*title = new_title;
|
|
|
|
Task::none()
|
|
}
|
|
Message::CategorySelected(new_category) => {
|
|
let Self::Reviewing { state, .. } = self else {
|
|
return Task::none();
|
|
};
|
|
|
|
let State::Loaded { category, .. } = state else {
|
|
return Task::none();
|
|
};
|
|
|
|
*category = new_category;
|
|
|
|
Task::none()
|
|
}
|
|
Message::Next => {
|
|
let Self::Reviewing {
|
|
changelog,
|
|
pending,
|
|
state,
|
|
preview,
|
|
..
|
|
} = self
|
|
else {
|
|
return Task::none();
|
|
};
|
|
|
|
let State::Loaded {
|
|
title,
|
|
category,
|
|
pull_request,
|
|
..
|
|
} = state
|
|
else {
|
|
return Task::none();
|
|
};
|
|
|
|
if let Some(entry) =
|
|
changelog::Entry::new(title, *category, pull_request)
|
|
{
|
|
changelog.push(entry);
|
|
|
|
let save = Task::perform(
|
|
changelog.clone().save(),
|
|
Message::ChangelogSaved,
|
|
);
|
|
|
|
*preview =
|
|
markdown::parse(&changelog.to_string()).collect();
|
|
|
|
if let Some(contribution) = pending.pop() {
|
|
*state = State::Loading(contribution.clone());
|
|
|
|
Task::batch([
|
|
save,
|
|
Task::perform(
|
|
changelog::PullRequest::fetch(contribution),
|
|
Message::PullRequestFetched,
|
|
),
|
|
])
|
|
} else {
|
|
*self = Self::Done;
|
|
save
|
|
}
|
|
} else {
|
|
Task::none()
|
|
}
|
|
}
|
|
Message::OpenPullRequest(id) => {
|
|
let _ = webbrowser::open(&format!(
|
|
"https://github.com/iced-rs/iced/pull/{id}"
|
|
));
|
|
|
|
Task::none()
|
|
}
|
|
Message::ChangelogSaved(Ok(())) => Task::none(),
|
|
|
|
Message::ChangelogListed(Err(error))
|
|
| Message::PullRequestFetched(Err(error))
|
|
| Message::ChangelogSaved(Err(error)) => {
|
|
log::error!("{error}");
|
|
|
|
Task::none()
|
|
}
|
|
Message::Quit => iced::exit(),
|
|
}
|
|
}
|
|
|
|
fn view(&self) -> Element<Message> {
|
|
match self {
|
|
Self::Loading => center("Loading...").into(),
|
|
Self::Done => center(
|
|
column![
|
|
text("Changelog is up-to-date! 🎉")
|
|
.shaping(text::Shaping::Advanced),
|
|
button("Quit").on_press(Message::Quit),
|
|
]
|
|
.spacing(10)
|
|
.align_x(Center),
|
|
)
|
|
.into(),
|
|
Self::Reviewing {
|
|
changelog,
|
|
pending,
|
|
state,
|
|
preview,
|
|
} => {
|
|
let progress = {
|
|
let total = pending.len() + changelog.len();
|
|
|
|
let bar = progress_bar(
|
|
0.0..=1.0,
|
|
changelog.len() as f32 / total as f32,
|
|
)
|
|
.style(progress_bar::secondary);
|
|
|
|
let label = text!(
|
|
"{amount_reviewed} / {total}",
|
|
amount_reviewed = changelog.len()
|
|
)
|
|
.font(Font::MONOSPACE)
|
|
.size(12);
|
|
|
|
stack![bar, center(label)]
|
|
};
|
|
|
|
let form: Element<_> = match state {
|
|
State::Loading(contribution) => {
|
|
text!("Loading #{}...", contribution.id).into()
|
|
}
|
|
State::Loaded {
|
|
pull_request,
|
|
description,
|
|
title,
|
|
category,
|
|
} => {
|
|
let details = {
|
|
let title = rich_text![
|
|
span(&pull_request.title)
|
|
.size(24)
|
|
.link(pull_request.id),
|
|
span(format!(" by {}", pull_request.author))
|
|
.font(Font {
|
|
style: font::Style::Italic,
|
|
..Font::default()
|
|
}),
|
|
]
|
|
.on_link_click(Message::OpenPullRequest)
|
|
.font(Font::MONOSPACE);
|
|
|
|
let description =
|
|
markdown(description, self.theme())
|
|
.map(Message::UrlClicked);
|
|
|
|
let labels =
|
|
row(pull_request.labels.iter().map(|label| {
|
|
container(
|
|
text(label)
|
|
.size(10)
|
|
.font(Font::MONOSPACE),
|
|
)
|
|
.padding(5)
|
|
.style(container::rounded_box)
|
|
.into()
|
|
}))
|
|
.spacing(10)
|
|
.wrap();
|
|
|
|
column![
|
|
title,
|
|
labels,
|
|
scrollable(description)
|
|
.spacing(10)
|
|
.width(Fill)
|
|
.height(Fill)
|
|
]
|
|
.spacing(10)
|
|
};
|
|
|
|
let title = text_input(
|
|
"Type a changelog entry title...",
|
|
title,
|
|
)
|
|
.on_input(Message::TitleChanged);
|
|
|
|
let category = pick_list(
|
|
changelog::Category::ALL,
|
|
Some(category),
|
|
Message::CategorySelected,
|
|
);
|
|
|
|
let next = button("Next →")
|
|
.on_press(Message::Next)
|
|
.style(button::success);
|
|
|
|
column![
|
|
details,
|
|
row![title, category, next].spacing(10)
|
|
]
|
|
.spacing(10)
|
|
.into()
|
|
}
|
|
};
|
|
|
|
let preview = if preview.is_empty() {
|
|
center(
|
|
container(
|
|
text("The changelog is empty... so far!").size(12),
|
|
)
|
|
.padding(10)
|
|
.style(container::rounded_box),
|
|
)
|
|
} else {
|
|
container(
|
|
scrollable(
|
|
markdown(
|
|
preview,
|
|
markdown::Settings::with_text_size(
|
|
12,
|
|
self.theme(),
|
|
),
|
|
)
|
|
.map(Message::UrlClicked),
|
|
)
|
|
.spacing(10),
|
|
)
|
|
.width(Fill)
|
|
.padding(10)
|
|
.style(container::rounded_box)
|
|
};
|
|
|
|
let review = column![container(form).height(Fill), progress]
|
|
.spacing(10)
|
|
.width(FillPortion(2));
|
|
|
|
row![review, preview].spacing(10).padding(10).into()
|
|
}
|
|
}
|
|
}
|
|
|
|
fn theme(&self) -> Theme {
|
|
Theme::TokyoNightStorm
|
|
}
|
|
}
|