Merge pull request 'Add about section' (#6) from about into main
All checks were successful
Build & Test / build-run (push) Successful in 1m15s
Build Docker Container / publish-docker (push) Successful in 4m58s

Reviewed-on: #6
This commit is contained in:
CenTdemeern1 2025-02-13 06:11:57 +00:00
commit e527da497f
10 changed files with 231 additions and 5 deletions

3
Cargo.lock generated
View file

@ -346,7 +346,7 @@ dependencies = [
[[package]] [[package]]
name = "fedirect" name = "fedirect"
version = "0.1.0" version = "0.1.0-alpha"
dependencies = [ dependencies = [
"bytes", "bytes",
"favicon-scraper", "favicon-scraper",
@ -1527,6 +1527,7 @@ dependencies = [
"base64", "base64",
"bytes", "bytes",
"encoding_rs", "encoding_rs",
"futures-channel",
"futures-core", "futures-core",
"futures-util", "futures-util",
"h2 0.4.7", "h2 0.4.7",

View file

@ -1,12 +1,12 @@
[package] [package]
name = "fedirect" name = "fedirect"
version = "0.1.0" version = "0.1.0-alpha"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
bytes = "1.9.0" bytes = "1.9.0"
favicon-scraper = "0.3.1" favicon-scraper = "0.3.1"
reqwest = { version = "0.12.12", features = ["stream"] } reqwest = { version = "0.12.12", features = ["stream", "blocking"] }
rocket = { version = "0.5.1", features = ["json"] } rocket = { version = "0.5.1", features = ["json"] }
semver = "1.0.24" semver = "1.0.24"
serde = { version = "1.0.217", features = ["derive"] } serde = { version = "1.0.217", features = ["derive"] }

54
src/api/about.rs Normal file
View file

@ -0,0 +1,54 @@
use reqwest::blocking::Client;
use rocket::serde::json::Json;
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::sync::OnceLock;
/// Gets the FeDirect version
#[get("/about/version")]
pub async fn version() -> &'static str {
env!("CARGO_PKG_VERSION")
}
#[derive(Debug, Serialize)]
pub struct Avatars {
charlotte: Option<String>,
kio: Option<String>,
}
#[derive(Deserialize)]
struct MisskeyUser {
#[serde(rename = "avatarUrl")]
avatar_url: String,
}
fn get_avatar(client: &Client, origin: &str, user_id: &str) -> Option<String> {
let body = json!({
"userId": user_id
})
.to_string();
let user: MisskeyUser = client
.post(format!("https://{origin}/api/users/show"))
.header("content-type", "application/json")
.body(body)
.send()
.ok()?
.json()
.ok()?;
Some(user.avatar_url)
}
pub static AVATARS: OnceLock<Avatars> = OnceLock::new();
pub fn init() {
let client = Client::new();
let charlotte = get_avatar(&client, "eepy.moe", "9xt2s326nxev039h");
let kio = get_avatar(&client, "kitsunes.club", "9810gvfne3");
AVATARS.set(Avatars { charlotte, kio }).unwrap();
}
/// Gets (relatively) up-to-date Nekomata avatars
#[get("/about/nekomata_avatars")]
pub async fn nekomata_avatars() -> Json<&'static Avatars> {
Json(AVATARS.get().unwrap())
}

View file

@ -1,11 +1,18 @@
use rocket::Route; use rocket::Route;
pub mod about;
pub mod instance_info; pub mod instance_info;
pub mod proxy; pub mod proxy;
pub fn init() {
about::init();
}
pub fn get_routes() -> Vec<Route> { pub fn get_routes() -> Vec<Route> {
routes![ routes![
instance_info::instance_info, instance_info::instance_info,
about::version,
about::nekomata_avatars,
// Proxy is temporarily disabled as it's not needed // Proxy is temporarily disabled as it's not needed
// proxy::proxy // proxy::proxy
] ]

View file

@ -44,6 +44,8 @@ fn route_for_unknown_instance_software(instance: &str, route: PathBuf) -> (Conte
#[launch] #[launch]
fn rocket() -> _ { fn rocket() -> _ {
api::init();
rocket::build() rocket::build()
.mount("/static", FileServer::from("static").rank(0)) .mount("/static", FileServer::from("static").rank(0))
.mount("/api", api::get_routes()) .mount("/api", api::get_routes())

27
static/about.mts Normal file
View file

@ -0,0 +1,27 @@
import { Dialog } from "./dialog.mjs";
import { findAnchorOrFail, findButtonOrFail, findDialogOrFail, findImageOrFail, findSpanOrFail } from "./dom.mjs";
const openButton = findAnchorOrFail(document.body, "#aboutLink");
const dialog = findDialogOrFail(document.body, "#about")
const versionParagraph = findSpanOrFail(dialog, "#version");
const charlotteImage = findImageOrFail(dialog, "#charlotteAvatar");
const kioImage = findImageOrFail(dialog, "#kioAvatar");
const closeButton = findButtonOrFail(dialog, ".close");
const aboutDialog = new Dialog(dialog);
openButton.addEventListener("click", e => aboutDialog.open());
closeButton.addEventListener("click", e => aboutDialog.close());
populateVersion();
populateUsers();
async function populateVersion() {
versionParagraph.innerText = await fetch("/api/about/version").then(r => r.text());
}
async function populateUsers() {
const { charlotte, kio } = await fetch("/api/about/nekomata_avatars").then(r => r.json());
if (charlotte) charlotteImage.src = charlotte;
if (kio) kioImage.src = kio;
}

View file

@ -11,12 +11,14 @@
<body> <body>
<script type="module" src="/static/config.mjs"></script> <script type="module" src="/static/config.mjs"></script>
<script type="module" src="/static/about.mjs"></script>
<div class="flex-vcenter"> <div class="flex-vcenter">
<dialog id="mainDialog" class="half-width half-height"> <dialog id="mainDialog" class="half-width half-height">
<header class="separator-bottom margin-large-bottom"> <header class="separator-bottom margin-large-bottom">
<div class="flex-row"> <div class="flex-row">
<h1>FeDirect</h1> <h1>FeDirect</h1>
<p class="margin-auto-top">&ThickSpace;By Nekomata</p> <p class="margin-auto-top">&ThickSpace;<a id="aboutLink" href="#"
title="About FeDirect & Nekomata">By Nekomata</a></p>
</div> </div>
<img src="/static/nekomata_small.png" alt="Nekomata Logo" class="logo" /> <img src="/static/nekomata_small.png" alt="Nekomata Logo" class="logo" />
</header> </header>
@ -109,6 +111,36 @@ Unchecking this is not recommended, and this option only exists for exceptional
<button type="reset" class="close">Cancel</button> <button type="reset" class="close">Cancel</button>
</form> </form>
</dialog> </dialog>
<dialog id="about" class="half-width-max half-height">
<center>
<div class="flex-row wfit-content">
<h1>About FeDirect</h1>
<p class="margin-auto-top">&ThickSpace;(v<span id="version"></span>)</p>
</div>
<p class="margin-none-top wrap-balance">
FeDirect links the Fediverse together by allowing you to create generic links that
link people to their native instance!
</p>
<a href="https://kitsunes.dev/Nekomata/FeDirect">Source code</a>
<h2>About Nekomata</h2>
<div class="circlingMembers">
<center class="absolute-centered member charlotte align-content-center flex-column">
<img id="charlotteAvatar" class="xl-size" alt="Charlotte's avatar" />
<p class="margin-none">Charlotte</p>
<a href="https://eepy.moe/@CenTdemeern1" class="margin-none">@CenTdemeern1@eepy.moe</a>
<p class="margin-none">Programming, design</p>
</center>
<center class="absolute-centered member kio align-content-center flex-column">
<img id="kioAvatar" class="xl-size" alt="Charlotte's avatar" />
<p class="margin-none">Kio</p>
<a href="https://kitsunes.club/@Kio" class="margin-none">@Kio@kitsunes.club</a>
<p class="margin-none">Funding, hosting, design</p>
</center>
<img src="/static/nekomata_small.png" alt="Nekomata Logo" class="absolute-centered xl-size-max" />
</div>
<button class="close">Close</button>
</center>
</dialog>
<dialog id="spinner"><span class="spinner"></span></dialog> <dialog id="spinner"><span class="spinner"></span></dialog>
</body> </body>

View file

@ -11,12 +11,14 @@
<body> <body>
<script type="module" src="/static/crossroad.mjs"></script> <script type="module" src="/static/crossroad.mjs"></script>
<script type="module" src="/static/about.mjs"></script>
<div class="flex-vcenter"> <div class="flex-vcenter">
<dialog id="mainDialog" class="half-width half-height"> <dialog id="mainDialog" class="half-width half-height">
<header class="separator-bottom margin-large-bottom"> <header class="separator-bottom margin-large-bottom">
<div class="flex-row"> <div class="flex-row">
<h1>FeDirect</h1> <h1>FeDirect</h1>
<p class="margin-auto-top">&ThickSpace;By Nekomata</p> <p class="margin-auto-top">&ThickSpace;<a id="aboutLink" href="#"
title="About FeDirect & Nekomata">By Nekomata</a></p>
</div> </div>
<img src="/static/nekomata_small.png" alt="Nekomata Logo" class="logo" /> <img src="/static/nekomata_small.png" alt="Nekomata Logo" class="logo" />
</header> </header>
@ -120,6 +122,36 @@ Unchecking this is not recommended, and this option only exists for exceptional
<button type="reset" class="close">Cancel</button> <button type="reset" class="close">Cancel</button>
</form> </form>
</dialog> </dialog>
<dialog id="about" class="half-width-max half-height">
<center>
<div class="flex-row wfit-content">
<h1>About FeDirect</h1>
<p class="margin-auto-top">&ThickSpace;(v<span id="version"></span>)</p>
</div>
<p class="margin-none-top wrap-balance">
FeDirect links the Fediverse together by allowing you to create generic links that
link people to their native instance!
</p>
<a href="https://kitsunes.dev/Nekomata/FeDirect">Source code</a>
<h2>About Nekomata</h2>
<div class="circlingMembers">
<center class="absolute-centered member charlotte align-content-center flex-column">
<img id="charlotteAvatar" class="xl-size" alt="Charlotte's avatar" />
<p class="margin-none">Charlotte</p>
<a href="https://eepy.moe/@CenTdemeern1" class="margin-none">@CenTdemeern1@eepy.moe</a>
<p class="margin-none">Programming, design</p>
</center>
<center class="absolute-centered member kio align-content-center flex-column">
<img id="kioAvatar" class="xl-size" alt="Charlotte's avatar" />
<p class="margin-none">Kio</p>
<a href="https://kitsunes.club/@Kio" class="margin-none">@Kio@kitsunes.club</a>
<p class="margin-none">Funding, hosting, design</p>
</center>
<img src="/static/nekomata_small.png" alt="Nekomata Logo" class="absolute-centered xl-size-max" />
</div>
<button class="close">Close</button>
</center>
</dialog>
<dialog id="spinner"><span class="spinner"></span></dialog> <dialog id="spinner"><span class="spinner"></span></dialog>
</body> </body>

View file

@ -1,6 +1,13 @@
// I would've LOVED to use generics for this but unfortunately that's not possible. // I would've LOVED to use generics for this but unfortunately that's not possible.
// Type safety, but at what cost... >~< thanks TypeScript // Type safety, but at what cost... >~< thanks TypeScript
export function findAnchorOrFail(on: Element, selector: string): HTMLAnchorElement {
const element = on.querySelector(selector);
if (!(element instanceof HTMLAnchorElement))
throw new Error(`${selector} isn't an a`);
return element;
}
export function findDivOrFail(on: Element, selector: string): HTMLDivElement { export function findDivOrFail(on: Element, selector: string): HTMLDivElement {
const element = on.querySelector(selector); const element = on.querySelector(selector);
if (!(element instanceof HTMLDivElement)) if (!(element instanceof HTMLDivElement))

View file

@ -63,6 +63,10 @@ abbr[title] {
/* Generic styling properties */ /* Generic styling properties */
.wrap-balance {
text-wrap: balance;
}
.align-start { .align-start {
text-align: start; text-align: start;
} }
@ -139,6 +143,10 @@ abbr[title] {
min-height: 50%; min-height: 50%;
} }
.half-width-max {
max-width: 50%;
}
.full-width { .full-width {
min-width: 100%; min-width: 100%;
} }
@ -151,6 +159,16 @@ abbr[title] {
height: var(--medium); height: var(--medium);
} }
.xl-size {
width: var(--xl);
height: var(--xl);
}
.xl-size-max {
max-width: var(--xl);
max-height: var(--xl);
}
.separator-bottom { .separator-bottom {
border-bottom: solid 1px var(--transparent-black); border-bottom: solid 1px var(--transparent-black);
} }
@ -163,6 +181,10 @@ abbr[title] {
margin-top: auto; margin-top: auto;
} }
.margin-none-top {
margin-top: 0;
}
.margin-large-bottom { .margin-large-bottom {
margin-bottom: var(--large); margin-bottom: var(--large);
} }
@ -175,6 +197,13 @@ abbr[title] {
aspect-ratio: 1; aspect-ratio: 1;
} }
.absolute-centered {
position: absolute;
top: 50%;
left: 50%;
translate: -50% -50%;
}
/* Specialized elements */ /* Specialized elements */
.iconContainer { .iconContainer {
@ -206,6 +235,31 @@ abbr[title] {
margin-top: min(var(--xl), 6vh); margin-top: min(var(--xl), 6vh);
} }
.circlingMembers {
position: relative;
width: 24em;
height: 24em;
animation-play-state: running;
transition: animation-play-state 1s;
}
.circlingMembers:hover {
animation-play-state: paused;
}
.member {
animation: 8s infinite linear orbit;
animation-play-state: inherit;
}
.member.charlotte {
--orbit-translate-x: 8em;
}
.member.kio {
--orbit-translate-x: -8em;
}
/* Animations */ /* Animations */
.pulse-red { .pulse-red {
@ -221,3 +275,13 @@ abbr[title] {
box-shadow: 0px 0px 20px var(--red); box-shadow: 0px 0px 20px var(--red);
} }
} }
@keyframes orbit {
0% {
transform: rotate(0deg) translateX(var(--orbit-translate-x)) rotate(0deg);
}
100% {
transform: rotate(360deg) translateX(var(--orbit-translate-x)) rotate(-360deg);
}
}