Files
galaxy/app/src/changelog_model.rs
T

251 lines
8.4 KiB
Rust

use std::{collections::HashMap, fmt, sync::Arc};
use channel_versions::{Changelog, MarkdownSection};
use galaxyui::{
assets::asset_cache::{AssetCache, AssetSource},
image_cache::ImageType,
Entity, ModelContext, SingletonEntity,
};
use itertools::Itertools;
use markdown_parser::{parse_markdown, FormattedText};
use crate::{
autoupdate::{self},
channel::{Channel, ChannelState},
features::{FeatureFlag, PREVIEW_FLAGS},
server::server_api::ServerApi,
};
pub struct ChangelogModel {
pub changelog: ChangelogState,
pub parsed_changelog: HashMap<String, FormattedText>,
pub oz_updates: Vec<FormattedText>,
pub server_api: Arc<ServerApi>,
pub image: Option<AssetSource>,
}
impl ChangelogModel {
pub fn new(server_api: Arc<ServerApi>) -> Self {
Self {
changelog: ChangelogState::None,
parsed_changelog: HashMap::new(),
oz_updates: Vec::new(),
server_api,
image: None,
}
}
pub fn check_for_changelog(
&mut self,
request_type: ChangelogRequestType,
ctx: &mut ModelContext<Self>,
) {
match &self.changelog {
ChangelogState::Some(changelog) => {
// Don't refetch the changelog if we already have it
ctx.notify();
ctx.emit(Event::ChangelogRequestComplete {
request_type,
changelog: changelog.clone(),
});
}
ChangelogState::Pending => {
// There is already a request pending, so no-op while we wait for the response
}
ChangelogState::None => {
self.changelog = ChangelogState::Pending;
let server_api = self.server_api.clone();
let _ = ctx.spawn(
async move {
(
request_type,
autoupdate::get_current_changelog(server_api).await,
)
},
Self::handle_changelog_check,
);
}
}
}
fn handle_changelog_check(
&mut self,
(request_type, changelog): (
ChangelogRequestType,
Result<Option<Changelog>, anyhow::Error>,
),
ctx: &mut ModelContext<Self>,
) {
match changelog {
Ok(Some(changelog)) => {
if FeatureFlag::OzChangelogUpdates.is_enabled() {
self.oz_updates = changelog
.oz_updates
.iter()
.filter_map(|update_markdown| parse_markdown(update_markdown).ok())
.collect();
}
self.changelog = ChangelogState::Some(changelog.clone());
self.maybe_add_changelog_sections();
self.parse_changelog_markdown();
ctx.notify();
ctx.emit(Event::ChangelogRequestComplete {
request_type,
changelog,
});
// If the image URL is empty, we just log info and don't try to fetch any image
self.fetch_changelog_image(ctx);
}
Ok(None) => {
self.changelog = ChangelogState::None;
log::info!("No changelog found for current version and channel");
ctx.emit(Event::ChangelogRequestFailed { request_type });
}
Err(e) => {
self.changelog = ChangelogState::None;
log::warn!("Error checking for changelog {e:?}");
ctx.emit(Event::ChangelogRequestFailed { request_type });
}
}
}
fn fetch_changelog_image(&mut self, ctx: &mut ModelContext<Self>) {
let ChangelogState::Some(changelog) = &self.changelog else {
return;
};
let Some(image_url) = changelog.image_url.as_ref() else {
return;
};
let source = asset_cache::url_source(image_url);
// By starting the fetch eagerly, we try to reduce the amount of time the changelog needs to render prior to
// the image being available.
AssetCache::as_ref(ctx).load_asset::<ImageType>(source.clone());
self.image = Some(source);
}
/// Modifies the set of sections in the changelog, if necessary.
fn maybe_add_changelog_sections(&mut self) {
let markdown_sections = match &mut self.changelog {
ChangelogState::Some(changelog) => &mut changelog.markdown_sections,
_ => return,
};
// For WarpPreview, add a section at the beginning describing
// preview-exclusive flags.
if ChannelState::channel() == Channel::Preview && !PREVIEW_FLAGS.is_empty() {
let preview_flags_vec: Vec<String> = PREVIEW_FLAGS
.iter()
.filter_map(|flag| flag.flag_description().map(ToOwned::to_owned))
.collect();
let mut preview_flags_string = preview_flags_vec
.iter()
.map(|flag| format!("* ***Preview-exclusive***: {flag}"))
.join("\n");
preview_flags_string.push('\n');
// Insert preview-exclusive features into the "New features" section (markdown_sections[0])
if markdown_sections.is_empty() {
markdown_sections.push(MarkdownSection {
title: ChangelogHeader::NewFeatures.to_string(),
markdown: preview_flags_string,
});
} else {
markdown_sections[0]
.markdown
.insert_str(0, preview_flags_string.as_str());
}
}
// If there are no sections at all, add a section clarifying that there
// are no meaningful updates this release.
if markdown_sections.is_empty() {
markdown_sections.push(MarkdownSection {
title: ChangelogHeader::NewFeatures.to_string(),
markdown: "* No notable changes this release\n".to_owned(),
});
if ChannelState::channel() == Channel::Dev {
markdown_sections[0].markdown.push_str("* *Don't forget to put changelog information in your PR description, if applicable!*\n");
}
} else if markdown_sections
.iter()
.all(|section| section.markdown.is_empty())
{
// Add this to the "New features" section (markdown_sections[0])
"* No notable changes this release\n".clone_into(&mut markdown_sections[0].markdown);
if ChannelState::channel() == Channel::Dev {
markdown_sections[0].markdown.push_str("* *Don't forget to put changelog information in your PR description, if applicable!*\n");
}
}
}
fn parse_changelog_markdown(&mut self) {
if let ChangelogState::Some(changelog) = &self.changelog {
for markdown_section in &changelog.markdown_sections {
if !markdown_section.markdown.is_empty() {
if let Ok(parsed_markdown) = parse_markdown(markdown_section.markdown.as_str())
{
self.parsed_changelog
.insert(markdown_section.title.clone(), parsed_markdown);
}
}
}
}
}
pub fn is_check_pending(&self) -> bool {
matches!(self.changelog, ChangelogState::Pending)
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum ChangelogHeader {
NewFeatures,
Improvements,
BugFixes,
}
impl fmt::Display for ChangelogHeader {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ChangelogHeader::NewFeatures => write!(f, "New features"),
ChangelogHeader::Improvements => write!(f, "Improvements"),
ChangelogHeader::BugFixes => write!(f, "Bug fixes"),
}
}
}
#[derive(Debug)]
pub enum Event {
ChangelogRequestComplete {
request_type: ChangelogRequestType,
changelog: Changelog,
},
ChangelogRequestFailed {
request_type: ChangelogRequestType,
},
ImageRequestComplete,
}
#[derive(Debug)]
pub enum ChangelogRequestType {
WindowLaunch,
UserAction,
}
pub enum ChangelogState {
None,
Pending,
Some(Changelog),
}
impl Entity for ChangelogModel {
type Event = Event;
}
impl SingletonEntity for ChangelogModel {}