43 Commits

Author SHA1 Message Date
3974999684 🍱 Add microsoft validation 2025-06-16 23:00:03 +08:00
4fab2183ce Rollback changes about Google AdSense 2025-03-25 21:33:08 +08:00
c6ccdd83a8 Google AdSense 2025-03-24 23:17:41 +08:00
d544eeccb6 🍱 Add two extra programs banner 2025-03-23 22:58:24 +08:00
51fd602cd0 🐛 Fix z index issue 2025-03-23 22:21:55 +08:00
65b46c0195 🐛 Trying to fix bug 2025-03-23 22:12:32 +08:00
abc3156149 🍱 Add developer program banner 2025-03-23 21:17:05 +08:00
991acbfb7b 🐛 Fix jwks request did not forward 2025-03-23 00:30:46 +08:00
63bcd3e58e 🐛 Fix compile error 2025-03-22 23:55:44 +08:00
266b14f169 🐛 Fix missing baseURL 2025-03-22 23:52:57 +08:00
2306ec893b 🐛 Yeah, bug fix again... 2025-03-22 23:49:56 +08:00
4e0ce9118d 🐛 Fix well known error 2025-03-22 23:43:15 +08:00
0276272b42 🐛 Fix OIDC bugs 2025-03-22 23:18:34 +08:00
41b887faf6 Account page support 2025-03-21 01:00:13 +08:00
b12d1deece 📝 Update Basic Law 2025-03-20 23:53:28 +08:00
f97310c01d 📱 Halfway to responsive 2025-03-20 22:07:26 +08:00
95e0d3fb29 💄 Home page background add supports for light mode 2025-03-20 21:42:39 +08:00
234043fece 📈 Add umami analyze 2025-03-20 21:38:49 +08:00
1739cd92b7 🐛 Add backward compatibility 2025-03-20 21:32:52 +08:00
ae31e72447 📝 Update terms 2025-03-20 13:08:26 +08:00
4efa211e9e 🐛 Fix bugs 2025-03-20 00:02:05 +08:00
6304305ff6 🗑️ Remove kb 2025-03-19 23:41:26 +08:00
9726d8f805 Finish solar network product page 2025-03-19 23:40:10 +08:00
1f2c4f33cb 📝 Add before you start section 2025-03-18 23:54:17 +08:00
e9e182ea48 Basic Solar Network product page 2025-03-18 23:15:37 +08:00
e4111dc06e 💄 Optimized header & landing page 2025-03-17 22:23:40 +08:00
3e7f259834 🗑️ Clean up layouts 2025-03-17 21:10:33 +08:00
97449bdc1e 🗑️ Clean up posts 2025-03-17 20:58:36 +08:00
975766302a 🗑️ Cleanup & Reborn 2025-03-17 01:24:03 +08:00
b9d89149b0 🐛 Fix bugs causing by upgraded to v2 2024-12-08 15:36:03 +08:00
6693acb24a Better attachment view page 2024-10-04 20:21:01 +08:00
a0cf66d2e1 📝 Update RoadSign docs 2024-10-04 17:55:42 +08:00
92e56e7e88 💄 Better docs page 2024-10-04 02:35:02 +08:00
295d2d5b95 🔨 Add roadsign config 2024-10-04 00:43:59 +08:00
f80df60858 🐛 Fix edit sticker use wrong API 2024-09-29 22:58:52 +08:00
8b4b6eb703 🌐 Add English docs 2024-09-28 15:53:51 +08:00
608bdc5d28 Breadcrumb 2024-09-28 15:30:13 +08:00
1ccb9e738e 📝 Migrate docs 2024-09-28 15:10:43 +08:00
fe0fffada2 ⬆️ Upgrade deps 2024-09-28 13:49:05 +08:00
9bcd809493 💄 Better docs page 2024-09-28 13:29:05 +08:00
dc740f8538 📱 Fix docs page responsive 2024-09-28 00:36:41 +08:00
7e3c3a1679 Docs page 2024-09-28 00:28:40 +08:00
729b7f3c00 🐛 Fix create sticker pack id is string 2024-09-27 13:10:25 +08:00
70 changed files with 1654 additions and 794 deletions

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"
}
}
]
}

12
app.vue
View File

@@ -34,3 +34,15 @@ onMounted(() => {
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>

View File

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

View File

@@ -0,0 +1,46 @@
<svg id="livetype" xmlns="http://www.w3.org/2000/svg" width="119.66407" height="40" viewBox="0 0 119.66407 40">
<title>Download_on_the_App_Store_Badge_US-UK_RGB_wht_092917</title>
<g>
<g>
<g>
<path d="M110.13477,0H9.53468c-.3667,0-.729,0-1.09473.002-.30615.002-.60986.00781-.91895.0127A13.21476,13.21476,0,0,0,5.5171.19141a6.66509,6.66509,0,0,0-1.90088.627A6.43779,6.43779,0,0,0,1.99757,1.99707,6.25844,6.25844,0,0,0,.81935,3.61816a6.60119,6.60119,0,0,0-.625,1.90332,12.993,12.993,0,0,0-.1792,2.002C.00587,7.83008.00489,8.1377,0,8.44434V31.5586c.00489.3105.00587.6113.01515.9219a12.99232,12.99232,0,0,0,.1792,2.0019,6.58756,6.58756,0,0,0,.625,1.9043A6.20778,6.20778,0,0,0,1.99757,38.001a6.27445,6.27445,0,0,0,1.61865,1.1787,6.70082,6.70082,0,0,0,1.90088.6308,13.45514,13.45514,0,0,0,2.0039.1768c.30909.0068.6128.0107.91895.0107C8.80567,40,9.168,40,9.53468,40H110.13477c.3594,0,.7246,0,1.084-.002.3047,0,.6172-.0039.9219-.0107a13.279,13.279,0,0,0,2-.1768,6.80432,6.80432,0,0,0,1.9082-.6308,6.27742,6.27742,0,0,0,1.6172-1.1787,6.39482,6.39482,0,0,0,1.1816-1.6143,6.60413,6.60413,0,0,0,.6191-1.9043,13.50643,13.50643,0,0,0,.1856-2.0019c.0039-.3106.0039-.6114.0039-.9219.0078-.3633.0078-.7246.0078-1.0938V9.53613c0-.36621,0-.72949-.0078-1.09179,0-.30664,0-.61426-.0039-.9209a13.5071,13.5071,0,0,0-.1856-2.002,6.6177,6.6177,0,0,0-.6191-1.90332,6.46619,6.46619,0,0,0-2.7988-2.7998,6.76754,6.76754,0,0,0-1.9082-.627,13.04394,13.04394,0,0,0-2-.17676c-.3047-.00488-.6172-.01074-.9219-.01269-.3594-.002-.7246-.002-1.084-.002Z"/>
<path d="M8.44483,39.125c-.30468,0-.602-.0039-.90429-.0107a12.68714,12.68714,0,0,1-1.86914-.1631,5.88381,5.88381,0,0,1-1.65674-.5479,5.40573,5.40573,0,0,1-1.397-1.0166,5.32082,5.32082,0,0,1-1.02051-1.3965,5.72186,5.72186,0,0,1-.543-1.6572,12.41351,12.41351,0,0,1-.1665-1.875c-.00634-.2109-.01464-.9131-.01464-.9131V8.44434S.88185,7.75293.8877,7.5498a12.37039,12.37039,0,0,1,.16553-1.87207,5.7555,5.7555,0,0,1,.54346-1.6621A5.37349,5.37349,0,0,1,2.61183,2.61768,5.56543,5.56543,0,0,1,4.01417,1.59521a5.82309,5.82309,0,0,1,1.65332-.54394A12.58589,12.58589,0,0,1,7.543.88721L8.44532.875H111.21387l.9131.0127a12.38493,12.38493,0,0,1,1.8584.16259,5.93833,5.93833,0,0,1,1.6709.54785,5.59374,5.59374,0,0,1,2.415,2.41993,5.76267,5.76267,0,0,1,.5352,1.64892,12.995,12.995,0,0,1,.1738,1.88721c.0029.2832.0029.5874.0029.89014.0079.375.0079.73193.0079,1.09179V30.4648c0,.3633,0,.7178-.0079,1.0752,0,.3252,0,.6231-.0039.9297a12.73126,12.73126,0,0,1-.1709,1.8535,5.739,5.739,0,0,1-.54,1.67,5.48029,5.48029,0,0,1-1.0156,1.3857,5.4129,5.4129,0,0,1-1.3994,1.0225,5.86168,5.86168,0,0,1-1.668.5498,12.54218,12.54218,0,0,1-1.8692.1631c-.2929.0068-.5996.0107-.8974.0107l-1.084.002Z" style="fill: #fff"/>
</g>
<g id="_Group_" data-name="&lt;Group&gt;">
<g id="_Group_2" data-name="&lt;Group&gt;">
<g id="_Group_3" data-name="&lt;Group&gt;">
<path id="_Path_" data-name="&lt;Path&gt;" d="M24.99671,19.88935a5.14625,5.14625,0,0,1,2.45058-4.31771,5.26776,5.26776,0,0,0-4.15039-2.24376c-1.74624-.1833-3.43913,1.04492-4.329,1.04492-.90707,0-2.27713-1.02672-3.75247-.99637a5.52735,5.52735,0,0,0-4.65137,2.8367c-2.01111,3.482-.511,8.59939,1.41551,11.414.96388,1.37823,2.09037,2.91774,3.56438,2.86315,1.4424-.05983,1.98111-.91977,3.7222-.91977,1.72494,0,2.23035.91977,3.73427.88506,1.54777-.02512,2.52292-1.38435,3.453-2.77563a11.39931,11.39931,0,0,0,1.579-3.21589A4.97284,4.97284,0,0,1,24.99671,19.88935Z"/>
<path id="_Path_2" data-name="&lt;Path&gt;" d="M22.15611,11.47681a5.06687,5.06687,0,0,0,1.159-3.62989,5.15524,5.15524,0,0,0-3.33555,1.72582,4.82131,4.82131,0,0,0-1.18934,3.4955A4.26259,4.26259,0,0,0,22.15611,11.47681Z"/>
</g>
</g>
<g>
<path d="M42.30178,27.13965h-4.7334l-1.13672,3.35645H34.42678l4.4834-12.418h2.083l4.4834,12.418H43.43752Zm-4.24316-1.54883h3.752L39.961,20.14355H39.9092Z"/>
<path d="M55.1592,25.96973c0,2.81348-1.50586,4.62109-3.77832,4.62109a3.0693,3.0693,0,0,1-2.84863-1.584h-.043v4.48438h-1.8584V21.44238h1.79883v1.50586h.03418a3.21162,3.21162,0,0,1,2.88281-1.60059C53.64455,21.34766,55.1592,23.16406,55.1592,25.96973Zm-1.91016,0c0-1.833-.94727-3.03809-2.39258-3.03809-1.41992,0-2.375,1.23047-2.375,3.03809,0,1.82422.95508,3.0459,2.375,3.0459C52.30178,29.01563,53.249,27.81934,53.249,25.96973Z"/>
<path d="M65.12453,25.96973c0,2.81348-1.50635,4.62109-3.77881,4.62109a3.0693,3.0693,0,0,1-2.84863-1.584h-.043v4.48438h-1.8584V21.44238h1.79883v1.50586h.03418a3.21162,3.21162,0,0,1,2.88281-1.60059C63.6094,21.34766,65.12453,23.16406,65.12453,25.96973Zm-1.91064,0c0-1.833-.94727-3.03809-2.39258-3.03809-1.41992,0-2.375,1.23047-2.375,3.03809,0,1.82422.95508,3.0459,2.375,3.0459C62.26662,29.01563,63.21389,27.81934,63.21389,25.96973Z"/>
<path d="M71.70949,27.03613c.1377,1.23145,1.334,2.04,2.96875,2.04,1.56641,0,2.69336-.80859,2.69336-1.91895,0-.96387-.67969-1.541-2.28906-1.93652l-1.60937-.3877c-2.28027-.55078-3.33887-1.61719-3.33887-3.34766,0-2.14258,1.86719-3.61426,4.51758-3.61426,2.625,0,4.42383,1.47168,4.48438,3.61426h-1.876c-.1123-1.23926-1.13672-1.9873-2.63379-1.9873s-2.52148.75684-2.52148,1.8584c0,.87793.6543,1.39453,2.25488,1.79l1.36816.33594c2.54785.60254,3.60547,1.626,3.60547,3.44238,0,2.32324-1.84961,3.77832-4.793,3.77832-2.75391,0-4.61328-1.4209-4.7334-3.667Z"/>
<path d="M83.34621,19.2998v2.14258h1.72168v1.47168H83.34621v4.99121c0,.77539.34473,1.13672,1.10156,1.13672a5.80752,5.80752,0,0,0,.61133-.043v1.46289a5.10351,5.10351,0,0,1-1.03223.08594c-1.833,0-2.54785-.68848-2.54785-2.44434V22.91406H80.16262V21.44238H81.479V19.2998Z"/>
<path d="M86.064,25.96973c0-2.84863,1.67773-4.63867,4.29395-4.63867,2.625,0,4.29492,1.79,4.29492,4.63867,0,2.85645-1.66113,4.63867-4.29492,4.63867C87.72512,30.6084,86.064,28.82617,86.064,25.96973Zm6.69531,0c0-1.9541-.89551-3.10742-2.40137-3.10742s-2.40137,1.16211-2.40137,3.10742c0,1.96191.89551,3.10645,2.40137,3.10645S92.7593,27.93164,92.7593,25.96973Z"/>
<path d="M96.18508,21.44238h1.77246v1.541h.043a2.1594,2.1594,0,0,1,2.17773-1.63574,2.86616,2.86616,0,0,1,.63672.06934v1.73828a2.59794,2.59794,0,0,0-.835-.1123,1.87264,1.87264,0,0,0-1.93652,2.083v5.37012h-1.8584Z"/>
<path d="M109.38332,27.83691c-.25,1.64355-1.85059,2.77148-3.89844,2.77148-2.63379,0-4.26855-1.76465-4.26855-4.5957,0-2.83984,1.64355-4.68164,4.19043-4.68164,2.50488,0,4.08008,1.7207,4.08008,4.46582v.63672h-6.39453v.1123a2.358,2.358,0,0,0,2.43555,2.56445,2.04834,2.04834,0,0,0,2.09082-1.27344Zm-6.28223-2.70215h4.52637a2.1773,2.1773,0,0,0-2.2207-2.29785A2.292,2.292,0,0,0,103.10109,25.13477Z"/>
</g>
</g>
</g>
<g id="_Group_4" data-name="&lt;Group&gt;">
<g>
<path d="M37.82619,8.731a2.63964,2.63964,0,0,1,2.80762,2.96484c0,1.90625-1.03027,3.002-2.80762,3.002H35.67092V8.731Zm-1.22852,5.123h1.125a1.87588,1.87588,0,0,0,1.96777-2.146,1.881,1.881,0,0,0-1.96777-2.13379h-1.125Z"/>
<path d="M41.68068,12.44434a2.13323,2.13323,0,1,1,4.24707,0,2.13358,2.13358,0,1,1-4.24707,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C44.57522,13.99463,45.01369,13.42432,45.01369,12.44434Z"/>
<path d="M51.57326,14.69775h-.92187l-.93066-3.31641h-.07031l-.92676,3.31641h-.91309l-1.24121-4.50293h.90137l.80664,3.436h.06641l.92578-3.436h.85254l.92578,3.436h.07031l.80273-3.436h.88867Z"/>
<path d="M53.85354,10.19482H54.709v.71533h.06641a1.348,1.348,0,0,1,1.34375-.80225,1.46456,1.46456,0,0,1,1.55859,1.6748v2.915h-.88867V12.00586c0-.72363-.31445-1.0835-.97168-1.0835a1.03294,1.03294,0,0,0-1.0752,1.14111v2.63428h-.88867Z"/>
<path d="M59.09377,8.437h.88867v6.26074h-.88867Z"/>
<path d="M61.21779,12.44434a2.13346,2.13346,0,1,1,4.24756,0,2.1338,2.1338,0,1,1-4.24756,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C64.11232,13.99463,64.5508,13.42432,64.5508,12.44434Z"/>
<path d="M66.4009,13.42432c0-.81055.60352-1.27783,1.6748-1.34424l1.21973-.07031v-.38867c0-.47559-.31445-.74414-.92187-.74414-.49609,0-.83984.18213-.93848.50049h-.86035c.09082-.77344.81836-1.26953,1.83984-1.26953,1.12891,0,1.76563.562,1.76563,1.51318v3.07666h-.85547v-.63281h-.07031a1.515,1.515,0,0,1-1.35254.707A1.36026,1.36026,0,0,1,66.4009,13.42432Zm2.89453-.38477v-.37646l-1.09961.07031c-.62012.0415-.90137.25244-.90137.64941,0,.40527.35156.64111.835.64111A1.0615,1.0615,0,0,0,69.29543,13.03955Z"/>
<path d="M71.34816,12.44434c0-1.42285.73145-2.32422,1.86914-2.32422a1.484,1.484,0,0,1,1.38086.79h.06641V8.437h.88867v6.26074h-.85156v-.71143h-.07031a1.56284,1.56284,0,0,1-1.41406.78564C72.0718,14.772,71.34816,13.87061,71.34816,12.44434Zm.918,0c0,.95508.4502,1.52979,1.20313,1.52979.749,0,1.21191-.583,1.21191-1.52588,0-.93848-.46777-1.52979-1.21191-1.52979C72.72121,10.91846,72.26613,11.49707,72.26613,12.44434Z"/>
<path d="M79.23,12.44434a2.13323,2.13323,0,1,1,4.24707,0,2.13358,2.13358,0,1,1-4.24707,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C82.12453,13.99463,82.563,13.42432,82.563,12.44434Z"/>
<path d="M84.66945,10.19482h.85547v.71533h.06641a1.348,1.348,0,0,1,1.34375-.80225,1.46456,1.46456,0,0,1,1.55859,1.6748v2.915H87.605V12.00586c0-.72363-.31445-1.0835-.97168-1.0835a1.03294,1.03294,0,0,0-1.0752,1.14111v2.63428h-.88867Z"/>
<path d="M93.51516,9.07373v1.1416h.97559v.74854h-.97559V13.2793c0,.47168.19434.67822.63672.67822a2.96657,2.96657,0,0,0,.33887-.02051v.74023a2.9155,2.9155,0,0,1-.4834.04541c-.98828,0-1.38184-.34766-1.38184-1.21582v-2.543h-.71484v-.74854h.71484V9.07373Z"/>
<path d="M95.70461,8.437h.88086v2.48145h.07031a1.3856,1.3856,0,0,1,1.373-.80664,1.48339,1.48339,0,0,1,1.55078,1.67871v2.90723H98.69v-2.688c0-.71924-.335-1.0835-.96289-1.0835a1.05194,1.05194,0,0,0-1.13379,1.1416v2.62988h-.88867Z"/>
<path d="M104.76125,13.48193a1.828,1.828,0,0,1-1.95117,1.30273A2.04531,2.04531,0,0,1,100.73,12.46045a2.07685,2.07685,0,0,1,2.07617-2.35254c1.25293,0,2.00879.856,2.00879,2.27V12.688h-3.17969v.0498a1.1902,1.1902,0,0,0,1.19922,1.29,1.07934,1.07934,0,0,0,1.07129-.5459Zm-3.126-1.45117h2.27441a1.08647,1.08647,0,0,0-1.1084-1.1665A1.15162,1.15162,0,0,0,101.63527,12.03076Z"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 441 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 787 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 770 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 749 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 461 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

View File

@@ -22,8 +22,9 @@ 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"],
"passport": ["HyperNet.Passport", "https://git.solsynth.dev/HyperNet/Passport"],
"paperclip": ["HyperNet.Paperclip", "https://git.solsynth.dev/HyperNet/Paperclip"],
"roadsign": ["RoadSign", "https://git.solsynth.dev/Goatworks/RoadSign"],
}
</script>

View File

@@ -2,7 +2,7 @@
<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>
<nuxt-link to="/terms/basic-law" 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>

View File

@@ -15,7 +15,7 @@
:key="item.code"
:value="item.code"
:active="locale == item.code"
@click.prevent.stop="setLocale(item.code)"
@click.prevent.stop="() => { setLocale(item.code); emits('update') }"
>
<v-list-item-title>{{ item.name }}</v-list-item-title>
</v-list-item>
@@ -24,5 +24,6 @@
</template>
<script lang="ts" setup>
const emits = defineEmits(['update'])
const { locale, locales, setLocale } = useI18n()
</script>

View File

@@ -2,15 +2,17 @@
<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 :to="`/users/${post.publisher?.name}`">
<v-avatar :image="getAttachmentUrl(post.publisher?.avatar)" icon="mdi-account-circle" />
</nuxt-link>
<div class="flex flex-col">
<span>{{ post.author?.nick }} <span class="text-xs">@{{ post.author?.name }}</span></span>
<span
>{{ post.publisher?.nick }} <span class="text-xs">@{{ post.publisher?.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 }}
{{ post.publisher?.description }}
</span>
<div v-if="post.type != 'story'" class="mt-1">
@@ -29,7 +31,7 @@
/>
</div>
<article v-if="post.type == 'story' || props.forceShowContent" class="text-base prose max-w-none">
<article v-if="(post.type == 'story' || props.forceShowContent) && post.body?.content" class="text-base prose max-w-none">
<m-d-c :value="post.body?.content"></m-d-c>
</article>
@@ -41,24 +43,19 @@
</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 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>
{{ 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"
>
<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}`"
@@ -73,10 +70,12 @@
</template>
<script setup lang="ts">
const props = defineProps<{ post: any, forceShowContent?: boolean, noClickableAttachment?: boolean }>()
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}`)
const url = computed(() =>
props.post?.alias ? `/posts/${props.post?.alias_prefix}/${props.post?.alias}` : `/posts/${props.post?.id}`,
)
</script>

View File

@@ -1,46 +0,0 @@
<template>
<v-carousel v-if="!loading" show-arrows="hover" cycle hide-delimiters progress="primary">
<v-carousel-item v-for="(item, i) in items" :key="i">
<v-sheet color="rgba(0, 0, 0, .4)" class="h-full w-full flex items-center justify-center post-container overflow-scroll">
<post-item class="mt-5 mb-2" force-show-content :post="item" />
</v-sheet>
</v-carousel-item>
</v-carousel>
<div v-else class="w-full h-full flex items-center justify-center">
<v-progress-circular indeterminate />
</div>
</template>
<script setup lang="ts">
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>
.post-container::-webkit-scrollbar {
display: none;
}
.post-container {
-ms-overflow-style: none;
scrollbar-width: none;
}
</style>

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

@@ -18,7 +18,7 @@
</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 />
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">
@@ -49,7 +49,7 @@
<script setup lang="ts">
const config = useRuntimeConfig()
const props = defineProps<{ item: any }>()
const props = defineProps<{ item: any, noCover?: boolean }>()
const item = computed(() => props.item)

View File

@@ -1,7 +1,7 @@
<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" />
<post-item v-if="status === 'success'" class="no-margin-post" :post="post" :force-show-content="props.forceShowContent" />
<div v-else>{{ t("loading") }}</div>
</div>
</template>

View File

@@ -5,13 +5,11 @@
</v-alert>
</v-expand-transition>
<v-data-table-server
<v-data-table
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"
>
@@ -74,23 +72,24 @@
<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.
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="Cancel" color="grey" @click="isActive.value = false"></v-btn>
<v-btn
text="Delete"
color="error"
@click="() => { deleteSticker(item); isActive.value = false }"
@click="
() => {
deleteSticker(item)
isActive.value = false
}
"
/>
</v-card-actions>
</v-card>
@@ -99,7 +98,7 @@
</td>
</tr>
</template>
</v-data-table-server>
</v-data-table>
</template>
<script setup lang="ts">
@@ -108,7 +107,7 @@ import { solarFetch } from "~/utils/request"
const config = useRuntimeConfig()
const { t } = useI18n()
const props = defineProps<{ packId: number, packPrefix?: string }>()
const props = defineProps<{ packId: number; packPrefix?: string }>()
const error = ref<null | string>(null)
@@ -125,34 +124,20 @@ const dataDefinitions: { [id: string]: any[] } = {
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
async function readStickers() {
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(),
}),
)
const res = await solarFetch("/cgi/uc/stickers/packs/" + props.packId)
if (res.status !== 200) {
error.value = await res.text()
} else {
const data = await res.json()
stickers.value = data["data"]
pagination.stickers.total = data["count"]
stickers.value = data["stickers"]
}
reverting.stickers = false
}
onMounted(() => readStickers({}))
onMounted(() => readStickers())
const submitting = ref(false)
@@ -165,7 +150,7 @@ async function deleteSticker(item: any) {
if (res.status !== 200) {
error.value = await res.text()
} else {
await readStickers({})
await readStickers()
}
submitting.value = false

View File

@@ -22,6 +22,6 @@ downloads:
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-download-link{items='downloads'}
:embed-post-item{id=914}

View File

@@ -48,7 +48,7 @@ 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. **
**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'}

View File

@@ -1,7 +1,48 @@
---
thumbnail: /thumbnails/products/roadsign.webp
title: RoadSign
description: The reserve proxy that powered our network. Powerful and easy to use.
description: The HTTP server that powered us. Great ability, and easy to use
author: [littlesheep]
archived: true
---
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,54 @@
---
title: User Agreement / Basic Law
date: 2025-03-19T16:12:21.897Z
---
This User Agreement (a.k.a. the Basic Law) is the framework of rules for all Solsynth LLC products, and other related regulations should comply with it, or the entry will not be effective.
This User Agreement will be referred to herein as “these Terms and Conditions” and Solsynth LLC will refer to it as “we” and “us”.
As used herein, account number, account, and Solarpass refer to the User's account on the Solar Network.
## Scope of Application
1. The User Agreement applies to all Solsynth LLC products, including but not limited to Solar Network, DietaryGuard, and AceField. 2.
2. Any prior events will still be subject to the latest revised version of these Terms and Conditions. 3.
3. All Solar Network users are deemed to have agreed to the contents of these Terms and Conditions and any subsequent updates to these Terms and Conditions upon registration.
4. The final interpretation of these Terms and Conditions is the responsibility of Solsynth LLC and the legislator of the relevant entry.
## Amendments and Updates to the Terms and Conditions
1. Changes to these Regulations shall be made by Solsynth LLC and the Legislator.
2. The Legislative Councilor's proposal to amend the Basic Law shall be supported by a referendum of more than **3/2** of the users and shall not be subject to the one-vote right of passage.
3. Legislators' proposals to amend other sub-regulations shall be supported by a referendum of more than **one-half** of the users.
4. Some special regulations are protected from amendment by legislators.
5. Regarding any subsequent updates to the ordinance, we will notify the content of the update by means of “notification on the website” or “email push”.
## Provision and Discontinuation of Services
1. Solsynth LLC will provide the same service to all natural persons in the world. 2.
2. We also reserve the right to discontinue the service to any user, and in principle we will inform the reason for discontinuance.
3. after the termination or suspension of the service, the user has the right to ask us to delete or export all user data.
4. In case of violation of the relevant regulations, the user will receive three kinds of penalties: **Warning, Suspension and Disablement**.
- Warning (Strike): The warning will not have any practical effect on the User and will be automatically revoked after 180 days without any bad behavior. If the user receives another warning within the warning period, the penalty will be upgraded to suspension.
- Suspension: There are two types of suspension: “Full Suspension” and “Partial Suspension”. Full Suspension” shall, in principle, have a time limit for entry into force.
- Full Suspension: The user will not be allowed to access any Solar Network and other services, and will not be able to log in to Solarpass.
- Partial deactivation: Partial disabling of the user's rights, e.g. uploading of files, publishing of posts, etc.
- Disablement: The user's entire account and all rights of Solsynth LLC to use other services are disabled. We also reserve the right to delete the relevant data.
5. A natural person can register and own only one Solarpass account, and we reserve the right to take action against other sub-accounts of the same User for deletion of data.
6. The transfer and sale of Solarpass accounts are strictly prohibited. If such behavior is discovered, measures will be taken to delete the relevant data immediately.
7. If a user opens a sub-account in any way during the penalty period in an attempt to evade the penalty, the sub-account shall be subject to deletion of data and the penalty shall be escalated or the time limit extended, as the case may be.
8. Bot accounts opened through the Developer Portal are not considered sub-accounts. *For more information on the use of bot accounts, please refer to the Developer Rules (/terms/developer-rules).
## 4. User Generated Content
1. we do not assume any responsibility for user-generated content posted on our Products. 2.
2. Regarding copyright infringement of content published by users on our products, we will remove the content in question; if we agree that there is a large amount of copyright infringement by the publisher, we will impose penalties of **warning and suspension of rights** depending on the situation.
3. In principle, we do not restrict users' freedom of expression, with the exception of the following cases, in which we will remove the content and penalize the publisher according to the circumstances:
- Copyright infringement
- Board-washing, meaningless content *See [community-safety-law](/terms/community-safety-law)* for details.
- Spreading rumors, fear-mongering, extremist speech *See the Community Safety Laws for more information.
4. With regard to the files uploaded by the User on Solar Network, they are considered to be public content on the Internet; at the moment of completion of the upload the User is considered to have authorized us with the required copyright to display the respective content.
## 5. User Privacy Protection
*The contents of this chapter are detailed in the [privacy-policy](/terms/privacy-policy)*

View File

@@ -1,50 +1,48 @@
---
title: Privacy Policy
date: 2024-08-15T15:18:48.218Z
title: Privacy Policy / Privacy Protection Law
date: 2025-03-19T16:12:21.897Z
---
## Introduction
This regulation is an expansion of Chapter 5 of the contents of the “Basic Law”. This entry applies the security protection policy (direct modification by the legislator is not allowed due to the design of user data security).
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.
We take your privacy very seriously. This Privacy Policy outlines the types of personal information we collect, how we use it, and the protective measures we take.
## Information Collection
## 5.1 Information Collection
We collect personal information only when necessary to provide our services.
This may include your name, email address, and other relevant details.
We only collect personal information that is necessary to provide our services. This includes:
## Use of Information
- Email
- Telephone number *optional*
- Name *optional
- Address *optional
- Date of Birth *Optional
- Gender *selectable
- Internet Address
- Device Identifier
- User behavior data
## 5.2 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
- Provide data necessary for the provision and use of our services
- communicate with you about updates to regulations or other important information
- analyze services to improve the quality of our services
## Data Sharing
## 5.3 Data Sharing
We do not sell, trade, or share your personal information with third parties except as required by law.
We do not sell or trade your personal information.
## Data Security
We share some of your personal information, which may include device identifiers and behavioral data, with our partner Google Analytics to help us analyze and improve our services, as described in Google's Privacy Policy (https://policies.google.com/privacy).
We implement robust security measures to protect your personal information from unauthorized access,
alteration, disclosure, or destruction.
## 5.4 Data Security
## Your Rights
We have implemented strong security measures (including, but not limited to, the use of industry-leading encryption algorithms, a database key rotation policy, etc.) to protect your personal information from unauthorized access, alteration, disclosure or destruction.
You have the right to:
## 5.5 Your Rights
Regardless of the penalties imposed on your account, you always have the right to:
- Access the personal information we hold about you
- Request corrections to your personal information
- Request correction of 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

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

@@ -1,7 +1,47 @@
---
thumbnail: /thumbnails/products/roadsign.webp
title: RoadSign
description: 为我们的网络提供动力的反向代理。功能强大,使用方便
description: 为我们的网络提供动力的 HTTP 服务器。功能强大,使用方便
author: [littlesheep]
archived: true
---
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,54 @@
---
title: 用户协议 / 基本法
date: 2025-03-19T16:12:21.897Z
---
本用户协议(又称基本法)是 Solsynth LLC 所有产品的规则框架,其他相关的条例应该遵守本条例,否则该条目不生效。
本文将省略称呼本用户协议为「本条例」Solsynth LLC 称为「我们」。
本文中帐号、帐户、及 Solarpass 均指代用户在 Solar Network 上开设的帐号。
## 1. 适用范围
1. 用户协议适用于所有 Solsynth LLC 的产品,包括但不限于 Solar Network、DietaryGuard 及 AceField。
2. 任何发生在以前的事件仍然适用最新版修订版的本条例。
3. 所有 Solar Network 用户在注册时视为同意本条例的内容以及其后续更新。
4. 条例的最终解释权归属于 Solsynth LLC 及相关条目立法委员。
## 2. 条例的修改和更新
1. 本条例的修改由 Solsynth LLC 和立法委员共同完成。
2. 立法委员提出关于修改「基本法」的提案应当得到超过**二分之三**的用户公投支持并不适用一票通过权。
3. 立法委员提出关于修改其他子条例的提案应当得到超过**二分之一**的用户公投支持。
4. 部份特殊条例实行保护方针不允许立法委员修改。
5. 关于后续任何的条例更新,我们将采取「站内通知」或「邮件推送」的方式通知内容更新。
## 3. 服务的提供与中断
1. Solsynth LLC 将向世界上所有的自然人提供同等的服务。
2. 我们同时保留向任何用户停止服务的权利,原则上我们会告知停止服务的原因。
3. 在用户的服务被终止或停权之后,用户有权向我们要求删除或导出所有的用户资料。
4. 用户在违反相关条例时,会收到**警告、停权、禁用**三种处罚措施。
- 警告 (Strike): 不会对用户造成任何实际上的影响,警告会在无任何不良行为 180 天后自动撤销。若用户在警告期内再次获得警告,处罚将升级为停权。
- 停权:停权分为两种类型「完全停权」和「部份停权」。其中「完全停权」原则上应有生效时限。
- 完全停权:用户将不允许存取任何 Solar Network 和其他服务内容,同时也会无法登陆 Solarpass。
- 部份停权:禁用用户的部份权利,例如上传文件、发布帖子等。
- 禁用:禁用用户的整个帐号和所有 Solsynth LLC 使用其他服务的权利。同时我们保留删除相关数据的权利。
5. 一个自然人只能注册、拥有一个 Solarpass 帐号,我们有权对其他同用户的子帐号采取删除数据的措施。
6. 关于 Solarpass 帐号的转让、出售是绝对禁止的行为,关于发现相关行为将立即采取删除相关数据的措施。
7. 若用户在处罚期间采取任何方式开设子帐号试图逃避处罚,应当对子帐号采取删除数据的措施,并且视情况升级处罚或延长时限。
8. 通过「开发者门户」开设的机器人帐号不属于子帐号范畴。*关于「机器人帐号」的使用规定,详见 [开发者守则](/terms/developer-rules)*
## 4. 用户生成内容
1. 我们不承担任何关于用户在我们产品上发表的内容的责任。
2. 关于用户在我们产品上发布的内容侵犯版权时,我们会对相关内容进行删除;若同意发布者有大量侵犯版权的情况,根据情况处以**警告及停权**的处罚。
3. 我们原则上不会限制用户的言论自由,但以下情况例外,我们会根据情况对相关内容进行删除并处罚发布者:
- 侵犯版权
- 洗板,无意义的内容 *详见 [社区治安条例](/terms/community-safety-law)*
- 散播谣言、恐慌、极端主义的言论 *详见 [社区治安条例](/terms/community-safety-law)*
4. 关于用户上传在 Solar Network 上的文件,视为互联网上的公开内容;在用户上传完成的即刻起,视为用户授权我们所需的版权展示相关的内容。
## 5. 用户隐私保护
*本章内容详见 [隐私保护法](/terms/privacy-policy)*

View File

@@ -1,44 +1,48 @@
---
title: 隐私策略
date: 2024-08-15T15:18:48.218Z
title: 隐私策略 / 隐私保护法
date: 2025-03-19T16:12:21.897Z
---
## 简介
本条例是对「基本法」内容第五章的扩充。本条目适用安全保护方针(因设计用户数据安全,不允许立法委员直接修改)。
我们非常重视您的隐私。本隐私政策概述了我们收集的个人信息类型、使用方式以及我们采取的保护措施。
## 信息收集
## 5.1 信息收集
我们仅在提供服务时收集必要的个人信息。这可能包括您的姓名、电子邮件地址以及其他相关信息。
我们仅在提供服务时收集必要的个人信息。这包括:
## 信息使用
- 电子邮件
- 电话号码 *可选*
- 姓名 *可选*
- 地址 *可选*
- 出生日期 *可选*
- 性别 *可选*
- 互联网地址
- 设备标识符
- 用户行为数据
## 5.2 信息使用
我们使用您的个人信息来:
- 提供和改进我们的服务
- 与您沟通更新或重要信息
- 确保遵守法律义务
- 提供和我们的服务使用的必要数据
- 与您沟通相关条例更新或其他重要信息
- 分析服务提升我们服务的质量
## 数据共享
## 5.3 数据共享
我们不会出售、交易或与第三方分享您的个人信息,法律要求除外
我们不会出售、交易您的个人信息
## 数据安全
我们与我们的合作伙伴 Google Analytics 共享您部份的个人信息,这可能包括设备标识符和行为数据,来帮助我们分析和改进我们的服务,详见 [Google 的隐私政策](https://policies.google.com/privacy)。
我们实施了强有力的安全措施,以保护您的个人信息免受未经授权的访问、更改、披露或销毁。
## 5.4 数据安全
## 您的权利
我们实施了强有力的安全措施(包括但不限于使用业界领先的加密算法,实行数据库密钥轮换政策等),以保护您的个人信息免受未经授权的访问、更改、披露或销毁。
您有权:
## 5.5 您的权利
无论您的帐号被如何处罚,您一直有权:
- 访问我们持有的关于您的个人信息
- 请求更正您的个人信息
- 请求删除您的个人信息
## 联系我们
如果您对本隐私政策或我们的数据处理方式有任何疑问或顾虑,请通过[您的联系方式]与我们联系。
## 政策变更
我们可能会不时更新本隐私政策。任何更改将发布在此页面上,且我们会通知您任何重大更改。

View File

@@ -1,77 +0,0 @@
---
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,20 +0,0 @@
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))

View File

@@ -69,11 +69,51 @@
"continueReading": "Continue Reading",
"download": "Download",
"downloadDescription": "Pick the right version for you",
"downloadSwitchPrerelease": "Switch to pre-release",
"downloadSwitchRelease": "Switch to regular release",
"downloadForApple": "Looking for iOS / macOS version?",
"downloadTestFlight": "TestFlight",
"downloadTestFlightDescription": "For pre-release version",
"downloadForDesktop": "Looking for desktop version?",
"downloadForDesktopDescription": "If the release does not contain the desktop version, you can still download the latest build from GitHub Action",
"downloadWithoutDownload": "Want have a try without downloading?",
"downloadWeb": "Web",
"downloadWebChina": "with China Mainland Optimized",
"attachmentUpload": "Upload new",
"attachmentCreate": "Create Attachment",
"attachmentCreateCaption": "Use Solar Network host your files",
"attachmentUploadProgress": "Uploading",
"attachmentUploadCompleted": "Uploaded",
"upload": "Upload",
"cancel": "Cancel"
"cancel": "Cancel",
"seeMore": "See more",
"solarNetworkDescription": "A open, free, and friendly social network.",
"solarNetworkBeforeYouStart": "Before you start",
"solarNetworkBeforeYouStartDescription": "Learn some culture and basics of Solar Network",
"solarNetworkFreedomOfSpeech": "Freedom of Speech",
"solarNetworkFreedomOfSpeechDescription": "While Solar Network protects your freedom of speech and does not manually delete posts, this does not mean you are not responsible for your words. Additionally, when the 'flag the post' feature is activated, your posts will be hidden from other users. We still encourage users to prioritize harmony and minimize conflicts.",
"solarNetworkConfirmAccount": "Confirm Account",
"solarNetworkConfirmAccountDescription": "After registering, please check your bound email for the account confirmation email. Otherwise, your account will be reclaimed within 24 hours, and during this period, no permissions will be assigned, affecting most functionalities.",
"solarNetworkNoImpersonation": "No Impersonation",
"solarNetworkNoImpersonationDescription": "Do not impersonate individuals either within or outside the platform, especially those with a certain level of recognition. Regardless of intent, if it causes misunderstanding among users, we reserve the right to take action on the relevant account and content.",
"solarNetworkReadDialog": "Read Error Messages",
"solarNetworkReadDialogDescription": "When encountering an error message, do not immediately take a screenshot and complain. Try to understand why the issue occurred. Then, seek help in the development channel or on GitHub instead of making complaint posts.",
"solarNetworkToS": "And, if you continue registering, means you accept our Terms & Conditions",
"solarNetworkToSCheck": "Check them out",
"solarNetworkFeat": "Features",
"solarNetworkFeatDescription": "Explore the core features of Solar Network",
"solarNetworkFeatDashboard": "Dashboard",
"solarNetworkFeatDashboardDescription": "A single place to information around the site, anytime, anywhere.",
"solarNetworkFeatExplore": "Explore",
"solarNetworkFeatExploreDescription": "Enjoy what you love, free from ads and algorithmic noise.",
"solarNetworkFeatChat": "Chat",
"solarNetworkFeatChatDescription": "Bridge distances, stay connected with friends and communities effortlessly.",
"solarNetworkFeatNews": "News",
"solarNetworkFeatNewsDescription": "Even without traveling afar, stay informed about the world's stories.",
"solarNetworkFeatStickers": "Stickers",
"solarNetworkFeatStickersDescription": "Express yourself beyond words with playful and vivid stickers.",
"solarNetworkFeatCompose": "Compose",
"solarNetworkFeatComposeDescription": "Write freely, speak boldly—your voice deserves to be heard.",
"solarNetworkJumpIn": "Jump into the community",
"solarNetworkNeedHelp": "Need help?"
}

View File

@@ -1,55 +0,0 @@
{
"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": "咩→咩↓"
}

View File

@@ -69,11 +69,55 @@
"continueReading": "继续阅读",
"download": "下载",
"downloadDescription": "选择适合你的版本下载",
"downloadSwitchPrerelease": "切换至预发行版本",
"downloadSwitchRelease": "切换至稳定版本",
"downloadForApple": "使用 iOS / macOS 的设备?",
"downloadTestFlight": "测试飞机 (TestFlight)",
"downloadTestFlightDescription": "提供预发行版本",
"downloadForDesktop": "使用桌面设备?",
"downloadForDesktopDescription": "通常如果发行未包含桌面版本,你仍然可以从 GitHub Action 处下载最新的构建",
"downloadWithoutDownload": "想不下载尝试一下?",
"downloadWeb": "网页版",
"downloadWebChina": "中国大陆特供版本 (优化过的内容分发网络)",
"attachmentUpload": "新传附件",
"attachmentCreate": "新建附件",
"attachmentCreateCaption": "使用 Solar Network 来托管你的文件",
"attachmentUploadProgress": "上传中",
"attachmentUploadCompleted": "上传完成",
"upload": "上传",
"cancel": "取消"
"cancel": "取消",
"seeMore": "查看更多",
"solarNetworkDescription": "开放、包容、和谐",
"solarNetworkBeforeYouStart": "桥豆麻袋",
"solarNetworkBeforeYouStartDescription": "在你开始之前,了解一些关于 Solar Network 文化和常识",
"solarNetworkFreedomOfSpeech": "言论自由",
"solarNetworkFreedomOfSpeechDescription": "尽管 Solar Network 保护你的言论自由,不会手动对帖子进行删除。但是这不代表你可以对你的言论不负责。同时 Solar Network 上的「吹哨」功能生效时会对其他用户隐藏你的帖子。我们还是希望用户能以和为贵,少发生争吵。",
"solarNetworkConfirmAccount": "确认账户",
"solarNetworkConfirmAccountDescription": "在注册之后记得前往您绑定的邮件获取确认帐号的邮件,否则您的帐号会在 24 小时内被回收,并且期间不会分配权限,影响绝大部分功能使用。",
"solarNetworkNoImpersonation": "不要冒充他人",
"solarNetworkNoImpersonationDescription": "不要冒充在站内 / 站外的人物,对方有一定知名度的甚是。无论出发点如何,对用户造成了误解时我们保留权利处理相关帐号和内容。",
"solarNetworkReadDialog": "阅读错误提示",
"solarNetworkReadDialogDescription": "遇到报错提示不要第一时间截图抱怨,尝试理解为什么这件事情发生。其次在开发频道或 GitHub 寻求帮助,不要发帖抱怨。",
"solarNetworkToS": "还有,如果你继续注册 Solarpass 帐号,这意味着你同意我们的各项条款",
"solarNetworkToSCheck": "阅读这些条款",
"solarNetworkFeat": "特色功能",
"solarNetworkFeatDescription": "浏览 Solar Network 的一些核心功能",
"solarNetworkFeatDashboard": "冲浪板",
"solarNetworkFeatDashboardDescription": "一处汇聚万千动向,随时捕捉站内资讯。",
"solarNetworkFeatExplore": "探索",
"solarNetworkFeatExploreDescription": "不受广告与算法羁绊,纯粹欣赏你热爱的风景。",
"solarNetworkFeatChat": "聊天",
"solarNetworkFeatChatDescription": "超越时空阻隔,与朋友和社群自在畅谈,情感相连。",
"solarNetworkFeatNews": "新闻",
"solarNetworkFeatNewsDescription": "纵然足不出户,依然洞悉世间冷暖,知晓天下风云。",
"solarNetworkFeatStickers": "贴图",
"solarNetworkFeatStickersDescription": "一枚贴图,胜过千言万语,趣味横生,情感尽现。",
"solarNetworkFeatCompose": "撰写",
"solarNetworkFeatComposeDescription": "在无拘无束的天地间,自由书写,勇敢表达,世界在倾听。",
"solarNetworkHighlightPosts": "Solar Favorite",
"solarNetworkHighlightPostsDescription": "Solar Network 社区用户中精选出来的精华帖",
"solarNetworkJumpIn": "现在加入",
"solarNetworkNeedHelp": "需要寻求帮助?",
"askHelpContactUs": "联系我们",
"askHelpReadTheDocs": "阅读文档"
}

View File

@@ -1,17 +1,10 @@
<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>
<v-app-bar-nav-icon @click="openDrawer = !openDrawer" />
<nuxt-link to="/dev" exact>
<h2 class="mt-1">Creator Hub</h2>
<nuxt-link to="/creator" exact>
<h2>Creator Hub</h2>
</nuxt-link>
<v-spacer></v-spacer>
@@ -45,12 +38,10 @@
</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"
titleTemplate: "%s | Solsynth Creator Hub",
})
</script>

View File

@@ -1,45 +1,53 @@
<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>
<v-app-bar app flat color="surface" class="app-bar-blur">
<v-container fluid class="mx-auto d-flex align-center justify-center pr-8 relative">
<v-app-bar-nav-icon @click="openDrawer = !openDrawer" class="z-10" />
<nuxt-link to="/" exact>
<h2 class="mt-1">Solsynth LLC</h2>
<nuxt-link to="/" exact class="z-10">
<h2 v-if="isLargeScreen">Solsynth LLC</h2>
<v-icon v-else icon="mdi-home" />
</nuxt-link>
<v-spacer></v-spacer>
<locale-select />
<user-menu />
<div class="absolute left-0 right-0 flex justify-center gap-2 w-screen">
<v-btn v-if="isLargeScreen" v-for="item in navItems" :to="item.to" exact :prepend-icon="item.icon">{{
t(item.title)
}}</v-btn>
<v-menu location="bottom center" v-else>
<template v-slot:activator="{ props }">
<v-btn v-bind="props" icon="mdi-dots-horizontal-circle" slim size="small" />
</template>
<v-list nav slim class="w-[280px]">
<v-list-item v-for="item in navItems" :to="item.to" :prepend-icon="item.icon">
<v-list-item-title>{{ t(item.title) }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</div>
<v-spacer></v-spacer>
<locale-select class="z-10" />
<user-menu class="z-10" />
</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-navigation-drawer v-model="openDrawer" location="left" width="300" temporary order="-1">
<v-list density="compact" nav color="primary">
<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 my-1" />
<v-list density="compact" nav color="primary">
<v-list-item title="Code Repository" prepend-icon="mdi-git" href="https://git.solsynth.dev" target="_blank" />
</v-list>
<v-divider class="border-opacity-50 mb-4 mt-0.5" />
<copyright no-centered service="capital" class="px-5" />
<copyright no-centered :service="['roadsign', 'capital']" class="px-5" />
<footer-links class="px-5 mt-3" />
</v-navigation-drawer>
@@ -50,9 +58,45 @@
</template>
<script setup lang="ts">
import Logo from "../assets/logo-w-shadow.png"
import { useBreakpoints, breakpointsVuetifyV3 } from "@vueuse/core"
const { t } = useI18n()
const { t } = useI18n()
const openDrawer = ref(false)
const breakpoints = useBreakpoints(breakpointsVuetifyV3)
const isLargeScreen = computed(() => breakpoints.isGreaterOrEqual("md").valueOf())
interface NavItem {
icon: string
title: string
to: string
}
const navItems: NavItem[] = [
{
icon: "mdi-shape",
title: "navProducts",
to: "/products",
},
{
icon: "mdi-note-text",
title: "navPosts",
to: "/posts",
},
{
icon: "mdi-image-multiple",
title: "navGallery",
to: "/gallery",
},
]
</script>
<style lang="css" scoped>
.app-bar-blur {
-webkit-mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 1) 40%, rgba(0, 0, 0, 0.5) 65%, rgba(0, 0, 0, 0) 100%);
mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 1) 40%, rgba(0, 0, 0, 0.5) 65%, rgba(0, 0, 0, 0) 100%);
mask-repeat: no-repeat;
mask-size: 100%;
}
</style>

View File

@@ -1,17 +1,10 @@
<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>
<v-container fluid class="mx-auto d-flex align-center justify-center pr-8">
<v-app-bar-nav-icon @click="openDrawer = !openDrawer" />
<nuxt-link to="/dev" exact>
<h2 class="mt-1">Developer Portal</h2>
<h2>Developer Portal</h2>
</nuxt-link>
<v-spacer></v-spacer>
@@ -45,12 +38,12 @@
</template>
<script setup lang="ts">
import Logo from "../assets/logo-w-shadow.png"
import Logo from "~/assets/logo-w-shadow.png"
const { t } = useI18n()
const openDrawer = ref(false)
useHead({
titleTemplate: "%s | Solsynth Dev Portal"
titleTemplate: "%s | Solsynth Dev Portal",
})
</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>

View File

@@ -0,0 +1,6 @@
export default defineNuxtRouteMiddleware((to) => {
// No further supported path prefix localization
if (to.path.startsWith("/zh-CN")) {
return navigateTo(to.fullPath.replace("/zh-CN", ""))
}
})

View File

