diff --git a/lang/en-US.ftl b/lang/en-US.ftl index bb84f784..7bd7ae4f 100644 --- a/lang/en-US.ftl +++ b/lang/en-US.ftl @@ -144,6 +144,8 @@ label-custom = Custom label-none = None label-change-count = Changes: {$total} label-unscanned = Unscanned +# This refers to a local file on the computer +label-file = File store-ea = EA store-epic = Epic diff --git a/src/gui/app.rs b/src/gui/app.rs index e9986c62..092a1663 100644 --- a/src/gui/app.rs +++ b/src/gui/app.rs @@ -1331,11 +1331,11 @@ impl Application for App { match action { EditAction::Add => { self.text_histories.secondary_manifests.push(Default::default()); - self.config.manifest.secondary.push("".to_string()); + self.config.manifest.secondary.push(Default::default()); } EditAction::Change(index, value) => { self.text_histories.secondary_manifests[index].push(&value); - self.config.manifest.secondary[index] = value; + self.config.manifest.secondary[index].set(value); } EditAction::Remove(index) => { self.text_histories.secondary_manifests.remove(index); @@ -1360,6 +1360,11 @@ impl Application for App { self.config.save(); Command::none() } + Message::SelectedSecondaryManifestKind(index, kind) => { + self.config.manifest.secondary[index].convert(kind); + self.config.save(); + Command::none() + } Message::EditedRedirect(action, field) => { match action { EditAction::Add => { @@ -1826,6 +1831,10 @@ impl Application for App { self.text_histories.rclone_executable.push(&path.raw()); self.config.apps.rclone.path = path; } + BrowseFileSubject::SecondaryManifest(i) => { + self.text_histories.secondary_manifests[i].push(&path.raw()); + self.config.manifest.secondary[i].set(path.raw()); + } } self.config.save(); Command::none() @@ -1905,6 +1914,12 @@ impl Application for App { Message::OpenFileSubject(subject) => { let path = match subject { BrowseFileSubject::RcloneExecutable => self.config.apps.rclone.path.clone(), + BrowseFileSubject::SecondaryManifest(i) => { + let Some(path) = self.config.manifest.secondary[i].path() else { + return Command::none(); + }; + path.clone() + } }; match path.parent_if_file() { @@ -1957,10 +1972,17 @@ impl Application for App { ), UndoSubject::Root(i) => shortcut .apply_to_strict_path_field(&mut self.config.roots[i].path, &mut self.text_histories.roots[i]), - UndoSubject::SecondaryManifest(i) => shortcut.apply_to_string_field( - &mut self.config.manifest.secondary[i], - &mut self.text_histories.secondary_manifests[i], - ), + UndoSubject::SecondaryManifest(i) => { + let history = &mut self.text_histories.secondary_manifests[i]; + match shortcut { + Shortcut::Undo => { + self.config.manifest.secondary[i].set(history.undo()); + } + Shortcut::Redo => { + self.config.manifest.secondary[i].set(history.redo()); + } + } + } UndoSubject::RedirectSource(i) => shortcut.apply_to_strict_path_field( &mut self.config.redirects[i].source, &mut self.text_histories.redirects[i].source, diff --git a/src/gui/common.rs b/src/gui/common.rs index 404f68ed..832e07a8 100644 --- a/src/gui/common.rs +++ b/src/gui/common.rs @@ -11,7 +11,9 @@ use crate::{ lang::{Language, TRANSLATOR}, prelude::{CommandError, Error, Finality, Privacy, StrictPath, SyncDirection}, resource::{ - config::{BackupFormat, RedirectKind, RootsConfig, SortKey, Theme, ZipCompression}, + config::{ + BackupFormat, RedirectKind, RootsConfig, SecondaryManifestConfigKind, SortKey, Theme, ZipCompression, + }, manifest::{Manifest, ManifestUpdate, Store}, }, scan::{ @@ -118,6 +120,7 @@ pub enum Message { EditedSecondaryManifest(EditAction), SelectedRootStore(usize, Store), SelectedRedirectKind(usize, RedirectKind), + SelectedSecondaryManifestKind(usize, SecondaryManifestConfigKind), EditedRedirect(EditAction, Option), EditedCustomGame(EditAction), EditedCustomGameFile(usize, EditAction), @@ -607,6 +610,7 @@ pub enum BrowseSubject { #[derive(Debug, Clone, PartialEq, Eq)] pub enum BrowseFileSubject { RcloneExecutable, + SecondaryManifest(usize), } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] diff --git a/src/gui/editor.rs b/src/gui/editor.rs index ae548237..7ea24d60 100644 --- a/src/gui/editor.rs +++ b/src/gui/editor.rs @@ -7,7 +7,7 @@ use iced::{ use crate::{ gui::{ button, - common::{BackupPhase, BrowseSubject, Message, ScrollSubject, UndoSubject}, + common::{BackupPhase, BrowseFileSubject, BrowseSubject, Message, ScrollSubject, UndoSubject}, shortcuts::TextHistories, style, widget::{checkbox, pick_list, text, Column, Container, IcedParentExt, Row, Tooltip}, @@ -15,7 +15,7 @@ use crate::{ lang::TRANSLATOR, resource::{ cache::Cache, - config::{Config, RedirectKind}, + config::{Config, RedirectKind, SecondaryManifestConfigKind}, manifest::Store, }, }; @@ -52,12 +52,17 @@ pub fn root<'a>(config: &Config, histories: &TextHistories, modifiers: &keyboard Container::new(content) } -pub fn manifest<'a>(config: &Config, cache: &'a Cache, histories: &TextHistories) -> Container<'a> { +pub fn manifest<'a>( + config: &Config, + cache: &'a Cache, + histories: &TextHistories, + modifiers: &keyboard::Modifiers, +) -> Container<'a> { let label_width = Length::Fixed(160.0); - let left_offset = Length::Fixed(70.0); let right_offset = Length::Fixed(70.0); - let get_checked = |url: &str, cache: &'a Cache| { + let get_checked = |url: Option<&str>, cache: &'a Cache| { + let url = url?; let cached = cache.manifests.get(url)?; let checked = match cached.checked { Some(x) => chrono::DateTime::::from(x) @@ -68,7 +73,8 @@ pub fn manifest<'a>(config: &Config, cache: &'a Cache, histories: &TextHistories Some(Container::new(text(checked)).width(label_width)) }; - let get_updated = |url: &str, cache: &'a Cache| { + let get_updated = |url: Option<&str>, cache: &'a Cache| { + let url = url?; let cached = cache.manifests.get(url)?; let updated = match cached.updated { Some(x) => chrono::DateTime::::from(x) @@ -86,11 +92,7 @@ pub fn manifest<'a>(config: &Config, cache: &'a Cache, histories: &TextHistories Row::new() .spacing(20) .align_items(Alignment::Center) - .push_if( - || !config.manifest.secondary.is_empty(), - || Space::with_width(left_offset), - ) - .push(text(TRANSLATOR.url_label()).width(Length::Fill)) + .push(Space::with_width(Length::Fill)) .push(Container::new(text(TRANSLATOR.checked_label())).width(label_width)) .push(Container::new(text(TRANSLATOR.updated_label())).width(label_width)) .push_if( @@ -102,13 +104,9 @@ pub fn manifest<'a>(config: &Config, cache: &'a Cache, histories: &TextHistories Row::new() .spacing(20) .align_items(Alignment::Center) - .push_if( - || !config.manifest.secondary.is_empty(), - || Space::with_width(left_offset), - ) .push(iced::widget::TextInput::new("", &config.manifest.url).width(Length::Fill)) - .push_some(|| get_checked(&config.manifest.url, cache)) - .push_some(|| get_updated(&config.manifest.url, cache)) + .push_some(|| get_checked(Some(&config.manifest.url), cache)) + .push_some(|| get_updated(Some(&config.manifest.url), cache)) .push_if( || !config.manifest.secondary.is_empty(), || Space::with_width(right_offset), @@ -131,9 +129,24 @@ pub fn manifest<'a>(config: &Config, cache: &'a Cache, histories: &TextHistories i, config.manifest.secondary.len(), )) + .push( + pick_list( + SecondaryManifestConfigKind::ALL, + Some(config.manifest.secondary[i].kind()), + move |v| Message::SelectedSecondaryManifestKind(i, v), + ) + .style(style::PickList::Primary) + .width(75), + ) .push(histories.input(UndoSubject::SecondaryManifest(i))) - .push_some(|| get_checked(&config.manifest.secondary[i], cache)) - .push_some(|| get_updated(&config.manifest.secondary[i], cache)) + .push_some(|| get_checked(config.manifest.secondary[i].url(), cache)) + .push_some(|| get_updated(config.manifest.secondary[i].url(), cache)) + .push_some(|| match config.manifest.secondary[i].kind() { + SecondaryManifestConfigKind::Local => { + Some(button::choose_file(BrowseFileSubject::SecondaryManifest(i), modifiers)) + } + SecondaryManifestConfigKind::Remote => None, + }) .push(button::remove(Message::EditedSecondaryManifest, i)), ) }); diff --git a/src/gui/modal.rs b/src/gui/modal.rs index 3162b23e..aaf0bd01 100644 --- a/src/gui/modal.rs +++ b/src/gui/modal.rs @@ -48,7 +48,7 @@ pub enum ModalField { impl ModalField { pub fn view<'a>(kind: ModalInputKind, histories: &TextHistories) -> Row<'a> { let label = match kind { - ModalInputKind::Url => TRANSLATOR.url_label(), + ModalInputKind::Url => TRANSLATOR.url_field(), ModalInputKind::Host => TRANSLATOR.host_label(), ModalInputKind::Port => TRANSLATOR.port_label(), ModalInputKind::Username => TRANSLATOR.username_label(), diff --git a/src/gui/screen.rs b/src/gui/screen.rs index c3128d49..2ae8a054 100644 --- a/src/gui/screen.rs +++ b/src/gui/screen.rs @@ -428,7 +428,7 @@ pub fn other<'a>( .push(text(TRANSLATOR.manifest_label()).width(100)) .push(button::refresh(Message::UpdateManifest, updating_manifest)), ) - .push(editor::manifest(config, cache, histories).padding([10, 0, 0, 0])), + .push(editor::manifest(config, cache, histories, modifiers).padding([10, 0, 0, 0])), ) .push( Column::new() diff --git a/src/gui/shortcuts.rs b/src/gui/shortcuts.rs index ce54c436..0e070991 100644 --- a/src/gui/shortcuts.rs +++ b/src/gui/shortcuts.rs @@ -227,7 +227,7 @@ impl TextHistories { } for x in &config.manifest.secondary { - histories.secondary_manifests.push(TextHistory::raw(x)); + histories.secondary_manifests.push(TextHistory::raw(&x.value())); } for x in &config.redirects { diff --git a/src/lang.rs b/src/lang.rs index 900d5b5a..0bd750a9 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -1034,7 +1034,15 @@ impl Translator { translate("label-arguments") } + pub fn file_label(&self) -> String { + translate("label-file") + } + pub fn url_label(&self) -> String { + translate("label-url") + } + + pub fn url_field(&self) -> String { self.field(&translate("label-url")) } diff --git a/src/resource.rs b/src/resource.rs index cc2da679..b926f71f 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -38,6 +38,11 @@ where Self::load_from_string(&content) } + fn load_from_existing(path: &std::path::PathBuf) -> Result { + let content = Self::load_raw(path)?; + Self::load_from_string(&content) + } + fn load_raw(path: &std::path::PathBuf) -> Result { Ok(std::fs::read_to_string(path)?) } diff --git a/src/resource/config.rs b/src/resource/config.rs index 312412ae..1c78212a 100644 --- a/src/resource/config.rs +++ b/src/resource/config.rs @@ -57,7 +57,120 @@ pub struct Runtime { pub struct ManifestConfig { pub url: String, #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub secondary: Vec, + pub secondary: Vec, +} + +impl ManifestConfig { + pub fn secondary_manifest_urls(&self) -> Vec<&str> { + self.secondary + .iter() + .filter_map(|x| match x { + SecondaryManifestConfig::Local { .. } => None, + SecondaryManifestConfig::Remote { url } => Some(url.as_str()), + }) + .collect() + } + + pub fn load_secondary_manifests(&self) -> Vec<(StrictPath, Manifest)> { + self.secondary + .iter() + .filter_map(|x| match x { + SecondaryManifestConfig::Local { path } => { + let manifest = Manifest::load_from_existing(&path.as_std_path_buf()).ok()?; + Some((path.clone(), manifest)) + } + SecondaryManifestConfig::Remote { url } => { + let path = Manifest::path_for(url, false); + let manifest = Manifest::load_from_existing(&path.as_std_path_buf()).ok()?; + Some((path.clone(), manifest)) + } + }) + .collect() + } +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum SecondaryManifestConfigKind { + Local, + #[default] + Remote, +} + +impl SecondaryManifestConfigKind { + pub const ALL: &'static [Self] = &[Self::Local, Self::Remote]; +} + +impl ToString for SecondaryManifestConfigKind { + fn to_string(&self) -> String { + match self { + Self::Local => TRANSLATOR.file_label(), + Self::Remote => TRANSLATOR.url_label(), + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[serde(untagged, rename_all = "camelCase")] +pub enum SecondaryManifestConfig { + Local { path: StrictPath }, + Remote { url: String }, +} + +impl SecondaryManifestConfig { + pub fn url(&self) -> Option<&str> { + match self { + Self::Local { .. } => None, + Self::Remote { url } => Some(url.as_str()), + } + } + + pub fn path(&self) -> Option<&StrictPath> { + match self { + Self::Local { path } => Some(path), + Self::Remote { .. } => None, + } + } + + pub fn value(&self) -> String { + match self { + Self::Local { path } => path.raw(), + Self::Remote { url } => url.to_string(), + } + } + + pub fn set(&mut self, value: String) { + match self { + Self::Local { path } => *path = StrictPath::new(value), + Self::Remote { url } => *url = value, + } + } + + pub fn kind(&self) -> SecondaryManifestConfigKind { + match self { + Self::Local { .. } => SecondaryManifestConfigKind::Local, + Self::Remote { .. } => SecondaryManifestConfigKind::Remote, + } + } + + pub fn convert(&mut self, kind: SecondaryManifestConfigKind) { + match (&self, kind) { + (Self::Local { path }, SecondaryManifestConfigKind::Remote) => { + *self = Self::Remote { url: path.raw() }; + } + (Self::Remote { url }, SecondaryManifestConfigKind::Local) => { + *self = Self::Local { + path: StrictPath::new(url.clone()), + }; + } + _ => {} + } + } +} + +impl Default for SecondaryManifestConfig { + fn default() -> Self { + Self::Remote { url: "".to_string() } + } } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] @@ -751,7 +864,7 @@ impl ResourceFile for Config { fn migrate(mut self) -> Self { self.roots.retain(|x| !x.path.raw().trim().is_empty()); - self.manifest.secondary.retain(|x| !x.trim().is_empty()); + self.manifest.secondary.retain(|x| !x.value().trim().is_empty()); self.redirects .retain(|x| !x.source.raw().trim().is_empty() && !x.target.raw().trim().is_empty()); self.backup.filter.ignored_paths.retain(|x| !x.raw().trim().is_empty()); @@ -1424,7 +1537,7 @@ mod tests { url: example.com etag: "foo" secondary: - - example.com/2 + - url: example.com/2 roots: - path: ~/steam store: steam @@ -1482,7 +1595,9 @@ mod tests { runtime: Default::default(), manifest: ManifestConfig { url: s("example.com"), - secondary: vec![s("example.com/2")] + secondary: vec![SecondaryManifestConfig::Remote { + url: s("example.com/2") + }] }, language: Language::English, theme: Theme::Light, diff --git a/src/resource/manifest.rs b/src/resource/manifest.rs index ba2de926..7a899245 100644 --- a/src/resource/manifest.rs +++ b/src/resource/manifest.rs @@ -288,7 +288,7 @@ impl Manifest { } } - fn path_for(url: &str, primary: bool) -> StrictPath { + pub fn path_for(url: &str, primary: bool) -> StrictPath { if primary { Self::path().into() } else { @@ -329,7 +329,7 @@ impl Manifest { out.push(Self::update_one(&config.url, &cache, force, true)); - for secondary in &config.secondary { + for secondary in config.secondary_manifest_urls() { out.push(Self::update_one(secondary, &cache, force, false)); } @@ -439,13 +439,8 @@ impl Manifest { } pub fn incorporate_extensions(&mut self, config: &Config) { - for url in &config.manifest.secondary { - let path = Self::path_for(url, false); - if let Ok(secondary) = Manifest::load_from(&path.as_std_path_buf()) { - self.incorporate_secondary_manifest(path, secondary); - } else { - log::warn!("Configured secondary manifest is invalid: {}", path.render()); - } + for (path, secondary) in config.manifest.load_secondary_manifests() { + self.incorporate_secondary_manifest(path, secondary); } for root in &config.roots {