mirror of
https://github.com/Xevion/time-banner.git
synced 2025-12-06 05:16:47 -06:00
feat: dynamic png-based clock favicon
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
use std::net::SocketAddr;
|
||||
|
||||
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 config::Configuration;
|
||||
@@ -34,6 +35,7 @@ async fn main() {
|
||||
|
||||
let app = Router::new()
|
||||
.route("/", get(index_handler))
|
||||
.route("/favicon.ico", get(favicon_handler))
|
||||
.route("/{path}", get(implicit_handler))
|
||||
.route("/rel/{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::template::OutputForm;
|
||||
use crate::utils::parse_path;
|
||||
use axum::extract::ConnectInfo;
|
||||
use axum::extract::Path;
|
||||
use axum::response::IntoResponse;
|
||||
use std::net::SocketAddr;
|
||||
|
||||
/// Root handler - redirects to current time in relative format.
|
||||
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()
|
||||
}
|
||||
|
||||
/// 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.
|
||||
pub async fn fallback_handler() -> impl IntoResponse {
|
||||
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 tera::{Context, Tera};
|
||||
use timeago::Formatter;
|
||||
@@ -36,9 +36,11 @@ pub enum OutputForm {
|
||||
Relative,
|
||||
/// Absolute display: "2025-01-17 14:30:00 UTC"
|
||||
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 {
|
||||
Abbreviation(String), // e.g. "CST"
|
||||
Iso(String), // e.g. "America/Chicago"
|
||||
@@ -58,19 +60,70 @@ pub struct RenderContext {
|
||||
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.
|
||||
///
|
||||
/// 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> {
|
||||
let mut template_context = Context::new();
|
||||
let formatter = Formatter::new();
|
||||
|
||||
template_context.insert(
|
||||
"text",
|
||||
match context.output_form {
|
||||
OutputForm::Relative => formatter.convert_chrono(context.value, Utc::now()),
|
||||
OutputForm::Absolute => context.value.to_rfc3339(),
|
||||
}
|
||||
.as_str(),
|
||||
);
|
||||
|
||||
OutputForm::Relative => {
|
||||
let formatter = Formatter::new();
|
||||
template_context.insert("text", &formatter.convert_chrono(context.value, Utc::now()));
|
||||
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