mirror of
https://github.com/Xevion/time-banner.git
synced 2025-12-06 13:16:44 -06:00
feat: dynamic png-based clock favicon
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
|
|
||||||
use crate::routes::{
|
use crate::routes::{
|
||||||
absolute_handler, fallback_handler, implicit_handler, index_handler, relative_handler,
|
absolute_handler, fallback_handler, favicon_handler, implicit_handler, index_handler,
|
||||||
|
relative_handler,
|
||||||
};
|
};
|
||||||
use axum::{routing::get, Router};
|
use axum::{routing::get, Router};
|
||||||
use config::Configuration;
|
use config::Configuration;
|
||||||
@@ -34,6 +35,7 @@ async fn main() {
|
|||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route("/", get(index_handler))
|
.route("/", get(index_handler))
|
||||||
|
.route("/favicon.ico", get(favicon_handler))
|
||||||
.route("/{path}", get(implicit_handler))
|
.route("/{path}", get(implicit_handler))
|
||||||
.route("/rel/{path}", get(relative_handler))
|
.route("/rel/{path}", get(relative_handler))
|
||||||
.route("/relative/{path}", get(relative_handler))
|
.route("/relative/{path}", get(relative_handler))
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ use crate::error::{get_error_response, TimeBannerError};
|
|||||||
use crate::render::render_time_response;
|
use crate::render::render_time_response;
|
||||||
use crate::template::OutputForm;
|
use crate::template::OutputForm;
|
||||||
use crate::utils::parse_path;
|
use crate::utils::parse_path;
|
||||||
|
use axum::extract::ConnectInfo;
|
||||||
use axum::extract::Path;
|
use axum::extract::Path;
|
||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
|
||||||
/// Root handler - redirects to current time in relative format.
|
/// Root handler - redirects to current time in relative format.
|
||||||
pub async fn index_handler() -> impl IntoResponse {
|
pub async fn index_handler() -> impl IntoResponse {
|
||||||
@@ -49,6 +51,19 @@ pub async fn implicit_handler(Path(path): Path<String>) -> impl IntoResponse {
|
|||||||
render_time_response(time, OutputForm::Absolute, extension).into_response()
|
render_time_response(time, OutputForm::Absolute, extension).into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handles `/favicon.ico` - generates a dynamic clock favicon showing the current time.
|
||||||
|
///
|
||||||
|
/// Logs the client IP address and returns a PNG image of an analog clock.
|
||||||
|
pub async fn favicon_handler(ConnectInfo(addr): ConnectInfo<SocketAddr>) -> impl IntoResponse {
|
||||||
|
let now = chrono::Utc::now();
|
||||||
|
|
||||||
|
// Log the IP address for the favicon request
|
||||||
|
tracing::info!("Favicon request from IP: {}", addr.ip());
|
||||||
|
|
||||||
|
// Generate clock favicon showing current time
|
||||||
|
render_time_response(now, OutputForm::Clock, "png").into_response()
|
||||||
|
}
|
||||||
|
|
||||||
/// Fallback handler for unmatched routes.
|
/// Fallback handler for unmatched routes.
|
||||||
pub async fn fallback_handler() -> impl IntoResponse {
|
pub async fn fallback_handler() -> impl IntoResponse {
|
||||||
get_error_response(TimeBannerError::NotFound).into_response()
|
get_error_response(TimeBannerError::NotFound).into_response()
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Timelike, Utc};
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use tera::{Context, Tera};
|
use tera::{Context, Tera};
|
||||||
use timeago::Formatter;
|
use timeago::Formatter;
|
||||||
@@ -36,9 +36,11 @@ pub enum OutputForm {
|
|||||||
Relative,
|
Relative,
|
||||||
/// Absolute display: "2025-01-17 14:30:00 UTC"
|
/// Absolute display: "2025-01-17 14:30:00 UTC"
|
||||||
Absolute,
|
Absolute,
|
||||||
|
/// Clock display: analog clock with hands showing the time
|
||||||
|
Clock,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Timezone specification formats.
|
/// Timezone specification formats (currently unused but reserved for future features).
|
||||||
pub enum TzForm {
|
pub enum TzForm {
|
||||||
Abbreviation(String), // e.g. "CST"
|
Abbreviation(String), // e.g. "CST"
|
||||||
Iso(String), // e.g. "America/Chicago"
|
Iso(String), // e.g. "America/Chicago"
|
||||||
@@ -58,19 +60,70 @@ pub struct RenderContext {
|
|||||||
pub now: Option<i64>,
|
pub now: Option<i64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Calculates clock hand positions for a given time.
|
||||||
|
///
|
||||||
|
/// Returns (hour_x, hour_y, minute_x, minute_y) coordinates for SVG rendering.
|
||||||
|
/// Clock center is at (16, 16) with appropriate hand lengths for a 32x32 favicon.
|
||||||
|
fn calculate_clock_hands(time: DateTime<Utc>) -> (f64, f64, f64, f64) {
|
||||||
|
let hour = time.hour() as f64;
|
||||||
|
let minute = time.minute() as f64;
|
||||||
|
|
||||||
|
// Calculate angles (12 o'clock = 0°, clockwise)
|
||||||
|
let hour_angle = ((hour % 12.0) + minute / 60.0) * 30.0; // 30° per hour
|
||||||
|
let minute_angle = minute * 6.0; // 6° per minute
|
||||||
|
|
||||||
|
// Convert to radians and adjust for SVG coordinate system (0° at top)
|
||||||
|
let hour_rad = (hour_angle - 90.0).to_radians();
|
||||||
|
let minute_rad = (minute_angle - 90.0).to_radians();
|
||||||
|
|
||||||
|
// Clock center and hand lengths
|
||||||
|
let center_x = 16.0;
|
||||||
|
let center_y = 16.0;
|
||||||
|
let hour_length = 7.0; // Shorter hour hand
|
||||||
|
let minute_length = 11.0; // Longer minute hand
|
||||||
|
|
||||||
|
// Calculate end positions
|
||||||
|
let hour_x = center_x + hour_length * hour_rad.cos();
|
||||||
|
let hour_y = center_y + hour_length * hour_rad.sin();
|
||||||
|
let minute_x = center_x + minute_length * minute_rad.cos();
|
||||||
|
let minute_y = center_y + minute_length * minute_rad.sin();
|
||||||
|
|
||||||
|
(hour_x, hour_y, minute_x, minute_y)
|
||||||
|
}
|
||||||
|
|
||||||
/// Renders a time value using the appropriate template.
|
/// Renders a time value using the appropriate template.
|
||||||
|
///
|
||||||
|
/// Uses different templates based on output form:
|
||||||
|
/// - Relative/Absolute: "basic.svg" with text content
|
||||||
|
/// - Clock: "clock.svg" with calculated hand positions
|
||||||
pub fn render_template(context: RenderContext) -> Result<String, tera::Error> {
|
pub fn render_template(context: RenderContext) -> Result<String, tera::Error> {
|
||||||
let mut template_context = Context::new();
|
let mut template_context = Context::new();
|
||||||
let formatter = Formatter::new();
|
|
||||||
|
|
||||||
template_context.insert(
|
|
||||||
"text",
|
|
||||||
match context.output_form {
|
match context.output_form {
|
||||||
OutputForm::Relative => formatter.convert_chrono(context.value, Utc::now()),
|
OutputForm::Relative => {
|
||||||
OutputForm::Absolute => context.value.to_rfc3339(),
|
let formatter = Formatter::new();
|
||||||
}
|
template_context.insert("text", &formatter.convert_chrono(context.value, Utc::now()));
|
||||||
.as_str(),
|
|
||||||
);
|
|
||||||
|
|
||||||
TEMPLATES.render("basic.svg", &template_context)
|
TEMPLATES.render("basic.svg", &template_context)
|
||||||
}
|
}
|
||||||
|
OutputForm::Absolute => {
|
||||||
|
template_context.insert("text", &context.value.to_rfc3339());
|
||||||
|
TEMPLATES.render("basic.svg", &template_context)
|
||||||
|
}
|
||||||
|
OutputForm::Clock => {
|
||||||
|
let (hour_x, hour_y, minute_x, minute_y) = calculate_clock_hands(context.value);
|
||||||
|
|
||||||
|
// Format to 2 decimal places to avoid precision issues
|
||||||
|
let hour_x_str = format!("{:.2}", hour_x);
|
||||||
|
let hour_y_str = format!("{:.2}", hour_y);
|
||||||
|
let minute_x_str = format!("{:.2}", minute_x);
|
||||||
|
let minute_y_str = format!("{:.2}", minute_y);
|
||||||
|
|
||||||
|
template_context.insert("hour_x", &hour_x_str);
|
||||||
|
template_context.insert("hour_y", &hour_y_str);
|
||||||
|
template_context.insert("minute_x", &minute_x_str);
|
||||||
|
template_context.insert("minute_y", &minute_y_str);
|
||||||
|
|
||||||
|
TEMPLATES.render("clock.svg", &template_context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
37
src/templates/clock.svg
Normal file
37
src/templates/clock.svg
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<!-- Clock face -->
|
||||||
|
<circle cx="16" cy="16" r="15" fill="#ffffff" stroke="#000000" stroke-width="1" />
|
||||||
|
|
||||||
|
<!-- Hour markers -->
|
||||||
|
<g stroke="#000000" stroke-width="1">
|
||||||
|
<!-- 12 o'clock -->
|
||||||
|
<line x1="16" y1="2" x2="16" y2="5" />
|
||||||
|
<!-- 3 o'clock -->
|
||||||
|
<line x1="30" y1="16" x2="27" y2="16" />
|
||||||
|
<!-- 6 o'clock -->
|
||||||
|
<line x1="16" y1="30" x2="16" y2="27" />
|
||||||
|
<!-- 9 o'clock -->
|
||||||
|
<line x1="2" y1="16" x2="5" y2="16" />
|
||||||
|
|
||||||
|
<!-- Minor markers -->
|
||||||
|
<line x1="24.5" y1="4.5" x2="23.5" y2="5.5" />
|
||||||
|
<line x1="27.5" y1="7.5" x2="26.5" y2="8.5" />
|
||||||
|
<line x1="27.5" y1="24.5" x2="26.5" y2="23.5" />
|
||||||
|
<line x1="24.5" y1="27.5" x2="23.5" y2="26.5" />
|
||||||
|
<line x1="7.5" y1="27.5" x2="8.5" y2="26.5" />
|
||||||
|
<line x1="4.5" y1="24.5" x2="5.5" y2="23.5" />
|
||||||
|
<line x1="4.5" y1="7.5" x2="5.5" y2="8.5" />
|
||||||
|
<line x1="7.5" y1="4.5" x2="8.5" y2="5.5" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Hour hand -->
|
||||||
|
<line x1="16" y1="16" x2="{{ hour_x }}" y2="{{ hour_y }}" stroke="#000000" stroke-width="2"
|
||||||
|
stroke-linecap="round" />
|
||||||
|
|
||||||
|
<!-- Minute hand -->
|
||||||
|
<line x1="16" y1="16" x2="{{ minute_x }}" y2="{{ minute_y }}" stroke="#000000"
|
||||||
|
stroke-width="1.5" stroke-linecap="round" />
|
||||||
|
|
||||||
|
<!-- Center dot -->
|
||||||
|
<circle cx="16" cy="16" r="1.5" fill="#000000" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
Reference in New Issue
Block a user