You've already forked AstralRinth
forked from didirus/AstralRinth
New instance settings in app (#3033)
* Tabbed interface component * Start instance settings * New instance settings, mostly done minus modpacks * Extract i18n * Some more fixes with settings, still no modpacks yet * Lint * Modpack installation settings * Change no friends language * Remove options legacy button * fix lint, small bug * fix invalid cond on friends ui * update resource management page --------- Signed-off-by: Geometrically <18202329+Geometrically@users.noreply.github.com> Co-authored-by: Jai A <jaiagr+gpg@pm.me> Co-authored-by: Geometrically <18202329+Geometrically@users.noreply.github.com>
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -9101,6 +9101,7 @@ dependencies = [
|
|||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"serde_with",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build 2.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
"tauri-build 2.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"tauri-plugin-deep-link",
|
"tauri-plugin-deep-link",
|
||||||
|
|||||||
@@ -357,6 +357,7 @@ function handleAuxClick(e) {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<SplashScreen v-if="!stateFailed" ref="splashScreen" data-tauri-drag-region />
|
<SplashScreen v-if="!stateFailed" ref="splashScreen" data-tauri-drag-region />
|
||||||
|
<div id="teleports"></div>
|
||||||
<div v-if="stateInitialized" class="app-grid-layout relative">
|
<div v-if="stateInitialized" class="app-grid-layout relative">
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<AppSettingsModal ref="settingsModal" />
|
<AppSettingsModal ref="settingsModal" />
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ const handleOptionsClick = async (args) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const search = ref('')
|
const search = ref('')
|
||||||
const group = ref('Category')
|
const group = ref('Group')
|
||||||
const sortBy = ref('Name')
|
const sortBy = ref('Name')
|
||||||
|
|
||||||
const filteredResults = computed(() => {
|
const filteredResults = computed(() => {
|
||||||
@@ -177,7 +177,7 @@ const filteredResults = computed(() => {
|
|||||||
|
|
||||||
instanceMap.get(instance.game_version).push(instance)
|
instanceMap.get(instance.game_version).push(instance)
|
||||||
})
|
})
|
||||||
} else if (group.value === 'Category') {
|
} else if (group.value === 'Group') {
|
||||||
instances.forEach((instance) => {
|
instances.forEach((instance) => {
|
||||||
if (instance.groups.length === 0) {
|
if (instance.groups.length === 0) {
|
||||||
instance.groups.push('None')
|
instance.groups.push('None')
|
||||||
@@ -242,7 +242,7 @@ const filteredResults = computed(() => {
|
|||||||
v-model="group"
|
v-model="group"
|
||||||
class="max-w-[16rem]"
|
class="max-w-[16rem]"
|
||||||
name="Group Dropdown"
|
name="Group Dropdown"
|
||||||
:options="['Category', 'Loader', 'Game version', 'None']"
|
:options="['Group', 'Loader', 'Game version', 'None']"
|
||||||
placeholder="Select..."
|
placeholder="Select..."
|
||||||
>
|
>
|
||||||
<span class="font-semibold text-primary">Group by: </span>
|
<span class="font-semibold text-primary">Group by: </span>
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ defineExpose({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['finish-install'])
|
||||||
|
|
||||||
const filteredVersions = computed(() => {
|
const filteredVersions = computed(() => {
|
||||||
return props.versions
|
return props.versions
|
||||||
})
|
})
|
||||||
@@ -34,9 +36,17 @@ const installing = computed(() => props.instance.install_stage !== 'installed')
|
|||||||
const inProgress = ref(false)
|
const inProgress = ref(false)
|
||||||
|
|
||||||
const switchVersion = async (versionId) => {
|
const switchVersion = async (versionId) => {
|
||||||
|
modpackVersionModal.value.hide()
|
||||||
inProgress.value = true
|
inProgress.value = true
|
||||||
await update_managed_modrinth_version(props.instance.path, versionId)
|
await update_managed_modrinth_version(props.instance.path, versionId)
|
||||||
inProgress.value = false
|
inProgress.value = false
|
||||||
|
emit('finish-install')
|
||||||
|
}
|
||||||
|
|
||||||
|
const onHide = () => {
|
||||||
|
if (!inProgress.value) {
|
||||||
|
emit('finish-install')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -45,9 +55,10 @@ const switchVersion = async (versionId) => {
|
|||||||
ref="modpackVersionModal"
|
ref="modpackVersionModal"
|
||||||
class="modpack-version-modal"
|
class="modpack-version-modal"
|
||||||
header="Change modpack version"
|
header="Change modpack version"
|
||||||
|
:on-hide="onHide"
|
||||||
>
|
>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<Card v-if="instance.linked_data" class="mod-card">
|
<div v-if="instance.linked_data" class="mod-card">
|
||||||
<div class="table">
|
<div class="table">
|
||||||
<div class="table-row with-columns table-head">
|
<div class="table-row with-columns table-head">
|
||||||
<div class="table-cell table-text download-cell" />
|
<div class="table-cell table-text download-cell" />
|
||||||
@@ -106,7 +117,7 @@ const switchVersion = async (versionId) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ModalWrapper ref="manageFriendsModal" header="Manage friends">
|
<ModalWrapper ref="manageFriendsModal" header="Manage friends">
|
||||||
<p v-if="acceptedFriends.length === 0">You have no friends :C</p>
|
<p v-if="acceptedFriends.length === 0">Add friends to share what you're playing!</p>
|
||||||
<div v-else class="flex flex-col gap-4 min-w-[20rem]">
|
<div v-else class="flex flex-col gap-4 min-w-[20rem]">
|
||||||
<input v-model="search" type="text" placeholder="Search friends..." class="w-full" />
|
<input v-model="search" type="text" placeholder="Search friends..." class="w-full" />
|
||||||
<div
|
<div
|
||||||
@@ -237,12 +237,12 @@ onUnmounted(() => {
|
|||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
<ModalWrapper ref="addFriendModal" header="Add a friend">
|
<ModalWrapper ref="addFriendModal" header="Add a friend">
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h2 class="m-0 text-xl">Username</h2>
|
<h2 class="m-0 text-lg font-extrabold text-contrast">Username</h2>
|
||||||
<p class="m-0 mt-1 leading-tight">You can add friends with their Modrinth username.</p>
|
<p class="m-0 mt-1 leading-tight">You can add friends with their Modrinth username.</p>
|
||||||
<input v-model="username" class="mt-2" type="text" placeholder="Enter username..." />
|
<input v-model="username" class="mt-2 w-full" type="text" placeholder="Enter username..." />
|
||||||
</div>
|
</div>
|
||||||
<ButtonStyled color="brand">
|
<ButtonStyled color="brand">
|
||||||
<button class="ml-auto" :disabled="username.length === 0" @click="addFriendFromModal">
|
<button :disabled="username.length === 0" @click="addFriendFromModal">
|
||||||
<UserPlusIcon />
|
<UserPlusIcon />
|
||||||
Add friend
|
Add friend
|
||||||
</button>
|
</button>
|
||||||
@@ -310,13 +310,12 @@ onUnmounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
<template v-else-if="acceptedFriends.length === 0">
|
<template v-else-if="acceptedFriends.length === 0">
|
||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
<div class="mb-2">You have no friends :C</div>
|
|
||||||
<div v-if="!userCredentials">
|
<div v-if="!userCredentials">
|
||||||
<span class="text-link cursor-pointer" @click="signIn">Sign in</span> to add friends!
|
<span class="text-link cursor-pointer" @click="signIn">Sign in</span> to add friends!
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
Why don't you
|
<span class="text-link cursor-pointer" @click="addFriendModal.show()">Add friends</span>
|
||||||
<span class="text-link cursor-pointer" @click="addFriendModal.show()">add one</span>?
|
to share what you're playing!
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,325 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||||
|
import { SpinnerIcon, TrashIcon, UploadIcon, PlusIcon, EditIcon, CopyIcon } from '@modrinth/assets'
|
||||||
|
import { Avatar, ButtonStyled, OverflowMenu, Checkbox } from '@modrinth/ui'
|
||||||
|
import { computed, ref, type Ref, watch } from 'vue'
|
||||||
|
import { duplicate, edit, edit_icon, list, remove } from '@/helpers/profile'
|
||||||
|
import { handleError } from '@/store/notifications'
|
||||||
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
|
import { open } from '@tauri-apps/plugin-dialog'
|
||||||
|
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
||||||
|
|
||||||
|
const { formatMessage } = useVIntl()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const deleteConfirmModal = ref()
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
instance: GameInstance
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const title = ref(props.instance.name)
|
||||||
|
const icon: Ref<string | undefined> = ref(props.instance.icon_path)
|
||||||
|
const groups = ref(props.instance.groups)
|
||||||
|
|
||||||
|
const newCategoryInput = ref('')
|
||||||
|
|
||||||
|
const installing = computed(() => props.instance.install_stage !== 'installed')
|
||||||
|
|
||||||
|
async function duplicateProfile() {
|
||||||
|
await duplicate(props.instance.path).catch(handleError)
|
||||||
|
trackEvent('InstanceDuplicate', {
|
||||||
|
loader: props.instance.loader,
|
||||||
|
game_version: props.instance.game_version,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const allInstances = ref((await list()) as GameInstance[])
|
||||||
|
const availableGroups = computed(() => [
|
||||||
|
...new Set([...allInstances.value.flatMap((instance) => instance.groups), ...groups.value]),
|
||||||
|
])
|
||||||
|
|
||||||
|
async function resetIcon() {
|
||||||
|
icon.value = undefined
|
||||||
|
await edit_icon(props.instance.path, null).catch(handleError)
|
||||||
|
trackEvent('InstanceRemoveIcon')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setIcon() {
|
||||||
|
const value = await open({
|
||||||
|
multiple: false,
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
name: 'Image',
|
||||||
|
extensions: ['png', 'jpeg', 'svg', 'webp', 'gif', 'jpg'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!value) return
|
||||||
|
|
||||||
|
icon.value = value
|
||||||
|
await edit_icon(props.instance.path, icon.value).catch(handleError)
|
||||||
|
|
||||||
|
trackEvent('InstanceSetIcon')
|
||||||
|
}
|
||||||
|
|
||||||
|
const editProfileObject = computed(() => ({
|
||||||
|
name: title.value.trim().substring(0, 32) ?? 'Instance',
|
||||||
|
groups: groups.value.map((x) => x.trim().substring(0, 32)).filter((x) => x.length > 0),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const toggleGroup = (group: string) => {
|
||||||
|
if (groups.value.includes(group)) {
|
||||||
|
groups.value = groups.value.filter((x) => x !== group)
|
||||||
|
} else {
|
||||||
|
groups.value.push(group)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const addCategory = () => {
|
||||||
|
const text = newCategoryInput.value.trim()
|
||||||
|
|
||||||
|
if (text.length > 0) {
|
||||||
|
groups.value.push(text.substring(0, 32))
|
||||||
|
newCategoryInput.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
[title, groups, groups],
|
||||||
|
async () => {
|
||||||
|
await edit(props.instance.path, editProfileObject.value)
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
const removing = ref(false)
|
||||||
|
async function removeProfile() {
|
||||||
|
removing.value = true
|
||||||
|
await remove(props.instance.path).catch(handleError)
|
||||||
|
removing.value = false
|
||||||
|
|
||||||
|
trackEvent('InstanceRemove', {
|
||||||
|
loader: props.instance.loader,
|
||||||
|
game_version: props.instance.game_version,
|
||||||
|
})
|
||||||
|
|
||||||
|
await router.push({ path: '/' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
name: {
|
||||||
|
id: 'instance.settings.tabs.general.name',
|
||||||
|
defaultMessage: 'Name',
|
||||||
|
},
|
||||||
|
libraryGroups: {
|
||||||
|
id: 'instance.settings.tabs.general.library-groups',
|
||||||
|
defaultMessage: 'Library groups',
|
||||||
|
},
|
||||||
|
libraryGroupsDescription: {
|
||||||
|
id: 'instance.settings.tabs.general.library-groups.description',
|
||||||
|
defaultMessage:
|
||||||
|
'Library groups allow you to organize your instances into different sections in your library.',
|
||||||
|
},
|
||||||
|
libraryGroupsEnterName: {
|
||||||
|
id: 'instance.settings.tabs.general.library-groups.enter-name',
|
||||||
|
defaultMessage: 'Enter group name',
|
||||||
|
},
|
||||||
|
libraryGroupsCreate: {
|
||||||
|
id: 'instance.settings.tabs.general.library-groups.create',
|
||||||
|
defaultMessage: 'Create new group',
|
||||||
|
},
|
||||||
|
editIcon: {
|
||||||
|
id: 'instance.settings.tabs.general.edit-icon',
|
||||||
|
defaultMessage: 'Edit icon',
|
||||||
|
},
|
||||||
|
selectIcon: {
|
||||||
|
id: 'instance.settings.tabs.general.edit-icon.select',
|
||||||
|
defaultMessage: 'Select icon',
|
||||||
|
},
|
||||||
|
replaceIcon: {
|
||||||
|
id: 'instance.settings.tabs.general.edit-icon.replace',
|
||||||
|
defaultMessage: 'Replace icon',
|
||||||
|
},
|
||||||
|
removeIcon: {
|
||||||
|
id: 'instance.settings.tabs.general.edit-icon.remove',
|
||||||
|
defaultMessage: 'Remove icon',
|
||||||
|
},
|
||||||
|
duplicateInstance: {
|
||||||
|
id: 'instance.settings.tabs.general.duplicate-instance',
|
||||||
|
defaultMessage: 'Duplicate instance',
|
||||||
|
},
|
||||||
|
duplicateInstanceDescription: {
|
||||||
|
id: 'instance.settings.tabs.general.duplicate-instance.description',
|
||||||
|
defaultMessage: 'Creates a copy of this instance, including worlds, configs, mods, etc.',
|
||||||
|
},
|
||||||
|
duplicateButtonTooltipInstalling: {
|
||||||
|
id: 'instance.settings.tabs.general.duplicate-button.tooltip.installing',
|
||||||
|
defaultMessage: 'Cannot duplicate while installing.',
|
||||||
|
},
|
||||||
|
duplicateButton: {
|
||||||
|
id: 'instance.settings.tabs.general.duplicate-button',
|
||||||
|
defaultMessage: 'Duplicate',
|
||||||
|
},
|
||||||
|
deleteInstance: {
|
||||||
|
id: 'instance.settings.tabs.general.delete',
|
||||||
|
defaultMessage: 'Delete instance',
|
||||||
|
},
|
||||||
|
deleteInstanceDescription: {
|
||||||
|
id: 'instance.settings.tabs.general.delete.description',
|
||||||
|
defaultMessage:
|
||||||
|
'Permanently deletes an instance from your device, including your worlds, configs, and all installed content. Be careful, as once you delete a instance there is no way to recover it.',
|
||||||
|
},
|
||||||
|
deleteInstanceButton: {
|
||||||
|
id: 'instance.settings.tabs.general.delete.button',
|
||||||
|
defaultMessage: 'Delete instance',
|
||||||
|
},
|
||||||
|
deletingInstanceButton: {
|
||||||
|
id: 'instance.settings.tabs.general.deleting.button',
|
||||||
|
defaultMessage: 'Deleting...',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ConfirmModalWrapper
|
||||||
|
ref="deleteConfirmModal"
|
||||||
|
title="Are you sure you want to delete this instance?"
|
||||||
|
description="If you proceed, all data for your instance will be permanently erased, including your worlds. You will not be able to recover it."
|
||||||
|
:has-to-type="false"
|
||||||
|
proceed-label="Delete"
|
||||||
|
@proceed="removeProfile"
|
||||||
|
/>
|
||||||
|
<div class="block">
|
||||||
|
<div class="float-end ml-4 relative group">
|
||||||
|
<OverflowMenu
|
||||||
|
v-tooltip="formatMessage(messages.editIcon)"
|
||||||
|
class="bg-transparent border-none appearance-none p-0 m-0 cursor-pointer group-active:scale-95 transition-transform"
|
||||||
|
:options="[
|
||||||
|
{
|
||||||
|
id: 'select',
|
||||||
|
action: () => setIcon(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'remove',
|
||||||
|
color: 'danger',
|
||||||
|
action: () => resetIcon(),
|
||||||
|
shown: !!icon,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
:src="icon ? convertFileSrc(icon) : icon"
|
||||||
|
size="108px"
|
||||||
|
class="!border-4 group-hover:brightness-75"
|
||||||
|
no-shadow
|
||||||
|
/>
|
||||||
|
<div class="absolute top-0 right-0 m-2">
|
||||||
|
<div
|
||||||
|
class="p-2 m-0 text-primary flex items-center justify-center aspect-square bg-button-bg rounded-full border-button-border border-solid border-[1px] hovering-icon-shadow"
|
||||||
|
>
|
||||||
|
<EditIcon aria-hidden="true" class="h-4 w-4 text-primary" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template #select>
|
||||||
|
<UploadIcon />
|
||||||
|
{{ icon ? formatMessage(messages.replaceIcon) : formatMessage(messages.selectIcon) }}
|
||||||
|
</template>
|
||||||
|
<template #remove> <TrashIcon /> {{ formatMessage(messages.removeIcon) }} </template>
|
||||||
|
</OverflowMenu>
|
||||||
|
</div>
|
||||||
|
<label for="instance-name" class="m-0 mb-1 text-lg font-extrabold text-contrast block">
|
||||||
|
{{ formatMessage(messages.name) }}
|
||||||
|
</label>
|
||||||
|
<div class="flex">
|
||||||
|
<input
|
||||||
|
id="instance-name"
|
||||||
|
v-model="title"
|
||||||
|
autocomplete="off"
|
||||||
|
maxlength="80"
|
||||||
|
class="flex-grow"
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<template v-if="instance.install_stage == 'installed'">
|
||||||
|
<div>
|
||||||
|
<h2
|
||||||
|
id="duplicate-instance-label"
|
||||||
|
class="m-0 mt-4 mb-1 text-lg font-extrabold text-contrast block"
|
||||||
|
>
|
||||||
|
{{ formatMessage(messages.duplicateInstance) }}
|
||||||
|
</h2>
|
||||||
|
<p class="m-0 mb-2">
|
||||||
|
{{ formatMessage(messages.duplicateInstanceDescription) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<ButtonStyled>
|
||||||
|
<button
|
||||||
|
v-tooltip="installing ? formatMessage(messages.duplicateButtonTooltipInstalling) : null"
|
||||||
|
aria-labelledby="duplicate-instance-label"
|
||||||
|
:disabled="installing"
|
||||||
|
@click="duplicateProfile"
|
||||||
|
>
|
||||||
|
<CopyIcon /> {{ formatMessage(messages.duplicateButton) }}
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</template>
|
||||||
|
<h2 class="m-0 mt-4 mb-1 text-lg font-extrabold text-contrast block">
|
||||||
|
{{ formatMessage(messages.libraryGroups) }}
|
||||||
|
</h2>
|
||||||
|
<p class="m-0 mb-2">
|
||||||
|
{{ formatMessage(messages.libraryGroupsDescription) }}
|
||||||
|
</p>
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<Checkbox
|
||||||
|
v-for="group in availableGroups"
|
||||||
|
:key="group"
|
||||||
|
:model-value="groups.includes(group)"
|
||||||
|
:label="group"
|
||||||
|
@click="toggleGroup(group)"
|
||||||
|
/>
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
|
<input
|
||||||
|
v-model="newCategoryInput"
|
||||||
|
type="text"
|
||||||
|
:placeholder="formatMessage(messages.libraryGroupsEnterName)"
|
||||||
|
@submit="() => addCategory"
|
||||||
|
/>
|
||||||
|
<ButtonStyled>
|
||||||
|
<button class="w-fit" @click="() => addCategory()">
|
||||||
|
<PlusIcon /> {{ formatMessage(messages.libraryGroupsCreate) }}
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h2 id="delete-instance-label" class="m-0 mt-4 mb-1 text-lg font-extrabold text-contrast block">
|
||||||
|
{{ formatMessage(messages.deleteInstance) }}
|
||||||
|
</h2>
|
||||||
|
<p class="m-0 mb-2">
|
||||||
|
{{ formatMessage(messages.deleteInstanceDescription) }}
|
||||||
|
</p>
|
||||||
|
<ButtonStyled color="red">
|
||||||
|
<button
|
||||||
|
aria-labelledby="delete-instance-label"
|
||||||
|
:disabled="removing"
|
||||||
|
@click="deleteConfirmModal.show()"
|
||||||
|
>
|
||||||
|
<SpinnerIcon v-if="removing" class="animate-spin" />
|
||||||
|
<TrashIcon v-else />
|
||||||
|
{{
|
||||||
|
removing
|
||||||
|
? formatMessage(messages.deletingInstanceButton)
|
||||||
|
: formatMessage(messages.deleteInstanceButton)
|
||||||
|
}}
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.hovering-icon-shadow {
|
||||||
|
box-shadow: var(--shadow-inset-sm), var(--shadow-raised);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Checkbox } from '@modrinth/ui'
|
||||||
|
import { computed, ref, watch } from 'vue'
|
||||||
|
import { handleError } from '@/store/notifications'
|
||||||
|
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||||
|
import { get } from '@/helpers/settings'
|
||||||
|
import { edit } from '@/helpers/profile'
|
||||||
|
|
||||||
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
instance: GameInstance
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const globalSettings = (await get().catch(handleError)) as AppSettings
|
||||||
|
|
||||||
|
const overrideHooks = ref(
|
||||||
|
!!props.instance.hooks.pre_launch ||
|
||||||
|
!!props.instance.hooks.wrapper ||
|
||||||
|
!!props.instance.hooks.post_exit,
|
||||||
|
)
|
||||||
|
const hooks = ref(props.instance.hooks ?? globalSettings.hooks)
|
||||||
|
|
||||||
|
const editProfileObject = computed(() => {
|
||||||
|
const editProfile: {
|
||||||
|
hooks?: Hooks
|
||||||
|
} = {}
|
||||||
|
|
||||||
|
if (overrideHooks.value) {
|
||||||
|
editProfile.hooks = hooks.value
|
||||||
|
}
|
||||||
|
|
||||||
|
return editProfile
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
[overrideHooks, hooks],
|
||||||
|
async () => {
|
||||||
|
await edit(props.instance.path, editProfileObject.value)
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
)
|
||||||
|
const messages = defineMessages({
|
||||||
|
hooks: {
|
||||||
|
id: 'instance.settings.tabs.hooks.title',
|
||||||
|
defaultMessage: 'Game launch hooks',
|
||||||
|
},
|
||||||
|
hooksDescription: {
|
||||||
|
id: 'instance.settings.tabs.hooks.description',
|
||||||
|
defaultMessage:
|
||||||
|
'Hooks allow advanced users to run certain system commands before and after launching the game.',
|
||||||
|
},
|
||||||
|
customHooks: {
|
||||||
|
id: 'instance.settings.tabs.hooks.custom-hooks',
|
||||||
|
defaultMessage: 'Custom launch hooks',
|
||||||
|
},
|
||||||
|
preLaunch: {
|
||||||
|
id: 'instance.settings.tabs.hooks.pre-launch',
|
||||||
|
defaultMessage: 'Pre-launch',
|
||||||
|
},
|
||||||
|
preLaunchDescription: {
|
||||||
|
id: 'instance.settings.tabs.hooks.pre-launch.description',
|
||||||
|
defaultMessage: 'Ran before the instance is launched.',
|
||||||
|
},
|
||||||
|
preLaunchEnter: {
|
||||||
|
id: 'instance.settings.tabs.hooks.pre-launch.enter',
|
||||||
|
defaultMessage: 'Enter pre-launch command...',
|
||||||
|
},
|
||||||
|
wrapper: {
|
||||||
|
id: 'instance.settings.tabs.hooks.wrapper',
|
||||||
|
defaultMessage: 'Wrapper',
|
||||||
|
},
|
||||||
|
wrapperDescription: {
|
||||||
|
id: 'instance.settings.tabs.hooks.wrapper.description',
|
||||||
|
defaultMessage: 'Wrapper command for launching Minecraft.',
|
||||||
|
},
|
||||||
|
wrapperEnter: {
|
||||||
|
id: 'instance.settings.tabs.hooks.wrapper.enter',
|
||||||
|
defaultMessage: 'Enter wrapper command...',
|
||||||
|
},
|
||||||
|
postExit: {
|
||||||
|
id: 'instance.settings.tabs.hooks.post-exit',
|
||||||
|
defaultMessage: 'Post-exit',
|
||||||
|
},
|
||||||
|
postExitDescription: {
|
||||||
|
id: 'instance.settings.tabs.hooks.post-exit.description',
|
||||||
|
defaultMessage: 'Ran after the game closes.',
|
||||||
|
},
|
||||||
|
postExitEnter: {
|
||||||
|
id: 'instance.settings.tabs.hooks.post-exit.enter',
|
||||||
|
defaultMessage: 'Enter post-exit command...',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h2 class="m-0 mb-1 text-lg font-extrabold text-contrast">
|
||||||
|
{{ formatMessage(messages.hooks) }}
|
||||||
|
</h2>
|
||||||
|
<p class="m-0">
|
||||||
|
{{ formatMessage(messages.hooksDescription) }}
|
||||||
|
</p>
|
||||||
|
<Checkbox v-model="overrideHooks" :label="formatMessage(messages.customHooks)" class="mt-2" />
|
||||||
|
|
||||||
|
<h2 class="mt-2 mb-1 text-lg font-extrabold text-contrast">
|
||||||
|
{{ formatMessage(messages.preLaunch) }}
|
||||||
|
</h2>
|
||||||
|
<p class="m-0">
|
||||||
|
{{ formatMessage(messages.preLaunchDescription) }}
|
||||||
|
</p>
|
||||||
|
<input
|
||||||
|
id="pre-launch"
|
||||||
|
v-model="hooks.pre_launch"
|
||||||
|
autocomplete="off"
|
||||||
|
:disabled="!overrideHooks"
|
||||||
|
type="text"
|
||||||
|
:placeholder="formatMessage(messages.preLaunchEnter)"
|
||||||
|
class="w-full mt-2"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2 class="mt-4 mb-1 text-lg font-extrabold text-contrast">
|
||||||
|
{{ formatMessage(messages.wrapper) }}
|
||||||
|
</h2>
|
||||||
|
<p class="m-0">
|
||||||
|
{{ formatMessage(messages.wrapperDescription) }}
|
||||||
|
</p>
|
||||||
|
<input
|
||||||
|
id="wrapper"
|
||||||
|
v-model="hooks.wrapper"
|
||||||
|
autocomplete="off"
|
||||||
|
:disabled="!overrideHooks"
|
||||||
|
type="text"
|
||||||
|
:placeholder="formatMessage(messages.wrapperEnter)"
|
||||||
|
class="w-full mt-2"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2 class="mt-4 mb-1 text-lg font-extrabold text-contrast">
|
||||||
|
{{ formatMessage(messages.postExit) }}
|
||||||
|
</h2>
|
||||||
|
<p class="m-0">
|
||||||
|
{{ formatMessage(messages.postExitDescription) }}
|
||||||
|
</p>
|
||||||
|
<input
|
||||||
|
id="post-exit"
|
||||||
|
v-model="hooks.post_exit"
|
||||||
|
autocomplete="off"
|
||||||
|
:disabled="!overrideHooks"
|
||||||
|
type="text"
|
||||||
|
:placeholder="formatMessage(messages.postExitEnter)"
|
||||||
|
class="w-full mt-2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,742 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
TransferIcon,
|
||||||
|
IssuesIcon,
|
||||||
|
HammerIcon,
|
||||||
|
DownloadIcon,
|
||||||
|
WrenchIcon,
|
||||||
|
UndoIcon,
|
||||||
|
SpinnerIcon,
|
||||||
|
UnplugIcon,
|
||||||
|
UnlinkIcon,
|
||||||
|
} from '@modrinth/assets'
|
||||||
|
import { Avatar, Checkbox, Chips, ButtonStyled, TeleportDropdownMenu } from '@modrinth/ui'
|
||||||
|
import { computed, type ComputedRef, type Ref, ref, shallowRef, watch } from 'vue'
|
||||||
|
import { edit, install, update_repair_modrinth } from '@/helpers/profile'
|
||||||
|
import { handleError } from '@/store/notifications'
|
||||||
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
|
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||||
|
import { get_loader_versions } from '@/helpers/metadata'
|
||||||
|
import { get_game_versions, get_loaders } from '@/helpers/tags'
|
||||||
|
import {
|
||||||
|
formatCategory,
|
||||||
|
type GameVersionTag,
|
||||||
|
type PlatformTag,
|
||||||
|
type Project,
|
||||||
|
type Version,
|
||||||
|
} from '@modrinth/utils'
|
||||||
|
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
||||||
|
import { get_project, get_version_many } from '@/helpers/cache'
|
||||||
|
import ModpackVersionModal from '@/components/ui/ModpackVersionModal.vue'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import type { GameInstance, ManifestLoaderVersion, Manifest } from '@/helpers/types.d.ts'
|
||||||
|
|
||||||
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
|
const repairConfirmModal = ref()
|
||||||
|
const modpackVersionModal = ref()
|
||||||
|
const modalConfirmUnpair = ref()
|
||||||
|
const modalConfirmReinstall = ref()
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
instance: GameInstance
|
||||||
|
offline?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const loader = ref(props.instance.loader)
|
||||||
|
const gameVersion = ref(props.instance.game_version)
|
||||||
|
|
||||||
|
const showSnapshots = ref(false)
|
||||||
|
|
||||||
|
const [
|
||||||
|
fabric_versions,
|
||||||
|
forge_versions,
|
||||||
|
quilt_versions,
|
||||||
|
neoforge_versions,
|
||||||
|
all_game_versions,
|
||||||
|
loaders,
|
||||||
|
] = await Promise.all([
|
||||||
|
get_loader_versions('fabric')
|
||||||
|
.then((manifest: Manifest) => shallowRef(manifest))
|
||||||
|
.catch(handleError),
|
||||||
|
get_loader_versions('forge')
|
||||||
|
.then((manifest: Manifest) => shallowRef(manifest))
|
||||||
|
.catch(handleError),
|
||||||
|
get_loader_versions('quilt')
|
||||||
|
.then((manifest: Manifest) => shallowRef(manifest))
|
||||||
|
.catch(handleError),
|
||||||
|
get_loader_versions('neo')
|
||||||
|
.then((manifest: Manifest) => shallowRef(manifest))
|
||||||
|
.catch(handleError),
|
||||||
|
get_game_versions()
|
||||||
|
.then((gameVersions: GameVersionTag[]) => shallowRef(gameVersions))
|
||||||
|
.catch(handleError),
|
||||||
|
get_loaders()
|
||||||
|
.then((value: PlatformTag[]) =>
|
||||||
|
value
|
||||||
|
.filter(
|
||||||
|
(item) => item.supported_project_types.includes('modpack') || item.name === 'vanilla',
|
||||||
|
)
|
||||||
|
.sort((a, b) => (a.name === 'vanilla' ? -1 : b.name === 'vanilla' ? 1 : 0)),
|
||||||
|
)
|
||||||
|
.then((loader: PlatformTag[]) => ref(loader))
|
||||||
|
.catch(handleError),
|
||||||
|
])
|
||||||
|
|
||||||
|
const modpackProject: Ref<Project | null> = ref(null)
|
||||||
|
const modpackVersion: Ref<Version | null> = ref(null)
|
||||||
|
const modpackVersions: Ref<Version[] | null> = ref(null)
|
||||||
|
const fetching = ref(true)
|
||||||
|
|
||||||
|
if (props.instance.linked_data && props.instance.linked_data.project_id && !props.offline) {
|
||||||
|
get_project(props.instance.linked_data.project_id, 'must_revalidate')
|
||||||
|
.then((project) => {
|
||||||
|
modpackProject.value = project
|
||||||
|
|
||||||
|
if (project && project.versions) {
|
||||||
|
get_version_many(project.versions, 'must_revalidate')
|
||||||
|
.then((versions: Version[]) => {
|
||||||
|
modpackVersions.value = versions.sort((a, b) =>
|
||||||
|
dayjs(b.date_published).diff(dayjs(a.date_published)),
|
||||||
|
)
|
||||||
|
modpackVersion.value =
|
||||||
|
versions.find(
|
||||||
|
(version: Version) => version.id === props.instance.linked_data?.version_id,
|
||||||
|
) ?? null
|
||||||
|
})
|
||||||
|
.catch(handleError)
|
||||||
|
.finally(() => {
|
||||||
|
fetching.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
handleError(err)
|
||||||
|
fetching.value = false
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
fetching.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentLoaderIcon = computed(
|
||||||
|
() => loaders?.value.find((x) => x.name === props.instance.loader)?.icon,
|
||||||
|
)
|
||||||
|
|
||||||
|
const gameVersionsForLoader = computed(() => {
|
||||||
|
return all_game_versions?.value.filter((item) => {
|
||||||
|
if (loader.value === 'fabric') {
|
||||||
|
return !!fabric_versions?.value.gameVersions.some((x) => item.version === x.id)
|
||||||
|
} else if (loader.value === 'forge') {
|
||||||
|
return !!forge_versions?.value.gameVersions.some((x) => item.version === x.id)
|
||||||
|
} else if (loader.value === 'quilt') {
|
||||||
|
return !!quilt_versions?.value.gameVersions.some((x) => item.version === x.id)
|
||||||
|
} else if (loader.value === 'neoforge') {
|
||||||
|
return !!neoforge_versions?.value.gameVersions.some((x) => item.version === x.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return []
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const hasSnapshots = computed(() =>
|
||||||
|
gameVersionsForLoader.value?.some((x) => x.version_type !== 'release'),
|
||||||
|
)
|
||||||
|
|
||||||
|
const selectableGameVersionNumbers = computed(() => {
|
||||||
|
return gameVersionsForLoader.value
|
||||||
|
?.filter((x) => x.version_type === 'release' || showSnapshots.value)
|
||||||
|
.map((x) => x.version)
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectableLoaderVersions: ComputedRef<ManifestLoaderVersion[] | undefined> = computed(() => {
|
||||||
|
if (gameVersion.value) {
|
||||||
|
if (loader.value === 'fabric') {
|
||||||
|
return fabric_versions?.value.gameVersions[0].loaders
|
||||||
|
} else if (loader.value === 'forge') {
|
||||||
|
return forge_versions?.value?.gameVersions?.find((item) => item.id === gameVersion.value)
|
||||||
|
?.loaders
|
||||||
|
} else if (loader.value === 'quilt') {
|
||||||
|
return quilt_versions?.value.gameVersions[0].loaders
|
||||||
|
} else if (loader.value === 'neoforge') {
|
||||||
|
return neoforge_versions?.value?.gameVersions?.find((item) => item.id === gameVersion.value)
|
||||||
|
?.loaders
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
})
|
||||||
|
const loaderVersionIndex: Ref<number> = ref(-1)
|
||||||
|
|
||||||
|
resetLoaderVersionIndex()
|
||||||
|
|
||||||
|
function resetLoaderVersionIndex() {
|
||||||
|
loaderVersionIndex.value =
|
||||||
|
selectableLoaderVersions.value?.findIndex((x) => x.id === props.instance.loader_version) ?? -1
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValid = computed(() => {
|
||||||
|
return (
|
||||||
|
selectableGameVersionNumbers.value?.includes(gameVersion.value) &&
|
||||||
|
((loaderVersionIndex.value !== undefined && loaderVersionIndex.value >= 0) ||
|
||||||
|
loader.value === 'vanilla')
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const isChanged = computed(() => {
|
||||||
|
return (
|
||||||
|
loader.value !== props.instance.loader ||
|
||||||
|
gameVersion.value !== props.instance.game_version ||
|
||||||
|
(loader.value !== 'vanilla' &&
|
||||||
|
loaderVersionIndex.value !== undefined &&
|
||||||
|
loaderVersionIndex.value >= 0 &&
|
||||||
|
selectableLoaderVersions.value?.[loaderVersionIndex.value].id !==
|
||||||
|
props.instance.loader_version)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(loader, () => {
|
||||||
|
loaderVersionIndex.value = 0
|
||||||
|
})
|
||||||
|
|
||||||
|
const editing = ref(false)
|
||||||
|
|
||||||
|
async function saveGvLoaderEdits() {
|
||||||
|
editing.value = true
|
||||||
|
|
||||||
|
const editProfile: { loader?: string; game_version?: string; loader_version?: string } = {}
|
||||||
|
editProfile.loader = loader.value
|
||||||
|
editProfile.game_version = gameVersion.value
|
||||||
|
|
||||||
|
if (loader.value !== 'vanilla' && loaderVersionIndex.value !== undefined) {
|
||||||
|
editProfile.loader_version = selectableLoaderVersions.value?.[loaderVersionIndex.value].id
|
||||||
|
} else {
|
||||||
|
loaderVersionIndex.value = -1
|
||||||
|
}
|
||||||
|
console.log('Editing:')
|
||||||
|
console.log(loader.value)
|
||||||
|
|
||||||
|
await edit(props.instance.path, editProfile).catch(handleError)
|
||||||
|
await repairProfile(false)
|
||||||
|
|
||||||
|
editing.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const installing = computed(() => props.instance.install_stage !== 'installed')
|
||||||
|
const repairing = ref(false)
|
||||||
|
const reinstalling = ref(false)
|
||||||
|
const changingVersion = ref(false)
|
||||||
|
|
||||||
|
async function repairProfile(force: boolean) {
|
||||||
|
if (force) {
|
||||||
|
repairing.value = true
|
||||||
|
}
|
||||||
|
await install(props.instance.path, force).catch(handleError)
|
||||||
|
if (force) {
|
||||||
|
repairing.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
trackEvent('InstanceRepair', {
|
||||||
|
loader: props.instance.loader,
|
||||||
|
game_version: props.instance.game_version,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function unpairProfile() {
|
||||||
|
await edit(props.instance.path, {
|
||||||
|
linked_data: null,
|
||||||
|
})
|
||||||
|
modpackProject.value = null
|
||||||
|
modpackVersion.value = null
|
||||||
|
modpackVersions.value = null
|
||||||
|
modalConfirmUnpair.value.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function repairModpack() {
|
||||||
|
reinstalling.value = true
|
||||||
|
await update_repair_modrinth(props.instance.path).catch(handleError)
|
||||||
|
reinstalling.value = false
|
||||||
|
|
||||||
|
trackEvent('InstanceRepair', {
|
||||||
|
loader: props.instance.loader,
|
||||||
|
game_version: props.instance.game_version,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
cannotWhileInstalling: {
|
||||||
|
id: 'instance.settings.tabs.installation.tooltip.cannot-while-installing',
|
||||||
|
defaultMessage: 'Cannot {action} while installing',
|
||||||
|
},
|
||||||
|
cannotWhileOffline: {
|
||||||
|
id: 'instance.settings.tabs.installation.tooltip.cannot-while-offline',
|
||||||
|
defaultMessage: 'Cannot {action} while offline',
|
||||||
|
},
|
||||||
|
cannotWhileRepairing: {
|
||||||
|
id: 'instance.settings.tabs.installation.tooltip.cannot-while-repairing',
|
||||||
|
defaultMessage: 'Cannot {action} while repairing',
|
||||||
|
},
|
||||||
|
currentlyInstalled: {
|
||||||
|
id: 'instance.settings.tabs.installation.currently-installed',
|
||||||
|
defaultMessage: 'Currently installed',
|
||||||
|
},
|
||||||
|
platform: {
|
||||||
|
id: 'instance.settings.tabs.installation.platform',
|
||||||
|
defaultMessage: 'Platform',
|
||||||
|
},
|
||||||
|
gameVersion: {
|
||||||
|
id: 'instance.settings.tabs.installation.game-version',
|
||||||
|
defaultMessage: 'Game version',
|
||||||
|
},
|
||||||
|
loaderVersion: {
|
||||||
|
id: 'instance.settings.tabs.installation.loader-version',
|
||||||
|
defaultMessage: '{loader} version',
|
||||||
|
},
|
||||||
|
showAllVersions: {
|
||||||
|
id: 'instance.settings.tabs.installation.show-all-versions',
|
||||||
|
defaultMessage: 'Show all versions',
|
||||||
|
},
|
||||||
|
install: {
|
||||||
|
id: 'instance.settings.tabs.installation.install',
|
||||||
|
defaultMessage: 'Install',
|
||||||
|
},
|
||||||
|
resetSelections: {
|
||||||
|
id: 'instance.settings.tabs.installation.reset-selections',
|
||||||
|
defaultMessage: 'Reset to current',
|
||||||
|
},
|
||||||
|
unknownVersion: {
|
||||||
|
id: 'instance.settings.tabs.installation.unknown-version',
|
||||||
|
defaultMessage: '(unknown version)',
|
||||||
|
},
|
||||||
|
repairConfirmTitle: {
|
||||||
|
id: 'instance.settings.tabs.installation.repair.confirm-title',
|
||||||
|
defaultMessage: 'Repair instance?',
|
||||||
|
},
|
||||||
|
repairConfirmDescription: {
|
||||||
|
id: 'instance.settings.tabs.installation.repair.description',
|
||||||
|
defaultMessage:
|
||||||
|
'Repairing reinstalls Minecraft dependencies and checks for corruption. This may resolve issues if your game is not launching due to launcher-related errors, but will not resolve issues or crashes related to installed mods.',
|
||||||
|
},
|
||||||
|
repairButton: {
|
||||||
|
id: 'instance.settings.tabs.installation.repair.button',
|
||||||
|
defaultMessage: 'Repair',
|
||||||
|
},
|
||||||
|
repairingButton: {
|
||||||
|
id: 'instance.settings.tabs.installation.repair.button.repairing',
|
||||||
|
defaultMessage: 'Repairing',
|
||||||
|
},
|
||||||
|
repairInProgress: {
|
||||||
|
id: 'instance.settings.tabs.installation.repair.in-progress',
|
||||||
|
defaultMessage: 'Repair in progress',
|
||||||
|
},
|
||||||
|
repairAction: {
|
||||||
|
id: 'instance.settings.tabs.installation.tooltip.action.repair',
|
||||||
|
defaultMessage: 'repair',
|
||||||
|
},
|
||||||
|
changeVersionCannotWhileFetching: {
|
||||||
|
id: 'instance.settings.tabs.installation.change-version.cannot-while-fetching',
|
||||||
|
defaultMessage: 'Fetching modpack versions',
|
||||||
|
},
|
||||||
|
changeVersionButton: {
|
||||||
|
id: 'instance.settings.tabs.installation.change-version.button',
|
||||||
|
defaultMessage: 'Change version',
|
||||||
|
},
|
||||||
|
changeVersionAction: {
|
||||||
|
id: 'instance.settings.tabs.installation.tooltip.action.change-version',
|
||||||
|
defaultMessage: 'change version',
|
||||||
|
},
|
||||||
|
installingButton: {
|
||||||
|
id: 'instance.settings.tabs.installation.change-version.button.installing',
|
||||||
|
defaultMessage: 'Installing',
|
||||||
|
},
|
||||||
|
installButton: {
|
||||||
|
id: 'instance.settings.tabs.installation.change-version.button.install',
|
||||||
|
defaultMessage: 'Install',
|
||||||
|
},
|
||||||
|
installingNewVersion: {
|
||||||
|
id: 'instance.settings.tabs.installation.change-version.in-progress',
|
||||||
|
defaultMessage: 'Installing new version',
|
||||||
|
},
|
||||||
|
minecraftVersion: {
|
||||||
|
id: 'instance.settings.tabs.installation.minecraft-version',
|
||||||
|
defaultMessage: 'Minecraft {version}',
|
||||||
|
},
|
||||||
|
noLoaderVersions: {
|
||||||
|
id: 'instance.settings.tabs.installation.no-loader-versions',
|
||||||
|
defaultMessage: '{loader} is not available for Minecraft {version}. Try another mod loader.',
|
||||||
|
},
|
||||||
|
noConnection: {
|
||||||
|
id: 'instance.settings.tabs.installation.no-connection',
|
||||||
|
defaultMessage: 'Cannot fetch linked modpack details. Please check your internet connection.',
|
||||||
|
},
|
||||||
|
noModpackFound: {
|
||||||
|
id: 'instance.settings.tabs.installation.no-modpack-found',
|
||||||
|
defaultMessage:
|
||||||
|
'This instance is linked to a modpack, but the modpack could not be found on Modrinth.',
|
||||||
|
},
|
||||||
|
debugInformation: {
|
||||||
|
id: 'instance.settings.tabs.installation.debug-information',
|
||||||
|
defaultMessage: 'Debug information:',
|
||||||
|
},
|
||||||
|
fetchingModpackDetails: {
|
||||||
|
id: 'instance.settings.tabs.installation.fetching-modpack-details',
|
||||||
|
defaultMessage: 'Fetching modpack details',
|
||||||
|
},
|
||||||
|
unlinkInstanceTitle: {
|
||||||
|
id: 'instance.settings.tabs.installation.unlink.title',
|
||||||
|
defaultMessage: 'Unlink from modpack',
|
||||||
|
},
|
||||||
|
unlinkInstanceDescription: {
|
||||||
|
id: 'instance.settings.tabs.installation.unlink.description',
|
||||||
|
defaultMessage: `This instance is linked to a modpack, which means mods can't be updated and you can't change the mod loader or Minecraft version. Unlinking will permanently disconnect this instance from the modpack.`,
|
||||||
|
},
|
||||||
|
unlinkInstanceButton: {
|
||||||
|
id: 'instance.settings.tabs.installation.unlink.button',
|
||||||
|
defaultMessage: 'Unlink instance',
|
||||||
|
},
|
||||||
|
unlinkInstanceConfirmTitle: {
|
||||||
|
id: 'instance.settings.tabs.installation.unlink.title',
|
||||||
|
defaultMessage: 'Are you sure you want to unlink this instance?',
|
||||||
|
},
|
||||||
|
unlinkInstanceConfirmDescription: {
|
||||||
|
id: 'instance.settings.tabs.installation.unlink.description',
|
||||||
|
defaultMessage:
|
||||||
|
'If you proceed, you will not be able to re-link it without creating an entirely new instance. You will no longer receive modpack updates and it will become a normal.',
|
||||||
|
},
|
||||||
|
reinstallModpackConfirmTitle: {
|
||||||
|
id: 'instance.settings.tabs.installation.reinstall.confirm.title',
|
||||||
|
defaultMessage: 'Are you sure you want to reinstall this instance?',
|
||||||
|
},
|
||||||
|
reinstallModpackConfirmDescription: {
|
||||||
|
id: 'instance.settings.tabs.installation.reinstall.confirm.description',
|
||||||
|
defaultMessage: `Reinstalling will reset content provided by the modpack to their original state.`,
|
||||||
|
},
|
||||||
|
reinstallModpackTitle: {
|
||||||
|
id: 'instance.settings.tabs.installation.reinstall.title',
|
||||||
|
defaultMessage: 'Reinstall modpack',
|
||||||
|
},
|
||||||
|
reinstallModpackDescription: {
|
||||||
|
id: 'instance.settings.tabs.installation.reinstall.description',
|
||||||
|
defaultMessage: `Resets all content provided by the modpack to their original state. This may fix unexpected behavior if changes have been made to the instance.`,
|
||||||
|
},
|
||||||
|
reinstallModpackButton: {
|
||||||
|
id: 'instance.settings.tabs.installation.reinstall.button',
|
||||||
|
defaultMessage: 'Reinstall modpack',
|
||||||
|
},
|
||||||
|
reinstallingModpackButton: {
|
||||||
|
id: 'instance.settings.tabs.installation.reinstall.button.reinstalling',
|
||||||
|
defaultMessage: 'Reinstalling modpack',
|
||||||
|
},
|
||||||
|
reinstallAction: {
|
||||||
|
id: 'instance.settings.tabs.installation.tooltip.action.reinstall',
|
||||||
|
defaultMessage: 'reinstall',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ConfirmModalWrapper
|
||||||
|
ref="repairConfirmModal"
|
||||||
|
:title="formatMessage(messages.repairConfirmTitle)"
|
||||||
|
:description="formatMessage(messages.repairConfirmDescription)"
|
||||||
|
:proceed-icon="HammerIcon"
|
||||||
|
:proceed-label="formatMessage(messages.repairButton)"
|
||||||
|
:danger="false"
|
||||||
|
@proceed="() => repairProfile(true)"
|
||||||
|
/>
|
||||||
|
<ModpackVersionModal
|
||||||
|
v-if="instance.linked_data && modpackVersions"
|
||||||
|
ref="modpackVersionModal"
|
||||||
|
:instance="instance"
|
||||||
|
:versions="modpackVersions"
|
||||||
|
@finish-install="
|
||||||
|
() => {
|
||||||
|
changingVersion = false
|
||||||
|
modpackVersion =
|
||||||
|
modpackVersions?.find(
|
||||||
|
(version: Version) => version.id === props.instance.linked_data?.version_id,
|
||||||
|
) ?? null
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<ConfirmModalWrapper
|
||||||
|
ref="modalConfirmUnpair"
|
||||||
|
:title="formatMessage(messages.unlinkInstanceConfirmTitle)"
|
||||||
|
:description="formatMessage(messages.unlinkInstanceConfirmDescription)"
|
||||||
|
:proceed-icon="UnlinkIcon"
|
||||||
|
:proceed-label="formatMessage(messages.unlinkInstanceButton)"
|
||||||
|
@proceed="() => unpairProfile()"
|
||||||
|
/>
|
||||||
|
<ConfirmModalWrapper
|
||||||
|
ref="modalConfirmReinstall"
|
||||||
|
:title="formatMessage(messages.reinstallModpackConfirmTitle)"
|
||||||
|
:description="formatMessage(messages.reinstallModpackConfirmDescription)"
|
||||||
|
:proceed-icon="DownloadIcon"
|
||||||
|
:proceed-label="formatMessage(messages.reinstallModpackButton)"
|
||||||
|
@proceed="() => repairModpack()"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<h2 id="project-name" class="m-0 mb-1 text-lg font-extrabold text-contrast block">
|
||||||
|
{{ formatMessage(messages.currentlyInstalled) }}
|
||||||
|
</h2>
|
||||||
|
<div
|
||||||
|
v-if="!modpackProject && instance.linked_data && offline && !fetching"
|
||||||
|
class="text-secondary font-medium mb-2"
|
||||||
|
>
|
||||||
|
<UnplugIcon class="top-[3px] relative" /> {{ formatMessage(messages.noConnection) }}
|
||||||
|
</div>
|
||||||
|
<div v-else-if="!modpackProject && instance.linked_data && !fetching" class="mb-2">
|
||||||
|
<p class="text-brand-red font-medium mt-0">
|
||||||
|
<IssuesIcon class="top-[3px] relative" /> {{ formatMessage(messages.noModpackFound) }}
|
||||||
|
</p>
|
||||||
|
<p>{{ formatMessage(messages.debugInformation) }}</p>
|
||||||
|
<div class="bg-bg p-6 rounded-2xl mt-2 text-sm text-secondary">
|
||||||
|
{{ instance.linked_data }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-4 items-center justify-between p-4 bg-bg rounded-2xl">
|
||||||
|
<div v-if="fetching" class="flex items-center gap-2 h-10">
|
||||||
|
<SpinnerIcon class="animate-spin" />
|
||||||
|
{{ formatMessage(messages.fetchingModpackDetails) }}
|
||||||
|
</div>
|
||||||
|
<template v-else>
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
|
<Avatar v-if="modpackProject" :src="modpackProject?.icon_url" size="40px" />
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="w-10 h-10 flex items-center justify-center rounded-full bg-button-bg border-solid border-[1px] border-button-border p-2 [&_svg]:h-full [&_svg]:w-full"
|
||||||
|
>
|
||||||
|
<div v-if="!!currentLoaderIcon" class="contents" v-html="currentLoaderIcon" />
|
||||||
|
<WrenchIcon v-else />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2 justify-center">
|
||||||
|
<span class="font-semibold leading-none">
|
||||||
|
{{
|
||||||
|
modpackProject
|
||||||
|
? modpackProject.title
|
||||||
|
: formatMessage(messages.minecraftVersion, { version: instance.game_version })
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
<span class="text-sm text-secondary leading-none">
|
||||||
|
{{
|
||||||
|
modpackProject
|
||||||
|
? modpackVersion
|
||||||
|
? modpackVersion?.version_number
|
||||||
|
: 'Unknown version'
|
||||||
|
: formatCategory(instance.loader)
|
||||||
|
}}
|
||||||
|
<template v-if="instance.loader !== 'vanilla' && !modpackProject">
|
||||||
|
{{ instance.loader_version || formatMessage(messages.unknownVersion) }}
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-1">
|
||||||
|
<ButtonStyled color="orange" type="transparent" hover-color-fill="background">
|
||||||
|
<button
|
||||||
|
v-tooltip="
|
||||||
|
repairing
|
||||||
|
? formatMessage(messages.repairInProgress)
|
||||||
|
: installing || reinstalling
|
||||||
|
? formatMessage(messages.cannotWhileInstalling, {
|
||||||
|
action: formatMessage(messages.repairAction),
|
||||||
|
})
|
||||||
|
: offline
|
||||||
|
? formatMessage(messages.cannotWhileOffline, {
|
||||||
|
action: formatMessage(messages.repairAction),
|
||||||
|
})
|
||||||
|
: null
|
||||||
|
"
|
||||||
|
:disabled="installing || repairing || reinstalling || offline"
|
||||||
|
@click="repairConfirmModal.show()"
|
||||||
|
>
|
||||||
|
<SpinnerIcon v-if="repairing" class="animate-spin" />
|
||||||
|
<HammerIcon v-else />
|
||||||
|
{{
|
||||||
|
repairing
|
||||||
|
? formatMessage(messages.repairingButton)
|
||||||
|
: formatMessage(messages.repairButton)
|
||||||
|
}}
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
<ButtonStyled v-if="modpackProject" hover-color-fill="background">
|
||||||
|
<button
|
||||||
|
v-tooltip="
|
||||||
|
changingVersion
|
||||||
|
? formatMessage(messages.installingNewVersion)
|
||||||
|
: repairing
|
||||||
|
? formatMessage(messages.cannotWhileRepairing, {
|
||||||
|
action: formatMessage(messages.changeVersionAction),
|
||||||
|
})
|
||||||
|
: installing || reinstalling
|
||||||
|
? formatMessage(messages.cannotWhileInstalling, {
|
||||||
|
action: formatMessage(messages.changeVersionAction),
|
||||||
|
})
|
||||||
|
: fetching && !modpackVersions
|
||||||
|
? formatMessage(messages.changeVersionCannotWhileFetching)
|
||||||
|
: offline
|
||||||
|
? formatMessage(messages.cannotWhileOffline, {
|
||||||
|
action: formatMessage(messages.changeVersionAction),
|
||||||
|
})
|
||||||
|
: null
|
||||||
|
"
|
||||||
|
:disabled="
|
||||||
|
changingVersion ||
|
||||||
|
repairing ||
|
||||||
|
installing ||
|
||||||
|
reinstalling ||
|
||||||
|
offline ||
|
||||||
|
fetching ||
|
||||||
|
!modpackVersions
|
||||||
|
"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
changingVersion = true
|
||||||
|
modpackVersionModal.show()
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<SpinnerIcon v-if="changingVersion" class="animate-spin" />
|
||||||
|
<TransferIcon v-else />
|
||||||
|
{{
|
||||||
|
changingVersion
|
||||||
|
? formatMessage(messages.installingButton)
|
||||||
|
: formatMessage(messages.changeVersionButton)
|
||||||
|
}}
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<template v-if="!instance.linked_data || !instance.linked_data.locked">
|
||||||
|
<h2 class="m-0 mt-4 text-lg font-extrabold text-contrast block">
|
||||||
|
{{ formatMessage(messages.platform) }}
|
||||||
|
</h2>
|
||||||
|
<Chips v-if="loaders" v-model="loader" :items="loaders.map((x) => x.name)" class="mt-2" />
|
||||||
|
<h2 class="m-0 mt-4 text-lg font-extrabold text-contrast block">
|
||||||
|
{{ formatMessage(messages.gameVersion) }}
|
||||||
|
</h2>
|
||||||
|
<div class="flex flex-wrap mt-2 gap-2">
|
||||||
|
<TeleportDropdownMenu
|
||||||
|
v-if="selectableGameVersionNumbers !== undefined"
|
||||||
|
v-model="gameVersion"
|
||||||
|
:options="selectableGameVersionNumbers"
|
||||||
|
name="Game Version Dropdown"
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
v-if="hasSnapshots"
|
||||||
|
v-model="showSnapshots"
|
||||||
|
:label="formatMessage(messages.showAllVersions)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<template v-if="loader !== 'vanilla'">
|
||||||
|
<h2 class="m-0 mt-4 text-lg font-extrabold text-contrast block">
|
||||||
|
{{ formatMessage(messages.loaderVersion, { loader: formatCategory(loader) }) }}
|
||||||
|
</h2>
|
||||||
|
<TeleportDropdownMenu
|
||||||
|
v-if="selectableLoaderVersions"
|
||||||
|
:model-value="selectableLoaderVersions[loaderVersionIndex]"
|
||||||
|
:options="selectableLoaderVersions"
|
||||||
|
:display-name="(option: ManifestLoaderVersion) => option?.id"
|
||||||
|
name="Version selector"
|
||||||
|
class="mt-2"
|
||||||
|
@change="(value) => (loaderVersionIndex = value.index)"
|
||||||
|
/>
|
||||||
|
<div v-else class="mt-2 text-brand-red flex gap-2 items-center">
|
||||||
|
<IssuesIcon />
|
||||||
|
{{ formatMessage(messages.noLoaderVersions, { loader: loader, version: gameVersion }) }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="mt-4 flex flex-wrap gap-2">
|
||||||
|
<ButtonStyled color="brand">
|
||||||
|
<button :disabled="!isValid || !isChanged || editing" @click="saveGvLoaderEdits()">
|
||||||
|
<SpinnerIcon v-if="editing" class="animate-spin" />
|
||||||
|
<DownloadIcon v-else />
|
||||||
|
{{
|
||||||
|
editing
|
||||||
|
? formatMessage(messages.installingButton)
|
||||||
|
: formatMessage(messages.installButton)
|
||||||
|
}}
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
<ButtonStyled>
|
||||||
|
<button
|
||||||
|
:disabled="!isChanged"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
loader = instance.loader
|
||||||
|
gameVersion = instance.game_version
|
||||||
|
resetLoaderVersionIndex()
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<UndoIcon />
|
||||||
|
{{ formatMessage(messages.resetSelections) }}
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<template v-if="instance.linked_data && instance.linked_data.locked">
|
||||||
|
<h2 class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
|
||||||
|
{{ formatMessage(messages.unlinkInstanceTitle) }}
|
||||||
|
</h2>
|
||||||
|
<p class="m-0">
|
||||||
|
{{ formatMessage(messages.unlinkInstanceDescription) }}
|
||||||
|
</p>
|
||||||
|
<ButtonStyled>
|
||||||
|
<button class="mt-2" @click="modalConfirmUnpair.show()">
|
||||||
|
<UnlinkIcon /> {{ formatMessage(messages.unlinkInstanceButton) }}
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
<template v-if="modpackProject">
|
||||||
|
<div>
|
||||||
|
<h2 class="m-0 mb-1 text-lg font-extrabold text-contrast block mt-4">
|
||||||
|
{{ formatMessage(messages.reinstallModpackTitle) }}
|
||||||
|
</h2>
|
||||||
|
<p class="m-0">
|
||||||
|
{{ formatMessage(messages.reinstallModpackDescription) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<ButtonStyled color="red" type="outlined">
|
||||||
|
<button
|
||||||
|
v-tooltip="
|
||||||
|
reinstalling
|
||||||
|
? formatMessage(messages.reinstallingModpackButton)
|
||||||
|
: repairing
|
||||||
|
? formatMessage(messages.cannotWhileRepairing, {
|
||||||
|
action: formatMessage(messages.reinstallAction),
|
||||||
|
})
|
||||||
|
: installing
|
||||||
|
? formatMessage(messages.cannotWhileInstalling, {
|
||||||
|
action: formatMessage(messages.reinstallAction),
|
||||||
|
})
|
||||||
|
: offline
|
||||||
|
? formatMessage(messages.cannotWhileOffline, {
|
||||||
|
action: formatMessage(messages.reinstallAction),
|
||||||
|
})
|
||||||
|
: null
|
||||||
|
"
|
||||||
|
class="mt-2"
|
||||||
|
:disabled="
|
||||||
|
changingVersion ||
|
||||||
|
repairing ||
|
||||||
|
installing ||
|
||||||
|
offline ||
|
||||||
|
fetching ||
|
||||||
|
!modpackVersions
|
||||||
|
"
|
||||||
|
@click="modalConfirmReinstall.show()"
|
||||||
|
>
|
||||||
|
<SpinnerIcon v-if="reinstalling" class="animate-spin" />
|
||||||
|
<DownloadIcon v-else />
|
||||||
|
{{
|
||||||
|
reinstalling
|
||||||
|
? formatMessage(messages.reinstallingModpackButton)
|
||||||
|
: formatMessage(messages.reinstallModpackButton)
|
||||||
|
}}
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Checkbox, Slider } from '@modrinth/ui'
|
||||||
|
import { CheckCircleIcon, XCircleIcon } from '@modrinth/assets'
|
||||||
|
import { computed, readonly, ref, watch } from 'vue'
|
||||||
|
import { edit, get_optimal_jre_key } from '@/helpers/profile'
|
||||||
|
import { handleError } from '@/store/notifications'
|
||||||
|
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||||
|
import JavaSelector from '@/components/ui/JavaSelector.vue'
|
||||||
|
import { get_max_memory } from '@/helpers/jre'
|
||||||
|
import { get } from '@/helpers/settings'
|
||||||
|
|
||||||
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
instance: GameInstance
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const globalSettings = (await get().catch(handleError)) as AppSettings
|
||||||
|
|
||||||
|
const overrideJavaInstall = ref(!!props.instance.java_path)
|
||||||
|
const optimalJava = readonly(await get_optimal_jre_key(props.instance.path).catch(handleError))
|
||||||
|
const javaInstall = ref({ path: optimalJava.path ?? props.instance.java_path })
|
||||||
|
|
||||||
|
const overrideJavaArgs = ref(props.instance.extra_launch_args?.length !== undefined)
|
||||||
|
const javaArgs = ref(
|
||||||
|
(props.instance.extra_launch_args ?? globalSettings.extra_launch_args).join(' '),
|
||||||
|
)
|
||||||
|
|
||||||
|
const overrideEnvVars = ref(props.instance.custom_env_vars?.length !== undefined)
|
||||||
|
const envVars = ref(
|
||||||
|
(props.instance.custom_env_vars ?? globalSettings.custom_env_vars)
|
||||||
|
.map((x) => x.join('='))
|
||||||
|
.join(' '),
|
||||||
|
)
|
||||||
|
|
||||||
|
const overrideMemorySettings = ref(!!props.instance.memory)
|
||||||
|
const memory = ref(props.instance.memory ?? globalSettings.memory)
|
||||||
|
const maxMemory = Math.floor((await get_max_memory().catch(handleError)) / 1024)
|
||||||
|
|
||||||
|
const editProfileObject = computed(() => {
|
||||||
|
const editProfile: {
|
||||||
|
java_path?: string
|
||||||
|
extra_launch_args?: string[]
|
||||||
|
custom_env_vars?: string[][]
|
||||||
|
memory?: MemorySettings
|
||||||
|
} = {}
|
||||||
|
|
||||||
|
if (overrideJavaInstall.value) {
|
||||||
|
if (javaInstall.value.path !== '') {
|
||||||
|
editProfile.java_path = javaInstall.value.path.replace('java.exe', 'javaw.exe')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (overrideJavaArgs.value) {
|
||||||
|
editProfile.extra_launch_args = javaArgs.value.trim().split(/\s+/).filter(Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (overrideEnvVars.value) {
|
||||||
|
editProfile.custom_env_vars = envVars.value
|
||||||
|
.trim()
|
||||||
|
.split(/\s+/)
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((x) => x.split('=').filter(Boolean))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (overrideMemorySettings.value) {
|
||||||
|
editProfile.memory = memory.value
|
||||||
|
}
|
||||||
|
|
||||||
|
return editProfile
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
[
|
||||||
|
overrideJavaInstall,
|
||||||
|
javaInstall,
|
||||||
|
overrideJavaArgs,
|
||||||
|
javaArgs,
|
||||||
|
overrideEnvVars,
|
||||||
|
envVars,
|
||||||
|
overrideMemorySettings,
|
||||||
|
memory,
|
||||||
|
],
|
||||||
|
async () => {
|
||||||
|
await edit(props.instance.path, editProfileObject.value)
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
javaInstallation: {
|
||||||
|
id: 'instance.settings.tabs.java.java-installation',
|
||||||
|
defaultMessage: 'Java installation',
|
||||||
|
},
|
||||||
|
javaArguments: {
|
||||||
|
id: 'instance.settings.tabs.java.java-arguments',
|
||||||
|
defaultMessage: 'Java arguments',
|
||||||
|
},
|
||||||
|
javaEnvironmentVariables: {
|
||||||
|
id: 'instance.settings.tabs.java.environment-variables',
|
||||||
|
defaultMessage: 'Environment variables',
|
||||||
|
},
|
||||||
|
javaMemory: {
|
||||||
|
id: 'instance.settings.tabs.java.java-memory',
|
||||||
|
defaultMessage: 'Memory allocated',
|
||||||
|
},
|
||||||
|
hooks: {
|
||||||
|
id: 'instance.settings.tabs.java.hooks',
|
||||||
|
defaultMessage: 'Hooks',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h2 id="project-name" class="m-0 mb-1 text-lg font-extrabold text-contrast block">
|
||||||
|
{{ formatMessage(messages.javaInstallation) }}
|
||||||
|
</h2>
|
||||||
|
<Checkbox v-model="overrideJavaInstall" label="Custom Java installation" class="mb-2" />
|
||||||
|
<template v-if="!overrideJavaInstall">
|
||||||
|
<div class="flex my-2 items-center gap-2 font-semibold">
|
||||||
|
<template v-if="javaInstall">
|
||||||
|
<CheckCircleIcon class="text-brand-green h-4 w-4" />
|
||||||
|
<span>Using default Java {{ optimalJava.major_version }} installation:</span>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="optimalJava">
|
||||||
|
<XCircleIcon class="text-brand-red h-5 w-5" />
|
||||||
|
<span
|
||||||
|
>Could not find a default Java {{ optimalJava.major_version }} installation. Please set
|
||||||
|
one below:</span
|
||||||
|
>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<XCircleIcon class="text-brand-red h-5 w-5" />
|
||||||
|
<span
|
||||||
|
>Could not automatically determine a Java installation to use. Please set one
|
||||||
|
below:</span
|
||||||
|
>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="javaInstall && !overrideJavaInstall"
|
||||||
|
class="p-4 bg-bg rounded-xl text-xs text-secondary leading-none font-mono"
|
||||||
|
>
|
||||||
|
{{ javaInstall.path }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<JavaSelector v-if="overrideJavaInstall || !javaInstall" v-model="javaInstall" />
|
||||||
|
<h2 id="project-name" class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
|
||||||
|
{{ formatMessage(messages.javaMemory) }}
|
||||||
|
</h2>
|
||||||
|
<Checkbox v-model="overrideMemorySettings" label="Custom memory allocation" class="mb-2" />
|
||||||
|
<Slider
|
||||||
|
id="max-memory"
|
||||||
|
v-model="memory.maximum"
|
||||||
|
:disabled="!overrideMemorySettings"
|
||||||
|
:min="512"
|
||||||
|
:max="maxMemory"
|
||||||
|
:step="64"
|
||||||
|
unit="MB"
|
||||||
|
/>
|
||||||
|
<h2 id="project-name" class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
|
||||||
|
{{ formatMessage(messages.javaArguments) }}
|
||||||
|
</h2>
|
||||||
|
<Checkbox v-model="overrideJavaArgs" label="Custom java arguments" class="my-2" />
|
||||||
|
<input
|
||||||
|
id="java-args"
|
||||||
|
v-model="javaArgs"
|
||||||
|
autocomplete="off"
|
||||||
|
:disabled="!overrideJavaArgs"
|
||||||
|
type="text"
|
||||||
|
class="w-full"
|
||||||
|
placeholder="Enter java arguments..."
|
||||||
|
/>
|
||||||
|
<h2 id="project-name" class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
|
||||||
|
{{ formatMessage(messages.javaEnvironmentVariables) }}
|
||||||
|
</h2>
|
||||||
|
<Checkbox v-model="overrideEnvVars" label="Custom environment variables" class="mb-2" />
|
||||||
|
<input
|
||||||
|
id="env-vars"
|
||||||
|
v-model="envVars"
|
||||||
|
autocomplete="off"
|
||||||
|
:disabled="!overrideEnvVars"
|
||||||
|
type="text"
|
||||||
|
class="w-full"
|
||||||
|
placeholder="Enter environmental variables..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Checkbox, Toggle } from '@modrinth/ui'
|
||||||
|
import { computed, ref, type Ref, watch } from 'vue'
|
||||||
|
import { handleError } from '@/store/notifications'
|
||||||
|
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||||
|
import { get } from '@/helpers/settings'
|
||||||
|
import { edit } from '@/helpers/profile'
|
||||||
|
|
||||||
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
instance: GameInstance
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const globalSettings = (await get().catch(handleError)) as AppSettings
|
||||||
|
|
||||||
|
const overrideWindowSettings = ref(
|
||||||
|
!!props.instance.game_resolution || !!props.instance.force_fullscreen,
|
||||||
|
)
|
||||||
|
const resolution: Ref<[number, number]> = ref(
|
||||||
|
props.instance.game_resolution ?? (globalSettings.game_resolution.slice() as [number, number]),
|
||||||
|
)
|
||||||
|
const fullscreenSetting: Ref<boolean> = ref(
|
||||||
|
props.instance.force_fullscreen ?? globalSettings.force_fullscreen,
|
||||||
|
)
|
||||||
|
|
||||||
|
const editProfileObject = computed(() => {
|
||||||
|
const editProfile: {
|
||||||
|
force_fullscreen?: boolean
|
||||||
|
game_resolution?: [number, number]
|
||||||
|
} = {}
|
||||||
|
|
||||||
|
if (overrideWindowSettings.value) {
|
||||||
|
editProfile.force_fullscreen = fullscreenSetting.value
|
||||||
|
|
||||||
|
if (!fullscreenSetting.value) {
|
||||||
|
editProfile.game_resolution = resolution.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return editProfile
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
[overrideWindowSettings, resolution, fullscreenSetting],
|
||||||
|
async () => {
|
||||||
|
await edit(props.instance.path, editProfileObject.value)
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
customWindowSettings: {
|
||||||
|
id: 'instance.settings.tabs.window.custom-window-settings',
|
||||||
|
defaultMessage: 'Custom window settings',
|
||||||
|
},
|
||||||
|
fullscreen: {
|
||||||
|
id: 'instance.settings.tabs.window.fullscreen',
|
||||||
|
defaultMessage: 'Fullscreen',
|
||||||
|
},
|
||||||
|
fullscreenDescription: {
|
||||||
|
id: 'instance.settings.tabs.window.fullscreen.description',
|
||||||
|
defaultMessage: 'Make the game start in full screen when launched (using options.txt).',
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
id: 'instance.settings.tabs.window.width',
|
||||||
|
defaultMessage: 'Width',
|
||||||
|
},
|
||||||
|
widthDescription: {
|
||||||
|
id: 'instance.settings.tabs.window.width.description',
|
||||||
|
defaultMessage: 'The width of the game window when launched.',
|
||||||
|
},
|
||||||
|
enterWidth: {
|
||||||
|
id: 'instance.settings.tabs.window.width.enter',
|
||||||
|
defaultMessage: 'Enter width...',
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
id: 'instance.settings.tabs.window.height',
|
||||||
|
defaultMessage: 'Height',
|
||||||
|
},
|
||||||
|
heightDescription: {
|
||||||
|
id: 'instance.settings.tabs.window.height.description',
|
||||||
|
defaultMessage: 'The height of the game window when launched.',
|
||||||
|
},
|
||||||
|
enterHeight: {
|
||||||
|
id: 'instance.settings.tabs.window.height.enter',
|
||||||
|
defaultMessage: 'Enter height...',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<Checkbox
|
||||||
|
v-model="overrideWindowSettings"
|
||||||
|
:label="formatMessage(messages.customWindowSettings)"
|
||||||
|
@update:model-value="
|
||||||
|
(value) => {
|
||||||
|
if (!value) {
|
||||||
|
resolution = globalSettings.game_resolution
|
||||||
|
fullscreenSetting = globalSettings.force_fullscreen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<div class="mt-2 flex items-center gap-4 justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 class="m-0 mb-1 text-lg font-extrabold text-contrast">
|
||||||
|
{{ formatMessage(messages.fullscreen) }}
|
||||||
|
</h2>
|
||||||
|
<p class="m-0">
|
||||||
|
{{ formatMessage(messages.fullscreenDescription) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Toggle
|
||||||
|
id="fullscreen"
|
||||||
|
:model-value="overrideWindowSettings ? fullscreenSetting : globalSettings.force_fullscreen"
|
||||||
|
:checked="fullscreenSetting"
|
||||||
|
:disabled="!overrideWindowSettings"
|
||||||
|
@update:model-value="
|
||||||
|
(e) => {
|
||||||
|
fullscreenSetting = e
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 flex items-center gap-4 justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 class="m-0 mb-1 text-lg font-extrabold text-contrast">
|
||||||
|
{{ formatMessage(messages.width) }}
|
||||||
|
</h2>
|
||||||
|
<p class="m-0">
|
||||||
|
{{ formatMessage(messages.widthDescription) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
id="width"
|
||||||
|
v-model="resolution[0]"
|
||||||
|
autocomplete="off"
|
||||||
|
:disabled="!overrideWindowSettings || fullscreenSetting"
|
||||||
|
type="number"
|
||||||
|
:placeholder="formatMessage(messages.enterWidth)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 flex items-center gap-4 justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 class="m-0 mb-1 text-lg font-extrabold text-contrast">
|
||||||
|
{{ formatMessage(messages.height) }}
|
||||||
|
</h2>
|
||||||
|
<p class="m-0">
|
||||||
|
{{ formatMessage(messages.heightDescription) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
id="height"
|
||||||
|
v-model="resolution[1]"
|
||||||
|
autocomplete="off"
|
||||||
|
:disabled="!overrideWindowSettings || fullscreenSetting"
|
||||||
|
type="number"
|
||||||
|
:placeholder="formatMessage(messages.enterHeight)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
GameIcon,
|
GameIcon,
|
||||||
CoffeeIcon,
|
CoffeeIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
|
import { TabbedModal } from '@modrinth/ui'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useVIntl, defineMessage } from '@vintl/vintl'
|
import { useVIntl, defineMessage } from '@vintl/vintl'
|
||||||
import AppearanceSettings from '@/components/ui/settings/AppearanceSettings.vue'
|
import AppearanceSettings from '@/components/ui/settings/AppearanceSettings.vue'
|
||||||
@@ -24,15 +25,8 @@ import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
|||||||
|
|
||||||
const themeStore = useTheming()
|
const themeStore = useTheming()
|
||||||
|
|
||||||
const modal = ref()
|
|
||||||
|
|
||||||
function show() {
|
|
||||||
modal.value.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
const selectedTab = ref(0)
|
|
||||||
const devModeCounter = ref(0)
|
const devModeCounter = ref(0)
|
||||||
|
|
||||||
const developerModeEnabled = defineMessage({
|
const developerModeEnabled = defineMessage({
|
||||||
@@ -59,8 +53,8 @@ const tabs = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: defineMessage({
|
name: defineMessage({
|
||||||
id: 'app.settings.tabs.java-versions',
|
id: 'app.settings.tabs.java-installations',
|
||||||
defaultMessage: 'Java versions',
|
defaultMessage: 'Java installations',
|
||||||
}),
|
}),
|
||||||
icon: CoffeeIcon,
|
icon: CoffeeIcon,
|
||||||
content: JavaSettings,
|
content: JavaSettings,
|
||||||
@@ -92,13 +86,18 @@ const tabs = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const modal = ref()
|
||||||
|
|
||||||
|
function show() {
|
||||||
|
modal.value.show()
|
||||||
|
}
|
||||||
|
|
||||||
defineExpose({ show })
|
defineExpose({ show })
|
||||||
|
|
||||||
const version = await getVersion()
|
const version = await getVersion()
|
||||||
const osPlatform = getOsPlatform()
|
const osPlatform = getOsPlatform()
|
||||||
const osVersion = getOsVersion()
|
const osVersion = getOsVersion()
|
||||||
</script>
|
</script>
|
||||||
/
|
|
||||||
<template>
|
<template>
|
||||||
<ModalWrapper ref="modal">
|
<ModalWrapper ref="modal">
|
||||||
<template #title>
|
<template #title>
|
||||||
@@ -106,18 +105,9 @@ const osVersion = getOsVersion()
|
|||||||
<SettingsIcon /> Settings
|
<SettingsIcon /> Settings
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<div class="grid grid-cols-[auto_1fr] gap-4">
|
|
||||||
<div class="flex flex-col gap-1 border-solid pr-4 border-0 border-r-[1px] border-divider">
|
|
||||||
<button
|
|
||||||
v-for="(tab, index) in tabs.filter((t) => !t.developerOnly || themeStore.devMode)"
|
|
||||||
:key="index"
|
|
||||||
:class="`flex gap-2 items-center text-left rounded-xl px-4 py-2 border-none text-nowrap font-semibold cursor-pointer active:scale-[0.97] transition-transform ${selectedTab === index ? 'bg-highlight text-brand' : 'bg-transparent text-button-text'}`"
|
|
||||||
@click="() => (selectedTab = index)"
|
|
||||||
>
|
|
||||||
<component :is="tab.icon" class="w-4 h-4" />
|
|
||||||
<span>{{ formatMessage(tab.name) }}</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
|
<TabbedModal :tabs="tabs.filter((t) => !t.developerOnly || themeStore.devMode)">
|
||||||
|
<template #footer>
|
||||||
<div class="mt-auto text-secondary text-sm">
|
<div class="mt-auto text-secondary text-sm">
|
||||||
<p v-if="themeStore.devMode" class="text-brand font-semibold m-0 mb-2">
|
<p v-if="themeStore.devMode" class="text-brand font-semibold m-0 mb-2">
|
||||||
{{ formatMessage(developerModeEnabled) }}
|
{{ formatMessage(developerModeEnabled) }}
|
||||||
@@ -133,8 +123,8 @@ const osVersion = getOsVersion()
|
|||||||
themeStore.devMode = !themeStore.devMode
|
themeStore.devMode = !themeStore.devMode
|
||||||
devModeCounter = 0
|
devModeCounter = 0
|
||||||
|
|
||||||
if (!themeStore.devMode && tabs[selectedTab].developerOnly === true) {
|
if (!themeStore.devMode && tabs[modal.selectedTab].developerOnly) {
|
||||||
selectedTab = 0
|
modal.setTab(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -152,10 +142,7 @@ const osVersion = getOsVersion()
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
<div class="w-[600px] h-[500px] overflow-y-auto">
|
</TabbedModal>
|
||||||
<component :is="tabs[selectedTab].content" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -25,10 +25,18 @@ defineProps({
|
|||||||
default: 'No description defined',
|
default: 'No description defined',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
proceedIcon: {
|
||||||
|
type: Object,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
proceedLabel: {
|
proceedLabel: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'Proceed',
|
default: 'Proceed',
|
||||||
},
|
},
|
||||||
|
danger: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['proceed'])
|
const emit = defineEmits(['proceed'])
|
||||||
@@ -61,9 +69,11 @@ function proceed() {
|
|||||||
:has-to-type="hasToType"
|
:has-to-type="hasToType"
|
||||||
:title="title"
|
:title="title"
|
||||||
:description="description"
|
:description="description"
|
||||||
|
:proceed-icon="proceedIcon"
|
||||||
:proceed-label="proceedLabel"
|
:proceed-label="proceedLabel"
|
||||||
:on-hide="onModalHide"
|
:on-hide="onModalHide"
|
||||||
:noblur="!themeStore.advancedRendering"
|
:noblur="!themeStore.advancedRendering"
|
||||||
|
:danger="danger"
|
||||||
@proceed="proceed"
|
@proceed="proceed"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
ChevronRightIcon,
|
||||||
|
CoffeeIcon,
|
||||||
|
InfoIcon,
|
||||||
|
WrenchIcon,
|
||||||
|
MonitorIcon,
|
||||||
|
CodeIcon,
|
||||||
|
} from '@modrinth/assets'
|
||||||
|
import { Avatar, TabbedModal } from '@modrinth/ui'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { defineMessage, useVIntl } from '@vintl/vintl'
|
||||||
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
import GeneralSettings from '@/components/ui/instance_settings/GeneralSettings.vue'
|
||||||
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||||
|
import InstallationSettings from '@/components/ui/instance_settings/InstallationSettings.vue'
|
||||||
|
import JavaSettings from '@/components/ui/instance_settings/JavaSettings.vue'
|
||||||
|
import WindowSettings from '@/components/ui/instance_settings/WindowSettings.vue'
|
||||||
|
import HooksSettings from '@/components/ui/instance_settings/HooksSettings.vue'
|
||||||
|
|
||||||
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
instance: {
|
||||||
|
type: Object,
|
||||||
|
default() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{
|
||||||
|
name: defineMessage({
|
||||||
|
id: 'instance.settings.tabs.general',
|
||||||
|
defaultMessage: 'General',
|
||||||
|
}),
|
||||||
|
icon: InfoIcon,
|
||||||
|
content: GeneralSettings,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: defineMessage({
|
||||||
|
id: 'instance.settings.tabs.installation',
|
||||||
|
defaultMessage: 'Installation',
|
||||||
|
}),
|
||||||
|
icon: WrenchIcon,
|
||||||
|
content: InstallationSettings,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: defineMessage({
|
||||||
|
id: 'instance.settings.tabs.window',
|
||||||
|
defaultMessage: 'Window',
|
||||||
|
}),
|
||||||
|
icon: MonitorIcon,
|
||||||
|
content: WindowSettings,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: defineMessage({
|
||||||
|
id: 'instance.settings.tabs.java',
|
||||||
|
defaultMessage: 'Java and memory',
|
||||||
|
}),
|
||||||
|
icon: CoffeeIcon,
|
||||||
|
content: JavaSettings,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: defineMessage({
|
||||||
|
id: 'instance.settings.tabs.hooks',
|
||||||
|
defaultMessage: 'Launch hooks',
|
||||||
|
}),
|
||||||
|
icon: CodeIcon,
|
||||||
|
content: HooksSettings,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const modal = ref()
|
||||||
|
|
||||||
|
function show() {
|
||||||
|
modal.value.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ show })
|
||||||
|
|
||||||
|
const titleMessage = defineMessage({
|
||||||
|
id: 'instance.settings.title',
|
||||||
|
defaultMessage: 'Settings',
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<ModalWrapper ref="modal">
|
||||||
|
<template #title>
|
||||||
|
<span class="flex items-center gap-2 text-lg font-semibold text-primary">
|
||||||
|
<Avatar
|
||||||
|
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : undefined"
|
||||||
|
size="24px"
|
||||||
|
/>
|
||||||
|
{{ instance.name }} <ChevronRightIcon />
|
||||||
|
<span class="font-extrabold text-contrast">{{ formatMessage(titleMessage) }}</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<TabbedModal :tabs="tabs.map((tab) => ({ ...tab, props: { instance } }))" />
|
||||||
|
</ModalWrapper>
|
||||||
|
</template>
|
||||||
@@ -49,6 +49,9 @@ function onModalHide() {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Modal ref="modal" :header="header" :noblur="!themeStore.advancedRendering" @hide="onModalHide">
|
<Modal ref="modal" :header="header" :noblur="!themeStore.advancedRendering" @hide="onModalHide">
|
||||||
|
<template #title>
|
||||||
|
<slot name="title" />
|
||||||
|
</template>
|
||||||
<slot />
|
<slot />
|
||||||
</Modal>
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { DropdownSelect, Toggle, ThemeSelector } from '@modrinth/ui'
|
import { Toggle, ThemeSelector, TeleportDropdownMenu } from '@modrinth/ui'
|
||||||
import { useTheming } from '@/store/state'
|
import { useTheming } from '@/store/state'
|
||||||
import { get, set } from '@/helpers/settings'
|
import { get, set } from '@/helpers/settings'
|
||||||
import { watch, ref } from 'vue'
|
import { watch, ref } from 'vue'
|
||||||
@@ -19,7 +19,7 @@ watch(
|
|||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<h2 class="m-0 text-2xl">Color theme</h2>
|
<h2 class="m-0 text-lg font-extrabold text-contrast">Color theme</h2>
|
||||||
<p class="m-0 mt-1">Select your preferred color theme for Modrinth App.</p>
|
<p class="m-0 mt-1">Select your preferred color theme for Modrinth App.</p>
|
||||||
|
|
||||||
<ThemeSelector
|
<ThemeSelector
|
||||||
@@ -31,7 +31,7 @@ watch(
|
|||||||
|
|
||||||
<div class="mt-4 flex items-center justify-between">
|
<div class="mt-4 flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="m-0 text-2xl">Advanced rendering</h2>
|
<h2 class="m-0 text-lg font-extrabold text-contrast">Advanced rendering</h2>
|
||||||
<p class="m-0 mt-1">
|
<p class="m-0 mt-1">
|
||||||
Enables advanced rendering such as blur effects that may cause performance issues without
|
Enables advanced rendering such as blur effects that may cause performance issues without
|
||||||
hardware-accelerated rendering.
|
hardware-accelerated rendering.
|
||||||
@@ -53,7 +53,7 @@ watch(
|
|||||||
|
|
||||||
<div v-if="os !== 'MacOS'" class="mt-4 flex items-center justify-between gap-4">
|
<div v-if="os !== 'MacOS'" class="mt-4 flex items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="m-0 mt-4 text-2xl">Native Decorations</h2>
|
<h2 class="m-0 text-lg font-extrabold text-contrast">Native Decorations</h2>
|
||||||
<p class="m-0 mt-1">Use system window frame (app restart required).</p>
|
<p class="m-0 mt-1">Use system window frame (app restart required).</p>
|
||||||
</div>
|
</div>
|
||||||
<Toggle
|
<Toggle
|
||||||
@@ -70,7 +70,7 @@ watch(
|
|||||||
|
|
||||||
<div class="mt-4 flex items-center justify-between">
|
<div class="mt-4 flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="m-0 mt-4 text-2xl">Minimize launcher</h2>
|
<h2 class="m-0 text-lg font-extrabold text-contrast">Minimize launcher</h2>
|
||||||
<p class="m-0 mt-1">Minimize the launcher when a Minecraft process starts.</p>
|
<p class="m-0 mt-1">Minimize the launcher when a Minecraft process starts.</p>
|
||||||
</div>
|
</div>
|
||||||
<Toggle
|
<Toggle
|
||||||
@@ -87,16 +87,14 @@ watch(
|
|||||||
|
|
||||||
<div class="mt-4 flex items-center justify-between">
|
<div class="mt-4 flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="m-0 mt-4 text-2xl">Default landing page</h2>
|
<h2 class="m-0 text-lg font-extrabold text-contrast">Default landing page</h2>
|
||||||
<p class="m-0 mt-1">Change the page to which the launcher opens on.</p>
|
<p class="m-0 mt-1">Change the page to which the launcher opens on.</p>
|
||||||
</div>
|
</div>
|
||||||
<DropdownSelect
|
<TeleportDropdownMenu
|
||||||
id="opening-page"
|
id="opening-page"
|
||||||
v-model="settings.default_page"
|
v-model="settings.default_page"
|
||||||
name="Opening page dropdown"
|
name="Opening page dropdown"
|
||||||
:options="['Home', 'Library']"
|
:options="['Home', 'Library']"
|
||||||
class="opening-page"
|
|
||||||
@change="updateDefaultPage"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -46,121 +46,136 @@ watch(
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<h2 class="m-0 text-2xl">Java arguments</h2>
|
<div>
|
||||||
<input
|
<h2 class="m-0 text-lg font-extrabold text-contrast">Window size</h2>
|
||||||
id="java-args"
|
|
||||||
v-model="settings.launchArgs"
|
|
||||||
autocomplete="off"
|
|
||||||
type="text"
|
|
||||||
class="installation-input"
|
|
||||||
placeholder="Enter java arguments..."
|
|
||||||
/>
|
|
||||||
|
|
||||||
<h2 class="mt-4 m-0 text-2xl">Environmental variables</h2>
|
<div class="flex items-center justify-between gap-4">
|
||||||
<input
|
<div>
|
||||||
id="env-vars"
|
<h3 class="mt-2 m-0 text-base font-extrabold text-primary">Fullscreen</h3>
|
||||||
v-model="settings.envVars"
|
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
|
||||||
autocomplete="off"
|
Overwrites the options.txt file to start in full screen when launched.
|
||||||
type="text"
|
</p>
|
||||||
class="installation-input"
|
</div>
|
||||||
placeholder="Enter environmental variables..."
|
|
||||||
/>
|
|
||||||
|
|
||||||
<h2 class="mt-4 m-0 text-2xl">Java memory</h2>
|
<Toggle
|
||||||
<p class="m-0 mt-1 leading-tight">The memory allocated to each instance when it is ran.</p>
|
id="fullscreen"
|
||||||
<Slider
|
:model-value="settings.force_fullscreen"
|
||||||
id="max-memory"
|
:checked="settings.force_fullscreen"
|
||||||
v-model="settings.memory.maximum"
|
@update:model-value="
|
||||||
:min="8"
|
(e) => {
|
||||||
:max="maxMemory"
|
settings.force_fullscreen = e
|
||||||
:step="64"
|
}
|
||||||
unit="MB"
|
"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<h2 class="mt-4 m-0 text-2xl">Hooks</h2>
|
|
||||||
|
|
||||||
<h3 class="mt-2 m-0 text-lg">Pre launch</h3>
|
|
||||||
<p class="m-0 mt-1 leading-tight">Ran before the instance is launched.</p>
|
|
||||||
<input
|
|
||||||
id="pre-launch"
|
|
||||||
v-model="settings.hooks.pre_launch"
|
|
||||||
autocomplete="off"
|
|
||||||
type="text"
|
|
||||||
placeholder="Enter pre-launch command..."
|
|
||||||
/>
|
|
||||||
|
|
||||||
<h3 class="mt-2 m-0 text-lg">Wrapper</h3>
|
|
||||||
<p class="m-0 mt-1 leading-tight">Wrapper command for launching Minecraft.</p>
|
|
||||||
<input
|
|
||||||
id="wrapper"
|
|
||||||
v-model="settings.hooks.wrapper"
|
|
||||||
autocomplete="off"
|
|
||||||
type="text"
|
|
||||||
placeholder="Enter wrapper command..."
|
|
||||||
/>
|
|
||||||
|
|
||||||
<h3 class="mt-2 m-0 text-lg">Post exit</h3>
|
|
||||||
<p class="m-0 mt-1 leading-tight">Ran after the game closes.</p>
|
|
||||||
<input
|
|
||||||
id="post-exit"
|
|
||||||
v-model="settings.hooks.post_exit"
|
|
||||||
autocomplete="off"
|
|
||||||
type="text"
|
|
||||||
placeholder="Enter post-exit command..."
|
|
||||||
/>
|
|
||||||
|
|
||||||
<h2 class="mt-4 m-0 text-2xl">Window size</h2>
|
|
||||||
|
|
||||||
<div class="flex items-center justify-between gap-4">
|
|
||||||
<div>
|
|
||||||
<h3 class="mt-2 m-0 text-lg">Fullscreen</h3>
|
|
||||||
<p class="m-0 mt-1 leading-tight">
|
|
||||||
Overwrites the options.txt file to start in full screen when launched.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Toggle
|
<div class="flex items-center justify-between gap-4">
|
||||||
id="fullscreen"
|
<div>
|
||||||
:model-value="settings.force_fullscreen"
|
<h3 class="mt-2 m-0 text-base font-extrabold text-primary">Width</h3>
|
||||||
:checked="settings.force_fullscreen"
|
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
|
||||||
@update:model-value="
|
The width of the game window when launched.
|
||||||
(e) => {
|
</p>
|
||||||
settings.force_fullscreen = e
|
</div>
|
||||||
}
|
|
||||||
"
|
<input
|
||||||
|
id="width"
|
||||||
|
v-model="settings.game_resolution[0]"
|
||||||
|
:disabled="settings.force_fullscreen"
|
||||||
|
autocomplete="off"
|
||||||
|
type="number"
|
||||||
|
placeholder="Enter width..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<h3 class="mt-2 m-0 text-base font-extrabold text-primary">Height</h3>
|
||||||
|
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
|
||||||
|
The height of the game window when launched.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
id="height"
|
||||||
|
v-model="settings.game_resolution[1]"
|
||||||
|
:disabled="settings.force_fullscreen"
|
||||||
|
autocomplete="off"
|
||||||
|
type="number"
|
||||||
|
class="input"
|
||||||
|
placeholder="Enter height..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="mt-4 bg-button-border border-none h-[1px]" />
|
||||||
|
|
||||||
|
<h2 class="mt-4 m-0 text-lg font-extrabold text-contrast">Memory allocated</h2>
|
||||||
|
<p class="m-0 mt-1 leading-tight">The memory allocated to each instance when it is ran.</p>
|
||||||
|
<Slider
|
||||||
|
id="max-memory"
|
||||||
|
v-model="settings.memory.maximum"
|
||||||
|
:min="512"
|
||||||
|
:max="maxMemory"
|
||||||
|
:step="64"
|
||||||
|
unit="MB"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center justify-between gap-4">
|
|
||||||
<div>
|
|
||||||
<h3 class="mt-2 m-0 text-lg">Width</h3>
|
|
||||||
<p class="m-0 mt-1 leading-tight">The width of the game window when launched.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<h2 class="mt-4 mb-2 text-lg font-extrabold text-contrast">Java arguments</h2>
|
||||||
<input
|
<input
|
||||||
id="width"
|
id="java-args"
|
||||||
v-model="settings.game_resolution[0]"
|
v-model="settings.launchArgs"
|
||||||
:disabled="settings.force_fullscreen"
|
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
type="number"
|
type="text"
|
||||||
placeholder="Enter width..."
|
placeholder="Enter java arguments..."
|
||||||
|
class="w-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center justify-between gap-4">
|
|
||||||
<div>
|
|
||||||
<h3 class="mt-2 m-0 text-lg">Height</h3>
|
|
||||||
<p class="m-0 mt-1 leading-tight">The height of the game window when launched.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<h2 class="mt-4 mb-2 text-lg font-extrabold text-contrast">Environmental variables</h2>
|
||||||
<input
|
<input
|
||||||
id="height"
|
id="env-vars"
|
||||||
v-model="settings.game_resolution[1]"
|
v-model="settings.envVars"
|
||||||
:disabled="settings.force_fullscreen"
|
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
type="number"
|
type="text"
|
||||||
class="input"
|
placeholder="Enter environmental variables..."
|
||||||
placeholder="Enter height..."
|
class="w-full"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<hr class="mt-4 bg-button-border border-none h-[1px]" />
|
||||||
|
|
||||||
|
<h2 class="mt-4 m-0 text-lg font-extrabold text-contrast">Hooks</h2>
|
||||||
|
|
||||||
|
<h3 class="mt-2 m-0 text-base font-extrabold text-primary">Pre launch</h3>
|
||||||
|
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">Ran before the instance is launched.</p>
|
||||||
|
<input
|
||||||
|
id="pre-launch"
|
||||||
|
v-model="settings.hooks.pre_launch"
|
||||||
|
autocomplete="off"
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter pre-launch command..."
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h3 class="mt-2 m-0 text-base font-extrabold text-primary">Wrapper</h3>
|
||||||
|
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
|
||||||
|
Wrapper command for launching Minecraft.
|
||||||
|
</p>
|
||||||
|
<input
|
||||||
|
id="wrapper"
|
||||||
|
v-model="settings.hooks.wrapper"
|
||||||
|
autocomplete="off"
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter wrapper command..."
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h3 class="mt-2 m-0 text-base font-extrabold text-primary">Post exit</h3>
|
||||||
|
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">Ran after the game closes.</p>
|
||||||
|
<input
|
||||||
|
id="post-exit"
|
||||||
|
v-model="settings.hooks.post_exit"
|
||||||
|
autocomplete="off"
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter post-exit command..."
|
||||||
|
class="w-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ function formatFlagName(name: string) {
|
|||||||
<template>
|
<template>
|
||||||
<div v-for="option in options" :key="option" class="mt-4 flex items-center justify-between">
|
<div v-for="option in options" :key="option" class="mt-4 flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="m-0 text-xl capitalize">{{ formatFlagName(option) }}</h2>
|
<h2 class="m-0 text-lg font-extrabold text-contrast capitalize">
|
||||||
|
{{ formatFlagName(option) }}
|
||||||
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Toggle
|
<Toggle
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ async function updateJavaVersion(version) {
|
|||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div v-for="(javaVersion, index) in [21, 17, 8]" :key="`java-${javaVersion}`">
|
<div v-for="(javaVersion, index) in [21, 17, 8]" :key="`java-${javaVersion}`">
|
||||||
<h2 class="m-0 text-2xl" :class="{ 'mt-4': index !== 0 }">Java {{ javaVersion }} location</h2>
|
<h2 class="m-0 text-lg font-extrabold text-contrast" :class="{ 'mt-4': index !== 0 }">
|
||||||
|
Java {{ javaVersion }} location
|
||||||
|
</h2>
|
||||||
<JavaSelector
|
<JavaSelector
|
||||||
:id="'java-selector-' + javaVersion"
|
:id="'java-selector-' + javaVersion"
|
||||||
v-model="javaVersions[javaVersion]"
|
v-model="javaVersions[javaVersion]"
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ watch(
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex items-center justify-between gap-4">
|
<div class="flex items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="m-0 text-2xl">Personalized ads</h2>
|
<h2 class="m-0 text-lg font-extrabold text-contrast">Personalized ads</h2>
|
||||||
<p class="m-0 mt-1 leading-tight">
|
<p class="m-0 text-sm">
|
||||||
Modrinth's ad provider, Aditude, shows ads based on your preferences. By disabling this
|
Modrinth's ad provider, Aditude, shows ads based on your preferences. By disabling this
|
||||||
option, you opt out and ads will no longer be shown based on your interests.
|
option, you opt out and ads will no longer be shown based on your interests.
|
||||||
</p>
|
</p>
|
||||||
@@ -44,8 +44,8 @@ watch(
|
|||||||
|
|
||||||
<div class="mt-4 flex items-center justify-between gap-4">
|
<div class="mt-4 flex items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="m-0 text-2xl">Telemetry</h2>
|
<h2 class="m-0 text-lg font-extrabold text-contrast">Telemetry</h2>
|
||||||
<p class="m-0 mt-1 leading-tight">
|
<p class="m-0 text-sm">
|
||||||
Modrinth collects anonymized analytics and usage data to improve our user experience and
|
Modrinth collects anonymized analytics and usage data to improve our user experience and
|
||||||
customize your experience. By disabling this option, you opt out and your data will no
|
customize your experience. By disabling this option, you opt out and your data will no
|
||||||
longer be collected.
|
longer be collected.
|
||||||
@@ -65,12 +65,14 @@ watch(
|
|||||||
|
|
||||||
<div class="mt-4 flex items-center justify-between gap-4">
|
<div class="mt-4 flex items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="m-0 text-2xl">Discord RPC</h2>
|
<h2 class="m-0 text-lg font-extrabold text-contrast">Discord RPC</h2>
|
||||||
<p class="m-0 mt-1 leading-tight">
|
<p class="m-0 text-sm">
|
||||||
Manages the Discord Rich Presence integration. Disabling this will cause 'Modrinth' to no
|
Manages the Discord Rich Presence integration. Disabling this will cause 'Modrinth' to no
|
||||||
longer show up as a game or app you are using on your Discord profile. This does not disable
|
longer show up as a game or app you are using on your Discord profile.
|
||||||
any instance-specific Discord Rich Presence integrations, such as those added by mods. (app
|
</p>
|
||||||
restart required to take effect)
|
<p class="m-0 mt-2 text-sm">
|
||||||
|
Note: This will not prevent any instance-specific Discord Rich Presence integrations, such
|
||||||
|
as those added by mods. (app restart required to take effect)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Toggle
|
<Toggle
|
||||||
|
|||||||
@@ -57,13 +57,13 @@ async function findLauncherDir() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<h2 class="m-0 text-2xl">App directory</h2>
|
<h2 class="m-0 text-lg font-extrabold text-contrast">App directory</h2>
|
||||||
<p class="m-0 mt-1">
|
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
|
||||||
The directory where the launcher stores all of its files. Changes will be applied after
|
The directory where the launcher stores all of its files. Changes will be applied after
|
||||||
restarting the launcher.
|
restarting the launcher.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="m-1 mt-2">
|
<div class="m-1 my-2">
|
||||||
<div class="iconified-input w-full">
|
<div class="iconified-input w-full">
|
||||||
<BoxIcon />
|
<BoxIcon />
|
||||||
<input id="appDir" v-model="settings.custom_dir" type="text" class="input" />
|
<input id="appDir" v-model="settings.custom_dir" type="text" class="input" />
|
||||||
@@ -73,31 +73,29 @@ async function findLauncherDir() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center justify-between gap-4 mt-4">
|
<div>
|
||||||
<div>
|
<ConfirmModalWrapper
|
||||||
<ConfirmModalWrapper
|
ref="purgeCacheConfirmModal"
|
||||||
ref="purgeCacheConfirmModal"
|
title="Are you sure you want to purge the cache?"
|
||||||
title="Are you sure you want to purge the cache?"
|
description="If you proceed, your entire cache will be purged. This may slow down the app temporarily."
|
||||||
description="If you proceed, your entire cache will be purged. This may slow down the app temporarily."
|
:has-to-type="false"
|
||||||
:has-to-type="false"
|
proceed-label="Purge cache"
|
||||||
proceed-label="Purge cache"
|
@proceed="purgeCache"
|
||||||
@proceed="purgeCache"
|
/>
|
||||||
/>
|
|
||||||
|
|
||||||
<h2 class="m-0 text-2xl">App cache</h2>
|
<h2 class="m-0 text-lg font-extrabold text-contrast">App cache</h2>
|
||||||
<p class="m-0 mt-1 leading-tight">
|
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
|
||||||
The Modrinth app stores a cache of data to speed up loading. This can be purged to force the
|
The Modrinth app stores a cache of data to speed up loading. This can be purged to force the
|
||||||
app to reload data. This may slow down the app temporarily.
|
app to reload data. This may slow down the app temporarily.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
<button id="purge-cache" class="btn min-w-max" @click="$refs.purgeCacheConfirmModal.show()">
|
|
||||||
<TrashIcon />
|
|
||||||
Purge cache
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<button id="purge-cache" class="btn min-w-max" @click="$refs.purgeCacheConfirmModal.show()">
|
||||||
|
<TrashIcon />
|
||||||
|
Purge cache
|
||||||
|
</button>
|
||||||
|
|
||||||
<h2 class="m-0 text-2xl mt-4">Maximum concurrent downloads</h2>
|
<h2 class="m-0 text-lg font-extrabold text-contrast mt-4">Maximum concurrent downloads</h2>
|
||||||
<p class="m-0 mt-1">
|
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
|
||||||
The maximum amount of files the launcher can download at the same time. Set this to a lower
|
The maximum amount of files the launcher can download at the same time. Set this to a lower
|
||||||
value if you have a poor internet connection. (app restart required to take effect)
|
value if you have a poor internet connection. (app restart required to take effect)
|
||||||
</p>
|
</p>
|
||||||
@@ -109,8 +107,8 @@ async function findLauncherDir() {
|
|||||||
:step="1"
|
:step="1"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<h2 class="m-0 mt-4 text-2xl">Maximum concurrent writes</h2>
|
<h2 class="mt-4 m-0 text-lg font-extrabold text-contrast">Maximum concurrent writes</h2>
|
||||||
<p class="m-0 mt-1">
|
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
|
||||||
The maximum amount of files the launcher can write to the disk at once. Set this to a lower
|
The maximum amount of files the launcher can write to the disk at once. Set this to a lower
|
||||||
value if you are frequently getting I/O errors. (app restart required to take effect)
|
value if you are frequently getting I/O errors. (app restart required to take effect)
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
105
apps/app-frontend/src/helpers/types.d.ts
vendored
Normal file
105
apps/app-frontend/src/helpers/types.d.ts
vendored
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import type { ModrinthId } from '@modrinth/utils'
|
||||||
|
|
||||||
|
type GameInstance = {
|
||||||
|
path: string
|
||||||
|
install_stage: InstallStage
|
||||||
|
|
||||||
|
name: string
|
||||||
|
icon_path?: string
|
||||||
|
|
||||||
|
game_version: string
|
||||||
|
loader: InstanceLoader
|
||||||
|
loader_version?: string
|
||||||
|
|
||||||
|
groups: string[]
|
||||||
|
|
||||||
|
linked_data?: LinkedData
|
||||||
|
|
||||||
|
created: Date
|
||||||
|
modified: Date
|
||||||
|
last_played?: Date
|
||||||
|
|
||||||
|
submitted_time_played: number
|
||||||
|
recent_time_played: number
|
||||||
|
|
||||||
|
java_path?: string
|
||||||
|
extra_launch_args?: string[]
|
||||||
|
custom_env_vars?: [string, string][]
|
||||||
|
|
||||||
|
memory?: MemorySettings
|
||||||
|
force_fullscreen?: boolean
|
||||||
|
game_resolution?: [number, number]
|
||||||
|
hooks: Hooks
|
||||||
|
}
|
||||||
|
|
||||||
|
type InstallStage = 'installed' | 'installing' | 'pack_installing' | 'not_installed'
|
||||||
|
|
||||||
|
type LinkedData = {
|
||||||
|
project_id: ModrinthId
|
||||||
|
version_id: ModrinthId
|
||||||
|
|
||||||
|
locked: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type InstanceLoader = 'vanilla' | 'forge' | 'fabric' | 'quilt' | 'neoforge'
|
||||||
|
|
||||||
|
type MemorySettings = {
|
||||||
|
maximum: number
|
||||||
|
}
|
||||||
|
|
||||||
|
type WindowSize = {
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
}
|
||||||
|
|
||||||
|
type Hooks = {
|
||||||
|
pre_launch?: string
|
||||||
|
wrapper?: string
|
||||||
|
post_exit?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Manifest = {
|
||||||
|
gameVersions: ManifestGameVersion[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type ManifestGameVersion = {
|
||||||
|
id: string
|
||||||
|
stable: boolean
|
||||||
|
loaders: ManifestLoaderVersion[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type ManifestLoaderVersion = {
|
||||||
|
id: string
|
||||||
|
url: string
|
||||||
|
stable: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type AppSettings = {
|
||||||
|
max_concurrent_downloads: number
|
||||||
|
max_concurrent_writes: number
|
||||||
|
|
||||||
|
theme: 'dark' | 'light' | 'oled'
|
||||||
|
default_page: 'Home' | 'Library'
|
||||||
|
collapsed_navigation: boolean
|
||||||
|
advanced_rendering: boolean
|
||||||
|
native_decorations: boolean
|
||||||
|
|
||||||
|
telemetry: boolean
|
||||||
|
discord_rpc: boolean
|
||||||
|
developer_mode: boolean
|
||||||
|
personalized_ads: boolean
|
||||||
|
|
||||||
|
onboarded: boolean
|
||||||
|
|
||||||
|
extra_launch_args: string[]
|
||||||
|
custom_env_vars: [string, string][]
|
||||||
|
memory: MemorySettings
|
||||||
|
force_fullscreen: boolean
|
||||||
|
game_resolution: [number, number]
|
||||||
|
hide_on_process_start: boolean
|
||||||
|
hooks: Hooks
|
||||||
|
|
||||||
|
custom_dir?: string
|
||||||
|
prev_custom_dir?: string
|
||||||
|
migrated: boolean
|
||||||
|
}
|
||||||
@@ -11,8 +11,8 @@
|
|||||||
"app.settings.tabs.feature-flags": {
|
"app.settings.tabs.feature-flags": {
|
||||||
"message": "Feature flags"
|
"message": "Feature flags"
|
||||||
},
|
},
|
||||||
"app.settings.tabs.java-versions": {
|
"app.settings.tabs.java-installations": {
|
||||||
"message": "Java versions"
|
"message": "Java installations"
|
||||||
},
|
},
|
||||||
"app.settings.tabs.privacy": {
|
"app.settings.tabs.privacy": {
|
||||||
"message": "Privacy"
|
"message": "Privacy"
|
||||||
@@ -23,6 +23,270 @@
|
|||||||
"instance.filter.updates-available": {
|
"instance.filter.updates-available": {
|
||||||
"message": "Updates available"
|
"message": "Updates available"
|
||||||
},
|
},
|
||||||
|
"instance.settings.tabs.general": {
|
||||||
|
"message": "General"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.general.delete": {
|
||||||
|
"message": "Delete instance"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.general.delete.button": {
|
||||||
|
"message": "Delete instance"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.general.delete.description": {
|
||||||
|
"message": "Permanently deletes an instance from your device, including your worlds, configs, and all installed content. Be careful, as once you delete a instance there is no way to recover it."
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.general.deleting.button": {
|
||||||
|
"message": "Deleting..."
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.general.duplicate-button": {
|
||||||
|
"message": "Duplicate"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.general.duplicate-button.tooltip.installing": {
|
||||||
|
"message": "Cannot duplicate while installing."
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.general.duplicate-instance": {
|
||||||
|
"message": "Duplicate instance"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.general.duplicate-instance.description": {
|
||||||
|
"message": "Creates a copy of this instance, including worlds, configs, mods, etc."
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.general.edit-icon": {
|
||||||
|
"message": "Edit icon"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.general.edit-icon.remove": {
|
||||||
|
"message": "Remove icon"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.general.edit-icon.replace": {
|
||||||
|
"message": "Replace icon"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.general.edit-icon.select": {
|
||||||
|
"message": "Select icon"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.general.library-groups": {
|
||||||
|
"message": "Library groups"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.general.library-groups.create": {
|
||||||
|
"message": "Create new group"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.general.library-groups.description": {
|
||||||
|
"message": "Library groups allow you to organize your instances into different sections in your library."
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.general.library-groups.enter-name": {
|
||||||
|
"message": "Enter group name"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.general.name": {
|
||||||
|
"message": "Name"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.hooks": {
|
||||||
|
"message": "Launch hooks"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.hooks.custom-hooks": {
|
||||||
|
"message": "Custom launch hooks"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.hooks.description": {
|
||||||
|
"message": "Hooks allow advanced users to run certain system commands before and after launching the game."
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.hooks.post-exit": {
|
||||||
|
"message": "Post-exit"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.hooks.post-exit.description": {
|
||||||
|
"message": "Ran after the game closes."
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.hooks.post-exit.enter": {
|
||||||
|
"message": "Enter post-exit command..."
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.hooks.pre-launch": {
|
||||||
|
"message": "Pre-launch"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.hooks.pre-launch.description": {
|
||||||
|
"message": "Ran before the instance is launched."
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.hooks.pre-launch.enter": {
|
||||||
|
"message": "Enter pre-launch command..."
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.hooks.title": {
|
||||||
|
"message": "Game launch hooks"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.hooks.wrapper": {
|
||||||
|
"message": "Wrapper"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.hooks.wrapper.description": {
|
||||||
|
"message": "Wrapper command for launching Minecraft."
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.hooks.wrapper.enter": {
|
||||||
|
"message": "Enter wrapper command..."
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.installation": {
|
||||||
|
"message": "Installation"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.installation.change-version.button": {
|
||||||
|
"message": "Change version"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.installation.change-version.button.install": {
|
||||||
|
"message": "Install"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.installation.change-version.button.installing": {
|
||||||
|
"message": "Installing"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.installation.change-version.cannot-while-fetching": {
|
||||||
|
"message": "Fetching modpack versions"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.installation.change-version.in-progress": {
|
||||||
|
"message": "Installing new version"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.installation.currently-installed": {
|
||||||
|
"message": "Currently installed"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.installation.debug-information": {
|
||||||
|
"message": "Debug information:"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.installation.fetching-modpack-details": {
|
||||||
|
"message": "Fetching modpack details"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.installation.game-version": {
|
||||||
|
"message": "Game version"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.installation.install": {
|
||||||
|
"message": "Install"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.installation.loader-version": {
|
||||||
|
"message": "{loader} version"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.installation.minecraft-version": {
|
||||||
|
"message": "Minecraft {version}"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.installation.no-connection": {
|
||||||
|
"message": "Cannot fetch linked modpack details. Please check your internet connection."
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.installation.no-loader-versions": {
|
||||||
|
"message": "{loader} is not available for Minecraft {version}. Try another mod loader."
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.installation.no-modpack-found": {
|
||||||
|
"message": "This instance is linked to a modpack, but the modpack could not be found on Modrinth."
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.installation.platform": {
|
||||||
|
"message": "Platform"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.installation.reinstall.button": {
|
||||||
|
"message": "Reinstall modpack"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.installation.reinstall.button.reinstalling": {
|
||||||
|
"message": "Reinstalling modpack"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.installation.reinstall.confirm.description": {
|
||||||
|
"message": "Reinstalling will reset content provided by the modpack to their original state."
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.installation.reinstall.confirm.title": {
|
||||||
|
"message": "Are you sure you want to reinstall this instance?"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.installation.reinstall.description": {
|
||||||
|
"message": "Resets all content provided by the modpack to their original state. This may fix unexpected behavior if changes have been made to the instance."
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.installation.reinstall.title": {
|
||||||
|
"message": "Reinstall modpack"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.installation.repair.button": {
|
||||||
|
"message": "Repair"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.installation.repair.button.repairing": {
|
||||||
|
"message": "Repairing"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.installation.repair.confirm-title": {
|
||||||
|
"message": "Repair instance?"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.installation.repair.description": {
|
||||||
|
"message": "Repairing reinstalls Minecraft dependencies and checks for corruption. This may resolve issues if your game is not launching due to launcher-related errors, but will not resolve issues or crashes related to installed mods."
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.installation.repair.in-progress": {
|
||||||
|
"message": "Repair in progress"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.installation.reset-selections": {
|
||||||
|
"message": "Reset to current"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.installation.show-all-versions": {
|
||||||
|
"message": "Show all versions"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.installation.tooltip.action.change-version": {
|
||||||
|
"message": "change version"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.installation.tooltip.action.reinstall": {
|
||||||
|
"message": "reinstall"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.installation.tooltip.action.repair": {
|
||||||
|
"message": "repair"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.installation.tooltip.cannot-while-installing": {
|
||||||
|
"message": "Cannot {action} while installing"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.installation.tooltip.cannot-while-offline": {
|
||||||
|
"message": "Cannot {action} while offline"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.installation.tooltip.cannot-while-repairing": {
|
||||||
|
"message": "Cannot {action} while repairing"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.installation.unknown-version": {
|
||||||
|
"message": "(unknown version)"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.installation.unlink.button": {
|
||||||
|
"message": "Unlink instance"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.installation.unlink.description": {
|
||||||
|
"message": "If you proceed, you will not be able to re-link it without creating an entirely new instance. You will no longer receive modpack updates and it will become a normal."
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.installation.unlink.title": {
|
||||||
|
"message": "Are you sure you want to unlink this instance?"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.java": {
|
||||||
|
"message": "Java and memory"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.java.environment-variables": {
|
||||||
|
"message": "Environment variables"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.java.hooks": {
|
||||||
|
"message": "Hooks"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.java.java-arguments": {
|
||||||
|
"message": "Java arguments"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.java.java-installation": {
|
||||||
|
"message": "Java installation"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.java.java-memory": {
|
||||||
|
"message": "Memory allocated"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.window": {
|
||||||
|
"message": "Window"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.window.custom-window-settings": {
|
||||||
|
"message": "Custom window settings"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.window.fullscreen": {
|
||||||
|
"message": "Fullscreen"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.window.fullscreen.description": {
|
||||||
|
"message": "Make the game start in full screen when launched (using options.txt)."
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.window.height": {
|
||||||
|
"message": "Height"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.window.height.description": {
|
||||||
|
"message": "The height of the game window when launched."
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.window.height.enter": {
|
||||||
|
"message": "Enter height..."
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.window.width": {
|
||||||
|
"message": "Width"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.window.width.description": {
|
||||||
|
"message": "The width of the game window when launched."
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.window.width.enter": {
|
||||||
|
"message": "Enter width..."
|
||||||
|
},
|
||||||
|
"instance.settings.title": {
|
||||||
|
"message": "Settings"
|
||||||
|
},
|
||||||
"search.filter.locked.instance": {
|
"search.filter.locked.instance": {
|
||||||
"message": "Provided by the instance"
|
"message": "Provided by the instance"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
@contextmenu.prevent.stop="(event) => handleRightClick(event, instance.path)"
|
@contextmenu.prevent.stop="(event) => handleRightClick(event, instance.path)"
|
||||||
>
|
>
|
||||||
<ExportModal ref="exportModal" :instance="instance" />
|
<ExportModal ref="exportModal" :instance="instance" />
|
||||||
|
<InstanceSettingsModal ref="settingsModal" :instance="instance" />
|
||||||
<ContentPageHeader>
|
<ContentPageHeader>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<Avatar :src="icon" :alt="instance.name" size="96px" />
|
<Avatar :src="icon" :alt="instance.name" size="96px" />
|
||||||
@@ -56,12 +57,9 @@
|
|||||||
<button disabled>Loading...</button>
|
<button disabled>Loading...</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled size="large" circular>
|
<ButtonStyled size="large" circular>
|
||||||
<RouterLink
|
<button v-tooltip="'Instance settings'" @click="settingsModal.show()">
|
||||||
v-tooltip="'Instance settings'"
|
|
||||||
:to="`/instance/${encodeURIComponent(route.params.id)}/options`"
|
|
||||||
>
|
|
||||||
<SettingsIcon />
|
<SettingsIcon />
|
||||||
</RouterLink>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled size="large" type="transparent" circular>
|
<ButtonStyled size="large" type="transparent" circular>
|
||||||
<OverflowMenu
|
<OverflowMenu
|
||||||
@@ -179,6 +177,7 @@ import dayjs from 'dayjs'
|
|||||||
import duration from 'dayjs/plugin/duration'
|
import duration from 'dayjs/plugin/duration'
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||||
import ExportModal from '@/components/ui/ExportModal.vue'
|
import ExportModal from '@/components/ui/ExportModal.vue'
|
||||||
|
import InstanceSettingsModal from '@/components/ui/modal/InstanceSettingsModal.vue'
|
||||||
|
|
||||||
dayjs.extend(duration)
|
dayjs.extend(duration)
|
||||||
dayjs.extend(relativeTime)
|
dayjs.extend(relativeTime)
|
||||||
@@ -362,6 +361,8 @@ const icon = computed(() =>
|
|||||||
instance.value.icon_path ? convertFileSrc(instance.value.icon_path) : null,
|
instance.value.icon_path ? convertFileSrc(instance.value.icon_path) : null,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const settingsModal = ref()
|
||||||
|
|
||||||
const timePlayed = computed(() => {
|
const timePlayed = computed(() => {
|
||||||
return instance.value.recent_time_played + instance.value.submitted_time_played
|
return instance.value.recent_time_played + instance.value.submitted_time_played
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -220,7 +220,7 @@
|
|||||||
<Slider
|
<Slider
|
||||||
v-model="memory.maximum"
|
v-model="memory.maximum"
|
||||||
:disabled="!overrideMemorySettings"
|
:disabled="!overrideMemorySettings"
|
||||||
:min="8"
|
:min="512"
|
||||||
:max="maxMemory"
|
:max="maxMemory"
|
||||||
:step="64"
|
:step="64"
|
||||||
unit="mb"
|
unit="mb"
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ export default {
|
|||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
icon: 'var(--color-icon)',
|
icon: 'var(--color-base)',
|
||||||
// Text
|
// Text
|
||||||
primary: 'var(--color-text)',
|
primary: 'var(--color-base)',
|
||||||
contrast: 'var(--color-contrast)',
|
contrast: 'var(--color-contrast)',
|
||||||
secondary: 'var(--color-secondary)',
|
secondary: 'var(--color-secondary)',
|
||||||
inactive: 'var(--color-text-inactive)',
|
inactive: 'var(--color-text-inactive)',
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ theseus = { path = "../../packages/app-lib", features = ["tauri"] }
|
|||||||
|
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_with = "3.0.0"
|
||||||
|
|
||||||
tauri = { git = "https://github.com/modrinth/tauri", rev = "9c36dd3", features = ["devtools", "macos-private-api", "protocol-asset", "unstable"] }
|
tauri = { git = "https://github.com/modrinth/tauri", rev = "9c36dd3", features = ["devtools", "macos-private-api", "protocol-asset", "unstable"] }
|
||||||
tauri-plugin-window-state = "2.2.0"
|
tauri-plugin-window-state = "2.2.0"
|
||||||
|
|||||||
@@ -282,19 +282,59 @@ pub struct EditProfile {
|
|||||||
|
|
||||||
pub game_version: Option<String>,
|
pub game_version: Option<String>,
|
||||||
pub loader: Option<ModLoader>,
|
pub loader: Option<ModLoader>,
|
||||||
pub loader_version: Option<String>,
|
#[serde(
|
||||||
|
default,
|
||||||
|
skip_serializing_if = "Option::is_none",
|
||||||
|
with = "::serde_with::rust::double_option"
|
||||||
|
)]
|
||||||
|
pub loader_version: Option<Option<String>>,
|
||||||
|
|
||||||
pub groups: Option<Vec<String>>,
|
pub groups: Option<Vec<String>>,
|
||||||
|
|
||||||
pub linked_data: Option<LinkedData>,
|
#[serde(
|
||||||
|
default,
|
||||||
|
skip_serializing_if = "Option::is_none",
|
||||||
|
with = "::serde_with::rust::double_option"
|
||||||
|
)]
|
||||||
|
pub linked_data: Option<Option<LinkedData>>,
|
||||||
|
|
||||||
pub java_path: Option<String>,
|
#[serde(
|
||||||
pub extra_launch_args: Option<Vec<String>>,
|
default,
|
||||||
pub custom_env_vars: Option<Vec<(String, String)>>,
|
skip_serializing_if = "Option::is_none",
|
||||||
|
with = "::serde_with::rust::double_option"
|
||||||
|
)]
|
||||||
|
pub java_path: Option<Option<String>>,
|
||||||
|
#[serde(
|
||||||
|
default,
|
||||||
|
skip_serializing_if = "Option::is_none",
|
||||||
|
with = "::serde_with::rust::double_option"
|
||||||
|
)]
|
||||||
|
pub extra_launch_args: Option<Option<Vec<String>>>,
|
||||||
|
#[serde(
|
||||||
|
default,
|
||||||
|
skip_serializing_if = "Option::is_none",
|
||||||
|
with = "::serde_with::rust::double_option"
|
||||||
|
)]
|
||||||
|
pub custom_env_vars: Option<Option<Vec<(String, String)>>>,
|
||||||
|
|
||||||
pub memory: Option<MemorySettings>,
|
#[serde(
|
||||||
pub force_fullscreen: Option<bool>,
|
default,
|
||||||
pub game_resolution: Option<WindowSize>,
|
skip_serializing_if = "Option::is_none",
|
||||||
|
with = "::serde_with::rust::double_option"
|
||||||
|
)]
|
||||||
|
pub memory: Option<Option<MemorySettings>>,
|
||||||
|
#[serde(
|
||||||
|
default,
|
||||||
|
skip_serializing_if = "Option::is_none",
|
||||||
|
with = "::serde_with::rust::double_option"
|
||||||
|
)]
|
||||||
|
pub force_fullscreen: Option<Option<bool>>,
|
||||||
|
#[serde(
|
||||||
|
default,
|
||||||
|
skip_serializing_if = "Option::is_none",
|
||||||
|
with = "::serde_with::rust::double_option"
|
||||||
|
)]
|
||||||
|
pub game_resolution: Option<Option<WindowSize>>,
|
||||||
pub hooks: Option<Hooks>,
|
pub hooks: Option<Hooks>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,28 +352,40 @@ pub async fn profile_edit(path: &str, edit_profile: EditProfile) -> Result<()> {
|
|||||||
if let Some(loader) = edit_profile.loader {
|
if let Some(loader) = edit_profile.loader {
|
||||||
prof.loader = loader;
|
prof.loader = loader;
|
||||||
}
|
}
|
||||||
prof.loader_version.clone_from(&edit_profile.loader_version);
|
if let Some(loader_version) = edit_profile.loader_version.clone() {
|
||||||
prof.linked_data.clone_from(&edit_profile.linked_data);
|
prof.loader_version = loader_version;
|
||||||
|
}
|
||||||
|
if let Some(linked_data) = edit_profile.linked_data.clone() {
|
||||||
|
prof.linked_data = linked_data;
|
||||||
|
}
|
||||||
if let Some(groups) = edit_profile.groups.clone() {
|
if let Some(groups) = edit_profile.groups.clone() {
|
||||||
prof.groups = groups;
|
prof.groups = groups;
|
||||||
}
|
}
|
||||||
|
if let Some(java_path) = edit_profile.java_path.clone() {
|
||||||
prof.java_path.clone_from(&edit_profile.java_path);
|
prof.java_path = java_path;
|
||||||
prof.memory = edit_profile.memory;
|
}
|
||||||
prof.game_resolution = edit_profile.game_resolution;
|
if let Some(memory) = edit_profile.memory.clone() {
|
||||||
prof.force_fullscreen = edit_profile.force_fullscreen;
|
prof.memory = memory;
|
||||||
|
}
|
||||||
|
if let Some(game_resolution) = edit_profile.game_resolution.clone() {
|
||||||
|
prof.game_resolution = game_resolution;
|
||||||
|
}
|
||||||
|
if let Some(force_fullscreen) = edit_profile.force_fullscreen.clone() {
|
||||||
|
prof.force_fullscreen = force_fullscreen;
|
||||||
|
}
|
||||||
if let Some(hooks) = edit_profile.hooks.clone() {
|
if let Some(hooks) = edit_profile.hooks.clone() {
|
||||||
prof.hooks = hooks;
|
prof.hooks = hooks;
|
||||||
}
|
}
|
||||||
|
|
||||||
prof.modified = chrono::Utc::now();
|
prof.modified = chrono::Utc::now();
|
||||||
|
|
||||||
prof.custom_env_vars
|
if let Some(custom_env_vars) = edit_profile.custom_env_vars.clone() {
|
||||||
.clone_from(&edit_profile.custom_env_vars);
|
prof.custom_env_vars = custom_env_vars;
|
||||||
prof.extra_launch_args
|
}
|
||||||
.clone_from(&edit_profile.extra_launch_args);
|
if let Some(extra_launch_args) = edit_profile.extra_launch_args.clone()
|
||||||
|
{
|
||||||
|
prof.extra_launch_args = extra_launch_args;
|
||||||
|
}
|
||||||
|
|
||||||
async { Ok(()) }
|
async { Ok(()) }
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ const config = {
|
|||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
icon: "var(--color-icon)",
|
icon: "var(--color-base)",
|
||||||
// Text
|
// Text
|
||||||
primary: "var(--color-text)",
|
primary: "var(--color-base)",
|
||||||
contrast: "var(--color-contrast)",
|
contrast: "var(--color-contrast)",
|
||||||
secondary: "var(--color-secondary)",
|
secondary: "var(--color-secondary)",
|
||||||
inactive: "var(--color-text-inactive)",
|
inactive: "var(--color-text-inactive)",
|
||||||
|
|||||||
1
packages/assets/icons/monitor.svg
Normal file
1
packages/assets/icons/monitor.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="14" x="2" y="3" rx="2"/><line x1="8" x2="16" y1="21" y2="21"/><line x1="12" x2="12" y1="17" y2="21"/></svg>
|
||||||
|
After Width: | Height: | Size: 314 B |
@@ -1,10 +1,11 @@
|
|||||||
<svg
|
<svg
|
||||||
|
width="24" height="24"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<circle
|
<circle
|
||||||
class="opacity-25"
|
opacity="0.25"
|
||||||
cx="12"
|
cx="12"
|
||||||
cy="12"
|
cy="12"
|
||||||
r="10"
|
r="10"
|
||||||
@@ -12,7 +13,7 @@
|
|||||||
stroke-width="4"
|
stroke-width="4"
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
class="opacity-75"
|
opacity="0.75"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
/>
|
/>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 387 B After Width: | Height: | Size: 404 B |
1
packages/assets/icons/unlink.svg
Normal file
1
packages/assets/icons/unlink.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-unlink"><path d="m18.84 12.25 1.72-1.71h-.02a5.004 5.004 0 0 0-.12-7.07 5.006 5.006 0 0 0-6.95 0l-1.72 1.71"/><path d="m5.17 11.75-1.71 1.71a5.004 5.004 0 0 0 .12 7.07 5.006 5.006 0 0 0 6.95 0l1.71-1.71"/><line x1="8" x2="8" y1="2" y2="5"/><line x1="2" x2="5" y1="8" y2="8"/><line x1="16" x2="16" y1="19" y2="22"/><line x1="19" x2="22" y1="16" y2="16"/></svg>
|
||||||
|
After Width: | Height: | Size: 561 B |
1
packages/assets/icons/unplug.svg
Normal file
1
packages/assets/icons/unplug.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-unplug"><path d="m19 5 3-3"/><path d="m2 22 3-3"/><path d="M6.3 20.3a2.4 2.4 0 0 0 3.4 0L12 18l-6-6-2.3 2.3a2.4 2.4 0 0 0 0 3.4Z"/><path d="M7.5 13.5 10 11"/><path d="M10.5 16.5 13 14"/><path d="m12 6 6 6 2.3-2.3a2.4 2.4 0 0 0 0-3.4l-2.6-2.6a2.4 2.4 0 0 0-3.4 0Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 473 B |
@@ -111,6 +111,7 @@ import _MessageIcon from './icons/message.svg?component'
|
|||||||
import _MicrophoneIcon from './icons/microphone.svg?component'
|
import _MicrophoneIcon from './icons/microphone.svg?component'
|
||||||
import _MinimizeIcon from './icons/minimize.svg?component'
|
import _MinimizeIcon from './icons/minimize.svg?component'
|
||||||
import _MinusIcon from './icons/minus.svg?component'
|
import _MinusIcon from './icons/minus.svg?component'
|
||||||
|
import _MonitorIcon from './icons/monitor.svg?component'
|
||||||
import _MonitorSmartphoneIcon from './icons/monitor-smartphone.svg?component'
|
import _MonitorSmartphoneIcon from './icons/monitor-smartphone.svg?component'
|
||||||
import _MoonIcon from './icons/moon.svg?component'
|
import _MoonIcon from './icons/moon.svg?component'
|
||||||
import _MoreHorizontalIcon from './icons/more-horizontal.svg?component'
|
import _MoreHorizontalIcon from './icons/more-horizontal.svg?component'
|
||||||
@@ -160,6 +161,8 @@ import _RedoIcon from './icons/redo.svg?component'
|
|||||||
import _UnknownIcon from './icons/unknown.svg?component'
|
import _UnknownIcon from './icons/unknown.svg?component'
|
||||||
import _UnknownDonationIcon from './icons/unknown-donation.svg?component'
|
import _UnknownDonationIcon from './icons/unknown-donation.svg?component'
|
||||||
import _UpdatedIcon from './icons/updated.svg?component'
|
import _UpdatedIcon from './icons/updated.svg?component'
|
||||||
|
import _UnlinkIcon from './icons/unlink.svg?component'
|
||||||
|
import _UnplugIcon from './icons/unplug.svg?component'
|
||||||
import _UploadIcon from './icons/upload.svg?component'
|
import _UploadIcon from './icons/upload.svg?component'
|
||||||
import _UserIcon from './icons/user.svg?component'
|
import _UserIcon from './icons/user.svg?component'
|
||||||
import _UserPlusIcon from './icons/user-plus.svg?component'
|
import _UserPlusIcon from './icons/user-plus.svg?component'
|
||||||
@@ -302,6 +305,7 @@ export const MessageIcon = _MessageIcon
|
|||||||
export const MicrophoneIcon = _MicrophoneIcon
|
export const MicrophoneIcon = _MicrophoneIcon
|
||||||
export const MinimizeIcon = _MinimizeIcon
|
export const MinimizeIcon = _MinimizeIcon
|
||||||
export const MinusIcon = _MinusIcon
|
export const MinusIcon = _MinusIcon
|
||||||
|
export const MonitorIcon = _MonitorIcon
|
||||||
export const MonitorSmartphoneIcon = _MonitorSmartphoneIcon
|
export const MonitorSmartphoneIcon = _MonitorSmartphoneIcon
|
||||||
export const MoonIcon = _MoonIcon
|
export const MoonIcon = _MoonIcon
|
||||||
export const MoreHorizontalIcon = _MoreHorizontalIcon
|
export const MoreHorizontalIcon = _MoreHorizontalIcon
|
||||||
@@ -351,6 +355,8 @@ export const RedoIcon = _RedoIcon
|
|||||||
export const UnknownIcon = _UnknownIcon
|
export const UnknownIcon = _UnknownIcon
|
||||||
export const UnknownDonationIcon = _UnknownDonationIcon
|
export const UnknownDonationIcon = _UnknownDonationIcon
|
||||||
export const UpdatedIcon = _UpdatedIcon
|
export const UpdatedIcon = _UpdatedIcon
|
||||||
|
export const UnlinkIcon = _UnlinkIcon
|
||||||
|
export const UnplugIcon = _UnplugIcon
|
||||||
export const UploadIcon = _UploadIcon
|
export const UploadIcon = _UploadIcon
|
||||||
export const UserIcon = _UserIcon
|
export const UserIcon = _UserIcon
|
||||||
export const UserPlusIcon = _UserPlusIcon
|
export const UserPlusIcon = _UserPlusIcon
|
||||||
|
|||||||
@@ -206,10 +206,6 @@ const colorVariables = computed(() => {
|
|||||||
transition: color 0.25s ease-in-out;
|
transition: color 0.25s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover svg:first-child {
|
|
||||||
color: var(--_hover-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
&[disabled],
|
&[disabled],
|
||||||
&[disabled='true'],
|
&[disabled='true'],
|
||||||
&.disabled,
|
&.disabled,
|
||||||
@@ -225,6 +221,11 @@ const colorVariables = computed(() => {
|
|||||||
|
|
||||||
&:not([disabled]):not([disabled='true']):not(.disabled) {
|
&:not([disabled]):not([disabled='true']):not(.disabled) {
|
||||||
@apply active:scale-95 hover:brightness-125 focus-visible:brightness-125 hover:bg-[--_hover-bg] hover:text-[--_hover-text] focus-visible:bg-[--_hover-bg] focus-visible:text-[--_hover-text];
|
@apply active:scale-95 hover:brightness-125 focus-visible:brightness-125 hover:bg-[--_hover-bg] hover:text-[--_hover-text] focus-visible:bg-[--_hover-bg] focus-visible:text-[--_hover-text];
|
||||||
|
|
||||||
|
&:hover svg:first-child,
|
||||||
|
&:focus-visible svg:first-child {
|
||||||
|
color: var(--_hover-icon, var(--_hover-text));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<DropdownIcon v-else-if="collapsingToggleStyle" aria-hidden="true" />
|
<DropdownIcon v-else-if="collapsingToggleStyle" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
<!-- aria-hidden is set so screenreaders only use the <button>'s aria-label -->
|
<!-- aria-hidden is set so screenreaders only use the <button>'s aria-label -->
|
||||||
<p v-if="label" aria-hidden="true">
|
<p v-if="label" aria-hidden="true" class="checkbox-label">
|
||||||
{{ label }}
|
{{ label }}
|
||||||
</p>
|
</p>
|
||||||
<slot v-else />
|
<slot v-else />
|
||||||
@@ -138,4 +138,8 @@ function toggle() {
|
|||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.checkbox-label {
|
||||||
|
color: var(--color-base);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
if (this.items.length > 0 && this.neverEmpty) {
|
if (this.items.length > 0 && this.neverEmpty && !this.modelValue) {
|
||||||
this.selected = this.items[0]
|
this.selected = this.items[0]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -53,7 +53,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { type Ref, ref } from 'vue'
|
||||||
import Button from './Button.vue'
|
import Button from './Button.vue'
|
||||||
import PopoutMenu from './PopoutMenu.vue'
|
import PopoutMenu from './PopoutMenu.vue'
|
||||||
|
|
||||||
@@ -108,11 +108,17 @@ defineOptions({
|
|||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
const dropdown = ref(null)
|
const dropdown: Ref<InstanceType<typeof PopoutMenu> | null> = ref(null)
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
dropdown.value.hide()
|
dropdown.value?.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const open = () => {
|
||||||
|
dropdown.value?.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ open, close })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
441
packages/ui/src/components/base/TeleportDropdownMenu.vue
Normal file
441
packages/ui/src/components/base/TeleportDropdownMenu.vue
Normal file
@@ -0,0 +1,441 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
ref="dropdown"
|
||||||
|
data-pyro-dropdown
|
||||||
|
tabindex="0"
|
||||||
|
role="combobox"
|
||||||
|
:aria-expanded="dropdownVisible"
|
||||||
|
class="relative inline-block h-9 w-full max-w-80"
|
||||||
|
@focus="onFocus"
|
||||||
|
@blur="onBlur"
|
||||||
|
@mousedown.prevent
|
||||||
|
@keydown="handleKeyDown"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
data-pyro-dropdown-trigger
|
||||||
|
class="duration-50 flex h-full w-full cursor-pointer select-none items-center justify-between gap-4 rounded-xl bg-button-bg px-4 py-2 shadow-sm transition-all ease-in-out"
|
||||||
|
:class="triggerClasses"
|
||||||
|
@click="toggleDropdown"
|
||||||
|
>
|
||||||
|
<span>{{ selectedOption }}</span>
|
||||||
|
<DropdownIcon
|
||||||
|
class="transition-transform duration-200 ease-in-out"
|
||||||
|
:class="{ 'rotate-180': dropdownVisible }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Teleport to="#teleports">
|
||||||
|
<transition
|
||||||
|
enter-active-class="transition-opacity duration-200 ease-out"
|
||||||
|
enter-from-class="opacity-0"
|
||||||
|
enter-to-class="opacity-100"
|
||||||
|
leave-active-class="transition-opacity duration-200 ease-in"
|
||||||
|
leave-from-class="opacity-100"
|
||||||
|
leave-to-class="opacity-0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="dropdownVisible"
|
||||||
|
ref="optionsContainer"
|
||||||
|
data-pyro-dropdown-options
|
||||||
|
class="experimental-styles-within fixed z-50 bg-button-bg shadow-lg"
|
||||||
|
:class="{
|
||||||
|
'rounded-b-xl': !isRenderingUp,
|
||||||
|
'rounded-t-xl': isRenderingUp,
|
||||||
|
}"
|
||||||
|
:style="positionStyle"
|
||||||
|
@keydown.stop="handleDropdownKeyDown"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="overflow-y-auto"
|
||||||
|
:style="{ height: `${virtualListHeight}px` }"
|
||||||
|
data-pyro-dropdown-options-virtual-scroller
|
||||||
|
@scroll="handleScroll"
|
||||||
|
>
|
||||||
|
<div :style="{ height: `${totalHeight}px`, position: 'relative' }">
|
||||||
|
<div
|
||||||
|
v-for="item in visibleOptions"
|
||||||
|
:key="item.index"
|
||||||
|
data-pyro-dropdown-option
|
||||||
|
:style="{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
transform: `translateY(${item.index * ITEM_HEIGHT}px)`,
|
||||||
|
width: '100%',
|
||||||
|
height: `${ITEM_HEIGHT}px`,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
:ref="(el) => handleOptionRef(el as HTMLElement, item.index)"
|
||||||
|
role="option"
|
||||||
|
:tabindex="focusedOptionIndex === item.index ? 0 : -1"
|
||||||
|
class="hover:brightness-85 flex h-full cursor-pointer select-none items-center px-4 transition-colors duration-150 ease-in-out focus:border-none focus:outline-none"
|
||||||
|
:class="{
|
||||||
|
'bg-brand font-bold text-brand-inverted': selectedValue === item.option,
|
||||||
|
'bg-bg-raised': focusedOptionIndex === item.index,
|
||||||
|
}"
|
||||||
|
:aria-selected="selectedValue === item.option"
|
||||||
|
@click="selectOption(item.option, item.index)"
|
||||||
|
@mouseover="focusedOptionIndex = item.index"
|
||||||
|
@focus="focusedOptionIndex = item.index"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
:id="`${name}-${item.index}`"
|
||||||
|
v-model="radioValue"
|
||||||
|
type="radio"
|
||||||
|
:value="item.option"
|
||||||
|
:name="name"
|
||||||
|
class="hidden"
|
||||||
|
/>
|
||||||
|
<label :for="`${name}-${item.index}`" class="w-full cursor-pointer">
|
||||||
|
{{ displayName(item.option) }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</Teleport>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts" generic="OptionValue extends string | number | Record<string, any>">
|
||||||
|
import { DropdownIcon } from '@modrinth/assets'
|
||||||
|
import { computed, ref, watch, onMounted, onUnmounted, nextTick } from 'vue'
|
||||||
|
import type { CSSProperties } from 'vue'
|
||||||
|
|
||||||
|
const ITEM_HEIGHT = 44
|
||||||
|
const BUFFER_ITEMS = 5
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
options: OptionValue[]
|
||||||
|
name: string
|
||||||
|
defaultValue?: OptionValue | null
|
||||||
|
placeholder?: string | number | null
|
||||||
|
modelValue?: OptionValue | null
|
||||||
|
renderUp?: boolean
|
||||||
|
disabled?: boolean
|
||||||
|
displayName?: (option: OptionValue) => string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
defaultValue: null,
|
||||||
|
placeholder: null,
|
||||||
|
modelValue: null,
|
||||||
|
renderUp: false,
|
||||||
|
disabled: false,
|
||||||
|
displayName: (option: OptionValue) => String(option),
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'input' | 'update:modelValue', value: OptionValue): void
|
||||||
|
(e: 'change', value: { option: OptionValue; index: number }): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const dropdownVisible = ref(false)
|
||||||
|
const selectedValue = ref<OptionValue | null>(props.modelValue || props.defaultValue)
|
||||||
|
const focusedOptionIndex = ref<number | null>(null)
|
||||||
|
const focusedOptionRef = ref<HTMLElement | null>(null)
|
||||||
|
const dropdown = ref<HTMLElement | null>(null)
|
||||||
|
const optionsContainer = ref<HTMLElement | null>(null)
|
||||||
|
const scrollTop = ref(0)
|
||||||
|
const isRenderingUp = ref(false)
|
||||||
|
const virtualListHeight = ref(300)
|
||||||
|
const lastFocusedElement = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
|
const positionStyle = ref<CSSProperties>({
|
||||||
|
position: 'fixed',
|
||||||
|
top: '0px',
|
||||||
|
left: '0px',
|
||||||
|
width: '0px',
|
||||||
|
zIndex: 999,
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleOptionRef = (el: HTMLElement | null, index: number) => {
|
||||||
|
if (focusedOptionIndex.value === index) {
|
||||||
|
focusedOptionRef.value = el
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onFocus = async () => {
|
||||||
|
if (!props.disabled) {
|
||||||
|
focusedOptionIndex.value = props.options.findIndex((option) => option === selectedValue.value)
|
||||||
|
lastFocusedElement.value = document.activeElement as HTMLElement
|
||||||
|
dropdownVisible.value = true
|
||||||
|
await updatePosition()
|
||||||
|
nextTick(() => {
|
||||||
|
dropdown.value?.focus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onBlur = (event: FocusEvent) => {
|
||||||
|
if (!isChildOfDropdown(event.relatedTarget as HTMLElement | null)) {
|
||||||
|
closeDropdown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isChildOfDropdown = (element: HTMLElement | null): boolean => {
|
||||||
|
let currentNode: HTMLElement | null = element
|
||||||
|
while (currentNode) {
|
||||||
|
if (currentNode === dropdown.value || currentNode === optionsContainer.value) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
currentNode = currentNode.parentElement
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalHeight = computed(() => props.options.length * ITEM_HEIGHT)
|
||||||
|
|
||||||
|
const visibleOptions = computed(() => {
|
||||||
|
const startIndex = Math.floor(scrollTop.value / ITEM_HEIGHT) - BUFFER_ITEMS
|
||||||
|
const visibleCount = Math.ceil(virtualListHeight.value / ITEM_HEIGHT) + 2 * BUFFER_ITEMS
|
||||||
|
|
||||||
|
return Array.from({ length: visibleCount }, (_, i) => {
|
||||||
|
const index = startIndex + i
|
||||||
|
if (index >= 0 && index < props.options.length) {
|
||||||
|
return {
|
||||||
|
index,
|
||||||
|
option: props.options[index],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}).filter((item): item is { index: number; option: OptionValue } => item !== null)
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectedOption = computed(() => {
|
||||||
|
if (selectedValue.value !== null && selectedValue.value !== undefined) {
|
||||||
|
return props.displayName(selectedValue.value as OptionValue)
|
||||||
|
}
|
||||||
|
return props.placeholder || 'Select an option'
|
||||||
|
})
|
||||||
|
|
||||||
|
const radioValue = computed<OptionValue>({
|
||||||
|
get() {
|
||||||
|
return props.modelValue ?? selectedValue.value ?? ''
|
||||||
|
},
|
||||||
|
set(newValue: OptionValue) {
|
||||||
|
emit('update:modelValue', newValue)
|
||||||
|
selectedValue.value = newValue
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const triggerClasses = computed(() => ({
|
||||||
|
'cursor-not-allowed opacity-50 grayscale': props.disabled,
|
||||||
|
'rounded-b-none': dropdownVisible.value && !isRenderingUp.value && !props.disabled,
|
||||||
|
'rounded-t-none': dropdownVisible.value && isRenderingUp.value && !props.disabled,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const updatePosition = async () => {
|
||||||
|
if (!dropdown.value) return
|
||||||
|
|
||||||
|
await nextTick()
|
||||||
|
const triggerRect = dropdown.value.getBoundingClientRect()
|
||||||
|
const viewportHeight = window.innerHeight
|
||||||
|
const margin = 8
|
||||||
|
|
||||||
|
const contentHeight = props.options.length * ITEM_HEIGHT
|
||||||
|
const preferredHeight = Math.min(contentHeight, 300)
|
||||||
|
|
||||||
|
const spaceBelow = viewportHeight - triggerRect.bottom
|
||||||
|
const spaceAbove = triggerRect.top
|
||||||
|
|
||||||
|
isRenderingUp.value = spaceBelow < preferredHeight && spaceAbove > spaceBelow
|
||||||
|
|
||||||
|
virtualListHeight.value = isRenderingUp.value
|
||||||
|
? Math.min(spaceAbove - margin, preferredHeight)
|
||||||
|
: Math.min(spaceBelow - margin, preferredHeight)
|
||||||
|
|
||||||
|
positionStyle.value = {
|
||||||
|
position: 'fixed',
|
||||||
|
left: `${triggerRect.left}px`,
|
||||||
|
width: `${triggerRect.width}px`,
|
||||||
|
zIndex: 999,
|
||||||
|
...(isRenderingUp.value
|
||||||
|
? { bottom: `${viewportHeight - triggerRect.top}px`, top: 'auto' }
|
||||||
|
: { top: `${triggerRect.bottom}px`, bottom: 'auto' }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openDropdown = async () => {
|
||||||
|
if (!props.disabled) {
|
||||||
|
closeAllDropdowns()
|
||||||
|
dropdownVisible.value = true
|
||||||
|
focusedOptionIndex.value = props.options.findIndex((option) => option === selectedValue.value)
|
||||||
|
lastFocusedElement.value = document.activeElement as HTMLElement
|
||||||
|
await updatePosition()
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
updatePosition()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleDropdown = () => {
|
||||||
|
if (!props.disabled) {
|
||||||
|
if (dropdownVisible.value) {
|
||||||
|
closeDropdown()
|
||||||
|
} else {
|
||||||
|
openDropdown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleResize = () => {
|
||||||
|
if (dropdownVisible.value) {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
updatePosition()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleScroll = (event: Event) => {
|
||||||
|
const target = event.target as HTMLElement
|
||||||
|
scrollTop.value = target.scrollTop
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (!dropdownVisible.value) {
|
||||||
|
if (event.key === 'Enter' || event.key === ' ') {
|
||||||
|
event.preventDefault()
|
||||||
|
lastFocusedElement.value = document.activeElement as HTMLElement
|
||||||
|
toggleDropdown()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
handleDropdownKeyDown(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDropdownKeyDown = (event: KeyboardEvent) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
|
||||||
|
switch (event.key) {
|
||||||
|
case 'ArrowDown':
|
||||||
|
event.preventDefault()
|
||||||
|
focusNextOption()
|
||||||
|
break
|
||||||
|
case 'ArrowUp':
|
||||||
|
event.preventDefault()
|
||||||
|
focusPreviousOption()
|
||||||
|
break
|
||||||
|
case 'Enter':
|
||||||
|
event.preventDefault()
|
||||||
|
if (focusedOptionIndex.value !== null) {
|
||||||
|
selectOption(props.options[focusedOptionIndex.value], focusedOptionIndex.value)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'Escape':
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
closeDropdown()
|
||||||
|
break
|
||||||
|
case 'Tab':
|
||||||
|
event.preventDefault()
|
||||||
|
if (event.shiftKey) {
|
||||||
|
focusPreviousOption()
|
||||||
|
} else {
|
||||||
|
focusNextOption()
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeDropdown = () => {
|
||||||
|
dropdownVisible.value = false
|
||||||
|
focusedOptionIndex.value = null
|
||||||
|
if (lastFocusedElement.value) {
|
||||||
|
lastFocusedElement.value.focus()
|
||||||
|
lastFocusedElement.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeAllDropdowns = () => {
|
||||||
|
const event = new CustomEvent('close-all-dropdowns')
|
||||||
|
window.dispatchEvent(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectOption = (option: OptionValue, index: number) => {
|
||||||
|
radioValue.value = option
|
||||||
|
emit('change', { option, index })
|
||||||
|
closeDropdown()
|
||||||
|
}
|
||||||
|
|
||||||
|
const focusNextOption = () => {
|
||||||
|
if (focusedOptionIndex.value === null) {
|
||||||
|
focusedOptionIndex.value = 0
|
||||||
|
} else {
|
||||||
|
focusedOptionIndex.value = (focusedOptionIndex.value + 1) % props.options.length
|
||||||
|
}
|
||||||
|
scrollToFocused()
|
||||||
|
nextTick(() => {
|
||||||
|
focusedOptionRef.value?.focus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const focusPreviousOption = () => {
|
||||||
|
if (focusedOptionIndex.value === null) {
|
||||||
|
focusedOptionIndex.value = props.options.length - 1
|
||||||
|
} else {
|
||||||
|
focusedOptionIndex.value =
|
||||||
|
(focusedOptionIndex.value - 1 + props.options.length) % props.options.length
|
||||||
|
}
|
||||||
|
scrollToFocused()
|
||||||
|
nextTick(() => {
|
||||||
|
focusedOptionRef.value?.focus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const scrollToFocused = () => {
|
||||||
|
if (focusedOptionIndex.value === null) return
|
||||||
|
|
||||||
|
const optionsElement = optionsContainer.value?.querySelector('.overflow-y-auto')
|
||||||
|
if (!optionsElement) return
|
||||||
|
|
||||||
|
const targetScrollTop = focusedOptionIndex.value * ITEM_HEIGHT
|
||||||
|
const scrollBottom = optionsElement.clientHeight
|
||||||
|
|
||||||
|
if (targetScrollTop < optionsElement.scrollTop) {
|
||||||
|
optionsElement.scrollTop = targetScrollTop
|
||||||
|
} else if (targetScrollTop + ITEM_HEIGHT > optionsElement.scrollTop + scrollBottom) {
|
||||||
|
optionsElement.scrollTop = targetScrollTop - scrollBottom + ITEM_HEIGHT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('resize', handleResize)
|
||||||
|
window.addEventListener('scroll', handleResize, true)
|
||||||
|
window.addEventListener('click', (event) => {
|
||||||
|
if (!isChildOfDropdown(event.target as HTMLElement)) {
|
||||||
|
closeDropdown()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
window.addEventListener('close-all-dropdowns', closeDropdown)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('resize', handleResize)
|
||||||
|
window.removeEventListener('scroll', handleResize, true)
|
||||||
|
window.removeEventListener('click', (event) => {
|
||||||
|
if (!isChildOfDropdown(event.target as HTMLElement)) {
|
||||||
|
closeDropdown()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
window.removeEventListener('close-all-dropdowns', closeDropdown)
|
||||||
|
lastFocusedElement.value = null
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(newValue) => {
|
||||||
|
selectedValue.value = newValue
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(dropdownVisible, async (newValue) => {
|
||||||
|
if (newValue) {
|
||||||
|
await updatePosition()
|
||||||
|
scrollTop.value = 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -29,6 +29,8 @@ export { default as ScrollablePanel } from './base/ScrollablePanel.vue'
|
|||||||
export { default as SimpleBadge } from './base/SimpleBadge.vue'
|
export { default as SimpleBadge } from './base/SimpleBadge.vue'
|
||||||
export { default as Slider } from './base/Slider.vue'
|
export { default as Slider } from './base/Slider.vue'
|
||||||
export { default as StatItem } from './base/StatItem.vue'
|
export { default as StatItem } from './base/StatItem.vue'
|
||||||
|
export { default as TagItem } from './base/TagItem.vue'
|
||||||
|
export { default as TeleportDropdownMenu } from './base/TeleportDropdownMenu.vue'
|
||||||
export { default as Toggle } from './base/Toggle.vue'
|
export { default as Toggle } from './base/Toggle.vue'
|
||||||
|
|
||||||
// Branding
|
// Branding
|
||||||
@@ -47,6 +49,7 @@ export { default as NewModal } from './modal/NewModal.vue'
|
|||||||
export { default as Modal } from './modal/Modal.vue'
|
export { default as Modal } from './modal/Modal.vue'
|
||||||
export { default as ConfirmModal } from './modal/ConfirmModal.vue'
|
export { default as ConfirmModal } from './modal/ConfirmModal.vue'
|
||||||
export { default as ShareModal } from './modal/ShareModal.vue'
|
export { default as ShareModal } from './modal/ShareModal.vue'
|
||||||
|
export { default as TabbedModal } from './modal/TabbedModal.vue'
|
||||||
|
|
||||||
// Navigation
|
// Navigation
|
||||||
export { default as Breadcrumbs } from './nav/Breadcrumbs.vue'
|
export { default as Breadcrumbs } from './nav/Breadcrumbs.vue'
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<NewModal ref="modal" :noblur="noblur" danger :on-hide="onHide">
|
<NewModal ref="modal" :noblur="noblur" :danger="danger" :on-hide="onHide">
|
||||||
<template #title>
|
<template #title>
|
||||||
<slot name="title">
|
<slot name="title">
|
||||||
<span class="font-extrabold text-contrast text-lg">{{ title }}</span>
|
<span class="font-extrabold text-contrast text-lg">{{ title }}</span>
|
||||||
</slot>
|
</slot>
|
||||||
</template>
|
</template>
|
||||||
<div>
|
<div>
|
||||||
<div class="markdown-body" v-html="renderString(description)" />
|
<div class="markdown-body max-w-[35rem]" v-html="renderString(description)" />
|
||||||
<label v-if="hasToType" for="confirmation" class="confirmation-label">
|
<label v-if="hasToType" for="confirmation" class="confirmation-label">
|
||||||
<span>
|
<span>
|
||||||
<strong>To verify, type</strong>
|
<strong>To verify, type</strong>
|
||||||
@@ -25,9 +25,9 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2 mt-6">
|
<div class="flex gap-2 mt-6">
|
||||||
<ButtonStyled color="red">
|
<ButtonStyled :color="danger ? 'red' : 'brand'">
|
||||||
<button :disabled="action_disabled" @click="proceed">
|
<button :disabled="action_disabled" @click="proceed">
|
||||||
<TrashIcon />
|
<component :is="proceedIcon" />
|
||||||
{{ proceedLabel }}
|
{{ proceedLabel }}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
@@ -68,6 +68,10 @@ const props = defineProps({
|
|||||||
default: 'No description defined',
|
default: 'No description defined',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
proceedIcon: {
|
||||||
|
type: Object,
|
||||||
|
default: TrashIcon,
|
||||||
|
},
|
||||||
proceedLabel: {
|
proceedLabel: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'Proceed',
|
default: 'Proceed',
|
||||||
@@ -76,6 +80,10 @@ const props = defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
danger: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
onHide: {
|
onHide: {
|
||||||
type: Function,
|
type: Function,
|
||||||
default() {
|
default() {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
<div class="modal-container experimental-styles-within" :class="{ shown: visible }">
|
<div class="modal-container experimental-styles-within" :class="{ shown: visible }">
|
||||||
<div class="modal-body flex flex-col bg-bg-raised rounded-2xl">
|
<div class="modal-body flex flex-col bg-bg-raised rounded-2xl">
|
||||||
<div
|
<div
|
||||||
|
data-tauri-drag-region
|
||||||
class="grid grid-cols-[auto_min-content] items-center gap-12 p-6 border-solid border-0 border-b-[1px] border-divider max-w-full"
|
class="grid grid-cols-[auto_min-content] items-center gap-12 p-6 border-solid border-0 border-b-[1px] border-divider max-w-full"
|
||||||
>
|
>
|
||||||
<div class="flex text-wrap break-words items-center gap-3 min-w-0">
|
<div class="flex text-wrap break-words items-center gap-3 min-w-0">
|
||||||
|
|||||||
47
packages/ui/src/components/modal/TabbedModal.vue
Normal file
47
packages/ui/src/components/modal/TabbedModal.vue
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { type Component, ref } from 'vue'
|
||||||
|
import { useVIntl, type MessageDescriptor } from '@vintl/vintl'
|
||||||
|
|
||||||
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
|
type Tab<Props> = {
|
||||||
|
name: MessageDescriptor
|
||||||
|
icon: Component
|
||||||
|
content: Component<Props>
|
||||||
|
props?: Props
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
tabs: Tab<unknown>[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const selectedTab = ref(0)
|
||||||
|
|
||||||
|
function setTab(index: number) {
|
||||||
|
selectedTab.value = index
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ selectedTab, setTab })
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="grid grid-cols-[auto_1fr]">
|
||||||
|
<div
|
||||||
|
class="flex flex-col gap-1 border-solid pr-4 border-0 border-r-[1px] border-divider min-w-[200px]"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-for="(tab, index) in tabs"
|
||||||
|
:key="index"
|
||||||
|
:class="`flex gap-2 items-center text-left rounded-xl px-4 py-2 border-none text-nowrap font-semibold cursor-pointer active:scale-[0.97] transition-all ${selectedTab === index ? 'bg-button-bgSelected text-button-textSelected' : 'bg-transparent text-button-text hover:bg-button-bg hover:text-contrast'}`"
|
||||||
|
@click="() => (selectedTab = index)"
|
||||||
|
>
|
||||||
|
<component :is="tab.icon" class="w-4 h-4" />
|
||||||
|
<span>{{ formatMessage(tab.name) }}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<slot name="footer" />
|
||||||
|
</div>
|
||||||
|
<div class="w-[600px] h-[500px] overflow-y-auto pl-4">
|
||||||
|
<component :is="tabs[selectedTab].content" v-bind="tabs[selectedTab].props ?? {}" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -101,7 +101,7 @@ const colorTheme = defineMessages({
|
|||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.theme-options {
|
.theme-options {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(12.5rem, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
|
||||||
gap: var(--gap-lg);
|
gap: var(--gap-lg);
|
||||||
|
|
||||||
.preview .example-card {
|
.preview .example-card {
|
||||||
|
|||||||
Reference in New Issue
Block a user