feat: hook up icon editing in sub menu

This commit is contained in:
tdgao
2026-08-18 22:38:20 -06:00
parent f3676e0623
commit 9cf119d65b
2 changed files with 127 additions and 2 deletions
@@ -1,20 +1,24 @@
<script setup lang="ts">
import {
ClipboardCopyIcon,
EditIcon,
EyeIcon,
FolderOpenIcon,
MinusIcon,
PaletteIcon,
PlayIcon,
PlusIcon,
StarIcon,
StopCircleIcon,
TrashIcon,
UploadIcon,
} from '@modrinth/assets'
import { defineMessages, useVIntl } from '@modrinth/ui'
import { computed, nextTick, onDeactivated, onUnmounted, ref, toRef, watch } from 'vue'
import Draggable from 'vuedraggable'
import ContextMenu from '@/components/ui/context-menu/index.vue'
import IconEditorModal from '@/components/ui/instance_settings/icon-editor-modal/index.vue'
import GroupInstancesModal from '@/components/ui/library/group-instances-modal.vue'
import InstanceGroup from '@/components/ui/library/instance-group/index.vue'
import InstanceGroupDnd from '@/components/ui/library/instance-group/instance-group-dnd.vue'
@@ -55,6 +59,30 @@ const messages = defineMessages({
id: 'app.library.instance.action.view-instance',
defaultMessage: 'View instance',
},
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',
},
createIcon: {
id: 'instance.settings.tabs.general.edit-icon.create',
defaultMessage: 'Create an icon',
},
editCreatedIcon: {
id: 'instance.settings.tabs.general.edit-icon.edit-created',
defaultMessage: 'Edit icon',
},
removeIcon: {
id: 'instance.settings.tabs.general.edit-icon.remove',
defaultMessage: 'Remove icon',
},
duplicateInstance: {
id: 'app.library.instance.action.duplicate',
defaultMessage: 'Duplicate instance',
@@ -78,10 +106,13 @@ const {
reorderGroups,
instanceOptions,
confirmDeleteModal,
iconEditorModal,
currentIconEditorInstance,
currentDeleteInstances,
clearLibraryInstanceSelection,
deleteInstance,
handleInstanceOption,
handleInstanceIconSaved,
selectedLibraryInstances,
setSelectedLibraryInstances,
toggleLibraryInstanceSelection,
@@ -254,6 +285,10 @@ function setConfirmDeleteModal(component: unknown) {
confirmDeleteModal.value = component as InstanceType<typeof ConfirmDeleteInstanceModal> | null
}
function setIconEditorModal(component: unknown) {
iconEditorModal.value = component as InstanceType<typeof IconEditorModal> | null
}
watch(selectedLibraryInstances, (selectedInstances) => {
if (selectedInstances.size === 0) {
anchorInstance.value = null
@@ -389,6 +424,12 @@ watch(selectedLibraryInstances, (selectedInstances) => {
:instances="currentDeleteInstances"
@delete="deleteInstance"
/>
<IconEditorModal
:ref="setIconEditorModal"
:instance-id="currentIconEditorInstance?.id"
:config="currentIconEditorInstance?.icon_config"
@saved="handleInstanceIconSaved"
/>
<ContextMenu :ref="setInstanceOptions" @option-clicked="handleInstanceOption">
<template #play> <PlayIcon /> {{ formatMessage(messages.play) }} </template>
<template #stop> <StopCircleIcon /> {{ formatMessage(messages.stop) }} </template>
@@ -401,6 +442,14 @@ watch(selectedLibraryInstances, (selectedInstances) => {
</template>
<template #add_content> <PlusIcon /> {{ formatMessage(messages.addContent) }} </template>
<template #edit> <EyeIcon /> {{ formatMessage(messages.viewInstance) }} </template>
<template #edit_icon> <EditIcon /> {{ formatMessage(messages.editIcon) }} </template>
<template #select_icon> <UploadIcon /> {{ formatMessage(messages.selectIcon) }} </template>
<template #replace_icon> <UploadIcon /> {{ formatMessage(messages.replaceIcon) }} </template>
<template #create_icon> <PaletteIcon /> {{ formatMessage(messages.createIcon) }} </template>
<template #edit_created_icon>
<PaletteIcon /> {{ formatMessage(messages.editCreatedIcon) }}
</template>
<template #remove_icon> <TrashIcon /> {{ formatMessage(messages.removeIcon) }} </template>
<template #duplicate>
<ClipboardCopyIcon /> {{ formatMessage(messages.duplicateInstance) }}
</template>
@@ -1,11 +1,13 @@
import type { Labrinth } from '@modrinth/api-client'
import { formatLoader, injectNotificationManager, useVIntl } from '@modrinth/ui'
import { open as openDialog } from '@tauri-apps/plugin-dialog'
import { useEventListener, useStorage } from '@vueuse/core'
import dayjs from 'dayjs'
import {
computed,
inject,
type InjectionKey,
nextTick,
provide,
type Ref,
ref,
@@ -13,10 +15,11 @@ import {
watchEffect,
} from 'vue'
import { trackEvent } from '@/helpers/analytics'
import { get_project_v3_many } from '@/helpers/cache.js'
import { toError } from '@/helpers/errors'
import { install_duplicate_instance } from '@/helpers/install'
import { edit, remove } from '@/helpers/instance'
import { edit, edit_icon, remove } from '@/helpers/instance'
import {
create_group as createInstanceGroup,
delete_group as deleteInstanceGroup,
@@ -28,7 +31,7 @@ import {
set_group_memberships as setInstanceGroupMemberships,
set_group_order as setInstanceGroupOrder,
} from '@/helpers/instance-groups'
import type { GameInstance } from '@/helpers/types'
import type { GameInstance, InstanceIconConfig } from '@/helpers/types'
export const librarySortOptions = [
'Name',
@@ -100,6 +103,10 @@ type ConfirmDeleteModal = {
show: () => void
}
type IconEditorModal = {
show: () => void
}
type ContextMenuSelection = {
option: string
item: InstanceCard
@@ -153,6 +160,12 @@ function createLibraryState(instances: Ref<GameInstance[]>) {
)
const currentContextGroupId = ref<string | null>(null)
const confirmDeleteModal = ref<ConfirmDeleteModal | null>(null)
const iconEditorModal = ref<IconEditorModal | null>(null)
const currentIconEditorInstanceId = ref<string | null>(null)
const currentIconEditorInstance = computed(
() =>
instances.value.find((instance) => instance.id === currentIconEditorInstanceId.value) ?? null,
)
const displayState = useStorage<{
group: LibraryGroupBy
@@ -1088,6 +1101,47 @@ function createLibraryState(instances: Ref<GameInstance[]>) {
await install_duplicate_instance(instanceId).catch((error) => handleError(toError(error)))
}
const selectInstanceIcon = async (item: InstanceCard) => {
const iconPath = await openDialog({
multiple: false,
filters: [
{
name: 'Image',
extensions: ['png', 'jpeg', 'svg', 'webp', 'gif', 'jpg'],
},
],
})
if (!iconPath) return
try {
await edit_icon(item.instance.id, iconPath)
trackEvent('InstanceSetIcon')
} catch (error) {
handleError(toError(error))
}
}
const removeInstanceIcon = async (item: InstanceCard) => {
try {
await edit_icon(item.instance.id, null)
trackEvent('InstanceRemoveIcon')
} catch (error) {
handleError(toError(error))
}
}
const openInstanceIconEditor = async (item: InstanceCard) => {
currentIconEditorInstanceId.value = item.instance.id
await nextTick()
iconEditorModal.value?.show()
trackEvent(item.instance.icon_config ? 'InstanceEditCreatedIcon' : 'InstanceCreateIcon')
}
const handleInstanceIconSaved = (_iconPath: string, _config: InstanceIconConfig) => {
trackEvent('InstanceSaveCreatedIcon')
}
const handleInstanceContextMenu = (
event: MouseEvent,
item: InstanceCard,
@@ -1114,6 +1168,14 @@ function createLibraryState(instances: Ref<GameInstance[]>) {
{ name: 'duplicate' },
{ name: 'open' },
{ name: 'copy' },
{
name: 'edit_icon',
children: [
{ name: item.instance.icon_path ? 'replace_icon' : 'select_icon' },
{ name: item.instance.icon_config ? 'edit_created_icon' : 'create_icon' },
...(item.instance.icon_path ? [{ name: 'remove_icon' }] : []),
],
},
...(currentContextGroupId.value
? [{ name: 'remove_from_group' }, { type: 'divider' }]
: [{ type: 'divider' }]),
@@ -1158,6 +1220,17 @@ function createLibraryState(instances: Ref<GameInstance[]>) {
case 'edit':
await item.seeInstance()
break
case 'select_icon':
case 'replace_icon':
await selectInstanceIcon(item)
break
case 'create_icon':
case 'edit_created_icon':
await openInstanceIconEditor(item)
break
case 'remove_icon':
await removeInstanceIcon(item)
break
case 'duplicate':
if (item.instance.install_stage === 'installed') {
await duplicateInstance(item.instance.id)
@@ -1220,6 +1293,8 @@ function createLibraryState(instances: Ref<GameInstance[]>) {
canCreateGroup,
instanceOptions,
confirmDeleteModal,
iconEditorModal,
currentIconEditorInstance,
currentDeleteInstances,
isSectionCollapsed,
setSectionCollapsed,
@@ -1251,6 +1326,7 @@ function createLibraryState(instances: Ref<GameInstance[]>) {
deleteInstance,
handleInstanceContextMenu,
handleInstanceOption,
handleInstanceIconSaved,
}
}