Migrate to SQLite for Internal Launcher Data (#1300)

* initial migration

* barebones profiles

* Finish profiles

* Add back file watcher

* UI support progress

* Finish most of cache

* Fix options page

* Fix forge, finish modrinth auth

* Accounts, process cache

* Run SQLX prepare

* Finish

* Run lint + actions

* Fix version to be compat with windows

* fix lint

* actually fix lint

* actually fix lint again
This commit is contained in:
Geometrically
2024-07-24 11:03:19 -07:00
committed by GitHub
parent 90f74427d9
commit 49a20a303a
156 changed files with 9208 additions and 8547 deletions

View File

@@ -10,6 +10,7 @@ import {
NavRow,
Card,
SearchFilter,
Avatar,
} from '@modrinth/ui'
import { formatCategoryHeader, formatCategory } from '@modrinth/utils'
import Multiselect from 'vue-multiselect'
@@ -17,29 +18,22 @@ import { handleError } from '@/store/state'
import { useBreadcrumbs } from '@/store/breadcrumbs'
import { get_categories, get_loaders, get_game_versions } from '@/helpers/tags'
import { useRoute, useRouter } from 'vue-router'
import { Avatar } from '@modrinth/ui'
import SearchCard from '@/components/ui/SearchCard.vue'
import InstallConfirmModal from '@/components/ui/InstallConfirmModal.vue'
import ModInstallModal from '@/components/ui/ModInstallModal.vue'
import SplashScreen from '@/components/ui/SplashScreen.vue'
import IncompatibilityWarningModal from '@/components/ui/IncompatibilityWarningModal.vue'
import { useFetch } from '@/helpers/fetch.js'
import { check_installed, get, get as getInstance } from '@/helpers/profile.js'
import { get as getInstance, get_projects as getInstanceProjects } from '@/helpers/profile.js'
import { convertFileSrc } from '@tauri-apps/api/tauri'
import { isOffline } from '@/helpers/utils'
import { offline_listener } from '@/helpers/events'
import { get_search_results } from '@/helpers/cache.js'
import { debounce } from '@/helpers/utils.js'
const router = useRouter()
const route = useRoute()
const offline = ref(await isOffline())
const unlistenOffline = await offline_listener((b) => {
offline.value = b
const offline = ref(!navigator.onLine)
window.addEventListener('offline', () => {
offline.value = true
})
window.addEventListener('online', () => {
offline.value = false
})
const confirmModal = ref(null)
const modInstallModal = ref(null)
const incompatibilityWarningModal = ref(null)
const breadcrumbs = useBreadcrumbs()
breadcrumbs.setContext({ name: 'Browse', link: route.path, query: route.query })
@@ -65,6 +59,7 @@ const maxResults = ref(20)
const currentPage = ref(1)
const projectType = ref(route.params.projectType)
const instanceContext = ref(null)
const instanceProjects = ref(null)
const ignoreInstanceLoaders = ref(false)
const ignoreInstanceGameVersions = ref(false)
@@ -88,7 +83,10 @@ if (route.query.il) {
ignoreInstanceLoaders.value = route.query.il === 'true'
}
if (route.query.i) {
instanceContext.value = await getInstance(route.query.i, true)
;[instanceContext.value, instanceProjects.value] = await Promise.all([
getInstance(route.query.i).catch(handleError),
getInstanceProjects(route.query.i).catch(handleError),
])
}
if (route.query.q) {
query.value = route.query.q
@@ -144,18 +142,16 @@ if (route.query.ai) {
}
async function refreshSearch() {
const base = 'https://api.modrinth.com/v2/'
const params = [`limit=${maxResults.value}`, `index=${sortType.value.name}`]
if (query.value.length > 0) {
params.push(`query=${query.value.replace(/ /g, '+')}`)
}
if (instanceContext.value) {
if (!ignoreInstanceLoaders.value && projectType.value === 'mod') {
orFacets.value = [`categories:${encodeURIComponent(instanceContext.value.metadata.loader)}`]
orFacets.value = [`categories:${encodeURIComponent(instanceContext.value.loader)}`]
}
if (!ignoreInstanceGameVersions.value) {
selectedVersions.value = [instanceContext.value.metadata.game_version]
selectedVersions.value = [instanceContext.value.game_version]
}
}
if (
@@ -224,13 +220,11 @@ async function refreshSearch() {
}
if (hideAlreadyInstalled.value) {
const installedMods = await get(instanceContext.value.path, false).then((x) =>
Object.values(x.projects)
.filter((x) => x.metadata.project)
.map((x) => x.metadata.project.id),
)
const installedMods = Object.values(instanceProjects.value)
.filter((x) => x.metadata)
.map((x) => x.metadata.project_id)
installedMods.map((x) => [`project_id != ${x}`]).forEach((x) => formattedFacets.push(x))
console.log(`facets=${JSON.stringify(formattedFacets)}`)
}
params.push(`facets=${JSON.stringify(formattedFacets)}`)
@@ -246,24 +240,24 @@ async function refreshSearch() {
}
}
let val = `${base}${url}`
let rawResults = await useFetch(val, 'search results', offline.value)
let rawResults = await get_search_results(`?${url}`)
if (!rawResults) {
rawResults = {
hits: [],
total_hits: 0,
limit: 1,
result: {
hits: [],
total_hits: 0,
limit: 1,
},
}
}
if (instanceContext.value) {
for (val of rawResults.hits) {
val.installed = await check_installed(instanceContext.value.path, val.project_id).then(
(x) => (val.installed = x),
for (const val of rawResults.result.hits) {
val.installed = Object.values(instanceProjects.value).some(
(x) => x.metadata && x.metadata.project_id === val.project_id,
)
}
}
results.value = rawResults
results.value = rawResults.result
}
async function onSearchChange(newPageNumber) {
@@ -282,6 +276,8 @@ async function onSearchChange(newPageNumber) {
}
}
const debouncedSearchChange = debounce(() => onSearchChange(1), 200)
const searchWrapper = ref(null)
async function onSearchChangeToTop(newPageNumber) {
await onSearchChange(newPageNumber)
@@ -505,13 +501,13 @@ const selectableProjectTypes = computed(() => {
if (instanceContext.value) {
if (
availableGameVersions.value.findIndex(
(x) => x.version === instanceContext.value.metadata.game_version,
(x) => x.version === instanceContext.value.game_version,
) <= availableGameVersions.value.findIndex((x) => x.version === '1.13')
) {
values.unshift({ label: 'Data Packs', href: `/browse/datapack` })
}
if (instanceContext.value.metadata.loader !== 'vanilla') {
if (instanceContext.value.loader !== 'vanilla') {
values.unshift({ label: 'Mods', href: '/browse/mod' })
}
} else {
@@ -528,8 +524,6 @@ const showVersions = computed(
)
const isModProject = computed(() => ['modpack', 'mod'].includes(projectType.value))
onUnmounted(() => unlistenOffline())
</script>
<template>
@@ -538,27 +532,19 @@ onUnmounted(() => unlistenOffline())
<Card v-if="instanceContext" class="small-instance">
<router-link :to="`/instance/${encodeURIComponent(instanceContext.path)}`" class="instance">
<Avatar
:src="
!instanceContext.metadata.icon ||
(instanceContext.metadata.icon && instanceContext.metadata.icon.startsWith('http'))
? instanceContext.metadata.icon
: convertFileSrc(instanceContext.metadata.icon)
"
:alt="instanceContext.metadata.name"
:src="instanceContext.icon_path ? convertFileSrc(instanceContext.icon_path) : null"
:alt="instanceContext.name"
size="sm"
/>
<div class="small-instance_info">
<span class="title">{{
instanceContext.metadata.name.length > 20
? instanceContext.metadata.name.substring(0, 20) + '...'
: instanceContext.metadata.name
instanceContext.name.length > 20
? instanceContext.name.substring(0, 20) + '...'
: instanceContext.name
}}</span>
<span>
{{
instanceContext.metadata.loader.charAt(0).toUpperCase() +
instanceContext.metadata.loader.slice(1)
}}
{{ instanceContext.metadata.game_version }}
{{ instanceContext.loader.charAt(0).toUpperCase() + instanceContext.loader.slice(1) }}
{{ instanceContext.game_version }}
</span>
</div>
</router-link>
@@ -598,7 +584,10 @@ onUnmounted(() => unlistenOffline())
>
<ClearIcon /> Clear filters
</Button>
<div v-if="isModProject || projectType === 'shader'" class="loaders">
<div
v-if="(isModProject && ignoreInstanceLoaders) || projectType === 'shader'"
class="loaders"
>
<h2>Loaders</h2>
<div v-for="loader in filteredLoaders" :key="loader">
<SearchFilter
@@ -693,9 +682,10 @@ onUnmounted(() => unlistenOffline())
<input
v-model="query"
autocomplete="off"
spellcheck="false"
type="text"
:placeholder="`Search ${projectType}s...`"
@input="onSearchChange(1)"
@input="debouncedSearchChange()"
/>
<Button class="r-btn" @click="() => clearSearch()">
<XIcon />
@@ -752,9 +742,6 @@ onUnmounted(() => unlistenOffline())
loader.supported_project_types?.includes(projectType),
),
]"
:confirm-modal="confirmModal"
:mod-install-modal="modInstallModal"
:incompatibility-warning-modal="incompatibilityWarningModal"
:installed="result.installed"
/>
</section>
@@ -768,9 +755,6 @@ onUnmounted(() => unlistenOffline())
<br />
</div>
</div>
<InstallConfirmModal ref="confirmModal" />
<ModInstallModal ref="modInstallModal" />
<IncompatibilityWarningModal ref="incompatibilityWarningModal" />
</template>
<style src="vue-multiselect/dist/vue-multiselect.css"></style>

View File

@@ -1,14 +1,13 @@
<script setup>
import { ref, onUnmounted, shallowRef, computed } from 'vue'
import { ref, onUnmounted, computed } from 'vue'
import { useRoute } from 'vue-router'
import RowDisplay from '@/components/RowDisplay.vue'
import { list } from '@/helpers/profile.js'
import { offline_listener, profile_listener } from '@/helpers/events'
import { profile_listener } from '@/helpers/events'
import { useBreadcrumbs } from '@/store/breadcrumbs'
import { useFetch } from '@/helpers/fetch.js'
import { handleError } from '@/store/notifications.js'
import dayjs from 'dayjs'
import { isOffline } from '@/helpers/utils'
import { get_search_results } from '@/helpers/cache.js'
const featuredModpacks = ref({})
const featuredMods = ref({})
@@ -19,45 +18,55 @@ const breadcrumbs = useBreadcrumbs()
breadcrumbs.setRootContext({ name: 'Home', link: route.path })
const recentInstances = shallowRef([])
const recentInstances = ref([])
const offline = ref(await isOffline())
const offline = ref(!navigator.onLine)
window.addEventListener('offline', () => {
offline.value = true
})
window.addEventListener('online', () => {
offline.value = false
})
const getInstances = async () => {
const profiles = await list(true).catch(handleError)
recentInstances.value = Object.values(profiles).sort((a, b) => {
return dayjs(b.metadata.last_played ?? 0).diff(dayjs(a.metadata.last_played ?? 0))
const profiles = await list().catch(handleError)
recentInstances.value = profiles.sort((a, b) => {
const dateA = dayjs(a.last_played ?? 0)
const dateB = dayjs(b.last_played ?? 0)
if (dateA.isSame(dateB)) {
return a.name.localeCompare(b.name)
}
return dateB - dateA
})
let filters = []
for (const instance of recentInstances.value) {
if (instance.metadata.linked_data && instance.metadata.linked_data.project_id) {
filters.push(`NOT"project_id"="${instance.metadata.linked_data.project_id}"`)
if (instance.linked_data && instance.linked_data.project_id) {
filters.push(`NOT"project_id"="${instance.linked_data.project_id}"`)
}
}
filter.value = filters.join(' AND ')
}
const getFeaturedModpacks = async () => {
const response = await useFetch(
`https://api.modrinth.com/v2/search?facets=[["project_type:modpack"]]&limit=10&index=follows&filters=${filter.value}`,
'featured modpacks',
offline.value,
const response = await get_search_results(
`?facets=[["project_type:modpack"]]&limit=10&index=follows&filters=${filter.value}`,
)
if (response) {
featuredModpacks.value = response.hits
featuredModpacks.value = response.result.hits
} else {
featuredModpacks.value = []
}
}
const getFeaturedMods = async () => {
const response = await useFetch(
'https://api.modrinth.com/v2/search?facets=[["project_type:mod"]]&limit=10&index=follows',
'featured mods',
offline.value,
)
const response = await get_search_results('?facets=[["project_type:mod"]]&limit=10&index=follows')
if (response) {
featuredMods.value = response.hits
featuredMods.value = response.result.hits
} else {
featuredModpacks.value = []
}
@@ -69,14 +78,8 @@ await Promise.all([getFeaturedModpacks(), getFeaturedMods()])
const unlistenProfile = await profile_listener(async (e) => {
await getInstances()
if (e.event === 'created' || e.event === 'removed') {
await Promise.all([getFeaturedModpacks(), getFeaturedMods()])
}
})
const unlistenOffline = await offline_listener(async (b) => {
offline.value = b
if (!b) {
if (e.event === 'added' || e.event === 'created' || e.event === 'removed') {
await Promise.all([getFeaturedModpacks(), getFeaturedMods()])
}
})
@@ -92,7 +95,6 @@ const total = computed(() => {
onUnmounted(() => {
unlistenProfile()
unlistenOffline()
})
</script>
@@ -105,6 +107,7 @@ onUnmounted(() => {
label: 'Jump back in',
route: '/library',
instances: recentInstances,
instance: true,
downloaded: true,
},
{

View File

@@ -4,34 +4,33 @@ import GridDisplay from '@/components/GridDisplay.vue'
import { list } from '@/helpers/profile.js'
import { useRoute } from 'vue-router'
import { useBreadcrumbs } from '@/store/breadcrumbs'
import { offline_listener, profile_listener } from '@/helpers/events.js'
import { profile_listener } from '@/helpers/events.js'
import { handleError } from '@/store/notifications.js'
import { Button } from '@modrinth/ui'
import { PlusIcon } from '@modrinth/assets'
import InstanceCreationModal from '@/components/ui/InstanceCreationModal.vue'
import { NewInstanceImage } from '@/assets/icons'
import { isOffline } from '@/helpers/utils'
const route = useRoute()
const breadcrumbs = useBreadcrumbs()
breadcrumbs.setRootContext({ name: 'Library', link: route.path })
const profiles = await list(true).catch(handleError)
const instances = shallowRef(Object.values(profiles))
const instances = shallowRef(await list().catch(handleError))
const offline = ref(await isOffline())
const unlistenOffline = await offline_listener((b) => {
offline.value = b
const offline = ref(!navigator.onLine)
window.addEventListener('offline', () => {
offline.value = true
})
window.addEventListener('online', () => {
offline.value = false
})
const unlistenProfile = await profile_listener(async () => {
const profiles = await list(true).catch(handleError)
instances.value = Object.values(profiles)
instances.value = await list().catch(handleError)
})
onUnmounted(() => {
unlistenProfile()
unlistenOffline()
})
</script>

View File

@@ -4,7 +4,7 @@ import { LogOutIcon, LogInIcon, BoxIcon, FolderSearchIcon, UpdatedIcon } from '@
import { Card, Slider, DropdownSelect, Toggle, Modal, Button } from '@modrinth/ui'
import { handleError, useTheming } from '@/store/state'
import { is_dir_writeable, change_config_dir, get, set } from '@/helpers/settings'
import { get_max_memory } from '@/helpers/jre'
import { get_java_versions, get_max_memory, set_java_version } from '@/helpers/jre'
import { get as getCreds, logout } from '@/helpers/mr_auth.js'
import JavaSelector from '@/components/ui/JavaSelector.vue'
import ModrinthLoginScreen from '@/components/ui/tutorial/ModrinthLoginScreen.vue'
@@ -12,6 +12,7 @@ import { mixpanel_opt_out_tracking, mixpanel_opt_in_tracking } from '@/helpers/m
import { open } from '@tauri-apps/api/dialog'
import { getOS } from '@/helpers/utils.js'
import { getVersion } from '@tauri-apps/api/app'
import { get_user } from '@/helpers/cache.js'
const pageOptions = ['Home', 'Library']
@@ -22,8 +23,8 @@ const version = await getVersion()
const accessSettings = async () => {
const settings = await get()
settings.javaArgs = settings.custom_java_args.join(' ')
settings.envArgs = settings.custom_env_args.map((x) => x.join('=')).join(' ')
settings.launchArgs = settings.extra_launch_args.join(' ')
settings.envVars = settings.custom_env_vars.map((x) => x.join('=')).join(' ')
return settings
}
@@ -31,7 +32,8 @@ const accessSettings = async () => {
const fetchSettings = await accessSettings().catch(handleError)
const settings = ref(fetchSettings)
const settingsDir = ref(settings.value.loaded_config_dir)
// const settingsDir = ref(settings.value.loaded_config_dir)
const maxMemory = ref(Math.floor((await get_max_memory().catch(handleError)) / 1024))
watch(
@@ -43,26 +45,14 @@ watch(
const setSettings = JSON.parse(JSON.stringify(newSettings))
if (setSettings.opt_out_analytics) {
if (setSettings.telemetry) {
mixpanel_opt_out_tracking()
} else {
mixpanel_opt_in_tracking()
}
for (const [key, value] of Object.entries(setSettings.java_globals)) {
if (value?.path === '') {
value.path = undefined
}
if (value?.path) {
value.path = value.path.replace('java.exe', 'javaw.exe')
}
console.log(`${key}: ${value}`)
}
setSettings.custom_java_args = setSettings.javaArgs.trim().split(/\s+/).filter(Boolean)
setSettings.custom_env_args = setSettings.envArgs
setSettings.extra_launch_args = setSettings.launchArgs.trim().split(/\s+/).filter(Boolean)
setSettings.custom_env_vars = setSettings.envVars
.trim()
.split(/\s+/)
.filter(Boolean)
@@ -78,22 +68,49 @@ watch(
setSettings.hooks.post_exit = null
}
if (!setSettings.custom_dir) {
setSettings.custom_dir = null
}
await set(setSettings)
},
{ deep: true },
)
const credentials = ref(await getCreds().catch(handleError))
const javaVersions = ref(await get_java_versions().catch(handleError))
async function updateJavaVersion(version) {
if (version?.path === '') {
version.path = undefined
}
if (version?.path) {
version.path = version.path.replace('java.exe', 'javaw.exe')
}
await set_java_version(version).catch(handleError)
}
async function fetchCredentials() {
const creds = await getCreds().catch(handleError)
console.log(creds)
if (creds && creds.user_id) {
creds.user = await get_user(creds.user_id).catch(handleError)
}
credentials.value = creds
}
const credentials = ref()
await fetchCredentials()
const loginScreenModal = ref()
async function logOut() {
await logout().catch(handleError)
credentials.value = await getCreds().catch(handleError)
await fetchCredentials()
}
async function signInAfter() {
loginScreenModal.value.hide()
credentials.value = await getCreds().catch(handleError)
await fetchCredentials()
}
async function findLauncherDir() {
@@ -103,24 +120,10 @@ async function findLauncherDir() {
title: 'Select a new app directory',
})
const writeable = await is_dir_writeable(newDir)
if (!writeable) {
handleError('The selected directory does not have proper permissions for write access.')
return
}
if (newDir) {
settingsDir.value = newDir
await refreshDir()
settings.value.custom_dir = newDir
}
}
async function refreshDir() {
await change_config_dir(settingsDir.value).catch(handleError)
settings.value = await accessSettings().catch(handleError)
settingsDir.value = settings.value.loaded_config_dir
}
</script>
<template>
@@ -131,13 +134,7 @@ async function refreshDir() {
<span class="label__title size-card-header">General settings</span>
</h3>
</div>
<Modal
ref="loginScreenModal"
class="login-screen-modal"
:noblur="!themeStore.advancedRendering"
>
<ModrinthLoginScreen :modal="true" :prev-page="signInAfter" :next-page="signInAfter" />
</Modal>
<ModrinthLoginScreen ref="loginScreenModal" :callback="signInAfter" />
<div class="adjacent-input">
<label for="theme">
<span class="label__title">Manage account</span>
@@ -164,15 +161,11 @@ async function refreshDir() {
<div class="app-directory">
<div class="iconified-input">
<BoxIcon />
<input id="appDir" v-model="settingsDir" type="text" class="input" />
<input id="appDir" v-model="settings.custom_dir" type="text" class="input" />
<Button class="r-btn" @click="findLauncherDir">
<FolderSearchIcon />
</Button>
</div>
<Button large @click="refreshDir">
<UpdatedIcon />
Refresh
</Button>
</div>
</Card>
<Card>
@@ -230,11 +223,11 @@ async function refreshDir() {
</label>
<Toggle
id="minimize-launcher"
:model-value="settings.hide_on_process"
:checked="settings.hide_on_process"
:model-value="settings.hide_on_process_start"
:checked="settings.hide_on_process_start"
@update:model-value="
(e) => {
settings.hide_on_process = e
settings.hide_on_process_start = e
}
"
/>
@@ -285,10 +278,11 @@ async function refreshDir() {
<div class="adjacent-input">
<label for="max-downloads">
<span class="label__title">Maximum concurrent downloads</span>
<span class="label__description"
>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.</span
>
<span class="label__description">
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)
</span>
</label>
<Slider
id="max-downloads"
@@ -302,10 +296,11 @@ async function refreshDir() {
<div class="adjacent-input">
<label for="max-writes">
<span class="label__title">Maximum concurrent writes</span>
<span class="label__description"
>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.</span
>
<span class="label__description">
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)
</span>
</label>
<Slider
id="max-writes"
@@ -324,37 +319,38 @@ async function refreshDir() {
</div>
<div class="adjacent-input">
<label for="opt-out-analytics">
<span class="label__title">Disable analytics</span>
<span class="label__title">Telemetry</span>
<span class="label__description">
Modrinth collects anonymized analytics and usage data to improve our user experience and
customize your experience. By enabling 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.
</span>
</label>
<Toggle
id="opt-out-analytics"
:model-value="settings.opt_out_analytics"
:checked="settings.opt_out_analytics"
:model-value="settings.telemetry"
:checked="settings.telemetry"
@update:model-value="
(e) => {
settings.opt_out_analytics = e
settings.telemetry = e
}
"
/>
</div>
<div class="adjacent-input">
<label for="disable-discord-rpc">
<span class="label__title">Disable Discord RPC</span>
<span class="label__title">Discord RPC</span>
<span class="label__description">
Disables the Discord Rich Presence integration. 'Modrinth' will no longer show up as a
game or app you are using on your Discord profile. This does not disable any
instance-specific Discord Rich Presence integrations, such as those added by mods.
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 any instance-specific Discord Rich Presence integrations, such as those added by
mods. (app restart required to take effect)
</span>
</label>
<Toggle
id="disable-discord-rpc"
v-model="settings.disable_discord_rpc"
:checked="settings.disable_discord_rpc"
v-model="settings.discord_rpc"
:checked="settings.discord_rpc"
/>
</div>
</Card>
@@ -364,25 +360,24 @@ async function refreshDir() {
<span class="label__title size-card-header">Java settings</span>
</h3>
</div>
<label for="java-21">
<span class="label__title">Java 21 location</span>
</label>
<JavaSelector id="java-17" v-model="settings.java_globals.JAVA_21" :version="21" />
<label for="java-17">
<span class="label__title">Java 17 location</span>
</label>
<JavaSelector id="java-17" v-model="settings.java_globals.JAVA_17" :version="17" />
<label for="java-8">
<span class="label__title">Java 8 location</span>
</label>
<JavaSelector id="java-8" v-model="settings.java_globals.JAVA_8" :version="8" />
<template v-for="version in [21, 17, 8]">
<label :for="'java-' + version">
<span class="label__title">Java {{ version }} location</span>
</label>
<JavaSelector
:id="'java-selector-' + version"
v-model="javaVersions[version]"
:version="version"
@update:model-value="updateJavaVersion"
/>
</template>
<hr class="card-divider" />
<label for="java-args">
<span class="label__title">Java arguments</span>
</label>
<input
id="java-args"
v-model="settings.javaArgs"
v-model="settings.launchArgs"
autocomplete="off"
type="text"
class="installation-input"
@@ -393,7 +388,7 @@ async function refreshDir() {
</label>
<input
id="env-vars"
v-model="settings.envArgs"
v-model="settings.envVars"
autocomplete="off"
type="text"
class="installation-input"
@@ -526,7 +521,7 @@ async function refreshDir() {
<div>
<label>
<span class="label__title">App version</span>
<span class="label__description">Theseus v{{ version }} </span>
<span class="label__description">Modrinth App v{{ version }} </span>
</label>
</div>
</Card>
@@ -551,16 +546,6 @@ async function refreshDir() {
margin: 1rem 0;
}
:deep(.login-screen-modal) {
.modal-container .modal-body {
width: auto;
.content {
background: none;
}
}
}
.app-directory {
display: flex;
flex-direction: row;

View File

@@ -2,20 +2,10 @@
<div class="instance-container">
<div class="side-cards">
<Card class="instance-card" @contextmenu.prevent.stop="handleRightClick">
<Avatar
size="lg"
:src="
!instance.metadata.icon ||
(instance.metadata.icon && instance.metadata.icon.startsWith('http'))
? instance.metadata.icon
: convertFileSrc(instance.metadata?.icon)
"
/>
<Avatar size="lg" :src="instance.icon_path ? convertFileSrc(instance.icon_path) : null" />
<div class="instance-info">
<h2 class="name">{{ instance.metadata.name }}</h2>
<span class="metadata">
{{ instance.metadata.loader }} {{ instance.metadata.game_version }}
</span>
<h2 class="name">{{ instance.name }}</h2>
<span class="metadata"> {{ instance.loader }} {{ instance.game_version }} </span>
</div>
<span class="button-group">
<Button v-if="instance.install_stage !== 'installed'" disabled class="instance-button">
@@ -26,7 +16,6 @@
color="danger"
class="instance-button"
@click="stopInstance('InstancePage')"
@mouseover="checkProcess"
>
<StopCircleIcon />
Stop
@@ -36,7 +25,6 @@
color="primary"
class="instance-button"
@click="startInstance('InstancePage')"
@mouseover="checkProcess"
>
<PlayIcon />
Play
@@ -135,22 +123,20 @@ import {
CheckCircleIcon,
UpdatedIcon,
} from '@modrinth/assets'
import { get, run } from '@/helpers/profile'
import {
get_all_running_profile_paths,
get_uuids_by_profile_path,
kill_by_uuid,
} from '@/helpers/process'
import { offline_listener, process_listener, profile_listener } from '@/helpers/events'
import { get, kill, run } from '@/helpers/profile'
import { get_by_profile_path } from '@/helpers/process'
import { process_listener, profile_listener } from '@/helpers/events'
import { useRoute, useRouter } from 'vue-router'
import { ref, onUnmounted } from 'vue'
import { handleError, useBreadcrumbs, useLoading } from '@/store/state'
import { isOffline, showProfileInFolder } from '@/helpers/utils.js'
import { showProfileInFolder } from '@/helpers/utils.js'
import ContextMenu from '@/components/ui/ContextMenu.vue'
import { mixpanel_track } from '@/helpers/mixpanel'
import { convertFileSrc } from '@tauri-apps/api/tauri'
import { useFetch } from '@/helpers/fetch'
import { handleSevereError } from '@/store/error.js'
import { get_project, get_version_many } from '@/helpers/cache.js'
import dayjs from 'dayjs'
const route = useRoute()
@@ -161,72 +147,71 @@ const instance = ref(await get(route.params.id).catch(handleError))
breadcrumbs.setName(
'Instance',
instance.value.metadata.name.length > 40
? instance.value.metadata.name.substring(0, 40) + '...'
: instance.value.metadata.name,
instance.value.name.length > 40
? instance.value.name.substring(0, 40) + '...'
: instance.value.name,
)
breadcrumbs.setContext({
name: instance.value.metadata.name,
name: instance.value.name,
link: route.path,
query: route.query,
})
const offline = ref(await isOffline())
const offline = ref(!navigator.onLine)
window.addEventListener('offline', () => {
offline.value = true
})
window.addEventListener('online', () => {
offline.value = false
})
const loadingBar = useLoading()
const uuid = ref(null)
const playing = ref(false)
const loading = ref(false)
const options = ref(null)
const startInstance = async (context) => {
loading.value = true
uuid.value = await run(route.params.id).catch(handleSevereError)
run(route.params.id).catch(handleSevereError)
loading.value = false
playing.value = true
mixpanel_track('InstanceStart', {
loader: instance.value.metadata.loader,
game_version: instance.value.metadata.game_version,
loader: instance.value.loader,
game_version: instance.value.game_version,
source: context,
})
}
const checkProcess = async () => {
const runningPaths = await get_all_running_profile_paths().catch(handleError)
if (runningPaths.includes(instance.value.path)) {
playing.value = true
return
}
const runningProcesses = await get_by_profile_path(route.params.id).catch(handleError)
playing.value = false
uuid.value = null
playing.value = runningProcesses.length > 0
}
// Get information on associated modrinth versions, if any
const modrinthVersions = ref([])
if (!(await isOffline()) && instance.value.metadata.linked_data?.project_id) {
modrinthVersions.value = await useFetch(
`https://api.modrinth.com/v2/project/${instance.value.metadata.linked_data.project_id}/version`,
'project',
)
if (!offline.value && instance.value.linked_data && instance.value.linked_data.project_id) {
const project = await get_project(instance.value.linked_data.project_id).catch(handleError)
if (project && project.versions) {
modrinthVersions.value = (await get_version_many(project.versions).catch(handleError)).sort(
(a, b) => dayjs(b.date_published) - dayjs(a.date_published),
)
}
}
await checkProcess()
const stopInstance = async (context) => {
playing.value = false
if (!uuid.value) {
const uuids = await get_uuids_by_profile_path(instance.value.path).catch(handleError)
uuid.value = uuids[0] // populate Uuid to listen for in the process_listener
uuids.forEach(async (u) => await kill_by_uuid(u).catch(handleError))
} else await kill_by_uuid(uuid.value).catch(handleError)
await kill(route.params.id).catch(handleError)
mixpanel_track('InstanceStop', {
loader: instance.value.metadata.loader,
game_version: instance.value.metadata.game_version,
loader: instance.value.loader,
game_version: instance.value.game_version,
source: context,
})
}
@@ -271,7 +256,7 @@ const handleOptionsClick = async (args) => {
break
case 'add_content':
await router.push({
path: `/browse/${instance.value.metadata.loader === 'vanilla' ? 'datapack' : 'mod'}`,
path: `/browse/${instance.value.loader === 'vanilla' ? 'datapack' : 'mod'}`,
query: { i: route.params.id },
})
break
@@ -302,17 +287,12 @@ const unlistenProfiles = await profile_listener(async (event) => {
})
const unlistenProcesses = await process_listener((e) => {
if (e.event === 'finished' && uuid.value === e.uuid) playing.value = false
})
const unlistenOffline = await offline_listener((b) => {
offline.value = b
if (e.event === 'finished' && e.profile_path_id === route.params.id) playing.value = false
})
onUnmounted(() => {
unlistenProcesses()
unlistenProfiles()
unlistenOffline()
})
</script>

View File

@@ -99,7 +99,7 @@ import { computed, nextTick, onBeforeUnmount, onMounted, onUnmounted, ref, watch
import dayjs from 'dayjs'
import isToday from 'dayjs/plugin/isToday'
import isYesterday from 'dayjs/plugin/isYesterday'
import { get_uuids_by_profile_path } from '@/helpers/process.js'
import { get_by_profile_path } from '@/helpers/process.js'
import { useRoute } from 'vue-router'
import { process_listener } from '@/helpers/events.js'
import { handleError } from '@/store/notifications.js'
@@ -209,9 +209,9 @@ const processedLogs = computed(() => {
async function getLiveStdLog() {
if (route.params.id) {
const uuids = await get_uuids_by_profile_path(route.params.id).catch(handleError)
const processes = await get_by_profile_path(route.params.id).catch(handleError)
let returnValue
if (uuids.length === 0) {
if (processes.length === 0) {
returnValue = emptyText.join('\n')
} else {
const logCursor = await get_latest_log_cursor(

View File

@@ -281,7 +281,7 @@
<div class="markdown-body">
<p>
Are you sure you want to remove
<strong>{{ functionValues.length }} project(s)</strong> from {{ instance.metadata.name }}?
<strong>{{ functionValues.length }} project(s)</strong> from {{ instance.name }}?
<br />
This action <strong>cannot</strong> be undone.
</p>
@@ -304,7 +304,7 @@
>{{ Array.from(projects.values()).filter((x) => x.disabled).length }} disabled
project(s)</strong
>
from {{ instance.metadata.name }}?
from {{ instance.name }}?
<br />
This action <strong>cannot</strong> be undone.
</p>
@@ -326,7 +326,7 @@
/>
<ExportModal v-if="projects.length > 0" ref="exportModal" :instance="instance" />
<ModpackVersionModal
v-if="instance.metadata.linked_data"
v-if="instance.linked_data"
ref="modpackVersionModal"
:instance="instance"
:versions="props.versions"
@@ -364,6 +364,7 @@ import { computed, onUnmounted, ref, watch } from 'vue'
import {
add_project_from_path,
get,
get_projects,
remove_project,
toggle_disable_project,
update_all,
@@ -372,12 +373,18 @@ import {
import { handleError } from '@/store/notifications.js'
import { mixpanel_track } from '@/helpers/mixpanel'
import { listen } from '@tauri-apps/api/event'
import { convertFileSrc } from '@tauri-apps/api/tauri'
import { highlightModInProfile } from '@/helpers/utils.js'
import { MenuIcon, ToggleIcon, TextInputIcon, AddProjectImage, PackageIcon } from '@/assets/icons'
import ExportModal from '@/components/ui/ExportModal.vue'
import ModpackVersionModal from '@/components/ui/ModpackVersionModal.vue'
import AddContentButton from '@/components/ui/AddContentButton.vue'
import {
get_organization_many,
get_project_many,
get_team_many,
get_version_many,
} from '@/helpers/cache.js'
import { profile_listener } from '@/helpers/events.js'
const props = defineProps({
instance: {
@@ -404,65 +411,102 @@ const props = defineProps({
},
})
const projects = ref([])
const selectionMap = ref(new Map())
const unlistenProfiles = await profile_listener(async (event) => {
if (
event.profile_path_id === props.instance.path &&
event.event === 'synced' &&
props.instance.install_stage !== 'pack_installing'
) {
await initProjects()
}
})
onUnmounted(() => {
unlistenProfiles()
})
const showingOptions = ref(false)
const isPackLocked = computed(() => {
return props.instance.metadata.linked_data && props.instance.metadata.linked_data.locked
return props.instance.linked_data && props.instance.linked_data.locked
})
const canUpdatePack = computed(() => {
if (!props.instance.metadata.linked_data) return false
return props.instance.metadata.linked_data.version_id !== props.instance.modrinth_update_version
if (!props.instance.linked_data || !props.versions || !props.versions[0]) return false
return props.instance.linked_data.version_id !== props.versions[0].id
})
const exportModal = ref(null)
const initProjects = (initInstance) => {
projects.value = []
if (!initInstance || !initInstance.projects) return
for (const [path, project] of Object.entries(initInstance.projects)) {
if (project.metadata.type === 'modrinth' && !props.offline) {
let owner = project.metadata.members.find((x) => x.role === 'Owner')
projects.value.push({
const projects = ref([])
const selectionMap = ref(new Map())
const initProjects = async () => {
const newProjects = []
const profileProjects = await get_projects(props.instance.path)
const fetchProjects = []
const fetchVersions = []
for (const value of Object.values(profileProjects)) {
if (value.metadata) {
fetchProjects.push(value.metadata.project_id)
fetchVersions.push(value.metadata.version_id)
}
}
const [modrinthProjects, modrinthVersions] = await Promise.all([
await get_project_many(fetchProjects).catch(handleError),
await get_version_many(fetchVersions).catch(handleError),
])
const [modrinthTeams, modrinthOrganizations] = await Promise.all([
await get_team_many(modrinthProjects.map((x) => x.team)).catch(handleError),
await get_organization_many(
modrinthProjects.map((x) => x.organization).filter((x) => !!x),
).catch(handleError),
])
for (const [path, file] of Object.entries(profileProjects)) {
if (file.metadata) {
const project = modrinthProjects.find((x) => file.metadata.project_id === x.id)
const version = modrinthVersions.find((x) => file.metadata.version_id === x.id)
const org = project.organization
? modrinthOrganizations.find((x) => x.id === project.organization)
: null
const team = modrinthTeams.find((x) => x[0].team_id === project.team)
let owner = org ? org.name : team.find((x) => x.is_owner).user.username
newProjects.push({
path,
name: project.metadata.project.title,
slug: project.metadata.project.slug,
author: owner ? owner.user.username : null,
version: project.metadata.version.version_number,
file_name: project.file_name,
icon: project.metadata.project.icon_url,
disabled: project.disabled,
updateVersion: project.metadata.update_version,
outdated: !!project.metadata.update_version,
project_type: project.metadata.project.project_type,
id: project.metadata.project.id,
})
} else if (project.metadata.type === 'inferred') {
projects.value.push({
path,
name: project.metadata.title ?? project.file_name,
author: project.metadata.authors[0],
version: project.metadata.version,
file_name: project.file_name,
icon: project.metadata.icon ? convertFileSrc(project.metadata.icon) : null,
disabled: project.disabled,
outdated: false,
project_type: project.metadata.project_type,
name: project.title,
slug: project.slug,
author: owner,
version: version.version_number,
file_name: file.file_name,
icon: project.icon_url,
disabled: file.file_name.endsWith('.disabled'),
updateVersion: file.update_version_id,
outdated: !!file.update_version_id,
project_type: project.project_type,
id: project.id,
})
} else {
projects.value.push({
newProjects.push({
path,
name: project.file_name,
name: file.file_name.replace('.disabled', ''),
author: '',
version: null,
file_name: project.file_name,
file_name: file.file_name,
icon: null,
disabled: project.disabled,
disabled: file.file_name.endsWith('.disabled'),
outdated: false,
project_type: null,
project_type: file.project_type,
})
}
}
projects.value = newProjects
const newSelectionMap = new Map()
for (const project of projects.value) {
newSelectionMap.set(
@@ -475,22 +519,7 @@ const initProjects = (initInstance) => {
}
selectionMap.value = newSelectionMap
}
initProjects(props.instance)
watch(
() => props.instance.projects,
() => {
initProjects(props.instance)
},
)
watch(
() => props.offline,
() => {
if (props.instance) initProjects(props.instance)
},
)
await initProjects()
const modpackVersionModal = ref(null)
const installing = computed(() => props.instance.install_stage !== 'installed')
@@ -633,8 +662,8 @@ const updateAll = async () => {
}
mixpanel_track('InstanceUpdateAll', {
loader: props.instance.metadata.loader,
game_version: props.instance.metadata.game_version,
loader: props.instance.loader,
game_version: props.instance.game_version,
count: setProjects.length,
selected: selected.value.length > 1,
})
@@ -659,8 +688,8 @@ const updateProject = async (mod) => {
mod.updateVersion = null
mixpanel_track('InstanceProjectUpdate', {
loader: props.instance.metadata.loader,
game_version: props.instance.metadata.game_version,
loader: props.instance.loader,
game_version: props.instance.game_version,
id: mod.id,
name: mod.name,
project_type: mod.project_type,
@@ -686,8 +715,8 @@ const toggleDisableMod = async (mod) => {
mod.path = newPath
mod.disabled = !mod.disabled
mixpanel_track('InstanceProjectDisable', {
loader: props.instance.metadata.loader,
game_version: props.instance.metadata.game_version,
loader: props.instance.loader,
game_version: props.instance.game_version,
id: mod.id,
name: mod.name,
project_type: mod.project_type,
@@ -707,8 +736,8 @@ const removeMod = async (mod) => {
projects.value = projects.value.filter((x) => mod.path !== x.path)
mixpanel_track('InstanceProjectRemove', {
loader: props.instance.metadata.loader,
game_version: props.instance.metadata.game_version,
loader: props.instance.loader,
game_version: props.instance.game_version,
id: mod.id,
name: mod.name,
project_type: mod.project_type,
@@ -820,7 +849,7 @@ watch(selectAll, () => {
const unlisten = await listen('tauri://file-drop', async (event) => {
for (const file of event.payload) {
if (file.endsWith('.mrpack')) continue
await add_project_from_path(props.instance.path, file, 'mod').catch(handleError)
await add_project_from_path(props.instance.path, file).catch(handleError)
}
initProjects(await get(props.instance.path).catch(handleError))
})

View File

@@ -25,7 +25,7 @@
<XIcon />
Cancel
</button>
<button class="btn btn-danger" :disabled="action_disabled" @click="unlockProfile">
<button class="btn btn-danger" @click="unlockProfile">
<LockIcon />
Unlock
</button>
@@ -50,7 +50,7 @@
<XIcon />
Cancel
</button>
<button class="btn btn-danger" :disabled="action_disabled" @click="unpairProfile">
<button class="btn btn-danger" @click="unpairProfile">
<XIcon />
Unpair
</button>
@@ -117,21 +117,13 @@
<span class="label__title">Icon</span>
</label>
<div class="input-group">
<Avatar
:src="!icon || (icon && icon.startsWith('http')) ? icon : convertFileSrc(icon)"
size="md"
class="project__icon"
/>
<Avatar :src="icon ? convertFileSrc(icon) : icon" size="md" class="project__icon" />
<div class="input-stack">
<button id="instance-icon" class="btn" @click="setIcon">
<UploadIcon />
Select icon
</button>
<button
:disabled="!(!icon || (icon && icon.startsWith('http')) ? icon : convertFileSrc(icon))"
class="btn"
@click="resetIcon"
>
<button :disabled="!icon" class="btn" @click="resetIcon">
<TrashIcon />
Remove icon
</button>
@@ -147,7 +139,7 @@
autocomplete="off"
maxlength="80"
type="text"
:disabled="instance.metadata.linked_data"
:disabled="instance.linked_data"
/>
<div class="adjacent-input">
@@ -358,7 +350,7 @@
/>
</div>
</Card>
<Card v-if="instance.metadata.linked_data">
<Card v-if="instance.linked_data">
<div class="label">
<h3>
<span class="label__title size-card-header">Modpack</span>
@@ -366,9 +358,7 @@
</div>
<div class="adjacent-input">
<label for="general-modpack-info">
<span class="label__description">
<strong>Modpack: </strong> {{ instance.metadata.name }}
</span>
<span class="label__description"> <strong>Modpack: </strong> {{ instance.name }} </span>
<span class="label__description">
<strong>Version: </strong>
{{
@@ -414,7 +404,7 @@
</Button>
</div>
<div v-if="props.instance.metadata.linked_data.project_id" class="adjacent-input">
<div v-if="instance.linked_data.project_id" class="adjacent-input">
<label for="change-modpack-version">
<span class="label__title">Change modpack version</span>
<span class="label__description">
@@ -502,7 +492,7 @@
</div>
</Card>
<ModpackVersionModal
v-if="instance.metadata.linked_data"
v-if="instance.linked_data"
ref="modpackVersionModal"
:instance="instance"
:versions="props.versions"
@@ -553,12 +543,7 @@ import { get } from '@/helpers/settings.js'
import JavaSelector from '@/components/ui/JavaSelector.vue'
import { convertFileSrc } from '@tauri-apps/api/tauri'
import { open } from '@tauri-apps/api/dialog'
import {
get_fabric_versions,
get_forge_versions,
get_neoforge_versions,
get_quilt_versions,
} from '@/helpers/metadata.js'
import { get_loader_versions } from '@/helpers/metadata.js'
import { get_game_versions, get_loaders } from '@/helpers/tags.js'
import { handleError } from '@/store/notifications.js'
import { mixpanel_track } from '@/helpers/mixpanel'
@@ -587,17 +572,17 @@ const props = defineProps({
const themeStore = useTheming()
const title = ref(props.instance.metadata.name)
const icon = ref(props.instance.metadata.icon)
const groups = ref(props.instance.metadata.groups)
const title = ref(props.instance.name)
const icon = ref(props.instance.icon_path)
const groups = ref(props.instance.groups)
const modpackVersionModal = ref(null)
const instancesList = Object.values(await list(true))
const instancesList = await list()
const availableGroups = ref([
...new Set(
instancesList.reduce((acc, obj) => {
return acc.concat(obj.metadata.groups)
return acc.concat(obj.groups)
}, []),
),
])
@@ -632,18 +617,18 @@ const globalSettings = await get().catch(handleError)
const modalConfirmUnlock = ref(null)
const modalConfirmUnpair = ref(null)
const javaSettings = props.instance.java ?? {}
const overrideJavaInstall = ref(!!javaSettings.override_version)
const overrideJavaInstall = ref(!!props.instance.java_path)
const optimalJava = readonly(await get_optimal_jre_key(props.instance.path).catch(handleError))
const javaInstall = ref(optimalJava ?? javaSettings.override_version ?? { path: '', version: '' })
const javaInstall = ref({ path: optimalJava.path ?? props.instance.java_path })
const overrideJavaArgs = ref(!!javaSettings.extra_arguments)
const javaArgs = ref((javaSettings.extra_arguments ?? globalSettings.custom_java_args).join(' '))
const overrideJavaArgs = ref(!!props.instance.extra_launch_args)
const javaArgs = ref(
(props.instance.extra_launch_args ?? globalSettings.extra_launch_args).join(' '),
)
const overrideEnvVars = ref(!!javaSettings.custom_env_args)
const overrideEnvVars = ref(!!props.instance.custom_env_vars)
const envVars = ref(
(javaSettings.custom_env_args ?? globalSettings.custom_env_args)
(props.instance.custom_env_vars ?? globalSettings.custom_env_vars)
.map((x) => x.join('='))
.join(' '),
)
@@ -652,18 +637,22 @@ 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 overrideWindowSettings = ref(!!props.instance.resolution || !!props.instance.fullscreen)
const resolution = ref(props.instance.resolution ?? globalSettings.game_resolution)
const overrideHooks = ref(!!props.instance.hooks)
const overrideWindowSettings = ref(
!!props.instance.game_resolution || !!props.instance.force_fullscreen,
)
const resolution = ref(props.instance.game_resolution ?? globalSettings.game_resolution)
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 fullscreenSetting = ref(!!props.instance.fullscreen)
const fullscreenSetting = ref(!!props.instance.force_fullscreen)
const unlinkModpack = ref(false)
const inProgress = ref(false)
const installing = computed(() => props.instance.install_stage !== 'installed')
const installedVersion = computed(() => props.instance?.metadata?.linked_data?.version_id)
const installedVersion = computed(() => props.instance?.linked_data?.version_id)
const installedVersionData = computed(() => {
if (!installedVersion.value) return null
return props.versions.find((version) => version.id === installedVersion.value)
@@ -706,34 +695,29 @@ const getLocalVersion = (path) => {
const editProfileObject = computed(() => {
const editProfile = {
metadata: {
name: title.value.trim().substring(0, 32) ?? 'Instance',
groups: groups.value.map((x) => x.trim().substring(0, 32)).filter((x) => x.length > 0),
loader_version: props.instance.metadata.loader_version,
linked_data: props.instance.metadata.linked_data,
},
name: title.value.trim().substring(0, 32) ?? 'Instance',
groups: groups.value.map((x) => x.trim().substring(0, 32)).filter((x) => x.length > 0),
loader_version: props.instance.loader_version,
linked_data: props.instance.linked_data,
java: {},
hooks: {},
}
if (overrideJavaInstall.value) {
if (javaInstall.value.path !== '') {
editProfile.java.override_version = javaInstall.value
editProfile.java.override_version.path = editProfile.java.override_version.path.replace(
'java.exe',
'javaw.exe',
)
editProfile.java_path = javaInstall.value.path.replace('java.exe', 'javaw.exe')
}
}
if (overrideJavaArgs.value) {
if (javaArgs.value !== '') {
editProfile.java.extra_arguments = javaArgs.value.trim().split(/\s+/).filter(Boolean)
editProfile.extra_launch_args = javaArgs.value.trim().split(/\s+/).filter(Boolean)
}
}
if (overrideEnvVars.value) {
if (envVars.value !== '') {
editProfile.java.custom_env_args = envVars.value
editProfile.custom_env_vars = envVars.value
.trim()
.split(/\s+/)
.filter(Boolean)
@@ -746,10 +730,10 @@ const editProfileObject = computed(() => {
}
if (overrideWindowSettings.value) {
editProfile.fullscreen = fullscreenSetting.value
editProfile.force_fullscreen = fullscreenSetting.value
if (!fullscreenSetting.value) {
editProfile.resolution = resolution.value
editProfile.game_resolution = resolution.value
}
}
@@ -758,10 +742,10 @@ const editProfileObject = computed(() => {
}
if (unlinkModpack.value) {
editProfile.metadata.linked_data = null
editProfile.linked_data = null
}
breadcrumbs.setName('Instance', editProfile.metadata.name)
breadcrumbs.setName('Instance', editProfile.name)
return editProfile
})
@@ -771,8 +755,8 @@ const repairing = ref(false)
async function duplicateProfile() {
await duplicate(props.instance.path).catch(handleError)
mixpanel_track('InstanceDuplicate', {
loader: props.instance.metadata.loader,
game_version: props.instance.metadata.game_version,
loader: props.instance.loader,
game_version: props.instance.game_version,
})
}
@@ -782,14 +766,14 @@ async function repairProfile(force) {
repairing.value = false
mixpanel_track('InstanceRepair', {
loader: props.instance.metadata.loader,
game_version: props.instance.metadata.game_version,
loader: props.instance.loader,
game_version: props.instance.game_version,
})
}
async function unpairProfile() {
const editProfile = props.instance
editProfile.metadata.linked_data = null
editProfile.linked_data = null
await edit(props.instance.path, editProfile)
installedVersion.value = null
installedVersionData.value = null
@@ -798,13 +782,13 @@ async function unpairProfile() {
async function unlockProfile() {
const editProfile = props.instance
editProfile.metadata.linked_data.locked = false
editProfile.linked_data.locked = false
await edit(props.instance.path, editProfile)
modalConfirmUnlock.value.hide()
}
const isPackLocked = computed(() => {
return props.instance.metadata.linked_data && props.instance.metadata.linked_data.locked
return props.instance.linked_data && props.instance.linked_data.locked
})
async function repairModpack() {
@@ -813,8 +797,8 @@ async function repairModpack() {
inProgress.value = false
mixpanel_track('InstanceRepair', {
loader: props.instance.metadata.loader,
game_version: props.instance.metadata.game_version,
loader: props.instance.loader,
game_version: props.instance.game_version,
})
}
@@ -825,8 +809,8 @@ async function removeProfile() {
removing.value = false
mixpanel_track('InstanceRemove', {
loader: props.instance.metadata.loader,
game_version: props.instance.metadata.game_version,
loader: props.instance.loader,
game_version: props.instance.game_version,
})
await router.push({ path: '/' })
@@ -843,10 +827,10 @@ const [
all_game_versions,
loaders,
] = await Promise.all([
get_fabric_versions().then(shallowRef).catch(handleError),
get_forge_versions().then(shallowRef).catch(handleError),
get_quilt_versions().then(shallowRef).catch(handleError),
get_neoforge_versions().then(shallowRef).catch(handleError),
get_loader_versions('fabric').then(shallowRef).catch(handleError),
get_loader_versions('forge').then(shallowRef).catch(handleError),
get_loader_versions('quilt').then(shallowRef).catch(handleError),
get_loader_versions('neo').then(shallowRef).catch(handleError),
get_game_versions().then(shallowRef).catch(handleError),
get_loaders()
.then((value) =>
@@ -859,8 +843,8 @@ const [
])
loaders.value.unshift('vanilla')
const loader = ref(props.instance.metadata.loader)
const gameVersion = ref(props.instance.metadata.game_version)
const loader = ref(props.instance.loader)
const gameVersion = ref(props.instance.game_version)
const selectableGameVersions = computed(() => {
return all_game_versions.value
.filter((item) => {
@@ -896,9 +880,7 @@ const selectableLoaderVersions = computed(() => {
return []
})
const loaderVersionIndex = ref(
selectableLoaderVersions.value.findIndex(
(x) => x.id === props.instance.metadata.loader_version?.id,
),
selectableLoaderVersions.value.findIndex((x) => x.id === props.instance.loader_version),
)
const isValid = computed(() => {
@@ -910,10 +892,9 @@ const isValid = computed(() => {
const isChanged = computed(() => {
return (
loader.value != props.instance.metadata.loader ||
gameVersion.value != props.instance.metadata.game_version ||
JSON.stringify(selectableLoaderVersions.value[loaderVersionIndex.value]) !==
JSON.stringify(props.instance.metadata.loader_version)
loader.value !== props.instance.loader ||
gameVersion.value !== props.instance.game_version ||
selectableLoaderVersions.value[loaderVersionIndex.value].id !== props.instance.loader_version
)
})
@@ -924,11 +905,11 @@ async function saveGvLoaderEdits() {
editing.value = true
let editProfile = editProfileObject.value
editProfile.metadata.loader = loader.value
editProfile.metadata.game_version = gameVersion.value
editProfile.loader = loader.value
editProfile.game_version = gameVersion.value
if (loader.value !== 'vanilla') {
editProfile.metadata.loader_version = selectableLoaderVersions.value[loaderVersionIndex.value]
editProfile.loader_version = selectableLoaderVersions.value[loaderVersionIndex.value].id
}
await edit(props.instance.path, editProfile).catch(handleError)
await repairProfile(false)

View File

@@ -4,26 +4,17 @@
<Card v-if="instance" class="small-instance">
<router-link class="instance" :to="`/instance/${encodeURIComponent(instance.path)}`">
<Avatar
:src="
!instance.metadata.icon ||
(instance.metadata.icon && instance.metadata.icon.startsWith('http'))
? instance.metadata.icon
: convertFileSrc(instance.metadata?.icon)
"
:alt="instance.metadata.name"
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : null"
:alt="instance.name"
size="sm"
/>
<div class="small-instance_info">
<span class="title">{{
instance.metadata.name.length > 20
? instance.metadata.name.substring(0, 20) + '...'
: instance.metadata.name
instance.name.length > 20 ? instance.name.substring(0, 20) + '...' : instance.name
}}</span>
<span>
{{
instance.metadata.loader.charAt(0).toUpperCase() + instance.metadata.loader.slice(1)
}}
{{ instance.metadata.game_version }}
{{ instance.loader.charAt(0).toUpperCase() + instance.loader.slice(1) }}
{{ instance.game_version }}
</span>
</div>
</router-link>
@@ -209,7 +200,6 @@
:project="data"
:versions="versions"
:members="members"
:dependencies="dependencies"
:instance="instance"
:install="install"
:installed="installed"
@@ -218,9 +208,6 @@
/>
</div>
</div>
<InstallConfirmModal ref="confirmModal" />
<ModInstallModal ref="modInstallModal" />
<IncompatibilityWarningModal ref="incompatibilityWarning" />
<ContextMenu ref="options" @option-clicked="handleOptionsClick">
<template #install> <DownloadIcon /> Install </template>
<template #open_link> <GlobeIcon /> Open in Modrinth <ExternalIcon /> </template>
@@ -263,79 +250,63 @@ import {
OpenCollectiveIcon,
} from '@/assets/external'
import { get_categories } from '@/helpers/tags'
import { install as packInstall } from '@/helpers/pack'
import {
list,
add_project_from_version as installMod,
check_installed,
get as getInstance,
remove_project,
} from '@/helpers/profile'
import { get as getInstance, get_projects as getInstanceProjects } from '@/helpers/profile'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import { useRoute } from 'vue-router'
import { ref, shallowRef, watch } from 'vue'
import { installVersionDependencies, isOffline } from '@/helpers/utils'
import InstallConfirmModal from '@/components/ui/InstallConfirmModal.vue'
import ModInstallModal from '@/components/ui/ModInstallModal.vue'
import { useBreadcrumbs } from '@/store/breadcrumbs'
import IncompatibilityWarningModal from '@/components/ui/IncompatibilityWarningModal.vue'
import { useFetch } from '@/helpers/fetch.js'
import { handleError } from '@/store/notifications.js'
import { convertFileSrc } from '@tauri-apps/api/tauri'
import ContextMenu from '@/components/ui/ContextMenu.vue'
import { mixpanel_track } from '@/helpers/mixpanel'
import { install as installVersion } from '@/store/install.js'
import { get_project, get_project_many, get_team, get_version_many } from '@/helpers/cache.js'
dayjs.extend(relativeTime)
const route = useRoute()
const breadcrumbs = useBreadcrumbs()
const confirmModal = ref(null)
const modInstallModal = ref(null)
const incompatibilityWarning = ref(null)
const options = ref(null)
const installing = ref(false)
const data = shallowRef(null)
const versions = shallowRef([])
const members = shallowRef([])
const dependencies = shallowRef([])
const categories = shallowRef([])
const instance = ref(null)
const instanceProjects = ref(null)
const installed = ref(false)
const installedVersion = ref(null)
const offline = ref(await isOffline())
async function fetchProjectData() {
;[
data.value,
versions.value,
members.value,
dependencies.value,
categories.value,
instance.value,
] = await Promise.all([
useFetch(`https://api.modrinth.com/v2/project/${route.params.id}`, 'project'),
useFetch(`https://api.modrinth.com/v2/project/${route.params.id}/version`, 'project'),
useFetch(`https://api.modrinth.com/v2/project/${route.params.id}/members`, 'project'),
useFetch(`https://api.modrinth.com/v2/project/${route.params.id}/dependencies`, 'project'),
get_categories().catch(handleError),
route.query.i ? getInstance(route.query.i, false).catch(handleError) : Promise.resolve(),
])
const project = await get_project(route.params.id).catch(handleError)
installed.value =
instance.value?.path &&
(await check_installed(instance.value.path, data.value.id).catch(handleError))
data.value = project
;[versions.value, members.value, categories.value, instance.value, instanceProjects.value] =
await Promise.all([
get_version_many(project.versions).catch(handleError),
get_team(project.team).catch(handleError),
get_categories().catch(handleError),
route.query.i ? getInstance(route.query.i).catch(handleError) : Promise.resolve(),
route.query.i ? getInstanceProjects(route.query.i).catch(handleError) : Promise.resolve(),
])
versions.value = versions.value.sort((a, b) => dayjs(b.date_published) - dayjs(a.date_published))
if (instanceProjects.value) {
const installedFile = Object.values(instanceProjects.value).find(
(x) => x.metadata && x.metadata.project_id === data.value.id,
)
if (installedFile) {
installed.value = true
installedVersion.value = installedFile.metadata.version_id
}
}
breadcrumbs.setName('Project', data.value.title)
installedVersion.value = instance.value
? Object.values(instance.value.projects).find(
(p) => p?.metadata?.version?.project_id === data.value.id,
)?.metadata?.version?.id
: null
}
if (!offline.value) await fetchProjectData()
await fetchProjectData()
watch(
() => route.params.id,
@@ -346,162 +317,22 @@ watch(
},
)
dayjs.extend(relativeTime)
const markInstalled = () => {
installed.value = true
}
async function install(version) {
installing.value = true
let queuedVersionData
if (instance.value) {
instance.value = await getInstance(instance.value.path, false).catch(handleError)
}
if (installed.value) {
const old_project = Object.entries(instance.value.projects)
.map(([key, value]) => ({
key,
value,
}))
.find((p) => p.value.metadata?.version?.project_id === data.value.id)
if (!old_project) {
// Switching too fast, old project is not recognized as a Modrinth project yet
await installVersion(
data.value.id,
version,
instance.value ? instance.value.path : null,
'ProjectPage',
(version) => {
installing.value = false
return
}
await remove_project(instance.value.path, old_project.key)
}
if (version) {
queuedVersionData = versions.value.find((v) => v.id === version)
} else {
if (data.value.project_type === 'modpack' || !instance.value) {
queuedVersionData = versions.value[0]
} else {
queuedVersionData = versions.value.find((v) =>
v.game_versions.includes(data.value.game_versions[0]),
)
}
}
if (data.value.project_type === 'modpack') {
const packs = Object.values(await list(true).catch(handleError))
if (
packs.length === 0 ||
!packs
.map((value) => value.metadata)
.find((pack) => pack.linked_data?.project_id === data.value.id)
) {
await packInstall(
data.value.id,
queuedVersionData.id,
data.value.title,
data.value.icon_url,
).catch(handleError)
mixpanel_track('PackInstall', {
id: data.value.id,
version_id: queuedVersionData.id,
title: data.value.title,
source: 'ProjectPage',
})
} else {
confirmModal.value.show(
data.value.id,
queuedVersionData.id,
data.value.title,
data.value.icon_url,
)
}
} else {
if (instance.value) {
if (!version) {
const gameVersion = instance.value.metadata.game_version
const loader = instance.value.metadata.loader
const selectedVersion = versions.value.find(
(v) =>
v.game_versions.includes(gameVersion) &&
(data.value.project_type === 'mod'
? v.loaders.includes(loader) || v.loaders.includes('minecraft')
: true),
)
if (!selectedVersion) {
incompatibilityWarning.value.show(
instance.value,
data.value.title,
versions.value,
markInstalled,
data.value.id,
data.value.project_type,
)
installing.value = false
return
} else {
queuedVersionData = selectedVersion
await installMod(instance.value.path, selectedVersion.id).catch(handleError)
await installVersionDependencies(instance.value, queuedVersionData)
installedVersion.value = selectedVersion.id
mixpanel_track('ProjectInstall', {
loader: instance.value.metadata.loader,
game_version: instance.value.metadata.game_version,
id: data.value.id,
project_type: data.value.project_type,
version_id: queuedVersionData.id,
title: data.value.title,
source: 'ProjectPage',
})
}
} else {
const gameVersion = instance.value.metadata.game_version
const loader = instance.value.metadata.loader
const compatible = versions.value.some(
(v) =>
v.game_versions.includes(gameVersion) &&
(data.value.project_type === 'mod'
? v.loaders.includes(loader) || v.loaders.includes('minecraft')
: true),
)
if (compatible) {
await installMod(instance.value.path, queuedVersionData.id).catch(handleError)
await installVersionDependencies(instance.value, queuedVersionData)
installedVersion.value = queuedVersionData.id
mixpanel_track('ProjectInstall', {
loader: instance.value.metadata.loader,
game_version: instance.value.metadata.game_version,
id: data.value.id,
project_type: data.value.project_type,
version_id: queuedVersionData.id,
title: data.value.title,
source: 'ProjectPage',
})
} else {
incompatibilityWarning.value.show(
instance.value,
data.value.title,
[queuedVersionData],
markInstalled,
data.value.id,
data.value.project_type,
)
installing.value = false
return
}
if (instance.value && version) {
installed.value = true
installedVersion.value = version
}
installed.value = true
} else {
modInstallModal.value.show(
data.value.id,
version ? [versions.value.find((v) => v.id === queuedVersionData.id)] : versions.value,
data.value.title,
data.value.project_type,
)
}
}
installing.value = false
},
)
}
const handleRightClick = (e) => {

View File

@@ -190,6 +190,7 @@ import { ref, watch, computed } from 'vue'
import { useRoute } from 'vue-router'
import { useBreadcrumbs } from '@/store/breadcrumbs'
import { SwapIcon } from '@/assets/icons'
import { get_project_many, get_version_many } from '@/helpers/cache.js'
const breadcrumbs = useBreadcrumbs()
@@ -204,10 +205,6 @@ const props = defineProps({
type: Array,
required: true,
},
dependencies: {
type: Object,
required: true,
},
members: {
type: Array,
required: true,
@@ -238,20 +235,45 @@ watch(
async () => {
if (route.params.version) {
version.value = props.versions.find((version) => version.id === route.params.version)
await refreshDisplayDependencies()
breadcrumbs.setName('Version', version.value.name)
}
},
)
const author = computed(() =>
props.members.find((member) => member.user.id === version.value.author_id),
props.members ? props.members.find((member) => member.user.id === version.value.author_id) : null,
)
const displayDependencies = computed(() =>
version.value.dependencies.map((dependency) => {
const version = props.dependencies.versions.find((obj) => obj.id === dependency.version_id)
const displayDependencies = ref({})
async function refreshDisplayDependencies() {
const projectIds = new Set()
const versionIds = new Set()
if (version.value.dependencies) {
for (const dependency of version.value.dependencies) {
if (dependency.project_id) {
projectIds.add(dependency.project_id)
}
if (dependency.version_id) {
versionIds.add(dependency.version_id)
}
}
}
const [projectDeps, versionDeps] = await Promise.all([
get_project_many([...projectIds]),
get_version_many([...versionIds]),
])
const dependencies = {
projects: projectDeps,
versions: versionDeps,
}
displayDependencies.value = version.value.dependencies.map((dependency) => {
const version = dependencies.versions.find((obj) => obj.id === dependency.version_id)
if (version) {
const project = props.dependencies.projects.find(
const project = dependencies.projects.find(
(obj) => obj.id === version.project_id || obj.id === dependency.project_id,
)
return {
@@ -261,7 +283,7 @@ const displayDependencies = computed(() =>
link: `/project/${project.slug}/version/${version.id}`,
}
} else {
const project = props.dependencies.projects.find((obj) => obj.id === dependency.project_id)
const project = dependencies.projects.find((obj) => obj.id === dependency.project_id)
if (project) {
return {
@@ -279,8 +301,9 @@ const displayDependencies = computed(() =>
}
}
}
}),
)
})
}
await refreshDisplayDependencies()
</script>
<style scoped lang="scss">

View File

@@ -187,8 +187,8 @@ const props = defineProps({
})
const filterVersions = ref([])
const filterLoader = ref(props.instance ? [props.instance?.metadata?.loader] : [])
const filterGameVersions = ref(props.instance ? [props.instance?.metadata?.game_version] : [])
const filterLoader = ref(props.instance ? [props.instance?.loader] : [])
const filterGameVersions = ref(props.instance ? [props.instance?.game_version] : [])
const currentPage = ref(1)