Axolotl desktop CI / guardrails (push) Has been cancelled
Axolotl desktop CI / desktop (macos-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (ubuntu-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (windows-latest) (push) Has been cancelled
Axolotl desktop CI / website (push) Has been cancelled
Repository checks / typos (push) Has been cancelled
Repository checks / tombi (push) Has been cancelled
Rust checks / shear (push) Has been cancelled
Sync source to CNB / Sync Git ref (push) Has been cancelled
Add pack sync markers, tagged mod updates, parallel progress, JWT downloads and retry recovery. Include pending onboarding, about scene, compatibility data pack and download fixes.
420 lines
13 KiB
Vue
420 lines
13 KiB
Vue
<script setup lang="ts">
|
|
import { ChevronDownIcon, ExternalIcon, ScaleIcon, UsersIcon } from '@modrinth/assets'
|
|
import { Avatar, defineMessages, NewButton as Button, useVIntl } from '@modrinth/ui'
|
|
import { getVersion } from '@tauri-apps/api/app'
|
|
import { openUrl } from '@tauri-apps/plugin-opener'
|
|
import { defineAsyncComponent, inject, nextTick, onScopeDispose, ref, shallowRef } from 'vue'
|
|
|
|
import EasterEggContributorsModal from '@/components/ui/easteregg/EasterEggContributorsModal.vue'
|
|
import EasterEggGameModal from '@/components/ui/easteregg/EasterEggGameModal.vue'
|
|
import { AxolotlBrandConfig } from '@/config'
|
|
import { contributors, type TeamMember, teamMembers } from '@/data/about'
|
|
|
|
import AboutScene from '../AboutScene.vue'
|
|
import { type AboutMemberExperience, getAboutMemberExperience } from './about-member-experiences'
|
|
|
|
const { formatMessage } = useVIntl()
|
|
const version = await getVersion()
|
|
const experienceHost = ref<HTMLElement>()
|
|
const activeMemberExperience = shallowRef<Extract<AboutMemberExperience, { kind: 'scene' }>>()
|
|
const pressingMemberName = ref<string>()
|
|
let longPressTimer: number | undefined
|
|
let pressStart = { x: 0, y: 0 }
|
|
let suppressNextMemberClick = false
|
|
const replayOnboarding = inject<(mode: 'main' | 'instance') => Promise<void>>('replayOnboarding')
|
|
|
|
const licenseUrl = `${AxolotlBrandConfig.repositoryUrl}/blob/main/LICENSE`
|
|
const thirdPartyLicensesUrl = `${AxolotlBrandConfig.repositoryUrl}/tree/main/third-party/licenses`
|
|
|
|
function cancelMemberLongPress() {
|
|
if (longPressTimer) window.clearTimeout(longPressTimer)
|
|
longPressTimer = undefined
|
|
pressingMemberName.value = undefined
|
|
}
|
|
|
|
function startMemberLongPress(member: TeamMember, event: PointerEvent) {
|
|
const experience = getAboutMemberExperience(member.experience)
|
|
if (!experience || event.button !== 0) return
|
|
|
|
cancelMemberLongPress()
|
|
pressStart = { x: event.clientX, y: event.clientY }
|
|
pressingMemberName.value = member.name
|
|
longPressTimer = window.setTimeout(async () => {
|
|
suppressNextMemberClick = true
|
|
cancelMemberLongPress()
|
|
if (experience.kind === 'color-mine') {
|
|
openColorMine()
|
|
return
|
|
}
|
|
activeMemberExperience.value = experience
|
|
await nextTick()
|
|
experienceHost.value?.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
|
}, experience.longPressDuration)
|
|
}
|
|
|
|
function moveMemberLongPress(event: PointerEvent) {
|
|
if (!longPressTimer) return
|
|
if (Math.hypot(event.clientX - pressStart.x, event.clientY - pressStart.y) > 8) {
|
|
cancelMemberLongPress()
|
|
}
|
|
}
|
|
|
|
function handleMemberClick(event: MouseEvent) {
|
|
if (!suppressNextMemberClick) return
|
|
suppressNextMemberClick = false
|
|
event.preventDefault()
|
|
event.stopPropagation()
|
|
}
|
|
|
|
function handleMemberContextMenu(member: TeamMember, event: MouseEvent) {
|
|
if (getAboutMemberExperience(member.experience)) event.preventDefault()
|
|
}
|
|
|
|
function closeMemberExperience() {
|
|
activeMemberExperience.value = undefined
|
|
}
|
|
|
|
const gameModal = ref<InstanceType<typeof EasterEggGameModal> | null>(null)
|
|
const contributorsModal = ref<InstanceType<typeof EasterEggContributorsModal> | null>(null)
|
|
const ColorMineModal = defineAsyncComponent(
|
|
() => import('@/components/ui/easteregg/color-mine/ColorMineModal.vue'),
|
|
)
|
|
const colorMineModal = ref<{ show: () => void }>()
|
|
const colorMineMounted = ref(false)
|
|
const colorMinePending = ref(false)
|
|
|
|
function openColorMine() {
|
|
colorMineMounted.value = true
|
|
if (colorMineModal.value) colorMineModal.value.show()
|
|
else colorMinePending.value = true
|
|
}
|
|
|
|
function colorMineReady() {
|
|
if (!colorMinePending.value) return
|
|
colorMinePending.value = false
|
|
colorMineModal.value?.show()
|
|
}
|
|
|
|
let typedBuffer = ''
|
|
const secretCodes = ['starlight']
|
|
const YUANSHEN_URL = 'https://ys.mihoyo.com/cloud/'
|
|
|
|
const konamiSequence = [
|
|
'ArrowUp',
|
|
'ArrowUp',
|
|
'ArrowDown',
|
|
'ArrowDown',
|
|
'ArrowLeft',
|
|
'ArrowRight',
|
|
'ArrowLeft',
|
|
'ArrowRight',
|
|
'KeyB',
|
|
'KeyA',
|
|
]
|
|
let konamiIndex = 0
|
|
|
|
function handleEasterEggKeydown(event: KeyboardEvent) {
|
|
typedBuffer = (typedBuffer + event.key).toLowerCase()
|
|
const maxCodeLen = Math.max(...secretCodes.map((c) => c.length))
|
|
if (typedBuffer.length > maxCodeLen) {
|
|
typedBuffer = typedBuffer.slice(-maxCodeLen)
|
|
}
|
|
if (typedBuffer.endsWith('yuanshen')) {
|
|
typedBuffer = ''
|
|
openUrl(YUANSHEN_URL).catch(() => {})
|
|
return
|
|
}
|
|
if (secretCodes.some((code) => typedBuffer.endsWith(code))) {
|
|
typedBuffer = ''
|
|
gameModal.value?.show()
|
|
return
|
|
}
|
|
|
|
const expected = konamiSequence[konamiIndex]
|
|
if (event.code === expected) {
|
|
konamiIndex++
|
|
if (konamiIndex === konamiSequence.length) {
|
|
konamiIndex = 0
|
|
contributorsModal.value?.show()
|
|
}
|
|
} else {
|
|
konamiIndex = event.code === konamiSequence[0] ? 1 : 0
|
|
}
|
|
}
|
|
|
|
function onEasterEggOpenGame() {
|
|
gameModal.value?.show()
|
|
}
|
|
|
|
document.addEventListener('keydown', handleEasterEggKeydown)
|
|
onScopeDispose(() => document.removeEventListener('keydown', handleEasterEggKeydown))
|
|
|
|
onScopeDispose(cancelMemberLongPress)
|
|
|
|
const messages = defineMessages({
|
|
productTitle: {
|
|
id: 'app.settings.about.product-title',
|
|
defaultMessage: 'About {productName}',
|
|
},
|
|
productDescription: {
|
|
id: 'app.settings.about.description',
|
|
defaultMessage: 'Starlight Launcher',
|
|
},
|
|
copyright: {
|
|
id: 'app.settings.about.copyright',
|
|
defaultMessage: 'Copyright © Axolotl All Rights Reserved.',
|
|
},
|
|
version: {
|
|
id: 'app.settings.about.version',
|
|
defaultMessage: 'Version {version}',
|
|
},
|
|
replayOnboarding: {
|
|
id: 'app.settings.about.replay-onboarding',
|
|
defaultMessage: 'Replay tour',
|
|
},
|
|
developmentTeam: {
|
|
id: 'app.settings.about.development-team',
|
|
defaultMessage: 'Development team',
|
|
},
|
|
licenseAttribution: {
|
|
id: 'app.settings.about.license-attribution',
|
|
defaultMessage: 'License & attribution',
|
|
},
|
|
attribution: {
|
|
id: 'app.settings.about.attribution',
|
|
defaultMessage:
|
|
'Starlight Launcher is a modified version of the Axolotl Launcher, which is based on the open-source Modrinth codebase.',
|
|
},
|
|
notAffiliated: {
|
|
id: 'app.settings.about.not-affiliated',
|
|
defaultMessage:
|
|
'Modrinth is a trademark of Rinth, Inc. Starlight Launcher is not affiliated with or endorsed by Rinth, Inc.',
|
|
},
|
|
originalSource: {
|
|
id: 'app.settings.about.original-source',
|
|
defaultMessage: 'Original Modrinth source',
|
|
},
|
|
projectLicense: {
|
|
id: 'app.settings.about.project-license',
|
|
defaultMessage: 'Project license (GPL-3.0)',
|
|
},
|
|
thirdPartyLicenses: {
|
|
id: 'app.settings.about.third-party-licenses',
|
|
defaultMessage: 'Third-party licenses',
|
|
},
|
|
contributors: {
|
|
id: 'app.settings.about.contributors',
|
|
defaultMessage: 'Contributors',
|
|
},
|
|
contributorsCount: {
|
|
id: 'app.settings.about.contributors-count',
|
|
defaultMessage: '{count, plural, one {# contributor} other {# contributors}}',
|
|
},
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div class="about-page flex flex-col gap-6">
|
|
<section id="settings-target-about-product" tabindex="-1" class="about-panel">
|
|
<div class="flex flex-col items-center gap-4">
|
|
<div
|
|
ref="experienceHost"
|
|
class="relative m-0 aspect-[3/1] w-full overflow-hidden rounded-xl"
|
|
style="
|
|
mask-image: linear-gradient(to bottom, black 97%, transparent 100%);
|
|
-webkit-mask-image: linear-gradient(to bottom, black 97%, transparent 100%);
|
|
"
|
|
>
|
|
<AboutScene :paused="Boolean(activeMemberExperience)" />
|
|
<component
|
|
:is="activeMemberExperience?.component"
|
|
v-if="activeMemberExperience"
|
|
@exit="closeMemberExperience"
|
|
/>
|
|
</div>
|
|
<div class="min-w-0 text-center">
|
|
<h2 class="m-0 text-xl font-semibold text-contrast">
|
|
{{
|
|
formatMessage(messages.productTitle, {
|
|
productName: AxolotlBrandConfig.productName,
|
|
})
|
|
}}
|
|
</h2>
|
|
<p class="m-0 mt-1 text-secondary">
|
|
{{ formatMessage(messages.version, { version }) }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<p class="m-0 mt-3 text-center text-primary">
|
|
{{ formatMessage(messages.productDescription) }}
|
|
</p>
|
|
<p class="m-0 mt-2 text-center text-xs text-secondary">
|
|
{{ formatMessage(messages.copyright) }}
|
|
</p>
|
|
</section>
|
|
|
|
<section>
|
|
<h3 class="m-0 mb-3 flex items-center gap-2 text-base font-semibold text-contrast">
|
|
<ScaleIcon class="size-5 text-secondary" />
|
|
{{ formatMessage(messages.licenseAttribution) }}
|
|
</h3>
|
|
<div class="about-panel about-panel-compact">
|
|
<p class="m-0 text-primary">
|
|
{{ formatMessage(messages.attribution) }}
|
|
</p>
|
|
<p class="m-0 mt-2 text-sm text-secondary">
|
|
{{ formatMessage(messages.notAffiliated) }}
|
|
</p>
|
|
</div>
|
|
<div class="mt-3 flex flex-wrap gap-2">
|
|
<a
|
|
:href="licenseUrl"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
class="inline-flex items-center gap-2 rounded-lg bg-surface-4 px-3 py-2 text-sm font-semibold text-contrast transition-colors hover:bg-surface-5"
|
|
>
|
|
{{ formatMessage(messages.projectLicense) }}
|
|
<ExternalIcon class="size-4 text-secondary" />
|
|
</a>
|
|
<a
|
|
:href="thirdPartyLicensesUrl"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
class="inline-flex items-center gap-2 rounded-lg bg-surface-4 px-3 py-2 text-sm font-semibold text-contrast transition-colors hover:bg-surface-5"
|
|
>
|
|
{{ formatMessage(messages.thirdPartyLicenses) }}
|
|
<ExternalIcon class="size-4 text-secondary" />
|
|
</a>
|
|
<a
|
|
href="https://github.com/modrinth/code"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
class="inline-flex items-center gap-2 rounded-lg bg-surface-4 px-3 py-2 text-sm font-semibold text-contrast transition-colors hover:bg-surface-5"
|
|
>
|
|
{{ formatMessage(messages.originalSource) }}
|
|
<ExternalIcon class="size-4 text-secondary" />
|
|
</a>
|
|
</div>
|
|
</section>
|
|
|
|
<section>
|
|
<h3 class="m-0 mb-3 flex items-center gap-2 text-base font-semibold text-contrast">
|
|
<UsersIcon class="size-5 text-secondary" />
|
|
{{ formatMessage(messages.developmentTeam) }}
|
|
</h3>
|
|
<div class="grid gap-3 sm:grid-cols-2">
|
|
<a
|
|
v-for="member in teamMembers"
|
|
:key="member.name"
|
|
:href="member.url ?? undefined"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
class="flex min-w-0 items-center gap-3 rounded-xl bg-surface-4 p-3 transition-colors hover:bg-surface-5"
|
|
:class="{ 'member-card-holding': pressingMemberName === member.name }"
|
|
@pointerdown="startMemberLongPress(member, $event)"
|
|
@pointermove="moveMemberLongPress"
|
|
@pointerup="cancelMemberLongPress"
|
|
@pointercancel="cancelMemberLongPress"
|
|
@pointerleave="cancelMemberLongPress"
|
|
@blur="cancelMemberLongPress"
|
|
@click="handleMemberClick"
|
|
@contextmenu="handleMemberContextMenu(member, $event)"
|
|
@dragstart.prevent
|
|
>
|
|
<Avatar
|
|
:src="member.avatarUrl"
|
|
:alt="member.name"
|
|
size="2.5rem"
|
|
circle
|
|
no-shadow
|
|
loading="lazy"
|
|
/>
|
|
<span class="min-w-0 flex-1 truncate font-semibold text-contrast">
|
|
{{ member.name }}
|
|
</span>
|
|
<ExternalIcon v-if="member.url" class="size-4 shrink-0 text-secondary" />
|
|
</a>
|
|
</div>
|
|
</section>
|
|
<details class="group pt-4 about-settings-details">
|
|
<summary
|
|
class="flex cursor-pointer list-none items-center gap-2 text-base font-semibold text-contrast [&::-webkit-details-marker]:hidden"
|
|
>
|
|
<UsersIcon class="size-5 text-secondary" />
|
|
<span>{{ formatMessage(messages.contributors) }}</span>
|
|
<span class="rounded-full bg-surface-4 px-2 py-0.5 text-xs text-secondary">
|
|
{{ formatMessage(messages.contributorsCount, { count: contributors.length }) }}
|
|
</span>
|
|
<ChevronDownIcon
|
|
class="ml-auto size-5 text-secondary transition-transform group-open:rotate-180"
|
|
/>
|
|
</summary>
|
|
<div class="mt-3 flex flex-wrap gap-2">
|
|
<a
|
|
v-for="contributor in contributors"
|
|
:key="contributor.name"
|
|
:href="contributor.url"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
class="flex min-w-0 items-center gap-1.5 rounded-full bg-surface-4 py-1 pl-1 pr-2.5 transition-colors hover:bg-surface-5"
|
|
>
|
|
<Avatar
|
|
:src="contributor.avatarUrl"
|
|
:alt="contributor.name"
|
|
size="1.5rem"
|
|
circle
|
|
no-shadow
|
|
loading="lazy"
|
|
/>
|
|
<span class="truncate text-sm text-primary">{{ contributor.name }}</span>
|
|
</a>
|
|
</div>
|
|
</details>
|
|
|
|
<div id="settings-target-about-replay-tour" tabindex="-1" class="flex flex-wrap gap-2">
|
|
<Button type="base" @click="replayOnboarding?.('main')">
|
|
{{ formatMessage(messages.replayOnboarding) }}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<EasterEggGameModal ref="gameModal" />
|
|
<ColorMineModal v-if="colorMineMounted" ref="colorMineModal" @vue:mounted="colorMineReady" />
|
|
<EasterEggContributorsModal ref="contributorsModal" @open-game="onEasterEggOpenGame" />
|
|
</template>
|
|
|
|
<style scoped>
|
|
.about-settings-details {
|
|
border-top: 1px solid
|
|
var(--settings-divider, color-mix(in srgb, var(--surface-4) 55%, transparent));
|
|
}
|
|
|
|
.about-panel {
|
|
padding: 1.25rem;
|
|
border: 1px solid
|
|
var(--settings-card-border, color-mix(in srgb, var(--surface-4) 72%, transparent));
|
|
border-radius: var(--radius-md);
|
|
background: var(--surface-2);
|
|
}
|
|
|
|
.about-panel-compact {
|
|
padding: var(--gap-lg);
|
|
}
|
|
|
|
.member-card-holding {
|
|
outline: 2px solid var(--color-brand);
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
.about-page :deep(.rounded-xl.bg-surface-4) {
|
|
border: 1px solid
|
|
var(--settings-card-border, color-mix(in srgb, var(--surface-4) 72%, transparent));
|
|
border-radius: var(--radius-md);
|
|
background: var(--surface-2);
|
|
}
|
|
|
|
.about-page :deep(.rounded-xl.bg-surface-2) {
|
|
border-radius: var(--radius-sm);
|
|
}
|
|
</style>
|