Library improvements (#126)

* Base impl

* Add grouping

* Fix some styling things

* Run linter

* add missing features

* add dev mode

---------

Co-authored-by: Geometrically <18202329+Geometrically@users.noreply.github.com>
Co-authored-by: Jai A <jaiagr+gpg@pm.me>
This commit is contained in:
Adrian O.V 2023-06-02 18:36:10 -04:00 committed by GitHub
parent 72fc215641
commit e0e9c3f166
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 398 additions and 88 deletions

13
Cargo.lock generated
View File

@ -53,6 +53,12 @@ dependencies = [
"alloc-no-stdlib", "alloc-no-stdlib",
] ]
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]] [[package]]
name = "android_system_properties" name = "android_system_properties"
version = "0.1.5" version = "0.1.5"
@ -603,13 +609,13 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]] [[package]]
name = "chrono" name = "chrono"
version = "0.4.24" version = "0.4.26"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5"
dependencies = [ dependencies = [
"android-tzdata",
"iana-time-zone", "iana-time-zone",
"js-sys", "js-sys",
"num-integer",
"num-traits", "num-traits",
"serde", "serde",
"time 0.1.45", "time 0.1.45",
@ -4274,6 +4280,7 @@ dependencies = [
name = "theseus_gui" name = "theseus_gui"
version = "0.0.0" version = "0.0.0"
dependencies = [ dependencies = [
"chrono",
"daedalus", "daedalus",
"futures", "futures",
"os_info", "os_info",

View File

@ -1,6 +1,6 @@
//! Authentication flow interface //! Authentication flow interface
use crate::{launcher::auth as inner, State}; use crate::{launcher::auth as inner, State};
use futures::prelude::*; use chrono::Utc;
use tokio::sync::oneshot; use tokio::sync::oneshot;
use crate::state::AuthTask; use crate::state::AuthTask;
@ -71,22 +71,20 @@ pub async fn refresh(user: uuid::Uuid) -> crate::Result<Credentials> {
let state = State::get().await?; let state = State::get().await?;
let mut users = state.users.write().await; let mut users = state.users.write().await;
let fetch_semaphore = &state.fetch_semaphore; let mut credentials = users.get(user).ok_or_else(|| {
futures::future::ready(users.get(user).ok_or_else(|| { crate::ErrorKind::OtherError(
crate::ErrorKind::OtherError(format!( "You are not logged in with a Minecraft account!".to_string(),
"You are not logged in with a Minecraft account!" )
))
.as_error() .as_error()
})) })?;
.and_then(|mut credentials| async move {
if chrono::offset::Utc::now() > credentials.expires { let fetch_semaphore = &state.fetch_semaphore;
inner::refresh_credentials(&mut credentials, fetch_semaphore) if Utc::now() > credentials.expires {
.await?; inner::refresh_credentials(&mut credentials, fetch_semaphore).await?;
} }
users.insert(&credentials).await?; users.insert(&credentials).await?;
Ok(credentials)
}) Ok(credentials)
.await
} }
/// Remove a user account from the database /// Remove a user account from the database

View File

@ -9,6 +9,7 @@ use crate::{
state::{self as st, MinecraftChild}, state::{self as st, MinecraftChild},
State, State,
}; };
use chrono::Utc;
use daedalus as d; use daedalus as d;
use daedalus::minecraft::VersionInfo; use daedalus::minecraft::VersionInfo;
use dunce::canonicalize; use dunce::canonicalize;
@ -432,6 +433,13 @@ pub async fn launch_minecraft(
let stdout_log_path = logs_dir.join("stdout.log"); let stdout_log_path = logs_dir.join("stdout.log");
let stderr_log_path = logs_dir.join("stderr.log"); let stderr_log_path = logs_dir.join("stderr.log");
crate::api::profile::edit(&profile.path, |prof| {
prof.metadata.last_played = Some(Utc::now());
async { Ok(()) }
})
.await?;
// Create Minecraft child by inserting it into the state // Create Minecraft child by inserting it into the state
// This also spawns the process and prepares the subsequent processes // This also spawns the process and prepares the subsequent processes
let mut state_children = state.children.write().await; let mut state_children = state.children.write().await;

View File

@ -10,6 +10,7 @@ use crate::util::fetch::{
fetch, fetch_json, write, write_cached_icon, IoSemaphore, fetch, fetch_json, write, write_cached_icon, IoSemaphore,
}; };
use crate::State; use crate::State;
use chrono::{DateTime, Utc};
use daedalus::get_hash; use daedalus::get_hash;
use daedalus::modded::LoaderVersion; use daedalus::modded::LoaderVersion;
use dunce::canonicalize; use dunce::canonicalize;
@ -30,9 +31,6 @@ const PROFILE_JSON_PATH: &str = "profile.json";
pub(crate) struct Profiles(pub HashMap<PathBuf, Profile>); pub(crate) struct Profiles(pub HashMap<PathBuf, Profile>);
// TODO: possibly add defaults to some of these values
pub const CURRENT_FORMAT_VERSION: u32 = 1;
#[derive( #[derive(
Serialize, Deserialize, Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize, Clone, Copy, Debug, Default, Eq, PartialEq,
)] )]
@ -75,13 +73,24 @@ pub struct ProfileMetadata {
pub icon: Option<PathBuf>, pub icon: Option<PathBuf>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub icon_url: Option<String>, pub icon_url: Option<String>,
#[serde(default)]
pub groups: Vec<String>,
pub game_version: String, pub game_version: String,
#[serde(default)] #[serde(default)]
pub loader: ModLoader, pub loader: ModLoader,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub loader_version: Option<LoaderVersion>, pub loader_version: Option<LoaderVersion>,
pub format_version: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub linked_data: Option<LinkedData>, pub linked_data: Option<LinkedData>,
#[serde(default)]
pub date_created: DateTime<Utc>,
#[serde(default)]
pub date_modified: DateTime<Utc>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_played: Option<DateTime<Utc>>,
} }
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug)]
@ -157,11 +166,14 @@ impl Profile {
name, name,
icon: None, icon: None,
icon_url: None, icon_url: None,
groups: vec![],
game_version: version, game_version: version,
loader: ModLoader::Vanilla, loader: ModLoader::Vanilla,
loader_version: None, loader_version: None,
format_version: CURRENT_FORMAT_VERSION,
linked_data: None, linked_data: None,
date_created: Utc::now(),
date_modified: Utc::now(),
last_played: None,
}, },
projects: HashMap::new(), projects: HashMap::new(),
java: None, java: None,
@ -182,6 +194,7 @@ impl Profile {
let file = let file =
write_cached_icon(file_name, cache_dir, icon, semaphore).await?; write_cached_icon(file_name, cache_dir, icon, semaphore).await?;
self.metadata.icon = Some(file); self.metadata.icon = Some(file);
self.metadata.date_modified = Utc::now();
Ok(()) Ok(())
} }
@ -400,6 +413,7 @@ impl Profile {
file_name: file_name.to_string(), file_name: file_name.to_string(),
}, },
); );
profile.metadata.date_modified = Utc::now();
} }
} }
@ -446,6 +460,7 @@ impl Profile {
let mut profiles = state.profiles.write().await; let mut profiles = state.profiles.write().await;
if let Some(profile) = profiles.0.get_mut(&self.path) { if let Some(profile) = profiles.0.get_mut(&self.path) {
profile.projects.insert(new_path.clone(), project); profile.projects.insert(new_path.clone(), project);
profile.metadata.date_modified = Utc::now();
} }
Ok(new_path) Ok(new_path)
@ -471,6 +486,7 @@ impl Profile {
if let Some(profile) = profiles.0.get_mut(&self.path) { if let Some(profile) = profiles.0.get_mut(&self.path) {
profile.projects.remove(path); profile.projects.remove(path);
profile.metadata.date_modified = Utc::now();
} }
} }
} else { } else {

View File

@ -26,6 +26,8 @@ pub struct Settings {
pub max_concurrent_writes: usize, pub max_concurrent_writes: usize,
pub version: u32, pub version: u32,
pub collapsed_navigation: bool, pub collapsed_navigation: bool,
#[serde(default)]
pub developer_mode: bool,
} }
impl Default for Settings { impl Default for Settings {
@ -43,6 +45,7 @@ impl Default for Settings {
max_concurrent_writes: 10, max_concurrent_writes: 10,
version: CURRENT_FORMAT_VERSION, version: CURRENT_FORMAT_VERSION,
collapsed_navigation: false, collapsed_navigation: false,
developer_mode: false,
} }
} }
} }

