aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGeckoEidechse <gecko.eidechse+git@pm.me>2023-01-29 15:19:18 +0100
committerGeckoEidechse <gecko.eidechse+git@pm.me>2023-01-29 15:19:18 +0100
commit672201785222ce0905cda8b228b1390899c1d1e8 (patch)
treec3d94ec262697b8024d89f39c1209791c2c72476
parentb66410e8c19e89acd39628350c4fe70e0e85ff32 (diff)
parent31b32c725ce4f47a6726cb0d8ef5f21ec6030580 (diff)
downloadFlightCore-fix/add-empty-dist-folder.tar.gz
FlightCore-fix/add-empty-dist-folder.zip
Merge branch 'main' into fix/add-empty-dist-folderfix/add-empty-dist-folder
-rw-r--r--README.md2
-rw-r--r--src-tauri/Cargo.lock40
-rw-r--r--src-tauri/Cargo.toml4
-rw-r--r--src-tauri/src/constants.rs9
-rw-r--r--src-tauri/src/github/release_notes.rs65
-rw-r--r--src-tauri/src/lib.rs9
-rw-r--r--src-tauri/src/main.rs93
-rw-r--r--src-tauri/src/mod_management/mod.rs249
-rw-r--r--src-tauri/src/repair_and_verify/mod.rs10
-rw-r--r--src-tauri/tauri.conf.json2
-rw-r--r--src-vue/src/App.vue5
-rw-r--r--src-vue/src/components/ModsMenu.vue118
-rw-r--r--src-vue/src/components/ThunderstoreModCard.vue83
-rw-r--r--src-vue/src/main.ts2
-rw-r--r--src-vue/src/plugins/modules/search.ts19
-rw-r--r--src-vue/src/plugins/store.ts97
-rw-r--r--src-vue/src/utils/FlightCoreVersion.d.ts5
-rw-r--r--src-vue/src/utils/SortOptions.d.ts8
-rw-r--r--src-vue/src/utils/thunderstore/ThunderstoreMod.d.ts2
-rw-r--r--src-vue/src/views/DeveloperView.vue8
-rw-r--r--src-vue/src/views/ModsView.vue94
-rw-r--r--src-vue/src/views/SettingsView.vue48
-rw-r--r--src-vue/src/views/ThunderstoreModsView.vue186
-rw-r--r--src-vue/src/views/mods/LocalModsView.vue122
-rw-r--r--src-vue/src/views/mods/ThunderstoreModsView.vue272
-rw-r--r--src-vue/tsconfig.json2
26 files changed, 1093 insertions, 461 deletions
diff --git a/README.md b/README.md
index 152cf059..f4f2e7ff 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
<img src="docs/assets/Square310x310Logo.png" width="200px">
<br>
<br>
- <a href="https://github.com/R2NorthstarTools/FlightCore/releases/download/v1.3.2/FlightCore_1.3.2_x64_en-US.msi"><img src="docs/assets/downloadbutton.png" width="300px"></a>
+ <a href="https://github.com/R2NorthstarTools/FlightCore/releases/download/v1.6.0/FlightCore_1.6.0_x64_en-US.msi"><img src="docs/assets/downloadbutton.png" width="300px"></a>
<br>
</p>
diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock
index cc2f4fbb..0376d592 100644
--- a/src-tauri/Cargo.lock
+++ b/src-tauri/Cargo.lock
@@ -81,7 +81,7 @@ checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6"
[[package]]
name = "app"
-version = "1.3.2"
+version = "1.6.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -1818,17 +1818,14 @@ dependencies = [
[[package]]
name = "libthermite"
-version = "0.3.4"
+version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9ced5f2756672c1379a5ee41771af8e1ead525a9f6b1e2a2139105a6754f6bb5"
+checksum = "31ec1288a3f54f21ee6bd7eec92c5d098d9acc82d3bfdcf3c5ae29498ddfdc0e"
dependencies = [
- "directories",
"futures-util",
"indicatif",
"log",
- "regex",
"reqwest",
- "ron",
"serde",
"serde_json",
"thiserror",
@@ -2995,17 +2992,6 @@ dependencies = [
]
[[package]]
-name = "ron"
-version = "0.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "300a51053b1cb55c80b7a9fde4120726ddf25ca241a1cbb926626f62fb136bff"
-dependencies = [
- "base64",
- "bitflags",
- "serde",
-]
-
-[[package]]
name = "rusqlite"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3251,18 +3237,18 @@ dependencies = [
[[package]]
name = "serde"
-version = "1.0.149"
+version = "1.0.152"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "256b9932320c590e707b94576e3cc1f7c9024d0ee6612dfbcf1cb106cbe8e055"
+checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
-version = "1.0.149"
+version = "1.0.152"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b4eae9b04cbffdfd550eb462ed33bc6a1b68c935127d008b27444d08380f94e4"
+checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e"
dependencies = [
"proc-macro2",
"quote",
@@ -3271,9 +3257,9 @@ dependencies = [
[[package]]
name = "serde_json"
-version = "1.0.89"
+version = "1.0.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "020ff22c755c2ed3f8cf162dbb41a7268d934702f3ed3631656ea597e08fc3db"
+checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883"
dependencies = [
"itoa 1.0.4",
"ryu",
@@ -3953,18 +3939,18 @@ checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c"
[[package]]
name = "thiserror"
-version = "1.0.37"
+version = "1.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e"
+checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
-version = "1.0.37"
+version = "1.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb"
+checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f"
dependencies = [
"proc-macro2",
"quote",
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index 7f3f073c..dcdf8d3b 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "app"
-version = "1.3.2"
+version = "1.6.0"
description = "A Tauri App"
authors = ["you"]
license = ""
@@ -25,7 +25,7 @@ steamlocate = "1.0.2"
# Error messages
anyhow = "1.0"
# libthermite for Northstar/mod install handling
-libthermite = "0.3.4"
+libthermite = "0.4.0"
# zip stuff
zip = "0.6.2"
# Regex
diff --git a/src-tauri/src/constants.rs b/src-tauri/src/constants.rs
new file mode 100644
index 00000000..4cc3c204
--- /dev/null
+++ b/src-tauri/src/constants.rs
@@ -0,0 +1,9 @@
+// This file stores various global constants values
+
+pub const APP_USER_AGENT: &str = "R2NorthstarTools/FlightCore";
+
+// URL of the Northstar masterserver
+pub const MASTER_SERVER_URL: &str = "https://northstar.tf";
+
+// server list endpoint
+pub const SERVER_BROWSER_ENDPOINT: &str = "/client/servers";
diff --git a/src-tauri/src/github/release_notes.rs b/src-tauri/src/github/release_notes.rs
index f139ead5..758f7ace 100644
--- a/src-tauri/src/github/release_notes.rs
+++ b/src-tauri/src/github/release_notes.rs
@@ -1,3 +1,4 @@
+use crate::constants::APP_USER_AGENT;
use serde::{Deserialize, Serialize};
use std::vec::Vec;
@@ -8,15 +9,20 @@ pub struct ReleaseInfo {
pub body: String,
}
+#[derive(Serialize, Deserialize, Debug, Clone)]
+pub struct FlightCoreVersion {
+ tag_name: String,
+ published_at: String,
+}
+
// Fetches repo release API and returns response as string
async fn fetch_github_releases_api(url: &str) -> Result<String, String> {
println!("Fetching releases notes from GitHub API");
- let user_agent = "R2NorthstarTools/FlightCore";
let client = reqwest::Client::new();
let res = client
.get(url)
- .header(reqwest::header::USER_AGENT, user_agent)
+ .header(reqwest::header::USER_AGENT, APP_USER_AGENT)
.send()
.await
.unwrap()
@@ -27,24 +33,26 @@ async fn fetch_github_releases_api(url: &str) -> Result<String, String> {
Ok(res)
}
-/// Checks if installed FlightCore version is up-to-date
-/// false -> FlightCore install is up-to-date
-/// true -> FlightCore install is outdated
-pub async fn check_is_flightcore_outdated() -> Result<bool, String> {
+/// Gets newest FlighCore version from GitHub
+#[tauri::command]
+pub async fn get_newest_flightcore_version() -> Result<FlightCoreVersion, String> {
// Get newest version number from GitHub API
println!("Checking GitHub API");
let url = "https://api.github.com/repos/R2NorthstarTools/FlightCore/releases/latest";
let res = fetch_github_releases_api(url).await?;
- let json_response: serde_json::Value =
+ let flightcore_version: FlightCoreVersion =
serde_json::from_str(&res).expect("JSON was not well-formatted");
println!("Done checking GitHub API");
- // Extract version number from JSON
- let newest_release_version = json_response
- .get("tag_name")
- .and_then(|value| value.as_str())
- .unwrap();
+ Ok(flightcore_version)
+}
+
+/// Checks if installed FlightCore version is up-to-date
+/// false -> FlightCore install is up-to-date
+/// true -> FlightCore install is outdated
+pub async fn check_is_flightcore_outdated() -> Result<bool, String> {
+ let newest_flightcore_release = get_newest_flightcore_version().await?;
// Get version of installed FlightCore...
let version = env!("CARGO_PKG_VERSION");
@@ -52,16 +60,10 @@ pub async fn check_is_flightcore_outdated() -> Result<bool, String> {
let version = format!("v{}", version);
// TODO: This shouldn't be a string compare but promper semver compare
- let is_outdated = version != newest_release_version;
+ let is_outdated = version != newest_flightcore_release.tag_name;
// If outdated, check how new the update is
if is_outdated {
- // Extract release date from JSON
- let release_date = json_response
- .get("published_at")
- .and_then(|value| value.as_str())
- .unwrap();
-
// Time to wait (2h) h * m * s
let threshold_seconds = 2 * 60 * 60;
@@ -69,7 +71,7 @@ pub async fn check_is_flightcore_outdated() -> Result<bool, String> {
let current_time = chrono::Utc::now();
// Get latest release time from GitHub API response
- let result = chrono::DateTime::parse_from_rfc3339(release_date)
+ let result = chrono::DateTime::parse_from_rfc3339(&newest_flightcore_release.published_at)
.unwrap()
.with_timezone(&chrono::Utc);
@@ -91,28 +93,9 @@ pub async fn get_northstar_release_notes() -> Result<Vec<ReleaseInfo>, String> {
let url = "https://api.github.com/repos/R2Northstar/Northstar/releases";
let res = fetch_github_releases_api(url).await?;
- let json_response: Vec<serde_json::Value> =
+ let release_info_vector: Vec<ReleaseInfo> =
serde_json::from_str(&res).expect("JSON was not well-formatted");
println!("Done checking GitHub API");
- return Ok(json_response
- .iter()
- .map(|release| ReleaseInfo {
- name: release
- .get("name")
- .and_then(|value| value.as_str())
- .unwrap()
- .to_string(),
- published_at: release
- .get("published_at")
- .and_then(|value| value.as_str())
- .unwrap()
- .to_string(),
- body: release
- .get("body")
- .and_then(|value| value.as_str())
- .unwrap()
- .to_string(),
- })
- .collect());
+ return Ok(release_info_vector);
}
diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs
index f8ea6aba..67f42f85 100644
--- a/src-tauri/src/lib.rs
+++ b/src-tauri/src/lib.rs
@@ -4,6 +4,7 @@ use anyhow::{anyhow, Context, Result};
mod northstar;
+pub mod constants;
mod platform_specific;
#[cfg(target_os = "windows")]
use platform_specific::windows;
@@ -38,6 +39,12 @@ pub struct NorthstarMod {
pub directory: String,
}
+#[derive(Serialize, Deserialize, Debug, Clone)]
+pub struct NorthstarServer {
+ #[serde(rename = "playerCount")]
+ pub player_count: i32,
+}
+
/// Check version number of a mod
pub fn check_mod_version_number(path_to_mod_folder: String) -> Result<String, anyhow::Error> {
// println!("{}", format!("{}/mod.json", path_to_mod_folder));
@@ -183,7 +190,7 @@ async fn do_install(nmod: &thermite::model::ModVersion, game_path: &std::path::P
let download_path = format!("{}/{}", download_directory.clone(), filename);
println!("{}", download_path);
- let nfile = thermite::core::actions::download_file(&nmod.url, download_path)
+ let nfile = thermite::core::manage::download_file(&nmod.url, download_path)
.await
.unwrap();
diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs
index e8912d60..4eab7a00 100644
--- a/src-tauri/src/main.rs
+++ b/src-tauri/src/main.rs
@@ -9,10 +9,15 @@ use std::{
time::Duration,
};
-use app::*;
+use app::{
+ constants::{APP_USER_AGENT, MASTER_SERVER_URL, SERVER_BROWSER_ENDPOINT},
+ *,
+};
mod github;
-use github::release_notes::{check_is_flightcore_outdated, get_northstar_release_notes};
+use github::release_notes::{
+ check_is_flightcore_outdated, get_newest_flightcore_version, get_northstar_release_notes,
+};
mod repair_and_verify;
use repair_and_verify::{
@@ -21,7 +26,8 @@ use repair_and_verify::{
mod mod_management;
use mod_management::{
- fc_download_mod_and_install, get_installed_mods_and_properties, set_mod_enabled_status,
+ delete_northstar_mod, delete_thunderstore_mod, fc_download_mod_and_install,
+ get_installed_mods_and_properties, set_mod_enabled_status,
};
mod northstar;
@@ -90,17 +96,20 @@ fn main() {
update_northstar_caller,
launch_northstar_caller,
check_is_flightcore_outdated_caller,
- get_log_list_caller,
- verify_game_files_caller,
- get_enabled_mods_caller,
- set_mod_enabled_status_caller,
- disable_all_but_core_caller,
+ get_log_list,
+ verify_game_files,
+ set_mod_enabled_status,
+ disable_all_but_core,
is_debug_mode,
get_northstar_release_notes,
linux_checks,
- get_installed_mods_caller,
+ get_installed_mods_and_properties,
install_mod_caller,
clean_up_download_folder_caller,
+ get_newest_flightcore_version,
+ delete_northstar_mod,
+ get_server_player_count,
+ delete_thunderstore_mod,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
@@ -274,43 +283,6 @@ async fn launch_northstar_caller(
}
#[tauri::command]
-/// Get list of Northstar logs
-async fn get_log_list_caller(game_install: GameInstall) -> Result<Vec<std::path::PathBuf>, String> {
- get_log_list(game_install)
-}
-
-#[tauri::command]
-async fn verify_game_files_caller(game_install: GameInstall) -> Result<String, String> {
- verify_game_files(game_install)
-}
-
-#[tauri::command]
-async fn get_enabled_mods_caller(
- game_install: GameInstall,
-) -> Result<serde_json::value::Value, String> {
- get_enabled_mods(game_install)
-}
-
-#[tauri::command]
-async fn set_mod_enabled_status_caller(
- game_install: GameInstall,
- mod_name: String,
- is_enabled: bool,
-) -> Result<(), String> {
- set_mod_enabled_status(game_install, mod_name, is_enabled)
-}
-
-#[tauri::command]
-async fn disable_all_but_core_caller(game_install: GameInstall) -> Result<(), String> {
- disable_all_but_core(game_install)
-}
-
-#[tauri::command]
-async fn get_installed_mods_caller(game_install: GameInstall) -> Result<Vec<NorthstarMod>, String> {
- get_installed_mods_and_properties(game_install)
-}
-
-#[tauri::command]
/// Installs the specified mod
async fn install_mod_caller(
game_install: GameInstall,
@@ -339,3 +311,32 @@ async fn clean_up_download_folder_caller(
Err(err) => Err(err.to_string()),
}
}
+
+/// Gets server and playercount from master server API
+#[tauri::command]
+async fn get_server_player_count() -> Result<(i32, usize), String> {
+ let url = format!("{MASTER_SERVER_URL}{SERVER_BROWSER_ENDPOINT}");
+ let client = reqwest::Client::new();
+ let res = client
+ .get(url)
+ .header(reqwest::header::USER_AGENT, APP_USER_AGENT)
+ .send()
+ .await
+ .unwrap()
+ .text()
+ .await
+ .unwrap();
+
+ let ns_servers: Vec<NorthstarServer> =
+ serde_json::from_str(&res).expect("JSON was not well-formatted");
+
+ // Get server count
+ let server_count = ns_servers.len();
+
+ // Sum up player count
+ let total_player_count: i32 = ns_servers.iter().map(|server| server.player_count).sum();
+
+ dbg!((total_player_count, server_count));
+
+ Ok((total_player_count, server_count))
+}
diff --git a/src-tauri/src/mod_management/mod.rs b/src-tauri/src/mod_management/mod.rs
index 4fa52127..c37882fe 100644
--- a/src-tauri/src/mod_management/mod.rs
+++ b/src-tauri/src/mod_management/mod.rs
@@ -3,6 +3,8 @@ use async_recursion::async_recursion;
use anyhow::{anyhow, Result};
use app::NorthstarMod;
+use serde::{Deserialize, Serialize};
+use std::io::Read;
use std::path::PathBuf;
use app::get_enabled_mods;
@@ -10,12 +12,45 @@ use app::GameInstall;
use json5;
+use crate::northstar::CORE_MODS;
+
pub const BLACKLISTED_MODS: [&str; 3] = [
"northstar-Northstar",
"northstar-NorthstarReleaseCandidate",
"ebkr-r2modman",
];
+#[derive(Debug, Clone)]
+struct ParsedThunderstoreModString {
+ author_name: String,
+ mod_name: String,
+ version: Option<String>,
+}
+
+impl std::str::FromStr for ParsedThunderstoreModString {
+ type Err = ();
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let mut parts = s.split("-");
+
+ let author_name = parts.next().unwrap().to_string();
+ let mod_name = parts.next().unwrap().to_string();
+ let version = parts.next().map(|s| s.to_string());
+
+ Ok(ParsedThunderstoreModString {
+ author_name,
+ mod_name,
+ version,
+ })
+ }
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone)]
+pub struct ThunderstoreManifest {
+ name: String,
+ version_number: String,
+}
+
/// Gets all currently installed and enabled/disabled mods to rebuild `enabledmods.json`
pub fn rebuild_enabled_mods_json(game_install: GameInstall) -> Result<(), String> {
let enabledmods_json_path = format!("{}/R2Northstar/enabledmods.json", game_install.game_path);
@@ -43,6 +78,7 @@ pub fn rebuild_enabled_mods_json(game_install: GameInstall) -> Result<(), String
}
/// Set the status of a passed mod to enabled/disabled
+#[tauri::command]
pub fn set_mod_enabled_status(
game_install: GameInstall,
mod_name: String,
@@ -105,6 +141,7 @@ fn parse_mod_json_for_mod_name(mod_json_path: String) -> Result<String, anyhow::
}
/// Parses `mod.json` for Thunderstore mod string
+/// This is a legacy function as nowadays `manifest.json` should be in Northtar mods folder and read from there
// TODO: Maybe pass PathBuf or serde json object
fn parse_mod_json_for_thunderstore_mod_string(
mod_json_path: String,
@@ -125,6 +162,36 @@ fn parse_mod_json_for_thunderstore_mod_string(
Ok(thunderstore_mod_string.to_string())
}
+/// Parses `manifest.json` for Thunderstore mod string
+fn parse_for_thunderstore_mod_string(nsmod_path: String) -> Result<String, anyhow::Error> {
+ let mod_json_path = format!("{}/mod.json", nsmod_path);
+ let manifest_json_path = format!("{}/manifest.json", nsmod_path);
+ let ts_author_txt_path = format!("{}/thunderstore_author.txt", nsmod_path);
+
+ // Attempt legacy method for getting Thunderstore string first
+ match parse_mod_json_for_thunderstore_mod_string(mod_json_path) {
+ Ok(thunderstore_mod_string) => return Ok(thunderstore_mod_string),
+ Err(_err) => (),
+ }
+
+ // Check if `manifest.json` exists and parse
+ let data = std::fs::read_to_string(manifest_json_path)?;
+ let thunderstore_manifest: ThunderstoreManifest = json5::from_str(&data)?;
+
+ // Check if `thunderstore_author.txt` exists and parse
+ let mut file = std::fs::File::open(ts_author_txt_path)?;
+ let mut thunderstore_author = String::new();
+ file.read_to_string(&mut thunderstore_author)?;
+
+ // Build mod string
+ let thunderstore_mod_string = format!(
+ "{}-{}-{}",
+ thunderstore_author, thunderstore_manifest.name, thunderstore_manifest.version_number
+ );
+
+ Ok(thunderstore_mod_string)
+}
+
/// Parse `mods` folder for installed mods.
fn parse_installed_mods(game_install: GameInstall) -> Result<Vec<NorthstarMod>, String> {
let ns_mods_folder = format!("{}/R2Northstar/mods/", game_install.game_path);
@@ -146,8 +213,9 @@ fn parse_installed_mods(game_install: GameInstall) -> Result<Vec<NorthstarMod>,
// Iterate over folders and check if they are Northstar mods
for directory in directories {
+ let directory_str = directory.to_str().unwrap().to_string();
// Check if mod.json exists
- let mod_json_path = format!("{}/mod.json", directory.to_str().unwrap());
+ let mod_json_path = format!("{}/mod.json", directory_str);
if !std::path::Path::new(&mod_json_path).exists() {
continue;
}
@@ -160,11 +228,11 @@ fn parse_installed_mods(game_install: GameInstall) -> Result<Vec<NorthstarMod>,
continue;
}
};
- let thunderstore_mod_string =
- match parse_mod_json_for_thunderstore_mod_string(mod_json_path.clone()) {
- Ok(thunderstore_mod_string) => Some(thunderstore_mod_string),
- Err(_err) => None,
- };
+ // Get Thunderstore mod string if it exists
+ let thunderstore_mod_string = match parse_for_thunderstore_mod_string(directory_str) {
+ Ok(thunderstore_mod_string) => Some(thunderstore_mod_string),
+ Err(_err) => None,
+ };
// Get directory path
let mod_directory = directory.to_str().unwrap().to_string();
@@ -185,6 +253,7 @@ fn parse_installed_mods(game_install: GameInstall) -> Result<Vec<NorthstarMod>,
/// Gets list of installed mods and their properties
/// - name
/// - is enabled?
+#[tauri::command]
pub fn get_installed_mods_and_properties(
game_install: GameInstall,
) -> Result<Vec<NorthstarMod>, String> {
@@ -217,8 +286,19 @@ async fn get_ns_mod_download_url(thunderstore_mod_string: String) -> Result<Stri
// TODO: This will crash the thread if not internet connection exist. `match` should be used instead
let index = thermite::api::get_package_index().await.unwrap().to_vec();
- // String replace works but more care should be taken in the future
- let ts_mod_string_url = thunderstore_mod_string.replace("-", "/");
+ // Parse mod string
+ let parsed_ts_mod_string: ParsedThunderstoreModString = match thunderstore_mod_string.parse() {
+ Ok(res) => res,
+ Err(_) => return Err("Failed to parse mod string".to_string()),
+ };
+
+ // Encode as URL
+ let ts_mod_string_url = format!(
+ "{}/{}/{}",
+ parsed_ts_mod_string.author_name,
+ parsed_ts_mod_string.mod_name,
+ parsed_ts_mod_string.version.unwrap()
+ );
for ns_mod in index {
// Iterate over all versions of a given mod
@@ -233,32 +313,6 @@ async fn get_ns_mod_download_url(thunderstore_mod_string: String) -> Result<Stri
Err("Could not find mod on Thunderstore".to_string())
}
-/// Adds given Thunderstore mod string to the given `mod.json`
-/// This way we can later check whether a mod is outdated based on the TS mod string
-fn add_thunderstore_mod_string(
- path_to_mod_json: String,
- thunderstore_mod_string: String,
-) -> Result<(), anyhow::Error> {
- // Read file into string and parse it
- let data = std::fs::read_to_string(path_to_mod_json.clone())?;
- let parsed_json: serde_json::Value = json5::from_str(&data)?;
-
- // Insert the Thunderstore mod string
- let mut parsed_json = parsed_json.as_object().unwrap().clone();
- parsed_json.insert(
- "ThunderstoreModString".to_string(),
- serde_json::Value::String(thunderstore_mod_string),
- );
-
- // And write back to disk
- std::fs::write(
- path_to_mod_json,
- serde_json::to_string_pretty(&parsed_json)?,
- )?;
-
- Ok(())
-}
-
/// Returns a vector of modstrings containing the dependencies of a given mod
async fn get_mod_dependencies(
thunderstore_mod_string: String,
@@ -348,38 +402,123 @@ pub async fn fc_download_mod_and_install(
);
// Download the mod
- let f = match thermite::core::actions::download_file(&download_url, path.clone()).await {
+ let f = match thermite::core::manage::download_file(&download_url, path.clone()).await {
Ok(f) => f,
Err(e) => return Err(e.to_string()),
};
+ // Get Thunderstore mod author
+ let author = thunderstore_mod_string.split("-").next().unwrap();
+
// Extract the mod to the mods directory
- let pkg = match thermite::core::actions::install_mod(&f, std::path::Path::new(&mods_directory))
- {
- Ok(pkg) => pkg,
+ match thermite::core::manage::install_mod(author, &f, std::path::Path::new(&mods_directory)) {
+ Ok(()) => (),
Err(err) => return Err(err.to_string()),
};
- dbg!(pkg.clone());
-
- // Add Thunderstore mod string to `mod.json` of installed NorthstarMods
- for nsmod in pkg.mods {
- let path_to_current_mod_json = format!(
- "{}/{}/mod.json",
- mods_directory,
- nsmod.path.to_string_lossy()
- );
- match add_thunderstore_mod_string(path_to_current_mod_json, thunderstore_mod_string.clone())
+
+ // Delete downloaded zip file
+ std::fs::remove_file(path).unwrap();
+
+ Ok(())
+}
+
+/// Deletes a given Northstar mod folder
+fn delete_mod_folder(ns_mod_directory: String) -> Result<(), String> {
+ let ns_mod_dir_path = std::path::Path::new(&ns_mod_directory);
+
+ // Safety check: Check whether `mod.json` exists and exit early if not
+ // If it does not exist, we might not be dealing with a Northstar mod
+ let mod_json_path = ns_mod_dir_path.join("mod.json");
+ if !mod_json_path.exists() {
+ // If it doesn't exist, return an error
+ return Err(format!("mod.json does not exist in {}", ns_mod_directory));
+ }
+
+ match std::fs::remove_dir_all(&ns_mod_directory) {
+ Ok(()) => Ok(()),
+ Err(err) => Err(format!("Failed deleting mod: {err}")),
+ }
+}
+
+/// Deletes a Northstar mod based on its name
+#[tauri::command]
+pub fn delete_northstar_mod(game_install: GameInstall, nsmod_name: String) -> Result<(), String> {
+ // Prevent deleting core mod
+ for core_mod in CORE_MODS {
+ if nsmod_name == core_mod {
+ return Err(format!("Cannot remove core mod {nsmod_name}"));
+ }
+ }
+
+ // Get installed mods
+ let installed_ns_mods = get_installed_mods_and_properties(game_install)?;
+
+ // Get folder name based on northstarmods
+ for installed_ns_mod in installed_ns_mods {
+ // Installed mod matches specified mod
+ if installed_ns_mod.name == nsmod_name {
+ // Delete folder
+ return delete_mod_folder(installed_ns_mod.directory);
+ }
+ }
+
+ Err(format!("Mod {nsmod_name} not found to be installed"))
+}
+
+/// Deletes all NorthstarMods related to a Thunderstore mod
+#[tauri::command]
+pub fn delete_thunderstore_mod(
+ game_install: GameInstall,
+ thunderstore_mod_string: String,
+) -> Result<(), String> {
+ // Prevent deleting core mod
+ for core_ts_mod in BLACKLISTED_MODS {
+ if thunderstore_mod_string == core_ts_mod {
+ return Err(format!("Cannot remove core mod {thunderstore_mod_string}"));
+ }
+ }
+
+ let parsed_ts_mod_string: ParsedThunderstoreModString =
+ thunderstore_mod_string.parse().unwrap();
+
+ // Get installed mods
+ let installed_ns_mods = get_installed_mods_and_properties(game_install)?;
+
+ // List of mod folders to remove
+ let mut mod_folders_to_remove: Vec<String> = Vec::new();
+
+ // Get folder name based on Thundestore mod string
+ for installed_ns_mod in installed_ns_mods {
+ if installed_ns_mod.thunderstore_mod_string.is_none() {
+ // Not a Thunderstore mod
+ continue;
+ }
+
+ let installed_ns_mod_ts_string: ParsedThunderstoreModString = installed_ns_mod
+ .thunderstore_mod_string
+ .unwrap()
+ .parse()
+ .unwrap();
+
+ // Installed mod matches specified Thunderstore mod string
+ if parsed_ts_mod_string.author_name == installed_ns_mod_ts_string.author_name
+ && parsed_ts_mod_string.mod_name == installed_ns_mod_ts_string.mod_name
{
- Ok(()) => (),
- Err(err) => {
- println!("Failed setting modstring for {}", nsmod.name);
- println!("{}", err);
- }
+ // Add folder to list of folder to remove
+ mod_folders_to_remove.push(installed_ns_mod.directory);
}
}
- // Delete downloaded zip file
- std::fs::remove_file(path).unwrap();
+ if !(mod_folders_to_remove.len() > 0) {
+ return Err(format!(
+ "No mods removed as no Northstar mods matching {thunderstore_mod_string} were found to be installed."
+ ));
+ }
+
+ // Delete given folders
+ for mod_folder in mod_folders_to_remove {
+ delete_mod_folder(mod_folder)?;
+ }
Ok(())
}
diff --git a/src-tauri/src/repair_and_verify/mod.rs b/src-tauri/src/repair_and_verify/mod.rs
index 64278a25..fb108f37 100644
--- a/src-tauri/src/repair_and_verify/mod.rs
+++ b/src-tauri/src/repair_and_verify/mod.rs
@@ -1,9 +1,13 @@
-use crate::{mod_management::{set_mod_enabled_status, rebuild_enabled_mods_json}, northstar::CORE_MODS};
+use crate::{
+ mod_management::{rebuild_enabled_mods_json, set_mod_enabled_status},
+ northstar::CORE_MODS,
+};
use anyhow::anyhow;
/// Contains various functions to repair common issues and verifying installation
use app::{get_enabled_mods, GameInstall};
/// Verifies Titanfall2 game files
+#[tauri::command]
pub fn verify_game_files(game_install: GameInstall) -> Result<String, String> {
dbg!(game_install);
Err("TODO, not yet implemented".to_string())
@@ -11,8 +15,8 @@ pub fn verify_game_files(game_install: GameInstall) -> Result<String, String> {
/// Disables all mods except core ones
/// Enables core mods if disabled
+#[tauri::command]
pub fn disable_all_but_core(game_install: GameInstall) -> Result<(), String> {
-
// Rebuild `enabledmods.json` first to ensure all mods are added
rebuild_enabled_mods_json(game_install.clone())?;
@@ -62,6 +66,8 @@ pub fn clean_up_download_folder(
Ok(())
}
+/// Get list of Northstar logs
+#[tauri::command]
pub fn get_log_list(game_install: GameInstall) -> Result<Vec<std::path::PathBuf>, String> {
let ns_log_folder = format!("{}/R2Northstar/logs", game_install.game_path);
diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json
index 9dde0f14..ca5ec9c3 100644
--- a/src-tauri/tauri.conf.json
+++ b/src-tauri/tauri.conf.json
@@ -8,7 +8,7 @@
},
"package": {
"productName": "FlightCore",
- "version": "1.3.2"
+ "version": "1.6.0"
},
"tauri": {
"allowlist": {
diff --git a/src-vue/src/App.vue b/src-vue/src/App.vue
index fc6992b7..c28919e1 100644
--- a/src-vue/src/App.vue
+++ b/src-vue/src/App.vue
@@ -50,7 +50,7 @@ export default {
<nav id="fc_menu-bar">
<!-- Navigation items -->
<el-menu
- default-active="/"
+ :default-active="$route.path"
router
mode="horizontal"
id="fc__menu_items"
@@ -59,7 +59,6 @@ export default {
<el-menu-item index="/">Play</el-menu-item>
<el-menu-item index="/changelog">Changelog</el-menu-item>
<el-menu-item index="/mods">Mods</el-menu-item>
- <el-menu-item index="/thunderstoreMods">Thunderstore</el-menu-item>
<el-menu-item index="/settings">Settings</el-menu-item>
<el-menu-item index="/dev" v-if="$store.state.developer_mode">Dev</el-menu-item>
</el-menu>
@@ -109,7 +108,7 @@ export default {
border-color: white;
}
-.el-menu > .el-menu-item {
+#fc__menu_items > .el-menu-item {
text-transform: uppercase;
border: none !important;
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
diff --git a/src-vue/src/components/ModsMenu.vue b/src-vue/src/components/ModsMenu.vue
new file mode 100644
index 00000000..9b62fcfa
--- /dev/null
+++ b/src-vue/src/components/ModsMenu.vue
@@ -0,0 +1,118 @@
+<template>
+ <nav class="fc_mods__menu">
+ <el-menu
+ default-active="1"
+ text-color="#fff"
+ >
+ <h5>Mods</h5>
+ <el-menu-item index="1" @click="$emit('showLocalMods', true)">
+ <el-icon><Folder /></el-icon>
+ <span>Local</span>
+ </el-menu-item>
+ <el-menu-item index="2" @click="$emit('showLocalMods', false)">
+ <el-icon><Connection /></el-icon>
+ <span>Online</span>
+ </el-menu-item>
+
+ <!-- Search inputs -->
+ <h5>Filter</h5>
+ <el-input v-model="$store.state.search.searchValue" placeholder="Search" clearable />
+ <el-select
+ v-if="!showingLocalMods"
+ v-model="$store.state.search.sortValue"
+ placeholder="Sort mods"
+ >
+ <el-option
+ v-for="item of sortValues"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+ <el-select
+ v-if="!showingLocalMods"
+ v-model="$store.state.search.selectedCategories"
+ multiple
+ placeholder="Select categories"
+ >
+ <el-option
+ v-for="item in $store.state.thunderstoreModsCategories"
+ :key="item"
+ :label="item"
+ :value="item"
+ />
+ </el-select>
+
+ </el-menu>
+ </nav>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue'
+import { SortOptions } from '../utils/SortOptions.d';
+
+export default defineComponent({
+ name: 'ModsMenu',
+ props: {
+ showingLocalMods: {
+ required: true,
+ type: Boolean
+ }
+ },
+ mounted() {
+ this.$store.state.search.sortValue = this.sortValues[3].value;
+ },
+ computed: {
+ sortValues(): {label: string, value: string}[] {
+ return Object.keys(SortOptions).map((key: string) => ({
+ value: key,
+ label: Object.values(SortOptions)[Object.keys(SortOptions).indexOf(key)]
+ }));
+ }
+ }
+})
+</script>
+
+<style scoped>
+.fc_mods__menu {
+ display: flex;
+ max-width: 222px;
+ min-width: 222px;
+ padding: 10px;
+}
+
+.fc_mods__menu h5 {
+ margin: 8px 0 16px 5px;
+}
+
+.fc_mods__menu h5:not(:first-child){
+ margin-top: 32px;
+}
+
+.fc_mods__menu > .el-menu {
+ background-color: transparent;
+ border: none;
+ width: 100%;
+}
+
+.fc_mods__menu > .el-menu > .el-menu-item {
+ height: 32px;
+ margin-bottom: 5px;
+ border-radius: 5px;
+ color: #e2e6e7;
+}
+
+.fc_mods__menu > .el-menu > .el-menu-item:hover {
+ background-color: #4e4e4e3b;
+}
+
+.fc_mods__menu > .el-menu > .el-menu-item.is-active {
+ color: white;
+ background-color: #4e4e4e7a;
+}
+
+.el-select {
+ width: 100%;
+ margin-top: 5px;
+}
+</style>
diff --git a/src-vue/src/components/ThunderstoreModCard.vue b/src-vue/src/components/ThunderstoreModCard.vue
index 1e742fa2..a202aa50 100644
--- a/src-vue/src/components/ThunderstoreModCard.vue
+++ b/src-vue/src/components/ThunderstoreModCard.vue
@@ -35,11 +35,30 @@
>
{{ modButtonText }}
</el-button>
- <el-button link type="info" class="infoBtn" @click="openURL(mod.package_url)">
+
+ <!-- Information dropdown menu -->
+ <el-button v-if="!modIsRemovable"
+ link type="info" class="infoBtn" @click="openURL(mod.package_url)">
<el-icon>
<InfoFilled />
</el-icon>
</el-button>
+
+ <el-dropdown v-else>
+ <el-icon class="infoBtn moreBtn">
+ <MoreFilled />
+ </el-icon>
+ <template #dropdown>
+ <el-dropdown-menu>
+ <el-dropdown-item @click="openURL(mod.package_url)">
+ More info
+ </el-dropdown-item>
+ <el-dropdown-item @click="deleteMod(mod)">
+ Remove mod
+ </el-dropdown-item>
+ </el-dropdown-menu>
+ </template>
+ </el-dropdown>
</span>
</div>
</el-card>
@@ -54,6 +73,8 @@ import {ThunderstoreModStatus} from "../utils/thunderstore/ThunderstoreModStatus
import {NorthstarMod} from "../utils/NorthstarMod";
import {GameInstall} from "../utils/GameInstall";
import {ElNotification} from "element-plus";
+import { NorthstarState } from "../utils/NorthstarState";
+import { ElMessageBox } from "element-plus";
export default defineComponent({
name: "ThunderstoreModCard",
@@ -138,6 +159,15 @@ export default defineComponent({
},
/**
+ * Tells if a Thunderstore mod can be removed.
+ * This is used to tell if we should display the "Remove mod" option.
+ **/
+ modIsRemovable(): boolean {
+ return [ThunderstoreModStatus.INSTALLED, ThunderstoreModStatus.OUTDATED]
+ .includes(this.modStatus);
+ },
+
+ /**
* This computes the total count of downloads of a given mod, by adding
* download count of each of its releases.
*/
@@ -166,6 +196,50 @@ export default defineComponent({
return `${dependencyStringMembers[0]}-${dependencyStringMembers[1]}`;
},
+ async deleteMod(mod: ThunderstoreMod) {
+
+ // Show pop-up to confirm delete
+ ElMessageBox.confirm(
+ 'Delete Thunderstore mod?',
+ 'Warning',
+ {
+ confirmButtonText: 'OK',
+ cancelButtonText: 'Cancel',
+ type: 'warning',
+ }
+ )
+ .then(async () => { // Deletion confirmed
+ let game_install = {
+ game_path: this.$store.state.game_path,
+ install_type: this.$store.state.install_type
+ } as GameInstall;
+
+ await invoke("delete_thunderstore_mod", { gameInstall: game_install, thunderstoreModString: this.latestVersion.full_name })
+ .then((message) => {
+ ElNotification({
+ title: `Removed ${mod.name}`,
+ message: message as string,
+ type: 'success',
+ position: 'bottom-right'
+ });
+ })
+ .catch((error) => {
+ ElNotification({
+ title: 'Error',
+ message: error,
+ type: 'error',
+ position: 'bottom-right'
+ });
+ })
+ .finally(() => {
+ this.$store.commit('loadInstalledMods');
+ });
+ })
+ .catch(() => { // Deletion cancelled
+ console.log("Deleting Thunderstore mod cancelled.")
+ })
+ },
+
async installMod (mod: ThunderstoreMod) {
let game_install = {
game_path: this.$store.state.game_path,
@@ -241,8 +315,13 @@ export default defineComponent({
.infoBtn {
width: 20px;
- padding: 0;
+ padding: 0 !important;
font-size: 20px;
border: none;
}
+
+.moreBtn {
+ margin-left: 10px;
+ height: auto;
+}
</style>
diff --git a/src-vue/src/main.ts b/src-vue/src/main.ts
index 25f1b064..95bea7af 100644
--- a/src-vue/src/main.ts
+++ b/src-vue/src/main.ts
@@ -6,7 +6,6 @@ import { store } from './plugins/store';
import PlayView from "./views/PlayView.vue";
import ChangelogView from "./views/ChangelogView.vue";
import ModsView from "./views/ModsView.vue";
-import ThunderstoreModsView from "./views/ThunderstoreModsView.vue";
import SettingsView from "./views/SettingsView.vue";
import DeveloperView from "./views/DeveloperView.vue";
import {createRouter, createWebHashHistory} from "vue-router";
@@ -35,7 +34,6 @@ const routes = [
{ path: '/', name: 'Main', component: async () => PlayView},
{ path: '/changelog', name: 'Changelog', component: async () => ChangelogView},
{ path: '/mods', name: 'Mods', component: async () => ModsView},
- { path: '/thunderstoreMods', name: 'Thunderstore mods', component: async () => ThunderstoreModsView},
{ path: '/settings', name: 'Settings', component: async () => SettingsView},
{ path: '/dev', name: 'Dev', component: async () => DeveloperView}
];
diff --git a/src-vue/src/plugins/modules/search.ts b/src-vue/src/plugins/modules/search.ts
new file mode 100644
index 00000000..16260387
--- /dev/null
+++ b/src-vue/src/plugins/modules/search.ts
@@ -0,0 +1,19 @@
+interface SearchStoreState {
+ searchValue: string
+}
+
+export const searchModule = {
+ state: () => ({
+ // This is the treated value of search input
+ searchValue: '',
+ // Selected mod categories
+ selectedCategories: [],
+ sortValue: {label: '', value: ''}
+ }),
+ getters: {
+ searchWords(state: SearchStoreState): string {
+ return state.searchValue.toLowerCase();
+ }
+ }
+ }
+ \ No newline at end of file
diff --git a/src-vue/src/plugins/store.ts b/src-vue/src/plugins/store.ts
index c22479e0..2eae843a 100644
--- a/src-vue/src/plugins/store.ts
+++ b/src-vue/src/plugins/store.ts
@@ -5,15 +5,17 @@ import { InstallType } from "../utils/InstallType";
import { invoke } from "@tauri-apps/api";
import { GameInstall } from "../utils/GameInstall";
import { ReleaseCanal } from "../utils/ReleaseCanal";
+import { FlightCoreVersion } from "../utils/FlightCoreVersion";
import { ElNotification, NotificationHandle } from 'element-plus';
import { NorthstarState } from '../utils/NorthstarState';
import { appDir } from '@tauri-apps/api/path';
import { open } from '@tauri-apps/api/dialog';
import { Store } from 'tauri-plugin-store-api';
-import {router} from "../main";
+import { router } from "../main";
import ReleaseInfo from "../utils/ReleaseInfo";
import { ThunderstoreMod } from '../utils/thunderstore/ThunderstoreMod';
import { NorthstarMod } from "../utils/NorthstarMod";
+import { searchModule } from './modules/search';
const persistentStore = new Store('flight-core-settings.json');
@@ -32,15 +34,25 @@ export interface FlightCoreStore {
releaseNotes: ReleaseInfo[],
thunderstoreMods: ThunderstoreMod[],
+ thunderstoreModsCategories: string[],
installed_mods: NorthstarMod[],
northstar_is_running: boolean,
- origin_is_running: boolean
+ origin_is_running: boolean,
+
+ player_count: number,
+ server_count: number,
+
+ // user custom settings
+ mods_per_page: number,
}
let notification_handle: NotificationHandle;
export const store = createStore<FlightCoreStore>({
+ modules: {
+ search: searchModule
+ },
state(): FlightCoreStore {
return {
developer_mode: false,
@@ -56,10 +68,16 @@ export const store = createStore<FlightCoreStore>({
releaseNotes: [],
thunderstoreMods: [],
+ thunderstoreModsCategories: [],
installed_mods: [],
northstar_is_running: false,
- origin_is_running: false
+ origin_is_running: false,
+
+ player_count: -1,
+ server_count: -1,
+
+ mods_per_page: 20,
}
},
mutations: {
@@ -80,7 +98,7 @@ export const store = createStore<FlightCoreStore>({
_initializeListeners(state);
},
updateCurrentTab(state: any, newTab: Tabs) {
- router.push({path: newTab});
+ router.push({ path: newTab });
},
async updateGamePath(state: FlightCoreStore) {
// Open a selection dialog for directories
@@ -232,6 +250,16 @@ export const store = createStore<FlightCoreStore>({
state.thunderstoreMods = mods.filter((mod: ThunderstoreMod) => {
return !removedMods.includes(mod.name) && !mod.is_deprecated;
});
+
+ // Retrieve categories from mods
+ state.thunderstoreModsCategories = mods
+ .map((mod: ThunderstoreMod) => mod.categories)
+ .filter((modCategories: string[]) => modCategories.length !== 0)
+ .reduce((accumulator: string[], modCategories: string[]) => {
+ accumulator.push( ...modCategories.filter((cat: string) => !accumulator.includes(cat)) );
+ return accumulator;
+ }, [])
+ .sort();
},
async loadInstalledMods(state: FlightCoreStore) {
let game_install = {
@@ -246,7 +274,7 @@ export const store = createStore<FlightCoreStore>({
}
// Call back-end for installed mods
- await invoke("get_installed_mods_caller", { gameInstall: game_install })
+ await invoke("get_installed_mods_and_properties", { gameInstall: game_install })
.then((message) => {
state.installed_mods = (message as NorthstarMod[]);
})
@@ -314,11 +342,17 @@ async function _initializeApp(state: any) {
}
// Grab "Enable releases switching" setting from store if possible
- const valueFromStore: {value: boolean} | null = await persistentStore.get('northstar-releases-switching');
+ const valueFromStore: { value: boolean } | null = await persistentStore.get('northstar-releases-switching');
if (valueFromStore) {
state.enableReleasesSwitch = valueFromStore.value;
}
+ // Grab "Thunderstore mods per page" setting from store if possible
+ const perPageFromStore: { value: number } | null = await persistentStore.get('thunderstore-mods-per-page');
+ if (perPageFromStore && perPageFromStore.value) {
+ state.mods_per_page = perPageFromStore.value;
+ }
+
// Get FlightCore version number
state.flightcore_version = await invoke("get_flightcore_version_number");
@@ -370,6 +404,16 @@ async function _initializeApp(state: any) {
// Check installed Northstar version if found
await _get_northstar_version_number(state);
}
+
+ await invoke<[number, number]>("get_server_player_count")
+ .then((message) => {
+ state.player_count = message[0];
+ state.server_count = message[1];
+ })
+ .catch((error) => {
+ console.warn("Failed getting player/server count");
+ console.warn(error);
+ });
}
async function _checkForFlightCoreUpdates(state: FlightCoreStore) {
@@ -377,9 +421,10 @@ async function _checkForFlightCoreUpdates(state: FlightCoreStore) {
let flightcore_is_outdated = await invoke("check_is_flightcore_outdated_caller") as boolean;
if (flightcore_is_outdated) {
+ let newest_flightcore_version = await invoke("get_newest_flightcore_version") as FlightCoreVersion;
ElNotification({
title: 'FlightCore outdated!',
- message: `Please update FlightCore. Running outdated version ${state.flightcore_version}`,
+ message: `Please update FlightCore.\nRunning outdated version ${state.flightcore_version}.\nNewest is ${newest_flightcore_version.tag_name}!`,
type: 'warning',
position: 'bottom-right',
duration: 0 // Duration `0` means the notification will not auto-vanish
@@ -407,23 +452,23 @@ function _initializeListeners(state: any) {
*/
async function _get_northstar_version_number(state: any) {
await invoke("get_northstar_version_number_caller", { gamePath: state.game_path })
- .then((message) => {
- let northstar_version_number: string = message as string;
- state.installed_northstar_version = northstar_version_number;
- state.northstar_state = NorthstarState.READY_TO_PLAY;
-
- invoke("check_is_northstar_outdated", { gamePath: state.game_path, northstarPackageName: state.northstar_release_canal })
- .then((message) => {
- if (message) {
- state.northstar_state = NorthstarState.MUST_UPDATE;
- }
- })
- .catch((error) => {
- console.error(error);
- alert(error);
- });
- })
- .catch((error) => {
- state.northstar_state = NorthstarState.INSTALL;
- })
+ .then((message) => {
+ let northstar_version_number: string = message as string;
+ state.installed_northstar_version = northstar_version_number;
+ state.northstar_state = NorthstarState.READY_TO_PLAY;
+
+ invoke("check_is_northstar_outdated", { gamePath: state.game_path, northstarPackageName: state.northstar_release_canal })
+ .then((message) => {
+ if (message) {
+ state.northstar_state = NorthstarState.MUST_UPDATE;
+ }
+ })
+ .catch((error) => {
+ console.error(error);
+ alert(error);
+ });
+ })
+ .catch((error) => {
+ state.northstar_state = NorthstarState.INSTALL;
+ })
}
diff --git a/src-vue/src/utils/FlightCoreVersion.d.ts b/src-vue/src/utils/FlightCoreVersion.d.ts
new file mode 100644
index 00000000..2516bf25
--- /dev/null
+++ b/src-vue/src/utils/FlightCoreVersion.d.ts
@@ -0,0 +1,5 @@
+// derived from release_notes.rs
+export interface FlightCoreVersion {
+ tag_name: string,
+ published_at: string,
+}
diff --git a/src-vue/src/utils/SortOptions.d.ts b/src-vue/src/utils/SortOptions.d.ts
new file mode 100644
index 00000000..6bdd0a4a
--- /dev/null
+++ b/src-vue/src/utils/SortOptions.d.ts
@@ -0,0 +1,8 @@
+export enum SortOptions {
+ NAME_ASC = 'By name (A to Z)',
+ NAME_DESC = 'By name (Z to A)',
+ DATE_ASC = 'By date (from oldest)',
+ DATE_DESC = 'By date (from newest)',
+ MOST_DOWNLOADED = "Most downloaded",
+ TOP_RATED = "Top rated"
+}
diff --git a/src-vue/src/utils/thunderstore/ThunderstoreMod.d.ts b/src-vue/src/utils/thunderstore/ThunderstoreMod.d.ts
index 6fddd06f..6387c47e 100644
--- a/src-vue/src/utils/thunderstore/ThunderstoreMod.d.ts
+++ b/src-vue/src/utils/thunderstore/ThunderstoreMod.d.ts
@@ -3,8 +3,10 @@ import { ThunderstoreModVersion } from "./ThunderstoreModVersion";
export interface ThunderstoreMod {
name: string;
owner: string;
+ date_updated: string;
rating_score: number;
package_url: string;
is_deprecated: boolean;
versions: ThunderstoreModVersion[];
+ categories: string[];
}
diff --git a/src-vue/src/views/DeveloperView.vue b/src-vue/src/views/DeveloperView.vue
index 9f8881d8..6770caaa 100644
--- a/src-vue/src/views/DeveloperView.vue
+++ b/src-vue/src/views/DeveloperView.vue
@@ -111,7 +111,7 @@ export default defineComponent({
game_path: this.$store.state.game_path,
install_type: this.$store.state.install_type
} as GameInstall;
- await invoke("disable_all_but_core_caller", { gameInstall: game_install }).then((message) => {
+ await invoke("disable_all_but_core", { gameInstall: game_install }).then((message) => {
ElNotification({
title: 'Success',
message: "Disabled all mods but core",
@@ -133,7 +133,7 @@ export default defineComponent({
game_path: this.$store.state.game_path,
install_type: this.$store.state.install_type
} as GameInstall;
- await invoke("get_installed_mods_caller", { gameInstall: game_install }).then((message) => {
+ await invoke("get_installed_mods_and_properties", { gameInstall: game_install }).then((message) => {
// Simply console logging for now
// In the future we should display the installed mods somewhere
console.log(message);
@@ -162,7 +162,7 @@ export default defineComponent({
} as GameInstall;
let mod_to_install = this.mod_to_install_field_string;
await invoke("install_mod_caller", { gameInstall: game_install, thunderstoreModString: mod_to_install }).then((message) => {
- // Show user notificatio if mod install completed.
+ // Show user notification if mod install completed.
ElNotification({
title: `Installed ${mod_to_install}`,
message: message as string,
@@ -185,7 +185,7 @@ export default defineComponent({
install_type: this.$store.state.install_type
} as GameInstall;
await invoke("clean_up_download_folder_caller", { gameInstall: game_install, force: true }).then((message) => {
- // Show user notificatio if mod install completed.
+ // Show user notification if task completed.
ElNotification({
title: `Done`,
message: `Done`,
diff --git a/src-vue/src/views/ModsView.vue b/src-vue/src/views/ModsView.vue
index b1d1aeff..7c309832 100644
--- a/src-vue/src/views/ModsView.vue
+++ b/src-vue/src/views/ModsView.vue
@@ -1,78 +1,50 @@
<template>
- <div class="fc-container">
- <el-scrollbar>
- <h3>Installed Mods:</h3>
- <div>
- <p v-if="installedMods.length === 0">No mods were found.</p>
- <el-card v-else shadow="hover" v-for="mod in installedMods" v-bind:key="mod.name">
- <el-switch style="--el-switch-on-color: #13ce66; --el-switch-off-color: #8957e5" v-model="mod.enabled"
- :before-change="() => updateWhichModsEnabled(mod)" :loading="global_load_indicator" />
- {{mod.name}}
- </el-card>
- </div>
- </el-scrollbar>
+ <div class="fc-container" style="display: flex">
+ <!-- Local mods/Thunderstore mods menu -->
+ <mods-menu
+ :showingLocalMods="show_local_mods"
+ @showLocalMods="(v) => show_local_mods = v"
+ />
+
+ <!-- Mods content -->
+ <div class="fc_mods__container">
+ <local-mods-view
+ v-if="show_local_mods"
+ />
+
+ <thunderstore-mods-view
+ v-else
+ clearable
+ />
+ </div>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
-import { ElNotification } from "element-plus";
-import { invoke } from '@tauri-apps/api/tauri';
-import { GameInstall } from "../utils/GameInstall";
-import { NorthstarMod } from "../utils/NorthstarMod"
+import ThunderstoreModsView from "./mods/ThunderstoreModsView.vue";
+import LocalModsView from "./mods/LocalModsView.vue";
+import ModsMenu from "../components/ModsMenu.vue";
export default defineComponent({
name: "ModsView",
+ components: {
+ ModsMenu,
+ LocalModsView,
+ ThunderstoreModsView
+ },
data() {
return {
- global_load_indicator: false
- }
- },
- async mounted() {
- this.$store.commit('loadInstalledMods');
- },
- computed: {
- installedMods(): NorthstarMod[] {
- return this.$store.state.installed_mods;
- }
- },
- methods: {
- async updateWhichModsEnabled(mod: NorthstarMod) {
- this.global_load_indicator = true;
-
- // Setup up struct
- let game_install = {
- game_path: this.$store.state.game_path,
- install_type: this.$store.state.install_type
- } as GameInstall;
-
- // enable/disable specific mod
- try {
- await invoke("set_mod_enabled_status_caller", {
- gameInstall: game_install,
- modName: mod.name,
- // Need to set it to the opposite of current state,
- // as current state is only updated after command is run
- isEnabled: !mod.enabled,
- })
- }
- catch (error) {
- ElNotification({
- title: 'Error',
- message: `${error}`,
- type: 'error',
- position: 'bottom-right'
- });
- this.global_load_indicator = false;
- return false;
- }
-
- this.global_load_indicator = false;
- return true;
+ show_local_mods: true,
}
}
});
</script>
-<style>
+<style scoped>
+.fc_mods__container {
+ display: flex;
+ width: 100%;
+ flex-direction: column;
+}
</style>
diff --git a/src-vue/src/views/SettingsView.vue b/src-vue/src/views/SettingsView.vue
index ff87c394..1c03fe4f 100644
--- a/src-vue/src/views/SettingsView.vue
+++ b/src-vue/src/views/SettingsView.vue
@@ -14,6 +14,24 @@
<el-button icon="Folder" @click="updateGamePath"/>
</template>
</el-input>
+
+ <!-- Thunderstore mods per page configuration -->
+ <div class="fc_parameter__panel">
+ <h3>Number of Thunderstore mods per page</h3>
+ <h6>
+ This has an impact on display performances when browsing Thunderstore mods.<br>
+ Set this value to 0 to disable pagination.
+ </h6>
+ <el-input
+ v-model="modsPerPage"
+ type="number"
+ >
+ <template #append>
+ <el-button @click="modsPerPage = 20">Reset to default</el-button>
+ </template>
+ </el-input>
+ </div>
+
<h3>About:</h3>
<div class="fc_northstar__version" @click="activateDeveloperMode">
FlightCore Version: {{ flightcoreVersion === '' ? 'Unknown version' : `${flightcoreVersion}` }}
@@ -64,6 +82,15 @@ export default defineComponent({
this.$store.commit('toggleReleaseCandidate');
}
}
+ },
+ modsPerPage: {
+ get(): number {
+ return this.$store.state.mods_per_page;
+ },
+ set(value: number) {
+ this.$store.state.mods_per_page = value;
+ persistentStore.set('thunderstore-mods-per-page', { value });
+ }
}
},
methods: {
@@ -86,6 +113,12 @@ export default defineComponent({
},
mounted() {
document.querySelector('input')!.disabled = true;
+ },
+ unmounted() {
+ if (('' + this.modsPerPage) === '') {
+ console.warn('Incorrect value for modsPerPage, resetting it to 20.');
+ this.modsPerPage = 20;
+ }
}
});
</script>
@@ -110,4 +143,19 @@ h3:first-of-type {
.el-switch {
margin-left: 50px;
}
+
+
+/* Parameter panel styles */
+.fc_parameter__panel {
+ margin: 30px 0;
+}
+
+.fc_parameter__panel h3 {
+ margin-bottom: 5px;
+}
+
+.fc_parameter__panel h6 {
+ margin-top: 0;
+ margin-bottom: 12px;
+}
</style>
diff --git a/src-vue/src/views/ThunderstoreModsView.vue b/src-vue/src/views/ThunderstoreModsView.vue
deleted file mode 100644
index 5b74bd1c..00000000
--- a/src-vue/src/views/ThunderstoreModsView.vue
+++ /dev/null
@@ -1,186 +0,0 @@
-<template>
- <div class="fc-container">
- <div v-if="mods.length === 0" class="fc__changelog__container">
- <el-progress :show-text="false" :percentage="50" :indeterminate="true" />
- </div>
- <el-scrollbar v-else class="container">
- <div class="card-container">
- <!-- Search filters -->
- <div class="filter_container">
- <el-input v-model="input" placeholder="Search" clearable @input="onFilterTextChange" />
- <!-- Message displayed when user is typing in search bar -->
- <div v-if="userIsTyping" class="modMessage search">
- Searching mods...
- </div>
- </div>
-
- <!-- Message displayed if no mod matched searched words -->
- <div v-if="filteredMods.length === 0 && input.length !== 0 && !userIsTyping" class="modMessage">
- No matching mod has been found.<br/>
- Try another search!
- </div>
-
- <!-- Mod cards -->
- <thunderstore-mod-card v-for="mod of modsList" v-bind:key="mod.name" :mod="mod" />
- </div>
- </el-scrollbar>
- </div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import { ThunderstoreMod } from "../utils/thunderstore/ThunderstoreMod";
-import ThunderstoreModCard from "../components/ThunderstoreModCard.vue";
-
-export default defineComponent({
- name: "ThunderstoreModsView",
- components: {ThunderstoreModCard},
- async mounted() {
- this.$store.commit('fetchThunderstoreMods');
- },
- computed: {
- mods(): ThunderstoreMod[] {
- return this.$store.state.thunderstoreMods;
- },
- modsList(): ThunderstoreMod[] {
- return this.input.length === 0 || this.userIsTyping ? this.mods : this.filteredMods;
- }
- },
- data() {
- return {
- input: '',
- filteredMods: [] as ThunderstoreMod[],
- modsBeingInstalled: [] as string[],
- userIsTyping: false,
- debouncedSearch: this.debounce((i: string) => this.filterMods(i))
- };
- },
- methods: {
- /**
- * This is a debounced version of the filterMods method, that calls
- * filterMods when user has stopped typing in the search bar (i.e.
- * waits 300ms).
- * It allows not to trigger filtering method (which is costly) each
- * time user inputs a character.
- */
- onFilterTextChange (searchString: string) {
- this.debouncedSearch(searchString);
- },
-
- /**
- * This method is called each time search input is modified, and
- * filters mods matching the input string.
- *
- * This converts research string and all researched fields to
- * lower case, to match mods regardless of font case.
- */
- filterMods(value: string) {
- if (value === '') {
- this.filteredMods = [];
- return;
- }
-
- const searchValue = value.toLowerCase();
-
- this.filteredMods = this.mods.filter((mod: ThunderstoreMod) => {
- return mod.name.toLowerCase().includes(searchValue)
- || mod.owner.toLowerCase().includes(searchValue)
- || mod.versions[0].description.toLowerCase().includes(searchValue);
- });
- },
-
- /**
- * This debounces a method, i.e. it prevents input method from being called
- * multiple times in a short period of time.
- * Stolen from https://www.freecodecamp.org/news/javascript-debounce-example/
- */
- debounce (func: Function, timeout = 200) {
- let timer: any;
- return (...args: any) => {
- this.userIsTyping = true;
- clearTimeout(timer);
- timer = setTimeout(() => {
- this.userIsTyping = false;
- func.apply(this, args);
- }, timeout);
- };
- }
- }
-});
-</script>
-
-<style scoped>
-.fc__changelog__container {
- padding: 20px 30px;
-}
-
-.el-timeline-item__timestamp {
- color: white !important;
- user-select: none !important;
-}
-
-.filter_container {
- margin: 5px;
-}
-
-.el-input {
- max-width: 300px;
-}
-
-.search {
- display: inline-block;
- margin: 0 0 0 10px !important;
-}
-
-.modMessage {
- color: white;
- margin: 20px 5px;
-}
-
-.card-container {
- margin: 0 auto;
-}
-
-/* Card container dynamic size */
-@media (max-width: 1000px) {
- .card-container {
- width: 752px;
- }
-}
-
-@media (max-width: 812px) {
- .card-container {
- width: 574px;
- }
-}
-
-@media (max-width: 624px) {
- .card-container {
- width: 376px;
- }
-}
-
-@media (min-width: 1000px) {
- .card-container {
- width: 940px;
- }
-}
-
-@media (min-width: 1188px) {
- .card-container {
- width: 1128px;
- }
-}
-
-@media (min-width: 1376px) {
- .card-container {
- width: 1316px;
- }
-}
-
-@media (min-width: 1565px) {
- .card-container {
- width: 1505px;
- }
-}
-</style>
diff --git a/src-vue/src/views/mods/LocalModsView.vue b/src-vue/src/views/mods/LocalModsView.vue
new file mode 100644
index 00000000..470ab4f7
--- /dev/null
+++ b/src-vue/src/views/mods/LocalModsView.vue
@@ -0,0 +1,122 @@
+<template>
+ <el-scrollbar>
+ <div>
+ <p v-if="mods.length === 0">No mods were found.</p>
+ <el-card v-else shadow="hover" v-for="mod in mods" v-bind:key="mod.name">
+ <el-switch style="--el-switch-on-color: #13ce66; --el-switch-off-color: #8957e5" v-model="mod.enabled"
+ :before-change="() => updateWhichModsEnabled(mod)" :loading="global_load_indicator" />
+ <el-popconfirm
+ title="Are you sure to delete this mod?"
+ @confirm="deleteMod(mod)"
+ >
+ <template #reference>
+ <el-button type="danger">Delete</el-button>
+ </template>
+ </el-popconfirm>
+ {{ mod.name }}
+ </el-card>
+ </div>
+ </el-scrollbar>
+</template>
+
+<script lang="ts">
+import { invoke } from '@tauri-apps/api';
+import { ElNotification } from 'element-plus';
+import { defineComponent } from 'vue';
+import { GameInstall } from '../../utils/GameInstall';
+import { NorthstarMod } from '../../utils/NorthstarMod';
+
+export default defineComponent({
+ name: 'LocalModsView',
+ computed: {
+ installedMods(): NorthstarMod[] {
+ return this.$store.state.installed_mods;
+ },
+ searchValue(): string {
+ return this.$store.getters.searchWords;
+ },
+ mods(): NorthstarMod[] {
+ if (this.searchValue.length === 0) {
+ return this.installedMods;
+ }
+
+ return this.installedMods.filter((mod: NorthstarMod) => {
+ return mod.name.toLowerCase().includes(this.searchValue);
+ });
+ }
+ },
+ data() {
+ return {
+ global_load_indicator: false,
+ };
+ },
+ methods: {
+ async updateWhichModsEnabled(mod: NorthstarMod) {
+ this.global_load_indicator = true;
+
+ // Setup up struct
+ let game_install = {
+ game_path: this.$store.state.game_path,
+ install_type: this.$store.state.install_type
+ } as GameInstall;
+
+ // enable/disable specific mod
+ try {
+ await invoke("set_mod_enabled_status", {
+ gameInstall: game_install,
+ modName: mod.name,
+ // Need to set it to the opposite of current state,
+ // as current state is only updated after command is run
+ isEnabled: !mod.enabled,
+ })
+ }
+ catch (error) {
+ ElNotification({
+ title: 'Error',
+ message: `${error}`,
+ type: 'error',
+ position: 'bottom-right'
+ });
+ this.global_load_indicator = false;
+ return false;
+ }
+
+ this.global_load_indicator = false;
+ return true;
+ },
+ async deleteMod(mod: NorthstarMod) {
+ let game_install = {
+ game_path: this.$store.state.game_path,
+ install_type: this.$store.state.install_type
+ } as GameInstall;
+ await invoke("delete_northstar_mod", { gameInstall: game_install, nsmodName: mod.name })
+ .then((message) => {
+ // Just a visual indicator that it worked
+ ElNotification({
+ title: `Success deleting ${mod.name}`,
+ type: 'success',
+ position: 'bottom-right'
+ });
+ })
+ .catch((error) => {
+ ElNotification({
+ title: 'Error',
+ message: error,
+ type: 'error',
+ position: 'bottom-right'
+ });
+ })
+ .finally(() => {
+ this.$store.commit('loadInstalledMods');
+ });
+ },
+ },
+ mounted() {
+ this.$store.commit('loadInstalledMods');
+ }
+})
+</script>
+
+<style scoped>
+
+</style>
diff --git a/src-vue/src/views/mods/ThunderstoreModsView.vue b/src-vue/src/views/mods/ThunderstoreModsView.vue
new file mode 100644
index 00000000..aaf15220
--- /dev/null
+++ b/src-vue/src/views/mods/ThunderstoreModsView.vue
@@ -0,0 +1,272 @@
+<template>
+ <div class="fc-container" style="padding: 0">
+ <div v-if="mods.length === 0" class="fc__changelog__container">
+ <el-progress :show-text="false" :percentage="50" :indeterminate="true" />
+ </div>
+ <el-scrollbar v-else class="container" ref="scrollbar">
+ <div class="card-container">
+ <div class="pagination_container">
+ <el-pagination
+ v-if="shouldDisplayPagination"
+ :currentPage="currentPageIndex + 1"
+ layout="prev, pager, next"
+ :page-size="modsPerPage"
+ :total="modsList.length"
+ @current-change="(e: number) => currentPageIndex = e - 1"
+ />
+ </div>
+
+ <!-- Message displayed if no mod matched searched words -->
+ <div v-if="filteredMods.length === 0" class="modMessage">
+ No matching mod has been found.<br/>
+ Try another search!
+ </div>
+
+ <!-- Mod cards -->
+ <thunderstore-mod-card v-for="mod of currentPageMods" v-bind:key="mod.name" :mod="mod" />
+ </div>
+
+ <!-- Bottom pagination -->
+ <div class="card-container">
+ <div class="pagination_container">
+ <el-pagination
+ class="fc_bottom__pagination"
+ v-if="shouldDisplayPagination"
+ :currentPage="currentPageIndex + 1"
+ layout="prev, pager, next"
+ :page-size="modsPerPage"
+ :total="modsList.length"
+ @current-change="onBottomPaginationChange"
+ />
+ </div>
+ </div>
+ </el-scrollbar>
+ </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, ref } from 'vue';
+import { ThunderstoreMod } from "../../utils/thunderstore/ThunderstoreMod";
+import ThunderstoreModCard from "../../components/ThunderstoreModCard.vue";
+import { ElScrollbar, ScrollbarInstance } from "element-plus";
+import { SortOptions } from "../../utils/SortOptions.d";
+import { ThunderstoreModVersion } from '../../utils/thunderstore/ThunderstoreModVersion';
+
+export default defineComponent({
+ name: "ThunderstoreModsView",
+ components: {ThunderstoreModCard},
+ async mounted() {
+ this.$store.commit('fetchThunderstoreMods');
+ },
+ computed: {
+ searchValue(): string {
+ return this.$store.getters.searchWords;
+ },
+ selectedCategories(): Object[] {
+ return this.$store.state.search.selectedCategories;
+ },
+ modSorting(): SortOptions {
+ return Object.values(SortOptions)[Object.keys(SortOptions).indexOf(this.$store.state.search.sortValue)];
+ },
+ mods(): ThunderstoreMod[] {
+ return this.$store.state.thunderstoreMods;
+ },
+ filteredMods(): ThunderstoreMod[] {
+ if (this.searchValue.length === 0 && this.selectedCategories.length === 0) {
+ return this.mods;
+ }
+
+ return this.mods.filter((mod: ThunderstoreMod) => {
+ // Filter with search words (only if search field isn't empty)
+ const inputMatches: boolean = this.searchValue.length === 0
+ || (mod.name.toLowerCase().includes(this.searchValue)
+ || mod.owner.toLowerCase().includes(this.searchValue)
+ || mod.versions[0].description.toLowerCase().includes(this.searchValue));
+
+ // Filter with categories (only if some categories are selected)
+ const categoriesMatch: boolean = this.selectedCategories.length === 0
+ || mod.categories
+ .filter((category: string) => this.selectedCategories.includes(category))
+ .length === this.selectedCategories.length;
+
+ return inputMatches && categoriesMatch;
+ });
+ },
+ modsList(): ThunderstoreMod[] {
+ // Use filtered mods if user is searching, vanilla list otherwise.
+ const mods: ThunderstoreMod[] = this.searchValue.length !== 0 || this.selectedCategories.length !== 0
+ ? this.filteredMods
+ : this.mods;
+
+ // Sort mods regarding user selected algorithm.
+ let compare: (a: ThunderstoreMod, b: ThunderstoreMod) => number;
+ switch(this.modSorting) {
+ case SortOptions.NAME_ASC:
+ compare = (a: ThunderstoreMod, b: ThunderstoreMod) => a.name.localeCompare(b.name);
+ break;
+ case SortOptions.NAME_DESC:
+ compare = (a: ThunderstoreMod, b: ThunderstoreMod) => -1 * a.name.localeCompare(b.name);
+ break;
+ case SortOptions.DATE_ASC:
+ compare = (a: ThunderstoreMod, b: ThunderstoreMod) => a.date_updated.localeCompare(b.date_updated);
+ break;
+ case SortOptions.DATE_DESC:
+ compare = (a: ThunderstoreMod, b: ThunderstoreMod) => -1 * a.date_updated.localeCompare(b.date_updated);
+ break;
+ case SortOptions.MOST_DOWNLOADED:
+ compare = (a: ThunderstoreMod, b: ThunderstoreMod) => {
+ const aTotal = a.versions.reduce((prev, next) => {
+ return {downloads: prev.downloads + next.downloads} as ThunderstoreModVersion;
+ }).downloads;
+ const bTotal = b.versions.reduce((prev, next) => {
+ return {downloads: prev.downloads + next.downloads} as ThunderstoreModVersion;
+ }).downloads;
+ return -1 * (aTotal - bTotal);
+ };
+ break;
+ case SortOptions.TOP_RATED:
+ compare = (a: ThunderstoreMod, b: ThunderstoreMod) => -1 * (a.rating_score - b.rating_score);
+ break;
+ default:
+ throw new Error('Unknown mod sorting.');
+ }
+
+ return mods.sort(compare);
+ },
+ modsPerPage(): number {
+ return parseInt(this.$store.state.mods_per_page);
+ },
+ currentPageMods(): ThunderstoreMod[] {
+ // User might want to display all mods on one page.
+ const perPageValue = this.modsPerPage != 0 ? this.modsPerPage : this.modsList.length;
+
+ const startIndex = this.currentPageIndex * perPageValue;
+ const endIndexCandidate = startIndex + perPageValue;
+ const endIndex = endIndexCandidate > this.modsList.length ? this.modsList.length : endIndexCandidate;
+ return this.modsList.slice(startIndex, endIndex);
+ },
+ shouldDisplayPagination(): boolean {
+ return this.modsPerPage != 0 && this.modsList.length > this.modsPerPage;
+ }
+ },
+ data() {
+ return {
+ modsBeingInstalled: [] as string[],
+ currentPageIndex: 0
+ };
+ },
+ methods: {
+ /**
+ * This updates current pagination and scrolls view to the top.
+ */
+ onBottomPaginationChange(index: number) {
+ this.currentPageIndex = index - 1;
+ (this.$refs.scrollbar as ScrollbarInstance).scrollTo({ top: 0, behavior: 'smooth' });
+ }
+ },
+ watch: {
+ searchValue(_: string, __: string) {
+ if (this.currentPageIndex !== 0) {
+ this.currentPageIndex = 0;
+ }
+ },
+ selectedCategories(_: string[], __: string[]) {
+ if (this.currentPageIndex !== 0) {
+ this.currentPageIndex = 0;
+ }
+ }
+ }
+});
+</script>
+
+<style scoped>
+.fc__changelog__container {
+ padding: 20px 30px;
+}
+
+.fc-container:deep(.el-scrollbar__view) {
+ padding-left: 0;
+ padding-right: 0;
+}
+
+.el-timeline-item__timestamp {
+ color: white !important;
+ user-select: none !important;
+}
+
+.search {
+ display: inline-block;
+ margin: 0 0 0 10px !important;
+}
+
+.modMessage {
+ color: white;
+ margin: 20px 5px;
+}
+
+.card-container {
+ margin: 0 auto;
+}
+
+.pagination_container {
+ margin: 5px auto;
+ padding: 0 5px;
+ max-width: 1000px;
+ justify-content: center;
+ display: flex;
+}
+
+.el-pagination {
+ margin: 0;
+}
+
+.fc_bottom__pagination {
+ padding-bottom: 20px !important;
+ padding-right: 10px;
+}
+
+/* Card container dynamic size */
+
+.card-container {
+ --thunderstore-mod-card-width: 178px;
+ --thunderstore-mod-card-margin: 5px;
+ --thunderstore-mod-card-columns-count: 1;
+
+ width: calc(var(--thunderstore-mod-card-width) * var(--thunderstore-mod-card-columns-count) + var(--thunderstore-mod-card-margin) * 2 * var(--thunderstore-mod-card-columns-count));
+}
+@media (min-width: 628px) {
+ .card-container {
+ --thunderstore-mod-card-columns-count: 2;
+ }
+}
+@media (min-width: 836px) {
+ .card-container {
+ --thunderstore-mod-card-columns-count: 3;
+ }
+}
+@media (min-width: 1006px) {
+ .card-container {
+ --thunderstore-mod-card-columns-count: 4;
+ }
+}
+@media (min-width: 1196px) {
+ .card-container {
+ --thunderstore-mod-card-columns-count: 5;
+ }
+}
+@media (min-width: 1386px) {
+ .card-container {
+ --thunderstore-mod-card-columns-count: 6;
+ }
+}
+@media (min-width: 1576px) {
+ .card-container {
+ --thunderstore-mod-card-columns-count: 7;
+ }
+}
+@media (min-width: 1766px) {
+ .card-container {
+ --thunderstore-mod-card-columns-count: 8;
+ }
+}
+</style>
diff --git a/src-vue/tsconfig.json b/src-vue/tsconfig.json
index d4aefa2c..7f0f0012 100644
--- a/src-vue/tsconfig.json
+++ b/src-vue/tsconfig.json
@@ -10,7 +10,7 @@
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
- "lib": ["ESNext", "DOM"],
+ "lib": ["ESNext", "DOM", "ES2018"],
"skipLibCheck": true
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],