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:
Wyatt Verchere
2023-09-12 09:27:03 -07:00
committed by GitHub
parent bc02192d80
commit 1e8852b540
63 changed files with 2677 additions and 719 deletions

View File

@@ -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) => {

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>