🎉 Initial Commit for the Sphere webpage

This commit is contained in:
2025-08-03 20:11:30 +08:00
parent adf62fb42b
commit 7d3236550c
31 changed files with 886 additions and 9 deletions

View File

@@ -0,0 +1,67 @@
<template>
<div class="h-full max-w-5xl container mx-auto px-8">
<n-grid cols="1 l:5" responsive="screen" :x-gap="16">
<n-gi span="3">
<n-infinite-scroll style="height: calc(100vh - 57px)" :distance="10" @load="fetchActivites">
<div v-for="activity in activites" :key="activity.id" class="mt-4">
<post-item v-if="activity.type == 'posts.new'" :item="activity.data" />
</div>
</n-infinite-scroll>
</n-gi>
<n-gi span="2" class="max-lg:order-first">
<n-card class="w-full mt-4" title="About">
<p>Welcome to the <b>Solar Network</b></p>
<p>The open social network. Friendly to everyone.</p>
<p class="mt-4 opacity-75 text-xs">
<span v-if="version == null">Loading...</span>
<span v-else>
v{{ version.version }} @
{{ version.commit.substring(0, 6) }}
{{ version.updatedAt }}
</span>
</p>
</n-card>
</n-gi>
</n-grid>
</div>
</template>
<script setup lang="ts">
import { NCard, NInfiniteScroll, NGrid, NGi } from 'naive-ui'
import { computed, onMounted, ref } from 'vue'
import { useUserStore } from '@/stores/user'
import PostItem from '@/components/PostItem.vue'
const userStore = useUserStore()
const version = ref<any>(null)
async function fetchVersion() {
const resp = await fetch('/api/version')
version.value = await resp.json()
}
onMounted(() => fetchVersion())
const loading = ref(false)
const activites = ref<any[]>([])
const activitesLast = computed(
() =>
activites.value.sort(
(a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime(),
)[0],
)
async function fetchActivites() {
loading.value = true
const resp = await fetch(
activitesLast.value == null
? '/api/activities'
: `/api/activities?cursor=${new Date(activitesLast.value.created_at).toISOString()}`,
)
activites.value.push(...(await resp.json()))
loading.value = false
}
onMounted(() => fetchActivites())
</script>

View File

@@ -0,0 +1,16 @@
<template>
<section class="h-full flex items-center justify-center">
<n-result status="404" title="404" description="Page not found">
<template #footer>
<n-button @click="router.push('/')">Go to Home</n-button>
</template>
</n-result>
</section>
</template>
<script lang="ts" setup>
import { NResult, NButton } from 'naive-ui'
import { useRouter } from 'vue-router';
const router = useRouter()
</script>

View File

@@ -0,0 +1,94 @@
export async function downloadAndDecryptFile(
url: string,
password: string,
fileName: string,
onProgress?: (progress: number) => void,
): Promise<void> {
const response = await fetch(url)
if (!response.ok) throw new Error(`Failed to fetch: ${response.status}`)
const contentLength = +(response.headers.get('Content-Length') || 0)
const reader = response.body!.getReader()
const chunks: Uint8Array[] = []
let received = 0
while (true) {
const { done, value } = await reader.read()
if (done) break
if (value) {
chunks.push(value)
received += value.length
if (contentLength && onProgress) {
onProgress(received / contentLength)
}
}
}
const fullBuffer = new Uint8Array(received)
let offset = 0
for (const chunk of chunks) {
fullBuffer.set(chunk, offset)
offset += chunk.length
}
const decryptedBytes = await decryptFile(fullBuffer, password)
// Create a blob and trigger a download
const blob = new Blob([decryptedBytes])
const downloadUrl = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = downloadUrl
a.download = fileName
document.body.appendChild(a)
a.click()
a.remove()
URL.revokeObjectURL(downloadUrl)
}
export async function decryptFile(fileBuffer: Uint8Array, password: string): Promise<Uint8Array> {
const salt = fileBuffer.slice(0, 16)
const nonce = fileBuffer.slice(16, 28)
const tag = fileBuffer.slice(28, 44)
const ciphertext = fileBuffer.slice(44)
const enc = new TextEncoder()
const keyMaterial = await crypto.subtle.importKey(
'raw',
enc.encode(password),
{ name: 'PBKDF2' },
false,
['deriveKey'],
)
const key = await crypto.subtle.deriveKey(
{ name: 'PBKDF2', salt, iterations: 100000, hash: 'SHA-256' },
keyMaterial,
{ name: 'AES-GCM', length: 256 },
false,
['decrypt'],
)
const fullCiphertext = new Uint8Array(ciphertext.length + tag.length)
fullCiphertext.set(ciphertext)
fullCiphertext.set(tag, ciphertext.length)
let decrypted: ArrayBuffer
try {
decrypted = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv: nonce, tagLength: 128 },
key,
fullCiphertext,
)
} catch {
throw new Error('Incorrect password or corrupted file.')
}
const magic = new TextEncoder().encode('DYSON1')
const decryptedBytes = new Uint8Array(decrypted)
for (let i = 0; i < magic.length; i++) {
if (decryptedBytes[i] !== magic[i]) {
throw new Error('Incorrect password or corrupted file.')
}
}
return decryptedBytes.slice(magic.length)
}