🗑️ Remove the built-in frontends

This commit is contained in:
2025-09-19 00:11:26 +08:00
parent cc59e046bd
commit 1e374a73c7
109 changed files with 0 additions and 8089 deletions

View File

@@ -1,9 +0,0 @@
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}]
charset = utf-8
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
end_of_line = lf
max_line_length = 100

View File

@@ -1 +0,0 @@
* text=auto eol=lf

View File

@@ -1,31 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
**/node_modules/highlight.js/
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo

View File

@@ -1,6 +0,0 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"singleQuote": true,
"printWidth": 100
}

View File

@@ -1,9 +0,0 @@
{
"recommendations": [
"Vue.volar",
"dbaeumer.vscode-eslint",
"EditorConfig.EditorConfig",
"oxc.oxc-vscode",
"esbenp.prettier-vscode"
]
}

View File

@@ -1 +0,0 @@
/// <reference types="vite/client" />

View File

@@ -1,31 +0,0 @@
import { globalIgnores } from 'eslint/config'
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
import pluginVue from 'eslint-plugin-vue'
import pluginOxlint from 'eslint-plugin-oxlint'
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'
// To allow more languages other than `ts` in `.vue` files, uncomment the following lines:
// import { configureVueProject } from '@vue/eslint-config-typescript'
// configureVueProject({ scriptLangs: ['ts', 'tsx'] })
// More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup
export default defineConfigWithVueTs(
{
name: 'app/files-to-lint',
files: ['**/*.{ts,mts,tsx,vue}'],
},
globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']),
pluginVue.configs['flat/essential'],
vueTsConfigs.recommended,
...pluginOxlint.configs['flat/recommended'],
{
rules: {
'vue/multi-word-component-names': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
},
},
skipFormatting,
)

View File

