Implement a changelog-generator tool and example
This commit is contained in:
parent
8fb939b5a9
commit
547e509683
6 changed files with 759 additions and 1 deletions
|
|
@ -8,8 +8,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
### Added
|
### Added
|
||||||
- `fetch_position` command in `window` module. [#2280](https://github.com/iced-rs/iced/pull/2280)
|
- `fetch_position` command in `window` module. [#2280](https://github.com/iced-rs/iced/pull/2280)
|
||||||
|
|
||||||
Many thanks to...
|
### Fixed
|
||||||
|
- Fix `block_on` in `iced_wgpu` hanging Wasm builds. [#2313](https://github.com/iced-rs/iced/pull/2313)
|
||||||
|
|
||||||
|
Many thanks to...
|
||||||
|
- @hecrj
|
||||||
- @n1ght-hunter
|
- @n1ght-hunter
|
||||||
|
|
||||||
## [0.12.1] - 2024-02-22
|
## [0.12.1] - 2024-02-22
|
||||||
|
|
|
||||||
23
examples/changelog/Cargo.toml
Normal file
23
examples/changelog/Cargo.toml
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
[package]
|
||||||
|
name = "changelog"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
iced.workspace = true
|
||||||
|
iced.features = ["tokio", "markdown", "highlighter", "debug"]
|
||||||
|
|
||||||
|
log.workspace = true
|
||||||
|
thiserror.workspace = true
|
||||||
|
tokio.features = ["fs", "process"]
|
||||||
|
tokio.workspace = true
|
||||||
|
|
||||||
|
serde = "1"
|
||||||
|
webbrowser = "1"
|
||||||
|
|
||||||
|
[dependencies.reqwest]
|
||||||
|
version = "0.12"
|
||||||
|
default-features = false
|
||||||
|
features = ["json", "rustls-tls"]
|
||||||
BIN
examples/changelog/fonts/changelog-icons.ttf
Normal file
BIN
examples/changelog/fonts/changelog-icons.ttf
Normal file
Binary file not shown.
354
examples/changelog/src/changelog.rs
Normal file
354
examples/changelog/src/changelog.rs
Normal file
|
|
@ -0,0 +1,354 @@
|
||||||
|
use serde::Deserialize;
|
||||||
|
use tokio::fs;
|
||||||
|
use tokio::process;
|
||||||
|
|
||||||
|
use std::env;
|
||||||
|
use std::fmt;
|
||||||
|
use std::io;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Changelog {
|
||||||
|
ids: Vec<u64>,
|
||||||
|
added: Vec<String>,
|
||||||
|
changed: Vec<String>,
|
||||||
|
fixed: Vec<String>,
|
||||||
|
removed: Vec<String>,
|
||||||
|
authors: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Changelog {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
ids: Vec::new(),
|
||||||
|
added: Vec::new(),
|
||||||
|
changed: Vec::new(),
|
||||||
|
fixed: Vec::new(),
|
||||||
|
removed: Vec::new(),
|
||||||
|
authors: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list() -> Result<(Self, Vec<Candidate>), Error> {
|
||||||
|
let mut changelog = Self::new();
|
||||||
|
|
||||||
|
{
|
||||||
|
let markdown = fs::read_to_string("CHANGELOG.md").await?;
|
||||||
|
|
||||||
|
if let Some(unreleased) = markdown.split("\n## ").nth(1) {
|
||||||
|
let sections = unreleased.split("\n\n");
|
||||||
|
|
||||||
|
for section in sections {
|
||||||
|
if section.starts_with("Many thanks to...") {
|
||||||
|
for author in section.lines().skip(1) {
|
||||||
|
let author = author.trim_start_matches("- @");
|
||||||
|
|
||||||
|
if author.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
changelog.authors.push(author.to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some((_, rest)) = section.split_once("### ") else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some((name, rest)) = rest.split_once("\n") else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let category = match name {
|
||||||
|
"Added" => Category::Added,
|
||||||
|
"Fixed" => Category::Fixed,
|
||||||
|
"Changed" => Category::Changed,
|
||||||
|
"Removed" => Category::Removed,
|
||||||
|
_ => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
for entry in rest.lines() {
|
||||||
|
let Some((_, id)) = entry.split_once('#') else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some((id, _)) = id.split_once(']') else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Ok(id): Result<u64, _> = id.parse() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
changelog.ids.push(id);
|
||||||
|
|
||||||
|
let target = match category {
|
||||||
|
Category::Added => &mut changelog.added,
|
||||||
|
Category::Changed => &mut changelog.added,
|
||||||
|
Category::Fixed => &mut changelog.fixed,
|
||||||
|
Category::Removed => &mut changelog.removed,
|
||||||
|
};
|
||||||
|
|
||||||
|
target.push(entry.to_owned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut candidates = Candidate::list().await?;
|
||||||
|
|
||||||
|
for reviewed_entry in changelog.entries() {
|
||||||
|
candidates.retain(|candidate| candidate.id != reviewed_entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((changelog, candidates))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.ids.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn entries(&self) -> impl Iterator<Item = u64> + '_ {
|
||||||
|
self.ids.iter().copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push(&mut self, entry: Entry) {
|
||||||
|
self.ids.push(entry.id);
|
||||||
|
|
||||||
|
let item = format!(
|
||||||
|
"- {title}. [#{id}](https://github.com/iced-rs/iced/pull/{id})",
|
||||||
|
title = entry.title,
|
||||||
|
id = entry.id
|
||||||
|
);
|
||||||
|
|
||||||
|
let target = match entry.category {
|
||||||
|
Category::Added => &mut self.added,
|
||||||
|
Category::Changed => &mut self.added,
|
||||||
|
Category::Fixed => &mut self.fixed,
|
||||||
|
Category::Removed => &mut self.removed,
|
||||||
|
};
|
||||||
|
|
||||||
|
target.push(item);
|
||||||
|
|
||||||
|
if !self.authors.contains(&entry.author) {
|
||||||
|
self.authors.push(entry.author);
|
||||||
|
self.authors.sort_by_key(|author| author.to_lowercase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Changelog {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
fn section(category: Category, entries: &[String]) -> String {
|
||||||
|
if entries.is_empty() {
|
||||||
|
return String::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
format!("### {category}\n{list}\n", list = entries.join("\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn thank_you<'a>(authors: impl IntoIterator<Item = &'a str>) -> String {
|
||||||
|
let mut list = String::new();
|
||||||
|
|
||||||
|
for author in authors {
|
||||||
|
list.push_str(&format!("- @{author}\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
format!("Many thanks to...\n{list}")
|
||||||
|
}
|
||||||
|
|
||||||
|
let changelog = [
|
||||||
|
section(Category::Added, &self.added),
|
||||||
|
section(Category::Changed, &self.changed),
|
||||||
|
section(Category::Fixed, &self.fixed),
|
||||||
|
section(Category::Removed, &self.removed),
|
||||||
|
thank_you(self.authors.iter().map(String::as_str)),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.filter(|section| !section.is_empty())
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
f.write_str(&changelog)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Entry {
|
||||||
|
pub id: u64,
|
||||||
|
pub title: String,
|
||||||
|
pub category: Category,
|
||||||
|
pub author: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Entry {
|
||||||
|
pub fn new(
|
||||||
|
title: &str,
|
||||||
|
category: Category,
|
||||||
|
pull_request: &PullRequest,
|
||||||
|
) -> Option<Self> {
|
||||||
|
let title = title.strip_suffix(".").unwrap_or(title);
|
||||||
|
|
||||||
|
if title.is_empty() {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(Self {
|
||||||
|
id: pull_request.id,
|
||||||
|
title: title.to_owned(),
|
||||||
|
category,
|
||||||
|
author: pull_request.author.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Category {
|
||||||
|
Added,
|
||||||
|
Changed,
|
||||||
|
Fixed,
|
||||||
|
Removed,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Category {
|
||||||
|
pub const ALL: &'static [Self] =
|
||||||
|
&[Self::Added, Self::Changed, Self::Fixed, Self::Removed];
|
||||||
|
|
||||||
|
pub fn guess(label: &str) -> Option<Self> {
|
||||||
|
Some(match label {
|
||||||
|
"feature" | "addition" => Self::Added,
|
||||||
|
"change" => Self::Changed,
|
||||||
|
"bug" | "fix" => Self::Fixed,
|
||||||
|
_ => None?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Category {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.write_str(match self {
|
||||||
|
Category::Added => "Added",
|
||||||
|
Category::Changed => "Changed",
|
||||||
|
Category::Fixed => "Fixed",
|
||||||
|
Category::Removed => "Removed",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Candidate {
|
||||||
|
pub id: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct PullRequest {
|
||||||
|
pub id: u64,
|
||||||
|
pub title: String,
|
||||||
|
pub description: String,
|
||||||
|
pub labels: Vec<String>,
|
||||||
|
pub author: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Candidate {
|
||||||
|
pub async fn list() -> Result<Vec<Candidate>, Error> {
|
||||||
|
let output = process::Command::new("git")
|
||||||
|
.args([
|
||||||
|
"log",
|
||||||
|
"--oneline",
|
||||||
|
"--grep",
|
||||||
|
"#[0-9]*",
|
||||||
|
"origin/latest..HEAD",
|
||||||
|
])
|
||||||
|
.output()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let log = String::from_utf8_lossy(&output.stdout);
|
||||||
|
|
||||||
|
Ok(log
|
||||||
|
.lines()
|
||||||
|
.filter(|title| !title.is_empty())
|
||||||
|
.filter_map(|title| {
|
||||||
|
let (_, pull_request) = title.split_once("#")?;
|
||||||
|
let (pull_request, _) = pull_request.split_once([')', ' '])?;
|
||||||
|
|
||||||
|
Some(Candidate {
|
||||||
|
id: pull_request.parse().ok()?,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn fetch(self) -> Result<PullRequest, Error> {
|
||||||
|
let request = reqwest::Client::new()
|
||||||
|
.request(
|
||||||
|
reqwest::Method::GET,
|
||||||
|
format!(
|
||||||
|
"https://api.github.com/repos/iced-rs/iced/pulls/{}",
|
||||||
|
self.id
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.header("User-Agent", "iced changelog generator")
|
||||||
|
.header(
|
||||||
|
"Authorization",
|
||||||
|
format!(
|
||||||
|
"Bearer {}",
|
||||||
|
env::var("GITHUB_TOKEN")
|
||||||
|
.map_err(|_| Error::GitHubTokenNotFound)?
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Schema {
|
||||||
|
title: String,
|
||||||
|
body: String,
|
||||||
|
user: User,
|
||||||
|
labels: Vec<Label>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct User {
|
||||||
|
login: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Label {
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
let schema: Schema = request.send().await?.json().await?;
|
||||||
|
|
||||||
|
Ok(PullRequest {
|
||||||
|
id: self.id,
|
||||||
|
title: schema.title,
|
||||||
|
description: schema.body,
|
||||||
|
labels: schema.labels.into_iter().map(|label| label.name).collect(),
|
||||||
|
author: schema.user.login,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, thiserror::Error)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("io operation failed: {0}")]
|
||||||
|
IOFailed(Arc<io::Error>),
|
||||||
|
|
||||||
|
#[error("http request failed: {0}")]
|
||||||
|
RequestFailed(Arc<reqwest::Error>),
|
||||||
|
|
||||||
|
#[error("no GITHUB_TOKEN variable was set")]
|
||||||
|
GitHubTokenNotFound,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<io::Error> for Error {
|
||||||
|
fn from(error: io::Error) -> Self {
|
||||||
|
Error::IOFailed(Arc::new(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<reqwest::Error> for Error {
|
||||||
|
fn from(error: reqwest::Error) -> Self {
|
||||||
|
Error::RequestFailed(Arc::new(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
10
examples/changelog/src/icon.rs
Normal file
10
examples/changelog/src/icon.rs
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
use iced::widget::{text, Text};
|
||||||
|
use iced::Font;
|
||||||
|
|
||||||
|
pub const FONT_BYTES: &[u8] = include_bytes!("../fonts/changelog-icons.ttf");
|
||||||
|
|
||||||
|
const FONT: Font = Font::with_name("changelog-icons");
|
||||||
|
|
||||||
|
pub fn copy() -> Text<'static> {
|
||||||
|
text('\u{e800}').font(FONT)
|
||||||
|
}
|
||||||
368
examples/changelog/src/main.rs
Normal file
368
examples/changelog/src/main.rs
Normal file
|
|
@ -0,0 +1,368 @@
|
||||||
|
mod changelog;
|
||||||
|
mod icon;
|
||||||
|
|
||||||
|
use crate::changelog::Changelog;
|
||||||
|
|
||||||
|
use iced::clipboard;
|
||||||
|
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::{Element, Fill, FillPortion, Font, Task, Theme};
|
||||||
|
|
||||||
|
pub fn main() -> iced::Result {
|
||||||
|
iced::application("Changelog Generator", Generator::update, Generator::view)
|
||||||
|
.font(icon::FONT_BYTES)
|
||||||
|
.theme(Generator::theme)
|
||||||
|
.run_with(Generator::new)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Generator {
|
||||||
|
Loading,
|
||||||
|
Empty,
|
||||||
|
Reviewing {
|
||||||
|
changelog: Changelog,
|
||||||
|
pending: Vec<changelog::Candidate>,
|
||||||
|
state: State,
|
||||||
|
preview: Vec<markdown::Item>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
enum State {
|
||||||
|
Loading(changelog::Candidate),
|
||||||
|
Loaded {
|
||||||
|
pull_request: changelog::PullRequest,
|
||||||
|
description: Vec<markdown::Item>,
|
||||||
|
title: String,
|
||||||
|
category: changelog::Category,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
enum Message {
|
||||||
|
ChangelogListed(
|
||||||
|
Result<(Changelog, Vec<changelog::Candidate>), changelog::Error>,
|
||||||
|
),
|
||||||
|
PullRequestFetched(Result<changelog::PullRequest, changelog::Error>),
|
||||||
|
UrlClicked(markdown::Url),
|
||||||
|
TitleChanged(String),
|
||||||
|
CategorySelected(changelog::Category),
|
||||||
|
Next,
|
||||||
|
OpenPullRequest(u64),
|
||||||
|
CopyPreview,
|
||||||
|
}
|
||||||
|
|
||||||
|
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(candidate) = pending.pop() {
|
||||||
|
let preview =
|
||||||
|
markdown::parse(&changelog.to_string()).collect();
|
||||||
|
|
||||||
|
*self = Self::Reviewing {
|
||||||
|
changelog,
|
||||||
|
pending,
|
||||||
|
state: State::Loading(candidate.clone()),
|
||||||
|
preview,
|
||||||
|
};
|
||||||
|
|
||||||
|
Task::perform(
|
||||||
|
candidate.fetch(),
|
||||||
|
Message::PullRequestFetched,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
*self = Self::Empty;
|
||||||
|
|
||||||
|
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::ChangelogListed(Err(error))
|
||||||
|
| Message::PullRequestFetched(Err(error)) => {
|
||||||
|
log::error!("{error}");
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
*preview =
|
||||||
|
markdown::parse(&changelog.to_string()).collect();
|
||||||
|
|
||||||
|
if let Some(candidate) = pending.pop() {
|
||||||
|
*state = State::Loading(candidate.clone());
|
||||||
|
|
||||||
|
Task::perform(
|
||||||
|
candidate.fetch(),
|
||||||
|
Message::PullRequestFetched,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// TODO: We are done!
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::OpenPullRequest(id) => {
|
||||||
|
let _ = webbrowser::open(&format!(
|
||||||
|
"https://github.com/iced-rs/iced/pull/{id}"
|
||||||
|
));
|
||||||
|
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::CopyPreview => {
|
||||||
|
let Self::Reviewing { changelog, .. } = self else {
|
||||||
|
return Task::none();
|
||||||
|
};
|
||||||
|
|
||||||
|
clipboard::write(changelog.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view(&self) -> Element<Message> {
|
||||||
|
match self {
|
||||||
|
Self::Loading => center("Loading...").into(),
|
||||||
|
Self::Empty => center("No changes found!").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(candidate) => {
|
||||||
|
text!("Loading #{}...", candidate.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: Element<_> = if preview.is_empty() {
|
||||||
|
center(
|
||||||
|
container(
|
||||||
|
text("The changelog is empty... so far!").size(12),
|
||||||
|
)
|
||||||
|
.padding(10)
|
||||||
|
.style(container::rounded_box),
|
||||||
|
)
|
||||||
|
.into()
|
||||||
|
} else {
|
||||||
|
let content = 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 copy = button(icon::copy().size(12))
|
||||||
|
.on_press(Message::CopyPreview)
|
||||||
|
.style(button::text);
|
||||||
|
|
||||||
|
center(stack![content, container(copy).align_right(Fill)])
|
||||||
|
.into()
|
||||||
|
};
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue