Compare commits

..

No commits in common. "archive/svelte" and "master" have entirely different histories.

158 changed files with 7230 additions and 718 deletions

4
.env Normal file
View File

@ -0,0 +1,4 @@
NUXT_PUBLIC_SOLAR_REALM=solar-network
NUXT_PUBLIC_SITE_URL=https://solsynth.dev
NUXT_PUBLIC_SOLAR_NETWORK_API=https://api.sn.solsynth.dev
NUXT_PUBLIC_SOLIAN_URL=https://sn.solsynth.dev

30
.gitignore vendored
View File

@ -1,11 +1,21 @@
.DS_Store
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist
# Node dependencies
node_modules
/build
/dist
/.svelte-kit
/package
.env
.env.*
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
*.lockb
*.lock
# Logs
logs
*.log
# Misc
.DS_Store
.fleet
.idea

8
.idea/.gitignore vendored
View File

@ -1,8 +0,0 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@ -1,63 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<HTMLCodeStyleSettings>
<option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
</HTMLCodeStyleSettings>
<JSCodeStyleSettings version="0">
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="USE_DOUBLE_QUOTES" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
<option name="SPACES_WITHIN_IMPORTS" value="true" />
</JSCodeStyleSettings>
<TypeScriptCodeStyleSettings version="0">
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="USE_DOUBLE_QUOTES" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
<option name="SPACES_WITHIN_IMPORTS" value="true" />
</TypeScriptCodeStyleSettings>
<VueCodeStyleSettings>
<option name="INTERPOLATION_NEW_LINE_AFTER_START_DELIMITER" value="false" />
<option name="INTERPOLATION_NEW_LINE_BEFORE_END_DELIMITER" value="false" />
</VueCodeStyleSettings>
<codeStyleSettings language="HTML">
<option name="SOFT_MARGINS" value="120" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JavaScript">
<option name="SOFT_MARGINS" value="120" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="TypeScript">
<option name="SOFT_MARGINS" value="120" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="Vue">
<option name="SOFT_MARGINS" value="120" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>

View File

@ -1,5 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

View File

@ -1,6 +0,0 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/Capital.iml" filepath="$PROJECT_DIR$/.idea/Capital.iml" />
</modules>
</component>
</project>

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

1
.npmrc
View File

@ -1 +0,0 @@
engine-strict=true

View File

@ -1,4 +0,0 @@
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock

View File

@ -1,8 +0,0 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 120,
"plugins": ["prettier-plugin-svelte"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}

8
.prettierrc.json Normal file
View File

@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"tabWidth": 2,
"singleQuote": false,
"printWidth": 120,
"trailingComma": "all"
}

16
.roadsignrc Normal file
View File

@ -0,0 +1,16 @@
{
"sync": {
"region": "capital",
"configPath": "roadsign.toml"
},
"deployments": [
{
"region": "capital",
"site": "capital-app",
"path": ".output",
"postDeploy": {
"command": "apk add nodejs npm; cd server && npm install --platform=linux --arch=x64 sharp"
}
}
]
}

View File

@ -1,3 +0,0 @@
# Capital
Solsynth LLC Official Website and some Users Portal.

48
app.vue Normal file
View File

@ -0,0 +1,48 @@
<template>
<v-app>
<nuxt-loading-indicator color="white" />
<nuxt-layout>
<nuxt-page />
</nuxt-layout>
</v-app>
</template>
<script setup lang="ts">
import { useTheme } from "vuetify"
import "@unocss/reset/tailwind.css"
const theme = useTheme()
const auth = useUserinfo()
const { locale } = useI18n()
watch(locale, (value) => {
useHead({
htmlAttrs: {
lang: value,
},
})
}, { deep: true, immediate: true })
onMounted(() => {
theme.global.name.value = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", event => {
theme.global.name.value = event.matches ? "dark" : "light"
})
auth.readProfiles()
})
</script>
<style>
.page-enter-active,
.page-leave-active {
transition: all 0.25s ease-in-out;
}
.page-enter-from,
.page-leave-to {
opacity: 0;
filter: blur(1rem);
}
</style>

22
assets/index.css Normal file
View File

@ -0,0 +1,22 @@
@import url('https://fonts.googleapis.com/css2?family=Comfortaa:wght@300..700&family=Noto+Sans+JP:wght@100..900&family=Noto+Sans+SC:wght@100..900&family=Noto+Sans+TC:wght@100..900&family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap');
html, body {
padding: 0;
margin: 0;
}
html, body, #app, .v-application {
overflow: auto !important;
font-family: "Comfortaa", "Noto Sans SC", "Noto Sans TC", "Noto Sans JP", sans-serif !important;
font-optical-sizing: auto;
font-weight: 500;
font-style: normal;
}
.font-mono, code, pre {
font-family: "Roboto Mono", monospace !important;
font-optical-sizing: auto;
font-weight: 500;
font-style: normal;
}

BIN
assets/logo-w-shadow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 550 KiB

BIN
assets/logo.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

35
components/Copyright.vue Normal file
View File

@ -0,0 +1,35 @@
<template>
<div class="text-xs text-grey" :class="props.noCentered ? 'text-left' : 'text-center'">
<p>{{ t("copyright") }} © {{ new Date().getFullYear() }} {{ t("brandNameFormal") }}</p>
<p v-if="services" class="flex" :class="props.noCentered ? 'justify-start' : 'justify-center'">
<span>Powered by</span>
<span class="flex services-list ms-1">
<a class="service underline" v-for="item in services" :href="projects[item][1]">
{{ projects[item][0] }}
</a>
</span>
</p>
</div>
</template>
<script setup lang="ts">
const props = defineProps<{ service?: string | string[], noCentered?: boolean }>()
const services = computed<string[]>(() => props.service instanceof Array ? (props.service ?? []) : (props.service ? [props.service] : []))
const { t } = useI18n()
const projects: { [id: string]: [string, string] } = {
"solar-network": ["Solar Network", "https://solsynth.dev/products/solar-network"],
"capital": ["Capital", "https://git.solsynth.dev/Goatworks/Capital"],
"passport": ["Hydrogen.Passport", "https://git.solsynth.dev/Hydrogen/Passport"],
"paperclip": ["Hydrogen.Paperclip", "https://git.solsynth.dev/Hydrogen/Paperclip"],
"roadsign": ["RoadSign", "https://git.solsynth.dev/Goatworks/RoadSign"],
}
</script>
<style scoped>
.services-list .service:nth-child(n+2)::before {
content: ", ";
}
</style>

View File

@ -0,0 +1,33 @@
<template>
<div class="text-xs text-grey sidebar-footer transition-opacity duration-500">
<div class="flex footer-links flex-wrap">
<nuxt-link to="/terms/privacy-policy" class="hover:underline">Privacy Policy</nuxt-link>
<nuxt-link to="/terms/user-agreement" class="hover:underline">Term of Service</nuxt-link>
</div>
<div class="flex footer-links flex-wrap">
<nuxt-link to="https://status.solsynth.dev" target="_blank" class="hover:underline">Status of Service</nuxt-link>
</div>
</div>
</template>
<style scoped>
.sidebar-footer {
opacity: 0.3;
}
.sidebar-footer:hover {
opacity: 1;
}
.footer-links *:not(:last-child):after {
content: "·";
font-family: monospace;
font-weight: bold;
text-align: center;
margin-left: 4px;
margin-right: 4px;
text-decoration: none !important;
display: inline-block;
}
</style>
<script setup lang="ts">
</script>

View File

@ -0,0 +1,29 @@
<template>
<v-menu>
<template v-slot:activator="{ props }">
<v-btn
size="small"
icon="mdi-translate"
v-bind="props"
/>
</template>
<v-list>
<v-list-item
class="w-48"
density="compact"
v-for="item in locales"
:key="item.code"
:value="item.code"
:active="locale == item.code"
@click.prevent.stop="() => { setLocale(item.code); emits('update') }"
>
<v-list-item-title>{{ item.name }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</template>
<script lang="ts" setup>
const emits = defineEmits(['update'])
const { locale, locales, setLocale } = useI18n()
</script>

82
components/PostItem.vue Normal file
View File

@ -0,0 +1,82 @@
<template>
<v-card :to="url" class="mx-[2.5ch] mb-3">
<v-card-text>
<div class="mb-3 flex flex-row gap-4">
<nuxt-link :to="`/users/${post.author?.name}`">
<v-avatar :image="post.author?.avatar" />
</nuxt-link>
<div class="flex flex-col">
<span>{{ post.author?.nick }} <span class="text-xs">@{{ post.author?.name }}</span></span>
<span v-if="post.body?.title" class="text-md">{{ post.body?.title }}</span>
<span v-if="post.body?.description" class="text-sm">{{ post.body?.description }}</span>
<span v-if="!post.body?.title && !post.body?.description" class="text-sm">
{{ post.author?.description }}
</span>
<div v-if="post.type != 'story'" class="mt-1">
<v-btn size="x-small" variant="flat" append-icon="mdi-arrow-right" :text="t('continueReading')" />
</div>
</div>
</div>
<div v-if="post.body?.thumbnail" class="mb-5">
<v-img
:src="`${config.public.solarNetworkApi}/cgi/uc/attachments/${post.body?.thumbnail}`"
:aspect-ratio="16 / 9"
alt="Post thumbnail"
class="rounded-md"
cover
/>
</div>
<article v-if="post.type == 'story' || props.forceShowContent" class="text-base prose max-w-none">
<m-d-c :value="post.body?.content"></m-d-c>
</article>
<v-card v-if="post.body?.attachments?.length > 0" class="mb-5">
<attachment-carousel
:no-clickable-attachment="props.noClickableAttachment"
:attachments="post.body?.attachments"
/>
</v-card>
<div class="text-sm flex flex-col">
<span class="flex flex-row gap-1">
<span>
{{ post.metric.reply_count }} {{ post.metric.reply_count > 1 ? "replies" : "reply" }},
</span>
<span>
{{ post.metric.reaction_count }} {{ post.metric.reaction_count > 1 ? "reactions" : "reaction" }}
</span>
</span>
<span>
{{ post.type.startsWith("a") ? "An" : "A" }} {{ post.type }} posted on
{{ new Date(post.published_at).toLocaleString() }}
</span>
</div>
<div
v-if="post.tags?.length > 0"
class="text-xs text-grey flex flex-row gap-1 mt-3"
>
<nuxt-link
v-for="tag in post.tags"
:to="`/posts/tags/${tag.alias}`"
class="hover:underline hover:underline-dotted"
@click.stop
>
#{{ tag.alias }}
</nuxt-link>
</div>
</v-card-text>
</v-card>
</template>
<script setup lang="ts">
const props = defineProps<{ post: any, forceShowContent?: boolean, noClickableAttachment?: boolean }>()
const config = useRuntimeConfig()
const { t } = useI18n()
const url = computed(() => props.post.alias ? `/posts/${props.post.area_alias}/${props.post.alias}` : `/posts/${props.post.id}`)
</script>

47
components/PostList.vue Normal file
View File

@ -0,0 +1,47 @@
<template>
<v-infinite-scroll :items="posts" :onLoad="loadPost">
<template v-for="item in posts" :key="item">
<post-item :post="item" no-clickable-attachment />
</template>
</v-infinite-scroll>
</template>
<script setup lang="ts">
const props = defineProps<{ author?: string, tag?: string, category?: string, realm?: string }>()
const config = useRuntimeConfig()
const posts = ref<any[]>([])
async function loadPost({ done }: any) {
const searchQueries = new URLSearchParams({
take: (10).toString(),
offset: posts.value.length.toString(),
})
if (props.author) {
searchQueries.set("author", props.author)
}
if (props.realm) {
searchQueries.set("realm", props.realm)
}
if (props.tag) {
searchQueries.set("tag", props.tag)
}
if (props.category) {
searchQueries.set("category", props.category)
}
const res = await fetch(`${config.public.solarNetworkApi}/cgi/co/posts?` + searchQueries)
const result = await res.json()
if (result.data.length > 0) {
posts.value.push(...result.data)
done("ok")
} else {
done("empty")
}
}
</script>

View File

@ -0,0 +1,32 @@
<template>
<v-infinite-scroll :items="posts" :onLoad="loadPost">
<template v-for="item in posts" :key="item">
<post-item :post="item" no-clickable-attachment />
</template>
</v-infinite-scroll>
</template>
<script setup lang="ts">
const props = defineProps<{ postId: number }>()
const config = useRuntimeConfig()
const posts = ref<any[]>([])
async function loadPost({ done }: any) {
const searchQueries = new URLSearchParams({
take: (10).toString(),
offset: posts.value.length.toString(),
})
const res = await fetch(`${config.public.solarNetworkApi}/cgi/co/posts/${props.postId}/replies?` + searchQueries)
const result = await res.json()
if (result.data.length > 0) {
posts.value.push(...result.data)
done("ok")
} else {
done("empty")
}
}
</script>

58
components/UserMenu.vue Executable file
View File

@ -0,0 +1,58 @@
<template>
<v-menu>
<template #activator="{ props }">
<v-btn flat exact v-bind="props" icon>
<v-avatar v-if="id.isReady" color="transparent" icon="mdi-account-circle" :image="avatar" />
<v-progress-circular v-else indeterminate size="20" width="2.5" />
</v-btn>
</template>
<v-list density="compact" v-if="id.isLoggedIn">
<v-list-item :title="nickname" :subtitle="username" />
<v-divider class="border-opacity-50 my-2" />
<v-list-item :title="t('userMenuDashboard')" prepend-icon="mdi-account-supervisor" exact to="/users/me" />
<v-list-item :title="t('userMenuSignOut')" prepend-icon="mdi-logout" @click="signOut"></v-list-item>
</v-list>
<v-list density="compact" v-else>
<v-list-item :title="t('userMenuSignIn')" prepend-icon="mdi-login-variant" to="/auth/sign-in" />
<v-list-item :title="t('userMenuSignUp')" prepend-icon="mdi-account-plus" to="/auth/sign-up" />
</v-list>
</v-menu>
</template>
<script setup lang="ts">
import { useUserinfo } from "@/stores/userinfo"
import { computed } from "vue"
const { t } = useI18n()
const config = useRuntimeConfig()
const id = useUserinfo()
const username = computed(() => {
if (id.isLoggedIn) {
return "@" + id.userinfo?.name
} else {
return "@visitor"
}
})
const nickname = computed(() => {
if (id.isLoggedIn) {
return id.userinfo?.nick
} else {
return "Anonymous"
}
})
const avatar = computed(() => {
return id.userinfo?.avatar ? `${config.public.solarNetworkApi}/cgi/uc/attachments/${id.userinfo?.avatar}` : void 0
})
function signOut() {
useAtk().value = null
useRtk().value = null
id.userinfo.value = null
reloadNuxtApp()
}
</script>

View File

@ -0,0 +1,182 @@
<template>
<v-expansion-panels>
<v-expansion-panel eager title="Tickets">
<template #text>
<v-card :loading="reverting.tickets" variant="outlined">
<v-data-table-server
density="compact"
:headers="dataDefinitions.tickets"
:items="tickets"
:items-length="pagination.tickets.total"
:loading="reverting.tickets"
v-model:items-per-page="pagination.tickets.pageSize"
@update:options="readTickets"
item-value="id"
class="overflow-y-auto text-no-wrap"
>
<template v-slot:item="{ item }: { item: any }">
<tr>
<td>{{ item.id }}</td>
<td>{{ item.ip_address }}</td>
<td>
<v-tooltip :text="item.user_agent" location="top">
<template #activator="{ props }">
<div v-bind="props" class="text-ellipsis whitespace-nowrap overflow-hidden max-w-[280px]">
{{ item.user_agent }}
</div>
</template>
</v-tooltip>
</td>
<td>{{ new Date(item.created_at).toLocaleString() }}</td>
<td>
<v-tooltip text="Sign Out">
<template #activator="{ props }">
<v-btn
v-bind="props"
variant="text"
size="x-small"
color="error"
icon="mdi-logout-variant"
@click="killTicket(item)"
/>
</template>
</v-tooltip>
</td>
</tr>
</template>
</v-data-table-server>
</v-card>
</template>
</v-expansion-panel>
<v-expansion-panel eager title="Events">
<template #text>
<v-card :loading="reverting.events" variant="outlined">
<v-data-table-server
density="compact"
:headers="dataDefinitions.events"
:items="events"
:items-length="pagination.events.total"
:loading="reverting.events"
v-model:items-per-page="pagination.events.pageSize"
@update:options="readEvents"
item-value="id"
class="overflow-y-auto text-no-wrap"
>
<template v-slot:item="{ item }: { item: any }">
<tr>
<td>{{ item.id }}</td>
<td>{{ item.type }}</td>
<td>{{ item.target }}</td>
<td>{{ item.ip_address }}</td>
<td>
<v-tooltip :text="item.user_agent" location="top">
<template #activator="{ props }">
<div v-bind="props" class="text-ellipsis whitespace-nowrap overflow-hidden max-w-[180px]">
{{ item.user_agent }}
</div>
</template>
</v-tooltip>
</td>
<td>{{ new Date(item.created_at).toLocaleString() }}</td>
</tr>
</template>
</v-data-table-server>
</v-card>
</template>
</v-expansion-panel>
</v-expansion-panels>
</template>
<script setup lang="ts">
import { solarFetch } from "~/utils/request"
const error = ref<string | null>(null)
const dataDefinitions: { [id: string]: any[] } = {
tickets: [
{ align: "start", key: "id", title: "ID" },
{ align: "start", key: "ip_address", title: "IP Address" },
{ align: "start", key: "user_agent", title: "User Agent" },
{ align: "start", key: "created_at", title: "Issued At" },
{ align: "start", key: "actions", title: "Actions", sortable: false },
],
events: [
{ align: "start", key: "id", title: "ID" },
{ align: "start", key: "type", title: "Type" },
{ align: "start", key: "target", title: "Affected Object" },
{ align: "start", key: "ip_address", title: "IP Address" },
{ align: "start", key: "user_agent", title: "User Agent" },
{ align: "start", key: "created_at", title: "Performed At" },
],
}
const tickets = ref<any>([])
const events = ref<any>([])
const reverting = reactive({ tickets: false, events: false })
const pagination = reactive({
tickets: { page: 1, pageSize: 5, total: 0 },
events: { page: 1, pageSize: 5, total: 0 },
})
async function readTickets({ page, itemsPerPage }: { page?: number; itemsPerPage?: number }) {
if (itemsPerPage) pagination.tickets.pageSize = itemsPerPage
if (page) pagination.tickets.page = page
reverting.tickets = true
const res = await solarFetch(
"/cgi/id/users/me/tickets?" +
new URLSearchParams({
take: pagination.tickets.pageSize.toString(),
offset: ((pagination.tickets.page - 1) * pagination.tickets.pageSize).toString(),
}),
)
if (res.status !== 200) {
error.value = await res.text()
} else {
const data = await res.json()
tickets.value = data["data"]
pagination.tickets.total = data["count"]
}
reverting.tickets = false
}
async function readEvents({ page, itemsPerPage }: { page?: number; itemsPerPage?: number }) {
if (itemsPerPage) pagination.events.pageSize = itemsPerPage
if (page) pagination.events.page = page
reverting.events = true
const res = await solarFetch(
"/cgi/id/users/me/events?" +
new URLSearchParams({
take: pagination.events.pageSize.toString(),
offset: ((pagination.events.page - 1) * pagination.events.pageSize).toString(),
}),
)
if (res.status !== 200) {
error.value = await res.text()
} else {
const data = await res.json()
events.value = data["data"]
pagination.events.total = data["count"]
}
reverting.events = false
}
Promise.all([readTickets({}), readEvents({})])
async function killTicket(item: any) {
reverting.tickets = true
const res = await solarFetch(`/cgi/id/users/me/tickets/${item.id}`, {
method: "DELETE",
})
if (res.status !== 200) {
error.value = await res.text()
} else {
await readTickets({})
error.value = null
}
reverting.tickets = false
}
</script>

View File

@ -0,0 +1,52 @@
<template>
<v-card v-if="!loading" density="compact" variant="outlined">
<div class="h-[500px] overflow-y-auto no-scrollbar">
<div v-for="item in items" class="mt-5 mb-2">
<post-item :key="item.id" force-show-content :post="item" />
</div>
<div class="mt-4 mb-5 flex justify-center">
<v-btn :text="t('seeMore')" size="small" variant="text" to="/activity" />
</div>
</div>
</v-card>
<v-card v-else density="compact" variant="outlined">
<div class="w-full h-full flex items-center justify-center">
<v-progress-circular indeterminate />
</div>
</v-card>
</template>
<script setup lang="ts">
const { t } = useI18n()
const config = useRuntimeConfig()
const items = ref<any[]>([])
const loading = ref(false)
async function load() {
loading.value = true
const res = await fetch(`${config.public.solarNetworkApi}/cgi/co/posts?take=5&realm=${config.public.solarRealm}`)
const result = await res.json()
items.value.push(...result.data)
loading.value = false
}
onMounted(() => {
load()
})
</script>
<style scoped>
.no-scrollbar {
scrollbar-width: none;
}
.no-scrollbar::-webkit-scrollbar {
width: 0;
display: none;
}
</style>

View File

@ -0,0 +1,33 @@
<template>
<v-carousel
hide-delimiter-background
hide-delimiters
:progress="attachments.length > 1 ? 'primary' : false"
:show-arrows="attachments.length > 1 ? 'hover' : false"
height="auto"
>
<v-carousel-item v-for="item in metadata" class="fill-height">
<nuxt-link v-if="item.mimetype.split('/')[0] == 'image' && !props.noClickableAttachment"
:to="`/gallery/${item.rid}`">
<attachment-renderer :item="item" />
</nuxt-link>
<div v-else>
<attachment-renderer :item="item" />
</div>
</v-carousel-item>
</v-carousel>
</template>
<script setup lang="ts">
const props = defineProps<{ attachments: string[], noClickableAttachment?: boolean }>()
const emits = defineEmits(["update:metadata"])
const config = useRuntimeConfig()
const { data } = await useFetch<any>(`${config.public.solarNetworkApi}/cgi/uc/attachments?take=${props.attachments.length}&id=${props.attachments.join(",")}`)
const metadata = computed(() => data.value.data)
watch(metadata, (value) => {
emits("update:metadata", value)
}, { deep: true, immediate: true })
</script>

View File

@ -0,0 +1,61 @@
<template>
<v-sheet v-if="item.is_mature && !showMature" color="rgba(0, 0, 0, .4)" height="calc(100% + 24px)" class="p-5">
<v-row class="fill-height" align="center" justify="center">
<v-col class="text-center">
<h1 class="text-xl font-bold text-white">Mature Content</h1>
<p class="text-md text-white">This content is rated and may not suitable for everyone to view.</p>
<div class="flex justify-center mt-3">
<v-btn
variant="text"
color="white"
prepend-icon="mdi-eye"
text="Reveal"
@click="showMature = true"
/>
</div>
</v-col>
</v-row>
</v-sheet>
<v-img v-else-if="item.mimetype.split('/')[0] == 'image'" :src="getAttachmentUrl(item.rid)" :alt="item.alt"
class="w-full h-full" :cover="!props.noCover" />
<video v-else-if="item.mimetype.split('/')[0] == 'video'" :src="getAttachmentUrl(item.rid)" class="w-full h-full"
controls @click.stop />
<v-sheet v-else color="rgba(0, 0, 0, .4)" height="calc(100% + 24px)" class="p-5">
<v-row class="fill-height" align="center" justify="center">
<v-col class="text-center">
<h1 class="text-xl font-bold text-white">
{{ item.alt }}
</h1>
<p class="text-md text-white">{{ item.mimetype }}</p>
<p class="text-sm text-white mt-3">Unable to preview, you can open it via other ways.</p>
<div class="flex justify-center mt-3">
<v-btn
variant="text"
color="white"
prepend-icon="mdi-launch"
text="Open in browser"
:href="getAttachmentUrl(item.rid)"
target="_blank"
/>
</div>
</v-col>
</v-row>
</v-sheet>
</template>
<script setup lang="ts">
const config = useRuntimeConfig()
const props = defineProps<{ item: any, noCover?: boolean }>()
const item = computed(() => props.item)
const showMature = ref(false)
function getAttachmentUrl(id: string) {
return `${config.public.solarNetworkApi}/cgi/uc/attachments/${id}`
}
</script>

View File

@ -0,0 +1,69 @@
<template>
<div class="flex items-center">
<v-form class="flex-grow-1" @submit.prevent="submit">
<v-text-field
:label="t('username')"
variant="solo"
density="comfortable"
class="mb-3"
:hide-details="true"
:disabled="props.loading"
v-model="probe"
/>
<v-expand-transition>
<v-alert v-show="error" variant="tonal" type="error" class="text-xs mb-3">
{{ t("errorOccurred", [error]) }}
</v-alert>
</v-expand-transition>
<div class="flex justify-between">
<v-btn type="button" variant="plain" color="grey-darken-3" to="/auth/sign-up">{{ t("userMenuSignUp") }}</v-btn>
<v-btn
type="submit"
variant="text"
color="primary"
class="justify-self-end"
append-icon="mdi-arrow-right"
:disabled="props.loading"
>
{{ t("next") }}
</v-btn>
</div>
</v-form>
</div>
</template>
<script setup lang="ts">
const { t } = useI18n()
const config = useRuntimeConfig()
const probe = ref("")
const error = ref<string | null>(null)
const props = defineProps<{ loading?: boolean }>()
const emits = defineEmits(["swap", "update:loading", "update:ticket"])
async function submit() {
if (!probe.value) return
emits("update:loading", true)
const res = await fetch(`${config.public.solarNetworkApi}/cgi/id/auth`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username: probe.value }),
})
if (res.status !== 200) {
error.value = await res.text()
} else {
const data = await res.json()
emits("update:ticket", data["ticket"])
if (data.is_finished) emits("swap", "completed")
else emits("swap", "mfa")
error.value = null
}
emits("update:loading", false)
}
</script>

View File

@ -0,0 +1,67 @@
<template>
<div>
<v-icon icon="mdi-lan-check" size="32" color="grey-darken-3" class="mb-3" />
<h1 class="font-bold text-xl">{{ t("signInCompleted") }}</h1>
<p>{{ t("signInCompletedCaption") }}</p>
<v-expand-transition>
<v-alert v-show="error" variant="tonal" type="error" class="text-xs mb-3">
{{ t("errorOccurred", [error]) }}
</v-alert>
</v-expand-transition>
</div>
</template>
<script setup lang="ts">
const { t } = useI18n()
const config = useRuntimeConfig()
const route = useRoute()
const router = useRouter()
const auth = useUserinfo()
const props = defineProps<{ loading?: boolean; currentFactor?: any; ticket?: any }>()
const emits = defineEmits(["update:loading"])
const error = ref<string | null>(null)
async function load() {
emits("update:loading", true)
await getToken(props.ticket.grant_token)
await auth.readProfiles()
setTimeout(() => callback(), 1850)
}
onMounted(() => load())
async function getToken(tk: string) {
const res = await fetch(`${config.public.solarNetworkApi}/cgi/id/auth/token`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
code: tk,
grant_type: "grant_token",
}),
})
if (res.status !== 200) {
const err = await res.text()
error.value = err
throw new Error(err)
} else {
const out = await res.json()
auth.setTokenSet(out["access_token"], out["refresh_token"])
error.value = null
}
}
function callback() {
if (route.query["close"]) {
window.close()
} else if (route.query["redirect_uri"]) {
window.open((route.query["redirect_uri"] as string) ?? "/", "_self")
} else {
router.push("/users/me")
}
}
</script>

View File

@ -0,0 +1,15 @@
<template>
<div class="w-full max-w-[720px]">
<v-expand-transition>
<v-alert v-show="route.query['redirect_uri']" variant="tonal" type="info" class="text-xs">
{{ t("callbackHint") }} <br />
<span class="font-mono">{{ route.query["redirect_uri"] }}</span>
</v-alert>
</v-expand-transition>
</div>
</template>
<script setup lang="ts">
const { t } = useI18n()
const route = useRoute()
</script>

View File

@ -0,0 +1,93 @@
<template>
<div class="flex items-center">
<v-form class="flex-grow-1" @submit.prevent="submit">
<div v-if="inputType === 'one-time-password'" class="text-center">
<p class="text-xs opacity-90">{{ t("multiFactorHint") }}</p>
<v-otp-input
class="pt-0"
variant="solo"
density="compact"
type="text"
:length="6"
v-model="password"
:loading="loading"
/>
</div>
<v-text-field
v-else
:label="t('password')"
type="password"
variant="solo"
density="comfortable"
:disabled="loading"
v-model="password"
/>
<v-expand-transition>
<v-alert v-show="error" variant="tonal" type="error" class="text-xs mb-3">
{{ t("errorOccurred", [error]) }}
</v-alert>
</v-expand-transition>
<div class="flex justify-end">
<v-btn
type="submit"
variant="text"
color="primary"
class="justify-self-end"
append-icon="mdi-arrow-right"
:disabled="loading"
>
{{ t("next") }}
</v-btn>
</div>
</v-form>
</div>
</template>
<script setup lang="ts">
const { t } = useI18n()
const config = useRuntimeConfig()
const password = ref("")
const error = ref<string | null>(null)
const props = defineProps<{ loading?: boolean; currentFactor?: any; ticket?: any }>()
const emits = defineEmits(["swap", "update:ticket", "update:loading"])
async function submit() {
emits("update:loading", true)
const res = await fetch(`${config.public.solarNetworkApi}/cgi/id/auth`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
ticket_id: props.ticket?.id,
factor_id: props.currentFactor?.id,
code: password.value,
}),
})
if (res.status !== 200) {
error.value = await res.text()
} else {
const data = await res.json()
error.value = null
password.value = ""
emits("update:ticket", data["ticket"])
if (data["is_finished"]) emits("swap", "completed")
else emits("swap", "mfa")
}
emits("update:loading", false)
}
const inputType = computed(() => {
switch (props.currentFactor?.type) {
case 0:
return "text"
case 1:
return "one-time-password"
default:
return "unknown"
}
})
</script>

View File

@ -0,0 +1,90 @@
<template>
<div class="flex items-center">
<div class="flex-grow-1">
<v-card class="mb-3">
<v-list density="compact" color="primary">
<v-list-item
v-for="(item, idx) in factors ?? []"
:key="idx"
:prepend-icon="getFactorType(item)?.icon"
:title="getFactorType(item)?.label"
:active="focus === item.id"
:disabled="getFactorAvailable(item)"
@click="focus = item.id"
/>
</v-list>
</v-card>
<v-expand-transition>
<v-alert v-show="error" variant="tonal" type="error" class="text-xs mb-3">
{{ t("errorOccurred", [error]) }}
</v-alert>
</v-expand-transition>
<div class="flex justify-end">
<v-btn variant="text" color="primary" class="justify-self-end" append-icon="mdi-arrow-right" @click="submit">
{{ t("next") }}
</v-btn>
</div>
</div>
</div>
</template>
<script setup lang="ts">
const { t } = useI18n()
const config = useRuntimeConfig()
const focus = ref<number | null>(null)
const factors = ref<any[]>([])
const error = ref<string | null>(null)
const props = defineProps<{ ticket?: any }>()
const emits = defineEmits(["swap", "update:loading", "update:currentFactor"])
async function load() {
emits("update:loading", true)
const res = await fetch(`${config.public.solarNetworkApi}/cgi/id/auth/factors?ticketId=${props.ticket.id}`)
if (res.status !== 200) {
error.value = await res.text()
} else {
factors.value = await res.json()
}
emits("update:loading", false)
}
onMounted(() => load())
async function submit() {
if (!focus.value) return
emits("update:loading", true)
const res = await fetch(`${config.public.solarNetworkApi}/cgi/id/auth/factors/${focus.value}`, {
method: "POST",
})
if (res.status !== 200 && res.status !== 204) {
error.value = await res.text()
} else {
const item = factors.value.find((item: any) => item.id === focus.value)
emits("update:currentFactor", item)
emits("swap", "applicator")
error.value = null
focus.value = null
}
emits("update:loading", false)
}
function getFactorType(item: any) {
switch (item.type) {
case 0:
return { icon: "mdi-form-textbox-password", label: t("multiFactorTypePassword") }
case 1:
return { icon: "mdi-email-fast", label: t("multiFactorTypeEmail") }
}
}
function getFactorAvailable(factor: any) {
const blacklist: number[] = props.ticket?.factor_trail ?? []
return blacklist.includes(factor.id)
}
</script>

View File

@ -0,0 +1,20 @@
<template>
<v-card :title="t('download')" :subtitle="t('downloadDescription')" density="comfortable">
<v-list>
<v-list-item
v-for="item in props.items"
:prepend-icon="item.icon"
:title="item.title"
:subtitle="item.desc"
:href="item.url"
target="_blank"
/>
</v-list>
</v-card>
</template>
<script setup lang="ts">
const props = defineProps<{ items: [{ title: string, icon: string, desc: string, url: string }] }>()
const { t } = useI18n()
</script>

View File

@ -0,0 +1,24 @@
<template>
<div class="my-2">
<div v-if="status == 'pending'">{{ t("loading") }}</div>
<post-item v-else class="no-margin-post" :post="post" :force-show-content="props.forceShowContent" />
</div>
</template>
<script setup lang="ts">
import PostItem from "~/components/PostItem.vue"
const props = defineProps<{ id: number, forceShowContent?: boolean }>()
const { t } = useI18n()
const config = useRuntimeConfig()
const { status, data: post } = await useFetch<any>(`${config.public.solarNetworkApi}/cgi/co/posts/${props.id}`)
</script>
<style scoped>
.no-margin-post {
margin: 0 !important;
}
</style>

View File

@ -0,0 +1,182 @@
<template>
<v-expand-transition>
<v-alert v-if="error" variant="tonal" type="error" class="text-xs mb-3">
{{ t("errorOccurred", [error]) }}
</v-alert>
</v-expand-transition>
<v-data-table-server
density="compact"
:headers="dataDefinitions.stickers"
:items="stickers"
:items-length="pagination.stickers.total"
:loading="reverting.stickers"
v-model:items-per-page="pagination.stickers.pageSize"
@update:options="readStickers"
item-value="id"
>
<template v-slot:item="{ item }: { item: any }">
<tr>
<td>{{ item.id }}</td>
<td>
<div class="item-texture-cell">
<v-img
cover
aspect-ratio="1"
width="28"
height="28"
color="grey-lighten-2"
rounded="sm"
:src="`${config.public.solarNetworkApi}/cgi/uc/attachments/${item.attachment.rid}`"
>
<template #placeholder>
<div class="d-flex align-center justify-center fill-height">
<v-progress-circular
size="x-small"
width="3"
color="grey-lighten-4"
indeterminate
></v-progress-circular>
</div>
</template>
</v-img>
<v-code class="px-2 w-fit font-mono">
{{ item.attachment.rid }}
</v-code>
</div>
</td>
<td>{{ item.name }}</td>
<td>{{ props.packPrefix + item.alias }}</td>
<td>
<v-btn
v-bind="props"
variant="text"
size="x-small"
color="warning"
icon="mdi-pencil"
class="ms-[-8px]"
:to="`/creator/stickers/${item.pack_id}/${item.id}/edit`"
/>
<v-dialog max-width="480">
<template #activator="{ props }">
<v-btn
v-bind="props"
variant="text"
size="x-small"
color="error"
icon="mdi-delete"
:disabled="submitting"
/>
</template>
<template v-slot:default="{ isActive }">
<v-card :title="`Delete sticker #${item.id}?`">
<v-card-text>
This action will delete this sticker, all content used it will no longer show your sticker.
But the attachment will still exists.
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
text="Cancel"
color="grey"
@click="isActive.value = false"
></v-btn>
<v-btn
text="Delete"
color="error"
@click="() => { deleteSticker(item); isActive.value = false }"
/>
</v-card-actions>
</v-card>
</template>
</v-dialog>
</td>
</tr>
</template>
</v-data-table-server>
</template>
<script setup lang="ts">
import { solarFetch } from "~/utils/request"
const config = useRuntimeConfig()
const { t } = useI18n()
const props = defineProps<{ packId: number, packPrefix?: string }>()
const error = ref<null | string>(null)
const dataDefinitions: { [id: string]: any[] } = {
stickers: [
{ align: "start", key: "id", title: "ID" },
{ align: "start", key: "attachment", title: "Texture" },
{ align: "start", key: "name", title: "Name" },
{ align: "start", key: "alias", title: "Alias" },
{ align: "start", key: "actions", title: "Actions", sortable: false },
],
}
const stickers = ref<any>([])
const reverting = reactive({ stickers: false })
const pagination = reactive({
stickers: { page: 1, pageSize: 5, total: 0 },
})
async function readStickers({ page, itemsPerPage }: { page?: number; itemsPerPage?: number }) {
if (itemsPerPage) pagination.stickers.pageSize = itemsPerPage
if (page) pagination.stickers.page = page
reverting.stickers = true
const res = await solarFetch(
"/cgi/uc/stickers?" +
new URLSearchParams({
pack: props.packId.toString(),
take: pagination.stickers.pageSize.toString(),
offset: ((pagination.stickers.page - 1) * pagination.stickers.pageSize).toString(),
}),
)
if (res.status !== 200) {
error.value = await res.text()
} else {
const data = await res.json()
stickers.value = data["data"]
pagination.stickers.total = data["count"]
}
reverting.stickers = false
}
onMounted(() => readStickers({}))
const submitting = ref(false)
async function deleteSticker(item: any) {
submitting.value = true
const res = await solarFetch(`/cgi/uc/stickers/${item.id}`, {
method: "DELETE",
})
if (res.status !== 200) {
error.value = await res.text()
} else {
await readStickers({})
}
submitting.value = false
}
</script>
<style scoped>
.item-texture-cell {
display: grid;
grid-template-columns: 28px auto;
gap: 6px;
width: fit-content;
}
</style>

View File

@ -0,0 +1,83 @@
<template>
<v-dialog max-width="640">
<template v-slot:activator="{ props }">
<slot name="activator" v-bind="{ props }" />
</template>
<template v-slot:default="{ isActive }">
<v-card title="Create Bot Key" :subtitle="`for bot @${props.item.name}`">
<v-form @submit.prevent="(evt) => { submit(evt).then(() => isActive.value = false) }">
<v-card-text class="pt-0 px-5">
<v-expand-transition>
<v-alert v-if="error" variant="tonal" type="error" class="text-xs mb-5">
{{ t("errorOccurred", [error]) }}
</v-alert>
</v-expand-transition>
<v-row>
<v-col cols="12" md="6">
<v-text-field label="Name" name="name" variant="outlined" hide-details />
</v-col>
<v-col cols="12" md="6">
<v-textarea auto-grow rows="1" label="Description" name="description" variant="outlined" hide-details />
</v-col>
<v-col cols="12">
<v-text-field type="number" label="Lifecycle" name="lifecycle" variant="outlined"
hint="How long will this key last (in seconds)" clearable persistent-hint />
</v-col>
</v-row>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn
text="Cancel"
color="grey"
@click="isActive.value = false"
/>
<v-btn
text="Create"
type="submit"
/>
</v-card-actions>
</v-form>
</v-card>
</template>
</v-dialog>
</template>
<script setup lang="ts">
const props = defineProps<{ item: any }>()
const emits = defineEmits(["completed"])
const { t } = useI18n()
const error = ref<null | string>(null)
const submitting = ref(false)
async function submit(evt: SubmitEvent) {
const data: any = Object.fromEntries(new FormData(evt.target as HTMLFormElement).entries())
if (!data.name) return
data.lifecycle = parseInt(data.lifecycle)
if (Number.isNaN(data.lifecycle)) delete data.lifecycle
submitting.value = true
const res = await solarFetch(`/cgi/id/dev/bots/${props.item.id}/keys`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
})
if (res.status != 200) {
error.value = await res.text()
throw new Error(error.value)
} else {
emits("completed")
}
submitting.value = false
}
</script>

View File