@@ -19,9 +19,7 @@ export default defineNuxtConfig({
},
posts: {
includeAppSources: false,
sources: [
"/api/sitemap/posts",
],
sources: ["/api/sitemap/posts"],
},
},
},
@@ -37,7 +35,6 @@ export default defineNuxtConfig({
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",
@@ -56,19 +53,18 @@ export default defineNuxtConfig({
},
routeRules: {
"/.well-known/openid-configuration": {
proxy: "/api/well-known/openid-configuration",
"/.well-known/**": {
proxy: "/api/well-known/**",
},
},
app: {
pageTransition: { name: "page", mode: "out-in" },
head: {
title: "Solsynth LLC",
titleTemplate: "%s | Solsynth",
meta: [],
link: [
{ rel: "icon", type: "image/png", href: "/icon.png" },
],
link: [{ rel: "icon", type: "image/png", href: "/icon.png" }],
},
},
@@ -78,11 +74,62 @@ export default defineNuxtConfig({
},
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"],
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",
@@ -96,6 +143,12 @@ export default defineNuxtConfig({
transpile: ["vuetify"],
},
umami: {
id: "eef151fb-07e2-461b-8b7f-2547aab735d4",
host: "https://us.umami.is",
autoTrack: true,
},
modules: [
"@unocss/nuxt",
"@nuxt/content",
@@ -104,7 +157,8 @@ export default defineNuxtConfig({
"@pinia/nuxt",
"@nuxtjs/i18n",
"nuxt-schema-org",
"nuxt-gtag",
"@vueuse/motion/nuxt",
"nuxt-umami",
(_options, nuxt) => {
nuxt.hooks.hook("vite:extendConfig", (config) => {
// @ts-expect-error
@@ -113,10 +167,6 @@ export default defineNuxtConfig({
},
],
gtag: {
id: "G-ZFJ7RX0JXF",
},
vite: {
vue: {
template: {

View File

@@ -11,29 +11,35 @@
},
"dependencies": {
"@mdi/font": "^7.4.47",
"@nuxt/content": "^2.13.2",
"@nuxt/image": "^1.8.0",
"@nuxtjs/i18n": "^8.5.3",
"@nuxtjs/sitemap": "^6.0.1",
"@pinia/nuxt": "^0.5.4",
"@nuxt/content": "^2.13.4",
"@nuxt/image": "^1.9.0",
"@nuxt/kit": "^3.16.0",
"@nuxtjs/i18n": "^8.5.6",
"@nuxtjs/sitemap": "^6.1.5",
"@octokit/rest": "^21.1.1",
"@pinia/nuxt": "^0.5.5",
"@vueuse/core": "^13.0.0",
"@vueuse/motion": "^3.0.3",
"feed": "^4.2.2",
"nuxt": "^3.13.2",
"nuxt": "^3.16.0",
"nuxt-gtag": "^2.1.0",
"nuxt-schema-org": "^3.4.0",
"pinia": "^2.2.2",
"nuxt-schema-org": "^3.5.0",
"nuxt-umami": "3.2.0",
"pinia": "^2.3.1",
"rehype-sanitize": "^6.0.0",
"rehype-stringify": "^10.0.0",
"rehype-stringify": "^10.0.1",
"remark": "^15.0.1",
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.0",
"remark-rehype": "^11.1.1",
"unhead": "1.9.0",
"unified": "^11.0.5",
"vue": "latest"
"vue": "^3.5.13"
},
"devDependencies": {
"@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.1"
"vite-plugin-vuetify": "^2.1.0",
"vuetify": "^3.7.16"
}
}

View File

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

View File

@@ -162,10 +162,13 @@ async function submit(evt: SubmitEvent) {
submitting.value = true
const res = await solarFetch(`/cgi/uc/stickers/packs/${route.params.id}`, {
const res = await solarFetch(`/cgi/uc/stickers/${route.params.sticker}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
body: JSON.stringify({
pack_id: parseInt(route.params.id.toString()),
...data,
}),
})
if (res.status != 200) {
error.value = await res.text()

View File

@@ -143,7 +143,7 @@ async function submit(evt: SubmitEvent) {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
pack_id: route.params.id,
pack_id: parseInt(route.params.id.toString()),
...data,
}),
})

View File

@@ -6,13 +6,7 @@
</div>
<div class="flex gap-2">
<v-btn
color="primary"
text="New"
append-icon="mdi-plus"
variant="tonal"
to="/creator/stickers/new"
/>
<v-btn color="primary" text="New" append-icon="mdi-plus" variant="tonal" to="/creator/stickers/new" />
</div>
</div>
@@ -24,10 +18,7 @@
<div class="mt-5">
<v-expansion-panels>
<v-expansion-panel
v-for="item in data"
:key="'sticker-pack#'+item.id"
>
<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>
@@ -87,16 +78,17 @@
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
text="Cancel"
color="grey"
@click="isActive.value = false"
></v-btn>
<v-btn text="Cancel" color="grey" @click="isActive.value = false"></v-btn>
<v-btn
text="Delete"
color="error"
@click="() => { deletePack(item); isActive.value = false }"
@click="
() => {
deletePack(item)
isActive.value = false
}
"
/>
</v-card-actions>
</v-card>
@@ -131,6 +123,7 @@ useHead({
})
const { t } = useI18n()
const ua = useUserinfo()
const loading = ref(false)
const error = ref<null | string>(null)
@@ -140,7 +133,7 @@ const data = ref<any[]>([])
async function readPacks() {
loading.value = true
const res = await solarFetch(`/cgi/uc/stickers/packs?take=10&offset=${data.value.length}`)
const res = await solarFetch(`/cgi/uc/stickers/packs?take=10&author=${ua.userinfo?.id}&offset=${data.value.length}`)
if (res.status != 200) {
error.value = await res.text()
} else {

View File

@@ -1,68 +1,79 @@
<template>
<v-container class="content-container mx-auto">
<div class="mt-3 mb-4.5 mx-[2.5ch] flex flex-row gap-4 items-center">
<nuxt-link :to="`/users/${attachment.account?.name}`">
<v-avatar :image="attachment.account?.avatar" />
</nuxt-link>
<div class="flex flex-col">
<span class="text-xs">Uploaded by</span>
<span>{{ attachment.account?.nick }} <span class="text-xs">@{{ attachment.account?.name }}</span></span>
<v-row class="h-[calc(100vh-24px)]" no-gutters>
<v-col cols="12" md="8">
<div class="h-full w-full flex justify-center items-center" :class="isMediumScreen ? 'flex-row' : 'flex-col'">
<div class="flex-grow-1 w-full">
<attachment-renderer :item="attachment" no-cover />
</div>
<v-divider v-if="isMediumScreen" vertical />
<v-divider v-else />
</div>
</div>
</v-col>
<v-col cols="12" md="4" class="px-5 pt-3">
<v-card class="mb-5">
<v-card-text class="flex flex-col gap-4">
<div class="flex flex-col" v-if="attachment?.alt">
<span class="text-xs font-bold">Alternative</span>
<span class="text-truncate">{{ attachment?.alt }}</span>
</div>
<div class="flex flex-col">
<span class="text-xs font-bold">Original File Name</span>
<span class="text-truncate">{{ attachment?.name }}</span>
</div>
<div class="flex flex-col">
<span class="text-xs font-bold">Size</span>
<span>{{ formatBytes(attachment?.size) }}</span>
</div>
<div class="flex flex-col" v-if="attachment?.metadata?.ratio">
<span class="text-xs font-bold">Aspect Ratio</span>
<span>
{{ attachment?.metadata?.width }}x{{ attachment?.metadata?.height }}
{{ attachment?.metadata?.ratio.toFixed(2) }}
</span>
</div>
<div class="flex flex-col" v-if="attachment?.mimetype">
<span class="text-xs font-bold">Mimetype</span>
<span>{{ attachment?.mimetype }}</span>
</div>
<div class="flex flex-col">
<span class="text-xs font-bold">Raw Data</span>
<v-code class="font-mono mt-1">{{ JSON.stringify(attachment.metadata, null, 4) }}</v-code>
</div>
</v-card-text>
</v-card>
<h2 class="section-header">Preview</h2>
<v-card class="mb-5">
<attachment-renderer :item="attachment" />
</v-card>
<h2 class="section-header">Metadata</h2>
<v-card class="mb-5">
<v-card-text class="flex flex-col gap-4">
<div class="flex flex-col" v-if="attachment?.alt">
<span class="text-xs font-bold">Alternative</span>
<span class="text-truncate">{{ attachment?.alt }}</span>
</div>
<div class="flex flex-col">
<span class="text-xs font-bold">Original File Name</span>
<span class="text-truncate">{{ attachment?.name }}</span>
</div>
<div class="flex flex-col">
<span class="text-xs font-bold">Size</span>
<span>{{ formatBytes(attachment?.size) }}</span>
</div>
<div class="flex flex-col" v-if="attachment?.metadata?.ratio">
<span class="text-xs font-bold">Aspect Ratio</span>
<span>
{{ attachment?.metadata?.width }}x{{ attachment?.metadata?.height }}
{{ attachment?.metadata?.ratio.toFixed(2) }}
</span>
</div>
<div class="flex flex-col" v-if="attachment?.mimetype">
<span class="text-xs font-bold">Mimetype</span>
<span>{{ attachment?.mimetype }}</span>
</div>
<div class="flex flex-col">
<span class="text-xs font-bold">Raw Data</span>
<v-code class="font-mono mt-1">{{ JSON.stringify(attachment.metadata, null, 4) }}</v-code>
</div>
</v-card-text>
</v-card>
<div class="text-xs text-grey flex flex-col mx-[2.5ch]">
<span>Solar Network Attachment Web Preview</span>
<span>Powered by <a class="underline" target="_blank" href="https://git.solsynth.dev/Hydrogen/Paperclip">Hydrogen.Paperclip</a></span>
</div>
</v-container>
<div class="text-xs text-grey flex flex-col mx-[2.5ch]">
<span>Solar Network Attachment Web Preview</span>
<span
>Powered by
<a class="underline" target="_blank" href="https://git.solsynth.dev/HyperNet/Paperclip"
>HyperNet.Paperclip</a
></span
>
</div>
</v-col>
</v-row>
</template>
<script setup lang="ts">
import { formatBytes } from "~/utils/format"
import { useDisplay } from "vuetify"
const route = useRoute()
const config = useRuntimeConfig()
const firstImage = ref<string | null>()
const firstVideo = ref<string | null>()
const { data: attachment } = await useFetch<any>(`${config.public.solarNetworkApi}/cgi/uc/attachments/${route.params.id}/meta`)
const isMediumScreen = useDisplay().mdAndUp
const { data: attachment } = await useFetch<any>(
`${config.public.solarNetworkApi}/cgi/uc/attachments/${route.params.id}/meta`,
)
definePageMeta({
layout: "minimal",
})
if (!attachment.value) {
throw createError({
@@ -71,17 +82,21 @@ if (!attachment.value) {
})
}
const title = computed(() => `Attachment from ${attachment.value.account.nick}`)
const title = computed(() => `Attachment ${attachment.value?.id}`)
watch(attachment, (value) => {
if (value.mimetype.split("/")[0] == "image") {
firstImage.value = `${config.public.solarNetworkApi}/cgi/uc/attachments/${value.id}`
}
watch(
attachment,
(value) => {
if (value.mimetype.split("/")[0] == "image") {
firstImage.value = `${config.public.solarNetworkApi}/cgi/uc/attachments/${value.id}`
}
if (value.mimetype.split("/")[0] == "video") {
firstVideo.value = `${config.public.solarNetworkApi}/cgi/uc/attachments/${value.id}`
}
}, { immediate: true, deep: true })
if (value.mimetype.split("/")[0] == "video") {
firstVideo.value = `${config.public.solarNetworkApi}/cgi/uc/attachments/${value.id}`
}
},
{ immediate: true, deep: true },
)
useHead({
title: title.value,
@@ -93,7 +108,6 @@ useHead({
})
useSeoMeta({
author: attachment.value?.account.nick,
title: title,
description: attachment.value?.alt,
ogTitle: title,
@@ -104,30 +118,4 @@ useSeoMeta({
publisher: "Solar Network",
ogSiteName: "Solsynth Capital",
})
function formatBytes(bytes: number, decimals = 2) {
if (!+bytes) return "0 Bytes"
const k = 1024
const dm = decimals < 0 ? 0 : decimals
const sizes = ["Bytes", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"]
const i = Math.floor(Math.log(bytes) / Math.log(k))
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`
}
</script>
<style scoped>
.content-container {
max-width: 70ch !important;
}
.section-header {
margin-left: 2.5ch;
margin-right: 2.5ch;
margin-bottom: 8px;
@apply text-lg;
}
</style>

View File

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

View File

@@ -1,39 +1,74 @@
<template>
<canvas ref="canvasRef" class="fixed top-0 left-0 w-screen h-screen opacity-50"></canvas>
<v-container class="flex flex-col my-2 px-12 gap-[4rem]">
<v-row class="content-section">
<v-col cols="12" md="4" class="flex justify-start">
<div class="flex flex-col items-start">
<h1 class="text-4xl font-bold">{{ t("brandName") }}</h1>
<p class="text-lg mt-3 max-w-2/3">
{{ t("indexIntroduce") }}
</p>
<p class="text-grey mt-2">
{{ t("indexProductListHint") }}
<v-icon icon="mdi-arrow-right" size="16" class="mb-0.5" />
</p>
</div>
</v-col>
<v-col cols="12" md="8">
<v-card>
<section class="content-section flex flex-col items-center justify-center text-center px-4">
<img
v-motion="{
initial: {
y: 100,
opacity: 0,
},
enter: {
y: 0,
opacity: 1,
transition: { duration: 0.8 },
},
}"
:src="Logo"
alt="Company Logo"
class="w-32 h-32 mb-4"
/>
<h1 class="text-4xl font-bold">Welcome to {{ t("brandName") }}</h1>
<p class="mt-2 text-lg">Building cool, open-source, and elegant apps for human.</p>
<v-btn class="mt-4" color="primary" prepend-icon="mdi-arrow-down" href="#products">{{ t("learnMore") }}</v-btn>
</section>
<section class="content-section py-16" id="products">
<div class="container mx-auto text-center">
<h2 class="text-3xl font-bold">Our Projects</h2>
<p>Take a peek of our works.</p>
<v-card class="mt-12">
<product-carousel class="carousel-section" :products="products as any[]" />
</v-card>
</v-col>
</v-row>
</div>
</section>
<v-row class="content-section">
<v-col cols="12" md="8">
<v-card class="max-h-[500px]">
<activity-carousel class="carousel-section" />
<v-col cols="12" md="6">
<v-card>
<v-list>
<v-list-item
title="GitHub"
subtitle="The place hosts most of our public projects' code"
prepend-icon="mdi-github"
href="https://github.com/Solsynth"
target="_blank"
/>
<v-list-item
lines="two"
title="Solsynth Code Repository"
subtitle="Our self-hosted git server, may contains some unpublished projects' code"
prepend-icon="mdi-git"
href="https://git.solsynth.dev/explore"
target="_blank"
/>
</v-list>
</v-card>
</v-col>
<v-col cols="12" md="4" class="flex justify-end" order="first" order-md="last">
<v-col cols="12" md="6" class="flex justify-end" order="first" order-md="last">
<div class="text-right flex flex-col items-end">
<h2 class="text-4xl font-bold">{{ t("indexActivities") }}</h2>
<p class="text-lg mt-3 max-w-2/3">
{{ t("indexActivitiesCaption") }}
<h2 class="text-4xl font-bold">
We<br />
❤️ Open-source
</h2>
<p class="text-md mt-3 max-w-2/3">
No software can run without the support of open source software, and our software is no exception.
Therefore, we feel it is important to contribute to open source as well.
</p>
<p class="text-grey mt-2">
<v-icon icon="mdi-arrow-left" size="16" class="mb-0.5" />
{{ t("indexActivitiesHint") }}
Check out our GitHub
</p>
</div>
</v-col>
@@ -42,6 +77,8 @@
</template>
<script setup lang="ts">
import Logo from "~/assets/logo-w-shadow.png"
import { getLocale } from "~/utils/locale"
const { t } = useI18n()
@@ -61,13 +98,100 @@ useSeoMeta({
})
const { data: products } = await useAsyncData("products", () => {
return queryContent("/products").where({ _locale: getLocale(), archived: { $ne: true } }).limit(5).find()
return queryContent("/products")
.where({ _locale: getLocale(), archived: { $ne: true } })
.limit(5)
.find()
})
const canvasRef = ref(null)
onMounted(() => {
const isDarkMode = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches
const canvas: HTMLCanvasElement = canvasRef.value!
const ctx = canvas.getContext("2d")!
const dpr = window.devicePixelRatio || 1
canvas.width = window.innerWidth * dpr
canvas.height = window.innerHeight * dpr
let particles: Particle[] = []
const numParticles = 100
class Particle {
x: number
y: number
vx: number
vy: number
size: number
constructor() {
this.x = Math.random() * canvas.width
this.y = Math.random() * canvas.height
this.vx = (Math.random() - 0.5) * 1.5
this.vy = (Math.random() - 0.5) * 1.5
this.size = Math.random() * 3 + 1
}
move() {
this.x += this.vx
this.y += this.vy
if (this.x <= 0 || this.x >= canvas.width) this.vx *= -1
if (this.y <= 0 || this.y >= canvas.height) this.vy *= -1
}
draw() {
ctx.beginPath()
ctx.arc(this.x * dpr, this.y * dpr, this.size * dpr, 0, Math.PI * 2)
ctx.fillStyle = isDarkMode ? "rgba(255, 255, 255, 0.8)" : "rgba(0, 0, 0, 0.8)"
ctx.fill()
}
}
function init() {
particles = []
for (let i = 0; i < numParticles; i++) {
particles.push(new Particle())
}
}
function drawLines() {
for (let i = 0; i < particles.length; i++) {
for (let j = i + 1; j < particles.length; j++) {
let dx = particles[i].x - particles[j].x
let dy = particles[i].y - particles[j].y
let distance = Math.sqrt(dx * dx + dy * dy)
if (distance < 100) {
ctx.beginPath()
ctx.moveTo(particles[i].x * dpr, particles[i].y * dpr)
ctx.lineTo(particles[j].x * dpr, particles[j].y * dpr)
ctx.strokeStyle = isDarkMode ? "rgba(255, 255, 255, 0.2)" : "rgba(0, 0, 0, 0.2)"
ctx.lineWidth = 0.5 * dpr
ctx.stroke()
}
}
}
}
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height)
particles.forEach((p) => {
p.move()
p.draw()
})
drawLines()
requestAnimationFrame(animate)
}
init()
animate()
})
</script>
<style scoped>
.carousel-section {
height: 96rem;
height: 120rem;
}
.content-section {
@@ -76,3 +200,10 @@ const { data: products } = await useAsyncData("products", () => {
place-items: center;
}
</style>
<style>
body,
html {
scroll-behavior: smooth;
}
</style>

View File

@@ -1,14 +1,19 @@
<template>
<v-container class="content-container mx-auto">
<div class="my-3 flex flex-row gap-4">
<nuxt-link :to="`/users/${post.author?.name}`">
<v-avatar :image="post.author?.avatar" />
<nuxt-link :to="`/publishers/${post.publisher?.name}`">
<v-avatar :image="getAttachmentUrl(post.publisher?.avatar)" />
</nuxt-link>
<div class="flex flex-col">
<span>{{ post.author?.nick }} <span class="text-xs">@{{ post.author?.name }}</span></span>
<span>
{{ post.publisher?.nick }}
<span class="text-xs">@{{ post.publisher?.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>
<span v-if="!post.body?.title && !post.body?.description" class="text-sm">{{
post.publisher?.description
}}</span>
</div>
</div>
@@ -20,22 +25,18 @@
/>
</v-card>
<article class="text-base prose xl:text-lg mx-auto">
<article v-if="post.body?.content" class="text-base prose xl:text-lg mx-auto">
<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 :attachments="post.body?.attachments" @update:metadata="args => attachments = args" />
<attachment-carousel :attachments="post.body?.attachments" @update:metadata="(args) => (attachments = args)" />
</v-card>
<div class="mb-3 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> {{ 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
@@ -43,10 +44,7 @@
</span>
</div>
<div
v-if="post.tags?.length > 0"
class="text-xs text-grey flex flex-row gap-1 mb-3"
>
<div v-if="post.tags?.length > 0" class="text-xs text-grey flex flex-row gap-1 mb-3">
<nuxt-link
v-for="tag in post.tags"
:to="`/posts/tags/${tag.alias}`"
@@ -71,10 +69,6 @@
</template>
<script setup lang="ts">
definePageMeta({
alias: ["/posts/:area/:id"],
})
const route = useRoute()
const config = useRuntimeConfig()
@@ -82,17 +76,9 @@ const attachments = ref<any[]>([])
const firstImage = ref<string | null>()
const firstVideo = ref<string | null>()
const slug = computed(() => {
if (route.params.area) {
return `${route.params.area}:${route.params.id}`
} else {
return route.params.id
}
})
const { t } = useI18n()
const { data: post } = await useFetch<any>(`${config.public.solarNetworkApi}/cgi/co/posts/${slug.value}`)
const { data: post } = await useFetch<any>(`${config.public.solarNetworkApi}/cgi/co/posts/${route.params.id}`)
if (!post.value) {
const { data: publisher } = await $fetch<any>(`${config.public.solarNetworkApi}/cgi/co/publishers/${route.params.id}`)
@@ -108,21 +94,29 @@ if (!post.value) {
navigateTo(`/posts/${post.value.area_alias}/${post.value.alias}`)
}
const title = computed(() => post.value.body?.title ? `${post.value.body?.title} by @${post.value.author.name}` : `Post by @${post.value.author.name}`)
const title = computed(() =>
post.value.body?.title
? `${post.value.body?.title} by @${post.value.publisher.name}`
: `Post by @${post.value.publisher.name}`,
)
const description = computed(() => post.value.body?.description ?? post.value.body?.content.substring(0, 280).trim())
watch(attachments, (value) => {
if (post.value.body?.thumbnail) {
firstImage.value = `${config.public.solarNetworkApi}/cgi/uc/attachments/${post.value.body?.thumbnail}`
}
if (value.length > 0 && value[0].mimetype.split("/")[0] == "image") {
firstImage.value = `${config.public.solarNetworkApi}/cgi/uc/attachments/${attachments.value[0].rid}`
}
watch(
attachments,
(value) => {
if (post.value.body?.thumbnail) {
firstImage.value = `${config.public.solarNetworkApi}/cgi/uc/attachments/${post.value.body?.thumbnail}`
}
if (value.length > 0 && value[0].mimetype.split("/")[0] == "image") {
firstImage.value = `${config.public.solarNetworkApi}/cgi/uc/attachments/${attachments.value[0].rid}`
}
if (value.length > 0 && value[0].mimetype.split("/")[0] == "video") {
firstVideo.value = `${config.public.solarNetworkApi}/cgi/uc/attachments/${attachments.value[0].rid}`
}
}, { immediate: true, deep: true })
if (value.length > 0 && value[0].mimetype.split("/")[0] == "video") {
firstVideo.value = `${config.public.solarNetworkApi}/cgi/uc/attachments/${attachments.value[0].rid}`
}
},
{ immediate: true, deep: true },
)
useHead({
title: title.value,
@@ -134,7 +128,7 @@ useHead({
})
useSeoMeta({
author: post.value.author.nick,
author: post.value.publisher.nick,
title: title,
articlePublishedTime: post.value.publishedAt,
description: description,
@@ -148,7 +142,7 @@ useSeoMeta({
ogSiteName: "Solsynth Capital",
})
const externalOpenLink = computed(() => `${config.public.solianUrl}/posts/view/${slug.value}`)
const externalOpenLink = computed(() => `${config.public.solianUrl}/posts/${route.params.id.toString().replace('/', ':')}`)
</script>
<style scoped>

View File

@@ -0,0 +1,436 @@
<template>
<v-container class="flex flex-col my-2 px-12 gap-[4rem]">
<section class="content-section flex flex-col items-center justify-center text-center" id="intro">
<div class="pt-1/3 mb-4 w-full relative">
<img :src="AlphaScreenshot" class="absolute bottom-2 left-0 right-0" />
<img
v-motion="{
initial: {
y: 100,
opacity: 0,
},
enter: {
y: 0,
opacity: 1,
transition: { duration: 0.8 },
},
}"
:src="Icon"
alt="Solar Network Logo"
class="w-32 h-32 p-2 z-10 mx-auto icon-glow bg-white dark:bg-black shadow-2xl rounded-xl"
/>
</div>
<div>
<h1 class="text-4xl font-bold">Solar Network</h1>
<p class="mt-2 text-lg">{{ t("solarNetworkDescription") }}</p>
<v-btn class="mt-4" color="primary" prepend-icon="mdi-arrow-down" href="#features">{{ t("learnMore") }}</v-btn>
</div>
</section>
<section class="content-section flex flex-col items-center justify-center text-center" id="features">
<h2 class="text-3xl font-bold">{{ t("solarNetworkFeat") }}</h2>
<p class="text-lg mb-4">{{ t("solarNetworkFeatDescription") }}</p>
<v-card class="w-full">
<v-tabs v-model="featuresTab" align-tabs="center" color="primary">
<v-tab
:prepend-icon="feat.icon"
:text="feat.title"
:value="feat.icon"
v-for="feat in features"
@mouseover="featuresTab = feat.icon"
/>
</v-tabs>
<v-tabs-window v-model="featuresTab">
<v-tabs-window-item :value="feat.icon" v-for="feat in features">
<v-card flat>
<v-img :aspect-ratio="16 / 9" :src="feat.image" cover></v-img>
<v-card-text>
<p class="text-lg mb-1">
{{ feat.description }}
</p>
</v-card-text>
</v-card>
</v-tabs-window-item>
</v-tabs-window>
</v-card>
</section>
<section class="content-section flex flex-col items-center justify-center" id="highlight-posts">
<v-row class="w-full" dense>
<v-col cols="12" md="6">
<div
class="max-h-[500px] overflow-y-auto posts-container"
ref="highlight-posts"
v-if="highlightPosts.status.value === 'success'"
>
<div v-for="post in highlightPosts.data.value">
<post-item :post="post" force-show-content class="mx-0" />
</div>
</div>
<v-progress-circular v-else indeterminate />
</v-col>
<v-col cols="12" md="6" class="text-right">
<h2 class="text-3xl font-bold">{{ t("solarNetworkHighlightPosts") }}<sup>®</sup></h2>
<p>{{ t("solarNetworkHighlightPostsDescription") }}</p>
<v-btn variant="text" color="white" slim prepend-icon="mdi-plus" href="#reminders">{{
t("solarNetworkJumpIn")
}}</v-btn>
</v-col>
</v-row>
</section>
<section class="content-section flex flex-col items-center justify-center text-center" id="reminders">
<h2 class="text-3xl font-bold">{{ t("solarNetworkBeforeYouStart") }}</h2>
<p class="text-lg">{{ t("solarNetworkBeforeYouStartDescription") }}</p>
<div class="max-h-[500px] w-full mt-4 text-left">
<v-row dense>
<v-col cols="12" md="4">
<v-card :title="t('solarNetworkFreedomOfSpeech')" prepend-icon="mdi-account-voice" density="comfortable">
<v-card-text>{{ t("solarNetworkFreedomOfSpeechDescription") }}</v-card-text>
</v-card>
</v-col>
<v-col cols="12" md="4">
<v-card :title="t('solarNetworkConfirmAccount')" prepend-icon="mdi-account-check" density="comfortable">
<v-card-text>{{ t("solarNetworkConfirmAccountDescription") }}</v-card-text>
</v-card>
<v-card
:title="t('solarNetworkNoImpersonation')"
prepend-icon="mdi-account-cancel"
density="comfortable"
class="mt-2"
>
<v-card-text>{{ t("solarNetworkNoImpersonationDescription") }}</v-card-text>
</v-card>
</v-col>
<v-col cols="12" md="4">
<v-card :title="t('solarNetworkReadDialog')" prepend-icon="mdi-alert-circle" density="comfortable">
<v-card-text>{{ t("solarNetworkReadDialogDescription") }}</v-card-text>
</v-card>
</v-col>
</v-row>
</div>
<p class="text-sm mt-4">{{ t("solarNetworkToS") }}</p>
<nuxt-link class="underline text-sm" to="/terms">{{ t("solarNetworkToSCheck") }}</nuxt-link>
</section>
<section class="content-section flex flex-col items-center justify-center text-center" id="downloads">
<h3 class="text-3xl font-bold">{{ t("download") }}</h3>
<p class="text-lg">
File-hosting & versioning by
<nuxt-link class="underline" to="https://github.com/Solsynth/HyperNet.Surface" target="_blank">GitHub</nuxt-link
><sup>®</sup>
</p>
<v-btn
v-if="hasPrerelease"
slim
density="compact"
prepend-icon="mdi-beta"
variant="text"
style="text-transform: none"
color="white"
@click="showPrerelease = !showPrerelease"
>
{{ showPrerelease ? t("downloadSwitchRelease") : t("downloadSwitchPrerelease") }}
</v-btn>
<div class="w-full mt-4 text-left">
<v-row dense class="flex-1">
<v-col cols="12" md="6">
<v-card
prepend-icon="mdi-alert-decagram"
:title="showPrerelease ? 'Latest pre-release' : 'Latest release'"
density="comfortable"
>
<v-card-text v-if="currentRelease.status.value === 'success'">
<p class="text-xs">
<code>{{ currentRelease.data.value?.tag_name }}</code>
</p>
<p class="font-bold text-lg">{{ latestRelease.data.value?.name }}</p>
<article class="prose prose-sm max-h-[360px] overflow-y-auto" style="max-width: unset">
<m-d-c :value="currentRelease.data.value!.body!" />
</article>
</v-card-text>
<div v-else>
<v-progress-circular class="px-5 my-3" indeterminate />
</div>
</v-card>
</v-col>
<v-col cols="12" md="6">
<v-card prepend-icon="mdi-download" title="Distributions" density="comfortable">
<div v-if="currentRelease.status.value === 'success'">
<v-list density="comfortable" slim>
<v-list-item
v-for="asset in currentRelease.data.value!.assets"
:key="asset.id"
:title="asset.label ?? asset.name"
:subtitle="formatBytes(asset.size)"
:href="asset.browser_download_url"
target="_blank"
/>
</v-list>
</div>
<div v-else>
<v-progress-circular class="px-5 my-3" indeterminate />
</div>
<v-card-text>
<p class="text-sm opacity-50 mb-2">{{ t("downloadForApple") }}</p>
<div class="flex align-center gap-2.5">
<nuxt-link
to="https://apps.apple.com/us/app/solian/id6499032345?itscg=30200&itsct=apps_box_link&mttnsubad=6499032345"
target="_blank"
>
<img :src="AppStoreDownload" />
</nuxt-link>
<div>
<nuxt-link to="https://testflight.apple.com/join/YJ0lmN6O" target="_blank" class="underline">
{{ t("downloadTestFlight") }}
</nuxt-link>
<p class="text-xs opacity-40">{{ t("downloadTestFlightDescription") }}</p>
</div>
</div>
<p class="text-sm opacity-50 mt-4">{{ t("downloadForDesktop") }}</p>
<p class="text-sm">{{ t("downloadForDesktopDescription") }}</p>
<p class="text-sm opacity-50 mt-4">{{ t("downloadWithoutDownload") }}</p>
<div class="text-sm flex gap-2 underline">
<nuxt-link to="https://sn.solsynth.dev" target="_blank">{{ t("downloadWeb") }}</nuxt-link>
<nuxt-link to="https://sn.solsynth.dev?cdn=cn" target="_blank">{{ t("downloadWebChina") }}</nuxt-link>
</div>
</v-card-text>
</v-card>
</v-col>
</v-row>
</div>
</section>
<section class="content-section flex flex-col items-center justify-center" id="help">
<h2 class="text-2xl font-bold text-center mb-4">{{ t("solarNetworkNeedHelp") }}</h2>
<div class="flex flex-col gap-2 w-[480px] max-w-screen">
<v-card :title="t('askHelpContactUs')" prepend-icon="mdi-email-fast" density="comfortable">
<v-card-text>
Contact our customer server at
<nuxt-link to="mailto:lily@solsynth.dev" class="underline">
<address>lily@solsynth.dev</address>
</nuxt-link>
</v-card-text>
</v-card>
<v-card :title="t('askHelpReadTheDocs')" prepend-icon="mdi-page-next" density="comfortable">
<v-card-text class="flex flex-col">
<nuxt-link to="https://kb.solsynth.dev" class="underline">Visit Goatpedia</nuxt-link>
<nuxt-link to="https://github.com/Solsynth/HyperNet.Surface/issues" class="underline"
>Visit GitHub Issue Tracker</nuxt-link
>
</v-card-text>
</v-card>
</div>
</section>
<copyright :service="['capital', 'roadsign']" />
</v-container>
</template>
<script lang="ts" setup>
import Icon from "~/assets/products/solar-network/icon.png"
import AlphaScreenshot from "~/assets/products/solar-network/alpha.webp"
import ScreenshotDashboard from "~/assets/products/solar-network/ft-dashboard.png"
import ScreenshotExplore from "~/assets/products/solar-network/ft-explore.png"
import ScreenshotChat from "~/assets/products/solar-network/ft-chat.png"
import ScreenshotNews from "~/assets/products/solar-network/ft-news.png"
import ScreenshotStickers from "~/assets/products/solar-network/ft-stickers.png"
import ScreenshotCompose from "~/assets/products/solar-network/ft-posting.png"
import AppStoreDownload from "~/assets/products/app-store-download.svg"
useHead({
title: "Solar Network",
})
import { formatBytes } from "~/utils/format"
import { Octokit } from "@octokit/rest"
const { t } = useI18n()
const featuresTab = ref()
interface FeatureItem {
title: string
description: string
icon: string
image: string
}
const features: FeatureItem[] = [
{
title: t("solarNetworkFeatDashboard"),
description: t("solarNetworkFeatDashboardDescription"),
icon: "mdi-view-dashboard",
image: ScreenshotDashboard,
},
{
title: t("solarNetworkFeatExplore"),
description: t("solarNetworkFeatExploreDescription"),
icon: "mdi-compass",
image: ScreenshotExplore,
},
{
title: t("solarNetworkFeatChat"),
description: t("solarNetworkFeatChatDescription"),
icon: "mdi-chat",
image: ScreenshotChat,
},
{
title: t("solarNetworkFeatNews"),
description: t("solarNetworkFeatNewsDescription"),
icon: "mdi-newspaper",
image: ScreenshotNews,
},
{
title: t("solarNetworkFeatStickers"),
description: t("solarNetworkFeatStickersDescription"),
icon: "mdi-sticker",
image: ScreenshotStickers,
},
{
title: t("solarNetworkFeatCompose"),
description: t("solarNetworkFeatComposeDescription"),
icon: "mdi-pencil",
image: ScreenshotCompose,
},
]
const latestRelease = useAsyncData("sn-latest-release", async () => {
const octo = new Octokit({})
const resp = await octo.repos.getLatestRelease({
owner: "Solsynth",
repo: "HyperNet.Surface",
})
return resp.data
})
const latestPrerelease = useAsyncData("sn-latest-prerelease", async () => {
const octo = new Octokit({})
const resp = await octo.repos.listReleases({
owner: "Solsynth",
repo: "HyperNet.Surface",
per_page: 1,
})
return resp.data[0]
})
const highlightPosts = useAsyncData("sn-highlight-posts", async () => {
const resp = await solarFetch("/cgi/co/recommendations")
const data = await resp.json()
return data
})
const showPrerelease = ref(false)
const currentRelease = computed(() => (showPrerelease.value ? latestPrerelease : latestRelease))
const hasPrerelease = computed<boolean>(
() => latestPrerelease.data?.value?.tag_name != latestRelease.data?.value?.tag_name,
)
const highlightPostContainer = useTemplateRef("highlight-posts")
function autoScroll() {
console.log("Auto scroll is called.")
const scrollSpeed = 1
let animationFrameId: number
let isScrolling = true
function scroll() {
if (!isScrolling) return
const container = highlightPostContainer.value!
if (container.scrollTop + container.clientHeight >= container.scrollHeight) {
container.scroll(0, 0)
} else {
container.scrollBy(0, scrollSpeed)
}
animationFrameId = requestAnimationFrame(scroll)
}
scroll()
const container = highlightPostContainer.value!
container.addEventListener("mouseenter", () => {
isScrolling = false
cancelAnimationFrame(animationFrameId)
})
container.addEventListener("mouseleave", () => {
if (!isScrolling) {
isScrolling = true
scroll()
}
})
}
watch(
highlightPostContainer,
(data) => {
if (data != null) {
autoScroll()
}
},
{ immediate: true, deep: true },
)
</script>
<style scoped>
.posts-container {
padding-top: 20px;
padding-bottom: 20px;
position: relative;
height: 500px;
overflow: hidden;
scrollbar-width: none;
mask-image: linear-gradient(
to bottom,
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 1) 20%,
rgba(0, 0, 0, 1) 80%,
rgba(0, 0, 0, 0) 100%
);
-webkit-mask-image: linear-gradient(
to bottom,
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 1) 20%,
rgba(0, 0, 0, 1) 80%,
rgba(0, 0, 0, 0) 100%
);
}
.posts-container::-webkit-scrollbar {
display: none;
}
.content-section {
min-height: calc(100vh - 80px);
height: auto;
display: flex;
place-items: center;
}
.icon-glow {
-webkit-filter: drop-shadow(0 0 7px rgba(0, 0, 0, 0.5));
filter: drop-shadow(0 0 7px rgba(0, 0, 0, 0.5));
}
@media (prefers-color-scheme: dark) {
.icon-glow {
-webkit-filter: invert() drop-shadow(0 0 7px rgba(255, 255, 255, 0.5));
filter: invert() drop-shadow(0 0 7px rgba(255, 255, 255, 0.5));
}
}
</style>
<style>
body,
html {
scroll-behavior: smooth;
}
</style>

View File

@@ -0,0 +1,69 @@
<template>
<v-container class="mx-auto">
<v-img v-if="urlOfBanner" :src="urlOfBanner" :aspect-ratio="16 / 5" class="rounded-md mb-3" cover />
<div class="mx-[2.5ch]">
<div class="my-5 mx-4 flex flex-row gap-4">
<v-avatar :image="urlOfAvatar" />
<div class="flex flex-col">
<span
>{{ account?.nick }} <span class="text-xs">@{{ account?.name }}</span></span
>
<span class="text-sm">{{ account?.description }}</span>
</div>
</div>
<div class="mb-7">
<v-card rounded="xl" class="mx-[-5px]">
<v-tabs v-model="tab" align-tabs="start" color="primary" hide-slider>
<v-tab :value="1">{{ t("userActivity") }}</v-tab>
</v-tabs>
</v-card>
</div>
<v-row>
<v-col row="12" lg="8">
<post-list class="mx-[-2.5ch] mt-[-16px]" v-if="account" :author="account.name" />
</v-col>
<v-col row="12" lg="4" order="first" order-lg="last">
<div class="sticky top-0 h-fit">
<v-card prepend-icon="mdi-information-outline" title="About">
<v-card-text>
<p><b>Description</b></p>
<p>{{ account.description }}</p>
<p class="mt-3"><b>Joined At</b></p>
<p>{{ new Date(account.created_at).toLocaleString() }}</p>
</v-card-text>
</v-card>
</div>
</v-col>
</v-row>
</div>
</v-container>
</template>
<script setup lang="ts">
const { t } = useI18n()
const route = useRoute()
const config = useRuntimeConfig()
const tab = ref(1)
const { data: account } = await useFetch<any>(`${config.public.solarNetworkApi}/cgi/co/publishers/${route.params.name}`)
if (account.value == null) {
throw createError({
statusCode: 404,
statusMessage: "User Not Found",
})
}
const urlOfAvatar = computed(() =>
account.value?.avatar ? `${config.public.solarNetworkApi}/cgi/uc/attachments/${account.value.avatar}` : void 0,
)
const urlOfBanner = computed(() =>
account.value?.banner ? `${config.public.solarNetworkApi}/cgi/uc/attachments/${account.value.banner}` : void 0,
)
const externalOpenLink = computed(() => `${config.public.solianUrl}/accounts/view/${route.params.name}`)
</script>

View File

@@ -6,34 +6,34 @@
<div class="my-5 mx-4 flex flex-row gap-4">
<v-avatar :image="urlOfAvatar" />
<div class="flex flex-col">
<span>{{ account?.nick }} <span class="text-xs">@{{ account?.name }}</span></span>
<span class="text-sm">{{ account?.description }}</span>
<span
>{{ account?.nick }} <span class="text-xs">@{{ account?.name }}</span></span
>
<p>
{{ accountStatus.status ? accountStatus.status.label : accountStatus["is_online"] ? "Online" : "Offline" }}
</p>
</div>
</div>
<div class="mb-7">
<v-card rounded="xl" class="mx-[-5px]">
<v-tabs
v-model="tab"
align-tabs="start"
color="primary"
hide-slider
>
<v-tab :value="1">{{ t("userActivity") }}</v-tab>
</v-tabs>
</v-card>
</div>
<v-row>
<v-col row="12" lg="8">
<post-list class="mx-[-2.5ch] mt-[-16px]" v-if="account" :author="account.name" />
<v-col cols="12" lg="8">
<v-card>
<v-card-text v-if="accountPageStatus.valueOf() === 'success'">
<div class="prose prose-sm" style="max-width: unset">
<m-d-c :value="accountPage.content" />
</div>
</v-card-text>
<v-card-text v-else>
<p class="font-italic">The user has no account page.</p>
</v-card-text>
</v-card>
</v-col>
<v-col row="12" lg="4" order="first" order-lg="last">
<v-col cols="12" lg="4" order="first" order-lg="last">
<div class="sticky top-0 h-fit">
<v-card prepend-icon="mdi-identifier" title="About">
<v-card-text>
<p><b>Description</b></p>
<p>{{ account.description }}</p>
<p>{{ account?.profile.description }}</p>
<p class="mt-3"><b>Joined At</b></p>
<p>{{ new Date(account.created_at).toLocaleString() }}</p>
</v-card-text>
@@ -46,16 +46,10 @@
</template>
<script setup lang="ts">
definePageMeta({
alias: ["/@:name(.*)*"],
})
const { t } = useI18n()
const route = useRoute()
const config = useRuntimeConfig()
const tab = ref(1)
const { data: account } = await useFetch<any>(`${config.public.solarNetworkApi}/cgi/id/users/${route.params.name}`)
if (account.value == null) {
@@ -65,8 +59,17 @@ if (account.value == null) {
})
}
const urlOfAvatar = computed(() => account.value?.avatar ? `${config.public.solarNetworkApi}/cgi/uc/attachments/${account.value.avatar}` : void 0)
const urlOfBanner = computed(() => account.value?.banner ? `${config.public.solarNetworkApi}/cgi/uc/attachments/${account.value.banner}` : void 0)
const { data: accountPage, status: accountPageStatus } = await useFetch<any>(
`${config.public.solarNetworkApi}/cgi/id/users/${route.params.name}/page`,
)
const { data: accountStatus, status: accountStatusStatus } = await useFetch<any>(
`${config.public.solarNetworkApi}/cgi/id/users/${route.params.name}/status`,
)
const externalOpenLink = computed(() => `${config.public.solianUrl}/accounts/view/${route.params.name}`)
const urlOfAvatar = computed(() =>
account.value?.avatar ? `${config.public.solarNetworkApi}/cgi/uc/attachments/${account.value.avatar}` : void 0,
)
const urlOfBanner = computed(() =>
account.value?.banner ? `${config.public.solarNetworkApi}/cgi/uc/attachments/${account.value.banner}` : void 0,
)
</script>

View File

@@ -7,7 +7,7 @@
<v-avatar :image="urlOfAvatar" />
<div class="flex flex-col">
<span>{{ auth.userinfo?.nick }} <span class="text-xs">@{{ auth.userinfo?.name }}</span></span>
<span class="text-sm">{{ auth.userinfo?.description }}</span>
<span class="text-sm">{{ auth.userinfo?.profile?.description }}</span>
</div>
</div>

View File

@@ -0,0 +1,7 @@
{
"associatedApplications": [
{
"applicationId": "5507fe22-1159-4e31-9e93-37b8b441620a"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

15
roadsign.toml Normal file
View File

@@ -0,0 +1,15 @@
id = "capital"
[[locations]]
id = "capital"
hosts = ["solsynth.dev", "www.solsynth.dev"]
paths = ["/"]
[[locations.destinations]]
id = "capital-destination"
uri = "http://127.0.0.1:3000"
[[applications]]
id = "capital-app"
workdir = "/workdir/capital"
command = ["node", "server/index.mjs"]
environment = []

View File

@@ -5,7 +5,7 @@ export default defineSitemapEventHandler(async () => {
const result = await res.json()
return result.data.map((item: any) => asSitemapUrl({
loc: item.alias ? `/posts/${item.area_alias}/${item.alias}` : `/posts/${item.id}`,
loc: item.alias ? `/posts/${item.alias_prefix}/${item.alias}` : `/posts/${item.id}`,
lastmod: item.edited_at ?? item.published_at,
priority: 0.7,
_sitemap: "posts",

View File

@@ -0,0 +1,9 @@
import { defineEventHandler } from 'h3'
export default defineEventHandler(async () => {
const config = useRuntimeConfig();
const resp = await fetch(`${config.public.solarNetworkApi}/cgi/id/well-known/jwks`)
return await resp.json()
})

View File

@@ -1,31 +1,20 @@
export default defineEventHandler((event) => {
const config = useRuntimeConfig()
import { defineEventHandler } from 'h3'
return {
"authorization_endpoint": `${config.public.siteUrl}/auth/authorize`,
"grant_types_supported": [
"authorization_code",
"implicit",
"refresh_token",
],
"id_token_signing_alg_values_supported": [
"HS512",
],
"issuer": config.public.siteUrl,
"response_types_supported": [
"code",
"token",
],
"subject_types_supported": [
"public",
],
"token_endpoint": `${config.public.solarNetworkApi}/cgi/id/auth/token`,
"token_endpoint_auth_methods_supported": [
"client_secret_post",
],
"token_endpoint_auth_signing_alg_values_supported": [
"HS512",
],
"userinfo_endpoint": `${config.public.solarNetworkApi}/cgi/id/users/me`,
export default defineEventHandler(async () => {
const config = useRuntimeConfig();
const siteUrl = config.public.siteUrl
const resp = await fetch(`${config.public.solarNetworkApi}/cgi/id/well-known/openid-configuration`)
const out: Record<string, any> = await resp.json()
out['authorization_endpoint'] = `${siteUrl}/auth/authorize`
out['jwks_uri'] = `${siteUrl}/.well-known/jwks`
for (const [k, v] of Object.entries(out)) {
if (typeof v === 'string' && v.startsWith('https://id.solsynth.dev/api')) {
out[k] = v.replace('https://id.solsynth.dev/api', `${config.public.solarNetworkApi}/cgi/id`)
}
}
})
return out
})

View File

@@ -3,11 +3,11 @@ import { ref } from "vue"
import { solarFetch } from "~/utils/request"
export function useAtk() {
return useCookie("__hydrogen_atk", { path: "/", maxAge: 31556952000 })
return useCookie("solar_network_atk", { path: "/", maxAge: 31556952000 })
}
export function useRtk() {
return useCookie("__hydrogen_rtk", { path: "/", maxAge: 31556952000 })
return useCookie("solar_network_rtk", { path: "/", maxAge: 31556952000 })
}
export function useLoggedInState() {
@@ -22,18 +22,23 @@ export const useUserinfo = defineStore("userinfo", () => {
let fetchCompleter: Completer<boolean> | null = null
let refreshCompleter: Completer<string> | null = null
const lastRefreshedAt = ref<Date | null>(null)
function setTokenSet(atk: string, rtk: string) {
lastRefreshedAt.value = new Date()
useAtk().value = atk
useRtk().value = rtk
}
async function getAtk() {
if (!useLoggedInState().value) return useAtk().value
if (lastRefreshedAt.value != null && Math.floor(Math.abs(Date.now() - lastRefreshedAt.value.getTime()) / 60000) < 3) {
return useAtk().value
const atk = useAtk()
if (!useLoggedInState().value) return atk.value
const parts = atk.value?.split(".") ?? []
if (parts.length != 3) return atk.value
const payload = JSON.parse(atob(parts[1]))
const exp: number = payload["exp"]
if (exp > Date.now() / 1000) {
return atk.value
}
if (refreshCompleter != null) {
@@ -57,7 +62,7 @@ export const useUserinfo = defineStore("userinfo", () => {
throw new Error(err)
} else {
const out = await res.json()
console.log("[PASSPORT] Access token has been refreshed now.")
console.log("[Passport] Access token has been refreshed now.")
setTokenSet(out["access_token"], out["refresh_token"])
refreshCompleter.complete(out["access_token"])
return out["access_token"]
@@ -97,12 +102,12 @@ export const useUserinfo = defineStore("userinfo", () => {
fetchCompleter = null
}
return { userinfo, lastRefreshedAt, isLoggedIn, isReady, fetchCompleter, setTokenSet, getAtk, readProfiles }
return { userinfo, isLoggedIn, isReady, fetchCompleter, setTokenSet, getAtk, readProfiles }
})
export class Completer<T> {
public readonly promise: Promise<T>
public complete: (value: (PromiseLike<T> | T)) => void
public complete: (value: PromiseLike<T> | T) => void
private reject: (reason?: any) => void
public constructor() {

11
utils/format.ts Normal file
View File

@@ -0,0 +1,11 @@
export function formatBytes(bytes: number, decimals = 2) {
if (!+bytes) return "0 Bytes"
const k = 1024
const dm = decimals < 0 ? 0 : decimals
const sizes = ["Bytes", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"]
const i = Math.floor(Math.log(bytes) / Math.log(k))
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`
}

View File

@@ -1,7 +1,9 @@
export function getLocale() {
export function getLocale(locale?: any) {
const fallbackLocale = "en"
const supportedLocales = ["en", "zh-CN"]
const { locale } = useI18n()
if (locale == null) {
locale = useI18n().locale
}
return supportedLocales.includes(locale.value) ? locale.value : fallbackLocale;
return supportedLocales.includes(locale.value) ? locale.value : fallbackLocale
}

View File

@@ -14,3 +14,13 @@ export async function solarFetch(input: string, init?: RequestInit) {
},
})
}
export function getAttachmentUrl(identifier: string | undefined): string | undefined {
if (identifier == null || identifier.length == 0) {
return undefined
}
if (identifier.startsWith("http")) {
return identifier
}
return `${useRuntimeConfig().public.solarNetworkApi}/cgi/uc/attachments/${identifier}`
}