Analytics + more bug fixes (#144)

* Analytics + more bug fixes

* debug deadlock

* Fix mostly everything

* merge fixes

* fix rest

* final fixeS
This commit is contained in:
Geometrically
2023-06-19 14:59:06 -07:00
committed by GitHub
parent 84d731b670
commit 1e78a7b6a8
51 changed files with 1285 additions and 491 deletions

View File

@@ -12,10 +12,16 @@ import {
Card,
DropdownSelect,
SearchIcon,
XIcon,
Button,
formatCategoryHeader,
ModalConfirm,
} from 'omorphia'
import ContextMenu from '@/components/ui/ContextMenu.vue'
import dayjs from 'dayjs'
import { useTheming } from '@/store/theme.js'
import { remove } from '@/helpers/profile.js'
import { handleError } from '@/store/notifications.js'
const props = defineProps({
instances: {
@@ -32,6 +38,19 @@ const props = defineProps({
const instanceOptions = ref(null)
const instanceComponents = ref(null)
const themeStore = useTheming()
const currentDeleteInstance = ref(null)
const confirmModal = ref(null)
async function deleteProfile() {
if (currentDeleteInstance.value) {
instanceComponents.value = instanceComponents.value.filter(
(x) => x.instance.path !== currentDeleteInstance.value
)
await remove(currentDeleteInstance.value).catch(handleError)
}
}
const handleRightClick = (event, item) => {
const baseOptions = [
{ name: 'add_content' },
@@ -68,13 +87,12 @@ const handleRightClick = (event, item) => {
}
const handleOptionsClick = async (args) => {
console.log(args)
switch (args.option) {
case 'play':
args.item.play()
args.item.play(null, 'InstanceGridContextMenu')
break
case 'stop':
args.item.stop()
args.item.stop(null, 'InstanceGridContextMenu')
break
case 'add_content':
await args.item.addContent()
@@ -82,15 +100,16 @@ const handleOptionsClick = async (args) => {
case 'edit':
await args.item.seeInstance()
break
case 'delete':
await args.item.deleteInstance()
break
case 'open':
await args.item.openFolder()
break
case 'copy':
await navigator.clipboard.writeText(args.item.instance.path)
break
case 'delete':
currentDeleteInstance.value = args.item.instance.path
confirmModal.value.show()
break
}
}
@@ -185,6 +204,15 @@ const filteredResults = computed(() => {
})
</script>
<template>
<ModalConfirm
ref="confirmModal"
title="Are you sure you want to delete this instance?"
description="If you proceed, all data for your instance will be removed. You will not be able to recover it."
:has-to-type="false"
proceed-label="Delete"
:noblur="!themeStore.advancedRendering"
@proceed="deleteProfile"
/>
<Card class="header">
<div class="iconified-input">
<SearchIcon />
@@ -222,7 +250,7 @@ const filteredResults = computed(() => {
</div>
</Card>
<div
v-for="(instanceSection, index) in Array.from(filteredResults, ([key, value]) => ({
v-for="instanceSection in Array.from(filteredResults, ([key, value]) => ({
key,
value,
}))"
@@ -235,7 +263,7 @@ const filteredResults = computed(() => {
</div>
<section class="instances">
<Instance
v-for="instance in instanceSection.value"
v-for="(instance, index) in instanceSection.value"
ref="instanceComponents"
:key="instance.id"
:instance="instance"
@@ -298,6 +326,10 @@ const filteredResults = computed(() => {
.iconified-input {
flex-grow: 1;
input {
min-width: 100%;
}
}
.sort-dropdown {

View File

@@ -12,10 +12,14 @@ import {
StopCircleIcon,
ExternalIcon,
EyeIcon,
ModalConfirm,
} from 'omorphia'
import Instance from '@/components/ui/Instance.vue'
import { onMounted, onUnmounted, ref } from 'vue'
import ContextMenu from '@/components/ui/ContextMenu.vue'
import { remove } from '@/helpers/profile.js'
import { handleError } from '@/store/notifications.js'
import { useTheming } from '@/store/state.js'
const props = defineProps({
instances: {
@@ -36,6 +40,19 @@ const modsRow = ref(null)
const instanceOptions = ref(null)
const instanceComponents = ref(null)
const themeStore = useTheming()
const currentDeleteInstance = ref(null)
const confirmModal = ref(null)
async function deleteProfile() {
if (currentDeleteInstance.value) {
instanceComponents.value = instanceComponents.value.filter(
(x) => x.instance.path !== currentDeleteInstance.value
)
await remove(currentDeleteInstance.value).catch(handleError)
}
}
const handlePaginationDisplay = () => {
for (let i = 0; i < props.instances.length; i++) {
let parentsRow = modsRow.value[i]
@@ -114,10 +131,10 @@ const handleInstanceRightClick = (event, passedInstance) => {
const handleOptionsClick = async (args) => {
switch (args.option) {
case 'play':
await args.item.play()
await args.item.play(null, 'InstanceRowContextMenu')
break
case 'stop':
await args.item.stop()
await args.item.stop(null, 'InstanceRowContextMenu')
break
case 'add_content':
await args.item.addContent()
@@ -126,7 +143,8 @@ const handleOptionsClick = async (args) => {
await args.item.seeInstance()
break
case 'delete':
await args.item.deleteInstance()
currentDeleteInstance.value = args.item.instance.path
confirmModal.value.show()
break
case 'open_folder':
await args.item.openFolder()
@@ -165,6 +183,15 @@ const getInstanceIndex = (rowIndex, index) => {
</script>
<template>
<ModalConfirm
ref="confirmModal"
title="Are you sure you want to delete this instance?"
description="If you proceed, all data for your instance will be removed. You will not be able to recover it."
:has-to-type="false"
proceed-label="Delete"
:noblur="!themeStore.advancedRendering"
@proceed="deleteProfile"
/>
<div class="content">
<div v-for="(row, rowIndex) in instances" :key="row.label" class="row">
<div class="header">

View File

@@ -65,6 +65,7 @@ import {
import { get, set } from '@/helpers/settings'
import { WebviewWindow } from '@tauri-apps/api/window'
import { handleError } from '@/store/state.js'
import mixpanel from 'mixpanel-browser'
defineProps({
expanded: {
@@ -131,6 +132,7 @@ const login = async () => {
await setAccount(loggedIn)
await refreshValues()
await window.close()
mixpanel.track('AccountLogIn')
}
const logout = async (id) => {
@@ -140,6 +142,7 @@ const logout = async (id) => {
await setAccount(accounts.value[0])
await refreshValues()
}
mixpanel.track('AccountLogOut')
}
const toggle = () => {

View File

@@ -1,5 +1,9 @@
<template>
<Modal ref="incompatibleModal" header="Incompatibility warning">
<Modal
ref="incompatibleModal"
header="Incompatibility warning"
:noblur="!themeStore.advancedRendering"
>
<div class="modal-body">
<p>
This {{ versions?.length > 0 ? 'project' : 'version' }} is not compatible with the instance
@@ -54,9 +58,14 @@
import { Button, Modal, XIcon, DownloadIcon, DropdownSelect, formatCategory } from 'omorphia'
import { add_project_from_version as installMod } from '@/helpers/profile'
import { defineExpose, ref } from 'vue'
import { handleError } from '@/store/state.js'
import { handleError, useTheming } from '@/store/state.js'
import mixpanel from 'mixpanel-browser'
const themeStore = useTheming()
const instance = ref(null)
const project = ref(null)
const projectType = ref(null)
const projectTitle = ref(null)
const versions = ref(null)
const selectedVersion = ref(null)
@@ -66,13 +75,26 @@ const installing = ref(false)
let markInstalled = () => {}
defineExpose({
show: (instanceVal, projectTitleVal, selectedVersions, extMarkInstalled) => {
show: (
instanceVal,
projectTitleVal,
selectedVersions,
extMarkInstalled,
projectIdVal,
projectTypeVal
) => {
instance.value = instanceVal
projectTitle.value = projectTitleVal
versions.value = selectedVersions
selectedVersion.value = selectedVersions[0]
project.value = projectIdVal
projectType.value = projectTypeVal
incompatibleModal.value.show()
markInstalled = extMarkInstalled
mixpanel.track('ProjectInstallStart', { source: 'ProjectIncompatibilityWarningModal' })
},
})
@@ -82,6 +104,16 @@ const install = async () => {
installing.value = false
markInstalled()
incompatibleModal.value.hide()
mixpanel.track('ProjectInstall', {
loader: instance.value.metadata.loader,
game_version: instance.value.metadata.game_version,
id: project.value,
version_id: selectedVersion.value.id,
project_type: projectType.value,
title: projectTitle.value,
source: 'ProjectIncompatibilityWarningModal',
})
}
</script>

View File

@@ -2,6 +2,10 @@
import { Button, Modal, XIcon, DownloadIcon } from 'omorphia'
import { install as pack_install } from '@/helpers/pack'
import { ref } from 'vue'
import mixpanel from 'mixpanel-browser'
import { useTheming } from '@/store/theme.js'
const themeStore = useTheming()
const version = ref('')
const title = ref('')
@@ -11,12 +15,14 @@ const confirmModal = ref(null)
const installing = ref(false)
defineExpose({
show: (id, projectId, projectTitle, projectIcon) => {
show: (id, projectIdVal, projectTitle, projectIcon) => {
version.value = id
projectId.value = projectId
projectId.value = projectIdVal
title.value = projectTitle
icon.value = projectIcon
confirmModal.value.show()
mixpanel.track('PackInstallStart')
},
})
@@ -24,11 +30,18 @@ async function install() {
installing.value = true
await pack_install(projectId.value, version.value, title.value, icon.value ? icon.value : null)
confirmModal.value.hide()
mixpanel.track('PackInstall', {
id: projectId.value,
version_id: version.value,
title: title.value,
source: 'ConfirmModal',
})
}
</script>
<template>
<Modal ref="confirmModal" header="Are you sure?">
<Modal ref="confirmModal" header="Are you sure?" :noblur="!themeStore.advancedRendering">
<div class="modal-body">
<p>You already have this modpack installed. Are you sure you want to install it again?</p>
<div class="input-group push-right">

View File

@@ -5,7 +5,7 @@ import { Card, DownloadIcon, StopCircleIcon, Avatar, AnimatedLogo, PlayIcon } fr
import { convertFileSrc } from '@tauri-apps/api/tauri'
import InstallConfirmModal from '@/components/ui/InstallConfirmModal.vue'
import { install as pack_install } from '@/helpers/pack'
import { list, remove, run } from '@/helpers/profile'
import { list, run } from '@/helpers/profile'
import {
get_all_running_profile_paths,
get_uuids_by_profile_path,
@@ -16,6 +16,7 @@ import { useFetch } from '@/helpers/fetch.js'
import { handleError } from '@/store/state.js'
import { showInFolder } from '@/helpers/utils.js'
import InstanceInstallModal from '@/components/ui/InstanceInstallModal.vue'
import mixpanel from 'mixpanel-browser'
const props = defineProps({
instance: {
@@ -91,6 +92,13 @@ const install = async (e) => {
props.instance.icon_url
).catch(handleError)
modLoading.value = false
mixpanel.track('PackInstall', {
id: props.instance.project_id,
version_id: versions[0].id,
title: props.instance.title,
source: 'InstanceCard',
})
} else
confirmModal.value.show(
props.instance.project_id,
@@ -99,21 +107,32 @@ const install = async (e) => {
props.instance.icon_url
)
} else {
modInstallModal.value.show(props.instance.project_id, versions)
modInstallModal.value.show(
props.instance.project_id,
versions,
props.instance.title,
props.instance.project_type
)
}
modLoading.value = false
}
const play = async (e) => {
const play = async (e, context) => {
e?.stopPropagation()
modLoading.value = true
uuid.value = await run(props.instance.path).catch(handleError)
modLoading.value = false
playing.value = true
mixpanel.track('InstancePlay', {
loader: props.instance.metadata.loader,
game_version: props.instance.metadata.game_version,
source: context,
})
}
const stop = async (e) => {
const stop = async (e, context) => {
e?.stopPropagation()
playing.value = false
@@ -126,11 +145,13 @@ const stop = async (e) => {
uuids.forEach(async (u) => await kill_by_uuid(u).catch(handleError))
} else await kill_by_uuid(uuid.value).catch(handleError) // If we still have the uuid, just kill it
uuid.value = null
}
mixpanel.track('InstanceStop', {
loader: props.instance.metadata.loader,
game_version: props.instance.metadata.game_version,
source: context,
})
const deleteInstance = async () => {
await remove(props.instance.path).catch(handleError)
uuid.value = null
}
const openFolder = async () => {
@@ -151,7 +172,6 @@ defineExpose({
stop,
seeInstance,
openFolder,
deleteInstance,
addContent,
instance: props.instance,
})
@@ -190,7 +210,7 @@ onUnmounted(() => unlisten())
<div
v-if="props.instance.metadata && playing === false && modLoading === false"
class="install cta button-base"
@click="play"
@click="(e) => play(e, 'InstanceCard')"
>
<PlayIcon />
</div>
@@ -200,7 +220,7 @@ onUnmounted(() => unlisten())
<div
v-else-if="playing === true"
class="stop cta button-base"
@click="stop"
@click="(e) => stop(e, 'InstanceCard')"
@mousehover="checkProcess"
>
<StopCircleIcon />

View File

@@ -1,5 +1,5 @@
<template>
<Modal ref="modal" header="Create instance">
<Modal ref="modal" header="Create instance" :noblur="!themeStore.advancedRendering">
<div class="modal-header">
<Chips v-model="creationType" :items="['custom', 'from file']" />
</div>
@@ -116,9 +116,13 @@ import {
} from '@/helpers/metadata'
import { handleError } from '@/store/notifications.js'
import Multiselect from 'vue-multiselect'
import mixpanel from 'mixpanel-browser'
import { useTheming } from '@/store/state.js'
import { listen } from '@tauri-apps/api/event'
import { install_from_file } from '@/helpers/pack.js'
const themeStore = useTheming()
const profile_name = ref('')
const game_version = ref('')
const loader = ref('vanilla')
@@ -144,6 +148,8 @@ defineExpose({
icon.value = null
display_icon.value = null
modal.value.show()
mixpanel.track('InstanceCreateStart', { source: 'CreationModal' })
},
})
@@ -195,6 +201,7 @@ const create_instance = async () => {
creating.value = true
const loader_version_value =
loader_version.value === 'other' ? specified_loader_version.value : loader_version.value
const loaderVersion = loader.value === 'vanilla' ? null : loader_version_value ?? 'stable'
modal.value.hide()
creating.value = false
@@ -206,6 +213,15 @@ const create_instance = async () => {
loader.value === 'vanilla' ? null : loader_version_value ?? 'stable',
icon.value
).catch(handleError)
mixpanel.track('InstanceCreate', {
profile_name: profile_name.value,
game_version: game_version.value,
loader: loader.value,
loader_version: loaderVersion,
has_icon: !!icon.value,
source: 'CreationModal',
})
}
const upload_icon = async () => {
@@ -253,11 +269,20 @@ const openFile = async () => {
modal.value.hide()
await install_from_file(newProject).catch(handleError)
mixpanel.track('InstanceCreate', {
source: 'CreationModalFileOpen',
})
}
listen('tauri://file-drop', async (event) => {
modal.value.hide()
await install_from_file(event.payload[0]).catch(handleError)
if (event.payload && event.payload.length > 0 && event.payload[0].endsWith('.mrpack')) {
await install_from_file(event.payload[0]).catch(handleError)
mixpanel.track('InstanceCreate', {
source: 'CreationModalFileDrop',
})
}
})
</script>

View File

@@ -12,16 +12,28 @@ import {
CheckIcon,
} from 'omorphia'
import { computed, ref } from 'vue'
import { add_project_from_version as installMod, check_installed, list } from '@/helpers/profile'
import {
add_project_from_version as installMod,
check_installed,
get,
list,
} from '@/helpers/profile'
import { tauri } from '@tauri-apps/api'
import { open } from '@tauri-apps/api/dialog'
import { convertFileSrc } from '@tauri-apps/api/tauri'
import { create } from '@/helpers/profile'
import { installVersionDependencies } from '@/helpers/utils'
import { handleError } from '@/store/notifications.js'
import mixpanel from 'mixpanel-browser'
import { useTheming } from '@/store/theme.js'
const themeStore = useTheming()
const versions = ref([])
const project = ref('')
const projectTitle = ref('')
const projectType = ref('')
const installModal = ref(null)
const searchFilter = ref('')
const showCreation = ref(false)
@@ -33,13 +45,18 @@ const gameVersion = ref(null)
const creatingInstance = ref(false)
defineExpose({
show: async (projectId, selectedVersions) => {
show: async (projectId, selectedVersions, title, type) => {
project.value = projectId
versions.value = selectedVersions
projectTitle.value = title
projectType.value = type
installModal.value.show()
searchFilter.value = ''
profiles.value = await getData()
mixpanel.track('ProjectInstallStart', { source: 'ProjectInstallModal' })
},
})
@@ -59,6 +76,16 @@ async function install(instance) {
instance.installedMod = true
instance.installing = false
mixpanel.track('ProjectInstall', {
loader: instance.metadata.loader,
game_version: instance.metadata.game_version,
id: project.value,
version_id: version.id,
project_type: projectType.value,
title: projectTitle.value,
source: 'ProjectInstallModal',
})
}
async function getData() {
@@ -88,6 +115,7 @@ async function getData() {
return filtered
}
const alreadySentCreation = ref(false)
const toggleCreation = () => {
showCreation.value = !showCreation.value
name.value = null
@@ -95,6 +123,11 @@ const toggleCreation = () => {
display_icon.value = null
gameVersion.value = null
loader.value = null
if (!alreadySentCreation.value) {
alreadySentCreation.value = false
mixpanel.track('InstanceCreateStart', { source: 'ProjectInstallModal' })
}
}
const upload_icon = async () => {
@@ -119,20 +152,46 @@ const reset_icon = () => {
const createInstance = async () => {
creatingInstance.value = true
const loader =
versions.value[0].loaders[0] !== 'forge' ||
versions.value[0].loaders[0] !== 'fabric' ||
versions.value[0].loaders[0] !== 'quilt'
? versions.value[0].loaders[0]
: 'vanilla'
const id = await create(
name.value,
versions.value[0].game_versions[0],
versions.value[0].loaders[0] !== 'forge' ||
versions.value[0].loaders[0] !== 'fabric' ||
versions.value[0].loaders[0] !== 'quilt'
? versions.value[0].loaders[0]
: 'vanilla',
loader,
'latest',
icon.value
).catch(handleError)
await installMod(id, versions.value[0].id).catch(handleError)
const instance = await get(id, true)
await installVersionDependencies(instance, versions.value)
mixpanel.track('InstanceCreate', {
profile_name: name.value,
game_version: versions.value[0].game_versions[0],
loader: loader,
loader_version: 'latest',
has_icon: !!icon.value,
source: 'ProjectInstallModal',
})
mixpanel.track('ProjectInstall', {
loader: loader,
game_version: versions.value[0].game_versions[0],
id: project.value,
version_id: versions.value[0].id,
project_type: projectType.value,
title: projectTitle.value,
source: 'ProjectInstallModal',
})
installModal.value.hide()
creatingInstance.value = false
}
@@ -143,7 +202,11 @@ const check_valid = computed(() => {
</script>
<template>
<Modal ref="installModal" header="Install project to instance">
<Modal
ref="installModal"
header="Install project to instance"
:noblur="!themeStore.advancedRendering"
>
<div class="modal-body">
<input
v-model="searchFilter"
@@ -159,7 +222,15 @@ const check_valid = computed(() => {
class="profile-button"
@click="$router.push(`/instance/${encodeURIComponent(profile.path)}`)"
>
<Avatar :src="convertFileSrc(profile.metadata.icon)" class="profile-image" />
<Avatar
:src="
!profile.metadata.icon ||
(profile.metadata.icon && profile.metadata.icon.startsWith('http'))
? profile.metadata.icon
: convertFileSrc(profile.metadata?.icon)
"
class="profile-image"
/>
{{ profile.metadata.name }}
</Button>
<Button :disabled="profile.installedMod || profile.installing" @click="install(profile)">

View File

@@ -1,5 +1,5 @@
<template>
<Modal ref="detectJavaModal" header="Select java version">
<Modal ref="detectJavaModal" header="Select java version" :noblur="!themeStore.advancedRendering">
<div class="auto-detect-modal">
<div class="table">
<div class="table-row table-head">
@@ -44,6 +44,10 @@ import {
get_all_jre,
} from '@/helpers/jre.js'
import { handleError } from '@/store/notifications.js'
import mixpanel from 'mixpanel-browser'
import { useTheming } from '@/store/theme.js'
const themeStore = useTheming()
const chosenInstallOptions = ref([])
const detectJavaModal = ref(null)
@@ -75,6 +79,10 @@ const emit = defineEmits(['submit'])
function setJavaInstall(javaInstall) {
emit('submit', javaInstall)
detectJavaModal.value.hide()
mixpanel.track('JavaAutoDetect', {
path: javaInstall.path,
version: javaInstall.version,
})
}
</script>
<style lang="scss" scoped>
@@ -83,7 +91,7 @@ function setJavaInstall(javaInstall) {
.table {
.table-row {
grid-template-columns: 1fr 4fr 1.5fr;
grid-template-columns: 1fr 4fr min-content;
}
span {

View File

@@ -52,6 +52,7 @@ import { get_jre } from '@/helpers/jre.js'
import { ref } from 'vue'
import { open } from '@tauri-apps/api/dialog'
import JavaDetectionModal from '@/components/ui/JavaDetectionModal.vue'
import mixpanel from 'mixpanel-browser'
const props = defineProps({
version: {
@@ -85,6 +86,11 @@ async function testJava() {
testingJava.value = false
testingJavaSuccess.value = !!result
mixpanel.track('JavaTest', {
path: props.modelValue ? props.modelValue.path : '',
success: !!result,
})
setTimeout(() => {
testingJavaSuccess.value = null
}, 2000)
@@ -101,6 +107,11 @@ async function handleJavaFileInput() {
version: props.version.toString(),
architecture: 'x86',
}
mixpanel.track('JavaManualSelect', {
path: filePath,
version: props.version,
})
}
emit('update:modelValue', result)

View File

@@ -113,6 +113,7 @@ import { useRouter } from 'vue-router'
import { progress_bars_list } from '@/helpers/state.js'
import ProgressBar from '@/components/ui/ProgressBar.vue'
import { handleError } from '@/store/notifications.js'
import mixpanel from 'mixpanel-browser'
const router = useRouter()
const card = ref(null)
@@ -140,6 +141,12 @@ const stop = async (path) => {
try {
const processes = await getProfileProcesses(path ?? selectedProfile.value.path)
await killProfile(processes[0])
mixpanel.track('InstanceStop', {
loader: currentProcesses.value[0].metadata.loader,
game_version: currentProcesses.value[0].metadata.game_version,
source: 'AppBar',
})
} catch (e) {
console.error(e)
}

View File

@@ -50,12 +50,7 @@
</div>
</div>
<div class="install">
<Button
:to="`/browse/${project.slug}`"
color="primary"
:disabled="installed || installing"
@click.stop="install()"
>
<Button color="primary" :disabled="installed || installing" @click.stop="install()">
<DownloadIcon v-if="!installed" />
<CheckIcon v-else />
{{ installing ? 'Installing' : installed ? 'Installed' : 'Install' }}
@@ -87,6 +82,7 @@ import { install as packInstall } from '@/helpers/pack.js'
import { installVersionDependencies } from '@/helpers/utils.js'
import { useFetch } from '@/helpers/fetch.js'
import { handleError } from '@/store/notifications.js'
import mixpanel from 'mixpanel-browser'
dayjs.extend(relativeTime)
const props = defineProps({
@@ -159,6 +155,13 @@ async function install() {
props.project.title,
props.project.icon_url
).catch(handleError)
mixpanel.track('PackInstall', {
id: props.project.project_id,
version_id: queuedVersionData.id,
title: props.project.title,
source: 'SearchCard',
})
} else {
props.confirmModal.show(
props.project.project_id,
@@ -174,16 +177,33 @@ async function install() {
props.instance,
props.project.title,
versions,
() => (installed.value = true)
() => (installed.value = true),
props.project.project_id,
props.project.project_type
)
installing.value = false
return
} else {
await installMod(props.instance.path, queuedVersionData.id).catch(handleError)
installVersionDependencies(props.instance, queuedVersionData)
await installVersionDependencies(props.instance, queuedVersionData)
mixpanel.track('ProjectInstall', {
loader: props.instance.metadata.loader,
game_version: props.instance.metadata.game_version,
id: props.project.project_id,
project_type: props.project.project_type,
version_id: queuedVersionData.id,
title: props.project.title,
source: 'SearchCard',
})
}
} else {
props.modInstallModal.show(props.project.project_id, versions)
props.modInstallModal.show(
props.project.project_id,
versions,
props.project.title,
props.project.project_type
)
installing.value = false
return
}