Files
Starlight_Lancher/apps/app-frontend/src/pages/Index.vue

316 lines
8.6 KiB
Vue

<script setup lang="ts">
import { LayoutTemplateIcon, MinimizeIcon } from '@modrinth/assets'
import {
defineMessages,
injectNotificationManager,
injectPageContext,
useVIntl,
} from '@modrinth/ui'
import { computed, onUnmounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { getActivePlayerName } from '@/components/home/home-utils'
import HomeDailyChallenge from '@/components/home/HomeDailyChallenge.vue'
import HomeInstancePickerModal from '@/components/home/HomeInstancePickerModal.vue'
import HomeLaunchProgress from '@/components/home/HomeLaunchProgress.vue'
import HomeMinecraftNews from '@/components/home/HomeMinecraftNews.vue'
import HomeMinimal from '@/components/home/HomeMinimal.vue'
import HomePlayInsights from '@/components/home/HomePlayInsights.vue'
import SkinSiteFrame from '@/components/ui/SkinSiteFrame.vue'
import { useNetworkStatus } from '@/composables/useNetworkStatus'
import { get_default_user, users } from '@/helpers/auth'
import { DIRECT_LINKS_SYNCED_EVENT } from '@/helpers/direct-link-sync'
import { instance_listener } from '@/helpers/events'
import { list } from '@/helpers/instance'
import { get as getSettings, set as setSettings } from '@/helpers/settings'
import type { GameInstance } from '@/helpers/types'
import { useBreadcrumbs } from '@/store/breadcrumbs'
import { useTheming } from '@/store/state'
import type { HomeLayout } from '@/store/theme'
const { handleError } = injectNotificationManager()
const route = useRoute()
const router = useRouter()
const breadcrumbs = useBreadcrumbs()
const { formatMessage } = useVIntl()
const { offline } = useNetworkStatus()
const themeStore = useTheming()
const pageContext = injectPageContext()
const messages = defineMessages({
home: { id: 'app.home.breadcrumb', defaultMessage: 'Home' },
switchToMinimal: {
id: 'app.home.layout.switch-to-minimal',
defaultMessage: 'Switch to Minimal Home',
},
switchToInformation: {
id: 'app.home.layout.switch-to-information',
defaultMessage: 'Switch to Information Home',
},
homeLayoutToggle: {
id: 'app.home.layout.toggle',
defaultMessage: 'Minimal Home',
},
})
breadcrumbs.setRootContext({ name: formatMessage(messages.home), link: route.path })
const instances = ref<GameInstance[]>([])
const playerName = ref<string | null>(null)
const instancePicker = ref<InstanceType<typeof HomeInstancePickerModal>>()
const isMinimal = computed(() => themeStore.homeLayout === 'minimal')
const switchingLayout = ref(false)
const floatingControlsStyle = computed(() => ({
bottom: themeStore.getFeatureFlag('page_path') ? '3.5rem' : '1rem',
right: `calc(${pageContext.floatingActionBarOffsets?.right.value ?? '0px'} + 1rem)`,
}))
const animateSidebarShow = ref(false)
setTimeout(() => {
animateSidebarShow.value = true
}, 200)
async function clearMissingMinimalInstance() {
const selectedId = themeStore.minimalHomeInstanceId
if (!selectedId || instances.value.some((instance) => instance.id === selectedId)) return
themeStore.minimalHomeInstanceId = null
try {
const settings = await getSettings()
if (settings.minimal_home_instance_id === null) return
settings.minimal_home_instance_id = null
await setSettings(settings)
} catch (error) {
handleError(error)
}
}
async function fetchInstances() {
try {
instances.value = await list()
await clearMissingMinimalInstance()
return true
} catch (error) {
handleError(error)
return false
}
}
async function fetchPlayerName() {
const selectedUser = await get_default_user(offline.value).catch(() => undefined)
if (!selectedUser) return
const accounts = await users(offline.value).catch(() => [])
playerName.value = getActivePlayerName(selectedUser, accounts)
}
async function selectMinimalInstance(instance: GameInstance) {
try {
const settings = await getSettings()
settings.minimal_home_instance_id = instance.id
await setSettings(settings)
themeStore.minimalHomeInstanceId = instance.id
} catch (error) {
handleError(error)
}
}
function createInstance() {
void router.push('/create')
}
async function toggleHomeLayout() {
if (switchingLayout.value) return
const previousLayout = themeStore.homeLayout
const nextLayout: HomeLayout = previousLayout === 'minimal' ? 'standard' : 'minimal'
switchingLayout.value = true
themeStore.homeLayout = nextLayout
try {
const settings = await getSettings()
settings.home_layout = nextLayout
await setSettings(settings)
} catch (error) {
themeStore.homeLayout = previousLayout
handleError(error)
} finally {
switchingLayout.value = false
}
}
const instancesLoaded = await fetchInstances()
if (!instancesLoaded || instances.value.length > 0) void fetchPlayerName()
window.addEventListener(DIRECT_LINKS_SYNCED_EVENT, fetchInstances)
const unlistenInstance = await instance_listener(async () => {
await fetchInstances()
})
onUnmounted(() => {
unlistenInstance()
window.removeEventListener(DIRECT_LINKS_SYNCED_EVENT, fetchInstances)
})
</script>
<template>
<HomeInstancePickerModal
ref="instancePicker"
:instances="instances"
:selected-instance-id="themeStore.minimalHomeInstanceId"
@select="selectMinimalInstance"
/>
<div class="home-content" :class="{ 'is-minimal': isMinimal }">
<SkinSiteFrame v-if="!isMinimal" />
<HomeMinimal
v-else
:instances="instances"
:player-name="playerName"
:selected-instance-id="themeStore.minimalHomeInstanceId"
@choose="instancePicker?.show()"
@create="createInstance"
/>
</div>
<div class="home-floating-controls" :style="floatingControlsStyle">
<button
v-tooltip="formatMessage(isMinimal ? messages.switchToInformation : messages.switchToMinimal)"
data-onboarding-id="home-layout-switch"
type="button"
role="switch"
class="home-layout-switch"
:class="{ 'is-minimal': isMinimal }"
:disabled="switchingLayout"
:aria-checked="isMinimal"
:aria-label="formatMessage(messages.homeLayoutToggle)"
@click="toggleHomeLayout"
>
<span class="home-layout-switch-option home-layout-switch-information" aria-hidden="true">
<LayoutTemplateIcon />
</span>
<span class="home-layout-switch-thumb" aria-hidden="true" />
<span class="home-layout-switch-option home-layout-switch-minimal" aria-hidden="true">
<MinimizeIcon />
</span>
</button>
</div>
<Teleport v-if="!isMinimal" to="#sidebar-default-teleport-target">
<div
class="flex min-w-0 flex-col slide-enter-active"
:class="{ 'slide-enter-from': !animateSidebarShow }"
>
<HomeLaunchProgress />
<HomePlayInsights />
<HomeDailyChallenge />
<HomeMinecraftNews />
</div>
</Teleport>
</template>
<style scoped>
.home-content {
/* The embedded site owns scrolling within the launcher viewport. */
height: calc(100dvh - var(--top-bar-height));
min-width: 0;
}
.home-content.is-minimal {
height: auto;
min-height: calc(100dvh - var(--top-bar-height));
}
.home-floating-controls {
position: fixed;
z-index: 40;
display: flex;
height: 2.5rem;
align-items: center;
gap: 0.125rem;
padding: 0.25rem;
box-sizing: border-box;
border: 1px solid var(--color-divider);
border-radius: 9999px;
background: var(--color-raised-bg);
box-shadow:
var(--shadow-button),
0 0.25rem 0.75rem rgb(0 0 0 / 20%);
isolation: isolate;
}
.home-layout-switch:focus-visible {
outline: none;
box-shadow: 0 0 0 4px var(--color-brand-shadow);
}
.home-layout-switch {
position: relative;
display: grid;
grid-template-columns: repeat(2, 2rem);
align-items: center;
/* width: 4.25rem; */ /* closes #210 */
height: 2rem;
margin: 0;
padding: 0;
border: 0;
border-radius: 9999px;
background: var(--color-button-bg);
cursor: pointer;
isolation: isolate;
transition:
filter 150ms ease,
transform 150ms ease;
}
.home-layout-switch:hover:not(:disabled) {
filter: brightness(var(--hover-brightness));
}
.home-layout-switch:active:not(:disabled) {
transform: scale(0.97);
}
.home-layout-switch:disabled {
cursor: not-allowed;
opacity: 0.6;
}
.home-layout-switch-thumb {
position: absolute;
top: 0.125rem;
left: 0.125rem;
z-index: 0;
width: 1.75rem;
height: 1.75rem;
border-radius: 9999px;
background: var(--color-brand);
transition: transform 180ms ease;
}
.home-layout-switch.is-minimal .home-layout-switch-thumb {
transform: translateX(2rem);
}
.home-layout-switch-option {
position: relative;
z-index: 1;
display: flex;
width: 2rem;
height: 1.75rem;
align-items: center;
justify-content: center;
color: var(--color-secondary);
transition: color 180ms ease;
}
.home-layout-switch-option :deep(svg) {
width: 1rem;
height: 1rem;
}
.home-layout-switch:not(.is-minimal) .home-layout-switch-information,
.home-layout-switch.is-minimal .home-layout-switch-minimal {
color: var(--color-accent-contrast);
}
</style>