93
theseus_gui/.eslintignore Normal file
View File

@ -0,0 +1,93 @@
node_modules
*.log*
.nuxt
.nitro
.cache
.output
.env
dist
*.md
package.json
dist/
generated/
!.gitkeep
# Created by .ignore support plugin (hsz.mobi)
### Node template
# Logs
/logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# Serverless directories
.serverless
# IDE / Editor
.idea
# Service worker
sw.*
# macOS
.DS_Store
# Vim swap files
*.swp
# pnpm files
pnpm-lock.yaml
/.npmrc
src-tauri/
*.rs

View File

@ -88,3 +88,6 @@ sw.*
# pnpm files # pnpm files
pnpm-lock.yaml pnpm-lock.yaml
/.npmrc /.npmrc
src-tauri/
*.rs

View File

@ -27,6 +27,7 @@ thiserror = "1.0"
tokio-stream = { version = "0.1", features = ["fs"] } tokio-stream = { version = "0.1", features = ["fs"] }
futures = "0.3" futures = "0.3"
daedalus = {version = "0.1.15", features = ["bincode"] } daedalus = {version = "0.1.15", features = ["bincode"] }
chrono = "0.4.26"
url = "2.2" url = "2.2"
uuid = { version = "1.1", features = ["serde", "v4"] } uuid = { version = "1.1", features = ["serde", "v4"] }

