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>
This commit is contained in:
Truman Gao
2026-08-18 01:08:09 +00:00
committed by GitHub
co-authored by Prospector
parent d5556271f9
commit e79e0d024b
347 changed files with 12456 additions and 4505 deletions
@@ -2,7 +2,8 @@
use crate::state::instances::{
ContentSet, ContentSetStatus, ContentSetSyncStatus, ContentSourceKind,
Instance, InstanceLaunchContext, InstanceLaunchOverrides,
Instance, InstanceIconBackground, InstanceIconConfig,
InstanceLaunchContext, InstanceLaunchOverrides,
InstanceLaunchOverridesData, InstanceLink, SharedInstanceAttachment,
SharedInstanceRole, playtime_to_storage,
};
@@ -12,6 +13,7 @@ use crate::state::{
use chrono::{DateTime, TimeZone, Utc};
use serde::de::DeserializeOwned;
use sqlx::{Executor, Sqlite, SqlitePool, Transaction};
use std::collections::HashSet;
use uuid::Uuid;
#[derive(Debug, sqlx::FromRow)]
@@ -161,10 +163,11 @@ pub(crate) struct InstanceLaunchOverridesRow {
#[derive(Debug)]
pub(crate) struct InstanceMetadataRecord {
pub instance: Instance,
pub icon_config: Option<InstanceIconConfig>,
pub applied_content_set: ContentSet,
pub link: InstanceLink,
pub shared_instance: Option<SharedInstanceAttachment>,
pub groups: Vec<String>,
pub group_ids: Vec<String>,
pub launch_overrides: InstanceLaunchOverrides,
}
@@ -184,6 +187,8 @@ struct InstanceMetadataRow {
update_channel: String,
name: String,
icon_path: Option<String>,
icon_config_background: Option<String>,
icon_config_symbol: Option<String>,
created: i64,
modified: i64,
last_played: Option<i64>,
@@ -221,7 +226,7 @@ struct InstanceMetadataRow {
imported_name: Option<String>,
imported_version_number: Option<String>,
imported_filename: Option<String>,
groups: String,
group_ids: String,
launch_overrides: Option<String>,
}
@@ -333,16 +338,31 @@ impl InstanceMetadataRow {
self.shared_sync_applied_update_id,
self.shared_sync_latest_available_update_id,
)?;
let groups = parse_groups(self.groups)?;
let group_ids = parse_group_ids(self.group_ids)?;
let launch_overrides =
launch_overrides_from_json(instance_id, self.launch_overrides)?;
let icon_config =
match (self.icon_config_background, self.icon_config_symbol) {
(Some(background), Some(symbol)) => Some(InstanceIconConfig {
background: deserialize_icon_background(background)?,
symbol,
}),
(None, None) => None,
_ => {
return Err(crate::ErrorKind::InputError(
"Instance icon config is incomplete".to_string(),
)
.into());
}
};
Ok(InstanceMetadataRecord {
instance,
icon_config,
applied_content_set,
link,
shared_instance,
groups,
group_ids,
launch_overrides,
})
}
@@ -538,6 +558,8 @@ macro_rules! query_instance_metadata {
i.update_channel AS "update_channel!: String",
i.name AS "name!: String",
i.icon_path AS "icon_path?: String",
config.background AS "icon_config_background?: String",
config.symbol AS "icon_config_symbol?: String",
i.created AS "created!: i64",
i.modified AS "modified!: i64",
i.last_played AS "last_played?: i64",
@@ -576,14 +598,16 @@ macro_rules! query_instance_metadata {
link.imported_version_number AS "imported_version_number?: String",
link.imported_filename AS "imported_filename?: String",
COALESCE((
SELECT json_group_array(group_name)
SELECT json_group_array(id)
FROM (
SELECT group_name
FROM instance_groups
WHERE instance_id = i.id
ORDER BY group_name
SELECT groups.id
FROM instance_group_memberships memberships
INNER JOIN instance_groups groups
ON groups.id = memberships.group_id
WHERE memberships.instance_id = i.id
ORDER BY groups.name
)
), '[]') AS "groups!: String",
), '[]') AS "group_ids!: String",
json(overrides.overrides) AS "launch_overrides?: String"
"#
+ $from
@@ -598,6 +622,8 @@ macro_rules! query_instance_metadata {
AND sync.provider = 'shared_instance'
LEFT JOIN instance_launch_overrides overrides
ON overrides.instance_id = i.id
LEFT JOIN instance_icon_configs config
ON config.instance_id = i.id
"#
+ $suffix,
$arg,
@@ -605,6 +631,157 @@ macro_rules! query_instance_metadata {
};
}
pub(crate) async fn update_instance_icon_config(
instance_id: &str,
config: Option<&InstanceIconConfig>,
tx: &mut Transaction<'_, Sqlite>,
) -> crate::Result<()> {
if let Some(config) = config {
let background = serde_json::to_string(&config.background)?;
let symbol = &config.symbol;
sqlx::query(
"
INSERT INTO instance_icon_configs (instance_id, background, symbol)
VALUES (?, ?, ?)
ON CONFLICT (instance_id) DO UPDATE SET
background = excluded.background,
symbol = excluded.symbol
",
)
.bind(instance_id)
.bind(&background)
.bind(symbol)
.execute(&mut **tx)
.await?;
update_recent_instance_icon_config(config, tx).await?;
} else {
sqlx::query(
"
DELETE FROM instance_icon_configs
WHERE instance_id = ?
",
)
.bind(instance_id)
.execute(&mut **tx)
.await?;
}
Ok(())
}
pub(crate) async fn update_instance_icon_if_empty(
instance_id: &str,
icon_path: &str,
config: &InstanceIconConfig,
pool: &SqlitePool,
) -> crate::Result<bool> {
let modified = Utc::now().timestamp();
let mut tx = pool.begin().await?;
let result = sqlx::query(
"
UPDATE instances
SET icon_path = ?, modified = ?
WHERE id = ? AND (icon_path IS NULL OR icon_path = '')
",
)
.bind(icon_path)
.bind(modified)
.bind(instance_id)
.execute(&mut *tx)
.await?;
if result.rows_affected() == 0 {
tx.rollback().await?;
return Ok(false);
}
update_instance_icon_config(instance_id, Some(config), &mut tx).await?;
tx.commit().await?;
Ok(true)
}
pub(crate) async fn update_recent_instance_icon_config(
config: &InstanceIconConfig,
tx: &mut Transaction<'_, Sqlite>,
) -> crate::Result<()> {
let background = serde_json::to_string(&config.background)?;
let used_at = Utc::now().timestamp_millis();
sqlx::query(
"
INSERT INTO recent_instance_icon_configs (background, symbol, used_at)
VALUES (?, ?, ?)
ON CONFLICT (background, symbol) DO UPDATE SET
used_at = excluded.used_at
",
)
.bind(&background)
.bind(&config.symbol)
.bind(used_at)
.execute(&mut **tx)
.await?;
sqlx::query(
"
DELETE FROM recent_instance_icon_configs
WHERE rowid NOT IN (
SELECT rowid
FROM recent_instance_icon_configs
ORDER BY used_at DESC, background, symbol
LIMIT 8
)
",
)
.execute(&mut **tx)
.await?;
Ok(())
}
pub(crate) async fn get_recent_instance_icon_configs(
pool: &SqlitePool,
) -> crate::Result<Vec<InstanceIconConfig>> {
let rows = sqlx::query_as::<_, InstanceIconConfigRow>(
"
SELECT background, symbol
FROM recent_instance_icon_configs
ORDER BY used_at DESC, background, symbol
LIMIT 8
",
)
.fetch_all(pool)
.await?;
rows.into_iter()
.map(|row| {
Ok(InstanceIconConfig {
background: deserialize_icon_background(row.background)?,
symbol: row.symbol,
})
})
.collect()
}
#[derive(sqlx::FromRow)]
struct InstanceIconConfigRow {
background: String,
symbol: String,
}
fn deserialize_icon_background(
background: String,
) -> crate::Result<InstanceIconBackground> {
match serde_json::from_str(&background) {
Ok(background) => Ok(background),
Err(_) if background.starts_with('#') => {
Ok(InstanceIconBackground::Color { value: background })
}
Err(error) => Err(error.into()),
}
}
pub(crate) async fn get_instance_metadata_by_id(
id: &str,
pool: &SqlitePool,
@@ -746,10 +923,12 @@ where
{
let rows = sqlx::query_scalar!(
"
SELECT group_name
FROM instance_groups
WHERE instance_id = ?
ORDER BY group_name
SELECT groups.id
FROM instance_group_memberships memberships
INNER JOIN instance_groups groups
ON groups.id = memberships.group_id
WHERE memberships.instance_id = ?
ORDER BY groups.name
",
instance_id,
)
@@ -759,6 +938,122 @@ where
Ok(rows)
}
pub(crate) async fn list_instance_groups(
pool: &SqlitePool,
) -> crate::Result<Vec<(String, String)>> {
let groups = sqlx::query_as::<_, (String, String)>(
"
SELECT id, name
FROM instance_groups
ORDER BY display_order, name, id
",
)
.fetch_all(pool)
.await?;
Ok(groups)
}
pub(crate) async fn create_instance_group(
id: &str,
name: &str,
pool: &SqlitePool,
) -> crate::Result<()> {
let mut tx = pool.begin().await?;
sqlx::query(
"
UPDATE instance_groups
SET display_order = display_order + 1
WHERE id != ?
",
)
.bind(crate::api::instance::FAVORITES_GROUP_ID)
.execute(&mut *tx)
.await?;
sqlx::query(
"
INSERT INTO instance_groups (id, name, display_order)
VALUES (?, ?, 0)
",
)
.bind(id)
.bind(name)
.execute(&mut *tx)
.await?;
tx.commit().await?;
Ok(())
}
pub(crate) async fn set_instance_group_order(
group_ids: &[String],
pool: &SqlitePool,
) -> crate::Result<()> {
let mut tx = pool.begin().await?;
let existing_group_ids = sqlx::query_scalar::<_, String>(
"
SELECT id
FROM instance_groups
WHERE id != ?
ORDER BY display_order, name, id
",
)
.bind(crate::api::instance::FAVORITES_GROUP_ID)
.fetch_all(&mut *tx)
.await?;
let existing_group_id_set = existing_group_ids
.iter()
.map(String::as_str)
.collect::<HashSet<_>>();
let mut seen_group_ids = HashSet::new();
let mut ordered_group_ids = Vec::with_capacity(existing_group_ids.len());
for group_id in group_ids {
if !seen_group_ids.insert(group_id.as_str()) {
return Err(crate::ErrorKind::InputError(format!(
"Duplicate instance group {group_id} in group order"
))
.into());
}
if !existing_group_id_set.contains(group_id.as_str()) {
return Err(crate::ErrorKind::InputError(format!(
"Unknown instance group {group_id}"
))
.into());
}
ordered_group_ids.push(group_id.as_str());
}
for group_id in &existing_group_ids {
if seen_group_ids.insert(group_id.as_str()) {
ordered_group_ids.push(group_id.as_str());
}
}
for (display_order, group_id) in ordered_group_ids.into_iter().enumerate() {
sqlx::query(
"
UPDATE instance_groups
SET display_order = ?
WHERE id = ?
",
)
.bind(display_order as i64)
.bind(group_id)
.execute(&mut *tx)
.await?;
}
tx.commit().await?;
Ok(())
}
pub(crate) async fn get_instance_launch_overrides<'e, E>(
instance_id: &str,
exec: E,
@@ -1032,12 +1327,12 @@ pub(crate) async fn set_shared_instance_attachment(
pub(crate) async fn replace_instance_groups(
instance_id: &str,
groups: &[String],
group_ids: &[String],
tx: &mut Transaction<'_, Sqlite>,
) -> crate::Result<()> {
sqlx::query!(
"
DELETE FROM instance_groups
DELETE FROM instance_group_memberships
WHERE instance_id = ?
",
instance_id,
@@ -1045,15 +1340,18 @@ pub(crate) async fn replace_instance_groups(
.execute(&mut **tx)
.await?;
for group in groups {
sqlx::query!(
for group_id in group_ids {
sqlx::query(
"
INSERT OR IGNORE INTO instance_groups (instance_id, group_name)
INSERT OR IGNORE INTO instance_group_memberships (
instance_id,
group_id
)
VALUES (?, ?)
",
instance_id,
group,
)
.bind(instance_id)
.bind(group_id)
.execute(&mut **tx)
.await?;
}
@@ -1264,7 +1562,7 @@ fn required_i64(value: Option<i64>, column: &str) -> crate::Result<i64> {
})
}
fn parse_groups(value: String) -> crate::Result<Vec<String>> {
fn parse_group_ids(value: String) -> crate::Result<Vec<String>> {
serde_json::from_str(&value).map_err(|err| {
crate::ErrorKind::InputError(format!(
"Invalid instance groups JSON: {err}"
@@ -5,8 +5,8 @@ use crate::state::instances::{
adapters::sqlite::{content_rows, instance_rows},
};
use crate::state::{
InstanceInstallStage, LauncherFeatureVersion, ModLoader, ReleaseChannel,
State,
InstanceIconConfig, InstanceInstallStage, LauncherFeatureVersion,
ModLoader, ReleaseChannel, State,
};
use crate::util::fetch;
use crate::util::io;
@@ -23,6 +23,7 @@ pub struct CreateInstance {
pub loader: ModLoader,
pub loader_version: Option<String>,
pub icon_path: Option<String>,
pub icon_config: Option<InstanceIconConfig>,
pub link: InstanceLink,
}
@@ -97,6 +98,14 @@ pub(crate) async fn create_instance(
let mut tx = state.pool.begin().await?;
instance_rows::insert_instance(&instance, &mut tx).await?;
if let Some(icon_config) = &input.icon_config {
instance_rows::update_instance_icon_config(
&instance_id,
Some(icon_config),
&mut tx,
)
.await?;
}
content_rows::insert_content_set(&content_set, &mut tx).await?;
instance_rows::upsert_instance_link(&instance_id, &input.link, &mut tx)
.await?;
@@ -170,7 +179,7 @@ async fn path_available(
.is_none())
}
async fn resolve_icon_path(
pub(crate) async fn resolve_icon_path(
icon_path: Option<&str>,
ignore_missing_remote_icon: bool,
state: &State,
@@ -1,5 +1,6 @@
use crate::state::instances::{
ContentSourceKind, Instance, InstanceLaunchOverrides, InstanceLink,
ContentSourceKind, Instance, InstanceIconConfig, InstanceLaunchOverrides,
InstanceLink,
adapters::sqlite::{content_rows, instance_rows},
};
use crate::state::{
@@ -21,8 +22,14 @@ pub struct EditInstance {
with = "serde_with::rust::double_option"
)]
pub icon_path: Option<Option<String>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "serde_with::rust::double_option"
)]
pub icon_config: Option<Option<InstanceIconConfig>>,
pub update_channel: Option<ReleaseChannel>,
pub groups: Option<Vec<String>>,
pub group_ids: Option<Vec<String>>,
pub link: Option<InstanceLink>,
pub launch_overrides: Option<InstanceLaunchOverridesPatch>,
pub content_set_patch: Option<AppliedContentSetPatch>,
@@ -174,9 +181,13 @@ pub(crate) async fn edit_instance(
.await?;
}
if let Some(groups) = &patch.groups {
instance_rows::replace_instance_groups(&instance.id, groups, &mut tx)
.await?;
if let Some(group_ids) = &patch.group_ids {
instance_rows::replace_instance_groups(
&instance.id,
group_ids,
&mut tx,
)
.await?;
}
if let Some(overrides) = launch_overrides.as_mut() {
@@ -184,6 +195,18 @@ pub(crate) async fn edit_instance(
.await?;
}
if let Some(icon_config) = &patch.icon_config {
instance_rows::update_instance_icon_config(
&instance.id,
icon_config.as_ref(),
&mut tx,
)
.await?;
} else if patch.icon_path.is_some() {
instance_rows::update_instance_icon_config(&instance.id, None, &mut tx)
.await?;
}
tx.commit().await?;
if should_mark_shared_instance_stale {
@@ -1,6 +1,6 @@
use crate::state::instances::{
ContentSet, Instance, InstanceLaunchOverrides, InstanceLink,
SharedInstanceAttachment, adapters::sqlite::instance_rows,
ContentSet, Instance, InstanceIconConfig, InstanceLaunchOverrides,
InstanceLink, SharedInstanceAttachment, adapters::sqlite::instance_rows,
};
use serde::{Deserialize, Serialize};
use sqlx::SqlitePool;
@@ -8,12 +8,14 @@ use sqlx::SqlitePool;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct InstanceMetadata {
pub instance: Instance,
#[serde(default)]
pub icon_config: Option<InstanceIconConfig>,
pub applied_content_set: ContentSet,
pub link: InstanceLink,
pub shared_instance: Option<SharedInstanceAttachment>,
#[serde(default)]
pub quarantined: bool,
pub groups: Vec<String>,
pub group_ids: Vec<String>,
pub launch_overrides: InstanceLaunchOverrides,
}
@@ -79,11 +81,12 @@ fn instance_metadata(
) -> InstanceMetadata {
InstanceMetadata {
instance: record.instance,
icon_config: record.icon_config,
applied_content_set: record.applied_content_set,
link: record.link,
shared_instance: record.shared_instance,
quarantined,
groups: record.groups,
group_ids: record.group_ids,
launch_overrides: record.launch_overrides,
}
}
@@ -1,6 +1,6 @@
mod create_instance;
pub use self::create_instance::CreateInstance;
pub(crate) use self::create_instance::create_instance;
pub(crate) use self::create_instance::{create_instance, resolve_icon_path};
mod edit_instance;
pub(crate) use self::edit_instance::edit_instance;
@@ -4,6 +4,25 @@ use crate::state::{
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum InstanceIconBackground {
Color {
value: String,
},
#[serde(rename = "linear-top-down-gradient")]
LinearTopDownGradient {
top_color: String,
bottom_color: String,
},
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct InstanceIconConfig {
pub background: InstanceIconBackground,
pub symbol: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Instance {
pub id: String,
+35 -7
View File
@@ -63,7 +63,6 @@ where
settings.telemetry = !legacy_settings.opt_out_analytics;
settings.discord_rpc = !legacy_settings.disable_discord_rpc;
settings.developer_mode = legacy_settings.developer_mode;
settings.onboarded = legacy_settings.fully_onboarded;
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;
@@ -588,14 +587,45 @@ where
.await?;
for group in input.groups {
sqlx::query!(
let group_id = match sqlx::query_scalar::<_, String>(
"
INSERT OR IGNORE INTO instance_groups (instance_id, group_name)
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 (?, ?)
",
instance_id_str,
group,
)
.bind(instance_id_str)
.bind(group_id)
.execute(exec)
.await?;
}
@@ -659,8 +689,6 @@ struct LegacySettings {
pub opt_out_analytics: bool,
#[serde(default)]
pub advanced_rendering: bool,
#[serde(default)]
pub fully_onboarded: bool,
#[serde(default = "default_settings_dir")]
pub loaded_config_dir: Option<PathBuf>,
}
+3
View File
@@ -21,6 +21,9 @@ pub use self::instances::*;
mod settings;
pub use self::settings::*;
mod onboarding_checklist;
pub use self::onboarding_checklist::*;
mod process;
pub use self::process::*;
@@ -0,0 +1,106 @@
use sqlx::{Executor, Sqlite};
#[derive(Clone, Debug)]
#[cfg_attr(
feature = "export-ts",
derive(ts_rs::TS, postcard_bindgen::PostcardBindings)
)]
#[serde_binhum::serde_binhum]
pub struct OnboardingChecklist {
pub has_created_instance: bool,
pub has_logged_into_minecraft: bool,
pub has_logged_into_modrinth: bool,
pub show_checklist: bool,
}
pub(crate) enum OnboardingChecklistItem {
CreatedInstance,
LoggedIntoMinecraft,
LoggedIntoModrinth,
}
pub(crate) async fn get_onboarding_checklist(
exec: impl Executor<'_, Database = Sqlite>,
) -> crate::Result<OnboardingChecklist> {
let row = sqlx::query!(
"
SELECT
has_created_instance,
has_logged_into_minecraft,
has_logged_into_modrinth,
show_checklist
FROM onboarding_checklist
WHERE id = 0
",
)
.fetch_one(exec)
.await?;
Ok(OnboardingChecklist {
has_created_instance: row.has_created_instance == 1,
has_logged_into_minecraft: row.has_logged_into_minecraft == 1,
has_logged_into_modrinth: row.has_logged_into_modrinth == 1,
show_checklist: row.show_checklist == 1,
})
}
pub(crate) async fn mark_onboarding_checklist_item(
item: OnboardingChecklistItem,
pool: &sqlx::SqlitePool,
) -> crate::Result<Option<OnboardingChecklist>> {
let result = match item {
OnboardingChecklistItem::CreatedInstance => {
sqlx::query!(
"
UPDATE onboarding_checklist
SET has_created_instance = TRUE
WHERE id = 0 AND has_created_instance = FALSE
",
)
.execute(pool)
.await?
}
OnboardingChecklistItem::LoggedIntoMinecraft => {
sqlx::query!(
"
UPDATE onboarding_checklist
SET has_logged_into_minecraft = TRUE
WHERE id = 0 AND has_logged_into_minecraft = FALSE
",
)
.execute(pool)
.await?
}
OnboardingChecklistItem::LoggedIntoModrinth => {
sqlx::query!(
"
UPDATE onboarding_checklist
SET has_logged_into_modrinth = TRUE
WHERE id = 0 AND has_logged_into_modrinth = FALSE
",
)
.execute(pool)
.await?
}
};
if result.rows_affected() == 0 {
return Ok(None);
}
sqlx::query!(
"
UPDATE onboarding_checklist
SET show_checklist = FALSE
WHERE id = 0
AND show_checklist = TRUE
AND has_created_instance = TRUE
AND has_logged_into_minecraft = TRUE
AND has_logged_into_modrinth = TRUE
"
)
.execute(pool)
.await?;
Ok(Some(get_onboarding_checklist(pool).await?))
}
+20 -27
View File
@@ -24,8 +24,6 @@ pub struct Settings {
pub discord_rpc: bool,
pub personalized_ads: bool,
pub onboarded: bool,
pub extra_launch_args: Vec<String>,
pub custom_env_vars: Vec<(String, String)>,
pub memory: MemorySettings,
@@ -85,7 +83,6 @@ impl Settings {
max_concurrent_writes, max_concurrent_downloads,
theme, locale, default_page, collapsed_navigation, hide_nametag_skins_page, advanced_rendering, native_decorations,
discord_rpc, developer_mode, telemetry, personalized_ads,
onboarded,
json(extra_launch_args) extra_launch_args, json(custom_env_vars) custom_env_vars,
mc_memory_max, mc_force_fullscreen, mc_game_resolution_x, mc_game_resolution_y, hide_on_process_start,
hook_pre_launch, hook_wrapper, hook_post_exit,
@@ -113,7 +110,6 @@ impl Settings {
discord_rpc: res.discord_rpc == 1,
developer_mode: res.developer_mode == 1,
personalized_ads: res.personalized_ads == 1,
onboarded: res.onboarded == 1,
extra_launch_args: res
.extra_launch_args
.as_ref()
@@ -186,33 +182,31 @@ impl Settings {
telemetry = $11,
personalized_ads = $12,
onboarded = $13,
extra_launch_args = jsonb($13),
custom_env_vars = jsonb($14),
mc_memory_max = $15,
mc_force_fullscreen = $16,
mc_game_resolution_x = $17,
mc_game_resolution_y = $18,
hide_on_process_start = $19,
extra_launch_args = jsonb($14),
custom_env_vars = jsonb($15),
mc_memory_max = $16,
mc_force_fullscreen = $17,
mc_game_resolution_x = $18,
mc_game_resolution_y = $19,
hide_on_process_start = $20,
hook_pre_launch = $20,
hook_wrapper = $21,
hook_post_exit = $22,
hook_pre_launch = $21,
hook_wrapper = $22,
hook_post_exit = $23,
custom_dir = $23,
prev_custom_dir = $24,
migrated = $25,
custom_dir = $24,
prev_custom_dir = $25,
migrated = $26,
toggle_sidebar = $26,
feature_flags = $27,
hide_nametag_skins_page = $28,
toggle_sidebar = $27,
feature_flags = $28,
hide_nametag_skins_page = $29,
skipped_update = $29,
pending_update_toast_for_version = $30,
auto_download_updates = $31,
skipped_update = $30,
pending_update_toast_for_version = $31,
auto_download_updates = $32,
version = $33
version = $32
",
max_concurrent_writes,
max_concurrent_downloads,
@@ -226,7 +220,6 @@ impl Settings {
self.developer_mode,
self.telemetry,
self.personalized_ads,
self.onboarded,
extra_launch_args,
custom_env_vars,
self.memory.maximum,