summaryrefslogblamecommitdiffstats
path: root/examples/changelog/src/main.rs
blob: 79fa37b5bb3cf7b0fc3ad62a9a49bde0fc626d67 (plain) (tree)
1
2
3
4
5
6
7
8
9
              


                                




                                                                         
                                                                  


                                                                                





                                 

                             
                                              


                                     
         


            
                                     










                                             
                                                                            






                                                                         

                                                 












                                                                       
                                                           





                                                                          
                                                                    



                                  
                                                                    


                                                    
                                       


























                                                                         

























































                                                                         




                                                 


                                                                          

                                                                      
 






                                                                            
                            

                                           











                                                               





                                                            
 
                            
             
                                          





                                                         









                                                           

























                                                                

                                                                       














































































                                                                             
                                                     






                                                                               
                        
                              













                                                                       
                                                  














                                                                            
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 {
    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).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(
                                    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
    }
}