You've already forked AstralRinth
forked from didirus/AstralRinth
Bugs again (#703)
* initial * more fixes * logs * more fixes * working rescuer * minor log display fix * mac fixes * minor fix * libsselinux1 * linux error * actions test * more bugs. Modpack page! BIG changes * changed minimum 64 -> 8 * removed modpack page moved to modal * removed unnecessary css * mac compile * many revs * Merge colorful logs (#725) * make implementation not dumb * run prettier * null -> true * Add line numbers & make errors more robust. * improvments * changes; virtual scroll --------- Co-authored-by: qtchaos <72168435+qtchaos@users.noreply.github.com> * omorphia colors, comments fix * fixes; _JAVA_OPTIONS * revs * mac specific * more mac * some fixes * quick fix * add java reinstall option --------- Co-authored-by: qtchaos <72168435+qtchaos@users.noreply.github.com> Co-authored-by: Jai A <jaiagr+gpg@pm.me>
This commit is contained in:
@@ -7,9 +7,11 @@ import {
|
||||
LibraryIcon,
|
||||
PlusIcon,
|
||||
SettingsIcon,
|
||||
FileIcon,
|
||||
Button,
|
||||
Notifications,
|
||||
XIcon,
|
||||
Card,
|
||||
} from 'omorphia'
|
||||
import { useLoading, useTheming } from '@/store/state'
|
||||
import AccountsCard from '@/components/ui/AccountsCard.vue'
|
||||
@@ -19,12 +21,12 @@ import Breadcrumbs from '@/components/ui/Breadcrumbs.vue'
|
||||
import RunningAppBar from '@/components/ui/RunningAppBar.vue'
|
||||
import SplashScreen from '@/components/ui/SplashScreen.vue'
|
||||
import ModrinthLoadingIndicator from '@/components/modrinth-loading-indicator'
|
||||
import { useNotifications } from '@/store/notifications.js'
|
||||
import { handleError, useNotifications } from '@/store/notifications.js'
|
||||
import { offline_listener, command_listener, warning_listener } from '@/helpers/events.js'
|
||||
import { MinimizeIcon, MaximizeIcon } from '@/assets/icons'
|
||||
import { MinimizeIcon, MaximizeIcon, ChatIcon } from '@/assets/icons'
|
||||
import { type } from '@tauri-apps/api/os'
|
||||
import { appWindow } from '@tauri-apps/api/window'
|
||||
import { isDev, getOS, isOffline } from '@/helpers/utils.js'
|
||||
import { isDev, getOS, isOffline, showLauncherLogsFolder } from '@/helpers/utils.js'
|
||||
import {
|
||||
mixpanel_track,
|
||||
mixpanel_init,
|
||||
@@ -40,6 +42,7 @@ import { confirm } from '@tauri-apps/api/dialog'
|
||||
import URLConfirmModal from '@/components/ui/URLConfirmModal.vue'
|
||||
import StickyTitleBar from '@/components/ui/tutorial/StickyTitleBar.vue'
|
||||
import OnboardingScreen from '@/components/ui/tutorial/OnboardingScreen.vue'
|
||||
import { install_from_file } from './helpers/pack'
|
||||
|
||||
const themeStore = useTheming()
|
||||
const urlModal = ref(null)
|
||||
@@ -51,14 +54,17 @@ const showOnboarding = ref(false)
|
||||
|
||||
const onboardingVideo = ref()
|
||||
|
||||
const failureText = ref(null)
|
||||
const os = ref('')
|
||||
|
||||
defineExpose({
|
||||
initialize: async () => {
|
||||
isLoading.value = false
|
||||
const { theme, opt_out_analytics, collapsed_navigation, advanced_rendering, fully_onboarded } =
|
||||
await get()
|
||||
const os = await getOS()
|
||||
// video should play if the user is not on linux, and has not onboarded
|
||||
videoPlaying.value = !fully_onboarded && os !== 'Linux'
|
||||
os.value = await getOS()
|
||||
videoPlaying.value = !fully_onboarded && os.value !== 'Linux'
|
||||
const dev = await isDev()
|
||||
const version = await getVersion()
|
||||
showOnboarding.value = !fully_onboarded
|
||||
@@ -98,6 +104,11 @@ defineExpose({
|
||||
onboardingVideo.value.play()
|
||||
}
|
||||
},
|
||||
failure: async (e) => {
|
||||
isLoading.value = false
|
||||
failureText.value = e
|
||||
os.value = await getOS()
|
||||
},
|
||||
})
|
||||
|
||||
const confirmClose = async () => {
|
||||
@@ -112,6 +123,10 @@ const confirmClose = async () => {
|
||||
}
|
||||
|
||||
const handleClose = async () => {
|
||||
if (failureText.value != null) {
|
||||
await TauriWindow.getCurrent().close()
|
||||
return
|
||||
}
|
||||
// State should respond immeiately if it's safe to close
|
||||
// If not, code is deadlocked or worse, so wait 2 seconds and then ask the user to confirm closing
|
||||
// (Exception: if the user is changing config directory, which takes control of the state, and it's taking a significant amount of time for some reason)
|
||||
@@ -129,6 +144,16 @@ const handleClose = async () => {
|
||||
await TauriWindow.getCurrent().close()
|
||||
}
|
||||
|
||||
const openSupport = async () => {
|
||||
window.__TAURI_INVOKE__('tauri', {
|
||||
__tauriModule: 'Shell',
|
||||
message: {
|
||||
cmd: 'open',
|
||||
path: 'https://discord.gg/modrinth',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
TauriWindow.getCurrent().listen(TauriEvent.WINDOW_CLOSE_REQUESTED, async () => {
|
||||
await handleClose()
|
||||
})
|
||||
@@ -193,9 +218,19 @@ document.querySelector('body').addEventListener('auxclick', function (e) {
|
||||
|
||||
const accounts = ref(null)
|
||||
|
||||
command_listener((e) => {
|
||||
console.log(e)
|
||||
urlModal.value.show(e)
|
||||
command_listener(async (e) => {
|
||||
if (e.event === 'RunMRPack') {
|
||||
// RunMRPack should directly install a local mrpack given a path
|
||||
if (e.path.endsWith('.mrpack')) {
|
||||
await install_from_file(e.path).catch(handleError)
|
||||
mixpanel_track('InstanceCreate', {
|
||||
source: 'CreationModalFileDrop',
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// Other commands are URL-based (deep linking)
|
||||
urlModal.value.show(e)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -209,6 +244,46 @@ command_listener((e) => {
|
||||
autoplay
|
||||
@ended="videoPlaying = false"
|
||||
/>
|
||||
<div v-if="failureText" class="failure dark-mode">
|
||||
<div class="appbar-failure dark-mode">
|
||||
<Button v-if="os != 'MacOS'" icon-only @click="TauriWindow.getCurrent().close()">
|
||||
<XIcon />
|
||||
</Button>
|
||||
</div>
|
||||
<div class="error-view dark-mode">
|
||||
<Card class="error-text">
|
||||
<div class="label">
|
||||
<h3>
|
||||
<span class="label__title size-card-header">Failed to initialize</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="error-div">
|
||||
Modrinth App failed to load correctly. This may be because of a corrupted file, or because
|
||||
the app is missing crucial files.
|
||||
</div>
|
||||
<div class="error-div">You may be able to fix it one of the following ways:</div>
|
||||
<ul class="error-div">
|
||||
<li>Ennsuring you are connected to the internet, then try restarting the app.</li>
|
||||
<li>Redownloading the app.</li>
|
||||
</ul>
|
||||
<div class="error-div">
|
||||
If it still does not work, you can seek support using the link below. You should provide
|
||||
the following error, as well as any recent launcher logs in the folder below.
|
||||
</div>
|
||||
<div class="error-div">The following error was provided:</div>
|
||||
|
||||
<Card class="error-message">
|
||||
{{ failureText.message }}
|
||||
</Card>
|
||||
|
||||
<div class="button-row push-right">
|
||||
<Button @click="showLauncherLogsFolder"><FileIcon />Open launcher logs</Button>
|
||||
|
||||
<Button @click="openSupport"><ChatIcon />Get support</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
<SplashScreen v-else-if="!videoPlaying && isLoading" app-loading />
|
||||
<OnboardingScreen v-else-if="showOnboarding" :finish="() => (showOnboarding = false)" />
|
||||
<div v-else class="container">
|
||||
@@ -393,6 +468,53 @@ command_listener((e) => {
|
||||
}
|
||||
}
|
||||
|
||||
.failure {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background-color: var(--color-bg);
|
||||
|
||||
.appbar-failure {
|
||||
display: flex; /* Change to flex to align items horizontally */
|
||||
justify-content: flex-end; /* Align items to the right */
|
||||
height: 3.25rem;
|
||||
//no select
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.error-view {
|
||||
display: flex; /* Change to flex to align items horizontally */
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
background-color: var(--color-bg);
|
||||
|
||||
color: var(--color-base);
|
||||
|
||||
.card {
|
||||
background-color: var(--color-raised-bg);
|
||||
}
|
||||
|
||||
.error-text {
|
||||
display: flex;
|
||||
max-width: 60%;
|
||||
gap: 0.25rem;
|
||||
flex-direction: column;
|
||||
|
||||
.error-div {
|
||||
// spaced out
|
||||
margin: 0.5rem;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
margin: 0.5rem;
|
||||
background-color: var(--color-button-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nav-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -522,4 +644,15 @@ command_listener((e) => {
|
||||
object-fit: cover;
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.button-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
gap: var(--gap-md);
|
||||
|
||||
.transparent {
|
||||
padding: var(--gap-sm) 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||
import dayjs from 'dayjs'
|
||||
import { useTheming } from '@/store/theme.js'
|
||||
import { remove } from '@/helpers/profile.js'
|
||||
import { duplicate, remove } from '@/helpers/profile.js'
|
||||
import { handleError } from '@/store/notifications.js'
|
||||
|
||||
const props = defineProps({
|
||||
@@ -51,11 +51,17 @@ async function deleteProfile() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleRightClick = (event, item) => {
|
||||
async function duplicateProfile(p) {
|
||||
await duplicate(p).catch(handleError)
|
||||
}
|
||||
|
||||
const handleRightClick = (event, profilePathId) => {
|
||||
const item = instanceComponents.value.find((x) => x.instance.path === profilePathId)
|
||||
const baseOptions = [
|
||||
{ name: 'add_content' },
|
||||
{ type: 'divider' },
|
||||
{ name: 'edit' },
|
||||
{ name: 'duplicate' },
|
||||
{ name: 'open' },
|
||||
{ name: 'copy' },
|
||||
{ type: 'divider' },
|
||||
@@ -100,6 +106,10 @@ const handleOptionsClick = async (args) => {
|
||||
case 'edit':
|
||||
await args.item.seeInstance()
|
||||
break
|
||||
case 'duplicate':
|
||||
if (args.item.instance.install_stage == 'installed')
|
||||
await duplicateProfile(args.item.instance.path)
|
||||
break
|
||||
case 'open':
|
||||
await args.item.openFolder()
|
||||
break
|
||||
@@ -131,7 +141,7 @@ const filteredResults = computed(() => {
|
||||
|
||||
if (sortBy.value === 'Game version') {
|
||||
instances.sort((a, b) => {
|
||||
return a.metadata.name.localeCompare(b.metadata.game_version)
|
||||
return a.metadata.game_version.localeCompare(b.metadata.game_version)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -285,11 +295,11 @@ const filteredResults = computed(() => {
|
||||
</div>
|
||||
<section class="instances">
|
||||
<Instance
|
||||
v-for="(instance, index) in instanceSection.value"
|
||||
v-for="instance in instanceSection.value"
|
||||
ref="instanceComponents"
|
||||
:key="instance.path"
|
||||
:key="instance.path + instance.install_stage"
|
||||
:instance="instance"
|
||||
@contextmenu.prevent.stop="(event) => handleRightClick(event, instanceComponents[index])"
|
||||
@contextmenu.prevent.stop="(event) => handleRightClick(event, instance.path)"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
@@ -298,6 +308,7 @@ const filteredResults = computed(() => {
|
||||
<template #stop> <StopCircleIcon /> Stop </template>
|
||||
<template #add_content> <PlusIcon /> Add content </template>
|
||||
<template #edit> <EyeIcon /> View instance </template>
|
||||
<template #duplicate> <ClipboardCopyIcon /> Duplicate instance</template>
|
||||
<template #delete> <TrashIcon /> Delete </template>
|
||||
<template #open> <FolderOpenIcon /> Open folder </template>
|
||||
<template #copy> <ClipboardCopyIcon /> Copy path </template>
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
kill_by_uuid,
|
||||
} from '@/helpers/process.js'
|
||||
import { handleError } from '@/store/notifications.js'
|
||||
import { remove, run } from '@/helpers/profile.js'
|
||||
import { duplicate, remove, run } from '@/helpers/profile.js'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { showProfileInFolder } from '@/helpers/utils.js'
|
||||
import { useFetch } from '@/helpers/fetch.js'
|
||||
@@ -70,11 +70,16 @@ async function deleteProfile() {
|
||||
}
|
||||
}
|
||||
|
||||
async function duplicateProfile(p) {
|
||||
await duplicate(p).catch(handleError)
|
||||
}
|
||||
|
||||
const handleInstanceRightClick = async (event, passedInstance) => {
|
||||
const baseOptions = [
|
||||
{ name: 'add_content' },
|
||||
{ type: 'divider' },
|
||||
{ name: 'edit' },
|
||||
{ name: 'duplicate' },
|
||||
{ name: 'open_folder' },
|
||||
{ name: 'copy_path' },
|
||||
{ type: 'divider' },
|
||||
@@ -150,6 +155,9 @@ const handleOptionsClick = async (args) => {
|
||||
path: `/instance/${encodeURIComponent(args.item.path)}/`,
|
||||
})
|
||||
break
|
||||
case 'duplicate':
|
||||
if (args.item.install_stage == 'installed') await duplicateProfile(args.item.path)
|
||||
break
|
||||
case 'delete':
|
||||
currentDeleteInstance.value = args.item.path
|
||||
deleteConfirmModal.value.show()
|
||||
@@ -237,7 +245,7 @@ onUnmounted(() => {
|
||||
<section v-if="row.instances[0].metadata" ref="modsRow" class="instances">
|
||||
<Instance
|
||||
v-for="instance in row.instances.slice(0, maxInstancesPerRow)"
|
||||
:key="instance?.project_id || instance?.id"
|
||||
:key="(instance?.project_id || instance?.id) + instance.install_stage"
|
||||
:instance="instance"
|
||||
@contextmenu.prevent.stop="(event) => handleInstanceRightClick(event, instance)"
|
||||
/>
|
||||
@@ -263,6 +271,7 @@ onUnmounted(() => {
|
||||
<template #edit> <EyeIcon /> View instance </template>
|
||||
<template #delete> <TrashIcon /> Delete </template>
|
||||
<template #open_folder> <FolderOpenIcon /> Open folder </template>
|
||||
<template #duplicate> <ClipboardCopyIcon /> Duplicate instance</template>
|
||||
<template #copy_path> <ClipboardCopyIcon /> Copy path </template>
|
||||
<template #install> <DownloadIcon /> Install </template>
|
||||
<template #open_link> <GlobeIcon /> Open in Modrinth <ExternalIcon /> </template>
|
||||
|
||||
@@ -105,7 +105,7 @@ import {
|
||||
GlobeIcon,
|
||||
ClipboardCopyIcon,
|
||||
} from 'omorphia'
|
||||
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { ref, computed, onMounted, onBeforeUnmount, onUnmounted } from 'vue'
|
||||
import {
|
||||
users,
|
||||
remove_user,
|
||||
@@ -116,6 +116,7 @@ import { get, set } from '@/helpers/settings'
|
||||
import { handleError } from '@/store/state.js'
|
||||
import { mixpanel_track } from '@/helpers/mixpanel'
|
||||
import QrcodeVue from 'qrcode.vue'
|
||||
import { process_listener } from '@/helpers/events'
|
||||
|
||||
defineProps({
|
||||
mode: {
|
||||
@@ -214,6 +215,12 @@ const handleClickOutside = (event) => {
|
||||
}
|
||||
}
|
||||
|
||||
const unlisten = await process_listener(async (e) => {
|
||||
if (e.event === 'launched') {
|
||||
await refreshValues()
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('click', handleClickOutside)
|
||||
})
|
||||
@@ -221,6 +228,10 @@ onMounted(() => {
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('click', handleClickOutside)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
unlisten()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { Button, Checkbox, Modal, SendIcon, XIcon } from 'omorphia'
|
||||
import { Button, Checkbox, Modal, XIcon, PlusIcon } from 'omorphia'
|
||||
import { PackageIcon, VersionIcon } from '@/assets/icons'
|
||||
import { ref } from 'vue'
|
||||
import { export_profile_mrpack, get_potential_override_folders } from '@/helpers/profile.js'
|
||||
@@ -24,9 +24,11 @@ defineExpose({
|
||||
|
||||
const exportModal = ref(null)
|
||||
const nameInput = ref(props.instance.metadata.name)
|
||||
const exportDescription = ref('')
|
||||
const versionInput = ref('1.0.0')
|
||||
const files = ref([])
|
||||
const folders = ref([])
|
||||
const showingFiles = ref(false)
|
||||
|
||||
const themeStore = useTheming()
|
||||
|
||||
@@ -93,7 +95,9 @@ const exportPack = async () => {
|
||||
props.instance.path,
|
||||
outputPath + `/${nameInput.value} ${versionInput.value}.mrpack`,
|
||||
filesToExport,
|
||||
versionInput.value
|
||||
versionInput.value,
|
||||
exportDescription.value,
|
||||
nameInput.value
|
||||
).catch((err) => handleError(err))
|
||||
exportModal.value.hide()
|
||||
}
|
||||
@@ -123,11 +127,31 @@ const exportPack = async () => {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<div class="labeled_input">
|
||||
<p>Description</p>
|
||||
|
||||
<div class="textarea-wrapper">
|
||||
<textarea v-model="exportDescription" placeholder="Enter modpack description..." />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table">
|
||||
<div class="table-head">
|
||||
<div class="table-cell">Select files and folders to include in pack</div>
|
||||
<div class="table-cell row-wise">
|
||||
Select files and folders to include in pack
|
||||
<Button
|
||||
class="sleek-primary collapsed-button"
|
||||
icon-only
|
||||
@click="() => (showingFiles = !showingFiles)"
|
||||
>
|
||||
<PlusIcon v-if="!showingFiles" />
|
||||
<XIcon v-else />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-content">
|
||||
<div v-if="showingFiles" class="table-content">
|
||||
<div v-for="[path, children] of folders" :key="path.name" class="table-row">
|
||||
<div class="table-cell file-entry">
|
||||
<div class="file-primary">
|
||||
@@ -177,10 +201,6 @@ const exportPack = async () => {
|
||||
<XIcon />
|
||||
Cancel
|
||||
</Button>
|
||||
<Button disabled>
|
||||
<SendIcon />
|
||||
Share
|
||||
</Button>
|
||||
<Button color="primary" @click="exportPack">
|
||||
<PackageIcon />
|
||||
Export
|
||||
@@ -261,4 +281,22 @@ const exportPack = async () => {
|
||||
gap: var(--gap-sm);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.row-wise {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.textarea-wrapper {
|
||||
// margin-top: 1rem;
|
||||
height: 12rem;
|
||||
textarea {
|
||||
max-height: 12rem;
|
||||
}
|
||||
.preview {
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -18,6 +18,14 @@
|
||||
"
|
||||
/>
|
||||
<span class="installation-buttons">
|
||||
<Button
|
||||
v-if="props.version"
|
||||
:disabled="props.disabled || installingJava"
|
||||
@click="reinstallJava"
|
||||
>
|
||||
<DownloadIcon />
|
||||
{{ installingJava ? 'Installing...' : 'Install recommended' }}
|
||||
</Button>
|
||||
<Button :disabled="props.disabled" @click="autoDetect">
|
||||
<SearchIcon />
|
||||
Auto detect
|
||||
@@ -44,8 +52,22 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Button, SearchIcon, PlayIcon, CheckIcon, XIcon, FolderSearchIcon } from 'omorphia'
|
||||
import { find_jre_17_jres, get_jre } from '@/helpers/jre.js'
|
||||
import {
|
||||
Button,
|
||||
SearchIcon,
|
||||
PlayIcon,
|
||||
CheckIcon,
|
||||
XIcon,
|
||||
FolderSearchIcon,
|
||||
DownloadIcon,
|
||||
} from 'omorphia'
|
||||
import {
|
||||
auto_install_java,
|
||||
find_jre_17_jres,
|
||||
find_jre_8_jres,
|
||||
get_jre,
|
||||
test_jre,
|
||||
} from '@/helpers/jre.js'
|
||||
import { ref } from 'vue'
|
||||
import { open } from '@tauri-apps/api/dialog'
|
||||
import JavaDetectionModal from '@/components/ui/JavaDetectionModal.vue'
|
||||
@@ -82,15 +104,21 @@ const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const testingJava = ref(false)
|
||||
const testingJavaSuccess = ref(null)
|
||||
|
||||
const installingJava = ref(false)
|
||||
|
||||
async function testJava() {
|
||||
testingJava.value = true
|
||||
let result = await get_jre(props.modelValue ? props.modelValue.path : '')
|
||||
testingJavaSuccess.value = await test_jre(
|
||||
props.modelValue ? props.modelValue.path : '',
|
||||
1,
|
||||
props.version
|
||||
)
|
||||
testingJava.value = false
|
||||
testingJavaSuccess.value = !!result
|
||||
|
||||
mixpanel_track('JavaTest', {
|
||||
path: props.modelValue ? props.modelValue.path : '',
|
||||
success: !!result,
|
||||
success: testingJavaSuccess.value,
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
@@ -109,13 +137,13 @@ async function handleJavaFileInput() {
|
||||
version: props.version.toString(),
|
||||
architecture: 'x86',
|
||||
}
|
||||
|
||||
mixpanel_track('JavaManualSelect', {
|
||||
path: filePath,
|
||||
version: props.version,
|
||||
})
|
||||
}
|
||||
|
||||
mixpanel_track('JavaManualSelect', {
|
||||
path: filePath,
|
||||
version: props.version,
|
||||
})
|
||||
|
||||
emit('update:modelValue', result)
|
||||
}
|
||||
}
|
||||
@@ -125,12 +153,43 @@ async function autoDetect() {
|
||||
if (!props.compact) {
|
||||
detectJavaModal.value.show(props.version, props.modelValue)
|
||||
} else {
|
||||
let versions = await find_jre_17_jres().catch(handleError)
|
||||
if (versions.length > 0) {
|
||||
emit('update:modelValue', versions[0])
|
||||
if (props.version == 8) {
|
||||
let versions = await find_jre_8_jres().catch(handleError)
|
||||
if (versions.length > 0) {
|
||||
emit('update:modelValue', versions[0])
|
||||
}
|
||||
} else {
|
||||
let versions = await find_jre_17_jres().catch(handleError)
|
||||
if (versions.length > 0) {
|
||||
emit('update:modelValue', versions[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function reinstallJava() {
|
||||
installingJava.value = true
|
||||
const path = await auto_install_java(props.version).catch(handleError)
|
||||
console.log('java path: ' + path)
|
||||
let result = await get_jre(path)
|
||||
|
||||
console.log('java result ' + result)
|
||||
if (!result) {
|
||||
result = {
|
||||
path: path,
|
||||
version: props.version.toString(),
|
||||
architecture: 'x86',
|
||||
}
|
||||
}
|
||||
|
||||
mixpanel_track('JavaReInstall', {
|
||||
path: path,
|
||||
version: props.version,
|
||||
})
|
||||
|
||||
emit('update:modelValue', result)
|
||||
installingJava.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
187
theseus_gui/src/components/ui/ModpackVersionModal.vue
Normal file
187
theseus_gui/src/components/ui/ModpackVersionModal.vue
Normal file
@@ -0,0 +1,187 @@
|
||||
<script setup>
|
||||
import { Button, Modal, CheckIcon, Badge } from 'omorphia'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useTheming } from '@/store/theme'
|
||||
import { update_managed_modrinth_version } from '@/helpers/profile'
|
||||
import { releaseColor } from '@/helpers/utils'
|
||||
import { SwapIcon } from '@/assets/icons/index.js'
|
||||
|
||||
const props = defineProps({
|
||||
versions: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
instance: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
show: () => {
|
||||
modpackVersionModal.value.show()
|
||||
},
|
||||
})
|
||||
|
||||
const filteredVersions = computed(() => {
|
||||
return props.versions
|
||||
})
|
||||
|
||||
const modpackVersionModal = ref(null)
|
||||
const installedVersion = computed(() => props.instance?.metadata?.linked_data?.version_id)
|
||||
const installing = computed(() => props.instance.install_stage !== 'installed')
|
||||
const inProgress = ref(false)
|
||||
|
||||
const themeStore = useTheming()
|
||||
|
||||
const switchVersion = async (versionId) => {
|
||||
inProgress.value = true
|
||||
await update_managed_modrinth_version(props.instance.path, versionId)
|
||||
inProgress.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
ref="modpackVersionModal"
|
||||
class="modpack-version-modal"
|
||||
header="Change modpack version"
|
||||
:noblur="!themeStore.advancedRendering"
|
||||
>
|
||||
<div class="modal-body">
|
||||
<Card v-if="instance.metadata.linked_data" class="mod-card">
|
||||
<div class="table">
|
||||
<div class="table-row with-columns 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>
|
||||
<div class="scrollable">
|
||||
<div
|
||||
v-for="version in filteredVersions"
|
||||
:key="version.id"
|
||||
class="table-row with-columns selectable"
|
||||
@click="$router.push(`/project/${$route.params.id}/version/${version.id}`)"
|
||||
>
|
||||
<div class="table-cell table-text">
|
||||
<Button
|
||||
:color="version.id === installedVersion ? '' : 'primary'"
|
||||
icon-only
|
||||
:disabled="inProgress || installing || version.id === installedVersion"
|
||||
@click.stop="() => switchVersion(version.id)"
|
||||
>
|
||||
<SwapIcon v-if="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>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.filter-header {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.with-columns {
|
||||
grid-template-columns: min-content 1fr 1fr;
|
||||
}
|
||||
|
||||
.scrollable {
|
||||
overflow-y: auto;
|
||||
max-height: 25rem;
|
||||
}
|
||||
|
||||
.card-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background-color: var(--color-raised-bg);
|
||||
}
|
||||
|
||||
.mod-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
overflow: hidden;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.version-link {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
|
||||
.version-badge {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.channel-indicator {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stacked-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.download-cell {
|
||||
width: 4rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: var(--gap-xl);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap-md);
|
||||
}
|
||||
|
||||
.table {
|
||||
border: 1px solid var(--color-bg);
|
||||
}
|
||||
</style>
|
||||
@@ -176,7 +176,7 @@ defineProps({
|
||||
</Card>
|
||||
</aside>
|
||||
<div ref="searchWrapper" class="search">
|
||||
<Promotion class="promotion" query-param="?r=launcher" />
|
||||
<Promotion class="promotion" :external="false" query-param="?r=launcher" />
|
||||
<Card class="project-type-container">
|
||||
<NavRow :links="selectableProjectTypes" />
|
||||
</Card>
|
||||
|
||||
@@ -22,8 +22,9 @@ const pageOptions = ['Home', 'Library']
|
||||
id="theme"
|
||||
name="Theme dropdown"
|
||||
:options="['Dark']"
|
||||
:disabled="true"
|
||||
:default-value="'dark'"
|
||||
class="theme-dropdown"
|
||||
class="theme-dropdown disable-children"
|
||||
/>
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
@@ -33,7 +34,7 @@ const pageOptions = ['Home', 'Library']
|
||||
>Change the style of the side navigation bar to a compact version.</span
|
||||
>
|
||||
</label>
|
||||
<Toggle id="collapsed-nav" :checked="false" />
|
||||
<Toggle id="collapsed-nav" :checked="false" :disabled="true" />
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<label for="advanced-rendering">
|
||||
@@ -43,7 +44,7 @@ const pageOptions = ['Home', 'Library']
|
||||
without hardware-accelerated rendering.
|
||||
</span>
|
||||
</label>
|
||||
<Toggle id="advanced-rendering" :checked="true" />
|
||||
<Toggle id="advanced-rendering" :checked="true" :disabled="true" />
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<label for="minimize-launcher">
|
||||
@@ -52,7 +53,7 @@ const pageOptions = ['Home', 'Library']
|
||||
>Minimize the launcher when a Minecraft process starts.</span
|
||||
>
|
||||
</label>
|
||||
<Toggle id="minimize-launcher" :checked="false" />
|
||||
<Toggle id="minimize-launcher" :checked="false" :disabled="true" />
|
||||
</div>
|
||||
<div class="opening-page">
|
||||
<label for="opening-page">
|
||||
@@ -65,6 +66,7 @@ const pageOptions = ['Home', 'Library']
|
||||
:options="pageOptions"
|
||||
default-value="Home"
|
||||
class="opening-page"
|
||||
:disabled="true"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -82,7 +84,7 @@ const pageOptions = ['Home', 'Library']
|
||||
lower value if you have a poor internet connection.</span
|
||||
>
|
||||
</label>
|
||||
<Slider id="max-downloads" :min="1" :max="10" :step="1" />
|
||||
<Slider id="max-downloads" :min="1" :max="10" :step="1" :disabled="true" />
|
||||
</div>
|
||||
|
||||
<div class="adjacent-input">
|
||||
@@ -93,7 +95,7 @@ const pageOptions = ['Home', 'Library']
|
||||
lower value if you are frequently getting I/O errors.</span
|
||||
>
|
||||
</label>
|
||||
<Slider id="max-writes" :min="1" :max="50" :step="1" />
|
||||
<Slider id="max-writes" :min="1" :max="50" :step="1" :disabled="true" />
|
||||
</div>
|
||||
</Card>
|
||||
<Card>
|
||||
@@ -110,7 +112,7 @@ const pageOptions = ['Home', 'Library']
|
||||
customize your experience. Opting out will disable this data collection.
|
||||
</span>
|
||||
</label>
|
||||
<Toggle id="opt-out-analytics" />
|
||||
<Toggle id="opt-out-analytics" :disabled="true" />
|
||||
</div>
|
||||
</Card>
|
||||
<Card>
|
||||
@@ -122,11 +124,11 @@ const pageOptions = ['Home', 'Library']
|
||||
<label for="java-17">
|
||||
<span class="label__title">Java 17 location</span>
|
||||
</label>
|
||||
<JavaSelector id="java-17" :version="17" model-value="" />
|
||||
<JavaSelector id="java-17" :version="17" model-value="" :disabled="true" />
|
||||
<label for="java-8">
|
||||
<span class="label__title">Java 8 location</span>
|
||||
</label>
|
||||
<JavaSelector id="java-8" :version="8" model-value="" />
|
||||
<JavaSelector id="java-8" :version="8" model-value="" :disabled="true" />
|
||||
<hr class="card-divider" />
|
||||
<label for="java-args">
|
||||
<span class="label__title">Java arguments</span>
|
||||
@@ -137,6 +139,7 @@ const pageOptions = ['Home', 'Library']
|
||||
type="text"
|
||||
class="installation-input"
|
||||
placeholder="Enter java arguments..."
|
||||
:disabled="true"
|
||||
/>
|
||||
<label for="env-vars">
|
||||
<span class="label__title">Environmental variables</span>
|
||||
@@ -147,6 +150,7 @@ const pageOptions = ['Home', 'Library']
|
||||
type="text"
|
||||
class="installation-input"
|
||||
placeholder="Enter environmental variables..."
|
||||
:disabled="true"
|
||||
/>
|
||||
<hr class="card-divider" />
|
||||
<div class="adjacent-input">
|
||||
@@ -156,7 +160,7 @@ const pageOptions = ['Home', 'Library']
|
||||
The memory allocated to each instance when it is ran.
|
||||
</span>
|
||||
</label>
|
||||
<Slider id="max-memory" :min="256" :max="10256" :step="1" unit="mb" />
|
||||
<Slider id="max-memory" :min="256" :max="10256" :step="1" unit="mb" :disabled="true" />
|
||||
</div>
|
||||
</Card>
|
||||
<Card>
|
||||
@@ -175,6 +179,7 @@ const pageOptions = ['Home', 'Library']
|
||||
autocomplete="off"
|
||||
type="text"
|
||||
placeholder="Enter pre-launch command..."
|
||||
:disabled="true"
|
||||
/>
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
@@ -182,7 +187,13 @@ const pageOptions = ['Home', 'Library']
|
||||
<span class="label__title">Wrapper</span>
|
||||
<span class="label__description"> Wrapper command for launching Minecraft. </span>
|
||||
</label>
|
||||
<input id="wrapper" autocomplete="off" type="text" placeholder="Enter wrapper command..." />
|
||||
<input
|
||||
id="wrapper"
|
||||
autocomplete="off"
|
||||
type="text"
|
||||
placeholder="Enter wrapper command..."
|
||||
:disabled="true"
|
||||
/>
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<label for="post-exit">
|
||||
@@ -194,6 +205,7 @@ const pageOptions = ['Home', 'Library']
|
||||
autocomplete="off"
|
||||
type="text"
|
||||
placeholder="Enter post-exit command..."
|
||||
:disabled="true"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -208,7 +220,13 @@ const pageOptions = ['Home', 'Library']
|
||||
<span class="label__title">Width</span>
|
||||
<span class="label__description"> The width of the game window when launched. </span>
|
||||
</label>
|
||||
<input id="width" autocomplete="off" type="number" placeholder="Enter width..." />
|
||||
<input
|
||||
id="width"
|
||||
autocomplete="off"
|
||||
type="number"
|
||||
placeholder="Enter width..."
|
||||
:disabled="true"
|
||||
/>
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<label for="height">
|
||||
@@ -221,6 +239,7 @@ const pageOptions = ['Home', 'Library']
|
||||
type="number"
|
||||
class="input"
|
||||
placeholder="Enter height..."
|
||||
:disabled="true"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -244,4 +263,8 @@ const pageOptions = ['Home', 'Library']
|
||||
.card-divider {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.disable-children * {
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -206,9 +206,7 @@ onMounted(() => {
|
||||
<Button v-if="twoFactorCode" color="primary" large @click="signIn2fa"> Login </Button>
|
||||
<Button v-else-if="loggingIn" color="primary" large @click="signIn"> Login </Button>
|
||||
<Button v-else color="primary" large @click="createAccount"> Create account </Button>
|
||||
<Button class="transparent" large @click="goToNextPage">
|
||||
{{ modal ? 'Continue' : 'Next' }}
|
||||
</Button>
|
||||
<Button v-if="!modal" class="transparent" large @click="goToNextPage"> Next </Button>
|
||||
</div>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
@@ -295,7 +295,7 @@ onMounted(async () => {
|
||||
:previous-function="prevPhase"
|
||||
:progress="phase"
|
||||
title="Settings"
|
||||
description="You can view and change the settings for the Modrinth App here. You can change the appearance, set and download new Java versions, and more."
|
||||
description="You will be able to view and change the settings for the Modrinth App here. You can change the appearance, set and download new Java versions, and more."
|
||||
/>
|
||||
<TutorialTip
|
||||
v-if="phase === 9"
|
||||
|
||||
@@ -59,6 +59,12 @@ export async function get_jre(path) {
|
||||
return await invoke('plugin:jre|jre_get_jre', { path })
|
||||
}
|
||||
|
||||
// Tests JRE version by running 'java -version' on it.
|
||||
// Returns true if the version is valid, and matches given (after extraction)
|
||||
export async function test_jre(path, majorVersion, minorVersion) {
|
||||
return await invoke('plugin:jre|jre_test_jre', { path, majorVersion, minorVersion })
|
||||
}
|
||||
|
||||
// Autodetect Java globals, by searching the users computer.
|
||||
// Returns a *NEW* JavaGlobals that can be put into Settings
|
||||
export async function autodetect_java_globals() {
|
||||
|
||||
@@ -6,37 +6,50 @@
|
||||
import { invoke } from '@tauri-apps/api/tauri'
|
||||
|
||||
/*
|
||||
A log is a struct containing the datetime string, stdout, and stderr, as follows:
|
||||
A log is a struct containing the filename string, stdout, and stderr, as follows:
|
||||
|
||||
pub struct Logs {
|
||||
pub datetime_string: String,
|
||||
pub filename: String,
|
||||
pub stdout: String,
|
||||
pub stderr: String,
|
||||
}
|
||||
*/
|
||||
|
||||
/// Get all logs that exist for a given profile
|
||||
/// This is returned as an array of Log objects, sorted by datetime_string (the folder name, when the log was created)
|
||||
/// This is returned as an array of Log objects, sorted by filename (the folder name, when the log was created)
|
||||
export async function get_logs(profilePath, clearContents) {
|
||||
return await invoke('plugin:logs|logs_get_logs', { profilePath, clearContents })
|
||||
}
|
||||
|
||||
/// Get a profile's log by datetime_string (the folder name, when the log was created)
|
||||
export async function get_logs_by_datetime(profilePath, datetimeString) {
|
||||
return await invoke('plugin:logs|logs_get_logs_by_datetime', { profilePath, datetimeString })
|
||||
/// Get a profile's log by filename
|
||||
export async function get_logs_by_filename(profilePath, filename) {
|
||||
return await invoke('plugin:logs|logs_get_logs_by_filename', { profilePath, filename })
|
||||
}
|
||||
|
||||
/// Get a profile's stdout only by datetime_string (the folder name, when the log was created)
|
||||
export async function get_output_by_datetime(profilePath, datetimeString) {
|
||||
return await invoke('plugin:logs|logs_get_output_by_datetime', { profilePath, datetimeString })
|
||||
/// Get a profile's log text only by filename
|
||||
export async function get_output_by_filename(profilePath, filename) {
|
||||
return await invoke('plugin:logs|logs_get_output_by_filename', { profilePath, filename })
|
||||
}
|
||||
|
||||
/// Delete a profile's log by datetime_string (the folder name, when the log was created)
|
||||
export async function delete_logs_by_datetime(profilePath, datetimeString) {
|
||||
return await invoke('plugin:logs|logs_delete_logs_by_datetime', { profilePath, datetimeString })
|
||||
/// Delete a profile's log by filename
|
||||
export async function delete_logs_by_filename(profilePath, filename) {
|
||||
return await invoke('plugin:logs|logs_delete_logs_by_filename', { profilePath, filename })
|
||||
}
|
||||
|
||||
/// Delete all logs for a given profile
|
||||
export async function delete_logs(profilePath) {
|
||||
return await invoke('plugin:logs|logs_delete_logs', { profilePath })
|
||||
}
|
||||
|
||||
/// Get the latest log for a given profile and cursor (startpoint to read withi nthe file)
|
||||
/// Returns:
|
||||
/*
|
||||
{
|
||||
cursor: u64
|
||||
output: String
|
||||
new_file: bool <- the cursor was too far, meaning that the file was likely rotated/reset. This signals to the frontend to clear the log and start over with this struct.
|
||||
}
|
||||
*/
|
||||
export async function get_latest_log_cursor(profilePath, cursor) {
|
||||
return await invoke('plugin:logs|logs_get_latest_log_cursor', { profilePath, cursor })
|
||||
}
|
||||
|
||||
@@ -47,12 +47,6 @@ export async function get_all_running_profiles() {
|
||||
return await invoke('plugin:process|process_get_all_running_profiles')
|
||||
}
|
||||
|
||||
/// Gets process stdout by UUID
|
||||
/// Returns String
|
||||
export async function get_output_by_uuid(uuid) {
|
||||
return await invoke('plugin:process|process_get_output_by_uuid', { uuid })
|
||||
}
|
||||
|
||||
/// Kills a process by UUID
|
||||
export async function kill_by_uuid(uuid) {
|
||||
return await invoke('plugin:process|process_kill_by_uuid', { uuid })
|
||||
|
||||
@@ -27,6 +27,11 @@ export async function create(name, gameVersion, modloader, loaderVersion, icon,
|
||||
})
|
||||
}
|
||||
|
||||
// duplicate a profile
|
||||
export async function duplicate(path) {
|
||||
return await invoke('plugin:profile_create|profile_duplicate', { path })
|
||||
}
|
||||
|
||||
// Remove a profile
|
||||
export async function remove(path) {
|
||||
return await invoke('plugin:profile|profile_remove', { path })
|
||||
@@ -44,6 +49,12 @@ export async function get_full_path(path) {
|
||||
return await invoke('plugin:profile|profile_get_full_path', { path })
|
||||
}
|
||||
|
||||
// Get's a mod's full fs path
|
||||
// Returns a path
|
||||
export async function get_mod_full_path(path, projectPath) {
|
||||
return await invoke('plugin:profile|profile_get_mod_full_path', { path, projectPath })
|
||||
}
|
||||
|
||||
// Get optimal java version from profile
|
||||
// Returns a java version
|
||||
export async function get_optimal_jre_key(path) {
|
||||
@@ -101,9 +112,9 @@ export async function remove_project(path, projectPath) {
|
||||
return await invoke('plugin:profile|profile_remove_project', { path, projectPath })
|
||||
}
|
||||
|
||||
// Update a managed Modrinth profile
|
||||
export async function update_managed_modrinth(path) {
|
||||
return await invoke('plugin:profile|profile_update_managed_modrinth', { path })
|
||||
// Update a managed Modrinth profile to a specific version
|
||||
export async function update_managed_modrinth_version(path, versionId) {
|
||||
return await invoke('plugin:profile|profile_update_managed_modrinth_version', { path, versionId })
|
||||
}
|
||||
|
||||
// Repair a managed Modrinth profile
|
||||
@@ -114,12 +125,21 @@ export async function update_repair_modrinth(path) {
|
||||
// Export a profile to .mrpack
|
||||
/// included_overrides is an array of paths to override folders to include (ie: 'mods', 'resource_packs')
|
||||
// Version id is optional (ie: 1.1.5)
|
||||
export async function export_profile_mrpack(path, exportLocation, includedOverrides, versionId) {
|
||||
export async function export_profile_mrpack(
|
||||
path,
|
||||
exportLocation,
|
||||
includedOverrides,
|
||||
versionId,
|
||||
description,
|
||||
name
|
||||
) {
|
||||
return await invoke('plugin:profile|profile_export_mrpack', {
|
||||
path,
|
||||
exportLocation,
|
||||
includedOverrides,
|
||||
versionId,
|
||||
description,
|
||||
name,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
add_project_from_version as installMod,
|
||||
check_installed,
|
||||
get_full_path,
|
||||
get_mod_full_path,
|
||||
} from '@/helpers/profile'
|
||||
import { useFetch } from '@/helpers/fetch.js'
|
||||
import { handleError } from '@/store/notifications.js'
|
||||
@@ -20,12 +21,21 @@ export async function showInFolder(path) {
|
||||
return await invoke('plugin:utils|show_in_folder', { path })
|
||||
}
|
||||
|
||||
export async function showLauncherLogsFolder() {
|
||||
return await invoke('plugin:utils|show_launcher_logs_folder', {})
|
||||
}
|
||||
|
||||
// Opens a profile's folder in the OS file explorer
|
||||
export async function showProfileInFolder(path) {
|
||||
const fullPath = await get_full_path(path)
|
||||
return await showInFolder(fullPath)
|
||||
}
|
||||
|
||||
export async function highlightModInProfile(profilePath, projectPath) {
|
||||
const fullPath = await get_mod_full_path(profilePath, projectPath)
|
||||
return await showInFolder(fullPath)
|
||||
}
|
||||
|
||||
export const releaseColor = (releaseType) => {
|
||||
switch (releaseType) {
|
||||
case 'release':
|
||||
|
||||
@@ -59,5 +59,6 @@ initialize_state()
|
||||
})
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err)
|
||||
console.error('Failed to initialize app', err)
|
||||
mountedApp.failure(err)
|
||||
})
|
||||
|
||||
@@ -133,11 +133,7 @@ async function refreshDir() {
|
||||
class="login-screen-modal"
|
||||
:noblur="!themeStore.advancedRendering"
|
||||
>
|
||||
<ModrinthLoginScreen
|
||||
:modal="true"
|
||||
:prev-page="$refs.loginScreenModal.show()"
|
||||
:next-page="signInAfter"
|
||||
/>
|
||||
<ModrinthLoginScreen :modal="true" :prev-page="signInAfter" :next-page="signInAfter" />
|
||||
</Modal>
|
||||
<div class="adjacent-input">
|
||||
<label for="theme">
|
||||
@@ -323,6 +319,21 @@ async function refreshDir() {
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<label for="disable-discord-rpc">
|
||||
<span class="label__title">Disable Discord RPC</span>
|
||||
<span class="label__description">
|
||||
Disables the Discord Rich Presence integration. 'Modrinth' will no longer show up as a
|
||||
game or app you are using on your Discord profile. This does not disable any
|
||||
instance-specific Discord Rich Presence integrations, such as those added by mods.
|
||||
</span>
|
||||
</label>
|
||||
<Toggle
|
||||
id="disable-discord-rpc"
|
||||
v-model="settings.disable_discord_rpc"
|
||||
:checked="settings.disable_discord_rpc"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
<Card>
|
||||
<div class="label">
|
||||
@@ -372,7 +383,7 @@ async function refreshDir() {
|
||||
<Slider
|
||||
id="max-memory"
|
||||
v-model="settings.memory.maximum"
|
||||
:min="256"
|
||||
:min="8"
|
||||
:max="maxMemory"
|
||||
:step="1"
|
||||
unit="mb"
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
</Card>
|
||||
</div>
|
||||
<div class="content">
|
||||
<Promotion query-param="?r=launcher" />
|
||||
<Promotion :external="false" query-param="?r=launcher" />
|
||||
<RouterView v-slot="{ Component }">
|
||||
<template v-if="Component">
|
||||
<Suspense @pending="loadingBar.startLoading()" @resolve="loadingBar.stopLoading()">
|
||||
@@ -84,6 +84,9 @@
|
||||
:instance="instance"
|
||||
:options="options"
|
||||
:offline="offline"
|
||||
:playing="playing"
|
||||
:versions="modrinthVersions"
|
||||
:installed="instance.install_stage !== 'installed'"
|
||||
></component>
|
||||
</Suspense>
|
||||
</template>
|
||||
@@ -149,6 +152,7 @@ import { isOffline, showProfileInFolder } from '@/helpers/utils.js'
|
||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||
import { mixpanel_track } from '@/helpers/mixpanel'
|
||||
import { convertFileSrc } from '@tauri-apps/api/tauri'
|
||||
import { useFetch } from '@/helpers/fetch'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
@@ -197,6 +201,15 @@ const checkProcess = async () => {
|
||||
uuid.value = null
|
||||
}
|
||||
|
||||
// Get information on associated modrinth versions, if any
|
||||
const modrinthVersions = ref([])
|
||||
if (!(await isOffline()) && instance.value.metadata.linked_data) {
|
||||
modrinthVersions.value = await useFetch(
|
||||
`https://api.modrinth.com/v2/project/${instance.value.metadata.linked_data.project_id}/version`,
|
||||
'project'
|
||||
)
|
||||
}
|
||||
|
||||
await checkProcess()
|
||||
|
||||
const stopInstance = async (context) => {
|
||||
|
||||
@@ -20,6 +20,15 @@
|
||||
Share
|
||||
</Button>
|
||||
<Button
|
||||
v-if="logs[selectedLogIndex] && logs[selectedLogIndex].live === true"
|
||||
@click="clearLiveLog()"
|
||||
>
|
||||
<TrashIcon />
|
||||
Clear
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
v-else
|
||||
:disabled="!logs[selectedLogIndex] || logs[selectedLogIndex].live === true"
|
||||
color="danger"
|
||||
@click="deleteLog()"
|
||||
@@ -29,14 +38,43 @@
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div ref="logContainer" class="log-text">
|
||||
<span
|
||||
v-for="(line, index) in logs[selectedLogIndex]?.stdout.split('\n')"
|
||||
:key="index"
|
||||
class="no-wrap"
|
||||
<div class="button-row">
|
||||
<input
|
||||
id="text-filter"
|
||||
v-model="searchFilter"
|
||||
autocomplete="off"
|
||||
type="text"
|
||||
class="text-filter"
|
||||
placeholder="Type to filter logs..."
|
||||
/>
|
||||
<div class="filter-group">
|
||||
<Checkbox
|
||||
v-for="level in levels"
|
||||
:key="level.toLowerCase()"
|
||||
v-model="levelFilters[level.toLowerCase()]"
|
||||
class="filter-checkbox"
|
||||
>
|
||||
{{ level }}</Checkbox
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="log-text">
|
||||
<RecycleScroller
|
||||
v-slot="{ item }"
|
||||
ref="logContainer"
|
||||
class="scroller"
|
||||
:items="displayProcessedLogs"
|
||||
direction="vertical"
|
||||
:item-size="20"
|
||||
key-field="id"
|
||||
>
|
||||
{{ line }} <br />
|
||||
</span>
|
||||
<div class="user no-wrap">
|
||||
<span :style="{ color: item.prefixColor, 'font-weight': item.weight }">{{
|
||||
item.prefix
|
||||
}}</span>
|
||||
<span :style="{ color: item.textColor }">{{ item.text }}</span>
|
||||
</div>
|
||||
</RecycleScroller>
|
||||
</div>
|
||||
<ShareModal
|
||||
ref="shareModal"
|
||||
@@ -56,20 +94,31 @@ import {
|
||||
ClipboardCopyIcon,
|
||||
DropdownSelect,
|
||||
ShareIcon,
|
||||
Checkbox,
|
||||
TrashIcon,
|
||||
ShareModal,
|
||||
} from 'omorphia'
|
||||
import { delete_logs_by_datetime, get_logs, get_output_by_datetime } from '@/helpers/logs.js'
|
||||
import { nextTick, onBeforeUnmount, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import {
|
||||
delete_logs_by_filename,
|
||||
get_logs,
|
||||
get_output_by_filename,
|
||||
get_latest_log_cursor,
|
||||
} from '@/helpers/logs.js'
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import dayjs from 'dayjs'
|
||||
import calendar from 'dayjs/plugin/calendar'
|
||||
import { get_output_by_uuid, get_uuids_by_profile_path } from '@/helpers/process.js'
|
||||
import isToday from 'dayjs/plugin/isToday'
|
||||
import isYesterday from 'dayjs/plugin/isYesterday'
|
||||
import { get_uuids_by_profile_path } from '@/helpers/process.js'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { process_listener } from '@/helpers/events.js'
|
||||
import { handleError } from '@/store/notifications.js'
|
||||
import { ofetch } from 'ofetch'
|
||||
|
||||
dayjs.extend(calendar)
|
||||
import { RecycleScroller } from 'vue-virtual-scroller'
|
||||
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
|
||||
|
||||
dayjs.extend(isToday)
|
||||
dayjs.extend(isYesterday)
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
@@ -82,11 +131,21 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
playing: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const currentLiveLog = ref(null)
|
||||
const currentLiveLogCursor = ref(0)
|
||||
const emptyText = ['No live game detected.', 'Start your game to proceed']
|
||||
|
||||
const logs = ref([])
|
||||
await setLogs()
|
||||
|
||||
const logsColored = true
|
||||
|
||||
const selectedLogIndex = ref(0)
|
||||
const copied = ref(false)
|
||||
const logContainer = ref(null)
|
||||
@@ -95,16 +154,86 @@ const userScrolled = ref(false)
|
||||
const isAutoScrolling = ref(false)
|
||||
const shareModal = ref(null)
|
||||
|
||||
const levels = ['Comment', 'Error', 'Warn', 'Info', 'Debug', 'Trace']
|
||||
const levelFilters = ref({})
|
||||
levels.forEach((level) => {
|
||||
levelFilters.value[level.toLowerCase()] = true
|
||||
})
|
||||
const searchFilter = ref('')
|
||||
|
||||
function shouldDisplay(processedLine) {
|
||||
if (!processedLine.level) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (!levelFilters.value[processedLine.level.toLowerCase()]) {
|
||||
return false
|
||||
}
|
||||
if (searchFilter.value !== '') {
|
||||
if (!processedLine.text.toLowerCase().includes(searchFilter.value.toLowerCase())) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Selects from the processed logs which ones should be displayed (shouldDisplay)
|
||||
// In addition, splits each line by \n. Each split line is given the same properties as the original line
|
||||
const displayProcessedLogs = computed(() => {
|
||||
return processedLogs.value.filter((l) => shouldDisplay(l))
|
||||
})
|
||||
|
||||
const processedLogs = computed(() => {
|
||||
// split based on newline and timestamp lookahead
|
||||
// (not just newline because of multiline messages)
|
||||
const splitPattern = /\n(?=(?:#|\[\d\d:\d\d:\d\d\]))/
|
||||
|
||||
const lines = logs.value[selectedLogIndex.value]?.stdout.split(splitPattern) || []
|
||||
const processed = []
|
||||
let id = 0
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
// Then split off of \n.
|
||||
// Lines that are not the first have prefix = null
|
||||
const text = getLineText(lines[i])
|
||||
const prefix = getLinePrefix(lines[i])
|
||||
const prefixColor = getLineColor(lines[i], true)
|
||||
const textColor = getLineColor(lines[i], false)
|
||||
const weight = getLineWeight(lines[i])
|
||||
const level = getLineLevel(lines[i])
|
||||
text.split('\n').forEach((line, index) => {
|
||||
processed.push({
|
||||
id: id,
|
||||
text: line,
|
||||
prefix: index === 0 ? prefix : null,
|
||||
prefixColor: prefixColor,
|
||||
textColor: textColor,
|
||||
weight: weight,
|
||||
level: level,
|
||||
})
|
||||
id += 1
|
||||
})
|
||||
}
|
||||
return processed
|
||||
})
|
||||
|
||||
async function getLiveLog() {
|
||||
if (route.params.id) {
|
||||
const uuids = await get_uuids_by_profile_path(route.params.id).catch(handleError)
|
||||
let returnValue
|
||||
if (uuids.length === 0) {
|
||||
returnValue = 'No live game detected. \nStart your game to proceed'
|
||||
returnValue = emptyText.join('\n')
|
||||
} else {
|
||||
returnValue = await get_output_by_uuid(uuids[0]).catch(handleError)
|
||||
const logCursor = await get_latest_log_cursor(
|
||||
props.instance.path,
|
||||
currentLiveLogCursor.value
|
||||
).catch(handleError)
|
||||
if (logCursor.new_file) {
|
||||
currentLiveLog.value = ''
|
||||
}
|
||||
currentLiveLog.value = currentLiveLog.value + logCursor.output
|
||||
currentLiveLogCursor.value = logCursor.cursor
|
||||
returnValue = currentLiveLog.value
|
||||
}
|
||||
|
||||
return { name: 'Live Log', stdout: returnValue, live: true }
|
||||
}
|
||||
return null
|
||||
@@ -112,9 +241,25 @@ async function getLiveLog() {
|
||||
|
||||
async function getLogs() {
|
||||
return (await get_logs(props.instance.path, true).catch(handleError)).reverse().map((log) => {
|
||||
log.name = dayjs(
|
||||
log.datetime_string.slice(0, 8) + 'T' + log.datetime_string.slice(9)
|
||||
).calendar()
|
||||
if (log.filename == 'latest.log') {
|
||||
log.name = 'Latest Log'
|
||||
} else {
|
||||
let filename = log.filename.split('.')[0]
|
||||
let day = dayjs(filename.slice(0, 10))
|
||||
if (day.isValid()) {
|
||||
if (day.isToday()) {
|
||||
log.name = 'Today'
|
||||
} else if (day.isYesterday()) {
|
||||
log.name = 'Yesterday'
|
||||
} else {
|
||||
log.name = day.format('MMMM D, YYYY')
|
||||
}
|
||||
// Displays as "Today-1", "Today-2", etc, matching minecraft log naming but with the date
|
||||
log.name = log.name + filename.slice(10)
|
||||
} else {
|
||||
log.name = filename
|
||||
}
|
||||
}
|
||||
log.stdout = 'Loading...'
|
||||
return log
|
||||
})
|
||||
@@ -152,29 +297,127 @@ watch(selectedLogIndex, async (newIndex) => {
|
||||
|
||||
if (logs.value.length > 1 && newIndex !== 0) {
|
||||
logs.value[newIndex].stdout = 'Loading...'
|
||||
logs.value[newIndex].stdout = await get_output_by_datetime(
|
||||
logs.value[newIndex].stdout = await get_output_by_filename(
|
||||
props.instance.path,
|
||||
logs.value[newIndex].datetime_string
|
||||
logs.value[newIndex].filename
|
||||
).catch(handleError)
|
||||
}
|
||||
})
|
||||
|
||||
if (logs.value.length >= 1) {
|
||||
if (logs.value.length > 1 && !props.playing) {
|
||||
selectedLogIndex.value = 1
|
||||
} else {
|
||||
selectedLogIndex.value = 0
|
||||
}
|
||||
|
||||
const deleteLog = async () => {
|
||||
if (logs.value[selectedLogIndex.value] && selectedLogIndex.value !== 0) {
|
||||
let deleteIndex = selectedLogIndex.value
|
||||
selectedLogIndex.value = deleteIndex - 1
|
||||
await delete_logs_by_datetime(
|
||||
props.instance.path,
|
||||
logs.value[deleteIndex].datetime_string
|
||||
).catch(handleError)
|
||||
await delete_logs_by_filename(props.instance.path, logs.value[deleteIndex].filename).catch(
|
||||
handleError
|
||||
)
|
||||
await setLogs()
|
||||
}
|
||||
}
|
||||
|
||||
const clearLiveLog = async () => {
|
||||
currentLiveLog.value = ''
|
||||
// does not reset cursor
|
||||
}
|
||||
|
||||
const isLineLevel = (text, level) => {
|
||||
if ((text.includes('/INFO') || text.includes('[System] [CHAT]')) && level === 'info') {
|
||||
return true
|
||||
}
|
||||
|
||||
if (text.includes('/WARN') && level === 'warn') {
|
||||
return true
|
||||
}
|
||||
|
||||
if (text.includes('/DEBUG') && level === 'debug') {
|
||||
return true
|
||||
}
|
||||
|
||||
if (text.includes('/TRACE') && level === 'trace') {
|
||||
return true
|
||||
}
|
||||
|
||||
const errorTriggers = ['/ERROR', 'Exception:', ':?]', 'Error', '[thread', ' at']
|
||||
if (level === 'error') {
|
||||
for (const trigger of errorTriggers) {
|
||||
if (text.includes(trigger)) return true
|
||||
}
|
||||
}
|
||||
|
||||
if (text.trim()[0] === '#' && level === 'comment') {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const getLineWeight = (text) => {
|
||||
if (
|
||||
!logsColored ||
|
||||
isLineLevel(text, 'info') ||
|
||||
isLineLevel(text, 'debug') ||
|
||||
isLineLevel(text, 'trace')
|
||||
) {
|
||||
return 'normal'
|
||||
}
|
||||
|
||||
if (isLineLevel(text, 'error') || isLineLevel(text, 'warn')) {
|
||||
return 'bold'
|
||||
}
|
||||
}
|
||||
|
||||
const getLineLevel = (text) => {
|
||||
for (const level of levels) {
|
||||
if (isLineLevel(text, level.toLowerCase())) {
|
||||
return level
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getLineColor = (text, prefix) => {
|
||||
if (isLineLevel(text, 'comment')) {
|
||||
return 'var(--color-green)'
|
||||
}
|
||||
|
||||
if (!logsColored || text.includes('[System] [CHAT]')) {
|
||||
return 'var(--color-white)'
|
||||
}
|
||||
if (
|
||||
(isLineLevel(text, 'info') || isLineLevel(text, 'debug') || isLineLevel(text, 'trace')) &&
|
||||
prefix
|
||||
) {
|
||||
return 'var(--color-blue)'
|
||||
}
|
||||
if (isLineLevel(text, 'warn')) {
|
||||
return 'var(--color-orange)'
|
||||
}
|
||||
if (isLineLevel(text, 'error')) {
|
||||
return 'var(--color-red)'
|
||||
}
|
||||
}
|
||||
|
||||
const getLinePrefix = (text) => {
|
||||
if (text.includes(']:')) {
|
||||
return text.split(']:')[0] + ']:'
|
||||
}
|
||||
}
|
||||
|
||||
const getLineText = (text) => {
|
||||
if (text.includes(']:')) {
|
||||
if (text.split(']:').length > 2) {
|
||||
return text.split(']:').slice(1).join(']:')
|
||||
}
|
||||
return text.split(']:')[1]
|
||||
} else {
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
function handleUserScroll() {
|
||||
if (!isAutoScrolling.value) {
|
||||
userScrolled.value = true
|
||||
@@ -185,19 +428,14 @@ interval.value = setInterval(async () => {
|
||||
if (logs.value.length > 0) {
|
||||
logs.value[0] = await getLiveLog()
|
||||
|
||||
const scroll = logContainer.value.getScroll()
|
||||
// Allow resetting of userScrolled if the user scrolls to the bottom
|
||||
if (selectedLogIndex.value === 0) {
|
||||
if (
|
||||
logContainer.value.scrollTop + logContainer.value.offsetHeight >=
|
||||
logContainer.value.scrollHeight - 10
|
||||
)
|
||||
userScrolled.value = false
|
||||
|
||||
if (scroll.end >= logContainer.value.$el.scrollHeight - 10) userScrolled.value = false
|
||||
if (!userScrolled.value) {
|
||||
await nextTick()
|
||||
isAutoScrolling.value = true
|
||||
logContainer.value.scrollTop =
|
||||
logContainer.value.scrollHeight - logContainer.value.offsetHeight
|
||||
logContainer.value.scrollToItem(displayProcessedLogs.value.length - 1)
|
||||
setTimeout(() => (isAutoScrolling.value = false), 50)
|
||||
}
|
||||
}
|
||||
@@ -206,9 +444,13 @@ interval.value = setInterval(async () => {
|
||||
|
||||
const unlistenProcesses = await process_listener(async (e) => {
|
||||
if (e.event === 'launched') {
|
||||
currentLiveLog.value = ''
|
||||
currentLiveLogCursor.value = 0
|
||||
selectedLogIndex.value = 0
|
||||
}
|
||||
if (e.event === 'finished') {
|
||||
currentLiveLog.value = ''
|
||||
currentLiveLogCursor.value = 0
|
||||
userScrolled.value = false
|
||||
await setLogs()
|
||||
selectedLogIndex.value = 1
|
||||
@@ -216,11 +458,11 @@ const unlistenProcesses = await process_listener(async (e) => {
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
logContainer.value.addEventListener('scroll', handleUserScroll)
|
||||
logContainer.value.$el.addEventListener('scroll', handleUserScroll)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
logContainer.value.removeEventListener('scroll', handleUserScroll)
|
||||
logContainer.value.$el.removeEventListener('scroll', handleUserScroll)
|
||||
})
|
||||
onUnmounted(() => {
|
||||
clearInterval(interval.value)
|
||||
@@ -257,7 +499,9 @@ onUnmounted(() => {
|
||||
color: var(--color-contrast);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 1.5rem;
|
||||
overflow: auto;
|
||||
overflow-x: auto; /* Enables horizontal scrolling */
|
||||
overflow-y: hidden; /* Disables vertical scrolling on this wrapper */
|
||||
white-space: nowrap; /* Keeps content on a single line */
|
||||
white-space: normal;
|
||||
color-scheme: dark;
|
||||
|
||||
@@ -265,4 +509,37 @@ onUnmounted(() => {
|
||||
white-space: pre;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-checkbox {
|
||||
margin-bottom: 0.3rem;
|
||||
font-size: 1rem;
|
||||
|
||||
svg {
|
||||
display: flex;
|
||||
align-self: center;
|
||||
justify-self: center;
|
||||
}
|
||||
}
|
||||
.filter-group {
|
||||
display: flex;
|
||||
padding: 0.6rem;
|
||||
flex-direction: row;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
:deep(.vue-recycle-scroller__item-wrapper) {
|
||||
overflow: visible; /* Enables horizontal scrolling */
|
||||
}
|
||||
|
||||
.scroller {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.user {
|
||||
height: 32%;
|
||||
padding: 0 12px;
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -26,21 +26,25 @@
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
v-if="isPackLinked"
|
||||
v-tooltip="'Modpack is up to date'"
|
||||
:disabled="updatingModpack || !canUpdatePack"
|
||||
v-if="canUpdatePack"
|
||||
:disabled="installing"
|
||||
color="secondary"
|
||||
@click="updateModpack"
|
||||
@click="modpackVersionModal.show()"
|
||||
>
|
||||
<UpdatedIcon />
|
||||
{{ updatingModpack ? 'Updating' : 'Update modpack' }}
|
||||
{{ installing ? 'Updating' : 'Update modpack' }}
|
||||
</Button>
|
||||
<Button v-else @click="exportModal.show()">
|
||||
<Button v-else-if="!isPackLocked" @click="exportModal.show()">
|
||||
<PackageIcon />
|
||||
Export modpack
|
||||
</Button>
|
||||
<Button v-if="!isPackLocked && projects.some((m) => m.outdated)" @click="updateAll">
|
||||
<UpdatedIcon />
|
||||
Update all
|
||||
</Button>
|
||||
|
||||
<DropdownButton
|
||||
v-if="!isPackLinked"
|
||||
v-if="!isPackLocked"
|
||||
:options="['search', 'from_file']"
|
||||
default-value="search"
|
||||
name="add-content-dropdown"
|
||||
@@ -107,9 +111,9 @@
|
||||
<ShareIcon />
|
||||
Share
|
||||
</Button>
|
||||
<div v-tooltip="isPackLinked ? 'Unpair this instance to remove mods' : ''">
|
||||
<div v-tooltip="isPackLocked ? 'Unlock this instance to remove mods' : ''">
|
||||
<Button
|
||||
:disabled="isPackLinked"
|
||||
:disabled="isPackLocked"
|
||||
class="transparent trash"
|
||||
@click="deleteWarning.show()"
|
||||
@mouseover="selectedOption = 'Delete'"
|
||||
@@ -118,20 +122,20 @@
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
<div v-tooltip="isPackLinked ? 'Unpair this instance to update mods' : ''">
|
||||
<div v-tooltip="isPackLocked ? 'Unlock this instance to update mods' : ''">
|
||||
<Button
|
||||
:disabled="isPackLinked || offline"
|
||||
:disabled="isPackLocked || offline"
|
||||
class="transparent update"
|
||||
@click="updateAll()"
|
||||
@click="updateSelected()"
|
||||
@mouseover="selectedOption = 'Update'"
|
||||
>
|
||||
<UpdatedIcon />
|
||||
Update
|
||||
</Button>
|
||||
</div>
|
||||
<div v-tooltip="isPackLinked ? 'Unpair this instance to toggle mods' : ''">
|
||||
<div v-tooltip="isPackLocked ? 'Unlock this instance to toggle mods' : ''">
|
||||
<Button
|
||||
:disabled="isPackLinked"
|
||||
:disabled="isPackLocked"
|
||||
class="transparent"
|
||||
@click="toggleSelected()"
|
||||
@mouseover="selectedOption = 'Toggle'"
|
||||
@@ -232,21 +236,18 @@
|
||||
<span v-tooltip="`${mod.version}`">{{ mod.version }}</span>
|
||||
</div>
|
||||
<div class="table-cell table-text manage">
|
||||
<div v-tooltip="isPackLinked ? 'Unpair this instance to remove mods.' : ''">
|
||||
<Button
|
||||
v-tooltip="'Remove project'"
|
||||
:disabled="isPackLinked"
|
||||
icon-only
|
||||
@click="removeMod(mod)"
|
||||
>
|
||||
<div v-tooltip="isPackLocked ? 'Unlock this instance to remove mods.' : 'Remove project'">
|
||||
<Button :disabled="isPackLocked" icon-only @click="removeMod(mod)">
|
||||
<TrashIcon />
|
||||
</Button>
|
||||
</div>
|
||||
<AnimatedLogo v-if="mod.updating" class="btn icon-only updating-indicator"></AnimatedLogo>
|
||||
<div v-tooltip="isPackLinked ? 'Unpair this instance to update mods.' : ''">
|
||||
<div
|
||||
v-else
|
||||
v-tooltip="isPackLocked ? 'Unlock this instance to update mods.' : 'Update project'"
|
||||
>
|
||||
<Button
|
||||
v-tooltip="'Update project'"
|
||||
:disabled="!mod.outdated || offline || isPackLinked"
|
||||
:disabled="!mod.outdated || offline || isPackLocked"
|
||||
icon-only
|
||||
@click="updateProject(mod)"
|
||||
>
|
||||
@@ -254,10 +255,10 @@
|
||||
<CheckIcon v-else />
|
||||
</Button>
|
||||
</div>
|
||||
<div v-tooltip="isPackLinked ? 'Unpair this instance to toggle mods.' : ''">
|
||||
<div v-tooltip="isPackLocked ? 'Unlock this instance to toggle mods.' : ''">
|
||||
<input
|
||||
id="switch-1"
|
||||
:disabled="isPackLinked"
|
||||
:disabled="isPackLocked"
|
||||
autocomplete="off"
|
||||
type="checkbox"
|
||||
class="switch stylized-toggle"
|
||||
@@ -268,7 +269,7 @@
|
||||
<Button
|
||||
v-tooltip="`Show ${mod.file_name}`"
|
||||
icon-only
|
||||
@click="showProfileInFolder(mod.path)"
|
||||
@click="highlightModInProfile(instance.path, mod.path)"
|
||||
>
|
||||
<FolderOpenIcon />
|
||||
</Button>
|
||||
@@ -301,6 +302,14 @@
|
||||
</DropdownButton>
|
||||
</div>
|
||||
</div>
|
||||
<Pagination
|
||||
v-if="projects.length > 0"
|
||||
:page="currentPage"
|
||||
:count="Math.ceil(search.length / 20)"
|
||||
class="pagination-after"
|
||||
:link-function="(page) => `?page=${page}`"
|
||||
@switch-page="switchPage"
|
||||
/>
|
||||
<Modal ref="deleteWarning" header="Are you sure?">
|
||||
<div class="modal-body">
|
||||
<div class="markdown-body">
|
||||
@@ -349,6 +358,12 @@
|
||||
share-text="Check out the projects I'm using in my modpack!"
|
||||
/>
|
||||
<ExportModal v-if="projects.length > 0" ref="exportModal" :instance="instance" />
|
||||
<ModpackVersionModal
|
||||
v-if="instance.metadata.linked_data"
|
||||
ref="modpackVersionModal"
|
||||
:instance="instance"
|
||||
:versions="props.versions"
|
||||
/>
|
||||
</template>
|
||||
<script setup>
|
||||
import {
|
||||
@@ -385,7 +400,6 @@ import {
|
||||
remove_project,
|
||||
toggle_disable_project,
|
||||
update_all,
|
||||
update_managed_modrinth,
|
||||
update_project,
|
||||
} from '@/helpers/profile.js'
|
||||
import { handleError } from '@/store/notifications.js'
|
||||
@@ -393,9 +407,10 @@ import { mixpanel_track } from '@/helpers/mixpanel'
|
||||
import { open } from '@tauri-apps/api/dialog'
|
||||
import { listen } from '@tauri-apps/api/event'
|
||||
import { convertFileSrc } from '@tauri-apps/api/tauri'
|
||||
import { showProfileInFolder } from '@/helpers/utils.js'
|
||||
import { highlightModInProfile } from '@/helpers/utils.js'
|
||||
import { MenuIcon, ToggleIcon, TextInputIcon, AddProjectImage, PackageIcon } from '@/assets/icons'
|
||||
import ExportModal from '@/components/ui/ExportModal.vue'
|
||||
import ModpackVersionModal from '@/components/ui/ModpackVersionModal.vue'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -418,20 +433,24 @@ const props = defineProps({
|
||||
return false
|
||||
},
|
||||
},
|
||||
versions: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const projects = ref([])
|
||||
const selectionMap = ref(new Map())
|
||||
const showingOptions = ref(false)
|
||||
const isPackLinked = computed(() => {
|
||||
return props.instance.metadata.linked_data
|
||||
const isPackLocked = computed(() => {
|
||||
return props.instance.metadata.linked_data && props.instance.metadata.linked_data.locked
|
||||
})
|
||||
const canUpdatePack = computed(() => {
|
||||
if (!props.instance.metadata.linked_data) return false
|
||||
return props.instance.metadata.linked_data.version_id !== props.instance.modrinth_update_version
|
||||
})
|
||||
const exportModal = ref(null)
|
||||
|
||||
console.log(props.instance)
|
||||
const initProjects = (initInstance) => {
|
||||
projects.value = []
|
||||
if (!initInstance || !initInstance.projects) return
|
||||
@@ -508,6 +527,9 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
const modpackVersionModal = ref(null)
|
||||
const installing = computed(() => props.instance.install_stage !== 'installed')
|
||||
|
||||
const searchFilter = ref('')
|
||||
const selectAll = ref(false)
|
||||
const selectedProjectType = ref('All')
|
||||
@@ -661,6 +683,7 @@ const selectUpdatable = () => {
|
||||
|
||||
const updateProject = async (mod) => {
|
||||
mod.updating = true
|
||||
await new Promise((resolve) => setTimeout(resolve, 0))
|
||||
mod.path = await update_project(props.instance.path, mod.path).catch(handleError)
|
||||
mod.updating = false
|
||||
|
||||
@@ -779,6 +802,14 @@ const toggleSelected = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const updateSelected = async () => {
|
||||
const promises = []
|
||||
for (const project of functionValues.value) {
|
||||
if (project.outdated) promises.push(updateProject(project))
|
||||
}
|
||||
await Promise.all(promises).catch(handleError)
|
||||
}
|
||||
|
||||
const enableAll = async () => {
|
||||
for (const project of functionValues.value) {
|
||||
if (project.disabled) {
|
||||
@@ -828,13 +859,6 @@ const handleContentOptionClick = async (args) => {
|
||||
}
|
||||
}
|
||||
|
||||
const updatingModpack = ref(false)
|
||||
const updateModpack = async () => {
|
||||
updatingModpack.value = true
|
||||
await update_managed_modrinth(props.instance.path).catch(handleError)
|
||||
updatingModpack.value = false
|
||||
}
|
||||
|
||||
watch(selectAll, () => {
|
||||
for (const [key, value] of Array.from(selectionMap.value)) {
|
||||
if (value !== selectAll.value) {
|
||||
@@ -1152,4 +1176,8 @@ onUnmounted(() => {
|
||||
height: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-after {
|
||||
margin-bottom: 5rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,6 +8,56 @@
|
||||
:noblur="!themeStore.advancedRendering"
|
||||
@proceed="removeProfile"
|
||||
/>
|
||||
<Modal
|
||||
ref="modalConfirmUnlock"
|
||||
header="Are you sure you want to unlock this instance?"
|
||||
:noblur="!themeStore.advancedRendering"
|
||||
>
|
||||
<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" :disabled="action_disabled" @click="unlockProfile">
|
||||
<LockIcon />
|
||||
Unlock
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
ref="modalConfirmUnpair"
|
||||
header="Are you sure you want to unpair this instance?"
|
||||
:noblur="!themeStore.advancedRendering"
|
||||
>
|
||||
<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" :disabled="action_disabled" @click="unpairProfile">
|
||||
<XIcon />
|
||||
Unpair
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
ref="changeVersionsModal"
|
||||
header="Change instance versions"
|
||||
@@ -191,7 +241,7 @@
|
||||
<Slider
|
||||
v-model="memory.maximum"
|
||||
:disabled="!overrideMemorySettings"
|
||||
:min="256"
|
||||
:min="8"
|
||||
:max="maxMemory"
|
||||
:step="1"
|
||||
unit="mb"
|
||||
@@ -298,22 +348,110 @@
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
<Card v-if="instance.metadata.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.metadata.name }}
|
||||
</span>
|
||||
<span class="label__description">
|
||||
<strong>Version: </strong>
|
||||
{{
|
||||
installedVersionData.name.charAt(0).toUpperCase() + installedVersionData.name.slice(1)
|
||||
}}
|
||||
</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 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.metadata.linked_data" class="adjacent-input">
|
||||
<label for="repair-profile">
|
||||
<span class="label__title">Unpair instance</span>
|
||||
<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">
|
||||
Removes the link to an external 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.
|
||||
Creates another copy of the instance, including saves, configs, mods, and everything.
|
||||
</span>
|
||||
</label>
|
||||
<Button id="repair-profile" @click="unpairProfile"> <XIcon /> Unpair </Button>
|
||||
<Button
|
||||
id="repair-profile"
|
||||
:disabled:="installing || inProgress || offline"
|
||||
@click="duplicateProfile"
|
||||
>
|
||||
<ClipboardCopyIcon /> Duplicate
|
||||
</Button>
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<label for="repair-profile">
|
||||
@@ -326,29 +464,12 @@
|
||||
<Button
|
||||
id="repair-profile"
|
||||
color="highlight"
|
||||
:disabled="repairing || offline"
|
||||
:disabled="installing || inProgress || repairing || offline"
|
||||
@click="repairProfile"
|
||||
>
|
||||
<HammerIcon /> Repair
|
||||
</Button>
|
||||
</div>
|
||||
<div v-if="props.instance.modrinth_update_version" class="adjacent-input">
|
||||
<label for="repair-profile">
|
||||
<span class="label__title">Reinstall modpack</span>
|
||||
<span class="label__description">
|
||||
Reinstalls Modrinth modpack and checks for corruption. Use this if your game is not
|
||||
launching due to your instance diverging from the Modrinth modpack.
|
||||
</span>
|
||||
</label>
|
||||
<Button
|
||||
id="repair-profile"
|
||||
color="highlight"
|
||||
:disabled="repairing || offline"
|
||||
@click="repairModpack"
|
||||
>
|
||||
<DownloadIcon /> Reinstall
|
||||
</Button>
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<label for="delete-profile">
|
||||
<span class="label__title">Delete instance</span>
|
||||
@@ -367,6 +488,12 @@
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
<ModpackVersionModal
|
||||
v-if="instance.metadata.linked_data"
|
||||
ref="modpackVersionModal"
|
||||
:instance="instance"
|
||||
:versions="props.versions"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -383,14 +510,19 @@ import {
|
||||
DropdownSelect,
|
||||
XIcon,
|
||||
SaveIcon,
|
||||
LockIcon,
|
||||
HammerIcon,
|
||||
DownloadIcon,
|
||||
ModalConfirm,
|
||||
DownloadIcon,
|
||||
ClipboardCopyIcon,
|
||||
Button,
|
||||
} from 'omorphia'
|
||||
import { SwapIcon } from '@/assets/icons'
|
||||
|
||||
import { Multiselect } from 'vue-multiselect'
|
||||
import { useRouter } from 'vue-router'
|
||||
import {
|
||||
duplicate,
|
||||
edit,
|
||||
edit_icon,
|
||||
get_optimal_jre_key,
|
||||
@@ -415,6 +547,7 @@ import { get_game_versions, get_loaders } from '@/helpers/tags.js'
|
||||
import { handleError } from '@/store/notifications.js'
|
||||
import { mixpanel_track } from '@/helpers/mixpanel'
|
||||
import { useTheming } from '@/store/theme.js'
|
||||
import ModpackVersionModal from '@/components/ui/ModpackVersionModal.vue'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -427,6 +560,10 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
versions: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const themeStore = useTheming()
|
||||
@@ -435,6 +572,8 @@ const title = ref(props.instance.metadata.name)
|
||||
const icon = ref(props.instance.metadata.icon)
|
||||
const groups = ref(props.instance.metadata.groups)
|
||||
|
||||
const modpackVersionModal = ref(null)
|
||||
|
||||
const instancesList = Object.values(await list(true))
|
||||
const availableGroups = ref([
|
||||
...instancesList.reduce((acc, obj) => {
|
||||
@@ -469,6 +608,9 @@ async function setIcon() {
|
||||
|
||||
const globalSettings = await get().catch(handleError)
|
||||
|
||||
const modalConfirmUnlock = ref(null)
|
||||
const modalConfirmUnpair = ref(null)
|
||||
|
||||
const javaSettings = props.instance.java ?? {}
|
||||
|
||||
const overrideJavaInstall = ref(!!javaSettings.override_version)
|
||||
@@ -496,6 +638,13 @@ const fullscreenSetting = ref(!!props.instance.fullscreen)
|
||||
|
||||
const unlinkModpack = ref(false)
|
||||
|
||||
const inProgress = ref(false)
|
||||
const installing = computed(() => props.instance.install_stage !== 'installed')
|
||||
const installedVersion = computed(() => props.instance?.metadata?.linked_data?.version_id)
|
||||
const installedVersionData = computed(() =>
|
||||
props.versions.find((version) => version.id === installedVersion.value)
|
||||
)
|
||||
|
||||
watch(
|
||||
[
|
||||
title,
|
||||
@@ -517,71 +666,78 @@ watch(
|
||||
unlinkModpack,
|
||||
],
|
||||
async () => {
|
||||
const editProfile = {
|
||||
metadata: {
|
||||
name: title.value.trim().substring(0, 32) ?? 'Instance',
|
||||
groups: groups.value.map((x) => x.trim().substring(0, 32)).filter((x) => x.length > 0),
|
||||
loader_version: props.instance.metadata.loader_version,
|
||||
linked_data: props.instance.metadata.linked_data,
|
||||
},
|
||||
java: {},
|
||||
}
|
||||
|
||||
if (overrideJavaInstall.value) {
|
||||
if (javaInstall.value.path !== '') {
|
||||
editProfile.java.override_version = javaInstall.value
|
||||
editProfile.java.override_version.path = editProfile.java.override_version.path.replace(
|
||||
'java.exe',
|
||||
'javaw.exe'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (overrideJavaArgs.value) {
|
||||
if (javaArgs.value !== '') {
|
||||
editProfile.java.extra_arguments = javaArgs.value.trim().split(/\s+/).filter(Boolean)
|
||||
}
|
||||
}
|
||||
|
||||
if (overrideEnvVars.value) {
|
||||
if (envVars.value !== '') {
|
||||
editProfile.java.custom_env_args = envVars.value
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.map((x) => x.split('=').filter(Boolean))
|
||||
}
|
||||
}
|
||||
|
||||
if (overrideMemorySettings.value) {
|
||||
editProfile.memory = memory.value
|
||||
}
|
||||
|
||||
if (overrideWindowSettings.value) {
|
||||
editProfile.fullscreen = fullscreenSetting.value
|
||||
|
||||
if (!fullscreenSetting.value) {
|
||||
editProfile.resolution = resolution.value
|
||||
}
|
||||
}
|
||||
|
||||
if (overrideHooks.value) {
|
||||
editProfile.hooks = hooks.value
|
||||
}
|
||||
|
||||
if (unlinkModpack.value) {
|
||||
editProfile.metadata.linked_data = null
|
||||
}
|
||||
|
||||
await edit(props.instance.path, editProfile)
|
||||
await edit(props.instance.path, editProfileObject.value)
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
const editProfileObject = computed(() => {
|
||||
const editProfile = {
|
||||
metadata: {
|
||||
name: title.value.trim().substring(0, 32) ?? 'Instance',
|
||||
groups: groups.value.map((x) => x.trim().substring(0, 32)).filter((x) => x.length > 0),
|
||||
loader_version: props.instance.metadata.loader_version,
|
||||
linked_data: props.instance.metadata.linked_data,
|
||||
},
|
||||
java: {},
|
||||
}
|
||||
|
||||
if (overrideJavaInstall.value) {
|
||||
if (javaInstall.value.path !== '') {
|
||||
editProfile.java.override_version = javaInstall.value
|
||||
editProfile.java.override_version.path = editProfile.java.override_version.path.replace(
|
||||
'java.exe',
|
||||
'javaw.exe'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (overrideJavaArgs.value) {
|
||||
if (javaArgs.value !== '') {
|
||||
editProfile.java.extra_arguments = javaArgs.value.trim().split(/\s+/).filter(Boolean)
|
||||
}
|
||||
}
|
||||
|
||||
if (overrideEnvVars.value) {
|
||||
if (envVars.value !== '') {
|
||||
editProfile.java.custom_env_args = envVars.value
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.map((x) => x.split('=').filter(Boolean))
|
||||
}
|
||||
}
|
||||
|
||||
if (overrideMemorySettings.value) {
|
||||
editProfile.memory = memory.value
|
||||
}
|
||||
|
||||
if (overrideWindowSettings.value) {
|
||||
editProfile.fullscreen = fullscreenSetting.value
|
||||
|
||||
if (!fullscreenSetting.value) {
|
||||
editProfile.resolution = resolution.value
|
||||
}
|
||||
}
|
||||
|
||||
if (overrideHooks.value) {
|
||||
editProfile.hooks = hooks.value
|
||||
}
|
||||
|
||||
if (unlinkModpack.value) {
|
||||
editProfile.metadata.linked_data = null
|
||||
}
|
||||
return editProfile
|
||||
})
|
||||
|
||||
const repairing = ref(false)
|
||||
|
||||
async function unpairProfile() {
|
||||
unlinkModpack.value = true
|
||||
async function duplicateProfile() {
|
||||
await duplicate(props.instance.path).catch(handleError)
|
||||
mixpanel_track('InstanceDuplicate', {
|
||||
loader: props.instance.metadata.loader,
|
||||
game_version: props.instance.metadata.game_version,
|
||||
})
|
||||
}
|
||||
|
||||
async function repairProfile() {
|
||||
@@ -595,10 +751,30 @@ async function repairProfile() {
|
||||
})
|
||||
}
|
||||
|
||||
async function unpairProfile() {
|
||||
const editProfile = props.instance
|
||||
editProfile.metadata.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.metadata.linked_data.locked = false
|
||||
await edit(props.instance.path, editProfile)
|
||||
modalConfirmUnlock.value.hide()
|
||||
}
|
||||
|
||||
const isPackLocked = computed(() => {
|
||||
return props.instance.metadata.linked_data && props.instance.metadata.linked_data.locked
|
||||
})
|
||||
|
||||
async function repairModpack() {
|
||||
repairing.value = true
|
||||
inProgress.value = true
|
||||
await update_repair_modrinth(props.instance.path).catch(handleError)
|
||||
repairing.value = false
|
||||
inProgress.value = false
|
||||
|
||||
mixpanel_track('InstanceRepair', {
|
||||
loader: props.instance.metadata.loader,
|
||||
@@ -711,12 +887,9 @@ const editing = ref(false)
|
||||
async function saveGvLoaderEdits() {
|
||||
editing.value = true
|
||||
|
||||
const editProfile = {
|
||||
metadata: {
|
||||
game_version: gameVersion.value,
|
||||
loader: loader.value,
|
||||
},
|
||||
}
|
||||
let editProfile = editProfileObject.value
|
||||
editProfile.metadata.loader = loader.value
|
||||
editProfile.metadata.game_version = gameVersion.value
|
||||
|
||||
if (loader.value !== 'vanilla') {
|
||||
editProfile.metadata.loader_version = selectableLoaderVersions.value[loaderVersionIndex.value]
|
||||
@@ -772,4 +945,39 @@ async function saveGvLoaderEdits() {
|
||||
: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>
|
||||
|
||||
@@ -168,7 +168,7 @@
|
||||
</Card>
|
||||
</div>
|
||||
<div v-if="data" class="content-container">
|
||||
<Promotion query-param="?r=launcher" />
|
||||
<Promotion :external="false" query-param="?r=launcher" />
|
||||
<Card class="tabs">
|
||||
<NavRow
|
||||
v-if="data.gallery.length > 0"
|
||||
|
||||
Reference in New Issue
Block a user