Merge branch 'main' into sophie/fix-tech-rev-refresh-status

This commit is contained in:
ThatGravyBoat
2026-06-27 02:49:48 -02:30
committed by GitHub
27 changed files with 305 additions and 100 deletions
+3
View File
@@ -96,6 +96,9 @@ jobs:
rustflags: ''
cache: false
- name: Setup mold
uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 / Mold 2.41.0
- name: Cache Cargo registry and index
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
+4
View File
@@ -84,6 +84,10 @@ jobs:
rustflags: ''
target: ${{ contains(matrix.platform, 'macos') && 'x86_64-apple-darwin' || '' }}
- name: Setup mold
if: contains(matrix.platform, 'ubuntu')
uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 / Mold 2.41.0
- name: Setup sccache
uses: mozilla-actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10
+3
View File
@@ -129,6 +129,9 @@ jobs:
components: clippy, rustfmt
cache: false
- name: Setup mold
uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 / Mold 2.41.0
- name: Cache Cargo registry and index
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae #v5.0.5
with:
+5 -6
View File
@@ -1341,12 +1341,11 @@ async function processPendingSurveys() {
const creds = await getCreds().catch(handleError)
const userId = creds?.user_id
const instances = await list().catch(handleError)
const isActivePlayer =
instances.findIndex(
(instance) =>
isWithinLastTwoWeeks(instance.last_played) && !isWithinLastTwoWeeks(instance.created),
) >= 0
const instances = (await list().catch(handleError)) ?? []
const isActivePlayer = instances.some(
(instance) =>
isWithinLastTwoWeeks(instance.last_played) && !isWithinLastTwoWeeks(instance.created),
)
let surveys = []
try {
+1
View File
@@ -97,6 +97,7 @@ type ReleaseChannel = 'release' | 'beta' | 'alpha'
export type InstanceLoader = 'vanilla' | 'forge' | 'fabric' | 'quilt' | 'neoforge'
type ContentFile = {
enabled: boolean
metadata?: {
project_id: string
version_id: string
+1 -1
View File
@@ -47,7 +47,7 @@ async fn main() -> theseus::Result<()> {
for world in worlds {
println!(
"World: {:?}/{:?} played at {:?}: {:#?}",
world.profile,
world.instance_id,
world.world.name,
world.world.last_played,
world.world.details
+8 -1
View File
@@ -526,4 +526,11 @@ yahoo.com, 100033, DIRECT, e1a5b5b6e3255540
#IndexExchange
indexexchange.com, 184169, DIRECT, 50b1c356f2c5c8fc
indexexchange.com, 198988, DIRECT, 50b1c356f2c5c8fc
indexexchange.com, 198988, DIRECT, 50b1c356f2c5c8fc
#AppNexus
Xandr.com, 2398, DIRECT
#Rubicon
rubiconproject.com, 24584, DIRECT, 0bfd66d529a55807
rubiconproject.com, 24586, DIRECT, 0bfd66d529a55807
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n\t\tUPDATE instances\n\t\tSET\n\t\t\tsubmitted_time_played = CASE\n\t\t\t\tWHEN submitted_time_played < 0 THEN ?\n\t\t\t\tWHEN submitted_time_played > ? THEN ?\n\t\t\t\tELSE submitted_time_played + ?\n\t\t\tEND,\n\t\t\trecent_time_played = 0,\n\t\t\tmodified = ?\n\t\tWHERE id = ?\n\t\t",
"describe": {
"columns": [],
"parameters": {
"Right": 6
},
"nullable": []
},
"hash": "3af5ce6fdc84ce12da06b92950d0249bdc4023c0b3d0c683318079ce93c9dd56"
}
@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "\n\t\tUPDATE instances\n\t\tSET\n\t\t\tsubmitted_time_played = submitted_time_played + ?,\n\t\t\trecent_time_played = 0,\n\t\t\tmodified = ?\n\t\tWHERE id = ?\n\t\t",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
},
"hash": "44f82d6bbf66f685e20bee5c9c58587c035eb94098831c690739393870d4e814"
}
@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "\n\t\tUPDATE instances\n\t\tSET recent_time_played = recent_time_played + ?, modified = ?\n\t\tWHERE id = ?\n\t\t",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
},
"hash": "8c0ef949dc78aa5d58b8fcd3e04c9c425cfac0520785d4e25d687c2d9ea82088"
}
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n\t\tUPDATE instance_files\n\t\tSET\n\t\t\trelative_path = ?,\n\t\t\tfile_name = ?,\n\t\t\tenabled = ?,\n\t\t\tmissing = 0,\n\t\t\tmodified_at = ?\n\t\tWHERE instance_id = ? AND relative_path = ?\n\t\t",
"describe": {
"columns": [],
"parameters": {
"Right": 6
},
"nullable": []
},
"hash": "c5f78eaae528b6f930bcc9ade15384564c86cd98a1e407515ea718127a3b1777"
}
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n\t\tUPDATE instances\n\t\tSET\n\t\t\trecent_time_played = CASE\n\t\t\t\tWHEN recent_time_played < 0 THEN ?\n\t\t\t\tWHEN recent_time_played > ? THEN ?\n\t\t\t\tELSE recent_time_played + ?\n\t\t\tEND,\n\t\t\tmodified = ?\n\t\tWHERE id = ?\n\t\t",
"describe": {
"columns": [],
"parameters": {
"Right": 6
},
"nullable": []
},
"hash": "d0bdfcba97a212ad80f56c1884ec2f984ce07a6d9498f19c24c6e2c031122fc5"
}
@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "\n\t\tUPDATE instance_files\n\t\tSET\n\t\t\trelative_path = ?,\n\t\t\tfile_name = ?,\n\t\t\tenabled = ?,\n\t\t\tmodified_at = ?\n\t\tWHERE instance_id = ? AND relative_path = ?\n\t\t",
"describe": {
"columns": [],
"parameters": {
"Right": 6
},
"nullable": []
},
"hash": "f93f5fca4f004dc70cab05f879993eb92fa6199c7356bbc2c5ca36468f916380"
}
@@ -0,0 +1,12 @@
UPDATE instances
SET
submitted_time_played = CASE
WHEN submitted_time_played < 0 THEN 0
ELSE submitted_time_played
END,
recent_time_played = CASE
WHEN recent_time_played < 0 THEN 0
ELSE recent_time_played
END
WHERE submitted_time_played < 0
OR recent_time_played < 0;
+27 -6
View File
@@ -250,9 +250,15 @@ impl Logs {
output: if clear_contents.unwrap_or(false) {
None
} else {
let state = State::get().await?;
Some(
get_output_by_filename(instance_path, log_type, &filename)
.await?,
get_output_by_filename_from_path(
&state,
instance_path,
log_type,
&filename,
)
.await?,
)
},
filename,
@@ -358,14 +364,12 @@ pub async fn get_logs_by_filename(
Logs::build(log_type, age, &instance_path, filename, Some(true)).await
}
#[tracing::instrument]
pub async fn get_output_by_filename(
async fn get_output_by_filename_from_path(
state: &State,
instance_path: &str,
log_type: LogType,
file_name: &str,
) -> crate::Result<CensoredString> {
let state = State::get().await?;
let logs_folder = match log_type {
LogType::InfoLog => state.directories.instance_logs_dir(instance_path),
LogType::CrashReport => {
@@ -409,6 +413,23 @@ pub async fn get_output_by_filename(
.into())
}
#[tracing::instrument]
pub async fn get_output_by_filename(
instance_id: &str,
log_type: LogType,
file_name: &str,
) -> crate::Result<CensoredString> {
let state = State::get().await?;
let instance_path = resolve_instance_path(instance_id, &state).await?;
get_output_by_filename_from_path(
&state,
&instance_path,
log_type,
file_name,
)
.await
}
#[tracing::instrument]
pub async fn delete_logs(instance_id: &str) -> crate::Result<()> {
let state = State::get().await?;
@@ -117,6 +117,7 @@ impl ModLoader {
pub struct ContentFile {
pub hash: String,
pub file_name: String,
pub enabled: bool,
pub size: u64,
pub metadata: Option<FileMetadata>,
pub update_version_id: Option<String>,
@@ -627,6 +627,7 @@ pub(crate) async fn rename_instance_file(
relative_path = ?,
file_name = ?,
enabled = ?,
missing = 0,
modified_at = ?
WHERE instance_id = ? AND relative_path = ?
",
@@ -856,11 +857,11 @@ pub(crate) async fn set_content_entry_enabled_for_file(
file_id: &str,
enabled: bool,
pool: &SqlitePool,
) -> crate::Result<()> {
) -> crate::Result<bool> {
let enabled = i64::from(enabled);
let modified_at = Utc::now().timestamp();
sqlx::query!(
let result = sqlx::query!(
"
UPDATE instance_content_entries
SET enabled = ?, modified_at = ?
@@ -874,7 +875,7 @@ pub(crate) async fn set_content_entry_enabled_for_file(
.execute(pool)
.await?;
Ok(())
Ok(result.rows_affected() > 0)
}
pub(crate) async fn remove_content_entries_for_file(
@@ -3,7 +3,7 @@
use crate::state::instances::{
ContentSet, ContentSetStatus, ContentSourceKind, Instance,
InstanceLaunchContext, InstanceLaunchOverrides,
InstanceLaunchOverridesData, InstanceLink,
InstanceLaunchOverridesData, InstanceLink, playtime_to_storage,
};
use crate::state::{
InstanceInstallStage, LauncherFeatureVersion, ModLoader, ReleaseChannel,
@@ -831,8 +831,12 @@ pub(crate) async fn insert_instance(
let created = instance.created.timestamp();
let modified = instance.modified.timestamp();
let last_played = instance.last_played.map(|value| value.timestamp());
let submitted_time_played = instance.submitted_time_played as i64;
let recent_time_played = instance.recent_time_played as i64;
let submitted_time_played = playtime_to_storage(
instance.submitted_time_played,
"submitted_time_played",
)?;
let recent_time_played =
playtime_to_storage(instance.recent_time_played, "recent_time_played")?;
sqlx::query!(
"
@@ -887,8 +891,12 @@ pub(crate) async fn update_instance(
let icon_path = instance.icon_path.as_deref();
let modified = instance.modified.timestamp();
let last_played = instance.last_played.map(|value| value.timestamp());
let submitted_time_played = instance.submitted_time_played as i64;
let recent_time_played = instance.recent_time_played as i64;
let submitted_time_played = playtime_to_storage(
instance.submitted_time_played,
"submitted_time_played",
)?;
let recent_time_played =
playtime_to_storage(instance.recent_time_played, "recent_time_played")?;
sqlx::query!(
"
@@ -661,13 +661,31 @@ pub(crate) async fn toggle_disable_project(
}
None => index_existing_file(&scope, &new_path, state).await?,
};
content_rows::set_content_entry_enabled_for_file(
let updated_entry = content_rows::set_content_entry_enabled_for_file(
&scope.content_set_id,
&file.id,
enabled,
&state.pool,
)
.await?;
if !updated_entry {
let project_type = ProjectType::get_from_parent_folder(&new_path)
.ok_or_else(|| {
crate::ErrorKind::InputError(format!(
"Unable to infer project type from {new_path}"
))
})?;
upsert_entry_for_file(
&scope,
&file,
project_type,
None,
None,
ContentSourceKind::Local,
&state.pool,
)
.await?;
}
Ok(new_path)
}
@@ -281,12 +281,31 @@ async fn plan_bulk_update(
instance_id: &str,
state: &State,
) -> crate::Result<BulkUpdatePlan> {
let updateable_paths =
bulk_updateable_project_paths(instance_id, state).await?;
if updateable_paths.is_empty() {
return Ok(BulkUpdatePlan {
project_updates: Vec::new(),
dependency_additions: Vec::new(),
});
}
let updates = check_content_updates(
instance_id,
Some(CacheBehaviour::MustRevalidate),
state,
)
.await?;
.await?
.into_iter()
.filter(|update| updateable_paths.contains(&update.relative_path))
.collect::<Vec<_>>();
if updates.is_empty() {
return Ok(BulkUpdatePlan {
project_updates: Vec::new(),
dependency_additions: Vec::new(),
});
}
let content_set =
content_rows::get_applied_content_set(instance_id, &state.pool)
.await?
@@ -320,6 +339,7 @@ async fn plan_bulk_update(
.collect::<HashMap<_, _>>();
let version_ids = installed
.iter()
.filter(|project| updateable_paths.contains(&project.relative_path))
.filter_map(|project| project.version_id.clone())
.chain(
updates
@@ -343,6 +363,7 @@ async fn plan_bulk_update(
let planned_versions = installed
.iter()
.filter(|project| project.enabled)
.filter(|project| updateable_paths.contains(&project.relative_path))
.filter_map(|project| {
let target_version_id = updates_by_path
.get(&project.relative_path)
@@ -379,6 +400,21 @@ async fn plan_bulk_update(
})
}
async fn bulk_updateable_project_paths(
instance_id: &str,
state: &State,
) -> crate::Result<HashSet<String>> {
let items = super::list_content::list_content(
instance_id,
None,
Some(CacheBehaviour::MustRevalidate),
state,
)
.await?;
Ok(items.into_iter().map(|item| item.file_path).collect())
}
async fn installed_projects(
instance_id: &str,
content_set: &ContentSet,
@@ -1,6 +1,6 @@
use crate::state::InstanceInstallStage;
use crate::state::instances::{
InstanceLaunchContext, adapters::sqlite::instance_rows,
InstanceLaunchContext, adapters::sqlite::instance_rows, playtime_to_storage,
};
use chrono::{DateTime, Utc};
use sqlx::SqlitePool;
@@ -120,16 +120,31 @@ pub(crate) async fn add_instance_recent_playtime(
seconds: u64,
pool: &SqlitePool,
) -> crate::Result<()> {
let seconds = seconds as i64;
if seconds == 0 {
return Ok(());
}
let seconds = playtime_to_storage(seconds, "recent_time_played")?;
let max_playtime = i64::MAX;
let max_playtime_before_increment = max_playtime - seconds;
let modified = Utc::now().timestamp();
sqlx::query!(
"
UPDATE instances
SET recent_time_played = recent_time_played + ?, modified = ?
SET
recent_time_played = CASE
WHEN recent_time_played < 0 THEN ?
WHEN recent_time_played > ? THEN ?
ELSE recent_time_played + ?
END,
modified = ?
WHERE id = ?
",
seconds,
max_playtime_before_increment,
max_playtime,
seconds,
modified,
instance_id,
)
@@ -144,19 +159,33 @@ pub(crate) async fn mark_instance_playtime_submitted(
recent_time_played: u64,
pool: &SqlitePool,
) -> crate::Result<()> {
let recent_time_played = recent_time_played as i64;
if recent_time_played == 0 {
return Ok(());
}
let recent_time_played =
playtime_to_storage(recent_time_played, "recent_time_played")?;
let max_playtime = i64::MAX;
let max_playtime_before_increment = max_playtime - recent_time_played;
let modified = Utc::now().timestamp();
sqlx::query!(
"
UPDATE instances
SET
submitted_time_played = submitted_time_played + ?,
submitted_time_played = CASE
WHEN submitted_time_played < 0 THEN ?
WHEN submitted_time_played > ? THEN ?
ELSE submitted_time_played + ?
END,
recent_time_played = 0,
modified = ?
WHERE id = ?
",
recent_time_played,
max_playtime_before_increment,
max_playtime,
recent_time_played,
modified,
instance_id,
)
@@ -208,6 +208,8 @@ pub(crate) async fn list_content(
)
.await?;
let imported_modpack_scope = is_imported_modpack_scope(&resolved, &link);
let linked_modpack_source_kind = linked_modpack_source_kind(&link);
let mut failed_modpack_identifier_lookup = false;
let modpack_ids = if imported_modpack_scope {
None
} else {
@@ -226,6 +228,7 @@ pub(crate) async fn list_content(
"Failed to fetch modpack identifiers: {}",
err
);
failed_modpack_identifier_lookup = true;
None
}
},
@@ -240,6 +243,12 @@ pub(crate) async fn list_content(
}
} else if let Some(ids) = modpack_ids.as_ref() {
ContentFilter::ExcludeModpack(ids)
} else if failed_modpack_identifier_lookup {
ContentFilter::ExcludeSourceKind {
source_kind: linked_modpack_source_kind
.unwrap_or(ContentSourceKind::ModrinthModpack),
exclude_untracked: true,
}
} else {
ContentFilter::All
};
@@ -735,6 +744,9 @@ async fn content_projects_for_scope(
update_version_id,
hash: file.sha1,
file_name: file.file_name,
enabled: entry.map_or(file.enabled, |entry| {
entry.enabled && file.enabled
}),
size: file.size,
metadata: file_metadata_from_entry_or_cache(entry, metadata),
project_type,
@@ -882,7 +894,7 @@ async fn content_files_to_content_items(
file_path: path.clone(),
id: file.hash.clone(),
size: file.size,
enabled: !file.file_name.ends_with(".disabled"),
enabled: file.enabled,
project_type: file.project_type,
project: project.map(|project| ContentItemProject {
id: project.id.clone(),
@@ -1097,6 +1109,20 @@ fn linked_modpack_ids(link: &InstanceLink) -> Option<(String, String)> {
}
}
fn linked_modpack_source_kind(
link: &InstanceLink,
) -> Option<ContentSourceKind> {
match link {
InstanceLink::ModrinthModpack { .. } => {
Some(ContentSourceKind::ModrinthModpack)
}
InstanceLink::ServerProjectModpack { .. } => {
Some(ContentSourceKind::ServerProject)
}
_ => None,
}
}
fn check_modpack_update(
installed_version_id: &str,
installed_version: &Version,
@@ -39,9 +39,18 @@ pub(crate) async fn sync_instance_content_files(
&state.api_semaphore,
)
.await?;
let hashes_by_path = hashes
let hashes_by_key = hashes
.into_iter()
.map(|hash| (hash.path.clone(), hash))
.map(|hash| {
(
format!(
"{}-{}",
hash.size,
hash.path.trim_end_matches(".disabled")
),
hash,
)
})
.collect::<HashMap<_, _>>();
let existing_files =
sqlite::content_rows::get_instance_files(&instance.id, &state.pool)
@@ -55,8 +64,8 @@ pub(crate) async fn sync_instance_content_files(
let mut files = Vec::new();
for file in scanned {
let cache_path = format!("{}/{}", instance.path, file.relative_path);
let Some(hash) = hashes_by_path.get(&cache_path) else {
let hash_key = file.hash_cache_key.trim_end_matches(".disabled");
let Some(hash) = hashes_by_key.get(hash_key) else {
continue;
};
let existing_file = existing_files_by_path.get(&file.relative_path);
@@ -20,3 +20,15 @@ pub struct Instance {
pub submitted_time_played: u64,
pub recent_time_played: u64,
}
pub(crate) fn playtime_to_storage(
value: u64,
column: &str,
) -> crate::Result<i64> {
i64::try_from(value).map_err(|_| {
crate::ErrorKind::InputError(format!(
"Expected {column} to fit in SQLite INTEGER"
))
.into()
})
}
@@ -3,7 +3,7 @@ use crate::jre::check_jre;
use crate::prelude::ModLoader;
use crate::state;
use crate::state::instances::{
InstanceLaunchOverrides, InstanceLaunchOverridesData,
InstanceLaunchOverrides, InstanceLaunchOverridesData, playtime_to_storage,
};
use crate::state::{
CacheValue, CachedEntry, CachedFile, CachedFileHash, CachedFileUpdate,
@@ -443,8 +443,12 @@ where
let created = input.created.timestamp();
let modified = input.modified.timestamp();
let last_played = input.last_played.map(|value| value.timestamp());
let submitted_time_played = input.submitted_time_played as i64;
let recent_time_played = input.recent_time_played as i64;
let submitted_time_played = playtime_to_storage(
input.submitted_time_played,
"submitted_time_played",
)?;
let recent_time_played =
playtime_to_storage(input.recent_time_played, "recent_time_played")?;
sqlx::query!(
"
+28 -27
View File
@@ -17,6 +17,7 @@ use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::ExitStatus;
use std::sync::LazyLock;
use std::time::Instant;
#[cfg(feature = "tauri")]
use tauri::Emitter;
use tempfile::TempDir;
@@ -752,46 +753,46 @@ impl Process {
uuid: Uuid,
) -> crate::Result<()> {
async fn update_playtime(
last_updated_playtime: &mut DateTime<Utc>,
last_updated_playtime: &mut Instant,
instance_id: &str,
force_update: bool,
) {
let diff = Utc::now()
.signed_duration_since(*last_updated_playtime)
.num_seconds();
if diff >= 60 || force_update {
let state = match crate::State::get().await {
Ok(state) => state,
Err(e) => {
tracing::warn!(
"Failed to get state for playtime update on instance {}: {}",
instance_id,
e
);
return;
}
};
if let Err(e) =
crate::state::instances::commands::add_instance_recent_playtime(
instance_id,
diff as u64,
&state.pool,
)
.await
{
let elapsed = last_updated_playtime.elapsed().as_secs();
if elapsed == 0 || (!force_update && elapsed < 60) {
return;
}
let state = match crate::State::get().await {
Ok(state) => state,
Err(e) => {
tracing::warn!(
"Failed to update playtime for instance {}: {}",
"Failed to get state for playtime update on instance {}: {}",
instance_id,
e
);
return;
}
*last_updated_playtime = Utc::now();
};
if let Err(e) =
crate::state::instances::commands::add_instance_recent_playtime(
instance_id,
elapsed,
&state.pool,
)
.await
{
tracing::warn!(
"Failed to update playtime for instance {}: {}",
instance_id,
e
);
}
*last_updated_playtime = Instant::now();
}
// Wait on current Minecraft Child
let mc_exit_status;
let mut last_updated_playtime = Utc::now();
let mut last_updated_playtime = Instant::now();
let state = crate::State::get().await?;
loop {
+10
View File
@@ -10,6 +10,16 @@ export type VersionEntry = {
}
const VERSIONS: VersionEntry[] = [
{
date: `2026-06-26T16:33:23+00:00`,
product: 'app',
version: '0.15.1',
body: `## Fixed
- Fixed issue where previous logs would not load in the Logs tab of an instance.
- Fixed issue where "Update all" would sometimes update linked modpack content.
- Fixed issue where disabled additional content would sometimes not appear as disabled in the UI.
- Fixed issue where instances with negative playtime fail to load. (How?)`,
},
{
date: `2026-06-25T21:22:14+00:00`,
product: 'app',