vrc_get_vpm/environment/
settings.rs

1use std::collections::HashSet;
2use std::fmt::Write;
3use std::path::{Path, PathBuf};
4
5use indexmap::IndexMap;
6use url::Url;
7
8use crate::environment::vpm_settings::VpmSettings;
9use crate::environment::vrc_get_settings::VrcGetSettings;
10use crate::environment::{AddUserPackageResult, PackageCollection};
11use crate::io::DefaultEnvironmentIo;
12use crate::package_manifest::LooseManifest;
13use crate::repository::RemoteRepository;
14use crate::utils::{normalize_path, try_load_json};
15use crate::{UserRepoSetting, io};
16
17#[derive(Debug, Clone)]
18pub struct Settings {
19    /// parsed settings
20    vpm: VpmSettings,
21    vrc_get: VrcGetSettings,
22}
23
24impl Settings {
25    pub async fn load(io: &DefaultEnvironmentIo) -> io::Result<Self> {
26        let settings = VpmSettings::load(io).await?;
27        let vrc_get_settings = VrcGetSettings::load(io).await?;
28
29        Ok(Self {
30            vpm: settings,
31            vrc_get: vrc_get_settings,
32        })
33    }
34
35    pub async fn save(&self, io: &DefaultEnvironmentIo) -> io::Result<()> {
36        self.vpm.save(io).await?;
37
38        Ok(())
39    }
40}
41
42/// VCC Settings / Stores
43impl Settings {
44    pub fn show_prerelease_packages(&self) -> bool {
45        self.vpm.show_prerelease_packages()
46    }
47
48    pub fn set_show_prerelease_packages(&mut self, value: bool) {
49        self.vpm.set_show_prerelease_packages(value);
50    }
51
52    pub fn default_project_path(&self) -> Option<&str> {
53        self.vpm.default_project_path()
54    }
55
56    pub fn set_default_project_path(&mut self, value: &str) {
57        self.vpm.set_default_project_path(value);
58    }
59
60    pub fn project_backup_path(&self) -> Option<&str> {
61        self.vpm.project_backup_path()
62    }
63
64    pub fn set_project_backup_path(&mut self, value: &str) {
65        self.vpm.set_project_backup_path(value);
66    }
67
68    pub fn unity_hub_path(&self) -> &str {
69        self.vpm.unity_hub()
70    }
71
72    pub fn set_unity_hub_path(&mut self, value: &str) {
73        self.vpm.set_unity_hub(value);
74    }
75}
76
77#[cfg(feature = "experimental-project-management")]
78impl Settings {
79    pub fn user_projects(&self) -> Option<&[Box<str>]> {
80        self.vpm.user_projects()
81    }
82
83    pub fn retain_user_projects(&mut self, f: impl FnMut(&str) -> bool) -> Option<Vec<Box<str>>> {
84        self.vpm.retain_user_projects(f)
85    }
86
87    pub fn add_user_project(&mut self, path: &str) {
88        self.vpm.add_user_project(path);
89    }
90
91    pub fn remove_user_project(&mut self, path: &str) {
92        self.vpm.remove_user_project(path);
93    }
94
95    pub fn load_from_db(&mut self, connection: &super::VccDatabaseConnection) -> io::Result<()> {
96        let projects = connection.get_projects();
97        let mut project_paths = projects
98            .iter()
99            .filter_map(|x| x.path())
100            .collect::<HashSet<_>>();
101
102        // remove removed projects
103        self.vpm
104            .retain_user_projects(|x| project_paths.contains(&x));
105
106        // add new projects
107        // Add all projects if userProjects key is absent.
108        if let Some(user_projects) = self.vpm.user_projects() {
109            for x in user_projects {
110                project_paths.remove(x.as_ref());
111            }
112        }
113
114        for x in project_paths {
115            self.vpm.add_user_project(x);
116        }
117
118        Ok(())
119    }
120}
121
122/// VPM Settings (vrc-get extensions)
123impl Settings {
124    pub fn ignore_curated_repository(&self) -> bool {
125        self.vrc_get.ignore_curated_repository()
126    }
127
128    pub fn ignore_official_repository(&self) -> bool {
129        self.vrc_get.ignore_official_repository()
130    }
131}
132
133/// User Package Managements
134impl Settings {
135    pub fn user_package_folders(&self) -> &[PathBuf] {
136        self.vpm.user_package_folders()
137    }
138
139    pub fn remove_user_package(&mut self, pkg_path: &Path) {
140        self.vpm.remove_user_package_folder(pkg_path);
141    }
142
143    pub async fn add_user_package(
144        &mut self,
145        pkg_path: &Path,
146        io: &DefaultEnvironmentIo,
147    ) -> AddUserPackageResult {
148        if !pkg_path.is_absolute() {
149            return AddUserPackageResult::NonAbsolute;
150        }
151
152        for x in self.vpm.user_package_folders() {
153            if x == pkg_path {
154                return AddUserPackageResult::AlreadyAdded;
155            }
156        }
157
158        match try_load_json::<LooseManifest>(io, &pkg_path.join("package.json")).await {
159            Ok(Some(LooseManifest(package_json))) => package_json,
160            _ => {
161                return AddUserPackageResult::BadPackage;
162            }
163        };
164
165        self.vpm.add_user_package_folder(pkg_path.to_owned());
166
167        AddUserPackageResult::Success
168    }
169}
170
171/// Repository Managements
172impl Settings {
173    pub fn get_user_repos(&self) -> &[UserRepoSetting] {
174        self.vpm.user_repos()
175    }
176
177    pub fn can_add_remote_repo(&self, url: &Url, remote_repo: &RemoteRepository) -> bool {
178        let user_repos = self.get_user_repos();
179        if user_repos.iter().any(|x| x.url() == Some(url)) {
180            return false;
181        }
182        // should we check more urls?
183        if !self.ignore_curated_repository()
184            && url.as_str() == "https://packages.vrchat.com/curated?download"
185        {
186            return false;
187        }
188        if !self.ignore_official_repository()
189            && url.as_str() == "https://packages.vrchat.com/official?download"
190        {
191            return false;
192        }
193
194        if let Some(repo_id) = remote_repo.id() {
195            // if there is id, check if there is already repo with same id
196            if user_repos.iter().any(|x| x.id() == Some(repo_id)) {
197                return false;
198            }
199            if repo_id == "com.vrchat.repos.official" && !self.vrc_get.ignore_official_repository()
200            {
201                return false;
202            }
203            if repo_id == "com.vrchat.repos.curated" && !self.vrc_get.ignore_curated_repository() {
204                return false;
205            }
206        }
207
208        true
209    }
210
211    pub fn add_remote_repo(
212        &mut self,
213        url: &Url,
214        name: Option<&str>,
215        headers: IndexMap<Box<str>, Box<str>>,
216        remote_repo: &RemoteRepository,
217        path_buf: &Path,
218    ) -> bool {
219        if !self.can_add_remote_repo(url, remote_repo) {
220            return false;
221        }
222
223        let repo_name = name.or(remote_repo.name()).map(Into::into);
224        let repo_id = remote_repo.id().map(Into::into);
225
226        let mut repo_setting = UserRepoSetting::new(
227            path_buf.to_path_buf().into_boxed_path(),
228            repo_name,
229            Some(url.clone()),
230            repo_id,
231        );
232        repo_setting.headers = headers;
233
234        self.vpm.add_user_repo(repo_setting);
235        true
236    }
237
238    pub fn add_local_repo(&mut self, path: &Path, name: Option<&str>) -> bool {
239        let path = normalize_path(path);
240
241        if self.get_user_repos().iter().any(|x| x.local_path() == path) {
242            return false;
243        }
244
245        self.vpm.add_user_repo(UserRepoSetting::new(
246            path.into(),
247            name.map(Into::into),
248            None,
249            None,
250        ));
251        true
252    }
253
254    pub fn remove_repo(
255        &mut self,
256        condition: impl Fn(&UserRepoSetting) -> bool,
257    ) -> Vec<UserRepoSetting> {
258        self.vpm.retain_user_repos(|x| !condition(x))
259    }
260
261    // auto configurations
262
263    /// Removes id-duplicated repositories
264    ///
265    /// If there are multiple repositories with the same id,
266    /// this function will remove all but the first one.
267    pub fn remove_id_duplication(&mut self) -> Vec<UserRepoSetting> {
268        let user_repos = self.get_user_repos();
269        if user_repos.is_empty() {
270            return vec![];
271        }
272
273        let mut used_ids = HashSet::new();
274
275        // retain operates in place, visiting each element exactly once in the original order.
276        // s
277        self.vpm.retain_user_repos(|repo| {
278            let mut to_add = true;
279            if let Some(id) = repo.id() {
280                to_add = used_ids.insert(id.to_owned());
281            }
282            if to_add {
283                // this means new id
284                true
285            } else {
286                false
287            }
288        })
289    }
290
291    pub fn update_id(&mut self, loaded: &PackageCollection) -> bool {
292        self.vpm.update_id(loaded)
293    }
294
295    pub fn export_repositories(&self) -> String {
296        let mut builder = String::new();
297
298        for setting in self.get_user_repos() {
299            let Some(url) = setting.url() else { continue };
300            if setting.headers().is_empty() {
301                writeln!(builder, "{url}").unwrap();
302            } else {
303                let mut add_url = Url::parse("vcc://vpm/addRepo").unwrap();
304                let mut query_builder = add_url.query_pairs_mut();
305                query_builder.clear();
306                query_builder.append_pair("url", url.as_str());
307
308                for (header_name, value) in setting.headers() {
309                    query_builder.append_pair("headers[]", &format!("{header_name}:{value}"));
310                }
311                drop(query_builder);
312
313                writeln!(builder, "{add_url}").unwrap();
314            }
315        }
316
317        builder
318    }
319}