feat:将每日新闻改为starlight皮肤站
This commit is contained in:
@ -1,64 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import { ExternalIcon, NewspaperIcon } from '@modrinth/assets'
|
||||
import {
|
||||
defineMessages,
|
||||
injectNotificationManager,
|
||||
useFormatDateTime,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { ExternalIcon } from '@modrinth/assets'
|
||||
import { defineMessages, injectNotificationManager, useVIntl } from '@modrinth/ui'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { useNetworkStatus } from '@/composables/useNetworkStatus'
|
||||
import { get_minecraft_news, type MinecraftNewsItem } from '@/helpers/mc_news'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const { handleError } = injectNotificationManager()
|
||||
const { offline } = useNetworkStatus()
|
||||
const formatDate = useFormatDateTime({ dateStyle: 'medium' })
|
||||
|
||||
const messages = defineMessages({
|
||||
news: { id: 'app.home.news.title', defaultMessage: 'Minecraft news' },
|
||||
openArticle: { id: 'app.home.news.open-article', defaultMessage: 'Read on minecraft.net' },
|
||||
widgetTitle: { id: 'app.home.skin-site.title', defaultMessage: 'StarLight skin site' },
|
||||
luckyIndex: { id: 'app.home.skin-site.lucky-index', defaultMessage: 'Lucky index' },
|
||||
jump: { id: 'app.home.skin-site.jump', defaultMessage: 'Jump to StarLight skin site' },
|
||||
})
|
||||
|
||||
const NEWS_COUNT = 12
|
||||
const NEWS_SKELETON_COUNT = 4
|
||||
const SKIN_PROFILE_URL = 'https://skin.starlight.cool/profile'
|
||||
const SKIN_HOME_URL = 'https://skin.starlight.cool/'
|
||||
|
||||
const newsItems = ref<MinecraftNewsItem[]>([])
|
||||
const loading = ref(true)
|
||||
const htmlDecoder = document.createElement('textarea')
|
||||
|
||||
get_minecraft_news(NEWS_COUNT)
|
||||
.then((items) => {
|
||||
newsItems.value = items.map((item) => ({
|
||||
...item,
|
||||
title: decodeHtmlEntities(item.title),
|
||||
}))
|
||||
})
|
||||
.catch(() => {
|
||||
newsItems.value = []
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
|
||||
const visible = computed(() => !offline.value && (loading.value || newsItems.value.length > 0))
|
||||
|
||||
function newsDateLabel(item: MinecraftNewsItem): string | null {
|
||||
if (!item.date) return null
|
||||
const parsed = new Date(item.date)
|
||||
return Number.isNaN(parsed.getTime()) ? null : formatDate(parsed)
|
||||
}
|
||||
|
||||
function decodeHtmlEntities(value: string): string {
|
||||
htmlDecoder.innerHTML = value
|
||||
return htmlDecoder.value
|
||||
}
|
||||
|
||||
async function openArticle(item: MinecraftNewsItem) {
|
||||
async function openProfile() {
|
||||
try {
|
||||
await openUrl(item.read_more_url)
|
||||
await openUrl(SKIN_PROFILE_URL)
|
||||
} catch (error) {
|
||||
handleError(error)
|
||||
}
|
||||
}
|
||||
|
||||
async function openSkinSite() {
|
||||
try {
|
||||
await openUrl(SKIN_HOME_URL)
|
||||
} catch (error) {
|
||||
handleError(error)
|
||||
}
|
||||
@ -66,59 +33,34 @@ async function openArticle(item: MinecraftNewsItem) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section
|
||||
v-if="visible"
|
||||
class="flex min-w-0 flex-col gap-3 border-0 border-b-[1px] border-solid border-[--brand-gradient-border] p-4"
|
||||
>
|
||||
<section class="flex min-w-0 flex-col gap-3 border-0 border-b-[1px] border-solid border-[--brand-gradient-border] p-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<NewspaperIcon class="size-4 shrink-0 text-secondary" aria-hidden="true" />
|
||||
<ExternalIcon class="size-4 shrink-0 text-secondary" aria-hidden="true" />
|
||||
<h2 class="m-0 truncate text-lg">
|
||||
{{ formatMessage(messages.news) }}
|
||||
{{ formatMessage(messages.widgetTitle) }}
|
||||
</h2>
|
||||
</div>
|
||||
<ul v-if="loading" class="m-0 flex list-none flex-col gap-1.5 p-0" aria-hidden="true">
|
||||
<li
|
||||
v-for="index in NEWS_SKELETON_COUNT"
|
||||
:key="index"
|
||||
class="flex animate-pulse items-center gap-2.5 px-1.5 py-1.5"
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full cursor-pointer items-center justify-between rounded-lg border border-[--brand-gradient-border] bg-transparent px-3 py-2 text-left transition-colors hover:bg-button-bg"
|
||||
@click="openProfile"
|
||||
>
|
||||
<div class="h-9 w-16 shrink-0 rounded-lg bg-button-bg" />
|
||||
<div class="flex min-w-0 flex-1 flex-col gap-1.5">
|
||||
<div class="h-3 w-full rounded bg-button-bg" />
|
||||
<div class="h-3 w-1/2 rounded bg-button-bg" />
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul v-else class="m-0 flex list-none flex-col p-0">
|
||||
<li v-for="item in newsItems" :key="`${item.date ?? ''}:${item.title}`" class="group min-w-0">
|
||||
<button
|
||||
v-tooltip="formatMessage(messages.openArticle)"
|
||||
type="button"
|
||||
class="flex w-full cursor-pointer items-center gap-2.5 rounded-lg border-0 bg-transparent px-1.5 py-1.5 text-left transition-colors hover:bg-button-bg"
|
||||
@click="openArticle(item)"
|
||||
>
|
||||
<img
|
||||
v-if="item.image_url"
|
||||
:src="item.image_url"
|
||||
alt=""
|
||||
class="h-9 w-16 shrink-0 rounded-lg object-cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
<div v-else class="h-9 w-16 shrink-0 rounded-lg bg-button-bg" />
|
||||
<div class="flex min-w-0 flex-1 flex-col gap-0.5">
|
||||
<span class="line-clamp-2 text-sm font-semibold leading-snug text-contrast">
|
||||
{{ item.title }}
|
||||
</span>
|
||||
<span v-if="newsDateLabel(item)" class="truncate text-xs text-secondary">
|
||||
{{ newsDateLabel(item) }}
|
||||
</span>
|
||||
</div>
|
||||
<ExternalIcon
|
||||
class="size-3.5 shrink-0 text-secondary opacity-0 transition-opacity group-hover:opacity-100 group-focus-visible:opacity-100"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<span class="text-sm font-semibold text-contrast">
|
||||
{{ formatMessage(messages.luckyIndex) }}
|
||||
</span>
|
||||
<ExternalIcon class="size-4 shrink-0 text-secondary" aria-hidden="true" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full cursor-pointer items-center justify-center gap-2 rounded-lg bg-[--brand-gradient] px-3 py-2 font-semibold text-[var(--color-contrast)] transition-opacity hover:opacity-90"
|
||||
@click="openSkinSite"
|
||||
>
|
||||
<ExternalIcon class="size-4 shrink-0" aria-hidden="true" />
|
||||
<span>{{ formatMessage(messages.jump) }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user