Some checks failed
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
388 lines
12 KiB
Vue
388 lines
12 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 { 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, teamMembers, type TeamMember } 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<AboutMemberExperience>()
|
|
const pressingMemberName = ref<string>()
|
|
let longPressTimer: ReturnType<typeof window.setTimeout> | 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 () => {
|
|
activeMemberExperience.value = experience
|
|
suppressNextMemberClick = true
|
|
cancelMemberLongPress()
|
|
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)
|
|
|
|
let typedBuffer = ''
|
|
const secretCodes = ['starlight']
|
|
|
|
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 (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 w-full overflow-hidden h-64 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 />
|
|
<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"
|
|
@pointerdown="startMemberLongPress(member, $event)"
|
|
@pointermove="moveMemberLongPress"
|
|
@pointerup="cancelMemberLongPress"
|
|
@pointerleave="cancelMemberLongPress"
|
|
@click="handleMemberClick"
|
|
@contextmenu="handleMemberContextMenu(member, $event)"
|
|
>
|
|
<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" />
|
|
<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);
|
|
}
|
|
|
|
.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>
|