@@ -1,15 +0,0 @@
<!doctype html>
<html lang="">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Solar Network</title>
<app-data />
<og-data />
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@@ -1,61 +0,0 @@
{
"name": "@solar-network/sphere",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --build",
"lint:oxlint": "oxlint . --fix -D correctness --ignore-path .gitignore",
"lint:eslint": "eslint . --fix",
"lint": "run-s lint:*",
"format": "prettier --write src/"
},
"dependencies": {
"@fingerprintjs/fingerprintjs": "^4.6.2",
"@fontsource-variable/nunito": "^5.2.6",
"@hcaptcha/vue3-hcaptcha": "^1.3.0",
"@milkdown/crepe": "^7.15.2",
"@milkdown/kit": "^7.15.2",
"@milkdown/vue": "^7.15.2",
"@tailwindcss/typography": "^0.5.16",
"@tailwindcss/vite": "^4.1.11",
"@vueuse/core": "^13.5.0",
"aspnet-prerendering": "^3.0.1",
"cfturnstile-vue3": "^2.0.0",
"chart.js": "^4.5.0",
"dayjs": "^1.11.13",
"marked": "^16.1.1",
"pinia": "^3.0.3",
"tailwindcss": "^4.1.11",
"tus-js-client": "^4.3.1",
"vue": "^3.5.17",
"vue-chartjs": "^5.3.2",
"vue-router": "^4.5.1"
},
"devDependencies": {
"@tsconfig/node22": "^22.0.2",
"@types/node": "^22.16.4",
"@vicons/material": "^0.13.0",
"@vitejs/plugin-vue": "^6.0.0",
"@vitejs/plugin-vue-jsx": "^5.0.1",
"@vue/eslint-config-prettier": "^10.2.0",
"@vue/eslint-config-typescript": "^14.6.0",
"@vue/tsconfig": "^0.7.0",
"eslint": "^9.31.0",
"eslint-plugin-oxlint": "~1.1.0",
"eslint-plugin-vue": "~10.2.0",
"jiti": "^2.4.2",
"naive-ui": "^2.42.0",
"npm-run-all2": "^8.0.4",
"oxlint": "~1.1.0",
"prettier": "3.5.3",
"typescript": "~5.8.3",
"vite": "npm:rolldown-vite@latest",
"vite-plugin-vue-devtools": "^7.7.7",
"vue-tsc": "^2.2.12"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

View File

@@ -1,10 +0,0 @@
@import "tailwindcss";
@plugin "@tailwindcss/typography";
@layer theme, base, components, utilities;
@layer base {
body {
font-family: 'Nunito Variable', sans-serif;
}
}

View File

@@ -1,20 +0,0 @@
<template>
<n-image v-if="itemType == 'image'" :src="remoteSource" class="rounded-md">
<template #error>
<img src="/image-broken.jpg" class="w-32 h-32 rounded-md" />
</template>
</n-image>
<audio v-else-if="itemType == 'audio'" :src="remoteSource" controls />
<video v-else-if="itemType == 'video'" :src="remoteSource" controls />
</template>
<script lang="ts" setup>
import { NImage } from 'naive-ui'
import { computed } from 'vue'
const props = defineProps<{ item: any }>()
const itemType = computed(() => props.item.mime_type.split('/')[0] ?? 'unknown')
const remoteSource = computed(() => `/cgi/drive/files/${props.item.id}?original=true`)
</script>

View File

@@ -1,178 +0,0 @@
<template>
<n-upload
abstract
with-credentials
@remove="handleRemove"
:create-thumbnail-url="createThumbnailUrl"
:custom-request="customRequest"
v-model:file-list="fileList"
>
<div class="flex flex-col gap-2">
<pub-select v-model:value="publisher" />
<n-input
type="textarea"
placeholder="What's happended?!"
v-model:value="content"
@keydown.meta.enter.exact="submit"
@keydown.ctrl.enter.exact="submit"
/>
<n-upload-file-list v-if="fileList" />
<div class="flex justify-between">
<div class="flex gap-2">
<n-upload-trigger #="{ handleClick }" abstract>
<n-button @click="handleClick">
<n-icon><upload-round /></n-icon>
</n-button>
</n-upload-trigger>
</div>
<n-button type="primary" icon-placement="right" :loading="submitting" @click="submit">
Post
<template #icon>
<n-icon><send-round /></n-icon>
</template>
</n-button>
</div>
</div>
</n-upload>
</template>
<script setup lang="ts">
import {
NInput,
NButton,
NIcon,
NUpload,
NUploadFileList,
NUploadTrigger,
useMessage,
type UploadSettledFileInfo,
type UploadCustomRequestOptions,
create,
type UploadFileInfo,
} from 'naive-ui'
import { SendRound, UploadRound } from '@vicons/material'
import { ref } from 'vue'
import * as tus from 'tus-js-client'
import PubSelect from './PubSelect.vue'
const emits = defineEmits(['posted'])
const publisher = ref<string | undefined>()
const content = ref('')
const fileList = ref<UploadFileInfo[]>([])
const submitting = ref(false)
async function submit() {
submitting.value = true
await fetch(`/api/posts?pub=${publisher.value}`, {
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify({
content: content.value,
attachments: fileList.value
.filter((e) => e.url != null)
.map((e) => e.url!.split('/').reverse()[0]),
}),
})
submitting.value = false
content.value = ''
fileList.value = []
emits('posted')
}
const messageDisplay = useMessage()
function customRequest({
file,
headers,
withCredentials,
onFinish,
onError,
onProgress,
}: UploadCustomRequestOptions) {
const requestHeaders: Record<string, string> = {}
const upload = new tus.Upload(file.file, {
endpoint: '/cgi/drive/tus',
retryDelays: [0, 3000, 5000, 10000, 20000],
removeFingerprintOnSuccess: false,
uploadDataDuringCreation: false,
metadata: {
filename: file.name,
'content-type': file.type ?? 'application/octet-stream',
},
headers: {
'X-DirectUpload': 'true',
...requestHeaders,
...headers,
},
onShouldRetry: () => false,
onError: function (error) {
if (error instanceof tus.DetailedError) {
const failedBody = error.originalResponse?.getBody()
if (failedBody != null)
messageDisplay.error(`Upload failed: ${failedBody}`, {
duration: 10000,
closable: true,
})
}
console.error('[DRIVE] Upload failed:', error)
onError()
},
onProgress: function (bytesUploaded, bytesTotal) {
onProgress({ percent: (bytesUploaded / bytesTotal) * 100 })
},
onSuccess: function (payload) {
const rawInfo = payload.lastResponse.getHeader('x-fileinfo')
const jsonInfo = JSON.parse(rawInfo as string)
console.log('[DRIVE] Upload successful: ', jsonInfo)
file.url = `/cgi/drive/files/${jsonInfo.id}`
file.type = jsonInfo.mime_type
onFinish()
},
onBeforeRequest: function (req) {
const xhr = req.getUnderlyingObject()
xhr.withCredentials = withCredentials
},
})
upload.findPreviousUploads().then(function (previousUploads) {
if (previousUploads.length) {
upload.resumeFromPreviousUpload(previousUploads[0])
}
upload.start()
})
}
function createThumbnailUrl(
_file: File | null,
fileInfo: UploadSettledFileInfo,
): string | undefined {
if (!fileInfo) return undefined
return fileInfo.url ?? undefined
}
function handleRemove(data: { file: UploadFileInfo; fileList: UploadFileInfo[] }) {
if (data.file.url == null) return true
const messageReactive = messageDisplay.loading('Deleting files...', {
duration: 0,
})
return new Promise((resolve) => {
fetch(`/cgi/drive/files/${data.file.url!.split('/').reverse()[0]}`, { method: 'DELETE' })
.then(() => {
messageReactive.destroy()
messageDisplay.success('File has been deleted')
resolve(true)
})
.catch((err) => {
messageReactive.destroy()
messageDisplay.error('Unable to delete this file: ' + err)
resolve(false)
})
})
}
</script>

View File

@@ -1,19 +0,0 @@
<template>
<div>
<milkdown />
</div>
</template>
<script setup lang="ts">
import { Milkdown, useEditor } from "@milkdown/vue";
import { Crepe } from "@milkdown/crepe";
import "@milkdown/crepe/theme/common/style.css";
import "@milkdown/crepe/theme/frame.css";
useEditor((root) => {
const crepe = new Crepe({
root,
});
return crepe;
})
</script>

View File

@@ -1,34 +0,0 @@
<template>
<div class="flex gap-3 items-center">
<n-avatar round :size="40" :src="publisherAvatar" />
<div class="flex-grow-1 flex flex-col">
<p class="flex gap-1 items-baseline">
<span class="font-bold">{{ props.item.publisher.nick }}</span>
<span class="text-xs">@{{ props.item.publisher.name }}</span>
</p>
<p class="text-xs flex gap-1">
<span>{{ dayjs(props.item.created_at).utc().fromNow() }}</span>
<span class="font-bold">·</span>
<span>{{ new Date(props.item.created_at).toLocaleString() }}</span>
</p>
</div>
</div>
</template>
<script lang="ts" setup>
import { NAvatar } from 'naive-ui'
import { computed } from 'vue'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import utc from 'dayjs/plugin/utc'
dayjs.extend(utc)
dayjs.extend(relativeTime)
const props = defineProps<{ item: any }>()
const publisherAvatar = computed(() =>
props.item.publisher.picture ? `/cgi/drive/files/${props.item.publisher.picture.id}` : undefined,
)
</script>

View File

@@ -1,53 +0,0 @@
<template>
<n-card>
<div class="flex flex-col gap-3">
<post-header :item="props.item" />
<div v-if="props.item.title || props.item.description">
<h2 class="text-lg" v-if="props.item.title">{{ props.item.title }}</h2>
<p class="text-sm" v-if="props.item.description">
{{ props.item.description }}
</p>
</div>
<article v-if="htmlContent" class="prose prose-sm dark:prose-invert prose-slate prose-p:m-0">
<div v-html="htmlContent"></div>
</article>
<div v-if="props.item.attachments">
<n-image-group>
<n-space>
<attachment-item
v-for="attachment in props.item.attachments"
:key="attachment.id"
:item="attachment"
/>
</n-space>
</n-image-group>
</div>
</div>
</n-card>
</template>
<script lang="ts" setup>
import { NCard, NImageGroup, NSpace } from 'naive-ui'
import { ref, watch } from 'vue'
import { Marked } from 'marked'
import PostHeader from './PostHeader.vue'
import AttachmentItem from './AttachmentItem.vue'
const props = defineProps<{ item: any }>()
const marked = new Marked()
const htmlContent = ref<string>('')
watch(
props.item,
async (value) => {
if (value.content) htmlContent.value = await marked.parse(value.content)
},
{ immediate: true, deep: true },
)
</script>

View File

@@ -1,97 +0,0 @@
<template>
<n-select
:options="pubStore.publishers"
label-field="nick"
value-field="name"
:value="props.value"
:render-label="renderLabel"
:render-tag="renderSingleSelectTag"
@update:value="(v) => emits('update:value', v)"
/>
</template>
<script setup lang="ts">
import { usePubStore } from '@/stores/pub'
import { NAvatar, NSelect, NText, type SelectRenderLabel, type SelectRenderTag } from 'naive-ui'
import { h, watch } from 'vue'
const pubStore = usePubStore()
const props = defineProps<{ value: string | undefined }>()
const emits = defineEmits(['update:value'])
watch(
pubStore,
(value) => {
if (!props.value && value.publishers) {
emits('update:value', pubStore.publishers[0].name)
}
},
{ deep: true, immediate: true },
)
const renderSingleSelectTag: SelectRenderTag = ({ option }: { option: any }) => {
return h(
'div',
{
style: {
display: 'flex',
alignItems: 'center',
},
},
[
h(NAvatar, {
src: option.picture
? `/cgi/drive/files/${option.picture.id}`
: undefined,
round: true,
size: 24,
style: {
marginRight: '8px',
},
}),
option.nick as string,
],
)
}
const renderLabel: SelectRenderLabel = (option: any) => {
return h(
'div',
{
style: {
display: 'flex',
alignItems: 'center',
},
},
[
h(NAvatar, {
src: option.picture
? `/cgi/drive/files/${option.picture.id}`
: undefined,
round: true,
size: 'small',
}),
h(
'div',
{
style: {
marginLeft: '8px',
padding: '4px 0',
},
},
[
h('div', null, [option.nick as string]),
h(
NText,
{ depth: 3, tag: 'div' },
{
default: () => `@${option.name}`,
},
),
],
),
],
)
}
</script>

View File

@@ -1,7 +0,0 @@
export {}
declare global {
interface Window {
DyPrefetch?: any
}
}

View File

@@ -1,115 +0,0 @@
<template>
<n-layout>
<n-layout-header class="border-b-1 flex justify-between items-center">
<router-link to="/" class="text-lg font-bold">Solar Network</router-link>
<div v-if="!hideUserMenu">
<n-dropdown
v-if="!userStore.isAuthenticated"
:options="guestOptions"
@select="handleGuestMenuSelect"
>
<n-button>Account</n-button>
</n-dropdown>
<n-dropdown v-else :options="userOptions" @select="handleUserMenuSelect" type="primary">
<n-button>{{ userStore.user.nick }}</n-button>
</n-dropdown>
</div>
</n-layout-header>
<n-layout-content embedded>
<router-view />
</n-layout-content>
</n-layout>
</template>
<script lang="ts" setup>
import { computed, h } from 'vue'
import { NLayout, NLayoutHeader, NLayoutContent, NButton, NDropdown, NIcon } from 'naive-ui'
import {
LogInOutlined,
PersonAddAlt1Outlined,
PersonOutlineRound,
DataUsageRound,
} from '@vicons/material'
import { useUserStore } from '@/stores/user'
import { useRoute, useRouter } from 'vue-router'
import { useServicesStore } from '@/stores/services'
const userStore = useUserStore()
const router = useRouter()
const route = useRoute()
const hideUserMenu = computed(() => {
return ['captcha', 'spells', 'login', 'create-account'].includes(route.name as string)
})
const guestOptions = [
{
label: 'Login',
key: 'login',
icon: () =>
h(NIcon, null, {
default: () => h(LogInOutlined),
}),
},
{
label: 'Create Account',
key: 'create-account',
icon: () =>
h(NIcon, null, {
default: () => h(PersonAddAlt1Outlined),
}),
},
]
const userOptions = computed(() => [
{
label: 'Dashboard',
key: 'dashboardUsage',
icon: () =>
h(NIcon, null, {
default: () => h(DataUsageRound),
}),
},
{
label: 'Profile',
key: 'profile',
icon: () =>
h(NIcon, null, {
default: () => h(PersonOutlineRound),
}),
},
])
const servicesStore = useServicesStore()
function handleGuestMenuSelect(key: string) {
if (key === 'login') {
window.open(servicesStore.getSerivceUrl('DysonNetwork.Pass', 'login')!, '_blank')
} else if (key === 'create-account') {
window.open(servicesStore.getSerivceUrl('DysonNetwork.Pass', 'create-account')!, '_blank')
}
}
function handleUserMenuSelect(key: string) {
if (key === 'profile') {
window.open(servicesStore.getSerivceUrl('DysonNetwork.Pass', 'accounts/me')!, '_blank')
} else {
router.push({ name: key })
}
}
</script>
<style scoped>
.n-layout-header {
padding: 8px 24px;
border-color: var(--n-border-color);
height: 57px; /* Fixed height */
display: flex;
align-items: center;
}
.n-layout-content {
height: calc(100vh - 57px); /* Adjust based on header height */
}
</style>

View File

@@ -1,16 +0,0 @@
import '@fontsource-variable/nunito';
import './assets/main.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import Root from './root.vue'
import router from './router'
const app = createApp(Root)
app.use(createPinia())
app.use(router)
app.mount('#app')

View File

@@ -1,61 +0,0 @@
<script setup lang="ts">
import LayoutDefault from './layouts/default.vue'
import { RouterView } from 'vue-router'
import {
NGlobalStyle,
NConfigProvider,
NMessageProvider,
NDialogProvider,
NLoadingBarProvider,
lightTheme,
darkTheme,
} from 'naive-ui'
import { usePreferredDark } from '@vueuse/core'
import { useUserStore } from './stores/user'
import { onMounted } from 'vue'
import { useServicesStore } from './stores/services'
import { MilkdownProvider } from '@milkdown/vue'
import { usePubStore } from './stores/pub'
const themeOverrides = {
common: {
fontFamily: 'Nunito Variable, v-sans, ui-system, -apple-system, sans-serif',
primaryColor: '#7D80BAFF',
primaryColorHover: '#9294C5FF',
primaryColorPressed: '#575B9DFF',
primaryColorSuppl: '#6B6FC1FF',
},
}
const isDark = usePreferredDark()
const userStore = useUserStore()
const servicesStore = useServicesStore()
const pubStore = usePubStore()
onMounted(() => {
userStore.initialize()
userStore.fetchUser()
servicesStore.fetchServices()
pubStore.fetchPublishers()
})
</script>
<template>
<milkdown-provider>
<n-config-provider :theme-overrides="themeOverrides" :theme="isDark ? darkTheme : lightTheme">
<n-global-style />
<n-loading-bar-provider>
<n-dialog-provider>
<n-message-provider placement="bottom">
<layout-default>
<router-view />
</layout-default>
</n-message-provider>
</n-dialog-provider>
</n-loading-bar-provider>
</n-config-provider>
</milkdown-provider>
</template>

View File

@@ -1,44 +0,0 @@
import { createRouter, createWebHistory } from 'vue-router'
import { useUserStore } from '@/stores/user'
import { useServicesStore } from '@/stores/services'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'index',
component: () => import('../views/index.vue'),
},
{
path: '/posts/:slug',
name: 'postDetail',
component: () => import('../views/posts.vue'),
},
],
})
router.beforeEach(async (to, from, next) => {
const userStore = useUserStore()
const servicesStore = useServicesStore()
// Initialize user state if not already initialized
if (!userStore.user) {
await userStore.fetchUser()
}
if (to.matched.some((record) => record.meta.requiresAuth) && !userStore.isAuthenticated) {
window.open(
servicesStore.getSerivceUrl(
'DysonNetwork.Pass',
'login?redirect=' + encodeURIComponent(window.location.href),
)!,
'_blank',
)
next('/')
} else {
next()
}
})
export default router

