feat(desktop): embed BoosAPI Desktop client as a subproject

Vendor the rebranded BoosAPI Desktop client (Tauri + Vue) into metapi as
a monorepo subdirectory under desktop/. Source only — node_modules,
dist, Cargo target and the upstream .git history are excluded.

desktop/ keeps its own independent Cargo workspace (desktop/src-tauri
with crates/*); it is not part of the backend boosapi crate, so a
`cargo check` at the metapi root still compiles only the backend. Build
the desktop app from desktop/src-tauri per desktop/README.md.

Note: app icons are still placeholder assets pending the final BoosAPI
logo. The updater signing keys live outside the repo and are not
included.
This commit is contained in:
boos
2026-06-22 11:32:57 +08:00
parent d9c78ecc73
commit e0a5f57793
2305 changed files with 449355 additions and 0 deletions
@@ -0,0 +1,136 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
const props = defineProps<{
repo: string
}>()
const stars = ref<number | null>(null)
const isLoading = ref(true)
onMounted(async () => {
try {
const response = await fetch(`https://api.github.com/repos/${props.repo}`)
if (response.ok) {
const data = await response.json()
stars.value = data.stargazers_count
}
}
catch {
// Silently fail if API request fails
}
finally {
isLoading.value = false
}
})
function formatNumber(num: number): string {
if (num >= 1000) {
return `${(num / 1000).toFixed(1)}k`
}
return num.toString()
}
</script>
<template>
<a
:href="`https://github.com/${repo}`"
target="_blank"
rel="noopener noreferrer"
class="github-star-badge"
aria-label="Star on GitHub"
>
<svg
class="star-icon"
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 16 16"
fill="currentColor"
>
<path
d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25z"
/>
</svg>
<span v-if="isLoading" class="stars-count">...</span>
<span v-else-if="stars !== null" class="stars-count">{{ formatNumber(stars) }}</span>
<span v-else class="stars-count">Star</span>
</a>
</template>
<style scoped>
.github-star-badge {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.4375rem 0.875rem;
background: linear-gradient(135deg, #f6f8fa 0%, #f1f3f5 100%);
border: 1px solid #d0d7de;
border-radius: 24px;
color: #24292f;
text-decoration: none;
font-size: 0.875rem;
font-weight: 600;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08), 0 1px 2px rgba(0, 0, 0, 0.04);
}
.github-star-badge:hover {
background: linear-gradient(135deg, #ffffff 0%, #f6f8fa 100%);
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12), 0 3px 6px rgba(0, 0, 0, 0.08);
}
.github-star-badge:active {
transform: translateY(0);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08), 0 1px 2px rgba(0, 0, 0, 0.04);
}
@media (prefers-color-scheme: dark) {
.github-star-badge {
background: linear-gradient(135deg, #161b22 0%, #0d1117 100%);
border-color: #30363d;
color: #c9d1d9;
}
.github-star-badge:hover {
background: linear-gradient(135deg, #21262d 0%, #161b22 100%);
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
}
}
.star-icon {
flex-shrink: 0;
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1));
transition: transform 0.25s ease;
}
.github-star-badge:hover .star-icon {
transform: scale(1.15) rotate(8deg);
}
.stars-count {
font-variant-numeric: tabular-nums;
letter-spacing: 0.025em;
min-width: 2ch;
text-align: center;
}
.stars-count:empty::before {
content: '...';
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.4;
}
}
</style>
@@ -0,0 +1,139 @@
<script setup lang="ts">
import DefaultTheme from 'vitepress/theme'
import { onMounted, ref } from 'vue'
const stars = ref<number | null>(null)
const isLoading = ref(true)
onMounted(async () => {
try {
const response = await fetch('https://api.github.com/repos/Simon-He95/markstream-vue')
if (response.ok) {
const data = await response.json()
stars.value = data.stargazers_count
}
}
catch {
// Silently fail if API request fails
}
finally {
isLoading.value = false
}
})
function formatNumber(num: number): string {
if (num >= 1000) {
return `${(num / 1000).toFixed(1)}k`
}
return num.toString()
}
</script>
<template>
<DefaultTheme.Layout>
<template #nav-bar-content-after>
<a
href="https://github.com/Simon-He95/markstream-vue"
target="_blank"
rel="noopener noreferrer"
class="github-star-badge-nav"
aria-label="Star on GitHub"
>
<svg
class="star-icon"
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 16 16"
fill="currentColor"
>
<path
d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25z"
/>
</svg>
<span v-if="isLoading" class="stars-count">...</span>
<span v-else-if="stars !== null" class="stars-count">{{ formatNumber(stars) }}</span>
<span v-else class="stars-count">Star</span>
</a>
</template>
</DefaultTheme.Layout>
</template>
<style>
.github-star-badge-nav {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.375rem 0.75rem;
margin-left: 0.75rem;
background: linear-gradient(135deg, #f6f8fa 0%, #f1f3f5 100%);
border: 1px solid #d0d7de;
border-radius: 20px;
color: #24292f;
text-decoration: none;
font-size: 0.8125rem;
font-weight: 600;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.github-star-badge-nav:hover {
background: linear-gradient(135deg, #ffffff 0%, #f6f8fa 100%);
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.github-star-badge-nav:active {
transform: translateY(0);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.dark .github-star-badge-nav {
background: linear-gradient(135deg, #161b22 0%, #0d1117 100%);
border-color: #30363d;
color: #c9d1d9;
}
.dark .github-star-badge-nav:hover {
background: linear-gradient(135deg, #21262d 0%, #161b22 100%);
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
}
.github-star-badge-nav .star-icon {
flex-shrink: 0;
filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.1));
transition: transform 0.25s ease;
}
.github-star-badge-nav:hover .star-icon {
transform: scale(1.1) rotate(5deg);
}
.github-star-badge-nav .stars-count {
font-variant-numeric: tabular-nums;
letter-spacing: 0.025em;
}
/* Animation for loading state */
.github-star-badge-nav .stars-count {
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
/* Remove animation when loaded */
.github-star-badge-nav .stars-count:not(:empty) {
animation: none;
}
</style>
@@ -0,0 +1,189 @@
<script setup lang="ts">
import { withBase } from 'vitepress'
interface Props {
title: string
description: string
note?: string
alipayLabel?: string
wechatLabel?: string
}
const props = withDefaults(defineProps<Props>(), {
alipayLabel: 'Alipay',
wechatLabel: 'WeChat Pay',
})
const alipaySrc = withBase('/sponsor/zhifubao.jpg')
const wechatSrc = withBase('/sponsor/weixin.jpg')
</script>
<template>
<section class="support-qrs">
<div class="support-qrs__copy">
<h2 class="support-qrs__title">
{{ props.title }}
</h2>
<p class="support-qrs__description">
{{ props.description }}
</p>
</div>
<div class="support-qrs__grid">
<figure class="support-qrs__card">
<div class="support-qrs__image-frame">
<img
class="support-qrs__image"
:src="alipaySrc"
:alt="`${props.alipayLabel} QR code`"
loading="lazy"
>
</div>
<figcaption class="support-qrs__label">
{{ props.alipayLabel }}
</figcaption>
</figure>
<figure class="support-qrs__card">
<div class="support-qrs__image-frame">
<img
class="support-qrs__image"
:src="wechatSrc"
:alt="`${props.wechatLabel} QR code`"
loading="lazy"
>
</div>
<figcaption class="support-qrs__label">
{{ props.wechatLabel }}
</figcaption>
</figure>
</div>
<p v-if="props.note" class="support-qrs__note">
{{ props.note }}
</p>
</section>
</template>
<style scoped>
.support-qrs {
margin: 1.5rem 0 2.25rem;
padding: 1.25rem;
border: 1px solid var(--vp-c-divider);
border-radius: 20px;
background:
radial-gradient(circle at top left, rgba(61, 120, 255, 0.12), transparent 34%),
linear-gradient(180deg, rgba(248, 250, 252, 0.92), rgba(255, 255, 255, 0.98));
}
.support-qrs__copy {
text-align: center;
}
.support-qrs__title {
margin: 0;
font-size: 1.25rem;
line-height: 1.25;
}
.support-qrs__description {
max-width: 44rem;
margin: 0.6rem auto 0;
color: var(--vp-c-text-2);
line-height: 1.7;
}
.support-qrs__grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1rem;
margin-top: 1.25rem;
}
.support-qrs__card {
display: flex;
flex-direction: column;
align-items: center;
margin: 0;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
border-radius: 16px;
background: rgba(255, 255, 255, 0.84);
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.06);
}
.support-qrs__image-frame {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
min-height: 320px;
padding: 0.875rem;
border-radius: 14px;
background: linear-gradient(180deg, rgba(248, 250, 252, 0.94), rgba(241, 245, 249, 0.9));
}
.support-qrs__image {
width: auto;
max-width: min(100%, 240px);
max-height: 320px;
height: auto;
object-fit: contain;
border-radius: 12px;
background: #fff;
box-shadow: 0 16px 32px rgba(15, 23, 42, 0.12);
}
.support-qrs__label {
margin-top: 0.85rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.support-qrs__note {
margin: 1rem auto 0;
text-align: center;
color: var(--vp-c-text-2);
font-size: 0.95rem;
line-height: 1.7;
}
.dark .support-qrs {
background:
radial-gradient(circle at top left, rgba(122, 162, 255, 0.16), transparent 36%),
linear-gradient(180deg, rgba(16, 24, 40, 0.92), rgba(10, 15, 26, 0.98));
}
.dark .support-qrs__card {
background: rgba(15, 23, 42, 0.78);
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.24);
}
.dark .support-qrs__image-frame {
background: linear-gradient(180deg, rgba(30, 41, 59, 0.82), rgba(15, 23, 42, 0.92));
}
@media (max-width: 640px) {
.support-qrs {
padding: 1rem;
border-radius: 16px;
}
.support-qrs__grid {
grid-template-columns: 1fr;
}
.support-qrs__card {
padding: 0.875rem;
}
.support-qrs__image-frame {
min-height: 260px;
padding: 0.75rem;
}
.support-qrs__image {
max-height: 260px;
}
}
</style>
@@ -0,0 +1,21 @@
import { useData } from 'vitepress'
/**
* VitePress theme composable for dark mode
* Uses VitePress's built-in isDark from useData()
*
* @example
* ```vue
* <script setup>
* const isDark = useDark()
* </script>
*
* <template>
* <MarkdownRender :is-dark="isDark" />
* </template>
* ```
*/
export function useDark() {
const { isDark } = useData()
return isDark
}
@@ -0,0 +1,24 @@
import type { EnhanceAppContext } from 'vitepress'
import TwoslashFloatingVue from '@shikijs/vitepress-twoslash/client'
import MarkdownRender from 'markstream-vue'
import Theme from 'vitepress/theme'
import GitHubStarBadge from './GitHubStarBadge.vue'
import Layout from './Layout.vue'
import SupportQRCodes from './SupportQRCodes.vue'
import '@shikijs/vitepress-twoslash/style.css'
import 'markstream-vue/index.css'
import './style.css'
export default {
extends: Theme,
Layout,
enhanceApp({ app }: EnhanceAppContext) {
app.use(TwoslashFloatingVue)
app.component('GitHubStarBadge', GitHubStarBadge)
app.component('MarkdownRender', MarkdownRender)
app.component('SupportQRCodes', SupportQRCodes)
},
}
// Export useDark for use in VitePress markdown files and components
export { useDark } from './composables/useDark'
@@ -0,0 +1 @@
/* Custom styles for markstream-vue theme */