finish sk restructure, a11y and optimisations
This commit is contained in:
		
							parent
							
								
									9ef27fd2a2
								
							
						
					
					
						commit
						3ae05b3f9f
					
				
					 61 changed files with 416 additions and 429 deletions
				
			
		
							
								
								
									
										13
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										13
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							|  | @ -7,9 +7,10 @@ | |||
|         "": { | ||||
|             "name": "spacesocial-client", | ||||
|             "version": "0.2.0_rev2", | ||||
|             "license": "ISC", | ||||
|             "license": "GPL-3.0", | ||||
|             "devDependencies": { | ||||
|                 "@sveltejs/adapter-auto": "^3.2.2", | ||||
|                 "@sveltejs/adapter-static": "^3.0.2", | ||||
|                 "@sveltejs/kit": "^2.5.17", | ||||
|                 "@sveltejs/vite-plugin-svelte": "^3.1.1", | ||||
|                 "svelte": "^4.2.18", | ||||
|  | @ -718,6 +719,16 @@ | |||
|                 "@sveltejs/kit": "^2.0.0" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@sveltejs/adapter-static": { | ||||
|             "version": "3.0.2", | ||||
|             "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.2.tgz", | ||||
|             "integrity": "sha512-/EBFydZDwfwFfFEuF1vzUseBoRziwKP7AoHAwv+Ot3M084sE/HTVBHf9mCmXfdM9ijprY5YEugZjleflncX5fQ==", | ||||
|             "dev": true, | ||||
|             "license": "MIT", | ||||
|             "peerDependencies": { | ||||
|                 "@sveltejs/kit": "^2.0.0" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@sveltejs/kit": { | ||||
|             "version": "2.5.17", | ||||
|             "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.17.tgz", | ||||
|  |  | |||
|  | @ -13,6 +13,7 @@ | |||
|     "license": "GPL-3.0", | ||||
|     "devDependencies": { | ||||
|         "@sveltejs/adapter-auto": "^3.2.2", | ||||
|         "@sveltejs/adapter-static": "^3.0.2", | ||||
|         "@sveltejs/kit": "^2.5.17", | ||||
|         "@sveltejs/vite-plugin-svelte": "^3.1.1", | ||||
|         "svelte": "^4.2.18", | ||||
|  |  | |||
|  | @ -1,221 +0,0 @@ | |||
| <script> | ||||
|     import Navigation from './ui/Navigation.svelte'; | ||||
|     import Widgets from './ui/Widgets.svelte'; | ||||
|     import Feed from './ui/Feed.svelte'; | ||||
|     import { Client } from './client/client.js'; | ||||
|     import Button from './ui/Button.svelte'; | ||||
|     import { get } from 'svelte/store'; | ||||
| 
 | ||||
|     let client = get(Client.get()); | ||||
|     let ready = client.app && client.app.token; | ||||
|     let instance_url_error = false; | ||||
|     let logging_in = false; | ||||
| 
 | ||||
|     let auth_code = new URLSearchParams(location.search).get("code"); | ||||
|     if (auth_code) { | ||||
|         client.getToken(auth_code).then(() => { | ||||
|             client.save(); | ||||
|             location = location.origin; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     if (client.app && client.app.token) { | ||||
|         // this triggers the client actually getting the authenticated user's data. | ||||
|         client.verifyCredentials().then(res => { | ||||
|             if (res) { | ||||
|                 console.log(`Logged in as @${client.user.username}@${client.user.host}`); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     function log_in(event) { | ||||
|         logging_in = true; | ||||
|         event.preventDefault(); | ||||
|         const host = event.target.host.value; | ||||
| 
 | ||||
|         client.init(host).then(res => { | ||||
|             logging_in = false; | ||||
|             if (!res) return; | ||||
|             if (res.constructor === String) { | ||||
|                 instance_url_error = res; | ||||
|                 return; | ||||
|             }; | ||||
|             let oauth_url = client.getOAuthUrl(); | ||||
|             location = oauth_url; | ||||
|         }); | ||||
|     } | ||||
| </script> | ||||
| 
 | ||||
| <div id="spacesocial-app"> | ||||
| 
 | ||||
|     <header> | ||||
|         <Navigation /> | ||||
|     </header> | ||||
| 
 | ||||
|     <main> | ||||
|         {#if ready} | ||||
|             <Feed /> | ||||
|         {:else} | ||||
|             <div> | ||||
|                 <form on:submit={log_in} id="login"> | ||||
|                     <h1>Space Social</h1> | ||||
|                     <p>Welcome, fediverse user!</p> | ||||
|                     <p>Please enter your instance domain to log in.</p> | ||||
|                     <div class="input-wrapper"> | ||||
|                         <input type="text" id="host" aria-label="instance domain" class={logging_in ? "throb" : ""}> | ||||
|                         {#if instance_url_error} | ||||
|                             <p class="error">{instance_url_error}</p> | ||||
|                         {/if} | ||||
|                     </div> | ||||
|                     <br> | ||||
|                     <button type="submit" id="login" class={logging_in ? "disabled" : ""}>Log in</button> | ||||
|                     <p><small> | ||||
|                         Please note this is | ||||
|                         <strong><em>extremely experimental software</em></strong>; | ||||
|                         things are likely to break! | ||||
|                         <br> | ||||
|                         If that's all cool with you, welcome aboard! | ||||
|                     </small></p> | ||||
| 
 | ||||
|                     <p class="form-footer">made with ❤️ by <a href="https://arimelody.me">ari melody</a>, 2024</p> | ||||
|                 </form> | ||||
|             </div> | ||||
|         {/if} | ||||
|     </main> | ||||
| 
 | ||||
|     <div id="widgets"> | ||||
|         <Widgets /> | ||||
|     </div> | ||||
| 
 | ||||
| </div> | ||||
| 
 | ||||
| <style> | ||||
|     #spacesocial-app { | ||||
|         margin: auto 0; | ||||
|         padding: 0 16px; | ||||
|         display: flex; | ||||
|         flex-direction: row; | ||||
|         justify-content: center; | ||||
|         gap: 16px; | ||||
|     } | ||||
| 
 | ||||
|     header, #widgets { | ||||
|         width: 300px; | ||||
|     } | ||||
| 
 | ||||
|     main { | ||||
|         width: 732px; | ||||
|     } | ||||
| 
 | ||||
|     div.pane { | ||||
|         margin-top: 16px; | ||||
|         padding: 20px 32px; | ||||
|         border: 1px solid #8884; | ||||
|         border-radius: 16px; | ||||
|         background-color: var(--bg1); | ||||
|     } | ||||
| 
 | ||||
|     form#login { | ||||
|         margin: 25vh 0 32px 0; | ||||
|         text-align: center; | ||||
|     } | ||||
| 
 | ||||
|     a { | ||||
|         color: var(--accent); | ||||
|         text-decoration: none; | ||||
|     } | ||||
| 
 | ||||
|     a:hover { | ||||
|         text-decoration: underline; | ||||
|     } | ||||
| 
 | ||||
|     .input-wrapper { | ||||
|         width: 360px; | ||||
|         margin: 0 auto; | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|         align-items: center; | ||||
|     } | ||||
| 
 | ||||
|     input[type=text] { | ||||
|         width: 100%; | ||||
|         padding: 12px; | ||||
|         display: block; | ||||
|         border-radius: 8px; | ||||
|         border: 1px solid var(--accent); | ||||
|         background-color: var(--bg-800); | ||||
| 
 | ||||
|         font-family: inherit; | ||||
|         font-weight: bold; | ||||
|         font-size: inherit; | ||||
|         color: var(--text); | ||||
| 
 | ||||
|         transition: box-shadow .2s; | ||||
|     } | ||||
| 
 | ||||
|     input[type=text]::placeholder { | ||||
|         opacity: .8; | ||||
|     } | ||||
| 
 | ||||
|     input[type=text]:focus { | ||||
|         outline: none; | ||||
|         box-shadow: 0 0 16px color-mix(in srgb, transparent, var(--accent) 25%); | ||||
|     } | ||||
| 
 | ||||
|     .error { | ||||
|         margin: 6px; | ||||
|         font-style: italic; | ||||
|         font-size: .9em; | ||||
|         color: red; | ||||
|         opacity: .7; | ||||
|     } | ||||
| 
 | ||||
|     button#login { | ||||
|         margin: -8px auto 0 auto; | ||||
|         padding: 12px 24px; | ||||
|         display: flex; | ||||
|         flex-direction: row; | ||||
|         justify-content: space-between; | ||||
|         align-items: center; | ||||
| 
 | ||||
|         font-family: inherit; | ||||
|         font-size: 1rem; | ||||
|         font-weight: 600; | ||||
|         text-align: left; | ||||
| 
 | ||||
|         border-radius: 8px; | ||||
|         border-width: 2px; | ||||
|         border-style: solid; | ||||
| 
 | ||||
|         background-color: var(--bg-700); | ||||
|         color: var(--text); | ||||
|         border-color: transparent; | ||||
| 
 | ||||
|         transition-property: border-color, background-color, color; | ||||
|         transition-timing-function: ease-out; | ||||
|         transition-duration: .1s; | ||||
| 
 | ||||
|         cursor: pointer; | ||||
|         text-align: center; | ||||
|         justify-content: center; | ||||
|     } | ||||
| 
 | ||||
|     button#login:hover { | ||||
|         background-color: color-mix(in srgb, var(--bg-700), var(--accent) 10%); | ||||
|         border-color: color-mix(in srgb, var(--bg-700), var(--accent) 20%); | ||||
|     } | ||||
| 
 | ||||
|     button#login:active { | ||||
|         background-color: color-mix(in srgb, var(--bg-700), var(--bg-800) 50%); | ||||
|         border-color: color-mix(in srgb, var(--bg-700), var(--bg-800) 10%); | ||||
|     } | ||||
| 
 | ||||
|     button#login.disabled { | ||||
|         opacity: .5; | ||||
|         cursor: initial; | ||||
|     } | ||||
| 
 | ||||
|     .form-footer { | ||||
|         opacity: .7; | ||||
|     } | ||||
| </style> | ||||
|  | @ -1,4 +1,4 @@ | |||
| @import url("/font/inter/inter.css"); | ||||
| @import url("../font/inter/inter.css"); | ||||
| 
 | ||||
| :root { | ||||
|     --bg-1000: #fff6de; | ||||
|  | @ -40,6 +40,32 @@ body { | |||
|     box-sizing: border-box; | ||||
| } | ||||
| 
 | ||||
| a { | ||||
|     color: var(--accent); | ||||
|     text-decoration: none; | ||||
| } | ||||
| 
 | ||||
| a:hover { | ||||
|     text-decoration: underline; | ||||
| } | ||||
| 
 | ||||
| #spacesocial-app { | ||||
|     margin: auto 0; | ||||
|     padding: 0 16px; | ||||
|     display: flex; | ||||
|     flex-direction: row; | ||||
|     justify-content: center; | ||||
|     gap: 16px; | ||||
| } | ||||
| 
 | ||||
| header, #widgets { | ||||
|     width: 300px; | ||||
| } | ||||
| 
 | ||||
| main { | ||||
|     width: 732px; | ||||
| } | ||||
| 
 | ||||
| .throb { | ||||
|     animation: .25s throb alternate infinite ease-in; | ||||
| } | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ import { get } from 'svelte/store'; | |||
| export async function createApp(host) { | ||||
|     let form = new FormData(); | ||||
|     form.append("client_name", "space social"); | ||||
|     form.append("redirect_uris", `${location.origin}/callback`); | ||||
|     form.append("redirect_uris", `${location.origin}`); | ||||
|     form.append("scopes", "read write push"); | ||||
|     form.append("website", "https://spacesocial.arimelody.me"); | ||||
| 
 | ||||
|  | @ -35,7 +35,7 @@ export function getOAuthUrl() { | |||
|     return `https://${client.instance.host}/oauth/authorize` + | ||||
|         `?client_id=${client.app.id}` + | ||||
|         "&scope=read+write+push" + | ||||
|         `&redirect_uri=${location.origin}/callback` + | ||||
|         `&redirect_uri=${location.origin}` + | ||||
|         "&response_type=code"; | ||||
| } | ||||
| 
 | ||||
|  | @ -44,7 +44,7 @@ export async function getToken(code) { | |||
|     let form = new FormData(); | ||||
|     form.append("client_id", client.app.id); | ||||
|     form.append("client_secret", client.app.secret); | ||||
|     form.append("redirect_uri", `${location.origin}/callback`); | ||||
|     form.append("redirect_uri", `${location.origin}`); | ||||
|     form.append("grant_type", "authorization_code"); | ||||
|     form.append("code", code); | ||||
|     form.append("scope", "read write push"); | ||||
|  | @ -107,7 +107,7 @@ export async function getTimeline(last_post_id) { | |||
|     return data; | ||||
| } | ||||
| 
 | ||||
| export async function getPost(post_id, parent_replies) { | ||||
| export async function getPost(post_id, ancestor_count) { | ||||
|     let client = get(Client.get()); | ||||
|     let url = `https://${client.instance.host}/api/v1/statuses/${post_id}`; | ||||
|     const data = await fetch(url, { | ||||
|  | @ -208,7 +208,7 @@ export async function unreactPost(post_id, shortcode) { | |||
|     return data; | ||||
| } | ||||
| 
 | ||||
| export async function parsePost(data, parent_replies, child_replies) { | ||||
| export async function parsePost(data, ancestor_count, with_context) { | ||||
|     let client = get(Client.get()); | ||||
|     let post = new Post(); | ||||
| 
 | ||||
|  | @ -218,9 +218,12 @@ export async function parsePost(data, parent_replies, child_replies) { | |||
|     post.text = data.content; | ||||
| 
 | ||||
|     post.reply = null; | ||||
|     if ((data.in_reply_to_id || data.reply) && parent_replies !== 0) { | ||||
|         const reply_data = data.reply || await getPost(data.in_reply_to_id, parent_replies - 1); | ||||
|         post.reply = await parsePost(reply_data, parent_replies - 1, false); | ||||
|     if (!with_context && // ancestor replies are handled in full later
 | ||||
|         (data.in_reply_to_id || data.reply) && | ||||
|         ancestor_count !== 0 | ||||
|     ) { | ||||
|         const reply_data = data.reply || await getPost(data.in_reply_to_id, ancestor_count - 1); | ||||
|         post.reply = await parsePost(reply_data, ancestor_count - 1, false); | ||||
|         // if the post returns false, we probably don't have permission to read it.
 | ||||
|         // we'll respect the thread's privacy, and leave it alone :)
 | ||||
|         if (post.reply === false) return false; | ||||
|  | @ -228,11 +231,22 @@ export async function parsePost(data, parent_replies, child_replies) { | |||
|     post.boost = data.reblog ? await parsePost(data.reblog, 1, false) : null; | ||||
| 
 | ||||
|     post.replies = []; | ||||
|     if (child_replies) { | ||||
|     if (with_context) { | ||||
|         const replies_data = await getPostContext(data.id); | ||||
|         if (replies_data && replies_data.descendants) { | ||||
|             for (let i in replies_data.descendants) { | ||||
|                 post.replies.push(await parsePost(replies_data.descendants[i], 0, false)); | ||||
|         if (replies_data) { | ||||
|             // posts this is replying to
 | ||||
|             if (replies_data.ancestors) { | ||||
|                 let head = post; | ||||
|                 while (replies_data.ancestors.length > 0) { | ||||
|                     head.reply = await parsePost(replies_data.ancestors.pop(), 0, false); | ||||
|                     head = head.reply; | ||||
|                 } | ||||
|             } | ||||
|             // posts in reply to this
 | ||||
|             if (replies_data.descendants) { | ||||
|                 for (let i in replies_data.descendants) { | ||||
|                     post.replies.push(await parsePost(replies_data.descendants[i], 0, false)); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  |  | |||
|  | @ -17,6 +17,7 @@ export default class Post { | |||
|     files; | ||||
|     url; | ||||
|     reply; | ||||
|     reply_id; | ||||
|     replies; | ||||
|     boost; | ||||
|     visibility; | ||||
|  |  | |||
|  | @ -1,10 +1,13 @@ | |||
| import sound_log from '../sound/log.ogg'; | ||||
| import sound_hello from '../sound/hello.ogg'; | ||||
| import sound_success from '../sound/success.ogg'; | ||||
| let sounds; | ||||
| 
 | ||||
| if (typeof Audio !== typeof undefined) { | ||||
|     sounds = { | ||||
|         "default": new Audio("/sound/log.ogg"), | ||||
|         "post": new Audio("/sound/success.ogg"), | ||||
|         "boost": new Audio("/sound/hello.ogg"), | ||||
|         "default": new Audio(sound_log), | ||||
|         "post": new Audio(sound_hello), | ||||
|         "boost": new Audio(sound_success), | ||||
|     }; | ||||
| } | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										43
									
								
								src/lib/timeline.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								src/lib/timeline.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,43 @@ | |||
| import { Client } from '$lib/client/client.js'; | ||||
| import { get, writable } from 'svelte/store'; | ||||
| import { parsePost } from '$lib/client/api.js'; | ||||
| 
 | ||||
| export let posts = writable([]); | ||||
| 
 | ||||
| let client = get(Client.get()); | ||||
| let loading = false; | ||||
| 
 | ||||
| export async function getTimeline(clean) { | ||||
|     if (loading) return; // no spamming!!
 | ||||
|     loading = true; | ||||
| 
 | ||||
|     let timeline_data; | ||||
|     if (get(posts).length === 0) timeline_data = await client.getTimeline() | ||||
|     else timeline_data = await client.getTimeline(get(posts)[get(posts).length - 1].id); | ||||
| 
 | ||||
|     if (!timeline_data) { | ||||
|         console.error(`Failed to retrieve timeline.`); | ||||
|         loading = false; | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     if (clean) posts.set([]); | ||||
| 
 | ||||
|     for (let i in timeline_data) { | ||||
|         const post_data = timeline_data[i]; | ||||
|         const post = await parsePost(post_data, 1, false); | ||||
|         if (!post) { | ||||
|             if (post === null || post === undefined) { | ||||
|                 if (post_data.id) { | ||||
|                     console.warn("Failed to parse post #" + post_data.id); | ||||
|                 } else { | ||||
|                     console.warn("Failed to parse post:"); | ||||
|                     console.warn(post_data); | ||||
|                 } | ||||
|             } | ||||
|             continue; | ||||
|         } | ||||
|         posts.update(current => [...current, post]); | ||||
|     } | ||||
|     loading = false; | ||||
| } | ||||
|  | @ -2,104 +2,33 @@ | |||
|     import Button from './Button.svelte'; | ||||
|     import Post from './post/Post.svelte'; | ||||
|     import Error from './Error.svelte'; | ||||
|     import { Client } from '../client/client.js'; | ||||
|     import { parsePost } from '../client/api.js'; | ||||
|     import { Client } from '$lib/client/client.js'; | ||||
|     import { parsePost } from '$lib/client/api.js'; | ||||
|     import { get } from 'svelte/store'; | ||||
|     import { posts, getTimeline } from '$lib/timeline.js'; | ||||
| 
 | ||||
|     let params = new URLSearchParams(location.search); | ||||
| 
 | ||||
|     let client = get(Client.get()); | ||||
|     let posts = []; | ||||
|     let loading = false; | ||||
|     let focus_post_id = location.pathname.startsWith("/post/") ? location.pathname.substring(6) : false; | ||||
| 
 | ||||
|     let error; | ||||
| 
 | ||||
|     async function getTimeline() { | ||||
|         if (loading) return; // no spamming!! | ||||
|         loading = true; | ||||
| 
 | ||||
|         let timeline_data; | ||||
|         if (posts.length === 0) timeline_data = await client.getTimeline() | ||||
|         else timeline_data = await client.getTimeline(posts[posts.length - 1].id); | ||||
| 
 | ||||
|         if (!timeline_data) { | ||||
|             console.error(`Failed to retrieve timeline.`); | ||||
|             loading = false; | ||||
|             return; | ||||
|     getTimeline(); | ||||
|     document.addEventListener("scroll", event => { | ||||
|         if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 2048) { | ||||
|             getTimeline(); | ||||
|         } | ||||
| 
 | ||||
|         for (let i in timeline_data) { | ||||
|             const post_data = timeline_data[i]; | ||||
|             const post = await parsePost(post_data, 1, false); | ||||
|             if (!post) { | ||||
|                 if (post === null || post === undefined) { | ||||
|                     if (post_data.id) { | ||||
|                         console.warn("Failed to parse post #" + post_data.id); | ||||
|                     } else { | ||||
|                         console.warn("Failed to parse post:"); | ||||
|                         console.warn(post_data); | ||||
|                     } | ||||
|                 } | ||||
|                 continue; | ||||
|             } | ||||
|             posts = [...posts, post]; | ||||
|         } | ||||
|         loading = false; | ||||
|     } | ||||
| 
 | ||||
|     async function getPost(post_id) { | ||||
|         loading = true; | ||||
| 
 | ||||
|         const post_data = await client.getPost(post_id); | ||||
|         if (!post_data) { | ||||
|             console.error(`Failed to retrieve post ${post_id}.`); | ||||
|             loading = false; | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         const post = await parsePost(post_data, 10, true); | ||||
|         posts = [post]; | ||||
|         for (let i in post.replies) { | ||||
|             posts = [...posts, post.replies[i]]; | ||||
|         } | ||||
|         loading = false; | ||||
|     } | ||||
| 
 | ||||
|     if (focus_post_id) { | ||||
|         getPost(focus_post_id); | ||||
|     } else { | ||||
|         getTimeline(); | ||||
|         document.addEventListener("scroll", event => { | ||||
|             if (!loading && window.innerHeight + window.scrollY >= document.body.offsetHeight - 2048) { | ||||
|                 getTimeline(); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|     }); | ||||
| </script> | ||||
| 
 | ||||
| <header> | ||||
|     <h1>Home</h1> | ||||
|     <nav> | ||||
|         <Button centered active>Home</Button> | ||||
|         <Button centered disabled>Local</Button> | ||||
|         <Button centered disabled>Federated</Button> | ||||
|     </nav> | ||||
| </header> | ||||
| 
 | ||||
| <div id="feed"> | ||||
| <div id="feed" role="feed"> | ||||
|     {#if posts.length <= 0} | ||||
|         <div class="throb"> | ||||
|             <span>just a moment...</span> | ||||
|         <div class="loading throb"> | ||||
|             <span>getting the feed...</span> | ||||
|         </div> | ||||
|     {/if} | ||||
|     {#each posts as post} | ||||
|         <Post post_data={post} focused={post.id === focus_post_id} /> | ||||
|     {#each $posts as post} | ||||
|         <Post post_data={post} /> | ||||
|     {/each} | ||||
| </div> | ||||
| 
 | ||||
| <style> | ||||
|     header { | ||||
|         width: 100%; | ||||
|         margin: 16px 0 8px 0; | ||||
|         display: flex; | ||||
|         flex-direction: row; | ||||
|  | @ -120,7 +49,7 @@ | |||
|         margin-bottom: 20vh; | ||||
|     } | ||||
| 
 | ||||
|     .throb { | ||||
|     .loading { | ||||
|         width: 100%; | ||||
|         height: 80vh; | ||||
|         display: flex; | ||||
|  |  | |||
|  | @ -5,6 +5,8 @@ | |||
|     import { Client } from '../client/client.js'; | ||||
|     import { play_sound } from '../sound.js'; | ||||
| 
 | ||||
|     const VERSION = APP_VERSION; | ||||
| 
 | ||||
|     let client = false; | ||||
|     Client.get().subscribe(c => { | ||||
|         client = c; | ||||
|  | @ -14,6 +16,13 @@ | |||
|     if (notification_count > 99) notification_count = "99+"; | ||||
| 
 | ||||
|     function goTimeline() { | ||||
|         if (location.pathname === "/") { | ||||
|             window.scrollTo({ | ||||
|                 top: 0, | ||||
|                 behavior: "smooth" | ||||
|             }); | ||||
|             return; | ||||
|         } | ||||
|         location = "/"; | ||||
|     } | ||||
| 
 | ||||
|  | @ -61,7 +70,7 @@ | |||
|         </div> | ||||
| 
 | ||||
|         <div id="account-button"> | ||||
|             <img src={client.user.avatar_url} class="account-avatar" height="64px" aria-hidden="true" on:click={() => play_sound()}> | ||||
|             <img src={client.user.avatar_url} class="account-avatar" height="64px" alt="" aria-hidden="true" on:click={() => play_sound()}> | ||||
|             <div class="account-name" aria-hidden="true"> | ||||
|                 <span class="nickname" title={client.user.nickname}>{client.user.nickname}</span> | ||||
|                 <span class="username" title={`@${client.user.username}@${client.user.host}`}> | ||||
|  | @ -73,7 +82,7 @@ | |||
|     </div> | ||||
|     {/if} | ||||
|     <span class="version"> | ||||
|         space social v{APP_VERSION} | ||||
|         space social v{VERSION} | ||||
|         <br> | ||||
|         <ul> | ||||
|             <li><a href="https://git.arimelody.me/ari/spacesocial-client">source</a></li> | ||||
|  | @ -252,26 +261,6 @@ | |||
|         font-size: .65em; | ||||
|     } | ||||
| 
 | ||||
|     .settings { | ||||
|         width: 32px; | ||||
|         height: 32px; | ||||
|         padding: none; | ||||
|         border: none; | ||||
|         font-size: inherit; | ||||
|         font-family: inherit; | ||||
|         background: none; | ||||
|         border-radius: 8px; | ||||
|         transition: background-color .1s; | ||||
|     } | ||||
| 
 | ||||
|     .settings:hover { | ||||
|         background-color: color-mix(in srgb, var(--bg-700), var(--text) 15%); | ||||
|     } | ||||
| 
 | ||||
|     .settings:active { | ||||
|         background-color: color-mix(in srgb, var(--bg-700), var(--bg-1000) 30%); | ||||
|     } | ||||
| 
 | ||||
|     .flex-row { | ||||
|         display: flex; | ||||
|         flex-direction: row; | ||||
|  |  | |||
|  | @ -15,22 +15,24 @@ | |||
|     <span class="post-context-time"> | ||||
|         <time title="{time_string}">{short_time(post.created_at)}</time> | ||||
|         {#if post.visibility !== "public"} | ||||
|             <span class="post-visibility">({post.visibility})</span> | ||||
|             <span class="post-visibility">- {post.visibility}</span> | ||||
|         {/if} | ||||
|     </span> | ||||
| </div> | ||||
| 
 | ||||
| <style> | ||||
|     .post-context { | ||||
|         margin-bottom: 8px; | ||||
|         padding-left: 58px; | ||||
|         padding: 12px 16px 0 74px; | ||||
|         display: flex; | ||||
|         flex-direction: row; | ||||
|         align-items: center; | ||||
|         font-size: .8em; | ||||
|         font-weight: 600; | ||||
|         color: var(--text); | ||||
|         opacity: .8; | ||||
|         transition: opacity .1s; | ||||
|         transition: opacity .1s, background-color .1s; | ||||
|         border-radius: 8px; | ||||
|         z-index: 1; | ||||
|     } | ||||
| 
 | ||||
|     .post-context-icon { | ||||
|  | @ -49,4 +51,8 @@ | |||
|     .post-context-time { | ||||
|         margin-left: auto; | ||||
|     } | ||||
| 
 | ||||
|     .post-visibility { | ||||
|         opacity: .7; | ||||
|     } | ||||
| </style> | ||||
|  |  | |||
|  | @ -11,6 +11,7 @@ | |||
|     import { get } from 'svelte/store'; | ||||
|     import { Client } from '../../client/client.js'; | ||||
|     import * as api from '../../client/api.js'; | ||||
|     import { goto } from '$app/navigation'; | ||||
| 
 | ||||
|     export let post_data; | ||||
|     export let focused = false; | ||||
|  | @ -25,7 +26,9 @@ | |||
|     } | ||||
| 
 | ||||
|     function gotoPost() { | ||||
|         location = `/post/${post.id}`; | ||||
|         if (focused) return; | ||||
|         if (event.key && event.key !== "Enter") return; | ||||
|         goto(`/post/${post.id}`); | ||||
|     } | ||||
| 
 | ||||
|     async function toggleBoost() { | ||||
|  | @ -80,25 +83,30 @@ | |||
|     let el; | ||||
|     onMount(() => { | ||||
|         if (focused) { | ||||
|             window.scrollTo(0, el.scrollHeight - 700); | ||||
|             window.scrollTo(0, el.scrollHeight); | ||||
|         } | ||||
|     }); | ||||
| 
 | ||||
|     let aria_label = post.user.username + '; ' + post.text + '; ' + post.created_at; | ||||
| </script> | ||||
| 
 | ||||
| <div class="post-container" aria-label={aria_label} bind:this={el}> | ||||
| <div class="post-container"> | ||||
|     {#if post.reply} | ||||
|         <ReplyContext post={post.reply} /> | ||||
|     {/if} | ||||
|     {#if is_boost && !post_context.text} | ||||
|         <BoostContext post={post_context} /> | ||||
|     {/if} | ||||
|     <article class={"post" + (focused ? " focused" : "")} on:click={!focused ? gotoPost() : null}> | ||||
|     <article | ||||
|             class={"post" + (focused ? " focused" : "")} | ||||
|             aria-label={aria_label} | ||||
|             bind:this={el} | ||||
|             on:click={gotoPost} | ||||
|             on:keydown={gotoPost}> | ||||
|         <PostHeader post={post} /> | ||||
|         <Body post={post} /> | ||||
|         <footer class="post-footer"> | ||||
|             <div class="post-reactions" on:click|stopPropagation> | ||||
|             <div class="post-reactions" aria-label="Reactions" on:click|stopPropagation on:keydown|stopPropagation> | ||||
|                 {#each post.reactions as reaction} | ||||
|                     <ReactionButton | ||||
|                             type="reaction" | ||||
|  | @ -116,7 +124,7 @@ | |||
|                     </ReactionButton> | ||||
|                 {/each} | ||||
|             </div> | ||||
|             <div class="post-actions" on:click|stopPropagation> | ||||
|             <div class="post-actions" aria-label="Post actions" on:click|stopPropagation on:keydown|stopPropagation> | ||||
|                 <ActionButton type="reply" label="Reply" bind:count={post.reply_count} sound="post" disabled>🗨️</ActionButton> | ||||
|                 <ActionButton type="boost" label="Boost" on:click={() => toggleBoost()} bind:active={post.boosted} bind:count={post.boost_count} sound="boost">🔁</ActionButton> | ||||
|                 <ActionButton type="favourite" label="Favourite" on:click={() => toggleFavourite()} bind:active={post.favourited} bind:count={post.favourite_count}>⭐</ActionButton> | ||||
|  | @ -130,37 +138,39 @@ | |||
| 
 | ||||
| <style> | ||||
|     .post-container { | ||||
|         width: 700px; | ||||
|         max-width: 700px; | ||||
|         width: 732px; | ||||
|         max-width: 732px; | ||||
|         margin-bottom: 8px; | ||||
|         padding: 16px; | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|         border-radius: 8px; | ||||
|         background-color: var(--bg-800); | ||||
|     } | ||||
| 
 | ||||
|     .post { | ||||
|         padding: 16px; | ||||
|         border-radius: 8px; | ||||
|         transition: background-color .1s; | ||||
|     } | ||||
| 
 | ||||
|     .post-container:hover { | ||||
|         background-color: color-mix(in srgb, var(--bg-800), black 5%); | ||||
|     } | ||||
| 
 | ||||
|     .post-container:hover :global(.post-context) { | ||||
|         opacity: 1; | ||||
|     } | ||||
|      | ||||
|     .post:not(.focused) { | ||||
|         cursor: pointer; | ||||
|     } | ||||
| 
 | ||||
|     .post.focused { | ||||
|         padding: 16px; | ||||
|         margin: -16px; | ||||
|         border-radius: 8px; | ||||
|         border: 1px solid color-mix(in srgb, transparent, var(--accent) 20%); | ||||
|         box-shadow: 0 0 16px color-mix(in srgb, transparent, var(--accent) 20%); | ||||
|     } | ||||
| 
 | ||||
|     .post:hover { | ||||
|         background-color: color-mix(in srgb, var(--bg-800), black 5%); | ||||
|     } | ||||
| 
 | ||||
|     .post-container:has(.post-context) .post { | ||||
|         padding-top: 40px; | ||||
|         margin-top: -32px; | ||||
|     } | ||||
| 
 | ||||
|     :global(.post-reactions) { | ||||
|         width: fit-content; | ||||
|         display: flex; | ||||
|  |  | |||
|  | @ -9,12 +9,16 @@ | |||
|     import { get } from 'svelte/store'; | ||||
|     import { Client } from '../../client/client.js'; | ||||
|     import * as api from '../../client/api.js'; | ||||
|     import { goto } from '$app/navigation'; | ||||
| 
 | ||||
|     export let post; | ||||
|     let time_string = post.created_at.toLocaleString(); | ||||
|     let aria_label = post.user.username + '; ' + post.text + '; ' + post.created_at; | ||||
| 
 | ||||
|     function gotoPost() { | ||||
|         location = `/post/${post.id}`; | ||||
|         if (focused) return; | ||||
|         if (event.key && event.key !== "Enter") return; | ||||
|         goto(`/post/${post.id}`); | ||||
|     } | ||||
| 
 | ||||
|     async function toggleBoost() { | ||||
|  | @ -71,7 +75,11 @@ | |||
|     <svelte:self post={post.reply} /> | ||||
| {/if} | ||||
| 
 | ||||
| <article class="post-reply" on:click={() => gotoPost()}> | ||||
| <article | ||||
|         class="post-reply" | ||||
|         aria-label={aria_label} | ||||
|         on:click={gotoPost} | ||||
|         on:keydown={gotoPost}> | ||||
|     <div class="line"></div> | ||||
|          | ||||
|     <div class="post-reply-main"> | ||||
|  | @ -80,7 +88,7 @@ | |||
|         <Body post={post} /> | ||||
| 
 | ||||
|         <footer class="post-footer"> | ||||
|             <div class="post-reactions" on:click|stopPropagation> | ||||
|             <div class="post-reactions" aria-label="Reactions" on:click|stopPropagation on:keydown|stopPropagation> | ||||
|                 {#each post.reactions as reaction} | ||||
|                     <ReactionButton | ||||
|                             type="reaction" | ||||
|  | @ -98,7 +106,7 @@ | |||
|                     </ReactionButton> | ||||
|                 {/each} | ||||
|             </div> | ||||
|             <div class="post-actions" on:click|stopPropagation> | ||||
|             <div class="post-actions" aria-label="Post actions" on:click|stopPropagation on:keydown|stopPropagation> | ||||
|                 <ActionButton type="reply" label="Reply" bind:count={post.reply_count} sound="post" disabled>🗨️</ActionButton> | ||||
|                 <ActionButton type="boost" label="Boost" on:click={() => toggleBoost()} bind:active={post.boosted} bind:count={post.boost_count} sound="boost">🔁</ActionButton> | ||||
|                 <ActionButton type="favourite" label="Favourite" on:click={() => toggleFavourite()} bind:active={post.favourited} bind:count={post.favourite_count}>⭐</ActionButton> | ||||
|  | @ -112,11 +120,18 @@ | |||
| 
 | ||||
| <style> | ||||
|     .post-reply { | ||||
|         padding-bottom: 24px; | ||||
|         padding: 16px 16px 16px 16px; | ||||
|         display: flex; | ||||
|         flex-direction: row; | ||||
|         color: var(--text); | ||||
|         align-items: stretch; | ||||
|         border-radius: 8px; | ||||
|         transition: background-color .1s; | ||||
|         cursor: pointer; | ||||
|     } | ||||
| 
 | ||||
|     .post-reply:hover { | ||||
|         background-color: color-mix(in srgb, var(--bg-800), black 5%); | ||||
|     } | ||||
| 
 | ||||
|     .post-avatar-container { | ||||
|  | @ -125,8 +140,8 @@ | |||
| 
 | ||||
|     .line { | ||||
|         position: relative; | ||||
|         top: 24px; | ||||
|         left: 25px; | ||||
|         top: 32px; | ||||
|         left: 23px; | ||||
|         border-right: 2px solid var(--bg-700); | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -8,7 +8,8 @@ | |||
|     import { get } from 'svelte/store'; | ||||
| 
 | ||||
|     let client = get(Client.get()); | ||||
|     let ready = client.app && client.app.token; | ||||
|     let ready = client.app; | ||||
|     let logged_in = ready && client.app.token; | ||||
|     let instance_url_error = false; | ||||
|     let logging_in = false; | ||||
| 
 | ||||
|  | @ -57,31 +58,46 @@ | |||
| 
 | ||||
|     <main> | ||||
|         {#if ready} | ||||
|             <Feed /> | ||||
|         {:else} | ||||
|             <div> | ||||
|                 <form on:submit={log_in} id="login"> | ||||
|                     <h1>Space Social</h1> | ||||
|                     <p>Welcome, fediverse user!</p> | ||||
|                     <p>Please enter your instance domain to log in.</p> | ||||
|                     <div class="input-wrapper"> | ||||
|                         <input type="text" id="host" aria-label="instance domain" class={logging_in ? "throb" : ""}> | ||||
|                         {#if instance_url_error} | ||||
|                             <p class="error">{instance_url_error}</p> | ||||
|                         {/if} | ||||
|                     </div> | ||||
|                     <br> | ||||
|                     <button type="submit" id="login" class={logging_in ? "disabled" : ""}>Log in</button> | ||||
|                     <p><small> | ||||
|                         Please note this is | ||||
|                         <strong><em>extremely experimental software</em></strong>; | ||||
|                         things are likely to break! | ||||
|                         <br> | ||||
|                         If that's all cool with you, welcome aboard! | ||||
|                     </small></p> | ||||
|             {#if logged_in} | ||||
|                 <header> | ||||
|                     <h1>Home</h1> | ||||
|                     <nav> | ||||
|                         <Button centered active>Home</Button> | ||||
|                         <Button centered disabled>Local</Button> | ||||
|                         <Button centered disabled>Federated</Button> | ||||
|                     </nav> | ||||
|                 </header> | ||||
| 
 | ||||
|                     <p class="form-footer">made with ❤️ by <a href="https://arimelody.me">ari melody</a>, 2024</p> | ||||
|                 </form> | ||||
|                 <Feed /> | ||||
|             {:else} | ||||
|                 <div> | ||||
|                     <form on:submit={log_in} id="login"> | ||||
|                         <h1>Space Social</h1> | ||||
|                         <p>Welcome, fediverse user!</p> | ||||
|                         <p>Please enter your instance domain to log in.</p> | ||||
|                         <div class="input-wrapper"> | ||||
|                             <input type="text" id="host" aria-label="instance domain" class={logging_in ? "throb" : ""}> | ||||
|                             {#if instance_url_error} | ||||
|                                 <p class="error">{instance_url_error}</p> | ||||
|                             {/if} | ||||
|                         </div> | ||||
|                         <br> | ||||
|                         <button type="submit" id="login" class={logging_in ? "disabled" : ""}>Log in</button> | ||||
|                         <p><small> | ||||
|                             Please note this is | ||||
|                             <strong><em>extremely experimental software</em></strong>; | ||||
|                             things are likely to break! | ||||
|                             <br> | ||||
|                             If that's all cool with you, welcome aboard! | ||||
|                         </small></p> | ||||
| 
 | ||||
|                         <p class="form-footer">made with ❤️ by <a href="https://arimelody.me">ari melody</a>, 2024</p> | ||||
|                     </form> | ||||
|                 </div> | ||||
|             {/if} | ||||
|         {:else} | ||||
|             <div class="loading throb"> | ||||
|                 <span>just a moment...</span> | ||||
|             </div> | ||||
|         {/if} | ||||
|     </main> | ||||
|  | @ -93,31 +109,6 @@ | |||
| </div> | ||||
| 
 | ||||
| <style> | ||||
|     #spacesocial-app { | ||||
|         margin: auto 0; | ||||
|         padding: 0 16px; | ||||
|         display: flex; | ||||
|         flex-direction: row; | ||||
|         justify-content: center; | ||||
|         gap: 16px; | ||||
|     } | ||||
| 
 | ||||
|     header, #widgets { | ||||
|         width: 300px; | ||||
|     } | ||||
| 
 | ||||
|     main { | ||||
|         width: 732px; | ||||
|     } | ||||
| 
 | ||||
|     div.pane { | ||||
|         margin-top: 16px; | ||||
|         padding: 20px 32px; | ||||
|         border: 1px solid #8884; | ||||
|         border-radius: 16px; | ||||
|         background-color: var(--bg1); | ||||
|     } | ||||
| 
 | ||||
|     form#login { | ||||
|         margin: 25vh 0 32px 0; | ||||
|         text-align: center; | ||||
|  | @ -221,4 +212,32 @@ | |||
|     .form-footer { | ||||
|         opacity: .7; | ||||
|     } | ||||
| 
 | ||||
|     .loading { | ||||
|         width: 100%; | ||||
|         height: 80vh; | ||||
|         display: flex; | ||||
|         justify-content: center; | ||||
|         align-items: center; | ||||
|         font-size: 2em; | ||||
|         font-weight: bold; | ||||
|     } | ||||
| 
 | ||||
|     main header { | ||||
|         width: 100%; | ||||
|         margin: 16px 0 8px 0; | ||||
|         display: flex; | ||||
|         flex-direction: row; | ||||
|     } | ||||
| 
 | ||||
|     main header h1 { | ||||
|         font-size: 1.5em; | ||||
|     } | ||||
| 
 | ||||
|     main header nav { | ||||
|         margin-left: auto; | ||||
|         display: flex; | ||||
|         flex-direction: row; | ||||
|         gap: 8px; | ||||
|     } | ||||
| </style> | ||||
|  |  | |||
							
								
								
									
										50
									
								
								src/routes/post/[id]/+layout.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								src/routes/post/[id]/+layout.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,50 @@ | |||
| <script> | ||||
|     import Navigation from '$lib/ui/Navigation.svelte'; | ||||
|     import Widgets from '$lib/ui/Widgets.svelte'; | ||||
|     import Button from '$lib/ui/Button.svelte'; | ||||
| </script> | ||||
| 
 | ||||
| <div id="spacesocial-app"> | ||||
| 
 | ||||
|     <header> | ||||
|         <Navigation /> | ||||
|     </header> | ||||
| 
 | ||||
|     <main> | ||||
|         <header> | ||||
|             <h1>Home</h1> | ||||
|             <nav> | ||||
|                 <Button centered active>Home</Button> | ||||
|                 <Button centered disabled>Local</Button> | ||||
|                 <Button centered disabled>Federated</Button> | ||||
|             </nav> | ||||
|         </header> | ||||
| 
 | ||||
|         <slot></slot> | ||||
|     </main> | ||||
| 
 | ||||
|     <div id="widgets"> | ||||
|         <Widgets /> | ||||
|     </div> | ||||
| 
 | ||||
| </div> | ||||
| 
 | ||||
| <style> | ||||
|     main header { | ||||
|         width: 100%; | ||||
|         margin: 16px 0 8px 0; | ||||
|         display: flex; | ||||
|         flex-direction: row; | ||||
|     } | ||||
| 
 | ||||
|     main header h1 { | ||||
|         font-size: 1.5em; | ||||
|     } | ||||
| 
 | ||||
|     main header nav { | ||||
|         margin-left: auto; | ||||
|         display: flex; | ||||
|         flex-direction: row; | ||||
|         gap: 8px; | ||||
|     } | ||||
| </style> | ||||
							
								
								
									
										46
									
								
								src/routes/post/[id]/+page.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/routes/post/[id]/+page.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,46 @@ | |||
| import Post from '$lib/ui/post/Post.svelte'; | ||||
| import { Client } from '$lib/client/client.js'; | ||||
| import { parsePost } from '$lib/client/api.js'; | ||||
| import { get } from 'svelte/store'; | ||||
| 
 | ||||
| export const prerender = true; | ||||
| export const ssr = false; | ||||
| 
 | ||||
| export async function load({ params }) { | ||||
|     let client = get(Client.get()); | ||||
|     if (client.app && client.app.token) { | ||||
|         // this triggers the client actually getting the authenticated user's data.
 | ||||
|         const res = await client.verifyCredentials() | ||||
|         if (res) { | ||||
|             console.log(`Logged in as @${client.user.username}@${client.user.host}`); | ||||
|         } else { | ||||
|             return null; | ||||
|         } | ||||
|     } else { | ||||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     const post_id = params.id; | ||||
| 
 | ||||
|     const post_data = await client.getPost(post_id); | ||||
|     if (!post_data) { | ||||
|         console.error(`Failed to retrieve post ${post_id}.`); | ||||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     const post = await parsePost(post_data, 10, true); | ||||
|     let posts = [post]; | ||||
|     for (let i in post.replies) { | ||||
|         const reply = post.replies[i]; | ||||
|         // if (i > 1 && reply.reply_id === post.replies[i - 1].id) {
 | ||||
|         //     let reply_head = posts.pop();
 | ||||
|         //     reply.reply = reply_head;
 | ||||
|         // }
 | ||||
|         posts.push(reply); | ||||
|         // console.log(reply);
 | ||||
|     } | ||||
| 
 | ||||
|     return { | ||||
|         posts: posts | ||||
|     }; | ||||
| } | ||||
							
								
								
									
										38
									
								
								src/routes/post/[id]/+page.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								src/routes/post/[id]/+page.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,38 @@ | |||
| <script> | ||||
|     import '$lib/app.css'; | ||||
|     import Post from '$lib/ui/post/Post.svelte'; | ||||
| 
 | ||||
|     export let data; | ||||
|     const main_post = data.posts[0]; | ||||
|     const replies = data.posts.slice(1); | ||||
| </script> | ||||
| 
 | ||||
| <div id="feed" role="feed"> | ||||
|     {#if data.posts.length <= 0} | ||||
|         <div class="throb"> | ||||
|             <span>just a moment...</span> | ||||
|         </div> | ||||
|     {:else} | ||||
|         <Post post_data={main_post} focused /> | ||||
|         <br> | ||||
|         {#each replies as post} | ||||
|             <Post post_data={post} /> | ||||
|         {/each} | ||||
|     {/if} | ||||
| </div> | ||||
| 
 | ||||
| <style> | ||||
|     #feed { | ||||
|         margin-bottom: 20vh; | ||||
|     } | ||||
| 
 | ||||
|     .throb { | ||||
|         width: 100%; | ||||
|         height: 80vh; | ||||
|         display: flex; | ||||
|         justify-content: center; | ||||
|         align-items: center; | ||||
|         font-size: 2em; | ||||
|         font-weight: bold; | ||||
|     } | ||||
| </style> | ||||
|  | @ -1,4 +1,5 @@ | |||
| import adapter from '@sveltejs/adapter-auto'; | ||||
| // import adapter from '@sveltejs/adapter-auto';
 | ||||
| import adapter from '@sveltejs/adapter-static'; | ||||
| import * as child_process from 'node:child_process'; | ||||
| 
 | ||||
| /** @type {import('@sveltejs/kit').Config} */ | ||||
|  | @ -7,7 +8,13 @@ const config = { | |||
| 		// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
 | ||||
| 		// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
 | ||||
| 		// See https://kit.svelte.dev/docs/adapters for more information about adapters.
 | ||||
| 		adapter: adapter(), | ||||
|         adapter: adapter({ | ||||
|             pages: 'build', | ||||
|             assets: 'build', | ||||
|             fallback: undefined, | ||||
|             precompress: false, | ||||
|             strict: true, | ||||
|         }), | ||||
|         version: { | ||||
|             name: child_process.execSync('git rev-parse HEAD').toString().trim() | ||||
|         } | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue