feat: add info event listener and payload for enhanced event handling

- Implemented `info_listener` in `events.js` to listen for 'info' events and handle payloads.
- Added `emit_info` function in `emit.rs` to emit 'info' events with a message payload.
- Defined `InfoPayload` struct in `mod.rs` to structure the data for 'info' events.
- Integrated `emit_info` calls in the Minecraft launch logic to provide feedback on account types.
- Introduced a new offline icon in SVG format and removed outdated pirate icons from assets.
- Updated asset index to include the new offline icon and removed references to deleted icons.
This commit is contained in:
2026-01-27 20:41:55 +03:00
parent 45519f5dbb
commit 572800d9ca
10 changed files with 293 additions and 194 deletions

View File

@@ -73,7 +73,7 @@ import { useCheckDisableMouseover } from '@/composables/macCssFix.js'
import { debugAnalytics, initAnalytics, optOutAnalytics, trackEvent } from '@/helpers/analytics'
import { check_reachable } from '@/helpers/auth.js'
import { get_user } from '@/helpers/cache.js'
import { command_listener, warning_listener } from '@/helpers/events.js'
import { command_listener, warning_listener, info_listener } from '@/helpers/events.js'
import { useFetch } from '@/helpers/fetch.js'
import { cancelLogin, get as getCreds, login, logout } from '@/helpers/mr_auth.ts'
import { list } from '@/helpers/profile.js'
@@ -286,6 +286,15 @@ async function setupApp() {
}),
)
// [AR] Info listener
await info_listener((e) =>
addNotification({
title: 'Info',
text: e.message,
type: 'info',
}),
)
useFetch(
`https://api.modrinth.com/appCriticalAnnouncement.json?version=${version}`,
'criticalAnnouncements',

View File

@@ -1,12 +1,24 @@
<template>
<div v-if="mode !== 'isolated'" ref="button"
<div
v-if="mode !== 'isolated'"
ref="button"
class="button-base mt-2 px-3 py-2 bg-button-bg rounded-xl flex items-center gap-2"
:class="{ expanded: mode === 'expanded' }" @click="toggleMenu">
<Avatar size="36px" :src="selectedAccount ? avatarUrl : 'https://launcher-files.modrinth.com/assets/steve_head.png'
" />
:class="{ expanded: mode === 'expanded' }"
@click="toggleMenu"
>
<Avatar
size="36px"
:src="
selectedAccount ? avatarUrl : 'https://launcher-files.modrinth.com/assets/steve_head.png'
"
/>
<div class="flex flex-col w-full">
<span>
<component :is="getAccountType(selectedAccount)" v-if="selectedAccount" class="vector-icon" />
<component
:is="getAccountType(selectedAccount)"
v-if="selectedAccount"
class="vector-icon"
/>
{{ selectedAccount ? selectedAccount.profile.name : 'Select account' }}
</span>
<span class="text-secondary text-xs">Minecraft account</span>
@@ -14,32 +26,46 @@
<DropdownIcon class="w-5 h-5 shrink-0" />
</div>
<transition name="fade">
<Card v-if="showCard || mode === 'isolated'" ref="card" class="account-card"
:class="{ expanded: mode === 'expanded', isolated: mode === 'isolated' }">
<Card
v-if="showCard || mode === 'isolated'"
ref="card"
class="account-card"
:class="{ expanded: mode === 'expanded', isolated: mode === 'isolated' }"
>
<div v-if="selectedAccount" class="selected account">
<Avatar size="xs" :src="avatarUrl" />
<div>
<h4>
<component :is="getAccountType(selectedAccount)" class="vector-icon" /> {{
selectedAccount.profile.name }}
<component :is="getAccountType(selectedAccount)" class="vector-icon" />
{{ selectedAccount.profile.name }}
</h4>
<p>Selected</p>
</div>
<Button v-tooltip="'Log out'" icon-only color="raised" @click="logout(selectedAccount.profile.id)">
<Button
v-tooltip="'Log out'"
icon-only
color="raised"
@click="logout(selectedAccount.profile.id)"
>
<TrashIcon />
</Button>
</div>
<div v-else class="login-section account">
<h4>Not signed in</h4>
<Button v-tooltip="'Log via Microsoft'" :disabled="microsoftLoginDisabled" icon-only @click="login()">
<Button
v-tooltip="'Log via Microsoft'"
:disabled="microsoftLoginDisabled"
icon-only
@click="login()"
>
<MicrosoftIcon v-if="!microsoftLoginDisabled" />
<SpinnerIcon v-else class="animate-spin" />
</Button>
<Button v-tooltip="'Add offline account'" icon-only @click="showOfflineLoginModal()">
<PirateIcon />
<OfflineIcon />
</Button>
<Button v-tooltip="'Log via Ely.by'" icon-only @click="showElybyLoginModal()">
<ElyByIcon v-if="!elybyLoginDisabled" />
<Button v-tooltip="'Log via Ely.by'" icon-only @click="showElyByLoginModal()">
<ElyByIcon v-if="!elyByLoginDisabled" />
<SpinnerIcon v-else class="animate-spin" />
</Button>
</div>
@@ -63,23 +89,37 @@
<SpinnerIcon v-else class="animate-spin" />
</Button>
<Button v-tooltip="'Add offline account'" icon-only @click="showOfflineLoginModal()">
<PirateIcon />
<OfflineIcon />
</Button>
<Button v-tooltip="'Log via Ely.by'" icon-only @click="showElybyLoginModal()">
<ElyByIcon v-if="!elybyLoginDisabled" />
<Button v-tooltip="'Log via Ely.by'" icon-only @click="showElyByLoginModal()">
<ElyByIcon v-if="!elyByLoginDisabled" />
<SpinnerIcon v-else class="animate-spin" />
</Button>
</div>
</Card>
</transition>
<ModalWrapper ref="addElybyModal" class="modal" header="Authenticate with Ely.by">
<ModalWrapper ref="requestElybyTwoFactorCodeModal" class="modal"
header="Ely.by requested 2FA code for authentication">
<ModalWrapper ref="addElyByModal" class="modal" header="Authenticate with Ely.by">
<ModalWrapper
ref="requestElyByTwoFactorCodeModal"
class="modal"
header="Ely.by requested 2FA code for authentication"
>
<div class="flex flex-col gap-4 px-6 py-5">
<label class="label">Enter your 2FA code</label>
<input v-model="elybyTwoFactorCode" type="text" placeholder="Your 2FA code here..." class="input" />
<input
v-model="elyByTwoFactorCode"
type="text"
placeholder="Your 2FA code here..."
class="input"
/>
<div class="mt-6 ml-auto">
<Button icon-only color="primary" class="continue-button" @click="addElybyProfile()">
<Button
:disabled="elyByLoginDisabled"
icon-only
color="primary"
class="continue-button"
@click="addElyByProfile()"
>
Continue
</Button>
</div>
@@ -87,11 +127,27 @@
</ModalWrapper>
<div class="flex flex-col gap-4 px-6 py-5">
<label class="label">Enter your player name or email (preferred)</label>
<input v-model="elybyLogin" type="text" placeholder="Your player name or email here..." class="input" />
<input
v-model="elyByLogin"
type="text"
placeholder="Your player name or email here..."
class="input"
/>
<label class="label">Enter your password</label>
<input v-model="elybyPassword" type="password" placeholder="Your password here..." class="input" />
<input
v-model="elyByPassword"
type="password"
placeholder="Your password here..."
class="input"
/>
<div class="mt-6 ml-auto">
<Button icon-only color="primary" class="continue-button" @click="addElybyProfile()">
<Button
:disabled="elyByLoginDisabled"
icon-only
color="primary"
class="continue-button"
@click="addElyByProfile()"
>
Login
</Button>
</div>
@@ -100,7 +156,12 @@
<ModalWrapper ref="addOfflineModal" class="modal" header="Add new offline account">
<div class="flex flex-col gap-4 px-6 py-5">
<label class="label">Enter your player name</label>
<input v-model="offlinePlayerName" type="text" placeholder="Your player name here..." class="input" />
<input
v-model="offlinePlayerName"
type="text"
placeholder="Your player name here..."
class="input"
/>
<div class="mt-6 ml-auto">
<Button icon-only color="primary" class="continue-button" @click="addOfflineProfile()">
Login
@@ -108,21 +169,28 @@
</div>
</div>
</ModalWrapper>
<ModalWrapper ref="authenticationElybyErrorModal" class="modal"
header="Error while proceeding authentication event with Ely.by">
<ModalWrapper
ref="authenticationElyByErrorModal"
class="modal"
header="Error while proceeding authentication event with Ely.by"
>
<div class="flex flex-col gap-4 px-6 py-5">
<label class="text-base font-medium text-red-700">
An error occurred while logging in.
</label>
<div class="mt-6 ml-auto">
<Button color="primary" class="retry-button" @click="retryAddElybyProfile">
<Button color="primary" class="retry-button" @click="retryAddElyByProfile">
Try again
</Button>
</div>
</div>
</ModalWrapper>
<ModalWrapper ref="inputElybyErrorModal" class="modal" header="Error while proceeding input event with Ely.by">
<ModalWrapper
ref="inputElyByErrorModal"
class="modal"
header="Error while proceeding input event with Ely.by"
>
<div class="flex flex-col gap-4 px-6 py-5">
<label class="text-base font-medium text-red-700">
An error occurred while adding the Ely.by account. Please follow the instructions below.
@@ -134,13 +202,17 @@
</ul>
<div class="mt-6 ml-auto">
<Button color="primary" class="retry-button" @click="retryAddElybyProfile">
<Button color="primary" class="retry-button" @click="retryAddElyByProfile">
Try again
</Button>
</div>
</div>
</ModalWrapper>
<ModalWrapper ref="inputErrorModal" class="modal" header="Error while proceeding input event with offline account">
<ModalWrapper
ref="inputOfflineErrorModal"
class="modal"
header="Error while proceeding input event with offline account"
>
<div class="flex flex-col gap-4 px-6 py-5">
<label class="text-base font-medium text-red-700">
An error occurred while adding the offline account. Please follow the instructions below.
@@ -149,9 +221,10 @@
<ul class="list-disc list-inside text-sm space-y-1">
<li>Check that you have entered the correct player name.</li>
<li>
Player name must be at least {{ minOfflinePlayerNameLength }} characters long and no more than
{{ maxOfflinePlayerNameLength }} characters.
Player name must be at least {{ minOfflinePlayerNameLength }} characters long and no more
than {{ maxOfflinePlayerNameLength }} characters.
</li>
<li>Make sure your name meets the format requirement `{{ nameExp }}`</li>
</ul>
<div class="mt-6 ml-auto">
@@ -161,7 +234,7 @@
</div>
</div>
</ModalWrapper>
<ModalWrapper ref="exceptionErrorModal" class="modal" header="Unexpected error occurred">
<ModalWrapper ref="unexpectedErrorModal" class="modal" header="Unexpected error occurred">
<div class="modal-body">
<label class="label">An unexpected error has occurred. Please try again later.</label>
</div>
@@ -169,35 +242,32 @@
</template>
<script setup>
import {
DropdownIcon,
TrashIcon,
PirateIcon as Offline,
MicrosoftIcon as License,
ElyByIcon as Elyby,
MicrosoftIcon,
PirateIcon,
ElyByIcon,
SpinnerIcon
} from '@modrinth/assets'
import { Avatar, Button, Card, injectNotificationManager } from '@modrinth/ui'
import { ref, computed, onMounted, onBeforeUnmount, onUnmounted } from 'vue'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import { trackEvent } from '@/helpers/analytics'
import {
elyby_auth_authenticate,
elyby_login,
get_default_user,
login as login_flow,
offline_login,
users,
remove_user,
set_default_user,
login as login_flow,
get_default_user,
users,
} from '@/helpers/auth'
import { trackEvent } from '@/helpers/analytics'
import { process_listener } from '@/helpers/events'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import { getPlayerHeadUrl } from '@/helpers/rendering/batch-skin-renderer.ts'
import { get_available_skins } from '@/helpers/skins'
import { handleSevereError } from '@/store/error.js'
import {
DropdownIcon,
ElyByIcon,
MicrosoftIcon,
OfflineIcon,
SpinnerIcon,
TrashIcon,
} from '@modrinth/assets'
import { Avatar, Button, Card, injectNotificationManager } from '@modrinth/ui'
import { computed, onBeforeUnmount, onMounted, onUnmounted, ref } from 'vue'
const { handleError } = injectNotificationManager()
@@ -213,34 +283,36 @@ const emit = defineEmits(['change'])
const accounts = ref({})
const microsoftLoginDisabled = ref(false)
const elybyLoginDisabled = ref(false)
const elyByLoginDisabled = ref(false)
const defaultUser = ref()
// [AR] • Feature
const clientToken = "astralrinth"
const clientToken = 'astralrinth'
const addOfflineModal = ref(null)
const addElybyModal = ref(null)
const requestElybyTwoFactorCodeModal = ref(null)
const authenticationElybyErrorModal = ref(null)
const inputElybyErrorModal = ref(null)
const inputErrorModal = ref(null)
const exceptionErrorModal = ref(null)
const addElyByModal = ref(null)
const requestElyByTwoFactorCodeModal = ref(null)
const authenticationElyByErrorModal = ref(null)
const inputElyByErrorModal = ref(null)
const inputOfflineErrorModal = ref(null)
const unexpectedErrorModal = ref(null)
const offlinePlayerName = ref('')
const elybyLogin = ref('')
const elybyPassword = ref('')
const elybyTwoFactorCode = ref('')
const minOfflinePlayerNameLength = 2
const elyByLogin = ref('')
const elyByPassword = ref('')
const elyByTwoFactorCode = ref('')
const minOfflinePlayerNameLength = 3
const maxOfflinePlayerNameLength = 20
const nameExp = 'a-zA-Z0-9_'
const nameRegex = new RegExp(`^[${nameExp}]+$`)
// [AR] • Feature
function getAccountType(account) {
switch (account.account_type) {
case 'microsoft':
return License
return MicrosoftIcon
case 'pirate':
return Offline
return OfflineIcon
case 'elyby':
return Elyby
return ElyByIcon
}
}
@@ -250,30 +322,31 @@ function showOfflineLoginModal() {
}
// [AR] • Feature
function showElybyLoginModal() {
addElybyModal.value?.show()
function showElyByLoginModal() {
addElyByModal.value?.show()
}
// [AR] • Feature
function retryAddOfflineProfile() {
inputErrorModal.value?.hide()
inputOfflineErrorModal.value?.hide()
clearOfflineFields()
showOfflineLoginModal()
}
// [AR] • Feature
function retryAddElybyProfile() {
authenticationElybyErrorModal.value?.hide()
inputElybyErrorModal.value?.hide()
clearElybyFields()
showElybyLoginModal()
function retryAddElyByProfile() {
authenticationElyByErrorModal.value?.hide()
inputElyByErrorModal.value?.hide()
elyByLoginDisabled.value = false
clearElyByFields()
showElyByLoginModal()
}
// [AR] • Feature
function clearElybyFields() {
elybyLogin.value = ''
elybyPassword.value = ''
elybyTwoFactorCode.value = ''
function clearElyByFields() {
elyByLogin.value = ''
elyByPassword.value = ''
elyByTwoFactorCode.value = ''
}
// [AR] • Feature
@@ -284,11 +357,14 @@ function clearOfflineFields() {
// [AR] • Feature
async function addOfflineProfile() {
const name = offlinePlayerName.value.trim()
const isValidName = name.length >= minOfflinePlayerNameLength && name.length <= maxOfflinePlayerNameLength
const isValidName =
nameRegex.test(name) &&
name.length >= minOfflinePlayerNameLength &&
name.length <= maxOfflinePlayerNameLength
if (!isValidName) {
addOfflineModal.value?.hide()
inputErrorModal.value?.show()
inputOfflineErrorModal.value?.show()
clearOfflineFields()
return
}
@@ -302,39 +378,36 @@ async function addOfflineProfile() {
await setAccount(result)
await refreshValues()
} else {
exceptionErrorModal.value?.show()
unexpectedErrorModal.value?.show()
}
} catch (error) {
handleError(error)
exceptionErrorModal.value?.show()
unexpectedErrorModal.value?.show()
} finally {
clearOfflineFields()
}
}
// [AR] • Feature
async function addElybyProfile() {
if (!elybyLogin.value || !elybyPassword.value) {
addElybyModal.value?.hide()
inputElybyErrorModal.value?.show()
clearElybyFields()
async function addElyByProfile() {
elyByLoginDisabled.value = true
if (!elyByLogin.value || !elyByPassword.value) {
addElyByModal.value?.hide()
inputElyByErrorModal.value?.show()
clearElyByFields()
return
}
elybyLoginDisabled.value = true
const login = elybyLogin.value.trim()
let password = elybyPassword.value.trim()
const twoFactorCode = elybyTwoFactorCode.value.trim()
// Parse ely.by credential fields
const login = elyByLogin.value.trim()
let password = elyByPassword.value.trim()
const twoFactorCode = elyByTwoFactorCode.value.trim()
if (password && twoFactorCode) {
password = `${password}:${twoFactorCode}`
}
try {
const raw_result = await elyby_auth_authenticate(
login,
password,
clientToken
)
const raw_result = await elyby_auth_authenticate(login, password, clientToken)
const json_data = JSON.parse(raw_result)
@@ -346,13 +419,13 @@ async function addElybyProfile() {
json_data.error === 'ForbiddenOperationException' &&
json_data.errorMessage?.includes('two factor')
) {
requestElybyTwoFactorCodeModal.value?.show()
requestElyByTwoFactorCodeModal.value?.show()
return
}
addElybyModal.value?.hide()
requestElybyTwoFactorCodeModal.value?.hide()
authenticationElybyErrorModal.value?.show()
addElyByModal.value?.hide()
requestElyByTwoFactorCodeModal.value?.hide()
authenticationElyByErrorModal.value?.show()
return
}
@@ -362,18 +435,18 @@ async function addElybyProfile() {
const result = await elyby_login(selectedProfileId, selectedProfileName, accessToken)
addElybyModal.value?.hide()
requestElybyTwoFactorCodeModal.value?.hide()
addElyByModal.value?.hide()
requestElyByTwoFactorCodeModal.value?.hide()
clearElybyFields()
clearElyByFields()
await setAccount(result)
await refreshValues()
} catch (err) {
handleError(err)
exceptionErrorModal.value?.show()
unexpectedErrorModal.value?.show()
} finally {
elybyLoginDisabled.value = false
elyByLoginDisabled.value = false
}
}
@@ -543,7 +616,6 @@ onUnmounted(() => {
gap: 1rem;
}
.vector-icon {
width: 12px;
height: 12px;

View File

@@ -97,3 +97,8 @@ export async function warning_listener(callback) {
export async function friend_listener(callback) {
return await listen('friend', (event) => callback(event.payload))
}
// [AR] Payload for the 'info' event
export async function info_listener(callback) {
return await listen('info', (event) => callback(event.payload))
}