Files
AstralRinth/apps/app-frontend/src/components/ui/RunningAppBar.vue
2025-04-19 18:01:41 +03:00

597 lines
14 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="action-groups">
<ButtonStyled v-if="currentLoadingBars.length > 0" color="brand" type="transparent" circular>
<button ref="infoButton" @click="toggleCard()">
<DownloadIcon />
</button>
</ButtonStyled>
<div v-if="offline" class="status">
<UnplugIcon />
<div class="running-text">
<span> Offline </span>
</div>
</div>
<div v-if="selectedProcess" class="status">
<span class="circle running" />
<div ref="profileButton" class="running-text">
<router-link :to="`/instance/${encodeURIComponent(selectedProcess.profile.path)}`">
{{ selectedProcess.profile.name }}
</router-link>
<div v-if="currentProcesses.length > 1" class="arrow button-base" :class="{ rotate: showProfiles }"
@click="toggleProfiles()">
<DropdownIcon />
</div>
</div>
<Button v-tooltip="'Stop instance'" icon-only class="icon-button stop" @click="stop(selectedProcess)">
<StopCircleIcon />
</Button>
<Button v-tooltip="'View logs'" icon-only class="icon-button" @click="goToTerminal()">
<TerminalSquareIcon />
</Button>
</div>
<div v-else class="status">
<span class="circle stopped" />
<span class="running-text"> No instances running </span>
</div>
<div v-if="updateState">
<a>
<Button class="download" :disabled="installState" @click="confirmUpdating(), getRemote(false, false)">
<DownloadIcon />
{{
installState
? "Downloading new update..."
: "Download new update"
}}
</Button>
</a>
</div>
<ModalWrapper ref="confirmUpdate" :has-to-type="false" header="Request to update the AstralRinth launcher">
<div class="modal-body">
<div class="markdown-body">
<p>The new version of the AstralRinth launcher is available.</p>
<p>Your version is outdated. We recommend that you update to the latest version.</p>
<p><strong> Warning </strong></p>
<p>
Before updating, make sure that you have saved all running instances and made a backup copy of the instances
that are valuable to you. Remember that the authors of the product are not responsible for the breakdown of
your files, so you should always make copies of them and keep them in a safe place.
</p>
</div>
<span>Source Git Astralium</span>
<span>Version on remote server <p id="releaseData" class="cosmic inline-fix"></p></span>
<span>Version on local device
<p class="cosmic inline-fix">v{{ version }}</p>
</span>
<div class="button-group push-right">
<Button class="download-modal" @click="confirmUpdate.hide()">
Decline</Button>
<Button class="download-modal" @click="approveUpdate()">
Accept
</Button>
</div>
</div>
</ModalWrapper>
</div>
<transition name="download">
<Card v-if="showCard === true && currentLoadingBars.length > 0" ref="card" class="info-card">
<div v-for="loadingBar in currentLoadingBars" :key="loadingBar.id" class="info-text">
<h3 class="info-title">
{{ loadingBar.title }}
</h3>
<ProgressBar :progress="Math.floor((100 * loadingBar.current) / loadingBar.total)" />
<div class="row">
{{ Math.floor((100 * loadingBar.current) / loadingBar.total) }}% {{ loadingBar.message }}
</div>
</div>
</Card>
</transition>
<transition name="download">
<Card v-if="showProfiles === true && currentProcesses.length > 0" ref="profiles" class="profile-card">
<Button v-for="process in currentProcesses" :key="process.uuid" class="profile-button"
@click="selectProcess(process)">
<div class="text"><span class="circle running" /> {{ process.profile.name }}</div>
<Button v-tooltip="'Stop instance'" icon-only class="icon-button stop" @click.stop="stop(process)">
<StopCircleIcon />
</Button>
<Button v-tooltip="'View logs'" icon-only class="icon-button" @click.stop="goToTerminal(process.profile.path)">
<TerminalSquareIcon />
</Button>
</Button>
</Card>
</transition>
</template>
<script setup>
import {
DownloadIcon,
StopCircleIcon,
TerminalSquareIcon,
DropdownIcon,
UnplugIcon,
} from '@modrinth/assets'
import { Button, ButtonStyled, Card } from '@modrinth/ui'
import { onBeforeUnmount, onMounted, ref } from 'vue'
import { get_all as getRunningProcesses, kill as killProcess } from '@/helpers/process'
import { loading_listener, process_listener } from '@/helpers/events'
import { useRouter } from 'vue-router'
import { progress_bars_list } from '@/helpers/state.js'
import ProgressBar from '@/components/ui/ProgressBar.vue'
import { handleError } from '@/store/notifications.js'
import { get_many } from '@/helpers/profile.js'
import { trackEvent } from '@/helpers/analytics'
import { getVersion } from '@tauri-apps/api/app'
const version = await getVersion()
import { installState, getRemote, updateState } from '@/helpers/update.js'
import ModalWrapper from './modal/ModalWrapper.vue'
const confirmUpdate = ref(null)
const confirmUpdating = async () => {
confirmUpdate.value.show()
}
const approveUpdate = async () => {
confirmUpdate.value.hide()
await getRemote(true, true)
}
await getRemote(true, false)
const router = useRouter()
const card = ref(null)
const profiles = ref(null)
const infoButton = ref(null)
const profileButton = ref(null)
const showCard = ref(false)
const showProfiles = ref(false)
const currentProcesses = ref([])
const selectedProcess = ref()
const refresh = async () => {
const processes = await getRunningProcesses().catch(handleError)
const profiles = await get_many(processes.map((x) => x.profile_path)).catch(handleError)
currentProcesses.value = processes.map((x) => ({
profile: profiles.find((prof) => x.profile_path === prof.path),
...x,
}))
if (!selectedProcess.value || !currentProcesses.value.includes(selectedProcess.value)) {
selectedProcess.value = currentProcesses.value[0]
}
}
await refresh()
const offline = ref(!navigator.onLine)
window.addEventListener('offline', () => {
offline.value = true
})
window.addEventListener('online', () => {
offline.value = false
})
const unlistenProcess = await process_listener(async () => {
await refresh()
})
const stop = async (process) => {
try {
await killProcess(process.uuid).catch(handleError)
trackEvent('InstanceStop', {
loader: process.profile.loader,
game_version: process.profile.game_version,
source: 'AppBar',
})
} catch (e) {
console.error(e)
}
await refresh()
}
const goToTerminal = (path) => {
router.push(`/instance/${encodeURIComponent(path ?? selectedProcess.value.profile.path)}/logs`)
}
const currentLoadingBars = ref([])
const refreshInfo = async () => {
const currentLoadingBarCount = currentLoadingBars.value.length
currentLoadingBars.value = Object.values(await progress_bars_list().catch(handleError)).map(
(x) => {
if (x.bar_type.type === 'java_download') {
x.title = 'Downloading Java ' + x.bar_type.version
}
if (x.bar_type.profile_path) {
x.title = x.bar_type.profile_path
}
if (x.bar_type.pack_name) {
x.title = x.bar_type.pack_name
}
return x
},
)
currentLoadingBars.value.sort((a, b) => {
if (a.loading_bar_uuid < b.loading_bar_uuid) {
return -1
}
if (a.loading_bar_uuid > b.loading_bar_uuid) {
return 1
}
return 0
})
if (currentLoadingBars.value.length === 0) {
showCard.value = false
} else if (currentLoadingBarCount < currentLoadingBars.value.length) {
showCard.value = true
}
}
await refreshInfo()
const unlistenLoading = await loading_listener(async () => {
await refreshInfo()
})
const selectProcess = (process) => {
selectedProcess.value = process
showProfiles.value = false
}
const handleClickOutsideCard = (event) => {
const elements = document.elementsFromPoint(event.clientX, event.clientY)
if (
card.value &&
card.value.$el !== event.target &&
!elements.includes(card.value.$el) &&
infoButton.value &&
!infoButton.value.contains(event.target)
) {
showCard.value = false
}
}
const handleClickOutsideProfile = (event) => {
const elements = document.elementsFromPoint(event.clientX, event.clientY)
if (
profiles.value &&
profiles.value.$el !== event.target &&
!elements.includes(profiles.value.$el) &&
!profileButton.value.contains(event.target)
) {
showProfiles.value = false
}
}
const toggleCard = async () => {
showCard.value = !showCard.value
showProfiles.value = false
await refreshInfo()
}
const toggleProfiles = async () => {
if (currentProcesses.value.length === 1) return
showProfiles.value = !showProfiles.value
showCard.value = false
}
onMounted(() => {
window.addEventListener('click', handleClickOutsideCard)
window.addEventListener('click', handleClickOutsideProfile)
})
onBeforeUnmount(() => {
window.removeEventListener('click', handleClickOutsideCard)
window.removeEventListener('click', handleClickOutsideProfile)
unlistenProcess()
unlistenLoading()
})
</script>
<style scoped lang="scss">
.inline-fix {
display: inline-flex;
margin-top: -2rem;
margin-bottom: -2rem;
//margin-left: 0.3rem;
}
.cosmic {
color: #3e8cde;
text-decoration: none;
text-shadow:
0 0 4px rgba(79, 173, 255, 0.5),
0 0 8px rgba(14, 98, 204, 0.5),
0 0 12px rgba(122, 31, 199, 0.5);
transition: color 0.35s ease;
}
.markdown-body {
:deep(table) {
width: auto;
}
:deep(hr),
:deep(h1),
:deep(h2) {
max-width: max(60rem, 90%);
}
:deep(ul),
:deep(ol) {
margin-left: 2rem;
}
}
.modal-body {
display: flex;
flex-direction: column;
gap: 1rem;
padding: var(--gap-lg);
text-align: left;
.button-group {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
}
strong {
color: var(--color-contrast);
}
}
.download {
color: #3e8cde;
border-radius: var(--radius-md);
border: 1px solid var(--color-button-bg);
// padding: var(--gap-sm) var(--gap-lg);
background-color: rgba(0, 0, 0, 0);
text-decoration: none;
text-shadow:
0 0 4px rgba(79, 173, 255, 0.5),
0 0 8px rgba(14, 98, 204, 0.5),
0 0 12px rgba(122, 31, 199, 0.5);
transition: color 0.35s ease;
display: flex;
flex-direction: row;
align-items: center;
gap: 0.5rem;
}
.download:hover,
.download:focus,
.download:active {
color: #10fae5;
text-shadow: #26065e;
}
.download-modal {
color: #3e8cde;
padding: var(--gap-sm) var(--gap-lg);
text-decoration: none;
text-shadow:
0 0 4px rgba(79, 173, 255, 0.5),
0 0 8px rgba(14, 98, 204, 0.5),
0 0 12px rgba(122, 31, 199, 0.5);
transition: color 0.35s ease;
}
.download-modal:hover,
.download-modal:focus,
.download-modal:active {
color: #10fae5;
text-shadow: #26065e;
}
.action-groups {
display: flex;
flex-direction: row;
align-items: center;
gap: var(--gap-md);
}
.arrow {
transition: transform 0.2s ease-in-out;
display: flex;
align-items: center;
&.rotate {
transform: rotate(180deg);
}
}
.status {
display: flex;
flex-direction: row;
align-items: center;
gap: 0.5rem;
border-radius: var(--radius-md);
border: 1px solid var(--color-button-bg);
padding: var(--gap-sm) var(--gap-lg);
}
.running-text {
display: flex;
flex-direction: row;
gap: var(--gap-xs);
white-space: nowrap;
overflow: hidden;
-webkit-user-select: none;
/* Safari */
-ms-user-select: none;
/* IE 10 and IE 11 */
user-select: none;
&.clickable:hover {
cursor: pointer;
}
}
.circle {
width: 0.5rem;
height: 0.5rem;
border-radius: 50%;
display: inline-block;
margin-right: 0.25rem;
&.running {
background-color: var(--color-brand);
}
&.stopped {
background-color: var(--color-base);
}
}
.icon-button {
background-color: rgba(0, 0, 0, 0);
box-shadow: none;
width: 1.25rem !important;
height: 1.25rem !important;
svg {
min-width: 1.25rem;
}
&.stop {
color: var(--color-red);
}
}
.info-card {
position: absolute;
top: 3.5rem;
right: 0.5rem;
z-index: 9;
width: 20rem;
background-color: var(--color-raised-bg);
box-shadow: var(--shadow-raised);
display: flex;
flex-direction: column;
gap: 1rem;
overflow: auto;
transition: all 0.2s ease-in-out;
border: 1px solid var(--color-button-bg);
&.hidden {
transform: translateY(-100%);
}
}
.loading-option {
display: flex;
flex-direction: row;
align-items: center;
gap: 0.5rem;
margin: 0;
padding: 0;
:hover {
background-color: var(--color-raised-bg-hover);
}
}
.loading-text {
display: flex;
flex-direction: column;
margin: 0;
padding: 0;
.row {
display: flex;
flex-direction: row;
align-items: center;
gap: 0.5rem;
}
}
.loading-icon {
width: 2.25rem;
height: 2.25rem;
display: block;
:deep(svg) {
left: 1rem;
width: 2.25rem;
height: 2.25rem;
}
}
.download-enter-active,
.download-leave-active {
transition: opacity 0.3s ease;
}
.download-enter-from,
.download-leave-to {
opacity: 0;
}
.progress-bar {
width: 100%;
}
.info-text {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
margin: 0;
padding: 0;
}
.info-title {
margin: 0;
}
.profile-button {
display: flex;
flex-direction: row;
align-items: center;
gap: var(--gap-sm);
width: 100%;
background-color: var(--color-raised-bg);
box-shadow: none;
.text {
margin-right: auto;
}
}
.profile-card {
position: absolute;
top: 3.5rem;
right: 0.5rem;
z-index: 9;
background-color: var(--color-raised-bg);
box-shadow: var(--shadow-raised);
display: flex;
flex-direction: column;
overflow: auto;
transition: all 0.2s ease-in-out;
border: 1px solid var(--color-button-bg);
padding: var(--gap-md);
&.hidden {
transform: translateY(-100%);
}
}
.link {
display: flex;
flex-direction: row;
align-items: center;
gap: var(--gap-sm);
margin: 0;
color: var(--color-text);
text-decoration: none;
}
</style>