mirror of
https://github.com/modrinth/code.git
synced 2026-08-25 17:14:50 +00:00
* refactor: remove unused legacy onboarded flag * sqlx prepare * feat: add onboarding checklist app db and add vue provider * feat: add welcome page * feat: add onboarding checklist component * feat: move checklist visibility source of truth in app db * feat: split up import modpack button * feat-small: style tweak * feat: remove discover section from homepage and add library * refactor: move welcome screen to component * refactor: move grid display into /library * refactor: rename existing components * refactor: split up the current library page files * feat: add create group modal * feat: add instance group definitions * feat: update accordian for instance groups * feat: add delete group * feat: implement rename instance groups * refactor: move instance group api out * feat: add context menu for groups * feat: style instance items in library * feat: add selected instance item state * feat: implement action bar for selection * feat: drag and drop to move between groups * feat: show ungrouped with header * feat: improve edit group name * small fix * small style fixes * feat: improve selection behaviour * fix: dont toggle group when context menu open * feat: drag and drop multiple selected * small * feat: add remove from group in action bar * feat: add delay on icon hiding in inline input * feat: improve icon hiding delay * fix: bad routes.js * feat: implement sort and filter options * refactor: dont prefix filenames * feat: change open/collapse group target * feat: improve ungrouped behaviour * feat: add shift to select multiple instances * fix: icon disappear transition * feat: add esc while dragging to cancel * feat: update sorting and moved label * feat: update context menu * fix: multiple context menus can be opened * refactor: instance groups to be identified by ID instead of name, names become display data * feat: creating new group always adds instance * feat: create group button, fix instance selection, and increase group name max length * feat: strokes * fix: show getting started checklist in all sidebar * feat: set size on microsoft login screen * fix: use ButtonStyled in Chips component * feat: creating groups with selected instances and fix some moving selection and duplication bugs * pnpm prepr * feat: add tooltip for instance already in group * feat: create instance with content in creation flow modal - search modpack becomes search projects - creation flow modal context receives prepareProjectInstall and createProjectInstall callbacks, from content-install.ts (shared with ContentInstallModal) * feat: update instance card styles * refactor: separate out instance card content * update styles * refactor: instance card * fix: width of instance * feat: update empty state * feat: update sort and grouping dropdowns * feat: better context menu options * feat: add group action buttons * feat: add/remove favourites group * remove manual sort * feat: improve the "New group" jank * feat: add group by instance type and change group sorting * feat: welcome screen styles match figma * feat: not signed in skins demo * feat: new hosting empty state * feat: new instance card style * feat: add tooltip and spice up animation more * feat: improve the transitions for groups after dropping many instances * feat: set instance group membership for bulk moving * feat: remove fade out * fix: allow dragging from buttons inside instance card * fix: library page load in behaviour * small fix * feat: add group ordering and reordering * feat: add drag to reorder groups * fix: switching page not clearing selected instances * style fixes * style fix * fix spacing * feat: jump in changes * feat: add styled newly created instances * feat: improve jump in cards styles * fix: welcome screen style * fix: empty state styles * feat: max 5 jump in items * feat: max new instances to 3 * fix: search icon * feat: clear selection after move * feat: skins list use check icon styles * feat: list will be deleted instances and default sort last played * fix: stroke * remove: no more group settings in instance general settings * fix: resolving modpack server install * feat: icon editor * feat: add three hue shifted variants * fix: surprise me doesnt pick symbol * feat: add customize icon to create instance modal * feat: skins loading state style * fix: welcome logo * fix: copy * fix: left over usage of ButtonStyled from main's refactor * feat: some leaked merge stuff * feat: server empty state styles * remove: instance helper text * remove: auto link in set up stage * feat: auto focus on search when modal open * fix: delete instance modal style * remove: modpack meta repeating * fix: icon * fix: font weight * fix: dropdown filter preview heights * pnpm prepr * fix: sign into modrinth flashing change in position * feat: add avatar transparent corner gets padding * feat: drag and hold over group opens group * feat: add random icon for custom instance * feat: update loading icon * polish: empty state for search * polish: jump in cards * fix: avatar could skip checking for image if tauri loads it first * feat: add icon customize and randomizer in content install modal for add to new instances * fix: avatar cannot read image pixels due to cors * refactor: popup notifications toast type for instance sharing notifications * feat: apply random icons modal and add popup notification for it * polish: style * fix: set_webview_visible * QA * qa * feat: add inner white 15 opacity stroke around avatar * add to catalog * pnpm prepr * fix: app event import * feat: add new icons and backgrounds for icon editor * feat: recents list generate from randomized icons and save icon in editor + icon editor modal fixes * qa * refactor: recipe -> config * fix: event bus * polish * feat: add safe guard against randomizing empty icons * feat: navigate to instance after creation * fix: icon file names * i18n on welcome screen * i18n pass on PR * fix: inconsistent max group name length * refactor: use sqlx macro * feat: add instance icon background selection drag * feat: update catalog order * fix: remove gradient for welcome screen * remove: frog * fix: min width * feat: update corner size * fix: context menu styles * prepr * fix: instance name trimming * reorder * fix: instance card loading state stayed until hovering * feat: add icon randomizer blacklist * blog * prepr --------- Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
1070 lines
34 KiB
Rust
1070 lines
34 KiB
Rust
use crate::data::{Dependency, ProjectType, User, Version};
|
|
use crate::jre::check_jre;
|
|
use crate::prelude::ModLoader;
|
|
use crate::state;
|
|
use crate::state::instances::{
|
|
InstanceLaunchOverrides, InstanceLaunchOverridesData, playtime_to_storage,
|
|
};
|
|
use crate::state::{
|
|
CacheValue, CachedEntry, CachedFile, CachedFileHash, CachedFileUpdate,
|
|
Credentials, DefaultPage, DependencyType, DeviceToken, DeviceTokenKey,
|
|
DeviceTokenPair, FileType, Hooks, InstanceInstallStage,
|
|
LauncherFeatureVersion, MemorySettings, ModrinthCredentials,
|
|
ReleaseChannel, TeamMember, Theme, VersionFile, WindowSize,
|
|
};
|
|
use crate::util::fetch::{IoSemaphore, read_json};
|
|
use chrono::{DateTime, Utc};
|
|
use p256::ecdsa::SigningKey;
|
|
use p256::pkcs8::DecodePrivateKey;
|
|
use serde::Deserialize;
|
|
use std::collections::HashMap;
|
|
use std::path::PathBuf;
|
|
use tokio::sync::Semaphore;
|
|
use uuid::Uuid;
|
|
|
|
use super::MinecraftProfile;
|
|
|
|
pub async fn migrate_legacy_data<'a, E>(exec: E) -> crate::Result<()>
|
|
where
|
|
E: sqlx::Executor<'a, Database = sqlx::Sqlite> + Copy,
|
|
{
|
|
let mut settings = state::Settings::get(exec).await?;
|
|
|
|
if settings.migrated {
|
|
return Ok(());
|
|
};
|
|
|
|
let Some(old_launcher_root) = default_settings_dir() else {
|
|
return Ok(());
|
|
};
|
|
let old_launcher_root_str = old_launcher_root.to_string_lossy().to_string();
|
|
|
|
let io_semaphore = IoSemaphore(Semaphore::new(10));
|
|
let settings_path = old_launcher_root.join("settings.json");
|
|
|
|
if let Ok(legacy_settings) =
|
|
read_json::<LegacySettings>(&settings_path, &io_semaphore).await
|
|
{
|
|
settings.max_concurrent_writes = legacy_settings.max_concurrent_writes;
|
|
settings.max_concurrent_downloads =
|
|
legacy_settings.max_concurrent_downloads;
|
|
settings.theme = match legacy_settings.theme {
|
|
LegacyTheme::Dark => Theme::Dark,
|
|
LegacyTheme::Light => Theme::Light,
|
|
LegacyTheme::Oled => Theme::Oled,
|
|
};
|
|
settings.default_page = match legacy_settings.default_page {
|
|
LegacyDefaultPage::Home => DefaultPage::Home,
|
|
LegacyDefaultPage::Library => DefaultPage::Library,
|
|
};
|
|
settings.collapsed_navigation = legacy_settings.collapsed_navigation;
|
|
settings.advanced_rendering = legacy_settings.advanced_rendering;
|
|
settings.native_decorations = legacy_settings.native_decorations;
|
|
settings.telemetry = !legacy_settings.opt_out_analytics;
|
|
settings.discord_rpc = !legacy_settings.disable_discord_rpc;
|
|
settings.developer_mode = legacy_settings.developer_mode;
|
|
settings.extra_launch_args = legacy_settings.custom_java_args;
|
|
settings.custom_env_vars = legacy_settings.custom_env_args;
|
|
settings.memory.maximum = legacy_settings.memory.maximum;
|
|
settings.force_fullscreen = legacy_settings.force_fullscreen;
|
|
settings.game_resolution.0 = legacy_settings.game_resolution.0;
|
|
settings.game_resolution.1 = legacy_settings.game_resolution.1;
|
|
settings.hide_on_process_start = legacy_settings.hide_on_process;
|
|
settings.hooks.pre_launch = legacy_settings.hooks.pre_launch;
|
|
settings.hooks.wrapper = legacy_settings.hooks.wrapper;
|
|
settings.hooks.post_exit = legacy_settings.hooks.post_exit;
|
|
|
|
if let Some(path) = legacy_settings
|
|
.loaded_config_dir
|
|
.clone()
|
|
.and_then(|x| x.to_str().map(|x| x.to_string()))
|
|
&& path != old_launcher_root_str
|
|
{
|
|
settings.custom_dir = Some(path);
|
|
}
|
|
|
|
settings.prev_custom_dir = Some(old_launcher_root_str.clone());
|
|
|
|
for (_, legacy_version) in legacy_settings.java_globals.0 {
|
|
if let Ok(java_version) =
|
|
check_jre(PathBuf::from(legacy_version.path)).await
|
|
{
|
|
java_version.upsert(exec).await?;
|
|
}
|
|
}
|
|
|
|
let modrinth_auth_path =
|
|
old_launcher_root.join("caches/metadata/auth.json");
|
|
if let Ok(creds) = read_json::<LegacyModrinthCredentials>(
|
|
&modrinth_auth_path,
|
|
&io_semaphore,
|
|
)
|
|
.await
|
|
{
|
|
ModrinthCredentials {
|
|
session: creds.session,
|
|
expires: creds.expires_at,
|
|
user_id: creds.user.id,
|
|
active: true,
|
|
}
|
|
.upsert(exec)
|
|
.await?;
|
|
}
|
|
|
|
let minecraft_auth_path =
|
|
old_launcher_root.join("caches/metadata/minecraft_auth.json");
|
|
if let Ok(minecraft_auth) = read_json::<LegacyMinecraftAuthStore>(
|
|
&minecraft_auth_path,
|
|
&io_semaphore,
|
|
)
|
|
.await
|
|
{
|
|
let minecraft_users_len = minecraft_auth.users.len();
|
|
for (uuid, legacy_credentials) in minecraft_auth.users {
|
|
Credentials {
|
|
offline_profile: MinecraftProfile {
|
|
id: legacy_credentials.id,
|
|
name: legacy_credentials.username,
|
|
..MinecraftProfile::default()
|
|
},
|
|
access_token: legacy_credentials.access_token,
|
|
refresh_token: legacy_credentials.refresh_token,
|
|
expires: legacy_credentials.expires,
|
|
active: minecraft_auth.default_user == Some(uuid)
|
|
|| minecraft_users_len == 1,
|
|
}
|
|
.upsert(exec)
|
|
.await?;
|
|
}
|
|
|
|
if let Some(device_token) = minecraft_auth.token
|
|
&& let Ok(private_key) =
|
|
SigningKey::from_pkcs8_pem(&device_token.private_key)
|
|
&& let Ok(uuid) = Uuid::parse_str(&device_token.id)
|
|
{
|
|
DeviceTokenPair {
|
|
token: DeviceToken {
|
|
issue_instant: device_token.token.issue_instant,
|
|
not_after: device_token.token.not_after,
|
|
token: device_token.token.token,
|
|
display_claims: device_token.token.display_claims,
|
|
},
|
|
key: DeviceTokenKey {
|
|
id: uuid,
|
|
key: private_key,
|
|
x: device_token.x,
|
|
y: device_token.y,
|
|
},
|
|
}
|
|
.upsert(exec)
|
|
.await?;
|
|
}
|
|
}
|
|
|
|
let mut cached_entries = vec![];
|
|
|
|
if let Ok(legacy_instances_dir) = std::fs::read_dir(
|
|
legacy_settings
|
|
.loaded_config_dir
|
|
.clone()
|
|
.unwrap_or_else(|| old_launcher_root.clone())
|
|
.join("profiles"),
|
|
) {
|
|
for entry in legacy_instances_dir.flatten() {
|
|
if !entry.path().is_dir() {
|
|
continue;
|
|
}
|
|
|
|
let legacy_config_path = entry.path().join("profile.json");
|
|
|
|
let Ok(profile) = read_json::<LegacyInstanceConfig>(
|
|
&legacy_config_path,
|
|
&io_semaphore,
|
|
)
|
|
.await
|
|
else {
|
|
continue;
|
|
};
|
|
|
|
for (path, project) in profile.projects {
|
|
let full_path = legacy_settings
|
|
.loaded_config_dir
|
|
.clone()
|
|
.unwrap_or_else(|| old_launcher_root.clone())
|
|
.join("profiles")
|
|
.join(&profile.path)
|
|
.join(&path);
|
|
|
|
if !full_path.exists() || !full_path.is_file() {
|
|
continue;
|
|
}
|
|
let sha512 = project.sha512;
|
|
|
|
if let LegacyProjectMetadata::Modrinth {
|
|
version,
|
|
members,
|
|
update_version,
|
|
..
|
|
} = project.metadata
|
|
&& let Some(file) = version
|
|
.files
|
|
.iter()
|
|
.find(|x| x.hashes.get("sha512") == Some(&sha512))
|
|
&& let Some(sha1) = file.hashes.get("sha1")
|
|
{
|
|
if let Ok(metadata) = full_path.metadata()
|
|
&& let Ok(modified_at_ns) =
|
|
state::file_modified_at_ns(&metadata)
|
|
{
|
|
let file_name = format!(
|
|
"{}/{}",
|
|
profile.path,
|
|
path.replace('\\', "/")
|
|
.replace(".disabled", "")
|
|
);
|
|
|
|
cached_entries.push(CacheValue::FileHash(
|
|
CachedFileHash {
|
|
path: file_name,
|
|
size: metadata.len(),
|
|
modified_at_ns,
|
|
hash: sha1.clone(),
|
|
project_type:
|
|
ProjectType::get_from_parent_folder(
|
|
&full_path,
|
|
),
|
|
project_id: Some(
|
|
version.project_id.clone(),
|
|
),
|
|
version_id: Some(version.id.clone()),
|
|
},
|
|
));
|
|
}
|
|
|
|
cached_entries.push(CacheValue::File(CachedFile {
|
|
hash: sha1.clone(),
|
|
project_id: version.project_id.clone(),
|
|
version_id: version.id.clone(),
|
|
}));
|
|
|
|
if let Some(update_version) = update_version {
|
|
let mod_loader: ModLoader =
|
|
profile.metadata.loader.into();
|
|
cached_entries.push(CacheValue::FileUpdate(
|
|
CachedFileUpdate {
|
|
hash: sha1.clone(),
|
|
game_version: profile
|
|
.metadata
|
|
.game_version
|
|
.clone(),
|
|
loaders: vec![
|
|
mod_loader.as_str().to_string(),
|
|
],
|
|
channel_policy: ReleaseChannel::Alpha
|
|
.key()
|
|
.to_string(),
|
|
update_version_id: update_version
|
|
.id
|
|
.clone(),
|
|
},
|
|
));
|
|
|
|
cached_entries.push(CacheValue::Version(
|
|
(*update_version).into(),
|
|
));
|
|
}
|
|
|
|
let members = members
|
|
.into_iter()
|
|
.map(|x| {
|
|
let user = User {
|
|
id: x.user.id,
|
|
username: x.user.username,
|
|
avatar_url: x.user.avatar_url,
|
|
bio: x.user.bio,
|
|
created: x.user.created,
|
|
role: x.user.role,
|
|
badges: 0,
|
|
};
|
|
|
|
cached_entries
|
|
.push(CacheValue::User(user.clone()));
|
|
|
|
TeamMember {
|
|
team_id: x.team_id,
|
|
user,
|
|
is_owner: x.role == "Owner",
|
|
role: x.role,
|
|
ordering: x.ordering,
|
|
}
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
cached_entries.push(CacheValue::Team(members));
|
|
|
|
cached_entries
|
|
.push(CacheValue::Version((*version).into()));
|
|
}
|
|
}
|
|
|
|
upsert_legacy_instance(
|
|
exec,
|
|
LegacyInstanceUpsert {
|
|
path: profile.path,
|
|
install_stage: match profile.install_stage {
|
|
LegacyInstanceInstallStage::Installed => {
|
|
InstanceInstallStage::Installed
|
|
}
|
|
LegacyInstanceInstallStage::Installing => {
|
|
InstanceInstallStage::MinecraftInstalling
|
|
}
|
|
LegacyInstanceInstallStage::PackInstalling => {
|
|
InstanceInstallStage::PackInstalling
|
|
}
|
|
LegacyInstanceInstallStage::NotInstalled => {
|
|
InstanceInstallStage::NotInstalled
|
|
}
|
|
},
|
|
launcher_feature_version: LauncherFeatureVersion::None,
|
|
name: profile.metadata.name,
|
|
icon_path: profile.metadata.icon,
|
|
game_version: profile.metadata.game_version,
|
|
loader: profile.metadata.loader.into(),
|
|
loader_version: profile
|
|
.metadata
|
|
.loader_version
|
|
.map(|x| x.id),
|
|
groups: profile.metadata.groups,
|
|
linked_data: profile.metadata.linked_data,
|
|
created: profile.metadata.date_created,
|
|
modified: profile.metadata.date_modified,
|
|
last_played: profile.metadata.last_played,
|
|
submitted_time_played: profile
|
|
.metadata
|
|
.submitted_time_played,
|
|
recent_time_played: profile.metadata.recent_time_played,
|
|
java_path: profile.java.as_ref().and_then(|x| {
|
|
x.override_version.clone().map(|x| x.path)
|
|
}),
|
|
extra_launch_args: profile
|
|
.java
|
|
.as_ref()
|
|
.and_then(|x| x.extra_arguments.clone()),
|
|
custom_env_vars: profile
|
|
.java
|
|
.and_then(|x| x.custom_env_args),
|
|
memory: profile
|
|
.memory
|
|
.map(|x| MemorySettings { maximum: x.maximum }),
|
|
force_fullscreen: profile.fullscreen,
|
|
game_resolution: profile
|
|
.resolution
|
|
.map(|x| WindowSize(x.0, x.1)),
|
|
hooks: Hooks {
|
|
pre_launch: profile
|
|
.hooks
|
|
.as_ref()
|
|
.and_then(|x| x.pre_launch.clone()),
|
|
wrapper: profile
|
|
.hooks
|
|
.as_ref()
|
|
.and_then(|x| x.wrapper.clone()),
|
|
post_exit: profile.hooks.and_then(|x| x.post_exit),
|
|
},
|
|
},
|
|
)
|
|
.await?;
|
|
}
|
|
}
|
|
|
|
CachedEntry::upsert_many(
|
|
&cached_entries
|
|
.into_iter()
|
|
.map(|x| {
|
|
let mut entry = x.get_entry();
|
|
entry.expires =
|
|
Utc::now().timestamp() - entry.type_.expiry();
|
|
entry
|
|
})
|
|
.collect::<Vec<_>>(),
|
|
exec,
|
|
)
|
|
.await?;
|
|
|
|
settings.migrated = true;
|
|
settings.update(exec).await?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
struct LegacyInstanceUpsert {
|
|
path: String,
|
|
install_stage: InstanceInstallStage,
|
|
launcher_feature_version: LauncherFeatureVersion,
|
|
name: String,
|
|
icon_path: Option<String>,
|
|
game_version: String,
|
|
loader: ModLoader,
|
|
loader_version: Option<String>,
|
|
groups: Vec<String>,
|
|
linked_data: Option<LegacyLinkedData>,
|
|
created: DateTime<Utc>,
|
|
modified: DateTime<Utc>,
|
|
last_played: Option<DateTime<Utc>>,
|
|
submitted_time_played: u64,
|
|
recent_time_played: u64,
|
|
java_path: Option<String>,
|
|
extra_launch_args: Option<Vec<String>>,
|
|
custom_env_vars: Option<Vec<(String, String)>>,
|
|
memory: Option<MemorySettings>,
|
|
force_fullscreen: Option<bool>,
|
|
game_resolution: Option<WindowSize>,
|
|
hooks: Hooks,
|
|
}
|
|
|
|
async fn upsert_legacy_instance<'a, E>(
|
|
exec: E,
|
|
input: LegacyInstanceUpsert,
|
|
) -> crate::Result<()>
|
|
where
|
|
E: sqlx::Executor<'a, Database = sqlx::Sqlite> + Copy,
|
|
{
|
|
let instance_id = format!("local:{}", Uuid::new_v4());
|
|
let content_set_id = format!("content-set:{}", Uuid::new_v4());
|
|
let instance_id_str = instance_id.as_str();
|
|
let content_set_id_str = content_set_id.as_str();
|
|
let path = input.path.as_str();
|
|
let install_stage = input.install_stage.as_str();
|
|
let launcher_feature_version = input.launcher_feature_version.as_str();
|
|
let update_channel = ReleaseChannel::Release.key();
|
|
let name = input.name.as_str();
|
|
let icon_path = input.icon_path.as_deref();
|
|
let game_version = input.game_version.as_str();
|
|
let loader = input.loader.as_str();
|
|
let loader_version = input.loader_version.as_deref();
|
|
let created = input.created.timestamp();
|
|
let modified = input.modified.timestamp();
|
|
let last_played = input.last_played.map(|value| value.timestamp());
|
|
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!(
|
|
"
|
|
INSERT OR REPLACE INTO instances (
|
|
id,
|
|
path,
|
|
applied_content_set_id,
|
|
install_stage,
|
|
launcher_feature_version,
|
|
update_channel,
|
|
name,
|
|
icon_path,
|
|
created,
|
|
modified,
|
|
last_played,
|
|
submitted_time_played,
|
|
recent_time_played
|
|
)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
",
|
|
instance_id_str,
|
|
path,
|
|
content_set_id_str,
|
|
install_stage,
|
|
launcher_feature_version,
|
|
update_channel,
|
|
name,
|
|
icon_path,
|
|
created,
|
|
modified,
|
|
last_played,
|
|
submitted_time_played,
|
|
recent_time_played,
|
|
)
|
|
.execute(exec)
|
|
.await?;
|
|
|
|
let (
|
|
source_kind,
|
|
link_kind,
|
|
modrinth_project_id,
|
|
modrinth_version_id,
|
|
server_project_id,
|
|
) = match input.linked_data {
|
|
Some(linked_data) => {
|
|
match (linked_data.project_id, linked_data.version_id) {
|
|
(Some(project_id), Some(version_id))
|
|
if version_id.is_empty() =>
|
|
{
|
|
(
|
|
"server_project",
|
|
"server_project",
|
|
None,
|
|
None,
|
|
Some(project_id),
|
|
)
|
|
}
|
|
(Some(project_id), Some(version_id)) => (
|
|
"modrinth_modpack",
|
|
"modrinth_modpack",
|
|
Some(project_id),
|
|
Some(version_id),
|
|
None,
|
|
),
|
|
_ => ("local", "unmanaged", None, None, None),
|
|
}
|
|
}
|
|
None => ("local", "unmanaged", None, None, None),
|
|
};
|
|
|
|
sqlx::query!(
|
|
"
|
|
INSERT OR REPLACE INTO instance_content_sets (
|
|
id,
|
|
instance_id,
|
|
name,
|
|
source_kind,
|
|
status,
|
|
game_version,
|
|
protocol_version,
|
|
loader,
|
|
loader_version,
|
|
created,
|
|
modified
|
|
)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
",
|
|
content_set_id_str,
|
|
instance_id_str,
|
|
"Default",
|
|
source_kind,
|
|
"available",
|
|
game_version,
|
|
None::<i64>,
|
|
loader,
|
|
loader_version,
|
|
created,
|
|
modified,
|
|
)
|
|
.execute(exec)
|
|
.await?;
|
|
|
|
sqlx::query!(
|
|
"
|
|
INSERT OR REPLACE INTO instance_links (
|
|
instance_id,
|
|
link_kind,
|
|
modrinth_project_id,
|
|
modrinth_version_id,
|
|
server_project_id,
|
|
content_project_id,
|
|
content_version_id,
|
|
hosting_server_id,
|
|
hosting_instance_ids,
|
|
hosting_active_instance_id,
|
|
shared_instance_id
|
|
)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, jsonb(?), ?, ?)
|
|
",
|
|
instance_id_str,
|
|
link_kind,
|
|
modrinth_project_id,
|
|
modrinth_version_id,
|
|
server_project_id,
|
|
None::<&str>,
|
|
None::<&str>,
|
|
None::<&str>,
|
|
"[]",
|
|
None::<&str>,
|
|
None::<&str>,
|
|
)
|
|
.execute(exec)
|
|
.await?;
|
|
|
|
for group in input.groups {
|
|
let group_id = match sqlx::query_scalar::<_, String>(
|
|
"
|
|
SELECT id
|
|
FROM instance_groups
|
|
WHERE name = ?
|
|
",
|
|
)
|
|
.bind(&group)
|
|
.fetch_optional(exec)
|
|
.await?
|
|
{
|
|
Some(group_id) => group_id,
|
|
None => {
|
|
let group_id = Uuid::new_v4().to_string();
|
|
sqlx::query(
|
|
"
|
|
INSERT INTO instance_groups (id, name)
|
|
VALUES (?, ?)
|
|
",
|
|
)
|
|
.bind(&group_id)
|
|
.bind(&group)
|
|
.execute(exec)
|
|
.await?;
|
|
group_id
|
|
}
|
|
};
|
|
|
|
sqlx::query(
|
|
"
|
|
INSERT OR IGNORE INTO instance_group_memberships (
|
|
instance_id,
|
|
group_id
|
|
)
|
|
VALUES (?, ?)
|
|
",
|
|
)
|
|
.bind(instance_id_str)
|
|
.bind(group_id)
|
|
.execute(exec)
|
|
.await?;
|
|
}
|
|
|
|
let launch_overrides = InstanceLaunchOverrides {
|
|
instance_id: instance_id.clone(),
|
|
java_path: input.java_path,
|
|
extra_launch_args: input.extra_launch_args,
|
|
custom_env_vars: input.custom_env_vars,
|
|
memory: input.memory,
|
|
force_fullscreen: input.force_fullscreen,
|
|
game_resolution: input.game_resolution,
|
|
hooks: input.hooks,
|
|
};
|
|
let launch_overrides_data = serde_json::to_string(
|
|
&InstanceLaunchOverridesData::from(&launch_overrides),
|
|
)?;
|
|
|
|
sqlx::query!(
|
|
"
|
|
INSERT OR REPLACE INTO instance_launch_overrides (
|
|
instance_id,
|
|
overrides
|
|
)
|
|
VALUES (?, jsonb(?))
|
|
",
|
|
instance_id_str,
|
|
launch_overrides_data,
|
|
)
|
|
.execute(exec)
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[derive(Deserialize, Debug, Clone)]
|
|
struct LegacySettings {
|
|
pub theme: LegacyTheme,
|
|
pub memory: LegacyMemorySettings,
|
|
#[serde(default)]
|
|
pub force_fullscreen: bool,
|
|
pub game_resolution: LegacyWindowSize,
|
|
pub custom_java_args: Vec<String>,
|
|
pub custom_env_args: Vec<(String, String)>,
|
|
pub java_globals: LegacyJavaGlobals,
|
|
pub hooks: LegacyHooks,
|
|
pub max_concurrent_downloads: usize,
|
|
pub max_concurrent_writes: usize,
|
|
pub collapsed_navigation: bool,
|
|
#[serde(default)]
|
|
pub disable_discord_rpc: bool,
|
|
#[serde(default)]
|
|
pub hide_on_process: bool,
|
|
#[serde(default)]
|
|
pub native_decorations: bool,
|
|
#[serde(default)]
|
|
pub default_page: LegacyDefaultPage,
|
|
#[serde(default)]
|
|
pub developer_mode: bool,
|
|
#[serde(default)]
|
|
pub opt_out_analytics: bool,
|
|
#[serde(default)]
|
|
pub advanced_rendering: bool,
|
|
#[serde(default = "default_settings_dir")]
|
|
pub loaded_config_dir: Option<PathBuf>,
|
|
}
|
|
|
|
fn default_settings_dir() -> Option<PathBuf> {
|
|
Some(dirs::config_dir()?.join("com.modrinth.theseus"))
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum LegacyTheme {
|
|
Dark,
|
|
Light,
|
|
Oled,
|
|
}
|
|
|
|
#[derive(Deserialize, Default, Debug, Clone, Copy)]
|
|
enum LegacyDefaultPage {
|
|
#[default]
|
|
Home,
|
|
Library,
|
|
}
|
|
|
|
#[derive(Deserialize, Debug, Clone)]
|
|
struct LegacyHooks {
|
|
pub pre_launch: Option<String>,
|
|
pub wrapper: Option<String>,
|
|
pub post_exit: Option<String>,
|
|
}
|
|
|
|
#[derive(Deserialize, Debug, Clone, Copy)]
|
|
struct LegacyMemorySettings {
|
|
pub maximum: u32,
|
|
}
|
|
|
|
#[derive(Deserialize, Debug, Clone, Copy)]
|
|
struct LegacyWindowSize(pub u16, pub u16);
|
|
|
|
#[derive(Debug, Deserialize, Clone)]
|
|
struct LegacyJavaGlobals(HashMap<String, LegacyJavaVersion>);
|
|
|
|
#[derive(Debug, PartialEq, Eq, Hash, Deserialize, Clone)]
|
|
struct LegacyJavaVersion {
|
|
pub path: String,
|
|
pub version: String,
|
|
pub architecture: String,
|
|
}
|
|
|
|
#[derive(Deserialize, Clone, Debug)]
|
|
struct LegacyModrinthUser {
|
|
pub id: String,
|
|
pub username: String,
|
|
// pub name: Option<String>,
|
|
pub avatar_url: Option<String>,
|
|
pub bio: Option<String>,
|
|
pub created: DateTime<Utc>,
|
|
pub role: String,
|
|
}
|
|
|
|
#[derive(Deserialize, Clone, Debug)]
|
|
struct LegacyModrinthCredentials {
|
|
pub session: String,
|
|
pub expires_at: DateTime<Utc>,
|
|
pub user: LegacyModrinthUser,
|
|
}
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
struct LegacyMinecraftAuthStore {
|
|
pub users: HashMap<Uuid, LegacyCredentials>,
|
|
pub token: Option<LegacySaveDeviceToken>,
|
|
pub default_user: Option<Uuid>,
|
|
}
|
|
|
|
#[derive(Deserialize, Clone, Debug)]
|
|
struct LegacyCredentials {
|
|
pub id: Uuid,
|
|
pub username: String,
|
|
pub access_token: String,
|
|
pub refresh_token: String,
|
|
pub expires: DateTime<Utc>,
|
|
}
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
struct LegacySaveDeviceToken {
|
|
pub id: String,
|
|
pub private_key: String,
|
|
pub x: String,
|
|
pub y: String,
|
|
pub token: LegacyDeviceToken,
|
|
}
|
|
|
|
#[derive(Deserialize, Clone, Debug)]
|
|
#[serde(rename_all = "PascalCase")]
|
|
struct LegacyDeviceToken {
|
|
pub issue_instant: DateTime<Utc>,
|
|
pub not_after: DateTime<Utc>,
|
|
pub token: String,
|
|
pub display_claims: HashMap<String, serde_json::Value>,
|
|
}
|
|
|
|
#[derive(Deserialize, Clone, Debug)]
|
|
struct LegacyInstanceConfig {
|
|
#[serde(default)]
|
|
pub install_stage: LegacyInstanceInstallStage,
|
|
#[serde(default)]
|
|
pub path: String,
|
|
pub metadata: LegacyInstanceMetadata,
|
|
pub java: Option<LegacyJavaSettings>,
|
|
pub memory: Option<LegacyMemorySettings>,
|
|
pub resolution: Option<LegacyWindowSize>,
|
|
pub fullscreen: Option<bool>,
|
|
pub hooks: Option<LegacyHooks>,
|
|
pub projects: HashMap<String, LegacyProject>,
|
|
}
|
|
|
|
#[derive(Deserialize, Clone, Debug)]
|
|
struct LegacyProject {
|
|
pub sha512: String,
|
|
// pub disabled: bool,
|
|
pub metadata: LegacyProjectMetadata,
|
|
// pub file_name: String,
|
|
}
|
|
|
|
#[derive(Deserialize, Clone, Debug)]
|
|
#[serde(tag = "type", rename_all = "snake_case")]
|
|
enum LegacyProjectMetadata {
|
|
Modrinth {
|
|
// project: Box<LegacyModrinthProject>,
|
|
version: Box<LegacyModrinthVersion>,
|
|
members: Vec<LegacyModrinthTeamMember>,
|
|
update_version: Option<Box<LegacyModrinthVersion>>,
|
|
},
|
|
Inferred,
|
|
Unknown,
|
|
}
|
|
|
|
// #[derive(Deserialize, Clone, Debug)]
|
|
// struct LegacyModrinthProject {
|
|
// pub id: String,
|
|
// pub slug: Option<String>,
|
|
// pub project_type: String,
|
|
// pub team: String,
|
|
// pub title: String,
|
|
// pub description: String,
|
|
// pub body: String,
|
|
//
|
|
// pub published: DateTime<Utc>,
|
|
// pub updated: DateTime<Utc>,
|
|
//
|
|
// pub client_side: LegacySideType,
|
|
// pub server_side: LegacySideType,
|
|
//
|
|
// pub downloads: u32,
|
|
// pub followers: u32,
|
|
//
|
|
// pub categories: Vec<String>,
|
|
// pub additional_categories: Vec<String>,
|
|
// pub game_versions: Vec<String>,
|
|
// pub loaders: Vec<String>,
|
|
//
|
|
// pub versions: Vec<String>,
|
|
//
|
|
// pub icon_url: Option<String>,
|
|
// }
|
|
|
|
#[derive(Deserialize, Clone, Debug)]
|
|
struct LegacyModrinthVersion {
|
|
pub id: String,
|
|
pub project_id: String,
|
|
pub author_id: String,
|
|
|
|
pub featured: bool,
|
|
|
|
pub name: String,
|
|
pub version_number: String,
|
|
pub changelog: String,
|
|
pub changelog_url: Option<String>,
|
|
|
|
pub date_published: DateTime<Utc>,
|
|
pub downloads: u32,
|
|
pub version_type: String,
|
|
|
|
pub files: Vec<LegacyModrinthVersionFile>,
|
|
pub dependencies: Vec<LegacyDependency>,
|
|
pub game_versions: Vec<String>,
|
|
pub loaders: Vec<String>,
|
|
}
|
|
|
|
impl From<LegacyModrinthVersion> for Version {
|
|
fn from(value: LegacyModrinthVersion) -> Self {
|
|
Version {
|
|
id: value.id,
|
|
project_id: value.project_id,
|
|
author_id: value.author_id,
|
|
featured: value.featured,
|
|
name: value.name,
|
|
version_number: value.version_number,
|
|
changelog: Some(value.changelog),
|
|
changelog_url: value.changelog_url,
|
|
date_published: value.date_published,
|
|
downloads: value.downloads,
|
|
version_type: value.version_type,
|
|
files: value
|
|
.files
|
|
.into_iter()
|
|
.map(|x| VersionFile {
|
|
hashes: x.hashes,
|
|
url: x.url,
|
|
filename: x.filename,
|
|
primary: x.primary,
|
|
size: x.size,
|
|
file_type: x.file_type.map(|x| match x {
|
|
LegacyFileType::RequiredResourcePack => {
|
|
FileType::RequiredResourcePack
|
|
}
|
|
LegacyFileType::OptionalResourcePack => {
|
|
FileType::OptionalResourcePack
|
|
}
|
|
LegacyFileType::Unknown => FileType::Unknown,
|
|
}),
|
|
})
|
|
.collect::<Vec<_>>(),
|
|
dependencies: value
|
|
.dependencies
|
|
.into_iter()
|
|
.map(|x| Dependency {
|
|
version_id: x.version_id,
|
|
project_id: x.project_id,
|
|
file_name: x.file_name,
|
|
dependency_type: match x.dependency_type {
|
|
LegacyDependencyType::Required => {
|
|
DependencyType::Required
|
|
}
|
|
LegacyDependencyType::Optional => {
|
|
DependencyType::Optional
|
|
}
|
|
LegacyDependencyType::Incompatible => {
|
|
DependencyType::Incompatible
|
|
}
|
|
LegacyDependencyType::Embedded => {
|
|
DependencyType::Embedded
|
|
}
|
|
},
|
|
})
|
|
.collect::<Vec<_>>(),
|
|
game_versions: value.game_versions,
|
|
loaders: value.loaders,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Deserialize, Clone, Debug)]
|
|
struct LegacyModrinthVersionFile {
|
|
pub hashes: HashMap<String, String>,
|
|
pub url: String,
|
|
pub filename: String,
|
|
pub primary: bool,
|
|
pub size: u32,
|
|
pub file_type: Option<LegacyFileType>,
|
|
}
|
|
|
|
#[derive(Deserialize, Clone, Debug)]
|
|
struct LegacyDependency {
|
|
pub version_id: Option<String>,
|
|
pub project_id: Option<String>,
|
|
pub file_name: Option<String>,
|
|
pub dependency_type: LegacyDependencyType,
|
|
}
|
|
|
|
#[derive(Deserialize, Clone, Debug)]
|
|
struct LegacyModrinthTeamMember {
|
|
pub team_id: String,
|
|
pub user: LegacyModrinthUser,
|
|
pub role: String,
|
|
pub ordering: i64,
|
|
}
|
|
|
|
#[derive(Deserialize, Copy, Clone, Debug)]
|
|
#[serde(rename_all = "lowercase")]
|
|
enum LegacyDependencyType {
|
|
Required,
|
|
Optional,
|
|
Incompatible,
|
|
Embedded,
|
|
}
|
|
|
|
// #[derive(Deserialize, Clone, Debug, Eq, PartialEq)]
|
|
// #[serde(rename_all = "kebab-case")]
|
|
// enum LegacySideType {
|
|
// Required,
|
|
// Optional,
|
|
// Unsupported,
|
|
// Unknown,
|
|
// }
|
|
|
|
#[derive(Deserialize, Copy, Clone, Debug)]
|
|
#[serde(rename_all = "kebab-case")]
|
|
enum LegacyFileType {
|
|
RequiredResourcePack,
|
|
OptionalResourcePack,
|
|
Unknown,
|
|
}
|
|
|
|
#[derive(Deserialize, Clone, Debug)]
|
|
struct LegacyInstanceMetadata {
|
|
pub name: String,
|
|
pub icon: Option<String>,
|
|
#[serde(default)]
|
|
pub groups: Vec<String>,
|
|
|
|
pub game_version: String,
|
|
#[serde(default)]
|
|
pub loader: LegacyModLoader,
|
|
pub loader_version: Option<LegacyLoaderVersion>,
|
|
|
|
pub linked_data: Option<LegacyLinkedData>,
|
|
|
|
#[serde(default)]
|
|
pub date_created: DateTime<Utc>,
|
|
#[serde(default)]
|
|
pub date_modified: DateTime<Utc>,
|
|
pub last_played: Option<DateTime<Utc>>,
|
|
#[serde(default)]
|
|
pub submitted_time_played: u64,
|
|
#[serde(default)]
|
|
pub recent_time_played: u64,
|
|
}
|
|
|
|
#[derive(Debug, Eq, PartialEq, Clone, Copy, Deserialize, Default)]
|
|
#[serde(rename_all = "lowercase")]
|
|
enum LegacyModLoader {
|
|
#[default]
|
|
Vanilla,
|
|
Forge,
|
|
Fabric,
|
|
Quilt,
|
|
NeoForge,
|
|
}
|
|
|
|
impl From<LegacyModLoader> for ModLoader {
|
|
fn from(value: LegacyModLoader) -> Self {
|
|
match value {
|
|
LegacyModLoader::Vanilla => ModLoader::Vanilla,
|
|
LegacyModLoader::Forge => ModLoader::Forge,
|
|
LegacyModLoader::Fabric => ModLoader::Fabric,
|
|
LegacyModLoader::Quilt => ModLoader::Quilt,
|
|
LegacyModLoader::NeoForge => ModLoader::NeoForge,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Deserialize, Clone, Debug)]
|
|
struct LegacyLinkedData {
|
|
pub project_id: Option<String>,
|
|
pub version_id: Option<String>,
|
|
}
|
|
|
|
#[derive(Deserialize, Clone, Debug)]
|
|
struct LegacyJavaSettings {
|
|
pub override_version: Option<LegacyJavaVersion>,
|
|
pub extra_arguments: Option<Vec<String>>,
|
|
pub custom_env_args: Option<Vec<(String, String)>>,
|
|
}
|
|
|
|
#[derive(Deserialize, Clone, Debug)]
|
|
struct LegacyLoaderVersion {
|
|
pub id: String,
|
|
}
|
|
|
|
#[derive(Deserialize, Clone, Copy, Debug, Default, Eq, PartialEq)]
|
|
#[serde(rename_all = "snake_case")]
|
|
enum LegacyInstanceInstallStage {
|
|
Installed,
|
|
Installing,
|
|
PackInstalling,
|
|
#[default]
|
|
NotInstalled,
|
|
}
|