View File

@@ -1,13 +0,0 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const usePubStore = defineStore('pub', () => {
const publishers = ref<any[]>([])
async function fetchPublishers() {
const resp = await fetch('/api/publishers')
publishers.value = await resp.json()
}
return { publishers, fetchPublishers }
})

View File

@@ -1,27 +0,0 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useServicesStore = defineStore('services', () => {
const services = ref<Record<string, string>>({})
async function fetchServices() {
try {
const response = await fetch('/cgi/.well-known/services')
if (!response.ok) {
throw new Error('Network response was not ok')
}
const data = await response.json()
services.value = data
} catch (error) {
console.error('Failed to fetch services:', error)
services.value = {}
}
}
function getSerivceUrl(serviceName: string, ...parts: string[]): string | null {
const baseUrl = services.value[serviceName] || null
return baseUrl ? `${baseUrl}/${parts.join('/')}` : null
}
return { services, fetchServices, getSerivceUrl }
})

View File

@@ -1,65 +0,0 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useUserStore = defineStore('user', () => {
// State
const user = ref<any>(null)
const isLoading = ref(false)
const error = ref<string | null>(null)
// Getters
const isAuthenticated = computed(() => !!user.value)
// Actions
async function fetchUser(reload = true) {
if (!reload && user.value) return
isLoading.value = true
error.value = null
try {
const response = await fetch('/cgi/id/accounts/me', {
credentials: 'include',
})
if (!response.ok) {
// If the token is invalid, clear it and the user state
throw new Error('Failed to fetch user information.')
}
user.value = await response.json()
} catch (e: any) {
error.value = e.message
user.value = null // Clear user data on error
} finally {
isLoading.value = false
}
}
function initialize() {
const allowedOrigin = import.meta.env.DEV ? window.location.origin : 'https://id.solian.app'
window.addEventListener('message', (event) => {
// IMPORTANT: Always check the origin of the message for security!
// This prevents malicious scripts from sending fake login status updates.
// Ensure event.origin exactly matches your identity service's origin.
if (event.origin !== allowedOrigin) {
console.warn(`[SYNC] Message received from unexpected origin: ${event.origin}. Ignoring.`)
return // Ignore messages from unknown origins
}
// Check if the message is the type we're expecting
if (event.data && event.data.type === 'DY:LOGIN_STATUS_CHANGE') {
const { loggedIn } = event.data
console.log(`[SYNC] Received login status change: ${loggedIn}`)
fetchUser() // Re-fetch user data on login status change
}
})
}
return {
user,
isLoading,
error,
isAuthenticated,
fetchUser,
initialize,
}
})

