Basic Solar Network product page

This commit is contained in:
2025-03-18 23:15:37 +08:00
parent e4111dc06e
commit e9e182ea48
22 changed files with 389 additions and 54 deletions

View File

@@ -27,9 +27,9 @@
<div class="flex flex-col" v-if="attachment?.metadata?.ratio">
<span class="text-xs font-bold">Aspect Ratio</span>
<span>
{{ attachment?.metadata?.width }}x{{ attachment?.metadata?.height }}
{{ attachment?.metadata?.ratio.toFixed(2) }}
</span>
{{ attachment?.metadata?.width }}x{{ attachment?.metadata?.height }}
{{ attachment?.metadata?.ratio.toFixed(2) }}
</span>
</div>
<div class="flex flex-col" v-if="attachment?.mimetype">
<span class="text-xs font-bold">Mimetype</span>
@@ -44,13 +44,19 @@
<div class="text-xs text-grey flex flex-col mx-[2.5ch]">
<span>Solar Network Attachment Web Preview</span>
<span>Powered by <a class="underline" target="_blank" href="https://git.solsynth.dev/Hydrogen/Paperclip">Hydrogen.Paperclip</a></span>
<span
>Powered by
<a class="underline" target="_blank" href="https://git.solsynth.dev/Hydrogen/Paperclip"
>Hydrogen.Paperclip</a
></span
>
</div>
</v-col>
</v-row>
</template>
<script setup lang="ts">
import { formatBytes } from "~/utils/format"
import { useDisplay } from "vuetify"
const route = useRoute()
@@ -61,7 +67,9 @@ const firstVideo = ref<string | null>()
const isMediumScreen = useDisplay().mdAndUp
const { data: attachment } = await useFetch<any>(`${config.public.solarNetworkApi}/cgi/uc/attachments/${route.params.id}/meta`)
const { data: attachment } = await useFetch<any>(
`${config.public.solarNetworkApi}/cgi/uc/attachments/${route.params.id}/meta`,
)
definePageMeta({
layout: "minimal",
@@ -76,15 +84,19 @@ if (!attachment.value) {
const title = computed(() => `Attachment ${attachment.value?.id}`)
watch(attachment, (value) => {
if (value.mimetype.split("/")[0] == "image") {
firstImage.value = `${config.public.solarNetworkApi}/cgi/uc/attachments/${value.id}`
}
watch(
attachment,
(value) => {
if (value.mimetype.split("/")[0] == "image") {
firstImage.value = `${config.public.solarNetworkApi}/cgi/uc/attachments/${value.id}`
}
if (value.mimetype.split("/")[0] == "video") {
firstVideo.value = `${config.public.solarNetworkApi}/cgi/uc/attachments/${value.id}`
}
}, { immediate: true, deep: true })
if (value.mimetype.split("/")[0] == "video") {
firstVideo.value = `${config.public.solarNetworkApi}/cgi/uc/attachments/${value.id}`
}
},
{ immediate: true, deep: true },
)
useHead({
title: title.value,
@@ -106,16 +118,4 @@ useSeoMeta({
publisher: "Solar Network",
ogSiteName: "Solsynth Capital",
})
function formatBytes(bytes: number, decimals = 2) {
if (!+bytes) return "0 Bytes"
const k = 1024
const dm = decimals < 0 ? 0 : decimals
const sizes = ["Bytes", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"]
const i = Math.floor(Math.log(bytes) / Math.log(k))
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`
}
</script>

View File

@@ -93,7 +93,7 @@ onMounted(() => {
const poolOptions = [
{ label: "Interactive", description: "Public indexable, no lifecycle.", value: "interactive" },
{ label: "Messaging", description: "Has lifecycle, will delete after 14 days.", value: "messaging" },
{ label: "Messaging", description: "Has lifecycle, will be deleted after 14 days.", value: "messaging" },
{ label: "Sticker", description: "Public indexable, privilege required.", value: "sticker", disabled: true },
{ label: "Dedicated Pool", description: "Your own configuration, coming soon.", value: "dedicated", disabled: true },
]
@@ -201,12 +201,12 @@ async function uploadSingleMultipart(chunkId: string) {
const chunkIdx: number = multipartInfo.value["file_chunks"][chunkId]
const chunk = content.value.slice(chunkIdx * multipartSize.value, (chunkIdx + 1) * multipartSize.value)
const data = new FormData()
data.set("file", chunk)
const resp = await solarFetch(`/cgi/uc/attachments/multipart/${multipartInfo.value.rid}/${chunkId}`, {
method: "POST",
body: data,
body: chunk,
headers: {
"Content-Type": "application/octet-stream",
},
signal: AbortSignal.timeout(3 * 60 * 1000),
})
if (resp.status != 200) throw new Error(await resp.text())

View File

@@ -1,4 +1,6 @@
<template>
<canvas ref="canvasRef" class="fixed top-0 left-0 w-screen h-screen opacity-50"></canvas>
<v-container class="flex flex-col my-2 px-12 gap-[4rem]">
<section class="content-section flex flex-col items-center justify-center text-center px-4">
<img
@@ -10,6 +12,7 @@
enter: {
y: 0,
opacity: 1,
transition: { duration: 0.8 }
},
}"
:src="Logo"
@@ -74,7 +77,7 @@
</template>
<script setup lang="ts">
import Logo from "../assets/logo-w-shadow.png"
import Logo from "~/assets/logo-w-shadow.png"
import { getLocale } from "~/utils/locale"
@@ -100,6 +103,88 @@ const { data: products } = await useAsyncData("products", () => {
.limit(5)
.find()
})
const canvasRef = ref(null)
onMounted(() => {
const canvas: HTMLCanvasElement = canvasRef.value!
const ctx = canvas.getContext("2d")!
const dpr = window.devicePixelRatio || 1;
canvas.width = window.innerWidth * dpr;
canvas.height = window.innerHeight * dpr;
let particles: Particle[] = []
const numParticles = 100
class Particle {
x: number
y: number
vx: number
vy: number
size: number
constructor() {
this.x = Math.random() * canvas.width
this.y = Math.random() * canvas.height
this.vx = (Math.random() - 0.5) * 1.5
this.vy = (Math.random() - 0.5) * 1.5
this.size = Math.random() * 3 + 1
}
move() {
this.x += this.vx
this.y += this.vy
if (this.x <= 0 || this.x >= canvas.width) this.vx *= -1
if (this.y <= 0 || this.y >= canvas.height) this.vy *= -1
}
draw() {
ctx.beginPath();
ctx.arc(this.x * dpr, this.y * dpr, this.size * dpr, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
ctx.fill();
}
}
function init() {
particles = []
for (let i = 0; i < numParticles; i++) {
particles.push(new Particle())
}
}
function drawLines() {
for (let i = 0; i < particles.length; i++) {
for (let j = i + 1; j < particles.length; j++) {
let dx = particles[i].x - particles[j].x;
let dy = particles[i].y - particles[j].y;
let distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 100) {
ctx.beginPath();
ctx.moveTo(particles[i].x * dpr, particles[i].y * dpr);
ctx.lineTo(particles[j].x * dpr, particles[j].y * dpr);
ctx.strokeStyle = 'rgba(255, 255, 255, 0.2)';
ctx.lineWidth = 0.5 * dpr;
ctx.stroke();
}
}
}
}
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height)
particles.forEach((p) => {
p.move()
p.draw()
})
drawLines()
requestAnimationFrame(animate)
}
init()
animate()
})
</script>
<style scoped>

View File

@@ -0,0 +1,185 @@
<template>
<v-container class="flex flex-col my-2 px-12 gap-[4rem]">
<section class="content-section flex flex-col items-center justify-center text-center px-4" id="intro">
<div class="pt-1/3 mb-4 w-full relative">
<img :src="AlphaScreenshot" class="absolute bottom-2 left-0 right-0" />
<img
v-motion="{
initial: {
y: 100,
opacity: 0,
},
enter: {
y: 0,
opacity: 1,
transition: { duration: 0.8 },
},
}"
:src="Icon"
alt="Solar Network Logo"
class="w-32 h-32 p-2 z-10 mx-auto icon-glow bg-white dark:bg-black shadow-2xl rounded-xl"
/>
</div>
<div>
<h1 class="text-4xl font-bold">Solar Network</h1>
<p class="mt-2 text-lg">{{ t("solarNetworkDescription") }}</p>
<v-btn class="mt-4" color="primary" prepend-icon="mdi-arrow-down" href="#products">{{ t("learnMore") }}</v-btn>
</div>
</section>
<section class="content-section flex flex-col items-center justify-center text-center px-4" id="downloads">
<h1 class="text-3xl font-bold">{{ t("download") }}</h1>
<p class="text-lg">
File-hosting & versioning by
<nuxt-link class="underline" to="https://github.com/Solsynth/HyperNet.Surface" target="_blank">GitHub</nuxt-link
><sup>®</sup>
</p>
<v-btn
v-if="hasPrerelease"
slim
density="compact"
prepend-icon="mdi-beta"
variant="text"
style="text-transform: none"
color="white"
@click="showPrerelease = !showPrerelease"
>
{{ showPrerelease ? t("downloadSwitchRelease") : t("downloadSwitchPrerelease") }}
</v-btn>
<div class="max-h-[500px] w-full mt-4 text-left">
<v-row dense>
<v-col cols="12" md="6">
<v-card
prepend-icon="mdi-alert-decagram"
:title="showPrerelease ? 'Latest pre-release' : 'Latest release'"
density="comfortable"
>
<v-card-text v-if="currentRelease.status.value === 'success'">
<p class="text-xs">
<code>{{ currentRelease.data.value?.tag_name }}</code>
</p>
<p class="font-bold text-lg">{{ latestRelease.data.value?.name }}</p>
<article class="prose prose-sm max-h-[360px] overflow-y-auto" style="max-width: unset">
<m-d-c :value="currentRelease.data.value!.body!" />
</article>
</v-card-text>
<div v-else>
<v-progress-circular class="px-5 my-3" indeterminate />
</div>
</v-card>
</v-col>
<v-col cols="12" md="6">
<v-card prepend-icon="mdi-download" title="Distributions" density="comfortable">
<div v-if="currentRelease.status.value === 'success'">
<v-list density="comfortable" slim>
<v-list-item
v-for="asset in currentRelease.data.value!.assets"
:key="asset.id"
:title="asset.label ?? asset.name"
:subtitle="formatBytes(asset.size)"
:href="asset.browser_download_url"
target="_blank"
/>
</v-list>
</div>
<div v-else>
<v-progress-circular class="px-5 my-3" indeterminate />
</div>
<v-card-text>
<p class="text-sm opacity-50 mb-2">{{ t('downloadForApple') }}</p>
<div class="flex align-center gap-2.5">
<nuxt-link
to="https://apps.apple.com/us/app/solian/id6499032345?itscg=30200&itsct=apps_box_link&mttnsubad=6499032345"
target="_blank"
>
<img :src="AppStoreDownload" />
</nuxt-link>
<div>
<nuxt-link to="https://testflight.apple.com/join/YJ0lmN6O" target="_blank" class="underline">
{{ t('downloadTestFlight') }}
</nuxt-link>
<p class="text-xs opacity-40">{{ t('downloadTestFlightDescription') }}</p>
</div>
</div>
<p class="text-sm opacity-50 mt-4">{{ t('downloadForDesktop') }}</p>
<p class="text-sm">{{ t('downloadForDesktopDescription') }}</p>
<p class="text-sm opacity-50 mt-4">{{ t('downloadWithoutDownload') }}</p>
<div class="text-sm flex gap-2 underline">
<nuxt-link to="https://sn.solsynth.dev" target="_blank">{{ t('downloadWeb') }}</nuxt-link>
<nuxt-link to="https://sn.solsynth.dev?cdn=cn" target="_blank"
>{{ t('downloadWebChina') }}</nuxt-link
>
</div>
</v-card-text>
</v-card>
</v-col>
</v-row>
</div>
</section>
</v-container>
</template>
<script lang="ts" setup>
import Icon from "~/assets/products/solar-network/icon.png"
import AlphaScreenshot from "~/assets/products/solar-network/alpha.webp"
import AppStoreDownload from "~/assets/products/app-store-download.svg"
import { formatBytes } from "~/utils/format"
import { Octokit } from "@octokit/rest"
const { t } = useI18n()
const latestRelease = useAsyncData("sn-latest-release", async () => {
const octo = new Octokit({})
const resp = await octo.repos.getLatestRelease({
owner: "Solsynth",
repo: "HyperNet.Surface",
})
return resp.data
})
const latestPrerelease = useAsyncData("sn-latest-prerelease", async () => {
const octo = new Octokit({})
const resp = await octo.repos.listReleases({
owner: "Solsynth",
repo: "HyperNet.Surface",
per_page: 1,
})
return resp.data[0]
})
const showPrerelease = ref(false)
const currentRelease = computed(() => (showPrerelease.value ? latestPrerelease : latestRelease))
const hasPrerelease = computed<boolean>(
() => latestPrerelease.data?.value?.tag_name != latestRelease.data?.value?.tag_name,
)
</script>
<style scoped>
.content-section {
min-height: calc(100vh - 80px);
display: flex;
place-items: center;
}
.icon-glow {
-webkit-filter: drop-shadow(0 0 7px rgba(0, 0, 0, 0.5));
filter: drop-shadow(0 0 7px rgba(0, 0, 0, 0.5));
}
@media (prefers-color-scheme: dark) {
.icon-glow {
-webkit-filter: invert() drop-shadow(0 0 7px rgba(255, 255, 255, 0.5));
filter: invert() drop-shadow(0 0 7px rgba(255, 255, 255, 0.5));
}
}
</style>
<style>
body,
html {
scroll-behavior: smooth;
}
</style>

View File

@@ -27,7 +27,7 @@
</v-col>
<v-col row="12" lg="4" order="first" order-lg="last">
<div class="sticky top-0 h-fit">
<v-card prepend-icon="mdi-identifier" title="About">
<v-card prepend-icon="mdi-information-outline" title="About">
<v-card-text>
<p><b>Description</b></p>
<p>{{ account.description }}</p>
@@ -53,7 +53,7 @@ const config = useRuntimeConfig()
const tab = ref(1)
const { data: account } = await useFetch<any>(`${config.public.solarNetworkApi}/cgi/co/publisher/${route.params.name}`)
const { data: account } = await useFetch<any>(`${config.public.solarNetworkApi}/cgi/co/publishers/${route.params.name}`)
if (account.value == null) {
throw createError({

View File

@@ -12,16 +12,7 @@
</div>
<div class="mb-7">
<v-card rounded="xl" class="mx-[-5px]">
<v-tabs
v-model="tab"
align-tabs="start"
color="primary"
hide-slider
>
<v-tab :value="1">{{ t("userActivity") }}</v-tab>
</v-tabs>
</v-card>
</div>
<v-row>
@@ -46,10 +37,6 @@
</template>
<script setup lang="ts">
definePageMeta({
alias: ["/@:name(.*)*"],
})
const { t } = useI18n()
const route = useRoute()
const config = useRuntimeConfig()
@@ -67,6 +54,4 @@ if (account.value == null) {
const urlOfAvatar = computed(() => account.value?.avatar ? `${config.public.solarNetworkApi}/cgi/uc/attachments/${account.value.avatar}` : void 0)
const urlOfBanner = computed(() => account.value?.banner ? `${config.public.solarNetworkApi}/cgi/uc/attachments/${account.value.banner}` : void 0)
const externalOpenLink = computed(() => `${config.public.solianUrl}/accounts/view/${route.params.name}`)
</script>