View File

@ -195,6 +195,7 @@ pub struct EditProfileMetadata {
pub game_version: Option<String>, pub game_version: Option<String>,
pub loader: Option<ModLoader>, pub loader: Option<ModLoader>,
pub loader_version: Option<LoaderVersion>, pub loader_version: Option<LoaderVersion>,
pub groups: Option<Vec<String>>,
} }
// Edits a profile // Edits a profile
@ -207,15 +208,19 @@ pub async fn profile_edit(
profile::edit(path, |prof| { profile::edit(path, |prof| {
if let Some(metadata) = edit_profile.metadata.clone() { if let Some(metadata) = edit_profile.metadata.clone() {
if let Some(name) = metadata.name { if let Some(name) = metadata.name {
prof.metadata.name = name prof.metadata.name = name;
} }
if let Some(game_version) = metadata.game_version { if let Some(game_version) = metadata.game_version {
prof.metadata.game_version = game_version prof.metadata.game_version = game_version;
} }
if let Some(loader) = metadata.loader { if let Some(loader) = metadata.loader {
prof.metadata.loader = loader prof.metadata.loader = loader;
}
prof.metadata.loader_version = metadata.loader_version;
if let Some(groups) = metadata.groups {
prof.metadata.groups = groups;
} }
prof.metadata.loader_version = metadata.loader_version
} }
prof.java = edit_profile.java.clone(); prof.java = edit_profile.java.clone();
@ -223,6 +228,8 @@ pub async fn profile_edit(
prof.resolution = edit_profile.resolution; prof.resolution = edit_profile.resolution;
prof.hooks = edit_profile.hooks.clone(); prof.hooks = edit_profile.hooks.clone();
prof.metadata.date_modified = chrono::Utc::now();
async { Ok(()) } async { Ok(()) }
}) })
.await?; .await?;

View File

@ -1,5 +1,8 @@
<script setup> <script setup>
import Instance from '@/components/ui/Instance.vue' import Instance from '@/components/ui/Instance.vue'
import { computed, ref } from 'vue'
import { SearchIcon, DropdownSelect, Card, formatCategoryHeader } from 'omorphia'
import dayjs from 'dayjs'
const props = defineProps({ const props = defineProps({
instances: { instances: {
@ -13,16 +16,143 @@ const props = defineProps({
default: '', default: '',
}, },
}) })
const search = ref('')
const group = ref('Category')
const filters = ref('All profiles')
const sortBy = ref('Name')
const filteredResults = computed(() => {
let instances = props.instances.filter((instance) => {
return instance.metadata.name.toLowerCase().includes(search.value.toLowerCase())
})
if (sortBy.value === 'Name') {
instances.sort((a, b) => {
return a.metadata.name.localeCompare(b.metadata.name)
})
}
if (sortBy.value === 'Game version') {
instances.sort((a, b) => {
return a.metadata.name.localeCompare(b.metadata.game_version)
})
}
if (sortBy.value === 'Last played') {
instances.sort((a, b) => {
return dayjs(b.metadata.last_played ?? 0).diff(dayjs(a.metadata.last_played ?? 0))
})
}
if (sortBy.value === 'Date created') {
instances.sort((a, b) => {
return dayjs(b.metadata.date_created).diff(dayjs(a.metadata.date_created))
})
}
if (sortBy.value === 'Date modified') {
instances.sort((a, b) => {
return dayjs(b.metadata.date_modified).diff(dayjs(a.metadata.date_modified))
})
}
if (filters.value === 'Custom instances') {
instances = instances.filter((instance) => {
return !instance.metadata?.linked_data
})
} else if (filters.value === 'Downloaded modpacks') {
instances = instances.filter((instance) => {
return instance.metadata?.linked_data
})
}
const instanceMap = new Map()
if (group.value === 'Loader') {
instances.forEach((instance) => {
const loader = formatCategoryHeader(instance.metadata.loader)
if (!instanceMap.has(loader)) {
instanceMap.set(loader, [])
}
instanceMap.get(loader).push(instance)
})
} else if (group.value === 'Game version') {
instances.forEach((instance) => {
if (!instanceMap.has(instance.metadata.game_version)) {
instanceMap.set(instance.metadata.game_version, [])
}
instanceMap.get(instance.metadata.game_version).push(instance)
})
} else if (group.value === 'Category') {
instances.forEach((instance) => {
if (instance.metadata.groups.length === 0) {
instance.metadata.groups.push('None')
}
for (const category of instance.metadata.groups) {
if (!instanceMap.has(category)) {
instanceMap.set(category, [])
}
instanceMap.get(category).push(instance)
}
})
} else {
return instanceMap.set('None', instances)
}
return instanceMap
})
</script> </script>
<template> <template>
<div class="row"> <Card class="header">
<div class="header"> <div class="iconified-input">
<p>{{ props.label }}</p> <SearchIcon />
<hr /> <input v-model="search" type="text" placeholder="Search" class="search-input" />
</div>
<div class="labeled_button">
<span>Sort by</span>
<DropdownSelect
v-model="sortBy"
class="sort-dropdown"
:options="['Name', 'Last played', 'Date created', 'Date modified', 'Game version']"
placeholder="Select..."
/>
</div>
<div class="labeled_button">
<span>Filter by</span>
<DropdownSelect
v-model="filters"
class="filter-dropdown"
:options="['All profiles', 'Custom instances', 'Downloaded modpacks']"
placeholder="Select..."
/>
</div>
<div class="labeled_button">
<span>Group by</span>
<DropdownSelect
v-model="group"
class="group-dropdown"
:options="['Category', 'Loader', 'Game version', 'None']"
placeholder="Select..."
/>
</div>
</Card>
<div
v-for="instanceSection in Array.from(filteredResults, ([key, value]) => ({ key, value }))"
:key="instanceSection.key"
class="row"
>
<div v-if="instanceSection.key !== 'None'" class="divider">
<p>{{ instanceSection.key }}</p>
<hr aria-hidden="true" />
</div> </div>
<section class="instances"> <section class="instances">
<Instance <Instance
v-for="instance in props.instances" v-for="instance in instanceSection.value"
:key="instance.id" :key="instance.id"
display="card" display="card"
:instance="instance" :instance="instance"
@ -34,17 +164,17 @@ const props = defineProps({
.row { .row {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: flex-start;
width: 100%; width: 100%;
padding: 1rem; padding: 1rem;
.header { .divider {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: inherit; align-items: center;
width: 100%; width: 100%;
margin-bottom: 1rem;
gap: 1rem; gap: 1rem;
margin-bottom: 1rem;
p { p {
margin: 0; margin: 0;
@ -59,50 +189,52 @@ const props = defineProps({
width: 100%; width: 100%;
border: none; border: none;
} }
.pagination {
display: inherit;
align-items: inherit;
svg {
background: var(--color-raised-bg);
border-radius: var(--radius-lg);
width: 1.3rem;
height: 1.2rem;
cursor: pointer;
margin-right: 0.5rem;
transition: all ease-in-out 0.1s;
&:hover {
filter: brightness(150%);
}
}
}
}
section {
display: flex;
align-items: inherit;
transition: all ease-in-out 0.4s;
gap: 1rem;
}
.instances {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
width: 100%;
gap: 1rem;
margin-right: auto;
scroll-behavior: smooth;
overflow-y: auto;
} }
} }
.dark-mode { .header {
.row { display: flex;
&:nth-child(odd) { flex-direction: row;
background-color: rgb(30, 31, 34); flex-wrap: wrap;
} justify-content: space-between;
gap: 1rem;
align-items: inherit;
margin: 1rem 1rem 0 !important;
padding: 1rem;
width: calc(100% - 2rem);
.iconified-input {
flex-grow: 1;
} }
.sort-dropdown {
width: 10rem;
}
.filter-dropdown {
width: 15rem;
}
.group-dropdown {
width: 10rem;
}
.labeled_button {
display: flex;
flex-direction: row;
align-items: center;
gap: 0.5rem;
white-space: nowrap;
}
}
.instances {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
width: 100%;
gap: 1rem;
margin-right: auto;
scroll-behavior: smooth;
overflow-y: auto;
} }
</style> </style>

View File

@ -152,9 +152,7 @@ onUnmounted(() => {
.dark-mode { .dark-mode {
.row { .row {
&:nth-child(odd) { background-color: rgb(30, 31, 34);
background-color: rgb(30, 31, 34);
}
} }
} }

View File

@ -362,6 +362,7 @@ onUnmounted(() => unlisten())
padding: 0.75rem !important; /* overrides card class */ padding: 0.75rem !important; /* overrides card class */
transition: 0.1s ease-in-out all !important; /* overrides Omorphia defaults */ transition: 0.1s ease-in-out all !important; /* overrides Omorphia defaults */
background: hsl(220, 11%, 17%) !important; background: hsl(220, 11%, 17%) !important;
margin-bottom: 0;
&:hover { &:hover {
filter: brightness(1) !important; filter: brightness(1) !important;

View File

@ -7,6 +7,7 @@ import { profile_listener } from '@/helpers/events'
import { useBreadcrumbs } from '@/store/breadcrumbs' import { useBreadcrumbs } from '@/store/breadcrumbs'
import { useFetch } from '@/helpers/fetch.js' import { useFetch } from '@/helpers/fetch.js'
import { handleError } from '@/store/notifications.js' import { handleError } from '@/store/notifications.js'
import dayjs from 'dayjs'
const featuredModpacks = ref({}) const featuredModpacks = ref({})
const featuredMods = ref({}) const featuredMods = ref({})
@ -21,7 +22,9 @@ const recentInstances = shallowRef([])
const getInstances = async () => { const getInstances = async () => {
const profiles = await list(true).catch(handleError) const profiles = await list(true).catch(handleError)
recentInstances.value = Object.values(profiles) recentInstances.value = Object.values(profiles).sort((a, b) => {
return dayjs(b.metadata.last_played ?? 0).diff(dayjs(a.metadata.last_played ?? 0))
})
let filters = [] let filters = []
for (const instance of recentInstances.value) { for (const instance of recentInstances.value) {

View File

@ -19,7 +19,7 @@ fetchSettings.javaArgs = fetchSettings.custom_java_args.join(' ')
fetchSettings.envArgs = fetchSettings.custom_env_args.map((x) => x.join('=')).join(' ') fetchSettings.envArgs = fetchSettings.custom_env_args.map((x) => x.join('=')).join(' ')
const settings = ref(fetchSettings) const settings = ref(fetchSettings)
const maxMemory = ref((await get_max_memory().catch(handleError)) / 1024) const maxMemory = ref(Math.floor((await get_max_memory().catch(handleError)) / 1024))
watch(settings.value, async (oldSettings, newSettings) => { watch(settings.value, async (oldSettings, newSettings) => {
const setSettings = JSON.parse(JSON.stringify(newSettings)) const setSettings = JSON.parse(JSON.stringify(newSettings))

View File

@ -82,6 +82,35 @@
Edit versions Edit versions
</button> </button>
</div> </div>
<div class="adjacent-input">
<label>
<span class="label__title">Categories</span>
<span class="label__description">
Set the categories of this instance, for display in the library page. This is purely
cosmetic.
</span>
</label>
<multiselect
v-model="groups"
:options="availableGroups"
:multiple="true"
:searchable="true"
:show-no-results="false"
:close-on-select="false"
:clear-search-on-select="false"
:show-labels="false"
:taggable="true"
tag-placeholder="Add new category"
placeholder="Select categories..."
@tag="
(newTag) => {
groups.push(newTag)
availableGroups.push(newTag)
}
"
/>
</div>
</section> </section>
<Card> <Card>
<div class="label"> <div class="label">
@ -283,9 +312,10 @@ import {
SaveIcon, SaveIcon,
HammerIcon, HammerIcon,
} from 'omorphia' } from 'omorphia'
import { Multiselect } from 'vue-multiselect'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { edit, edit_icon, get_optimal_jre_key, install, remove } from '@/helpers/profile.js' import { edit, edit_icon, get_optimal_jre_key, install, list, remove } from '@/helpers/profile.js'
import { computed, onMounted, readonly, ref, shallowRef, watch } from 'vue' import { computed, readonly, ref, shallowRef, watch } from 'vue'
import { get_max_memory } from '@/helpers/jre.js' import { get_max_memory } from '@/helpers/jre.js'
import { get } from '@/helpers/settings.js' import { get } from '@/helpers/settings.js'
import JavaSelector from '@/components/ui/JavaSelector.vue' import JavaSelector from '@/components/ui/JavaSelector.vue'
@ -306,6 +336,16 @@ const props = defineProps({
const title = ref(props.instance.metadata.name) const title = ref(props.instance.metadata.name)
const icon = ref(props.instance.metadata.icon) const icon = ref(props.instance.metadata.icon)
const groups = ref(props.instance.metadata.groups)
const instancesList = Object.values(await list(true))
const availableGroups = ref([
...new Set(
instancesList.reduce((acc, obj) => {
return acc.concat(obj.metadata.groups)
}, [])
),
])
async function resetIcon() { async function resetIcon() {
icon.value = null icon.value = null
@ -347,7 +387,7 @@ const envVars = ref(
const overrideMemorySettings = ref(!!props.instance.memory) const overrideMemorySettings = ref(!!props.instance.memory)
const memory = ref(props.instance.memory ?? globalSettings.memory) const memory = ref(props.instance.memory ?? globalSettings.memory)
const maxMemory = (await get_max_memory().catch(handleError)) / 1024 const maxMemory = Math.floor((await get_max_memory().catch(handleError)) / 1024)
const overrideWindowSettings = ref(!!props.instance.resolution) const overrideWindowSettings = ref(!!props.instance.resolution)
const resolution = ref(props.instance.resolution ?? globalSettings.game_resolution) const resolution = ref(props.instance.resolution ?? globalSettings.game_resolution)
@ -358,6 +398,7 @@ const hooks = ref(props.instance.hooks ?? globalSettings.hooks)
watch( watch(
[ [
title, title,
groups.value,
overrideJavaInstall, overrideJavaInstall,
javaInstall, javaInstall,
overrideJavaArgs, overrideJavaArgs,
@ -375,6 +416,7 @@ watch(
const editProfile = { const editProfile = {
metadata: { metadata: {
name: title.value, name: title.value,
groups: groups.value,
}, },
java: {}, java: {},
} }
@ -520,8 +562,6 @@ async function saveGvLoaderEdits() {
editing.value = false editing.value = false
changeVersionsModal.value.hide() changeVersionsModal.value.hide()
} }
onMounted(() => console.log(loader.value))
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">