You've already forked AstralRinth
forked from didirus/AstralRinth
Merge commit '81ec068747a39e927c42273011252daaa58f1e14' into feature-clean
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -31,19 +31,21 @@ window.addEventListener('online', () => {
|
||||
const getInstances = async () => {
|
||||
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)
|
||||
recentInstances.value = profiles
|
||||
.filter((x) => x.last_played)
|
||||
.sort((a, b) => {
|
||||
const dateA = dayjs(a.last_played)
|
||||
const dateB = dayjs(b.last_played)
|
||||
|
||||
if (dateA.isSame(dateB)) {
|
||||
return a.name.localeCompare(b.name)
|
||||
}
|
||||
if (dateA.isSame(dateB)) {
|
||||
return a.name.localeCompare(b.name)
|
||||
}
|
||||
|
||||
return dateB - dateA
|
||||
})
|
||||
return dateB - dateA
|
||||
})
|
||||
|
||||
const filters = []
|
||||
for (const instance of recentInstances.value) {
|
||||
for (const instance of profiles) {
|
||||
if (instance.linked_data && instance.linked_data.project_id) {
|
||||
filters.push(`NOT"project_id"="${instance.linked_data.project_id}"`)
|
||||
}
|
||||
@@ -99,37 +101,34 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<RowDisplay v-if="total > 0" :instances="[
|
||||
{
|
||||
label: 'Jump back in',
|
||||
route: '/library',
|
||||
instances: recentInstances,
|
||||
instance: true,
|
||||
downloaded: true,
|
||||
},
|
||||
{
|
||||
label: 'Popular packs',
|
||||
route: '/browse/modpack',
|
||||
instances: featuredModpacks,
|
||||
downloaded: false,
|
||||
},
|
||||
{
|
||||
label: 'Popular mods',
|
||||
route: '/browse/mod',
|
||||
instances: featuredMods,
|
||||
downloaded: false,
|
||||
},
|
||||
]" :can-paginate="true" />
|
||||
<div class="p-6 flex flex-col gap-2">
|
||||
<h1 v-if="recentInstances" class="m-0 text-2xl">Welcome back!</h1>
|
||||
<h1 v-else class="m-0 text-2xl">Welcome to AstralRinth App!</h1>
|
||||
<RowDisplay
|
||||
v-if="total > 0"
|
||||
:instances="[
|
||||
{
|
||||
label: 'Recently played',
|
||||
route: '/library',
|
||||
instances: recentInstances,
|
||||
instance: true,
|
||||
downloaded: true,
|
||||
compact: true,
|
||||
},
|
||||
{
|
||||
label: 'Discover a modpack',
|
||||
route: '/browse/modpack',
|
||||
instances: featuredModpacks,
|
||||
downloaded: false,
|
||||
},
|
||||
{
|
||||
label: 'Discover mods',
|
||||
route: '/browse/mod',
|
||||
instances: featuredMods,
|
||||
downloaded: false,
|
||||
},
|
||||
]"
|
||||
:can-paginate="true"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,669 +0,0 @@
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { LogOutIcon, LogInIcon, BoxIcon, FolderSearchIcon, TrashIcon, PirateShipIcon, UpdatedIcon } from '@modrinth/assets'
|
||||
import { Card, Slider, DropdownSelect, Toggle, Button } from '@modrinth/ui'
|
||||
import { handleError, useTheming } from '@/store/state'
|
||||
import { get, set } from '@/helpers/settings'
|
||||
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'
|
||||
import { optOutAnalytics } from '@/helpers/analytics'
|
||||
import { open } from '@tauri-apps/plugin-dialog'
|
||||
import { getOS } from '@/helpers/utils.js'
|
||||
// import { getVersion } from '@tauri-apps/api/app'
|
||||
import { get_user, purge_cache_types } from '@/helpers/cache.js'
|
||||
|
||||
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
||||
|
||||
import { version, development_build } from '../../package.json'
|
||||
import {
|
||||
getRemote,
|
||||
getBranches,
|
||||
launcherUrl,
|
||||
latestBetaCommitLink,
|
||||
latestBetaCommitTruncatedSha,
|
||||
} from '@/helpers/update.js'
|
||||
|
||||
const pageOptions = ['Home', 'Library']
|
||||
|
||||
const themeStore = useTheming()
|
||||
|
||||
const accessSettings = async () => {
|
||||
const settings = await get()
|
||||
|
||||
settings.launchArgs = settings.extra_launch_args.join(' ')
|
||||
settings.envVars = settings.custom_env_vars.map((x) => x.join('=')).join(' ')
|
||||
|
||||
return settings
|
||||
}
|
||||
|
||||
const fetchSettings = await accessSettings().catch(handleError)
|
||||
|
||||
const settings = ref(fetchSettings)
|
||||
|
||||
const maxMemory = ref(Math.floor((await get_max_memory().catch(handleError)) / 1024))
|
||||
|
||||
watch(
|
||||
settings,
|
||||
async (oldSettings, newSettings) => {
|
||||
if (oldSettings.loaded_config_dir !== newSettings.loaded_config_dir) {
|
||||
return
|
||||
}
|
||||
|
||||
const setSettings = JSON.parse(JSON.stringify(newSettings))
|
||||
|
||||
if (setSettings.telemetry) {
|
||||
// optInAnalytics()
|
||||
} else {
|
||||
optOutAnalytics()
|
||||
}
|
||||
|
||||
setSettings.extra_launch_args = setSettings.launchArgs.trim().split(/\s+/).filter(Boolean)
|
||||
setSettings.custom_env_vars = setSettings.envVars
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.map((x) => x.split('=').filter(Boolean))
|
||||
|
||||
if (!setSettings.hooks.pre_launch) {
|
||||
setSettings.hooks.pre_launch = null
|
||||
}
|
||||
if (!setSettings.hooks.wrapper) {
|
||||
setSettings.hooks.wrapper = null
|
||||
}
|
||||
if (!setSettings.hooks.post_exit) {
|
||||
setSettings.hooks.post_exit = null
|
||||
}
|
||||
|
||||
if (!setSettings.custom_dir) {
|
||||
setSettings.custom_dir = null
|
||||
}
|
||||
|
||||
await set(setSettings)
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
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)
|
||||
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)
|
||||
await fetchCredentials()
|
||||
}
|
||||
|
||||
async function signInAfter() {
|
||||
await fetchCredentials()
|
||||
}
|
||||
|
||||
async function findLauncherDir() {
|
||||
const newDir = await open({
|
||||
multiple: false,
|
||||
directory: true,
|
||||
title: 'Select a new app directory',
|
||||
})
|
||||
|
||||
if (newDir) {
|
||||
settings.value.custom_dir = newDir
|
||||
}
|
||||
}
|
||||
|
||||
async function purgeCache() {
|
||||
await purge_cache_types([
|
||||
'project',
|
||||
'version',
|
||||
'user',
|
||||
'team',
|
||||
'organization',
|
||||
'loader_manifest',
|
||||
'minecraft_manifest',
|
||||
'categories',
|
||||
'report_types',
|
||||
'loaders',
|
||||
'game_versions',
|
||||
'donation_platforms',
|
||||
'file_update',
|
||||
'search_results',
|
||||
]).catch(handleError)
|
||||
}
|
||||
|
||||
await getRemote(false, false)
|
||||
await getBranches()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="settings-page">
|
||||
<Card>
|
||||
<div class="label">
|
||||
<h3>
|
||||
<span class="label__title size-card-header">General settings</span>
|
||||
</h3>
|
||||
</div>
|
||||
<ModrinthLoginScreen ref="loginScreenModal" :callback="signInAfter" />
|
||||
<div class="adjacent-input">
|
||||
<label for="sign-in">
|
||||
<span class="label__title">Manage account</span>
|
||||
<span v-if="credentials" class="label__description">
|
||||
You are currently logged in as {{ credentials.user.username }}.
|
||||
</span>
|
||||
<span v-else> Sign in to your Modrinth account. </span>
|
||||
</label>
|
||||
<button v-if="credentials" id="sign-in" class="btn" @click="logOut">
|
||||
<LogOutIcon />
|
||||
Sign out
|
||||
</button>
|
||||
<button v-else id="sign-in" class="btn" @click="$refs.loginScreenModal.show()">
|
||||
<LogInIcon />
|
||||
Sign in
|
||||
</button>
|
||||
</div>
|
||||
<ConfirmModalWrapper ref="purgeCacheConfirmModal" title="Are you sure you want to purge the cache?"
|
||||
description="If you proceed, your entire cache will be purged. This may slow down the app temporarily."
|
||||
:has-to-type="false" proceed-label="Purge cache" @proceed="purgeCache" />
|
||||
<div class="adjacent-input">
|
||||
<label for="purge-cache">
|
||||
<span class="label__title">App cache</span>
|
||||
<span class="label__description">
|
||||
The Modrinth app stores a cache of data to speed up loading. This can be purged to force
|
||||
the app to reload data. <br />
|
||||
This may slow down the app temporarily.
|
||||
</span>
|
||||
</label>
|
||||
<button id="purge-cache" class="btn" @click="$refs.purgeCacheConfirmModal.show()">
|
||||
<TrashIcon />
|
||||
Purge cache
|
||||
</button>
|
||||
</div>
|
||||
<label for="appDir">
|
||||
<span class="label__title">App directory</span>
|
||||
<span class="label__description">
|
||||
The directory where the launcher stores all of its files. Changes will be applied after
|
||||
restarting the launcher.
|
||||
</span>
|
||||
</label>
|
||||
<div class="app-directory">
|
||||
<div class="iconified-input">
|
||||
<BoxIcon />
|
||||
<input id="appDir" v-model="settings.custom_dir" type="text" class="input" />
|
||||
<Button class="r-btn" @click="findLauncherDir">
|
||||
<FolderSearchIcon />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card>
|
||||
<div class="label">
|
||||
<h3>
|
||||
<span class="label__title size-card-header">Display</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<label for="theme">
|
||||
<span class="label__title">Color theme</span>
|
||||
<span class="label__description">Change the global launcher color theme.</span>
|
||||
</label>
|
||||
<DropdownSelect id="theme" name="Theme dropdown" :options="themeStore.themeOptions"
|
||||
:default-value="settings.theme" :model-value="settings.theme" class="theme-dropdown" @change="(e) => {
|
||||
themeStore.setThemeState(e.option.toLowerCase())
|
||||
settings.theme = themeStore.selectedTheme
|
||||
}
|
||||
" />
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<label for="advanced-rendering">
|
||||
<span class="label__title">Advanced rendering</span>
|
||||
<span class="label__description">
|
||||
Enables advanced rendering such as blur effects that may cause performance issues
|
||||
without hardware-accelerated rendering.
|
||||
</span>
|
||||
</label>
|
||||
<Toggle id="advanced-rendering" :model-value="themeStore.advancedRendering"
|
||||
:checked="themeStore.advancedRendering" @update:model-value="(e) => {
|
||||
themeStore.advancedRendering = e
|
||||
settings.advanced_rendering = themeStore.advancedRendering
|
||||
}
|
||||
" />
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<label for="minimize-launcher">
|
||||
<span class="label__title">Minimize launcher</span>
|
||||
<span class="label__description">Minimize the launcher when a Minecraft process starts.</span>
|
||||
</label>
|
||||
<Toggle id="minimize-launcher" :model-value="settings.hide_on_process_start"
|
||||
:checked="settings.hide_on_process_start" @update:model-value="(e) => {
|
||||
settings.hide_on_process_start = e
|
||||
}
|
||||
" />
|
||||
</div>
|
||||
<div v-if="getOS() != 'MacOS'" class="adjacent-input">
|
||||
<label for="native-decorations">
|
||||
<span class="label__title">Native decorations</span>
|
||||
<span class="label__description">Use system window frame (app restart required).</span>
|
||||
</label>
|
||||
<Toggle id="native-decorations" :model-value="settings.native_decorations"
|
||||
:checked="settings.native_decorations" @update:model-value="(e) => {
|
||||
settings.native_decorations = e
|
||||
}
|
||||
" />
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<label for="opening-page">
|
||||
<span class="label__title">Default landing page</span>
|
||||
<span class="label__description">Change the page to which the launcher opens on.</span>
|
||||
</label>
|
||||
<DropdownSelect id="opening-page" name="Opening page dropdown" :options="pageOptions"
|
||||
:default-value="settings.default_page" :model-value="settings.default_page" class="opening-page" @change="(e) => {
|
||||
settings.default_page = e.option
|
||||
}
|
||||
" />
|
||||
</div>
|
||||
</Card>
|
||||
<Card>
|
||||
<div class="label">
|
||||
<h3>
|
||||
<span class="label__title size-card-header">Resource management</span>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<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. (app restart required to take
|
||||
effect)
|
||||
</span>
|
||||
</label>
|
||||
<Slider id="max-downloads" v-model="settings.max_concurrent_downloads" :min="1" :max="10" :step="1" />
|
||||
</div>
|
||||
|
||||
<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. (app restart required to take
|
||||
effect)
|
||||
</span>
|
||||
</label>
|
||||
<Slider id="max-writes" v-model="settings.max_concurrent_writes" :min="1" :max="50" :step="1" />
|
||||
</div>
|
||||
</Card>
|
||||
<Card>
|
||||
<div class="label">
|
||||
<h3>
|
||||
<span class="label__title size-card-header">Privacy</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<label for="opt-out-analytics">
|
||||
<span class="label__title">Personalized ads</span>
|
||||
<span class="label__description">
|
||||
Modrinth's ad provider, Aditude, shows ads based on your preferences. By disabling this
|
||||
option, you opt out and ads will no longer be shown based on your interests.
|
||||
</span>
|
||||
</label>
|
||||
<Toggle
|
||||
id="opt-out-analytics"
|
||||
:model-value="settings.personalized_ads"
|
||||
:checked="settings.personalized_ads"
|
||||
@update:model-value="
|
||||
(e) => {
|
||||
settings.personalized_ads = e
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<label for="opt-out-analytics">
|
||||
<span class="label__title">Telemetry</span>
|
||||
<span class="label__description">
|
||||
(Always disabled by AstralRinth) • Modrinth collects anonymized analytics and usage data to improve our user experience and
|
||||
customize your experience. By disabling this option, you opt out and your data will no
|
||||
longer be collected.
|
||||
</span>
|
||||
</label>
|
||||
<Toggle id="opt-out-analytics" :model-value="settings.telemetry" :disabled="!settings.telemetry" :checked="settings.telemetry"
|
||||
@update:model-value="(e) => {
|
||||
settings.telemetry = e
|
||||
}
|
||||
" />
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<label for="disable-discord-rpc">
|
||||
<span class="label__title">Discord RPC</span>
|
||||
<span class="label__description">
|
||||
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.discord_rpc" :checked="settings.discord_rpc" />
|
||||
</div>
|
||||
</Card>
|
||||
<Card>
|
||||
<div class="label">
|
||||
<h3>
|
||||
<span class="label__title size-card-header">Java settings</span>
|
||||
</h3>
|
||||
</div>
|
||||
<template v-for="javaVersion in [21, 17, 8]" :key="`java-${javaVersion}`">
|
||||
<label :for="'java-' + javaVersion">
|
||||
<span class="label__title">Java {{ javaVersion }} location</span>
|
||||
</label>
|
||||
<JavaSelector
|
||||
:id="'java-selector-' + javaVersion"
|
||||
v-model="javaVersions[javaVersion]"
|
||||
:version="javaVersion"
|
||||
@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.launchArgs" autocomplete="off" type="text" class="installation-input"
|
||||
placeholder="Enter java arguments..." />
|
||||
<label for="env-vars">
|
||||
<span class="label__title">Environmental variables</span>
|
||||
</label>
|
||||
<input id="env-vars" v-model="settings.envVars" autocomplete="off" type="text" class="installation-input"
|
||||
placeholder="Enter environmental variables..." />
|
||||
<hr class="card-divider" />
|
||||
<div class="adjacent-input">
|
||||
<label for="max-memory">
|
||||
<span class="label__title">Java memory</span>
|
||||
<span class="label__description">
|
||||
The memory allocated to each instance when it is ran.
|
||||
</span>
|
||||
</label>
|
||||
<Slider
|
||||
id="max-memory"
|
||||
v-model="settings.memory.maximum"
|
||||
:min="8"
|
||||
:max="maxMemory"
|
||||
:step="64"
|
||||
unit="MB"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
<Card>
|
||||
<div class="label">
|
||||
<h3>
|
||||
<span class="label__title size-card-header">Hooks</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<label for="pre-launch">
|
||||
<span class="label__title">Pre launch</span>
|
||||
<span class="label__description"> Ran before the instance is launched. </span>
|
||||
</label>
|
||||
<input id="pre-launch" v-model="settings.hooks.pre_launch" autocomplete="off" type="text"
|
||||
placeholder="Enter pre-launch command..." />
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<label for="wrapper">
|
||||
<span class="label__title">Wrapper</span>
|
||||
<span class="label__description"> Wrapper command for launching Minecraft. </span>
|
||||
</label>
|
||||
<input id="wrapper" v-model="settings.hooks.wrapper" autocomplete="off" type="text"
|
||||
placeholder="Enter wrapper command..." />
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<label for="post-exit">
|
||||
<span class="label__title">Post exit</span>
|
||||
<span class="label__description"> Ran after the game closes. </span>
|
||||
</label>
|
||||
<input id="post-exit" v-model="settings.hooks.post_exit" autocomplete="off" type="text"
|
||||
placeholder="Enter post-exit command..." />
|
||||
</div>
|
||||
</Card>
|
||||
<Card>
|
||||
<div class="label">
|
||||
<h3>
|
||||
<span class="label__title size-card-header">Window size</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<label for="fullscreen">
|
||||
<span class="label__title">Fullscreen</span>
|
||||
<span class="label__description">
|
||||
Overwrites the options.txt file to start in full screen when launched.
|
||||
</span>
|
||||
</label>
|
||||
<Toggle id="fullscreen" :model-value="settings.force_fullscreen" :checked="settings.force_fullscreen"
|
||||
@update:model-value="(e) => {
|
||||
settings.force_fullscreen = e
|
||||
}
|
||||
" />
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<label for="width">
|
||||
<span class="label__title">Width</span>
|
||||
<span class="label__description"> The width of the game window when launched. </span>
|
||||
</label>
|
||||
<input id="width" v-model="settings.game_resolution[0]" :disabled="settings.force_fullscreen" autocomplete="off"
|
||||
type="number" placeholder="Enter width..." />
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<label for="height">
|
||||
<span class="label__title">Height</span>
|
||||
<span class="label__description"> The height of the game window when launched. </span>
|
||||
</label>
|
||||
<input id="height" v-model="settings.game_resolution[1]" :disabled="settings.force_fullscreen"
|
||||
autocomplete="off" type="number" class="input" placeholder="Enter height..." />
|
||||
</div>
|
||||
</Card>
|
||||
<Card>
|
||||
<div class="label inline-fix">
|
||||
<h3>
|
||||
<span class="label__title size-card-header in"
|
||||
> About
|
||||
<p v-if="development_build" class="development option">
|
||||
You are using a development version, there may be errors.
|
||||
</p>
|
||||
</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div>
|
||||
<label>
|
||||
<span class="label__title inl">AstralRinth <PirateShipIcon /> Version • {{ version }}</span>
|
||||
|
||||
<span class="label__description"
|
||||
>Latest beta commit •
|
||||
<a class="github" :href="latestBetaCommitLink">{{
|
||||
latestBetaCommitTruncatedSha
|
||||
}}</a></span
|
||||
>
|
||||
<span class="label__description"
|
||||
>All latest versions always published on GitHub
|
||||
<a class="github" :href="launcherUrl">Our GitHub repository</a></span
|
||||
>
|
||||
|
||||
<span class="label__title">Update Checker</span>
|
||||
|
||||
<span class="label__description"
|
||||
>Version on remote server •
|
||||
<p id="releaseData" class="cosmic inline-fix"></p>
|
||||
</span>
|
||||
<span class="label__description"
|
||||
>Version on local device •
|
||||
<p class="cosmic inline-fix">v{{ version }}</p></span
|
||||
>
|
||||
</label>
|
||||
<div class="inline-item-group">
|
||||
<Button icon-only @click="getRemote(false, false), getBranches()">
|
||||
<UpdatedIcon /> Check for updates
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.settings-page {
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.installation-input {
|
||||
width: 100% !important;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.theme-dropdown {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.card-divider {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.app-directory {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--gap-sm);
|
||||
|
||||
.iconified-input {
|
||||
flex-grow: 1;
|
||||
|
||||
input {
|
||||
flex-basis: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.development {
|
||||
color: #ff6a00;
|
||||
text-decoration: none;
|
||||
text-shadow:
|
||||
0 0 4px rgba(79, 173, 255, 0.5),
|
||||
0 0 8px rgba(14, 98, 204, 0.5),
|
||||
0 0 12px rgba(122, 31, 199, 0.5);
|
||||
transition: color 1.5s ease;
|
||||
}
|
||||
.development:hover,
|
||||
.development:focus,
|
||||
.development:active {
|
||||
color: #4800d3;
|
||||
text-shadow: #801313;
|
||||
}
|
||||
|
||||
.cosmic {
|
||||
color: #3e8cde;
|
||||
text-decoration: none;
|
||||
text-shadow:
|
||||
0 0 4px rgba(79, 173, 255, 0.5),
|
||||
0 0 8px rgba(14, 98, 204, 0.5),
|
||||
0 0 12px rgba(122, 31, 199, 0.5);
|
||||
transition: color 0.35s ease;
|
||||
}
|
||||
.cosmic:hover,
|
||||
.cosmic:focus,
|
||||
.cosmic:active {
|
||||
color: #10fae5;
|
||||
text-shadow: #26065e;
|
||||
}
|
||||
|
||||
.download {
|
||||
color: #3e8cde;
|
||||
border: none;
|
||||
padding: var(--gap-sm) var(--gap-lg);
|
||||
//background-color: rgba(0, 0, 0, 0.0);
|
||||
text-decoration: none;
|
||||
text-shadow:
|
||||
0 0 4px rgba(79, 173, 255, 0.5),
|
||||
0 0 8px rgba(14, 98, 204, 0.5),
|
||||
0 0 12px rgba(122, 31, 199, 0.5);
|
||||
transition: color 0.35s ease;
|
||||
}
|
||||
.download:hover,
|
||||
.download:focus,
|
||||
.download:active {
|
||||
color: #10fae5;
|
||||
text-shadow: #26065e;
|
||||
}
|
||||
|
||||
a.github {
|
||||
color: #3e8cde;
|
||||
text-decoration: none;
|
||||
text-shadow:
|
||||
0 0 4px rgba(79, 173, 255, 0.5),
|
||||
0 0 8px rgba(14, 98, 204, 0.5),
|
||||
0 0 12px rgba(122, 31, 199, 0.5);
|
||||
transition: color 0.35s ease;
|
||||
}
|
||||
|
||||
a.github:hover,
|
||||
a.github:focus,
|
||||
a.github:active {
|
||||
color: #10fae5;
|
||||
text-shadow: #26065e;
|
||||
}
|
||||
|
||||
.inline-item-group {
|
||||
display: inline-flex;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.inline-fix {
|
||||
display: inline-flex;
|
||||
margin-top: -2rem;
|
||||
margin-bottom: -2rem;
|
||||
}
|
||||
|
||||
.download-modal {
|
||||
color: #3e8cde;
|
||||
padding: var(--gap-sm) var(--gap-lg);
|
||||
text-decoration: none;
|
||||
text-shadow:
|
||||
0 0 4px rgba(79, 173, 255, 0.5),
|
||||
0 0 8px rgba(14, 98, 204, 0.5),
|
||||
0 0 12px rgba(122, 31, 199, 0.5);
|
||||
transition: color 0.35s ease;
|
||||
}
|
||||
.download-modal:hover,
|
||||
.download-modal:focus,
|
||||
.download-modal:active {
|
||||
color: #10fae5;
|
||||
text-shadow: #26065e;
|
||||
}
|
||||
|
||||
.option {
|
||||
background: var(--color-bg);
|
||||
border-radius: var(--radius-lg);
|
||||
width: auto;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-top: auto;
|
||||
margin-left: 0.5rem;
|
||||
font-size: 1rem;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,4 @@
|
||||
import Index from './Index.vue'
|
||||
import Browse from './Browse.vue'
|
||||
import Library from './Library.vue'
|
||||
import Settings from './Settings.vue'
|
||||
|
||||
export { Index, Browse, Library, Settings }
|
||||
export { Index, Browse }
|
||||
|
||||
@@ -1,97 +1,129 @@
|
||||
<template>
|
||||
<div class="instance-container">
|
||||
<div class="side-cards pb-4" @scroll="$refs.promo.scroll()">
|
||||
<Card class="instance-card" @contextmenu.prevent.stop="handleRightClick">
|
||||
<Avatar size="md" :src="instance.icon_path ? convertFileSrc(instance.icon_path) : null" />
|
||||
<div class="instance-info">
|
||||
<h2 class="name">{{ instance.name }}</h2>
|
||||
<span class="metadata"> {{ instance.loader }} {{ instance.game_version }} </span>
|
||||
<div
|
||||
class="p-6 pr-2 pb-4"
|
||||
@contextmenu.prevent.stop="(event) => handleRightClick(event, instance.path)"
|
||||
>
|
||||
<ExportModal ref="exportModal" :instance="instance" />
|
||||
<InstanceSettingsModal ref="settingsModal" :instance="instance" :offline="offline" />
|
||||
<ContentPageHeader>
|
||||
<template #icon>
|
||||
<Avatar :src="icon" :alt="instance.name" size="96px" :tint-by="instance.path" />
|
||||
</template>
|
||||
<template #title>
|
||||
{{ instance.name }}
|
||||
</template>
|
||||
<template #summary> </template>
|
||||
<template #stats>
|
||||
<div
|
||||
class="flex items-center gap-2 font-semibold transform capitalize border-0 border-solid border-divider pr-4 md:border-r"
|
||||
>
|
||||
<GameIcon class="h-6 w-6 text-secondary" />
|
||||
{{ instance.loader }} {{ instance.game_version }}
|
||||
</div>
|
||||
<span class="button-group">
|
||||
<Button v-if="instance.install_stage !== 'installed'" disabled class="instance-button">
|
||||
Installing...
|
||||
</Button>
|
||||
<Button
|
||||
v-else-if="playing === true"
|
||||
color="danger"
|
||||
class="instance-button"
|
||||
@click="stopInstance('InstancePage')"
|
||||
>
|
||||
<StopCircleIcon />
|
||||
Stop
|
||||
</Button>
|
||||
<Button
|
||||
<div class="flex items-center gap-2 font-semibold">
|
||||
<TimerIcon class="h-6 w-6 text-secondary" />
|
||||
<template v-if="timePlayed > 0">
|
||||
{{ timePlayedHumanized }}
|
||||
</template>
|
||||
<template v-else> Never played </template>
|
||||
</div>
|
||||
</template>
|
||||
<template #actions>
|
||||
<div class="flex gap-2">
|
||||
<ButtonStyled v-if="instance.install_stage !== 'installed'" color="brand" size="large">
|
||||
<button disabled>Installing...</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else-if="playing === true" color="red" size="large">
|
||||
<button @click="stopInstance('InstancePage')">
|
||||
<StopCircleIcon />
|
||||
Stop
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled
|
||||
v-else-if="playing === false && loading === false"
|
||||
color="primary"
|
||||
class="instance-button"
|
||||
@click="startInstance('InstancePage')"
|
||||
color="brand"
|
||||
size="large"
|
||||
>
|
||||
<PlayIcon />
|
||||
Play
|
||||
</Button>
|
||||
<Button
|
||||
<button @click="startInstance('InstancePage')">
|
||||
<PlayIcon />
|
||||
Play
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled
|
||||
v-else-if="loading === true && playing === false"
|
||||
disabled
|
||||
class="instance-button"
|
||||
color="brand"
|
||||
size="large"
|
||||
>
|
||||
Loading...
|
||||
</Button>
|
||||
<Button
|
||||
v-tooltip="'Open instance folder'"
|
||||
class="instance-button"
|
||||
@click="showProfileInFolder(instance.path)"
|
||||
>
|
||||
<FolderOpenIcon />
|
||||
Folder
|
||||
</Button>
|
||||
</span>
|
||||
<hr class="card-divider" />
|
||||
<div class="pages-list">
|
||||
<RouterLink :to="`/instance/${encodeURIComponent($route.params.id)}/`" class="btn">
|
||||
<BoxIcon />
|
||||
Content
|
||||
</RouterLink>
|
||||
<RouterLink :to="`/instance/${encodeURIComponent($route.params.id)}/logs`" class="btn">
|
||||
<FileIcon />
|
||||
Logs
|
||||
</RouterLink>
|
||||
<RouterLink :to="`/instance/${encodeURIComponent($route.params.id)}/options`" class="btn">
|
||||
<SettingsIcon />
|
||||
Options
|
||||
</RouterLink>
|
||||
<button disabled>Loading...</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled size="large" circular>
|
||||
<button v-tooltip="'Instance settings'" @click="settingsModal.show()">
|
||||
<SettingsIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled size="large" type="transparent" circular>
|
||||
<OverflowMenu
|
||||
:options="[
|
||||
{
|
||||
id: 'open-folder',
|
||||
action: () => showProfileInFolder(instance.path),
|
||||
},
|
||||
{
|
||||
id: 'export-mrpack',
|
||||
action: () => $refs.exportModal.show(),
|
||||
},
|
||||
]"
|
||||
>
|
||||
<MoreVerticalIcon />
|
||||
<template #share-instance> <UserPlusIcon /> Share instance </template>
|
||||
<template #host-a-server> <ServerIcon /> Create a server </template>
|
||||
<template #open-folder> <FolderOpenIcon /> Open folder </template>
|
||||
<template #export-mrpack> <PackageIcon /> Export modpack </template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<div class="content">
|
||||
<RouterView v-slot="{ Component }">
|
||||
<template v-if="Component">
|
||||
<Suspense @pending="loadingBar.startLoading()" @resolve="loadingBar.stopLoading()">
|
||||
<component
|
||||
:is="Component"
|
||||
:instance="instance"
|
||||
:options="options"
|
||||
:offline="offline"
|
||||
:playing="playing"
|
||||
:versions="modrinthVersions"
|
||||
:installed="instance.install_stage !== 'installed'"
|
||||
></component>
|
||||
</Suspense>
|
||||
</template>
|
||||
</RouterView>
|
||||
</div>
|
||||
</template>
|
||||
</ContentPageHeader>
|
||||
</div>
|
||||
<div class="px-6">
|
||||
<NavTabs :links="tabs" />
|
||||
</div>
|
||||
<div class="p-6 pt-4">
|
||||
<RouterView v-slot="{ Component }" :key="instance.path">
|
||||
<template v-if="Component">
|
||||
<Suspense
|
||||
:key="instance.path"
|
||||
@pending="loadingBar.startLoading()"
|
||||
@resolve="loadingBar.stopLoading()"
|
||||
>
|
||||
<component
|
||||
:is="Component"
|
||||
:instance="instance"
|
||||
:options="options"
|
||||
:offline="offline"
|
||||
:playing="playing"
|
||||
:versions="modrinthVersions"
|
||||
:installed="instance.install_stage !== 'installed'"
|
||||
></component>
|
||||
<template #fallback>
|
||||
<LoadingIndicator />
|
||||
</template>
|
||||
</Suspense>
|
||||
</template>
|
||||
</RouterView>
|
||||
</div>
|
||||
<ContextMenu ref="options" @option-clicked="handleOptionsClick">
|
||||
<template #play> <PlayIcon /> Play </template>
|
||||
<template #stop> <StopCircleIcon /> Stop </template>
|
||||
<template #add_content> <PlusIcon /> Add Content </template>
|
||||
<template #add_content> <PlusIcon /> Add content </template>
|
||||
<template #edit> <EditIcon /> Edit </template>
|
||||
<template #copy_path> <ClipboardCopyIcon /> Copy Path </template>
|
||||
<template #open_folder> <ClipboardCopyIcon /> Open Folder </template>
|
||||
<template #copy_link> <ClipboardCopyIcon /> Copy Link </template>
|
||||
<template #open_link> <ClipboardCopyIcon /> Open In Modrinth <ExternalIcon /> </template>
|
||||
<template #copy_path> <ClipboardCopyIcon /> Copy path </template>
|
||||
<template #open_folder> <ClipboardCopyIcon /> Open folder </template>
|
||||
<template #copy_link> <ClipboardCopyIcon /> Copy link </template>
|
||||
<template #open_link> <ClipboardCopyIcon /> Open in Modrinth <ExternalIcon /> </template>
|
||||
<template #copy_names><EditIcon />Copy names</template>
|
||||
<template #copy_slugs><HashIcon />Copy slugs</template>
|
||||
<template #copy_links><GlobeIcon />Copy Links</template>
|
||||
<template #copy_links><GlobeIcon />Copy links</template>
|
||||
<template #toggle><EditIcon />Toggle selected</template>
|
||||
<template #disable><XIcon />Disable selected</template>
|
||||
<template #enable><CheckCircleIcon />Enable selected</template>
|
||||
@@ -103,11 +135,18 @@
|
||||
</ContextMenu>
|
||||
</template>
|
||||
<script setup>
|
||||
import { Button, Avatar, Card } from '@modrinth/ui'
|
||||
import {
|
||||
BoxIcon,
|
||||
Avatar,
|
||||
ContentPageHeader,
|
||||
ButtonStyled,
|
||||
OverflowMenu,
|
||||
LoadingIndicator,
|
||||
} from '@modrinth/ui'
|
||||
import {
|
||||
UserPlusIcon,
|
||||
ServerIcon,
|
||||
PackageIcon,
|
||||
SettingsIcon,
|
||||
FileIcon,
|
||||
PlayIcon,
|
||||
StopCircleIcon,
|
||||
EditIcon,
|
||||
@@ -121,27 +160,94 @@ import {
|
||||
XIcon,
|
||||
CheckCircleIcon,
|
||||
UpdatedIcon,
|
||||
MoreVerticalIcon,
|
||||
GameIcon,
|
||||
TimerIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { get, kill, run } from '@/helpers/profile'
|
||||
import { get, get_full_path, 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 { ref, onUnmounted, computed, watch } from 'vue'
|
||||
import { handleError, useBreadcrumbs, useLoading } from '@/store/state'
|
||||
import { showProfileInFolder } from '@/helpers/utils.js'
|
||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||
import NavTabs from '@/components/ui/NavTabs.vue'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import { handleSevereError } from '@/store/error.js'
|
||||
import { get_project, get_version_many } from '@/helpers/cache.js'
|
||||
import dayjs from 'dayjs'
|
||||
import duration from 'dayjs/plugin/duration'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import ExportModal from '@/components/ui/ExportModal.vue'
|
||||
import InstanceSettingsModal from '@/components/ui/modal/InstanceSettingsModal.vue'
|
||||
|
||||
dayjs.extend(duration)
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const router = useRouter()
|
||||
const breadcrumbs = useBreadcrumbs()
|
||||
|
||||
const instance = ref(await get(route.params.id).catch(handleError))
|
||||
const offline = ref(!navigator.onLine)
|
||||
window.addEventListener('offline', () => {
|
||||
offline.value = true
|
||||
})
|
||||
window.addEventListener('online', () => {
|
||||
offline.value = false
|
||||
})
|
||||
|
||||
const instance = ref()
|
||||
const modrinthVersions = ref([])
|
||||
const playing = ref(false)
|
||||
const loading = ref(false)
|
||||
|
||||
async function fetchInstance() {
|
||||
instance.value = await get(route.params.id).catch(handleError)
|
||||
|
||||
if (!offline.value && instance.value.linked_data && instance.value.linked_data.project_id) {
|
||||
get_project(instance.value.linked_data.project_id, 'must_revalidate')
|
||||
.catch(handleError)
|
||||
.then((project) => {
|
||||
if (project && project.versions) {
|
||||
get_version_many(project.versions, 'must_revalidate')
|
||||
.catch(handleError)
|
||||
.then((versions) => {
|
||||
modrinthVersions.value = versions.sort(
|
||||
(a, b) => dayjs(b.date_published) - dayjs(a.date_published),
|
||||
)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const runningProcesses = await get_by_profile_path(route.params.id).catch(handleError)
|
||||
|
||||
playing.value = runningProcesses.length > 0
|
||||
}
|
||||
|
||||
await fetchInstance()
|
||||
watch(
|
||||
() => route.params.id,
|
||||
async () => {
|
||||
if (route.params.id && route.path.startsWith('/instance')) {
|
||||
await fetchInstance()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
const tabs = computed(() => [
|
||||
{
|
||||
label: 'Content',
|
||||
href: `/instance/${encodeURIComponent(route.params.id)}`,
|
||||
},
|
||||
{
|
||||
label: 'Logs',
|
||||
href: `/instance/${encodeURIComponent(route.params.id)}/logs`,
|
||||
},
|
||||
])
|
||||
|
||||
breadcrumbs.setName(
|
||||
'Instance',
|
||||
@@ -156,18 +262,8 @@ breadcrumbs.setContext({
|
||||
query: route.query,
|
||||
})
|
||||
|
||||
const offline = ref(!navigator.onLine)
|
||||
window.addEventListener('offline', () => {
|
||||
offline.value = true
|
||||
})
|
||||
window.addEventListener('online', () => {
|
||||
offline.value = false
|
||||
})
|
||||
|
||||
const loadingBar = useLoading()
|
||||
|
||||
const playing = ref(false)
|
||||
const loading = ref(false)
|
||||
const options = ref(null)
|
||||
|
||||
const startInstance = async (context) => {
|
||||
@@ -187,32 +283,6 @@ const startInstance = async (context) => {
|
||||
})
|
||||
}
|
||||
|
||||
const checkProcess = async () => {
|
||||
const runningProcesses = await get_by_profile_path(route.params.id).catch(handleError)
|
||||
|
||||
playing.value = runningProcesses.length > 0
|
||||
}
|
||||
|
||||
// Get information on associated modrinth versions, if any
|
||||
const modrinthVersions = ref([])
|
||||
if (!offline.value && instance.value.linked_data && instance.value.linked_data.project_id) {
|
||||
get_project(instance.value.linked_data.project_id, 'must_revalidate')
|
||||
.catch(handleError)
|
||||
.then((project) => {
|
||||
if (project && project.versions) {
|
||||
get_version_many(project.versions, 'must_revalidate')
|
||||
.catch(handleError)
|
||||
.then((versions) => {
|
||||
modrinthVersions.value = versions.sort(
|
||||
(a, b) => dayjs(b.date_published) - dayjs(a.date_published),
|
||||
)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
await checkProcess()
|
||||
|
||||
const stopInstance = async (context) => {
|
||||
playing.value = false
|
||||
await kill(route.params.id).catch(handleError)
|
||||
@@ -276,9 +346,11 @@ const handleOptionsClick = async (args) => {
|
||||
case 'open_folder':
|
||||
await showProfileInFolder(instance.value.path)
|
||||
break
|
||||
case 'copy_path':
|
||||
await navigator.clipboard.writeText(instance.value.path)
|
||||
case 'copy_path': {
|
||||
const fullPath = await get_full_path(instance.value.path)
|
||||
await navigator.clipboard.writeText(fullPath)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,7 +367,35 @@ const unlistenProfiles = await profile_listener(async (event) => {
|
||||
})
|
||||
|
||||
const unlistenProcesses = await process_listener((e) => {
|
||||
if (e.event === 'finished' && e.profile_path_id === route.params.id) playing.value = false
|
||||
if (e.event === 'finished' && e.profile_path_id === route.params.id) {
|
||||
playing.value = false
|
||||
}
|
||||
})
|
||||
|
||||
const icon = computed(() =>
|
||||
instance.value.icon_path ? convertFileSrc(instance.value.icon_path) : null,
|
||||
)
|
||||
|
||||
const settingsModal = ref()
|
||||
|
||||
const timePlayed = computed(() => {
|
||||
return instance.value.recent_time_played + instance.value.submitted_time_played
|
||||
})
|
||||
|
||||
const timePlayedHumanized = computed(() => {
|
||||
const duration = dayjs.duration(timePlayed.value, 'seconds')
|
||||
const hours = Math.floor(duration.asHours())
|
||||
if (hours >= 1) {
|
||||
return hours + ' hour' + (hours > 1 ? 's' : '')
|
||||
}
|
||||
|
||||
const minutes = Math.floor(duration.asMinutes())
|
||||
if (minutes >= 1) {
|
||||
return minutes + ' minute' + (minutes > 1 ? 's' : '')
|
||||
}
|
||||
|
||||
const seconds = Math.floor(duration.asSeconds())
|
||||
return seconds + ' second' + (seconds > 1 ? 's' : '')
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,974 +0,0 @@
|
||||
<template>
|
||||
<ConfirmModalWrapper
|
||||
ref="modal_confirm"
|
||||
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"
|
||||
@proceed="removeProfile"
|
||||
/>
|
||||
<ModalWrapper ref="modalConfirmUnlock" header="Are you sure you want to unlock this instance?">
|
||||
<div class="modal-delete">
|
||||
<div
|
||||
class="markdown-body"
|
||||
v-html="
|
||||
'If you proceed, you will not be able to re-lock it without using the `Reinstall modpack` button.'
|
||||
"
|
||||
/>
|
||||
<div class="input-group push-right">
|
||||
<button class="btn" @click="$refs.modalConfirmUnlock.hide()">
|
||||
<XIcon />
|
||||
Cancel
|
||||
</button>
|
||||
<button class="btn btn-danger" @click="unlockProfile">
|
||||
<LockIcon />
|
||||
Unlock
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
|
||||
<ModalWrapper ref="modalConfirmUnpair" header="Are you sure you want to unpair this instance?">
|
||||
<div class="modal-delete">
|
||||
<div
|
||||
class="markdown-body"
|
||||
v-html="
|
||||
'If you proceed, you will not be able to re-pair it without creating an entirely new instance.'
|
||||
"
|
||||
/>
|
||||
<div class="input-group push-right">
|
||||
<button class="btn" @click="$refs.modalConfirmUnpair.hide()">
|
||||
<XIcon />
|
||||
Cancel
|
||||
</button>
|
||||
<button class="btn btn-danger" @click="unpairProfile">
|
||||
<XIcon />
|
||||
Unpair
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
|
||||
<ModalWrapper ref="changeVersionsModal" header="Change instance versions">
|
||||
<div class="change-versions-modal universal-body">
|
||||
<div class="input-row">
|
||||
<p class="input-label">Loader</p>
|
||||
<Chips v-model="loader" :items="loaders" :never-empty="false" />
|
||||
</div>
|
||||
<div class="input-row">
|
||||
<p class="input-label">Game Version</p>
|
||||
<div class="versions">
|
||||
<DropdownSelect
|
||||
v-model="gameVersion"
|
||||
:options="selectableGameVersions"
|
||||
name="Game Version Dropdown"
|
||||
render-up
|
||||
/>
|
||||
<Checkbox v-model="showSnapshots" class="filter-checkbox" label="Include snapshots" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="loader !== 'vanilla'" class="input-row">
|
||||
<p class="input-label">Loader Version</p>
|
||||
<DropdownSelect
|
||||
:model-value="selectableLoaderVersions[loaderVersionIndex]"
|
||||
:options="selectableLoaderVersions"
|
||||
:display-name="(option) => option?.id"
|
||||
name="Version selector"
|
||||
render-up
|
||||
@change="(value) => (loaderVersionIndex = value.index)"
|
||||
/>
|
||||
</div>
|
||||
<div class="push-right input-group">
|
||||
<button class="btn" @click="$refs.changeVersionsModal.hide()">
|
||||
<XIcon />
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
:disabled="!isValid || !isChanged || editing"
|
||||
@click="saveGvLoaderEdits()"
|
||||
>
|
||||
<SaveIcon />
|
||||
{{ editing ? 'Saving...' : 'Save changes' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
<section class="card">
|
||||
<div class="label">
|
||||
<h3>
|
||||
<span class="label__title size-card-header">Instance</span>
|
||||
</h3>
|
||||
</div>
|
||||
<label for="instance-icon">
|
||||
<span class="label__title">Icon</span>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<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" class="btn" @click="resetIcon">
|
||||
<TrashIcon />
|
||||
Remove icon
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label for="project-name">
|
||||
<span class="label__title">Name</span>
|
||||
</label>
|
||||
<input
|
||||
id="profile-name"
|
||||
v-model="title"
|
||||
autocomplete="off"
|
||||
maxlength="80"
|
||||
type="text"
|
||||
:disabled="instance.linked_data"
|
||||
/>
|
||||
|
||||
<div class="adjacent-input">
|
||||
<label for="edit-versions">
|
||||
<span class="label__title">Edit mod loader/game versions</span>
|
||||
<span class="label__description">
|
||||
Allows you to change the mod loader, loader version, or game version of the instance.
|
||||
</span>
|
||||
</label>
|
||||
<button
|
||||
id="edit-versions"
|
||||
class="btn"
|
||||
:disabled="offline"
|
||||
@click="$refs.changeVersionsModal.show()"
|
||||
>
|
||||
<EditIcon />
|
||||
Edit versions
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="adjacent-input">
|
||||
<label>
|
||||
<span class="label__title">Categories</span>
|
||||
<span class="label__description">
|
||||
Set the categories of this instance, for display in the library page. This is purely
|
||||
cosmetic.
|
||||
</span>
|
||||
</label>
|
||||
<multiselect
|
||||
v-model="groups"
|
||||
:options="availableGroups"
|
||||
:multiple="true"
|
||||
:searchable="true"
|
||||
:show-no-results="false"
|
||||
:close-on-select="false"
|
||||
:clear-search-on-select="false"
|
||||
:show-labels="false"
|
||||
:taggable="true"
|
||||
tag-placeholder="Add new category"
|
||||
placeholder="Select categories..."
|
||||
@tag="
|
||||
(newTag) => {
|
||||
groups.push(newTag.trim().substring(0, 32))
|
||||
availableGroups.push(newTag.trim().substring(0, 32))
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
<Card>
|
||||
<div class="label">
|
||||
<h3>
|
||||
<span class="label__title size-card-header">Java</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="settings-group">
|
||||
<h3>Installation</h3>
|
||||
<Checkbox v-model="overrideJavaInstall" label="Override global java installations" />
|
||||
<JavaSelector v-model="javaInstall" :disabled="!overrideJavaInstall" />
|
||||
</div>
|
||||
<hr class="card-divider" />
|
||||
<div class="settings-group">
|
||||
<h3>Java arguments</h3>
|
||||
<Checkbox v-model="overrideJavaArgs" label="Override global java arguments" />
|
||||
<input
|
||||
id="java-args"
|
||||
v-model="javaArgs"
|
||||
autocomplete="off"
|
||||
:disabled="!overrideJavaArgs"
|
||||
type="text"
|
||||
class="installation-input"
|
||||
placeholder="Enter java arguments..."
|
||||
/>
|
||||
</div>
|
||||
<div class="settings-group">
|
||||
<h3>Environment variables</h3>
|
||||
<Checkbox v-model="overrideEnvVars" label="Override global environment variables" />
|
||||
<input
|
||||
v-model="envVars"
|
||||
autocomplete="off"
|
||||
:disabled="!overrideEnvVars"
|
||||
type="text"
|
||||
class="installation-input"
|
||||
placeholder="Enter environment variables..."
|
||||
/>
|
||||
</div>
|
||||
<hr class="card-divider" />
|
||||
<div class="settings-group">
|
||||
<h3>Java memory</h3>
|
||||
<Checkbox v-model="overrideMemorySettings" label="Override global memory settings" />
|
||||
<Slider
|
||||
v-model="memory.maximum"
|
||||
:disabled="!overrideMemorySettings"
|
||||
:min="8"
|
||||
:max="maxMemory"
|
||||
:step="64"
|
||||
unit="mb"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
<Card>
|
||||
<div class="label">
|
||||
<h3>
|
||||
<span class="label__title size-card-header">Window</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<Checkbox v-model="overrideWindowSettings" label="Override global window settings" />
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<label for="fullscreen">
|
||||
<span class="label__title">Fullscreen</span>
|
||||
<span class="label__description">
|
||||
Make the game start in full screen when launched (using options.txt).
|
||||
</span>
|
||||
</label>
|
||||
<Toggle
|
||||
id="fullscreen"
|
||||
:model-value="fullscreenSetting"
|
||||
:checked="fullscreenSetting"
|
||||
:disabled="!overrideWindowSettings"
|
||||
@update:model-value="
|
||||
(e) => {
|
||||
fullscreenSetting = e
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<label for="width">
|
||||
<span class="label__title">Width</span>
|
||||
<span class="label__description"> The width of the game window when launched. </span>
|
||||
</label>
|
||||
<input
|
||||
id="width"
|
||||
v-model="resolution[0]"
|
||||
autocomplete="off"
|
||||
:disabled="!overrideWindowSettings || fullscreenSetting"
|
||||
type="number"
|
||||
placeholder="Enter width..."
|
||||
/>
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<label for="height">
|
||||
<span class="label__title">Height</span>
|
||||
<span class="label__description"> The height of the game window when launched. </span>
|
||||
</label>
|
||||
<input
|
||||
id="height"
|
||||
v-model="resolution[1]"
|
||||
autocomplete="off"
|
||||
:disabled="!overrideWindowSettings || fullscreenSetting"
|
||||
type="number"
|
||||
class="input"
|
||||
placeholder="Enter height..."
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
<Card>
|
||||
<div class="label">
|
||||
<h3>
|
||||
<span class="label__title size-card-header">Hooks</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<Checkbox v-model="overrideHooks" label="Override global hooks" />
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<label for="pre-launch">
|
||||
<span class="label__title">Pre launch</span>
|
||||
<span class="label__description"> Ran before the instance is launched. </span>
|
||||
</label>
|
||||
<input
|
||||
id="pre-launch"
|
||||
v-model="hooks.pre_launch"
|
||||
autocomplete="off"
|
||||
:disabled="!overrideHooks"
|
||||
type="text"
|
||||
placeholder="Enter pre-launch command..."
|
||||
/>
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<label for="wrapper">
|
||||
<span class="label__title">Wrapper</span>
|
||||
<span class="label__description"> Wrapper command for launching Minecraft. </span>
|
||||
</label>
|
||||
<input
|
||||
id="wrapper"
|
||||
v-model="hooks.wrapper"
|
||||
autocomplete="off"
|
||||
:disabled="!overrideHooks"
|
||||
type="text"
|
||||
placeholder="Enter wrapper command..."
|
||||
/>
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<label for="post-exit">
|
||||
<span class="label__title">Post exit</span>
|
||||
<span class="label__description"> Ran after the game closes. </span>
|
||||
</label>
|
||||
<input
|
||||
id="post-exit"
|
||||
v-model="hooks.post_exit"
|
||||
autocomplete="off"
|
||||
:disabled="!overrideHooks"
|
||||
type="text"
|
||||
placeholder="Enter post-exit command..."
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
<Card v-if="instance.linked_data">
|
||||
<div class="label">
|
||||
<h3>
|
||||
<span class="label__title size-card-header">Modpack</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<label for="general-modpack-info">
|
||||
<span class="label__description"> <strong>Modpack: </strong> {{ instance.name }} </span>
|
||||
<span class="label__description">
|
||||
<strong>Version: </strong>
|
||||
{{
|
||||
installedVersionData?.name != null
|
||||
? installedVersionData.name.charAt(0).toUpperCase() +
|
||||
installedVersionData.name.slice(1)
|
||||
: getLocalVersion(props.instance.path)
|
||||
}}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div v-if="!isPackLocked" class="adjacent-input">
|
||||
<Card class="unlocked-instance">
|
||||
This is an unlocked instance. There may be unexpected behaviour unintended by the modpack
|
||||
creator.
|
||||
</Card>
|
||||
</div>
|
||||
<div v-else class="adjacent-input">
|
||||
<label for="unlock-profile">
|
||||
<span class="label__title">Unlock instance</span>
|
||||
<span class="label__description">
|
||||
Allows modifications to the instance, which allows you to add projects to the modpack. The
|
||||
pack will remain linked, and you can still change versions. Only mods listed in the
|
||||
modpack will be modified on version changes.
|
||||
</span>
|
||||
</label>
|
||||
<Button id="unlock-profile" @click="$refs.modalConfirmUnlock.show()">
|
||||
<LockIcon /> Unlock
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="adjacent-input">
|
||||
<label for="unpair-profile">
|
||||
<span class="label__title">Unpair instance</span>
|
||||
<span class="label__description">
|
||||
Removes the link to an external Modrinth modpack on the instance. This allows you to edit
|
||||
modpacks you download through the browse page but you will not be able to update the
|
||||
instance from a new version of a modpack if you do this.
|
||||
</span>
|
||||
</label>
|
||||
<Button id="unpair-profile" @click="$refs.modalConfirmUnpair.show()">
|
||||
<XIcon /> Unpair
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
Changes to another version of the modpack, allowing upgrading or downgrading. This will
|
||||
replace all files marked as relevant to the modpack.
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<Button
|
||||
id="change-modpack-version"
|
||||
:disabled="inProgress || installing"
|
||||
@click="modpackVersionModal.show()"
|
||||
>
|
||||
<SwapIcon />
|
||||
Change modpack version
|
||||
</Button>
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<label for="repair-modpack">
|
||||
<span class="label__title">Reinstall modpack</span>
|
||||
<span class="label__description">
|
||||
Removes all projects and reinstalls Modrinth modpack. Use this to fix unexpected behaviour
|
||||
if your instance is diverging from the Modrinth modpack. This also re-locks the instance.
|
||||
</span>
|
||||
</label>
|
||||
<Button id="repair-modpack" color="highlight" :disabled="offline" @click="repairModpack">
|
||||
<DownloadIcon /> Reinstall
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
<Card>
|
||||
<div class="label">
|
||||
<h3>
|
||||
<span class="label__title size-card-header">Instance management</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div v-if="instance.install_stage == 'installed'" class="adjacent-input">
|
||||
<label for="duplicate-profile">
|
||||
<span class="label__title">Duplicate instance</span>
|
||||
<span class="label__description">
|
||||
Creates another copy of the instance, including saves, configs, mods, and everything.
|
||||
</span>
|
||||
</label>
|
||||
<Button
|
||||
id="repair-profile"
|
||||
:disabled:="installing || inProgress || offline"
|
||||
@click="duplicateProfile"
|
||||
>
|
||||
<ClipboardCopyIcon /> Duplicate
|
||||
</Button>
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<label for="repair-profile">
|
||||
<span class="label__title">Repair instance</span>
|
||||
<span class="label__description">
|
||||
Reinstalls Minecraft dependencies and checks for corruption. Use this if your game is not
|
||||
launching due to launcher-related errors.
|
||||
</span>
|
||||
</label>
|
||||
<Button
|
||||
id="repair-profile"
|
||||
color="highlight"
|
||||
:disabled="installing || inProgress || repairing || offline"
|
||||
@click="repairProfile(true)"
|
||||
>
|
||||
<HammerIcon /> Repair
|
||||
</Button>
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<label for="delete-profile">
|
||||
<span class="label__title">Delete instance</span>
|
||||
<span class="label__description">
|
||||
Fully removes a instance from the disk. Be careful, as once you delete a instance there is
|
||||
no way to recover it.
|
||||
</span>
|
||||
</label>
|
||||
<Button
|
||||
id="delete-profile"
|
||||
color="danger"
|
||||
:disabled="removing"
|
||||
@click="$refs.modal_confirm.show()"
|
||||
>
|
||||
<TrashIcon /> Delete
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
<ModpackVersionModal
|
||||
v-if="instance.linked_data"
|
||||
ref="modpackVersionModal"
|
||||
:instance="instance"
|
||||
:versions="props.versions"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
TrashIcon,
|
||||
UploadIcon,
|
||||
EditIcon,
|
||||
XIcon,
|
||||
SaveIcon,
|
||||
LockIcon,
|
||||
HammerIcon,
|
||||
DownloadIcon,
|
||||
ClipboardCopyIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { Button, Toggle, Card, Slider, Checkbox, Avatar, Chips, DropdownSelect } from '@modrinth/ui'
|
||||
import { SwapIcon } from '@/assets/icons'
|
||||
|
||||
import { Multiselect } from 'vue-multiselect'
|
||||
import { useRouter } from 'vue-router'
|
||||
import {
|
||||
duplicate,
|
||||
edit,
|
||||
edit_icon,
|
||||
get_optimal_jre_key,
|
||||
install,
|
||||
list,
|
||||
remove,
|
||||
update_repair_modrinth,
|
||||
} from '@/helpers/profile.js'
|
||||
import { computed, readonly, ref, shallowRef, watch } from 'vue'
|
||||
import { get_max_memory } from '@/helpers/jre.js'
|
||||
import { get } from '@/helpers/settings.js'
|
||||
import JavaSelector from '@/components/ui/JavaSelector.vue'
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import { open } from '@tauri-apps/plugin-dialog'
|
||||
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 { useBreadcrumbs } from '@/store/breadcrumbs'
|
||||
import ModpackVersionModal from '@/components/ui/ModpackVersionModal.vue'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
||||
|
||||
const breadcrumbs = useBreadcrumbs()
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const props = defineProps({
|
||||
instance: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
offline: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
versions: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
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 = await list()
|
||||
const availableGroups = ref([
|
||||
...new Set(
|
||||
instancesList.reduce((acc, obj) => {
|
||||
return acc.concat(obj.groups)
|
||||
}, []),
|
||||
),
|
||||
])
|
||||
|
||||
async function resetIcon() {
|
||||
icon.value = null
|
||||
await edit_icon(props.instance.path, null).catch(handleError)
|
||||
trackEvent('InstanceRemoveIcon')
|
||||
}
|
||||
|
||||
async function setIcon() {
|
||||
const value = await open({
|
||||
multiple: false,
|
||||
filters: [
|
||||
{
|
||||
name: 'Image',
|
||||
extensions: ['png', 'jpeg', 'svg', 'webp', 'gif', 'jpg'],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
if (!value) return
|
||||
|
||||
icon.value = value.path ?? value
|
||||
await edit_icon(props.instance.path, icon.value).catch(handleError)
|
||||
|
||||
trackEvent('InstanceSetIcon')
|
||||
}
|
||||
|
||||
const globalSettings = await get().catch(handleError)
|
||||
|
||||
const modalConfirmUnlock = ref(null)
|
||||
const modalConfirmUnpair = ref(null)
|
||||
|
||||
const overrideJavaInstall = ref(!!props.instance.java_path)
|
||||
const optimalJava = readonly(await get_optimal_jre_key(props.instance.path).catch(handleError))
|
||||
const javaInstall = ref({ path: optimalJava.path ?? props.instance.java_path })
|
||||
|
||||
const overrideJavaArgs = ref(props.instance.extra_launch_args?.length !== undefined)
|
||||
const javaArgs = ref(
|
||||
(props.instance.extra_launch_args ?? globalSettings.extra_launch_args).join(' '),
|
||||
)
|
||||
|
||||
const overrideEnvVars = ref(props.instance.custom_env_vars?.length !== undefined)
|
||||
const envVars = ref(
|
||||
(props.instance.custom_env_vars ?? globalSettings.custom_env_vars)
|
||||
.map((x) => x.join('='))
|
||||
.join(' '),
|
||||
)
|
||||
|
||||
const overrideMemorySettings = ref(!!props.instance.memory)
|
||||
const memory = ref(props.instance.memory ?? globalSettings.memory)
|
||||
const maxMemory = Math.floor((await get_max_memory().catch(handleError)) / 1024)
|
||||
|
||||
const 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.force_fullscreen)
|
||||
|
||||
const unlinkModpack = ref(false)
|
||||
|
||||
const inProgress = ref(false)
|
||||
const installing = computed(() => props.instance.install_stage !== 'installed')
|
||||
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)
|
||||
})
|
||||
|
||||
watch(
|
||||
[
|
||||
title,
|
||||
groups,
|
||||
groups,
|
||||
overrideJavaInstall,
|
||||
javaInstall,
|
||||
overrideJavaArgs,
|
||||
javaArgs,
|
||||
overrideEnvVars,
|
||||
envVars,
|
||||
overrideMemorySettings,
|
||||
memory,
|
||||
overrideWindowSettings,
|
||||
resolution,
|
||||
fullscreenSetting,
|
||||
overrideHooks,
|
||||
hooks,
|
||||
unlinkModpack,
|
||||
],
|
||||
async () => {
|
||||
await edit(props.instance.path, editProfileObject.value)
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
const getLocalVersion = (path) => {
|
||||
const pathSlice = path.split(' ').slice(-1).toString()
|
||||
// If the path ends in (1), (2), etc. it's a duplicate instance and no version can be obtained.
|
||||
if (/^\(\d\)/.test(pathSlice)) {
|
||||
return 'Unknown'
|
||||
}
|
||||
return pathSlice
|
||||
}
|
||||
|
||||
const editProfileObject = computed(() => {
|
||||
const editProfile = {
|
||||
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_path = javaInstall.value.path.replace('java.exe', 'javaw.exe')
|
||||
}
|
||||
}
|
||||
|
||||
if (overrideJavaArgs.value) {
|
||||
editProfile.extra_launch_args = javaArgs.value.trim().split(/\s+/).filter(Boolean)
|
||||
}
|
||||
|
||||
if (overrideEnvVars.value) {
|
||||
editProfile.custom_env_vars = envVars.value
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.map((x) => x.split('=').filter(Boolean))
|
||||
}
|
||||
|
||||
if (overrideMemorySettings.value) {
|
||||
editProfile.memory = memory.value
|
||||
}
|
||||
|
||||
if (overrideWindowSettings.value) {
|
||||
editProfile.force_fullscreen = fullscreenSetting.value
|
||||
|
||||
if (!fullscreenSetting.value) {
|
||||
editProfile.game_resolution = resolution.value
|
||||
}
|
||||
}
|
||||
|
||||
if (overrideHooks.value) {
|
||||
editProfile.hooks = hooks.value
|
||||
}
|
||||
|
||||
if (unlinkModpack.value) {
|
||||
editProfile.linked_data = null
|
||||
}
|
||||
|
||||
breadcrumbs.setName('Instance', editProfile.name)
|
||||
|
||||
return editProfile
|
||||
})
|
||||
|
||||
const repairing = ref(false)
|
||||
|
||||
async function duplicateProfile() {
|
||||
await duplicate(props.instance.path).catch(handleError)
|
||||
trackEvent('InstanceDuplicate', {
|
||||
loader: props.instance.loader,
|
||||
game_version: props.instance.game_version,
|
||||
})
|
||||
}
|
||||
|
||||
async function repairProfile(force) {
|
||||
repairing.value = true
|
||||
await install(props.instance.path, force).catch(handleError)
|
||||
repairing.value = false
|
||||
|
||||
trackEvent('InstanceRepair', {
|
||||
loader: props.instance.loader,
|
||||
game_version: props.instance.game_version,
|
||||
})
|
||||
}
|
||||
|
||||
async function unpairProfile() {
|
||||
const editProfile = props.instance
|
||||
editProfile.linked_data = null
|
||||
await edit(props.instance.path, editProfile)
|
||||
installedVersion.value = null
|
||||
installedVersionData.value = null
|
||||
modalConfirmUnpair.value.hide()
|
||||
}
|
||||
|
||||
async function unlockProfile() {
|
||||
const editProfile = props.instance
|
||||
editProfile.linked_data.locked = false
|
||||
await edit(props.instance.path, editProfile)
|
||||
modalConfirmUnlock.value.hide()
|
||||
}
|
||||
|
||||
const isPackLocked = computed(() => {
|
||||
return props.instance.linked_data && props.instance.linked_data.locked
|
||||
})
|
||||
|
||||
async function repairModpack() {
|
||||
inProgress.value = true
|
||||
await update_repair_modrinth(props.instance.path).catch(handleError)
|
||||
inProgress.value = false
|
||||
|
||||
trackEvent('InstanceRepair', {
|
||||
loader: props.instance.loader,
|
||||
game_version: props.instance.game_version,
|
||||
})
|
||||
}
|
||||
|
||||
const removing = ref(false)
|
||||
async function removeProfile() {
|
||||
removing.value = true
|
||||
await remove(props.instance.path).catch(handleError)
|
||||
removing.value = false
|
||||
|
||||
trackEvent('InstanceRemove', {
|
||||
loader: props.instance.loader,
|
||||
game_version: props.instance.game_version,
|
||||
})
|
||||
|
||||
await router.push({ path: '/' })
|
||||
}
|
||||
|
||||
const changeVersionsModal = ref(null)
|
||||
const showSnapshots = ref(false)
|
||||
|
||||
const [
|
||||
fabric_versions,
|
||||
forge_versions,
|
||||
quilt_versions,
|
||||
neoforge_versions,
|
||||
all_game_versions,
|
||||
loaders,
|
||||
] = await Promise.all([
|
||||
get_loader_versions('fabric').then(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) =>
|
||||
value
|
||||
.filter((item) => item.supported_project_types.includes('modpack'))
|
||||
.map((item) => item.name.toLowerCase()),
|
||||
)
|
||||
.then(ref)
|
||||
.catch(handleError),
|
||||
])
|
||||
loaders.value.unshift('vanilla')
|
||||
|
||||
const loader = ref(props.instance.loader)
|
||||
const gameVersion = ref(props.instance.game_version)
|
||||
const selectableGameVersions = computed(() => {
|
||||
return all_game_versions.value
|
||||
.filter((item) => {
|
||||
let defaultVal = item.version_type === 'release' || showSnapshots.value
|
||||
if (loader.value === 'fabric') {
|
||||
defaultVal &= fabric_versions.value.gameVersions.some((x) => item.version === x.id)
|
||||
} else if (loader.value === 'forge') {
|
||||
defaultVal &= forge_versions.value.gameVersions.some((x) => item.version === x.id)
|
||||
} else if (loader.value === 'quilt') {
|
||||
defaultVal &= quilt_versions.value.gameVersions.some((x) => item.version === x.id)
|
||||
} else if (loader.value === 'neoforge') {
|
||||
defaultVal &= neoforge_versions.value.gameVersions.some((x) => item.version === x.id)
|
||||
}
|
||||
|
||||
return defaultVal
|
||||
})
|
||||
.map((item) => item.version)
|
||||
})
|
||||
|
||||
const selectableLoaderVersions = computed(() => {
|
||||
if (gameVersion.value) {
|
||||
if (loader.value === 'fabric') {
|
||||
return fabric_versions.value.gameVersions[0].loaders
|
||||
} else if (loader.value === 'forge') {
|
||||
return forge_versions.value.gameVersions.find((item) => item.id === gameVersion.value).loaders
|
||||
} else if (loader.value === 'quilt') {
|
||||
return quilt_versions.value.gameVersions[0].loaders
|
||||
} else if (loader.value === 'neoforge') {
|
||||
return neoforge_versions.value.gameVersions.find((item) => item.id === gameVersion.value)
|
||||
.loaders
|
||||
}
|
||||
}
|
||||
return []
|
||||
})
|
||||
const loaderVersionIndex = ref(
|
||||
selectableLoaderVersions.value.findIndex((x) => x.id === props.instance.loader_version),
|
||||
)
|
||||
|
||||
const isValid = computed(() => {
|
||||
return (
|
||||
selectableGameVersions.value.includes(gameVersion.value) &&
|
||||
(loaderVersionIndex.value >= 0 || loader.value === 'vanilla')
|
||||
)
|
||||
})
|
||||
|
||||
const isChanged = computed(() => {
|
||||
return (
|
||||
loader.value !== props.instance.loader ||
|
||||
gameVersion.value !== props.instance.game_version ||
|
||||
(loaderVersionIndex.value >= 0 &&
|
||||
selectableLoaderVersions.value[loaderVersionIndex.value].id !== props.instance.loader_version)
|
||||
)
|
||||
})
|
||||
|
||||
watch(loader, () => (loaderVersionIndex.value = 0))
|
||||
|
||||
const editing = ref(false)
|
||||
async function saveGvLoaderEdits() {
|
||||
editing.value = true
|
||||
|
||||
const editProfile = editProfileObject.value
|
||||
editProfile.loader = loader.value
|
||||
editProfile.game_version = gameVersion.value
|
||||
|
||||
if (loader.value !== 'vanilla') {
|
||||
editProfile.loader_version = selectableLoaderVersions.value[loaderVersionIndex.value].id
|
||||
} else {
|
||||
loaderVersionIndex.value = -1
|
||||
}
|
||||
await edit(props.instance.path, editProfile).catch(handleError)
|
||||
await repairProfile(false)
|
||||
|
||||
editing.value = false
|
||||
changeVersionsModal.value.hide()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.change-versions-modal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1rem;
|
||||
gap: 1rem;
|
||||
|
||||
:deep(.animated-dropdown .options) {
|
||||
max-height: 13.375rem;
|
||||
}
|
||||
|
||||
.input-label {
|
||||
font-size: 1rem;
|
||||
font-weight: bolder;
|
||||
color: var(--color-contrast);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.versions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
margin: 1rem 0;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.installation-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(button.checkbox) {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.unlocked-instance {
|
||||
background-color: var(--color-bg);
|
||||
}
|
||||
|
||||
.modal-delete {
|
||||
padding: var(--gap-lg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.markdown-body {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.confirmation-label {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.confirmation-text {
|
||||
padding-right: 0.25ch;
|
||||
margin: 0 0.25rem;
|
||||
}
|
||||
|
||||
.confirmation-input {
|
||||
input {
|
||||
width: 20rem;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.button-group {
|
||||
margin-left: auto;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,5 @@
|
||||
import Index from './Index.vue'
|
||||
import Mods from './Mods.vue'
|
||||
import Options from './Options.vue'
|
||||
import Logs from './Logs.vue'
|
||||
|
||||
export { Index, Mods, Options, Logs }
|
||||
export { Index, Mods, Logs }
|
||||
|
||||
17
apps/app-frontend/src/pages/library/Custom.vue
Normal file
17
apps/app-frontend/src/pages/library/Custom.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<script setup>
|
||||
import GridDisplay from '@/components/GridDisplay.vue'
|
||||
|
||||
defineProps({
|
||||
instances: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<GridDisplay
|
||||
v-if="instances.length > 0"
|
||||
label="Instances"
|
||||
:instances="instances.filter((i) => !i.linked_data)"
|
||||
/>
|
||||
</template>
|
||||
17
apps/app-frontend/src/pages/library/Downloaded.vue
Normal file
17
apps/app-frontend/src/pages/library/Downloaded.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<script setup>
|
||||
import GridDisplay from '@/components/GridDisplay.vue'
|
||||
|
||||
defineProps({
|
||||
instances: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<GridDisplay
|
||||
v-if="instances.length > 0"
|
||||
label="Instances"
|
||||
:instances="instances.filter((i) => i.linked_data)"
|
||||
/>
|
||||
</template>
|
||||
@@ -1,15 +1,15 @@
|
||||
<script setup>
|
||||
import { onUnmounted, ref, shallowRef } from 'vue'
|
||||
import GridDisplay from '@/components/GridDisplay.vue'
|
||||
import { list } from '@/helpers/profile.js'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
||||
import { useBreadcrumbs } from '@/store/breadcrumbs.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 NavTabs from '@/components/ui/NavTabs.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const breadcrumbs = useBreadcrumbs()
|
||||
@@ -35,17 +35,31 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<GridDisplay v-if="instances.length > 0" label="Instances" :instances="instances" />
|
||||
<div v-else class="no-instance">
|
||||
<div class="icon">
|
||||
<NewInstanceImage />
|
||||
<div class="p-6 flex flex-col gap-3">
|
||||
<h1 class="m-0 text-2xl hidden">Library</h1>
|
||||
<NavTabs
|
||||
:links="[
|
||||
{ label: 'All instances', href: `/library` },
|
||||
{ label: 'Downloaded', href: `/library/downloaded` },
|
||||
{ label: 'Custom', href: `/library/custom` },
|
||||
{ label: 'Shared with me', href: `/library/shared`, shown: false },
|
||||
{ label: 'Saved', href: `/library/saved`, shown: false },
|
||||
]"
|
||||
/>
|
||||
<template v-if="instances.length > 0">
|
||||
<RouterView :instances="instances" />
|
||||
</template>
|
||||
<div v-else class="no-instance">
|
||||
<div class="icon">
|
||||
<NewInstanceImage />
|
||||
</div>
|
||||
<h3>No instances found</h3>
|
||||
<Button color="primary" :disabled="offline" @click="$refs.installationModal.show()">
|
||||
<PlusIcon />
|
||||
Create new instance
|
||||
</Button>
|
||||
<InstanceCreationModal ref="installationModal" />
|
||||
</div>
|
||||
<h3>No instances found</h3>
|
||||
<Button color="primary" :disabled="offline" @click="$refs.installationModal.show()">
|
||||
<PlusIcon />
|
||||
Create new instance
|
||||
</Button>
|
||||
<InstanceCreationModal ref="installationModal" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
13
apps/app-frontend/src/pages/library/Overview.vue
Normal file
13
apps/app-frontend/src/pages/library/Overview.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<script setup>
|
||||
import GridDisplay from '@/components/GridDisplay.vue'
|
||||
|
||||
defineProps({
|
||||
instances: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<GridDisplay v-if="instances.length > 0" label="Instances" :instances="instances" />
|
||||
</template>
|
||||
6
apps/app-frontend/src/pages/library/index.js
Normal file
6
apps/app-frontend/src/pages/library/index.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import Index from './Index.vue'
|
||||
import Overview from './Overview.vue'
|
||||
import Downloaded from './Downloaded.vue'
|
||||
import Custom from './Custom.vue'
|
||||
|
||||
export { Index, Overview, Downloaded, Custom }
|
||||
@@ -1,12 +1,11 @@
|
||||
<template>
|
||||
<Card>
|
||||
<div class="markdown-body" v-html="renderHighlightedString(project?.body ?? '')" />
|
||||
<ProjectPageDescription :description="project.body" />
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { renderHighlightedString } from '@modrinth/utils'
|
||||
import { Card } from '@modrinth/ui'
|
||||
import { Card, ProjectPageDescription } from '@modrinth/ui'
|
||||
|
||||
defineProps({
|
||||
project: {
|
||||
@@ -21,22 +20,3 @@ export default {
|
||||
name: 'Description',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.markdown-body {
|
||||
:deep(table) {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
:deep(hr),
|
||||
:deep(h1),
|
||||
:deep(h2) {
|
||||
max-width: max(60rem, 90%);
|
||||
}
|
||||
|
||||
:deep(ul),
|
||||
:deep(ol) {
|
||||
margin-left: 2rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -195,7 +195,7 @@ document.addEventListener('keypress', keyListener)
|
||||
|
||||
.expanded-image-modal {
|
||||
position: fixed;
|
||||
z-index: 10;
|
||||
z-index: 11;
|
||||
overflow: auto;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
@@ -1,247 +1,157 @@
|
||||
<template>
|
||||
<div class="root-container">
|
||||
<div v-if="data" class="project-sidebar" @scroll="$refs.promo.scroll()">
|
||||
<Card v-if="instance" class="small-instance">
|
||||
<router-link class="instance" :to="`/instance/${encodeURIComponent(instance.path)}`">
|
||||
<Avatar
|
||||
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : null"
|
||||
:alt="instance.name"
|
||||
size="sm"
|
||||
/>
|
||||
<div class="small-instance_info">
|
||||
<span class="title">{{
|
||||
instance.name.length > 20 ? instance.name.substring(0, 20) + '...' : instance.name
|
||||
}}</span>
|
||||
<span>
|
||||
{{ instance.loader.charAt(0).toUpperCase() + instance.loader.slice(1) }}
|
||||
{{ instance.game_version }}
|
||||
</span>
|
||||
</div>
|
||||
</router-link>
|
||||
</Card>
|
||||
<Card class="sidebar-card" @contextmenu.prevent.stop="handleRightClick">
|
||||
<Avatar size="md" :src="data.icon_url" />
|
||||
<div class="instance-info">
|
||||
<h2 class="name">{{ data.title }}</h2>
|
||||
{{ data.description }}
|
||||
</div>
|
||||
<Categories
|
||||
class="tags"
|
||||
:categories="
|
||||
categories.filter(
|
||||
(cat) => data.categories.includes(cat.name) && cat.project_type === 'mod',
|
||||
)
|
||||
"
|
||||
type="ignored"
|
||||
<div>
|
||||
<Teleport to="#sidebar-teleport-target">
|
||||
<ProjectSidebarCompatibility
|
||||
:project="data"
|
||||
:tags="{ loaders: allLoaders, gameVersions: allGameVersions }"
|
||||
class="project-sidebar-section"
|
||||
/>
|
||||
<ProjectSidebarLinks link-target="_blank" :project="data" class="project-sidebar-section" />
|
||||
<ProjectSidebarCreators
|
||||
:organization="null"
|
||||
:members="members"
|
||||
:org-link="(slug) => `https://modrinth.com/organization/${slug}`"
|
||||
:user-link="(username) => `https://modrinth.com/user/${username}`"
|
||||
link-target="_blank"
|
||||
class="project-sidebar-section"
|
||||
/>
|
||||
<ProjectSidebarDetails
|
||||
:project="data"
|
||||
:has-versions="versions.length > 0"
|
||||
:link-target="`_blank`"
|
||||
class="project-sidebar-section"
|
||||
/>
|
||||
</Teleport>
|
||||
<div class="flex flex-col gap-4 p-6">
|
||||
<InstanceIndicator v-if="instance" :instance="instance" />
|
||||
<template v-if="data">
|
||||
<Teleport
|
||||
v-if="themeStore.featureFlags.project_background"
|
||||
to="#background-teleport-target"
|
||||
>
|
||||
<EnvironmentIndicator
|
||||
:client-side="data.client_side"
|
||||
:server-side="data.server_side"
|
||||
:type="data.project_type"
|
||||
/>
|
||||
</Categories>
|
||||
<hr class="card-divider" />
|
||||
<div class="button-group">
|
||||
<Button
|
||||
color="primary"
|
||||
class="instance-button"
|
||||
:disabled="installed === true || installing === true"
|
||||
@click="install(null)"
|
||||
>
|
||||
<DownloadIcon v-if="!installed && !installing" />
|
||||
<CheckIcon v-else-if="installed" />
|
||||
{{ installing ? 'Installing...' : installed ? 'Installed' : 'Install' }}
|
||||
</Button>
|
||||
<a
|
||||
:href="`https://modrinth.com/${data.project_type}/${data.slug}`"
|
||||
rel="external"
|
||||
class="btn"
|
||||
>
|
||||
<ExternalIcon />
|
||||
Site
|
||||
</a>
|
||||
</div>
|
||||
</Card>
|
||||
<Card class="sidebar-card">
|
||||
<div class="stats">
|
||||
<div class="stat">
|
||||
<DownloadIcon aria-hidden="true" />
|
||||
<p>
|
||||
<strong>{{ formatNumber(data.downloads) }}</strong>
|
||||
<span class="stat-label"> download<span v-if="data.downloads !== '1'">s</span></span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<HeartIcon aria-hidden="true" />
|
||||
<p>
|
||||
<strong>{{ formatNumber(data.followers) }}</strong>
|
||||
<span class="stat-label"> follower<span v-if="data.followers !== '1'">s</span></span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="stat date">
|
||||
<CalendarIcon aria-hidden="true" />
|
||||
<span
|
||||
><span class="date-label">Created </span> {{ dayjs(data.published).fromNow() }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="stat date">
|
||||
<UpdatedIcon aria-hidden="true" />
|
||||
<span
|
||||
><span class="date-label">Updated </span> {{ dayjs(data.updated).fromNow() }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="card-divider" />
|
||||
<div class="button-group">
|
||||
<Button class="instance-button" disabled>
|
||||
<ReportIcon />
|
||||
Report
|
||||
</Button>
|
||||
<Button class="instance-button" disabled>
|
||||
<HeartIcon />
|
||||
Follow
|
||||
</Button>
|
||||
</div>
|
||||
<hr class="card-divider" />
|
||||
<div class="links">
|
||||
<a
|
||||
v-if="data.issues_url"
|
||||
:href="data.issues_url"
|
||||
class="title"
|
||||
rel="noopener nofollow ugc external"
|
||||
>
|
||||
<IssuesIcon aria-hidden="true" />
|
||||
<span>Issues</span>
|
||||
</a>
|
||||
<a
|
||||
v-if="data.source_url"
|
||||
:href="data.source_url"
|
||||
class="title"
|
||||
rel="noopener nofollow ugc external"
|
||||
>
|
||||
<CodeIcon aria-hidden="true" />
|
||||
<span>Source</span>
|
||||
</a>
|
||||
<a
|
||||
v-if="data.wiki_url"
|
||||
:href="data.wiki_url"
|
||||
class="title"
|
||||
rel="noopener nofollow ugc external"
|
||||
>
|
||||
<WikiIcon aria-hidden="true" />
|
||||
<span>Wiki</span>
|
||||
</a>
|
||||
<a
|
||||
v-if="data.discord_url"
|
||||
:href="data.discord_url"
|
||||
class="title"
|
||||
rel="noopener nofollow ugc external"
|
||||
>
|
||||
<DiscordIcon aria-hidden="true" />
|
||||
<span>Discord</span>
|
||||
</a>
|
||||
<a
|
||||
v-for="(donation, index) in data.donation_urls"
|
||||
:key="index"
|
||||
:href="donation.url"
|
||||
rel="noopener nofollow ugc external"
|
||||
>
|
||||
<BuyMeACoffeeIcon v-if="donation.id === 'bmac'" aria-hidden="true" />
|
||||
<PatreonIcon v-else-if="donation.id === 'patreon'" aria-hidden="true" />
|
||||
<KoFiIcon v-else-if="donation.id === 'ko-fi'" aria-hidden="true" />
|
||||
<PaypalIcon v-else-if="donation.id === 'paypal'" aria-hidden="true" />
|
||||
<OpenCollectiveIcon v-else-if="donation.id === 'open-collective'" aria-hidden="true" />
|
||||
<HeartIcon v-else-if="donation.id === 'github'" />
|
||||
<CoinsIcon v-else />
|
||||
<span v-if="donation.id === 'bmac'">Buy Me a Coffee</span>
|
||||
<span v-else-if="donation.id === 'patreon'">Patreon</span>
|
||||
<span v-else-if="donation.id === 'paypal'">PayPal</span>
|
||||
<span v-else-if="donation.id === 'ko-fi'">Ko-fi</span>
|
||||
<span v-else-if="donation.id === 'github'">GitHub Sponsors</span>
|
||||
<span v-else>Donate</span>
|
||||
</a>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<div v-if="data" class="content-container">
|
||||
<Card class="tabs">
|
||||
<NavRow
|
||||
v-if="data.gallery.length > 0"
|
||||
<ProjectBackgroundGradient :project="data" />
|
||||
</Teleport>
|
||||
<ProjectHeader :project="data" @contextmenu.prevent.stop="handleRightClick">
|
||||
<template #actions>
|
||||
<ButtonStyled size="large" color="brand">
|
||||
<button
|
||||
v-tooltip="installed ? `This project is already installed` : null"
|
||||
:disabled="installed || installing"
|
||||
@click="install(null)"
|
||||
>
|
||||
<DownloadIcon v-if="!installed && !installing" />
|
||||
<CheckIcon v-else-if="installed" />
|
||||
{{ installing ? 'Installing...' : installed ? 'Installed' : 'Install' }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled size="large" circular type="transparent">
|
||||
<OverflowMenu
|
||||
:tooltip="`More options`"
|
||||
:options="[
|
||||
{
|
||||
id: 'follow',
|
||||
disabled: true,
|
||||
tooltip: 'Coming soon',
|
||||
action: () => {},
|
||||
},
|
||||
{
|
||||
id: 'save',
|
||||
disabled: true,
|
||||
tooltip: 'Coming soon',
|
||||
action: () => {},
|
||||
},
|
||||
{
|
||||
id: 'open-in-browser',
|
||||
link: `https://modrinth.com/${data.project_type}/${data.slug}`,
|
||||
external: true,
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
id: 'report',
|
||||
color: 'red',
|
||||
hoverFilled: true,
|
||||
link: `https://modrinth.com/report?item=project&itemID=${data.id}`,
|
||||
},
|
||||
]"
|
||||
aria-label="More options"
|
||||
>
|
||||
<MoreVerticalIcon aria-hidden="true" />
|
||||
<template #open-in-browser> <ExternalIcon /> Open in browser </template>
|
||||
<template #follow> <HeartIcon /> Follow </template>
|
||||
<template #save> <BookmarkIcon /> Save </template>
|
||||
<template #report> <ReportIcon /> Report </template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</ProjectHeader>
|
||||
<NavTabs
|
||||
:links="[
|
||||
{
|
||||
label: 'Description',
|
||||
href: `/project/${$route.params.id}/`,
|
||||
href: `/project/${$route.params.id}`,
|
||||
},
|
||||
{
|
||||
label: 'Versions',
|
||||
href: `/project/${$route.params.id}/versions`,
|
||||
href: {
|
||||
path: `/project/${$route.params.id}/versions`,
|
||||
query: { l: instance?.loader, g: instance?.game_version },
|
||||
},
|
||||
subpages: ['version'],
|
||||
},
|
||||
{
|
||||
label: 'Gallery',
|
||||
href: `/project/${$route.params.id}/gallery`,
|
||||
shown: data.gallery.length > 0,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
<NavRow
|
||||
v-else
|
||||
:links="[
|
||||
{
|
||||
label: 'Description',
|
||||
href: `/project/${$route.params.id}/`,
|
||||
},
|
||||
{
|
||||
label: 'Versions',
|
||||
href: `/project/${$route.params.id}/versions`,
|
||||
},
|
||||
]"
|
||||
<RouterView
|
||||
:project="data"
|
||||
:versions="versions"
|
||||
:members="members"
|
||||
:instance="instance"
|
||||
:install="install"
|
||||
:installed="installed"
|
||||
:installing="installing"
|
||||
:installed-version="installedVersion"
|
||||
/>
|
||||
</Card>
|
||||
<RouterView
|
||||
:project="data"
|
||||
:versions="versions"
|
||||
:members="members"
|
||||
:instance="instance"
|
||||
:install="install"
|
||||
:installed="installed"
|
||||
:installing="installing"
|
||||
:installed-version="installedVersion"
|
||||
/>
|
||||
</template>
|
||||
<template v-else> Project data couldn't not be loaded. </template>
|
||||
</div>
|
||||
<ContextMenu ref="options" @option-clicked="handleOptionsClick">
|
||||
<template #install> <DownloadIcon /> Install </template>
|
||||
<template #open_link> <GlobeIcon /> Open in Modrinth <ExternalIcon /> </template>
|
||||
<template #copy_link> <ClipboardCopyIcon /> Copy link </template>
|
||||
</ContextMenu>
|
||||
</div>
|
||||
<ContextMenu ref="options" @option-clicked="handleOptionsClick">
|
||||
<template #install> <DownloadIcon /> Install </template>
|
||||
<template #open_link> <GlobeIcon /> Open in Modrinth <ExternalIcon /> </template>
|
||||
<template #copy_link> <ClipboardCopyIcon /> Copy link </template>
|
||||
</ContextMenu>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
BookmarkIcon,
|
||||
MoreVerticalIcon,
|
||||
DownloadIcon,
|
||||
ReportIcon,
|
||||
HeartIcon,
|
||||
UpdatedIcon,
|
||||
CalendarIcon,
|
||||
IssuesIcon,
|
||||
WikiIcon,
|
||||
CoinsIcon,
|
||||
CodeIcon,
|
||||
ExternalIcon,
|
||||
CheckIcon,
|
||||
GlobeIcon,
|
||||
ClipboardCopyIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { Categories, EnvironmentIndicator, Card, Avatar, Button, NavRow } from '@modrinth/ui'
|
||||
import { formatNumber } from '@modrinth/utils'
|
||||
import {
|
||||
BuyMeACoffeeIcon,
|
||||
DiscordIcon,
|
||||
PatreonIcon,
|
||||
PaypalIcon,
|
||||
KoFiIcon,
|
||||
OpenCollectiveIcon,
|
||||
} from '@/assets/external'
|
||||
import { get_categories } from '@/helpers/tags'
|
||||
ProjectHeader,
|
||||
ProjectSidebarCompatibility,
|
||||
ButtonStyled,
|
||||
OverflowMenu,
|
||||
ProjectSidebarLinks,
|
||||
ProjectSidebarCreators,
|
||||
ProjectSidebarDetails,
|
||||
ProjectBackgroundGradient,
|
||||
} from '@modrinth/ui'
|
||||
|
||||
import { get_categories, get_game_versions, get_loaders } from '@/helpers/tags'
|
||||
import { get as getInstance, get_projects as getInstanceProjects } from '@/helpers/profile'
|
||||
import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
@@ -249,17 +159,20 @@ import { useRoute } from 'vue-router'
|
||||
import { ref, shallowRef, watch } from 'vue'
|
||||
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
||||
import { handleError } from '@/store/notifications.js'
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||
import { install as installVersion } from '@/store/install.js'
|
||||
import { get_project, get_team, get_version_many } from '@/helpers/cache.js'
|
||||
import NavTabs from '@/components/ui/NavTabs.vue'
|
||||
import { useTheming } from '@/store/state.js'
|
||||
import InstanceIndicator from '@/components/ui/InstanceIndicator.vue'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
const route = useRoute()
|
||||
const breadcrumbs = useBreadcrumbs()
|
||||
const themeStore = useTheming()
|
||||
|
||||
const options = ref(null)
|
||||
const installing = ref(false)
|
||||
const data = shallowRef(null)
|
||||
const versions = shallowRef([])
|
||||
@@ -271,6 +184,11 @@ const instanceProjects = ref(null)
|
||||
const installed = ref(false)
|
||||
const installedVersion = ref(null)
|
||||
|
||||
const [allLoaders, allGameVersions] = await Promise.all([
|
||||
get_loaders().catch(handleError).then(ref),
|
||||
get_game_versions().catch(handleError).then(ref),
|
||||
])
|
||||
|
||||
async function fetchProjectData() {
|
||||
const project = await get_project(route.params.id, 'must_revalidate').catch(handleError)
|
||||
|
||||
@@ -300,13 +218,11 @@ async function fetchProjectData() {
|
||||
|
||||
await fetchProjectData()
|
||||
|
||||
const promo = ref(null)
|
||||
watch(
|
||||
() => route.params.id,
|
||||
async () => {
|
||||
if (route.params.id && route.path.startsWith('/project')) {
|
||||
await fetchProjectData()
|
||||
promo.value.scroll()
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -329,28 +245,30 @@ async function install(version) {
|
||||
)
|
||||
}
|
||||
|
||||
const handleRightClick = (e) => {
|
||||
options.value.showMenu(e, data.value, [
|
||||
{ name: 'install' },
|
||||
{ type: 'divider' },
|
||||
{ name: 'open_link' },
|
||||
{ name: 'copy_link' },
|
||||
const options = ref(null)
|
||||
const handleRightClick = (event) => {
|
||||
options.value.showMenu(event, data.value, [
|
||||
{
|
||||
name: 'install',
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
name: 'open_link',
|
||||
},
|
||||
{
|
||||
name: 'copy_link',
|
||||
},
|
||||
])
|
||||
}
|
||||
|
||||
const handleOptionsClick = (args) => {
|
||||
switch (args.option) {
|
||||
case 'install':
|
||||
install(null)
|
||||
break
|
||||
case 'open_link':
|
||||
window.__TAURI_INVOKE__('tauri', {
|
||||
__tauriModule: 'Shell',
|
||||
message: {
|
||||
cmd: 'open',
|
||||
path: `https://modrinth.com/${args.item.project_type}/${args.item.slug}`,
|
||||
},
|
||||
})
|
||||
openUrl(`https://modrinth.com/${args.item.project_type}/${args.item.slug}`)
|
||||
break
|
||||
case 'copy_link':
|
||||
navigator.clipboard.writeText(
|
||||
@@ -518,27 +436,7 @@ const handleOptionsClick = (args) => {
|
||||
}
|
||||
}
|
||||
|
||||
.small-instance {
|
||||
padding: var(--gap-lg);
|
||||
border-radius: var(--radius-md);
|
||||
margin-bottom: var(--gap-md);
|
||||
|
||||
.instance {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0;
|
||||
|
||||
.title {
|
||||
font-weight: 600;
|
||||
color: var(--color-contrast);
|
||||
}
|
||||
}
|
||||
|
||||
.small-instance_info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: 0.25rem 0;
|
||||
}
|
||||
.project-sidebar-section {
|
||||
@apply p-4 flex flex-col gap-2 border-0 border-b-[1px] border-[--brand-gradient-border] border-solid;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,165 +1,82 @@
|
||||
<template>
|
||||
<Card class="filter-header">
|
||||
<div class="manage">
|
||||
<multiselect
|
||||
v-model="filterLoader"
|
||||
:options="
|
||||
versions
|
||||
.flatMap((value) => value.loaders)
|
||||
.filter((value, index, self) => self.indexOf(value) === index)
|
||||
"
|
||||
:multiple="true"
|
||||
:searchable="true"
|
||||
:show-no-results="false"
|
||||
:close-on-select="false"
|
||||
:clear-search-on-select="false"
|
||||
:show-labels="false"
|
||||
:selectable="() => versions.length <= 6"
|
||||
placeholder="Filter loader..."
|
||||
:custom-label="(option) => option.charAt(0).toUpperCase() + option.slice(1)"
|
||||
/>
|
||||
<multiselect
|
||||
v-model="filterGameVersions"
|
||||
:options="
|
||||
versions
|
||||
.flatMap((value) => value.game_versions)
|
||||
.filter((value, index, self) => self.indexOf(value) === index)
|
||||
"
|
||||
:multiple="true"
|
||||
:searchable="true"
|
||||
:show-no-results="false"
|
||||
:close-on-select="false"
|
||||
:clear-search-on-select="false"
|
||||
:show-labels="false"
|
||||
:selectable="() => versions.length <= 6"
|
||||
placeholder="Filter versions..."
|
||||
:custom-label="(option) => option.charAt(0).toUpperCase() + option.slice(1)"
|
||||
/>
|
||||
<multiselect
|
||||
v-model="filterVersions"
|
||||
:options="
|
||||
versions
|
||||
.map((value) => value.version_type)
|
||||
.filter((value, index, self) => self.indexOf(value) === index)
|
||||
"
|
||||
:multiple="true"
|
||||
:searchable="true"
|
||||
:show-no-results="false"
|
||||
:close-on-select="false"
|
||||
:clear-search-on-select="false"
|
||||
:show-labels="false"
|
||||
:selectable="() => versions.length <= 6"
|
||||
placeholder="Filter release channel..."
|
||||
:custom-label="(option) => option.charAt(0).toUpperCase() + option.slice(1)"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
class="no-wrap clear-filters"
|
||||
:disabled="
|
||||
filterVersions.length === 0 && filterLoader.length === 0 && filterGameVersions.length === 0
|
||||
"
|
||||
:action="clearFilters"
|
||||
<div>
|
||||
<ProjectPageVersions
|
||||
:loaders="loaders"
|
||||
:game-versions="gameVersions"
|
||||
:versions="versions"
|
||||
:project="project"
|
||||
:version-link="(version) => `/project/${project.id}/version/${version.id}`"
|
||||
>
|
||||
<ClearIcon />
|
||||
Clear filters
|
||||
</Button>
|
||||
</Card>
|
||||
<Pagination
|
||||
:page="currentPage"
|
||||
:count="Math.ceil(filteredVersions.length / 20)"
|
||||
class="pagination-before"
|
||||
:link-function="(page) => `?page=${page}`"
|
||||
@switch-page="switchPage"
|
||||
/>
|
||||
<Card class="mod-card">
|
||||
<div class="table">
|
||||
<div class="table-row table-head">
|
||||
<div class="table-cell table-text download-cell" />
|
||||
<div class="name-cell table-cell table-text">Name</div>
|
||||
<div class="table-cell table-text">Supports</div>
|
||||
<div class="table-cell table-text">Stats</div>
|
||||
</div>
|
||||
<div
|
||||
v-for="version in filteredVersions.slice((currentPage - 1) * 20, currentPage * 20)"
|
||||
:key="version.id"
|
||||
class="table-row selectable"
|
||||
@click="$router.push(`/project/${$route.params.id}/version/${version.id}`)"
|
||||
>
|
||||
<div class="table-cell table-text">
|
||||
<Button
|
||||
:color="installed && version.id === installedVersion ? '' : 'primary'"
|
||||
icon-only
|
||||
<template #actions="{ version }">
|
||||
<ButtonStyled circular type="transparent">
|
||||
<button
|
||||
v-tooltip="`Install`"
|
||||
:class="{
|
||||
'group-hover:!bg-brand group-hover:[&>svg]:!text-brand-inverted':
|
||||
!installed || version.id !== installedVersion,
|
||||
}"
|
||||
:disabled="installing || (installed && version.id === installedVersion)"
|
||||
@click.stop="() => install(version.id)"
|
||||
>
|
||||
<DownloadIcon v-if="!installed" />
|
||||
<SwapIcon v-else-if="installed && version.id !== installedVersion" />
|
||||
<CheckIcon v-else />
|
||||
</Button>
|
||||
</div>
|
||||
<div class="name-cell table-cell table-text">
|
||||
<div class="version-link">
|
||||
{{ version.name.charAt(0).toUpperCase() + version.name.slice(1) }}
|
||||
<div class="version-badge">
|
||||
<div class="channel-indicator">
|
||||
<Badge
|
||||
:color="releaseColor(version.version_type)"
|
||||
:type="
|
||||
version.version_type.charAt(0).toUpperCase() + version.version_type.slice(1)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
{{ version.version_number }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-cell table-text stacked-text">
|
||||
<span>
|
||||
{{
|
||||
version.loaders.map((str) => str.charAt(0).toUpperCase() + str.slice(1)).join(', ')
|
||||
}}
|
||||
</span>
|
||||
<span>
|
||||
{{ version.game_versions.join(', ') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="table-cell table-text stacked-text">
|
||||
<div>
|
||||
<span> Published on </span>
|
||||
<strong>
|
||||
{{
|
||||
new Date(version.date_published).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
})
|
||||
}}
|
||||
</strong>
|
||||
</div>
|
||||
<div>
|
||||
<strong>
|
||||
{{ formatNumber(version.downloads) }}
|
||||
</strong>
|
||||
<span> Downloads </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular type="transparent">
|
||||
<OverflowMenu
|
||||
v-if="false"
|
||||
class="group-hover:!bg-button-bg"
|
||||
:options="[
|
||||
{
|
||||
id: 'install-elsewhere',
|
||||
action: () => {},
|
||||
shown: false && !!instance,
|
||||
color: 'primary',
|
||||
hoverFilled: true,
|
||||
},
|
||||
{
|
||||
id: 'open-in-browser',
|
||||
link: `https://modrinth.com/${project.project_type}/${project.slug}/version/${version.id}`,
|
||||
},
|
||||
]"
|
||||
aria-label="More options"
|
||||
>
|
||||
<MoreVerticalIcon aria-hidden="true" />
|
||||
<template #install-elsewhere>
|
||||
<DownloadIcon aria-hidden="true" />
|
||||
Add to another instance
|
||||
</template>
|
||||
<template #open-in-browser> <ExternalIcon /> Open in browser </template>
|
||||
</OverflowMenu>
|
||||
<a
|
||||
v-else
|
||||
v-tooltip="`Open in browser`"
|
||||
class="group-hover:!bg-button-bg"
|
||||
:href="`https://modrinth.com/${project.project_type}/${project.slug}/version/${version.id}`"
|
||||
target="_blank"
|
||||
>
|
||||
<ExternalIcon />
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</ProjectPageVersions>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Card, Button, Pagination, Badge } from '@modrinth/ui'
|
||||
import { CheckIcon, ClearIcon, DownloadIcon } from '@modrinth/assets'
|
||||
import { formatNumber } from '@modrinth/utils'
|
||||
import Multiselect from 'vue-multiselect'
|
||||
import { releaseColor } from '@/helpers/utils'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { ProjectPageVersions, ButtonStyled, OverflowMenu } from '@modrinth/ui'
|
||||
import { CheckIcon, DownloadIcon, ExternalIcon, MoreVerticalIcon } from '@modrinth/assets'
|
||||
import { ref } from 'vue'
|
||||
import { SwapIcon } from '@/assets/icons/index.js'
|
||||
import { get_game_versions, get_loaders } from '@/helpers/tags.js'
|
||||
import { handleError } from '@/store/notifications.js'
|
||||
|
||||
const props = defineProps({
|
||||
defineProps({
|
||||
project: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
versions: {
|
||||
type: Array,
|
||||
required: true,
|
||||
@@ -186,40 +103,10 @@ const props = defineProps({
|
||||
},
|
||||
})
|
||||
|
||||
const filterVersions = ref([])
|
||||
const filterLoader = ref(props.instance ? [props.instance?.loader] : [])
|
||||
const filterGameVersions = ref(props.instance ? [props.instance?.game_version] : [])
|
||||
|
||||
const currentPage = ref(1)
|
||||
|
||||
const clearFilters = () => {
|
||||
filterVersions.value = []
|
||||
filterLoader.value = []
|
||||
filterGameVersions.value = []
|
||||
}
|
||||
|
||||
const filteredVersions = computed(() => {
|
||||
return props.versions.filter(
|
||||
(projectVersion) =>
|
||||
(filterGameVersions.value.length === 0 ||
|
||||
filterGameVersions.value.some((gameVersion) =>
|
||||
projectVersion.game_versions.includes(gameVersion),
|
||||
)) &&
|
||||
(filterLoader.value.length === 0 ||
|
||||
filterLoader.value.some((loader) => projectVersion.loaders.includes(loader))) &&
|
||||
(filterVersions.value.length === 0 ||
|
||||
filterVersions.value.includes(projectVersion.version_type)),
|
||||
)
|
||||
})
|
||||
|
||||
function switchPage(page) {
|
||||
currentPage.value = page
|
||||
}
|
||||
|
||||
//watch all the filters and if a value changes, reset to page 1
|
||||
watch([filterVersions, filterLoader, filterGameVersions], () => {
|
||||
currentPage.value = 1
|
||||
})
|
||||
const [loaders, gameVersions] = await Promise.all([
|
||||
get_loaders().catch(handleError).then(ref),
|
||||
get_game_versions().catch(handleError).then(ref),
|
||||
])
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
Reference in New Issue
Block a user