2024-12-17 23:06:01 -08:00
< script setup lang = "ts" >
2025-08-13 21:48:52 +01:00
import { CopyIcon , EditIcon , PlusIcon , SpinnerIcon , TrashIcon , UploadIcon } from '@modrinth/assets'
import {
2025-08-14 21:48:38 +01:00
Avatar ,
ButtonStyled ,
Checkbox ,
injectNotificationManager ,
OverflowMenu ,
2025-08-13 21:48:52 +01:00
} from '@modrinth/ui'
import { convertFileSrc } from '@tauri-apps/api/core'
2024-12-17 23:06:01 -08:00
import { open } from '@tauri-apps/plugin-dialog'
import { defineMessages , useVIntl } from '@vintl/vintl'
2025-08-14 21:48:38 +01:00
import { computed , type Ref , ref , watch } from 'vue'
2024-12-17 23:06:01 -08:00
import { useRouter } from 'vue-router'
2025-08-14 21:48:38 +01:00
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
import { trackEvent } from '@/helpers/analytics'
import { duplicate , edit , edit _icon , list , remove } from '@/helpers/profile'
2025-08-13 21:48:52 +01:00
import type { GameInstance , InstanceSettingsTabProps } from '../../../helpers/types'
2024-12-17 23:06:01 -08:00
2025-08-13 21:48:52 +01:00
const { handleError } = injectNotificationManager ( )
2024-12-17 23:06:01 -08:00
const { formatMessage } = useVIntl ( )
const router = useRouter ( )
const deleteConfirmModal = ref ( )
2024-12-18 15:09:16 -08:00
const props = defineProps < InstanceSettingsTabProps > ( )
2024-12-17 23:06:01 -08:00
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 ( ) {
2025-08-14 21:48:38 +01:00
await duplicate ( props . instance . path ) . catch ( handleError )
trackEvent ( 'InstanceDuplicate' , {
loader : props . instance . loader ,
game _version : props . instance . game _version ,
} )
2024-12-17 23:06:01 -08:00
}
const allInstances = ref ( ( await list ( ) ) as GameInstance [ ] )
const availableGroups = computed ( ( ) => [
2025-08-14 21:48:38 +01:00
... new Set ( [ ... allInstances . value . flatMap ( ( instance ) => instance . groups ) , ... groups . value ] ) ,
2024-12-17 23:06:01 -08:00
] )
async function resetIcon ( ) {
2025-08-14 21:48:38 +01:00
icon . value = undefined
await edit _icon ( props . instance . path , null ) . catch ( handleError )
trackEvent ( 'InstanceRemoveIcon' )
2024-12-17 23:06:01 -08:00
}
async function setIcon ( ) {
2025-08-14 21:48:38 +01:00
const value = await open ( {
multiple : false ,
filters : [
{
name : 'Image' ,
extensions : [ 'png' , 'jpeg' , 'svg' , 'webp' , 'gif' , 'jpg' ] ,
} ,
] ,
} )
2024-12-17 23:06:01 -08:00
2025-08-14 21:48:38 +01:00
if ( ! value ) return
2024-12-17 23:06:01 -08:00
2025-08-14 21:48:38 +01:00
icon . value = value
await edit _icon ( props . instance . path , icon . value ) . catch ( handleError )
2024-12-17 23:06:01 -08:00
2025-08-14 21:48:38 +01:00
trackEvent ( 'InstanceSetIcon' )
2024-12-17 23:06:01 -08:00
}
const editProfileObject = computed ( ( ) => ( {
2025-08-14 21:48:38 +01:00
name : title . value . trim ( ) . substring ( 0 , 32 ) ? ? 'Instance' ,
groups : groups . value . map ( ( x ) => x . trim ( ) . substring ( 0 , 32 ) ) . filter ( ( x ) => x . length > 0 ) ,
2024-12-17 23:06:01 -08:00
} ) )
const toggleGroup = ( group : string ) => {
2025-08-14 21:48:38 +01:00
if ( groups . value . includes ( group ) ) {
groups . value = groups . value . filter ( ( x ) => x !== group )
} else {
groups . value . push ( group )
}
2024-12-17 23:06:01 -08:00
}
const addCategory = ( ) => {
2025-08-14 21:48:38 +01:00
const text = newCategoryInput . value . trim ( )
2024-12-17 23:06:01 -08:00
2025-08-14 21:48:38 +01:00
if ( text . length > 0 ) {
groups . value . push ( text . substring ( 0 , 32 ) )
newCategoryInput . value = ''
}
2024-12-17 23:06:01 -08:00
}
watch (
2025-08-14 21:48:38 +01:00
[ title , groups , groups ] ,
async ( ) => {
await edit ( props . instance . path , editProfileObject . value )
} ,
{ deep : true } ,
2024-12-17 23:06:01 -08:00
)
const removing = ref ( false )
async function removeProfile ( ) {
2025-08-14 21:48:38 +01:00
removing . value = true
await remove ( props . instance . path ) . catch ( handleError )
removing . value = false
2024-12-17 23:06:01 -08:00
2025-08-14 21:48:38 +01:00
trackEvent ( 'InstanceRemove' , {
loader : props . instance . loader ,
game _version : props . instance . game _version ,
} )
2024-12-17 23:06:01 -08:00
2025-08-14 21:48:38 +01:00
await router . push ( { path : '/' } )
2024-12-17 23:06:01 -08:00
}
const messages = defineMessages ( {
2025-08-14 21:48:38 +01:00
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...' ,
} ,
2024-12-17 23:06:01 -08:00
} )
< / script >
< template >
2025-08-14 21:48:38 +01:00
< 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"
: show - ad - on - close = "false"
@ 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"
: tint - by = "props.instance.path"
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 >
2024-12-17 23:06:01 -08:00
< / template >
< style scoped lang = "scss" >
. hovering - icon - shadow {
2025-08-14 21:48:38 +01:00
box - shadow : var ( -- shadow - inset - sm ) , var ( -- shadow - raised ) ;
2024-12-17 23:06:01 -08:00
}
< / style >