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, state: State, preview: Vec, }, Done, } enum State { Loading(changelog::Contribution), Loaded { pull_request: changelog::PullRequest, description: Vec, title: String, category: changelog::Category, }, } #[derive(Debug, Clone)] enum Message { ChangelogListed( Result<(Changelog, Vec), changelog::Error>, ), PullRequestFetched(Result), UrlClicked(markdown::Url), TitleChanged(String), CategorySelected(changelog::Category), Next, OpenPullRequest(u64), ChangelogSaved(Result<(), changelog::Error>), Quit, } impl Generator { fn new() -> (Self, Task) { ( Self::Loading, Task::perform(Changelog::list(), Message::ChangelogListed), ) } fn update(&mut self, message: Message) -> Task { 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).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 { 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( Message::OpenPullRequest(pull_request.id) ), span(format!(" by {}", pull_request.author)) .font(Font { style: font::Style::Italic, ..Font::default() }), ] .font(Font::MONOSPACE); let description = markdown::view( description, markdown::Settings::default(), markdown::Style::from_palette( self.theme().palette(), ), ) .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::view( preview, markdown::Settings::with_text_size(12), markdown::Style::from_palette( self.theme().palette(), ), ) .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 } }