View File

@@ -1,90 +0,0 @@
<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.startsWith('posts')"
:item="activity.data"
@click="router.push('/posts/' + activity.id)"
/>
</div>
</n-infinite-scroll>
</n-gi>
<n-gi span="2" class="max-lg:order-first">
<n-card class="w-full mt-4" title="About" v-if="!userStore.user">
<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-card class="mt-4 w-full">
<post-editor @posted="refreshActivities" />
</n-card>
<n-alert closable class="mt-4" w-full type="info" title="Looking for Solian?">
The flutter based web app Solian has been moved to
<n-a href="https://web.solian.app" target="_blank">web.solian.app</n-a>
<n-hr />
网页版 Solian 已经被移动到
<n-a href="https://web.solian.app" target="_blank">web.solian.app</n-a>
</n-alert>
</n-gi>
</n-grid>
</div>
</template>
<script setup lang="ts">
import { NCard, NInfiniteScroll, NGrid, NGi, NAlert, NA, NHr } from 'naive-ui'
import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { useUserStore } from '@/stores/user'
import PostEditor from '@/components/PostEditor.vue'
import PostItem from '@/components/PostItem.vue'
const router = useRouter()
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[Math.max(activites.value.length - 1, 0)])
const activitesHasMore = ref(true)
async function fetchActivites() {
if (loading.value) return
if (!activitesHasMore.value) return
loading.value = true
const resp = await fetch(
activitesLast.value == null
? '/api/activities'
: `/api/activities?cursor=${new Date(activitesLast.value.created_at).toISOString()}`,
)
const data = await resp.json()
activites.value = [...activites.value, ...data]
activitesHasMore.value = data[0]?.type != 'empty'
loading.value = false
}
onMounted(() => fetchActivites())
async function refreshActivities() {
activites.value = []
fetchActivites()
}
</script>

