Compare commits
46 Commits
dbcc12156c
...
archive/nu
| Author | SHA1 | Date | |
|---|---|---|---|
| 3974999684 | |||
| 4fab2183ce | |||
| c6ccdd83a8 | |||
| d544eeccb6 | |||
| 51fd602cd0 | |||
| 65b46c0195 | |||
| abc3156149 | |||
| 991acbfb7b | |||
| 63bcd3e58e | |||
| 266b14f169 | |||
| 2306ec893b | |||
| 4e0ce9118d | |||
| 0276272b42 | |||
| 41b887faf6 | |||
| b12d1deece | |||
| f97310c01d | |||
| 95e0d3fb29 | |||
| 234043fece | |||
| 1739cd92b7 | |||
| ae31e72447 | |||
| 4efa211e9e | |||
| 6304305ff6 | |||
| 9726d8f805 | |||
| 1f2c4f33cb | |||
| e9e182ea48 | |||
| e4111dc06e | |||
| 3e7f259834 | |||
| 97449bdc1e | |||
| 975766302a | |||
| b9d89149b0 | |||
| 6693acb24a | |||
| a0cf66d2e1 | |||
| 92e56e7e88 | |||
| 295d2d5b95 | |||
| f80df60858 | |||
| 8b4b6eb703 | |||
| 608bdc5d28 | |||
| 1ccb9e738e | |||
| fe0fffada2 | |||
| 9bcd809493 | |||
| dc740f8538 | |||
| 7e3c3a1679 | |||
| 729b7f3c00 | |||
| 9e7881745a | |||
| 7151d71463 | |||
| e5a5463b63 |
16
.roadsignrc
Normal 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
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
46
assets/products/app-store-download.svg
Executable 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="<Group>">
|
||||
<g id="_Group_2" data-name="<Group>">
|
||||
<g id="_Group_3" data-name="<Group>">
|
||||
<path id="_Path_" data-name="<Path>" 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="<Path>" 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="<Group>">
|
||||
<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 |
BIN
assets/products/solar-network/alpha.webp
Executable file
|
After Width: | Height: | Size: 696 KiB |
BIN
assets/products/solar-network/ft-chat.png
Executable file
|
After Width: | Height: | Size: 441 KiB |
BIN
assets/products/solar-network/ft-dashboard.png
Executable file
|
After Width: | Height: | Size: 787 KiB |
BIN
assets/products/solar-network/ft-explore.png
Executable file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
assets/products/solar-network/ft-news.png
Executable file
|
After Width: | Height: | Size: 770 KiB |
BIN
assets/products/solar-network/ft-posting.png
Executable file
|
After Width: | Height: | Size: 749 KiB |
BIN
assets/products/solar-network/ft-stickers.png
Executable file
|
After Width: | Height: | Size: 461 KiB |
BIN
assets/products/solar-network/icon.png
Executable file
|
After Width: | Height: | Size: 118 KiB |
BIN
assets/products/solar-network/labeled.webp
Executable file
|
After Width: | Height: | Size: 2.0 MiB |
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
29
components/LocaleSelect.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<v-menu>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn
|
||||
size="small"
|
||||
icon="mdi-translate"
|
||||
v-bind="props"
|
||||
/>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item
|
||||
class="w-48"
|
||||
density="compact"
|
||||
v-for="item in locales"
|
||||
:key="item.code"
|
||||
:value="item.code"
|
||||
:active="locale == item.code"
|
||||
@click.prevent.stop="() => { setLocale(item.code); emits('update') }"
|
||||
>
|
||||
<v-list-item-title>{{ item.name }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const emits = defineEmits(['update'])
|
||||
const { locale, locales, setLocale } = useI18n()
|
||||
</script>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
52
components/activity/List.vue
Normal 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>
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
167
components/creator/stickers/DataTable.vue
Normal file
@@ -0,0 +1,167 @@
|
||||
<template>
|
||||
<v-expand-transition>
|
||||
<v-alert v-if="error" variant="tonal" type="error" class="text-xs mb-3">
|
||||
{{ t("errorOccurred", [error]) }}
|
||||
</v-alert>
|
||||
</v-expand-transition>
|
||||
|
||||
<v-data-table
|
||||
density="compact"
|
||||
:headers="dataDefinitions.stickers"
|
||||
:items="stickers"
|
||||
:loading="reverting.stickers"
|
||||
@update:options="readStickers"
|
||||
item-value="id"
|
||||
>
|
||||
<template v-slot:item="{ item }: { item: any }">
|
||||
<tr>
|
||||
<td>{{ item.id }}</td>
|
||||
<td>
|
||||
<div class="item-texture-cell">
|
||||
<v-img
|
||||
cover
|
||||
aspect-ratio="1"
|
||||
width="28"
|
||||
height="28"
|
||||
color="grey-lighten-2"
|
||||
rounded="sm"
|
||||
:src="`${config.public.solarNetworkApi}/cgi/uc/attachments/${item.attachment.rid}`"
|
||||
>
|
||||
<template #placeholder>
|
||||
<div class="d-flex align-center justify-center fill-height">
|
||||
<v-progress-circular
|
||||
size="x-small"
|
||||
width="3"
|
||||
color="grey-lighten-4"
|
||||
indeterminate
|
||||
></v-progress-circular>
|
||||
</div>
|
||||
</template>
|
||||
</v-img>
|
||||
|
||||
<v-code class="px-2 w-fit font-mono">
|
||||
{{ item.attachment.rid }}
|
||||
</v-code>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ item.name }}</td>
|
||||
<td>{{ props.packPrefix + item.alias }}</td>
|
||||
<td>
|
||||
<v-btn
|
||||
v-bind="props"
|
||||
variant="text"
|
||||
size="x-small"
|
||||
color="warning"
|
||||
icon="mdi-pencil"
|
||||
class="ms-[-8px]"
|
||||
:to="`/creator/stickers/${item.pack_id}/${item.id}/edit`"
|
||||
/>
|
||||
|
||||
<v-dialog max-width="480">
|
||||
<template #activator="{ props }">
|
||||
<v-btn
|
||||
v-bind="props"
|
||||
variant="text"
|
||||
size="x-small"
|
||||
color="error"
|
||||
icon="mdi-delete"
|
||||
:disabled="submitting"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-slot:default="{ isActive }">
|
||||
<v-card :title="`Delete sticker #${item.id}?`">
|
||||
<v-card-text>
|
||||
This action will delete this sticker, all content used it will no longer show your sticker. But the
|
||||
attachment will still exists.
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<v-btn text="Cancel" color="grey" @click="isActive.value = false"></v-btn>
|
||||
|
||||
<v-btn
|
||||
text="Delete"
|
||||
color="error"
|
||||
@click="
|
||||
() => {
|
||||
deleteSticker(item)
|
||||
isActive.value = false
|
||||
}
|
||||
"
|
||||
/>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
</v-dialog>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { solarFetch } from "~/utils/request"
|
||||
|
||||
const config = useRuntimeConfig()
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps<{ packId: number; packPrefix?: string }>()
|
||||
|
||||
const error = ref<null | string>(null)
|
||||
|
||||
const dataDefinitions: { [id: string]: any[] } = {
|
||||
stickers: [
|
||||
{ align: "start", key: "id", title: "ID" },
|
||||
{ align: "start", key: "attachment", title: "Texture" },
|
||||
{ align: "start", key: "name", title: "Name" },
|
||||
{ align: "start", key: "alias", title: "Alias" },
|
||||
{ align: "start", key: "actions", title: "Actions", sortable: false },
|
||||
],
|
||||
}
|
||||
|
||||
const stickers = ref<any>([])
|
||||
|
||||
const reverting = reactive({ stickers: false })
|
||||
|
||||
async function readStickers() {
|
||||
reverting.stickers = true
|
||||
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["stickers"]
|
||||
}
|
||||
reverting.stickers = false
|
||||
}
|
||||
|
||||
onMounted(() => readStickers())
|
||||
|
||||
const submitting = ref(false)
|
||||
|
||||
async function deleteSticker(item: any) {
|
||||
submitting.value = true
|
||||
|
||||
const res = await solarFetch(`/cgi/uc/stickers/${item.id}`, {
|
||||
method: "DELETE",
|
||||
})
|
||||
if (res.status !== 200) {
|
||||
error.value = await res.text()
|
||||
} else {
|
||||
await readStickers()
|
||||
}
|
||||
|
||||
submitting.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.item-texture-cell {
|
||||
display: grid;
|
||||
grid-template-columns: 28px auto;
|
||||
gap: 6px;
|
||||
width: fit-content;
|
||||
}
|
||||
</style>
|
||||
@@ -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}
|
||||
|
||||
@@ -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'}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
54
content/en/terms/basic-law.mdx
Normal 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)*
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
54
content/zh-CN/terms/basic-law.mdx
Normal 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)*
|
||||
@@ -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 您的权利
|
||||
|
||||
无论您的帐号被如何处罚,您一直有权:
|
||||
|
||||
- 访问我们持有的关于您的个人信息
|
||||
- 请求更正您的个人信息
|
||||
- 请求删除您的个人信息
|
||||
|
||||
## 联系我们
|
||||
|
||||
如果您对本隐私政策或我们的数据处理方式有任何疑问或顾虑,请通过[您的联系方式]与我们联系。
|
||||
|
||||
## 政策变更
|
||||
|
||||
我们可能会不时更新本隐私政策。任何更改将发布在此页面上,且我们会通知您任何重大更改。
|
||||
|
||||
@@ -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 保留对此协议的最终解释权
|
||||
@@ -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))
|
||||
@@ -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?"
|
||||
}
|
||||
|
||||
@@ -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": "咩→咩↓"
|
||||
}
|
||||
@@ -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": "阅读文档"
|
||||
}
|
||||
|
||||
47
layouts/creator-hub.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<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-app-bar-nav-icon @click="openDrawer = !openDrawer" />
|
||||
|
||||
<nuxt-link to="/creator" exact>
|
||||
<h2>Creator Hub</h2>
|
||||
</nuxt-link>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<locale-select />
|
||||
<user-menu />
|
||||
</v-container>
|
||||
</v-app-bar>
|
||||
|
||||
<v-navigation-drawer v-model="openDrawer" location="left" width="300" floating>
|
||||
<v-list density="compact" nav color="primary">
|
||||
<v-list-item title="Back" prepend-icon="mdi-arrow-left" to="/" exact />
|
||||
</v-list>
|
||||
|
||||
<v-divider class="border-opacity-50 my-1" />
|
||||
|
||||
<v-list density="compact" nav color="primary">
|
||||
<v-list-item title="Stickers" prepend-icon="mdi-sticker-emoji" to="/creator/stickers" exact />
|
||||
</v-list>
|
||||
|
||||
<v-divider class="border-opacity-50 mb-4 mt-1" />
|
||||
|
||||
<copyright no-centered service="capital" class="px-5" />
|
||||
|
||||
<footer-links class="px-5 mt-3" />
|
||||
</v-navigation-drawer>
|
||||
|
||||
<v-main>
|
||||
<slot />
|
||||
</v-main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { t } = useI18n()
|
||||
const openDrawer = ref(false)
|
||||
|
||||
useHead({
|
||||
titleTemplate: "%s | Solsynth Creator Hub",
|
||||
})
|
||||
</script>
|
||||
@@ -1,67 +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>
|
||||
|
||||
<v-menu>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn
|
||||
size="small"
|
||||
icon="mdi-translate"
|
||||
v-bind="props"
|
||||
/>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item
|
||||
class="w-48"
|
||||
density="compact"
|
||||
v-for="item in locales"
|
||||
:key="item.code"
|
||||
:value="item.code"
|
||||
:active="locale == item.code"
|
||||
@click.prevent.stop="setLocale(item.code)"
|
||||
>
|
||||
<v-list-item-title>{{ item.name }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-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>
|
||||
|
||||
<user-menu />
|
||||
<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-navigation-drawer v-model="openDrawer" location="left" width="300" temporary order="-1">
|
||||
<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-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="Developer Portal" prepend-icon="mdi-code-tags" to="/dev" exact />
|
||||
<v-list-item title="Creator Hub" prepend-icon="mdi-pencil" disabled exact />
|
||||
<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>
|
||||
@@ -72,9 +58,45 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Logo from "../assets/logo-w-shadow.png"
|
||||
import { useBreakpoints, breakpointsVuetifyV3 } from "@vueuse/core"
|
||||
|
||||
const { locale, locales, setLocale, 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>
|
||||
|
||||
@@ -1,44 +1,15 @@
|
||||
<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>
|
||||
|
||||
<v-menu>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn
|
||||
size="small"
|
||||
icon="mdi-translate"
|
||||
v-bind="props"
|
||||
/>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item
|
||||
class="w-48"
|
||||
density="compact"
|
||||
v-for="item in locales"
|
||||
:key="item.code"
|
||||
:value="item.code"
|
||||
:active="locale == item.code"
|
||||
@click.prevent.stop="setLocale(item.code)"
|
||||
>
|
||||
<v-list-item-title>{{ item.name }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
|
||||
<locale-select />
|
||||
<user-menu />
|
||||
</v-container>
|
||||
</v-app-bar>
|
||||
@@ -67,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 { locale, locales, setLocale, t } = useI18n()
|
||||
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
@@ -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>
|
||||
6
middleware/redirectLocale.global.ts
Normal 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", ""))
|
||||
}
|
||||
})
|
||||
@@ -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: {
|
||||
|
||||
32
package.json
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
24
pages/creator/index.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<v-container fluid class="h-[calc(100vh-80px)] flex flex-col justify-center items-center text-center">
|
||||
<v-icon icon="mdi-brush" size="64" />
|
||||
<div class="text-2xl">Hello, creator!</div>
|
||||
<div class="max-w-[320px]">Switch page using navigator above to get start creating contents on Solar Network.</div>
|
||||
|
||||
<div class="text-xs font-mono text-grey mt-5">
|
||||
@{{ auth.userinfo?.name }} · {{ auth.userinfo?.id.toString().padStart(8, "0") }}
|
||||
</div>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
layout: "creator-hub",
|
||||
middleware: ["auth"],
|
||||
})
|
||||
|
||||
useHead({
|
||||
title: "Landing",
|
||||
})
|
||||
|
||||
const auth = useUserinfo()
|
||||
</script>
|
||||
181
pages/creator/stickers/[id]/[sticker]/edit.vue
Normal file
@@ -0,0 +1,181 @@
|
||||
<template>
|
||||
<v-container class="px-12">
|
||||
<div class="flex justify-between items-center mt-5">
|
||||
<div class="flex items-end gap-2">
|
||||
<h1 class="text-2xl">Edit sticker: {{ data?.name ?? "Loading" }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<v-btn
|
||||
color="grey"
|
||||
text="Cancel"
|
||||
prepend-icon="mdi-arrow-left"
|
||||
variant="tonal"
|
||||
to="/creator/stickers"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-expand-transition>
|
||||
<v-alert v-if="error" variant="tonal" type="error" class="text-xs mt-5 mb-3">
|
||||
{{ t("errorOccurred", [error]) }}
|
||||
</v-alert>
|
||||
</v-expand-transition>
|
||||
|
||||
<v-form class="mt-5" @submit.prevent="submit">
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-card title="Pack info" prepend-icon="mdi-sticker-emoji" density="compact">
|
||||
<v-card-text class="mt-2">
|
||||
<p class="text-lg"><b>{{ pack?.name ?? "Loading..." }}</b></p>
|
||||
<p>{{ pack?.description ?? "Please stand by..." }}</p>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
label="Name"
|
||||
name="name"
|
||||
variant="outlined"
|
||||
persistent-hint
|
||||
hint="A human friendly name for user to recognize this sticker"
|
||||
v-model="stickerName"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
label="Alias"
|
||||
name="alias"
|
||||
variant="outlined"
|
||||
persistent-hint
|
||||
hint="A placeholder of this sticker, will prepend pack's prefix"
|
||||
v-model="stickerAlias"
|
||||
>
|
||||
<template #prepend-inner>
|
||||
<p class="ms-1 me-[-5px] text-grey">{{ pack?.prefix }}</p>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
label="Attachment"
|
||||
name="attachment_id"
|
||||
variant="outlined"
|
||||
persistent-hint
|
||||
v-model="attachmentRid"
|
||||
>
|
||||
<template #details>
|
||||
<p class="order-first v-messages">
|
||||
The texture / image of this sticker, you can upload one from
|
||||
<nuxt-link to="/gallery/new?pool=c3RpY2tlcg" target="_blank" class="underline">here</nuxt-link>
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<template #prepend-inner>
|
||||
<v-img
|
||||
cover
|
||||
aspect-ratio="1"
|
||||
width="28"
|
||||
height="28"
|
||||
color="grey-lighten-2"
|
||||
rounded="sm"
|
||||
:src="attachmentRid.length > 0 ? `${config.public.solarNetworkApi}/cgi/uc/attachments/${attachmentRid}` : `example.com/not-found`"
|
||||
>
|
||||
<template #placeholder>
|
||||
<div class="d-flex align-center justify-center fill-height" v-if="attachmentRid.length > 0">
|
||||
<v-progress-circular
|
||||
size="x-small"
|
||||
width="3"
|
||||
color="grey-lighten-4"
|
||||
indeterminate
|
||||
></v-progress-circular>
|
||||
</div>
|
||||
<div class="d-flex align-center justify-center fill-height" v-else>
|
||||
<v-icon icon="mdi-image-broken-variant" class="block" size="18" />
|
||||
</div>
|
||||
</template>
|
||||
</v-img>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<v-btn type="submit" text="Save changes" append-icon="mdi-content-save" :disabled="data == null"
|
||||
:loading="submitting" />
|
||||
</div>
|
||||
</v-form>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
layout: "creator-hub",
|
||||
middleware: ["auth"],
|
||||
})
|
||||
|
||||
useHead({
|
||||
title: "Edit Sticker",
|
||||
})
|
||||
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
const data = ref<any>(null)
|
||||
const pack = ref<any>(null)
|
||||
|
||||
const attachmentRid = ref<string>("")
|
||||
const stickerName = ref<string>("")
|
||||
const stickerAlias = ref<string>("")
|
||||
|
||||
async function readPack() {
|
||||
const res = await solarFetch(`/cgi/uc/stickers/packs/${route.params.id}`)
|
||||
if (res.status != 200) {
|
||||
error.value = await res.text()
|
||||
} else {
|
||||
pack.value = await res.json()
|
||||
}
|
||||
}
|
||||
|
||||
async function readSticker() {
|
||||
const res = await solarFetch(`/cgi/uc/stickers/${route.params.sticker}`)
|
||||
if (res.status != 200) {
|
||||
error.value = await res.text()
|
||||
} else {
|
||||
data.value = await res.json()
|
||||
stickerName.value = data.value?.name
|
||||
stickerAlias.value = data.value?.alias
|
||||
attachmentRid.value = data.value?.attachment.rid
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => Promise.all([readPack(), readSticker()]))
|
||||
|
||||
const error = ref<null | string>(null)
|
||||
const submitting = ref(false)
|
||||
|
||||
async function submit(evt: SubmitEvent) {
|
||||
const data = Object.fromEntries(new FormData(evt.target as HTMLFormElement).entries())
|
||||
if (!data.name) return
|
||||
|
||||
submitting.value = true
|
||||
|
||||
const res = await solarFetch(`/cgi/uc/stickers/${route.params.sticker}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
pack_id: parseInt(route.params.id.toString()),
|
||||
...data,
|
||||
}),
|
||||
})
|
||||
if (res.status != 200) {
|
||||
error.value = await res.text()
|
||||
} else {
|
||||
navigateTo("/creator/stickers")
|
||||
}
|
||||
|
||||
submitting.value = false
|
||||
}
|
||||
</script>
|
||||
118
pages/creator/stickers/[id]/edit.vue
Normal file
@@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<v-container class="px-12">
|
||||
<div class="flex justify-between items-center mt-5">
|
||||
<div class="flex items-end gap-2">
|
||||
<h1 class="text-2xl">Edit sticker pack: {{ data?.name ?? "Loading" }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<v-btn
|
||||
color="grey"
|
||||
text="Cancel"
|
||||
prepend-icon="mdi-arrow-left"
|
||||
variant="tonal"
|
||||
to="/creator/stickers"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-expand-transition>
|
||||
<v-alert v-if="error" variant="tonal" type="error" class="text-xs mt-5 mb-3">
|
||||
{{ t("errorOccurred", [error]) }}
|
||||
</v-alert>
|
||||
</v-expand-transition>
|
||||
|
||||
<v-form class="mt-5" @submit.prevent="submit">
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
label="Name"
|
||||
name="name"
|
||||
variant="outlined"
|
||||
persistent-hint
|
||||
hint="A human friendly name for user to recognize this sticker pack"
|
||||
:model-value="data?.name"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
label="Prefix"
|
||||
name="prefix"
|
||||
variant="outlined"
|
||||
persistent-hint
|
||||
hint="A prefix for every sticker in this pack, will add before sticker's alias"
|
||||
:model-value="data?.prefix"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<v-textarea
|
||||
auto-grow
|
||||
rows="3"
|
||||
label="Description"
|
||||
name="description"
|
||||
variant="outlined"
|
||||
persistent-hint
|
||||
hint="A description for user to know about this sticker pack"
|
||||
:model-value="data?.description"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<v-btn type="submit" text="Save changes" append-icon="mdi-content-save" :disabled="data == null"
|
||||
:loading="submitting" />
|
||||
</div>
|
||||
</v-form>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
layout: "creator-hub",
|
||||
middleware: ["auth"],
|
||||
})
|
||||
|
||||
useHead({
|
||||
title: "Edit Sticker Pack",
|
||||
})
|
||||
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
|
||||
const data = ref<any>(null)
|
||||
|
||||
async function readPack() {
|
||||
const res = await solarFetch(`/cgi/uc/stickers/packs/${route.params.id}`)
|
||||
if (res.status != 200) {
|
||||
error.value = await res.text()
|
||||
} else {
|
||||
data.value = await res.json()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => readPack())
|
||||
|
||||
const error = ref<null | string>(null)
|
||||
const submitting = ref(false)
|
||||
|
||||
async function submit(evt: SubmitEvent) {
|
||||
const data = Object.fromEntries(new FormData(evt.target as HTMLFormElement).entries())
|
||||
if (!data.name) return
|
||||
|
||||
submitting.value = true
|
||||
|
||||
const res = await solarFetch(`/cgi/uc/stickers/packs/${route.params.id}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
if (res.status != 200) {
|
||||
error.value = await res.text()
|
||||
} else {
|
||||
navigateTo("/creator/stickers")
|
||||
}
|
||||
|
||||
submitting.value = false
|
||||
}
|
||||
</script>
|
||||
158
pages/creator/stickers/[id]/new.vue
Normal file
@@ -0,0 +1,158 @@
|
||||
<template>
|
||||
<v-container class="px-12">
|
||||
<div class="flex justify-between items-center mt-5">
|
||||
<div class="flex items-end gap-2">
|
||||
<h1 class="text-2xl">Create a new sticker</h1>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<v-btn
|
||||
color="grey"
|
||||
text="Cancel"
|
||||
prepend-icon="mdi-arrow-left"
|
||||
variant="tonal"
|
||||
to="/creator/stickers"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-expand-transition>
|
||||
<v-alert v-if="error" variant="tonal" type="error" class="text-xs mt-5 mb-3">
|
||||
{{ t("errorOccurred", [error]) }}
|
||||
</v-alert>
|
||||
</v-expand-transition>
|
||||
|
||||
<v-form class="mt-5" @submit.prevent="submit">
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-card title="Pack info" prepend-icon="mdi-sticker-emoji" density="compact">
|
||||
<v-card-text class="mt-2">
|
||||
<p class="text-lg"><b>{{ data?.name ?? "Loading..." }}</b></p>
|
||||
<p>{{ data?.description ?? "Please stand by..." }}</p>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
label="Name"
|
||||
name="name"
|
||||
variant="outlined"
|
||||
persistent-hint
|
||||
hint="A human friendly name for user to recognize this sticker"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
label="Alias"
|
||||
name="alias"
|
||||
variant="outlined"
|
||||
persistent-hint
|
||||
hint="A placeholder of this sticker, will prepend pack's prefix"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
label="Attachment"
|
||||
name="attachment_id"
|
||||
variant="outlined"
|
||||
persistent-hint
|
||||
v-model="attachmentRid"
|
||||
>
|
||||
<template #details>
|
||||
<p class="order-first v-messages">
|
||||
The texture / image of this sticker, you can upload one from
|
||||
<nuxt-link to="/gallery/new?pool=c3RpY2tlcg" target="_blank" class="underline">here</nuxt-link>
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<template #prepend-inner>
|
||||
<v-img
|
||||
cover
|
||||
aspect-ratio="1"
|
||||
width="28"
|
||||
height="28"
|
||||
color="grey-lighten-2"
|
||||
rounded="sm"
|
||||
:src="attachmentRid.length > 0 ? `${config.public.solarNetworkApi}/cgi/uc/attachments/${attachmentRid}` : `example.com/not-found`"
|
||||
>
|
||||
<template #placeholder>
|
||||
<div class="d-flex align-center justify-center fill-height" v-if="attachmentRid.length > 0">
|
||||
<v-progress-circular
|
||||
size="x-small"
|
||||
width="3"
|
||||
color="grey-lighten-4"
|
||||
indeterminate
|
||||
></v-progress-circular>
|
||||
</div>
|
||||
<div class="d-flex align-center justify-center fill-height" v-else>
|
||||
<v-icon icon="mdi-image-broken-variant" class="block" size="18"/>
|
||||
</div>
|
||||
</template>
|
||||
</v-img>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<v-btn type="submit" text="Create" append-icon="mdi-plus" :disabled="data == null" :loading="submitting" />
|
||||
</div>
|
||||
</v-form>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
layout: "creator-hub",
|
||||
middleware: ["auth"],
|
||||
})
|
||||
|
||||
useHead({
|
||||
title: "New Sticker",
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
const attachmentRid = ref<string>("")
|
||||
|
||||
const data = ref<any>(null)
|
||||
|
||||
async function readPack() {
|
||||
const res = await solarFetch(`/cgi/uc/stickers/packs/${route.params.id}`)
|
||||
if (res.status != 200) {
|
||||
error.value = await res.text()
|
||||
} else {
|
||||
data.value = await res.json()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => readPack())
|
||||
|
||||
const error = ref<null | string>(null)
|
||||
const submitting = ref(false)
|
||||
|
||||
async function submit(evt: SubmitEvent) {
|
||||
const data = Object.fromEntries(new FormData(evt.target as HTMLFormElement).entries())
|
||||
if (!data.name) return
|
||||
|
||||
submitting.value = true
|
||||
|
||||
const res = await solarFetch("/cgi/uc/stickers", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
pack_id: parseInt(route.params.id.toString()),
|
||||
...data,
|
||||
}),
|
||||
})
|
||||
if (res.status != 200) {
|
||||
error.value = await res.text()
|
||||
} else {
|
||||
navigateTo("/creator/stickers")
|
||||
}
|
||||
|
||||
submitting.value = false
|
||||
}
|
||||
</script>
|
||||
166
pages/creator/stickers/index.vue
Normal file
@@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<v-container class="px-12">
|
||||
<div class="flex justify-between items-center mt-5">
|
||||
<div class="flex items-end gap-2">
|
||||
<h1 class="text-2xl">Stickers & packs</h1>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<v-btn color="primary" text="New" append-icon="mdi-plus" variant="tonal" to="/creator/stickers/new" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-expand-transition>
|
||||
<v-alert v-if="error" variant="tonal" type="error" class="text-xs mt-5 mb-3">
|
||||
{{ t("errorOccurred", [error]) }}
|
||||
</v-alert>
|
||||
</v-expand-transition>
|
||||
|
||||
<div class="mt-5">
|
||||
<v-expansion-panels>
|
||||
<v-expansion-panel v-for="item in data" :key="'sticker-pack#' + item.id">
|
||||
<template #title>
|
||||
<div class="flex items-center gap-2">
|
||||
<p>{{ item.name }}</p>
|
||||
<v-chip size="x-small" class="font-mono" rounded color="primary">#{{ item.id }}</v-chip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #text>
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<p><b>Description</b></p>
|
||||
<p>{{ item.description }}</p>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6" lg="4">
|
||||
<p><b>Pack Prefix</b></p>
|
||||
<v-code class="font-mono mt-0.5 px-3 w-fit">
|
||||
<span v-if="item.prefix.length == 0"><i>no prefix</i></span>
|
||||
<span v-else>{{ item.prefix }}</span>
|
||||
</v-code>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6" lg="4">
|
||||
<p><b>Actions</b></p>
|
||||
<div class="flex mx-[-10px]">
|
||||
<v-btn
|
||||
variant="text"
|
||||
size="x-small"
|
||||
color="info"
|
||||
icon="mdi-sticker-plus"
|
||||
:to="`/creator/stickers/${item.id}/new`"
|
||||
/>
|
||||
<v-btn
|
||||
variant="text"
|
||||
size="x-small"
|
||||
color="warning"
|
||||
icon="mdi-pencil"
|
||||
:to="`/creator/stickers/${item.id}/edit`"
|
||||
/>
|
||||
|
||||
<v-dialog max-width="480">
|
||||
<template #activator="{ props }">
|
||||
<v-btn
|
||||
v-bind="props"
|
||||
variant="text"
|
||||
size="x-small"
|
||||
color="error"
|
||||
icon="mdi-delete"
|
||||
:disabled="submitting"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-slot:default="{ isActive }">
|
||||
<v-card :title="`Delete sticker pack #${item.id}?`">
|
||||
<v-card-text>
|
||||
This action will delete the stickers belongs to it and cannot be undone.
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<v-btn text="Cancel" color="grey" @click="isActive.value = false"></v-btn>
|
||||
|
||||
<v-btn
|
||||
text="Delete"
|
||||
color="error"
|
||||
@click="
|
||||
() => {
|
||||
deletePack(item)
|
||||
isActive.value = false
|
||||
}
|
||||
"
|
||||
/>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<p><b>Stickers</b></p>
|
||||
<v-card variant="outlined" class="mx-[-0.5ch] mt-1">
|
||||
<creator-stickers-data-table :pack-id="item.id" :pack-prefix="item.prefix" />
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
</div>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { solarFetch } from "~/utils/request"
|
||||
|
||||
definePageMeta({
|
||||
layout: "creator-hub",
|
||||
middleware: ["auth"],
|
||||
})
|
||||
|
||||
useHead({
|
||||
title: "Stickers",
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
const ua = useUserinfo()
|
||||
|
||||
const loading = ref(false)
|
||||
const error = ref<null | string>(null)
|
||||
|
||||
const data = ref<any[]>([])
|
||||
|
||||
async function readPacks() {
|
||||
loading.value = true
|
||||
|
||||
const res = await solarFetch(`/cgi/uc/stickers/packs?take=10&author=${ua.userinfo?.id}&offset=${data.value.length}`)
|
||||
if (res.status != 200) {
|
||||
error.value = await res.text()
|
||||
} else {
|
||||
const out = await res.json()
|
||||
data.value.push(...out["data"])
|
||||
}
|
||||
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
onMounted(() => readPacks())
|
||||
|
||||
const submitting = ref(false)
|
||||
|
||||
async function deletePack(item: any) {
|
||||
submitting.value = true
|
||||
|
||||
const res = await solarFetch(`/cgi/uc/stickers/packs/${item.id}`, {
|
||||
method: "DELETE",
|
||||
})
|
||||
if (res.status !== 200) {
|
||||
error.value = await res.text()
|
||||
} else {
|
||||
data.value = []
|
||||
await readPacks()
|
||||
}
|
||||
|
||||
submitting.value = false
|
||||
}
|
||||
</script>
|
||||
99
pages/creator/stickers/new.vue
Normal file
@@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<v-container class="px-12">
|
||||
<div class="flex justify-between items-center mt-5">
|
||||
<div class="flex items-end gap-2">
|
||||
<h1 class="text-2xl">Create a new sticker pack</h1>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<v-btn
|
||||
color="grey"
|
||||
text="Cancel"
|
||||
prepend-icon="mdi-arrow-left"
|
||||
variant="tonal"
|
||||
to="/creator/stickers"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-expand-transition>
|
||||
<v-alert v-if="error" variant="tonal" type="error" class="text-xs mt-5 mb-3">
|
||||
{{ t("errorOccurred", [error]) }}
|
||||
</v-alert>
|
||||
</v-expand-transition>
|
||||
|
||||
<v-form class="mt-5" @submit.prevent="submit">
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
label="Name"
|
||||
name="name"
|
||||
variant="outlined"
|
||||
persistent-hint
|
||||
hint="A human friendly name for user to recognize this sticker pack"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
label="Prefix"
|
||||
name="prefix"
|
||||
variant="outlined"
|
||||
persistent-hint
|
||||
hint="A prefix for every sticker in this pack, will add before sticker's alias"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<v-textarea
|
||||
auto-grow
|
||||
rows="3"
|
||||
label="Description"
|
||||
name="description"
|
||||
variant="outlined"
|
||||
persistent-hint
|
||||
hint="A description for user to know about this sticker pack"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<v-btn type="submit" text="Create" append-icon="mdi-plus" :loading="submitting" />
|
||||
</div>
|
||||
</v-form>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
layout: "creator-hub",
|
||||
middleware: ["auth"],
|
||||
})
|
||||
|
||||
useHead({
|
||||
title: "New Sticker Pack",
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const error = ref<null | string>(null)
|
||||
const submitting = ref(false)
|
||||
|
||||
async function submit(evt: SubmitEvent) {
|
||||
const data = Object.fromEntries(new FormData(evt.target as HTMLFormElement).entries())
|
||||
if (!data.name) return
|
||||
|
||||
submitting.value = true
|
||||
|
||||
const res = await solarFetch("/cgi/uc/stickers/packs", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
if (res.status != 200) {
|
||||
error.value = await res.text()
|
||||
} else {
|
||||
navigateTo('/creator/stickers')
|
||||
}
|
||||
|
||||
submitting.value = false
|
||||
}
|
||||
</script>
|
||||
@@ -45,6 +45,7 @@
|
||||
size="x-small"
|
||||
color="info"
|
||||
icon="mdi-key"
|
||||
class="ms-[-8px]"
|
||||
/>
|
||||
</template>
|
||||
</dev-bot-token-dialog>
|
||||
@@ -99,6 +100,7 @@ import { solarFetch } from "~/utils/request"
|
||||
|
||||
definePageMeta({
|
||||
layout: "dev-portal",
|
||||
middleware: ["auth"],
|
||||
})
|
||||
|
||||
useHead({
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
layout: "dev-portal",
|
||||
middleware: ["auth"],
|
||||
})
|
||||
|
||||
useHead({
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<div class="max-w-[320px]">Switch page using navigator above to get start developing with Solar Network.</div>
|
||||
|
||||
<div class="text-xs font-mono text-grey mt-5">
|
||||
@{{ auth.userinfo?.name }} · {{ auth.userinfo?.id.toString().padStart(8, '0') }}
|
||||
@{{ auth.userinfo?.name }} · {{ auth.userinfo?.id.toString().padStart(8, "0") }}
|
||||
</div>
|
||||
</v-container>
|
||||
</template>
|
||||
@@ -13,6 +13,7 @@
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
layout: "dev-portal",
|
||||
middleware: ["auth"],
|
||||
})
|
||||
|
||||
useHead({
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
<v-select
|
||||
label="Storage pool"
|
||||
variant="underlined"
|
||||
variant="solo"
|
||||
:items="poolOptions"
|
||||
item-title="label"
|
||||
item-value="value"
|
||||
@@ -80,10 +80,21 @@ useHead({
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
|
||||
onMounted(() => {
|
||||
if (route.query.pool) {
|
||||
pool.value = atob(decodeURIComponent(route.query.pool.toString()))
|
||||
if (pool.value == "dedicated") {
|
||||
pool.value = poolOptions[0].value
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
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 },
|
||||
]
|
||||
|
||||
@@ -190,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())
|
||||
|
||||
185
pages/index.vue
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
436
pages/products/solar-network.vue
Normal 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>
|
||||
69
pages/publishers/[name].vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<v-card class="w-28 aspect-square cursor-not-allowed" disabled>
|
||||
<v-card class="w-28 aspect-square cursor-not-allowed" to="/creator">
|
||||
<v-card-text class="flex flex-col justify-center items-center text-center h-full">
|
||||
<v-icon icon="mdi-pencil" size="32" />
|
||||
<span class="text-sm mt-1.75">Creator Hub</span>
|
||||
|
||||
7
public/.well-known/microsoft-identity-association.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"associatedApplications": [
|
||||
{
|
||||
"applicationId": "5507fe22-1159-4e31-9e93-37b8b441620a"
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
public/bento/programs/developer-banner.webp
Executable file
|
After Width: | Height: | Size: 15 KiB |
BIN
public/bento/programs/moderator-banner.webp
Executable file
|
After Width: | Height: | Size: 15 KiB |
BIN
public/bento/programs/stellar-banner.webp
Executable file
|
After Width: | Height: | Size: 18 KiB |
@@ -1,5 +1,8 @@
|
||||
User-agent: *
|
||||
Disallow: /embed
|
||||
Disallow: /dev
|
||||
Disallow: /creator
|
||||
Disallow: /users/me
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://solsynth.dev/sitemap.xml
|
||||
|
||||
BIN
public/thumbnails/docs/solar-archive-thumbnail.webp
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
public/thumbnails/docs/solar-network-user-manual.webp
Normal file
|
After Width: | Height: | Size: 83 KiB |
15
roadsign.toml
Normal 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 = []
|
||||
@@ -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",
|
||||
|
||||
9
server/api/well-known/jwks.ts
Normal 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()
|
||||
})
|
||||
@@ -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
|
||||
})
|
||||
@@ -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
@@ -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]}`
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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}`
|
||||
}
|
||||
|
||||