M README.md => README.md +18 -2
@@ 160,7 160,15 @@ Themes should have, roughly, the structure below:
<html lang="en"> <!-- Or really whatever you want, but I speak English. -->
<head>
<meta charset="UTF-8">
- <style> /* styles */ </style>
+ <style>
+ {{#if font.css}}
+ {{{font.css}}}
+ {{/if}}
+ {{#if (or font.name font.css)}}
+ * { font-family: '{{#if font.name}}{{font.name}}{{/if}}{{#if font.url}}included_font{{/if}}' }
+ {{/if}}
+ /* actual styles */
+ </style>
</head>
<body>
{{#if error}}<p>{{error}}</p>{{else}}
@@ 225,7 233,15 @@ If there was an error, the object will just be `{ error: String }`, otherwise it
// the most recently played track.
now_playing: Boolean,
},
-
+
+ // Custom font info.
+ font: {
+ // Set if the theme should replace the custom font with something.
+ name: String?
+ // Will contain CSS which includes a custom font as 'included_font'.
+ css: String?
+ }
+
// A set of extraneous query parameters.
//
// This should be considered UNTRUSTED, as the requester has full
M src/config.rs => src/config.rs +1 -1
@@ 62,7 62,7 @@ pub struct State {
http: Client,
default_refresh: Duration,
- whitelist_refresh: Duration
+ whitelist_refresh: Duration,
}
impl State {
M src/ctx.rs => src/ctx.rs +17 -4
@@ 2,6 2,7 @@ use reqwest::StatusCode;
use super::deserialize as de;
use std::sync::Arc;
use std::collections::BTreeMap;
+use super::font::FontQuery;
pub mod model {
use std::sync::Arc;
@@ 66,6 67,13 @@ pub mod model {
pub loved: bool
}
+ #[derive(serde::Serialize, Debug)]
+ #[serde(untagged)]
+ pub enum Font {
+ Url { css: Arc<str> },
+ Name { name: Arc<str> },
+ }
+
/// The context passed in to all themes.
///
/// Serialized as untagged, so themes should check if `error` is set and decide whether to show an error state or try rendering user info.
@@ 75,15 83,15 @@ pub mod model {
/// Contains text explaining a potential error.
Error { error: &'static str },
/// Contains data about a user and what they're listening to.
- Data { user: User, scrobble: Scrobble, query: BTreeMap<String, String> }
+ Data { user: User, scrobble: Scrobble, font: Option<Font>, query: BTreeMap<String, String>, }
}
}
#[derive(Debug)]
pub struct ResponseCtx(pub model::Data, pub StatusCode);
-impl From<(Result<Arc<(de::User, de::Track)>, (StatusCode, &'static str)>, BTreeMap<String, String>)> for ResponseCtx {
- fn from(v: (Result<Arc<(de::User, de::Track)>, (StatusCode, &'static str)>, BTreeMap<String, String>)) -> ResponseCtx {
- let (v, q) = v;
+impl From<(Result<Arc<(de::User, de::Track)>, (StatusCode, &'static str)>, Option<FontQuery>, BTreeMap<String, String>)> for ResponseCtx {
+ fn from(v: (Result<Arc<(de::User, de::Track)>, (StatusCode, &'static str)>, Option<FontQuery>, BTreeMap<String, String>)) -> ResponseCtx {
+ let (v, f, q) = v;
match v {
Ok(a) => {
let (user, track) = a.as_ref();
@@ 115,6 123,11 @@ impl From<(Result<Arc<(de::User, de::Track)>, (StatusCode, &'static str)>, BTree
url: track.url.clone(),
loved: track.loved.unwrap_or(false)
},
+ font: match f {
+ Some(FontQuery { font: Some(s), include_font: None }) => Some(model::Font::Name { name: s }),
+ Some(FontQuery { include_font: Some(s), .. }) => Some(model::Font::Url { css: format!("@font-face {{ font-family: 'included_font'; src: url('{}'); }}", s.replace("\\", "\\\\").replace("'", "\\'")).into() }),
+ _ => None,
+ },
query: q
}, StatusCode::OK)
},
A src/font.rs => src/font.rs +10 -0
@@ 0,0 1,10 @@
+use std::sync::Arc;
+
+#[derive(serde::Deserialize, Debug, Default)]
+#[serde(default)]
+#[serde(rename = "kebab-case")]
+pub struct FontQuery {
+ pub font: Option<Arc<str>>,
+ pub include_font: Option<Arc<str>>,
+// pub small_font: Option<()>
+}
M src/lib.rs => src/lib.rs +1 -0
@@ 4,6 4,7 @@ pub mod deserialize;
pub mod cache;
pub mod config;
pub mod ctx;
+pub mod font;
pub use config::STATE;
pub use ctx::ResponseCtx;
M src/main.rs => src/main.rs +5 -1
@@ 6,6 6,7 @@ use std::collections::BTreeMap;
use std::fs::File;
use std::sync::Arc;
use lfm_embed::{STATE, ResponseCtx};
+use lfm_embed::font::FontQuery;
use warp::Filter;
#[derive(serde::Deserialize, Debug)]
@@ 13,6 14,9 @@ struct UserQuery {
#[serde(default)]
theme: Option<Arc<str>>,
#[serde(flatten)]
+ #[serde(default)]
+ font: Option<FontQuery>,
+ #[serde(flatten)]
rest: BTreeMap<String, String>
}
@@ 40,7 44,7 @@ async fn main() {
.then(|s, q: UserQuery| async move {
log::debug!(target: "lfm::server::user", "Handling request for user `{s}` with {q:?}");
let (ctx, dur) = STATE.get_userinfo(&s).await;
- let ResponseCtx(mut data, status) = (ctx, q.rest).into();
+ let ResponseCtx(mut data, status) = (ctx, q.font, q.rest).into();
let theme = q.theme.filter(|a| STATE.handlebars().has_template(&a)).unwrap_or_else(|| STATE.default_theme());
log::debug!(target: "lfm::server::user", "Using theme {theme}");
M src/themes/plain.hbs => src/themes/plain.hbs +9 -2
@@ 1,5 1,5 @@
<!doctype html>
-<html lang="en" style="font-size: 0.5cm; margin: 0.5rem; overflow: hidden;">
+<html lang="en" style="margin: 20px; overflow: hidden;">
<head>
<meta charset="UTF-8"/>
<title>{{#if error}}Error!{{else}}@{{user.name}}'s Last.fm Stats{{/if}}</title>
@@ 11,6 11,13 @@
a:visited { color: pink }
a { color: cyan; }
{{/if}}
+ * { font-size: {{#if font}}17px{{else}}20px{{/if}}; }
+ {{#if font.css}}
+ {{{font.css}}}
+ {{/if}}
+ {{#if (or font.name font.css)}}
+ * { font-family: '{{#if font.name}}{{font.name}}{{/if}}{{#if font.url}}included_font{{/if}}' }
+ {{/if}}
p { margin: 0px; padding: 0px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
</style>
</head>
@@ 18,7 25,7 @@
{{#if error}}
<p style="white-space: unset;">{{error}}</p>
{{else}}
- <a style="float: left;" target="_blank" href="{{scrobble.url}}"><img src="{{scrobble.image_url}}" style="height: 4.0rem; border: solid var(--b) 0.2rem; margin: 0.1rem;" /></a>
+ <a style="float: left;" target="_blank" href="{{scrobble.url}}"><img src="{{scrobble.image_url}}" style="height: 80px; border: solid var(--b) 4px; margin: 2px;" /></a>
<p><a target="_blank" href="{{user.url}}">@{{user.name}}</a>{{#if scrobble.now_playing}}
is scrobbling{{else}}'s last scrobble was
{{/if}}