diff --git a/Cargo.lock b/Cargo.lock index c40413bbf..d89677e07 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4609,7 +4609,7 @@ dependencies = [ [[package]] name = "theseus" -version = "0.4.0" +version = "0.5.0" dependencies = [ "async-recursion", "async-tungstenite", @@ -4654,7 +4654,7 @@ dependencies = [ [[package]] name = "theseus_cli" -version = "0.4.0" +version = "0.5.0" dependencies = [ "argh", "color-eyre", @@ -4681,7 +4681,7 @@ dependencies = [ [[package]] name = "theseus_gui" -version = "0.4.0" +version = "0.5.0" dependencies = [ "chrono", "cocoa", diff --git a/theseus/Cargo.toml b/theseus/Cargo.toml index d6f0300c2..30c3dc8ed 100644 --- a/theseus/Cargo.toml +++ b/theseus/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "theseus" -version = "0.4.0" +version = "0.5.0" authors = ["Jai A "] edition = "2018" diff --git a/theseus/src/api/jre.rs b/theseus/src/api/jre.rs index eae027eb6..f83bc8b8f 100644 --- a/theseus/src/api/jre.rs +++ b/theseus/src/api/jre.rs @@ -6,7 +6,7 @@ use std::path::PathBuf; use crate::event::emit::{emit_loading, init_loading}; use crate::state::CredentialsStore; use crate::util::fetch::{fetch_advanced, fetch_json}; -use crate::util::io; + use crate::util::jre::extract_java_majorminor_version; use crate::{ state::JavaGlobals, @@ -117,10 +117,6 @@ pub async fn auto_install_java(java_version: u32) -> crate::Result { let path = state.directories.java_versions_dir().await; - if path.exists() { - io::remove_dir_all(&path).await?; - } - let mut archive = zip::ZipArchive::new(std::io::Cursor::new(file)) .map_err(|_| { crate::Error::from(crate::ErrorKind::InputError( diff --git a/theseus/src/api/pack/install_mrpack.rs b/theseus/src/api/pack/install_mrpack.rs index 1723d5462..f9b2e57ac 100644 --- a/theseus/src/api/pack/install_mrpack.rs +++ b/theseus/src/api/pack/install_mrpack.rs @@ -273,9 +273,7 @@ pub async fn install_zipped_mrpack_files( profile::edit_icon(&profile_path, Some(&potential_icon)).await?; } - if let Some(profile_val) = - crate::api::profile::get(&profile_path, None).await? - { + if let Some(profile_val) = profile::get(&profile_path, None).await? { crate::launcher::install_minecraft(&profile_val, Some(loading_bar)) .await?; diff --git a/theseus/src/api/profile/mod.rs b/theseus/src/api/profile/mod.rs index 1fc712c50..6791f1153 100644 --- a/theseus/src/api/profile/mod.rs +++ b/theseus/src/api/profile/mod.rs @@ -10,6 +10,7 @@ use crate::pack::install_from::{ use crate::prelude::{JavaVersion, ProfilePathId, ProjectPathId}; use crate::state::ProjectMetadata; +use crate::util::fetch; use crate::util::io::{self, IOError}; use crate::{ auth::{self, refresh}, @@ -22,6 +23,7 @@ pub use crate::{ }; use async_zip::tokio::write::ZipFileWriter; use async_zip::{Compression, ZipEntryBuilder}; +use serde_json::json; use std::collections::HashMap; @@ -878,6 +880,65 @@ pub async fn run_credentials( Ok(mc_process) } +/// Update playtime- sending a request to the server to update the playtime +#[tracing::instrument] +#[theseus_macros::debug_pin] +pub async fn try_update_playtime(path: &ProfilePathId) -> crate::Result<()> { + let state = State::get().await?; + + let profile = get(path, None).await?.ok_or_else(|| { + crate::ErrorKind::OtherError(format!( + "Tried to update playtime for a nonexistent or unloaded profile at path {}!", + path + )) + })?; + let updated_recent_playtime = profile.metadata.recent_time_played; + + let res = if updated_recent_playtime > 0 { + // Create update struct to send to Labrinth + let modrinth_pack_version_id = + profile.metadata.linked_data.and_then(|l| l.version_id); + let playtime_update_json = json!({ + "seconds": updated_recent_playtime, + "loader": profile.metadata.loader.to_string(), + "game_version": profile.metadata.game_version, + "parent": modrinth_pack_version_id, + }); + // Copy this struct for every Modrinth project in the profile + let mut hashmap: HashMap = HashMap::new(); + for (_, project) in profile.projects { + if let ProjectMetadata::Modrinth { version, .. } = project.metadata + { + hashmap.insert(version.id, playtime_update_json.clone()); + } + } + + let creds = state.credentials.read().await; + fetch::post_json( + "https://api.modrinth.com/analytics/playtime", + serde_json::to_value(hashmap)?, + &state.fetch_semaphore, + &creds, + ) + .await + } else { + Ok(()) + }; + + // If successful, update the profile metadata to match submitted + if res.is_ok() { + let mut profiles = state.profiles.write().await; + if let Some(profile) = profiles.0.get_mut(path) { + profile.metadata.submitted_time_played += updated_recent_playtime; + profile.metadata.recent_time_played = 0; + } + } + // Sync either way + State::sync().await?; + + res +} + fn get_modrinth_pack_list(packfile: &PackFormat) -> Vec { packfile .files diff --git a/theseus/src/launcher/mod.rs b/theseus/src/launcher/mod.rs index 2e7162aa8..f9d546f61 100644 --- a/theseus/src/launcher/mod.rs +++ b/theseus/src/launcher/mod.rs @@ -103,6 +103,7 @@ pub async fn install_minecraft( profile: &Profile, existing_loading_bar: Option, ) -> crate::Result<()> { + let sync_projects = existing_loading_bar.is_some(); let loading_bar = init_or_edit_loading( existing_loading_bar, LoadingBarType::MinecraftDownload { @@ -123,6 +124,10 @@ pub async fn install_minecraft( .await?; State::sync().await?; + if sync_projects { + Profile::sync_projects_task(profile.profile_id(), true); + } + let state = State::get().await?; let instance_path = &io::canonicalize(&profile.get_profile_full_path().await?)?; diff --git a/theseus/src/state/children.rs b/theseus/src/state/children.rs index 78438cced..efa2b0424 100644 --- a/theseus/src/state/children.rs +++ b/theseus/src/state/children.rs @@ -1,4 +1,5 @@ use super::{Profile, ProfilePathId}; +use chrono::{DateTime, Utc}; use std::path::{Path, PathBuf}; use std::process::ExitStatus; use std::{collections::HashMap, sync::Arc}; @@ -12,6 +13,7 @@ use tracing::error; use crate::event::emit::emit_process; use crate::event::ProcessPayloadType; +use crate::profile; use crate::util::io::IOError; use tokio::task::JoinHandle; @@ -29,6 +31,7 @@ pub struct MinecraftChild { pub manager: Option>>, // None when future has completed and been handled pub current_child: Arc>, pub output: SharedOutput, + pub last_updated_playtime: DateTime, // The last time we updated the playtime for the associated profile } impl Children { @@ -94,6 +97,7 @@ impl Children { post_command, pid, current_child.clone(), + profile_relative_path.clone(), ))); emit_process( @@ -104,6 +108,8 @@ impl Children { ) .await?; + let last_updated_playtime = Utc::now(); + // Create MinecraftChild let mchild = MinecraftChild { uuid, @@ -111,6 +117,7 @@ impl Children { current_child, output: shared_output, manager, + last_updated_playtime, }; let mchild = Arc::new(RwLock::new(mchild)); @@ -128,11 +135,13 @@ impl Children { post_command: Option, mut current_pid: u32, current_child: Arc>, + associated_profile: ProfilePathId, ) -> crate::Result { let current_child = current_child.clone(); // Wait on current Minecraft Child let mut mc_exit_status; + let mut last_updated_playtime = Utc::now(); loop { if let Some(t) = current_child .write() @@ -145,8 +154,61 @@ impl Children { } // sleep for 10ms tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; + + // Auto-update playtime every minute + let diff = Utc::now() + .signed_duration_since(last_updated_playtime) + .num_seconds(); + if diff >= 60 { + if let Err(e) = + profile::edit(&associated_profile, |mut prof| { + prof.metadata.recent_time_played += diff as u64; + async { Ok(()) } + }) + .await + { + tracing::warn!( + "Failed to update playtime for profile {}: {}", + associated_profile, + e + ); + } + last_updated_playtime = Utc::now(); + } } + // Now fully complete- update playtime one last time + let diff = Utc::now() + .signed_duration_since(last_updated_playtime) + .num_seconds(); + if let Err(e) = profile::edit(&associated_profile, |mut prof| { + prof.metadata.recent_time_played += diff as u64; + async { Ok(()) } + }) + .await + { + tracing::warn!( + "Failed to update playtime for profile {}: {}", + associated_profile, + e + ); + } + + // Publish play time update + // Allow failure, it will be stored locally and sent next time + // Sent in another thread as first call may take a couple seconds and hold up process ending + tokio::spawn(async move { + if let Err(e) = + profile::try_update_playtime(&associated_profile).await + { + tracing::warn!( + "Failed to update playtime for profile {}: {}", + associated_profile, + e + ); + } + }); + { // Clear game played for Discord RPC // May have other active processes, so we clear to the next running process diff --git a/theseus/src/state/discord.rs b/theseus/src/state/discord.rs index 2adb4bb7d..a9d2001f9 100644 --- a/theseus/src/state/discord.rs +++ b/theseus/src/state/discord.rs @@ -99,6 +99,7 @@ impl DiscordGuard { Ok(()) } + /* /// Clear the activity pub async fn clear_activity( &self, @@ -137,7 +138,7 @@ impl DiscordGuard { res.map_err(could_not_clear_err)?; } Ok(()) - } + }*/ /// Clear the activity, but if there is a running profile, set the activity to that instead pub async fn clear_to_default( @@ -160,7 +161,7 @@ impl DiscordGuard { ) .await?; } else { - self.clear_activity(reconnect_if_fail).await?; + self.set_activity("Idling...", reconnect_if_fail).await?; } Ok(()) } diff --git a/theseus/src/state/mod.rs b/theseus/src/state/mod.rs index 24ace5c18..4276e62f2 100644 --- a/theseus/src/state/mod.rs +++ b/theseus/src/state/mod.rs @@ -156,7 +156,7 @@ impl State { ))); emit_loading(&loading_bar, 10.0, None).await?; - let is_offline = !fetch::check_internet(&fetch_semaphore, 3).await; + let is_offline = !fetch::check_internet(3).await; let metadata_fut = Metadata::init(&directories, !is_offline, &io_semaphore); @@ -185,6 +185,10 @@ impl State { let safety_processes = SafeProcesses::new(); let discord_rpc = DiscordGuard::init().await?; + { + // Add default Idling to discord rich presence + let _ = discord_rpc.set_activity("Idling...", true).await; + } // Starts a loop of checking if we are online, and updating Self::offine_check_loop(); @@ -323,7 +327,7 @@ impl State { /// Refreshes whether or not the launcher should be offline, by whether or not there is an internet connection pub async fn refresh_offline(&self) -> crate::Result<()> { - let is_online = fetch::check_internet(&self.fetch_semaphore, 3).await; + let is_online = fetch::check_internet(3).await; let mut offline = self.offline.write().await; @@ -341,7 +345,7 @@ pub async fn init_watcher() -> crate::Result> { let (mut tx, mut rx) = channel(1); let file_watcher = new_debouncer( - Duration::from_secs_f32(0.25), + Duration::from_secs_f32(2.0), None, move |res: DebounceEventResult| { futures::executor::block_on(async { @@ -394,7 +398,10 @@ pub async fn init_watcher() -> crate::Result> { Profile::crash_task(profile_path_id); } else if !visited_paths.contains(&new_path) { if subfile { - Profile::sync_projects_task(profile_path_id); + Profile::sync_projects_task( + profile_path_id, + false, + ); visited_paths.push(new_path); } else { Profiles::sync_available_profiles_task( diff --git a/theseus/src/state/mr_auth.rs b/theseus/src/state/mr_auth.rs index 54ae3f617..ac54ad538 100644 --- a/theseus/src/state/mr_auth.rs +++ b/theseus/src/state/mr_auth.rs @@ -222,7 +222,7 @@ pub async fn login_password( ) -> crate::Result { let resp = fetch_advanced( Method::POST, - &format!("https://{MODRINTH_API_URL}auth/login"), + &format!("{MODRINTH_API_URL}auth/login"), None, Some(serde_json::json!({ "username": username, diff --git a/theseus/src/state/profiles.rs b/theseus/src/state/profiles.rs index bb278c0c8..8f9a6b587 100644 --- a/theseus/src/state/profiles.rs +++ b/theseus/src/state/profiles.rs @@ -183,6 +183,10 @@ pub struct ProfileMetadata { pub date_modified: DateTime, #[serde(skip_serializing_if = "Option::is_none")] pub last_played: Option>, + #[serde(default)] + pub submitted_time_played: u64, + #[serde(default)] + pub recent_time_played: u64, } #[derive(Serialize, Deserialize, Clone, Debug)] @@ -265,6 +269,8 @@ impl Profile { date_created: Utc::now(), date_modified: Utc::now(), last_played: None, + submitted_time_played: 0, + recent_time_played: 0, }, projects: HashMap::new(), java: None, @@ -324,7 +330,7 @@ impl Profile { }); } - pub fn sync_projects_task(profile_path_id: ProfilePathId) { + pub fn sync_projects_task(profile_path_id: ProfilePathId, force: bool) { tokio::task::spawn(async move { let span = tracing::span!(tracing::Level::INFO, "sync_projects_task"); @@ -339,32 +345,34 @@ impl Profile { let profile = crate::api::profile::get(&profile_path_id, None).await?; if let Some(profile) = profile { - let paths = profile.get_profile_full_project_paths().await?; + if profile.install_stage != ProfileInstallStage::PackInstalling || force { + let paths = profile.get_profile_full_project_paths().await?; - let caches_dir = state.directories.caches_dir(); - let creds = state.credentials.read().await; - let projects = crate::state::infer_data_from_files( - profile.clone(), - paths, - caches_dir, - &state.io_semaphore, - &state.fetch_semaphore, - &creds, - ) - .await?; - drop(creds); + let caches_dir = state.directories.caches_dir(); + let creds = state.credentials.read().await; + let projects = crate::state::infer_data_from_files( + profile.clone(), + paths, + caches_dir, + &state.io_semaphore, + &state.fetch_semaphore, + &creds, + ) + .await?; + drop(creds); - let mut new_profiles = state.profiles.write().await; - if let Some(profile) = new_profiles.0.get_mut(&profile_path_id) { - profile.projects = projects; + let mut new_profiles = state.profiles.write().await; + if let Some(profile) = new_profiles.0.get_mut(&profile_path_id) { + profile.projects = projects; + } + emit_profile( + profile.uuid, + &profile_path_id, + &profile.metadata.name, + ProfilePayloadType::Synced, + ) + .await?; } - emit_profile( - profile.uuid, - &profile_path_id, - &profile.metadata.name, - ProfilePayloadType::Synced, - ) - .await?; } else { tracing::warn!( "Unable to fetch single profile projects: path {profile_path_id} invalid", @@ -980,7 +988,7 @@ impl Profiles { .await?, ) .await?; - Profile::sync_projects_task(profile_path_id); + Profile::sync_projects_task(profile_path_id, false); } Ok::<(), crate::Error>(()) } diff --git a/theseus/src/util/fetch.rs b/theseus/src/util/fetch.rs index 72ebe557e..3637378b2 100644 --- a/theseus/src/util/fetch.rs +++ b/theseus/src/util/fetch.rs @@ -213,18 +213,41 @@ pub async fn fetch_mirrors( } /// Using labrinth API, checks if an internet response can be found, with a timeout in seconds -#[tracing::instrument(skip(semaphore))] +#[tracing::instrument] #[theseus_macros::debug_pin] -pub async fn check_internet(semaphore: &FetchSemaphore, timeout: u64) -> bool { - let result = fetch( - "https://api.modrinth.com", - None, - semaphore, - &CredentialsStore(None), - ); - let result = - tokio::time::timeout(Duration::from_secs(timeout), result).await; - matches!(result, Ok(Ok(_))) +pub async fn check_internet(timeout: u64) -> bool { + REQWEST_CLIENT + .get("https://launcher-files.modrinth.com/detect.txt") + .timeout(Duration::from_secs(timeout)) + .send() + .await + .is_ok() +} + +/// Posts a JSON to a URL +#[tracing::instrument(skip(json_body, semaphore))] +#[theseus_macros::debug_pin] +pub async fn post_json( + url: &str, + json_body: serde_json::Value, + semaphore: &FetchSemaphore, + credentials: &CredentialsStore, +) -> crate::Result +where + T: DeserializeOwned, +{ + let io_semaphore = semaphore.0.read().await; + let _permit = io_semaphore.acquire().await?; + + let mut req = REQWEST_CLIENT.post(url).json(&json_body); + if let Some(creds) = &credentials.0 { + req = req.header("Authorization", &creds.session); + } + + let result = req.send().await?.error_for_status()?; + + let value = result.json().await?; + Ok(value) } pub async fn read_json( diff --git a/theseus_cli/Cargo.toml b/theseus_cli/Cargo.toml index 73aa8915d..762d23120 100644 --- a/theseus_cli/Cargo.toml +++ b/theseus_cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "theseus_cli" -version = "0.4.0" +version = "0.5.0" authors = ["Jai A "] edition = "2018" diff --git a/theseus_gui/package.json b/theseus_gui/package.json index 0dbba1653..273e5effd 100644 --- a/theseus_gui/package.json +++ b/theseus_gui/package.json @@ -1,7 +1,7 @@ { "name": "theseus_gui", "private": true, - "version": "0.4.0", + "version": "0.5.0", "type": "module", "scripts": { "dev": "vite", @@ -18,7 +18,7 @@ "floating-vue": "^2.0.0-beta.20", "mixpanel-browser": "^2.47.0", "ofetch": "^1.0.1", - "omorphia": "^0.4.34", + "omorphia": "^0.4.35", "pinia": "^2.1.3", "qrcode.vue": "^3.4.0", "tauri-plugin-window-state-api": "github:tauri-apps/tauri-plugin-window-state#v1", diff --git a/theseus_gui/pnpm-lock.yaml b/theseus_gui/pnpm-lock.yaml index 80983ca43..027d36dfa 100644 --- a/theseus_gui/pnpm-lock.yaml +++ b/theseus_gui/pnpm-lock.yaml @@ -21,8 +21,8 @@ dependencies: specifier: ^1.0.1 version: 1.0.1 omorphia: - specifier: ^0.4.34 - version: 0.4.34 + specifier: ^0.4.35 + version: 0.4.35 pinia: specifier: ^2.1.3 version: 2.1.3(vue@3.3.4) @@ -1348,8 +1348,8 @@ packages: ufo: 1.1.2 dev: false - /omorphia@0.4.34: - resolution: {integrity: sha512-6uAH1kgzbYYmJDM41Vy4/MhzT9kRj+s1t8IknHKeOQqmVft+wPtv/pbA7pqTMfCzBOarLKKO5s4sNlz8TeMmaQ==} + /omorphia@0.4.35: + resolution: {integrity: sha512-ZxA6sJKWZbiG49l/gTG25cxAvTcIfVSLhuIV2e+LSY0nwkZO4EFvxhzGNz0exR3lVs+OdDCdJyb1U2QYMVbVrA==} dependencies: dayjs: 1.11.7 floating-vue: 2.0.0-beta.20(vue@3.3.4) diff --git a/theseus_gui/src-tauri/Cargo.toml b/theseus_gui/src-tauri/Cargo.toml index c95589920..de8b7a061 100644 --- a/theseus_gui/src-tauri/Cargo.toml +++ b/theseus_gui/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "theseus_gui" -version = "0.4.0" +version = "0.5.0" description = "A Tauri App" authors = ["you"] license = "" diff --git a/theseus_gui/src-tauri/tauri.conf.json b/theseus_gui/src-tauri/tauri.conf.json index 988242373..0ad4f999a 100644 --- a/theseus_gui/src-tauri/tauri.conf.json +++ b/theseus_gui/src-tauri/tauri.conf.json @@ -8,7 +8,7 @@ }, "package": { "productName": "Modrinth App", - "version": "0.4.0" + "version": "0.5.0" }, "tauri": { "allowlist": { @@ -83,7 +83,7 @@ } }, "security": { - "csp": "default-src 'self'; connect-src https://modrinth.com https://*.modrinth.com https://mixpanel.com https://*.mixpanel.com; font-src https://cdn-raw.modrinth.com/fonts/inter/; img-src tauri: https: data: blob: 'unsafe-inline' asset: https://asset.localhost" + "csp": "default-src 'self'; connect-src https://modrinth.com https://*.modrinth.com https://mixpanel.com https://*.mixpanel.com https://*.cloudflare.com; font-src https://cdn-raw.modrinth.com/fonts/inter/; img-src tauri: https: data: blob: 'unsafe-inline' asset: https://asset.localhost; script-src https://*.cloudflare.com; frame-src https://*.cloudflare.com; style-src unsafe-inline" }, "updater": { "active": true, @@ -96,11 +96,11 @@ "titleBarStyle": "Overlay", "hiddenTitle": true, "fullscreen": false, - "height": 650, + "height": 800, "resizable": true, "title": "Modrinth App", "width": 1280, - "minHeight": 630, + "minHeight": 700, "minWidth": 1100, "visible": false, "decorations": false diff --git a/theseus_gui/src-tauri/tauri.macos.conf.json b/theseus_gui/src-tauri/tauri.macos.conf.json index a138f6ec8..4cbb5c91d 100644 --- a/theseus_gui/src-tauri/tauri.macos.conf.json +++ b/theseus_gui/src-tauri/tauri.macos.conf.json @@ -5,11 +5,11 @@ "titleBarStyle": "Overlay", "hiddenTitle": true, "fullscreen": false, - "height": 650, + "height": 800, "resizable": true, "title": "Modrinth App", "width": 1280, - "minHeight": 630, + "minHeight": 700, "minWidth": 1100, "visible": false, "decorations": true diff --git a/theseus_gui/src/assets/external/google.svg b/theseus_gui/src/assets/external/google.svg index 19263ff27..77053fc88 100644 --- a/theseus_gui/src/assets/external/google.svg +++ b/theseus_gui/src/assets/external/google.svg @@ -1,6 +1,5 @@ { diff --git a/theseus_gui/src/components/ui/tutorial/OnboardingScreen.vue b/theseus_gui/src/components/ui/tutorial/OnboardingScreen.vue index b4cea83da..120b99c23 100644 --- a/theseus_gui/src/components/ui/tutorial/OnboardingScreen.vue +++ b/theseus_gui/src/components/ui/tutorial/OnboardingScreen.vue @@ -83,15 +83,18 @@ const finishOnboarding = async () => { async function fetchSettings() { const fetchSettings = await get().catch(handleError) + if (!fetchSettings.java_globals) { + fetchSettings.java_globals = {} + } if (!fetchSettings.java_globals.JAVA_17) { - const path = await auto_install_java(17).catch(handleError) - fetchSettings.java_globals.JAVA_17 = await get_jre(path).catch(handleError) + const path1 = await auto_install_java(17).catch(handleError) + fetchSettings.java_globals.JAVA_17 = await get_jre(path1).catch(handleError) } if (!fetchSettings.java_globals.JAVA_8) { - const path = await auto_install_java(8).catch(handleError) - fetchSettings.java_globals.JAVA_8 = await get_jre(path).catch(handleError) + const path2 = await auto_install_java(8).catch(handleError) + fetchSettings.java_globals.JAVA_8 = await get_jre(path2).catch(handleError) } await set(fetchSettings).catch(handleError) diff --git a/theseus_gui/src/helpers/fetch.js b/theseus_gui/src/helpers/fetch.js index 8c0870e08..ff3e8b62e 100644 --- a/theseus_gui/src/helpers/fetch.js +++ b/theseus_gui/src/helpers/fetch.js @@ -2,7 +2,7 @@ import { ofetch } from 'ofetch' import { handleError } from '@/store/state.js' import { getVersion } from '@tauri-apps/api/app' -export const useFetch = async (url, item) => { +export const useFetch = async (url, item, isSilent) => { try { const version = await getVersion() @@ -10,7 +10,9 @@ export const useFetch = async (url, item) => { headers: { 'User-Agent': `modrinth/theseus/${version} (support@modrinth.com)` }, }) } catch (err) { - handleError({ message: `Error fetching ${item}` }) + if (!isSilent) { + handleError({ message: `Error fetching ${item}` }) + } console.error(err) } } diff --git a/theseus_gui/src/pages/Browse.vue b/theseus_gui/src/pages/Browse.vue index cf2f4ea5f..e6374c779 100644 --- a/theseus_gui/src/pages/Browse.vue +++ b/theseus_gui/src/pages/Browse.vue @@ -237,15 +237,22 @@ async function refreshSearch() { let val = `${base}${url}` - const rawResults = await useFetch(val, 'search results') + const rawResults = await useFetch(val, 'search results', offline.value) + results.value = rawResults + if (!rawResults) { + results.value = { + hits: [], + total_hits: 0, + limit: 1, + } + } if (instanceContext.value) { - for (let val of rawResults.hits) { + for (let val of results.value) { val.installed = await check_installed(instanceContext.value.path, val.project_id).then( (x) => (val.installed = x) ) } } - results.value = rawResults } async function onSearchChange(newPageNumber) { @@ -510,7 +517,7 @@ onUnmounted(() => unlistenOffline())