mod element;
mod style;
mod update;
use crate::cli::Opts;
use crate::localization::{localized_string, LANG};
use crate::Result;
use ajour_core::{
addon::{Addon, AddonFolder, AddonState},
cache::catalog_download_latest_or_use_cache,
cache::{
load_addon_cache, load_fingerprint_cache, AddonCache, AddonCacheEntry, FingerprintCache,
},
catalog::{self, Catalog, CatalogAddon},
config::{ColumnConfig, ColumnConfigV2, Config, Flavor, Language, SelfUpdateChannel},
error::*,
fs::PersistentData,
repository::{
Changelog, CompressionFormat, GlobalReleaseChannel, ReleaseChannel, RepositoryPackage,
},
theme::{load_user_themes, Theme},
utility::{self, get_latest_release},
};
use ajour_weak_auras::Aura;
use ajour_widgets::header;
use async_std::sync::{Arc, Mutex};
use chrono::{DateTime, NaiveDateTime, Utc};
use iced::{
button, pick_list, scrollable, text_input, Align, Application, Button, Column, Command,
Container, Element, HorizontalAlignment, Length, PickList, Row, Scrollable, Settings, Space,
Subscription, Text, TextInput,
};
use image::ImageFormat;
use isahc::http::Uri;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::RwLock;
use std::time::{Duration, Instant};
use strfmt::strfmt;
use element::{DEFAULT_FONT_SIZE, DEFAULT_PADDING};
static WINDOW_ICON: &[u8] = include_bytes!("../../resources/windows/ajour.ico");
#[derive(Debug, Clone, PartialEq)]
pub enum State {
Start,
Ready,
Loading,
}
impl Default for State {
fn default() -> Self {
State::Start
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Mode {
MyAddons(Flavor),
MyWeakAuras(Flavor),
Install,
Catalog,
Settings,
About,
}
impl std::fmt::Display for Mode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Mode::MyAddons(_) => localized_string("my-addons"),
Mode::MyWeakAuras(_) => localized_string("my-weakauras"),
Mode::Install => localized_string("install"),
Mode::Catalog => localized_string("catalog"),
Mode::Settings => localized_string("settings"),
Mode::About => localized_string("about"),
}
)
}
}
#[derive(Debug, Clone)]
#[allow(clippy::large_enum_variant)]
pub enum Interaction {
Delete(String),
Expand(ExpandType),
Ignore(String),
SelectBackupDirectory(),
SelectWowDirectory(Option<Flavor>),
OpenDirectory(PathBuf),
OpenLink(String),
Refresh(Mode),
Unignore(String),
Update(String),
UpdateAll(Mode),
SortColumn(ColumnKey),
SortCatalogColumn(CatalogColumnKey),
SortAuraColumn(AuraColumnKey),
FlavorSelected(Flavor),
ResizeColumn(Mode, header::ResizeEvent),
ScaleUp,
ScaleDown,
Backup,
ToggleColumn(bool, ColumnKey),
ToggleCatalogColumn(bool, CatalogColumnKey),
ToggleHideIgnoredAddons(bool),
MoveColumnLeft(ColumnKey),
MoveColumnRight(ColumnKey),
MoveCatalogColumnLeft(CatalogColumnKey),
MoveCatalogColumnRight(CatalogColumnKey),
ModeSelected(Mode),
CatalogQuery(String),
InstallScmQuery(String),
InstallScmUrl,
InstallAddon(Flavor, String, InstallKind),
CatalogCategorySelected(CatalogCategory),
CatalogResultSizeSelected(CatalogResultSize),
CatalogSourceSelected(CatalogSource),
UpdateAjour,
ToggleBackupFolder(bool, BackupFolderKind),
PickSelfUpdateChannel(SelfUpdateChannel),
PickGlobalReleaseChannel(GlobalReleaseChannel),
PickBackupCompressionFormat(CompressionFormat),
PickLocalizationLanguage(Language),
AlternatingRowColorToggled(bool),
ResetColumns,
ToggleDeleteSavedVariables(bool),
AddonsQuery(String),
ToggleAutoUpdateAddons(bool),
}
#[derive(Debug)]
#[allow(clippy::large_enum_variant)]
pub enum Message {
CachesLoaded(Result<(FingerprintCache, AddonCache)>),
DownloadedAddon((DownloadReason, Flavor, String, Result<(), DownloadError>)),
Error(anyhow::Error),
Interaction(Interaction),
LatestRelease(Option<utility::Release>),
None(()),
Parse(()),
ParsedAddons((Flavor, Result<Vec<Addon>, ParseError>)),
UpdateFingerprint((Flavor, String, Result<(), ParseError>)),
ThemeSelected(String),
ReleaseChannelSelected(ReleaseChannel),
ThemesLoaded(Vec<Theme>),
UnpackedAddon(
(
DownloadReason,
Flavor,
String,
Result<Vec<AddonFolder>, FilesystemError>,
),
),
UpdateWowDirectory((Option<PathBuf>, Option<Flavor>)),
UpdateBackupDirectory(Option<PathBuf>),
RuntimeEvent(iced_native::Event),
LatestBackup(Option<NaiveDateTime>),
BackupFinished(Result<NaiveDateTime, FilesystemError>),
CatalogDownloaded(Result<Catalog, DownloadError>),
InstallAddonFetched((Flavor, String, Result<Addon, RepositoryError>)),
AjourUpdateDownloaded(Result<(PathBuf, PathBuf), DownloadError>),
AddonCacheUpdated(Result<AddonCacheEntry, CacheError>),
AddonCacheEntryRemoved(Result<Option<AddonCacheEntry>, CacheError>),
RefreshCatalog(Instant),
CheckLatestRelease(Instant),
CheckWeakAurasInstalled((Flavor, bool)),
ListWeakAurasAccounts((Flavor, Result<Vec<String>, ajour_weak_auras::Error>)),
WeakAurasAccountSelected(String),
ParsedAuras((Flavor, Result<Vec<Aura>, ajour_weak_auras::Error>)),
AurasUpdated((Flavor, Result<Vec<String>, ajour_weak_auras::Error>)),
FetchedChangelog((Addon, Result<Changelog, RepositoryError>)),
CheckRepositoryUpdates(Instant),
RepositoryPackagesFetched((Flavor, Result<Vec<RepositoryPackage>, DownloadError>)),
}
pub struct Ajour {
state: HashMap<Mode, State>,
error: Option<anyhow::Error>,
mode: Mode,
addons: HashMap<Flavor, Vec<Addon>>,
addons_scrollable_state: scrollable::State,
weakauras_scrollable_state: scrollable::State,
settings_scrollable_state: scrollable::State,
about_scrollable_state: scrollable::State,
config: Config,
expanded_type: ExpandType,
self_update_state: SelfUpdateState,
refresh_btn_state: button::State,
settings_btn_state: button::State,
about_btn_state: button::State,
update_all_btn_state: button::State,
header_state: HeaderState,
theme_state: ThemeState,
fingerprint_cache: Option<Arc<Mutex<FingerprintCache>>>,
addon_cache: Option<Arc<Mutex<AddonCache>>>,
addon_mode_btn_state: button::State,
weakaura_mode_btn_state: button::State,
catalog_mode_btn_state: button::State,
install_mode_btn_state: button::State,
scale_state: ScaleState,
backup_state: BackupState,
column_settings: ColumnSettings,
catalog_column_settings: CatalogColumnSettings,
onboarding_directory_btn_state: button::State,
catalog: Option<Catalog>,
install_addons: HashMap<Flavor, Vec<InstallAddon>>,
catalog_last_updated: Option<DateTime<Utc>>,
catalog_search_state: CatalogSearchState,
catalog_header_state: CatalogHeaderState,
catalog_categories_per_source_cache: HashMap<String, Vec<CatalogCategory>>,
website_btn_state: button::State,
donation_btn_state: button::State,
open_config_dir_btn_state: button::State,
install_from_scm_state: InstallFromScmState,
self_update_channel_state: SelfUpdateChannelState,
default_addon_release_channel_picklist_state: pick_list::State<GlobalReleaseChannel>,
weak_auras_is_installed: bool,
weak_auras_state: HashMap<Flavor, WeakAurasState>,
aura_header_state: AuraHeaderState,
reset_columns_btn_state: button::State,
localization_picklist_state: pick_list::State<Language>,
flavor_picklist_state: pick_list::State<Flavor>,
addons_search_state: AddonsSearchState,
wow_directories: Vec<WowDirectoryState>,
default_backup_compression_format: pick_list::State<CompressionFormat>,
}
impl Default for Ajour {
fn default() -> Self {
Self {
state: [(Mode::Catalog, State::Loading)].iter().cloned().collect(),
error: None,
mode: Mode::MyAddons(Flavor::Retail),
addons: HashMap::new(),
addons_scrollable_state: Default::default(),
weakauras_scrollable_state: Default::default(),
settings_scrollable_state: Default::default(),
about_scrollable_state: Default::default(),
config: Config::default(),
expanded_type: ExpandType::None,
self_update_state: Default::default(),
refresh_btn_state: Default::default(),
settings_btn_state: Default::default(),
about_btn_state: Default::default(),
update_all_btn_state: Default::default(),
header_state: Default::default(),
theme_state: Default::default(),
fingerprint_cache: None,
addon_cache: None,
addon_mode_btn_state: Default::default(),
weakaura_mode_btn_state: Default::default(),
catalog_mode_btn_state: Default::default(),
install_mode_btn_state: Default::default(),
scale_state: Default::default(),
backup_state: Default::default(),
column_settings: Default::default(),
catalog_column_settings: Default::default(),
onboarding_directory_btn_state: Default::default(),
catalog: None,
install_addons: Default::default(),
catalog_last_updated: None,
catalog_search_state: Default::default(),
catalog_header_state: Default::default(),
catalog_categories_per_source_cache: Default::default(),
website_btn_state: Default::default(),
donation_btn_state: Default::default(),
open_config_dir_btn_state: Default::default(),
install_from_scm_state: Default::default(),
self_update_channel_state: SelfUpdateChannelState {
picklist: Default::default(),
options: SelfUpdateChannel::all(),
},
default_addon_release_channel_picklist_state: Default::default(),
weak_auras_is_installed: Default::default(),
weak_auras_state: Default::default(),
aura_header_state: Default::default(),
reset_columns_btn_state: Default::default(),
localization_picklist_state: Default::default(),
flavor_picklist_state: Default::default(),
addons_search_state: Default::default(),
wow_directories: Flavor::ALL
.iter()
.map(|f| WowDirectoryState {
flavor: *f,
button_state: Default::default(),
})
.collect::<Vec<WowDirectoryState>>(),
default_backup_compression_format: Default::default(),
}
}
}
impl Application for Ajour {
type Executor = iced::executor::Default;
type Message = Message;
type Flags = Config;
fn new(config: Config) -> (Self, Command<Message>) {
let init_commands = vec![
Command::perform(load_caches(), Message::CachesLoaded),
Command::perform(
get_latest_release(config.self_update_channel),
Message::LatestRelease,
),
Command::perform(load_user_themes(), Message::ThemesLoaded),
Command::perform(
catalog_download_latest_or_use_cache(),
Message::CatalogDownloaded,
),
];
let mut ajour = Ajour::default();
apply_config(&mut ajour, config);
(ajour, Command::batch(init_commands))
}
fn title(&self) -> String {
String::from("Ajour")
}
fn scale_factor(&self) -> f64 {
self.scale_state.scale
}
fn subscription(&self) -> Subscription<Self::Message> {
let runtime_subscription = iced_native::subscription::events().map(Message::RuntimeEvent);
let catalog_subscription =
iced_futures::time::every(Duration::from_secs(60 * 5)).map(Message::RefreshCatalog);
let new_release_subscription = iced_futures::time::every(Duration::from_secs(60 * 60))
.map(Message::CheckLatestRelease);
let check_updates_subscription = iced_futures::time::every(Duration::from_secs(60 * 30))
.map(Message::CheckRepositoryUpdates);
iced::Subscription::batch(vec![
runtime_subscription,
catalog_subscription,
new_release_subscription,
check_updates_subscription,
])
}
fn update(&mut self, message: Message) -> Command<Message> {
match update::handle_message(self, message) {
Ok(x) => x,
Err(e) => Command::perform(async { e }, Message::Error),
}
}
fn view(&mut self) -> Element<Message> {
let color_palette = self
.theme_state
.themes
.iter()
.find(|(name, _)| name == &self.theme_state.current_theme_name)
.as_ref()
.unwrap_or(&&("Dark".to_string(), Theme::dark()))
.1
.palette;
let flavor = self.config.wow.flavor;
let has_addons = {
let addons = self.addons.entry(flavor).or_default();
!&addons.is_empty()
};
let has_auras = {
let aura_state = self.weak_auras_state.entry(flavor).or_default();
!aura_state.auras.is_empty()
};
let release_copy = if let Some(release) = &self.self_update_state.latest_release {
Some(release.clone())
} else {
None
};
let menu_container = element::menu::data_container(
color_palette,
&self.mode,
&self.state,
&self.error,
&self.config,
&mut self.settings_btn_state,
&mut self.about_btn_state,
&mut self.addon_mode_btn_state,
&mut self.weakaura_mode_btn_state,
&mut self.catalog_mode_btn_state,
&mut self.install_mode_btn_state,
&mut self.self_update_state,
&mut self.flavor_picklist_state,
self.weak_auras_is_installed,
);
let column_config = self.header_state.column_config();
let catalog_column_config = self.catalog_header_state.column_config();
let aura_column_config = self.aura_header_state.column_config();
let mut content = Column::new().push(menu_container);
content = content.push(Space::new(Length::Units(0), Length::Units(DEFAULT_PADDING)));
match self.mode {
Mode::MyAddons(flavor) => {
let addons = self.addons.entry(flavor).or_default();
let has_addons = !&addons.is_empty();
let query = self.addons_search_state.query.clone();
let menu_addons_container = element::my_addons::menu_container(
color_palette,
flavor,
&mut self.update_all_btn_state,
&mut self.refresh_btn_state,
&mut self.addons_search_state,
&self.state,
addons,
&self.config,
);
content = content.push(menu_addons_container);
let addon_row_titles = element::my_addons::titles_row_header(
color_palette,
addons,
&mut self.header_state.state,
&mut self.header_state.columns,
self.header_state.previous_column_key,
self.header_state.previous_sort_direction,
);
let mut addons_scrollable = Scrollable::new(&mut self.addons_scrollable_state)
.spacing(1)
.height(Length::FillPortion(1))
.style(style::Scrollable(color_palette));
for (idx, addon) in addons.iter_mut().enumerate() {
if addon.state == AddonState::Ignored && self.config.hide_ignored_addons {
continue;
}
if query.is_some() && addon.fuzzy_score.is_none() {
continue;
}
let is_addon_expanded = match &self.expanded_type {
ExpandType::Details(a) => a.primary_folder_id == addon.primary_folder_id,
ExpandType::Changelog { addon: a, .. } => {
addon.primary_folder_id == a.primary_folder_id
}
ExpandType::None => false,
};
let is_odd = if self.config.alternating_row_colors {
Some(idx % 2 != 0)
} else {
None
};
let addon_data_cell = element::my_addons::data_row_container(
color_palette,
addon,
is_addon_expanded,
&self.expanded_type,
&self.config,
&column_config,
is_odd,
);
addons_scrollable = addons_scrollable.push(addon_data_cell);
}
let bottom_space =
Space::new(Length::FillPortion(1), Length::Units(DEFAULT_PADDING));
if has_addons {
content = content
.push(addon_row_titles)
.push(addons_scrollable)
.push(bottom_space)
}
}
Mode::MyWeakAuras(flavor) => {
let weak_auras_state = self.weak_auras_state.entry(flavor).or_default();
let num_auras = weak_auras_state.auras.len();
let num_available = weak_auras_state
.auras
.iter()
.filter(|a| a.has_update())
.count();
let is_updating = weak_auras_state.is_updating;
let updates_queued = weak_auras_state
.auras
.iter()
.filter(|a| a.status() == ajour_weak_auras::AuraStatus::UpdateQueued)
.count()
== num_available
&& num_available > 0;
let menu_container = element::my_weakauras::menu_container(
color_palette,
flavor,
&mut self.update_all_btn_state,
&mut self.refresh_btn_state,
&self.state,
num_auras,
num_available > 0,
is_updating,
updates_queued,
&mut weak_auras_state.account_picklist,
&weak_auras_state.accounts,
weak_auras_state.chosen_account.clone(),
);
content = content.push(menu_container);
let aura_row_titles = element::my_weakauras::titles_row_header(
color_palette,
&weak_auras_state.auras,
&mut self.aura_header_state.state,
&mut self.aura_header_state.columns,
self.aura_header_state.previous_column_key,
self.aura_header_state.previous_sort_direction,
);
let mut scrollable = Scrollable::new(&mut self.weakauras_scrollable_state)
.spacing(1)
.height(Length::FillPortion(1))
.style(style::Scrollable(color_palette));
for (idx, aura) in weak_auras_state.auras.iter().enumerate() {
let is_odd = if self.config.alternating_row_colors {
Some(idx % 2 != 0)
} else {
None
};
let row = element::my_weakauras::data_row_container(
color_palette,
aura,
&aura_column_config,
is_odd,
);
scrollable = scrollable.push(row);
}
let bottom_space =
Space::new(Length::FillPortion(1), Length::Units(DEFAULT_PADDING));
if num_auras > 0 {
content = content
.push(aura_row_titles)
.push(scrollable)
.push(bottom_space)
}
}
Mode::Install => {
let query = self
.install_from_scm_state
.query
.as_deref()
.unwrap_or_default();
let url = query.parse::<Uri>().ok();
let is_valid_url = url
.map(|url| {
let host = url.host().map(|h| h.to_lowercase());
host.as_deref() == Some("gitlab.com")
|| host.as_deref() == Some("github.com")
})
.unwrap_or_default();
let default = vec![];
let addons = self.addons.get(&flavor).unwrap_or(&default);
let installed = addons
.iter()
.filter_map(|a| {
let id = a.repository_id()?;
let a = id.parse::<Uri>().ok()?;
let b = query.parse::<Uri>().ok()?;
if a.host().map(|s| s.to_lowercase()) == b.host().map(|s| s.to_lowercase())
&& a.path().to_lowercase() == b.path().to_lowercase()
{
Some(a)
} else {
None
}
})
.count()
> 0;
let install_status = self
.install_addons
.entry(flavor)
.or_default()
.iter()
.find(|a| a.kind == InstallKind::Source)
.map(|a| a.status.clone());
let install_text = Text::new(if installed {
localized_string("installed")
} else {
match install_status {
Some(InstallStatus::Downloading) => localized_string("downloading"),
Some(InstallStatus::Unpacking) => localized_string("unpacking"),
Some(InstallStatus::Retry) => localized_string("retry"),
Some(InstallStatus::Unavailable) => localized_string("unavailable"),
Some(InstallStatus::Error(_)) | None => {
let flavor = self.config.wow.flavor;
let mut vars = HashMap::new();
vars.insert("flavor".to_string(), &flavor);
let fmt = localized_string("install-for-flavor");
strfmt(&fmt, &vars).unwrap()
}
}
})
.size(DEFAULT_FONT_SIZE);
let install_button_title_container = Container::new(install_text)
.center_x()
.center_y()
.width(Length::Units(150))
.height(Length::Units(24));
let mut install_button = Button::new(
&mut self.install_from_scm_state.install_button_state,
install_button_title_container,
)
.style(style::DefaultBoxedButton(color_palette));
if matches!(install_status, None) && !installed && is_valid_url {
install_button = install_button.on_press(Interaction::InstallScmUrl);
}
let install_button: Element<Interaction> = install_button.into();
let mut install_scm_query = TextInput::new(
&mut self.install_from_scm_state.query_state,
&localized_string("install-from-url-example")[..],
query,
Interaction::InstallScmQuery,
)
.size(DEFAULT_FONT_SIZE)
.padding(10)
.width(Length::Units(350))
.style(style::CatalogQueryInput(color_palette));
if !installed
&& !matches!(install_status, Some(InstallStatus::Error(_)))
&& is_valid_url
{
install_scm_query = install_scm_query.on_submit(Interaction::InstallScmUrl);
}
let install_scm_query: Element<Interaction> = install_scm_query.into();
let description = Text::new(localized_string("install-from-url-description"))
.size(DEFAULT_FONT_SIZE)
.width(Length::Fill)
.horizontal_alignment(HorizontalAlignment::Center);
let description_container = Container::new(description)
.width(Length::Fill)
.style(style::NormalBackgroundContainer(color_palette));
let query_row = Row::new()
.push(Space::new(Length::Units(DEFAULT_PADDING), Length::Units(0)))
.push(install_scm_query.map(Message::Interaction))
.push(install_button.map(Message::Interaction))
.push(Space::new(Length::Units(DEFAULT_PADDING), Length::Units(0)))
.align_items(Align::Center)
.spacing(1);
let mut error_text: String = String::from(" ");
if let Some(InstallStatus::Error(error)) = install_status {
error_text = error;
}
let column = Column::new()
.push(description_container)
.push(Space::new(Length::Units(0), Length::Units(DEFAULT_PADDING)))
.push(query_row)
.push(Space::new(Length::Units(0), Length::Units(DEFAULT_PADDING)))
.push(
Container::new(Text::new(error_text).size(DEFAULT_FONT_SIZE))
.style(style::NormalErrorBackgroundContainer(color_palette)),
)
.align_items(Align::Center);
let container = Container::new(column)
.width(Length::Fill)
.center_y()
.center_x()
.height(Length::Fill);
content = content.push(container);
}
Mode::Catalog => {
if let Some(catalog) = &self.catalog {
let default = vec![];
let addons = self.addons.get(&flavor).unwrap_or(&default);
let query = self
.catalog_search_state
.query
.as_deref()
.unwrap_or_default();
let catalog_query = TextInput::new(
&mut self.catalog_search_state.query_state,
&localized_string("search-for-addon")[..],
query,
Interaction::CatalogQuery,
)
.size(DEFAULT_FONT_SIZE)
.padding(10)
.width(Length::FillPortion(3))
.style(style::CatalogQueryInput(color_palette));
let catalog_query: Element<Interaction> = catalog_query.into();
let catalog_source = self
.config
.catalog_source
.map(CatalogSource::Choice)
.unwrap_or(CatalogSource::None);
let source_picklist = PickList::new(
&mut self.catalog_search_state.sources_state,
&self.catalog_search_state.sources,
Some(catalog_source),
Interaction::CatalogSourceSelected,
)
.text_size(14)
.width(Length::Fill)
.style(style::SecondaryPickList(color_palette));
let source_picklist: Element<Interaction> = source_picklist.into();
let source_picklist_container =
Container::new(source_picklist.map(Message::Interaction))
.center_y()
.style(style::NormalForegroundContainer(color_palette))
.height(Length::Fill)
.width(Length::FillPortion(1));
let category_picklist = PickList::new(
&mut self.catalog_search_state.categories_state,
&self.catalog_search_state.categories,
Some(self.catalog_search_state.category.clone()),
Interaction::CatalogCategorySelected,
)
.text_size(14)
.width(Length::Fill)
.style(style::SecondaryPickList(color_palette));
let category_picklist: Element<Interaction> = category_picklist.into();
let category_picklist_container =
Container::new(category_picklist.map(Message::Interaction))
.center_y()
.style(style::NormalForegroundContainer(color_palette))
.height(Length::Fill)
.width(Length::FillPortion(1));
let result_size_picklist = PickList::new(
&mut self.catalog_search_state.result_sizes_state,
&self.catalog_search_state.result_sizes,
Some(self.catalog_search_state.result_size),
Interaction::CatalogResultSizeSelected,
)
.text_size(14)
.width(Length::Fill)
.style(style::SecondaryPickList(color_palette));
let result_size_picklist: Element<Interaction> = result_size_picklist.into();
let result_size_picklist_container =
Container::new(result_size_picklist.map(Message::Interaction))
.center_y()
.style(style::NormalForegroundContainer(color_palette))
.height(Length::Fill)
.width(Length::FillPortion(1));
let catalog_query_row = Row::new()
.push(Space::new(Length::Units(DEFAULT_PADDING), Length::Units(0)))
.push(catalog_query.map(Message::Interaction))
.push(source_picklist_container)
.push(category_picklist_container)
.push(result_size_picklist_container)
.push(Space::new(
Length::Units(DEFAULT_PADDING + 5),
Length::Units(0),
))
.spacing(1);
let catalog_query_container = Container::new(catalog_query_row)
.width(Length::Fill)
.height(Length::Units(34))
.center_y();
let catalog_row_titles = element::catalog::titles_row_header(
color_palette,
catalog,
&mut self.catalog_header_state.state,
&mut self.catalog_header_state.columns,
self.catalog_header_state.previous_column_key,
self.catalog_header_state.previous_sort_direction,
);
let mut catalog_scrollable =
Scrollable::new(&mut self.catalog_search_state.scrollable_state)
.spacing(1)
.height(Length::FillPortion(1))
.style(style::Scrollable(color_palette));
let install_addons = self.install_addons.entry(flavor).or_default();
for (idx, addon) in self
.catalog_search_state
.catalog_rows
.iter_mut()
.enumerate()
{
let is_odd = if self.config.alternating_row_colors {
Some(idx % 2 != 0)
} else {
None
};
let installed_for_flavor = addons.iter().any(|a| {
(a.curse_id() == Some(addon.addon.id)
&& addon.addon.source == catalog::Source::Curse)
|| (a.tukui_id() == Some(&addon.addon.id.to_string())
&& addon.addon.source == catalog::Source::Tukui)
|| (a.wowi_id() == Some(&addon.addon.id.to_string())
&& addon.addon.source == catalog::Source::WowI)
|| (a.hub_id() == Some(addon.addon.id)
&& addon.addon.source == catalog::Source::TownlongYak)
});
let install_addon = install_addons.iter().find(|a| {
addon.addon.id.to_string() == a.id
&& matches!(a.kind, InstallKind::Catalog { .. })
});
let catalog_data_cell = element::catalog::data_row_container(
color_palette,
&self.config,
addon,
&catalog_column_config,
installed_for_flavor,
install_addon,
is_odd,
);
catalog_scrollable = catalog_scrollable.push(catalog_data_cell);
}
let bottom_space =
Space::new(Length::FillPortion(1), Length::Units(DEFAULT_PADDING));
content = content
.push(catalog_query_container)
.push(Space::new(Length::Fill, Length::Units(5)));
if self.config.catalog_source.is_none() {
let status = element::status::data_container(
color_palette,
&localized_string("select-catalog-source-title")[..],
&localized_string("select-catalog-source-description")[..],
None,
);
content = content.push(status);
} else {
content = content.push(catalog_row_titles).push(catalog_scrollable);
}
content = content.push(bottom_space)
}
}
Mode::Settings => {
let settings_container = element::settings::data_container(
color_palette,
&mut self.settings_scrollable_state,
&self.config,
&mut self.theme_state,
&mut self.scale_state,
&mut self.backup_state,
&mut self.default_backup_compression_format,
&mut self.column_settings,
&column_config,
&mut self.catalog_column_settings,
&catalog_column_config,
&mut self.open_config_dir_btn_state,
&mut self.self_update_channel_state,
&mut self.default_addon_release_channel_picklist_state,
&mut self.reset_columns_btn_state,
&mut self.localization_picklist_state,
&mut self.wow_directories,
);
content = content.push(settings_container)
}
Mode::About => {
let about_container = element::about::data_container(
color_palette,
&release_copy,
&mut self.about_scrollable_state,
&mut self.website_btn_state,
&mut self.donation_btn_state,
);
content = content.push(about_container)
}
}
let container: Option<Container<Message>> = match self.mode {
Mode::MyAddons(flavor) => {
let state = self
.state
.get(&Mode::MyAddons(flavor))
.cloned()
.unwrap_or_default();
match state {
State::Start => Some(element::status::data_container(
color_palette,
&localized_string("setup-ajour-title")[..],
&localized_string("setup-ajour-description")[..],
Some(&mut self.onboarding_directory_btn_state),
)),
State::Loading => {
let flavor = flavor.to_string().to_lowercase();
let mut vars = HashMap::new();
vars.insert("flavor".to_string(), &flavor);
let fmt = localized_string("parsing-addons");
Some(element::status::data_container(
color_palette,
&localized_string("loading")[..],
strfmt(&fmt, &vars).unwrap().as_str(),
None,
))
}
State::Ready => {
let flavor = flavor.to_string().to_lowercase();
let mut vars = HashMap::new();
vars.insert("flavor".to_string(), &flavor);
let fmt = localized_string("no-addons-for-flavor");
if !has_addons {
Some(element::status::data_container(
color_palette,
&localized_string("woops")[..],
strfmt(&fmt, &vars).unwrap().as_str(),
None,
))
} else {
None
}
}
}
}
Mode::Settings => None,
Mode::About => None,
Mode::Install => None,
Mode::MyWeakAuras(flavor) => {
let state = self
.state
.get(&Mode::MyWeakAuras(flavor))
.cloned()
.unwrap_or_default();
match state {
State::Start => Some(element::status::data_container(
color_palette,
&localized_string("setup-weakauras-title")[..],
&localized_string("setup-weakauras-description")[..],
None,
)),
State::Loading => {
let flavor = flavor.to_string().to_lowercase();
let mut vars = HashMap::new();
vars.insert("flavor".to_string(), &flavor);
let fmt = localized_string("parsing-weakauras");
Some(element::status::data_container(
color_palette,
&localized_string("loading")[..],
strfmt(&fmt, &vars).unwrap().as_str(),
None,
))
}
State::Ready => {
if !has_auras {
let flavor = flavor.to_string();
let mut vars = HashMap::new();
vars.insert("flavor".to_string(), &flavor);
let fmt = localized_string("no-known-weakauras");
Some(element::status::data_container(
color_palette,
&localized_string("woops")[..],
strfmt(&fmt, &vars).unwrap().as_str(),
None,
))
} else {
None
}
}
}
}
Mode::Catalog => {
let state = self.state.get(&Mode::Catalog).cloned().unwrap_or_default();
match state {
State::Start => None,
State::Loading => Some(element::status::data_container(
color_palette,
&localized_string("loading")[..],
&localized_string("loading-catalog")[..],
None,
)),
State::Ready => None,
}
}
};
if let Some(c) = container {
content = content.push(c);
};
Container::new(content)
.width(Length::Fill)
.height(Length::Fill)
.style(style::NormalBackgroundContainer(color_palette))
.into()
}
}
pub fn run(opts: Opts) {
let config: Config = Config::load_or_default().expect("loading config on application startup");
LANG.set(RwLock::new(config.language.language_code()))
.expect("setting LANG from config");
log::debug!("config loaded:\n{:#?}", &config);
let mut settings = Settings::default();
settings.window.size = config.window_size.unwrap_or((900, 620));
#[cfg(not(target_os = "linux"))]
{
settings.window.min_size = Some((600, 300));
}
#[cfg(feature = "wgpu")]
{
let antialiasing = opts.antialiasing.unwrap_or(true);
log::debug!("antialiasing: {}", antialiasing);
settings.antialiasing = antialiasing;
}
#[cfg(feature = "opengl")]
{
let antialiasing = opts.antialiasing.unwrap_or(false);
log::debug!("antialiasing: {}", antialiasing);
settings.antialiasing = antialiasing;
}
let image = image::load_from_memory_with_format(WINDOW_ICON, ImageFormat::Ico)
.expect("loading icon")
.to_rgba8();
let (width, height) = image.dimensions();
let icon = iced::window::Icon::from_rgba(image.into_raw(), width, height);
settings.window.icon = Some(icon.unwrap());
settings.flags = config;
Ajour::run(settings).expect("running Ajour gui");
}
pub struct InstallFromScmState {
pub query: Option<String>,
pub query_state: text_input::State,
pub install_button_state: button::State,
}
impl Default for InstallFromScmState {
fn default() -> Self {
InstallFromScmState {
query: None,
query_state: Default::default(),
install_button_state: Default::default(),
}
}
}
pub struct WowDirectoryState {
pub flavor: Flavor,
pub button_state: button::State,
}
impl Default for WowDirectoryState {
fn default() -> Self {
WowDirectoryState {
flavor: Default::default(),
button_state: Default::default(),
}
}
}
#[derive(Debug, Clone)]
pub enum ExpandType {
Details(Addon),
Changelog {
addon: Addon,
changelog: Option<Changelog>,
},
None,
}
#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq)]
pub enum ColumnKey {
Title,
LocalVersion,
RemoteVersion,
Status,
Channel,
Author,
GameVersion,
DateReleased,
Source,
Summary,
FuzzyScore,
}
impl ColumnKey {
fn title(self) -> String {
use ColumnKey::*;
match self {
Title => localized_string("addon"),
LocalVersion => localized_string("local"),
RemoteVersion => localized_string("remote"),
Status => localized_string("status"),
Channel => localized_string("channel"),
Author => localized_string("author"),
GameVersion => localized_string("game-version"),
DateReleased => localized_string("latest-release"),
Source => localized_string("source"),
Summary => localized_string("summary"),
FuzzyScore => unreachable!("fuzzy score not used as an actual column"),
}
}
fn as_string(self) -> String {
use ColumnKey::*;
let s = match self {
Title => "title",
LocalVersion => "local",
RemoteVersion => "remote",
Status => "status",
Channel => "channel",
Author => "author",
GameVersion => "game_version",
DateReleased => "date_released",
Source => "source",
Summary => "summary",
FuzzyScore => unreachable!("fuzzy score not used as an actual column"),
};
s.to_string()
}
}
impl From<&str> for ColumnKey {
fn from(s: &str) -> Self {
match s {
"title" => ColumnKey::Title,
"local" => ColumnKey::LocalVersion,
"remote" => ColumnKey::RemoteVersion,
"status" => ColumnKey::Status,
"channel" => ColumnKey::Channel,
"author" => ColumnKey::Author,
"game_version" => ColumnKey::GameVersion,
"date_released" => ColumnKey::DateReleased,
"source" => ColumnKey::Source,
"summary" => ColumnKey::Summary,
_ => panic!("Unknown ColumnKey for {}", s),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SortDirection {
Asc,
Desc,
}
impl SortDirection {
fn toggle(self) -> SortDirection {
match self {
SortDirection::Asc => SortDirection::Desc,
SortDirection::Desc => SortDirection::Asc,
}
}
}
pub struct AddonsSearchState {
pub query: Option<String>,
pub query_state: text_input::State,
}
impl Default for AddonsSearchState {
fn default() -> Self {
AddonsSearchState {
query: Default::default(),
query_state: Default::default(),
}
}
}
pub struct HeaderState {
state: header::State,
previous_column_key: Option<ColumnKey>,
previous_sort_direction: Option<SortDirection>,
columns: Vec<ColumnState>,
}
impl HeaderState {
fn column_config(&self) -> Vec<(ColumnKey, Length, bool)> {
self.columns
.iter()
.map(|c| (c.key, c.width, c.hidden))
.collect()
}
}
impl Default for HeaderState {
fn default() -> Self {
Self {
state: Default::default(),
previous_column_key: None,
previous_sort_direction: None,
columns: vec![
ColumnState {
key: ColumnKey::Title,
btn_state: Default::default(),
width: Length::Fill,
hidden: false,
order: 0,
},
ColumnState {
key: ColumnKey::LocalVersion,
btn_state: Default::default(),
width: Length::Units(150),
hidden: false,
order: 1,
},
ColumnState {
key: ColumnKey::RemoteVersion,
btn_state: Default::default(),
width: Length::Units(150),
hidden: false,
order: 2,
},
ColumnState {
key: ColumnKey::Status,
btn_state: Default::default(),
width: Length::Units(85),
hidden: false,
order: 3,
},
ColumnState {
key: ColumnKey::Channel,
btn_state: Default::default(),
width: Length::Units(85),
hidden: true,
order: 4,
},
ColumnState {
key: ColumnKey::Author,
btn_state: Default::default(),
width: Length::Units(85),
hidden: true,
order: 5,
},
ColumnState {
key: ColumnKey::GameVersion,
btn_state: Default::default(),
width: Length::Units(110),
hidden: true,
order: 6,
},
ColumnState {
key: ColumnKey::DateReleased,
btn_state: Default::default(),
width: Length::Units(110),
hidden: true,
order: 7,
},
ColumnState {
key: ColumnKey::Source,
btn_state: Default::default(),
width: Length::Units(110),
hidden: true,
order: 8,
},
ColumnState {
key: ColumnKey::Summary,
btn_state: Default::default(),
width: Length::Units(110),
hidden: true,
order: 9,
},
],
}
}
}
pub struct ColumnState {
key: ColumnKey,
btn_state: button::State,
width: Length,
hidden: bool,
order: usize,
}
impl From<&ColumnState> for ColumnConfigV2 {
fn from(column: &ColumnState) -> Self {
let width = if let Length::Units(width) = column.width {
Some(width)
} else {
None
};
ColumnConfigV2 {
key: column.key.as_string(),
width,
hidden: column.hidden,
}
}
}
pub struct ColumnSettings {
pub scrollable_state: scrollable::State,
pub columns: Vec<ColumnSettingState>,
}
impl Default for ColumnSettings {
fn default() -> Self {
ColumnSettings {
scrollable_state: Default::default(),
columns: vec![
ColumnSettingState {
key: ColumnKey::Title,
order: 0,
up_btn_state: Default::default(),
down_btn_state: Default::default(),
},
ColumnSettingState {
key: ColumnKey::LocalVersion,
order: 1,
up_btn_state: Default::default(),
down_btn_state: Default::default(),
},
ColumnSettingState {
key: ColumnKey::RemoteVersion,
order: 2,
up_btn_state: Default::default(),
down_btn_state: Default::default(),
},
ColumnSettingState {
key: ColumnKey::Status,
order: 3,
up_btn_state: Default::default(),
down_btn_state: Default::default(),
},
ColumnSettingState {
key: ColumnKey::Channel,
order: 4,
up_btn_state: Default::default(),
down_btn_state: Default::default(),
},
ColumnSettingState {
key: ColumnKey::Author,
order: 5,
up_btn_state: Default::default(),
down_btn_state: Default::default(),
},
ColumnSettingState {
key: ColumnKey::GameVersion,
order: 6,
up_btn_state: Default::default(),
down_btn_state: Default::default(),
},
ColumnSettingState {
key: ColumnKey::DateReleased,
order: 7,
up_btn_state: Default::default(),
down_btn_state: Default::default(),
},
ColumnSettingState {
key: ColumnKey::Source,
order: 8,
up_btn_state: Default::default(),
down_btn_state: Default::default(),
},
ColumnSettingState {
key: ColumnKey::Summary,
order: 9,
up_btn_state: Default::default(),
down_btn_state: Default::default(),
},
],
}
}
}
pub struct ColumnSettingState {
pub key: ColumnKey,
pub order: usize,
pub up_btn_state: button::State,
pub down_btn_state: button::State,
}
#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq)]
pub enum CatalogColumnKey {
Title,
Description,
Source,
NumDownloads,
GameVersion,
DateReleased,
Install,
Categories,
}
impl CatalogColumnKey {
fn title(self) -> String {
use CatalogColumnKey::*;
match self {
Title => localized_string("addon"),
Description => localized_string("description"),
Source => localized_string("source"),
NumDownloads => localized_string("num-downloads"),
GameVersion => localized_string("game-version"),
DateReleased => localized_string("latest-release"),
Categories => localized_string("categories"),
CatalogColumnKey::Install => localized_string("status"),
}
}
fn as_string(self) -> String {
use CatalogColumnKey::*;
let s = match self {
Title => "addon",
Description => "description",
Source => "source",
NumDownloads => "num_downloads",
GameVersion => "game_version",
DateReleased => "date_released",
Categories => "categories",
CatalogColumnKey::Install => "install",
};
s.to_string()
}
}
impl From<&str> for CatalogColumnKey {
fn from(s: &str) -> Self {
match s {
"addon" => CatalogColumnKey::Title,
"description" => CatalogColumnKey::Description,
"source" => CatalogColumnKey::Source,
"num_downloads" => CatalogColumnKey::NumDownloads,
"install" => CatalogColumnKey::Install,
"game_version" => CatalogColumnKey::GameVersion,
"date_released" => CatalogColumnKey::DateReleased,
"categories" => CatalogColumnKey::Categories,
_ => panic!("Unknown CatalogColumnKey for {}", s),
}
}
}
pub struct CatalogHeaderState {
state: header::State,
previous_column_key: Option<CatalogColumnKey>,
previous_sort_direction: Option<SortDirection>,
columns: Vec<CatalogColumnState>,
}
impl CatalogHeaderState {
fn column_config(&self) -> Vec<(CatalogColumnKey, Length, bool)> {
self.columns
.iter()
.map(|c| (c.key, c.width, c.hidden))
.collect()
}
}
impl Default for CatalogHeaderState {
fn default() -> Self {
Self {
state: Default::default(),
previous_column_key: None,
previous_sort_direction: None,
columns: vec![
CatalogColumnState {
key: CatalogColumnKey::Title,
btn_state: Default::default(),
width: Length::Fill,
hidden: false,
order: 0,
},
CatalogColumnState {
key: CatalogColumnKey::Description,
btn_state: Default::default(),
width: Length::Units(150),
hidden: false,
order: 1,
},
CatalogColumnState {
key: CatalogColumnKey::Source,
btn_state: Default::default(),
width: Length::Units(110),
hidden: true,
order: 2,
},
CatalogColumnState {
key: CatalogColumnKey::NumDownloads,
btn_state: Default::default(),
width: Length::Units(105),
hidden: true,
order: 3,
},
CatalogColumnState {
key: CatalogColumnKey::GameVersion,
btn_state: Default::default(),
width: Length::Units(105),
hidden: true,
order: 4,
},
CatalogColumnState {
key: CatalogColumnKey::DateReleased,
btn_state: Default::default(),
width: Length::Units(105),
hidden: false,
order: 5,
},
CatalogColumnState {
key: CatalogColumnKey::Install,
btn_state: Default::default(),
width: Length::Units(85),
hidden: false,
order: 6,
},
CatalogColumnState {
key: CatalogColumnKey::Categories,
btn_state: Default::default(),
width: Length::Units(85),
hidden: true,
order: 7,
},
],
}
}
}
pub struct CatalogColumnState {
key: CatalogColumnKey,
btn_state: button::State,
width: Length,
hidden: bool,
order: usize,
}
impl From<&CatalogColumnState> for ColumnConfigV2 {
fn from(column: &CatalogColumnState) -> Self {
let width = if let Length::Units(width) = column.width {
Some(width)
} else {
None
};
ColumnConfigV2 {
key: column.key.as_string(),
width,
hidden: column.hidden,
}
}
}
pub struct CatalogColumnSettings {
pub scrollable_state: scrollable::State,
pub columns: Vec<CatalogColumnSettingState>,
}
impl Default for CatalogColumnSettings {
fn default() -> Self {
CatalogColumnSettings {
scrollable_state: Default::default(),
columns: vec![
CatalogColumnSettingState {
key: CatalogColumnKey::Title,
order: 0,
up_btn_state: Default::default(),
down_btn_state: Default::default(),
},
CatalogColumnSettingState {
key: CatalogColumnKey::Description,
order: 1,
up_btn_state: Default::default(),
down_btn_state: Default::default(),
},
CatalogColumnSettingState {
key: CatalogColumnKey::Source,
order: 2,
up_btn_state: Default::default(),
down_btn_state: Default::default(),
},
CatalogColumnSettingState {
key: CatalogColumnKey::NumDownloads,
order: 3,
up_btn_state: Default::default(),
down_btn_state: Default::default(),
},
CatalogColumnSettingState {
key: CatalogColumnKey::GameVersion,
order: 4,
up_btn_state: Default::default(),
down_btn_state: Default::default(),
},
CatalogColumnSettingState {
key: CatalogColumnKey::DateReleased,
order: 5,
up_btn_state: Default::default(),
down_btn_state: Default::default(),
},
CatalogColumnSettingState {
key: CatalogColumnKey::Install,
order: 6,
up_btn_state: Default::default(),
down_btn_state: Default::default(),
},
CatalogColumnSettingState {
key: CatalogColumnKey::Categories,
order: 7,
up_btn_state: Default::default(),
down_btn_state: Default::default(),
},
],
}
}
}
pub struct CatalogColumnSettingState {
pub key: CatalogColumnKey,
pub order: usize,
pub up_btn_state: button::State,
pub down_btn_state: button::State,
}
pub struct CatalogSearchState {
pub catalog_rows: Vec<CatalogRow>,
pub scrollable_state: scrollable::State,
pub query: Option<String>,
pub query_state: text_input::State,
pub result_size: CatalogResultSize,
pub result_sizes: Vec<CatalogResultSize>,
pub result_sizes_state: pick_list::State<CatalogResultSize>,
pub category: CatalogCategory,
pub categories: Vec<CatalogCategory>,
pub categories_state: pick_list::State<CatalogCategory>,
pub sources: Vec<CatalogSource>,
pub sources_state: pick_list::State<CatalogSource>,
}
impl Default for CatalogSearchState {
fn default() -> Self {
CatalogSearchState {
catalog_rows: Default::default(),
scrollable_state: Default::default(),
query: None,
query_state: Default::default(),
result_size: Default::default(),
result_sizes: CatalogResultSize::all(),
result_sizes_state: Default::default(),
category: Default::default(),
categories: Default::default(),
categories_state: Default::default(),
sources: CatalogSource::all(),
sources_state: Default::default(),
}
}
}
pub struct CatalogRow {
install_button_state: button::State,
addon: CatalogAddon,
}
impl From<CatalogAddon> for CatalogRow {
fn from(addon: CatalogAddon) -> Self {
Self {
install_button_state: Default::default(),
addon,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct InstallAddon {
id: String,
kind: InstallKind,
status: InstallStatus,
addon: Option<Addon>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum InstallStatus {
Downloading,
Unpacking,
Retry,
Unavailable,
Error(String),
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum InstallKind {
Catalog { source: catalog::Source },
Source,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum CatalogCategory {
All,
Choice(String),
}
impl std::fmt::Display for CatalogCategory {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CatalogCategory::All => {
let localized_string = &localized_string("all-categories")[..];
write!(f, "{}", localized_string)
}
CatalogCategory::Choice(name) => write!(f, "{}", name),
}
}
}
impl Default for CatalogCategory {
fn default() -> Self {
CatalogCategory::All
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum CatalogResultSize {
_25,
_50,
_100,
_500,
}
impl Default for CatalogResultSize {
fn default() -> Self {
CatalogResultSize::_25
}
}
impl CatalogResultSize {
pub fn all() -> Vec<CatalogResultSize> {
vec![
CatalogResultSize::_25,
CatalogResultSize::_50,
CatalogResultSize::_100,
CatalogResultSize::_500,
]
}
pub fn as_usize(self) -> usize {
match self {
CatalogResultSize::_25 => 25,
CatalogResultSize::_50 => 50,
CatalogResultSize::_100 => 100,
CatalogResultSize::_500 => 500,
}
}
}
impl std::fmt::Display for CatalogResultSize {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut vars = HashMap::new();
vars.insert("number".to_string(), self.as_usize());
let fmt = localized_string("catalog-results");
write!(f, "{}", strfmt(&fmt, &vars).unwrap())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum CatalogSource {
Choice(catalog::Source),
None,
}
impl CatalogSource {
pub fn all() -> Vec<CatalogSource> {
vec![
CatalogSource::Choice(catalog::Source::Curse),
CatalogSource::Choice(catalog::Source::Tukui),
CatalogSource::Choice(catalog::Source::WowI),
CatalogSource::Choice(catalog::Source::TownlongYak),
]
}
}
impl std::fmt::Display for CatalogSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let empty_display_string = &localized_string("select-catalog-source-picklist")[..];
let s = match self {
CatalogSource::Choice(source) => match source {
catalog::Source::Curse => "Curse",
catalog::Source::Tukui => "Tukui",
catalog::Source::WowI => "WowInterface",
catalog::Source::TownlongYak => "TownlongYak",
catalog::Source::Other => panic!("Unsupported catalog source"),
},
CatalogSource::None => empty_display_string,
};
write!(f, "{}", s)
}
}
pub struct ThemeState {
themes: Vec<(String, Theme)>,
current_theme_name: String,
pick_list_state: pick_list::State<String>,
}
impl Default for ThemeState {
fn default() -> Self {
let themes = vec![
("Alliance".to_string(), Theme::alliance()),
("Ayu".to_string(), Theme::ayu()),
("Dark".to_string(), Theme::dark()),
("Dracula".to_string(), Theme::dracula()),
("Ferra".to_string(), Theme::ferra()),
("Forest Night".to_string(), Theme::forest_night()),
("Gruvbox".to_string(), Theme::gruvbox()),
("Horde".to_string(), Theme::horde()),
("Light".to_string(), Theme::light()),
("Nord".to_string(), Theme::nord()),
("One Dark".to_string(), Theme::one_dark()),
("Outrun".to_string(), Theme::outrun()),
("Solarized Dark".to_string(), Theme::solarized_dark()),
("Solarized Light".to_string(), Theme::solarized_light()),
("Sort".to_string(), Theme::sort()),
];
ThemeState {
themes,
current_theme_name: "Dark".to_string(),
pick_list_state: Default::default(),
}
}
}
pub struct ScaleState {
scale: f64,
up_btn_state: button::State,
down_btn_state: button::State,
}
impl Default for ScaleState {
fn default() -> Self {
ScaleState {
scale: 1.0,
up_btn_state: Default::default(),
down_btn_state: Default::default(),
}
}
}
#[derive(Debug, Clone)]
pub enum BackupFolderKind {
AddOns,
#[allow(clippy::upper_case_acronyms)]
WTF,
}
#[derive(Default)]
pub struct BackupState {
backing_up: bool,
last_backup: Option<NaiveDateTime>,
directory_btn_state: button::State,
backup_now_btn_state: button::State,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum DownloadReason {
Update,
Install,
}
#[derive(Debug, Clone, Copy)]
pub enum SelfUpdateStatus {
InProgress,
Failed,
}
impl std::fmt::Display for SelfUpdateStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
SelfUpdateStatus::InProgress => localized_string("updating"),
SelfUpdateStatus::Failed => localized_string("failed"),
};
write!(f, "{}", s)
}
}
#[derive(Default, Debug)]
pub struct SelfUpdateState {
latest_release: Option<utility::Release>,
status: Option<SelfUpdateStatus>,
btn_state: button::State,
}
#[derive(Debug)]
pub struct SelfUpdateChannelState {
picklist: pick_list::State<SelfUpdateChannel>,
options: [SelfUpdateChannel; 2],
}
#[derive(Debug, Default)]
pub struct WeakAurasState {
chosen_account: Option<String>,
account_picklist: pick_list::State<String>,
accounts: Vec<String>,
auras: Vec<Aura>,
is_updating: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq)]
pub enum AuraColumnKey {
Title,
LocalVersion,
RemoteVersion,
Author,
Type,
Status,
}
impl AuraColumnKey {
fn title(self) -> String {
use AuraColumnKey::*;
match self {
Title => localized_string("aura"),
LocalVersion => localized_string("local"),
RemoteVersion => localized_string("remote"),
Author => localized_string("author"),
Type => localized_string("type"),
Status => localized_string("status"),
}
}
fn as_string(self) -> String {
use AuraColumnKey::*;
let s = match self {
Title => "title",
LocalVersion => "local",
RemoteVersion => "remote",
Author => "author",
Type => "type",
Status => "status",
};
s.to_string()
}
}
impl From<&str> for AuraColumnKey {
fn from(s: &str) -> Self {
match s {
"title" => AuraColumnKey::Title,
"local" => AuraColumnKey::LocalVersion,
"remote" => AuraColumnKey::RemoteVersion,
"author" => AuraColumnKey::Author,
"type" => AuraColumnKey::Type,
"status" => AuraColumnKey::Status,
_ => panic!("Unknown AuraColumnKey for {}", s),
}
}
}
pub struct AuraHeaderState {
state: header::State,
previous_column_key: Option<AuraColumnKey>,
previous_sort_direction: Option<SortDirection>,
columns: Vec<AuraColumnState>,
}
impl AuraHeaderState {
fn column_config(&self) -> Vec<(AuraColumnKey, Length, bool)> {
self.columns
.iter()
.map(|c| (c.key, c.width, c.hidden))
.collect()
}
}
impl Default for AuraHeaderState {
fn default() -> Self {
Self {
state: Default::default(),
previous_column_key: None,
previous_sort_direction: None,
columns: vec![
AuraColumnState {
key: AuraColumnKey::Title,
btn_state: Default::default(),
width: Length::Fill,
hidden: false,
},
AuraColumnState {
key: AuraColumnKey::LocalVersion,
btn_state: Default::default(),
width: Length::Units(120),
hidden: false,
},
AuraColumnState {
key: AuraColumnKey::RemoteVersion,
btn_state: Default::default(),
width: Length::Units(120),
hidden: false,
},
AuraColumnState {
key: AuraColumnKey::Author,
btn_state: Default::default(),
width: Length::Units(85),
hidden: false,
},
AuraColumnState {
key: AuraColumnKey::Type,
btn_state: Default::default(),
width: Length::Units(85),
hidden: false,
},
AuraColumnState {
key: AuraColumnKey::Status,
btn_state: Default::default(),
width: Length::Units(110),
hidden: false,
},
],
}
}
}
pub struct AuraColumnState {
key: AuraColumnKey,
btn_state: button::State,
width: Length,
hidden: bool,
}
impl From<&AuraColumnState> for ColumnConfigV2 {
fn from(column: &AuraColumnState) -> Self {
let width = if let Length::Units(width) = column.width {
Some(width)
} else {
None
};
ColumnConfigV2 {
key: column.key.as_string(),
width,
hidden: column.hidden,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord)]
pub struct AuraStatus(pub ajour_weak_auras::AuraStatus);
impl std::fmt::Display for AuraStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use ajour_weak_auras::AuraStatus::*;
let s = if let Some(key) = match self.0 {
Idle => None,
UpdateAvailable => Some("weakaura-update-available"),
UpdateQueued => Some("completed"),
} {
localized_string(key)
} else {
"".to_string()
};
write!(f, "{}", s)
}
}
async fn load_caches() -> Result<(FingerprintCache, AddonCache)> {
let fingerprint_cache = load_fingerprint_cache().await?;
let addon_cache = load_addon_cache().await?;
Ok((fingerprint_cache, addon_cache))
}
fn apply_config(ajour: &mut Ajour, config: Config) {
match &config.column_config {
ColumnConfig::V1 {
local_version_width,
remote_version_width,
status_width,
} => {
ajour
.header_state
.columns
.get_mut(1)
.as_mut()
.unwrap()
.width = Length::Units(*local_version_width);
ajour
.header_state
.columns
.get_mut(2)
.as_mut()
.unwrap()
.width = Length::Units(*remote_version_width);
ajour
.header_state
.columns
.get_mut(3)
.as_mut()
.unwrap()
.width = Length::Units(*status_width);
}
ColumnConfig::V2 { columns } => {
ajour.header_state.columns.iter_mut().for_each(|a| {
if let Some((idx, column)) = columns
.iter()
.enumerate()
.filter_map(|(idx, column)| {
if column.key == a.key.as_string() {
Some((idx, column))
} else {
None
}
})
.next()
{
a.width = column.width.map_or(Length::Fill, Length::Units);
a.hidden = column.hidden;
a.order = idx;
}
});
ajour.column_settings.columns.iter_mut().for_each(|a| {
if let Some(idx) = columns
.iter()
.enumerate()
.filter_map(|(idx, column)| {
if column.key == a.key.as_string() {
Some(idx)
} else {
None
}
})
.next()
{
a.order = idx;
}
});
ajour.header_state.columns.sort_by_key(|c| c.order);
ajour.column_settings.columns.sort_by_key(|c| c.order);
}
ColumnConfig::V3 {
my_addons_columns,
catalog_columns,
aura_columns,
} => {
ajour.header_state.columns.iter_mut().for_each(|a| {
if let Some((idx, column)) = my_addons_columns
.iter()
.enumerate()
.filter_map(|(idx, column)| {
if column.key == a.key.as_string() {
Some((idx, column))
} else {
None
}
})
.next()
{
a.width = if a.key == ColumnKey::Title {
Length::Fill
} else {
column.width.map_or(Length::Fill, Length::Units)
};
a.hidden = column.hidden;
a.order = idx;
}
});
ajour.column_settings.columns.iter_mut().for_each(|a| {
if let Some(idx) = my_addons_columns
.iter()
.enumerate()
.filter_map(|(idx, column)| {
if column.key == a.key.as_string() {
Some(idx)
} else {
None
}
})
.next()
{
a.order = idx;
}
});
ajour
.catalog_column_settings
.columns
.iter_mut()
.for_each(|a| {
if let Some(idx) = catalog_columns
.iter()
.enumerate()
.filter_map(|(idx, column)| {
if column.key == a.key.as_string() {
Some(idx)
} else {
None
}
})
.next()
{
a.order = idx;
}
});
ajour.catalog_header_state.columns.iter_mut().for_each(|a| {
if let Some((idx, column)) = catalog_columns
.iter()
.enumerate()
.filter_map(|(idx, column)| {
if column.key == a.key.as_string() {
Some((idx, column))
} else {
None
}
})
.next()
{
a.width = if a.key == CatalogColumnKey::Title {
Length::Fill
} else {
column.width.map_or(Length::Fill, Length::Units)
};
a.hidden = column.hidden;
a.order = idx;
}
});
ajour.aura_header_state.columns.iter_mut().for_each(|a| {
if let Some((_idx, column)) = aura_columns
.iter()
.enumerate()
.filter_map(|(idx, column)| {
if column.key == a.key.as_string() {
Some((idx, column))
} else {
None
}
})
.next()
{
a.width = if a.key == AuraColumnKey::Title {
Length::Fill
} else {
column.width.map_or(Length::Fill, Length::Units)
};
}
});
ajour.header_state.columns.sort_by_key(|c| c.order);
ajour.column_settings.columns.sort_by_key(|c| c.order);
ajour.catalog_header_state.columns.sort_by_key(|c| c.order);
ajour
.catalog_column_settings
.columns
.sort_by_key(|c| c.order);
}
}
ajour.theme_state.current_theme_name = config.theme.as_deref().unwrap_or("Dark").to_string();
ajour.scale_state.scale = config.scale.unwrap_or(1.0);
ajour.mode = Mode::MyAddons(config.wow.flavor);
ajour.config = config;
if ajour.config.wow.directory.is_some() {
for flavor in Flavor::ALL.iter() {
let path = ajour.config.wow.directory.as_ref().unwrap();
let flavor_path = ajour.config.get_flavor_directory_for_flavor(flavor, path);
if flavor_path.exists() {
ajour.config.wow.directories.insert(*flavor, flavor_path);
}
}
ajour.config.wow.directory = None;
}
let _ = &ajour.config.save();
}