@ -0,0 +1,157 @@
<template>
<v-dialog max-width="640">
<template v-slot:activator="{ props }">
<slot name="activator" v-bind="{ props }" />
</template>
<v-card title="Bot Keys" :subtitle="`of bot @${props.item.name}`">
<v-card-text class="pb-0 pt-0">
<v-card variant="outlined">
<v-data-table-server
density="default"
:headers="dataDefinitions.keys"
:items="keys"
:items-length="pagination.keys.total"
:loading="reverting.keys"
v-model:items-per-page="pagination.keys.pageSize"
@update:options="readKeys"
item-value="id"
class="overflow-y-auto text-no-wrap"
>
<template v-slot:item="{ item }: { item: any }">
<tr>
<td>{{ item.id }}</td>
<td>
<p>{{ item.name }}</p>
<p class="text-xs">{{ item.description }}</p>
</td>
<td>{{ new Date(item.created_at).toLocaleString() }}</td>
<td>
<dev-bot-token-grant :item="item">
<template #activator="{ props }">
<v-btn
v-bind="props"
variant="text"
size="x-small"
color="info"
icon="mdi-key-variant"
/>
</template>
</dev-bot-token-grant>
<v-dialog max-width="480">
<template #activator="{ props }">
<v-btn
v-bind="props"
variant="text"
size="x-small"
color="error"
icon="mdi-delete"
:disabled="submitting"
/>
</template>
<template v-slot:default="{ isActive }">
<v-card :title="`Delete token ${item.name}?`">
<v-card-text>
This action will delete the token and invalid it immediately.
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
text="Cancel"
color="grey"
@click="isActive.value = false"
></v-btn>
<v-btn
text="Delete"
color="error"
@click="() => { revokeKey(item); isActive.value = false }"
/>
</v-card-actions>
</v-card>
</template>
</v-dialog>
</td>
</tr>
</template>
</v-data-table-server>
</v-card>
</v-card-text>
<div class="flex justify-end px-5.5 py-5">
<dev-bot-token-create :item="props.item" @completed="readKeys({})">
<template #activator="{ props }">
<v-btn variant="flat" text="Create" append-icon="mdi-plus" v-bind="props" />
</template>
</dev-bot-token-create>
</div>
</v-card>
</v-dialog>
</template>
<script setup lang="ts">
import { solarFetch } from "~/utils/request"
const props = defineProps<{ item: any }>()
const keys = ref<any[]>([])
const error = ref<null | string>(null)
const dataDefinitions: { [id: string]: any[] } = {
keys: [
{ align: "start", key: "id", title: "ID" },
{ align: "start", key: "name", title: "Name" },
{ align: "start", key: "created_at", title: "Created At" },
{ align: "start", key: "actions", title: "Actions", sortable: false },
],
}
const reverting = reactive({ keys: false })
const pagination = reactive({
keys: { page: 1, pageSize: 5, total: 0 },
})
async function readKeys({ page, itemsPerPage }: { page?: number; itemsPerPage?: number }) {
if (itemsPerPage) pagination.keys.pageSize = itemsPerPage
if (page) pagination.keys.page = page
reverting.keys = true
const res = await solarFetch(
`/cgi/id/dev/bots/${props.item.id}/keys?` +
new URLSearchParams({
take: pagination.keys.pageSize.toString(),
offset: ((pagination.keys.page - 1) * pagination.keys.pageSize).toString(),
}),
)
if (res.status !== 200) {
error.value = await res.text()
} else {
const data = await res.json()
keys.value = data["data"]
pagination.keys.total = data["count"]
}
reverting.keys = false
}
onMounted(() => readKeys({}))
async function revokeKey(item: any) {
submitting.value = true
const res = await solarFetch(`/cgi/id/dev/bots/${item.account_id}/keys/${item.id}`, {
method: "DELETE",
})
if (res.status !== 200) {
error.value = await res.text()
} else {
await readKeys({ page: 1 })
}
submitting.value = false
}
const submitting = ref(false)
</script>

View File

@ -0,0 +1,103 @@
<template>
<v-dialog max-width="640">
<template v-slot:activator="{ props }">
<slot name="activator" v-bind="{ props }" />
</template>
<v-card title="Bot Key" :subtitle="`#${props.item.id.toString().padStart(8, '0')}`">
<v-card-text>
<v-row>
<v-col cols="6">
<div class="flex justify-between items-center">
<span>Granted</span>
<v-icon :icon="getIcon(props.item.ticket.last_grant_at != null)" size="16" />
</div>
</v-col>
<v-col cols="6">
<div class="flex justify-between items-center">
<span>Lifecycle</span>
<span class="font-mono">{{ props.item.lifecycle ?? "-" }}</span>
</div>
</v-col>
</v-row>
<v-expand-transition>
<v-alert v-if="error" variant="tonal" type="error" class="text-xs mt-5">
{{ t("errorOccurred", [error]) }}
</v-alert>
</v-expand-transition>
<v-expand-transition>
<div v-if="token" class="flex flex-col gap-2 mt-5">
<div>
<p class="mb-0.25">Access Token</p>
<v-code class="font-mono px-3 mx-[-4px] overflow-y-auto text-no-wrap">
{{ token.access_token }}
</v-code>
</div>
<div>
<p class="mb-0.25">Refresh Token</p>
<v-code class="font-mono px-3 mx-[-4px] overflow-y-auto text-no-wrap">
{{ token.refresh_token }}
</v-code>
</div>
</div>
</v-expand-transition>
</v-card-text>
<div class="flex justify-end px-5.5 py-5">
<v-btn variant="tonal" text="Roll / Grant" append-icon="mdi-refresh" :loading="submitting" @click="getToken" />
</div>
</v-card>
</v-dialog>
</template>
<script setup lang="ts">
const { t } = useI18n()
const props = defineProps<{ item: any }>()
const error = ref<null | string>(null)
const token = ref<null | { access_token: string, refresh_token: string }>(null)
const submitting = ref(false)
function getIcon(value: boolean): string {
if (value) return "mdi-check"
else return "mdi-close"
}
async function getToken() {
submitting.value = true
let code = props.item.ticket.grant_token
if (props.item.ticket.last_grant_at != null) {
const res = await solarFetch(`/cgi/id/dev/bots/${props.item.account_id}/keys/${props.item.id}/roll`, {
method: "POST",
})
if (res.status != 200) {
error.value = await res.text()
submitting.value = false
return
} else {
code = (await res.json()).ticket.grant_token
}
}
const res = await solarFetch("/cgi/id/auth/token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
grant_type: "grant_token",
code: code,
}),
})
if (res.status != 200) {
error.value = await res.text()
} else {
token.value = await res.json()
}
submitting.value = false
}
</script>

View File

@ -0,0 +1,14 @@
<template>
<v-list-item :active="route.hash.replace('#', '') == link.id" :to="{ hash: '#'+link.id }">
<template #prepend>
<v-icon icon="mdi-menu-right" :style="`padding-left: ${props.padding ?? 0}rem`" />
</template>
{{ link.text }}
</v-list-item>
</template>
<script setup lang="ts">
const props = defineProps<{ link: any, padding?: number }>()
const route = useRoute()
</script>

View File

@ -0,0 +1,11 @@
<template>
<template v-for="link in links">
<docs-table-of-content-link :link="link" :padding="props.padding" />
<table-of-content-links v-if="link.children" :links="link.children" :padding="(props.padding ?? 0) + 2" />
</template>
</template>
<script setup lang="ts">
const props = defineProps<{ links: any[], padding?: number }>()
</script>

View File

@ -0,0 +1,13 @@
<template>
<v-list density="compact" nav color="primary">
<template v-for="link in links">
<docs-table-of-content-link :link="link" :padding="props.padding" />
<docs-table-of-content-links v-if="link.children" :links="link.children" :padding="(props.padding ?? 0) + 2" />
</template>
</v-list>
</template>
<script setup lang="ts">
const props = defineProps<{ links: any[], padding?: number }>()
</script>

View File

@ -0,0 +1,47 @@
<template>
<v-carousel show-arrows="hover" cycle hide-delimiters>
<v-carousel-item v-for="(item, i) in props.products" :key="i" :src="item?.thumbnail" cover>
<v-sheet color="rgba(0, 0, 0, .4)" height="calc(100% + 24px)" class="p-5">
<v-row class="fill-height" align="center" justify="center">
<v-col class="text-center">
<h1 class="text-4xl font-bold text-white" :class="item?.archived ? 'line-through' : null">
{{ item?.title }}
</h1>
<p class="text-lg text-white">{{ item?.description }}</p>
<div class="flex justify-center mt-3">
<v-btn variant="text" color="white" prepend-icon="mdi-school" :text="t('learnMore')" :to="item._path" />
<v-btn
v-if="item?.url"
variant="text"
color="white"
prepend-icon="mdi-launch"
:text="t('open')"
:href="item?.url"
target="_blank"
/>
</div>
<div class="flex justify-center">
<v-chip
v-if="item?.archived"
label
prepend-icon="mdi-archive"
variant="text"
color="warning"
size="small"
>
{{ t("productArchived") }}
</v-chip>
</div>
</v-col>
</v-row>
</v-sheet>
</v-carousel-item>
</v-carousel>
</template>
<script setup lang="ts">
const props = defineProps<{ products: any[] }>()
const { t } = useI18n()
</script>

17
content/en/docs/index.mdx Normal file
View File

