feat: 嵌入 StarLight 皮肤站并同步登录状态

This commit is contained in:
2026-09-13 21:34:09 +08:00
parent 1593ac7a7c
commit 3471cd3da2
13 changed files with 449 additions and 311 deletions

View File

@ -1,14 +1,5 @@
<script setup lang="ts">
import {
CheckIcon,
GridIcon,
LayoutTemplateIcon,
MinimizeIcon,
MoveIcon,
PencilIcon,
PlusIcon,
RotateCounterClockwiseIcon,
} from '@modrinth/assets'
import { LayoutTemplateIcon, MinimizeIcon } from '@modrinth/assets'
import {
defineMessages,
injectNotificationManager,
@ -18,20 +9,14 @@ import {
import { computed, onUnmounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import {
createDefaultHomeDashboard,
createHomeDashboardSaveQueue,
type HomeDashboardConfig,
normalizeHomeDashboard,
} from '@/components/home/home-dashboard'
import { getActivePlayerName } from '@/components/home/home-utils'
import HomeDailyChallenge from '@/components/home/HomeDailyChallenge.vue'
import HomeDashboard from '@/components/home/HomeDashboard.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 HomeLaunchProgress from '@/components/home/HomeLaunchProgress.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'
@ -41,7 +26,7 @@ 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 { FeatureFlag, HomeLayout } from '@/store/theme'
import type { HomeLayout } from '@/store/theme'
const { handleError } = injectNotificationManager()
const route = useRoute()
@ -66,58 +51,15 @@ const messages = defineMessages({
id: 'app.home.layout.toggle',
defaultMessage: 'Minimal Home',
},
switchToGridWidgetLayout: {
id: 'app.home.widgets.layout.switch-to-grid',
defaultMessage: 'Switch to grid widget layout',
},
switchToFreeWidgetLayout: {
id: 'app.home.widgets.layout.switch-to-free',
defaultMessage: 'Switch to free widget layout',
},
widgetLayoutToggle: {
id: 'app.home.widgets.layout.toggle',
defaultMessage: 'Widget layout mode',
},
resetWidgets: {
id: 'app.home.widgets.reset-confirm',
defaultMessage: 'Restore the default widget layout?',
},
customizeWidgets: {
id: 'app.home.widgets.customize',
defaultMessage: 'Customize widgets',
},
doneEditing: { id: 'app.home.widgets.done', defaultMessage: 'Finish editing' },
addWidget: { id: 'app.home.widgets.add', defaultMessage: 'Add widget' },
resetWidgetLayout: {
id: 'app.home.widgets.reset',
defaultMessage: 'Restore default widgets',
},
})
const recentProjectsInHomeFlag: FeatureFlag = 'worlds_in_home'
breadcrumbs.setRootContext({ name: formatMessage(messages.home), link: route.path })
const instances = ref<GameInstance[]>([])
const playerName = ref<string | null>(null)
const dashboardConfig = ref<HomeDashboardConfig | null>(null)
const dashboard = ref<InstanceType<typeof HomeDashboard>>()
const dashboardEditing = ref(false)
const instancePicker = ref<InstanceType<typeof HomeInstancePickerModal>>()
const isMinimal = computed(() => themeStore.homeLayout === 'minimal')
const isFreeWidgetLayout = computed(() => dashboardConfig.value?.layout === 'free')
const switchingLayout = ref(false)
const dashboardSaveQueue = createHomeDashboardSaveQueue(
async (config) => {
const settings = await getSettings()
settings.home_widgets = config
await setSettings(settings)
},
(config) => {
dashboardConfig.value = config
},
handleError,
)
const floatingControlsStyle = computed(() => ({
bottom: themeStore.getFeatureFlag('page_path') ? '3.5rem' : '1rem',
right: `calc(${pageContext.floatingActionBarOffsets?.right.value ?? '0px'} + 1rem)`,
@ -162,38 +104,6 @@ async function fetchPlayerName() {
playerName.value = getActivePlayerName(selectedUser, accounts)
}
async function loadDashboardConfig() {
try {
const settings = await getSettings()
const normalized = normalizeHomeDashboard(settings.home_widgets)
if (normalized) {
dashboardConfig.value = normalized
return
}
const config = createDefaultHomeDashboard(themeStore.getFeatureFlag(recentProjectsInHomeFlag))
dashboardConfig.value = config
settings.home_widgets = config
await setSettings(settings)
} catch (error) {
dashboardConfig.value = createDefaultHomeDashboard(
themeStore.getFeatureFlag(recentProjectsInHomeFlag),
)
handleError(error)
}
}
function updateDashboardConfig(config: HomeDashboardConfig) {
const previous = dashboardConfig.value ?? config
dashboardConfig.value = config
void dashboardSaveQueue.enqueue(config, previous)
}
function resetDashboardConfig() {
if (!window.confirm(formatMessage(messages.resetWidgets))) return
updateDashboardConfig(createDefaultHomeDashboard())
}
async function selectMinimalInstance(instance: GameInstance) {
try {
const settings = await getSettings()
@ -214,10 +124,8 @@ async function toggleHomeLayout() {
const previousLayout = themeStore.homeLayout
const nextLayout: HomeLayout = previousLayout === 'minimal' ? 'standard' : 'minimal'
const previousEditing = dashboardEditing.value
switchingLayout.value = true
themeStore.homeLayout = nextLayout
if (nextLayout === 'minimal') dashboardEditing.value = false
try {
const settings = await getSettings()
@ -225,28 +133,14 @@ async function toggleHomeLayout() {
await setSettings(settings)
} catch (error) {
themeStore.homeLayout = previousLayout
dashboardEditing.value = previousEditing
handleError(error)
} finally {
switchingLayout.value = false
}
}
function toggleDashboardEditing() {
dashboardEditing.value = !dashboardEditing.value
}
function toggleWidgetLayout() {
dashboard.value?.setLayout(isFreeWidgetLayout.value ? 'grid' : 'free')
}
function openWidgetPicker() {
dashboard.value?.openWidgetPicker()
}
const instancesLoaded = await fetchInstances()
if (!instancesLoaded || instances.value.length > 0) void fetchPlayerName()
await loadDashboardConfig()
window.addEventListener(DIRECT_LINKS_SYNCED_EVENT, fetchInstances)
@ -267,16 +161,8 @@ onUnmounted(() => {
:selected-instance-id="themeStore.minimalHomeInstanceId"
@select="selectMinimalInstance"
/>
<div class="min-h-full">
<HomeDashboard
v-if="!isMinimal && dashboardConfig"
ref="dashboard"
:config="dashboardConfig"
:instances="instances"
:player-name="playerName"
:editing="dashboardEditing"
@change="updateDashboardConfig"
/>
<div class="home-content" :class="{ 'is-minimal': isMinimal }">
<SkinSiteFrame v-if="!isMinimal" />
<HomeMinimal
v-else
@ -288,71 +174,6 @@ onUnmounted(() => {
/>
</div>
<div class="home-floating-controls" :style="floatingControlsStyle">
<template v-if="!isMinimal">
<button
v-if="dashboardEditing"
v-tooltip="formatMessage(messages.addWidget)"
type="button"
class="home-floating-action"
:aria-label="formatMessage(messages.addWidget)"
@click="openWidgetPicker"
>
<PlusIcon />
</button>
<button
v-if="dashboardEditing"
v-tooltip="formatMessage(messages.resetWidgetLayout)"
type="button"
class="home-floating-action"
:aria-label="formatMessage(messages.resetWidgetLayout)"
@click="resetDashboardConfig"
>
<RotateCounterClockwiseIcon />
</button>
<button
v-tooltip="
formatMessage(dashboardEditing ? messages.doneEditing : messages.customizeWidgets)
"
data-onboarding-id="home-widget-customize"
type="button"
class="home-floating-action"
:class="{ 'is-active': dashboardEditing }"
:aria-label="
formatMessage(dashboardEditing ? messages.doneEditing : messages.customizeWidgets)
"
:aria-pressed="dashboardEditing"
@click="toggleDashboardEditing"
>
<CheckIcon v-if="dashboardEditing" />
<PencilIcon v-else />
</button>
<button
v-if="dashboardEditing"
v-tooltip="
formatMessage(
isFreeWidgetLayout
? messages.switchToGridWidgetLayout
: messages.switchToFreeWidgetLayout,
)
"
type="button"
role="switch"
class="home-layout-switch home-widget-layout-switch"
:class="{ 'is-free': isFreeWidgetLayout }"
:aria-checked="isFreeWidgetLayout"
:aria-label="formatMessage(messages.widgetLayoutToggle)"
@click="toggleWidgetLayout"
>
<span class="home-layout-switch-option home-widget-layout-grid" aria-hidden="true">
<GridIcon />
</span>
<span class="home-layout-switch-thumb" aria-hidden="true" />
<span class="home-layout-switch-option home-widget-layout-free" aria-hidden="true">
<MoveIcon />
</span>
</button>
<span class="home-floating-divider" aria-hidden="true" />
</template>
<button
v-tooltip="formatMessage(isMinimal ? messages.switchToInformation : messages.switchToMinimal)"
data-onboarding-id="home-layout-switch"
@ -388,6 +209,17 @@ onUnmounted(() => {
</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;
@ -406,57 +238,11 @@ onUnmounted(() => {
isolation: isolate;
}
.home-floating-action {
display: flex;
width: 2rem;
height: 2rem;
align-items: center;
justify-content: center;
padding: 0;
border: 0;
border-radius: 9999px;
background: transparent;
color: var(--color-secondary);
cursor: pointer;
transition:
background-color 120ms ease,
color 120ms ease,
filter 150ms ease,
transform 150ms ease;
}
.home-floating-action:hover {
background: var(--color-button-bg);
color: var(--color-contrast);
}
.home-floating-action:active {
transform: scale(0.96);
}
.home-floating-action:focus-visible,
.home-layout-switch:focus-visible {
outline: none;
box-shadow: 0 0 0 4px var(--color-brand-shadow);
}
.home-floating-action.is-active {
background: var(--color-brand);
color: var(--color-accent-contrast);
}
.home-floating-action :deep(svg) {
width: 1rem;
height: 1rem;
}
.home-floating-divider {
width: 1px;
height: 1.25rem;
margin: 0 0.125rem;
background: var(--color-divider);
}
.home-layout-switch {
position: relative;
display: grid;
@ -505,10 +291,6 @@ onUnmounted(() => {
transform: translateX(2rem);
}
.home-widget-layout-switch.is-free .home-layout-switch-thumb {
transform: translateX(2rem);
}
.home-layout-switch-option {
position: relative;
z-index: 1;
@ -527,9 +309,7 @@ onUnmounted(() => {
}
.home-layout-switch:not(.is-minimal) .home-layout-switch-information,
.home-layout-switch.is-minimal .home-layout-switch-minimal,
.home-widget-layout-switch:not(.is-free) .home-widget-layout-grid,
.home-widget-layout-switch.is-free .home-widget-layout-free {
.home-layout-switch.is-minimal .home-layout-switch-minimal {
color: var(--color-accent-contrast);
}
</style>