Files
modrinth/packages/app-lib/src/state/legacy_converter.rs
T
Truman GaoandProspector e79e0d024b feat: app onboarding and library (#6923)
* 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>
2026-08-18 01:08:09 +00:00

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,
}