@ -0,0 +1,17 @@
---
icon: mdi-airplane-landing
title: Welcome to Landing
description: Welcome to Solsynth's Knowledge Base - The Solar Archive
---
![Solar Archive Thumbnail](/thumbnails/docs/solar-archive-thumbnail.webp)
Welcome to the Solsynth Archive!
The Solsynth Archive, also known as the Solar Archive, is the largest known product database of Solsynth LLC. It is operated by Solsynth LLC and community-driven, with content and resources provided by the community, while being officially monitored and corrected.
The archive is still under construction, but in the future, you will be able to access all our materials here.
You can contribute to our documentation by forking our [Capital](https://git.solsynth.dev/Goatworks/Capital) repository and submitting PRs to modify the files under `content/<lang>/docs`. Contributions are welcome, whether its adding new content or correcting inaccuracies.
*P.S. You can use Solarpass for one-click login to the Solsynth Code Repository.*

View File

@ -0,0 +1,23 @@
---
icon: mdi-web
title: Solar Network
description: The Next-Generation Social Network by Solsynth LLC
---
![Solar Archive Thumbnail](/thumbnails/docs/solar-network-user-manual.webp)
Solar Network is a social network developed by Solsynth LLC, aiming to become the next-generation social network.
## Tech Stack
The Solar Network project follows the classic frontend-backend separation architecture, with the entire project divided into two parts: the frontend (Solian) and the backend (Hydrogen.Dealer, Hydrogen.Passport, etc.).
### Frontend
The frontend of Solar Network is a cross-platform client built with Flutter. For more details, check the [related page](solar-network/solian).
### Backend
The backend of Solar Network follows what we define as a "mid-service" architecture. Compared to microservices, each individual service is responsible for more tasks and is larger in scope, allowing us to maintain multiple projects more easily. At the same time, issues with a single service won't cause an overall outage.
At the center of it all is our core service — Hydrogen.Dealer, which serves as both the service discovery system and mid-service gateway. It is also the only external interface for Solar Network, with `api.sn.solsynth.dev` being the gateway exposed by Hydrogen.Dealer.

View File

@ -0,0 +1,9 @@
---
icon: mdi-palette
title: Creator Program
description: Welcome to the Solar Network Creator Program, Let's Co-create Solar Network
---
The Creator Program is an initiative by Solar Network designed to encourage users to create content. The goal of the program is to help creators use Solar Network to produce higher-quality content.
Join the Creator Program to receive more official support, get early access to new features, and provide valuable feedback to shape the future of Solar Network!

View File

@ -0,0 +1,38 @@
---
icon: mdi-sticker-emoji
title: Stickers and Sticker Packs
description: Stickers, Emotes, and Emojis
---
Stickers help users express their emotions better on Solar Network. This article introduces how to upload and use a sticker.
## Sticker Packs
Stickers must be part of a sticker pack. To create a sticker pack, go to the "Creator Hub" > "Stickers" section on the website sidebar.
## Stickers
Before creating a sticker, youll need to prepare the content. It is recommended to use a PNG or GIF image that is 1024x1024 pixels (minimum 128x128). The background can be transparent or not, but avoid solid color fills. Large white fills may cause discomfort for users in dark mode, akin to a flashbang.
Once the sticker pack is created, open it and select the plus symbol under "Actions" to add a sticker.
You might need to upload an attachment for the sticker material. After uploading, fill in the "Attachment" field with the Random ID (the series of characters after the #) from the completed upload. If the content displays correctly, the connection is successful.
After that, simply fill out the form to finish.
## Usage
To use a sticker, type a placeholder in your content, formatted as `:<pack prefix><sticker alias>:`.
For example, if a sticker pack has the prefix `solar` and a sticker alias `Hello`, the resulting placeholder would be `:solarHello:`.
Dont worry if that seems confusing—most of the time, we provide automatic suggestions. Simply type a colon in the Solian text box, followed by part of the placeholder, and suggestions will appear.
### Size Variations
In Solar Network, you may notice stickers appear in different sizes due to Smart Resize. The rules are as follows:
1. If only one sticker is present, it will appear at 128x128.
2. If three or fewer stickers are present, they will appear at 32x32.
3. If more than three stickers are present, or if stickers are embedded within text, they will appear at 20x20.
These adjustments are applied within a single paragraph.

View File

@ -0,0 +1,10 @@
---
icon: mdi-oci
title: Open Program
description: Welcome to the Solar Network Open Program, Let Us Help Your Application Grow
---
The Open Program is a collection of developer-friendly APIs and tools from Solar Network.
We adhere to principles that aim not to burden developers — no unnecessary encryption of parameters, no parameter obfuscation, and user-friendly API interfaces. We provide RESTful API endpoints to ensure the best possible developer experience.
Start exploring now and see what you can do with Solar Network!

View File

@ -0,0 +1,71 @@
---
icon: mdi-pencil-ruler
title: API Standards
description: The guidelines we follow when designing Solar Network service APIs
---
This article covers the paradigms we follow when designing Solar Network APIs, helping you better interact with our APIs for secondary development.
## Minimization
Our APIs aim to be minimalistic. Unlike some major platforms, where the response includes not only data but also a bunch of status codes, messages, and request IDs, we keep such information in the HTTP headers. The HTTP response body contains only the raw data, with no extra information (for paginated endpoints, an additional field for total count will be included).
## CRUD Operations
Our APIs generally follow RESTful design patterns. If you're unfamiliar with RESTful principles, heres how we practice it:
### Request Methods
- `GET` for fetching data
- `POST` for creating or performing some operations
- `PUT` for updating (though in RESTful principles it's also defined for creation, we dont use it that way)
- `PATCH` for updating (rarely used)
- `DELETE` for removing data
### Path Mapping
If you use `POST` to create data at an endpoint, using `GET` on the same endpoint will typically list the data.
Appending `/<id>` to the path will fetch a specific data entry. Switching the request method to `PUT` updates the entry, and using `DELETE` removes it.
If additional actions are needed, append paths after `/<id>`, usually for operations handled via `POST`.
Heres an example of path mapping for posts:
*Note: `:id` is a path parameter.*
- `GET /posts` - Retrieves a list of posts (paginated)
- `GET /posts/:id` - Retrieves a specific post
- `GET /posts/:id/replies` - Retrieves replies for a specific post (paginated)
- `POST /posts` - ~~Creates a post~~ (removed in the new version due to post types; use the specific post type creation endpoint)
- `PUT /posts/:id` - ~~Updates a post~~ (removed in the new version due to post types; use the specific post type update endpoint)
- `DELETE /posts/:id` - Deletes a post
- `POST /posts/:id/pin` - Pins a post
- `POST /posts/:id/react` - Reacts to a post
## Error Handling
We dont understand why, despite HTTP providing a complete set of status codes, other large companies still create their own. For HTTP status codes, heres a summary of common meanings:
- `500` - Internal Server Error — No need to worry; just file an issue if it happens frequently.
- `400` - Bad Request — Check the documentation and request body.
- `404` - Data not found or incorrect API path.
- `403` - Forbidden — You dont have permission.
- `401` - Unauthorized — API token required but not provided.
- `200` - Success
- `204` - No Content — Common for delete operations (though often forgotten during API development).
If the response status is not `2xx`, we usually return a `plain/text` response instead of `application/json`, providing a simple line of text indicating the error.
> If youre not good at English, dont keep asking us about errors — use a translator! Why else would we write error messages?
## Super Gateway
The Super Gateway refers to our [Hydrogen.Dealer](https://git.solsynth.dev/Hydrogen/Dealer). In most cases, you wont directly access our services; requests are forwarded through the Dealer gateway. Were not even sure why we created this.
Our API base URL is `api.sn.solsynth.dev`. How do you use it? Its simple. Access `/cgi/<service name>`, and this path will be forwarded to the corresponding services `/api` endpoint. In the latest version, we also introduced aliases for these services, making the URLs more readable.
- `/cgi/id` or `/cgi/auth` — Authentication service [Hydrogen.Passport](https://git.solsynth.dev/Hydrogen/Passport)
- `/cgi/uc` or `/cgi/files` — Attachment service [Hydrogen.Paperclip](https://git.solsynth.dev/Hydrogen/Paperclip)
- `/cgi/co` or `/cgi/interactive` — Post service [Hydrogen.Interactive](https://git.solsynth.dev/Hydrogen/Interactive)
- `/cgi/im` or `/cgi/messaging` — Messaging service [Hydrogen.Messaging](https://git.solsynth.dev/Hydrogen/Messaging)
> Fun fact: You might have noticed that the new aliases are actually the subdomains used before we had the Super Gateway.

View File

@ -0,0 +1,104 @@
---
icon: mdi-open-in-app
title: Solian Chain
description: Solian is the official cross-platform client developed by Solsynth LLC.
---
Solian is the cross-platform Solar Network client built with Flutter, and currently, its our only frontend.
# Usage
To use Solian, you can either download the client or open it directly in your browser. Thanks to Flutters cross-platform support, you can access the web version of Solian at https://lian.solsynth.dev. However, due to browser limitations, some features may be missing or affected.
## Download
There are many ways to download Solsynth, but make sure to download from officially certified channels.
1. The official release version from the repository: https://git.solsynth.dev/Hydrogen/Solian/releases
2. The test version from the official file storage: https://files.solsynth.dev/production01/solian
3. Official TestFlight (iOS and some macOS): https://testflight.apple.com/join/YJ0lmN6O
The Windows version is a portable version. You can place it in a directory you're familiar with and run it directly.
The web version also supports PWA (Progressive Web Application), which can replace some desktop usage.
## Installation
Below are the technical instructions for installing Solian on different platforms.
### Android
It is recommended to download the latest test version from the **file storage**. It has the latest fixes and is the most stable. ~~The test version is more stable than the stable version.~~
You can open and install the downloaded APK file directly. For Chinese phones, additional steps may be required for verification, but please avoid searching for and downloading from built-in app stores.
### iOS/macOS
Use TestFlight for installation. First, click the link above to download the TestFlight app. Then, click "Start Testing" in the second step of the link to join the test.
TestFlight has a limited number of testing slots. Once the time is right, we will release Solian on the App Store (non-China region), where you can search and download it.
### Windows
After downloading from any trusted source, extract it to a directory, and you can start using it.
**Note:** It seems that, due to a potential Flutter support issue, the Windows version often freezes for a while during the first startup before displaying the main window. Please be patient and avoid clicking repeatedly, as it may take 5 to 30 seconds. Repeated clicking may open multiple windows.
### Linux
Please build it yourself. I believe you can do it — good luck!
## Build It Yourself
### Preparing the Environment
Building Solian requires the Flutter SDK. Please download the latest version from the official site. Alternatively, you can download it from a China mirror.
After installing Flutter, follow the official documentation to install other platform-specific dependencies (e.g., Windows requires VS2022, Android requires Android Studio, and for iOS/macOS, its better to use the official pre-built version).
In addition to installing the Flutter SDK, we need Rust for system-level dependencies. Please download the latest version from the official Rust site.
Now that we have Flutter and Rust, we need one more thing — SQLite3, to support local databases for chat and future modules.
For Linux, you need to install the corresponding SQLite3 development dependencies:
```sh
# for ubuntu
sudo apt-get -y install libsqlite3-0 libsqlite3-dev
```
For Windows, download the
[sqlite3.dll](https://github.com/tekartik/sqflite/raw/master/sqflite_common_ffi/lib/src/windows/sqlite3.dll)
and place it in the running directory.
No additional steps are needed for macOS or mobile builds.
### Building the Code
Next, its time to build the code. Ensure that you have `git` installed on your build machine. Alternatively, you can download the code as a compressed archive.
Once `git` is installed, use the following command to clone the code:
```sh
git clone https://git.solsynth.dev/Hydrogen/Solian.git
```
Navigate to the corresponding directory and install dependencies using the following command:
```sh
flutter pub get
```
This will download dependencies from [pub.dev](https://pub.dev), hosted by Google. Connectivity within mainland China might be questionable. Refer to mirror sites for solutions.
Once the dependencies are installed, you can proceed with the build. Just one line of code:
```sh
# for windows
flutter build windows
# for macos
flutter build macos
# for linux
flutter build linux
# for ios
flutter build ipa
# for android
flutter build apk
```
You can also build other formats for Android, such as `aab`, but please prepare the necessary signing materials yourself.

View File

@ -0,0 +1,27 @@
---
thumbnail: /thumbnails/products/acefield.webp
title: AceField
description: An experimental multiplayer top-down view shooting game that created by Solsynth LLC affiliation Highland Entertainment.
author: [littlesheep]
url: https://files.solsynth.dev/production01/acefield
downloads:
- title: macOS
icon: mdi-apple
desc: macOS 12 or above
url: https://files.solsynth.dev/production01/acefield/AceField_MacOS_arm64.dmg
- title: Windows
icon: mdi-microsoft-windows
desc: Windows 7 or above
url: https://files.solsynth.dev/production01/acefield/Windows_x86_64.zip
- title: Linux
icon: mdi-linux
desc: Any linux distro
url: https://files.solsynth.dev/production01/acefield/Linux_x86_64.zip
---
AceField which is stands for wonderful place to battle.
We can't just use the name Battlefield because it already became a trademark of Electronic Arts.
:embed-download-link{:items='downloads'}
:embed-post-item{id=914}

View File

@ -0,0 +1,29 @@
---
thumbnail: /thumbnails/products/dietary-guard.webp
title: DietaryGuard
description: A simple app that help you keep dietary, so not die.
author: [littlesheep]
downloads:
- title: iOS
icon: mdi-apple-ios
desc: iOS 10 or above, via Testflight
url: https://testflight.apple.com/join/pYb6wRbr
- title: Android
icon: mdi-android
desc: Android 9 or above
url: https://files.solsynth.dev/production01/dietary-guard/app-arm64-v8a-release.apk
---
A simple app to look up the ingredients of your food,
with a little something along the way to keep you from eating something you shouldn't eat and going to the hospital.
## Highlight
1. Get authoritative and accurate food nutritional data via USDA FoodData Central API.
2. Customize the alert rules to know at a glance what food is not suitable for you to eat.
3. Simple and easy to use
4. Lightweight software, less than 8M.
:embed-download-link{:items='downloads'}
:embed-post-item{id=887}

View File

@ -0,0 +1,55 @@
---
thumbnail: /thumbnails/products/rhythm-box.webp
title: RhythmBox
description: Yet another Spotify third-party client.
author: [littlesheep]
downloads:
- title: macOS
icon: mdi-apple-ios
desc: iOS 12 or above, via sideload
url: https://files.solsynth.dev/production01/rhythm-box/ios-ipa.ipa
- title: macOS
icon: mdi-apple
desc: macOS 12 or above. Due to codesign issue, please compile for your self.
url: https://git.solsynth.dev/LittleSheep/RhythmBox
- title: Windows
icon: mdi-microsoft-windows
desc: Built by GitHub Actions
url: https://github.com/Solsynth/RhythmBox
- title: Android
icon: mdi-android
desc: Android 9 or above
url: https://files.solsynth.dev/production01/rhythm-box/app-arm64-v8a-release.apk
---
Another Spotify third-party client. Multi-platform support, as it is built using Flutter.
The project is inspired and supported by [spotube](https://spotube.krtirtho.dev).
Their original app is good enough. But I just want to redesign the UI and make it ready to add more features and more backend support.
## Highlights
Compared to the original spotube. The project adds more audio sources, such as Netease Cloud Music, Kugou, and provides the ability to use it in mainland China.
The project also focuses on the playback experience of VOCALOID songs.
We improved the search and ranking algorithms so that queries will select fewer cover versions in favor of the original.
Due to the termination of jiosaavn's service in Asia (other regions may also be affected). We removed the jiosaavn audio source.
## Roadmap
Seet at [GitHub](https://github.com/Solsynth/RhythmBox) or [Solsynth Git Repository](https://git.solsynth.dev/LittleSheep/RhythmBox)
## License
This project is open source under the APGLv3 license. The original spotube project is open source under the BSD-Clause4 license, copyright Kingkor Roy Tirtho.
All rights to this project are owned by LittleSheep and Solsynth LLC.
## Download
**Note: The Windows version is built via Github Actions. To download, go to the GitHub repository at the link below, find the checkmark next to the most recent commit, and select the `Details` item in the `build-exe` pop-up window. Expand the step-by-step log for Archive production artifacts, which will contain a download link to unzip it. You will need to be logged into your GitHub account to download.**
:embed-download-link{:items='downloads'}
:embed-post-item{id=923}

View File

@ -0,0 +1,48 @@
---
thumbnail: /thumbnails/products/roadsign.webp
title: RoadSign
description: The HTTP server that powered us. Great ability, and easy to use
author: [littlesheep]
---
RoadSign is an HTTP server developed by Solsynth LLC.
Its support for HTTP protocol is not excellent, but it is definitely handy for accelerating your project deployment!
It even made us abandon Netlify and Vercel.
## Highlight Features
- RoadSign CLI deploys projects with one line of command
- Full control over your traffic
- Featured Transformer to modify requests
- Built-in Warden thread management
## Installation
It is recommended to use docker for installation. The following is an example docker-compose.yml
```yaml
services:
roadsign:
image: xsheep2010/roadsign:delta
restart: unless-stopped
ports:
- 8000:8000
- 81:81
volumes:
- "/srv/roadsign/config:/config"
- "/srv/roadsign/workdir:/workdir"
- "/srv/roadsign/settings.toml:/settings.toml"
```
It is recommended to have RoadSign behind a real reverse proxy, so do not listen to 443 and 80 here, use 8000 to let the reverse proxy do the upstream.
Port 81 is the management API port that the side-loading API needs to use, which can be changed in the settings.
It is also recommended to install RoadSign CLI on your local machine
```sh
$ npm i -g roadsign-cli
```
## 使用
Watch the full RoadSign CLI deployment project demo at Asciiema 👉 https://asciinema.org/a/678744

View File

@ -0,0 +1,39 @@
---
thumbnail: /thumbnails/products/solar-network.webp
title: Solar Network
description: Next Generation Network Center
author: [littlesheep]
url: https://sn.solsynth.dev
downloads:
- title: macOS
icon: mdi-apple
desc: macOS 12 or above, via Testflight
url: https://testflight.apple.com/join/YJ0lmN6O
- title: iOS
icon: mdi-apple-ios
desc: iOS 12 or above, via Testflight
url: https://testflight.apple.com/join/YJ0lmN6O
- title: Android
icon: mdi-android
desc: Android 9 or above
url: https://files.solsynth.dev/production01/solian/app-arm64-v8a-release.apk
- title: Web
icon: mdi-web
desc: Web-based version with support for major browsers
url: https://sn.solsynth.dev
---
Solar Network is a groundbreaking, multi-functional platform that seamlessly integrates social interaction, real-time chat, and high-quality audio and video calls to create an engaging and interactive unified experience.
With Solar Network, users can not only easily create and manage their own communities, but also stay connected with friends, fans and team members anytime, anywhere.
Whether it's discussing work projects, sharing life moments, or enjoying a fun time, Solar Network provides a smooth and efficient way to communicate.
The platform is designed to meet a wide range of social needs, enabling every user to find a sense of belonging and build deep connections with others in a welcoming and diverse community.
## Support
If you need any support from us, go ahead and email [lily@solsynth.dev](mailto:lily@solsynth.dev). We're always waiting for you.
## Download
Download Solar Network official client Solian to use Solar Network.
:embed-download-link{:items='downloads'}

View File

@ -0,0 +1,50 @@
---
title: Privacy Policy
date: 2024-08-15T15:18:48.218Z
---
## Introduction
We take your privacy seriously.
This privacy policy outlines the types of personal information we collect,
how we use it, and the measures we take to protect your data.
## Information Collection
We collect personal information only when necessary to provide our services.
This may include your name, email address, and other relevant details.
## Use of Information
We use your personal information to:
- Provide and improve our services
- Communicate with you about updates or important information
- Ensure compliance with legal obligations
## Data Sharing
We do not sell, trade, or share your personal information with third parties except as required by law.
## Data Security
We implement robust security measures to protect your personal information from unauthorized access,
alteration, disclosure, or destruction.
## Your Rights
You have the right to:
- Access the personal information we hold about you
- Request corrections to your personal information
- Request the deletion of your personal information
## Contact Us
If you have any questions or concerns about this privacy policy or our data practices,
please contact us at lily@solsynth.dev.
## Changes to This Policy
We may update this privacy policy from time to time.
Any changes will be posted on this page, and we will notify you of any significant changes.

View File

@ -0,0 +1,77 @@
---
title: User Agreement
date: 2024-08-15T15:18:48.218Z
---
This Agreement applies to all Solsynth LLC products, including but not limited to Solar Network, Solian, DietaryGuard, AceField.
## Provision and Discontinuance of Service
Solsynth LLC will provide equal service to all living things in the world, including grasshoppers.
We also reserve the right to stop service to any user. We do not require prior notice for discontinuing services to some users.
## User Generated Content
Any content posted on Solar Network (including but not limited to posts, articles, attachments) grants Solsynth LLC the right to display it by default.
Unless otherwise stated by the user, all rights are reserved by the original poster, and reprints should be authorized by the original poster.
### Reproduction Recognition
Unless specifically stated by the poster, all content is subject to the definition of reprint in this section.
Republishing means uploading the content of the original post to another platform or to the Solar Network, either unchanged or with minor modifications, provided that simultaneous reposting of the post, embedded components, and links to the presentation do not constitute republishing.
Republishing also requires attribution when authorized by the original poster.
### Freedom of Speech
We do not remove user-generated content except in cases of misuse of resources. We will not ask any user to remove any content.
However, Solsynth LLC reserves the right to restrict and stop the display of content to the public that violates community guidelines (e.g., obscenity, violence, gore, anti-social, terrorist organizations, etc.).
Although you have 100% freedom of speech on Solar Network. However, please be aware that freedom of speech does not mean that you will not be held accountable for what you say.
#### Restriction and Discontinuation
- Restriction of Display: Discontinuation of related tweets, while retaining the right to access them directly through resource identifiers and sharing links.
- Cease display: stop all access to the resource by anyone other than the author.
## Resource Misuse Prevention Policy
Although there are no capacity limitations for using Solar Network's data hosting services, resources determined to be abusive will be disenfranchised from some features.
Solsynth LLC reserves the right to reclaim space on previously uploaded resources for deletion.
### Determination of Misuse
- Uploading without using: e.g. uploading excessive attachments in Solar Network's Interactive Attachment Pool and not linking them to posts.
- Meaningless Posts: meaningless shuffling or wasting of Solar Network's storage resources
- Misuse: using Solar Network's public resources as if they were your own dedicated pool (see the Wiki's Dedicated Pools page for details).
The Solsynth Trust & Safety Team is ultimately responsible for determining misuse.
## Secondary Releases
A secondary release is when our assets are downloaded and re-hosted on another site.
### Product Secondary Release
Unless otherwise stated, Solsynth LLC products are not available for secondary distribution, please do not download our product builds and upload them twice to another site.
Please do not download our product builds and upload them to other sites. **Secondary distribution for commercial use is not permitted. **.
What you should do is post a link to our product on another site. Or use the embedded component. And indicate Solsynth LLC All Rights Reserved.
If you want to build a mirror site of our products, please contact us to waive this rule.
### Secondary distribution of source code
We do not allow any form of redistribution of source code (except for Forks).
This includes, but is not limited to, mirroring code repositories on GitHub or the Solsynth Code Repository to other Git providers such as GitLab, Gitee, and so on.
**Selling source code twice is not allowed. **
For more information on source code usage regulations, please follow the open source license used by the project.
If you would like to set up a mirror of our source code, please contact us to waive this policy.
*****
Solsynth LLC reserves the right of final interpretation of this agreement.

View File

@ -0,0 +1,18 @@
---
icon: mdi-airplane-landing
title: 欢迎着陆
description: 欢迎来到 Solsynth 的知识库 —— 太阳能档案
---
![Solar Archive Thumbnail](/thumbnails/docs/solar-archive-thumbnail.webp)
欢迎来到 Solsynth 资料库!
Solsynth 资料库,又称太阳档案馆,现知的最大 Solsynth LLC 产品资料库。由 Solsynth LLC 运营。由社区提供内容及资源,官方监督改正。
我们目前还在修建档案馆,但是未来这里将可以查阅到我们的所有资料。
你可以通过 Fork 我们的 [Capital](https://git.solsynth.dev/Goatworks/Capital) 来编辑 `content/<lang>/docs` 内部的文件并提交 PR 来贡献我们的文档。
无论是新增内容、修改不正确的地方,都欢迎贡献。
*P.S. 你可以使用 Solarpass 一键登陆 Solsynth Code Repository*

View File

@ -0,0 +1,25 @@
---
icon: mdi-web
title: Solar Network
description: Solsynth LLC 的下一代社交网络
---
![Solar Archive Thumbnail](/thumbnails/docs/solar-network-user-manual.webp)
Solar Network 是 Solsynth LLC 开发的社交网络,目标成为下一代社交网络。
## 技术栈
Solar Network 项目是经典的前后端分离项目,整体项目分为两部分:前端 (Solian),后端 (Hydrogen.Dealer, Hydrogen.Passport, etc...)
### 前端
Solar Network 的前端是 Flutter 构建的全平台支持客户端,详情可以查看 [相关页面](solar-network/solian)
### 后端
Solar Network 的后端根据我们的定义是一个「中服务」架构,它相比微服务相比,单个服务负责的东西更多,体量更大,这可以帮我们更好地维护多个项目,
但同时部分服务出现问题不会造成整体 Outage
在这一切的中心是我们的核心服务 —— Hydrogen.Dealer服务发现和中服务网关同时是唯一一个 Solar Network 地外部接口,
`api.sn.solsynth.dev` 就是 Hydrogen.Dealer 暴露出来的网关

View File

@ -0,0 +1,9 @@
---
icon: mdi-palette
title: 创作者计划
description: 欢迎来到 Solar Network 创作者计划,在这里共创 Solar Network
---
创作者计划是 Solar Network 为了推动用户创作内容而诞生的一个企划,企划旨在帮助创作者更好的使用 Solar Network 产出更高质量的内容。
加入创作者计划,获得更多官方支持,第一时间使用新功能,为未来的 Solar Network 提出宝贵的建议吧!

View File

@ -0,0 +1,40 @@
---
icon: mdi-sticker-emoji
title: 贴图及贴图包
description: Stickers, Emotes 和 Emoji
---
贴图可以帮助用户通过 Solar Network 更好的表达他们的情绪,这篇文章将会向你介绍如何上传、使用一个贴图
## 贴图包
贴图必须跟随一个贴图包要创建一个贴图包你可以转到官网侧边栏的「Creator Hub」>「Stickers」来创建一个
## 贴图
贴图的创建,你首先需要准备内容。建议为一个 1024x1024 像素大小(最小为 128x128 的大小,否则可能造成效果不佳)的 PNG 或 GIF 图片,
透明背景或内容背景均可,但请不要使用纯色填充。大面积的白色填充可能会给暗色模式的用户带来闪光弹。
之后打开刚刚创建的贴图包在「Actions」中点选加号标识来创建一个贴图。
你可能需要上传一个附件来添加贴图的材质,并使用上传完成后界面的 Random ID (井号后面的一连串字符) 填写在 Attachment 字段里,
能成功显示出内容即代表连接成功
之后完成表单的填写即可。
## 使用
使用贴图,需要在你的内容中键入一个文字占位符,具体的组成为 `:<pack prefix><sticker alias>:`。
例如一个贴图包的前缀(`prefix`)为 `solar`,一个该贴图包的的贴图别名(`alias`)为 `Hello`,组成出来的占位符为 `:solarHello:`
不过不明白也别担心,大多数情况我们都有自动提示。只需在 Solian 内文本框键入一个英文冒号,再开始键入占位符的一部分即可。
### 大小变化
在 Solar Network 中,你可能看到大小不同的贴图,这是因为 Smart Resize 在背后发功。具体规则如下。
1. 当仅有一个贴图,将使用 128x128 的大小
2. 当有三个及一下的题图,将使用 32x32 的大小
3. 当有三个以上的贴图,或贴图夹杂在文本内时,将使用 20x20 的大小
这一系列的匹配都将发生在一个段落中。

View File

@ -0,0 +1,10 @@
---
icon: mdi-oci
title: 开放计划
description: 欢迎来到 Solar Network 开放计划,让我们助力你的应用成长
---
开放计划是 Solar Network 一系列的开发者友好的 API 和小工具集合。
我们坚持不为开发者添堵的规则,非必要不加密参数,永不混淆参数,人性化 API 接口RESTful API 端点,尽可能的让开发者体验好。
现在就开始浏览,看看你能用 Solar Network 做些什么吧!

View File

@ -0,0 +1,70 @@
---
icon: mdi-pencil-ruler
title: API 标准
description: 在设计 Solar Network 服务 API 时惯用的准则
---
这篇文章是关于我们平时在设计 Solar Network API 时的范式是怎样的,能够帮助你更好的调用我们的 API 来进行第二次开发
## 最小化
我们的 API 一般追求极简,不像某些大平台的 API 一样除了数据之外的格式还有一大堆什么状态码、信息、请求 ID 什么的。这些信息我们都选择放在 HTTP 的 Header 部分。HTTP 的响应体就是纯粹的数据,无其他信息(需要分页的数据接口会额外返回一个数据总数)。
## 增删查改
我们的 API 基本上都是遵循 RESTful 设计范式的,如果你不知道什么是 RESTful可以看以下我们理解的实践的 RESTful
### 请求方法
- `GET` 查询
- `POST` 创建、进行某种操作
- `PUT` 更新(虽在 RESTful 中也被定义为创建数据的行为,但是我们不使用)
- `PATCH` 更新(不常用)
- `DELETE` 删除
### 路径映射
假如你 POST 了一个地址来创建数据,那么用 GET 方法访问相同的地址大概就是列出数据的列表。
在其后面加上 `/<id>` 就是单独读取某个数据,将请求方法改成 PUT 便是更新该条数据,改成 DELETE 就是删除该条数据。
如果在 `/<id>` 再加上东西基本上就是 POST 方法来执行某个操作。
例如以下是我们帖子的路径映射
*注:`:id` 系路径参数*
- `GET /posts` 获取帖子列表(分页)
- `GET /posts/:id` 获取单个帖子
- `GET /posts/:id/replies` 获取单个帖子的回复(分页)
- `POST /posts` ~~创建帖子~~(于新版本因为引入帖子类型移除,需使用对应类型的创建接口)
- `PUT /posts/:id` ~~更新帖子~~(于新版本因为引入帖子类型移除,需使用对应类型的更新接口)
- `DELETE /posts/:id` 删除帖子
- `POST /posts/:id/pin` 置顶帖子
- `POST /posts/:id/react` 对帖子作出反应
## 错误处理
我们不理解为什么 HTTP 有给一套完善的状态码系统,其他大厂却仍选择自立门户。关于响应的 HTTP 状态码,以下是一些常用的含义代表。
- `500` 服务器内部错误 —— 你不用管,如果多见记得抛 issue
- `400` 请求参数错误 —— 看文档,核查请求体
- `404` 数据不存在或是接口路径不对
- `403` 没有权限
- `401` 需要授权 —— 需要授权的 API 但你没有提供 API 令牌
- `200` 成功
- `204` 无内容 —— 常见于删除 *虽然后时候写 API 会忘记删除内容时改成这个*
如果响应不是 `2xx` 的状态码,一般我们都不会返回 `application/json` 的数据,而是一个 `plain/text`,一行简单的文字来代表你犯了什么错。
> 如果你是英语白痴,遇到报错别老来问我们,用用翻译好吗?不然我们写报错信息干嘛。
## 超级网关
超级网关指的是我们的 [Hydrogen.Dealer](https://git.solsynth.dev/Hydrogen/Dealer),一般情况下你都不会直接访问我们的服务,都是走 Dealer 的网关转发的。虽然我们也不知道为什么写了个这个东西。
我们 API 的地址为 `api.sn.solsynth.dev`,怎么用呢?很简单。访问 `/cgi/<service name>` 即可,这样的地址会被转发到对应服务的 `/api` 端点。新版本我们还给这些服务加了点别名,这样你的 URL 可以变得更好看点。
- `/cgi/id` 或 `/cgi/auth` —— 授权服务 [Hydrogen.Passport](https://git.solsynth.dev/Hydrogen/Passport)
- `/cgi/uc` 或 `/cgi/files` —— 附件服务 [Hydrogen.Paperclip](https://git.solsynth.dev/Hydrogen/Paperclip)
- `/cgi/co` 或 `/cgi/interactive` —— 帖子服务 [Hydrogen.Interactive](https://git.solsynth.dev/Hydrogen/Interactive)
- `/cgi/im` 或 `/cgi/messaging` —— 聊天服务 [Hydrogen.Messaging](https://git.solsynth.dev/Hydrogen/Messaging)
> 冷知识:你可能注意到了我们新配置的别名其实就是之前没有超级网关时他们使用的子域名。

View File

@ -0,0 +1,106 @@
---
icon: mdi-open-in-app
title: Solian 索链
description: Solian 是由 Solsynth LLC 官方编写的全平台支持客户端。
---
Solian 是由 Flutter 编写的全平台 Solar Network 客户端,也是我们目前唯一的前端。
# 使用
想要使用 Solian你可以下载客户端也可以直接在浏览器中打开。得益于 Flutter 的全平台支持,你可以在 https://lian.solsynth.dev 访问到 Solian 网页版。但由于浏览器限制,部分功能可能欠缺或受到影响。
## 下载
下载 Solsynth 的方式很多,但一定请从官方认证的渠道下载。
1. 官方仓库发布的正式版本 https://git.solsynth.dev/Hydrogen/Solian/releases
2. 官方文件托管柜发布的测试版本 https://files.solsynth.dev/production01/solian
3. 官方 TestFlight (iOS 与少量 macOS) https://testflight.apple.com/join/YJ0lmN6O
Windows 版本系免安装版本,放置于一个您熟悉的目录即可使用。
Web 版本同时支持 PWA 渐进式网页应用,可以替代一部分桌面端使用。
## 安装
以下是个平台安装 Solian 的技术要领。
### Android
推荐从**文件托管柜**下载最新测试版,版本最新,修复最全,最稳定。~~测试版比稳定版稳定~~
下载下来的 APK 档案可以直接打开安装。中国版手机可能需要额外步骤验证,但请不要使用自带应用商店搜索下载。
### iOS/macOS
使用 TestFlight 安装。可以点击上方链接首先下载安装 TestFlight App。再点击上方链接的第二部开始测试来参加测试。
TestFlight 的测试名额有限,等到时机成熟我们会将 Solian 发布于非中国区的 App Store可以前往 App Store 搜索下载。
### Windows
Windows 从任意可信渠道下载后解压到一个目录即可使用。
**注意:** Windows 版本不知是否属于 Flutter 的支持问题,在第一次启动加载时总是会卡好一会才弹出主窗口。不用反复点击,请耐心等待,可能会使用
5 到 30 秒。如果多次点击可能会打开多个窗口。
### Linux
请自行构建。我相信你们可以的,加油哦~
## 自行构建
### 环境准备
构建 Solian 需要使用 Flutter SDK请在官网下载最新版安装。也可以从中国镜像站下载安装。
安装完成 Flutter 请根据官方文档下载其他对应平台需要的开发依赖,例如 Windows 需要 VS2022、Android 需要 Android
Studio、iOS/macOS 我劝你还是用官方版本构建的吧。
除开安装 Flutter SDK我们还需要使用 Rust 做系统级依赖支持。请从 Rust 官方下载最新版本。
现在我们有了 Flutter、Rust还少一个东西为了实现聊天及未来的其他模块本地数据库支持。
Linux 版本还需要安装对应的 SQLite3 开发依赖。
```sh
# for ubuntu
sudo apt-get -y install libsqlite3-0 libsqlite3-dev
```
Windows 需要下载
[sqlite3.dll](https://github.com/tekartik/sqflite/raw/master/sqflite_common_ffi/lib/src/windows/sqlite3.dll)
放置在运行目录。
macOS 及手机端构建不需要其他操作。
### 构建代码
之后就是构建代码的时候了。确保你在构建机器上安装了 `git` 版本管理工具。或者你想直接下载代码压缩档案也不是不行。
确保 `git` 安装之后可以使用以下命令克隆代码。
```sh
git clone https://git.solsynth.dev/Hydrogen/Solian.git
```
之后导航到对应目录,使用以下命令安装依赖。
```sh
flutter pub get
```
该操作会从 [pub.dev](https://pub.dev) 上下载依赖,而 pub.dev 是由 Google 托管提供。所以中国大陆的连接性要被打个问号。具体可以参考中国大陆镜像站点查询解决方案。
完成依赖获取后就可编译了,一行命令就搞定。
```sh
# for windows
flutter build windows
# for macos
flutter build macos
# for linux
flutter build linux
# for ios
flutter build ipa
# for android
flutter build apk
```
你也可以为 Android 平台构建 `aab` 等其他格式的应用包。但是对应签名素材请自行准备。

View File

@ -0,0 +1,27 @@
---
thumbnail: /thumbnails/products/acefield.webp
title: AceField
description: 由索尔辛茨附属高岛互娱制作的一款实验性多人自上而下视角射击游戏。
author: [littlesheep]
url: https://files.solsynth.dev/production01/acefield
downloads:
- title: macOS
icon: mdi-apple
desc: macOS 12 或以上
url: https://files.solsynth.dev/production01/acefield/AceField_MacOS_arm64.dmg
- title: Windows
icon: mdi-microsoft-windows
desc: Windows 7 或以上
url: https://files.solsynth.dev/production01/acefield/Windows_x86_64.zip
- title: Linux
icon: mdi-linux
desc: 主流 Linux 发行版
url: https://files.solsynth.dev/production01/acefield/Linux_x86_64.zip
---
AceField 是 “战斗的好地方” 的缩写。
我们不能只用《战地》这个名字,因为它是 Electronic Arts 的商标。
:embed-download-link{:items='downloads'}
:embed-post-item{id=914}

View File

@ -0,0 +1,28 @@
---
thumbnail: /thumbnails/products/dietary-guard.webp
title: 膳食管家
description: 一个简单的查询食物成份的应用程式
author: [littlesheep]
downloads:
- title: iOS
icon: mdi-apple-ios
desc: iOS 10 或以上,通过 Testflight
url: https://testflight.apple.com/join/pYb6wRbr
- title: Android
icon: mdi-android
desc: Android 9 或以上
url: https://files.solsynth.dev/production01/dietary-guard/app-arm64-v8a-release.apk
---
一个简单的查询食物成份的应用,还能顺便提示你一点东西,避免你吃到什么不该吃的吃进医院。
## 高光
1. 透过 USDA FoodData Central API 获取权威、准确的食品营养成分数据
2. 自定义告警规则,不适合自己食用的食物一目了然
3. 功能简单,上手容易
4. 轻量软件,只有 8M 不到
:embed-download-link{:items='downloads'}
:embed-post-item{id=887}

View File

@ -0,0 +1,55 @@
---
thumbnail: /thumbnails/products/rhythm-box.webp
title: RhythmBox
description: 又一个 Spotify 第三方客户端,但不止步于此。
author: [littlesheep]
downloads:
- title: macOS
icon: mdi-apple-ios
desc: iOS 12 或以上,通过侧载自行安装
url: https://files.solsynth.dev/production01/rhythm-box/ios-ipa.ipa
- title: macOS
icon: mdi-apple
desc: macOS 12 或以上,因为签名问题,请自行编译使用
url: https://git.solsynth.dev/LittleSheep/RhythmBox
- title: Windows
icon: mdi-microsoft-windows
desc: 通过 GitHub Actions 构建
url: https://github.com/Solsynth/RhythmBox
- title: Android
icon: mdi-android
desc: Android 9 或以上
url: https://files.solsynth.dev/production01/rhythm-box/app-arm64-v8a-release.apk
---
又一个 Spotify 第三方客户端。支持多平台,因为使用 Flutter 构建。
该项目受到 [spotube](https://spotube.krtirtho.dev) 的启发和支持。
他们的原始应用程序已经足够好了。但我只想重新设计用户界面,并使其准备好添加更多功能和更多后端支持。
## 亮点
与原始 spotube 相比。该项目添加了更多音频源,例如网易云音乐、酷狗,并提供在中国大陆使用的能力。
同时,该项目还专注于 VOCALOID 歌曲的播放体验。
我们改进了搜索和排名算法,使查询将减少选择翻唱版本,而是选择原始版本。
由于 jiosaavn 在亚洲地区的终止服务(其他地区可能也受到影响)。我们删除了 jiosaavn 音频源。
## 开发计划
在 [GitHub](https://github.com/Solsynth/RhythmBox) 或 [Solsynth Git Repository](https://git.solsynth.dev/LittleSheep/RhythmBox) 查看
## 许可
本项目在 APGLv3 许可证下开源。原始 spotube 项目在 BSD-Clause4 许可证下开源,版权归 Kingkor Roy Tirtho 所有。
本项目的所有权利归 LittleSheep 和 Solsynth LLC 所有。
## 下载
**注Windows 版本通过 Github Actions 编译,下载请前往下述链接的 GitHub 仓库找到最近提交旁「打勾」的图标,在弹出的提示窗口选择 `build-exe` 那项的 「Details」。展开 Archive production artifacts 的步骤日志,里面会包含一个下载链接,解压服用即可。下载需要登陆 GitHub 账号。**
:embed-download-link{:items='downloads'}
:embed-post-item{id=923}

View File

@ -0,0 +1,47 @@
---
thumbnail: /thumbnails/products/roadsign.webp
title: RoadSign
description: 为我们的网络提供动力的 HTTP 服务器。功能强大,使用方便
author: [littlesheep]
---
RoadSign 是由 Solsynth LLC 开发的 HTTP 服务器,其对 HTTP 协议的支持算不上优秀,
但是对于加速你的项目部署,一定算得上趁手!甚至让我们抛弃了 Netlify 和 Vercel。
## 特色
- RoadSign CLI 一行命令部署项目
- 完全控制你的流量
- 特色的 Transformer 来修改请求
- 内置 Warden 线程管理
## 安装
推荐使用 docker 进行安装,以下是示例 docker-compose.yml
```yaml
services:
roadsign:
image: xsheep2010/roadsign:delta
restart: unless-stopped
ports:
- 8000:8000
- 81:81
volumes:
- "/srv/roadsign/config:/config"
- "/srv/roadsign/workdir:/workdir"
- "/srv/roadsign/settings.toml:/settings.toml"
```
推荐让 RoadSign 在一个真正的反向代理后,所以在此不监听 443 和 80使用 8000 让反向代理做上流。
其中 81 端口是侧载 API 需要使用的管理 API 端口,可以在设置内修改。
同时推荐在本地机器上安装 RoadSign CLI
```sh
$ npm i -g roadsign-cli
```
## 使用
在 Asciiema 观看完整的 RoadSign CLI 部署项目演示 👉 https://asciinema.org/a/678744

View File

@ -0,0 +1,39 @@
---
thumbnail: /thumbnails/products/solar-network.webp
title: Solar Network
description: 下一代网络中心
author: littlesheep
url: https://sn.solsynth.dev
downloads:
- title: macOS
icon: mdi-apple
desc: macOS 12 或以上,通过 TestFlight
url: https://testflight.apple.com/join/YJ0lmN6O
- title: iOS
icon: mdi-apple-ios
desc: iOS 12 或以上,通过 Testflight
url: https://testflight.apple.com/join/YJ0lmN6O
- title: Android
icon: mdi-android
desc: Android 9 或以上
url: https://files.solsynth.dev/production01/solian/app-arm64-v8a-release.apk
- title: Web
icon: mdi-web
desc: 基于 Web 的版本,支持主流浏览器
url: https://sn.solsynth.dev
---
Solar Network 是一个开创性的多功能平台,它将社交互动、实时聊天和高质量音视频通话无缝集成,打造出一个极具吸引力和互动性的统一体验。
通过 Solar Network用户不仅可以轻松创建和管理自己的社区还能够随时随地与好友、粉丝和团队成员保持紧密联系。
无论是讨论工作项目、分享生活点滴还是享受娱乐时光Solar Network 都能为您提供顺畅、高效的沟通桥梁。
这个平台旨在满足各种社交需求,使每一位用户都能在一个温馨而多样化的社区中找到归属感,并与他人建立深厚的联系。
## 支持
如果你需要任何形式的支持或者有任何疑问,欢迎写邮件至 [lily@solsynth.dev](mailto:lily@solsynth.dev)。我们将竭诚为你服务。
## 下载
下载 Solar Network 官方客户端 Solian 来使用 Solar Network。
:embed-download-link{:items='downloads'}

View File

@ -0,0 +1,44 @@
---
title: 隐私策略
date: 2024-08-15T15:18:48.218Z
---
## 简介
我们非常重视您的隐私。本隐私政策概述了我们收集的个人信息类型、使用方式以及我们采取的保护措施。
## 信息收集
我们仅在提供服务时收集必要的个人信息。这可能包括您的姓名、电子邮件地址以及其他相关信息。
## 信息使用
我们使用您的个人信息来:
- 提供和改进我们的服务
- 与您沟通更新或重要信息
- 确保遵守法律义务
## 数据共享
我们不会出售、交易或与第三方分享您的个人信息,法律要求除外。
## 数据安全
我们实施了强有力的安全措施,以保护您的个人信息免受未经授权的访问、更改、披露或销毁。
## 您的权利
您有权:
- 访问我们持有的关于您的个人信息
- 请求更正您的个人信息
- 请求删除您的个人信息
## 联系我们
如果您对本隐私政策或我们的数据处理方式有任何疑问或顾虑,请通过[您的联系方式]与我们联系。
## 政策变更
我们可能会不时更新本隐私政策。任何更改将发布在此页面上,且我们会通知您任何重大更改。

View File

@ -0,0 +1,77 @@
---
title: 用户协议
date: 2024-08-15T15:18:48.218Z
---
本协议适用于所有 Solsynth LLC 的产品,包括但不限于 Solar Network、Solian、DietaryGuard、AceField。
## 服务的提供与中断
Solsynth LLC 将向世界上所有的生物提供同等的服务,包括草履虫。
同时也保留向任意用户停止提供服务的权利。关于停止部分用户的服务,我们不需要提前通知。
## 用户生成内容
任意发布在 Solar Network 上的内容(包括但不限于帖子、文章、附件)都默认授权 Solsynth LLC 予以展示的权利。
除非用户特别声明,所有内容均为原帖主保留所有权利,转载请先向原帖主授权。
### 转载的认定
无帖主特别声明,所有内容均适用本条转载的定义。
转载指将原帖的内容原封不动或略作改动上传到别的平台或 Solar Network。但同时转帖、嵌入式组件与展示展开的链接不构成转载。
转载即时在原帖主授权的情况下也需表明出处。
### 言论的自由
除滥用资源的情况,我们不会将用户生成内容进行删除。也不会做出要求任何用户删除任何内容的要求。
但 Solsynth LLC 始终保留对于违反社区准则的内容(如淫秽、暴力、血腥、反社会、恐怖组织等)限制与停止向公众展示的权利。
尽管在 Solar Network 上你拥有 100% 的言论自由。但还请清楚,言论自由不代表不用对自己的言论负责。
#### 限制展示与停止展示
- 限制展示:停止相关的推送,但是任保留直接通过资源标识符和分享连接访问的权利
- 停止展示:全面停止除作者之外任何人访问该资源的权利
## 防止资源滥用条例
尽管使用 Solar Network 的数据托管服务并无任何的容量限制,但经过判定的滥用资源将会被取消使用部分功能的权利。
并且之前上传的资源 Solsynth LLC 有权对其进行删除空间回收。
### 滥用的认定
- 传而不用:例如在 Solar Network 的 Interactive 附件池中过度上传附件并不将附件与帖子连接
- 无意义帖:无意义洗版或浪费 Solar Network 的存储资源
- 走错片场:将 Solar Network 公有资源当作自己的专用资源池使用(详见维基《专用资源池》页面)
滥用的认定最终解释权归属于 Solsynth Trust & Safety Team
## 二次发布
二次发布指将我们的资产下载并重新托管到别站。
### 制品二次发布
除特殊声明Solsynth LLC 的产品均不允许二次发布,请勿将我们的产品构建下载并二次上传于其他站点。
**二次作为商用发布更是不允许的。**
你应该做的是将我们的产品链接贴上他站。或使用嵌入式组件。并且表明 Solsynth LLC 版权所有。
若您想搭建我们制品的镜像站,请与我们取得联系以豁免此条例。
### 源码二次发布
我们不允许任何形式的源码二次发布Fork 除外)。
包括但不限于,将 GitHub 或 Solsynth Code Repository 上的代码仓库镜像于 GitLab、Gitee 等其他 Git 提供者。
**二次售卖源码更是不允许的。**
关于更多的源码使用条例,请遵循项目使用的开源许可证。
若您想搭建我们源码的镜像站,请与我们取得联系以豁免此条例。
*****
Solsynth LLC 保留对此协议的最终解释权

View File

@ -1,36 +0,0 @@
import js from '@eslint/js';
import ts from 'typescript-eslint';
import svelte from 'eslint-plugin-svelte';
import prettier from 'eslint-config-prettier';
import globals from 'globals';
/** @type {import('eslint').Linter.FlatConfig[]} */
export default [
js.configs.recommended,
...ts.configs.recommended,
...svelte.configs['flat/recommended'],
prettier,
...svelte.configs['flat/prettier'],
{
languageOptions: {
globals: {
...globals.browser,
...globals.node
}
}
},
{
files: ['**/*.svelte'],
languageOptions: {
parserOptions: {
parser: ts.parser
}
},
rules: {
'css-unused-selector': 'off',
},
},
{
ignores: ['build/', '.svelte-kit/', 'dist/']
},
];

View File

@ -0,0 +1,20 @@
import fs from "fs"
const tones = ["↑", "→", "↓", "↗", "↘"]
const raw = fs.readFileSync("../lang/zh-CN.json", "utf-8")
const original: { [id: string]: string } = JSON.parse(raw)
const result: { [id: string]: string } = {}
for (const key in original) {
let str = ""
for (const char of original[key]) {
const tone = tones[Math.floor(Math.random() * tones.length)]
str += "咩" + tone
}
result[key] = str
}
fs.writeFileSync("../lang/ml-SG.json", JSON.stringify(result))

3
i18n.config.ts Normal file
View File

@ -0,0 +1,3 @@
export default defineI18nConfig(() => ({
fallbackLocale: 'en',
}))

80
lang/en-US.json Normal file
View File

@ -0,0 +1,80 @@
{
"brandName": "Solsynth",
"brandNameFormal": "Solsynth LLC",
"navProducts": "Products",
"navActivity": "Activity",
"navActivityCaption": "Explore our official recent activities.",
"navGallery": "Gallery",
"navGalleryCaption": "Explore files that uploaded by Solar Network users.",
"navPosts": "Posts",
"navPostsCaption": "Explore posts that posted by Solar Network users.",
"navPostsCaptionWithTag": "Explore posts with tag {0}.",
"navPostsCaptionWithCategory": "Explore posts in category {0}.",
"navPostsCaptionWithPublisher": "Explore posts posted by {0}.",
"indexIntroduce": "An energetic software company that create wonderful software which everyone love.",
"indexProductListHint": "See some of our products just there",
"indexActivities": "Activities",
"indexActivitiesCaption": "Keep in touch,\nand learn what we doing recently.",
"indexActivitiesHint": "See some posts in our realm just here",
"userMenuDashboard": "Dashboard",
"userMenuSignOut": "Sign Out",
"userMenuSignIn": "Sign In",
"userMenuSignUp": "Create Account",
"next": "Next",
"errorOccurred": " Something went wrong... {0}",
"username": "Name",
"nickname": "Nick",
"email": "Email Address",
"password": "Password",
"copyright": "Copyright",
"signUpTitle": "Create an account",
"signUpCaption": "Create an account on Solar Network. Then enjoy all our services.",
"signUpCompleted": "You successfully created an account on Solar Network. Now sign in to your account and start exploring!",
"signUpCompletedAction": "Let's go!",
"signInTitle": "Sign In",
"signInCaption": "Sign in via your account to access the entire Solar Network.",
"multiFactorCaption": "We need to verify that the person trying to access your account is you.",
"multiFactorHint": "Check your inbox",
"multiFactorTypeEmail": "Email One-time-password",
"multiFactorTypePassword": "Password",
"signInCompleted": "All Done",
"signInCompletedCaption": "Welcome back! You just signed in right now! We're going to direct you to dashboard...",
"authorizeTitle": "Connect to third-party",
"authorizeCaption": "One Solar Network account, entire Internet.",
"authorizeErrorHint": "It's usually not our fault. Try bringing this link to give feedback to the developer of the app you came from.",
"authorizeRedirectHint": "After approve their request, you will be redirect to",
"authorizeCompleted": "Authorized",
"authorizeCompletedCaption": "You're done! We successfully established connection between you and {0}.",
"authorizeCompletedRedirect": "Now you can continue your their app, we will redirect you soon.",
"authorizeCompletedRedirectHint": "Teleporting you to...",
"decline": "Decline",
"approve": "Approve",
"transferredToSolianHint": "This part of the functionality has been transferred to our application Solian, please download it or open it in your browser. To learn more, please visit the project description page.",
"personalize": "Personalize",
"personalizeCaption": "Bring your own color to the Solar Network.",
"security": "Security",
"securityCaption": "Guard your Solar Network account.",
"userActivity": "Activity",
"userActivityCaption": "Recent posts of this user.",
"productArchived": "Archived",
"callbackHint": "You need to sign in before access that page. After you signed in, you will be redirected to:",
"lastUpdatedAt": "Last updated at {0}",
"learnMore": "Learn more",
"open": "Open",
"openInSite": "Open in Website",
"postReplies": "Replies",
"postRepliesCaption": "All replies of this post",
"language": "Language",
"embedWidget": "Solar Network Embed Widget",
"continueReading": "Continue Reading",
"download": "Download",
"downloadDescription": "Pick the right version for you",
"attachmentUpload": "Upload new",
"attachmentCreate": "Create Attachment",
"attachmentCreateCaption": "Use Solar Network host your files",
"attachmentUploadProgress": "Uploading",
"attachmentUploadCompleted": "Uploaded",
"upload": "Upload",
"cancel": "Cancel",
"seeMore": "See more"
}

55
lang/tb-SG.json Normal file
View File

@ -0,0 +1,55 @@
{
"brandName": "咩→咩↗咩↘咩↑",
"brandNameFormal": "咩↓咩↓咩→咩↘咩↗咩↗咩↘咩↗咩↓咩↘",
"navProducts": "咩↑咩→",
"navActivity": "咩→咩↑",
"navActivityCaption": "咩→咩↑咩↑咩↑咩↗咩↑咩↗咩↘咩↗咩↑咩→咩↘",
"navGallery": "咩→咩↑",
"navGalleryCaption": "咩↓咩↘咩↓咩↘咩↘咩↓咩↑咩↘咩→咩↗咩↗咩↑咩↗咩↗咩↓咩↗咩→咩↑咩↘咩↑咩↓咩→咩↑咩↗",
"indexIntroduce": "咩↓咩↗咩↓咩↑咩↘咩↓咩↑咩↑咩↗咩↗咩↗咩↓咩↘咩↗咩→咩→咩↓咩↓咩↘咩→咩↓咩↓咩↑咩↑咩→",
"indexProductListHint": "咩↗咩↓咩↘咩↑咩↑咩↘咩↓咩↑咩↘咩↓咩↗咩↘",
"indexActivities": "咩↘咩↗",
"indexActivitiesCaption": "咩↑咩↘咩→咩↗咩↑咩↗咩↓咩→咩↗咩↑咩↓咩→咩↑咩↓咩↓咩↑咩→咩→咩↑咩↗咩→咩→咩↓咩→",
"indexActivitiesHint": "咩↑咩↓咩↘咩→咩→咩↓咩↘咩→咩↘咩↗咩→咩↓",
"userMenuDashboard": "咩↓咩↗咩↗",
"userMenuSignOut": "咩↑咩→",
"userMenuSignIn": "咩→咩→",
"userMenuSignUp": "咩↑咩↘咩→咩↗",
"next": "咩↑咩→咩↗",
"errorOccurred": "咩→咩↑咩↘咩→咩→咩↓咩↓咩↑咩↘咩↘",
"username": "咩→咩↓咩↑",
"nickname": "咩↘咩→咩↘",
"email": "咩↓咩↓咩↓咩↓",
"password": "咩↘咩↓",
"copyright": "咩↑咩↗咩↓咩↗",
"signUpTitle": "咩↘咩→咩↗咩↗",
"signUpCaption": "咩↑咩↓咩↘咩↓咩↑咩→咩↗咩↓咩↘咩↘咩↘咩↓咩↓咩↑咩↑咩↘咩↗咩↘咩↑咩↓咩↘咩↓咩↘咩→咩↗咩↗咩→咩↘咩↘咩↗咩↘咩→咩↑咩→咩↓",
"signUpCompleted": "咩↓咩→咩↓咩↓咩↗咩↗咩→咩↗咩↑咩↗咩→咩→咩↘咩→咩→咩↗咩↑咩↘咩↓咩↓咩↓咩↑咩↗咩↓咩↓咩↑咩↗咩↘咩→咩↓咩↘咩↓咩↗咩↘咩↓咩↗咩↗咩↓咩↑",
"signUpCompletedAction": "咩↘咩↗",
"signInTitle": "咩↑咩↘",
"signInCaption": "咩↗咩↘咩↑咩↑咩↘咩↑咩→咩↘咩→咩→咩↑咩↑咩↑咩↓咩↘咩↓咩↗咩→咩↘咩↓咩↓咩↓咩↑咩→咩↗咩↘咩↘咩→",
"multiFactorCaption": "咩→咩→咩↓咩↓咩↓咩↘咩→咩↗咩↓咩↓咩↑咩↘咩↑咩↓咩↓咩↗咩→咩↗咩↓咩↑",
"multiFactorHint": "咩↓咩→咩↓咩↑咩↑咩↑咩↑",
"multiFactorTypeEmail": "咩↘咩→咩↑咩↓咩↑咩↘咩↓咩↗咩↑",
"signInCompleted": "咩→咩↓",
"signInCompletedCaption": "咩↑咩↘咩↗咩↗咩↑咩→咩↗咩↑咩↓咩↑咩↘咩↑咩↓咩↘咩↓咩↓咩↘咩↘咩↗咩↑咩↗咩→咩↘咩↗咩↑咩→咩↓",
"transferredToSolianHint": "咩↑咩→咩↑咩↑咩→咩→咩↘咩→咩↓咩→咩↑咩↘咩↘咩↑咩↗咩↗咩↓咩→咩↑咩↓咩↓咩↗咩↗咩↑咩→咩↑咩↗咩↓咩→咩↑咩→咩↑咩↘咩↘咩↗咩↘咩↑咩↘咩↘咩↓咩↘咩↑咩↑咩↗咩↑咩↘咩→咩→咩↓咩↘咩↗咩↓咩↑咩↑咩↘",
"personalize": "咩↓咩↑咩↑",
"personalizeCaption": "咩↑咩↓咩↘咩↑咩↓咩↑咩→咩↑咩↘咩↓咩→咩↘咩↗咩→咩↑咩↗咩↑咩→咩↓咩→咩↗咩↓咩↘",
"security": "咩↗咩↓",
"securityCaption": "咩↓咩↗咩→咩→咩↘咩↑咩↘咩↗咩↓咩→咩↘咩→咩→咩↘咩→咩↓咩↗咩↗咩↑咩→咩↓咩↘",
"userActivity": "咩↗咩↗",
"userActivityCaption": "咩↑咩↑咩↗咩↓咩↑咩↗咩↗咩↗咩↓",
"productArchived": "咩↘咩↓咩→",
"callbackHint": "咩↑咩↘咩↓咩↓咩↓咩↓咩↗咩↑咩↘咩↗咩↗咩→咩↗咩↑咩↑咩↓咩↗咩↘咩↑咩↗咩↗咩↓咩↓咩↘咩↑咩↗咩↗咩→咩↓",
"authorizeTitle": "咩↑咩↑咩→咩↑咩↘咩↗",
"authorizeCaption": "咩↓咩↘咩↑咩↓咩↗咩↘咩↓咩↘咩↗咩↘咩↑咩↘咩↓咩→咩↓咩↑咩↗咩↘咩↘咩↗咩↗咩↘咩↓咩↑咩↘咩→咩↑咩↘",
"authorizeErrorHint": "咩↑咩↓咩↘咩→咩↓咩↗咩↗咩↑咩↘咩↘咩↓咩↑咩→咩↘咩↘咩↗咩↓咩↘咩↗咩↓咩↓咩↗咩↗咩↑咩→咩→咩↓咩↘咩↑咩→",
"authorizeRedirectHint": "咩↓咩↓咩→咩↑咩↓咩→咩→咩↑咩↓咩↗咩↘咩↗咩↑咩↗咩→咩↗咩↑",
"authorizeCompleted": "咩↗咩→咩↘",
"authorizeCompletedCaption": "咩↓咩↓咩↗咩↗咩↑咩↘咩↘咩↑咩↗咩↘咩↑咩→咩↓咩↑咩↘咩↗咩↗咩↑咩↘咩↗咩↘咩↘咩↑",
"authorizeCompletedRedirect": "咩→咩↘咩↑咩↓咩↗咩↓咩→咩→咩↗咩↘咩→咩↑咩↘咩→咩↘咩↘咩↓咩↘咩→咩→咩↗咩↘咩↑咩↗咩→咩↗",
"authorizeCompletedRedirectHint": "咩↑咩↑咩↑咩↘咩→咩↗",
"decline": "咩↗咩↘",
"approve": "咩→咩↓"
}

80
lang/zh-CN.json Normal file
View File

@ -0,0 +1,80 @@
{
"brandName": "索尔辛茨",
"brandNameFormal": "索尔辛茨实业有限公司",
"navProducts": "制品",
"navActivity": "动态",
"navActivityCaption": "了解我们官方近期的活动。",
"navGallery": "相簿",
"navGalleryCaption": "探索整个 Solar Network 上的文件。",
"navPosts": "帖子",
"navPostsCaption": "探索整个 Solar Network 上的帖子。",
"navPostsCaptionWithTag": "探索带有 {0} 标签的帖子",
"navPostsCaptionWithCategory": "探索被分类为 {0} 的帖子",
"navPostsCaptionWithPublisher": "探索 {0} 发表的帖子",
"indexIntroduce": "一家充满活力的软件公司,创造了人人喜爱的精彩软件。",
"indexProductListHint": "在这里看看我们的一些产品",
"indexActivities": "动态",
"indexActivitiesCaption": "开发软件,闭门造车是大忌,了解我们最近在做什么。",
"indexActivitiesHint": "看看我们领域中的一些帖子",
"userMenuDashboard": "仪表盘",
"userMenuSignOut": "登出",
"userMenuSignIn": "登陆",
"userMenuSignUp": "注册帐号",
"next": "下一步",
"errorOccurred": "发生错误了… {0}",
"username": "用户名",
"nickname": "显示名",
"email": "邮件地址",
"password": "密码",
"copyright": "版权所有",
"signUpTitle": "创建账号",
"signUpCaption": "在 Solar Network 上创建一个帐号,以享受我们所有的服务。",
"signUpCompleted": "您已成功创建 Solar Network 账户。现在登录您的账户,开始探索吧!",
"signUpCompletedAction": "出发",
"signInTitle": "登陆",
"signInCaption": "通过您的账户登录以访问整个 Solar Network。",
"multiFactorCaption": "我们需要验证试图访问您账户的人是您本人。",
"multiFactorHint": "检查您的收件箱",
"multiFactorTypeEmail": "电子邮件一次性密码",
"multiFactorTypePassword": "账号密码",
"signInCompleted": "完成",
"signInCompletedCaption": "欢迎回来!您刚刚登录成功!我们将引导您进入仪表板...",
"transferredToSolianHint": "此部分功能已转移到我们的应用程序 Solian请下载或在浏览器中打开。如需了解更多信息请访问项目描述页面。",
"personalize": "个性化",
"personalizeCaption": "为 Solar Network 染上你的色彩。",
"security": "安全",
"securityCaption": "保护您的 Solar Network 账户。",
"userActivity": "活动",
"userActivityCaption": "此用户的最新帖子。",
"productArchived": "已归档",
"callbackHint": "访问该页面前,您需要先登录。登录后,我们会将把您重定向到:",
"authorizeTitle": "连接到第三方",
"authorizeCaption": "一个 Solar Network 账户,连接整个互联网。",
"authorizeErrorHint": "通常这不是我们的错误。尝试将此链接反馈给您来源应用的开发者。",
"authorizeRedirectHint": "在批准他们的请求后,您将被重定向到",
"authorizeCompleted": "已授权",
"authorizeCompletedCaption": "完成!我们已成功在您与 {0} 之间建立连接。",
"authorizeCompletedRedirect": "现在您可以继续使用他们的应用,我们会很快将您重定向。",
"authorizeCompletedRedirectHint": "正在传送到…",
"decline": "拒绝",
"approve": "批准",
"lastUpdatedAt": "最后更新于 {0}",
"learnMore": "了解更多",
"open": "打开",
"openInSite": "在站点里打开",
"postReplies": "回复",
"postRepliesCaption": "该帖子的全部回复",
"language": "语言",
"embedWidget": "Solar Network 嵌入式组件",
"continueReading": "继续阅读",
"download": "下载",
"downloadDescription": "选择适合你的版本下载",
"attachmentUpload": "新传附件",
"attachmentCreate": "新建附件",
"attachmentCreateCaption": "使用 Solar Network 来托管你的文件",
"attachmentUploadProgress": "上传中",
"attachmentUploadCompleted": "上传完成",
"upload": "上传",
"cancel": "取消",
"seeMore": "查看更多"
}

56
layouts/creator-hub.vue Normal file
View File

@ -0,0 +1,56 @@
<template>
<v-app-bar flat color="primary" scroll-behavior="hide" scroll-threshold="800">
<v-container fluid class="mx-auto d-flex align-center justify-center px-8">
<v-tooltip>
<template #activator="{ props }">
<div @click="openDrawer = !openDrawer" v-bind="props" class="cursor-pointer">
<v-img class="me-4 ms-1" width="32" height="32" alt="Logo" :src="Logo" />
</div>
</template>
Open / close drawer
</v-tooltip>
<nuxt-link to="/dev" exact>
<h2 class="mt-1">Creator Hub</h2>
</nuxt-link>
<v-spacer></v-spacer>
<locale-select />
<user-menu />
</v-container>
</v-app-bar>
<v-navigation-drawer v-model="openDrawer" location="left" width="300" floating>
<v-list density="compact" nav color="primary">
<v-list-item title="Back" prepend-icon="mdi-arrow-left" to="/" exact />
</v-list>
<v-divider class="border-opacity-50 my-1" />
<v-list density="compact" nav color="primary">
<v-list-item title="Stickers" prepend-icon="mdi-sticker-emoji" to="/creator/stickers" exact />
</v-list>
<v-divider class="border-opacity-50 mb-4 mt-1" />
<copyright no-centered service="capital" class="px-5" />
<footer-links class="px-5 mt-3" />
</v-navigation-drawer>
<v-main>
<slot />
</v-main>
</template>
<script setup lang="ts">
import Logo from "../assets/logo-w-shadow.png"
const { t } = useI18n()
const openDrawer = ref(false)
useHead({
titleTemplate: "%s | Solsynth Creator Hub"
})
</script>

59
layouts/default.vue Normal file
View File

@ -0,0 +1,59 @@
<template>
<v-app-bar flat color="primary">
<v-container fluid class="mx-auto d-flex align-center justify-center px-8">
<v-tooltip>
<template #activator="{ props }">
<div @click="openDrawer = !openDrawer" v-bind="props" class="cursor-pointer">
<v-img class="me-4 ms-1" width="32" height="32" alt="Logo" :src="Logo" />
</div>
</template>
Open / close drawer
</v-tooltip>
<nuxt-link to="/" exact>
<h2 class="mt-1">Solsynth LLC</h2>
</nuxt-link>
<v-spacer></v-spacer>
<locale-select />
<user-menu />
</v-container>
</v-app-bar>
<v-navigation-drawer v-model="openDrawer" location="left" width="300" floating>
<v-list density="compact" nav color="primary">
<v-list-item :title="t('navProducts')" prepend-icon="mdi-shape" to="/products" exact />
<v-list-item :title="t('navPosts')" prepend-icon="mdi-note-text" to="/posts" exact />
<v-list-item :title="t('navActivity')" prepend-icon="mdi-newspaper-variant-multiple-outline" to="/activity" exact />
<v-list-item :title="t('navGallery')" prepend-icon="mdi-image-multiple" to="/gallery" exact />
</v-list>
<v-divider class="border-opacity-50 my-1" />
<v-list density="compact" nav color="primary">
<v-list-item title="Knowledge Base" prepend-icon="mdi-library" to="/docs" exact />
<v-list-item title="Developer Portal" prepend-icon="mdi-code-tags" to="/dev" exact />
<v-list-item title="Creator Hub" prepend-icon="mdi-pencil" to="/creator" exact />
</v-list>
<v-divider class="border-opacity-50 mb-4 mt-0.5" />
<copyright no-centered :service="['roadsign', 'capital']" class="px-5" />
<footer-links class="px-5 mt-3" />
</v-navigation-drawer>
<v-main>
<slot />
</v-main>
</template>
<script setup lang="ts">
import Logo from "../assets/logo-w-shadow.png"
const { t } = useI18n()
const openDrawer = ref(false)
</script>

56
layouts/dev-portal.vue Normal file
View File

@ -0,0 +1,56 @@
<template>
<v-app-bar flat color="primary" scroll-behavior="hide" scroll-threshold="800">
<v-container fluid class="mx-auto d-flex align-center justify-center px-8">
<v-tooltip>
<template #activator="{ props }">
<div @click="openDrawer = !openDrawer" v-bind="props" class="cursor-pointer">
<v-img class="me-4 ms-1" width="32" height="32" alt="Logo" :src="Logo" />
</div>
</template>
Open / close drawer
</v-tooltip>
<nuxt-link to="/dev" exact>
<h2 class="mt-1">Developer Portal</h2>
</nuxt-link>
<v-spacer></v-spacer>
<locale-select />
<user-menu />
</v-container>
</v-app-bar>
<v-navigation-drawer v-model="openDrawer" location="left" width="300" floating>
<v-list density="compact" nav color="primary">
<v-list-item title="Back" prepend-icon="mdi-arrow-left" to="/" exact />
</v-list>
<v-divider class="border-opacity-50 my-1" />
<v-list density="compact" nav color="primary">
<v-list-item title="Bots" prepend-icon="mdi-robot" to="/dev/bots" exact />
</v-list>
<v-divider class="border-opacity-50 mb-4 mt-1" />
<copyright no-centered service="capital" class="px-5" />
<footer-links class="px-5 mt-3" />
</v-navigation-drawer>
<v-main>
<slot />
</v-main>
</template>
<script setup lang="ts">
import Logo from "../assets/logo-w-shadow.png"
const { t } = useI18n()
const openDrawer = ref(false)
useHead({
titleTemplate: "%s | Solsynth Dev Portal"
})
</script>

115
layouts/docs.vue Normal file
View File

@ -0,0 +1,115 @@
<template>
<v-app-bar flat color="primary">
<v-container fluid class="mx-auto d-flex align-center justify-center pe-8">
<v-app-bar-nav-icon class="me-1" @click="openDrawer = !openDrawer" />
<nuxt-link to="/docs" exact>
<h2 class="mt-0.5">Solsynth Knowledge Base</h2>
</nuxt-link>
<v-spacer></v-spacer>
<locale-select @update="reload" />
<user-menu />
</v-container>
</v-app-bar>
<v-navigation-drawer v-model="openDrawer" location="left" width="300" floating>
<content-navigation v-slot="{ navigation }" :query="navQuery" :key="route.path">
<v-list density="compact" nav color="primary" class="mt-1">
<v-list-item
v-if="navNotRoot"
title="Previous"
prepend-icon="mdi-page-previous"
exact
@click="previousNav"
/>
<v-list-item
v-if="navigation[0]?.children"
v-for="link of fullyFlatMap(navigation[0])"
:key="link._path"
:title="link.title"
:to="link._path"
:prepend-icon="link.icon ?? 'mdi-text-box'"
exact
/>
<v-list-item v-else title="No Content" prepend-icon="mdi-close-box-outline" disabled />
</v-list>
</content-navigation>
<v-divider class="border-opacity-50 mb-4 mt-0.5" />
<copyright no-centered service="capital" class="px-5" />
<footer-links class="px-5 mt-3" />
</v-navigation-drawer>
<v-app-bar color="transparent" density="compact" class="backdrop-blur-md">
<v-app-bar-nav-icon icon="mdi-home" to="/" class="ms-4" />
<div class="flex items-center justify-center h-[60px]">
<v-breadcrumbs :items="breadcrumb" density="compact" class="px-0 mt-0.5"></v-breadcrumbs>
</div>
</v-app-bar>
<v-main>
<slot />
</v-main>
</template>
<script setup lang="ts">
const { locale } = useI18n()
const route = useRoute()
const breadcrumb = computed(() => {
const arr = route.path.replace(/^\/|\/$/g, "").split("/")
arr.shift()
return arr.map((x, idx) => ({
title: x,
to: `/docs/${arr.slice(0, idx + 1).join("/")}`,
}))
})
const navNotRoot = computed(() => route.path.split("/").length > 2)
const navQuery = ref(calcNavQuery())
function calcNavQuery(path?: string) {
return {
where: {
_path: new RegExp("^\\/" + (path ?? route.path).replace(/^\/|\/$/g, "") + "\\/[^\\/]+\\/?$"),
_locale: getLocale(locale),
},
}
}
watch(route, (value) => {
navQuery.value = calcNavQuery(value.path)
}, { immediate: true, deep: true })
function previousNav() {
const arr = route.path.split("/")
arr.pop()
navigateTo(arr.join("/"))
}
function fullyFlatMap(input: any): any[] {
const result: any[] = []
const pathSet = new Set<string>()
for (const item of input?.children ?? []) {
result.push(item)
result.push(...fullyFlatMap(item))
}
return result.filter((x) => {
if (pathSet.has(x._path)) return false
pathSet.add(x._path)
return true
})
}
const openDrawer = ref(false)
function reload() {
window.location.reload()
}
</script>

24
layouts/minimal.vue Normal file
View File

@ -0,0 +1,24 @@
<template>
<v-system-bar flat color="primary" class="px-5 flex justify-center">
<v-btn icon="mdi-arrow-left" variant="text" color="white" size="x-small" class="mt-[2px]" @click="goBack" />
<h2 class="mt-1">Solsynth LLC</h2>
<v-spacer />
</v-system-bar>
<v-main>
<slot />
</v-main>
</template>
<script setup lang="ts">
const router = useRouter()
function goBack() {
if (window.history.length > 0) {
router.go(-1)
} else {
navigateTo("/")
}
}
</script>

7
middleware/auth.ts Normal file
View File

@ -0,0 +1,7 @@
export default defineNuxtRouteMiddleware((to, from) => {
const state = useLoggedInState()
if (!state.value) {
return navigateTo(`/auth/sign-in?redirect_uri=${to.fullPath}`)
}
})

View File

@ -1,3 +0,0 @@
[build]
command = "npm run build"
publish = "build"

130
nuxt.config.ts Normal file
View File

@ -0,0 +1,130 @@
import vuetify, { transformAssetUrls } from "vite-plugin-vuetify"
export default defineNuxtConfig({
devtools: { enabled: true },
site: {
url: "https://solsynth.dev",
name: "Solsynth LLC",
},
sitemap: {
strictNuxtContentPaths: true,
cacheMaxAgeSeconds: 3600,
sitemapsPathPrefix: "/sitemap",
sitemaps: {
pages: {
includeAppSources: true,
exclude: ["/flow/**"],
defaults: { priority: 0.8 },
},
posts: {
includeAppSources: false,
sources: [
"/api/sitemap/posts",
],
},
},
},
i18n: {
strategy: "no_prefix",
detectBrowserLanguage: {
useCookie: true,
cookieCrossOrigin: true,
cookieKey: "__capital_i18n",
redirectOn: "no prefix",
},
locales: [
{ code: "en", name: "English", file: "en-US.json" },
{ code: "zh-CN", name: "简体中文", file: "zh-CN.json" },
{ code: "tb-SG", name: "音调羊文", file: "tb-SG.json" },
],
lazy: true,
langDir: "lang",
defaultLocale: "en",
},
css: ["~/assets/index.css"],
runtimeConfig: {
public: {
siteUrl: "https://solsynth.dev",
solarRealm: "solar-network",
solarNetworkApi: "https://api.sn.solsynth.dev",
solianUrl: "https://sn.solsynth.dev",
},
},
routeRules: {
"/.well-known/openid-configuration": {
proxy: "/api/well-known/openid-configuration",
},
},
app: {
pageTransition: { name: "page", mode: "out-in" },
head: {
title: "Solsynth LLC",
titleTemplate: "%s | Solsynth",
meta: [],
link: [
{ rel: "icon", type: "image/png", href: "/icon.png" },
],
},
},
content: {
api: {
baseURL: "/api/content",
},
highlight: {
theme: { default: "github-light", dark: "github-dark" },
langs: ["json", "yaml", "toml", "java", "javascript", "astro", "css", "scss", "dart", "go", "typescript", "c", "csharp",
"cpp", "bat", "bash", "sh", "dockerfile", "erlang", "fsharp", "markdown", "log",
"lua", "objc", "swift", "regex", "ruby", "rust", "postcss", "blade", "asciidoc", "cmake", "cobol", "pascal",
"nginx", "angular-html", "angular-ts", "gdscript", "gdshader", "gdresource", "groovy", "gql", "python",
"crystal", "sql", "plsql", "kotlin", "html", "vue", "gleam", "julia", "lisp", "xml", "csv"],
},
locales: ["en", "zh-CN"],
defaultLocale: "en",
},
pinia: {
storesDirs: ["./stores/**"],
},
build: {
transpile: ["vuetify"],
},
modules: [
"@unocss/nuxt",
"@nuxt/content",
"@nuxt/image",
"@nuxtjs/sitemap",
"@pinia/nuxt",
"@nuxtjs/i18n",
"nuxt-schema-org",
"nuxt-gtag",
(_options, nuxt) => {
nuxt.hooks.hook("vite:extendConfig", (config) => {
// @ts-expect-error
config.plugins.push(vuetify({ autoImport: true }))
})
},
],
gtag: {
id: "G-ZFJ7RX0JXF",
},
vite: {
vue: {
template: {
transformAssetUrls,
},
},
},
compatibilityDate: "2024-08-10",
})

View File

@ -1,59 +1,39 @@
{
"name": "capital",
"version": "0.0.1",
"name": "nuxt-app",
"private": true,
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build && npm run package",
"preview": "vite preview",
"package": "svelte-kit sync && svelte-package && publint",
"prepublishOnly": "npm run package",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --check . && eslint .",
"format": "prettier --write ."
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"svelte": "./dist/index.js"
}
},
"files": [
"dist",
"!dist/**/*.test.*",
"!dist/**/*.spec.*"
],
"peerDependencies": {
"svelte": "^4.0.0"
"dependencies": {
"@mdi/font": "^7.4.47",
"@nuxt/content": "^2.13.2",
"@nuxt/image": "^1.8.0",
"@nuxtjs/i18n": "^8.5.5",
"@nuxtjs/sitemap": "^6.1.1",
"@pinia/nuxt": "^0.5.4",
"feed": "^4.2.2",
"nuxt": "^3.13.2",
"nuxt-gtag": "^2.1.0",
"nuxt-schema-org": "^3.4.0",
"pinia": "^2.2.2",
"rehype-sanitize": "^6.0.0",
"rehype-stringify": "^10.0.1",
"remark": "^15.0.1",
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.1",
"unified": "^11.0.5",
"vue": "latest"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/adapter-netlify": "^4.2.0",
"@sveltejs/enhanced-img": "^0.2.1",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/package": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"@tailwindcss/typography": "^0.5.13",
"@types/eslint": "^8.56.7",
"autoprefixer": "^10.4.19",
"daisyui": "^4.12.2",
"eslint": "^9.0.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.36.0",
"globals": "^15.0.0",
"postcss": "^8.4.38",
"prettier": "^3.1.1",
"prettier-plugin-svelte": "^3.1.2",
"publint": "^0.1.9",
"svelte": "^4.2.7",
"svelte-check": "^3.6.0",
"tailwindcss": "^3.4.4",
"tslib": "^2.4.1",
"typescript": "^5.0.0",
"typescript-eslint": "^8.0.0-alpha.20",
"vite": "^5.0.11"
},
"svelte": "./dist/index.js",
"types": "./dist/index.d.ts",
"type": "module"
"@unocss/nuxt": "^0.61.9",
"@unocss/preset-typography": "^0.61.9",
"@unocss/reset": "^0.61.9",
"vite-plugin-vuetify": "^2.0.4",
"vuetify": "^3.7.2"
}
}

37
pages/activity/index.vue Normal file
View File

@ -0,0 +1,37 @@
<template>
<v-container class="content-container mx-auto">
<div class="my-3 mx-[1.5ch]">
<div class="flex gap-1">
<h1 class="text-2xl">{{ t("navActivity") }}</h1>
<v-btn size="x-small" variant="text" icon="mdi-rss" slim to="/activity/feed" />
</div>
<span>{{ t("navActivityCaption") }}</span>
</div>
<post-list class="mx-[-2.5ch]" :realm="config.public.solarRealm" />
</v-container>
</template>
<script setup lang="ts">
const { t } = useI18n()
useHead({
title: t("navActivity"),
})
useSeoMeta({
title: t("navActivity"),
ogTitle: t("navActivity"),
description: t("navActivityCaption"),
ogDescription: t("navActivityCaption"),
ogType: "website",
})
const config = useRuntimeConfig()
</script>
<style scoped>
.content-container {
max-width: 70ch !important;
}
</style>

154
pages/auth/authorize.vue Executable file
View File

@ -0,0 +1,154 @@
<template>
<v-container class="h-screen flex flex-col gap-3 items-center justify-center">
<v-card class="w-full max-w-[720px] overflow-auto" :loading="loading">
<v-card-text class="card-grid pa-9">
<div>
<v-avatar color="accent" icon="mdi-connection" size="large" class="card-rounded mb-2" />
<h1 class="text-2xl">{{ t("authorizeTitle") }}</h1>
<p>{{ t("authorizeCaption") }}</p>
</div>
<v-window :touch="false" :model-value="panel" class="pa-2 mx-[-0.5rem]">
<v-window-item value="confirm">
<div class="flex flex-col gap-2">
<v-expand-transition>
<v-alert v-show="error" variant="tonal" type="error" class="text-xs mb-3">
<p>{{ t("errorOccurred", [error]) }}</p>
<br />
<p class="font-bold">
{{ t("authorizeErrorHint") }}
</p>
</v-alert>
</v-expand-transition>
<div v-if="!error">
<h1 class="font-bold text-xl">{{ metadata?.name ?? "Loading" }}</h1>
<p>{{ metadata?.description ?? "Hold on a second please!" }}</p>
<div class="mt-3">
<div class="mt-5 flex justify-between">
<v-btn prepend-icon="mdi-close" variant="text" color="error" :disabled="loading" @click="decline">
{{ t("decline") }}
</v-btn>
<v-btn append-icon="mdi-check" variant="tonal" color="success" :disabled="loading" @click="approve">
{{ t("approve") }}
</v-btn>
</div>
<div class="mt-5 text-xs text-center opacity-75">
<p>{{ t("authorizeRedirectHint") }}</p>
<p class="text-mono">{{ route.query["redirect_uri"] }}</p>
</div>
</div>
</div>
</div>
</v-window-item>
<v-window-item value="callback">
<div>
<v-icon icon="mdi-fire" size="32" color="grey-darken-3" class="mb-3" />
<h1 class="font-bold text-xl">{{ t("authorizeCompleted") }}</h1>
<p>{{ t("authorizeCompletedCaption", [metadata?.name]) }}</p>
<p class="mt-3">{{ t("authorizeCompletedRedirect") }}</p>
<p class="mt-3">{{ t("authorizeCompletedRedirectHint") }}</p>
<p class="text-xs text-mono">{{ route.query["redirect_uri"] }}</p>
</div>
</v-window-item>
</v-window>
</v-card-text>
</v-card>
<copyright service="passport" />
</v-container>
</template>
<script setup lang="ts">
import { solarFetch } from "~/utils/request"
definePageMeta({
middleware: ["auth"],
})
const { t } = useI18n()
const route = useRoute()
const error = ref<string | null>(null)
const loading = ref(true)
const metadata = ref<any>(null)
const panel = ref("confirm")
async function tryAuthorize() {
loading.value = true
const res = await solarFetch(`/cgi/id/auth/o/authorize${window.location.search}`)
if (res.status !== 200) {
error.value = await res.text()
} else {
const data = await res.json()
if (data["ticket"]) {
panel.value = "callback"
callback(data["ticket"])
} else {
metadata.value = data["client"]
loading.value = false
}
}
}
onMounted(() => tryAuthorize())
function decline() {
if (window.history.length > 0) {
window.history.back()
} else {
window.close()
}
}
async function approve() {
loading.value = true
const res = await solarFetch(`/cgi/id/auth/o/authorize${window.location.search}`, {
method: "POST",
})
if (res.status !== 200) {
error.value = await res.text()
loading.value = false
} else {
const data = await res.json()
panel.value = "callback"
setTimeout(() => callback(data["ticket"]), 1850)
}
}
function callback(ticket: any) {
const url = `${route.query["redirect_uri"]}?code=${ticket["grant_token"]}&state=${route.query["state"]}`
window.open(url, "_self")
}
</script>
<style scoped>
.card-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
@media (max-width: 768px) {
.card-grid {
grid-template-columns: 1fr;
}
}
.card-rounded {
border-radius: 8px;
}
</style>

112
pages/auth/sign-in.vue Normal file
View File

@ -0,0 +1,112 @@
<template>
<v-container class="h-[calc(100vh-80px)] flex flex-col gap-3 items-center justify-center">
<auth-callback-hint />
<v-card class="w-full max-w-[720px] overflow-auto" :loading="loading">
<v-card-text class="card-grid pa-9">
<div>
<v-avatar color="accent" icon="mdi-login-variant" size="large" class="card-rounded mb-2" />
<h1 class="text-2xl">{{ t("signInTitle") }}</h1>
<p v-if="ticket" class="max-w-5/6">{{ t("multiFactorCaption") }}</p>
<p v-else class="max-w-5/6">{{ t("signInCaption") }}</p>
</div>
<v-window :touch="false" :model-value="panel" class="pa-2 mx-[-0.5rem]">
<v-window-item v-for="(k, idx) in Object.keys(panels)" :key="idx" :value="k">
<component
:is="panels[k]"
@swap="(val: string) => (panel = val)"
v-model:loading="loading"
v-model:currentFactor="currentFactor"
v-model:ticket="ticket"
/>
</v-window-item>
</v-window>
</v-card-text>
</v-card>
<copyright service="passport" />
</v-container>
</template>
<script setup lang="ts">
import { type Component, onMounted, ref } from "vue"
import FactorPicker from "~/components/auth/FactorPicker.vue"
import FactorApplicator from "~/components/auth/FactorApplicator.vue"
import AccountAuthenticate from "~/components/auth/Authenticate.vue"
import AuthenticateCompleted from "~/components/auth/AuthenticateCompleted.vue"
const { t } = useI18n()
useHead({
title: t("signInTitle"),
})
const route = useRoute()
const loading = ref(false)
const currentFactor = ref<any>(null)
const ticket = ref<any>(null)
async function pickUpTicket() {
if (route.query["ticketId"]) {
loading.value = true
const res = await fetch(`/api/auth/tickets/${route.query["ticketId"]}`)
if (res.status == 200) {
ticket.value = await res.json()
if (ticket.value["available_at"] != null) panel.value = "completed"
else panel.value = "mfa"
}
loading.value = false
}
}
onMounted(() => pickUpTicket())
const id = useUserinfo()
const router = useRouter()
watch(
id,
(value) => {
if (value.isLoggedIn) {
if (route.query["close"]) {
window.close()
} else if (route.query["redirect_uri"]) {
window.open((route.query["redirect_uri"] as string) ?? "/", "_self")
} else {
router.push("/users/me")
}
}
},
{ deep: true, immediate: true },
)
const panel = ref("authenticate")
const panels: { [id: string]: Component } = {
authenticate: AccountAuthenticate,
mfa: FactorPicker,
applicator: FactorApplicator,
completed: AuthenticateCompleted,
}
</script>
<style scoped>
.card-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
@media (max-width: 768px) {
.card-grid {
grid-template-columns: 1fr;
}
}
.card-rounded {
border-radius: 8px;
}
</style>

166
pages/auth/sign-up.vue Executable file
View File

@ -0,0 +1,166 @@
<template>
<v-container class="h-[calc(100vh-80px)] flex flex-col gap-3 items-center justify-center">
<auth-callback-hint />
<v-card class="w-full max-w-[720px] overflow-auto" :loading="loading">
<v-card-text class="card-grid pa-9">
<div>
<v-avatar color="accent" icon="mdi-login-variant" size="large" class="card-rounded mb-2" />
<h1 class="text-2xl">{{ t("signUpTitle") }}</h1>
<p class="max-w-5/6">{{ t("signUpCaption") }}</p>
</div>
<div class="flex items-center">
<v-form class="flex-grow-1" @submit.prevent="submit">
<v-row dense class="mb-3">
<v-col :cols="6">
<v-text-field
hide-details
:label="t('username')"
autocomplete="username"
variant="solo"
density="comfortable"
v-model="data.name"
/>
</v-col>
<v-col :cols="6">
<v-text-field
hide-details
:label="t('nickname')"
autocomplete="nickname"
variant="solo"
density="comfortable"
v-model="data.nick"
/>
</v-col>
<v-col :cols="12">
<v-text-field
hide-details
:label="t('email')"
type="email"
variant="solo"
density="comfortable"
v-model="data.email"
/>
</v-col>
<v-col :cols="12">
<v-text-field
hide-details
:label="t('password')"
type="password"
autocomplete="new-password"
variant="solo"
density="comfortable"
v-model="data.password"
/>
</v-col>
</v-row>
<v-expand-transition>
<v-alert v-show="error" variant="tonal" type="error" class="text-xs mb-3">
{{ t("errorOccurred", [error]) }}
</v-alert>
</v-expand-transition>
<div class="flex justify-between">
<v-btn type="button" variant="plain" color="grey-darken-3" to="/auth/sign-in">
{{ t("userMenuSignIn") }}
</v-btn>
<v-btn type="submit" variant="text" color="primary" append-icon="mdi-arrow-right" :disabled="loading">
{{ t("next") }}
</v-btn>
</div>
</v-form>
</div>
</v-card-text>
</v-card>
<v-dialog v-model="done" class="max-w-[560px]">
<v-card title="Congratulations">
<template #text>
{{ t("signUpCompleted") }}
</template>
<template #actions>
<div class="flex flex-grow-1 justify-end">
<v-btn @click="callback">{{ t("signUpCompletedAction") }}</v-btn>
</div>
</template>
</v-card>
</v-dialog>
<copyright service="passport" />
</v-container>
</template>
<script setup lang="ts">
import { ref } from "vue"
import { useRoute, useRouter } from "vue-router"
const error = ref<string | null>(null)
const { t } = useI18n()
const config = useRuntimeConfig()
useHead({
title: t('signUpTitle'),
})
const route = useRoute()
const router = useRouter()
const done = ref(false)
const loading = ref(false)
const data = ref({
name: "",
nick: "",
email: "",
password: "",
})
async function submit() {
const payload = data.value
if (!payload.name || !payload.nick || !payload.email || !payload.password) return
loading.value = true
const res = await fetch(`${config.public.solarNetworkApi}/cgi/id/users`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
})
if (res.status !== 200) {
error.value = await res.text()
} else {
done.value = true
error.value = null
}
loading.value = false
}
function callback() {
if (route.params["closable"]) {
window.close()
} else {
router.push("/auth/sign-in")
}
}
</script>
<style scoped>
.card-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
@media (max-width: 768px) {
.card-grid {
grid-template-columns: 1fr;
}
}
.card-rounded {
border-radius: 8px;
}
</style>

24
pages/creator/index.vue Normal file
View File

@ -0,0 +1,24 @@
<template>
<v-container fluid class="h-[calc(100vh-80px)] flex flex-col justify-center items-center text-center">
<v-icon icon="mdi-brush" size="64" />
<div class="text-2xl">Hello, creator!</div>
<div class="max-w-[320px]">Switch page using navigator above to get start creating contents on Solar Network.</div>
<div class="text-xs font-mono text-grey mt-5">
@{{ auth.userinfo?.name }} · {{ auth.userinfo?.id.toString().padStart(8, "0") }}
</div>
</v-container>
</template>
<script setup lang="ts">
definePageMeta({
layout: "creator-hub",
middleware: ["auth"],
})
useHead({
title: "Landing",
})
const auth = useUserinfo()
</script>

View File

@ -0,0 +1,181 @@
<template>
<v-container class="px-12">
<div class="flex justify-between items-center mt-5">
<div class="flex items-end gap-2">
<h1 class="text-2xl">Edit sticker: {{ data?.name ?? "Loading" }}</h1>
</div>
<div class="flex gap-2">
<v-btn
color="grey"
text="Cancel"
prepend-icon="mdi-arrow-left"
variant="tonal"
to="/creator/stickers"
/>
</div>
</div>
<v-expand-transition>
<v-alert v-if="error" variant="tonal" type="error" class="text-xs mt-5 mb-3">
{{ t("errorOccurred", [error]) }}
</v-alert>
</v-expand-transition>
<v-form class="mt-5" @submit.prevent="submit">
<v-row>
<v-col cols="12">
<v-card title="Pack info" prepend-icon="mdi-sticker-emoji" density="compact">
<v-card-text class="mt-2">
<p class="text-lg"><b>{{ pack?.name ?? "Loading..." }}</b></p>
<p>{{ pack?.description ?? "Please stand by..." }}</p>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" md="6">
<v-text-field
label="Name"
name="name"
variant="outlined"
persistent-hint
hint="A human friendly name for user to recognize this sticker"
v-model="stickerName"
/>
</v-col>
<v-col cols="12" md="6">
<v-text-field
label="Alias"
name="alias"
variant="outlined"
persistent-hint
hint="A placeholder of this sticker, will prepend pack's prefix"
v-model="stickerAlias"
>
<template #prepend-inner>
<p class="ms-1 me-[-5px] text-grey">{{ pack?.prefix }}</p>
</template>
</v-text-field>
</v-col>
<v-col cols="12">
<v-text-field
label="Attachment"
name="attachment_id"
variant="outlined"
persistent-hint
v-model="attachmentRid"
>
<template #details>
<p class="order-first v-messages">
The texture / image of this sticker, you can upload one from
<nuxt-link to="/gallery/new?pool=c3RpY2tlcg" target="_blank" class="underline">here</nuxt-link>
</p>
</template>
<template #prepend-inner>
<v-img
cover
aspect-ratio="1"
width="28"
height="28"
color="grey-lighten-2"
rounded="sm"
:src="attachmentRid.length > 0 ? `${config.public.solarNetworkApi}/cgi/uc/attachments/${attachmentRid}` : `example.com/not-found`"
>
<template #placeholder>
<div class="d-flex align-center justify-center fill-height" v-if="attachmentRid.length > 0">
<v-progress-circular
size="x-small"
width="3"
color="grey-lighten-4"
indeterminate
></v-progress-circular>
</div>
<div class="d-flex align-center justify-center fill-height" v-else>
<v-icon icon="mdi-image-broken-variant" class="block" size="18" />
</div>
</template>
</v-img>
</template>
</v-text-field>
</v-col>
</v-row>
<div class="flex justify-end">
<v-btn type="submit" text="Save changes" append-icon="mdi-content-save" :disabled="data == null"
:loading="submitting" />
</div>
</v-form>
</v-container>
</template>
<script setup lang="ts">
definePageMeta({
layout: "creator-hub",
middleware: ["auth"],
})
useHead({
title: "Edit Sticker",
})
const { t } = useI18n()
const route = useRoute()
const config = useRuntimeConfig()
const data = ref<any>(null)
const pack = ref<any>(null)
const attachmentRid = ref<string>("")
const stickerName = ref<string>("")
const stickerAlias = ref<string>("")
async function readPack() {
const res = await solarFetch(`/cgi/uc/stickers/packs/${route.params.id}`)
if (res.status != 200) {
error.value = await res.text()
} else {
pack.value = await res.json()
}
}
async function readSticker() {
const res = await solarFetch(`/cgi/uc/stickers/${route.params.sticker}`)
if (res.status != 200) {
error.value = await res.text()
} else {
data.value = await res.json()
stickerName.value = data.value?.name
stickerAlias.value = data.value?.alias
attachmentRid.value = data.value?.attachment.rid
}
}
onMounted(() => Promise.all([readPack(), readSticker()]))
const error = ref<null | string>(null)
const submitting = ref(false)
async function submit(evt: SubmitEvent) {
const data = Object.fromEntries(new FormData(evt.target as HTMLFormElement).entries())
if (!data.name) return
submitting.value = true
const res = await solarFetch(`/cgi/uc/stickers/${route.params.sticker}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
pack_id: parseInt(route.params.id.toString()),
...data,
}),
})
if (res.status != 200) {
error.value = await res.text()
} else {
navigateTo("/creator/stickers")
}
submitting.value = false
}
</script>

View File

@ -0,0 +1,118 @@
<template>
<v-container class="px-12">
<div class="flex justify-between items-center mt-5">
<div class="flex items-end gap-2">
<h1 class="text-2xl">Edit sticker pack: {{ data?.name ?? "Loading" }}</h1>
</div>
<div class="flex gap-2">
<v-btn
color="grey"
text="Cancel"
prepend-icon="mdi-arrow-left"
variant="tonal"
to="/creator/stickers"
/>
</div>
</div>
<v-expand-transition>
<v-alert v-if="error" variant="tonal" type="error" class="text-xs mt-5 mb-3">
{{ t("errorOccurred", [error]) }}
</v-alert>
</v-expand-transition>
<v-form class="mt-5" @submit.prevent="submit">
<v-row>
<v-col cols="12" md="6">
<v-text-field
label="Name"
name="name"
variant="outlined"
persistent-hint
hint="A human friendly name for user to recognize this sticker pack"
:model-value="data?.name"
/>
</v-col>
<v-col cols="12" md="6">
<v-text-field
label="Prefix"
name="prefix"
variant="outlined"
persistent-hint
hint="A prefix for every sticker in this pack, will add before sticker's alias"
:model-value="data?.prefix"
/>
</v-col>
<v-col cols="12">
<v-textarea
auto-grow
rows="3"
label="Description"
name="description"
variant="outlined"
persistent-hint
hint="A description for user to know about this sticker pack"
:model-value="data?.description"
/>
</v-col>
</v-row>
<div class="flex justify-end">
<v-btn type="submit" text="Save changes" append-icon="mdi-content-save" :disabled="data == null"
:loading="submitting" />
</div>
</v-form>
</v-container>
</template>
<script setup lang="ts">
definePageMeta({
layout: "creator-hub",
middleware: ["auth"],
})
useHead({
title: "Edit Sticker Pack",
})
const { t } = useI18n()
const route = useRoute()
const data = ref<any>(null)
async function readPack() {
const res = await solarFetch(`/cgi/uc/stickers/packs/${route.params.id}`)
if (res.status != 200) {
error.value = await res.text()
} else {
data.value = await res.json()
}
}
onMounted(() => readPack())
const error = ref<null | string>(null)
const submitting = ref(false)
async function submit(evt: SubmitEvent) {
const data = Object.fromEntries(new FormData(evt.target as HTMLFormElement).entries())
if (!data.name) return
submitting.value = true
const res = await solarFetch(`/cgi/uc/stickers/packs/${route.params.id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
})
if (res.status != 200) {
error.value = await res.text()
} else {
navigateTo("/creator/stickers")
}
submitting.value = false
}
</script>

View File

@ -0,0 +1,158 @@
<template>
<v-container class="px-12">
<div class="flex justify-between items-center mt-5">
<div class="flex items-end gap-2">
<h1 class="text-2xl">Create a new sticker</h1>
</div>
<div class="flex gap-2">
<v-btn
color="grey"
text="Cancel"
prepend-icon="mdi-arrow-left"
variant="tonal"
to="/creator/stickers"
/>
</div>
</div>
<v-expand-transition>
<v-alert v-if="error" variant="tonal" type="error" class="text-xs mt-5 mb-3">
{{ t("errorOccurred", [error]) }}
</v-alert>
</v-expand-transition>
<v-form class="mt-5" @submit.prevent="submit">
<v-row>
<v-col cols="12">
<v-card title="Pack info" prepend-icon="mdi-sticker-emoji" density="compact">
<v-card-text class="mt-2">
<p class="text-lg"><b>{{ data?.name ?? "Loading..." }}</b></p>
<p>{{ data?.description ?? "Please stand by..." }}</p>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" md="6">
<v-text-field
label="Name"
name="name"
variant="outlined"
persistent-hint
hint="A human friendly name for user to recognize this sticker"
/>
</v-col>
<v-col cols="12" md="6">
<v-text-field
label="Alias"
name="alias"
variant="outlined"
persistent-hint
hint="A placeholder of this sticker, will prepend pack's prefix"
/>
</v-col>
<v-col cols="12">
<v-text-field
label="Attachment"
name="attachment_id"
variant="outlined"
persistent-hint
v-model="attachmentRid"
>
<template #details>
<p class="order-first v-messages">
The texture / image of this sticker, you can upload one from
<nuxt-link to="/gallery/new?pool=c3RpY2tlcg" target="_blank" class="underline">here</nuxt-link>
</p>
</template>
<template #prepend-inner>
<v-img
cover
aspect-ratio="1"
width="28"
height="28"
color="grey-lighten-2"
rounded="sm"
:src="attachmentRid.length > 0 ? `${config.public.solarNetworkApi}/cgi/uc/attachments/${attachmentRid}` : `example.com/not-found`"
>
<template #placeholder>
<div class="d-flex align-center justify-center fill-height" v-if="attachmentRid.length > 0">
<v-progress-circular
size="x-small"
width="3"
color="grey-lighten-4"
indeterminate
></v-progress-circular>
</div>
<div class="d-flex align-center justify-center fill-height" v-else>
<v-icon icon="mdi-image-broken-variant" class="block" size="18"/>
</div>
</template>
</v-img>
</template>
</v-text-field>
</v-col>
</v-row>
<div class="flex justify-end">
<v-btn type="submit" text="Create" append-icon="mdi-plus" :disabled="data == null" :loading="submitting" />
</div>
</v-form>
</v-container>
</template>
<script setup lang="ts">
definePageMeta({
layout: "creator-hub",
middleware: ["auth"],
})
useHead({
title: "New Sticker",
})
const { t } = useI18n()
const route = useRoute()
const config = useRuntimeConfig()
const attachmentRid = ref<string>("")
const data = ref<any>(null)
async function readPack() {
const res = await solarFetch(`/cgi/uc/stickers/packs/${route.params.id}`)
if (res.status != 200) {
error.value = await res.text()
} else {
data.value = await res.json()
}
}
onMounted(() => readPack())
const error = ref<null | string>(null)
const submitting = ref(false)
async function submit(evt: SubmitEvent) {
const data = Object.fromEntries(new FormData(evt.target as HTMLFormElement).entries())
if (!data.name) return
submitting.value = true
const res = await solarFetch("/cgi/uc/stickers", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
pack_id: parseInt(route.params.id.toString()),
...data,
}),
})
if (res.status != 200) {
error.value = await res.text()
} else {
navigateTo("/creator/stickers")
}
submitting.value = false
}
</script>

View File

@ -0,0 +1,173 @@
<template>
<v-container class="px-12">
<div class="flex justify-between items-center mt-5">
<div class="flex items-end gap-2">
<h1 class="text-2xl">Stickers & packs</h1>
</div>
<div class="flex gap-2">
<v-btn
color="primary"
text="New"
append-icon="mdi-plus"
variant="tonal"
to="/creator/stickers/new"
/>
</div>
</div>
<v-expand-transition>
<v-alert v-if="error" variant="tonal" type="error" class="text-xs mt-5 mb-3">
{{ t("errorOccurred", [error]) }}
</v-alert>
</v-expand-transition>
<div class="mt-5">
<v-expansion-panels>
<v-expansion-panel
v-for="item in data"
:key="'sticker-pack#'+item.id"
>
<template #title>
<div class="flex items-center gap-2">
<p>{{ item.name }}</p>
<v-chip size="x-small" class="font-mono" rounded color="primary">#{{ item.id }}</v-chip>
</div>
</template>
<template #text>
<v-row>
<v-col cols="12">
<p><b>Description</b></p>
<p>{{ item.description }}</p>
</v-col>
<v-col cols="12" md="6" lg="4">
<p><b>Pack Prefix</b></p>
<v-code class="font-mono mt-0.5 px-3 w-fit">
<span v-if="item.prefix.length == 0"><i>no prefix</i></span>
<span v-else>{{ item.prefix }}</span>
</v-code>
</v-col>
<v-col cols="12" md="6" lg="4">
<p><b>Actions</b></p>
<div class="flex mx-[-10px]">
<v-btn
variant="text"
size="x-small"
color="info"
icon="mdi-sticker-plus"
:to="`/creator/stickers/${item.id}/new`"
/>
<v-btn
variant="text"
size="x-small"
color="warning"
icon="mdi-pencil"
:to="`/creator/stickers/${item.id}/edit`"
/>
<v-dialog max-width="480">
<template #activator="{ props }">
<v-btn
v-bind="props"
variant="text"
size="x-small"
color="error"
icon="mdi-delete"
:disabled="submitting"
/>
</template>
<template v-slot:default="{ isActive }">
<v-card :title="`Delete sticker pack #${item.id}?`">
<v-card-text>
This action will delete the stickers belongs to it and cannot be undone.
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
text="Cancel"
color="grey"
@click="isActive.value = false"
></v-btn>
<v-btn
text="Delete"
color="error"
@click="() => { deletePack(item); isActive.value = false }"
/>
</v-card-actions>
</v-card>
</template>
</v-dialog>
</div>
</v-col>
<v-col cols="12">
<p><b>Stickers</b></p>
<v-card variant="outlined" class="mx-[-0.5ch] mt-1">
<creator-stickers-data-table :pack-id="item.id" :pack-prefix="item.prefix" />
</v-card>
</v-col>
</v-row>
</template>
</v-expansion-panel>
</v-expansion-panels>
</div>
</v-container>
</template>
<script setup lang="ts">
import { solarFetch } from "~/utils/request"
definePageMeta({
layout: "creator-hub",
middleware: ["auth"],
})
useHead({
title: "Stickers",
})
const { t } = useI18n()
const loading = ref(false)
const error = ref<null | string>(null)
const data = ref<any[]>([])
async function readPacks() {
loading.value = true
const res = await solarFetch(`/cgi/uc/stickers/packs?take=10&offset=${data.value.length}`)
if (res.status != 200) {
error.value = await res.text()
} else {
const out = await res.json()
data.value.push(...out["data"])
}
loading.value = false
}
onMounted(() => readPacks())
const submitting = ref(false)
async function deletePack(item: any) {
submitting.value = true
const res = await solarFetch(`/cgi/uc/stickers/packs/${item.id}`, {
method: "DELETE",
})
if (res.status !== 200) {
error.value = await res.text()
} else {
data.value = []
await readPacks()
}
submitting.value = false
}
</script>

View File

@ -0,0 +1,99 @@
<template>
<v-container class="px-12">
<div class="flex justify-between items-center mt-5">
<div class="flex items-end gap-2">
<h1 class="text-2xl">Create a new sticker pack</h1>
</div>
<div class="flex gap-2">
<v-btn
color="grey"
text="Cancel"
prepend-icon="mdi-arrow-left"
variant="tonal"
to="/creator/stickers"
/>
</div>
</div>
<v-expand-transition>
<v-alert v-if="error" variant="tonal" type="error" class="text-xs mt-5 mb-3">
{{ t("errorOccurred", [error]) }}
</v-alert>
</v-expand-transition>
<v-form class="mt-5" @submit.prevent="submit">
<v-row>
<v-col cols="12" md="6">
<v-text-field
label="Name"
name="name"
variant="outlined"
persistent-hint
hint="A human friendly name for user to recognize this sticker pack"
/>
</v-col>
<v-col cols="12" md="6">
<v-text-field
label="Prefix"
name="prefix"
variant="outlined"
persistent-hint
hint="A prefix for every sticker in this pack, will add before sticker's alias"
/>
</v-col>
<v-col cols="12">
<v-textarea
auto-grow
rows="3"
label="Description"
name="description"
variant="outlined"
persistent-hint
hint="A description for user to know about this sticker pack"
/>
</v-col>
</v-row>
<div class="flex justify-end">
<v-btn type="submit" text="Create" append-icon="mdi-plus" :loading="submitting" />
</div>
</v-form>
</v-container>
</template>
<script setup lang="ts">
definePageMeta({
layout: "creator-hub",
middleware: ["auth"],
})
useHead({
title: "New Sticker Pack",
})
const { t } = useI18n()
const error = ref<null | string>(null)
const submitting = ref(false)
async function submit(evt: SubmitEvent) {
const data = Object.fromEntries(new FormData(evt.target as HTMLFormElement).entries())
if (!data.name) return
submitting.value = true
const res = await solarFetch("/cgi/uc/stickers/packs", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
})
if (res.status != 200) {
error.value = await res.text()
} else {
navigateTo('/creator/stickers')
}
submitting.value = false
}
</script>

172
pages/dev/bots/index.vue Normal file
View File

@ -0,0 +1,172 @@
<template>
<v-container class="px-12">
<div class="flex justify-between items-center mt-5">
<div class="flex items-end gap-2">
<h1 class="text-2xl">Bots</h1>
<v-chip rounded="xl" size="small" class="font-mono mb-0.5">
{{ pagination.bots.total }}/{{ currentBotLimit }} quota
</v-chip>
</div>
<div class="flex gap-2">
<v-btn
color="primary"
text="New"
append-icon="mdi-plus"
variant="tonal"
to="/dev/bots/new"
/>
</div>
</div>
<div class="mt-5">
<v-card>
<v-data-table-server
density="compact"
:headers="dataDefinitions.bots"
:items="bots"
:items-length="pagination.bots.total"
:loading="reverting.bots"
v-model:items-per-page="pagination.bots.pageSize"
@update:options="readBots"
item-value="id"
>
<template v-slot:item="{ item }: { item: any }">
<tr>
<td>{{ item.id }}</td>
<td>{{ item.name }}</td>
<td>{{ new Date(item.created_at).toLocaleString() }}</td>
<td>
<dev-bot-token-dialog :item="item">
<template #activator="{ props }">
<v-btn
v-bind="props"
variant="text"
size="x-small"
color="info"
icon="mdi-key"
class="ms-[-8px]"
/>
</template>
</dev-bot-token-dialog>
<v-dialog max-width="480">
<template #activator="{ props }">
<v-btn
v-bind="props"
variant="text"
size="x-small"
color="error"
icon="mdi-delete"
:disabled="submitting"
/>
</template>
<template v-slot:default="{ isActive }">
<v-card :title="`Delete bot ${item.name}?`">
<v-card-text>
This action will delete the bot and all resources related to this bot and cannot be undone.
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
text="Cancel"
color="grey"
@click="isActive.value = false"
></v-btn>
<v-btn
text="Delete"
color="error"
@click="() => { deleteBot(item); isActive.value = false }"
/>
</v-card-actions>
</v-card>
</template>
</v-dialog>
</td>
</tr>
</template>
</v-data-table-server>
</v-card>
</div>
</v-container>
</template>
<script setup lang="ts">
import { solarFetch } from "~/utils/request"
definePageMeta({
layout: "dev-portal",
middleware: ["auth"],
})
useHead({
title: "Bots",
})
const auth = useUserinfo()
const error = ref<null | string>(null)
const dataDefinitions: { [id: string]: any[] } = {
bots: [
{ align: "start", key: "id", title: "ID" },
{ align: "start", key: "name", title: "Name" },
{ align: "start", key: "created_at", title: "Created At" },
{ align: "start", key: "actions", title: "Actions", sortable: false },
],
}
const bots = ref<any>([])
const currentBotLimit = computed(() => auth.userinfo?.perm_nodes["CreateBots"] ?? 0)
const reverting = reactive({ bots: false })
const pagination = reactive({
bots: { page: 1, pageSize: 5, total: 0 },
})
async function readBots({ page, itemsPerPage }: { page?: number; itemsPerPage?: number }) {
if (itemsPerPage) pagination.bots.pageSize = itemsPerPage
if (page) pagination.bots.page = page
reverting.bots = true
const res = await solarFetch(
"/cgi/id/dev/bots?" +
new URLSearchParams({
take: pagination.bots.pageSize.toString(),
offset: ((pagination.bots.page - 1) * pagination.bots.pageSize).toString(),
}),
)
if (res.status !== 200) {
error.value = await res.text()
} else {
const data = await res.json()
bots.value = data["data"]
pagination.bots.total = data["count"]
}
reverting.bots = false
}
onMounted(() => readBots({}))
const submitting = ref(false)
async function deleteBot(item: any) {
submitting.value = true
const res = await solarFetch(`/cgi/id/dev/bots/${item.id}`, {
method: "DELETE",
})
if (res.status !== 200) {
error.value = await res.text()
} else {
await readBots({ page: 1 })
}
submitting.value = false
}
</script>

95
pages/dev/bots/new.vue Normal file
View File

@ -0,0 +1,95 @@
<template>
<v-container class="px-12">
<div class="flex justify-between items-center mt-5">
<div class="flex items-end gap-2">
<h1 class="text-2xl">Create a new bot</h1>
</div>
<div class="flex gap-2">
<v-btn
color="grey"
text="Cancel"
prepend-icon="mdi-arrow-left"
variant="tonal"
to="/dev/bots"
/>
</div>
</div>
<v-expand-transition>
<v-alert v-if="error" variant="tonal" type="error" class="text-xs mt-5 mb-3">
{{ t("errorOccurred", [error]) }}
</v-alert>
</v-expand-transition>
<v-form class="mt-5" @submit.prevent="submit">
<v-row>
<v-col cols="12" md="6" lg="4">
<v-text-field
label="Name"
name="name"
variant="outlined"
hide-details
/>
</v-col>
<v-col cols="12" md="6" lg="4">
<v-text-field
label="Nick"
name="nick"
variant="outlined"
hide-details
/>
</v-col>
<v-col cols="12" md="6" lg="4">
<v-textarea
auto-grow
rows="1"
label="Description"
name="description"
variant="outlined"
/>
</v-col>
</v-row>
<div class="flex justify-end">
<v-btn type="submit" text="Create" append-icon="mdi-plus" :loading="submitting" />
</div>
</v-form>
</v-container>
</template>
<script setup lang="ts">
definePageMeta({
layout: "dev-portal",
middleware: ["auth"],
})
useHead({
title: "New Bot",
})
const { t } = useI18n()
const error = ref<null | string>(null)
const submitting = ref(false)
async function submit(evt: SubmitEvent) {
const data = Object.fromEntries(new FormData(evt.target as HTMLFormElement).entries())
if (!data.name) return
submitting.value = true
const res = await solarFetch("/cgi/id/dev/bots", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
})
if (res.status != 200) {
error.value = await res.text()
} else {
navigateTo('/dev/bots')
}
submitting.value = false
}
</script>

Some files were not shown because too many files have changed in this diff Show More