View File

@@ -1,16 +0,0 @@
<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

@@ -1,100 +0,0 @@
<template>
<div v-if="post" class="container max-w-5xl mx-auto mt-4">
<n-grid cols="1 l:5" responsive="screen" :x-gap="16" :y-gap="16">
<n-gi span="3">
<post-item :item="post" />
</n-gi>
<n-gi span="2">
<n-card title="About the author">
<div class="relative mb-7">
<img
class="object-cover rounded-lg"
style="aspect-ratio: 16/7"
:src="publisherBackground"
/>
<div class="absolute left-3 bottom-[-24px]">
<n-avatar :src="publisherAvatar" :size="64" round bordered />
</div>
</div>
<div class="flex flex-col">
<p class="flex gap-1 items-baseline">
<span class="font-bold">
{{ post.publisher.nick }}
</span>
<span class="text-sm"> @{{ post.publisher.name }} </span>
</p>
<div class="max-h-96 overflow-y-auto">
<div
class="prose prose-sm dark:prose-invert prose-slate"
v-if="publisherBio"
v-html="publisherBio"
></div>
</div>
</div>
</n-card>
</n-gi>
</n-grid>
</div>
<div v-else-if="notFound" class="flex justify-center items-center h-full">
<n-result
status="404"
title="Post not found"
description="The post you are looking cannot be found, it might be deleted, or you have no permission to view it or it just never been posted."
/>
</div>
<div v-else class="flex justify-center items-center h-full">
<n-spin />
</div>
</template>
<script setup lang="ts">
import { NGrid, NGi, NCard, NAvatar } from 'naive-ui'
import { computed, onMounted, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import { Marked } from 'marked'
import PostItem from '@/components/PostItem.vue'
const route = useRoute()
const post = ref<any>()
const notFound = ref(false)
async function fetchPost() {
if (window.DyPrefetch?.Post != null) {
console.log('[Fetch] Use the pre-rendered post data.')
post.value = window.DyPrefetch.post
return
}
console.log('[Fetch] Using the API to load user data.')
try {
const resp = await fetch(`/api/posts/${route.params.slug}`)
post.value = await resp.json()
} catch (err) {
console.error(err)
notFound.value = true
}
}
onMounted(() => fetchPost())
const publisherAvatar = computed(() =>
post.value.publisher.picture ? `/cgi/drive/files/${post.value.publisher.picture.id}` : undefined,
)
const publisherBackground = computed(() =>
post.value.publisher.background
? `/cgi/drive/files/${post.value.publisher.background.id}`
: undefined,
)
const marked = new Marked()
const publisherBio = ref('')
watch(
post,
async (value) => {
if (value?.publisher?.bio) publisherBio.value = await marked.parse(value.publisher.bio)
},
{ immediate: true, deep: true },
)
</script>

View File

@@ -1,94 +0,0 @@
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)
}

View File

@@ -1,12 +0,0 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue", "./**/*.d.ts"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"paths": {
"@/*": ["./src/*"]
}
}
}

View File

@@ -1,11 +0,0 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
}
]
}

View File

@@ -1,19 +0,0 @@
{
"extends": "@tsconfig/node22/tsconfig.json",
"include": [
"vite.config.*",
"vitest.config.*",
"cypress.config.*",
"nightwatch.conf.*",
"playwright.config.*",
"eslint.config.*"
],
"compilerOptions": {
"noEmit": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"module": "ESNext",
"moduleResolution": "Bundler",
"types": ["node"]
}
}

View File

@@ -1,36 +0,0 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import vueDevTools from 'vite-plugin-vue-devtools'
import tailwindcss from '@tailwindcss/vite'
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
// https://vite.dev/config/
export default defineConfig({
base: '/',
plugins: [vue(), vueJsx(), vueDevTools(), tailwindcss()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
server: {
proxy: {
'/api/tus': {
target: 'http://localhost:5090',
changeOrigin: true,
},
'/api': {
target: 'http://localhost:5071',
changeOrigin: true,
},
'/cgi': {
target: 'http://localhost:5071',
changeOrigin: true,
}
},
},
})